fork of go-gitdiff with jj support
at v0.3.0 4.5 kB view raw
1package gitdiff 2 3import ( 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. 13func (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 39func (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 77func (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 155func 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 175func max(a, b int64) int64 { 176 if a > b { 177 return a 178 } 179 return b 180}