Merge branch 'garden-shears'
[msysgit.git] / share / msysGit / shears.sh
blob1295c509e9528c6f22b869a4ec17889488b85e35
1 #!/bin/sh
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").
8 # options:
9 # --merging
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
14 # the script.
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.
21 die () {
22 echo "$*" >&2
23 exit 1
26 git_dir="$(git rev-parse --git-dir)" ||
27 die "Not in a Git directory"
29 help () {
30 cat >&2 << EOF
31 Usage: $0 [options] <upstream>
33 Options:
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
37 EOF
38 exit 1
41 # Extra commands for use in the rebase script
42 extra_commands="edit bud finish mark rewind merge start_merging_rebase cleanup"
44 edit () {
45 GIT_EDITOR="$1" &&
46 GIT_SEQUENCE_EDITOR="$GIT_EDITOR" &&
47 export GIT_EDITOR GIT_SEQUENCE_EDITOR &&
48 shift &&
49 case "$*" in
50 */git-rebase-todo)
51 sed -e '/^noop/d' < "$1" >> "$git_dir"/SHEARS-SCRIPT &&
52 mv "$git_dir"/SHEARS-SCRIPT "$1"
53 "$GIT_EDITOR" "$@" &&
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 &/'"
59 done)" &&
60 eval sed $exprs < "$git_dir"/SHEARS-SCRIPT > "$1"
63 exec "$GIT_EDITOR" "$@"
64 esac
67 mark () {
68 git update-ref -m "Marking '$1' as rewritten" refs/rewritten/"$1" HEAD
71 rewind () {
72 git reset --hard refs/rewritten/"$1"
75 bud () {
76 shorthead="$(git rev-parse --short --verify HEAD)" &&
77 git for-each-ref refs/rewritten/ |
78 grep "^$shorthead" ||
79 die "Refusing to leave unmarked revision $shorthead behind"
80 git reset --hard refs/rewritten/onto
83 finish () {
84 mark "$@" &&
85 bud
88 merge () {
89 # parse command-line arguments
90 parents=
91 while test $# -gt 0 && test "a$1" != a-C
93 parents="$parents $1" &&
94 shift
95 done &&
96 if test "a$1" = "a-C"
97 then
98 shift &&
99 orig="$1" &&
100 shift &&
101 # determine whether the merge needs to be redone
102 p="$(git rev-parse HEAD)$parents" &&
103 o="$(git rev-list -1 --parents $orig |
104 sed "s/[^ ]*//")" &&
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}
110 done &&
111 # either redo merge or fast-forward
112 if test -z "$p$o"
113 then
114 git reset --hard $orig
115 return
116 fi &&
117 msg="$(git cat-file commit $orig |
118 sed "1,/^$/d")"
119 else
120 msg=
122 for parent in $parents
124 test -z "$msg" ||
125 msg="$msg and "
126 msg="$msg'$parent'"
127 p="$p $(git rev-parse --verify refs/rewritten/$parent \
128 2> /dev/null ||
129 echo $parent)"
130 done &&
131 msg="Merge $msg into HEAD"
132 fi &&
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"
140 cleanup () {
141 rm -f "$git_dir"/SHEARS-SCRIPT &&
142 for rewritten
144 git update-ref -d refs/rewritten/$rewritten
145 done &&
146 for rewritten in $(git for-each-ref refs/rewritten/ |
147 sed 's/^[^ ]* commit.refs\/rewritten\///')
149 test onto = "$rewritten" ||
150 merge $rewritten
151 git update-ref -d refs/rewritten/$rewritten
152 done &&
153 git config --unset alias..r
156 merging=
157 base_message=
158 onto=
159 recreate=
160 force=
161 while test $# -gt 0
163 case "$1" in
164 -m|--merging)
165 merging=t
166 base_message=
168 --merging=*)
169 merging=t
170 base_message="${1#--merging=}"
172 --onto)
173 shift
174 onto="$1"
176 --onto=*)
177 onto="${1#--onto=}"
179 --recreate)
180 shift
181 recreate="$recreate $1"
183 --recreate=*)
184 recreate="$recreate ${1#--recreate=}"
186 --force|-f)
187 force=t
189 -h|--help)
190 help
193 echo "Unknown option: $1" >&2
194 exit 1
197 break
199 esac
200 shift
201 done
203 case " $extra_commands " in
204 *" $1 "*)
205 command="$1"
206 shift
207 "$command" "$@"
208 exit
210 esac
212 string2regex () {
213 echo "$*" |
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" |
221 tr ' ' '-'
224 commit_name_map=
225 name_commit () {
226 name="$(echo "$commit_name_map" |
227 sed -n "s/^$1 //p")"
228 echo "${name:-$1}"
231 ensure_labeled () {
232 for n in "$@"
234 case " $needslabel " in
235 *" $n "*)
238 needslabel="$needslabel $n"
240 esac
241 done
244 generate_script () {
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 \
248 $upstream..$head | \
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 \
257 $upstream..$head)"
259 todo=
260 if test -n "$merging"
261 then
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
268 $base_message
270 todo="start_merging_rebase \"$shorthead\""
272 todo="$(printf '%s\n%s\n' "$todo" \
273 "mark onto")"
275 toberebased=" $(echo "$list" | cut -f 1 -d ' ' | tr '\n' ' ')"
276 handled=
277 needslabel=
279 # each tip is an end point of a commit->first parent chain
280 branch_tips="$(echo "$list" |
281 cut -f 3- -d ' ' |
282 tr ' ' '\n' |
283 grep -v '^$')"
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" |
294 head -n 1)"
295 if test -n "$merged_by"
296 then
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")"
302 done
303 branch_name_dupes="$(echo "$commit_name_map" |
304 sed 's/[^ ]* //' |
305 sort |
306 uniq -d)"
307 if test -n "$branch_name_dupes"
308 then
309 exprs="$(echo "$branch_name_dupes" |
310 while read branch_name
312 printf " -e '%s'" \
313 "$(string2regex "$branch_name")"
314 done)"
315 commit_name_map="$(echo "$commit_name_map" |
316 eval grep -v $exprs)"
319 tip_total=$(printf '%s' "$branch_tips" | wc -l)
320 tip_counter=0
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
332 subtodo=
333 commit=$tip
334 while true
336 printf '\tcommit %s...\r' "$commit" >&2
337 # if already handled, this is our branch point
338 case "$handled " in
339 *" $commit "*)
340 ensure_labeled $commit
341 subtodo="$(printf '\nrewind %s # %s\n%s' \
342 "$(name_commit $commit)" \
343 "$(git show -s --format=%s $commit)" \
344 "$subtodo")"
345 break
347 esac
349 line="$(echo "$list" | grep "^$commit ")"
350 # if there is no line, branch from the 'onto' commit
351 if test -z "$line"
352 then
353 subtodo="$(printf '\nbud\n%s' \
354 "$subtodo")"
355 break
357 parents=${line#* }
358 case "$parents" in
359 *' '*)
360 # merge
361 parents2="`for parent in ${parents#* }
363 case "$toberebased" in
364 *" $parent "*)
365 printf refs/rewritten/
367 esac
368 echo "$(name_commit $parent) "
369 done`"
370 subtodo="$(printf '%s # %s\n%s' \
371 "merge $parents2-C $commit" \
372 "$(git show -s --format=%s $commit)" \
373 "$subtodo")"
376 # non-merge commit
377 line="$(echo "$origtodo" |
378 grep "^pick $commit")"
379 if test -z "$line"
380 then
381 line="# skip $commit"
383 subtodo="$(printf '%s\n%s' "$line" "$subtodo")"
385 esac
386 handled="$handled $commit"
387 commit=${parents%% *}
388 done
390 branch_name="$(name_commit "$tip")"
391 test -n "$branch_name" &&
392 test "$branch_name" = "$tip" ||
393 subtodo="$(echo "$subtodo" |
394 sed -e "1a\\
395 # Branch: $branch_name")"
397 todo="$(printf '%s\n\n%s' "$todo" "$subtodo")"
398 done
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)\\
412 done
414 lastline=9999
415 while true
417 fixup="$(echo "$todo" |
418 sed "$lastline,\$d" |
419 grep -n -e '^pick [^ ]* \(fixup\|squash\)!' |
420 tail -n 1)"
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" |
434 tail -n 1)"
435 targetline=${targetline%%:*}
436 if test -n "$targetline"
437 then
438 todo="$(echo "$todo" |
439 sed -e "${linenumber}d" \
440 -e "${targetline}a\\
441 $command $shortsha1 $oneline")"
442 lastline=$(($linenumber+1))
443 else
444 echo "UNHANDLED: $oneline" >&2
445 lastline=$(($linenumber))
447 done
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")" \
463 > "$partfile"
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" |
470 sed 's/:.*//')"
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"
475 echo "$todo" |
476 sed -n "${linenumber}p" >> "$partfile"
477 todo="$(echo "$todo" |
478 sed "${linenumber}d")"
479 done
481 linenumber="$(echo "$todo" |
482 grep -n "^bud\$" |
483 tail -n 1 |
484 sed 's/:.*//')"
486 printf 'mark %s\n\nbud\n' \
487 "$(name_commit $mark)" >> "$partfile"
488 todo="$(echo "$todo" |
489 sed -e "${linenumber}r$partfile" \
490 -e "\$a\\
491 merge refs/rewritten/$mark -C $(name_commit $merge)")"
492 done
494 needslabel="$(for commit in $needslabel
496 printf ' %s' $(name_commit $commit)
497 done)"
498 todo="$(printf '%s\n\n%s' "$todo" "cleanup $needslabel")"
499 echo "$todo" | uniq
502 this="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
503 setup () {
504 existing=$(git for-each-ref --format='%(refname)' refs/rewritten/)
505 test -z "$existing" ||
506 if test -n "$force"
507 then
508 for ref in $existing
510 git update-ref -d $ref
511 done
512 else
513 die "$(printf '%s %s:\n%s\n' \
514 'There are still rewritten revisions' \
515 '(use --force to delete)' \
516 "$existing")"
519 alias="$(git config --get alias..r)"
520 test -z "$alias" ||
521 test "a$alias" = "a!sh \"$this\"" ||
522 test -n "$force" ||
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 &&
529 #!/bin/sh
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"
543 test $# = 1 ||
544 help
546 head="$(git rev-parse HEAD)" &&
547 upstream="$1" &&
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!'
556 setup
558 # Rebase!
559 git rebase -i --onto "$onto" HEAD