添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

有很多工具可以研究和分析Git的历史提交,在前面的实践中已经用过很多相关的Git命令进行查看历史提交、查看文件的历史版本、进行差异比较等。本章除了对之前用到的相关Git命令作以总结外,还要再介绍几款图形化的客户端。

2.8.1. 图形工具:gitk

gitk是最早实现的一个图形化的Git版本库浏览器软件,基于tcl/tk实现,因此gitk非常简洁,本身就是一个1万多行的tcl脚本写成的。gitk的代码已经和Git的代码放在同一个版本库中,gitk随Git一同发布,不用特别的安装即可运行。gitk可以显示提交的分支图,可以显示提交,文件,版本间差异等。

在版本库中调用gitk,就会浏览该版本库,显示其提交分支图。gitk可以像命令行工具一样使用不同的参数进行调用。

  • 显示所有的分支。

    $ gitk --all
    
  • 显示2周以来的提交。

    $ gitk --since="2 weeks ago"
    
  • 显示某个里程碑(v2.6.12)以来,针对某些目录和文件(include/scsi目录和drivers/scsi目录)的提交。

    $ gitk v2.6.12.. include/scsi drivers/scsi
    
  • 灰色的stash
  • gitk使用tcl/tk开发,在显示上没有系统中原生图形应用那么漂亮的界面,甚至可以用丑陋来形容,下面介绍的gitg和qgit在易用性上比gitk进步了不少。

    2.8.2. 图形工具:gitg

    gitg是使用GTK+图形库实现的一个Git版本库浏览器软件。Linux下最著名的Gnome桌面环境使用的就是GTK+,因此在Linux下gitg有着非常漂亮的原生的图形界面。gitg不但能够实现gitk的全部功能,即浏览提交历史和文件,还能帮助执行提交。

    在Linux上安装gitg很简单,例如在Debian或Ubuntu上,直接运行下面的命令就可以进行安装。

    $ sudo aptitude install gitg
    

    安装完毕就可以在可执行路径中找到gitg

    $ which gitg
    /usr/bin/gitg
    

    为了演示gitg具备提交功能,先在工作区作出一些修改。

  • 删除没有用到的hello.h文件。

    $ cd /path/to/my/workspace/demo
    $ rm src/hello.h
    
  • README文件后面追加一行。

    $ echo "Wait..." >> README
    
  • 当前工作区的状态。

    $ git status -s
     M README
     D src/hello.h
    

    gitg还是一个比较新的项目,在本文撰写的时候,gitg才是0.0.6版本,相比下面要介绍的qgit还缺乏很多功能。例如gitg没有文件的blame(追溯)界面,也不能直接将文件检出,但是gitg整体的界面风格,以及易用的提交界面给人的印象非常深刻。

    2.8.3. 图形工具:qgit

    前面介绍的gitg是基于GTK+这一Linux标准的图形库,那么也许有读者已经猜到qgit是使用Linux另外一个著名的图形库QT实现的Git版本库浏览器软件。QT的知名度不亚于GTK+,是著名的KDE桌面环境用到的图形库,也是蓄势待发准备和Android一较高低的MeeGo的UI核心。qgit目前的版本是2.3,相比前面介绍的gitg其经历的开发周期要长了不少,因此也提供了更多的功能。

    在Linux上安装qgit很简单,例如在Debian或Ubuntu上,直接运行下面的命令就可以进行安装。

    $ sudo aptitude install qgit
    

    安装完毕就可以在可执行路径中找到qgit

    $ which qgit
    /usr/bin/qgit
    

    qgitgitg一样不但能够浏览提交历史和文件,还能帮助执行提交。为了测试提交,将在上一节所做的提交回滚。

  • 使用重置命令回滚最后一次提交。

    $ git reset HEAD^
    Unstaged changes after reset:
    M       README
    M       src/hello.h
    
  • 当前工作区的状态。

    $ git status
    # On branch master
    # Changed but not updated:
    #   (use "git add/rm <file>..." to update what will be committed)
    #   (use "git checkout -- <file>..." to discard changes in working directory)
    #       modified:   README
    #       deleted:    src/hello.h
    no changes added to commit (use "git add" and/or "git commit -a")
    
  • 绿色的master分支。
  • 黄色的hello_1.0old_practice里程碑。
  • 灰色的stash标签,显示在了创建时候的位置,并其包含的针对暂存区状态的提交也显示出来。
  • 最顶端显示一行绿色背景的文件:工作区有改动。
  • qgit的右键菜单非常丰富,上图显示了鼠标右击提交时显示的弹出菜单,可以创建、切换标签或分支,可以将提交导出为补丁文件。

    点击qgit右下方变更文件列表窗口,可以选择将文件检出或者直接查看。

    2.8.4. 命令行工具

    上面介绍的几款图形界面的Git版本库浏览器最大的特色就是更好看的提交关系图,还能非常方便的浏览历史提交的目录树,并从历史提交的目录树中提取文件等。这些操作对于Git命令行同样可以完成。使用Git命令行探索版本库历史对于读者来说并不新鲜,因为在前几章的实践中已经用到了相关命令,展示了对历史记录的操作。本节对这些命令的部分要点进行强调和补充。

    前面历次实践的提交基本上是线性的提交,研究起来没有挑战性。为了能够更加接近于实际又不失简洁,我构造了一个版本库,放在了Github上。可以通过如下操作在本地克隆这个示例版本库。

    $ cd /path/to/my/workspace/
    $ git clone git://github.com/ossxp-com/gitdemo-commit-tree.git
    Cloning into gitdemo-commit-tree...
    remote: Counting objects: 63, done.
    remote: Compressing objects: 100% (51/51), done.
    remote: Total 63 (delta 8), reused 0 (delta 0)
    Receiving objects: 100% (63/63), 65.95 KiB, done.
    Resolving deltas: 100% (8/8), done.
    $ cd gitdemo-commit-tree
    

    运行gitg命令,显示其提交关系图。

    是不是有点“乱花渐欲迷人眼”的感觉。如果把提交用里程碑标识的圆圈来代表,稍加排列就会看到下面的更为直白的提交关系图。

    Git的大部分命令可以使用提交版本作为参数(如:git diff <commit-id>),有的命令则使用一个版本范围作为参数(如:git log <rev1>..<rev2>)。Git的提交有着各式各样的表示法,提交范围也是一样,下面就通过两个命令git rev-parsegit rev-list分别研究一下Git的版本表示法和版本范围表示法。

    2.8.4.1. 版本表示法:git rev-parse

    命令git rev-parse是Git的一个底层命令,其功能非常丰富(或者说杂乱),很多Git脚本或工具都会用到这条命令。

    此命令的部分应用在“Git初始化”章节中就已经看到。例如可以显示Git版本库的位置(--git-dir),当前工作区目录的深度(--show-cdup),甚至可以用于被Git无关应用用于解析命令行参数(--parseopt)。

    此命令可以显示当前版本库中的引用。

  • 显示分支。

    $ git rev-parse --symbolic --branches
    
  • 显示里程碑。

    $ git rev-parse --symbolic --tags
    
  • 显示定义的所有引用。

    其中refs/remotes/目录下的引用成为远程分支(或远程引用),在后面的章节会予以介绍。

    $ git rev-parse --symbolic --glob=refs/*
    refs/heads/master
    refs/remotes/origin/HEAD
    refs/remotes/origin/master
    refs/tags/A
    refs/tags/B
    refs/tags/C
    refs/tags/D
    refs/tags/E
    refs/tags/F
    refs/tags/G
    refs/tags/H
    refs/tags/I
    refs/tags/J
    

    命令git rev-parse另外一个重要的功能就是将一个Git对象表达式表示为对应的SHA1哈希值。针对本节开始克隆的版本库gitdemo-commit-tree,做如下操作。

  • 显示HEAD对应的SHA1哈希值。

    $ git rev-parse  HEAD
    6652a0dce6a5067732c00ef0a220810a7230655e
    
  • 命令git describe的输出也可以显示为SHA1哈希值。

    $ git describe
    A-1-g6652a0d
    $ git rev-parse A-1-g6652a0d
    6652a0dce6a5067732c00ef0a220810a7230655e
    
  • 可以同时显示多个表达式的SHA1哈希值。

    下面的操作可以看出master和refs/heads/master都可以用于指代master分支。

    $ git rev-parse  master  refs/heads/master
    6652a0dce6a5067732c00ef0a220810a7230655e
    6652a0dce6a5067732c00ef0a220810a7230655e
    
  • 可以用哈希值的前几位指代整个哈希值。

    $ git rev-parse  6652  6652a0d
    6652a0dce6a5067732c00ef0a220810a7230655e
    6652a0dce6a5067732c00ef0a220810a7230655e
    
  • 里程碑的两种表示法均指向相同的对象。

    里程碑对象不一定是提交,有可能是一个Tag对象。Tag对象包含说明或者签名,还包括到对应提交的指向。

    $ git rev-parse  A  refs/tags/A
    c9b03a208288aebdbfe8d84aeb984952a16da3f2
    c9b03a208288aebdbfe8d84aeb984952a16da3f2
    
  • 里程碑A指向了一个Tag对象而非提交的时候,用下面的三个表示法都可以指向里程碑对应的提交。

    实际上下面的语法也可以直接作用于轻量级里程碑(直接指向提交的里程碑)或者作用于提交本身。

    $ git rev-parse  A^{}  A^0  A^{commit}
    81993234fc12a325d303eccea20f6fd629412712
    81993234fc12a325d303eccea20f6fd629412712
    81993234fc12a325d303eccea20f6fd629412712
    
  • A的第一个父提交就是B所指向的提交。

    回忆之前的介绍,^操作符代表着父提交。当一个提交有多个父提交时,可以通过在符号^后面跟上一个数字表示第几个父提交。A^ 就相当于 A^1。而B^0代表了B所指向的一个Commit对象(因为B是Tag对象)。

    $ git rev-parse  A^  A^1  B^0
    776c5c9da9dcbb7e463c061d965ea47e73853b6e
    776c5c9da9dcbb7e463c061d965ea47e73853b6e
    776c5c9da9dcbb7e463c061d965ea47e73853b6e
    
  • 更为复杂的表示法。

    连续的^符号依次沿着父提交进行定位至某一祖先提交。^后面的数字代表该提交的第几个父提交。

    $ git rev-parse  A^^3^2  F^2  J^{}
    3252fcce40949a4a622a1ac012cb120d6b340ac8
    3252fcce40949a4a622a1ac012cb120d6b340ac8
    3252fcce40949a4a622a1ac012cb120d6b340ac8
    
  • 记号~<n>就相当于连续<n>个符号^

    $ git rev-parse  A~3  A^^^  G^0
    e80aa7481beda65ae00e35afc4bc4b171f9b0ebf
    e80aa7481beda65ae00e35afc4bc4b171f9b0ebf
    e80aa7481beda65ae00e35afc4bc4b171f9b0ebf
    
  • 显示里程碑A对应的目录树。下面两种写法都可以。

    $ git rev-parse  A^{tree}  A:
    95ab9e7db14ca113d5548dc20a4872950e8e08c0
    95ab9e7db14ca113d5548dc20a4872950e8e08c0
    
  • 显示树里面的文件,下面两种表示法均可。

    $ git rev-parse  A^{tree}:src/Makefile  A:src/Makefile
    96554c5d4590dbde28183e9a6a3199d526eeb925
    96554c5d4590dbde28183e9a6a3199d526eeb925
    
  • 暂存区里的文件和HEAD中的文件相同。

    $ git rev-parse  :gitg.png  HEAD:gitg.png
    fc58966ccc1e5af24c2c9746196550241bc01c50
    fc58966ccc1e5af24c2c9746196550241bc01c50
    
  • 还可以通过在提交日志中查找字串的方式显示提交。

    $ git rev-parse :/"Commit A"
    81993234fc12a325d303eccea20f6fd629412712
    
  • 再有就是reflog相关的语法,参见“Git重置”章节中关于reflog的介绍。

    $ git rev-parse HEAD@{0} master@{0}
    6652a0dce6a5067732c00ef0a220810a7230655e
    6652a0dce6a5067732c00ef0a220810a7230655e
    
  • 一个提交ID实际上就可以代表一个版本列表。含义是:该版本开始的所有历史提交。

    $ git rev-list --oneline  A
    8199323 Commit A: merge B with C.
    0cd7f2e commit C.
    776c5c9 Commit B: merge D with E and F
    beb30ca Commit F: merge I with J
    212efce Commit D: merge G with H
    634836c commit I.
    3252fcc commit J.
    83be369 commit E.
    2ab52ad commit H.
    e80aa74 commit G.
    
  • 两个或多个版本,相当于每个版本单独使用时指代的列表的并集。

    $ git rev-list --oneline  D  F
    beb30ca Commit F: merge I with J
    212efce Commit D: merge G with H
    634836c commit I.
    3252fcc commit J.
    2ab52ad commit H.
    e80aa74 commit G.
    
  • 在一个版本前面加上符号(^)含义是取反,即排除这个版本及其历史版本。

    $ git rev-list --oneline  ^G D
    212efce Commit D: merge G with H
    2ab52ad commit H.
    
  • 和上面等价的“点点”表示法。使用两个点连接两个版本,如G..D,就相当于^G D

    $ git rev-list --oneline  G..D
    212efce Commit D: merge G with H
    2ab52ad commit H.
    
  • 版本取反,参数的顺序不重要,但是“点点”表示法前后的版本顺序很重要。

  • 语法:^B C

    $ git rev-list --oneline  ^B C
    0cd7f2e commit C.
    
  • 语法:C ^B

    $ git rev-list --oneline  C ^B
    0cd7f2e commit C.
    
  • 语法:B..C相当于^B C

    $ git rev-list --oneline  B..C
    0cd7f2e commit C.
    
  • 语法:C..B相当于^C B

    $ git rev-list --oneline  C..B
    776c5c9 Commit B: merge D with E and F
    212efce Commit D: merge G with H
    83be369 commit E.
    2ab52ad commit H.
    e80aa74 commit G.
    
  • 三点表示法,两个版本的前后顺序没有关系。

    实际上r1...r2相当于r1 r2 --not $(git merge-base --all r1 r2),和顺序无关。

    $ git rev-list --oneline  C...B
    0cd7f2e commit C.
    776c5c9 Commit B: merge D with E and F
    212efce Commit D: merge G with H
    83be369 commit E.
    2ab52ad commit H.
    e80aa74 commit G.
    
  • 某提交的历史提交,自身除外,用语法r1^@表示。

    $ git rev-list --oneline  B^@
    beb30ca Commit F: merge I with J
    212efce Commit D: merge G with H
    634836c commit I.
    3252fcc commit J.
    83be369 commit E.
    2ab52ad commit H.
    e80aa74 commit G.
    
  • 提交本身不包括其历史提交,用语法r1^!表示。

    $ git rev-list --oneline  B^!
    776c5c9 Commit B: merge D with E and F
    $ git rev-list --oneline  F^! D
    beb30ca Commit F: merge I with J
    212efce Commit D: merge G with H
    2ab52ad commit H.
    

    命令git log是老朋友了,在前面的章节中曾经大量的出现,用于显示提交历史。

    参数代表版本范围

    当不使用任何参数调用,相当于使用了缺省的参数HEAD,即显示当前HEAD能够访问到的所有历史提交。还可以使用上面介绍的版本范围表示法,例如:

    $ git log --oneline F^! D
    beb30ca Commit F: merge I with J
    212efce Commit D: merge G with H
    2ab52ad commit H.
    e80aa74 commit G.
    

    分支图显示

    通过--graph参数调用git log可以显示字符界面的提交关系图,而且不同的分支还可以用不同的颜色来表示。如果希望每次查看日志的时候都看到提交关系图,可以设置一个别名,用别名来调用。

    $ git config --global alias.glog "log --graph"
    

    定义别名之后,每次希望自动显示提交关系图,就可以使用别名命令:

    $ git glog --oneline
    * 6652a0d Add Images for git treeview.
    *   8199323 Commit A: merge B with C.
    | * 0cd7f2e commit C.
    *-. \   776c5c9 Commit B: merge D with E and F
    |\ \ \
    | | |/
    | | *   beb30ca Commit F: merge I with J
    | | |\
    | | | * 3252fcc commit J.
    | | * 634836c commit I.
    | * 83be369 commit E.
    *   212efce Commit D: merge G with H
    | * 2ab52ad commit H.
    * e80aa74 commit G.
    

    显示最近的几条日志

    可以使用参数-<n>(<n>为数字),显示最近的<n>条日志。例如下面的命令显示最近的3条日志。

    $ git log -3 --pretty=oneline
    6652a0dce6a5067732c00ef0a220810a7230655e Add Images for git treeview.
    81993234fc12a325d303eccea20f6fd629412712 Commit A: merge B with C.
    0cd7f2ea245d90d414e502467ac749f36aa32cc4 commit C.
    

    显示每次提交的具体改动

    使用参数-p可以在显示日志的时候同时显示改动。

    $ git log -p -1
    commit 6652a0dce6a5067732c00ef0a220810a7230655e
    Author: Jiang Xin <[email protected]>
    Date:   Thu Dec 9 16:07:11 2010 +0800
        Add Images for git treeview.
        Signed-off-by: Jiang Xin <[email protected]>
    diff --git a/gitg.png b/gitg.png
    new file mode 100644
    index 0000000..fc58966
    Binary files /dev/null and b/gitg.png differ
    diff --git a/treeview.png b/treeview.png
    new file mode 100644
    index 0000000..a756d12
    Binary files /dev/null and b/treeview.png differ
    

    因为是二进制文件改动,缺省不显示改动的内容。实际上Git的差异文件提供对二进制文件的支持,在后面“Git应用”章节予以专题介绍。

    显示每次提交的变更概要

    使用-p参数会让日志输出显得非常冗余,当不需要知道具体的改动而只想知道改动在哪些文件上,可以使用--stat参数。输出的变更概要像极了Linux的diffstat命令的输出。

    $ git log --stat --oneline  I..C
    0cd7f2e commit C.
     README    |    1 +
     doc/C.txt |    1 +
     2 files changed, 2 insertions(+), 0 deletions(-)
    beb30ca Commit F: merge I with J
    3252fcc commit J.
     README           |    7 +++++++
     doc/J.txt        |    1 +
     src/.gitignore   |    3 +++
     src/Makefile     |   27 +++++++++++++++++++++++++++
     src/main.c       |   10 ++++++++++
     src/version.h.in |    6 ++++++
     6 files changed, 54 insertions(+), 0 deletions(-)
    

    Git的差异输出命令提供了很多输出模板提供选择,可以根据需要选择冗余显示或者精简显示。

  • 参数--pretty=raw显示提交的原始数据。可以显示提交对应的树ID。

    $ git log --pretty=raw -1
    commit 6652a0dce6a5067732c00ef0a220810a7230655e
    tree e33be9e8e7ca5f887c7d5601054f2f510e6744b8
    parent 81993234fc12a325d303eccea20f6fd629412712
    author Jiang Xin <[email protected]> 1291882031 +0800
    committer Jiang Xin <[email protected]> 1291882892 +0800
        Add Images for git treeview.
        Signed-off-by: Jiang Xin <[email protected]>
    
  • 参数--pretty=fuller会同时显示作者和提交者,两者可以不同。

    $ git log --pretty=fuller -1
    commit 6652a0dce6a5067732c00ef0a220810a7230655e
    Author:     Jiang Xin <[email protected]>
    AuthorDate: Thu Dec 9 16:07:11 2010 +0800
    Commit:     Jiang Xin <[email protected]>
    CommitDate: Thu Dec 9 16:21:32 2010 +0800
        Add Images for git treeview.
        Signed-off-by: Jiang Xin <[email protected]>
    
  • 参数--pretty=oneline显然会提供最精简的日志输出。也可以使用--oneline参数,效果近似。

    $ git log --pretty=oneline -1
    6652a0dce6a5067732c00ef0a220810a7230655e Add Images for git treeview.
    commit 212efce1548795a1edb08e3708a50989fcd73cce
    Merge: e80aa74 2ab52ad
    Author: Jiang Xin <[email protected]>
    Date:   Thu Dec 9 14:06:34 2010 +0800
        Commit D: merge G with H
        Signed-off-by: Jiang Xin <[email protected]>
     README    |    2 ++
     doc/D.txt |    1 +
     doc/H.txt |    1 +
     3 files changed, 4 insertions(+), 0 deletions(-)
    
  • 使用git cat-file显示里程碑D及其提交。

    参数-p的含义是美观的输出(pretty)。

    $ git cat-file -p D^0
    tree 1c22e90c6bf150ee1cde6cefb476abbb921f491f
    parent e80aa7481beda65ae00e35afc4bc4b171f9b0ebf
    parent 2ab52ad2a30570109e71b56fa1780f0442059b3c
    author Jiang Xin <[email protected]> 1291874794 +0800
    committer Jiang Xin <[email protected]> 1291875877 +0800
    Commit D: merge G with H
    Signed-off-by: Jiang Xin <[email protected]>
    

    2.8.4.4. 差异比较:git diff

    Git差异比较功能在前面的实践中也反复的接触过了,尤其是在介绍暂存区的相关章节重点介绍了git diff命令如何对工作区、暂存区、版本库进行比较。

  • 比较里程碑B和里程碑A,用命令:git diff B A
  • 比较工作区和里程碑A,用命令:git diff A
  • 比较暂存区和里程碑A,用命令:git diff --cached A
  • 比较工作区和暂存区,用命令:git diff
  • 比较暂存区和HEAD,用命令:git diff --cached
  • 比较工作区和HEAD,用命令:git diff HEAD
  • Git中文件在版本间的差异比较

    差异比较还可以使用路径参数,只显示不同版本间该路径下文件的差异。语法格式:

    $ git diff <commit1> <commit2> -- <paths>
    

    非Git目录/文件的差异比较

    命令git diff还可以在Git版本库之外执行,对非Git目录进行比较,就像GNU的diff命令一样。之所以提供这个功能是因为Git差异比较命令更为强大,提供了对GNU差异比较的扩展支持。

    $ git diff <path1> <path2>
    

    扩展的差异语法

    Git扩展了GNU的差异比较语法,提供了对重命名、二进制文件、文件权限变更的支持。在后面的“Git应用”辟专题介绍二进制文件的差异比较和补丁的应用。

    逐词比较,而非缺省的逐行比较

    Git的差异比较缺省是逐行比较,分别显示改动前的行和改动后的行,到底改动哪里还需要仔细辨别。Git还提供一种逐词比较的输出,有的人会更喜欢。使用--word-diff参数可以显示逐词比较。

    $ git diff --word-diff
    diff --git a/src/book/02-use-git/080-git-history-travel.rst b/src/book/02-use-git/080-git-history-travel.rst
    index f740203..2dd3e6f 100644
    --- a/src/book/02-use-git/080-git-history-travel.rst
    +++ b/src/book/02-use-git/080-git-history-travel.rst
    @@ -681,7 +681,7 @@ Git的大部分命令可以使用提交版本作为参数(如:git diff),
      [-18:23:48 jiangxin@hp:~/gitwork/gitbook/src/book$-]{+$+} git log --stat --oneline  I..C
      0cd7f2e commit C.
       README    |    1 +
       doc/C.txt |    1 +
    

    上面的逐词差异显示是有颜色显示的:删除内容[-...-]用红色表示,添加的内容{+...+}用绿色表示。

    2.8.4.5. 文件追溯:git blame

    在软件开发过程中当发现Bug并定位到具体的代码时,Git的文件追溯命令可以指出是谁在什么时候,什么版本引入的此Bug。

    当针对文件执行git blame命令,就会逐行显示文件,在每一行的行首显示此行最早是在什么版本引入的,由谁引入。

    $ cd /path/to/my/workspace/gitdemo-commit-tree
    $ git blame README
    ^e80aa74 (Jiang Xin 2010-12-09 14:00:33 +0800  1) DEMO program for git-scm-book.
    ^e80aa74 (Jiang Xin 2010-12-09 14:00:33 +0800  2)
    ^e80aa74 (Jiang Xin 2010-12-09 14:00:33 +0800  3) Changes
    ^e80aa74 (Jiang Xin 2010-12-09 14:00:33 +0800  4) =======
    ^e80aa74 (Jiang Xin 2010-12-09 14:00:33 +0800  5)
    81993234 (Jiang Xin 2010-12-09 14:30:15 +0800  6) * create node A.
    0cd7f2ea (Jiang Xin 2010-12-09 14:29:09 +0800  7) * create node C.
    776c5c9d (Jiang Xin 2010-12-09 14:27:31 +0800  8) * create node B.
    beb30ca7 (Jiang Xin 2010-12-09 14:11:01 +0800  9) * create node F.
    ^3252fcc (Jiang Xin 2010-12-09 14:00:33 +0800 10) * create node J.
    ^634836c (Jiang Xin 2010-12-09 14:00:33 +0800 11) * create node I.
    ^83be369 (Jiang Xin 2010-12-09 14:00:33 +0800 12) * create node E.
    212efce1 (Jiang Xin 2010-12-09 14:06:34 +0800 13) * create node D.
    ^2ab52ad (Jiang Xin 2010-12-09 14:00:33 +0800 14) * create node H.
    ^e80aa74 (Jiang Xin 2010-12-09 14:00:33 +0800 15) * create node G.
    ^e80aa74 (Jiang Xin 2010-12-09 14:00:33 +0800 16) * initialized.
    

    只想查看某几行,使用-L n,m参数,如下:

    $ git blame -L 6,+5 README
    81993234 (Jiang Xin 2010-12-09 14:30:15 +0800  6) * create node A.
    0cd7f2ea (Jiang Xin 2010-12-09 14:29:09 +0800  7) * create node C.
    776c5c9d (Jiang Xin 2010-12-09 14:27:31 +0800  8) * create node B.
    beb30ca7 (Jiang Xin 2010-12-09 14:11:01 +0800  9) * create node F.
    ^3252fcc (Jiang Xin 2010-12-09 14:00:33 +0800 10) * create node J.
    

    2.8.4.6. 二分查找:git bisect

    前面的文件追溯是建立在问题(Bug)已经定位(到代码上)的基础之上,然后才能通过错误的行(代码)找到人(提交者),打板子(教育或惩罚)。那么如何定位问题呢?Git的二分查找命令可以提供帮助。

    二分查找并不神秘,也不是万灵药,是建立在测试的基础之上的。实际上每个进行过软件测试的人都曾经使用过:“最新的版本出现Bug了,但是在给某某客户的版本却没有这个问题,所以问题肯定出在两者之间的某次代码提交上”。

    Git提供的git bisect命令是基于版本库的,自动化的问题查找和定位工作流程。取代传统软件测试中粗放式的、针对软件发布版本的、无法定位到代码的测试。

    执行二分查找,在发现问题后,首先要找到一个正确的版本,如果所发现的问题从软件最早的版本就是错的,那么就没有必要执行二分查找了,还是老老实实的Debug吧。但是如果能够找到一个正确的版本,即在这个正确的版本上问题没有发生,那么就可以开始使用git bisect命令在版本库中进行二分查找了:

  • 工作区切换到已知的“好版本”和“坏版本”的中间的一个版本。
  • 执行测试,问题重现,将版本库当前版本库为“坏版本”,如果问题没有重现,将当前版本标记为“好版本”。
  • 重复1-2,直至最终找到第一个导致问题出现的版本。
  • 下面是示例版本库标记了提交ID后的示意图,在这个示例版本库中试验二分查找流程:首先标记最新提交(HEAD)是“坏的”,G提交是好的,然后通过查找最终定位到坏提交(B)。

    在下面的试验中定义坏提交的依据很简单,如果在doc/目录中包含文件B.txt,则此版本是“坏”的。(这个示例太简陋,不要见笑,聪明的读者可以直接通过doc/B.txt文件就可追溯到B提交。)

    下面开始通过手动测试(查找doc/B.txt存在与否),借助Git二分查找定位“问题”版本。

  • 首先确认工作在master分支。

    $ cd /path/to/my/workspace/gitdemo-commit-tree/
    $ git checkout master
    Already on 'master'
    
  • 开始二分查找。

    $ git bisect start
    
  • 已经当前版本是“坏提交”,因为存在文件doc/B.txt。而G版本是“好提交”,因为不存在文件doc/B.txt

    $ git cat-file -t master:doc/B.txt
    $ git cat-file -t G:doc/B.txt
    fatal: Not a valid object name G:doc/B.txt
    
  • 将当前版本(HEAD)标记为“坏提交”,将G版本标记为“好提交”。

    $ git bisect bad
    $ git bisect good G
    Bisecting: 5 revisions left to test after this (roughly 2 steps)
    [0cd7f2ea245d90d414e502467ac749f36aa32cc4] commit C.
    
  • 自动定位到C提交。没有文件doc/B.txt,也是一个好提交。

    $ git describe
    $ ls doc/B.txt
    ls: 无法访问doc/B.txt: 没有那个文件或目录
    
  • 标记当前版本(C提交)为“好提交”。

    $ git bisect good
    Bisecting: 3 revisions left to test after this (roughly 2 steps)
    [212efce1548795a1edb08e3708a50989fcd73cce] Commit D: merge G with H
    
  • 现在定位到D版本,这也是一个“好提交”。

    $ git describe
    $ ls doc/B.txt
    ls: 无法访问doc/B.txt: 没有那个文件或目录
    
  • 标记当前版本(D提交)为“好提交”。

    $ git bisect good
    Bisecting: 1 revision left to test after this (roughly 1 step)
    [776c5c9da9dcbb7e463c061d965ea47e73853b6e] Commit B: merge D with E and F
    
  • 现在定位到B版本,这是一个“坏提交”。

    $ git bisect bad
    Bisecting: 0 revisions left to test after this (roughly 0 steps)
    [83be36956c007d7bfffe13805dd2081839fd3603] commit E.
    
  • 现在定位到E版本,这是一个“好提交”。当标记E为好提交之后,输出显示已经成功定位到引入坏提交的最接近的版本。

    $ git bisect good
    776c5c9da9dcbb7e463c061d965ea47e73853b6e is the first bad commit
    
  • 最终定位的坏提交用引用refs/bisect/bad标识。可以如下方法切换到该版本。

    $ git checkout bisect/bad
    Previous HEAD position was 83be369... commit E.
    HEAD is now at 776c5c9... Commit B: merge D with E and F
    
  • 当对“Bug”定位和修复后,撤销二分查找在版本库中遗留的临时文件和引用。

    撤销二分查找后,版本库切换回执行二分查找之前所在的分支。

    $ git bisect reset
    Previous HEAD position was 776c5c9... Commit B: merge D with E and F
    Switched to branch 'master'
    

    把“好提交”标记成了“坏提交”该怎么办?

    在执行二分查找的过程中,一不小心就有可能犯错,将“好提交”标记为“坏提交”,或者相反。这将导致前面的查找过程也前功尽弃。Git的二分查找提供一个恢复查找进度的办法。

  • 例如对E提交,本来是一个“好版本”却被错误的标记为“坏版本”。

    $ git bisect bad
    83be36956c007d7bfffe13805dd2081839fd3603 is the first bad commit
    
  • git bisect log命令查看二分查找的日志记录。

    把二分查找的日志保存在一个文件中。

    $ git bisect log > logfile
    
  • 编辑这个文件,删除记录了错误动作的行。

    以井号(#)开始的行是注释。

    $ cat logfile
    # bad: [6652a0dce6a5067732c00ef0a220810a7230655e] Add Images for git treeview.
    # good: [e80aa7481beda65ae00e35afc4bc4b171f9b0ebf] commit G.
    git bisect start 'master' 'G'
    # good: [0cd7f2ea245d90d414e502467ac749f36aa32cc4] commit C.
    git bisect good 0cd7f2ea245d90d414e502467ac749f36aa32cc4
    # good: [212efce1548795a1edb08e3708a50989fcd73cce] Commit D: merge G with H
    git bisect good 212efce1548795a1edb08e3708a50989fcd73cce
    # bad: [776c5c9da9dcbb7e463c061d965ea47e73853b6e] Commit B: merge D with E and F
    git bisect bad 776c5c9da9dcbb7e463c061d965ea47e73853b6e
    
  • 结束上一次出错的二分查找。

    $ git bisect reset
    Previous HEAD position was 83be369... commit E.
    Switched to branch 'master'
    
  • 通过日志文件恢复进度。

    $ git bisect replay logfile
    We are not bisecting.
    Bisecting: 5 revisions left to test after this (roughly 2 steps)
    [0cd7f2ea245d90d414e502467ac749f36aa32cc4] commit C.
    Bisecting: 0 revisions left to test after this (roughly 0 steps)
    [83be36956c007d7bfffe13805dd2081839fd3603] commit E.
    
  • 再一次回到了提交E,这一次不要标记错了。

    $ git describe
    $ git bisect good
    776c5c9da9dcbb7e463c061d965ea47e73853b6e is the first bad commit
    
  • 如果脚本的退出码是0,正在测试的版本是一个“好版本”。
  • 如果脚本的退出码是125,正在测试的版本被跳过。
  • 如果脚本的退出码是1到127(125除外),正在测试的版本是一个“坏版本”。
  • 对于本例写一个自动化测试太简单了,无非就是判断文件是否存在,存在返回错误码1,不存在返回错误码0。

    测试脚本good-or-bad.sh如下:

    #!/bin/sh
    [ -f doc/B.txt ] && exit 1
    exit 0
    

    用此自动化脚本执行二分查找就非常简单了。

  • 从已知的坏版本master和好版本G,开始新一轮的二分查找。

    $ git bisect start master G
    Bisecting: 5 revisions left to test after this (roughly 2 steps)
    [0cd7f2ea245d90d414e502467ac749f36aa32cc4] commit C.
    
  • 自动化测试,使用脚本good-or-bad.sh

    $ git bisect run sh good-or-bad.sh
    running sh good-or-bad.sh
    Bisecting: 3 revisions left to test after this (roughly 2 steps)
    [212efce1548795a1edb08e3708a50989fcd73cce] Commit D: merge G with H
    running sh good-or-bad.sh
    Bisecting: 1 revision left to test after this (roughly 1 step)
    [776c5c9da9dcbb7e463c061d965ea47e73853b6e] Commit B: merge D with E and F
    running sh good-or-bad.sh
    Bisecting: 0 revisions left to test after this (roughly 0 steps)
    [83be36956c007d7bfffe13805dd2081839fd3603] commit E.
    running sh good-or-bad.sh
    776c5c9da9dcbb7e463c061d965ea47e73853b6e is the first bad commit
    bisect run success
    
  • 定位到的“坏版本”是B。

    $ git describe refs/bisect/bad
    
  •  
    推荐文章
    仗义的羽毛球  ·  OpenWRT编译指南 - 阿风小子
    2 天前
    腼腆的荔枝  ·  Dangit, Git!?!
    13 分钟前
    强悍的牛排  ·  删除文件 - Git教程 - 廖雪峰的官方网站
    13 分钟前
    坏坏的皮蛋  ·  “甘道夫”版李尔王、“精灵王子”演罗密欧,高清戏剧影像再现中土世界
    1 月前
    聪明伶俐的青蛙  ·  Issue 23548: TypeError in event loop finalizer, new in Python 3.4.3 - Python tracker
    3 月前
    刚分手的红薯  ·  黎以局势紧张 美国准备从黎巴嫩撤侨_新闻频道_央视网(cctv.com)
    4 月前
    重情义的小马驹  ·  WPF的路由事件概述 - 键盘上的烟灰 - 博客园
    4 月前
    含蓄的闹钟  ·  穿成一岁奶萌小团霸(一书成神)全本免费在线阅读-起点中文网官方正版
    6 月前