Git
Chapters ▾ 1st Edition

7.2 Настройка Git - Git-атрибуты

Git-атрибуты

Некоторые настройки могут быть заданы для отдельных путей, и тогда Git будет применять их только для некоторых подкаталогов или набора файлов. Такие настройки, специфичные по отношению к путям, называются атрибутами и задаются либо в файле .gitattributes в одном из каталогов проекта (обычно в корне), либо в файле .git/info/attributes, если вы не хотите, чтобы файл с атрибутами попал в коммит вместе с остальными файлами проекта.

Использование атрибутов позволяет, например, задать разные стратегии слияния для отдельных файлов или каталогов проекта, или объяснить Git'у, как сравнивать нетекстовые файлы, или сделать так, чтобы Git пропускал данные через фильтр перед тем, как выгрузить или записать данные в репозиторий. В этом разделе мы рассмотрим некоторые из доступных в Git'е атрибутов и рассмотрим несколько практических примеров их использования.

Бинарные файлы

Есть один клёвый трюк, для которого можно использовать атрибуты — можно указать Git'у, какие файлы являются бинарными (в случае если по-другому определить это не получается), и дать ему специальные инструкции о том, как с этими файлами работать. Например, некоторые текстовые файлы могут быть машинными — генерируемыми программой — для них нет смысла вычислять дельты, в то время как для некоторых бинарных файлов получение дельт может быть полезным. Дальше мы увидим, как сказать Git'у, какие файлы какие.

Определение бинарных файлов

Некоторые файлы выглядят как текстовые, но по существу должны рассматриваться как бинарные данные. Например, проекты Xcode на Mac'ах содержат файл, оканчивающийся на .pbxproj, который, по сути, является набором JSON-данных (текстовый формат данных для javascript), записываемым IDE, в котором сохраняются ваши настройки сборки и прочее. Хоть технически это и текстовый файл, потому что содержит только ASCII-символы, но нет смысла рассматривать его как таковой, потому что на самом деле это легковесная база данных — вы не сможете слить её содержимое, если два человека внесут в неё изменение, получение дельт тоже, как правило, ничем вам не поможет. Этот файл предназначается для обработки программой. По сути, лучше рассматривать этот файл как бинарный.

Чтобы заставить Git обращаться со всеми pbxproj-файлами как с бинарными, добавьте следующую строку в файл .gitattributes:

*.pbxproj -crlf -diff

Теперь Git не будет пытаться конвертировать CRLF-концы строк или исправлять проблемы с ними. Также он не будет пытаться получить дельту для изменений в этом файле при запуске git show или git diff в вашем проекте. В Git'е есть предустановленный макрос binary, который означает то же, что и -crlf -diff:

*.pbxproj binary

Получение дельты для бинарных файлов

Функциональность атрибутов Git'а может быть использована для эффективного получения дельт бинарных файлов. Сделать это можно, объяснив Git'у, как сконвертировать ваши бинарные данные в текстовый формат, для которого можно выполнить сравнение с помощью обычного diff. Осталось только понять, как получить текстовое представление для бинарных данных. Идеальный вариант — найти подходящую утилиту для конвертирования нужного формата в текстовый вид. Но, к сожалению, получить хорошее текстовое представление можно только для весьма ограниченного набора бинарных форматов. Для большинства же бинарных форматов, например, для графических или аудио данных, получить читаемый текстовый вид не представляется возможным. Но если мы не можем получить текстовое представление содержимого, мы зачастую можем получить читаемое описание содержимого или метаданные. Метаданные не дают полное представление о содержимом файле, но, во всяком случае, это лучше чем ничего.

Далее мы рассмотрим оба подхода на примерах популярных бинарных форматов.

Замечание: Существуют разные виды бинарных файлов с текстовым содержимым, для которых вам, может быть, не удастся найти подходящий конвёртер. В данном случае вы можете попробовать вытащить текст с помощью утилиты strings. Некоторые из таких файлов могут использовать кодировку UTF-16 или могут быть написаны не в латинице, в таких файлах strings не найдёт ничего хорошего. Полезность strings может сильно варьироваться. Тем не менее, strings доступен на большинстве Mac- и Linux-систем, так что он может быть хорошим первым вариантом для того, чтобы сделать подобное со многими бинарными форматами.

Документы MS Word

Для начала мы используем описанный подход, чтобы решить одну из самых раздражающих проблем, известных человечеству: версионный контроль документов Word. Всем известно, что Word — это самый ужасающий из всех существующих редакторов, но, как ни странно, все им пользуются. Если вы хотите поместить документы Word под версионный контроль, вы можете запихнуть их в Git-репозиторий и время от времени делать коммиты. Но что в этом хорошего? Если вы запустите git diff как обычно, то увидите только что-то наподобие этого:

$ git diff 
diff --git a/chapter1.doc b/chapter1.doc
index 88839c4..4afcb7c 100644
Binary files a/chapter1.doc and b/chapter1.doc differ

У вас не получится сравнить две версии между собой, только если вы не выгрузите их обе и просмотрите их вручную, так? Оказывается, можно сделать это достаточно успешно, используя атрибуты Git'а. Поместите следующую строку в свой файл .gitattributes:

*.doc diff=word

Она говорит Git'у, что все файлы, соответствующие указанному шаблону (.doc) должны использовать фильтр "word" при попытке посмотреть дельту с изменениями. Что такое фильтр "word"? Нам нужно его изготовить. Сейчас мы настроим Git на использование программы catdoc, специально написанной для того, чтобы вытаскивать текстовую информацию из бинарных документов MS Word (скачать её можно по адресу http://www.45.free.net/~vitus/software/catdoc/), для конвертирования документов Word в читаемые текстовые файлы, которые Git затем правильно сравнит:

$ git config diff.word.textconv catdoc

Этой командой в свой .git/config вы добавите следующую секцию:

[diff "word"]
    textconv = catdoc

Теперь Git знает, что если ему надо найти дельту между двумя снимками состояния, и какие-то их файлы заканчиваются на .doc, он должен прогнать эти файлы через фильтр "word", который определён как программа catdoc. Так вы фактически сделаете текстовые версии своих Word-файлов перед тем, как получить для них дельту.

Рассмотрим пример. Я поместил главу 1 настоящей книги в Git, добавил немного текста в один параграф и сохранил документ. Затем я выполнил git diff, чтобы увидеть, что изменилось:

$ git diff
diff --git a/chapter1.doc b/chapter1.doc
index c1c8a0a..b93c9e4 100644
--- a/chapter1.doc
+++ b/chapter1.doc
@@ -128,7 +128,7 @@ and data size)
 Since its birth in 2005, Git has evolved and matured to be easy to use
 and yet retain these initial qualities. It’s incredibly fast, it’s
 very efficient with large projects, and it has an incredible branching
-system for non-linear development.
+system for non-linear development (See Chapter 3).

Git коротко и ясно дал мне знать, что я добавил строку "(See Chapter 3)", так оно и есть. Работает идеально.

Текстовые файлы в формате OpenDocument

Тот же подход, который мы использовали для файлов MS Word (*.doc), может быть использован и для текстовых файлов в формате OpenDocument, созданных в OpenOffice.org.

Добавим следующую строку в файл .gitattributes:

*.odt diff=odt

Теперь настроим фильтр odt в .git/config:

[diff "odt"]
    binary = true
    textconv = /usr/local/bin/odt-to-txt

Файлы в формате OpenDocument на самом деле являются запакованными zip'ом каталогами с множеством файлов (содержимое в XML-формате, таблицы стилей, изображения и т.д.). Мы напишем свой сценарий для извлечения содержимого и вывода его в виде обычного текста. Создайте файл /usr/local/bin/odt-to-txt (можете создать его в любом другом каталоге) со следующим содержимым:

#! /usr/bin/env perl
# Сценарий для конвертации OpenDocument Text (.odt) в обычный текст.
# Автор: Philipp Kempgen

if (! defined($ARGV[0])) {
    print STDERR "Не задано имя файла!\n";
    print STDERR "Использование: $0 имя файла\n";
    exit 1;
}

my $content = '';
open my $fh, '-|', 'unzip', '-qq', '-p', $ARGV[0], 'content.xml' or die $!;
{
    local $/ = undef;  # считываем файл целиком
    $content = <$fh>;
}
close $fh;
$_ = $content;
s/<text:span\b[^>]*>//g;           # удаляем span'ы
s/<text:h\b[^>]*>/\n\n*****  /g;   # заголовки
s/<text:list-item\b[^>]*>\s*<text:p\b[^>]*>/\n    --  /g;  # элементы списков
s/<text:list\b[^>]*>/\n\n/g;       # списки
s/<text:p\b[^>]*>/\n  /g;          # параграфы
s/<[^>]+>//g;                      # удаляем все XML-теги
s/\n{2,}/\n\n/g;                   # удаляем подряд идущие пустые строки
s/\A\n+//;                         # удаляем пустые строки в начале
print "\n", $_, "\n\n";

Сделайте его исполняемым

chmod +x /usr/local/bin/odt-to-txt

Теперь git diff сможет сказать вам, что изменилось в .odt файлах.

Изображения

Ещё одна интересная проблема, которую можно решить таким способом, это сравнение файлов изображений. Один из способов сделать это — прогнать PNG-файлы через фильтр, извлекающий их EXIF-информацию — метаданные, которые дописываются в большинство форматов изображений. Если скачаете и установите программу exiftool, то сможете воспользоваться ею, чтобы извлечь из изображений текстовую информацию о метаданных, так чтобы diff хоть как-то показал вам текстовое представление произошедших изменений:

$ echo '*.png diff=exif' >> .gitattributes
$ 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:17 10:12:35-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 сначала считает контрольную сумму для файла. Несмотря на это, вы можете вставлять текст в файл во время его выгрузки и удалять его перед добавлением в коммит. Атрибуты Git'а предлагают два варианта сделать это.

Во-первых, вы можете внедрять SHA-1-сумму блоба в поле $Id$ в файл автоматически. Если установить соответствующий атрибут для одного или нескольких файлов, то в следующий раз, когда вы будете выгружать данные из этой ветки, Git будет заменять это поле SHA-суммой блоба. Обратите внимание, что это SHA-1 не коммита, а самого блоба.

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

В следующий раз, когда вы будете выгружать этот файл, Git автоматически вставит в него SHA его блоба:

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

Однако, такой результат мало применим. Если вы раньше пользовались развёртыванием ключа в CVS или Subversion, можете добавлять метку даты — SHA не особенно полезен, так как он довольно случаен, и к тому же, глядя на две SHA-суммы, никак не определить какая из них новее.

Как оказывается, можно написать свои собственные фильтры, которые будут делать подстановки в файлах при коммитах и выгрузке файлов. Для этого надо задать фильтры "clean" и "smudge". В файле .gitattributes можно задать фильтр для определённых путей и затем установить сценарии, которые будут обрабатывать файлы непосредственно перед выгрузкой ("smudge", см. рис. 7-2) и прямо перед коммитом ("clean", см. рис. 7-3). Эти фильтры можно настроить на совершение абсолютно любых действий.


Рисунок 7-2. Фильтр “smudge” выполняется при checkout.


Рисунок 7-3. Фильтр “clean” выполняется при помещении файлов в индекс.

В сообщении первоначального коммита, добавляющего эту функциональность, дан простой пример того, как можно пропустить весь свой исходный код на C через программу indent перед коммитом. Сделать это можно, задав атрибут filter в файле .gitattributes так, чтобы он пропускал файлы *.c через фильтр "indent":

*.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, по сути, является холостой — она выдаёт те же данные, которые получила. Фактически эта комбинация профильтровывает все файлы с исходным кодом на C через indent перед тем, как сделать коммит.

Ещё один интересный пример — это развёртывание ключа $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$, которые видит в stdin, и выводит результат — такое должно быть несложно реализовать на любом удобном вам языке. Давайте назовём этот файл 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$ так, чтобы вернуть всё в начальное состояние. Теперь, когда наш фильтр готов, можете протестировать его, создав файл с ключом $Date$ и установив для этого файла Git-атрибут, который задействует для него новый фильтр:

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

Если мы сейчас добавим эти изменения в коммит и снова выгрузим файл, то мы увидим, что ключевое слово было заменено правильно:

$ 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 вы добавите в коммит и будете его распространять вместе с проектом, а драйвер (в нашем случае dater) — нет. Так что не везде оно будет работать. Когда будете проектировать свои фильтры, постарайтесь сделать так, чтобы при возникновении в них ошибки проект не переставал работать правильно.

Экспорт репозитория

Ещё атрибуты в Git'е позволяют делать некоторые интересные вещи при экспортировании архива с проектом.

export-ignore

Вы можете попросить Git не экспортировать определённые файлы и каталоги при создании архива. Если у вас есть подкаталог или файл, который вы не желаете включать в архив, но хотите, чтобы в проекте он был, можете установить для такого файла атрибут export-ignore.

Например, скажем, у вас в подкаталоге test/ имеются некоторые тестовые файлы, и нет никакого смысла добавлять их в тарбол при экспорте проекта. Тогда добавим следующую строку в файл с Git-атрибутами:

test/ export-ignore

Теперь, если вы запустите git archive, чтобы создать тарбол с проектом, этот каталог в архив включён не будет.

export-subst

Ещё одна вещь, которую можно сделать с архивами, — это сделать какую-нибудь простую подстановку ключевых слов. Git позволяет добавить в любой файл строку вида $Format:$ с любыми кодами форматирования, доступными в --pretty=format (многие из этих кодов мы рассматривали в главе 2). Например, если вам захотелось добавить в проект файл с именем LAST_COMMIT, в который при запуске git archive будет автоматически помещаться дата последнего коммита, то такой файл вы можете сделать следующим образом:

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

После запуска git archive этот файл у вас в архиве будет иметь содержимое следующего вида:

$ cat LAST_COMMIT
Last commit date: $Format:Tue Apr 21 08:38:48 2009 -0700$

Стратегии слияния

Атрибуты Git'а могут также быть использованы для того, чтобы попросить Git использовать другие стратегии слияния для определённых файлов в проекте. Одна очень полезная возможность — это сказать Git'у, чтобы он не пытался слить некоторые файлы, если для них есть конфликт, а просто выбрал ваш вариант, предпочтя его чужому.

Это полезно в том случае, если ветка в вашем проекте разошлась с исходной, но вам всё же хотелось бы иметь возможность слить изменения из неё обратно, проигнорировав некоторые файлы. Скажем, у вас есть файл с настройками базы данных, который называется database.xml, и в двух ветках он разный, и вы хотите влить другую свою ветку, не трогая файл с настройками базы данных. Задайте атрибут следующим образом:

database.xml merge=ours

При вливании другой ветки, вместо конфликтов слияния для файла database.xml, вы увидите следующее:

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

В данном случае database.xml остался в том варианте, в каком и был изначально.