
Начинающие (а иногда этим грешат и опытные) разработчики, не до конца понимают принципы создания и работы над коммитами в git. Тут имеется ввиду не механика и команды типа “git commit …”, а общие и глобальные вещи. Например:
- А когда делать коммиты?
- Что в них писать?
- Есть ли какие-то общие правила для их создания?
- Как не надо коммитить?
Если ты начинающий разработчик, то эта статья точно тебе пригодиться. А если у тебя огромный опыт и ты думаешь, что тебя уже ничем не удивить, то… Не будем торопиться… Давай проверим? )))
Теория
Давайте начнем немного с теории, чтобы понять, о чем будет речь. Что вообще такое коммит в git?
Коммит в Git — это команда в системе контроля версий Git, которая фиксирует изменения в репозитории. Когда пользователь использует её, создается «снимок» текущего состояния проекта, включая все внесенные изменения.
Каждый коммит содержит информацию о том, какие файлы были изменены, кто и когда сделал изменения, а также комментарии к этим изменениям.
Коммиты в Git дают возможность:
- откатить проект к предыдущим версиям;
- сравнивать изменения;
- объединять ветки и выполнять другие операции управления исходным кодом;
А теперь, давайте рассмотрим то, что побудило меня написать эту статью, а именно, примеры неправильного подхода к коммитам.
Коммиты в стиле: начинаю кодить с утра, делаю коммит вечером
Первый пример, который встречается очень часто — это, пришел на работу в 9 утра и начал писать код, а сделал коммит только в обед… Или вообще вечером ^_^ (признавайтесь, кто так делает?)
Иногда этим грешат все и даже те, кто знает, что так делать нельзя. На самом деле это не правильный подход.
Главный минус этого подхода в том, что в процессе правок можно один час писать просто идеальный код, потом вы сделали логическую ошибку (все мы люди, да) и не заметили ее сразу, и еще час что-то писали.
И только через этот час стало понятно, что в коде есть ошибка и приложение сломалось, а вот где непонятно и даже отладка с этим может не справиться. Есть не маленький риск, что, разбираясь с измененным кодом вы придете к выводу, что проще откатить вообще все изменения и переписать все с нуля. При этом вы потеряете, в том числе, и тот доработанный код, к которому вопросов, в общем-то, и не было.
Это как “засэйвиться” в компьютерной игре. Если этого долго не делать, то потом может быть мучительно больно. Придется начинать не с последнего сохранения, а черт знает откуда…
Минусов использования такого подхода к коммитам много, не будем подробно останавливаться, что еще не так с таким стилем кодинга.
Работа без веток
Вторая часто встречающаяся ошибка работы с коммитами — это коммит сразу в master или main-ветку без использования промежуточных веток.
Вообще, git ценен именно тем, что есть возможность работать с разными ветками и странно, что многие этой возможностью вообще не пользуются.
Аргумент - я работаю один над этим проектом.
Main-ветку необходимо использовать для того, чтобы в ней находилась полноценно работающая версия нашего ПО. А вливать изменения в main из дополнительных веток нужно только после того, как все будет проверено, пройдены тесты, и чтобы мы были уверены в добавленном функционале.
Даже, если изменения не большие.
Даже, если вы пишите вообще один в этом репозитории.
Был у меня в команде такой сотрудник, который думал: “а че такова?” и закоммитил в main без должного тестирования, а потом в релизе ловили фонарь.
Конечно же, так лучше не делать. Настройте сборку и тестирование, проверяйте при вливании в main изменения. Все CI/CD системы могут автоматически билдить / тестировать / собирать дистрибутивы. Но это тема отдельного разговора.
3. Отсутствие атомарности в коммитах
Рассмотрим третий пример, где присутствует ошибка работы с коммитами.
Добавляемый коммит содержит сразу несколько не подчиненных одной цели, или не связанных между собой изменений. Вообще, стоило начинать с этого пункта, так как именно он, на самом деле, является самым главным.
Пример неправильно скомпонованных по изменениям коммитов, хорошо описывается народной пословицей: “В огороде бузина, а в Киеве дядька”.
Она отлично подходит для понимания сути проблемы. В таких коммитах будет одновременно намешаны: исправление ошибки + новые фичи + правки документации и тестов и т.д.
Чем это плохо? Это вызовет определенные проблемы при работе в команде. Если вы работаете не один, и другой человек по какой-то причине будет вынужден изучать ваши изменения в коде, очевидно, что он не поймет зачем вы сделали такой коммит и что вообще хотели сделать изначально?
Ну и те же проблемы, что и в пункте 1. Если есть ошибки в первой части, а во второй их нет, то откат придется делать для всего либо как-то дополнительно исхитриться, бороздя просторы команд git.
Если в проекте вы пишите код один, то все равно надо делать коммиты атомарными. При исправлении конкретной ошибки исправляйте именно ее. Не нужно вмещать в один коммит все, что пришло вам в голову пока вы писали код. Так вы теряете фокус на проблеме.
Conventional Commits
Давайте пойдем дальше…
Вы скажите: “Ну окей. Ты привел столько примеров как неправильно, а как тогда правильно?”
На самом деле, не только я один задавался таким вопросом. Умные люди придумали Conventional Commits — это такое соглашение о структуре сообщений коммитов, которое предлагает простой и понятный набор правил для создания истории изменений. Есть такой сайт: https://www.conventionalcommits.org/ru/v1.0.0/, где разместили эти правила на разных языках.
Каждый может ознакомиться с этими правилами. Там есть, в том числе, перевод на русский.
На хабре есть статьи на эту тему, но все равно, давайте я чуть-чуть пробегусь по содержанию. Проговорим основные тезисы этого соглашения.
Структурированный формат: каждое сообщение коммита следует определенному вот такому формату:
<тип>[необязательный контекст]: <описание> [необязательное тело] [необязательная(ые) сноска(и)]
где:
- сначала идет <тип> и обозначает тип изменений. Например, feat (новый функционал), fix (исправление), chore (рутинная работа), docs (документация), build (сборка релиза) и т.п.
- контекст является необязательным и указывает часть проекта, к которой относится коммит (например, компонент или файл). Часто контекст берут в скобки. Иногда используют восклицательный знак или дописку, но об этом поговорим чуть позже.
- описание — краткое описание сделанных изменений.
- тело — необязательно. Здесь можно подробно описать, что мы сделали (если есть такое желание)
- сноски — можно написать кто автор ревью, или ссылки на документацию как работает функционал для коммита.
Если мне не изменяет память, то длина первой строки должна быть не более 70 символов.
Примеры:
Вот такой первый пример:
fix(api)!: Исправлена ошибка метода PATCHfix - означает, что исправлена ошибка. Контекст указывает на то, что ошибка исправлена в api. Восклицательный знак (или аналог в сноске словосочетание BREAKING CHANGE) говорит о том, что изменение нарушает обратную совместимость.
feat: Отправка электронного письма клиенту, когда товар будет отправлен
feat - добавлен новый функционал.
feat: разрешить предоставленному объекту конфигурации расширять другие конфигурации BREAKING CHANGE: `extends` key в конфигурационном файле теперь используется для расширения других конфигурационных файлов
У себя в компании мы используем gitlab. А в контексте мы всегда пишем номер тикета с буквой “T” и тире, а дальше номер тикета. Например “T-2309”. Это означает, что подробности находятся в тикете под номер 2309. Если правильно все настроить, то gitlab сделает в коммитах такие тикеты кликабельными и появится возможность переходить из gitlab прямо в тикет и там увидеть подробности задачи, которую мы решаем. Это очень удобно.
Я думаю понятно, что с таким подходом к коммитам сильно не забалуешь.
Если следовать этому соглашению, то автоматически снимается вопрос том, что в одном коммите мы не сможем добавить и “дядьку и бузину”. Нужно выбирать тип, а это заставляет разработчика определяться над чем он сейчас работает и добавляет осознанности.
Так же, становится понятно, что и коммиты в стиле “пришел на работу начал кодить, закоммитил в обед” тоже нельзя будет сделать. Т.к. в таком случае, будет много не связанных между собой типов коммитов, а мы их должны коммитить по типам и контексту.
Ну и да. Коммиты в main мы убрать не сможем. Но плюс в том, что они станут атомарными и мы сможем быстро вычленить и при желании нивелировать те изменения, которые они привносят. Тоже плюс.
Давайте еще обсудим один важный вопрос. А именно атомарность коммитов.
Атомарность коммитов
В чистом коде, часто упоминают принцип единственной ответственности, который гласит, что единица кода (например, функция или "компонент") должна выполнять только одну задачу. Благодаря этому код легче тестировать и использовать повторно. Хорошая практика — это использовать подобный подход к коммитам.
Вы должны быть в состоянии описать свои изменения одним осмысленным сообщением, не пытаясь добавить к нему дополнительную информацию о какой-то несвязанной работе, которую вы сделали.
Другой способ определить атомарный коммит — это коммит, который может быть отменен без каких-либо нежелательных побочных эффектов или регрессий, помимо тех, что вы ожидаете, основываясь на его сообщении.
Если коммит удаляется из истории коммитов Git, но при этом удаляются изменения, не связанные с общим сообщением коммита, значит, этот коммит не был атомарным.
Еще раз и перефразируем упрощенно.
Атомарность — это когда каждый коммит преследует какую-то одну конкретную цель, которую можно описать одним предложением.
Старайтесь делать коммиты именно так.
Примеры атомарности:
-
feat(T-123): Добавлена функция, которая возвращает задолженность покупателя
-
feat(T-456): Изменен внешний вид компонента выбора файла
-
docs(T-123): Дополнена документация по задолженности покупателя
-
ci(T-789): Добавлена шаг в pipeline тестирования проекта в docker
-
refactor(T-123): Исправлены замечания SonarQube в модуле …
Такой переход к атомарности, позволяет в разработке, даже думать по-другому.
Ты начинаешь размышлять не о том, как сделать вот эту большую задачу за один присест, а о том, как ее разбить на много маленьких и сгруппированных по типу задач. А это уже, мы говорим о декомпозиции задачи.
Видите? Говорили про коммиты, а договорились вон аж до чего! Говорим уже про декомпозицию))) Но тут все логично. Так и должно быть.
Отлично! Скажешь ты - мой юный друг и задашь справедливый вопрос: а как же начать писать код и делать такие коммиты, чтобы соблюсти всё, вот это вот, о чем мы говорили?
Пример
Могу рассказать, как делаю лично я. Понятно, что мы все разные, подход к кодингу у каждого свой. И да, это не инструкция к действию, просто мой опыт и мне так удобно.
Пусть у нас есть такая шуточная большая задача: “Надо съесть слона” и делаем эту задачу мы одни. Прежде всего эту задачу нужно декомпозировать.
Кто помнит, что говорил Глеб Архангельский про то, как надо есть слона? Правильно. “По кусочкам!”.

Беру планшет (ну или лист бумаги) и записываю подзадачи по порядку. Обводя каждую в кружочек:

Ну вы поняли, что дальше…
Тут прям кровожадность. :) Но это просто пример.
Если шаги тоже крупные (а у нас в примере это так), то их снова декомпозируем. Я визуал. Мне проще глазками видеть схемы, список задач, связи и т.п.
Наша цель получить задачи, которые легки в реализации и будут понятны любому разработчику. В описании обязательно должны быть DoD (definition of done) - определение готовности задания (когда будет считать, что задача выполнена) и DoR (definition of ready) - определение готовности для взятия в работу (когда можно будет работать с задачей, возможно каких-то данных не хватает перед началом работы).
Идем дальше. После того как список готов, записаны все шаги, которые надо сделать переношу это все в Управление IT-отделом 8. Это наш продукт, над которым мы работаем и в нем же ведем учет всех тикетов разработки и техподдержки. Где вести тикеты не принципиально, можете использовать Jira, Trello и т.п. Я рассказываю про сам подход.
Дальше я начинаю выполнять любую из этих задач, либо ту, которую необходимо выполнить прямо сейчас и без нее глобальная задача дальше не пойдет.
Выполняю ее.
В процессе выполнения для каждого тикета я делаю отдельную ветку и выполняю столько коммитов, сколько потребуется (почти всегда это больше одного коммита). Когда делаю коммиты исхожу из правил в conventional commits (см. выше).
После выполнения, вливаюсь в main-ветку и зачеркиваю кружочек с подзадачей в планшете.
Эта операция с кружочками доставляет мне удовольствие. Я вижу, что работы по крупной задаче двигаются. Можно вести процент выполнения задач и видеть на каком мы этапе.
Список незакрытых подзадач постепенно становится все меньше и меньше. Параллельно не забываю закрывать задачи в программе учета задач.
В процессе работы я могу столкнуться с ошибкой / проблемой, или необходимостью чего-то дополнительного, что надо сделать в каком-то другому месте. В этом случае я не тороплюсь ее выполнять тут же, а добавляю новым пунктом в свой супер-список и в программу учета задач и возвращаюсь к ней, когда буду готов, или до нее дойдет очередь.
Вместо листочка с бумагой, или планшета, можно использовать чек-лист, или каждый из кружочков сделать отдельной задачей в системе учета задач. На самом деле это тоже не важно. Важен принцип декомпозиции.
Вернемся к нашему листочку. Когда все пункты зачеркнуты, наступает магия. Большая задача выполнена!
Красота!
Зачем мне вообще этот листочек? Ответ прост: мне так нравится. Замечали, за собой, что, когда делаешь большую задачу, часто “остываешь” в процессе? Все потому, что, когда мы добавляем коммиты, мы не видим общую картину и от этого не появляется чувство неудовлетворения.
С декомпозицией задач есть один тонкий момент… На сколько сильно нужно декомпозировать задачи? Если говорить про степень детализации то, на мой взгляд, доходить до абсурда не стоит. Когда тикет на разработку будет содержать больше текста чем реализация этой задачи, наверное, можно делать такие задачи и покрупнее. Но тут все индивидуально. Есть и такие задачи, которые делаешь день, а изменения содержат один символ.
Ни в коем случае не навязываю свой подход к процессу работы над задачами. Это уже личный выбор каждого разработчика. Мне, всегда интересно как это делают другие и что они используют в своей работе. Узнать подробности каких-то конкретных кейсов у опытных разработчиков почти всегда дает буст любому разработчику. Все мы учимся друг у друга.
Итоги
Давайте подведем итоги.
-
Хороший коммит в git должен быть определенного типа и преследовать лишь одну цель.
-
Хороший коммит — это, прежде всего, атомарное изменение. Удаление которого, не должно влиять ни на что другое, не связанное с этим изменением.
-
Создавайте отдельные ветки в git под каждую задачу и делайте такие коммиты, со структурой которых, легко могут разобраться другие члены команды.
-
Свои правила и договоренности по коду в команде — это хорошо. Самое главное, чтобы все члены команды эти договоренности соблюдали.
Коллеги, есть что добавить? Поделитесь пожалуйста своим опытом. Если какой-то еще момент я упустил, просьба: оставьте в комментариях свои наблюдения из вашего опыта какие еще ошибки делают разработчики с коммитами.