解读那些令人困惑 Git 术语

你有觉得哪些 Git 术语很让人困惑吗?

解读那些令人困惑 Git 术语

我正在一步步解释 Git 的方方面面。在使用 Git 近 15 年后,我已经非常习惯于 Git 的特性,很容易忘记它令人困惑的地方。

因此,我在 Mastodon 上进行了调查:

你有觉得哪些 Git 术语很让人困惑吗?我计划写篇博客,来解读 Git 中一些奇怪的术语,如:“分离的 HEAD 状态”,“快速前移”,“索引/暂存区/已暂存”,“比 origin/main 提前 1 个提交”等等。

我收到了许多有洞见的答案,我在这里试图概述其中的一部分。下面是这些术语的列表:

  • HEAD 和 “heads”
  • “分离的 HEAD 状态”
  • 在合并或变基时的 “ours” 和 “theirs”
  • “你的分支已经与 ‘origin/main’ 同步”
  • HEAD^HEAD~HEAD^^HEAD~~HEAD^2HEAD~2
  • .....
  • “可以快速前移”
  • “引用”、“符号引用”
  • refspecs
  • “tree-ish”
  • “索引”、“暂存的”、“已缓存的”
  • “重置”、“还原”、“恢复”
  • “未跟踪的文件”、“追踪远程分支”、“跟踪远程分支”
  • 检出
  • reflog
  • 合并、变基和遴选
  • rebase –onto
  • 提交
  • 更多复杂的术语

我已经尽力讲解了这些术语,但它们几乎覆盖了 Git 的每一个主要特性,这对一篇博客而言显然过于繁重,所以在某些地方可能会有一些粗糙。

HEAD 和 “heads”

有些人表示他们对 HEADrefs/heads/main 这些术语感到困惑,因为听起来像是一些复杂的技术内部实现。

以下是一个快速概述:

  • “heads” 就是 “分支”。在 Git 内部,分支存储在一个名为 .git/refs/heads 的目录中。(从技术上讲,官方 Git 术语表 中明确表示分支是所有的提交,而 head 只是最近的提交,但这只是同一事物的两种不同思考方式)
  • HEAD 是当前的分支,它被存储在 .git/HEAD 中。

我认为,“head 是一个分支,HEAD 是当前的分支” 或许是 Git 中最奇怪的术语选择,但已经设定好了,想要更清晰的命名方案已经为时已晚,我们继续。

“HEAD 是当前的分支” 有一些重要的例外情况,我们将在下面讨论。

“分离的 HEAD 状态”

你可能已经看到过这条信息:

“`
$ git checkout v0.1
You are in ‘detached HEAD’ state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

[…]

“`

(消息译文:你处于 “分离 HEAD” 的状态。你可以四处看看,进行试验性的更改并提交,你可以通过切换回一个分支来丢弃这个状态下做出的任何提交。)

这条信息的实质是:

  • 在 Git 中,通常你有一个已经检出的 “当前分支”,例如 main
  • 存放当前分支的地方被称为 HEAD
  • 你做出的任何新提交都会被添加到你的当前分支,如果你运行 git merge other_branch,这也会影响你的当前分支。
  • 但是,HEAD 不一定必须是一个分支!它也可以是一个提交 ID。
  • Git 会称这种状态(HEAD 是提交 ID 而不是分支)为 “分离的 HEAD 状态”
  • 例如,你可以通过检出一个标签来进入分离的 HEAD 状态,因为标签不是分支
  • 如果你没有当前分支,一系列事情就断链了:
    • git pull 根本就无法工作(因为它的全部目的就是更新你的当前分支)
    • 除非以特殊方式使用 git push,否则它也无法工作
    • git commitgit mergegit rebasegit cherry-pick 仍然可以工作,但它们会留下“孤儿”提交,这些提交没有连接到任何分支,因此找到这些提交会很困难
  • 你可以通过创建一个新的分支或切换到一个现有的分支来退出分离的 HEAD 状态

在合并或变基中的 “ours” 和 “theirs”

遇到合并冲突时,你可以运行 git checkout --ours file.txt 来选择 “ours” 版本中的 file.txt。但问题是,什么是 “ours”,什么是 “theirs” 呢?

我总感觉此类术语混淆不清,也因此从未用过 git checkout --ours,但我还是查找相关资料试图理清。

在合并的过程中,这是如何运作的:当前分支是 “ours”,你要合并进来的分支是 “theirs”,这样看来似乎很合理。

“`
$ git checkout merge-into-ours # 当前分支是 “ours”
$ git merge from-theirs # 我们正要合并的分支是 “theirs”

“`

而在变基的过程中就刚好相反 —— 当前分支是 “theirs”,我们正在变基到的目标分支是 “ours”,如下:

“`
$ git checkout theirs # 当前分支是 “theirs”
$ git rebase ours # 我们正在变基到的目标分支是 “ours”

“`

我以为之所以会如此,因为在操作过程中,git rebase main 其实是将当前分支合并到 main (它类似于 git checkout main; git merge current_branch),尽管如此我仍然觉得此类术语会造成混淆。

这个精巧的小网站 对 “ours” 和 “theirs” 的术语进行了解释。

人们也提到,VSCode 将 “ours”/“theirs” 称作 “当前的更改”/“收到的更改”,同样会引起混淆。

“你的分支已经与 origin/main 同步”

此信息貌似很直白 —— 你的 main 分支已经与源端同步!

但它实际上有些误导。可能会让你以为这意味着你的 main 分支已经是最新的,其实不然。它真正的含义是 —— 如果你最后一次运行 git fetchgit pull 是五天前,那么你的 main 分支就是与五天前的所有更改同步。

因此,如果你没有意识到这一点,它对你的安全感其实是一种误导。

我认为 Git 理论上可以给出一个更有用的信息,像是“与五天前上一次获取的源端 main 是同步的”,因为最新一次获取的时间是在 reflog 中记录的,但它没有这么做。

HEAD^HEAD~HEAD^^HEAD~~HEAD^2HEAD~2

我早就清楚 HEAD^ 代表前一次提交,但我很长一段时间都困惑于 HEAD~HEAD^ 之间的区别。

我查询资料,得到了如下的对应关系:

  • HEAD^HEAD~ 是同一件事情(指向前 1 个提交)
  • HEAD^^^HEAD~~~HEAD~3 是同一件事情(指向前 3 个提交)
  • HEAD^3 指向提交的第三个父提交,它与 HEAD~3 是不同的

这看起来有些奇怪,为什么 HEAD~HEAD^ 是同一个概念?以及,“第三个父提交”是什么?难道就是父提交的父提交的父提交?(剧透:并非如此)让我们一起深入探讨一下!

大部分提交只有一个父提交。但是合并提交有多个父提交 – 因为它们合并了两个或更多的提交。在 Git 中,HEAD^ 意味着 “HEAD 提交的父提交”。但是如果 HEAD 是一个合并提交,那 HEAD^ 又代表怎么回事呢?

答案是,HEAD^ 指向的是合并提交的第一个父提交,HEAD^2 是第二个父提交,HEAD^3 是第三个父提交,等等。

但我猜他们也需要一个方式来表示“前三个提交”,所以 HEAD^3 是当前提交的第三个父提交(如果当前提交是一个合并提交,可能会有很多父提交),而 HEAD~3 是父提交的父提交的父提交。

我想,从我们之前对合并提交 “ours”/“theirs” 的讨论来看,HEAD^ 是 “ours”,HEAD^2 是 “theirs”。

.....

这是两个命令:

  • git log main..test
  • git log main...test

我从没用过 ..... 这两个命令,所以我得查一下 man git-range-diff。我的理解是比如这样一个情况:

“`
A – B main
\
C – D test

“`

  • main..test 对应的是提交 C 和 D
  • test..main 对应的是提交 B
  • main...test 对应的是提交 B,C,和 D

更有挑战的是,git diff 显然也支持 .....,但它们在 git log 中的意思完全不同?我的理解如下:

  • git log test..main 显示在 main 而不在 test 的更改,但是 git log test...main 则会显示 两边 的改动。
  • git diff test..main 显示 test 变动 main 变动(它比较 BD),而 git diff test...main 会比较 AD(它只会给你显示一边的差异)。

有关这个的更多讨论可以参考 这篇博客文章

“可以快速前移”

git status 中,我们会经常遇到如下的信息:

“`
$ git status
On branch main
Your branch is behind ‘origin/main’ by 2 commits, and can be fast-forwarded.
(use “git pull” to update your local branch)

“`

(消息译文:你现在处于 main 分支上。你的分支比 origin/main 分支落后了 2 个提交,可以进行快速前进。 (使用 git pull 命令可以更新你的本地分支))

但“快速前移” 到底是何意?本质上,它在告诉我们这两个分支基本如下图所示(最新的提交在右侧):

“`
main: A – B – C
origin/main: A – B – C – D – E

“`

或者,从另一个角度理解就是:

“`
A – B – C – D – E (origin/main)
|
main

“`

这里,origin/main 仅仅多出了 2 个 main 不存在的提交,因此我们可以轻松地让 main 更新至最新 —— 我们所需要做的就是添加上那 2 个提交。事实上,这几乎不可能出错 —— 不存在合并冲突。快速前进式合并是个非常棒的事情!这是合并两个分支最简单的方式。

运行完 git pull 之后,你会得到如下状态:

“`
main: A – B – C – D – E
origin/main: A – B – C – D – E

“`

下面这个例子展示了一种不能快速前进的状态。

“`
A – B – C – X (main)
|
– – D – E (origin/main)

“`

此时,main 分支上有一个 origin/main 分支上无的提交(X),所以无法执行快速前移。在此种情况,git status 就会如此显示:

“`
$ git status
Your branch and ‘origin/main’ have diverged,
and have 1 and 2 different commits each, respectively.

“`

(你的分支和 origin/main 分支已经产生了分歧,其中各有 1 个和 2 个不同的提交。)

“引用”、“符号引用”

在使用 Git 时,“引用” 一词可能会使人混淆。实际上,Git 中被称为 “引用” 的实例至少有三种:

  • 分支和标签,例如 mainv0.2
  • HEAD,代表当前活跃的分支
  • 诸如 HEAD^^^ 这样的表达式,Git 会将其解析成一个提交 ID。确切说,这可能并非 “引用”,我想 Git 将其称作 “版本参数”,但我个人并未使用过这个术语。

个人而言,“符号引用” 这个术语颇为奇特,因为我觉得我只使用过 HEAD(即当前分支)作为符号引用。而 HEAD 在 Git 中占据核心位置,多数 Git 核心命令的行为都基于 HEAD 的值,因此我不太确定将其泛化成一个概念的实际意义。

refspecs

.git/config 配置 Git 远程仓库时,你可能会看到这样的代码 +refs/heads/main:refs/remotes/origin/main

“`
[remote “origin”]
url = git@github.com:jvns/pandas-cookbook
fetch = +refs/heads/main:refs/remotes/origin/main

“`

我对这段代码的含义并不十分清楚,我通常只是在使用 git clonegit remote add 配置远程仓库时采用默认配置,并没有动机去深究或改变。

“tree-ish”

git checkout 的手册页中,我们可以看到:

“`
git checkout [-f|–ours|–theirs|-m|–conflict=