Pro Git
professional version control
To help translate the book, please fork the book at GitHub and push your contributions.
Git属性
一些设置项也能被运用于特定的路径中,这样,Git 以对一个特定的子目录或子文件集运用那些设置项。这些设置项被称为 Git 属性,可以在你目录中的.gitattributes
文件内进行设置 (通常是你项目的根目录),也可以当你不想让这些属性文件和项目文件一同提交时,在.git/info/attributes
进行设置。
使用属性,你可以对个别文件或目录定义不同的合并策略,让 Git 知道怎样比较非文本文件,在你提交或签出前让 Git 过滤内容。你将在这部分了解到能在自己的项目中使用的属性,以及一些实例。
二进制文件
你可以用 Git 属性让其知道哪些是二进制文件(以防 Git 没有识别出来),以及指示怎样处理这些文件,这点很酷。例如,一些文本文件是由机器产生的,而且无法比较,而一些二进制文件可以比较 — 你将会了解到怎样让 Git 识别这些文件。
识别二进制文件
一些文件看起来像是文本文件,但其实是作为二进制数据被对待。例如,在Mac上的Xcode项目含有一个以.pbxproj
结尾的文件,它是由记录设置项的IDE写到磁盘的JSON数据集(纯文本javascript数据类型)。虽然技术上看它是由ASCII字符组成的文本文件,但你并不认为如此,因为它确实是一个轻量级数据库 — 如果有2人改变了它,你通常无法合并和比较内容,只有机器才能进行识别和操作,于是,你想把它当成二进制文件。
让 Git 把所有pbxproj
文件当成二进制文件,在.gitattributes
文件中设置如下:
*.pbxproj -crlf -diff
现在,Git 会尝试转换和修正CRLF(回车换行)问题,也不会当你在项目中运行git show或git diff时,比较不同的内容。在Git 1.6及之后的版本中,可以用一个宏代替-crlf -diff
:
*.pbxproj binary
比较二进制文件
在Git 1.6及以上版本中,你能利用 Git 属性来有效地比较二进制文件。可以设置 Git 把二进制数据转换成文本格式,用通常的diff来比较。
这个特性很酷,而且鲜为人知,因此我会结合实例来讲解。首先,要解决的是最令人头疼的问题:对Word文档进行版本控制。很多人对Word文档又恨又爱,如果想对其进行版本控制,你可以把文件加入到 Git 库中,每次修改后提交即可。但这样做没有一点实际意义,因为运行git diff
命令后,你只能得到如下的结果:
$ git diff
diff --git a/chapter1.doc b/chapter1.doc
index 88839c4..4afcb7c 100644
Binary files a/chapter1.doc and b/chapter1.doc differ
你不能直接比较两个不同版本的Word文件,除非进行手动扫描,不是吗? Git 属性能很好地解决此问题,把下面的行加到.gitattributes
文件:
*.doc diff=word
当你要看比较结果时,如果文件扩展名是”doc”,Git 调用”word”过滤器。什么是”word”过滤器呢?其实就是 Git 使用strings
程序,把Word文档转换成可读的文本文件,之后再进行比较:
$ git config diff.word.textconv strings
现在如果在两个快照之间比较以.doc
结尾的文件,Git 对这些文件运用”word”过滤器,在比较前把Word文件转换成文本文件。
下面展示了一个实例,我把此书的第一章纳入 Git 管理,在一个段落中加入了一些文本后保存,之后运行git diff
命令,得到结果如下:
$ git diff
diff --git a/chapter1.doc b/chapter1.doc
index c1c8a0a..b93c9e4 100644
--- a/chapter1.doc
+++ b/chapter1.doc
@@ -8,7 +8,8 @@ re going to cover Version Control Systems (VCS) and Git basics
re going to cover how to get it and set it up for the first time if you don
t already have it on your system.
In Chapter Two we will go over basic Git usage - how to use Git for the 80%
-s going on, modify stuff and contribute changes. If the book spontaneously
+s going on, modify stuff and contribute changes. If the book spontaneously
+Let's see if this works.
Git 成功且简洁地显示出我增加的文本”Let’s see if this works”。虽然有些瑕疵,在末尾显示了一些随机的内容,但确实可以比较了。如果你能找到或自己写个Word到纯文本的转换器的话,效果可能会更好。 strings
可以在大部分Mac和Linux系统上运行,所以它是处理二进制格式的第一选择。
你还能用这个方法比较图像文件。当比较时,对JPEG文件运用一个过滤器,它能提炼出EXIF信息 — 大部分图像格式使用的元数据。如果你下载并安装了exiftool
程序,可以用它参照元数据把图像转换成文本。比较的不同结果将会用文本向你展示:
$ echo '*.png diff=exif' >> .gitattributes
$ git config diff.exif.textconv exiftool
如果在项目中替换了一个图像文件,运行git diff
命令的结果如下:
diff --git a/image.png b/image.png
index 88839c4..4afcb7c 100644
--- a/image.png
+++ b/image.png
@@ -1,12 +1,12 @@
ExifTool Version Number : 7.74
-File Size : 70 kB
-File Modification Date/Time : 2009:04:21 07:02:45-07:00
+File Size : 94 kB
+File Modification Date/Time : 2009:04:21 07:02:43-07:00
File Type : PNG
MIME Type : image/png
-Image Width : 1058
-Image Height : 889
+Image Width : 1056
+Image Height : 827
Bit Depth : 8
Color Type : RGB with Alpha
你会发现文件的尺寸大小发生了改变。
关键字扩展
使用SVN或CVS的开发人员经常要求关键字扩展。在 Git 中,你无法在一个文件被提交后修改它,因为 Git 会先对该文件计算校验和。然而,你可以在签出时注入文本,在提交前删除它。 Git 属性提供了2种方式这么做。
首先,你能够把blob的SHA-1校验和自动注入文件的$Id$
字段。如果在一个或多个文件上设置了此字段,当下次你签出分支的时候,Git 用blob的SHA-1值替换那个字段。注意,这不是提交对象的SHA校验和,而是blob本身的校验和:
$ echo '*.txt ident' >> .gitattributes
$ echo '$Id$' > test.txt
下次签出文件时,Git 入了blob的SHA值:
$ rm text.txt
$ git checkout -- text.txt
$ cat test.txt
$Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $
然而,这样的显示结果没有多大的实际意义。这个SHA的值相当地随机,无法区分日期的前后,所以,如果你在CVS或Subversion中用过关键字替换,一定会包含一个日期值。
因此,你能写自己的过滤器,在提交文件到暂存区或签出文件时替换关键字。有2种过滤器,”clean”和”smudge”。在 .gitattributes
文件中,你能对特定的路径设置一个过滤器,然后设置处理文件的脚本,这些脚本会在文件签出前(”smudge”,见图 7-2)和提交到暂存区前(”clean”,见图7-3)被调用。这些过滤器能够做各种有趣的事。
![](18333fig0702-tn.png)
图7-2. 签出时,“smudge”过滤器被触发。
![](18333fig0703-tn.png)
图7-3. 提交到暂存区时,“clean”过滤器被触发。
这里举一个简单的例子:在暂存前,用indent
(缩进)程序过滤所有C源代码。在.gitattributes
文件中设置”indent”过滤器过滤*.c
文件:
*.c filter=indent
然后,通过以下配置,让 Git 知道”indent”过滤器在遇到”smudge”和”clean”时分别该做什么:
$ git config --global filter.indent.clean indent
$ git config --global filter.indent.smudge cat
于是,当你暂存*.c
文件时,indent
程序会被触发,在把它们签出之前,cat
程序会被触发。但cat
程序在这里没什么实际作用。这样的组合,使C源代码在暂存前被indent
程序过滤,非常有效。
另一个例子是类似RCS的$Date$
关键字扩展。为了演示,需要一个小脚本,接受文件名参数,得到项目的最新提交日期,最后把日期写入该文件。下面用Ruby脚本来实现:
#! /usr/bin/env ruby
data = STDIN.read
last_date = `git log --pretty=format:"%ad" -1`
puts data.gsub('$Date$', '$Date: ' + last_date.to_s + '$')
该脚本从git log
命令中得到最新提交日期,找到文件中的所有$Date$
字符串,最后把该日期填充到$Date$
字符串中 — 此脚本很简单,你可以选择你喜欢的编程语言来实现。把该脚本命名为expand_date
,放到正确的路径中,之后需要在 Git 中设置一个过滤器(dater
),让它在签出文件时调用expand_date
,在暂存文件时用Perl清除之:
$ git config filter.dater.smudge expand_date
$ git config filter.dater.clean 'perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"'
这个Perl小程序会删除$Date$
字符串里多余的字符,恢复$Date$
原貌。到目前为止,你的过滤器已经设置完毕,可以开始测试了。打开一个文件,在文件中输入$Date$
关键字,然后设置 Git 属性:
$ echo '# $Date$' > date_test.txt
$ echo 'date*.txt filter=dater' >> .gitattributes
如果暂存该文件,之后再签出,你会发现关键字被替换了:
$ git add date_test.txt .gitattributes
$ git commit -m "Testing date expansion in Git"
$ rm date_test.txt
$ git checkout date_test.txt
$ cat date_test.txt
# $Date: Tue Apr 21 07:26:52 2009 -0700$
虽说这项技术对自定义应用来说很有用,但还是要小心,因为.gitattributes
文件会随着项目一起提交,而过滤器(例如:dater
)不会,所以,过滤器不会在所有地方都生效。当你在设计这些过滤器时要注意,即使它们无法正常工作,也要让整个项目运作下去。
导出仓库
Git属性在导出项目归档时也能发挥作用。
export-ignore
当产生一个归档时,可以设置 Git 不导出某些文件和目录。如果你不想在归档中包含一个子目录或文件,但想他们纳入项目的版本管理中,你能对应地设置export-ignore
属性。
例如,在test/
子目录中有一些测试文件,在项目的压缩包中包含他们是没有意义的。因此,可以增加下面这行到 Git 属性文件中:
test/ export-ignore
现在,当运行git archive来创建项目的压缩包时,那个目录不会在归档中出现。
export-subst
还能对归档做一些简单的关键字替换。在第2章中已经可以看到,可以以--pretty=format
形式的简码在任何文件中放入$Format:$
字符串。例如,如果想在项目中包含一个叫作LAST_COMMIT
的文件,当运行git archive
时,最后提交日期自动地注入进该文件,可以这样设置:
$ echo 'Last commit date: $Format:%cd$' > LAST_COMMIT
$ echo "LAST_COMMIT export-subst" >> .gitattributes
$ git add LAST_COMMIT .gitattributes
$ git commit -am 'adding LAST_COMMIT file for archives'
运行git archive
后,打开该文件,会发现其内容如下:
$ cat LAST_COMMIT
Last commit date: $Format:Tue Apr 21 08:38:48 2009 -0700$
合并策略
通过 Git 属性,还能对项目中的特定文件使用不同的合并策略。一个非常有用的选项就是,当一些特定文件发生冲突,Git 会尝试合并他们,而使用你这边的合并。
如果项目的一个分支有歧义或比较特别,但你想从该分支合并,而且需要忽略其中某些文件,这样的合并策略是有用的。例如,你有一个数据库设置文件database.xml,在2个分支中他们是不同的,你想合并一个分支到另一个,而不弄乱该数据库文件,可以设置属性如下:
database.xml merge=ours
如果合并到另一个分支,database.xml文件不会有合并冲突,显示如下:
$ git merge topic
Auto-merging database.xml
Merge made by recursive.
这样,database.xml会保持原样。