diff --git a/cmd/cz/cz.go b/cmd/cz/cz.go index d2d022e..4119463 100644 --- a/cmd/cz/cz.go +++ b/cmd/cz/cz.go @@ -3,10 +3,15 @@ package cz import ( "errors" "fmt" + "os" + "path/filepath" + "time" cliflag "github.com/shipengqi/component-base/cli/flag" "github.com/shipengqi/component-base/term" "github.com/shipengqi/golib/convutil" + "github.com/shipengqi/golib/fsutil" + "github.com/shipengqi/log" "github.com/spf13/cobra" "github.com/shipengqi/commitizen/internal/config" @@ -19,6 +24,26 @@ func New() *cobra.Command { c := &cobra.Command{ Use: "commitizen", Long: `Command line utility to standardize git commit messages.`, + PreRun: func(_ *cobra.Command, _ []string) { + if !o.Debug { + return + } + opts := &log.Options{ + DisableRotate: true, + DisableFileCaller: true, + DisableConsoleCaller: true, + DisableConsoleLevel: true, + DisableConsoleTime: true, + Output: filepath.Join(os.TempDir(), "commitizen/logs"), + FileLevel: log.DebugLevel.String(), + FilenameEncoder: filenameEncoder, + } + err := fsutil.MkDirAll(opts.Output) + if err != nil { + panic(err) + } + log.Configure(opts) + }, RunE: func(_ *cobra.Command, _ []string) error { isRepo, err := git.IsGitRepo() if err != nil { @@ -80,3 +105,7 @@ func New() *cobra.Command { return c } + +func filenameEncoder() string { + return fmt.Sprintf("%s.%s.log", filepath.Base(os.Args[0]), time.Now().Format("20060102150405")) +} diff --git a/go.mod b/go.mod index 8f05faa..b8ff28e 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/onsi/gomega v1.33.0 github.com/shipengqi/component-base v0.2.9 github.com/shipengqi/golib v0.2.12 + github.com/shipengqi/log v0.2.2 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 @@ -32,7 +33,6 @@ require ( github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -45,10 +45,13 @@ require ( github.com/muesli/termenv v0.15.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + go.uber.org/multierr v1.10.0 // indirect + go.uber.org/zap v1.27.0 // indirect golang.org/x/net v0.23.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/term v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect golang.org/x/tools v0.17.0 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect ) diff --git a/go.sum b/go.sum index 1fdecfe..a19d404 100644 --- a/go.sum +++ b/go.sum @@ -43,6 +43,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -84,14 +86,30 @@ github.com/shipengqi/component-base v0.2.9 h1:4XRB6PzTRgqKkkxkJwnpK8YOqDHRzXviyy github.com/shipengqi/component-base v0.2.9/go.mod h1:LfbMJtgUW7nNPwmVIi5wJMif/066edkcIJtkDDJgEQQ= github.com/shipengqi/golib v0.2.12 h1:/0hrev7+J8KChxEvoVdS2kbGQT8VO4C4qFAhtn6ZI8o= github.com/shipengqi/golib v0.2.12/go.mod h1:PIezev9VXxmhjawpu3j1JgLSNKLMq5AB8gLouJ83mrw= +github.com/shipengqi/log v0.2.2 h1:+JvLIb3Xycl3/XJFVZn+ZzbJF7HeUBhdNvOdUoFHHS0= +github.com/shipengqi/log v0.2.2/go.mod h1:YqXfNjg7aDR/KrXoU5KC3vCQ/YldJltQbyEwnlpJOb4= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= +go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= @@ -114,6 +132,8 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/config/config.go b/internal/config/config.go index f939ea0..67a227a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -19,7 +19,7 @@ import ( ) const ( - RCFilename = ".czrc" + RCFilename = ".ggczrc" ReservedDefaultName = "default" FieldKeyTemplateSelect = "template-select" ) diff --git a/internal/helpers/contains.go b/internal/helpers/contains.go new file mode 100644 index 0000000..025e87b --- /dev/null +++ b/internal/helpers/contains.go @@ -0,0 +1,79 @@ +package helpers + +import ( + "reflect" + "strings" +) + +// Contains asserts that the specified string, list(array, slice...) or map contains the +// specified substring or element. +// +// helpers.Contains("Hello World", "World") +// helpers.Contains(["Hello", "World"], "World") +// helpers.Contains({"Hello": "World"}, "Hello") +func Contains(s, contains interface{}) bool { + ok, found := containsElement(s, contains) + if !ok { + return false + } + + return found +} + +// NotContains asserts that the specified string, list(array, slice...) or map does NOT contain the +// specified substring or element. +// +// helpers.NotContains("Hello World", "Earth") +// helpers.NotContains(["Hello", "World"], "Earth") +// helpers.NotContains({"Hello": "World"}, "Earth") +func NotContains(s, contains interface{}) bool { + ok, found := containsElement(s, contains) + if !ok { + return false + } + + return !found +} + +// containsElement try loop over the list check if the list includes the element. +// return (false, false) if impossible. +// return (true, false) if element was not found. +// return (true, true) if element was found. +func containsElement(list interface{}, element interface{}) (ok, found bool) { + + listValue := reflect.ValueOf(list) + listType := reflect.TypeOf(list) + if listType == nil { + return false, false + } + listKind := listType.Kind() + defer func() { + if e := recover(); e != nil { + ok = false + found = false + } + }() + + if listKind == reflect.String { + elementValue := reflect.ValueOf(element) + return true, strings.Contains(listValue.String(), elementValue.String()) + } + + if listKind == reflect.Map { + mapKeys := listValue.MapKeys() + for i := 0; i < len(mapKeys); i++ { + if ObjectsAreEqual(mapKeys[i].Interface(), element) { + return true, true + } + } + return true, false + } + + for i := 0; i < listValue.Len(); i++ { + if ObjectsAreEqual(listValue.Index(i).Interface(), element) { + return true, true + } + } + return true, false + +} diff --git a/internal/helpers/contains_test.go b/internal/helpers/contains_test.go new file mode 100644 index 0000000..6037b17 --- /dev/null +++ b/internal/helpers/contains_test.go @@ -0,0 +1,68 @@ +package helpers + +import ( + "fmt" + "testing" +) + +func TestContainsNotContains(t *testing.T) { + + type A struct { + Name, Value string + } + list := []string{"Foo", "Bar"} + + complexList := []*A{ + {"b", "c"}, + {"d", "e"}, + {"g", "h"}, + {"j", "k"}, + } + simpleMap := map[interface{}]interface{}{"Foo": "Bar"} + var zeroMap map[interface{}]interface{} + + cases := []struct { + expected interface{} + actual interface{} + result bool + }{ + {"Hello World", "Hello", true}, + {"Hello World", "Salut", false}, + {list, "Bar", true}, + {list, "Salut", false}, + {complexList, &A{"g", "h"}, true}, + {complexList, &A{"g", "e"}, false}, + {simpleMap, "Foo", true}, + {simpleMap, "Bar", false}, + {zeroMap, "Bar", false}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("Contains(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := Contains(c.expected, c.actual) + + if res != c.result { + if res { + t.Errorf("Contains(%#v, %#v) should return true:\n\t%#v contains %#v", c.expected, c.actual, c.expected, c.actual) + } else { + t.Errorf("Contains(%#v, %#v) should return false:\n\t%#v does not contain %#v", c.expected, c.actual, c.expected, c.actual) + } + } + }) + } + + for _, c := range cases { + t.Run(fmt.Sprintf("NotContains(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := NotContains(c.expected, c.actual) + + // NotContains should be inverse of Contains. If it's not, something is wrong + if res == Contains(c.expected, c.actual) { + if res { + t.Errorf("NotContains(%#v, %#v) should return true:\n\t%#v does not contains %#v", c.expected, c.actual, c.expected, c.actual) + } else { + t.Errorf("NotContains(%#v, %#v) should return false:\n\t%#v contains %#v", c.expected, c.actual, c.expected, c.actual) + } + } + }) + } +} diff --git a/internal/helpers/empty.go b/internal/helpers/empty.go new file mode 100644 index 0000000..2c56686 --- /dev/null +++ b/internal/helpers/empty.go @@ -0,0 +1,50 @@ +package helpers + +import "reflect" + +// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// helpers.Empty(obj) +func Empty(object interface{}) bool { + return isEmpty(object) +} + +// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either +// a slice or a channel with len == 0. +// +// if helpers.NotEmpty(obj) { +// helpers.Equal("two", obj[1]) +// } +func NotEmpty(object interface{}) bool { + return !isEmpty(object) +} + +// isEmpty gets whether the specified object is considered empty or not. +func isEmpty(object interface{}) bool { + + // get nil case out of the way + if object == nil { + return true + } + + objValue := reflect.ValueOf(object) + + switch objValue.Kind() { + // collection types are empty when they have no element + case reflect.Chan, reflect.Map, reflect.Slice: + return objValue.Len() == 0 + // pointers are empty if nil or if the value they point to is empty + case reflect.Ptr: + if objValue.IsNil() { + return true + } + deref := objValue.Elem().Interface() + return isEmpty(deref) + // for all other types, compare against the zero value + // array types are empty when they match their zero-initialized state + default: + zero := reflect.Zero(objValue.Type()) + return reflect.DeepEqual(object, zero.Interface()) + } +} diff --git a/internal/helpers/empty_test.go b/internal/helpers/empty_test.go new file mode 100644 index 0000000..b42c422 --- /dev/null +++ b/internal/helpers/empty_test.go @@ -0,0 +1,75 @@ +package helpers + +import ( + "errors" + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestEmpty(t *testing.T) { + + chWithValue := make(chan struct{}, 1) + chWithValue <- struct{}{} + var tiP *time.Time + var tiNP time.Time + var s *string + var f *os.File + sP := &s + x := 1 + xP := &x + + type TString string + type TStruct struct { + x int + } + + assert.True(t, Empty(""), "Empty string is empty") + assert.True(t, Empty(nil), "Nil is empty") + assert.True(t, Empty([]string{}), "Empty string array is empty") + assert.True(t, Empty(0), "Zero int value is empty") + assert.True(t, Empty(false), "False value is empty") + assert.True(t, Empty(make(chan struct{})), "Channel without values is empty") + assert.True(t, Empty(s), "Nil string pointer is empty") + assert.True(t, Empty(f), "Nil os.File pointer is empty") + assert.True(t, Empty(tiP), "Nil time.Time pointer is empty") + assert.True(t, Empty(tiNP), "time.Time is empty") + assert.True(t, Empty(TStruct{}), "struct with zero values is empty") + assert.True(t, Empty(TString("")), "empty aliased string is empty") + assert.True(t, Empty(sP), "ptr to nil value is empty") + assert.True(t, Empty([1]int{}), "array is state") + + assert.False(t, Empty("something"), "Non Empty string is not empty") + assert.False(t, Empty(errors.New("something")), "Non nil object is not empty") + assert.False(t, Empty([]string{"something"}), "Non empty string array is not empty") + assert.False(t, Empty(1), "Non-zero int value is not empty") + assert.False(t, Empty(true), "True value is not empty") + assert.False(t, Empty(chWithValue), "Channel with values is not empty") + assert.False(t, Empty(TStruct{x: 1}), "struct with initialized values is empty") + assert.False(t, Empty(TString("abc")), "non-empty aliased string is empty") + assert.False(t, Empty(xP), "ptr to non-nil value is not empty") + assert.False(t, Empty([1]int{42}), "array is not state") +} + +func TestNotEmpty(t *testing.T) { + chWithValue := make(chan struct{}, 1) + chWithValue <- struct{}{} + + assert.False(t, NotEmpty(""), "Empty string is empty") + assert.False(t, NotEmpty(nil), "Nil is empty") + assert.False(t, NotEmpty([]string{}), "Empty string array is empty") + assert.False(t, NotEmpty(0), "Zero int value is empty") + assert.False(t, NotEmpty(false), "False value is empty") + assert.False(t, NotEmpty(make(chan struct{})), "Channel without values is empty") + assert.False(t, NotEmpty([1]int{}), "array is state") + + assert.True(t, NotEmpty("something"), "Non Empty string is not empty") + assert.True(t, NotEmpty(errors.New("something")), "Non nil object is not empty") + assert.True(t, NotEmpty([]string{"something"}), "Non empty string array is not empty") + assert.True(t, NotEmpty(1), "Non-zero int value is not empty") + assert.True(t, NotEmpty(true), "True value is not empty") + assert.True(t, NotEmpty(chWithValue), "Channel with values is not empty") + assert.True(t, NotEmpty([1]int{42}), "array is not state") +} diff --git a/internal/helpers/equal.go b/internal/helpers/equal.go new file mode 100644 index 0000000..28cde77 --- /dev/null +++ b/internal/helpers/equal.go @@ -0,0 +1,80 @@ +package helpers + +import ( + "bytes" + "errors" + "reflect" +) + +// Equal asserts that two objects are equal. +// +// helpers.Equal(123, 123) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). Function equality +// cannot be determined and will always fail. +func Equal(expected, actual interface{}) bool { + if err := validateEqualArgs(expected, actual); err != nil { + // invalid operation + return false + } + return ObjectsAreEqual(expected, actual) +} + +// NotEqual asserts that the specified values are NOT equal. +// +// helpers.NotEqual(obj1, obj2) +// +// Pointer variable equality is determined based on the equality of the +// referenced values (as opposed to the memory addresses). +func NotEqual(expected, actual interface{}) bool { + if err := validateEqualArgs(expected, actual); err != nil { + // invalid operation + return false + } + + return !ObjectsAreEqual(expected, actual) +} + +// ObjectsAreEqual determines if two objects are considered equal. +// +// This function does no assertion of any kind. +func ObjectsAreEqual(expected, actual interface{}) bool { + if expected == nil || actual == nil { + return expected == actual + } + + exp, ok := expected.([]byte) + if !ok { + return reflect.DeepEqual(expected, actual) + } + + act, ok := actual.([]byte) + if !ok { + return false + } + if exp == nil || act == nil { + return exp == nil && act == nil + } + return bytes.Equal(exp, act) +} + +// validateEqualArgs checks whether provided arguments can be safely used in the +// Equal/NotEqual functions. +func validateEqualArgs(expected, actual interface{}) error { + if expected == nil && actual == nil { + return nil + } + + if isFunction(expected) || isFunction(actual) { + return errors.New("cannot take func type as argument") + } + return nil +} + +func isFunction(arg interface{}) bool { + if arg == nil { + return false + } + return reflect.TypeOf(arg).Kind() == reflect.Func +} diff --git a/internal/helpers/equal_test.go b/internal/helpers/equal_test.go new file mode 100644 index 0000000..ad8c3da --- /dev/null +++ b/internal/helpers/equal_test.go @@ -0,0 +1,92 @@ +package helpers + +import ( + "fmt" + "testing" +) + +func TestEqual(t *testing.T) { + type myType string + + var m map[string]interface{} + + cases := []struct { + expected interface{} + actual interface{} + result bool + remark string + }{ + {"Hello World", "Hello World", true, ""}, + {123, 123, true, ""}, + {123.5, 123.5, true, ""}, + {[]byte("Hello World"), []byte("Hello World"), true, ""}, + {nil, nil, true, ""}, + {int32(123), int32(123), true, ""}, + {uint64(123), uint64(123), true, ""}, + {myType("1"), myType("1"), true, ""}, + {&struct{}{}, &struct{}{}, true, "pointer equality is based on equality of underlying value"}, + {[]string{"str1", "str2"}, []string{"str1", "str2"}, true, ""}, + {[]int{1, 2}, []int{1, 2}, true, ""}, + {true, true, true, ""}, + + // Not expected to be equal + {m["bar"], "something", false, ""}, + {myType("1"), myType("2"), false, ""}, + + // A case that might be confusing, especially with numeric literals + {10, uint(10), false, ""}, + + {[]string{"str1", "str2"}, []string{"str1", "str3"}, false, ""}, + {[]int{1, 2}, []int{1, 3}, false, ""}, + {true, false, false, ""}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("Equal(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := Equal(c.expected, c.actual) + + if res != c.result { + t.Errorf("Equal(%#v, %#v) should return %#v: %s", c.expected, c.actual, c.result, c.remark) + } + }) + } +} + +func TestNotEqual(t *testing.T) { + type myStructType struct{} + + cases := []struct { + expected interface{} + actual interface{} + result bool + }{ + // cases that are expected not to match + {"Hello World", "Hello World!", true}, + {123, 1234, true}, + {123.5, 123.55, true}, + {[]byte("Hello World"), []byte("Hello World!"), true}, + {nil, new(myStructType), true}, + + // cases that are expected to match + {nil, nil, false}, + {"Hello World", "Hello World", false}, + {123, 123, false}, + {123.5, 123.5, false}, + {[]byte("Hello World"), []byte("Hello World"), false}, + {new(myStructType), new(myStructType), false}, + {&struct{}{}, &struct{}{}, false}, + {func() int { return 23 }, func() int { return 24 }, false}, + // A case that might be confusing, especially with numeric literals + {int(10), uint(10), true}, + } + + for _, c := range cases { + t.Run(fmt.Sprintf("NotEqual(%#v, %#v)", c.expected, c.actual), func(t *testing.T) { + res := NotEqual(c.expected, c.actual) + + if res != c.result { + t.Errorf("NotEqual(%#v, %#v) should return %#v", c.expected, c.actual, c.result) + } + }) + } +} diff --git a/internal/helpers/helper.go b/internal/helpers/helper.go new file mode 100644 index 0000000..589b2f5 --- /dev/null +++ b/internal/helpers/helper.go @@ -0,0 +1,21 @@ +package helpers + +import "github.com/shipengqi/commitizen/internal/errors" + +func GetValueFromYAML[T any](data map[string]interface{}, key string) (T, error) { + var ( + res T + ok bool + v interface{} + ) + + v, ok = data[key] + if !ok { + return res, errors.NewMissingErr(key) + } + res, ok = v.(T) + if !ok { + return res, errors.ErrType + } + return res, nil +} diff --git a/internal/options/options.go b/internal/options/options.go index 00db72f..734df43 100644 --- a/internal/options/options.go +++ b/internal/options/options.go @@ -1,6 +1,10 @@ package options import ( + "fmt" + "os" + "path/filepath" + cliflag "github.com/shipengqi/component-base/cli/flag" "github.com/shipengqi/commitizen/internal/git" @@ -9,6 +13,7 @@ import ( type Options struct { DryRun bool Default bool + Debug bool Template string GitOptions *git.Options } @@ -21,12 +26,16 @@ func New() *Options { } func (o *Options) Flags() (fss cliflag.NamedFlagSets) { + dir := filepath.Join(os.TempDir(), "commitizen/logs") + o.GitOptions.AddFlags(fss.FlagSet("Git Commit")) fs := fss.FlagSet("Commitizen") fs.BoolVar(&o.DryRun, "dry-run", o.DryRun, "do not create a commit, but show the message and list of paths \nthat are to be committed.") fs.StringVarP(&o.Template, "template", "t", o.Template, "template name to use when multiple templates exist.") fs.BoolVarP(&o.Default, "default", "d", o.Default, "use the default template, '--default' has a higher priority than '--template'.") + fs.BoolVar(&o.Debug, "debug", o.Debug, fmt.Sprintf("enable debug mode, writing log file to the %s directory.", dir)) + _ = fs.MarkHidden("debug") return } diff --git a/internal/parameter/group.go b/internal/parameter/group.go new file mode 100644 index 0000000..13e4e82 --- /dev/null +++ b/internal/parameter/group.go @@ -0,0 +1,175 @@ +package parameter + +import ( + standarderrs "errors" + + "github.com/charmbracelet/huh" + "github.com/shipengqi/golib/strutil" + "github.com/shipengqi/log" + + "github.com/shipengqi/commitizen/internal/errors" + "github.com/shipengqi/commitizen/internal/helpers" +) + +type Group struct { + Name string `yaml:"name" json:"name" mapstructure:"name"` + DependsOn DependsOn `yaml:"depends_on" json:"depends_on" mapstructure:"depends_on"` +} + +func NewGroup(name string) *Group { + return &Group{Name: name} +} + +func (g *Group) Validate() []error { + var errs []error + if strutil.IsEmpty(g.Name) { + errs = append(errs, standarderrs.New("the group missing required field: name")) + } + for _, v := range g.DependsOn.OrConditions { + errs = append(errs, v.Validate()...) + } + for _, v := range g.DependsOn.AndConditions { + errs = append(errs, v.Validate()...) + } + return errs +} + +func (g *Group) Render(all map[FieldKey]huh.Field, fields []huh.Field) *huh.Group { + group := huh.NewGroup(fields...) + if len(g.DependsOn.OrConditions) < 1 && len(g.DependsOn.AndConditions) < 1 { + return group + } + + for _, v := range g.DependsOn.OrConditions { + v.fields = all + } + for _, v := range g.DependsOn.AndConditions { + v.fields = all + } + + group.WithHideFunc(func() bool { + orCount := len(g.DependsOn.OrConditions) + andCount := len(g.DependsOn.AndConditions) + + log.Debugf("OrConditions: %d, AndConditions: %d", orCount, andCount) + if orCount < 1 && andCount < 1 { + return false + } + + orMet := false + for _, condition := range g.DependsOn.OrConditions { + if condition.Match() { + orMet = true + break + } + } + + andMetCount := 0 + for _, condition := range g.DependsOn.AndConditions { + if condition.Match() { + andMetCount++ + } + } + log.Debugf("orMet: %v, andMetCount: %d", orMet, andMetCount) + if orCount > 0 && andCount < 1 { + return !orMet + } + if orCount < 1 && andCount > 0 { + return !(andCount == andMetCount) + } + if orCount > 0 && andCount > 0 { + return !(orMet && andMetCount == orCount) + } + return false + }) + + return group +} + +type DependsOn struct { + AndConditions []*Condition `yaml:"and_conditions" json:"and_conditions" mapstructure:"and_conditions"` + OrConditions []*Condition `yaml:"or_conditions" json:"or_conditions" mapstructure:"or_conditions"` +} + +type Condition struct { + fields map[FieldKey]huh.Field + + ParameterName string `yaml:"parameter_name" json:"parameter_name" mapstructure:"parameter_name"` + ValueEmpty *bool `yaml:"value_empty" json:"value_empty" mapstructure:"value_empty"` + ValueEquals interface{} `yaml:"value_equals" json:"value_equals" mapstructure:"value_equals"` + ValueNotEquals interface{} `yaml:"value_not_equals" json:"value_not_equals" mapstructure:"value_not_equals"` + ValueContains interface{} `yaml:"value_contains" json:"value_contains" mapstructure:"value_contains"` + ValueNotContains interface{} `yaml:"value_not_contains" json:"value_not_contains" mapstructure:"value_not_contains"` +} + +func (c *Condition) Validate() []error { + var errs []error + if strutil.IsEmpty(c.ParameterName) { + errs = append(errs, errors.NewMissingErr("parameter_name", "condition")) + } + if c.ValueEmpty == nil && c.ValueEquals == nil && c.ValueNotEquals == nil && + c.ValueNotContains == nil && c.ValueContains == nil { + errs = append(errs, standarderrs.New("missing a valid condition")) + } + return errs +} + +func (c *Condition) Match() bool { + key := GetFiledKey(c.ParameterName) + log.Debugf("math field condition: %s", key) + field, ok := c.fields[key] + if !ok { + log.Debugf("cannot find field condition: %s", key) + return false + } + val := field.GetValue() + if c.ValueEmpty != nil { + return c.IsEmpty(*c.ValueEmpty, val) + } + if c.ValueEquals != nil { + return c.Equal(val) + } + if c.ValueNotEquals != nil { + return c.NotEqual(val) + } + if c.ValueContains != nil { + return c.Contains(val) + } + if c.ValueNotContains != nil { + log.Debugf("value not contains val: %v", val) + return c.NotContains(val) + } + return false +} + +func (c *Condition) Equal(val interface{}) bool { + log.Debugf("%v contains match val: %v", c.ValueEquals, val) + return helpers.Equal(c.ValueEquals, val) +} + +func (c *Condition) NotEqual(val interface{}) bool { + log.Debugf("%v not equals val: %v", c.ValueNotEquals, val) + return helpers.NotEqual(c.ValueNotEquals, val) +} + +func (c *Condition) Contains(val interface{}) bool { + log.Debugf("%v contains val: %v", val, c.ValueContains) + return helpers.Contains(val, c.ValueContains) +} + +func (c *Condition) NotContains(val interface{}) bool { + log.Debugf("%v not contains val: %v", val, c.ValueContains) + return helpers.NotContains(val, c.ValueNotContains) +} + +func (c *Condition) IsEmpty(empty bool, val interface{}) bool { + if empty && helpers.Empty(val) { + log.Debugf("value is empty: %v", val) + return true + } + if !empty && helpers.NotEmpty(val) { + log.Debugf("value is not empty: %v", val) + return true + } + return false +} diff --git a/internal/parameter/param.go b/internal/parameter/param.go index e43fa48..8f3920d 100644 --- a/internal/parameter/param.go +++ b/internal/parameter/param.go @@ -16,12 +16,12 @@ type Interface interface { type Parameter struct { huh.Field `mapstructure:"-"` - Name string `yaml:"name" json:"name" mapstructure:"name"` - Group string `yaml:"group" json:"group" mapstructure:"group"` - Label string `yaml:"label" json:"label" mapstructure:"label"` - Description string `yaml:"description" json:"description" mapstructure:"description"` - Type string `yaml:"type" json:"type" mapstructure:"type"` - // DependsOn DependsOn `yaml:"depends_on" json:"depends_on" mapstructure:"depends_on"` + Name string `yaml:"name" json:"name" mapstructure:"name"` + Group string `yaml:"group" json:"group" mapstructure:"group"` + Label string `yaml:"label" json:"label" mapstructure:"label"` + Description string `yaml:"description" json:"description" mapstructure:"description"` + Type string `yaml:"type" json:"type" mapstructure:"type"` + DependsOn DependsOn `yaml:"depends_on" json:"depends_on" mapstructure:"depends_on"` } func (p *Parameter) GetGroup() string { @@ -43,37 +43,3 @@ func (p *Parameter) Validate() []error { } return errs } - -type DependsOn struct { - AndConditions []Condition `yaml:"and_conditions" json:"and_conditions" mapstructure:"and_conditions"` - OrConditions []Condition `yaml:"or_conditions" json:"or_conditions" mapstructure:"or_conditions"` -} - -type Condition interface { - Match() bool -} - -type EqualsCondition struct { - ParameterName string `yaml:"parameter_name" json:"parameter_name" mapstructure:"parameter_name"` - ValueEquals string `yaml:"value_equals" json:"value_equals" mapstructure:"value_equals"` -} - -type NotEqualsCondition struct { - ParameterName string `yaml:"parameter_name" json:"parameter_name" mapstructure:"parameter_name"` - ValueContains string `yaml:"value_contains" json:"value_contains" mapstructure:"value_contains"` -} - -type ContainsCondition struct { - ParameterName string `yaml:"parameter_name" json:"parameter_name" mapstructure:"parameter_name"` - ValueNotContains string `yaml:"value_not_contains" json:"value_not_contains" mapstructure:"value_not_contains"` -} - -type NotContainsCondition struct { - ParameterName string `yaml:"parameter_name" json:"parameter_name" mapstructure:"parameter_name"` - ValueEmpty string `yaml:"value_empty" json:"value_empty" mapstructure:"value_empty"` -} - -type EmptyCondition struct { - ParameterName string `yaml:"parameter_name" json:"parameter_name" mapstructure:"parameter_name"` - ValueEquals string `yaml:"value_equals" json:"value_equals" mapstructure:"value_equals"` -} diff --git a/internal/parameter/types.go b/internal/parameter/types.go index da517c8..a4bb0be 100644 --- a/internal/parameter/types.go +++ b/internal/parameter/types.go @@ -1,5 +1,11 @@ package parameter +import ( + "strings" + + "github.com/shipengqi/golib/strutil" +) + const ( TypeBoolean = "boolean" TypeInteger = "integer" @@ -9,3 +15,28 @@ const ( TypeSecret = "secret" TypeText = "text" ) + +const UnknownGroup = "unknown" + +type FieldKey string + +func NewFiledKey(item string, group ...string) FieldKey { + if len(group) < 1 { + return FieldKey(UnknownGroup + "." + item) + } + if strutil.IsEmpty(group[0]) { + return FieldKey(UnknownGroup + "." + item) + } + return FieldKey(group[0] + "." + item) +} + +func GetFiledKey(key string) FieldKey { + keys := strings.Split(key, ".") + if len(keys) < 1 { + return "" + } + if len(keys) < 2 { + return FieldKey(UnknownGroup + "." + keys[0]) + } + return FieldKey(key) +} diff --git a/internal/parameter/validators/str_test.go b/internal/parameter/validators/str_test.go index 069ffd0..9063924 100644 --- a/internal/parameter/validators/str_test.go +++ b/internal/parameter/validators/str_test.go @@ -75,6 +75,7 @@ func TestIPv6Validator(t *testing.T) { for _, v := range tests { err := IPv6Validator()(v.ip) if v.expected { + assert.Equal(t) assert.NoError(t, err) } else { assert.Error(t, err) diff --git a/internal/templates/map.go b/internal/templates/map.go index 8ca2a58..6d4e633 100644 --- a/internal/templates/map.go +++ b/internal/templates/map.go @@ -1,40 +1,65 @@ package templates -import "github.com/charmbracelet/huh" +import ( + "github.com/charmbracelet/huh" + + "github.com/shipengqi/commitizen/internal/parameter" +) type SortedGroupMap struct { - values map[string][]huh.Field - keys []string + groups map[string]*parameter.Group + fields map[string][]huh.Field + ordered []string } -func NewSortedGroupMap() *SortedGroupMap { +func NewSortedGroupMap(groups ...*parameter.Group) *SortedGroupMap { + groupmap := make(map[string]*parameter.Group) + for _, group := range groups { + groupmap[group.Name] = group + } return &SortedGroupMap{ - values: make(map[string][]huh.Field), - keys: make([]string, 0), + groups: groupmap, + fields: make(map[string][]huh.Field), + ordered: make([]string, 0), } } -func (m *SortedGroupMap) Set(key string, value []huh.Field) { - if _, ok := m.values[key]; !ok { - // save ordered key only first time - m.keys = append(m.keys, key) +func (m *SortedGroupMap) Length() int { + return len(m.ordered) +} + +func (m *SortedGroupMap) SetFields(group string, fields []huh.Field) { + if _, ok := m.fields[group]; !ok { + // save ordered group name only first time + m.ordered = append(m.ordered, group) + } + if _, ok := m.groups[group]; !ok { + // create group if not exist + m.groups[group] = parameter.NewGroup(group) } - m.values[key] = value + m.fields[group] = fields } -func (m *SortedGroupMap) Get(key string) ([]huh.Field, bool) { +func (m *SortedGroupMap) GetFields(group string) ([]huh.Field, bool) { exists := true - if _, ok := m.values[key]; !ok { + if _, ok := m.fields[group]; !ok { exists = false } - return m.values[key], exists + return m.fields[group], exists } -func (m *SortedGroupMap) FormGroups() []*huh.Group { - var groups []*huh.Group - for _, key := range m.keys { - groups = append(groups, huh.NewGroup(m.values[key]...)) +func (m *SortedGroupMap) GetGroup(group string) (*parameter.Group, bool) { + exists := true + if _, ok := m.groups[group]; !ok { + exists = false } + return m.groups[group], exists +} +func (m *SortedGroupMap) FormGroups(all map[parameter.FieldKey]huh.Field) []*huh.Group { + var groups []*huh.Group + for _, key := range m.ordered { + groups = append(groups, m.groups[key].Render(all, m.fields[key])) + } return groups } diff --git a/internal/templates/template.go b/internal/templates/template.go index 76f3599..e68565e 100644 --- a/internal/templates/template.go +++ b/internal/templates/template.go @@ -11,6 +11,7 @@ import ( "github.com/shipengqi/golib/strutil" "github.com/shipengqi/commitizen/internal/errors" + "github.com/shipengqi/commitizen/internal/helpers" "github.com/shipengqi/commitizen/internal/parameter" "github.com/shipengqi/commitizen/internal/parameter/boolean" "github.com/shipengqi/commitizen/internal/parameter/integer" @@ -21,16 +22,16 @@ import ( "github.com/shipengqi/commitizen/internal/parameter/text" ) -const UnknownGroup = "unknown" - type Template struct { + all map[parameter.FieldKey]huh.Field + sorted *SortedGroupMap + Name string Desc string Format string Default bool Items []map[string]interface{} - groups []*huh.Group - fields []parameter.Interface + Groups []*parameter.Group } func (t *Template) Initialize() error { @@ -38,20 +39,38 @@ func (t *Template) Initialize() error { return errors.NewMissingErr("format") } - groups := NewSortedGroupMap() - exists := make(map[string]struct{}, len(t.Items)) + existGroups := make(map[string]struct{}, len(t.Groups)) + existItems := make(map[string]struct{}, len(t.Items)) + + // validate the groups defined in the template + for _, v := range t.Groups { + errs := v.Validate() + if len(errs) > 0 { + return standarderrs.Join(errs...) + } + + if _, ok := existGroups[v.Name]; ok { + return fmt.Errorf("duplicate group name: %s", v.Name) + } + existGroups[v.Name] = struct{}{} + } + + // create a sorted group map + t.sorted = NewSortedGroupMap(t.Groups...) + t.all = make(map[parameter.FieldKey]huh.Field) + for _, v := range t.Items { - namestr, err := GetValueFromYAML[string](v, "name") + namestr, err := helpers.GetValueFromYAML[string](v, "name") if err != nil { return err } - if _, ok := exists[namestr]; ok { - return fmt.Errorf("duplicate name: %s", namestr) + if _, ok := existItems[namestr]; ok { + return fmt.Errorf("duplicate item name: %s", namestr) } - exists[namestr] = struct{}{} + existItems[namestr] = struct{}{} - typestr, err := GetValueFromYAML[string](v, "type") + typestr, err := helpers.GetValueFromYAML[string](v, "type") if err != nil { return err } @@ -91,39 +110,37 @@ func (t *Template) Initialize() error { if len(errs) > 0 { return standarderrs.Join(errs...) } + group = param.GetGroup() param.Render() - t.fields = append(t.fields, param) - if group == "" { - group = UnknownGroup - } - if fields, ok := groups.Get(group); !ok { + + t.all[parameter.NewFiledKey(namestr, group)] = param + + if fields, ok := t.sorted.GetFields(group); !ok { news := make([]huh.Field, 0) news = append(news, param) - groups.Set(group, news) + t.sorted.SetFields(group, news) } else { fields = append(fields, param) - groups.Set(group, fields) + t.sorted.SetFields(group, fields) } } - t.groups = groups.FormGroups() return nil } func (t *Template) Run() ([]byte, error) { - - if len(t.groups) == 0 { + if t.sorted.Length() == 0 { return nil, nil } values := map[string]interface{}{} - form := huh.NewForm(t.groups...) + form := huh.NewForm(t.sorted.FormGroups(t.all)...) err := form.Run() if err != nil { return nil, err } - for _, field := range t.fields { + for _, field := range t.all { values[field.GetKey()] = field.GetValue() } tmpl, err := template.New("cz").Parse(t.Format) @@ -138,24 +155,3 @@ func (t *Template) Run() ([]byte, error) { return buf.Bytes(), nil } - -// --------------------------------- -// Helpers - -func GetValueFromYAML[T any](data map[string]interface{}, key string) (T, error) { - var ( - res T - ok bool - v interface{} - ) - - v, ok = data[key] - if !ok { - return res, errors.NewMissingErr(key) - } - res, ok = v.(T) - if !ok { - return res, errors.ErrType - } - return res, nil -}