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