Nowy odcinekIle kosztuje Odoo? Community, Enterprise i realny koszt hostingu

Git przekleństwo i zbawienie w jednym

Tymoteusz Motylewski
Technologie i programowanie
12 min

Git jest jak nóż, można go używać dobrze albo źle. Praktyczna pomoc dla osób pracujących w zespołach.

Nie bój się gita!

Najważniejsze, nie bój się. Jeśli nie jesteś pewien czy dobrze robisz, przetestuj to.

reset —hard

Jeśli chcesz coś zrobić na branchu, ale boisz się że coś popsujesz, to wystarczy że upewnisz się że masz jego identyczną wersję w źródłowej wersji (origin):

git diff origin/<nazwa brancha>

Wtedy cokolwiek zrobisz na branchu, zawsze będziesz mógł go zresetować do wersji z której zaczynałeś:

git reset --hard origin/<nazwa brancha>

branche testowe

Git pozwala tworzyć nowe branche w dowolnych ilościach i to tak żeby nikt inny ich nie widział. Przykład tworzenia brancha testowego:

git branch -b <nazwa brancha>-test

Jeśli coś pójdzie nie po naszej myśli (np. rebase), możemy przejść z powrotem na oryginalny branch:

git checkout <nazwa brancha>

Usunąć testowy branch:

git branch -D <nazwa brancha>-test

push —force

git push --force pozwala nadpisać historię brancha w repozytorium. Gdy usunęliśmy przypadkowo mastera, lub domergowaliśmy stage do mastera, możemy poprosić kogoś z teamu, aby wszedł na popsuty branch i nadpisać popsutą wersję stabilną wersją.

checkout

Jeśli znamy hash ostatniego commita ze stabilnej wersji, możemy przejść na niego:

git checkout <hash commita>

Usuńmy branch który chcemy naprawić:

git branch -D <nazwa brancha>

i utworzyć go na nowo:

git checkout -b <nazwa brancha>

reflog

Git jest na tyle zmyślnym systemem że niemal wszystko da się naprawić. W ostateczności możemy skorzystać z reflog’a. Wyszukujemy ostatni wpis sprzed rozpoczęcia rebase’a i kopiujemy jego hash (a85e451), lub numer wpisu (HEAD@{3}) i przechodzimy na niego:

git checkout <hash lub numer wpisu>

Po czym usuwamy uszkodzonego brancha:

git branch -D <nazwa brancha>

i tworzymy go na nowo:

git checkout -b <nazwa brancha>

Konsola nie gryzie

Druga najważniejsza sprawa: nie korzystaj z programów do pracy z gitem jeśli nie wiesz jak działają i jak działa git. Często osoby które ledwo co wiedzą o git’ie korzystają z różnych programów, do jego obsługi, aby ułatwić sobie życie. W większości przypadków prowadzi to do złych przyzwyczajeń, bałaganu w repozytorium i dziwnych sytuacji.

Autor wróciła do konsoli, bo w niej bardziej panuje nad tym co się dzieje. Jednak konflikty rozwiązuje z pomocą PHPStorma.

Jeśli chcesz zrozumieć jak działa git, to praca w konsoli zdecydowanie Ci to ułatwi.

Workflow

Trunk-based Development

To podejście, w którym wszyscy deweloperzy commitują swoje zmiany bezpośrednio do jednej głównej gałęzi (trunk). Zmiany są często mergowane do trunku, minimalizując liczbę długoterminowych branchy.

Plusy:

  • szybkość wykrywania błędów
  • bardzo duża szybkość wdrażania funkcjonalności
  • minimalna ilość konfliktów
  • bardzo prosty workflow

Minusy:

  • bardzo duży brak stabilności
  • wymaga doświadczonych programistów którzy są odpowiedzialni i zdyscyplinowani
  • brak możliwości wydawania konkretnych funkcjonalności

Feature Branch Workflow

W tym systemie mamy jeden główny branch z którego są tworzone feature branche. Każdy dla osobnej funkcjonalności. Po skończeniu prac jest on bezpośrednio mergowany do mastera.

Plusy:

  • częstość występowania konfliktów jest niska
  • duża szybkość wprowadzania nowych funkcjonalności

Minusy:

  • brak stabilnego brancha który w każdej chwili może być wydany na produkcję
  • brak z automatu możliwości wydawania konkretnych funkcjonalności
  • duża częstotliwość błędów uniemożliwiających pracę

Stosowane głównie gdy tworzony jest nowy projekt, który nie jest wydawany produkcyjnie aż do ostatecznej wersji.

Stage flow

W tym systemie jest jeden branch służący do wyjść produkcyjnych (master), jeden branch do wyjść na środowisko testowe (stage) i feature branche z funkcjonalnościami. Każdy feature branch jest tworzony z mastera i po wykonaniu prac jest mergowany do brancha testowego (stage). Po przejściu testów, feature branch jest mergowany do mastera.

Plusy:

  • stabilny branch główny
  • możliwość wydawania pojedynczych funkcjonalności
  • mała ilość krytycznych błędów w repozytorium

Minusy:

  • częste występowanie konfliktów na różnych etapach pracy
  • potrzeba usuwania brancha testowego i tworzenia go najlepiej co deploy z mastera
  • potrzeba rebasowania feature branchy do mastera co merge innego feature brancha do mastera
  • duże skomplikowanie

Stosowany głównie w projektach rozwojowych w których funkcjonalności muszą być wydawane w różnym czasie.

Git Flow

Z głównego brancha jest tworzony branch rozwojowy (develop) i z niego tworzone są feature branche. One są mergowane do brancha rozwojowego. Jak uzbiera się ich odpowiednia/ustalona ilość, tworzony jest branch wydaniowy (release) z rozwojowego. Jest on doprowadzany do pełnej stabilności, a potem mergowany do głównego brancha i ten ostatni jest tagowany.

Plusy:

  • częstość występowania konfliktów jest stosunkowo mała
  • szybkość wprowadzania zmian ogólnodostępnych
  • stabilność branch głównego

Minusy:

  • brak możliwości wydawania pojedynczej funkcjonalności
  • brak możliwości korzystania z pojedynczego środowiska testowego

Dlaczego warto dbać o porządek w repozytorium?

  • Aby ułatwić sobie życie
  • Aby ułatwić innym życie
  • Abyś nie marnował czasu innych
  • Abyś nie wkurzał innych
  • Abyś nie dodawał pracy innym
  • Abyś nie marnował swojego czasu
  • Abyś nie wkurzał siebie
  • Abyś nie dodawał sobie pracy
  • Aby żyło się wszystkim lepiej i zapanował na świecie pokój i harmonia

Dobre praktyki

Commits

Tytuł w commitach powinien:

  • opisywać ogólny zarys zmian
  • być zwięzły, ale bez przesady
  • rozpoczynać się od numeru zadania

Może się zdarzyć że natrafimy na jakiś kod którego logiki lub celu nie rozumiemy. Wtedy możemy skorzystać np. z komendy git blame <nazwa pliku> która wyświetli nam cały plik linijka, po linijce z informacją kto, kiedy i w jakim commicie zrobił ostatnio zmianę.

Następnie z pomocą git show <hash commita> możemy zobaczyć wszystkie zmiany, wraz z tytułem i komentarzem do commita.

Od tego jaki jest tytuł i komentarz oraz czytelność zmian, zależy czy sami dojdziemy co autor miał na myśli. Dlatego warto robić komentarze tak żeby ktoś inny nie zawracał nam głowy pytaniami.

Tytuł nie powinien być rozwlekły, ale powinien opisywać ogólnie czego dotyczyły zmiany.

Przykładowe sytuacje

Tytuł “Refactoring” nie za wiele nam mówi. Dokładniejszy opis można dodać w komentarzu (następna linia po wpisaniu tytułu commita, bez limitu znaków).

Przykład:

Refactoring

- Entity\User
- Entity\Address
- Entity\Order
- Service\Factory\User
- Service\Factory\Order
- Service\Factory\Address

Dzięki temu przeglądając historię zmian pliku, możemy skupić się najpierw na przeglądaniu tytułów, a potem weryfikacji komentarza.

W komentarzu do commita nie należy opisywać szczegółowo logiki wprowadzonych zmian. Od tego jest opis zadania np. w jira.

1 commit = 1 funkcjonalność

Czasami jakaś funkcjonalność przestała działać i chcesz znaleść commity które ostatnio zostały dodane i jej dotyczyły. Dobrze żeby komentarz w pierwszej linii informował jakiej funkcjonalności dotyczy.

Co jeśli commit dotyczy wielu funkcjonalności? 1 commit = 1 funkcjonalność. Dzięki temu komentarz będzie prostszy i czytelniejszy, a zmiany w kodzie będą łatwiejsze do zrozumienia.

Przykład:

  • “[TICKET-121] Add entity User” – Utworzenie encji, podpięcie relacji i dodanie migracji
  • “[TICKET-121] Add User grid” – Utworzenie kontrolera z tabelą
  • “[TICKET-121] Add add/edit form for User” – Utworzenie formularza do tworzenia/edycji
  • “[TICKET-121] Add translation for User controls” – Dodanie tłumaczeń
  • “[TICKET-121] Add User fixtures” – Dodanie fixture
  • “[TICKET-121] Add unit tests” – Dodanie testów jednostkowych

Należy tylko pamiętać, że każdy z tych commitów musi być działający.

Jeśli podczas realizacji zadania zauważysz że metoda z której korzystasz wymaga refactoringu, to dobrze jest realizowane zmiany wrzucić do osobnego commitu.

Dzięki tej zasadzie:

  • łatwiej odnaleźć kommit powodujący błędy
  • można wycofać część funkcjonalności która powoduje błędy
  • łatwiej robi się Code Review

Branche

Zdarza się że podczas pracy na feature branchu coś poszło nie tak. Zamiast naprawić problem, albo “zresetować” brancha, niektórzy tworzą nowego brancha z jakimś dopiskiem.

Zamiast tego “zresetuj” branch:

git reset –hard origin/<nazwa brancha>

Jeśli już wypchnąłeś “popsutego” brancha do wspólnego repozytorium, to nie twórz nowego brancha. Usuń go:

git remove -D <nazwa brancha>

i utwórz go ponownie. Jak już wszystko będzie działać, to wypnij go nadpisując popsutą wersję:

git push –force --set-upstream origin <nazwa brancha>

Przydatne metody

Tabulator

Tabulator, czyli autouzupełnianie. Dzięki niemu nie będziesz robić literówek, nie musisz pamiętać całych komend i jej parametrów, nie musisz kopiować nazw branchy.

add .

Dodaje wszystkie zmodyfikowane pliki do przechowalni (staging area). Nie musisz dodawać każdego pliku osobno.

add *

Działa jak powyższa komenda, ale bez dodawania plików i katalogów zaczynających się od kropki.

commit -am

a w tym przypadku oznacza że git przed dokonaniem commita, ma dodać wszystkie zmiany do przechowalni. Należy pamiętać że działa to tylko na plikach już wersjonowanych.

stash

Metoda ta pozwala na wycofanie wszystkich zmian bez ich utraty. Należy pamiętać że dotyczy to tylko plików “wersjonowanych” (git add).

Aby przywrócić zmiany wystarczy dodać parametr pop (git stash pop), który przywraca zmiany i usuwa je z listy.

checkout -

Parametr - oznacza poprzedni branch na którym byliśmy.

Wyświetlanie aktualnego brancha

Jeśli pracujesz z gitem w konsoli, warto ustawić wyświetlanie aktualnego brancha.

Jeśli pracujesz na linuxie i nie masz żadnych nakładek to wystarczy że na końcu pliku ~/.bashrc wrzucisz poniższy kod:

parse_git_branch() {
  git branch 2> /dev/null | sed -e '/^\[^\*\]/d' -e 's/\* \\(.\*\\)/ (\\1)/'
}
export PS1="\u@\h \[\\033[32m\]\w\[\\033[33m\]\$(parse_git_branch)\[\\033[00m\] $ "

add <katalog>

Czasami zdarzy się że z jakiegoś powodu chcemy cofnąć zmianę w dużej liczbie plików. Zamiast ręcznie kopiować je do metody git checkout, można spróbować wykorzystać przechowalnię.

Możemy je dodać na podstawie ścieżki:

git add src/Service

A następnie cofnąć wszystkie zmiany spoza przechowalni:

git checkout .

add -p

Może się zdarzyć że nie chcemy od razu wszystkich zmian w pliku dodawać do przechowalni. Dodawanie plików do przechowalni z parametrem p powoduje że git rozdziela zmiany na części i przy każdej pyta się co z nią zrobić.

  • y – powoduje dodanie całej paczki zmian do przechowalni
  • n – pomija zmiany
  • q – anuluj dodawanie
  • s – rozdziela na mniejsze części
  • e – pozwala ręcznie zedytować co zostanie dodane
  • ? – wyświetla listę wszystkich opcji

tig

Tig to narzędzie do interaktywnego przeglądania historii gita z konsoli. Po odpaleniu w konsoli tig wyświetli się lista commitów z ich datą, autorem, komentarzem i uproszczonym drzewkiem branchy.

Strzałkami ↑ i ↓ zmienia się zaznaczony commit. Enterem otwiera się okno podglądu commita. Przycisk ‘Q’ powoduje wyłączenie podglądu.

Główną zaletą jest możliwość przejrzenia historii konkretnego pliku:

tig <adres pliku>

Jak utrzymać porządek w repozytorium

Poprawianie commita

Okazało się że w commicie znajduje się plik którego nie powinno być, albo dopiero po zrobieniu wszystkich commitów odpaliłeś CS Fixera i masz zmiany do dodania.

Ostatni commit

Jeśli zmiany chcesz dodać do ostatniego commita, to wystarczy że dodasz je do przechowalni, a następnie zrobisz commita z parametrem ammend:

git commit --amend

Wtedy git nie utworzy nowego commita, ale doda zmiany do ostatniego wraz z możliwością zmiany tytułu i komentarza.

Któryś z poprzednich commitów

W przypadku zmian dla starszych commitów, sytuacja nieco się komplikuje. Dodaj zmiany do przechowalni. Wyszukaj w historii commita którego chcesz zaktualizować i skopiuj jego hash. Następnie zcommituj zmiany z parametrem fixup:

git commit --fixup=779245451cb388c3fa686f83cf086715839e3683

i zrebasuj:

git rebase --interactive --autosquash 779245451cb388c3fa686f83cf086715839e3683^

lub:

git rebase --i --autosquash 779245451cb388c3fa686f83cf086715839e3683^

Jeśli się zdarzą konflikty, to je rozwiąż. Masz już poprawionego commita.

Usuwanie zbędnych commitów

Ostatnie commity

Jeśli chcemy usunąć z historii brancha kilka ostatnich commitów, wystarczy wykorzystać metodę:

git reset –hard ~<ilość commitów do usunięcia>

Commity z historii

Jeśli chcemy usunąć jakiś commit który nie jest ostatnim w historii brancha, możemy użyć do tego metody rebase:

git rebase --interactive <hash commita>^

Parametr interactive spowoduje że wyświetli nam się edytowalna lista commitów, zaczynająca się od podanego przez nas commita. Przed każdym commitem znajduje się napis pick co oznacza że commit normalnie ma być brany pod uwagę w historii. Aby dany commit został usunięty z historii zamiast pick musimy wpisać d (od drop).

Gdy dużo konfliktów

Jeśli podczas rebasowania występuje dużo konfliktów, możemy skorzystać z metody cherry-pick. Spisujemy sobie hashe commitów które chcemy zachować, tworzymy nowy branch z bazowego brancha i dodajemy w dowolnej kolejności branche:

git cherry-pick <hash brancha>

Po każdym cherry-picku możemy dodać jakiś nowy commit.

Wyłączenie części zmian

Może się zdarzyć że chcemy usunąć jakiś commit, ale nie możemy modyfikować historii brancha. Wtedy z pomocą przychodzi metoda revert:

git revert <hash commita>

Spowoduje to utworzenie nowego commita ze zmianami odwrotnymi do tych w podanym komicie.

Jeśli jakaś funkcjonalność którą chcemy wyłączyć jest w kilku commitach, to należy pamiętać aby revertować w kolejności od najnowszego do najstarszego commita, aby uniknąć konfliktów.

Jeśli w przyszłości będziemy chcieli przywrócić daną funkcjonalność to możemy po prostu usunąć commit revertujący.

Jak posprzątać branch z nadmiaru commitów

Miękkie wycofanie commitów

Podczas pracy może być nam szkoda czasu na robienie commitów w sposób uporządkowany. Dlatego jak już skończyliśmy pracę nad zadaniem, możemy wycofać wszystkie utworzone na szybko commity, nie tracąc zmian i utworzyć commity zgodnie z dobrymi praktykami:

git reset –soft HEAD~<ilość commitów>

Następnie z pomocą parametru p metody add możemy tworzyć commity według uznania.

Mergowanie/squash commitów

Jeśli z jakiegoś powodu chcemy połączyć kilka commitów, będących jeden po drugim w historii, w jeden:

git rebase –interactive <hash commita do którego chcemy zmergować pozostałe>

Przy commitach które chcemy zmergować musimy zmienić przypis pick na squash (jeśli chcemy zachować komentarza commita), lub fixup (jeśli nie chcemy mergować komentarza commita).

Wypychanie poprawionych commitów do repozytorium

Jeśli commit który chcemy poprawić został wcześniej wypchnięty do wspólnego repozytorium, to zanim zaczniemy poprawiać należy poinformować wszystkich w projekcie że chcesz zmodyfikować historię brancha i poprosić żeby nikt nic do niego nie wypychał.

Na wszelki wypadek utwórz nowy branch na którym utworzysz commita ze wszystkimi zmianami. Następnie pobierz najnowszą wersję brancha. Po wykonaniu zmian, zmieni się historia w stosunku do tej istniejącej w głównym repozytorium.

Wtedy gdy sprawdzisz status (git status), zobaczysz komunikat podobny do tego że branch jest “ahead” i “behind”.

Aby ujednolicić historię musisz wypchnąć swoje zmiany z parametrem force:

git push –force

Co nadpisze dotychczasową historię twoją wersją. Teraz poinformuj pozostałych że muszą odświeżyć swoje historie brancha poprzez:

git fetch
git rebase origin/<nazwa brancha>

i rozwiązać ewentualne konflikty.

Jest to dosyć kłopotliwa i niebezpieczna czynność, więc zastanów się czy na pewno możesz to zrobić.

Dobrą praktyką jest ustawienie blokad na branchach bazowych, tak aby nie można było do nich nic pushować, a jedynie mergować.

Stash VS Commit

Czy zawsze, kiedy potrzebujemy na chwilę “odłożyć zmiany” aby później do nich wrócić, należy je stashować? Teoretycznie nie ma w tym nic złego. Jednak stash ma tendencję do puchnięcia, bo często zapominamy o zmianach które w nim trzymamy.

Proponuję, gdy zmieniamy wątek (przechodzimy na innego brancha) stosować commita. Zawsze po powrocie możemy go usunąć nie tracąc zmian. Stasha zaś stosować w jednym wątku, gdy np. chcemy zrobić pula, lub rebasa; przetestować mechanizm bez dotychczasowych zmian.

Rebase VS Merge

Czasem zdarza się że podczas rebasowania kilkukrotnie musimy rozwiązywać te same konflikty. Prowadzi to niektórych do wniosku, że lepiej jest mergować, bo wtedy rozwiązujemy wszystkie konflikty jednorazowo.

Powoduje to jednak spore komplikacje:

  • Przy próbie mergowania takiego brancha do brancha źródłowego, otrzymamy te same konflikty, ponieważ w obydwu branchach będą te same zmiany ale w różnych commitach
  • W MR gitlab wyświetli wszystkie domergowane zmiany jako nowe
  • Może to doprowadzić do sytuacji w której kod który został świadomie usunięty z repozytorium, po domergowaniu takiego brancha, ponownie się pojawi
Tymoteusz Motylewski
Tymoteusz MotylewskiCTO & Co-founder Macopedia

CTO i współtwórca Macopedii, członek zarządu Stowarzyszenia TYPO3. Odpowiada za kierunek techniczny wdrożeń – od platform B2B i silników cen po headless TYPO3. Na blogu pisze o warsztacie: wydajności, dostępności cyfrowej i pracy z narzędziami, których programista używa codziennie.