0
0
mirror of https://github.com/thegeeklab/git-sv.git synced 2024-06-02 17:39:39 +02:00

feat: generate release-notes and commit-log from tag when flag -t is used

This commit is contained in:
Beatriz Vieira 2020-02-01 18:19:38 -03:00
parent 14270fd86f
commit afa57122ba
6 changed files with 155 additions and 29 deletions

View File

@ -4,7 +4,9 @@ import (
"encoding/json"
"fmt"
"sv4git/sv"
"time"
"github.com/Masterminds/semver"
"github.com/urfave/cli"
)
@ -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)
}

View File

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

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

@ -33,7 +33,7 @@ const markdownTemplate = `# v{{.Version}} ({{.Date}})
// 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)
}
})