Переписываем список коммитеров в Git

git git-history

Иногда возникает потребность переписать коммитеров в Git-репозитории. Задача достаточно редкая, но иногда всё-таки приходится ей заниматься. Давайте разберёмся в ситуации подробней. Прежде всего, взглянем на текущий список коммитеров:

$ git log --pretty=format:"%an <%aE>" | sort -u

Допустим, мы получили следующий список:

Ivan <ivan@gmail.com>
Ivan <ivan.ivanov@gmail.com>
Ivan <ivan-ivan@gmail.com>
Ivan Ivanov <ivan.ivanov@gmail.com>
Vanya Ivanov <ivan.ivanov@gmail.com>
Vanya <ivan.ivanov@gmail.com>

Наблюдаем следующую проблему: некий Иван Иванов делал коммиты, указывая каждый раз разную информацию об имени пользователя и почтовом адресе. Для начала нужно дать по рукам Ивану и сказать, чтобы больше так не делал. Лучше всего использовать для всех коммитов одинаковую учётную информацию (например, Ivan Ivanov <ivan.ivanov@gmail.com>). Проблема может встать особенно остро, если в проекте используются дополнительные сервисы, которые работают с репозиторием (code review system, build server и т.п.). Ну, а пока Иван размышляет над своим поведением, мы займёмся переписыванием истории.

На текущий момент в репозитории имеется ряд коммитеров с именем Ivan . Давайте их все объединим! А поможет нам в этом замечательная команда git filter-branch:

$ git filter-branch --commit-filter '
        if [ "$GIT_AUTHOR_NAME" = "Ivan" ];
        then
                GIT_AUTHOR_NAME="Ivan Ivanov";
                GIT_AUTHOR_EMAIL="ivan.ivanov@gmail.com";
                git commit-tree "$@";
        else
                git commit-tree "$@";
        fi' HEAD

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

Ivan Ivanov <ivan.ivanov@gmail.com>
Vanya Ivanov <ivan.ivanov@gmail.com>
Vanya <ivan.ivanov@gmail.com>

После перезаписи истории git сохраняет оригинальные указатели на ветки в .git/refs/original . Для выполнения следующей перезаписи истории (давайте объединим пользователей по почтовому адресу) нам необходимо либо удалить эту папку, либо выполнить команду с ключом -f :

$ git filter-branch -f --commit-filter '
        if [ "$GIT_AUTHOR_EMAIL" = "ivan.ivanov@gmail.com" ];
        then
                GIT_AUTHOR_NAME="Ivan Ivanov";
                GIT_AUTHOR_EMAIL="ivan.ivanov@gmail.com";
                git commit-tree "$@";
        else
                git commit-tree "$@";
        fi' HEAD

Ура! У нас остался единственный коммитер:

Ivan Ivanov <ivan.ivanov@gmail.com>

Проверив правильность изменений, можно с чистой совестью удалить папку .git/refs/original с бэкапом данных. После этого в репозитории будет находится много мусора. Не помешает явно избавиться от него c помощью git gc:

$ git gc

Далее наступает ответственный этап: отправка изменённой истории на сервер:

$ git push --all -f origin

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

Важно!

  • Помните, что коммиты содержат SHA-1 своих родителей, поэтому будут переписаны SHA-1 не только целевых коммитов, но и всех их потомков. Соответственно, если была привязка сторонних сервисов к коммитам вашего репозитория по SHA-1, то она «погибнет» после перезаписи истории. Да и остальные разработчики в вашей команде будут безмерно удивлены полностью переписанной истории на сервере. Поэтому не используйте перезапись истории на сервере, если у вас нет действительно веских причин для этого.
  • Процесс перезаписи истории быстрым не назовёшь, время работы прямо пропорционально общему количеству коммитов. Если ваш репозиторий достаточно большой, то придётся запастись терпением.