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  }