276 lines
6.8 KiB
Go
276 lines
6.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/diff"
|
|
"github.com/pkg/diff/write"
|
|
"github.com/urfave/cli/v2"
|
|
"golang.org/x/mod/semver"
|
|
|
|
"github.com/make-go-great/color-go"
|
|
"github.com/make-go-great/ioe-go"
|
|
"github.com/make-go-great/markdown-go"
|
|
"github.com/make-go-great/rst-go"
|
|
|
|
"github.com/haunt98/changeloguru/internal/changelog"
|
|
"github.com/haunt98/changeloguru/internal/convention"
|
|
"github.com/haunt98/changeloguru/internal/git"
|
|
)
|
|
|
|
const autoCommitMessageTemplate = "chore(changelog): generate %s"
|
|
|
|
var (
|
|
ErrUnknownFiletype = errors.New("unknown filetype")
|
|
ErrInvalidVersion = errors.New("invalid version")
|
|
)
|
|
|
|
func (a *action) RunGenerate(c *cli.Context) error {
|
|
a.getFlags(c)
|
|
|
|
// Show help if there is no flags
|
|
if c.NumFlags() == 0 {
|
|
return cli.ShowCommandHelp(c, commandGenerateName)
|
|
}
|
|
|
|
useLatestTag := false
|
|
if a.flags.interactive {
|
|
fmt.Printf("Input version (%s):\n", flagVersionUsage)
|
|
a.flags.version = ioe.ReadInput()
|
|
|
|
if a.flags.interactiveFrom {
|
|
fmt.Printf("Input from (%s):\n", flagFromUsage)
|
|
a.flags.from = ioe.ReadInputEmpty()
|
|
}
|
|
|
|
if a.flags.interactiveTo {
|
|
fmt.Printf("Input to (%s):\n", flagToUsage)
|
|
a.flags.to = ioe.ReadInputEmpty()
|
|
} else {
|
|
useLatestTag = true
|
|
}
|
|
}
|
|
|
|
repo, err := git.NewRepository(a.flags.repository)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if useLatestTag {
|
|
tags, err := repo.SemVerTags()
|
|
if err == nil && len(tags) > 0 {
|
|
a.flags.to = tags[len(tags)-1].Version.Original()
|
|
}
|
|
}
|
|
|
|
aliasFrom := a.flags.from
|
|
if aliasFrom == "" {
|
|
aliasFrom = "latest"
|
|
}
|
|
|
|
aliasTo := a.flags.to
|
|
if aliasTo == "" {
|
|
aliasTo = "earliest"
|
|
}
|
|
|
|
color.PrintAppOK(name, fmt.Sprintf("Generate changelog from [%s] to [%s]", aliasFrom, aliasTo))
|
|
|
|
commits, err := repo.Log(a.flags.from, a.flags.to)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
conventionalCommits := a.getConventionalCommits(commits)
|
|
|
|
finalOutput := a.getFinalOutput()
|
|
|
|
version, err := a.getVersion()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := a.generateChangelog(conventionalCommits, finalOutput, version); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := a.doGit(finalOutput, version); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *action) getConventionalCommits(commits []git.Commit) []convention.Commit {
|
|
conventionalCommits := make([]convention.Commit, 0, len(commits))
|
|
for _, commit := range commits {
|
|
conventionalCommit, err := convention.NewCommit(commit)
|
|
if err != nil {
|
|
a.log("Failed to new conventional commits %+v: %s", commit, err)
|
|
// Skip bad commit and move on
|
|
continue
|
|
}
|
|
|
|
conventionalCommits = append(conventionalCommits, conventionalCommit)
|
|
}
|
|
|
|
return conventionalCommits
|
|
}
|
|
|
|
func (a *action) getFinalOutput() string {
|
|
nameWithExt := a.flags.filename + "." + a.flags.filetype
|
|
finalOutput := filepath.Join(a.flags.repository, a.flags.output, nameWithExt)
|
|
|
|
a.log("Final output %s", finalOutput)
|
|
|
|
return finalOutput
|
|
}
|
|
|
|
func (a *action) getVersion() (string, error) {
|
|
if a.flags.version == "" {
|
|
return "", fmt.Errorf("empty version: %w", ErrInvalidVersion)
|
|
}
|
|
|
|
if !strings.HasPrefix(a.flags.version, "v") {
|
|
a.flags.version = "v" + a.flags.version
|
|
}
|
|
|
|
if !semver.IsValid(a.flags.version) {
|
|
return "", fmt.Errorf("invalid semver %s: %w", a.flags.version, ErrInvalidVersion)
|
|
}
|
|
|
|
a.log("Version %s", a.flags.version)
|
|
|
|
return a.flags.version, nil
|
|
}
|
|
|
|
func (a *action) generateChangelog(commits []convention.Commit, finalOutput, version string) error {
|
|
switch a.flags.filetype {
|
|
case markdownFiletype:
|
|
return a.generateMarkdownChangelog(finalOutput, version, commits)
|
|
case rstFiletype:
|
|
return a.generateRSTChangelog(finalOutput, version, commits)
|
|
default:
|
|
return fmt.Errorf("unknown filetype %s: %w", a.flags.filetype, ErrUnknownFiletype)
|
|
}
|
|
}
|
|
|
|
func (a *action) generateMarkdownChangelog(output, version string, commits []convention.Commit) error {
|
|
// If changelog file already exist, parse markdown from exist file
|
|
var oldNodes []markdown.Node
|
|
bytes, err := os.ReadFile(output)
|
|
if err == nil {
|
|
oldNodes = changelog.ParseMarkdown(string(bytes))
|
|
}
|
|
|
|
// Generate markdown from commits
|
|
nodes := changelog.GenerateMarkdown(commits, a.flags.scopes, version, time.Now())
|
|
|
|
// Final changelog with new commits above old commits
|
|
nodes = append(nodes, oldNodes...)
|
|
changelogText := markdown.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), 0o600); err != nil {
|
|
return fmt.Errorf("failed to write file %s: %w", output, err)
|
|
}
|
|
|
|
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
|
|
nodes := changelog.GenerateRST(commits, a.flags.scopes, version, time.Now())
|
|
|
|
// Final changelog with new commits above old commits
|
|
nodes = append(nodes, 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), 0o600); err != nil {
|
|
return fmt.Errorf("failed to write file %s: %w", output, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (a *action) doGit(finalOutput, version string) error {
|
|
if !a.flags.autoGitCommit {
|
|
return nil
|
|
}
|
|
|
|
// TODO: disable until https://github.com/go-git/go-git/issues/180 is fixed
|
|
// if err := repo.Commit(commitMsg, finalOutput); err != nil {
|
|
// return err
|
|
// }
|
|
cmdOutput, err := exec.Command("git", "add", finalOutput).CombinedOutput()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.log("Git add output:\n%s", cmdOutput)
|
|
|
|
commitMsg := fmt.Sprintf(autoCommitMessageTemplate, version)
|
|
|
|
cmdOutput, err = exec.Command("git", "commit", "-m", commitMsg).CombinedOutput()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.log("Git commit output:\n%s", cmdOutput)
|
|
|
|
if a.flags.autoGitTag {
|
|
cmdOutput, err = exec.Command("git", "tag", version, "-m", commitMsg).CombinedOutput()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.log("Git tag output:\n%s", cmdOutput)
|
|
}
|
|
|
|
if a.flags.autoGitPush {
|
|
cmdOutput, err = exec.Command("git", "push").CombinedOutput()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.log("Git push output:\n%s", cmdOutput)
|
|
|
|
if a.flags.autoGitTag {
|
|
cmdOutput, err = exec.Command("git", "push", "origin", version).CombinedOutput()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
a.log("Git push tag output:\n%s", cmdOutput)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|