this repo has no description
0
fork

Configure Feed

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

Split source files by patch type (text/binary)

The tests were already split up in this way, so it makes sense to have
the parsing functions split as well.

+406 -392
+179
gitdiff/binary.go
··· 1 + package gitdiff 2 + 3 + import ( 4 + "bytes" 5 + "compress/zlib" 6 + "fmt" 7 + "io" 8 + "io/ioutil" 9 + "strconv" 10 + "strings" 11 + ) 12 + 13 + func (p *parser) ParseBinaryFragments(f *File) (n int, err error) { 14 + isBinary, hasData, err := p.ParseBinaryMarker() 15 + if err != nil || !isBinary { 16 + return 0, err 17 + } 18 + 19 + f.IsBinary = true 20 + if !hasData { 21 + return 0, nil 22 + } 23 + 24 + forward, err := p.ParseBinaryFragmentHeader() 25 + if err != nil { 26 + return 0, err 27 + } 28 + if forward == nil { 29 + return 0, p.Errorf(0, "missing data for binary patch") 30 + } 31 + if err := p.ParseBinaryChunk(forward); err != nil { 32 + return 0, err 33 + } 34 + f.BinaryFragment = forward 35 + 36 + // valid for reverse to not exist, but it must be valid if present 37 + reverse, err := p.ParseBinaryFragmentHeader() 38 + if err != nil { 39 + return 1, err 40 + } 41 + if reverse == nil { 42 + return 1, nil 43 + } 44 + if err := p.ParseBinaryChunk(reverse); err != nil { 45 + return 1, err 46 + } 47 + f.ReverseBinaryFragment = reverse 48 + 49 + return 1, nil 50 + } 51 + 52 + func (p *parser) ParseBinaryMarker() (isBinary bool, hasData bool, err error) { 53 + switch p.Line(0) { 54 + case "GIT binary patch\n": 55 + hasData = true 56 + case "Binary files differ\n": 57 + case "Files differ\n": 58 + default: 59 + return false, false, nil 60 + } 61 + 62 + if err = p.Next(); err != nil && err != io.EOF { 63 + return false, false, err 64 + } 65 + return true, hasData, nil 66 + } 67 + 68 + func (p *parser) ParseBinaryFragmentHeader() (*BinaryFragment, error) { 69 + parts := strings.SplitN(strings.TrimSuffix(p.Line(0), "\n"), " ", 2) 70 + if len(parts) < 2 { 71 + return nil, nil 72 + } 73 + 74 + frag := &BinaryFragment{} 75 + switch parts[0] { 76 + case "delta": 77 + frag.Method = BinaryPatchDelta 78 + case "literal": 79 + frag.Method = BinaryPatchLiteral 80 + default: 81 + return nil, nil 82 + } 83 + 84 + var err error 85 + if frag.Size, err = strconv.ParseInt(parts[1], 10, 64); err != nil { 86 + nerr := err.(*strconv.NumError) 87 + return nil, p.Errorf(0, "binary patch: invalid size: %v", nerr.Err) 88 + } 89 + 90 + if err := p.Next(); err != nil && err != io.EOF { 91 + return nil, err 92 + } 93 + return frag, nil 94 + } 95 + 96 + func (p *parser) ParseBinaryChunk(frag *BinaryFragment) error { 97 + // Binary fragments are encoded as a series of base85 encoded lines. Each 98 + // line starts with a character in [A-Za-z] giving the number of bytes on 99 + // the line, where A = 1 and z = 52, and ends with a newline character. 100 + // 101 + // The base85 encoding means each line is a multiple of 5 characters + 2 102 + // additional characters for the length byte and the newline. The fragment 103 + // ends with a blank line. 104 + const ( 105 + shortestValidLine = "A00000\n" 106 + maxBytesPerLine = 52 107 + ) 108 + 109 + var data bytes.Buffer 110 + buf := make([]byte, maxBytesPerLine) 111 + for { 112 + line := p.Line(0) 113 + if line == "\n" { 114 + break 115 + } 116 + if len(line) < len(shortestValidLine) || (len(line)-2)%5 != 0 { 117 + return p.Errorf(0, "binary patch: corrupt data line") 118 + } 119 + 120 + byteCount, seq := int(line[0]), line[1:len(line)-1] 121 + switch { 122 + case 'A' <= byteCount && byteCount <= 'Z': 123 + byteCount = byteCount - 'A' + 1 124 + case 'a' <= byteCount && byteCount <= 'z': 125 + byteCount = byteCount - 'a' + 27 126 + default: 127 + return p.Errorf(0, "binary patch: invalid length byte") 128 + } 129 + 130 + // base85 encodes every 4 bytes into 5 characters, with up to 3 bytes of end padding 131 + maxByteCount := len(seq) / 5 * 4 132 + if byteCount > maxByteCount || byteCount < maxByteCount-3 { 133 + return p.Errorf(0, "binary patch: incorrect byte count") 134 + } 135 + 136 + if err := base85Decode(buf[:byteCount], []byte(seq)); err != nil { 137 + return p.Errorf(0, "binary patch: %v", err) 138 + } 139 + data.Write(buf[:byteCount]) 140 + 141 + if err := p.Next(); err != nil { 142 + if err == io.EOF { 143 + return p.Errorf(0, "binary patch: unexpected EOF") 144 + } 145 + return err 146 + } 147 + } 148 + 149 + if err := inflateBinaryChunk(frag, &data); err != nil { 150 + return p.Errorf(0, "binary patch: %v", err) 151 + } 152 + 153 + // consume the empty line that ended the fragment 154 + if err := p.Next(); err != nil && err != io.EOF { 155 + return err 156 + } 157 + return nil 158 + } 159 + 160 + func inflateBinaryChunk(frag *BinaryFragment, r io.Reader) error { 161 + zr, err := zlib.NewReader(r) 162 + if err != nil { 163 + return err 164 + } 165 + 166 + data, err := ioutil.ReadAll(zr) 167 + if err != nil { 168 + return err 169 + } 170 + if err := zr.Close(); err != nil { 171 + return err 172 + } 173 + 174 + if int64(len(data)) != frag.Size { 175 + return fmt.Errorf("%d byte fragment inflated to %d", frag.Size, len(data)) 176 + } 177 + frag.Data = data 178 + return nil 179 + }
+47
gitdiff/file_header.go
··· 13 13 devNull = "/dev/null" 14 14 ) 15 15 16 + // ParseNextFileHeader finds and parses the next file header in the stream. If 17 + // a header is found, it returns a file and all input before the header. It 18 + // returns nil if no headers are found before the end of the input. 19 + func (p *parser) ParseNextFileHeader() (*File, string, error) { 20 + var preamble strings.Builder 21 + var file *File 22 + for { 23 + // check for disconnected fragment headers (corrupt patch) 24 + frag, err := p.ParseTextFragmentHeader() 25 + if err != nil { 26 + // not a valid header, nothing to worry about 27 + goto NextLine 28 + } 29 + if frag != nil { 30 + return nil, "", p.Errorf(-1, "patch fragment without file header: %s", frag.Header()) 31 + } 32 + 33 + // check for a git-generated patch 34 + file, err = p.ParseGitFileHeader() 35 + if err != nil { 36 + return nil, "", err 37 + } 38 + if file != nil { 39 + return file, preamble.String(), nil 40 + } 41 + 42 + // check for a "traditional" patch 43 + file, err = p.ParseTraditionalFileHeader() 44 + if err != nil { 45 + return nil, "", err 46 + } 47 + if file != nil { 48 + return file, preamble.String(), nil 49 + } 50 + 51 + NextLine: 52 + preamble.WriteString(p.Line(0)) 53 + if err := p.Next(); err != nil { 54 + if err == io.EOF { 55 + break 56 + } 57 + return nil, "", err 58 + } 59 + } 60 + return nil, "", nil 61 + } 62 + 16 63 func (p *parser) ParseGitFileHeader() (*File, error) { 17 64 const prefix = "diff --git " 18 65
-392
gitdiff/parser.go
··· 5 5 6 6 import ( 7 7 "bufio" 8 - "bytes" 9 - "compress/zlib" 10 8 "fmt" 11 9 "io" 12 - "io/ioutil" 13 - "strconv" 14 - "strings" 15 10 ) 16 11 17 12 // Parse parses a patch with changes to one or more files. Any content before ··· 79 74 lines [3]string 80 75 } 81 76 82 - // ParseNextFileHeader finds and parses the next file header in the stream. If 83 - // a header is found, it returns a file and all input before the header. It 84 - // returns nil if no headers are found before the end of the input. 85 - func (p *parser) ParseNextFileHeader() (*File, string, error) { 86 - var preamble strings.Builder 87 - var file *File 88 - for { 89 - // check for disconnected fragment headers (corrupt patch) 90 - frag, err := p.ParseTextFragmentHeader() 91 - if err != nil { 92 - // not a valid header, nothing to worry about 93 - goto NextLine 94 - } 95 - if frag != nil { 96 - return nil, "", p.Errorf(-1, "patch fragment without file header: %s", frag.Header()) 97 - } 98 - 99 - // check for a git-generated patch 100 - file, err = p.ParseGitFileHeader() 101 - if err != nil { 102 - return nil, "", err 103 - } 104 - if file != nil { 105 - return file, preamble.String(), nil 106 - } 107 - 108 - // check for a "traditional" patch 109 - file, err = p.ParseTraditionalFileHeader() 110 - if err != nil { 111 - return nil, "", err 112 - } 113 - if file != nil { 114 - return file, preamble.String(), nil 115 - } 116 - 117 - NextLine: 118 - preamble.WriteString(p.Line(0)) 119 - if err := p.Next(); err != nil { 120 - if err == io.EOF { 121 - break 122 - } 123 - return nil, "", err 124 - } 125 - } 126 - return nil, "", nil 127 - } 128 - 129 - // ParseTextFragments parses text fragments until the next file header or the 130 - // end of the stream and attaches them to the given file. It returns the number 131 - // of fragments that were added. 132 - func (p *parser) ParseTextFragments(f *File) (n int, err error) { 133 - for { 134 - frag, err := p.ParseTextFragmentHeader() 135 - if err != nil { 136 - return n, err 137 - } 138 - if frag == nil { 139 - return n, nil 140 - } 141 - 142 - if f.IsNew && frag.OldLines > 0 { 143 - return n, p.Errorf(-1, "new file depends on old contents") 144 - } 145 - if f.IsDelete && frag.NewLines > 0 { 146 - return n, p.Errorf(-1, "deleted file still has contents") 147 - } 148 - 149 - if err := p.ParseTextChunk(frag); err != nil { 150 - return n, err 151 - } 152 - 153 - f.TextFragments = append(f.TextFragments, frag) 154 - n++ 155 - } 156 - } 157 - 158 77 // Next advances the parser by one line. It returns any error encountered while 159 78 // reading the line, including io.EOF when the end of stream is reached. 160 79 func (p *parser) Next() error { ··· 205 124 func (p *parser) Errorf(delta int64, msg string, args ...interface{}) error { 206 125 return fmt.Errorf("gitdiff: line %d: %s", p.lineno+delta, fmt.Sprintf(msg, args...)) 207 126 } 208 - 209 - func (p *parser) ParseTextFragmentHeader() (*TextFragment, error) { 210 - const ( 211 - startMark = "@@ -" 212 - endMark = " @@" 213 - ) 214 - 215 - if !strings.HasPrefix(p.Line(0), startMark) { 216 - return nil, nil 217 - } 218 - 219 - parts := strings.SplitAfterN(p.Line(0), endMark, 2) 220 - if len(parts) < 2 { 221 - return nil, p.Errorf(0, "invalid fragment header") 222 - } 223 - 224 - f := &TextFragment{} 225 - f.Comment = strings.TrimSpace(parts[1]) 226 - 227 - header := parts[0][len(startMark) : len(parts[0])-len(endMark)] 228 - ranges := strings.Split(header, " +") 229 - if len(ranges) != 2 { 230 - return nil, p.Errorf(0, "invalid fragment header") 231 - } 232 - 233 - var err error 234 - if f.OldPosition, f.OldLines, err = parseRange(ranges[0]); err != nil { 235 - return nil, p.Errorf(0, "invalid fragment header: %v", err) 236 - } 237 - if f.NewPosition, f.NewLines, err = parseRange(ranges[1]); err != nil { 238 - return nil, p.Errorf(0, "invalid fragment header: %v", err) 239 - } 240 - 241 - if err := p.Next(); err != nil && err != io.EOF { 242 - return nil, err 243 - } 244 - return f, nil 245 - } 246 - 247 - func (p *parser) ParseTextChunk(frag *TextFragment) error { 248 - if p.Line(0) == "" { 249 - return p.Errorf(0, "no content following fragment header") 250 - } 251 - 252 - isNoNewlineLine := func(s string) bool { 253 - // test for "\ No newline at end of file" by prefix because the text 254 - // changes by locale (git claims all versions are at least 12 chars) 255 - return len(s) >= 12 && s[:2] == "\\ " 256 - } 257 - 258 - oldLines, newLines := frag.OldLines, frag.NewLines 259 - for { 260 - line := p.Line(0) 261 - op, data := line[0], line[1:] 262 - 263 - switch op { 264 - case '\n': 265 - data = "\n" 266 - fallthrough // newer GNU diff versions create empty context lines 267 - case ' ': 268 - oldLines-- 269 - newLines-- 270 - if frag.LinesAdded == 0 && frag.LinesDeleted == 0 { 271 - frag.LeadingContext++ 272 - } else { 273 - frag.TrailingContext++ 274 - } 275 - frag.Lines = append(frag.Lines, Line{OpContext, data}) 276 - case '-': 277 - oldLines-- 278 - frag.LinesDeleted++ 279 - frag.TrailingContext = 0 280 - frag.Lines = append(frag.Lines, Line{OpDelete, data}) 281 - case '+': 282 - newLines-- 283 - frag.LinesAdded++ 284 - frag.TrailingContext = 0 285 - frag.Lines = append(frag.Lines, Line{OpAdd, data}) 286 - default: 287 - // this may appear in middle of fragment if it's for a deleted line 288 - if isNoNewlineLine(line) { 289 - last := &frag.Lines[len(frag.Lines)-1] 290 - last.Line = strings.TrimSuffix(last.Line, "\n") 291 - break 292 - } 293 - // TODO(bkeyes): if this is because we hit the next header, it 294 - // would be helpful to return the miscounts line error. We could 295 - // either test for the common headers ("@@ -", "diff --git") or 296 - // assume any invalid op ends the fragment; git returns the same 297 - // generic error in all cases so either is compatible 298 - return p.Errorf(0, "invalid line operation: %q", op) 299 - } 300 - 301 - next := p.Line(1) 302 - if oldLines <= 0 && newLines <= 0 && !isNoNewlineLine(next) { 303 - break 304 - } 305 - 306 - if err := p.Next(); err != nil { 307 - if err == io.EOF { 308 - break 309 - } 310 - return err 311 - } 312 - } 313 - 314 - if oldLines != 0 || newLines != 0 { 315 - hdr := max(frag.OldLines-oldLines, frag.NewLines-newLines) + 1 316 - return p.Errorf(-hdr, "fragment header miscounts lines: %+d old, %+d new", -oldLines, -newLines) 317 - } 318 - 319 - if err := p.Next(); err != nil && err != io.EOF { 320 - return err 321 - } 322 - return nil 323 - } 324 - 325 - func (p *parser) ParseBinaryFragments(f *File) (n int, err error) { 326 - isBinary, hasData, err := p.ParseBinaryMarker() 327 - if err != nil || !isBinary { 328 - return 0, err 329 - } 330 - 331 - f.IsBinary = true 332 - if !hasData { 333 - return 0, nil 334 - } 335 - 336 - forward, err := p.ParseBinaryFragmentHeader() 337 - if err != nil { 338 - return 0, err 339 - } 340 - if forward == nil { 341 - return 0, p.Errorf(0, "missing data for binary patch") 342 - } 343 - if err := p.ParseBinaryChunk(forward); err != nil { 344 - return 0, err 345 - } 346 - f.BinaryFragment = forward 347 - 348 - // valid for reverse to not exist, but it must be valid if present 349 - reverse, err := p.ParseBinaryFragmentHeader() 350 - if err != nil { 351 - return 1, err 352 - } 353 - if reverse == nil { 354 - return 1, nil 355 - } 356 - if err := p.ParseBinaryChunk(reverse); err != nil { 357 - return 1, err 358 - } 359 - f.ReverseBinaryFragment = reverse 360 - 361 - return 1, nil 362 - } 363 - 364 - func (p *parser) ParseBinaryMarker() (isBinary bool, hasData bool, err error) { 365 - switch p.Line(0) { 366 - case "GIT binary patch\n": 367 - hasData = true 368 - case "Binary files differ\n": 369 - case "Files differ\n": 370 - default: 371 - return false, false, nil 372 - } 373 - 374 - if err = p.Next(); err != nil && err != io.EOF { 375 - return false, false, err 376 - } 377 - return true, hasData, nil 378 - } 379 - 380 - func (p *parser) ParseBinaryFragmentHeader() (*BinaryFragment, error) { 381 - parts := strings.SplitN(strings.TrimSuffix(p.Line(0), "\n"), " ", 2) 382 - if len(parts) < 2 { 383 - return nil, nil 384 - } 385 - 386 - frag := &BinaryFragment{} 387 - switch parts[0] { 388 - case "delta": 389 - frag.Method = BinaryPatchDelta 390 - case "literal": 391 - frag.Method = BinaryPatchLiteral 392 - default: 393 - return nil, nil 394 - } 395 - 396 - var err error 397 - if frag.Size, err = strconv.ParseInt(parts[1], 10, 64); err != nil { 398 - nerr := err.(*strconv.NumError) 399 - return nil, p.Errorf(0, "binary patch: invalid size: %v", nerr.Err) 400 - } 401 - 402 - if err := p.Next(); err != nil && err != io.EOF { 403 - return nil, err 404 - } 405 - return frag, nil 406 - } 407 - 408 - func (p *parser) ParseBinaryChunk(frag *BinaryFragment) error { 409 - // Binary fragments are encoded as a series of base85 encoded lines. Each 410 - // line starts with a character in [A-Za-z] giving the number of bytes on 411 - // the line, where A = 1 and z = 52, and ends with a newline character. 412 - // 413 - // The base85 encoding means each line is a multiple of 5 characters + 2 414 - // additional characters for the length byte and the newline. The fragment 415 - // ends with a blank line. 416 - const ( 417 - shortestValidLine = "A00000\n" 418 - maxBytesPerLine = 52 419 - ) 420 - 421 - var data bytes.Buffer 422 - buf := make([]byte, maxBytesPerLine) 423 - for { 424 - line := p.Line(0) 425 - if line == "\n" { 426 - break 427 - } 428 - if len(line) < len(shortestValidLine) || (len(line)-2)%5 != 0 { 429 - return p.Errorf(0, "binary patch: corrupt data line") 430 - } 431 - 432 - byteCount, seq := int(line[0]), line[1:len(line)-1] 433 - switch { 434 - case 'A' <= byteCount && byteCount <= 'Z': 435 - byteCount = byteCount - 'A' + 1 436 - case 'a' <= byteCount && byteCount <= 'z': 437 - byteCount = byteCount - 'a' + 27 438 - default: 439 - return p.Errorf(0, "binary patch: invalid length byte") 440 - } 441 - 442 - // base85 encodes every 4 bytes into 5 characters, with up to 3 bytes of end padding 443 - maxByteCount := len(seq) / 5 * 4 444 - if byteCount > maxByteCount || byteCount < maxByteCount-3 { 445 - return p.Errorf(0, "binary patch: incorrect byte count") 446 - } 447 - 448 - if err := base85Decode(buf[:byteCount], []byte(seq)); err != nil { 449 - return p.Errorf(0, "binary patch: %v", err) 450 - } 451 - data.Write(buf[:byteCount]) 452 - 453 - if err := p.Next(); err != nil { 454 - if err == io.EOF { 455 - return p.Errorf(0, "binary patch: unexpected EOF") 456 - } 457 - return err 458 - } 459 - } 460 - 461 - if err := inflateBinaryChunk(frag, &data); err != nil { 462 - return p.Errorf(0, "binary patch: %v", err) 463 - } 464 - 465 - // consume the empty line that ended the fragment 466 - if err := p.Next(); err != nil && err != io.EOF { 467 - return err 468 - } 469 - return nil 470 - } 471 - 472 - func inflateBinaryChunk(frag *BinaryFragment, r io.Reader) error { 473 - zr, err := zlib.NewReader(r) 474 - if err != nil { 475 - return err 476 - } 477 - 478 - data, err := ioutil.ReadAll(zr) 479 - if err != nil { 480 - return err 481 - } 482 - if err := zr.Close(); err != nil { 483 - return err 484 - } 485 - 486 - if int64(len(data)) != frag.Size { 487 - return fmt.Errorf("%d byte fragment inflated to %d", frag.Size, len(data)) 488 - } 489 - frag.Data = data 490 - return nil 491 - } 492 - 493 - func parseRange(s string) (start int64, end int64, err error) { 494 - parts := strings.SplitN(s, ",", 2) 495 - 496 - if start, err = strconv.ParseInt(parts[0], 10, 64); err != nil { 497 - nerr := err.(*strconv.NumError) 498 - return 0, 0, fmt.Errorf("bad start of range: %s: %v", parts[0], nerr.Err) 499 - } 500 - 501 - if len(parts) > 1 { 502 - if end, err = strconv.ParseInt(parts[1], 10, 64); err != nil { 503 - nerr := err.(*strconv.NumError) 504 - return 0, 0, fmt.Errorf("bad end of range: %s: %v", parts[1], nerr.Err) 505 - } 506 - } else { 507 - end = 1 508 - } 509 - 510 - return 511 - } 512 - 513 - func max(a, b int64) int64 { 514 - if a > b { 515 - return a 516 - } 517 - return b 518 - }
gitdiff/parser_binary_test.go gitdiff/binary_test.go
gitdiff/parser_text_test.go gitdiff/text_test.go
+180
gitdiff/text.go
··· 1 + package gitdiff 2 + 3 + import ( 4 + "fmt" 5 + "io" 6 + "strconv" 7 + "strings" 8 + ) 9 + 10 + // ParseTextFragments parses text fragments until the next file header or the 11 + // end of the stream and attaches them to the given file. It returns the number 12 + // of fragments that were added. 13 + func (p *parser) ParseTextFragments(f *File) (n int, err error) { 14 + for { 15 + frag, err := p.ParseTextFragmentHeader() 16 + if err != nil { 17 + return n, err 18 + } 19 + if frag == nil { 20 + return n, nil 21 + } 22 + 23 + if f.IsNew && frag.OldLines > 0 { 24 + return n, p.Errorf(-1, "new file depends on old contents") 25 + } 26 + if f.IsDelete && frag.NewLines > 0 { 27 + return n, p.Errorf(-1, "deleted file still has contents") 28 + } 29 + 30 + if err := p.ParseTextChunk(frag); err != nil { 31 + return n, err 32 + } 33 + 34 + f.TextFragments = append(f.TextFragments, frag) 35 + n++ 36 + } 37 + } 38 + 39 + func (p *parser) ParseTextFragmentHeader() (*TextFragment, error) { 40 + const ( 41 + startMark = "@@ -" 42 + endMark = " @@" 43 + ) 44 + 45 + if !strings.HasPrefix(p.Line(0), startMark) { 46 + return nil, nil 47 + } 48 + 49 + parts := strings.SplitAfterN(p.Line(0), endMark, 2) 50 + if len(parts) < 2 { 51 + return nil, p.Errorf(0, "invalid fragment header") 52 + } 53 + 54 + f := &TextFragment{} 55 + f.Comment = strings.TrimSpace(parts[1]) 56 + 57 + header := parts[0][len(startMark) : len(parts[0])-len(endMark)] 58 + ranges := strings.Split(header, " +") 59 + if len(ranges) != 2 { 60 + return nil, p.Errorf(0, "invalid fragment header") 61 + } 62 + 63 + var err error 64 + if f.OldPosition, f.OldLines, err = parseRange(ranges[0]); err != nil { 65 + return nil, p.Errorf(0, "invalid fragment header: %v", err) 66 + } 67 + if f.NewPosition, f.NewLines, err = parseRange(ranges[1]); err != nil { 68 + return nil, p.Errorf(0, "invalid fragment header: %v", err) 69 + } 70 + 71 + if err := p.Next(); err != nil && err != io.EOF { 72 + return nil, err 73 + } 74 + return f, nil 75 + } 76 + 77 + func (p *parser) ParseTextChunk(frag *TextFragment) error { 78 + if p.Line(0) == "" { 79 + return p.Errorf(0, "no content following fragment header") 80 + } 81 + 82 + isNoNewlineLine := func(s string) bool { 83 + // test for "\ No newline at end of file" by prefix because the text 84 + // changes by locale (git claims all versions are at least 12 chars) 85 + return len(s) >= 12 && s[:2] == "\\ " 86 + } 87 + 88 + oldLines, newLines := frag.OldLines, frag.NewLines 89 + for { 90 + line := p.Line(0) 91 + op, data := line[0], line[1:] 92 + 93 + switch op { 94 + case '\n': 95 + data = "\n" 96 + fallthrough // newer GNU diff versions create empty context lines 97 + case ' ': 98 + oldLines-- 99 + newLines-- 100 + if frag.LinesAdded == 0 && frag.LinesDeleted == 0 { 101 + frag.LeadingContext++ 102 + } else { 103 + frag.TrailingContext++ 104 + } 105 + frag.Lines = append(frag.Lines, Line{OpContext, data}) 106 + case '-': 107 + oldLines-- 108 + frag.LinesDeleted++ 109 + frag.TrailingContext = 0 110 + frag.Lines = append(frag.Lines, Line{OpDelete, data}) 111 + case '+': 112 + newLines-- 113 + frag.LinesAdded++ 114 + frag.TrailingContext = 0 115 + frag.Lines = append(frag.Lines, Line{OpAdd, data}) 116 + default: 117 + // this may appear in middle of fragment if it's for a deleted line 118 + if isNoNewlineLine(line) { 119 + last := &frag.Lines[len(frag.Lines)-1] 120 + last.Line = strings.TrimSuffix(last.Line, "\n") 121 + break 122 + } 123 + // TODO(bkeyes): if this is because we hit the next header, it 124 + // would be helpful to return the miscounts line error. We could 125 + // either test for the common headers ("@@ -", "diff --git") or 126 + // assume any invalid op ends the fragment; git returns the same 127 + // generic error in all cases so either is compatible 128 + return p.Errorf(0, "invalid line operation: %q", op) 129 + } 130 + 131 + next := p.Line(1) 132 + if oldLines <= 0 && newLines <= 0 && !isNoNewlineLine(next) { 133 + break 134 + } 135 + 136 + if err := p.Next(); err != nil { 137 + if err == io.EOF { 138 + break 139 + } 140 + return err 141 + } 142 + } 143 + 144 + if oldLines != 0 || newLines != 0 { 145 + hdr := max(frag.OldLines-oldLines, frag.NewLines-newLines) + 1 146 + return p.Errorf(-hdr, "fragment header miscounts lines: %+d old, %+d new", -oldLines, -newLines) 147 + } 148 + 149 + if err := p.Next(); err != nil && err != io.EOF { 150 + return err 151 + } 152 + return nil 153 + } 154 + 155 + func parseRange(s string) (start int64, end int64, err error) { 156 + parts := strings.SplitN(s, ",", 2) 157 + 158 + if start, err = strconv.ParseInt(parts[0], 10, 64); err != nil { 159 + nerr := err.(*strconv.NumError) 160 + return 0, 0, fmt.Errorf("bad start of range: %s: %v", parts[0], nerr.Err) 161 + } 162 + 163 + if len(parts) > 1 { 164 + if end, err = strconv.ParseInt(parts[1], 10, 64); err != nil { 165 + nerr := err.(*strconv.NumError) 166 + return 0, 0, fmt.Errorf("bad end of range: %s: %v", parts[1], nerr.Err) 167 + } 168 + } else { 169 + end = 1 170 + } 171 + 172 + return 173 + } 174 + 175 + func max(a, b int64) int64 { 176 + if a > b { 177 + return a 178 + } 179 + return b 180 + }