5.1 Что такое зона
Зона в Git - это место, где хранится какая-то версия твоих файлов. Их три:
- Working tree (рабочее дерево) - твои файлы на диске, те самые, которые ты правишь в редакторе.
- Index (индекс, он же staging area) - промежуточное место,
где Git собирает следующий коммит. Двоичный файл
.git/index. - Repository (репозиторий) - то, что закоммичено. Объекты в
.git/objects/, на которые указывают ветки в.git/refs/.
Любая команда Git, которая что-то делает с твоими файлами, двигает их между этими зонами. Иногда добавляет, иногда забирает, иногда сравнивает. Поэтому первый вопрос про каждую команду - «с какими зонами она работает».
working tree ──→ индекс ──→ репозиторий
↑ │
└──────────────────────────────┘
checkout
Стрелка от working tree вправо - это git add. От индекса
вправо - git commit. Стрелка обратно - git checkout или
git restore.
5.2 Зона 1: working tree
Working tree - то, что ты видишь в редакторе. Файлы, директории,
права доступа. Git к ним не привязан напрямую: он просто лежит
рядом в .git/, наблюдает и при команде что-то делает.
Git не отслеживает файлы автоматически. Изменения нужно явно отправить в индекс. Это значит:
- Создал новый файл - он untracked, Git ничего о нём не знает.
- Изменил отслеживаемый файл - он modified, но изменения пока
только в рабочем дереве.
- Удалил файл - Git это увидит как deleted, но удаление тоже надо застейджить.
Все три состояния показывает git status. Раздел «Untracked
files» - это пункт 1, «Changes not staged for commit» - пункты
2 и 3.
Работа с working tree - это работа в любом текстовом редакторе
или IDE. Git туда вмешивается только когда явно попросишь -
через checkout, restore, reset --hard.
5.3 Зона 2: индекс (staging area)
Индекс - самая необычная часть Git. В других VCS его обычно нет: ты редактируешь файлы и сразу коммитишь. В Git между «редактируешь» и «коммитишь» есть промежуточный шаг - выбрать, что именно пойдёт в коммит.
Физически индекс - это двоичный файл .git/index. В нём для
каждого отслеживаемого пути записано:
- имя файла,
- права доступа,
- SHA blob'а с содержимым,
- метаданные stat-а файла (для быстрого определения изменений).
Когда мы говорим «застейджить» (git add), это означает: прочитать
файл из рабочего дерева, создать blob, обновить запись в индексе.
Один из частых вопросов про индекс - «зачем он вообще». Ответ приходит, когда работаешь над чем-то сложным:
- Поправил два не связанных бага в одном файле. Хочется
закоммитить их отдельно.
git add -pпозволяет застейджить часть изменений из файла. - Сделал большую правку, но один файл ещё не готов. Стейджишь всё кроме него, коммитишь чистую часть, дорабатываешь второй отдельно.
- Готовишь несколько коммитов, но не хочется их сразу пушить. Каждый раз стейджишь нужный подмножество, коммитишь, повторяешь.
Без индекса каждый коммит - это снимок всего, что лежит в рабочем дереве. С индексом - снимок того, что выбрал.
Команды для работы с индексом:
git add <path>- добавить файл (или директорию) в индекс.git add -p- интерактивно, по hunk'ам.git rm --cached <path>- убрать из индекса, но оставить в рабочем дереве.git restore --staged <path>- откатить стейдж конкретного файла (то, что раньше делалgit reset HEAD <path>).
5.3.1 Копнуть глубже: индекс - это не только staging
Слово «индекс» в Git перегружено. Помимо staging area, оно же используется как:
- кэш состояния рабочего дерева - для ускорения
git status. Без индекса Git каждый раз пересчитывал бы SHA каждого файла, чтобы понять, изменился ли он.- рабочая зона merge - при конфликте слияния в индексе для
каждого конфликтного файла лежит три версии (base, ours,
theirs). Это
--ours,--theirs,--baseварианты вgit checkout.
- рабочая зона merge - при конфликте слияния в индексе для
каждого конфликтного файла лежит три версии (base, ours,
theirs). Это
- хранилище для
git stash. Хотя stash сейчас сделан через отдельные коммиты, исторически он жил в индексе.
Поэтому в документации можно встретить «index», «cache» (старое
имя), «staging area» - это всё про один и тот же .git/index.
В скриптах исторически осталось --cached (git rm --cached,
git ls-files --cached) - синоним для «работа с индексом».
5.4 Карта команд: что куда двигает
Самая полезная картинка для отладки запутанных ситуаций - таблица «команда × зона». В каждой ячейке - что команда делает с этой зоной.
| Команда | Working tree | Index | Repository |
|---|---|---|---|
git add <file> | читает | пишет | - |
git add -p | читает | пишет частично | - |
git commit | - | читает | пишет |
git commit -a | читает | пишет, читает | пишет |
git commit --amend | - | читает | переписывает HEAD |
git rm <file> | удаляет | удаляет | - |
git rm --cached <file> | - | удаляет | - |
git mv | переименовывает | переименовывает | - |
git restore <file> | пишет (из индекса) | - | - |
git restore --staged <file> | - | пишет (из HEAD) | - |
git restore --source=HEAD~3 <file> | пишет (из коммита) | - | читает |
git checkout <branch> | пишет (из коммита) | пишет (из коммита) | читает, двигает HEAD |
git switch <branch> | пишет | пишет | читает, двигает HEAD |
git reset --soft <commit> | - | - | двигает HEAD |
git reset --mixed <commit> (по умолчанию) | - | пишет (из коммита) | двигает HEAD |
git reset --hard <commit> | пишет (из коммита) | пишет (из коммита) | двигает HEAD |
git stash | читает, очищает | читает, очищает | пишет stash-коммит |
git stash pop | пишет | пишет | удаляет stash-коммит |
git merge <branch> | пишет | пишет | пишет merge-коммит |
git pull | пишет | пишет | пишет (fetch + merge) |
Если запомнить три зоны и заглядывать в эту таблицу при сомнении -
команды Git перестают казаться непредсказуемыми. Большая часть
путаницы возникает из-за того, что одна команда (например,
reset) при разных флагах трогает разное количество зон.
5.6 Четыре варианта git diff
git diff - это команда «покажи разницу между двумя версиями».
Версии могут лежать в разных зонах. Поэтому у git diff четыре
основных режима, по одному на каждую интересную пару зон.
git diff
Сравнивает working tree с индексом.
Это «что я ещё не застейджил». Если ты поправил файл, но не
сделал git add - git diff покажет твою правку. Если
застейджил всё - выведет пустоту.
git diff --staged (или --cached)
Сравнивает индекс с HEAD.
Это «что войдёт в следующий коммит». Если ничего не стейджил -
пусто. Если застейджил - увидишь именно те изменения, которые
попадут в git commit.
git diff HEAD
Сравнивает working tree с HEAD.
Это «всё, что отличается от последнего коммита» - независимо от того, застейджено или нет. Сумма первых двух.
git diff <commit-A> <commit-B>
Сравнивает два коммита в репозитории.
Working tree и индекс не участвуют. Это удобно для просмотра, что
изменилось между релизами, в feature-branch, между тегами:
git diff v1.0 v2.0.
Все варианты можно сужать конкретными путями:
git diff HEAD -- src/api.ts
git diff main feature -- backend/
Шорткаты, которые экономят время:
git diff --stat- только сводка (файлы и количество строк).git diff -w- игнорировать изменения в whitespace.git diff --word-diff- построчно показать изменения по словам, не по строкам. Удобно для текстов и markdown.
5.7 Типичные сценарии, объяснённые через зоны
Несколько частых ситуаций становятся прозрачными, если думать зонами.
«Я случайно сделал git add и хочу откатить стейдж».
Файл переехал из working tree в индекс. Откатить = вернуть
индекс к состоянию HEAD. Это git restore --staged <file>.
Working tree не трогаем.
«Я закоммитил, а потом понял что хочу добавить ещё один файл в этот же коммит».
Стейдж файл (git add), потом git commit --amend. amend
берёт текущее состояние индекса и переписывает HEAD-коммит.
Working tree не двигается, репозиторий получает новый коммит
на месте старого (старый становится висячим).
«Я хочу временно убрать незакоммиченные правки, чтобы переключить ветку».
git stash - забирает working tree и индекс в специальный
stash-коммит, чистит working tree до HEAD. После переключения
ветки и работы - git stash pop возвращает обратно.
«Я хочу полностью откатить рабочую копию до состояния последнего коммита».
git reset --hard HEAD. Жёсткий вариант - переписывает все три
зоны до HEAD. Утратишь незакоммиченные правки безвозвратно.
Перед этим - git stash на всякий случай, если не уверен.
«Я случайно сделал git reset --hard и хочу вернуть
состояние».
Если коммит был - git reflog покажет SHA состояния «до». git reset --hard <тот-SHA> вернёт ветку. Если правки не были
закоммичены - увы, рабочее дерево потеряно. Это и есть тот
случай, когда коммитить часто - выгодно.
«Файл по ошибке попал в git, надо убрать из репозитория, но оставить на диске».
git rm --cached <file> + добавить в .gitignore + закоммитить.
Файл уйдёт из индекса (и из следующего коммита), но останется в
рабочем дереве. Из истории при этом он не пропадёт - для этого
нужен git filter-repo, тема отдельная.
5.7.1 Подводный камень: git reset с тремя режимами
git reset <commit> - самая опасная команда из часто используемых.
У неё три флага, и разница принципиальная.
| Флаг | Working tree | Index | HEAD |
|---|---|---|---|
--soft | не трогает | не трогает | двигает |
--mixed (default) | не трогает | переписывает из commit | двигает |
--hard | переписывает | переписывает | двигает |
--soft- самый безопасный. Передвигает только ветку, всё остальное остаётся как было. Использование: «хочу переделать последний коммит, оставив правки в индексе».--mixed- по умолчанию. Двигает ветку и переписывает индекс. Working tree остаётся как есть. Использование: «отменить несколько коммитов, оставив изменения как unstaged».--hard- переписывает всё. Незакоммиченные правки теряются. Использование: «полностью откатить состояние, потери не страшны».
--hard особенно коварен на новых ветках, у которых нет
аналога в reflog. Если хочешь подстраховаться - сделай тег или
ветку перед reset: git tag backup-before-reset. Потом тег
легко удалить, а если что-то пошло не так - откатиться обратно.
Уроки в sandbox
lab-5.1. Прогнать файл через все три зоны и посмотреть на каждом шаге
Цель - глазами увидеть, что зоны действительно три и команды двигают данные между ними. На каждом шаге проверяем все четыре варианта diff.
Создай новый репозиторий:
mkdir three-areas && cd three-areas && git init.Создай файл и сделай первый коммит, чтобы у HEAD появилось значение:
echo "version 1" > note.txt && git add note.txt && git commit -m "первая версия".Поправь файл:
echo "version 2" > note.txt. Запусти все четыре diff подряд:git diff(покажет правку),git diff --staged(пусто),git diff HEAD(покажет правку),git diff HEAD~0 HEAD(пусто). Сверь с таблицей из главы.Застейджи правку:
git add note.txt. Снова прогоняй все четыре diff:git diff(пусто),git diff --staged(покажет правку),git diff HEAD(покажет правку),git diff HEAD HEAD(пусто). Заметь: правка теперь в индексе, working tree совпадает с индексом.Сделай коммит:
git commit -m "вторая версия". Все четыре diff теперь пустые. Файл проехал все три зоны до конца.Эксперимент с
--amend: поправь файл снова (echo "version 2.1" > note.txt && git add note.txt && git commit --amend --no-edit). Запустиgit log --oneline- увидишь, что коммитов всё ещё два, но второй переписан. Запустиgit reflog- там видна и старая, и новая версия второго коммита.Эксперимент с
reset --softvs--mixed: сделай ещё одну правку, закоммить (echo "version 3" > note.txt && git add note.txt && git commit -m "третья"). Теперьgit reset --soft HEAD~1. Запустиgit status- увидишь правки уже в индексе, готовые к новому коммиту. Откатись:git reset --hard HEAD@{1}(используя reflog). Запустиgit reset --mixed HEAD~1- правки переехали из индекса в working tree.Зафиксируй в голове:
--softничего не теряет,--mixedсдвигает данные на одну зону «вниз»,--hardсбрасывает индекс и tracked-файлы в working tree к состоянию <commit> (untracked файлы при этом не трогает - для них нуженgit clean).
sandbox с автопроверкой - открыть в песочнице
Резюме
- В Git три зоны: working tree (файлы на диске), index/staging (`.git/index`), repository (`.git/objects/`).
- `git add` двигает данные из working tree в индекс. `git commit` - из индекса в репозиторий.
- Индекс позволяет собирать коммит из частей. Без него каждый коммит был бы снимком всего рабочего дерева.
- Таблица «команда × зона» - самый полезный инструмент для отладки запутанных ситуаций в Git.
- У `git diff` четыре основных режима: без флагов (wt ↔ index), `--staged` (index ↔ HEAD), `HEAD` (wt ↔ HEAD), `<A> <B>` (commit ↔ commit).
- У `git reset` три режима: `--soft` (только HEAD), `--mixed` (HEAD + index, по умолчанию), `--hard` (всё). `--hard` теряет незакоммиченные правки безвозвратно.
- Большинство «магических» команд Git становятся понятны, если думать терминами трёх зон и того, что команда читает и куда пишет.
Контрольные вопросы
В чём смысл индекса? Почему нельзя коммитить сразу из рабочего дерева, как в SVN?
Показать ответ
Индекс позволяет выбрать, что именно пойдёт в следующий коммит, а не коммитить всё содержимое рабочей копии скопом. Это даёт три практических плюса: можно разбивать несвязанные изменения на отдельные коммиты (
git add -pпо hunk'ам); можно работать над несколькими вещами параллельно и коммитить их по очереди; можно собирать «чистые» коммиты, в которые не попадают временные правки и debug-выводы из соседних файлов. В SVN всего этого нет - там коммит всегда снимок всего рабочего дерева целиком.Какая команда покажет «всё, что я изменил, но не закоммитил» (включая и застейдженное, и нет)?
Показать ответ
git diff HEAD. Без флагаgit diffсравнивает только working tree с индексом, и не показывает то, что уже застейджено.git diff --staged- наоборот, только то, что застейджено.git diff HEAD- сравнивает working tree (по факту с учётом индекса) с последним коммитом, это и есть полная сумма правок с момента HEAD.Я сделал `git reset --hard HEAD~3`. Можно ли вернуть последние три коммита?
Показать ответ
Да, если за прошедшее время не было
git gc --prune=now. Откройgit reflog- увидишь строки с SHA состояния «до» reset'а. Возьми SHA нужного состояния и сделайgit reset --hard <sha>илиgit branch rescue <sha>. Reflog по умолчанию хранит записи 30 дней, так что времени достаточно. Незакоммиченные правки в рабочей копии, конечно, не вернутся - они никогда не существовали в виде Git-объектов.В чём разница между `git rm <file>` и `git rm --cached <file>`?
Показать ответ
git rm <file>удаляет файл и из индекса, и с диска. Следующий коммит зафиксирует удаление.git rm --cached <file>удаляет только из индекса; файл остаётся на диске как untracked. Полезно когда файл случайно был закоммичен (например,.env), и нужно вычистить его из истории, не удаляя локально. После этого обычно добавляют файл в.gitignore, чтобы он не попал в индекс снова.Что произойдёт с висячим коммитом после `git commit --amend`?
Показать ответ
Старая версия коммита остаётся в
.git/objects/, но никакая ветка на неё больше не указывает. Это «висячий» (dangling) коммит. Он живёт, пока его держит reflog HEAD: для недостижимых записей по умолчанию 30 дней (gc.reflogExpireUnreachable), для достижимых - 90 (gc.reflogExpire). Самgit gcзапускается не по календарю, а либо явно, либо какgc --autoна других командах, когда накопилось достаточно loose-объектов. Пока истечение reflog не прошло -git reflogпокажет SHA старой версии, и её можно восстановить черезgit reset --hard <sha>или создать ветку.