[TOC]

7. Git 工具

7.1 Git 工具 - 选择修订版本

现在,你已经学习了管理或者维护 Git 仓库、实现代码控制所需的大多数日常命令和工作流程。 你已经尝试了跟踪和提交文件的基本操作,并且掌握了暂存区和轻量级地分支及合并的威力。
接下来你将学习一些 Git 的强大功能,这些功能你可能并不会在日常操作中使用,但在某些时候你可能会需要。

选择修订版本

Git 能够以多种方式来指定单个提交、一组提交、或者一定范围内的提交。 了解它们并不是必需的,但是了解一下总没坏处。

单个修订版本

你可以通过任意一个提交的 40 个字符的完整 SHA-1 散列值来指定它, 不过还有很多更人性化的方式来做同样的事情。本节将会介绍获取单个提交的多种方法。

简短的 SHA-1

Git 十分智能,你只需要提供 SHA-1 的前几个字符就可以获得对应的那次提交, 当然你提供的 SHA-1 字符数量不得少于 4 个,并且没有歧义——也就是说, 当前对象数据库中没有其它对象以这段 SHA-1 开头。
例如,要查看你知道其中添加了某个功能的提交,首先运行 git log 命令来定位该提交:

$ git log
commit 734713bc047d87bf7eac9674765ae793478c50d3
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800
    fixed refs handling, added gc auto, updated 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
    added some blame and merge stuff

在本例中,假设你想要的提交其 SHA-1 以 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 changed the version number
085bb3b removed unnecessary test code
a11bef0 first commit

通常 8 到 10 个字符就已经足够在一个项目中避免 SHA-1 的歧义。 例如,到 2019 年 2 月为止,Linux 内核这个相当大的 Git 项目, 其对象数据库中有超过 875,000 个提交,包含七百万个对象,也只需要前 12 个字符就能保证唯一性。

Note 关于 SHA-1 的简短说明
许多人觉得他们的仓库里有可能出现两个不同的对象其 SHA-1 值相同。 然后呢?
如果你真的向仓库里提交了一个对象,它跟之前的某个 不同 对象的 SHA-1 值相同, Git 会发现该对象的散列值已经存在于仓库里了,于是就会认为该对象被写入,然后直接使用它。 如果之后你想检出那个对象时,你将得到先前那个对象的数据。
但是这种情况发生的概率十分渺小。 SHA-1 摘要长度是 20 字节,也就是 160 位。 2^80 个随机哈希对象才有 50% 的概率出现一次冲突 (计算冲突机率的公式是 p = (n(n-1)/2) * (1/2^160)) )。 2^80 是 1.2 x 10^24,也就是一亿亿亿,这是地球上沙粒总数的 1200 倍。
举例说一下怎样才能产生一次 SHA-1 冲突。 如果地球上 65 亿个人类都在编程,每人每秒都在产生等价于整个 Linux 内核历史(650 万个 Git 对象)的代码, 并将之提交到一个巨大的 Git 仓库里面,这样持续两年的时间才会产生足够的对象, 使其拥有 50% 的概率产生一次 SHA-1 对象冲突, 这比你编程团队的成员同一个晚上在互不相干的意外中被狼袭击并杀死的机率还要小。

分支引用

引用特定提交的一种直接方法是,若它是一个分支的顶端的提交, 那么可以在任何需要引用该提交的 Git 命令中直接使用该分支的名称。 例如,你想要查看一个分支的最后一次提交的对象,假设 topic1 分支指向提交 ca82a6d... , 那么以下的命令是等价的:

$ git show ca82a6dff817ec66f44342007202690a93763949
$ git show topic1

如果你想知道某个分支指向哪个特定的 SHA-1,或者想看任何一个例子中被简写的 SHA-1, 你可以使用一个叫做 rev-parse 的 Git 探测工具。 你可以在 Git 内部原理 中查看更多关于探测工具的信息。 简单来说,rev-parse 是为了底层操作而不是日常操作设计的。 不过,有时你想看 Git 现在到底处于什么状态时,它可能会很有用。 你可以在你的分支上执行 rev-parse

$ git rev-parse topic1
ca82a6dff817ec66f44342007202690a93763949

引用日志

当你在工作时, Git 会在后台保存一个引用日志(reflog), 引用日志记录了最近几个月你的 HEAD 和分支引用所指向的历史。
你可以使用 git reflog 来查看引用日志

$ git reflog
734713b HEAD@{0}: commit: fixed refs handling, added gc auto, updated
d921970 HEAD@{1}: merge phedders/rdocs: Merge made by the 'recursive' strategy.
1c002dd HEAD@{2}: commit: added 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

每当你的 HEAD 所指向的位置发生了变化,Git 就会将这个信息存储到引用日志这个历史记录里。 你也可以通过 reflog 数据来获取之前的提交历史。 如果你想查看仓库中 HEAD 在五次前的所指向的提交,你可以使用 @{n} 来引用 reflog 中输出的提交记录。

$ git show HEAD@{5}

你同样可以使用这个语法来查看某个分支在一定时间前的位置。 例如,查看你的 master 分支在昨天的时候指向了哪个提交,你可以输入

$ git show master@{yesterday}

就会显示昨天 master 分支的顶端指向了哪个提交。 这个方法只对还在你引用日志里的数据有用,所以不能用来查好几个月之前的提交。
可以运行 git log -g 来查看类似于 git log 输出格式的引用日志信息:

$ git log -g master
commit 734713bc047d87bf7eac9674765ae793478c50d3
Reflog: master@{0} (Scott Chacon <schacon@gmail.com>)
Reflog message: commit: fixed refs handling, added gc auto, updated
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Jan 2 18:32:33 2009 -0800
    fixed refs handling, added gc auto, updated 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'

值得注意的是,引用日志只存在于本地仓库,它只是一个记录你在 自己 的仓库里做过什么的日志。 其他人拷贝的仓库里的引用日志不会和你的相同,而你新克隆一个仓库的时候,引用日志是空的,因为你在仓库里还没有操作。 git show HEAD@{2.months.ago} 这条命令只有在你克隆了一个项目至少两个月时才会显示匹配的提交—— 如果你刚刚克隆了仓库,那么它将不会有任何结果返回。

Tip 将引用日志想作 Git 版的 shell 历史记录
如果你有 UNIX 或者 Linux 的背景,不妨将引用日志想作 Git 版的 shell 历史记录, 重点在于仅与你和你的会话相关,而与他人无关。

祖先引用

祖先引用是另一种指明一个提交的方式。 如果你在引用的尾部加上一个 ^(脱字符), Git 会将其解析为该引用的上一个提交。 假设你的提交历史是:

$ git log --pretty=format:'%h %s' --graph
* 734713b fixed refs handling, added gc auto, updated tests
*   d921970 Merge commit 'phedders/rdocs'
|\
| * 35cfb2b Some rdoc changes
* | 1c002dd added 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'

| Note | 在 Windows 上转义脱字符
在 Windows 的 cmd.exe 中,^ 是一个特殊字符,因此需要区别对待。 你可以双写它或者将提交引用放在引号中:``` $ git show HEAD^ # 在 Windows 上无法工作 $ git show HEAD^^ # 可以 $ git show “HEAD^” # 可以

 |
| --- | --- |

你也可以在 `^` 后面添加一个数字来指明想要 **哪一个** 父提交——例如 `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 added 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` 代表“第一父提交的第一父提交”,也就是“祖父提交”——Git 会根据你指定的次数获取对应的第一父提交。 例如,在之前的列出的提交历史中,`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` 来取得之前引用的第二父提交(假设它是一个合并提交)。
<a name="AW4r3"></a>
#### 提交区间
你已经学会如何单次的提交,现在来看看如何指明一定区间的提交。 当你有很多分支时,这对管理你的分支时十分有用, 你可以用提交区间来解决“这个分支还有哪些提交尚未合并到主分支?”的问题
<a name="WOP8C"></a>
##### 双点
最常用的指明提交区间语法是双点。 这种语法可以让 Git 选出在一个分支中而不在另一个分支中的提交。 例如,你有如下的提交历史 [Example history for range selection.](https://git-scm.com/book/zh/v2/ch00/double_dot)<br />![](https://cdn.nlark.com/yuque/0/2020/png/278385/1592923804258-afb0a7b6-a920-4af7-b5d8-c8ca9a619b2a.png#align=left&display=inline&height=148&margin=%5Bobject%20Object%5D&originHeight=148&originWidth=800&size=0&status=done&style=none&width=800)<br />Figure 137. Example history for range selection.<br />你想要查看 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` 中的提交。 如果你执行 `git push` 并且你的当前分支正在跟踪 `origin/master`,由 `git log origin/master..HEAD` 所输出的提交就是会被传输到远端服务器的提交。如果你留空了其中的一边, Git 会默认为 `HEAD`。 例如, `git log origin/master..` 将会输出与之前例子相同的结果 —— Git 使用 HEAD 来代替留空的一边。
<a name="kZGOk"></a>
##### 多点
双点语法很好用,但有时候你可能需要两个以上的分支才能确定你所需要的修订, 比如查看哪些提交是被包含在某些分支中的一个,但是不在你当前的分支上。 Git 允许你在任意引用前加上 `^` 字符或者 `--not` 来指明你不希望提交被包含其中的分支。 因此下列三个命令是等价的:

$ git log refA..refB $ git log ^refA refB $ git log refB —not refA

这个语法很好用,因为你可以在查询中指定超过两个的引用,这是双点语法无法实现的。 比如,你想查看所有被 `refA` 或 `refB` 包含的但是不被 `refC` 包含的提交,你可以使用以下任意一个命令:

$ git log refA refB ^refC $ git log refA refB —not refC

这就构成了一个十分强大的修订查询系统,你可以通过它来查看你的分支里包含了哪些东西。
<a name="wrHjx"></a>
##### 三点
最后一种主要的区间选择语法是三点,这个语法可以选择出被两个引用 **之一** 包含但又不被两者同时包含的提交。 再看看之前双点例子中的提交历史。 如果你想看 `master` 或者 `experiment` 中包含的但不是两者共有的提交,你可以执行:

$ git log master…experiment F E D C

这和通常 `log` 按日期排序的输出一样,仅仅给出了4个提交的信息。<br />这种情形下,`log` 命令的一个常用参数是 `--left-right`,它会显示每个提交到底处于哪一侧的分支。 这会让输出数据更加清晰。

$ git log —left-right master…experiment < F < E

D C

有了这些工具,你就可以十分方便地查看你 Git 仓库中的提交。
<a name="TnrY5"></a>
## 7.2 Git 工具 - 交互式暂存
<a name="9qwgf"></a>
### 交互式暂存
本节中的几个交互式 Git 命令可以帮助你将文件的特定部分组合成提交。 当你在修改了大量文件后,希望这些改动能拆分为若干提交而不是混杂在一起成为一个提交时,这几个工具会非常有用。 通过这种方式,可以确保提交是逻辑上独立的变更集,同时也会使其他开发者在与你工作时很容易地审核。 如果运行 `git add` 时使用 `-i` 或者 `--interactive` 选项,Git 将会进入一个交互式终端模式,显示类似下面的东西:

$ git add -i staged unstaged path 1: unchanged +0/-1 TODO 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb Commands 1: [s]tatus 2: [u]pdate 3: [r]evert 4: [a]dd untracked 5: [p]atch 6: [d]iff 7: [q]uit 8: [h]elp What now>

可以看到这个命令以和平时非常不同的视图显示了暂存区——基本上与 `git status` 是相同的信息,但是更简明扼要一些。 它将暂存的修改列在左侧,未暂存的修改列在右侧。<br />在这块区域后是“Commands”命令区域。 在这里你可以做一些工作,包括暂存文件、取消暂存文件、暂存文件的一部分、添加未被追踪的文件、显示暂存内容的区别。
<a name="NmvSf"></a>
#### 暂存与取消暂存文件
如果在 `What now>` 提示符后键入 `u` 或 `2`(更新),它会问你想要暂存哪个文件:

What now> u staged unstaged path 1: unchanged +0/-1 TODO 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb Update>>

要暂存 `TODO` 和 `index.html` 文件,可以输入数字:

Update>> 1,2 staged unstaged path

  • 1: unchanged +0/-1 TODO
  • 2: unchanged +1/-1 index.html 3: unchanged +5/-1 lib/simplegit.rb Update>>
    每个文件前面的 `*` 意味着选中的文件将会被暂存。 如果在 `Update>>` 提示符后不输入任何东西并直接按回车,Git 将会暂存之前选择的文件:
    
    Update>> updated 2 paths Commands 1: [s]tatus 2: [u]pdate 3: [r]evert 4: [a]dd untracked 5: [p]atch 6: [d]iff 7: [q]uit 8: [h]elp What now> s
         staged     unstaged path
    
    1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb
    现在可以看到 `TODO` 与 `index.html` 文件已经被暂存而 `simplegit.rb` 文件还未被暂存。 如果这时想要取消暂存 TODO 文件,使用 `r` 或 `3`(撤消)选项:
    
    Commands 1: [s]tatus 2: [u]pdate 3: [r]evert 4: [a]dd untracked 5: [p]atch 6: [d]iff 7: [q]uit 8: [h]elp What now> r
         staged     unstaged path
    
    1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb Revert>> 1
         staged     unstaged path
    
  • 1: +0/-1 nothing TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb Revert>> [enter] reverted one path
    再次查看 Git 状态,可以看到已经取消暂存 `TODO` 文件:
    
    Commands 1: [s]tatus 2: [u]pdate 3: [r]evert 4: [a]dd untracked 5: [p]atch 6: [d]iff 7: [q]uit 8: [h]elp What now> s
         staged     unstaged path
    
    1: unchanged +0/-1 TODO 2: +1/-1 nothing index.html 3: unchanged +5/-1 lib/simplegit.rb
    如果想要查看已暂存内容的区别,可以使用 `d` 或 `6`(区别)命令。 它会显示暂存文件的一个列表,可以从中选择想要查看的暂存区别。 这跟你在命令行指定 `git diff --cached` 非常相似:
    
    Commands 1: [s]tatus 2: [u]pdate 3: [r]evert 4: [a]dd untracked 5: [p]atch 6: [d]iff 7: [q]uit 8: [h]elp What now> d
         staged     unstaged path
    
    1: +1/-1 nothing index.html Review diff>> 1 diff —git a/index.html b/index.html index 4d07108..4335f49 100644 —- a/index.html +++ b/index.html @@ -16,7 +16,7 @@ Date Finder

    - +