Merge branch 'pw/add-p-recount' into next
authorJunio C Hamano <gitster@pobox.com>
Tue, 6 Mar 2018 23:11:48 +0000 (15:11 -0800)
committerJunio C Hamano <gitster@pobox.com>
Tue, 6 Mar 2018 23:11:48 +0000 (15:11 -0800)
"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

1  2 
git-add--interactive.perl
t/t3701-add-interactive.sh
index d9d8ff3090e914421ab67071117789f6b3554475,ab022ec0737607ef6b3302e6f9203835732cbe37..7a0c95fd0deb685a43fd3aa4bd88515ce0eb9fcd
@@@ -262,7 -262,7 +262,7 @@@ sub list_modified 
                }
        }
  
 -      for (run_cmd_pipe(qw(git diff-files --numstat --summary --raw --), @ARGV)) {
 +      for (run_cmd_pipe(qw(git diff-files --ignore-submodules=dirty --numstat --summary --raw --), @ARGV)) {
                if (($add, $del, $file) =
                    /^([-\d]+)  ([-\d]+)        (.*)/) {
                        $file = unquote_path($file);
@@@ -677,7 -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 +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 = ();
                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) {
                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 +896,9 @@@ sub merge_hunk 
                        $n_cnt++;
                        push @line, $line;
                        next;
+               } elsif ($line =~ /^\\/) {
+                       push @line, $line;
+                       next;
                }
  
                last if ($o1_ofs <= $ofs);
                        $n_cnt++;
                        push @line, $line;
                        next;
+               } elsif ($line =~ /^\\/) {
+                       push @line, $line;
+                       next;
                }
                $ofs++;
                $o_cnt++;
                }
                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 +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 +1037,30 @@@ marked for discarding.")
  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 +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 {
@@@ -1184,13 -1248,7 +1248,13 @@@ d - do not apply this hunk or any of th
  );
  
  sub help_patch_cmd {
 -      print colored $help_color, __($help_patch_modes{$patch_mode}), "\n", __ <<EOF ;
 +      local $_;
 +      my $other = $_[0] . ",?";
 +      print colored $help_color, __($help_patch_modes{$patch_mode}), "\n",
 +              map { "$_\n" } grep {
 +                      my $c = quotemeta(substr($_, 0, 1));
 +                      $other =~ /,$c/
 +              } split "\n", __ <<EOF ;
  g - select a hunk to go to
  / - search for a hunk matching the given regex
  j - leave this hunk undecided, see next undecided hunk
@@@ -1308,39 -1366,39 +1372,39 @@@ sub display_hunks 
  
  my %patch_update_prompt_modes = (
        stage => {
 -              mode => N__("Stage mode change [y,n,q,a,d,/%s,?]? "),
 -              deletion => N__("Stage deletion [y,n,q,a,d,/%s,?]? "),
 -              hunk => N__("Stage this hunk [y,n,q,a,d,/%s,?]? "),
 +              mode => N__("Stage mode change [y,n,q,a,d%s,?]? "),
 +              deletion => N__("Stage deletion [y,n,q,a,d%s,?]? "),
 +              hunk => N__("Stage this hunk [y,n,q,a,d%s,?]? "),
        },
        stash => {
 -              mode => N__("Stash mode change [y,n,q,a,d,/%s,?]? "),
 -              deletion => N__("Stash deletion [y,n,q,a,d,/%s,?]? "),
 -              hunk => N__("Stash this hunk [y,n,q,a,d,/%s,?]? "),
 +              mode => N__("Stash mode change [y,n,q,a,d%s,?]? "),
 +              deletion => N__("Stash deletion [y,n,q,a,d%s,?]? "),
 +              hunk => N__("Stash this hunk [y,n,q,a,d%s,?]? "),
        },
        reset_head => {
 -              mode => N__("Unstage mode change [y,n,q,a,d,/%s,?]? "),
 -              deletion => N__("Unstage deletion [y,n,q,a,d,/%s,?]? "),
 -              hunk => N__("Unstage this hunk [y,n,q,a,d,/%s,?]? "),
 +              mode => N__("Unstage mode change [y,n,q,a,d%s,?]? "),
 +              deletion => N__("Unstage deletion [y,n,q,a,d%s,?]? "),
 +              hunk => N__("Unstage this hunk [y,n,q,a,d%s,?]? "),
        },
        reset_nothead => {
 -              mode => N__("Apply mode change to index [y,n,q,a,d,/%s,?]? "),
 -              deletion => N__("Apply deletion to index [y,n,q,a,d,/%s,?]? "),
 -              hunk => N__("Apply this hunk to index [y,n,q,a,d,/%s,?]? "),
 +              mode => N__("Apply mode change to index [y,n,q,a,d%s,?]? "),
 +              deletion => N__("Apply deletion to index [y,n,q,a,d%s,?]? "),
 +              hunk => N__("Apply this hunk to index [y,n,q,a,d%s,?]? "),
        },
        checkout_index => {
 -              mode => N__("Discard mode change from worktree [y,n,q,a,d,/%s,?]? "),
 -              deletion => N__("Discard deletion from worktree [y,n,q,a,d,/%s,?]? "),
 -              hunk => N__("Discard this hunk from worktree [y,n,q,a,d,/%s,?]? "),
 +              mode => N__("Discard mode change from worktree [y,n,q,a,d%s,?]? "),
 +              deletion => N__("Discard deletion from worktree [y,n,q,a,d%s,?]? "),
 +              hunk => N__("Discard this hunk from worktree [y,n,q,a,d%s,?]? "),
        },
        checkout_head => {
 -              mode => N__("Discard mode change from index and worktree [y,n,q,a,d,/%s,?]? "),
 -              deletion => N__("Discard deletion from index and worktree [y,n,q,a,d,/%s,?]? "),
 -              hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d,/%s,?]? "),
 +              mode => N__("Discard mode change from index and worktree [y,n,q,a,d%s,?]? "),
 +              deletion => N__("Discard deletion from index and worktree [y,n,q,a,d%s,?]? "),
 +              hunk => N__("Discard this hunk from index and worktree [y,n,q,a,d%s,?]? "),
        },
        checkout_nothead => {
 -              mode => N__("Apply mode change to index and worktree [y,n,q,a,d,/%s,?]? "),
 -              deletion => N__("Apply deletion to index and worktree [y,n,q,a,d,/%s,?]? "),
 -              hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d,/%s,?]? "),
 +              mode => N__("Apply mode change to index and worktree [y,n,q,a,d%s,?]? "),
 +              deletion => N__("Apply deletion to index and worktree [y,n,q,a,d%s,?]? "),
 +              hunk => N__("Apply this hunk to index and worktree [y,n,q,a,d%s,?]? "),
        },
  );
  
@@@ -1396,7 -1454,7 +1460,7 @@@ sub patch_update_file 
                        $other .= ',J';
                }
                if ($num > 1) {
 -                      $other .= ',g';
 +                      $other .= ',g,/';
                }
                for ($i = 0; $i < $num; $i++) {
                        if (!defined $hunk[$i]{USE}) {
                                }
                                next;
                        }
 -                      elsif ($other =~ /g/ && $line =~ /^g(.*)/) {
 +                      elsif ($line =~ /^g(.*)/) {
                                my $response = $1;
 +                              unless ($other =~ /g/) {
 +                                      error_msg __("No other hunks to goto\n");
 +                                      next;
 +                              }
                                my $no = $ix > 10 ? $ix - 10 : 0;
                                while ($response eq '') {
                                        $no = display_hunks(\@hunk, $no);
                        }
                        elsif ($line =~ m|^/(.*)|) {
                                my $regex = $1;
 +                              unless ($other =~ m|/|) {
 +                                      error_msg __("No other hunks to search\n");
 +                                      next;
 +                              }
                                if ($1 eq "") {
                                        print colored $prompt_color, __("search for regex? ");
                                        $regex = <STDIN>;
                                        next;
                                }
                        }
 -                      elsif ($other =~ /s/ && $line =~ /^s/) {
 +                      elsif ($line =~ /^s/) {
 +                              unless ($other =~ /s/) {
 +                                      error_msg __("Sorry, cannot split this hunk\n");
 +                                      next;
 +                              }
                                my @split = split_hunk($hunk[$ix]{TEXT}, $hunk[$ix]{DISPLAY});
                                if (1 < @split) {
                                        print colored $header_color, sprintf(
                                $num = scalar @hunk;
                                next;
                        }
 -                      elsif ($other =~ /e/ && $line =~ /^e/) {
 +                      elsif ($line =~ /^e/) {
 +                              unless ($other =~ /e/) {
 +                                      error_msg __("Sorry, cannot edit this hunk\n");
 +                                      next;
 +                              }
                                my $newhunk = edit_hunk_loop($head, \@hunk, $ix);
                                if (defined $newhunk) {
                                        splice @hunk, $ix, 1, $newhunk;
index 058698df6a4a9811b9db84fb5900472c47c61798,e5c66f750087fc9938b527c447bc814cbab48506..a9a9478a293f97920c6e3fb6d31653d8b37989a6
@@@ -10,6 -10,19 +10,19 @@@ the
        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 +35,20 @@@ test_expect_success 'status works (init
  '
  
  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 +72,20 @@@ test_expect_success 'status works (comm
  '
  
  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 &&
  
  
  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 +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' '
  '
  
  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' '
        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 +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
- '
- # Expected output, similar to the patch but w/ diff at the top
+       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, 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' '
        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' '
        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)"' '
  '
  
  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 +383,7 @@@ test_expect_success 'patch mode ignore
        +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 +412,7 @@@ test_expect_success 'patch-mode via -i 
  
        echo test >expect &&
        git diff --cached --name-only >actual &&
-       test_cmp expect actual
+       diff_cmp expect actual
  '
  
  test_expect_success 'add -p handles globs' '
@@@ -493,52 -498,34 +498,82 @@@ test_expect_success 'add -p works even 
        test_cmp expect actual
  '
  
 +test_expect_success 'setup different kinds of dirty submodules' '
 +      test_create_repo for-submodules &&
 +      (
 +              cd for-submodules &&
 +              test_commit initial &&
 +              test_create_repo dirty-head &&
 +              (
 +                      cd dirty-head &&
 +                      test_commit initial
 +              ) &&
 +              cp -R dirty-head dirty-otherwise &&
 +              cp -R dirty-head dirty-both-ways &&
 +              git add dirty-head &&
 +              git add dirty-otherwise dirty-both-ways &&
 +              git commit -m initial &&
 +
 +              cd dirty-head &&
 +              test_commit updated &&
 +              cd ../dirty-both-ways &&
 +              test_commit updated &&
 +              echo dirty >>initial &&
 +              : >untracked &&
 +              cd ../dirty-otherwise &&
 +              echo dirty >>initial &&
 +              : >untracked
 +      ) &&
 +      git -C for-submodules diff-files --name-only >actual &&
 +      cat >expected <<-\EOF &&
 +      dirty-both-ways
 +      dirty-head
 +      dirty-otherwise
 +      EOF
 +      test_cmp expected actual &&
 +      git -C for-submodules diff-files --name-only --ignore-submodules=dirty >actual &&
 +      cat >expected <<-\EOF &&
 +      dirty-both-ways
 +      dirty-head
 +      EOF
 +      test_cmp expected actual
 +'
 +
 +test_expect_success 'status ignores dirty submodules (except HEAD)' '
 +      git -C for-submodules add -i </dev/null >output &&
 +      grep dirty-head output &&
 +      grep dirty-both-ways output &&
 +      ! 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