Merge branch 'tr/reset-checkout-patch'
authorJunio C Hamano <gitster@pobox.com>
Mon, 7 Sep 2009 22:24:38 +0000 (15:24 -0700)
committerJunio C Hamano <gitster@pobox.com>
Mon, 7 Sep 2009 22:24:38 +0000 (15:24 -0700)
* tr/reset-checkout-patch:
  stash: simplify defaulting to "save" and reject unknown options
  Make test case number unique
  tests: disable interactive hunk selection tests if perl is not available
  DWIM 'git stash save -p' for 'git stash -p'
  Implement 'git stash save --patch'
  Implement 'git checkout --patch'
  Implement 'git reset --patch'
  builtin-add: refactor the meat of interactive_add()
  Add a small patch-mode testing library
  git-apply--interactive: Refactor patch mode code
  Make 'git stash -k' a short form for 'git stash save --keep-index'

14 files changed:
Documentation/git-checkout.txt
Documentation/git-reset.txt
Documentation/git-stash.txt
builtin-add.c
builtin-checkout.c
builtin-reset.c
commit.h
git-add--interactive.perl
git-stash.sh
t/lib-patch-mode.sh [new file with mode: 0755]
t/t2016-checkout-patch.sh [new file with mode: 0755]
t/t3903-stash.sh
t/t3904-stash-patch.sh [new file with mode: 0755]
t/t7105-reset-patch.sh [new file with mode: 0755]

index b1314b5..37c1810 100644 (file)
@@ -11,6 +11,7 @@ SYNOPSIS
 'git checkout' [-q] [-f] [-m] [<branch>]
 'git checkout' [-q] [-f] [-m] [-b <new_branch>] [<start_point>]
 'git checkout' [-f|--ours|--theirs|-m|--conflict=<style>] [<tree-ish>] [--] <paths>...
+'git checkout' --patch [<tree-ish>] [--] [<paths>...]
 
 DESCRIPTION
 -----------
@@ -25,7 +26,7 @@ use the --track or --no-track options, which will be passed to `git
 branch`.  As a convenience, --track without `-b` implies branch
 creation; see the description of --track below.
 
-When <paths> are given, this command does *not* switch
+When <paths> or --patch are given, this command does *not* switch
 branches.  It updates the named paths in the working tree from
 the index file, or from a named <tree-ish> (most often a commit).  In
 this case, the `-b` and `--track` options are meaningless and giving
@@ -115,6 +116,16 @@ the conflicted merge in the specified paths.
        "merge" (default) and "diff3" (in addition to what is shown by
        "merge" style, shows the original contents).
 
+-p::
+--patch::
+       Interactively select hunks in the difference between the
+       <tree-ish> (or the index, if unspecified) and the working
+       tree.  The chosen hunks are then applied in reverse to the
+       working tree (and if a <tree-ish> was specified, the index).
++
+This means that you can use `git checkout -p` to selectively discard
+edits from your current working tree.
+
 <branch>::
        Branch to checkout; if it refers to a branch (i.e., a name that,
        when prepended with "refs/heads/", is a valid ref), then that
index abb25d1..469cf6d 100644 (file)
@@ -10,6 +10,7 @@ SYNOPSIS
 [verse]
 'git reset' [--mixed | --soft | --hard | --merge] [-q] [<commit>]
 'git reset' [-q] [<commit>] [--] <paths>...
+'git reset' --patch [<commit>] [--] [<paths>...]
 
 DESCRIPTION
 -----------
@@ -23,8 +24,9 @@ the undo in the history.
 If you want to undo a commit other than the latest on a branch,
 linkgit:git-revert[1] is your friend.
 
-The second form with 'paths' is used to revert selected paths in
-the index from a given commit, without moving HEAD.
+The second and third forms with 'paths' and/or --patch are used to
+revert selected paths in the index from a given commit, without moving
+HEAD.
 
 
 OPTIONS
@@ -50,6 +52,15 @@ OPTIONS
        and updates the files that are different between the named commit
        and the current commit in the working tree.
 
+-p::
+--patch::
+       Interactively select hunks in the difference between the index
+       and <commit> (defaults to HEAD).  The chosen hunks are applied
+       in reverse to the index.
++
+This means that `git reset -p` is the opposite of `git add -p` (see
+linkgit:git-add[1]).
+
 -q::
        Be quiet, only report errors.
 
index 2f5ca7b..3f14b72 100644 (file)
@@ -13,7 +13,7 @@ SYNOPSIS
 'git stash' drop [-q|--quiet] [<stash>]
 'git stash' ( pop | apply ) [--index] [-q|--quiet] [<stash>]
 'git stash' branch <branchname> [<stash>]
-'git stash' [save [--keep-index] [-q|--quiet] [<message>]]
+'git stash' [save [--patch] [-k|--[no-]keep-index] [-q|--quiet] [<message>]]
 'git stash' clear
 'git stash' create
 
@@ -42,15 +42,27 @@ is also possible).
 OPTIONS
 -------
 
-save [--keep-index] [-q|--quiet] [<message>]::
+save [--patch] [--[no-]keep-index] [-q|--quiet] [<message>]::
 
        Save your local modifications to a new 'stash', and run `git reset
-       --hard` to revert them.  This is the default action when no
-       subcommand is given. The <message> part is optional and gives
-       the description along with the stashed state.
+       --hard` to revert them.  The <message> part is optional and gives
+       the description along with the stashed state.  For quickly making
+       a snapshot, you can omit _both_ "save" and <message>, but giving
+       only <message> does not trigger this action to prevent a misspelled
+       subcommand from making an unwanted stash.
 +
 If the `--keep-index` option is used, all changes already added to the
 index are left intact.
++
+With `--patch`, you can interactively select hunks from in the diff
+between HEAD and the working tree to be stashed.  The stash entry is
+constructed such that its index state is the same as the index state
+of your repository, and its worktree contains only the changes you
+selected interactively.  The selected changes are then rolled back
+from your worktree.
++
+The `--patch` option implies `--keep-index`.  You can use
+`--no-keep-index` to override this.
 
 list [<options>]::
 
index 006fd08..a571473 100644 (file)
@@ -131,27 +131,27 @@ static const char **validate_pathspec(int argc, const char **argv, const char *p
        return pathspec;
 }
 
-int interactive_add(int argc, const char **argv, const char *prefix)
+int run_add_interactive(const char *revision, const char *patch_mode,
+                       const char **pathspec)
 {
-       int status, ac;
+       int status, ac, pc = 0;
        const char **args;
-       const char **pathspec = NULL;
 
-       if (argc) {
-               pathspec = validate_pathspec(argc, argv, prefix);
-               if (!pathspec)
-                       return -1;
-       }
+       if (pathspec)
+               while (pathspec[pc])
+                       pc++;
 
-       args = xcalloc(sizeof(const char *), (argc + 4));
+       args = xcalloc(sizeof(const char *), (pc + 5));
        ac = 0;
        args[ac++] = "add--interactive";
-       if (patch_interactive)
-               args[ac++] = "--patch";
+       if (patch_mode)
+               args[ac++] = patch_mode;
+       if (revision)
+               args[ac++] = revision;
        args[ac++] = "--";
-       if (argc) {
-               memcpy(&(args[ac]), pathspec, sizeof(const char *) * argc);
-               ac += argc;
+       if (pc) {
+               memcpy(&(args[ac]), pathspec, sizeof(const char *) * pc);
+               ac += pc;
        }
        args[ac] = NULL;
 
@@ -160,6 +160,21 @@ int interactive_add(int argc, const char **argv, const char *prefix)
        return status;
 }
 
+int interactive_add(int argc, const char **argv, const char *prefix)
+{
+       const char **pathspec = NULL;
+
+       if (argc) {
+               pathspec = validate_pathspec(argc, argv, prefix);
+               if (!pathspec)
+                       return -1;
+       }
+
+       return run_add_interactive(NULL,
+                                  patch_interactive ? "--patch" : NULL,
+                                  pathspec);
+}
+
 static int edit_patch(int argc, const char **argv, const char *prefix)
 {
        char *file = xstrdup(git_path("ADD_EDIT.patch"));
index 36e2116..d050c37 100644 (file)
@@ -566,6 +566,13 @@ static int git_checkout_config(const char *var, const char *value, void *cb)
        return git_xmerge_config(var, value, cb);
 }
 
+static int interactive_checkout(const char *revision, const char **pathspec,
+                               struct checkout_opts *opts)
+{
+       return run_add_interactive(revision, "--patch=checkout", pathspec);
+}
+
+
 int cmd_checkout(int argc, const char **argv, const char *prefix)
 {
        struct checkout_opts opts;
@@ -574,6 +581,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
        struct branch_info new;
        struct tree *source_tree = NULL;
        char *conflict_style = NULL;
+       int patch_mode = 0;
        struct option options[] = {
                OPT__QUIET(&opts.quiet),
                OPT_STRING('b', NULL, &opts.new_branch, "new branch", "branch"),
@@ -588,6 +596,7 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
                OPT_BOOLEAN('m', "merge", &opts.merge, "merge"),
                OPT_STRING(0, "conflict", &conflict_style, "style",
                           "conflict style (merge or diff3)"),
+               OPT_BOOLEAN('p', "patch", &patch_mode, "select hunks interactively"),
                OPT_END(),
        };
        int has_dash_dash;
@@ -602,6 +611,10 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
        argc = parse_options(argc, argv, prefix, options, checkout_usage,
                             PARSE_OPT_KEEP_DASHDASH);
 
+       if (patch_mode && (opts.track > 0 || opts.new_branch
+                          || opts.new_branch_log || opts.merge || opts.force))
+               die ("--patch is incompatible with all other options");
+
        /* --track without -b should DWIM */
        if (0 < opts.track && !opts.new_branch) {
                const char *argv0 = argv[0];
@@ -708,6 +721,9 @@ no_reference:
                if (!pathspec)
                        die("invalid path specification");
 
+               if (patch_mode)
+                       return interactive_checkout(new.name, pathspec, &opts);
+
                /* Checkout paths */
                if (opts.new_branch) {
                        if (argc == 1) {
@@ -723,6 +739,9 @@ no_reference:
                return checkout_paths(source_tree, pathspec, &opts);
        }
 
+       if (patch_mode)
+               return interactive_checkout(new.name, NULL, &opts);
+
        if (opts.new_branch) {
                struct strbuf buf = STRBUF_INIT;
                if (strbuf_check_branch_ref(&buf, opts.new_branch))
index 0fc0b07..73e6022 100644 (file)
@@ -143,6 +143,17 @@ static void update_index_from_diff(struct diff_queue_struct *q,
        }
 }
 
+static int interactive_reset(const char *revision, const char **argv,
+                            const char *prefix)
+{
+       const char **pathspec = NULL;
+
+       if (*argv)
+               pathspec = get_pathspec(prefix, argv);
+
+       return run_add_interactive(revision, "--patch=reset", pathspec);
+}
+
 static int read_from_tree(const char *prefix, const char **argv,
                unsigned char *tree_sha1, int refresh_flags)
 {
@@ -184,6 +195,7 @@ static void prepend_reflog_action(const char *action, char *buf, size_t size)
 int cmd_reset(int argc, const char **argv, const char *prefix)
 {
        int i = 0, reset_type = NONE, update_ref_status = 0, quiet = 0;
+       int patch_mode = 0;
        const char *rev = "HEAD";
        unsigned char sha1[20], *orig = NULL, sha1_orig[20],
                                *old_orig = NULL, sha1_old_orig[20];
@@ -199,6 +211,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
                                "reset HEAD, index and working tree", MERGE),
                OPT_BOOLEAN('q', NULL, &quiet,
                                "disable showing new HEAD in hard reset and progress message"),
+               OPT_BOOLEAN('p', "patch", &patch_mode, "select hunks interactively"),
                OPT_END()
        };
 
@@ -252,6 +265,12 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
                die("Could not parse object '%s'.", rev);
        hashcpy(sha1, commit->object.sha1);
 
+       if (patch_mode) {
+               if (reset_type != NONE)
+                       die("--patch is incompatible with --{hard,mixed,soft}");
+               return interactive_reset(rev, argv + i, prefix);
+       }
+
        /* git reset tree [--] paths... can be used to
         * load chosen paths from the tree into the index without
         * affecting the working tree nor HEAD. */
index d1674d7..f4fc5c5 100644 (file)
--- a/commit.h
+++ b/commit.h
@@ -140,6 +140,8 @@ int is_descendant_of(struct commit *, struct commit_list *);
 int in_merge_bases(struct commit *, struct commit **, int);
 
 extern int interactive_add(int argc, const char **argv, const char *prefix);
+extern int run_add_interactive(const char *revision, const char *patch_mode,
+                              const char **pathspec);
 
 static inline int single_parent(struct commit *commit)
 {
index 06f7060..392efb9 100755 (executable)
@@ -72,6 +72,79 @@ sub colored {
 
 # command line options
 my $patch_mode;
+my $patch_mode_revision;
+
+sub apply_patch;
+sub apply_patch_for_checkout_commit;
+sub apply_patch_for_stash;
+
+my %patch_modes = (
+       'stage' => {
+               DIFF => 'diff-files -p',
+               APPLY => sub { apply_patch 'apply --cached', @_; },
+               APPLY_CHECK => 'apply --cached',
+               VERB => 'Stage',
+               TARGET => '',
+               PARTICIPLE => 'staging',
+               FILTER => 'file-only',
+       },
+       'stash' => {
+               DIFF => 'diff-index -p HEAD',
+               APPLY => sub { apply_patch 'apply --cached', @_; },
+               APPLY_CHECK => 'apply --cached',
+               VERB => 'Stash',
+               TARGET => '',
+               PARTICIPLE => 'stashing',
+               FILTER => undef,
+       },
+       'reset_head' => {
+               DIFF => 'diff-index -p --cached',
+               APPLY => sub { apply_patch 'apply -R --cached', @_; },
+               APPLY_CHECK => 'apply -R --cached',
+               VERB => 'Unstage',
+               TARGET => '',
+               PARTICIPLE => 'unstaging',
+               FILTER => 'index-only',
+       },
+       'reset_nothead' => {
+               DIFF => 'diff-index -R -p --cached',
+               APPLY => sub { apply_patch 'apply --cached', @_; },
+               APPLY_CHECK => 'apply --cached',
+               VERB => 'Apply',
+               TARGET => ' to index',
+               PARTICIPLE => 'applying',
+               FILTER => 'index-only',
+       },
+       'checkout_index' => {
+               DIFF => 'diff-files -p',
+               APPLY => sub { apply_patch 'apply -R', @_; },
+               APPLY_CHECK => 'apply -R',
+               VERB => 'Discard',
+               TARGET => ' from worktree',
+               PARTICIPLE => 'discarding',
+               FILTER => 'file-only',
+       },
+       'checkout_head' => {
+               DIFF => 'diff-index -p',
+               APPLY => sub { apply_patch_for_checkout_commit '-R', @_ },
+               APPLY_CHECK => 'apply -R',
+               VERB => 'Discard',
+               TARGET => ' from index and worktree',
+               PARTICIPLE => 'discarding',
+               FILTER => undef,
+       },
+       'checkout_nothead' => {
+               DIFF => 'diff-index -R -p',
+               APPLY => sub { apply_patch_for_checkout_commit '', @_ },
+               APPLY_CHECK => 'apply',
+               VERB => 'Apply',
+               TARGET => ' to index and worktree',
+               PARTICIPLE => 'applying',
+               FILTER => undef,
+       },
+);
+
+my %patch_mode_flavour = %{$patch_modes{stage}};
 
 sub run_cmd_pipe {
        if ($^O eq 'MSWin32' || $^O eq 'msys') {
@@ -190,7 +263,14 @@ sub list_modified {
                return if (!@tracked);
        }
 
-       my $reference = is_initial_commit() ? get_empty_tree() : 'HEAD';
+       my $reference;
+       if (defined $patch_mode_revision and $patch_mode_revision ne 'HEAD') {
+               $reference = $patch_mode_revision;
+       } elsif (is_initial_commit()) {
+               $reference = get_empty_tree();
+       } else {
+               $reference = 'HEAD';
+       }
        for (run_cmd_pipe(qw(git diff-index --cached
                             --numstat --summary), $reference,
                             '--', @tracked)) {
@@ -613,12 +693,24 @@ sub add_untracked_cmd {
        print "\n";
 }
 
+sub run_git_apply {
+       my $cmd = shift;
+       my $fh;
+       open $fh, '| git ' . $cmd;
+       print $fh @_;
+       return close $fh;
+}
+
 sub parse_diff {
        my ($path) = @_;
-       my @diff = run_cmd_pipe(qw(git diff-files -p --), $path);
+       my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
+       if (defined $patch_mode_revision) {
+               push @diff_cmd, $patch_mode_revision;
+       }
+       my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
        my @colored = ();
        if ($diff_use_color) {
-               @colored = run_cmd_pipe(qw(git diff-files -p --color --), $path);
+               @colored = run_cmd_pipe("git", @diff_cmd, qw(--color --), $path);
        }
        my (@hunk) = { TEXT => [], DISPLAY => [], TYPE => 'header' };
 
@@ -881,6 +973,7 @@ sub edit_hunk_manually {
                or die "failed to open hunk edit file for writing: " . $!;
        print $fh "# Manual hunk edit mode -- see bottom for a quick guide\n";
        print $fh @$oldtext;
+       my $participle = $patch_mode_flavour{PARTICIPLE};
        print $fh <<EOF;
 # ---
 # To remove '-' lines, make them ' ' lines (context).
@@ -888,7 +981,7 @@ sub edit_hunk_manually {
 # Lines starting with # will be removed.
 #
 # If the patch applies cleanly, the edited hunk will immediately be
-# marked for staging. If it does not apply cleanly, you will be given
+# marked for $participle. If it does not apply cleanly, you will be given
 # an opportunity to edit again. If all lines of the hunk are removed,
 # then the edit is aborted and the hunk is left unchanged.
 EOF
@@ -922,11 +1015,8 @@ EOF
 
 sub diff_applies {
        my $fh;
-       open $fh, '| git apply --recount --cached --check';
-       for my $h (@_) {
-               print $fh @{$h->{TEXT}};
-       }
-       return close $fh;
+       return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --recount --check',
+                            map { @{$_->{TEXT}} } @_);
 }
 
 sub _restore_terminal_and_die {
@@ -992,12 +1082,14 @@ sub edit_hunk_loop {
 }
 
 sub help_patch_cmd {
-       print colored $help_color, <<\EOF ;
-y - stage this hunk
-n - do not stage this hunk
-q - quit, do not stage this hunk nor any of the remaining ones
-a - stage this and all the remaining hunks in the file
-d - do not stage this hunk nor any of the remaining hunks in the file
+       my $verb = lc $patch_mode_flavour{VERB};
+       my $target = $patch_mode_flavour{TARGET};
+       print colored $help_color, <<EOF ;
+y - $verb this hunk$target
+n - do not $verb this hunk$target
+q - quit, do not $verb this hunk nor any of the remaining ones
+a - $verb this and all the remaining hunks in the file
+d - do not $verb this hunk nor any of the remaining hunks in the file
 g - select a hunk to go to
 / - search for a hunk matching the given regex
 j - leave this hunk undecided, see next undecided hunk
@@ -1010,8 +1102,40 @@ e - manually edit the current hunk
 EOF
 }
 
+sub apply_patch {
+       my $cmd = shift;
+       my $ret = run_git_apply $cmd . ' --recount', @_;
+       if (!$ret) {
+               print STDERR @_;
+       }
+       return $ret;
+}
+
+sub apply_patch_for_checkout_commit {
+       my $reverse = shift;
+       my $applies_index = run_git_apply 'apply '.$reverse.' --cached --recount --check', @_;
+       my $applies_worktree = run_git_apply 'apply '.$reverse.' --recount --check', @_;
+
+       if ($applies_worktree && $applies_index) {
+               run_git_apply 'apply '.$reverse.' --cached --recount', @_;
+               run_git_apply 'apply '.$reverse.' --recount', @_;
+               return 1;
+       } elsif (!$applies_index) {
+               print colored $error_color, "The selected hunks do not apply to the index!\n";
+               if (prompt_yesno "Apply them to the worktree anyway? ") {
+                       return run_git_apply 'apply '.$reverse.' --recount', @_;
+               } else {
+                       print colored $error_color, "Nothing was applied.\n";
+                       return 0;
+               }
+       } else {
+               print STDERR @_;
+               return 0;
+       }
+}
+
 sub patch_update_cmd {
-       my @all_mods = list_modified('file-only');
+       my @all_mods = list_modified($patch_mode_flavour{FILTER});
        my @mods = grep { !($_->{BINARY}) } @all_mods;
        my @them;
 
@@ -1142,8 +1266,9 @@ sub patch_update_file {
                for (@{$hunk[$ix]{DISPLAY}}) {
                        print;
                }
-               print colored $prompt_color, 'Stage ',
-                 ($hunk[$ix]{TYPE} eq 'mode' ? 'mode change' : 'this hunk'),
+               print colored $prompt_color, $patch_mode_flavour{VERB},
+                 ($hunk[$ix]{TYPE} eq 'mode' ? ' mode change' : ' this hunk'),
+                 $patch_mode_flavour{TARGET},
                  " [y,n,q,a,d,/$other,?]? ";
                my $line = prompt_single_character;
                if ($line) {
@@ -1317,16 +1442,9 @@ sub patch_update_file {
 
        if (@result) {
                my $fh;
-
-               open $fh, '| git apply --cached --recount';
-               for (@{$head->{TEXT}}, @result) {
-                       print $fh $_;
-               }
-               if (!close $fh) {
-                       for (@{$head->{TEXT}}, @result) {
-                               print STDERR $_;
-                       }
-               }
+               my @patch = (@{$head->{TEXT}}, @result);
+               my $apply_routine = $patch_mode_flavour{APPLY};
+               &$apply_routine(@patch);
                refresh();
        }
 
@@ -1367,11 +1485,41 @@ EOF
 sub process_args {
        return unless @ARGV;
        my $arg = shift @ARGV;
-       if ($arg eq "--patch") {
-               $patch_mode = 1;
-               $arg = shift @ARGV or die "missing --";
+       if ($arg =~ /--patch(?:=(.*))?/) {
+               if (defined $1) {
+                       if ($1 eq 'reset') {
+                               $patch_mode = 'reset_head';
+                               $patch_mode_revision = 'HEAD';
+                               $arg = shift @ARGV or die "missing --";
+                               if ($arg ne '--') {
+                                       $patch_mode_revision = $arg;
+                                       $patch_mode = ($arg eq 'HEAD' ?
+                                                      'reset_head' : 'reset_nothead');
+                                       $arg = shift @ARGV or die "missing --";
+                               }
+                       } elsif ($1 eq 'checkout') {
+                               $arg = shift @ARGV or die "missing --";
+                               if ($arg eq '--') {
+                                       $patch_mode = 'checkout_index';
+                               } else {
+                                       $patch_mode_revision = $arg;
+                                       $patch_mode = ($arg eq 'HEAD' ?
+                                                      'checkout_head' : 'checkout_nothead');
+                                       $arg = shift @ARGV or die "missing --";
+                               }
+                       } elsif ($1 eq 'stage' or $1 eq 'stash') {
+                               $patch_mode = $1;
+                               $arg = shift @ARGV or die "missing --";
+                       } else {
+                               die "unknown --patch mode: $1";
+                       }
+               } else {
+                       $patch_mode = 'stage';
+                       $arg = shift @ARGV or die "missing --";
+               }
                die "invalid argument $arg, expecting --"
                    unless $arg eq "--";
+               %patch_mode_flavour = %{$patch_modes{$patch_mode}};
        }
        elsif ($arg ne "--") {
                die "invalid argument $arg, expecting --";
index d61c9d0..4febbbf 100755 (executable)
@@ -7,7 +7,7 @@ USAGE="list [<options>]
    or: $dashless drop [-q|--quiet] [<stash>]
    or: $dashless ( pop | apply ) [--index] [-q|--quiet] [<stash>]
    or: $dashless branch <branchname> [<stash>]
-   or: $dashless [save [--keep-index] [-q|--quiet] [<message>]]
+   or: $dashless [save [-k|--keep-index] [-q|--quiet] [<message>]]
    or: $dashless clear"
 
 SUBDIRECTORY_OK=Yes
@@ -21,6 +21,14 @@ trap 'rm -f "$TMP-*"' 0
 
 ref_stash=refs/stash
 
+if git config --get-colorbool color.interactive; then
+       help_color="$(git config --get-color color.interactive.help 'red bold')"
+       reset_color="$(git config --get-color '' reset)"
+else
+       help_color=
+       reset_color=
+fi
+
 no_changes () {
        git diff-index --quiet --cached HEAD --ignore-submodules -- &&
        git diff-files --quiet --ignore-submodules
@@ -68,19 +76,44 @@ create_stash () {
                git commit-tree $i_tree -p $b_commit) ||
                die "Cannot save the current index state"
 
-       # state of the working tree
-       w_tree=$( (
+       if test -z "$patch_mode"
+       then
+
+               # state of the working tree
+               w_tree=$( (
+                       rm -f "$TMP-index" &&
+                       cp -p ${GIT_INDEX_FILE-"$GIT_DIR/index"} "$TMP-index" &&
+                       GIT_INDEX_FILE="$TMP-index" &&
+                       export GIT_INDEX_FILE &&
+                       git read-tree -m $i_tree &&
+                       git add -u &&
+                       git write-tree &&
+                       rm -f "$TMP-index"
+               ) ) ||
+                       die "Cannot save the current worktree state"
+
+       else
+
                rm -f "$TMP-index" &&
-               cp -p ${GIT_INDEX_FILE-"$GIT_DIR/index"} "$TMP-index" &&
-               GIT_INDEX_FILE="$TMP-index" &&
-               export GIT_INDEX_FILE &&
-               git read-tree -m $i_tree &&
-               git add -u &&
-               git write-tree &&
-               rm -f "$TMP-index"
-       ) ||
+               GIT_INDEX_FILE="$TMP-index" git read-tree HEAD &&
+
+               # find out what the user wants
+               GIT_INDEX_FILE="$TMP-index" \
+                       git add--interactive --patch=stash -- &&
+
+               # state of the working tree
+               w_tree=$(GIT_INDEX_FILE="$TMP-index" git write-tree) ||
                die "Cannot save the current worktree state"
 
+               git diff-tree -p HEAD $w_tree > "$TMP-patch" &&
+               test -s "$TMP-patch" ||
+               die "No changes selected"
+
+               rm -f "$TMP-index" ||
+               die "Cannot remove temporary index (can't happen)"
+
+       fi
+
        # create the stash
        if test -z "$stash_msg"
        then
@@ -95,15 +128,31 @@ create_stash () {
 
 save_stash () {
        keep_index=
+       patch_mode=
        while test $# != 0
        do
                case "$1" in
-               --keep-index)
+               -k|--keep-index)
+                       keep_index=t
+                       ;;
+               --no-keep-index)
+                       keep_index=
+                       ;;
+               -p|--patch)
+                       patch_mode=t
                        keep_index=t
                        ;;
                -q|--quiet)
                        GIT_QUIET=t
                        ;;
+               --)
+                       shift
+                       break
+                       ;;
+               -*)
+                       echo "error: unknown option for 'stash save': $1"
+                       usage
+                       ;;
                *)
                        break
                        ;;
@@ -131,11 +180,22 @@ save_stash () {
                die "Cannot save the current status"
        say Saved working directory and index state "$stash_msg"
 
-       git reset --hard ${GIT_QUIET:+-q}
-
-       if test -n "$keep_index" && test -n $i_tree
+       if test -z "$patch_mode"
        then
-               git read-tree --reset -u $i_tree
+               git reset --hard ${GIT_QUIET:+-q}
+
+               if test -n "$keep_index" && test -n $i_tree
+               then
+                       git read-tree --reset -u $i_tree
+               fi
+       else
+               git apply -R < "$TMP-patch" ||
+               die "Cannot remove worktree changes"
+
+               if test -z "$keep_index"
+               then
+                       git reset
+               fi
        fi
 }
 
@@ -307,6 +367,18 @@ apply_to_branch () {
        drop_stash $stash
 }
 
+# The default command is "save" if nothing but options are given
+seen_non_option=
+for opt
+do
+       case "$opt" in
+       -*) ;;
+       *) seen_non_option=t; break ;;
+       esac
+done
+
+test -n "$seen_non_option" || set "save" "$@"
+
 # Main command set
 case "$1" in
 list)
@@ -358,12 +430,13 @@ branch)
        apply_to_branch "$@"
        ;;
 *)
-       if test $# -eq 0
-       then
+       case $# in
+       0)
                save_stash &&
                say '(To restore them type "git stash apply")'
-       else
+               ;;
+       *)
                usage
-       fi
+       esac
        ;;
 esac
diff --git a/t/lib-patch-mode.sh b/t/lib-patch-mode.sh
new file mode 100755 (executable)
index 0000000..75a3ee2
--- /dev/null
@@ -0,0 +1,41 @@
+. ./test-lib.sh
+
+if ! test_have_prereq PERL; then
+       say 'skipping --patch tests, perl not available'
+       test_done
+fi
+
+set_state () {
+       echo "$3" > "$1" &&
+       git add "$1" &&
+       echo "$2" > "$1"
+}
+
+save_state () {
+       noslash="$(echo "$1" | tr / _)" &&
+       cat "$1" > _worktree_"$noslash" &&
+       git show :"$1" > _index_"$noslash"
+}
+
+set_and_save_state () {
+       set_state "$@" &&
+       save_state "$1"
+}
+
+verify_state () {
+       test "$(cat "$1")" = "$2" &&
+       test "$(git show :"$1")" = "$3"
+}
+
+verify_saved_state () {
+       noslash="$(echo "$1" | tr / _)" &&
+       verify_state "$1" "$(cat _worktree_"$noslash")" "$(cat _index_"$noslash")"
+}
+
+save_head () {
+       git rev-parse HEAD > _head
+}
+
+verify_saved_head () {
+       test "$(cat _head)" = "$(git rev-parse HEAD)"
+}
diff --git a/t/t2016-checkout-patch.sh b/t/t2016-checkout-patch.sh
new file mode 100755 (executable)
index 0000000..4d1c2e9
--- /dev/null
@@ -0,0 +1,107 @@
+#!/bin/sh
+
+test_description='git checkout --patch'
+
+. ./lib-patch-mode.sh
+
+test_expect_success 'setup' '
+       mkdir dir &&
+       echo parent > dir/foo &&
+       echo dummy > bar &&
+       git add bar dir/foo &&
+       git commit -m initial &&
+       test_tick &&
+       test_commit second dir/foo head &&
+       set_and_save_state bar bar_work bar_index &&
+       save_head
+'
+
+# note: bar sorts before dir/foo, so the first 'n' is always to skip 'bar'
+
+test_expect_success 'saying "n" does nothing' '
+       set_and_save_state dir/foo work head &&
+       (echo n; echo n) | git checkout -p &&
+       verify_saved_state bar &&
+       verify_saved_state dir/foo
+'
+
+test_expect_success 'git checkout -p' '
+       (echo n; echo y) | git checkout -p &&
+       verify_saved_state bar &&
+       verify_state dir/foo head head
+'
+
+test_expect_success 'git checkout -p with staged changes' '
+       set_state dir/foo work index
+       (echo n; echo y) | git checkout -p &&
+       verify_saved_state bar &&
+       verify_state dir/foo index index
+'
+
+test_expect_success 'git checkout -p HEAD with NO staged changes: abort' '
+       set_and_save_state dir/foo work head &&
+       (echo n; echo y; echo n) | git checkout -p HEAD &&
+       verify_saved_state bar &&
+       verify_saved_state dir/foo
+'
+
+test_expect_success 'git checkout -p HEAD with NO staged changes: apply' '
+       (echo n; echo y; echo y) | git checkout -p HEAD &&
+       verify_saved_state bar &&
+       verify_state dir/foo head head
+'
+
+test_expect_success 'git checkout -p HEAD with change already staged' '
+       set_state dir/foo index index
+       # the third n is to get out in case it mistakenly does not apply
+       (echo n; echo y; echo n) | git checkout -p HEAD &&
+       verify_saved_state bar &&
+       verify_state dir/foo head head
+'
+
+test_expect_success 'git checkout -p HEAD^' '
+       # the third n is to get out in case it mistakenly does not apply
+       (echo n; echo y; echo n) | git checkout -p HEAD^ &&
+       verify_saved_state bar &&
+       verify_state dir/foo parent parent
+'
+
+# The idea in the rest is that bar sorts first, so we always say 'y'
+# first and if the path limiter fails it'll apply to bar instead of
+# dir/foo.  There's always an extra 'n' to reject edits to dir/foo in
+# the failure case (and thus get out of the loop).
+
+test_expect_success 'path limiting works: dir' '
+       set_state dir/foo work head &&
+       (echo y; echo n) | git checkout -p dir &&
+       verify_saved_state bar &&
+       verify_state dir/foo head head
+'
+
+test_expect_success 'path limiting works: -- dir' '
+       set_state dir/foo work head &&
+       (echo y; echo n) | git checkout -p -- dir &&
+       verify_saved_state bar &&
+       verify_state dir/foo head head
+'
+
+test_expect_success 'path limiting works: HEAD^ -- dir' '
+       # the third n is to get out in case it mistakenly does not apply
+       (echo y; echo n; echo n) | git checkout -p HEAD^ -- dir &&
+       verify_saved_state bar &&
+       verify_state dir/foo parent parent
+'
+
+test_expect_success 'path limiting works: foo inside dir' '
+       set_state dir/foo work head &&
+       # the third n is to get out in case it mistakenly does not apply
+       (echo y; echo n; echo n) | (cd dir && git checkout -p foo) &&
+       verify_saved_state bar &&
+       verify_state dir/foo head head
+'
+
+test_expect_success 'none of this moved HEAD' '
+       verify_saved_head
+'
+
+test_done
index 7a3fb67..5514f74 100755 (executable)
@@ -200,4 +200,23 @@ test_expect_success 'drop -q is quiet' '
        test ! -s output.out
 '
 
+test_expect_success 'stash -k' '
+       echo bar3 > file &&
+       echo bar4 > file2 &&
+       git add file2 &&
+       git stash -k &&
+       test bar,bar4 = $(cat file),$(cat file2)
+'
+
+test_expect_success 'stash --invalid-option' '
+       echo bar5 > file &&
+       echo bar6 > file2 &&
+       git add file2 &&
+       test_must_fail git stash --invalid-option &&
+       test_must_fail git stash save --invalid-option &&
+       test bar5,bar6 = $(cat file),$(cat file2) &&
+       git stash -- -message-starting-with-dash &&
+       test bar,bar2 = $(cat file),$(cat file2)
+'
+
 test_done
diff --git a/t/t3904-stash-patch.sh b/t/t3904-stash-patch.sh
new file mode 100755 (executable)
index 0000000..f37e3bc
--- /dev/null
@@ -0,0 +1,55 @@
+#!/bin/sh
+
+test_description='git checkout --patch'
+. ./lib-patch-mode.sh
+
+test_expect_success 'setup' '
+       mkdir dir &&
+       echo parent > dir/foo &&
+       echo dummy > bar &&
+       git add bar dir/foo &&
+       git commit -m initial &&
+       test_tick &&
+       test_commit second dir/foo head &&
+       echo index > dir/foo &&
+       git add dir/foo &&
+       set_and_save_state bar bar_work bar_index &&
+       save_head
+'
+
+# note: bar sorts before dir, so the first 'n' is always to skip 'bar'
+
+test_expect_success 'saying "n" does nothing' '
+       set_state dir/foo work index
+       (echo n; echo n) | test_must_fail git stash save -p &&
+       verify_state dir/foo work index &&
+       verify_saved_state bar
+'
+
+test_expect_success 'git stash -p' '
+       (echo n; echo y) | git stash save -p &&
+       verify_state dir/foo head index &&
+       verify_saved_state bar &&
+       git reset --hard &&
+       git stash apply &&
+       verify_state dir/foo work head &&
+       verify_state bar dummy dummy
+'
+
+test_expect_success 'git stash -p --no-keep-index' '
+       set_state dir/foo work index &&
+       set_state bar bar_work bar_index &&
+       (echo n; echo y) | git stash save -p --no-keep-index &&
+       verify_state dir/foo head head &&
+       verify_state bar bar_work dummy &&
+       git reset --hard &&
+       git stash apply --index &&
+       verify_state dir/foo work index &&
+       verify_state bar dummy bar_index
+'
+
+test_expect_success 'none of this moved HEAD' '
+       verify_saved_head
+'
+
+test_done
diff --git a/t/t7105-reset-patch.sh b/t/t7105-reset-patch.sh
new file mode 100755 (executable)
index 0000000..c1f4fc3
--- /dev/null
@@ -0,0 +1,69 @@
+#!/bin/sh
+
+test_description='git reset --patch'
+. ./lib-patch-mode.sh
+
+test_expect_success 'setup' '
+       mkdir dir &&
+       echo parent > dir/foo &&
+       echo dummy > bar &&
+       git add dir &&
+       git commit -m initial &&
+       test_tick &&
+       test_commit second dir/foo head &&
+       set_and_save_state bar bar_work bar_index &&
+       save_head
+'
+
+# note: bar sorts before foo, so the first 'n' is always to skip 'bar'
+
+test_expect_success 'saying "n" does nothing' '
+       set_and_save_state dir/foo work work
+       (echo n; echo n) | git reset -p &&
+       verify_saved_state dir/foo &&
+       verify_saved_state bar
+'
+
+test_expect_success 'git reset -p' '
+       (echo n; echo y) | git reset -p &&
+       verify_state dir/foo work head &&
+       verify_saved_state bar
+'
+
+test_expect_success 'git reset -p HEAD^' '
+       (echo n; echo y) | git reset -p HEAD^ &&
+       verify_state dir/foo work parent &&
+       verify_saved_state bar
+'
+
+# The idea in the rest is that bar sorts first, so we always say 'y'
+# first and if the path limiter fails it'll apply to bar instead of
+# dir/foo.  There's always an extra 'n' to reject edits to dir/foo in
+# the failure case (and thus get out of the loop).
+
+test_expect_success 'git reset -p dir' '
+       set_state dir/foo work work
+       (echo y; echo n) | git reset -p dir &&
+       verify_state dir/foo work head &&
+       verify_saved_state bar
+'
+
+test_expect_success 'git reset -p -- foo (inside dir)' '
+       set_state dir/foo work work
+       (echo y; echo n) | (cd dir && git reset -p -- foo) &&
+       verify_state dir/foo work head &&
+       verify_saved_state bar
+'
+
+test_expect_success 'git reset -p HEAD^ -- dir' '
+       (echo y; echo n) | git reset -p HEAD^ -- dir &&
+       verify_state dir/foo work parent &&
+       verify_saved_state bar
+'
+
+test_expect_success 'none of this moved HEAD' '
+       verify_saved_head
+'
+
+
+test_done