Перейти к основному содержимому
Отдельные темы06ГЛАВА 06Ревью, коммиты, релизыv1.2

Ревью, коммиты, релизы

Прочитано0%
Квизы0/6
Тренажёрыне пройден0/2
Уровень 1 · Новичок

Зачем это нужно​

Код, который работает, — это половина работы. Вторая половина в том, как этот код попал в репозиторий: понятно ли из истории, что и зачем менялось; можно ли по описанию pull request'а за минуту понять, что проверять; понятно ли из версии и changelog, что изменилось между двумя релизами. Эту половину видно снаружи — её читают ревьюер, судья на чемпионате и работодатель, который открыл твой репозиторий в вкладке рядом с резюме.

Глава «Английский на практике» уже дала словарь: типы Conventional Commits, разметку Nit / Blocking, заголовки Keep a Changelog. Здесь словарь не пересказывается — здесь он применяется. Вопрос не «как переводится feat», а «как из твоих коммитов получается номер версии, и почему коммит фикс этот механизм ломает».

Предполагается, что ветки, PR и rebase уже не вызывают вопросов: слова main, merge, force-with-lease, CI дальше используются как знакомые.

Коммит — это единица смысла, а не единица времени​

Самая распространённая модель у новичка: коммит — это «сохранение», как 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.

КОММИТ · ОДНА ЕДИНИЦА СМЫСЛАбыло: одна кучафикспарсер + вёрстка + README+ переименование класса412 строк в 9 файлахgit add -pстало: три коммитаfix(parser): пустая строкаfeat(ui): кнопка экспортаdocs: раздел про запусккаждый можно отдельно понять, отменить и попасть с ним в changeloggit add -p раскладывает кучу правок по осмысленным коммитам: один коммит — одно изменение
Одна куча правок против трёх осмысленных коммитов: git add -p раскладывает изменения по смыслу, а не по времени.

Как разобрать кашу, если она уже случилась​

Ситуация, знакомая всем: увлёкся, наработал на три разных изменения, ничего не коммитил. 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: в теле остаётся: он объясняет, что именно сломалось и куда переезжать.

Отвечено вопросов: 0 из 4
В одном файле оказались правки от двух разных изменений, а закоммитить их нужно по отдельности. Что поможет?
Зачем в теле коммита писать «почему», если «что» и так видно в diff?
Ты переименовал публичный метод, которым пользуется другой модуль. Как это отражается в коммите?
Почему заголовок коммита держат в пределах примерно 50 символов?
Отвечено верно: 0 из 4

Pull request: как объяснить свою работу за минуту​

Pull request — это не «кнопка отправки кода». Это письмо, в котором ты объясняешь другому человеку, что сделал и как ему это проверить. Ревьюер открывает десяток PR за день; тот, что понятен за минуту, будет просмотрен сегодня, а тот, где написано «сделал», — «когда-нибудь потом».

Рабочий шаблон описания — три вопроса:

## Что
Добавил кнопку «Скачать CSV» на странице отчётов и сервис выгрузки.

## Зачем
Задача #12: бухгалтерия выгружала данные копированием из таблицы вручную.

## Как проверить
1. Открыть /reports, выбрать период «Август».
2. Нажать «Скачать CSV».
3. В файле — заголовок и по строке на заказ; разделитель «;», Excel открывает без плясок.

Closes #12

Заголовок PR пишут по тем же правилам, что и заголовок коммита: feat(export): выгрузка отчёта в CSV читается мгновенно, Правки — не читается вообще. Если в репозитории включена кнопка Squash and merge, заголовок PR ещё и становится сообщением итогового коммита в main — тем более стоит написать его нормально.

PULL REQUEST · ЧТО ЧИТАЕТ РЕВЬЮЕРfeat(export): выгрузка отчёта в CSV+186 −24Чтодобавил кнопку «Скачать CSV» и сервис выгрузкиЗачемпо задаче #12: бухгалтерия просила выгрузку без ручного копированияКак проверить1) открыть /reports 2) нажать «Скачать CSV» 3) в файле 3 строки и заголовокCloses #12issue закроется самскриншот, если менялся интерфейс
Хороший PR отвечает на три вопроса — что, зачем, как проверить — и связывает себя с issue строкой Closes #12.

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

Связь с 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, где каждый следующий базируется на предыдущем. Это чуть больше возни с ветками, зато ревью идёт параллельно работе, а не после неё.

Отвечено вопросов: 0 из 4
Какая строка в описании PR закроет issue №12 автоматически при вливании?
Что обязательно должно быть в описании PR, кроме «что сделано»?
Задача большая и честно не делится на независимые куски. Как быть с размером PR?
Зачем нужен черновик PR (Draft pull request)?
Отвечено верно: 0 из 4

Как ревьюить чужой код​

Ревью — навык, которому обычно нигде не учат: человека просто однажды назначают ревьюером, и он либо пишет 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 остаётся резюме: «обсудили, договорились делать так-то».

Отвечено вопросов: 0 из 4
В каком порядке разумно проверять чужой PR?
Ты нашёл в PR мелкую придирку к имени переменной, которая не мешает вливать. Как её пометить?
Какая формулировка замечания сработает лучше?
Почему PR на 2000 строк получает менее качественное ревью, чем PR на 200?
Отвечено верно: 0 из 4

Как принимать ревью​

Первая реакция на десять комментариев к своему 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 · ДВЕРЬ ОДНАgit push main! [remote rejected]protected branch hook declinedветка → PRrequired checkstests · lint · buildapproveревьюерmainпрямой push отбивает сервер; единственный путь в main — PR с зелёным CI и одобрением
Прямой push в защищённый main отбивает сервер: единственный путь — pull request с зелёными обязательными проверками и одобрением ревьюера.

Дополнительно обычно включают «ветка должна быть актуальна относительно 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 — прямое требование ревью.

ОТ КОММИТА ДО РЕЛИЗАкоммитыfeat(export): CSVAddedfix(auth): пустой токенFixedBREAKING CHANGEChangedCHANGELOG.mdверсия и тег2.0.0MAJOR подняли из-за BREAKINGgit tag -a v2.0.0GitHub Releaseслова из коммитов становятся разделами changelog, changelog — номером версии, версия — тегом и релизом
Цепочка целиком: типы коммитов дают разделы changelog, changelog — номер версии по semver, версия — тег и GitHub Release.
Отвечено вопросов: 0 из 5
Текущая версия 1.5.0. В релиз вошли только исправления багов, интерфейс не менялся. Какой номер у следующей версии?
Ты поставил тег git tag -a v1.5.0 и сделал обычный git push. На GitHub тега нет. Почему?
Что означает версия 0.4.2?
Когда удобнее всего писать запись в CHANGELOG.md?
Зачем нужен CHANGELOG.md, если есть git log?
Отвечено верно: 0 из 5

Типичные ошибки​

Собрано из того, что встречается в студенческих репозиториях чаще всего.

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

Коммит-мусор в истории. 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-проекте — редкость, и именно поэтому они бросаются в глаза. Это сигнал «человек доводит проект до состояния релиза», а не «набор файлов, который однажды запускался».

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

Полный цикл: одна задача от начала до релиза​

Соберём главу в один сценарий — так это выглядит в обычной неделе.

  1. Взял issue #12 «Выгрузка отчёта в CSV». Создал ветку feature/csv-export от свежего main.
  2. Работал коммитами по смыслу: refactor(report): вынести формирование строк в ReportBuilder, затем feat(export): выгрузка отчёта в CSV, затем test(export): проверка заголовка CSV. Один файл содержал правки от двух изменений — разложил через git add -p.
  3. Добавил строку в CHANGELOG.md в раздел [Unreleased] → ### Added: «Кнопка „Скачать CSV“ на странице отчётов».
  4. Открыл PR: заголовок feat(export): выгрузка отчёта в CSV, в описании — что / зачем / как проверить, скриншот кнопки, строка Closes #12. Diff — 186 строк.
  5. CI прогнал тесты и линтер: зелено. Кнопка Merge разблокировалась только после этого — main защищён.
  6. Ревьюер оставил три комментария: Nit про имя переменной, Question про пустой список, Blocking про разделитель, зашитый прямо в коде. Первое поправил, на второе ответил (случай возможен, добавил проверку и тест), третье вынес в настройку. На четвёртое замечание — «заодно перепиши старый ReportPrinter» — ответил «завёл issue #47, сделаю следующим PR».
  7. После approve — Squash and merge. Issue #12 закрылось само.
  8. Через неделю набралось достаточно на релиз: раздел [Unreleased] в changelog переименовал в ## [1.5.0] - 2026-09-04, завёл новый пустой [Unreleased]. Версия выросла с 1.4.3 до 1.5.0 — были feat, но не было BREAKING CHANGE.
  9. Поставил и отправил тег: git tag -a v1.5.0 -m "Выгрузка отчётов в CSV", git push origin v1.5.0. На GitHub создал Release с release notes из changelog.

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

Отвечено вопросов: 0 из 3
В ревью прилетело справедливое замечание про соседний старый класс, не связанный с твоей задачей. Как ответить правильно?
Какое замечание нельзя откладывать в «следующий PR»?
Почему прямой push в main отключают в живых проектах?
Отвечено верно: 0 из 3

Практика: подготовь релиз​

Ветку влили в main, версия готова к выпуску — осталось оформить. В учебном репозитории уже лежит CHANGELOG.md с незакрытым разделом [Unreleased] и файл VERSION со старым номером. Подготовь релиз: заведи каталог release/ с заметками release/v1.5.0.md и убери временный файл notes.tmp, который случайно попал в репозиторий.

Задание: Создай release/v1.5.0.md с заметками и удали notes.tmp
  • ○ release/v1.5.0.md
  • ○ notes.tmp — не должно существовать
Учебный терминал. Введите help, чтобы увидеть список команд.
student@pgk:~$
Как это устроено под капотом
Никакого настоящего Linux здесь нет: файловая система — обычный JS-объект в памяти браузера, где папка — это вложенный объект, а файл — строка с содержимым. Команды ls, cd, mkdir, rm и остальные — функции, которые читают и меняют этот объект, а cp -r — просто глубокое копирование куска JSON. После каждой команды проверка квеста проходит по требуемым путям и смотрит, существуют ли они в этом дереве. Перезагрузишь страницу — «диск» создастся заново с нуля, так что ломать тут можно безнаказанно.
Совет

Прочитать раздел [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.mdAdded / 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) помогает только в паре с отзывом, а не вместо.

Дальше по теме:

Сводная проверка по всей главе: 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 вырос из соглашения об истории коммитов проекта Angular — команде нужно было генерировать changelog автоматически, и для этого пришлось договориться о словах в заголовке. Сегодня по этому формату живут тысячи репозиториев, а инструменты вроде semantic-release вычисляют по нему номер версии без единого человеческого решения.

Видео по теме

ВидеоP3. Ревью кода / Pull Request и Merge Request | ULearn GitФИИТ УрФУ
ВидеоЧто такое Pull Request \ Merge Request. "Git - это просто!" - Урок 9JavaClass
Видео6 советов для правильного написания git commit сообщенийPurpleSchool | Anton Larichev
ВидеоОбновления без страха: объясняю SemVer (MAJOR/MINOR/PATCH)Agile Clarity
Видео1.2 Git – Теги – Использование тегов для релиза с describe и archiveJavaScript.ru

Комментарии

Обсуждение живёт в GitHub Discussions и грузится с чужого домена — поэтому только по нажатию.