章节 ▾ 第二版

7.1 Git 工具 - 修订版本选择

到目前为止,你已经学到了日常管理或维护 Git 仓库以进行源代码控制所需的大多数命令和工作流程。你已经完成了跟踪和提交文件的基本任务,并且掌握了暂存区以及轻量级主题分支和合并的强大功能。

现在,你将探索 Git 能够做的一些非常强大的事情,这些事情可能不常用,但在某些时候可能会用到。

修订版本选择

Git 允许你以多种方式引用单个提交、一组提交或一系列提交。它们不一定显而易见,但了解它们很有帮助。

单个修订版本

你显然可以通过其完整的 40 个字符的 SHA-1 哈希来引用任何单个提交,但也有更人性化的方式来引用提交。本节概述了引用任何提交的各种方式。

短 SHA-1

如果你提供 SHA-1 哈希的前几个字符,只要该部分哈希至少有四个字符长且没有歧义,Git 就足够智能,可以识别你所引用的提交;也就是说,对象数据库中没有其他对象的哈希以相同的前缀开头。

例如,要检查你已知添加了特定功能的特定提交,你可能首先运行 git log 命令来定位该提交

$ git log
commit 734713bc047d87bf7eac9674765ae793478c50d3
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800

    Fix refs handling, add gc auto, update tests

commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 14:58:32 2008 -0800

    Add some blame and merge stuff

在这种情况下,假设你对哈希以 1c002dd... 开头的提交感兴趣。你可以使用以下任何 git show 变体来检查该提交(假设较短的版本没有歧义)

$ git show 1c002dd4b536e7479fe34593e72e6c6c1819e53b
$ git show 1c002dd4b536e7479f
$ git show 1c002d

Git 可以为你的 SHA-1 值找出一个简短、唯一的缩写。如果你将 --abbrev-commit 传递给 git log 命令,输出将使用较短的值,但会保持它们的唯一性;它默认为使用七个字符,但如有必要,会将其加长以保持 SHA-1 的无歧义性

$ git log --abbrev-commit --pretty=oneline
ca82a6d Change the version number
085bb3b Remove unnecessary test code
a11bef0 Initial commit

通常,八到十个字符足以在一个项目中保持唯一。例如,截至 2019 年 2 月,Linux 内核(一个相当大的项目)在其对象数据库中拥有超过 875,000 个提交和近七百万个对象,没有两个对象的 SHA-1 在前 12 个字符中是相同的。

注意
关于 SHA-1 的简短说明

很多人在某个时候会担心,他们会因为偶然巧合,在他们的仓库中拥有两个不同的对象,它们哈希到相同的 SHA-1 值。那会怎么样?

如果你碰巧提交了一个对象,其哈希值与仓库中先前*不同*的对象的 SHA-1 值相同,Git 会看到你 Git 数据库中已有的先前对象,假设它已经写入,并简单地重用它。如果你在某个时候尝试再次检出该对象,你将始终获得第一个对象的数据。

然而,你应该知道这种情景发生的可能性是多么微乎其微。SHA-1 摘要是 20 字节或 160 位。确保单个冲突概率达到 50% 所需的随机哈希对象数量约为 280(确定冲突概率的公式是 p = (n(n-1)/2) * (1/2^160))。280 是 1.2 x 1024,即一万亿的十亿倍。这是地球上沙粒数量的 1,200 倍。

这里有一个例子,让你了解要产生 SHA-1 冲突需要什么。如果地球上所有 65 亿人类都在编程,并且每秒钟,每个人都在生成相当于整个 Linux 内核历史(650 万个 Git 对象)的代码,并将其推送到一个巨大的 Git 仓库中,那么大约需要 2 年,该仓库才能包含足够的对象,使其单个 SHA-1 对象冲突的概率达到 50%。因此,自然发生的 SHA-1 冲突的可能性,比你的编程团队的每个成员在同一个晚上因无关事件被狼袭击并杀死还要小。

如果你投入价值数千美元的计算能力,就可能合成两个具有相同哈希的文件,正如 2017 年 2 月在 https://shattered.io/ 上所证明的那样。Git 正在转向使用 SHA256 作为默认哈希算法,该算法对碰撞攻击更具弹性,并且已经有代码可以帮助缓解这种攻击(尽管不能完全消除)。

分支引用

引用特定提交的一种直接方法是,如果它是某个分支的尖端提交;在这种情况下,你可以在任何需要提交引用的 Git 命令中简单地使用分支名称。例如,如果你想检查分支上的最后一个提交对象,以下命令是等效的,假设 topic1 分支指向提交 ca82a6d...

$ git show ca82a6dff817ec66f44342007202690a93763949
$ git show topic1

如果你想查看特定 SHA-1 分支指向哪个,或者如果你想查看这些示例中的任何一个在 SHA-1 方面最终会是什么,你可以使用一个名为 rev-parse 的 Git 底层工具。你可以查阅 Git 内部原理 了解更多关于底层工具的信息;基本上,rev-parse 用于低级操作,不适合日常使用。然而,当你需要了解实际情况时,它有时会很有用。在这里,你可以在你的分支上运行 rev-parse

$ git rev-parse topic1
ca82a6dff817ec66f44342007202690a93763949

RefLog 短名称

当你工作时,Git 在后台做的事情之一是维护一个“reflog”——记录了你的 HEAD 和分支引用在过去几个月中的位置。

你可以使用 git reflog 查看你的 reflog

$ git reflog
734713b HEAD@{0}: commit: Fix refs handling, add gc auto, update tests
d921970 HEAD@{1}: merge phedders/rdocs: Merge made by the 'recursive' strategy.
1c002dd HEAD@{2}: commit: Add some blame and merge stuff
1c36188 HEAD@{3}: rebase -i (squash): updating HEAD
95df984 HEAD@{4}: commit: # This is a combination of two commits.
1c36188 HEAD@{5}: rebase -i (squash): updating HEAD
7e05da5 HEAD@{6}: rebase -i (pick): updating HEAD

每当你的分支尖端因任何原因更新时,Git 都会将该信息存储在这个临时历史记录中。你也可以使用你的 reflog 数据来引用旧的提交。例如,如果你想查看仓库 HEAD 的第五个先前值,你可以使用在 reflog 输出中看到的 @{5} 引用

$ git show HEAD@{5}

你也可以使用这种语法来查看分支在特定时间之前的位置。例如,要查看你的 master 分支昨天在哪里,你可以输入

$ git show master@{yesterday}

这将显示你的 master 分支尖端昨天在哪里。此技术仅适用于仍在你的 reflog 中的数据,因此你不能用它来查找几个月之前的提交。

要查看格式化为 git log 输出的 reflog 信息,你可以运行 git log -g

$ git log -g master
commit 734713bc047d87bf7eac9674765ae793478c50d3
Reflog: master@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: commit: Fix refs handling, add gc auto, update tests
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800

    Fix refs handling, add gc auto, update tests

commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Reflog: master@{1} (Scott Chacon <schacon@gmail.com>)
Reflog message: merge phedders/rdocs: Merge made by recursive.
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

需要注意的是,reflog 信息是严格本地的——它只记录了*你*在*你的*仓库中所做的事情。这些引用在其他人的仓库副本上不会相同;此外,在你最初克隆仓库后,你将拥有一个空的 reflog,因为你的仓库中尚未发生任何活动。运行 git show HEAD@{2.months.ago} 将只在你至少两个月前克隆项目时显示匹配的提交——如果你在此之后才克隆,你将只看到你的第一个本地提交。

提示
将 reflog 视为 Git 的 shell 历史版本

如果你有 UNIX 或 Linux 背景,你可以将 reflog 视为 Git 的 shell 历史版本,这强调了其中的内容显然只与你和你的“会话”相关,与可能在同一台机器上工作的其他人无关。

注意
在 PowerShell 中转义大括号

在使用 PowerShell 时,大括号如 {} 是特殊字符,必须进行转义。你可以用反引号 ` 转义它们,或者将提交引用放在引号中

$ git show HEAD@{0}     # will NOT work
$ git show HEAD@`{0`}   # OK
$ git show "HEAD@{0}"   # OK

祖先引用

指定提交的另一种主要方法是通过其祖先。如果你在引用末尾放置一个 ^(插入符号),Git 会将其解析为该提交的父级。假设你查看项目的历史记录

$ git log --pretty=format:'%h %s' --graph
* 734713b Fix refs handling, add gc auto, update tests
*   d921970 Merge commit 'phedders/rdocs'
|\
| * 35cfb2b Some rdoc changes
* | 1c002dd Add some blame and merge stuff
|/
* 1c36188 Ignore *.gem
* 9b29157 Add open3_detach to gemspec file list

然后,你可以通过指定 HEAD^ 来查看上一个提交,这意味着“HEAD 的父级”

$ git show HEAD^
commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Merge: 1c002dd... 35cfb2b...
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'
注意
在 Windows 上转义插入符号

在 Windows 的 cmd.exe 中,^ 是一个特殊字符,需要特殊处理。你可以将其加倍或将提交引用放在引号中

$ git show HEAD^     # will NOT work on Windows
$ git show HEAD^^    # OK
$ git show "HEAD^"   # OK

你也可以在 ^ 后面指定一个数字来标识你想要的*哪个*父级;例如,d921970^2 表示“d921970 的第二个父级”。这种语法仅适用于合并提交,合并提交有多个父级——合并提交的*第一个*父级来自你合并时所在的分支(通常是 master),而合并提交的*第二个*父级来自被合并的分支(比如 topic

$ git show d921970^
commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <schacon@gmail.com>
Date:   Thu Dec 11 14:58:32 2008 -0800

    Add some blame and merge stuff

$ git show d921970^2
commit 35cfb2b795a55793d7cc56a6cc2060b4bb732548
Author: Paul Hedderly <paul+git@mjr.org>
Date:   Wed Dec 10 22:22:03 2008 +0000

    Some rdoc changes

另一个主要的祖先规范是 ~(波浪号)。这也指的是第一个父级,所以 HEAD~HEAD^ 是等价的。当你指定一个数字时,差异就变得明显了。HEAD~2 表示“第一个父级的第一个父级”,或者“祖父级”——它遍历第一个父级的次数是你指定的次数。例如,在前面列出的历史记录中,HEAD~3 将是

$ git show HEAD~3
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date:   Fri Nov 7 13:47:59 2008 -0500

    Ignore *.gem

这也可以写成 HEAD~~~,同样是第一个父级的第一个父级的第一个父级

$ git show HEAD~~~
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <tom@mojombo.com>
Date:   Fri Nov 7 13:47:59 2008 -0500

    Ignore *.gem

你也可以组合这些语法——你可以通过使用 HEAD~3^2 等方式获取先前引用的第二个父级(假设它是一个合并提交)。

提交范围

既然你已经可以指定单个提交了,那么让我们看看如何指定提交范围。这对于管理你的分支特别有用——如果你有很多分支,你可以使用范围规范来回答诸如“这个分支上有什么工作我尚未合并到我的主分支中?”之类的问题。

双点

最常见的范围规范是双点语法。这基本上是要求 Git 解析一个提交可到达但另一个提交不可到达的提交范围。例如,假设你有一个提交历史记录,看起来像 范围选择的示例历史记录

Example history for range selection
图 136. 范围选择的示例历史记录

假设你想查看你的 experiment 分支中有哪些尚未合并到你的 master 分支中的内容。你可以要求 Git 只显示这些提交的日志,使用 master..experiment——这意味着“所有从 experiment 可达但从 master 不可达的提交。”为了这些示例的简洁和清晰,图中的提交对象字母在实际日志输出中按其显示顺序使用

$ git log master..experiment
D
C

另一方面,如果你想查看相反的内容——master 中有但 experiment 中没有的所有提交——你可以反转分支名称。experiment..master 显示 master 中所有从 experiment 不可达的内容

$ git log experiment..master
F
E

如果你想保持 experiment 分支最新并预览你即将合并的内容,这很有用。这种语法的另一个常见用途是查看你即将推送到远程的内容

$ git log origin/master..HEAD

此命令显示你的当前分支中不在你的 origin 远程的 master 分支中的任何提交。如果你运行 git push 并且你的当前分支正在跟踪 origin/master,那么 git log origin/master..HEAD 列出的提交是将被传输到服务器的提交。你也可以省略语法的一侧,让 Git 假定为 HEAD。例如,通过键入 git log origin/master..,你可以获得与前面示例相同的结果——如果缺少一侧,Git 会替换为 HEAD

多个点

双点语法作为简写很有用,但也许你想指定两个以上的分支来指示你的修订版本,例如查看在几个分支中的哪些提交不在你当前所在的分支中。Git 允许你通过在任何你不想看到可达提交的引用之前使用 ^ 字符或 --not 来实现。因此,以下三个命令是等效的

$ git log refA..refB
$ git log ^refA refB
$ git log refB --not refA

这很好,因为使用这种语法,你可以在查询中指定两个以上的引用,而双点语法无法做到这一点。例如,如果你想查看所有可从 refArefB 到达但不可从 refC 到达的提交,你可以使用以下任一命令

$ git log refA refB ^refC
$ git log refA refB --not refC

这使得修订查询系统非常强大,应该可以帮助你弄清楚你的分支中有什么。

三点

最后一个主要的范围选择语法是三点语法,它指定了可由*两个*引用中的*任一*到达但不能由两者同时到达的所有提交。回想一下 范围选择的示例历史记录 中的示例提交历史。如果你想查看 masterexperiment 中有什么但没有共同引用的内容,你可以运行

$ git log master...experiment
F
E
D
C

同样,这会给你正常的 log 输出,但只显示这四个提交的提交信息,按照传统的提交日期排序显示。

在这种情况下,与 log 命令一起使用的常见开关是 --left-right,它显示每个提交属于范围的哪一侧。这有助于使输出更有用

$ git log --left-right master...experiment
< F
< E
> D
> C

有了这些工具,你可以更轻松地让 Git 知道你想检查哪个或哪些提交。