Vielfältiges Git!

Git kann auch mehrere…

Im normalen Entwickleralltag nutzen wir die Standardmöglichkeiten, die git bietet. Aber gelegentlich können selten genutzte Fähigkeiten von git das Leben angenehm machen, wenn man sie denn kennt. Dieser Artikel stellt einige solcher Fähigkeiten vor.

Wie ein roter Faden durchzieht „Vielfachheit“ die Fähigkeiten, die in diesem Artikel konkret vorgestellt werden. Damit ist gemeint, dass von einem Repository aus „mehrere“ verwaltet werden können, wo im normalen Alltag meistens „eins“ genügt. Beispiele gefällig? Man nutzt man im Normalfall nur einen Worktree (Verzeichnisbaum) auf der eigenen Festplatte, ein remote Repository „origin“ und einen Versionsgraphen im lokalen Repository.

Der Normalfall
Der Normalfall

Im Folgenden werden unter anderem Situationen vorgestellt, in denen mehrere Arbeitsverzeichnisse, mehrere Remotes und mehrere Versionsbäume sich nützlich machen.

Mehrere Arbeitsverzeichnisse

Eine solche Situation ist die Unterbrechung. Irgend etwas schiebt sich in der Dringlichkeit nach vorne. Was man gerade getan hat, muss unterbrochen werden und warten. Ein klassisches Beispiel aus der DevOps-Praxis: Man entwickelt an einem Featurebranch. Plötzlich hängt etwas in der CI/CD-Pipeline und ist im master zu fixen, und zwar schnell.

Es gibt mehrere Möglichkeiten, mit so einer Situation umzugehen. Namentlich kann man git stash benutzen, um den Featurebranchzustand zu sichern; dann wechselt man vom Featurebranch zum master und nimmt die Arbeit am CI/CD-Problem auf.

Eine andere, probate Möglichkeit bietet sich dadurch, dass mit Git ein Respository auf der lokalen Festplatte mehr als ein Arbeitsverzeichnis („Worktree“) verwalten kann.

Zwei Arbeitsverzeichnisse hängen am selben lokalen Repository.
Zwei Arbeitsverzeichnisse hängen am selben lokalen Repository.

Man erzeugt so ein zweites Arbeitsverzeichnis aus der Wurzel des ersten heraus, zum Beispiel mit

git worktree add ../worktree2 master

Nun kann man mit einem schlichten cd ../worktree2 in das so erzeugte Arbeitsverzeichnis wechseln, in dem master bereits ausgecheckt ist, und mit einem ähnlichen cd wieder zurück ins ursprüngliche mit dem Featurebranch. Überhaupt ermöglichen multiple Arbeitsverzeichnisse bequemen und schnellen Kontextwechsel. Zwei IDE-Instanzen können parallel laufen, dann braucht man bei Bedarf nur zwischen den Fenstern zu wechseln.

In der geschilderten Beispielsituation, dass an der CI/CD-Pipeline etwas repariert werden muss, kann das sehr angenehm sein: Während der CI/CD-Job erst losläuft, hat man schon wieder die Arbeit am Featurebranch aufgenommen.

Intern hat worktree2 in seinem Verwaltungsverzeichnis .git nur eine Art Verweis auf das eigentliche Repository. Dadurch stehen alle Commits, Branches, Remotes usw. in beiden gleichartig zur Verfügung. Man kann also zum Beispiel in einem Arbeitsverzeichnis einen Commit zu einem Branch hinzufügen und diesen Commit dann in einem zweiten Arbeitsverzeichnis in einen anderen Branch hineinmergen.

Den Index gibt es naturgemäß für jeden Worktree einzeln. So bleiben git add hier und git add dort unabhängig voneinander. Übrigens wehrt sich Git dagegen, den selben Branch in zwei verschiedenen Arbeitsverzeichnissen auszuchecken.

Hat man das Arbeiten mit parallelen Arbeitsverzeichnissen für sich entdeckt, mag man vielleicht gleich beim Anlegen eines neuen Featurebranches für ihn ein eigenes Arbeitsverzeichnis vorsehen. Dafür bietet git add worktree eine ähnliche Funktionalität -b new_branch wie git checkout. Man nutzt sie zum Beispiel so:

git fetch origin
git worktree add -b f_branch ../f_branch_worktree origin/master

Ein solches Arbeitsverzeichnis lässt sich wieder abräumen, zum Beispiel mit folgender Zeile:

git worktree remove ../f_branch_worktree

Die Arbeit mit mehreren Worktrees hat sich in der alltäglichen Praxis des Autors bewährt. Allerdings warnt die Dokumentation derzeit, dass dieses Feature von Git noch experimentell ist, und rät insgesondere davon ab, von Repositorys mit Submodulen mehrere Arbeitsverzeichnisse zu generieren.

Mehrere Remotes

Das Git-Repository auf der eigenen Festplatte kann mit mehreren „Remotes“ kommunizieren, also fernen Git-Repositorys, wie sie normalerweise auf irgendwelchen Git-Servern liegen.

Mehrere Remotes können zum Beispiel nützlich sein, wenn man temporär mit einer Person zusammenarbeitet, die auf den offiziellen Git-Server keinen Zugriff hat. Dem Autor ist das mehrfach passiert, zuletzt mit einer Praktikantin. Einerseits wünschte das betreffende Projekt ihre Mitarbeit. Andererseits wollte man für diese (kurzzeitige) Mitarbeit nicht den Aufwand treiben, der Praktikantin einen Account im Firmen-LDAP einzurichten. Am LDAP-Account hängt aber der Zugriff auf das offizielle Repo. Was nun? Kriegt man die Zusammenarbeit trotz dieser Rahmenbedingungen hin?

Das geht recht problemlos, indem man ein zweites, temporäres Remoterepo anlegt, auf das beide zugreifen können:

Temporäres zweites Git-Remote
Temporäres zweites Git-Remote

Schnell mal eben ein temporäres Remote

Es gibt dazu konkret verschiedene Möglichkeiten, wie man mal eben ein temporäres Repository aufsetzen kann, auf das zwei Personen A und B zugreifen können.

Besonders bequem hat man es, wenn es irgendwo einen Rechner gibt, auf dem git installiert ist und auf den sowohl A als auch B per ssh zugreifen können.

Das könnte ein ohnehin vorhandener Server sein. Auch eine kurzfristig in irgendeiner Cloud oder bei einem Provider angemietete virtuelle oder reale Maschine ist brauchbar. Selbst ein Winzling (ein Raspberry Pi oder ähnliches) reicht für diesen Zweck. Einer der beiden Arbeitsplatzrechner (der von A oder der von B) lässt sich ebenfalls prima einsetzen, Vertrauen zwischen A und B vorausgesetzt. Man kann auch einen Dockercontainer nutzen.

Für so einen temporären Repositoryserver braucht man sich übrigens normalerweise keine Gedanken über Backups zu machen (was die Sache weiter vereinfacht). Denn alle Information sind in den lokalen Repositorys auch vorhanden, und was wichtig ist, wandert über kurz oder lang ins offizielle Repository. Nach Ende der Kooperation kann das temporäre Repository einfach ohne Weiteres gelöscht werden und gut ist.

Ist ein entsprechender Server gefunden, so ist das nötige temporäre Respository mit wenigen Handgriffen eingerichtet.

Im folgenden Beispiel haben beide auf dem Host server.example.org Zugriff auf den gemeinsam genutzten User user. Einer von beiden bringt die Sache in Gang etwa so:

ssh user@server.example.org mkdir -p collab-repo
ssh user@server.example.org cd collab-repo '&&' git init --bare

Anschließend richtet A für sein lokales Repository ein neue Verbindung zu einem „Remote“ mit dem Namen collab ein, mit

git remote add collab user@server.example.org:collab-repo

und füllt dieses neue temporäre Repo mit

git push collab master

B holt sich dieses Material mit

git clone user@server.example.org:collab-repo

Nutzen A und B für den ssh-Zugriff auf server.example.org unterschiedliche Nutzer auser und buser, so wird es geringfügig komplizierter. Gewöhnlich wird man eine gemeinsame Gruppe z.B. ggroup auf server.example.org finden oder erstellen, der auser und buser beide angehören.

In diesem Fall kann A das Repository so in Gang bringen:

ssh auser@server.example.org mkdir -p collab-repo
ssh auser@server.example.org chgrp ggroup collab-repo
ssh auser@server.example.org chmod g+sw collab-repo
ssh auser@server.example.org cd collab-repo '&&' git init --bare --share=group

Anschließend nutzt Person A auser@server.example.org:collab-repo und Person B buser@server.example.org:/home/auser/collab-repo für den Zugriff.

Eventuell ist es noch nötig, /home/auser mit chmod a+rx /home/auser für lesende Zugriffe zu öffnen. Das ermöglicht unter Umständen lesenden Zugriff durch andere User von server.example.org auch auf andere Dateien von auser. Will man das vermeiden, nutzt man alternativ ein neutrales Verzeichnis (/var/lib/collab-repo oder ähnlich).

Arbeiten mit einem temporären Kollaborationsrepository

Wenn man so ein temporäres Repository erst einmal hat, können die beiden beteiligten Personen A und B bequem damit arbeiten.

B möchte zum Beispiel in einem Featurebranch arbeiten:

git fetch
git checkout -b b_contrib origin/master

A kann neues Material aus dem offiziellen Repository jederzeit zur Verfügung stellen:

git fetch origin
git push collab origin/master:master

B nimmt dieses Material entgegen, wie bei Featurebranches üblich:

git fetch origin
git rebase origin/master

B kann Material zur Verfügung stellen:

git push origin -u b_contrib

und A schaut es sich an:

git fetch collab
git checkout -b b_contrib collab/b_contrib

(Alternativ richtet A sich dafür einen eigenen Worktree ein.)

Soll dieses Material im offiziellen Repository als Featurebranch auftauchen, so kann A es dort zugänglich machen:

git push origin collab/b_contrib:b_contrib

Ross und beide Reiter nennen

Bei der vorgestellten Arbeitsweise bleiben die Commits von B erhalten. Sie finden sich später komplett mit Checkin-Kommentar, Zeitstempel und Autorenangabe „B“ im offiziellen Repo. Dass sie durch Vermittlung von A dort gelandet sind, geht aus dem Repositoryinhalt nicht mehr hervor.

Das kann im Einzelfall erwünscht sein oder unerwünscht. Möglicherweise möchte man die Mitwirkung von A langfristig nachvollziehen können. Für diesen Zwecke bietet sich eine andere selten genutzte Vielfachheit an, die Git bietet.

Und zwar kann selbstverständlich jedes Versionsmanagementsystem von Welt für jede Änderung die Frage beantworten: „Wer war das?“ Git bietet an dieser Stelle Mehrwert: Für jeden Commit werden nicht nur ein, sondern zwei Verantwortliche ins Repository eingetragen. Als „Author“ wird in der Git-Terminologie die Person bezeichnet, die die Änderung inhaltlich entwickelte, als „Committer“, wer sie ins Repository eintrug. Die beiden sind häufig identisch, aber sie können durchaus verschieden sein.

Zwei Verantwortliche in jedem Commit
Zwei Verantwortliche in jedem Commit

Einen neuen Commit kennzeichne ich als eintragende Person als den inhaltlichen Beitrag von, zum Beispiel „A. U. Thor“, mit einem Befehl wie

git commit --author='A U Thor <a.u.thor@example.org>'

Die entstehenden kompletten Angaben kann man sich mit git log anschauen, für den letzten Commit zum Beispiel mit

git log --pretty=fuller HEAD^..HEAD

Ein beispielhafter Output sieht aus wie folgt:

commit 84601e93dff652b0c8c2cbc1ec9e476366b06888 (HEAD -> git-vielfalt-artikel)
Author:     A U Thor <a.u.thor@example.org>
AuthorDate: Mon Mar 4 14:23:18 2019 +0100
Commit:     Andreas Krüger <andreas.krueger@innoq.com>
CommitDate: Mon Mar 4 14:23:18 2019 +0100

    Sample commit with different author.

Es ist auch möglich, sich nachträglich als Committer einzutragen. Dazu erstellt man einfach eine passende Kopie dieses Materials. In der oben beschriebenen Situation könnte A dazu folgende Befehle ausführen:

git checkout b_contrib
git rebase --no-ff origin/master

Repo mit einem Commit

Die bisher vorgeschlagene Methode der Zusammenarbeit von A und B basiert auf vollwertigen Repositorys, die jeweils die gesamte Vergangenheit des Projekts mit an Bord haben. Diese historische Vollständigkeit ist manchmal nützlich, aber gelegentlich auch überflüssiger Ballast. Das Hauptthemas des Artikels, „Vielzahl statt Einzahl“, wird deshalb hier kurzfristig umgekehrt: Es soll von Repositorys die Rede sein, die statt der sonst üblichen Gesamthistorie nur einen Commit enthalten.

Eine (teilweise) Kopie eines Repositorys mit unvollständiger Historie nennt die Git-Dokumentation „shallow“. Man erzeugt sie mit einer entsprechenden --depth-Option von git clone.

Shallow Clone
Shallow Clone

Als Beispiel soll eine Zusammenarbeit zwischen A und B diesmal auf dem Featurebranch f_branch erfolgen. Den hat A bereits angelegt. Dann kann A ein Repository mit nur dem letzten Commit von f_branch erzeugen und an B weitergeben. Das geht notfalls sogar ohne eigenen Git-Server.

Angenommen, das lokale Repo von A liegt in $HOME/lokal-repo. Dann erzeugt A die unvollständige Kopie (in einem Verzeichnis, das vorzugsweise gerade nicht in einem Git-Worktree liegt) mit:

git clone --bare --single-branch -b f_branch --depth 1 file://$HOME/lokal-repo

Die so erzeugte teilweise Repository-Kopie enthält zunächst noch einen Verweis auf das eigene lokale $HOME/lokal-repo. Den entfernt A:

git remote remove origin

Das so präparierte Repository kann A nun in ein ZIP-Archiv o.ä. einpacken und dieses auf irgend einem passenden Weg B zur Verfügung stellen. So ein Vorgehen empfiehlt sich vor allem dann, wenn ein SSH-Server noch nicht gefunden ist, die Netzwerkverbindung zwischen A und B lahmt und B möglichst schnell loslegen soll.

B wird dieses Archiv wieder auspacken und das entstehende Repository als Basis für eine git clone - Operation benutzen. Mit dem daraus entstehenden Arbeitsrepository arbeitet B dann lokal weiter wie gewohnt.

Später soll B Ergebnisse zur Verfügung stellen. Man kann dann problemlos nachträglich „aufrüsten“ auf einen gemeinsam erreichbaren Clone, auf den B Schreibzugriff hat, zum Beispiel ssh-basiert wie oben.

Wenn nur Textdateien geändert wurden, kann B auch eine „Patchdatei“ an A schicken, also den Output von git diff. Das geht bequem via E-Mail. Mit git apply und anschließendem git commit --author ... kann A die Ergebnisse von B lokal integrieren.

Unvollständige Kopien: Auch sonst nützlich!

Die Idee, sich beim Kopieren eines Repositorys auf den letzten Commit zu beschränken, ist auch sonst gelegentlich nützlich. Viele Builds, CI-Pipelines und ähnliche automatisierte Prozesse können auf einfache Weise erheblich beschleunigt werden, wenn einem ohnehin vorhandenen Befehl git clone die Option --depth 1 mitgeben wird.

Derselbe Trick kann auch nützlich sein, wenn man auf den Inhalt eines OpenSource-Repositorys neugierig ist. Man will gerne so schnell wie möglich einen Worktree auf der eigenen Festplatte begutachten können. Die Historie des Repositorys interessiert erst später, in zweiter Linie.

In diesem Fall besorgt man sich das Repository zunächst mit git clone --depth 1. Das geht schnell, entsprechend bald kann man anfangen, sich im Worktree umzusehen. Währenddessen lässt man im Hintergrund den langen Download der Gesamthistorie laufen:

git fetch --unshallow

Wenn man das auf einem Repository mit vollständiger Historie aufruft, erscheint natürlich eine Fehlermeldung. Bei einigen Git-Versionen ist diese Fehlermeldung verwirrend falsch ins Deutsche übersetzt worden:

fatal: Die Option --unshallow kann nicht in einem Repository
mit unvollständiger Historie verwendet werden.

Das sollte „… mit vollständiger Historie …“ heißen.

Wenn man von einem Repository beim initialen Klonen nur den letzten Commit haben wollte, holt Git auch bei späteren git fetch-Operationen zunächst nur den betreffenden Branch. Bei von Anfang an komplett geklonten Repositorys holt git fetch dagegen normalerweise alle vorhandenen Branches. Man kann nachträglich konfigurieren, dass alle Branches geholt werden sollen, mit:

git config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'

Mehrere Identitäten

Im Zusammenhang mit OpenSource tritt manchmal ein Problem auf. Und zwar betrifft es Entwicklerinnen und Entwickler, die für „Lohn und Brot“ in kommerziellen Projekten arbeiten und sich zusätzlich in ihrer Freizeit für Open-Source Projekte engagieren. Auf den ersten Augenschein hin sieht es so aus, als müssten sie sich entscheiden: Welche E-Mail-Anschrift sollen sie für die eigenen Git-Aktivitäten benutzen: die berufliche oder die private?

Viele entscheiden sich für die private. Es kann ganz amüsant sein, sich für ein internes kommerzielles Repository von example.com eine Gesamtliste der im Projekt benutzten E-Mail-Anschriften zu besorgen, die nicht zu example.com gehören:

git log --pretty='%ae' | sort -u | grep -v example.com

Obwohl solche Kommandozeilen oft viel Output erzeugen, besteht dieses Problem eigentlich nur scheinbar. Denn Git erlaubt durchaus mehrere Identitäten derselben Person: Man kann die eigene Identität für jedes Repository einzeln konfigurieren.

Konkret wechselt man in ein Repository und konfiguriert dort spezifisch die in die Commits dieses Repositorys einzutragende eigene E-Mail-Anschrift, sowie, wenn man für den Namen eine andere Variante benutzen möchte als sonst, auch diesen. Dafür genügt es, git config wie bei der Ersteinrichtung von git aufzurufen, aber jetzt die Option --global wegzulassen. Dadurch wird die Konfiguration Repository-spezifisch:

git config user.email 'a.u.thor@example.org'
git config user.name 'A. U. Thor'

Mehrere Repositorys synchronisieren: Die Pumpe.

Eine kurzfristige Kopie eines Repositorys auf einem zweiten Server kann nützlich sein, wie dargelegt. Aber auch für einen langfristigen „Zweitwohnsitz“ eines Repositorys gibt es oft gute Gründe.

Im einfachsten Fall ist dabei die Version auf einem der Server die führende Instanz.

In solchen Fällen nutzt man gerne eine „Pumpe“. Damit ist Funktionalität gemeint, die automatisch alle Commits vom führenden Repository in das andere kopiert.

Wann kann man so etwas gebrauchen? Zum Beispiel, wenn man ein intern benutztes Repository veröffentlicht hat, aber nicht alle Teammitglieder und CI/CD/Build-Pipelines zwingen will, sich Credentials für das öffentliche Angebot zu besorgen. Statt dessen bleibt das alte, intern benutzte Repository in Gebrauch wie bisher. Eine Pumpe hält die öffentlich sichtbare Instanz automatisch aktuell.

In unserer Praxis hatten wir neulich einen anderen Fall: Das Entwicklungsteam ist gewohnt, auf den Server eines Git-as-a-Service Anbieters zuzugreifen. Aber ein alternatives Angebot bietet eine angenehme CI-Lösung (für dort gehostete Repositorys), die wir nutzen wollten. Auch hier half eine Pumpe.

So eine Pumpe soll wartungsfrei laufen, gehört also automatisiert und in einen CI/CD-Job untergebracht. Die nötige Funktionalität ist in jeder passablen Skriptsprache schnell geschrieben. Es folgt eine kurze Übersicht, was dafür zu tun ist. Die hier beschriebene Beispielpumpe nutzt ein lokales Repository, sie soll von remote_from nach remote_to kopieren.

Die Pumpe wird zunächst aufrufen

git fetch -p remote_from
git fetch -p remote_to

um sich beidseitig auf den neuesten Stand zu bringen. Dann wird dieser Stand extrahiert, indem

git for-each-ref --format '%(refname) %(objectname)' refs/remotes/remote_from
git for-each-ref --format '%(refname) %(objectname)' refs/remotes/remote_to

läuft und der jeweilige Output (einzeln) aufgefangen wird. Das ergibt je eine Liste von Branchnamen (HEAD ignoriert man) mit SHAs. Bei aktuellen Versionen von git kann man sich das Leben übrigens noch leichter machen, indem man hier %(refname:lstrip=3) benutzt.

Beide Listen kann man nun mit ein paar Zeilen Code der genutzten Skriptsprache vergleichen. Auf Unterschiede wird reagiert wie folgt:

Ein Branch branch_n von remote_from, den es auf remote_to noch nicht gibt oder der dort auf einen anderen SHA hinausläuft als auf remote_from, kopiert man mit

git push -q --force remote_to refs/remote/remote_from/branch_n:refs/heads/branch_n

Einen old_branch, den es auf remote_from nicht mehr gibt, aber noch auf remote_to, löscht man mit

git push -q remote_to :old_branch

Regelmäßig aufgerufen, wird die so programmierte Pumpe remote_to stets auf denselben Stand zwingen, den remote_from hat.

Mehrere Credentials im Zugriff

Innerhalb der CI/CD-Umgebung braucht die Pumpe zwei Credentials, um auf beide Repositorys zuzugreifen. Ein gängiger Weg ist, dass Credentials in CI/CD-Umgebungen mit Hilfe von Umgebungsvariablen zur Verfügung gestellt werden. Wie kann man das praktisch organisieren?

Das kommt auf den Zugriff an. Da gibt es (passend zum Thema des Artikels) wieder verschiedene Möglichkeiten. Die üblichen sind SSH und HTTPS.

SSH Credentials

Auf ein entferntes („remote“) Repository user@host.example.org (oft git@host.example.org) wird mit SSH zugegriffen. Damit der Zugriff funktioniert, sind normalerweise zwei Bedingungen zu erfüllen:

  • Der SSH-Hostkey des Servers ist lokal bekannt.

  • Der SSH-Key des lokalen Users ist auf dem Server eingetragen mit den entsprechenden Berechtigungen.

Um die erste Bedingung zu erfüllen, fängt man den Hostkey einmalig manuell auf mit

ssh -o UserKnownHostsFile=known_hosts user@host.example.org

Wenn man mit diesem Host auf dem eigenen Rechner schon gearbeitet hat, überprüft man, ob der so aufgefangene Host-Key in der neuen Datei known_hosts im derzeitigen Verzeichnis mit dem in der zentralen $HOME/.ssh/known_hosts übereinstimmt. Dazu vergleicht man den Output von:

ssh-keygen -F host.example.org -l -f $HOME/.ssh/known_hosts
ssh-keygen -F host.example.org -l -f known_hosts

Damit haben wir den öffentlichen Host-Key in der Datei known_hosts im derzeitigen Verzeichnis. An dieser Datei ist nichts Geheimes. Sie kann z.B. in ein Git-Repository eingecheckt und in ein Dockerimage kopiert werden. Um sie zu nutzen, kopiert man sie ins Verzeichnis $HOME/.ssh des betreffenden Benutzers, der in der CI/CD-Umgebung die Pumpe ausführt.

Will man auf mehrere Server via SSH zugreifen, so werden die entsprechenden Angaben hintereinander in eine gemeinsame known_host kopiert.

Das eigentliche Credential für die Pumpe ist der private SSH-Schlüssel. Den erzeugt man zum Beispiel mit

ssh-keygen -N '' -f id_rsa

Dieses Kommando erzeugt zwei Dateien, id_rsa.pub und id_rsa. Die Datei id_rsa.pub ist der öffentliche Schlüssel, den kopiert man einmalig in die entsprechende UI des Gitservers. Diese Datei braucht nicht geschützt oder geheimgehalten werden. Die Pumpe selbst benötigt diese Datei übrigens nicht.

Der Inhalt von id_rsa ist dagegen zu schützen. Wer diesen Inhalt kennt, der kann auf die entsprechenden Repos auf dem Gitserver zugreifen (also genau das, was die Pumpe tun soll). Diesen Inhalt konfiguriert man in der entsprechenden UI des CI/CD-Umgebung als „Geheimnis“, das während eines Laufes der Pumpe als Umgebungsvariable zur Verfügung steht, zum Beispiel GIT_PRIVATE_KEY.

Damit die Pumpe diesen Key nutzen kann, läuft am Anfang des Jobs:

mkdir -p "$HOME/.ssh"
touch "$HOME/.ssh/id_rsa"
chmod go-rwx "$HOME/.ssh" "$HOME/.ssh/id_rsa"
echo "$GIT_PRIVATE_KEY" > "$HOME/.ssh/id_rsa"

Die Datei known_hosts gehört ins selbe Verzeichnis $HOME/.ssh.

HTTPS Credentials

Beim Zugriff auf ein Git-Repository via HTTPS kommt man mit klassischen User und Password-Angaben weiter. Die stellt ein Skript zur Verfügung, das darauf wartet, mit dem Parameter get aufgerufen zu werden, um dann zwei Zeilen Output zu erzeugen: Eine mit der User-Angabe, eine für das Password.

Dieses Skript kann so aussehen:

#!/bin/bash

if test "$1" = get
then
  echo username="${GIT_USER}"
  echo password="${GIT_PASSWD}"
fi

Die nötigen Angaben konfiguriert man in der jeweiligen CI/CD-Umgebung als Geheimnisse, die in den Umgebungsvariablen GIT_USER und GIT_PASSWD zur Verfügung stehen.

Dieses Skript stellt man für die Pumpe zur Verfügung, im Beispiel eines Docker-Container vielleicht als /usr/local/bin/gitcreds. Es wird dann für Zugriffe auf einen Git-Server https://git.example.org aktiviert durch den Befehl

git config --global credential.https://git.example.org.helper /usr/local/bin/gitcreds

Wenn man Credentials für mehrere Server braucht, arbeitet man möglicherweise mit mehreren Instanzen gitcreds1, gitcreds2 und so weiter. Alternativ genügt auch ein einzelnes Skript. Indem es STDIN auswertet, kann es erfahren, welche Credentials gerade gebraucht werden. Einzelheiten hierzu finden sich in der Git-Dokumentation; der passende Einstieg ist die Beschreibung von „Credential Helpers“ in api-credentials.

Mehrere Bäume

Nach diesem Ausflug zur „Pumpe“ sei abschließend noch auf eine etwas obskure Mehrfach-Möglichkeit von Git eingegangen. Sie wird nicht so oft gebraucht, aber wenn, ist sie da: Und zwar kann Git im selben Repository auch komplett getrennte Branchbäume verwalten.

Normalerweise gibt es in einem Repository nur einen Commit ohne Vorgänger: Den ersten. Aber es kann durchaus auch mehrere solcher Commits geben. Graphentheoretisch ausgedrückt: Der Commitgraph kann unzusammenhängend sein.

Repo mit Branches ohne gemeinsame Vorfahren.
Repo mit Branches ohne gemeinsame Vorfahren.

Man kann in einem existierenden Repository einen neuen leeren Branch ohne jede Commits und Vorfahren erzeugen zum Beispiel mit

git checkout --orphan isolated-branch

Die Situation ist ähnlich der nach git init: Ein Commit, den man in dieser Situation erzeugt, ist ein „erster“ Commit, also ein Commit ohne Vorgänger. Man fängt sozusagen noch einmal ganz von vorne an.

Dies kann nützlich sein unmittelbar vor der Veröffentlichung bisher interner Arbeit. Etwas hat klein angefangen, als privates Feierabendprojekt einer einzelnen Person, hat einige Irrungen und Wirrungen durchlebt, aber sich dann ganz manierlich entwickelt und soll nun als Open-Source-Repository debütieren.

In dieser Situation ist es eine offene Frage, ob man die Irrungen und Wirrungen mit veröffentlichen will. Die genauen Zeitstempel, wann man seinerzeit aktiv war, sind verhältnismäßig intime personenbezogene Daten, die man vielleicht nicht zur Verfügung stellen will. An die Stelle der Gesamthistorie kann man ein Art „Paukenschlag-Commit“ treten lassen, der das gesamte bisher Erreichte sozusagen „aus dem Nichts“ entstehen lässt.

Ein beispielhaftes Vorgehen (das anschließend im Detail besprochen wird) ist:

git branch -m master old-master
git checkout --orphan master
git reset old-master -- .
git commit -m 'Results thus far, without convoluted history.'

Hier wird zunächst der bisherige master umbenannt in old-master. In diesem Augenblick gibt es keinen Branch master in diesem Repository. In der nächsten Zeile wird master als leerer Branch neu hergestellt, ohne Commit, Vorgänger oder Historie.

Anschließend wird eine Variante von git reset benutzt. Diese Variante kopiert den gesamten Datenbestand des letzten Commits von old-master direkt in den Index. (Man beachte den diese Kommandozeile abschließenden Punkt.)

Es ist etwas ungewohnt, dass die Daten direkt im Index landen und nicht im Arbeitsverzeichnis. Wer das nicht weiß, den kann der Output von git status verwirren.

Mit dem abschließenden git commit wird ein neuer Commit ohne Vorgänger angelegt (und nebenbei wird das Arbeitsverzeichnis gefüllt). Man kann sich anschließend mit gitk oder git log --pretty=raw davon überzeugen, dass der neue Commit tatsächlich keine Vorgänger hat. Dieser Commit ist der gewünschte „Paukenschlag-Commit“, der veröffentlicht werden kann.

Copy+Paste mit Git

Eine Situation mit mehreren getrennten Zusammenhangskomponenten des Commitgraphen lässt sich auch anders herstellen: Man konfiguriert dazu für das lokale Repository zwei verschiedene Remotes, irgendwelche zwei Repositorys, die inhaltlich nichts miteinander zu tun haben.

Das kann gelegentlich nützlich sein, um einzelne Dateien oder Verzeichnisbäume von einem anderen Repository zu übernehmen, ohne, dass Commits des anderen Repositorys deshalb in die eigene Versionsgeschichte integriert werden.

Die oben schon benutzte Variante von git reset erlaubt es ganz allgemein, bequem Material (Dateien und Verzeichnisse) von einem beliebigen Commit direkt in den Index zu kopieren. Es ist dabei nicht nötig, einen Umweg über Kopien im Arbeitsverzeichnis zu gehen.

Der Ursprungscommit des Materials erscheint dadurch nicht als Vorgänger. Man kann also kurzfristig ein zweites Repository als ein neues Remote lokal einbinden, Material mit git reset kopieren, und das Remote wieder entfernen. Dadurch entsteht keinerlei dauerhafte Beziehung zum zweiten Repository.

Fazit

Git bietet vielfache Möglichkeiten auch abseits des allgemein Üblichen. Wenn man sie kennt, kann man sich immer mal wieder das Leben leichter machen. Obendrein hilft die Kenntnis dieser Möglichkeiten, besser zu durchschauen, was man beim Üblichen eigentlich tut. Das verschafft das angenehme Gefühl, bei der Nutzung von Git mehr „Wasser unter den Kiel“ zu haben.

TAGS

Kommentare

Um die Kommentare zu sehen, bitte unserer Cookie Vereinbarung zustimmen. Mehr lesen