Learning Platform
Глоссарий Troubleshooting
Урок 11.02 · 30 мин
Начальный
resetsoftmixedhardHEADthree-treesrewrite-history

git reset — двигаем HEAD по истории

git reset — одна из самых пугающих команд для junior. Из-за --hard репутация у неё «сжигает работу». На самом деле reset — это просто способ сказать ветке: «теперь ты указываешь на другой коммит». Сколько ущерба он нанесёт твоему working tree — зависит от того, какой из трёх режимов ты выбрал.

В этом уроке разберём, что точно меняет каждый режим, как мысленно представлять разницу между soft/mixed/hard, и почему --hard на самом деле не так страшен (потому что есть reflog — урок 04).


Базовая ментальная модель

Команда git reset <mode> <ref> делает до трёх действий по порядку:

Что меняет каждый mode reset
—soft--soft: только указатель ветки
двигает HEADСдвигает HEAD (и текущую ветку) на указанный коммит
—mixed (default)--mixed: default режим
двигает HEAD + перезаписывает indexСдвигает HEAD + перезаписывает index новым HEAD
—hard--hard: самый разрушительный
двигает HEAD + index + working treeHEAD + index + working tree — всё синхронизируется с указанным коммитом

Главное правило: чем выше в этом списке режим, тем меньше он трогает. --soft — самый безопасный, ничего не уничтожает в файлах. --hard — самый разрушительный, может стереть несохранённое.


Setup: общий пример для всех режимов

Создадим репо с тремя коммитами:

$ git init reset-demo && cd reset-demo
$ echo "v1" > pipeline.py && git add . && git commit -m "C1: initial"
$ echo "v2" > pipeline.py && git commit -am "C2: add transform"
$ echo "v3" > pipeline.py && git commit -am "C3: add validation"

$ git log --oneline
ghi9012 (HEAD -> main) C3: add validation
def5678 C2: add transform
abc1234 C1: initial

Хотим откатиться на C1 (через HEAD~2 или хэш abc1234). Сравним, что произойдёт в трёх режимах.


Режим 1: —soft (только HEAD)

$ git reset --soft HEAD~2

$ git log --oneline
abc1234 (HEAD -> main) C1: initial

$ cat pipeline.py
v3                                  # working tree не тронули!

$ git status
On branch main
Changes to be committed:
  modified:   pipeline.py           # изменения из C2+C3 теперь в index

Что произошло:

  1. HEAD сдвинули с C3 на C1. Коммиты C2 и C3 «осиротели» — больше не на ветке main.
  2. Index не тронули — он по-прежнему содержит снэпшот из C3.
  3. Working tree не тронули — на диске всё ещё v3.

Результат: разница между «текущим состоянием» (v3 на диске и в index) и «новым HEAD» (C1, где v1) теперь висит в index как готовое к новому коммиту изменение.

TIP

git reset --soft HEAD~N — это способ «сжать» N коммитов в один. Откатили HEAD, все накопленные изменения остались в index, делаем git commit -m "Merged feature" — получили один коммит вместо N. Альтернатива interactive rebase squash из модуля 8.

Когда юзать —soft

  • Сжать последние N коммитов в один (squash до push).
  • Откатиться, не теряя ни строчки изменений: всё остаётся в index, можно перекоммитить с правильным message.
  • «Я закоммитил в неправильную ветку» — git reset --soft HEAD~1, переключиться на правильную ветку, закоммитить.

Режим 2: —mixed (default — HEAD + index)

--mixed — режим по умолчанию. git reset HEAD~2 без флага = git reset --mixed HEAD~2.

# Сначала вернёмся к C3, чтобы повторить эксперимент
$ git reset --hard ghi9012     # вернули как было

$ git reset --mixed HEAD~2     # = git reset HEAD~2

$ git log --oneline
abc1234 (HEAD -> main) C1: initial

$ cat pipeline.py
v3                              # working tree не тронули

$ git status
On branch main
Changes not staged for commit:
  modified:   pipeline.py        # в working tree, НЕ в index

Разница с --soft: index синхронизирован с новым HEAD (C1, v1). Изменения c v1 на v3 теперь в working tree как «не staged».

То есть --mixed делает:

  1. HEAD -> C1.
  2. Index -> перезаписан снэпшотом из C1.
  3. Working tree -> не тронут.

Изменения не теряются, но их надо заново git add-нуть, чтобы закоммитить.

Когда юзать —mixed

  • «Я хочу пересмотреть, что класть в коммит». Reset снёс staging area, делай git add заново по частям через git add -p.
  • Сжать коммиты и пересоставить staging area с нуля.

Режим 3: —hard (всё уничтожаем)

$ git reset --hard ghi9012     # вернули как было

$ git reset --hard HEAD~2

$ git log --oneline
abc1234 (HEAD -> main) C1: initial

$ cat pipeline.py
v1                              # working tree откатился!

$ git status
On branch main
nothing to commit, working tree clean

--hard синхронизировал всё: HEAD на C1, index в состояние C1, файлы на диске тоже в состояние C1. Никакого следа от C2/C3 на ветке нет. Working tree чистый — как будто C2 и C3 не существовали.

DANGER

git reset --hard уничтожает несохранённые изменения в working tree (те, что не были закоммичены). Это нельзя восстановить через reflog — reflog хранит только коммиты, не working tree. Если ты редактировал файл, не закоммитил, и сделал --hard — изменения навсегда потеряны.

А коммиты-то восстановимы?

Да — но через reflog (урок 04). C2 и C3 после --hard не в ветке main, но существуют как orphan objects в .git/objects ещё ~90 дней до garbage collection. Reflog запомнил их хэши:

$ git reflog
abc1234 (HEAD -> main) HEAD@{0}: reset: moving to HEAD~2
ghi9012 HEAD@{1}: commit: C3: add validation
def5678 HEAD@{2}: commit: C2: add transform
abc1234 HEAD@{3}: commit (initial): C1: initial

$ git reset --hard HEAD@{1}    # вернули обратно через reflog

Поэтому --hard страшен, но не катастрофичен — пока есть reflog. Снесённые --hard-ом working tree изменения, которые не были в коммите — вот это уже не вернёшь.

Когда юзать —hard

  • «Я хочу полностью забыть мои локальные эксперименты и начать с чистой ветки». Например, ветка зашла в тупик, проще откатить и начать заново.
  • Синхронизироваться с удалённой веткой: git reset --hard origin/main. Уничтожает локальные коммиты, которые ещё не запушены. Юзай только если ты уверен, что эти коммиты тебе не нужны.

Полная сравнительная таблица

Действие—soft—mixed (default)—hard
Двигает HEADдадада
Перезаписывает indexнетдада
Перезаписывает working treeнетнетда
Несохранённые изменения в working treeсохраненысохраненыуничтожены
Изменения, которые были в коммитах после нового HEADв indexв working treeпотеряны (но в reflog)
Опасностьнизкаясредняявысокая

Диаграмма: путь файла в каждом режиме

Состояние pipeline.py после reset на C1, исходно v3 в HEAD/index/wt
После --soft: HEAD на C1 (v1), index хранит v3, файл на диске v3
HEAD: v1C1: v1
index: v3index хранит v3 — готов к коммиту изменения
wt: v3working tree: v3 на диске
После --mixed: HEAD на C1, index синхронизирован с C1, working tree остался v3
HEAD: v1C1: v1
index: v1index синхронизирован с HEAD
wt: v3working tree: v3 на диске, не staged
После --hard: всё синхронизировано с C1, изменения v3 потеряны из working tree (восстановимы только из reflog коммита)
HEAD: v1C1: v1
index: v1index синхронизирован с HEAD
wt: v1working tree: v1, изменения уничтожены

Что точно НЕ делает reset

Распространённые заблуждения:

  1. Reset не удаляет коммиты из репозитория. Он двигает указатель ветки. Сами коммиты живут в .git/objects пока их не подберёт garbage collection (обычно через ~90 дней + если они unreachable).

  2. Reset не работает с remote. git reset --hard HEAD~2 локально откатит твою ветку. На сервере (origin/main) ничего не изменится, пока ты не сделаешь git push --force (что почти всегда плохая идея на shared веткой).

  3. Reset не trigger-ит файловую систему хитро. Это просто запись новых SHA в .git/refs/heads/<branch> + обновление index + опционально файлов.


DE-сценарии

Сценарий A: «закоммитил в feature, надо было в hotfix»

# Делал hotfix, но забыл переключиться на ветку hotfix/critical
$ git log --oneline
abc1234 (HEAD -> feature/users) Fix critical bug in DAG

# Откатываем коммит, оставляя изменения в working tree
$ git reset --mixed HEAD~1
$ git switch hotfix/critical
$ git add . && git commit -m "Fix critical bug in DAG"

Сценарий B: «сжимаю 5 экспериментальных коммитов в один перед PR»

$ git log --oneline
e5e5e5e (HEAD -> feature/etl) try variant 5
d4d4d4d try variant 4
c3c3c3c try variant 3
b2b2b2b try variant 2
a1a1a1a try variant 1
def5678 (origin/main) main commit

# Сжимаем всё до main
$ git reset --soft origin/main
$ git commit -m "feat: ETL pipeline для users"
$ git log --oneline
fffffff (HEAD -> feature/etl) feat: ETL pipeline для users
def5678 (origin/main) main commit

Сценарий C: «синхронизируюсь с origin, мои локальные коммиты не нужны»

$ git fetch origin
$ git reset --hard origin/main
# Локальная main теперь точно как origin/main, все локальные коммиты ушли
WARNING

В сценарии C ты теряешь все локальные коммиты, которые ещё не запушены. Если они тебе нужны — сначала git branch backup-local или git stash, потом --hard.


Reset vs Restore — когда что

ХочуКоманда
Откатить файл к HEADgit restore <file>
Unstage файлgit restore --staged <file>
Откатить HEAD на N коммитов, сохранить изменения в indexgit reset --soft HEAD~N
Откатить HEAD на N коммитов, изменения в working treegit reset --mixed HEAD~N
Откатить HEAD на N коммитов, ВСЁ снестиgit reset --hard HEAD~N
Отменить публичный коммит безопасноgit revert <commit> (урок 03)

Мнемоника: restore — для файлов, reset — для HEAD, revert — для shared history.


Попробуй сам

Воссоздай setup и поиграйся со всеми тремя режимами:

$ mkdir reset-play && cd reset-play
$ git init
$ for i in 1 2 3; do echo "v$i" > f.txt && git add . && git commit -m "C$i"; done

# Поэкспериментируй
$ git reset --soft HEAD~2 && git status && git reset --hard ghi9012   # вернуть как было — подставь свой хэш
$ git reset --mixed HEAD~2 && git status && git reset --hard <C3-hash>
$ git reset --hard HEAD~2 && git status && cat f.txt    # увидел v1?
$ git reflog                                             # коммиты С2/С3 ещё видны
$ git reset --hard HEAD@{1}                              # восстановили

Killer takeaway

git reset — это «двигай HEAD на N коммитов назад/вперёд». Три режима отличаются тем, насколько глубоко синхронизировать остальные области: --soft только HEAD, --mixed ещё index, --hard ещё и working tree. --hard опасен только для uncommitted изменений — закоммиченное всегда восстановимо через reflog (урок 04). Никогда не делай --hard на ветке с несохранённой работой без git stash перед этим.

Первый bash-скрипт: shebang и аргументы
Проверка знанийKnowledge check
ОтветAnswer

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 5. Что точно меняет `git reset --soft HEAD~2`?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 5