1
0
Fork 0
mirror of https://github.com/Luzifer/git-changerelease.git synced 2024-10-18 06:04:20 +00:00

Modernize code, update deps, fix linter errors

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2023-11-11 15:27:37 +01:00
parent 199a1b4984
commit 810e271c3b
Signed by: luzifer
GPG key ID: D91C3E91E4CAD6F5
9 changed files with 335 additions and 632 deletions

View file

@ -9,10 +9,6 @@ update-sharness:
curl -sSLo ./integration/aggregate-results.sh https://cdn.rawgit.com/chriscool/sharness/$(SHARNESS_VERSION)/aggregate-results.sh
curl -sSLo ./integration/Makefile https://cdn.rawgit.com/chriscool/sharness/$(SHARNESS_VERSION)/test/Makefile
generate:
go-bindata -o assets.go assets/...
gofmt -s -w assets.go
test:
go test .
go vet .

322
assets.go
View file

@ -1,322 +1,26 @@
// Code generated by go-bindata. DO NOT EDIT.
// sources:
// assets/git_changerelease.yaml
// assets/log_template.md
package main
import (
"bytes"
"compress/gzip"
"embed"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"time"
)
func bindataRead(data []byte, name string) ([]byte, error) {
gz, err := gzip.NewReader(bytes.NewBuffer(data))
//go:embed assets/*
var assetFS embed.FS
func asset(name string) ([]byte, error) {
data, err := assetFS.ReadFile(name)
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
return data, fmt.Errorf("reading asset: %w", err)
}
var buf bytes.Buffer
_, err = io.Copy(&buf, gz)
clErr := gz.Close()
return data, nil
}
func mustAsset(name string) []byte {
data, err := asset(name)
if err != nil {
return nil, fmt.Errorf("Read %q: %v", name, err)
panic(err)
}
if clErr != nil {
return nil, err
}
return buf.Bytes(), nil
}
type asset struct {
bytes []byte
info fileInfoEx
}
type fileInfoEx interface {
os.FileInfo
MD5Checksum() string
}
type bindataFileInfo struct {
name string
size int64
mode os.FileMode
modTime time.Time
md5checksum string
}
func (fi bindataFileInfo) Name() string {
return fi.name
}
func (fi bindataFileInfo) Size() int64 {
return fi.size
}
func (fi bindataFileInfo) Mode() os.FileMode {
return fi.mode
}
func (fi bindataFileInfo) ModTime() time.Time {
return fi.modTime
}
func (fi bindataFileInfo) MD5Checksum() string {
return fi.md5checksum
}
func (fi bindataFileInfo) IsDir() bool {
return false
}
func (fi bindataFileInfo) Sys() interface{} {
return nil
}
var _bindataAssetsGitchangereleaseyaml = []byte(
"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xb4\x92\xcf\x6a\x1b\x31\x10\xc6\xef\x7a\x8a\x0f\xfb\xd2\x1e\x56\x24\x3d" +
"\xfa\xd0\x43\x68\xe3\x53\x21\x94\xd2\x4b\x49\xcd\x78\x77\x56\xab\x56\x2b\x6d\x47\x92\x1d\x13\xf2\xee\x45\x92\x6d" +
"\x0a\x81\xde\x72\x1b\xd0\xf7\xe7\x27\x8d\xba\xae\x53\x6a\x8d\x4f\x36\xd2\xde\x31\xb6\x0f\x5b\x44\x6b\xbc\xf5\x06" +
"\x63\x10\x24\x32\x11\xd6\xa3\xa7\xc8\x38\x85\x8c\x21\xc0\x87\x84\x23\xf9\x84\x14\x30\xd1\x81\xab\x81\x07\xb5\x6e" +
"\xea\x20\x17\x51\x3b\xe4\x84\xbc\xbc\x0a\x36\x36\x69\x35\xb4\xd6\x5d\x0b\xd8\x15\xfb\x06\x23\xb9\xc8\x85\xe9\xde" +
"\xba\xc4\x82\x90\x13\xfa\x30\xcf\x36\x45\xe4\x58\xfc\x04\x61\x93\x1d\x09\xf8\x69\x11\x8e\xd1\x06\x8f\x99\x52\x3f" +
"\x21\x78\xa4\x89\xcf\x7a\xb5\xc6\xcc\x31\x92\x61\x8d\xbb\x13\x06\x1e\x29\xbb\x84\x3d\xbb\x70\x84\x8d\xa0\x66\x62" +
"\xa9\x44\x0f\x5f\x31\xb3\x18\x8e\x25\x64\x6b\xd3\x94\xf7\x5a\x59\xe3\x83\xf0\xee\x1c\x13\x37\x0a\xe8\xb0\xfa\xf9" +
"\xa5\x08\xb1\x64\xe7\x20\xfc\x27\x73\x4c\xab\x42\xfc\xf9\xc0\x72\x3a\x97\x5f\xaa\x5b\x47\xc1\x0e\x9e\x11\xc6\xca" +
"\xf7\x9a\x3f\x16\x3c\xdb\x9e\xd1\x7a\x4c\x2c\x8c\xa3\x75\x0e\x3d\xe5\xc8\x20\x2c\x25\xa6\x3b\xb0\xd4\xdb\x5a\xdf" +
"\x0b\x97\x9d\xbc\xbb\xd5\x37\xfa\x06\xdd\x47\x94\xe1\xf6\xbd\x56\xb5\x6f\x57\xe5\x17\xdc\x1f\xe3\xfd\xa3\x7d\x7a" +
"\x73\xc4\x99\x7e\x05\xf9\x1f\xe2\x87\x32\x5c\x11\xab\xfc\x8a\x78\xb7\x7f\x14\xa6\xdf\xd6\x9b\xca\xf9\x8d\xe7\xc5" +
"\x51\xe2\xf2\xc9\xc6\x20\x33\xa5\x7f\x16\x7b\x05\xef\x83\x4f\x64\xeb\xa7\xaa\xa7\x13\x79\xc3\x2e\x98\xf3\xa4\xd6" +
"\x38\x4e\xb6\x9f\x1a\xe6\x9e\x91\x23\x0f\x25\x91\x86\xa1\x1a\x12\x19\xa4\xa0\x95\xb0\x2b\xa8\xbb\x16\x7f\x59\xf7" +
"\x06\xab\x45\x78\x21\x29\xaf\x51\x05\x78\x7e\xd6\xdf\xdb\xfd\x5e\x5e\x56\x4a\x69\xad\xd5\xdf\x00\x00\x00\xff\xff" +
"\x8b\x01\xe7\xd6\x42\x03\x00\x00")
func bindataAssetsGitchangereleaseyamlBytes() ([]byte, error) {
return bindataRead(
_bindataAssetsGitchangereleaseyaml,
"assets/git_changerelease.yaml",
)
}
func bindataAssetsGitchangereleaseyaml() (*asset, error) {
bytes, err := bindataAssetsGitchangereleaseyamlBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{
name: "assets/git_changerelease.yaml",
size: 834,
md5checksum: "",
mode: os.FileMode(420),
modTime: time.Unix(1537952886, 0),
}
a := &asset{bytes: bytes, info: info}
return a, nil
}
var _bindataAssetsLogtemplatemd = []byte(
"\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x52\x56\xa8\xae\x56\xd0\xf3\x4b\xad\x28\x09\x4b\x2d\x2a\xce\xcc\xcf\x53" +
"\xa8\xad\x55\xd0\x87\x08\xe6\x97\xeb\xb9\xe5\x17\xe5\x26\x96\x28\x28\x19\x19\x18\x98\xe9\x1a\x18\xea\x1a\x18\x29" +
"\x29\xd4\xd6\x72\x55\x57\x2b\x14\x25\xe6\xa5\xa7\x2a\xa8\xe4\x64\xe6\xa5\x2a\x58\xd9\x2a\xe8\xf9\xe4\xa7\xfb\x64" +
"\xe6\xa5\x16\x83\xa4\x15\x14\xb4\x40\x26\x80\x25\xf5\x82\x4b\x93\xb2\x52\x93\x4b\x20\xda\x74\x15\x52\xf3\x52\x40" +
"\x4c\x90\x11\x7a\xfe\x39\x29\x3e\xf9\xe9\x20\x2e\x20\x00\x00\xff\xff\x04\x83\xa4\x75\x87\x00\x00\x00")
func bindataAssetsLogtemplatemdBytes() ([]byte, error) {
return bindataRead(
_bindataAssetsLogtemplatemd,
"assets/log_template.md",
)
}
func bindataAssetsLogtemplatemd() (*asset, error) {
bytes, err := bindataAssetsLogtemplatemdBytes()
if err != nil {
return nil, err
}
info := bindataFileInfo{
name: "assets/log_template.md",
size: 135,
md5checksum: "",
mode: os.FileMode(436),
modTime: time.Unix(1468497058, 0),
}
a := &asset{bytes: bytes, info: info}
return a, nil
}
//
// Asset loads and returns the asset for the given name.
// It returns an error if the asset could not be found or
// could not be loaded.
//
func Asset(name string) ([]byte, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err)
}
return a.bytes, nil
}
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist}
}
//
// MustAsset is like Asset but panics when Asset would return an error.
// It simplifies safe initialization of global variables.
// nolint: deadcode
//
func MustAsset(name string) []byte {
a, err := Asset(name)
if err != nil {
panic("asset: Asset(" + name + "): " + err.Error())
}
return a
}
//
// AssetInfo loads and returns the asset info for the given name.
// It returns an error if the asset could not be found or could not be loaded.
//
func AssetInfo(name string) (os.FileInfo, error) {
cannonicalName := strings.Replace(name, "\\", "/", -1)
if f, ok := _bindata[cannonicalName]; ok {
a, err := f()
if err != nil {
return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err)
}
return a.info, nil
}
return nil, &os.PathError{Op: "open", Path: name, Err: os.ErrNotExist}
}
//
// AssetNames returns the names of the assets.
// nolint: deadcode
//
func AssetNames() []string {
names := make([]string, 0, len(_bindata))
for name := range _bindata {
names = append(names, name)
}
return names
}
//
// _bindata is a table, holding each asset generator, mapped to its name.
//
var _bindata = map[string]func() (*asset, error){
"assets/git_changerelease.yaml": bindataAssetsGitchangereleaseyaml,
"assets/log_template.md": bindataAssetsLogtemplatemd,
}
//
// AssetDir returns the file names below a certain
// directory embedded in the file by go-bindata.
// For example if you run go-bindata on data/... and data contains the
// following hierarchy:
// data/
// foo.txt
// img/
// a.png
// b.png
// then AssetDir("data") would return []string{"foo.txt", "img"}
// AssetDir("data/img") would return []string{"a.png", "b.png"}
// AssetDir("foo.txt") and AssetDir("notexist") would return an error
// AssetDir("") will return []string{"data"}.
//
func AssetDir(name string) ([]string, error) {
node := _bintree
if len(name) != 0 {
cannonicalName := strings.Replace(name, "\\", "/", -1)
pathList := strings.Split(cannonicalName, "/")
for _, p := range pathList {
node = node.Children[p]
if node == nil {
return nil, &os.PathError{
Op: "open",
Path: name,
Err: os.ErrNotExist,
}
}
}
}
if node.Func != nil {
return nil, &os.PathError{
Op: "open",
Path: name,
Err: os.ErrNotExist,
}
}
rv := make([]string, 0, len(node.Children))
for childName := range node.Children {
rv = append(rv, childName)
}
return rv, nil
}
type bintree struct {
Func func() (*asset, error)
Children map[string]*bintree
}
var _bintree = &bintree{Func: nil, Children: map[string]*bintree{
"assets": {Func: nil, Children: map[string]*bintree{
"git_changerelease.yaml": {Func: bindataAssetsGitchangereleaseyaml, Children: map[string]*bintree{}},
"log_template.md": {Func: bindataAssetsLogtemplatemd, Children: map[string]*bintree{}},
}},
}}
// RestoreAsset restores an asset under the given directory
func RestoreAsset(dir, name string) error {
data, err := Asset(name)
if err != nil {
return err
}
info, err := AssetInfo(name)
if err != nil {
return err
}
err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755))
if err != nil {
return err
}
err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode())
if err != nil {
return err
}
return os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime())
}
// RestoreAssets restores an asset under the given directory recursively
func RestoreAssets(dir, name string) error {
children, err := AssetDir(name)
// File
if err != nil {
return RestoreAsset(dir, name)
}
// Dir
for _, child := range children {
err = RestoreAssets(dir, filepath.Join(name, child))
if err != nil {
return err
}
}
return nil
}
func _filePath(dir, name string) string {
cannonicalName := strings.Replace(name, "\\", "/", -1)
return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...)
return data
}

View file

@ -2,8 +2,10 @@ package main
import (
"errors"
"fmt"
"os"
"github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
)
@ -19,19 +21,27 @@ func loadConfig() (*configFile, error) {
var err error
if _, err = os.Stat(cfg.ConfigFile); err != nil {
return nil, errors.New("Config file does not exist, use --create-config to create one")
return nil, errors.New("config file does not exist, use --create-config to create one")
}
c := &configFile{}
if err = yaml.Unmarshal(MustAsset("assets/git_changerelease.yaml"), c); err != nil {
return nil, err
if err = yaml.Unmarshal(mustAsset("assets/git_changerelease.yaml"), c); err != nil {
return nil, fmt.Errorf("unmarshalling default config: %w", err)
}
dataFile, err := os.Open(cfg.ConfigFile)
if err != nil {
return nil, err
return nil, fmt.Errorf("opening config file: %w", err)
}
defer dataFile.Close()
defer func() {
if err := dataFile.Close(); err != nil {
logrus.WithError(err).Debug("closing config file (leaked fd)")
}
}()
return c, yaml.NewDecoder(dataFile).Decode(c)
if err = yaml.NewDecoder(dataFile).Decode(c); err != nil {
return c, fmt.Errorf("decoding config file: %w", err)
}
return c, nil
}

129
git.go
View file

@ -3,8 +3,12 @@ package main
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path"
"regexp"
"strings"
)
@ -30,10 +34,110 @@ type commit struct {
BumpType semVerBump
}
func applyTag(stringVersion string) error {
var err error
if _, err = gitErr("add", cfg.ChangelogFile); err != nil {
return fmt.Errorf("adding changelog file: %w", err)
}
commitMessage, err := renderTemplate("commitMessage", []byte(config.ReleaseCommitMessage), struct {
Version string
}{
Version: stringVersion,
})
if err != nil {
return fmt.Errorf("building commit message: %w", err)
}
if _, err := gitErr("commit", "-m", string(commitMessage)); err != nil {
return fmt.Errorf("committing changelog: %w", err)
}
tagType := "-s" // By default use signed tags
if config.DiableTagSigning {
tagType = "-a" // If requested switch to annotated tags
}
if _, err := gitErr("tag", tagType, "-m", stringVersion, stringVersion); err != nil {
return fmt.Errorf("tagging release: %w", err)
}
return nil
}
//revive:disable-next-line:flag-parameter // Fine in this case
func fetchGitLogs(since string, fetchAll bool) ([]commit, error) {
// Fetch logs since last tag / since repo start
logArgs := []string{"log", `--format=` + gitLogFormat, "--abbrev-commit"}
if !fetchAll {
logArgs = append(logArgs, fmt.Sprintf("%s..HEAD", since))
}
rawLogs, err := gitErr(logArgs...)
if err != nil {
return nil, fmt.Errorf("reading git log entries: %w", err)
}
logs := []commit{}
for _, l := range strings.Split(rawLogs, "\n") {
if l == "" {
continue
}
pl, err := parseCommit(l)
if err != nil {
return nil, errors.New("git used an unexpected log format")
}
addLog := true
for _, match := range config.IgnoreMessages {
r := regexp.MustCompile(match)
if r.MatchString(pl.Subject) {
addLog = false
break
}
}
if addLog {
logs = append(logs, *pl)
}
}
return logs, nil
}
func filenameInGitRoot(name string) (string, error) {
root, err := git(io.Discard, "rev-parse", "--show-toplevel")
if err != nil {
return "", fmt.Errorf("resolving repo root: %w", err)
}
return path.Join(root, name), nil
}
func git(errOut io.Writer, args ...string) (string, error) {
buf := bytes.NewBuffer([]byte{})
cmd := exec.Command("git", args...)
cmd.Stdout = buf
cmd.Stderr = errOut
err := cmd.Run()
return strings.TrimSpace(buf.String()), err
}
func gitErr(args ...string) (string, error) {
return git(os.Stderr, args...)
}
func gitSilent(args ...string) (string, error) {
return git(io.Discard, args...)
}
func parseCommit(line string) (*commit, error) {
t := strings.Split(line, "\t")
if len(t) != 4 {
return nil, errors.New("Unexpected line format")
if len(t) != len(gitLogFormatParts) {
return nil, errors.New("unexpected line format")
}
c := &commit{
@ -55,24 +159,3 @@ func parseCommit(line string) (*commit, error) {
return c, nil
}
func git(stderrEnabled bool, args ...string) (string, error) {
buf := bytes.NewBuffer([]byte{})
cmd := exec.Command("git", args...)
cmd.Stdout = buf
if stderrEnabled {
cmd.Stderr = os.Stderr
}
err := cmd.Run()
return strings.TrimSpace(buf.String()), err
}
func gitErr(args ...string) (string, error) {
return git(true, args...)
}
func gitSilent(args ...string) (string, error) {
return git(false, args...)
}

16
go.mod
View file

@ -1,11 +1,17 @@
module github.com/Luzifer/git-changerelease
go 1.14
go 1.21
require (
github.com/Luzifer/rconfig/v2 v2.2.1
github.com/Luzifer/rconfig/v2 v2.4.0
github.com/Masterminds/semver/v3 v3.2.1
github.com/mitchellh/go-homedir v1.1.0
github.com/pkg/errors v0.9.1
github.com/sirupsen/logrus v1.6.0
gopkg.in/yaml.v2 v2.3.0
github.com/sirupsen/logrus v1.9.3
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.14.0 // indirect
gopkg.in/validator.v2 v2.0.1 // indirect
)

48
go.sum
View file

@ -1,24 +1,34 @@
github.com/Luzifer/rconfig v1.2.0 h1:waD1sqasGVSQSrExpLrQ9Q1JmMaltrS391VdOjWXP/I=
github.com/Luzifer/rconfig/v2 v2.2.1 h1:zcDdLQlnlzwcBJ8E0WFzOkQE1pCMn3EbX0dFYkeTczg=
github.com/Luzifer/rconfig/v2 v2.2.1/go.mod h1:OKIX0/JRZrPJ/ZXXWklQEFXA6tBfWaljZbW37w+sqBw=
github.com/Luzifer/rconfig/v2 v2.4.0 h1:MAdymTlExAZ8mx5VG8xOFAtFQSpWBipKYQHPOmYTn9o=
github.com/Luzifer/rconfig/v2 v2.4.0/go.mod h1:hWF3ZVSusbYlg5bEvCwalEyUSY+0JPJWUiIu7rBmav8=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19 h1:WB265cn5OpO+hK3pikC9hpP1zI/KTwmyMFKloW9eOVc=
gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY=
gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

304
main.go
View file

@ -1,26 +1,28 @@
package main
//go:generate make generate
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"io/fs"
"os"
"os/exec"
"path"
"regexp"
"strings"
"text/template"
"time"
"github.com/mitchellh/go-homedir"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"github.com/sirupsen/logrus"
"github.com/Luzifer/rconfig/v2"
)
const (
fileModeChangelog = 0o644
fileModeConfig = 0o600
)
var (
cfg = struct {
ChangelogFile string `flag:"changelog" default:"History.md" description:"File to write the changelog to"`
@ -29,8 +31,8 @@ var (
MkConfig bool `flag:"create-config" default:"false" description:"Copy an example configuration file to the location of --config"`
NoEdit bool `flag:"no-edit" default:"false" description:"Do not open the $EDITOR to modify the changelog"`
PreRelease string `flag:"pre-release" default:"" description:"Pre-Release information to append to the version (e.g. 'beta' or 'alpha.1')"`
ReleaseMeta string `flag:"release-meta" default:"" description:"Release metadata to append to the version (e.g. 'exp.sha.5114f85' or '20130313144700')"`
PreRelease string `flag:"pre-release" default:"" description:"Pre-Release information to append to the version (i.e. 'beta' or 'alpha.1')"`
ReleaseMeta string `flag:"release-meta" default:"" description:"Release metadata to append to the version (i.e. 'exp.sha.5114f85' or '20130313144700')"`
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
}{}
@ -39,91 +41,69 @@ var (
version = "dev"
matchers = make(map[*regexp.Regexp]semVerBump)
errExitZero = errors.New("should exit zero now")
)
func applyTag(stringVersion string) error {
var err error
if _, err = gitErr("add", cfg.ChangelogFile); err != nil {
return errors.Wrap(err, "Unable to add changelog file")
func initApp() (err error) {
rconfig.AutoEnv(true)
if err = rconfig.Parse(&cfg); err != nil {
return fmt.Errorf("parsing cli options: %w", err)
}
commitMessage, err := quickTemplate("commitMessage", []byte(config.ReleaseCommitMessage), map[string]interface{}{
"Version": stringVersion,
})
if cfg.VersionAndExit {
fmt.Printf("git-changerelease %s\n", version) //nolint:forbidigo // Fine in this case
return errExitZero
}
cfg.ConfigFile, err = homedir.Expand(cfg.ConfigFile)
if err != nil {
return errors.Wrap(err, "Unable to compile commit message")
}
if _, err := gitErr("commit", "-m", string(commitMessage)); err != nil {
return errors.Wrap(err, "Unable to commit changelog")
return fmt.Errorf("expanding file path: %w", err)
}
tagType := "-s" // By default use signed tags
if config.DiableTagSigning {
tagType = "-a" // If requested switch to annotated tags
var l logrus.Level
if l, err = logrus.ParseLevel(cfg.LogLevel); err != nil {
return fmt.Errorf("parsing log-level: %w", err)
}
logrus.SetLevel(l)
if cfg.MkConfig {
if err = os.WriteFile(cfg.ConfigFile, mustAsset("assets/git_changerelease.yaml"), fileModeConfig); err != nil {
return fmt.Errorf("writing example config to %q: %w", cfg.ConfigFile, err)
}
logrus.Infof("wrote an example configuration to %q", cfg.ConfigFile)
return errExitZero
}
if _, err := gitErr("tag", tagType, "-m", stringVersion, stringVersion); err != nil {
return errors.Wrap(err, "Unable to tag release")
if !cfg.NoEdit && os.Getenv("EDITOR") == "" {
return errors.New("tried to open the changelog in the editor but there is no $EDITOR in your env")
}
if config, err = loadConfig(); err != nil {
return fmt.Errorf("loading config file: %w", err)
}
// Collect matchers
if err = loadMatcherRegex(config.MatchPatch, semVerBumpPatch); err != nil {
return fmt.Errorf("loading patch matcher: %w", err)
}
if err = loadMatcherRegex(config.MatchMajor, semVerBumpMajor); err != nil {
return fmt.Errorf("loading major matcher: %w", err)
}
if cfg.ChangelogFile, err = filenameInGitRoot(cfg.ChangelogFile); err != nil {
return fmt.Errorf("getting absolute path to changelog file: %w", err)
}
return nil
}
func fetchGitLogs(since string, fetchAll bool) ([]commit, error) {
// Fetch logs since last tag / since repo start
logArgs := []string{"log", `--format=` + gitLogFormat, "--abbrev-commit"}
if !fetchAll {
logArgs = append(logArgs, fmt.Sprintf("%s..HEAD", since))
}
rawLogs, err := gitErr(logArgs...)
if err != nil {
return nil, errors.Wrap(err, "Unable to read git log entries")
}
logs := []commit{}
for _, l := range strings.Split(rawLogs, "\n") {
if l == "" {
continue
}
pl, err := parseCommit(l)
if err != nil {
return nil, errors.New("Git used an unexpected log format")
}
addLog := true
for _, match := range config.IgnoreMessages {
r := regexp.MustCompile(match)
if r.MatchString(pl.Subject) {
addLog = false
break
}
}
if addLog {
logs = append(logs, *pl)
}
}
return logs, nil
}
func filenameToGitRoot(fn string) (string, error) {
root, err := git(false, "rev-parse", "--show-toplevel")
if err != nil {
return "", errors.Wrap(err, "Unable to fetch root dir")
}
return path.Join(root, fn), nil
}
func loadMatcherRegex(matches []string, bump semVerBump) error {
for _, match := range matches {
r, err := regexp.Compile(match)
if err != nil {
return errors.Wrapf(err, "Unable to parse regex '%s'", match)
return fmt.Errorf("parsing regex %q: %w", match, err)
}
matchers[r] = bump
}
@ -132,7 +112,13 @@ func loadMatcherRegex(matches []string, bump semVerBump) error {
}
func main() {
prepareRun()
var err error
if err = initApp(); err != nil {
if errors.Is(err, errExitZero) {
os.Exit(0)
}
logrus.WithError(err).Fatal("initializing app")
}
// Get last tag
lastTag, err := gitSilent("describe", "--tags", "--abbrev=0", `--match=v[0-9]*\.[0-9]*\.[0-9]*`)
@ -142,28 +128,28 @@ func main() {
logs, err := fetchGitLogs(lastTag, err != nil)
if err != nil {
log.WithError(err).Fatal("Could not fetch git logs")
logrus.WithError(err).Fatal("fetching git logs")
}
if len(logs) == 0 {
log.Info("Found no changes since last tag, stopping now.")
logrus.Info("found no changes since last tag, stopping now.")
return
}
// Generate new version
newVersion, err := newVersionFromLogs(lastTag, logs)
if err != nil {
log.WithError(err).Fatal("Was unable to bump version")
logrus.WithError(err).Fatal("bumping version")
}
// Render log
if newVersion, err = renderLog(newVersion, logs); err != nil {
log.WithError(err).Fatal("Could not write changelog")
if newVersion, err = renderLogAndGetVersion(newVersion, logs); err != nil {
logrus.WithError(err).Fatal("writing changelog")
}
// Write the tag
if err = applyTag("v" + newVersion.String()); err != nil {
log.WithError(err).Fatal("Unable to apply tag")
logrus.WithError(err).Fatal("applying tag")
}
}
@ -171,140 +157,104 @@ func newVersionFromLogs(lastTag string, logs []commit) (*semVer, error) {
// Tetermine increase type
semVerBumpType, err := selectBumpType(logs)
if err != nil {
return nil, errors.Wrap(err, "Could not determine how to increase the version")
return nil, fmt.Errorf("determining bump type: %w", err)
}
// Generate new version
newVersion, err := parseSemVer(lastTag)
if err != nil {
return nil, errors.Wrap(err, "Was unable to parse previous version")
return nil, fmt.Errorf("parsing previous version: %w", err)
}
if newVersion.PreReleaseInformation == "" && cfg.PreRelease == "" {
newVersion.Bump(semVerBumpType)
newVersion.Bump(semVerBumpType)
if err = newVersion.SetPrerelease(cfg.PreRelease); err != nil {
return newVersion, fmt.Errorf("setting prerelease: %w", err)
}
if err = newVersion.SetMetadata(cfg.ReleaseMeta); err != nil {
return newVersion, fmt.Errorf("setting metadata: %w", err)
}
newVersion.PreReleaseInformation = cfg.PreRelease
newVersion.MetaData = cfg.ReleaseMeta
return newVersion, nil
}
func prepareRun() {
var err error
rconfig.AutoEnv(true)
if err = rconfig.Parse(&cfg); err != nil {
log.WithError(err).Fatal("Unable to parse commandline options")
}
if cfg.VersionAndExit {
fmt.Printf("git-changerelease %s\n", version)
os.Exit(0)
}
cfg.ConfigFile, err = homedir.Expand(cfg.ConfigFile)
if err != nil {
log.WithError(err).Fatal("Could not expand config file path")
}
var l log.Level
if l, err = log.ParseLevel(cfg.LogLevel); err != nil {
log.WithError(err).Fatal("Unable to parse log level")
} else {
log.SetLevel(l)
}
if cfg.MkConfig {
if err = ioutil.WriteFile(cfg.ConfigFile, MustAsset("assets/git_changerelease.yaml"), 0600); err != nil {
log.WithError(err).Fatalf("Could not write example configuration to %q", cfg.ConfigFile)
}
log.Infof("Wrote an example configuration to %q", cfg.ConfigFile)
os.Exit(0)
}
if !cfg.NoEdit && os.Getenv("EDITOR") == "" {
log.Fatal("You chose to open the changelog in the editor but there is no $EDITOR in your env")
}
if config, err = loadConfig(); err != nil {
log.WithError(err).Fatal("Unable to load config file")
}
// Collect matchers
if err = loadMatcherRegex(config.MatchPatch, semVerBumpPatch); err != nil {
log.WithError(err).Fatal("Unable to load patch matcher expressions")
}
if err = loadMatcherRegex(config.MatchMajor, semVerBumpMajor); err != nil {
log.WithError(err).Fatal("Unable to load major matcher expressions")
}
if cfg.ChangelogFile, err = filenameToGitRoot(cfg.ChangelogFile); err != nil {
log.WithError(err).Fatal("Unable to get absolute path to changelog file")
}
}
func quickTemplate(name string, tplSrc []byte, values map[string]interface{}) ([]byte, error) {
tpl, err := template.New(name).Parse(string(tplSrc))
if err != nil {
return nil, errors.New("Unable to parse log template: " + err.Error())
}
buf := bytes.NewBuffer([]byte{})
if err := tpl.Execute(buf, values); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func readChangelog() string {
func readChangelog() (string, error) {
if _, err := os.Stat(cfg.ChangelogFile); err != nil {
log.Warn("Changelog file does not yet exist, creating one")
return ""
if errors.Is(err, fs.ErrNotExist) {
logrus.Warn("changelog file does not yet exist, creating one")
return "", nil
}
return "", fmt.Errorf("getting file stat: %w", err)
}
d, err := ioutil.ReadFile(cfg.ChangelogFile)
d, err := os.ReadFile(cfg.ChangelogFile)
if err != nil {
log.WithError(err).Fatal("Unable to read old changelog")
return "", fmt.Errorf("reading file: %w", err)
}
return string(d)
return string(d), nil
}
func renderLog(newVersion *semVer, logs []commit) (*semVer, error) {
c, err := quickTemplate("log_template", MustAsset("assets/log_template.md"), map[string]interface{}{
"NextVersion": newVersion,
"Now": time.Now(),
"LogLines": logs,
"OldLog": readChangelog(),
func renderLogAndGetVersion(newVersion *semVer, logs []commit) (*semVer, error) {
oldLog, err := readChangelog()
if err != nil {
return nil, fmt.Errorf("reading old changelog: %w", err)
}
c, err := renderTemplate("log_template", mustAsset("assets/log_template.md"), struct {
NextVersion *semVer
Now time.Time
LogLines []commit
OldLog string
}{
NextVersion: newVersion,
Now: time.Now(),
LogLines: logs,
OldLog: oldLog,
})
if err != nil {
return nil, errors.Wrap(err, "Unable to compile log")
return nil, fmt.Errorf("rendering log: %w", err)
}
if err = ioutil.WriteFile(cfg.ChangelogFile, bytes.TrimSpace(c), 0644); err != nil {
return nil, errors.Wrap(err, "Unable to write new changelog")
// Strip whitespaces on start / end
c = bytes.TrimSpace(c)
if err = os.WriteFile(cfg.ChangelogFile, c, fileModeChangelog); err != nil {
return nil, fmt.Errorf("writing changelog: %w", err)
}
// Spawning editor
if !cfg.NoEdit {
editor := exec.Command(os.Getenv("EDITOR"), cfg.ChangelogFile)
editor := exec.Command(os.Getenv("EDITOR"), cfg.ChangelogFile) //#nosec:G204 // This is intended to use OS editor with configured changelog file
editor.Stdin = os.Stdin
editor.Stdout = os.Stdout
editor.Stderr = os.Stderr
if err = editor.Run(); err != nil {
return nil, errors.New("Editor ended with non-zero status, stopping here")
return nil, fmt.Errorf("editor process caused error: %w", err)
}
}
// Read back version from changelog file
changelog := strings.Split(readChangelog(), "\n")
changelog := strings.Split(string(c), "\n")
if len(changelog) < 1 {
return nil, errors.New("Changelog is empty, no way to read back the version")
return nil, errors.New("changelog is empty, no way to read back the version")
}
newVersion, err = parseSemVer(strings.Split(changelog[0], " ")[1])
if err != nil {
return nil, errors.Wrap(err, "Unable to parse new version from log")
return nil, fmt.Errorf("parsing new version from log: %w", err)
}
return newVersion, nil
}
func renderTemplate(name string, tplSrc []byte, values any) ([]byte, error) {
tpl, err := template.New(name).Parse(string(tplSrc))
if err != nil {
return nil, fmt.Errorf("parsing template: %w", err)
}
buf := new(bytes.Buffer)
if err := tpl.Execute(buf, values); err != nil {
return nil, fmt.Errorf("executing template: %w", err)
}
return buf.Bytes(), nil
}

View file

@ -2,8 +2,9 @@ package main
import (
"errors"
"strconv"
"strings"
"fmt"
"github.com/Masterminds/semver/v3"
)
type semVerBump uint
@ -16,78 +17,52 @@ const (
)
type semVer struct {
Major, Minor, Patch int
PreReleaseInformation string
MetaData string
*semver.Version
}
func (s *semVer) String() string {
v := []string{strings.Join([]string{
strconv.Itoa(s.Major),
strconv.Itoa(s.Minor),
strconv.Itoa(s.Patch),
}, ".")}
if s.PreReleaseInformation != "" {
v = append(v, "-"+s.PreReleaseInformation)
}
if s.MetaData != "" {
v = append(v, "+"+s.MetaData)
func (s *semVer) SetMetadata(metadata string) error {
nv, err := s.Version.SetMetadata(metadata)
if err != nil {
return fmt.Errorf("setting metadata: %w", err)
}
return strings.Join(v, "")
s.Version = &nv
return nil
}
func (s *semVer) SetPrerelease(prerelease string) error {
nv, err := s.Version.SetPrerelease(prerelease)
if err != nil {
return fmt.Errorf("setting prerelease: %w", err)
}
s.Version = &nv
return nil
}
func parseSemVer(version string) (*semVer, error) {
var (
s semVer
err error
)
version = strings.TrimLeft(version, "v") // Ensure the version is not prefixed like v0.1.0
t := strings.SplitN(version, "+", 2)
if len(t) == 2 {
s.MetaData = t[1]
}
t = strings.SplitN(t[0], "-", 2)
if len(t) == 2 {
s.PreReleaseInformation = t[1]
}
elements := strings.Split(t[0], ".")
if len(elements) != 3 {
return nil, errors.New("Version does not match semantic versioning format")
}
s.Major, err = strconv.Atoi(elements[0])
v, err := semver.NewVersion(version)
if err != nil {
return nil, err
return nil, fmt.Errorf("parsing semver: %w", err)
}
s.Minor, err = strconv.Atoi(elements[1])
if err != nil {
return nil, err
}
s.Patch, err = strconv.Atoi(elements[2])
if err != nil {
return nil, err
}
return &s, nil
return &semVer{v}, nil
}
func (s *semVer) Bump(bumpType semVerBump) {
var nv semver.Version
switch bumpType {
case semVerBumpPatch:
s.Patch++
nv = s.Version.IncPatch()
case semVerBumpMinor:
s.Patch = 0
s.Minor++
nv = s.Version.IncMinor()
case semVerBumpMajor:
s.Patch = 0
s.Minor = 0
s.Major++
nv = s.Version.IncMajor()
}
s.Version = &nv
}
func selectBumpType(logs []commit) (semVerBump, error) {
@ -101,7 +76,7 @@ func selectBumpType(logs []commit) (semVerBump, error) {
if bump == semVerBumpUndecided {
// Impossible to reach
return semVerBumpUndecided, errors.New("Could not decide for any bump type")
return semVerBumpUndecided, errors.New("could not decide for any bump type")
}
return bump, nil

View file

@ -1,31 +0,0 @@
package main
import (
"reflect"
"testing"
)
func TestSemVerParseValid(t *testing.T) {
tests := map[string]semVer{
"1.9.0": {Major: 1, Minor: 9, Patch: 0, PreReleaseInformation: "", MetaData: ""},
"4.9.0": {Major: 4, Minor: 9, Patch: 0, PreReleaseInformation: "", MetaData: ""},
"1068.6.0": {Major: 1068, Minor: 6, Patch: 0, PreReleaseInformation: "", MetaData: ""},
"1.0.0-alpha": {Major: 1, Minor: 0, Patch: 0, PreReleaseInformation: "alpha", MetaData: ""},
"1.0.0-alpha.1": {Major: 1, Minor: 0, Patch: 0, PreReleaseInformation: "alpha.1", MetaData: ""},
"1.0.0-0.3.7": {Major: 1, Minor: 0, Patch: 0, PreReleaseInformation: "0.3.7", MetaData: ""},
"1.0.0-x.7.z.92": {Major: 1, Minor: 0, Patch: 0, PreReleaseInformation: "x.7.z.92", MetaData: ""},
"1.0.0-alpha+001": {Major: 1, Minor: 0, Patch: 0, PreReleaseInformation: "alpha", MetaData: "001"},
"1.0.0+20130313144700": {Major: 1, Minor: 0, Patch: 0, PreReleaseInformation: "", MetaData: "20130313144700"},
"1.0.0-beta+exp.sha.5114f85": {Major: 1, Minor: 0, Patch: 0, PreReleaseInformation: "beta", MetaData: "exp.sha.5114f85"},
}
for version, exp := range tests {
s, e := parseSemVer(version)
if e != nil {
t.Errorf("Parse of version '%s' failed: %s", version, e)
}
if !reflect.DeepEqual(exp, *s) {
t.Errorf("Parse of version '%s' (%#v) did not match expectation: %#v", version, exp, s)
}
}
}