1. 背景
Git 作为一种分布式版本管理工具,已经越来越普及了,绝大部分公司都使用 Git 进行代码的版本管理,甚至是文档的版本管理。包括一些 wiki 平台,底层也使用了 Git 进行版本管理,提供了查看文档修改记录、多版本比对、甚至版本回退等一系列功能。
在使用 Git 的日常中,有这么一种场景,相信很多开发者都遇到过,就是差异比对。如果是还未提交的代码与已提交代码的差异,那么在工作区或者暂存区我们就可以看到修改的内容。如果是比对两个分支、两个 tag、甚至两个不同的 commit 之间的代码差异呢,除了拷贝一份代码出来,使用比对工具进行比对之外,还有没有其他的办法呢,所幸 Git 已经为我们提供了这么一个差异比对功能:diff。
什么是 diff:diff 就是目标文本和源文本之间的区别,或者说是将目标文本变成源文本所需要的一系列操作。Git diff 使用的算法默认是 Myers,一个好的算法可以为我们产生最简短最直观的差异,对 Myers 算法感兴趣的同学可以通过这篇文章了解下:Git 是怎样生成 diff 的:Myers 算法,本文主要讲的是如何查看 diff。
2. 方案
2.1 SourceTree 查看差异
SourceTree 作为业界有名的 Git 版本管理 GUI 工具,它不仅提供了版本管理功能,还为用户提供了非常友好的界面,使用 SourceTree 不仅可以查看代码提交的历史记录,分支路径,还可以直观快捷的查看两个不同版本之间的差异。
作为例子,首先在本地创建一个文件夹,在该文件夹路径下,创建一个 Git 仓库。操作如下:
$ cd /{path}/DiffTest
$ git init
这样一来,文件夹 DiffTest 下的所有文件的增删改重命名等各种变更操作都会被纳入版本管理中。接下来我在路径下陆续新增文本文件 readme.md、main.c,二进制文件 heart.png、dove.png,并分多次提交。然后对该路径下的文件进行移动、删除、重命名、内容修改等操作,也分多次提交。提交记录如以下 SourceTree 截图所示:
每一行代表了一个提交记录,“描述”一栏是提交时写的 commit message,“提交”那一栏的十六进制数字代表了这个 commit 的 hash 值,你可以把它当成 commit 的 ID。
如果我们想比对两个 commit 之间的差异,可以同时选中两个 commit,这时在提交图表的下方就可以看到两个 commit 的差异比对了,如上图所示。上图的区域 2 给出了摘要信息,告诉用户展示的内容是“xxxx与xxxx之间的所有差异”(xxxx分别表示两个 commit 的 hash 值),区域 1 展示了被改动的文件列表,区域 3 则展示了左边选中的文件的改动内容。上图区域 1 每一行前面有个 icon,第一个紫色的 icon 表示“移动/重命名”,第二个红色的 icon 表示“删除”,第三个黄色的表示“修改”,第四个绿色的表示“新增”,这四种代表了 Git 中最常出现的四种操作。后缀为 .png 的文件为二进制文件,.c 跟 .md 的文件为文本文件,文本文件的修改可以在右侧区域 3 看到具体的修改内容,标红的内容表示该行从 a 版本删除(此处 a 版本指 commit 27d76d0),标绿的表示在 b 版本(此处 b 版本指commit 417dc0c)新增的内容。注意,此时的差异不包括 a 版本那个 commit 所进行的修改。
2.2 git difftool 查看差异
除了 SourceTree,还有很多比对工具可以给我们提供类似的服务,如 DiffMerge、Kaleidoscope、Beyond Compare。DiffMerge 虽然好用,但是不支持中文显示,Kaleidoscope 苹果商店的售价又太贵,所以本文选择了 Beyond Compare 为例做个展示。
首先去官网下载对应电脑系统的版本并安装,接着启动 Beyond Compare,在菜单中选择 “Install Command Line Tools”,然后在终端使用以下命令对 Git 进行全局配置(该命令目前适用于 Git 版本为 2.3 以上,Beyond Compare 版本为 3.0 以上的情况,详细内容可参考官网的技术支持):
$ git config --global diff.tool bc
这样我们就可以在终端通过 Git 命令使用 Beyond Compare 进行差异比对了。命令格式如下:
$ git difftool
还是以刚刚两个 commit 为例:
$ cd /{path}/DiffTest
$ git difftool 27d76d0 417dc0c
$
$ Viewing (1/4): 'dove.png'
$ Launch 'bc3' [Y/n]? n
$
$ Viewing (2/4): 'heart.png'
$ Launch 'bc3' [Y/n]? n
$
$ Viewing (3/4): 'newFolder/main.c'
$ Launch 'bc3' [Y/n]? n
$
$ Viewing (4/4): 'readme.md'
$ Launch 'bc3' [Y/n]? y
每一个变动都会问你是否要打开查看,选择 n 就判断下一个,选择 y 就打开 Beyond Compare,关闭 Beyond Compare 时再进入下一个文件的判断,直到遍历完所有变更。如果你觉得这样太麻烦,可以带上参数 “-y” 或 “--no-prompt”,此时会跳过询问这一步,直接打开,但是也只能关闭当前窗口才能查看下一个变更,想要一次性打开所有变更,可使用参数 “-d” 或 “--dir-diff”。
$ git difftool -d 27d76d0 417dc0c
效果如下图,与 SourceTree 看起来略为不同,应该是内置的算法不同所导致的:
关于 git-difftool 命令的更多参数,可在官网相关页面查看。
2.3 git diff 输出差异文件
除了使用 GUI 工具,命令行也可以直接查看差异,命令格式如下:
$ git diff
以前面的例子做个展示:
$ git diff 27d76d0 417dc0c
终端返回如下图:
带上参数 “>> fileName” 可将这些差异输出到文件 fileName 中,命令格式如下:
$ git diff >>fileName
我们来解读下这个差异文件。每一个变更文件作为一个段落,以 “diff --git a/file b/file” 开头,表示 a 版本的 file 文件和 b 版本的 file 文件进行比对。每一个变更文件(如果涉及到内容的修改)可以有一个或多个的变更段落,每个变更段落以 “@@ -beginline,totalline +beginline,totalline @@” 开头。其中 beginline 表示修改开始于第几行,totalline 表示这一段修改涉及的总行数。“-”表示 a 版本,“+”表示 b 版本。变更段落中绿色且以“+”开始的表示新增的内容,红色且以“-”开始的表示删除的内容。
关于 git-diff 命令的更多用法,可在官网相关页面查看。譬如可以把 commit 改为 branch 比较不同的分支,或者比较不同的两个 tag。还可以只查看某个后缀的变更,譬如只查看后缀为 .c 的文件的变更:
$ git diff -- **/*.c
2.4 git format-patch 生成补丁
patch 即是补丁,与前面的差异输出不同,patch 是以 commit 为单位生成的。一个commit 会生成一个 patch,而 patch 可以应用到项目中,类似 cherry-pick,不同的是cherry-pick 不能远程发送给其他人。format-patch 的命令格式如下:
$ git format-patch ..
如果 commitB 是当前分支的最后一个 commit,可以略去,还是同一个例子,输出 commit hash 为 27d76d0 和 commit hash 为 417dc0c 之间的 patch,由于 417dc0c 是 master 分支最后一个 commit,命令如下(参数 -o 表示输出 patch 文件的保存路径,不写的话默认保存到当前仓库目录下):
$ git format-patch 27d76d0 -o ~/patch
得到如下输出:
我们可以看到,输出的每一个补丁文件都以序号开头,从 1 开始,再使用了 commit message的第一行(如果 commit message 太长的话)作为文件名,后缀是 .patch。
第一步,在应用补丁文件之前通过如下命令检查补丁文件涉及到哪些修改:
$ git apply --stat
效果如下:
第二步,切换到我们想要应用补丁的分支下,检查某个具体的补丁是否能成功应用,命令格式如下:
$ git apply --check
如果能成功应用,则不会有任何输出,如果不能,会输出可能发生的错误,如下图:
所在的 branch 的分支并不存在 main.c 文件,补丁 1 的内容是新增 main.c,所以能成功应用。但如果直接使用补丁 2,补丁 2的改动是修改文件 main.c 因此会报错 “不存在 main.c 文件”。
第三步,应用补丁。命令格式如下,应用完补丁文件后,该补丁的修改就出现在工作区了,再执行 commit 即可提交修改:
$ git apply
如果希望直接提交修改,且保留原补丁文件的提交者信息到新的提交上,可使用如下命令格式:
$ git am --signoff
新的 commit 信息如下图所示,可以看到 commit message 与原来的一样,且底下多了一行信息 “Signed-off-by”,注明了原来的代码提交者:
注意,如果某个补丁应用失败,会导致整个补丁都不会集成,如果想当冲突发生时,手动解决冲突,可以在命令 “git apply” 或 “git am --signoff” 后面带上参数 “--reject”,这样不冲突的部分会出现在工作区,冲突的部分会生成 .rej 文件,命令格式如下:
$ git am --signoff --reject
解决完冲突后执行 “git am --continue” 就可以继续往下执行了。也可以通过命令 “git am --abort” 停止集成补丁,解决完冲突后手动通过 “git commit” 命令提交代码,但是这样一来补丁文件中包含的 commit 信息就会失去了。
2.5 Android Studio 差异比对
Android 开发的同学都离不开使用 Android Studio,感谢 daifan 同学补充了第五个方案,使用 Android Studio 进行差异比对。
Android Studio 自带 Git 管理工具,操作方便,而且也提供了文件版本对比、项目分支对比等功能。文件版本对比,即比较同一个文件在不同 commit 结点下的不同版本,操作方法如下:
1、在需要对比的文件上点击右键,在弹出菜单选择 Git - Compare with:
2、在下图中选择想要比对的两个 commit 结点,点击右键,在弹出菜单中选择 Compare。
还可以右键选择 Git - Show History,选择想要对比的两个 commit 结点,然后同样右键选择 Compare,如下图所示:
如果想对比不同分支下的文件,由于 Android Studio 只能实现当前分支(当前结点)与其他分支的对比,于是我们首先要切换到准备进行对比的其中一个分支上,选择需要对比的文件(如果想对比整个项目,那么就选中整个项目的总目录),然后右键选择 Git - Compare with Branch,在弹出的分支列表窗口中选择想要对比的分支即可。
3. 总结
如果只是想查看差异,个人最推荐第一个方案:使用 SourceTree 查看差异,更直观更便捷。如果是由于某种情况,需要把修改通过文件的形式远程发送给别的开发者,第四个方案是最合适的。第二个方案还有很多不同的 difftool 工具,不喜欢用 SourceTree 的小伙伴可以自己尝试更多不同的工具。关于 Git 仓库下如何通过 diff 功能进行差异比对这个话题就讲到这里了,希望能帮到有需要的同学。