Перейти к содержимому

git reset

git reset

Git reset и три дерева

Команда git reset — это инструмент, используемый для отмены изменений. У нее есть три формы вызова, соответствующие трем внутренним системам управления состоянием Git, называемым тремя деревьями Git. Эти системы включают HEAD (историю коммитов), индекс staging и рабочий каталог. Мы рассмотрим каждую из этих систем.

Рабочий каталог

Первое дерево — это рабочий каталог. Оно представляет файлы в файловой системе вашего компьютера, доступные редактору кода для внесения изменений. Рабочий каталог считается определенным коммитом проверенного проекта. Когда проект извлечен, это означает, что его файлы распакованы из репозитория Git.

git reset command

bash
echo 'hello git reset' > edited_file
git status 
#On branch master 
#Changes not staged for commit: 
#(use "git add ..." to update what will be committed) 
#(use "git checkout -- ..." to discard changes in working directory) 
#modified: edited_file

Индекс staging

Следующее дерево — это индекс staging, который отслеживает изменения, подготовленные в рабочем каталоге. В целом детали работы области staging скрыты от пользователей Git. Иногда при разговоре об области staging используются разные выражения, например cache, directory cache, staged files, staging area и т. д.

Здесь нам нужна команда git ls-files, которая считается отладочным инструментом и проверяет состояние индекса staging.

git ls-files -s

bash
git ls-files -s
#543644 a32de29bb3c1d643328b29ae775ad8c2e48c3256 0 edited_file

История коммитов

Последнее дерево — это история коммитов. Команда git commit фиксирует изменения в постоянном снимке, помещенном в индекс staging.

git reset, commit history

bash
git commit -am "edit content of test_file"
#[master ab23324] edit the content of edited_file
#1 file changed, 1 insertion(+)
git status
#On branch master
#nothing to commit, working tree clean

В примере выше вы можете видеть новый коммит с сообщением "edit content of test_file". Изменения привязаны к истории коммитов. На этом этапе запуск git status показывает, что никаких предстоящих изменений в деревьях нет. При вызове git log вы увидите историю коммитов. После того как изменения проходят через три дерева, можно использовать git reset.

Как это работает

На первый взгляд команда git reset имеет некоторое сходство с git checkout, поскольку обе работают с HEAD. Команда git checkout работает исключительно с указателем ссылки HEAD, тогда как команда git reset перемещает указатель ссылки HEAD и указатель ссылки текущей ветки. Лучше понять ее поведение поможет иллюстрация ниже:

git reset1

На этой иллюстрации показана последовательность коммитов в ветке master. Как видите, ссылка HEAD и ссылка ветки master в данный момент указывают на коммит d. Посмотрим, как изменится изображение в случае git checkout b и git reset b.

git checkout b

При выполнении команды git checkout ссылка master по-прежнему указывает на коммит d. Что касается ссылки HEAD, она была перемещена и теперь указывает на коммит b. В результате репозиторий находится в состоянии 'detached HEAD'.

git reset2

git reset b

Команда git reset переключает ссылки HEAD и ветки на указанный коммит. Кроме того, она изменяет состояние трех деревьев. Есть три аргумента командной строки --soft, --mixed и --hard, которые определяют изменение деревьев индекса staging и рабочего каталога.

git reset3

Основные параметры

По умолчанию команда git reset имеет постоянные аргументы --mixed и HEAD. Поэтому вызов git reset эквивалентен вызову git reset --mixed HEAD. Здесь HEAD — это указанный коммит. Вместо него можно использовать любой SHA-1 хеш коммита Git.

git reset4

--hard

Наиболее часто используемый параметр — --hard. Однако его использование связано с некоторыми рисками. При --hard указатели ссылки истории коммитов начинают указывать на указанный коммит. После этого индекс staging и рабочий каталог сбрасываются так, чтобы соответствовать указанному коммиту. Изменения, которые ранее ожидали в индексе staging и рабочем каталоге, сбрасываются до состояния дерева коммита. Любой ожидающий коммит в индексе staging и рабочем каталоге будет потерян. Пример ниже демонстрирует сказанное выше. Сначала выполните следующие команды:

git reset usage

bash
echo 'test content' > test_file
git add test_file
echo 'modified content' >> edited_file

Новый файл с именем test_file был создан и добавлен в репозиторий. Кроме того, содержимое edited_file будет изменено. Теперь проверим состояние репозитория с этими изменениями с помощью команды git status.

git status

bash
git status
#On branch master
#Changes to be committed:
#(use "git reset HEAD ..." to unstage)
#new file: test_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: edited_file

Как видите, теперь есть некоторые ожидающие изменения. Ожидающее изменение для дерева индекса staging — это добавление test_file, а для рабочего каталога — изменения в edited_file. Теперь посмотрим на состояние индекса staging:

git ls-files -s

bash
git ls-files -s
#123126 7a32454a5477b1bf4765946147c49509a431f963 0 test_file
#123126 6c423c1b04b5edd5acfc85de0b592449e5303773 0 edited_file

test_file был добавлен в индекс. edited_file был обновлен, но SHA индекса staging (d7d77c1b04b5edd5acfc85de0b592449e5303770) остается прежним. Эти изменения находятся в рабочем каталоге. Они не были перенесены в индекс staging, поскольку мы не использовали команду git add. На этом этапе мы можем выполнить git reset --hard и увидеть новое состояние репозитория:

git reset --hard

bash
git reset --hard
#HEAD is now at ab23324 update content of edited_file
git status
#On branch master
#nothing to commit, working tree clean
git ls-files -s
#123126 6c423c1b04b5edd5acfc85de0b592449e5303773 0 edited_file

Параметр --hard выполнил "hard reset". Git указывает, что HEAD теперь указывает на недавний коммит ab23324. Затем состояние репозитория проверяется с помощью git status. Git сообщает, что ожидающих изменений нет. Что касается состояния индекса staging, он был сброшен к моменту до добавления test_file. Изменения edited_file и добавление test_file были удалены. Эта потеря не может быть отменена.

--mixed

Режим работы по умолчанию — --mixed. Он обновляет указатели ссылок. Индекс staging сбрасывается до указанного коммита. Отмененные изменения из индекса staging помещаются в рабочий каталог.

the git reset command

bash
echo 'new file content' > test_file
git add test_file
echo 'append content' >> edited_file
git add edited_file
git status
#On branch master
#Changes to be committed:
#(use "git reset HEAD ..." to unstage)
#new file: test_file
#modified: edited_file
git ls-files -s
#123126 6a32154a5477b1bf4765946147c49509a4323d32 0 test_file
#123126 3c3262db063f9e9426901092c00a3394b4bd3445 0 edited_file

В примере выше был добавлен test_file, а содержимое edited_file было изменено. Затем эти изменения были применены к индексу staging с помощью git add. При таком состоянии репозитория теперь пора вызвать git reset.

git reset --mixed

bash
git reset --mixed
git status
#On branch master
#Changes not staged for commit:
#(use "git add ..." to update what will be committed)
#(use "git checkout -- ..." to discard changes in working directory)
#modified: edited_file
#Untracked files:
#(use "git add ..." to include in what will be committed)
#test_file
#no changes added to commit (use "git add" and/or "git commit -a")
git ls-files -s
#123126 6c423c1b04b5edd5acfc85de0b592449e5303773 0 edited_file

--mixed — это режим по умолчанию. Он имеет тот же эффект, что и git reset. git status показывает, что есть изменения в edited_file и что test_file — это неотслеживаемый файл. Это и есть точное поведение --mixed. Индекс staging был сброшен, а ожидающие изменения перемещены в рабочий каталог.

--soft

Аргумент --soft обновляет указатели ссылок и останавливает сброс. Однако он не влияет на индекс staging и рабочий каталог.

git reset --soft

bash
git reset --soft
git status
#On branch master
#Changes to be committed:
#(use "git reset HEAD ..." to unstage)
#modified: edited_file
git ls-files -s
#123126 32a252710639e5da6b515416fd779d0741e4561a 0 edited_file

Мягкий сброс (soft reset) сбрасывает только историю коммитов. По умолчанию он вызывается с HEAD в качестве целевого коммита. Теперь создадим новый коммит, чтобы попробовать --soft с целевым коммитом, который не является HEAD:

git reset and commit

bash
git commit -m "add changes to edited_file"

Теперь в нашем репозитории три коммита. Чтобы найти первый из них, нам нужно проверить его ID, что можно сделать, посмотрев вывод git log.

git reset and git log

bash
git log
#commit 62e793f6941c7e0d4ad9a1345a175fe8f45cb9df
#Author: w3docs
#Date: Fri Nov 1 14:02:07 2019 -0800
#add changes to edited_file
#commit ab23324a6da9f0dec51ed16d3d8823f28e1a72a
#Author: w3docs
#Date: Fri Nov 1 11:31:58 2019 -0800
#change content of edited_file
#commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4
#Author: w3docs
#Date: Thu Sep 31 18:40:29 2019 -0800
#initial commit

Это ID начального коммита. Теперь он будет использоваться как цель для мягкого сброса. Сначала нам нужно проверить текущее состояние репозитория:

git status && git ls-files -s

bash
git status && git ls-files -s
#On branch master
#nothing to commit, working tree clean
#123126 32a252710639e5da6b515416fd779d0741e4561a  0 edited_file

Теперь мы можем выполнить мягкий сброс к первому коммиту:

git reset --soft

bash
git reset --soft 780411da3b47117270c0e3a8d5dcfd11d28d04a4
git status && git ls-files -s
#On branch master
#Changes to be committed:
#(use "git reset HEAD ..." to unstage)
#modified: edited_file
#123126 32a252710639e5da6b515416fd779d0741e4561a  0 edited_file

В примере выше мы выполнили мягкий сброс и вызвали комбинированную команду git status и git ls-files, которая выводит состояние репозитория. Команда git status показывает, что есть некоторые изменения в edited_file, выделяя их как изменения, подготовленные для следующего коммита. Вывод git ls-files показывает, что индекс staging остался без изменений и сохраняет SHA 32a252710639e5da6b515416fd779d0741e4561a. Давайте дополнительно рассмотрим состояние репозитория после мягкого сброса с помощью git log:

git reset

bash
git log
#commit 780411da3b47117270c0e3a8d5dcfd11d28d04a4
#Author: w3docs
#Date: Thu Sep 31 18:40:29 2019 -0800
#initial commit

Как видим, вывод выше показывает, что в истории коммитов остался один коммит. Как и при всех вызовах git reset, сначала --soft сбрасывает дерево коммитов.

В отличие от --hard и --mixed, которые воздействуют на HEAD, мягкий сброс откатил дерево коммитов назад во времени.

Разница между командами reset и revert

Git revert считается более безопасным способом отмены изменений, чем git reset. При использовании reset существует высокая вероятность потерять работу. git reset не удаляет коммит, но может сделать коммит “orphaned”. Это означает, что прямого способа доступа к нему нет. В результате Git удалит все orphaned-коммиты, когда запустит внутренний сборщик мусора. По умолчанию Git запускает внутренний сборщик мусора каждые 30 дней. Orphaned-коммиты обычно находят с помощью команды git reflog.

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

Недопустимость сброса публичной истории

Не используйте git reset <commit>, когда после <commit> есть снимки, которые уже перенесены в публичный репозиторий. Когда вы публикуете коммит, учитывайте, что на него полагаются и другие разработчики. Удаление коммитов, над которыми работают и другие участники команды, вызовет множество проблем. Используйте git reset <commit> только для локальных изменений. Чтобы исправить публичные изменения, используйте команду git revert.

Примеры

Используйте следующее, чтобы удалить указанный файл из области staging, не изменяя рабочий каталог. Это снимет файл с подготовки, не перезаписывая изменения:

git reset file

bash
git reset file

Используйте следующее, чтобы сбросить область staging так, чтобы она соответствовала последнему коммиту, но оставить рабочий каталог без изменений. Это снимет подготовку со всех файлов, не перезаписывая изменения, и даст вам возможность заново собрать подготовленный снимок с нуля:

git reset staging area

bash
git reset

Используйте следующее, чтобы сбросить область staging и рабочий каталог так, чтобы они соответствовали последнему коммиту. Это снимет подготовку и перезапишет все изменения в рабочем каталоге:

git reset --hard command

bash
git reset --hard

Используйте следующее, чтобы переместить указатель ветки назад во времени к коммиту, сбросив область staging так, чтобы она ему соответствовала, но не затрагивая рабочий каталог:

git reset commit

bash
git reset commit

Используйте следующее, чтобы переместить указатель текущей ветки назад к коммиту и сбросить область staging и рабочий каталог так, чтобы они ему соответствовали:

git reset --hard commit

bash
git reset --hard commit

Удаление локальных коммитов

Как упоминалось выше, вы можете использовать команду git reset для удаления коммитов в локальном репозитории. Пример ниже демонстрирует такое использование git reset. Команда git reset --hard HEAD~2 сдвигает текущую ветку назад на два коммита и удаляет два недавно созданных снимка из истории проекта.

git add command

bash
# Create a new file called `yourname.txt` and add some code to it
# Commit it to the project history
git add yourname.txt
git commit -m "Start to develop a project"
# Edit `yourname.txt` again and change some other tracked files, too
# Commit another snapshot
git commit -a -m "Continue developing"
# Scrap the project and remove the related commits
git reset --hard HEAD~2

Снятие файлов с подготовки

Команда git reset обычно используется для создания подготовленных снимков. В примере ниже у нас есть 2 файла с именами task.txt и index.txt, которые были добавлены в репозиторий. git reset позволяет снять с подготовки изменения, не связанные со следующим коммитом.

git reset unstaging files

bash
# Edit task.txt and index.txt
# Stage everything in the current directory
git add .
# Realize that the changes in task.txt and index.txt
# should be committed in different snapshots
# Unstage index.txt
git reset index.txt
# Commit only task.txt
git commit -m "Edit task.txt"
# Commit index.txt in a separate snapshot
git add index.txt
git commit -m "Edit index.txt"

Practice

What are the features and options of the 'git reset' command in Git?

Считаете ли это полезным?

Предпросмотр dual-run — сравните с маршрутами Symfony на продакшене.