Одним летним днем ​​в конце августовской жары 2014 года тогдашний президент США Барак Обама принял решение, которое шокировало всю страну: он надел другой костюм. Возникший в результате спор о коричневом костюме доминировал в цикле новостей и распространялся по разным причинам, но в конечном итоге он был вызван новизной самого костюма. Подобно черным водолазкам Стива Джобса и серым рубашкам Марка Цукерберга, Обама каждый день носил одни и те же синие или серые костюмы.

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

Итак, какое отношение все это имеет к любимой всеми системе контроля версий, git? Как и во многих случаях в программировании, не существует «правильного» способа структурировать коммит git или управлять историей git проекта; вы должны просто выбрать руководящий принцип и организовать вокруг него свои паттерны. Я лично верю в выбор стратегии, которая снижает усталость от принятия решений (и «умственную усталость» в более широком смысле) для всех различных «потребителей» коммита. Я более подробно расскажу о том, как это сделать ниже (и не стесняйтесь просто перейти к этому пронумерованному списку, если хотите), но я думаю, что очень важно сначала объяснить, почему Я верю в это. И нам нужно начать с того, для кого на самом деле предназначен коммит.

Чья это история?

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

Имея это в виду, стоит отметить, что есть две широкие категории людей, которые в какой-то момент просматривают конкретный коммит:

  1. Рецензенты кода
    Те, кто просматривает фиксацию до ее добавления в историю в процессе проверки кода. Этих людей обычно называют «ревьюверами кода». В хорошей команде каждый в какой-то момент будет рецензентом кода, и для каждого набора новых изменений кода может быть несколько изменений.
  2. Code Detectives
    Те, кто просматривает фиксацию после ее слияния.Как правило, это лица, возвращаясь к истории, чтобы попытаться понять, почему что-то было добавлено или когда была введена ошибка. За неимением лучшего названия я буду называть этих людей «детективами по коду», чтобы отличить их от людей, описанных выше.

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

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

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

Решения, Решения

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

Без какого-либо дополнительного контекста при просмотре одной строки «добавленного» кода необходимо принять следующие решения:

  1. Это совершенно новая строка кода?
  2. Если это не новая строка кода, является ли она существующей строкой кода, которая просто была перенесена откуда-то еще?
  3. Если это не новая строка кода и она не была перемещена, является ли это тривиальной модификацией существующей строки (например, изменением форматирования) или это законное логическое изменение?
  4. Если это либо совершенно новая строка кода, либо модификация, которая приводит к логическому изменению, то зачем это делается? Правильно ли это сделано? Можно ли его упростить или улучшить?

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

Мы можем видеть аналогичный процесс для каждой «удаленной» строки кода:

  1. Эта строка полностью удаляется?
  2. Если он не удаляется полностью, то перемещается или модифицируется?
  3. Если он неудаляется полностью, а не просто перемещается, является ли это результатом тривиальной модификации (например, форматирования) или результатом логического изменения?
  4. Если это действительно логическая модификация, то почему она модифицируется? Правильно ли это делается?
  5. Если линия удаляется полностью, почему она больше не нужна?

Еще раз, последние два пункта, что действительно важно.

Итак, это, наконец, возвращает нас к усталости от принятия решений:

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

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

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

1. Размещайте тривиальные модификации в собственных коммитах

Самое простое и самое важное, что нужно сделать, это разделить тривиальные модификации на отдельные коммиты. Некоторые примеры этого включают:

  • Изменения форматирования кода
  • Переименование функции/переменной/класса
  • Переупорядочивание функций/переменных/импортов внутри класса
  • Удаление неиспользуемого кода
  • Перемещение файлов

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

Рассмотрим следующий коммит, смешивающий тривиальные изменения с нетривиальными:

Сообщение фиксации: "Обновить список допустимых фруктов"

Сколько времени вам понадобилось, чтобы заметить нетривиальные изменения? Теперь посмотрите, что произойдет, если эти два изменения разделить на два отдельных коммита:

Сообщение фиксации: "Обновить допустимое форматирование списка фруктов"

Сообщение фиксации: "Добавьте даты в список допустимых фруктов"

Фиксацию «только форматирование» можно по существу игнорировать, а дополнения кода можно обнаружить сразу же с первого взгляда.

2. Поместите рефакторинг кода в собственные коммиты

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

Например, как быстро вы сможете обнаружить здесь ошибку?

Сообщение фиксации: "Обновить логику подсказки"

Как насчет выделения рефакторинга?

Сообщение фиксации: "Извлечь ставку чаевых по умолчанию"

Сообщение фиксации: "Разрешить индивидуальную ставку чаевых"

3. Разместите исправления ошибок в собственных коммитах

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

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

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

4. Размещайте отдельные логические изменения в собственных коммитах

После разделения вышеперечисленных типов изменений у вас должен остаться один коммит с законными, логическими изменениями для добавления/обновления/удаления функциональности. Для небольшого краткого изменения этого часто бывает достаточно. Однако иногда этот коммит добавляет совершенно новую функцию (с тестами) и достигает 1000+ строк (или больше). Git не будет представлять эти изменения связным образом, и для успешного понимания этого кода проверяющему потребуется пропустить и сохранить большую часть этих строк в памяти сразу, чтобы следовать дальше. Наряду с усталостью от принятия решений, связанной с обработкой каждой строки, растягивание рабочей памяти таким образом утомительно для ума и, в конечном счете, неэффективно.

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

5. Объедините любые изменения обзора с коммитами, которым они принадлежат.

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

- <Initial commits>
- Respond to review feedback
- Work
- More work
- Addressing more review feedback

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

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

Рассмотрим это начальное изменение, за которым последовало несколько «рабочих» коммитов:

А теперь представьте, что вы видите эти изменения уже в самом процессе (или даже годы спустя). Разве вы не хотели бы просто увидеть следующее?

6. Перебазируйте, перебазируйте, перебазируйте!

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

  1. Объедините основную ветвь с функциональной ветвью. Это создаст «коммит слияния», в который будут включены все изменения кода, необходимые для устранения конфликтов. Если ветка функций особенно старая, эти типы коммитов могут быть существенными.
  2. Перебазируйте функциональную ветку относительно основной ветки. Конечным продуктом здесь является новый набор коммитов, которые действуют так, как если бы они были только что созданы на основе обновленной основной ветки. Любые конфликты должны быть устранены в рамках процесса перебазирования, но все доказательства исходной версии кода исчезнут.

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

Если вы заботитесь о создании чистой истории (а вы должны!), то лучшим вариантом здесь будет перебазирование: все изменения строятся упорядоченно и линейно. Вам не нужны причудливые инструменты для просмотра истории, чтобы понять взаимосвязь между ветвями.

Рассмотрим следующую историю проекта, в которой используется слияние ветвей:

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

В первом случае отношения между изменениями должны быть нанесены на карту, обдуманы и расшифрованы; во втором вы просто течете вперед и назад во времени.

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

Контраргументы

Безусловно, против такого подхода можно привести аргументы, и у меня было множество дискуссий с людьми, которые с ним не согласны. Эти моменты не лишены достоинств, и, как я упоминал в начале статьи, не существует единственного «правильного» способа структурировать коммиты. Я хочу быстро выделить некоторые моменты, которые я слышал, и высказать свои мысли по каждому из них.

«То, что вы так сильно беспокоитесь о структуре коммитов, замедляет разработку».

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

«В моем проекте используются инструменты, которые не позволяют вносить даже тривиальные изменения форматирования».

Я согласен с тем, что это отличный способ свести к минимуму вред, который в противном случае причиняется оттоком кода, связанным с форматированием. Как разработчик Android, я твердо верю в использование автоформатеров всей командой и клянусь такими инструментами, как ktlint. Тем не менее, я также знаю из первых рук, настроив все эти инструменты, что они не идеальны, и существует множество возможных изменений форматирования, о которых они совершенно не знают. И, как обсуждалось выше, некоторые тривиальные изменения — это не просто изменения форматирования, например изменение порядка кода. Всегда будут тривиальные изменения кода, которые можно внести, и поэтому должен быть план, как лучше с ними справиться.

«Не все сайты хостинга git разрешают запросы на вытягивание с несколькими коммитами».

Это очень верно! Мои рекомендации в первую очередь основаны на использовании таких инструментов, как GitHub и GitLab, которые позволяют PR иметь столько коммитов, сколько вы хотите, но есть такие инструменты, как Gerrit, которые этого не делают. В этом случае просто рассматривайте каждый коммит как отдельный PR. Это создает дополнительные накладные расходы для автора (а иногда и для рецензентов), но я считаю, что в долгосрочной перспективе это стоит затраченных усилий. Могут быть даже способы упростить этот процесс и связать эти отдельные PR друг с другом, например, используя зависимые изменения в Gerrit.

«Один коммит гарантирует, что все изменения компилируются и проходят тесты».

Это тоже очень хороший момент. Автоматические проверки, которые выполняются на сайтах хостинга git, обычно выполняются только для всего набора изменений, а не для каждого отдельного коммита. Если на этом пути есть сломанная фиксация, которая исправлена ​​более поздними изменениями, нет никакого способа автоматически обнаружить это. Вы хотите, чтобы каждый коммит мог работать сам по себе на тот случай, если вам когда-нибудь понадобится вернуться и протестировать состояние кода на тот момент, чтобы отследить ошибки и т. д. Как понятное правило, это должно требоваться для каждого коммита в multi-commit PR как для компиляции, так и для прохождения любых соответствующих тестов, но нет способа строго обеспечить это (кроме того, чтобы сделать каждый коммит своим собственным PR). Это требует бдительности, но это просто то, что нужно сопоставить с преимуществами, которые дает разделение кода.

«Один коммит обеспечивает наибольший контекст для всех изменений».

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

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

Последние мысли

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

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

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

Брайан работает в Livefront, где он всегда пытается сделать еще немного (git) истории.