feat: recognize Jira issue keys in commit body

As outlined in #183 it can be useful when following the
conventional commit syntax to separate conventional
commit type in the subject from work item references
which often appear in the commit body or commit footer

this commit:

- adds a new config option 'jira.issue.key_pattern' which
  is an optional regex used to match issue keys in the
  commit subject and body

- ensures that the Jira Issue Type does not overwrite a
  Conventional Commit Type; the 'jira.issue.type_maps'
  map is only used as a backup when the Conventional
  Commit Type parsed out of the Subject header is empty

- runs the 'jira.issue.key_pattern' regex against both
  the commit Subject header and the entire commit Body

NOTE: the current code assumes that only one Jira work
      item will be bound to a single commit, and this
      appears consistent with the spirit of Conventional
      Commits. I have sometimes found it useful to write
      a Conventional Commit where one 'fix' can close
      multiple Jira work items, so a future issue/change
      might consider updating the model to support multiple
      Jira work items in a single commit message.
This commit is contained in:
David Alpert 2023-11-20 15:10:49 -06:00
parent d93ef22384
commit 2a7ca9ecb0
6 changed files with 98 additions and 32 deletions

View file

@ -61,6 +61,7 @@ type commitParser struct {
reMention *regexp.Regexp
reSignOff *regexp.Regexp
reCoAuthor *regexp.Regexp
reJiraKey *regexp.Regexp
reJiraIssueDescription *regexp.Regexp
}
@ -71,7 +72,7 @@ func newCommitParser(logger *Logger, client gitcmd.Client, jiraClient JiraClient
joinedIssuePrefix := joinAndQuoteMeta(opts.IssuePrefix, "|")
joinedNoteKeywords := joinAndQuoteMeta(opts.NoteKeywords, "|")
return &commitParser{
p := commitParser{
logger: logger,
client: client,
jiraClient: jiraClient,
@ -87,6 +88,12 @@ func newCommitParser(logger *Logger, client gitcmd.Client, jiraClient JiraClient
reCoAuthor: regexp.MustCompile(`Co-authored-by:\s+([\p{L}\s\-\[\]]+)\s+<([\w+\-\[\].@]+)>`),
reJiraIssueDescription: regexp.MustCompile(opts.JiraIssueDescriptionPattern),
}
if opts.JiraKeyPattern != "" {
p.reJiraKey = regexp.MustCompile(opts.JiraKeyPattern)
}
return &p
}
func (p *commitParser) Parse(rev string) ([]*Commit, error) {
@ -153,6 +160,11 @@ func (p *commitParser) parseCommit(input string) *Commit {
}
}
// Jira
if commit.JiraIssueID != "" {
p.processJiraIssue(commit, commit.JiraIssueID)
}
commit.Refs = p.uniqRefs(commit.Refs)
commit.Mentions = p.uniqMentions(commit.Mentions)
@ -206,6 +218,8 @@ func (p *commitParser) processHeader(commit *Commit, input string) {
assignDynamicValues(commit, opts.HeaderPatternMaps, res[0][1:])
}
p.populateJiraIssueIDByJiraKeyRegex(commit, input)
// Merge
res = p.reMerge.FindAllStringSubmatch(input, -1)
if len(res) > 0 {
@ -225,11 +239,6 @@ func (p *commitParser) processHeader(commit *Commit, input string) {
// refs & mentions
commit.Refs = p.parseRefs(input)
commit.Mentions = p.parseMentions(input)
// Jira
if commit.JiraIssueID != "" {
p.processJiraIssue(commit, commit.JiraIssueID)
}
}
func (p *commitParser) extractLineMetadata(commit *Commit, line string) bool {
@ -268,6 +277,8 @@ func (p *commitParser) processBody(commit *Commit, input string) {
// body
commit.Body = input
p.populateJiraIssueIDByJiraKeyRegex(commit, input)
// notes & refs & mentions
commit.Notes = []*Note{}
inNote := false
@ -314,6 +325,18 @@ func (p *commitParser) processBody(commit *Commit, input string) {
p.trimSpaceInNotes(commit)
}
func (p *commitParser) populateJiraIssueIDByJiraKeyRegex(commit *Commit, input string) {
if p.reJiraKey != nil {
m := p.reJiraKey.FindAllString(input, -1)
if len(m) > 0 {
// NOTE: a git commit may reference multiple jira issues... here we capture only the first
// a future change may resolve this by updating the commit structure to support
// multiple issues
commit.JiraIssueID = m[0]
}
}
}
func (*commitParser) trimSpaceInNotes(commit *Commit) {
for _, note := range commit.Notes {
note.Body = strings.TrimSpace(note.Body)
@ -432,7 +455,10 @@ func (p *commitParser) processJiraIssue(commit *Commit, issueID string) {
p.logger.Error(fmt.Sprintf("Failed to parse Jira story %s: %s\n", issueID, err))
return
}
commit.Type = p.config.Options.JiraTypeMaps[issue.Fields.Type.Name]
if commit.Type == "" {
// don't override an explicit Conventional Commit Type
commit.Type = p.config.Options.JiraTypeMaps[issue.Fields.Type.Name]
}
commit.JiraIssue = &JiraIssue{
Type: issue.Fields.Type.Name,
Summary: issue.Fields.Summary,