Калина Алексей блог программиста

Правим историю Git с помощью интерактивного rebase

Система контроля версий — неотъемлемый инструмент в рабочем процессе любого разработчика. С его помощью вы можете сохранять состояния вашего проекта, переключаться между ними и поддерживать разработку несколькими людьми. Самой популярной системой контроля версий на текущий момент является git.

В этом посте мы не будем изучать основы git — есть множество туториалов, в том числе интерактивных. Предполагается, что вы знаете как создавать коммиты, просматривать историю и выполнять другие базовые команды. Мы займемся не самой распространенной проблемой при использовании git. Бывают ситуации, когда по невнимательности или чьей-то некомпетентности в репозитории оказывается нежелательная информация. Возможно даже, что после этого она была удалена и в текущей версии проекта ее нет, но она осталась в истории git и, откатившись к определенной версии, ее можно будет восстановить. Изменять историю мы будем с помощью git-команды rebase в режиме interactive.

Какие проблемы будем решать

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

Предостережение

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

Исправить опечатку в commit message

Для демонстрации возможностей interactive rebase я создал тестовый репозиторий, в котором буду моделировать описанные выше проблемы.

Начнем с ситуации, когда вы сделали коммит и сразу осознали, что допустили ошибку в его message:

* 0be0764 | add info about tet in readme (HEAD -> commit-message-typo) [Alexey Kalina]
* 972078a | add test on division by zero [Alexey Kalina]
* 005fd5c | init (master) [Alexey Kalina]

В этом случае использование interactive rebase даже не потребуется. Все, что вам нужно — использовать команду git commit --amend, которая позволяет изменить предыдущий коммит. После вызова команды откроется дефолтный редактор, в котором нужно исправить текст сообщения, сохраниться и выйти.

add info about test in readme
    
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# Date:      Sun Nov 3 11:31:50 2019 +0300
#
# On branch commit-message-typo
# Changes to be committed:
#       modified:   README.md
#

Аналогично решаются и остальные проблемы в случае, когда неудачный коммит — последний (перед вызовом git commit --amend сделайте необходимые изменения и добавьте файлы в stage-область). Поэтому далее будем рассматривать только вариант, когда после проблемного коммита были и другие.

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

* 27917ff | update readme (HEAD -> commit-message-typo) [Alexey Kalina]
* 84e261c | add new calculation test [Alexey Kalina]
* 0be0764 | add info about tet in readme [Alexey Kalina]
* 972078a | add test on division by zero [Alexey Kalina]
* 005fd5c | init (master) [Alexey Kalina]

Здесь в дело вступает интерактивный rebase. Команда rebase позволяет перемещать коммиты между ветками, в данном случае мы будем их перемещать на то же самое место. А благодаря интерактивному режиму (флаг --interactive/-i), каждый из перемещаемых коммитов можно редактировать, изменяя текст сообщения, используемые файлы и их содержимое.

Необходимо вызвать rebase на коммите, предшествующем тому, в котором нужно, что-то изменить. Для этого нужно либо передать хэш этого коммита (972078a), либо насколько он отстает от текущего (HEAD~3). Я предпочитаю первый вариант, поскольку история может быть гораздо длиннее, а считать коммиты не хочется.

git rebase -i 972078a

Вновь откроется дефолтный редактор, в котором необходимо выбрать коммиты для изменения.

pick 0be0764 add info about tet in readme
pick 84e261c add new calculation test
pick 27917ff update readme

# Rebase 972078a..27917ff onto 972078a (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup <commit> = like "squash", but discard this commit's log message
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
# .       create a merge commit using the original merge commit's
# .       message (or the oneline, if no original merge commit was
# .       specified). Use -c <commit> to reword the commit message.
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

Как можно увидеть, вся необходимая информация описана в этом файле. Изначально все коммиты берутся без изменений (об этом говорит слово pick). Для тех коммитов, которые требуют исправлений, замените pick на соответствующий вариант. Так как мы хотим изменить сообщение коммита 0be0764, заменим pick на reword либо в сокращенном варианте – r:

reword 0be0764 add info about tet in readme

После того, как вы сохранитесь и выйдете, откроется редактор с точно таким же содержимым, как в случае commit --amend. Остается только изменить текст сообщения и выйти. История отредактирована:

* dcb51d5 | update readme (HEAD -> commit-message-typo) [Alexey Kalina]
* 9a01464 | add new calculation test [Alexey Kalina]
* 6212924 | add info about test in readme [Alexey Kalina]
* 972078a | add test on division by zero [Alexey Kalina]
* 005fd5c | init (master) [Alexey Kalina]

Удалить приватную информацию из файла

Допустим, в какой-то момент мы добавили в публичный доступ логин и пароль к нашему сервису. Так выглядит история:

* 11b5cef | add version settings (HEAD -> private-info) [Alexey Kalina]
* b3b8d34 | add test settings [Alexey Kalina]
* b94ffdb | add login and password [Alexey Kalina]
* bfe4560 | add config [Alexey Kalina]
* 005fd5c | init (master) [Alexey Kalina]

Снова воспользуемся interactive rebase. Вызов на коммите bfe4560, откроет редактор со следующим содержимым:

pick b94ffdb add login and password
pick df4044b add test settings
pick d9f0ff4 add version settings

У нас есть два варианта. Если коммит b94ffdb содержит и другие изменения помимо нежелательных, то следует заменить pick на edit, что означает исправление данного коммита, и переименовать коммит соответствующим образом. В случае, если это единственная правка в коммите, то его следует удалить (для этого используйте слово drop).

Если вы решили исправить коммит, то после закрытия редактора, репозиторий окажется в состоянии соответствующем этому коммиту. Сделайте изменения и выполните команду git commit --amend, тем самым переписав коммит. Далее выполните команду git rebase --continue, которая продолжает процесс rebase, перемещаясь вверх по истории. Если изменения, которые вы произвели пересекаются с изменениями в следующих коммитах, вам будет предложено исправить конфликты. Например:

{
<<<<<<< HEAD
    "configuration": "prod"
=======
    "configuration": "prod",
    "login": "admin",
    "password": "password",
    "test_dir": "test/"
>>>>>>> b3b8d34... add test settings
}

Исправьте состояние файла на текущем коммите:

{
    "configuration": "prod",
    "test_dir": "test/"
}

Далее добавьте файл в stage-область с помощью git add и продолжите процесс командой git rebase --continue. После завершения rebase получаем вывод:

Successfully rebased and updated refs/heads/private-info

В случае удаления коммита процесс будет аналогичным.

Избавиться от ненужных файлов

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

* e22c934 | delete logs (HEAD -> log-files) [Alexey Kalina]
* 4c5f6eb | add another important feature [Alexey Kalina]
* 6618972 | add important feature [Alexey Kalina]
* 005fd5c | init (master) [Alexey Kalina]

Для этого для начала найдем, в каких коммитах файл с логами менялся (в первую очередь нас интересует, когда он появился). Это можно сделать с помощью команды git log. Укажите путь к файлу, который ищем (используйте --follow, если файл мог быть переименован).

git log -- 0.log

Результат:

* e22c934 | delete logs (HEAD -> log-files) [Alexey Kalina]
* 4c5f6eb | add another important feature [Alexey Kalina]
* 6618972 | add important feature [Alexey Kalina]

Теперь, зная к какому коммиту откатываться, воспользуемся интерактивным rebase.

git rebase -i 005fd5c

Заменим pick на edit для коммита 6618972 и укажем drop вместо pick для коммита e22c934. После этого мы перенесемся в состояние репозитория на коммите, в котором впервые добавили ненужный файл. Здесь нам необходимо физически удалить файл с диска и закоммитить это изменение с помощью git commit --amend. Тут не стоит бояться, что файл пропадет. То, что мы удалили этот файл в промежуточном состоянии никак не повлияет на то, что он останется у вас на диске в итоговом состоянии. Продолжаем rebase с помощью git rebase --continue.

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

Другие возможности interactive rebase

С помощью интерактивного rebase можно существенно изменить историю git-репозитория. Из других наиболее полезных возможностей этой команды я бы выделил объединение и разделение коммитов. Благодаря этим приемам историю можно сделать более аккуратной. С полным списком возможностей можно ознакомиться в документации.