这里写自定义目录标题
- 引子
- 分支的直观模型
- 在 git 中,分支是完整的提交记录
- 分支用commit ID存储
- 人们的直觉通常并没有那么错
- rebase 使用“直观”的分支概念
- `merge`也使用“直观”的分支概念
- github pull request 也使用直观的想法
- 直觉很好,但它也有一些局限性
- 主干和分支
- git 允许你“向后”进行 rebase
- git 的分支之间缺乏层次结构,这有点奇怪
- git 的分支 UI 也很奇怪
- 在 GitHub 中,default分支是特殊的
- 就这样!
引子
关于git branch,我经常听到人们说他们觉得 git branch的工作方式违反直觉。 我就纳闷了:什么是“直觉”的分支概念,它与 git 的实际工作方式有何不同?
因此,在这篇文章中,我想简单谈谈
1)我认为很多人都有直观的思维模型
2)git 内部如何表示分支(“分支是指向提交的指针”等)
3)“直观模型”和其实际运作方式实际上是非常密切相关的
4)直观模型的一些局限性以及它可能导致问题的原因
这篇文章中没有一些很新颖的东西,都是些常识性的东西。
分支的直观模型
当然,人们对branch有许多不同的直觉。我认为以下直觉与现实的“苹果树的分支”比喻最为接近的。
我猜很多人会这样看待 git 分支:下图中粉红色的有两次提交的是一个“分支”。
我认为该图有两个重要之处:
1)该分支上有 2 个提交
2)该分支有一个“父级”(main),它是其的一个分支
这似乎很有道理,但 git 定义分支的方式并不是这样——最重要的是,git 没有分支“父级”的概念。那么 git 如何定义分支呢?
在 git 中,分支是完整的提交记录
在 git 中,分支是所有先前提交的完整历史记录,而不仅仅是“分支”提交。因此,在上面的图片中,两个分支(main和branch)都有 4 个提交。
git上有一个示例存储库,其分支设置方式与上图相同。让我们看看这两个分支:
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
分支用commit ID存储
在 git 内部,分支被存储为带有提交 ID 的小型文本文件。该提交是分支上的最新提交。这就是我在开头提到的“技术上正确”的定义。
让我们看一下示例 repo 中的main文本文件:mybranch
$ cat .git/refs/heads/main
70f727acbe9ea3e3ed3092605721d2eda8ebb3f4
$ cat .git/refs/heads/mybranch
13cb960ad86c78bfa2a85de21cd54818105692bc
这是有道理的:70f727是main分支上的最新提交,并且13cb96是mybranch
上的最新提交。
这种方法之所以有效,是因为每个提交都包含一个指向其父级的指针,所以 git 可以按照指针链来获取分支上的每个提交。
就像我之前提到的,这里缺少的是这两个分支之间的关系。没有迹象表明这mybranch
是main
的一个分支。
我们已看到了分支的直观概念是如何“错误的”,但是它在一些非常重要的方面又是正确的。
人们的直觉通常并没有那么错
我认为告诉人们他们对 git 的直觉是“错误的”这种说法相当正常。我觉得这有点愚蠢——一般来说,即使人们对某个东西的看法在技术上是错误的,但是他们通常也会出于非常正当的理由而产生这种直觉!“错误”的模型可能非常有用。
因此,让我们从 3 个方面来讨论一下分支的直观“分支”概念与我们在实践中实际使用 git 的方式非常接近。
rebase 使用“直观”的分支概念
来再看看篇头的那个图
当你在main
分支对分支mybranch
上进行rebase时,它会将“直观”分支上的提交(仅 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
的一个分支,所以你需要明确告诉它在哪里重新定位分支。
merge
也使用“直观”的分支概念
合并不会复制提交,但它们确实需要一个“基本”提交:合并的工作方式是查看两组更改(从共享基础开始),然后合并它们。
让我们撤消刚刚执行的变基,然后看看合并基础是什么。
$ git switch mybranch
$ git reset --hard 13cb960 # undo the rebase
$ git merge-base main mybranch
3997a466c50d2618f10d435d36ef12d5c6f62f57
这给了我们分支所在的“base”提交3997a4。根据我们的直观图片,你可能会认为这就是提交。
github pull request 也使用直观的想法
如果我们在 GitHub 上创建一个合并merge mybranch
到 main
,它还会向我们显示 2 个提交:提交x和y。这很有意义并且也符合我们对分支的直观概念。
我假设如果你在 GitLab 上发出合并请求,它会显示类似的内容。
直觉很好,但它也有一些局限性
实际上让我们对分支的直观定义感觉上是不错的!分支的“直观”概念与merge、rebase 以及 GitHub pull 请求的工作方式完全匹配。
在merge、rebase或者 pull 请求时(例如git rebase main),你确实需要明确指定其他分支,因为 git 不知道你想基于哪个分支。
但是分支的直观概念有一个相当严重的问题:你直观地思考的方式在main分支和其他分支的方式不同, 但是git 并不知道这一点。
那么让我们来探讨一下不同类型的 git 分支。
主干和分支
对于人类来说,main
和mybranch
是相当不同的,并且你在如何使用它们方面可能有着相当不同的意图。
我认为将一些分支视为“主干”分支,将一些分支视为“分支”是很正常的。你也可以拥有分支的分支。
当然,git 本身并没有做出这样的区分(“分支”这个术语是我编造的!),但它是哪种分支肯定会影响你如何区分对待它。
例如:
1)你可能会重新rebase了 mybranch 到 main 但你可能不会把main分支rebase到mybranch,那就很奇怪了!
2)一般而言,人们在在覆盖“主干”分支的历史记录时比在一些短期的分支上覆盖历史记录时要谨慎得多
git 允许你“向后”进行 rebase
我认为 git 让人们感到困惑的一件事是 —— 因为 git 不知道一个分支是否是另一个分支的“分支”,所以它不会给你任何关于是否/何时适合将分支 X 重新定位到分支 Y 上的指导,你只需要知道。
例如,您可以执行以下任一操作:
$ git checkout main
$ git rebase mybranch
或者
$ git checkout mybranch
$ git rebase main
Git 很乐意让你做任何一种,尽管在这种情况下git rebase main
非常正常而且git rebase mybranch
很奇怪。很多人说他们觉得这很令人困惑,这里有一张两种 rebase 的图片:
类似地,你可以“向后”进行merge,尽管这比执行向后rebase更为常见 - 出于不同的原因,merge mybranch
到main
和main merge到 mybranch
都是有用的事情。
以下是两种合并方式的图表:
git 的分支之间缺乏层次结构,这有点奇怪
我经常听到“main分支并不特殊”的说法,对此我感到很困惑——在我工作的大多数存储库中,main分支都 非常特殊!为什么人们说它不特殊?
我认为关键在于尽管分支之间确实和main存在关系(通常很特殊!),但 git 对这些关系并不感知。
每次运行 git 命令(例如git rebase
或git merge
)时,都必须明确告诉 git 分支之间的关系,如果您犯了错误,事情就会变得非常奇怪。
我不知道 git 在这里的设计是“正确”还是“错误”(它肯定有一些优点和缺点,而且我已经厌倦了阅读关于它的无休止的争论),但我确实认为它让很多人感到惊讶,这是有原因的。
git 的分支 UI 也很奇怪
假设您只想查看分支上的“分支”提交,正如我们所讨论的,这是完全正常的事情。
下面演示了如何使用以下命令查看我们分支上的 2 个分支提交git log:
$ git switch mybranch
$ git log main..mybranch --oneline
13cb960 (HEAD -> mybranch, origin/mybranch) y
9554dab x
你可以像这样查看这 2 次提交的合并差异git diff:
$ git diff main...mybranch
因此,用git log查看带有的2 个提交(x和y),需要使用 2 个点 (… ) git log ..
,但要用git diff查看带有的相同提交,您需要使用 3 个点 (… ) , 如 git diff...
就我个人而言,我永远无法记住…和…是什么意思,所以我完全避免使用它们,尽管原则上它们看起来很有用。
在 GitHub 中,default分支是特殊的
另外,值得一提的是,GitHub 确实有一个“特殊分支”:每个 GitHub repo 都有一个“默认分支”(用 git 术语来说,它就是HEAD指向的),它在以下方面是特别的:
1) 这是你在git clone存储库中签出的内容
2) 这是拉取请求的默认目的地
3)github 会建议你保护默认分支不被强制推送
或许还有更多我没有想到的。
就这样!
回想起来,这一切似乎都非常明显,但我花了很长时间才弄清楚分支的更“直观”的想法是什么,因为我已经习惯了技术上的“分支是对提交的引用”的定义。
我也没有真正想过 git 如何让你在每次运行git rebase
或者 git merge
时告诉它分支之间的层次结构——对我来说,这样做是第二天性,没有什么大不了的,但现在我想想,很容易看出为什么有人会混淆。