changeloguru/main.go

261 lines
6.3 KiB
Go
Raw Normal View History

package main
import (
2020-11-10 16:28:58 +00:00
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/haunt98/changeloguru/pkg/changelog"
2020-11-09 08:46:42 +00:00
"github.com/haunt98/changeloguru/pkg/convention"
"github.com/haunt98/changeloguru/pkg/git"
"github.com/urfave/cli/v2"
"golang.org/x/mod/semver"
)
const (
name = "changeloguru"
description = "generate changelog from conventional commits"
currentDir = "."
2020-12-17 09:54:32 +00:00
markdownFiletype = "md"
defaultRepositry = currentDir
defaultOutputDir = currentDir
defaultFilename = "CHANGELOG"
defaultFiletype = markdownFiletype
defaultVersion = "0.1.0"
fromFlag = "from"
excludeToFlag = "exclude-to"
includeToFlag = "include-to"
versionFlag = "version"
repositoryFlag = "repository"
outputDirFlag = "output-dir"
filenameFlag = "filename"
filetypeFlag = "filetype"
verboseFlag = "verbose"
)
func main() {
a := &action{
flags: make(map[string]string),
args: make(map[string]string),
}
app := &cli.App{
Name: name,
Usage: description,
Flags: []cli.Flag{
&cli.StringFlag{
Name: fromFlag,
2020-12-17 10:34:50 +00:00
Usage: "generate from `COMMIT`",
},
&cli.StringFlag{
2020-11-10 16:28:58 +00:00
Name: excludeToFlag,
2020-12-17 10:34:50 +00:00
Usage: "generate to `COMMIT` (exclude that commit)",
2020-11-10 16:28:58 +00:00
},
&cli.StringFlag{
Name: includeToFlag,
2020-12-17 10:34:50 +00:00
Usage: "generate to `COMMIT` (include that commit)",
},
&cli.StringFlag{
Name: versionFlag,
Usage: fmt.Sprintf("`VERSION` to generate, follow Semantic Versioning, example 1.0.0 (default is %s)", defaultVersion),
},
&cli.StringFlag{
Name: repositoryFlag,
Usage: fmt.Sprintf("`REPOSITORY` path, example /go/src/myapp (default is %s)", defaultRepositry),
},
&cli.StringFlag{
Name: outputDirFlag,
Usage: fmt.Sprintf("`OUTPUT_DIR` path, example /go/src/myapp (default is %s)", defaultOutputDir),
},
2020-12-17 09:54:32 +00:00
&cli.StringFlag{
Name: filenameFlag,
Usage: fmt.Sprintf("output `FILENAME` (default is %s)", defaultFilename),
2020-12-17 09:54:32 +00:00
},
&cli.StringFlag{
Name: filetypeFlag,
Usage: fmt.Sprintf("output `FILETYPE` support md (markdown) (default is %s)", defaultFiletype),
2020-12-17 09:54:32 +00:00
},
&cli.BoolFlag{
Name: verboseFlag,
Aliases: []string{"v"},
Usage: "show what is going on",
},
},
Action: a.Run,
}
if err := app.Run(os.Args); err != nil {
log.Fatal(err)
}
}
type action struct {
verbose bool
flags map[string]string
args map[string]string
}
func (a *action) Run(c *cli.Context) error {
// Set up
a.getFlags(c)
commits, err := a.getCommits()
if err != nil {
return err
}
a.log("commits %+v", commits)
conventionalCommits := a.getConventionalCommits(commits)
a.log("conventional commits %+v", conventionalCommits)
if err := a.generateChangelog(conventionalCommits); err != nil {
return err
}
return nil
}
func (a *action) getFlags(c *cli.Context) {
a.verbose = c.Bool(verboseFlag)
a.flags[fromFlag] = c.String(fromFlag)
a.flags[excludeToFlag] = c.String(excludeToFlag)
a.flags[includeToFlag] = c.String(includeToFlag)
a.flags[versionFlag] = a.getFlagValue(c, versionFlag, defaultVersion)
a.flags[repositoryFlag] = a.getFlagValue(c, repositoryFlag, defaultRepositry)
a.flags[outputDirFlag] = a.getFlagValue(c, outputDirFlag, defaultOutputDir)
a.flags[filenameFlag] = a.getFlagValue(c, filenameFlag, defaultFilename)
a.flags[filetypeFlag] = a.getFlagValue(c, filetypeFlag, defaultFiletype)
}
func (a *action) getFlagValue(c *cli.Context, flag, fallback string) string {
value := c.String(flag)
if value == "" {
value = fallback
}
return value
}
func (a *action) getCommits() ([]git.Commit, error) {
repository := a.flags[repositoryFlag]
a.log("repository %s", repository)
r, err := git.NewRepository(repository)
if err != nil {
return nil, err
}
fromRev := a.flags[fromFlag]
a.log("from revision %s", fromRev)
excludeToRev := a.flags[excludeToFlag]
a.log("exclude to revision %s", excludeToRev)
includeToRev := a.flags[includeToFlag]
a.log("include to revision %s", includeToRev)
2020-11-10 16:28:58 +00:00
if excludeToRev != "" && includeToRev != "" {
return nil, errors.New("excludeToFlag and includeToFlag can not appear same time")
}
2020-11-10 16:28:58 +00:00
if excludeToRev != "" {
2020-11-10 16:28:58 +00:00
return r.LogExcludeTo(fromRev, excludeToRev)
}
if includeToRev != "" {
return r.LogIncludeTo(fromRev, includeToRev)
2020-11-10 16:28:58 +00:00
}
return r.Log(fromRev)
}
func (a *action) getConventionalCommits(commits []git.Commit) []convention.Commit {
2020-11-09 08:46:42 +00:00
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)
2020-11-09 08:46:42 +00:00
continue
}
conventionalCommits = append(conventionalCommits, conventionalCommit)
}
return conventionalCommits
}
func (a *action) generateChangelog(commits []convention.Commit) error {
outputPath, _, filetype := a.getOutputPath()
version, err := a.getVersion()
if err != nil {
return err
}
switch filetype {
case markdownFiletype:
return a.generateMarkdownChangelog(outputPath, version, commits)
default:
return fmt.Errorf("unknown filetype %s", filetype)
}
}
func (a *action) getOutputPath() (string, string, string) {
outputDir := a.flags[outputDirFlag]
2020-12-17 09:54:32 +00:00
filename := a.flags[filenameFlag]
filetype := a.flags[filetypeFlag]
2020-12-17 10:53:38 +00:00
nameWithExt := filename + "." + filetype
path := filepath.Join(outputDir, nameWithExt)
a.log("output path %s", path)
return path, filename, filetype
}
func (a *action) getVersion() (string, error) {
version := a.flags[versionFlag]
if !strings.HasPrefix(version, "v") {
version = "v" + version
}
if !semver.IsValid(version) {
return "", fmt.Errorf("invalid semver %s", version)
}
2020-12-17 10:53:38 +00:00
a.log("version %s", version)
return version, nil
2020-12-17 09:54:32 +00:00
}
func (a *action) generateMarkdownChangelog(outputPath, version string, commits []convention.Commit) error {
// If CHANGELOG file already exist
var oldData string
bytes, err := ioutil.ReadFile(outputPath)
if err == nil {
oldData = string(bytes)
}
markdownGenerator := changelog.NewMarkdownGenerator(oldData, version, time.Now())
newData := markdownGenerator.Generate(commits)
if err := ioutil.WriteFile(outputPath, []byte(newData), 0644); err != nil {
return fmt.Errorf("failed to write file %s: %w", outputPath, err)
}
return nil
}
func (a *action) log(format string, v ...interface{}) {
if a.verbose {
log.Printf(format, v...)
}
}