Прежде чем ты тронешь хоть одну команду из этого урока, запомни главное: git reset практически всегда обратим. У Git есть встроенная сеть безопасности - reflog (подробно разберём в уроке 28). Каждый раз, когда HEAD или ветка двигаются - коммит, reset, rebase, merge, checkout - Git пишет строчку в журнал перемещений. Это значит, что даже после самого жёсткого reset ты обычно можешь посмотреть, где ветка стояла раньше, и вернуться туда.
Код: Выделить всё
git reflog
a1b2c3d (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
9f8e7d6 HEAD@{1}: commit: add payment retry
4c5d6e7 HEAD@{2}: commit: wip cart
a1b2c3d HEAD@{3}: commit: base layout

Три дерева Git: HEAD, индекс, рабочий каталог
Чтобы понять git reset, надо держать в голове, что Git одновременно работает с тремя снимками - их часто называют "три дерева". Это не абстракция из учебника, это буквально три места, куда Git смотрит.
- HEAD - указатель на последний коммит твоей текущей ветки. Технически HEAD ссылается на ветку (например, refs/heads/main), а та - на конкретный SHA. Это "снимок того, что уже зафиксировано".
- Индекс (он же staging area, он же кэш) - физический файл .git/index. Это снимок того, что попадёт в следующий коммит. Когда ты делаешь git add, ты копируешь файл из рабочего каталога в индекс.
- Рабочий каталог (working tree) - реальные файлы на диске, которые ты редактируешь в редакторе. Тут живут все твои несохранённые правки.
Что именно делает git reset под капотом
git reset <commit> выполняет до трёх шагов, и режим (--soft, --mixed, --hard) определяет, на каком шаге Git остановится.
Шаг 1 (всегда). Git берёт файл .git/refs/heads/<твоя-ветка> и переписывает в нём SHA на тот коммит, что ты указал. Это и есть "перемещение ветки". Заметь: двигается ветка, на которую смотрит HEAD, а не сам HEAD напрямую (если ты не в detached-состоянии). После этого шага история "обрезана" - старый коммит больше не достижим по ветке, но жив в reflog. На этом останавливается --soft.
Шаг 2 (--mixed и --hard). Git перезаписывает файл .git/index, делая его копией нового HEAD. То есть индекс "забывает" всё, что было заиндексировано, и становится таким, каким был бы сразу после checkout этого коммита. На этом останавливается --mixed - и это режим по умолчанию.
Шаг 3 (только --hard). Git проходит по рабочему каталогу и приводит отслеживаемые файлы в соответствие с индексом - то есть физически перезаписывает файлы на диске. Вот здесь и происходит потеря несохранённых правок.
Ключевая мысль: чем "мягче" режим, тем меньше деревьев затронуто и тем безопаснее команда. --soft трогает только refs/heads. --hard трогает всё, включая твои файлы на диске.
Наглядная таблица: что куда двигается
Держи эту таблицу под рукой - она отвечает на 90% вопросов про git reset hard, git reset soft и дефолтный режим.
Код: Выделить всё
Режим | HEAD/ветка | Индекс | Рабочий каталог | Где окажутся правки
-----------|------------|-----------|-----------------|--------------------------
--soft | двигает | НЕ трогает| НЕ трогает | в индексе (staged)
--mixed | двигает | сбрасывает| НЕ трогает | в рабочем кат. (unstaged)
(дефолт) | | | |
--hard | двигает | сбрасывает| перезаписывает | УНИЧТОЖЕНЫ (нет в reflog)
Применение 1: убрать файл из индекса (git reset HEAD)
Самый частый кейс новичка. Ты сделал git add . и понял, что зацепил лишний файл - например, .env с секретами. Как убрать из индекса git, не теряя сам файл? Вот тут работает форма git reset с путём.
Код: Выделить всё
git add .
git status
On branch main
Changes to be staged:
new file: .env
modified: src/app.py
git reset HEAD .env
Unstaged changes after reset:
M .env
git status
Changes to be committed:
modified: src/app.py
Untracked files:
.env
Применение 2: отменить коммит git, сохранив изменения
Закоммитил рано, забыл файл, или хочешь пересобрать историю красивее. Чтобы отменить коммит git и оставить правки - это --soft или --mixed по HEAD~1.
Код: Выделить всё
git log --oneline
9f8e7d6 (HEAD -> main) add payment retry
a1b2c3d base layout
git reset --soft HEAD~1
git status
Changes to be committed:
modified: src/payment.py
Применение 3: откатить ветку назад (когда --hard оправдан)
Ветка ушла не туда - наворотил локальных экспериментов, всё надо стереть начисто и встать на чистое состояние. Вот тут git reset hard законен.
Код: Выделить всё
git reset --hard origin/main
HEAD is now at 4c5d6e7 release 2.3.0
- git status - убедись, что нет ценных незакоммиченных правок;
- git stash - если правки есть, спрячь их (stash переживёт reset);
- git branch backup-сегодня - поставь метку на текущий HEAD, чтобы коммиты гарантированно остались достижимы и без рытья в reflog.
reset vs revert vs restore: не путай инструменты
Три похожие на слух команды решают разные задачи.
- git reset - двигает ветку назад и переписывает историю. Локальный инструмент. Для общих, уже запушенных веток опасен.
- git revert - НЕ трогает историю, а создаёт новый коммит, который отменяет изменения старого. Безопасен для публичных веток: история растёт вперёд, никто не теряет коммиты. Откатываешь зарелизенное - бери revert, а не reset.
- git restore (и legacy git checkout <path>) - восстанавливает содержимое файлов, не двигая ветку вообще. git restore <файл> вернёт файл к версии из индекса, git restore --source=HEAD~1 <файл> - к версии из конкретного коммита.
Типичные грабли
- reset --hard на запушенной ветке + force-push. Ты отмотал и сделал git push --force - коллеги, у кого были твои старые коммиты, получат расхождение, а кто-то затрёт чужую работу. Если уж переписываешь общую историю, бери git push --force-with-lease: он откажется пушить, если кто-то залил новое поверх тебя.
- Думаешь, что reflog спасёт от потери файлов. Нет. reflog хранит коммиты. Несохранённые правки в рабочем каталоге при --hard исчезают без следа - их не было в reflog, потому что они не были закоммичены. Перед --hard - git status и git stash.
- Путаница reset HEAD <file> и reset <commit>. С путём reset работает как --mixed по индексу (снимает индексацию). Без пути - двигает ветку. Один и тот же глагол, разное поведение - смотри, есть ли путь в конце.
- reset --soft и думаешь, что файлы пропали. Они в индексе. git status сразу покажет "Changes to be committed". Ничего не потеряно.
- HEAD~1 против HEAD^1. HEAD~1 - первый родитель на одно поколение назад (то, что почти всегда нужно). HEAD^2 - второй родитель merge-коммита. Для линейной отмены коммита бери HEAD~1.
Создай песочницу и прогони все три режима, наблюдая git status после каждого.
Код: Выделить всё
git init reset-lab && cd reset-lab
git config init.defaultBranch main
printf "v1\n" > file.txt && git add file.txt && git commit -m "c1"
printf "v2\n" >> file.txt && git add file.txt && git commit -m "c2"
printf "v3\n" >> file.txt && git add file.txt && git commit -m "c3"
git log --oneline # видишь c3 c2 c1
# 1) soft: отмотать c3, изменения остаются в индексе
git reset --soft HEAD~1
git status # file.txt в "Changes to be committed"
git reset --soft HEAD@{1} # вернуть c3 обратно через reflog
# 2) mixed (дефолт): изменения в рабочем каталоге, не в индексе
git reset HEAD~1
git status # file.txt "not staged for commit"
git add file.txt && git commit -m "c3"
# 3) hard: ставим закладку, потом отматываем начисто
git branch safety
git reset --hard HEAD~2
git log --oneline # остался только c1
git reset --hard safety # всё вернулось через ветку-закладку
Контрольные вопросы
- Какие из трёх деревьев (HEAD/ветка, индекс, рабочий каталог) трогает git reset --mixed, а какие оставляет нетронутыми?
- Ты сделал git add секретного файла. Какой именно командой убрать его из индекса git, не теряя файл и его содержимое?
- Ты запушил коммит в общую ветку и хочешь его отменить. Почему здесь нельзя git reset, и что брать вместо него?
- Что хранит reflog, а что НЕ хранит - и почему перед git reset --hard полезно сделать git stash или git branch?
git reset - это рычаг, двигающий данные обратно по трём деревьям. --soft двигает только ветку (правки в индексе), --mixed (дефолт) ещё и сбрасывает индекс (правки в файлах), --hard добивает до рабочего каталога и стирает несохранённое. С путём reset снимает индексацию (git reset HEAD <файл>), без пути - откатывает ветку. Пока ты помнишь, что коммиты живут в reflog, а незакоммиченные файлы - нет, reset перестаёт быть страшным и становится одним из самых полезных инструментов в твоём арсенале.