From 8f0bef6df91c48d79f982bdb55f784ce445ba5b2 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Thu, 13 Aug 2009 14:29:39 +0200 Subject: git-apply--interactive: Refactor patch mode code This makes some aspects of the 'git add -p' loop configurable (within the code), so that we can later reuse git-add--interactive for other similar tools. Most fields are fairly straightforward, but APPLY gets a subroutine (instead of just a string a la 'apply --cached') so that we can handle 'checkout -p', which will need to atomically apply the patch twice (index and worktree). Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano --- git-add--interactive.perl | 82 +++++++++++++++++++++++++++++++---------------- 1 file changed, 55 insertions(+), 27 deletions(-) (limited to 'git-add--interactive.perl') diff --git a/git-add--interactive.perl b/git-add--interactive.perl index df9f231635..360610314e 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -73,6 +73,22 @@ sub colored { # command line options my $patch_mode; +sub apply_patch; + +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', + }, +); + +my %patch_mode_flavour = %{$patch_modes{stage}}; + sub run_cmd_pipe { if ($^O eq 'MSWin32' || $^O eq 'msys') { my @invalid = grep {m/[":*]/} @_; @@ -613,12 +629,21 @@ 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}); + 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' }; @@ -877,6 +902,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 <{TEXT}}; - } - return close $fh; + return run_git_apply($patch_mode_flavour{APPLY_CHECK} . ' --recount --check', + map { @{$_->{TEXT}} } @_); } sub _restore_terminal_and_die { @@ -988,12 +1011,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, <{BINARY}) } @all_mods; my @them; @@ -1138,8 +1172,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) { @@ -1313,16 +1348,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(); } -- cgit v1.2.1 From d002ef4d9446b9fe4d0c397131edce58781df2f1 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Sat, 15 Aug 2009 13:48:31 +0200 Subject: 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 -- [files...] which offers hunks from the diff between the index and for forward application to the index. (That is, the basic case is just = HEAD.) Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano --- git-add--interactive.perl | 57 +++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 4 deletions(-) (limited to 'git-add--interactive.perl') diff --git a/git-add--interactive.perl b/git-add--interactive.perl index 360610314e..d14f48c837 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -72,6 +72,7 @@ sub colored { # command line options my $patch_mode; +my $patch_mode_revision; sub apply_patch; @@ -85,6 +86,24 @@ my %patch_modes = ( 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 @@ 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 '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 --"; -- cgit v1.2.1 From 4f353658b9c15e9188530fac5ae79d0aa1538e85 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Sat, 15 Aug 2009 13:48:30 +0200 Subject: Implement 'git checkout --patch' This introduces a --patch mode for git-checkout. In the index usage git checkout --patch -- [files...] it lets the user discard edits from the at the granularity of hunks (by selecting hunks from 'git diff' and then reverse applying them to the worktree). We also accept a revision argument. In the case git checkout --patch HEAD -- [files...] we offer hunks from the difference between HEAD and the worktree, and reverse applies them to both index and worktree, allowing you to discard staged changes completely. In the non-HEAD usage git checkout --patch -- [files...] it offers hunks from the difference between the worktree and . The chosen hunks are then applied to both index and worktree. The application to worktree and index is done "atomically" in the sense that we first check if the patch applies to the index (it should always apply to the worktree). If it does not, we give the user a choice to either abort or apply to the worktree anyway. Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano --- git-add--interactive.perl | 61 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) (limited to 'git-add--interactive.perl') diff --git a/git-add--interactive.perl b/git-add--interactive.perl index d14f48c837..21746d5f28 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -75,6 +75,7 @@ my $patch_mode; my $patch_mode_revision; sub apply_patch; +sub apply_patch_for_checkout_commit; my %patch_modes = ( 'stage' => { @@ -104,6 +105,33 @@ my %patch_modes = ( 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}}; @@ -1069,6 +1097,29 @@ sub apply_patch { 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($patch_mode_flavour{FILTER}); my @mods = grep { !($_->{BINARY}) } @all_mods; @@ -1432,6 +1483,16 @@ sub process_args { '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') { $patch_mode = 'stage'; $arg = shift @ARGV or die "missing --"; -- cgit v1.2.1 From dda1f2a5c3aca5072aada32eef159067ba16f0e9 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Thu, 13 Aug 2009 14:29:44 +0200 Subject: Implement 'git stash save --patch' This adds a hunk-based mode to git-stash. You can select hunks from the difference between HEAD and worktree, and git-stash will build a stash that reflects these changes. The index state of the stash is the same as your current index, and we also let --patch imply --keep-index. Note that because the selected hunks are rolled back from the worktree but not the index, the resulting state may appear somewhat confusing if you had also staged these changes. This is not entirely satisfactory, but due to the way stashes are applied, other solutions would require a change to the stash format. Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano --- git-add--interactive.perl | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) (limited to 'git-add--interactive.perl') diff --git a/git-add--interactive.perl b/git-add--interactive.perl index 21746d5f28..8f66b825dd 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -76,6 +76,7 @@ my $patch_mode_revision; sub apply_patch; sub apply_patch_for_checkout_commit; +sub apply_patch_for_stash; my %patch_modes = ( 'stage' => { @@ -87,6 +88,15 @@ my %patch_modes = ( 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', @_; }, @@ -1493,8 +1503,8 @@ sub process_args { 'checkout_head' : 'checkout_nothead'); $arg = shift @ARGV or die "missing --"; } - } elsif ($1 eq 'stage') { - $patch_mode = 'stage'; + } elsif ($1 eq 'stage' or $1 eq 'stash') { + $patch_mode = $1; $arg = shift @ARGV or die "missing --"; } else { die "unknown --patch mode: $1"; -- cgit v1.2.1