Skip to content

Commit 668993f

Browse files
committed
Merge branch 'mh/rebase-fixup'
* mh/rebase-fixup: rebase -i: Retain user-edited commit messages after squash/fixup conflicts t3404: Set up more of the test repo in the "setup" step rebase -i: For fixup commands without squashes, do not start editor rebase -i: Change function make_squash_message into update_squash_message rebase -i: Extract function do_with_author rebase -i: Handle the author script all in one place in do_next rebase -i: Extract a function "commit_message" rebase -i: Simplify commit counting for generated commit messages rebase -i: Improve consistency of commit count in generated commit messages t3404: Test the commit count in commit messages generated by "rebase -i" rebase -i: Introduce a constant AMEND rebase -i: Introduce a constant AUTHOR_SCRIPT rebase -i: Document how temporary files are used rebase -i: Use symbolic constant $MSG consistently rebase -i: Use "test -n" instead of "test ! -z" rebase -i: Inline expression rebase -i: Remove dead code rebase -i: Make the condition for an "if" more transparent
2 parents cea20f2 + 6bdcd0d commit 668993f

File tree

3 files changed

+219
-112
lines changed

3 files changed

+219
-112
lines changed

git-rebase--interactive.sh

Lines changed: 143 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,67 @@ autosquash move commits that begin with squash!/fixup! under -i
3535
require_work_tree
3636

3737
DOTEST="$GIT_DIR/rebase-merge"
38+
39+
# The file containing rebase commands, comments, and empty lines.
40+
# This file is created by "git rebase -i" then edited by the user. As
41+
# the lines are processed, they are removed from the front of this
42+
# file and written to the tail of $DONE.
3843
TODO="$DOTEST"/git-rebase-todo
44+
45+
# The rebase command lines that have already been processed. A line
46+
# is moved here when it is first handled, before any associated user
47+
# actions.
3948
DONE="$DOTEST"/done
49+
50+
# The commit message that is planned to be used for any changes that
51+
# need to be committed following a user interaction.
4052
MSG="$DOTEST"/message
53+
54+
# The file into which is accumulated the suggested commit message for
55+
# squash/fixup commands. When the first of a series of squash/fixups
56+
# is seen, the file is created and the commit message from the
57+
# previous commit and from the first squash/fixup commit are written
58+
# to it. The commit message for each subsequent squash/fixup commit
59+
# is appended to the file as it is processed.
60+
#
61+
# The first line of the file is of the form
62+
# # This is a combination of $COUNT commits.
63+
# where $COUNT is the number of commits whose messages have been
64+
# written to the file so far (including the initial "pick" commit).
65+
# Each time that a commit message is processed, this line is read and
66+
# updated. It is deleted just before the combined commit is made.
4167
SQUASH_MSG="$DOTEST"/message-squash
68+
69+
# If the current series of squash/fixups has not yet included a squash
70+
# command, then this file exists and holds the commit message of the
71+
# original "pick" commit. (If the series ends without a "squash"
72+
# command, then this can be used as the commit message of the combined
73+
# commit without opening the editor.)
74+
FIXUP_MSG="$DOTEST"/message-fixup
75+
76+
# $REWRITTEN is the name of a directory containing files for each
77+
# commit that is reachable by at least one merge base of $HEAD and
78+
# $UPSTREAM. They are not necessarily rewritten, but their children
79+
# might be. This ensures that commits on merged, but otherwise
80+
# unrelated side branches are left alone. (Think "X" in the man page's
81+
# example.)
4282
REWRITTEN="$DOTEST"/rewritten
83+
4384
DROPPED="$DOTEST"/dropped
85+
86+
# A script to set the GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and
87+
# GIT_AUTHOR_DATE that will be used for the commit that is currently
88+
# being rebased.
89+
AUTHOR_SCRIPT="$DOTEST"/author-script
90+
91+
# When an "edit" rebase command is being processed, the SHA1 of the
92+
# commit to be edited is recorded in this file. When "git rebase
93+
# --continue" is executed, if there are any staged changes then they
94+
# will be amended to the HEAD commit, but only provided the HEAD
95+
# commit is still the commit to be edited. When any other rebase
96+
# command is processed, this file is deleted.
97+
AMEND="$DOTEST"/amend
98+
4499
PRESERVE_MERGES=
45100
STRATEGY=
46101
ONTO=
@@ -72,6 +127,11 @@ output () {
72127
esac
73128
}
74129

130+
# Output the commit message for the specified commit.
131+
commit_message () {
132+
git cat-file commit "$1" | sed "1,/^$/d"
133+
}
134+
75135
run_pre_rebase_hook () {
76136
if test -z "$OK_TO_SKIP_PRE_REBASE" &&
77137
test -x "$GIT_DIR/hooks/pre-rebase"
@@ -131,10 +191,10 @@ make_patch () {
131191
echo "Root commit"
132192
;;
133193
esac > "$DOTEST"/patch
134-
test -f "$DOTEST"/message ||
135-
git cat-file commit "$1" | sed "1,/^$/d" > "$DOTEST"/message
136-
test -f "$DOTEST"/author-script ||
137-
get_author_ident_from_commit "$1" > "$DOTEST"/author-script
194+
test -f "$MSG" ||
195+
commit_message "$1" > "$MSG"
196+
test -f "$AUTHOR_SCRIPT" ||
197+
get_author_ident_from_commit "$1" > "$AUTHOR_SCRIPT"
138198
}
139199

140200
die_with_patch () {
@@ -152,25 +212,33 @@ has_action () {
152212
sane_grep '^[^#]' "$1" >/dev/null
153213
}
154214

215+
# Run command with GIT_AUTHOR_NAME, GIT_AUTHOR_EMAIL, and
216+
# GIT_AUTHOR_DATE exported from the current environment.
217+
do_with_author () {
218+
GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \
219+
GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \
220+
GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \
221+
"$@"
222+
}
223+
155224
pick_one () {
156225
no_ff=
157226
case "$1" in -n) sha1=$2; no_ff=t ;; *) sha1=$1 ;; esac
158227
output git rev-parse --verify $sha1 || die "Invalid commit name: $sha1"
159228
test -d "$REWRITTEN" &&
160229
pick_one_preserving_merges "$@" && return
161-
if test ! -z "$REBASE_ROOT"
230+
if test -n "$REBASE_ROOT"
162231
then
163232
output git cherry-pick "$@"
164233
return
165234
fi
166235
parent_sha1=$(git rev-parse --verify $sha1^) ||
167236
die "Could not get the parent of $sha1"
168237
current_sha1=$(git rev-parse --verify HEAD)
169-
if test "$no_ff$current_sha1" = "$parent_sha1"; then
238+
if test -z "$no_ff" -a "$current_sha1" = "$parent_sha1"
239+
then
170240
output git reset --hard $sha1
171-
test "a$1" = a-n && output git reset --soft $current_sha1
172-
sha1=$(git rev-parse --short $sha1)
173-
output warn Fast-forward to $sha1
241+
output warn Fast-forward to $(git rev-parse --short $sha1)
174242
else
175243
output git cherry-pick "$@"
176244
fi
@@ -271,14 +339,11 @@ pick_one_preserving_merges () {
271339
# redo merge
272340
author_script=$(get_author_ident_from_commit $sha1)
273341
eval "$author_script"
274-
msg="$(git cat-file commit $sha1 | sed -e '1,/^$/d')"
342+
msg="$(commit_message $sha1)"
275343
# No point in merging the first parent, that's HEAD
276344
new_parents=${new_parents# $first_parent}
277-
if ! GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \
278-
GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \
279-
GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \
280-
output git merge $STRATEGY -m "$msg" \
281-
$new_parents
345+
if ! do_with_author output \
346+
git merge $STRATEGY -m "$msg" $new_parents
282347
then
283348
printf "%s\n" "$msg" > "$GIT_DIR"/MERGE_MSG
284349
die_with_patch $sha1 "Error redoing merge $sha1"
@@ -302,50 +367,66 @@ nth_string () {
302367
esac
303368
}
304369

305-
make_squash_message () {
370+
update_squash_messages () {
306371
if test -f "$SQUASH_MSG"; then
307-
# We want to be careful about matching only the commit
308-
# message comment lines generated by this function.
309-
# "[snrt][tdh]" matches the nth_string endings.
310-
COUNT=$(($(sed -n "s/^# Th[^0-9]*\([1-9][0-9]*\)[snrt][tdh] commit message.*:/\1/p" \
311-
< "$SQUASH_MSG" | sed -ne '$p')+1))
312-
echo "# This is a combination of $COUNT commits."
313-
sed -e 1d -e '2,/^./{
314-
/^$/d
315-
}' <"$SQUASH_MSG"
372+
mv "$SQUASH_MSG" "$SQUASH_MSG".bak || exit
373+
COUNT=$(($(sed -n \
374+
-e "1s/^# This is a combination of \(.*\) commits\./\1/p" \
375+
-e "q" < "$SQUASH_MSG".bak)+1))
376+
{
377+
echo "# This is a combination of $COUNT commits."
378+
sed -e 1d -e '2,/^./{
379+
/^$/d
380+
}' <"$SQUASH_MSG".bak
381+
} >$SQUASH_MSG
316382
else
383+
commit_message HEAD > "$FIXUP_MSG" || die "Cannot write $FIXUP_MSG"
317384
COUNT=2
318-
echo "# This is a combination of two commits."
319-
echo "# The first commit's message is:"
320-
echo
321-
git cat-file commit HEAD | sed -e '1,/^$/d'
385+
{
386+
echo "# This is a combination of 2 commits."
387+
echo "# The first commit's message is:"
388+
echo
389+
cat "$FIXUP_MSG"
390+
} >$SQUASH_MSG
322391
fi
323392
case $1 in
324393
squash)
394+
rm -f "$FIXUP_MSG"
325395
echo
326396
echo "# This is the $(nth_string $COUNT) commit message:"
327397
echo
328-
git cat-file commit $2 | sed -e '1,/^$/d'
398+
commit_message $2
329399
;;
330400
fixup)
331401
echo
332402
echo "# The $(nth_string $COUNT) commit message will be skipped:"
333403
echo
334-
# Comment the lines of the commit message out using
335-
# "# " rather than "# " to make them less likely to
336-
# confuse the sed regexp above.
337-
git cat-file commit $2 | sed -e '1,/^$/d' -e 's/^/# /'
404+
commit_message $2 | sed -e 's/^/# /'
338405
;;
339-
esac
406+
esac >>$SQUASH_MSG
340407
}
341408

342409
peek_next_command () {
343410
sed -n -e "/^#/d" -e "/^$/d" -e "s/ .*//p" -e "q" < "$TODO"
344411
}
345412

413+
# A squash/fixup has failed. Prepare the long version of the squash
414+
# commit message, then die_with_patch. This code path requires the
415+
# user to edit the combined commit message for all commits that have
416+
# been squashed/fixedup so far. So also erase the old squash
417+
# messages, effectively causing the combined commit to be used as the
418+
# new basis for any further squash/fixups. Args: sha1 rest
419+
die_failed_squash() {
420+
mv "$SQUASH_MSG" "$MSG" || exit
421+
rm -f "$FIXUP_MSG"
422+
cp "$MSG" "$GIT_DIR"/MERGE_MSG || exit
423+
warn
424+
warn "Could not apply $1... $2"
425+
die_with_patch $1 ""
426+
}
427+
346428
do_next () {
347-
rm -f "$DOTEST"/message "$DOTEST"/author-script \
348-
"$DOTEST"/amend || exit
429+
rm -f "$MSG" "$AUTHOR_SCRIPT" "$AMEND" || exit
349430
read command sha1 rest < "$TODO"
350431
case "$command" in
351432
'#'*|''|noop)
@@ -373,7 +454,7 @@ do_next () {
373454
pick_one $sha1 ||
374455
die_with_patch $sha1 "Could not apply $sha1... $rest"
375456
make_patch $sha1
376-
git rev-parse --verify HEAD > "$DOTEST"/amend
457+
git rev-parse --verify HEAD > "$AMEND"
377458
warn "Stopped at $sha1... $rest"
378459
warn "You can amend the commit now, with"
379460
warn
@@ -400,45 +481,34 @@ do_next () {
400481
die "Cannot '$squash_style' without a previous commit"
401482

402483
mark_action_done
403-
make_squash_message $squash_style $sha1 > "$MSG"
404-
failed=f
484+
update_squash_messages $squash_style $sha1
405485
author_script=$(get_author_ident_from_commit HEAD)
486+
echo "$author_script" > "$AUTHOR_SCRIPT"
487+
eval "$author_script"
406488
output git reset --soft HEAD^
407-
pick_one -n $sha1 || failed=t
489+
pick_one -n $sha1 || die_failed_squash $sha1 "$rest"
408490
case "$(peek_next_command)" in
409491
squash|s|fixup|f)
410-
USE_OUTPUT=output
411-
MSG_OPT=-F
412-
EDIT_OR_FILE="$MSG"
413-
cp "$MSG" "$SQUASH_MSG"
492+
# This is an intermediate commit; its message will only be
493+
# used in case of trouble. So use the long version:
494+
do_with_author output git commit --no-verify -F "$SQUASH_MSG" ||
495+
die_failed_squash $sha1 "$rest"
414496
;;
415497
*)
416-
USE_OUTPUT=
417-
MSG_OPT=
418-
EDIT_OR_FILE=-e
419-
rm -f "$SQUASH_MSG" || exit
420-
cp "$MSG" "$GIT_DIR"/SQUASH_MSG
421-
rm -f "$GIT_DIR"/MERGE_MSG || exit
498+
# This is the final command of this squash/fixup group
499+
if test -f "$FIXUP_MSG"
500+
then
501+
do_with_author git commit --no-verify -F "$FIXUP_MSG" ||
502+
die_failed_squash $sha1 "$rest"
503+
else
504+
cp "$SQUASH_MSG" "$GIT_DIR"/SQUASH_MSG || exit
505+
rm -f "$GIT_DIR"/MERGE_MSG
506+
do_with_author git commit --no-verify -e ||
507+
die_failed_squash $sha1 "$rest"
508+
fi
509+
rm -f "$SQUASH_MSG" "$FIXUP_MSG"
422510
;;
423511
esac
424-
echo "$author_script" > "$DOTEST"/author-script
425-
if test $failed = f
426-
then
427-
# This is like --amend, but with a different message
428-
eval "$author_script"
429-
GIT_AUTHOR_NAME="$GIT_AUTHOR_NAME" \
430-
GIT_AUTHOR_EMAIL="$GIT_AUTHOR_EMAIL" \
431-
GIT_AUTHOR_DATE="$GIT_AUTHOR_DATE" \
432-
$USE_OUTPUT git commit --no-verify \
433-
$MSG_OPT "$EDIT_OR_FILE" || failed=t
434-
fi
435-
if test $failed = t
436-
then
437-
cp "$MSG" "$GIT_DIR"/MERGE_MSG
438-
warn
439-
warn "Could not apply $sha1... $rest"
440-
die_with_patch $sha1 ""
441-
fi
442512
;;
443513
*)
444514
warn "Unknown command: $command $sha1 $rest"
@@ -598,21 +668,20 @@ do
598668
then
599669
: Nothing to commit -- skip this
600670
else
601-
. "$DOTEST"/author-script ||
671+
. "$AUTHOR_SCRIPT" ||
602672
die "Cannot find the author identity"
603673
amend=
604-
if test -f "$DOTEST"/amend
674+
if test -f "$AMEND"
605675
then
606676
amend=$(git rev-parse --verify HEAD)
607-
test "$amend" = $(cat "$DOTEST"/amend) ||
677+
test "$amend" = $(cat "$AMEND") ||
608678
die "\
609679
You have uncommitted changes in your working tree. Please, commit them
610680
first and then run 'git rebase --continue' again."
611681
git reset --soft HEAD^ ||
612682
die "Cannot rewind the HEAD"
613683
fi
614-
export GIT_AUTHOR_NAME GIT_AUTHOR_EMAIL GIT_AUTHOR_DATE &&
615-
git commit --no-verify -F "$DOTEST"/message -e || {
684+
do_with_author git commit --no-verify -F "$MSG" -e || {
616685
test -n "$amend" && git reset --soft $amend
617686
die "Could not commit staged changes."
618687
}
@@ -739,13 +808,6 @@ first and then run 'git rebase --continue' again."
739808
test t = "$VERBOSE" && : > "$DOTEST"/verbose
740809
if test t = "$PRESERVE_MERGES"
741810
then
742-
# $REWRITTEN contains files for each commit that is
743-
# reachable by at least one merge base of $HEAD and
744-
# $UPSTREAM. They are not necessarily rewritten, but
745-
# their children might be.
746-
# This ensures that commits on merged, but otherwise
747-
# unrelated side branches are left alone. (Think "X"
748-
# in the man page's example.)
749811
if test -z "$REBASE_ROOT"
750812
then
751813
mkdir "$REWRITTEN" &&

t/lib-rebase.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22

33
# After setting the fake editor with this function, you can
44
#
5-
# - override the commit message with $FAKE_COMMIT_MESSAGE,
5+
# - override the commit message with $FAKE_COMMIT_MESSAGE
66
# - amend the commit message with $FAKE_COMMIT_AMEND
77
# - check that non-commit messages have a certain line count with $EXPECT_COUNT
8+
# - check the commit count in the commit message header with $EXPECT_HEADER_COUNT
89
# - rewrite a rebase -i script as directed by $FAKE_LINES.
910
# $FAKE_LINES consists of a sequence of words separated by spaces.
1011
# The following word combinations are possible:
@@ -25,6 +26,9 @@ set_fake_editor () {
2526
cat >> fake-editor.sh <<\EOF
2627
case "$1" in
2728
*/COMMIT_EDITMSG)
29+
test -z "$EXPECT_HEADER_COUNT" ||
30+
test "$EXPECT_HEADER_COUNT" = $(sed -n '1s/^# This is a combination of \(.*\) commits\./\1/p' < "$1") ||
31+
exit
2832
test -z "$FAKE_COMMIT_MESSAGE" || echo "$FAKE_COMMIT_MESSAGE" > "$1"
2933
test -z "$FAKE_COMMIT_AMEND" || echo "$FAKE_COMMIT_AMEND" >> "$1"
3034
exit

0 commit comments

Comments
 (0)