章节 ▾ 第二版

8.2 定制 Git - Git Attributes

Git Attributes

其中一些设置也可以针对特定路径进行指定,这样 Git 只会将这些设置应用于某个子目录或部分文件。这些路径特定的设置被称为 Git 属性,可以在你的一个目录(通常是项目的根目录)中的 .gitattributes 文件中设置,或者如果你不想将属性文件提交到项目中,则可以在 .git/info/attributes 文件中设置。

使用属性,你可以为项目中的单个文件或目录指定不同的合并策略,告诉 Git 如何区分非文本文件,或者让 Git 在文件提交到或取出 Git 之前过滤内容。在本节中,你将学习一些可以在 Git 项目中设置的路径属性,并看到一些实际使用此功能的示例。

二进制文件

你可以使用 Git 属性的一个很酷的技巧是告诉 Git 哪些文件是二进制的(在它可能无法自行判断的情况下),并为 Git 提供处理这些文件的特殊指令。例如,有些文本文件可能是机器生成的,不可进行差异比较,而有些二进制文件则可以进行差异比较。你将看到如何告诉 Git 哪些是哪些。

识别二进制文件

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

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

*.pbxproj binary

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

二进制文件差异比较

你还可以使用 Git 属性功能来有效地比较二进制文件。你通过告诉 Git 如何将二进制数据转换为文本格式来实现,以便可以通过常规差异比较进行比较。

首先,你将使用此技术来解决人类已知的最烦人的问题之一:版本控制 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 文件创建了不错的基于文本的版本。

这是一个示例:本书的第一章被转换为 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 风格的关键字扩展经常被习惯于这些系统的开发人员要求。这在 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 是否比另一个更旧或更新。

事实证明,你可以编写自己的过滤器来在提交/检出时对文件进行替换。这些被称为“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