-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathpraxis.txt
2533 lines (2044 loc) · 97.4 KB
/
praxis.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
[[chap.praxis]]
== Praktische Versionsverwaltung ==
Das folgende Kapitel stellt alle wesentlichen Techniken vor, die Sie
im täglichen Umgang mit Git einsetzen werden. Neben einer
genaueren Beschreibung des Index und wie man alte Versionen
wiederherstellt, liegt der Fokus auf der effektiven Arbeit mit
Branches.
[[sec.branches]]
=== Referenzen: Branches und Tags ===
``Branch'' und ``Merge'' sind im CVS-/SVN-Umfeld für
Neulinge oft ein Buch mit sieben Siegeln, für Könner regelmäßig Grund
zum Haare raufen. In Git sind das Abzweigen im Entwicklungszyklus
('Branching') und das anschließende Wiederzusammenführen
('Merging') alltäglich, einfach, transparent und schnell. Es
kommt häufig vor, dass ein Entwickler an einem Tag mehrere Branches
erstellt und mehrere Merges durchführt.
Das Tool Gitk ist hilfreich, um bei mehreren Branches nicht den
Überblick zu verlieren. Mit
`gitk --all` zeigen Sie alle Branches an. Das Tool visualisiert den im vorigen Abschnitt
erläuterten Commit-Graphen. Jeder Commit stellt eine Zeile dar.
Branches werden als grüne Labels, Tags als gelbe Zeiger dargestellt.
Für weitere Informationen siehe <<sec.gitk>>.
.Das Beispiel-Repository aus <<ch.interna>> in Gitk. Zur Illustration wurde der zweite Commit mit dem Tag `v0.1` versehen.
image::bilder_ebook/gitk-basic.png[id="fig.gitk-basic",scaledwidth="90%",width="90%"]
Da Branches in Git ``billig'' und Merges einfach sind, können
Sie es sich leisten, Branches exzessiv zu verwenden. Sie wollen etwas
probieren, einen kleinen Bugfix vorbereiten oder mit einem
experimentellen Feature beginnen? Für all das erstellen Sie jeweils
einen neuen Branch. Sie wollen testen, ob sich ein Branch mit dem
anderen verträgt? Führen Sie die beiden zusammen, testen Sie alles,
und löschen Sie danach den Merge wieder und entwickeln weiter. Das
ist gängige Praxis unter Entwicklern, die Git einsetzen.
Zunächst wollen wir uns mit Referenzen generell auseinandersetzen.
Referenzen sind nichts weiter als symbolische Namen für die schwierig
zu merkenden SHA-1-Summen von Commits.
Diese Referenzen liegen in `.git/refs/`. Der Name einer
Referenz wird anhand des Dateinamens, das Ziel anhand des Inhalts der
Datei bestimmt. Der Master-Branch, auf dem Sie schon die ganze Zeit
arbeiten, sieht darin zum Beispiel so aus:
[subs="macros,quotes"]
--------
$ *cat .git/refs/heads/master*
89062b72afccda5b9e8ed77bf82c38577e603251
--------
[TIP]
===================
Wenn Git sehr viele Referenzen verwalten muss, liegen diese nicht
zwingend als Dateien unterhalb von `.git/refs/`. Git erstellt
dann stattdessen einen Container, der 'gepackte Referenzen' ('Packed
Refs') enthält: Eine Zeile pro Referenz mit Name und SHA-1-Summe. Das
sequentielle Auflösen vieler Referenzen geht dann schneller.
Git-Kommandos suchen Branches und Tags in der Datei `.git/packed-refs`, wenn die entsprechende Datei
`.git/refs/<name>` nicht existiert.
===================
Unterhalb von `.git/refs/` gibt es verschiedene Verzeichnisse,
die für die ``Art'' von Referenz stehen. Fundamental
unterscheiden sich diese Referenzen aber nicht, lediglich darin, wann
und wie sie angewendet werden. Die Referenzen, die Sie am häufigsten
verwenden werden, sind Branches. Sie sind unter `.git/refs/heads/` gespeichert. 'Heads' bezeichnet das,
was in anderen Systemen zuweilen auch ``Tip'' genannt wird:
Den neuesten Commit auf einem Entwicklungsstrang.footnote:[Das hindert Sie natürlich nicht, einen
Branch auf einen Commit ``irgendwo in der Mitte'' zu setzen,
was auch sinnvoll sein kann.] Branches rücken weiter, wenn Sie
Commits auf einem Branch erstellen -- sie bleiben also an der Spitze
der Versionsgeschichte.
.Der Branch referenziert immer den aktuellsten Commit
image::bilder_ebook/commit.png[id="fig.commit",scaledwidth="80%",width="80%"]
Branches in Repositories anderer Entwickler (z.B. der Master-Branch
des offiziellen Repositorys), sog.
Remote-Tracking-Branches, werden unter `.git/refs/remotes/` abgelegt (siehe <<sec.remote_tracking_branches>>). Tags, statische
Referenzen, die meist der Versionierung dienen, liegen unter `.git/refs/tags/` (siehe <<sec.tags>>).
[[sec.branch-refs]]
==== HEAD und andere symbolische Referenzen ====
Eine Referenz, die Sie selten explizit, aber ständig implizit
benutzen, ist `HEAD`. Sie referenziert meist den gerade
ausgecheckten Branch, hier `master`:
[subs="macros,quotes"]
--------
$ *cat .git/HEAD*
ref: refs/heads/master
--------
`HEAD` kann auch direkt auf einen Commit zeigen, wenn Sie
`git checkout <commit-id>` eingeben. Sie sind dann
allerdings im sogenannten 'Detached-Head'-Modus, in dem Commits
möglicherweise verlorengehen, siehe auch
<<sec.detached-head>>.
Der `HEAD` bestimmt, welche Dateien im Working Tree zu finden
sind, welcher Commit Vorgänger bei der Erstellung eines neuen wird,
welcher Commit per `git show` angezeigt wird etc. Wenn wir
hier von ``dem aktuellen Branch'' sprechen, dann ist damit
technisch korrekt der `HEAD` gemeint.
Die simplen Kommandos `log`, `show` und `diff`
nehmen ohne weitere Argumente `HEAD` als erstes Argument an.
Die Ausgabe von `git log` ist gleich der von `git log HEAD` usw. -- dies gilt für die meisten Kommandos, die auf einem
Commit operieren, wenn Sie keinen explizit angeben. `HEAD` ist
somit vergleichbar mit der Shell-Variable `PWD`, die angibt
``wo man ist''.
Wenn wir von einem Commit sprechen, dann ist es einem Kommando in der
Regel egal, ob man die Commit-ID komplett oder verkürzt angibt oder
den Commit über eine Referenz, wie z.B. ein Tag oder Branch,
ansteuert. Eine solche Referenz muss aber nicht immer eindeutig sein.
Was passiert, wenn es einen Branch `master` gibt und ein Tag
gleichen Namens? Git überprüft, ob die folgenden Referenzen
existieren:
* `.git/<name>` (meist nur sinnvoll für `HEAD` o.ä.)
* `.git/refs/<name>`
* `.git/refs/tags/<name>`
* `.git/refs/heads/<name>`
* `.git/refs/remotes/<name>`
* `.git/refs/remotes/<name>/HEAD`
Die erste gefundene Referenz nimmt Git als Treffer an. Sie sollten
also Tags immer ein eindeutiges Schema geben, um sie nicht mit
Branches zu verwechseln. So können Sie Branches direkt über den Namen
statt über `heads/<name>` ansprechen.
Besonders wichtig sind dafür die Suffixe `^` und `~<n>`. Die Syntax
`<ref>^` bezeichnet den direkten Vorfahren von `<ref>`. Dieser muss
aber nicht immer eindeutig sein: Wenn zwei oder mehr Branches
zusammengeführt wurden, hat der Merge-Commit mehrere direkte
Vorfahren. `<ref>^` bzw. `<ref>^1` bezeichnen dann den ersten
'direkten' Vorfahren, `<ref>^2` den zweiten usw.footnote:[Aufgrund der
Tatsache, dass bei einem Merge die Reihenfolge der direkten Vorfahren
gespeichert wird, ist es wichtig, immer vom kleineren 'in' den
größeren Branch zu mergen, also z.B.{empty}{nbsp}`topic` nach `master`. Wenn Sie
dann mit `master^^` Commits im Master-Branch untersuchen wollen,
landen Sie nicht auf einmal auf Commits aus dem Topic-Branch (siehe
auch <<sec.merge>>).] Die Syntax `HEAD^^` bedeutet also ``der zwei
Ebenen vorher liegende direkte Vorfahre des aktuellen
Commits''. Achten Sie darauf, dass `^` in Ihrer Shell möglicherweise
eine spezielle Bedeutung hat und Sie es durch Anführungszeichen oder
mit einem Backslash schützen müssen.
.Relative-Referenzen, `^` und `~<n>`
image::bilder_ebook/relative-refs.png[id="fig.relative-refs",scaledwidth="65%",width="65%"]
Die Syntax `<ref>~<n>` kommt einer
'n'-fachen Wiederholung von `^` gleich:
`HEAD~10` bezeichnet also den zehnten direkten
Vorgänger des aktuellen Commits. Achtung: Das heißt nicht, dass
zwischen `HEAD` und `HEAD~10` nur elf
Commits liegen: Da `^` bei einem etwaigen Merge nur dem
ersten Strang folgt, liegen zwischen den beiden Referenzen die elf
und alle durch einen Merge integrierten weiteren Commits.
Die Syntax ist übrigens in der Man-Page `git-rev-parse(1)` im Abschnitt
``Specifying Revisions'' dokumentiert.
[[sec.branch-management]]
==== Branches verwalten ====
Ein Branch ist in Git im Nu erstellt. Git muss lediglich den aktuell
ausgecheckten Commit identifizieren und die SHA-1-Summe in der Datei
`.git/refs/heads/<branch-name>` ablegen.
[subs="macros,quotes"]
--------
$ *time git branch neuer-branch*
git branch neuer-branch 0.00s user 0.00s system 100% cpu 0.008 total
--------
Das Kommando ist so schnell, weil (im Gegensatz zu anderen Systemen)
keine Dateien kopiert und keine weiteren Metadaten abgelegt werden
müssen. Informationen über die Struktur der Versionsgeschichte sind
immer aus dem Commit, den ein Branch referenziert, und seinen
Vorfahren ableitbar.
Hier eine Übersicht der wichtigsten Optionen:
`git branch [-v]`:: Listet lokale Branches auf.
Dabei ist der aktuell ausgecheckte Branch mit einem Sternchen
markiert. Mit `-v` werden außerdem die Commit-IDs, auf die
die Branches zeigen, sowie die erste Zeile der Beschreibung der
entsprechenden Commits angezeigt.
+
[subs="macros,quotes"]
--------
$ *git branch -v*
maint 65f13f2 Start 1.7.5.1 maintenance track
* master 791a765 Update draft release notes to 1.7.6
next b503560 Merge branch \'master' into next
pu d7a491c Merge branch \'js/info-man-path' into pu
--------
`git branch <branch> [<ref>]`:: Erstellt einen neuen
Branch `<branch>`, der auf Commit `<ref>` zeigt
(`<ref>` kann die SHA-1-Summe eines Commits sein, ein
anderer Branch usw.). Wenn Sie keine Referenz
angeben, ist dies `HEAD`, der aktuelle Branch.
`git branch -m <neuer-name>`::
`git branch -m <alter-name> <neuer-name>`
+
In der ersten Form
wird der aktuelle Branch in `<neuer-name>` umbenannt.
In der zweiten Form wird `<alter-name>` in
`<neuer-name>` umbenannt. Das Kommando schlägt fehl,
wenn dadurch ein anderer Branch überschrieben würde.
+
[subs="macros,quotes"]
--------
$ *git branch -m master*
fatal: A branch named \'master' already exists.
--------
+
Wenn Sie einen Branch umbenennen, gibt Git keine Meldung aus. Sie können
also hinterher überprüfen, dass die Umbenennung erfolgreich war:
+
[subs="macros,quotes"]
--------
$ *git branch*
* master
test
$ *git branch -m test pu/feature*
$ *git branch*
* master
pu/feature
--------
`git branch -M ...`:: Wie `-m`, nur dass
ein Branch auch umbenannt wird, wenn dadurch ein anderer
überschrieben wird. Achtung: Dabei können Commits des
überschriebenen Branches verlorengehen!
`git branch -d <branch>`:: Löscht
`<branch>`. Sie können mehrere Branches gleichzeitig
angeben. Git weigert sich, einen Branch zu löschen,
wenn er noch nicht komplett in seinen Upstream-Branch, oder, falls
dieser nicht existiert, in `HEAD`, also den aktuellen Branch,
integriert ist. (Mehr über Upstream-Branches finden Sie in
<<sec.pull>>.)
`git branch -D ...`:: Löscht einen Branch, auch wenn
er Commits enthält, die noch nicht in den Upstream- oder aktuellen Branch
integriert wurden. Achtung: Diese Commits können möglicherweise
verlorengehen, wenn sie nicht anders referenziert werden.
[[sec.branch-checkout]]
===== Branches wechseln: checkout =====
Branches wechseln Sie mit `git checkout <branch>`. Wenn Sie
einen Branch erstellen und direkt darauf wechseln
wollen, verwenden Sie `git checkout -b <branch>`. Das Kommando
ist äquivalent zu `git branch <branch> && git checkout
<branch>`.
Was passiert bei einem Checkout? Jeder Branch referenziert einen
Commit, der wiederum einen Tree referenziert, also das Abbild einer
Verzeichnisstruktur. Ein `git checkout <branch>` löst nun die
Referenz `<branch>` auf einen Commit auf und repliziert den
Tree des Commits auf den Index und auf den Working Tree (d.h. auf
das Dateisystem).
Da Git weiß, in welcher Version Dateien aktuell in Index und Working
Tree vorliegen, müssen nur die Dateien, die sich auf dem aktuellen und
dem neuen Branch unterscheiden, ausgecheckt werden.
Git macht es Anwendern schwer, Informationen zu verlieren. Daher
wird ein Checkout eher fehlschlagen als eventuell nicht abgespeicherte
Änderungen in einer Datei überschreiben. Das passiert in den folgenden
beiden Fällen:
* Der Checkout würde eine Datei im Working Tree
überschreiben, in der sich Änderungen befinden. Git gibt folgende
Fehlermeldung aus: `error: Your local changes to the following files
would be overwritten by checkout: datei`.
* Der Checkout würde eine ungetrackte Datei überschreiben,
d.h. eine Datei, die nicht von Git verwaltet wird. Git bricht dann mit
der Fehlermeldung ab: `error: The following untracked working tree
files would be overwritten by checkout: datei`.
Liegen allerdings Änderungen im Working Tree oder Index vor, die mit
beiden Branches verträglich sind, übernimmt ein Checkout diese
Änderungen. Das sieht dann z.B. so aus:
[subs="macros,quotes"]
--------
$ *git checkout master*
A neue-datei.txt
Switched to branch 'master'
--------
Das bedeutet, dass die Datei `neue-datei.txt` hinzugefügt
wurde, die auf keinem der beiden Branches existiert. Da hier also
keine Informationen verlorengehen können, wird die Datei einfach
übernommen. Die Meldung: `A neue-datei.txt` erinnert Sie, um
welche Dateien Sie sich noch kümmern sollten. Dabei steht `A`
für hinzugefügt ('added'), `D` für gelöscht ('deleted')
und `M` für geändert ('modified').
Wenn Sie ganz sicher sind, dass Sie Ihre Änderungen nicht mehr
brauchen, können Sie per `git checkout -f` die Fehlermeldungen
ignorieren und den Checkout trotzdem ausführen.
Wenn Sie sowohl die Änderungen behalten als auch den Branch wechseln
wollen (Beispiel: Arbeit unterbrechen und auf einem anderen Branch
einen Fehler korrigieren), dann hilft `git stash` (<<sec.stash>>).
[[sec.branch-naming]]
===== Konventionen zur Benennung von Branches =====
Sie können Branches prinzipiell fast beliebig benennen. Ausnahmen sind
aber Leerzeichen, einige Sonderzeichen mit spezieller Bedeutung für Git
(z.B.{empty}{nbsp}`*`, `^`, `:`, `~`), sowie zwei aufeinanderfolgende Punkte
(`..`) oder ein Punkt am Anfang des Namens.footnote:[Wie Git eine
Referenz auf Gültigkeit überprüft, können Sie bei Bedarf in der Man-Page
`git-check-ref-format(1)` nachlesen.]
Sinnvollerweise sollten Sie Branch-Namen immer komplett in
Kleinbuchstaben angeben. Da Git Branch-Namen unter
`.git/refs/heads/` als Dateien verwaltet, ist die Groß- und
Kleinschreibung wesentlich.
Sie können Branches in ``Namespaces'' gruppieren, indem Sie
als Separator einen `/` verwenden. Branches, die mit der
Übersetzung einer Software zu tun haben, können Sie dann z.B.{empty}{nbsp}`i18n/german`, `i18n/english` etc. nennen. Auch können
Sie, wenn sich mehrere Entwickler ein Repository teilen,
``private'' Branches unter `<username>/<topic>`
anlegen. Diese Namespaces werden durch eine Verzeichnisstruktur
abgebildet, so dass dann unter `.git/refs/heads/` ein
Verzeichnis `<username>/` mit der Branch-Datei `<topic>`
erstellt wird.
Der Hauptentwicklungszweig Ihres Projekts sollte immer `master`
heißen. Bugfixes werden häufig auf einem Branch `maint` (kurz
für ``maintenance'') verwaltet. Das nächste Release wird
meist auf `next` vorbereitet. Features, die sich noch in einem
experimentellen Zustand befinden, sollten in `pu` (für
``proposed updates'') entwickelt werden oder in
`pu/<feature>`. Eine detailliertere Beschreibung, wie Sie mit
Branches die Entwicklung strukturieren und Release-Zyklen
organisieren, finden Sie in <<sec.workflows>> über Workflows.
[[sec.no-ref-commits]]
===== Gelöschte Branches und ``verlorene'' Commits =====
Commits kennen jeweils einen oder mehrere Vorgänger. Daher kann man
den Commit-Graphen ``gerichtet'', d.h. von neueren zu
älteren Commits, durchlaufen, bis man an einem Wurzel-Commit ankommt.
Andersherum geht das nicht: Wenn ein Commit seinen Nachfolger kennen
würde, müsste diese Version irgendwo gespeichert werden. Dadurch würde
sich die SHA-1-Summe des Commits ändern, worauf der Nachfolger den
entsprechend neuen Commit referenzieren müsste, dadurch eine neue
SHA-1-Summe erhielte, so dass wiederum der Vorgänger geändert werden
müsste usw. Git kann also die Commits nur von einer benannten
Referenz aus (z.B. ein Branch oder `HEAD`) in Richtung
früherer Commits durchgehen.
Wenn daher die ``Spitze'' eines Branches gelöscht wird, wird
der oberste Commit nicht mehr referenziert (im Git-Jargon:
'unreachable'). Dadurch wird der Vorgänger nicht mehr
referenziert usw. -- bis der nächste Commit auftaucht, der irgendwie
referenziert wird (sei es von einem Branch oder dadurch, dass er einen
Nachfolger hat, der wiederum von einem Branch referenziert wird).
Wenn Sie einen Branch löschen, werden die Commits auf diesem Branch
also nicht gelöscht, sie gehen nur ``verloren''. Git findet
sie einfach nicht mehr.
In der Objektdatenbank sind sie allerdings noch eine Weile lang
vorhanden.footnote:[Wie lange sie dort verweilen, bestimmen Sie
mit entsprechenden Einstellungen für die 'Garbage Collection'
(Wartungsmechanismen), siehe <<sec.gc>>.] Sie können also
einen Branch ohne weiteres wiederherstellen, indem Sie den vorherigen
(und vermeintlich gelöschten) Commit explizit als Referenz angeben:
[subs="macros,quotes"]
--------
$ *git branch -D test*
Deleted branch test (was e32bf29).
$ *git branch test e32bf29*
--------
Eine weitere Möglichkeit, gelöschte Commits wiederzufinden, ist das
'Reflog' (siehe dafür <<sec.reflog>>).
[[sec.tags]]
==== Tags – Wichtige Versionen markieren ====
SHA-1-Summen sind zwar eine sehr elegante Lösung, um Versionen
dezentral zu beschreiben, aber semantikarm und für Menschen
unhandlich. Im Gegensatz zu linearen Revisionsnummern sagen uns
Commit-IDs allein nichts über die Reihenfolge der Versionen.
Während der Entwicklung von Softwareprojekten müssen
verschiedene ``wichtige'' Versionen so markiert
werden, dass sie leicht in dem Repository zu finden sind. Die
wichtigsten sind meist solche, die veröffentlicht werden, die
sogenannten 'Releases'. Auch 'Release Candidates' werden
häufig auf diese Weise markiert, also Versionen, die die Basis für die
nächste Version bilden und im Zuge der Qualitätssicherung auf
kritische Fehler untersucht werden, ohne dass neue Features
hinzugefügt werden. Je nach Projekt und Entwicklungsmodell gibt es
verschiedene Konventionen, um Releases zu bezeichnen, und Abläufe, wie
sie vorbereitet und publiziert werden.
Im Open-Source-Bereich haben sich zwei Versionierungsschemata
durchgesetzt: die klassische 'Major/Minor/Micro-Versionierung'
und neuerdings auch die 'datumsbasierte Versionierung'. Bei der
Major/Minor/Micro-Versionierung, welche z.B. beim Linux-Kernel und
auch Git eingesetzt wird, ist eine Version durch drei (oft auch vier)
Zahlen gekennzeichnet: `2.6.39` oder `1.7.1`. Bei der
datumsbasierten Versionierung hingegen ist die Bezeichnung aus dem
Zeitpunkt des Releases abgeleitet, z.B.: `2011.05` oder
`2011-05-19`. Das hat den großen Vorteil, dass das Alter einer
Version leicht ersichtlich ist.footnote:[Eine
detaillierte Übersicht der Vor- und Nachteile der beiden Schemata
sowie eine Beschreibung des Release-Prozesses usw. finden Sie im
Kapitel 6 des Buches 'Open Source Projektmanagement' von Michael
Prokop (Open Source Press, München, 2010).]
Git bietet Ihnen mit 'Tags' (``Etiketten'') die Möglichkeit,
beliebige Git-Objekte -- meist Commits -- zu markieren, um markante
Zustände in der Entwicklungsgeschichte hervorzuheben. Tags sind, wie
Branches auch, als Referenzen auf Objekte implementiert. Im Gegensatz
zu Branches jedoch sind Tags statisch, das heißt, sie werden nicht
verschoben, wenn neue Commits hinzukommen, und zeigen stets auf
dasselbe Objekt. Es gibt zwei Arten von Tags: 'Annotated' (mit
Anmerkungen versehen) und 'Lightweight'
(``leichtgewichtig'', d.h. ohne Anmerkungen). Annotated
Tags sind mit Metadaten -- z.B. Autor, Beschreibung oder
GPG-Signatur -- versehen. Lightweight Tags zeigen hingegen
``einfach nur'' auf ein bestimmtes Git-Objekt. Für beide Arten
von Tags legt Git unter `.git/refs/tags/` bzw.
`.git/packed-refs` Referenzen an. Der Unterschied ist,
dass Git für jedes Annotated Tag ein spezielles Git-Objekt -- und zwar
ein 'Tag-Objekt' -- in der Objektdatenbank anlegt, um die
Metadaten sowie die SHA-1-Summe des markierten Objekts zu speichern,
während ein Lightweight Tag direkt auf das markierte Objekt zeigt.
<<fig.tag-objekt>> zeigt den Inhalt eines Tag-Objekts;
vergleichen Sie auch die anderen Git-Objekte, <<fig.objekte>>.
.Das Tag-Objekt
image::bilder_ebook/tags.png[id="fig.tag-objekt",scaledwidth="90%",width="90%"]
Das gezeigte Tag-Objekt hat sowohl eine Größe (158 Byte) als auch eine
SHA-1-Summe. Es enthält die Bezeichnung (`0.1`), den Objekt-Typ
und die SHA-1-Summe des referenzierten Objekts sowie den Namen und
E-Mail des Autors, der im Git-Jargon 'Tagger' heißt. Außerdem
enthält das Tag eine Tag-Message, die zum Beispiel die Version beschreibt,
sowie optional eine GPG-Signatur. Im Git-Projekt etwa besteht eine Tag-Message
aus der aktuellen Versionsbezeichnung und der Signatur des
Maintainers.
Schauen wir im Folgenden zunächst, wie Sie Tags lokal verwalten. Wie
Sie Tags zwischen Repositories austauschen, beschreibt <<sec.remote-tags>>.
[[sec.tags-verwalten]]
===== Tags verwalten =====
Tags verwalten Sie mit dem Kommando `git tag`. Ohne Argumente
zeigt es alle vorhandenen Tags an. Je nach Projektgröße lohnt es sich,
die Ausgabe mit der Option `-l` und einem entsprechenden Muster
einzuschränken. Mit folgendem Befehl zeigen Sie alle Varianten der
Version `1.7.1` des Git-Projekts an, also sowohl die
Release-Candidates mit dem Zusatz `-rc*` sowie die
(vierstelligen) Maintenance-Releases:
[subs="macros,quotes"]
--------
$ *git tag -l v1.7.1**
v1.7.1
v1.7.1-rc0
v1.7.1-rc1
v1.7.1-rc2
v1.7.1.1
v1.7.1.2
v1.7.1.3
v1.7.1.4
--------
Den Inhalt eines Tags liefert Ihnen `git show`:
[subs="macros,quotes"]
--------
$ *git show 0.1 | head*
tag 0.1
Tagger: Valentin Haenel <pass:quotes[[email protected]]>
Date: Wed Mar 23 16:52:03 2011 +0100
Erste Veröffentlichung
commit e2c67ebb6d2db2aab831f477306baa44036af635
Author: Valentin Haenel <pass:quotes[[email protected]]>
Date: Sat Jan 8 20:30:58 2011 +0100
--------
Gitk stellt Tags als gelbe, pfeilartige Kästchen dar, die sich
deutlich von den grünen, rechteckigen Branches unterscheiden:
.Tags in Gitk
image::bilder_ebook/tag-screenshot.png[id="fig.tag-gitk",scaledwidth="90%",width="90%"]
[[sec.lightweight-tags]]
===== Lightweight Tags =====
Um den `HEAD` mit einem Lightweight Tag zu versehen, übergeben
Sie den gewünschten Namen an das Kommando (in diesem Beispiel, um einen
wichtigen Commit zu markieren):
[subs="macros,quotes"]
--------
$ *git tag api-aenderung*
$ *git tag*
api-aenderung
--------
Sie können aber auch die SHA-1-Summe eines Objekts oder eine valide
Revisionsbezeichnung (z.B.{empty}{nbsp}`master` oder `HEAD~23`)
angeben, um ein Objekt nachträglich zu markieren.
[subs="macros,quotes"]
--------
$ *git tag pre-regression HEAD~23*
$ *git tag*
api-aenderung
pre-regression
--------
Tags sind einzigartig -- sollten Sie versuchen, ein Tag erneut zu
erzeugen, bricht Git mit einer Fehlermeldung ab:
[subs="macros,quotes"]
--------
$ *git tag pre-regression*
fatal: tag \'pre-regression' already exists
--------
[[sec.annotated-tags]]
===== Annotated Tags =====
Annotated Tags erzeugen Sie mit der Option `-a`. Wie bei
`git commit` öffnet sich ein Editor, mit dem Sie die
Tag-Message verfassen. Oder Sie übergeben die Tag-Message mit der
Option `-m` -- dann ist die Option `-a` redundant:
[subs="macros,quotes"]
--------
$ *git tag -m "Zweite Veröffentlichung" 0.2*
--------
[[sec.signierte-tags]]
===== Signierte Tags =====
Um ein signiertes Tag zu überprüfen, verwenden Sie die Option
`-v` ('verify'):
[subs="macros,quotes"]
--------
$ *git tag -v v1.7.1*
object d599e0484f8ebac8cc50e9557a4c3d246826843d
type commit
tag v1.7.1
tagger Junio C Hamano <pass:quotes[[email protected]]> 1272072587 -0700
Git 1.7.1
gpg: Signature made Sat Apr 24 03:29:47 2010 CEST using DSA key ID F3119B9A
gpg: Good signature from "Junio C Hamano <pass:quotes[[email protected]]>"
...
--------
Das setzt natürlich voraus, dass Sie sowohl GnuPG installiert als auch
den Schlüssel des Signierenden bereits importiert haben.
Um selbst Tags zu signieren, müssen Sie zunächst den dafür bevorzugten
Key einstellen:
[subs="macros,quotes"]
--------
$ *git config --global user.signingkey <GPG-Key-ID>*
--------
Nun können Sie signierte Tags mit der Option `-s` ('sign')
erstellen:
[subs="macros,quotes"]
--------
$ *git tag -s -m "Dritte Veröffentlichung" 3.0*
--------
[[sec.tags-loeschen]]
===== Tags löschen und überschreiben =====
Mit den Optionen `-d` und `-f` löschen Sie Tags bzw.
überschreiben sie:
[subs="macros,quotes"]
--------
$ *git tag -d 0.2*
Deleted tag \'0.2' (was 4773c73)
--------
Die Optionen sind mit Vorsicht zu genießen, besonders wenn Sie die
Tags nicht nur lokal verwenden, sondern auch veröffentlichen. Unter
bestimmten Umständen kann es dazu kommen, dass Tags unterschiedliche
Commits bezeichnen -- Version `1.0` im Repository X zeigt auf
einen anderen Commit als Version `1.0` im Repository Y. Aber
sehen Sie hierzu auch <<sec.remote-tags>>.
[[sec.tags-lightweight-vs-heavyweight]]
===== Lightweight vs. Annotated Tags =====
Für die öffentliche Versionierung von Software sind allgemein
Annotated Tags sinnvoller. Sie enthalten im Gegensatz zu Lightweight
Tags Metainformationen, aus denen zu ersehen ist, wer wann ein Tag
erstellt hat -- der Ansprechpartner ist eindeutig. Auch erfahren
Benutzer einer Software so, wer eine bestimmte Version abgesegnet hat.
Zum Beispiel ist klar, dass Junio C. Hamano die Git-Version 1.7.1
getaggt hat -- sie hat also quasi sein ``Gütesiegel''. Die
Aussage bestätigt natürlich auch die kryptographische Signatur.
Lightweight Tags hingegen eignen sich vor allem, um lokal Markierungen
anzubringen, zum Beispiel um bestimmte, für die aktuelle Aufgabe
relevante Commits zu kennzeichnen. Achten Sie aber darauf, solche Tags
nicht in ein öffentliches Repository hochzuladen (siehe
<<sec.remote-tags>>), da diese sich sonst verbreiten könnten.
Sofern Sie die Tags nur lokal verwenden, können Sie sie auch löschen,
wenn sie ihren Dienst erfüllt haben (s.o.).
[[sec.non-commit-tags]]
===== Non-Commit Tags =====
Mit Tags markieren Sie beliebige Git-Objekte, also nicht nur Commits,
sondern auch Tree-, Blob- und sogar Tag-Objekte selbst! Das klassische
Beispiel ist, den öffentlichen GPG-Schlüssel, der von dem Maintainer
eines Projekts zum Signieren von Tags verwendet wird, in einem Blob zu
hinterlegen.
So zeigt das Tag `junio-gpg-pub` im Git-Repository von Git auf den
Schlüssel von Junio C. Hamano:
[subs="macros,quotes"]
--------
$ *git show junio-gpg-pub | head -5*
tag junio-gpg-pub
Tagger: Junio C Hamano <pass:quotes[[email protected]]>
Date: Tue Dec 13 16:33:29 2005 -0800
GPG key to sign git.git archive.
--------
Weil dieses Blob-Objekt von keinem Tree referenziert wird, ist die
Datei quasi getrennt vom eigentlichen Code, aber dennoch im Repository
vorhanden. Außerdem ist ein Tag auf einen ``einsamen'' Blob
notwendig, damit dieser nicht als 'unreachable' gilt und im Zuge
der Repository-Wartung gelöscht wird.footnote:[Um einen solchen getaggten Blob in ein
Repository aufzunehmen, bedienen Sie sich des folgenden
Kommandos: `git tag -am "<beschreibung>" <tag-name>
$(git hash-object -w <datei>)`.]
Um den Schlüssel zu verwenden, gehen Sie wie folgt vor:
[subs="macros,quotes"]
--------
$ *git cat-file blob junio-gpg-pub | gpg --import*
gpg: key F3119B9A: public key "Junio C Hamano <pass:quotes[[email protected]]>" imported
gpg: Total number processed: 1
gpg: imported: 1
--------
Sie können dann, wie oben beschrieben, alle Tags im Git-via-Git-Repository
verifizieren.
[[sec.git-describe]]
===== Commits beschreiben =====
Tags sind sehr nützlich, um beliebige Commits ``besser'' zu
beschreiben. Das Kommando `git describe` gibt eine
Beschreibung, die aus dem aktuellsten Tag und dessen relativer
Position im Commit-Graphen besteht. Hier ein Beispiel aus dem
Git-Projekt: Wir beschreiben einen Commit mit dem SHA-1-Präfix `28ba96a`,
der sich im Commit-Graphen sieben Commits nach der Version `1.7.1`
befindet:
.Der zu beschreibende Commit in Grau hervorgehoben
image::bilder_ebook/describe-screenshot.png[id="fig.describe",scaledwidth="90%",width="90%"]
[subs="macros,quotes"]
--------
$ *git describe --tags*
v1.7.1-7-g28ba96a
--------
Die Ausgabe von `git describe` ist wie folgt formatiert:
--------
<tag>-<position>-g<SHA-1>
--------
Das Tag ist `v1.7.1`; die Position besagt, dass sich sieben
neue Commits zwischen dem Tag und dem beschriebenen Commit
befinden.footnote:[Es handelt sich hierbei um die
Commits, die mit `git log v1.7.1..28ba96a` erfasst werden.]
Das Kürzel `g` vor der ID besagt, dass die Beschreibung aus
einem Git-Repository abgeleitet ist, was in Umgebungen mit mehreren
Versionsverwaltungssystemen nützlich ist. Standardmäßig sucht
`git describe` nur nach Annotated Tags, mit der Option
`--tags` erweitern Sie die Suche auch auf Lightweight Tags.
Das Kommando ist sehr nützlich, weil es einen inhaltsbasierten
Bezeichner in etwas für Menschen Sinnvolles übersetzt:
`v1.7.1-7-g28ba96a` ist deutlich näher an `v1.7.1` als
`v1.7.1-213-g3183286`. Dadurch können Sie die Ausgaben sinnvoll
-- wie im Git-Projekt auch -- direkt in die Software einkompilieren:
[subs="macros,quotes"]
--------
$ *git describe*
v1.7.5-rc2-8-g0e73bb4
$ *make*
GIT_VERSION = 1.7.5.rc2.8.g0e73bb
...
$ *./git --version*
git version 1.7.5.rc2.8.g0e73bb
--------
Somit weiß ein Benutzer ungefähr, welche Version er hat, und kann
nachvollziehen, aus welchem Commit die Version kompiliert wurde.
[[sec.undo]]
=== Versionen wiederherstellen ===
Ziel einer Versionskontrollsoftware ist es nicht nur, Änderungen
zwischen Commits zu untersuchen. Wichtig ist vor allem auch, ältere
Versionen einer Datei oder ganzer Verzeichnisbäume wiederherzustellen
oder Änderungen rückgängig zu machen. Dafür sind in Git insbesondere
die Kommandos `checkout`, `reset` und `revert`
zuständig.
//\label{sec:checkout}
Das Git-Kommando `checkout` kann nicht nur Branches wechseln,
sondern auch Dateien aus früheren Commits wiederherstellen. Die Syntax
lautet allgemein:
--------
git checkout [-f] <referenz> -- <muster>
--------
`checkout` löst die angegebene Referenz (und wenn diese fehlt,
`HEAD`) auf einen Commit auf und extrahiert alle Dateien, die
auf `<muster>` passen, in den Working Tree. Ist
`<muster>` ein Verzeichnis, bezieht sich das auf alle darin
enthaltenen Dateien und Unterverzeichnisse. Sofern Sie kein Muster
explizit angeben, werden alle Dateien ausgecheckt. Dabei werden
Änderungen an einer Datei nicht einfach überschrieben, es sei denn,
Sie geben die Option `-f` an (s.o.). Außerdem wird
`HEAD` auf den entsprechenden Commit (bzw. Branch) gesetzt.
Wenn Sie allerdings ein Muster angeben, dann überschreibt
`checkout` diese Datei(en) ohne Nachfrage. Um also alle
Änderungen an `<datei>` zu
verwerfen, geben Sie `git checkout -- <datei>` ein: Git
ersetzt dann `<datei>` durch die Version im aktuellen Branch.
Auf diese Weise können Sie auch den älteren Zustand einer Datei
rekonstruieren:
[subs="macros,quotes"]
--------
$ *git checkout ce66692 -- <datei>*
--------
Das doppelte Minus trennt die Muster von den Optionen bzw.
Argumenten. Es ist allerdings nicht notwendig: Gibt es keine Branches
oder andere Referenzen mit dem Namen, versucht Git, eine solche Datei
zu finden. Die Separierung macht also nur eindeutig, dass Sie die
entsprechende(n) Datei(en) wiederherstellen möchten.
Um den Inhalt einer Datei aus einem bestimmten Commit anzuschauen,
ohne sie auszuchecken, nutzen Sie das folgende Kommando:
[subs="macros,quotes"]
--------
$ *git show ce66692:<datei>*
--------
[TIP]
==================
Mit `--patch` bzw. `-p` rufen Sie `git checkout` im interaktiven Modus
auf. Der Ablauf ist der gleiche wie bei `git add -p` (siehe
<<sec.add-p>>), jedoch können Sie hier Hunks einer Datei schrittweise
zurücksetzen.
==================
[[sec.detached-head]]
==== Detached HEAD ====
Wenn Sie einen Commit auschecken, der nicht durch einen Branch
referenziert wird, befinden Sie sich im sogenannten
'Detached-HEAD'-Modus:
[subs="macros,quotes"]
--------
$ *git checkout 3329661*
Note: checking out \'3329661'.
You are in \'detached HEAD' state. You can look around, make
experimental changes and commit them, and you can discard any
commits you make in this state without impacting any branches
by performing another checkout.
If you want to create a new branch to retain commits you create,
you may do so (now or later) by using -b with the checkout command
again. Example:
git checkout -b new_branch_name
HEAD is now at 3329661... Add LICENSE file
--------
Wie die Erklärung, die Sie durch setzen der Option
`advice.detachedHead` auf `false` ausblenden können,
schon warnt, werden Änderungen, die Sie nun tätigen, im Zweifel
verlorengehen: Da Ihr `HEAD` danach die einzige direkte
Referenz auf den Commit ist, werden weitere Commits nicht direkt von
einem Branch referenziert (sie sind 'unreachable', s.o.).
Im Detached-HEAD-Modus zu arbeiten bietet sich also vor allem dann an,
wenn Sie schnell etwas probieren wollen: Ist der Fehler eigentlich
schon im Commit `3329661` aufgetaucht? Gab es zum
Zeitpunkt von `3329661` eigentlich schon die Datei
`README`?
[TIP]
============
Wenn Sie von dem ausgecheckten Commit aus mehr
machen wollen als sich bloß umzuschauen und beispielsweise testen möchten,
ob Ihre Software schon damals einen bestimmten Bug hatte, sollten Sie
einen Branch erstellen:
[subs="macros,quotes"]
--------
$ *git checkout -b <temp-branch>*
--------
Dann können Sie wie gewohnt Commits machen, ohne befürchten zu müssen,
dass diese verlorengehen.
============
[[sec.revert]]
==== Commits rückgängig machen ====
Wenn Sie alle Änderungen, die ein Commit einbringt, rückgängig machen
wollen, hilft das Kommando `revert`. Es löscht aber keinen
Commit, sondern erstellt einen neuen, dessen Änderungen genau dem
Gegenteil des anderen Commits entsprechen: Gelöschte Zeilen werden zu
hinzugefügten und umgekehrt.
Angenommen, Sie haben einen Commit, der eine Datei `LICENSE`
erstellt. Der Patch des entsprechenden Commits sieht so aus:
--------
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1 @@
+This software is released under the GNU GPL version 3 or newer.
--------
Nun können Sie die Änderungen rückgängig machen:
[subs="macros,quotes"]
--------
$ *git revert 3329661*
Finished one revert.
[master a68ad2d] Revert "Add LICENSE file"
1 files changed, 0 insertions(+), 1 deletions(-)
delete mode 100644 LICENSE
--------
Git erstellt einen neuen Commit auf dem aktuellen Branch -- sofern Sie
nichts anderes angeben -- mit der Beschreibung `Revert "<Alte Commit-Nachricht>"`. Dieser Commit sieht so aus:
[subs="macros,quotes"]
--------
$ *git show*
commit a68ad2d41e9219383449d703521573477ee7da48
Author: Julius Plenz <pass:quotes[feh@mali]>
Date: Mon Mar 7 05:28:47 2011 +0100
Revert "Add LICENSE file"
This reverts commit 3329661775af3c52e6b2ad7e9e7e7d789ba62712.
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index 3fd9c20..0000000
--- a/LICENSE
pass:quotes[\+++ /dev/null]
@@ -1 +0,0 @@
-This software is released under the GNU GPL version 3 or newer.
--------
Beachten Sie also, dass in der Versionsgeschichte eines Projekts ab
nun sowohl der Commit als auch der Revert auftauchen. Sie machen also
nur die 'Änderungen' rückgängig, löschen aber keine Informationen
aus der Versionsgeschichte.
Sie sollten daher `revert` nur einsetzen, wenn Sie eine
Änderung, die bereits veröffentlicht wurde, rückgängig machen müssen.
Entwickeln Sie allerdings lokal in einem eigenen Branch, ist es
sinnvoller, diese Commits komplett zu löschen (siehe dafür den
folgenden Abschnitt über `reset` sowie das Thema 'Rebase', <<sec.rebase>>).
Sofern Sie einen Revert durchführen wollen, allerdings nicht für
sämtliche Änderungen des Commits, sondern nur für die einer Datei,
können Sie sich zum Beispiel so behelfen:
[subs="macros,quotes"]
--------
$ *git show -R 3329661 -- LICENSE | git apply --index*
$ *git commit -m \'Revert change to LICENSE from 3329661'*
--------
Das Kommando `git show` gibt die Änderungen von Commit
`3329661` aus, die sich auf die Datei `LICENSE`
beziehen. Die Option `-R` sorgt dafür, dass das
Unified-Diff-Format ``andersherum'' angezeigt wird
('reverse'). Die Ausgabe wird an `git apply`
weitergeleitet, um die Änderungen an der Datei und dem Index
vorzunehmen. Anschließend werden die Änderungen eingecheckt.
Eine weitere Möglichkeit, eine Änderung rückgängig zu machen, besteht
darin, eine Datei aus einem vorherigen Commit auszuchecken, sie dem
Index hinzuzufügen und neu einzuchecken:
[subs="macros,quotes"]
--------
$ *git checkout 3329661 -- <datei>*
$ *git add <datei>*
$ *git commit -m \'Reverting <datei> to resemble 3329661'*
--------
[[sec.reset]]
==== Reset und der Index ====
Wenn Sie einen Commit gänzlich löschen, also nicht nur rückgängig
machen, dann verwenden Sie `git reset`. Das Reset-Kommando
setzt den `HEAD` (und damit auch den aktuellen Branch) sowie
wahlweise auch Index und Working Tree auf einen bestimmten Commit.
Die Syntax lautet `git reset [<option>] [<commit>]`.
Die wichtigsten Reset-Typen sind die folgenden:
`--soft`:: Setzt nur den `HEAD` zurück;
Index und Working Tree bleiben unberührt.
`--mixed`:: Voreinstellung, wenn Sie keine Option
angeben. Setzt `HEAD` und Index auf den angegebenen Commit,
die Dateien im Working Tree bleiben aber unberührt.
`--hard`:: Synchronisiert `HEAD`, Index und
Working Tree und setzt sie auf den gleichen Commit. Dabei gehen