Ревью, коммиты, релизы
Зачем это нужно
Код, который работает, — это половина работы. Вторая половина в том, как этот код попал в репозиторий: понятно ли из истории, что и зачем менялось; можно ли по описанию pull request'а за минуту понять, что проверять; понятно ли из версии и changelog, что изменилось между двумя релизами. Эту половину видно снаружи — её читают ревьюер, судья на чемпионате и работодатель, который открыл твой репозиторий в вкладке рядом с резюме.
Глава «Английский на практике» уже дала словарь: типы Conventional Commits, разметку Nit / Blocking, заголовки Keep a Changelog. Здесь словарь не пересказывается — здесь он применяется. Вопрос не «как переводится feat», а «как из твоих коммитов получается номер версии, и почему коммит фикс этот механизм ломает».
Предполагается, что ветки, PR и rebase уже не вызывают вопросов: слова main, merge, force-with-lease, CI дальше используются как знакомые.
- Зачем это нужно
- Коммит — это единица смысла, а не единица времени
- Conventional Commits: формат, который потом работает за тебя
- Pull request: как объяснить свою работу за минуту
- Как ревьюить чужой код
- Как принимать ревью
- Защита main: почему туда нельзя пушить напрямую
- Semver: что означают три числа
- Теги и релизы: зафиксировать состояние
- CHANGELOG.md: журнал для людей
- Типичные ошибки
- На чемпионате и в портфолио
- Полный цикл: одна задача от начала до релиза
- Практика: подготовь релиз
- Чек-лист
- Шпаргалка
- Что должен уметь
- Куда дальше: проверенные ресурсы
Коммит — это единица смысла, а не единица времени
Самая распространённая модель у новичка: коммит — это «сохранение», как Ctrl+S. Поработал час — закоммитил всё, что накопилось, с сообщением «фикс». Модель удобная ровно до первого момента, когда историю нужно прочитать: найти, когда сломалась выгрузка отчёта, откатить одно изменение, не задев остальные, или объяснить ревьюеру, что вообще происходит в diff'е на 400 строк.
Правильная модель другая: коммит — это одно завершённое изменение, которое можно назвать одной фразой. Если для описания коммита нужно слово «и» («поправил парсер и добавил кнопку и обновил README»), это три коммита, которые случайно оказались в одном.
Что даёт атомарный коммит на практике:
- Читаемая история.
git log --onelineстановится оглавлением проекта, а не столбиком слова «фикс». - Точный откат.
git revert <хеш>отменяет ровно одно изменение. Если в коммите было ещё три, revert утащит их с собой. - Работающий
git bisect. Поиск коммита, в котором сломался тест, имеет смысл только когда коммит маленький: bisect укажет на коммит из шести строк, а не на кучу из четырёхсот. - Быстрый review. Ревьюер смотрит diff покоммитно и понимает логику работы, а не разгадывает ребус.
Теперь честно про сообщения. Вот реальный список из студенческого репозитория:
* 7f2a119 фикс
* 3d8c440 работает
* 91ab077 asdf
* c02e5f1 ещё фикс
* a19bd35 final
* 5e7c8a2 final2
Проблема здесь не в эстетике. Через две недели автор сам не ответит, что чинил коммит ещё фикс и почему final2 идёт после final. Через два месяца ревьюер, ищущий, когда в проект попал баг, потратит на это полчаса вместо минуты. А на чемпионате такая история — это прямая потеря баллов в критерии «ведение репозитория»: судья открывает git log, видит шесть слов «фикс» и ставит по критерию ноль, даже если код идеален.
Та же история, переписанная как надо:
* 7f2a119 fix(export): не падать на пустом списке заказов
* 3d8c440 feat(export): кнопка выгрузки отчёта в CSV
* 91ab077 refactor(report): вынести формирование строк в ReportBuilder
* c02e5f1 test(export): проверка заголовка CSV
* a19bd35 docs: как запустить проект локально
* 5e7c8a2 chore: обновить зависимости до актуальных версий
Столько же работы, столько же времени — но эту историю можно читать, искать по ней и делать из неё changelog.
Как разобрать кашу, если она уже случилась
Ситуация, знакомая всем: увлёкся, наработал на три разных изменения, ничего не коммитил. git status показывает девять изменённых файлов. Дальше есть два пути.
Путь простой — по файлам. Если изменения лежат в разных файлах, добавляй в индекс не всё сразу, а порциями:
git add src/export/CsvWriter.kt src/export/ExportService.kt
git commit -m "feat(export): выгрузка отчёта в CSV"
git add README.md
git commit -m "docs: раздел про запуск проекта"
Путь точный — по кускам внутри файла. Если в одном файле оказались правки от двух разных изменений, есть git add -p (от --patch): git показывает diff кусками (hunk) и по каждому спрашивает, добавлять ли его в индекс.
$ git add -p src/report/ReportBuilder.kt
@@ -12,7 +12,7 @@ class ReportBuilder {
- private val rows = mutableListOf<String>()
+ private val rows = mutableListOf<ReportRow>()
(1/3) Stage this hunk [y,n,q,a,d,s,e,?]?
Основные ответы: y — взять этот кусок, n — пропустить, s — разбить кусок на более мелкие, если git взял слишком много, q — выйти. Отобрал куски одного изменения — коммитишь; оставшиеся правки остаются в рабочей директории и уходят в следующий коммит.
git add -p полезен не только для разбора каши. Это ещё и способ перечитать свой diff перед коммитом: git буквально показывает по кускам всё, что ты собираешься отправить. Половина случайно закоммиченных println() и закомментированных блоков ловится именно здесь — до того, как их увидит ревьюер.
Проверить себя перед коммитом можно двумя командами: git diff показывает, что ещё не в индексе, git diff --staged — что именно уйдёт в коммит. Вторую забывают чаще, а именно она отвечает на вопрос «что я сейчас коммичу».
Conventional Commits: формат, который потом работает за тебя
Формат сообщения по Conventional Commits знаком из главы про английский:
тип(область): краткое описание
тело коммита: подробности, если нужны
BREAKING CHANGE: описание сломанной совместимости
Здесь важно не запомнить типы (feat, fix, docs, refactor, test, chore — они уже разобраны), а понять, зачем городить формат. Ответ: потому что дальше по этому формату работают машины и люди.
- Из коммитов с типами автоматически собирается changelog:
featпопадают в разделAdded,fix— вFixed. Существуют готовые инструменты (standard-version,release-please,semantic-release), которые делают это без человека. - Из типов выводится номер версии:
fix→ PATCH,feat→ MINOR,BREAKING CHANGE→ MAJOR. - По области (scope) история фильтруется:
git log --oneline --grep "^feat(export)"покажет всё, что происходило с выгрузкой, за минуту.
То есть формат — это не бюрократия, а способ один раз написать информацию, которая потом переиспользуется трижды.
Разбор живого сообщения
feat(export): выгрузка отчёта в CSV
Кнопка «Скачать CSV» на странице отчётов. Формируем файл на сервере,
чтобы не тянуть весь набор строк в браузер: на 50 000 заказов вкладка
подвисала на несколько секунд.
Разделитель — точка с запятой: Excel в русской локали иначе
не разбивает строку на колонки.
Closes #12
Что тут по частям:
- Заголовок — тип, область, суть в повелительном наклонении и без точки в конце. Ограничение по длине — около 50 символов: длиннее не помещается в
git log --onelineи в интерфейс GitHub. - Пустая строка между заголовком и телом обязательна: без неё git считает весь текст одним заголовком.
- Тело отвечает на вопрос «почему так, а не иначе». Что сделано — видно из diff. А вот почему разделитель — точка с запятой, из diff не видно никогда: через полгода это спасёт того, кто соберётся «починить неправильный разделитель».
- Футер — служебные строки:
Closes #12,BREAKING CHANGE:,Co-authored-by:.
Про scope: это не обязательная часть, а подсказка «в какой части проекта». Хорошие области — по модулям проекта (auth, export, ui, api, db), а не по типам файлов (kotlin, xml). Область должна быть понятна человеку, который смотрит на список коммитов и хочет понять, что где менялось.
BREAKING CHANGE: главный сигнал
BREAKING CHANGE — это заявление: «код, который работал с прошлой версией, может перестать работать». Ставится, если ты убрал или переименовал публичную функцию, изменил формат конфига, поменял значение по умолчанию, убрал поле из API-ответа.
feat(api)!: убрать поле status из ответа /orders
Поле дублировало state и годами расходилось с ним по значениям.
BREAKING CHANGE: клиенты, читающие response.status, должны перейти
на response.state. Значения совпадают, кроме "in_progress" —
раньше оно приходило как "processing".
Восклицательный знак после типа (feat(api)!) — короткая пометка того же самого: сразу видно в git log --oneline, что коммит ломающий. Абзац BREAKING CHANGE: в теле остаётся: он объясняет, что именно сломалось и куда переезжать.
Pull request: как объяснить свою работу за минуту
Pull request — это не «кнопка отправки кода». Это письмо, в котором ты объясняешь другому человеку, что сделал и как ему это проверить. Ревьюер открывает десяток PR за день; тот, что понятен за минуту, будет просмотрен сегодня, а тот, где написано «сделал», — «когда-нибудь потом».
Рабочий шаблон описания — три вопроса:
## Что
Добавил кнопку «Скачать CSV» на странице отчётов и сервис выгрузки.
## Зачем
Задача #12: бухгалтерия выгружала данные копированием из таблицы вручную.
## Как проверить
1. Открыть /reports, выбрать период «Август».
2. Нажать «Скачать CSV».
3. В файле — заголовок и по строке на заказ; разделитель «;», Excel открывает без плясок.
Closes #12
Заголовок PR пишут по тем же правилам, что и заголовок коммита: feat(export): выгрузка отчёта в CSV читается мгновенно, Правки — не читается вообще. Если в репозитории включена кнопка Squash and merge, заголовок PR ещё и становится сообщением итогового коммита в main — тем более стоит написать его нормально.
Скриншоты. Если менялся интерфейс, вставь картинку «до / после» — иначе ревьюер должен собрать ветку локально и запустить, только чтобы посмотреть на кнопку. Одна картинка экономит ему десять минут, а тебе — день ожидания.
Связь с issue. Строка Closes #12 в описании PR (или в сообщении коммита) — не просто ссылка: когда PR вливают в ветку по умолчанию, GitHub автоматически закрывает issue №12. Работают также Fixes #12 и Resolves #12 — все три равнозначны. А вот просто #12 или «см. issue 12» ничего не закрывает: это обычное упоминание со ссылкой.
Ещё одна деталь, которую новички не знают: PR можно открыть черновиком (Draft pull request). Черновик нельзя влить, и он честно говорит команде «работа идёт, но взгляните на подход». Это правильный способ спросить «я вообще туда копаю?» до того, как написано 500 строк не туда.
Опиши, как проверить твою работу, даже если кажется, что «и так очевидно». Ревьюер не знает ни твоего тестового пользователя, ни того, что для воспроизведения нужен период «Август» — с «Июлем» данных нет и кнопка отдаёт пустой файл. Половина комментариев «у меня не работает» — это не баги, а отсутствие инструкции по проверке.
Размер PR: почему 2000 строк не ревьюят
Между размером PR и качеством ревью зависимость обратная и очень резкая. PR на 200 строк ревьюер читает построчно и находит настоящие проблемы. PR на 2000 строк он открывает, видит полосу прокрутки длиной в километр, откладывает «на вечер», через день пролистывает по диагонали и пишет LGTM. Формально ревью было. Фактически — нет.
Почему так:
- Внимание кончается. После сорока минут чтения чужого кода мозг перестаёт замечать детали. Крупные исследования индустрии сходятся на цифре: за один заход человек внимательно читает несколько сотен строк, дальше плотность найденных дефектов резко падает.
- Огромный PR нельзя частично одобрить. В маленьком PR один спорный кусок обсуждается и правится за десять минут. В большом любое замечание тормозит всю пачку изменений целиком.
- Ветка успевает протухнуть. Пока PR на 2000 строк неделю ждёт ревью,
mainуезжает вперёд, и автор получает конфликты вместо одобрения.
Ориентир, к которому стоит стремиться: до 400 строк diff в PR, лучше — до 200. Если получается больше, изменение почти всегда разбивается:
- отдельным PR — рефакторинг и переименования (там сотни строк механических изменений, которые незачем читать вместе с логикой);
- отдельным PR — новая функциональность;
- отдельным PR — обновление зависимостей;
- отдельным PR — форматирование (иначе автоформатер перекрасит полфайла и утопит в шуме два содержательных изменения).
Отдельно про порядок: если большая задача не делится на независимые куски, её делят на цепочку PR, где каждый следующий базируется на предыдущем. Это чуть больше возни с ветками, зато ревью идёт параллельно работе, а не после неё.
Как ревьюить чужой код
Ревью — навык, которому обычно нигде не учат: человека просто однажды назначают ревьюером, и он либо пишет LGTM не глядя, либо придирается к пробелам. И то, и другое — не ревью.
Работает простой порядок из четырёх вопросов, строго сверху вниз. Смысл порядка в том, что не найдя ответ на первый вопрос, бессмысленно спорить про четвёртый.
1. Работает ли? Решает ли код заявленную задачу; что будет на пустом списке, на null, на отрицательном числе, на очень большом вводе. Здесь же — «а есть ли случай, при котором это точно сломается».
2. Понятно ли? Понятно ли имя переменной без комментария; можно ли прочитать функцию сверху вниз и не запутаться; нет ли «магических чисел» вроде if (status == 3). Если тебе как ревьюеру непонятно — это дефект кода, а не твоей внимательности.
3. Безопасно ли? Не уехали ли в репозиторий пароли, токены и ключи; проверяется ли ввод пользователя; не собирается ли SQL-запрос склейкой строк; не пишутся ли в лог персональные данные.
4. Покрыто ли? Есть ли тест на новое поведение и особенно — тест на тот баг, который чинит fix. Тест, который падал до правки и проходит после, — лучшее доказательство, что баг действительно починен.
Что в этот список не входит: расстановка пробелов, длина строк, порядок импортов. Это работа линтера и автоформатера. Если команда спорит о форматировании в комментариях к PR — команде не хватает не ревью, а конфига форматера в репозитории.
Nit и Blocking: серьёзность замечания
Разметка серьёзности — самый дешёвый способ не превратить ревью в ссору. Из главы про английский: Nit (nitpick) — мелкая необязательная придирка, Blocking — то, без чего вливать нельзя. Практика такая: помечай каждый комментарий явно, и автор сразу видит, что обязательно, а что на его усмотрение.
Nit: имя `d` не читается — может, `daysLeft`?
Question: а что здесь произойдёт, если список пустой?
Blocking: токен в открытом виде в коде — он попадёт в историю git
и останется там навсегда. Нужно вынести в переменную окружения.
Три метки покрывают почти всё: Nit — можно проигнорировать, Question — сначала объясни, а решать будем после ответа, Blocking — до слияния обязательно.
Как формулировать, чтобы не получилось по-хамски
Одно и то же техническое содержание можно упаковать так, что автор его исправит, и так, что автор пойдёт защищаться. Разница механическая: говори о коде, а не о человеке, и предлагай, а не приказывай.
| Плохо | Хорошо |
|---|---|
| «Ты что, не знаешь про map?» | «Здесь цикл можно заменить на map — станет на три строки короче. Не принципиально, на твоё усмотрение.» |
| «Это ужасный код» | «Функция на 80 строк тяжело читается. Может, вынести проверку прав в отдельный метод?» |
| «Опять забыл проверку» | «Что будет, если orders придёт пустым? Кажется, здесь упадёт на first().» |
| «Так никто не делает» | «У нас в проекте похожее место сделано через ReportBuilder — может, переиспользовать его для единообразия?» |
| «Перепиши всё» | «Blocking: логика оплаты смешана с UI, из-за этого её нельзя протестировать. Давай обсудим, как разделить — могу накидать вариант.» |
Три приёма, которые делают почти всю работу:
- Вопрос вместо приговора. «Что будет, если…» вынуждает подумать; «здесь баг» вынуждает оправдываться. При этом вопрос честный: автор может ответить, что случай невозможен, — и он будет прав.
- Явная необязательность. Фраза «на твоё усмотрение» превращает придирку в предложение и снимает половину напряжения.
- Похвала — тоже комментарий. «О, вот это решение с кэшем — красиво» стоит две секунды и полностью меняет тон обсуждения. Ревью, в котором есть только замечания, читается как обвинительное заключение, даже если каждый пункт по делу.
И общее правило вежливости: если обсуждение свелось к трём кругам ответов в комментариях, оно закончилось. Дальше — пять минут голосом или в чате, а в PR остаётся резюме: «обсудили, договорились делать так-то».
Как принимать ревью
Первая реакция на десять комментариев к своему PR у всех одинаковая: «меня разносят». Это не так — но ощущение реальное, и с ним нужно уметь работать, потому что от него зависит скорость, с которой ты растёшь как инженер.
Единственное правило, которое всё решает: не спорь с фактами, спорь с выводами.
- Ревьюер пишет: «здесь упадёт на пустом списке». Это факт — проверяется за десять секунд. Спорить не о чем: либо падает, либо нет.
- Ревьюер пишет: «поэтому нужно переписать на Stream API». Это вывод — и обсуждать его можно и нужно: возможно, достаточно одной проверки на пустоту.
Из этого правила выводится всё остальное:
Задавай уточняющие вопросы, а не оправдывайся. «Не понял замечание — можешь показать, какой ввод сюда приведёт?» — нормальный ответ. «Ну у меня же работало» — не ответ.
Отвечай на каждый комментарий. Даже коротко: «поправил», «согласен, но сделаю иначе — вот почему», «Nit, оставлю как есть». Ревьюер видит, что его прочитали, и не проверяет то же самое второй раз.
Не принимай на свой счёт. Ревьюят код, а не тебя. Три комментария в PR — это не «я плохой разработчик», а «этот код станет лучше за пятнадцать минут».
Скажи спасибо за пойманный баг. Ревьюер потратил своё время и нашёл проблему до пользователей — это буквально подарок.
«Сделаю в отдельном PR» — правильный ответ
Отдельный приём, который отличает опытного человека от новичка. В ревью часто прилетает замечание вида «а ещё вот этот старый класс рядом плохо написан, давай заодно поправим». Оно может быть абсолютно справедливым — и всё равно не должно попадать в текущий PR: иначе PR на 150 строк превращается в PR на 900, и всё, что написано выше про размер, случается сразу.
Правильный ответ:
Согласен, там действительно каша. Но это не связано с текущей задачей
и раздует PR втрое — завёл issue #47, сделаю следующим PR.
Ключевое здесь — завёл issue. «Потом сделаю» без задачи в трекере — это вежливая форма «никогда». Со ссылкой на issue отказ перестаёт быть отказом и становится планом.
Когда так отвечать нельзя: если замечание про безопасность (утёкший ключ, отсутствие проверки ввода) или про то, что твой код прямо сейчас ломает поведение. Такие правки в «следующий PR» не откладывают.
Обратная сторона того же навыка: если ревьюер прав, а тебе просто лень — это не «отдельный PR», это долг, который вернётся. Честная формулировка отличается от отговорки наличием issue и сроком.
Полезная привычка: перед тем как отправлять PR на ревью, прочитай собственный diff на вкладке «Files changed» как чужой. Отладочные println, закомментированные куски, случайно закоммиченный файл конфигурации IDE — всё это ловится за две минуты и экономит ревьюеру целый круг комментариев. Половина замечаний, которые ты получаешь, — те, что нашёл бы сам.
Защита main: почему туда нельзя пушить напрямую
В любом живом проекте прямой push в main рано или поздно отключают. Причина не в недоверии, а в статистике: в main попадает то, что никто не прочитал и что не прогнало тесты — а именно main разворачивается на боевом сервере и именно от него все начинают свои ветки. Один сломанный коммит в main блокирует всю команду.
Правило защищённой ветки (branch protection на GitHub, protected branch в других сервисах) включает обычно три вещи:
- Запрет прямого push. Изменения попадают в
mainтолько через pull request. Попытка запушить напрямую отбивается сервером:
! [remote rejected] main -> main (protected branch hook declined)
error: failed to push some refs to 'github.com:team/project.git'
- Required status checks — обязательные проверки. Кнопка Merge не нажимается, пока CI не отчитался зелёным: тесты прошли, линтер молчит, сборка собралась. Это и есть «зелёный CI как условие мержа»: не пожелание, а техническое ограничение.
- Required reviews — обязательное одобрение. Как минимум один approve от другого человека. Часто плюс запрет одобрять собственный PR.
Дополнительно обычно включают «ветка должна быть актуальна относительно main» (require branches to be up to date): перед слиянием ветку нужно подтянуть под свежий main, чтобы проверки прогонялись на том коде, который реально получится после мержа. Именно здесь пригождается git rebase main или git merge main из главы про rebase.
Почему это важно понимать, а не просто терпеть: правило защиты объясняет странности, с которыми ты столкнёшься. «Почему кнопка Merge серая» — упал один required check. «Почему мой push отбило» — ветка защищена. «Почему просят подтянуть main» — включено требование актуальности. Ни одно из этих сообщений не является ошибкой в твоей работе.
Semver: что означают три числа
Semantic Versioning (semver) — соглашение о том, что номер версии несёт смысл, а не просто растёт. Формат: MAJOR.MINOR.PATCH, например 2.3.1.
| Часть | Когда поднимают | Что это обещает тем, кто пользуется |
|---|---|---|
| MAJOR (2.x.x) | сломана обратная совместимость | «обновление может потребовать правок в вашем коде» |
| MINOR (x.3.x) | добавлена функциональность, старое работает как раньше | «можно обновляться спокойно, появилось новое» |
| PATCH (x.x.1) | исправлен баг, интерфейс не менялся | «обновляйтесь не глядя, стало только лучше» |
Правило переноса: при подъёме старшей части младшие обнуляются. После 2.3.1 идёт 2.3.2 (fix), 2.4.0 (feat), 3.0.0 (breaking change) — не 3.3.1.
Прямая связка с типами коммитов, о которой шла речь выше:
fix(auth): не падать на пустом токене → 1.4.2 → 1.4.3
feat(export): выгрузка в CSV → 1.4.3 → 1.5.0
feat(api)!: убрать поле status из /orders → 1.5.0 → 2.0.0
Что такое 0.x. Версии, начинающиеся с нуля (0.3.1), означают «проект в разработке, ничего не обещаю»: по стандарту в ветке 0.x ломающие изменения разрешены в любой момент, и обычно их выпускают подъёмом MINOR (0.3.0 → 0.4.0). Первый релиз, который считается стабильным и на который можно опираться, — 1.0.0. Учебные и хакатонные проекты почти всегда живут в 0.x, и это нормально: 1.0.0 — это обещание поддерживать интерфейс, а не отметка «доделали».
Semver придуман ради проблемы, которую называют dependency hell: когда библиотека A требует библиотеку C версии «какой-нибудь», а библиотека B — «другой какой-нибудь», и без правил разрешить это невозможно. Именно поэтому в файлах зависимостей встречаются записи вроде ^1.4.2 — «любая версия до 2.0.0»: значок опирается ровно на обещание semver, что внутри одной MAJOR ничего не сломается.
Теги и релизы: зафиксировать состояние
Номер версии сам по себе — просто строка. Чтобы к версии можно было вернуться, состояние репозитория помечают тегом.
Главная ловушка: теги не уезжают на сервер сами. Обычный git push отправляет коммиты ветки, но не теги — их нужно отправить явно:
git tag -a v1.5.0 -m "Выгрузка отчётов в CSV" # поставить тег на текущий коммит
git push origin v1.5.0 # отправить один тег
git push --tags # отправить все локальные теги сразу
git tag # список тегов
git show v1.5.0 # что за коммит и что за сообщение
Тег ставится на конкретный коммит и больше не двигается — в этом весь смысл. Ветка main уезжает вперёд каждый день, а v1.5.0 навсегда указывает на то состояние кода, которое было выпущено. git checkout v1.5.0 даёт ровно тот код, который получили пользователи.
GitHub Releases — надстройка над тегом. Релиз на GitHub — это тег плюс страница: заголовок, release notes (описание изменений для людей) и, при желании, прикреплённые файлы сборки (.apk, .jar, .zip). Создать релиз можно из интерфейса (Releases → Draft a new release, выбрать существующий тег или создать новый) — а release notes GitHub предложит сгенерировать сам из заголовков влитых PR. Это ещё одна причина писать заголовки PR по-человечески: они буквально становятся текстом релиза.
Что писать в release notes: то же, что в changelog за эту версию, только человеческим языком и без внутренних подробностей. Пользователю нужны «что нового», «что починили», «что сломается при обновлении» — а не список хешей.
CHANGELOG.md: журнал для людей
Changelog — файл в корне репозитория, где записано, что изменилось в каждой версии. Формально его можно заменить командой git log, и именно это возражение звучит чаще всего. Ответ стандарта Keep a Changelog: git log — это журнал для машин и разработчиков этого проекта, а changelog — для людей, которые проектом пользуются. В логе триста коммитов, включая chore: обновить зависимости; в changelog — десять строк о том, что реально изменилось для пользователя.
Формат:
# Changelog
## [Unreleased]
### Added
- Экспорт отчёта в формате XLSX.
## [1.5.0] - 2026-09-04
### Added
- Кнопка «Скачать CSV» на странице отчётов.
- Фильтр отчётов по периоду.
### Changed
- Отчёт формируется на сервере: страница больше не подвисает на больших объёмах.
### Fixed
- Пустой список заказов больше не приводит к падению при выгрузке.
## [1.4.3] - 2026-08-21
### Fixed
- Вход с пустым токеном возвращает понятную ошибку вместо 500.
Шесть разделов стандарта: Added (появилось), Changed (изменилось в существующем поведении), Deprecated (ещё работает, но скоро исчезнет), Removed (удалено), Fixed (починено), Security (исправления безопасности). Пустые разделы не пишут.
Правила, которые чаще всего нарушают:
- Новые версии сверху. Читающий почти всегда хочет узнать про последнее.
- Дата в формате
ГГГГ-ММ-ДД. Однозначно читается в любой стране — в отличие от04.09.2026. - Раздел
[Unreleased]наверху. Сюда пишут по ходу работы; в момент релиза он переименовывается в номер версии с датой, а сверху заводится новый пустой[Unreleased]. - Пишет человек, а не скрипт. Автоматически собранный из коммитов список — это тот же
git log, только в другом файле. Смысл changelog в переводе с языка коммитов на язык пользователя.
Кто и когда его пишет. Самый рабочий вариант: строку в [Unreleased] добавляет автор PR — в том же PR, где изменение. Это одна строка в момент, когда всё свежо в голове. Альтернатива — вспоминать перед релизом, что вообще произошло за месяц: получается хуже и всегда с пропусками. В некоторых командах наличие записи в changelog для feat и fix — прямое требование ревью.
Типичные ошибки
Собрано из того, что встречается в студенческих репозиториях чаще всего.
Один коммит на всю задачу в конце дня. Внутри пять несвязанных изменений, сообщение «сделал задание». Такой коммит нельзя ни отревьюить, ни откатить частично. Лечится привычкой коммитить по завершении каждого осмысленного куска, а не по расписанию.
Коммит-мусор в истории. fix, fix2, фикс фикса, тест, убрал лишнее. Если каша уже случилась, но ветка ещё не отправлена, её убирают интерактивным rebase со словом fixup — ровно тот сценарий из главы про rebase.
PR без описания. Заголовок Правки, тело пустое. Ревьюер вынужден реконструировать замысел по diff — а это в разы дольше, чем прочитать три строки.
PR, в который вошло «заодно». Задача была про кнопку экспорта, а в diff ещё переименование класса, обновление зависимостей и переформатирование трёх файлов. Три отдельных PR ревьюются за десять минут; один общий — за час и с худшим результатом.
Ответ на ревью в стиле «у меня работает». Ревьюер сообщил факт (падает на пустом списке) — факт проверяется, а не оспаривается. Спорить можно с предложенным решением, но не с воспроизводимым поведением.
Молчание в ответ на комментарии. Автор молча всё поправил и запушил. Ревьюер не знает, что учтено, а что проигнорировано, и перечитывает весь diff заново. Одна строка «поправил» под каждым комментарием экономит ему полчаса.
Секреты, замеченные только на ревью. Токен или пароль в коде — это не «поправим в следующем коммите»: он уже в истории git и остаётся там навсегда, даже если удалить его следующим коммитом. Ключ придётся отзывать и выпускать заново. Именно поэтому «безопасно ли» стоит третьим пунктом ревью, а не последним.
Тег поставлен, но не отправлен. Локально v1.0.0 есть, на GitHub релиза нет. Классика — git push --tags забывают все и по многу раз.
Версия поднята «на глаз». 1.0.0 → 2.0.0, потому что «много всего сделали», хотя ничего не сломано. Номер версии — это обещание, а не показатель объёма работы: MAJOR означает «у вас может сломаться», и ни что иное.
Changelog, написанный за десять минут до сдачи. Получается пересказ git log с формулировками вроде «рефакторинг ReportBuilder» — пользователю это ничего не говорит. Пиши по одной строке в [Unreleased] в момент изменения.
На чемпионате и в портфолио
На чемпионате ведение репозитория — обычно отдельный критерий с реальными баллами, и проверяется он механически: судья открывает git log, историю PR и смотрит теги. Что даёт баллы:
- Осмысленные коммиты по ходу работы. История из шести-десяти коммитов с типами
feat/fix/docsпоказывает процесс. История из одного коммита «готово» в последние пять минут — не показывает ничего, даже если код отличный. - Соблюдение формата, если он задан в условии. Если в задании написано «коммиты по Conventional Commits» — это проверяемое требование, а не пожелание. Нарушение снимает баллы автоматически, независимо от качества кода.
- Работающая ссылка issue → PR → коммит. В командных заданиях по этой цепочке судья понимает, кто что делал.
Closes #12в описании PR — самый дешёвый балл на всём чемпионате. - Тег на финальном состоянии.
v1.0.0на сданном коммите и заполненныйCHANGELOG.md— признак того, что участник понимает, что такое релиз, а не только что такое «код компилируется».
Отдельно про тайминг: наводить порядок в истории в последние десять минут — плохая стратегия. Разумнее коммитить нормально сразу: это не медленнее (сообщение пишется десять секунд), а в конце остаётся время на проверку, а не на разбор каши через rebase -i под таймер.
В портфолио твой репозиторий читают в конкретном порядке: README → список коммитов → один-два открытых PR. Что видит работодатель:
- История коммитов — это единственное свидетельство того, как ты работаешь. Код показывает результат, история — процесс: умеешь ли разбивать задачу, доводишь ли до конца, возвращаешься ли чинить.
- Аккуратный PR с описанием и скриншотами читается как «человек работал в команде или как минимум понимает, как это устроено» — даже если PR ты сам себе и одобрил в учебном проекте.
- Теги и
CHANGELOG.mdв pet-проекте — редкость, и именно поэтому они бросаются в глаза. Это сигнал «человек доводит проект до состояния релиза», а не «набор файлов, который однажды запускался».
Отдельно замечу неочевидное: пустой репозиторий с одним идеальным коммитом выглядит хуже, чем репозиторий с двадцатью честными коммитами и парой откатов. Первое читается как «залил чужой код», второе — как настоящая работа.
Полный цикл: одна задача от начала до релиза
Соберём главу в один сценарий — так это выглядит в обычной неделе.
- Взял issue #12 «Выгрузка отчёта в CSV». Создал ветку
feature/csv-exportот свежегоmain. - Работал коммитами по смыслу:
refactor(report): вынести формирование строк в ReportBuilder, затемfeat(export): выгрузка отчёта в CSV, затемtest(export): проверка заголовка CSV. Один файл содержал правки от двух изменений — разложил черезgit add -p. - Добавил строку в
CHANGELOG.mdв раздел[Unreleased]→### Added: «Кнопка „Скачать CSV“ на странице отчётов». - Открыл PR: заголовок
feat(export): выгрузка отчёта в CSV, в описании — что / зачем / как проверить, скриншот кнопки, строкаCloses #12. Diff — 186 строк. - CI прогнал тесты и линтер: зелено. Кнопка Merge разблокировалась только после этого —
mainзащищён. - Ревьюер оставил три комментария:
Nitпро имя переменной,Questionпро пустой список,Blockingпро разделитель, зашитый прямо в коде. Первое поправил, на второе ответил (случай возможен, добавил проверку и тест), третье вынес в настройку. На четвёртое замечание — «заодно перепиши старый ReportPrinter» — ответил «завёл issue #47, сделаю следующим PR». - После approve — Squash and merge. Issue #12 закрылось само.
- Через неделю набралось достаточно на релиз: раздел
[Unreleased]в changelog переименовал в## [1.5.0] - 2026-09-04, завёл новый пустой[Unreleased]. Версия выросла с1.4.3до1.5.0— былиfeat, но не былоBREAKING CHANGE. - Поставил и отправил тег:
git tag -a v1.5.0 -m "Выгрузка отчётов в CSV",git push origin v1.5.0. На GitHub создал Release с release notes из changelog.
Ни на одном шаге не было ничего сложного — но именно эта последовательность отличает «код на компьютере» от «проект, которым можно пользоваться».
Практика: подготовь релиз
Ветку влили в main, версия готова к выпуску — осталось оформить. В учебном
репозитории уже лежит CHANGELOG.md с незакрытым разделом [Unreleased] и
файл VERSION со старым номером. Подготовь релиз: заведи каталог release/
с заметками release/v1.5.0.md и убери временный файл notes.tmp, который
случайно попал в репозиторий.
- ○ release/v1.5.0.md
- ○ notes.tmp — не должно существовать
Как это устроено под капотом
Прочитать раздел [Unreleased] поможет cat CHANGELOG.md, а перенести строку в заметки — echo "- вход по одноразовому коду" >> release/v1.5.0.md. Каталога release ещё нет: mkdir release.
Дальше — руки. Команды релиза набираются редко, а ошибиться в них дорого:
git tag без -a создаёт лёгкий тег без автора и даты, а --force-with-lease
отличается от --force тем, что не затрёт чужую работу.
Цель: точность ≥ 90%
git add -p src/auth.js
Чек-лист
Перед коммитом:
- Изменение одно и называется одной фразой без «и».
- Посмотрел
git diff --staged— там нет отладочныхprintln, закомментированных кусков и лишних файлов. - Сообщение по формату:
тип(область): суть, до ~50 символов, без точки в конце. - Если решение неочевидное — в теле коммита написано «почему».
- Если сломана совместимость — есть
BREAKING CHANGE:.
Перед открытием PR:
- Ветка подтянута под свежий
main, конфликтов нет. - Diff меньше ~400 строк; если больше — разбил на несколько PR.
- Заголовок PR читается как заголовок коммита.
- В описании есть что / зачем / как проверить.
- Есть
Closes #N, если PR закрывает issue. - Есть скриншот, если менялся интерфейс.
- Строка добавлена в
CHANGELOG.mdв раздел[Unreleased]. - Прочитал собственный diff как чужой.
Когда ревьюишь:
- Прошёл по порядку: работает → понятно → безопасно → покрыто тестами.
- Каждый комментарий помечен:
Nit/Question/Blocking. - Формулировки — про код, а не про человека; есть хотя бы одно предложение вместо приказа.
- Отметил то, что сделано хорошо.
Когда принимаешь ревью:
- Ответил на каждый комментарий, даже одним словом.
- Не спорил с фактами; спорил (если было о чём) с выводами.
- Всё, что отложено, оформлено issue, а не обещанием.
Перед релизом:
- Номер версии выбран по semver, а не «на глаз».
-
[Unreleased]в changelog переименован в версию с датой в форматеГГГГ-ММ-ДД. - Тег поставлен:
git tag -a vX.Y.Z -m "...". - Тег отправлен:
git push origin vX.Y.Z. - Release notes написаны для людей, а не скопированы из
git log.
Шпаргалка
| Команда / правило | Что делает |
|---|---|
git add -p <файл> | добавить в индекс отдельные куски diff — разложить кашу по коммитам |
git diff --staged | посмотреть ровно то, что уйдёт в коммит |
тип(область): суть | формат заголовка: feat, fix, docs, refactor, test, chore |
BREAKING CHANGE: в футере | сломана обратная совместимость → поднимается MAJOR |
Closes #12 в PR или коммите | issue закроется автоматически при вливании в основную ветку |
| Размер PR | цель — до ~200 строк, потолок — ~400; больше разбивают |
Nit / Question / Blocking | необязательно / нужен ответ / без этого не вливаем |
| Порядок ревью | работает → понятно → безопасно → покрыто тестами |
| Ответ на ревью | не спорить с фактами, спорить с выводами; отложенное — в issue |
Защита main | прямой push запрещён, обязательны зелёный CI и approve |
MAJOR.MINOR.PATCH | сломано / добавлено / починено; при подъёме старшего младшие обнуляются |
0.x.y | ранняя разработка, ломать можно; стабильность обещает 1.0.0 |
git tag -a v1.5.0 -m "..." | аннотированный тег на текущий коммит |
git push origin v1.5.0 | отправить тег — обычный push теги не отправляет |
CHANGELOG.md | Added / Changed / Deprecated / Removed / Fixed / Security, новое сверху |
Углубиться
Автоматизация релизов. Инструменты вроде semantic-release, standard-version и release-please читают историю коммитов, сами вычисляют следующий номер версии по типам, генерируют черновик changelog, ставят тег и создают GitHub Release. Работают они ровно потому, что коммиты написаны по формату: без Conventional Commits вся эта автоматика невозможна в принципе. Это и есть практический ответ на вопрос «зачем формат».
Conventional Comments. Существует такое же соглашение для комментариев ревью: praise:, nitpick:, suggestion:, issue:, question:, thought:. Метки Nit и Blocking из этой главы — упрощённая, самая распространённая версия той же идеи.
Trunk-based development против long-lived branches. Два подхода к тому, как долго живут ветки. Trunk-based: очень короткие ветки, вливаемые в main по нескольку раз в день, недоделанное прячется за фича-флагами. Противоположный подход: длинные ветки на недели с большим слиянием в конце. Первый выигрывает как раз за счёт размера PR — вливать по 100 строк каждый день безопаснее, чем 3000 строк раз в месяц.
CODEOWNERS. Файл в репозитории, в котором указано, кто отвечает за какие директории. GitHub автоматически назначает этих людей ревьюерами PR, затрагивающего их файлы. В больших проектах это единственный способ не искать ревьюера вручную.
Почему секрет нельзя «удалить следующим коммитом». Коммит хранит состояние целиком, а история неизменяема: удалив ключ в новом коммите, ты оставляешь его в старом, доступном по хешу навсегда. Единственный правильный ответ на утёкший ключ — отозвать и выпустить новый; чистка истории (git filter-repo, BFG) помогает только в паре с отзывом, а не вместо.
Дальше по теме:
- Conventional Commits 1.0.0 — сам стандарт со всеми правилами формата
- git-tag — официальная документация — аннотированные и лёгкие теги, подпись, удаление
Сводная проверка по всей главе: 12 вопросов, 5:00 на всё, без подсказок по ходу. Вопросы показываются по одному, ответ изменить нельзя. Если время выйдет — экзамен завершится с тем, что успел ответить. Пересдавать можно сколько угодно раз.
Что должен уметь
- Разбивать работу на атомарные коммиты и раскладывать смешанные правки через
git add -p. - Писать сообщения по Conventional Commits: заголовок с типом и областью, тело с ответом «почему», футер с
BREAKING CHANGEиCloses #N. - Оформлять PR так, чтобы его можно было отревьюить за минуту: заголовок, что / зачем / как проверить, скриншот, ссылка на issue.
- Держать размер PR в разумных пределах и объяснять, почему это влияет на качество ревью.
- Ревьюить чужой код по порядку «работает → понятно → безопасно → покрыто» и помечать серьёзность замечаний.
- Формулировать замечания про код, а не про человека, и отвечать на чужие без спора с фактами.
- Объяснять, что даёт защита
mainи почему кнопка Merge может быть заблокирована. - Выбирать номер версии по semver и понимать, что означает
0.x. - Ставить и отправлять аннотированные теги, оформлять GitHub Release.
- Вести
CHANGELOG.mdпо Keep a Changelog: раздел[Unreleased]по ходу работы, переименование в версию перед релизом.
Куда дальше: проверенные ресурсы
-
Conventional Commits 1.0.0 — первоисточник формата коммитов: типы, scope, футеры,
BREAKING CHANGE. -
Semantic Versioning 2.0.0 — правила номера версии по-русски, включая раздел про
0.xи предрелизные версии. -
Keep a Changelog — как вести журнал изменений: разделы, порядок,
[Unreleased], частые ошибки. -
GitHub Docs: создание pull request — черновики PR, описание, связывание с issue.
-
GitHub Docs: о ревью pull request — комментарии, approve, request changes.
-
GitHub Docs: управление релизами — теги, release notes, прикреплённые файлы сборки.
-
GitHub Docs: о защищённых ветках — required checks, обязательные ревью, запрет прямого push.
-
Видео и материалы сообщества — ниже — ролики по теме и ссылки от студентов и преподавателей смотри в самом низу страницы.
Формат Conventional Commits вырос из соглашения об истории коммитов проекта Angular — команде нужно было генерировать changelog автоматически, и для этого пришлось договориться о словах в заголовке. Сегодня по этому формату живут тысячи репозиториев, а инструменты вроде semantic-release вычисляют по нему номер версии без единого человеческого решения.
Комментарии
Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.