章節 ▾ 第二版

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