Міжнародная канферэнцыя распрацоўнікаў і карыстальнікаў свабодных праграм

Безупречная история в Git или Mercurial

Алексей Хлебников, Осло, Норвегия

LVEE 2014

History of development saved in version control systems (VCS) is very important. It simplifies investigation of problems, reversion of regressions, picking specific changes for specific customers or releases, learning code for new developers in the team, generally keeping control over the code, assigning blame, and so on. However, after long development of a complex software product, its VCS history is often hard to read. The talk shows ways to remedy the problem by consistent use of branching, rebasing and squashing, with detailed examples for Git and Mercurial.

Введение

Существуют различные способы использования 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

Назад