Git
Chapters ▾ 2nd Edition

8.2 Git aanpassen - Git attributen

Git attributen

Sommige van deze kunnen ook worden ingericht voor een pad, op deze manier zal Git deze instellingen alleen gebruiken voor een subdirectory of een subset van bestanden. Deze pad-specifieke instellingen worden Git attributen genoemd, en worden bewaard in een .gitattributes bestand in een van je directories (normaal gesproken de root van je project) of in het .git/info/attribures bestand als je dit attributen bestand niet met je project wilt committen.

Met het gebruik van attributen, kan je dingen doen als het specificeren van separate merge strategieën voor individuele bestanden of directories in je project, Git vertellen hoe niet-tekst bestanden moeten worden gediffd, of Git inhoud laten filteren voordat je het naar of van Git in- of uitcheckt. In deze paragraaf zullen we je een en ander vertellen over de attributen die je kunt inrichten op je paden in je Git project en je zult een aantal voorbeelden zien hoe deze mogelijkheden in de praktijk gebruikt kunnen worden.

Binaire bestanden

Een aardig voorbeeld waar je Git attributen voor kunt gebruiken is Git vertellen welke bestanden binair zijn (in die gevallen waar het niet zelf kan bepalen) en Git speciale instructies te geven over hoe deze bestanden te behandelen. Bijvoorbeeld, sommige tekst bestanden kunnen door applicaties zijn gegenereerd en daarmee niet diff-baar, terwijl sommige binaire bestanden juist wel kunnen worden gediffd. We zullen je laten zien hoe je Git kunt vertellen het onderscheid te maken.

Binaire bestanden herkennen

Sommige bestanden zien er uit als tekst bestanden, maar moeten praktisch gezien gewoon behandeld worden als binaire gegevens. Als voorbeeld, Xcode projecten op de Mac bevatten een bestand dat eindigt op .pbxproj, wat eigenlijk gewoon een JSON (platte tekst Javascript gegevens formaat) dataset is die door de IDE naar schijf geschreven wordt, waarin je bouwinstellingen staan en zo voorts. Alhoewel het technisch gesproken een tekst bestand is (omdat het allemaal UTF-8 is), wil je het niet als dusdanig behandelen omdat het eigenlijk een lichtgewicht database is - je kunt de inhoud niet mergen als twee personen het wijzigen, en diffs zijn over het algemeen niet echt nuttig. De bedoeling van dit bestand is om door een machine te worden verwerkt. Effectief wil je het behandelen als een binair bestand.

Om Git te vertellen om alle pbxproj bestanden als binaire gegevens te behandelen, voeg je de volgende regel to aan je .gitattributes bestand:

*.pbxproj binary

Nu zal Git niet proberen om CRLF gevallen te converteren of te repareren, en ook zal het niet proberen om een diff te bepalen of af te drukken voor wijzigingen in dit bestand als je git show of git diff op je project aanroept.

Binaire bestanden diffen

Je kunt de Git attributen functionaliteit ook gebruiken om binaire bestanden feitelijk te diffen. Je kunt dit doen door Git te vertellen hoe het je binaire gegevens moet converteren naar een tekst formaat die weer via een normale diff kan worden vergeleken.

Allereerst zal je deze techniek gebruiken om een van de meest ergerlijke problemen die er voor de mensheid bestaan op te lossen: het onder versie beheer plaatsen van Microsoft Word documenten. Iedereen weet dat Word een van de afschuwwekkendste tekstverwerkers is die bestaat maar, die vreemd genoeg, nog steeds door iedereen gebruikt wordt. Als je Word documenten onder versie beheer wilt brengen, kan je ze in een Git repository stoppen en eens in de zoveel tijd committen, maar wat is het nut hiervan? Als je git diff gewoon zou aanroepen, zou je alleen maar iets als dit zien:

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

Je kunt twee versies niet rechtstreeks vergelijken tenzij je ze uitcheckt en dan handmatig vergelijkt, toch? We zullen laten zien dat je dit redelijk goed kunt doen met Git attributen. Zet de volgende regel in je .gitattributes bestand:

*.docx diff=word

Dit vertelt Git dat elk bestand dat past in het patroon (.docx) het “word” filter moet gebruiken als je een diff probeert te bekijken waar verschillen in zitten. Wat is het “word” filter? Je moet het zelf inrichten. Hier ga je Git configureren om het docx2txt programma te gebruiken om Word documenten in leesbare tekstbestanden te converteren, die vervolgens juist kunnen worden gedifft.

Allereerst moet je docx2txt installeren, je kunt het downloaden van http://docx2txt.sourceforge.net. Volg de instructies in het INSTALL bestand om het ergens neer te zetten waar je shell het kan vinden. Vervolgens, schrijf je een wrapper script om de uitvoer te converteren naar het formaat dat Git verwacht. Maak een bestand ergens in je pad en noem dezer docx2txt en zet dit hierin:

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

Vergeet dit niet op dit bestand chmod a+x aan te roepen. Tot slot kan je Git configureren om dit script te gebruiken:

$ git config diff.word.textconv docx2txt

Nu weet Git dat als het een diff probeert uit te voeren tussen twee snapshots, en een van de bestandsnamen eindigt op .docx, dat het deze bestanden door het “word” filter moet halen die als het docx2txt programma gedefinieerd is. Effectief maakt dit mooie tekst-gebaseerde versies van je Word bestanden voordat het probeert deze te diffen.

Hier is een voorbeeld: Hoofdstuk 1 van di boek was geconverteerd naar een Word formaat en gecommit in een Git repository. Toen is er een nieuwe sectie toegevoegd. Dit is wat git diff laat zien:

$ 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 vertelt ons succesvol en bondig dat we de tekenreeks “Testing: 1, 2, 3,” hebben toegevoegd, wat juist is. Het is niet perfect - wijzigingen in formattering worden niet zichtbaar - maar het werkt wel.

Een ander interessant probleem wat je op deze manier kunt oplossen betreft het diffen van bestanden met afbeeldingen. Een manier om dit te doen is om afbeeldingen door een filter te halen die hun EXIF informatie extraheert - metadata die bij de meeste grafische bestanden worden vastgelegd. Als je het exiftool downloadt en installeert, kan je deze gebruiken om je afgeeldingen naar tekst over de metadata te converteren, zodat de diff je in elk geval een tekstuele representatie van wijzigingen kan laten zien:

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

Als je een afbeelding in je project vervangt en dan git diff aanroept, zie je iets als dit:

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

Je kunt eenvoudig zien dat de bestandsgrootte en de dimensies van je afbeelding beide zijn veranderd.

Sleutelwoord expansie (Keyword expansion)

Keyword expansion zoals je dit kan vinden bij SVN of CVS wordt vaak gewenst door ontwikkelaars die gewend zijn aan die systemen. Het grote probleem met dit is in Git is dat je een bestand niet kunt aanvullen met informatie over de commit nadat je het hebt gecommit, omdat Git eerst een checksum van het bestand maakt. Je kunt echter tekst in een bestand injecteren als het uitgecheckt wordt en het weer verwijderen voordat het aan een commit wordt toegevoegd. Git attributen geven je twee manieren om dit te bereiken.

Als eerste kan je automatisch de SHA-1 checksum van een blob in een $Id$ veld injecteren. Als je dit attribuut op een bestand of een aantal bestanden zet dan zal Git, als je die branch een volgende keer uitcheckt. dat veld vervangen met de SHA-1 van de blob. Het is belangrijk om op te merken dat het niet de SHA-1 van de commit is, maar van de blob zelf:

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

De volgende keer als je dit bestand uitcheckt, zal Git de SHA-1 van de blob injecteren:

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

Dit resultaat is echter van beperkt nut. Als je keyword substitutie in CVS of Subversion gebruikt hebt, kan je een datestamp bijsluiten - de SHA-1 is eigenlijk niet zo nuttig, omdat het redelijk willekeurig is en je kunt aan een SHA-1 niet zien of het ouder of jonger is dan een andere door alleen er naar te kijken.

Je kunt echter je eigen filters schrijven om substituties in bestanden doen bij commit/checkout. Dit worden “clean” (kuis) en “smudge” (besmeur) filters genoemd. In het .gitattributes bestand kan je een filter instellen voor specifieke paden en scripts maken die de bestanden bewerken vlak voordat ze worden uitgechecked (“smudge”, zie Het “smudge” filter wordt bij checkout aangeroepen.) en vlak voordat ze worden gestaged (“clean”, zie Het “clean” filter wordt aangeroepen als bestanden worden gestaged.). Deze filters kunnen worden verteld om allerhande leuke dingen te doen.

Het ``smudge'' filter wordt bij checkout aangeroepen.
Figure 144. Het “smudge” filter wordt bij checkout aangeroepen.
Het ``clean'' filter wordt aangeroepen als bestanden worden gestaged.
Figure 145. Het “clean” filter wordt aangeroepen als bestanden worden gestaged.

Het orginele commit bericht voor deze functionaliteit geeft een simpel voorbeeld van al je C broncode door het indent programma te laten bewerken voor het committen. Je kunt het inrichten door het filter attribuut in je .gitattributes bestand *.c bestanden te laten filteren met het “indent” filter:

*.c filter=indent

Daarna vertel je Git wat het “indent” filter moet doen bij smudge en clean:

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

In dit geval, als je bestanden commit die lijken op *.c, zal Git deze door het indent programma halen voordat het deze staged en ze door het cat programma halen voordat het ze weer naar schijf uitchecked. Het cat programma doet eigenlijk niets: het geeft de gegevens die binnenkomen ongewijzigd door. Deze combinatie zal effectief alle C broncode door indent laten filteren voor het te committen.

Een ander interessant voorbeeld veroorzaakt $Date$ keyword expansie, zoals bij RCS. Om dit goed te doen heb je een klein scriptje nodig dat een bestandsnaam neemt, de laatste commit datum vinden voor dit project en dan de datum in dat bestand injecteren. Hier is een kort Ruby script dat precies dit doet:

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

Al wat dit script doet is de laatste commit datum van het git log commando uitlezen, en dat in elke $Date$ tekenreeks die het in stdin ziet zetten, en drukt het resultaat af - het zou eenvoudig moeten zijn om dit te doen in een taal waar je het meest vertrouwd mee bent. Je kunt dit bestand expand_date noemen en het op je pad plaatsen. Nu moet je een filter opzetten in Git (noem het dater) en het vertellen om je expand_date filter te gebruiken om de bestanden te besmeuren bij uitchecken. Je kunt een Perl expressie gebruiken om dat bij commit weer op te kuisen:

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

Dit stukje Perl haalt alles weg wat het ziet in een $Date$ tekenreeks, om terug te halen waar je mee was begonnen. Nu je filter gereed is, kan je het uitproberen door een bestand te maken met je $Date$ keyword en daarna een Git attribuut op te zetten voor dat bestand die je nieuwe filter activeert:

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

Als je deze wijzigingen commit en het bestand weer uitcheckt, zal je het keyword juist vervangen zien:

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

Je kunt zien hoe krachtig deze techniek kan zijn voor eigen toepassingen. Je moet echter wel voorzichtig zijn, omdat het .gitattributes bestand gecommit wordt en met het project wordt verspreid, maar dat het uitvoerende element (in dit geval dater) dat niet wordt, zodat het niet overal zal werken. Als je deze filters ontwerpt, moeten ze in staat zijn om netjes te falen en het project nog steeds goed te laten werken.

Je repository exporteren

De Git attribute gegevens staan je ook toe om interessante dingen te doen als je een archief van je project exporteert.

export-ignore

Je kunt Git vertellen dat sommige bestanden of directories niet geëxporteerd moeten worden bij het genereren van een archief. Als er een subdirectory of bestand is waarvan je niet wilt dat het wordt meegenomen in je archief bestand, maar dat je wel in je project ingecheckt wil hebben, kan je die bestanden benoemen met behulp van het export-ignore attribuut.

Bijvoorbeeld, stel dat je wat testbestanden in een test/ subdirectory hebt, en dat het geen zin heeft om die in de tarball export van je project mee te nemen. Dan kan je de volgende regel in je Git attributes bestand toevoegen:

test/ export-ignore

Als je nu git archive uitvoert om een tarball van je project te maken, zal die directory niet meegenomen worden in het archief.

export-subst

Als je bestanden exporteert voor deployment kan je de formattering en sleutelwoord expansie van git log toepassen om delen van bestanden selecteren die met het export-subst attribuut zijn gemarkeerd.

Bijvoorbeeld, als je een bestand genaamd LAST_COMMIT wilt meenemen in je project, waarin de laatste commit datum op van het moment dat git archive liep automatisch wordt geïnjecteerd, kan je het bestand als volgt instellen:

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

Als je git archive uitvoert, zal de inhoud van dat bestand er zo uit zien:

$ 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

De vervangingen kunnen bijvoorbeeld het commit bericht en elke git note omvatten, en git log kan eenvoudige word wrapping uitvoeren:

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

Het archief wat hieruit komt is bruikbaar voor deployment werkzaamheden, maar zoals elk geexporteerd archief is het niet echt toepasbaar voor ontwikkel werk.

Merge strategieën

Je kunt Git attributen ook gebruiken om Git te vertellen dat het verschillende merge strategieën moet gebruiken voor specifieke bestanden in je project. Een erg handige toepassing is Git te vertellen dat het bepaalde bestanden niet moet proberen te mergen als ze conflicten hebben, maar jouw versie moeten gebruiken in plaats van die van de ander.

Dit is handig als een branch in jouw project uiteen is gelopen of gespecialiseerd is, maar je wel in staat wilt zijn om veranderingen daarvan te mergen, en je wilt bepaalde bestanden negeren. Stel dat je een database instellingen bestand hebt dat database.xml heet en tussen twee branches verschilt, en je wilt de andere branch mergen zonder jouw database bestand overhoop te halen. Je kunt dan een attribuut als volgt instellen:

database.xml merge=ours

En daarna een loze ours merge strategie definiëren met:

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

Als je in de andere branch merged, dan zul je in plaats van merge conflicten met het database.xml bestand zoiets als dit zien:

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

In dit geval blijft database.xml staan op de versie die je oorspronkelijk al had.