U prethodnom postu u serijalu o korištenju distrbuiranih sustava za verzioniranje projekata pokazao sam osnove korištenja git-a, ali se nisam dotaknuo zanimljivih dijelova kod kojih distribuirani sustavi pokazuju svoju pravu stranu - grananje projekata u različite verzije, spajanje verzija nazad u jednu i repozicioniranje pojedinih grana u odnosu na druge grane (rebasing).
Ovi pojmovi koriste se u većini (svim?) distribuiranim SCM sustavima, ali se ovdje situacija već dovoljno komplicira da različiti sustavi na ne posve identične načine pristupaju istom problemu. Zbog toga je ovaj post posve specifičan i priča o git-u. Ukoliko koristite neki drugi SCM, imajte na umu da je stvar slična, ali se pojedinosti vjerojatno podosta razlikuju.
Grananje (branch)
U našem hello world projektu zasad imamo samo jednu granu, master, na kojoj smo radili sve promjene. To nam je odgovaralo jer smo točno znali što želimo napraviti slijedeće i nismo imali potrebe raditi više stvari paralelno (ili eksperimentirati u više različitih smjerova odjednom).
No, sada bismo željeli početi pisati i dokumentaciju za projekt. Kako nam se promjene vezane uz dokumentaciju ne bi mješale sa eventualnim nevezanim promjenama koda koje ćemo možda htjeti raditi, dokumentaciju ćemo dodavati u posebnoj grani projekta koju ćemo na kraju spojiti u glavnu. Kreirajmo novu granu doc iz postojeće master grane i odmah se prebacimo u nju:
> git checkout -b doc master
Switched to a new branch 'doc'
Za početak ćemo samo kreirati datoteku README.txt i u nju staviti osnovnu informaciju o programu:
Ovo je tipični "Hello World" program pisan u C-u.
Dodajmo datoteku u repozitorij, napravimo commit, i pogledajmo što smo dobili:
> git add README.txt
> git commit -m "dodan README"
[doc 7bb37b0] dodan README
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 README.txt
> git log
commit 7bb37b04d7eeb36dc91e60ef1d01e2648bb2e813
Author: Senko Rasic
Date: Sat Jan 23 06:06:26 2010 +0100
dodan README
commit 459b8bd04f3667425705d29f2c3a10bb45e79f5c
Author: Senko Rasic <senko@localhost>
Date: Fri Dec 11 00:53:32 2009 +0100
malo uljepsano
commit ac3a48559de3b7f225ffa96c504a8586057fb4b9
Author: Senko Rasic <senko@localhost>
Date: Fri Dec 11 00:38:16 2009 +0100
prva verzija
Kako smo granu doc stvorili iz grane master, ona je uključuje sve dosadašnje promjene. Ovaj prikaz nam nije previše koristan ukoliko želimo vidjeti samo razliku između trenutne grane i mastera. To možemo napraviti ovako:
> git log master..HEAD
commit 7bb37b04d7eeb36dc91e60ef1d01e2648bb2e813
Author: Senko Rasic <senko@localhost>
Date: Sat Jan 23 06:06:26 2010 +0100
dodan README
Argumentom master..HEAD zatražili smo log svih promjena do zadnjeg commita u trenutnoj grani (HEAD) koje se ne nalaze u master grani. Osim od-do, parametar može biti i samo jedna oznaka, čime se traži log svih promjena od početka do te oznake. Tako će git log master prikazati log svih promjena na grani master bez obzira u kojoj smo grani trenutno. Oznaka HEAD uvijek pokazuje na posljednji commit u trenutnoj grani, pa je git log HEAD isto što i samo git log.
Dokumentiranje našeg programa dobro napreduje, ali paralelno s tim sjetili smo se da bi htjeli napraviti još neke kozmetičke promjene na programu. Na njima želimo raditi nezavisno o procesu dokumentiranja pa ćemo kreirati novu granu. Kako ne bi imali poluzavršenu dokumentaciju u novoj grani, novu granu beauty ćemo opet kreirati iz master grane:
> git checkout -b beauty master
Switched to a new branch 'beauty'
Trenutna inačica programa nije posve legalan C program, stoga ćemo je opet malo doraditi, tj. preraditi u nešto pristojnije:
#include <stdio.h>
int main(void)
{
printf("Hello world\n");
return 0;
}
Sada smo već napravili nekoliko grana i u svakoj po nekoliko commitova i sve je teže pratiti gdje se nalazi što. Podsjetimo se koje grane imamo i što one sadrže:
> git branch
* beauty
doc
master
> git log master..beauty
commit 5e547517e1a29a08b3594f6b8c8c0a289070376d
Author: Senko Rasic <senko@localhost>
Date: Sat Jan 23 06:31:05 2010 +0100
popravljen kod
> git log master..doc
...
> git log master
...
Spajanje grana (merge)
Nakon što smo napravili sve što smo htjeli i sve committali te nakon što smo zaključili da smo zadovoljni promjenama na ovoj grani, željeli bismo je spojiti nazad u master.
Napomena: radi jednostavnosti primjera, ovdje nam se grana sastoji od samo jednog commita te ćemo je odmah spojiti nazad, što izgleda kao puno kompliciranja nizašto. No u stvarnom radu promjene obično nisu ovako trivijalne i grane se sastoje od nekoliko (možda desetke) committova, kod čega branch/merge ima smisla.
> git checkout master
Switched to branch 'master'
> git merge beauty
Updating 459b8bd..5e54751
Fast-forward
hello.c | 5 ++++-
1 files changed, 4 insertions(+), 1 deletions(-)
Prilikom ovog mergea, master nije imao nikakvih dodatnih commitova kojih nema u beauty grani pa se git nije morao mnogo mučiti sa spajanjem - samo je napravio fast-forward odnosno zalijepio sve nove commitove iz beauty grane u master granu. Pomoću git log možemo se uvjeriti da se uistinu radi o istim committovima (jedinstveni ID im je jednak).
Spajanjem grana beauty nije nestala. Ona još uvijek postoji, iako sad kad su svi njeni commitovi i u masteru, nema razlike između nje i mastera - jednako kao da smo je tek sad kreirali. U daljnjem radu opet možemo koristiti istu granu i dodavati nove committove koje periodički spajamo nazad u master. Stvar je izbora (i specifične organizacije projekta na kojem radite) želite li imati dugotrajnije grane koje periodički spajate s masterom ili za nove promjene radite nove grane.
Ukoliko odlučimo da nam grana više ne treba, možemo je obrisati:
> git branch -d beauty
Deleted branch beauty (was 5e54751).
Repozicioniranje grana (rebase)
Sretni sa dosadašnjim napretkom na master grani, vraćamo se nazad na doc. No sada master ima neke comitove koje doc nema. Zasad nema problema, git bi comittove pametno spojio, no ukoliko i ubuduće mijenjamo master (tj. dodajemo nove comitove), razlika će se sve više povećavati i sve će veća biti vjerojatnost da (slučajno ili namjerno) negdje modificiramo istu stvar i uzrokujemo konflikt.
Kako bi to minimizirali, dugovječne grane možemo repozicionirati, tj. rebazirati (rebase). Rebaziranjem pomičemo točku u kojoj se grana “odvojila” od grane iz koje smo je stvorili (ovdje, mastera), tako da izgleda kao da smo je tek sada stvorili.
> git checkout doc
Switched to branch 'doc'
> git rebase master
First, rewinding head to replay your work on top of it...
Applying: dodan README
> git log
commit 6c0516dbe782173b57997039576e3b38bc20997c
Author: Senko Rasic <senko@localhost>
Date: Sat Jan 23 06:06:26 2010 +0100
dodan README
commit 5e547517e1a29a08b3594f6b8c8c0a289070376d
Author: Senko Rasic <senko@localhost>
Date: Sat Jan 23 06:31:05 2010 +0100
popravljen kod
Primjetite da je nakon rebaziranja “dodan README” comit ispao zadnji, iako je zadnja promjena koju smo mi stvarno na cijelom projektu napravili bio “popravljen kod” u beauty grani. Također, primjetite da se ID zadnjeg comitta promjenio - to zapravo više nije isti commit. Radi se o tome da je git doslovno pospremio patcheve, obrisao comittove, a kasnije ih vraćao jedan po jedan i automatski radio nove comittove.

repozicioniranje grane
Kao rezultat toga, grana doc više uopće ne izgleda isto. Ako solo radite na projektu, to za vas ne igra nikakvu ulogu. Ali ukoliko nekoliko ljudi radi na istom projektu, potreban je oprez - ukoliko je netko kod sebe povukao vašu granu, a vi ju naknadno rebazirate, stanje comittova u toj grani u vašem repozitoriju više se neće slagati sa stanjem u njihovom repozitoriju. Kod takvih situacija osnovno pravilo je - nikad ne modificirajte povijest grane koju je netko drugi već povukao od vas.
Merge ili rebase?
Nije odmah očito zašto bi radili rebase ako već imamo spajanje grana, no postoji razlika. Kod spajanja promjene iz druge grane “naljepe” se na trenutnu granu. Ukoliko dođe do konflikta, nemamo informaciju koji od comitova iz trenutne grane je uzrokovao konflikt. Stoga ga nećemo moći niti prepraviti, nego ćemo rješenje konflikta samo zalijepiti kao još jedan novi commit.
Spajanje logički znači “sav dosadašnji rad na grani koja se spaja sa trenutnom granom od sada se nalazi i u ovoj grani”. Ukoliko bismo koristili merge za povlačenje novih promjena mastera u doc, efekt bi bio kao da smo rekli “ok, od sada će nam doc biti glavna grana u projektu”.
Za razliku od toga, repozicioniranje znači “promjeni trenutnu granu tako da izgleda da sam sve promjene radio nad sadašnjim sadržajem mastera (odnosno granom nad kojom radim rebase - baznu granu)”, odnosno svojevrsno “osvježavanje patcheva” koje imamo u odnosu na master. Konkretno, rebase radi slijedeće:
- privremeno miče sve comittove iz trenutne grane koji nisu u baznoj grani i sprema ih na sigurno
- povlači sve nove comittove iz bazne grane koji nisu u trenutnoj (fast-forward), nakon čega bazna grana i trenutna grana imaju isti sadržaj
- jedan po jedan vraća nazad comittove koji su spremljeni na sigurno
Ukoliko u nekom trenu dođe do konflikta, rebase će se zaustaviti i imat ćemo priliku prepraviti commit koji uzrokuje konflikt. Na taj način praktički “osvježavamo” trenutnu granu, tako da promjene koja ona predstavlja budu primjenjive na trenutno stanje bazne grane. Ovo nam omogućuje da imamo dugovječne grane sa relativno velikim promjenama u odnosu na baznu granu, a da te promjene uvjek budu ažurne.
Kada koristiti rebase ili merge možete pročitati i u Linusovom mailu o toj temi na LKML - “Some git best practices” post na LWN sadrži malo šire objašnjenje za nas koji ne pratimo LKML.
Join us next time…
Rukovanje granama je vjerojatno najbitniji i najkompliciraniji dio git-a. Nakon što smo to apsolvirali, možemo se opet baciti na laganije, a opet korisne stvari koje korisnicima gita život čine ugodnijim i lakšim. Pridružite mi se slijedeći put dok istražujem interaktivno dodavanje promjena, pretraživanje povijesti, tagiranje, cherry-picking, …