GNU diff 和 git diff-tree
作者:郑凯
这只是在学习 git 里 object id 概念过程中的一个实际例子,如果对 object id 已经很熟悉了那可能不需要看了
目前在配置文件管理上有这么一个需求。同一个目录有三个格式相同的目录,目录下的文件和格式几乎相同,根目录在 git 里,因此三个目录会产生不同的历史。需求就是,可以比较任意历史的任意目录。
这个网页工具的最终效果是这样:
我开始是分成三步来做这事
第一步,分别 cd 到三个不同目录获取列表,下行中的 $name
是三个目录名( dev
/ prod
/ stable
)
git log -n 50 --pretty=format:"%h %ct $name" .
第二步,根据列表生成不同的目录,例如 dc65220d-dev
或者 ffd38cd7-prod
这种目录,表明 commit id 和 name
第三步,任意两个目录之间 diff -u -r
,并把这一操作最终显示在网页上
但是同事 sung1011 觉得方法不优雅,因为需要占用大量硬盘空间,又给出了改进方法,直接在 git 库里做比较,原来的第二步取消,第三步改为这么两步
第 3.1 步,根据 commit id 和 name 获取 object id
git ls-tree dc65220d 'config/dev'
返回结果类似,第三段 9e5bbaa0995dc2afc2e5d265385541e62c60bc1a
就是 object id,注意第二段的类型,tree
表示目录,如果 blob
就是单个文件
040000 tree 9e5bbaa0995dc2afc2e5d265385541e62c60bc1a config/dev
用 ls-tree
分别获取这两个目录的 object id
第 3.2 步,使用 git diff-tree
比较,跟之前的 diff
很相似,只是目录名被换成
object id,例如
git diff-tree -u -r 9e5bbaa0995dc2afc2e5d265385541e62c60bc1a d2d1482c22e5a1a99f4ecfb1bb17e9a5d5d27975
回过头来再看第一步,取列表也可以改一下,使用裸仓库(git clone --bare
)。
原本的操作,需要分别进入目录取 log,在裸仓库要变形一下,原本的:
cd "config/$name" && git log -n 50 --pretty=format:"%h %ct $name" .
改写为在仓库目录任意位置:
git log -n 50 --pretty=format:"%h $name %ct" -- "config/$name"
虽然 git diff-tree
看起来更优雅(不需要一堆临时目录),但实际使用效果并不够理想
-
在 diff 文件时细微的差异,例如有新增文件,GNU diff 里只会列一下文件名,而
diff-tree
会显示完整文件内容并且每行开头有个+
号,但实际上从产品角度想要的还是 GNU diff 的那种。为了解决这个问题查到参数--diff-filter=[(A|C|D|M|R|T|U|X|B)...[*]]
可以只选M
而忽略A|D
,但看到这个参数后又引发了新的疑惑,不知道diff-tree
的时候是否会有R
(renamed)、C
(copied)等内容,因为只是想判断两个目录的绝对差异,而不是想查明上下文的历史变化。 -
diff-tree
的时间是不稳定的,这很好理解,因为节省了空间,所以查询内容有大量的指针跳转,需要花更多时间。在我的机械硬盘上diff-tree
花的时间大体分三档:1ms 左右,50ms 左右、2s 左右,而diff
是稳定的 350ms 左右。虽然结果已经被缓存了,但diff-tree
首次查询时间波动还是有点大。 -
这是纯粹需求的原因,不是实现的问题。diff 之后下一步就是复制选定的配置目录到某台测试机上,以及在 diff 过程中有可能需要查看某个版本的完整文件。这就需要
git cat-file
之类的,而如果是生成到很多目录的话,直接通过 Nginx 访问静态文件而不需要自定义接口了。
最终只是把仓库改成裸仓库,而没用 diff-tree
,但通过这次实践,增加了对 object id
的理解,记下来也许以后别的需求会用得上。