Коммит — единица истории. Через полгода именно по коммитам вы (или коллега) будете разбираться, что и зачем менялось. Разница между историей-помощником и историей-помойкой — в ремесле: что класть в коммит, как его подписывать и что в репозиторий не пускать вообще.
.gitignore вычёркивает .env и build/ ещё в рабочей папке — Git их просто не показывает. Из оставшегося git add набирает в индекс ровно тот кусок, который войдёт в коммит, а git commit превращает индекс в новый узел истории. Две правки, случайно оказавшиеся в одном файле, дают два узла — и каждый откатывается отдельно.
Атомарный коммит: одно изменение — один снимокспросят на собеседовании
Хороший коммит — атомарный: одно логическое изменение целиком. Починили баг — коммит; отформатировали код — отдельный коммит; добавили функцию — третий. Плохие крайности: «коммит-свалка» (баг + рефакторинг + новая функция в одном) и коммит «сохранился на всякий случай» с половиной недописанной мысли.
Зачем это: атомарные коммиты можно по одному откатывать, переносить между ветками и понимать при ревью. Свалку — только целиком.
Точность набора обеспечивает индекс:
git add db/migrations/0042_order_total.sql # только нужные файлы
git add -p # интерактивно: по кусочкам внутри файла!
git commit -m "fix: пересчёт суммы при удалении последней позиции"
git add -p — недооценённая команда: она показывает изменения кусками и спрашивает, какие включить. Так из одного файла с двумя правками собираются два чистых коммита.
Сообщение коммита: ответ на вопрос «зачем»
Первая строка — короткое резюме, без точки в конце: в ленте git log --oneline видно только её. Планку называет сама документация Git: не больше 50 символов. Длиннее — и строку начнёт обрезать и лента в терминале, и карточка коммита на сервере. Форма бывает разная — повеление (починить), отглагольное существительное (пересчёт суммы), описание симптома (сумма не обнулялась); важно не какая, а что вся команда пишет одинаково. Что именно изменилось — видно из кода; сообщение отвечает на то, чего в коде нет: зачем.
- Плохо:
fix,правки,wip,изменил сервис заказов. - Хорошо:
fix: сумма корзины не обнулялась при удалении последней позиции.
Многие команды используют префиксы conventional commits (feat:, fix:, refactor:, docs:) — по ним удобно фильтровать историю и собирать changelog автоматически. Если нужно больше контекста — пустая строка после резюме и абзац подробностей.
Две ленты git log --oneline: по верхней не понять ни что менялось, ни зачем, по нижней видно и то и другое.
Читать историю: log, diff, blame
git log --oneline -15 # последние 15 коммитов кратко
git log -p file.txt # история конкретного файла с изменениями
git diff # что изменено, но ещё не в индексе
git diff --staged # что уже в индексе (войдёт в коммит)
git show abc123 # что сделал конкретный коммит
git blame db/schema.sql # кто и в каком коммите менял каждую строку
git blame звучит обвиняюще, но это инструмент археологии, а не поиска виноватых: нашли странную строку → blame → коммит → его сообщение объясняет «зачем» → а если нет, известно, кого спросить. Связка «diff перед коммитом» — привычка, отсекающая случайные изменения: секунда просмотра спасает от закоммиченного отладочного вывода.
Одной строкой --oneline история нужна редко: при отладке из неё ищут. Ключи, которые окупаются в первую же неделю:
git log --oneline --graph --all -20 # лента с ветками и слияниями
git log -S "discount_rate" --oneline # коммиты, где эта строка появилась или исчезла
git log --grep "PROJ-142" # по тексту сообщения: вся история задачи
git log --author=ivan --since="2 weeks ago" # кто и когда
git log --oneline -- src/billing/ # только коммиты, трогавшие этот путь
git log -L 40,60:db/schema.sql # история конкретных строк файла
-S (его зовут «кирка») отвечает на вопрос «когда эту функцию удалили», на который blame ответить не может: удалённой строки в файле нет. А --graph --all показывает, как ветки расходились и сходились, и это первое, что открывают, когда история выглядит непонятно.
У blame две слабости, и обе лечатся ключами. Массовое переформатирование или перенос файла записывают на автора того коммита все строки разом, и настоящая история пропадает за «Reformat code»; git blame -w игнорирует изменения пробелов, -C (можно повторить дважды) отслеживает строки, переехавшие из других файлов, а --ignore-rev пропускает названный коммит, и его номер команды кладут в файл .git-blame-ignore-revs, чтобы все пропускали его автоматически. Вторая слабость в том, что сообщение коммита не всегда объясняет «зачем»; тогда от коммита идут к обсуждению: по номеру задачи в сообщении или по pull request, куда коммит попал, GitHub и GitLab показывают его прямо на странице коммита.
.gitignore: чему в истории не место
Git предлагает коммитить всё, что видит, но не всё должно попадать в репозиторий: артефакты сборки воспроизводятся из кода, зависимости из lock-файла, а настройки редактора у каждого свои. Файл .gitignore в корне проекта перечисляет пути, которые Git перестаёт замечать. Правило одно: игнорируется генерируемое и личное, коммитится исходное и общее.
# артефакты сборки — воспроизводятся из кода
build/
# зависимости — восстанавливаются из lock-файла
node_modules/
# логи
*.log
# настройки IDE
.idea/
# СЕКРЕТЫ — никогда в git!
.env
Зависимости и их lock-файл живут по разные стороны этого правила: node_modules/, .venv/, target/ и build/ восстанавливаются и в историю не идут, а lock-файл (package-lock.json, go.sum, poetry.lock, gradle.lockfile) коммитят обязательно, иначе у коллеги соберётся другой набор версий.
Строка про .env важнее остальных. Секреты (пароли, ключи API) не должны попадать в историю никогда — история распределённая и вечная. Вычистить секрет из неё технически можно, но это не спасает: на публичном хостинге старый коммит ещё какое-то время открывается прямо по своему идентификатору, а у коллег, успевших забрать историю, он уже лежит в локальной копии. Работает ровно одно действие — сменить сам ключ. Чтобы секрет не доехал до сервера вовсе, в конвейер ставят проверку истории на секреты (Gitleaks); где такие проверки стоят в конвейере, разбирает статья про его принципы. Попавший в коммит секрет считается скомпрометированным с этой секунды, и вопрос только в том, как быстро его отзовут.
Две ловушки .gitignore
Комментарии здесь стоят отдельными строками, и это не вкусовщина. В .gitignore символ # начинает комментарий только в начале строки; если написать build/ # артефакты, Git примет за шаблон всю строку целиком и пойдёт искать внутри папки build что-то с именем # артефакты. Такого пути в проекте нет, правило молча не срабатывает, а build/ продолжает попадать в коммиты. Проверить, что именно игнорируется, помогает git status --ignored.
Вторая ловушка того же файла обиднее. .gitignore действует только на то, за чем Git ещё не следит. Если build/ хоть раз попал в коммит, Git уже считает папку своей, и добавленная строка ничего не изменит: build/ так и будет всплывать в git status после каждой сборки. Папку надо сначала убрать из индекса, оставив файлы на диске:
git rm -r --cached build/ # Git перестаёт следить, с диска ничего не пропадает
git commit -m "chore: убрать build из репозитория"
Вот после этого правило из .gitignore начинает работать.
Что показывает git status: untracked и уборка
Новичок кладёт в папку файл, запускает git status и не находит его среди «изменённых»: Git показывает его отдельно, в блоке Untracked files, а в коротком виде (git status -s) помечает двумя знаками вопроса ??. Это правильно: Git ничего не знает о файле, пока его хотя бы раз не добавили в индекс. Изменённый это тот, чей прошлый снимок уже есть в истории. Отсюда три состояния в выводе: untracked (Git не следит), modified (следит, файл изменён, но в индекс не положен), staged (лежит в индексе и войдёт в следующий коммит).
Удалять и переименовывать отслеживаемые файлы удобнее через Git, чем через файловый менеджер: тогда индекс обновится сам.
git rm docs/old-runbook.md # удалить с диска и из индекса
git rm --cached secrets.env # перестать отслеживать, но оставить на диске
git mv docs/setup.md docs/install.md # переименовать, Git запишет это как перемещение
git clean -n # показать, какие untracked-файлы будут удалены
git clean -fd # удалить их вместе с пустыми папками
git clean без -n необратим: untracked-файлы не попадали ни в один снимок, и вернуть их будет неоткуда. Поэтому сначала всегда сухой прогон.
Поправить последний коммитспросят на собеседовании
Забыли файл или опечатались в сообщении — не создавайте коммит «fix предыдущего»:
git add забытый-файл.txt
git commit --amend # заменит последний коммит новым
Осторожность одна: --amend переписывает коммит: старый снимок заменяется новым с другим идентификатором, а не правится на месте. Пока коммит только у вас, это никого не касается; если он уже на сервере, обычный push откажет, и --amend попадает под золотое правило переписывания истории. Безопасно, пока он не отправлен на сервер. Правило «не переписывать отправленное» подробно разобрано в статье про rebase.
Глубже: секрет попал в историюрасширенное
Ключ от базы или токен, закоммиченный по ошибке, «удалить трудно» не потому, что Git упрям, а потому, что снимок с ним останется в истории у всех, кто успел сделать pull. Из своей истории его вычищают инструментом, который переписывает все коммиты, где встречался файл или строка: git filter-repo (рекомендуемый, ставится отдельно) или BFG Repo-Cleaner.
git filter-repo --path config/prod.env --invert-paths # выбросить файл из всех коммитов
git push --force --all && git push --force --tags
После этого хеши всех коммитов начиная с первого «заражённого» меняются, и каждому в команде придётся заново клонировать репозиторий. Но главное не это. На GitHub и GitLab старый коммит ещё некоторое время открывается по прямому адресу с его SHA, лежит в кешах форков и в логах CI. Поэтому единственное, что действительно закрывает утечку, это ротация: старый ключ отзывают, выпускают новый, и лишь потом чистят историю, чтобы секрет не всплывал в поиске.
Глубже: хуки до коммитарасширенное
Секрет в истории, забытый форматер, тест, который не запускали: всё это можно ловить до того, как коммит создан. Git перед каждым коммитом запускает скрипт .git/hooks/pre-commit, если он есть и исполняем; ненулевой код возврата отменяет коммит.
#!/bin/sh
# .git/hooks/pre-commit
gitleaks protect --staged --redact || exit 1
make lint || exit 1
Чем это отличается от проверки в PR: хук работает у вас на машине, до того как коммит уехал на сервер, и ловит секрет, который в PR уже стал бы утечкой. Зато хук легко обойти: git commit --no-verify пропускает его, а сам файл хука не коммитится, каждый ставит его себе сам (поэтому команды держат конфигурацию в репозитории и раскатывают её инструментами вроде pre-commit или husky). Правило одно: хук ускоряет обратную связь, но не заменяет проверку в CI. Что именно обязан ловить конвейер, разобрано в стандартах.
Глубже: git bisect: найти сломавший коммитрасширенноеспросят на собеседовании
История как инструмент отладки работает так: тест, который проходил неделю назад, сегодня падает, а между двумя точками сто коммитов. Проверять по одному значит сто прогонов; git bisect делает двоичный поиск и укладывается в семь.
git bisect start
git bisect bad # текущий коммит сломан
git bisect good v2.3.0 # здесь точно работало
# Git переключает на середину; проверяем и отвечаем:
git bisect good # или git bisect bad
git bisect reset # вернуться, когда виновник назван
Если проверка автоматизируется, отвечать руками не нужно: git bisect run make test сам гоняет команду на каждом шаге и находит первый плохой коммит. Здесь и окупается дисциплина атомарных коммитов: когда каждый снимок собирается и означает одно изменение, найденный коммит сразу говорит, что сломалось; в коммите «правки» на сорок файлов поиск заканчивается там же, где начался.
Глубже: подпись коммитоврасширенное
Имя и почта в коммите это просто строки из git config, их может написать кто угодно. Поэтому компании, где история кода служит юридическим следом, требуют подписанных коммитов: рядом с коммитом лежит подпись, сервер проверяет её и показывает отметкой verified.
Подписывать можно GPG-ключом или тем же SSH-ключом, которым вы ходите на сервер, второй путь проще:
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true # подписывать всегда, без -S
Открытый ключ добавляют в аккаунт как signing key (это отдельный пункт настроек, не тот, что для доступа). После этого git log --show-signature показывает, кем подписан коммит, а в правилах защиты ветки включают требование подписи. Вместе с подписью часто требуют и Signed-off-by (git commit -s): это не криптография, а формальное согласие с лицензией вклада.
Коротко
- Атомарный коммит: одно логическое изменение.
git add -pсобирает коммит по кусочкам. - Сообщение отвечает «зачем»: короткое резюме, при нужде — подробности абзацем. Префиксы feat/fix помогают фильтровать.
- История — инструмент: log для ленты, diff перед коммитом, blame для археологии.
- .gitignore: сборка, зависимости, IDE-файлы — вон; секреты — никогда в git, попали — ротация.
--amendчинит последний коммит, пока он не отправлен.git statusделит файлы на untracked, modified и staged; удалять и переименовывать черезgit rmиgit mv,git cleanтолько после-n.- Секрет в истории лечит ротация ключа, а уже потом
git filter-repo; сломавший коммит ищетgit bisect, до коммита проверяет хук pre-commit. - Историю ищут ключами:
-Sпо содержимому,--grepпо сообщению,-- путьпо файлам,--graph --allпо веткам;blame -w -Cи.git-blame-ignore-revsспасают от переформатирований;--amendсоздаёт новый коммит с другим идентификатором.
Что почитать дальше
- Ветки и слияние — следующая тема цикла.
- Отмена и спасение — reset, revert и reflog, когда что-то пошло не так.
- Rebase и cherry-pick — как привести серию коммитов в порядок до того, как её увидят коллеги.
- Что такое Git — если пропустили начало.