From 9e69555771bfdcfa0c5f0ade343f1300e427b5ba Mon Sep 17 00:00:00 2001 From: Valentin Kiselev Date: Fri, 10 Jan 2025 10:50:16 +0300 Subject: [PATCH] feat: add validate command (#915) --- .gitignore | 1 - cmd/commands.go | 1 + cmd/commands_no_self_update.go | 1 + cmd/validate.go | 24 +++++++++ go.mod | 11 ++-- go.sum | 50 +++++++++++++------ internal/config/command.go | 8 +-- internal/config/load.go | 27 ++++++---- internal/config/script.go | 2 +- internal/lefthook/validate.go | 91 ++++++++++++++++++++++++++++++++++ schema.json | 24 +++++++-- 11 files changed, 201 insertions(+), 39 deletions(-) create mode 100644 cmd/validate.go create mode 100644 internal/lefthook/validate.go diff --git a/.gitignore b/.gitignore index ac70140c..4c317ad7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ .idea/ /lefthook /lefthook-local.yml -/gen_schema tmp/ dist/ diff --git a/cmd/commands.go b/cmd/commands.go index 6310a4c3..86702c64 100644 --- a/cmd/commands.go +++ b/cmd/commands.go @@ -20,4 +20,5 @@ var commands = [...]command{ run{}, dump{}, selfUpdate{}, + validate{}, } diff --git a/cmd/commands_no_self_update.go b/cmd/commands_no_self_update.go index 7fa07cac..51fc27d8 100644 --- a/cmd/commands_no_self_update.go +++ b/cmd/commands_no_self_update.go @@ -19,4 +19,5 @@ var commands = [...]command{ uninstall{}, run{}, dump{}, + validate{}, } diff --git a/cmd/validate.go b/cmd/validate.go new file mode 100644 index 00000000..90cad86e --- /dev/null +++ b/cmd/validate.go @@ -0,0 +1,24 @@ +package cmd + +import ( + _ "embed" + + "github.com/spf13/cobra" + + "github.com/evilmartians/lefthook/internal/lefthook" +) + +type validate struct{} + +func (validate) New(opts *lefthook.Options) *cobra.Command { + return &cobra.Command{ + Use: "validate", + Short: "Validate lefthook config", + Long: addDoc, + Example: "lefthook validate", + Args: cobra.NoArgs, + RunE: func(_cmd *cobra.Command, _args []string) error { + return lefthook.Validate(opts) + }, + } +} diff --git a/go.mod b/go.mod index fa2c425e..167f7e91 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/creack/pty v1.1.24 github.com/gobwas/glob v0.2.3 github.com/invopop/jsonschema v0.13.0 + github.com/kaptinlin/jsonschema v0.2.2 github.com/knadh/koanf/maps v0.1.1 github.com/knadh/koanf/parsers/json v0.1.0 github.com/knadh/koanf/parsers/toml/v2 v2.1.0 @@ -23,7 +24,6 @@ require ( github.com/schollz/progressbar/v3 v3.17.1 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.1 - github.com/stoewer/go-strcase v1.3.0 github.com/stretchr/testify v1.9.0 ) @@ -34,6 +34,11 @@ require ( github.com/charmbracelet/x/ansi v0.4.2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/go-viper/mapstructure/v2 v2.1.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/goccy/go-yaml v1.13.4 // indirect + github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 // indirect + github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092 // indirect + github.com/kaptinlin/go-i18n v0.1.3 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect @@ -47,7 +52,7 @@ require ( require ( github.com/alessio/shellescape v1.4.1 - github.com/fatih/color v1.14.1 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect @@ -59,6 +64,6 @@ require ( github.com/spf13/pflag v1.0.5 // indirect golang.org/x/sys v0.27.0 // indirect golang.org/x/term v0.26.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/text v0.19.0 // indirect gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 7aa2bf99..9dbb0a8a 100644 --- a/go.sum +++ b/go.sum @@ -20,21 +20,41 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fatih/color v1.14.1 h1:qfhVLaG5s+nCROl1zJsZRxFeYrHLqWroPOQ8BWiNb4w= -github.com/fatih/color v1.14.1/go.mod h1:2oHN61fhTpgcxD3TSWCgKDiH1+x4OiDVVGH8WlgGZGg= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-viper/mapstructure/v2 v2.1.0 h1:gHnMa2Y/pIxElCH2GlZZ1lZSsn6XMtufpGyP1XxdC/w= github.com/go-viper/mapstructure/v2 v2.1.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.13.4 h1:XOnLX9GqT+kH/gB7YzCMUiDBFU9B7pm3HZz6kyeDPkk= +github.com/goccy/go-yaml v1.13.4/go.mod h1:IjYwxUiJDoqpx2RmbdjMUceGHZwYLon3sfOGl5Hi9lc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976 h1:b70jEaX2iaJSPZULSUxKtm73LBfsCrMsIlYCUgNGSIs= +github.com/gotnospirit/makeplural v0.0.0-20180622080156-a5f48d94d976/go.mod h1:ZGQeOwybjD8lkCjIyJfqR5LD2wMVHJ31d6GdPxoTsWY= +github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092 h1:c7gcNWTSr1gtLp6PyYi3wzvFCEcHJ4YRobDgqmIgf7Q= +github.com/gotnospirit/messageformat v0.0.0-20221001023931-dfe49f1eb092/go.mod h1:ZZAN4fkkful3l1lpJwF8JbW41ZiG9TwJ2ZlqzQovBNU= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/invopop/jsonschema v0.13.0 h1:KvpoAJWEjR3uD9Kbm2HWJmqsEaHt8lBUpd0qHcIi21E= github.com/invopop/jsonschema v0.13.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/kaptinlin/go-i18n v0.1.3 h1:Zmc2sp3N3eNxAPEiyfdbZgF+QF8LZdOdZNR1gHefUe4= +github.com/kaptinlin/go-i18n v0.1.3/go.mod h1:giU+qqtzFZ2U0ksKKVuSxtIFzBLkMA/vlKTeJDyyM2c= +github.com/kaptinlin/jsonschema v0.2.2 h1:aspDbCaqAJ/GSnzmtaSesC0+lnTOjLRamFB/k8mo60s= +github.com/kaptinlin/jsonschema v0.2.2/go.mod h1:HkWM5Yd1hA7K5nvRx/A67wQw/khr6b0/DHrP5CWgAbY= github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= github.com/knadh/koanf/parsers/json v0.1.0 h1:dzSZl5pf5bBcW0Acnu20Djleto19T0CfHcvZ14NJ6fU= @@ -51,6 +71,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= @@ -77,7 +99,6 @@ github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1n github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= @@ -95,31 +116,28 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 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/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= -github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= -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/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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE= +github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw= +golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U= +golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4= +golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -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/command.go b/internal/config/command.go index 40a1f5f0..63495eb4 100644 --- a/internal/config/command.go +++ b/internal/config/command.go @@ -10,10 +10,10 @@ type Command struct { Run string `json:"run" mapstructure:"run" toml:"run" yaml:"run"` Files string `json:"files,omitempty" mapstructure:"files" toml:"files,omitempty" yaml:",omitempty"` - Skip interface{} `json:"skip,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"skip" toml:"skip,omitempty,inline" yaml:",omitempty"` - Only interface{} `json:"only,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"only" toml:"only,omitempty,inline" yaml:",omitempty"` - Tags []string `json:"tags,omitempty" mapstructure:"tags" toml:"tags,omitempty" yaml:",omitempty"` - Env map[string]string `json:"env,omitempty" mapstructure:"env" toml:"env,omitempty" yaml:",omitempty"` + Skip interface{} `json:"skip,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"skip" toml:"skip,omitempty,inline" yaml:",omitempty"` + Only interface{} `json:"only,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"only" toml:"only,omitempty,inline" yaml:",omitempty"` + Tags []string `json:"tags,omitempty" jsonschema:"oneof_type=string;array" mapstructure:"tags" toml:"tags,omitempty" yaml:",omitempty"` + Env map[string]string `json:"env,omitempty" mapstructure:"env" toml:"env,omitempty" yaml:",omitempty"` FileTypes []string `json:"file_types,omitempty" koanf:"file_types" mapstructure:"file_types" toml:"file_types,omitempty" yaml:"file_types,omitempty"` diff --git a/internal/config/load.go b/internal/config/load.go index 1125b8f0..5bd1f01b 100644 --- a/internal/config/load.go +++ b/internal/config/load.go @@ -76,26 +76,25 @@ func loadOne(k *koanf.Koanf, filesystem afero.Fs, root string, names []string) e return ConfigNotFoundError{fmt.Sprintf("No config files with names %q have been found in \"%s\"", names, root)} } -// Loads configs from the given directory with extensions. -func Load(filesystem afero.Fs, repo *git.Repository) (*Config, error) { +func LoadKoanf(filesystem afero.Fs, repo *git.Repository) (*koanf.Koanf, *koanf.Koanf, error) { main := koanf.New(".") // Load main (e.g. lefthook.yml) if err := loadOne(main, filesystem, repo.RootPath, mainConfigNames); err != nil { - return nil, err + return nil, nil, err } // Save `extends` and `remotes` extends := main.Strings("extends") var remotes []*Remote if err := main.Unmarshal("remotes", &remotes); err != nil { - return nil, err + return nil, nil, err } // Deprecated var remote *Remote if err := main.Unmarshal("remote", &remote); err != nil { - return nil, err + return nil, nil, err } // Backward compatibility for `remote`. Will be deleted in future major release @@ -107,12 +106,12 @@ func Load(filesystem afero.Fs, repo *git.Repository) (*Config, error) { // Load main `extends` if err := extend(secondary, filesystem, repo.RootPath, extends); err != nil { - return nil, err + return nil, nil, err } // Load main `remotes` if err := loadRemotes(secondary, filesystem, repo, remotes); err != nil { - return nil, err + return nil, nil, err } // Load optional local config (e.g. lefthook-local.yml) @@ -120,7 +119,7 @@ func Load(filesystem afero.Fs, repo *git.Repository) (*Config, error) { if err := loadOne(secondary, filesystem, repo.RootPath, localConfigNames); err != nil { var configNotFoundErr ConfigNotFoundError if ok := errors.As(err, &configNotFoundErr); !ok { - return nil, err + return nil, nil, err } noLocal = true } @@ -129,10 +128,20 @@ func Load(filesystem afero.Fs, repo *git.Repository) (*Config, error) { localExtends := secondary.Strings("extends") if !noLocal && !slices.Equal(extends, localExtends) { if err := extend(secondary, filesystem, repo.RootPath, localExtends); err != nil { - return nil, err + return nil, nil, err } } + return main, secondary, nil +} + +// Loads configs from the given directory with extensions. +func Load(filesystem afero.Fs, repo *git.Repository) (*Config, error) { + main, secondary, err := LoadKoanf(filesystem, repo) + if err != nil { + return nil, err + } + var config Config config.SourceDir = DefaultSourceDir diff --git a/internal/config/script.go b/internal/config/script.go index 77142497..543abdfc 100644 --- a/internal/config/script.go +++ b/internal/config/script.go @@ -10,7 +10,7 @@ type Script struct { Skip interface{} `json:"skip,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"skip" toml:"skip,omitempty,inline" yaml:",omitempty"` Only interface{} `json:"only,omitempty" jsonschema:"oneof_type=boolean;array" mapstructure:"only" toml:"only,omitempty,inline" yaml:",omitempty"` - Tags []string `json:"tags,omitempty" mapstructure:"tags" toml:"tags,omitempty" yaml:",omitempty"` + Tags []string `json:"tags,omitempty" jsonschema:"oneof_type=string;array" mapstructure:"tags" toml:"tags,omitempty" yaml:",omitempty"` Env map[string]string `json:"env,omitempty" mapstructure:"env" toml:"env,omitempty" yaml:",omitempty"` Priority int `json:"priority,omitempty" mapstructure:"priority" toml:"priority,omitempty" yaml:",omitempty"` diff --git a/internal/lefthook/validate.go b/internal/lefthook/validate.go new file mode 100644 index 00000000..94c42e12 --- /dev/null +++ b/internal/lefthook/validate.go @@ -0,0 +1,91 @@ +package lefthook + +import ( + "errors" + "fmt" + "strings" + + "github.com/kaptinlin/jsonschema" + + "github.com/evilmartians/lefthook/internal/config" + "github.com/evilmartians/lefthook/internal/log" +) + +const schemaUrl = "https://raw.githubusercontent.com/evilmartians/lefthook/master/schema.json" + +func Validate(opts *Options) error { + lefthook, err := initialize(opts) + if err != nil { + return fmt.Errorf("couldn't initialize lefthook: %w", err) + } + + main, secondary, err := config.LoadKoanf(lefthook.Fs, lefthook.repo) + if err != nil { + return err + } + + compiler := jsonschema.NewCompiler() + + schema, err := compiler.GetSchema(schemaUrl) + if err != nil { + return err + } + + result := schema.Validate(main.Raw()) + if !result.IsValid() { + details := result.ToList() + logValidationErrors(0, *details) + + return errors.New("Validation failed for main config") + } + + result = schema.Validate(secondary.Raw()) + if !result.IsValid() { + details := result.ToList() + logValidationErrors(0, *details) + + return errors.New("Validation failed for secondary config") + } + + log.Info("All good") + return nil +} + +func logValidationErrors(indent int, details jsonschema.List) { + if details.Valid { + return + } + + if len(details.InstanceLocation) > 0 { + logDetail(indent, details) + + indent += 2 + } + + for _, d := range details.Details { + logValidationErrors(indent, d) + } +} + +func logDetail(indent int, details jsonschema.List) { + var errors []string + if len(details.Errors) > 0 { + for _, err := range details.Errors { + errors = append(errors, err) + } + } + + option := strings.Repeat(" ", indent) + strings.TrimLeft(details.InstanceLocation, "/") + ":" + + if len(errors) == 0 { + option = log.Gray(option) + } else { + option = log.Yellow(option) + } + + if len(details.Details) > 0 { + log.Info(option) + } else { + log.Info(option, log.Red(strings.Join(errors, ","))) + } +} diff --git a/schema.json b/schema.json index 5d8f9cc5..cfc176ce 100644 --- a/schema.json +++ b/schema.json @@ -31,10 +31,17 @@ ] }, "tags": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array" + } + ], "items": { "type": "string" - }, - "type": "array" + } }, "env": { "additionalProperties": { @@ -347,10 +354,17 @@ ] }, "tags": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array" + } + ], "items": { "type": "string" - }, - "type": "array" + } }, "env": { "additionalProperties": { @@ -378,7 +392,7 @@ "type": "object" } }, - "$comment": "Last updated on 2025.01.09.", + "$comment": "Last updated on 2025.01.10.", "properties": { "min_version": { "type": "string",