Whitelists mit gitignore

Posted by Start Bootstrap on March 06, 2014

“.gitignore” als Whitelist, geht das?

.gitignore ist eine Blacklist-Datei (Negativliste), d. h. sie gibt an, welche Dateien man von der Versionierung ausschließen möchte. Wenn man aber nur wenige Verzeichnisse in einem großen Verzeichnisbaum mit Git versionieren möchte, dann wäre eine Whitelist praktischer. Geht das auch?

Kurze Antwort

Ja, aber es ist ein klein wenig umständlich. Die folgende .gitignore-Datei zeigt, wie es geht:

# .gitignore
# Erstmal alles ignorieren
*

# Verzeichnis /a/b/c/ "un-ignorieren"
!/a/
!/a/b/
!/a/b/c/
!/a/b/c/**

Etwas längere Antwort

Tatsächlich ist die Sache überraschend kompliziert.

Oft gibt es in einem Projekt Dateien oder Verzeichnisse, die man nicht versionieren möchte, z. B. generierte Dateien, Editor-Backups oder lokale Konfigurationen. Mit Hilfe der .gitignore-Datei kann solche Dateien ignorieren lassen. Diese werde dann mit git status nicht mehr angezeigt und bei git add -all nicht hinzugefügt.

Manchmal gibt es aber auch die Situation, dass man eine große Verzeichnisstruktur vorliegen hat, davon aber nur einige wenige Dateien und Verzeichnisse versionieren möchte. Zum Beispiel: Ein /etc/-Verzeichnis enthält Konfigurationen für viele Tools. Einige davon sind für mein Projekt relevant und nur diese möchte ich versionieren. Mit Git ist das (natürlich) auch möglich. Aber es gibt ein paar kleine Fallstricke dabei.

Beispiel

Wir haben ein Projekt mit mehreren Dateien in verschiedenen Verzeichnissen, wollen aber nur die Dateien unterhab von /a/b/c/ versionieren.

repo $ git status --short --untracked-files=all
?? .gitignore
?? a/b/c/d/file-d
?? a/b/c/file-c
?? a/b/file-b
?? a/file-a
?? file

Man könnte dazu jedes unerwünschte Verzeichnis in die .gitignore eintragen, dann bliebe übrig, was man möchte. Bei großen Projekten kann das aber mühsam sein und müsste auch ständig nachgepflegt werden, wenn neue Verzeichnisse hinzukommen.

Stattdessen können wir einfach mit einem beherzten * erstmal alles ausschließen, um dann später Verzeichnisse, die wir versioneren möchten, zu un-ignorieren.

# .gitignore
*

Wenn wir wollen, können wir einzelne Dateien trotzdem in die Versionierung aufnehmen, denn git add -f sticht .gitignore

repo $ git add -f a/b/c/file-c
repo $ git status --short --untracked-files=all
A  a/b/c/file-c

Der Nachteil dabei: Es wird nur die einzelne Datei aufgenommen, aber das Verzeichnis /a/b/c/ bleibt weiterhin unbeobachtet. git status zeigt beispielsweise nicht, wenn dort noch neue unversionierte Dateien auftauchen.

Nützlicher ist die !-Notation. Durch Zeilen mit vorangestelltem ! können ausgeblendete Dateien und Verzeichnisse laut Manual-Page un-ignoriert werden:

# .gitignore
*
!/a/b/c/
repo $ git status --short --untracked-files=all

WTF! Warum ist /a/b/c/ nicht wieder aktiv geworden? Laut Dokumentation wirkt ! auf Pfade, die in vorigen Zeilen ausgeblendet wurden. Das Verzeichnis /a/b/c/ wurde doch in der Zeile mit dem * ausgeblendet, oder? Warum ist sie dann jetzt nicht wieder sichtbar? Tatsächlich wurde durch * das Verzeichnis /a/ ausgeschlossen. Die darunter liegenden Verzeichnisse wurden nur implizit mit ausgeschlossen.

Was wir tun können, ist: Zuerst mit /a/ das Verzeichnis wieder zurückholen. Und dann mit /a/** dafür sorgen, dass auch alle Dateien und Verzeichnisse darunter wieder sichtbar werden (diese waren ja ebenfalls durch * ausgeschlossen)

# .gitignore
*
!/a/
!/a/**
repo $ git status --short --untracked-files=all
?? a/b/c/d/file-d
?? a/b/c/file-c
?? a/b/file-b
?? a/file-a

Jetzt sind alle Dateien unterhalb von /a/ wieder drin! Und das kann man beliebig tief fortsetzen. Weil’s so schön war ;-) gleich nochmal:

# .gitignore
*
!/a/
!/a/b/
!/a/b/c/
!/a/b/c/**
repo $ git status --short --untracked-files=all
?? a/b/c/d/file-d
?? a/b/c/file-c

Ein wenig seltsam, aber es funktioniert. Typisch Git eben ;-)

Wozu die vielen Slashes?

In unserem Beispiel würde es auch ohne Slashes am Anfang und Ende der Pfade gehen.

# .gitignore
*
!a
!a/b
!a/b/c
!a/b/c/**

Ganz korrekt wäre das aber nicht, denn Slashes am Anfang sorgen dafür, dass nur Pfade die so beginnen matchen. Slashes am Ende sorgen dafür, dass nur Verzeichnisse matchen. Im Beispiel sind jetzt alle Dateien und Verzeichnisse, die a heißen drin:

repo $ git status --short --untracked-files=all
?? a/a/a/a
?? a/b/c/d/file-d
?? a/b/c/file-c