Справочник по Git и GitHub · модуль 2 · первый репозиторий в VS Code
Глава 2.6
Отмена изменений:
discard, amend, revert, reset
В главе разобраны способы отменить сделанное в Git. Какой из них нужен, зависит от того, где находится правка: только в файле, в последнем коммите или в коммите, сделанном раньше. Для каждого способа показаны кнопка в VS Code или команда, что меняется в истории и что можно потерять. В конце — как вернуть коммит, стёртый по ошибке.
- Категория
- Основы работы
- Уровень
- Начальный
- Что получится
- Умение отменить правку, исправить коммит и откатить старый
- Сколько занимает
- Около тридцати минут за компьютером
§1Где находится правка и чем её отменять
В главе 2.3 было показано, что правка проходит три места: рабочую папку, индекс и историю. Отмена устроена так же: способ зависит от того, куда правка успела дойти.
Все способы делятся на две группы. Первые меняют только файлы и индекс, история проекта остаётся прежней. Вторые меняют историю: убирают коммит, заменяют его другим или добавляют новый. Для второй группы действует правило: коммиты, которые уже отправлены на GitHub, не убирают и не заменяют. Их отменяют только новым коммитом.
Зачем это нужно в работе
- Пробная правка не подошла. Цвета поменяли, результат хуже прежнего. Правку нужно выбросить и вернуть файл к последнему коммиту.
- Опечатка в сообщении или забытый файл. Коммит только что сделан, и в нём ошибка. Исправить его проще, чем делать второй коммит «исправил опечатку».
- Правка на сайте оказалась ошибкой. Коммит неделю назад поменял отступы, и теперь их нужно вернуть. Остальные коммиты после него трогать нельзя.
- Что-то стёрто по ошибке. Git записывает в журнал каждое действие с историей, и по этой записи стёртый коммит можно вернуть.
В главе используется проект sait-vizitka в том виде, в каком он остался после главы 2.5: одиннадцать коммитов, семь из них на GitHub ещё не отправлены.
§2Шаг 1. Отменить правку в файле
Первый случай: правка есть только в файле. В индекс и в историю она не попадала.
Пробуем тёмное оформление. В styles.css в блоке body меняем местами цвета фона и текста:
body {
margin: 0;
padding: 60px;
font-family: system-ui, sans-serif;
background: #F4F1EC;
color: #1B1A19;
}
body {
margin: 0;
padding: 60px;
font-family: system-ui, sans-serif;
background: #1B1A19;
color: #F4F1EC;
}
Заодно создаём в корне проекта файл proba.html, чтобы посмотреть на тёмную тему отдельно. Сохраняем оба файла.
<!doctype html> <html lang="ru"> <body> <h1>Проба тёмной темы</h1> </body> </html>
proba.html (U) и изменённый styles.css (M).Результат не понравился. Нажимаем на styles.css в панели, чтобы увидеть, что будет отменено:
Наводим курсор на styles.css в списке Changes и нажимаем круговую стрелку Discard Changes. VS Code переспрашивает:
Нажимаем Discard File. Файл пропадает из списка, в редакторе снова светлые цвета.
Та же отмена в терминале:
git restore styles.css
Команда ничего не печатает. После неё git status --short не показывает styles.css: файл совпадает с последним коммитом.
Discard перезаписывает файл версией из последнего коммита. Правка в историю не попадала, поэтому Git её не хранит, и в корзину она тоже не попадает. Если среди выбрасываемого есть что-то нужное, сначала сделайте коммит или скопируйте текст. Один участок файла отменяет круговая стрелка в окне сравнения — она рядом со значком Stage Block из главы 2.3.
Что получилось.styles.css снова такой, как в последнем коммите, пробные цвета стёрты. В панели остался proba.html.
§3Шаг 2. Удалить новый файл
В панели остался второй пробный файл — proba.html. С новым файлом отмена работает иначе.
У proba.html прошлой версии нет: файл новый, в коммитах его не было. Круговая стрелка у такого файла означает удаление. Наводим курсор на proba.html и нажимаем её:
Новый файл VS Code переносит в корзину системы, и оттуда его можно достать. В терминале новый файл удаляет команда git clean. Она стирает файл сразу, минуя корзину:
git clean -f proba.html
Что получилось.Пробных файлов в проекте нет, список изменений пуст.
§4Шаг 3. Исправить последний коммит
Следующий случай: правка уже в коммите, и этот коммит последний. Сначала сделаем такой коммит с двумя ошибками, потом исправим.
Добавляем на страницу контактов ссылку на профиль GitHub. В kontakty.html под строкой с почтой появляется новая строка:
<h1>Контакты</h1> <p>Почта: student@example.com</p> </body>
<h1>Контакты</h1> <p>Почта: student@example.com</p> <p><a href="https://github.com/MaximBytecamp">Профиль на GitHub</a></p> </body>
В конец styles.css добавляем цвет ссылок:
a {
color: #C2410C;
}
Отбираем плюсом только kontakty.html и коммитим с сообщением «Добавил сылку на GitHub».
В коммите две ошибки: опечатка в сообщении и забытый styles.css. Коммит последний и на GitHub не отправлен, поэтому его можно исправить. Второй коммит для этого не нужен.
kontakty.html.- Отбираем плюсом
styles.css— он войдёт в исправленный коммит. - В поле сообщения пишем правильный текст: «Добавил ссылку на GitHub».
- Нажимаем стрелку справа от кнопки Commit и выбираем Commit (Amend).
Что произошло, видно по имени коммита. Слово HEAD в команде означает коммит, на котором вы сейчас стоите, — в графе он отмечен меткой main:
git show --stat --oneline HEAD
7531a77 Добавил ссылку на GitHub
kontakty.html | 1 +
styles.css | 4 ++++
2 files changed, 5 insertions(+)
До исправления коммит назывался 171a006, после — 7531a77. Amend создаёт новый коммит с исправленным содержимым и ставит его на место старого. Имя коммита вычисляется из содержимого (глава 2.4), поэтому новое содержимое даёт новое имя.
То же в терминале: отобрать забытый файл и заменить последний коммит.
git add styles.css git commit --amend -m "Добавил ссылку на GitHub"
Если коммит уже на GitHub, у сервера и у коллег осталась старая версия 171a006, а у вас — новая 7531a77. Истории разошлись, и обычная отправка не пройдёт. Поэтому правило такое: amend применяют к коммитам, которые ещё не отправлены. В графе это коммиты выше метки origin/main.
Что получилось.Вместо коммита с опечаткой в истории исправленный коммит с обоими файлами. Число коммитов не изменилось.
§5Шаг 4. Отменить последний коммит целиком
Бывает, что коммит сделан слишком рано и правку нужно доработать. Тогда последний коммит отменяют целиком, а его правки возвращают в индекс.
Задаём цвет заголовку. В styles.css в блок h1 добавляем строку:
h1 {
font-size: 34px;
}
h1 {
font-size: 34px;
color: #C2410C;
}
Сразу коммитим с сообщением «Покрасил заголовок». Потом становится видно, что цвет заголовка совпал с цветом ссылок и их не различить. Коммит сделан рано: правку нужно доработать.
Для этого у раздела CHANGES есть меню. Наводим курсор на строку CHANGES, нажимаем «…» справа и выбираем Commit → Undo Last Commit.
После Undo Last Commit коммит исчезает из истории, а его содержимое остаётся в индексе. В терминале этой кнопке соответствует команда reset с ключом --soft. Запись HEAD~ означает коммит перед текущим, HEAD~1 — то же самое. Подробно reset разобран в шаге 6.
git reset --soft HEAD~
Меняем цвет заголовка на более тёмный, чтобы он отличался от ссылок:
h1 {
font-size: 34px;
color: #C2410C;
}
h1 {
font-size: 34px;
color: #9A3412;
}
Отбираем файл плюсом и делаем коммит заново с тем же сообщением — оно уже стоит в поле. То же в терминале:
git add styles.css git commit -m "Покрасил заголовок"
Что получилось.Коммит «Покрасил заголовок» сделан заново, с цветом темнее ссылок.
§6Шаг 5. Отменить старый коммит
Третий случай: ошибка в коммите, сделанном давно, и после него уже есть другие коммиты. Amend и Undo Last Commit работают только с последним коммитом, здесь нужен другой способ.
Отступы страницы решено вернуть к 40px. Их увеличил коммит «Увеличил отступы страницы» из главы 2.3. После него сделано ещё семь коммитов, и все они нужны.
Для такого случая есть revert — новый коммит, который делает обратное указанному: убирает добавленные им строки и возвращает удалённые. Сам старый коммит остаётся в истории.
В VS Code 1.138 кнопки для revert нет, понадобится терминал. Сначала копируем имя коммита: правой кнопкой по нему в разделе GRAPH → Copy Commit Hash.
Открываем терминал (Ctrl + `) и вставляем имя после команды. Ключ --no-edit оставляет сообщение, которое Git составит сам. Без него откроется редактор для сообщения. Если в главе 1.3 вы оставили редактором Vim, сообщение сохраняется и редактор закрывается так: Esc, затем :wq и Enter.
git revert --no-edit 121e750
Auto-merging styles.css
[main 9f5312d] Revert "Увеличил отступы страницы"
1 file changed, 1 insertion(+), 1 deletion(-)
Имя коммита у вас будет своим — то, которое скопировали из графа. Первая строка ответа сообщает, что Git сам сопоставил правки в styles.css. Вторая — что создан коммит 9f5312d; перед его именем стоит метка main, та же, что в графе. Что в новом коммите:
git show HEAD
commit 9f5312d038b94569434acb99241c171738962489
Author: Student <student@example.com>
Revert "Увеличил отступы страницы"
This reverts commit 121e750ab4239f5782cb63dc5f58a5b03b154b1c.
- padding: 60px;
+ padding: 40px;
Одна строка изменена в обратную сторону: было 60px, стало 40px. В сообщении Git указал, какой коммит отменён. Остальные коммиты не тронуты.
Ассоциация: исправление в бухгалтерской книге
В бухгалтерской книге ошибочную запись не вырывают и не замазывают. Ниже делают новую запись на ту же сумму с обратным знаком и ссылкой на ошибочную. Итог становится верным, а по книге видно, что было и как исправили.
Revert — такая же обратная запись. Ошибочный коммит остаётся в истории, над ним появляется коммит с обратными изменениями и ссылкой на него в сообщении. Файлы становятся такими, как до ошибки.
Где не совпадаетСумму с обратным знаком вычесть можно всегда. Git вычитает строки, и если после ошибочного коммита те же строки кто-то менял ещё раз, revert остановится с конфликтом: Git не сможет сам решить, какой вариант оставить. Конфликты разберём в главе 3.4.
Revert подходит и для коммитов, которые уже на GitHub: он только добавляет коммит, прежние остаются на месте. Поэтому отправленные коммиты отменяют через revert.
Что получилось.В истории новый коммит Revert "Увеличил отступы страницы", отступ снова 40px. Остальные коммиты на месте.
§7Шаг 6. Три режима reset на пробном коммите
Revert исправляет ошибку новым коммитом. Команда reset действует иначе: она убирает коммиты из истории. Это нужно, когда коммит сделан по ошибке и на GitHub ещё не отправлен.
Reset возвращает историю к указанному коммиту. Все коммиты после него исчезают из графа.
Правка из коммита записана в трёх местах: в истории, в индексе и в файле. Reset с любым ключом убирает коммит из истории. Ключ --soft, --mixed или --hard определяет, вернутся ли к предыдущему коммиту индекс и файл:
40px на месте. Голубая клетка — место вернулось к коммиту revert. С каждым ключом голубых клеток на одну больше.Ассоциация: заклеенная коробка из главы 2.3
Коробку с вещами заклеили и подписали, а потом передумали. Её можно расклеить и оставить вещи внутри. Можно выложить вещи на стол. Можно выбросить вместе с коробкой.
--soft расклеивает коробку: правки остаются в индексе. --mixed выкладывает их на стол: правки остаются в файлах, индекс пуст. --hard выбрасывает: правки стёрты.
Где не совпадаетВыброшенную коробку не вернуть. Коммит после --hard Git ещё хранит, и его можно достать по журналу — это шаг 7. Незакоммиченные правки после --hard пропадают насовсем.
Проверим это на пробном коммите: сделаем его и отменим тремя способами по очереди. На каждом кадре слева панель Source Control, справа открытый файл styles.css. В styles.css увеличиваем размер заголовка:
h1 {
font-size: 34px;
color: #9A3412;
}
h1 {
font-size: 40px;
color: #9A3412;
}
Отбираем файл плюсом и коммитим с сообщением «Проба reset». То же в терминале:
git add styles.css git commit -m "Проба reset"
11↑. Список изменений пуст. В файле строка 10: font-size: 40px.Режим --soft: правка остаётся в индексе и в файле
Отменяем пробный коммит с ключом --soft. Запись HEAD~ означает коммит перед текущим — здесь это коммит revert:
git reset --soft HEAD~
--soft. Коммита «Проба reset» в графе нет, сверху revert. Файл стоит в Staged Changes. В строке 10 по-прежнему 40px.Убран только коммит. Правка отобрана в индекс, поэтому для нового коммита достаточно нажать Commit. Кнопка Undo Last Commit из шага 4 выполняет эту же команду.
Возвращаем пробный коммит: пишем в поле сообщение «Проба reset» и нажимаем ✓ Commit. То же в терминале:
git commit -m "Проба reset"
Режим --mixed: правка остаётся только в файле
Этот режим включается, если ключ не указать совсем:
git reset HEAD~
Unstaged changes after reset:
M styles.css
--mixed. Коммита нет, файл стоит в Changes, раздела Staged Changes нет. В строке 10 по-прежнему 40px.Убраны коммит и отбор в индекс. Файл не менялся, но для нового коммита его нужно отобрать заново. Возвращаем пробный коммит ещё раз: плюс у файла, сообщение «Проба reset», ✓ Commit.
git add styles.css git commit -m "Проба reset"
Режим --hard: правка стёрта в индексе и в файле
Последний режим стирает и коммит, и его правку:
git reset --hard HEAD~
HEAD is now at 9f5312d Revert "Увеличил отступы страницы"
--hard. Коммита нет, список изменений пуст, на кнопке снова 10↑. В строке 10 теперь 34px: Git перезаписал файл.Убраны коммит, отбор в индекс и сама правка в файле. Строка font-size: 40px осталась только в стёртом коммите. Если бы в папке лежали другие незакоммиченные правки, они бы тоже стёрлись, и вернуть их нельзя.
Итог трёх опытов:
| Команда | Панель Source Control | Файл styles.css | Как сделать коммит заново |
|---|---|---|---|
git reset --soft HEAD~ | Файл в Staged Changes | 40px | Commit |
git reset HEAD~(то же, что --mixed) | Файл в Changes | 40px | Плюс у файла и Commit |
git reset --hard HEAD~ | Список пуст | 34px | Правки нет: её вносят заново или возвращают коммит по журналу (шаг 7) |
Reset убирает коммиты из истории, как amend заменяет последний. Для коммитов, которые уже на GitHub, действует то же правило: история у вас и на сервере разойдётся. Отправленные коммиты отменяют через revert.
Что получилось.Пробный коммит стёрт вместе с правкой, размер заголовка снова 34px. Коммит revert из шага 5 на месте.
§8Шаг 7. Вернуть стёртый коммит
Пробный коммит стёрт командой reset --hard. Git при этом его не удалил: коммит можно найти по журналу и вернуть.
Git ведёт журнал действий с историей: каждый коммит, amend, reset и revert записывается туда вместе с именем коммита, который после этого действия стал текущим. Журнал называется reflog и хранится только на вашем компьютере.
git reflog -12
9f5312d HEAD@{0}: reset: moving to HEAD~
837cf71 HEAD@{1}: commit: Проба reset
9f5312d HEAD@{2}: reset: moving to HEAD~
62b060b HEAD@{3}: commit: Проба reset
9f5312d HEAD@{4}: reset: moving to HEAD~
df14b2a HEAD@{5}: commit: Проба reset
9f5312d HEAD@{6}: revert: Revert "Увеличил отступы страницы"
91fb5a6 HEAD@{7}: commit: Покрасил заголовок
7531a77 HEAD@{8}: reset: moving to HEAD~
17d24cf HEAD@{9}: commit: Покрасил заголовок
7531a77 HEAD@{10}: commit (amend): Добавил ссылку на GitHub
171a006 HEAD@{11}: commit: Добавил сылку на GitHub
Журнал читается сверху вниз, от последнего действия к первому. Запись HEAD@{1} означает «состояние на одно действие раньше», HEAD@{2} — на два, и так далее. В журнале все шаги этой главы, снизу вверх:
HEAD@{11}иHEAD@{10}— коммит с опечаткой и его исправление через amend (шаг 3);HEAD@{9}иHEAD@{8}— поспешный коммит и Undo Last Commit. VS Code записал отмену какreset: moving to HEAD~(шаг 4);HEAD@{7}— тот же коммит, сделанный заново,HEAD@{6}— revert (шаг 5);HEAD@{5}–HEAD@{0}— три опыта шага 6: каждый раз коммит «Проба reset» и reset. У трёх пробных коммитов разные имена —df14b2a,62b060b,837cf71: это три отдельных коммита.
Последний пробный коммит 837cf71 стоит в строке HEAD@{1}. Reset делает последним любой указанный коммит, в том числе убранный из истории. Возвращаемся к нему той же командой reset --hard, указав вместо HEAD~ запись из журнала:
git reset --hard 'HEAD@{1}'
HEAD is now at 837cf71 Проба reset
11↑.Ключ --hard здесь сработал так же, как в шаге 6: файл приведён к коммиту, который стал последним. В styles.css снова 40px, список изменений пуст.
Вместо HEAD@{1} можно написать имя коммита из журнала: git reset --hard 837cf71. Кавычки вокруг HEAD@{1} нужны в PowerShell: без них фигурные скобки там означают другое. В терминале macOS и Linux команда работает и без кавычек.
Так же достаются старые версии после amend: git show 171a006 покажет коммит с опечаткой, хотя в истории его уже нет. Git хранит такие коммиты не меньше тридцати дней и только потом удаляет.
Пробный коммит был нужен только для опыта. Убираем его той же командой, что в шаге 6:
git reset --hard HEAD~
HEAD is now at 9f5312d Revert "Увеличил отступы страницы"
Проверяем, что проект вернулся к состоянию после шага 5:
git log --oneline -3 9f5312d Revert "Увеличил отступы страницы" 91fb5a6 Покрасил заголовок 7531a77 Добавил ссылку на GitHub git status --short
Сверху коммит revert, пробного коммита в списке нет. git status --short ничего не напечатал: незакоммиченных правок нет, в styles.css снова font-size: 34px. В журнале git reflog при этом прибавились две записи: возврат к 837cf71 и последний reset.
Что получилось.Стёртый коммит возвращён по журналу и затем убран. Проект в том же состоянии, что после шага 5: четырнадцать коммитов, десять из них на GitHub ещё не отправлены, сверху коммит revert.
§9Справка: какой способ выбрать
| Что нужно | В VS Code | Команда | Меняет историю |
|---|---|---|---|
| Выбросить правку в файле | Круговая стрелка Discard Changes у файла | git restore файл | Нет. Правка теряется |
| Выбросить правку в одном участке | Круговая стрелка в окне сравнения | git restore -p файл | Нет |
| Удалить новый файл | Круговая стрелка у файла с U → Move to Trash | git clean -f файл | Нет |
| Вернуть файл из индекса | − у файла в Staged Changes | git restore --staged файл | Нет. Правка остаётся в файле |
| Исправить последний коммит | Стрелка у Commit → Commit (Amend) | git commit --amend | Да, только до отправки |
| Отменить последний коммит, правки сохранить | «…» → Commit → Undo Last Commit | git reset --soft HEAD~ | Да, только до отправки |
| Отменить старый или отправленный коммит | Кнопки нет | git revert --no-edit хеш | Добавляет коммит |
| Вернуть проект к коммиту, всё новее стереть | Кнопки нет | git reset --hard хеш | Да, только до отправки. Незакоммиченное теряется |
| Найти стёртый коммит | Кнопки нет | git reflog | Нет, только показывает |
Перед отменой
origin/main, он уже на GitHub: только revert.reset --hard стирают его без возможности вернуть. Если сомневаетесь, сначала сделайте коммит.§10Частые вопросы
reset --hard пропадает насовсем.git reset --soft: файлы не меняются.§11Проверьте себя
Вы полчаса правили index.html, результат не нужен. Коммита после начала правок не было. Как вернуть файл?
Круговая стрелка Discard Changes у файла в панели или git restore index.html. Файл станет таким, как в последнем коммите. Выброшенные правки не вернуть.
Коммит только что сделан, в сообщении опечатка. На GitHub его ещё не отправляли. Что сделать?
Написать правильное сообщение в поле и выбрать Commit (Amend) в меню кнопки Commit, или выполнить git commit --amend -m "правильный текст". Последний коммит заменится новым.
Коммит, сделанный три дня назад и уже отправленный на GitHub, сломал страницу. Какой способ отмены выбрать и почему?
git revert --no-edit хеш. Коммит уже на сервере, поэтому историю переписывать нельзя. Revert добавит новый коммит с обратными изменениями, и его можно отправить как обычно.
Чем git reset --soft HEAD~ отличается от git reset --hard HEAD~?
Оба убирают последний коммит из истории. С --soft его правки остаются в индексе, готовые к новому коммиту. С --hard они стираются, а файлы становятся такими, как в предыдущем коммите.
После git reset --hard пропал нужный коммит. Как его вернуть?
Выполнить git reflog, найти строку с этим коммитом и вернуться к нему: git reset --hard имя-коммита или git reset --hard 'HEAD@{N}'.
Почему после amend у коммита другое имя?
Имя коммита вычисляется из его содержимого: файлов, сообщения, автора, даты и предыдущего коммита. Amend создаёт коммит с другим содержимым, поэтому имя тоже другое.
§12Шпаргалка по главе
Discard Changes
git restore файл
Discard → Move to Trash
git clean -f файл
Commit (Amend)
Undo Last Commit
git commit --amend
git reset --soft HEAD~
git revert --no-edit хеш
добавляет коммит
--soft — правки в индексе
--mixed — правки в файлах
--hard — правки стёрты
git reflog
git reset --hard хеш
Итог главы
Способ отмены выбирают по тому, где находится правка. Discard Changes, в терминале git restore, возвращает файл к последнему коммиту, и выброшенная правка пропадает насовсем. Новый файл VS Code переносит в корзину. Последний коммит исправляет Commit (Amend) — он создаёт новый коммит на месте старого, с другим именем, — а Undo Last Commit убирает коммит и возвращает его правки в индекс. Старый коммит отменяет git revert: он добавляет коммит с обратными изменениями и ничего не удаляет из истории, поэтому подходит и для коммитов на GitHub. Amend, Undo Last Commit и git reset переписывают историю, их применяют только к неотправленным коммитам. reset --hard стирает и коммиты, и незакоммиченные правки. Коммиты после него можно найти в журнале git reflog и вернуть. На этом модуль 2 закончен. Следующий модуль посвящён веткам: как вести несколько линий работы в одном проекте и собирать их вместе.
- Кадры и ответы команд получены на клоне проекта sait-vizitka в Git версии 2.51.1 и VS Code версии 1.138.
- Команды
git restore,git clean,git commit --amend— документация git-restore, git-clean, git-commit. - Режимы
git resetи работаgit revert— документация git-reset и git-revert. - Журнал
git reflogи срок хранения записей — документация git-reflog и git-gc (параметрgc.reflogExpireUnreachable, по умолчанию30дней). - Кнопки Discard Changes, Commit (Amend) и Undo Last Commit — документация VS Code, раздел Source Control.