章节 ▾ 第二版

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,即一百万亿亿。这相当于地球上沙粒数量的 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 Internals 来获取更多关于管道工具的信息;基本上,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 中转义插入符

cmd.exe 的 Windows 中,^ 是一个特殊字符,需要不同对待。您可以将其加倍,或者将提交引用放在引号中

$ 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 分支。您可以通过 master..experiment 来要求 Git 只显示那些提交的日志——这意味着“从 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 知道您想要检查哪个或哪些提交。