Create a gist now

Instantly share code, notes, and snippets.

What would you like to do?
Rewrite history to squash all commits wiht message starts with 'fixup!' to its first parent. By drdr.xp@gmail.com
#!/bin/bash
usage()
{
local name=${0##*/}
cat >&2 <<-END
Rewrite history to squash all commits wiht message starts with 'fixup!' to
its first parent. By drdr.xp@gmail.com
Usage:
> $name [-f] [-t] [-p <pattern>] <rev-list options>...
Options:
-f Force to remove backup ref from previous $name.
-p <pattern> Squash commits with mssage starts with <pattern>.
By default <pattern> is 'fixup!'.
-t Update all ref touched. Usefull when squashing history with
merges.
By default, with a repo like:
* a6910e2 (master) Merge commit '75275ed'
|\\
| * 75275ed (branch-fix) fixup! ok
| * 2e85eb7 ok
|/
* b66353d init
> $name master
Will only update "master", but leave "branch-fix" where it
was. It results in:
* 0982b05 (master) Merge commit '75275ed'
|\\
| * 377a349 ok
|/
* b66353d init
With "-t" it also update ref "branch-fix" after squashing:
> $name -t master
* 0982b05 (master) Merge commit '75275ed'
|\\
| * 377a349 (branch-fix) ok
|/
* b66353d init
It is same with specifying which ref to update manually:
> $name -t master branch-fix
-c Clean up backup refs which is left by history rewriting.
Backup refs are something like:
"refs/original/refs/heads/master"
Example:
> $name -f -p fixup b66353d..master
moves master from a6910e2 to 4d4a5e4:
* 4d4a5e4 (HEAD, master) ok
| * a6910e2 fixup! fixup! ok
| * 75275ed fixup! ok
| * 2e85eb7 ok
|/
* b66353d init
Tips:
$name uses git-filter-branch to process history, which leaves backup refs
like: "refs/original/refs/heads/master".
I suggest to keep them but if you really find it annoying, just force
auto-squash to run again with empty-history to remove them:
> $name -f HEAD..
END
}
die()
{
echo "Failure $@" >&2
exit 1
}
# workdir=./.git-rewrite/t/
# mappingdir=./.git-rewrite/map
squash_dir=.git-auto-squash
_orig_dir=$(pwd)
mkdir -p "$squash_dir/squash_to" "$squash_dir/tree_to_use" \
&& squash_dir="$(cd "$squash_dir"; pwd)" \
|| exit 1
# Remove squash_dir on exit
trap 'cd "$_orig_dir"; rm -rf "$squash_dir"' 0
find_refs()
{
mkdir -p "$squash_dir/commit" || die "mkdir $squash_dir/commit"
for commit in $(git rev-list --reverse --topo-order --default HEAD --parents --simplify-merges "$@")
do
echo 1 >"$squash_dir/commit/$commit"
done
git show-ref --heads |
while read commit full_ref
do
if test -r "$squash_dir/commit/$commit"
then
echo "$full_ref"
fi
done
}
force=
update_refs=
pattern="fixup!"
while :
do
case $1 in
"-h"|"--help"|"")
usage
exit 0
;;
-f)
shift
force=" -f "
;;
-t)
shift
update_refs=1
;;
-c)
git filter-branch -f --commit-filter "" HEAD..HEAD
exit $?
;;
-p)
shift
pattern="$1"
shift
if test -z $pattern
then
usage
exit 1
fi
;;
*)
break
;;
esac
done
if test "$update_refs" = "1"
then
update_refs=$(find_refs "$@")
echo "Also update ref: $update_refs"
fi
vars='squash_dir="'"$squash_dir"'"'
funs=$(cat << \END
record_squash()
{
local commit=$1
local tree=$2
local parent=$3
local target=
if test -r "$squash_dir/squash_to/$parent"
then
target=$(cat "$squash_dir/squash_to/$parent")
else
target=$parent
fi
echo $target >"$squash_dir/squash_to/$commit"
echo $tree >"$squash_dir/tree_to_use/$target"
}
get_squash_target()
{
local commit=$1
if test -r "$squash_dir/squash_to/$commit"
then
cat "$squash_dir/squash_to/$commit"
else
echo $commit
fi
}
does_contain()
{
local trunk=$1
local branch=$2
test ".$(git rev-list $trunk..$branch -n1)" = "."
}
END)
# The first pass collect commits need to be squashed
git filter-branch $force --commit-filter "$vars; $funs;"'
cat > "$workdir/../this_message"
mes="$(cat "$workdir/../this_message")"
# input arguments are "<tree-ish> -p <firstparent> [-p <parent>].."
tree=$1
shift
parents="$@"
firstparent=$2
case $mes in
"'"$pattern"'"*)
record_squash $GIT_COMMIT $tree $firstparent
;;
*)
# remove: -p first_parent
shift
shift
need_keep=0
# If there is no secondary parent, it is a normal commit and
# should be kept.
if [ -z "$1" ]; then
need_keep=1
fi
# If there are unsquashed and unmerged parent, keep this commit.
# Otherwise its first parent already contains all secondary parent
# then it can be removed.
while [ -n "$1" ]
do
p=$2
shift
shift
if does_contain $firstparent $(get_squash_target $p)
then
:
else
need_keep=1
break
fi
done
if test "$need_keep" = "0"; then
record_squash $GIT_COMMIT $tree $firstparent
fi
;;
esac
git commit-tree $tree $parents < $workdir/../this_message
' "$@"
# without force, our responsibility to remove backup refs
git filter-branch $force --commit-filter "$vars; $funs;"'
if test -r "$squash_dir/squash_to/$GIT_COMMIT"
then
tree=$1
shift;
# normal commit
if test "$#" = "2"
then
skip_commit $tree "$@"
else
# merge, do not pass squashed parent to children.
# this also removed non-squashed parent if there are more than 3
# parents.
skip_commit $tree $1 $2
fi
else
if test -r "$squash_dir/tree_to_use/$GIT_COMMIT"
then
tree=$(cat "$squash_dir/tree_to_use/$GIT_COMMIT")
else
tree=$1
fi
shift # remove tree from args
git commit-tree $tree "$@"
fi
' "$@" $update_refs
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment