Git-Interna

Bisher haben wir uns angeschaut, wie ihr mit Git die unterschiedlichen Zustände eures Codes verwalten könnt. Hier wollen wir euch nun zeigen, welche Daten- und Speichermodelle Git zugrundeliegen.

Datenmodell

Ihr werdet Git besser nutzen können, wenn ihr das Datenmodell verstanden habt. Dabei verwaltet Git vier Arten von Daten:

Objekte

Alle Commits, Trees, Blobs und Tags in einem Git-Repository werden als Git-Objekte gespeichert, die sich nach ihrer Erstellung nie mehr ändern und eine eindeutige ID haben, z. B. 3a5c279ea2f5d18498b61c229571d2449305a0. D. h., dass ihr mit der ID eines Objekts jederzeit dessen Inhalt wiederherstellen könnt, solange das Objekt nicht gelöscht wurde.

Commit

Ein Commit ist ein Snapshot des gesamten Git-Repository, der durch einen SHA-Wert eindeutig identifiziert werden kann und mindestens die folgenden Angaben enthält:

  • Verzeichnisstruktur aller Dateien dieser Version des Repositorys und Inhalt jeder Datei, gespeichert als Tree-ID des obersten Verzeichnisses des Commits.

  • ID(s) des oder der übergeordneten Commits. Der erste Commit eines Repository hat keine übergeordneten Commits, reguläre Commits haben einen übergeordneten Commit, Merge-Commits haben zwei oder mehr übergeordnete Commits.

  • Autor*in und Zeitpunkt, zu dem der Commit erstellt wurde.

  • Committer und Zeitpunkt, zu dem der Commit committet wurde.

  • Commit-Nachricht

Beispiel:

$ git cat-file -p main
tree 47cc0283b10bd5e4e8a0d61537d13bba3bfad916
parent 63825a43e213ef8a7904a8994976ac86284d32bd
author veit <veit@cusy.io> 1770370977 +0100
committer veit <veit@cusy.io> 1770370977 +0100

:memo: Add links to Python speed

Wie alle anderen Objekte können auch Commits nach ihrer Erstellung nicht mehr geändert werden. Wenn ihr also einen Commit mit git commit --amend ändern wollt, wird tatsächlich ein neuer Commit mit demselben Parent erstellt. Und auch wenn ihr euch einen Commit mit git show anzeigen lasst, so wird das Diff zu diesem Zeitpunkt erst berechnet.

Tree

Darstellung eines Verzeichnisses in Git und kann Dateien oder andere Bäume (also Unterverzeichnisse) enthalten. Für jedes Element im Baum listet er Folgendes auf:

  • Dateiname

  • Dateityp:

    • normale Datei

    • ausführbare Datei

    • symbolischer Link

    • Verzeichnis

    • Gitlink (für Submodule)

  • Objekt-ID mit dem Inhalt der Datei, des Verzeichnisses oder des gitlinks

Beispiel:

$ git cat-file -p main^{tree}
040000 tree 2f59a223f7dc767f4776e77762d208fa72bfd343 .dvc
040000 tree 75833fd33271db55b6f1c96915f60f98a60b51a0 .github
100644 blob 36d2dc5a5228cbf65b8cfe913565c9be49db1a3d .gitignore
...
$ git cat-file -p 2f59a223f7dc767f4776e77762d208fa72bfd343
100644 blob 669784da1fe0818e9abb795f73b7faf393832f2e .gitignore
100644 blob 0a66f9a9ab72e3a99994803de8337f523b1b93d0 config
$ git cat-file -p 36d2dc5a5228cbf65b8cfe913565c9be49db1a3d
# SPDX-FileCopyrightText: 2019 cusy GmbH
#
# SPDX-License-Identifier: BSD-3-Clause
...

Hinweis

Die erste Spalte eines Baum-Eintrags orientiert sich grob an den Unix-Dateirechten, tatsächlich können mit Git jedoch keine Unix-Dateirechte verwaltet werden. Hierzu sind Erweiterungen, wie z.B. etckeeper erforderlich.

Blob

Ein Blob-Objekt enthält den Inhalt einer Datei.

Bei jedem Commit speichert Git den gesamten Inhalt jeder Datei, den ihr geändert habt, als Blob. Wenn ihr beispielsweise einen Commit habt, der zwei Dateien in einem Repository ändert, erstellt dieser Commit zwei neue Blobs, sodass Commits selbst in sehr großen Repositories relativ wenig Speicherplatz beanspruchen.

Tag

Tag-Objekte enthalten mindestens die folgenden Felder:

  • ID des Objekts, auf das es verweist

  • Typ des Objekts, auf das es verweist

  • Tag-Nachricht

  • Tagger und Tag-Datum

Beispiel:

$ git cat-file -p 24.3.0
object aa366cc9af3497544338482f82bdeb21f1dd3c21
type commit
tag 24.3.0
tagger Veit Schiele <veit@cusy.io> 1732086922 +0100

Referenzen

Referenzen sind eine Möglichkeit, Commits einen Namen zu geben, der einfacher, zu merken ist, z. B. für Zweige, Tags, Entfernte Zweige u.s.w. Git verwendet häufig ref als Abkürzung für solche Referenzen. Die wichtigsten Referenzen sind:

.git/refs/heads/BRANCHNAME

Ein Branch bezieht sich auf die ID des neuesten Commits auf diesem Branch. Um die Historie der Commits auf einem Branch abzurufen, beginnt Git bei der Commit-ID, auf die der Branch verweist, und sieht sich dann die übergeordneten Commits an. Referenzen können sich beziehen auf

  • eine Objekt-ID, in der Regel eine Commit-ID

  • eine andere, symbolische Referenz

.git/refs/tags/TAGNAME

Ein Tag bezieht sich auf eine Commit-ID, eine Tag-Objekt-ID oder eine andere Objekt-ID.

.git/HEAD

HEAD ist der Ort, an dem Git euren aktuellen Branch speichert. HEAD kann entweder

  • eine symbolische Referenz auf euren aktuellen Branch sein, z.B. ref: refs/heads/main.

  • eine direkte Referenz auf eine Commit-ID wenn es keinen aktuellen Branch gibt, also in einem detached HEAD state.

.git/refs/remotes/REMOTE/BRANCHNAME

Ein Remote-Tracking-Branch bezieht sich auf eine Commit-ID. Mit git fetch könnt ihr diese ggf. aktualisieren und wenn git status Your branch is up to date with 'origin/main' ausgibt, bezieht es sich darauf.

refs/remotes/{REMOTE}/HEAD ist eine symbolische Referenz auf den Standard-Zweig des Remote-Repositories.

Index

Liste von Dateien und deren Inhalten, die als Blob gespeichert sind. Mit git add könnt ihr Dateien zum Index hinzufügen oder den Inhalt einer Datei im Index aktualisieren.

Im Gegensatz zu einem Tree ist der Index eine flache Liste von Dateien. Wenn ihr committet, konvertiert Git die Liste der Dateien im Index in einen Verzeichnisbaum und verwendet diesen Baum für den neuen Commit. Jeder Indexeintrag hat vier Felder:

  1. Einer der folgenden vier Dateitypen:

    • reguläre Datei

    • ausführbare Datei

    • symbolischer Link

    • gitlink (für Submodule)

  2. Blob-ID der Datei oder Commit-ID des Submoduls

  3. Staging-Nummer, normalerweise 0. Bei einen Merge-Konflikt kann es jedoch auch mehrere Versionen desselben Dateinamens im Index geben.

  4. Dateipfad

Reflog

Jedes Mal, wenn ein Branch, ein Remote-Tracking-Branch oder HEAD aktualisiert wird, aktualisiert Git ein Protokoll namens Reflog für diese Referenz, z.B. in .git/logs/refs/heads/main:

0000000000000000000000000000000000000000 492e16edcf9cdb3371492be59735e517a17cc86c veit <veit@cusy.io> 1739549686 +0100  clone: from github.com:cusyio/Python4DataScience-de.git
492e16edcf9cdb3371492be59735e517a17cc86c c40bfa2a238e824b619f760494ce5ce0769851c3 veit <veit@cusy.io> 1739549907 +0100  commit: Update git docs
c40bfa2a238e824b619f760494ce5ce0769851c3 fa39661bb7fa93b420870845cb174529e8d62552 veit <veit@cusy.io> 1739549971 +0100  rebase (finish): refs/heads/main onto b7214df753ecbd01acd90d8f3dcd359e02441249
...

Jeder Eintrag des Reflog enthält:

  • Commit-ID

  • Commit-ID des nachfolgenden Commits

  • Autor*in

  • E-Mail-Adresse

  • Zeitstempel, zu dem die Änderung vorgenommen wurde

  • Protokollmeldung, z. B.:

    • clone: from REMOTE-URL

    • commit: COMMIT-MESSAGE

    • rebase (finish): refs/heads/main onto BASIC-COMMIT-ID

Reflogs protokollieren Änderungen, die in eurem lokalen Repository vorgenommen wurden. Sie werden jedoch nicht im Remote Repository geteilt.

Siehe auch

Speichermodell

Packfiles

Das Format, in dem Git Objekte auf der Festplatte speichert, wird als loose-Objektformat bezeichnet. Um Platz zu sparen packt Git gelegentlich jedoch mehrere dieser Objekte in eine einzige Binärdatei, die als Packfile bezeichnet wird, um Platz zu sparen und effizienter arbeiten zu können. Ihr könnt das Packen auch manuell mit git push oder git gc ausführen. Dadurch werden in .git/objects/ die meisten eurer Objekte gelöscht und ein neues Dateipaar erstellt:

$ find .git/objects -type f
.git/objects/pack/pack-e9282cda3898f806f7bd108a3675c9e4d236915c.pack
.git/objects/pack/pack-e9282cda3898f806f7bd108a3675c9e4d236915c.idx
*.pack

enthält den Inhalt aller Objekte, die aus eurem Dateisystem entfernt wurden.

.idx

enthält die Offsets dieser Packdatei, sodass schnell zu einem bestimmten Objekt gespringen werden kann.

Ggf. verbleibende Objekte sind Blobs, auf die kein Commit verweist, sog. dangling references, z. B. Dateien im Arbeitsverzeichnis, die nie zu einem Commit hinzugefügt wurden.

Wenn Git Objekte packt, sucht es nach Dateien mit ähnlichen Namen und Größen und speichert nur die Deltas von einer Version der Datei zur nächsten. Mit git verify-pack könnt ihr euch das Packfile ansehen und erkennen, wie Git Speicherplatz sparte:

$ git verify-pack -v .git/objects/pack/pack-e9282cda3898f806f7bd108a3675c9e4d236915c.pack
...
dd1827ebf73b22d9f5828eec005eda4d79520f57 blob   147 140 389838
0a66f9a9ab72e3a99994803de8337f523b1b93d0 blob   31 43 389978 1 dd1827ebf73b22d9f5828eec005eda4d79520f57
...
.git/objects/pack/pack-e9282cda3898f806f7bd108a3675c9e4d236915c.pack: ok
  • Blob 0a66f9a verweist auf den nachfolgenden Blob dd1827e.

  • Die dritte Spalte gibt die Größe des Objekts im Packfile an, sodass ihr sehen könnt, dass dd1827e 147 Bytes einnimmt, während 0a66f9a nur 31 Bytes einnimmt.

  • Die aktuelle Datei wird also unverändert gespeichert, während die ursprüngliche Version als Delta gespeichert ist. Dies erlaubt einen schnelleren Zugriff auf die jeweils neueste Version einer Datei.

  • Die allgemeine Syntax von git verify-pack -v ist:

    OBJECT-ID TYPE SIZE SIZE-IN-PACKFILE OFFSET-IN-PACKFILE [DEPTH BASE-ID]