“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
,我们可以读取它的父哈希ID
G
。这使得Git可以查找提交
G
;
G
包含其父级的哈希ID
F
。这使得Git可以查找提交
F
,其中包含其父哈希ID。通过重复此过程,Git可以从
最后一个提交一直工作到
第一个提交。
2
这意味着我们只需要记住
最后的提交。在这种情况下可能会有多个“最后的提交”。
...--F--G--H <-- master
\
I--J <-- develop
请注意,
H
是
master
上的最后一次提交,而
J
是
develop
上的最后一次提交。
分支名称选择这些
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
。
...
\
I
这个图表示我们正在分支
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”,所以现在我们有:
...
\
I
并且K
是在develop
分支上的最新提交。
请注意,即使使用
git commit --amend
命令也无法更改提交!接下来我们将了解它的作用,但是这里有一个提示。如果您删除了一个提交,进行了更改,并使用该更改创建新的提交,则会得到一个具有不同哈希ID的
不同提交。这与您更改了
什么无关(除了不同的更改会导致不同的哈希ID):在Git中,任何
不同的提交都将具有不同的哈希ID。只有当您保留每个位的相同内容-相同的快照,相同的作者,相同的日志消息和相同的日期时间戳时,才能获得原始哈希ID。但是那样你并没有创建一个
新的提交:你又一次创建了
旧的提交,它具有相同的父级,相同的快照,相同的日志消息甚至是相同的时间戳。昨天您创建了原始提交,今天您又创建了一个新的提交-它是相同的提交!
4从技术上讲,索引保存对冻结副本的引用:它仅保存一个 blob 哈希 ID,加上文件名,再加上一些有关工作树的缓存信息(因此被称为“cache”)。
如果您开始使用 git ls-files --stage
和 git update-index
等命令来查看或更改索引中的内容,就会发现这种差异。但除了这些情况,您可以将索引视为每个文件的副本。
5如果您曾经使用过其他早期的版本控制系统,您可能还记得如何输入“提交”或“检出”类型的命令并外出用餐,因为需要花费许多秒或分钟的时间来完成操作。如今,有些人认为 Git 很慢:他们不知道什么是真正的“慢”。
git commit --amend的作用
git commit --amend
实际上只是:
- 几乎像平常一样编写新的提交,除了
- 不使用当前提交作为新提交的父提交,而使用当前提交的父提交作为新提交的父提交。
此外,默认情况下,它允许我们在创建新提交时编辑当前提交的提交消息。
假设我们有:
...
\
I
当你意识到自己忘记了git add
文件,或者想要修复提交消息时。如果需要,可以进行未完成的git add
,然后运行:
git commit --amend
Git会收集提交信息,但这次它会在一个文件上打开编辑器,该文件保存了提交
K
的提交信息。如果需要,您可以进行编辑,写出并退出编辑器,
git commit
将创建新的提交 - 但是它不会将其父级设置为
K
,而是将其父级设置为
K
的父级,也就是
J
。这将创建一个新的提交,我们可以称之为
L
或
K'
;让我们使用
K'
。作为最后一步,
git commit
将
K'
的哈希ID写入当前分支名称:
...
\
I
\
K [abandoned]
请注意,提交
K
仍然存在于存储库中。只是没有
分支名称可以通过它来
找到提交
K
6。现在,
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
所做的就是最多三件事情:
首先,它移动了当前分支名称。您选择一个提交——存储库中的任何提交,在图形中的任何位置——并告诉git reset
您想要当前分支名称(即HEAD
所附加的名称)指向该提交。git reset
使此操作发生。
然后,如果您使用了--soft
选项,git reset
将停止。否则,它会加载刚刚移动到的提交中的索引。
接下来,如果您使用了--mixed
选项,或者没有使用这些选项之一,git reset
将停止。否则,它会使您的工作目录与对索引所做的更新匹配。
因此,如果我们查看这个图形:
...
\
I
执行git reset --soft HEAD~1
命令后,我们选择的提交是J
:HEAD~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'
,并进行绘制:
...
\
I
\
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'
,父级分别为
J
和
L
。如果没有使用
--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)
整个长链从A
到I
都是一堆实验。现在该功能已经正常工作,您希望有一个提交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]
您的存储库中仍保留着提交记录A
到I
,只能通过feature
和HEAD
reflogs找到,如果需要它们,请在30天内重新获取;但是现在git log master..feature
仅显示一个提交记录AI
。 AI
中的快照与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,你通常会发现许多 扭曲的小通道,都很相似,而找到正确的那个可能非常棘手。
git commit --amend
和git reset --soft
是两个完全不同的操作。然而,先执行git reset --soft
,然后再执行git commit
会产生与git commit --amend
类似的结果。 - undefined--amend
。 - undefined