Git
章节 ▾ 第二版

7.1 Git 工具 - 修订选择

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

现在,您将探索 Git 可以执行的许多强大的功能,这些功能可能并非日常使用,但您可能在某些时候需要它们。

修订选择

Git 允许您以多种方式引用单个提交、一组提交或提交范围。它们并非显而易见,但值得了解。

单个修订

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

简短 SHA-1

Git 足够智能,可以识别您要引用的提交,即使您只提供 SHA-1 哈希值的前几个字符,只要该部分哈希值至少有四个字符并且是唯一的;也就是说,对象数据库中没有其他对象可以具有以相同前缀开头的哈希值。

例如,要检查您知道已添加某些功能的特定提交,您可能首先运行 git log 命令来找到该提交

$ git log
commit 734713bc047d87bf7eac9674765ae793478c50d3
Author: Scott Chacon <[email protected]>
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 <[email protected]>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <[email protected]>
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 值的简短、唯一的缩写。如果您向 git log 命令传递 --abbrev-commit,则输出将使用较短的值,但会保持它们唯一;默认情况下使用七个字符,但如果需要则会延长它们以保持 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,也就是 100 万亿亿。这相当于地球上沙粒数量的 1200 倍。

以下示例可以帮助你了解实现 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 <[email protected]>)
Reflog message: commit: Fix refs handling, add gc auto, update tests
Author: Scott Chacon <[email protected]>
Date:   Fri Jan 2 18:32:33 2009 -0800

    Fix refs handling, add gc auto, update tests

commit d921970aadf03b3cf0e71becdaab3147ba71cdef
Reflog: master@{1} (Scott Chacon <[email protected]>)
Reflog message: merge phedders/rdocs: Merge made by recursive.
Author: Scott Chacon <[email protected]>
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 <[email protected]>
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 <[email protected]>
Date:   Thu Dec 11 14:58:32 2008 -0800

    Add some blame and merge stuff

$ git show d921970^2
commit 35cfb2b795a55793d7cc56a6cc2060b4bb732548
Author: Paul Hedderly <[email protected]>
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 <[email protected]>
Date:   Fri Nov 7 13:47:59 2008 -0500

    Ignore *.gem

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

$ git show HEAD~~~
commit 1c3618887afb5fbcbea25b7c013f4e2114448b8d
Author: Tom Preston-Werner <[email protected]>
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