你好!我一直在思考 git 分支,并且不断听到人们说他们发现 git 分支的工作方式是违反直觉的。这让我思考:分支的“直观”概念可能是什么,它与 git 的实际工作方式有何不同?
所以在这篇文章中我想简单谈谈
- 我想很多人都有一个直观的心理模型
- git 实际上如何在内部表示分支(“技术上正确”的定义)
- “直观模型”和它的实际工作方式实际上是密切相关的
- 直观模型的一些限制以及为什么它可能会导致问题
这篇文章中没有什么是开创性的,所以我会尽量保持简短。
直观的分支模型
当然,人们对分支有很多不同的直觉。我认为这是最接近物理上的“苹果树的树枝”隐喻的一个。
我的猜测是,很多人对 git 分支的看法是这样的:图中粉红色的 2 个提交位于一个“分支”上。
我认为这张图有两点很重要:
- 该分支有 2 个提交
- 该分支有一个“父级”(
main
),它是其分支
这看起来很合理,但这不是 git 定义分支的方式——最重要的是,git 没有任何分支“父”的概念。那么git是如何定义分支的呢?
在git中,一个分支就是完整的历史记录
在 git 中,分支是每个先前提交的完整历史记录,而不仅仅是“分支”提交。因此,在上面的图片中,两个分支( main
和branch
)都有 4 个提交。
我在https://github.com/jvns/branch-example制作了一个示例存储库,其分支设置方式与上图相同。让我们看看这两个分支:
main
有 4 个提交:
$ git log --oneline main 70f727a d f654888 c 3997a46 b a74606f a
mybranch
也有 4 个提交。底部的两个提交在两个分支之间共享。
$ git log --oneline mybranch 13cb960 y 9554dab x 3997a46 b a74606f a
因此mybranch
有 4 个提交,而不仅仅是“分支”提交的 2 个提交13cb960
和9554dab
。
你可以让 git 绘制两个分支上的所有提交,如下所示:
$ git log --all --oneline --graph * 70f727a (HEAD -> main, origin/main) d * f654888 c | * 13cb960 (origin/mybranch, mybranch) y | * 9554dab x |/ * 3997a46 b * a74606f a
分支存储为提交 ID
在 git 内部,分支存储为微小的文本文件,其中包含提交 ID。该提交是分支上的最新提交。这就是我一开始所说的“技术上正确”的定义。
让我们看看示例存储库中main
和mybranch
的文本文件:
$ cat .git/refs/heads/main 70f727acbe9ea3e3ed3092605721d2eda8ebb3f4 $ cat .git/refs/heads/mybranch 13cb960ad86c78bfa2a85de21cd54818105692bc
这是有道理的: 70f727
是main
上的最新提交, 13cb96
是mybranch
上的最新提交。
正如我之前提到的,这里缺少的一件事是这两个分支之间的任何关系。没有迹象表明mybranch
是main
的分支。
既然我们已经讨论了分支的直观概念是如何“错误”的,我想谈谈它在一些非常重要的方面如何也是正确的。
人们的直觉通常并没有那么错误
我认为告诉人们他们对 git 的直觉是“错误的”是很流行的。我觉得这很愚蠢——一般来说,即使人们对某个主题的直觉在某些方面在技术上是不正确的,人们通常也会出于非常合理的原因而拥有直觉! “错误”的模型可能非常有用。
因此,让我们讨论一下分支的直观“分支”概念与我们在实践中实际使用 git 的方式非常紧密匹配的 3 种方式。
变基使用分支的“直观”概念
现在让我们回到原来的图片。
当你在main
上 rebase mybranch
时,它会获取“直观”分支上的提交(只有 2 个粉红色的提交)并将它们重播到main
上。
结果是仅复制 2( x
和y
)。看起来是这样的:
$ git switch mybranch $ git rebase main $ git log --oneline mybranch 952fa64 (HEAD -> mybranch) y 7d50681 x 70f727a (origin/main, main) d f654888 c 3997a46 b a74606f a
这里git rebase
创建了两个新的提交( 952fa64
和7d50681
),其信息来自之前的两个x
和y
提交。
所以直观模型并没有那么错误!它准确地告诉您 rebase 中会发生什么。
但是因为 git 不知道mybranch
是main
的分支,所以您需要明确告诉它在哪里重新设置分支的基础。
合并也使用分支的“直观”概念
合并不会复制提交,但它们确实需要“基础”提交:合并的工作方式是它查看两组更改(从共享基础开始),然后合并它们。
让我们撤消刚才所做的变基,然后看看合并基是什么。
$ git switch mybranch $ git reset --hard 13cb960 # undo the rebase $ git merge-base main mybranch 3997a466c50d2618f10d435d36ef12d5c6f62f57
这为我们提供了分支分支的“基础”提交3997a4
。这正是您认为可能基于我们的直观图景的提交。
github pull requests 也使用直观的想法
如果我们在 GitHub 上创建拉取请求以将mybranch
合并到main
中,它还会向我们显示 2 个提交:提交x
和y
。这是有道理的,也符合我们对分支的直观概念。
我假设如果您在 GitLab 上发出合并请求,它会向您显示类似的内容。
直觉很好,但有一些限制
这使得我们对分支的直观定义实际上看起来相当不错!关于分支的“直观”想法与合并、变基以及 GitHub 拉取请求的工作方式完全一致。
在合并或变基或发出拉取请求时(例如git rebase main
),您确实需要显式指定另一个分支,因为 git 不知道您认为您的分支基于哪个分支。
但是分支的直观概念有一个相当严重的问题:你直观地思考main
和分支分支的方式非常不同,而 git 不知道这一点。
那么让我们来谈谈不同类型的 git 分支。
主干和枝干
对于人类来说, main
和mybranch
是非常不同的,并且您对于如何使用它们可能有非常不同的意图。
我认为将某些分支视为“主干”分支,而将某些分支视为“分支”是很正常的。你也可以有一个分支的分支。
当然,git 本身并没有做出任何这样的区分(“分支”这个术语是我刚刚编造的!),但是它是一个什么样的分支肯定会影响你如何对待它。
例如:
- 您可能会将
mybranch
重新设置为main
,但您可能不会将main
重新设置为mybranch
– 这会很奇怪! - 一般来说,人们在重写“主干”分支上的历史时比短命的分支分支要小心得多
git 让你“向后”进行变基
我认为人们对 git 感到厌烦的一件事是——因为 git 没有任何关于一个分支是否是另一个分支的“分支”的概念,它不会给你任何关于是否/何时适合对分支 X 进行变基的指导在Y分支上。你只需要知道。
例如,您可以执行以下任一操作:
$ git checkout main $ git rebase mybranch
或者
$ git checkout mybranch $ git rebase main
Git 会很乐意让你做其中任何一个,尽管git rebase main
非常正常,而git rebase mybranch
则相当奇怪。
类似地,你可以“向后”合并,尽管这比向后变基要简单得多——出于不同的原因,将mybranch
合并到main
以及将main
合并到mybranch
都是有用的事情。
git 分支之间缺乏层次结构有点奇怪
我经常听到“ main
分支并不特别”这样的说法,并且我一直对此感到困惑 – 在我工作的大多数存储库中, main
非常特别!为什么人们说不是?
我认为重点是,即使分支之间确实存在关系( main
通常很特殊!),git 对这些关系一无所知。
每次运行git rebase
或git merge
这样的 git 命令时,你都必须明确地告诉 git 分支之间的关系,如果你犯了一个错误,事情就会变得非常奇怪。
我不知道 git 在这里的设计是“对”还是“错”(它肯定有一些优点和缺点,我已经厌倦了阅读关于它的无休止的争论),但我确实认为这对很多人来说是令人惊讶的人们有充分的理由。
就这样!
回想起来,这一切似乎都非常明显,但我花了很长时间才弄清楚分支的更“直观”想法可能是什么,因为我太习惯了技术上的“分支是对提交的引用”定义。
我也没有真正想过 git 如何让你在每次运行git rebase
或git merge
命令时告诉它分支之间的层次结构 – 对我来说,这样做是第二天性,这不是什么大问题,但现在我在想,很容易看出有人会混淆。
原文: https://jvns.ca/blog/2023/11/23/branches-intuition-reality/