Git - Kapitola 2. Zkoumání git historie

Jak použít příkaz bisect pro nalezení regrese / Pojmenovávání commitů / Vytváření tagů / Procházení revizí / Generování diffů / Prohlížení starých verzí souborů / Příklady / Počítání počet commitů ve větvi / Zkontrolujte zda dvě větve ukazují na stejnou historii / První otagovaná verze zahrnující daný fix / Zobrazení commitů unikátních pro danou větev / Vytváření changelogu a tarballu pro vydání software / Vyhledávání commitů odkazující na soubor s daným obsahem.

Obsah

Jak použít příkaz bisect pro nalezení regrese
Pojmenovávání commitů
Vytváření tagů
Procházení revizí
Generování diffů
Prohlížení starých verzí souborů
Příklady
Počítání počet commitů ve větvi
Zkontrolujte zda dvě větve ukazují na stejnou historii
První otagovaná verze zahrnující daný fix
Zobrazení commitů unikátních pro danou větev
Vytváření changelogu a tarballu pro vydání software
Vyhledávání commitů odkazující na soubor s daným obsahem

O gitu je nejlepší přemýšlet jako o nástroji pro ukládání historie skupiny souborů. Dosahuje toho ukládáním komprimovaného snapshotu obsahu hierarchie souborů, společně s "commity" které ukazují vztahy mezi těmito snapshoty.

Git poskytuje extrémně flexibilní a rychlé nástroje pro zkoumání historie projektu.

Začneme s jedním specializovaným nástrojem užitečným pro zjištění který commit zanesl do projektu chybu.

Jak použít příkaz bisect pro nalezení regrese

Předpokládejme že verze 2.6.18 vašeho projektu fungovala, ale verze v "master" větvi padá. Někdy je nejlepším způsobem, jak najít příčinu takové chyby, provedení brute-force prohledání historie projektu jehož cílem je nalezení commitu který způsobil problém. Příkaz git-bisect(1) vám v tom může pomoci:

$ git bisect start
$ git bisect good v2.6.18
$ git bisect bad master
Bisecting: 3537 revisions left to test after this
[65934a9a028b88e83e2b0f8b36618fe503349f8e] BLOCK: Make USB storage
depend on SCSI rather than selecting it [try #6]

Pokud v tuto chvíli spustíte "git branch", uvidíte že git vás dočasně přesunul do "(no branch)". HEAD je nyní odpojen od jakékoliv větve a ukazuje přímo na commit (s ID commitu 65934...) který je dosažitelný z "master" větve ale nikoliv z v2.6.18. Zkompilujte a otestujte ho, a zjistěte jestli spadne. Předpokládejme že spadne. Potom:

$ git bisect bad
Bisecting: 1769 revisions left to test after this
[7eff82c8b1511017ae605f0c99ac275a7e21b867] i2c-core: Drop useless
bitmaskings

načte starší verzi. Pokračujte tímto způsobem, přičemž v každém kroku gitu řeknete jestli je daná revize dobrá nebo špatná, a všimněte si že počet revizí je vždy zhruba poloviční ve srovnání s předchozím krokem.

Po zhruba 13 testech (v tomto případě), se dostanete k ID commitu který je příčinou chyby. Potom můžete prozkoumat commit pomocí příkazu git-show(1), zjistit kdo ho napsal, a poslat mu e-mail s oznámením chyby a ID commitu. Nakonec spusťte

$ git bisect reset

čímž se vrátíte do větve ve které jste byli předtím.

Všimněte si že verze kterou pro vás příkaz git bisect načte je pouze doporučení, a můžete klidně zkusit jinou verzi pokud si myslíte že je to dobrý nápad. Například, čas od času můžete narazit na commit který rozbil něco nesouvisejícího, takže spusťte

$ git bisect visualize

což spustí gitk a označí zvolený commit poznámkou "bisect". Zvolte bezpečně vypadající commit někde v blízkosti, poznamenejte si commit id, a načtěte ho pomocí:

$ git reset --hard fb47ddb2db...

potom revizi otestujte, dle výsledku spusťte "bisect good" nebo "bisect good", a pokračujte.

Namísto "git bisect visualize" a potom "git reset —hard fb47ddb2db…", můžete prostě říct gitu že chcete aktuální commit přeskočit:

$ git bisect skip

V tomto případě, nicméně, git případně nemusí být schopen oznámit první špatnou revizi mezi prvním přeskočeným commitem a dalším špatným commitem.

Existují také způsoby jak zautomatizovat proces půlení (bisect) pokud máte testovací skript který vám může rozlišit dobré a špatné commity. Podrobnosti najdete v nápovědě k příkazu git-bisect(1) o této a dalších vlastnostech příkazu "git bisect".

Pojmenovávání commitů

Zatím jsme viděli několik způsobů pojmenování commitů:

  • 40-znakové hexadecimální jméno objektu
  • jméno větve: odkazuje na commit na vrcholu dané větve
  • jméno tagu: odkazuje na commit označený daným tagem (viděli jsme že větve a tagy jsou zvláštními případy odkazů).
  • HEAD: odkazuje na vrchol aktuální větve

Existuje mnoho dalších způsobů; kompletní seznam způsobů pojmenování revizí najdete v odstavci "SPECIFYING REVISIONS" v manuálové stránce k příkazu git-rev-parse(1). Několik příkladů:

$ git show fb47ddb2 # několik prvních znaků jména objektu obvykle
                    # stačí k unikátní identifikac
$ git show HEAD^    # rodič HEAD commitu
$ git show HEAD^^   # předek
$ git show HEAD~4   # pra-pra-pra-prapředek

Připomeňme že merge commity mohou mít více neř jednoho předka; ^ a ~ standardně následují prvního předka, ale i toto si můžete zvolit:

$ git show HEAD^1   # show the first parent of HEAD
$ git show HEAD^2   # show the second parent of HEAD

Navíc k HEAD, existuje několik speciálních jmen pro commity:

Merge (o kterých se budeme bavit později), stejně jako operace jako například git reset, které mění aktuálně načtený commit, obvykle nastavují  ORIG_HEAD na hodnotu kterou HEAD měl před danou operací.

Operace git fetch vždy ukládá vrchol poslední načtené větve ve FETCH_HEAD. Například, pokud spustíte git fetch bez zadání lokální větve jako cíle operace

$ git fetch git://example.com/proj.git theirbranch

načtené commity budou dostupné v FETCH_HEAD.

Až budeme diskutovat o mergování uvidíme také speciální jméno MERGE_HEAD, které odkazuje na další větev kterou mergujeme do současné větve.

Příkaz git-rev-parse(1) je nízkoúrovňový příkaz který je čas od času užitečný pro překlady některého jména commitu na objektové jméno tohoto commitu:

$ git rev-parse origin
e05db0fd4f31dde7005f075a84f96b360d05984b

Vytváření tagů

Můžete také vytvořit tag odkazující na určitý commit; po spuštění

$ git tag stable-1 1b2e1d63ff

můžete použít stable-1 k odkazům na commit 1b2e1d63ff.

To vytváří "odlehčený" tag. Pokud byste k tagu také rádi přidali komentář, a případně ho digitálně podepsali, potom byste měli raději vytvořit tag objekt; detaily najdete v manuálové stránce k příkazu git-tag(1).

Procházení revizí

Příkaz git-log(1) může může vypsat seznam commitů. Sám o sobě vypisuje všechny commity dostupné z předka; ale můžete mu zadávat také daleko specifičtější požadavky:

$ git log v2.5..        # commity od (nedostupné z) v2.5
$ git log test..master  # commits dostupné z master ale ne z test
$ git log master..test  # ... dostupné z test ale ne z master
$ git log master...test # ... dostupné buď z test nebo master, ale ne
                          z obou
$ git log --since="2 weeks ago" # commity za poslední 2 týdny
$ git log Makefile      # commity které modifikovaly Makefile
$ git log fs/           # ... které modifikovaly soubory v fs/
$ git log -S'foo()'     # commity které přidávají nebo odstraňují data
                        # odpovídající řetězci 'foo()'

A pochopitelně můžete tyto příkazy kombinovat; následující příkaz vyhledá commity od v2.5 které upravily Makefile nebo jakýkoliv soubor v adresáři fs:

$ git log v2.5.. Makefile fs/

Můžete také požádat git log o zobrazení patchů:

$ git log -p

Podívejte se volbu "—pretty" v manuálové stránce příkazu git-log(1) pro další možnosti zobrazení.

Všimněte si že git log začíná výpis nejnovějšími commity a postupuje zpět přes jejich předky; nicméně, protože git historie může obsahovat několik nezávislých linií vývoje, konkrétní pořadí ve kterém jsou commity vypisovány je víceméně libovolné.

Generování diffů

Pomocí příkazu git-diff(1) můžete generovat diffy mezi libovolnými verzemi:

$ git diff master..test

Tím získáte diff mezi dvěma vrcholy větví. Pokud byste raději získali diff od společného předka po test, můžete použít tři tečky namísto dvou:

$ git diff master...test

Někdy chcete spíše množinu patchů; k tomu můžete použít příkaz git-format-patch(1):

$ git format-patch master..test

čímž vygenerujete soubor s patchem pro každý commit dosažitelný z testu ale ne z větve master.

Prohlížení starých verzí souborů

Starou verzi souboru si vždy můžete prohlédnout po prostém načtení korektní revize. Ale občas je výhodnější když si starou verzi jednoho souboru můžete zobrazit bez checkoutování čehokoliv; toho dosáhnete následujícím příkazem:

$ git show v2.5:fs/locks.c

Před dvojtečkou můžete zadat cokoliv co pojmenovává commit, a za ním cesta k souboru spravovanému gitem.

Příklady

Počítání počet commitů ve větvi

Řekněme že chcete vědět kolik commitů jste udělali do větve "mybranch" od okamžiku kdy se oddělila od "origin":

$ git log --pretty=oneline origin..mybranch | wc -l

Alternativně můžete tento typ věcí zjišťovaný pomocí low-level příkazu git-rev-list(1), který pouze vypíše SHA-1 identifikátory daných commitů:

$ git rev-list origin..mybranch | wc -l

Zkontrolujte zda dvě větve ukazují na stejnou historii

Předpokládejme že chcete zjistit zda dvě větve odkazují na stejný bod v historii. Příkaz

$ git diff origin..master

vám řekne zda je obsah projektu je stejný v obou větvích; teoreticky je však možné že ke stejnému obsahu projektu se došlo dvěma různými historickými cestami. Můžete srovnat objektová jména:

$ git rev-list origin
e05db0fd4f31dde7005f075a84f96b360d05984b
$ git rev-list master
e05db0fd4f31dde7005f075a84f96b360d05984b

Nebo si můžete vzpomenout že operátor "..." vybírá všechny commity dosažitelné z jedné nebo druhé cesty, ale nikoliv z obou najednou; takže

$ git log origin...master

nevrátí žádné commity pokud jsou dvě větve ekvivalentní.

První otagovaná verze zahrnující daný fix

Předpokládejme že víte že commit e05db0fd opravuje určitý problém. Chcete najít první otagovaný release obsahující tento fix.

Jistě, může existovat více než jedna odpověď - pokud se historie po commitu e05db0fd rozvětvila, potom by mohlo existovat několik "nejstarších" otagovaných revizí.

Můžete pouze vizuálně prozkoumat commity od e05db0fd:

$ gitk e05db0fd..

Nebo můžete využít příkaz git-name-rev(1), který vrátí jméno commitu na základě tagů odkazujících na některý z potomků commitu:

$ git name-rev --tags e05db0fd
e05db0fd tags/v1.5.0-rc1^0~23

Příkaz git-describe(1) dělá opak, pojmenovává revizi pomocí tagu na kterém je daný commit založen:

$ git describe e05db0fd
v1.5.0-rc0-260-ge05db0f

ale to vám občas může pomoci zjistit které tagy mohly přijít po daném commitu.

Pokud pouze chcete ověřit zda daná otagovaná verze obsahuje daný commit, můžete použít příkaz git-merge-base(1):

$ git merge-base e05db0fd v1.5.0-rc1
e05db0fd4f31dde7005f075a84f96b360d05984b

Příkaz merge-base najde společného předka daných dvou commitů, a pokud jedna z revizí je potomkem druhé tak vrátí jedno či druhé; čili příklad uvedený výše znamená že e05db0fd je předek revize v1.5.0-rc1.

Alternativně můžete použít příkaz

$ git log v1.5.0-rc1..e05db0fd

který vygeneruje prázdný výstup právě tehdy když v1.5.0-rc1 v sobě zahrnuje e05db0fd, protože jeho výstupem jsou pouze commity nedostupné z v1.5.0-rc1.

A ještě jedna alternativa, příkaz git-show-branch(1) vypíše seznam commitů dostupných z jeho argumentů s obrazovkou na levé straně označující ze kterých argumentů je commit dostupný. Takže můžete použít něco jako

$ git show-branch e05db0fd v1.5.0-rc0 v1.5.0-rc1 v1.5.0-rc2
! [e05db0fd] Fix warnings in sha1_file.c - use C99 printf format if
available
 ! [v1.5.0-rc0] GIT v1.5.0 preview
  ! [v1.5.0-rc1] GIT v1.5.0-rc1
   ! [v1.5.0-rc2] GIT v1.5.0-rc2
...

potom hledejte řádek vypadající jako

+ ++ [e05db0fd] Fix warnings in sha1_file.c - use C99 printf format if
available

Což znamená že e05db0fd je dostupná sama ze sebe, z v1.5.0-rc1 a z v1.5.0-rc2, ale ne z v1.5.0-rc0.

Zobrazení commitů unikátních pro danou větev

Dejme tomu že chcete vidět všechny commity dostupné z hrotu větve nazvané "master" ale ne z žádné jiné větve ve vašem repositáři.

Všechny vrcholy ve vašem repositáři získáte příkazem git-show-ref(1):

$ git show-ref --heads
bf62196b5e363d73353a9dcf094c59595f3153b7 refs/heads/core-tutorial
db768d5504c1bb46f63ee9d6e1772bd047e05bf9 refs/heads/maint
a07157ac624b2524a059a3414e99f6f44bebc1e7 refs/heads/master
24dbc180ea14dc1aebe09f14c8ecf32010690627 refs/heads/tutorial-2
1e87486ae06626c2f31eaa63d26fc0fd646c8af2 refs/heads/tutorial-fixes

Pomocí standardních utilit cut a grep můžete získat pouze jména vrcholů větví, a odstranit "master":

$ git show-ref --heads | cut -d' ' -f2 | grep -v '^refs/heads/master'
refs/heads/core-tutorial
refs/heads/maint
refs/heads/tutorial-2
refs/heads/tutorial-fixes

A potom můžeme chcít zjistit které commity jsou dostupné z master větve ale ne z dalších větví:

$ gitk master --not $( git show-ref --heads | cut -d' ' -f2 |
                                grep -v '^refs/heads/master' )

Evidentně je možno nekonečně mnoho variací; například pro vypsání všech commitů dostupných z některého vrcholu ale ne z žádného tagu v repositáři:

$ gitk $( git show-ref --heads ) --not  $( git show-ref --tags )

(Vysvětlení syntaxe pro výběr commitů jako je například volba —not najdete v manuálové stránce příkazu git-rev-parse(1)).

Vytváření changelogu a tarballu pro vydání software

Příkaz git-archive(1) může vytvořit tar nebo zip archiv z libovolné verze projektu; například:

$ git archive --format=tar --prefix=project/ HEAD | gzip >latest.tar.gz

použije HEAD pro vytvoření tar archivu ve kterém je před každé jméno souboru  doplněno "project/".

Pokud vydáváte novou verzi softwarového projektu, můžete chtít současně vytvořit changelog pro přiložení k oznámení o vydání.

Linus Torvalds, například, vytváří nová vydání kernelu jejich otagováním, a poté spuštěním:

$ release-script 2.6.12 2.6.13-rc6 2.6.13-rc7

kde release-scriptje shellový skript vypadající zhruba takto:

#!/bin/sh
stable="$1"
last="$2"
new="$3"
echo "# git tag v$new"
echo "git archive --prefix=linux-$new/ v$new | \
                                    gzip -9 > ../linux-$new.tar.gz"
echo "git diff v$stable v$new | gzip -9 > ../patch-$new.gz"
echo "git log --no-merges v$new ^v$last > ../ChangeLog-$new"
echo "git shortlog --no-merges v$new ^v$last > ../ShortLog"
echo "git diff --stat --summary -M v$last v$new > ../diffstat-$new"

a potom pouze zkopíruje výstup příkazy na výstupu po ověření že vypadají OK.

Vyhledávání commitů odkazující na soubor s daným obsahem

Někdo vám předá kopii souboru, a zeptá se vás které commity upravily soubor tak že před nebo po commitu měl daný obsah. To můžete zjistit takto:

$  git log --raw --abbrev=40 --pretty=oneline |
        grep -B 1 `git hash-object filename`

Zjišťování proč toto funguje je ponecháno jako cvičení pro (pokročilého) studenta. Manuálové stránky k příkazům git-log(1), git-diff-tree(1), a git-hash-object(1) se vám nejspíše budou hodit.

Komentáře

K tomuto článku zatím žádné komentáře neexistují (nebo čekají na schválení).

Nový komentář

Všechny komentáře podléhají schválení - mezi odesláním komentáře a jeho zobrazením na této stránce tedy může být prodleva. Vyplníte-li e-mailovou adresu, budete o schválení či neschválení komentáře informováni.

V titulku ani v textu nejsou povoleny HTML tagy - budou automaticky odstraněny. Odstavec ukončíte prázdným řádkem.

(nepovinné)