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