Git
章节 ▾ 第 2 版

8.2 自定义 Git - Git 属性

Git 属性

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

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

二进制文件

可以使用 Git 属性的一个很酷的技巧是告诉 Git 哪些文件是二进制文件(在 Git 无法识别的情况下),并向 Git 提供有关如何处理这些文件的特殊说明。例如,某些文本文件可能是机器生成的并且不可 diff,而某些二进制文件可以 diff。你将了解如何告诉 Git 哪个是哪个。

识别二进制文件

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

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

*.pbxproj binary

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

二进制文件的 diff

你还可以使用 Git 属性功能来有效地对二进制文件进行 diff。为此,你需要告诉 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 文档转换为可读的文本文件,然后它将正确地进行比较。

首先,您需要安装 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 样式的关键字扩展。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 过滤器在检出时弄脏文件。您将使用 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