Git之深入解析如何选择修订的版本

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Git之深入解析如何选择修订的版本相关的知识,希望对你有一定的参考价值。

一、前言

二、简短的 SHA-1

  • Git 十分智能,只需要提供 SHA-1 的前几个字符就可以获得对应的那次提交,当然提供的 SHA-1 字符数量不得少于 4 个,并且没有歧义。也就是说, 当前对象数据库中没有其它对象以这段 SHA-1 开头。
  • 例如,要查看知道其中添加了某个功能的提交,首先运行 git log 命令来定位该提交:
$ git log
commit 734713bc047d87bf7eac9674765ae793478c50d3
Author: Scott Chacon <kody@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 <kody@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'

commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <kody@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 个字符就能保证唯一性。
  • 许多人觉得仓库里有可能出现两个不同的对象其 SHA-1 值相同,然后呢?
    • 如果真的向仓库里提交了一个对象,它跟之前的某个不同对象的 SHA-1 值相同,Git 会发现该对象的散列值已经存在于仓库里了,于是就会认为该对象被写入,然后直接使用它。如果之后想检出那个对象时,将得到先前那个对象的数据。
    • 但是这种情况发生的概率十分渺小,SHA-1 摘要长度是 20 字节,也就是 160 位,280 个随机哈希对象才有 50% 的概率出现一次冲突 (计算冲突机率的公式是 p = (n(n-1)/2) * (1/2160)) )。 280 是 1.2 x 1024,也就是一亿亿亿,这是地球上沙粒总数的 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 探测工具。简单来说,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} 这条命令只有在克隆了一个项目至少两个月时才会显示匹配的提交,如果刚刚克隆了仓库,那么它将不会有任何结果返回。

五、祖先引用

  • 祖先引用是另一种指明一个提交的方式,如果在引用的尾部加上一个 ^(脱字符),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 <kody@gmail.com>
Date:   Thu Dec 11 15:08:43 2008 -0800

    Merge commit 'phedders/rdocs'
  • 也可以在 ^ 后面添加一个数字来指明想要哪一个父提交。例如 d921970^2 代表 “d921970 的第二父提交” 这个语法只适用于合并的提交,因为合并提交会有多个父提交。合并提交的第一父提交是你合并时所在分支(通常为 master),而第二父提交是你所合并的分支(例如 topic):
$ git show d921970^
commit 1c002dd4b536e7479fe34593e72e6c6c1819e53b
Author: Scott Chacon <kody@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 来取得之前引用的第二父提交(假设它是一个合并提交)。

六、提交区间

  • 了解了如何指定单次的提交,现在来看看如何指明一定区间的提交。当有很多分支时,这对管理分支十分有用, 可以用提交区间来解决“这个分支还有哪些提交尚未合并到主分支?”的问题。
  • 最常用的指明提交区间语法是双点,这种语法可以让 Git 选出在一个分支中而不在另一个分支中的提交。例如,有如下的提交历史:

  • 想要查看 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 来代替留空的一边。

七、多点

  • 双点语法很好用,但有时候可能需要两个以上的分支才能确定所需要的修订,比如查看哪些提交是被包含在某些分支中的一个,但是不在当前的分支上。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
  • 这就构成了一个十分强大的修订查询系统,可以通过它来查看分支里包含了哪些东西。

八、三点

  • 最后一种主要的区间选择语法是三点,这个语法可以选择出被两个引用之一包含但又不被两者同时包含的提交。再看看之前双点例子中的提交历史,如果想看 master 或者 experiment 中包含的但不是两者共有的提交,可以执行:
$ git log master...experiment
F
E
D
C
  • 这和通常 log 按日期排序的输出一样,仅仅给出了4个提交的信息。这种情形下,log 命令的一个常用参数是 --left-right,它会显示每个提交到底处于哪一侧的分支,这会让输出数据更加清晰。
$ git log --left-right master...experiment
< F
< E
> D
> C

以上是关于Git之深入解析如何选择修订的版本的主要内容,如果未能解决你的问题,请参考以下文章

Git之深入解析如何使用Git的分布式工作流程与如何管理多人开发贡献的项目

Git之深入解析如何解决.git目录过大的问题

Git内部原理之深入解析传输协议

Git之深入解析如何重写提交历史

Git之深入解析如何交互式暂存

Git之深入解析如何替换数据库中的Git对象