mirror of
https://github.com/git/git
synced 2024-11-05 18:59:29 +00:00
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: Thomas Rast <trast@student.ethz.ch> Signed-off-by: Junio C Hamano <gitster@pobox.com>
This commit is contained in:
parent
46b5139cae
commit
d002ef4d94
4 changed files with 154 additions and 6 deletions
|
@ -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.
|
||||
|
||||
|
|
|
@ -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,
|
||||
unsigned char *tree_sha1, int refresh_flags)
|
||||
{
|
||||
|
@ -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 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];
|
||||
|
@ -198,6 +210,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()
|
||||
};
|
||||
|
||||
|
@ -251,6 +264,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. */
|
||||
|
|
|
@ -72,6 +72,7 @@ sub colored {
|
|||
|
||||
# command line options
|
||||
my $patch_mode;
|
||||
my $patch_mode_revision;
|
||||
|
||||
sub apply_patch;
|
||||
|
||||
|
@ -85,6 +86,24 @@ sub colored {
|
|||
PARTICIPLE => 'staging',
|
||||
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}};
|
||||
|
@ -206,7 +225,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)) {
|
||||
|
@ -640,6 +666,9 @@ sub run_git_apply {
|
|||
sub parse_diff {
|
||||
my ($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) {
|
||||
|
@ -1391,11 +1420,31 @@ sub help_cmd {
|
|||
sub process_args {
|
||||
return unless @ARGV;
|
||||
my $arg = shift @ARGV;
|
||||
if ($arg eq "--patch") {
|
||||
$patch_mode = 1;
|
||||
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 '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 --"
|
||||
unless $arg eq "--";
|
||||
%patch_mode_flavour = %{$patch_modes{$patch_mode}};
|
||||
}
|
||||
elsif ($arg ne "--") {
|
||||
die "invalid argument $arg, expecting --";
|
||||
|
|
69
t/t7105-reset-patch.sh
Executable file
69
t/t7105-reset-patch.sh
Executable 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
|
Loading…
Reference in a new issue