Skip to content

Commit 69b060c

Browse files
committed
Merge branch 'tr/add-i-e'
* tr/add-i-e: git-add--interactive: manual hunk editing mode git-add--interactive: remove hunk coalescing git-add--interactive: replace hunk recounting with apply --recount
2 parents a9a3e82 + ac083c4 commit 69b060c

File tree

3 files changed

+174
-100
lines changed

3 files changed

+174
-100
lines changed

Documentation/git-add.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,7 @@ patch::
236236
k - leave this hunk undecided, see previous undecided hunk
237237
K - leave this hunk undecided, see previous hunk
238238
s - split the current hunk into smaller hunks
239+
e - manually edit the current hunk
239240
? - print help
240241
+
241242
After deciding the fate for all hunks, if there is any hunk

git-add--interactive.perl

Lines changed: 106 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,18 @@
1818
$diff_use_color ? (
1919
$repo->get_color('color.diff.frag', 'cyan'),
2020
) : ();
21+
my ($diff_plain_color) =
22+
$diff_use_color ? (
23+
$repo->get_color('color.diff.plain', ''),
24+
) : ();
25+
my ($diff_old_color) =
26+
$diff_use_color ? (
27+
$repo->get_color('color.diff.old', 'red'),
28+
) : ();
29+
my ($diff_new_color) =
30+
$diff_use_color ? (
31+
$repo->get_color('color.diff.new', 'green'),
32+
) : ();
2133

2234
my $normal_color = $repo->get_color("", "reset");
2335

@@ -682,92 +694,104 @@ sub split_hunk {
682694
return @split;
683695
}
684696

685-
sub find_last_o_ctx {
686-
my ($it) = @_;
687-
my $text = $it->{TEXT};
688-
my ($o_ofs, $o_cnt) = parse_hunk_header($text->[0]);
689-
my $i = @{$text};
690-
my $last_o_ctx = $o_ofs + $o_cnt;
691-
while (0 < --$i) {
692-
my $line = $text->[$i];
693-
if ($line =~ /^ /) {
694-
$last_o_ctx--;
695-
next;
696-
}
697-
last;
698-
}
699-
return $last_o_ctx;
697+
698+
sub color_diff {
699+
return map {
700+
colored((/^@/ ? $fraginfo_color :
701+
/^\+/ ? $diff_new_color :
702+
/^-/ ? $diff_old_color :
703+
$diff_plain_color),
704+
$_);
705+
} @_;
700706
}
701707

702-
sub merge_hunk {
703-
my ($prev, $this) = @_;
704-
my ($o0_ofs, $o0_cnt, $n0_ofs, $n0_cnt) =
705-
parse_hunk_header($prev->{TEXT}[0]);
706-
my ($o1_ofs, $o1_cnt, $n1_ofs, $n1_cnt) =
707-
parse_hunk_header($this->{TEXT}[0]);
708-
709-
my (@line, $i, $ofs, $o_cnt, $n_cnt);
710-
$ofs = $o0_ofs;
711-
$o_cnt = $n_cnt = 0;
712-
for ($i = 1; $i < @{$prev->{TEXT}}; $i++) {
713-
my $line = $prev->{TEXT}[$i];
714-
if ($line =~ /^\+/) {
715-
$n_cnt++;
716-
push @line, $line;
717-
next;
718-
}
708+
sub edit_hunk_manually {
709+
my ($oldtext) = @_;
719710

720-
last if ($o1_ofs <= $ofs);
711+
my $hunkfile = $repo->repo_path . "/addp-hunk-edit.diff";
712+
my $fh;
713+
open $fh, '>', $hunkfile
714+
or die "failed to open hunk edit file for writing: " . $!;
715+
print $fh "# Manual hunk edit mode -- see bottom for a quick guide\n";
716+
print $fh @$oldtext;
717+
print $fh <<EOF;
718+
# ---
719+
# To remove '-' lines, make them ' ' lines (context).
720+
# To remove '+' lines, delete them.
721+
# Lines starting with # will be removed.
722+
#
723+
# If the patch applies cleanly, the edited hunk will immediately be
724+
# marked for staging. If it does not apply cleanly, you will be given
725+
# an opportunity to edit again. If all lines of the hunk are removed,
726+
# then the edit is aborted and the hunk is left unchanged.
727+
EOF
728+
close $fh;
721729

722-
$o_cnt++;
723-
$ofs++;
724-
if ($line =~ /^ /) {
725-
$n_cnt++;
726-
}
727-
push @line, $line;
730+
my $editor = $ENV{GIT_EDITOR} || $repo->config("core.editor")
731+
|| $ENV{VISUAL} || $ENV{EDITOR} || "vi";
732+
system('sh', '-c', $editor.' "$@"', $editor, $hunkfile);
733+
734+
open $fh, '<', $hunkfile
735+
or die "failed to open hunk edit file for reading: " . $!;
736+
my @newtext = grep { !/^#/ } <$fh>;
737+
close $fh;
738+
unlink $hunkfile;
739+
740+
# Abort if nothing remains
741+
if (!grep { /\S/ } @newtext) {
742+
return undef;
728743
}
729744

730-
for ($i = 1; $i < @{$this->{TEXT}}; $i++) {
731-
my $line = $this->{TEXT}[$i];
732-
if ($line =~ /^\+/) {
733-
$n_cnt++;
734-
push @line, $line;
735-
next;
736-
}
737-
$ofs++;
738-
$o_cnt++;
739-
if ($line =~ /^ /) {
740-
$n_cnt++;
741-
}
742-
push @line, $line;
745+
# Reinsert the first hunk header if the user accidentally deleted it
746+
if ($newtext[0] !~ /^@/) {
747+
unshift @newtext, $oldtext->[0];
748+
}
749+
return \@newtext;
750+
}
751+
752+
sub diff_applies {
753+
my $fh;
754+
open $fh, '| git apply --recount --cached --check';
755+
for my $h (@_) {
756+
print $fh @{$h->{TEXT}};
743757
}
744-
my $head = ("@@ -$o0_ofs" .
745-
(($o_cnt != 1) ? ",$o_cnt" : '') .
746-
" +$n0_ofs" .
747-
(($n_cnt != 1) ? ",$n_cnt" : '') .
748-
" @@\n");
749-
@{$prev->{TEXT}} = ($head, @line);
758+
return close $fh;
750759
}
751760

752-
sub coalesce_overlapping_hunks {
753-
my (@in) = @_;
754-
my @out = ();
761+
sub prompt_yesno {
762+
my ($prompt) = @_;
763+
while (1) {
764+
print colored $prompt_color, $prompt;
765+
my $line = <STDIN>;
766+
return 0 if $line =~ /^n/i;
767+
return 1 if $line =~ /^y/i;
768+
}
769+
}
755770

756-
my ($last_o_ctx);
771+
sub edit_hunk_loop {
772+
my ($head, $hunk, $ix) = @_;
773+
my $text = $hunk->[$ix]->{TEXT};
757774

758-
for (grep { $_->{USE} } @in) {
759-
my $text = $_->{TEXT};
760-
my ($o_ofs) = parse_hunk_header($text->[0]);
761-
if (defined $last_o_ctx &&
762-
$o_ofs <= $last_o_ctx) {
763-
merge_hunk($out[-1], $_);
775+
while (1) {
776+
$text = edit_hunk_manually($text);
777+
if (!defined $text) {
778+
return undef;
779+
}
780+
my $newhunk = { TEXT => $text, USE => 1 };
781+
if (diff_applies($head,
782+
@{$hunk}[0..$ix-1],
783+
$newhunk,
784+
@{$hunk}[$ix+1..$#{$hunk}])) {
785+
$newhunk->{DISPLAY} = [color_diff(@{$text})];
786+
return $newhunk;
764787
}
765788
else {
766-
push @out, $_;
789+
prompt_yesno(
790+
'Your edited hunk does not apply. Edit again '
791+
. '(saying "no" discards!) [y/n]? '
792+
) or return undef;
767793
}
768-
$last_o_ctx = find_last_o_ctx($out[-1]);
769794
}
770-
return @out;
771795
}
772796

773797
sub help_patch_cmd {
@@ -781,6 +805,7 @@ sub help_patch_cmd {
781805
k - leave this hunk undecided, see previous undecided hunk
782806
K - leave this hunk undecided, see previous hunk
783807
s - split the current hunk into smaller hunks
808+
e - manually edit the current hunk
784809
? - print help
785810
EOF
786811
}
@@ -885,6 +910,7 @@ sub patch_update_file {
885910
if (hunk_splittable($hunk[$ix]{TEXT})) {
886911
$other .= '/s';
887912
}
913+
$other .= '/e';
888914
for (@{$hunk[$ix]{DISPLAY}}) {
889915
print;
890916
}
@@ -949,6 +975,12 @@ sub patch_update_file {
949975
$num = scalar @hunk;
950976
next;
951977
}
978+
elsif ($line =~ /^e/) {
979+
my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
980+
if (defined $newhunk) {
981+
splice @hunk, $ix, 1, $newhunk;
982+
}
983+
}
952984
else {
953985
help_patch_cmd($other);
954986
next;
@@ -962,47 +994,21 @@ sub patch_update_file {
962994
}
963995
}
964996

965-
@hunk = coalesce_overlapping_hunks(@hunk);
966-
967997
my $n_lofs = 0;
968998
my @result = ();
969999
if ($mode->{USE}) {
9701000
push @result, @{$mode->{TEXT}};
9711001
}
9721002
for (@hunk) {
973-
my $text = $_->{TEXT};
974-
my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
975-
parse_hunk_header($text->[0]);
976-
977-
if (!$_->{USE}) {
978-
# We would have added ($n_cnt - $o_cnt) lines
979-
# to the postimage if we were to use this hunk,
980-
# but we didn't. So the line number that the next
981-
# hunk starts at would be shifted by that much.
982-
$n_lofs -= ($n_cnt - $o_cnt);
983-
next;
984-
}
985-
else {
986-
if ($n_lofs) {
987-
$n_ofs += $n_lofs;
988-
$text->[0] = ("@@ -$o_ofs" .
989-
(($o_cnt != 1)
990-
? ",$o_cnt" : '') .
991-
" +$n_ofs" .
992-
(($n_cnt != 1)
993-
? ",$n_cnt" : '') .
994-
" @@\n");
995-
}
996-
for (@$text) {
997-
push @result, $_;
998-
}
1003+
if ($_->{USE}) {
1004+
push @result, @{$_->{TEXT}};
9991005
}
10001006
}
10011007

10021008
if (@result) {
10031009
my $fh;
10041010

1005-
open $fh, '| git apply --cached';
1011+
open $fh, '| git apply --cached --recount';
10061012
for (@{$head->{TEXT}}, @result) {
10071013
print $fh $_;
10081014
}

t/t3701-add-interactive.sh

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,73 @@ test_expect_success 'revert works (commit)' '
6666
grep "unchanged *+3/-0 file" output
6767
'
6868

69+
cat >expected <<EOF
70+
EOF
71+
cat >fake_editor.sh <<EOF
72+
EOF
73+
chmod a+x fake_editor.sh
74+
test_set_editor "$(pwd)/fake_editor.sh"
75+
test_expect_success 'dummy edit works' '
76+
(echo e; echo a) | git add -p &&
77+
git diff > diff &&
78+
test_cmp expected diff
79+
'
80+
81+
cat >patch <<EOF
82+
@@ -1,1 +1,4 @@
83+
this
84+
+patch
85+
-doesn't
86+
apply
87+
EOF
88+
echo "#!$SHELL_PATH" >fake_editor.sh
89+
cat >>fake_editor.sh <<\EOF
90+
mv -f "$1" oldpatch &&
91+
mv -f patch "$1"
92+
EOF
93+
chmod a+x fake_editor.sh
94+
test_set_editor "$(pwd)/fake_editor.sh"
95+
test_expect_success 'bad edit rejected' '
96+
git reset &&
97+
(echo e; echo n; echo d) | git add -p >output &&
98+
grep "hunk does not apply" output
99+
'
100+
101+
cat >patch <<EOF
102+
this patch
103+
is garbage
104+
EOF
105+
test_expect_success 'garbage edit rejected' '
106+
git reset &&
107+
(echo e; echo n; echo d) | git add -p >output &&
108+
grep "hunk does not apply" output
109+
'
110+
111+
cat >patch <<EOF
112+
@@ -1,0 +1,0 @@
113+
baseline
114+
+content
115+
+newcontent
116+
+lines
117+
EOF
118+
cat >expected <<EOF
119+
diff --git a/file b/file
120+
index b5dd6c9..f910ae9 100644
121+
--- a/file
122+
+++ b/file
123+
@@ -1,4 +1,4 @@
124+
baseline
125+
content
126+
-newcontent
127+
+more
128+
lines
129+
EOF
130+
test_expect_success 'real edit works' '
131+
(echo e; echo n; echo d) | git add -p &&
132+
git diff >output &&
133+
test_cmp expected output
134+
'
135+
69136
if test "$(git config --bool core.filemode)" = false
70137
then
71138
say 'skipping filemode tests (filesystem does not properly support modes)'

0 commit comments

Comments
 (0)