Как объединить последовательные коммиты с одинаковыми именами?
Представьте себе такой git-репозиторий:
Как видно, что в таком git-репозитории есть две группы коммитов, которые имеют одинаковые имена и идут последовательно:
- сделал фичу A
- сделал фичу A
- сделал фичу B
- сделал фичу B
- сделал фичу B
Как сделать команду git filter-branch такой, чтобы она объединила такие коммиты?
В результате я хочу получить это:

Я не хочу потерять изменения. Суммарные изменения должны попасть в объединённые коммиты.
Это лишь пример, на практике в git-репозитории около 15к коммитов, в которых такие повторения встречаются до 50 коммитов подряд. Так что мне нужна такая команда git filter-branch, которая будет действовать "по правилу". Я не могу знать заранее какие коммиты нужно будет объединить, потому что их очень много.
Ответы (2 шт):
Вам нужен интерактивный 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
