Составление описания изменений на примере git-фиксаций

На протяжении последних лет, я часто использую git в повседневной работе. За эти годы я работал с несколькими командами и с большим количеством git-хранилищ(repository). Хотя уровень команд и их знакомства с git был разным, одно было неизменно: в большинстве своём git использовался либо не правильно, либо нерационально. Кроме самого использования git, я столкнулся и с другой проблемой – никто(я преувеличиваю) не умеет писать описание фиксации изменений(commit). В результате всего этого мы, как правило, имели убогую историю хранилища и почти священный трепет перед git у многих членов команды. В настоящей заметке я бы хотел рассмотреть самые базовые правила(в данной заметке я не ставлю цели ликвидировать “священный трепет”, для этого есть книги по git), которым должен следовать любой разработчик использующий git. Конечно, с моими правилами кто-то может не согласится и это его право. На то они и мои правила. Целью данной заметки является как повышение культуры составления описаний изменений в общем, так и структуризация и фиксация тех правил, соблюдения которых мне бы хотелось видеть в тех командах, в которых я имею честь работать.

Описание изменений

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

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

Таким образом, описание фиксации изменений служит по меньшей мере двум целям:

  • Облегчает понимание кода, т.к. человек имеет описание того, что было изменено. Ему не нужно продираться сквозь дебри кода, лишь для того, чтобы понять смысл манипуляций с оным.
  • Облегчает понимание причины изменений, т.к. человек имеет описание того, зачем изменения были представлены. Это, в частности, помогает оперативно отменить некоторые изменения, если тот, кто просматривает изменения находит причину их появления не существенной или ошибочной.

Это становится тем важнее, чем большим промежутком времени разделены время изучения фиксации изменений и время её публикации. Ведь очень часто бывает, что в поисках той или иной проблемы, программисту приходится глубоко копаться в истории хранилища. И найдя “ту самую фиксацию”, в которой спорный код был представлен, хорошо бы понять зачем он был представлен. Это ещё более усугубляется в том случае, если чел��век сделавший эти изменения уже не доступен для допроса. Да и в случае его доступности, никто, как правило, не помнить зачем он именно так написал. Неоднократно сталкивался с тем, что на вопрос: “Зачем тут эта строчка(строчки)?”, человек отвечает: “Надо вспоминать, давно это было”. Такой проблемы не должно возникнуть если подходить к описанию изменений со всей тщательностью, описывая в том числе и те решения, которые могут впоследствии вызвать вопросы.

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

Плохое описание

В качестве примера хранилища, на основе которого я буду рассматривать плохие описания, я выбрал хранилище FFmpeg кода – достаточно известная и активно разрабатываемая библиотека(набор библиотек).

Под каждым оригинальным сообщением, я привёл SHA-код соответствующей фиксации, чтобы вы могли увидеть эти изменения самостоятельно. Я ни в коем случае не выбирал гадких утят среди лепестков роз – я брал их почти подряд.

Начнём:

avfilter/vf_scale: fix log message category

SHA-1: cab8fc624b401624be938c59b23c23136d5c5cb5

Что можно понять из этого сообщения? Исправили категорию относящуюся к ведению журнала. Из этого описания ясна цель внесения изменений, но совершенно непонятно зачем это было сделано. В чём была проблема предыдущей категории? Чем новая лучше?

hevc: fix skip_flag

SHA-1: 772f7f4eddbc4450443743984bf1d4da2f4bf84c

Целью вышеозначенного изменения было исправление некого skip_flag. Но зачем его исправили? Что с ним было не так? Чем это изменение полезно пользователю библиотеки, или, быть может, программисту FFmpeg? А может это изменение каким-то образом улучшает воспроизведение(я понятия не имею), что отразится на конечном пользователи плеера основанного на FFmpeg? Подобные вещи нужно обязательно упоминать!

Fix standalone compilation of the webm_dash_manifest demuxer.

SHA-1: bcb7220d1cf99d5090ed8f6ff76421d8f506ec3a

Ещё одно описание, в котором ясна лишь цель. А в чём была проблема с компиляцией до этого? Может быть это исправление не требовалось, а автор просто чего-то не понял? Так тоже очень часто бывает, а из такого описание сложно понять, что же побудило автора исправлять компиляцию.

Merge commit '1a880b2fb8456ce68eefe5902bac95fea1e6a72d'

SHA-1: 706f81a2c27552fdc9d216834eedd36f5804c080

Подобные описания – мои “любимые”, из самого описания невозможно понять ничего. Кроме, конечно, того, что это слияние(merge). Я прекрасно понимаю, что подобные описания генерируются github’ом автоматически, но нельзя же такое оставлять! Человек должен идти на другую фиксацию, чтобы понять к чему это слияние относится. Зачем кому-то знать, что лежит в основе фиксации: слияние или прямое изменение? Как это отразится на работе? В большинстве случаев – это знание бесполезно. Поэтому такие описание просто нельзя оставлять, в простейшем случае нужно просто скопировать описание фиксации, которое идёт вместе с запросом на слияние(pull request). Можно пойти и более сложным путём - дописать описание фиксации, если автор изменений не потрудился этого сделать самостоятельно, а вы, по какой-то причине, не может переложить этот груз ответственности на него. Но, как бы то ни было, у фиксации должно быть нормальное описание!

Итак, рассмотрев примеры описаний, которые нельзя назвать образцовыми или даже достойными, перейдём к тому, что же я предлагаю в качестве “нормального” описания.

Хорошее описание

На мой взгляд, хорошее описание фиксации изменений выглядит следующим образом:

[<Номер задачи>] Краткое, но ёмкое описание сути изменений.

Подробное описание изменений.

[Список изменений, напрямую к данной задаче не относящихся]

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

Краткое описание Описание сути изменений. Написание краткого описания(он же заголовок фиксации в git)

Подробное описание Здесь уже можно разойтись и написать столько текста, сколько требуется, чтобы у других людей не возникло вопросов по-поводу цели, а так же самих изменений. В этой части нужно описать всё, что может быть непонятно из кода, а так же всё, что в код не попало(например, почему использована сортировка типа А, а не Б). В эту же секцию стоит поместить описание того, как данное изменение улучшает работу приложение/сайта/библиотеки/…

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

Пример:

ABC-111 Избавился от неуловимого падения приложения.

Приложение падало из-за не сериализованного доступа к <общий_ресурс>. 
Добавил мьютекс, как временное решение(лучшим решением будет отказ от <общий_ресурс>).

Другие изменения:
* Переименовал <класс>
* Удалил старый код по проверке <чего-то>, т.к. мы больше не используем <что-то>

Конечно, подробное описание, как правило, более объёмно, но это просто пример того, о чем я говорю. Как вы видите. у описание есть заголовок, потом раскрывается суть изменений и всё оканчивается списком изменений, который к задаче ABC-111 не имеют отношения. При этом, комментарий про “лучшее решение”, можно было бы поместить и в описание задачи, т.к. непосредственно к этой фиксации он отношение имеет весьма опосредованное. Тем не менее, если в команде не принято оставлять подобные комментарии в описании задачи, его можно оставить прямо в описании фиксации.

Хотя мой пример довольно надуман, я полагаю, что польза подобного описания довольно прозрачна: читая подобное описание разработчик видит, что изменения относятся к задаче ABC-111, просмотря которую он может понять, что за проблема была решена в этой фиксации. Прочтя подробное описание, разработчик может понять в чём была проблема, с точки зрения кода. И как она была решена.

К сожаление, как отсутствие подробного описания может быть проблемой, так и избыточное описание может стать таковой. Во всём нужно знать меру, и не повторять словами то, что написано в коде. Описание должно быть достаточно высокоуровневым. Как в коде, комментарии типа

//Инкрементируем счётчик
counter++;

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

Когда можно схалтурить?

Хотя хорошее описание фиксаций это действительно важный элемент содержания хранилища кода в порядке, есть ситуации когда написание оного может стать помехой. Не секрет, что составление хорошего описания не всегда процесс быстрый и это может сказаться весьма плохо на количестве фиксаций, которые делает разработчик в рамках работы над задачей. И если фиксации, идущие в главную ветвь, всегда должны содержать подробное описание изменений, то так называемые тематические ветки(topic branch, feature branch etc.), на мой взгляд не должны так жёстко регулироваться.

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

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

Главное, что стоит уяснить для себя – составление хорошо построенного описания не должно стать помехой в частоте фиксирования изменений. Не нужно накапливать изменения из-за того, что не хочется писать много описаний. Изменения должны быть настолько малыми, насколько позволяет задача. А если вы не знаете, что написать по-поводу какого-либо изменения, то зачем вы вообще его сделали?

Слияния

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

  • Слияние побочной ветки в основную.
  • Слияние основной ветки в побочную.
  • Слияние ветки с самой собой.

Для начала следует пояснить, что я называю основной, а что побочной ветками. Основной веткой, я называю такую ветку, в которой на постоянной основе работают несколько разработчиков и в которой представлены изменения, которые могут быть не связаны между собой ничем, кроме того, что они принадлежат одному хранилищу. Такие ветки существуют довольно продолжительное время. Примером основной ветки, может быть ветка master, выделенная ветка под релиз и т.п.

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

Теперь давайте разберём каждый случай отдельно:

Слияние побочной ветки в основную

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

Слияние основной ветки в побочную

Слияние основной ветки в побочную это рутинная операция, которая должна проводится настолько часто, насколько это возможно(не реже одного раза в день). Цель подобного слияния – получения всех последних изменений в побочной ветке, чтобы адаптировать свой код соответственно. Т.к. мы производим слияние в побочную ветку, а так же принимая во внимание тот факт, что мы уже имеем описание всех изменений в ветке основной – мы можем оставить сообщение, которое генерируется git автоматически. Либо же составить что-то нейтральное самостоятельно:

Получение последних изменений из master

Слияние ветки с самой собой

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

  1. Борис закончил свои изменение и зафиксировал их; он ещё не отправил их на сервер.
  2. Иннокентий тоже зафиксировал свои изменения и отправил их на сервер(git push)
  3. Борис пытается выполнить команду git push, но получает ошибку с предложением обновить локальное хранилище
  4. Вняв совету из ошибки Борис выполняет git pull
  5. После этого он выполняет git push и команда выполняется без ошибки.

Можно поздравить Бориса, он только что создал фиксацию-слияние с автоматически сгенерированным сообщением вида:

Merge branch '<имя ветки>' of <URL хранилища> into <имя ветки>

Почему произошло слияние? Потому что git pull, это ничто иное как комбинация git fetch и git merge. И так как в удалённой ветке есть фиксация, которой нет в ветке локальной, то fast forward слияние не может быть выполнено, а значит будем иметь полноценную фиксацию-слияние. Подобного можно избежать, постоянно выполняя git pull, прежде чем фиксировать своим изменения. Но и это может не спасти, т.к. всегда остаётся шанс, что кто-то успеет отослать изменения на сервер быстрее, чем это сделаете вы. Тем не менее есть вполне надёжный способ защититься от подобных фиксаций – git pull -r. Данная команда есть комбинация  git fetch и git rebase, что позволяет избавится от избыточного слияние. На мой взгляд, git pull –r должна всегда использоваться вместо простого git pull, т.к. ничто не портит историю хранилища так сильно, как слияние ветки с самой собой(я не говорю тут о тех командах, которые играются с историей, они, безусловно, ещё хуже)