shears.sh: introduce 'bud' and 'finish' commands
[msysgit.git] / share / msysGit / shears.sh
blobe3f67add820b323d740e8a856ec07b6443657483
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 EOF
37 exit 1
40 # Extra commands for use in the rebase script
41 extra_commands="edit bud finish mark rewind merge start_merging_rebase cleanup"
43 edit () {
44 GIT_EDITOR="$1" &&
45 GIT_SEQUENCE_EDITOR="$GIT_EDITOR" &&
46 export GIT_EDITOR GIT_SEQUENCE_EDITOR &&
47 shift &&
48 case "$*" in
49 */git-rebase-todo)
50 sed -e '/^noop/d' < "$1" >> "$git_dir"/SHEARS-SCRIPT &&
51 mv "$git_dir"/SHEARS-SCRIPT "$1"
52 "$GIT_EDITOR" "$@" &&
53 mv "$1" "$git_dir"/SHEARS-SCRIPT &&
54 exprs="$(for command in $extra_commands
56 printf " -e 's/^$command\$/exec git .r &/'"
57 printf " -e 's/^$command /exec git .r &/'"
58 done)" &&
59 eval sed $exprs < "$git_dir"/SHEARS-SCRIPT > "$1"
62 exec "$GIT_EDITOR" "$@"
63 esac
66 mark () {
67 git update-ref -m "Marking '$1' as rewritten" refs/rewritten/"$1" HEAD
70 rewind () {
71 git reset --hard refs/rewritten/"$1"
74 bud () {
75 shorthead="$(git rev-parse --short --verify HEAD)" &&
76 git for-each-ref refs/rewritten/ |
77 grep "^$shorthead" ||
78 die "Refusing to leave unmarked revision $shorthead behind"
79 git reset --hard refs/rewritten/onto
82 finish () {
83 mark "$@" &&
84 bud
87 merge () {
88 # parse command-line arguments
89 parents=
90 while test $# -gt 0 && test "a$1" != a-C
92 parents="$parents $1" &&
93 shift
94 done &&
95 if test "a$1" = "a-C"
96 then
97 shift &&
98 orig="$1" &&
99 shift &&
100 # determine whether the merge needs to be redone
101 p="$(git rev-parse HEAD)$parents" &&
102 o="$(git rev-list -1 --parents $orig |
103 sed "s/[^ ]*//")" &&
104 while p=${p# }; o=${o# }; test -n "$p$o"
106 p1=${p%% *}; o1=${o%% *};
107 test $o1 = "$(git rev-parse "$p1")" || break
108 p=${p#$p1}; o=${o#$o1}
109 done &&
110 # either redo merge or fast-forward
111 if test -z "$p$o"
112 then
113 git reset --hard $orig
114 return
115 fi &&
116 msg="$(git cat-file commit $orig |
117 sed "1,/^$/d")"
118 else
119 msg=
121 for parent in $parents
123 test -z "$msg" ||
124 msg="$msg and "
125 msg="$msg'$parent'"
126 p="$p $(git rev-parse --verify refs/rewritten/$parent \
127 2> /dev/null ||
128 echo $parent)"
129 done &&
130 msg="Merge $msg into HEAD"
131 fi &&
132 git merge -n --no-ff -m "$msg" $p
135 start_merging_rebase () {
136 git merge -s ours -m "$(cat "$git_dir"/SHEARS-MERGING-MESSAGE)" "$1"
139 cleanup () {
140 rm -f "$git_dir"/SHEARS-SCRIPT &&
141 for rewritten
143 git update-ref -d refs/rewritten/$rewritten
144 done &&
145 for rewritten in $(git for-each-ref refs/rewritten/ |
146 sed 's/^[^ ]* commit.refs\/rewritten\///')
148 test onto = "$rewritten" ||
149 merge $rewritten
150 git update-ref -d refs/rewritten/$rewritten
151 done &&
152 git config --unset alias..r
155 merging=
156 base_message=
157 onto=
158 while test $# -gt 0
160 case "$1" in
161 -m|--merging)
162 merging=t
163 base_message=
165 --merging=*)
166 merging=t
167 base_message="${1#--merging=}"
169 --onto)
170 shift
171 onto="$1"
173 --onto=*)
174 onto="${1#--onto=}"
176 -h|--help)
177 help
180 echo "Unknown option: $1" >&2
181 exit 1
184 break
186 esac
187 shift
188 done
190 case " $extra_commands " in
191 *" $1 "*)
192 command="$1"
193 shift
194 "$command" "$@"
195 exit
197 esac
199 string2regex () {
200 echo "$*" |
201 sed 's/[][\\\/*?]/\\&/g'
204 merge2branch_name () {
205 git show -s --format=%s "$1" |
206 sed -n -e "s/^Merge [^']*'\([^']*\).*/\1/p" \
207 -e "s/^Merge pull request #[0-9]* from //p" |
208 tr ' ' '-'
211 ensure_labeled () {
212 for n in "$@"
214 case " $needslabel " in
215 *" $n "*)
218 needslabel="$needslabel $n"
220 esac
221 done
224 generate_script () {
225 origtodo="$(git rev-list --no-merges --cherry-pick --pretty=oneline \
226 --abbrev-commit --abbrev=7 --reverse --left-right --topo-order \
227 $upstream..$head | \
228 sed -n "s/^>/pick /p")"
229 shorthead=$(git rev-parse --short $head)
230 shortonto=$(git rev-parse --short $onto)
232 # --topo-order has the bad habit of breaking first-parent chains over
233 # merges, so we generate the topoligical order ourselves here
235 list="$(git log --format='%h %p' --topo-order --reverse \
236 $upstream..$head)"
238 todo=
239 if test -n "$merging"
240 then
241 from=$(git rev-parse --short "$upstream") &&
242 to=$(git rev-parse --short "$onto") &&
243 cat > "$git_dir"/SHEARS-MERGING-MESSAGE << EOF &&
244 Start the merging-rebase to $onto
246 This commit starts the rebase of $from to $to
247 $base_message
249 todo="start_merging_rebase \"$shorthead\""
251 todo="$(printf '%s\n%s\n' "$todo" \
252 "mark onto")"
254 toberebased=" $(echo "$list" | cut -f 1 -d ' ' | tr '\n' ' ')"
255 handled=
256 needslabel=
258 # each tip is an end point of a commit->first parent chain
259 branch_tips="$(echo "$list" |
260 cut -f 3- -d ' ' |
261 tr ' ' '\n' |
262 grep -v '^$')"
264 ensure_labeled $branch_tips
266 branch_tips="$(printf '%s\n%s' "$branch_tips" "$shorthead")"
268 for tip in $branch_tips
270 # if this is not a commit to be rebased, skip
271 case "$toberebased" in *" $tip "*) ;; *) continue;; esac
273 # if it is handled already, skip
274 case "$handled " in *" $tip "*) continue;; esac
276 # start sub-todo for this tip
277 subtodo=
278 commit=$tip
279 while true
281 # if already handled, this is our branch point
282 case "$handled " in
283 *" $commit "*)
284 ensure_labeled $commit
285 subtodo="$(printf '\nrewind %s # %s\n%s' \
286 "$(name_commit $commit)" \
287 "$(git show -s --format=%s $commit)" \
288 "$subtodo")"
289 break
291 esac
293 line="$(echo "$list" | grep "^$commit ")"
294 # if there is no line, branch from the 'onto' commit
295 if test -z "$line"
296 then
297 subtodo="$(printf '\nbud\n%s' \
298 "$subtodo")"
299 break
301 parents=${line#* }
302 case "$parents" in
303 *' '*)
304 # merge
305 parents2="`for parent in ${parents#* }
307 case "$toberebased" in
308 *" $parent "*)
309 printf refs/rewritten/
311 esac
312 echo "$parent "
313 done`"
314 subtodo="$(printf '%s # %s\n%s' \
315 "merge $parents2-C $commit" \
316 "$(git show -s --format=%s $commit)" \
317 "$subtodo")"
320 # non-merge commit
321 line="$(echo "$origtodo" |
322 grep "^pick $commit")"
323 if test -z "$line"
324 then
325 line="# skip $commit"
327 subtodo="$(printf '%s\n%s' "$line" "$subtodo")"
329 esac
330 handled="$handled $commit"
331 commit=${parents%% *}
332 done
334 # try to figure out the branch name
335 merged_by="$(echo "$list" |
336 sed -n "s/^\([^ ]*\) [^ ]* $tip$/\1/p" |
337 head -n 1)"
338 if test -n "$merged_by"
339 then
340 branch_name="$(merge2branch_name "$merged_by")"
341 test -z "$branch_name" ||
342 subtodo="$(echo "$subtodo" |
343 sed -e "1a\\
344 # Branch: $branch_name")"
347 todo="$(printf '%s\n\n%s' "$todo" "$subtodo")"
348 done
350 for commit in $needslabel
352 linenumber="$(echo "$todo" |
353 grep -n -e "^\(pick\|# skip\) $commit" \
354 -e "^merge [0-9a-f/ ]* -C $commit")"
355 linenumber=${linenumber%%:*}
356 test -n "$linenumber" ||
357 die "Internal error: could not find $commit in $todo"
358 todo="$(echo "$todo" |
359 sed "${linenumber}a\\
360 mark $commit\\
362 done
364 lastline=9999
365 while true
367 fixup="$(echo "$todo" |
368 sed "$lastline,\$d" |
369 grep -n -e '^pick [^ ]* \(fixup\|squash\)!' |
370 tail -n 1)"
371 test -n "$fixup" || break
373 linenumber=${fixup%%:*}
374 oneline="${fixup#* }"
375 shortsha1="${oneline%% *}"
376 oneline="${oneline#* }"
377 command=${oneline%%!*}
378 oneline="${oneline#*! }"
379 oneline_regex="^pick [^ ]* $(string2regex "$oneline")\$"
380 targetline="$(echo "$todo" |
381 sed "$linenumber,\$d" |
382 grep -n "$oneline_regex" |
383 tail -n 1)"
384 targetline=${targetline%%:*}
385 if test -n "$targetline"
386 then
387 todo="$(echo "$todo" |
388 sed -e "${linenumber}d" \
389 -e "${targetline}a\\
390 $command $shortsha1 $oneline")"
391 lastline=$(($linenumber+1))
392 else
393 echo "UNHANDLED: $oneline" >&2
394 lastline=$(($linenumber))
396 done
398 todo="$(printf '%s\n\n%s' "$todo" "cleanup $needslabel")"
399 echo "$todo" | uniq
402 this="$(cd "$(dirname "$0")" && pwd)/$(basename "$0")"
403 setup () {
404 test -z "$(git for-each-ref refs/rewritten/)" ||
405 die "There are still rewritten revisions"
407 alias="$(git config --get alias..r)"
408 test -z "$alias" ||
409 test "a$alias" = "a!sh \"$this\"" ||
410 die "There is already an '.r' alias!"
412 git config alias..r "!sh \"$this\"" &&
413 generate_script > "$git_dir"/SHEARS-SCRIPT &&
414 GIT_EDITOR="$(cd "$git_dir" && pwd)/SHEARS-EDITOR" &&
415 cat > "$GIT_EDITOR" << EOF &&
416 #!/bin/sh
418 exec "$this" edit "$(git var GIT_EDITOR)" "\$@"
420 chmod +x "$GIT_EDITOR" &&
421 GIT_EDITOR="\"$GIT_EDITOR\"" &&
422 GIT_SEQUENCE_EDITOR="$GIT_EDITOR" &&
423 export GIT_EDITOR GIT_SEQUENCE_EDITOR
426 test ! -d "$git_dir"/rebase-merge &&
427 test ! -d "$git_dir"/rebase-apply ||
428 die "Rebase already in progress"
430 test $# = 1 ||
431 help
433 head="$(git rev-parse HEAD)" &&
434 upstream="$1" &&
435 onto=${onto:-$upstream}||
436 die "Could not determine rebase parameters"
438 setup
440 # Rebase!
441 git rebase -i --onto "$onto" HEAD