Merge branch 'mm/rebase-i-exec'
authorJunio C Hamano <gitster@pobox.com>
Sun, 22 Aug 2010 06:29:11 +0000 (23:29 -0700)
committerJunio C Hamano <gitster@pobox.com>
Sun, 22 Aug 2010 06:29:11 +0000 (23:29 -0700)
* 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

1  2 
Documentation/git-rebase.txt
git-rebase--interactive.sh
t/t3404-rebase-interactive.sh
t/test-lib.sh
index b4314568f5a742d186e69b97f782193d2883e6b3,9c68b667e9133b808ad422b8435173aa96d90e01..3b87f1a1b67dbd64e9fd5e5cd11dd648e3f92ab2
@@@ -250,13 -250,6 +250,13 @@@ on top of the <upstream> branch using t
  the 'ours' strategy simply discards all patches from the <branch>,
  which makes little sense.
  
 +-X <strategy-option>::
 +--strategy-option=<strategy-option>::
 +      Pass the <strategy-option> through to the merge strategy.
 +      This implies `\--merge` and, if no strategy has been
 +      specified, `-s recursive`.  Note the reversal of 'ours' and
 +      'theirs' as noted in above for the `-m` option.
 +
  -q::
  --quiet::
        Be quiet. Implies --no-stat.
@@@ -466,6 -459,30 +466,30 @@@ sure that the current HEAD is "B", and 
  $ 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
  -----------------
index b94c2a03867ddcd1fa0c2051f81cf14547b550f0,e16dbe201d2c67dad043be0ccd4965c61f8a0909..3419247d03e4147c777a1c7cbd7a39a61c1129df
@@@ -119,7 -119,7 +119,7 @@@ run 'git rebase --continue'
  export GIT_CHERRY_PICK_HELP
  
  warn () {
 -      echo "$*" >&2
 +      printf '%s\n' "$*" >&2
  }
  
  output () {
@@@ -537,6 -537,34 +537,34 @@@ do_next () 
                esac
                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"
                if git rev-parse --verify -q "$sha1" >/dev/null
@@@ -591,22 -619,30 +619,30 @@@ do_rest () 
  # skip picking commits whose parents are unchanged
  skip_unnecessary_picks () {
        fd=3
-       while read -r command sha1 rest
+       while read -r command rest
        do
                # fd=3 means we skip the command
-               case "$fd,$command,$(git rev-parse --verify --quiet $sha1^)" in
-               3,pick,"$ONTO"*|3,p,"$ONTO"*)
+               case "$fd,$command" in
+               3,pick|3,p)
                        # pick a commit whose parent is current $ONTO -> skip
-                       ONTO=$sha1
+                       sha1=$(printf '%s' "$rest" | cut -d ' ' -f 1)
+                       case "$(git rev-parse --verify --quiet "$sha1"^)" in
+                       "$ONTO"*)
+                               ONTO=$sha1
+                               ;;
+                       *)
+                               fd=1
+                               ;;
+                       esac
                        ;;
-               3,#*|3,,*)
+               3,#*|3,)
                        # copy comments
                        ;;
                *)
                        fd=1
                        ;;
                esac
-               printf '%s\n' "$command${sha1:+ }$sha1${rest:+ }$rest" >&$fd
+               printf '%s\n' "$command${rest:+ }$rest" >&$fd
        done <"$TODO" >"$TODO.new" 3>>"$DONE" &&
        mv -f "$TODO".new "$TODO" &&
        case "$(peek_next_command)" in
@@@ -649,12 -685,12 +685,12 @@@ rearrange_squash () 
                case " $used" in
                *" $sha1 "*) continue ;;
                esac
 -              echo "$pick $sha1 $message"
 +              printf '%s\n' "$pick $sha1 $message"
                while read -r squash action msg
                do
                        case "$message" in
                        "$msg"*)
 -                              echo "$action $squash $action! $msg"
 +                              printf '%s\n' "$action $squash $action! $msg"
                                used="$used$squash "
                                ;;
                        esac
@@@ -895,7 -931,7 +931,7 @@@ first and then run 'git rebase --contin
                do
                        if test t != "$PRESERVE_MERGES"
                        then
 -                              echo "pick $shortsha1 $rest" >> "$TODO"
 +                              printf '%s\n' "pick $shortsha1 $rest" >> "$TODO"
                        else
                                sha1=$(git rev-parse $shortsha1)
                                if test -z "$REBASE_ROOT"
                                if test f = "$preserve"
                                then
                                        touch "$REWRITTEN"/$sha1
 -                                      echo "pick $shortsha1 $rest" >> "$TODO"
 +                                      printf '%s\n' "pick $shortsha1 $rest" >> "$TODO"
                                fi
                        fi
                done
  #  e, edit = use commit, but stop for amending
  #  s, squash = use commit, but meld into previous commit
  #  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.
  # However, if you remove everything, the rebase will be aborted.
index 3af3f603fb2ad34368b4f53f523bdd71dffdf854,56891e6c74c0883236074b39fdbd83a96c5c9658..af3b663aeee8354c6f8f2dff05666f6d70c07055
@@@ -64,6 -64,67 +64,67 @@@ test_expect_success 'setup' 
        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_path_is_file touch-one &&
+       test_path_is_file touch-two &&
+       test_path_is_missing touch-three " (should have stopped before)" &&
+       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_path_is_file touch-three &&
+       test_path_is_file "touch-file  name with spaces" &&
+       test_path_is_file 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_path_is_file 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' '
        git checkout branch2 &&
        git rebase -i F &&
@@@ -143,17 -204,16 +204,17 @@@ test_expect_success 'abort' 
        git rebase --abort &&
        test $(git rev-parse new-branch1) = $(git rev-parse HEAD) &&
        test "$(git symbolic-ref -q HEAD)" = "refs/heads/branch1" &&
-       ! test -d .git/rebase-merge
+       test_path_is_missing .git/rebase-merge
  '
  
  test_expect_success 'abort with error when new base cannot be checked out' '
        git rm --cached file1 &&
        git commit -m "remove file in base" &&
        test_must_fail git rebase -i master > output 2>&1 &&
 -      grep "Untracked working tree file .file1. would be overwritten" \
 +      grep "The following untracked working tree files would be overwritten by checkout:" \
                output &&
-       ! test -d .git/rebase-merge &&
 +      grep "file1" output &&
+       test_path_is_missing .git/rebase-merge &&
        git reset --hard HEAD^
  '
  
@@@ -638,19 -698,13 +699,19 @@@ test_expect_success 'set up commits wit
        git commit -a -m "end with slash\\" &&
        echo >>file1 &&
        test_tick &&
 +      git commit -a -m "something (\000) that looks like octal" &&
 +      echo >>file1 &&
 +      test_tick &&
 +      git commit -a -m "something (\n) that looks like a newline" &&
 +      echo >>file1 &&
 +      test_tick &&
        git commit -a -m "another commit"
  '
  
  test_expect_success 'rebase-i history with funny messages' '
        git rev-list A..funny >expect &&
        test_tick &&
 -      FAKE_LINES="1 2" git rebase -i A &&
 +      FAKE_LINES="1 2 3 4" git rebase -i A &&
        git rev-list A.. >actual &&
        test_cmp expect actual
  '
diff --combined t/test-lib.sh
index 29fd7209cf1aff851f0f1f95ccecd0d949b3da6c,e913286e2d747385cef3d35d10fb0695d61a93f9..3a3d4c4723d4dece710f4f959ab689e1f8fb8760
        -v|--v|--ve|--ver|--verb|--verbo|--verbos|--verbose)
                verbose=t; shift ;;
        -q|--q|--qu|--qui|--quie|--quiet)
 -              quiet=t; shift ;;
 +              # Ignore --quiet under a TAP::Harness. Saying how many tests
 +              # passed without the ok/not ok details is always an error.
 +              test -z "$HARNESS_ACTIVE" && quiet=t; shift ;;
        --with-dashes)
                with_dashes=t; shift ;;
        --no-color)
                color=; shift ;;
 -      --no-python)
 -              # noop now...
 -              shift ;;
        --va|--val|--valg|--valgr|--valgri|--valgrin|--valgrind)
                valgrind=t; verbose=t; shift ;;
        --tee)
@@@ -256,10 -257,6 +256,10 @@@ q_to_cr () 
        tr Q '\015'
  }
  
 +q_to_tab () {
 +      tr Q '\011'
 +}
 +
  append_cr () {
        sed -e 's/$/Q/' | tr Q '\015'
  }
@@@ -545,6 -542,38 +545,38 @@@ test_external_without_stderr () 
        fi
  }
  
+ # debugging-friendly alternatives to "test [-f|-d|-e]"
+ # The commands test the existence or non-existence of $1. $2 can be
+ # given to provide a more precise diagnosis.
+ test_path_is_file () {
+       if ! [ -f "$1" ]
+       then
+               echo "File $1 doesn't exist. $*"
+               false
+       fi
+ }
+ test_path_is_dir () {
+       if ! [ -d "$1" ]
+       then
+               echo "Directory $1 doesn't exist. $*"
+               false
+       fi
+ }
+ test_path_is_missing () {
+       if [ -e "$1" ]
+       then
+               echo "Path exists:"
+               ls -ld "$1"
+               if [ $# -ge 1 ]; then
+                       echo "$*"
+               fi
+               false
+       fi
+ }
  # This is not among top-level (test_expect_success | test_expect_failure)
  # but is a prefix that can be used in the test script, like:
  #