linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
Intro
Lessons
Footer
linuxlab-УчебникиЦеныО платформеКонфиденциальность и куки
Copyright © 2026 LinuxLab. Все права защищены.
linuxlab.io
Учебники▾
  • Линукс и сети
    Файловая система, процессы, TCP/IP, BGP и OSPF
    →
  • Terraform и IaC
    HCL, state, plan/apply на sandbox LocalStack
    →
  • Git и GitHub
    Объектная модель, plumbing, ветвление, GitHub Actions
    →
Все учебники →
ЦеныО платформеВойтиСоздать аккаунт
/
  • Введение
  • Главы
  • How it works
  • Уроки
  • База знаний
  • Собеседование
Часть II — Внутренности Git

$ глава 5 · 45 минут

Три зоны: working tree, индекс, репозиторий

Когда говорят «закоммитил файл», скрывают важную деталь: файл прошёл через три зоны. Сначала был только в рабочей копии, потом попал в индекс, потом - в репозиторий. Понимание того, где именно находится файл в данный момент, отсекает половину запутанных ситуаций в Git.

Этой главой мы закрываем часть II - внутренности Git. Дальше начнётся часть III, ежедневная работа: ветки, слияния, исправления, rebase. Но чтобы все эти команды собирались в голове в стройную картину, надо иметь чёткое представление о трёх зонах, между которыми они двигают данные.

5.1 Что такое зона

Зона в Git - это место, где хранится какая-то версия твоих файлов. Их три:

  1. Working tree (рабочее дерево) - твои файлы на диске, те самые, которые ты правишь в редакторе.
  2. Index (индекс, он же staging area) - промежуточное место, где Git собирает следующий коммит. Двоичный файл .git/index.
  3. 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.
  • хранилище для git stash. Хотя stash сейчас сделан через отдельные коммиты, исторически он жил в индексе.

Поэтому в документации можно встретить «index», «cache» (старое имя), «staging area» - это всё про один и тот же .git/index. В скриптах исторически осталось --cached (git rm --cached, git ls-files --cached) - синоним для «работа с индексом».

5.4 Карта команд: что куда двигает

Самая полезная картинка для отладки запутанных ситуаций - таблица «команда × зона». В каждой ячейке - что команда делает с этой зоной.

КомандаWorking treeIndexRepository
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.

Все варианты можно сужать конкретными путями:

bash
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 treeIndexHEAD
--softне трогаетне трогаетдвигает
--mixed (default)не трогаетпереписывает из commitдвигает
--hardпереписываетпереписываетдвигает
  • --soft - самый безопасный. Передвигает только ветку, всё остальное остаётся как было. Использование: «хочу переделать последний коммит, оставив правки в индексе».
    • --mixed - по умолчанию. Двигает ветку и переписывает индекс. Working tree остаётся как есть. Использование: «отменить несколько коммитов, оставив изменения как unstaged».
    • --hard - переписывает всё. Незакоммиченные правки теряются. Использование: «полностью откатить состояние, потери не страшны».

--hard особенно коварен на новых ветках, у которых нет аналога в reflog. Если хочешь подстраховаться - сделай тег или ветку перед reset: git tag backup-before-reset. Потом тег легко удалить, а если что-то пошло не так - откатиться обратно.

Уроки в sandbox

lab-5.1. Прогнать файл через все три зоны и посмотреть на каждом шаге

Цель - глазами увидеть, что зоны действительно три и команды двигают данные между ними. На каждом шаге проверяем все четыре варианта diff.

  1. Создай новый репозиторий: mkdir three-areas && cd three-areas && git init.

  2. Создай файл и сделай первый коммит, чтобы у HEAD появилось значение: echo "version 1" > note.txt && git add note.txt && git commit -m "первая версия".

  3. Поправь файл: echo "version 2" > note.txt. Запусти все четыре diff подряд: git diff (покажет правку), git diff --staged (пусто), git diff HEAD (покажет правку), git diff HEAD~0 HEAD (пусто). Сверь с таблицей из главы.

  4. Застейджи правку: git add note.txt. Снова прогоняй все четыре diff: git diff (пусто), git diff --staged (покажет правку), git diff HEAD (покажет правку), git diff HEAD HEAD (пусто). Заметь: правка теперь в индексе, working tree совпадает с индексом.

  5. Сделай коммит: git commit -m "вторая версия". Все четыре diff теперь пустые. Файл проехал все три зоны до конца.

  6. Эксперимент с --amend: поправь файл снова (echo "version 2.1" > note.txt && git add note.txt && git commit --amend --no-edit). Запусти git log --oneline - увидишь, что коммитов всё ещё два, но второй переписан. Запусти git reflog - там видна и старая, и новая версия второго коммита.

  7. Эксперимент с reset --soft vs --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.

  8. Зафиксируй в голове: --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 становятся понятны, если думать терминами трёх зон и того, что команда читает и куда пишет.

Контрольные вопросы

  1. В чём смысл индекса? Почему нельзя коммитить сразу из рабочего дерева, как в SVN?

    Показать ответ

    Индекс позволяет выбрать, что именно пойдёт в следующий коммит, а не коммитить всё содержимое рабочей копии скопом. Это даёт три практических плюса: можно разбивать несвязанные изменения на отдельные коммиты (git add -p по hunk'ам); можно работать над несколькими вещами параллельно и коммитить их по очереди; можно собирать «чистые» коммиты, в которые не попадают временные правки и debug-выводы из соседних файлов. В SVN всего этого нет - там коммит всегда снимок всего рабочего дерева целиком.

  2. Какая команда покажет «всё, что я изменил, но не закоммитил» (включая и застейдженное, и нет)?

    Показать ответ

    git diff HEAD. Без флага git diff сравнивает только working tree с индексом, и не показывает то, что уже застейджено. git diff --staged - наоборот, только то, что застейджено. git diff HEAD - сравнивает working tree (по факту с учётом индекса) с последним коммитом, это и есть полная сумма правок с момента HEAD.

  3. Я сделал `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-объектов.

  4. В чём разница между `git rm <file>` и `git rm --cached <file>`?

    Показать ответ

    git rm <file> удаляет файл и из индекса, и с диска. Следующий коммит зафиксирует удаление. git rm --cached <file> удаляет только из индекса; файл остаётся на диске как untracked. Полезно когда файл случайно был закоммичен (например, .env), и нужно вычистить его из истории, не удаляя локально. После этого обычно добавляют файл в .gitignore, чтобы он не попал в индекс снова.

  5. Что произойдёт с висячим коммитом после `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> или создать ветку.

← Предыдущая04-plumbingСледующая →06-branches
Footer
linuxlab-
Copyright © 2026 LinuxLab. Все права защищены.
Учебники
Цены
О платформе
Конфиденциальность и куки