Сброс и откат истории git

21.08.2021

При использовании git иногда возникает необходимость откатывать изменения. Причиной тому могут быть внезапно возникшие баги, которые не удалось выявить на этапе тестирования. А если речь идет о локальном репозитории, то причин может быть еще больше.

Создадим тестовый репозиторий с которым будет проводить эксперименты:

•••
bash
> git init Инициализирован пустой репозиторий Git в /home/byurrer/reps/revert/.git/ > touch file.txt > git commit -am 'added file.txt' [master (корневой коммит) b9d0814] added file.txt 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 file.txt > echo "1" >> file.txt > git commit -am 'writed 1' [master 7c94686] writed 1 1 file changed, 1 insertion(+) > echo "2" >> file.txt > git commit -am 'writed 2' [master 911d49a] writed 2 1 file changed, 1 insertion(+) > echo "3" >> file.txt > git commit -am 'writed 3' [master 894f7ff] writed 3 1 file changed, 1 insertion(+) > git log --oneline 894f7ff (HEAD -> master) writed 3 911d49a writed 2 7c94686 writed 1 b9d0814 added file.txt

Сброс истории reset

reset - инструмент изменения истории по 3 направлениям, иначе их называют деревья (дополнительно про деревья можн опочитать здесь и здесь):

  • история коммитов (сюда же можно отнести HEAD)
  • раздел проиндексированных файлов
  • рабочий каталог
git reset не должна применяться к публичной истории, так как при синхронизации между участниками разработки, их локальные репозитории будут отставать.

Команда git reset имеет 3 опции, каждая из которых возвдейтвует на определенный набор деревьев:

  • --soft - меняет дерево коммитов
  • --mixed - меняет дерево коммитов и раздел проиндексированных файлов
  • --hard - меняет дерево коммитов, раздел проиндексированных файлов и рабочий каталог
Для дальнейшего рассмотрения сферы влияния каждой из перечисленных опций, внесем изменения в репозиторий, добавив в файл file.txt еще одну строку, закоммитим и просмотрим состояние репозитория:
•••
bash
> echo "4" >> file.txt > git commit -am 'writed 4' [master 5fafd6c] writed 4 1 file changed, 2 insertions(+) > git status На ветке master нечего коммитить, нет изменений в рабочем каталоге > git log --oneline 5fafd6c (HEAD -> master) writed 4 894f7ff writed 3 911d49a writed 2 7c94686 writed 1 b9d0814 added file.txt

soft

Сделаем мягкий сброс к предыдущему коммиту и посмотрим состояние репозитория:

•••
bash
> git reset --soft 894f7ff > git status На ветке master Изменения, которые будут включены в коммит: (use "git restore --staged <file>..." to unstage) изменено: file.txt > git log --oneline 894f7ff (HEAD -> master) writed 3 911d49a writed 2 7c94686 writed 1 b9d0814 added file.txt

git reset --soft сбрасывает только историю коммитов, не затрагивая рабочую директорию и раздел проиндексированных файлов. При помощи мягкого сброса можно удалять коммиты, без потери индекса данных и изменений файлов.

mixed

Сделаем смешанный сброс к предыдущему коммиту и посмотрим состояние репозитория:

•••
bash
> git reset --mixed 894f7ff Непроиндексированные изменения после сброса: M file.txt > git status На ветке master Изменения, которые не в индексе для коммита: (используйте «git add <файл>…», чтобы добавить файл в индекс) (use "git restore <file>..." to discard changes in working directory) изменено: file.txt нет изменений добавленных для коммита (используйте «git add» и/или «git commit -a»)

git reset --mixed сбрасывает историю коммитов и раздел проидексированных файлов, не затрагивая рабочую директорию. При помощи смешанного сброса можно удалять коммиты и индексированные данные, но без потери изменений файлов.

hard

Сделаем жесткий сброс к предыдущему коммиту и посмотрим состояние репозитория:

•••
bash
> git reset --hard 894f7ff HEAD сейчас на 894f7ff writed 3 > git status На ветке master нечего коммитить, нет изменений в рабочем каталоге > git log --oneline 894f7ff (HEAD -> master) writed 3 911d49a writed 2 7c94686 writed 1 b9d0814 added file.txt

git reset --hard сбрасывает все. Без возможности восстановления.

Откат изменений revert

revert - безопасное исключение изменений коммита с сохранением истории и добавлением нового коммита.

Пробуем отменить последний коммит:

•••
bash
> git revert HEAD Revert "writed 3" This reverts commit d3e6055f6a8a55fb9f4c84c8c061edc7dbab2d93. # Пожалуйста, введите сообщение коммита для ваших изменений. Строки, # начинающиеся с «#» будут проигнорированы, а пустое сообщение # отменяет процесс коммита. # # На ветке master # Ваша ветка обновлена в соответствии с «origin/master». # # Изменения, которые будут включены в коммит: # изменено: file.txt #

Посмотрим историю репозитория:

•••
bash
> git log --oneline 136b515 (HEAD -> master) Revert "writed 3" 894f7ff writed 3 911d49a writed 2 7c94686 writed 1 b9d0814 added file.txt

Вернем все как было (для чистоты следующих примеров):

•••
bash
> git reset --hard 894f7ff HEAD сейчас на 894f7ff writed 3

Попробуем отменить коммит ниже текущего:

•••
bash
> git revert 911d49a Автослияние file.txt КОНФЛИКТ (содержимое): Конфликт слияния в file.txt error: не удалось обратить изменения коммита 911d49a… writed 2 подсказка: после разрешения конфликтов, пометьте исправленные пути подсказка: с помощью «git add <пути>» или «git rm <пути>» подсказка: и сделайте коммит с помощью «git commit»

Еще раз взглянем на историю репозитория:

•••
bash
> git log --oneline 894f7ff (HEAD -> master) writed 3 911d49a writed 2 7c94686 writed 1 b9d0814 added file.txt

Отменяя коммит 911d49a остается коммит 894f7ff после него, который вносит изменения в тот же файл что и коммит 911d49a, что ведет к конфликту слияния.

В данном случае есть вариант решать конфликт вручную, либо произвести серию отмен (revert) коммитов до нужного коммита:

•••
bash
> git revert 7c94686..HEAD [master 8f2ff6a] Revert "writed 3" 1 file changed, 1 deletion(-) [master 97aaf82] Revert "writed 2" 1 file changed, 1 deletion(-)

Посмотрим историю:

•••
bash
> git log --oneline 97aaf82 (HEAD -> master) Revert "writed 2" 8f2ff6a Revert "writed 3" 894f7ff writed 3 911d49a writed 2 7c94686 writed 1 b9d0814 added file.txt

Серия отмен коммитов происходит в полуоткрытом промежутке (7c94686, HEAD], то есть отменяется все начиная от HEAD и до 7c94686, но не включая 7c94686.

Теперь добавим в репозиторий новый файл:

•••
bash
> touch file2.txt > git add . > git commit -m 'added file2.txt' [master ae1a44b] added file2.txt 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 file2.txt > git log --oneline ae1a44b (HEAD -> master) added file2.txt 894f7ff writed 3 911d49a writed 2 7c94686 writed 1 b9d0814 added file.txt

Появился новый коммит не затрагивающий изменения файла file.txt. Попробуем отменить коммит ниже текущего и отменить последние изменения в файле file.txt:

•••
bash
> git revert 894f7ff [master 136b515] Revert "writed 3" 1 file changed, 1 deletion(-)

Очевидно, конфликтов нет.

revert отменяет изменения коммита не удаляя его из истории, создавая при этом новый коммит

Вывод

git revert предназначена для безопасной отмены публичных коммитов, а git reset - для отмены локальных изменений.