@matheusbsbs / matheus-bagliano / commits / d002ef4d94
Fork 0

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>

Thomas Rast committed Aug 15, 2009 at 13:48 UTC d002ef4d9446b9fe4d0c397131edce58781df2f1
4 files changed +154 -6
Documentation/git-reset.txt
+13 -2
@@ -10,6 +10,7 @@ SYNOPSIS
10 [verse]
11 'git reset' [--mixed | --soft | --hard | --merge] [-q] [<commit>]
12 'git reset' [-q] [<commit>] [--] <paths>...
13 +'git reset' --patch [<commit>] [--] [<paths>...]
14
15 DESCRIPTION
16 -----------
@@ -23,8 +24,9 @@ the undo in the history.
24 If you want to undo a commit other than the latest on a branch,
25 linkgit:git-revert[1] is your friend.
26
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.
30
31
32 OPTIONS
@@ -50,6 +52,15 @@ OPTIONS
52 and updates the files that are different between the named commit
53 and the current commit in the working tree.
54
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 +
64 -q::
65 Be quiet, only report errors.
66
builtin-reset.c
+19
@@ -142,6 +142,17 @@ static void update_index_from_diff(struct diff_queue_struct *q,
142 }
143 }
144
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 +
156 static int read_from_tree(const char *prefix, const char **argv,
157 unsigned char *tree_sha1, int refresh_flags)
158 {
@@ -183,6 +194,7 @@ static void prepend_reflog_action(const char *action, char *buf, size_t size)
194 int cmd_reset(int argc, const char **argv, const char *prefix)
195 {
196 int i = 0, reset_type = NONE, update_ref_status = 0, quiet = 0;
197 + int patch_mode = 0;
198 const char *rev = "HEAD";
199 unsigned char sha1[20], *orig = NULL, sha1_orig[20],
200 *old_orig = NULL, sha1_old_orig[20];
@@ -198,6 +210,7 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
210 "reset HEAD, index and working tree", MERGE),
211 OPT_BOOLEAN('q', NULL, &quiet,
212 "disable showing new HEAD in hard reset and progress message"),
213 + OPT_BOOLEAN('p', "patch", &patch_mode, "select hunks interactively"),
214 OPT_END()
215 };
216
@@ -251,6 +264,12 @@ int cmd_reset(int argc, const char **argv, const char *prefix)
264 die("Could not parse object '%s'.", rev);
265 hashcpy(sha1, commit->object.sha1);
266
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 +
273 /* git reset tree [--] paths... can be used to
274 * load chosen paths from the tree into the index without
275 * affecting the working tree nor HEAD. */
git-add--interactive.perl
+53 -4
@@ -72,6 +72,7 @@ sub colored {
72
73 # command line options
74 my $patch_mode;
75 +my $patch_mode_revision;
76
77 sub apply_patch;
78
@@ -85,6 +86,24 @@ my %patch_modes = (
86 PARTICIPLE => 'staging',
87 FILTER => 'file-only',
88 },
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 + },
107 );
108
109 my %patch_mode_flavour = %{$patch_modes{stage}};
@@ -206,7 +225,14 @@ sub list_modified {
225 return if (!@tracked);
226 }
227
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 + }
236 for (run_cmd_pipe(qw(git diff-index --cached
237 --numstat --summary), $reference,
238 '--', @tracked)) {
@@ -640,6 +666,9 @@ sub run_git_apply {
666 sub parse_diff {
667 my ($path) = @_;
668 my @diff_cmd = split(" ", $patch_mode_flavour{DIFF});
669 + if (defined $patch_mode_revision) {
670 + push @diff_cmd, $patch_mode_revision;
671 + }
672 my @diff = run_cmd_pipe("git", @diff_cmd, "--", $path);
673 my @colored = ();
674 if ($diff_use_color) {
@@ -1391,11 +1420,31 @@ EOF
1420 sub process_args {
1421 return unless @ARGV;
1422 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 + }
1445 die "invalid argument $arg, expecting --"
1446 unless $arg eq "--";
1447 + %patch_mode_flavour = %{$patch_modes{$patch_mode}};
1448 }
1449 elsif ($arg ne "--") {
1450 die "invalid argument $arg, expecting --";
t/t7105-reset-patch.sh new
+69
@@ -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