Три состояния файла: как устроен git на самом деле
Главное, что отличает Git от копирования папок и от старых систем - у каждого файла в твоём проекте есть три места, где он может жить одновременно, и в каждом из них может быть своя версия. Это и есть знаменитые три состояния.
- Рабочий каталог (working directory) - обычные файлы на диске, которые ты редактируешь в редакторе. То, что видишь в проводнике. Тут ты ломаешь, чинишь, экспериментируешь.
- Индекс (staging area, он же staging) - промежуточная зона. Сюда ты складываешь то, что хочешь положить в следующий коммит. Это твоя "корзина перед кассой": набрал, проверил, потом оплатил.
- Репозиторий (HEAD) - зафиксированная история. Тут лежат коммиты - снимки, которые уже сделаны и никуда не денутся.
Код: Выделить всё
git add file.txtКод: Выделить всё
git commitЗачем вообще нужна эта средняя зона, индекс? Почему нельзя коммитить прямо из рабочего каталога, как делает SVN? Затем, что индекс даёт тебе контроль над тем, что именно войдёт в коммит. Ты поправил три файла, но логически это две разные задачи. Ты добавляешь в индекс только два файла по первой задаче, коммитишь их с понятным сообщением, потом добавляешь третий и коммитишь отдельно. Рабочий каталог при этом не трогался. Staging area - это не бюрократия, это инструмент для аккуратных, атомарных коммитов. На поздних уроках мы будем добавлять в индекс даже отдельные куски одного файла через
Код: Выделить всё
git add -p
Смотрим состояния глазами: git status и diff
Слова словами, давай увидим три состояния вживую. Создадим репозиторий и поиграем.
Код: Выделить всё
$ git init demo
Initialized empty Git repository in /home/user/demo/.git/
$ cd demo
$ git switch -c main
Switched to a new branch 'main'
$ echo "привет" > readme.txt
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
readme.txt
nothing added to commit but untracked files present (use "git add" to track)
Код: Выделить всё
git initКод: Выделить всё
git switch -c mainКод: Выделить всё
$ git add readme.txt
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: readme.txt
Код: Выделить всё
$ echo "вторая строка" >> readme.txt
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: readme.txt
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
modified: readme.txt
- - показывает разницу между рабочим каталогом и индексом (что ты ещё не добавил).
Код: Выделить всё
git diff - - показывает разницу между индексом и последним коммитом (что уйдёт в коммит).
Код: Выделить всё
git diff --staged
Коммит - это снимок, а не разница (git снимок против SVN)
Теперь самое контринтуитивное и самое важное. Что такое commit? Большинство приходит из мира, где система версий хранит разницы (дельты): "в этой ревизии добавили строку 5, удалили строку 12". Так работал SVN, так работал CVS. Git устроен принципиально иначе.
Коммит в Git - это снимок (snapshot) всего проекта целиком на момент фиксации, плюс ссылка на родительский коммит, плюс метаданные (автор, дата, сообщение). Не разница. Снимок.
Звучит расточительно: неужели Git хранит полную копию всех файлов в каждом коммите? На пальцах - да, концептуально каждый коммит знает полное состояние проекта. Но Git не дурак: если файл между коммитами не менялся, новый снимок просто ссылается на ту же самую версию файла, что и раньше, а не дублирует её. Как именно это сделано - через хеши и объекты - мы разберём в модуле внутренностей. Сейчас держи в голове правильную интуицию: коммит = полная фотография проекта, а экономия места - забота Git, не твоя.
Почему снимки лучше дельт? Несколько причин, которые ты почувствуешь руками:
- Скорость. Чтобы показать тебе состояние проекта на любом коммите, Git не пересобирает его, складывая сотни дельт от начала истории. Он просто берёт готовый снимок. Переключение веток поэтому почти мгновенное.
- Целостность. Снимок зафиксирован весь и сразу. Нет ситуации "дельта 47 побилась, и всё после неё нечитаемо".
- Простота слияний. Когда Git мержит ветки, он сравнивает три снимка (две ветки и их общий предок) - это надёжнее, чем накатывать цепочки дельт.
Граф коммитов git: ветка - это указатель, HEAD - где я сейчас
Коммиты не висят в воздухе - они связаны. Каждый коммит (кроме самого первого) хранит ссылку на своего родителя. Сделал коммит A, потом коммит B - B указывает на A как на родителя. Получается цепочка, направленная назад во времени. Это и есть граф коммитов git. В общем случае это не просто линия, а DAG - направленный ациклический граф (когда есть ветвления и слияния, у коммита может быть два родителя), но начинать всегда удобнее с простой цепочки.
Введём единую легенду графа, которую будем использовать весь курс. Запомни её сейчас раз и навсегда:
- Кружок (узел) - это коммит-снимок.
- Стрелка - связь "коммит указывает на своего родителя". Стрелка всегда смотрит назад, в прошлое.
- Ярлык-узел (прямоугольная метка, привязанная к кружку) - это ветка, HEAD или тег. Указатель на конкретный коммит.
Код: Выделить всё
main
|
C1 <---- C2 <---- C3
^
HEAD
Ветка в Git - это не папка с файлами и не копия проекта. Ветка - это просто подвижный указатель на один коммит. Лёгкий ярлык, ничего больше.
Создать ветку = создать ещё один такой ярлык (это операция в доли миллисекунды, поэтому ветки в Git дешёвые, в отличие от SVN, где ветка - это копирование). Сделать коммит на ветке = создать новый снимок и передвинуть ярлык ветки вперёд, на свежий коммит.
А что такое HEAD? Это специальный указатель "где я сейчас". Обычно HEAD указывает не прямо на коммит, а на ветку ("я сейчас на ветке main"). Когда ты коммитишь, двигается ветка, а HEAD едет вместе с ней, потому что прикреплён к ней. Посмотрим реально:
Код: Выделить всё
$ git log --oneline --decorate
a1b2c3d (HEAD -> main) добавил вторую строку
9f8e7d6 первый коммит
Мини-глоссарий: HEAD, индекс, ветка, ref, upstream, base
Эти шесть слов будут звучать в каждом уроке. Зафиксируем короткие, честные определения, без воды.
- HEAD - указатель "где я сейчас". Почти всегда указывает на текущую ветку, а через неё - на коммит. Твоё "ты находишься здесь" на карте истории.
- Индекс (staging area) - промежуточная зона между рабочим каталогом и репозиторием. Черновик следующего коммита. Физически лежит в файле .git/index.
- Ветка (branch) - подвижный лёгкий указатель на коммит. Не копия файлов. Двигается вперёд при каждом коммите.
- Ref (ссылка) - общее имя для любого указателя на коммит. Ветка - это ref, тег - это ref, HEAD - тоже ref. Ветки лежат под именами вида refs/heads/main. Считай ref "именованной закладкой".
- Upstream (вышестоящая ветка) - удалённая ветка, которую отслеживает твоя локальная. Например, локальная main отслеживает origin/main. Именно по upstream Git понимает, куда пушить и насколько ты отстал или опередил ("ahead 2, behind 1").
- Base (база, merge base) - общий предок двух веток. Тот последний коммит, от которого ветки разошлись. Git опирается на base при слияниях и rebase, чтобы понять, что в каждой ветке поменялось относительно общей точки.
Типичные грабли ментальной модели
- "Я сделал git add, теперь можно править файл, изменения подхватятся". Нет. add делает снимок файла в индекс в момент команды. Поправил после add - правка осталась только в рабочем каталоге, в коммит не попадёт, пока не сделаешь add ещё раз. Мы это видели выше в git status: один файл в двух зонах.
- "Ветка - это отдельная папка/копия проекта". Нет. Ветка - ярлык на коммит. Все ветки делят одну рабочую папку, ты просто переключаешь, какой снимок в ней разложен.
- "Коммит хранит мои изменения (дельту)". Нет, коммит хранит полный снимок состояния. "Изменения" - это то, что Git вычисляет на лету, сравнивая два снимка, когда ты просишь git diff или git show.
- "HEAD - это последний коммит". Не совсем. HEAD - это где ты сейчас. Обычно это вершина текущей ветки, но если ты переключился на старый коммит, HEAD будет там, а вершина ветки останется впереди.
Открой терминал и повтори по шагам. Цель - своими глазами увидеть, как файл движется рабочий каталог -> индекс -> репозиторий.
- Шаг 1. Создай чистый репозиторий и ветку main:
Код: Выделить всё
git init lab && cd lab && git switch -c main - Шаг 2. Создай файл и посмотри статус - файл untracked, он только в рабочем каталоге:
Код: Выделить всё
echo "строка 1" > notes.txt git status - Шаг 3. Положи файл в индекс и проверь, что он "Changes to be committed":
Код: Выделить всё
git add notes.txt git status - Шаг 4. Дополни файл и снова глянь статус. Файл должен появиться в обеих секциях - в индексе старая версия, в рабочем каталоге новая:
Код: Выделить всё
echo "строка 2" >> notes.txt git status - Шаг 5. Сравни два diff и почувствуй разницу зон:
Код: Выделить всё
git diff git diff --staged - Шаг 6. Добавь свежую версию и сделай снимок-коммит:
Код: Выделить всё
git add notes.txt git commit -m "первые заметки" - Шаг 7. Посмотри граф и найди легенду в выводе - кружок-коммит и ярлыки HEAD и main:
Код: Выделить всё
git log --oneline --decorate --graph
Контрольные вопросы
- Чем индекс (staging area) отличается от рабочего каталога, и зачем вообще нужна эта промежуточная зона?
- Коммит хранит снимок всего проекта или только разницу с прошлым коммитом? Какое практическое преимущество даёт выбранный Git подход?
- Что физически представляет собой ветка в Git и почему создание ветки - почти бесплатная операция?
- Что такое HEAD и чем "HEAD указывает на main" отличается от "detached HEAD"?
Ты собрал ментальную модель git, на которую будет опираться весь курс. Три состояния: рабочий каталог (правишь), индекс (готовишь следующий коммит), репозиторий (зафиксированная история), и изменения движутся между ними через add и commit. Коммит - это снимок всего проекта плюс ссылка на родителя, а не дельта, и отсюда растёт вся скорость и надёжность Git. История - это граф из коммитов-кружков со стрелками на родителей, ветка - лёгкий подвижный ярлык на коммит, HEAD - "ты находишься здесь". Эту легенду графа (кружок, стрелка, ярлык) мы пронесём через весь курс. Дальше будет только конкретнее: следующие уроки - про коммиты, ветки и переключение на практике, а в модуле внутренностей вскроем, как снимки превращаются в хеши и объекты.