From 8cbd4310821ea2957d32a36c34fa314cf99ca9f3 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Wed, 2 Jul 2008 23:59:16 +0200 Subject: [PATCH 1/3] git-add--interactive: replace hunk recounting with apply --recount We recounted the postimage offsets to compensate for hunks that were not selected. Now apply --recount can do the job for us. Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano --- git-add--interactive.perl | 30 +++--------------------------- 1 file changed, 3 insertions(+), 27 deletions(-) diff --git a/git-add--interactive.perl b/git-add--interactive.perl index 903953e68e..e1964a588e 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -970,39 +970,15 @@ sub patch_update_file { push @result, @{$mode->{TEXT}}; } for (@hunk) { - my $text = $_->{TEXT}; - my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = - parse_hunk_header($text->[0]); - - if (!$_->{USE}) { - # We would have added ($n_cnt - $o_cnt) lines - # to the postimage if we were to use this hunk, - # but we didn't. So the line number that the next - # hunk starts at would be shifted by that much. - $n_lofs -= ($n_cnt - $o_cnt); - next; - } - else { - if ($n_lofs) { - $n_ofs += $n_lofs; - $text->[0] = ("@@ -$o_ofs" . - (($o_cnt != 1) - ? ",$o_cnt" : '') . - " +$n_ofs" . - (($n_cnt != 1) - ? ",$n_cnt" : '') . - " @@\n"); - } - for (@$text) { - push @result, $_; - } + if ($_->{USE}) { + push @result, @{$_->{TEXT}}; } } if (@result) { my $fh; - open $fh, '| git apply --cached'; + open $fh, '| git apply --cached --recount'; for (@{$head->{TEXT}}, @result) { print $fh $_; } From 0beee4c6dec15292415e3d56075c16a76a22af54 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Wed, 2 Jul 2008 23:59:44 +0200 Subject: [PATCH 2/3] git-add--interactive: remove hunk coalescing Current git-apply has no trouble at all applying chunks that have overlapping context, as produced by the splitting feature. So we can drop the manual coalescing. Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano --- git-add--interactive.perl | 89 --------------------------------------- 1 file changed, 89 deletions(-) diff --git a/git-add--interactive.perl b/git-add--interactive.perl index e1964a588e..a4234d39e9 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -682,93 +682,6 @@ sub split_hunk { return @split; } -sub find_last_o_ctx { - my ($it) = @_; - my $text = $it->{TEXT}; - my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]); - my $i = @{$text}; - my $last_o_ctx = $o_ofs + $o_cnt; - while (0 < --$i) { - my $line = $text->[$i]; - if ($line =~ /^ /) { - $last_o_ctx--; - next; - } - last; - } - return $last_o_ctx; -} - -sub merge_hunk { - my ($prev, $this) = @_; - my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) = - parse_hunk_header($prev->{TEXT}[0]); - my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) = - parse_hunk_header($this->{TEXT}[0]); - - my (@line, $i, $ofs, $o_cnt, $n_cnt); - $ofs = $o0_ofs; - $o_cnt = $n_cnt = 0; - for ($i = 1; $i < @{$prev->{TEXT}}; $i++) { - my $line = $prev->{TEXT}[$i]; - if ($line =~ /^\+/) { - $n_cnt++; - push @line, $line; - next; - } - - last if ($o1_ofs <= $ofs); - - $o_cnt++; - $ofs++; - if ($line =~ /^ /) { - $n_cnt++; - } - push @line, $line; - } - - for ($i = 1; $i < @{$this->{TEXT}}; $i++) { - my $line = $this->{TEXT}[$i]; - if ($line =~ /^\+/) { - $n_cnt++; - push @line, $line; - next; - } - $ofs++; - $o_cnt++; - if ($line =~ /^ /) { - $n_cnt++; - } - push @line, $line; - } - my $head = ("@@ -$o0_ofs" . - (($o_cnt != 1) ? ",$o_cnt" : '') . - " +$n0_ofs" . - (($n_cnt != 1) ? ",$n_cnt" : '') . - " @@\n"); - @{$prev->{TEXT}} = ($head, @line); -} - -sub coalesce_overlapping_hunks { - my (@in) = @_; - my @out = (); - - my ($last_o_ctx); - - for (grep { $_->{USE} } @in) { - my $text = $_->{TEXT}; - my ($o_ofs) = parse_hunk_header($text->[0]); - if (defined $last_o_ctx && - $o_ofs <= $last_o_ctx) { - merge_hunk($out[-1], $_); - } - else { - push @out, $_; - } - $last_o_ctx = find_last_o_ctx($out[-1]); - } - return @out; -} sub help_patch_cmd { print colored $help_color, <<\EOF ; @@ -962,8 +875,6 @@ sub patch_update_file { } } - @hunk = coalesce_overlapping_hunks(@hunk); - my $n_lofs = 0; my @result = (); if ($mode->{USE}) { From ac083c47ea226b470afab39d975e718a475a3c78 Mon Sep 17 00:00:00 2001 From: Thomas Rast Date: Thu, 3 Jul 2008 00:00:00 +0200 Subject: [PATCH 3/3] git-add--interactive: manual hunk editing mode Adds a new option 'e' to the 'add -p' command loop that lets you edit the current hunk in your favourite editor. If the resulting patch applies cleanly, the edited hunk will immediately be marked for staging. If it does not apply cleanly, you will be given an opportunity to edit again. If all lines of the hunk are removed, then the edit is aborted and the hunk is left unchanged. Applying the changed hunk(s) relies on Johannes Schindelin's new --recount option for git-apply. Note that the "real patch" test intentionally uses (echo e; echo n; echo d) | git add -p even though the 'n' and 'd' are superfluous at first sight. They serve to get out of the interaction loop if git add -p wrongly concludes the patch does not apply. Many thanks to Jeff King for lots of help and suggestions. Signed-off-by: Thomas Rast Signed-off-by: Junio C Hamano --- Documentation/git-add.txt | 1 + git-add--interactive.perl | 119 +++++++++++++++++++++++++++++++++++++ t/t3701-add-interactive.sh | 67 +++++++++++++++++++++ 3 files changed, 187 insertions(+) diff --git a/Documentation/git-add.txt b/Documentation/git-add.txt index 011a743652..46dd56c12a 100644 --- a/Documentation/git-add.txt +++ b/Documentation/git-add.txt @@ -236,6 +236,7 @@ patch:: k - leave this hunk undecided, see previous undecided hunk K - leave this hunk undecided, see previous hunk s - split the current hunk into smaller hunks + e - manually edit the current hunk ? - print help + After deciding the fate for all hunks, if there is any hunk diff --git a/git-add--interactive.perl b/git-add--interactive.perl index a4234d39e9..801d7c0251 100755 --- a/git-add--interactive.perl +++ b/git-add--interactive.perl @@ -18,6 +18,18 @@ my ($fraginfo_color) = $diff_use_color ? ( $repo->get_color('color.diff.frag', 'cyan'), ) : (); +my ($diff_plain_color) = + $diff_use_color ? ( + $repo->get_color('color.diff.plain', ''), + ) : (); +my ($diff_old_color) = + $diff_use_color ? ( + $repo->get_color('color.diff.old', 'red'), + ) : (); +my ($diff_new_color) = + $diff_use_color ? ( + $repo->get_color('color.diff.new', 'green'), + ) : (); my $normal_color = $repo->get_color("", "reset"); @@ -683,6 +695,105 @@ sub split_hunk { } +sub color_diff { + return map { + colored((/^@/ ? $fraginfo_color : + /^\+/ ? $diff_new_color : + /^-/ ? $diff_old_color : + $diff_plain_color), + $_); + } @_; +} + +sub edit_hunk_manually { + my ($oldtext) = @_; + + my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff"; + my $fh; + open $fh, '>', $hunkfile + 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; + print $fh <config("core.editor") + || $ENV{VISUAL} || $ENV{EDITOR} || "vi"; + system('sh', '-c', $editor.' "$@"', $editor, $hunkfile); + + open $fh, '<', $hunkfile + or die "failed to open hunk edit file for reading: " . $!; + my @newtext = grep { !/^#/ } <$fh>; + close $fh; + unlink $hunkfile; + + # Abort if nothing remains + if (!grep { /\S/ } @newtext) { + return undef; + } + + # Reinsert the first hunk header if the user accidentally deleted it + if ($newtext[0] !~ /^@/) { + unshift @newtext, $oldtext->[0]; + } + return \@newtext; +} + +sub diff_applies { + my $fh; + open $fh, '| git apply --recount --cached --check'; + for my $h (@_) { + print $fh @{$h->{TEXT}}; + } + return close $fh; +} + +sub prompt_yesno { + my ($prompt) = @_; + while (1) { + print colored $prompt_color, $prompt; + my $line = ; + return 0 if $line =~ /^n/i; + return 1 if $line =~ /^y/i; + } +} + +sub edit_hunk_loop { + my ($head, $hunk, $ix) = @_; + my $text = $hunk->[$ix]->{TEXT}; + + while (1) { + $text = edit_hunk_manually($text); + if (!defined $text) { + return undef; + } + my $newhunk = { TEXT => $text, USE => 1 }; + if (diff_applies($head, + @{$hunk}[0..$ix-1], + $newhunk, + @{$hunk}[$ix+1..$#{$hunk}])) { + $newhunk->{DISPLAY} = [color_diff(@{$text})]; + return $newhunk; + } + else { + prompt_yesno( + 'Your edited hunk does not apply. Edit again ' + . '(saying "no" discards!) [y/n]? ' + ) or return undef; + } + } +} + sub help_patch_cmd { print colored $help_color, <<\EOF ; y - stage this hunk @@ -694,6 +805,7 @@ J - leave this hunk undecided, see next hunk k - leave this hunk undecided, see previous undecided hunk K - leave this hunk undecided, see previous hunk s - split the current hunk into smaller hunks +e - manually edit the current hunk ? - print help EOF } @@ -798,6 +910,7 @@ sub patch_update_file { if (hunk_splittable($hunk[$ix]{TEXT})) { $other .= '/s'; } + $other .= '/e'; for (@{$hunk[$ix]{DISPLAY}}) { print; } @@ -862,6 +975,12 @@ sub patch_update_file { $num = scalar @hunk; next; } + elsif ($line =~ /^e/) { + my $newhunk = edit_hunk_loop($head, \@hunk, $ix); + if (defined $newhunk) { + splice @hunk, $ix, 1, $newhunk; + } + } else { help_patch_cmd($other); next; diff --git a/t/t3701-add-interactive.sh b/t/t3701-add-interactive.sh index fae64eae9f..e95663d8e6 100755 --- a/t/t3701-add-interactive.sh +++ b/t/t3701-add-interactive.sh @@ -66,6 +66,73 @@ test_expect_success 'revert works (commit)' ' grep "unchanged *+3/-0 file" output ' +cat >expected <fake_editor.sh < diff && + test_cmp expected diff +' + +cat >patch <fake_editor.sh +cat >>fake_editor.sh <<\EOF +mv -f "$1" oldpatch && +mv -f patch "$1" +EOF +chmod a+x fake_editor.sh +test_set_editor "$(pwd)/fake_editor.sh" +test_expect_success 'bad edit rejected' ' + git reset && + (echo e; echo n; echo d) | git add -p >output && + grep "hunk does not apply" output +' + +cat >patch <output && + grep "hunk does not apply" output +' + +cat >patch <expected <output && + test_cmp expected output +' + if test "$(git config --bool core.filemode)" = false then say 'skipping filemode tests (filesystem does not properly support modes)'