2015-01-29

Редактирование истории в git

Этот пост что-то вроде Advanced mercurial но для git. Рано или поздно все сталкиваются с двумя проблемами: у меня куда-то пропали коммиты и я запушил не туда. Во втором случае есть простой случай, когда никто не успел спулить и когда неправильный кормит у всей команды. Все опасаются применять команды с ключами --hard и --force, но это совсем не страшно, если понимаешь, что все обратимо и ты понимаешь чего хочешь достичь этими командами.

Концептуальное отличие от mercurial: в git коммит обязательно должен принадлежать бранчу (без бранча коммит становится detached head), а бранч может быть только один. В mercurial ветки могут быть анонимными, более того может быть несколько веток с одним именем. Чтобы коммит стал виден, должен существовать путь до него из какого-либо бранча. То самое отличие кроны и корневой системы (почему-то именно такая аналогия приходит в голову). В mercurial чтобы коммит был доступен должен существовать путь до него от 0-го коммита, а имена и прочее это мелочи. Соответственно и можно четко проследить начало и конец любой ветви, в отличии от git, где коммиты эфемерные дельты болтающиеся по узлам DAG. Хорошо это или плохо - зависит от, но в централизованной разработке мне больше импонирует mercurial с долгоживущими и вполне материальными ветвями, хотя это не значит что там нельзя редактировать историю - дельта алгебра везде одна. Git же заточен именно под распределенность и множество центральных узлов - помимо ветки foo-branch, обязательно есть origin/foo-branch или another/foo-branch с четким разделением откуда приехал коммит - в mercurial же именование веток сквозное по всем репозиториям, если стянули изменения то они уже ваши и отделить где чьё не получится.

Дальше пойдет описание трех сценариев в демке с github. Демка пошагово демонстрирует работу Алисы и Боба с общим репозиторием. Запускаем, читаем комментарии, жмем батон. Демка должна работать на unix-like системах где есть bash. Кстати, ознакомьтесь со статьей tonsky - прольет свет на внутренности git.

Восстановление коммитов

У нас есть несколько коммитов:

* c39e99a (HEAD, master) totally more readable <alice>
* 3fb6f50 should work now. <alice>
* fe1e8a8 QuickFix. <alice>

Мы их выводим красиво раскрашенными командой с форматированием похожим на --oneline, но с добавлением автора: git log --date-order --pretty=format:%C(yellow)%h%Creset%C(green)%Creset%C(auto)%d%Creset %s %C(magenta)<%an> --decorate --all --graph

Итак, метка HEAD и бранч master на последнем коммите, более поздние сверху. Мы дропаем первые два коммита git reset --hard HEAD^^ и история ожидаемо выглядит

* fe1e8a8 (HEAD, master) QuickFix. <alice>

Вопрос: где всё? В ref-log вестимо, чтобы отобразить полную историю сначала собираем все коммиты в список, и затем выводим в лог: git log --date-order --pretty=format:%C(yellow)%h%Creset%C(green)%Creset%C(auto)%d%Creset %s %C(magenta)<%an> --decorate --all --graph $(git rev-list --reflog --all) на демке список будет автоматически раскрыт. Итак, в истории у нас два верхних коммита не принадлежат никакому бранчу и в простом логе недоступны.

* c39e99a totally more readable <alice>
* 3fb6f50 should work now. <alice>
* fe1e8a8 (HEAD, master) QuickFix. <alice>

Чтобы их вернуть, есть много способов, сам git предлагает создать бранч и смерджить с master: 

git checkout -b lost_commits
git checkout master
git merge lost_commits
git branch -d lost_commits

История теперь выглядит так:

* 4aa9db9 (HEAD, master, lost_commits) Herpderp, shoulda check if it does really compile. <alice>
* c39e99a totally more readable <alice>
* 3fb6f50 should work now. <alice>
* fe1e8a8 QuickFix. <alice>

Отмена коммита в удаленном репозитории

Простой случай, когда никто не успел стянуть и накоммитить поверх, либо вы пушите в свою ветку с которой никто не работает. В этом случае push --force совершенно безопасен.

История Алисы выглядит так

* 1f97ef0 (HEAD, origin/master, master) Nitpicking... <alice>
* 4aa9db9 Herpderp, shoulda check if it does really compile. <alice>
* c39e99a totally more readable <alice>
* 3fb6f50 should work now. <alice>
* fe1e8a8 QuickFix. <alice>

Алиса хочет удалить верхний коммит, который она уже запушила (origin/master).

git reset HEAD^ --hard
git push origin --force

Вуаля, история теперь выглядит так

* 4aa9db9 (HEAD, origin/master, master) Herpderp... <alice>
* c39e99a totally more readable <alice>
* 3fb6f50 should work now. <alice>
* fe1e8a8 QuickFix. <alice>

Если кто-то успел стянуть вашу ветку, а потом она случайно переехала на другой коммит, то при пуле git ругнется, мол бранчи указывают совсем ну туда куда ожидалось, спокойно исправит ссылки на новые и продолжит работу как ни в чем не бывало.

Сложный случай редактирования истории

Алиса запушила изменения, Боб запушил свои изменения поверх и нужно избавиться от одного из старых коммитов.
* 623c081 (HEAD, origin/master, master) Fixing Sarah's bugs. <bob>
* 5facd35 small is a real HTML tag, who knew. <alice>
* 0bb2195 and so the crazy refactoring process sees the sunlight after some months in the dark! <alice>
* 4aa9db9 Herpderp, shoulda check if it does really compile. <alice>
* c39e99a totally more readable <alice>
* 3fb6f50 should work now. <alice>
* fe1e8a8 QuickFix. <alice>

Алиса хочет избавиться от своего коммита 5facd35 от который в origin и, очевидно, у Боба потому, что он успел закоммитить и запушить свои изменения. Используем интерактивный rebase и закомментируем pick 5facd35 чтобы он не попал в новую историю.

git rebase -i HEAD^^

В редакторе испавим и сохраним

#pick 5facd35 small is a real HTML tag, who knew.
pick 623c081 Fixing Sarah's bugs.

Если возникнут конфликты, то исправляем их, коммитим и продолжаем rebase

git add -A
git commit -m Fixing Sarah's bugs.
git rebase --continue

История теперь выглядит расщепленной

* 678e268 (HEAD, master) Fixing Sarah's bugs. <alice>
| * 623c081 (origin/master) Fixing Sarah's bugs. <bob>
| * 5facd35 small is a real HTML tag, who knew. <alice>
|/
* 0bb2195 and so the crazy refactoring process sees the sunlight after some months in the dark! <alice>
* 4aa9db9 Herpderp, shoulda check if it does really compile. <alice>
* c39e99a totally more readable <alice>
* 3fb6f50 should work now. <alice>
* fe1e8a8 QuickFix. <alice>

Мы делаем git push origin --force и переходим к Бобу, он видит следующее

* 678e268 (origin/master) Fixing Sarah's bugs. <alice>
| * 9c446c9 (HEAD, master) Ok, 5am, it works.  For real. <bob>
| * 623c081 Fixing Sarah's bugs. <bob>
| * 5facd35 small is a real HTML tag, who knew. <alice>
|/
* 0bb2195 and so the crazy refactoring process sees the sunlight after some months in the dark! <alice>
* 4aa9db9 Herpderp, shoulda check if it does really compile. <alice>
* c39e99a totally more readable <alice>
* 3fb6f50 should work now. <alice>
* fe1e8a8 QuickFix. <alice>

Он успел сделать коммит но не успел запушить. Обыконовенного git pull origin хватит чтобы его единственный коммит переехал на origin/master, даже конфликтов не будет.

* b741ec5 (HEAD, master) Ok, 5am, it works.  For real. <bob>
* 678e268 (origin/master) Fixing Sarah's bugs. <alice>
* 0bb2195 and so the crazy refactoring process sees the sunlight after some months in the dark! <alice>
* 4aa9db9 Herpderp, shoulda check if it does really compile. <alice>
* c39e99a totally more readable <alice>
* 3fb6f50 should work now. <alice>
* fe1e8a8 QuickFix. <alice>

Проверим ref-log и убедимся, что ничего не пропало git log --date-order --pretty=format:%C(yellow)%h%Creset%C(green)%Creset%C(auto)%d%Creset %s %C(magenta)<%an> --decorate --all --graph $(git rev-list --reflog --all) на самом деле все коммиты на месте, хоть и не видны потому что нет бранча, их спокойно можно воскресить, дав имя

* b741ec5 (HEAD, master) Ok, 5am, it works.  For real. <bob>
* 678e268 (origin/master) Fixing Sarah's bugs. <alice>
| * 9c446c9 Ok, 5am, it works.  For real. <bob>
| * 623c081 Fixing Sarah's bugs. <bob>
| * 5facd35 small is a real HTML tag, who knew. <alice>
|/
* 0bb2195 and so the crazy refactoring process sees the sunlight after some months in the dark! <alice>
* 4aa9db9 Herpderp, shoulda check if it does really compile. <alice>
* c39e99a totally more readable <alice>
* 3fb6f50 should work now. <alice>
* fe1e8a8 QuickFix. <alice>

Итоги

История не пропадает, редактировать и пушить в общий репозиторий не опасно, главное предупреждать, чтобы не возникало войны альтернативных историй.