章节 ▾ 第二版

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 百万亿亿。这相当于地球上沙粒数量的 1,200 倍。

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

scroll-to-top