Pytanie, które stawia sobie wiele osób pracujących z gitem. Zarówno początkujący jak i bardziej doświadczeni programiści nie zawsze „czują” różnicę w git merge vs git rebase. Dlatego dziś przy pomocy moich autorskich grafik postaram się rozwiać wątpliwości raz na zawsze 🤨
O co całe to zamieszanie?
Głównym „problemem” jest to, że zarówno polecenie merge jak i rebase można wykorzystywać do łączenia zmian z różnych gałęzi. Oczywiście rebase w trybie interactive daje nam szerszy zakres możliwości ale myślę, że to materiał na osobny wpis. Choć oba polecenia dają podobny wynik (przeniesienie kodu z gałęzi A do gałęzi B), to robią to zupełnie inaczej. Jeden bardziej bezpieczny, a drugi hmm… mniej. Który jest który? Przyjrzyjmy się im bliżej!
Jak działa git merge?
W poleceniu merge możemy wyszczególnić dwa tryby działania: fast-forward oraz 3-way merge (no-fast-forward). Uruchomienie jednego bądź drugiego algorytmu jest zależne od stanu w jakim znajdują się „mergowane” gałęzie lub w wyniku dodatkowego parametru, który podamy przy poleceniu (git merge —no-ff).
Fast-forward
Najprzyjemniejsza sytuacja podczas przenoszenia zmian pomiędzy branchami. Występuje w momencie gdy kod gałęzi bazowej – main, która była źródłem dla nowej gałęzi – feature, nie zmienił się. Podsumowując, kod z gałęzi main nadal znajduje się w feature, przez co wskaźnik HEAD może „przesunąć się” do najnowszego commita. W rezultacie uzyskujemy liniową historię projektu, a cała operacja jest „bezbolesna” i nie pozostawia po sobie śladu.
3-ways merge
Przypadek bardziej „skomplikowany”, w którym zwykłe przejście wskaźnika HEAD do najnowszego commita nie jest możliwe, gdyż stan bazowej gałęzi main zmienił się od momentu powstania gałęzi feature (commit M4). Git w jakiś sposób musi uzupełnić brakujące informacje, dlatego tworzy tzw. merge commit = commit pomocniczy, w którym łączy zmiany z obu gałęzi (M5 = F1 + F2 + M4).
WAŻNE! Jeżeli na obu gałęziach kod projektu był modyfikowany w tym samym obszarze pliku, mogą pojawić się konflikty, które należy rozwiązać. Dlaczego? Ponieważ git nie wie, która wersja kodu jest aktualna i konieczne jest ręczne wskazanie zmian nadających się do przeniesienia – z gałęzi main, feature, a może z obu?
Jak działa git rebase?
Dodatkowy commit, który powstaje przy operacji merge nie zawsze jest mile widziany w historii projektu. Są osoby/zespoły, którym zależy aby po połączeniu zmian zachować nieprzerwaną liniowość w historii, która może dawać większe możliwości w zakresie analizy, testów czu debuggowania błędów. W takiej sytuacji bardzo często jako alternatywa dla merge używany jest właśnie rebase. Dzięki niemu kod na gałęzi jest wyrównywany do „wspólnej bazy”, a nowe commity trafiają na samą górę drzewa. Niestety… jest tutaj pewien haczyk, na który trzeba bardzo uważać!
Podstawową zasadą, o której należy pamiętać jest fakt, iż rebase modyfikuje historię projektu. Commity, które są tymczasowo odłączane i finalnie trafiają na górę drzewa otrzymują zupełnie nowe identyfikatory, gdyż zmieniają się ich „rodzice”. Jeżeli gałęzie są „prywatne” – tylko do własnego użytku, nie stanowi to większego problemu. Ale w sytuacji gdy branche są „publiczne” i inni developerzy z nich korzystają, może to wygenerować sporo problemów…
Jak nie korzystać z rebase?
Powyższy przykład obrazuje sytuację, której powinniśmy unikać, tzn. wykonywać rebase na gałęziach publicznych – w tym przypadku main. Na grafice widzimy, że polecenie sprowadza do „wspólnej bazy” kod z gałęzi main na podstawie feature. Commit M4 zostaje chwilowo odłączony, F1 oraz F2 trafiają do main z tymi samymi ID, a na końcu do historii ponownie zostaje dołączony commit M4. Niestety… tym razem z nowym ID, ponieważ jego rodzicem nie jest już commit M3, tylko F2. Jeżeli ktoś z zespołu bazował na commicie M4 przed wykonaniem polecenia rebase, myślę że po tej operacji przeżyje mini zawał 😉
Lepsze rozwiązanie
To jak można poradzić sobie z tym problemem? Wykonywać rebase na gałęziach prywatnych! Musimy odwrócić sytuację i to gałąź feature (prywatna) sprowadzić do wspólnej bazy z main. Dzięki temu, publiczne commity nie zmienią swoich ID, a commity z gałęzi feature trafią na górę drzewa z nowymi identyfikatorami.
No tak ale zmiany nie trafiły na gałąź main, tylko cały czas są na gałęzi feature. Co z tym można teraz zrobić?
Jest to bardzo dobra sytuacja! Można w ten sposób przetestować całość przed wysłaniem kodu na publiczną gałąź – zyskaliśmy tym samym dodatkową formę zabezpieczenia i utrzymania stabilności projektu.
Pamiętasz początek tego wpisu? Zerknij jeszcze raz na drzewo „After rebase”, przypomina Ci coś? Bardzo podobna sytuacja była w pierwszej grafice. Jeżeli w gałęzi feature jest aktualny stan gałęzi bazowej (main), po wykonaniu polecenia merge zadziała algorytm fast-forward, który zachowa liniowość w historii projektu i nie pozostawi po sobie śladu w formie dodatkowych commitów.
Słowem końca, jakie są różnice?
- merge nie modyfikuje historii projektu, rebase tak – nadaje zupełnie nowe ID commitom
- merge przesuwa wskaźnik HEAD lub tworzy dodatkowy commit wynikowy, rebase tymczasowo odłącza nadmiarowe commity -> wyrównuje kod do wspólnej bazy -> przywraca odłączone commity
- rebase w trybie interactive posiada szerszy wachlarz możliwości
- nieuważne użycie rebase może wygenerować sporo problemów
- merge jest bardzo dobrym uzupełnieniem polecenia rebase
Więcej informacji wraz z przykładami znajdziesz w poniższym video na moim kanale YT 😊
Daj lajka i czytaj dalej!
Jeżeli chcesz być na bieżąco z artykułami i jesteś ciekawy co będzie dalej, daj lajka na moim profilu FB, a przede wszystkim zapisz się do newslettera! Spodobał Ci się artykuł? Z pewnością zaciekawią Cię inne wpisy na blogu lub filmy na kanale YT!
Dzięki za Twój czas, widzimy się niebawem! 🫡