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 39 // additional location information, if it is available. 40 40 type ApplyError struct { 41 41 // Line is the one-indexed line number in the source data 42 - Line int 42 + Line int64 43 43 // Fragment is the one-indexed fragment number in the file 44 44 Fragment int 45 45 // FragmentLine is the one-indexed line number in the fragment ··· 75 75 for _, arg := range args { 76 76 switch v := arg.(type) { 77 77 case lineNum: 78 - e.Line = int(v) + 1 78 + e.Line = int64(v) + 1 79 79 case fragNum: 80 80 e.Fragment = int(v) + 1 81 81 case fragLineNum: ··· 92 92 // If the apply fails, ApplyStrict returns an *ApplyError wrapping the cause. 93 93 // Partial data may be written to dst in this case. 94 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 + 95 101 if f.IsBinary { 96 - data, err := ioutil.ReadAll(src) 97 - if err != nil { 98 - return applyError(err) 99 - } 100 102 if f.BinaryFragment != nil { 101 103 return f.BinaryFragment.Apply(dst, bytes.NewReader(data)) 102 104 } ··· 104 106 return applyError(err) 105 107 } 106 108 107 - lr, ok := src.(LineReader) 108 - if !ok { 109 - lr = NewLineReader(src, 0) 110 - } 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)) 111 113 114 + var next int64 112 115 for i, frag := range f.TextFragments { 113 - if err := frag.ApplyStrict(dst, lr); err != nil { 116 + next, err = frag.ApplyStrict(dst, lra, next) 117 + if err != nil { 114 118 return applyError(err, fragNum(i)) 115 119 } 116 120 } 117 121 118 - _, err := io.Copy(dst, unwrapLineReader(lr)) 119 - return applyError(err) 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 + } 120 141 } 121 142 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 { 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) { 131 149 // application code assumes fragment fields are consistent 132 150 if err := f.Validate(); err != nil { 133 - return applyError(err) 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"}) 134 163 } 135 164 136 - // line numbers are zero-indexed, positions are one-indexed 137 - limit := f.OldPosition - 1 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 + } 138 173 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)) 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 + } 144 180 } 181 + preimage = preimage[fragStart-start:] 145 182 183 + // apply the changes in the fragment 146 184 used := int64(0) 147 185 for i, line := range f.Lines { 148 - if err := applyTextLine(dst, nextLine, line); err != nil { 149 - return applyError(err, lineNum(n), fragLineNum(i)) 186 + if err := applyTextLine(dst, line, preimage, used); err != nil { 187 + next = fragStart + used 188 + return next, applyError(err, lineNum(next), fragLineNum(i)) 150 189 } 151 190 if line.Old() { 152 191 used++ 153 192 } 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 193 } 165 - 166 - return nil 194 + return fragStart + used, nil 167 195 } 168 196 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 - } 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"} 175 200 } 176 - switch line.Op { 177 - case OpContext, OpAdd: 201 + if line.New() { 178 202 _, err = io.WriteString(dst, line.Line) 179 203 } 180 204 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 205 } 209 206 210 207 // Apply writes data from src to dst, modifying it as described by the
+9 -8
gitdiff/apply_test.go
··· 57 57 }, 58 58 Err: &Conflict{}, 59 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 - }, 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 + // }, 67 68 } 68 69 69 70 for name, test := range tests { ··· 84 85 frag := files[0].TextFragments[0] 85 86 86 87 var dst bytes.Buffer 87 - err = frag.ApplyStrict(&dst, NewLineReader(bytes.NewReader(src), 0)) 88 + _, err = frag.ApplyStrict(&dst, NewLineReaderAt(bytes.NewReader(src)), 0) 88 89 if test.Err != nil { 89 90 checkApplyError(t, test.Err, err) 90 91 return
+2 -2
gitdiff/gitdiff.go
··· 139 139 140 140 // Old returns true if the line appears in the old content of the fragment. 141 141 func (fl Line) Old() bool { 142 - return fl.Op != OpAdd 142 + return fl.Op == OpContext || fl.Op == OpDelete 143 143 } 144 144 145 145 // New returns true if the line appears in the new content of the fragment. 146 146 func (fl Line) New() bool { 147 - return fl.Op == OpAdd 147 + return fl.Op == OpContext || fl.Op == OpAdd 148 148 } 149 149 150 150 // NoEOL returns true if the line is missing a trailing newline character.
+5 -11
gitdiff/io.go
··· 128 128 return 0, io.EOF 129 129 } 130 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)) 131 + size, readOffset := lookupLines(r.index, offset, int64(len(lines))) 137 132 138 133 b := make([]byte, size) 139 134 if _, err := r.r.ReadAt(b, readOffset); err != nil { ··· 144 139 } 145 140 146 141 for n = 0; n < len(lines) && offset+int64(n) < int64(len(r.index)); n++ { 147 - i := int(offset) + n 142 + i := offset + int64(n) 148 143 start, end := readOffset, r.index[i] 149 144 if i > 0 { 150 145 start = r.index[i-1] ··· 193 188 194 189 // lookupLines gets the byte offset and size of a range of lines from an index 195 190 // 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) { 191 + func lookupLines(index []int64, start, n int64) (size int64, offset int64) { 192 + if start > int64(len(index)) { 198 193 offset = index[len(index)-1] 199 194 } else if start > 0 { 200 195 offset = index[start-1] 201 196 } 202 197 if n > 0 { 203 - // TODO(bkeyes): check types for overflow 204 - if start+n > len(index) { 198 + if start+n > int64(len(index)) { 205 199 size = index[len(index)-1] - offset 206 200 } else { 207 201 size = index[start+n-1] - offset