Merge branch 'pw/add-p-recount'

"git add -p" has been lazy in coalescing split patches before
passing the result to underlying "git apply", leading to corner
case bugs; the logic to prepare the patch to be applied after hunk
selections has been tightened.

* pw/add-p-recount:
  add -p: don't rely on apply's '--recount' option
  add -p: fix counting when splitting and coalescing
  add -p: calculate offset delta for edited patches
  add -p: adjust offsets of subsequent hunks when one is skipped
  t3701: add failing test for pathological context lines
  t3701: don't hard code sha1 hash values
  t3701: use test_write_lines and write_script
  t3701: indent here documents
  add -i: add function to format hunk header
This commit is contained in:
Junio C Hamano 2018-03-14 12:01:04 -07:00
commit 436d18f2d0
2 changed files with 249 additions and 150 deletions

View file

@ -677,7 +677,7 @@ sub add_untracked_cmd {
sub run_git_apply {
my $cmd = shift;
my $fh;
open $fh, '| git ' . $cmd . " --recount --allow-overlap";
open $fh, '| git ' . $cmd . " --allow-overlap";
print $fh @_;
return close $fh;
}
@ -751,6 +751,15 @@ sub parse_hunk_header {
return ($o_ofs, $o_cnt, $n_ofs, $n_cnt);
}
sub format_hunk_header {
my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) = @_;
return ("@@ -$o_ofs" .
(($o_cnt != 1) ? ",$o_cnt" : '') .
" +$n_ofs" .
(($n_cnt != 1) ? ",$n_cnt" : '') .
" @@\n");
}
sub split_hunk {
my ($text, $display) = @_;
my @split = ();
@ -784,6 +793,11 @@ sub split_hunk {
while (++$i < @$text) {
my $line = $text->[$i];
my $display = $display->[$i];
if ($line =~ /^\\/) {
push @{$this->{TEXT}}, $line;
push @{$this->{DISPLAY}}, $display;
next;
}
if ($line =~ /^ /) {
if ($this->{ADDDEL} &&
!defined $next_hunk_start) {
@ -838,11 +852,7 @@ sub split_hunk {
my $o_cnt = $hunk->{OCNT};
my $n_cnt = $hunk->{NCNT};
my $head = ("@@ -$o_ofs" .
(($o_cnt != 1) ? ",$o_cnt" : '') .
" +$n_ofs" .
(($n_cnt != 1) ? ",$n_cnt" : '') .
" @@\n");
my $head = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
my $display_head = $head;
unshift @{$hunk->{TEXT}}, $head;
if ($diff_use_color) {
@ -886,6 +896,9 @@ sub merge_hunk {
$n_cnt++;
push @line, $line;
next;
} elsif ($line =~ /^\\/) {
push @line, $line;
next;
}
last if ($o1_ofs <= $ofs);
@ -904,6 +917,9 @@ sub merge_hunk {
$n_cnt++;
push @line, $line;
next;
} elsif ($line =~ /^\\/) {
push @line, $line;
next;
}
$ofs++;
$o_cnt++;
@ -912,11 +928,7 @@ sub merge_hunk {
}
push @line, $line;
}
my $head = ("@@ -$o0_ofs" .
(($o_cnt != 1) ? ",$o_cnt" : '') .
" +$n0_ofs" .
(($n_cnt != 1) ? ",$n_cnt" : '') .
" @@\n");
my $head = format_hunk_header($o0_ofs, $o_cnt, $n0_ofs, $n_cnt);
@{$prev->{TEXT}} = ($head, @line);
}
@ -925,14 +937,35 @@ sub coalesce_overlapping_hunks {
my @out = ();
my ($last_o_ctx, $last_was_dirty);
my $ofs_delta = 0;
for (grep { $_->{USE} } @in) {
for (@in) {
if ($_->{TYPE} ne 'hunk') {
push @out, $_;
next;
}
my $text = $_->{TEXT};
my ($o_ofs) = parse_hunk_header($text->[0]);
my ($o_ofs, $o_cnt, $n_ofs, $n_cnt) =
parse_hunk_header($text->[0]);
unless ($_->{USE}) {
$ofs_delta += $o_cnt - $n_cnt;
# If this hunk has been edited then subtract
# the delta that is due to the edit.
if ($_->{OFS_DELTA}) {
$ofs_delta -= $_->{OFS_DELTA};
}
next;
}
if ($ofs_delta) {
$n_ofs += $ofs_delta;
$_->{TEXT}->[0] = format_hunk_header($o_ofs, $o_cnt,
$n_ofs, $n_cnt);
}
# If this hunk was edited then adjust the offset delta
# to reflect the edit.
if ($_->{OFS_DELTA}) {
$ofs_delta += $_->{OFS_DELTA};
}
if (defined $last_o_ctx &&
$o_ofs <= $last_o_ctx &&
!$_->{DIRTY} &&
@ -1004,6 +1037,30 @@ sub color_diff {
marked for applying."),
);
sub recount_edited_hunk {
local $_;
my ($oldtext, $newtext) = @_;
my ($o_cnt, $n_cnt) = (0, 0);
for (@{$newtext}[1..$#{$newtext}]) {
my $mode = substr($_, 0, 1);
if ($mode eq '-') {
$o_cnt++;
} elsif ($mode eq '+') {
$n_cnt++;
} elsif ($mode eq ' ') {
$o_cnt++;
$n_cnt++;
}
}
my ($o_ofs, undef, $n_ofs, undef) =
parse_hunk_header($newtext->[0]);
$newtext->[0] = format_hunk_header($o_ofs, $o_cnt, $n_ofs, $n_cnt);
my (undef, $orig_o_cnt, undef, $orig_n_cnt) =
parse_hunk_header($oldtext->[0]);
# Return the change in the number of lines inserted by this hunk
return $orig_o_cnt - $orig_n_cnt - $o_cnt + $n_cnt;
}
sub edit_hunk_manually {
my ($oldtext) = @_;
@ -1102,25 +1159,32 @@ sub prompt_yesno {
}
sub edit_hunk_loop {
my ($head, $hunk, $ix) = @_;
my $text = $hunk->[$ix]->{TEXT};
my ($head, $hunks, $ix) = @_;
my $hunk = $hunks->[$ix];
my $text = $hunk->{TEXT};
while (1) {
$text = edit_hunk_manually($text);
if (!defined $text) {
my $newtext = edit_hunk_manually($text);
if (!defined $newtext) {
return undef;
}
my $newhunk = {
TEXT => $text,
TYPE => $hunk->[$ix]->{TYPE},
TEXT => $newtext,
TYPE => $hunk->{TYPE},
USE => 1,
DIRTY => 1,
};
$newhunk->{OFS_DELTA} = recount_edited_hunk($text, $newtext);
# If this hunk has already been edited then add the
# offset delta of the previous edit to get the real
# delta from the original unedited hunk.
$hunk->{OFS_DELTA} and
$newhunk->{OFS_DELTA} += $hunk->{OFS_DELTA};
if (diff_applies($head,
@{$hunk}[0..$ix-1],
@{$hunks}[0..$ix-1],
$newhunk,
@{$hunk}[$ix+1..$#{$hunk}])) {
$newhunk->{DISPLAY} = [color_diff(@{$text})];
@{$hunks}[$ix+1..$#{$hunks}])) {
$newhunk->{DISPLAY} = [color_diff(@{$newtext})];
return $newhunk;
}
else {

View file

@ -10,6 +10,19 @@ then
test_done
fi
diff_cmp () {
for x
do
sed -e '/^index/s/[0-9a-f]*[1-9a-f][0-9a-f]*\.\./1234567../' \
-e '/^index/s/\.\.[0-9a-f]*[1-9a-f][0-9a-f]*/..9abcdef/' \
-e '/^index/s/ 00*\.\./ 0000000../' \
-e '/^index/s/\.\.00*$/..0000000/' \
-e '/^index/s/\.\.00* /..0000000 /' \
"$x" >"$x.filtered"
done
test_cmp "$1.filtered" "$2.filtered"
}
test_expect_success 'setup (initial)' '
echo content >file &&
git add file &&
@ -22,20 +35,20 @@ test_expect_success 'status works (initial)' '
'
test_expect_success 'setup expected' '
cat >expected <<EOF
new file mode 100644
index 0000000..d95f3ad
--- /dev/null
+++ b/file
@@ -0,0 +1 @@
+content
EOF
cat >expected <<-\EOF
new file mode 100644
index 0000000..d95f3ad
--- /dev/null
+++ b/file
@@ -0,0 +1 @@
+content
EOF
'
test_expect_success 'diff works (initial)' '
(echo d; echo 1) | git add -i >output &&
sed -ne "/new file/,/content/p" <output >diff &&
test_cmp expected diff
diff_cmp expected diff
'
test_expect_success 'revert works (initial)' '
git add file &&
@ -59,20 +72,20 @@ test_expect_success 'status works (commit)' '
'
test_expect_success 'setup expected' '
cat >expected <<EOF
index 180b47c..b6f2c08 100644
--- a/file
+++ b/file
@@ -1 +1,2 @@
baseline
+content
EOF
cat >expected <<-\EOF
index 180b47c..b6f2c08 100644
--- a/file
+++ b/file
@@ -1 +1,2 @@
baseline
+content
EOF
'
test_expect_success 'diff works (commit)' '
(echo d; echo 1) | git add -i >output &&
sed -ne "/^index/,/content/p" <output >diff &&
test_cmp expected diff
diff_cmp expected diff
'
test_expect_success 'revert works (commit)' '
git add file &&
@ -83,39 +96,32 @@ test_expect_success 'revert works (commit)' '
test_expect_success 'setup expected' '
cat >expected <<EOF
EOF
'
test_expect_success 'setup fake editor' '
>fake_editor.sh &&
chmod a+x fake_editor.sh &&
test_set_editor "$(pwd)/fake_editor.sh"
cat >expected <<-\EOF
EOF
'
test_expect_success 'dummy edit works' '
test_set_editor : &&
(echo e; echo a) | git add -p &&
git diff > diff &&
test_cmp expected diff
diff_cmp expected diff
'
test_expect_success 'setup patch' '
cat >patch <<EOF
@@ -1,1 +1,4 @@
this
+patch
-does not
apply
EOF
cat >patch <<-\EOF
@@ -1,1 +1,4 @@
this
+patch
-does not
apply
EOF
'
test_expect_success 'setup fake editor' '
echo "#!$SHELL_PATH" >fake_editor.sh &&
cat >>fake_editor.sh <<\EOF &&
mv -f "$1" oldpatch &&
mv -f patch "$1"
EOF
chmod a+x fake_editor.sh &&
write_script "fake_editor.sh" <<-\EOF &&
mv -f "$1" oldpatch &&
mv -f patch "$1"
EOF
test_set_editor "$(pwd)/fake_editor.sh"
'
@ -126,10 +132,10 @@ test_expect_success 'bad edit rejected' '
'
test_expect_success 'setup patch' '
cat >patch <<EOF
this patch
is garbage
EOF
cat >patch <<-\EOF
this patch
is garbage
EOF
'
test_expect_success 'garbage edit rejected' '
@ -139,34 +145,34 @@ test_expect_success 'garbage edit rejected' '
'
test_expect_success 'setup patch' '
cat >patch <<EOF
@@ -1,0 +1,0 @@
baseline
+content
+newcontent
+lines
EOF
cat >patch <<-\EOF
@@ -1,0 +1,0 @@
baseline
+content
+newcontent
+lines
EOF
'
test_expect_success 'setup expected' '
cat >expected <<EOF
diff --git a/file b/file
index b5dd6c9..f910ae9 100644
--- a/file
+++ b/file
@@ -1,4 +1,4 @@
baseline
content
-newcontent
+more
lines
EOF
cat >expected <<-\EOF
diff --git a/file b/file
index b5dd6c9..f910ae9 100644
--- a/file
+++ b/file
@@ -1,4 +1,4 @@
baseline
content
-newcontent
+more
lines
EOF
'
test_expect_success 'real edit works' '
(echo e; echo n; echo d) | git add -p &&
git diff >output &&
test_cmp expected output
diff_cmp expected output
'
test_expect_success 'skip files similarly as commit -a' '
@ -178,7 +184,7 @@ test_expect_success 'skip files similarly as commit -a' '
git reset &&
git commit -am commit &&
git diff >expected &&
test_cmp expected output &&
diff_cmp expected output &&
git reset --hard HEAD^
'
rm -f .gitignore
@ -222,52 +228,67 @@ test_expect_success 'setup again' '
# Write the patch file with a new line at the top and bottom
test_expect_success 'setup patch' '
cat >patch <<EOF
index 180b47c..b6f2c08 100644
--- a/file
+++ b/file
@@ -1,2 +1,4 @@
+firstline
baseline
content
+lastline
EOF
cat >patch <<-\EOF
index 180b47c..b6f2c08 100644
--- a/file
+++ b/file
@@ -1,2 +1,4 @@
+firstline
baseline
content
+lastline
\ No newline at end of file
EOF
'
# Expected output, similar to the patch but w/ diff at the top
# Expected output, diff is similar to the patch but w/ diff at the top
test_expect_success 'setup expected' '
cat >expected <<EOF
diff --git a/file b/file
index b6f2c08..61b9053 100755
--- a/file
+++ b/file
@@ -1,2 +1,4 @@
+firstline
baseline
content
+lastline
EOF
echo diff --git a/file b/file >expected &&
cat patch |sed "/^index/s/ 100644/ 100755/" >>expected &&
cat >expected-output <<-\EOF
--- a/file
+++ b/file
@@ -1,2 +1,4 @@
+firstline
baseline
content
+lastline
\ No newline at end of file
@@ -1,2 +1,3 @@
+firstline
baseline
content
@@ -1,2 +2,3 @@
baseline
content
+lastline
\ No newline at end of file
EOF
'
# Test splitting the first patch, then adding both
test_expect_success 'add first line works' '
test_expect_success C_LOCALE_OUTPUT 'add first line works' '
git commit -am "clear local changes" &&
git apply patch &&
(echo s; echo y; echo y) | git add -p file &&
git diff --cached > diff &&
test_cmp expected diff
printf "%s\n" s y y | git add -p file 2>error |
sed -n -e "s/^Stage this hunk[^@]*\(@@ .*\)/\1/" \
-e "/^[-+@ \\\\]"/p >output &&
test_must_be_empty error &&
git diff --cached >diff &&
diff_cmp expected diff &&
test_cmp expected-output output
'
test_expect_success 'setup expected' '
cat >expected <<EOF
diff --git a/non-empty b/non-empty
deleted file mode 100644
index d95f3ad..0000000
--- a/non-empty
+++ /dev/null
@@ -1 +0,0 @@
-content
EOF
cat >expected <<-\EOF
diff --git a/non-empty b/non-empty
deleted file mode 100644
index d95f3ad..0000000
--- a/non-empty
+++ /dev/null
@@ -1 +0,0 @@
-content
EOF
'
test_expect_success 'deleting a non-empty file' '
@ -278,15 +299,15 @@ test_expect_success 'deleting a non-empty file' '
rm non-empty &&
echo y | git add -p non-empty &&
git diff --cached >diff &&
test_cmp expected diff
diff_cmp expected diff
'
test_expect_success 'setup expected' '
cat >expected <<EOF
diff --git a/empty b/empty
deleted file mode 100644
index e69de29..0000000
EOF
cat >expected <<-\EOF
diff --git a/empty b/empty
deleted file mode 100644
index e69de29..0000000
EOF
'
test_expect_success 'deleting an empty file' '
@ -297,23 +318,17 @@ test_expect_success 'deleting an empty file' '
rm empty &&
echo y | git add -p empty &&
git diff --cached >diff &&
test_cmp expected diff
diff_cmp expected diff
'
test_expect_success 'split hunk setup' '
git reset --hard &&
for i in 10 20 30 40 50 60
do
echo $i
done >test &&
test_write_lines 10 20 30 40 50 60 >test &&
git add test &&
test_tick &&
git commit -m test &&
for i in 10 15 20 21 22 23 24 30 40 50 60
do
echo $i
done >test
test_write_lines 10 15 20 21 22 23 24 30 40 50 60 >test
'
test_expect_success 'split hunk "add -p (edit)"' '
@ -334,17 +349,7 @@ test_expect_success 'split hunk "add -p (edit)"' '
'
test_expect_failure 'split hunk "add -p (no, yes, edit)"' '
cat >test <<-\EOF &&
5
10
20
21
30
31
40
50
60
EOF
test_write_lines 5 10 20 21 30 31 40 50 60 >test &&
git reset &&
# test sequence is s(plit), n(o), y(es), e(dit)
# q n q q is there to make sure we exit at the end.
@ -378,7 +383,7 @@ test_expect_success 'patch mode ignores unmerged entries' '
+changed
EOF
git diff --cached >diff &&
test_cmp expected diff
diff_cmp expected diff
'
test_expect_success TTY 'diffs can be colorized' '
@ -407,7 +412,7 @@ test_expect_success 'patch-mode via -i prompts for files' '
echo test >expect &&
git diff --cached --name-only >actual &&
test_cmp expect actual
diff_cmp expect actual
'
test_expect_success 'add -p handles globs' '
@ -541,4 +546,34 @@ test_expect_success 'status ignores dirty submodules (except HEAD)' '
! grep dirty-otherwise output
'
test_expect_success 'set up pathological context' '
git reset --hard &&
test_write_lines a a a a a a a a a a a >a &&
git add a &&
git commit -m a &&
test_write_lines c b a a a a a a a b a a a a >a &&
test_write_lines a a a a a a a b a a a a >expected-1 &&
test_write_lines b a a a a a a a b a a a a >expected-2 &&
# check editing can cope with missing header and deleted context lines
# as well as changes to other lines
test_write_lines +b " a" >patch
'
test_expect_success 'add -p works with pathological context lines' '
git reset &&
printf "%s\n" n y |
git add -p &&
git cat-file blob :a >actual &&
test_cmp expected-1 actual
'
test_expect_success 'add -p patch editing works with pathological context lines' '
git reset &&
# n q q below is in case edit fails
printf "%s\n" e y n q q |
git add -p &&
git cat-file blob :a >actual &&
test_cmp expected-2 actual
'
test_done