fork of go-gitdiff with jj support
at v0.7.1 4.8 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 oldLines, newLines := frag.OldLines, frag.NewLines 83 for oldLines > 0 || newLines > 0 { 84 line := p.Line(0) 85 op, data := line[0], line[1:] 86 87 switch op { 88 case '\n': 89 data = "\n" 90 fallthrough // newer GNU diff versions create empty context lines 91 case ' ': 92 oldLines-- 93 newLines-- 94 if frag.LinesAdded == 0 && frag.LinesDeleted == 0 { 95 frag.LeadingContext++ 96 } else { 97 frag.TrailingContext++ 98 } 99 frag.Lines = append(frag.Lines, Line{OpContext, data}) 100 case '-': 101 oldLines-- 102 frag.LinesDeleted++ 103 frag.TrailingContext = 0 104 frag.Lines = append(frag.Lines, Line{OpDelete, data}) 105 case '+': 106 newLines-- 107 frag.LinesAdded++ 108 frag.TrailingContext = 0 109 frag.Lines = append(frag.Lines, Line{OpAdd, data}) 110 case '\\': 111 // this may appear in middle of fragment if it's for a deleted line 112 if isNoNewlineMarker(line) { 113 removeLastNewline(frag) 114 break 115 } 116 fallthrough 117 default: 118 // TODO(bkeyes): if this is because we hit the next header, it 119 // would be helpful to return the miscounts line error. We could 120 // either test for the common headers ("@@ -", "diff --git") or 121 // assume any invalid op ends the fragment; git returns the same 122 // generic error in all cases so either is compatible 123 return p.Errorf(0, "invalid line operation: %q", op) 124 } 125 126 if err := p.Next(); err != nil { 127 if err == io.EOF { 128 break 129 } 130 return err 131 } 132 } 133 134 if oldLines != 0 || newLines != 0 { 135 hdr := max(frag.OldLines-oldLines, frag.NewLines-newLines) + 1 136 return p.Errorf(-hdr, "fragment header miscounts lines: %+d old, %+d new", -oldLines, -newLines) 137 } 138 if frag.LinesAdded == 0 && frag.LinesDeleted == 0 { 139 return p.Errorf(0, "fragment contains no changes") 140 } 141 142 // check for a final "no newline" marker since it is not included in the 143 // counters used to stop the loop above 144 if isNoNewlineMarker(p.Line(0)) { 145 removeLastNewline(frag) 146 if err := p.Next(); err != nil && err != io.EOF { 147 return err 148 } 149 } 150 151 return nil 152} 153 154func isNoNewlineMarker(s string) bool { 155 // test for "\ No newline at end of file" by prefix because the text 156 // changes by locale (git claims all versions are at least 12 chars) 157 return len(s) >= 12 && s[:2] == "\\ " 158} 159 160func removeLastNewline(frag *TextFragment) { 161 if len(frag.Lines) > 0 { 162 last := &frag.Lines[len(frag.Lines)-1] 163 last.Line = strings.TrimSuffix(last.Line, "\n") 164 } 165} 166 167func parseRange(s string) (start int64, end int64, err error) { 168 parts := strings.SplitN(s, ",", 2) 169 170 if start, err = strconv.ParseInt(parts[0], 10, 64); err != nil { 171 nerr := err.(*strconv.NumError) 172 return 0, 0, fmt.Errorf("bad start of range: %s: %v", parts[0], nerr.Err) 173 } 174 175 if len(parts) > 1 { 176 if end, err = strconv.ParseInt(parts[1], 10, 64); err != nil { 177 nerr := err.(*strconv.NumError) 178 return 0, 0, fmt.Errorf("bad end of range: %s: %v", parts[1], nerr.Err) 179 } 180 } else { 181 end = 1 182 } 183 184 return 185} 186 187func max(a, b int64) int64 { 188 if a > b { 189 return a 190 } 191 return b 192}