fork of go-gitdiff with jj support
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Move apply methods to an Applier type

This removes the distinction between "strict" and "fuzzy" application
by allowing future methods on Applier that control settings. It also
avoids state tracking in the text fragment apply signature by moving it
into the Applier type.

While in practice, an Applier will be used once and discarded, the
capability is provided to reset it for multiple uses.

+230 -100
+134 -79
gitdiff/apply.go
··· 1 package gitdiff 2 3 import ( 4 - "bytes" 5 "errors" 6 "fmt" 7 "io" 8 - "io/ioutil" 9 ) 10 11 // Conflict indicates an apply failed due to a conflict between the patch and ··· 85 return e 86 } 87 88 - // ApplyStrict writes data from src to dst, modifying it as described by the 89 - // fragments in the file. For text files, each fragment, including all context 90 - // lines, must exactly match src at the expected line number. 91 // 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 - } 105 - _, err = dst.Write(data) 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) ··· 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:] ··· 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) { ··· 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 208 - // fragment. 209 - // 210 - // Unlike text fragments, binary fragments do not distinguish between strict 211 - // and non-strict application. 212 - func (f *BinaryFragment) Apply(dst io.Writer, src io.ReaderAt) error { 213 switch f.Method { 214 case BinaryPatchLiteral: 215 if _, err := dst.Write(f.Data); err != nil { 216 return applyError(err) 217 } 218 case BinaryPatchDelta: 219 - if err := applyBinaryDeltaFragment(dst, src, f.Data); err != nil { 220 return applyError(err) 221 } 222 default: 223 return applyError(fmt.Errorf("unsupported binary patch method: %v", f.Method)) 224 } 225 - 226 return nil 227 } 228 229 func applyBinaryDeltaFragment(dst io.Writer, src io.ReaderAt, frag []byte) error { 230 srcSize, delta := readBinaryDeltaSize(frag) 231 - if err := checkBinarySrcSize(srcSize, src); err != nil { 232 return err 233 } 234 ··· 342 return size, delta, err 343 } 344 345 - func checkBinarySrcSize(size int64, src io.ReaderAt) error { 346 - start := size 347 - if start > 0 { 348 - start-- 349 } 350 - var b [2]byte 351 - n, err := src.ReadAt(b[:], start) 352 - if err == io.EOF && (size == 0 && n == 0) || (size > 0 && n == 1) { 353 - return nil 354 } 355 - if err != nil && err != io.EOF { 356 - return err 357 - } 358 - return &Conflict{"fragment src size does not match actual src size"} 359 } 360 361 func wrapEOF(err error) error {
··· 1 package gitdiff 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 ) 8 9 // Conflict indicates an apply failed due to a conflict between the patch and ··· 83 return e 84 } 85 86 + var ( 87 + errApplyInProgress = errors.New("gitdiff: incompatible apply in progress") 88 + ) 89 + 90 + const ( 91 + applyInitial = iota 92 + applyText 93 + applyBinary 94 + ) 95 + 96 + // Applier applies changes described in fragments to source data. If changes 97 + // are described in multiple fragments, those fragments must be applied in 98 + // order, usually by calling ApplyFile. 99 // 100 + // By default, Applier operates in "strict" mode, where fragment content and 101 + // positions must exactly match those of the source. 102 + // 103 + // If an error occurs while applying, methods on Applier return instances of 104 + // *ApplyError that annotate the wrapped error with additional information 105 + // when available. If the error is because of a conflict between a fragment and 106 + // the source, the wrapped error will be a *Conflict. 107 + // 108 + // While an Applier can apply both text and binary fragments, only one fragment 109 + // type can be used without resetting the Applier. The first fragment applied 110 + // sets the type for the Applier. Mixing fragment types or mixing 111 + // fragment-level and file-level applies results in an error. 112 + type Applier struct { 113 + src io.ReaderAt 114 + lineSrc LineReaderAt 115 + nextLine int64 116 + applyType int 117 + } 118 + 119 + // NewApplier creates an Applier that reads data from src. If src is a 120 + // LineReaderAt, it is used directly to apply text fragments. 121 + func NewApplier(src io.ReaderAt) *Applier { 122 + a := new(Applier) 123 + a.Reset(src) 124 + return a 125 + } 126 + 127 + // Reset resets the input and internal state of the Applier. If src is nil, the 128 + // existing source is reused. 129 + func (a *Applier) Reset(src io.ReaderAt) { 130 + if src != nil { 131 + a.src = src 132 + if lineSrc, ok := src.(LineReaderAt); ok { 133 + a.lineSrc = lineSrc 134 + } else { 135 + a.lineSrc = &lineReaderAt{r: src} 136 + } 137 } 138 + a.nextLine = 0 139 + a.applyType = applyInitial 140 + } 141 142 + // ApplyFile applies the changes in all of the fragments of f and writes the 143 + // result to dst. 144 + func (a *Applier) ApplyFile(dst io.Writer, f *File) error { 145 + if a.applyType != applyInitial { 146 + return applyError(errApplyInProgress) 147 } 148 149 + if f.IsBinary && f.BinaryFragment != nil { 150 + return a.ApplyBinaryFragment(dst, f.BinaryFragment) 151 + } 152 153 + // TODO(bkeyes): sort fragments by start position 154 + // TODO(bkeyes): merge overlapping fragments 155 156 for i, frag := range f.TextFragments { 157 + if err := a.ApplyTextFragment(dst, frag); err != nil { 158 return applyError(err, fragNum(i)) 159 } 160 } 161 162 + return applyError(a.Flush(dst)) 163 + } 164 165 + // ApplyTextFragment applies the changes in the fragment f and writes unwritten 166 + // data before the start of the fragment and the result to dst. If multiple 167 + // text fragments apply to the same source, ApplyTextFragment must be called in 168 + // order of increasing start position. As a result, each fragment can be 169 + // applied at most once before a call to Reset. 170 + func (a *Applier) ApplyTextFragment(dst io.Writer, f *TextFragment) error { 171 + switch a.applyType { 172 + case applyInitial, applyText: 173 + default: 174 + return applyError(errApplyInProgress) 175 } 176 + a.applyType = applyText 177 178 // application code assumes fragment fields are consistent 179 if err := f.Validate(); err != nil { 180 + return applyError(err) 181 } 182 183 // lines are 0-indexed, positions are 1-indexed (but new files have position = 0) ··· 187 } 188 fragEnd := fragStart + f.OldLines 189 190 + start := a.nextLine 191 if fragStart < start { 192 + return applyError(&Conflict{"fragment overlaps with an applied fragment"}) 193 + } 194 + 195 + if f.OldPosition == 0 { 196 + ok, err := isLen(a.src, 0) 197 + if err != nil { 198 + return applyError(err) 199 + } 200 + if !ok { 201 + return applyError(&Conflict{"cannot create new file from non-empty src"}) 202 + } 203 } 204 205 preimage := make([][]byte, fragEnd-start) 206 + n, err := a.lineSrc.ReadLinesAt(preimage, start) 207 switch { 208 case err == nil: 209 case err == io.EOF && n == len(preimage): // last line of frag has no newline character 210 default: 211 + return applyError(err, lineNum(start+int64(n))) 212 } 213 214 // copy leading data before the fragment starts 215 for i, line := range preimage[:fragStart-start] { 216 if _, err := dst.Write(line); err != nil { 217 + a.nextLine = start + int64(i) 218 + return applyError(err, lineNum(a.nextLine)) 219 } 220 } 221 preimage = preimage[fragStart-start:] ··· 224 used := int64(0) 225 for i, line := range f.Lines { 226 if err := applyTextLine(dst, line, preimage, used); err != nil { 227 + a.nextLine = fragStart + used 228 + return applyError(err, lineNum(a.nextLine), fragLineNum(i)) 229 } 230 if line.Old() { 231 used++ 232 } 233 } 234 + a.nextLine = fragStart + used 235 + return nil 236 } 237 238 func applyTextLine(dst io.Writer, line Line, preimage [][]byte, i int64) (err error) { ··· 242 if line.New() { 243 _, err = io.WriteString(dst, line.Line) 244 } 245 + return err 246 } 247 248 + // Flush writes any data following the last applied fragment to dst. 249 + func (a *Applier) Flush(dst io.Writer) (err error) { 250 + switch a.applyType { 251 + case applyInitial: 252 + _, err = copyFrom(dst, a.src, 0) 253 + case applyText: 254 + _, err = copyLinesFrom(dst, a.lineSrc, a.nextLine) 255 + case applyBinary: 256 + // nothing to flush, binary apply "consumes" full source 257 + } 258 + return err 259 + } 260 + 261 + // ApplyBinaryFragment applies the changes in the fragment f and writes the 262 + // result to dst. At most one binary fragment can be applied before a call to 263 + // Reset. 264 + func (a *Applier) ApplyBinaryFragment(dst io.Writer, f *BinaryFragment) error { 265 + if a.applyType != applyInitial { 266 + return applyError(errApplyInProgress) 267 + } 268 + a.applyType = applyText 269 + 270 + if f == nil { 271 + return applyError(errors.New("nil fragment")) 272 + } 273 + 274 switch f.Method { 275 case BinaryPatchLiteral: 276 if _, err := dst.Write(f.Data); err != nil { 277 return applyError(err) 278 } 279 case BinaryPatchDelta: 280 + if err := applyBinaryDeltaFragment(dst, a.src, f.Data); err != nil { 281 return applyError(err) 282 } 283 default: 284 return applyError(fmt.Errorf("unsupported binary patch method: %v", f.Method)) 285 } 286 return nil 287 } 288 289 func applyBinaryDeltaFragment(dst io.Writer, src io.ReaderAt, frag []byte) error { 290 srcSize, delta := readBinaryDeltaSize(frag) 291 + if err := checkBinarySrcSize(src, srcSize); err != nil { 292 return err 293 } 294 ··· 402 return size, delta, err 403 } 404 405 + func checkBinarySrcSize(r io.ReaderAt, size int64) error { 406 + ok, err := isLen(r, size) 407 + if err != nil { 408 + return err 409 } 410 + if !ok { 411 + return &Conflict{"fragment src size does not match actual src size"} 412 } 413 + return nil 414 } 415 416 func wrapEOF(err error) error {
+11 -15
gitdiff/apply_test.go
··· 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 { ··· 82 t.Fatalf("patch should contain exactly one fragment, but it has %d", len(files[0].TextFragments)) 83 } 84 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 ··· 153 t.Fatalf("patch should contain exactly one file, but it has %d", len(files)) 154 } 155 156 - frag := files[0].BinaryFragment 157 - if frag == nil { 158 - t.Fatalf("patch should contain a binary fragment, but it was nil") 159 - } 160 161 var dst bytes.Buffer 162 - err = frag.Apply(&dst, bytes.NewReader(src)) 163 if test.Err != nil { 164 checkApplyError(t, test.Err, err) 165 return
··· 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 { ··· 81 t.Fatalf("patch should contain exactly one fragment, but it has %d", len(files[0].TextFragments)) 82 } 83 84 + applier := NewApplier(bytes.NewReader(src)) 85 86 var dst bytes.Buffer 87 + err = applier.ApplyTextFragment(&dst, files[0].TextFragments[0]) 88 if test.Err != nil { 89 checkApplyError(t, test.Err, err) 90 return ··· 152 t.Fatalf("patch should contain exactly one file, but it has %d", len(files)) 153 } 154 155 + applier := NewApplier(bytes.NewReader(src)) 156 157 var dst bytes.Buffer 158 + err = applier.ApplyBinaryFragment(&dst, files[0].BinaryFragment) 159 if test.Err != nil { 160 checkApplyError(t, test.Err, err) 161 return
+6 -1
gitdiff/gitdiff.go
··· 1 package gitdiff 2 3 import ( 4 "fmt" 5 "os" 6 ) ··· 64 // Validate checks that the fragment is self-consistent and appliable. Validate 65 // returns an error if and only if the fragment is invalid. 66 func (f *TextFragment) Validate() error { 67 var ( 68 oldLines, newLines int64 69 leadingContext, trailingContext int64 ··· 117 118 // if a file is being created, it can only contain additions 119 if f.OldPosition == 0 && f.OldLines != 0 { 120 - return fmt.Errorf("file creation fragment contains context or deletion lines") 121 } 122 123 return nil
··· 1 package gitdiff 2 3 import ( 4 + "errors" 5 "fmt" 6 "os" 7 ) ··· 65 // Validate checks that the fragment is self-consistent and appliable. Validate 66 // returns an error if and only if the fragment is invalid. 67 func (f *TextFragment) Validate() error { 68 + if f == nil { 69 + return errors.New("nil fragment") 70 + } 71 + 72 var ( 73 oldLines, newLines int64 74 leadingContext, trailingContext int64 ··· 122 123 // if a file is being created, it can only contain additions 124 if f.OldPosition == 0 && f.OldLines != 0 { 125 + return errors.New("file creation fragment contains context or deletion lines") 126 } 127 128 return nil
+79 -5
gitdiff/io.go
··· 24 ReadLinesAt(lines [][]byte, offset int64) (n int, err error) 25 } 26 27 - // NewLineReaderAt creates a LineReaderAt from an io.ReaderAt. 28 - func NewLineReaderAt(r io.ReaderAt) LineReaderAt { 29 - return &lineReaderAt{r: r} 30 - } 31 - 32 type lineReaderAt struct { 33 r io.ReaderAt 34 index []int64 ··· 129 } 130 return 131 }
··· 24 ReadLinesAt(lines [][]byte, offset int64) (n int, err error) 25 } 26 27 type lineReaderAt struct { 28 r io.ReaderAt 29 index []int64 ··· 124 } 125 return 126 } 127 + 128 + func isLen(r io.ReaderAt, n int64) (bool, error) { 129 + off := n - 1 130 + if off < 0 { 131 + off = 0 132 + } 133 + 134 + var b [2]byte 135 + nr, err := r.ReadAt(b[:], off) 136 + if err == io.EOF { 137 + return (n == 0 && nr == 0) || (n > 0 && nr == 1), nil 138 + } 139 + return false, err 140 + } 141 + 142 + // copyFrom writes bytes starting from offset off in src to dst stopping at the 143 + // end of src or at the first error. copyFrom returns the number of bytes 144 + // written and any error. 145 + func copyFrom(dst io.Writer, src io.ReaderAt, off int64) (written int64, err error) { 146 + buf := make([]byte, 32*1024) // stolen from io.Copy 147 + for { 148 + nr, rerr := src.ReadAt(buf, off) 149 + if nr > 0 { 150 + nw, werr := dst.Write(buf[0:nr]) 151 + if nw > 0 { 152 + written += int64(nw) 153 + } 154 + if werr != nil { 155 + err = werr 156 + break 157 + } 158 + if nr != nw { 159 + err = io.ErrShortWrite 160 + break 161 + } 162 + } 163 + if rerr != nil { 164 + if rerr != io.EOF { 165 + err = rerr 166 + } 167 + break 168 + } 169 + } 170 + return written, err 171 + } 172 + 173 + // copyLinesFrom writes lines starting from line off in src to dst stopping at 174 + // the end of src or at the first error. copyLinesFrom returns the number of 175 + // lines written and any error. 176 + func copyLinesFrom(dst io.Writer, src LineReaderAt, off int64) (written int64, err error) { 177 + buf := make([][]byte, 32) 178 + ReadLoop: 179 + for { 180 + nr, rerr := src.ReadLinesAt(buf, off) 181 + if nr > 0 { 182 + for _, line := range buf[0:nr] { 183 + nw, werr := dst.Write(line) 184 + if nw > 0 { 185 + written++ 186 + } 187 + if werr != nil { 188 + err = werr 189 + break ReadLoop 190 + } 191 + if len(line) != nw { 192 + err = io.ErrShortWrite 193 + break ReadLoop 194 + } 195 + } 196 + } 197 + if rerr != nil { 198 + if rerr != io.EOF { 199 + err = rerr 200 + } 201 + break 202 + } 203 + } 204 + return written, err 205 + }