зеркало из https://github.com/microsoft/git.git
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:
Коммит
54f0bdc811
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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>]::
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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. */
|
||||
|
|
2
commit.h
2
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)
|
||||
{
|
||||
|
|
|
@ -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 --";
|
||||
|
|
113
git-stash.sh
113
git-stash.sh
|
@ -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
|
||||
|
|
|
@ -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)"
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
Загрузка…
Ссылка в новой задаче