mirror of
https://github.com/thegeeklab/git-sv.git
synced 2024-11-13 21:30:40 +00:00
feat: add support for current-version, next-version, commit-log and release-notes
BREAKING CHANGE: first release
This commit is contained in:
parent
d101a63bdb
commit
9374b3addc
43
Makefile
Normal file
43
Makefile
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
.PHONY: usage build run test
|
||||||
|
|
||||||
|
OK_COLOR=\033[32;01m
|
||||||
|
NO_COLOR=\033[0m
|
||||||
|
ERROR_COLOR=\033[31;01m
|
||||||
|
WARN_COLOR=\033[33;01m
|
||||||
|
|
||||||
|
PKGS = $(shell go list ./...)
|
||||||
|
BIN = git-sv
|
||||||
|
|
||||||
|
ECHOFLAGS ?=
|
||||||
|
|
||||||
|
BUILDOS ?= linux
|
||||||
|
BUILDARCH ?= amd64
|
||||||
|
BUILDENVS ?= CGO_ENABLED=0 GOOS=$(BUILDOS) GOARCH=$(BUILDARCH)
|
||||||
|
BUILDFLAGS ?= -a -installsuffix cgo --ldflags '-extldflags "-lm -lstdc++ -static"'
|
||||||
|
|
||||||
|
usage: Makefile
|
||||||
|
@echo $(ECHOFLAGS) "to use make call:"
|
||||||
|
@echo $(ECHOFLAGS) " make <action>"
|
||||||
|
@echo $(ECHOFLAGS) ""
|
||||||
|
@echo $(ECHOFLAGS) "list of available actions:"
|
||||||
|
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
|
||||||
|
|
||||||
|
## build: build git-sv
|
||||||
|
build: test
|
||||||
|
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Building binary ($(BUILDOS)/$(BUILDARCH)/$(BIN))...$(NO_COLOR)"
|
||||||
|
@$(BUILDENVS) go build -v $(BUILDFLAGS) -o bin/$(BUILDOS)_$(BUILDARCH)/$(BIN) ./cmd/git-sv
|
||||||
|
|
||||||
|
## test: run unit tests
|
||||||
|
test:
|
||||||
|
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Running tests...$(NO_COLOR)"
|
||||||
|
@go test $(PKGS)
|
||||||
|
|
||||||
|
## run: run gitlabels-cli
|
||||||
|
run:
|
||||||
|
@echo $(ECHOFLAGS) "$(OK_COLOR)==> Running bin/$(BUILDOS)_$(BUILDARCH)/$(BIN)...$(NO_COLOR)"
|
||||||
|
@./bin/$(BUILDOS)_$(BUILDARCH)/$(BIN) $(args)
|
||||||
|
|
||||||
|
## tidy: execute go mod tidy
|
||||||
|
tidy:
|
||||||
|
@echo $(ECHOFLAGS) "$(OK_COLOR)==> runing tidy"
|
||||||
|
@go mod tidy
|
27
cmd/git-sv/config.go
Normal file
27
cmd/git-sv/config.go
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/kelseyhightower/envconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config env vars for cli configuration
|
||||||
|
type Config struct {
|
||||||
|
MajorVersionTypes []string `envconfig:"MAJOR_VERSION_TYPES" default:""`
|
||||||
|
MinorVersionTypes []string `envconfig:"MINOR_VERSION_TYPES" default:"feat"`
|
||||||
|
PatchVersionTypes []string `envconfig:"PATCH_VERSION_TYPES" default:"build,ci,docs,fix,perf,refactor,style,test"`
|
||||||
|
IncludeUnknownTypeAsPatch bool `envconfig:"INCLUDE_UNKNOWN_TYPE_AS_PATCH" default:"true"`
|
||||||
|
CommitMessageMetadata map[string]string `envconfig:"COMMIT_MESSAGE_METADATA" default:"breakingchange:BREAKING CHANGE,issueid:jira"`
|
||||||
|
TagPattern string `envconfig:"TAG_PATTERN" default:"v%d.%d.%d"`
|
||||||
|
ReleaseNotesTags map[string]string `envconfig:"RELEASE_NOTES_TAGS" default:"fix:Bug Fixes,feat:Features"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig() Config {
|
||||||
|
var c Config
|
||||||
|
err := envconfig.Process("SV", &c)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err.Error())
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
85
cmd/git-sv/handlers.go
Normal file
85
cmd/git-sv/handlers.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"sv4git/sv"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func currentVersionHandler(git sv.Git) func(c *cli.Context) error {
|
||||||
|
return func(c *cli.Context) error {
|
||||||
|
describe := git.Describe()
|
||||||
|
|
||||||
|
currentVer, err := sv.ToVersion(describe)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("%d.%d.%d\n", currentVer.Major(), currentVer.Minor(), currentVer.Patch())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func nextVersionHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *cli.Context) error {
|
||||||
|
return func(c *cli.Context) 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
commits, err := git.Log(describe)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting git log, message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nextVer := semverProcessor.NexVersion(currentVer, commits)
|
||||||
|
fmt.Printf("%d.%d.%d\n", nextVer.Major(), nextVer.Minor(), nextVer.Patch())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func commitLogHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor) func(c *cli.Context) error {
|
||||||
|
return func(c *cli.Context) error {
|
||||||
|
describe := git.Describe()
|
||||||
|
|
||||||
|
commits, err := git.Log(describe)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting git log, message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, commit := range commits {
|
||||||
|
content, err := json.Marshal(commit)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println(string(content))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseNotesHandler(git sv.Git, semverProcessor sv.SemVerCommitsProcessor, rnProcessor sv.ReleaseNoteProcessor) func(c *cli.Context) error {
|
||||||
|
return func(c *cli.Context) 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
commits, err := git.Log(describe)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error getting git log, message: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
nextVer := semverProcessor.NexVersion(currentVer, commits)
|
||||||
|
|
||||||
|
releasenote := rnProcessor.Get(commits)
|
||||||
|
fmt.Println(rnProcessor.Format(releasenote, nextVer))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
52
cmd/git-sv/main.go
Normal file
52
cmd/git-sv/main.go
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sv4git/sv"
|
||||||
|
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := loadConfig()
|
||||||
|
|
||||||
|
git := sv.NewGit(cfg.CommitMessageMetadata, cfg.TagPattern)
|
||||||
|
semverProcessor := sv.NewSemVerCommitsProcessor(cfg.IncludeUnknownTypeAsPatch, cfg.MajorVersionTypes, cfg.MinorVersionTypes, cfg.PatchVersionTypes)
|
||||||
|
releasenotesProcessor := sv.NewReleaseNoteProcessor(cfg.ReleaseNotesTags)
|
||||||
|
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "sv"
|
||||||
|
app.Usage = "semantic version for git"
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
{
|
||||||
|
Name: "current-version",
|
||||||
|
Aliases: []string{"cv"},
|
||||||
|
Usage: "get last released version from git",
|
||||||
|
Action: currentVersionHandler(git),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "next-version",
|
||||||
|
Aliases: []string{"nv"},
|
||||||
|
Usage: "generate the next version based on git commit messages",
|
||||||
|
Action: nextVersionHandler(git, semverProcessor),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "commit-log",
|
||||||
|
Aliases: []string{"cl"},
|
||||||
|
Usage: "list all commit logs since last version as jsons",
|
||||||
|
Action: commitLogHandler(git, semverProcessor),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "release-notes",
|
||||||
|
Aliases: []string{"rn"},
|
||||||
|
Usage: "generate release notes",
|
||||||
|
Action: releaseNotesHandler(git, semverProcessor, releasenotesProcessor),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
apperr := app.Run(os.Args)
|
||||||
|
if apperr != nil {
|
||||||
|
log.Fatal(apperr)
|
||||||
|
}
|
||||||
|
}
|
10
go.mod
Normal file
10
go.mod
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
module sv4git
|
||||||
|
|
||||||
|
go 1.13
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
19
go.sum
Normal file
19
go.sum
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||||
|
github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0 h1:EoUDS0afbrsXAZ9YQ9jdu/mZ2sXgT1/2yyNng4PGlyM=
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
|
||||||
|
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
|
||||||
|
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=
|
||||||
|
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=
|
150
sv/git.go
Normal file
150
sv/git.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package sv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
logSeparator = "##"
|
||||||
|
endLine = "~~"
|
||||||
|
breakingChangesTag = "BREAKING CHANGE:"
|
||||||
|
issueIDTag = "jira:"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Git commands
|
||||||
|
type Git interface {
|
||||||
|
Describe() string
|
||||||
|
Log(lastTag string) ([]GitCommitLog, error)
|
||||||
|
Tag(version semver.Version) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitCommitLog description of a single commit log
|
||||||
|
type GitCommitLog struct {
|
||||||
|
Hash string `json:"hash,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Scope string `json:"scope,omitempty"`
|
||||||
|
Subject string `json:"subject,omitempty"`
|
||||||
|
Body string `json:"body,omitempty"`
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitImpl git command implementation
|
||||||
|
type GitImpl struct {
|
||||||
|
messageMetadata map[string]string
|
||||||
|
tagPattern string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGit constructor
|
||||||
|
func NewGit(messageMetadata map[string]string, tagPattern string) *GitImpl {
|
||||||
|
return &GitImpl{messageMetadata: messageMetadata, tagPattern: tagPattern}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Describe runs git describe, it no tag found, return empty
|
||||||
|
func (GitImpl) Describe() string {
|
||||||
|
cmd := exec.Command("git", "describe", "--abbrev=0")
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(strings.Trim(string(out), "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log return git log
|
||||||
|
func (g GitImpl) Log(lastTag 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseLogOutput(g.messageMetadata, string(out)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag create a git tag
|
||||||
|
func (g GitImpl) Tag(version semver.Version) error {
|
||||||
|
tagMsg := fmt.Sprintf("-v \"Version %d.%d.%d\"", version.Major(), version.Minor(), version.Patch())
|
||||||
|
cmd := exec.Command("git", "tag", "-a "+fmt.Sprintf(g.tagPattern, version.Major(), version.Minor(), version.Patch()), tagMsg)
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseLogOutput(messageMetadata map[string]string, log string) []GitCommitLog {
|
||||||
|
scanner := bufio.NewScanner(strings.NewReader(log))
|
||||||
|
scanner.Split(splitAt([]byte(endLine)))
|
||||||
|
var logs []GitCommitLog
|
||||||
|
for scanner.Scan() {
|
||||||
|
if text := strings.TrimSpace(strings.Trim(scanner.Text(), "\"")); text != "" {
|
||||||
|
logs = append(logs, parseCommitLog(messageMetadata, text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return logs
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCommitLog(messageMetadata map[string]string, commit string) GitCommitLog {
|
||||||
|
content := strings.Split(strings.Trim(commit, "\""), logSeparator)
|
||||||
|
commitType, scope, subject := parseCommitLogMessage(content[1])
|
||||||
|
|
||||||
|
metadata := make(map[string]string)
|
||||||
|
for k, v := range messageMetadata {
|
||||||
|
if tagValue := extractTag(v, content[2]); tagValue != "" {
|
||||||
|
metadata[k] = tagValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return GitCommitLog{
|
||||||
|
Hash: content[0],
|
||||||
|
Type: commitType,
|
||||||
|
Scope: scope,
|
||||||
|
Subject: subject,
|
||||||
|
Body: content[2],
|
||||||
|
Metadata: metadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCommitLogMessage(message string) (string, string, string) {
|
||||||
|
regex := regexp.MustCompile("([a-z]+)(\\((.*)\\))?: (.*)")
|
||||||
|
result := regex.FindStringSubmatch(message)
|
||||||
|
if len(result) != 5 {
|
||||||
|
return "", "", message
|
||||||
|
}
|
||||||
|
return result[1], result[3], strings.TrimSpace(result[4])
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractTag(tag, text string) string {
|
||||||
|
regex := regexp.MustCompile(tag + ": (.*)")
|
||||||
|
result := regex.FindStringSubmatch(text)
|
||||||
|
if len(result) < 2 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return result[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
func splitAt(b []byte) func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
return func(data []byte, atEOF bool) (advance int, token []byte, err error) {
|
||||||
|
dataLen := len(data)
|
||||||
|
|
||||||
|
if atEOF && dataLen == 0 {
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if i := bytes.Index(data, b); i >= 0 {
|
||||||
|
return i + len(b), data[0:i], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if atEOF {
|
||||||
|
return dataLen, data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
}
|
98
sv/releasenotes.go
Normal file
98
sv/releasenotes.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package sv
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"text/template"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver"
|
||||||
|
)
|
||||||
|
|
||||||
|
type releaseNoteTemplate struct {
|
||||||
|
Version string
|
||||||
|
Date string
|
||||||
|
Sections map[string]ReleaseNoteSection
|
||||||
|
BreakingChanges []string
|
||||||
|
}
|
||||||
|
|
||||||
|
const markdownTemplate = `# v{{.Version}} ({{.Date}})
|
||||||
|
|
||||||
|
{{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}}
|
||||||
|
{{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
|
||||||
|
{{range $k,$v := .BreakingChanges}}
|
||||||
|
- {{$v}}{{end}}
|
||||||
|
{{end}}`
|
||||||
|
|
||||||
|
// ReleaseNoteProcessor release note processor interface.
|
||||||
|
type ReleaseNoteProcessor interface {
|
||||||
|
Get(commits []GitCommitLog) ReleaseNote
|
||||||
|
Format(releasenote ReleaseNote, version semver.Version) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseNoteProcessorImpl release note based on commit log.
|
||||||
|
type ReleaseNoteProcessorImpl struct {
|
||||||
|
tags map[string]string
|
||||||
|
template *template.Template
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewReleaseNoteProcessor ReleaseNoteProcessor constructor.
|
||||||
|
func NewReleaseNoteProcessor(tags map[string]string) *ReleaseNoteProcessorImpl {
|
||||||
|
template := template.Must(template.New("markdown").Parse(markdownTemplate))
|
||||||
|
return &ReleaseNoteProcessorImpl{tags: tags, template: template}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get generate a release note based on commits.
|
||||||
|
func (p ReleaseNoteProcessorImpl) Get(commits []GitCommitLog) ReleaseNote {
|
||||||
|
sections := make(map[string]ReleaseNoteSection)
|
||||||
|
var breakingChanges []string
|
||||||
|
for _, commit := range commits {
|
||||||
|
if name, exists := p.tags[commit.Type]; exists {
|
||||||
|
section, sexists := sections[commit.Type]
|
||||||
|
if !sexists {
|
||||||
|
section = ReleaseNoteSection{Name: name}
|
||||||
|
}
|
||||||
|
section.Items = append(section.Items, commit)
|
||||||
|
sections[commit.Type] = section
|
||||||
|
}
|
||||||
|
if value, exists := commit.Metadata[BreakingChangeTag]; exists {
|
||||||
|
breakingChanges = append(breakingChanges, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReleaseNote{Date: time.Now(), Sections: sections, BreakingChanges: breakingChanges}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format format a release note.
|
||||||
|
func (p ReleaseNoteProcessorImpl) Format(releasenote ReleaseNote, version semver.Version) string {
|
||||||
|
templateVars := releaseNoteTemplate{
|
||||||
|
Version: fmt.Sprintf("%d.%d.%d", version.Major(), version.Minor(), version.Patch()),
|
||||||
|
Date: releasenote.Date.Format("2006-01-02"),
|
||||||
|
Sections: releasenote.Sections,
|
||||||
|
BreakingChanges: releasenote.BreakingChanges,
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
p.template.Execute(&b, templateVars)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseNote release note.
|
||||||
|
type ReleaseNote struct {
|
||||||
|
Date time.Time
|
||||||
|
Sections map[string]ReleaseNoteSection
|
||||||
|
BreakingChanges []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReleaseNoteSection release note section.
|
||||||
|
type ReleaseNoteSection struct {
|
||||||
|
Name string
|
||||||
|
Items []GitCommitLog
|
||||||
|
}
|
96
sv/semver.go
Normal file
96
sv/semver.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package sv
|
||||||
|
|
||||||
|
import "github.com/Masterminds/semver"
|
||||||
|
|
||||||
|
type versionType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
none versionType = iota
|
||||||
|
patch
|
||||||
|
minor
|
||||||
|
major
|
||||||
|
)
|
||||||
|
|
||||||
|
// ToVersion parse string to semver.Version
|
||||||
|
func ToVersion(value string) (semver.Version, error) {
|
||||||
|
version := value
|
||||||
|
if version == "" {
|
||||||
|
version = "0.0.0"
|
||||||
|
}
|
||||||
|
v, err := semver.NewVersion(version)
|
||||||
|
return *v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// BreakingChangeTag breaking change tag from commit metadata
|
||||||
|
const BreakingChangeTag string = "breakingchange"
|
||||||
|
|
||||||
|
// SemVerCommitsProcessor interface
|
||||||
|
type SemVerCommitsProcessor interface {
|
||||||
|
NexVersion(version semver.Version, commits []GitCommitLog) semver.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
// SemVerCommitsProcessorImpl process versions using commit log
|
||||||
|
type SemVerCommitsProcessorImpl struct {
|
||||||
|
MajorVersionTypes map[string]struct{}
|
||||||
|
MinorVersionTypes map[string]struct{}
|
||||||
|
PatchVersionTypes map[string]struct{}
|
||||||
|
IncludeUnknownTypeAsPatch bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSemVerCommitsProcessor SemanticVersionCommitsProcessorImpl constructor
|
||||||
|
func NewSemVerCommitsProcessor(unknownAsPatch bool, majorTypes, minorTypes, patchTypes []string) *SemVerCommitsProcessorImpl {
|
||||||
|
return &SemVerCommitsProcessorImpl{
|
||||||
|
IncludeUnknownTypeAsPatch: unknownAsPatch,
|
||||||
|
MajorVersionTypes: toMap(majorTypes),
|
||||||
|
MinorVersionTypes: toMap(minorTypes),
|
||||||
|
PatchVersionTypes: toMap(patchTypes),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NexVersion calculates next version based on commit log
|
||||||
|
func (p SemVerCommitsProcessorImpl) NexVersion(version semver.Version, commits []GitCommitLog) semver.Version {
|
||||||
|
var versionToUpdate = none
|
||||||
|
for _, commit := range commits {
|
||||||
|
if v := p.versionTypeToUpdate(commit); v > versionToUpdate {
|
||||||
|
versionToUpdate = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch versionToUpdate {
|
||||||
|
case major:
|
||||||
|
return version.IncMajor()
|
||||||
|
case minor:
|
||||||
|
return version.IncMinor()
|
||||||
|
case patch:
|
||||||
|
return version.IncPatch()
|
||||||
|
default:
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p SemVerCommitsProcessorImpl) versionTypeToUpdate(commit GitCommitLog) versionType {
|
||||||
|
if _, exists := commit.Metadata[BreakingChangeTag]; exists {
|
||||||
|
return major
|
||||||
|
}
|
||||||
|
if _, exists := p.MajorVersionTypes[commit.Type]; exists {
|
||||||
|
return major
|
||||||
|
}
|
||||||
|
if _, exists := p.MinorVersionTypes[commit.Type]; exists {
|
||||||
|
return minor
|
||||||
|
}
|
||||||
|
if _, exists := p.PatchVersionTypes[commit.Type]; exists {
|
||||||
|
return patch
|
||||||
|
}
|
||||||
|
if p.IncludeUnknownTypeAsPatch {
|
||||||
|
return patch
|
||||||
|
}
|
||||||
|
return none
|
||||||
|
}
|
||||||
|
|
||||||
|
func toMap(values []string) map[string]struct{} {
|
||||||
|
result := make(map[string]struct{})
|
||||||
|
for _, v := range values {
|
||||||
|
result[v] = struct{}{}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user