Посмотреть прошлое: 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.
- Старт. В
~/projects/portfolio:git statusчистый,git log --onelineпоказывает семь коммитов. - Загляните в прошлое.
git switch --detach <хеш «Добавить стили»>, затемls: нетREADME.md. Вернитесь командойgit switch main— файл на месте. - Достаньте старый файл.
git restore --source <хеш первого коммита> index.html, посмотритеgit diff --stat, а затем верните всё как было:git restore index.html. - Неудачный коммит. В
style.cssзаменитеbackground: #fafafa;наbackground: #ffe4e1;, затемgit add style.cssиgit commit -m "Поменять цвет фона". - Отмените его.
git revert HEAD, в редакторе сохраните подпись и закройте окно.git log --onelineпоказывает «Revert "Поменять цвет фона"» поверх «Поменять цвет фона», а вstyle.cssснова#fafafa. - Reset на пробу. Замените в
style.cssmargin-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. - 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.