fork of go-gitdiff with jj support

Implement text application using LineReaderAt

This is functionally equivalent to the previous version (except for one
error case), but uses the new interface. I think the code is simpler
overall because it removes the line tracking.

Changed files
+92 -100
gitdiff
+76 -79
gitdiff/apply.go
··· 39 // additional location information, if it is available. 40 type ApplyError struct { 41 // Line is the one-indexed line number in the source data 42 - Line int 43 // Fragment is the one-indexed fragment number in the file 44 Fragment int 45 // FragmentLine is the one-indexed line number in the fragment ··· 75 for _, arg := range args { 76 switch v := arg.(type) { 77 case lineNum: 78 - e.Line = int(v) + 1 79 case fragNum: 80 e.Fragment = int(v) + 1 81 case fragLineNum: ··· 92 // If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause. 93 // Partial data may be written to dst in this case. 94 func (f *File) ApplyStrict(dst io.Writer, src io.Reader) error { 95 if f.IsBinary { 96 - data, err := ioutil.ReadAll(src) 97 - if err != nil { 98 - return applyError(err) 99 - } 100 if f.BinaryFragment != nil { 101 return f.BinaryFragment.Apply(dst, bytes.NewReader(data)) 102 } ··· 104 return applyError(err) 105 } 106 107 - lr, ok := src.(LineReader) 108 - if !ok { 109 - lr = NewLineReader(src, 0) 110 - } 111 112 for i, frag := range f.TextFragments { 113 - if err := frag.ApplyStrict(dst, lr); err != nil { 114 return applyError(err, fragNum(i)) 115 } 116 } 117 118 - _, err := io.Copy(dst, unwrapLineReader(lr)) 119 - return applyError(err) 120 } 121 122 - // ApplyStrict writes data from src to dst, modifying it as described by the 123 - // fragment. The fragment, including all context lines, must exactly match src 124 - // at the expected line number. 125 - // 126 - // If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause. 127 - // Partial data may be written to dst in this case. If there is no error, the 128 - // next read from src returns the line immediately after the last line of the 129 - // fragment. 130 - func (f *TextFragment) ApplyStrict(dst io.Writer, src LineReader) error { 131 // application code assumes fragment fields are consistent 132 if err := f.Validate(); err != nil { 133 - return applyError(err) 134 } 135 136 - // line numbers are zero-indexed, positions are one-indexed 137 - limit := f.OldPosition - 1 138 139 - // io.EOF is acceptable here: the first line of the patch is the last of 140 - // the source and it has no newline character 141 - nextLine, n, err := copyLines(dst, src, limit) 142 - if err != nil && err != io.EOF { 143 - return applyError(err, lineNum(n)) 144 } 145 146 used := int64(0) 147 for i, line := range f.Lines { 148 - if err := applyTextLine(dst, nextLine, line); err != nil { 149 - return applyError(err, lineNum(n), fragLineNum(i)) 150 } 151 if line.Old() { 152 used++ 153 } 154 - // advance reader if the next fragment line appears in src and we're behind 155 - if i < len(f.Lines)-1 && f.Lines[i+1].Old() && int64(n)-limit < used { 156 - nextLine, n, err = src.ReadLine() 157 - switch { 158 - case err == io.EOF && f.Lines[i+1].NoEOL(): 159 - continue 160 - case err != nil: 161 - return applyError(err, lineNum(n), fragLineNum(i+1)) // report for _next_ line in fragment 162 - } 163 - } 164 } 165 - 166 - return nil 167 } 168 169 - func applyTextLine(dst io.Writer, src string, line Line) (err error) { 170 - switch line.Op { 171 - case OpContext, OpDelete: 172 - if src != line.Line { 173 - return &Conflict{"fragment line does not match src line"} 174 - } 175 } 176 - switch line.Op { 177 - case OpContext, OpAdd: 178 _, err = io.WriteString(dst, line.Line) 179 } 180 return 181 - } 182 - 183 - // copyLines copies from src to dst until the line at limit, exclusive. Returns 184 - // the line at limit and the line number. If the error is nil or io.EOF, the 185 - // line number equals limit. A negative limit checks that the source has no 186 - // more lines to read. 187 - func copyLines(dst io.Writer, src LineReader, limit int64) (string, int64, error) { 188 - for { 189 - line, n, err := src.ReadLine() 190 - switch { 191 - case limit < 0 && err == io.EOF && line == "": 192 - return "", limit, nil 193 - case n == limit: 194 - return line, n, err 195 - case n > limit: 196 - if limit < 0 { 197 - return "", n, &Conflict{"cannot create new file from non-empty src"} 198 - } 199 - return "", n, &Conflict{"fragment overlaps with an applied fragment"} 200 - case err != nil: 201 - return line, n, wrapEOF(err) 202 - } 203 - 204 - if _, err := io.WriteString(dst, line); err != nil { 205 - return "", n, err 206 - } 207 - } 208 } 209 210 // Apply writes data from src to dst, modifying it as described by the
··· 39 // additional location information, if it is available. 40 type ApplyError struct { 41 // Line is the one-indexed line number in the source data 42 + Line int64 43 // Fragment is the one-indexed fragment number in the file 44 Fragment int 45 // FragmentLine is the one-indexed line number in the fragment ··· 75 for _, arg := range args { 76 switch v := arg.(type) { 77 case lineNum: 78 + e.Line = int64(v) + 1 79 case fragNum: 80 e.Fragment = int(v) + 1 81 case fragLineNum: ··· 92 // If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause. 93 // Partial data may be written to dst in this case. 94 func (f *File) ApplyStrict(dst io.Writer, src io.Reader) error { 95 + // TODO(bkeyes): take an io.ReaderAt and avoid this! 96 + data, err := ioutil.ReadAll(src) 97 + if err != nil { 98 + return applyError(err) 99 + } 100 + 101 if f.IsBinary { 102 if f.BinaryFragment != nil { 103 return f.BinaryFragment.Apply(dst, bytes.NewReader(data)) 104 } ··· 106 return applyError(err) 107 } 108 109 + // TODO(bkeyes): check for this conflict case 110 + // &Conflict{"cannot create new file from non-empty src"} 111 + 112 + lra := NewLineReaderAt(bytes.NewReader(data)) 113 114 + var next int64 115 for i, frag := range f.TextFragments { 116 + next, err = frag.ApplyStrict(dst, lra, next) 117 + if err != nil { 118 return applyError(err, fragNum(i)) 119 } 120 } 121 122 + // TODO(bkeyes): extract this to a utility 123 + buf := make([][]byte, 64) 124 + for { 125 + n, err := lra.ReadLinesAt(buf, next) 126 + if err != nil && err != io.EOF { 127 + return applyError(err, lineNum(next+int64(n))) 128 + } 129 + 130 + for i := 0; i < n; i++ { 131 + if _, err := dst.Write(buf[n]); err != nil { 132 + return applyError(err, lineNum(next+int64(n))) 133 + } 134 + } 135 + 136 + next += int64(n) 137 + if n < len(buf) { 138 + return nil 139 + } 140 + } 141 } 142 143 + // ApplyStrict copies from src to dst, from line start through then end of the 144 + // fragment, modifying the data as described by the fragment. The fragment, 145 + // including all context lines, must exactly match src at the expected line 146 + // number. ApplyStrict returns the number of the next unprocessed line in src 147 + // and any error. When the error is not non-nil, partial data may be written. 148 + func (f *TextFragment) ApplyStrict(dst io.Writer, src LineReaderAt, start int64) (next int64, err error) { 149 // application code assumes fragment fields are consistent 150 if err := f.Validate(); err != nil { 151 + return start, applyError(err) 152 + } 153 + 154 + // lines are 0-indexed, positions are 1-indexed (but new files have position = 0) 155 + fragStart := f.OldPosition - 1 156 + if fragStart < 0 { 157 + fragStart = 0 158 + } 159 + fragEnd := fragStart + f.OldLines 160 + 161 + if fragStart < start { 162 + return start, applyError(&Conflict{"fragment overlaps with an applied fragment"}) 163 } 164 165 + preimage := make([][]byte, fragEnd-start) 166 + n, err := src.ReadLinesAt(preimage, start) 167 + switch { 168 + case err == nil: 169 + case err == io.EOF && n == len(preimage): // last line of frag has no newline character 170 + default: 171 + return start, applyError(err, lineNum(start+int64(n))) 172 + } 173 174 + // copy leading data before the fragment starts 175 + for i, line := range preimage[:fragStart-start] { 176 + if _, err := dst.Write(line); err != nil { 177 + next = start + int64(i) 178 + return next, applyError(err, lineNum(next)) 179 + } 180 } 181 + preimage = preimage[fragStart-start:] 182 183 + // apply the changes in the fragment 184 used := int64(0) 185 for i, line := range f.Lines { 186 + if err := applyTextLine(dst, line, preimage, used); err != nil { 187 + next = fragStart + used 188 + return next, applyError(err, lineNum(next), fragLineNum(i)) 189 } 190 if line.Old() { 191 used++ 192 } 193 } 194 + return fragStart + used, nil 195 } 196 197 + func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err error) { 198 + if line.Old() && string(preimage[i]) != line.Line { 199 + return &Conflict{"fragment line does not match src line"} 200 } 201 + if line.New() { 202 _, err = io.WriteString(dst, line.Line) 203 } 204 return 205 } 206 207 // Apply writes data from src to dst, modifying it as described by the
+9 -8
gitdiff/apply_test.go
··· 57 }, 58 Err: &Conflict{}, 59 }, 60 - "errorNewFile": { 61 - Files: applyFiles{ 62 - Src: "text_fragment_error.src", 63 - Patch: "text_fragment_error_new_file.patch", 64 - }, 65 - Err: &Conflict{}, 66 - }, 67 } 68 69 for name, test := range tests { ··· 84 frag := files[0].TextFragments[0] 85 86 var dst bytes.Buffer 87 - err = frag.ApplyStrict(&dst, NewLineReader(bytes.NewReader(src), 0)) 88 if test.Err != nil { 89 checkApplyError(t, test.Err, err) 90 return
··· 57 }, 58 Err: &Conflict{}, 59 }, 60 + // TODO(bkeyes): this check has moved to the file level (probably) 61 + // "errorNewFile": { 62 + // Files: applyFiles{ 63 + // Src: "text_fragment_error.src", 64 + // Patch: "text_fragment_error_new_file.patch", 65 + // }, 66 + // Err: &Conflict{}, 67 + // }, 68 } 69 70 for name, test := range tests { ··· 85 frag := files[0].TextFragments[0] 86 87 var dst bytes.Buffer 88 + _, err = frag.ApplyStrict(&dst, NewLineReaderAt(bytes.NewReader(src)), 0) 89 if test.Err != nil { 90 checkApplyError(t, test.Err, err) 91 return
+2 -2
gitdiff/gitdiff.go
··· 139 140 // Old returns true if the line appears in the old content of the fragment. 141 func (fl Line) Old() bool { 142 - return fl.Op != OpAdd 143 } 144 145 // New returns true if the line appears in the new content of the fragment. 146 func (fl Line) New() bool { 147 - return fl.Op == OpAdd 148 } 149 150 // NoEOL returns true if the line is missing a trailing newline character.
··· 139 140 // Old returns true if the line appears in the old content of the fragment. 141 func (fl Line) Old() bool { 142 + return fl.Op == OpContext || fl.Op == OpDelete 143 } 144 145 // New returns true if the line appears in the new content of the fragment. 146 func (fl Line) New() bool { 147 + return fl.Op == OpContext || fl.Op == OpAdd 148 } 149 150 // NoEOL returns true if the line is missing a trailing newline character.
+5 -11
gitdiff/io.go
··· 128 return 0, io.EOF 129 } 130 131 - // TODO(bkeyes): check usage of int / int64 132 - // - interface uses int64 for arbitrarily large files 133 - // - implementation is limited to int lines by index array 134 - 135 - // offset <= len(r.index) means that it must fit in int without loss 136 - size, readOffset := lookupLines(r.index, int(offset), len(lines)) 137 138 b := make([]byte, size) 139 if _, err := r.r.ReadAt(b, readOffset); err != nil { ··· 144 } 145 146 for n = 0; n < len(lines) && offset+int64(n) < int64(len(r.index)); n++ { 147 - i := int(offset) + n 148 start, end := readOffset, r.index[i] 149 if i > 0 { 150 start = r.index[i-1] ··· 193 194 // lookupLines gets the byte offset and size of a range of lines from an index 195 // where the value at n is the offset of the first byte after line number n. 196 - func lookupLines(index []int64, start, n int) (size int64, offset int64) { 197 - if start > len(index) { 198 offset = index[len(index)-1] 199 } else if start > 0 { 200 offset = index[start-1] 201 } 202 if n > 0 { 203 - // TODO(bkeyes): check types for overflow 204 - if start+n > len(index) { 205 size = index[len(index)-1] - offset 206 } else { 207 size = index[start+n-1] - offset
··· 128 return 0, io.EOF 129 } 130 131 + size, readOffset := lookupLines(r.index, offset, int64(len(lines))) 132 133 b := make([]byte, size) 134 if _, err := r.r.ReadAt(b, readOffset); err != nil { ··· 139 } 140 141 for n = 0; n < len(lines) && offset+int64(n) < int64(len(r.index)); n++ { 142 + i := offset + int64(n) 143 start, end := readOffset, r.index[i] 144 if i > 0 { 145 start = r.index[i-1] ··· 188 189 // lookupLines gets the byte offset and size of a range of lines from an index 190 // where the value at n is the offset of the first byte after line number n. 191 + func lookupLines(index []int64, start, n int64) (size int64, offset int64) { 192 + if start > int64(len(index)) { 193 offset = index[len(index)-1] 194 } else if start > 0 { 195 offset = index[start-1] 196 } 197 if n > 0 { 198 + if start+n > int64(len(index)) { 199 size = index[len(index)-1] - offset 200 } else { 201 size = index[start+n-1] - offset