Статьи

Git: Сохранение вашей истории в чистоте


Когда мы недавно перешли с Subversion на Git, с самого начала стало ясно, что нам нужны некоторые дополнительные правила для поддержания чистоты истории.
Первое, на что мы наткнулись, это так называемые автоматические ‘коммиты слияния’, выполняемые git.

В случае, если вы не знаете, что они; представьте типичную закрытую настройку проекта с центральным репозиторием для разделения работы между несколькими разработчиками. Теперь предположим, что разработчик Джон совершил и внес некоторые изменения. Тем временем Джим разрабатывает что-то в той же ветке в своем локальном хранилище. Джим не вытащил из центрального, поэтому центральное хранилище еще впереди. Если Джим готов со своим кодом, он вносит изменения в свой репозиторий. Теперь, когда он пытается перенести свою работу в центральное положение, git теперь будет инструктировать Джима сначала потянуть (чтобы получить последние изменения из центрального), прежде чем он сможет снова нажать. С того момента, как Джим делает «git pull», git делает коммит слияния, указывая на коммит, выполненный Джоном.

Это ново, когда приходит из Subversion, так как с SVN вы не сможете зафиксировать в первую очередь. Сначала вам нужно будет выполнить «svn up» перед выполнением «svn commit», чтобы ваши изменения все равно были в вашей локальной рабочей копии (в отличие от git, где они уже зафиксированы в вашем локальном репозитории). Чтобы быть ясным: эти коммиты слияния не формируют техническую проблему, поскольку они «естественны» (в некоторой степени), но они сильно загромождают историю.

Чтобы показать, как это будет выглядеть в нашей истории, мы смоделируем это локально, используя три репозитория (центральное, чистое git-репо и john / jim, оба клона центрального). Сначала Джон совершает и подталкивает john.file1. Затем Джим создает и фиксирует jim.file1. Если Джим сейчас попытается продвинуть свою работу, он получит:

 
! [rejected]        master ->; master (fetch first)
error: failed to push some refs to '/tmp/central/'
hint: Updates were rejected because the remote contains work that you do
hint: not have locally. This is usually caused by another repository pushing
hint: to the same ref. You may want to first merge the remote changes (e.g.,
hint: 'git pull') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details

Если Джим просто «вытянет» изменения из источника, они будут объединены, и Джим, в свою очередь, сможет перенести свои изменения в центр. Все хорошо. Тем не менее, наша история теперь будет содержать так называемый коммит слияния, как показано ниже:

Как уже говорилось ранее, это происходит потому, что местное отделение продвинулось, по сравнению с центральным, с обязательством Джима. Поэтому git больше не может делать перемотку вперед. Git будет (по умолчанию) в основном объединяться с изменениями, которые он получал от источника, и поэтому должен делать коммит. Изменение сообщения о коммите все еще возможно, но в любом случае вы получите реальный коммит. В результате, кажется, что изменение делается дважды, если смотреть на историю, что может быть довольно раздражающим. Кроме того, если команда совершает / продвигает быстро (как они должны), многие из этих записей появятся и действительно начнут загромождать историю.

Решением для этого является перебазировка вместо слияния. Это можно сделать, выполнив:

 
git pull --rebase

При использовании ‘git pull’ git сначала перенесет изменения из источника в локальное хранилище, а затем (по умолчанию) объединит их с вашей локальной рабочей копией. Если вместо этого выполнить перебазирование, git (временно) отменит ваши коммиты, перемотает вашу ветку вперед и повторно применит ваши изменения. Эти коммиты будут новыми коммитами, так как старые выбрасываются. Они получают новый идентификатор SHA1 и все лайки.

Однако ребаз является лишь решением в конкретном сценарии, поэтому мы бы не сделали его дефолтом для всего. Из
книги Git :

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

Пока вы не играете с коммитами, которые уже были переданы другим, проблем нет. Мы работаем в стандартном закрытом Java-проекте с единым центральным репозиторием, который придерживается следующих правил:

  • Разработчики не синхронизируют репозитории напрямую друг с другом, они всегда идут через центральный
  • Разработчики не стремятся ни к чему другому, кроме центрального

В этом случае нет никакой опасности, что коммиты будут обнародованы перед их перебазированием с помощью git pull. Таким образом, мы можем безопасно сделать ребаз «git pull» автоматически для нашей главной ветки проекта, используя:

 
git config branch.master.rebase true

Еще одна проблема, которая привлекла мое внимание (которая возникает при рассмотрении проблемы автоматической фиксации слияния), заключается в том, что наша структура ветвления была неоптимальной. Мы используем подход с ветвлением функций, но поскольку ветвление (и слияние) функций с Subversion сопряжено с большими трудностями, мы сделали это только в случае крайней необходимости. Однако при использовании git создание веток и слияние просто, поэтому мы изменили наши правила, чтобы сделать их более естественными:

  • There must be a (in our case; more) stable master branch on which only completed features will be merged into. Theoretically speaking, no one works directly on the master. This implies that almost all work is executed on feature branches
  • Releases are made from a release branch which is a copy of the master branch at the point the release is triggered
  • The master log must be as clean and straightforward as possible: each feature being merged back into the master must be exactly one commit with the log message stating the ticket and the history showing all changes relevant to that feature.

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

  • Обзоры кода должны быть сделаны на функцию, так или иначе
  • Ветвь функции должна сначала быть обновлена ​​до последней версии мастера. Это происходило регулярно при разработке этой функции (вы хотите быть в курсе мастера, насколько это возможно), но, безусловно, в последний раз, прежде чем вы планируете реинтегрировать эту функцию. С git это означает, что вы просто делаете «git merge master», находясь в ветви функций, и это завершится за считанные секунды. В этом случае конфликты (других функций, которые были в то же время реинтегрированы в мастер) могут быть разрешены в ветке функций.
  • Тесты и лайки должны быть запущены на этой функции и должны быть на 100% успешными перед реинтеграцией этой функции.

При следовании этим правилам следующие 3 строки будут реинтегрировать объект, выполняющийся один за другим, превращая его в почти атомарную операцию и, таким образом, фактически делая ненужной перезагрузку:

 
git checkout master
git pull
git merge --no-commit --squash >feature branch<
git commit -am "FEATURE-XYZ"
git push 

Теперь, даже работая таким образом, мы оставляем для rebase значение true для мастера, поскольку дистанционное управление все еще может измениться. В случае, если это произошло (после совершения слияния), простое ‘git pull’ (с неявной перебазировкой) решит проблему так, что наш push будет успешным.

Другие важные правила для сохранения чистой истории используют ‘—squash’, как указано выше:

—Squash отбросит любое отношение к функции, с которой он пришел. Делая это, вы можете создать единый коммит, который содержит все изменения, не перенося никакой другой истории из вашей функции в ваш мастер. Это в основном то, что вы хотите. Функция может содержать множество «ненужных» коммитов, которые вы не хотите видеть на своем мастере. В типичном случае вы просто хотите увидеть результат, а не промежуточные звенья. Также обратите внимание, что —squash также подразумевает —no-ff, поскольку git не будет перематывать вперед, даже если это возможно (в противном случае вы не сможете получить ни одного коммита слияния).

Чтобы проиллюстрировать это, когда мы просто реинтегрируем, используя ‘git merge’, история будет выглядеть так:

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

При отключении перемотки вперед (git merge —no-ff) история будет выглядеть так:

Немного лучше, поскольку теперь мы можем видеть, какие коммиты связаны с feature-xyz. Тем не менее, изменения по-прежнему разбросаны по различным коммитам, и у нас все еще есть другие коммиты, в которых мы не заинтересованы. Кто-то, кому нужна история Feature-Xyz, обычно не интересуется тем, как развивалась функция или сколько раз класс подвергался рефакторингу. Обычно вас интересует только конечный результат: изменения / дополнения, которые будут добавлены в основную ветку. Добавляя «—squash», мы выполняем чистую и единственную фиксацию в нашем мастере, которая перечисляет все изменения для этой функции:

Обратите внимание, что история функции не потеряна, конечно. Вы все еще можете вернуться в ветку функций и посмотреть историю, если это будет необходимо.

Параметр —no-commit удобен, если вы скептически (как и я) и хотите в последний раз просмотреть измененные / добавленные файлы, прежде чем завершить реинтеграцию. Если вы не включите эту опцию, Git будет напрямую фиксировать слияние, не оставляя времени для окончательной проверки.

С другой стороны: чтобы выполнить проверку кода на объектах до того, как они могут быть реинтегрированы, я стремлюсь создать (только локальную) ветвь мастера, являющуюся своего рода веткой «пробного запуска». Затем эта функция реинтегрируется в эту ветку проверки (без внесения изменений) просто для проверки кода. При этом все измененные файлы будут помечены как загрязненные вашей IDE, что позволит легко увидеть, что на самом деле изменилось (и сравнить их с ревизией HEAD). Затем я постепенно фиксирую изменения по мере их просмотра. Когда проверка кода будет завершена, слияние будет полностью зафиксировано в ветви обзора, и затем оно может быть объединено обратно в ветвь функций, прежде чем она будет наконец реинтегрирована в основную ветку. Я (обычно) не делаю ветку обзора удаленно видимой, а просто удаляю их локально после завершения проверки.Теоретически можно также слить ветку обзора обратно в master напрямую, но, поскольку эта ветка обзора будет удалена, ветвь реальных функций никогда не получит изменения, выполненные в результате проверки кода, и вы потеряете эту историю.