Этот пост что-то вроде 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.
Концептуальное отличие от 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>
Итоги
История не пропадает, редактировать и пушить в общий репозиторий не опасно, главное предупреждать, чтобы не возникало войны альтернативных историй.