Git
Chapters ▾ 2nd Edition

8.2 Git einrichten - Git-Attribute

Git-Attribute

Einige dieser Einstellungen können auch für einen Pfad angegeben werden, so dass Git diese Einstellungen nur für ein Unterverzeichnis oder eine Teilmenge von Dateien anwendet. Diese pfadspezifischen Einstellungen heißen Git-Attribute und werden entweder in einer .gitattributes Datei in einem deiner Verzeichnisse (normalerweise dem Stammverzeichnis deines Projekts) oder in der .git/info/attributes Datei festgelegt, falls du nicht willst, dass die Attributdatei mit deinem Projekt verknüpft wird.

Mit Hilfe von Attributen kannst du beispielsweise separate Merge-Strategien für einzelne Dateien oder Verzeichnisse eines Projekts festlegen, Git sagen, wie man Nicht-Text-Dateien unterscheidet oder den Inhalt von Git filtern lassen, bevor du ihn in Git ein- oder auscheckst. In diesem Abschnitt erfährst du mehr über einige der Attribute, die du in deinem Git-Projekt auf deine Pfade setzen kannst, und siehst einige Beispiele für die praktische Anwendung dieser Funktion.

Binäre Dateien

Ein toller Kniff, für den du Git-Attribute verwenden kannst, ist das Erkennen von binären Dateien (falls es sonst nicht möglich ist, es herauszufinden) und Git anzuweisen, wie man mit diesen Dateien umgeht. So können beispielsweise einige Textdateien maschinell erzeugt und nicht mehr gedifft werden, während andere Binärdateien gedifft werden können. Du wirst sehen, wie du Git mitteilen kannst, welche welche ist.

Binärdateien identifizieren

Einige Dateien sehen aus wie Textdateien, sind aber im Grunde genommen wie Binärdateien zu behandeln. Xcode-Projekte unter macOS enthalten beispielsweise eine Datei, die mit .pbxproj endet. Diese ist im Grunde genommen ein JSON-Datensatz (Klartext JavaScript Datenformat), der von der IDE auf die Festplatte geschrieben wird, deine Build-Einstellungen aufzeichnet usw. Obwohl es sich technisch gesehen um eine Textdatei handelt (weil sie eigentlich komplett UTF-8 ist), willst du sie nicht als solche behandeln, denn es handelt sich hierbei um eine sehr einfache Datenbank. Du kannst den Inhalt nicht mergen, wenn zwei Personen ihn ändern. Diffs sind im Allgemeinen nicht hilfreich. Die Datei ist für den internen Betrieb auf einem Rechner bestimmt. Im Prinzip solltest du sie wie eine Binärdatei behandeln.

Um Git anzuweisen, alle pbxprojj Dateien als Binärdaten zu behandeln, fügst du die folgende Zeile zu deine .gitattributes Datei hinzu:

*.pbxproj binary

Jetzt wird Git nicht versuchen, CRLF-Probleme zu konvertieren oder zu beheben; noch wird es versuchen, ein Diff für Änderungen in dieser Datei zu berechnen oder auszugeben, wenn du git show oder git diff bei deinem Projekt ausführst.

Unterschiede in Binärdateien vergleichen

Du kannst auch die Git-Attribut-Funktionalität verwenden, um Binärdateien effektiv zu unterscheiden. Dafür musst du Git mitteilen, wie es deine Binärdateien in ein Textformat konvertieren soll, das über das normale diff verglichen werden kann.

Vielleicht wirst du diese Technik nutzen, um eines der lästigsten Probleme zu lösen, die es gibt: die Versionskontrolle von Microsoft Word-Dokumenten. Wenn du Word-Dokumente versionieren willst, kannst du sie in ein Git-Repository legen und ab und zu committen; aber was nützt das? Wenn du git diff wie gewohnt startest, siehst du nur so etwas wie das hier:

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

Du kannst zwei Versionen nicht direkt vergleichen, es sei denn, du checkst sie aus und scannst sie manuell, richtig? Wie sich herausstellt, kann man das ziemlich gut mit Git-Attributen machen. Füge die folgende Zeile in deine .gitattributes Datei ein:

*.docx diff=word

Hiermit erfährt Git, dass jede Datei, die mit diesem Suchmuster übereinstimmt (.docx), den Filter „word“ verwenden sollte, wenn du einen Diff anzeigen lassen willst, der Änderungen enthält. Was ist der Filter „word“? Du musst ihn einrichten. Nachfolgend konfigurierst du Git so, dass es das Programm docx2txt verwendet, um Word-Dokumente in lesbare Textdateien zu konvertieren, die es dann richtig auswertet.

Zuerst musst du docx2txt installieren. Du kannst es von https://sourceforge.net/projects/docx2txt herunterladen. Folgen den Anweisungen in der INSTALL Datei, um es an einen Ort zu platzieren, an dem deine Shell es finden kann. Als nächstes schreibst du ein Wrapper-Skript, um die Ausgabe in das von Git erwartete Format zu konvertieren. Erstellen eine Datei namens docx2txt, irgendwo innerhalb deines Pfads und füge diesen Inhalt hinzu:

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

Vergiss nicht, die Dateirechte mit chmod a+x zu ändern. Schließlich kannst du Git so einrichten, dass es dieses Skript verwendet:

$ git config diff.word.textconv docx2txt

Jetzt ist Git darüber informiert, dass es bei dem Versuch, einen Diff zwischen zwei Snapshots zu machen, der Dateien enthält, die auf .docx enden, diese Dateien durch den „word“ Filter laufen lassen soll, der als docx2txt Programm definiert ist. Auf diese Weise entstehen im Vorfeld gute Textversionen deiner Word-Dateien, die du leichter diffen kannst.

Hier ist ein Beispiel: Kapitel 1 dieses Buches wurde in das Word-Format konvertiert und in ein Git-Repository committet. Dann wurde ein neuer Absatz hinzugefügt. Hier ist das, was git diff zeigt:

$ 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 sagt uns sehr treffend und klar, dass wir die Zeichenkette „Testing: 1, 2, 3.“ hinzugefügt haben, was richtig ist. Was aber nicht perfekt ist: Formatierungsänderungen würden hier nicht angezeigt – aber es funktioniert.

Ein weiteres spannendes Problem, das du auf diese Weise lösen kannst, ist die diffen von Bilddateien. Die eine Methode besteht darin, Bilddateien durch einen Filter zu leiten, der ihre EXIF-Informationen extrahiert – das sind Metadaten, die mit den meisten Bildformaten aufgezeichnet werden. Nach dem Herunterladen und Installieren des exiftool Programms kannst du damit die Metadaten deiner Bilder in Text umwandeln. So zeigt dir das Diff zumindest eine schriftliche Darstellung der vorgenommenen Änderungen an. Füge die folgende Zeile in deine .gitattributes Datei ein:

*.png diff=exif

So konfigurierst du Git, um dieses Tool zu verwenden:

$ git config diff.exif.textconv exiftool

Wenn du ein Bild in deinem Projekt ersetzt und dann git diff ausführst, siehst du etwas ähnliches wie hier:

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

Du kannst leicht erkennen, dass sich sowohl die Dateigröße als auch die Bildgröße geändert hat.

Schlüsselwort-Erweiterung

Die SVN- oder CVS-artige Schlüsselwort-Erweiterung wird oft von Entwicklern gefordert, die mit diesen Systemen arbeiten. Das Hauptproblem in Git ist, dass du eine Datei mit Informationen über einen Commit nach dem Committen nicht ändern kannst, da zuerst die Prüfsumme der Datei von Git ermittelt wird. Wenn die Datei ausgecheckt ist kannst du jedoch Text in sie einfügen und ihn wieder entfernen, bevor er zu einem Commit hinzugefügt wird. Die Git-Attribute stellen dir dafür zwei Möglichkeiten zur Verfügung.

Erstens kannst du die SHA-1-Prüfsumme eines Blobs automatisch in ein $Id$ Feld in der Datei einfügen. Wird dieses Attribut auf eine Datei oder eine Gruppe von Dateien gesetzt, dann wird Git beim nächsten Auschecken dieses Branchs das entsprechende Feld durch den SHA-1 des Blobs ersetzen. Dabei muss man beachten, dass es nicht das SHA-1 des Commits ist, sondern das des Blobs an sich. Füge die folgende Zeile in deine .gitattributes Datei ein:

*.txt ident

Füge eine $Id$ Referenz einer Testdatei hinzu:

$ echo '$Id$' > test.txt

Beim nächsten Auschecken dieser Datei fügt Git den SHA-1 des Blobs ein:

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

Dieses Ergebnis ist jedoch von begrenztem Nutzen. Wenn du die Ersetzung von Schlüsselwörtern in CVS oder Subversion verwendet hast, kannst du einen Datums-Stempel hinzufügen. Der SHA-1 ist nicht allzu hilfreich, weil er eher willkürlich ist und du nicht erkennen kannst, ob ein SHA-1 älter oder neuer als ein anderer ist.

Es zeigt sich, dass du deine eigenen Filter schreiben kannst, um Ersetzungen in Dateien bei einem Commit/Checkout durchzuführen. Diese werden als „saubere“ (engl. clean) und „verschmutzte“ (engl. smudge) Filter bezeichnet. In der Datei .gitattributes kannst du einen Filter für bestimmte Pfade setzen und dann Skripte einrichten, die die Dateien verarbeiten, kurz bevor sie ausgecheckt werden (für „smudge“, siehe Der „smudge“ Filter wird beim Auschecken ausgeführt) und kurz bevor sie zum Commit vorgemerkt werden (für „clean“, siehe Der „clean“ Filter wird ausgeführt, wenn Dateien zum Commit vorgemerkt werden). Diese Filter können so eingestellt werden, um damit alle möglichen interessanten Aufgaben zu erledigen.

Der „smudge“ Filter wird beim Auschecken ausgeführt
Abbildung 169. Der „smudge“ Filter wird beim Auschecken ausgeführt
Der „clean“ Filter wird ausgeführt
Abbildung 170. Der „clean“ Filter wird ausgeführt, wenn Dateien zum Commit vorgemerkt werden

Die ursprüngliche Commit-Meldung dieser Funktion zeigt ein einfaches Anwendungsbeispiel, wie du deinen C-Quellcode vor dem Commit durch das indent Programm laufen lassen kannst. Du kannst es so einrichten, dass das Filterattribut in deiner .gitattributes Datei so gesetzt ist, dass *.c Dateien mit dem Filter „indent“ gefiltert werden:

*.c filter=indent

Dann weist du Git an, was der „indent“ Filter bei smudge und clean bewirkt:

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

Wenn du nun Dateien committest, die mit *.c übereinstimmen, wird Git sie das Indent-Programm durchlaufen lassen, bevor es sie der Staging-Area hinzufügt. Dann werden sie durch das cat Programm laufen, bevor es sie wieder auf die Festplatte auscheckt. Das cat Programm macht im Grunde genommen nichts: Es übergibt die gleichen Daten, die es bekommt. Diese Kombination filtert effektiv alle C-Quellcode-Dateien durch Einrückung (engl. indent), bevor sie committet werden.

Ein weiteres interessantes Beispiel ist die $Date$ Schlüsselwort-Erweiterung im RCS-Stil. Um das richtig anzuwenden, benötigst du ein kleines Skript, das einen Dateinamen entgegennimmt, das letzte Commit-Datum für dieses Projekt ermittelt und das Datum in die Datei einfügt. Hier ist ein kleines Ruby-Skript, das das erledigt:

#! /usr/bin/env ruby
data = STDIN.read
last_date = `git log --pretty=format:"%ad" -1`
puts data.gsub('$Date$', '$Date: ' + last_date.to_s + '$')

Alles, was das Skript macht, ist das neueste Commit-Datum aus dem git log Befehl zu ermitteln, es in alle $Date$ Zeichenketten zu ersetzen, die es in stdin erkennt und das Ergebnis auszugeben. Es sollte nicht schwierig sein, in der Programmiersprache zu erstellen mit der du am besten zurechtkommst. Du kannst diese Datei expand_date nennen und in deinem Pfad aufnehmen. Nun musst du einen Filter in Git einrichten (nenne ihn z.B. dater) und ihm sagen, dass er deinen expand_date Filter verwenden soll, um die Dateien beim Auschecken zu bearbeiten (engl. smudge). Ein Perl-Ausdruck dient dazu, das beim Commit zu bereinigen:

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

Dieses Perl-Snippet entfernt alles, was es in einer $Date$ Zeichenkette sieht, um an den Ausgangspunkt zurückzukehren. Nachdem dein Filter jetzt fertig ist, kannst du ihn testen, indem du ein Git-Attribut für diese Datei einrichtest, das den neuen Filter aktiviert und eine Datei mit deinem $Date$ Schlüsselwort erstellst:

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

Wenn du diese Änderungen committest und die Datei erneut auscheckst, ist das Schlüsselwort ordnungsgemäß ersetzt:

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

Hier siehst du, wie leistungsfähig diese Technik für maßgeschneiderte Anwendungen sein kann. Du solltest jedoch vorsichtig sein, da die .gitattributes Datei committed und mit dem Projekt weitergegeben wird, der Treiber (hier: dater) aber nicht. Es wird also nicht überall funktionieren. Wenn du diese Filter entwirfst, sollten deine Mitstreiter, bei fehlerhaften Ergebnissen, dennoch problemlos in der Lage sein das Projekt einwandfrei laufen zu lassen.

Exportieren deiner Repositorys

Mit Git-Attribut-Daten kannst du interessante Aufgaben beim Exportieren deines Projekts erledigen.

export-ignore

Du kannst Git anweisen, bestimmte Dateien oder Verzeichnisse nicht zu exportieren, wenn du ein Archiv erstellst. Wenn es ein Unterverzeichnis oder eine Datei gibt, die du nicht in deine Archivdatei aufnehmen möchtest, aber in dein Projekt einchecken möchtest, kannst du diese Dateien über das Attribut export-ignore markieren.

Angenommen, du hast einige Testdateien in einem test/ Unterverzeichnis und es ist nicht sinnvoll, sie in den Tarball-Export deines Projekts aufzunehmen. Man kann die folgende Zeile zu der Datei mit den Git-Attributen hinzufügen:

test/ export-ignore

Wenn du nun git archive ausführst, um einen Tarball deines Projekts zu erstellen, wird dieses Verzeichnis nicht in das Archiv aufgenommen.

export-subst

Beim Exportieren von Dateien für das Deployment kannst du die Formatierung und Schlüsselwort-Erweiterung von git log auf ausgewählte Teile von Dateien anwenden, die mit dem Attribut export-subst markiert sind.

Wenn du beispielsweise eine Datei mit dem Namen LAST_COMMIT in dein Projekt aufnehmen möchtest und Metadaten über den letzten Commit automatisch in das Projekt eingespeist werden sollen, sobald git archive läuft, kannst du beispielsweise deine .gitattributes und LAST_COMMIT Dateien so einrichten:

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'

Wenn du git archive ausführst, sieht der Inhalt der archivierten Datei so aus:

$ 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

Die Ersetzungen können z.B. die Commit-Meldung und beliebige git notes enthalten, und git log kann einfache Zeilenumbrüche durchführen:

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

Das so entstandene Archiv ist für das Deployment geeignet, aber wie jedes andere exportierte Archiv ist es nicht zur weiteren Entwicklungsarbeit verwendbar.

Merge-Strategien

Du kannst Git-Attribute auch verwenden, um Git anzuweisen, verschiedene Merge-Strategien für spezifische Dateien in deinem Projekt zu verwenden. Eine sehr nützliche Option ist es, Git anzuweisen, dass es nicht versuchen soll, bestimmte Dateien zusammenzuführen, wenn sie Konflikte haben, sondern deine Version der Daten zu benutzen, anstelle der von jemand anderem.

Das ist nützlich, wenn ein Branch in deinem Projekt auseinander gelaufen ist oder sich geändert hat, du jedoch weiterhin Änderungen von ihn mergen möchtest und dabei einige Dateien ignorieren möchtest. Angenommen, du hast eine Datenbank-Einstellungsdatei namens database.xml, die sich in zwei Branches voneinander unterscheidet. Du willst in deinem zweiten Branch mergen, ohne die Datenbankdatei zu beschädigen. Dann kannst du so ein Attribut einrichten:

database.xml merge=ours

Dann definierst du die Dummy-Merge-Strategie ours mit:

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

Wenn du in dem zweiten Branch mergst, siehst du statt Merge-Konflikte mit der Datei database.xml folgendes:

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

In diesem Fall bleibt die Datei database.xml auf der Version, die sie ursprünglich gehabt hat.

scroll-to-top