diff --git a/gitdiff/apply.go b/gitdiff/apply.go index e1c7209..94970df 100644 --- a/gitdiff/apply.go +++ b/gitdiff/apply.go @@ -120,6 +120,19 @@ func Apply(dst io.Writer, src io.ReaderAt, f *File, opts ...ApplyOption) error { return applier.Close() case len(f.TextFragments) > 0: + // creating a file requires an empty source; a fragment starting at + // position 0 is not enough to detect this, as zero-context patches use + // the same position to insert before the first line of an existing file + if f.IsNew { + ok, err := isLen(src, 0) + if err != nil { + return applyError(err) + } + if !ok { + return applyError(&Conflict{"cannot create new file from non-empty src"}) + } + } + frags := make([]*TextFragment, len(f.TextFragments)) copy(frags, f.TextFragments) @@ -137,6 +150,14 @@ func Apply(dst io.Writer, src io.ReaderAt, f *File, opts ...ApplyOption) error { return applyError(err, fragNum(i)) } } + // deleting a file must consume the whole source; a fragment ending at + // new position 0 is not enough to detect this, as zero-context patches + // use the same position to delete the first line of a file + if f.IsDelete { + if err := applier.checkFullDelete(); err != nil { + return err + } + } return applier.Close() default: diff --git a/gitdiff/apply_test.go b/gitdiff/apply_test.go index c43e9c6..196bb50 100644 --- a/gitdiff/apply_test.go +++ b/gitdiff/apply_test.go @@ -19,6 +19,14 @@ func TestApplyTextFragment(t *testing.T) { "addEnd": {Files: getApplyFiles("text_fragment_add_end")}, "addEndNoEOL": {Files: getApplyFiles("text_fragment_add_end_noeol")}, + // zero-context fragments: the old range is empty and OldPosition is the + // line the content follows, not the line it starts at + "insertStart": {Files: getApplyFiles("text_fragment_insert_start")}, + "insertMiddle": {Files: getApplyFiles("text_fragment_insert_middle")}, + "insertEnd": {Files: getApplyFiles("text_fragment_insert_end")}, + "insertNoEOL": {Files: getApplyFiles("text_fragment_insert_noeol")}, + "deleteStart": {Files: getApplyFiles("text_fragment_delete_start")}, + "changeStart": {Files: getApplyFiles("text_fragment_change_start")}, "changeMiddle": {Files: getApplyFiles("text_fragment_change_middle")}, "changeEnd": {Files: getApplyFiles("text_fragment_change_end")}, @@ -68,13 +76,6 @@ func TestApplyTextFragment(t *testing.T) { }, Err: &Conflict{}, }, - "errorNewFile": { - Files: applyFiles{ - Src: "text_fragment_error.src", - Patch: "text_fragment_error_new_file.patch", - }, - Err: &Conflict{}, - }, } for name, test := range tests { @@ -200,6 +201,19 @@ func TestApplyFile(t *testing.T) { }, Err: &Conflict{}, }, + "textInsertZeroContext": { + Files: getApplyFiles("file_text_insert_zero_context"), + }, + "textDeleteStartZeroContext": { + Files: getApplyFiles("file_text_delete_start"), + }, + "textErrorNewNonEmpty": { + Files: applyFiles{ + Src: "file_text.src", + Patch: "file_text_error_new_non_empty.patch", + }, + Err: &Conflict{}, + }, "binaryModify": { Files: getApplyFiles("file_bin_modify"), }, diff --git a/gitdiff/apply_text.go b/gitdiff/apply_text.go index 0948178..e8c0ad2 100644 --- a/gitdiff/apply_text.go +++ b/gitdiff/apply_text.go @@ -64,8 +64,13 @@ func (a *TextApplier) ApplyFragment(f *TextFragment) error { return applyError(err) } - // lines are 0-indexed, positions are 1-indexed (but new files have position = 0) + // lines are 0-indexed, positions are 1-indexed; a fragment with no old + // lines inserts after OldPosition instead of starting at it, so position 0 + // inserts before the first line fragStart := f.OldPosition - 1 + if f.OldLines == 0 { + fragStart = f.OldPosition + } if fragStart < 0 { fragStart = 0 } @@ -79,16 +84,6 @@ func (a *TextApplier) ApplyFragment(f *TextFragment) error { return applyError(&Conflict{"fragment overlaps with an applied fragment"}) } - if f.OldPosition == 0 { - ok, err := isLen(a.src, 0) - if err != nil { - return applyError(err) - } - if !ok { - return applyError(&Conflict{"cannot create new file from non-empty src"}) - } - } - preimage, err := readPreimage(a.lineSrc, start, fragEnd-start) if err != nil { return applyError(err) @@ -116,18 +111,21 @@ func (a *TextApplier) ApplyFragment(f *TextFragment) error { } a.nextLine = fragStart + used - // new position of +0,0 mean a full delete, so check for leftovers - if f.NewPosition == 0 && f.NewLines == 0 { - var b [1][]byte - n, err := a.lineSrc.ReadLinesAt(b[:], a.nextLine) - if err != nil && err != io.EOF { - return applyError(err, lineNum(a.nextLine)) - } - if n > 0 { - return applyError(&Conflict{"src still has content after full delete"}, lineNum(a.nextLine)) - } - } + return nil +} +// checkFullDelete returns a *Conflict if the source contains content that was +// not consumed by the applied fragments. Deleting a file is a property of the +// file header, so only [Apply] can decide when this check applies. +func (a *TextApplier) checkFullDelete() error { + var b [1][]byte + n, err := a.lineSrc.ReadLinesAt(b[:], a.nextLine) + if err != nil && err != io.EOF { + return applyError(err, lineNum(a.nextLine)) + } + if n > 0 { + return applyError(&Conflict{"src still has content after full delete"}, lineNum(a.nextLine)) + } return nil } diff --git a/gitdiff/testdata/apply/file_text_delete_start.out b/gitdiff/testdata/apply/file_text_delete_start.out new file mode 100644 index 0000000..36642bb --- /dev/null +++ b/gitdiff/testdata/apply/file_text_delete_start.out @@ -0,0 +1,5 @@ +line 2 +line 3 +line 4 +line 5 +line 6 diff --git a/gitdiff/testdata/apply/file_text_delete_start.patch b/gitdiff/testdata/apply/file_text_delete_start.patch new file mode 100644 index 0000000..fe77006 --- /dev/null +++ b/gitdiff/testdata/apply/file_text_delete_start.patch @@ -0,0 +1,5 @@ +diff --git a/gitdiff/testdata/apply/file_text_delete_start.src b/gitdiff/testdata/apply/file_text_delete_start.src +--- a/gitdiff/testdata/apply/file_text_delete_start.src ++++ b/gitdiff/testdata/apply/file_text_delete_start.src +@@ -1 +0,0 @@ +-line 1 diff --git a/gitdiff/testdata/apply/file_text_delete_start.src b/gitdiff/testdata/apply/file_text_delete_start.src new file mode 100644 index 0000000..f985857 --- /dev/null +++ b/gitdiff/testdata/apply/file_text_delete_start.src @@ -0,0 +1,6 @@ +line 1 +line 2 +line 3 +line 4 +line 5 +line 6 diff --git a/gitdiff/testdata/apply/file_text_error_new_non_empty.patch b/gitdiff/testdata/apply/file_text_error_new_non_empty.patch new file mode 100644 index 0000000..cfdac1f --- /dev/null +++ b/gitdiff/testdata/apply/file_text_error_new_non_empty.patch @@ -0,0 +1,8 @@ +diff --git a/gitdiff/testdata/apply/file_text.src b/gitdiff/testdata/apply/file_text.src +new file mode 100644 +--- /dev/null ++++ b/gitdiff/testdata/apply/file_text.src +@@ -0,0 +1,3 @@ ++this is line 1 ++this is line 2 ++this is line 3 diff --git a/gitdiff/testdata/apply/file_text_insert_zero_context.out b/gitdiff/testdata/apply/file_text_insert_zero_context.out new file mode 100644 index 0000000..64c2f87 --- /dev/null +++ b/gitdiff/testdata/apply/file_text_insert_zero_context.out @@ -0,0 +1,8 @@ +new line a +line 1 +line 2 +line 3 +line 4 +line 5 +line 6 +new line b diff --git a/gitdiff/testdata/apply/file_text_insert_zero_context.patch b/gitdiff/testdata/apply/file_text_insert_zero_context.patch new file mode 100644 index 0000000..679e205 --- /dev/null +++ b/gitdiff/testdata/apply/file_text_insert_zero_context.patch @@ -0,0 +1,7 @@ +diff --git a/gitdiff/testdata/apply/file_text_insert_zero_context.src b/gitdiff/testdata/apply/file_text_insert_zero_context.src +--- a/gitdiff/testdata/apply/file_text_insert_zero_context.src ++++ b/gitdiff/testdata/apply/file_text_insert_zero_context.src +@@ -0,0 +1 @@ ++new line a +@@ -6,0 +8 @@ line 6 ++new line b diff --git a/gitdiff/testdata/apply/file_text_insert_zero_context.src b/gitdiff/testdata/apply/file_text_insert_zero_context.src new file mode 100644 index 0000000..f985857 --- /dev/null +++ b/gitdiff/testdata/apply/file_text_insert_zero_context.src @@ -0,0 +1,6 @@ +line 1 +line 2 +line 3 +line 4 +line 5 +line 6 diff --git a/gitdiff/testdata/apply/text_fragment_delete_start.out b/gitdiff/testdata/apply/text_fragment_delete_start.out new file mode 100644 index 0000000..e69de29 diff --git a/gitdiff/testdata/apply/text_fragment_delete_start.patch b/gitdiff/testdata/apply/text_fragment_delete_start.patch new file mode 100644 index 0000000..ea27f12 --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_delete_start.patch @@ -0,0 +1,5 @@ +diff --git a/gitdiff/testdata/apply/fragment_delete_start.src b/gitdiff/testdata/apply/fragment_delete_start.src +--- a/gitdiff/testdata/apply/fragment_delete_start.src ++++ b/gitdiff/testdata/apply/fragment_delete_start.src +@@ -1 +0,0 @@ +-line 1 diff --git a/gitdiff/testdata/apply/text_fragment_delete_start.src b/gitdiff/testdata/apply/text_fragment_delete_start.src new file mode 100644 index 0000000..a92d664 --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_delete_start.src @@ -0,0 +1,3 @@ +line 1 +line 2 +line 3 diff --git a/gitdiff/testdata/apply/text_fragment_error_new_file.patch b/gitdiff/testdata/apply/text_fragment_error_new_file.patch deleted file mode 100644 index f4fbee6..0000000 --- a/gitdiff/testdata/apply/text_fragment_error_new_file.patch +++ /dev/null @@ -1,7 +0,0 @@ -diff --git a/gitdiff/testdata/apply/text_fragment_error.src b/gitdiff/testdata/apply/text_fragment_error.src ---- a/gitdiff/testdata/apply/text_fragment_error.src -+++ b/gitdiff/testdata/apply/text_fragment_error.src -@@ -0,0 +1,3 @@ -+line 1 -+line 2 -+line 3 diff --git a/gitdiff/testdata/apply/text_fragment_insert_end.out b/gitdiff/testdata/apply/text_fragment_insert_end.out new file mode 100644 index 0000000..648fd44 --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_insert_end.out @@ -0,0 +1,5 @@ +line 1 +line 2 +line 3 +new line a +new line b diff --git a/gitdiff/testdata/apply/text_fragment_insert_end.patch b/gitdiff/testdata/apply/text_fragment_insert_end.patch new file mode 100644 index 0000000..5fd8e5c --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_insert_end.patch @@ -0,0 +1,6 @@ +diff --git a/gitdiff/testdata/apply/fragment_insert_end.src b/gitdiff/testdata/apply/fragment_insert_end.src +--- a/gitdiff/testdata/apply/fragment_insert_end.src ++++ b/gitdiff/testdata/apply/fragment_insert_end.src +@@ -3,0 +4,2 @@ line 3 ++new line a ++new line b diff --git a/gitdiff/testdata/apply/text_fragment_insert_end.src b/gitdiff/testdata/apply/text_fragment_insert_end.src new file mode 100644 index 0000000..a92d664 --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_insert_end.src @@ -0,0 +1,3 @@ +line 1 +line 2 +line 3 diff --git a/gitdiff/testdata/apply/text_fragment_insert_middle.out b/gitdiff/testdata/apply/text_fragment_insert_middle.out new file mode 100644 index 0000000..3c21168 --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_insert_middle.out @@ -0,0 +1,3 @@ +line 1 +line 2 +new line a diff --git a/gitdiff/testdata/apply/text_fragment_insert_middle.patch b/gitdiff/testdata/apply/text_fragment_insert_middle.patch new file mode 100644 index 0000000..9aaa829 --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_insert_middle.patch @@ -0,0 +1,5 @@ +diff --git a/gitdiff/testdata/apply/fragment_insert_middle.src b/gitdiff/testdata/apply/fragment_insert_middle.src +--- a/gitdiff/testdata/apply/fragment_insert_middle.src ++++ b/gitdiff/testdata/apply/fragment_insert_middle.src +@@ -2,0 +3 @@ line 2 ++new line a diff --git a/gitdiff/testdata/apply/text_fragment_insert_middle.src b/gitdiff/testdata/apply/text_fragment_insert_middle.src new file mode 100644 index 0000000..a92d664 --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_insert_middle.src @@ -0,0 +1,3 @@ +line 1 +line 2 +line 3 diff --git a/gitdiff/testdata/apply/text_fragment_insert_noeol.out b/gitdiff/testdata/apply/text_fragment_insert_noeol.out new file mode 100644 index 0000000..384d599 --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_insert_noeol.out @@ -0,0 +1,2 @@ +line 1 +new line a diff --git a/gitdiff/testdata/apply/text_fragment_insert_noeol.patch b/gitdiff/testdata/apply/text_fragment_insert_noeol.patch new file mode 100644 index 0000000..9424675 --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_insert_noeol.patch @@ -0,0 +1,5 @@ +diff --git a/gitdiff/testdata/apply/fragment_insert_noeol.src b/gitdiff/testdata/apply/fragment_insert_noeol.src +--- a/gitdiff/testdata/apply/fragment_insert_noeol.src ++++ b/gitdiff/testdata/apply/fragment_insert_noeol.src +@@ -1,0 +2 @@ line 1 ++new line a diff --git a/gitdiff/testdata/apply/text_fragment_insert_noeol.src b/gitdiff/testdata/apply/text_fragment_insert_noeol.src new file mode 100644 index 0000000..8cf2f17 --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_insert_noeol.src @@ -0,0 +1,3 @@ +line 1 +line 2 +line 3 \ No newline at end of file diff --git a/gitdiff/testdata/apply/text_fragment_insert_start.out b/gitdiff/testdata/apply/text_fragment_insert_start.out new file mode 100644 index 0000000..b202b92 --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_insert_start.out @@ -0,0 +1,2 @@ +new line a +new line b diff --git a/gitdiff/testdata/apply/text_fragment_insert_start.patch b/gitdiff/testdata/apply/text_fragment_insert_start.patch new file mode 100644 index 0000000..b7f9a7a --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_insert_start.patch @@ -0,0 +1,6 @@ +diff --git a/gitdiff/testdata/apply/fragment_insert_start.src b/gitdiff/testdata/apply/fragment_insert_start.src +--- a/gitdiff/testdata/apply/fragment_insert_start.src ++++ b/gitdiff/testdata/apply/fragment_insert_start.src +@@ -0,0 +1,2 @@ ++new line a ++new line b diff --git a/gitdiff/testdata/apply/text_fragment_insert_start.src b/gitdiff/testdata/apply/text_fragment_insert_start.src new file mode 100644 index 0000000..a92d664 --- /dev/null +++ b/gitdiff/testdata/apply/text_fragment_insert_start.src @@ -0,0 +1,3 @@ +line 1 +line 2 +line 3