1
0
mirror of https://github.com/git/git synced 2024-07-07 19:39:27 +00:00

Merge branch 'tr/reset-checkout-patch'

* 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'
This commit is contained in:
Junio C Hamano 2009-09-07 15:24:38 -07:00
commit 54f0bdc811
14 changed files with 677 additions and 76 deletions

View 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

View 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.

View 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>]::

View File

@ -131,10 +131,37 @@ static const char **validate_pathspec(int argc, const char **argv, const char *p
return pathspec;
}
int run_add_interactive(const char *revision, const char *patch_mode,
const char **pathspec)
{
int status, ac, pc = 0;
const char **args;
if (pathspec)
while (pathspec[pc])
pc++;
args = xcalloc(sizeof(const char *), (pc + 5));
ac = 0;
args[ac++] = "add--interactive";
if (patch_mode)
args[ac++] = patch_mode;
if (revision)
args[ac++] = revision;
args[ac++] = "--";
if (pc) {
memcpy(&(args[ac]), pathspec, sizeof(const char *) * pc);
ac += pc;
}
args[ac] = NULL;
status = run_command_v_opt(args, RUN_GIT_CMD);
free(args);
return status;
}
int interactive_add(int argc, const char **argv, const char *prefix)
{
int status, ac;
const char **args;
const char **pathspec = NULL;
if (argc) {
@ -143,21 +170,9 @@ int interactive_add(int argc, const char **argv, const char *prefix)
return -1;
}
args = xcalloc(sizeof(const char *), (argc + 4));
ac = 0;
args[ac++] = "add--interactive";
if (patch_interactive)
args[ac++] = "--patch";
args[ac++] = "--";
if (argc) {
memcpy(&(args[ac]), pathspec, sizeof(const char *) * argc);
ac += argc;
}
args[ac] = NULL;
status = run_command_v_opt(args, RUN_GIT_CMD);
free(args);
return status;
return run_add_interactive(NULL,
patch_interactive ? "--patch" : NULL,
pathspec);
}
static int edit_patch(int argc, const char **argv, const char *prefix)

View 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 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
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 @@ int cmd_checkout(int argc, const char **argv, const char *prefix)
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))

View 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. */

View File

@ -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)
{

View File

@ -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 @@ sub edit_hunk_manually {
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 @@ sub help_patch_cmd {
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 @@ sub help_cmd {
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 --";

View File

@ -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

41
t/lib-patch-mode.sh Executable file
View File

@ -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)"
}

107
t/t2016-checkout-patch.sh Executable file
View File

@ -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

View File

@ -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

55
t/t3904-stash-patch.sh Executable file
View File

@ -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

69
t/t7105-reset-patch.sh Executable file
View File

@ -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