diff --git a/Makefile b/Makefile index 76af18d..862c50e 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: usage build run test +.PHONY: usage build test run tidy release release-all OK_COLOR=\033[32;01m NO_COLOR=\033[0m @@ -10,7 +10,8 @@ BIN = git-sv ECHOFLAGS ?= -VERSION ?= +BUILD_TIME = $(shell date +"%Y%m%d%H%M") +VERSION ?= dev-$(BUILD_TIME) BUILDOS ?= linux BUILDARCH ?= amd64 diff --git a/README.md b/README.md index c1aaf6b..98ecf29 100644 --- a/README.md +++ b/README.md @@ -42,30 +42,27 @@ git sv next-version #### Usage +use `--help` or `-h` to get usage information, dont forget that some commands have unique options too + ```bash -NAME: - sv - semantic version for git - -USAGE: - git-sv [global options] command [command options] [arguments...] - -VERSION: - 1.0.0 - -COMMANDS: - current-version, cv get last released version from git - next-version, nv generate the next version based on git commit messages - commit-log, cl list all commit logs since last version as jsons - release-notes, rn generate release notes - tag, tg generate tag with version based on git commit messages - help, h Shows a list of commands or help for one command - -GLOBAL OPTIONS: - --help, -h show help - --version, -v print the version +# sv help +git-sv -h +# sv release-notes command help +git-sv rn -h ``` +##### Available commands + +| Variable | description | +| --------- | ---------- | +| current-version, cv | get last released version from git | +| next-version, nv | generate the next version based on git commit messages | +| commit-log, cl | list all commit logs since last version as jsons | +| release-notes, rn | generate release notes | +| tag, tg | generate tag with version based on git commit messages | +| help, h | Shows a list of commands or help for one command | + ## Development ### Makefile diff --git a/cmd/git-sv/handlers.go b/cmd/git-sv/handlers.go index bb9f0d8..7615184 100644 --- a/cmd/git-sv/handlers.go +++ b/cmd/git-sv/handlers.go @@ -4,8 +4,10 @@ import ( "encoding/json" "fmt" "sv4git/sv" + "time" - "github.com/urfave/cli" + "github.com/Masterminds/semver" + "github.com/urfave/cli/v2" ) func currentVersionHandler(git sv.Git) func(c *cli.Context) error { @@ -30,7 +32,7 @@ func nextVersionHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) f return fmt.Errorf("error parsing version: %s from describe, message: %v", describe, err) } - commits, err := git.Log(describe) + commits, err := git.Log(describe, "") if err != nil { return fmt.Errorf("error getting git log, message: %v", err) } @@ -43,9 +45,14 @@ func nextVersionHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) f func commitLogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *cli.Context) error { return func(c *cli.Context) error { - describe := git.Describe() + var commits []sv.GitCommitLog + var err error - commits, err := git.Log(describe) + if tag := c.String("t"); tag != "" { + commits, err = getTagCommits(git, tag) + } else { + commits, err = git.Log(git.Describe(), "") + } if err != nil { return fmt.Errorf("error getting git log, message: %v", err) } @@ -61,29 +68,99 @@ func commitLogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) fun } } +func getTagCommits(git sv.Git, tag string) ([]sv.GitCommitLog, error) { + prev, _, err := getTags(git, tag) + if err != nil { + return nil, err + } + return git.Log(prev, tag) +} + func releaseNotesHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnProcessor sv.ReleaseNoteProcessor) func(c *cli.Context) error { return func(c *cli.Context) error { + var commits []sv.GitCommitLog + var rnVersion semver.Version + var date time.Time + var err error - describe := git.Describe() - - currentVer, err := sv.ToVersion(describe) - if err != nil { - return fmt.Errorf("error parsing version: %s from describe, message: %v", describe, err) + if tag := c.String("t"); tag != "" { + rnVersion, date, commits, err = getTagVersionInfo(git, semverProcessor, tag) + } else { + rnVersion, date, commits, err = getNextVersionInfo(git, semverProcessor) } - commits, err := git.Log(describe) if err != nil { - return fmt.Errorf("error getting git log, message: %v", err) + return err } - nextVer := semverProcessor.NextVersion(currentVer, commits) - - releasenote := rnProcessor.Get(commits) - fmt.Println(rnProcessor.Format(releasenote, nextVer)) + releasenote := rnProcessor.Get(date, commits) + fmt.Println(rnProcessor.Format(releasenote, rnVersion)) return nil } } +func getTagVersionInfo(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, tag string) (semver.Version, time.Time, []sv.GitCommitLog, error) { + tagVersion, err := sv.ToVersion(tag) + if err != nil { + return semver.Version{}, time.Time{}, nil, fmt.Errorf("error parsing version: %s from tag, message: %v", tag, err) + } + + previousTag, currentTag, err := getTags(git, tag) + if err != nil { + return semver.Version{}, time.Time{}, nil, fmt.Errorf("error listing tags, message: %v", err) + } + + commits, err := git.Log(previousTag, tag) + if err != nil { + return semver.Version{}, time.Time{}, nil, fmt.Errorf("error getting git log from tag: %s, message: %v", tag, err) + } + + return tagVersion, currentTag.Date, commits, nil +} + +func getTags(git sv.Git, tag string) (string, sv.GitTag, error) { + tags, err := git.Tags() + if err != nil { + return "", sv.GitTag{}, err + } + + index := find(tag, tags) + if index < 0 { + return "", sv.GitTag{}, fmt.Errorf("tag: %s not found", tag) + } + + previousTag := "" + if index > 0 { + previousTag = tags[index-1].Name + } + return previousTag, tags[index], nil +} + +func find(tag string, tags []sv.GitTag) int { + for i := 0; i < len(tags); i++ { + if tag == tags[i].Name { + return i + } + } + return -1 +} + +func getNextVersionInfo(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) (semver.Version, time.Time, []sv.GitCommitLog, error) { + describe := git.Describe() + + currentVer, err := sv.ToVersion(describe) + if err != nil { + return semver.Version{}, time.Time{}, nil, fmt.Errorf("error parsing version: %s from describe, message: %v", describe, err) + } + + commits, err := git.Log(describe, "") + if err != nil { + return semver.Version{}, time.Time{}, nil, fmt.Errorf("error getting git log, message: %v", err) + } + + return semverProcessor.NextVersion(currentVer, commits), time.Now(), commits, nil +} + func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnProcessor sv.ReleaseNoteProcessor) func(c *cli.Context) error { return func(c *cli.Context) error { describe := git.Describe() @@ -93,7 +170,7 @@ func tagHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnProcess return fmt.Errorf("error parsing version: %s from describe, message: %v", describe, err) } - commits, err := git.Log(describe) + commits, err := git.Log(describe, "") if err != nil { return fmt.Errorf("error getting git log, message: %v", err) } diff --git a/cmd/git-sv/main.go b/cmd/git-sv/main.go index 6e8e8da..4bca52a 100644 --- a/cmd/git-sv/main.go +++ b/cmd/git-sv/main.go @@ -5,7 +5,7 @@ import ( "os" "sv4git/sv" - "github.com/urfave/cli" + "github.com/urfave/cli/v2" ) // Version for git-sv @@ -22,7 +22,7 @@ func main() { app.Name = "sv" app.Version = Version app.Usage = "semantic version for git" - app.Commands = []cli.Command{ + app.Commands = []*cli.Command{ { Name: "current-version", Aliases: []string{"cv"}, @@ -40,12 +40,14 @@ func main() { Aliases: []string{"cl"}, Usage: "list all commit logs since last version as jsons", Action: commitLogHandler(git, semverProcessor), + Flags: []cli.Flag{&cli.StringFlag{Name: "t", Usage: "get commit log from tag"}}, }, { Name: "release-notes", Aliases: []string{"rn"}, Usage: "generate release notes", Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor), + Flags: []cli.Flag{&cli.StringFlag{Name: "t", Usage: "get release note from tag"}}, }, { Name: "tag", diff --git a/go.mod b/go.mod index 3fdbad0..14bdc18 100644 --- a/go.mod +++ b/go.mod @@ -6,5 +6,5 @@ require ( github.com/Masterminds/semver v1.5.0 github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/kelseyhightower/envconfig v1.4.0 - github.com/urfave/cli v1.22.1 + github.com/urfave/cli/v2 v2.1.1 ) diff --git a/go.sum b/go.sum index 30d1ebf..c3152d9 100644 --- a/go.sum +++ b/go.sum @@ -13,7 +13,7 @@ github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -github.com/urfave/cli v1.22.1 h1:+mkCCcOFKPnCmVYVcURKps1Xe+3zP90gSYGNfRkjoIY= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/urfave/cli/v2 v2.1.1 h1:Qt8FeAtxE/vfdrLmR3rxR6JRE0RoVmbXu8+6kZtYU4k= +github.com/urfave/cli/v2 v2.1.1/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/sv/git.go b/sv/git.go index df5cfa9..67892d9 100644 --- a/sv/git.go +++ b/sv/git.go @@ -7,6 +7,7 @@ import ( "os/exec" "regexp" "strings" + "time" "github.com/Masterminds/semver" ) @@ -21,8 +22,9 @@ const ( // Git commands type Git interface { Describe() string - Log(lastTag string) ([]GitCommitLog, error) + Log(initialTag, endTag string) ([]GitCommitLog, error) Tag(version semver.Version) error + Tags() ([]GitTag, error) } // GitCommitLog description of a single commit log @@ -35,6 +37,12 @@ type GitCommitLog struct { Metadata map[string]string `json:"metadata,omitempty"` } +// GitTag git tag info +type GitTag struct { + Name string + Date time.Time +} + // GitImpl git command implementation type GitImpl struct { messageMetadata map[string]string @@ -57,11 +65,17 @@ func (GitImpl) Describe() string { } // Log return git log -func (g GitImpl) Log(lastTag string) ([]GitCommitLog, error) { +func (g GitImpl) Log(initialTag, endTag string) ([]GitCommitLog, error) { format := "--pretty=format:\"%h" + logSeparator + "%s" + logSeparator + "%b" + endLine + "\"" - cmd := exec.Command("git", "log", format) - if lastTag != "" { - cmd = exec.Command("git", "log", lastTag+"..HEAD", format) + var cmd *exec.Cmd + if initialTag == "" && endTag == "" { + cmd = exec.Command("git", "log", format) + } else if endTag == "" { + cmd = exec.Command("git", "log", initialTag+"..HEAD", format) + } else if initialTag == "" { + cmd = exec.Command("git", "log", endTag, format) + } else { + cmd = exec.Command("git", "log", initialTag+".."+endTag, format) } out, err := cmd.CombinedOutput() @@ -85,6 +99,32 @@ func (g GitImpl) Tag(version semver.Version) error { return pushCommand.Run() } +// Tags list repository tags +func (g GitImpl) Tags() ([]GitTag, error) { + cmd := exec.Command("git", "tag", "-l", "--format", "%(taggerdate:iso8601)#%(refname:short)") + out, err := cmd.CombinedOutput() + if err != nil { + return nil, err + } + return parseTagsOutput(string(out)) +} + +func parseTagsOutput(input string) ([]GitTag, error) { + scanner := bufio.NewScanner(strings.NewReader(input)) + var result []GitTag + for scanner.Scan() { + if line := strings.TrimSpace(scanner.Text()); line != "" { + values := strings.Split(line, "#") + date, err := time.Parse("2006-01-02 15:04:05 -0700", values[0]) + if err != nil { + return nil, fmt.Errorf("failed to parse tag data, message: %v", err) + } + result = append(result, GitTag{Name: values[1], Date: date}) + } + } + return result, nil +} + func parseLogOutput(messageMetadata map[string]string, log string) []GitCommitLog { scanner := bufio.NewScanner(strings.NewReader(log)) scanner.Split(splitAt([]byte(endLine))) diff --git a/sv/helpers_test.go b/sv/helpers_test.go index e6d2451..6d54f11 100644 --- a/sv/helpers_test.go +++ b/sv/helpers_test.go @@ -19,9 +19,9 @@ func commitlog(t string, metadata map[string]string) GitCommitLog { } } -func releaseNote(sections map[string]ReleaseNoteSection, breakingChanges []string) ReleaseNote { +func releaseNote(date time.Time, sections map[string]ReleaseNoteSection, breakingChanges []string) ReleaseNote { return ReleaseNote{ - Date: time.Now().Truncate(time.Minute), + Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChanges, } diff --git a/sv/releasenotes.go b/sv/releasenotes.go index ef2a867..991c3a4 100644 --- a/sv/releasenotes.go +++ b/sv/releasenotes.go @@ -16,24 +16,24 @@ type releaseNoteTemplate struct { BreakingChanges []string } -const markdownTemplate = `# v{{.Version}} ({{.Date}}) +const markdownTemplate = `## v{{.Version}} ({{.Date}}) -{{if .Sections.feat}}## {{.Sections.feat.Name}} +{{if .Sections.feat}}### {{.Sections.feat.Name}} {{range $k,$v := .Sections.feat.Items}} - {{if $v.Scope}}**{{$v.Scope}}:** {{end}}{{$v.Subject}} ({{$v.Hash}}) {{if $v.Metadata.issueid}}({{$v.Metadata.issueid}}){{end}}{{end}}{{end}} -{{if .Sections.fix}}## {{.Sections.fix.Name}} +{{if .Sections.fix}}### {{.Sections.fix.Name}} {{range $k,$v := .Sections.fix.Items}} - {{if $v.Scope}}**{{$v.Scope}}:** {{end}}{{$v.Subject}} ({{$v.Hash}}) {{if $v.Metadata.issueid}}({{$v.Metadata.issueid}}){{end}}{{end}}{{end}} -{{if .BreakingChanges}}## Breaking Changes +{{if .BreakingChanges}}### Breaking Changes {{range $k,$v := .BreakingChanges}} - {{$v}}{{end}} {{end}}` // ReleaseNoteProcessor release note processor interface. type ReleaseNoteProcessor interface { - Get(commits []GitCommitLog) ReleaseNote + Get(date time.Time, commits []GitCommitLog) ReleaseNote Format(releasenote ReleaseNote, version semver.Version) string } @@ -50,7 +50,7 @@ func NewReleaseNoteProcessor(tags map[string]string) *ReleaseNoteProcessorImpl { } // Get generate a release note based on commits. -func (p ReleaseNoteProcessorImpl) Get(commits []GitCommitLog) ReleaseNote { +func (p ReleaseNoteProcessorImpl) Get(date time.Time, commits []GitCommitLog) ReleaseNote { sections := make(map[string]ReleaseNoteSection) var breakingChanges []string for _, commit := range commits { @@ -67,7 +67,7 @@ func (p ReleaseNoteProcessorImpl) Get(commits []GitCommitLog) ReleaseNote { } } - return ReleaseNote{Date: time.Now().Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChanges} + return ReleaseNote{Date: date.Truncate(time.Minute), Sections: sections, BreakingChanges: breakingChanges} } // Format format a release note. diff --git a/sv/releasenotes_test.go b/sv/releasenotes_test.go index b264b26..c65d063 100644 --- a/sv/releasenotes_test.go +++ b/sv/releasenotes_test.go @@ -3,34 +3,41 @@ package sv import ( "reflect" "testing" + "time" ) func TestReleaseNoteProcessorImpl_Get(t *testing.T) { + date := time.Now() + tests := []struct { name string + date time.Time commits []GitCommitLog want ReleaseNote }{ { name: "mapped tag", + date: date, commits: []GitCommitLog{commitlog("t1", map[string]string{})}, - want: releaseNote(map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), + want: releaseNote(date, map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), }, { name: "unmapped tag", + date: date, commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{})}, - want: releaseNote(map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), + want: releaseNote(date, map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, nil), }, { name: "breaking changes tag", + date: date, commits: []GitCommitLog{commitlog("t1", map[string]string{}), commitlog("unmapped", map[string]string{"breakingchange": "breaks"})}, - want: releaseNote(map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, []string{"breaks"}), + want: releaseNote(date, map[string]ReleaseNoteSection{"t1": rnSection("Tag 1", []GitCommitLog{commitlog("t1", map[string]string{})})}, []string{"breaks"}), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := NewReleaseNoteProcessor(map[string]string{"t1": "Tag 1", "t2": "Tag 2"}) - if got := p.Get(tt.commits); !reflect.DeepEqual(got, tt.want) { + if got := p.Get(tt.date, tt.commits); !reflect.DeepEqual(got, tt.want) { t.Errorf("ReleaseNoteProcessorImpl.Get() = %v, want %v", got, tt.want) } })