Рабочий цикл: staging, commit, diff, status
Теперь, когда ты знаешь из предыдущей главы, что внутри .git/ лежит база объектов, refs и index-файл, пора превратить это знание в ежедневный навык. Рабочий цикл в Git — это конвейер из трёх областей: рабочая директория (файлы, которые ты редактируешь), staging area / index (снимок будущего коммита) и репозиторий (неизменяемая история). Вся сила Git — в том, что эти области разделены: ты можешь править десять файлов, но зафиксировать в одном коммите три, а остальные оставить «в работе».
В продакшене этот цикл — валюта командной работы. Ревьювер читает твои коммиты по одному, и если каждый коммит атомарный и с хорошим сообщением, код-ревью проходит в разы быстрее. CI роняет сборку — по истории понятно, какой коммит виноват и зачем он был нужен. Через полгода git blame и git log по файлу расскажут историю решений лучше любой wiki. Всё это начинается с дисциплины на уровне add → commit.
Разбираем конвейер целиком: схема областей, git add под микроскопом, .gitignore во всех правилах, --amend, философия маленьких коммитов, conventional commits и чтение git status/git diff без единой непонятной строки.
Три области: карта рабочего цикла
Заголовок раздела «Три области: карта рабочего цикла» git add git commit┌───────────────┐ ┌───────────────┐ ┌───────────────────┐│ РАБОЧАЯ │ ─────▶ │ INDEX │ ─────▶ │ РЕПОЗИТОРИЙ ││ ДИРЕКТОРИЯ │ │ (STAGING) │ │ (.git/objects) ││ │ │ │ │ ││ файлы как │ │ снимок того, │ │ неизменяемые ││ их правишь │ │ что войдёт в │ │ commit → tree → ││ ты прямо │ │ следующий │ │ blobs ││ сейчас │ │ коммит │ │ │└───────┬───────┘ └───────────────┘ └───────────────────┘ │ │ │ │◀──────────────────────┴────── git checkout/restore (файл) │◀────────────────────────────────── git reset (mixed/hard) │◀────────────────────────────────── git merge/rebaseВажно: все стрелки двусторонние по смыслу — Git умеет двигать изменения между любыми двумя областями (git diff сравнивает любую пару, restore/reset/checkout переносят содержимое назад). В этой главе фокус на пути «вперёд»: workdir → index → repository.
Типичная сессия выглядит так:
# правишь файлы в редакторе...git status # что изменилось?git diff # как именно изменилось (workdir vs index)git add src/auth.ts # кандидат в коммит №1git add -p src/api.ts # из файла — только часть хунков в коммит №1git commit -m "feat(auth): вход через OAuth"git add src/api.ts # остаток правок — в коммит №2git commit -m "refactor(api): типизировать ответы"git add в деталях: что попадает в index
Заголовок раздела «git add в деталях: что попадает в index»git add (см. git-add(1); механика — в главе про internals) делает три вещи: пишет blob в базу, считает хэш, обновляет запись в index. Но у команды есть нюансы, которые бьют каждый день:
git add файлфиксирует снимок на момент вызова. Если послеaddснова поправить файл — в index останется старая версия, аgit statusпокажет одновременно «staged: modified» и «unstaged: modified». Нужна актуальная версия — add придётся повторить.-p(patch mode) — самый профессиональный режим. Git показывает изменения «хунками» (кусками diff) и спрашивает:y— взять,n— пропустить,s— разбить хунк мельче,e— отредактировать вручную. Так из одного файла делают два атомарных коммита.-A/.добавляют всё, включая удалённые файлы (git add -Aэквивалентенgit add .; git add -u). Удобно, но опасно: мусор попадёт в коммит.git add -fпринудительно добавляет файл, который игнорируется.gitignore(например, запиннить изменённыйpackage-lock.json, если он в игноре).
Проверить, что реально в index:
git diff --cached # staged: index vs HEADgit ls-files --stage # сырой список записей index.gitignore: все правила с примерами
Заголовок раздела «.gitignore: все правила с примерами».gitignore (полный синтаксис паттернов — gitignore(5)) — текстовый файл с паттернами, по одному на строку. Git проверяет паттерны против пути файла и решает: untracked-файлы, попавшие под паттерн, вообще не показываются в git status и не добавляются массово.
# 1. Комментарий — строка на #
# 2. Обычный файл или папка (в любой директории репозитория)node_modules/*.logdist/
# 3. Ведущий слэш — от корня репозитория (без слэша — везде)/build/ # только корневая build/temp/ # temp/ на ЛЮБОМ уровне
# 4. Ведущий ! — отрицание (исключение из игнора)*.env!.env.example # example всё-таки коммитим
# 5. Двойная звезда — на любую глубину вложенности**/coverage/docs/**/*.pdf
# 6. Звезда не пересекает слэш; вопрос — один символ; [abc] — классphoto-????.jpg # photo-0001.jpg, но не photo-00001.jpgsrc/[0-9]*.ts
# 7. Слэш в конце — только директорииlogs/ # игнорирует директорию logs/, но не файл logsТри грабли, о которых забывают:
- Игнор не действует на уже отслеживаемые файлы. Если
config.envуже в истории, строка в.gitignoreничего не изменит. Нужно сначалаgit rm --cached config.env(удалит из index, оставит на диске) и закоммить. - Пробелы и спецсимволы экранируются бэкслешем:
my\ file.txt. - Порядок имеет значение: более позднее отрицание
!побеждает ранний игнор, но не может «вернуть» файл, если игнор заблокировал родительскую директорию — тогда Git вообще не смотрит внутрь. Правильно: не игнорить саму папку, а игнорить её содержимое с исключениями:
# ПЛОХО: logs/ потом !logs/important.log — не сработает# ХОРОШО:logs/*!logs/important.loggit commit: атомарность, amend, сообщения
Заголовок раздела «git commit: атомарность, amend, сообщения»Коммит должен быть атомарным: одно логическое изменение, которое (в идеале) самостоятельно компилируется, проходит тесты и имеет смысл при revert. «Коммичу всё разом в пятницу вечером» — антипаттерн: ревью превращается в мучение, git bisect (поиск коммита, сломавшего сборку) бессмысленен, откат одной «фичи» из каши невозможен.
Практический тест атомарности: можешь ли ты описать коммит одним предложением без слов «и», «также», «заодно»? Нет — разбивай.
git commit --amend (см. git-commit(1)) дописывает текущий index в предыдущий коммит, а не создаёт новый:
до amend: после amend:A ← B ← C(main) A ← B ← C'(main) ^ index: новые правки ^ | index пуст, C' = └─ твои правки (ещё не в истории) C + правкиC’ — новый объект с новым хэшем (другой родитель? нет, тот же, но другое дерево и сообщение). Старый C остаётся в базе до gc. Отсюда железное правило: не amend’ишь коммиты, которые уже запушены — у коллег C, у тебя C’, и это конфликт истории (потребуется force-push).
Amend удобен, чтобы дописать забытый файл в «только что сделанный» коммит или поправить опечатку в сообщении:
git add forgotten.tsgit commit --amend --no-edit # дописать файлы, сообщение не трогатьgit commit --amend -m "feat(auth): исправить опечатку в сообщении"Хорошие сообщения. Правило индустрии (классика жанра — статья Chris Beams «How to Write a Git Commit Message») — первая строка (subject) до 72 символов, в повелительном наклонении, без точки в конце; далее пустая строка и тело с мотивацией («почему», а не «что» — «что» видно в diff):
исправить гонку при одновременной записи сессии
Раньше два параллельных запроса могли перезаписатьсессионный файл друг друга: запись не была атомарной.Теперь пишем во временный файл и делаем rename,что атомарно на POSIX.
Задача: PROJ-1234Conventional Commits: разбор формата
Заголовок раздела «Conventional Commits: разбор формата»Conventional Commits — стандарт сообщений, от которого растут автоматический чейнджлоги, семантическое версионирование и триггеры CI:
feat(api): добавить эндпоинт обновления профиля└─┬─┘ └┬┘ └──────────────────┬─────────────────┘ │ │ └─ subject: что, в повелительном наклонении │ └─ scope (опционально): область кода └─ type: feat | fix | docs | style | refactor | perf | test | build | ci | chore | revertПравила:
- type обязателен,
featиfixвлияют на версию (minor/patch в semver-релизах). - BREAKING CHANGE: в теле или
!после type/scope — major-версия:feat(api)!: удалить устаревший /v1/users. - Scope — свободное слово из терминологии проекта (
api,ui,auth), помогает быстрой навигации по истории. - Subject — без заглавной буквы и без точки (соглашение, а не догма).
Практическая ценность:
# автоматический чейнджлог из историиgit log --pretty=format:"%s" v1.0.0..HEAD | grep "^feat" > changelog-features.txt
# CI: линт коммитов через commitlint, релизы через release-pleasegit status и git diff: читать без магии
Заголовок раздела «git status и git diff: читать без магии»git status сравнивает три области и раскладывает файлы по корзинам:
On branch mainYour branch is ahead of 'origin/main' by 2 commits. ← heads/main vs remotes/origin/main
Changes to be committed: ← index ≠ HEAD (попадёт в коммит) (use "git restore --staged <file>..." to unstage) modified: src/auth.ts
Changes not staged for commit: ← workdir ≠ index (не попадёт!) (use "git add <file>..." to update what will be committed) modified: src/api.ts
Untracked files: ← есть на диске, нет ни в index, ни в HEAD (use "git add <file>..." to include in what will be committed) src/new-module.tsТри зоны вывода — это ровно три пары различий «область vs область». Соответствующие git diff (см. git-diff(1)):
git diff # workdir vs index (что НЕ попадёт в коммит)git diff --cached # index vs HEAD (что попадёт в коммит)git diff HEAD # workdir vs HEAD (всё несохранённое, суммарно)git diff main feature # два коммита/refs целикомОдин полный проход «как про» перед каждым коммитом:
git status # карта: что где лежитgit diff # ревью своих незакоммиченных правокgit add -p # выборочная индексацияgit diff --cached # финальное ревью будущего коммитаgit commit -m "feat(auth): ..."Профессиональные приёмы коммита
Заголовок раздела «Профессиональные приёмы коммита»Три приёма, которые отличают «коммичу как получится» от «коммичу как инженер»:
git commit -v — открывает редактор с полным diff будущего коммита прямо под сообщением. Последний шанс отреагировать: «постой, я не хотел коммитить этот файл» или «здесь остался отладочный console.log». Приучайся: сообщение пишешь, глядя на реальный diff, а не на память.
git commit -am "..." — комбинация «добавить все отслеживаемые + закоммитить». Удобно, но с двумя ловушками: untracked-файлы не добавляются (новый файл молча не попадёт в коммит), а частично проиндексированные правки будут проигнорированы — -a перезапишет index полным снимком рабочей директории. Для атомарной работы с -p эта команда не подходит в принципе.
--fixup и --autosquash — механизм «доложить правки к существующему коммиту» без ручного rebase. Работает так:
# история: A ← B ← C(main), в B надо дописать правкиgit add -p # индексируем только нужноеgit commit --fixup B # создаётся коммит "fixup! <сообщение B>"git rebase -i --autosquash main # Git сам расставит squash к BРезультат: B дополнен правками, коммит fixup! исчезает, история чистая. В паре с rebase --autosquash это стандартный способ реагировать на замечания ревьюера: каждое замечание — отдельный fixup, а перед мерджем история собирается автоматически. Полную механику rebase разбираем в главе про merge/rebase.
Типичные ошибки и грабли
Заголовок раздела «Типичные ошибки и грабли»- «Закоммитил и сразу вижу опечатку — правлю файл и коммичу “fix typo”». Лишний коммит в истории. Если не пушил —
git commit --amend --no-edit. Пушил —git revertили отдельный фикс (см. главу про merge/rebase). - Коммит-монстр «фича целиком, разом». Сложно ревьюить, невозможно bisect’ить. Решение:
git add -pи серия атомарных коммитов. .gitignoreне работает на уже закоммиченный файл.git rm --cached файл+ коммит — только так. Частый случай: запушили.env— после чистки его всё равно надо считать скомпрометированным и менять секреты.git add .в корне «по привычке». Тянет сгенерированные файлы, локальные конфиги, временные артефакты. Приучайся добавлять осознанно.- Сообщение «update» / «fixes» / «wip». Через полгода это мусор. Минимум: тип + область + действие. Идеал: тело с мотивацией.
- Игнор с
папка/и потом удивление, почему!папка/важный.txtне работает. Git не заходит в игнорируемую директорию. Игнорипапка/*+!папка/важный.txt. git diff«пустой», а файл менялся. Скорее всего изменения уже в index — смотриgit diff --cached.
Вопросы на собеседовании
Заголовок раздела «Вопросы на собеседовании»- Чем staged отличается от unstaged? Staged — запись в index отличается от HEAD (попадёт в коммит); unstaged — рабочая директория отличается от index (не попадёт). Оба состояния могут существовать у одного файла одновременно.
- Как разбить правки одного файла на два коммита?
git add -p файл: интерактивно выбрать хунки в первый коммит, закоммитить, затемgit addостаток и второй коммит. - Что делает
git commit --amend? Создаёт ли новый коммит? Создаёт новый объект коммита (C’) с тем же родителем, но новым деревом/сообщением, и двигает ветку. Старый коммит остаётся в базе до gc. Поэтому нельзя amend’ить запушенное. - Файл в
.gitignore, ноgit statusвсё равно показывает его как modified. Почему? Файл уже отслеживается (есть в HEAD). Игнор действует только на untracked-файлы. Лечитсяgit rm --cached. - Как посмотреть, что войдёт в следующий коммит?
git diff --cached(index vs HEAD). А «что я наменял с последнего add» —git diff(workdir vs index). - Зачем Conventional Commits, кроме красивых сообщений? Машиночитаемая история: автоматические чейнджлоги, semver-релизы (feat→minor, fix→patch, BREAKING→major), условные CI-триггеры, линтинг через commitlint.
- Паттерн
*.logигнорируетlogs/debug.log? А/build/—src/build/? Первый — да (без слэша паттерн действует на любую глубину). Второй — нет: ведущий слэш якорит паттерн к корню репозитория.
Практика
Заголовок раздела «Практика»- В песочном репозитории сделай правки в трёх файлах так, чтобы один был «staged + ещё доработан после add», второй — только unstaged, третий — untracked. Сделай так, чтобы
git statusпоказал все три корзины одновременно. Критерий: вывод статуса объясним каждой строкой. - Разбей правки одного файла на два атомарных коммита через
git add -p(понадобится файл с двумя независимыми изменениями). Критерий:git log -pпоказывает два коммита, каждый с одним логическим изменением. - Настрой
.gitignoreдля проекта:node_modules/,dist/,*.log,.envс исключением.env.example, директорияdata/с исключениемdata/seed.json. Проверь:git status --ignoredпоказывает ожидаемое. - Создай файл, закоммить его, добавь в
.gitignore, убедись, что игнор не сработал. Исправь ситуацию правильной командой. Критерий: после фикса файл modified больше не светится, но остался на диске. - Напиши серию из пяти коммитов в формате Conventional Commits с разными type (feat, fix, refactor, docs, test) и одним с
!. Критерий:git log --onelineчитается как мини-чейнджлог;git log --grep="^feat"находит фичи. - Закоммить файл с опечаткой в сообщении, не пуша. Исправь сообщение через
--amend. Критерий:git log --onelineпоказывает исправленное сообщение, а в reflog (git reflog) виден и старый, и новый коммит.
Что почитать
Заголовок раздела «Что почитать»- Pro Git, глава 2: Основы Git — базовый рабочий цикл от первоисточника.
- gitignore(5) — man-страница с полным синтаксисом паттернов — исчерпывающее описание правил.
- Conventional Commits, спецификация — официальная спецификация на русском.
- commitlint — линтер сообщений коммитов для CI.
- How to Write a Git Commit Message (Chris Beams) — классическая статья о семи правилах хорошего сообщения.
- Pro Git, глава 7: Интерактивное индексирование —
git add -i/-pглубже.