Skip to content

Commit

Permalink
feat: support trimming any text lines preceding the line containing t…
Browse files Browse the repository at this point in the history
…he conventional commit type (#416)
  • Loading branch information
albertocsm authored Jun 26, 2024
1 parent 76674a7 commit 178aa79
Show file tree
Hide file tree
Showing 15 changed files with 189 additions and 13 deletions.
10 changes: 10 additions & 0 deletions cmd/uplift/changelog.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ uplift changelog --no-stage
# Generate a changelog with multiline commit messages
uplift changelog --multiline
# Generate a changelog trimming any lines preceding the conventional commit type
uplift changelog --trim-header
# Generate a changelog with prerelease tags being skipped
uplift changelog --skip-prerelease`
)
Expand All @@ -94,6 +97,7 @@ type changelogOptions struct {
Sort string
Multiline bool
SkipPrerelease bool
TrimHeader bool
*globalOptions
}

Expand Down Expand Up @@ -136,6 +140,7 @@ func newChangelogCmd(gopts *globalOptions, out io.Writer) *changelogCommand {
f.StringVar(&chglogCmd.Opts.Sort, "sort", "", "the sort order of commits within each changelog entry")
f.BoolVar(&chglogCmd.Opts.Multiline, "multiline", false, "include multiline commit messages within changelog (skips truncation)")
f.BoolVar(&chglogCmd.Opts.SkipPrerelease, "skip-prerelease", false, "skips the creation of a changelog entry for a prerelease")
f.BoolVar(&chglogCmd.Opts.TrimHeader, "trim-header", false, "strip any lines preceding the conventional commit type in the commit message")

chglogCmd.Cmd = cmd
return chglogCmd
Expand Down Expand Up @@ -204,6 +209,11 @@ func setupChangelogContext(opts changelogOptions, out io.Writer) (*context.Conte
ctx.Changelog.SkipPrerelease = ctx.Config.Changelog.SkipPrerelease
}

ctx.Changelog.TrimHeader = opts.TrimHeader
if !ctx.Changelog.TrimHeader && ctx.Config.Changelog != nil {
ctx.Changelog.TrimHeader = ctx.Config.Changelog.TrimHeader
}

// Sort order provided as a command-line flag takes precedence
ctx.Changelog.Sort = opts.Sort
if ctx.Changelog.Sort == "" && cfg.Changelog != nil {
Expand Down
22 changes: 22 additions & 0 deletions cmd/uplift/changelog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,25 @@ fix: 1
assert.NotContains(t, cl, "## 0.1.0-pre.2")
assert.NotContains(t, cl, "## 0.1.0-pre.1")
}

func TestChangelog_TrimHeader(t *testing.T) {
log := `>(tag: 0.1.0) feat: this is a commit
>this line that should be ignored
this line that should also be ignored
feat: second commit`
gittest.InitRepository(t, gittest.WithLog(log))

chglogCmd := newChangelogCmd(noChangesPushed(), os.Stdout)
chglogCmd.Cmd.SetArgs([]string{"--trim-header"})

err := chglogCmd.Cmd.Execute()
require.NoError(t, err)

assert.True(t, changelogExists(t))

cl := readChangelog(t)
assert.Contains(t, cl, `feat: this is a commit`)
assert.Contains(t, cl, "feat: second commit")
assert.NotContains(t, cl, "this line that should be ignored")
assert.NotContains(t, cl, "this line that should also be ignored")
}
6 changes: 6 additions & 0 deletions cmd/uplift/release.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ type releaseOptions struct {
Sort string
Multiline bool
SkipPrerelease bool
TrimHeader bool
*globalOptions
}

Expand Down Expand Up @@ -137,6 +138,7 @@ func newReleaseCmd(gopts *globalOptions, out io.Writer) *releaseCommand {
f.StringVar(&relCmd.Opts.Sort, "sort", "", "the sort order of commits within each changelog entry")
f.BoolVar(&relCmd.Opts.Multiline, "multiline", false, "include multiline commit messages within changelog (skips truncation)")
f.BoolVar(&relCmd.Opts.SkipPrerelease, "skip-changelog-prerelease", false, "skips the creation of a changelog entry for a prerelease")
f.BoolVar(&relCmd.Opts.TrimHeader, "trim-header", false, "strip any lines preceding the conventional commit type in the commit message")

relCmd.Cmd = cmd
return relCmd
Expand Down Expand Up @@ -209,6 +211,10 @@ func setupReleaseContext(opts releaseOptions, out io.Writer) (*context.Context,
if !ctx.Changelog.SkipPrerelease && ctx.Config.Changelog != nil {
ctx.Changelog.SkipPrerelease = ctx.Config.Changelog.SkipPrerelease
}
ctx.Changelog.TrimHeader = opts.TrimHeader
if !ctx.Changelog.TrimHeader && ctx.Config.Changelog != nil {
ctx.Changelog.TrimHeader = ctx.Config.Changelog.TrimHeader
}

// By default ensure the ci(uplift): commits are excluded also
ctx.Changelog.Exclude = append(ctx.Changelog.Exclude, "ci(uplift):")
Expand Down
22 changes: 22 additions & 0 deletions cmd/uplift/release_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,3 +310,25 @@ func TestRelease_SkipChangelogPrerelease(t *testing.T) {
assert.NotContains(t, cl, "## 0.1.0-pre.2")
assert.NotContains(t, cl, "## 0.1.0-pre.1")
}

func TestRelease_TrimHeader(t *testing.T) {
log := `> feat: this is a commit
>this line that should be ignored
this line that should also be ignored
feat: second commit`
gittest.InitRepository(t, gittest.WithLog(log))

relCmd := newReleaseCmd(noChangesPushed(), os.Stdout)
relCmd.Cmd.SetArgs([]string{"--trim-header"})

err := relCmd.Cmd.Execute()
require.NoError(t, err)

assert.True(t, changelogExists(t))

cl := readChangelog(t)
assert.Contains(t, cl, `feat: this is a commit`)
assert.Contains(t, cl, "feat: second commit")
assert.NotContains(t, cl, "this line that should be ignored")
assert.NotContains(t, cl, "this line that should also be ignored")
}
8 changes: 8 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,11 @@ Prevent any prerelease from being included in your changelog. Upon your next rel
```sh
uplift changelog --skip-prerelease
```

## Trim Header

Trims any lines preceding the conventional commit type in the commit message

```sh
uplift changelog --skip-prerelease
```
2 changes: 2 additions & 0 deletions docs/reference/cli/release.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ uplift release --no-prefix
commits for the changelog
--multiline include multiline commit messages within
changelog (skips truncation)
--trim-header trims any lines preceding the conventional commit type
in the commit message
--no-prefix strip the default 'v' prefix from the next
calculated semantic version
--prerelease string append a prerelease suffix to next calculated
Expand Down
12 changes: 8 additions & 4 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ changelog:
# default behaviour of truncating a commit message to its first line
multiline: true
# Trims any lines preceding the conventional commit type in the
# commit message
trimHeader: true
# Skips generating a changelog for any prerelease. All commits from
# a prerelease will be appended to the changelog entry for the next
# release
Expand Down Expand Up @@ -243,10 +247,10 @@ git:
skipTag: true
skipBranch: false
# A list of case sensitive files that will be committed to the repo when the
# release is created. Typically these files are managed as part of the
# release process e.g. generated by `hooks`. This allows additional
# information to be included in a release such as a
# A list of case sensitive files that will be committed to the repo when the
# release is created. Typically these files are managed as part of the
# release process e.g. generated by `hooks`. This allows additional
# information to be included in a release such as a
# Software Bill Of Materials (SBOM) that is created by an external tool.
includeArtifacts:
- file.txt
Expand Down
12 changes: 11 additions & 1 deletion docs/static/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,11 @@
"description": "Include multiline commit messages within the changelog. Disables default behaviour of truncating a commit message to its first line",
"type": "boolean"
},
"trimHeader": {
"$comment": "https://upliftci.dev/reference/config#changelog",
"description": "Trims any lines preceding the conventional commit type in the commit message",
"type": "boolean"
},
"skipPrerelease": {
"$comment": "https://upliftci.dev/reference/config#changelog",
"description": "Skips generating a changelog for any prerelease. All commits from a prerelease will be appended to the changelog entry for the next release",
Expand Down Expand Up @@ -190,9 +195,14 @@
"multiline"
]
},
{
"required": [
"trimHeader"
]
},
{
"skipPrerelease": [
"multiline"
"skipPrerelease"
]
}
]
Expand Down
1 change: 1 addition & 0 deletions internal/config/uplift.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ type Changelog struct {
Include []string `yaml:"include" validate:"required_without_all=Sort Exclude,dive,min=1"`
Multiline bool `yaml:"multiline"`
SkipPrerelease bool `yaml:"skipPrerelease"`
TrimHeader bool `yaml:"trimHeader"`
}

// Git defines configuration for how uplift interacts with git
Expand Down
1 change: 1 addition & 0 deletions internal/context/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ type Changelog struct {
PreTag bool
Multiline bool
SkipPrerelease bool
TrimHeader bool
}

// New constructs a context that captures both runtime configuration and
Expand Down
37 changes: 33 additions & 4 deletions internal/semver/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ import (
// against a semantic version
type Increment string

type ParseOptions struct {
TrimHeader bool
}

const (
// NoIncrement represents no increment change to a semantic version
NoIncrement Increment = "None"
Expand All @@ -56,16 +60,26 @@ const (
// log against the conventional commit standards defined, @see:
// https://www.conventionalcommits.org/en/v1.0.0/
func ParseLog(log []git.LogEntry) Increment {
return ParseLogWithOptions(log, ParseOptions{TrimHeader: false})
}

func ParseLogWithOptions(log []git.LogEntry, options ParseOptions) Increment {
mode := NoIncrement
for _, entry := range log {
// Check for the existence of a conventional commit type
idx := strings.Index(entry.Message, colonSpace)
if idx == -1 {
colonSpaceIdx := strings.Index(entry.Message, colonSpace)
if colonSpaceIdx == -1 {
continue
}

leadingType := strings.ToUpper(entry.Message[:idx])
if leadingType[idx-1] == breakingBang || multilineBreaking(entry.Message) {
startIdx := 0
// Commit messages may have leading lines before the conventional commit type
if options.TrimHeader {
startIdx = FindStartIdx(entry.Message)
}

leadingType := strings.ToUpper(entry.Message[startIdx:colonSpaceIdx])
if leadingType[len(leadingType)-1] == breakingBang || multilineBreaking(entry.Message) {
return MajorIncrement
}

Expand Down Expand Up @@ -122,3 +136,18 @@ func multilineBreaking(msg string) bool {
return strings.HasPrefix(footer, "BREAKING CHANGE: ") ||
strings.HasPrefix(footer, "BREAKING-CHANGE: ")
}

func FindStartIdx(msg string) int {
colonIdx := strings.Index(msg, colonSpace)
if colonIdx == -1 {
return 0
}

trimmedMsg := msg[:colonIdx]
leadingLineBreakIdx := strings.LastIndex(trimmedMsg, "\n")
if leadingLineBreakIdx == -1 {
return 0
}

return leadingLineBreakIdx + 1
}
13 changes: 13 additions & 0 deletions internal/semver/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,16 @@ refactor: tidy up some bits of the code`,
inc := ParseLog(log)
assert.Equal(t, PatchIncrement, inc)
}

func TestParseLog_TrimHeader(t *testing.T) {
log := []git.LogEntry{
{
Message: `this line that should be ignored
this line that should also be ignored
feat: shiny new feature has been added`,
},
}

inc := ParseLogWithOptions(log, ParseOptions{TrimHeader: true})
assert.Equal(t, MinorIncrement, inc)
}
11 changes: 10 additions & 1 deletion internal/task/changelog/changelog.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,10 @@ func (t Task) Run(ctx *context.Context) error {
for i := range rels {
for j := range rels[i].Changes {
msg := rels[i].Changes[j].Message
if ctx.Changelog.TrimHeader {
startIdx := semver.FindStartIdx(msg)
msg = msg[startIdx:]
}
msg = strings.ReplaceAll(msg, "\n", "\n ")
msg = strings.ReplaceAll(msg, "\n \n", "\n\n")

Expand All @@ -165,8 +169,13 @@ func (t Task) Run(ctx *context.Context) error {
for i := range rels {
for j := range rels[i].Changes {
msg := rels[i].Changes[j].Message
if ctx.Changelog.TrimHeader {
startIdx := semver.FindStartIdx(msg)
msg = msg[startIdx:]
rels[i].Changes[j].Message = msg
}
if idx := strings.Index(msg, "\n"); idx > -1 {
rels[i].Changes[j].Message = strings.TrimSpace(msg[:idx])
rels[i].Changes[j].Message = strings.TrimSpace(msg)
}
}
}
Expand Down
43 changes: 41 additions & 2 deletions internal/task/changelog/changelog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ ci: tweak
}

func TestRun_MultilineMessages(t *testing.T) {
log := `> (tag: 1.1.0) feat: this is a multiline commmit
log := `> (tag: 1.1.0) feat: this is a multiline commit
That should be displayed across multiple lines within the changelog.
It should be formatted as expected.
Expand Down Expand Up @@ -770,7 +770,7 @@ With the correct indentation for rendering in markdown

expected := fmt.Sprintf(`## 1.1.0 - %s
- %s feat: this is a multiline commmit
- %s feat: this is a multiline commit
That should be displayed across multiple lines within the changelog.
It should be formatted as expected.
Expand Down Expand Up @@ -946,3 +946,42 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

assert.Equal(t, expected, readChangelog(t))
}

func TestRun_TrimHeader(t *testing.T) {
log := `> (tag: 1.1.0) feat: this is a commit
>this line that should be ignored
this line that should also be ignored
feat: second commit
> (tag: 1.0.0) not included in changelog`
gittest.InitRepository(t, gittest.WithLog(log))
glog := gittest.Log(t)

var buf bytes.Buffer
ctx := &context.Context{
Out: &buf,
Changelog: context.Changelog{
TrimHeader: true,
DiffOnly: true,
},
CurrentVersion: semver.Version{
Raw: "1.0.0",
},
NextVersion: semver.Version{
Raw: "1.1.0",
},
SCM: context.SCM{
Provider: context.Unrecognised,
},
}

err := Task{}.Run(ctx)
require.NoError(t, err)

expected := fmt.Sprintf(`## 1.1.0 - %s
- %s feat: this is a commit
- %s feat: second commit
`, changelogDate(t), fmt.Sprintf("`%s`", glog[0].AbbrevHash), fmt.Sprintf("`%s`", glog[1].AbbrevHash))

assert.Equal(t, expected, buf.String())
}
2 changes: 1 addition & 1 deletion internal/task/nextsemver/nextsemver.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ func (t Task) Run(ctx *context.Context) error {
}

// Identify any commit that will trigger the largest semantic version bump
inc := semver.ParseLog(glog.Commits)
inc := semver.ParseLogWithOptions(glog.Commits, semver.ParseOptions{TrimHeader: ctx.Changelog.TrimHeader})
if inc == semver.NoIncrement {
ctx.NoVersionChanged = true

Expand Down

0 comments on commit 178aa79

Please sign in to comment.