Git početnica, 3. dio – rukovanje promjenama
Nakon što smo u prošlom postu proučili kako raditi grananje, spajanje i rebaziranje grana, git nam već može biti poprilično koristan alat. Kako projekt raste, git nam može pomoći da ne izgubimo glavu u različitim granama i verzijama koje održavamo unutar jednog projekta.
Interaktivno dodavanje promjena
Prilikom rada na projektu, često ćemo da bi stvar uopće natjerali da radi morati napraviti nekoliko nezavisnih promjena, a tek onda biti sigurni da smo dobili dobar rezultat. Obično tek tada želimo napraviti commit, ali bismo htjeli da nezavisne promjene idu u nezavisne commitove. Ako se te promjene nalaze u istoj datoteci, običan git add nam ne pomaže jer bi on označio cijelu datoteku za commit.
U tom slučaju možemo koristiti interaktivno dodavanje. Ono nam pruža veću kontrolu nad time što dodajemo u commit, a i preglednije je (i pruža nam priliku za review napravljenoga prije commitanja), pa preporučam da ga uvijek koristite, čak i kad želite dodati sve promjene.
Recimo da smo napravili dvije nezavisne izmjene u hello.c. datoteci i želimo ih committati odvojeno (primjer radim u novoj test grani koju ću kasnije obrisati):
> git checkout -b test
Switched to a new branch 'test'
> git diff
diff --git a/hello.c b/hello.c
index 0bb4941..d560074 100644
--- a/hello.c
+++ b/hello.c
@@ -1,3 +1,4 @@
+#include <stdlib.h>
#include <stdio.h>
int main(void)
@@ -5,3 +6,9 @@ int main(void)
printf("Hello world\n");
return 0;
}
+
+void unused(void)
+{
+ puts("I'm never used");
+}
+
> git add -p
diff --git a/hello.c b/hello.c
index 0bb4941..d560074 100644
--- a/hello.c
+++ b/hello.c
@@ -1,3 +1,4 @@
+#include
#include
int main(void)
Stage this hunk [y,n,q,a,d,/,j,J,g,e,?]? y
@@ -5,3 +6,9 @@ int main(void)
printf("Hello world\n");
return 0;
}
+
+void unused(void)
+{
+ puts("I'm never used");
+}
+
Stage this hunk [y,n,q,a,d,/,K,g,e,?]? n
Ovo će označiti (tj. spremiti u index) samo prvu promjenu u datoteci za committanje. To možemo provjeriti sa git diff --cached:
git diff --cached diff --git a/hello.c b/hello.c index 0bb4941..c6efc61 100644 --- a/hello.c +++ b/hello.c @@ -1,3 +1,4 @@ +#include <stdlib.h> #include <stdio.h> int main(void)
Pregled promjena u datotekama
Nakon što neko vrijeme radite na projektu, većina datoteka unutar projekta
vjerojatno će biti mijenjana mnogo puta. Kako bi vidjeli koje promjene smo radili
nad jednom datotekom (ili direktorijem), možemo koristiti još jedan oblik
već poznate git log naredbe:
> git log hello.c commit 5e547517e1a29a08b3594f6b8c8c0a289070376d Author: Senko Rasic <senko@localhost> Date: Sat Jan 23 06:31:05 2010 +0100 popravljen kod commit 04f3667425705d29f2c3a10bb45e79f5cc6b7d7d Author: Senko Rasic <senko@localhost> Date: Sat Jan 23 05:49:59 2010 +0100 malo uljepsano commit 6e70237aaa84a0c8ff74bd0fad6cccb4d118b695 Author: Senko Rasic <senko@localhost> Date: Sat Jan 23 05:49:03 2010 +0100 prva verzija
Ovaj ispis dobar je ako želimo kronološki pregled promjena. No često je slučaj
da nas zanima zašto je baš neki dio (trenutne) datoteke napisan tako je, tj. koji
je razlog za dodavanje (ili promjenu) pojedine linije unutar koda.
Pregled promjena po pojedinim linijama datoteke omogućuje nam git blame,
tako nazvan jer nam govori tko je odgovoran za pojedinu liniju datoteke i zašto:
> git blame hello.c
5e547517 (Senko Rasic 2010-01-23 06:31:05 +0100 1) #include <stdio.h>
5e547517 (Senko Rasic 2010-01-23 06:31:05 +0100 2)
5e547517 (Senko Rasic 2010-01-23 06:31:05 +0100 3) int main(void)
04f36674 (Senko Rasic 2010-01-23 05:49:59 +0100 4) {
04f36674 (Senko Rasic 2010-01-23 05:49:59 +0100 5) printf("Hello world\n");
5e547517 (Senko Rasic 2010-01-23 06:31:05 +0100 6) return 0;
04f36674 (Senko Rasic 2010-01-23 05:49:59 +0100 7) }
Popis za svaku liniju sadrži skraćeni ID commita, autora te datum promjene.
Nakon što identificiramo promjenu koja nas zanima, pomoću već poznate
git show naredbe možemo vidjeti i ostale detalje te promjene.
Označavanje (tagiranje) commitova
Naš “Hello World” projekt već ima praktički svu funkcionalnost koju želimo,
stoga je pravo vrijeme da napravimo naš prvi release. Osim što možemo napraviti
arhivu izvornog koda (kao tar.gz ili u nekom drugom formatu) i poslati je korisnicima
svog programa, korisno je i u samom repozitoriju označiti da smo napravili
release.
Kako bismo “zapamtili” da smo napravili release, možemo dodati oznaku (tag) na
trenutni commit. Kako svaki commit zna za sve koji su se dogodili prije njega,
ta oznaka poslužit će nam da bi na jednostavan način dolazili do stanja projekta
u raznim fazama razvoja, bez da pamtimo IDeve comittova.
Oznaka (tj. tag) je (pojednostavljeno) najobičniji “alias” na ID commita.
Postavljenu oznaku možemo koristiti praktički svugdje gdje moramo navoditi ID
commita. Možemo ga koristiti kako god želimo – najčešća praksa je obilježavanje
kad se dogodilo nešto značajno u projektu, kao što je release.
Tagirajmo release 0.1 našeg projekta. Kako release radimo iz master
grane, pripazit ćemo ne tagiramo commit u krivoj grani:
> git checkout master > git tag -m "postavljamo tag" "helloworld-0.1"
tagove možemo pregledavati pomoću git tag -l, a brisati pomoću
git tag -d <ime_taga>.
Prilikom postavljanja oznake, možemo je i kriptografski potpisati (gpg-om,
uz korištenje gpg privatnog ključa za mail adresu koju koristimo kod rada
s repozitorijem), korištenjem opcije -s.
Sretni sa napravljenim releasom, krećemo dalje sa razvojem projekta. Verzija
0.1 je potpun program, ali mu nedostaje build sustav, zbog čega svaki put moramo
ručno pozivati kompilator. Namjera nam je to riješiti korištenjem make
alata.
Kreirajmo konfiguracijsku datoteku za make zvanu Makefile
(napomena: make je izbirljiv glede sintakse, stoga je bitno na određenim mjestima
koristiti tab umjesto razmaka, što je posebno naznačeno u ovom listingu):
CC = gcc OBJS = hello.o CFLAGS = -Wall -Werror -O3 -g .PHONY: all clean all: hello hello: $(OBJS) <tab>$(CC) $(OBJS) -o $@ %.o: %.c <tab>$(CC) $(CFLAGS) -c $< -o $@ clean: <tab>rm -f hello $(OBJS)
Dodajmo novostvorenu datoteku u projekt, repozicionirajmo doc granu
(pošto imamo novosti u masteru) i spojimo je natrag u master granu.
Pošto smo završili sa doc granom, na kraju je možemo i obrisati.
> git add Makefile > git commit -m "dodan Makefile" master b23a04b] dodan Makefile 1 files changed, 17 insertions(+), 0 deletions(-) create mode 100644 Makefile > 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 checkout master Switched to branch 'master' > git merge doc Updating b23a04b..302ef80 Fast-forward README.txt | 1 + 1 files changed, 1 insertions(+), 0 deletions(-) create mode 100644 README.txt > git branch -d doc Deleted branch doc (was 302ef80).
Pregršt novih promjena! Pogledajmo što smo sve izmjenili od zadnjeg releasea:
< git log helloworld-0.1..HEAD commit 302ef80039bd55fbbc680d84e37e5ebcf7f63a5c Author: Senko Rasic <senko@localhost> Date: Sat Jan 23 06:06:26 2010 +0100 dodan README commit b23a04b47dbc1044d44d94ee0a5df967f8f1b26d Author: Senko Rasic <senko@localhost> Date: Fri Feb 5 16:02:54 2010 +0100 dodan Makefile
Poput grana, tagove je moguće checkout-ati, čime se vraćamo u prošlost,
u stanje projekta kakvo je bilo u trenutku tagiranja. Vratimo se malo u prošlost
do naše verzije 0.1:
> git checkout helloworld-0.1 Note: moving to 'helloworld-0.1' which isn't a local branch If you want to create a new branch from this checkout, you may do so (now or later) by using -b with the checkout command again. Example: git checkout -bHEAD is now at 5e54751... popravljen kod
Git nas upozorava da smo došli na commit koji nije zadnji u nekoj grani. Ukoliko
želimo raditi bilo kakve modifikacije na projektu od ove točke, moramo kreirati novu
granu u koju će one biti spremljene.
Razvojne i stabilne grane
Ovo nam daje dobru ideju - možemo kreirati novu granu s početkom u helloworld-0.1,
koja služi samo za "održavanje" postojećeg koda i ispravljanje pogrešaka, dok ćemo sav
novi razvoj na projektu raditi kao i do sada na masteru. Ovime se želimo
osigurati da imamo "stabilnu verziju" projekta u koju nam neće slučajno upadati i
nove stvari koje razvijamo na "razvojnoj verziji" projekta na master grani.
> git checkout -b helloworld-0 helloworld-0.1 Switched to a new branch 'helloworld-0'
Grana helloworld-0 sadrži commit tagiran sa 'helloworld-0.1' i
sve njegove prethodnike i predstavlja stanje projekta kakvo je bilo u trenutku
kada smo tagirali taj commit. Granu potom možemo koristiti samo za promjene na
stabilnoj/releasanoj verziji, dok nam master ostaje za novi razvoj.
Cherry-picking
Ponekad prilikom rada na novoj verziji uočimo i ispravimo pogrešku koja postoji
i u već releasanoj verziji. Iako bismo ispravke mogli napraviti posebno na obje
grane, jednostavnije je ispravak napraviti na jednoj (obično razvojnoj, tj.
masteru) i nakon toga istu zakrpu primjeniti i na drugu. Ukoliko ispravku radimo
na masteru, primjenjivanje zakrpe i na starije release grane se zove
backporting.
Da bismo napravili backport commita koji ispravlja neku grešku, ne možemo
koristiti niti merge niti rebase, jer oni preuzimaju sve commitove iz mastera.
U slučaju kada želimo izabrati samo jedan commit (ili nekoliko njih, ali ne
cijelu granu) iz jedne grane i primjeniti ga na drugu, možemo koristiti
git cherry-pick.
Korisnici verzije 0.1 našeg softvera su ogorčeni jer ručno moraju pokretati
gcc. Smatraju da je to bug, a kako mi već imamo rješenje u obliku Makefilea
u master grani, rješenje je jednostavno - backportati commit koji je dodao
Makefile i napraviti novi release na 0.* grani.
> git cherry-pick b23a04b47dbc1044d44d94ee0a5df967f8f1b26d Finished one cherry-pick. [helloworld-0 f60bcbd] dodan Makefile 1 files changed, 17 insertions(+), 0 deletions(-) create mode 100644 Makefile
Traženi commit je preuzet u trenutnu granu. Preciznije, promjene
koje je traženi commit napravio su napravljene i spremljene i za trenutnu granu.
Ovo je bitno zbog toga što se ID commita mijenja - kao i kod rebasea, originalni
i cherry-pickani commitovi nisu isti. Ovisno o daljnjim promjenama koje radimo
na grani, možemo se naći u situaciji da granu više nije moguće rebazirati ili
spojiti sa masterom, odnosno trebali bi rješavati konflikte. Stoga bi trebalo
izbjegavati cherry-picking iz grane koju u budućnosti želimo spojiti sa trenutnom
granom.
Više o konfliktima te što napraviti kad vam se oni dogode pročitajte u slijedećem postu u ovom serijalu, za par dana...