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

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

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

Переписывать публичную историю - сомнительная затея, мы же разработчики, а не политики. Но прежде чем оставить след в истории - подумай, насколько он ценен.

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

$ 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 направлениям, иначе их называют деревья (дополнительно про деревья можн опочитать здесь и здесь):

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

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

Для дальнейшего рассмотрения сферы влияния каждой из перечисленных опций, внесем изменения в репозиторий, добавив в файл file.txt еще одну строку, закоммитим и просмотрим состояние репозитория:

$ 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

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

$  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

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

$ 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

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

$ 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 - безопасное исключение изменений коммита с сохранением истории и добавлением нового коммита.

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

$ git revert HEAD

Revert "writed 3"

This reverts commit d3e6055f6a8a55fb9f4c84c8c061edc7dbab2d93.

# Пожалуйста, введите сообщение коммита для ваших изменений. Строки,
# начинающиеся с «#» будут проигнорированы, а пустое сообщение
# отменяет процесс коммита.
#
# На ветке master
# Ваша ветка обновлена в соответствии с «origin/master».
#
# Изменения, которые будут включены в коммит:
#       изменено:      file.txt
#

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

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

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

$ git reset --hard 894f7ff
HEAD сейчас на 894f7ff writed 3

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

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

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

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

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

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

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

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

$ 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.

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

$ 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:

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

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

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

Вывод

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