Три дерева: working tree, index, repository
Это самый важный урок всего курса. Если вы поймёте концепцию three trees — все остальные модули будут лёгкими. Если не поймёте — каждый раз, когда возникнет нестандартная ситуация (особенно при git reset, git restore, конфликтах), вы будете теряться.
Когда люди говорят «Git сложный», в 90% случаев они на самом деле имеют в виду: «я не понимаю, что такое index и зачем он нужен». В этом уроке разберём концептуально, что такое три дерева, почему так устроено, и какие команды между какими деревьями двигают данные.
Что такое «дерево» в Git
В этом контексте «дерево» означает состояние файловой системы: набор файлов и директорий со своими содержимыми. Не путать с tree-объектом Git (это конкретный тип объекта внутри Git, мы разберём в уроке 5). Здесь «дерево» — это абстрактное состояние.
В Git одновременно существуют три таких состояния (дерева):
Это ключевой концепт. Запомните его навсегда.
Звучит абстрактно? Сейчас разберём каждое дерево в подробностях.
Working Tree: что вы видите глазами
Working tree (рабочее дерево) — это то, что видно в файловом менеджере. Если вы откроете директорию проекта и посмотрите файлы — это и есть working tree. Каждый раз, когда вы редактируете файл в редакторе и сохраняете — вы меняете working tree.
# Создали проект
mkdir my-project && cd my-project
echo "print('hello')" > main.py
echo "# My Project" > README.md
# Working tree:
ls
# main.py README.md
Эти два файла — это working tree. Они физически лежат на диске. Я могу удалить main.py через rm main.py — это меняет working tree. Я могу открыть main.py в редакторе, добавить строку и сохранить — это тоже меняет working tree.
Working tree не зависит от Git. Эти файлы — обычные файлы операционной системы. Git их «знает» (если они в репозитории), но не контролирует.
Index (он же Staging Area): промежуточное состояние
Тут начинается отличие Git от других VCS. В большинстве систем (SVN, Mercurial) есть два состояния: working copy и committed history. В Git между ними есть третье — index.
Index — это «снимок, который вы готовите для следующего commit». Он лежит в файле .git/index (бинарный формат). Содержит список файлов с их SHA-1 хешами.
Зачем это нужно? В CVCS commit просто записывает всё, что есть на диске. В Git вы можете точно выбирать, что попадает в commit:
- У вас есть 5 изменённых файлов
- Логически они относятся к двум разным задачам: 2 файла — баг-фикс, 3 файла — новая фича
- Вы можете сделать
git addтолько на 2 файла для багфикса git commit— commit с только этими 2 файлами в нём- Дальше
git addна остальные 3 файла,git commit— второй коммит для фичи
Это и есть selective commit: вы создаёте логически чистую историю, даже когда в работе много разного одновременно. В SVN такого нет — там commit пишет ровно то, что на диске.
Кроме того, через git add -p (interactive) можно добавлять в index не весь файл целиком, а отдельные куски (hunks) из файла. Это позволяет один файл, в котором два логических изменения, разнести по двум коммитам.
Index — это первое концептуальное место, где junior спотыкаются. Чтобы привыкнуть, представьте index как «корзину», в которую вы кладёте товары перед оплатой. Working tree — это магазин (что есть). Index — корзина (что я готовлю к покупке). Commit — оплата (зафиксировал покупку). Между этими тремя состояниями есть граница.
HEAD: указатель на последний commit
HEAD — это «текущий коммит». Это специальная ссылка в Git, указывающая на коммит, в котором вы сейчас находитесь. Когда вы на ветке main и последний коммит — a1b2c3..., HEAD указывает на a1b2c3....
HEAD физически — это файл .git/HEAD:
cat .git/HEAD
# ref: refs/heads/main
Это указатель на ветку. А ветка — файл с SHA коммита:
cat .git/refs/heads/main
# a1b2c3d4e5f6...
После git commit HEAD автоматически обновляется на новый коммит. После git switch <branch> HEAD меняется на ветку.
В контексте «трёх деревьев» HEAD — это состояние дерева, как было в последнем коммите. Когда мы говорим «сравнить с HEAD», мы имеем в виду: сравнить с тем, что было в последнем коммите.
В упрощённом виде: «HEAD = последний commit, в котором вы находитесь». Когда мы делаем git commit, новый коммит создаётся, HEAD двигается на него.
Как команды перемещают данные между деревьями
Главные команды Git — это перемещения данных между working tree, index и HEAD. Понимание, какая команда что делает — это понимание Git.
Ключевые операции:
git add <file>— копирует файл из working tree в index (готовит к коммиту)git commit— записывает index как новый commit, обновляет HEADgit restore <file>— копирует файл из index в working tree (отменяет изменения в WT)git restore --staged <file>— убирает файл из index (но оставляет в working tree). Эквивалентgit reset HEAD <file>git diff— без аргументов сравнивает WT и index (что изменилось, но ещё не добавлено)git diff --staged— сравнивает index и HEAD (что добавлено, готово к commit)git diff HEAD— сравнивает WT и HEAD (всё изменённое — и в WT, и в index)
Пример: пошаговый обзор
Давайте проследим, как меняются три дерева при типичной работе.
Шаг 1: Создаём файл
mkdir three-trees-demo && cd three-trees-demo
git init
echo "hello world" > a.txt
Состояние:
git status:
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
a.txt
«Untracked» — Git видит файл, но не отслеживает его (не в index).
Шаг 2: git add
git add a.txt
git status:
On branch main
No commits yet
Changes to be committed:
new file: a.txt
Теперь файл в index. Готов к commit.
Шаг 3: git commit
git commit -m "Add a.txt"
git status:
On branch main
nothing to commit, working tree clean
Все три дерева синхронизированы.
Шаг 4: Изменяем файл в WT
echo "hello world, modified" > a.txt
git status:
On branch main
Changes not staged for commit:
modified: a.txt
WT отличается от index. Изменения не staged (не добавлены в index).
Шаг 5: git add, потом ещё изменения в WT
git add a.txt
echo "hello world, modified TWICE" > a.txt
Все три дерева отличаются! git status:
On branch main
Changes to be committed:
modified: a.txt
Changes not staged for commit:
modified: a.txt
«modified» в обеих секциях для одного файла — это означает, что одна версия в index, другая в working tree.
git diff:
diff --git a/a.txt b/a.txt
index ad...
--- a/a.txt
+++ b/a.txt
@@ -1 +1 @@
-hello world, modified
+hello world, modified TWICE
Diff между index и WT.
git diff --staged:
diff --git a/a.txt b/a.txt
index 3b...ad
--- a/a.txt
+++ b/a.txt
@@ -1 +1 @@
-hello world
+hello world, modified
Diff между HEAD и index.
Тот, кто понимает эту картину, понимает Git.
Mental model для запоминания
Запоминать через метафору магазина:
- Working tree — магазин: все товары доступны, можете трогать.
- Index — корзина: то, что вы готовите к оплате. Можете класть в корзину (
git add), возвращать на полку (git restore --staged). - HEAD — чек: финальная фиксация покупки. После оплаты товары в чеке. Следующая покупка начинается с пустой корзины, но магазин полон.
Эта метафора объясняет:
- Зачем нужен index: чтобы выбирать, что вы покупаете. Без него — оплата всего, что трогали в магазине.
- Почему
git addнужен повторно при каждом изменении: вы клали этот товар в корзину раньше, потом взяли его обратно (изменили) — нужно положить заново. - Почему
git commitфиксирует только то, что в index: оплата покупает то, что в корзине, а не всё, что в магазине.
В следующих уроках
В следующих уроках разберём конкретно, что происходит при git init (что появилось в .git/), при git add/git commit/git status (детально), что такое anatomy of commit (объект внутри), и какие объекты вообще существуют (blob, tree, commit, tag).
Но если из этого урока вы вынесли «три дерева, три состояния, между ними команды двигают данные» — фундамент заложен. Все остальные модули будут на этом фундаменте.
Попробуй сам
Проверьте понимание на собственной машине. Создайте песочницу и пройдите пошагово:
mkdir -p ~/git-sandbox/lesson-03-trees
cd ~/git-sandbox/lesson-03-trees
git init
# Шаг 1: новый файл — только в WT
echo "first" > a.txt
git status # Untracked
# Шаг 2: добавили в index
git add a.txt
git status # Changes to be committed
# Шаг 3: коммит — все три синхронизированы
git commit -m "Add a.txt"
git status # nothing to commit
# Шаг 4: изменили — отличается только WT от index
echo "second" > a.txt
git status # Changes not staged
git diff # покажет diff WT vs index
# Шаг 5: добавили в index, изменили снова в WT
git add a.txt
echo "third" > a.txt
git status # И modified, и Changes to be committed
git diff # WT vs index: second -> third
git diff --staged # index vs HEAD: first -> second
# Шаг 6: коммит — теперь HEAD == index, но WT отличается
git commit -m "Update to second"
git status # Changes not staged (только WT отличается)
Прокатите всю последовательность, посмотрите вывод каждой команды. Если в какой-то момент git status неочевиден — вернитесь к схеме трёх деревьев.
ETL: staging-зона как ключевой паттерн