7.1 Атомарность коммита
Атомарный коммит делает одну вещь. Не «допилил login + поменял header + обновил версию react». Три коммита: один про login, один про header, один про react.
Правила:
- Одна логическая правка - один коммит. Если в сообщении появляется «и», это часто два коммита.
- Сборка проходит на каждом коммите. Не «коммит N сломан,
в N+1 починил». Каждый коммит должен компилироваться,
тесты проходить. Иначе
bisectсломается. - Коммит обратим. Если этот коммит revert'нуть - приложение должно работать дальше. Не «без этого коммита всё развалится потому что в коммите N+5 мы на него опёрлись».
- Никаких "WIP", "fix typo", "addresses comments". Это
процессуальный шум, которому не место в публичной истории.
Перед PR такие коммиты сжимаются (
squashилиinteractive rebase).
Зачем это нужно:
git bisectзаlog(N)шагов находит коммит-источник бага. Если в истории есть «сломанные» промежуточные коммиты - bisect врёт.git revert <sha>откатит проблемный коммит без боли. Если в коммите три не связанных правки, revert заденет и невинные.git blameпокажет «кто и когда» написал строку с проблемой. Если коммит «допилил всё подряд» - не поможет понять контекст.
См. atomic-commit.
7.2 Структура сообщения коммита
Самый влиятельный стандарт - короткий пост Tim Pope «A Note About Git Commit Messages» (2008). Семь правил:
Краткая суть в повелительном наклонении, ≤ 50 символов
Развёрнутое описание, если нужно. Перенос по 72 колонкам.
Объясняет «почему», не «что» - что и так видно в diff'е.
- Допустимы маркеры
- И ссылки на тикеты
Closes #123
Co-authored-by: Имя <email@example.com>
Семь правил:
- Отделяй заголовок от тела пустой строкой.
- Заголовок ≤ 50 символов.
- Заголовок с заглавной буквы.
- Без точки в конце заголовка.
- Повелительное наклонение:
Add,Fix, неAdded,Adds. - Тело перенесено по 72 колонкам.
- Тело объясняет «что» и «почему», не «как».
Повелительное - это не каприз. Если читать сообщение коммита как «если применить этот коммит, он …», то фраза должна продолжаться естественно: «Add login form» (если применить, добавит форму). «Added login form» - звучит криво. Так же пишут GitHub, GitLab и сам Git в авто-сообщениях merge/revert.
7.3 Conventional Commits
Поверх Tim Pope-стиля многие команды надстраивают формат - Conventional Commits. Идея: префикс заголовка по типу изменения.
<type>(<scope>): <subject>
[optional body]
[optional footer]
Типы:
feat- новая фича для пользователяfix- баг-фиксdocs- только документацияstyle- форматирование, точки с запятой, whitespacerefactor- изменение кода без новой фичи и без фиксаperf- улучшение производительностиtest- добавление/исправление тестовchore- обслуживание (обновление зависимостей, конфигов)build,ci- инфраструктура сборки и CIrevert- отмена другого коммита
Scope опционален - модуль или подсистема. Subject - то же, что в Tim Pope-стиле: повелительное, без точки, ≤ 50 (с учётом префикса).
Примеры:
feat(auth): add password reset flow
fix(api): handle null in user.profile
docs(readme): document SSL certificate setup
chore(deps): bump react to 18.3.1
refactor!: drop support for Node 16
Восклицательный знак ! после типа - пометка «ломающее
изменение». Альтернатива - BREAKING CHANGE: в футере:
feat(api)!: rename POST /user to POST /users
BREAKING CHANGE: endpoint /user no longer exists,
use plural /users.
Зачем это надо. Главные две причины:
- Автоматическая генерация changelog. Инструменты вроде
standard-versionилиrelease-pleaseпарсят историю, группируют по типам, создают CHANGELOG.md и git-тег. - Автоматический выбор версии для semver.
fix:→ patch,feat:→ minor,BREAKING CHANGE:→ major.
Не нужен - если нет процесса автоматического релиза. Тогда возвращайся к простому Tim Pope-стилю, никто не пострадает.
См. conventional-commits.
7.4 Semver: что значат три цифры
Semver (semantic versioning) - конвенция: MAJOR.MINOR.PATCH,
например 1.4.2.
- PATCH (1.4.2) - баг-фиксы, обратно-совместимые. Если
пользователь обновится с 1.4.1 на 1.4.2 - ничего не сломается.
- MINOR (1.4.0) - новая функциональность, обратно совместимая. Можно использовать новое; старое продолжает работать.
- MAJOR (1.0.0) - ломающие изменения. Что-то старое больше не работает или работает иначе. Обновление требует миграции.
Дополнительные ярлыки:
1.4.2-beta.1,1.4.2-rc.3- pre-release.1.4.2+20260527.git7c8a1- build-метаданные.
На что чаще ошибаются:
- 0.x.y - пока major = 0, semver не действует строго. Версия считается «нестабильной», ломающие изменения могут быть и в minor. Это «обещание не давать обещаний».
- 1.0.0 означает «API стабильный». Не «продакшен-готовый»,
а именно «теперь обещаю semver-совместимость». Многие
проекты годами сидят на
0.xименно поэтому. - PATCH с переписанным внутри допустим, если поведение снаружи не поменялось. Внутренние правки никого не волнуют, пока публичный контракт остаётся.
Связь с Conventional Commits через инструменты типа
semantic-release: посмотри коммиты с последнего тега,
найди BREAKING CHANGE → major, feat: → minor, иначе patch.
Создай тег, запушь. Всё автоматически.
См. semver.
7.5 git commit --amend: можно и нельзя
--amend переписывает последний коммит. Не создаёт новый -
именно переписывает. Используется для:
- Поправить сообщение -
git commit --amend -m "новое". - Добавить забытый файл -
git add forgot.txt && git commit --amend --no-edit. Файл добавится в предыдущий коммит, сообщение останется как было. - Поправить содержимое - если только что закоммитил с
ошибкой, можно отредактировать файлы,
git add,git commit --amend.
Физически --amend создаёт новый коммит (с новым SHA, так
как содержимое поменялось) и двигает ветку на него. Старый
коммит остаётся в .git/objects/ как dangling - reflog его
ещё месяц видит.
Когда нельзя
Если коммит уже запушен и кто-то его подтянул - --amend
ломает историю. У тебя локально один SHA, у других
разработчиков другой. При следующем push Git откажет:
! [rejected] feature -> feature (non-fast-forward)
Варианты:
- Force-push через
--force-with-lease. Безопасно для своей feature-ветки, если никто другой её не успел затащить. - Не amend'ить, а сделать новый коммит. Это правильный
вариант для общих веток (
main,develop).
Правило: amend разрешён до первого push. После push - только на личных ветках и только с force-with-lease.
См. amend.
7.6 Разбить накопившуюся правку через git add -p
Часто случается: писал-писал, изменил пять файлов на разные темы. Хочется закоммитить их как три атомарных коммита.
git add -p (patch mode) разбирает каждый файл на hunk'и
(куски ±3 строки контекста) и спрашивает по каждому:
Stage this hunk [y,n,q,a,d,s,e,?]?
Главные ответы:
y- застейджитьn- пропуститьs- разбить на более мелкие куски (если Git может)e- открыть в редакторе и убрать ненужные строки рукамиq- выйти, оставив остальные неrastress'енными
Типичный сценарий: одна правка касается auth/, вторая - billing/, третья - мелкий refactor в utils/.
git add -p src/auth/
git commit -m "feat(auth): add OAuth provider"
git add -p src/billing/
git commit -m "fix(billing): handle null currency"
git add -p src/utils/
git commit -m "refactor(utils): extract date helpers"
Если правки переплетены внутри одного файла - Git разделит
на hunk'и автоматически, и патч-режим спросит по каждому. Если
Git не угадал (две правки на смежных строках) - s или e
помогут.
Альтернатива GUI - git gui (idle интерфейс из стандартной
поставки Git), там разделение по hunk'ам через клики мышью.
Большинство IDE (VS Code, JetBrains) умеют то же самое в
встроенной staging-панели.
7.6.1 Что НЕ класть в коммит
Так же важно знать, чего в коммит лучше не пускать.
Секреты. Пароли, токены, ключи. Если попали в commit и
запушены - считай скомпрометированы. Даже если потом удалить
коммит, история остаётся в reflog и у тех, кто успел склонить.
Менять ключ - единственный надёжный путь. Защита - pre-commit
hook с gitleaks или trufflehog, плюс scanner на сервере
(GitHub Secret Scanning, GitLab Secret Detection).
Большие бинарные файлы. PSD, MP4, ZIP, дамп БД. Git хранит снимки, и каждое изменение бинаря - это полная копия. Репозиторий быстро разрастается. Решение - Git LFS (Large File Storage), при котором сам файл живёт во внешнем хранилище, а в Git только pointer.
Артефакты сборки. node_modules/, build/, *.pyc,
target/. Они генерируются из исходников, дублируют объём,
провоцируют конфликты. .gitignore обязателен.
IDE-файлы пользователя. .idea/workspace.xml, .vscode/*
(кроме общих настроек), *.swp, *.bak. Часть общих
настроек проекта (.vscode/settings.json, .editorconfig)
можно и нужно коммитить.
Локальные конфиги с реальными данными. .env,
config.local.json. Шаблон (.env.example) - да. Реальный
файл - никогда.
Закомментированный мёртвый код. Git помнит всё; если код
понадобится - посмотри в git log или git show. Хранить
рядом для «вдруг» - путь к каше.
Уроки в sandbox
lab-7.1. Разбить смешанную правку на три атомарных коммита
Цель - освоить git add -p и понять, как из накопившейся
работы вытащить чистые атомарные коммиты. После лабы становится
понятно, что атомарность - не теория, а двухминутная привычка.
Создай репозиторий с одним файлом и закоммить:
mkdir atomic-lab && cd atomic-lab && git init && cat > server.js << 'EOF' function add(a, b) { return a + b } function sub(a, b) { return a - b } function mul(a, b) { return a * b } module.exports = { add, sub, mul } EOF git add server.js && git commit -m "initial server.js".Сделай три не связанных правки в одном файле: переименуй
subвsubtract(refactor), добавь функциюdiv(feat), добавь null-проверку вadd(fix). Откройserver.jsи поправь руками.Запусти
git status- увидишь один modified файл. Запустиgit diff- увидишь три не связанные правки.Запусти
git add -p server.js. Git предложит первый hunk - скорее всего объединит несколько правок (они на близких строках). Ответьsдля попытки разбить на меньшие куски, потомy/nдля каждого.Цель - застейджить только refactor (переименование). Если разделить не получается чисто -
e(edit) и убери из патча лишние строки руками. Закоммить:git commit -m "refactor: rename sub to subtract".Повтори с feat:
git add -p server.js, выбери только добавлениеdiv. Закоммит:git commit -m "feat: add div function".Третий проход: всё что осталось -
git add server.js && git commit -m "fix: handle null in add".Сверь итог:
git log --onelineдолжен показать 4 коммита (initial + три по теме).git show HEAD~2,git show HEAD~1,git show HEAD- каждый показывает ровно одну логическую правку.
sandbox с автопроверкой - открыть в песочнице
Резюме
- Атомарный коммит делает одну вещь. Сборка проходит на каждом коммите, коммит обратим через revert без побочных эффектов.
- Сообщение коммита: повелительное наклонение, ≤ 50 символов в заголовке, пустая строка, тело на 72 колонки. Объясняет «почему», не «что».
- Conventional Commits - формат `<type>(<scope>): <subject>`. Используется для авто-генерации changelog и выбора версии при релизе.
- Semver: MAJOR.MINOR.PATCH. Patch - обратно-совместимые баг-фиксы, minor - новая совместимая фича, major - ломающие изменения.
- `git commit --amend` переписывает последний коммит. Можно до первого push, нельзя - после (если ветку видят другие).
- Накопившуюся смешанную правку разбивай через `git add -p`. По hunk'ам выбираешь, что войдёт в текущий коммит.
- В коммит НЕ кладут: секреты, бинарные файлы, артефакты сборки, IDE-метаданные, реальные конфиги, закомментированный мёртвый код.
Контрольные вопросы
Почему «один файл - один коммит» - плохое правило, а «одна логическая правка - один коммит» - хорошее?
Показать ответ
Файл и логическая правка - разные вещи. Один большой рефакторинг может тронуть 30 файлов и должен ехать одним коммитом. Один файл с двумя не связанными правками должен ехать двумя коммитами. Файлы - это структура хранения, коммиты - структура изменений. Правило «один файл = один коммит» приведёт к раздробленной истории, где
git bisectне сможет нормально локализовать баг, потому что половинный коммит ломает сборку.Я сделал `git commit --amend` после `git push`. Теперь push отказывается. Что делать?
Показать ответ
Зависит от того, личная ли это ветка. Если это feature-ветка, которой пользуешься только ты -
git push --force-with-lease(не--force!). Lease-вариант проверит, что remote не успел уйти вперёд, иначе откажет. Если ветку видят другие разработчики или этоmain/master---forceнельзя. Правильное решение - сделать обычный новый коммит с правкой, а amend забыть. Для будущего: договорись с собой не amend'ить после push на любые не-личные ветки.У меня в репозитории случайно оказался файл с паролем в открытом виде. Я его удалил, закоммитил, запушил. Этого достаточно?
Показать ответ
Нет. Удаление в новом коммите оставляет старый коммит с паролем в истории. Все, у кого есть копия (через clone, fork, зеркало, кэш CI/CD), видят пароль в
git log -pилиgit show <старый-sha>. Считай пароль скомпрометированным - сначала смени его, потом думай про историю. Зачистка истории возможна черезgit filter-repo+ force-push, но требует координации со всеми, кто работает с репо. Часто проще смириться, что в reflog'е оно ещё месяц, и сменить пароль.Зачем нужен префикс `feat:` / `fix:` в Conventional Commits, если у меня и так линейная история без релизов?
Показать ответ
Если нет процесса автоматического релиза - может и не нужен. Conventional Commits окупаются в двух сценариях: (1) автогенерация CHANGELOG.md из истории, (2) автоматический выбор semver-уровня при релизе. Если CHANGELOG ты пишешь руками, а версию ставишь руками - формат добавит только обсуждение в команде «как писать сообщения». Если запускаешь релизы через
semantic-release/release-please/standard-version- формат становится обязательным, потому что эти инструменты только так умеют. Бери его именно под потребность, не «потому что модно».Чем `--amend --no-edit` отличается от `--amend` без флагов?
Показать ответ
Без флагов
git commit --amendоткрывает редактор для редактирования сообщения. С--no-edit- оставляет старое сообщение как есть. Используется когда--amendнужен только чтобы подмешать в коммит дополнительные изменения (забытый файл, поправку существующего). Без--no-editпридётся редактор каждый раз закрывать вручную, что замедляет повтор.