提交 d002ef4d 编写于 作者: T Thomas Rast 提交者: Junio C Hamano

Implement 'git reset --patch'

This introduces a --patch mode for git-reset.  The basic case is

  git reset --patch -- [files...]

which acts as the opposite of 'git add --patch -- [files...]': it
offers hunks for *un*staging.  Advanced usage is

  git reset --patch <revision> -- [files...]

which offers hunks from the diff between the index and <revision> for
forward application to the index.  (That is, the basic case is just
<revision> = HEAD.)
Signed-off-by: NThomas Rast <trast@student.ethz.ch>
Signed-off-by: NJunio C Hamano <gitster@pobox.com>
上级 46b5139c
...@@ -10,6 +10,7 @@ SYNOPSIS ...@@ -10,6 +10,7 @@ SYNOPSIS
[verse] [verse]
'git reset' [--mixed | --soft | --hard | --merge] [-q] [<commit>] 'git reset' [--mixed | --soft | --hard | --merge] [-q] [<commit>]
'git reset' [-q] [<commit>] [--] <paths>... 'git reset' [-q] [<commit>] [--] <paths>...
'git reset' --patch [<commit>] [--] [<paths>...]
DESCRIPTION DESCRIPTION
----------- -----------
...@@ -23,8 +24,9 @@ the undo in the history. ...@@ -23,8 +24,9 @@ the undo in the history.
If you want to undo a commit other than the latest on a branch, If you want to undo a commit other than the latest on a branch,
linkgit:git-revert[1] is your friend. linkgit:git-revert[1] is your friend.
The second form with 'paths' is used to revert selected paths in The second and third forms with 'paths' and/or --patch are used to
the index from a given commit, without moving HEAD. revert selected paths in the index from a given commit, without moving
HEAD.
OPTIONS OPTIONS
...@@ -50,6 +52,15 @@ OPTIONS ...@@ -50,6 +52,15 @@ OPTIONS
and updates the files that are different between the named commit and updates the files that are different between the named commit
and the current commit in the working tree. 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:: -q::
Be quiet, only report errors. Be quiet, only report errors.
......
...@@ -142,6 +142,17 @@ static void update_index_from_diff(struct diff_queue_struct *q, ...@@ -142,6 +142,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, static int read_from_tree(const char *prefix, const char **argv,
unsigned char *tree_sha1, int refresh_flags) unsigned char *tree_sha1, int refresh_flags)
{ {
...@@ -183,6 +194,7 @@ static void prepend_reflog_action(const char *action, char *buf, size_t size) ...@@ -183,6 +194,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 cmd_reset(int argc, const char **argv, const char *prefix)
{ {
int i = 0, reset_type = NONE, update_ref_status = 0, quiet = 0; int i = 0, reset_type = NONE, update_ref_status = 0, quiet = 0;
int patch_mode = 0;
const char *rev = "HEAD"; const char *rev = "HEAD";
unsigned char sha1[20], *orig = NULL, sha1_orig[20], unsigned char sha1[20], *orig = NULL, sha1_orig[20],
*old_orig = NULL, sha1_old_orig[20]; *old_orig = NULL, sha1_old_orig[20];
...@@ -198,6 +210,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix) ...@@ -198,6 +210,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
"reset HEAD, index and working tree", MERGE), "reset HEAD, index and working tree", MERGE),
OPT_BOOLEAN('q', NULL, &quiet, OPT_BOOLEAN('q', NULL, &quiet,
"disable showing new HEAD in hard reset and progress message"), "disable showing new HEAD in hard reset and progress message"),
OPT_BOOLEAN('p', "patch", &patch_mode, "select hunks interactively"),
OPT_END() OPT_END()
}; };
...@@ -251,6 +264,12 @@ int cmd_reset(int argc, const char **argv, const char *prefix) ...@@ -251,6 +264,12 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
die("Could not parse object '%s'.", rev); die("Could not parse object '%s'.", rev);
hashcpy(sha1, commit->object.sha1); 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 /* git reset tree [--] paths... can be used to
* load chosen paths from the tree into the index without * load chosen paths from the tree into the index without
* affecting the working tree nor HEAD. */ * affecting the working tree nor HEAD. */
......
...@@ -72,6 +72,7 @@ sub colored { ...@@ -72,6 +72,7 @@ sub colored {
# command line options # command line options
my $patch_mode; my $patch_mode;
my $patch_mode_revision;
sub apply_patch; sub apply_patch;
...@@ -85,6 +86,24 @@ sub colored { ...@@ -85,6 +86,24 @@ sub colored {
PARTICIPLE => 'staging', PARTICIPLE => 'staging',
FILTER => 'file-only', FILTER => 'file-only',
}, },
'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',
},
); );
my %patch_mode_flavour = %{$patch_modes{stage}}; my %patch_mode_flavour = %{$patch_modes{stage}};
...@@ -206,7 +225,14 @@ sub list_modified { ...@@ -206,7 +225,14 @@ sub list_modified {
return if (!@tracked); 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 for (run_cmd_pipe(qw(git diff-index --cached
--numstat --summary), $reference, --numstat --summary), $reference,
'--', @tracked)) { '--', @tracked)) {
...@@ -640,6 +666,9 @@ sub run_git_apply { ...@@ -640,6 +666,9 @@ sub run_git_apply {
sub parse_diff { sub parse_diff {
my ($path) = @_; my ($path) = @_;
my @diff_cmd = split(" ", $patch_mode_flavour{DIFF}); 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 @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
my @colored = (); my @colored = ();
if ($diff_use_color) { if ($diff_use_color) {
...@@ -1391,11 +1420,31 @@ sub help_cmd { ...@@ -1391,11 +1420,31 @@ sub help_cmd {
sub process_args { sub process_args {
return unless @ARGV; return unless @ARGV;
my $arg = shift @ARGV; my $arg = shift @ARGV;
if ($arg eq "--patch") { if ($arg =~ /--patch(?:=(.*))?/) {
$patch_mode = 1; if (defined $1) {
if ($1 eq 'reset') {
$patch_mode = 'reset_head';
$patch_mode_revision = 'HEAD';
$arg = shift @ARGV or die "missing --"; $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 'stage') {
$patch_mode = 'stage';
$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 --" die "invalid argument $arg, expecting --"
unless $arg eq "--"; unless $arg eq "--";
%patch_mode_flavour = %{$patch_modes{$patch_mode}};
} }
elsif ($arg ne "--") { elsif ($arg ne "--") {
die "invalid argument $arg, expecting --"; die "invalid argument $arg, expecting --";
......
#!/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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册