Montag, 11. April 2011

git - commits mit rebase zusammenfassen

Moin Moin,

das Versionskontrollsystem git bietet einen großen Umfang von Features um den Source Code eines Projektes professionell und sicher zu verwalten. Dieser Post behandelt einen Aspekt von git. Er richtet sich an Benutzer von git die mehr oder weniger täglich damit arbeiten und die Grundlagen bereits verinnerlicht haben.


Ziel dieses Posts


Bei der täglichen Arbeit, z.B. auf einem branch, kann es nützlich sein, mehrere commits zusammen zu fassen. Dafür bietet git den Befehl

$ git rebase -i

Wie git rebase funktioniert und angewendet wird, soll der Inhalt dieses Posts sein.


Testumgebung schaffen


Um die Funktionsweise von git rebase anschaulich erklären zu können, wird zuerst eine Testumgebung erstellt. Sprich, ein neuese git Repository wird erstellt.

$ mkdir gitrebase
$ cd gitrebase/
$ git init
Initialized empty Git repository in /tmp/gitrebase/.git/
$ touch file_with_content
$ git add .
$ git commit -m 'create testfile'
[master (root-commit) d1beb13] create testfile
 0 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 file_with_content

Die Schritte sind ziemlich straight forward. Es wird ein Verzeichnis erstellt, in welchem eine Datei für Testzwecke erstellt wird. Danach wird diese Datei dem Repository Index hinzugefügt und mit dem commit Befehl wird die Änderung im Repository aufgezeichnet. Der Befehl git log zeigt die Änderungen am Repository auf:

$ git log
commit d1beb13cfd94c134544a1e068fac24b2b6bd56b5
Author: Andreas Wenk <andreas.wenk@sinnerschrader.de>
Date:   Tue Apr 5 17:17:57 2011

    create testfile

Im nächsten Schritt wird die Datei einige Male geändert und die Änderungen werden im Log aufgezeichnet. Das Vorgehen folgt dabei diesem Muster:

$ vi file_with_content
$ git commit -a -m 'first change'
[master 7da887b] first change
 1 files changed, 1 insertions(+), 0 deletions(-)

Für unsere Betrachtungen genügen ein paar commits. Das Log sieht dann in etwa so aus:

$ git log
commit 847f8c7bf98c1e907b5c8ab1f08f3a9b38b2b40b
Author: Andreas Wenk <andreas.wenk@sinnerschrader.de>
Date:   Tue Apr 5 17:29:07 2011

    third change

commit 240da4170ad23f736c365d1c25435654e31a54c3
Author: Andreas Wenk <andreas.wenk@sinnerschrader.de>
Date:   Tue Apr 5 17:28:49 2011

    second change

commit 7da887ba6fb01ca3ac3f6a1928a45bef3c49e7d4
Author: Andreas Wenk <andreas.wenk@sinnerschrader.de>
Date:   Tue Apr 5 17:26:19 2011

    first change

commit d1beb13cfd94c134544a1e068fac24b2b6bd56b5
Author: Andreas Wenk <andreas.wenk@sinnerschrader.de>
Date:   Tue Apr 5 17:17:57 2011

    create testfile

Damit ist die Testumgebung erstellt.


Aus drei mach eins

In einem großen Projekt mit mehreren Entwicklern sollten unterschiedliche Aufgaben und Bereiche in branches bearbeitet und später, z.B. im master branch, zusammengeführt werden. Von diesem können dann Software Versionen erstellt werden und das Deployment, basierend auf diesen Versionen, gestartet werden.

Eine rule of thumb bei der Entwicklungsarbeit ist, viele kleine commits zu erstellen. Das erleichtert  das Nachvollziehen des Entwicklungsprozess, da der Entwickler meistens intuitiv zusammengehörige Schritte als commits erstellt. Ausserdem ist diese Vorgehensweise bei der Fehlersuche und einem notwendigen "zurückrollen" extrem hilfreich.

Viele commits bedeuten allerdings auch eine sehr große commit history. Um einer möglichen Flut vorzubeugen, kommt also git rebase ins Spiel um mehrere commits zusammen zu fassen. Die Änderungen am Dokument im hier besprochenen Beispiel sind alle ziemlich ähnlich und könnten somit in einen einzigen commit zusammengefasst werden.

Bei der konkreten Anwendung von git rebase wird ein Startpunkt erwartet, nach (!) welchem die commits zusammengefasst werden sollen. Die Option -i wird genutzt, um git rebase im interaktiven Modus auszuführen. In unserem Beispiel sollen alle commits nach dem initialen commit mit dem SHA-1 Hash d1beb13cfd94 zusammengefasst werden. Deshalb ist der Aufruf folgender:

$ git rebase -i d1beb13cfd94c13

Daraufhin erhält man folgende Ausgabe:

pick 7da887b first change
pick 240da41 second change
pick 847f8c7 third change

# Rebase d1beb13..847f8c7 onto d1beb13
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

Es besteht auch die Möglichkeit einen Bereich anzugeben:

$ git rebase -i d1beb13cfd94c13 240da4170ad23f7

pick 7da887b first change
pick 240da41 second change

# Rebase d1beb13..240da41 onto d1beb13
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

Hier würden also nur die beiden nächsten Commits betrachtet werden.

git gibt dem Benutzer im interaktiven Modus eine Erklärung, welche Commands genutzt werden können frei Haus mit. Übrigens wird diese Ansicht in dem für git definierten Editor geöffnet. Die Einstellung hierfür wird in $HOME/.gitconfig getroffen:

[core]
    editor = vim

Standardmäßig ist der Command für die einzelnen commits bei der Ausführung von rebase pick. Das bedeutet, die so markierten commits werden ohne Änderung übernommen. Die getroffenen Einstellungen sind also Anweisungen, was während der Ausführung von git rebase mit den einzelnen commits geschehen soll. Das Ziel hier soll sein, alle commit messages beizubehalten, aber alle commits in einen einzigen zusammen zu fassen, nämlich im commit 7da887ba6fb0. Die Anweisungen sehen also so aus:

p 7da887b first change
s 240da41 second change
s 847f8c7 third change

Bei einer großen Liste von commits kann festgelegt werden, dass irgend ein commit unberührt bleiben soll (p oder pick) oder einfach mit dem vorherigen verschmolzen werden soll ohne commit message (f oder fixup) oder die commit message während des rebase Vorgangs geändert werden soll (e oder edit) usw. . Allerdings muss in der Liste zumindest ein commit angegeben sein, mit welchem die anderen commits verschmolzen werden sollen. Ist dies nicht der Fall beschwert sich git mit:

Cannot 'squash' without a previous commit

Nach dem Speichern der Änderungen im Editor, wird der rebase Vorgang automatisch angestossen und wartet mit einer neuen Ausgabe auf:

# This is a combination of 3 commits.
# The first commit's message is:
first change

# This is the 2nd commit message:

second change

# This is the 3rd commit message:

third change

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# Not currently on any branch.
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
# modified:   file_with_content

Sinnvoll ist es an dieser Stelle, die einzelnen commit messages einfach bei zu behalten - ggf. mit Änderungen - um später im Log besser sehen zu können, was genau getan wurde. Wenn die Ausgabe im Editior gespeichert bzw. geschlossen wird, bestätigt git die erfolgreiche rebase Operation:

[detached HEAD e7edfb6] first change
 1 files changed, 5 insertions(+), 0 deletions(-)
Successfully rebased and updated refs/heads/master.

Schliesslich zeigt uns git log das Ergebnis:

$ git log
commit e7edfb61e1feed468a352207cfff48feb7f49903
Author: Andreas Wenk <andreas.wenk@sinnerschrader.de>
Date:   Tue Apr 5 17:26:19 2011

    first change
   
    second change
   
    third change

commit d1beb13cfd94c134544a1e068fac24b2b6bd56b5
Author: Andreas Wenk <andreas.wenk@sinnerschrader.de>
Date:   Tue Apr 5 17:17:57 2011

    create testfile

Interessant ist der zusammengefasste commit: der SHA-1 Wert des commit hat sich (natürlich) geändert aber die EInträge für Datum und Autor sind immer noch die gleichen wie beim ursprünglichen commit 7da887ba6fb01ca3a.

Ziel erreicht

Die hier gezeigte Nutzung von git rebase ist eine mögliche Nutzung von rebase. Es gibt noch einige weitere Nutzungsmöglichgkeiten. Dem Leser sei an dieser Stelle das übliche man git-reabse ans Herz gelegt - getreu dem Motto: RTFM.

Viel Spass und Erfolg bei der Nutzung von git.

Andy Wenk

1 Kommentar:

  1. Kennst du denn auch ORIG_HEAD nach git merge?

    Geht ungefähr so:

    $ # auf dem Feature-Branch
    $ git rebase master
    $ git checkout master
    $ git merge feature_branch
    $ git rebase -i ORIG_HEAD

    AntwortenLöschen