3 # Rebase the thicket of branches -- including their merge structure -- on top
4 # of the specified upstream branch (defaults to 'junio/next'), optionally
5 # retaining "fast-forwardability" by fake-merging (using the "ours" strategy)
6 # the previous state on top of the current upstream state ("merging rebase").
10 # start the rebased branch by a fake merge of the previous state
12 # The idea is to generate our very own rebase script, then call rebase -i with
13 # our fake editor to put the rebase script into place and then let the user edit
16 # To make things prettier, we rewrite the rebase script after letting the user
17 # edit it, to replace "new" rebase commands with calls to a temporary alias ".r"
18 # that is added to help with starting the merging rebase, merging things, and
19 # cleaning up afterwards.
26 git_dir
="$(git rev-parse --git-dir)" ||
27 die
"Not in a Git directory"
31 Usage: $0 [options] <upstream>
34 -m|--merging[=<msg>] allow fast-forwarding the current to the rebased branch
35 --onto=<commit> rebase onto the given commit
36 --recreate=<merge> recreate the branch merged in the specified commit
41 # Extra commands for use in the rebase script
42 extra_commands
="edit bud finish mark rewind merge start_merging_rebase cleanup"
46 GIT_SEQUENCE_EDITOR
="$GIT_EDITOR" &&
47 export GIT_EDITOR GIT_SEQUENCE_EDITOR
&&
51 sed -e '/^noop/d' < "$1" >> "$git_dir"/SHEARS-SCRIPT
&&
52 mv "$git_dir"/SHEARS-SCRIPT
"$1"
54 mv "$1" "$git_dir"/SHEARS-SCRIPT
&&
55 exprs
="$(for command in $extra_commands
57 printf " -e 's/^$command\$/exec git .r &/'"
58 printf " -e 's/^$command /exec git .r &/'"
60 eval sed $exprs < "$git_dir"/SHEARS-SCRIPT
> "$1"
63 exec "$GIT_EDITOR" "$@"
68 git update-ref
-m "Marking '$1' as rewritten" refs
/rewritten
/"$1" HEAD
72 git
reset --hard refs
/rewritten
/"$1"
76 shorthead
="$(git rev-parse --short --verify HEAD)" &&
77 git for-each-ref refs
/rewritten
/ |
79 die
"Refusing to leave unmarked revision $shorthead behind"
80 git
reset --hard refs
/rewritten
/onto
89 # parse command-line arguments
91 while test $# -gt 0 && test "a$1" != a-C
93 parents
="$parents $1" &&
101 # determine whether the merge needs to be redone
102 p
="$(git rev-parse HEAD)$parents" &&
103 o
="$(git rev-list -1 --parents $orig |
105 while p
=${p# }; o
=${o# }; test -n "$p$o"
107 p1
=${p%% *}; o1
=${o%% *};
108 test $o1 = "$(git rev-parse "$p1")" ||
break
109 p
=${p#$p1}; o
=${o#$o1}
111 # either redo merge or fast-forward
114 git
reset --hard $orig
117 msg
="$(git cat-file commit $orig |
122 for parent
in $parents
127 p
="$p $(git rev-parse --verify refs/rewritten/$parent \
131 msg
="Merge $msg into HEAD"
133 git merge
-n --no-ff -m "$msg" $p
136 start_merging_rebase
() {
137 git merge
-s ours
-m "$(cat "$git_dir"/SHEARS-MERGING-MESSAGE)" "$1"
141 rm -f "$git_dir"/SHEARS-SCRIPT
&&
144 git update-ref
-d refs
/rewritten
/$rewritten
146 for rewritten
in $
(git for-each-ref refs
/rewritten
/ |
147 sed 's/^[^ ]* commit.refs\/rewritten\///')
149 test onto
= "$rewritten" ||
151 git update-ref
-d refs
/rewritten
/$rewritten
153 git config
--unset alias..r
170 base_message
="${1#--merging=}"
181 recreate
="$recreate $1"
184 recreate
="$recreate ${1#--recreate=}"
193 echo "Unknown option: $1" >&2
203 case " $extra_commands " in
214 sed 's/[][\\\/*?]/\\&/g'
217 merge2branch_name
() {
218 git show
-s --format=%s
"$1" |
219 sed -n -e "s/^Merge [^']*'\([^']*\).*/\1/p" \
220 -e "s/^Merge pull request #[0-9]* from //p" |
226 name
="$(echo "$commit_name_map" |
234 case " $needslabel " in
238 needslabel
="$needslabel $n"
245 echo "Generating script..." >&2
246 origtodo
="$(git rev-list --no-merges --cherry-pick --pretty=oneline \
247 --abbrev-commit --abbrev=7 --reverse --left-right --topo-order \
249 sed -n "s
/^
>/pick
/p
")"
250 shorthead
=$
(git rev-parse
--short $head)
251 shortonto
=$
(git rev-parse
--short $onto)
253 # --topo-order has the bad habit of breaking first-parent chains over
254 # merges, so we generate the topoligical order ourselves here
256 list
="$(git log --format='%h %p' --topo-order --reverse \
260 if test -n "$merging"
262 from
=$
(git rev-parse
--short "$upstream") &&
263 to
=$
(git rev-parse
--short "$onto") &&
264 cat > "$git_dir"/SHEARS-MERGING-MESSAGE
<< EOF &&
265 Start the merging-rebase to $onto
267 This commit starts the rebase of $from to $to
270 todo
="start_merging_rebase \"$shorthead\""
272 todo
="$(printf '%s\n%s\n' "$todo" \
275 toberebased
=" $(echo "$list" | cut -f 1 -d ' ' | tr '\n' ' ')"
279 # each tip is an end point of a commit->first parent chain
280 branch_tips
="$(echo "$list" |
285 ensure_labeled
$branch_tips
287 branch_tips
="$(printf '%s\n%s' "$branch_tips" "$shorthead")"
289 # set up the map tip -> branch name
290 for tip
in $branch_tips
292 merged_by
="$(echo "$list" |
293 sed -n "s
/^\
([^
]*\
) [^
]* $tip$
/\
1/p
" |
295 if test -n "$merged_by"
297 branch_name
="$(merge2branch_name "$merged_by")"
298 test -z "$branch_name" ||
299 commit_name_map
="$(printf '%s\n%s' \
300 "$tip $branch_name" "$commit_name_map")"
303 branch_name_dupes
="$(echo "$commit_name_map" |
307 if test -n "$branch_name_dupes"
309 exprs
="$(echo "$branch_name_dupes" |
310 while read branch_name
313 "$
(string2regex
"$branch_name")"
315 commit_name_map
="$(echo "$commit_name_map" |
316 eval grep -v $exprs)"
319 tip_total
=$
(printf '%s' "$branch_tips" |
wc -l)
321 for tip
in $branch_tips
323 printf '%d/%d...\r' $tip_counter $tip_total >&2
324 tip_counter
=$
(($tip_counter+1))
325 # if this is not a commit to be rebased, skip
326 case "$toberebased" in *" $tip "*) ;; *) continue;; esac
328 # if it is handled already, skip
329 case "$handled " in *" $tip "*) continue;; esac
331 # start sub-todo for this tip
336 printf '\tcommit %s...\r' "$commit" >&2
337 # if already handled, this is our branch point
340 ensure_labeled
$commit
341 subtodo
="$(printf '\nrewind %s # %s\n%s' \
342 "$
(name_commit
$commit)" \
343 "$
(git show
-s --format=%s
$commit)" \
349 line
="$(echo "$list" | grep "^
$commit ")"
350 # if there is no line, branch from the 'onto' commit
353 subtodo
="$(printf '\nbud\n%s' \
361 parents2
="`for parent in ${parents#* }
363 case "$toberebased" in
365 printf refs/rewritten/
368 echo "$
(name_commit
$parent) "
370 subtodo
="$(printf '%s # %s\n%s' \
371 "merge
$parents2-C $commit" \
372 "$
(git show
-s --format=%s
$commit)" \
377 line
="$(echo "$origtodo" |
378 grep "^pick
$commit")"
381 line
="# skip $commit"
383 subtodo
="$(printf '%s\n%s' "$line" "$subtodo")"
386 handled
="$handled $commit"
387 commit
=${parents%% *}
390 branch_name
="$(name_commit "$tip")"
391 test -n "$branch_name" &&
392 test "$branch_name" = "$tip" ||
393 subtodo
="$(echo "$subtodo" |
395 # Branch: $branch_name")"
397 todo
="$(printf '%s\n\n%s' "$todo" "$subtodo")"
400 for commit
in $needslabel
402 linenumber
="$(echo "$todo" |
403 grep -n -e "^\
(pick\|
# skip\) $commit" \
404 -e "^merge [0-9a-f/ ]* -C $commit")"
405 linenumber=${linenumber%%:*}
406 test -n "$linenumber" ||
407 die "Internal error
: could not
find $commit in $todo"
408 todo="$
(echo "$todo" |
409 sed "${linenumber}a\\
410 mark $(name_commit $commit)\\
417 fixup="$
(echo "$todo" |
418 sed "$lastline,\$d" |
419 grep -n -e '^pick [^ ]* \(fixup\|squash\)!' |
421 test -n "$fixup" || break
422 printf '%s...\r' "$fixup" >&2
424 linenumber=${fixup%%:*}
425 oneline="${fixup#* }"
426 shortsha1="${oneline%% *}"
427 oneline="${oneline#* }"
428 command=${oneline%%!*}
429 oneline="${oneline#*! }"
430 oneline_regex="^pick
[^
]* $
(string2regex
"$oneline")\$
"
431 targetline="$
(echo "$todo" |
432 sed "$linenumber,\$d" |
433 grep -n "$oneline_regex" |
435 targetline=${targetline%%:*}
436 if test -n "$targetline"
438 todo="$
(echo "$todo" |
439 sed -e "${linenumber}d" \
441 $command $shortsha1 $oneline")"
442 lastline=$(($linenumber+1))
444 echo "UNHANDLED
: $oneline" >&2
445 lastline=$(($linenumber))
449 while test -n "$recreate"
451 recreate="${recreate# }"
452 merge="${recreate%% *}"
453 recreate="${recreate#$merge}"
454 printf 'Recreating %s...\r' "$merge" >&2
456 mark="$
(git rev-parse
--short --verify "$merge^2")" ||
457 die "Could not
find merge commit
: $merge^
2"
459 branch_name="$
(merge2branch_name
"$merge")"
460 partfile="$git_dir/SHEARS-PART
"
461 printf '%s' "$
(test -z "$branch_name" ||
462 echo "# Branch to recreate: $branch_name")" \
464 for sha1 in $(git rev-list --reverse $merge^..$merge^2)
466 msg="$
(git show
-s --format=%s
$sha1)"
467 msg_regex="^pick
[^
]* $
(string2regex
"$msg")\$
"
468 linenumber="$
(echo "$todo" |
469 grep -n "$msg_regex" |
471 test -n "$linenumber" ||
472 die "Not a commit to rebase
: $msg"
473 test 1 = $(echo "$linenumber" | wc -l) ||
474 die "More than one match
for: $msg"
476 sed -n "${linenumber}p
" >> "$partfile"
477 todo="$
(echo "$todo" |
478 sed "${linenumber}d")"
481 linenumber="$
(echo "$todo" |
486 printf 'mark %s\n\nbud\n' \
487 "$
(name_commit
$mark)" >> "$partfile"
488 todo="$
(echo "$todo" |
489 sed -e "${linenumber}r$partfile" \
491 merge refs/rewritten/$mark -C $(name_commit $merge)")"
494 needslabel="$
(for commit
in $needslabel
496 printf ' %s' $
(name_commit
$commit)
498 todo="$
(printf '%s\n\n%s' "$todo" "cleanup $needslabel")"
502 this="$
(cd "$(dirname "$0")" && pwd)/$
(basename "$0")"
504 existing=$(git for-each-ref --format='%(refname)' refs/rewritten/)
505 test -z "$existing" ||
510 git update-ref -d $ref
513 die "$
(printf '%s %s:\n%s\n' \
514 'There are still rewritten revisions' \
515 '(use --force to delete)' \
519 alias="$
(git config
--get alias..r
)"
521 test "a
$alias" = "a
!sh
\"$this\"" ||
523 die "There is already an
'.r' alias!"
525 git config alias..r "!sh
\"$this\"" &&
526 generate_script > "$git_dir"/SHEARS-SCRIPT &&
527 GIT_EDITOR="$
(cd "$git_dir" && pwd)/SHEARS-EDITOR
" &&
528 cat > "$GIT_EDITOR" << EOF &&
531 exec "$this" edit "$
(git var GIT_EDITOR
)" "\$@
"
533 chmod +x "$GIT_EDITOR" &&
534 GIT_EDITOR="\"$GIT_EDITOR\"" &&
535 GIT_SEQUENCE_EDITOR="$GIT_EDITOR" &&
536 export GIT_EDITOR GIT_SEQUENCE_EDITOR
539 test ! -d "$git_dir"/rebase-merge &&
540 test ! -d "$git_dir"/rebase-apply ||
541 die "Rebase already
in progress
"
546 head="$
(git rev-parse HEAD
)" &&
548 onto=${onto:-$upstream}||
549 die "Could not determine rebase parameters
"
551 git update-index -q --ignore-submodules --refresh &&
552 git diff-files --quiet --ignore-submodules &&
553 git diff-index --cached --quiet --ignore-submodules HEAD -- ||
554 die 'There are uncommitted changes!'
559 git rebase -i --onto "$onto" HEAD