Skip to content

Commit cd035b1

Browse files
moygitster
authored andcommitted
rebase -i: add exec command to launch a shell command
The typical usage pattern would be to run a test (or simply a compilation command) at given points in history. The shell command is ran (from the worktree root), and the rebase is stopped when the command fails, to give the user an opportunity to fix the problem before continuing with "git rebase --continue". This needs a little rework of skip_unnecessary_picks, which wasn't robust enough to deal with lines like exec >"file name with many spaces" in the todolist. The new version extracts command, sha1 and rest from each line, but outputs the line itself verbatim to avoid changing the whitespace layout. Signed-off-by: Matthieu Moy <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 64fdc08 commit cd035b1

File tree

4 files changed

+122
-3
lines changed

4 files changed

+122
-3
lines changed

Documentation/git-rebase.txt

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

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

463487
SPLITTING COMMITS
464488
-----------------

git-rebase--interactive.sh

Lines changed: 35 additions & 3 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,10 +619,13 @@ 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 line
595623
do
624+
command=$(echo "$line" | sed 's/ */ /' | cut -d ' ' -f 1)
625+
sha1=$(echo "$line" | sed 's/ */ /' | cut -d ' ' -f 2)
626+
rest=$(echo "$line" | sed 's/ */ /' | cut -d ' ' -f 3-)
596627
# fd=3 means we skip the command
597-
case "$fd,$command,$(git rev-parse --verify --quiet $sha1^)" in
628+
case "$fd,$command,$(git rev-parse --verify --quiet "$sha1"^)" in
598629
3,pick,"$ONTO"*|3,p,"$ONTO"*)
599630
# pick a commit whose parent is current $ONTO -> skip
600631
ONTO=$sha1
@@ -606,7 +637,7 @@ skip_unnecessary_picks () {
606637
fd=1
607638
;;
608639
esac
609-
echo "$command${sha1:+ }$sha1${rest:+ }$rest" >&$fd
640+
echo "$line" >&$fd
610641
done <"$TODO" >"$TODO.new" 3>>"$DONE" &&
611642
mv -f "$TODO".new "$TODO" &&
612643
case "$(peek_next_command)" in
@@ -957,6 +988,7 @@ first and then run 'git rebase --continue' again."
957988
# e, edit = use commit, but stop for amending
958989
# s, squash = use commit, but meld into previous commit
959990
# f, fixup = like "squash", but discard this commit's log message
991+
# x <cmd>, exec <cmd> = Run a shell command <cmd>, and stop if it fails
960992
#
961993
# If you remove a line here THAT COMMIT WILL BE LOST.
962994
# However, if you remove everything, the rebase will be aborted.

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: 61 additions & 0 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 -f touch-one &&
83+
test -f touch-two &&
84+
! test -f touch-three &&
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 -f touch-three &&
92+
test -f "touch-file name with spaces" &&
93+
test -f 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 -f 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 &&

0 commit comments

Comments
 (0)