-
1. Erste Schritte
-
2. Git Grundlagen
-
3. Git Branching
- 3.1 Branches auf einen Blick
- 3.2 Einfaches Branching und Merging
- 3.3 Branch-Management
- 3.4 Branching-Workflows
- 3.5 Remote-Branches
- 3.6 Rebasing
- 3.7 Zusammenfassung
-
4. Git auf dem Server
- 4.1 Die Protokolle
- 4.2 Git auf einem Server einrichten
- 4.3 Erstellung eines SSH-Public-Keys
- 4.4 Einrichten des Servers
- 4.5 Git-Daemon
- 4.6 Smart HTTP
- 4.7 GitWeb
- 4.8 GitLab
- 4.9 Von Drittanbietern gehostete Optionen
- 4.10 Zusammenfassung
-
5. Verteiltes Git
-
6. GitHub
-
7. Git Tools
- 7.1 Revisions-Auswahl
- 7.2 Interaktives Stagen
- 7.3 Stashen und Bereinigen
- 7.4 Deine Arbeit signieren
- 7.5 Suchen
- 7.6 Den Verlauf umschreiben
- 7.7 Reset entzaubert
- 7.8 Fortgeschrittenes Merging
- 7.9 Rerere
- 7.10 Debuggen mit Git
- 7.11 Submodule
- 7.12 Bundling
- 7.13 Replace (Ersetzen)
- 7.14 Anmeldeinformationen speichern
- 7.15 Zusammenfassung
-
8. Git einrichten
- 8.1 Git Konfiguration
- 8.2 Git-Attribute
- 8.3 Git Hooks
- 8.4 Beispiel für Git-forcierte Regeln
- 8.5 Zusammenfassung
-
9. Git und andere VCS-Systeme
- 9.1 Git als Client
- 9.2 Migration zu Git
- 9.3 Zusammenfassung
-
10. Git Interna
-
A1. Anhang A: Git in anderen Umgebungen
- A1.1 Grafische Schnittstellen
- A1.2 Git in Visual Studio
- A1.3 Git in Visual Studio Code
- A1.4 Git in IntelliJ / PyCharm / WebStorm / PhpStorm / RubyMine
- A1.5 Git in Sublime Text
- A1.6 Git in Bash
- A1.7 Git in Zsh
- A1.8 Git in PowerShell
- A1.9 Zusammenfassung
-
A2. Anhang B: Git in deine Anwendungen einbetten
- A2.1 Die Git-Kommandozeile
- A2.2 Libgit2
- A2.3 JGit
- A2.4 go-git
- A2.5 Dulwich
-
A3. Anhang C: Git Kommandos
- A3.1 Setup und Konfiguration
- A3.2 Projekte importieren und erstellen
- A3.3 Einfache Snapshot-Funktionen
- A3.4 Branching und Merging
- A3.5 Projekte gemeinsam nutzen und aktualisieren
- A3.6 Kontrollieren und Vergleichen
- A3.7 Debugging
- A3.8 Patchen bzw. Fehlerkorrektur
- A3.9 E-mails
- A3.10 Externe Systeme
- A3.11 Administration
- A3.12 Basisbefehle
8.4 Git einrichten - Beispiel für Git-forcierte Regeln
Beispiel für Git-forcierte Regeln
In diesem Abschnitt wirst du das Erlernte nutzen, um einen Git-Workflow einzurichten, der ein benutzerdefiniertes Commit-Beschreibungs-Format prüft und nur bestimmten Benutzern erlaubt, ausgewählte Unterverzeichnisse in einem Projekt zu ändern. Du erstellst Client-Skripte, die den Entwickler erkennen lassen, ob ihr Push abgelehnt wird. Ebenso werden Server-Skripte erstellt, die die Einhaltung von Richtlinien erzwingen.
Die hier vorgestellten Skripte sind in Ruby geschrieben, teils wegen unserem Know-How, teils aber auch weil Ruby leicht zu lesen ist (auch wenn man es nicht schreiben kann). Allerdings kann jede beliebige Programmiersprache verwendet werden. Alle mit Git vertriebenen Beispiel-Hook-Skripte sind entweder in Perl oder Bash verfasst. Diese kannst du dir bei Bedarf anschauen und anpassen.
Serverseitiger Hook
Die gesamte serverseitige Arbeit wird in die update
Datei in deinem hooks
Verzeichnis übernommen.
Der update
Hook läuft einmal pro gepushtem Branch und benötigt drei Argumente:
-
Der Name der Referenz, zu der gepusht wird
-
Die alte Revision, in der sich dieser Branch befunden hat
-
Die neue Revision, die gepusht werden soll
Wenn der Push über SSH ausgeführt wird, hast du auch Zugriff auf den Benutzer, der den Push durchführt.
Auch wenn du es jedem erlaubt hast, sich mit einem bestimmten Benutzer (z.B. „git“) über die Public-Key-Authentifizierung zu verbinden, musst du diesem Benutzer möglicherweise einen Shell-Wrapper zur Verfügung stellen, der anhand des öffentlichen Schlüssels ermittelt, welcher Benutzer sich verbindet, und eine entsprechende Umgebungsvariable festlegen.
Hier gehen wir davon aus, dass sich der verbindende Benutzer in der Umgebungsvariablen $USER
befindet, so dass dein Update-Skript mit dem Sammeln aller benötigten Informationen beginnt:
#!/usr/bin/env ruby
$refname = ARGV[0]
$oldrev = ARGV[1]
$newrev = ARGV[2]
$user = ENV['USER']
puts "Enforcing Policies..."
puts "(#{$refname}) (#{$oldrev[0,6]}) (#{$newrev[0,6]})"
Ja, das sind globale Variablen. Verurteile es nicht – es ist auf diese Weise leichter zu demonstrieren.
Ein bestimmtes Commit-Message-Format erzwingen
Deine erste Herausforderung besteht darin, die Einhaltung eines bestimmten Formats für jede Commit-Nachricht durchzusetzen. Nur um ein Ziel zu haben, gehst du davon aus, dass jede Nachricht eine Zeichenkette enthalten muss, die wie „ref: 1234“ aussieht. Du möchtest, dass jeder Commit auf einen Arbeitsschritt in deinem Ticketing-System verweist. Du musst dir jeden Commit ansehen, der gepusht wird. Du prüfst, ob sich diese Zeichenkette in der Commit-Beschreibung befindet. Falls die Zeichenkette bei einem der Commits fehlt, wird der Push mit einem Exit-Status ungleich Null abgelehnt.
Du kannst eine Liste der SHA-1-Werte aller gepushten Commits erhalten, indem du die Werte $newrev
und $oldrev
verwendest und sie an das Git-Basiskommando (engl. Plumbing Command) git rev-list
übergibst.
Das ist im Grunde genommen der Befehl git log, der aber standardmäßig nur die SHA-1-Werte und keine anderen Informationen ausgibt.
Um also eine Liste aller Commit SHA-1-en zu erhalten, die zwischen zwei verschiedenen Commit SHA-1 liegen, kannst du in etwa so vorgehen:
$ git rev-list 538c33..d14fc7
d14fc7c847ab946ec39590d87783c69b031bdfb7
9f585da4401b0a3999e84113824d15245c13f0be
234071a1be950e2a8d078e6141f5cd20c1e61ad3
dfa04c9ef3d5197182f13fb5b9b1fb7717d2222a
17716ec0f1ff5c77eff40b7fe912f9f6cfd0e475
Du kannst diese Ausgabe nehmen, die Nachricht für jeden dieser Commit SHA-1s abgreifen und diese Nachricht gegen einen regulären Ausdruck testen, der nach einem Zeichen-Muster sucht.
Du musst dir überlegen, wie du die Commit-Nachricht von jedem dieser Commits erhältst.
Um die unformatierten Commit-Daten zu ermitteln, kannst du ein anderes Basiskommando namens git cat-file
verwenden.
Wir werden alle diese Basiskommandos in Kapitel 10, Git Interna im Detail betrachten. Dieser Befehl gibt Folgendes aus:
$ git cat-file commit ca82a6
tree cfda3bf379e4f8dba8717dee55aab78aef7f4daf
parent 085bb3bcb608e1e8451d4b2432f8ecbe6306e7e7
author Scott Chacon <schacon@gmail.com> 1205815931 -0700
committer Scott Chacon <schacon@gmail.com> 1240030591 -0700
Change the version number
Eine gibt eine einfache Möglichkeit, um die Commit Nachricht mit einem SHA-1-Wert zu erhalten. Nimm dazu einfach den Text nach der ersten Leerzeile.
Auf Unix-Systemen kannst du mit dem sed
Befehl arbeiten:
$ git cat-file commit ca82a6 | sed '1,/^$/d'
Change the version number
Du kannst auf diese Weise die Commit-Nachricht von jedem Commit, der gepusht werden soll, übernehmen und mit einem Exit-Code ungleich Null beenden, wenn du etwas findest, das nicht mit deinen Regeln übereinstimmt. Um das Skript zu verlassen und den Push abzulehnen, gib einen Rückgabewert ungleich Null aus. Die gesamte Methode sieht dann so aus:
$regex = /\[ref: (\d+)\]/
# enforced custom commit message format
def check_message_format
missed_revs = `git rev-list #{$oldrev}..#{$newrev}`.split("\n")
missed_revs.each do |rev|
message = `git cat-file commit #{rev} | sed '1,/^$/d'`
if !$regex.match(message)
puts "[POLICY] Your message is not formatted correctly"
exit 1
end
end
end
check_message_format
Wenn du das in dein update
Skript einfügst, werden Updates mit Commit-Nachrichten, die nicht deinen Regel entsprechen, abgelehnt.
Ein benutzerbasiertes ACL-System einrichten
Angenommen, du möchtest einen Prozess hinzufügen, der eine Zugriffskontrollliste (ACL) verwendet, die festlegt, welche Benutzer Änderungen an welchen Teilen deines Projekte vornehmen dürfen.
Einige Personen haben vollen Zugriff, andere können Änderungen nur zu bestimmten Unterverzeichnissen oder bestimmten Dateien pushen.
Um das zu erreichen, musst du diese Regeln in eine acl
Datei schreiben, die in deinem Git-Repository auf dem Server liegt.
Mit dem update
Hook kannst du diese Regeln prüfen. So kannst du feststellen, welche Dateien für die zu übertragenden Commits eingeführt werden und entscheiden, ob der Benutzer, der den Push durchführt, Zugriff hat, um diese Dateien zu aktualisieren.
Zuerst musst du deine ACL-Datei erstellen.
Hier verwendest du ein Format ähnlich dem CVS ACL-Mechanismus: Es verwendet eine Reihe von Zeilen, wobei das erste Feld avail
(verfügbar) oder unavail
(nicht verfügbar) ist. Das nächste Feld ist eine kommagetrennte Liste der gültigen Benutzer und das letzte Feld ist der Pfad, für den die Regel gilt (blank bedeutet Open Access).
Alle diese Felder werden durch ein Pipe-Zeichen (|
) getrennt.
In diesem Beispiel hast du ein Reihe von Administratoren, einige Dokumentations-Autoren mit Zugriff auf das doc
Verzeichnis und einen Entwickler, der nur Zugriff auf die Verzeichnisse lib
und tests
hat. deine ACL-Datei sieht dann wie folgt aus:
avail|nickh,pjhyett,defunkt,tpw
avail|usinclair,cdickens,ebronte|doc
avail|schacon|lib
avail|schacon|tests
Du beginnst damit, diese Daten in eine Struktur einzulesen, die du weiterverwenden kannst.
In diesem Beispiel wirst du, um das Beispiel einfach zu halten, nur die avail
Anweisungen einführen.
Hier folgt eine Methode, die dir ein assoziatives Array liefert, wobei der Schlüssel der Benutzername und der Wert ein Array von Pfaden ist, auf die der Benutzer Schreibzugriff hat:
def get_acl_access_data(acl_file)
# read in ACL data
acl_file = File.read(acl_file).split("\n").reject { |line| line == '' }
access = {}
acl_file.each do |line|
avail, users, path = line.split('|')
next unless avail == 'avail'
users.split(',').each do |user|
access[user] ||= []
access[user] << path
end
end
access
end
In der zuvor angesehenen ACL-Datei gibt die get_acl_access_data
Methode eine Datenstruktur zurück, die wie folgt aussieht:
{"defunkt"=>[nil],
"tpw"=>[nil],
"nickh"=>[nil],
"pjhyett"=>[nil],
"schacon"=>["lib", "tests"],
"cdickens"=>["doc"],
"usinclair"=>["doc"],
"ebronte"=>["doc"]}
Nachdem du nun die Berechtigungen geklärt hast, musst du ermitteln, welche Pfade die gepushten Commits geändert haben. So kannst du sicherstellen, dass der Benutzer, der gepusht hat, Zugriff auf alle diese Pfade erhält.
Mit der Option --name-only
des git log
Befehls kannst du relativ einfach sehen, welche Dateien in einem einzelnen Commit geändert wurden (wurde in Kapitel 2, Git Grundlagen kurz erwähnt):
$ git log -1 --name-only --pretty=format:'' 9f585d
README
lib/test.rb
Wenn du die verwendete ACL-Struktur, die von der get_acl_access_data
Methode zurückgegeben wird, mit den aufgelisteten Dateien in jedem der Commits vergleichst, kannst du feststellen, ob der Benutzer die Berechtigung hat, um alle seine Commits zu pushen:
# only allows certain users to modify certain subdirectories in a project
def check_directory_perms
access = get_acl_access_data('acl')
# see if anyone is trying to push something they can't
new_commits = `git rev-list #{$oldrev}..#{$newrev}`.split("\n")
new_commits.each do |rev|
files_modified = `git log -1 --name-only --pretty=format:'' #{rev}`.split("\n")
files_modified.each do |path|
next if path.size == 0
has_file_access = false
access[$user].each do |access_path|
if !access_path # user has access to everything
|| (path.start_with? access_path) # access to this path
has_file_access = true
end
end
if !has_file_access
puts "[POLICY] You do not have access to push to #{path}"
exit 1
end
end
end
end
check_directory_perms
Du erhältst eine Liste der neuen Commits, die mit git rev-list
auf deinen Server gepusht werden.
Dann stellst du für jeden dieser Commits fest, welche der Dateien geändert werden sollen und stellst sicher, dass der Benutzer, der den Push ausführt, Zugriff auf alle zu ändernden Pfade hat.
Jetzt können deine Benutzer keine Commits mit unstrukturierten Nachrichten oder Dateien außerhalb der erlaubten Pfade pushen.
Austesten
Wenn du chmod u+x.git/hooks/update
ausführst, welches die Datei ist in die du deinen Code einfügen solltest, und dann versuchst, ein Commit mit einer nicht konformen Beschreibung zu pushen, dann erhältst du wahrscheinlich das hier:
$ git push -f origin master
Counting objects: 5, done.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 323 bytes, done.
Total 3 (delta 1), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
Enforcing Policies...
(refs/heads/master) (8338c5) (c5b616)
[POLICY] Your message is not formatted correctly
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master
To git@gitserver:project.git
! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'
Hier sind noch ein paar interessante Details zu finden. Erstens, du siehst, so der Hook gestartet ist.
Enforcing Policies...
(refs/heads/master) (fb8c72) (c56860)
Denke daran, dass das ganz am Anfang deines Update-Skripts ausgegeben hast.
Alles, was dein Skript an stdout
weitergibt, wird an den Client übertragen.
Das nächste, was du beachten solltest, ist die Fehlermeldung.
[POLICY] Your message is not formatted correctly
error: hooks/update exited with error code 1
error: hook declined to update refs/heads/master
Die erste Zeile wurde von dir, die anderen beiden wurden von Git ausgegeben. Die teilt dir mit, dass das Update-Skript ungleich Null beendet wurde. Das ist der Grund, warum dein Push abgelehnt wurde. Zum Schluss noch Folgendes:
To git@gitserver:project.git
! [remote rejected] master -> master (hook declined)
error: failed to push some refs to 'git@gitserver:project.git'
Du erhältst eine Remote-Ablehnungs-Nachricht für jede Referenz, die von deinem Hook abgelehnt wurde. Sie zeigt dir an, dass sie genau wegen diesem Hook-Fehlers abgelehnt wurde.
Wenn außerdem jemand versucht, eine Datei, auf die er keinen Zugriff hat, zu bearbeiten und einen Commit damit pusht, dann wird er etwas Ähnliches sehen.
Versucht ein Dokumentations-Author zum Beispiel, einen Commit zu pushen, indem er etwas im lib
Verzeichnis ändert, wird ihm folgendes angezeigt:
[POLICY] You do not have access to push to lib/test.rb
Von jetzt an, solange dieses update
Skript verfügbar und ausführbar ist, wird dein Repository nie eine Commit-Beschreibung ohne dein eigenes Schema haben, und deine Benutzer werden in einer „Sandbox“ untergebracht sein.
Clientseitige Hooks
Der Nachteil dieses Konzepts ist das Gejammer, das unweigerlich entsteht, wenn die Commit-Pushes deiner Benutzer abgelehnt werden. Die Tatsache, dass die sorgfältig gestalteten Arbeiten in letzter Minute abgelehnt werden, kann äußerst frustrierend und irritierend sein. Darüber hinaus müssen sie ihre Verlaufsdaten bearbeiten, um sie zu korrigieren, was nicht unbedingt etwas für schwache Nerven ist.
Die Antwort auf dieses Dilemma ist, einige clientseitige Hooks bereitzustellen, die Benutzer ausführen können, um sie darüber zu informieren, dass sie etwas unternehmen, das der Server wahrscheinlich ablehnen wird.
Auf diese Weise können sie mögliche Probleme vor dem Commit klären, bevor es schwieriger wird sie zu beheben.
Da Hooks nicht mit einem Klon eines Projekts übertragen werden, musst du diese Skripte auf andere Weise bereitstellen. Dann müssen deine Benutzer sie in ihr .git/hooks
Verzeichnis kopieren und sie ausführbar machen.
Du kannst diese Hooks innerhalb des Projekts oder in einem separaten Projekt weitergeben, aber Git wird sie nicht automatisch einrichten.
Am Anfang solltest du deine Commit-Beschreibung kurz vor jeder Übertragung überprüfen, damit du dir sicher bist, dass der Server deine Änderungen nicht aufgrund schlecht formatierter Commit-Beschreibungen ablehnt.
Dazu kannst du den commit-msg
Hook hinzufügen.
Wenn du die Nachricht aus der als erstes Argument übergebenen Datei liest und mit dem Patternmuster vergleichst, kannst du Git zwingen, die Übertragung abzubrechen, wenn es keine Übereinstimmung gibt:
#!/usr/bin/env ruby
message_file = ARGV[0]
message = File.read(message_file)
$regex = /\[ref: (\d+)\]/
if !$regex.match(message)
puts "[POLICY] Your message is not formatted correctly"
exit 1
end
Wenn dieses Skript (in .git/hooks/commit-msg
) vorhanden und ausführbar ist und du mit einer Nachricht committest, die nicht korrekt formatiert ist, siehst du das hier:
$ git commit -am 'Test'
[POLICY] Your message is not formatted correctly
In diesem Fall wurde kein Commit durchgeführt. Wenn deine Nachricht jedoch das richtige Muster enthält, erlaubt dir Git die Übertragung:
$ git commit -am 'Test [ref: 132]'
[master e05c914] Test [ref: 132]
1 file changed, 1 insertions(+), 0 deletions(-)
Als nächstes solltest du sicherstellen, dass du keine Dateien änderst, die sich außerhalb deines ACL-Bereichs befinden.
Wenn das .git
Verzeichnis deines Projekts eine Kopie der ACL-Datei enthält, die du zuvor verwendet hast, dann wird das folgende pre-commit
Skript diese Einschränkungen für dich erzwingen:
#!/usr/bin/env ruby
$user = ENV['USER']
# [ insert acl_access_data method from above ]
# only allows certain users to modify certain subdirectories in a project
def check_directory_perms
access = get_acl_access_data('.git/acl')
files_modified = `git diff-index --cached --name-only HEAD`.split("\n")
files_modified.each do |path|
next if path.size == 0
has_file_access = false
access[$user].each do |access_path|
if !access_path || (path.index(access_path) == 0)
has_file_access = true
end
if !has_file_access
puts "[POLICY] You do not have access to push to #{path}"
exit 1
end
end
end
check_directory_perms
Das ist ungefähr das gleiche Skript wie der serverseitige Teil, allerdings mit zwei wichtigen Unterschieden.
Erstens befindet sich die ACL-Datei an einer anderen Stelle, da dieses Skript aus deinem Arbeitsverzeichnis und nicht aus deinem .git
Verzeichnis läuft.
Du musst den Pfad zur ACL-Datei ändern, von:
access = get_acl_access_data('acl')
zu:
access = get_acl_access_data('.git/acl')
Der andere wichtige Unterschied ist die Art und Weise, wie du eine Liste der Dateien erhältst, die geändert wurden. Da die serverseitige Methode das Log des Commits betrachtet und der Commit an dieser Stelle noch nicht aufgezeichnet wurde, musst du stattdessen deine Dateiliste aus der Staging-Area holen. Anstelle von:
files_modified = `git log -1 --name-only --pretty=format:'' #{ref}`
musst du Folgendes benutzen:
files_modified = `git diff-index --cached --name-only HEAD`
Aber das sind die einzigen beiden Unterschiede – ansonsten funktioniert das Skript wie gehabt.
Ein Nachteil ist, dass es erwartet, dass du lokal mit dem gleichen Benutzer arbeitest, den du auf dem Remotesystem verwendest.
Wenn das anders ist, musst du die Variable $user
manuell setzen.
Außerdem können wir hier sicherstellen, dass der Benutzer keine „non-fast-forwarded“ Referenzen pusht. Um eine Referenz zu erhalten, die kein „fast-forward“ ist, musst du entweder über einen Commit hinaus rebasen, den du bereits hochgeladen hast oder versuchen, einen anderen lokalen Branch auf den gleichen Remote-Branch zu pushen.
Wahrscheinlich ist der Server bereits mit receive.denyDeletes
und receive.denyNonFastForwards
konfiguriert, um diese Richtlinie zu erzwingen. Somit ist die einzige unbeabsichtigte Aktion, die du abfangen kannst ein Rebase-Commit, welcher bereits gepusht wurde.
Hier folgt ein Beispiel für ein Pre-Rebase-Skript, das das überprüft. Es erhält eine Liste aller Commits, die du gerade neu schreiben willst, und prüft, ob sie in einer deiner Remote-Referenzen vorhanden sind. Wenn es einen findet, der von einer deinen Remote-Referenzen aus erreichbar ist, bricht es den Rebase ab.
#!/usr/bin/env ruby
base_branch = ARGV[0]
if ARGV[1]
topic_branch = ARGV[1]
else
topic_branch = "HEAD"
end
target_shas = `git rev-list #{base_branch}..#{topic_branch}`.split("\n")
remote_refs = `git branch -r`.split("\n").map { |r| r.strip }
target_shas.each do |sha|
remote_refs.each do |remote_ref|
shas_pushed = `git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
if shas_pushed.split("\n").include?(sha)
puts "[POLICY] Commit #{sha} has already been pushed to #{remote_ref}"
exit 1
end
end
end
Dieses Skript verwendet eine Syntax, die in Kapitel 7, Revisions-Auswahl nicht behandelt wurde. Du erhältst eine Liste der Commits, die bereits gepusht wurden, wenn du diese Anweisung aufrufst:
`git rev-list ^#{sha}^@ refs/remotes/#{remote_ref}`
Die SHA^@
Syntax löst alle Vorgänger (engl. parents) dieses Commits auf.
Du suchst nach jedem Commit, der vom letzten Commit auf dem Remote aus erreichbar ist und der von keinem Parent einer der SHA-1s, die du hochladen möchten, erreichbar ist – was bedeutet, dass es ein „fast-forward“ ist.
Der größte Nachteil dieses Ansatzes ist, dass er sehr langsam sein kann und oft unnötig ist. Wenn du nicht versuchst, den Push mit -f
zu erzwingen, wird der Server dich warnen und den Push nicht akzeptieren.
Es ist aber ein interessanter Test und kann dir theoretisch helfen, einen Rebase zu vermeiden, den du später vielleicht wieder zurücknehmen und reparieren musst.