diff --git a/cmd/new.go b/cmd/new.go index 2e4f503..c5ad929 100644 --- a/cmd/new.go +++ b/cmd/new.go @@ -1,14 +1,9 @@ package cmd import ( - "fmt" - "time" - - "github.com/briandowns/spinner" "github.com/chelnak/gh-changelog/internal/pkg/changelog" "github.com/chelnak/gh-changelog/internal/pkg/writer" "github.com/spf13/cobra" - "github.com/spf13/viper" ) var newCmd = &cobra.Command{ @@ -16,16 +11,10 @@ var newCmd = &cobra.Command{ Short: "Creates a new changelog from activity in the current repository", Long: "Creates a new changelog from activity the current repository.", RunE: func(command *cobra.Command, args []string) error { - s := spinner.New(spinner.CharSets[11], 100*time.Millisecond) - _ = s.Color("green") - s.FinalMSG = fmt.Sprintf("✅ Open %s or run 'gh changelog show' to view your changelog.\n", viper.GetString("file_name")) - - changeLog, err := changelog.MakeFullChangelog(s) + changeLog, err := changelog.NewChangelog() if err != nil { return err } - - s.Stop() return writer.Write(changeLog) }, } diff --git a/internal/pkg/changelog/builder.go b/internal/pkg/changelog/builder.go new file mode 100644 index 0000000..a0232a6 --- /dev/null +++ b/internal/pkg/changelog/builder.go @@ -0,0 +1,212 @@ +package changelog + +import ( + "fmt" + "strings" + "time" + + "github.com/briandowns/spinner" + "github.com/chelnak/gh-changelog/internal/pkg/gitclient" + "github.com/chelnak/gh-changelog/internal/pkg/githubclient" + "github.com/chelnak/gh-changelog/internal/pkg/utils" + "github.com/google/go-github/v43/github" + "github.com/spf13/viper" +) + +type Entry struct { + Tag string + NextTag string + Date time.Time + Added []string + Changed []string + Deprecated []string + Removed []string + Fixed []string + Security []string + Other []string +} + +func (e *Entry) Append(section string, entry string) error { + switch strings.ToLower(section) { + case "added": + e.Added = append(e.Added, entry) + case "changed": + e.Changed = append(e.Changed, entry) + case "deprecated": + e.Deprecated = append(e.Deprecated, entry) + case "removed": + e.Removed = append(e.Removed, entry) + case "fixed": + e.Fixed = append(e.Fixed, entry) + case "security": + e.Security = append(e.Security, entry) + case "other": + e.Other = append(e.Other, entry) + default: + return fmt.Errorf("unknown entry type '%s'", section) + } + + return nil +} + +type ChangeLog struct { + RepoName string + RepoOwner string + Entries []Entry +} + +func NewChangeLogBuilder(gitClient *gitclient.GitClient, githubClient *githubclient.GitHubClient, tags []*gitclient.Ref) *changeLogBuilder { + s := spinner.New(spinner.CharSets[11], 100*time.Millisecond) + _ = s.Color("green") + s.FinalMSG = fmt.Sprintf("✅ Open %s or run 'gh changelog show' to view your changelog.\n", viper.GetString("file_name")) + + return &changeLogBuilder{ + spinner: s, + gitClient: gitClient, + githubClient: githubClient, + tags: tags, + } +} + +type changeLogBuilder struct { + spinner *spinner.Spinner + gitClient *gitclient.GitClient + githubClient *githubclient.GitHubClient + tags []*gitclient.Ref +} + +func (builder *changeLogBuilder) Build() (*ChangeLog, error) { + changeLog := &ChangeLog{ + RepoName: builder.githubClient.RepoContext.Name, + RepoOwner: builder.githubClient.RepoContext.Owner, + Entries: []Entry{}, + } + + builder.spinner.Start() + err := builder.buildChangeLog(changeLog) + if err != nil { + builder.spinner.FinalMSG = "" + builder.spinner.Stop() + return nil, err + } + + builder.spinner.Stop() + return changeLog, nil +} + +func (builder *changeLogBuilder) buildChangeLog(changeLog *ChangeLog) error { + for idx, currentTag := range builder.tags { + builder.spinner.Suffix = fmt.Sprintf(" Processing tags: 🏷️ %s", currentTag.Name) + + var nextTag *gitclient.Ref + var err error + if idx+1 == len(builder.tags) { + nextTag, err = builder.gitClient.GetFirstCommit() + if err != nil { + return fmt.Errorf("could not get first commit: %v", err) + } + } else { + nextTag = builder.tags[idx+1] + } + + pullRequests, err := builder.githubClient.GetPullRequestsBetweenDates(nextTag.Date, currentTag.Date) + if err != nil { + return fmt.Errorf( + "could not get pull requests for range '%s - %s': %v", + nextTag.Date, + currentTag.Date, + err, + ) + } + + entry, err := builder.populateEntry( + currentTag.Name, + nextTag.Name, + currentTag.Date, + pullRequests, + ) + if err != nil { + return fmt.Errorf("could not process pull requests: %v", err) + } + + changeLog.Entries = append(changeLog.Entries, *entry) + } + + return nil +} + +func (builder *changeLogBuilder) populateEntry(currentTag string, nextTag string, date time.Time, pullRequests []*github.Issue) (*Entry, error) { + entry := &Entry{ + Tag: currentTag, + NextTag: nextTag, + Date: date, + Added: []string{}, + Changed: []string{}, + Deprecated: []string{}, + Removed: []string{}, + Fixed: []string{}, + Security: []string{}, + Other: []string{}, + } + + excludedLabels := viper.GetStringSlice("excluded_labels") + for _, pr := range pullRequests { + if !hasExcludedLabel(excludedLabels, pr) { + line := fmt.Sprintf( + "%s [#%d](https://github.com/%s/%s/pull/%d) ([%s](https://github.com/%s))\n", + pr.GetTitle(), + pr.GetNumber(), + builder.githubClient.RepoContext.Owner, + builder.githubClient.RepoContext.Name, + pr.GetNumber(), + pr.GetUser().GetLogin(), + pr.GetUser().GetLogin(), + ) + + section := getSection(pr.Labels) + if section != "" { + err := entry.Append(section, line) + if err != nil { + return nil, err + } + } + } + } + + return entry, nil +} + +func hasExcludedLabel(excludedLabels []string, pr *github.Issue) bool { + for _, label := range pr.Labels { + if utils.Contains(excludedLabels, label.GetName()) { + return true + } + } + + return false +} + +func getSection(labels []*github.Label) string { + sections := viper.GetStringMapStringSlice("sections") + + lookup := make(map[string]string) + for k, v := range sections { + for _, label := range v { + lookup[label] = k + } + } + + section := "" + skipUnlabelledEntries := viper.GetBool("skip_entries_without_label") + for _, label := range labels { + if _, ok := lookup[label.GetName()]; ok { + section = lookup[label.GetName()] + } else { + if !skipUnlabelledEntries { + section = "Other" + } + } + } + + return section +} diff --git a/internal/pkg/changelog/changelog.go b/internal/pkg/changelog/changelog.go index 35ecae8..5c40353 100644 --- a/internal/pkg/changelog/changelog.go +++ b/internal/pkg/changelog/changelog.go @@ -2,18 +2,13 @@ package changelog import ( "fmt" - "time" - "github.com/briandowns/spinner" "github.com/chelnak/gh-changelog/internal/pkg/gitclient" "github.com/chelnak/gh-changelog/internal/pkg/githubclient" - "github.com/chelnak/gh-changelog/internal/pkg/utils" - "github.com/google/go-github/v43/github" - "github.com/spf13/viper" ) -func MakeFullChangelog(spinner *spinner.Spinner) (*ChangeLogProperties, error) { - client, err := githubclient.NewGitHubClient() +func NewChangelog() (*ChangeLog, error) { + githubClient, err := githubclient.NewGitHubClient() if err != nil { return nil, fmt.Errorf("❌ %s", err) } @@ -23,115 +18,20 @@ func MakeFullChangelog(spinner *spinner.Spinner) (*ChangeLogProperties, error) { return nil, fmt.Errorf("❌ %s", err) } - changeLog := NewChangeLogProperties(client.RepoContext.Owner, client.RepoContext.Name) - - spinner.Suffix = " Gathering all tags" - spinner.Start() - tags, err := gitClient.GetTags() if err != nil { return nil, fmt.Errorf("❌ could not get tags: %v", err) } - spinner.Suffix = " Gathering all pull requests" - for idx, currentTag := range tags { - spinner.Suffix = fmt.Sprintf(" Processing tags: 🏷️ %s", currentTag.Name) - - var nextTag *gitclient.Ref - if idx+1 == len(tags) { - nextTag, err = gitClient.GetFirstCommit() - if err != nil { - return nil, fmt.Errorf("❌ could not get first commit: %v", err) - } - } else { - nextTag = tags[idx+1] - } - - pullRequests, err := client.GetPullRequestsBetweenDates(nextTag.Date, currentTag.Date) - if err != nil { - return nil, fmt.Errorf( - "❌ could not get pull requests for range '%s - %s': %v", - nextTag.Date, - currentTag.Date, - err, - ) - } - - tagProperties, err := getTagProperties( - currentTag.Name, - nextTag.Name, - currentTag.Date, - pullRequests, - viper.GetStringSlice("excluded_labels"), - client.RepoContext, - ) - if err != nil { - return nil, fmt.Errorf("❌ could not process pull requests: %v", err) - } - - changeLog.Tags = append(changeLog.Tags, *tagProperties) - } - - return changeLog, nil -} - -func getTagProperties(currentTag string, nextTag string, date time.Time, pullRequests []*github.Issue, excludedLabels []string, repoContext githubclient.RepoContext) (*TagProperties, error) { - tagProperties := NewTagProperties(currentTag, nextTag, date) - for _, pr := range pullRequests { - if !hasExcludedLabel(excludedLabels, pr) { - entry := fmt.Sprintf( - "%s [#%d](https://github.com/%s/%s/pull/%d) ([%s](https://github.com/%s))\n", - pr.GetTitle(), - pr.GetNumber(), - repoContext.Owner, - repoContext.Name, - pr.GetNumber(), - pr.GetUser().GetLogin(), - pr.GetUser().GetLogin(), - ) - - section := getSection(pr.Labels) - err := tagProperties.Append(section, entry) - if err != nil { - return nil, err - } - } - } - - return tagProperties, nil -} - -func hasExcludedLabel(excludedLabels []string, pr *github.Issue) bool { - for _, label := range pr.Labels { - if utils.Contains(excludedLabels, label.GetName()) { - return true - } + if len(tags) < 1 { + return nil, fmt.Errorf("💡 no tags found in the current repository") } - return false -} - -func getSection(labels []*github.Label) string { - sections := viper.GetStringMapStringSlice("sections") - - lookup := make(map[string]string) - for k, v := range sections { - for _, label := range v { - lookup[label] = k - } - } - - section := "" - for _, label := range labels { - if _, ok := lookup[label.GetName()]; ok { - section = lookup[label.GetName()] - } - } - - skipUnlabelledEntries := viper.GetBool("skip_entries_without_label") - if !skipUnlabelledEntries { - section = "Other" + builder := NewChangeLogBuilder(gitClient, githubClient, tags) + changeLog, err := builder.Build() + if err != nil { + return nil, fmt.Errorf("❌ %s", err) } - return section + return changeLog, nil } diff --git a/internal/pkg/changelog/properties.go b/internal/pkg/changelog/properties.go deleted file mode 100644 index 71dcd70..0000000 --- a/internal/pkg/changelog/properties.go +++ /dev/null @@ -1,72 +0,0 @@ -package changelog - -import ( - "fmt" - "strings" - "time" -) - -type ChangeLogProperties struct { - RepoName string - RepoOwner string - Tags []TagProperties -} - -func NewChangeLogProperties(repoOwner string, repoName string) *ChangeLogProperties { - return &ChangeLogProperties{ - RepoName: repoName, - RepoOwner: repoOwner, - Tags: []TagProperties{}, - } -} - -type TagProperties struct { - Tag string - NextTag string - Date time.Time - Added []string - Changed []string - Deprecated []string - Removed []string - Fixed []string - Security []string - Other []string -} - -func NewTagProperties(tag string, nextTag string, date time.Time) *TagProperties { - return &TagProperties{ - Tag: tag, - NextTag: nextTag, - Date: date, - Added: []string{}, - Changed: []string{}, - Deprecated: []string{}, - Removed: []string{}, - Fixed: []string{}, - Security: []string{}, - Other: []string{}, - } -} - -func (tp *TagProperties) Append(section string, entry string) error { - switch strings.ToLower(section) { - case "added": - tp.Added = append(tp.Added, entry) - case "changed": - tp.Changed = append(tp.Changed, entry) - case "deprecated": - tp.Deprecated = append(tp.Deprecated, entry) - case "removed": - tp.Removed = append(tp.Removed, entry) - case "fixed": - tp.Fixed = append(tp.Fixed, entry) - case "security": - tp.Security = append(tp.Security, entry) - case "other": - tp.Other = append(tp.Other, entry) - default: - return fmt.Errorf("unknown entry type '%s'", section) - } - - return nil -} diff --git a/internal/pkg/configuration/configuration.go b/internal/pkg/configuration/configuration.go index b6cf1aa..e9ce32a 100644 --- a/internal/pkg/configuration/configuration.go +++ b/internal/pkg/configuration/configuration.go @@ -23,15 +23,12 @@ func InitConfig() error { } } - SetDefaults() - err := viper.SafeWriteConfig() - if err != nil { - return fmt.Errorf("failed to write config: %s", err) - } - - err = viper.ReadInConfig() - if err != nil { - return fmt.Errorf("failed to read config: %s", err) + if err := viper.ReadInConfig(); err != nil { + SetDefaults() + err := viper.SafeWriteConfig() + if err != nil { + return fmt.Errorf("failed to write config: %s", err) + } } return nil diff --git a/internal/pkg/writer/writer.go b/internal/pkg/writer/writer.go index 2e05f20..a08f52a 100644 --- a/internal/pkg/writer/writer.go +++ b/internal/pkg/writer/writer.go @@ -9,13 +9,13 @@ import ( ) //lintLint:ignore U1000 -func Write(changeLog *changelog.ChangeLogProperties) error { +func Write(changeLog *changelog.ChangeLog) error { var tmplSrc = `# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org). -{{range .Tags}} +{{range .Entries}} ## [{{.Tag}}](https://github.com/{{$.RepoOwner}}/{{$.RepoName}}/tree/{{.Tag}}) - ({{.Date.Format "2006-01-02"}}) [Full Changelog](https://github.com/{{$.RepoOwner}}/{{$.RepoName}}/compare/{{.Tag}}...{{.NextTag}})