From d7136c1f64f15628fa9eee8e1bf18b9111b5d1fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20T=C3=B6lle?= Date: Mon, 15 Jul 2024 16:45:03 +0200 Subject: [PATCH] feat: update changelog file --- changelog.go | 88 +++++++++++++++++++++++++++++++++---------- changelog.md.tpl | 16 ++++++++ changelog_test.go | 96 ++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 179 insertions(+), 21 deletions(-) create mode 100644 changelog.md.tpl diff --git a/changelog.go b/changelog.go index ac43bb6..6074f15 100644 --- a/changelog.go +++ b/changelog.go @@ -2,45 +2,95 @@ package rp import ( "bytes" + _ "embed" + "fmt" "html/template" + "io" "log" + "os" + "regexp" "github.com/go-git/go-git/v5" ) +const ( + ChangelogFile = "CHANGELOG.md" + ChangelogFileBuffer = "CHANGELOG.md.tmp" + ChangelogHeader = "# Changelog" +) + var ( changelogTemplate *template.Template + + headerRegex = regexp.MustCompile(`^# Changelog\n`) ) +//go:embed changelog.md.tpl +var rawChangelogTemplate string + func init() { var err error - changelogTemplate, err = template.New("changelog").Parse(`## [{{.Version}}]({{.VersionLink}}) -{{- if (gt (len .Features) 0) }} -### Features - -{{ range .Features -}} -- {{ if .Scope }}**{{.Scope}}**: {{end}}{{.Description}} -{{ end -}} -{{- end -}} -{{- if (gt (len .Fixes) 0) }} -### Bug Fixes - -{{ range .Fixes -}} -- {{ if .Scope }}**{{.Scope}}**: {{end}}{{.Description}} -{{ end -}} -{{- end -}} -`, - ) + changelogTemplate, err = template.New("changelog").Parse(rawChangelogTemplate) if err != nil { log.Fatalf("failed to parse changelog template: %v", err) } } -func UpdateChangelog(wt *git.Worktree, commits []AnalyzedCommit) error { +func UpdateChangelogFile(wt *git.Worktree, newEntry string) error { + file, err := wt.Filesystem.OpenFile(ChangelogFile, os.O_RDWR|os.O_CREATE, 0644) + if err != nil { + return err + } + defer file.Close() + + content, err := io.ReadAll(file) + if err != nil { + return err + } + + headerIndex := headerRegex.FindIndex(content) + if headerIndex == nil && len(content) != 0 { + return fmt.Errorf("unexpected format of CHANGELOG.md, header does not match") + } + if headerIndex != nil { + // Remove the header from the content + content = content[headerIndex[1]:] + } + + err = file.Truncate(0) + if err != nil { + return err + } + _, err = file.Seek(0, io.SeekStart) + if err != nil { + return err + } + + _, err = file.Write([]byte(ChangelogHeader + "\n\n" + newEntry)) + if err != nil { + return err + } + + _, err = file.Write(content) + if err != nil { + return err + } + + // Close file to make sure it is written to disk. + err = file.Close() + if err != nil { + return err + } + + _, err = wt.Add(ChangelogFile) + if err != nil { + return err + } + return nil } -func formatChangelog(commits []AnalyzedCommit, version, link string) (string, error) { +func NewChangelogEntry(commits []AnalyzedCommit, version, link string) (string, error) { features := make([]AnalyzedCommit, 0) fixes := make([]AnalyzedCommit, 0) diff --git a/changelog.md.tpl b/changelog.md.tpl new file mode 100644 index 0000000..f466287 --- /dev/null +++ b/changelog.md.tpl @@ -0,0 +1,16 @@ +## [{{.Version}}]({{.VersionLink}}) +{{- if (gt (len .Features) 0) }} +### Features + +{{ range .Features -}} +- {{ if .Scope }}**{{.Scope}}**: {{end}}{{.Description}} +{{ end -}} +{{- end -}} +{{- if (gt (len .Fixes) 0) }} +### Bug Fixes + +{{ range .Fixes -}} +- {{ if .Scope }}**{{.Scope}}**: {{end}}{{.Description}} +{{ end -}} +{{- end -}} +` diff --git a/changelog_test.go b/changelog_test.go index c0f05bb..c5e2f20 100644 --- a/changelog_test.go +++ b/changelog_test.go @@ -1,16 +1,108 @@ package rp import ( + "io" + "log" "testing" + "github.com/go-git/go-git/v5" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/apricote/releaser-pleaser/internal/testutils" ) func ptr[T any](input T) *T { return &input } -func Test_formatChangelog(t *testing.T) { +func TestUpdateChangelogFile(t *testing.T) { + tests := []struct { + name string + repoFn testutils.Repo + entry string + expectedContent string + newFile bool + wantErr assert.ErrorAssertionFunc + }{ + { + name: "empty repo", + repoFn: testutils.WithTestRepo(), + entry: "## v1.0.0\n", + expectedContent: "# Changelog\n\n## v1.0.0\n", + newFile: true, + wantErr: assert.NoError, + }, + { + name: "repo with well-formatted changelog", + repoFn: testutils.WithTestRepo(testutils.WithCommit("feat: add changelog", testutils.WithFile(ChangelogFile, `# Changelog + +## v0.0.1 + +- Bazzle + +## v0.1.0 + +### Bazuuum +`))), + entry: "## v1.0.0\n\n- Version 1, juhu.\n", + expectedContent: `# Changelog + +## v1.0.0 + +- Version 1, juhu. + +## v0.0.1 + +- Bazzle + +## v0.1.0 + +### Bazuuum +`, + wantErr: assert.NoError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + repo := tt.repoFn(t) + wt, err := repo.Worktree() + require.NoError(t, err, "failed to get worktree") + + err = UpdateChangelogFile(wt, tt.entry) + if !tt.wantErr(t, err) { + return + } + + wtStatus, err := wt.Status() + require.NoError(t, err, "failed to get worktree status") + + assert.Len(t, wtStatus, 1, "worktree status does not have the expected entry number") + + changelogFileStatus := wtStatus.File(ChangelogFile) + + if tt.newFile { + assert.Equal(t, git.Unmodified, changelogFileStatus.Worktree, "unexpected file status in worktree") + assert.Equal(t, git.Added, changelogFileStatus.Staging, "unexpected file status in staging") + } else { + assert.Equal(t, git.Modified, changelogFileStatus.Worktree, "unexpected file status in worktree") + assert.Equal(t, git.Modified, changelogFileStatus.Staging, "unexpected file status in staging") + } + + changelogFile, err := wt.Filesystem.Open(ChangelogFile) + require.NoError(t, err) + defer changelogFile.Close() + + changelogFileContent, err := io.ReadAll(changelogFile) + require.NoError(t, err) + + assert.Equal(t, tt.expectedContent, string(changelogFileContent)) + }) + } +} + +func Test_NewChangelogEntry(t *testing.T) { type args struct { commits []AnalyzedCommit version string @@ -111,7 +203,7 @@ func Test_formatChangelog(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, err := formatChangelog(tt.args.commits, tt.args.version, tt.args.link) + got, err := NewChangelogEntry(tt.args.commits, tt.args.version, tt.args.link) if !tt.wantErr(t, err) { return }