0
0
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:
Beatriz Vieira 2020-02-01 19:08:41 -03:00 committed by GitHub
commit 7183bc4a1f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 185 additions and 61 deletions

View File

@ -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

View File

@ -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

View File

@ -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)
}

View File

@ -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
View File

@ -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
View File

@ -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=

View File

@ -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)))

View File

@ -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,
}

View File

@ -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.

View File

@ -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)
}
})