最新提交应该是逐步包含之前所有更改的提交,对吗?
如果是这样,那么-squash有什么用,只是将所有提交压缩成一个以清理历史记录?
squash实际上做的是删除除最新提交外的所有提交?
对于cherry-picking也是如此。如果最新提交包含之前所有更改,那么为什么需要选择旧的提交?
最新提交应该是逐步包含之前所有更改的提交,对吗?
如果是这样,那么-squash有什么用,只是将所有提交压缩成一个以清理历史记录?
squash实际上做的是删除除最新提交外的所有提交?
对于cherry-picking也是如此。如果最新提交包含之前所有更改,那么为什么需要选择旧的提交?
我不确定你是否有误解。很多老的源代码管理系统将每个提交(或称为check-in等)存储为“更改”。Git并不是这样:每个提交都具有完整的文件,作为类型为“blob”的仓库对象存储。1
“blob”对象位于底层,由“tree”对象指向(它们保存文件/目录的名称和可执行位),而树则由“commit”对象指向。(还有一个最后一种仓库级别的对象类型,“注释标记”,通常指向一个提交。)因此,给定提交SHA-1和仓库路径(例如dir/file),git首先提取提交对象,然后导航到需要拥有一个名为“dir”的条目的树。该条目需要导向另一个树,该树必须有一个名为“file”的条目,然后该条目应该是一个blob,那就是该提交中出现的dir/file的版本。
分支和标签名称(如master)只是人类可读的单词,给出底层仓库对象的“真实名称”SHA-1。提交对象在其中具有父SHA-1值,允许git动态提取提交图。
当然,您仍然可以从git中获取change-set。它只是每次都会动态计算。
假设我们有这个提交的图形,其中我使用一个大写字母来表示每个40个字符的SHA-1,以及一些分支名称:
A - B - C - D <-- master
\
E - F <-- branch
master
指向提交记录D
(实际上可能是dcfaa9d9767a010c143ffd42b01b84d2abb4cffc
)。该提交记录有一个父提交C
(实际上是222c4dd…
)。C
有一个父提交B
,B
有一个父提交A
,而A
根本没有父提交-它是一个根提交。D
开始并向后通过任何父项来到达提交F
;只能通过其分支名称branch
到达。 F
的父提交是E
,E
(唯一)父提交是B
,因此,从F
开始,我们可以通过B
向后工作到A
。branch
合并到master
”-让我们暂时这样做,然后再撤消:$ git checkout master
Switched to branch 'master'
$ git merge -m regular-merge branch
[snip]
我们创建一个新的合并提交M
:
A - B - C - D - M <-- master
\ /
E - F <-- branch
这个(真实的、非“压扁”)合并有两个父提交,D
和F
。(重要的是,D
是“第一个”父提交:这告诉你哪个提交最初是“在 master
上”的,而不是来自于 branch
。)因此,现在通过 branch
可以到达的两个提交(从 F
开始向后遍历),也可以在 master
上找到。
那么各种文件内容怎么处理呢?这取决于执行合并的人。如果使用 Git 的自动合并,在没有冲突变更的情况下,“将 branch
合并到 master
” 将得到预期结果。但是,你可以使用 -s ours
进行合并,以丢弃 E
和 F
中的变更内容。仍然会得到一个合并提交 M
,但其树与提交 D
中的树相同。2
随时可以要求 Git 生成从一个提交到另一个提交的变更集。所以,如果想查看 B
和 E
之间的差异,可以找到它们各自的 SHA-1 并执行:
git diff <sha1-for-B> <sha1-for-E>
要查看 E
和 F
之间发生了什么变化,您只需使用它们各自的 SHA-1 值即可;要查看“分支 branch
上发生了什么事”,请使用 B
和 F
的 SHA-1 值。
作为一种非常方便的便利,要查看某个提交与其父提交之间发生了什么 —— 在这里我们不用担心合并,因为它们有多个父节点 —— 我们可以直接使用例如:
git show <sha1-for-F>
git show
命令将(除其他功能外)查找 F
的父级,并在 E
和 F
之间运行差异,以显示更改内容。git show branch
通常情况下,如果原始的SHA-1可用,则分支名称也可以使用(但反之不一定成立)。
自然地,由于SHA-1是笨重的,因此有更多的命名方式;但现在让我们忽略这些细节,最后来看一下“压缩合并”和“挑选提交”。
让我们“撤消”对master
的合并操作,使得master
和branch
再次分离:3
$ git status # just checking! "git reset --hard" could lose work
# On branch master
nothing to commit, working directory clean
$ git reset --hard master^
[snip]
我们现在回到这个提交图:
A - B - C - D <-- master
\
E - F <-- branch
假设我们有一个解决了一个讨厌的bug的变更F
,并且我们想要在master
分支中复制它。我们尝试使用git merge branch
将其合并,但是这也带来了还没有准备好的变更E
。
因此,现在我们只需要“挑选”提交F
:
$ git cherry-pick branch
[snip]
通常情况下,我们可以使用分支名称来确定该分支尖端的(单个)提交。
这告诉git收集E
和F
之间的更改,就像git show
一样。但是,它不会向我们显示这些更改,而是尝试将这些更改打补丁到当前(HEAD
)提交中。由于我们在master分支上,HEAD
提交是提交D
。所以这提取了从E
到F
的更改,将其应用于D
,并且如果成功,则进行新提交。让我们称之为P
(Pick):
A - B - C - D - P <-- master
\
E - F <-- branch
这里的P
和F
的内容可能会非常不同,但是从D
到P
的变化与从E
到F
的变化是相同的。使用git show master
和git show branch
命令输出的差异应该非常相似-行号可能会有所改变(或者甚至有很大的变化),但是显示的更改应该是一样的。
让我们像之前丢弃合并M
一样丢弃P
4。请注意,我们仍然在分支master
上,并且它是干净的(没有任何操作,也没有需要提交的内容,尽管这次我没有用git status
查看):
$ git reset --hard master^
[snip]
这一次,让我们进行一个“压缩合并”(squash-merge
)。
这个 git merge
命令的操作与常规合并非常相似,只不过它不是像合并提交 M
那样使用两个父提交,而是设置了一个 "压缩合并" 提交。 让我们称其为 S
,它只有一个父提交。 由于它不会真正执行提交(压缩意味着--no-commit
选项),因此我们必须显式地进行提交的步骤:
$ git merge --squash branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested
$ git commit -m squash-merge
[snip]
现在我们有这个:
A - B - C - D - S <-- master
\
E - F <-- branch
S
代表的提交中,tree(即所有文件的集合)与常规合并所得到的树相同。在这种情况下,这相当于将B
和F
之间的git diff
作为单个补丁应用于提交D
的树内容。5
换句话说,S
包含了"B
和E
之间的更改以及E
和F
之间的更改",并应用于提交D
。但它只有一个父提交。正是这种提交图形的差异使其成为“压缩合并”,而不是常规合并。
当然,如果常规合并无法完成——因为提交E
未准备好合并到master
——那么压缩合并也不会成功。因此,在这种情况下,使用 cherry-pick 是明智的选择。
重要说明:请注意,每次我们对分支master
进行操作添加新提交时——合并M
、挑选P
或压缩提交S
——分支master
都会自动向前移动到指向最新的提交。这就是在 Git 中区分分支(或“本地分支”)与其他标签的方式。分支名称只是一个提交 ID,随着添加新提交,它会自动移动。标签名称的工作方式与分支名称完全相同,只是它们不会移动。
1好吧,blob 具有它们的副本,但是它们被压缩(使用 deflate 压缩),只要它们是“松散对象”。最终,松散对象会被“打包”,以节省更多的空间,并且可以对包进行“增量压缩”,这样就可以获得旧增量型 SCM 中提供的所有空间节省效果——实际上还要更多,因为任何一个对象都可以对任何其他对象进行压缩,至少在理论上。文件 foo 不必只针对“文件 foo 的先前版本”进行压缩。
2这主要是作为记录“关闭”分支的一种方法,而不是简单地舍弃它。
3reset --hard
做了两件事情:把工作目录修改回到提交 D
时的状态,同时将分支标签 master
再次指向提交 D
。这里的简单后缀符号 ^
告诉 Git 跟随“第一个父提交”(first parent)。另一种主要的语法,“向前回溯 N 次提交”,例如master~3
表示回溯3个提交,也是跟随“第一个父提交”的,所以从合并提交 M
开始,master~3
回溯到 D
、C
、B
。但是,一旦重置生效,master
就再次指向提交 D
,因此往回走3步会先到 C
,然后是 B
,最后是 A
。
4 顺带说一下,你或者说你应该会好奇:“我们随意扔掉的这些提交会发生什么?” 答案是:它们在仓库中通过“reflog”标记存活,直到reflog条目过期。默认情况下,若条目可达,则 reflog 条目在90天后“过期”——为了避免这个脚注变得太长而变得过于技术化,这里不过多展开;如果条目是“不可达”的,则在30天后过期。这些提交都是“不可达的”,因此它们将在大约一个月后被垃圾回收(garbage-collected),以及这些提交使用的任何树和 blob。
5假设您没有像将 ours
策略应用到合并提交一样做一些奇怪的事情,但那是无用的。(此外,与不属于 Git 的 patch
命令不同的是,Git 在执行合并、挑选某些提交等操作时是聪明的,因为它通常可以判断您是否已经应用了特定的补丁,并且不会尝试重复应用它。)
-n
特别选取它进行修复并恢复。 - torek