Skip to content

Commit 5cba122

Browse files
committed
Merge branch 'mm/rebase-i-exec'
* mm/rebase-i-exec: git-rebase--interactive.sh: use printf instead of echo to print commit message git-rebase--interactive.sh: rework skip_unnecessary_picks test-lib: user-friendly alternatives to test [-d|-f|-e] rebase -i: add exec command to launch a shell command Conflicts: git-rebase--interactive.sh t/t3404-rebase-interactive.sh
2 parents b95d0a2 + d1c3b10 commit 5cba122

File tree

7 files changed

+174
-11
lines changed

7 files changed

+174
-11
lines changed

Documentation/git-rebase.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -466,6 +466,30 @@ sure that the current HEAD is "B", and call
466466
$ git rebase -i -p --onto Q O
467467
-----------------------------
468468

469+
Reordering and editing commits usually creates untested intermediate
470+
steps. You may want to check that your history editing did not break
471+
anything by running a test, or at least recompiling at intermediate
472+
points in history by using the "exec" command (shortcut "x"). You may
473+
do so by creating a todo list like this one:
474+
475+
-------------------------------------------
476+
pick deadbee Implement feature XXX
477+
fixup f1a5c00 Fix to feature XXX
478+
exec make
479+
pick c0ffeee The oneline of the next commit
480+
edit deadbab The oneline of the commit after
481+
exec cd subdir; make test
482+
...
483+
-------------------------------------------
484+
485+
The interactive rebase will stop when a command fails (i.e. exits with
486+
non-0 status) to give you an opportunity to fix the problem. You can
487+
continue with `git rebase --continue`.
488+
489+
The "exec" command launches the command in a shell (the one specified
490+
in `$SHELL`, or the default shell if `$SHELL` is not set), so you can
491+
use shell features (like "cd", ">", ";" ...). The command is run from
492+
the root of the working tree.
469493

470494
SPLITTING COMMITS
471495
-----------------

git-rebase--interactive.sh

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,34 @@ do_next () {
537537
esac
538538
record_in_rewritten $sha1
539539
;;
540+
x|"exec")
541+
read -r command rest < "$TODO"
542+
mark_action_done
543+
printf 'Executing: %s\n' "$rest"
544+
# "exec" command doesn't take a sha1 in the todo-list.
545+
# => can't just use $sha1 here.
546+
git rev-parse --verify HEAD > "$DOTEST"/stopped-sha
547+
${SHELL:-@SHELL_PATH@} -c "$rest" # Actual execution
548+
status=$?
549+
if test "$status" -ne 0
550+
then
551+
warn "Execution failed: $rest"
552+
warn "You can fix the problem, and then run"
553+
warn
554+
warn " git rebase --continue"
555+
warn
556+
exit "$status"
557+
fi
558+
# Run in subshell because require_clean_work_tree can die.
559+
if ! (require_clean_work_tree)
560+
then
561+
warn "Commit or stash your changes, and then run"
562+
warn
563+
warn " git rebase --continue"
564+
warn
565+
exit 1
566+
fi
567+
;;
540568
*)
541569
warn "Unknown command: $command $sha1 $rest"
542570
if git rev-parse --verify -q "$sha1" >/dev/null
@@ -591,22 +619,30 @@ do_rest () {
591619
# skip picking commits whose parents are unchanged
592620
skip_unnecessary_picks () {
593621
fd=3
594-
while read -r command sha1 rest
622+
while read -r command rest
595623
do
596624
# fd=3 means we skip the command
597-
case "$fd,$command,$(git rev-parse --verify --quiet $sha1^)" in
598-
3,pick,"$ONTO"*|3,p,"$ONTO"*)
625+
case "$fd,$command" in
626+
3,pick|3,p)
599627
# pick a commit whose parent is current $ONTO -> skip
600-
ONTO=$sha1
628+
sha1=$(printf '%s' "$rest" | cut -d ' ' -f 1)
629+
case "$(git rev-parse --verify --quiet "$sha1"^)" in
630+
"$ONTO"*)
631+
ONTO=$sha1
632+
;;
633+
*)
634+
fd=1
635+
;;
636+
esac
601637
;;
602-
3,#*|3,,*)
638+
3,#*|3,)
603639
# copy comments
604640
;;
605641
*)
606642
fd=1
607643
;;
608644
esac
609-
printf '%s\n' "$command${sha1:+ }$sha1${rest:+ }$rest" >&$fd
645+
printf '%s\n' "$command${rest:+ }$rest" >&$fd
610646
done <"$TODO" >"$TODO.new" 3>>"$DONE" &&
611647
mv -f "$TODO".new "$TODO" &&
612648
case "$(peek_next_command)" in
@@ -957,6 +993,7 @@ first and then run 'git rebase --continue' again."
957993
# e, edit = use commit, but stop for amending
958994
# s, squash = use commit, but meld into previous commit
959995
# f, fixup = like "squash", but discard this commit's log message
996+
# x <cmd>, exec <cmd> = Run a shell command <cmd>, and stop if it fails
960997
#
961998
# If you remove a line here THAT COMMIT WILL BE LOST.
962999
# However, if you remove everything, the rebase will be aborted.

t/README

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,13 @@ library for your script to use.
467467
<expected> file. This behaves like "cmp" but produces more
468468
helpful output when the test is run with "-v" option.
469469

470+
- test_path_is_file <file> [<diagnosis>]
471+
test_path_is_dir <dir> [<diagnosis>]
472+
test_path_is_missing <path> [<diagnosis>]
473+
474+
Check whether a file/directory exists or doesn't. <diagnosis> will
475+
be displayed if the test fails.
476+
470477
- test_when_finished <script>
471478

472479
Prepend <script> to a list of commands to run to clean up

t/lib-rebase.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ for line in $FAKE_LINES; do
4747
case $line in
4848
squash|fixup|edit|reword)
4949
action="$line";;
50+
exec*)
51+
echo "$line" | sed 's/_/ /g' >> "$1";;
5052
"#")
5153
echo '# comment' >> "$1";;
5254
">")

t/t3404-rebase-interactive.sh

Lines changed: 63 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,67 @@ test_expect_success 'setup' '
6464
done
6565
'
6666

67+
# "exec" commands are ran with the user shell by default, but this may
68+
# be non-POSIX. For example, if SHELL=zsh then ">file" doesn't work
69+
# to create a file. Unseting SHELL avoids such non-portable behavior
70+
# in tests.
71+
SHELL=
72+
73+
test_expect_success 'rebase -i with the exec command' '
74+
git checkout master &&
75+
(
76+
FAKE_LINES="1 exec_>touch-one
77+
2 exec_>touch-two exec_false exec_>touch-three
78+
3 4 exec_>\"touch-file__name_with_spaces\";_>touch-after-semicolon 5" &&
79+
export FAKE_LINES &&
80+
test_must_fail git rebase -i A
81+
) &&
82+
test_path_is_file touch-one &&
83+
test_path_is_file touch-two &&
84+
test_path_is_missing touch-three " (should have stopped before)" &&
85+
test $(git rev-parse C) = $(git rev-parse HEAD) || {
86+
echo "Stopped at wrong revision:"
87+
echo "($(git describe --tags HEAD) instead of C)"
88+
false
89+
} &&
90+
git rebase --continue &&
91+
test_path_is_file touch-three &&
92+
test_path_is_file "touch-file name with spaces" &&
93+
test_path_is_file touch-after-semicolon &&
94+
test $(git rev-parse master) = $(git rev-parse HEAD) || {
95+
echo "Stopped at wrong revision:"
96+
echo "($(git describe --tags HEAD) instead of master)"
97+
false
98+
} &&
99+
rm -f touch-*
100+
'
101+
102+
test_expect_success 'rebase -i with the exec command runs from tree root' '
103+
git checkout master &&
104+
mkdir subdir && cd subdir &&
105+
FAKE_LINES="1 exec_>touch-subdir" \
106+
git rebase -i HEAD^ &&
107+
cd .. &&
108+
test_path_is_file touch-subdir &&
109+
rm -fr subdir
110+
'
111+
112+
test_expect_success 'rebase -i with the exec command checks tree cleanness' '
113+
git checkout master &&
114+
(
115+
FAKE_LINES="exec_echo_foo_>file1 1" &&
116+
export FAKE_LINES &&
117+
test_must_fail git rebase -i HEAD^
118+
) &&
119+
test $(git rev-parse master^) = $(git rev-parse HEAD) || {
120+
echo "Stopped at wrong revision:"
121+
echo "($(git describe --tags HEAD) instead of master^)"
122+
false
123+
} &&
124+
git reset --hard &&
125+
git rebase --continue
126+
'
127+
67128
test_expect_success 'no changes are a nop' '
68129
git checkout branch2 &&
69130
git rebase -i F &&
@@ -143,7 +204,7 @@ test_expect_success 'abort' '
143204
git rebase --abort &&
144205
test $(git rev-parse new-branch1) = $(git rev-parse HEAD) &&
145206
test "$(git symbolic-ref -q HEAD)" = "refs/heads/branch1" &&
146-
! test -d .git/rebase-merge
207+
test_path_is_missing .git/rebase-merge
147208
'
148209

149210
test_expect_success 'abort with error when new base cannot be checked out' '
@@ -153,7 +214,7 @@ test_expect_success 'abort with error when new base cannot be checked out' '
153214
grep "The following untracked working tree files would be overwritten by checkout:" \
154215
output &&
155216
grep "file1" output &&
156-
! test -d .git/rebase-merge &&
217+
test_path_is_missing .git/rebase-merge &&
157218
git reset --hard HEAD^
158219
'
159220

t/t3407-rebase-abort.sh

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ testrebase() {
3838
# Clean up the state from the previous one
3939
git reset --hard pre-rebase &&
4040
test_must_fail git rebase$type master &&
41-
test -d "$dotest" &&
41+
test_path_is_dir "$dotest" &&
4242
git rebase --abort &&
4343
test $(git rev-parse to-rebase) = $(git rev-parse pre-rebase) &&
4444
test ! -d "$dotest"
@@ -49,7 +49,7 @@ testrebase() {
4949
# Clean up the state from the previous one
5050
git reset --hard pre-rebase &&
5151
test_must_fail git rebase$type master &&
52-
test -d "$dotest" &&
52+
test_path_is_dir "$dotest" &&
5353
test_must_fail git rebase --skip &&
5454
test $(git rev-parse HEAD) = $(git rev-parse master) &&
5555
git rebase --abort &&
@@ -62,7 +62,7 @@ testrebase() {
6262
# Clean up the state from the previous one
6363
git reset --hard pre-rebase &&
6464
test_must_fail git rebase$type master &&
65-
test -d "$dotest" &&
65+
test_path_is_dir "$dotest" &&
6666
echo c > a &&
6767
echo d >> a &&
6868
git add a &&

t/test-lib.sh

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,38 @@ test_external_without_stderr () {
545545
fi
546546
}
547547

548+
# debugging-friendly alternatives to "test [-f|-d|-e]"
549+
# The commands test the existence or non-existence of $1. $2 can be
550+
# given to provide a more precise diagnosis.
551+
test_path_is_file () {
552+
if ! [ -f "$1" ]
553+
then
554+
echo "File $1 doesn't exist. $*"
555+
false
556+
fi
557+
}
558+
559+
test_path_is_dir () {
560+
if ! [ -d "$1" ]
561+
then
562+
echo "Directory $1 doesn't exist. $*"
563+
false
564+
fi
565+
}
566+
567+
test_path_is_missing () {
568+
if [ -e "$1" ]
569+
then
570+
echo "Path exists:"
571+
ls -ld "$1"
572+
if [ $# -ge 1 ]; then
573+
echo "$*"
574+
fi
575+
false
576+
fi
577+
}
578+
579+
548580
# This is not among top-level (test_expect_success | test_expect_failure)
549581
# but is a prefix that can be used in the test script, like:
550582
#

0 commit comments

Comments
 (0)