章节 ▾ 第二版

8.2 自定义 Git - Git 属性

Git 属性

其中一些设置还可以针对某个路径指定,以便 Git 仅将这些设置应用于子目录或文件子集。这些特定于路径的设置称为 Git 属性,可以在目录(通常是项目的根目录)中的 .gitattributes 文件中设置,也可以在 .git/info/attributes 文件中设置(如果您不希望将属性文件提交到您的项目中)。

使用属性,您可以执行以下操作:为项目中的单个文件或目录指定单独的合并策略,告诉 Git 如何区分非文本文件,或者让 Git 在您将内容检入或检出 Git 之前对其进行过滤。在本节中,您将了解可以在 Git 项目的路径上设置的一些属性,并了解一些在实践中使用此功能的示例。

二进制文件

您可以使用 Git 属性的一个很酷的技巧是告诉 Git 哪些文件是二进制文件(在它无法弄清楚的情况下),并给出关于如何处理这些文件的特殊说明。例如,某些文本文件可能是机器生成的,并且不可区分,而某些二进制文件可以区分。您将看到如何告诉 Git 哪个是哪个。

识别二进制文件

有些文件看起来像文本文件,但实际上应该被视为二进制数据。例如,macOS 上的 Xcode 项目包含一个以 .pbxproj 结尾的文件,这基本上是一个 JSON(纯文本 JavaScript 数据格式)数据集,由 IDE 写入磁盘,用于记录您的构建设置等等。虽然它在技术上是一个文本文件(因为它都是 UTF-8),但您不想将其视为文本文件,因为它实际上是一个轻量级数据库——如果两个人更改它,您无法合并内容,并且差异通常没有帮助。该文件旨在由机器使用。本质上,您希望像处理二进制文件一样处理它。

要告诉 Git 将所有 pbxproj 文件视为二进制数据,请将以下行添加到您的 .gitattributes 文件中

*.pbxproj binary

现在,Git 不会尝试转换或修复 CRLF 问题;当您在项目上运行 git showgit diff 时,它也不会尝试计算或打印此文件中更改的差异。

区分二进制文件

您还可以使用 Git 属性功能有效地区分二进制文件。您可以通过告诉 Git 如何将二进制数据转换为可以通过普通 diff 进行比较的文本格式来实现此目的。

首先,你将使用这项技术来解决人类已知的最烦人的问题之一:Microsoft Word 文档的版本控制。如果你想要对 Word 文档进行版本控制,你可以将它们放入 Git 仓库并时不时地提交;但这有什么用呢?如果你正常运行 git diff,你只会看到类似这样的东西:

$ git diff
diff --git a/chapter1.docx b/chapter1.docx
index 88839c4..4afcb7c 100644
Binary files a/chapter1.docx and b/chapter1.docx differ

你无法直接比较两个版本,除非你检出它们并手动扫描,对吧? 实际上,你可以使用 Git 属性很好地完成此操作。 将以下行放入你的 .gitattributes 文件中:

*.docx diff=word

这告诉 Git,任何匹配此模式 (.docx) 的文件,当你尝试查看包含更改的差异时,都应该使用“word”过滤器。 什么是“word”过滤器? 你必须设置它。 在这里,你将配置 Git 使用 docx2txt 程序将 Word 文档转换为可读的文本文件,然后 Git 将正确地进行比较。

首先,你需要安装 docx2txt;你可以从 https://sourceforge.net/projects/docx2txt 下载它。 按照 INSTALL 文件中的说明将其放置在你的 shell 可以找到的地方。 接下来,你将编写一个包装脚本,将输出转换为 Git 期望的格式。 创建一个位于你的路径中的文件,名为 docx2txt,并添加以下内容:

#!/bin/bash
docx2txt.pl "$1" -

不要忘记 chmod a+x 该文件。 最后,你可以配置 Git 以使用此脚本:

$ git config diff.word.textconv docx2txt

现在 Git 知道,如果它尝试在两个快照之间进行比较,并且任何文件的扩展名以 .docx 结尾,它应该通过 “word” 过滤器运行这些文件,该过滤器被定义为 docx2txt 程序。 这实际上在尝试比较之前生成了 Word 文件的漂亮的基于文本的版本。

这是一个示例:本书的第 1 章被转换为 Word 格式并提交到 Git 仓库中。 然后添加了一个新段落。 这是 git diff 显示的内容:

$ git diff
diff --git a/chapter1.docx b/chapter1.docx
index 0b013ca..ba25db5 100644
--- a/chapter1.docx
+++ b/chapter1.docx
@@ -2,6 +2,7 @@
 This chapter will be about getting started with Git. We will begin at the beginning by explaining some background on version control tools, then move on to how to get Git running on your system and finally how to get it setup to start working with. At the end of this chapter you should understand why Git is around, why you should use it and you should be all setup to do so.
 1.1. About Version Control
 What is "version control", and why should you care? Version control is a system that records changes to a file or set of files over time so that you can recall specific versions later. For the examples in this book you will use software source code as the files being version controlled, though in reality you can do this with nearly any type of file on a computer.
+Testing: 1, 2, 3.
 If you are a graphic or web designer and want to keep every version of an image or layout (which you would most certainly want to), a Version Control System (VCS) is a very wise thing to use. It allows you to revert files back to a previous state, revert the entire project back to a previous state, compare changes over time, see who last modified something that might be causing a problem, who introduced an issue and when, and more. Using a VCS also generally means that if you screw things up or lose files, you can easily recover. In addition, you get all this for very little overhead.
 1.1.1. Local Version Control Systems
 Many people's version-control method of choice is to copy files into another directory (perhaps a time-stamped directory, if they're clever). This approach is very common because it is so simple, but it is also incredibly error prone. It is easy to forget which directory you're in and accidentally write to the wrong file or copy over files you don't mean to.

Git 成功且简洁地告诉我们,我们添加了字符串“Testing: 1, 2, 3.”,这是正确的。 这不是完美的——格式更改不会在这里显示——但它确实有效。

你可以通过这种方式解决的另一个有趣的问题涉及比较图像文件。 一种方法是通过过滤器运行图像文件,该过滤器提取它们的 EXIF 信息——元数据,这些元数据与大多数图像格式一起记录。 如果你下载并安装 exiftool 程序,你可以使用它将你的图像转换为关于元数据的文本,因此至少差异会向你显示发生的任何更改的文本表示。 将以下行放入你的 .gitattributes 文件中:

*.png diff=exif

配置 Git 以使用此工具:

$ 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 系统的开发者经常要求使用 SVN 或 CVS 风格的关键字扩展。 这在 Git 中的主要问题是,你在提交后无法修改包含有关提交信息的文件的内容,因为 Git 首先会检查文件的校验和。 但是,你可以在检出文件时将文本注入到文件中,并在将其添加到提交之前再次删除它。 Git 属性为你提供了两种方法来做到这一点。

首先,你可以自动将 blob 的 SHA-1 校验和注入到文件中的 $Id$ 字段中。 如果你在一个或一组文件上设置此属性,那么下次你检出该分支时,Git 会将该字段替换为 blob 的 SHA-1。 重要的是要注意,它不是提交的 SHA-1,而是 blob 本身的 SHA-1。 将以下行放入你的 .gitattributes 文件中:

*.txt ident

$Id$ 引用添加到测试文件:

$ echo '$Id$' > test.txt

下次你检出此文件时,Git 会注入 blob 的 SHA-1:

$ rm test.txt
$ git checkout -- test.txt
$ cat test.txt
$Id: 42812b7653c7b88933f8a9d6cad0ca16714b9bb3 $

但是,该结果的用处有限。 如果你在 CVS 或 Subversion 中使用了关键字替换,你可以包含一个日期戳——SHA-1 没有那么有用,因为它相当随机,并且你无法仅通过查看它们来判断一个 SHA-1 比另一个 SHA-1 更旧或更新。

事实证明,你可以编写自己的过滤器,以便在提交/检出时在文件中进行替换。 这些称为“clean”和“smudge”过滤器。 在 .gitattributes 文件中,你可以为特定路径设置过滤器,然后设置脚本,这些脚本将在文件检出之前(“smudge”,参见 “smudge”过滤器在检出时运行)和暂存之前(“clean”,参见 “clean”过滤器在文件暂存时运行)处理文件。 这些过滤器可以设置为执行各种有趣的事情。

The “smudge” filter is run on checkout
图 169. “smudge”过滤器在检出时运行
The “clean” filter is run when files are staged
图 170. “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 匹配的文件时,Git 会在暂存之前通过 indent 程序运行它们,然后在将它们检出到磁盘上之前通过 cat 程序运行它们。 cat 程序基本上什么都不做:它吐出相同的数据。 这种组合有效地在提交之前通过 indent 过滤所有 C 源代码文件。

另一个有趣的例子是获取 $Date$ 关键字扩展,RCS 风格。 为了正确地做到这一点,你需要一个小脚本,它接受一个文件名,找出该项目的最后提交日期,并将日期插入到文件中。 这是一个小的 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 命令中获取最新的提交日期,将其放入它在 stdin 中看到的任何 $Date$ 字符串中,并打印结果——在你最舒服的任何语言中都应该很容易做到这一点。 你可以将此文件命名为 expand_date 并将其放在你的路径中。 现在,你需要在 Git 中设置一个过滤器(称之为 dater),并告诉它使用你的 expand_date 过滤器来涂抹(smudge)检出的文件。 你将使用 Perl 表达式在提交时清理它:

$ git config filter.dater.smudge expand_date
$ git config filter.dater.clean 'perl -pe "s/\\\$Date[^\\\$]*\\\$/\\\$Date\\\$/"'

这个 Perl 代码段会删除它在 $Date$ 字符串中看到的任何内容,以返回到你开始的地方。 现在你的过滤器已准备就绪,你可以通过为该文件设置 Git 属性来测试它,该属性会启用新过滤器并创建一个带有 $Date$ 关键字的文件:

date*.txt filter=dater
$ echo '# $Date$' > date_test.txt

如果你提交这些更改并再次检出该文件,你将看到关键字已正确替换:

$ git add date_test.txt .gitattributes
$ git commit -m "Test 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/ 子目录中有一些测试文件,并且将它们包含在你的项目的 tarball 导出中没有意义。 你可以将以下行添加到你的 Git 属性文件中:

test/ export-ignore

现在,当你运行 git archive 来创建你的项目的 tarball 时,该目录将不会包含在存档中。

export-subst

导出用于部署的文件时,你可以将 git log 的格式化和关键字扩展处理应用于标有 export-subst 属性的文件的选定部分。

例如,如果你想在你的项目中包含一个名为 LAST_COMMIT 的文件,并在 git archive 运行时自动将有关最后一次提交的元数据注入到其中,你可以例如像这样设置你的 .gitattributesLAST_COMMIT 文件:

LAST_COMMIT export-subst
$ echo 'Last commit date: $Format:%cd by %aN$' > LAST_COMMIT
$ git add LAST_COMMIT .gitattributes
$ git commit -am 'adding LAST_COMMIT file for archives'

当你运行 git archive 时,存档文件的内容将如下所示:

$ git archive HEAD | tar xCf ../deployment-testing -
$ cat ../deployment-testing/LAST_COMMIT
Last commit date: Tue Apr 21 08:38:48 2009 -0700 by Scott Chacon

替换可以包括例如提交消息和任何 git notes,并且 git log 可以进行简单的单词换行

$ echo '$Format:Last commit: %h by %aN at %cd%n%+w(76,6,9)%B$' > LAST_COMMIT
$ git commit -am 'export-subst uses git log'\''s custom formatter

git archive uses git log'\''s `pretty=format:` processor
directly, and strips the surrounding `$Format:` and `$`
markup from the output.
'
$ git archive @ | tar xfO - LAST_COMMIT
Last commit: 312ccc8 by Jim Hill at Fri May 8 09:14:04 2015 -0700
       export-subst uses git log's custom formatter

         git archive uses git log's `pretty=format:` processor directly, and
         strips the surrounding `$Format:` and `$` markup from the output.

生成的存档适合于部署工作,但与任何导出的存档一样,它不适合于进一步的开发工作。

合并策略

你还可以使用 Git 属性来告诉 Git 对项目中的特定文件使用不同的合并策略。 一个非常有用的选项是告诉 Git 当它们发生冲突时不要尝试合并特定文件,而是使用你这一方的合并而不是其他人的。

如果你的项目中的某个分支已发散或已专门化,但你想能够从其中合并更改,并且你想忽略某些文件,这将很有帮助。 假设你有一个名为 database.xml 的数据库设置文件,该文件在两个分支中是不同的,并且你想合并你的其他分支而不弄乱数据库文件。 你可以像这样设置一个属性:

database.xml merge=ours

然后使用以下命令定义一个虚拟的 ours 合并策略:

$ git config --global merge.ours.driver true

如果你合并到其他分支中,而不是与 database.xml 文件发生合并冲突,你将看到类似这样的内容:

$ git merge topic
Auto-merging database.xml
Merge made by recursive.

在这种情况下,database.xml 保持在你最初拥有的任何版本。

scroll-to-top