feat: auto commit after generating changelog

main
sudo pacman -Syu 2022-07-03 23:19:14 +07:00 committed by sudo pacman -Syu
parent ecd31677ce
commit 7051e7fa89
5 changed files with 101 additions and 66 deletions

View File

@ -30,6 +30,7 @@ type action struct {
verbose bool verbose bool
dryRun bool dryRun bool
interactive bool interactive bool
autoCommit bool
} }
} }
@ -54,6 +55,7 @@ func (a *action) getFlags(c *cli.Context) {
a.flags.filetype = c.String(flagFiletype) a.flags.filetype = c.String(flagFiletype)
a.flags.dryRun = c.Bool(flagDryRun) a.flags.dryRun = c.Bool(flagDryRun)
a.flags.interactive = c.Bool(flagInteractive) a.flags.interactive = c.Bool(flagInteractive)
a.flags.autoCommit = c.Bool(flagAutoCommit)
a.log("flags %+v", a.flags) a.log("flags %+v", a.flags)
} }

View File

@ -20,6 +20,8 @@ import (
"golang.org/x/mod/semver" "golang.org/x/mod/semver"
) )
const autoCommitMessageTemplate = "chore(changelog): generate version %s"
var ( var (
ErrUnknownFiletype = errors.New("unknown filetype") ErrUnknownFiletype = errors.New("unknown filetype")
ErrInvalidVersion = errors.New("invalid version") ErrInvalidVersion = errors.New("invalid version")
@ -37,34 +39,45 @@ func (a *action) RunGenerate(c *cli.Context) error {
fmt.Printf("Input version (%s):\n", usageFlagVersion) fmt.Printf("Input version (%s):\n", usageFlagVersion)
a.flags.version = ioe.ReadInput() a.flags.version = ioe.ReadInput()
fmt.Printf("Input from (%s):\n", usageFrom) fmt.Printf("Input from (%s):\n", usageFlagFrom)
a.flags.from = ioe.ReadInputEmpty() a.flags.from = ioe.ReadInputEmpty()
fmt.Printf("Input to (%s):\n", usageTo) fmt.Printf("Input to (%s):\n", usageFlagTo)
a.flags.to = ioe.ReadInputEmpty() a.flags.to = ioe.ReadInputEmpty()
} }
commits, err := a.getCommits() repo, err := git.NewRepository(a.flags.repository)
if err != nil {
return err
}
commits, err := repo.Log(a.flags.from, a.flags.to)
if err != nil { if err != nil {
return err return err
} }
conventionalCommits := a.getConventionalCommits(commits) conventionalCommits := a.getConventionalCommits(commits)
if err := a.generateChangelog(conventionalCommits); err != nil { finalOutput := a.getFinalOutput()
version, err := a.getVersion()
if err != nil {
return err return err
} }
return nil if err := a.generateChangelog(conventionalCommits, finalOutput, version); err != nil {
} return err
func (a *action) getCommits() ([]git.Commit, error) {
r, err := git.NewRepository(a.flags.repository)
if err != nil {
return nil, err
} }
return r.Log(a.flags.from, a.flags.to) if a.flags.autoCommit {
commitMsg := fmt.Sprintf(autoCommitMessageTemplate, version)
if err := repo.Commit(commitMsg, a.flags.filename, finalOutput); err != nil {
return err
}
}
return nil
} }
func (a *action) getConventionalCommits(commits []git.Commit) []convention.Commit { func (a *action) getConventionalCommits(commits []git.Commit) []convention.Commit {
@ -83,24 +96,6 @@ func (a *action) getConventionalCommits(commits []git.Commit) []convention.Commi
return conventionalCommits return conventionalCommits
} }
func (a *action) generateChangelog(commits []convention.Commit) error {
finalOutput := a.getFinalOutput()
version, err := a.getVersion()
if err != nil {
return err
}
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) getFinalOutput() string { func (a *action) getFinalOutput() string {
nameWithExt := a.flags.filename + "." + a.flags.filetype nameWithExt := a.flags.filename + "." + a.flags.filetype
finalOutput := filepath.Join(a.flags.output, nameWithExt) finalOutput := filepath.Join(a.flags.output, nameWithExt)
@ -128,6 +123,17 @@ func (a *action) getVersion() (string, error) {
return a.flags.version, nil 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 { func (a *action) generateMarkdownChangelog(output, version string, commits []convention.Commit) error {
// If changelog file already exist, parse markdown from exist file // If changelog file already exist, parse markdown from exist file
var oldNodes []markdown.Node var oldNodes []markdown.Node

View File

@ -22,27 +22,29 @@ const (
flagFiletype = "filetype" flagFiletype = "filetype"
flagDryRun = "dry-run" flagDryRun = "dry-run"
flagInteractive = "interactive" flagInteractive = "interactive"
flagAutoCommit = "auto-commit"
commandGenerate = "generate" commandGenerate = "generate"
usageGenerate = "generate changelog" usageCommandGenerate = "generate changelog"
usageVerbose = "show what is going on" usageFlagVerbose = "show what is going on"
usageFlagVersion = "`VERSION` to generate, follow Semantic Versioning" usageFlagVersion = "`VERSION` to generate, follow Semantic Versioning"
usageFrom = "from `COMMIT`, which is kinda new commit, default is latest commit" usageFlagFrom = "from `COMMIT`, which is kinda new commit, default is latest commit"
usageTo = "to `COMMIT`, which is kinda old commit, default is oldest commit" usageFlagTo = "to `COMMIT`, which is kinda old commit, default is oldest commit"
usageScope = "scope to generate" usageFlagScope = "scope to generate"
usageRepository = "`REPOSITORY` directory path" usageFlagRepository = "`REPOSITORY` directory path"
usageOutput = "`OUTPUT` directory path" usageFlagOutput = "`OUTPUT` directory path"
usageFilename = "output `FILENAME`" usageFlagFilename = "output `FILENAME`"
usageFiletype = "output `FILETYPE`" usageFlagFiletype = "output `FILETYPE`"
usageDryRun = "demo run without actually changing anything" usageFlagDryRun = "demo run without actually changing anything"
usageInteractive = "interactive mode, default is true" usageFlagInteractive = "interactive mode"
usageFlagAutoCommit = "enable auto commit after generating changelog"
) )
var ( var (
aliasGenerate = []string{"g", "gen"} aliasCommandGenerate = []string{"g", "gen"}
aliasVerbose = []string{"v"} aliasFlagVerbose = []string{"v"}
aliasInteractive = []string{"i"} aliasFlagInteractive = []string{"i"}
) )
type App struct { type App struct {
@ -58,13 +60,13 @@ func NewApp() *App {
Commands: []*cli.Command{ Commands: []*cli.Command{
{ {
Name: commandGenerate, Name: commandGenerate,
Aliases: aliasGenerate, Aliases: aliasCommandGenerate,
Usage: usageGenerate, Usage: usageCommandGenerate,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.BoolFlag{ &cli.BoolFlag{
Name: flagVerbose, Name: flagVerbose,
Aliases: aliasVerbose, Aliases: aliasFlagVerbose,
Usage: usageVerbose, Usage: usageFlagVerbose,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: flagVersion, Name: flagVersion,
@ -72,46 +74,51 @@ func NewApp() *App {
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: flagFrom, Name: flagFrom,
Usage: usageFrom, Usage: usageFlagFrom,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: flagTo, Name: flagTo,
Usage: usageTo, Usage: usageFlagTo,
}, },
&cli.StringSliceFlag{ &cli.StringSliceFlag{
Name: flagScope, Name: flagScope,
Usage: usageScope, Usage: usageFlagScope,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: flagRepository, Name: flagRepository,
Usage: usageRepository, Usage: usageFlagRepository,
Value: defaultRepository, Value: defaultRepository,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: flagOutput, Name: flagOutput,
Usage: usageOutput, Usage: usageFlagOutput,
Value: defaultOutput, Value: defaultOutput,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: flagFilename, Name: flagFilename,
Usage: usageFilename, Usage: usageFlagFilename,
Value: defaultFilename, Value: defaultFilename,
}, },
&cli.StringFlag{ &cli.StringFlag{
Name: flagFiletype, Name: flagFiletype,
Usage: usageFiletype, Usage: usageFlagFiletype,
Value: defaultFiletype, Value: defaultFiletype,
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: flagDryRun, Name: flagDryRun,
Usage: usageDryRun, Usage: usageFlagDryRun,
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: flagInteractive, Name: flagInteractive,
Usage: usageInteractive, Usage: usageFlagInteractive,
Aliases: aliasInteractive, Aliases: aliasFlagInteractive,
Value: true, Value: true,
}, },
&cli.BoolFlag{
Name: flagAutoCommit,
Usage: usageFlagAutoCommit,
Value: true,
},
}, },
Action: a.RunGenerate, Action: a.RunGenerate,
}, },

View File

@ -18,23 +18,24 @@ const (
// Repository is an abstraction for git-repository // Repository is an abstraction for git-repository
type Repository interface { type Repository interface {
Log(fromRev, toRev string) ([]Commit, error) Log(fromRev, toRev string) ([]Commit, error)
Commit(commitMessage string, paths ...string) error
} }
type repo struct { type repo struct {
r *git.Repository gitRepo *git.Repository
} }
type stopFn func(*object.Commit) error type stopFn func(*object.Commit) error
// NewRepository return Repository from path // NewRepository return Repository from path
func NewRepository(path string) (Repository, error) { func NewRepository(path string) (Repository, error) {
r, err := git.PlainOpen(path) gitRepo, err := git.PlainOpen(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &repo{ return &repo{
r: r, gitRepo: gitRepo,
}, nil }, nil
} }
@ -44,7 +45,7 @@ func (r *repo) Log(fromRev, toRev string) ([]Commit, error) {
fromRev = head fromRev = head
} }
fromHash, err := r.r.ResolveRevision(plumbing.Revision(fromRev)) fromHash, err := r.gitRepo.ResolveRevision(plumbing.Revision(fromRev))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to resolve %s: %w", fromRev, err) return nil, fmt.Errorf("failed to resolve %s: %w", fromRev, err)
} }
@ -53,7 +54,7 @@ func (r *repo) Log(fromRev, toRev string) ([]Commit, error) {
return r.logWithStopFn(fromHash, nil, nil) return r.logWithStopFn(fromHash, nil, nil)
} }
toHash, err := r.r.ResolveRevision(plumbing.Revision(toRev)) toHash, err := r.gitRepo.ResolveRevision(plumbing.Revision(toRev))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to resolve %s: %w", toRev, err) return nil, fmt.Errorf("failed to resolve %s: %w", toRev, err)
} }
@ -61,8 +62,27 @@ func (r *repo) Log(fromRev, toRev string) ([]Commit, error) {
return r.logWithStopFn(fromHash, nil, stopAtHash(toHash)) return r.logWithStopFn(fromHash, nil, stopAtHash(toHash))
} }
func (r *repo) Commit(commitMessage string, paths ...string) error {
gitWorktree, err := r.gitRepo.Worktree()
if err != nil {
return fmt.Errorf("failed to get git worktree: %w", err)
}
for _, path := range paths {
if _, err := gitWorktree.Add(path); err != nil {
return fmt.Errorf("failed to git add %s: %w", path, err)
}
}
if _, err := gitWorktree.Commit(commitMessage, &git.CommitOptions{}); err != nil {
return fmt.Errorf("failed to git commit: %w", err)
}
return nil
}
func (r *repo) logWithStopFn(fromHash *plumbing.Hash, beginFn, endFn stopFn) ([]Commit, error) { func (r *repo) logWithStopFn(fromHash *plumbing.Hash, beginFn, endFn stopFn) ([]Commit, error) {
cIter, err := r.r.Log(&git.LogOptions{ cIter, err := r.gitRepo.Log(&git.LogOptions{
From: *fromHash, From: *fromHash,
}) })
if err != nil { if err != nil {

View File

@ -24,7 +24,7 @@ func (s *RepositorySuite) SetupTest() {
s.NoError(err) s.NoError(err)
s.repo = &repo{ s.repo = &repo{
r: s.r, gitRepo: s.r,
} }
} }