章节 ▾ 第二版

8.2 自定义 Git - Git 属性

Git 属性

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

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

二进制文件

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

识别二进制文件

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

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

*.pbxproj binary

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

区分二进制文件

你还可以使用 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”过滤器来运行这些文件,而“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 命令获取最新的提交日期,将其放入它在标准输入中看到的任何 $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$ 字符串中它看到的任何内容,以恢复到你开始时的状态。现在你的过滤器已准备就绪,你可以通过为该文件设置一个 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 会保持你最初的版本。