---
author: Julia Evans
category: 软件开发
comments_data: []
count:
  commentnum: 0
  favtimes: 1
  likes: 0
  sharetimes: 0
  viewnum: 2361
date: '2023-12-01 00:41:12'
editorchoice: false
excerpt: 我不断从他人那里听说他们觉得 Git 分支的操作方式违反直觉。
fromurl: https://jvns.ca/blog/2023/11/23/branches-intuition-reality/
id: 16430
islctt: true
largepic: /images/p/202312/01/004025i72vi4t0o7027cyf.png
permalink: /article-16430-1.html
pic: /images/p/202312/01/004025i72vi4t0o7027cyf.png.thumb.jpg
related: []
reviewer: wxy
selector: lujun9972
summary: 我不断从他人那里听说他们觉得 Git 分支的操作方式违反直觉。
tags:
- Git
- 分支
thumb: false
title: Git 分支：直觉与现实
titlepic: true
translator: ChatGPT
updated: '2023-12-01 00:41:12'
---

![](/images/p/202312/01/004025i72vi4t0o7027cyf.png)


你好！我一直在投入写作一本关于 Git 的小册，因此我对 Git 分支投入了许多思考。我不断从他人那里听说他们觉得 Git 分支的操作方式违反直觉。这使我开始思考：直觉上的分支概念可能是什么样，以及它如何与 Git 的实际操作方式区别开来？


在这篇文章中，我想简洁地讨论以下几点内容：


* 我认为许多人可能有的一个直觉性的思维模型
* Git 如何在内部实现分支的表示（例如，“分支是对提交的指针”）
* 这种“直觉模型”与实际操作方式之间的紧密关联
* 直觉模型的某些局限性，以及为何它可能引发问题


本文无任何突破性内容，我会尽量保持简洁。


### 分支的直观模型


当然，人们对分支有许多不同的直觉。我自己认为最符合“苹果树的一个分支”这一物理比喻的可能是下面这个。


我猜想许多人可能会这样理解 Git 分支：在下图中，两个红色的提交就代表一个“分支”。


![](/images/p/202312/01/004113hsfe1ukeh8rt1f8t.png)


我认为在这个示意图中有两点很重要：


1. 分支上有两个提交
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` 上的最新提交。


这样做的原因是，每个提交都包含一种指向其父级的指针，所以 Git 可以通过追踪这些指针链来找到分支上所有的提交。


正如我前文所述，这里遗漏的一个重要因素是这两个分支间的任何关联关系。从这里能看出，`mybranch` 是 `main` 的一个分支——这一点并没有被表明出来。


既然我们已经探讨了直观理解的分支概念是如何不成立的，我接下来想讨论的是，为何它在某些重要的方面又是如何成立的。


### 人们的直观感觉通常并非全然错误


我发现，告诉人们他们对 Git 的直觉理解是“错误的”的说法颇为流行。我觉得这样的说法有些可笑——总的来说，即使人们关于某个题目的直觉在某些方面在技术上不精确，但他们通常会有完全合理的理由来支持他们的直觉！即使是“不正确的”模型也可能极其有用。


现在，我们来讨论三种情况，其中直觉上的“分支”概念与我们实际在操作中如何使用 Git 非常相符。


### 变基操作使用的是“直观”的分支概念


现在，让我们回到最初的图片。


![](/images/p/202312/01/004113hsfe1ukeh8rt1f8t.png)


当你在 `main` 上对 `mybranch` 执行 <ruby> 变基 <rt>  rebase </rt></ruby> 操作时，它将取出“直观”分支上的提交（只有两个红色的提交）然后将它们应用到 `main` 上。


执行结果就是，只有两次提交（`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` 提交。


所以直觉上的模型并不完全错误！它很精确地告诉你在变基中发生了什么。


但因为 Git 不知道 `mybranch` 是 `main` 的一个分叉，你需要显式地告诉它在何处进行变基。


### 合并操作也使用了“直观”的分支概念


合并操作并不复制提交，但它们确实需要一个“<ruby> 基础 <rt>  base </rt></ruby>”提交：合并的工作原理是查看两组更改（从共享基础开始），然后将它们合并。


我们撤销刚才完成的变基操作，然后看看合并基础是什么。



```
$ git switch mybranch
$ git reset --hard 13cb960  # 撤销 rebase
$ git merge-base main mybranch
3997a466c50d2618f10d435d36ef12d5c6f62f57

```

这里我们获得了分支分离出来的“基础”提交，也就是 `3997a4`。这正是你可能会基于我们的直观图片想到的提交。


### GitHub 的拉取请求也使用了直观的概念


如果我们在 GitHub 上创建一个拉取请求，打算将 `mybranch` 合并到 `main`，这个请求会展示出两次提交：也就是 `x` 和 `y`。这完全符合我们的预期，也和我们对分支的直观认识相符。


![](/images/p/202312/01/004113qbuwytnuqtkhjl3l.png)


我想，如果你在 GitLab 上发起一个合并请求，那显示的内容应该会与此类似。


### 直观理解颇为精准，但它有一定局限性


这使我们的对分支直观定义看起来相当准确！这个“直观”的概念和合并、变基操作以及 GitHub 拉取请求的工作方式完全吻合。


当你在进行合并、变基或创建拉取请求时，你需要明确指定另一个分支（如 `git rebase main`），因为 Git 不知道你的分支是基于哪个分支的。


然而，关于分支的直观理解有一个比较严重的问题：你直觉上认为 `main` 分支和某个分离的分支有很大的区别，但 Git 并不清楚这点。


所以，现在我们要来讨论一下 Git 分支的不同种类。


### 主干和派生分支


对于人类来说，`main` 和 `mybranch` 有着显著的区别，你可能针对如何使用它们，有着截然不同的意图。


通常，我们会将某些分支视为“<ruby> 主干 <rt>  trunk </rt></ruby>”分支，同时将其他一些分支看作是“派生”。你甚至可能有派生的派生分支。


当然，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` 则显得格外奇怪。许多人表示他们对此感到困惑，所以我提供了一个展示两种变基类型的图片以供参考：


![](/images/p/202312/01/004114c97y1nk9n96pymk7.png)


相似地，你可以进行“反向”的合并，尽管这相较于反向变基要正常得多——将 `mybranch` 合并到 `main` 和将 `main` 合并到 `mybranch` 都有各自的益处。


下面是一个展示你可以进行的两种合并方式的示意图：


![](/images/p/202312/01/004115hccjb6cqqqqbegbf.png)


### Git 对于分支之间缺乏层次结构感觉有些奇怪


我经常听到 “`main` 分支没什么特别的” 的表述，而这令我感到困惑——对于我来说，我处理的大部分仓库里，`main` 无疑是非常特别的！那么人们为何会称其为不特别呢？


我觉得，重点在于：尽管分支确实存在彼此间的关系（`main` 通常是非常特别的！），但 Git 并不知情这些关系。


每当你执行如 `git rebase` 或 `git merge` 这样的 `git` 命令时，你都必须明确地告诉 Git 分支间的关系，如果你出错，结果可能会相当混乱。


我不知道 Git 在此方面的设计究竟“对”还是“错”（无疑它有利有弊，而我已对无休止的争论感到厌倦），但我认为，这对于许多人来说，原因在于它有些出人意料。


### Git 关于分支的用户界面也同样怪异


假设你只想查看某个分支上的“派生”提交，正如我们之前讨论的，这是完全正常的需求。


下面是用 `git log` 查看我们分支上的两次派生提交的方法：



```
$ git switch mybranch
$ git log main..mybranch --oneline
13cb960 (HEAD -> mybranch, origin/mybranch) y
9554dab x

```

你可以用 `git diff` 这样查看同样两次提交的合并差异：



```
$ git diff main...mybranch

```

因此，如果你想使用 `git log` 查看 `x` 和 `y` 这两次提交，你需要用到两个点（`..`），但查看同样的提交使用 `git diff`，你却需要用到三个点（`...`）。


我个人从来都记不住 `..` 和 `...` 的具体用意，所以我通常虽然它们在原则上可能很有用，但我选择尽量避免使用它们。


### 在 GitHub 上，默认分支具有特殊性


同样值得一提的是，在 GitHub 上存在一种“特殊的分支”：每一个 GitHub 仓库都有一个“默认分支”（在 Git 术语中，就是 `HEAD` 所指向的地方），具有以下的特别之处：


* 初次克隆仓库时，默认会检出这个分支
* 它作为拉取请求的默认接收分支
* GitHub 建议应该保护这个默认分支，防止被强制推送，等等。


很可能还有许多我未曾想到的场景。


### 总结


这些说法在回顾时看似是显而易见的，但实际上我花费了大量时间去搞清楚一个更“直观”的分支概念，这是因为我已经习惯了技术性的定义，“分支是对某次提交的引用”。


同样，我也没有真正去思索过如何在每次执行 `git rebase` 或 `git merge` 命令时，让 Git 明确理解你分支之间的层次关系——对我而言，这已经成为第二天性，并没有觉得有何困扰。但当我反思这个问题时，可以明显看出，这很容易导致某些人混淆。


*（题图：MJ/a5a52832-fac8-4190-b3bd-fec70166aa16）*




---


via: <https://jvns.ca/blog/2023/11/23/branches-intuition-reality/>


作者：[Julia Evans](https://jvns.ca/) 选题：[lujun9972](https://github.com/lujun9972) 译者：[ChatGPT](https://linux.cn/lctt/ChatGPT) 校对：[wxy](https://github.com/wxy)


本文由 [LCTT](https://github.com/LCTT/TranslateProject) 原创编译，[Linux中国](https://linux.cn/) 荣誉推出