this repo has no description
0
fork

Configure Feed

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

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.

+92 -100
+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