提交 --amend 和重置 --soft 的区别是什么?

3

我的问题的思路是研究...
commit --amendreset --soft 之间的本质区别是什么。

研究过程步骤如下:

vim index.js > edit > save
git add index.js
git commit -m '...'
git push origin

现在我需要重写之前发送的提交。为此,我应该使用:
vim index.js > edit > save
git add index.js
git commit --amend --no-edit
git push --force origin

基本上,我有另一个SHA1值,为了类比,在.git/objects目录中有三个对象,但git log只显示了两个SHA1值,我完全同意这一点,因为提交已被修改。
让我们回到之前的情况。我没有执行git commit --amend,而是执行了git reset --soft HEAD~。HEAD指针代表文件的stage版本,写入一些代码并执行:
git add index.js > git commit -m '...' > git push --force origin

.git/objects目录包含一个以上的SHA1值,但历史记录已经使用新的SHA1进行修改。

所以我想说的是命令git commit --amendgit reset --soft相同的行为

我的理解正确吗?


1
在官方的git书籍中有这样一句话:“如果你已经推送了最后一次提交,请不要修改它。” - undefined
git commit --amendgit reset --soft是两个完全不同的操作。然而,先执行git reset --soft,然后再执行git commit会产生与git commit --amend类似的结果。 - undefined
1
@LeszekMazur,先生,您绝对应该在引用中添加“如果您的项目有合作者”的参考...否则,在推送之后,您仍然可以调用--amend - undefined
2个回答

7
两个命令都可以用于实现类似的最终目标,但它们的作用是不同的。git commit --amend 命令使用暂存区中的内容创建一个新提交,这个新提交替换了先前分支HEAD所在的提交。
另一方面,git reset --soft 命令将分支的HEAD指针向后移动一个提交,但保留了暂存区和工作目录中的内容。这样,你的分支现在看起来好像在之前的提交上,但你做的所有工作都出现在已暂存的更改中。如果你现在在工作目录中更改东西,将这些更改暂存并提交,那么你也会创建一个新提交来替换旧的HEAD,这与git commit --amend产生了类似的结果。 git reset --soft (等同于 git reset --soft HEAD~1)相对于git commit --amend有一个优势,那就是前者可以跨越多个提交进行重置。如果你想重写分支上的最近4个提交,这将非常有用。另一方面,git commit --amend 只能为HEAD提交执行操作。

git commit --amend的一个好处是你不会丢失提交信息,也不需要再次输入。同时,它还能方便地查看修正差异(git commit,编辑x,git diffgit commit --amend x等)。 - undefined
1
对于提交消息,git commit 命令有 -c <commit-ish>(可编辑)和 -C <commit-ish>(不可编辑)选项,用于重用 commit-ish 的提交消息。 - undefined
2
@ElpieKay: 我经常使用 git commit --amend --no-edit 命令来将一个快速的一行修复添加到提交中,当我在 git show 输出中注意到一个拼写错误时,以保留之前的提交信息。 - undefined

1
“commit --amend”和“reset --soft”之间的本质区别是什么?
一个是提交(commit),另一个是重置(reset)?这是一种有意的幽默回答,但实际上它也是正确的。它们只是不同的操作。
要正确理解所有这些,您需要了解Git的索引(index),Git如何实际进行提交,以及分支名称(branch names)的工作方式。从清晰定义提交(commit)的含义和作用开始会有所帮助。
Git的全部内容都围绕提交(commits);每个分支名称只指向一个提交(commit)。
新手往往认为Git是关于文件或分支的,但它绝对不是关于文件的(尽管它确实存储它们),而且单词“branch”是模糊的,Git并不是关于大多数Git新手使用的含义。Git实际上完全是关于提交(commits)的。提交(commit)是Git中基本的面向用户的存储单位。
每个提交都存储一个快照——所有文件的完整副本,以及一些元数据,提供有关提交的信息:例如,谁在什么时候进行了提交。每个提交都获得一个唯一的哈希ID,永久保留以表示该提交。无论在任何地方,每个Git仓库都同意该哈希ID表示该提交,仓库要么拥有该提交并因此拥有该哈希ID,要么没有,就没有。一旦创建,任何提交——实际上是任何内部Git对象——都不能再改变。因此,快照永远被冻结在时间中。
每个提交的元数据中都可以包含一些早期现有提交的哈希ID。这些是提交的父提交。大多数提交只存储一个父哈希ID。从子向父的这些链接形成链。
... <-F <-G <-H ...

如果我们在提交H,我们可以读取它的父哈希IDG。这使得Git可以查找提交GG包含其父级的哈希IDF。这使得Git可以查找提交F,其中包含其父哈希ID。通过重复此过程,Git可以从最后一个提交一直工作到第一个提交。2 这意味着我们只需要记住最后的提交。在这种情况下可能会有多个“最后的提交”。
...--F--G--H   <-- master
         \
          I--J   <-- develop

请注意,Hmaster上的最后一次提交,而Jdevelop上的最后一次提交。 分支名称选择这些tip提交。从这里开始,Git可以向后工作。请注意,提交G两个分支上;如果我们将H单独放在一行上,则可能更清晰:
          H   <-- master
         /
...--F--G
         \
          I--J   <-- develop

(这两个图表示同一个代码库)。

当存在像这样多个分支时,我们需要一种方法来知道用 git checkout branch 或新近出现的git switch branch 命令所切换到的是哪一个分支。为了跟踪,我们可以在其中一个分支名称后附上特殊的名称为 HEAD 的标识,以此来绘制图示:

...--F--G--H   <-- master (HEAD)
         \
          I--J   <-- develop

这个图表示我们在分支master上,并检出了提交H
...--F--G--H   <-- master
         \
          I--J   <-- develop (HEAD)

这个图表示我们正在分支develop上,并且检出了提交J

1提交可以分解为组成部分——树对象、blob对象和相互引用树对象的底层提交对象,但这个层次不是用户使用Git的地方。

2一些提交——Git称之为合并提交——包含两个或多个哈希ID。从这样的提交开始,Git向后工作到两个(或所有)父级,引入历史上的一个分叉点。请注意,合并将事物汇集在一起,但由于Git向后工作,它作为一个分歧点。当分支分叉时,Git向后工作,将它们带回一起。

每个存储库中至少有一个提交没有父级,因为它是第一个提交,无法指向后面。没有父级的提交是根提交。


我们从索引中创建新的提交

如上所述,提交是永久冻结的:一旦我们进行了提交,就再也不能更改它。任何提交的任何部分都永远不会更改。3这包括每个提交中存储的所有文件,在快照中。它们不仅被冻结,而且还以一种特殊的只读、Git专用、压缩格式存储:您计算机上的其他程序甚至无法读取这些文件。

这意味着 Git 必须将文件从提交中提取到某个地方,以便它们变得有用。那个地方就是您的工作区,Git 称之为您的“工作树”或“工作区”。在这里,您的文件具有日常形式:它们没有冻结,也没有压缩;每个计算机程序都可以使用它们。您可以随心所欲地使用您的工作树:毕竟,它是您的

Git可以从工作目录创建新的提交。其他版本控制系统也可以这样做。但是Git不是这样的。相反,在当前提交(你检出的分支上的提交,Git使用特殊名称HEAD找到它)和工作目录之间,Git将所有文件存储在一个特殊的区域中,Git称之为索引、暂存区或者很少用的缓存。这三个名称都指的是同一件事情。索引或暂存区(我在这里称为“索引”)最初至少保存了所有已提交文件的副本。它们以冻结格式存在,就像提交中一样,但与提交不同,它们实际上并没有被冻结:你可以覆盖它们。

因此,每个文件都有三个活动版本:当前提交中冻结的HEAD副本;您可以随时替换的索引副本;以及您可以查看和使用的工作树副本。您可以编辑工作树副本,然后运行git add file。您必须一直运行git add,原因现在很清楚:每次运行git add都会从工作树(它具有计算机日常使用的形式)复制文件到索引/暂存区域中,其中它是Git喜欢的冻结形式。

现在我们可以看到git commit的作用以及为什么它相对较快。5所有git commit需要做的就是将已经存在于索引中的内容以正确的格式打包成一个新的提交。首先,它必须收集日志消息,并添加您的名称、当前日期时间和所有这些类型的内容;然后它必须在提交的元数据中设置新提交的父哈希 ID。然后,它可以使用来自索引的预冻结文件进行提交。
新提交的父级是当前提交(除了像我们所看到的那样的--amend)。新提交(例如K)被写入所有提交的集合中,并指回当前提交。
...--F--G--H   <-- master
         \
          I--J   <-- develop (HEAD)
              \
               K

现在,神奇的部分发生了:Git将新提交的哈希值写入与“HEAD”附加的名称。在这种情况下,是“develop”,所以现在我们有:
...--F--G--H   <-- master
         \
          I--J--K   <-- develop (HEAD)

并且K是在develop分支上的最新提交。


请注意,即使使用git commit --amend命令也无法更改提交!接下来我们将了解它的作用,但是这里有一个提示。如果您删除了一个提交,进行了更改,并使用该更改创建新的提交,则会得到一个具有不同哈希ID的不同提交。这与您更改了什么无关(除了不同的更改会导致不同的哈希ID):在Git中,任何不同的提交都将具有不同的哈希ID。只有当您保留每个位的相同内容-相同的快照,相同的作者,相同的日志消息和相同的日期时间戳时,才能获得原始哈希ID。但是那样你并没有创建一个新的提交:你又一次创建了旧的提交,它具有相同的父级,相同的快照,相同的日志消息甚至是相同的时间戳。昨天您创建了原始提交,今天您又创建了一个新的提交-它是相同的提交!

4从技术上讲,索引保存对冻结副本的引用:它仅保存一个 blob 哈希 ID,加上文件名,再加上一些有关工作树的缓存信息(因此被称为“cache”)。

如果您开始使用 git ls-files --stagegit update-index 等命令来查看或更改索引中的内容,就会发现这种差异。但除了这些情况,您可以将索引视为每个文件的副本。

5如果您曾经使用过其他早期的版本控制系统,您可能还记得如何输入“提交”或“检出”类型的命令并外出用餐,因为需要花费许多秒或分钟的时间来完成操作。如今,有些人认为 Git 很慢:他们不知道什么是真正的“慢”。


git commit --amend的作用

git commit --amend实际上只是:

  • 几乎像平常一样编写新的提交,除了
  • 不使用当前提交作为新提交的父提交,而使用当前提交的父提交作为新提交的父提交。

此外,默认情况下,它允许我们在创建新提交时编辑当前提交的提交消息。

假设我们有:

...--F--G--H   <-- master
         \
          I--J--K   <-- develop (HEAD)

当你意识到自己忘记了git add文件,或者想要修复提交消息时。如果需要,可以进行未完成的git add,然后运行:

git commit --amend

Git会收集提交信息,但这次它会在一个文件上打开编辑器,该文件保存了提交K的提交信息。如果需要,您可以进行编辑,写出并退出编辑器,git commit将创建新的提交 - 但是它不会将其父级设置为K,而是将其父级设置为K的父级,也就是J。这将创建一个新的提交,我们可以称之为LK';让我们使用K'。作为最后一步,git commitK'的哈希ID写入当前分支名称:
...--F--G--H   <-- master
         \
          I--J--K'  <-- develop (HEAD)
              \
               K   [abandoned]

请注意,提交K仍然存在于存储库中。只是没有分支名称可以通过它来找到提交K6。现在,develop名称指向提交K'
因此,git commit --amend似乎更改了提交记录。但它实际上只是将提交记录推到一边,并在其位置上放置一个新的和改进的(好吧,可能)替代品。
我们可以在 reflogs 中找到 K 的哈希 ID:目前,develop 的 reflog 在 develop@{1} 处有它,HEAD 的 reflog 在 HEAD@{1} 处有它。然而,大多数 Git 命令都不会查看 reflogs,并且 reflogs 是可选的。reflog 条目最终会过期,一旦它们消失了,提交 K 就会从 Git 的 Grim Collector(git gc)中变成未受保护的,git gc 会回收被遗弃和未受保护的提交和其他丢失的 Git 对象。
最终的意义是通常情况下,你至少可以在 30 天内找回丢失的提交,这是默认的最小 reflog 条目保留时间。通常情况下,git gc 处理所有这些事情,包括过期的旧 reflog 条目,Git 会自动偶尔运行 git gc,如果 Git 认为需要的话。

git reset 移动分支名称,可选择重置索引

git reset 命令比 git commit --amend 命令复杂得多,主要是因为太多的不同操作都被塞进了一个 git reset 命令中。如果我们忽略其中大部分操作,而集中关注 git reset 最基本的操作模式,那么 git reset 所做的就是最多三件事情:

  1. 首先,它移动了当前分支名称。您选择一个提交——存储库中的任何提交,在图形中的任何位置——并告诉git reset您想要当前分支名称(即HEAD所附加的名称)指向提交。git reset使此操作发生。

  2. 然后,如果您使用了--soft选项,git reset将停止。否则,它会加载刚刚移动到的提交中的索引

  3. 接下来,如果您使用了--mixed选项,或者没有使用这些选项之一,git reset将停止。否则,它会使您的工作目录与对索引所做的更新匹配。

因此,如果我们查看这个图形:

...--F--G--H   <-- master
         \
          I--J--K   <-- develop (HEAD)

执行git reset --soft HEAD~1命令后,我们选择的提交是JHEAD~1表示找到HEAD所选的提交(即K并向后移动一步,这将落在J处。 因此,git reset的第一步意味着develop移动到J,得到以下结果:

...--F--G--H   <-- master
         \
          I--J   <-- develop (HEAD)
              \
               K   [abandoned]

请注意,这看起来与我们从git commit --amend得到的非常相似,只是这里没有提交K'
我们告诉git reset使用--soft重置,在第2步将重置索引,它会退出。索引保持不变。我们的工作区也保持不变。如果索引在一刻钟前与提交K匹配 - 这很可能是这样的 - 那么它仍然与提交K匹配。如果我们的工作区匹配提交K,则仍然匹配。(如果没有,则现在工作区并不重要。)
如果我们现在运行git commit,Git将像往常一样收集日志消息,打包索引中的任何内容 - 这很可能仍然与K匹配 - 并创建一个新的提交。让我们称之为提交K',并进行绘制:
...--F--G--H   <-- master
         \
          I--J--K'  <-- develop (HEAD)
              \
               K   [abandoned]

最终,我们得到的东西与使用 git commit --amend 得到的一样: 一个新的提交 K' (具有其自己的哈希 ID),其父提交是 J,内容是索引中的内容。

所以 --amend 和 reset-and-commit 是相同的...除非它们不是

--amend 版本更简单: 我们只运行一个命令。 它还允许我们修改一个合并提交。 比如说,我们有这个:

          I--J
         /    \
...--G--H      M   <-- branch (HEAD)
         \    /
          K--L

我们可以使用 git commit --amend 命令来替换掉提交记录 M 并创建一个新的提交记录 M',利用索引的内容(很可能与 M 的快照相同)和一个新的日志信息。这时,我们得到了一个有两个父级的合并提交记录 M',父级分别为 JL。如果没有使用 --amend,要创建一个合并提交记录会比较困难7,而使用 git reset --soft 再进行一次提交也无法实现。
不过,同样地,git commit --amend 只会回溯到前一个提交记录。通过使用 git reset --soft,我们可以将一系列提交记录“消失”。例如,假设我们有以下提交记录:
...--o--*--o   <-- master
         \
          A--B--C--D--E--F--G--H--I   <-- feature (HEAD)

整个长链从AI都是一堆实验。现在该功能已经正常工作,您希望有一个提交AI来完成所有操作。

有多种方法可以实现此目的,8但如果您刚刚提交了I,使您的索引和工作树与提交I匹配,那么您现在可以使用git reset --soft HEAD~9重置名称为feature,使其指向提交*。然后,您可以使用当前索引(从I中获取的快照)进行git commit,以创建新的提交AI

          AI   <-- feature (HEAD)
         /
...--o--*--o   <-- master
         \
          A--B--C--D--E--F--G--H--I   [abandoned]

您的存储库中仍保留着提交记录AI,只能通过featureHEAD reflogs找到,如果需要它们,请在30天内重新获取;但是现在git log master..feature仅显示一个提交记录AIAI中的快照与I中的快照相匹配,但看起来您已经将所有内容都合并为一个惊人的提交记录。


7Git作为工具集,有几种方法可以使新提交“成为”合并。最直接的方法是降至git commit本身的级别,进入组成一个新提交的部件,但您还可以创建一个包含哈希ID的.git/MERGE_HEAD文件。尽管如此,这些都不是为日常轻松使用而设计的。

8通常的方法是使用git merge --squash,它允许您使AI跟在master的末尾,但通常会使用一个新的分支名称:

             AI   <-- completed-feature (HEAD)
            /
...--o--*--o   <-- master
         \
          A--B--C--D--E--F--G--H--I   <-- feature

因为分支名称并不重要,重要的是提交哈希 ID;分支名称只是为您记录它们,所以可以不使用第二个分支名称来完成所有这些操作。但通常不明智地放弃提交并在 reflogs 中寻找它,这样很容易搞砸事情。如果你开始查看你的 reflogs,你通常会发现许多 扭曲的小通道,都很相似,而找到正确的那个可能非常棘手。


这肯定是你之前发布过的内容的重复。等一下...让我检查一下。 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接