章节 ▾ 第二版

7.1 Git 工具 - 修订版本选择

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

现在,你将探索 Git 的一些非常强大的功能。你可能不会在日常工作中频繁使用它们,但在某些时候可能会用到。

修订版本选择

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

单个修订版本

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

短 SHA-1

Git 非常智能,如果你提供 SHA-1 哈希的前几个字符,它就能识别出你指的是哪个提交,前提是该部分哈希至少有四个字符长且没有歧义;也就是说,在对象数据库中没有其他对象的哈希以相同的前缀开头。

例如,要查看你添加了特定功能的某个提交,你可以先运行 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,即 10 亿亿亿。这相当于地球上沙子数量的 1,200 倍。

这里有一个例子让你了解发生 SHA-1 碰撞需要什么条件。如果地球上所有 65 亿人都在编程,并且每秒钟每个人都在生产相当于整个 Linux 内核历史(650 万个 Git 对象)的代码,并将其推送到一个巨大的 Git 仓库中,大约需要 2 年时间,该仓库才会包含足够的对象,从而有 50% 的概率出现单一的 SHA-1 对象碰撞。因此,发生自然的 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 知道你想检查哪个或哪些提交。