diff --git a/go.mod b/go.mod index 4b0f92d..fd6b4cc 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/haunt98/clock v0.2.0 github.com/haunt98/color v0.2.0 github.com/haunt98/markdown-go v0.4.0 + github.com/haunt98/rst-go v0.2.0 // indirect github.com/kevinburke/ssh_config v1.1.0 // indirect github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index 5273d9c..25aae6e 100644 --- a/go.sum +++ b/go.sum @@ -44,6 +44,8 @@ github.com/haunt98/color v0.2.0 h1:SvzXU4cmQTrxQCJ5WaLyCF1lqhJilkfeF5bJgWatGBY= github.com/haunt98/color v0.2.0/go.mod h1:lp75YVDcNuGgMemx8srNli5gwH8a6qEfFKgPl7e1q8s= github.com/haunt98/markdown-go v0.4.0 h1:Nurl3oAc3gn4Ph/0PqyEhDxf9W+BzqYV7FTIrz/+M4E= github.com/haunt98/markdown-go v0.4.0/go.mod h1:E18hIq5n6hy+aQ+U6o+MuMJdZoq7/5n50FG+JsjuFIs= +github.com/haunt98/rst-go v0.2.0 h1:9hGgw7MtDMLoTx4+NEOJ/0aZXUHIb0nesJ4sqUmMA/U= +github.com/haunt98/rst-go v0.2.0/go.mod h1:Bj9IO8ktaGBdOBjEZw9CTYCmOUv/2GIXPhV/rfXEJN0= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= diff --git a/internal/changelog/const.go b/internal/changelog/const.go new file mode 100644 index 0000000..0cc0a84 --- /dev/null +++ b/internal/changelog/const.go @@ -0,0 +1,7 @@ +package changelog + +const ( + title = "CHANGELOG" + + defaultNodesLen = 10 +) diff --git a/internal/changelog/markdown.go b/internal/changelog/markdown.go index c0aeccc..8b3fe8e 100644 --- a/internal/changelog/markdown.go +++ b/internal/changelog/markdown.go @@ -11,10 +11,6 @@ import ( ) const ( - title = "CHANGELOG" - - defaultNodesLen = 10 - firstLevel = 1 secondLevel = 2 thirdLevel = 3 diff --git a/internal/changelog/rst.go b/internal/changelog/rst.go new file mode 100644 index 0000000..38d23a2 --- /dev/null +++ b/internal/changelog/rst.go @@ -0,0 +1,83 @@ +package changelog + +import ( + "fmt" + "strings" + "time" + + "github.com/haunt98/changeloguru/internal/convention" + "github.com/haunt98/clock" + "github.com/haunt98/rst-go" +) + +func GenerateRST(commits []convention.Commit, scopes map[string]struct{}, version string, when time.Time) []rst.Node { + if len(commits) == 0 { + return nil + } + + commitBases := make(map[string][]rst.Node) + commitBases[addedType] = make([]rst.Node, 0, defaultNodesLen) + commitBases[fixedType] = make([]rst.Node, 0, defaultNodesLen) + commitBases[othersType] = make([]rst.Node, 0, defaultNodesLen) + + for _, commit := range commits { + // If scopes is empty or commit scope is empty, pass all commits + if len(scopes) != 0 && commit.Scope != "" { + // Skip commit outside scopes + if _, ok := scopes[commit.Scope]; !ok { + continue + } + } + + t := getType(commit.Type) + switch t { + case addedType: + commitBases[addedType] = append(commitBases[addedType], rst.NewListItem(commit.String())) + case fixedType: + commitBases[fixedType] = append(commitBases[fixedType], rst.NewListItem(commit.String())) + case othersType: + commitBases[othersType] = append(commitBases[othersType], rst.NewListItem(commit.String())) + default: + continue + } + } + + // Adding each type and header to nodes + nodes := make([]rst.Node, 0, len(commitBases[addedType])+len(commitBases[fixedType])+len(commitBases[othersType])) + + if len(commitBases[addedType]) != 0 { + nodes = append(nodes, rst.NewSubSection(addedType)) + nodes = append(nodes, commitBases[addedType]...) + } + + if len(commitBases[fixedType]) != 0 { + nodes = append(nodes, rst.NewSubSection(fixedType)) + nodes = append(nodes, commitBases[fixedType]...) + } + + if len(commitBases[othersType]) != 0 { + nodes = append(nodes, rst.NewSubSection(othersType)) + nodes = append(nodes, commitBases[othersType]...) + } + + // Adding title, version to nodes + versionHeader := fmt.Sprintf("%s (%s)", version, clock.FormatDate(when)) + nodes = append([]rst.Node{ + rst.NewTitle(title), + rst.NewSection(versionHeader), + }, nodes...) + + return nodes +} + +func ParseRST(data string) []rst.Node { + lines := strings.Split(data, "\n\n") + nodes := rst.Parse(lines) + + // Remove title + if len(nodes) > 0 && rst.Equal(nodes[0], rst.NewTitle(title)) { + nodes = nodes[1:] + } + + return nodes +} diff --git a/internal/changelog/rst_test.go b/internal/changelog/rst_test.go new file mode 100644 index 0000000..864f2ea --- /dev/null +++ b/internal/changelog/rst_test.go @@ -0,0 +1,129 @@ +package changelog + +import ( + "testing" + "time" + + "github.com/haunt98/changeloguru/internal/convention" + "github.com/haunt98/rst-go" + "github.com/sebdah/goldie/v2" +) + +func TestGenerateRST(t *testing.T) { + tests := []struct { + name string + commits []convention.Commit + scopes map[string]struct{} + version string + when time.Time + }{ + { + name: "empty old data", + commits: []convention.Commit{ + { + RawHeader: "feat: new feature", + Type: convention.FeatType, + }, + { + RawHeader: "fix: new fix", + Type: convention.FixType, + }, + { + RawHeader: "chore: new build", + Type: convention.ChoreType, + }, + }, + version: "v1.0.0", + when: time.Date(2020, 1, 18, 0, 0, 0, 0, time.Local), + }, + { + name: "many commits", + commits: []convention.Commit{ + { + RawHeader: "feat: new feature", + Type: convention.FeatType, + }, + { + RawHeader: "feat: support new client", + Type: convention.FeatType, + }, + { + RawHeader: "fix: new fix", + Type: convention.FixType, + }, + { + RawHeader: "fix: wrong color", + Type: convention.FixType, + }, + { + RawHeader: "chore: new build", + Type: convention.ChoreType, + }, + { + RawHeader: "chore(github): release on github", + Type: convention.ChoreType, + }, + { + RawHeader: "chore(gitlab): release on gitlab", + Type: convention.ChoreType, + }, + { + RawHeader: "unleash the dragon", + Type: convention.MiscType, + }, + }, + version: "v1.0.0", + when: time.Date(2020, 1, 18, 0, 0, 0, 0, time.Local), + }, + { + name: "ignore commits outside scope", + commits: []convention.Commit{ + { + RawHeader: "feat: new feature", + Type: convention.FeatType, + }, + { + RawHeader: "feat(a): support new client", + Type: convention.FeatType, + Scope: "a", + }, + { + RawHeader: "fix: new fix", + Type: convention.FixType, + }, + { + RawHeader: "fix(b): wrong color", + Type: convention.FixType, + Scope: "b", + }, + { + RawHeader: "chore(a): new build", + Type: convention.ChoreType, + Scope: "a", + }, + { + RawHeader: "chore(b): new build", + Type: convention.ChoreType, + Scope: "b", + }, + { + RawHeader: "unleash the dragon", + Type: convention.MiscType, + }, + }, + scopes: map[string]struct{}{ + "a": {}, + }, + version: "v1.0.0", + when: time.Date(2020, 3, 22, 0, 0, 0, 0, time.Local), + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + g := goldie.New(t) + got := GenerateRST(tc.commits, tc.scopes, tc.version, tc.when) + g.Assert(t, t.Name(), []byte(rst.GenerateText(got))) + }) + } +} diff --git a/internal/changelog/testdata/TestGenerateRST/empty_old_data.golden b/internal/changelog/testdata/TestGenerateRST/empty_old_data.golden new file mode 100644 index 0000000..6114892 --- /dev/null +++ b/internal/changelog/testdata/TestGenerateRST/empty_old_data.golden @@ -0,0 +1,21 @@ +========= +CHANGELOG +========= + +v1.0.0 (2020-01-18) +=================== + +Added +----- + +- feat: new feature + +Fixed +----- + +- fix: new fix + +Others +------ + +- chore: new build diff --git a/internal/changelog/testdata/TestGenerateRST/ignore_commits_outside_scope.golden b/internal/changelog/testdata/TestGenerateRST/ignore_commits_outside_scope.golden new file mode 100644 index 0000000..77f756f --- /dev/null +++ b/internal/changelog/testdata/TestGenerateRST/ignore_commits_outside_scope.golden @@ -0,0 +1,25 @@ +========= +CHANGELOG +========= + +v1.0.0 (2020-03-22) +=================== + +Added +----- + +- feat: new feature + +- feat(a): support new client + +Fixed +----- + +- fix: new fix + +Others +------ + +- chore(a): new build + +- unleash the dragon diff --git a/internal/changelog/testdata/TestGenerateRST/many_commits.golden b/internal/changelog/testdata/TestGenerateRST/many_commits.golden new file mode 100644 index 0000000..5222398 --- /dev/null +++ b/internal/changelog/testdata/TestGenerateRST/many_commits.golden @@ -0,0 +1,31 @@ +========= +CHANGELOG +========= + +v1.0.0 (2020-01-18) +=================== + +Added +----- + +- feat: new feature + +- feat: support new client + +Fixed +----- + +- fix: new fix + +- fix: wrong color + +Others +------ + +- chore: new build + +- chore(github): release on github + +- chore(gitlab): release on gitlab + +- unleash the dragon diff --git a/internal/cli/action.go b/internal/cli/action.go index b88e547..a284e0c 100644 --- a/internal/cli/action.go +++ b/internal/cli/action.go @@ -9,6 +9,7 @@ import ( const ( currentDir = "." markdownFiletype = "md" + rstFiletype = "rst" defaultRepository = currentDir defaultOutput = currentDir diff --git a/internal/cli/action_generate.go b/internal/cli/action_generate.go index 4c793a0..56c436c 100644 --- a/internal/cli/action_generate.go +++ b/internal/cli/action_generate.go @@ -11,6 +11,7 @@ import ( "github.com/haunt98/changeloguru/internal/convention" "github.com/haunt98/changeloguru/internal/git" "github.com/haunt98/markdown-go" + "github.com/haunt98/rst-go" "github.com/pkg/diff" "github.com/pkg/diff/write" "github.com/urfave/cli/v2" @@ -75,6 +76,8 @@ func (a *action) generateChangelog(commits []convention.Commit) error { switch a.flags.filetype { case markdownFiletype: return a.generateMarkdownChangelog(realOutput, version, commits) + case rstFiletype: + return a.generateRSTChangelog(realOutput, version, commits) default: return fmt.Errorf("unknown filetype %s", a.flags.filetype) } @@ -138,3 +141,35 @@ func (a *action) generateMarkdownChangelog(output, version string, commits []con return nil } + +func (a *action) generateRSTChangelog(output, version string, commits []convention.Commit) error { + // If changelog file already exist, parse markdown from exist file + var oldNodes []rst.Node + bytes, err := os.ReadFile(output) + if err == nil { + oldNodes = changelog.ParseRST(string(bytes)) + } + + // Generate markdown from commits + newNodes := changelog.GenerateRST(commits, a.flags.scopes, version, time.Now()) + + // Final changelog with new commits above old commits + nodes := append(newNodes, oldNodes...) + changelogText := rst.GenerateText(nodes) + + // Demo run + if a.flags.dryRun { + if err := diff.Text("old", "new", string(bytes), changelogText, os.Stdout, write.TerminalColor()); err != nil { + return fmt.Errorf("failed to diff old and new changelog: %w", err) + } + + return nil + } + + // Actually writing to changelog file + if err := os.WriteFile(output, []byte(changelogText), 0644); err != nil { + return fmt.Errorf("failed to write file %s: %w", output, err) + } + + return nil +}