Moje odkazy
Obsah článku:
vydáno: 16. 6. 2022 23:46, aktualizováno: 16. 6. 2022 23:46
Git a Mercurial jsou stejná generace verzovacích systémů: distribuované VCS. Vznikly v roce 2005 jen pár měsíců po sobě. Jednou z nevýhod Gitu oproti Mercurialu je to, že nepodporuje kopírování souborů.
Zatímco v Mercurialu máme hg mv
a hg cp
, v Gitu máme jen git mv
.
Soubor – myšleno včetně jeho historie – můžeme v Gitu jen přesunout, ale ne zkopírovat. Tedy alespoň ne jednoduše… V dnešním článku si ukážeme trik, jak i v Gitu soubory kopírovat včetně jejich historie nebo přesouvat jejich části jinam, což se hodí obecně, i např. v Mercurialu nebo jiných VCS.
Historie verzovacího systému obsahuje cenné informace, které se hodí, když studujeme nebo upravujeme starší kód – je dobré vědět, kdo, kdy a hlavně proč danou část kódu napsal. To nám dává potřebný kontext, bez kterého by pochopení dané logiky bylo obtížnější a zdlouhavější. V ideálním případě dohledáme konkrétní změnový požadavek, na základě kterého byla daná úprava do kódu zanesena.
Pokud kód jen připisujeme, historie se zachovává automaticky. Horší je to ale v případě, kdy refaktorujeme např. přesuneme některé metody do samostatných souborů. Git v tomto případě ztratí historii přesunutých řádků a jako autora eviduje toho, kdo je přesunul. Existuje sice volba git log --follow
(Git se pak snaží uhodnout, kdy šlo o kopii nebo přesun), ale to je takové pseudo-řešení ve stylu s křížkem po funuse a s podobným refaktoringem nám stejně nepomůže.
Oproti tomu v Mercurialu uděláme jen např. hg cp PůvodníTřída.java NováTřída.java
v původním souboru metodu smažeme, v novém smažeme všechny ostatní a změníme název třídy. Nebo uděláme refaktoring – vytažení metody do nové třídy – pomocí IDE a následně jen řekneme verzovacímu systému, že má soubor evidovat jako kopii: hg cp --after PůvodníTřída.java NováTřída.java
. Historie zůstane zachována a autorem přesunutých řádků je stále původní autor, nikoli ten, kdo je přesunul.
Stejně dobře funguje kopírování souborů i v – dnes již poněkud obstarožním – systému Subversion. Zde uděláme jen svn cp PůvodníTřída.java NováTřída.java
a historie zůstane zachována. Funkce blame/annotate pak funguje i ve všech nástrojích jako např. Netbeans IDE, kde můžeme hezky sledovat historii nebo autorství jednotlivých řádků.
Abychom dosáhli podobného výsledku v Gitu, musíme vytvořit novou větev. V ní soubor přejmenujeme (kód v tuhle chvíli nemusí dávat smysl, nemusí jít ani zkompilovat, ale to nevadí). Následně se přepneme na původní větev a začleníme větev s přejmenovaným souborem. Musíme ale říct Gitu, aby nedělal tzv. fast-forward (pouhé posunutí štítku větve/záložky bez přidání nového uzlu a dvou hran do grafu) a aby nás nechal do kódu ještě sáhnout a udělat další změny.
echo "Ahoj," >> ahoj.txt
echo "tohle psala" >> ahoj.txt
echo "Dominika" >> ahoj.txt
git add ahoj.txt
git commit --author="Dominika <dominika@example.com>" -m "vytvořen soubor ahoj.txt"
git checkout -b kopie
git mv ahoj.txt nazdar.txt
git commit --author="Howard <howard@example.com>" -m "přejmenování: ahoj.txt → nazdar.txt"
git checkout master
git merge --no-ff --no-commit kopie
git reset HEAD ahoj.txt
git checkout ahoj.txt
git commit --author="Howard <howard@example.com>" -m "kopie: ahoj.txt → nazdar.txt …"
git branch --delete kopie # v příkladu jsem pomocnou větev pro názornost zachoval
Graf historie vypadá takto:
$ git log --all --graph
* commit 84a6cd6c13f971289a4cdc040662e633e35b5608 (HEAD -> master)
|\ Merge: ad7b3ba 0c7f338
| | Author: Howard <howard@example.com>
| | Date: Sun Jun 12 22:31:12 2022 +0200
| |
| | kopie: ahoj.txt → nazdar.txt
| | Merge branch 'kopie'
| |
| * commit 0c7f338dec62a6e5a97d2e70ea60599ba15fbaa5 (kopie)
|/ Author: Howard <howard@example.com>
| Date: Sun Jun 12 22:30:02 2022 +0200
|
| přejmenování: ahoj.txt → nazdar.txt
|
* commit ad7b3ba1d4d855b999b25b5458a7efe363840696 (origin/master)
Author: Dominika <dominika@example.com>
Date: Sun Jun 12 22:28:37 2022 +0200
vytvořen soubor ahoj.txt
A autorem řádků v obou souborech je Dominika:
$ git blame nazdar.txt
^ad7b3ba ahoj.txt (Dominika 2022-06-12 22:28:37 +0200 1) Ahoj,
^ad7b3ba ahoj.txt (Dominika 2022-06-12 22:28:37 +0200 2) tohle psala
^ad7b3ba ahoj.txt (Dominika 2022-06-12 22:28:37 +0200 3) Dominika
$ git blame ahoj.txt
^ad7b3ba (Dominika 2022-06-12 22:28:37 +0200 1) Ahoj,
^ad7b3ba (Dominika 2022-06-12 22:28:37 +0200 2) tohle psala
^ad7b3ba (Dominika 2022-06-12 22:28:37 +0200 3) Dominika
Vidíme i původní datum a u souboru nazdar.txt
je uvedeno, že řádky vznikly původně v souboru ahoj.txt
.
Stažení příkladu:
git clone https://git-zaloha.frantovo.cz/git.frantovo.cz/git-cp/01-prosta-kopie/
Alternativní postup je, že původní soubor přejmenujeme na nějaký dočasný název, máme trochu jednodušší git merge
a následně soubor zase přejmenujeme zpět. Ale to mi připadá méně vhodné, protože do historie zanášíme pomocné přejmenování, které tam z logiky věci být nemělo.
Podobně jako v předchozím případě postupujeme, když děláme refaktoring a přesouváme např. některé metody do nové třídy. Začátek je stejný a před posledním git commit
změníme název třídy uvnitř nového souboru a v obou smažeme část, která patří do toho druhého.
Původní zdroj:
public class PrvniTrida {
private int pocitadlo = 0;
public void pozdrav() {
System.out.println("ahoj " + ++pocitadlo);
}
public static void main(String[] args) {
PrvniTrida t = new PrvniTrida();
t.pozdrav();
t.pozdrav();
t.pozdrav();
}
}
Postup:
git add PrvniTrida.java
git commit --author="Dominika <dominika@example.com>" -m "vytvořen soubor PrvniTrida.java"
git checkout -b kopie
git mv PrvniTrida.java DruhaTrida.java
git commit --author="Howard <howard@example.com>" -m "přejmenování: PrvniTrida.java → DruhaTrida.java"
git checkout master
git merge --no-ff --no-commit kopie
git reset HEAD PrvniTrida.java
git checkout -- PrvniTrida.java
# upravíme obě třídy, aby každá obsahovala svoji část kódu
git commit --author="Howard <howard@example.com>" -m "refaktoring metody: PrvniTrida.java → DruhaTrida.java …"
Autorem metody pozdrav()
a proměnné pocitadlo
je pořád Dominika, přestože se kód teď nachází v jiném souboru:
$ git blame PrvniTrida.java ^4f367a2 (Dominika 2022-06-14 18:57:30 +0200 1) public class PrvniTrida { ^4f367a2 (Dominika 2022-06-14 18:57:30 +0200 2) ^4f367a2 (Dominika 2022-06-14 18:57:30 +0200 3) public static void main(String[] args) { a4e820b6 (Howard 2022-06-14 18:59:36 +0200 4) DruhaTrida t = new DruhaTrida(); ^4f367a2 (Dominika 2022-06-14 18:57:30 +0200 5) t.pozdrav(); ^4f367a2 (Dominika 2022-06-14 18:57:30 +0200 6) t.pozdrav(); ^4f367a2 (Dominika 2022-06-14 18:57:30 +0200 7) t.pozdrav(); ^4f367a2 (Dominika 2022-06-14 18:57:30 +0200 8) } ^4f367a2 (Dominika 2022-06-14 18:57:30 +0200 9) ^4f367a2 (Dominika 2022-06-14 18:57:30 +0200 10) } $ git blame DruhaTrida.java a4e820b6 DruhaTrida.java (Howard 2022-06-14 18:59:36 +0200 1) public class DruhaTrida { ^4f367a2 PrvniTrida.java (Dominika 2022-06-14 18:57:30 +0200 2) ^4f367a2 PrvniTrida.java (Dominika 2022-06-14 18:57:30 +0200 3) private int pocitadlo = 0; ^4f367a2 PrvniTrida.java (Dominika 2022-06-14 18:57:30 +0200 4) ^4f367a2 PrvniTrida.java (Dominika 2022-06-14 18:57:30 +0200 5) public void pozdrav() { ^4f367a2 PrvniTrida.java (Dominika 2022-06-14 18:57:30 +0200 6) System.out.println("ahoj " + ++pocitadlo); ^4f367a2 PrvniTrida.java (Dominika 2022-06-14 18:57:30 +0200 7) } ^4f367a2 PrvniTrida.java (Dominika 2022-06-14 18:57:30 +0200 8) ^4f367a2 PrvniTrida.java (Dominika 2022-06-14 18:57:30 +0200 9) }
Stažení příkladu:
git clone https://git-zaloha.frantovo.cz/git.frantovo.cz/git-cp/02-presun-kodu-do-noveho-souboru/
Graf verzí vypadá obdobně jako v předchozím případě:
$ git log --all --graph * commit a4e820b6c91e3f9422e107aee38007d8b965db03 (HEAD -> master, origin/master) |\ Merge: 4f367a2 2de9241 | | Author: Howard <howard@example.com> | | Date: Tue Jun 14 18:59:36 2022 +0200 | | | | refaktoring metody: PrvniTrida.java → DruhaTrida.java | | Merge branch 'kopie' | | | * commit 2de924134fe486d977cfc4593f3be8262ed444e4 (origin/kopie, kopie) |/ Author: Howard <howard@example.com> | Date: Tue Jun 14 18:58:23 2022 +0200 | | přejmenování: PrvniTrida.java → DruhaTrida.java | * commit 4f367a2368e9cb3eafc68fc576fcad56cf9195b8 Author: Dominika <dominika@example.com> Date: Tue Jun 14 18:57:30 2022 +0200 vytvořen soubor PrvniTrida.java
Zde mimochodem vidíme, že v rámci git merge
se můžou odehrát i významné zásahy do kódu potažmo do logiky programu. Nelze tedy automaticky předpokládat, že tyto operace ve VCS jsou neškodné a že program bude (správně) fungovat protože přece obě verze, které se tu spojily, fungovaly. Dokonce i když do tohoto procesu nikdo ručně nezasahuje a git merge
proběhne „hladce“, výsledkem může být nezkompilovatelný kód nebo nefunkční program.
Tento případ je hodně podobný, ale liší se v tom, že soubor DruhaTrida.java
již existuje, takže ho ve větvi kopie
přepíšeme souborem PrvniTrida.java
, abychom správně navázali historii řádků, které chceme přesunout. Musíme to ale udělat ve dvou krocích, protože jinak by to Git považoval za smazání souboru PrvniTrida.java
a modifikaci DruhaTrida.java
, nikoli za přejmenování/přepis. Kód je v tu chvíli nezkompilovatelný, ale to nevadí. Po přepnutí se zpět na hlavní větev začneme git merge
a před jeho dokončením pomocí git commit
dáme vše zase do pořádku.
$ git log --all --graph * commit db95c22d204717122b28b426147d92cca08ebda8 (HEAD -> master, origin/master) |\ Merge: 39fd543 2b175f9 | | Author: Howard <howard@example.com> | | Date: Thu Jun 16 00:56:43 2022 +0200 | | | | refaktoring metody: PrvniTrida.java → DruhaTrida.java | | Merge branch 'kopie' | | | * commit 2b175f939bfb7065c38e4c9b9ba3056a37054599 (kopie) | | Author: Howard <howard@example.com> | | Date: Thu Jun 16 00:53:41 2022 +0200 | | | | přejmenování: PrvniTrida.java → DruhaTrida.java | | | * commit a2fb5b3231680c4358e555c0cf73fe49d58f62de |/ Author: Howard <howard@example.com> | Date: Thu Jun 16 00:53:19 2022 +0200 | | smazání: DruhaTrida.java | * commit 39fd5435e2b818b2e620ea11fc3697232e707732 Author: Dominika <dominika@example.com> Date: Thu Jun 16 00:48:55 2022 +0200 vytvořeny soubory PrvniTrida.java a DruhaTrida.java
Stažení příkladu:
git clone https://git-zaloha.frantovo.cz/git.frantovo.cz/git-cp/03-presun-kodu-do-jineho-souboru/
Podle Gitu je autorem všech řádků v druhém souboru Dominika:
$ git blame PrvniTrida.java ^39fd543 (Dominika 2022-06-16 00:48:55 +0200 1) public class PrvniTrida { ^39fd543 (Dominika 2022-06-16 00:48:55 +0200 2) ^39fd543 (Dominika 2022-06-16 00:48:55 +0200 3) public static void main(String[] args) { db95c22d (Howard 2022-06-16 00:56:43 +0200 4) DruhaTrida t = new DruhaTrida(); ^39fd543 (Dominika 2022-06-16 00:48:55 +0200 5) t.pozdrav(); ^39fd543 (Dominika 2022-06-16 00:48:55 +0200 6) t.pozdrav(); ^39fd543 (Dominika 2022-06-16 00:48:55 +0200 7) t.pozdrav(); ^39fd543 (Dominika 2022-06-16 00:48:55 +0200 8) } ^39fd543 (Dominika 2022-06-16 00:48:55 +0200 9) ^39fd543 (Dominika 2022-06-16 00:48:55 +0200 10) } $ git blame DruhaTrida.java ^39fd543 DruhaTrida.java (Dominika 2022-06-16 00:48:55 +0200 1) public class DruhaTrida { ^39fd543 DruhaTrida.java (Dominika 2022-06-16 00:48:55 +0200 2) ^39fd543 PrvniTrida.java (Dominika 2022-06-16 00:48:55 +0200 3) private int pocitadlo = 0; ^39fd543 PrvniTrida.java (Dominika 2022-06-16 00:48:55 +0200 4) ^39fd543 PrvniTrida.java (Dominika 2022-06-16 00:48:55 +0200 5) public void pozdrav() { ^39fd543 PrvniTrida.java (Dominika 2022-06-16 00:48:55 +0200 6) System.out.println("ahoj " + ++pocitadlo); ^39fd543 PrvniTrida.java (Dominika 2022-06-16 00:48:55 +0200 7) } ^39fd543 PrvniTrida.java (Dominika 2022-06-16 00:48:55 +0200 8) ^39fd543 DruhaTrida.java (Dominika 2022-06-16 00:48:55 +0200 9) }
Ona ale ve skutečnosti napsala jen řádky 1 a 9, zatímco ty zbývající tam transplantoval Howard – ze souboru, který tedy rovněž psala Dominika, ale její kód se původně nacházel v jiném kontextu…
Postupnými úpravami kódu se můžeme dostat do stavu, kdy program sice dělá, co má, ale uspořádání metod nebo třeba konstant není optimální – logičtější a přehlednější by bylo jiné pořadí. V tu chvíli máme dilema, jestli kód refaktorovat a přijít o historii jednotlivých řádků nebo ne. Ve skutečnosti je to dilema falešné, protože můžeme mít oboje – jen je s tím trochu práce. Opět to můžeme řešit tím, že vytvoříme novou větev, čímž získáme dva předky a díky tomu se nám historie jednotlivých řádků správně naváže. V této pomocné větvi soubor trochu pročistíme a necháme v něm jen relevantní části.
Stažení příkladu:
git clone https://git-zaloha.frantovo.cz/git.frantovo.cz/git-cp/04-presun-kodu-do-tehoz-souboru/
V první verzi, kterou psala Dominika, byla na začátku metoda main()
a až za ní proměnná a metoda pozdrav()
. Howard pořadí změnil a metodu main()
přesunul na konec souboru, ale díky tanečkům s pomocnou větví, zůstala autorem kódu Dominika:
$ git blame PrvniTrida.java ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 1) public class PrvniTrida { ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 2) ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 3) private int pocitadlo = 0; 1162429c (Howard 2022-06-16 20:28:22 +0200 4) ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 5) public void pozdrav() { ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 6) System.out.println("ahoj " + ++pocitadlo); ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 7) } 1162429c (Howard 2022-06-16 20:28:22 +0200 8) ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 9) public static void main(String[] args) { ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 10) PrvniTrida t = new PrvniTrida(); ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 11) t.pozdrav(); ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 12) t.pozdrav(); ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 13) t.pozdrav(); ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 14) } ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 15) ^06b3450 (Dominika 2022-06-16 01:18:37 +0200 16) }
Pokud by Howard neudělal pomocnou větev a následně git merge --no-ff --no-commit kopie
, Git by to považoval za smazání řádků a vytvoření nových, jejichž autorství by přisuzoval Howardovi. Informace o tom, kdo je skutečným autorem a z jaké doby kód pochází, by se ztratila.
Dnes jsme si ukázali, jak dělat refaktoring a zachovat při tom původní informace o autorství a další metadata. Vzhledem k tomu, že v Gitu je tento postup poměrně pracný (a přidává do grafu další odbočku), ho asi nebudeme používat vždy – nebo ho automatizujeme pomocí skriptů. Tak jako tak, jsou ale případy, kdy se ta práce a pečlivost vyplatí a zachování historie a kontinuita stojí za to.
Dále bychom měli pamatovat na to, že s historií ve verzovacím systému jde poměrně kreativně manipulovat. A to i v případě, kdy vyloženě nefalšujeme jméno autora a datum (tam si můžeme napsat, co chceme). Metadata ve verzovacím systému mohou být na první pohled zavádějící, i když přímočarým podvodům zabráníme a nastavíme náš server tak, aby při přijímání push
operace kontroloval, že datum je smysluplné a že autor jednotlivých commitů je shodný s uživatelem, který provádí push
. Jednotlivé řádky psané někým jiným totiž můžeme při troše snahy poskládat tak, že z toho bude úplně jiný program, než dotyčný napsal. Historie ve verzovacím systému nám pomáhá, pokud je pravdivá a byla budována s dobrým úmyslem. Pokud ale provádíme audit nebo obecně předpokládáme i zlý úmysl, nesmíme se nechat zmást výpisem git blame
– jednotlivé řádky sice mohou pocházet od důvěryhodného autora a můžou být i dost staré (prověřené časem), ale někdo je mohl přeskládat tak, že program teď dělá něco úplně jiného. Nemluvě o tom, že smysl programu lze zásadně změnit i tím, že některé řádky jednoduše smažeme, což v git blame
není vidět vůbec.
Některé verzovací systémy (Monotone) staví již v základu na kryptografii a vše podepisují. V jiných (Mercurial, Fossil, Git, Bazaar, Darcs) můžeme podepisovat alespoň volitelně pomocí GPG. Nicméně i při použití kryptografie můžeme věřit jen celým podepsaným verzím, nikoli jednotlivým řádkům (i když je jejich autorství nesporné, mohly být vytrženy z kontextu).
Témata: [hack] [verzovací systémy] [taháky]
Za tento článek zatím nikdo autora nekamenoval