forked from gitbuch/gitbuch_cc
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgrundlagen.txt
1476 lines (1178 loc) · 56.9 KB
/
grundlagen.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.interna]]
== Grundlagen ==
In diesem Kapitel stellen wir Ihnen die wichtigsten Git-Kommandos vor,
mit Hilfe derer Sie Ihre Projektdateien in Git verwalten. Unabdingbar
für eine fortgeschrittene Nutzung ist das Verständnis des
Git-Objektmodells; dieses wichtige Konzept behandeln wir im zweiten Abschnitt
des Kapitels. Mögen die Ausführungen zunächst allzu theoretisch
scheinen, so möchten wir Sie Ihnen dennoch sehr ans Herz legen. Alle
weiteren Aktionen werden Ihnen mit dem Wissen um diese Hintergründe
deutlich leichter von der Hand gehen.
[[sec.grundlagen]]
=== Git-Kommandos ===
Die Kommandos, die Sie zum Einstieg kennengelernt haben (vor allem
`add` und `commit`), arbeiten auf dem Index. Im
Folgenden werden wir uns genauer mit dem Index auseinandersetzen und
die erweiterte Benutzung dieser Kommandos behandeln.
[[sec.index]]
==== Index ====
Der Inhalt von Dateien liegt für Git auf drei Ebenen, dem
'Working Tree', dem 'Index' und dem 'Repository'. Der
Working Tree entspricht den Dateien, wie sie auf dem Dateisystem Ihres
Arbeitsrechners liegen -- wenn Sie also Dateien mit einem Editor
bearbeiten, mit `grep` darin suchen etc., operieren Sie immer
auf dem Working Tree.
Das Repository ist der Sammelbehälter für Commits, also Änderungen,
versehen mit Angaben zu Autor, Datum und Beschreibung. Die Commits
ergeben zusammen die 'Versionsgeschichte'.
Git führt nun, im Gegensatz zu vielen anderen
Versionskontrollsystemen, eine Neuerung ein, den Index. Es handelt
sich um eine etwas schwierig greifbare Zwischenebene zwischen Working
Tree und Repository. Er dient dazu, Commits vorzubereiten. Das
bedeutet, dass Sie nicht immer 'alle' Änderungen, die Sie an
einer Datei vorgenommen haben, auch als Commit einchecken müssen.
Die Git-Kommandos `add` und `reset` agieren (in ihrer
Grundform) auf dem Index und bringen Änderungen in den Index ein bzw.
löschen diese wieder; erst das Kommando `commit` überträgt die
Datei, wie sie im Index vorgehalten wird, in das Repository (<<fig.index>>).
.Kommandos `add`, `reset` und `commit`
image::bilder_ebook/index.png[id="fig.index",width="90%"]
Im Ausgangszustand, das heißt wenn `git status` die Nachricht
`nothing to commit` ausgibt, sind Working Tree und Index mit
`HEAD` synchronisiert. Der Index ist also nicht
``leer'', sondern enthält die Dateien im gleichen Zustand, wie
sie im Working Tree vorliegen.
In der Regel ist dann der Arbeitsablauf folgender: Zuerst nehmen Sie mit
einem Editor eine Veränderung am Working Tree vor. Diese Veränderung
wird durch `add` in den Index übernommen und schließlich per
`commit` im Repository abgespeichert.
Sie können sich die Unterschiede zwischen diesen drei Ebenen jeweils
durch das `diff`-Kommando anzeigen lassen. Ein simples
`git diff` zeigt die Unterschiede zwischen Working Tree und
Index an -- also die Unterschiede zwischen den (tatsächlichen) Dateien
auf Ihrem Arbeitssystem und den Dateien, wie sie eingecheckt würden,
wenn Sie `git commit` aufrufen würden.
Das Kommando `git diff --staged` zeigt hingegen die
Unterschiede zwischen Index (der auch 'Staging Area' genannt
wird) und Repository an, also die Unterschiede, die ein Commit ins
Repository übertragen würde. Im Ausgangszustand, wenn Working Tree und Index
mit `HEAD` synchron sind, erzeugen weder `git diff` noch
`git diff --staged` eine Ausgabe.
Wollen Sie alle
Änderungen an allen Dateien übernehmen, gibt es zwei
Abkürzungen: Zunächst die Option `-u` bzw. `--update`
von `git add`. Dadurch werden alle Veränderungen in den Index
übertragen, aber noch kein Commit erzeugt. Weiter abkürzen können Sie
mit der Option `-a` bzw. `--all` von `git
commit`. Dies ist eine Kombination aus `git add -u` und
`git commit`, wodurch alle Veränderungen an allen Dateien in
einem Commit zusammengefasst werden -- Sie umgehen den Index.
Vermeiden Sie es, sich diese Optionen zur Angewohnheit zu machen -- sie
sind zwar gelegentlich als Abkürzung ganz praktisch, verringern aber
die Flexibilität.
[[sec.diff-color-words]]
===== Diff auf Wortbasis =====
Ein alternatives Ausgabeformat für `git diff` ist das
sog. 'Word-Diff', das über die Option
`--word-diff` zur Verfügung steht. Statt der entfernten
und hinzugefügten Zeilen zeigt die Ausgabe von `git diff`
mit einer entsprechenden Syntax sowie farblich kodiert die
hinzugekommenen (grün) und entfernten (rot)
Wörter.footnote:[Standardmäßig sind
Wörter durch ein oder mehr Leerzeichen getrennt; Sie können aber einen
anderen regulären Ausdruck angeben, um zu bestimmen, was ein Wort ist:
`git diff --word-diff-regex=<regex>`. Siehe hierzu auch die
Man-Page `git-diff(1)`.] Das ist dann praktisch, wenn Sie in
einer Datei nur einzelne Wörter ändern, beispielsweise bei der
Korrektur von AsciiDoc- oder LaTeX-Dokumenten, denn ein Diff ist schwierig zu lesen,
wenn sich hinzugefügte und entfernte Zeile nur durch ein einziges Wort
unterscheiden:
[subs="macros,quotes"]
--------
$ *git diff*
...
- die Option \`--color-words` zur Verfgung steht. Statt der entfernten
+ die Option \`--color-words` zur Verfügung steht. Statt der entfernten
...
--------
Verwenden Sie hingegen die Option `--word-diff`, so werden nur geänderte
Wörter entsprechend markiert angezeigt; außerdem werden Zeilenumbrüche
ignoriert, was ebenfalls sehr praktisch ist, weil eine Neuausrichtung
der Wörter nicht als Änderung in die Diff-Ausgabe eingeht:
[subs="macros,quotes"]
--------
$ *git diff --word-diff*
...
--color-words zur [-Verfgung-]{+Verfügung+} steht.
...
--------
[TIP]
=================
Falls Sie viel mit Fließtext arbeiten, bietet es sich an, ein Alias zur
Abkürzung dieses Befehls einzurichten, so dass Sie beispielsweise nur
noch `git dw` eingeben müssen:
[subs="macros,quotes"]
------
$ *git config --global alias.dw "diff --word-diff"*
------
=================
[[sec.add-p]]
==== Commits schrittweise erstellen ====
Warum aber sollte man Commits schrittweise erstellen -- will man nicht
immer alle Änderungen auch einchecken?
Ja, natürlich will man seine Änderungen in der Regel vollständig
übernehmen. Es kann allerdings sinnvoll sein, sie in Schritten
einzupflegen, um etwa die Entwicklungsgeschichte besser abzubilden.
Ein Beispiel: Sie haben in den vergangenen drei Stunden intensiv an
Ihrem Software-Projekt gearbeitet, haben aber, weil es so spannend
war, vergessen, die vier neuen Features in handliche Commits zu
verpacken. Zudem sind die Features über diverse Dateien verstreut.
Im besten Fall wollen Sie also selektiv arbeiten, d.h. nicht alle
Veränderungen aus einer Datei in einen Commit übernehmen, sondern nur
bestimmte Zeilen (Funktionen, Definitionen, Tests, ...), und das auch
noch aus verschiedenen Dateien.
Der Index von Git bietet dafür die gewünschte Flexibilität. Sie
sammeln einige Änderungen im Index und verpacken sie in einem Commit
-- alle anderen Änderungen bleiben aber nach wie vor in den Dateien
erhalten.
Wir wollen das anhand des ``Hello World!''-Beispiels aus dem
vorigen Kapitel illustrieren. Zur Erinnerung der Inhalt der Datei
`hello.pl`:
--------
# Hello World! in Perl
print "Hello World!\n";
--------
Nun präparieren wir die Datei so, dass sie mehrere unabhängige
Veränderungen hat, die wir 'nicht' in einem einzelnen Commit
zusammenfassen wollen. Zunächst fügen wir eine 'Shebang'-Zeile
am Anfang hinzu.footnote:[Das ist eine Anweisung für
den Kernel, welches Programm zum Interpretieren des Scripts verwendet
werden soll. Typische Shebang-Zeilen sind etwa `#!/bin/sh` oder `#!/usr/bin/perl`.] Außerdem
kommt eine Zeile hinzu, die den Autor benennt, sowie eine
Perl-Anweisung `use strict`, die den Perl-Interpreter anweist,
bei der Syntaxanalyse möglichst streng zu sein. Wichtig ist für unser
Beispiel, dass die Datei an mehreren Stellen verändert wurde:
--------
#!/usr/bin/perl
# Hello World! in Perl
# Author: Valentin Haenel
use strict;
print "Hello World!\n";
--------
Mit einem einfachen `git add hello.pl` würden alle neuen Zeilen
dem Index hinzugefügt -- der Stand der Datei im Index wäre also der
gleiche wie im Working Tree. Stattdessen verwenden wir die Option
`--patch` bzw. kurz `-p`.footnote:[Genaugenommen führt die Option
`-p` direkt in den 'Patch-Mode' des
'Interactive-Mode' von `git add`. Der Interactive-Mode
wird aber in der Praxis -- im Gegensatz zu dem Patch-Mode -- sehr
selten verwendet und ist deswegen hier nicht weiter beschrieben. Die
Dokumentation dazu finden Sie in der Man-Page `git-add(1)` im
Abschnitt ``Interactive Mode''.] Dies hat zur Folge, dass
wir interaktiv gefragt werden, welche Veränderungen wir dem Index
hinzufügen wollen. Git bietet uns jede Veränderung einzeln an, und wir
können von Fall zu Fall entscheiden, wie wir mit dieser verfahren
wollen:
[subs="macros,quotes"]
--------
$ *git add -p*
diff --git a/hello.pl b/hello.pl
index c6f28d5..908e967 100644
--- a/hello.pl
pass:quotes[\+++ b/hello.pl]
@@ -1,2 +1,5 @@
+\#!/usr/bin/perl
# Hello World! in Perl
+# Author: Valentin Haenel
+use strict;
print "Hello World!\n";
Stage this hunk [y,n,q,a,d,/,s,e,?]?
--------
Hier zeigt Git alle Änderungen an, da sie im Code sehr nah
beieinander liegen. Bei weit auseinanderliegenden oder auf
verschiedene Dateien verteilten Veränderungen werden sie getrennt
angeboten. Der Begriff 'Hunk' bezeichnet lose zusammenhängende
Zeilen im Quellcode. Wir haben an dieser Stelle unter anderem folgende Optionen:
--------
Stage this hunk[y,n,q,a,d,/,s,e,?]?
--------
Die Optionen sind jeweils nur einen Buchstaben lang und schwierig zu
merken. Eine kleine Erinnerung erhalten Sie immer durch '[?]'.
Die wichtigsten Optionen haben wir im Folgenden
zusammengefasst.
`y` ('yes'):: Übernimm den aktuellen Hunk in den Index.
`n` ('no'):: Übernimm den aktuellen Hunk nicht.
`q` ('quit'):: Übernimm weder den aktuellen Hunk noch einen der folgenden.
`a` ('all'):: Übernimm den aktuellen Hunk und alle, die folgen (in der aktuellen Datei).
`s` ('split'):: Versuche, den aktuellen Hunk zu teilen.
`e` ('edit'):: Editiere den aktuellen Hunk.footnote:[Git öffnet dann
den Hunk in einem Editor; unten sehen Sie eine Anleitung, wie Sie den
Hunk editieren: Um gelöschte Zeilen (mit `-` präfigiert) zu löschen –
also nicht dem Index hinzuzufügen, sie aber im Working Tree zu
behalten! –, ersetzen Sie das Minuszeichen durch ein Leerzeichen (die
Zeile wird zu ``Kontext''). Um `+`-Zeilen zu löschen, entfernen Sie
diese einfach aus dem Hunk.]
In dem Beispiel teilen wir den aktuellen Hunk und geben
`s` für 'split' ein.
[subs="macros,quotes"]
--------
Stage this hunk [y,n,q,a,d,/,s,e,?]? *[s]*
Split into 2 hunks.
@@ -1 +1,2 @@
+#!/usr/bin/perl
# Hello World! in Perl
--------
Git bestätigt, dass der Hunk erfolgreich geteilt werden konnte, und
bietet uns nun ein Diff an, das nur die Shebang-Zeile
enthält.footnote:[Sie können Hunks in der Regel
aber nicht beliebig teilen. Zumindest eine Zeile 'Kontext',
also eine Zeile ohne Präfix `+` oder `-`, muss
dazwischen liegen. Wollen Sie den Hunk dennoch teilen, müssen Sie
mit `e` für 'edit' arbeiten.] Wir geben `y` für
'yes' an und beim nächsten Hunk `q` für 'quit'. Um
zu überprüfen, ob alles geklappt hat, verwenden wir `git diff`
mit der Option `--staged`, die den Unterschied zwischen
Index und `HEAD` (dem neuesten Commit)
anzeigt:
[subs="macros,quotes"]
--------
$ *git diff --staged*
diff --git a/hello.pl b/hello.pl
index c6f28d5..d2cc6dc 100644
--- a/hello.pl
pass:quotes[\+++ b/hello.pl]
@@ -1,2 +1,3 @@
+#!/usr/bin/perl
# Hello World! in Perl
print "Hello World!\n";
--------
Um zu sehen, welche Veränderungen sich noch 'nicht' im Index
befinden, reicht ein einfacher Aufruf von `git diff`, der uns
zeigt, dass sich -- wie erwartet -- noch zwei Zeilen im Working Tree
befinden:
[subs="macros,quotes"]
--------
$ *git diff*
diff --git a/hello.pl b/hello.pl
index d2cc6dc..908e967 100644
--- a/hello.pl
pass:quotes[\+++ b/hello.pl]
@@ -1,3 +1,5 @@
\#!/usr/bin/perl
# Hello World! in Perl
+# Author: Valentin Haenel
+use strict;
print "Hello World!\n";
--------
An dieser Stelle könnten wir einen Commit erzeugen, wollen zur
Demonstration aber noch einmal von vorn beginnen. Darum setzen wir
mit `git reset HEAD` den Index zurück.
[subs="macros,quotes"]
--------
$ *git reset HEAD*
Unstaged changes after reset:
M hello.pl
--------
Git bestätigt und nennt die Dateien, in denen sich Veränderungen
befinden; in diesem Fall ist es nur die eine.
Das Kommando `git reset` ist gewissermaßen das Gegenstück zu
`git add`: Statt Unterschiede aus dem Working Tree in den Index
zu übertragen, überträgt `reset` Unterschiede aus dem
Repository in den Index. Änderungen 'in den' Working Tree zu
übertragen, ist möglicherweise destruktiv, da Ihre Änderungen
verlorengehen könnten. Daher ist dies nur mit der Option
`--hard` möglich, die wir in <<sec.reset>>
behandeln.
Sollten Sie häufiger `git add -p` verwenden, ist es nur eine
Frage der Zeit, bis Sie versehentlich einen Hunk auswählen, den Sie
eigentlich gar nicht wollten. Sollte der Index leer gewesen sein, ist
dies kein Problem, da Sie ihn ja zurücksetzen können, um von vorn
anzufangen. Problematisch wird es erst, wenn Sie bereits viele
Veränderungen im Index aufgezeichnet haben und diese nicht verlieren
möchten, Sie also einen bestimmten Hunk aus dem Index entfernen, ohne
die anderen Hunks anfassen zu wollen.
Analog zu `git add -p` gibt es daher den Befehl `git
reset -p`, der einzelne Hunks wieder aus dem Index entfernt. Um das
zu demonstrieren, übernehmen wir zunächst alle Veränderungen mit
`git add hello.pl` und starten `git reset -p`.
[subs="macros,quotes"]
--------
$ *git reset -p*
diff --git a/hello.pl b/hello.pl
index c6f28d5..908e967 100644
--- a/hello.pl
pass:quotes[\+++ b/hello.pl]
@@ -1,2 +1,5 @@
+\#!/usr/bin/perl
# Hello World! in Perl
+# Author: Valentin Haenel
+use strict;
print "Hello World!\n";
Unstage this hunk [y,n,q,a,d,/,s,e,?]?
--------
Wie bei dem Beispiel mit `git add -p` bietet Git nach und nach
Hunks an, jedoch sind es diesmal alle Hunks im Index. Entsprechend
lautet die Frage: `Unstage this hunk [y,n,q,a,d,/,s,e,?]?`, also
ob wir den Hunk wieder aus dem Index herausnehmen möchten. Wie gehabt,
erhalten wir durch die Eingabe des Fragezeichens eine erweiterte
Beschreibung der verfügbaren Optionen. Wir drücken an dieser Stelle
einmal `s` für 'split', einmal `n` für 'no'
und einmal `y` für 'yes'. Damit sollte sich jetzt nur die
Shebang-Zeile im Index befinden:
[subs="macros,quotes"]
--------
$ *git diff --staged*
diff --git a/hello.pl b/hello.pl
index c6f28d5..d2cc6dc 100644
--- a/hello.pl
pass:quotes[\+++ b/hello.pl]
@@ -1,2 +1,3 @@
+#!/usr/bin/perl
# Hello World! in Perl
print "Hello World!\n";
--------
[TIP]
=================
Bei den interaktiven Modi von `git add` und `git
reset` müssen Sie nach Eingabe einer Option die Enter-Taste
drücken. Mit folgender Konfigurationseinstellung sparen Sie sich
diesen zusätzlichen Tastendruck.
[subs="macros,quotes"]
--------
$ *git config --global interactive.singlekey true*
--------
=================
Ein Wort der Warnung:
Ein `git add -p` kann dazu verleiten, Versionen einer Datei
einzuchecken, die nicht lauffähig oder syntaktisch korrekt sind
(z.B. weil Sie eine wesentliche Zeile vergessen haben). Verlassen
Sie sich daher nicht darauf, dass Ihr Commit korrekt ist, nur weil
`make` -- was auf den Dateien des Working Tree arbeitet! --
erfolgreich durchläuft. Auch wenn ein späterer Commit das Problem
behebt, stellt dies unter anderem bei der automatisierten Fehlersuche
via Bisect (siehe <<sec.bisect>>) ein Problem dar.
[[sec.commit]]
==== Commits erstellen ====
Sie wissen nun, wie Sie Änderungen zwischen Working Tree, Index und
Repository austauschen. Wenden wir uns nun dem Kommando `git
commit` zu, mit dem Sie Änderungen im Repository
``festschreiben''.
Ein Commit hält den Stand aller Dateien Ihres Projekts zu einem
bestimmten Zeitpunkt fest und enthält zudem
Metainformationen:footnote:[Sie können
diese Informationen u.a. in `gitk` sehen oder mit dem
Kommando `git log --pretty=fuller`.]
* Name des Autors und E-Mail-Adresse
* Name des Committers und E-Mail-Adresse
* Erstellungsdatum
* Commit-Datum
Tatsächlich ist es so, dass der Name des Autors 'nicht' der Name
des Committers (der den Commit einpflegt) sein muss. Häufig werden
Commits von Maintainern integriert oder bearbeitet (z.B.
durch `rebase`, was auch die Committer-Informationen anpasst,
siehe <<sec.rebase>>). Die Committer-Informationen sind aber
in der Regel von nachrangiger Bedeutung -- die meisten Programme
zeigen nur den Autor und das Datum der Commit-Erstellung an.
Wenn Sie einen Commit erstellen, verwendet Git die im vorherigen
Abschnitt konfigurierten Einstellungen `user.name` und
`user.email`, um den Commit zu kennzeichnen.
Bei einem Aufruf von `git commit` ohne zusätzliche Argumente
fasst Git alle Veränderungen im Index zu einem Commit zusammen und
öffnet einen Editor, mit dem Sie eine Commit-Message erstellen. Die
Nachricht enthält jedoch immer eine mit Rautezeichen (`#`)
auskommentierte Anleitung bzw. Informationen darüber, welche Dateien
durch den Commit geändert werden. Rufen Sie `git commit -v`
auf, erhalten Sie unterhalb der Anleitung noch ein Diff der
Änderungen, die Sie einchecken werden. Das ist vor allem praktisch, um
einen Überblick über die Änderungen zu behalten und die
Auto-Vervollständigungsfunktion Ihres Editors zu verwenden.
Sobald Sie den Editor beenden, erstellt Git den Commit. Geben Sie
keine Commit-Nachricht an oder löschen den gesamten Inhalt der Datei,
bricht Git ab und erstellt keinen Commit.
Wollen Sie nur eine Zeile schreiben, bietet sich die Option
`--message` oder kurz `-m` an, mit der Sie direkt auf
der Kommandozeile die Nachricht angeben und so den Editor umgehen:
[subs="macros,quotes"]
--------
$ *git commit -m "Dies ist die Commit-Nachricht"*
--------
[[sec.ci-amend]]
===== Einen Commit verbessern =====
Wenn Sie vorschnell `git commit` eingegeben haben, den Commit aber noch geringfügig verbessern wollen, hilft die
Option `--amend` (``berichtigen''). Die Option
veranlasst Git, die Änderungen im Index dem eben getätigten Commit
``hinzuzufügen''.footnote:[Tatsächlich erstellt Git einen neuen Commit, dessen Änderungen eine Kombination der Änderungen des alten Commits und des Index ist. Der neue Commit 'ersetzt' dann den alten.] Außerdem können Sie die
Commit-Nachricht anpassen. Beachten Sie, dass sich die SHA-1-Summe des
Commits in jedem Fall ändert.
Mit dem Aufruf `git commit --amend` verändern Sie nur den
aktuellen Commit auf einem Branch. Wie Sie weiter zurückliegende
Commits verbessern, beschreibt <<sec.rebase-onto-ci-amend>>.
[TIP]
============
Der Aufruf von `git commit --amend` startet automatisch einen Editor, so
dass Sie auch noch die Commit-Nachricht bearbeiten können. Häufig wollen
Sie aber nur noch eine kleine Korrektur an einer Datei vornehmen, ohne die
Nachricht anzupassen. Für die Autoren bewährt sich in dieser Situation
ein Alias `fixup`:
[subs="macros,quotes"]
-------
$ *git config --global alias.fixup "commit --amend --no-edit"*
-------
============
[[sec.commit-msg]]
===== Gute Commit-Nachrichten =====
Wie sollte eine Commit-Nachricht aussehen? An der äußeren Form lässt
sich nicht viel ändern: Die Commit-Nachricht muss mindestens eine
Zeile lang sein, die am besten aber maximal 50 Zeichen umfasst. Das
macht Auflistungen der Commits besser lesbar. Sofern Sie eine
genauere Beschreibung hinzufügen wollen (was äußerst empfehlenswert
ist!), trennen Sie diese von der ersten Zeile durch eine Leerzeile.
Keine Zeile sollte -- wie auch bei E-Mails üblich -- länger als 76
Zeichen sein.
Commit-Nachrichten folgen oft den Gewohnheiten oder Besonderheiten
eines Projekts. Möglicherweise gibt es Konventionen, wie zum Beispiel
Referenzen zum Bugtracking- oder Ticket-System oder ein Link zur
entsprechenden API-Dokumentation.
Beachten Sie die folgenden Punkte beim Verfassen einer
Commit-Beschreibung:
* Erstellen Sie niemals leere Commit-Nachrichten. Auch
Commit-Nachrichten wie `Update`, `Verbesserung`,
`Fix` etc. sind ebenso aussagekräftig wie eine leere
Nachricht -- dann können Sie es auch gleich lassen.
* Ganz wichtig: Beschreiben Sie, 'warum' etwas verändert
wurde und welche Implikationen das haben kann. 'Was' verändert
wurde, ist immer aus dem Diff ersichtlich!
* Seien Sie kritisch und vermerken Sie, wenn Sie glauben,
dass noch Verbesserungsbedarf besteht oder der Commit möglicherweise
an anderer Stelle Fehler einführt.
* Die erste Zeile sollte nicht länger als 50 Zeichen sein,
damit bleibt die Ausgabe der Versionsgeschichte stets gut formatiert
und lesbar.
* Wird die Nachricht länger, sollte in der ersten Zeile eine
kurze Zusammenfassung (mit den wichtigen Schlagwörtern) stehen.
Nach einer Leerzeile folgt dann eine umfangreiche Beschreibung.
Wir können nicht häufig genug betonen, wie wichtig eine gute
Commit-Beschreibung ist. Beim Commit sind einem Entwickler die
Änderungen noch gut im Gedächtnis, aber schon nach wenigen Tagen ist
die Motivation dahinter oft vergessen. Auch Ihre Kollegen oder
Projektmitstreiter werden es Ihnen danken, weil sie Änderungen viel
schneller erfassen können.
Eine gute Commit-Nachricht zu schreiben hilft auch, kurz darüber zu
reflektieren, was schon geschafft ist und was noch ansteht. Vielleicht
merken Sie beim Schreiben, dass Sie noch ein wesentliches Detail
vergessen haben.
Man kann auch über eine Zeitbilanz argumentieren: Die Zeit, die Sie
benötigen, um eine gute Commit-Nachricht zu schreiben, beläuft sich
auf ein bis zwei Minuten. Um wie viel Zeit wird sich die Fehlersuche
aber verringern, wenn jeder Commit gut dokumentiert ist? Wie viel Zeit
sparen Sie anderen (und sich selbst), wenn Sie zu einem --
möglicherweise schwer verständlichen -- Diff noch eine gute
Beschreibung mitliefern? Auch das Blame-Tool, das jede Zeile einer Datei mit
dem Commit, der sie zuletzt geändert hat, annotiert, wird bei
ausführlichen Commit-Beschreibungen zu einem unerlässlichen Hilfsmittel
werden (siehe <<sec.blame>>).
Wenn Sie nicht gewöhnt sind, ausführliche Commit-Nachrichten zu
schreiben, fangen Sie heute damit an. Übung macht den Meister, und
wenn Sie sich erst einmal daran gewöhnt haben, geht die Arbeit schnell
von der Hand -- Sie selbst und andere profitieren davon.
Das Repository des Git-Projekts ist ein Paradebeispiel für gute
Commit-Nachrichten. Ohne Details von Git zu kennen, wissen Sie schnell,
wer warum was geändert hat. Außerdem sieht man, durch wie viele Hände
solch ein Commit geht, bevor er integriert wird.
Leider sind die Commit-Nachrichten in den meisten Projekten dennoch
sehr spartanisch gehalten; seien Sie also nicht enttäuscht, wenn Ihre
Mitstreiter schreibfaul sind, sondern gehen Sie mit gutem Beispiel und
ausführlichen Beschreibungen voran.
[[sec.git-mv-rm]]
==== Dateien verschieben und löschen ====
Wenn Sie Dateien, die von Git verwaltet werden, löschen oder
verschieben wollen, dann verwenden Sie dafür `git rm` bzw.
`git mv`. Sie wirken wie die regulären Unix-Kommandos,
modifizieren aber darüber hinaus den Index, so dass die Aktion in den
nächsten Commit einfließt.footnote:[Durch
`git rm` löschen Sie eine Datei mit dem nächsten Commit; sie
bleibt jedoch im Commit-Verlauf erhalten. Wie man eine Datei
vollständig, also auch aus der Versionsgeschichte, löscht, ist in
<<sec.fb-censor>> nachzulesen.]
Analog zu den Standard-Unix-Kommandos akzeptiert `git rm` auch
die Optionen `-r` und `-f`, um rekursiv zu löschen bzw.
das Löschen zu erzwingen. Auch `git mv` bietet eine Option
`-f` ('force'), falls der neue Dateiname schon existiert
und überschrieben werden soll. Beide Kommandos akzeptieren die Option
`-n` bzw. `--dry-run`, die bewirkt, dass der Vorgang
simuliert wird, Dateien also nicht modifiziert werden.
[TIP]
================
Um eine Datei 'nur' aus dem Index zu löschen, verwenden Sie
`git rm --cached`. Sie bleibt dann im Working Tree
erhalten.
================
Sie werden häufiger vergessen, eine Datei über `git mv` zu
verschieben oder per `git rm` zu löschen, und stattdessen die
Standard-Unix-Kommandos verwenden. In diesem Fall markieren Sie die
(schon per `rm` gelöschte) Datei einfach auch als gelöscht im
Index, und zwar per `git rm <datei>`.
Für eine Umbenennung gehen Sie so vor: Markieren Sie zunächst den
alten Dateinamen per `git rm <alter-name>` als gelöscht. Fügen
Sie dann die neue Datei hinzu: `git add <neuer-name>`.
Überprüfen Sie anschließend per `git status`, ob die Datei als
``umbenannt'' gekennzeichnet ist.
[TIP]
================
Intern spielt es für Git keine Rolle, ob Sie eine Datei regulär per
`mv` verschieben, dann `git add <neuer-name>` und `git rm
<alter-name>` ausführen. In jedem Fall wird lediglich die Referenz auf
ein Blob-Objekt geändert (siehe <<sec.objektmodell>>).
Git kommt allerdings mit einer sogenannten 'Rename Detection': Wenn
ein Blob gleich ist und nur von einem anderen Dateinamen referenziert
wird, dann fasst Git dies als eine Umbenennung auf. Wollen Sie die
Geschichte einer Datei untersuchen und ihr bei eventuellen
Umbenennungen folgen, verwenden Sie das folgende Kommando:
[subs="macros,quotes"]
--------
$ *git log --follow -- <datei>*
--------
================
[[sec.grep]]
==== grep auf einem Repository ====
Wenn Sie nach einem Ausdruck in allen Dateien Ihres Projektes suchen
wollen, bietet sich normalerweise ein Aufruf von `grep -R
<ausdruck> .` an.
Git bietet allerdings ein eigenes Grep-Kommando, das Sie per
`git grep <ausdruck>` aufrufen. In der Regel sucht das
Kommando den Ausdruck in allen von Git verwalteten Dateien. Wollen Sie
stattdessen nur einen Teil der Dateien untersuchen, können Sie das
Muster explizit angeben. Mit folgendem Kommando finden Sie alle
Vorkommnisse von `border-color` in allen CSS-Dateien:
[subs="macros,quotes"]
--------
$ *git grep border-color -- \'*.css'*
--------
Die Grep-Implementation von Git unterstützt alle gängigen Flags, die
auch in GNU Grep vorhanden sind. Allerdings ist ein Aufruf von
`git grep` in der Regel um eine Größenordnung schneller, da Git
durch die Objektdatenbank sowie das Multithread-Design des Kommandos
wesentliche Performance-Vorteile hat.
[TIP]
=============
Die populäre `grep`-Alternative `ack` zeichnet sich vor allem dadurch
aus, dass es die auf das Suchmuster passenden Zeilen einer Datei unter
einer entsprechenden ``Überschrift'' zusammenfasst, sowie prägnante
Farben verwendet. Sie können die Ausgabe von `ack` mit `git grep`
emulieren, indem Sie folgendes Alias verwenden:
[subs="macros,quotes"]
-------
$ *git config alias.ack '!git -c color.grep.filename="green bold" \*
*-c color.grep.match="black yellow" -c color.grep.linenumber="yellow bold" \*
*grep -n --break --heading --color=always --untracked'*
-------
=============
[[sec.git-log]]
==== Die Projektgeschichte untersuchen ====
Mit `git log` untersuchen Sie die Versionsgeschichte des
Projekts. Die Optionen dieses Kommandos (die großteils auch für
`git show` funktionieren) sind sehr umfangreich, wir werden im
Folgenden die wichtigsten vorstellen.
Ohne weitere Argumente gibt `git log` für jeden Commit Autor,
Datum, Commit-ID sowie die komplette Commit-Nachricht aus. Das ist
dann praktisch, wenn Sie einen schnellen Überblick benötigen, wer wann
was gemacht hat. Allerdings ist die Liste etwas unhandlich, sobald Sie
viele Commits betrachten.
Wollen Sie nur die kürzlich erstellten Commits anschauen, begrenzen Sie die
Ausgabe von `git log` durch die Option `-<n>` auf 'n'
Commits. Die letzten vier Commits erhalten Sie zum Beispiel mit:
[subs="macros,quotes"]
--------
$ *git log -4*
--------
Um einen einzelnen Commit anzuzeigen, geben Sie stattdessen ein:
[subs="macros,quotes"]
--------
$ *git log -1 <commit>*
--------
Das Argument `<commit>` ist eine legale Bezeichnung für einen einzelnen
Commit, z.B. die Commit-ID bzw. SHA-1-Summe. Wenn Sie jedoch
nichts angeben, verwendet Git automatisch `HEAD`. Abgesehen von einzelnen
Commits versteht das Kommando allerdings auch sog. 'Commit-Ranges' (Reihe
von Commits), siehe <<sec.commit-ranges-intro>>.
Die Option `-p` (`--patch`) fügt den vollen Patch im
Unified-Diff-Format unter der Beschreibung an. Damit ist also ein
`git show <commit>` von der Ausgabe äquivalent zu `git
log -1 -p <commit>`.
Wollen Sie die Commits in komprimierter Form anzeigen, empfiehlt sich
die Option `--oneline`: Sie fasst jeden Commit mit seiner
abgekürzten SHA-1-Summe und der ersten Zeile der Commit-Nachricht
zusammen. Daher ist es wichtig, dass Sie in dieser Zeile möglichst
hilfreiche Informationen verpacken! Das sieht dann zum Beispiel so
aus:footnote:[Dieses und die folgenden
Beispiele stammen aus dem Git-Repository.]
[subs="macros,quotes"]
--------
$ *git log --oneline*
*25f3af3* Correctly report corrupted objects
*786dabe* tests: compress the setup tests
*91c031d* tests: cosmetic improvements to the repo-setup test
*b312b41* exec_cmd: remove unused extern
--------
Die Option `--oneline` ist nur ein Alias für
`--pretty=oneline`. Es gibt noch andere Möglichkeiten, die
Ausgabe von `git log` anzupassen. Die möglichen Werte für die
Option `--pretty` sind:
`oneline`:: Commit-ID und erste Zeile der Beschreibung
`short`:: Commit-ID, erste Zeile der
Beschreibung sowie Autor des Commits; Ausgabe umfasst vier Zeilen.
`medium`:: Default; Ausgabe von Commit-ID, Autor,
Datum und kompletter Beschreibung.
`full`:: Commit-ID, Name des Autors, Name des
Committers und vollständige Beschreibung -- 'kein' Datum.
`fuller`:: Wie `medium`, aber zusätzlich
Datum und Name des Committers.
`email`:: Formatiert die Informationen von
`medium` so, dass sie wie eine E-Mail aussehen.
`format:<string>`:: Durch Platzhalter beliebig
anpassbares Format; für Details siehe die Man-Page `git-log(1)`,
Abschnitt ``Pretty Formats''.
Unabhängig davon können Sie unterhalb der Commit-Nachricht weitere
Informationen über die Veränderungen durch den Commit ausgeben.
Betrachten Sie folgende Beispiele, in denen deutlich wird, welche
Dateien an wie vielen Stellen geändert wurden:
[subs="macros,quotes"]
--------
$ *git log -1 --oneline 4868b2ea*
4868b2e setup: officially support --work-tree without --git-dir
$ git log -1 --oneline *--name-status* 4868b2ea
4868b2e setup: officially support --work-tree without --git-dir
M setup.c
M t/t1510-repo-setup.sh
$ git log -1 --oneline *--stat* 4868b2ea
4868b2e setup: officially support --work-tree without --git-dir
setup.c | 19 +++++
t/t1510-repo-setup.sh | 210 pass:quotes[+++\+\+\+\+\+\+\+\+\+\+\+\+\+\+\++++++------------------]
2 files changed, 134 insertions(+), 95 deletions(-)
$ git log -1 --oneline *--shortstat* 4868b2ea
4868b2e setup: officially support --work-tree without --gi-dir
2 files changed, 134 insertions(+), 95 deletions(-)
--------
[[sec.git-log-dates]]
===== Zeitliche Einschränkungen =====
Sie können die anzuzeigenden Commits zeitlich eingrenzen, und zwar mit
den Optionen `--after` bzw. `--since` sowie
`--until` bzw. `--before`. Die Optionen sind jeweils
synonym, liefern also dieselben Ergebnisse.
Sie können absolute Daten in jedem gängigen Format angeben oder auch
relative Daten, hier einige Beispiele:
[subs="macros,quotes"]
--------
$ *git log --after='Tue Feb 1st, 2011'*
$ *git log --since='2011-01-01'*
$ *git log --since='two weeks ago' --before='one week ago'*
$ *git log --since='yesterday'*
--------
[[sec.git-log-files]]
===== Einschränkungen auf Dateiebene =====
Geben Sie nach einem `git log`-Aufruf einen oder mehrere Datei-
oder Verzeichnisnamen an, wird Git nur die Commits anzeigen, die
zumindest eine der angegebenen Dateien betrifft. Gute Strukturierung
eines Projekts vorausgesetzt, lässt sich die Ausgabe der Commits stark
begrenzen und eine bestimmte Änderung rasch finden.
Da Dateinamen möglicherweise mit Branches oder Tags kollidieren,
sollten Sie die Dateinamen sicherheitshalber nach einem `--`
angeben, der besagt, dass nur noch Datei-Argumente folgen.
[subs="macros,quotes"]
--------
$ *git log -- main.c*
$ *git log -- *.h*
$ *git log -- Documentation/*
--------
Diese Aufrufe geben nur die Commits aus, in denen Änderungen an der
Datei `main.c`, einer `.h`-Datei respektive an einer
Datei unterhalb von `Documentation/` vorgenommen wurden.
[[sec.git-log-grep]]
===== grep für Commits =====
Sie können auch im Stile von `grep` nach Commits suchen; hier
stehen die Optionen `--author`, `--committer` und
`--grep` zur Verfügung.
Die ersten beiden Optionen filtern die Commits erwartungsgemäß nach
Autor- bzw. Committer-Name oder -Adresse. So listen Sie zum Beispiel alle
Commits, die Linus Torvalds seit Anfang 2010 gemacht hat:
[subs="macros,quotes"]
--------
$ *git log --since='2010-01-01' --author='Linus Torvalds'*
--------
Hier können Sie auch nur Teile des Namens bzw. der E-Mail-Adresse angeben; die
Suche nach `'Linus'` würde also dasselbe Ergebnis produzieren.
Mit `--grep` suchen Sie zum Beispiel nach Schlagwörtern oder
Satzteilen in der Commit-Nachricht, etwa nach allen Commits, in denen
das Wort ``fix'' vorkommt (ohne die Groß- und Kleinschreibung
zu beachten):
[subs="macros,quotes"]
--------
$ *git log -i --grep=fix*
--------
Die Option `-i` (bzw. `--regexp-ignore-case`) bewirkt, dass
`git log` die Groß- und Kleinschreibung des Musters ignoriert
(funktioniert auch in Verbindung mit `--author` und
`--committer`).
Alle drei Optionen behandeln die Werte -- wie `grep` auch --
als reguläre Ausdrücke (siehe die Man-Page `regex(7)`). Durch
`-E` und `-F` wird das Verhalten der
Optionen analog zu `egrep` und `fgrep` umgestellt:
erweiterte reguläre Ausdrücke zu verwenden bzw. nach dem literalen
Suchterm (dessen spezielle Zeichen ihre Bedeutung verlieren) zu suchen.
[TIP]
================
Um nach 'Änderungen' zu suchen, verwenden Sie das sog. 'Pickaxe'-Tool
(``Spitzhacke''). So finden Sie Commits, in deren Diff ein bestimmter
regulärer Ausdruck vorkommt (```grep` für Diffs''):
[subs="macros,quotes"]
--------
$ *git log -p -G<regex>*
--------
Der `<regex>` ist direkt, d.h. ohne Leerzeichen, nach der
Pickaxe-Option `-G` anzugeben. Die Option `--pickaxe-all` bewirkt, dass
alle Veränderungen des Commits aufgelistet werden, nicht nur
diejenigen, die die gesuchte Änderung enthalten.
Beachten Sie, dass in früheren Git-Versionen für diese Operation die
Option `-S` zuständig war, die allerdings einen Unterschied zu
`-G` aufweist: Sie findet nur die Commits, die die 'Anzahl' der
Vorkommnisse des Musters ändern -- insbesondere werden
Code-Verschiebungen, also Entfernen und Hinzufügen an anderer Stelle in
einer Datei, nicht gefunden.
================
Mit diesen Werkzeugen gerüstet, können Sie nun selbst Massen von
Commits bändigen. Geben Sie nur entsprechend viele Kriterien an, um
die Anzahl der Commits zu verringern.
[[sec.commit-ranges-intro]]
==== Commit-Ranges ====
Bisher haben wir lediglich Kommandos betrachtet, die nur einen
einzelnen Commit als Argument fordern, explizit identifiziert durch
seine Commit-ID oder implizit durch den symbolischen Namen
`HEAD`, der den jeweils aktuellsten Commit referenziert.
Das Kommando `git show` zeigt Informationen zu einem Commit an,
das Kommando `git log` beginnt bei einem Commit, und geht dann
so weit in der Versionsgeschichte zurück, bis der Anfang des
Repositorys (der sogenannte 'Root-Commit') erreicht ist.
Ein wichtiges Hilfsmittel, um eine Reihe von Commits anzugeben, sind
sogenannte Commit-Ranges der Form `<commit1>..<commit2>`. Da
wir bislang noch nicht mit mehreren Branches (Zweigen) arbeiten,
ist dies einfach ein Ausschnitt der Commits in einem Repository, und
zwar von `<commit1>` exklusive bis `<commit2>`
inklusive. Sofern Sie eine der beiden Grenzen weglassen, nimmt Git
dafür den Wert `HEAD` an.
[[sec.git-diff]]
==== Unterschiede zwischen Commits ====
Das Kommando `git show` bzw. `git log -p` hat bisher
immer nur den Unterschied zu dem jeweils vorherigen Commit ausgegeben.
Wollen Sie die Unterschiede mehrerer Commits einsehen, hilft
das Kommando `git diff`.
Das Diff-Kommando erfüllt mehrere Aufgaben. Wie bereits gesehen,
können Sie ohne weitere Angabe von Commits die Unterschiede zwischen
Working Tree und Index bzw. mit der Option `--staged` die
Unterschiede zwischen Index und `HEAD` untersuchen.
Wenn Sie dem Kommando aber zwei Commits bzw. eine Commit-Range
übergeben, wird stattdessen der Unterschied zwischen diesen
Commits angezeigt.
[[sec.objektmodell]]
=== Das Objektmodell ===
Git basiert auf einem simplen, aber äußerst mächtigen Objektmodell. Es
dient dazu, die typischen Elemente eines Repositorys (Dateien,
Verzeichnisse, Commits) und die Entwicklung über die Zeit abzubilden.
Das Verständnis dieses Modells ist von großer Bedeutung und hilft sehr
dabei, von typischen Git-Arbeitsschritten zu abstrahieren und sie
so besser zu verstehen.
Im Folgenden dient uns als Beispiel wieder ein ``Hello
World!''-Programm, diesmal in der Programmiersprache Python.footnote:[Sie
können das Repository, das auf den folgenden Seiten detailliert
untersucht wird, mit dem Befehl `git clone
git://github.com/gitbuch/objektmodell-beispiel.git` herunterladen.]