章节 ▾ 第二版

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

一般来说,8 到 10 个字符足以保证在一个项目中的唯一性。 例如,截至 2019 年 2 月,Linux 内核(这是一个相当大的项目)有超过 875,000 个提交和近 700 万个对象在其对象数据库中,并且没有任何两个对象的前 12 个 SHA-1 字符是相同的。

注意
关于 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 年时间,该存储库才包含足够的对象,以至于有 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