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 <Matthieu.Moy@imag.fr>
Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
Matthieu Moy 2010-08-10 17:17:51 +02:00 коммит произвёл Junio C Hamano
Родитель 64fdc08dac
Коммит cd035b1cef
4 изменённых файлов: 122 добавлений и 3 удалений

Просмотреть файл

@ -459,6 +459,30 @@ sure that the current HEAD is "B", and call
$ git rebase -i -p --onto Q O $ git rebase -i -p --onto Q O
----------------------------- -----------------------------
Reordering and editing commits usually creates untested intermediate
steps. You may want to check that your history editing did not break
anything by running a test, or at least recompiling at intermediate
points in history by using the "exec" command (shortcut "x"). You may
do so by creating a todo list like this one:
-------------------------------------------
pick deadbee Implement feature XXX
fixup f1a5c00 Fix to feature XXX
exec make
pick c0ffeee The oneline of the next commit
edit deadbab The oneline of the commit after
exec cd subdir; make test
...
-------------------------------------------
The interactive rebase will stop when a command fails (i.e. exits with
non-0 status) to give you an opportunity to fix the problem. You can
continue with `git rebase --continue`.
The "exec" command launches the command in a shell (the one specified
in `$SHELL`, or the default shell if `$SHELL` is not set), so you can
use shell features (like "cd", ">", ";" ...). The command is run from
the root of the working tree.
SPLITTING COMMITS SPLITTING COMMITS
----------------- -----------------

Просмотреть файл

@ -537,6 +537,34 @@ do_next () {
esac esac
record_in_rewritten $sha1 record_in_rewritten $sha1
;; ;;
x|"exec")
read -r command rest < "$TODO"
mark_action_done
printf 'Executing: %s\n' "$rest"
# "exec" command doesn't take a sha1 in the todo-list.
# => can't just use $sha1 here.
git rev-parse --verify HEAD > "$DOTEST"/stopped-sha
${SHELL:-@SHELL_PATH@} -c "$rest" # Actual execution
status=$?
if test "$status" -ne 0
then
warn "Execution failed: $rest"
warn "You can fix the problem, and then run"
warn
warn " git rebase --continue"
warn
exit "$status"
fi
# Run in subshell because require_clean_work_tree can die.
if ! (require_clean_work_tree)
then
warn "Commit or stash your changes, and then run"
warn
warn " git rebase --continue"
warn
exit 1
fi
;;
*) *)
warn "Unknown command: $command $sha1 $rest" warn "Unknown command: $command $sha1 $rest"
if git rev-parse --verify -q "$sha1" >/dev/null if git rev-parse --verify -q "$sha1" >/dev/null
@ -591,10 +619,13 @@ do_rest () {
# skip picking commits whose parents are unchanged # skip picking commits whose parents are unchanged
skip_unnecessary_picks () { skip_unnecessary_picks () {
fd=3 fd=3
while read -r command sha1 rest while read -r line
do do
command=$(echo "$line" | sed 's/ */ /' | cut -d ' ' -f 1)
sha1=$(echo "$line" | sed 's/ */ /' | cut -d ' ' -f 2)
rest=$(echo "$line" | sed 's/ */ /' | cut -d ' ' -f 3-)
# fd=3 means we skip the command # fd=3 means we skip the command
case "$fd,$command,$(git rev-parse --verify --quiet $sha1^)" in case "$fd,$command,$(git rev-parse --verify --quiet "$sha1"^)" in
3,pick,"$ONTO"*|3,p,"$ONTO"*) 3,pick,"$ONTO"*|3,p,"$ONTO"*)
# pick a commit whose parent is current $ONTO -> skip # pick a commit whose parent is current $ONTO -> skip
ONTO=$sha1 ONTO=$sha1
@ -606,7 +637,7 @@ skip_unnecessary_picks () {
fd=1 fd=1
;; ;;
esac esac
echo "$command${sha1:+ }$sha1${rest:+ }$rest" >&$fd echo "$line" >&$fd
done <"$TODO" >"$TODO.new" 3>>"$DONE" && done <"$TODO" >"$TODO.new" 3>>"$DONE" &&
mv -f "$TODO".new "$TODO" && mv -f "$TODO".new "$TODO" &&
case "$(peek_next_command)" in case "$(peek_next_command)" in
@ -957,6 +988,7 @@ first and then run 'git rebase --continue' again."
# e, edit = use commit, but stop for amending # e, edit = use commit, but stop for amending
# s, squash = use commit, but meld into previous commit # s, squash = use commit, but meld into previous commit
# f, fixup = like "squash", but discard this commit's log message # f, fixup = like "squash", but discard this commit's log message
# x <cmd>, exec <cmd> = Run a shell command <cmd>, and stop if it fails
# #
# If you remove a line here THAT COMMIT WILL BE LOST. # If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted. # However, if you remove everything, the rebase will be aborted.

Просмотреть файл

@ -47,6 +47,8 @@ for line in $FAKE_LINES; do
case $line in case $line in
squash|fixup|edit|reword) squash|fixup|edit|reword)
action="$line";; action="$line";;
exec*)
echo "$line" | sed 's/_/ /g' >> "$1";;
"#") "#")
echo '# comment' >> "$1";; echo '# comment' >> "$1";;
">") ">")

Просмотреть файл

@ -64,6 +64,67 @@ test_expect_success 'setup' '
done done
' '
# "exec" commands are ran with the user shell by default, but this may
# be non-POSIX. For example, if SHELL=zsh then ">file" doesn't work
# to create a file. Unseting SHELL avoids such non-portable behavior
# in tests.
SHELL=
test_expect_success 'rebase -i with the exec command' '
git checkout master &&
(
FAKE_LINES="1 exec_>touch-one
2 exec_>touch-two exec_false exec_>touch-three
3 4 exec_>\"touch-file__name_with_spaces\";_>touch-after-semicolon 5" &&
export FAKE_LINES &&
test_must_fail git rebase -i A
) &&
test -f touch-one &&
test -f touch-two &&
! test -f touch-three &&
test $(git rev-parse C) = $(git rev-parse HEAD) || {
echo "Stopped at wrong revision:"
echo "($(git describe --tags HEAD) instead of C)"
false
} &&
git rebase --continue &&
test -f touch-three &&
test -f "touch-file name with spaces" &&
test -f touch-after-semicolon &&
test $(git rev-parse master) = $(git rev-parse HEAD) || {
echo "Stopped at wrong revision:"
echo "($(git describe --tags HEAD) instead of master)"
false
} &&
rm -f touch-*
'
test_expect_success 'rebase -i with the exec command runs from tree root' '
git checkout master &&
mkdir subdir && cd subdir &&
FAKE_LINES="1 exec_>touch-subdir" \
git rebase -i HEAD^ &&
cd .. &&
test -f touch-subdir &&
rm -fr subdir
'
test_expect_success 'rebase -i with the exec command checks tree cleanness' '
git checkout master &&
(
FAKE_LINES="exec_echo_foo_>file1 1" &&
export FAKE_LINES &&
test_must_fail git rebase -i HEAD^
) &&
test $(git rev-parse master^) = $(git rev-parse HEAD) || {
echo "Stopped at wrong revision:"
echo "($(git describe --tags HEAD) instead of master^)"
false
} &&
git reset --hard &&
git rebase --continue
'
test_expect_success 'no changes are a nop' ' test_expect_success 'no changes are a nop' '
git checkout branch2 && git checkout branch2 &&
git rebase -i F && git rebase -i F &&