Безупречная история в Git или Mercurial
LVEE 2014
Введение
Существуют различные способы использования VCS в процессе разработки. Некоторые команды просто коммитят всё в основную ветку одного репозитория. Другие используют ветвление (branching), слияние (merging), несколько репозиториев (cloning). Ниже предлагается вариант, который удобен разработчикам и в то же время оптимизирован для улучшения читаемости истории VCS. Иными словами, как правильно бранчить, сквошить и ребэйсить код, используя команды Git и Mercurial.
Важные приёмы процесса разработки
Ветвление (branching)
Ветвление имеет следующие преимущества:
- Работая в отдельной ветке над отдельным кейсом (case), вы можете свободно экспериментировать, не боясь сломать mainline. Это важно не только технически, но и психологически: над разработчиком не довлеет груз ответственности и он может позволить себе большую свободу действий.
- Соответственно, коммиты других участников проекта на других ветках не смогут ничего сломать на вашей собственной ветке.
- Все коммиты, относящиеся к данному кейсу, сгруппированы. Они идут по порядку, в отличие от ситуации, когда все участники разработки используют только одну ветку. Это облегчает понимание кода данного кейса и улучшает историю в VCS.
- Пока код данной ветки не доставлен в mainline, можно редактировать историю (остановимся на этом позже).
Без ветвления все коммиты идут вперемешку на mainline:
При ветвлении коммиты группируются по кейсам:
Rebasing
Rebasing может применяться в процессе работы над кейсом, а также для доставки коммитов в mainline.
Rebasing в процессе работы позволяет:
- Получить ответвление от обновлённого mainline.
- Разрешать merge-conflicts небольшими порциями в ходе работы, а не одним большим куском в самом конце.
- Тестировать код своего кейса относительно нового mainline без его доставки в этот самый mainline.
Rebase вместо Merge как метод доставки кода в mainline позволяет получить:
- Линейную историю в VCS. Линия проще и наглядней, чем граф.
- Меньше проблем с blame, bisect и revert. Эти команды работают лучше на коммитах с одним родителем, чем с несколькими.
- Возможность удалять очень старые ветки (и их коммиты) из репозитория. Это повышает быстродействие репозитория. А если эти ветки всё-таки нужны – их можно хранить в архивном репозитории или в бэкапе. Тем, кто считает замедление репозитория при увеличении количества коммитов надуманной проблемой, есть смысл ознакомиться с исследованием Facebook1.
При доставке кода в mainline через Merge, mainline состоит в основном из merge-коммитов:
Тот же самый граф, но без ярко выраженного вертикального mainline:
Как видно, в такой истории довольно трудно разобраться, особенно по прошествии долгого времени, или если разбирающийся – новый человек на проекте. В этом графе даже mainline трудно найти.
А вот история, которая получается при Rebase:
Как видно, история линейна, что сильно улучшает её читаемость. Но можно сделать ещё лучше – уменьшить количество коммитов. И в этом поможет squashing.
Squashing
Squashing – это слияние нескольких коммитов в один. Squashing позволяет получить:
- Компактную историю. Это тем важнее, чем дольше идёт разработка и чем больше коммитов собралось в репозитории.
- Меньше мусора в истории.
- Более лёгкий откат изменений.
Если после работы над кейсом ветка содержит много мусора в истории…
…squashing позволит избавиться от этого мусора, слив все комиты в один:
Конкретные команды, шаг за шагом
Приведем примеры команд для Git и Mercurial в виде таблицы:
Git | Mercurial | |
---|---|---|
Ответвление от mainline | git checkout –b case4 | hg book case4 |
Работа над кейсом | git commit -m “Commit 1” git commit -m “Commit 2” … |
hg commit -m “Commit 1” hg commit -m “Commit 2” … |
Rebase и squash | git checkout –b case4-2 git rebase —interactive main |
(нужны расширения Rebase и Histedit) hg rebase —keep —dest main hg histedit main hg book case4-2 |
Отдельно коснёмся “табу на rebase после push”. Существует довольно распространённый миф, что если ветка запушена (push) на сервер – все возможности ребэйса (rebase) для неё потеряны, потому что если ветку проребэйсить и запушить на сервер опять (что возможно только с ключом —force), то это создаст несоответствие между репозиторием на сервере и репозиториями других разработчиков. В результате эти разработчики при попытке подтянуть эту ветку с сервера получат сломанную ветку.
На самом деле rebase возможен, если выполнять его правильно. На примере команд, приведённых в таблице, видно, что при ребэйсинге создаётся новая ветка, case4-2. Это принципиальный момент. Первоначальная ветка, case4, так и остаётся на своём месте, и получается аналог copy-on-write. Таким образом консистентность репозитория не нарушается, и ветка case4 не ломается – она просто устаревает. Теперь про неё можно забыть, а дальнейшую разработку, если она ещё продолжается, вести на ветке case4-2.
Также следует обратить внимание на ключ —interactive для Git и команду histedit для Mercurial. В результате их использования Git или Mercurial вызывают текстовый редактор, в котором разработчик может редактировать историю своей ветки: помечать коммиты для правки комментария, сливать несколько коммитов вместе, менять коммиты местами, удалять ненужные коммиты.
В сущности, многие коммиты – просто мусор в истории VCS: неудавшиеся эксперименты, багфиксы, фиксы компиляции, чистка неиспользуемых переменных, исправления опечаток в комментариях. Подобный материал в истории VCS не представляет ровным счётом никакого интереса. Как правило, от разработчика требуется имплементация фичи X или исправление бага Y, и желательно одним куском (то есть, как правило (хоть и не всегда), одним коммитом). А детали того, через что разработчик прошёл в процессе разработки, никого не интересуют. По этой причине мелкие правящие коммиты всегда имеет смысл объединить с “главными” коммитами, которые они дополняют. Это же относится и к фиксам в результате code review.
Делать слишком много “главных” коммитов для одного кейса тоже не имеет смысла. Наоборот, для большинства кейсов перед доставкой кода в mainline лучше слить все коммиты в один и использовать название кейса как комментарий этого единственного коммита. Если имел место рефакторинг без изменения функционала – его имеет смысл выделить в отдельный коммит. Если имели место фиксы багов mainline’a, которые проявились при работе над данным кейсом – их тоже имеет смысл выделить в отдельные коммиты, и поместить эти коммиты перед основной разработкой. Других коммитов не нужно. Это и есть squashing – слияние нескольких коммитов в один.
Кроме чистки мусора в истории, squashing также помогает убрать коммиты, которые не компилируются, путём слияния с фиксом компиляции. Это важно, если используется bisect, а также в случае отката изменений.
Если вам захочется оставить в истории VCS свои чаяния и веяния по поводу данного кейса из ностальгических соображений – есть возможность сохранить их только для себя на той старой ветке case4, которая была любезно сохранена при copy-on-write-rebase. Не перетаскивая свой мусор в mainline, разработчик заодно не создает аналогичного искушения для товарищей по команде.
Доставка кода в mainline
Итак, работа над кейсом завершена, код кейса оттестирован с новейшим mainline. После финальных rebase и squash на ветке должна находиться краткая и красивая история кейса, а сама ветка основана на верхушке mainline. Это и есть подходящий момент для доставки кода кейса в mainline. Для этого надо всего лишь переместить указатель mainline вперёд по ветке case4-2 – сделать fast-forward. Это можно сделать несколькими способами; автор предпочитает такие:
Git | Mercurial |
---|---|
git push . case4-2:main | (hg update case4-2) hg book main # “book” does fast-forward in this case |
Важно обратить внимание на точку после git push. Она означает “текущий репозиторий”. Однако можно пушить и сразу в origin:
git push origin case4-2:mainПри этом не следует опасаться поломки origin/main, т.к. если предлагаемый push не fast-forward, Git не позволит выполнить push без ключа —force.
Выводы
В результате использования предложенного подхода мы получаем:
- Удобство разработки на отдельных ветвях.
- Возможность всегда работать и тестировать свои изменения относительно новейшего mainline.
- Линейную историю.
- Группировку коммитов по кейсам.
- Безупречную и компактную историю без мусора.
Ссылки
1 http://thread.gmane.org/gmane.comp.version-control.git/189776
Abstract licensed under Creative Commons Attribution-ShareAlike 4.0 International license
Back