mirror of
https://github.com/Luzifer/git-changerelease.git
synced 2024-12-20 11:01:16 +00:00
Modernize code, update deps, fix linter errors
Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
parent
199a1b4984
commit
810e271c3b
9 changed files with 335 additions and 632 deletions
4
Makefile
4
Makefile
|
@ -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/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
|
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:
|
test:
|
||||||
go test .
|
go test .
|
||||||
go vet .
|
go vet .
|
||||||
|
|
322
assets.go
322
assets.go
|
@ -1,322 +1,26 @@
|
||||||
// Code generated by go-bindata. DO NOT EDIT.
|
|
||||||
// sources:
|
|
||||||
// assets/git_changerelease.yaml
|
|
||||||
// assets/log_template.md
|
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"embed"
|
||||||
"compress/gzip"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func bindataRead(data []byte, name string) ([]byte, error) {
|
//go:embed assets/*
|
||||||
gz, err := gzip.NewReader(bytes.NewBuffer(data))
|
var assetFS embed.FS
|
||||||
|
|
||||||
|
func asset(name string) ([]byte, error) {
|
||||||
|
data, err := assetFS.ReadFile(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Read %q: %v", name, err)
|
return data, fmt.Errorf("reading asset: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
return data, nil
|
||||||
_, err = io.Copy(&buf, gz)
|
}
|
||||||
clErr := gz.Close()
|
|
||||||
|
|
||||||
|
func mustAsset(name string) []byte {
|
||||||
|
data, err := asset(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Read %q: %v", name, err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if clErr != nil {
|
return data
|
||||||
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, "/")...)...)
|
|
||||||
}
|
}
|
||||||
|
|
22
config.go
22
config.go
|
@ -2,8 +2,10 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
yaml "gopkg.in/yaml.v2"
|
yaml "gopkg.in/yaml.v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,19 +21,27 @@ func loadConfig() (*configFile, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if _, err = os.Stat(cfg.ConfigFile); err != nil {
|
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{}
|
c := &configFile{}
|
||||||
if err = yaml.Unmarshal(MustAsset("assets/git_changerelease.yaml"), c); err != nil {
|
if err = yaml.Unmarshal(mustAsset("assets/git_changerelease.yaml"), c); err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("unmarshalling default config: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dataFile, err := os.Open(cfg.ConfigFile)
|
dataFile, err := os.Open(cfg.ConfigFile)
|
||||||
if err != nil {
|
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
129
git.go
|
@ -3,8 +3,12 @@ package main
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,10 +34,110 @@ type commit struct {
|
||||||
BumpType semVerBump
|
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) {
|
func parseCommit(line string) (*commit, error) {
|
||||||
t := strings.Split(line, "\t")
|
t := strings.Split(line, "\t")
|
||||||
if len(t) != 4 {
|
if len(t) != len(gitLogFormatParts) {
|
||||||
return nil, errors.New("Unexpected line format")
|
return nil, errors.New("unexpected line format")
|
||||||
}
|
}
|
||||||
|
|
||||||
c := &commit{
|
c := &commit{
|
||||||
|
@ -55,24 +159,3 @@ func parseCommit(line string) (*commit, error) {
|
||||||
|
|
||||||
return c, nil
|
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
16
go.mod
|
@ -1,11 +1,17 @@
|
||||||
module github.com/Luzifer/git-changerelease
|
module github.com/Luzifer/git-changerelease
|
||||||
|
|
||||||
go 1.14
|
go 1.21
|
||||||
|
|
||||||
require (
|
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/mitchellh/go-homedir v1.1.0
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/sirupsen/logrus v1.6.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
gopkg.in/yaml.v2 v2.3.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
48
go.sum
|
@ -1,24 +1,34 @@
|
||||||
github.com/Luzifer/rconfig v1.2.0 h1:waD1sqasGVSQSrExpLrQ9Q1JmMaltrS391VdOjWXP/I=
|
github.com/Luzifer/rconfig/v2 v2.4.0 h1:MAdymTlExAZ8mx5VG8xOFAtFQSpWBipKYQHPOmYTn9o=
|
||||||
github.com/Luzifer/rconfig/v2 v2.2.1 h1:zcDdLQlnlzwcBJ8E0WFzOkQE1pCMn3EbX0dFYkeTczg=
|
github.com/Luzifer/rconfig/v2 v2.4.0/go.mod h1:hWF3ZVSusbYlg5bEvCwalEyUSY+0JPJWUiIu7rBmav8=
|
||||||
github.com/Luzifer/rconfig/v2 v2.2.1/go.mod h1:OKIX0/JRZrPJ/ZXXWklQEFXA6tBfWaljZbW37w+sqBw=
|
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/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/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
|
||||||
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/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 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
|
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc=
|
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
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/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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/validator.v2 v2.0.0-20180514200540-135c24b11c19/go.mod h1:o4V0GXN9/CAmCsvJ0oXYZvrZOe7syiDZSN1GWGZTGzc=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY=
|
||||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8=
|
||||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
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
304
main.go
|
@ -1,26 +1,28 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
//go:generate make generate
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
"github.com/pkg/errors"
|
"github.com/sirupsen/logrus"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/Luzifer/rconfig/v2"
|
"github.com/Luzifer/rconfig/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fileModeChangelog = 0o644
|
||||||
|
fileModeConfig = 0o600
|
||||||
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfg = struct {
|
cfg = struct {
|
||||||
ChangelogFile string `flag:"changelog" default:"History.md" description:"File to write the changelog to"`
|
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"`
|
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"`
|
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')"`
|
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 (e.g. 'exp.sha.5114f85' or '20130313144700')"`
|
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"`
|
VersionAndExit bool `flag:"version" default:"false" description:"Prints current version and exits"`
|
||||||
}{}
|
}{}
|
||||||
|
@ -39,91 +41,69 @@ var (
|
||||||
version = "dev"
|
version = "dev"
|
||||||
|
|
||||||
matchers = make(map[*regexp.Regexp]semVerBump)
|
matchers = make(map[*regexp.Regexp]semVerBump)
|
||||||
|
|
||||||
|
errExitZero = errors.New("should exit zero now")
|
||||||
)
|
)
|
||||||
|
|
||||||
func applyTag(stringVersion string) error {
|
func initApp() (err error) {
|
||||||
var err error
|
rconfig.AutoEnv(true)
|
||||||
if _, err = gitErr("add", cfg.ChangelogFile); err != nil {
|
if err = rconfig.Parse(&cfg); err != nil {
|
||||||
return errors.Wrap(err, "Unable to add changelog file")
|
return fmt.Errorf("parsing cli options: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
commitMessage, err := quickTemplate("commitMessage", []byte(config.ReleaseCommitMessage), map[string]interface{}{
|
if cfg.VersionAndExit {
|
||||||
"Version": stringVersion,
|
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 {
|
if err != nil {
|
||||||
return errors.Wrap(err, "Unable to compile commit message")
|
return fmt.Errorf("expanding file path: %w", err)
|
||||||
}
|
|
||||||
if _, err := gitErr("commit", "-m", string(commitMessage)); err != nil {
|
|
||||||
return errors.Wrap(err, "Unable to commit changelog")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tagType := "-s" // By default use signed tags
|
var l logrus.Level
|
||||||
if config.DiableTagSigning {
|
if l, err = logrus.ParseLevel(cfg.LogLevel); err != nil {
|
||||||
tagType = "-a" // If requested switch to annotated tags
|
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 {
|
if !cfg.NoEdit && os.Getenv("EDITOR") == "" {
|
||||||
return errors.Wrap(err, "Unable to tag release")
|
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
|
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 {
|
func loadMatcherRegex(matches []string, bump semVerBump) error {
|
||||||
for _, match := range matches {
|
for _, match := range matches {
|
||||||
r, err := regexp.Compile(match)
|
r, err := regexp.Compile(match)
|
||||||
if err != nil {
|
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
|
matchers[r] = bump
|
||||||
}
|
}
|
||||||
|
@ -132,7 +112,13 @@ func loadMatcherRegex(matches []string, bump semVerBump) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
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
|
// Get last tag
|
||||||
lastTag, err := gitSilent("describe", "--tags", "--abbrev=0", `--match=v[0-9]*\.[0-9]*\.[0-9]*`)
|
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)
|
logs, err := fetchGitLogs(lastTag, err != nil)
|
||||||
if 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 {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate new version
|
// Generate new version
|
||||||
newVersion, err := newVersionFromLogs(lastTag, logs)
|
newVersion, err := newVersionFromLogs(lastTag, logs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithError(err).Fatal("Was unable to bump version")
|
logrus.WithError(err).Fatal("bumping version")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render log
|
// Render log
|
||||||
if newVersion, err = renderLog(newVersion, logs); err != nil {
|
if newVersion, err = renderLogAndGetVersion(newVersion, logs); err != nil {
|
||||||
log.WithError(err).Fatal("Could not write changelog")
|
logrus.WithError(err).Fatal("writing changelog")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the tag
|
// Write the tag
|
||||||
if err = applyTag("v" + newVersion.String()); err != nil {
|
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
|
// Tetermine increase type
|
||||||
semVerBumpType, err := selectBumpType(logs)
|
semVerBumpType, err := selectBumpType(logs)
|
||||||
if err != nil {
|
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
|
// Generate new version
|
||||||
newVersion, err := parseSemVer(lastTag)
|
newVersion, err := parseSemVer(lastTag)
|
||||||
if err != nil {
|
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
|
return newVersion, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareRun() {
|
func readChangelog() (string, error) {
|
||||||
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 {
|
|
||||||
if _, err := os.Stat(cfg.ChangelogFile); err != nil {
|
if _, err := os.Stat(cfg.ChangelogFile); err != nil {
|
||||||
log.Warn("Changelog file does not yet exist, creating one")
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
return ""
|
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 {
|
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) {
|
func renderLogAndGetVersion(newVersion *semVer, logs []commit) (*semVer, error) {
|
||||||
c, err := quickTemplate("log_template", MustAsset("assets/log_template.md"), map[string]interface{}{
|
oldLog, err := readChangelog()
|
||||||
"NextVersion": newVersion,
|
if err != nil {
|
||||||
"Now": time.Now(),
|
return nil, fmt.Errorf("reading old changelog: %w", err)
|
||||||
"LogLines": logs,
|
}
|
||||||
"OldLog": readChangelog(),
|
|
||||||
|
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 {
|
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 {
|
// Strip whitespaces on start / end
|
||||||
return nil, errors.Wrap(err, "Unable to write new changelog")
|
c = bytes.TrimSpace(c)
|
||||||
|
|
||||||
|
if err = os.WriteFile(cfg.ChangelogFile, c, fileModeChangelog); err != nil {
|
||||||
|
return nil, fmt.Errorf("writing changelog: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spawning editor
|
// Spawning editor
|
||||||
if !cfg.NoEdit {
|
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.Stdin = os.Stdin
|
||||||
editor.Stdout = os.Stdout
|
editor.Stdout = os.Stdout
|
||||||
editor.Stderr = os.Stderr
|
editor.Stderr = os.Stderr
|
||||||
if err = editor.Run(); err != nil {
|
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
|
// Read back version from changelog file
|
||||||
changelog := strings.Split(readChangelog(), "\n")
|
changelog := strings.Split(string(c), "\n")
|
||||||
if len(changelog) < 1 {
|
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])
|
newVersion, err = parseSemVer(strings.Split(changelog[0], " ")[1])
|
||||||
if err != nil {
|
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
|
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
|
||||||
|
}
|
||||||
|
|
91
semver.go
91
semver.go
|
@ -2,8 +2,9 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"strconv"
|
"fmt"
|
||||||
"strings"
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type semVerBump uint
|
type semVerBump uint
|
||||||
|
@ -16,78 +17,52 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type semVer struct {
|
type semVer struct {
|
||||||
Major, Minor, Patch int
|
*semver.Version
|
||||||
PreReleaseInformation string
|
|
||||||
MetaData string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *semVer) String() string {
|
func (s *semVer) SetMetadata(metadata string) error {
|
||||||
v := []string{strings.Join([]string{
|
nv, err := s.Version.SetMetadata(metadata)
|
||||||
strconv.Itoa(s.Major),
|
if err != nil {
|
||||||
strconv.Itoa(s.Minor),
|
return fmt.Errorf("setting metadata: %w", err)
|
||||||
strconv.Itoa(s.Patch),
|
|
||||||
}, ".")}
|
|
||||||
|
|
||||||
if s.PreReleaseInformation != "" {
|
|
||||||
v = append(v, "-"+s.PreReleaseInformation)
|
|
||||||
}
|
|
||||||
if s.MetaData != "" {
|
|
||||||
v = append(v, "+"+s.MetaData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
func parseSemVer(version string) (*semVer, error) {
|
||||||
var (
|
v, err := semver.NewVersion(version)
|
||||||
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])
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("parsing semver: %w", err)
|
||||||
}
|
}
|
||||||
s.Minor, err = strconv.Atoi(elements[1])
|
return &semVer{v}, nil
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
s.Patch, err = strconv.Atoi(elements[2])
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &s, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *semVer) Bump(bumpType semVerBump) {
|
func (s *semVer) Bump(bumpType semVerBump) {
|
||||||
|
var nv semver.Version
|
||||||
|
|
||||||
switch bumpType {
|
switch bumpType {
|
||||||
case semVerBumpPatch:
|
case semVerBumpPatch:
|
||||||
s.Patch++
|
nv = s.Version.IncPatch()
|
||||||
|
|
||||||
case semVerBumpMinor:
|
case semVerBumpMinor:
|
||||||
s.Patch = 0
|
nv = s.Version.IncMinor()
|
||||||
s.Minor++
|
|
||||||
case semVerBumpMajor:
|
case semVerBumpMajor:
|
||||||
s.Patch = 0
|
nv = s.Version.IncMajor()
|
||||||
s.Minor = 0
|
|
||||||
s.Major++
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.Version = &nv
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectBumpType(logs []commit) (semVerBump, error) {
|
func selectBumpType(logs []commit) (semVerBump, error) {
|
||||||
|
@ -101,7 +76,7 @@ func selectBumpType(logs []commit) (semVerBump, error) {
|
||||||
|
|
||||||
if bump == semVerBumpUndecided {
|
if bump == semVerBumpUndecided {
|
||||||
// Impossible to reach
|
// 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
|
return bump, nil
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue