Podíváme se na nejpopulárnější verzovací systém: Git. Začneme od základního verzování, ukážeme si větvení a spolupráci s kolegy. Na závěr si ukážeme, jak Git používat v praxi. Vše bude doplněno o ilustrace a příklady tak, aby to pochopil i naprostý začátečník.
Úvod
Motivace
Už se vám stalo, že projekt fungoval krásně. Pak jste se rozhodli ho trochu upravit, a tím jste si celý projekt rozbili? Nejlépe těsně před tím, než jste ho měli někomu ukázat. Hodila by se nám možnost, vrátit se k původní verzi, která ještě fungovala.
Nebo pokud se objeví nová chyba, můžeme se podívat na změny, které se prováděli, a hledat chybu jen tam. Například když kolega dodělá po dvou týdnech novou feature a odjede na dovolenou. Díky Gitu se v jeho novém kódu jednoduše zorientujete a případné chyby budete hledat opravdu jen tam, kde je mohl napáchat.
Historii lze použít i jako metriku pro měření náročnosti některé feature. Pokud nějaké úkony děláte opakovaně, snadno v Gitu změříte jak dlouho trvají a komu. Obecně lze na log verzí dělat spousta zajímavých query.
Vývojáři ale našli mnoho dalších výhod, a dnes je Git ve většině projektů naprosto nepostradatelný. Jedním z užitečných oborů je tzv. GitOps, který se snaží dělat různé kontroly a automatizace právě pomocí Gitu.
Proč Git vznikl?
Autor Gitu je Linus Torvalds, který je mimo jiné autor Linuxu. Právě projekt Linux vyprovokoval vznik Gitu. Nejprve si Linus spravoval verze ručně, jako složky. To se ale rychle stalo neudržitelné, a tak Linus hledal systém, který by verze spravoval. Žádný open-source projekt ale nesplňoval, tehdy ještě nejasné, požadavky, které Linus měl. Sáhl tedy po proprietárním a placeném řešení BitKeeper, na které dostal licenci zdarma. Majitelem BitKeeperu byl Larry McVoy, který byl zároveň jeden z významných vývojářů Linuxu.
S tím, že řešení bylo properietární, bylo v komunitě překvapivě spojeno spousta kontroverzí. Larry McVoy navíc ve svobodný software nevěřil a docházelo ke konfliktům. Linus byl ale rozumný a šlo mu o řešení, ne o filozofii.
Bohužel s licencí zdarma bylo spojeno mnoho omezení. Například se žádný vývojář Linuxu nesměl podílet na vývoji konkurence BitKeeperu. Také byl zakázán reverse engineering protokolu, který BitKeeper používal, protože by to komunitě umožňilo mít vlastní, neomezené klienty. To byl nakonec i důvod, proč Linux o licenci přišel. Někdo se pokusil o reverse engineering.
Mezitím vzniklo spoustu open source systémů pro správu verzí, ale žádný z nich Linusovi nevyhovoval. Problém bylo, že Linus ani vlastně pořádně neřekl, co od systému požaduje. Proto se rozhodl pro vlastní řešení.
A tak 3. dubna 2005 začal vývoj, 6. dubna už projekt oznámil, 7. dubna běžel systém na serveru a 27. dubna už překonal Linusovi požadavky na rychlost. Výkon byl absolutně bezkonkurenční, a to byl velký tahák.
Konečně totiž vyšly najevo Linusovi požadavky na systém:
- Rychlost
- Distribuovanost
- Ochrana proti chybám (úmyslným i neúmyslným)
Všechny požadavky byly splněny. Ze začátku byl kritizován za složité rozhraní. Nebyla to ale úplně tak chyba. Spíše než verzovací systém, se Git choval jako souborový systém, což pro vývojáře kernelu, Linuse, dávalo smysl. Tomuto rozhraní se říká plumbing a je v Gitu dodnes. Naštěstí ale už máme i rozhraní porcelain, které je uživatelsky přívětivější. Právě porcelain používá většina uživatelů.
A dnes? Git je nejpoužívanější verzovací systém na světě. Jeho popularita je tak velká, že se stal synonymem pro verzování kódu. Stejně jako se říká "Google" místo "hledat na internetu", říká se "Git" místo "verzovat kód".
Co je to verzování kódu?
Verzování je zaznamenávání změn v kódu. Jednoduše by to mohlo fungovat tak, že pro záznam změny si celý zdrojový kód nakopírujeme a nějak intuitivně pojmenujeme. Budeme tak mít složky: projekt-v1, projekt-v2, atd. Kde v každé bude kompletní kopie projektu, každá ale trochu jiná. Takový přístup se dříve opravdu používal, ale je dlouhodobě neudržitelný a vyžaduje silnou disciplínu.
Zároveň je s takovým přístupem těžké spolupracovat ve více lidech. Museli byste mít složku s verzemi někde přístupnou všem kolegům pro psaní. Navíc, co když dva budou chtít přidat novou verzi zároveň?
Proto lze použít systém, který verze bude spravovat pro nás. V pozadí, ať si klidně pro každou verzi celý projekt nakopíruje, ale ať nám dá přehledné rozhraní, ve kterém budeme pracovat s verzemi, ne složkami. Zároveň by měl umožnit verze synchronizovat mezi více lidmi tak, aby mohli efektivně spolupracovat.
Jak funguje Git?
Pojďme se podívat na Git, systém pro správu verzí.
Commit
Git nám umožňuje pořizovat snapshoty zdrojového kódu a ukládá je jako verze. To znamená, že na zálohu se lze dívat jako na verzi. Na pozadí to vypadá tak, že Git nejprve pořídí snapshot celého projektu. Poté vytvoří další objekt, tzv. commit, který přidá k snapshotu užitečné informace, jako:
- SHA-1 checksum snapshotu
- Autora a jeho email
- Datum vytvoření
- Zprávu
a dvě reference: na předchozí commit a snapshot. Díky referenci na snapshot ho budeme schopni načíst právě přes commit. A pomocí reference na předchozí commit je Git schopný sestavit historii jako seznam verzí. Můžeme si představit takto:
Commit je velmi důležitý objekt v Gitu, který nás dělí od absolutní anarchie. Kdybychom si dělali zálohy ručně, došli bychom k řešení BEZ commitů. Proto commit dává snapshotům význam a uspořádání. Většina operací s Gitem se bude týkat právě commitů.
Pozn.: Možná jste si někteří všimli, že lžu. Objekt snapshot v Gitu konkrétně není, je to definované trochu jinak. Ano, vím, ale pro účely představení tohoto systému, je to intuitivnější.
Vytvoření repozitáře
Pojďme si to uvést do praxe. Na úvod budeme verzovat jednoduchý projekt s dvěma soubory: README.md a main.py, které vypadají takto:
README.md
# Projekt Ukazkovy projekt pro praci s Gitem.
main.py
print("Hello, Git!")
Začneme tím, že si ve složce s projektem založíme prázdný repozitář pomocí git init.
$ git init
hint: Using 'master' as the name for the initial branch. This default branch name hint: is subject to change. To configure the initial branch name to use in all hint: of your new repositories, which will suppress this warning, call: hint: hint: git config --global init.defaultBranch <name> hint: hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and hint: 'development'. The just-created branch can be renamed via this command: hint: hint: git branch -m <name>
Initialized empty Git repository in /cesta/k/projektu/.git/
Tento poslední řádek je část výstupu, která nás zajímá. Říká, že se vytvořil prázdný repozitář.
Ještě bych zmínil, že Git se Vám bude snažit celou dobu pomoct. Bude Vám psát rady, jako vidíme výše.
Když si necháme vypsat všechny prvky ve složce, uvidíme, že přibyla složka pro repozitář: .git:

Než budeme pokračovat, ukážeme si užitečný příkaz git status, který nám ukáže stavy různých souborů: které soubory jsou změněné, a které jsou v indexu, ale nejsou commitnuté.
$ git status
On branch master No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
README.md
main.py
nothing added to commit but untracked files present (use "git add" to track)
Zde vidíme tzv. Untracked files. To jsou soubory, které jsme ještě neverzovali. Jakmile nějaký soubor začneme verzovat, zobrazí se jako tracked. Zároveň vidíme konkrétní soubory (README.md, main.py), které jsou v této kategorii.
Na posledním řádku se nám Git snaží opět radit. Pokud chceme soubory verzovat, musíme je nejprve přidat do indexu pomocí git add <file>. Pojďme to udělat:
$ git add README.md main.py
$ git status
On branch master No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: README.md
new file: main.py
Vidíme, že kategorie se změnila na Changes to be committed. To znamená, že soubory jsou v indexu a připraveny ke commitnutí.
Zde vidíme soubory, které jsou připraveny ke commitnutí.
Zároveň nám Git radí, jak soubory z indexu odstranit pomocí git rm --cached <file>. POZOR: Pokud bychom použili pouze git rm main.py, soubor by se nám odstranil z workspace!
Zbývá už jen vytvořit commit pomocí git commit.
$ git commit -m "initial"
[master (root-commit) 06659fe] initial 2 files changed, 4 insertions(+) create mode 100644 README.md create mode 100644 main.py
Možnost -m <msg> slouží k zadání zprávy. Zpráva je povinná a kdybychom možnost nepoužili, Git nám otevře textový editor a donutí nás zprávu napsat. Použít možnost -m je ale pro nás teď jednodušší.
Na historii se můžeme podívat pomocí git log. Tento příkaz má mnoho možností pro spoustu zajímavých dotazů. Prozatím nám stačí jeho základní verze.
$ git log
commit 06659fe9b3764ca7d905974f41643c7f5765ceca (HEAD -> master) Author: Martin Slachta <[email protected]> Date: Sun Mar 23 21:56:09 2025 +0100 initial
Zatím není historie nijak zajímavá – obsahuje pouze jeden commit. Vidíme: checksum, autora, datum a naši zprávu. Zpráva může být (a u větších projektů bývá) klidně více odstavců, proto tolik místa.
.gitignore
Menší vsuvka: přidávat soubory do indexu jeden po druhém je otravné, hlavně ve větších projektech. Existuje proto argument -a pro git add, který přidá všechny soubory do indexu.
Bohužel však ve většině projektů budou soubory, které verzovat nechcete. Například: zkompilované binární soubory, soukromé data nebo metadata editoru kódu. Proto můžeme vytvořit soubor .gitignore, kde na řádky napíšeme cesty, které chceme ignorovat. Git ho automaticky hledá a pokud najde, tak bude brát v potaz.
Podívejme se na to, jak takový .gitignore vypadá:
.gitignore
tmp/ soubor-ktery-nechcu.txt *.java
Můžeme použít i tzv. wildcards. Příkladem je *.java, což znamená, ignoruj všechny soubory s koncovkou .java, např. Program.java. Git díky tomu bude ignorovat všechny Java soubory.
.gitignore není třeba psát ručně. Pro většinu jazyků už existuje hotová verze, např. tady: https://github.com/github/gitignore.
Druhý commit
Pojďme udělat ještě jeden commit. Upravíme main.py:
main.py
print("Hello, Git!")
Pokud chceme spojit git add -a a git commit, můžeme použít možnost -a příkazu git commit, která před commitem přidá všechny soubory do indexu:
$ git commit -a -m "upraveni main.py"
[master 4d42964] upraveni main.py 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log
commit 4d42964ec7ea6a6724134debfb19bf0856084619 (HEAD -> master) Author: Martin Slachta <[email protected]> Date: Sun Mar 23 22:06:21 2025 +0100 upraveni main.py commit 06659fe9b3764ca7d905974f41643c7f5765ceca Author: Martin Slachta <[email protected]> Date: Sun Mar 23 21:56:09 2025 +0100 initial
Po drůhém commitu už log vypadá zajímavěji. Ukažme si ještě možnost --oneline, která historii vypíše přehledněji:
$ git log --oneline
4d42964 (HEAD -> master) upraveni main.py 06659fe initial
Každý commit se vypsal pouze na jeden řádek. Všimněme si také, že se zkrátili checksumy na pouhých 7 znaků. Ty stačí, protože žádný jiný commit nemá stejných prvních 7 znaků. Kdyby měl, Git by vypsal prvních 8, atd.
Možná jste si všimli jakéhosi (HEAD -> master). Interpretovat to můžeme tak, že HEAD je ukazatel a ukazuje na master. Oba termíny si vysvětlíme. HEAD je důležitý ukazatel v Gitu. Ukazuje na verzi, kterou máme aktuálně načtenou ve workspace. Zároveň když budeme dělat změny, Git se podívá na snapshot této verze, porovná ho s workspace a díky tomu určuje, co je změna.
Při skoku na jiný commit se HEAD jednoduše přesune na tento commit a Git načte do workspace novou verzi. Pokud ale máme ve workspace nějaké změněné soubory, přeskočit jinam nám Git nedovolí, dokud změny buď nezkartujeme nebo necommitneme.
To, na co HEAD ukazuje (->), je ale jakýsi master. To je název větve. Pojďme se na to podívat blíže.
Větve
Větve jsou v Gitu pro většinu lidí tzv. killer feature. Git umožňuje velmi snadno větvit, což znamená, že více commitů má stejného předchůdce. Tím se nám verze začnou rozcházet. Proč bychom to ale chtěli?
Větev je v Gitu jednoduše pohyblivá reference na commit. Tento commit budeme nazývat hlava větve a všechny jeho předchůdce vč. hlavy budeme nazývat historií větve. A to nám stačí. Commity, které nejsou součástí historie žádné větve, jsou v podstatě ztraceny.
Podívejme se na příklad rozvětvené historie:
Na obrázku vidíme modře commity, červeně větve a žlutě HEAD. Šipky z commitů ukazují na jejich předchůdce a šipky z větví na jejich hlavu.
Vidíme, že v historii větve master jsou commity: C7, C5, C2 a C1, kde C7 je její hlava. Větev dumb-idea má v historii commity: C4, C3, C2 a C1 a C4 je její hlava. Z toho vidíme, že dvě větve mají společné commity C2 a C1. Nakonec vidíme, že commit C6 není v historii žádné větve a je tedy považován za ztracený.
My jako uživatel můžeme mezi větvemi libovolně přeskakovat pomocí git checkout <nazev-vetve>. Tento příkaz v podstatě jen přemístí HEAD a načte novou verzi do workspace. Například git checkout dumb-idea by změnil historii takto (pouze se přesunula hlava):
Vytvoření větve
Větvit můžeme začít tak, že vytvoříme novou větev (referenci na commit) nad libovolným existujícím commitem. Představme si, že máme klasicky větev master, kde pracujeme na našem projektu. Najednou ale dostaneme trochu odvážnější nápad, jak naprogramovat novou feature. Nápadem si ale moc nejsme jistí, a nechceme tím kazit větev master. Vytvoříme tak novou větev dumb-idea.
$ git branch dumb-idea
Tento příkaz vytvoří nad commitem, na který ukazuje HEAD, novou větev s názvem dumb-idea. Když se podíváme do logu, uvidíme novou větev, která ukazuje na commit 4d42964:
$ git log --oneline
4d42964 (HEAD -> master, dumb-idea) upraveni main.py 06659fe initial
Všechny větve, které ukazují na commit, budou v logu v závorkách, oddělené čárkou.
Představit si to můžeme takto:
Pořád ale HEAD ukazuje na master. Přeskočit na novou větev můžeme pomocí:
$ git checkout dumb-idea
Switched to branch 'dumb-idea'
$ git log --oneline
4d42964 (HEAD -> dumb-idea, master) upraveni main.py 06659fe initial
Mezi větvemi můžeme libovolně skákat, a to je další obrovská výhoda Gitu. Větve jsou implementované tak lehké a rychlé, že přeskakovaní je téměř okamžité. Navíc při skoku Git jednoduše načte už připravený snapshot do workspace.
Teď když jsme v naší vlastní větvi dumb-idea, upravíme soubor main.py a uděláme commit.
main.py
text = "Hello, Git! I have a dumb idea!" print(text)
$ git commit -a -m "dumb idea"
[dumb-idea 223ec6d] dumb idea 1 file changed, 2 insertions(+), 1 deletion(-)
$ git log --oneline
223ec6d (HEAD -> dumb-idea) dumb idea 4d42964 (master) upraveni main.py 06659fe initial
V logu teď vidíme, že se nám větev dumb-idea posunula na nový commit. Větev master zůstala na místě:
A přesně to je způsob, jak v Gitu začít větvit. Kdykoliv se můžeme na master vrátit pomocí git checkout master, a tím bychom jen přesunuli HEAD. Zopakujme si ale jednu věc: jakmile uděláme skok s HEAD, načte se nový snapshot do workspace, a tím by se mohli přepsat změny, které jsme dělali a necommitnuli. Proto když Git najde změny v trackovaném souboru, nedovolí nám přeskočit na jinou větev, dokud neuděláme commit.
Skočme teď do větve master a udělejme commit. Nový commit bude mít jako předchůdce C2 a po vytvoření se na něj přesune master.
Shrnutí
Na libovolném commitu můžeme vytvořit větev pomocí:
$ git branch <nazev-vetve> <commit>
Na větev přeskočíme pomocí:
$ git checkout <nazev-vetve>
Skok na jinou větev znamená načtení nové verze do workspace a posun HEAD. Pokud máme ve workspace změny, Git nám skok nedovolí, dokud změny nezrušíme nebo neuděláme commit.
Když uděláme nový commit, jako jeho předchůdce bude commit, na který ukazuje HEAD. Následně se posune větev na tento commit. Ačkoliv vlastně HEAD neukazuje na commit ale větev, stačí posunou větev. HEAD pořád bude ukazovat na posunutou větev.
Na libovolném existujícím commitu můžeme vytvořit větev. Pokud no novou větev skočíme a začneme dělat commity, budeme posunovat pouze tuto novou větev. Žádná jiná větev se nezmění.
Merge
Pojďme přidat ještě jeden commit do našeho hloupého nápadu:
main.py
text = "Hello, Git! I have a good idea!" print(text)
$ git commit -a -m "good idea"
[dumb-idea 4c801c1] good idea 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline
4c801c1 (HEAD -> dumb-idea) good idea 223ec6d dumb idea 4d42964 (master) upraveni main.py 06659fe initial
Z našeho hloupého nápadu se stala geniální myšlenka, kterou bychom chtěli přidat do masteru. To můžeme udělat pomocí operace merge, která umí spojit obě větve do jedné. To může udělat vícero způsoby, pojďme si je představit a rozebrat:
Fast-forward
Nejjednodušší příklad je, kdybychom vytvořili větev dumb-idea a udělali merge ještě předtím, než něco stihlo přibýt do větve master.
Pojďme na tomto příkladě provést merge. Nejprve je nutné přeskočit na větev, do které chceme něco připojit: git checkout master. Pak už můžeme mergenout dumb-idea pomocí git merge dumb-idea. Výsledek bude vypadat takto:
Jediné, co Git udělal je, že přesunul větev master na C4. Ukážeme si to prakticky:
$ git checkout master
Switched to branch 'master'
$ git merge dumb-idea
Updating 4d42964..4c801c1 Fast-forward main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-)
$ git log --oneline
4c801c1 (HEAD -> master, dumb-idea) good idea 223ec6d dumb idea 4d42964 upraveni main.py 06659fe initial
V logu vidíme, že se větev master posunula na commit 4c801c1, který je posledním commit větve dumb-idea. Větev dumb-idea zůstala na místě. Tento způsob merge, který jen přesune naši větev na větev, kterou mergujeme, je tzv. fast-forward.
Merge commit
Problém je, že předchozí způsob jde opravdu použít jen, pokud v původní větvi nepřibyl žádný commit od doby, co jsme začali větvit. To je ale pouze ideální případ. Vraťme se zpátky a zkusme si, co se stane, když se větev master změní a historie bude vypadat třeba takto:
Kdybychom teď udělali merge, Git vytvoří tzv. merge commit a posune na ni větev master a historie tak po bude vypadat takto:
Proč to funguje? No najednou jsou všechny commity obou větví v historii větve master (jsou přístupné šipkami z nového commitu Merge). Uvědomme si, že merge commit je klasický commit jen s tím rozdílem, že má více předchůdců. Jako každý jiný commit ale musí mít zprávu. V minulém příkladě se žádný nový commit nevytvořil, a tak po nás Git žádnou zprávu ani nechtěl. Tentokrát by ji ale potřeboval. Proto můžeme použít možnost -m <msg> u příkazu git merge.
Opět si to ukážeme i prakticky. Pro commit C4 bychom například změnili soubor README.md:
README.md
# Projekt Ukazkovy projekt pro praci s Gitem. Ukazeme si i merge.
Následně skočíme do větve master a uděláme commit:
$ git checkout master
Switched to branch 'master'
$ git commit -a -m "upraveni README.md"
[master e0b7700] upraveni README.md 1 file changed, 2 insertions(+)
$ git log --oneline
e0b7700 (HEAD -> master) upraveni README.md 4d42964 upraveni main.py 06659fe initial
Nyní máme větev master na commitu e0b7700 a větev dumb-idea na commitu 4c801c1. V logu se nám ale teď ukáže jen historie větve master. Abychom viděli celou histrorii, můžeme použít --all a --graph:
$ git log --oneline --all --graph
* e0b7700 (HEAD -> master) upraveni README.md | * 4c801c1 (dumb-idea) good idea | * 223ec6d dumb idea |/ * 4d42964 upraveni main.py * 06659fe initial
Teď máme stále na řádku commit, ale vidíme i kde došlo k větvení. Napoprvé je to trochu nepřehledné, ale naštěstí už existují i grafické nástroje pro zobrazení historie. Ty jsou ale nad rámec tohoto přízpěvku.
Pojďme teď zkusit mergenout dumb-idea do masteru.
$ git merge dumb-idea -m "spojeni vetve dumb-idea do masteru"
Merge made by the 'ort' strategy. main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-)
$ git log --oneline --all --graph
* b9a804b (HEAD -> master) spojeni vetve dumb-idea do masteru
|\ | * 4c801c1 (dumb-idea) good idea | * 223ec6d dumb idea * | e0b7700 upraveni README.md |/ * 4d42964 upraveni main.py * 06659fe initial
Git vytvořil commit b9a804b, který má dva předchůdce. Zároveň si všimněme, že se posunula pouze větev master. dumb-idea o merge ani neví.
Super, tak větev dumb-idea se nám spojila s master bez problémů. Větev master se posunula na tento commit a větev dumb-idea zůstala na svém místě.
Merge konflikt
Může ale nastat situace, a to si pište, že nastane, kdy Git nebude schopný větve automaticky spojit. S tím je spojený jeden z velkých strašáků Gitu, tzv. merge konflikt. Znamená to, že se snažíme spojit větve, ale v obou se změnila stejná část stejného souboru. Git pak neví, jak má změny spojit dohromady. V ten moment merge pozastaví, označí všechny takové oblasti jako konflikty a nechá nás je vyřešit.
Jak se takový merge konflikt řeší? Vlastně to není ani zdaleka tak strašné a Git je ohledně toho opět velmi elegantní. Git přímo upraví soubory ve workspace, a se speciálním formátováním vloží do konfliktních souborů verzi oblasti z obou větví a společného commitu. Takže máme obě verze a můžeme si vybrat, kterou chceme nechat. Pokud chceme, můžeme klidně napsat i vlastní verzi.
Ukážeme si ilustrativní příklad. Pokud bychom měli takovouto historii:
A v commitu C4 a C3 bychom měli změny v souboru README.md, tak by konflikt, který by vznikl merge, vypadal v README.md takto:
README.md
Obsah souboru před blokem konfliktu. <<<<<<<
Změny z větve, do které mergujeme.
|||||||
Verze ve společném commitu obou větví.
=======
Změny z větve, kterou mergujeme.
>>>>>>> Obsah souboru za blokem konfliktu.
Barvy sedí pro commity v předchozí ilustraci :)
Pro každý konflikt Git takovýto blok vytvoří. Ohraničený bude právě <<<<<<< a >>>>>>>.
Verze ve společném commitu se může hodit, protože může dát kontext tomu, proč se změny dělaly.
Pojďme si to ukázat na příkladu. Představme si, že před merge jsme ještě přidali commit, kde jsme upravili main.py:
main.py
print("Hello, Git! I am master.")
A vytvoříme commit a uděláme merge:
$ git commit -a -m "uprava main.py -- vypis vetve"
[master 32dd586] uprava main.py -- vypis vetve 1 file changed, 1 insertion(+), 1 deletion(-)
$ git merge dumb-idea -m "spojeni vetve dumb-idea do masteru"
Auto-merging main.py CONFLICT (content): Merge conflict in main.py Automatic merge failed; fix conflicts and then commit the result.
Vidíme, že selhal automatický merge. Upozornil nás na merge konflikt v main.py. Stále nevyřešené konflikty vidíme pomocí git status:
$ git status
On branch master
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: main.py
no changes added to commit (use "git add" and/or "git commit -a")
Pokud konflikt nechceme řešit a chceme to vzdát, můžeme použít git merge --abort. K mergi by překvapivě nedošlo a větev by se vrátila do stavu před ním (zase by vrátila konflikty).
Pojďme se podívat, jak takový konflikt v main.py vypadá:
main.py
<<<<<<< HEAD
print("Hello, Git! I am master.")
||||||| 4d42964
print("Hello, Git!")
=======
text = "Hello, Git! I have a good idea."
print(text)
>>>>>>> dumb-idea
Celkem děsivé. Důležitá je ale část mezi <<<<<<< HEAD a =======. To je náš původní kód. Mezi ======= a >>>>>>> dumb-idea je kód z větve, kterou mergujeme. My musíme ručně vybrat, který kód chceme nechat. Popř. kód můžeme klidně celý přepsat. Upravíme tedy soubor např. takto:
main.py
print("Hello, Git! I am master and I have a good idea.")
Tohle je validní vyřešení konfliktu. Teď stačí soubor přidat do indexu a udělat commit:
$ git add main.py
$ git status
On branch master
All conflicts fixed but you are still merging.
(use "git commit" to conclude merge)
Changes to be committed:
modified: main.py
Git nám říká, že konflikty jsou vyřešené. Také radí, ať merge dokončíme pomocí git commit.
$ git commit -m "spojeni vetve dumb-idea do masteru"
[master 40f1039] spojeni vetve dumb-idea do masteru
A hotovo! Máme za sebou to nejtěžší z celého Gitu!
Spolupráce
Pojďme se konečně podívat na spolupráci mezi více lidmi. Ale nebojte, všechno důležité už umíte. Tohle už je jen aplikace toho, co jsme si řekli.
Git je distribuovaný systém. To znamená, že každý má svůj vlastní repozitář. Spolupráce tak spočívá v posílání našich commitů a větví ostatním do jejich repozitářů. Git jako distribuovaný systém k tomu používá push a pull.
Musíme si uvědomit, že každý, s kým spolupracujeme na konkrétním projektu, má svůj vlastní repozitář. Pro spolupráci se v Gitu zvolí jeden hlavní repozitář, který bude dostupný na serveru. K tomu můžeme využít službu jako GitHub nebo GitLab.
Vzdálené repozitáře
V Gitu máme tzv. vzdálený repozitář. To je repozitář, se kterým můžeme pracovat vzdáleně. Co nám to umožní je propagovat naše změny na tento repozitář, nebo z něj změny získat. Vzdálené repozitáře si pojmenováváme. Z toho plyne, že jich můžeme mít více. Jeden nám teď stačí.
GitHub
Ukážeme si, jak založit hlavní repozitář na službě GitHub a označit si ho jako vzdálený u nás na zařízení. Následně pošleme repozitáři naše změny a ověříme, že na vzdáleném repozitáři jsou. Nakonec si nasimulujeme kamaráda vývojáře, který také pushne své změny, a které si pullneme.
Začneme tím, že si vytvoříme účet na službě GitHub. Jakmile budeme na hlavní obrazovce, klikneme na New repository.

Otevře se nám formulář, do kterého vyplníme základní informace o repozitáři, jako název a popisek. Krom toho není třeba nic upravovat.

Po kliknutí na Create repository už máme úspěšně vytvořený prázdný repozitář. Dokonce nám Git radí, jak začít:

Radí nám několik variant. Žádná zatím není pro nás. My si tento repozitář označíme jako vzdálený se jménem origin, pomocí:
$ git remote add origin [email protected]:Bequen/example-repo.git
Pozor: vzdálený repozitář si můžeme pojmenovat jakkoliv! Název origin je pouze často používaný název pro hlavní repozitář. Jestli si ho ale chceme pojmenovat knedlik, tak proč ne:
$ git remote add knedlik [email protected]:Bequen/example-repo.git
Příkaz je sice dlouhý, ale v celku intuitivní. Pokud bychom vzdálený repozitář pojmenovaný origin chtěli odebrat, použijeme:
$ git remote rm origin
Push
Sdílení práce s ostatními spočívá v push našich změn na vzdálený repozitář a v pull změn ze vzdáleného repozitáře. Push propaguje naše změny na zvolený vzdálený repozitář. Pull stáhne změny z vzdáleného repozitáře do našeho. Obě akce se vztahují vždy pouze na jednu konkrétní větev a jeden konkrétní vzdálený repozitář. Příkazy vypadají takto:
$ git push <vzdálený-repozitář> <větev>
$ git pull <vzdálený-repozitář> <větev>
Pojďme nejprve pushnout naši větev master pomocí git push origin master.
$ git push origin master
Enumerating objects: 22, done. Counting objects: 100% (22/22), done. Delta compression using up to 16 threads Compressing objects: 100% (18/18), done. Writing objects: 100% (22/22), 2.04 KiB | 1.02 MiB/s, done. Total 22 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0) remote: Resolving deltas: 100% (1/1), done.
To github.com:Bequen/example-repo.git
* [new branch] master -> master
Všimněte si, že Git nám říká, že jsme vytvořili novou větev master na vzdáleném repozitáři. Do té doby byl vzdálený repozitář úplně prázdný.
Když se teď podíváme na GitHub, uvidíme naše soubory:

Podívejme se i do logu, kde uvidíme zajímavou věc:
$ git log --oneline --graph --all
* 40f1039 (HEAD -> master, origin/master, origin/HEAD) spojeni vetve dumb-idea do masteru
|\ | * 4c801c1 good idea | * 223ec6d dumb idea * | 32dd586 uprava main.py -- vypis vetve * | e0b7700 upraveni README.md |/ * 4d42964 upraveni main.py * 06659fe initial
Vidíme, že u hlavy master máme i origin/master. To je větev, která je na vzdáleném repozitáři. To znamená, že s těmito větvemi ze vzdálených repozitářů můžeme pracovat, jako by byly přímo u nás. To je elegantní virtualizace. Pojďme se teď podívat na pull.
Clone
Nasimulujeme si kamaráda, který chce na projektu taky pracovat. Nejprve si repozitář musí naklonovat k sobě, aby na něm mohl pracovat, pomocí git clone:
$ git clone [email protected]:Bequen/example-repo.git /kam/se/ma/repo/naklonovat
Ve složce /kam/se/ma/repo/naklonovat se mu automaticky připraví .git a workspace, jako by byl checkoutnutý na master.
V logu uvidí to samé co my:
$ git log --oneline --graph --all
* 40f1039 (head -> master, origin/master, origin/head) spojeni vetve dumb-idea do masteru |\ | * 4c801c1 good idea | * 223ec6d dumb idea * | 32dd586 uprava main.py -- vypis vetve * | e0b7700 upraveni readme.md |/ * 4d42964 upraveni main.py * 06659fe initial
Pull
Představme si, že kamarád změnil soubor main.py a udělal commit:
$ git commit -a -m "uprava main.py -- rozdeleni na dva radky"
[master 047f718] uprava main.py -- rozdeleni na dva radky 1 file changed, 2 insertions(+), 1 deletion(-)
$ git push origin master
Enumerating objects: 5, done. Counting objects: 100% (5/5), done. Delta compression using up to 16 threads Compressing objects: 100% (3/3), done. Writing objects: 100% (3/3), 365 bytes | 365.00 KiB/s, done. Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To github.com:Bequen/example-repo.git
40f1039..047f718 master -> master
Na posledním řádku výstupu vidíme, že jsme udělali push do masteru. Zároveň vidíme úsek historie, kterou jsme pushnuli: 40f1039 až 047f718, což sedí.
Teď si zkusíme commit stáhnout k sobě. Uděláme to pomalu a místo git pull použijeme git fetch. Ten totiž aktualizuje pouze větev origin/master.
$ git fetch origin master
remote: Enumerating objects: 5, done. remote: Counting objects: 100% (5/5), done. remote: Compressing objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0 (from 0) Unpacking objects: 100% (3/3), 345 bytes | 345.00 KiB/s, done.
From github.com:Bequen/example-repo
40f1039..047f718 master -> origin/master
Zde vidíme, že se nám stáhla větev master z origin do origin/master.
Pojďme se na obě větve, master a origin/master, podívat:
$ git log master --oneline --graph
* 40f1039 (HEAD -> master) spojeni vetve dumb-idea do masteru |\ | * 4c801c1 (dumb-idea) good idea | * 223ec6d dumb idea * | 32dd586 uprava main.py -- vypis vetve * | e0b7700 upraveni README.md |/ * 4d42964 upraveni main.py * 06659fe initial
$ git log origin/master --oneline --graph --all
* 047f718 (origin/master, origin/HEAD) uprava main.py -- rozdeleni na dva radky
* 40f1039 (HEAD -> master) spojeni vetve dumb-idea do masteru |\ | * 4c801c1 (dumb-idea) good idea | * 223ec6d dumb idea * | 32dd586 uprava main.py -- vypis vetve * | e0b7700 upraveni README.md |/ * 4d42964 upraveni main.py * 06659fe initial
Skutečně vidíme, že nový commit od kamaráda: 047f718, je pouze v origin/master.
Co teď? No pokud chceme dostat změny do naší větve master, musíme udělat merge. To uděláme pomocí git merge origin/master.
$ git merge origin/master
Updating 40f1039..047f718 Fast-forward main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-)
Podívejme se do logu a ověřme, že se změny dostaly do naší větve:
$ git log --graph --oneline
* 047f718 (HEAD -> master, origin/master, origin/HEAD) uprava main.py -- rozdeleni na dva radky
* 40f1039 spojeni vetve dumb-idea do masteru |\ | * 4c801c1 (dumb-idea) good idea | * 223ec6d dumb idea * | 32dd586 uprava main.py -- vypis vetve * | e0b7700 upraveni README.md |/ * 4d42964 upraveni main.py * 06659fe initial
Vidíme, že na poslední commit už neukazuje jen větev origin/master, ale i náš lokální master, na který ukazuje HEAD.
Tak a máme to! Stejně bychom poslali naše změny my. Ještě upozorním, že i tento merge může způsobit konflikt, jako klasický merge. Ale to už všechno znáte :)
A teď si řekneme, že git pull origin master je vlastně zkratka za git fetch origin master a git merge origin/master.
Rebase
Máme ale menší problém. Pokaždé, co si chceme aktualizovat náš repozitář, děláme v podstatě merge. To je problém, protože pokaždé riskujeme, že budeme mít merge commit nebo nedej bože merge konflikt. Proto existuje druhý přístup, tzv. rebase. Ten pouze přepojí naše commity na poslední commit ze vzdálené větve. Ukažme si to na příkladu. Představme si, že někdo jiný pushnul commit C5 do origin/master, zatímco my jsme vytvořili commity C3 a C4. Naše historie by vypadala takto:
Uděláme git fetch a místo merge uděláme git rebase origin/master. Co se stane? Git vezme naše commity C3 a C4 a přepojí je na poslední commit C5. Takže naše historie bude vypadat takto:
Ukážeme si to na praktickém příkladě. Vrátíme náš předchozí merge, který proběhl jako fast-forward, a uděláme ještě commit:
$ git log --all --oneline --graph
* 597db22 (HEAD -> master) upraven popisek | * 047f718 (origin/master, origin/HEAD) uprava main.py -- rozdeleni na dva radky |/ * 40f1039 spojeni vetve dumb-idea do masteru |\ | * 4c801c1 (dumb-idea) good idea | * 223ec6d dumb idea * | 32dd586 uprava main.py -- vypis vetve * | e0b7700 upraveni README.md |/ * 4d42964 upraveni main.py * 06659fe initial
Jsme na větvi master a uděláme git rebase origin/master:
$ git rebase origin/master
Successfully rebased and updated refs/heads/master.
$ git log --oneline --all --graph
* 4a94fbd (HEAD -> master) upraven popisek * 047f718 (origin/master, origin/HEAD) uprava main.py -- rozdeleni na dva radky * 40f1039 spojeni vetve dumb-idea do masteru |\ | * 4c801c1 (dumb-idea) good idea | * 223ec6d dumb idea * | 32dd586 uprava main.py -- vypis vetve * | e0b7700 upraveni README.md |/ * 4d42964 upraveni main.py * 06659fe initial
Vidíme, že nám commit s upraven popisek přesunul na poslední commit origin/master, který byl uprava main.py -- .... Možná jste si všimli jedné, možná nepříjemné věci. Nepoužil jsem hash commitu, ale jen zprávu, protože hash je jiný. V hashsumu je totiž i odkaz na předchozí commit. Když ale chceme předchozí commit změnit, Git vytvoří nový commit, a tím pádem jiným hashsumem. Na to pozor, protože pokud někdo zase pracoval s původním commitem 597db22, tak má teď docela problém.
V případě, že rebase používáme pouze při fetch, je to neškodné. Ale rebase lze použít na libovolné větve, nejen ty vzdálené. Někde se používá workflow, že se merge nedělá vůbec, takže když chcete přidat své změny do hlavní větve master, uděláte rebase své větve na master. V komunitě panuje velká debata, který workflow je lepší. Každá má pro a proti:
-
Rebase:
- dělá historii přehlednější, protože neobsahuje merge commity
- musíte si dát veliký pozor, abyste rebasem nepřepsali commit, na který někdo spoléhá
-
Merge
- historie se nemění – nic neriskujete
- historie se může stát méně přehlednou, protože například
git log --graphbude hodně široký
Nakonec si řekneme, že:
$ git fetch origin master
$ git merge origin/master
Lze udělat najednou pomocí:
$ git pull origin master
A pokud chceme udělat rebase místo merge tak:
$ git pull --rebase origin master
Shrnutí
Už víme, co je to vzdálený repozitář. Umíme si jeden zřídit na GitHubu a následně si ho přidat lokálně pomocí:
$ git remote add <jméno> <url>
Zároveň umíme vzdálený repozitář naklonovat, pokud už někde existuje:
$ git clone <url>
Také umíme pushnout naše změny z větve do vzdáleného repozitáře:
$ git push <vzdálený-repozitář> <větev>
A stáhnout změny z větve vzdáleného repozitáře:
$ git fetch <vzdálený-repozitář> <větev>
Také víme, jak se vzdálenými větvemi pracovat pomocí git rebase a git merge:
$ git rebase <vzdálený-repozitář>/<větev>
$ git merge <vzdálený-repozitář>/<větev>
A známe příkaz, který udělá fetch a merge:
$ git pull <vzdálený-repozitář> <větev>