Я использую git в работе уже несколько лет, но в последние месяцы заметил, что у меня сменился способ работы с ним. Попробую изложить, как я использую git, и что с этого имею.

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

Большие коммиты

Старый способ работы был унаследован со старых систем контроля версий. Вкратце его можно назвать “большой кусок работы”. Суть очень проста: пишем-пишем-пишем код, а когда тот готов, сохраняем его в VCS.

Чем это плохо? Отсутствует сохранение промежуточного результата. Смешиваются изменения, которые приносят прогресс, и эксперименты, которые следует отбросить. Смешиваются новые фичи и рефакторинг, не изменяющий поведения. Как результат, итоговый коммит получается неряшливым и не слишком понятным.

Программисты, использующие git недавно, также могут создавать коммиты в процессе работы “когда получается”. Такой вариант не сильно отличается от предыдущего. Может быть, даже хуже: в коммиты попадает промежуточный код, от которого потом отказались. На мой взгляд, это весьма вредно. Кто-то другой, копаясь в истории, может надолго смутиться, увидев неправильный код, а то даже и ошибочно принять его за образец для подражания.

Мелкие коммиты

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

Работа с кодом у меня проходит в две фазы. Образно говоря, их можно назвать “время разбрасывать камни” и “время собирать камни”. Порой они сменяют друг друга однократно, а порой чередуются много раз подряд. Обычно это зависит от объёма изменений.

Различие между фазами заключается в том, как мы обращаемся с коммитами.

Разбрасываем камни

Это простой, быстрый и весёлый этап. Мы просто создаём коммиты на каждый чих. Подчистили код? Сделали коммит. Провели рефакторинг? Сделали коммит. Добавили кусок функционала? Сделали коммит.

Критически важно, чтобы в каждом коммите содержалось одно понятное и цельное изменение. Это даёт нам несколько ключевых преимуществ:

  • Так проще описать изменения. Вместо мало что говорящего сообщения “Тикет 1234” мы можем написать нечто конкретное, а-ля “Класс Fooblinator диспетчеризует клиентов по типу доставки”. Это нам потом ещё очень пригодится.
  • Pазные цельные изменения меньше пересекаются друг с другом. Полного отсутствия пересечений, конечно, практически никогда не бывает, но к этому стоит стремиться :)
  • Mы можем активно экспериментировать в том же коде, просто сохраняя экспериментальные изменения в отдельные коммиты. Потом эти коммиты можно будет легко выкинуть.

Порой бывает, что даже одно цельное изменение бывает удобнее разделить на два или более. Например, мы провели какой-то рефакторинг, который затронул и старый код (существовавший до нашей фичи), и новый (созданный нами в процессе). В таком случае я отдельно сохраняю изменения в старом коде, и отдельно изменения в новом. Это упростит жизнь на второй фазе.

Собираем камни

Если на первой фазе мы создавали много мелких коммитов, то теперь мы начинаем их объединять. Главная рабочая команда на этом этапе - git rebase -i. Также очень помогают git log, git cherry и незаменимый gitk ;)

Интерактивный ребэйз - самая приятная фича git, которую я открыл для себя в последнее время. Фактически, именно он подтолкнул меня к подобному изменению в workflow. Суть его довольно проста: после вызова команды список предназначенных для перебазирования коммитов открывается в текстовом редакторе (мой любимый vim!), где можно указать, как поступить с каждым коммитом. Можно также менять порядок коммитов, выбрасывать их (просто удалив из списка), и даже приказывать запустить между коммитами произвольные команды шелла! И после выхода из редактора git постарается выполнить все указанные действия.

Так как я веду разработку в отдельной ветке, то в 95% случаев формат команды выглядит одинаково:

git rebase -i master

Чаще всего я использую следующие приёмы:

  • Eсли коммит содержит какой-то экспериментальный код, который не должен попасть в итоговый результат, то он просто выбрасывается.
  • Если коммит содержит мелкое исправление к предыдущему коммиту, то их можно слить с помощью команды fix.
  • Если коммит содержит существенное исправление к предыдущему коммиту, то их можно слить с помощью команды squash. Это даст возможность отредактировать сообщение итогового коммита, включив в него информацию из обоих исходных коммитов. Каждый исходный коммит может содержать немного комментариев, но при слиянии мы соединяем их, и так постепенно вырастает большое и подробное описание коммита.
  • Чaсто коммиты требуется отсортировать, прежде чем сливать друг с другом. Для этого достаточно лишь поменять их порядок при редактировании ребэйза. Чем меньше и цельнее наши коммиты, тем легче осуществлять этот шаг.
  • Однако, порой становится понятно, что какой-то конкретный коммит содержит слишком много изменений. В таком случае его можно разбить на части с помощью команды edit. Ребэйз остановится на этом коммите, и мы можем удалить последний коммит и отредактировать изменения. После чего продолжить ребэйз с помощью git rebase --continue.
  • Наличие команды run для запуска произвольных команд шелла, безусловно, радует. Но мне пока она ни разу не требовалась.

Если наши команды слишком безумны, и коммиты не могут быть наложены в указанном порядке, то на помощь всегда готов прийти git rebase --abort. Мой собственный опыт подобных неудач подсказывает ещё один дельный совет по применению ребэйза: не стоит увлекаться и сразу перетасовывать все коммиты с ног на голову. Если что-то пойдёт не так, то есть риск, что придётся надолго застрять в исправлении конфликтов в коде. Куда проще и быстрее выполнить ребэйз несколько раз подряд, каждый раз применяя только одно изменение в структуре коммитов.

Профит!

Подобный подход может выглядеть сложным и “избыточным”, но на практике временнЫе затраты на него совсем невелики, а выгода вполне очевидна.

Никогда раньше у меня не было настолько подробных и чётких commit message’ей, которые писались бы так легко! Вся хитрость заключается в том, что я пишу их по кусочкам, а затем сливаю эти кусочки друг с другом. Каждый небольшой и цельный коммит описать намного проще, чем всё изменение в целом. И объединить два предложения в абзац тоже проще, чем написать их с ноля.

Теперь я могу выстраивать цепочку коммитов перед изданием не в том порядке, в котором я правил код, а так, чтобы они рассказывали историю читателю моего кода (сам люблю хорошие истории!). Например, расчистка кода и предварительный рефакторинг отправляются в начало истории, а сама новая фича выходит уже в конце, на подготовленную сцену. Коммит, очищенный от других изменений, проще читать и понимать, его проще перенести в другую версию продукта.

Есть ли недостатки или сложности у моего подхода? Прежде всего, он требует определённой практики. Если не создавать микрокоммиты в правильный момент, то на фазе ребэйза придётся помучаться. Поэтому требуется определённое время, чтобы настроить внутреннее чутьё, которое будет подсказывать “давай, делай коммит прямо сейчас!”.

Ещё бывает сложно управляться с коммитами, когда их становится слишком много. Учитывая ограниченность человеческого мозга, фаза “слишком много” может наступать уже на 15-20 мелких коммитах. О чём это обычно говорит? О том, что пришло время собирать камни.

Интересно, есть ли аналог интерактивного ребэйза у mercurial и других соперников git? За 3-4 месяца применения этой команды я понял, что это просто мега-убойная вещь.