Żaden programista nie wyobraża sobie dziś pracy bez systemów kontroli wersji. Wśród nich niekwestionowanymi liderami jest Subversion (znany szerzej, jako SVN) oraz znacznie zyskujący na popularności Git. Na temat historii i możliwości gita znalazłem dziesiątki stron i kilka książek, zatem pominę tę część i postaram się przedstawić praktyki, które mogą ułatwić pracę w zespole programistów.
Commitować wszystko, czy nie wszystko: oto jest pytanie
Co prawda git, z założenia, powinien służyć do wersjonowania plików tekstowych, to warto wersjonować wszelkie materiały związane z pracą nad projektem jak diagramy UML, czy opisy wymagań (nawet, gdy te znajdują się w plikach pdf lub docx). Taki zabieg zapewni dostęp do dokumentacji projektowej każdemu deweloperowi. Unika się za to wersjonowania plików binarnych, pochodzących z kompilacji. Wszelkie .exe, .bin, .pyc powinny być wypisane w pliku .gitignore
, którego przykład zamieszczam poniżej.
Kolejnym dylematem jest jak często commitować. Zaleca się jak najczęściej. Dopisanie nowej funkcji: commit. Dodanie nowego pliku: commit. Jest też prosta opcja: git add -p
, która pozwoli nam interaktywnie wybrać, co dodać do najbliższego commitu. Nie ma obawy, że takie commity „zajmą dużo pamięci”. Git, w przeciwieństwie do np. SVN-a, zapisuje tylko zmiany, czyli dodanie nowego pliku o wadze 1kB sprawi, że rozmiar repozytorium wzrośnie o 1kB (a nawet mniej, gdyż Git dysponuje całkiem wydajnym systemem kompresji danych). Inną kwestią jest dylemat: czy można commitować coś, co nie działa? Powszechną zasadą panującą wśród użytkowników systemów kontroli wersji jest commitowanie kodu, który da się skompilować. Git rozwiązuje sprawę inaczej: mamy jedną gałąź master, na którą zaleca się wrzucać kod kompilowalny i najlepiej przetestowany, gałąź wspólną, nad którą obecnie pracuje zespół – tutaj wedle ustaleń zespołu, ale też najczęściej kod, który da się skompilować oraz najlepszą część gita: własny branch, na którym możemy robić, co nam się żywnie podoba.
Struktura gałęzi w repozytorium
Skoro już doszliśmy do tematu branchy: dzięki bardzo elastycznemu systemowi „drzewa”, mamy możliwość dostosowania repozytorium do swoich wymagań. Nie ma tutaj z góry narzuconej metodologii, co najwyżej kilka przykładów, jak można porządkować pracę. Pierwszy, chyba najczęściej stosowany w powiązaniu z metodologią SCRUM-ową, to posiadania gałęzi master, zawsze sprawnej, i tworzenie nowego brancha dla każdego sprintu. Następnie z takiego brancha sprintowego odchodzą gałęzie powiązane z konkretnymi zadaniami na dany sprint. Tutaj zależnie od wielkości zespołu. Jeżeli jedna osoba zajmuje się daną funkcjonalnością to sama decyduje o stanie tej gałęzi. Jeżeli wiele, to gałąź znów może się rozchodzić na poszczególnych developerów. Na koniec branche są składane, do nadrzędnych, (ale tylko te, które są sprawne, tj. kompilowalne i najlepiej przetestowane) i dopiero następuje merge do mastera. Merge do mastera kończy dany sprint, a zabawa zaczyna się od nowa wraz z kolejnym sprintem.

Merge, rebase i cherry-pick, czyli scalanie zmian między branchami
By efektywnie pracować w zespole, warto nauczyć się także poprawnie merge’ować oraz taggować. Pierwsze – merge’owanie, czyli scalanie zmian dokonanych na dwóch różnych branchach, może być zrobione na kilka sposobów (jak wszystko): rebase oraz merge. Git rebase
polega na przeniesieniu danego commita z jednego brancha, na drugi. Niestety: tracimy przez to część historii, bo powstaje złudzenie, że dany commit zawsze był w tej gałęzi. Lepszym podejściem jest użycie git merge
, które powoduje scalenie dwóch gałęzi, co jest lepiej widoczne w historii i zapewnia lepszą kontrolę na rozwojem projektu. Jeżeli scalamy dwie gałęzie, np. sprintu w mastera to warto zaznaczyć, że takie zdarzenie miało miejsce. Samo użycie git merge może spowodować, że git sam scali zmiany (czyli nastąpi fast-forward). Dlatego lepiej w takim przypadku wykonać polecenie git merge --no-ff
, czyli merge bez fast-forward, który wymusi na git’cie stworzenie oddzielnego commita, opisującego co, skąd zostało zmergowane. Pytanie: jaki sens ma rebase? Przydaje się do sprzątania w celu uniknięcia tworzenia commitów, takich jak poprawianie literówek. Wyobraźmy sobie, że na naszym prywatnym branchu mamy listę commitów na branchu subject_list:
Historia oryginalna, przed rebase |
e3ffwty9 |
Added CSS |
s0b64a2 |
Removed a typo in list name |
j9b6la2 |
Added list to view |
j9b6la2 |
Added list to view |
tun832v |
Created controller for listing subjects |
W tym przypadku commit s0b64a2 jest redundantny, używamy zatem polecenia git rebase -i subject_list
, co wywoła interaktywny edytor (zapewne VIMa) z listą commitów (starsze na dole): oraz instrukcją, z której wynika, że słowo kluczowe „pick” oznacza by dołączyć dany commit. Szukamy w instrukcji polecenia „squash”, które ściska dany commit z poprzednim. Modyfikujemy zatem listę do postaci:
Przykład interaktywnego rebase |
pick |
e3ffwty9 |
Added CSS |
squash |
s0b64a2 |
Removed a typo in list name |
pick |
j9b6la2 |
Added list to view |
pick |
tun832v |
Created controller for listing subjects |
I zapisujemy. Git przeniesie nas do nowego okna, które służy do edycji nowego commita. Wszystkie niepuste i niezakomentowane linie (czyli bez #) znajdą się w commit message nowego commita. Po edycji wg własnego widzimisię znów zapisujemy i teraz nasz historia wygląda tak:
Historia po rebase |
op34ze9 |
Added CSS |
pick |
ty7bywq |
Added list to view |
tun832v |
Created controller for listing subjects |
Przeglądając commit „ty7bywq Added list to view” znajdziemy zmiany, które wcześniej były rozbite na dwa oddzielne. Warto zauważyć, że rebase tworzy nowe commity, co widać po nowych hashach. Tylko jeden pozostał bez zmian, gdyż nie ruszaliśmy ani jego, ani jego przodków. Taki zabieg warto wykonywać przed pushem do zdalnego repozytorium, gdyż NIGDY, pod żadnym pozorem nie wolno modyfikować zdalnego repozytorium. Jest to zbrodnia porównywalna do nazywania zmiennych / funkcji w stylu zmiennaA, czy funkcja1 – po prostu się tego nie robi. Z rebase warto też korzystać przy okazji pullowania, dzięki czemu nie utworzymy zbędnego punktu w historii, gdy synchronizujemy naszą pracę ze zdalnym repozytorium. Git może w tym przypadku korzystać z rebase automatycznie, jeżeli wcześniej ustawimy flagę pull.rebase na true, np. za pomocą komendy: git config --global --bool pull.rebase true
. Jeśli chodzi o łączenie branchy to nie można nie wspomnieć o poleceniu cherry pick, czyli jak nazwa wskazuje, wzięciu wisienki. Polecenie git cherry-pick hash
wybiera zmiany dokonane we wskazanym commitcie i scala je z aktywną gałęzią. Wyobraźmy sobie sytuację, że pracujemy nad sprintem, a w tym czasie wykryto i naprawiono buga na masterze. Jeżeli Bug ma wpływ na nasz obecny sprint to warto wprowadzić hotfix u nas, ale najlepiej identyczny z już zrobionym. W tym celu pobieramy wisienkę z mastera, czyli: git cherry-pick hash_z_hotfixem
i cieszymy się poprawionym kodem. Cherry pick kopiuje tylko commit, który mu wskażemy, na początek brancha nad którym pracujemy.

Na pierwszy rzut oka: cherry-pick jest tak „subtelny”, że niemal nie widać różnicy

Tagi – etykietowanie kamieni milowych w projekcie
Następnym zagadnieniem, które warto poruszyć to tagowanie, czyli szczególne oznaczenie konkretnego commita, np. realease’u. Git udostępnia dwa rodzaje tagowania: proste i z adnotacją. W prawdziwych projektach nie powinno się stosować prostego modelu, gdyż polega ono na nadaniu nazwy jakiemuś commitowi, zaś w praktyce: git tworzy coś na kształt brancha. Prawdziwy tag zawiera adnotację, w której znajdziemy, oprócz nazwy, opis etykiety, datę utworzenia, oraz dane autora. Dokonujemy tego korzystając z polecenia git tag -a
, domyślnie oznaczymy tym ostatni commit, jeśli dodamy hash konkretnego commita, to jego oznaczymy. Etykiety to nie tylko dekoracja, pozwalają sprawniej poruszać się po repozytorium. Możemy je przeszukiwać, np. poleceniem git tag -l 'v1.0.*'
, które zwróci wszystkie tagi zaczynające się od v1.0, albo wypisać dane commita, który opisują git show nazwa_taga.
Proste zasady dobrych commit message’y

Być może powinienem był od tego zacząć: commity. Standardowy kształt to po prostu jednolinijkowy opis (max 50 znaków), po którym będziemy w stanie określić, co zostało zrobione (patrz rysunek). Trzymając się zasady commitować małe rzeczy, ale często, nie powinno być z tym problemu. Większe commity, np. feature do sprint brancha, a już na pewno WSZYSTKIE do mastera powinny być bardziej szczegółowo opisywane w formacie: jedna linijka głównego opisu, pusta linijka, i dopiero długi, skrupulatny opis, co wnosimy danym commitem. Taki format się przydaje, gdy chcemy wysłać maile z pomocą gita. Wtedy mamy wyraźny rozdział na temat i treść maila. Istnieje też drugi powód, by odróżniać temat od treści commita: wpisując git shortlog uzyskamy listę commiterów wraz z wylistowanymi tytułami ich commitów.
Przykładowy efekt polecenia git shortlog |
$git shortlog
Daszczu (1):
Initial commit
Leszq (3):
Basic project architecture
Documentation
Merge remote-tracking branch ‚origin/master’
Misiu (6):
updated .gitignore
deleted .suo
new user attributes
user profile created
hook test Signed-off-by: Misiu
andrut (1):
Initial database
|
Hooki – automatyczna kontrola pracy z repozytorium
Jeżeli nawet ustalimy sobie w zespole jakieś reguły commit message’y, to może się zdarzyć, że ktoś się zapomni. Jak już pisałem: co na serwerze, na serwerze zostać musi. Tutaj przychodzą nam z pomocą tzw. hooki. Są to skrypty, umieszczone w katalogu .git/hooks
, które wykonują się w zależności od akcji użytkownika. Gdy wejdziemy do ww. katalogu znajdziemy kilka przykładowych skryptów napisanych w bashu. Aby je aktywować wystarczy usunąć rozszerzenie „.sample”. Np. odblokowując prepare-commit-msg i odkomentowując ostatnie dwie linijki sprawimy, że każdy commit będzie miał dodane w treści dane autora.
Plik: git/hooks/prepare-commit-msg
Ten hook otrzymuje jeden parametr: ścieżkę do pliku commit message
|
#!/bin/sh
SOB=$(git var GIT_AUTHOR_IDENT |sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
grep -qs "^$SOB" "$1" || echo "$SOB" >> "$1"
|
Inny przykładowy hook to update, który blokuje możliwość przyjmowania tagów bez adnotacji – bardzo przydatne. Niestety ten, aby być użytecznym, musi się znajdować na serwerze zdalnym, gdyż nie blokuje tworzenia prostych tagów, ale uniemożliwia przyjęcie ich od kogoś innego. W Internecie znajdzie się naprawdę dużo przykładowych hooków. Warto też wiedzieć, że mogą być pisane w dowolnym języku skryptowym, jak python czy perl. Wystarczy, że będą miały dostępny interpreter (#!/bin/python
, #!/bin/perl
, #!/bin/bash
).
Aliasy, czyli droga na skróty
Niektóre polecenia wykorzystujemy po kilkadziesiąt razy dziennie, zwłaszcza, gdy mają dodatkowe parametry, warto je sobie zapisać. Git udostępnia funkcjonalnośc zwaną aliasami, każdy może zdefiniować sobie własne, wpisując w konsoli: nazwa_aliasu = polecenia
, które byś wpisał po git Co potem wywołujemy poleceniem git nazwa_aliasu
. Ja postaram się przedstawić kilka, z których korzystam najczęściej:
Alias |
Efekt |
lg =log –graph –format=’%C(yellow bold)%h%Creset %C(bold blue white)%d%Creset %C(white bold)%s%Creset%n %C(magenta bold)%cr%Creset %C(green bold)%an%Creset’ |
Mój ulubiony alias – w przestępny i kolorowy sposób prezentuję drzewo aktywnego brancha, a po dodaniu parametru –all (git lg --all ) całego repozytorium. |
lasttag = describe –tags –abbrev=0 |
Wypisuje ostatni tag na obecnym branchu |
brclean = „!f() { git branch –merged ${1-master} | grep -v ” ${1-master}$” | xargs -r git branch -d; }; f” |
Czyszczenie repozytorium z branchy, które zostały zmerge’owane do mastera – przydatne, sprzątanie przy dłużej żyjących projektach |
lf = log –pretty=’%C(yellow bold)%h%Creset %C(bold blue white)%d%Creset %n %s %C(green bold)[%cn]’ –numstat |
Wypisuje jakie pliki zostały zmienione w poszeczególnych commitach (numrki oznaczają linie dodane / linie usunięte) |
Coś dla osób, które pracowały z SVN-em: |
st = status |
Jeżeli już ktoś zagląda do tej sekcji, to pewnie wie, co robią te skróty ;-) |
ci = commit |
co = checkout |
df = diff |
up = pull |
Skrytka: git stash
Jest Jeszcze jedna, użyteczna opcja w git’cie: git stash
, ale wydaje mi się, że często zapomniana. Standardowy przykład zastosowania: jesteśmy w trakcie pracy na nową funkcjonalnością i nagle przychodzi przełożony: zrób mi coś na teraz. Teraz powstaje problem: co zrobić z rozgrzebanym kodem? Niestety większość użytkowników po prostu commituje zmiany, bierze inny branch i na nim robi zadanie od szefa. Lepszym podejściem będzie „odłożenie na półkę” dokonanych zmian, a następnie ich zaaplikowanie na nowo. Polecenie git stash
zapamięta wszelkie, niezacommitowane zmiany i przywróci stan kopii roboczej do ostatniego commita. Jest to prawie to co, jakbyśmy zacommitowali, ale nieco mniej roboty (jedno polecenie vs 2). Natomiast warto znać dodatkowe przełączniki: git stash -u
, które doda wszystkie nie wersjonowane pliki (untracked) oraz git stash -a
, które zapamięta wszystko (all), co jest w danej chwili w kopii roboczej. Nie ważne którą opcję wybierzemy: po wykonaniu zadania dodatkowego, przywracamy stan pracy poleceniem git stash pop
i kontynuujemy ją.
Jak jeszcze ułatwić sobie życie?
Na koniec chciałbym przedstawić kilka dodatkowych funkcji, które przydają się w dużych projektach. Po pierwsze: kogo obwinić (ang. blame), że wpisał linijkę kodu, która psuje program? Służy do tego polecenie git blame ścieżka_do_pliku
, która wypisze kto jest autorem każdej linijki w pliku.
Przykład wyniku dla polecenia: git blame major_changes.latex |
28d64132 |
(Tomasz 2015-07-15 11:07:27) |
\item changing main.py – DONE |
f6eccc78 |
(Tomasz 2015-07-08 10:56:20) |
\item stats generation -DONE |
3101436b |
(Vesemir 2015-06-24 11:16:24) |
\item typo in theme – DONE |
92dbc83e |
(Vesemir 2015-06-17 11:27:17) |
\item performance tests uncompleted |
a46077f6 |
(Vesemir 2015-05-13 11:19:19) |
\item another step in server configuration – DONE |
28d64132 |
(Tomasz 2015-07-15 11:07:27) |
\item network issues solved |
f66ccc78 |
(Tomasz 2015-07-08 10:56:20) |
\item replaced colors |
28d44132 |
(Tomasz 2015-07-15 11:07:27) |
\item \textcolor{medium-gray}{build improvements – TODO} |
a46077f6 |
(Vesemir 2015-05-13 11:19:19) |
\item \textcolor{medium-gray}{changes in hotspot – TODO} |
1ae58e56 |
(burny 2015-03-04 09:41:19) |
\item \textcolor{medium-gray}{script for setup – TODO} |
9464ce99 |
(andrut 2014-10-15 08:07:33) |
\end{enumerate} |
Kolejną wartą uwagi funkcją jest możliwość przeszukiwania brancha. Na przykład poniższe polecenie: git log -S main.py --author zgredek --before="2015-08-15 00:00" --after="2015-08-01"
wypisze wszystkie commity, utworzone między 1 a 15 sierpnia 2015 roku, autorstwa zgredka, w których zmienia się ilość wystąpień słowa „main.py”, czyli zostaje dodana linijka np. import main.py, albo usunięta. Naturalnie parametry autora, czasu utworzenia są nieobowiązkowe. Za jedną z największych zalet SVN-a uważam możliwość pobrania tylko konkretnego pliku / folderu. Niedawno dowiedziałem się, że jest to też możliwe za pomocą gita! Wystarczy użyć polecenia git checkout commit_hash sciezka/do/pliku
. Dodatkowo, podobnie jak w SVN-ie można wybrać gdzie ma się znaleźć dany plik. Wystarczy skorzystać z parametru --work-tree
, np.: git --work-tree=/ścieżka/gdzie/checkoutujemy/ checkout HEAD src/main.py
Podsumowanie
Ukazując w niniejszym artykule zaledwie kilka z potencjalnych możliwości gita posłużono się opisami właściwości i przykładami użycia, zachęcając drogich czytelników do samodzielnej zabawy i do poszukiwania kolejnych przydatnych zastosowań. Projekt git powstał w 2005 roku i w porównaniu ze starszymi systemami kontroli wersji można dojść do wniosku, że podbija serca programistów, zauważając wciąż rosnącą popularność. Poniższy wykres może to tylko potwierdzić.

Skoro od czasu powstania git zdążył zdobyć tak wielką popularność, to z pewnością jego możliwości zdążyły zachwycić użytkowników. Git, jako projekt należący do wolnego oprogramowania, jest wciąż rozwijany przez programistów na całym świecie, więc ten wachlarz możliwości będzie rósł. Może, więc warto, chociaż tylko spróbować i sprawdzić, dlaczego tak bardzo git stał się królem wśród systemów kontroli wersji?