$ sudo teach IT
Модуль 3 · Отмена и .gitignore · Урок 3.3

Revert и reset

Вы научитесь смотреть прошлое проекта и отменять старые коммиты безопасным и опасным способами.

Теория + практика~30 минутНовичокgit revertgit resetHEAD

Посмотреть прошлое: git switch --detach

Чтобы отменить коммит в Git, есть два способа: git revert создаёт новый коммит, который откатывает старый (история остаётся честной), а git reset двигает историю назад и стирает коммиты из неё. Первый безопасен всегда, второй годится только для коммитов, которыми вы ни с кем не делились. Вы попробуете оба, а начнёте с более мягкого: научитесь просто смотреть, каким проект был раньше.

Раньше мы правили только последний коммит. Но если ошибка была три коммита назад, сначала полезно заглянуть в то время и убедиться, что вы выбрали верный момент.

Простыми словами

История коммитов — лента фотографий проекта. К любой можно подойти, осмотреться и вернуться к самой свежей. Ничего при этом не меняется.

Официальный термин: HEAD

Чтобы объяснить, как это работает, вспомним HEAD — метку «вы находитесь здесь». Обычно она стоит на самом свежем коммите (на имени main, подробнее про него — в модуле про ветки). Команда git switch --detach <хеш> (detach — «отсоединить») переставляет HEAD прямо на выбранный коммит. Такое состояние называется detached HEAD, то есть «отсоединённый HEAD». Возьмите хеш коммита «Добавить стили» из своего git log --oneline (у вас он будет другой):

Терминал
$ git switch --detach 8c23940
HEAD is now at 8c23940 Добавить стили
$ git status
HEAD detached at 8c23940
Untracked files:
  (use "git add <file>..." to include in what will be committed)
	notes/

nothing added to commit but untracked files present (use "git add" to track)
$ ls
index.html  notes  style.css
$ git switch main
Previous HEAD position was 8c23940 Добавить стили
Switched to branch 'main'
$ ls
README.md  index.html  notes  style.css

Разбор по строкам

  • HEAD is now at 8c23940 Добавить стили — «HEAD теперь на коммите 8c23940». Рабочая папка стала такой, какой была на момент этого коммита.
  • HEAD detached at 8c23940 — git status напоминает, что вы не на главной линии истории, а «в гостях» у старого коммита.
  • ls не показывает README.md — он появился в следующем коммите. Git подменил отслеживаемые файлы их старыми версиями.
  • notes/ снова в статусе: .gitignore появился позже, и тогда Git ещё не знал, что папку надо скрывать.
  • git switch main — возвращение на самую свежую версию. Previous HEAD position означает «прежнее положение HEAD». Вместо main можно написать git switch - (дефис — «туда, откуда пришёл»).

Старый способ и длинное сообщение

В старых статьях это делают через git checkout <хеш>. Тогда Git печатает длинное сообщение You are in 'detached HEAD' state…. По-русски: «Вы в отсоединённом HEAD. Можно осматриваться и пробовать, но чтобы сохранить новые коммиты, нужна ветка; вернуться можно командой git switch -». Ветки появятся в следующем модуле, пока здесь только смотрим.

В прошлом не коммитьте

Если сделать коммит в отсоединённом состоянии и вернуться на main, коммит пропадёт из git log. Git при этом предупредит: Warning: you are leaving 1 commit behind, not connected to any of your branches (вы оставляете коммит, не связанный ни с одной веткой). Он не удалён, но найти его трудно: как это сделать, расскажет урок про reflog.

Взять файл из прошлого

Иногда нужно не переезжать на старый коммит, а достать оттуда один файл. Для этого у git restore есть флаг --source (source — «источник»). Он говорит, из какого коммита брать содержимое. Допустим, вы хотите увидеть index.html таким, каким он был в первом коммите:

Терминал
$ git restore --source 027a150 index.html
$ git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   index.html

no changes added to commit (use "git add" and/or "git commit -a")
$ git restore index.html
$ git status
On branch main
nothing to commit, working tree clean

Разбор по строкам

  • git restore --source 027a150 index.html — вместо текущей версии файла кладёт в рабочую папку версию из коммита 027a150. История при этом не двигается.
  • modified: index.html — для Git это обычная несохранённая правка: её можно закоммитить или отбросить.
  • Повторный git restore index.html без --source вернул файл к версии последнего коммита.

git revert: отмена новым коммитом

Теперь самое важное. Вы закоммитили изменение, и оно оказалось неудачным: например, поменяли цвет фона на розовый. Коммит уже не последний или вы им поделились, поэтому --amend не подходит. Нужна отмена, которая не трогает историю.

Простыми словами

Бухгалтерская книга: вы записали лишнюю трату. Страницу не вырывают, а вносят новую запись «возврат той же суммы». Итог верный, все записи на месте.

Официальный термин и пример

git revert <коммит> (revert — «откатить», «обратить») создаёт новый коммит, который делает обратное тому, что сделал указанный. Для учебного примера сделайте коммит «Поменять цвет фона»: в style.css замените background: #fafafa; на background: #ffe4e1;, затем git add style.css и git commit -m "Поменять цвет фона". Теперь отменим его:

Терминал
$ git revert HEAD
[main 5acc5ae] Revert "Поменять цвет фона"
 1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline
5acc5ae (HEAD -> main) Revert "Поменять цвет фона"
875cb0a Поменять цвет фона
746ac3d Добавить описание сайта
12d081f Добавить .gitignore

Разбор по строкам

  • git revert HEAD — «отмени самый свежий коммит». Вместо HEAD можно указать хеш любого коммита, например git revert 875cb0a.
  • Git откроет редактор с готовой подписью Revert "Поменять цвет фона" и пояснением This reverts commit …. Сохраните файл и закройте окно — коммит создастся. Чтобы редактор не открывался, добавьте --no-edit.
  • 1 insertion(+), 1 deletion(-) — одна строка добавлена и одна убрана: Git вернул прежний цвет фона.
  • В git log оба коммита: исходный и отменяющий. История честная, видно и ошибку, и её исправление.

Главное достоинство revert: он ничего не стирает, поэтому безопасен всегда, в том числе для коммитов, которые уже видели другие. Отменять можно и не последний коммит (git revert <хеш>). Если позже те же строки менялись снова, Git сообщит о конфликте; такие случаи мы разберём в модуле про ветки.

git reset: передвинуть историю назад

Бывает, что коммит ещё не опубликован и просто не нужен: пробовали идею и передумали. Тогда можно убрать сам коммит. Для этого служит git reset (reset — «сбросить», «вернуть в исходное»).

Простыми словами

Если revert — новая запись в книге, то reset — перемотка ленты назад: всё снятое после выбранной точки исчезает из истории.

Официальный термин: три режима

Команда git reset <коммит> переставляет текущий коммит (HEAD) на указанный. Что делать с изменениями из «сброшенных» коммитов — решает режим:

РежимКуда сдвинулась историяИндексФайлы в рабочей папке
--softназад на указанный коммитизменения остались подготовленнымиправки на месте
--mixed (по умолчанию)назад на указанный коммиточищенправки на месте, но не подготовлены
--hardназад на указанный коммиточищенправки удалены

Попробуем на учебном коммите. Допустим, вы увеличили отступ под заголовком в style.css (margin-bottom: 4px; на 24px) и закоммитили это как «Увеличить отступ заголовка». Запись HEAD~1 означает «на один коммит раньше текущего».

Терминал
$ git reset --soft HEAD~1
$ git log --oneline
5acc5ae (HEAD -> main) Revert "Поменять цвет фона"
875cb0a Поменять цвет фона
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   style.css

Коммит исчез, а правка осталась в индексе: можно закоммитить заново. Вернём его и попробуем режим по умолчанию:

Терминал
$ git commit -m "Увеличить отступ заголовка"
$ git reset HEAD~1
Unstaged changes after reset:
M	style.css
$ git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   style.css

no changes added to commit (use "git add" and/or "git commit -a")

Правка лежит в файле, но не подготовлена. Теперь жёсткий режим: подготовьте файл, закоммитьте ещё раз и сбросьте:

Терминал
$ git add style.css
$ git commit -m "Увеличить отступ заголовка"
$ git reset --hard HEAD~1
HEAD is now at 5acc5ae Revert "Поменять цвет фона"
$ git status
On branch main
nothing to commit, working tree clean
$ grep margin-bottom style.css
  margin-bottom: 4px;

Разбор по строкам

  • git reset --soft HEAD~1 — коммит убран, а его изменения ждут в индексе (Changes to be committed).
  • git reset HEAD~1 — то же без --soft, это режим --mixed. Строка Unstaged changes after reset перечисляет файлы, правки в которых остались, но из индекса убраны.
  • git reset --hard HEAD~1 — коммит убран вместе с правками: HEAD is now at … означает «HEAD теперь на этом коммите», а в style.css снова прежние 4px.

reset --hard стирает несохранённое

В режиме --hard пропадает не только коммит, но и все незакоммиченные правки в отслеживаемых файлах: их нельзя вернуть, как и после git restore. Перед --hard всегда смотрите git status и убедитесь, что терять нечего. А если сбрасываете коммит, который уже кто-то получил, истории разойдутся, поэтому для опубликованного используйте revert.

Частая путаница: revert и reset

revert добавляет новый коммит, отменяющий старый, и ничего не стирает. reset убирает коммиты из истории. Если сомневаетесь, выбирайте revert.

Что выбрать

СитуацияЧто использовать
Правка в файле, ещё не закоммичена, не нужнаgit restore файл
В индекс попало лишнееgit restore --staged файл
Ошибка в самом последнем коммите (подпись или забытый файл), коммит не опубликованgit commit --amend
Коммит свой, не опубликован и просто не нуженgit reset (при сомнении — --soft или режим по умолчанию, не --hard)
Коммит старый или уже опубликованgit revert
Нужно посмотреть прошлое или достать старый файлgit switch --detach, git restore --source

Практика у себя

Повторим всё на визитке. Хеши подставляйте свои: смотрите их в git log --oneline.

  1. Старт. В ~/projects/portfolio: git status чистый, git log --oneline показывает семь коммитов.
  2. Загляните в прошлое. git switch --detach <хеш «Добавить стили»>, затем ls: нет README.md. Вернитесь командой git switch main — файл на месте.
  3. Достаньте старый файл. git restore --source <хеш первого коммита> index.html, посмотрите git diff --stat, а затем верните всё как было: git restore index.html.
  4. Неудачный коммит. В style.css замените background: #fafafa; на background: #ffe4e1;, затем git add style.css и git commit -m "Поменять цвет фона".
  5. Отмените его. git revert HEAD, в редакторе сохраните подпись и закройте окно. git log --oneline показывает «Revert "Поменять цвет фона"» поверх «Поменять цвет фона», а в style.css снова #fafafa.
  6. Reset на пробу. Замените в style.css margin-bottom: 4px; на margin-bottom: 24px;, git add style.css, git commit -m "Увеличить отступ заголовка". Выполните git reset --soft HEAD~1: коммит исчез, файл в разделе Changes to be committed. Закоммитьте снова (то же сообщение), затем git reset HEAD~1: файл в Changes not staged for commit.
  7. Reset --hard. Снова git add style.css, коммит с тем же сообщением, затем git reset --hard HEAD~1. Ответ HEAD is now at … Revert "Поменять цвет фона", а в файле снова 4px.

Итог (хеши у вас будут другие):

Терминал
$ git log --oneline
5acc5ae (HEAD -> main) Revert "Поменять цвет фона"
875cb0a Поменять цвет фона
746ac3d Добавить описание сайта
12d081f Добавить .gitignore
284ecf7 Исправить опечатку в заголовке
cb00bb8 Добавить раздел «Мои проекты»
f58d529 Добавить README
8c23940 Добавить стили
027a150 Добавить главную страницу

Проверьте себя

  • git log --oneline показывает девять коммитов; сверху — «Revert "Поменять цвет фона"», а «Увеличить отступ заголовка» в истории нет.
  • git status сообщает nothing to commit, working tree clean.
  • В style.css фон #fafafa и margin-bottom: 4px;.
  • Вы можете объяснить, почему для опубликованного коммита нужен revert, а не reset.

Частые ошибки

fatal: a branch is expected, got commit '8c23940'. Вы написали git switch <хеш> без флага. Добавьте --detach, как и подсказывает Git.

Коммит «пропал» после работы в отсоединённом состоянии. Вы закоммитили в прошлом и ушли на main. Коммит не удалён, но не виден в git log: найдём его через reflog в следующем уроке.

После reset --hard пропали правки. Незакоммиченные изменения не восстановить. Закоммиченные ещё можно найти (следующий урок).

Что важно запомнить

  • git switch --detach <хеш> позволяет посмотреть старый коммит, git switch main возвращает назад. В прошлом не коммитим.
  • git restore --source <хеш> файл достаёт один файл из прошлого.
  • git revert <коммит> создаёт новый коммит-отмену. Безопасно всегда, история сохраняется.
  • git reset --soft|--mixed|--hard <коммит> двигает историю назад; --hard стирает правки. Только для своих неопубликованных коммитов.
  • Сомневаетесь — выбирайте revert.

Мы не раз говорили, что «пропавший» коммит можно потом найти. Правда ли Git так легко теряет историю? Нет, и в следующем уроке вы узнаете, как вернуть коммит, который казался потерянным.

Подробно — в документации: git revert, git reset.

На следующий урок