feat: add support for rendering .Body after .Subject as part of list (#121)

When attempting to render a commit body below the summary line of the
commit there are two problems:

1) The text needs to be indented two spaces to appear as part of the
list.

2) Notes (e.g. BREAKING CHANGE) are included in the body and end up
being repeating in a Notes section (if this is part of your template).

To address #1 add an `indent` func to the template parsing.
To address #2 add a `TrimmedBody` to the `Commit` fields.

The `TrimmedBody` will include everything in `Body` but not any
`Ref`s, `Note`s, `Mention`s, `CoAuthors`, or `Signers`.

Both the CoAuthors and Signers are now first class in the Commit
struct.

With both of these a template block like:

```
{{ if .TrimmedBody -}}
{{ indent .TrimmedBody 2 }}
{{ end -}}
```

Will render the trimmed down body section as intended.

See TestGeneratorWithTimmedBody for example of desired output.
This commit is contained in:
Mason J. Katz 2021-03-22 14:04:57 -07:00 committed by GitHub
parent 2caa67cc76
commit 9a0d584745
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 239 additions and 28 deletions

View file

@ -343,6 +343,14 @@ func (gen *Generator) render(w io.Writer, unreleased *Unreleased, versions []*Ve
}
return ""
},
// indent all lines of s n spaces
"indent": func(s string, n int) string {
if len(s) == 0 {
return ""
}
pad := strings.Repeat(" ", n)
return pad + strings.ReplaceAll(s, "\n", "\n"+pad)
},
}
fname := filepath.Base(gen.config.Template)

View file

@ -463,3 +463,82 @@ func TestGeneratorWithTagFiler(t *testing.T) {
[Unreleased]: https://github.com/git-chglog/git-chglog/compare/v1.0.0...HEAD`, strings.TrimSpace(buf.String()))
}
func TestGeneratorWithTimmedBody(t *testing.T) {
assert := assert.New(t)
testName := "trimmed_body"
setup(testName, func(commit commitFunc, tag tagFunc, _ gitcmd.Client) {
commit("2018-01-01 00:00:00", "feat: single line commit", "")
commit("2018-01-01 00:01:00", "feat: multi-line commit", `
More details about the change and why it went in.
BREAKING CHANGE:
When using .TrimmedBody Notes are not included and can only appear in the Notes section.
Signed-off-by: First Last <first.last@mail.com>
Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>`)
commit("2018-01-01 00:00:00", "feat: another single line commit", "")
tag("1.0.0")
})
gen := NewGenerator(NewLogger(os.Stdout, os.Stderr, false, true),
&Config{
Bin: "git",
WorkingDir: filepath.Join(testRepoRoot, testName),
Template: filepath.Join(cwd, "testdata", testName+".md"),
Info: &Info{
Title: "CHANGELOG Example",
RepositoryURL: "https://github.com/git-chglog/git-chglog",
},
Options: &Options{
CommitFilters: map[string][]string{
"Type": {
"feat",
},
},
CommitSortBy: "Scope",
CommitGroupBy: "Type",
CommitGroupSortBy: "Title",
CommitGroupTitleMaps: map[string]string{
"feat": "Features",
},
HeaderPattern: "^(\\w*)(?:\\(([\\w\\$\\.\\-\\*\\s]*)\\))?\\:\\s(.*)$",
HeaderPatternMaps: []string{
"Type",
"Scope",
"Subject",
},
NoteKeywords: []string{
"BREAKING CHANGE",
},
},
})
buf := &bytes.Buffer{}
err := gen.Generate(buf, "")
output := strings.ReplaceAll(strings.TrimSpace(buf.String()), "\r\n", "\n")
assert.Nil(err)
assert.Equal(`<a name="unreleased"></a>
## [Unreleased]
<a name="1.0.0"></a>
## 1.0.0 - 2018-01-01
### Features
- another single line commit
- multi-line commit
More details about the change and why it went in.
- single line commit
### BREAKING CHANGE
When using .TrimmedBody Notes are not included and can only appear in the Notes section.
[Unreleased]: https://github.com/git-chglog/git-chglog/compare/1.0.0...HEAD`, output)
}

View file

@ -59,6 +59,8 @@ type commitParser struct {
reIssue *regexp.Regexp
reNotes *regexp.Regexp
reMention *regexp.Regexp
reSignOff *regexp.Regexp
reCoAuthor *regexp.Regexp
reJiraIssueDescription *regexp.Regexp
}
@ -81,6 +83,8 @@ func newCommitParser(logger *Logger, client gitcmd.Client, jiraClient JiraClient
reIssue: regexp.MustCompile("(?:" + joinedIssuePrefix + ")(\\d+)"),
reNotes: regexp.MustCompile("^(?i)\\s*(" + joinedNoteKeywords + ")[:\\s]+(.*)"),
reMention: regexp.MustCompile(`@([\w-]+)`),
reSignOff: regexp.MustCompile(`Signed-off-by:\s+([\p{L}\s\-\[\]]+)\s+<([\w+\-\[\].@]+)>`),
reCoAuthor: regexp.MustCompile(`Co-authored-by:\s+([\p{L}\s\-\[\]]+)\s+<([\w+\-\[\].@]+)>`),
reJiraIssueDescription: regexp.MustCompile(opts.JiraIssueDescriptionPattern),
}
}
@ -228,6 +232,36 @@ func (p *commitParser) processHeader(commit *Commit, input string) {
}
}
func (p *commitParser) extractLineMetadata(commit *Commit, line string) bool {
meta := false
refs := p.parseRefs(line)
if len(refs) > 0 {
meta = true
commit.Refs = append(commit.Refs, refs...)
}
mentions := p.parseMentions(line)
if len(mentions) > 0 {
meta = true
commit.Mentions = append(commit.Mentions, mentions...)
}
coAuthors := p.parseCoAuthors(line)
if len(coAuthors) > 0 {
meta = true
commit.CoAuthors = append(commit.CoAuthors, coAuthors...)
}
signers := p.parseSigners(line)
if len(signers) > 0 {
meta = true
commit.Signers = append(commit.Signers, signers...)
}
return meta
}
func (p *commitParser) processBody(commit *Commit, input string) {
input = convNewline(input, "\n")
@ -237,30 +271,29 @@ func (p *commitParser) processBody(commit *Commit, input string) {
// notes & refs & mentions
commit.Notes = []*Note{}
inNote := false
trim := false
fenceDetector := newMdFenceDetector()
lines := strings.Split(input, "\n")
// body without notes & refs & mentions
trimmedBody := make([]string, 0, len(lines))
for _, line := range lines {
if !inNote {
trim = false
}
fenceDetector.Update(line)
if !fenceDetector.InCodeblock() {
refs := p.parseRefs(line)
if len(refs) > 0 {
inNote = false
commit.Refs = append(commit.Refs, refs...)
}
mentions := p.parseMentions(line)
if len(mentions) > 0 {
inNote = false
commit.Mentions = append(commit.Mentions, mentions...)
}
if !fenceDetector.InCodeblock() && p.extractLineMetadata(commit, line) {
trim = true
inNote = false
}
// Q: should this check also only be outside of code blocks?
res := p.reNotes.FindAllStringSubmatch(line, -1)
if len(res) > 0 {
inNote = true
trim = true
for _, r := range res {
commit.Notes = append(commit.Notes, &Note{
Title: r[1],
@ -271,8 +304,13 @@ func (p *commitParser) processBody(commit *Commit, input string) {
last := commit.Notes[len(commit.Notes)-1]
last.Body = last.Body + "\n" + line
}
if !trim {
trimmedBody = append(trimmedBody, line)
}
}
commit.TrimmedBody = strings.TrimSpace(strings.Join(trimmedBody, "\n"))
p.trimSpaceInNotes(commit)
}
@ -317,6 +355,30 @@ func (p *commitParser) parseRefs(input string) []*Ref {
return refs
}
func (p *commitParser) parseSigners(input string) []Contact {
res := p.reSignOff.FindAllStringSubmatch(input, -1)
contacts := make([]Contact, len(res))
for i, r := range res {
contacts[i].Name = r[1]
contacts[i].Email = r[2]
}
return contacts
}
func (p *commitParser) parseCoAuthors(input string) []Contact {
res := p.reCoAuthor.FindAllStringSubmatch(input, -1)
contacts := make([]Contact, len(res))
for i, r := range res {
contacts[i].Name = r[1]
contacts[i].Email = r[2]
}
return contacts
}
func (p *commitParser) parseMentions(input string) []string {
res := p.reMention.FindAllStringSubmatch(input, -1)
mentions := make([]string, len(res))

View file

@ -103,13 +103,14 @@ func TestCommitParserParse(t *testing.T) {
Source: "",
},
},
Notes: []*Note{},
Mentions: []string{},
Header: "feat(*): Add new feature #123",
Type: "feat",
Scope: "*",
Subject: "Add new feature #123",
Body: "",
Notes: []*Note{},
Mentions: []string{},
Header: "feat(*): Add new feature #123",
Type: "feat",
Scope: "*",
Subject: "Add new feature #123",
Body: "",
TrimmedBody: "",
},
{
Hash: &Hash{
@ -166,6 +167,7 @@ Fixes #3
Closes #1
BREAKING CHANGE: This is breaking point message.`,
TrimmedBody: `This is body message.`,
},
{
Hash: &Hash{
@ -200,6 +202,7 @@ BREAKING CHANGE: This is breaking point message.`,
@tsuyoshiwada
@hogefuga
@FooBarBaz`,
TrimmedBody: `Has mention body`,
},
{
Hash: &Hash{
@ -280,6 +283,7 @@ class MyController extends Controller {
Fixes #123
Closes username/repository#456`, "```", "```"),
TrimmedBody: `This mixed body message.`,
},
{
Hash: &Hash{
@ -300,14 +304,15 @@ Closes username/repository#456`, "```", "```"),
Revert: &Revert{
Header: "fix(core): commit message",
},
Refs: []*Ref{},
Notes: []*Note{},
Mentions: []string{},
Header: "Revert \"fix(core): commit message\"",
Type: "",
Scope: "",
Subject: "",
Body: "This reverts commit f755db78dcdf461dc42e709b3ab728ceba353d1d.",
Refs: []*Ref{},
Notes: []*Note{},
Mentions: []string{},
Header: "Revert \"fix(core): commit message\"",
Type: "",
Scope: "",
Subject: "",
Body: "This reverts commit f755db78dcdf461dc42e709b3ab728ceba353d1d.",
TrimmedBody: "This reverts commit f755db78dcdf461dc42e709b3ab728ceba353d1d.",
},
}, commits)
}

View file

@ -8,6 +8,12 @@ type Hash struct {
Short string
}
// Contact of co-authors and signers
type Contact struct {
Name string
Email string
}
// Author of commit
type Author struct {
Name string
@ -70,6 +76,8 @@ type Commit struct {
Refs []*Ref
Notes []*Note
Mentions []string // Name of the user included in the commit header or body
CoAuthors []Contact // (e.g. `Co-authored-by: user <user@email>`)
Signers []Contact // (e.g. `Signed-off-by: user <user@email>`)
JiraIssue *JiraIssue // If no issue id found in header, `nil` is assigned
Header string // (e.g. `feat(core)[RNWY-310]: Add new feature`)
Type string // (e.g. `feat`)
@ -77,6 +85,7 @@ type Commit struct {
Subject string // (e.g. `Add new feature`)
JiraIssueID string // (e.g. `RNWY-310`)
Body string
TrimmedBody string // Body without any Notes/Refs/Mentions/CoAuthors/Signers
}
// CommitGroup is a collection of commits grouped according to the `CommitGroupBy` option

48
testdata/trimmed_body.md vendored Normal file
View file

@ -0,0 +1,48 @@
{{ if .Versions -}}
<a name="unreleased"></a>
## [Unreleased]
{{ if .Unreleased.CommitGroups -}}
{{ range .Unreleased.CommitGroups -}}
### {{ .Title }}
{{ range .Commits -}}
- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
{{ if .TrimmedBody -}}
{{ indent .TrimmedBody 2 }}
{{ end -}}
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}
{{ range .Versions }}
<a name="{{ .Tag.Name }}"></a>
## {{ if .Tag.Previous }}[{{ .Tag.Name }}]{{ else }}{{ .Tag.Name }}{{ end }} - {{ datetime "2006-01-02" .Tag.Date }}
{{ range .CommitGroups -}}
### {{ .Title }}
{{ range .Commits -}}
- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }}
{{ if .TrimmedBody -}}
{{ indent .TrimmedBody 2 }}
{{ end -}}
{{ end }}
{{ end -}}
{{- if .NoteGroups -}}
{{ range .NoteGroups -}}
### {{ .Title }}
{{ range .Notes }}
{{ .Body }}
{{ end }}
{{ end -}}
{{ end -}}
{{ end -}}
{{- if .Versions }}
[Unreleased]: {{ .Info.RepositoryURL }}/compare/{{ $latest := index .Versions 0 }}{{ $latest.Tag.Name }}...HEAD
{{ range .Versions -}}
{{ if .Tag.Previous -}}
[{{ .Tag.Name }}]: {{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}
{{ end -}}
{{ end -}}
{{ end -}}