Modyfikacja historii w Gicie

4

W poście przedstawiającym Gita wspomniałem o możliwości modyfikacji historii – i dzisiaj więcej na ten temat. Jest to funkcjonalność naprawdę nie do przecenienia. Commit nie jest już czynnością ostateczną, z którą nie można nic zrobić, jak nas przyzwyczaił SVN. Wtedy przed puszczeniem zmian trzeba się było zastanawiać i analizować dokonane zmiany. Tutaj natomiast bardzo sensownym trybem pracy jest lokalne zatwierdzanie zmian tak często jak mamy na to ochotę – ja na przykład nienawidzę mieć jednocześnie zmodyfikowanych wielu plików co skutkuje bardzo częstymi commitami. Fakt, że mogę w dowolnej chwili zatrzymać sie i zmienić zaaplikowane wbitki daje baaardzo dużo swobody. Zamiast myśleć przed KAŻDYM commitem czy faktycznie jest już na niego pora, mogę owo myślenie zostawić na koniec dnia – gdy będę robił merge bądź publikował zmiany.

Poprzez MODYFIKACJĘ historii rozumiem: edytowanie komentarzy, rozbicie jednego commita na kilka mniejszych, złączenie kilku commitów w jeden większy, zmianę kolejności a nawet ich usunięcie! Git – power is back!

Wtrącenie: najwięcej da się wynieść z poniższych opisów wprowadzając je od razu w życie – zachęcam dlatego do zrobienia sobie repozytorium i eksperymentowania razem ze mną. Czytanie "na sucho" także powinno pewne rzeczy naświetlić, ale z pewnością poświęcony czas nie będzie tak owocnie wykorzystany.

Edycja ostatniego commita

Mi osobiście często zdarza się zakończyć jakiś refactoring, puścić commita i w tej samej minucie zobaczyć, że w jeszcze jednym pliku przydałaby się związana z tym zmiana. Rozwiązanie jest bardzo proste, wystarczy dodać odpowiednią opcję do komendy commit:

  1:  git add.
  2:  git commit --amend

Najpierw dodajemy nowe zmiany do stanu "do zacommitowania", a następnie dorzucamy je do poprzedniej wbitki.

W ten sam sposób można modyfikować wiadomość wysłaną ostatnim razem.

Edycja X ostatnich commitów

Ten scenariusz przedstawia niesamowicie potężną komendę: git rebase. Pozwala ona na dowolne właściwie mieszanie w historii repozytorium. Chcąc zmodyfikować trzy ostatnie commity wybieramy :

  1:  git rebase -i head~3

W efekcie uzyskamy w skonfigurowanym edytorze tekstu (o nim pisałem tu) otwarty plik tekstowy wyglądający mniej więcej tak:

  1:  pick cef78eb some commit
  2:  pick 16b5524 next commit
  3:  pick 9c93f7c current head
  4:  
  5:  # Rebase f482b98..9c93f7c onto f482b98
  6:  #
  7:  # Commands:
  8:  #  p, pick = use commit
  9:  #  e, edit = use commit, but stop for amending
 10:  #  s, squash = use commit, but meld into previous commit
 11:  #
 12:  # If you remove a line here THAT COMMIT WILL BE LOST.
 13:  # However, if you remove everything, the rebase will be aborted.
 14:  #

Wygenerowane Instrukcje mówią właściwie wszystko. Usunięcie commita z listy spowoduje wykasowanie go (oczywiście wraz z wprowadzonymi przezeń zmianami) z historii. Przeniesienie w górę/w dół to modyfikacja jego miejsca w historii. Zmiana "pick" na "s" to doklejenie commita do poprzedniego (można jednocześnie przenieść commit w czasie i dokleić go do innego!). A "e" sprawi, że w proces modyfikacji zatrzyma się na etapie aplikowania tej właśnie zmiany dając nam możliwość dowolnej jej modyfikacji poprzez przedstawiony wyżej commit –amend.

Rozbicie commita na kilka mniejszych

Ten scenariusz zasługuje na osobne potraktowanie. Do stanu przygotowanego do rozbicia commita na kilka mniejszych dochodzimy poprzez wywołanie powyższej instrukcji

  1:  git rebase -i ...

i wybranie komendy "edit" przy zbyt dużym commicie. Po zaakceptowaniu pliku z takimi komendami rozpocznie się proces nadpisywania historii. Z powodu naszego "e" Git zatrzyma się od razu PO zaaplikowaniu oznaczonego w ten sposób commita. Co to oznacza? Oprócz wspomnianej możliwości edycji wiadomości z nim wysłanej oraz dorzucenia dodatkowych modyfikacji, możemy (będąc cały czas w tym samym miejscu w historii) WYCOFAĆ zawarte w nim zmiany. Za pomocą komendy reset:

  1:  git reset head~1

Taka instrukcja spowoduje, że w working copy będziemy cały czas mieli zmodyfikowane pliki, jednak wyrzucimy je z historii. Dzięki temu możemy wybrać pliki, które powinny znaleźć się w pierwszym mniejszym commicie (poprzez git add) -> zaaplikować je (normalne git commit) -> przygotować kolejną wbitkę -> zaaplikować ->… i tak dalej, aż do oczyszczenia working copy ze zmian.

Na koniec instruujemy Gita, że zakończyliśmy modyfikację:

  1:  git rebase --continue

…i nadpisywanie jest kontynuowane.

Synchronizacja aktualnej gałęzi z inną gałęzią

Pracując na pobocznej gałęzi (czyli w Gicie – cały czas) możemy zdecydować się na wprowadzenie zmian z gałęzi rodzicielskiej wprost do naszej historii. Jeśli będziemy robić to dość regularnie, zdecydowanie uprości to końcowy merge. Po wykonaniu takiej instrukcji:

  1:  git rebase master

nasza aktualna gałąź, oryginalnie odchodząca od jakiegoś punktu w przeszłości gałęzi master, będzie od teraz widoczna jako odgałęzienie wyprowadzone z jej jej aktualnego HEADa.

Anulowanie rebase

W każdej chwili możemy zdecydować się "a jednak nie chce mi się teraz modyfikować tej historii…". Nic nie zmusza nas do dokończenia aktualnie wykonywanej czynności, wystarczy komenda:

  1:  git rebase --abort

i wracamy do stanu sprzed rebase.

Powrót do stanu sprzed zakończonego właśnie rebase

Zdarzyło mi się niedawno, że niechcący usunąłem z historii parę bardzo ważnych commitów. Straciłem właściwie prawie cały dzień pracy. Po kilku chwilach paniki okazało się jednak, że jest na to lekarstwo. Oto instrukcja która może niejednokrotnie uratować d…zień dobry?

  1:  git reset --hard orig_head

Działa nie tylko na rebase, ale także na spartolony merge.


Uwaga: przedstawione mechanizmy są bardzo potężne i oddają w ręce użytkownika ogromną władzę, pozwalając na nieograniczoną dowolność podczas pracy z własnym repozytorium. Trzeba jednak pamiętać, że "with great power comes great responsibility" (ale teraz mądrze wyglądam, nie?). O ile w swoich lokalnych gałęziach możemy mieszać jak nam się podoba, to już modyfikacja opublikowanych commitów, zmiana historii publicznych gałęzi, jest wysoce niewskazana. Nietrudno w ten sposób nieźle napsuć całemu zespołowi.

Z tego też powodu polecam lekturę dokumentacji wszystkich podlinkowanych komend jak również rozdziałów "Rewriting history and maintaining patch series" z oficjalnego manuala oraz "Lessons of History" z książki Git Magic.

Po takim przygotowaniu dzięki Gitowi będziecie skakać back to the past niczym zakręcony szalony profesor – i będzie się wam to podobać!

Share.

About Author

Programista, trener, prelegent, pasjonat, blogger. Autor podcasta programistycznego: DevTalk.pl. Jeden z liderów Białostockiej Grupy .NET i współorganizator konferencji Programistok. Od 2008 Microsoft MVP w kategorii .NET. Więcej informacji znajdziesz na stronie O autorze. Napisz do mnie ze strony Kontakt. Dodatkowo: Twitter, Facebook, YouTube.

4 Comments

Newsletter devstyle!
Dołącz do 2000 programistów!
  Zero spamu. Tylko ciekawe treści.
Dzięki za zaufanie!
Do przeczytania wkrótce!
Niech DEV będzie z Tobą!