diff --git a/README.md b/README.md index 3e767100..c9eda7ce 100644 --- a/README.md +++ b/README.md @@ -574,25 +574,41 @@ This sample pattern can match both forms of commit headers: The following is a sample: ```yaml - jira: - info: - username: u - token: p - url: https://jira.com - issue: - type_maps: - Task: fix - Story: feat - description_pattern: "(.*)" + options: + jira: + info: + username: u + token: p + url: https://jira.com + issue: + key_pattern: "\\b(JIRA-\\d+)" + type_maps: + Task: fix + Story: feat + description_pattern: "(.*)" ``` -Here you need to define Jira URL, access username and token (password). If you -don't want to write your Jira access credential in configure file, you may define -them with environment variables: `JIRA_URL`, `JIRA_USERNAME` and `JIRA_TOKEN`. +#### `options.jira.info` -You also needs to define a issue type map. In above sample, Jira issue type `Task` +Options to configure a Jira API client: + +| Key | Required | Type | Default | Description | +|:-----------|:---------|:-------|:--------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `username` | N | String | none | Jira username; can also be read as an env var from `JIRA_USERNAME` | +| `token` | N | String | none | Jira [API token](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/); can also be read as an env var from `JIRA_TOKEN` | +| `url` | N | String | none | Base URL for your Jira instance (e.g. https://myorg.atlassian.net/);
can also be read as an env var from `JIRA_URL` | + +#### `options.jira.issue` + +You can also define an issue type map. In the above sample, Jira issue type `Task` will be mapped to `fix` and `Story` will be mapped to `feat`. +| Key | Required | Type | Default | Description | +|:----------------------|:---------|:--------------------|:--------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `key_pattern` | N | String | none | A golang-compatible regular expression used to match against commit header and body to detect related Jira issues. | +| `type_maps` | N | `map[string]string` | none | A dictionary-style map used to infer the Conventional Commit Type from the Jira Issue Type, used when the Conventional Commit Subject does not provide a Commit Type. | +| `description_pattern` | N | String | none | A golang-compatible regular expression used to select out of a Jira Issue's Description field the subset of text to select for `.JiraIssue.Description` | + As a Jira story's description could be very long, you might not want to include the entire description into change log. In that case, you may define `description_pattern` like above, so that only content embraced with ` ... ` @@ -608,17 +624,16 @@ data. For example: ### {{ .Title }} {{ range .Commits -}} - {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} -{{ if .JiraIssue }} {{ .JiraIssue.Description }} -{{ end }} + {{ if .JiraIssue }} {{ .JiraIssue.Description }} {{ end }} {{ end }} {{ end -}} ``` Within a `Commit`, the following Jira data can be used in template: -- `.JiraIssue.Summary` - Summary of the Jira story -- `.JiraIssue.Description` - Description of the Jira story -- `.JiraIssue.Type` - Original type of the Jira story, and `.Type` will be mapped type. +- `.JiraIssue.Summary` - Summary of the Jira issue +- `.JiraIssue.Description` - Description of the Jira issue +- `.JiraIssue.Type` - Original type of the Jira issue (also available as `.Type` unless the commit provided a Conventional Commit Type. - `.JiraIssue.Labels` - A list of strings, each is a Jira label. ## FAQ diff --git a/chglog.go b/chglog.go index fc5cf9e3..4f510265 100644 --- a/chglog.go +++ b/chglog.go @@ -39,6 +39,7 @@ type Options struct { RevertPatternMaps []string // Similar to `HeaderPatternMaps` NoteKeywords []string // Keyword list to find `Note`. A semicolon is a separator, like `:` (e.g. `BREAKING CHANGE`) JiraUsername string + JiraKeyPattern string // Optional regex to match jira issue keys from commit message body JiraToken string JiraURL string JiraTypeMaps map[string]string diff --git a/cmd/git-chglog/config.go b/cmd/git-chglog/config.go index abfe82e8..66486504 100644 --- a/cmd/git-chglog/config.go +++ b/cmd/git-chglog/config.go @@ -59,6 +59,7 @@ type JiraClientInfoOptions struct { // JiraIssueOptions ... type JiraIssueOptions struct { + KeyPattern string `yaml:"key_pattern"` TypeMaps map[string]string `yaml:"type_maps"` DescriptionPattern string `yaml:"description_pattern"` } @@ -326,6 +327,7 @@ func (config *Config) Convert(ctx *CLIContext) *chglog.Config { RevertPattern: opts.Reverts.Pattern, RevertPatternMaps: opts.Reverts.PatternMaps, NoteKeywords: opts.Notes.Keywords, + JiraKeyPattern: opts.Jira.Issue.KeyPattern, JiraUsername: orValue(ctx.JiraUsername, opts.Jira.ClintInfo.Username), JiraToken: orValue(ctx.JiraToken, opts.Jira.ClintInfo.Token), JiraURL: orValue(ctx.JiraURL, opts.Jira.ClintInfo.URL), diff --git a/commit_parser.go b/commit_parser.go index 0bb67f83..4d5a4290 100644 --- a/commit_parser.go +++ b/commit_parser.go @@ -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, diff --git a/commit_parser_test.go b/commit_parser_test.go index 600d1163..00ea851e 100644 --- a/commit_parser_test.go +++ b/commit_parser_test.go @@ -398,6 +398,7 @@ func TestCommitParserParseWithJira(t *testing.T) { "JiraIssueID", "Subject", }, + JiraKeyPattern: "\\b(JIRA-\\d+)", JiraTypeMaps: map[string]string{ "Story": "feat", }, @@ -406,11 +407,28 @@ func TestCommitParserParseWithJira(t *testing.T) { commits, err := parser.Parse("HEAD") assert.Nil(err) + + assert.Equal(2, len(commits)) + commit := commits[0] - assert.Equal(commit.JiraIssueID, "JIRA-1111") - assert.Equal(commit.JiraIssue.Type, "Story") - assert.Equal(commit.JiraIssue.Summary, "summary of JIRA-1111") - assert.Equal(commit.JiraIssue.Description, "description of JIRA-1111") - assert.Equal(commit.JiraIssue.Labels, []string{"GA"}) - assert.Equal(commit.Type, "feat") + assert.Equal("JIRA-1111", commit.JiraIssueID) + if commit.JiraIssue == nil { + t.Fatal("did not load jira issue JIRA-1111") + } + assert.Equal("Story", commit.JiraIssue.Type) + assert.Equal("summary of JIRA-1111", commit.JiraIssue.Summary) + assert.Equal("description of JIRA-1111", commit.JiraIssue.Description) + assert.Equal([]string{"GA"}, commit.JiraIssue.Labels) + assert.Equal("feat", commit.Type) + + commit = commits[1] + assert.Equal("JIRA-1112", commit.JiraIssueID) + if commit.JiraIssue == nil { + t.Fatal("did not load jira issue JIRA-1112") + } + assert.Equal("Story", commit.JiraIssue.Type) + assert.Equal("summary of JIRA-1112", commit.JiraIssue.Summary) + assert.Equal("description of JIRA-1112", commit.JiraIssue.Description) + assert.Equal([]string{"GA"}, commit.JiraIssue.Labels) + assert.Equal("fix", commit.Type) } diff --git a/testdata/gitlog_jira.txt b/testdata/gitlog_jira.txt index f535f2e1..e3d66705 100644 --- a/testdata/gitlog_jira.txt +++ b/testdata/gitlog_jira.txt @@ -1 +1,5 @@ @@__CHGLOG__@@HASH:65cf1add9735dcc4810dda3312b0792236c97c4e 65cf1add@@__CHGLOG_DELIMITER__@@AUTHOR:tsuyoshi wada mail@example.com 1514808000@@__CHGLOG_DELIMITER__@@COMMITTER:tsuyoshi wada mail@example.com 1514808000@@__CHGLOG_DELIMITER__@@SUBJECT:[JIRA-1111]: Add new feature #123@@__CHGLOG_DELIMITER__@@BODY: This is body message. + +@@__CHGLOG__@@HASH:14ef0b6d386c5432af9292eab3c8314fa3001bc7 14ef0b6d@@__CHGLOG_DELIMITER__@@AUTHOR:tsuyoshi wada mail@example.com 1515153600@@__CHGLOG_DELIMITER__@@COMMITTER:tsuyoshi wada mail@example.com 1515153600@@__CHGLOG_DELIMITER__@@SUBJECT:fix: Feature didn't work right@@__CHGLOG_DELIMITER__@@BODY:This is body message. + +Fixes JIRA-1112