Data Points
Git: alles nur Daten.
Laden Sie die Codebeispiele herunter
Quellcodeverwaltung in einer Datenspalte? Wenn die Quellcodeverwaltung nichts weiter als eine einzige große Datenbank sein soll, dann lassen Sie uns eintauchen in ein aufregendes Datenabenteuer. Vorab sei jedoch gesagt, dass ich diesen Monat nicht über Git schreibe, weil ich eine Expertin bin. Im Gegenteil, ich habe schon manchen Kampf damit ausgefochten. Das beweist alleine schon mein etwas blutleeres GitHub-Profil. Immer wenn die Rede auf Git kam, wechselte ich verstohlen das Thema, um mich nicht als Git-Laie zu outen. Schließlich arbeiteten die meisten meiner Entwicklerkollegen wie selbstverständlich mit diesem Tool.
Als ich kürzlich meinem Freund und Ruby-Entwickler Alan Peabody (github.com/alanpeabody) am Rande einer Hacker-Happy-Hour von meiner Git-Paranoia erzählte, erwiderte er, “Mensch Julie, das sind doch nur Daten”. Daten? “Wow, ich liebe Daten! Erzähl mir mehr davon!" Und Alan erzählte mir, wie Git mit einer Datenbank voller Schlüssel-Wert-Paare arbeitet. Statt jedoch die anwenderfreundlichen Benutzeroberflächentools, die so genannten "Porzellanbefehle" (porcelain) zu benutzen, empfahl er mir, mit den etwas plumperen "Klempnerbefehlen" (plumbing) und APIs zu experimentieren.
Git verfügt also über eine Datenbank und einen Low-Level-Befehlssatz! Das hat mein Interesse schlagartig geweckt. Inzwischen beschäftige ich mich schon eine ganze Weile mit Git: den Low-Level-Befehlen, dem Lesen und Schreiben von Repository-Aktivitätsdaten und dem Zusammenspiel der verschiedenen Objekttypen. Ich bin noch weit davon entfernt, eine Expertin zu sein, fühle mich aber immer sicherer mit Git. Vor allem aber habe ich Spaß am Experimentieren mit dem Tool und sehe es nicht mehr als Buch mit sieben Siegeln an.
Wenn Sie sich davon angesprochen fühlen, dürfen Sie auf keinen Fall die Kapitel "Git Basics" und "Git Internals" des Onlinebuchs "Pro Git" von Scott Chacon (Apress, 2009) auf git-scm.com/book verpassen.
Die Sache mit der Git-Datenbank war mir natürlich auch vorher schon klar, aber aus Respekt vor der Lernkurve hatte ich es wohl verdrängt. Jetzt aber geht's mitten rein ins Vergnügen. Im weiteren Artikel werde ich mit der Datenbank und einigen Codebespielen interagieren. Warten wir es ab, wie sich das auf die Datenbank auswirkt, die mein Repository darstellt.
Anstatt mit einem völlig leeren Repository zu beginnen, verwende ich ein bereits bestehendes Repository aus den Anfängen von Entity Framework 7 (GitHub.com/aspnet/EntityFramework). Da es sich dabei um ein sehr dynamisches Projekt handelt, können sich die Dateien, auf die ich mich in dieser Kolumne beziehe, bereits geändert haben.
Ich führe die Befehle in Windows PowerShell mit posh-git aus. So profitiere ich in der Befehlszeile von zusätzlichen Statusinformationen, Farbcodierung und Befehlszeilenergänzung. Informationen zum Einrichten dieses Tools finden Sie in dem Download, das für diesen Artikel verfügbar ist.
Anlegen eines Repositorys und Untersuchen der Git-Assets
Der erste Schritt besteht darin, das bestehende Repository zu klonen. Dazu führe ich in meinem github-Ordner den Befehl "git clone" aus:
D:\User Documents\github> git clone git://github.com/aspnet/EntityFramework
Schon der erste Schritt ist ein Erfolgserlebnis! Git legt unter dem Namen des Repositorys einen neuen Ordner in meinem Startverzeichnis an: "D:\User Documents\github\EntityFramework". Wenn wir während des Kopiervorgangs im Datei-Explorer den Ordner "EntityFramework" öffnen, sehen wir, dass als erstes der Unterordner ".git" erstellt wird. Dabei handelt es sich um das Repository, oder anders gesagt um unsere Git-Datenbank. Git verwendet die zugehörigen Daten, um das Quellrepository im so genannten Arbeitsverzeichnis nachzubilden, also dem Grundgerüst des Ordners "EntityFramework".
Nach diesem Schritt sieht der neue Ordner "EntityFramework" genauso aus wie jeder andere Visual Studio-Projektmappenordner. Es gibt jedoch eine Ausnahme: Der Ordner ".git" ist eine vollständige Kopie des geklonten Repositorys inklusive aller Branches (Verzweigungen). Beachten Sie dazu die Dateistruktur in Abbildung 1. Eine Branch in Git ist nichts anderes als ein Zeiger auf einen Commit, und ein Commit ist ein Zeiger auf einen Schnappschuss des Arbeitsverzeichnisses. Auch wenn die Haupt-Branch des Git-Repositorys den Standardnamen "master" erhält, müssen Sie nicht zwingend bei dem Namen bleiben. Die Standard-Branch des EF-Teams verweist beispielsweise auf eine Branch mit dem Namen "dev". Mithilfe von Branches lässt sich Code sicher ändern und testen, bevor die Änderungen in andere Branches übernommen werden.
Abbildung 1 – Projektmappenordner mit einer Kopie des Repositorys, einschließlich des Repositorys im Ordner ".git"
Der übrige Inhalt des Ordners "EntityFramework" stellt das Arbeitsverzeichnis dar. Irgendwann ist es soweit, dass Sie die Dateien, die Sie im Arbeitsverzeichnis hinzugefügt, geändert oder gelöscht haben, von Git nachverfolgen lassen. Dieser Vorgang wird als "Staging" bezeichnet, d. h., Informationen über Änderungen sind gespeichert, und die Dateien warten auf den Commit. Die Änderungen bleiben auch nach dem Commit in der Datenbank gespeichert. Schließlich werden die Änderungen mit "push" auf den Server verschoben. Da mein Fokus auf der Datenbankaktivität liegt, werde ich hier nicht weiter auf diesen Schritt eingehen. Es gibt eine Reihe weiterer Begriffe wie Verzeichnisbaum, Verzweigungen, Verweise und Header, die wir hier ebenfalls auslassen.
Was hat es aber mit unserer Datenbank auf sich? Ist es eine relationale Datenbank wie SQL Server oder SQL CE? Nein. Es handelt sich um eine Sammlung von Dateien, die die Git-Objekte darstellen. Der Name jeder Datei und der Inhalt basieren auf Hashwerten. Zur Erinnerung: Die Datenbank ist eine Sammlung von Schlüssel-Wert-Paaren. Der Dateiname ist der Schlüssel, der das Objekt in der Datenbank darstellt, und der Inhalt entspricht dem Wert. Diese Sammlung von Objekten ergibt die Datenbank, die unser Repository darstellt. Jedes Schlüssel-Wert-Paar wird im Ordner ".git/objects" und eine Masterliste der Objekte in einer Indexdatei (IDX) gespeichert. Andere Objekte sorgen dafür, die verschiedenen Verzweigungen nachzuverfolgen. Mitunter werden viele Dateien nur dupliziert, ohne jemals bearbeitet zu werden. Git verfügt über eine Methode, um auf diese Dateien zu verweisen, anstatt Kopien davon anzulegen. So wird verhindert, dass sich die Git-Datenbank explosionsartig vergrößert.
Die Objektdateien sind nicht sichtbar, da sie von Anfang an zusammen mit ihrer IDX-Datei in einer Packdatei komprimiert werden. Diese wird im Unterordner "PACK" gespeichert, wie in Abbildung 2 dargestellt.
Abbildung 2 – In einer PACK-Datei komprimierte, nicht sichtbare Objekte
Was weiß Git über meinen Code?
Bevor ich anfange, meinen Code zu ändern, möchte ich wissen, welche Informationen Git über mein Arbeitsverzeichnis und das Repository speichert. Also kehre ich zu Windows PowerShell zurück und aktiviere "Posh-it", um meine Git-Befehlszeile anwenderfreundlicher zu gestalten. Zunächst wechsle ich in das Verzeichnis "EntityFramework" (cd EntityFramework, das gute alte DOS lässt grüßen). "Posh-git", das über die Windows PowerShell-Umgebung gesteuert wird, zeigt den Unterordner ".git" an. Abbildung 3 Mein Windows PowerShell-Fenster hat jetzt den Titel "posh~git", und der aktuelle Status wird durch gelbe eckige Klammern hervorgehoben – das macht Posh-Git aus. Aktuell sehen wir, dass mein Arbeitsverzeichnis die Verzweigung "dev" verwendet, also die Haupt-Branch des Repositorys EF7. Beim Kopieren des Repositorys wird diese Branch von Git automatisch "ausgecheckt" und in mein Arbeitsverzeichnis verschoben. Die eigentliche Bedeutung von "Auschecken" in Git liegt darin, dass auf der Basis der angegebenen Branch das Arbeitsverzeichnis angelegt wird. Es ist immer wieder beeindruckend, im Datei-Explorer alles bis auf den Ordner ".git" zu löschen und durch die Eingabe von “git checkout dev” in Windows PowerShell den ganzen Verzeichnisinhalt neu erstellen zu lassen. Unterbrechen Sie die Internetverbindung, wenn Sie sicher sein wollen, dass keine Dateien vom Server gezogen werden.
Abbildung 3 – Windows PowerShell mit aktiviertem "posh-git"
Mit dem Git-Befehl "status" kann ich eine Übersicht meines Arbeitsverzeichnisses von Git anfordern. Alle Git-Befehle beginnen mit der Adressierung von Git:
git status
Git gibt die folgende Meldung zurück:
On branch dev
nothing to commit, working directory clean
Das ergibt Sinn, alles steht auf Anfang.
Erfahren Sie, wie Git auf Änderungen an Dateien im Arbeitsverzeichnis reagiert.
Jetzt machen wir die Probe aufs Exempel, um zu sehen, wie sich der Status ändert.
Dazu ändere ich eine meiner liebsten EF-Klassen: "DbContext.cs" (im Ordner "src\EntityFramework"). Ganz ohne Visual Studio kann ich mit NotePad++ oder meinem bevorzugten Text-Editor arbeiten.
Am Anfang habe ich den Kommentar
// Julie was here
hinzugefügt und gespeichert.
Wenn ich "git status" dann erneut ausführe, ist die Statusmeldung schon aussagekräftiger, wie in Abbildung 4 dargestellt.
Abbildung 4 – Status nach dem Ändern einer Datei im Arbeitsverzeichnis (rote Schrift zeigt den Status des Arbeitsverzeichnisses an)
Git erkennt, dass die Datei geändert wurde (siehe die schwer lesbare rote Schrift), sich aber noch nicht im Staging-Bereich befindet. Git hat also noch nicht mit der Nachverfolgung begonnen, sondern die Dateien aus dem Arbeitsverzeichnis erst mit der Datenbank abgeglichen. Beachten Sie auch, dass "dev" in der Befehlszeile den neuen Status "+0 ~1 -0" hat. Der Status in roter Schrift bezieht sich auf das Arbeitsverzeichnis und besagt, dass 0 neue Dateien, 1 geänderte Datei und 0 gelöschten Dateien vorliegen.
Was bedeutet das für die Datenbank? Ein Blick in den Datei-Explorer zeigt, dass sich der Zeitstempel und die Größe der Indexdatei nicht geändert haben. Das Gleiche gilt für den Ordner "objects". Die Git-Datenbank hat also von der Änderung in meinem Arbeitsverzeichnis nichts bemerkt.
Hallo Git, was ist mit der Nachverfolgung meiner Datei?
Git beginnt erst mit der Nachverfolgung von Dateien, wenn es dazu aufgefordert wird. Die herkömmlichen Porzellan-Befehle bewirken, dass Git Änderungen hinzufügt und die richtigen Dateien lädt. Ich werde nun anstelle der beliebten, anwenderfreundlichen Befehle die Low-Level-Befehle verwenden, damit ich jeden einzelnen Schritt in Git genau steuern kann. Bei der folgenden Pfadangabe wird Groß-/Kleinschreibung unterschieden:
git update-index --add src\EntityFramework\DbContext.cs
Durch diesen Befehl ändert sich der Status in der Befehlszeile. Er lautet zwar immer noch "+0 ~1 -0", doch die Schrift ist jetzt grün anstatt rot. Wir sehen demnach den Status der Indexdatei. Git hat jetzt 0 neue Objekte, 1 geändertes Objekt und 0 gelöschte Objekte erkannt, die auf den Commit warten.
Und wie sieht es mit der Git-Indexdatei und dem Ordner "objects" aus?
Der Zeitstempel der Indexdatei im Verzeichnis ".git" hat sich geändert. Daran ist erkennbar, dass sich die Objektdatei, die die Klasse "DbContext.cs" darstellt, geändert hat. Die Statusabfrage ergibt also, dass 1 geänderte Datei nachverfolgt wird. Den Entity Framework-Programmierern kommt das vielleicht bekannt vor. Die Indexdatei verhält sich ähnlich wie EF DbContext, durch die Änderungen der Entitäten nachverfolgt werden! DbContext weiß, wann eine Objektinstanz hinzugefügt, geändert oder gelöscht wurde. Die Indexdatei funktioniert ähnlich,
nur wird dieses Mal auch das Objekt angezeigt. Wenn Sie zu ".git/objects" navigieren, sehen Sie einen neuen Ordner. Nach der Codebearbeitung in Visual Studio wird sogar noch mehr angezeigt, z. B., ob sich die Projektdatei geändert hat. Da ich aber Notepad++ verwendet habe, sehen wir nur den neuen Ordner "ae". Der Ordner enthält eine Datei, deren Name auf einem Hashwert basiert, wie in Abbildung 5 dargestellt.
Abbildung 5 – Ein von Git nachverfolgtes Objekt im Ordner "objects"
Dieses Objekt ist Teil meiner Datenbank und überschreibt das Objekt, das die in der PACK-Datei zwischengespeicherte Datei "DbContext.cs" darstellt. Die neue Objektdatei enthält den hashcodierten Inhalt von "DbContext.cs" einschließlich meiner Änderung.
Ich kann ihn zwar nicht lesen, aber Git schon. Mit dem Befehl "cat-file" weise ich Git an, den gesamten Inhalt nachzuschlagen:
git cat-file -p aeb6db24b9de85b7b7cb833379387f1754caa146
Mit dem Parameter "-p" lässt sich der Text lesbar darstellen. Auf den Parameter folgt der Name des aufzulistenden Objekts, das aus einer Kombination aus Ordnername (ae) und Dateiname besteht. Git verfügt über Kurzbefehle zur Darstellung dieses Objektnamens.
Noch interessanter wird es, wenn auch die Änderung ausgegeben wird. Mit dem folgenden Befehl frage ich Änderungen in der Branch (mit dem Namen "dev") ab:
git diff dev
Die Antwort, die ich zur besseren Lesbarkeit mit Zeilennummern versehen habe, wird in Abbildung 6 angezeigt.
Abbildung 6 Ausgabe des Befehls "git diff"
Der Text ist farbig gestaltet, sodass abweichende Informationen auf Anhieb erkennbar sind. In Zeile 8 weist ein (roter) Bindestrich auf eine gelöschte [leere] Zeile hin. Zeile 9 beginnt mit einem Pluszeichen und ist grün, was auf eine neue Zeile hindeutet. Hätte ich dem Index weitere Objekte für geänderte oder neue Dateien hinzugefügt, würden sie hier ebenfalls angezeigt werden.
Trotz des Binärformats kann ich die Indexdatei durchsuchen. Der Befehl "git ls-files" ist eine praktische Lösung zur Auflistung aller Dateien, die durch Repositoryobjekte in der Indexdatei dargestellt werden. Da "ls-files" jedoch den zwischengespeicherten Index und das Arbeitsverzeichnis abfragt, muss ich mir unter allen Dateien meiner Projektmappe die wirklich interessanten heraussuchen. Wenn der Git-Befehl "grep" mit dem Befehl "ls-files" kombiniert wird, lassen sich die Dateien jedoch filtern. Ich füge den Befehl "grep" (Unterscheidung nach Groß-/Kleinschreibung) hinzu und verwende den Parameter "-s" (Stage), damit "ls-files" auch den Objektnamen anzeigt:
git ls-files -s |grep DbContext.cs
Die Angabe des Stage-Parameters "-s" erzwingt die Ausgabe eines FileMode-Indikators (100664 steht für eine nicht ausführbare Datei mit Gruppenschreibrecht) und des Hashdateinamens des Objekts, dem er zugeordnet ist:
100644 aeb6db24b9de85b7b7cb833379387f1754caa146 0
src/EntityFramework/DbContext.cs
Zu "ls-files" gäbe es noch einiges mehr zu sagen, aber wir belassen es im Moment dabei, dass Git uns die Zuordnung zwischen Objektdatei und der Datei "DbContext.cs" in meinem Arbeitsverzeichnis anzeigt.
Wie wirken sich Commits auf die Git-Datenbank aus?
Als Nächstes erfahren wir, wie sich der Wechsel vom Stage- in den Commit-Status auf Objekte auswirkt. Zur Erinnerung: Die Commits finden nur auf dem Computer statt. Kein Commit gelangt in das ursprüngliche Repository auf dem Server. Mit dem Befehl "git commit" müssen Sie den Parameter "-m" gefolgt von einer Notiz angeben. Denken Sie daran, dass der Commit für alle Stage-Elemente ausgeführt wird, auch wenn wir uns hier auf eines beschränkt haben:
git commit -m "Edited DbContext.cs"
Git gibt als Antwort den neu generierten Git-Datenbanknamen des Objekts (2291c49) zurück. Dieses enthält die Commitinformationen mit Angaben dazu, wofür ein Commit ausgeführt wurde. Die Notiz zur Einfügung gibt den Typ der Änderung an: 1 Datei wurde hinzugefügt:
[dev 2291c49] Edited DbContext.cs
1 file changed, 1 insertion(+)
D:\User Documents\github\
entityframework [dev]>
Die Befehlszeile ist jetzt leer. Der Status zeigt an, was seit dem letzten Commit passiert ist: nichts. Alles steht also wieder auf Anfang.
Wie hat sich der Commit aber auf meine Datenbank ausgewirkt? Ich stelle fest, dass die Indexdatei nicht aktualisiert wurde – der Zeitstempel ist unverändert.
Es gibt jedoch vier neue Ordner im Verzeichnis "objects", von denen jeder ein eigenes Hashobjekt enthält. Mit "cat-file" schlage ich den Dateiinhalt nach. Als erstes sehe ich eine Liste (ähnlich wie bei "ls-files") aller Dateien und Ordner im Verzeichnis "src\EntityFramework" zusammen mit ihren Objektnamen. Dann folgt eine Liste aller Objekte im Ordner "src", also eine Liste der Unterordner. Dabei handelt es sich in der Git-Sprache um "Verzeichnisbäume" und nicht um "Ordner". Das dritte Objekt enthält eine Liste aller Dateien und Ordner im Stammordner "EntityFramework". Das endgültige Objekt enthält Daten, die zur Synchronisierung mit dem Server erforderlich sind: meine Committer- und Autorenidentität (meine E-Mail-Adresse), ein Objekt für "tree" und ein weiteres für "parent". Dieses endgültige Objekt entspricht genau dem Objekt, das in der Antwort auf meinen Commit übermittelt wurde. Es befindet sich im Ordner 22, und sein Dateiname beginnt mit 91c49.
Alle diese Objekte werden verwendet, um die Dateien zur gegebenen Zeit mit "push" in das Repository auf dem Server zu verschieben.
Kann ich es mit Git aufnehmen?
Ich betrachte Git inzwischen als Freund, weil ich es von Grund auf untersucht habe. Ich liebe das Wechselspiel von Ursache und Wirkung und die Arbeit mit dem grundlegenden Befehlssatz. Git ist definitiv kein Schreckgespinst mehr für mich, und durch das Ausprobieren bin ich auf Dinge in der Befehlssprache gestoßen, die das Arbeiten mit Git zu einer wunderbaren Erfahrung machen. Nebenbei bemerkt, habe ich noch nie so viel Zeit in Windows PowerShell verbracht.
Es gibt Programmierer, die nichts auf Git kommen lassen wie Cori Drew, die mir via Skype mehrere Male Beistand geleistet hat, wenn ich nicht mehr weiterkam. Viele Programmierer brauchen Git wie die Luft zum Atmen. Sie wollten mir Git näherbringen, aber ich habe stets abgewunken. Ich ziehe schon jetzt einen großen Nutzen aus dem Projekt, auch wenn es noch viel zu entdecken gibt. Meine Faszination für Daten und mein Interesse für das Funktionieren von Zusammenhängen haben mich an diesen Punkt gebracht. Das verdanke ich Alan Peabody und seiner Inspiration.
Nachtrag: Eine Woche nach Abschluss dieses Artikels hatte mich das Git-Fieber bei zwei Soloprojekten wieder voll gepackt. Ich fühlte mich fast wie ein Profi, so schnell ging mir das Verzweigen und Zusammenführen von der Hand! Mein eigenes Experiment war ein großer Erfolg. Genau das wünsche ich auch Ihnen.
Julie Lerman ist Microsoft MVP, .NET-Mentor und Unternehmensberaterin. Sie lebt in den Bergen von Vermont. Sie hält bei User Groups und Konferenzen in der ganzen Welt Vorträge zum Thema Datenzugriff und anderen .NET-Themen. Julie Lerman führt unter thedatafarm.com/blog einen Blog. Sie ist die Autorin von "Programming Entity Framework" (2010) sowie von "Code First" (2011) und "DbContext" (2012). Alle Ausgaben sind im Verlag O’Reilly Media erschienen. Folgen Sie ihr auf Twitter unter twitter.com/julielerman, und besuchen Sie ihre Pluralsight-Kurse unter juliel.me/PS-Videos.
Unser Dank gilt den folgenden technischen Experten für die Durchsicht dieses Artikels: Cori Drew (coridrew@gmail.com) und Alan Peabody (gapeabody@gmail.com)