Skip to content

Commit d002ef4

Browse files
trastgitster
authored andcommitted
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 <[email protected]> Signed-off-by: Junio C Hamano <[email protected]>
1 parent 46b5139 commit d002ef4

File tree

4 files changed

+154
-6
lines changed

4 files changed

+154
-6
lines changed

Documentation/git-reset.txt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ SYNOPSIS
1010
[verse]
1111
'git reset' [--mixed | --soft | --hard | --merge] [-q] [<commit>]
1212
'git reset' [-q] [<commit>] [--] <paths>...
13+
'git reset' --patch [<commit>] [--] [<paths>...]
1314

1415
DESCRIPTION
1516
-----------
@@ -23,8 +24,9 @@ the undo in the history.
2324
If you want to undo a commit other than the latest on a branch,
2425
linkgit:git-revert[1] is your friend.
2526

26-
The second form with 'paths' is used to revert selected paths in
27-
the index from a given commit, without moving HEAD.
27+
The second and third forms with 'paths' and/or --patch are used to
28+
revert selected paths in the index from a given commit, without moving
29+
HEAD.
2830

2931

3032
OPTIONS
@@ -50,6 +52,15 @@ OPTIONS
5052
and updates the files that are different between the named commit
5153
and the current commit in the working tree.
5254

55+
-p::
56+
--patch::
57+
Interactively select hunks in the difference between the index
58+
and <commit> (defaults to HEAD). The chosen hunks are applied
59+
in reverse to the index.
60+
+
61+
This means that `git reset -p` is the opposite of `git add -p` (see
62+
linkgit:git-add[1]).
63+
5364
-q::
5465
Be quiet, only report errors.
5566

builtin-reset.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,17 @@ static void update_index_from_diff(struct diff_queue_struct *q,
142142
}
143143
}
144144

145+
static int interactive_reset(const char *revision, const char **argv,
146+
const char *prefix)
147+
{
148+
const char **pathspec = NULL;
149+
150+
if (*argv)
151+
pathspec = get_pathspec(prefix, argv);
152+
153+
return run_add_interactive(revision, "--patch=reset", pathspec);
154+
}
155+
145156
static int read_from_tree(const char *prefix, const char **argv,
146157
unsigned char *tree_sha1, int refresh_flags)
147158
{
@@ -183,6 +194,7 @@ static void prepend_reflog_action(const char *action, char *buf, size_t size)
183194
int cmd_reset(int argc, const char **argv, const char *prefix)
184195
{
185196
int i = 0, reset_type = NONE, update_ref_status = 0, quiet = 0;
197+
int patch_mode = 0;
186198
const char *rev = "HEAD";
187199
unsigned char sha1[20], *orig = NULL, sha1_orig[20],
188200
*old_orig = NULL, sha1_old_orig[20];
@@ -198,6 +210,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
198210
"reset HEAD, index and working tree", MERGE),
199211
OPT_BOOLEAN('q', NULL, &quiet,
200212
"disable showing new HEAD in hard reset and progress message"),
213+
OPT_BOOLEAN('p', "patch", &patch_mode, "select hunks interactively"),
201214
OPT_END()
202215
};
203216

@@ -251,6 +264,12 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
251264
die("Could not parse object '%s'.", rev);
252265
hashcpy(sha1, commit->object.sha1);
253266

267+
if (patch_mode) {
268+
if (reset_type != NONE)
269+
die("--patch is incompatible with --{hard,mixed,soft}");
270+
return interactive_reset(rev, argv + i, prefix);
271+
}
272+
254273
/* git reset tree [--] paths... can be used to
255274
* load chosen paths from the tree into the index without
256275
* affecting the working tree nor HEAD. */

git-add--interactive.perl

Lines changed: 53 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ sub colored {
7272

7373
# command line options
7474
my $patch_mode;
75+
my $patch_mode_revision;
7576

7677
sub apply_patch;
7778

@@ -85,6 +86,24 @@ sub colored {
8586
PARTICIPLE => 'staging',
8687
FILTER => 'file-only',
8788
},
89+
'reset_head' => {
90+
DIFF => 'diff-index -p --cached',
91+
APPLY => sub { apply_patch 'apply -R --cached', @_; },
92+
APPLY_CHECK => 'apply -R --cached',
93+
VERB => 'Unstage',
94+
TARGET => '',
95+
PARTICIPLE => 'unstaging',
96+
FILTER => 'index-only',
97+
},
98+
'reset_nothead' => {
99+
DIFF => 'diff-index -R -p --cached',
100+
APPLY => sub { apply_patch 'apply --cached', @_; },
101+
APPLY_CHECK => 'apply --cached',
102+
VERB => 'Apply',
103+
TARGET => ' to index',
104+
PARTICIPLE => 'applying',
105+
FILTER => 'index-only',
106+
},
88107
);
89108

90109
my %patch_mode_flavour = %{$patch_modes{stage}};
@@ -206,7 +225,14 @@ sub list_modified {
206225
return if (!@tracked);
207226
}
208227

209-
my $reference = is_initial_commit() ? get_empty_tree() : 'HEAD';
228+
my $reference;
229+
if (defined $patch_mode_revision and $patch_mode_revision ne 'HEAD') {
230+
$reference = $patch_mode_revision;
231+
} elsif (is_initial_commit()) {
232+
$reference = get_empty_tree();
233+
} else {
234+
$reference = 'HEAD';
235+
}
210236
for (run_cmd_pipe(qw(git diff-index --cached
211237
--numstat --summary), $reference,
212238
'--', @tracked)) {
@@ -640,6 +666,9 @@ sub run_git_apply {
640666
sub parse_diff {
641667
my ($path) = @_;
642668
my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
669+
if (defined $patch_mode_revision) {
670+
push @diff_cmd, $patch_mode_revision;
671+
}
643672
my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
644673
my @colored = ();
645674
if ($diff_use_color) {
@@ -1391,11 +1420,31 @@ sub help_cmd {
13911420
sub process_args {
13921421
return unless @ARGV;
13931422
my $arg = shift @ARGV;
1394-
if ($arg eq "--patch") {
1395-
$patch_mode = 1;
1396-
$arg = shift @ARGV or die "missing --";
1423+
if ($arg =~ /--patch(?:=(.*))?/) {
1424+
if (defined $1) {
1425+
if ($1 eq 'reset') {
1426+
$patch_mode = 'reset_head';
1427+
$patch_mode_revision = 'HEAD';
1428+
$arg = shift @ARGV or die "missing --";
1429+
if ($arg ne '--') {
1430+
$patch_mode_revision = $arg;
1431+
$patch_mode = ($arg eq 'HEAD' ?
1432+
'reset_head' : 'reset_nothead');
1433+
$arg = shift @ARGV or die "missing --";
1434+
}
1435+
} elsif ($1 eq 'stage') {
1436+
$patch_mode = 'stage';
1437+
$arg = shift @ARGV or die "missing --";
1438+
} else {
1439+
die "unknown --patch mode: $1";
1440+
}
1441+
} else {
1442+
$patch_mode = 'stage';
1443+
$arg = shift @ARGV or die "missing --";
1444+
}
13971445
die "invalid argument $arg, expecting --"
13981446
unless $arg eq "--";
1447+
%patch_mode_flavour = %{$patch_modes{$patch_mode}};
13991448
}
14001449
elsif ($arg ne "--") {
14011450
die "invalid argument $arg, expecting --";

t/t7105-reset-patch.sh

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#!/bin/sh
2+
3+
test_description='git reset --patch'
4+
. ./lib-patch-mode.sh
5+
6+
test_expect_success 'setup' '
7+
mkdir dir &&
8+
echo parent > dir/foo &&
9+
echo dummy > bar &&
10+
git add dir &&
11+
git commit -m initial &&
12+
test_tick &&
13+
test_commit second dir/foo head &&
14+
set_and_save_state bar bar_work bar_index &&
15+
save_head
16+
'
17+
18+
# note: bar sorts before foo, so the first 'n' is always to skip 'bar'
19+
20+
test_expect_success 'saying "n" does nothing' '
21+
set_and_save_state dir/foo work work
22+
(echo n; echo n) | git reset -p &&
23+
verify_saved_state dir/foo &&
24+
verify_saved_state bar
25+
'
26+
27+
test_expect_success 'git reset -p' '
28+
(echo n; echo y) | git reset -p &&
29+
verify_state dir/foo work head &&
30+
verify_saved_state bar
31+
'
32+
33+
test_expect_success 'git reset -p HEAD^' '
34+
(echo n; echo y) | git reset -p HEAD^ &&
35+
verify_state dir/foo work parent &&
36+
verify_saved_state bar
37+
'
38+
39+
# The idea in the rest is that bar sorts first, so we always say 'y'
40+
# first and if the path limiter fails it'll apply to bar instead of
41+
# dir/foo. There's always an extra 'n' to reject edits to dir/foo in
42+
# the failure case (and thus get out of the loop).
43+
44+
test_expect_success 'git reset -p dir' '
45+
set_state dir/foo work work
46+
(echo y; echo n) | git reset -p dir &&
47+
verify_state dir/foo work head &&
48+
verify_saved_state bar
49+
'
50+
51+
test_expect_success 'git reset -p -- foo (inside dir)' '
52+
set_state dir/foo work work
53+
(echo y; echo n) | (cd dir && git reset -p -- foo) &&
54+
verify_state dir/foo work head &&
55+
verify_saved_state bar
56+
'
57+
58+
test_expect_success 'git reset -p HEAD^ -- dir' '
59+
(echo y; echo n) | git reset -p HEAD^ -- dir &&
60+
verify_state dir/foo work parent &&
61+
verify_saved_state bar
62+
'
63+
64+
test_expect_success 'none of this moved HEAD' '
65+
verify_saved_head
66+
'
67+
68+
69+
test_done

0 commit comments

Comments
 (0)