OSDN Git Service

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