git reset: три дерева и отмена индексации и коммитов

Рейтинг: 45.3% · 9 голосов
Самый подробный курс по Git - от первого коммита до объектной базы и packfiles. Терминал и первый репозиторий, ветки и слияние, конфликты, удалённая работа и форджи (GitHub, GitLab), rebase и переписывание истории, спасение кода через reflog и fsck, stash и worktree, bisect и blame, подмодули и монорепо, хуки и подпись коммитов, внутреннее устройство и масштаб. Для новичков и профи. Актуально на 2026 (Git 2.54, дорожная карта 3.0).
Ответить
Аватара пользователя
Pavel_Git
Сообщения: 52
Зарегистрирован: 11 май 2026, 05:31

git reset: три дерева и отмена индексации и коммитов

Сообщение Pavel_Git »

Оглавление курса (52)
  1. Что такое контроль версий и почему победил Git
  2. Установка Git на Windows, macOS и Linux и первая настройка
  3. Терминал и как спрашивать у Git: выживание в командной строке
  4. Ментальная модель Git: три состояния, коммит-снимок и граф
  5. Первый репозиторий: init, add, commit, status
  6. Просмотр изменений: status, diff и индексация по частям
  7. История проекта: git log, диапазоны и поиск
  8. .gitignore: что не должно попасть в репозиторий
  9. .gitattributes: окончания строк, бинарники и атрибуты
  10. Ветки как указатели: branch, switch и detached HEAD
  11. Слияние веток: fast-forward против трёхстороннего merge
  12. Конфликты слияния: анатомия и уверенное разрешение
  13. Практикум: твой первый день с Git от нуля до push
  14. Удалённые репозитории: remote, fetch, базовый pull и refspec
  15. git push: безопасная отправка и защита от перезаписи
  16. Клонирование: shallow, partial clone, bundle и backfill
  17. GitHub, GitLab и форки: pull request, rulesets и защита веток
  18. Рабочие процессы команды: GitFlow, GitHub Flow, trunk-based
  19. Merge queue и merge trains: безопасное вливание в trunk
  20. Культура code review и Conventional Commits
  21. git reset: три дерева и отмена индексации и коммитов (вы здесь)
  22. git revert: безопасная отмена в общей истории
  23. Правка последнего коммита: git commit --amend
  24. git rebase: пересадка истории и золотое правило
  25. Интерактивный rebase: переписать серию коммитов
  26. git cherry-pick: перенос отдельных коммитов
  27. Stacked diffs: цепочка зависимых PR
  28. Массовое переписывание истории: git filter-repo и git replace
  29. git reflog: журнал, который помнит всё
  30. Восстановление утерянных коммитов, веток и файлов
  31. Откат опасных операций: reset --hard, сломанный rebase, плохой merge
  32. git stash: тайник для незавершённой работы
  33. git worktree: несколько рабочих деревьев одного репозитория
  34. git bisect: бинарный поиск регрессии
  35. git blame, mailmap и археология кода
  36. Теги, релизы, archive и bundle
  37. Подмодули: внешние репозитории внутри проекта
  38. Subtree, монорепозитории и sparse-checkout
  39. Git LFS и большие файлы
  40. Вспомогательные команды: clean, mv, rm, restore
  41. Псевдонимы и кастомизация .gitconfig
  42. Хуки Git: автоматизация на клиенте и сервере
  43. Подпись коммитов и тегов: SSH, GPG и Sigstore
  44. Внутреннее устройство I: объектная база Git
  45. Внутреннее устройство II: ссылки, HEAD, notes и reftable
  46. Внутреннее устройство III: индекс, packfiles, gc и обслуживание
  47. От SHA-1 к SHA-256: переход на новый хеш
  48. Огромные репозитории: partial clone, FSMonitor, Scalar
  49. Git в CI/CD: shallow, кеш, merge_group и безопасность
  50. Куда движется Git: дорожная карта 3.0 и экосистема
  51. Профессиональный setup: собираем рабочее окружение Git
  52. 30+ типичных ошибок Git и как из них выбираться
Сначала про страховку: reset почти всегда обратим

Прежде чем ты тронешь хоть одну команду из этого урока, запомни главное: 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
Видишь HEAD@{1} с SHA 9f8e7d6? Это состояние ветки до reset. Чтобы отменить только что сделанный reset, достаточно git reset --hard HEAD@{1} - и ты снова на двух потерянных коммитах. Поэтому не бойся reset. Бойся только одного: незакоммиченных правок в рабочем каталоге, потому что reflog их не хранит. reflog знает про коммиты, а не про твои несохранённые изменения в файлах. Запомнили это - идём в механику.

Изображение

Три дерева Git: HEAD, индекс, рабочий каталог

Чтобы понять git reset, надо держать в голове, что Git одновременно работает с тремя снимками - их часто называют "три дерева". Это не абстракция из учебника, это буквально три места, куда Git смотрит.
  • HEAD - указатель на последний коммит твоей текущей ветки. Технически HEAD ссылается на ветку (например, refs/heads/main), а та - на конкретный SHA. Это "снимок того, что уже зафиксировано".
  • Индекс (он же staging area, он же кэш) - физический файл .git/index. Это снимок того, что попадёт в следующий коммит. Когда ты делаешь git add, ты копируешь файл из рабочего каталога в индекс.
  • Рабочий каталог (working tree) - реальные файлы на диске, которые ты редактируешь в редакторе. Тут живут все твои несохранённые правки.
Обычный цикл работы - это перекладывание данных между этими тремя деревьями. Правишь файл (меняется рабочий каталог) -> git add (правка едет в индекс) -> git commit (индекс становится новым HEAD). git reset - это команда, которая двигает данные в обратную сторону, и весь фокус в том, до какого из трёх деревьев она дойдёт.

Что именно делает 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)
Прочитай построчно. --soft: отмотал коммиты, но всё их содержимое лежит готовое к новому коммиту. --mixed: отмотал, содержимое в файлах, но надо заново git add. --hard: отмотал и стёр - чистый рабочий каталог на состоянии целевого коммита.

Применение 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
git reset HEAD .env скопировал версию .env из HEAD обратно в индекс - то есть выкинул его из staging. Файл на диске цел, изменения целы, просто он больше не попадёт в коммит. Заметь: когда ты пишешь reset с путём, режимы --soft/--hard запрещены - reset по пути всегда работает как --mixed (трогает индекс, не трогает рабочий каталог). В современном Git (switch/restore стабильны с 2.51) для этой задачи рекомендуют git restore --staged .env - он читается понятнее и не путается с перемещением ветки. Но git reset HEAD <файл> ты встретишь в каждом втором гайде, поэтому знать обязан.

Применение 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
После --soft HEAD~1 коммит "add payment retry" исчез из ветки, но все его изменения лежат в индексе, готовые к новому коммиту. Это идеальный способ переделать сообщение или досыпать файлов: git reset --soft HEAD~1, потом git add чего не хватало, потом git commit заново. Если же сделать git reset HEAD~1 без флага (то есть --mixed), правки окажутся не в индексе, а просто в изменённых файлах - удобно, когда хочешь пересобрать коммит из кусков через git add -p. Историю ты переписал локально, поэтому если коммит уже был запушен - см. грабли ниже.

Применение 3: откатить ветку назад (когда --hard оправдан)

Ветка ушла не туда - наворотил локальных экспериментов, всё надо стереть начисто и встать на чистое состояние. Вот тут git reset hard законен.

Код: Выделить всё

git reset --hard origin/main
HEAD is now at 4c5d6e7 release 2.3.0
Эта команда говорит: "забудь все мои локальные коммиты и правки, сделай ветку точной копией origin/main". Рабочий каталог стёрт до этого состояния. Когда --hard допустим: ты сознательно выбрасываешь локальную работу, и у тебя нет несохранённого, что жалко терять. Как подстраховаться перед --hard:
  • git status - убедись, что нет ценных незакоммиченных правок;
  • git stash - если правки есть, спрячь их (stash переживёт reset);
  • git branch backup-сегодня - поставь метку на текущий HEAD, чтобы коммиты гарантированно остались достижимы и без рытья в reflog.
Сделал ветку-закладку - и любой --hard превращается в безопасную операцию.

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 - двигаю ветку, restore - чиню файл, revert - публично отменяю чужой/общий коммит новым коммитом.

Типичные грабли
  • 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.
Мини-лаба: повтори руками за 5 минут

Создай песочницу и прогони все три режима, наблюдая 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    # всё вернулось через ветку-закладку
Проверь reflog: git reflog покажет каждый шаг и докажет, что любой reset тут обратим.

Контрольные вопросы
  • Какие из трёх деревьев (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 перестаёт быть страшным и становится одним из самых полезных инструментов в твоём арсенале.
👍3 ❤️3 🔥2 😄 🤔
Аватара пользователя
major7
Сообщения: 1
Зарегистрирован: 11 май 2026, 13:29

Re: git reset: три дерева и отмена индексации и коммитов

Сообщение major7 »

Всю жизнь делал git reset --hard зажмурившись и молясь. Закладка через git branch перед хардом - простая штука, а я её не знал. Спасибо, теперь не страшно.
👍1 ❤️2 🔥 😄 🤔
Аватара пользователя
minori
Сообщения: 1
Зарегистрирован: 28 май 2026, 22:49

Re: git reset: три дерева и отмена индексации и коммитов

Сообщение minori »

Вопрос: а если я сделал reset --soft HEAD~3, схлопнул три коммита в один и закоммитил - это считается squash? Или squash это что-то отдельное при merge?
👍1 ❤️1 🔥1 😄 🤔
Ответить
← Предыдущая глава
Культура code review и Conventional Commits
Следующая глава →
git revert: безопасная отмена в общей истории

Все главы курса «Git профессионально: от первого коммита до внутреннего устройства»

Поделиться темой: ✈ Telegram VK
Похожие запросы: git commit: фиксация измененийgit tag: теги и релизыgit blame: кто изменил строкуПодпись коммитов в Git (SSH, GPG)git cherry-pick: перенос коммитовgit worktree: несколько рабочих деревьев

Вернуться в «Git профессионально: от первого коммита до внутреннего устройства»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 2 гостя