]> asedeno.scripts.mit.edu Git - git.git/blob - git-rebase--interactive.sh
Merge branch 'wc/rebase-insn'
[git.git] / git-rebase--interactive.sh
1 #!/bin/sh
2 #
3 # Copyright (c) 2006 Johannes E. Schindelin
4
5 # SHORT DESCRIPTION
6 #
7 # This script makes it easy to fix up commits in the middle of a series,
8 # and rearrange commits.
9 #
10 # The original idea comes from Eric W. Biederman, in
11 # http://article.gmane.org/gmane.comp.version-control.git/22407
12
13 USAGE='(--continue | --abort | --skip | [--preserve-merges] [--verbose]
14         [--onto <branch>] <upstream> [<branch>])'
15
16 OPTIONS_SPEC=
17 . git-sh-setup
18 require_work_tree
19
20 DOTEST="$GIT_DIR/.dotest-merge"
21 TODO="$DOTEST"/git-rebase-todo
22 DONE="$DOTEST"/done
23 MSG="$DOTEST"/message
24 SQUASH_MSG="$DOTEST"/message-squash
25 REWRITTEN="$DOTEST"/rewritten
26 PRESERVE_MERGES=
27 STRATEGY=
28 VERBOSE=
29 test -d "$REWRITTEN" && PRESERVE_MERGES=t
30 test -f "$DOTEST"/strategy && STRATEGY="$(cat "$DOTEST"/strategy)"
31 test -f "$DOTEST"/verbose && VERBOSE=t
32
33 GIT_CHERRY_PICK_HELP="  After resolving the conflicts,
34 mark the corrected paths with 'git add <paths>', and
35 run 'git rebase --continue'"
36 export GIT_CHERRY_PICK_HELP
37
38 warn () {
39         echo "$*" >&2
40 }
41
42 output () {
43         case "$VERBOSE" in
44         '')
45                 output=$("$@" 2>&1 )
46                 status=$?
47                 test $status != 0 && printf "%s\n" "$output"
48                 return $status
49                 ;;
50         *)
51                 "$@"
52                 ;;
53         esac
54 }
55
56 require_clean_work_tree () {
57         # test if working tree is dirty
58         git rev-parse --verify HEAD > /dev/null &&
59         git update-index --refresh &&
60         git diff-files --quiet &&
61         git diff-index --cached --quiet HEAD -- ||
62         die "Working tree is dirty"
63 }
64
65 ORIG_REFLOG_ACTION="$GIT_REFLOG_ACTION"
66
67 comment_for_reflog () {
68         case "$ORIG_REFLOG_ACTION" in
69         ''|rebase*)
70                 GIT_REFLOG_ACTION="rebase -i ($1)"
71                 export GIT_REFLOG_ACTION
72                 ;;
73         esac
74 }
75
76 mark_action_done () {
77         sed -e 1q < "$TODO" >> "$DONE"
78         sed -e 1d < "$TODO" >> "$TODO".new
79         mv -f "$TODO".new "$TODO"
80         count=$(($(grep -ve '^$' -e '^#' < "$DONE" | wc -l)))
81         total=$(($count+$(grep -ve '^$' -e '^#' < "$TODO" | wc -l)))
82         printf "Rebasing (%d/%d)\r" $count $total
83         test -z "$VERBOSE" || echo
84 }
85
86 make_patch () {
87         parent_sha1=$(git rev-parse --verify "$1"^) ||
88                 die "Cannot get patch for $1^"
89         git diff-tree -p "$parent_sha1".."$1" > "$DOTEST"/patch
90         test -f "$DOTEST"/message ||
91                 git cat-file commit "$1" | sed "1,/^$/d" > "$DOTEST"/message
92         test -f "$DOTEST"/author-script ||
93                 get_author_ident_from_commit "$1" > "$DOTEST"/author-script
94 }
95
96 die_with_patch () {
97         make_patch "$1"
98         die "$2"
99 }
100
101 die_abort () {
102         rm -rf "$DOTEST"
103         die "$1"
104 }
105
106 has_action () {
107         grep -vqe '^$' -e '^#' "$1"
108 }
109
110 pick_one () {
111         no_ff=
112         case "$1" in -n) sha1=$2; no_ff=t ;; *) sha1=$1 ;; esac
113         output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
114         test -d "$REWRITTEN" &&
115                 pick_one_preserving_merges "$@" && return
116         parent_sha1=$(git rev-parse --verify $sha1^) ||
117                 die "Could not get the parent of $sha1"
118         current_sha1=$(git rev-parse --verify HEAD)
119         if test "$no_ff$current_sha1" = "$parent_sha1"; then
120                 output git reset --hard $sha1
121                 test "a$1" = a-n && output git reset --soft $current_sha1
122                 sha1=$(git rev-parse --short $sha1)
123                 output warn Fast forward to $sha1
124         else
125                 output git cherry-pick "$@"
126         fi
127 }
128
129 pick_one_preserving_merges () {
130         case "$1" in -n) sha1=$2 ;; *) sha1=$1 ;; esac
131         sha1=$(git rev-parse $sha1)
132
133         if test -f "$DOTEST"/current-commit
134         then
135                 current_commit=$(cat "$DOTEST"/current-commit) &&
136                 git rev-parse HEAD > "$REWRITTEN"/$current_commit &&
137                 rm "$DOTEST"/current-commit ||
138                 die "Cannot write current commit's replacement sha1"
139         fi
140
141         # rewrite parents; if none were rewritten, we can fast-forward.
142         fast_forward=t
143         preserve=t
144         new_parents=
145         for p in $(git rev-list --parents -1 $sha1 | cut -d' ' -f2-)
146         do
147                 if test -f "$REWRITTEN"/$p
148                 then
149                         preserve=f
150                         new_p=$(cat "$REWRITTEN"/$p)
151                         test $p != $new_p && fast_forward=f
152                         case "$new_parents" in
153                         *$new_p*)
154                                 ;; # do nothing; that parent is already there
155                         *)
156                                 new_parents="$new_parents $new_p"
157                                 ;;
158                         esac
159                 fi
160         done
161         case $fast_forward in
162         t)
163                 output warn "Fast forward to $sha1"
164                 test $preserve = f || echo $sha1 > "$REWRITTEN"/$sha1
165                 ;;
166         f)
167                 test "a$1" = a-n && die "Refusing to squash a merge: $sha1"
168
169                 first_parent=$(expr "$new_parents" : ' \([^ ]*\)')
170                 # detach HEAD to current parent
171                 output git checkout $first_parent 2> /dev/null ||
172                         die "Cannot move HEAD to $first_parent"
173
174                 echo $sha1 > "$DOTEST"/current-commit
175                 case "$new_parents" in
176                 ' '*' '*)
177                         # redo merge
178                         author_script=$(get_author_ident_from_commit $sha1)
179                         eval "$author_script"
180                         msg="$(git cat-file commit $sha1 | sed -e '1,/^$/d')"
181                         # No point in merging the first parent, that's HEAD
182                         new_parents=${new_parents# $first_parent}
183                         # NEEDSWORK: give rerere a chance
184                         if ! GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \
185                                 GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \
186                                 GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \
187                                 output git merge $STRATEGY -m "$msg" \
188                                         $new_parents
189                         then
190                                 printf "%s\n" "$msg" > "$GIT_DIR"/MERGE_MSG
191                                 die Error redoing merge $sha1
192                         fi
193                         ;;
194                 *)
195                         output git cherry-pick "$@" ||
196                                 die_with_patch $sha1 "Could not pick $sha1"
197                         ;;
198                 esac
199                 ;;
200         esac
201 }
202
203 nth_string () {
204         case "$1" in
205         *1[0-9]|*[04-9]) echo "$1"th;;
206         *1) echo "$1"st;;
207         *2) echo "$1"nd;;
208         *3) echo "$1"rd;;
209         esac
210 }
211
212 make_squash_message () {
213         if test -f "$SQUASH_MSG"; then
214                 COUNT=$(($(sed -n "s/^# This is [^0-9]*\([1-9][0-9]*\).*/\1/p" \
215                         < "$SQUASH_MSG" | tail -n 1)+1))
216                 echo "# This is a combination of $COUNT commits."
217                 sed -n "2,\$p" < "$SQUASH_MSG"
218         else
219                 COUNT=2
220                 echo "# This is a combination of two commits."
221                 echo "# The first commit's message is:"
222                 echo
223                 git cat-file commit HEAD | sed -e '1,/^$/d'
224                 echo
225         fi
226         echo "# This is the $(nth_string $COUNT) commit message:"
227         echo
228         git cat-file commit $1 | sed -e '1,/^$/d'
229 }
230
231 peek_next_command () {
232         sed -n "1s/ .*$//p" < "$TODO"
233 }
234
235 do_next () {
236         rm -f "$DOTEST"/message "$DOTEST"/author-script \
237                 "$DOTEST"/amend || exit
238         read command sha1 rest < "$TODO"
239         case "$command" in
240         '#'*|'')
241                 mark_action_done
242                 ;;
243         pick|p)
244                 comment_for_reflog pick
245
246                 mark_action_done
247                 pick_one $sha1 ||
248                         die_with_patch $sha1 "Could not apply $sha1... $rest"
249                 ;;
250         edit|e)
251                 comment_for_reflog edit
252
253                 mark_action_done
254                 pick_one $sha1 ||
255                         die_with_patch $sha1 "Could not apply $sha1... $rest"
256                 make_patch $sha1
257                 : > "$DOTEST"/amend
258                 warn
259                 warn "You can amend the commit now, with"
260                 warn
261                 warn "  git commit --amend"
262                 warn
263                 exit 0
264                 ;;
265         squash|s)
266                 comment_for_reflog squash
267
268                 has_action "$DONE" ||
269                         die "Cannot 'squash' without a previous commit"
270
271                 mark_action_done
272                 make_squash_message $sha1 > "$MSG"
273                 case "$(peek_next_command)" in
274                 squash|s)
275                         EDIT_COMMIT=
276                         USE_OUTPUT=output
277                         cp "$MSG" "$SQUASH_MSG"
278                         ;;
279                 *)
280                         EDIT_COMMIT=-e
281                         USE_OUTPUT=
282                         rm -f "$SQUASH_MSG" || exit
283                         ;;
284                 esac
285
286                 failed=f
287                 author_script=$(get_author_ident_from_commit HEAD)
288                 output git reset --soft HEAD^
289                 pick_one -n $sha1 || failed=t
290                 echo "$author_script" > "$DOTEST"/author-script
291                 case $failed in
292                 f)
293                         # This is like --amend, but with a different message
294                         eval "$author_script"
295                         GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \
296                         GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \
297                         GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \
298                         $USE_OUTPUT git commit -F "$MSG" $EDIT_COMMIT
299                         ;;
300                 t)
301                         cp "$MSG" "$GIT_DIR"/MERGE_MSG
302                         warn
303                         warn "Could not apply $sha1... $rest"
304                         die_with_patch $sha1 ""
305                         ;;
306                 esac
307                 ;;
308         *)
309                 warn "Unknown command: $command $sha1 $rest"
310                 die_with_patch $sha1 "Please fix this in the file $TODO."
311                 ;;
312         esac
313         test -s "$TODO" && return
314
315         comment_for_reflog finish &&
316         HEADNAME=$(cat "$DOTEST"/head-name) &&
317         OLDHEAD=$(cat "$DOTEST"/head) &&
318         SHORTONTO=$(git rev-parse --short $(cat "$DOTEST"/onto)) &&
319         if test -d "$REWRITTEN"
320         then
321                 test -f "$DOTEST"/current-commit &&
322                         current_commit=$(cat "$DOTEST"/current-commit) &&
323                         git rev-parse HEAD > "$REWRITTEN"/$current_commit
324                 NEWHEAD=$(cat "$REWRITTEN"/$OLDHEAD)
325         else
326                 NEWHEAD=$(git rev-parse HEAD)
327         fi &&
328         case $HEADNAME in
329         refs/*)
330                 message="$GIT_REFLOG_ACTION: $HEADNAME onto $SHORTONTO)" &&
331                 git update-ref -m "$message" $HEADNAME $NEWHEAD $OLDHEAD &&
332                 git symbolic-ref HEAD $HEADNAME
333                 ;;
334         esac && {
335                 test ! -f "$DOTEST"/verbose ||
336                         git diff-tree --stat $(cat "$DOTEST"/head)..HEAD
337         } &&
338         rm -rf "$DOTEST" &&
339         git gc --auto &&
340         warn "Successfully rebased and updated $HEADNAME."
341
342         exit
343 }
344
345 do_rest () {
346         while :
347         do
348                 do_next
349         done
350 }
351
352 while test $# != 0
353 do
354         case "$1" in
355         --continue)
356                 comment_for_reflog continue
357
358                 test -d "$DOTEST" || die "No interactive rebase running"
359
360                 # commit if necessary
361                 git rev-parse --verify HEAD > /dev/null &&
362                 git update-index --refresh &&
363                 git diff-files --quiet &&
364                 ! git diff-index --cached --quiet HEAD -- &&
365                 . "$DOTEST"/author-script && {
366                         test ! -f "$DOTEST"/amend || git reset --soft HEAD^
367                 } &&
368                 export GIT_AUTHOR_NAME GIT_AUTHOR_NAME GIT_AUTHOR_DATE &&
369                 git commit -F "$DOTEST"/message -e
370
371                 require_clean_work_tree
372                 do_rest
373                 ;;
374         --abort)
375                 comment_for_reflog abort
376
377                 test -d "$DOTEST" || die "No interactive rebase running"
378
379                 HEADNAME=$(cat "$DOTEST"/head-name)
380                 HEAD=$(cat "$DOTEST"/head)
381                 case $HEADNAME in
382                 refs/*)
383                         git symbolic-ref HEAD $HEADNAME
384                         ;;
385                 esac &&
386                 output git reset --hard $HEAD &&
387                 rm -rf "$DOTEST"
388                 exit
389                 ;;
390         --skip)
391                 comment_for_reflog skip
392
393                 test -d "$DOTEST" || die "No interactive rebase running"
394
395                 output git reset --hard && do_rest
396                 ;;
397         -s|--strategy)
398                 case "$#,$1" in
399                 *,*=*)
400                         STRATEGY="-s "$(expr "z$1" : 'z-[^=]*=\(.*\)') ;;
401                 1,*)
402                         usage ;;
403                 *)
404                         STRATEGY="-s $2"
405                         shift ;;
406                 esac
407                 ;;
408         --merge)
409                 # we use merge anyway
410                 ;;
411         -C*)
412                 die "Interactive rebase uses merge, so $1 does not make sense"
413                 ;;
414         -v|--verbose)
415                 VERBOSE=t
416                 ;;
417         -p|--preserve-merges)
418                 PRESERVE_MERGES=t
419                 ;;
420         -i|--interactive)
421                 # yeah, we know
422                 ;;
423         ''|-h)
424                 usage
425                 ;;
426         *)
427                 test -d "$DOTEST" &&
428                         die "Interactive rebase already started"
429
430                 git var GIT_COMMITTER_IDENT >/dev/null ||
431                         die "You need to set your committer info first"
432
433                 comment_for_reflog start
434
435                 ONTO=
436                 case "$1" in
437                 --onto)
438                         ONTO=$(git rev-parse --verify "$2") ||
439                                 die "Does not point to a valid commit: $2"
440                         shift; shift
441                         ;;
442                 esac
443
444                 require_clean_work_tree
445
446                 if test ! -z "$2"
447                 then
448                         output git show-ref --verify --quiet "refs/heads/$2" ||
449                                 die "Invalid branchname: $2"
450                         output git checkout "$2" ||
451                                 die "Could not checkout $2"
452                 fi
453
454                 HEAD=$(git rev-parse --verify HEAD) || die "No HEAD?"
455                 UPSTREAM=$(git rev-parse --verify "$1") || die "Invalid base"
456
457                 mkdir "$DOTEST" || die "Could not create temporary $DOTEST"
458
459                 test -z "$ONTO" && ONTO=$UPSTREAM
460
461                 : > "$DOTEST"/interactive || die "Could not mark as interactive"
462                 git symbolic-ref HEAD > "$DOTEST"/head-name 2> /dev/null ||
463                         echo "detached HEAD" > "$DOTEST"/head-name
464
465                 echo $HEAD > "$DOTEST"/head
466                 echo $UPSTREAM > "$DOTEST"/upstream
467                 echo $ONTO > "$DOTEST"/onto
468                 test -z "$STRATEGY" || echo "$STRATEGY" > "$DOTEST"/strategy
469                 test t = "$VERBOSE" && : > "$DOTEST"/verbose
470                 if test t = "$PRESERVE_MERGES"
471                 then
472                         # $REWRITTEN contains files for each commit that is
473                         # reachable by at least one merge base of $HEAD and
474                         # $UPSTREAM. They are not necessarily rewritten, but
475                         # their children might be.
476                         # This ensures that commits on merged, but otherwise
477                         # unrelated side branches are left alone. (Think "X"
478                         # in the man page's example.)
479                         mkdir "$REWRITTEN" &&
480                         for c in $(git merge-base --all $HEAD $UPSTREAM)
481                         do
482                                 echo $ONTO > "$REWRITTEN"/$c ||
483                                         die "Could not init rewritten commits"
484                         done
485                         MERGES_OPTION=
486                 else
487                         MERGES_OPTION=--no-merges
488                 fi
489
490                 SHORTUPSTREAM=$(git rev-parse --short $UPSTREAM)
491                 SHORTHEAD=$(git rev-parse --short $HEAD)
492                 SHORTONTO=$(git rev-parse --short $ONTO)
493                 git rev-list $MERGES_OPTION --pretty=oneline --abbrev-commit \
494                         --abbrev=7 --reverse --left-right --cherry-pick \
495                         $UPSTREAM...$HEAD | \
496                         sed -n "s/^>/pick /p" > "$TODO"
497                 cat >> "$TODO" << EOF
498
499 # Rebase $SHORTUPSTREAM..$SHORTHEAD onto $SHORTONTO
500 #
501 # Commands:
502 #  pick = use commit
503 #  edit = use commit, but stop for amending
504 #  squash = use commit, but meld into previous commit
505 #
506 # If you remove a line here THAT COMMIT WILL BE LOST.
507 # However, if you remove everything, the rebase will be aborted.
508 #
509 EOF
510
511                 has_action "$TODO" ||
512                         die_abort "Nothing to do"
513
514                 cp "$TODO" "$TODO".backup
515                 git_editor "$TODO" ||
516                         die "Could not execute editor"
517
518                 has_action "$TODO" ||
519                         die_abort "Nothing to do"
520
521                 output git checkout $ONTO && do_rest
522                 ;;
523         esac
524         shift
525 done