forked from gitbuch/gitbuch_cc
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathautomatisierung.txt
1919 lines (1508 loc) · 69.3 KB
/
automatisierung.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
[[ch.scripting]]
== Git automatisieren ==
In diesem Kapitel stellen wir fortgeschrittene Techniken zum
Automatisieren von Git vor. Im ersten Abschnitt über
'Git-Attribute' zeigen wir Ihnen, wie Sie Git anweisen, bestimmte
Dateien gesondert zu behandeln, zum Beispiel um bei Grafiken ein
externes Diff-Kommando aufzurufen.
Weiter geht es mit 'Hooks' -- kleine Scripte, die beim Aufruf
verschiedener Git-Kommandos ausgeführt werden, beispielsweise um alle
Entwickler per E-Mail zu benachrichtigen, wenn neue Commits im
Repository eintreffen.
Danach geben wir eine grundlegende Einführung ins Scripting mit
Git und zeigen Ihnen nützliche 'Plumbing-Kommandos'.
Zum Abschluss stellen wir das mächtige `filter-branch`-Kommando
vor, mit dem Sie die Projektgeschichte im großen Stil umschreiben,
etwa um eine Datei mit einem Passwort aus 'allen' Commits zu
entfernen.
[[sec.attributes]]
=== Git-Attribute – Dateien gesondert behandeln ===
Über 'Git-Attribute' können Sie einzelnen oder einer Gruppe von
Dateien bestimmte Eigenschaften zuweisen, so dass Git sie besonders
behandelt; Beispiele wären, für bestimmte Dateien das Zeilenende zu
forcieren oder sie als binär zu markieren.
Die Attribute schreiben Sie wahlweise in die Datei
`.gitattributes` oder `.git/info/attributes`.
Letztere gilt für ein Repository und wird nicht von Git verwaltet.
Eine Datei `.gitattributes` wird in der Regel eingecheckt, so
dass alle Entwickler diese Attribute verwenden. Außerdem können Sie in
Unterverzeichnissen weitere Attribut-Definitionen hinterlegen.
Eine Zeile in dieser Datei hat das Format:
--------
<muster> <attrib1> <attrib2> ...
--------
Ein Beispiel:
--------
*.eps binary
*.tex -text
*.c filter=indent
--------
In der Regel können Attribute gesetzt (z.B.{empty}{nbsp}`binary`),
aufgehoben (`-text`) oder auf einen Wert gesetzt werden
(`filter=indent`). Die Man-Page
`gitattributes(5)` beschreibt detailliert, wie Git die
Attribute interpretiert.
Ein Projekt, das parallel auf Windows- und Unix-Rechnern entwickelt
wird, leidet darunter, dass die Entwickler verschiedene Konventionen
für Zeilenenden verwenden. Dies ist bedingt durch das Betriebssystem:
Windows-Systeme verwenden einen Carriage Return, gefolgt von einem
Line Feed (CRLF), während unixoide Systeme nur einen Line Feed (LF)
verwenden.
Über geeignete Git-Attribute bestimmen Sie eine adäquate Policy -- in
diesem Fall sind die Attribute `text` bzw. `eol`
zuständig. Das Attribut `text` bewirkt, dass die Zeilenenden
``normalisiert'' werden. Egal, ob der Editor eines Entwicklers
CRLF oder nur LF verwendet, Git wird im Blob nur die Version mit LF
speichern. Setzen Sie das Attribut auf `auto`, wird Git
diese Normalisierung nur ausführen, wenn die Datei auch wie Text
aussieht.
Das Attribut `eol` hingegen bestimmt, was bei einem Checkout
passiert. Unabhängig von der Einstellung `core.eol` des Nutzers
können Sie so für einige Dateien z.B. CRLF vorgeben (weil das Format
dies benötigt).
--------
*.txt text
*.csv eol=crlf
--------
Mit diesen Attributen werden `.txt`-Dateien intern immer mit LF
gespeichert und bei Bedarf (plattform- bzw. nutzerabhängig) als CRLF
ausgecheckt. CSV-Dateien hingegen werden auf allen Plattformen mit
CRLF ausgecheckt. (Intern wird Git all diese Blobs mit einfachen
LF-Endungen speichern.)
[[sec.smudge-clean]]
==== Filter: Smudge und Clean ====
Git bietet über 'Filter' eine Möglichkeit, Dateien nach einem
Checkout zu ``verschmutzen'' ('smudge') und vor einem
`git add` wieder zu ``säubern'' ('clean').
Die Filter erhalten keine Argumente, sondern nur den Inhalt des Blobs
auf Standard-In. Die Ausgabe des Programms wird als neuer Blob
verwendet.
Für einen Filter müssen Sie jeweils ein Smudge- und ein Clean-Kommando
definieren. Fehlt eine der Definitionen oder ist der Filter
`cat`, wird der Blob unverändert übernommen.
Welcher Filter für welche Art von Dateien verwendet wird, definieren
Sie über das Git-Attribut `filter`. Um beispielsweise C-Dateien
vor einem Commit automatisch richtig einzurücken, können Sie folgende
Filterdefinitionen verwenden (statt `<indent>` sind
beliebige andere Namen möglich):
[subs="macros,quotes"]
--------
$ *git config filter.<indent>.clean indent*
$ *git config filter.<indent>.smudge cat*
$ *echo '*.c filter=<indent>' > .git/info/attributes*
--------
Um eine C-Datei zu ``säubern'', ruft Git nun automatisch das
Programm `indent` auf, das auf Standardsystemen installiert
sein sollte.footnote:[Sie können das Programm
`indent` aus dem GNU-Projekt von
http://www.gnu.org/software/indent/ herunterladen.]
[[sec.smudge-clean-keywords]]
==== Keywords in Dateien ====
So lassen sich prinzipiell auch die bekannten Keyword-Expansionen
realisieren, so dass z.B. `$Version$` zu `$Version:
v1.5.4-rc2$` wird.
Sie definieren die Filter in Ihrer Konfiguration und statten dann
entsprechende Dateien mit diesem Git-Attribut aus. Das geht z.B. so:
[subs="macros,quotes"]
--------
$ *git config filter.version.smudge \~/bin/git-version.smudge*
$ *git config filter.version.clean ~/bin/git-version.clean*
$ *echo '* filter=version' > .git/info/attributes*
--------
Ein Filter, der das `$Version$`-Keyword ersetzt bzw. wieder
aufräumt, könnte als Perl-Einzeiler realisiert werden; zunächst der
Smudge-Filter:
//\label{code:smudge}
--------
#!/bin/sh
version=`git describe --tags`
exec perl -pe 's/$Version(:\s[^$]+)?$/$Version: '"$version"'$/g'
--------
Und der Clean-Filter:
//\label{code:clean}
--------
#!/usr/bin/perl -p
s/$Version: [^$]+$/$Version$/g
--------
Wichtig ist, dass mehrmalige Anwendung eines solchen Filters keine
unkontrollierten Veränderungen in der Datei vornimmt. Ein doppelter
Aufruf von Smudge sollte durch einen einfachen Aufruf von Clean wieder
behoben werden können.
[[sec.smudge-clean-dontuse]]
===== Einschränkungen =====
Das Konzept von Filtern in Git ist bewusst simpel gehalten und wird
auch in künftigen Versionen nicht erweitert werden. Die Filter
erhalten 'keine' Informationen über den Kontext, in dem sich Git
gerade befindet: Passiert ein Checkout? Ein Merge? Ein Diff? Sie
erhalten lediglich den Blob-Inhalt. Die Filter sollen also nur
'kontextunabhängige' Manipulationen durchführen.
Zum Zeitpunkt, da Smudge aufgerufen wird, ist der `HEAD`
möglicherweise noch nicht auf dem aktuellen Stand (der obige Filter
würde bei einem `git checkout` eine falsche Versionsnummer in
die Datei schreiben, da er 'vor' dem Versetzen des `HEAD`
aufgerufen wird). Die Filter eignen sich also nur bedingt zur
Keyword-Expansion.
Das mag zwar Nutzer, die sich an dieses Feature in anderen
Versionskontrollsystemen gewöhnt haben, verärgern. Es gibt allerdings
keine guten Argumente, 'innerhalb' eines Versionskontrollsystems
eine solche Expansion durchzuführen. Die internen Mechanismen, die Git
verwendet, um zu überprüfen, ob Dateien verändert wurden, werden
lahmgelegt (da sie immer durch den Clean-Filter geschickt werden
müssen). Außerdem kann man aufgrund der Struktur von Git-Repositories
einen Blob durch die Commits bzw. Trees hindurch
``verfolgen'', kann also bei Bedarf die Zugehörigkeit einer
Datei zu einem Commit immer an ihrem Inhalt erkennen.
Eine Keyword-Expansion ist also nur 'außerhalb' von Git sinnvoll.
Dafür ist dann aber nicht Git zuständig, sondern ein entsprechendes
`Makefile`-Target oder ein Script. So kann beispielsweise ein
`make dist` alle Vorkommen von `VERSION` durch die
Ausgabe von `git describe --tags` ersetzen. Git wird die
Dateien als ``geändert'' anzeigen. Sobald die Dateien verteilt
sind (z.B. als Tarball), kann mit `git reset --hard` wieder
aufgeräumt werden.
//\label{sec:export-subst}
Alternativ sorgt das Attribut `export-subst` dafür, dass eine
Expansion der Form `$Format:<Pretty>$` durchgeführt wird.
Dabei muss `<Pretty>` ein Format sein, das für `git log
--pretty=format:<Pretty>` gültig ist, also z.B.{empty}{nbsp}`%h` für die
gekürzte Commit-Hash-Summe. Git expandiert diese Attribute nur, wenn
die Datei per `git archive`
(siehe <<sec.release-create>>) verpackt wird.
[[sec.external-diff]]
==== Eigene Diff-Programme ====
Der interne Diff-Mechanismus von Git eignet sich sehr gut für alle
Arten von Plaintext. Er versagt aber bei Binärdateien -- Git gibt
lediglich aus, ob sie sich unterscheiden oder nicht. Wenn Sie
allerdings ein Projekt haben, in dem Sie Binärdaten verwalten müssen,
wie z.B. PDF-, OpenOffice-Dokumente oder Bilder, dann ist es
sinnvoll, ein spezielles Programm zu definieren, das sinnvolle Diffs
dieser Dateien erstellt.
So gibt es beispielsweise `antiword` und `pdftotext`, um
Word-Dokumente und PDFs nach Plaintext zu konvertieren. Für
OpenOffice-Formate gibt es analoge Scripte. Bei Bildern können Sie
Kommandos aus der ImageMagick-Suite verwenden (siehe auch das Beispiel
weiter unten). Wenn Sie statistische Daten verwalten, können Sie die
geänderten Datensets nebeneinander plotten. Je nach Beschaffenheit
der Daten gibt es meist adäquate Möglichkeiten, Veränderungen zu
visualisieren.
Solche Konvertierungsprozesse sind natürlich verlustbehaftet: Sie
können diese Diff-Ausgabe nicht nutzen, um beispielsweise in einem
Merge-Konflikt sinnvoll Änderungen in den Dateien vorzunehmen. Aber um
einen schnellen Überblick zu erhalten, wer was geändert hat, reichen
solche Techniken allemal aus.
[[sec.external-diff-parameters]]
===== API für externe Diff-Programme =====
Git bietet eine simple API für eigene Diff-Filter. Einem Diff-Filter
werden immer die folgenden sieben Argumente übergeben:
. Pfad (Name der Datei im Git-Repository)
. alte Version der Datei
. alte SHA-1-ID des Blobs
. alte Unix-Rechte
. neue Version der Datei
. neue SHA-1-ID des Blobs
. neue Unix-Rechte
Die Argumente 2 und 5 sind möglicherweise temporäre Dateien, die
gelöscht werden, sobald sich das Diff-Programm wieder beendet; Sie
müssen sich also nicht um das Aufräumen kümmern.
Wenn eine der beiden Dateien nicht existiert (neu hinzugefügt oder
gelöscht), dann wird `/dev/null` als Dateiname übergeben. Der
entsprechende Blob ist dann `00000`..., auch in dem Fall,
dass eine Datei noch nicht als festes Objekt in der
Objektdatenbank liegt (also nur im Working Tree oder Index). Diese
Fälle muss das Diff-Kommando entsprechend behandeln können.
[[sec.diff-config]]
===== Externe Diffs konfigurieren =====
Es gibt zwei Möglichkeiten, ein externes Diff-Programm aufzurufen. Die
erste Methode ist temporär: Setzen Sie einfach vor dem Aufruf von
`git diff` die Umgebungsvariable `GIT_EXTERNAL_DIFF`
auf den Pfad zu Ihrem Programm:
[subs="macros,quotes"]
--------
$ *GIT_EXTERNAL_DIFF=</pfad/zum/diff-kommando> git diff HEAD^*
--------
Die andere Möglichkeit ist persistent, erfordert aber ein wenig
Konfiguration. Zunächst definieren Sie ein eigenes Diff-Kommando
`<name>`:
[subs="macros,quotes"]
--------
$ *git config diff.<name>.command </pfad/zum/diff-kommando>*
--------
Das Kommando muss mit den oben erwähnten sieben Argumenten umgehen
können. Nun müssen Sie über das Git-Attribut `diff` definieren,
welches Diff-Programm aufgerufen wird. Schreiben Sie dazu
z.B. folgende Zeilen in die Datei `.gitattributes`:
--------
*.jpg diff=imgdiff
*.pdf diff=pdfdiff
--------
Wenn Sie die Datei einchecken, müssen andere Nutzer auch entsprechende
Kommandos für `imgdiff` bzw. `pdfdiff` gesetzt haben,
sonst sehen sie die reguläre Ausgabe. Wollen Sie diese Einstellung nur
für ein Repository vorgeben, schreiben Sie diese Informationen nach
`.git/info/attributes`.
[[sec.diff-immages]]
===== Bilder vergleichen =====
Ein häufiger Anwendungsfall sind Bilder: Was hat sich zwischen zwei
Versionen eines Bildes geändert? Das zu visualisieren, ist nicht immer
einfach. Das Tool `compare` aus der ImageMagick-Suite markiert
für Bilder gleicher Größe die Stellen, die sich geändert haben. Auch
kann man die beiden Bilder hintereinander animieren und durch das
``Flackern'' erkennen, wo das Bild geändert wurde.
Stattdessen wollen wir ein Programm, das die beiden Bilder
gegenüberstellt. Zwischen den beiden Bildern wird eine Art
``Differenz'' dargestellt: Alle Bereiche, in denen Änderungen
aufgetreten sind, werden aus dem 'neuen' Bild auf weißen
Untergrund kopiert. Das Diff zeigt also, welche Bereiche hinzugekommen
sind.
Dafür speichern wir folgendes Script unter
`$HOME/bin/imgdiff`:footnote:[Das Kommando `convert` ist
Teil der ImageMagick-Suite. Wenn Sie `-clone 1-2` durch
`-clone 0,2` ersetzen, werden die unterschiedlichen Bereiche
aus dem 'alten' Bild kopiert.]
--------
#!/bin/sh
OLD="$2"
NEW="$5"
# "xc:none" ist "Nichts", entspricht einem fehlenden Bild
[ "$OLD" = "/dev/null" ] && OLD="xc:none"
[ "$NEW" = "/dev/null" ] && NEW="xc:none"
exec convert "$OLD" "$NEW" -alpha off \
\( -clone 0-1 -compose difference -composite -threshold 0 \) \
\( -clone 1-2 -compose copy_opacity -composite \
-compose over -background white -flatten \) \
-delete 2 -swap 1,2 +append \
-background white -flatten x:
--------
Zuletzt müssen wir noch das Diff-Kommando konfigurieren und dessen
Verwendung durch einen Eintrag in der Datei
`.git/info/attributes` sicherstellen.
[subs="macros,quotes"]
--------
$ *git config diff.imgdiff.command ~/bin/imgdiff*
$ *echo "*.gif diff=imgdiff" > .git/info/attributes*
--------
Als Beispiel verwenden wir die Ursprungsversionen des Tux.footnote:[Die Grafiken wurden zum Release von Kernel
2.0 von Larry Ewing erstellt und finden sich unter
http://www.isc.tamu.edu/~lewing/linux/.]
Zunächst fügen wir den schwarzweißen Tux ein:
[subs="macros,quotes"]
--------
$ *wget pass:quotes[http://www.isc.tamu.edu/~lewing/linux/sit3-bw-tran.1.gif] \*
*-Otux.gif*
$ *git add tux.gif && git commit -m "tux hinzugefügt"*
--------
Im nächsten Commit wird er durch eine eingefärbte Version ersetzt:
[subs="macros,quotes"]
--------
$ wget pass:quotes[http://www.isc.tamu.edu/~lewing/linux/sit3-bw**o**-tran.1.gif \ ]
-Otux.gif
$ *git diff*
--------
Die Ausgabe des Kommandos `git diff` ist ein Fenster mit
folgendem Inhalt: Links die alte, rechts die neue Version, und
in der Mitte eine Maske derjenigen Teile des neuen Bildes, die anders
als das alte sind.
.Die Ausgabe von `git diff` mit dem eigenen Diff-Programm `imgdiff`
image::bilder_ebook/tux-diff.png[id="fig.tux-diff",width="90%"]
Das Beispiel mit dem Tux inkl. Anleitung finden Sie auch in einem Repository
unter: https://github.com/gitbuch/tux-diff-demo.
[[sec.hooks]]
=== Hooks ===
Hooks bieten einen Mechanismus, in wichtige Git-Kommandos
``einzuhaken'' und eigene Aktionen auszuführen. In der Regel
sind Hooks daher kleine Shell-Scripte, um automatisierte Aufgaben zu
erfüllen, wie z.B. E-Mails zu versenden, sobald neue Commits
hochgeladen werden, oder vor einem Commit auf Whitespace-Fehler zu
überprüfen und ggf. eine Warnung auszugeben.
Damit Hooks von Git ausgeführt werden, müssen sie im Verzeichnis
`hooks/` im Git-Verzeichnis liegen, also unter
`.git/hooks/` bzw. unter `hooks/` auf
oberster Ebene bei Bare Repositories. Zudem müssen sie ausführbar
sein.
Git installiert bei einem `git init` automatisch Beispiel-Hooks, diese
tragen aber die Endung `<hook>.sample` und werden daher ohne das Zutun des Nutzers
(Umbenennung der Dateien) nicht ausgeführt.
Einen mitgelieferten Hook aktivieren Sie also z.B. so:
[subs="macros,quotes"]
--------
$ *mv .git/hooks/commit-msg.sample .git/hooks/commit-msg*
--------
Hooks kommen in zwei Klassen: solche, die lokal ausgeführt werden
(Commit-Nachrichten bzw. Patches überprüfen, Aktionen nach einem
Merge oder Checkout ausführen etc.), und solche, die
serverseitig ausgeführt werden, wenn Sie Änderungen per `git
push` veröffentlichen.footnote:[``Serverseitig'' heißt hier nur,
dass sie nicht im lokalen Repository ausgeführt werden, sondern auf
der ``Gegenseite''.]
Hooks, deren Name mit `pre-` beginnt, können häufig dazu
benutzt werden, zu entscheiden, ob eine Aktion ausgeführt wird oder
nicht. Beendet sich ein `pre`-Hook nicht erfolgreich (d.h.
mit einem Exit-Status ungleich Null), wird die Aktion
abgebrochen. Eine technische Dokumentation der Funktionsweise finden
Sie in der Man-Page `githooks(5)`.
[[sec.hooks-commit]]
==== Commits ====
`pre-commit`:: Wird aufgerufen, bevor die Commit-Nachricht abgefragt
wird. Beendet sich der Hook mit einem Wert ungleich Null, wird der
Commit-Vorgang abgebrochen. Der per Default installierte Hook
überprüft, ob eine neu hinzugefügte Datei Nicht-ASCII-Zeichen im
Dateinamen trägt, und ob in den geänderten Dateien Whitespace-Fehler vorhanden
sind. Mit der Option `-n` bzw. `--no-verify` überspringt `git commit`
diesen Hook.
`prepare-commit-msg`:: Wird ausgeführt, direkt bevor die Nachricht in
einem Editor angezeigt wird. Erhält bis zu drei Parameter, von denen
der erste die Datei ist, in der die Commit-Nachricht gespeichert ist,
so dass sie editiert werden kann. Der Hook kann z.B. automatisiert
Zeilen hinzufügen. Ein Exit-Status ungleich Null bricht den
Commit-Vorgang ab. Dieser Hook kann allerdings nicht übersprungen
werden und sollte daher nicht die Funktionalität von `pre-commit`
duplizieren oder ersetzen.
`commit-msg`:: Wird ausgeführt, nachdem die Commit-Nachricht
eingegeben wurde. Das einzige Argument ist die Datei, in der die
Nachricht gespeichert ist, so dass sie modifiziert werden kann
(Normalisierung). Dieser Hook kann durch `-n` bzw. `--no-verify`
übersprungen werden; beendet er sich nicht erfolgreich, bricht der
Commit-Vorgang ab.
`post-commit`:: Wird aufgerufen, nachdem ein Commit erstellt wurde.
Diese Hooks agieren nur lokal und dienen dazu, bestimmte Richtlinien
bezüglich der Commits bzw. der Commit-Nachrichten durchzusetzen.
Besonders der `pre-commit`-Hook ist dabei hilfreich. Zum
Beispiel zeigen manche Editoren nicht adäquat an, wenn am Ende der
Zeile Leerzeichen sind oder Leerzeilen Leerzeichen enthalten. Das ist
wiederum störend, wenn andere Entwickler neben regulären Änderungen
auch noch Whitespace aufräumen müssen. Hier hilft Git mit folgendem
Kommando:
[subs="macros,quotes"]
--------
$ *git diff --cached --check*
hooks.tex:82: trailing whitespace.
*+* auch noch Whitespace aufräumen müssen._
--------
Die Option `--check` lässt `git diff` überprüfen, ob
solche Whitespace-Fehler vorliegen, und beendet sich nur erfolgreich,
wenn die Änderungen fehlerfrei sind. Schreiben Sie dieses Kommando in
Ihren `pre-commit`-Hook, werden Sie immer gewarnt, wenn Sie
Whitespace-Fehler einchecken wollen. Sind Sie ganz sicher, können Sie
den Hook einfach temporär per `git commit -n` aussetzen.
Ganz analog können Sie auch für eine Scriptsprache Ihrer Wahl das
``Syntax überprüfen''-Kommando in diesem Hook speichern. So
zum Beispiel folgender Block für Perl-Scripte:
--------
git diff --diff-filter=MA --cached --name-only |
while read file; do
if [ -f $file ] && [ $(head -n 1 $file) = "#!/usr/bin/perl" ]; then
perl -c $file || exit 1
fi
done
true
--------
Die Namen aller im Index veränderten Dateien (Diff-Filter
`modified` und `added`, siehe auch
<<sec.scripting-find-changes>>) werden
an eine Subshell weitergeleitet, die pro Datei überprüft, ob die erste
Zeile ein Perl-Script ist. Wenn ja, wird die Datei mit `perl
-c` überprüft. Falls sich ein Syntaxfehler in der Datei befindet,
gibt das Kommando eine entsprechende Fehlermeldung aus, und das
`exit 1` beendet den Hook, so dass Git den Commit-Vorgang
abbricht, noch bevor ein Editor geöffnet wird, um die Commit-Nachricht
einzugeben.
Das schließende `true` wird z.B. benötigt, wenn eine
Nicht-Perl-Datei editiert wurde: Dann schlägt das If-Konstrukt fehl,
die Shell gibt den Rückgabewert des letzten Kommandos wieder, und
obwohl es nichts zu bemängeln gibt, wird Git den Commit nicht
ausführen. Durch die Zeile `true` war der Hook erfolgreich,
wenn alle Durchläufe der `while`-Schleife erfolgreich waren.
Der Hook kann natürlich vereinfacht werden, wenn man annimmt, dass
alle Perl-Dateien als `<name>.pl` vorliegen. Dann reicht
der folgende Code:
--------
git ls-files -z -- '*.pl' | xargs -z -n 1 perl -c
--------
Weil Sie im Zweifel nur die von Git verwalteten Dateien überprüfen
wollen, eignet sich hier ein `git ls-files` besser als ein
simples `ls`, denn das würde auch nicht getrackte Dateien, die
auf `.pl` enden, auflisten.
Neben der Überprüfung der Syntax können Sie natürlich auch Programme
im Stil von Lint einsetzen, die den Quellcode auf
``unschöne'' oder nicht portable Konstrukte überprüfen.
Solche Hooks sind äußerst sinnvoll, um nicht versehentlich
fehlerhaften Code einzuchecken. Sind Warnungen unangebracht, können
Sie den Hook `pre-commit` ja immer über die Option `-n`
beim Committen überspringen.
[[sec.hooks-server]]
==== Serverseitig ====
Die folgenden Hooks werden auf Empfängerseite von `git
receive-pack` aufgerufen, nachdem der Nutzer im lokalen Repository
`git push` eingegeben hat.
Für einen Push-Vorgang erstellt `git send-pack` auf der lokalen
Seite 'ein' Packfile (siehe auch <<sec.od>>), das
von `git receive-pack` auf der Empfängerseite entgegengenommen
wird. Ein solches Packfile enthält die neuen Werte einer oder mehrerer
Referenzen sowie die Commits, die das Empfänger-Repository benötigt,
um die Versionsgeschichte komplett abzubilden. Welche Commits das
sind, handeln die beiden Seiten vorher aus (ähnlich einer
Merge-Basis).
`pre-receive`:: Der Hook wird einmal aufgerufen und erhält auf
Standard-Input eine Liste der geänderten Referenzen (Format
s.u.). Wenn der Hook sich nicht erfolgreich beendet, verweigert `git
receive-pack` die Annahme (der gesamte Push-Vorgang schlägt fehl).
`update`:: Wird einmal 'pro geänderter Referenz' aufgerufen und erhält
drei Argumente: den alten Stand der Referenz, den vorgeschlagenen
neuen sowie den Namen der Referenz. Beendet sich der Hook nicht
erfolgreich, wird das Update der einzelnen Referenz verweigert (im
Gegensatz zu `pre-receive`, wo nur einem ganzen Packfile zugestimmt
werden kann oder nicht).
`post-receive`:: Analog zu `pre-receive`, aber wird erst aufgerufen,
'nachdem' die Referenzen geändert wurden (kann also keinen Einfluss
mehr nehmen, ob das Packfile angenommen wird oder nicht).
`post-update`:: Nachdem alle Referenzen geändert wurden, wird dieser
Hook einmal ausgeführt und erhält die Namen aller geänderten
Referenzen als Argumente. Der Hook bekommt aber nicht mitgeteilt, auf
welchem Stand die Referenzen vorher waren bzw. jetzt sind. (Dafür
können Sie `post-receive` verwenden.) Ein typischer Anwendungsfall ist
ein Aufruf von `git update-server-info`, der nötig ist, wenn Sie ein
Repository per HTTP anbieten wollen.
[[sec.hooks-receive-format]]
===== Das Format der receive-Hooks =====
Die Hooks `pre-receive` und `post-receive` erhalten
eine äquivalente Eingabe auf Standard-Input. Das Format ist das
folgende:
--------
<alte-sha1> <neue-sha1> <name-der-referenz>
--------
Das kann zum Beispiel so aussehen:
--------
0000000...0000000 ca0e8cf...12b14dc refs/heads/newbranch
ca0e8cf...12b14dc 0000000...0000000 refs/heads/oldbranch
6618257...93afb8d 62dec1c...ac5373b refs/heads/master
--------
Eine SHA-1-Summe aus lauter Nullen bedeutet ``nicht
vorhanden''. Die erste Zeile beschreibt also eine Referenz, die
vorher nicht vorhanden war, während die zweite Zeile das Löschen einer
Referenz bedeutet. Die dritte Zeile stellt ein reguläres Update dar.
Sie können die Referenzen bequem mit folgender Schleife einlesen:
--------
while read old new ref; do
# ...
done
--------
In `old` und `new` sind dann die SHA-1-Summen
gespeichert, während `ref` den Namen der Referenz enthält. Ein
`git log $old..$new` würde alle neuen Commits auflisten. Die
Standard-Ausgabe wird an `git send-pack` auf der Seite, auf der
`git push` eingegeben wurde, weitergeleitet. Sie können also
mögliche Fehlermeldungen oder Reports unmittelbar an den Nutzer
weiterleiten.
[[sec.hooks-email]]
===== E-Mails verschicken =====
Eine praktische Anwendung des `post-receive`-Hooks ist, E-Mails
zu verschicken, sobald neue Commits im Repository vorliegen. Das
können Sie natürlich selbst programmieren, allerdings gibt es schon
ein fertiges Script, das mit Git geliefert wird. Im
Quellverzeichnis von Git finden Sie es unter
`contrib/hooks/post-receive-email`, manche Distributionen,
z.B. Debian, installieren es auch zusammen mit Git nach
`/usr/share/doc/git/contrib/hooks/post-receive-email`.
Sobald Sie den Hook in das Unterverzeichnis `hooks/` Ihres Bare
Repositorys kopiert und ausführbar gemacht haben, können Sie noch die
Konfiguration entsprechend anpassen:
[subs="macros,quotes"]
--------
$ *less config*
...
[hooks]
mailinglist = "Autor Eins <pass:quotes[[email protected]]>, pass:quotes[[email protected]]"
envelopesender = "pass:quotes[[email protected]]"
emailprefix = "[project] "
--------
Damit wird für jeden Push-Vorgang pro Referenz eine Mail mit einer
Zusammenfassung der neuen Commits verschickt. Die Mail geht an alle
Empfänger, die in `hooks.mailinglist` definiert sind, und
stammt von `hooks.envelopesender`. Der
Subject-Zeile wird das `hooks.emailprefix` vorangestellt, so
dass die E-Mail leichter wegsortiert werden kann. Weitere Optionen
sind in den Kommentaren des Hooks dokumentiert.
[[sec.hooks-update]]
===== Der update-Hook =====
Der `update`-Hook wird für jede Referenz einzeln aufgerufen.
Er eignet sich daher besonders gut, eine Art
``Zugriffsregelung'' auf bestimmte Branches zu implementieren.
Tatsächlich wird der `update`-Hook zum Beispiel von Gitolite
(siehe <<sec.gitolite>>) genutzt, um zu entscheiden, ob ein
Branch modifiziert werden darf oder nicht. Gitolite implementiert den
Hook als Perl-Script, das überprüft, ob die entsprechende Berechtigung
vorliegt, und sich entsprechend mit dem Rückgabewert Null oder nicht
Null beendet.
[[sec.hooks-deploy]]
===== Deployment über Hooks =====
Git versteht sich als Versionsverwaltungssystem und weiß nichts von
Deployment-Prozessen. Über den Update-Hook können Sie allerdings –
z.B. für Web-Applikationen – ein einfaches Deployment-Verfahren
implementieren.
Der folgende `update`-Hook wird, sofern der
`master`-Branch geändert wurde, die Änderungen auf
`/var/www/www.example.com` replizieren:
--------
[ "$3" = "refs/heads/master" ] || exit 0
env GIT_WORK_TREE=/var/www/www.example.com git checkout -f
--------
Sobald Sie also neue Commits per `git push` in den
Master-Branch des Servers hochladen, wird dieser Hook die Web-Präsenz
automatisch aktualisieren.
[[sec.hooks-am]]
==== Patches anwenden ====
Die folgenden Hooks werden jeweils von `git am` aufgerufen, wenn
ein oder mehrere Patches angewendet werden.
`applypatch-msg`:: Wird aufgerufen, bevor ein Patch angewendet
wird. Der Hook erhält als einzigen Parameter die Datei, in der die
Commit-Nachricht des Patches gespeichert ist. Der Hook kann die
Nachricht bei Bedarf verändern. Ein Exit-Status ungleich Null
veranlasst `git am`, den Patch nicht anzunehmen.
`pre-applypatch`:: Wird aufgerufen, nachdem ein Patch angewendet
wurde, aber bevor die Änderung committet wird. Ein Exit-Status
ungleich Null veranlasst `git am`, den Patch nicht anzunehmen.
`post-applypatch`:: Wird aufgerufen, nachdem ein Patch eingepflegt
wurde.
Die per Default installierten Hooks führen, sofern aktiviert, die
entsprechenden Commit-Hooks `commit-msg` und
`pre-commit` aus.
[[sec.hooks-misc]]
==== Sonstige Hooks ====
`pre-rebase`:: Wird ausgeführt, bevor ein Rebase-Prozess
beginnt. Erhält als Argumente die Referenzen, die auch dem
Rebase-Kommando übergeben werden (also erhält der Hook z.B. bei dem
Kommando `git rebase master topic` die Argumente `master` und
`topic`). Anhand des Exit-Status entscheidet `git rebase`, ob der
Rebase-Vorgang ausgeführt wird oder nicht.
// TODO(mw/jp): Können wir hier wieder sichtbare Leerzeichen haben?
// Zumindest in meinem lokalen AsciiDoc macht das nur "normale"
// Leerzeichen hin. (Ich will \textvisiblespace)
`pre-push`:: Wird ausgeführt, bevor ein Push-Vorgang startet. Erhält auf
Standard-Input Zeilen der Form
`<lokale-ref>`␣`<lokale-sha1>`␣`<remote-ref>`␣`<remote-sha1>`.
Beendet sich der Hook nicht erfolgreich, so wird der Push-Vorgang
abgebrochen.
`post-rewrite`:: Wird von Kommandos aufgerufen, die Commits
umschreiben (momentan nur `git commit --amend` und `git rebase`).
Erhält auf Standard-Input eine Liste im Format
`<alte-sha1>`␣`<neue-sha1>`.
`post-checkout`:: Wird nach einem Checkout aufgerufen. Die ersten
beiden Parameter sind die alte und neue Referenz, auf die `HEAD`
zeigt. Der dritte Parameter ist ein Flag, das anzeigt, ob ein Branch
gewechselt wurde (`1`) oder einzelne Dateien ausgecheckt wurden (`0`).
`post-merge`:: Wird ausgeführt, wenn ein Merge erfolgreich beendet
wurde. Der Hook erhält als Argument eine `1`, wenn der Merge ein sog.
Squash-Merge war, also ein Merge, der keinen Commit erstellt, sondern
nur die Dateien im Working Tree bearbeitet hat.
`pre-auto-gc`:: Wird aufgerufen, bevor `git gc --auto` ausgeführt
wird. Verhindert die Ausführung der automatischen Garbage-Collection,
wenn der Rückgabewert ungleich Null ist.
Die `post-checkout`- und `post-commit`-Hooks können Sie
gut verwenden, um Git ``echte'' Dateizugriffsrechte
beizubringen. Ein Blob-Objekt spiegelt nämlich nicht genau den Inhalt
einer Datei und ihrer Zugriffsrechte wider. Stattdessen kennt Git nur
``ausführbar'' oder ``nicht ausführbar''.footnote:[Würde Git die kompletten
Zugriffsrechte aufnehmen, dann wäre eine Datei gleichen Inhalts bei
zwei verschiedenen Entwicklern, die unterschiedliche
`umask(2)`-Einstellungen verwenden, nicht der gleiche Blob.
Um das zu verhindern, verwendet Git ein vereinfachtes
Rechtemanagement.]
Das im Git-Quellverzeichnis unter
`contrib/hooks/setgitperms.perl` abgelegte
Script bietet eine vorgefertigte Lösung, die Sie in die o.g. Hooks
integrieren können. Das Script speichert die wirklichen
Zugriffsrechte in einer Datei `.gitmeta` ab. Wenn Sie das
Einlesen (Option `-r`) im `pre-commit`-Hook vornehmen
und die Hooks `post-checkout` und `post-merge` mit dem
Kommando zum Schreiben der Rechte ausstatten (Option `-w`),
dann sollten die Zugriffsrechte Ihrer Dateien nun persistent sein. Für
die genauen Kommandos siehe die Kommentare in der Datei.
Die Zugriffsrechte sind natürlich nur zwischen Checkouts stabil --
sofern Sie die Datei `.gitmeta` nicht einchecken und die
Benutzung der Hooks forcieren, bekommen Klone dieses Repositorys
natürlich nur die ``einfachen'' Zugriffsrechte.
[[sec.scripting]]
=== Eigene Git-Kommandos schreiben ===
Git folgt mit seiner Einteilung in Subkommandos der Unix-Philosophie
``Ein Tool, ein Job''. Außerdem teilt es die Subkommandos in
zwei Kategorien: 'Porcelain' und 'Plumbing'.
Porcelain bezeichnet das ``gute Porzellan'', das für den
Endnutzer aus dem Schrank geholt wird: ein aufgeräumtes
Nutzerinterface und menschenlesbare Ausgaben. Die Plumbing-Kommandos
hingegen werden vor allem für die ``Klempnerarbeit'' in
Scripten verwendet und haben eine maschinenlesbare Ausgabe (meist
zeilenweise mit eindeutigen Trennzeichen).
Tatsächlich ist ein wesentlicher Teil der Porcelain-Kommandos als
Shell-Script realisiert. Sie verwenden intern die diversen
Plumbing-Kommandos, präsentieren aber nach außen hin ein
verständliches Interface. Die Kommandos `rebase`, `am`,
`bisect` und `stash` sind nur einige Beispiele.
Es ist daher sinnvoll und einfach, selbst Shell-Scripte zu schreiben,
um häufig auftretende Aufgaben in Ihrem Arbeitsablauf zu
automatisieren. Das können zum Beispiel Scripte sein, die den
Release-Prozess der Software steuern, automatische Changelogs
erstellen oder andere auf das Projekt zugeschnittene Operationen.
Ein eigenes Git-Kommando zu schreiben, ist denkbar einfach: Sie müssen
lediglich eine ausführbare Datei in einem Verzeichnis Ihres
`$PATH` ablegen (also z.B. in
`~/bin`), dessen Name mit `git-`
beginnt. Wenn Sie `git <kommando>` eingeben und
`<kommando>` ist weder ein Alias noch ein bekanntes Kommando,
dann versucht Git einfach, `git-<kommando>` auszuführen.
[TIP]
========
Auch wenn Sie prinzipiell Scripte in einer beliebigen Sprache
schreiben können, empfehlen wir Ihnen die Verwendung von
Shell-Scripten: Nicht nur sind sie für Außenstehende leichter
verständlich, vor allem aber sind die typischen Operationen, mit denen
man Git-Kommandos kombiniert – Programme aufrufen, Ausgabeum- bzw.
-weiterleitung – mit der Shell ``intuitiv'' machbar und bedürfen
keiner umständlichen Konstrukte, wie z.B. in Perl mit `qx()` oder in
Python mit `os.popen()`.
Wenn Sie Shell-Scripte schreiben, achten Sie bitte auf
POSIX-Kompatibilität!footnote:[Sie können Ihre Shell-Scripte z.B. auf
http://www.shellcheck.net/ automatisch überprüfen lassen.]
Dazu gehört insbesondere, keine ``Bashismen'' wie `[[ ... ]]` zu
verwenden (die POSIX-Entsprechung lautet `[ ... ]`). Wenn Ihr Script
nicht auch problemlos mit der Dash{empty}footnote:[Die 'Debian Alquimist
Shell', ein Fork der 'Alquimist Shell', ist eine besonders kleine,
schnelle Shell, die POSIX-kompatibel ist. Sie stellt auf vielen
modernen Debian-Systemen sowie auf Ubuntu die Standard-Shell
`/bin/sh`.] läuft, sollten Sie die verwendete Shell explizit in der
Shebang-Zeile angeben, z.B. via `#!/bin/bash`.
========
Sämtliche im folgenden Abschnitt vorgestellten Scripte finden Sie auch
online, in der Scriptsammlung für dieses Buch.footnote:[https://github.com/gitbuch/buch-scripte]
[[sec.scripting-init]]
==== Initialisierung ====
Typischerweise wollen Sie sicherstellen, dass Ihr Script in einem
Repository ausgeführt wird. Für notwendige Initialisierungsaufgaben
bietet Git das `git-sh-setup` an. Dieses Shell-Script sollten
Sie direkt nach der Shebang-Zeile per `.` einbinden (in
interaktiven Shells bekannt als `source`):
--------
#!/bin/sh
. $(git --exec-path)/git-sh-setup
--------
Sofern Git kein Repository entdecken kann, bricht
`git-sh-setup` ab. Außerdem bricht das Script ab, wenn es nicht
auf oberster Ebene in einem Repository ausgeführt wird. Ihr Script
kommt dadurch nicht zur Ausführung, und es wird eine entsprechende
Fehlermeldung ausgegeben. Dieses Verhalten können Sie umgehen, indem
Sie vor dem Aufruf die Variable `NONGIT_OK` bzw.
`SUBDIRECTORY_OK` setzen.
Neben diesem Initialisierungsmechanismus stehen einige Funktionen
bereit, die häufig auftretende Aufgaben erledigen. Nachfolgend eine
Übersicht über die wichtigsten:
`cd_to_toplevel`:: Wechselt auf die oberste Ebene des Git-Repositorys.
`say`:: Gibt die Argumente aus, es sei denn, `GIT_QUIET` ist gesetzt.
`git_editor`:: Öffnet den für Git eingestellten Editor auf den
angegebenen Dateien. Es ist besser, diese Funktion zu verwenden als
``blind''{empty}{nbsp}`$EDITOR`. Git verwendet dies auch als Fallback.
`git_pager`:: Öffnet analog den für Git definierten Pager.
`require_work_tree`:: Die Funktion bricht mit einer Fehlermeldung ab,
wenn es keinen Working Tree zum Repository gibt -- das ist bei Bare
Repositories der Fall. Sie sollten diese Funktion also
sicherheitshalber aufrufen, wenn Sie auf Dateien aus dem Working Tree
zugreifen wollen.
[[sec.scripting-pos]]
==== Position im Repository ====
In Scripten werden Sie häufig die Information benötigen, aus welchem
Verzeichnis das Script aufgerufen wurde. Dafür bietet das Git-Kommando
`rev-parse` einige Optionen. Das folgende Script, abgelegt
unter `~/bin/git-whereami`, verdeutlicht, wie man
sich innerhalb eines Repositorys ``zurechtfinden'' kann.
--------
#!/bin/sh
SUBDIRECTORY_OK=Yes
. $(git --exec-path)/git-sh-setup
gitdir="$(git rev-parse --git-dir)"
absolute="$(git rev-parse --show-toplevel)"
relative="$(git rev-parse --show-cdup)"
prefix="$(git rev-parse --show-prefix)"
echo "gitdir absolute relative prefix"
echo "$gitdir $absolute $relative $prefix"
--------
Die Ausgabe sieht wie folgt aus:
[subs="macros,quotes"]
--------
$ *git whereami*
gitdir absolute relative prefix
.git /tmp/repo
$ *cd ganz/tief*
$ *git whereami*
gitdir absolute relative prefix
/tmp/repo/.git /tmp/repo ../../ ganz/tief/
--------
Besonders wichtig ist das Präfix, das Sie per `--show-prefix`
erhalten. Wenn Ihr Kommando Dateinamen entgegennimmt und Sie die
Blobs, denen sie entsprechen, in der Objektdatenbank finden wollen,
müssen Sie dieses Präfix vor den Dateinamen setzen. Wenn Sie sich
im Verzeichnis `ganz/tief` befinden und dem Script den
Dateinamen `README` übergeben, dann findet es den
entsprechenden Blob im aktuellen Tree via `ganz/tief/README`.
[[sec.scripting-rev-list]]
==== Referenzen auflisten: rev-list ====
Herzstück der Plumbing-Kommandos ist `git rev-list`
('revision list'). Seine Grundfunktion besteht darin, ein oder
mehrere Referenzen auf die SHA-1-Summe(n) aufzulösen, denen sie
entsprechen.
Mit einem `git log <ref1>..<ref2>` zeigen Sie die
Commit-Nachrichten von `<ref1>` (exklusive) bis
`<ref2>` (inklusive) an. Das Kommando `git
rev-list` löst diese Referenz auf die einzelnen Commits auf, die
davon betroffen sind, und gibt sie Zeile für Zeile aus:
[subs="macros,quotes"]
--------
$ *git rev-list master..topic*
f4a6a973e38f9fac4b421181402be229786dbee9
bb8d8c12a4c9e769576f8ddeacb6eb4eedfa3751
c7c331668f544ac53de01bc2d5f5024dda7af283
--------
Ein Script, das auf einem oder mehreren Commits operiert, kann also
Angaben, wie andere Git-Kommandos sie auch verstehen, einfach an
`rev-list` weiterleiten. Schon kann Ihr Script auch mit
komplizierten Ausdrücken umgehen.
Das Kommando können Sie beispielsweise nutzen, um zu überprüfen, ob
ein Fast-Forward von einem Branch auf einen anderen möglich ist. Ein
Fast-Forward von `<ref1>` auf `<ref2>` ist genau dann
möglich, wenn Git im Commit-Graphen von `<ref2>` aus den Commit, den
`<ref1>` markiert, erreichen kann. Oder anders ausgedrückt: Es
gibt keinen von `<ref1>` erreichbaren Commit, der nicht
auch von `<ref2>` erreichbar wäre.
--------
#!/bin/sh
SUBDIRECTORY_OK=Yes
. $(git --exec-path)/git-sh-setup
[ $# -eq 2 ] || { echo "usage: $(basename $0) <ref1> <ref2>"; exit 1; }
for i in $1 $2
do
if ! git rev-parse --verify $i >| /dev/null 2>&1 ; then