git reset
Подробно о команде git reset: три дерева Git, режимы --soft, --mixed, --hard и примеры использования.

git reset — основная команда для отмены изменений в локальном репозитории. В зависимости от выбранного режима она может убрать файл из индекса, отменить подготовленные изменения или переместить ветку назад к более раннему коммиту. Поскольку команда может переписывать указатель ветки и удалять работу, она мощная, но её легко использовать неправильно.
На этой странице объясняются три «дерева», с которыми работает git reset, рассматриваются три режима (--soft, --mixed, --hard) с примерами, а также показано, когда лучше использовать git revert.
Когда использовать git reset
Используйте git reset, когда нужно переписать локальную историю или индекс, которые ещё не были опубликованы:
- Убрать файл из индекса, добавленный по ошибке:
git reset <file>. - Очистить весь индекс для пересборки следующего коммита с нуля:
git reset. - Полностью отменить локальные коммиты и изменения:
git reset --hard <commit>. - Объединить или переписать несколько последних коммитов перед отправкой.
Если коммиты, которые нужно отменить, уже были отправлены в общую ветку, используйте git revert — эта команда создаёт новый коммит, который отменяет изменения, не переписывая историю, от которой зависят другие участники.
Git reset и три дерева
git reset имеет три формы вызова, соответствующие трём внутренним системам управления состоянием Git, которые часто называют тремя деревьями Git:
- HEAD — история коммитов (снимок, на который сейчас указывает ветка).
- Индекс (staging index) — изменения, подготовленные для следующего коммита.
- Рабочий каталог — файлы на диске в вашем редакторе.
Рассмотрим каждую из этих систем по очереди.
Рабочий каталог
Первое дерево — рабочий каталог. Он представляет файлы на файловой системе компьютера, которые может изменять ваш редактор. Рабочий каталог синхронизирован с конкретным коммитом извлечённого проекта: при извлечении проекта Git распаковывает декомпрессированные версии файлов репозитория на диск.
Если мы редактируем отслеживаемый файл, git status сообщает о нём как об изменённом, ещё не подготовленном изменении в рабочем каталоге:
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 index)
Второе дерево — индекс (staging index), который отслеживает изменения, подготовленные для следующего коммита. Git обычно скрывает внутреннее устройство индекса от пользователя. Его также называют: кэш, кэш каталога, staged files или staging area.
Для непосредственного просмотра индекса можно использовать git ls-files -s — отладочный инструмент, который выводит подготовленные записи вместе с их хэшами объектов:
git ls-files -s
#543644 a32de29bb3c1d643328b29ae775ad8c2e48c3256 0 edited_fileИстория коммитов
Третье дерево — история коммитов. Команда git commit берёт всё, что находится в индексе, и записывает это как постоянный снимок в истории:
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, и указатель текущей ветки. Поведение команды лучше понять с помощью иллюстрации ниже:

На иллюстрации показана последовательность коммитов в ветке master. Как видно, ссылка HEAD и ссылка на ветку master в данный момент указывают на коммит d. Посмотрим, как изменится картина при выполнении git checkout b и git reset b.
git checkout b
При выполнении команды git checkout ссылка на master по-прежнему указывает на коммит d. Что касается ссылки HEAD, она была перемещена и теперь указывает на коммит b. В результате репозиторий находится в состоянии «detached HEAD» (отсоединённый HEAD).

git reset b
Команда git reset перемещает и HEAD, и ссылку текущей ветки на указанный коммит. Она также может изменять состояние индекса и рабочего каталога. Три параметра командной строки — --soft, --mixed и --hard — определяют, насколько далеко распространяется сброс:

Основные параметры
По умолчанию git reset запускается с аргументами --mixed и HEAD. Таким образом, вызов git reset равнозначен git reset --mixed HEAD. Здесь HEAD — целевой коммит; его можно заменить любой ссылкой на коммит, например SHA-1 хэшем (git reset 0a1b2c3) или относительным указателем (git reset HEAD~2).
В таблице ниже показано, какие деревья затрагивает каждый режим:
| Режим | История коммитов (HEAD) | Индекс | Рабочий каталог |
|---|---|---|---|
--soft | перемещается | без изменений | без изменений |
--mixed (по умолчанию) | перемещается | сбрасывается до цели | без изменений |
--hard | перемещается | сбрасывается до цели | сбрасывается до цели (изменения теряются) |
Простой способ запомнить: --soft оставляет изменения подготовленными, --mixed переводит их в неподготовленные правки, а --hard полностью их удаляет.

--hard
--hard — самый мощный и самый опасный параметр. Он перемещает ссылку истории коммитов на целевой коммит, а затем приводит индекс и рабочий каталог в соответствие с этим коммитом. Любые ожидающие изменения в индексе и рабочем каталоге удаляются — и эту потерю нельзя отменить с помощью git reset.
Предупреждение:
--hardбезвозвратно удаляет незакоммиченную работу. Сначала выполнитеgit status, чтобы понять, что вы собираетесь потерять.
Для демонстрации сначала создадим несколько ожидающих изменений:
echo 'test content' > test_file
git add test_file
echo 'modified content' >> edited_fileБыл создан и подготовлен новый файл test_file, а содержимое edited_file изменено в рабочем каталоге. Проверим состояние репозитория командой git status:
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Теперь в двух деревьях есть ожидающие изменения: индекс содержит новый test_file, а рабочий каталог — изменения в edited_file. Посмотрим на состояние индекса:
git ls-files -s
#123126 7a32454a5477b1bf4765946147c49509a431f963 0 test_file
#123126 6c423c1b04b5edd5acfc85de0b592449e5303773 0 edited_fileФайл test_file был добавлен в индекс. Файл edited_file был изменён, но SHA индекса (d7d77c1b04b5edd5acfc85de0b592449e5303770) остаётся прежним. Эти изменения находятся в рабочем каталоге. Они не перенесены в индекс, поскольку команда git add не выполнялась. Теперь выполним git reset --hard и проверим новое состояние репозитория:
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 выполнил «жёсткий сброс». Git сообщает, что HEAD указывает на последний коммит ab23324. Затем состояние репозитория проверяется с помощью git status. Git сообщает, что ожидающих изменений нет. Что касается состояния индекса, он был сброшен до точки до добавления test_file. Изменения в edited_file и добавление test_file были удалены. Эту потерю нельзя отменить.
--mixed
--mixed — режим по умолчанию. Он перемещает указатели ссылок и сбрасывает индекс до целевого коммита, но не трогает рабочий каталог. Изменения, убранные из индекса, снова появляются как правки в рабочем каталоге, поэтому ничего не теряется — вы просто получаете возможность повторно подготовить их.
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, оба изменения перенесены в индекс с помощью git add. С таким состоянием репозитория пришло время выполнить git reset:
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. Индекс был сброшен, а ожидающие изменения перенесены в рабочий каталог.
--soft
Аргумент --soft перемещает указатели ссылок и на этом останавливается. Он не трогает индекс и рабочий каталог, поэтому всё, что было закоммичено, остаётся подготовленным и готовым к повторному коммиту. Это режим, который нужен при объединении нескольких коммитов в один.
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Мягкий сброс перемещает только историю коммитов. По умолчанию он нацелен на HEAD. Создадим новый коммит, а затем выполним сброс --soft с целевым коммитом, отличным от HEAD:
git commit -m "add changes to edited_file"Теперь в репозитории три коммита. Чтобы найти первый, смотрим его ID в выводе git log:
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
#On branch master
#nothing to commit, working tree clean
#123126 32a252710639e5da6b515416fd779d0741e4561a 0 edited_fileТеперь выполним мягкий сброс до первого коммита:
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 показывает, что индекс остался без изменений и сохранил SHA 32a252710639e5da6b515416fd779d0741e4561a. Проверим историю коммитов после мягкого сброса с помощью git log:
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, поскольку git reset может привести к потере работы. git reset не удаляет коммит немедленно, но может сделать его осиротевшим — ни одна ветка или тег больше не указывает на него, поэтому прямого способа добраться до него нет. Git в конечном счёте удаляет осиротевшие объекты при запуске сборщика мусора (git gc), который по умолчанию удаляет недостижимые объекты старше примерно 90 дней (записи рефлога истекают через 90 дней, или через 30 дней для недостижимых). До этого момента обычно можно восстановить осиротевший коммит с помощью команды git reflog.
Другое ключевое различие: git revert предназначен для отмены публичных, уже опубликованных коммитов путём добавления нового обратного коммита, тогда как git reset предназначен для отмены локальных изменений в рабочем каталоге и индексе.
Никогда не сбрасывайте опубликованную историю
Не выполняйте git reset <commit>, если после <commit> есть снимки, которые уже были отправлены в общий репозиторий. После публикации коммита другие разработчики полагаются на него. Перезапись или удаление коммитов, которые уже получили другие участники команды, приведёт к расхождению историй и болезненным конфликтам слияния. Используйте git reset только для коммитов, существующих исключительно в вашем локальном репозитории. Для отмены публичных изменений используйте команду git revert.
Примеры
Удалить конкретный файл из индекса без изменения рабочего каталога — файл убирается из подготовленных, но правки сохраняются:
git reset <file>Сбросить весь индекс в соответствие с последним коммитом, оставив рабочий каталог без изменений. Это убирает все файлы из подготовленных, не удаляя изменения, так что можно заново собрать подготовленный снимок с нуля:
git resetСбросить и индекс, и рабочий каталог в соответствие с последним коммитом. Это удаляет все незакоммиченные изменения в рабочем каталоге:
git reset --hardПереместить указатель ветки назад к заданному коммиту и сбросить индекс в соответствие с ним, но не трогать рабочий каталог:
git reset <commit>Переместить указатель текущей ветки назад к заданному коммиту и сбросить и индекс, и рабочий каталог в соответствие с ним:
git reset --hard <commit>Удаление локальных коммитов
Как показано выше, с помощью git reset можно удалять коммиты в локальном репозитории. В примере ниже git reset --hard HEAD~2 перемещает текущую ветку на два коммита назад, удаляя два последних снимка из истории проекта:
# 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 — точная настройка того, что войдёт в следующий коммит. В примере ниже есть два файла, task.txt и index.txt, оба подготовлены. git reset позволяет убрать из индекса изменения, которые не должны войти в следующий коммит, чтобы каждый файл можно было закоммитить отдельно:
# 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"