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'

1  2 
Documentation/git-checkout.txt
Documentation/git-stash.txt
builtin-add.c
builtin-checkout.c
builtin-reset.c
commit.h
git-add--interactive.perl
git-stash.sh

@@@ -11,6 -11,7 +11,7 @@@ SYNOPSI
  '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 +26,7 @@@ use the --track or --no-track options, 
  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
@@@ -45,11 -46,9 +46,11 @@@ file can be discarded to recreate the o
  OPTIONS
  -------
  -q::
 +--quiet::
        Quiet, suppress feedback messages.
  
  -f::
 +--force::
        When switching branches, proceed even if the index or the
        working tree differs from HEAD.  This is used to throw away
        local changes.
@@@ -115,6 -114,16 +116,16 @@@ the conflicted merge in the specified p
        "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
@@@ -13,7 -13,7 +13,7 @@@ SYNOPSI
  '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 +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>]::
  
@@@ -114,8 -126,7 +126,8 @@@ no conflicts
  
  clear::
        Remove all the stashed states. Note that those states will then
 -      be subject to pruning, and may be difficult or impossible to recover.
 +      be subject to pruning, and may be impossible to recover (see
 +      'Examples' below for a possible strategy).
  
  drop [-q|--quiet] [<stash>]::
  
@@@ -218,20 -229,6 +230,20 @@@ $ edit/build/test remaining part
  $ git commit foo -m 'Remaining parts'
  ----------------------------------------------------------------
  
 +Recovering stashes that were cleared/dropped erroneously::
 +
 +If you mistakenly drop or clear stashes, they cannot be recovered
 +through the normal safety mechanisms.  However, you can try the
 +following incantation to get a list of stashes that are still in your
 +repository, but not reachable any more:
 ++
 +----------------------------------------------------------------
 +git fsck --unreachable |
 +grep commit | cut -d\  -f3 |
 +xargs git log --merges --no-walk --grep=WIP
 +----------------------------------------------------------------
 +
 +
  SEE ALSO
  --------
  linkgit:git-checkout[1],
diff --combined builtin-add.c
@@@ -105,8 -105,8 +105,8 @@@ static void refresh(int verbose, const 
        for (specs = 0; pathspec[specs];  specs++)
                /* nothing */;
        seen = xcalloc(specs, 1);
 -      refresh_index(&the_index, verbose ? REFRESH_SAY_CHANGED : REFRESH_QUIET,
 -                    pathspec, seen);
 +      refresh_index(&the_index, verbose ? REFRESH_IN_PORCELAIN : REFRESH_QUIET,
 +                    pathspec, seen, "Unstaged changes after refreshing the index:");
        for (i = 0; i < specs; i++) {
                if (!seen[i])
                        die("pathspec '%s' did not match any files", pathspec[i]);
@@@ -131,27 -131,27 +131,27 @@@ static const char **validate_pathspec(i
        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;
  
        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"));
diff --combined builtin-checkout.c
@@@ -402,9 -402,7 +402,9 @@@ static int merge_working_tree(struct ch
                topts.dir = xcalloc(1, sizeof(*topts.dir));
                topts.dir->flags |= DIR_SHOW_IGNORED;
                topts.dir->exclude_per_dir = ".gitignore";
 -              tree = parse_tree_indirect(old->commit->object.sha1);
 +              tree = parse_tree_indirect(old->commit ?
 +                                         old->commit->object.sha1 :
 +                                         (unsigned char *)EMPTY_TREE_SHA1_BIN);
                init_tree_desc(&trees[0], tree->buffer, tree->size);
                tree = parse_tree_indirect(new->commit->object.sha1);
                init_tree_desc(&trees[1], tree->buffer, tree->size);
@@@ -543,6 -541,14 +543,6 @@@ static int switch_branches(struct check
                parse_commit(new->commit);
        }
  
 -      if (!old.commit && !opts->force) {
 -              if (!opts->quiet) {
 -                      warning("You appear to be on a branch yet to be born.");
 -                      warning("Forcing checkout of %s.", new->name);
 -              }
 -              opts->force = 1;
 -      }
 -
        ret = merge_working_tree(opts, &old, new);
        if (ret)
                return ret;
@@@ -566,6 -572,13 +566,13 @@@ static int git_checkout_config(const ch
        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;
        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"),
                            2),
                OPT_SET_INT('3', "theirs", &opts.writeout_stage, "stage",
                            3),
 -              OPT_BOOLEAN('f', NULL, &opts.force, "force"),
 +              OPT_BOOLEAN('f', "force", &opts.force, "force"),
                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;
        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 -727,9 +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) {
                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))
diff --combined builtin-reset.c
@@@ -108,8 -108,7 +108,8 @@@ static int update_index_refresh(int fd
        if (read_cache() < 0)
                return error("Could not read index");
  
 -      result = refresh_cache(flags) ? 1 : 0;
 +      result = refresh_index(&the_index, (flags), NULL, NULL,
 +                             "Unstaged changes after reset:") ? 1 : 0;
        if (write_cache(fd, active_cache, active_nr) ||
                        commit_locked_index(index_lock))
                return error ("Could not refresh index");
@@@ -143,6 -142,17 +143,17 @@@ static void update_index_from_diff(stru
        }
  }
  
+ 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 -194,7 +195,7 @@@ static void prepend_reflog_action(cons
  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];
                                "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()
        };
  
                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. */
                        die("Cannot do %s reset with paths.",
                                        reset_type_names[reset_type]);
                return read_from_tree(prefix, argv + i, sha1,
 -                              quiet ? REFRESH_QUIET : REFRESH_SAY_CHANGED);
 +                              quiet ? REFRESH_QUIET : REFRESH_IN_PORCELAIN);
        }
        if (reset_type == NONE)
                reset_type = MIXED; /* by default */
                break;
        case MIXED: /* Report what has not been updated. */
                update_index_refresh(0, NULL,
 -                              quiet ? REFRESH_QUIET : REFRESH_SAY_CHANGED);
 +                              quiet ? REFRESH_QUIET : REFRESH_IN_PORCELAIN);
                break;
        }
  
diff --combined commit.h
+++ b/commit.h
@@@ -64,7 -64,6 +64,7 @@@ enum cmit_fmt 
  };
  
  extern int non_ascii(int);
 +extern int has_non_ascii(const char *text);
  struct rev_info; /* in revision.h, it circularly uses enum cmit_fmt */
  extern char *reencode_commit_message(const struct commit *commit,
                                     const char **encoding_p);
@@@ -123,8 -122,6 +123,8 @@@ struct commit_graft *read_graft_line(ch
  int register_commit_graft(struct commit_graft *, int);
  struct commit_graft *lookup_commit_graft(const unsigned char *sha1);
  
 +const unsigned char *lookup_replace_object(const unsigned char *sha1);
 +
  extern struct commit_list *get_merge_bases(struct commit *rev1, struct commit *rev2, int cleanup);
  extern struct commit_list *get_merge_bases_many(struct commit *one, int n, struct commit **twos, int cleanup);
  extern struct commit_list *get_octopus_merge_bases(struct commit_list *in);
@@@ -140,6 -137,8 +140,8 @@@ int is_descendant_of(struct commit *, s
  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)
  {
@@@ -72,6 -72,79 +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 +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 +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' };
  
@@@ -841,10 -933,6 +933,10 @@@ sub coalesce_overlapping_hunks 
        my ($last_o_ctx, $last_was_dirty);
  
        for (grep { $_->{USE} } @in) {
 +              if ($_->{TYPE} ne 'hunk') {
 +                      push @out, $_;
 +                      next;
 +              }
                my $text = $_->{TEXT};
                my ($o_ofs) = parse_hunk_header($text->[0]);
                if (defined $last_o_ctx &&
@@@ -881,6 -969,7 +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).
  # 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
  
  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 -1078,14 +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 -1098,40 +1102,40 @@@ e - manually edit the current hun
  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 -1262,9 +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) {
  
        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();
        }
  
  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 --";
diff --combined git-stash.sh
@@@ -7,7 -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 +21,14 @@@ trap 'rm -f "$TMP-*"' 
  
  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 +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
  
  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
                        ;;
                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
  }
  
@@@ -162,6 -222,10 +222,6 @@@ show_stash () 
  }
  
  apply_stash () {
 -      git update-index -q --refresh &&
 -      git diff-files --quiet --ignore-submodules ||
 -              die 'Cannot apply to a dirty working tree, please stage your changes'
 -
        unstash_index=
  
        while test $# != 0
                shift
        done
  
 -      # current index state
 -      c_tree=$(git write-tree) ||
 -              die 'Cannot apply a stash in the middle of a merge'
 +      if test $# = 0
 +      then
 +              have_stash || die 'Nothing to apply'
 +      fi
  
        # stash records the work tree, and is a merge between the
        # base commit (first parent) and the index tree (second parent).
 -      s=$(git rev-parse --verify --default $ref_stash "$@") &&
 -      w_tree=$(git rev-parse --verify "$s:") &&
 -      b_tree=$(git rev-parse --verify "$s^1:") &&
 -      i_tree=$(git rev-parse --verify "$s^2:") ||
 +      s=$(git rev-parse --quiet --verify --default $ref_stash "$@") &&
 +      w_tree=$(git rev-parse --quiet --verify "$s:") &&
 +      b_tree=$(git rev-parse --quiet --verify "$s^1:") &&
 +      i_tree=$(git rev-parse --quiet --verify "$s^2:") ||
                die "$*: no valid stashed state found"
  
 +      git update-index -q --refresh &&
 +      git diff-files --quiet --ignore-submodules ||
 +              die 'Cannot apply to a dirty working tree, please stage your changes'
 +
 +      # current index state
 +      c_tree=$(git write-tree) ||
 +              die 'Cannot apply a stash in the middle of a merge'
 +
        unstashed_index_tree=
        if test -n "$unstash_index" && test "$b_tree" != "$i_tree" &&
                        test "$c_tree" != "$i_tree"
@@@ -307,6 -362,18 +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 -425,13 +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