我已经阅读了Git内部here和here并了解了提交内容,以及树和blob。
我知道Git存储单个文件而不是文件差异(增量),并且根据需要实时计算后面的文件。文档还经常谈到“两次提交之间的区别”(无论是父母和子女,祖先/后代还是两者都没有)。
然而,我不清楚Git如何在各种情况下计算这些增量(挑选,合并,变基)。在每种情况下都考虑哪些文件(即提交的文件)?
我已经读过根据该结构,单个提交可以被认为是一个完整的分支(即导致该提交的提交历史),在某种意义上,对于给定的文件,我可以遍历所有版本分支回来(虽然不一定回到它的根据我想;只需回到之前的文件版本即可)。如果我的假设是错误的,请澄清。
答案 0 :(得分:1)
规则在概念上很简单,但在实践中变得复杂。
真实git merge
使用提交DAG来查找合并基础。合并基础被定义为最低共同祖先(以明显的方式推广到任意DAG,其中可能存在多个LCA,而对于简单的树,其中总是存在唯一的LCA)。给定两次提交,git merge-base
命令将从DAG中找到(默认)或全部(--all
)合并基础提交。
如果存在多个合并库,则算法取决于-s
(策略)参数。默认的recursive
策略使用递归合并合并库(还有什么?:-))。这是目前以慢 - 简单 - 愚蠢的方式完成的:如果有5个合并基础,Git合并其中两个(根据需要找到这两个的合并基础)并从结果中进行“虚拟提交”,将该结果与5列表中的下一个(第3个)将 结果与第4个合并,并将 与第5个合并,以获得最终的虚拟合并基础。 (为了使这一切正常工作,我相信Git实际上做了真正的提交。没有理由不这样做:这些未引用的提交将在以后自动被垃圾收集。)
resolve
策略只选择多个合并库中的一个,并将其作为基础。
在任何情况下,一旦我们有一个合并基本哈希ID $base
和两个分支提示,两个差异就会得到:
git diff $base $tip1
git diff $base $tip2
(或多或少 - 如果需要,可以对--rename-limit
值进行一些调整,具体取决于额外的合并命令参数,并且所有这些都假设没有特殊的合并驱动程序;实际的合并发生在逐个文件中,但是每个文件的合并基础版本来自$base
,任何重命名检测首先发生在两个提交范围的差异中。
git cherry-pick
命令将每次提交与其父级进行区分,然后首先尝试将生成的delta应用为补丁。如果失败则会退回“三路合并”,但合并基础是逐个文件而不是提交提交,因为它使用格式化补丁中的Index:
信息。每个补丁文件中有一个Index:
行,给出了两个blob的SHA-1 ID。
因此,合并基础最初完全被忽略:cherry-pick只使用补丁作为补丁。只有当补丁不适用时(如在git apply
中),樱桃选择才会回退到三向合并(如git apply -3
中所示)。 blob本身也必须存在于您的存储库中 - 对于一个樱桃选择,它始终存在;对于电子邮件补丁的文字git apply
,它可能不会。
此时要合并的两个差异是:
git diff $indexbase $file1
the diff in the patch # equivalent to git diff $indexbase $file2
其中$indexbase
是由Index:
行中的哈希ID提取的文件,而$file1
是工作树中的文件。 (除非您使用HEAD
,否则此文件与git cherry-pick -n
提交相匹配。)在任意(通过电子邮件发送)的修补程序中,您根本不需要$file2
,只需差异;在一个挑选樱桃的补丁中,$file2
是正在提交的提交文件的版本(但由于我们已经有了差异,所以不需要它!)。
如果您选择合并提交,则必须告诉Git 该合并提交的哪个父级将用于生成changeset-as-patch。此步骤完全是手动的。
在功能上,rebase包含一系列樱桃挑选操作。从rebase中省略了合并提交。 (交互式rebase的--preserve-merges
操作使 new 合并,完全忽略原始合并。)交互式rebase实际上运行git cherry-pick
(每次提交一次一个被复制),而非交互式rebase尝试使用git format-patch <args> | git am -3
,如果可以的话(格式化修补删除“空”提交,这样只有-k
才可以)。
在某些情况下,通过实际的git rev-list --cherry-pick
选择对应的差异来提交要复制的提交,或者,出于算法目的,这些提交是等效的。