github.com/brandonmanuel/git-chglog@v0.0.0-20200903004639-7a62fa08787a/commit_parser.go (about) 1 package chglog 2 3 import ( 4 "fmt" 5 "regexp" 6 "strconv" 7 "strings" 8 "time" 9 10 gitcmd "github.com/tsuyoshiwada/go-gitcmd" 11 ) 12 13 var ( 14 // constants 15 separator = "@@__CHGLOG__@@" 16 delimiter = "@@__CHGLOG_DELIMITER__@@" 17 18 // fields 19 hashField = "HASH" 20 authorField = "AUTHOR" 21 committerField = "COMMITTER" 22 subjectField = "SUBJECT" 23 bodyField = "BODY" 24 25 // formats 26 hashFormat = hashField + ":%H\t%h" 27 authorFormat = authorField + ":%an\t%ae\t%at" 28 committerFormat = committerField + ":%cn\t%ce\t%ct" 29 subjectFormat = subjectField + ":%s" 30 bodyFormat = bodyField + ":%b" 31 32 // log 33 logFormat = separator + strings.Join([]string{ 34 hashFormat, 35 authorFormat, 36 committerFormat, 37 subjectFormat, 38 bodyFormat, 39 }, delimiter) 40 ) 41 42 func joinAndQuoteMeta(list []string, sep string) string { 43 arr := make([]string, len(list)) 44 for i, s := range list { 45 arr[i] = regexp.QuoteMeta(s) 46 } 47 return strings.Join(arr, sep) 48 } 49 50 type commitParser struct { 51 client gitcmd.Client 52 config *Config 53 reHeader *regexp.Regexp 54 reMerge *regexp.Regexp 55 reRevert *regexp.Regexp 56 reRef *regexp.Regexp 57 reIssue *regexp.Regexp 58 reNotes *regexp.Regexp 59 reMention *regexp.Regexp 60 } 61 62 func newCommitParser(client gitcmd.Client, config *Config) *commitParser { 63 opts := config.Options 64 65 joinedRefActions := joinAndQuoteMeta(opts.RefActions, "|") 66 joinedIssuePrefix := joinAndQuoteMeta(opts.IssuePrefix, "|") 67 joinedNoteKeywords := joinAndQuoteMeta(opts.NoteKeywords, "|") 68 69 return &commitParser{ 70 client: client, 71 config: config, 72 reHeader: regexp.MustCompile(opts.HeaderPattern), 73 reMerge: regexp.MustCompile(opts.MergePattern), 74 reRevert: regexp.MustCompile(opts.RevertPattern), 75 reRef: regexp.MustCompile("(?i)(" + joinedRefActions + ")\\s?([\\w/\\.\\-]+)?(?:" + joinedIssuePrefix + ")(\\d+)"), 76 reIssue: regexp.MustCompile("(?:" + joinedIssuePrefix + ")(\\d+)"), 77 reNotes: regexp.MustCompile("^(?i)\\s*(" + joinedNoteKeywords + ")[:\\s]+(.*)"), 78 reMention: regexp.MustCompile("@([\\w-]+)"), 79 } 80 } 81 82 func (p *commitParser) Parse(rev string) ([]*Commit, error) { 83 out, err := p.client.Exec( 84 "log", 85 rev, 86 "--no-decorate", 87 "--pretty="+logFormat, 88 ) 89 90 fmt.Println(out) 91 92 if err != nil { 93 return nil, err 94 } 95 96 processor := p.config.Options.Processor 97 lines := strings.Split(out, separator) 98 lines = lines[1:] 99 commits := make([]*Commit, len(lines)) 100 101 for i, line := range lines { 102 commit := p.parseCommit(line) 103 104 if processor != nil { 105 commit = processor.ProcessCommit(commit) 106 if commit == nil { 107 continue 108 } 109 } 110 111 commits[i] = commit 112 } 113 114 return commits, nil 115 } 116 117 func (p *commitParser) parseCommit(input string) *Commit { 118 commit := &Commit{} 119 tokens := strings.Split(input, delimiter) 120 121 for _, token := range tokens { 122 firstSep := strings.Index(token, ":") 123 field := token[0:firstSep] 124 value := strings.TrimSpace(token[firstSep+1:]) 125 126 switch field { 127 case hashField: 128 commit.Hash = p.parseHash(value) 129 case authorField: 130 commit.Author = p.parseAuthor(value) 131 case committerField: 132 commit.Committer = p.parseCommitter(value) 133 case subjectField: 134 p.processHeader(commit, value) 135 case bodyField: 136 p.processBody(commit, value) 137 } 138 } 139 140 commit.Refs = p.uniqRefs(commit.Refs) 141 commit.Mentions = p.uniqMentions(commit.Mentions) 142 143 return commit 144 } 145 146 func (p *commitParser) parseHash(input string) *Hash { 147 arr := strings.Split(input, "\t") 148 149 return &Hash{ 150 Long: arr[0], 151 Short: arr[1], 152 } 153 } 154 155 func (p *commitParser) parseAuthor(input string) *Author { 156 arr := strings.Split(input, "\t") 157 ts, err := strconv.Atoi(arr[2]) 158 if err != nil { 159 ts = 0 160 } 161 162 return &Author{ 163 Name: arr[0], 164 Email: arr[1], 165 Date: time.Unix(int64(ts), 0), 166 } 167 } 168 169 func (p *commitParser) parseCommitter(input string) *Committer { 170 author := p.parseAuthor(input) 171 172 return &Committer{ 173 Name: author.Name, 174 Email: author.Email, 175 Date: author.Date, 176 } 177 } 178 179 func (p *commitParser) processHeader(commit *Commit, input string) { 180 opts := p.config.Options 181 182 // header (raw) 183 commit.Header = input 184 185 var res [][]string 186 187 // Type, Scope, Subject etc ... 188 res = p.reHeader.FindAllStringSubmatch(input, -1) 189 if len(res) > 0 { 190 assignDynamicValues(commit, opts.HeaderPatternMaps, res[0][1:]) 191 } 192 193 // Merge 194 res = p.reMerge.FindAllStringSubmatch(input, -1) 195 if len(res) > 0 { 196 merge := &Merge{} 197 assignDynamicValues(merge, opts.MergePatternMaps, res[0][1:]) 198 commit.Merge = merge 199 } 200 201 // Revert 202 res = p.reRevert.FindAllStringSubmatch(input, -1) 203 if len(res) > 0 { 204 revert := &Revert{} 205 assignDynamicValues(revert, opts.RevertPatternMaps, res[0][1:]) 206 commit.Revert = revert 207 } 208 209 // refs & mentions 210 commit.Refs = p.parseRefs(input) 211 commit.Mentions = p.parseMentions(input) 212 } 213 214 func (p *commitParser) processBody(commit *Commit, input string) { 215 input = convNewline(input, "\n") 216 217 // body 218 commit.Body = input 219 220 // notes & refs & mentions 221 commit.Notes = []*Note{} 222 inNote := false 223 fenceDetector := newMdFenceDetector() 224 lines := strings.Split(input, "\n") 225 226 for _, line := range lines { 227 fenceDetector.Update(line) 228 229 if !fenceDetector.InCodeblock() { 230 refs := p.parseRefs(line) 231 if len(refs) > 0 { 232 inNote = false 233 commit.Refs = append(commit.Refs, refs...) 234 } 235 236 mentions := p.parseMentions(line) 237 if len(mentions) > 0 { 238 inNote = false 239 commit.Mentions = append(commit.Mentions, mentions...) 240 } 241 } 242 243 res := p.reNotes.FindAllStringSubmatch(line, -1) 244 245 if len(res) > 0 { 246 inNote = true 247 for _, r := range res { 248 commit.Notes = append(commit.Notes, &Note{ 249 Title: r[1], 250 Body: r[2], 251 }) 252 } 253 } else if inNote { 254 last := commit.Notes[len(commit.Notes)-1] 255 last.Body = last.Body + "\n" + line 256 } 257 } 258 259 p.trimSpaceInNotes(commit) 260 } 261 262 func (*commitParser) trimSpaceInNotes(commit *Commit) { 263 for _, note := range commit.Notes { 264 note.Body = strings.TrimSpace(note.Body) 265 } 266 } 267 268 func (p *commitParser) parseRefs(input string) []*Ref { 269 refs := []*Ref{} 270 271 // references 272 res := p.reRef.FindAllStringSubmatch(input, -1) 273 274 for _, r := range res { 275 refs = append(refs, &Ref{ 276 Action: r[1], 277 Source: r[2], 278 Ref: r[3], 279 }) 280 } 281 282 // issues 283 res = p.reIssue.FindAllStringSubmatch(input, -1) 284 for _, r := range res { 285 duplicate := false 286 for _, ref := range refs { 287 if ref.Ref == r[1] { 288 duplicate = true 289 } 290 } 291 if !duplicate { 292 refs = append(refs, &Ref{ 293 Action: "", 294 Source: "", 295 Ref: r[1], 296 }) 297 } 298 } 299 300 return refs 301 } 302 303 func (p *commitParser) parseMentions(input string) []string { 304 res := p.reMention.FindAllStringSubmatch(input, -1) 305 mentions := make([]string, len(res)) 306 307 for i, r := range res { 308 mentions[i] = r[1] 309 } 310 311 return mentions 312 } 313 314 func (p *commitParser) uniqRefs(refs []*Ref) []*Ref { 315 arr := []*Ref{} 316 317 for _, ref := range refs { 318 exist := false 319 for _, r := range arr { 320 if ref.Ref == r.Ref && ref.Action == r.Action && ref.Source == r.Source { 321 exist = true 322 } 323 } 324 if !exist { 325 arr = append(arr, ref) 326 } 327 } 328 329 return arr 330 } 331 332 func (p *commitParser) uniqMentions(mentions []string) []string { 333 arr := []string{} 334 335 for _, mention := range mentions { 336 exist := false 337 for _, m := range arr { 338 if mention == m { 339 exist = true 340 } 341 } 342 if !exist { 343 arr = append(arr, mention) 344 } 345 } 346 347 return arr 348 } 349 350 var ( 351 fenceTypes = []string{ 352 "```", 353 "~~~", 354 " ", 355 "\t", 356 } 357 ) 358 359 type mdFenceDetector struct { 360 fence int 361 } 362 363 func newMdFenceDetector() *mdFenceDetector { 364 return &mdFenceDetector{ 365 fence: -1, 366 } 367 } 368 369 func (d *mdFenceDetector) InCodeblock() bool { 370 return d.fence > -1 371 } 372 373 func (d *mdFenceDetector) Update(input string) { 374 for i, s := range fenceTypes { 375 if d.fence < 0 { 376 if strings.Index(input, s) == 0 { 377 d.fence = i 378 break 379 } 380 } else { 381 if strings.Index(input, s) == 0 && i == d.fence { 382 d.fence = -1 383 break 384 } 385 } 386 } 387 }