Ты сохранил файл. Нажал Ctrl+S, увидел, что точка в заголовке окна пропала - значит, записалось на диск. И вот тут новичок спотыкается о первую и самую коварную стену Git: для Git твой сохранённый файл всё ещё не существует. Сохранение на диск и сохранение в историю - две разные вселенные. Ты можешь редактировать файл хоть сто раз, а Git будет упрямо твердить: ничего не закоммичено, изменения потеряются. Почему? Потому что между диском и историей стоит барьер, который называется индекс (он же staging area). Эта глава - про то, как создать репозиторий git с нуля, как продавить через него файл и почему между add и commit есть смысл. Разберём четыре команды, на которых стоит всё остальное: git init, git status, git add, git commit. И главное - выработаем привычку, которая спасёт тебя тысячу раз: смотреть в компас после каждого шага.

git init: что появляется в .git
Создать git репозиторий - это одна команда. Заходишь в пустую папку проекта и пишешь:
Код: Выделить всё
$ mkdir hello && cd hello
$ git init
Initialized empty Git repository in /home/dev/hello/.git/
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
Код: Выделить всё
$ git config --global init.defaultBranch main
Код: Выделить всё
$ ls -F .git/
HEAD config description hooks/ info/ objects/ refs/
$ cat .git/HEAD
ref: refs/heads/main
- HEAD - указатель "где я сейчас". Это текстовый файл, в нём строчка ref: refs/heads/main. То есть HEAD показывает на ветку main. Сама ветка ещё не существует как файл - её создаст только первый коммит.
- objects/ - хранилище всех данных: содержимое файлов, деревья каталогов, коммиты. Сейчас пусто. Каждый раз, когда ты коммитишь, сюда добавляются объекты.
- refs/heads/ - тут лежат ветки. Каждая ветка - это файлик с одним хешем коммита внутри. Пока пусто.
- config - локальные настройки именно этого репозитория.
- hooks/ - скрипты-перехватчики (про них позже в курсе).
git status - главный компас новичка
Прежде чем что-то делать, заведи рефлекс: после каждой команды смотри git status. Это не команда "на всякий случай", это твой навигатор. Он отвечает на три вопроса разом: на какой я ветке, что изменилось, и что попадёт в следующий коммит. Спроси его сразу после init:
Код: Выделить всё
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
nothing added to commit but untracked files present (use "git add" to track)
Код: Выделить всё
$ echo "print('hello')" > app.py
$ git status
On branch main
No commits yet
Untracked files:
(use "git add <file>..." to include in what will be committed)
app.py
nothing added to commit but untracked files present (use "git add" to track)
Индекс как барьер номер один: метафора до всякого add
Здесь спотыкается каждый. Давай разберёмся по-человечески, ПОЧЕМУ Git не коммитит твои файлы сразу, а требует какой-то лишний шаг.
В Git между рабочим каталогом (твои файлы на диске) и историей (коммиты) стоит промежуточный слой - индекс, он же staging area, он же "индекс подготовленных изменений". Это не файлы и не история. Это черновик твоего следующего коммита. Физически он лежит в .git/index - один бинарный файл, список того, что войдёт в коммит, когда ты нажмёшь "отправить".
Метафора первая - корзина в интернет-магазине. Полки магазина - это весь твой рабочий каталог, все файлы. Корзина - это индекс. Касса - это коммит. Ты ходишь по магазину и кидаешь в корзину ровно то, что хочешь купить именно сейчас. Положил молоко и хлеб в корзину (git add) - но колбасу оставил на полке, передумал. На кассе (git commit) ты платишь только за содержимое корзины. Молоко с хлебом уходят в чек (в историю), колбаса остаётся лежать на полке нетронутой. Без корзины касса не знает, что ты вообще хочешь купить - именно поэтому add нужен, даже если файл давно сохранён на диск.
Метафора вторая, для тех, кто пишет письма. Рабочий каталог - это черновик, который ты правишь. Индекс - это то, что ты уже вставил в окно нового письма, готовое к отправке. Коммит - кнопка "Отправить". Ты можешь дописывать черновик сколько угодно, но получатель увидит только то, что было в письме в момент нажатия "Отправить". Скопировал в письмо два абзаца из черновика (add), отправил (commit) - ушли только они.
Зачем вообще этот лишний слой, почему нельзя коммитить всё сразу? Потому что он даёт тебе контроль над тем, ЧТО войдёт в коммит. Ты поправил три разных вещи в проекте, но хочешь сделать два аккуратных коммита по смыслу. Индекс позволяет собрать в один коммит только связанные изменения, а остальное оставить на потом. Это основа чистой истории, и позже, в уроке про git add -p, мы научимся складывать в корзину даже не файлы целиком, а отдельные куски одного файла. Но фундамент - вот этот: диск, корзина, чек. Три состояния, три разных места.
git add: из рабочего каталога в индекс
Кладём файл в корзину:
Код: Выделить всё
$ git add app.py
$ git status
On branch main
No commits yet
Changes to be committed:
(use "git rm --cached <file>..." to unstage)
new file: app.py
Состояния по шагам, чтобы картинка в голове была чёткой:
- Шаг 0 (создали файл): диск - есть, индекс - пусто. Status: Untracked, красный.
- Шаг 1 (git add): диск - есть, индекс - есть. Status: Changes to be committed, зелёный, new file.
Код: Выделить всё
$ echo "# v2" >> app.py
$ git status
Changes to be committed:
new file: app.py
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
modified: app.py
git commit: из индекса в историю
Догнали корзину и пробиваем чек:
Код: Выделить всё
$ git add app.py
$ git commit -m "Добавил точку входа app.py"
[main (root-commit) a1b2c3d] Добавил точку входа app.py
1 file changed, 2 insertions(+)
create mode 100644 app.py
- main (root-commit) - коммит лёг в ветку main. root-commit значит, что это самый первый коммит, у него нет родителя. Заодно этим коммитом наконец-то создался файл .git/refs/heads/main.
- a1b2c3d - короткий хеш коммита (у тебя будет другой). Полный хеш - сорок символов, это адрес коммита в истории.
- 1 file changed, 2 insertions(+) - статистика. Один файл, две добавленные строки.
Код: Выделить всё
$ git status
On branch main
nothing to commit, working tree clean
Флаг -m задаёт сообщение прямо в строке. Если написать git commit без -m, Git откроет редактор (по дефолту тот, что в core.editor, часто vi или nano), чтобы ты набрал сообщение там. Удобно для длинных сообщений с телом. Если в редакторе сохранить пустое сообщение и выйти, Git отменит коммит - "Aborting commit due to empty commit message". Сменить редактор на привычный можно один раз:
Код: Выделить всё
$ git config --global core.editor "nano"
git clone: когда репозиторий уже есть
Не всегда ты создаёшь репозиторий с нуля. Часто он уже живёт на GitHub или GitLab, и тебе надо забрать его к себе. Для этого git clone:
Код: Выделить всё
$ git clone https://github.com/owner/project.git
Cloning into 'project'...
remote: Enumerating objects: 128, done.
remote: Counting objects: 100% (128/128), done.
Receiving objects: 100% (128/128), 42.10 KiB, done.
Resolving deltas: 100% (50/50), done.
Типичные грабли новичка
- Забыл add. Поправил файл, сразу git commit -m "...", а в ответ "nothing to commit, working tree clean" или "no changes added to commit". Git закоммитил пустоту или вообще ничего: изменения остались на диске, в корзину ты их не положил. Лечится взглядом в git status ДО коммита. Видишь красное и "Changes not staged" - значит, надо add.
- Identity не настроен. Первый в жизни коммит часто падает так:
Код: Выделить всё
$ git commit -m "first"
Author identity unknown
*** Please tell me who you are.
Run
git config --global user.email "you@example.com"
git config --global user.name "Your Name"
Код: Выделить всё
$ git config --global user.name "Ivan Petrov"
$ git config --global user.email "ivan@example.com"
- Пустой коммит по ошибке. git commit без изменений в индексе ничего не создаст и поругается. Если коммит без изменений нужен осознанно (например, дёрнуть CI) - есть флаг --allow-empty, но это редкий специальный случай, не дефолт.
- Закоммитил мусор. git add . хватает всё подряд, включая лог-файлы, секреты, папки сборки. Привычка смотреть git status перед add спасает от того, чтобы случайно положить в корзину то, чему там не место. Фильтр через .gitignore - тема ближайшего урока.
Пройди весь цикл сам, и обязательно вызывай git status после КАЖДОГО шага - смотри, как меняется компас.
Код: Выделить всё
# 1. Настрой identity и дефолтную ветку (один раз на машину)
git config --global user.name "Твоё Имя"
git config --global user.email "ty@example.com"
git config --global init.defaultBranch main
# 2. Создай репозиторий
mkdir lab-git && cd lab-git
git init
git status # On branch main, No commits yet
# 3. Создай файл и посмотри состояние untracked
echo "hello git" > readme.txt
git status # readme.txt красный, Untracked
# 4. Положи в индекс (корзину) и сравни вывод
git add readme.txt
git status # readme.txt зелёный, Changes to be committed, new file
# 5. Закоммить и проверь чистое дерево
git commit -m "Добавь readme проекта"
git status # nothing to commit, working tree clean
# 6. Поправь файл, посмотри modified, повтори add+commit
echo "вторая строка" >> readme.txt
git status # modified, Changes not staged (красный)
git add readme.txt
git commit -m "Дополни readme второй строкой"
# 7. Загляни внутрь репозитория
cat .git/HEAD # ref: refs/heads/main
ls .git/refs/heads/ # main
Контрольные вопросы
- Файл сохранён на диск, но git status показывает его красным в разделе Untracked. Почему Git не возьмёт его в коммит автоматически и какая команда это исправит?
- Ты сделал git add app.py, потом ещё раз отредактировал app.py. Status показывает файл в двух разделах сразу. Что попадёт в коммит, если сделать commit прямо сейчас, и почему?
- Что конкретно лежит в .git/HEAD сразу после git init и почему файла .git/refs/heads/main ещё нет до первого коммита?
- Чем git clone отличается от git init и какие шаги он делает за тебя одной командой?
Четыре команды - и ты уже умеешь главное. git init создаёт .git, и весь репозиторий живёт там. git status - твой компас, смотри в него после каждого шага, это не паранойя, а профессиональная привычка. Между диском и историей стоит индекс - корзина покупок, черновик письма, отдельный слой, который ты наполняешь через git add и пробиваешь через git commit. Жизненный цикл любого файла: untracked -> staged -> committed, и красный/зелёный цвет в status всегда честно говорит, на какой ты стадии. А git clone забирает готовый репозиторий целиком. Дальше будет глубже, но фундамент - вот этот, и он никуда не денется до самого конца курса.