Как объединить последовательные коммиты с одинаковыми именами?

Представьте себе такой git-репозиторий:

git graph

Как видно, что в таком git-репозитории есть две группы коммитов, которые имеют одинаковые имена и идут последовательно:

  • сделал фичу A
  • сделал фичу A
  • сделал фичу B
  • сделал фичу B
  • сделал фичу B

Как сделать команду git filter-branch такой, чтобы она объединила такие коммиты?

В результате я хочу получить это:
git graph

Я не хочу потерять изменения. Суммарные изменения должны попасть в объединённые коммиты.

Это лишь пример, на практике в git-репозитории около 15к коммитов, в которых такие повторения встречаются до 50 коммитов подряд. Так что мне нужна такая команда git filter-branch, которая будет действовать "по правилу". Я не могу знать заранее какие коммиты нужно будет объединить, потому что их очень много.


Ответы (2 шт):

Автор решения: Roman Konoval

Вам нужен интерактивный rebase.

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

git rebase -i HEAD~6

В результате выполнения, откроется редактор который покажет вам историю комитов, с подсказками какие операции возможны:

pick f7f3f6d git init
pick 310154e feature A
pick a5f4a0d feature A
pick ffaabbd fix
pick a5fbb0d feature B
pick b5f4a03 feature B
pick f5fea00 feature B

# Rebase 710f0f8..a5f4a0d onto 710f0f8
#
# 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

Теперь комиты, которые хотите оставить не трогаете, т.е. действие в первой колонке должно быть pick - взять как есть, а для тех, которые нужно слить в предыдущие отбрасывая их собственное сообщение, меняете действие на fixup:

pick f7f3f6d git init
pick 310154e feature A
fixup a5f4a0d feature A
pick ffaabbd fix
pick a5fbb0d feature B
fixup b5f4a03 feature B
fixup f5fea00 feature B

и сохраняете и закрываете редактор.

→ Ссылка
Автор решения: Алексей Ковальчук

Оставлю решение, которое получилось у меня.

Допустим, git-репозиторий находится в директории /path/to/git/. Создаем копию этого репозитория во временной директории:

cp -r /path/to/git /tmp/git-original

Позже напишу, зачем это нужно.

Далее нужно создать такой скрипт по пути /tmp/check-skip-commit.sh:

#!/bin/bash

CURRENT_DIRECTORY=`pwd`;
GIT_COMMIT="$1"
GIT_ORIGINAL_PATH="$2"

cd $GIT_ORIGINAL_PATH
CHILDREN=`git rev-list --all --not $GIT_COMMIT^@ --children | grep "^$GIT_COMMIT" | sed -E "s/[^ ]+ |$GIT_COMMIT//"`
cd $CURRENT_DIRECTORY

if [ -z "$CHILDREN" ]
then
   exit 1
fi

if [[ $CHILDREN == *" "* ]]
then
   exit 1
fi

cd $GIT_ORIGINAL_PATH
COMMIT_MESSAGE=`git log -1 --pretty=format:%s $GIT_COMMIT`
CHILD_MESSAGE=`git log -1 --pretty=format:%s $CHILDREN`
cd $CURRENT_DIRECTORY

if [ "$COMMIT_MESSAGE" = "$CHILD_MESSAGE" ]
then
   exit 0
fi

exit 1

И делаем его исполняемым:

chmod 777 /tmp/check-skip-commit.sh

Этот скрипт принимает первым параметром hash коммита, а вторым - путь к директории git-репозитория.

Этот скрипт проверяет, нужно ли пропустить коммит или нет. Для тех коммитов, у которых имеется только один дочерний коммит с таким же именем, скрипт вернет exit code равный 1, а иначе 0. Пропуск коммита по сути означает его удаление, а изменения коммита будут перенесены в его дочерний коммит, что по сути является объединением двух коммитов.

Дальше осталось запустить процесс, переходим в директорию с репозиторием:

cd /path/to/git/

И запускам команду:

git filter-branch -f --commit-filter '
    if /tmp/check-skip-commit.sh $GIT_COMMIT /tmp/git-original
    then
        skip_commit "$@";
    else
        git commit-tree "$@";
    fi' -- --all

В этой команде используется наш скрипт /tmp/check-skip-commit.sh. Вторым аргументом для него передается путь к копии репозитория /tmp/git-original. Это нужно для того, чтобы скрипт мог получить имя коммита и имя дочернего коммита. Использовать оригинальный репозиторий /path/to/git/ не получится, потому что он уже в процессе перезаписи (по крайней мере у меня так не работает, выдает ошибки).


Можно усложнить условие объединения. Для этого в скрипте /tmp/check-skip-commit.sh можно изменить это условие под себя:

if [ "$COMMIT_MESSAGE" = "$CHILD_MESSAGE" ]
then
   exit 0
fi
→ Ссылка