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  }