refactor: better changelog parser and generator (#11)
* refactor(changelog): remove old, new prefix * refactor: using new changelog func in cli * refactor: replace markdown.Generate to GenerateText * feat: move --verbose flag for generate command * fix: diff new changelog with old changelog * chore(markdown): remove unused GenerateLines Co-authored-by: Tran Hau <ngtranhau@gmail.com>main
parent
c495ff942c
commit
ab4ec67925
|
@ -19,51 +19,11 @@ const (
|
||||||
thirdLevel = 3
|
thirdLevel = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
type MarkdownGenerator struct {
|
func GenerateMarkdown(commits []convention.Commit, scopes map[string]struct{}, version string, t time.Time) []markdown.Node {
|
||||||
oldData string
|
|
||||||
version string
|
|
||||||
t time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMarkdownGenerator(oldData, version string, t time.Time) *MarkdownGenerator {
|
|
||||||
return &MarkdownGenerator{
|
|
||||||
oldData: oldData,
|
|
||||||
version: version,
|
|
||||||
t: t,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *MarkdownGenerator) Generate(commits []convention.Commit, scopes map[string]struct{}) string {
|
|
||||||
newBases := g.getNewNodes(commits, scopes)
|
|
||||||
if len(newBases) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes := make([]markdown.Node, 0, defaultNodesLen)
|
|
||||||
|
|
||||||
// title
|
|
||||||
nodes = append(nodes, markdown.NewHeader(firstLevel, title))
|
|
||||||
|
|
||||||
// version
|
|
||||||
nodes = append(nodes, markdown.NewHeader(secondLevel, g.getVersionHeader()))
|
|
||||||
|
|
||||||
// new
|
|
||||||
nodes = append(nodes, newBases...)
|
|
||||||
|
|
||||||
// old
|
|
||||||
oldNodes := g.getOldNodes()
|
|
||||||
nodes = append(nodes, oldNodes...)
|
|
||||||
|
|
||||||
return markdown.Generate(nodes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (g *MarkdownGenerator) getNewNodes(commits []convention.Commit, scopes map[string]struct{}) []markdown.Node {
|
|
||||||
if len(commits) == 0 {
|
if len(commits) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]markdown.Node, 0, defaultNodesLen)
|
|
||||||
|
|
||||||
commitBases := make(map[string][]markdown.Node)
|
commitBases := make(map[string][]markdown.Node)
|
||||||
commitBases[addedType] = make([]markdown.Node, 0, defaultNodesLen)
|
commitBases[addedType] = make([]markdown.Node, 0, defaultNodesLen)
|
||||||
commitBases[fixedType] = make([]markdown.Node, 0, defaultNodesLen)
|
commitBases[fixedType] = make([]markdown.Node, 0, defaultNodesLen)
|
||||||
|
@ -91,44 +51,46 @@ func (g *MarkdownGenerator) getNewNodes(commits []convention.Commit, scopes map[
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Adding each type and header to nodes
|
||||||
|
nodes := make([]markdown.Node, 0, len(commitBases[addedType])+len(commitBases[fixedType])+len(commitBases[othersType]))
|
||||||
|
|
||||||
if len(commitBases[addedType]) != 0 {
|
if len(commitBases[addedType]) != 0 {
|
||||||
result = append(result, markdown.NewHeader(thirdLevel, addedType))
|
nodes = append(nodes, markdown.NewHeader(thirdLevel, addedType))
|
||||||
result = append(result, commitBases[addedType]...)
|
nodes = append(nodes, commitBases[addedType]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(commitBases[fixedType]) != 0 {
|
if len(commitBases[fixedType]) != 0 {
|
||||||
result = append(result, markdown.NewHeader(thirdLevel, fixedType))
|
nodes = append(nodes, markdown.NewHeader(thirdLevel, fixedType))
|
||||||
result = append(result, commitBases[fixedType]...)
|
nodes = append(nodes, commitBases[fixedType]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(commitBases[othersType]) != 0 {
|
if len(commitBases[othersType]) != 0 {
|
||||||
result = append(result, markdown.NewHeader(thirdLevel, othersType))
|
nodes = append(nodes, markdown.NewHeader(thirdLevel, othersType))
|
||||||
result = append(result, commitBases[othersType]...)
|
nodes = append(nodes, commitBases[othersType]...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
// Adding title, version to nodes
|
||||||
|
nodes = append([]markdown.Node{
|
||||||
|
markdown.NewHeader(firstLevel, title),
|
||||||
|
markdown.NewHeader(secondLevel, getVersionHeader(version, t)),
|
||||||
|
}, nodes...)
|
||||||
|
|
||||||
|
return nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *MarkdownGenerator) getOldNodes() []markdown.Node {
|
func ParseMarkdown(data string) []markdown.Node {
|
||||||
if g.oldData == "" {
|
lines := strings.Split(data, string(markdown.NewlineToken))
|
||||||
return nil
|
nodes := markdown.Parse(lines)
|
||||||
|
|
||||||
|
// remove title
|
||||||
|
if len(nodes) > 0 && markdown.Equal(nodes[0], markdown.NewHeader(firstLevel, title)) {
|
||||||
|
nodes = nodes[1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
result := make([]markdown.Node, 0, defaultNodesLen)
|
return nodes
|
||||||
|
|
||||||
lines := strings.Split(g.oldData, string(markdown.NewlineToken))
|
|
||||||
|
|
||||||
result = append(result, markdown.Parse(lines)...)
|
|
||||||
|
|
||||||
// remove title as we will add later
|
|
||||||
if len(result) > 0 && markdown.Equal(result[0], markdown.NewHeader(firstLevel, title)) {
|
|
||||||
result = result[1:]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
func getVersionHeader(version string, t time.Time) string {
|
||||||
}
|
year, month, day := t.Date()
|
||||||
|
return fmt.Sprintf("%s (%d-%d-%d)", version, year, month, day)
|
||||||
func (g *MarkdownGenerator) getVersionHeader() string {
|
|
||||||
year, month, day := g.t.Date()
|
|
||||||
return fmt.Sprintf("%s (%d-%d-%d)", g.version, year, month, day)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,22 +5,20 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/haunt98/changeloguru/pkg/convention"
|
"github.com/haunt98/changeloguru/pkg/convention"
|
||||||
|
"github.com/haunt98/changeloguru/pkg/markdown"
|
||||||
"github.com/sebdah/goldie/v2"
|
"github.com/sebdah/goldie/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMarkdownGeneratorGenerate(t *testing.T) {
|
func TestGenerateMarkdown(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
oldData string
|
|
||||||
version string
|
|
||||||
t time.Time
|
|
||||||
commits []convention.Commit
|
commits []convention.Commit
|
||||||
scopes map[string]struct{}
|
scopes map[string]struct{}
|
||||||
|
version string
|
||||||
|
t time.Time
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "empty old data",
|
name: "empty old data",
|
||||||
version: "v1.0.0",
|
|
||||||
t: time.Date(2020, 1, 18, 0, 0, 0, 0, time.Local),
|
|
||||||
commits: []convention.Commit{
|
commits: []convention.Commit{
|
||||||
{
|
{
|
||||||
RawHeader: "feat: new feature",
|
RawHeader: "feat: new feature",
|
||||||
|
@ -35,11 +33,11 @@ func TestMarkdownGeneratorGenerate(t *testing.T) {
|
||||||
Type: convention.ChoreType,
|
Type: convention.ChoreType,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
version: "v1.0.0",
|
||||||
|
t: time.Date(2020, 1, 18, 0, 0, 0, 0, time.Local),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "many commits",
|
name: "many commits",
|
||||||
version: "v1.0.0",
|
|
||||||
t: time.Date(2020, 1, 18, 0, 0, 0, 0, time.Local),
|
|
||||||
commits: []convention.Commit{
|
commits: []convention.Commit{
|
||||||
{
|
{
|
||||||
RawHeader: "feat: new feature",
|
RawHeader: "feat: new feature",
|
||||||
|
@ -74,11 +72,11 @@ func TestMarkdownGeneratorGenerate(t *testing.T) {
|
||||||
Type: convention.MiscType,
|
Type: convention.MiscType,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
version: "v1.0.0",
|
||||||
|
t: time.Date(2020, 1, 18, 0, 0, 0, 0, time.Local),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "ignore commits outside scope",
|
name: "ignore commits outside scope",
|
||||||
version: "v1.0.0",
|
|
||||||
t: time.Date(2020, 3, 22, 0, 0, 0, 0, time.Local),
|
|
||||||
commits: []convention.Commit{
|
commits: []convention.Commit{
|
||||||
{
|
{
|
||||||
RawHeader: "feat: new feature",
|
RawHeader: "feat: new feature",
|
||||||
|
@ -116,15 +114,16 @@ func TestMarkdownGeneratorGenerate(t *testing.T) {
|
||||||
scopes: map[string]struct{}{
|
scopes: map[string]struct{}{
|
||||||
"a": struct{}{},
|
"a": struct{}{},
|
||||||
},
|
},
|
||||||
|
version: "v1.0.0",
|
||||||
|
t: time.Date(2020, 3, 22, 0, 0, 0, 0, time.Local),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
g := goldie.New(t)
|
g := goldie.New(t)
|
||||||
markdownGenerator := NewMarkdownGenerator(tc.oldData, tc.version, tc.t)
|
got := GenerateMarkdown(tc.commits, tc.scopes, tc.version, tc.t)
|
||||||
got := markdownGenerator.Generate(tc.commits, tc.scopes)
|
g.Assert(t, t.Name(), []byte(markdown.GenerateText(got)))
|
||||||
g.Assert(t, t.Name(), []byte(got))
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,27 +104,35 @@ func (a *action) getVersion() (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *action) generateMarkdownChangelog(output, version string, commits []convention.Commit) error {
|
func (a *action) generateMarkdownChangelog(output, version string, commits []convention.Commit) error {
|
||||||
// If CHANGELOG file already exist
|
// If changelog file already exist, parse markdown from exist file
|
||||||
var oldData string
|
var oldNodes []markdown.Node
|
||||||
bytes, err := os.ReadFile(output)
|
bytes, err := os.ReadFile(output)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
oldData = string(bytes)
|
oldNodes = changelog.ParseMarkdown(string(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
markdownGenerator := changelog.NewMarkdownGenerator(oldData, version, time.Now())
|
// Generate markdown from commits
|
||||||
newData := markdownGenerator.Generate(commits, a.flags.scopes)
|
newNodes := changelog.GenerateMarkdown(commits, a.flags.scopes, version, time.Now())
|
||||||
|
|
||||||
|
// Final changelog
|
||||||
|
nodes := append(newNodes, oldNodes...)
|
||||||
|
changelogText := markdown.GenerateText(nodes)
|
||||||
|
|
||||||
|
// Demo run
|
||||||
if a.flags.dryRun {
|
if a.flags.dryRun {
|
||||||
oldLines := strings.Split(oldData, string(markdown.NewlineToken))
|
oldLines := strings.Split(string(bytes), string(markdown.NewlineToken))
|
||||||
newLines := strings.Split(newData, string(markdown.NewlineToken))
|
newLines := strings.Split(changelogText, string(markdown.NewlineToken))
|
||||||
if err := diff.Slices("old", "new", oldLines, newLines, os.Stdout); err != nil {
|
if err := diff.Slices("old", "new",
|
||||||
return fmt.Errorf("failed to diff old and new data: %w", err)
|
oldLines, newLines,
|
||||||
|
os.Stdout); err != nil {
|
||||||
|
return fmt.Errorf("failed to diff old and new changelog: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(output, []byte(newData), 0o644); err != 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 fmt.Errorf("failed to write file %s: %w", output, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,8 +2,8 @@ package markdown
|
||||||
|
|
||||||
import "strings"
|
import "strings"
|
||||||
|
|
||||||
// Generate return string which represents all markdown nodes
|
// GenerateText return single string which represents all markdown nodes
|
||||||
func Generate(bases []Node) string {
|
func GenerateText(bases []Node) string {
|
||||||
lines := make([]string, len(bases))
|
lines := make([]string, len(bases))
|
||||||
for i, base := range bases {
|
for i, base := range bases {
|
||||||
lines[i] = base.String()
|
lines[i] = base.String()
|
||||||
|
|
|
@ -32,7 +32,7 @@ func TestGenerate(t *testing.T) {
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
got := Generate(tc.bases)
|
got := GenerateText(tc.bases)
|
||||||
assert.Equal(t, tc.want, got)
|
assert.Equal(t, tc.want, got)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,10 @@ const (
|
||||||
|
|
||||||
// Parse return all markdown nodes from lines
|
// Parse return all markdown nodes from lines
|
||||||
func Parse(lines []string) []Node {
|
func Parse(lines []string) []Node {
|
||||||
|
if len(lines) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
bases := make([]Node, 0, defaultBaseLen)
|
bases := make([]Node, 0, defaultBaseLen)
|
||||||
|
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
|
|
Loading…
Reference in New Issue