Vimdiff – nástroj drsňáků
Publikováno: 12.12.2017
Text vyšel původně na autorově webu.
Musím se vám k něčemu přiznat… Už patnáct let je Vim můj nejoblíbenější textový editor. A občas, čas od času, i hlavní nástroj na programování.
Umím si poeditovat vimrc
, který po léta udržuju a vylepšuju. Dokonce jsem se i naučil trochu Vim script/VimL a napsal dva zanedbatelné a nedotažené pluginy (pro Gradle a WSDL).
Ale vždycky jsem se jako čert kříži vyhýbal jedné věci – používání vimdiff. Nicméně na každého jednou dojde. Z určitých (pro článek nepodstatných) důvodů jsem si nemohl pro nové vývojové prostředí nastavit P4Merge a tak jsem vstoupil do zapovězené komnaty.
Disclaimer: Tenhle článek píšu jako shrnutí toho, jak jsem práci s vimdiff pochopil. Pokud máte víc zkušeností, budu rád, když se podělíte v komentářích.
2-way merge
Nejjednodušší způsob, jak používat vimdiff – pokud pomineme, že umí dělat i „plain old“ diff – je 2-way merge: máme vedle sebe dvě okna se zvýrazněnými rozdíly a chceme mezi nimi tyto změny propagovat.
Stavu na předešlém obrázku, který je výchozí pro merge, se dá dosáhnout několika způsoby:
- Příkazem:
vimdiff myFile.txt theirFile.txt
- Příkazem:
vim -d myFile.txt theirFile.txt
- Kombinací příkazů:
vim myFile.txt
:diffsplit theirFile.txt
- Kombinací příkazů
vim -O myFile.txt theirFile.txt
(vsplit obou souborů):diffthis
(zapne diff na aktuálním bufferu)Ctrl-W Ctrl-W
(skok do druhého bufferu):diffthis
(zapne diff v druhém bufferu)
Základní příkazy
Tak, diff máme zobrazený, co s ním? První věc – je potřeba se v diffu umět pohybovat. Kromě toho, že můžete použít jakýkoli skok, který znáte z běžného Vimu, jsou tu dva příkazy, které umožňují skákat po jednotlivých rozdílech:
]c
skočí na následující diff[c
skočí na předcházející diff
Za druhé – chceme propagovat změny z/do aktuálního bufferu: skočíme na diff, který chceme upravit a:
do
, nebo:diffget
natáhne změny z „druhého“ bufferu do toho aktuálního.dp
, nebo:diffput
propaguje změny z aktuálního bufferu do „toho druhého“.
Za třetí – změny uložíme. Kromě příkazů na standardní ukládání (:w
, ZZ
atd.) se může hodit:
:only
zavře všechny ostatní buffery kromě toho aktuálního:qall
zavře všechny otevřené buffery:only | wq
zavře ostatní buffery + uloží stávající + ukončí Vim. Cool!
Eventuálně začtvrté – pokud věci nejdou hladce, může se šiknout:
:diffupdate
znovu proskenuje a překreslí rozdíly (u komplikovanějších mergů nemusí Vim správně pochopit danou změnu):set wrap
nastavení zalamování řádků (hodí se při velmi dlouhých řádcích, typicky některá XML)zo/zc
otevře/zavře skryté (folded) řádky
3-way merge
Nemusím vám říkat, že 2-way merge je pro školáky – profíci makaj v Gitu, či v Mercurialu a tam je dvoucestný merge nedostačující. Ke slovu přichází 3-way merge. Co to je?
3-way merge není nic složitého. V podstatě jde o to, že máme dvě verze, které mají společného předka. V mergovacím nástroji pak vidíme všechny tři verze vedle sebe a většinou máme k dispozici ještě čtvrté okno s aktuálním výsledkem merge.
Nastavení Gitu
Nastavení spolupráce Gitu a vimdiff je jednoduché – stačí spustit z příkazové řádky následující sadu příkazů:
$ git config --global merge.tool vimdiff $ git config --global merge.conflictstyle diff3 $ git config --global mergetool.prompt false $ git config --global mergetool.keepBackup false
Pokud se podíváte do ~/.gitconfig
, měli byste tam vidět:
[merge]
tool = vimdiff
conflictstyle = diff3
[mergetool]
prompt = false
keepBackup = false
Nastavení Mercurialu
Nastavení Mercurialu je podobně jednoduché. Otevřeme soubor ~/.hgrc
příkazem
$ hg config --edit
a vložíme následující řádky
[ui]
merge = vimdiff
[merge-tools]
vimdiff.executable = vimdiff
vimdiff.args = -f -d $output -M $local $base $other -c "wincmd J" -c "set modifiable" -c "set write"
vimdiff.premerge = keep
[extensions]
hgext.extdiff =
[extdiff]
cmd.vimdiff = vimdiff
Sekce [extensions]
a [extdiff]
nejsou pro merge nutné, ale hodí se, pokud chceme vimdiff používat jako dodatečný externí diff nástroj. Sekundární diff spustíme příkazem hg vimdiff
.
Základní příkazy
Základní příkazy jsou stejné jako v sekci 2-way merge, s výjimkou příkazů dp
/do
(:diffput
/:diffget
) – pokud bychom je nyní použili, vimdiff nám zahlásí chybu:
More than two buffers in diff mode, don't know which one to use
To je v pořádku: u 3-way merge se ve vimdiff otevřou 4 buffery, všechny v diff módu. Takže do té doby, než se vimdiff naučí komunikovat telepaticky, je potřeba mu říct, ze kterého bufferu chceme danou změnu natáhnout.
Klasické merge flow vypadá následovně:
- Začínáme v dolním „výsledkovém“ bufferu.
]c
(skočit na následující diff, který chceme mergovat):diffget <identifikace-bufferu>
získáme změnu z daného bufferu (viz dále)- Opakujeme 2-3.
:only | wq
uložíme merge.
Obecně, identifikátor bufferu získáme příkazem :ls
. To je ale dost nepraktické a nepřehledné. Další možnost je identifikovat buffer částečným názvem souboru. Tady přichází na pomoc jak Git, tak Mercurial, který přidávají k názvům souborů příhodný suffix.
Merge v Gitu
Git přidává do názvů mergovaných souborů následující suffixy, v pořadí zleva doprava: LOCAL
(vlevo), BASE
(uprostřed), REMOTE
(vpravo). Pro natažení změny z (levého) bufferu LOCAL můžeme použít příkaz :diffg LO
.
Výpis bufferů pro Git:
:ls 1 #a "./myFile_LOCAL_7424.txt" line 1 2 a "./myFile_BASE_7424.txt" line 0 3 a "./myFile_REMOTE_7424.txt" line 0 4 %a "myFile.txt" line 12
Merge v Mercurialu
Mercurial přidává do názvů mergovaných souborů následující suffixy, v pořadí zleva doprava: orig
(vlevo), base
(uprostřed), other
(vpravo). Pro natažení změny z (levého) bufferu orig můžeme použít příkaz :diffg orig
.
Výpis bufferů pro Mercurial:
:ls 1 %a "myFile.txt" line 2 2 a- "myFile.txt.orig" line 0 3 a- "/tmp/myFile.txt~base.iZwwuA" line 0 4 a- "/tmp/myFile.txt~other.km9Itr" line 0
Co mi (zatím) schází?
Musím říct, že potom, co jsem si vimdiff osahal, pochopil jeho logiku a naučil se jeho příkazy, jsem si ho docela oblíbil.
Jediná výtka zatím jde za jeho neschopností skákat přímo po konfliktech – Git i Mercurial dělají výborně automatické merge a ty jsou samozřejmě vidět ve vimdiffu taky, jako pouhá změna bez konfliktu. Mít nějaký příkaz, který rozlišuje pouhý diff a konflikt, by bylo fajn.