gitattributes中的filter

世有因果知因求果發表於2015-08-29

.gitattributes檔案就是一個簡單的text文字檔案,它的作用是gives attributes to pathnames.

該檔案中的一些配置可以為某些特定目錄或者檔案來設定,這樣Git就僅僅對一個子目錄或者檔案子集來應用規則。這些path-specific配置被稱為Git atttributes並且要麼在你的project root目錄中的.gitattributes檔案來配置或者如果你不想把.gitattributes檔案commit到專案中的話,那麼也可以放在.git/info/attributes檔案中。

該檔案中的每一行都有如下格式:

pattern    attr1 attr2 ...

當pattern匹配了目錄時,那麼在本行中所列出的attributes將會被賦予該目錄

每一個屬性對於特定的目錄可以是以下狀態:

Set
The path has the attribute with special value "true"; this is specified by listing only the name of the attribute in the attribute list.

Unset
The path has the attribute with special value "false"; this is specified by listing the name of the attribute prefixed with a dash - in the attribute list.

Set to a value
The path has the attribute with specified string value; this is specified by listing the name of the attribute followed by an equal sign = and its value in the attribute list.

Unspecified
No pattern matches the path, and nothing says if the path has or does not have the attribute, the attribute for the path is said to be Unspecified.

 

使用attributes,你可以做以下事情:比如對不同的檔案集或者專案中的不同目錄指定不同的merge strategy,告訴git如何來diff非text檔案,或者Git在你check in/check out時 git如何來過濾檔案的內容。

binary檔案

你能使用git attributes的功能來實現的一個很cool的事情是:告訴git哪些檔案是binary二進位制檔案並且給Git關於如何處理這些檔案的特定的指令。比如,一些text檔案有可能是由機器產生的,而不能diff出來,然而有些binary檔案卻可以被diff.你將需要告訴Git誰是誰。

一些檔案看起來像是text檔案,但是可能其目的卻是被用作binary data.比如,Xcode專案包含一個檔案,以.pbxroj為副檔名,這時一耳光簡單的JSON檔案,它記錄了你的build配置等資訊。雖然技術上說,它是一個文字文(因為它都是UTF-8編碼的),但是實際上它確實一個輕量級的資料庫,因此它並不是一個真正的文字檔案,因為他的內容是不能簡單的merge或者diff的。檔案本身主要是由機器來使用的,簡單的說,你應該把它當做binary檔案來對待。

為了告訴git將pbxproj檔案都以binray data來處理,需要增加以下行到.gitattributes檔案中:

*.pbxproj binary

現在Git將不會試圖變換或者fix CRLF的問題;也不會當你執行git show或者git diff時試圖計算或者列印一一些changes。

Diffing Binary Files

你可以使用Git attributes的功能有效地diff二進位制檔案。你可以通過告訴git如何轉換你的二進位制檔案為可以被git diff來比較的通用text格式來實現二進位制檔案的比較功能。

首先,使用這個技術來解決令人煩惱的word檔案變更比較吧。如果你對word文件做版本控制,你會發現一旦執行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 attributes特性,你可以實現像文字檔案一樣來比較不同。在.gitattributes檔案中增加:

*.docx diff=word

這將告訴Git,任何匹配副檔名為.docx的文件當使用git diff命令檢視變更時將使用"word" filter命令。那麼什麼是word filter呢?你必須要配置它。這裡你可以配置git使用docx2txt程式來轉換word文件為一個可讀的text檔案,這樣就能輕鬆實現diff功能了。

首先,你要安裝docx2txt程式,隨後你需要寫一個wrapper指令碼來變換輸出為git期望的格式。建立一個docx2txt的指令碼:

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

將上述指令碼chmod a+x,以便可以執行。最後,配置git來使用這個指令碼:

$ git config diff.word.textconv docx2txt

現在git知道如果試圖執行兩個快照之間的diff時,任何以.docx為副檔名的檔案,它都應該執行word filter(被定義為docx2txt程式)這將有效地在試圖比較他們之前轉換word檔案為普通的text檔案以便比較。

下面是一個輸出的例子:

$ 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.

另外一個你可以通過這種模式來解決的二進位制檔案比較問題的是:image檔案比較。一種方法是通過一個能夠抽取image檔案的EXIF資訊(metadata資訊)的filter來執行image files比較。你可以下載並且安裝exiftool程式,你使用它來轉換image檔案為你所需要的關於metadata的text檔案。

$ echo '*.png diff=exif' >> .gitattributes
$ git config diff.exif.textconv exiftool

keyword expansion

SVN-/CVS-Style keyword expansion經常被開發人員提出需求。在git中這個keyword expansion功能的主要問題是:你不能修改commit的任何資訊,雖然注入text到一個檔案中不被允許,因為git使用checksum機制來確保檔案安全。然而,你可以在checkout時注入,而在放到commit時刪除一段text來規避這種情況。

首先你可以自動注入一個blob的SHA-1 checksum到一個$Id$域中。如果你在一個檔案或一組檔案中設定這個attribute,那麼下一次你checkout那個branch時,git會自動更換使用那個blob的SHA-1資訊來更換那個$Id$域。需要注意的是那個id不是commit的sha-1而是blob的sha-1 checksum:

$ echo '*.txt ident' >> .gitattributes
$ echo '$Id$' > test.txt

下一次你checkout這個檔案時,git將注入blob的sha-1:

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

然而,那個結果卻只能限制使用。如果你使用過CVS的keyword substitution功能,你可以包含一個datastamp資訊。

如何實現呢?你可以通過寫一個你自己的filter來實現黨檔案commit/checkout時自動替換這個資訊。這些功能需要通過"clean"和"smudge" filter來實現。在.gitattributes檔案中,你可以為一些特定路徑來設定一個filter,隨後設定當他們被checkout(smudge)時需要執行的處理該檔案的指令碼程式,以及當他們將被stage時(“clean"filter).這些filter可以被用來執行許多有趣的事情:

當checkout時執行上述smudge filter;

當檔案被staged(commit)時執行clean filter

前面介紹過通過一個indent filter program來執行C原始碼在commit之前自動indent的功能。回顧一下:你通過設定.gitattributes中的filter屬性來配置所有*.c檔案將使用indent filter

*.c filter=indent

然後,告訴git,indent filter在smudge和clean時,該indent filter應該做什麼:

$ git config --global filter.indent.clean indent
$ git config --global filter.indent.smudge cat

在上面這個例子中,當你commit *.c檔案時,git將在commit之前執行indent程式,在checkout之前執行cat程式。這個組合便有效地在commit之前filter了所有c源程式程式碼。

另外一個有趣的例子是:獲取$Date$keyword expansion.為了有效實現它,你需要一個小的指令碼,該指令碼帶一個檔名,指出最近的commit日期,並且插入到檔案中。下面是一耳光ruby script:

#! /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命令中獲取最新的commit日期,並把該資訊放到任何它在stdin中發現的$Date$ string,並且列印結果。你可以將該檔案命名為expand_date並且放到你的path中。現在你需要設定一耳光filter(我們就叫他dater filter),並且告訴git使用expand_date filter來smudge files on checkout。你需要一個perl expression 在commit時清除掉:

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

這個perl指令碼strips out anything it sees in a $Date$ string。現在你的filter已經就緒,你可以通過建立一個包含$Date$關鍵詞的檔案然後建立filter:

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

如果你commit那些變更並且checkout檔案,你可以看到關鍵詞已經被替換:

$ git add date_test.txt .gitattributes
$ git commit -m "Testing 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檔案被commit了,並且被隨著專案repo來傳播,然而由於driver(這裡是dater)並不會隨著project repo自然就ready,所以有可能在一些情況下並不能工作。當你設計這些filter時,他們可能會優雅地失敗,但專案本身仍然能夠正常工作。

Exporting your repository

Git attribute data也允許你在exporting專案的archive時做一些有趣的事情。

export-ignore

你可以告訴git不要在生成一個archive時,不要export一些檔案或者目錄。如果有一些子目錄或者檔案你不希望放到archive檔案中但是你又需要放到專案checkout的working directory中去時,你可以通過export-ignore這個attribute來指定。

例如,你有一些test檔案在test/目錄下,它本身對於export你的專案作為一個歸檔並沒有意義,你可以增加下面的行在git attributes檔案中:

test/ export-ignore

現在當你執行git archive來建立project的tarball時,那個目錄並不會放入

注意:下面這個git archive命令工作的前提是.gitattributes(包含export-ignore屬性指示)已經commit到庫中了!~

git archive --format=zip -o test233.zip HEAD

export-subst

當輸出檔案以便部署時,你可以應用git log的格式和keyword-expansion來選擇那些被標示為export-subst屬性的子檔案集。

例如,如果你希望包含一個命名為LAST_COMMIT的檔案在你的專案中,並且關於最後的commit的metadata資訊在git archive命令執行時自動注入那個檔案中,你可以設定下面的資訊:

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

當你執行git archive時,那麼achived file將由以下內容:

$ 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

被替換的資訊可以包含commit message和任何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.

產生的archive適合於deployment,但是像任何exported archive一樣,它並不適合做任何繼續的開發。

//注意所有git archive 支援的placeholder都在git help log under the --pretty-format section可以看到

merge strategies:

你可以使用git attributes來告訴git對特定的專案檔案使用不同的merge strategies。一個非常有用的選項是告訴git當發生衝突時不要試圖merge特定的檔案,而是使用你優選的衝突方來作為最後merge的結果。

比如你有一個database.xml檔案包含了資料庫的配置資訊,該檔案在兩個branch中是不同的,而你希望merge in your other branch without messing up the database file.你可以通過設定一個屬性如下:

database.xml merge=ours

然後頂一個一個dummy ours merge strategy with:

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

 

如果你merge in the other branch,不會有database.xml檔案的merge conflicts,將會繼續保留你在原本branch上的檔案內容,你看到如下內容:

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

在這種情況下,database.xml將停留在branch to merge into的分支上的原來版本上。也就是說你在master branch上做merge topic,那麼將保留master上的database.xml檔案,不會有任何衝突

 

$: mkdir gitest
$: cd gittest
$: git init
$: echo "setup merge=ours" >> .gitattributes 
$: echo "master" >> setup
$: git add setup .gitattributes
$: git commit -a -m ...
$: git branch test
$: git checkout test
$: echo "test" >> setup
$: git commit -a -m ...
$: git checkout master
$: git merge test
Expected result: setup contains the word "master", instead git performs a ff merge and setup is "test'.)

上面的snippet可以通過在~/.gitconfig檔案或者.git/config檔案中增加以下section來實現:

[merge "ours"]
    name = "Keep ours merge"
    driver = true

 

相關文章