August 2017

Band 32, Nummer 8

DevOps – Git-Interna: Architektur und Indexdateien

Von Jonathan Waldman | August 2017

In meinem letzten Artikel (msdn.com/magazine/mt809117) habe ich gezeigt, wie Git einen gerichteten azyklischen Graphen (Directed Acyclic Graph, DAG) zum Organisieren der Commit-Objekte eines Repositorys verwendet. Außerdem habe ich die Blob-, Tree- und Tag-Objekte untersucht, auf die sich Commit-Objekte beziehen können. Abgeschossen habe ich den Artikel mit einer Einführung in Branching und dabei auch den Unterschied zwischen „HEAD“ und „head“ erläutert. Der damalige Artikel schafft die Voraussetzungen für diesen Artikel, in dem ich die „Dreibaum“-Architektur von Git und die Wichtigkeit der Indexdatei behandele. Wenn Sie diese zusätzlichen Git-Interna verstehen, erweitern Sie Ihre grundlegenden Kenntnisse, damit Sie effektiver in der Verwendung von Git werden und neue Einblicke erlangen können, während Sie verschiedene Git-Vorgänge anhand der grafischen Git-Tools in der Visual Studio-IDE untersuchen.

Erinnern Sie sich aus dem letzten Artikel daran, dass Visual Studio mit Git mithilfe einer Git-API kommuniziert und dass die Git-Tools der Visual Studio-IDE die Komplexität und die Funktionen des zugrunde liegenden Git-Moduls abstrahieren. Das ist ein Segen für Entwickler, die einen Versionskontrollworkflow implementieren möchten, ohne die Git-Befehlszeilenschnittstelle (Command-Line Interface, CLI) verwenden zu müssen. Leider können die ansonsten hilfreichen Abstraktionen der IDE manchmal auch Verwirrung stiften. Betrachten Sie z. B. einen grundlegenden Workflow: Hinzufügen eines Projekts zur Git-Quellcodeverwaltung, Ändern der Projektdateien, Staging der Dateien und anschließendes Committen der Stagingdateien. Zu diesem Zweck öffnen Sie den Bereich „Änderungen“ von Team Explorer, um die Liste der geänderten Dateien anzuzeigen, dann wählen Sie die für das Staging vorgesehenen Dateien aus. Sehen Sie sich den Screenshot ganz links in Abbildung 1 an, der zeigt, dass ich zwei Dateien im Arbeitsverzeichnis geändert habe (Markierung 1).

Der Bereich „Änderungen“ von Team Explorer kann die gleiche Datei in den Abschnitten „Änderungen“ und „Bereitgestellte Änderungen“ anzeigen

Abbildung 1: Der Bereich „Änderungen“ von Team Explorer kann die gleiche Datei in den Abschnitten „Änderungen“ und „Bereitgestellte Änderungen“ anzeigen

Im nächsten Screenshot habe ich das Staging für eine der geänderten Dateien ausgeführt: „Program.cs“ (Markierung 2). Dabei scheint „Program.cs“ aus der Liste „Änderungen“ in die Liste „Bereitgestellte Änderungen“ „gewandert“ zu sein. Wenn ich „Program.cs“ weiter bearbeite und die Kopie des Arbeitsverzeichnisses speichere, wird die Datei weiterhin im Abschnitt „Bereitgestellte Änderungen“ (Markierung 3) angezeigt, aber auch im Abschnitt „Änderungen“ (Markierung 4)! Wenn Sie nicht wissen, wie Git hinter den Kulissen agiert, wären Sie wahrscheinlich verwirrt, bis Sie herausgefunden haben, dass zwei „Kopien“ von „Program.cs“ vorhanden sind: eine Kopie im Arbeitsordner und eine Kopie in der internen Git-Datenbank der Objekte. Selbst wenn Ihnen dies bewusst ist, kann Ihnen völlig unklar sein, was geschehen würde, wenn Sie das Staging der bereitgestellten Datei aufheben, versuchen, die zweite geänderte Kopie von „Program.cs“ bereitzustellen, Änderungen an der Arbeitskopie rückgängig machen oder Branches wechseln.

Wenn Sie wirklich verstehen möchten, welche Aktionen Git ausführt, wenn Sie Dateien bereitstellen, die Bereitstellung aufheben, Änderungen rückgängig machen, committen und auschecken, müssen Sie zunächst die Architektur von Git verstehen.

Die Git-Dreibaumarchitektur

Git implementiert eine Dreibaumarchitektur (ein „Baum“ bezieht sich in diesem Kontext auf eine Verzeichnisstruktur und Dateien). Erläuterungen zu Abbildung 2 von links nach rechts: Die Git-Dreibaumarchitektur nutzt die wichtige Indexdatei für intelligente und effiziente Leistung. Der erste Baum ist die Sammlung der Dateien und Ordner im Arbeitsverzeichnis (das Betriebssystemverzeichnis, das den verborgenen Ordner „.git“ enthält). Der zweite Baum wird normalerweise in einer Binärdatei namens „index“ gespeichert, die sich im Stamm des Ordner „.git“ befindet. Der dritte Baum besteht aus Git-Objekten, die den DAG darstellen (erinnern Sie sich daran, dass sich SHA-1-benannte Git-Objekte in Ordnern befinden, deren Namen aus Hexwerten aus zwei Zeichen bestehen („.git\objects“), und sie können auch in „Paketdateien“ gespeichert werden, die sich in „.git\objects\pack“ sowie in Dateipfaden befinden, die von der Datei „.git\objects\info\alternates2“ definiert werden). Denken Sie daran, dass das Git-Repository durch alle Dateien definiert wird, die im Ordner „.git“ gespeichert sind. Entwickler bezeichnen den DAG häufig als Git-Repository, aber das ist nicht ganz richtig: Der Index und der DAG befinden sich beide im Git-Repository.

Die Git-Dreibaumarchitektur nutzt die wichtige Indexdatei für intelligente und effiziente Leistung

Abbildung 2: Die Git-Dreibaumarchitektur nutzt die wichtige Indexdatei für intelligente und effiziente Leistung

Beachten Sie Folgendes: Auch wenn jeder Baum eine Verzeichnisstruktur und Dateien speichert, nutzen die einzelnen Bäume verschiedene Datenstrukturen, um baumspezifische Metadaten zu verwalten und die Speicherung und den Abruf zu optimieren. Der erste Baum (der Arbeitsverzeichnisbaum, auch als „Arbeitsbaum“ bezeichnet) besteht einfach aus den Betriebssystemdateien und -ordnern (hier sind keine besonderen Datenstrukturen außer denen auf Betriebssystemebene vorhanden) und erfüllt die Anforderungen von Softwareentwicklern und Visual Studio. Der zweite Baum (der Git-Index) überspannt das Arbeitsverzeichnis und die Commit-Objekte, die den DAG bilden. Auf diese Weise kann Git schnelle Vergleiche von Dateiinhalten des Arbeitsverzeichnisses sowie schnelle Commits ausführen. Der dritte Baum (der DAG) ermöglicht es Git, ein verlaufsrobustes Versionskontrollsystem nachzuverfolgen. Git fügt den Elementen hilfreiche Metadaten hinzu, die im Index und in Commit-Objekten gespeichert werden. Die im Index gespeicherten Metadaten unterstützen z. B. das Erkennen von Änderungen an Dateien im Arbeitsverzeichnis, während die in Commit-Objekten gespeicherten Metadaten die Nachverfolgung ermöglichen, wer den Commit ausgegeben hat und aus welchem Grund.

Als kurze Zusammenfassung der drei Bäume in der Dreibaumarchitektur und Perspektive für den Schwerpunkt des restlichen Artikels lässt sich Folgendes sagen: Sie kennen bereits die Funktionsweise des Arbeitsverzeichnisbaums, weil es sich tatsächlich um das Dateisystem des Betriebssystems handelt, in dessen Verwendung Sie bereits äußerst versiert sind. Wenn Sie meinen früheren Artikel gelesen haben, sollten Sie über ausreichende Arbeitskenntnisse bezüglich des DAG verfügen. Zu diesem Zeitpunkt ist daher der Indexbaum (ab jetzt als „Index“ bezeichnet) das fehlende Bindeglied, das das Arbeitsverzeichnis und den DAG überspannt. Tatsächlich spielt der Index eine so wichtige Rolle, dass er im Rest dieses Artikels das einzige Thema sein wird.

Funktionsweise des Index

Vielleicht haben Sie gehört, dass der Index mit dem „Stagingbereich“ gleichzusetzen sei. Darin liegt zwar ein Körnchen Wahrheit, aber diese Aussage straft seine wahre Funktion Lügen. Er muss nicht nur den Stagingbereich unterstützen, sondern es Git auch ermöglichen, Änderungen an Dateien in Ihrem Arbeitsverzeichnis zu erkennen. Er muss den Branchmergevorgang vermitteln, damit Sie Konflikte dateiweise lösen und den Mergevorgang jederzeit sicher abbrechen können. Zudem muss er bereitgestellte Dateien und Ordner in die Tree-Objekte konvertieren, deren Verweise in das nächste Commit-Objekt geschrieben werden. Git verwendet den Index außerdem, um Informationen zu Dateien im Arbeitsbaum und zu Objekten zu speichern, die aus dem DAG abgerufen werden. Auf diese Weise wird der Index als eine Art von Cache genutzt. Untersuchen wir nun den Index etwas genauer.

Der Index implementiert seinen eigenes, in sich abgeschlossenes Dateisystem, das ihm ermöglicht, Verweise auf Ordner und Dateien zusammen mit den zugehörigen Metadaten zu speichern. Wie und wann Git diesen Index aktualisiert, hängt von der Art des ausgegebenen Git-Befehls sowie den angegebenen Befehlsoptionen ab (wenn Sie möchten, können Sie sogar den Git-Plumbingbefehl „update-index“ verwenden, um den Index selbst zu verwalten). Eine ausführliche Erläuterung dieser Vorgänge sprengt daher den Rahmen dieses Artikels. Wenn Sie mit den Visual Studio Git-Tools arbeiten, sollten Sie sich jedoch der Hauptmöglichkeiten der Aktualisierung des Index durch Git bewusst sein und außerdem eine Vorstellung davon haben, wie Git die im Index gespeicherten Informationen verwendet. Abbildung 3 zeigt, dass Git den Index mit Daten des Arbeitsverzeichnisses aktualisiert, wenn Sie das Staging für eine Datei ausführen. Der Index wird hingegen mit DAG-Daten aktualisiert, wenn Sie einen Mergevorgang initiieren (wenn Mergekonflikte vorhanden sind), klonen oder pullen bzw. Branches wechseln. Andererseits verwendet Git im Index gespeicherte Informationen, wenn der DAG nach dem Ausgeben eines Commits aktualisiert wird. Dies gilt auch beim Aktualisieren des Arbeitsverzeichnisses nach dem Klonen oder Pullen bzw. nach dem Wechseln von Branches. Sobald Sie verstanden haben, dass Git den Index verwendet und dass der Index so viele Git-Vorgänge überspannt, werden Sie die erweiterten Git-Befehle zu schätzen wissen, die den Index ändern und Ihnen ein effektives Mittel an die Hand geben, die Funktionsweise von Git geschickt zu beeinflussen.

Primäre Git-Aktionen

Abbildung 3: Primäre Git-Aktionen, die den Index aktualisieren (grün), und Git-Aktionen, die Informationen aus dem Index verwenden (rot)

Erstellen wir nun eine neue Datei im Arbeitsverzeichnis, um zu sehen, was geschieht, wenn diese in den Index geschrieben wird. Sobald Sie das Staging dieser Datei ausführen, erstellt Git einen Header mithilfe der folgenden Zeichenfolgen-Verkettungsformel:

blob{Leerzeichen}{Dateilänge in Bytes}{Null-Terminierungszeichen}

Git verkettet dann den Header am Anfang des Dateiinhalts. Für eine Textdatei, die die Zeichenfolge „Hello“ enthält, würden der Header und der Dateiinhalt also eine Zeichenfolge generieren, die folgendermaßen aussieht (denken Sie daran, dass ein Nullzeichen vor dem Buchstaben „H“ vorhanden ist):

blob 5Hello

Damit dies klarer wird, sehen Sie hier die hexadezimale Version dieser Zeichenfolge:

62 6C 6F 62 20 35 00 48 65 6C 6C 6F

Git berechnet dann den SHA-1-Wert für die Zeichenfolge:

5ab2f8a4323abafb10abb68657d9d39f1a775057

Im nächsten Schritt untersucht Git den vorhandenen Index, um zu ermitteln, ob bereits ein Eintrag für diesen Ordner-\Dateinamen mit dem gleichen SHA-1-Wert vorhanden ist. Wenn dies der Fall ist, wird das Blob-Objekt im Ordner „.git\objects“ ermittelt und sein Datum für „date-modified“ aktualisiert (Git überschreibt niemals Objekte, die bereits im Repository vorhanden sind, sondern aktualisiert das Datum „last-modified“ so, dass dieses neu hinzugefügte Objekt erst verzögert für die Garbage Collection berücksichtigt wird). Andernfalls werden die ersten beiden Zeichen der SHA-1-Zeichenfolge als Verzeichnisname in „.git\objects“ verwendet und die verbleibenden 38 Zeichen zum Benennen der Blob-Datei, bevor diese mit zlib komprimiert und ihr Inhalt geschrieben wird. In meinem Beispiel erstellt Git einen Ordner in „.git\objects“ mit dem Namen „5a“ und schreibt dann das Blob-Objekt als eine Datei mit dem Namen „b2f8a4323abafb10abb68657d9d39f1a775057“ in diesen Ordner.

Wenn Git ein Blob-Objekt auf diese Weise erstellt, sind Sie vielleicht erstaunt, dass eine erwartete Dateieigenschaft im Blob-Objekt unübersehbar fehlt: der Dateiname! Dies ist jedoch entwurfsbedingt. Rufen Sie sich in Erinnerung, dass Git ein inhaltsorientiertes Dateisystem ist und als solches SHA-1-benannte Blob-Objekte verwaltet und keine Dateien. Auf jedes Blob-Objekt wird normalerweise durch mindestens ein Tree-Objekt verwiesen, und auf Tree-Objekte wird ihrerseits normalerweise durch Commit-Objekte verwiesen. Letztlich drücken die Tree-Objekte von Git die Ordnerstruktur der Dateien aus, für die Sie das Staging ausführen. Git erstellt diese Tree-Objekte jedoch erst, wenn Sie einen Commit ausgeben. Daraus können Sie Folgendes schließen: Wenn Git nur den Index zum Vorbereiten eines Commit-Objekts verwendet, müssen auch die file-path-Verweise für jeden Blob im Index erfasst werden. Und genau das ist der Fall. Selbst wenn zwei Blobs den gleichen SHA-1-Wert aufweisen, sind beide als separater Eintrag im Index vorhanden, solange jeder Blob einem anderen Dateinamen oder Pfad-/Dateiwert zugeordnet ist.

Git speichert außerdem Dateimetadaten mit jedem Blob-Objekt, das in den Index geschrieben wird, z. B. das Erstellungs- und Änderungsdatum einer Datei. Git nutzt diese Informationen zum effizienten Erkennen von Änderungen an Dateien in Ihrem Arbeitsverzeichnis unter Verwendung von file-date-Vergleichen und Heuristik anstelle von Brute-Force-Neuberechnung der SHA-1-Werte für jede Datei im Arbeitsverzeichnis. Durch eine solche Strategie werden die Informationen schneller verfügbar, die im Bereich „Änderungen“ von Team Explorer oder bei der Ausgabe des Git-Porcelain-Statusbefehls angezeigt werden.

Sobald ein Indexeintrag für eine Datei des Arbeitsverzeichnisses und die zugehörigen Metadaten verfügbar sind, erfolgt eine sogenannte „Nachverfolgung“ der Datei durch Git, weil Git auf einfache Weise die eigene Kopie der Datei mit der Kopie vergleichen kann, die im Arbeitsverzeichnis verbleibt. Unter technischen Aspekten ist eine nachverfolgte Datei eine Datei, die auch im Arbeitsverzeichnis vorhanden ist und in den nächsten Commit eingeschlossen werden muss. Im Gegensatz dazu stehen nicht nachverfolgte Dateien, bei denen zwei Typen unterschieden werden: Dateien, die im Arbeitsverzeichnis vorhanden sind, aber nicht im Index, und Dateien, die explizit als nicht nachzuverfolgen gekennzeichnet sind (siehe Abschnitt „Indexerweiterungen“). Zusammenfassend lässt sich sagen, dass Git durch den Index leistungsfähig genug ist, um zu ermitteln, welche Dateien nachverfolgt werden, welche Dateien nicht nachverfolgt werden und welche Dateien nachverfolgt werden sollten.

Damit Sie die spezifischen Inhalte des Index besser verstehen, sollten wir ein konkretes Beispiel verwenden, indem wir ein neues Visual Studio-Projekt erstellen. Die Komplexität dieses Projekts ist nicht weiter wichtig: Sie benötigen nur wenige Dateien, um die Vorgänge adäquat darzustellen. Erstellen Sie eine neue Konsolenanwendung namens „MSDNConsoleApp“, und aktivieren Sie die Kontrollkästchen „Projektmappenverzeichnis erstellen“ und „Neues Git-Repository erstellen“. Klicken Sie auf „OK“, um die Projektmappe zu erstellen.

Ich werde gleich einige Git-Befehle ausgeben. Wenn Sie diese auch auf Ihrem System ausführen möchten, öffnen Sie im Arbeitsverzeichnis ein Eingabeaufforderungsfenster, und greifen Sie auf dieses Fenster zu, während Sie die einzelnen Schritte nachvollziehen. Eine Möglichkeit, schnell ein Git-Befehlsfenster für ein bestimmtes Git-Repository zu öffnen, besteht im Zugreifen auf das Visual Studio-Menü „Team“ und Auswählen von „Verbindungen verwalten“. Es wird eine Liste der lokalen Git-Repositorys zusammen mit dem Pfad zum Arbeitsverzeichnis des betreffenden Repositorys angezeigt. Klicken Sie mit der rechten Maustaste auf den Namen des Repositorys, und wählen Sie „Eingabeaufforderung öffnen“ aus, um ein Fenster zu öffnen, in das Sie Git CLI-Befehle eingeben können.

Nachdem Sie die Projektmappe erstellt haben, öffnen Sie den Bereich „Branches“ von Team Explorer (Abbildung 4, Markierung 1), um anzuzeigen, dass Git einen Standardbranch namens „master“ erstellt hat (Markierung 2). Klicken Sie mit der rechten Maustaste auf den master-Branch (Markierung 2), und wählen Sie „Versionsgeschichte anzeigen“ aus (Markierung 3), um die beiden Commits anzuzeigen, die Visual Studio in Ihrem Auftrag erstellt hat (Markierung 4). Der erste Commit weist die Commitmeldung „GITIGNORE und GITATTRIBUTES hinzufügen“, der zweite „Projektdateien hinzufügen“ auf.

Anzeigen der Versionsgeschichte

Abbildung 4: Anzeigen der Versionsgeschichte, um zu sehen, welche Aktionen Visual Studio ausführt, wenn Sie ein neues Projekt erstellen

Öffnen Sie den Team Explorer-Bereich „Änderungen“. Visual Studio verwendet die Git-API, um die Elemente in diesem Fenster mit Daten aufzufüllen. Dabei handelt es sich um die Visual Studio-Version des Git-Statusbefehls. Zurzeit gibt dieses Fenster an, dass keine nicht bereitgestellten Änderungen im Arbeitsverzeichnis vorhanden sind. Git ermittelt dies, indem jeder Indexeintrag mit jeder Datei des Arbeitsverzeichnisses verglichen wird. Git verfügt mit den Dateieinträgen des Index und den zugehörigen Dateimetadaten über alle Informationen, die erforderlich sind, um zu ermitteln, ob Sie Änderungen, Hinzufügungen oder Löschungen vorgenommen bzw. Dateien im Arbeitsverzeichnis umbenannt haben (ausgenommen die Dateien, die in der .gitignore-Datei aufgeführt werden).

Der Index spielt also eine Schlüsselrolle dabei, dass Git die Unterschiede zwischen Ihrem Arbeitsverzeichnisbaum und dem Commit-Objekt kennt, auf das „HEAD“ verweist. Wenn Sie etwas mehr darüber erfahren möchten, welche Art von Informationen der Index für das Git-Modul bereitstellt, navigieren Sie zum Befehlszeilenfenster, das Sie vorhin geöffnet haben, und geben Sie den folgenden Plumbingbefehl aus:

git ls-files --stage

Sie können diesen Befehl jederzeit ausgeben, um eine vollständige Liste der Dateien zu generieren, die sich zurzeit im Index befinden. Auf meinem System erfolgt die folgende Ausgabe:

100644 1ff0c423042b46cb1d617b81efb715defbe8054d 0       .gitattributes
100644 3c4efe206bd0e7230ad0ae8396a3c883c8207906 0       .gitignore
100644 f18cc2fac0bc0e4aa9c5e8655ed63fa33563ab1d 0       MSDNConsoleApp.sln
100644 88fa4027bda397de6bf19f0940e5dd6026c877f9 0       MSDNConsoleApp/App.config
100644 d837dc8996b727d6f6d2c4e788dc9857b840148a 0       MSDNConsoleApp/MSDNConsoleApp.csproj
100644 27e0d58c613432852eab6b9e693d67e5c6d7aba7 0       MSDNConsoleApp/Program.cs
100644 785cfad3244d5e16842f4cf8313c8a75e64adc38 0       MSDNConsoleApp/Properties/AssemblyInfo.cs

Die erste Spalte der Ausgabe ist ein Dateimodus des Unix-Betriebssystems im Oktalformat. Git unterstützt jedoch nicht den vollständigen Bereich von Dateimoduswerten. Wahrscheinlich werden nur 100644 (für Nicht-EXE-Dateien) und 100755 (für auf Unix basierende EXE-Dateien angezeigt; Git für Windows verwendet auch 100644 für ausführbare Dateitypen). Die zweite Spalte ist der SHA-1-Wert für die Datei. Die dritte Spalte stellt den Mergestatuswert für die Datei dar: 0, wenn kein Konflikt vorliegt und 1, 2 oder 3, wenn ein Mergekonflikt vorhanden ist. Beachten Sie außerdem, dass der Pfad und der Dateiname für jedes der sieben Blob-Objekte im Index gespeichert wird. Git verwendet den Pfadwert beim Erstellen von Tree-Objekten vor dem nächsten Commit (dazu gleich mehr).

Untersuchen wir nun die Indexdatei selbst. Da es sich um eine Binärdatei handelt, verwende ich HexEdit 4 (einen Freeware-Hex-Editor von hexedit.com) zum Anzeigen des Inhalts (Abbildung 5 zeigt einen Ausschnitt).

Ein Hex-Dump der Git-Indexdatei für das Projekt

Abbildung 5: Ein Hex-Dump der Git-Indexdatei für das Projekt

Abbildung 6: Das Format der Git-Indexheaderdaten

Indexdatei: Headereintrag
00 - 03
(4 Bytes)
DIRC Fester Header für einen Verzeichniscacheeintrag.
Alle Indexdateien beginnen mit diesem Eintrag.
04 - 07
(4 Bytes)
Version Indexversionsnummer (Git für Windows
verwendet zurzeit Version 2).
08 - 11
(4 Bytes)
Anzahl der Einträge Als ein 4-Byte-Wert unterstützt der Index bis
zu 4.294.967.296 Einträge!

Die ersten 12 Bytes des Index enthalten den Header (siehe Abbildung 6). Die ersten 4 Bytes enthalten immer die Zeichen „DIRC“ (Kurzform von „Directory Cache“, Verzeichniscache). Dies ist einer der Gründe dafür, dass der Git-Index häufig als der „Cache“ bezeichnet wird. Die nächsten 4 Bytes enthalten die Indexversionsnummer, die standardmäßig 2 ist, wenn Sie nicht bestimmte Features von Git verwenden (z. B. Sparse Checkout). In diesem Fall kann sie ggf. auf Version 3 oder 4 festgelegt werden. Die letzten 4 Bytes enthalten die Anzahl der Dateieinträge, die weiter unten im Index enthalten sind.

Auf den 12-Byte-Header folgt eine Liste mit n Indexeinträgen, wobei n mit der Anzahl der im Indexheader beschriebenen Einträge übereinstimmt. Abbildung 7 zeigt das Format für jeden Indexeintrag. Git sortiert Indexeinträge basierend auf dem Feld „Pfad/Dateiname“ in aufsteigender Reihenfolge.

Abbildung 7: Git-Indexdatei, Datenformat des Indexeintrags

Indexdatei, Indexeintrag
4 Bytes 32-Bit-Erstellungszeit in Sekunden Anzahl der Sekunden seit 1. Januar 1970, 00:00:00 Uhr.
4 Bytes 32-Bit-Erstellungszeit, Nanosekundenkomponente Nanosekundenkomponente der Erstellungszeit in Sekunden.
4 Bytes 32-Bit-Änderungszeit in Sekunden Anzahl der Sekunden seit 1. Januar 1970, 00:00:00 Uhr.
4 Bytes 32-Bit-Änderungszeit, Nanosekundenkomponente Nanosekundenkomponente der Erstellungszeit in Sekunden.
4 Bytes device Der Datei zugeordnete Metadaten. Diese stammen aus Dateiattributen, die im Unix-Betriebssystem verwendet werden.
4 Bytes inode
4 Bytes mode
4 Bytes user id
4 Bytes group id
4 Bytes file content length Anzahl der Inhaltsbytes in der Datei.
20 Bytes SHA-1 SHA-1-Wert des zugehörigen Blob-Objekts.
2 Bytes Flags (High-Bits zu Low-Bits) 1 Bit: Flag assume-valid/assume-unchanged, 1-Bit: erweitertes Flag (muss 0 für Versionen kleiner als 3 sein, wenn 1, folgen zusätzliche 2 Bytes vor dem Pfad\Dateinamen), 2-Bit: Mergestufe, 12-Bit: Länge Pfad\Dateiname (wenn kleiner als 0xFFF)
2 Bytes
(Version 3
oder höher)
Flags (High-Bits zu Low-Bits)
1-Bit: Reserviert für zukünftige Verwendung
1-Bit: Flag skip-worktree (Sparse Checkout)
1-Bit: Flag intent-to-add (git add -N)
13-Bit: Nicht verwendet, muss null sein
Variable Länge Pfad/Dateiname NULL-terminiert

Die ersten 8 Bytes geben die Erstellungszeit der Datei als Offset vom 1. Januar 1970 (Mitternacht) an. Die zweiten 8 Bytes geben die Änderungszeit der Datei als Offset vom 1. Januar 1970 (Mitternacht) an. Die nächsten fünf 4-Byte-Werte („device“, „inode“, „mode“, „user id“ und „group id“) sind file-attribute-Metadaten, die sich auf das Hostbetriebssystem beziehen. Der einzige unter Windows verwendet Wert ist „mode“. Meistens weist „mode“ den Oktalwert 100644 auf, den ich weiter oben beim Zeigen der Ausgabe aus dem ls-files-Befehl erwähnt habe (dieser Wert wird in den 4-Byte-814AH-Wert konvertiert, den Sie an Position 26H in Abbildung 5) sehen können.

Auf die Metadaten folgt die 4-Byte-Länge des Dateiinhalts. In Abbildung 5 beginnt dieser Wert bei 030, der 00 00 0A 15 (2,581 dezimal) anzeigt: die Länge der Datei „.gitattributes“ in meinem System:

05/08/2017  09:24 PM    <DIR>          .
05/08/2017  09:24 PM    <DIR>          ..
05/08/2017  09:24 PM             2,581 .gitattributes
05/08/2017  09:24 PM             4,565 .gitignore
05/08/2017  09:24 PM    <DIR>          MSDNConsoleApp
05/08/2017  09:24 PM             1,009 MSDNConsoleApp.sln
               3 File(s)          8,155 bytes

               3 Dir(s)  92,069,982,208 bytes free

An Offset 034H befindet sich der 20-Byte-SHA-1-Wert für das Blob-Objekt:

1ff0c423042b46cb1d617b81efb715defbe8054d.

Erinnern Sie sich daran, dass dieser SHA-1-Wert auf das Blob-Objekt verweist, das den Dateiinhalt für die betreffende Datei enthält: „.gitattributes“.

An Offset 048H befindet sich ein 2-Byte-Wert, der zwei 1-Bit-Flags enthält, einen 2-Bit-Mergestufenwert und einen 12-Bit-Wert für die Länge des Pfads/Dateinamens für den aktuellen Indexeintrag. Von den zwei 1-Bit-Flags legt das High-Order-Bit fest, ob für den Indexeintrag das Flag „assume-unchanged“ festgelegt wird (normalerweise mithilfe des Git-Plumbingbefehls „update-index“). Das Low-Order-Bit gibt an, ob weitere zwei Datenbytes dem Eintrag für den Pfad\Dateinamen vorangestellt werden. Dieses Bit kann nur für die Indexversionen 3 oder höher 1 sein). Die nächsten 2 Bits enthalten wie weiter oben beschrieben einen merge-stage-Wert, der zwischen 0 und 3 liegt. Der 12-Bit-Wert enthält die Länge der Zeichenfolge für den Pfad\Dateinamen.

Wenn das erweiterte Flag festgelegt wurde, enthält ein 2-Byte-Wert die Flags „skip-worktree“ und „intent-to-add bit“ sowie Füllplatzhalter.

Eine Bytesequenz variabler Länge enthält schließlich den Pfad\Dateinamen. Dieser Wert ist mit mindestens einem NULL-Zeichen terminiert. Auf diese Terminierung folgt das nächste Blob-Objekt im Index oder mindestens ein Indexerweiterungseintrag (wie Sie gleich sehen werden).

Weiter oben habe ich erwähnt, dass Git Tree-Objekte erst erstellt, nachdem ein Commit für das bereitgestellte Element ausgeführt wurde. Dies bedeutet, dass der Index nur mit Pfad-/Dateinamen und Verweisen auf Blob-Objekte startet. Sobald Sie einen Commit ausgeben, aktualisiert Git jedoch den Index so, dass er Verweise auf die Tree-Objekte enthält, die während des letzten Commits erstellt wurden. Wenn diese Verzeichnisverweise während des nächsten Commits noch in Ihrem Arbeitsverzeichnis vorhanden sind, können die zwischengespeicherten Tree-Objektverweise verwendet werden, um den Arbeitsaufwand für Git beim nächsten Commit zu verringern. Wie Sie sehen, ist die Funktion des Index außerordentlich facettenreich. Aus diesem Grund wird er als Index, Stagingbereich und Cache beschrieben.

Der in Abbildung 7 gezeigte Indexeintrag unterstützt nur Blob-Objektverweise. Zum Speichern von Tree-Objekten verwendet Git eine Erweiterung.

Indexerweiterungen

Der Index kann Erweiterungseinträge umfassen, in denen spezialisierte Datenströme zum Bereitstellen zusätzlicher Informationen für das Git-Modul gespeichert werden, die das Modul ggf. beim Überwachen von Dateien im Arbeitsverzeichnis sowie beim Vorbereiten des nächsten Commits berücksichtigen kann. Zum Zwischenspeichern von während des letzten Commits erstellen Tree-Objekten fügt Git dem Index ein Tree-Erweiterungsobjekt für den Stamm des Arbeitsverzeichnisses sowie für jedes Unterverzeichnis hinzu.

Abbildung 5, Markierung 2 zeigt die Schlussbytes des Index und erfasst die im Index gespeicherten Tree-Objekte. Abbildung 8 zeigt das Format für die Tree-Erweiterungsdaten.

Abbildung 8: Das Datenformat für das Tree-Erweiterungsobjekt der Git-Indexdatei

Indexdatei: Zwischengespeicherter Tree-Erweiterungsheader
4 Bytes TREE Feste Signatur für einen zwischengespeicherten Tree-Erweiterungseintrag.
4 Bytes 32-Bit-Wert, der die Länge von TREE-Erweiterungsdaten darstellt  

 

Zwischengespeicherter Tree-Erweiterungseintrag
Variable Pfad NULL-terminierte Pfadzeichenfolge (nur für den Stammbaum null).
ASCII-Wert Anzahl der Einträge ASCII-Wert, der die Anzahl der Einträge im Index darstellt, die von diesem Tree-Eintrag abgedeckt werden.
1 Byte 20H (Leerzeichen)  
ASCII-Wert Anzahl der Unterbäume ASCII-Wert, der die Anzahl der Unterbäume dieses Baums darstellt.
1 Byte 0AH (Zeilenvorschubzeichen)  
20 Bytes SHA-1-Wert des Tree-Objekts SHA-1-Werte des Tree-Objekts, die dieser
Eintrag generiert.

Der Tree-Erweiterungsdatenheader an Offset 284H besteht aus der Zeichenfolge „TREE“ (markiert den Start der zwischengespeicherten Tree-Erweiterungsdaten), gefolgt von einem 32-Bit-Wert, der die Länge der folgenden Erweiterungsdaten angibt. Die nächsten Einträge beziehen sich auf jeden Tree-Eintrag: Der erste Eintrag ist ein NULL-terminierter Zeichenfolgenwert variabler Länge für den Tree-Pfad (oder einfach NULL für den Stammbaum). Der folgende Wert ist ein ASCII-Wert. Er muss daher als „7“ (wie im Hex-Editor angezeigt) gelesen werden. Er gibt die Anzahl der durch den aktuelle Baum abgedeckten Blob-Einträge an (weil dies der Stammbaum ist, ist die gleiche Anzahl von Einträgen vorhanden, die zuvor beim Ausgeben des Git-Befehls „ls-files stage“ angezeigt wurde). Das nächste Zeichen ist ein Leerzeichen, gefolgt von einem weiteren ASCII-Wert, um die Anzahl der Unterbäume des aktuellen Baums darzustellen.

Der Stammbaum für unser Projekt besitzt nur einen Unterbaum: „MSDNConsoleApp“. Auf diesen Wert folgt ein Zeilenvorschubzeichen und dann der SHA-1-Wert für den Baum. Der SHA-1-Wert beginnt an Offset 291 mit „0d21e2“.

Bestätigen wir nun, dass „0d21e2“ tatsächlich der SHA1-Wert des Stammbaums ist. Navigieren Sie zu diesem Zweck zum Befehlsfenster, und geben Sie Folgendes ein:

git log

Dieser Befehl zeigt Details zu den vor Kurzem erfolgten Commits an:

commit 5192391e9f907eeb47aa38d1c6a3a4ea78e33564
Author: Jonathan Waldman <jonathan.waldman@live.com>
Date:   Mon May 8 21:24:15 2017 -0500

  Add project files.

commit dc0d3343fa24e912f08bc18aaa6f664a4a020079
Author: Jonathan Waldman <jonathan.waldman@live.com>
Date:   Mon May 8 21:24:07 2017 -0500

  Add .gitignore and .gitattributes.

Der jüngste Commit ist der Commit mit dem Zeitstempel „21:24:15“. Dieser hat also den Index zuletzt aktualisiert. Ich kann den SHA-1-Wert dieses Commits zum Suchen nach dem SHA-1-Wert des Stammbaums verwenden:

git cat-file -p 51923

Dieser Befehl generiert die folgende Ausgabe:

tree 0d21e2f7f760f77ead2cb85cc128efb13f56401d
parent dc0d3343fa24e912f08bc18aaa6f664a4a020079
author Jonathan Waldman <jonathan.waldman@live.com> 1494296655 -0500
committer Jonathan Waldman <jonathan.waldman@live.com> 1494296655 -0500

Der Tree-Eintrag oben ist das Stammbaumobjekt. Er bestätigt, dass der Wert „0d21e2“ an Offset 291H im Indexdump tatsächlich der SHA-1-Wert für das Stammbaumobjekt ist.

Die anderen Tree-Einträge folgen unmittelbar nach dem SHA-1-Wert ab Offset 2A5H. Führen Sie den folgenden Befehl aus, um die SHA-1-Werte für zwischengespeicherte Tree-Objekte unter dem Stammbaum zu bestätigen:

git ls-tree -r -d master

Dieser Befehl zeigt nur die Tree-Objekte rekursiv für den aktuellen Branch an:

040000 tree c7c367f2d5688dddc25e59525cc6b8efd0df914d    MSDNConsoleApp
040000 tree 2723ceb04eda3051abf913782fadeebc97e0123c    MSDNConsoleApp/Properties

Der mode-Wert von 040000 in der ersten Spalte gibt an, dass dieses Objekt ein Verzeichnis und keine Datei ist.

Schließlich enthalten die letzten 20 Bytes des Index einen SHA-1-Hashwert, der den Index selbst darstellt: Git verwendet diesen SHA-1-Wert wie erwartet zum Überprüfen der Datenintegrität des Index.

Auch wenn ich alle Einträge in der Beispielindexdatei dieses Artikels behandelt habe, sind größere und komplexere Indexdateien die Norm. Das Indexdateiformat unterstützt z. B. die folgenden weiteren Erweiterungsdatenströme:

  • Einen Datenstrom, der Mergevorgänge und die Auflösung vom Mergekonflikten unterstützt. Er trägt die Signatur „REUC“ (für „Resolve Undo Conflict, Konflikt beim Rückgängigmachen lösen).
  • Einen Datenstrom zum Verwalten eines Caches mit nicht nachverfolgten Dateien (dabei handelt es sich um Dateien, die von der Nachverfolgung ausgeschlossen werden sollen; sie werden in den Dateien „.gitignore“ und „.git\info\exclude“ und durch die Datei angegeben, auf die „core.excludesfile“ verweist). Er trägt die Signatur „UNTR“.
  • Einen Datenstrom zum Unterstützen eines Modus zum Teilen des Index, um Indexaktualisierungen für sehr große Indexdateien zu beschleunigen. Er trägt die Signatur „link“.

Das Erweiterungsfeature des Index ermöglicht Ergänzungen seiner Funktionalität.

Zusammenfassung

In diesem Artikel habe ich mich mit der Git-Dreibaumarchitektur beschäftigt und die Details hinter deren Indexdatei untersucht. Ich habe gezeigt, dass Git den Index als Reaktion auf bestimmte Vorgänge aktualisiert und außerdem im Index enthaltene Informationen verwendet, um andere Vorgänge auszuführen.

Git kann durchaus verwendet werden, ohne sich viele Gedanken um den Index zu machen. Wenn Sie jedoch wissen, wie der Index funktioniert, erhalten Sie wertvolle Einblicke in die Kernfunktionen von Git und können nachvollziehen, wie Git Änderungen an Dateien im Arbeitsverzeichnis erkennt, was der Stagingbereich ist und warum er Sinn macht, wie Git Mergevorgänge verwaltet und warum Git einige Vorgänge so schnell ausführen kann. Außerdem verstehen Sie Befehlszeilenvarianten der Auscheck- und Rebasebefehle besser sowie den Unterschied zwischen Teil-, gemischten und Vollrückstellungen. Mithilfe solcher Features können Sie angeben, ob der Index, das Arbeitsverzeichnis oder sowohl der Index als auch das Arbeitsverzeichnis beim Ausgeben bestimmter Befehle aktualisiert werden sollten. Solche Optionen werden in der Praxis durchaus verwendet, wie die Literatur zu Git-Workflows, -Strategien und erweiterten Vorgängen belegt. Das Ziel dieses Artikels besteht darin, Ihnen die Wichtigkeit der Funktion des Index vor Augen zu führen, damit Sie besser einschätzen können, wie er genutzt werden kann.


Jonathan Waldman ist ein Microsoft Certified Professional, der bereits seit ihrer Einführung mit Microsoft-Technologien gearbeitet hat. Sein Spezialgebiet ist Softwareergonomie. Waldman ist Mitglied des technischen Pluralsight-Teams und leitet zurzeit Softwareentwicklungsprojekte aus dem öffentlichen und privaten Sektor. Sie erreichen ihn unter jonathan.waldman@live.com.

Unser Dank gilt den folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Kraig Brockschmidt, Saeed Noursalehi, Ralph Squillace und Edward Thomson


Diesen Artikel im MSDN Magazine-Forum diskutieren