mirror of
https://github.com/thegeeklab/git-sv.git
synced 2024-11-21 22:10:39 +00:00
Merge pull request #1 from bvieira/tag-change-log
Feature: generate release notes and commit log from a existing tag
This commit is contained in:
commit
7183bc4a1f
5
Makefile
5
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
|
||||
|
37
README.md
37
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
|
||||
|
@ -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,27 +68,97 @@ 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
|
||||
|
||||
if tag := c.String("t"); tag != "" {
|
||||
rnVersion, date, commits, err = getTagVersionInfo(git, semverProcessor, tag)
|
||||
} else {
|
||||
rnVersion, date, commits, err = getNextVersionInfo(git, semverProcessor)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 fmt.Errorf("error parsing version: %s from describe, message: %v", describe, err)
|
||||
return semver.Version{}, time.Time{}, nil, 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)
|
||||
return semver.Version{}, time.Time{}, nil, fmt.Errorf("error getting git log, message: %v", err)
|
||||
}
|
||||
|
||||
nextVer := semverProcessor.NextVersion(currentVer, commits)
|
||||
|
||||
releasenote := rnProcessor.Get(commits)
|
||||
fmt.Println(rnProcessor.Format(releasenote, nextVer))
|
||||
return nil
|
||||
}
|
||||
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 {
|
||||
@ -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)
|
||||
}
|
||||
|
@ -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",
|
||||
|
2
go.mod
2
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
|
||||
)
|
||||
|
4
go.sum
4
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=
|
||||
|
50
sv/git.go
50
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)))
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user