git reset
Команда git reset — это сложный универсальный инструмент для отмены изменений. Она имеет три основные формы вызова, соответствующие аргументам командной строки —soft, —mixed, —hard . Каждый из этих трех аргументов соответствует трем внутренним механизмам управления состоянием Git: дереву коммитов ( HEAD ), разделу проиндексированных файлов и рабочему каталогу.
Git reset и три дерева Git
Чтобы понять, как используется команда git reset , необходимо разобраться с внутренними системами управления состоянием в Git. Иногда эти механизмы называют «тремя деревьями» Git. «Деревья» — возможно, не самое точное название, поскольку это не традиционные древовидные структуры данных в строгом смысле слова. Тем не менее это структуры данных на основе узлов и указателей, которые Git использует для отслеживания истории внесения правок. Лучший способ продемонстрировать эти механизмы — создать набор изменений в репозитории и проследить его по трем деревьям.
Начнем работу с создания нового репозитория с помощью приведенных ниже команд.
$ mkdir git_reset_test
$ cd git_reset_test/
$ git init .
Initialized empty Git repository in /git_reset_test/.git/
$ touch reset_lifecycle_file
$ git add reset_lifecycle_file
$ git commit -m"initial commit"
[main (root-commit) d386d86] initial commit
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 reset_lifecycle_file
В приведенном выше примере кода создается новый репозиторий Git с одним пустым файлом reset_lifecycle_file . На этом этапе репозиторий имеет один коммит ( d386d86 ), в котором отражено добавление файла reset_lifecycle_file .
Связанные материалы
Шпаргалка по Git
СМ. РЕШЕНИЕ
Изучите Git с помощью Bitbucket Cloud
Рабочий каталог
Первое дерево, которое мы рассмотрим, — рабочий каталог. Это дерево синхронизировано с локальной файловой системой и отображает непосредственные изменения, внесенные в содержимое файлов и каталогов.
$ echo 'hello git reset' > reset_lifecycle_file
$ git status
On branch main
Changes not staged for commit:
(use "git add . " to update what will be committed)
(use "git checkout -- . " to discard changes in working directory)
modified: reset_lifecycle_file
В нашем демонстрационном репозитории изменим и добавим содержимое в файл reset_lifecycle_file . Вызов команды git status показывает, что Git знает об изменениях в этом файле. В данный момент эти изменения являются частью первого дерева — рабочего каталога. Для отображения изменений в рабочем каталоге можно использовать команду git status . Измененные файлы будут отображаться красным цветом с префиксом «modified»
Раздел проиндексированных файлов
Следующее дерево — раздел проиндексированных файлов. Это дерево отслеживает изменения рабочего каталога, которые были добавлены с помощью команды git add , для сохранения в следующем коммите. Это дерево представляет собой сложный внутренний механизм кэширования. В целом Git пытается скрыть от пользователя подробности реализации раздела проиндексированных файлов.
Для полного просмотра состояния раздела проиндексированных файлов необходимо использовать менее известную команду Git — git ls-files . Команда git ls-files по сути является утилитой отладки для проверки состояния дерева раздела проиндексированных файлов.
git ls-files -s
100644 e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 0 reset_lifecycle_file
Здесь мы выполнили команду git ls-files с параметром -s (или —stage ). Без параметра -s вывод команды git ls-files представляет собой просто список путей и имен файлов, которые в данный момент являются частью индекса. Параметр -s отображает дополнительные метаданные файлов, находящихся в разделе проиндексированных файлов. Эти метаданные — биты режима проиндексированного контента, имя объекта и номер в индексе. Здесь нас интересует второе значение, имя объекта ( d7d77c1b04b5edd5acfc85de0b592449e5303770 ). Это стандартный хеш SHA-1 объекта Git, представляющий собой хеш содержимого файлов. В истории коммитов хранятся собственные SHA объектов для идентификации указателей на коммиты и ссылки, а в разделе проиндексированных файлов есть свои SHA объектов для отслеживания версий файлов в индексе.
Далее мы добавим измененный файл reset_lifecycle_file в раздел проиндексированных файлов.
$ git add reset_lifecycle_file
$ git status
On branch main Changes to be committed:
(use "git reset HEAD . " to unstage)
modified: reset_lifecycle_file
Здесь мы вызываем команду git add reset_lifecycle_file , которая добавляет файл в раздел проиндексированных файлов. Теперь при вызове команды git status файл reset_lifecycle_file отображается зеленым цветом, как изменение, подлежащее коммиту («Changes to be committed»). Важно отметить, что команда git status не отображает истинное представление раздела проиндексированных файлов. Вывод git status отображает различия между историей коммитов и разделом проиндексированных файлов. Давайте рассмотрим содержимое раздела проиндексированных файлов на данный момент.
$ git ls-files -s 100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file
Видно, что SHA объекта для файла reset_lifecycle_file изменился с e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 на d7d77c1b04b5edd5acfc85de0b592449e5303770 .
История коммитов
Последнее дерево — история коммитов. Команда git commit добавляет изменения в постоянный снимок, который находится в истории коммитов. Этот снимок также включает состояние раздела проиндексированных файлов на момент выполнения коммита.
$ git commit -am"update content of reset_lifecycle_file"
[main dc67808] update content of reset_lifecycle_file
1 file changed, 1 insertion(+)
$ git status
On branch main
nothing to commit, working tree clean
Здесь мы создали новый коммит с комментарием update content of resetlifecyclefile . В историю коммитов был добавлен набор изменений. Вызов команды git status в этой точке показывает, что ни в одном дереве нет ожидающих изменений. Выполнение команды git log отобразит дерево коммитов. Теперь, когда мы проследили за этим набором изменений во всех трех деревьях, можно приступать к использованию команды git reset .
Порядок действий
На первый взгляд, поведение команды git reset схоже с поведением команды git checkout . Но команда git checkout работает исключительно с указателем HEAD , а git reset перемещает указатель HEAD и указатель текущей ветки. Чтобы лучше продемонстрировать это поведение, рассмотрим следующий пример.
В этом примере показана последовательность коммитов в ветке main . Сейчас и указатель HEAD , и указатель на главную ветку main указывают на коммит d. Теперь давайте выполним обе команды, git checkout b и git reset b , и сравним результат.
git checkout b
После выполнения команды git checkout указатель main по-прежнему ссылается на коммит d . Указатель HEAD переместился и теперь ссылается на коммит b . В данный момент репозиторий находится в состоянии открепленного указателя HEAD .
git reset b
Команда git reset перемещает и указатель HEAD , и указатель ветки на заданный коммит.
Помимо обновления указателей на коммит команда git reset изменяет состояние трех деревьев. Указатели меняются всегда, то есть происходит обновление третьего дерева, дерева коммитов. Аргументы командной строки —soft, —mixed и —hard определяют, каким образом необходимо изменить деревья раздела проиндексированных файлов и рабочего каталога.
Основные параметры
По умолчанию при вызове команды git reset используются неявные аргументы —mixed и HEAD . Таким образом, выполнение команды git reset эквивалентно выполнению команды git reset —mixed HEAD . В этом случае HEAD является указателем на конкретный коммит. Вместо HEAD можно использовать любой хеш SHA-1 коммита Git.
‘—hard
Это самый прямой, ОПАСНЫЙ и часто используемый вариант. При использовании аргумента —hard указатели в истории коммитов обновляются на указанный коммит. Затем происходит сброс раздела проиндексированных файлов и рабочего каталога до указанного коммита. Все предыдущие ожидающие изменения в разделе проиндексированных файлов и рабочем каталоге сбрасываются в соответствии с состоянием дерева коммитов. Это значит, что любая работа, находившаяся в состоянии ожидания в разделе проиндексированных файлов и рабочем каталоге, будет потеряна.
Чтобы продемонстрировать это, продолжим работать в репозитории, созданном ранее для примера с тремя деревьями. Сначала внесем в репозиторий некоторые изменения. Выполните в нем следующие команды:
$ echo 'new file content' > new_file
$ git add new_file
$ echo 'changed content' >> reset_lifecycle_file
С помощью этих команд мы создали новый файл с именем new_file и добавили его в репозиторий. Кроме того, было изменено содержимое файла reset_lifecycle_file . А теперь давайте выполним команду git status и посмотрим, как эти изменения повлияли на состояние репозитория.
$ git status
On branch main
Changes to be committed:
(use "git reset HEAD . " to unstage)
new file: new_file
Changes not staged for commit:
(use "git add . " to update what will be committed)
(use "git checkout -- . " to discard changes in working directory)
modified: reset_lifecycle_file
Здесь мы вызываем команду git add reset_lifecycle_file , которая добавляет файл в раздел проиндексированных файлов. Теперь при вызове команды git status файл reset_lifecycle_file отображается зеленым цветом, как изменение, подлежащее коммиту («Changes to be committed»). Важно отметить, что команда git status не отображает истинное представление раздела проиндексированных файлов. Вывод git status отображает различия между историей коммитов и разделом проиндексированных файлов. Давайте рассмотрим содержимое раздела проиндексированных файлов на данный момент.
$ git ls-files -s
100644 8e66654a5477b1bf4765946147c49509a431f963 0 new_file
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file
Мы видим, что в индекс добавлен файл new_file . Мы внесли изменения в файл reset_lifecycle_file , но его SHA в разделе проиндексированных файлов ( d7d77c1b04b5edd5acfc85de0b592449e5303770 ) остался прежним. Это ожидаемый результат, поскольку мы не использовали команду git add для добавления этих изменений в раздел проиндексированных файлов. Эти изменения присутствуют и в рабочем каталоге.
Теперь давайте выполним команду git reset —hard и изучим новое состояние репозитория.
$ git reset --hard
HEAD is now at dc67808 update content of reset_lifecycle_file
$ git status
On branch main
nothing to commit, working tree clean
$ git ls-files -s
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file
Здесь мы выполнили «жесткий сброс» с помощью параметра —hard . Вывод Git сообщает, что указатель HEAD показывает на последний коммит, dc67808 . Далее проверяем состояние репозитория с помощью команды git status . Git сообщает, что ожидающих изменений нет. Проверяем также состояние раздела проиндексированных файлов и видим, что он был сброшен к состоянию, предшествовавшему добавлению файла new_file . Изменения, которые мы внесли в файл reset_lifecycle_file , а также добавление файла new_file уничтожены. Важно понимать: восстановить эти потерянные данные невозможно.
‘—mixed
Это режим работы по умолчанию. Указатели ссылок обновляются. Раздел проиндексированных файлов сбрасывается до состояния указанного коммита. Любые изменения, которые были отменены в разделе проиндексированных файлов, перемещаются в рабочий каталог. Давайте продолжим.
$ echo 'new file content' > new_file
$ git add new_file
$ echo 'append content' >> reset_lifecycle_file
$ git add reset_lifecycle_file
$ git status
On branch main
Changes to be committed:
(use "git reset HEAD . " to unstage)
new file: new_file
modified: reset_lifecycle_file
$ git ls-files -s
100644 8e66654a5477b1bf4765946147c49509a431f963 0 new_file
100644 7ab362db063f9e9426901092c00a3394b4bec53d 0 reset_lifecycle_file
В приведенном выше примере мы внесли в репозиторий некоторые изменения: добавили файл new_file и изменили содержимое файла reset_lifecycle_file . Затем эти изменения были добавлены в раздел проиндексированных файлов с помощью команды git add . Теперь выполним команду reset по отношению к репозиторию в данном состоянии.
$ git reset --mixed
$ git status
On branch main
Changes not staged for commit:
(use "git add . " to update what will be committed)
(use "git checkout -- . " to discard changes in working directory)
modified: reset_lifecycle_file
Untracked files:
(use "git add . " to include in what will be committed)
new_file
no changes added to commit (use "git add" and/or "git commit -a")
$ git ls-files -s
100644 d7d77c1b04b5edd5acfc85de0b592449e5303770 0 reset_lifecycle_file
В данном случае мы выполнили «смешанный сброс». Напоминаем, что —mixed является режимом по умолчанию и выполнение команды git reset приведет к тому же результату. Изучение вывода команд git status и git ls-files показывает, что раздел проиндексированных файлов был сброшен до состояния, когда в этом разделе находился только файл reset_lifecycle_file . SHA объекта для файла reset_lifecycle_file был сброшен к предыдущей версии.
Обратите внимание: команда git status показывает нам, что появились изменения в файле reset_lifecycle_file и существует неотслеживаемый файл new_file . Это явный результат действия параметра —mixed . Раздел проиндексированных файлов был сброшен, а ожидающие изменения перемещены в рабочий каталог. Для сравнения, при использовании параметра —hard были сброшены и раздел проиндексированных файлов, и рабочий каталог, что привело к потере этих обновлений.
‘—soft
При передаче аргумента —soft выполняется обновление указателей, и на этом операция сброса останавливается. Раздел проиндексированных файлов и рабочий каталог остаются неизменными. Четко продемонстрировать такое поведение довольно сложно. Давайте продолжим работать с нашим демонстрационным репозиторием и подготовим его к мягкому сбросу.
$ git add reset_lifecycle_file
$ git ls-files -s
100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file
$ git status
On branch main
Changes to be committed:
(use "git reset HEAD . " to unstage)
modified: reset_lifecycle_file
Untracked files:
(use "git add . " to include in what will be committed)
new_file
Здесь мы снова воспользовались командой git add , чтобы добавить измененный файл reset_lifecycle_file в раздел проиндексированных файлов. Чтобы убедиться, что индекс обновлен, посмотрим на вывод команды git ls-files . Теперь в выводе команды git status строка «Changes to be committed» (Изменения, подлежащие коммиту) окрашена в зеленый цвет. Файл new_file из предыдущего примера находится в рабочем каталоге как неотслеживаемый. Удалим его с помощью простой команды rm new_file , поскольку в следующих примерах он нам больше не понадобится.
Теперь давайте мягко сбросим текущее состояние репозитория.
$ git reset --soft
$ git status
On branch main
Changes to be committed:
(use "git reset HEAD . " to unstage)
modified: reset_lifecycle_file
$ git ls-files -s
100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file
Мы выполнили «мягкий сброс». Изучение состояния репозитория с помощью команд git status и git ls-files показывает, что ничего не изменилось. Это ожидаемый результат. Мягкий сброс влияет только на историю коммитов. По умолчанию при вызове команды git reset в качестве целевого коммита используется HEAD . Поскольку HEAD уже указывал на нашу историю коммитов и мы выполнили неявный сброс до HEAD , в реальности ничего не произошло.
Для получения более полного представления о параметре —soft и правильного его использования нам потребуется целевой коммит, отличный от HEAD . У нас уже есть файл reset_lifecycle_file , находящийся в разделе проиндексированных файлов. Давайте создадим новый коммит.
$ git commit -m"prepend content to reset_lifecycle_file"
На данный момент в нашем репозитории находится три коммита. Мы выполним возврат к первому коммиту. Для этого нам потребуется идентификатор первого коммита. Его можно найти, просмотрев вывод команды git log .
$ git log
commit 62e793f6941c7e0d4ad9a1345a175fe8f45cb9df
Author: bitbucket
Date: Fri Dec 1 15:03:07 2017 -0800
prepend content to reset_lifecycle_file
commit dc67808a6da9f0dec51ed16d3d8823f28e1a72a
Author: bitbucket
Date: Fri Dec 1 10:21:57 2017 -0800
update content of reset_lifecycle_file
commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4
Author: bitbucket
Date: Thu Nov 30 16:50:39 2017 -0800
initial commit
Помните, что идентификаторы в истории коммитов уникальны для каждой системы. Это означает, что идентификатор коммита в этом примере будет отличаться от идентификатора, который вы увидите на своей машине. Идентификатор интересующего нас коммита для этого примера — 780411da3b47117270c0e3a8d5dcfd11d28d04a4 . Это идентификатор, соответствующий первичному коммиту «initial commit». Теперь укажем его в качестве целевого для нашей операции мягкого сброса.
Прежде чем вернуться назад во времени, проверим текущее состояние репозитория.
$ git status && git ls-files -s
On branch main
nothing to commit, working tree clean
100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file
Здесь мы выполнили составную команду, состоящую из команд git status и git ls-files -s . Она показывает, что в репозитории есть ожидающие изменения, а файл reset_lifecycle_file в разделе проиндексированных файлов находится в версии 67cc52710639e5da6b515416fd779d0741e3762e . Держа в уме эти сведения, выполним мягкий сброс до нашего первого коммита.
$git reset --soft 780411da3b47117270c0e3a8d5dcfd11d28d04a4
$ git status && git ls-files -s
On branch main
Changes to be committed:
(use "git reset HEAD . " to unstage)
modified: reset_lifecycle_file
100644 67cc52710639e5da6b515416fd779d0741e3762e 0 reset_lifecycle_file
Приведенный выше код выполняет «мягкий сброс», а также вызывает составную команду, которая включает git status и git ls-files и выводит информацию о состоянии репозитория. Мы можем изучить эту информацию и сделать несколько интересных наблюдений. Во-первых, git status указывает на наличие изменений в файле reset_lifecycle_file и сообщает, что эти изменения проиндексированы для выполнения коммита. Во-вторых, входные данные git ls-files указывают, что раздел проиндексированных файлов не изменился и значение SHA осталось прежним: 67cc52710639e5da6b515416fd779d0741e3762e.
Чтобы выяснить, что произошло при этом сбросе, выполним команду git log:
$ git log commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4 Author: bitbucket Date: Thu Nov 30 16:50:39 2017 -0800 initial commit
Теперь вывод команды log показывает, что в истории коммитов находится единственный коммит. Это четко иллюстрирует, что делает параметр —soft . Как и при всех вызовах команды git reset , сначала происходит сброс дерева коммитов. Наши предыдущие примеры с параметрами —hard и —mixed воздействовали на указатель HEAD и не возвращали дерево коммитов в предыдущее состояние. Во время мягкого сброса происходит только сброс дерева коммитов.
Почему же команда git status сообщает о наличии измененных файлов? Это может сбивать с толку. Параметр —soft не затрагивает раздел проиндексированных файлов, поэтому изменения в этом разделе сохраняются в истории коммитов. В этом можно убедиться, просмотрев вывод команды git ls-files -s , показывающий, что SHA для файла reset_lifecycle_file остается неизменным. Напомним, что команда git status показывает не состояние «трех деревьев», а различия между ними. В данном случае она показывает, что изменения в разделе проиндексированных файлов опережают изменения в истории коммитов, как если бы мы уже добавили их в индекс.
Разница между командами git reset и git revert
Команда git revert — это «безопасный» способ отмены изменений, а вот git reset — наоборот. При выполнении git reset есть реальный риск потерять наработки. Команда git reset никогда не удаляет коммиты, однако может оставить их без родителя, т. е. указатель потеряет прямой путь для доступа к ним. Такие коммиты обычно можно найти и восстановить с помощью команды git reflog. После запуска внутреннего «сборщика мусора» система Git навсегда удалит все коммиты без родителя. По умолчанию запуск происходит каждые 30 дней. История коммитов — одно из «трех деревьев Git»; два других — раздел проиндексированных файлов и рабочий каталог — не отличаются таким же постоянством, как коммиты. При использовании данного инструмента необходимо соблюдать осторожность, поскольку это одна из немногих команд Git, которые могут привести к потере наработок.
Команда revert предназначена для безопасной отмены публичных коммитов, а git reset — для отмены локальных изменений в разделе проиндексированных файлов и рабочем каталоге. Поскольку они предназначены для разных целей, их реализация также различается: команда reset полностью удаляет набор изменений, тогда как команда revert оставляет исходный набор изменений и использует новый коммит для применения отмены.
Не используйте reset в публичной истории
Никогда не используйте команду git reset , если после этого в публичный репозиторий были отправлены какие-либо снимки состояния. После того как коммит опубликован, нужно учитывать, что на него могут полагаться другие разработчики.
При удалении коммита, после которого другие участники команды начали работу, могут возникнуть серьезные проблемы. Когда коллеги попытаются синхронизироваться с вашим репозиторием, часть истории проекта будет просто отсутствовать. На следующей схеме показано, что происходит при использовании команды reset для публичного коммита. Ветка origin/main является версией вашей локальной главной ветки main в центральном репозитории.
Как только вы добавите новые коммиты после выполнения команды reset, система Git будет считать, что локальная история отклонилась от ветки origin/main , а коммит слияния, необходимый для синхронизации репозиториев, скорее всего, собьет с толку вашу команду и помешает ей.
Команду git reset <коммит> можно использовать только для локальных экспериментов, в которых что-то пошло не так, а не для публичных изменений. Если необходимо исправить публичный коммит, воспользуйтесь специальной командой git revert .
Примеры
git reset <file>
Удаляет указанный файл из раздела проиндексированных файлов, но оставляет рабочий каталог без изменений. Эта команда удаляет из индекса подготовленный файл, не перезаписывая все изменения.
git reset
Сбрасывает раздел проиндексированных файлов до состояния последнего коммита, но оставляет рабочий каталог без изменений. Эта команда удаляет из индекса все подготовленные файлы, не перезаписывая все изменения, что позволяет повторно собрать снимок состояния с нуля.
git reset --hard
Сбрасывает раздел проиндексированных файлов и рабочий каталог до состояния последнего коммита. Флаг —hard говорит Git, что нужно не только отменить изменения в разделе проиндексированных файлов, но и перезаписать все изменения в рабочем каталоге. Другими словами, эта команда уничтожит все неотправленные изменения, поэтому перед ее использованием убедитесь, что вы действительно хотите удалить ваши локальные наработки.
git reset
Перемещает указатель текущей ветки на более ранний коммит , сбрасывает раздел проиндексированных файлов до состояния этого коммита, но не затрагивает рабочий каталог. Все изменения, внесенные после <коммита> , останутся в рабочем каталоге, чтобы вы могли повторно сделать коммит истории проекта с использованием более мелких и упорядоченных снимков состояния.
git reset --hard
Перемещает указатель текущей ветки на более ранний <коммит> и сбрасывает как раздел проиндексированных файлов, так и рабочий каталог до состояния этого коммита. Эта команда уничтожит не только неотправленные изменения, но и все коммиты, которые были добавлены после указанного коммита.
Удаление файла из раздела проиндексированных файлов
Команда git reset часто встречается при подготовке проиндексированного снимка состояния. В следующем примере предполагается, что у вас есть два файла с именами hello.py и main.py , которые вы уже добавили в репозиторий.
# Edit both hello.py and main.py
# Stage everything in the current directory
git add .
# Realize that the changes in hello.py and main.py
# should be committed in different snapshots
# Unstage main.py
git reset main.py
# Commit only hello.py
git commit -m "Make some changes to hello.py"
# Commit main.py in a separate snapshot
git add main.py
git commit -m "Edit main.py"
Как видите, команда git reset помогает соблюдать согласованность коммитов, позволяя не вносить изменения, которые не связаны со следующим коммитом.
Удаление локальных коммитов
В следующем примере показан более продвинутый сценарий использования. Он демонстрирует, что происходит, когда вы некоторое время работали над новой экспериментальной функцией, но после добавления нескольких коммитов состояния решили полностью все удалить.
# Create a new file called `foo.py` and add some code to it
# Commit it to the project history
git add foo.py
git commit -m "Start developing a crazy feature"
# Edit `foo.py` again and change some other tracked files, too
# Commit another snapshot
git commit -a -m "Continue my crazy feature"
# Decide to scrap the feature and remove the associated commits
git reset --hard HEAD~2
Команда git reset HEAD~2 перемещает указатель текущей ветки на два коммита назад, по сути удаляя из истории проекта оба снимка состояния, которые мы только что создали. Помните, что этот вид команды reset можно использовать только для неопубликованных коммитов. Никогда не выполняйте эту операцию, если вы уже отправили свои коммиты в общий репозиторий.
Резюме
Итак, git reset — это мощная команда, используемая для отмены локальных изменений в репозитории Git. Команда git reset работает с «тремя деревьями Git»: историей коммитов ( HEAD ), разделом проиндексированных файлов и рабочим каталогом. Существует три параметра командной строки, соответствующие этим трем деревьям. Эти параметры — —soft, —mixed и —hard — можно передавать команде git reset .
В этой статье мы воспользовались несколькими другими командами Git для демонстрации reset. Подробнее об этих командах см. на соответствующих страницах: git status, git log, git add, git checkout, git reflog и git revert.
2.4 Основы Git — Операции отмены
В любой момент вам может потребоваться что-либо отменить. Здесь мы рассмотрим несколько основных способов отмены сделанных изменений. Будьте осторожны, не все операции отмены в свою очередь можно отменить! Это одна из редких областей Git, где неверными действиями можно необратимо удалить результаты своей работы.
Отмена может потребоваться, если вы сделали коммит слишком рано, например, забыв добавить какие-то файлы или комментарий к коммиту. Если вы хотите переделать коммит — внесите необходимые изменения, добавьте их в индекс и сделайте коммит ещё раз, указав параметр —amend :
$ git commit --amend
Эта команда использует область подготовки (индекс) для внесения правок в коммит. Если вы ничего не меняли с момента последнего коммита (например, команда запущена сразу после предыдущего коммита), то снимок состояния останется в точности таким же, а всё что вы сможете изменить — это ваше сообщение к коммиту.
Запустится тот же редактор, только он уже будет содержать сообщение предыдущего коммита. Вы можете редактировать сообщение как обычно, однако, оно заменит сообщение предыдущего коммита.
Например, если вы сделали коммит и поняли, что забыли проиндексировать изменения в файле, который хотели добавить в коммит, то можно сделать следующее:
$ git commit -m 'Initial commit' $ git add forgotten_file $ git commit --amend
В итоге получится единый коммит — второй коммит заменит результаты первого.
Примечание
Очень важно понимать, что когда вы вносите правки в последний коммит, вы не столько исправляете его, сколько заменяете новым, который полностью его перезаписывает. В результате всё выглядит так, будто первоначальный коммит никогда не существовал, а так же он больше не появится в истории вашего репозитория.
Очевидно, смысл изменения коммитов в добавлении незначительных правок в последние коммиты и, при этом, в избежании засорения истории сообщениями вида «Ой, забыл добавить файл» или «Исправление грамматической ошибки».
Отмена индексации файла
Следующие два раздела демонстрируют как работать с индексом и изменениями в рабочем каталоге. Радует, что команда, которой вы определяете состояние этих областей, также подсказывает вам как отменять изменения в них. Например, вы изменили два файла и хотите добавить их в разные коммиты, но случайно выполнили команду git add * и добавили в индекс оба. Как исключить из индекса один из них? Команда git status напомнит вам:
$ git add * $ git status On branch master Changes to be committed: (use "git reset HEAD . " to unstage) renamed: README.md -> README modified: CONTRIBUTING.md
Прямо под текстом «Changes to be committed» говорится: используйте git reset HEAD … для исключения из индекса. Давайте последуем этому совету и отменим индексирование файла CONTRIBUTING.md :
$ git reset HEAD CONTRIBUTING.md Unstaged changes after reset: M CONTRIBUTING.md $ git status On branch master Changes to be committed: (use "git reset HEAD . " to unstage) renamed: README.md -> README Changes not staged for commit: (use "git add . " to update what will be committed) (use "git checkout -- . " to discard changes in working directory) modified: CONTRIBUTING.md
Команда выглядит несколько странно, но — работает! Файл CONTRIBUTING.md изменен, но больше не добавлен в индекс.
Примечание
Команда git reset может быть опасной если вызвать её с параметром —hard . В приведённом примере файл не был затронут, следовательно команда относительно безопасна.
На текущий момент этот магический вызов — всё, что вам нужно знать о команде git reset . Мы рассмотрим в деталях что именно делает reset и как с её помощью делать действительно интересные вещи в разделе Раскрытие тайн reset главы 7.
Отмена изменений в файле
Что делать, если вы поняли, что не хотите сохранять свои изменения файла CONTRIBUTING.md ? Как можно просто отменить изменения в нём — вернуть к тому состоянию, которое было в последнем коммите (или к начальному после клонирования, или ещё как-то полученному)? Нам повезло, что git status подсказывает и это тоже.
В выводе команды из последнего примера список изменений выглядит примерно так:
Changes not staged for commit: (use "git add . " to update what will be committed) (use "git checkout -- . " to discard changes in working directory) modified: CONTRIBUTING.md
Здесь явно сказано как отменить существующие изменения. Давайте так и сделаем:
$ git checkout -- CONTRIBUTING.md $ git status On branch master Changes to be committed: (use "git reset HEAD . " to unstage) renamed: README.md -> README
Как видите, откат изменений выполнен.
Важно понимать, что git checkout — — опасная команда. Все локальные изменения в файле пропадут — Git просто заменит его версией из последнего коммита. Ни в коем случае не используйте эту команду, если вы не уверены, что изменения в файле вам не нужны.
Если вы хотите сохранить изменения в файле, но прямо сейчас их нужно отменить, то есть способы получше, такие как ветвление и припрятывание — мы рассмотрим их в главе Ветвление в Git.
Помните, всё что попало в коммит почти всегда Git может восстановить. Можно восстановить даже коммиты из веток, которые были удалены, или коммиты, перезаписанные параметром —amend (см. Восстановление данных). Но всё, что не было включено в коммит и потеряно — скорее всего, потеряно навсегда.
Отмена действий с помощью git restore
Git версии 2.23.0 представил новую команду: git restore . По сути, это альтернатива git reset, которую мы только что рассмотрели. Начиная с версии 2.23.0, Git будет использовать git restore вместо git reset для многих операций отмены.
Давайте проследим наши шаги и отменим действия с помощью git restore вместо git reset .
Отмена индексации файла с помощью git restore
В следующих двух разделах показано, как работать с индексом и изменениями рабочей копии с помощью git restore . Приятно то, что команда, которую вы используете для определения состояния этих двух областей, также напоминает вам, как отменить изменения в них. Например, предположим, что вы изменили два файла и хотите зафиксировать их как два отдельных изменения, но случайно набираете git add * и индексируете их оба. Как вы можете убрать из индекса один из двух? Команда git status напоминает вам:
$ git add * $ git status On branch master Changes to be committed: (use "git restore --staged . " to unstage) modified: CONTRIBUTING.md renamed: README.md -> README
Прямо под текстом «Changes to be committed», написано использовать git restore —staged … для отмены индексации файла. Итак, давайте воспользуемся этим советом, чтобы убрать из индекса файл CONTRIBUTING.md :
$ git restore --staged CONTRIBUTING.md $ git status On branch master Changes to be committed: (use "git restore --staged . " to unstage) renamed: README.md -> README Changes not staged for commit: (use "git add . " to update what will be committed) (use "git restore . " to discard changes in working directory) modified: CONTRIBUTING.md
Файл CONTRIBUTING.md изменен, но снова не индексирован.
Откат изменённого файла с помощью git restore
Что, если вы поймете, что не хотите сохранять изменения в файле CONTRIBUTING.md ? Как легко его откатить — вернуть обратно к тому, как он выглядел при последнем коммите (или изначально клонирован, или каким-либо образом помещён в рабочий каталог)? К счастью, git status тоже говорит, как это сделать. В выводе последнего примера, неиндексированная область выглядит следующим образом:
Changes not staged for commit: (use "git add . " to update what will be committed) (use "git restore . " to discard changes in working directory) modified: CONTRIBUTING.md
Он довольно недвусмысленно говорит, как отменить сделанные вами изменения. Давайте сделаем то, что написано:
$ git restore CONTRIBUTING.md $ git status On branch master Changes to be committed: (use "git restore --staged . " to unstage) renamed: README.md -> README
Важно понимать, что git restore — опасная команда. Любые локальные изменения, внесённые в этот файл, исчезнут — Git просто заменит файл последней зафиксированной версией. Никогда не используйте эту команду, если точно не знаете, нужны ли вам эти несохранённые локальные изменения.
Как отменить git reset hard
Прежде всего надо понять что такое гит и что делает git reset —hard . Гит это набор ссылок, где каждая хранит изменения файлов. Команда git reset перемещает указатель на выбранную ссылку, а флаг —hard еще и обновляет все файлы в соотвествии с ссылкой. Отсюда решение на первый взгляд парадоксальное — чтобы отменить git reset —hard нужно сделать git reset —hard на отмененную ссылку.
echo 'foobaz' > 1.txt git add . git commit -m 'add 1.txt' # [main (root-commit) dd64acb] add 1.txt # 1 file changed, 1 insertion(+) # create mode 100644 1.txt echo 'hellowordl' > 2.txt git add . git commit -m 'add 2.txt' # [main c626c6c] add 2.txt # 1 file changed, 1 insertion(+) # create mode 100644 2.txt git log # c626c6c (HEAD -> main) add 2.txt # dd64acb add 1.txt git reset --hard HEAD^1 # HEAD is now at dd64acb add 1.txt cat 2.txt # cat: 2.txt: No such file or directory # команда git reflog позволяет посмотреть всю историю коммитов и найти хеш нужного нам коммита git reflog # dd64acb HEAD@: reset: moving to HEAD^1 # c626c6c (HEAD -> main) HEAD@: commit: add 2.txt # dd64acb HEAD@: commit (initial): add 1.txt git reset --hard c626c6c # HEAD is now at c626c6c add 2.txt cat 2.txt # hellowordl
Как это отменить?! Git-команды для исправления своих ошибок
Если вы что-то сделали в Git’е, а потом очень сильно пожалели, не отчаивайтесь: возможно, всё можно исправить. Рассказываем, как это сделать.
Если вы ошиблись в Git’е, разобраться, что происходит и как это исправить, — непростая задача. Документация Git — это кроличья нора, из которой вы вылезете только зная конкретное название команды, которая решит вашу проблему.
Рассказываем о командах, которые помогут вам выбраться из проблемных ситуаций.
Вот блин, я сделал что-то не то… У Git ведь есть машина времени?!
git reflog # Тут вы увидите всё, что вы делали # в Git во всех ветках. # У каждого элемента есть индекс HEAD@. # Найдите тот, после которого всё сломалось. git reset HEAD@ # Машина времени к вашим услугам.
Так вы можете восстановить то, что случайно удалили, и откатить слияние, после которого всё сломалось. reflog используется очень часто — давайте поблагодарим того, кто предложил добавить его в Git.
Введение в Git: от установки до основных команд
Я только что сделал коммит и заметил, что нужно кое-что поправить!
# Внесите изменения git add . # или добавьте файлы по отдельности. git commit --amend --no-edit # Теперь последний коммит содержит ваши изменения. # ВНИМАНИЕ! Никогда не изменяйте опубликованные коммиты.
Обычно эта команда нужна если вы что-то закоммитили, а потом заметили какую-то мелочь, например отсутствующий пробел после знака = . Конечно вы можете внести изменения новым коммитом, а потом объединить коммиты с помощью rebase -i , но это гораздо дольше.
Внимание Никогда не изменяйте коммиты в публичной ветке. Используйте эту команду только для коммитов в локальной ветке, иначе вам конец.
Мне нужно изменить сообщение последнего коммита!
git commit --amend # Открывает редактор сообщений коммита.
Тупые требования к оформлению сообщений…
Я случайно закоммитил что-то в мастер, хотя должен был в новую ветку!
# Эта команда создаст новую ветку из текущего состояния мастера. git branch some-new-branch-name # А эта — удалит последний коммит из мастер-ветки. git reset HEAD~ --hard git checkout some-new-branch-name # Теперь ваш коммит полностью независим :)
Команды не сработают, если вы уже закоммитили в публичную ветку. В таком случае может помочь git reset HEAD@ вместо HEAD~ .
Ну отлично. Я закоммитил не в ту ветку!
# Отменяет последний коммит, но оставляет изменения доступными. git reset HEAD~ --soft git stash # Переключаемся на нужную ветку. git checkout name-of-the-correct-branch git stash pop # Добавьте конкретные файл или не парьтесь и закиньте все сразу. git add . git commit -m «Тут будет ваше сообщение» # Теперь ваши изменения в нужной ветке.
Многие в такой ситуации предлагают использовать cherry-pick , так что можете выбрать, что вам больше по душе.
git checkout name-of-the-correct-branch # Берём последний коммит из мастера. git cherry-pick master # Удаляем его из мастера. git checkout master git reset HEAD~ --hard
Самые типичные ошибки и вопросы, связанные с Git, и удобные способы их решения
Я пытаюсь запустить diff, но ничего не происходит
Если вы знаете, что изменения были внесены, но diff пуст, то возможно вы индексировали изменения (через add ). Поэтому вам нужно использовать специальный флаг.
git diff --staged
Конечно, «это не баг, а фича», но с первого взгляда это чертовски неоднозначно.
Мне нужно каким-то образом отменить коммит, который был сделан 5 коммитов назад
# Найдите коммит, который нужно отменить. git log # Можно использовать стрелочки, чтобы прокручивать список вверх и вниз. # Сохраните хэш нужного коммита. git revert [тот хэш] # Git создаст новый коммит, отменяющий выбранный. # Отредактируйте сообщение коммита или просто сохраните его.
Вам не обязательно откатываться назад и копипастить старые файлы, замещая ими новые. Если вы закоммитили баг, то коммит можно отменить с помощью revert .
Помимо этого, откатить можно не целый коммит, а отдельный файл. Но следуя канону Git’а, это будут уже совсем другие команды…
Мне нужно отменить изменения в файле
# Найдите хэш коммита, до которого нужно откатиться. git log # Сохраните хэш нужного коммита. git checkout [тот хэш] --path/to/file # Теперь в индексе окажется старая версия файла. git commit -m «О май гадбл, вы даже не использовали копипаст»
Именно поэтому checkout — лучший инструмент для отката изменений в файлах.
Давай по новой, Миша, всё х**ня
cd .. sudo rm -r fucking-git-repo-dir git clone https://some.github.url/fucking-git-repo-dir.git cd fucking-git-repo-dir
Если вам нужно полностью откатиться до исходной версии (т. е. отменить все изменения), то можете попробовать сделать так.
Будьте осторожны, эти команды разрушительны и необратимы.
# Получить последнее состояние origin. git fetch origin git checkout master git reset --hard origin/master # Удалить неиндексированные файлы и папки. git clean -d --force # Повторить checkout/reset/clean для каждой испорченной ветки.
Эти команды Git нужны для экстренных ситуаций, но пригодиться могут не только они. Про другие команды с пояснениями писали тут: