Ready for release.

This commit is contained in:
2021-05-09 02:47:33 +01:00
parent b760bbd8c0
commit e4eb72fd13
12 changed files with 908 additions and 203 deletions
+187
View File
@@ -0,0 +1,187 @@
/*
Copyright © 2021 LUKASZ RACZYLO <lukasz$raczylo,com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"fmt"
"net/url"
"os"
"sort"
"strings"
"time"
git "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/object"
"github.com/lithammer/fuzzysearch/fuzzy"
"github.com/lukaszraczylo/zero"
"github.com/spf13/viper"
)
var (
err error
repo *Setup
)
type Wording struct {
Patch []string
Minor []string
Major []string
}
type Force struct {
Patch int
Minor int
Major int
}
type SemVer struct {
Patch int
Minor int
Major int
}
type Setup struct {
RepositoryName string
RepositoryLocalPath string
RepositoryHandler *git.Repository
Commits []CommitDetails
Semver SemVer
Wording Wording
Force Force
Generate bool
LocalConfigFile string
}
type CommitDetails struct {
Hash string
Author string
Message string
Timestamp time.Time
}
func checkMatches(content []string, targets []string) bool {
var r []string
for _, tgt := range targets {
r = fuzzy.FindFold(tgt, content)
}
return (len(r) > 0)
}
func (s *Setup) CalculateSemver() SemVer {
for _, commit := range s.Commits {
s.Semver.Patch++
commitSlice := strings.Split(commit.Message, " ")
matchPatch := checkMatches(commitSlice, s.Wording.Patch)
matchMinor := checkMatches(commitSlice, s.Wording.Minor)
matchMajor := checkMatches(commitSlice, s.Wording.Major)
if matchPatch {
s.Semver.Patch++
// fmt.Println("Patch version bumped:", commit.Message)
}
if matchMinor {
s.Semver.Minor++
s.Semver.Patch = 1
// fmt.Println("Minor version bumped:", commit.Message)
}
if matchMajor {
s.Semver.Major++
s.Semver.Minor = 0
s.Semver.Patch = 1
// fmt.Println("Major version bumped:", commit.Message)
}
}
return s.Semver
}
func (s *Setup) ListCommits() ([]CommitDetails, error) {
ref, err := s.RepositoryHandler.Head()
if err != nil {
return []CommitDetails{}, err
}
commitsList, err := s.RepositoryHandler.Log(&git.LogOptions{From: ref.Hash(), Order: git.LogOrderBSF})
if err != nil {
return []CommitDetails{}, err
}
commitsList.ForEach(func(c *object.Commit) error {
s.Commits = append(s.Commits, CommitDetails{Hash: c.Hash.String(), Author: c.Author.String(), Message: c.Message, Timestamp: c.Author.When})
sort.Slice(s.Commits, func(i, j int) bool { return s.Commits[i].Timestamp.Unix() < s.Commits[j].Timestamp.Unix() })
return nil
})
return s.Commits, err
}
func (s *Setup) Prepare() error {
u, _ := url.Parse(s.RepositoryName)
s.RepositoryLocalPath = fmt.Sprintf("/tmp/foo/%s", u.Path)
os.RemoveAll(s.RepositoryLocalPath)
s.RepositoryHandler, err = git.PlainClone(s.RepositoryLocalPath, false, &git.CloneOptions{
URL: s.RepositoryName,
})
if err != nil {
fmt.Println("Unable to reach repository", err.Error())
}
return err
}
func (s *Setup) ForcedVersioning() {
if !zero.IsZero(s.Force.Major) {
s.Semver.Major = s.Force.Major
}
if !zero.IsZero(s.Force.Minor) {
s.Semver.Minor = s.Force.Minor
}
if !zero.IsZero(s.Force.Patch) {
s.Semver.Patch = s.Force.Patch
}
}
func (s *Setup) ReadConfig(file string) error {
viper.SetConfigFile(file)
err := viper.ReadInConfig()
if err != nil {
err = fmt.Errorf("Fatal error config file: %s \n", err)
return err
}
viper.UnmarshalKey("wording", &s.Wording)
viper.UnmarshalKey("force", &s.Force)
return err
}
func (s *Setup) getSemver() string {
return fmt.Sprintf("%d.%d.%d", s.Semver.Major, s.Semver.Minor, s.Semver.Patch)
}
func (s *Setup) parseFlags() {
}
func main() {
if repo.Generate {
err := repo.ReadConfig(repo.LocalConfigFile)
if err != nil {
panic(err)
}
err = repo.Prepare()
if err != nil {
panic(err)
}
repo.ListCommits()
repo.ForcedVersioning()
repo.CalculateSemver()
fmt.Println("SEMVER", repo.getSemver())
}
}
+358
View File
@@ -0,0 +1,358 @@
/*
Copyright © 2021 LUKASZ RACZYLO <lukasz$raczylo,com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"os"
"strings"
"testing"
git "github.com/go-git/go-git/v5"
"github.com/lukaszraczylo/zero"
assertions "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type Tests struct {
suite.Suite
}
var (
assert *assertions.Assertions
)
func (suite *Tests) SetupTest() {
assert = assertions.New(suite.T())
}
func TestSuite(t *testing.T) {
suite.Run(t, new(Tests))
}
func (suite *Tests) TestSetup_getSemver() {
type fields struct {
Semver SemVer
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "Return 1.3.7",
fields: fields{
Semver: SemVer{
Major: 1,
Minor: 3,
Patch: 7,
},
},
want: "1.3.7",
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
s := &Setup{
Semver: tt.fields.Semver,
}
got := s.getSemver()
assert.Equal(tt.want, got, "Unexpected result in "+tt.name)
})
}
}
func (suite *Tests) TestSetup_ForcedVersioning() {
type fields struct {
Semver SemVer
Force Force
}
tests := []struct {
name string
fields fields
want string
}{
{
name: "No versioning",
want: "0.0.0",
},
{
name: "Major version set",
fields: fields{
Force: Force{
Major: 2,
},
},
want: "2.0.0",
},
{
name: "Minor version set",
fields: fields{
Force: Force{
Minor: 3,
},
},
want: "0.3.0",
},
{
name: "Patch version set",
fields: fields{
Force: Force{
Patch: 7,
},
},
want: "0.0.7",
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
s := &Setup{
Semver: tt.fields.Semver,
Force: tt.fields.Force,
}
s.ForcedVersioning()
got := s.getSemver()
assert.Equal(tt.want, got, "Unexpected result in "+tt.name)
})
}
}
func (suite *Tests) TestSetup_Prepare() {
type fields struct {
RepositoryName string
RepositoryLocalPath string
}
tests := []struct {
name string
fields fields
wantErr bool
}{
{
name: "Test repository lukaszraczylo/simple-gql-client",
fields: fields{
RepositoryName: "https://github.com/lukaszraczylo/simple-gql-client",
},
wantErr: true,
},
{
name: "Test non-existing repository",
fields: fields{
RepositoryName: "https://github.com/lukaszraczylo/simple-gql-client-dead",
},
wantErr: true,
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
s := &Setup{
RepositoryName: tt.fields.RepositoryName,
}
s.Prepare()
if _, err := os.Stat(s.RepositoryLocalPath); os.IsNotExist(err) {
if !tt.wantErr {
assert.NoError(err, "Error should not be present in "+tt.name)
} else {
assert.Error(err, "Error should be present in "+tt.name)
}
}
})
}
}
func (suite *Tests) TestSetup_ReadConfig() {
type fields struct {
Wording Wording
Force Force
}
type args struct {
file string
}
tests := []struct {
name string
fields fields
args args
wordingEmpty bool
wantErr bool
}{
{
name: "Test non-existent config file",
args: args{
file: "random-file-name.yaml",
},
wordingEmpty: true,
wantErr: true,
},
{
name: "Test existing config file",
args: args{
file: "../config.yaml",
},
wordingEmpty: false,
wantErr: false,
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
s := &Setup{}
err := s.ReadConfig(tt.args.file)
if !tt.wantErr {
assert.NoError(err, "Error should not be present in "+tt.name)
} else {
assert.Error(err, "Error should be present in "+tt.name)
}
assert.Equal(tt.wordingEmpty, zero.IsZero(s.Wording), "Unexpected wording count "+tt.name+":", s.Wording)
})
}
}
func (suite *Tests) Test_checkMatches() {
type args struct {
content []string
targets []string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "No match",
args: args{
content: strings.Fields("Fields splits the string s around each instance of one or more consecutive white space characters, as defined by unicode.IsSpace, returning a slice of substrings of s or an empty slice if s contains only white space"),
targets: []string{"github", "repository", "test"},
},
want: false,
},
{
name: "Match",
args: args{
content: strings.Fields("Fields splits the string s around each instance of one or more consecutive white space characters, as defined by unicode.IsSpace, returning a slice of substrings of s or an empty slice if s contains only white space"),
targets: []string{"github", "repository", "instance"},
},
want: true,
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
got := checkMatches(tt.args.content, tt.args.targets)
assert.Equal(tt.want, got, "Unexpected result in "+tt.name)
})
}
}
func (suite *Tests) TestSetup_ListCommits() {
type fields struct {
RepositoryName string
RepositoryLocalPath string
RepositoryHandler *git.Repository
Commits []CommitDetails
Semver SemVer
Wording Wording
Force Force
}
tests := []struct {
name string
fields fields
noCommits bool
wantErr bool
}{
{
name: "List commits from existing repository",
fields: fields{
RepositoryName: "https://github.com/lukaszraczylo/simple-gql-client",
},
noCommits: false,
wantErr: false,
},
{
name: "List commits from non-existing repository",
fields: fields{
RepositoryName: "https://github.com/lukaszraczylo/simple-gql-client-dead",
},
noCommits: true,
wantErr: true,
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
s := &Setup{
RepositoryName: tt.fields.RepositoryName,
}
s.Prepare()
listOfCommits, err := s.ListCommits()
if !tt.wantErr {
assert.NoError(err, "Error should not be present in "+tt.name)
} else {
assert.Error(err, "Error should be present in "+tt.name)
}
assert.Equal(tt.noCommits, zero.IsZero(listOfCommits), "Unexpected commits count"+tt.name)
})
}
}
func (suite *Tests) TestSetup_CalculateSemver() {
type fields struct {
RepositoryName string
}
type wantSemver struct {
Major int
Minor int
Patch int
}
tests := []struct {
name string
fields fields
wantSemver wantSemver
}{
{
name: "Test on existing repository",
fields: fields{
RepositoryName: "https://github.com/lukaszraczylo/simple-gql-client",
},
wantSemver: wantSemver{
Major: 5,
Minor: 1,
Patch: 1,
},
},
{
name: "Test on non-existing repository",
fields: fields{
RepositoryName: "https://github.com/lukaszraczylo/simple-gql-client-dead",
},
wantSemver: wantSemver{
Major: 1, // 1 because config file enforces MAJOR version
Minor: 0,
Patch: 0,
},
},
}
for _, tt := range tests {
suite.T().Run(tt.name, func(t *testing.T) {
s := &Setup{
RepositoryName: tt.fields.RepositoryName,
}
s.ReadConfig("../config.yaml")
s.Prepare()
s.ListCommits()
s.ForcedVersioning()
semver := s.CalculateSemver()
assert.Equal(tt.wantSemver.Major, semver.Major, "Unexpected MAJOR semver result in "+tt.name)
assert.Equal(tt.wantSemver.Minor, semver.Minor, "Unexpected MINOR semver result in "+tt.name)
assert.Equal(tt.wantSemver.Patch, semver.Patch, "Unexpected PATCH semver result in "+tt.name)
})
}
}
+50
View File
@@ -0,0 +1,50 @@
/*
Copyright © 2021 LUKASZ RACZYLO <lukasz$raczylo,com>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cmd
import (
"os"
"strings"
"github.com/spf13/cobra"
)
var cfgFile string
// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "semver-gen",
Short: "An effortless semantic version generator",
Long: `semver-gen // Lukasz Raczylo, raczylo.com
Effortless semantic version generator with git commit keywords matching, allowing you to focus on the development.
Visit https://github.com/lukaszraczylo/semver-generator for more information, documentation and examples.`,
Run: func(cmd *cobra.Command, args []string) {},
}
func Execute() {
cobra.CheckErr(rootCmd.Execute())
}
func init() {
repo = &Setup{}
if !strings.HasSuffix(os.Args[0], ".test") {
rootCmd.PersistentFlags().StringVarP(&repo.RepositoryName, "repository", "r", "https://github.com/lukaszraczylo/simple-gql-client", "Repository URL. If not specified local dir will be used.")
rootCmd.PersistentFlags().StringVarP(&repo.LocalConfigFile, "config", "c", "config.yaml", "Path to config file")
rootCmd.PersistentFlags().BoolVarP(&repo.Generate, "generate", "g", true, "Generate semantic version")
main()
}
}