This repository has been archived by the owner. It is now read-only.
Permalink
Switch branches/tags
v1.5.3-rc4.mingw.2 v1.5.3-rc4.mingw.1 msysGit-v0.6 msysGit-1.6.1-preview20090211 msysGit-1.5.4-rc0-preview20071217 WinGit-1.5.3-preview20071010 WinGit-0.2-alpha WinGit-0.1-alpha GitMe-0.4.2 Git-preview20080301 Git-preview20080221-with-git-svn Git-preview20080220-with-git-svn Git-preview20080219-with-git-svn Git-1.9.5-preview20150319 Git-1.9.5-preview20141217 Git-1.9.4-preview20140929 Git-1.9.4-preview20140815 Git-1.9.4-preview20140611 Git-1.9.2-preview20140411 Git-1.9.0-preview20140217 Git-1.8.5.2-preview20131230 Git-1.8.4-preview20130916 Git-1.8.3-preview20130601 Git-1.8.1.2-preview20130201 Git-1.8.0-preview20121022 Git-1.7.11-preview20120710 Git-1.7.11-preview20120704 Git-1.7.11-preview20120620 Git-1.7.10-preview20120409 Git-1.7.9-preview20120201 Git-1.7.8-preview20111206 Git-1.7.7.1-preview20111027 Git-1.7.7-preview20111014 Git-1.7.7-preview20111012 Git-1.7.6-preview20110720 Git-1.7.6-preview20110708 Git-1.7.4-preview20110204 Git-1.7.3.2-preview20101025 Git-1.7.3.1-preview20101002 Git-1.7.2.3-preview20100911 Git-1.7.1-preview20100612 Git-1.7.0.2-preview20100407.msysGit-netinstall Git-1.7.0.2-preview20100407-2.msysGit-netinstall Git-1.7.0.2-preview20100309 Git-1.7.0.2-preview20100309.msysGit-installers Git-1.6.5.1 Git-1.6.5.1-preview20100112-with-cheetah Git-1.6.4-preview20090730 Git-1.6.4-preview20090729 Git-1.6.3.2-preview20090608 Git-1.6.3.2-preview20090607 Git-1.6.3-preview20090507 Git-1.6.3-preview20090507-2 Git-1.6.2.2-preview20090408 Git-1.6.2.1-preview20090322 Git-1.6.2-preview20090308 Git-1.6.1-preview20081227 Git-1.6.0.2-preview20080923 Git-1.6.0.2-preview20080921 Git-1.5.6.1-preview20080701 Git-1.5.6-preview20080622 Git-1.5.5-preview20080413 Git-1.5.4-rc5-preview20080128 Git-1.5.4-rc3-preview20080113 Git-1.5.4-rc2-preview20071228 Git-1.5.4-rc0-preview20071217 Git-1.5.4-preview20080202 Git-1.5.3.6-preview20071126 Git-1.5.3.5-preview20071114 Git-1.5.3-preview20071027 Git-1.5.3-preview20071019
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 614 lines (558 sloc) 14.4 KB
#!/bin/sh
#
# This script intends to help rebasing a "thicket of branches", i.e. a branch
# that contains merged topic branches.
#
# The main use case is the development of Git for Windows: its integration
# branch needs to be rebased frequently, to facilitate submitting patch series
# to upstream Git. Git for Windows' integration branch contains dozens of
# topic branches that are at various stages of readiness, though, hence it is
# essential to maintain the branch structure.
#
# As an additional convenience, this script supports a method developed by the
# Git for Windows project to rebase while *still* retaining the ability to
# fast-forward from previous states of the branch, called the 'merging
# rebase': instead of starting the rebase directly on top of the upstream
# commit, the previous history is integrated via a "fake" merge (i.e. using
# the 'ours' strategy, in effect reverting all of the changes that are about
# to be rebased).
#
# Example usage (regular Git for Windows workflow)
#
# git fetch junio
# BASE="$(git rev-parse ":/Start the merging-rebase")"
# shears.sh --onto junio/master $BASE
#
# Usage: shears [options] <upstream>
# options:
# -m,--merging[=<message>]
# start the rebased branch by a fake merge of the previous state
# --onto=<commit>
# rebase onto the given revision
# -f,--force
# force operation even if a previous run was aborted unsuccessfully
#
# Technical implementation notes:
#
# The idea is to generate a rebase script with "new" commands, i.e. in
# addition to "pick", "reword" and friends, additional commands are handled:
#
# bud
# rewinds to the <onto> commit, to start "budding" a new branch
#
# mark <nickname>
# assign a nick name to the current revision, for later use with "merge" or
# "rewind"
#
# rewind <nickname>
# reset the HEAD to the given revision (the previous state should be marked
# with a nickname first, to avoid losing commits)
#
# finish <nickname>
# mark a topic branch as complete, starting the next one
#
# merge [-C <commit>] <nickname>
# merge the given topic branch, optionally using the commit message of a
# specific commit
#
# start_merging_rebase
# start the rebase by a fake merge, i.e. incorporating the commits about to
# be rebased but reverting all their changes, to maintain fast-forwardability
#
# cleanup
# clean up all temporary aliases and refs; This *must* be the last command.
#
# To support these additional commands, we not only generate our very own
# rebase script, but then call rebase -i with our fake editor to put the
# rebase script into place and then let the user edit the script.
#
# After letting the user edit the script, we rewrite the rebase script,
# replacing the "new" rebase commands with calls to a temporary alias ".r"
# that simply calls the command implementations contained in the shears.sh
# script itself.
die () {
echo "$*" >&2
exit 1
}
git_dir="$(git rev-parse --git-dir)" ||
die "Not in a Git directory"
help () {
cat >&2 << EOF
Usage: $0 [options] <upstream>
Options:
-m|--merging[=<msg>] allow fast-forwarding the current to the rebased branch
--onto=<commit> rebase onto the given commit
--recreate=<merge> recreate the branch merged in the specified commit
EOF
exit 1
}
# Extra commands for use in the rebase script
extra_commands="edit bud finish mark rewind merge start_merging_rebase cleanup"
edit () {
GIT_EDITOR="$1" &&
GIT_SEQUENCE_EDITOR="$GIT_EDITOR" &&
export GIT_EDITOR GIT_SEQUENCE_EDITOR &&
shift &&
case "$*" in
*/git-rebase-todo)
sed -e '/^noop/d' < "$1" >> "$git_dir"/SHEARS-SCRIPT &&
mv "$git_dir"/SHEARS-SCRIPT "$1"
"$GIT_EDITOR" "$@" &&
mv "$1" "$git_dir"/SHEARS-SCRIPT &&
exprs="$(for command in $extra_commands
do
printf " -e 's/^$command\$/exec git .r &/'"
printf " -e 's/^$command /exec git .r &/'"
done)" &&
eval sed $exprs < "$git_dir"/SHEARS-SCRIPT > "$1"
;;
*)
exec "$GIT_EDITOR" "$@"
esac
}
mark () {
git update-ref -m "Marking '$1' as rewritten" refs/rewritten/"$1" HEAD
}
rewind () {
git reset --hard refs/rewritten/"$1"
}
bud () {
shorthead="$(git rev-parse --short --verify HEAD)" &&
git for-each-ref refs/rewritten/ |
grep "^$shorthead" ||
die "Refusing to leave unmarked revision $shorthead behind"
git reset --hard refs/rewritten/onto
}
finish () {
mark "$@" &&
bud
}
merge () {
# parse command-line arguments
parents=
while test $# -gt 0 && test "a$1" != a-C
do
parents="$parents $1" &&
shift
done &&
if test "a$1" = "a-C"
then
shift &&
orig="$1" &&
shift &&
# determine whether the merge needs to be redone
p="$(git rev-parse HEAD)$parents" &&
o="$(git rev-list -1 --parents $orig |
sed "s/[^ ]*//")" &&
while p=${p# }; o=${o# }; test -n "$p$o"
do
p1=${p%% *}; o1=${o%% *};
test $o1 = "$(git rev-parse "$p1")" || break
p=${p#$p1}; o=${o#$o1}
done &&
# either redo merge or fast-forward
if test -z "$p$o"
then
git reset --hard $orig
return
fi &&
msg="$(git cat-file commit $orig |
sed "1,/^$/d")"
else
msg=
p=
for parent in $parents
do
test -z "$msg" ||
msg="$msg and "
msg="$msg'$parent'"
p="$p $(git rev-parse --verify refs/rewritten/$parent \
2> /dev/null ||
echo $parent)"
done &&
msg="Merge $msg into HEAD"
fi &&
git merge -n --no-ff -m "$msg" $p
}
start_merging_rebase () {
git merge -s ours -m "$(cat "$git_dir"/SHEARS-MERGING-MESSAGE)" "$1"
}
cleanup () {
rm -f "$git_dir"/SHEARS-SCRIPT &&
for rewritten
do
git update-ref -d refs/rewritten/$rewritten
done &&
for rewritten in $(git for-each-ref refs/rewritten/ |
sed 's/^[^ ]* commit.refs\/rewritten\///')
do
test onto = "$rewritten" ||
merge $rewritten
git update-ref -d refs/rewritten/$rewritten
done &&
git config --unset alias..r
}
merging=
base_message=
onto=
recreate=
force=
while test $# -gt 0
do
case "$1" in
-m|--merging)
merging=t
base_message=
;;
--merging=*)
merging=t
base_message="${1#--merging=}"
;;
--onto)
shift
onto="$1"
;;
--onto=*)
onto="${1#--onto=}"
;;
--recreate)
shift
recreate="$recreate $1"
;;
--recreate=*)
recreate="$recreate ${1#--recreate=}"
;;
--force|-f)
force=t
;;
-h|--help)
help
;;
-*)
echo "Unknown option: $1" >&2
exit 1
;;
*)
break
;;
esac
shift
done
case " $extra_commands " in
*" $1 "*)
command="$1"
shift
"$command" "$@"
exit
;;
esac
string2regex () {
echo "$*" |
sed 's/[][\\\/*?]/\\&/g'
}
merge2branch_name () {
git show -s --format=%s "$1" |
sed -n -e "s/^Merge [^']*'\([^']*\).*/\1/p" \
-e "s/^Merge pull request #[0-9]* from //p" |
tr ' ' '-'
}
commit_name_map=
name_commit () {
name="$(echo "$commit_name_map" |
sed -n "s/^$1 //p")"
echo "${name:-$1}"
}
ensure_labeled () {
for n in "$@"
do
case " $needslabel " in
*" $n "*)
;;
*)
needslabel="$needslabel $n"
;;
esac
done
}
generate_script () {
echo "Generating script..." >&2
origtodo="$(git rev-list --no-merges --cherry-pick --pretty=oneline \
--abbrev-commit --abbrev=7 --reverse --left-right --topo-order \
$upstream..$head | \
sed -n "s/^>/pick /p")"
shorthead=$(git rev-parse --short $head)
shortonto=$(git rev-parse --short $onto)
# --topo-order has the bad habit of breaking first-parent chains over
# merges, so we generate the topoligical order ourselves here
list="$(git log --format='%h %p' --topo-order --reverse \
$upstream..$head)"
todo=
if test -n "$merging"
then
from=$(git rev-parse --short "$upstream") &&
to=$(git rev-parse --short "$onto") &&
cat > "$git_dir"/SHEARS-MERGING-MESSAGE << EOF &&
Start the merging-rebase to $onto
This commit starts the rebase of $from to $to
$base_message
EOF
todo="start_merging_rebase \"$shorthead\""
fi
todo="$(printf '%s\n%s\n' "$todo" \
"mark onto")"
toberebased=" $(echo "$list" | cut -f 1 -d ' ' | tr '\n' ' ')"
handled=
needslabel=
# each tip is an end point of a commit->first parent chain
branch_tips="$(echo "$list" |
cut -f 3- -d ' ' |
tr ' ' '\n' |
grep -v '^$')"
ensure_labeled $branch_tips
branch_tips="$(printf '%s\n%s' "$branch_tips" "$shorthead")"
# set up the map tip -> branch name
for tip in $branch_tips
do
merged_by="$(echo "$list" |
sed -n "s/^\([^ ]*\) [^ ]* $tip$/\1/p" |
head -n 1)"
if test -n "$merged_by"
then
branch_name="$(merge2branch_name "$merged_by")"
test -z "$branch_name" ||
commit_name_map="$(printf '%s\n%s' \
"$tip $branch_name" "$commit_name_map")"
fi
done
branch_name_dupes="$(echo "$commit_name_map" |
sed 's/[^ ]* //' |
sort |
uniq -d)"
if test -n "$branch_name_dupes"
then
exprs="$(echo "$branch_name_dupes" |
while read branch_name
do
printf " -e '%s'" \
"$(string2regex "$branch_name")"
done)"
commit_name_map="$(echo "$commit_name_map" |
eval grep -v $exprs)"
fi
tip_total=$(printf '%s' "$branch_tips" | wc -l)
tip_counter=0
for tip in $branch_tips
do
printf '%d/%d...\r' $tip_counter $tip_total >&2
tip_counter=$(($tip_counter+1))
# if this is not a commit to be rebased, skip
case "$toberebased" in *" $tip "*) ;; *) continue;; esac
# if it is handled already, skip
case "$handled " in *" $tip "*) continue;; esac
# start sub-todo for this tip
subtodo=
commit=$tip
while true
do
printf '\tcommit %s...\r' "$commit" >&2
# if already handled, this is our branch point
case "$handled " in
*" $commit "*)
ensure_labeled $commit
subtodo="$(printf '\nrewind %s # %s\n%s' \
"$(name_commit $commit)" \
"$(git show -s --format=%s $commit)" \
"$subtodo")"
break
;;
esac
line="$(echo "$list" | grep "^$commit ")"
# if there is no line, branch from the 'onto' commit
if test -z "$line"
then
subtodo="$(printf '\nbud\n%s' \
"$subtodo")"
break
fi
parents=${line#* }
case "$parents" in
*' '*)
# merge
parents2="`for parent in ${parents#* }
do
case "$toberebased" in
*" $parent "*)
printf refs/rewritten/
;;
esac
echo "$(name_commit $parent) "
done`"
subtodo="$(printf '%s # %s\n%s' \
"merge $parents2-C $commit" \
"$(git show -s --format=%s $commit)" \
"$subtodo")"
;;
*)
# non-merge commit
line="$(echo "$origtodo" |
grep "^pick $commit")"
if test -z "$line"
then
line="# skip $commit"
fi
subtodo="$(printf '%s\n%s' "$line" "$subtodo")"
;;
esac
handled="$handled $commit"
commit=${parents%% *}
done
branch_name="$(name_commit "$tip")"
test -n "$branch_name" &&
test "$branch_name" = "$tip" ||
subtodo="$(echo "$subtodo" |
sed -e "1a\\
# Branch: $branch_name")"
todo="$(printf '%s\n\n%s' "$todo" "$subtodo")"
done
for commit in $needslabel
do
linenumber="$(echo "$todo" |
grep -n -e "^\(pick\|# skip\) $commit" \
-e "^merge [-_\\.0-9a-zA-Z/ ]* -C $commit")"
linenumber=${linenumber%%:*}
test -n "$linenumber" ||
die "Internal error: could not find $commit ($(name_commit $commit)) in $todo"
todo="$(echo "$todo" |
sed "${linenumber}a\\
mark $(name_commit $commit)\\
")"
done
lastline=9999
while true
do
fixup="$(echo "$todo" |
sed "$lastline,\$d" |
grep -n -e '^pick [^ ]* \(fixup\|squash\)!' |
tail -n 1)"
test -n "$fixup" || break
printf '%s...\r' "$fixup" >&2
linenumber=${fixup%%:*}
oneline="${fixup#* }"
shortsha1="${oneline%% *}"
oneline="${oneline#* }"
command=${oneline%%!*}
oneline="${oneline#*! }"
oneline_regex="^pick [^ ]* $(string2regex "$oneline")\$"
targetline="$(echo "$todo" |
sed "$linenumber,\$d" |
grep -n "$oneline_regex" |
tail -n 1)"
targetline=${targetline%%:*}
if test -n "$targetline"
then
todo="$(echo "$todo" |
sed -e "${linenumber}d" \
-e "${targetline}a\\
$command $shortsha1 $oneline")"
lastline=$(($linenumber+1))
else
echo "UNHANDLED: $oneline" >&2
lastline=$(($linenumber))
fi
done
while test -n "$recreate"
do
recreate="${recreate# }"
merge="${recreate%% *}"
recreate="${recreate#$merge}"
printf 'Recreating %s...\r' "$merge" >&2
mark="$(git rev-parse --short --verify "$merge^2")" ||
die "Could not find merge commit: $merge^2"
branch_name="$(merge2branch_name "$merge")"
partfile="$git_dir/SHEARS-PART"
printf '%s' "$(test -z "$branch_name" ||
echo "# Branch to recreate: $branch_name")" \
> "$partfile"
for sha1 in $(git rev-list --reverse $merge^..$merge^2)
do
msg="$(git show -s --format=%s $sha1)"
msg_regex="^pick [^ ]* $(string2regex "$msg")\$"
linenumber="$(echo "$todo" |
grep -n "$msg_regex" |
sed 's/:.*//')"
test -n "$linenumber" ||
die "Not a commit to rebase: $msg"
test 1 = $(echo "$linenumber" | wc -l) ||
die "More than one match for: $msg"
echo "$todo" |
sed -n "${linenumber}p" >> "$partfile"
todo="$(echo "$todo" |
sed "${linenumber}d")"
done
linenumber="$(echo "$todo" |
grep -n "^bud\$" |
tail -n 1 |
sed 's/:.*//')"
printf 'mark %s\n\nbud\n' \
"$(name_commit $mark)" >> "$partfile"
todo="$(echo "$todo" |
sed -e "${linenumber}r$partfile" \
-e "\$a\\
merge refs/rewritten/$mark -C $(name_commit $merge)")"
done
needslabel="$(for commit in $needslabel
do
printf ' %s' $(name_commit $commit)
done)"
todo="$(printf '%s\n\n%s' "$todo" "cleanup $needslabel")"
echo "$todo" | uniq
}
this="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
setup () {
existing=$(git for-each-ref --format='%(refname)' refs/rewritten/)
test -z "$existing" ||
if test -n "$force"
then
for ref in $existing
do
git update-ref -d $ref
done
else
die "$(printf '%s %s:\n%s\n' \
'There are still rewritten revisions' \
'(use --force to delete)' \
"$existing")"
fi
alias="$(git config --get alias..r)"
test -z "$alias" ||
test "a$alias" = "a!sh \"$this\"" ||
test -n "$force" ||
die "There is already an '.r' alias!"
git config alias..r "!sh \"$this\"" &&
generate_script > "$git_dir"/SHEARS-SCRIPT &&
GIT_EDITOR="$(cd "$git_dir" && pwd)/SHEARS-EDITOR" &&
cat > "$GIT_EDITOR" << EOF &&
#!/bin/sh
exec "$this" edit "$(git var GIT_EDITOR)" "\$@"
EOF
chmod +x "$GIT_EDITOR" &&
GIT_EDITOR="\"$GIT_EDITOR\"" &&
GIT_SEQUENCE_EDITOR="$GIT_EDITOR" &&
export GIT_EDITOR GIT_SEQUENCE_EDITOR
}
test ! -d "$git_dir"/rebase-merge &&
test ! -d "$git_dir"/rebase-apply ||
die "Rebase already in progress"
test $# = 1 ||
help
head="$(git rev-parse HEAD)" &&
upstream="$1" &&
onto=${onto:-$upstream}||
die "Could not determine rebase parameters"
git update-index -q --ignore-submodules --refresh &&
git diff-files --quiet --ignore-submodules &&
git diff-index --cached --quiet --ignore-submodules HEAD -- ||
die 'There are uncommitted changes!'
setup
# Rebase!
git rebase -i --onto "$onto" HEAD