diff --git a/.gitignore b/.gitignore index a5f767a..4b0c5d1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,5 @@ hookz .vscode/ -coverage.out \ No newline at end of file +coverage.out +.gitorade \ No newline at end of file diff --git a/.gitorade b/.gitorade deleted file mode 100755 index f6018a1..0000000 --- a/.gitorade +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -git add . -git commit -S -m "$2" -git push origin $1 -git tag -a $3 -m "$2" -git push origin --tags \ No newline at end of file diff --git a/.hookz.yaml b/.hookz.yaml index c6ae6fc..fb10528 100644 --- a/.hookz.yaml +++ b/.hookz.yaml @@ -1,16 +1,25 @@ - version: 2.0 + version: 2.1.1 hooks: - type: pre-commit actions: - name: "Git Pull (Ensure there are no upstream changes that are not local)" exec: git args: ["pull"] - - name: "Go Tidy" + - name: "Go Tidy (Recursive)" + script: " + #!/bin/bash \n + echo -e Tidying all found go.mod occurrences\n + find . -name go.mod -print0 | xargs -0 -n1 dirname | xargs -L 1 bash -c 'cd \"$0\" && pwd && go mod tidy' \n + " + - name: "Go Build (Ensure pulled modules do not break the build)" exec: go - args: ["mod", "tidy"] + args: ["build"] - name: "Update all go dependencies to latest" exec: go args: ["get", "-u", "./..."] + - name: "Run gofmt to format the code" + exec: gofmt + args: ["-s", "-w", "**/*.go"] - name: "Add all changed files during the pre-commit stage" exec: git args: ["add", "."] diff --git a/Makefile b/Makefile index 1aee700..04acff0 100644 --- a/Makefile +++ b/Makefile @@ -19,5 +19,9 @@ build: ## Builds the application test: ## Runs tests and coverage go test -v -coverprofile=coverage.out ./... && go tool cover -func=coverage.out +install: build ## Builds an executable local version of Hookz and puts in in /usr/local/bin + sudo chmod +x hookz + sudo mv hookz /usr/local/bin + all: title build test ## Makes all targets diff --git a/README.md b/README.md index d7cba5d..0d96e8d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ # hookz -[![Go Report Card](https://goreportcard.com/badge/github.com/devops-kung-fu/hookz)](https://goreportcard.com/report/github.com/devops-kung-fu/hookz) +[![Go Report Card](https://goreportcard.com/badge/github.com/devops-kung-fu/hookz)](https://goreportcard.com/report/github.com/devops-kung-fu/hookz) ![GitHub release (latest by date)](https://img.shields.io/github/v/release/devops-kung-fu/hookz) Manages git hooks inside a local git repository based on a configuration. @@ -25,18 +25,18 @@ To install hookz, [download the latest release](https://github.com/devops-kung- Linux Example: ```bash -sudo chmod +x hookz-2.0.0-linux-amd64 -sudo mv hookz-2.0.0-linux-amd64 /usr/local/bin/hookz +sudo chmod +x hookz-2.1.0-linux-amd64 +sudo mv hookz-2.1.0-linux-amd64 /usr/local/bin/hookz ``` ## Configuration Hookz uses a configuration file to generate hooks in your local git repository. This file needs to be in the root of your repository and must be named *.hookz.yaml* -Take for example the following configuration: +### Example Configuration ``` yaml -version: 2.0 +version: 2.1 hooks: - type: pre-commit actions: @@ -59,17 +59,46 @@ hooks: args: ["-e", "Done!"] ``` -Hooks will read this exampe configuration and create a pre-commit hook and a post-commit hook based on this yaml. +Hooks will read this example configuration and create a pre-commit hook and a post-commit hook based on this yaml. An action with an URL will download the binary from the defined URL and configure the hook to execute the command with the defined arguments before a commit happens. The post-commit in this configuration will execute a command named "dude" with the arguments "Hello World" after a commit has occurred. Note that the _dude_ command must be on your path. If it isn't this post-commit will fail because the command isn't found. -## Support for multiple commands in a hook +### Optional elements + +The following notes apply to the elements in the YAML: + +|Attribute|Notes| +|---|---| +|URL|If this exists, then exec and script are ignored. The URL must be a link to an executable binary| +|exec|If this exists then URL and script are ignored| +|script|If this exists then URL, exec, and args are ignored| +|args|Optional in all cases| + +### Inline scripting + +Scripts can be embedded into the .hookz.yaml in multiline format such as follows: + +__NOTE:__ There needs to be a \n at the end of a line if a multi-line statement exists in the script: node, and special characters need to be escaped properly. + +``` yaml +- type: pre-commit + actions: + - name: "Go Tidy (Recursive)" + script: " + #!/bin/bash \n + echo -e Tidying all found go.mod occurrences \n + find . -name go.mod -print0 | xargs -0 -n1 dirname | xargs -L 1 bash -c 'cd \"$0\" && pwd && go mod tidy' \n + " +``` +If you have args flags set, they can be referenced as $1, $2, etc. in your script in a similar manner as passing parameters in. Any scripting language is supported. + +### Support for multiple commands in a hook If multiple hooks are defined in the configuration with the same type (ie: pre-commit) they will be configured to run in the order they appear in the file. There is no need to group types together, they will be written to the appropriate hooks. -## Hook types +### Hook types Hook types that will execute are the same as supported by _git_. Examples are as follows: @@ -87,7 +116,7 @@ Hook types that will execute are the same as supported by _git_. Examples are as * pre-receive * update -## Return Codes +### Return Codes Any non-zero return code from a command executed in a hook will return a FAIL. @@ -95,7 +124,7 @@ Any non-zero return code from a command executed in a hook will return a FAIL. ![](img/hookz.png) -To generate the hooks as defined in your configuration simply execute the following command in the root of your local repository where the .hookz.yaml file resides: +To generate the hooks as defined in your configuration simply execute the following command in the _root of your local repository_ where the .hookz.yaml file resides: ``` bash hookz initialize # you can also use the init alias @@ -113,7 +142,14 @@ To re-download any file defined in an URL key: hookz update ``` -## Verbose option +### Applying changes to the .hookz.yaml +If there is a modification to the .hookz.yaml file in your application, you'll need to apply the changes using the following: + +``` bash +hookz reset +``` + +### Verbose option The initialize (init) and reset command optionally take a verbosity flag to indicate extended output should be displayed when a hook executes. This is handy for debugging or seeing errors that may be suppressed by hookz. @@ -123,10 +159,24 @@ hookz reset --verbose ``` ## Example Hooks +### Recursively tidy all go.mod files in subdirectories + +```yaml +version: 2.1.0 +hooks: + - type: pre-commit + actions: + - name: "Go Tidy (Recursive)" + script: " + #!/bin/bash \n + echo -e Tidying all found go.mod occurrences\n + find . -name go.mod -print0 | xargs -0 -n1 dirname | xargs -L 1 bash -c 'cd \"$0\" && pwd && go mod tidy' \n + " +``` ### Update all go modules to the latest version before committing ```yaml -version: 2.0 +version: 2.1.0 hooks: - type: pre-commit actions: @@ -138,7 +188,7 @@ hooks: ### Pull from your remote branch before committing ``` yaml -version: 2.0 +version: 2.1.0 hooks: - type: pre-commit actions: diff --git a/cmd/init.go b/cmd/init.go index 5395e5e..150b681 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,11 +1,11 @@ package cmd import ( - "errors" "fmt" "os" "path/filepath" + "github.com/segmentio/ksuid" "github.com/spf13/cobra" ) @@ -47,14 +47,54 @@ func createFile(name string) error { return nil } +func createScriptFile(content string) (name string, err error) { + var _, statErr = os.Stat(name) + k, idErr := ksuid.NewRandom() + name, _ = filepath.Abs(fmt.Sprintf(".git/hooks/%s", k.String())) + if idErr != nil { + fmt.Printf("Error generating KSUID: %v\n", err) + return + } + + if os.IsNotExist(statErr) { + hookzFile, hookzFileErr := filepath.Abs(fmt.Sprintf(".git/hooks/%s.hookz", k.String())) + createFile(hookzFile) + if err != nil { + err = hookzFileErr + return + } + + var file, createErr = os.Create(name) + if err != nil { + err = createErr + return + } + + err = os.Chmod(name, 0777) + if err != nil { + return + } + + file, err = os.OpenFile(name, os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return + } + _, err = file.WriteString(content) + if err != nil { + return + } + + defer file.Close() + } + + return +} + func writeHooks() error { var config, err = readConfig() if err != nil { return err } - if config.Version != Version { - return errors.New("Version Mismatch: Expected v1.1 - Check your .hookz.yaml configuration\n") - } exitCodeBlock := ` commandexit=$? @@ -99,23 +139,34 @@ blackText='\033[0;30m' if err != nil { return err } - _, err = file.WriteString(fmt.Sprintf("%s", header)) + _, err = file.WriteString(header) if err != nil { return err } defer file.Close() - + fmt.Println(fmt.Sprintf("\n[*] Writing %s ", hook.Type)) for _, action := range hook.Actions { + var argsString string for _, arg := range action.Args { argsString = fmt.Sprintf("%s %s", argsString, arg) } - if action.URL != nil { + if action.Exec == nil && action.URL != nil { filename, _ := downloadURL(*action.URL) action.Exec = &filename } + + if action.Exec == nil && action.Script != nil { + scriptFileName, err := createScriptFile(*action.Script) + if err != nil { + return err + } + action.Exec = &scriptFileName + } + + fmt.Println(fmt.Sprintf(" Adding %s action: %s", hook.Type, action.Name)) _, err = file.WriteString(fmt.Sprintf("name='%s'\ntype='%s'\n", action.Name, hook.Type)) if err != nil { return err diff --git a/cmd/remove.go b/cmd/remove.go index b4f3559..8854f70 100644 --- a/cmd/remove.go +++ b/cmd/remove.go @@ -8,12 +8,13 @@ import ( var ( removeCmd = &cobra.Command{ - Use: "remove", - Short: "Removes the hooks as defined in the .hooks.yaml file.", - Long: "Removes the hooks as defined in the .hooks.yaml file.", + Use: "remove", + Aliases: []string{"delete"}, + Short: "Removes the hooks as defined in the .hooks.yaml file and any generated scripts.", + Long: "Removes the hooks as defined in the .hooks.yaml file and any generated scripts.", Run: func(cmd *cobra.Command, args []string) { hookzHeader() - fmt.Println("Removing git hooks...") + fmt.Println("[*] Removing existing hooks...") if isErrorBool(removeHooks(), "[ERROR]") { return } diff --git a/cmd/reset.go b/cmd/reset.go index 8947346..1c84fbe 100644 --- a/cmd/reset.go +++ b/cmd/reset.go @@ -13,7 +13,9 @@ var ( Long: "Rebuilds the hooks as defined in the .hooks.yaml file.", Run: func(cmd *cobra.Command, args []string) { hookzHeader() - fmt.Println("Resetting git hooks...") + fmt.Println("Resetting git hooks") + fmt.Println() + fmt.Println("[*] Removing existing hooks...") if isErrorBool(removeHooks(), "[ERROR]") { return } diff --git a/cmd/root.go b/cmd/root.go index 5e64edc..fb0ad40 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -2,6 +2,7 @@ package cmd import ( + "errors" "fmt" "io/ioutil" "log" @@ -18,12 +19,12 @@ import ( var ( //Verbose identifies if extended output should be configured during init and reset - Version float64 + Version = "2.1.0" Verbose bool rootCmd = &cobra.Command{ Use: "hookz", Short: `Manages commit hooks inside a local git repository`, - Version: "2.0.0", + Version: Version, } ) @@ -36,7 +37,7 @@ func Execute() { } func init() { - Version = 2.0 + } func readConfig() (config Configuration, err error) { @@ -55,11 +56,19 @@ func readConfig() (config Configuration, err error) { if err != nil { return } + // Check version + ver := strings.Split(config.Version, ".") + verMatch := strings.Split(Version, ".") + if fmt.Sprintf("%v.%v", ver[0], ver[1]) != fmt.Sprintf("%v.%v", verMatch[0], verMatch[1]) { + err = errors.New(fmt.Sprintf("Version Mismatch: Expected v%v.%v - Check your .hookz.yaml configuration\n", verMatch[0], verMatch[1])) + } return } func hookzHeader() { - fmt.Println("Hookz (https://github.com/devops-kung-fu/hookz)") + fmt.Println("Hookz") + fmt.Println("https://github.com/devops-kung-fu/hookz") + fmt.Printf("Version: %s\n", Version) fmt.Println("") } @@ -81,8 +90,8 @@ func isErrorBool(err error, pre string) (b bool) { func checkExt(ext string, pathS string) (files []string, err error) { filepath.Walk(pathS, func(path string, f os.FileInfo, err error) error { if !f.IsDir() { - r, err := regexp.MatchString(ext, f.Name()) - if err == nil && r { + match, _ := regexp.MatchString(ext, f.Name()) + if match { files = append(files, f.Name()) } } @@ -110,9 +119,10 @@ func removeHooks() (err error) { var hookName = fullPath[0 : len(fullPath)-len(ext)] os.Remove(hookName) parts := strings.Split(hookName, "/") - fmt.Println(fmt.Sprintf("[*] Deleted %s", parts[len(parts)-1])) + fmt.Println(fmt.Sprintf(" Deleted %s", parts[len(parts)-1])) } } + fmt.Println("[*] Successfully removed existing hooks!") return } diff --git a/cmd/structs.go b/cmd/structs.go index f87da00..859b925 100644 --- a/cmd/structs.go +++ b/cmd/structs.go @@ -3,8 +3,8 @@ package cmd // Generated by https://quicktype.io type Configuration struct { - Version float64 `json:"version"` - Hooks []Hook `json:"hooks"` + Version string `json:"version"` + Hooks []Hook `json:"hooks"` } type Hook struct { @@ -13,19 +13,9 @@ type Hook struct { } type Action struct { - Name string `json:"name"` - URL *string `json:"URL,omitempty"` - Args []string `json:"args"` - Exec *string `json:"exec,omitempty"` + Name string `json:"name"` + URL *string `json:"URL,omitempty"` + Args []string `json:"args,omitempty"` + Exec *string `json:"exec,omitempty"` + Script *string `json:"script,omitempty"` } - -//Configuration holds the commit hook definition loaded out of .hookz.yaml -// type Configuration struct { -// Hooks []struct { -// Name string `json:"name"` -// Type string `json:"type"` -// URL *string `json:"url,omitempty"` -// Args []string `json:"args"` -// Exec *string `json:"exec,omitempty"` -// } -// } diff --git a/go.mod b/go.mod index 54b412c..691503d 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.16 require ( github.com/cavaliercoder/grab v2.0.0+incompatible + github.com/segmentio/ksuid v1.0.3 github.com/spf13/cobra v1.1.3 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 8ac3069..393e1b4 100644 --- a/go.sum +++ b/go.sum @@ -143,6 +143,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/segmentio/ksuid v1.0.3 h1:FoResxvleQwYiPAVKe1tMUlEirodZqlqglIuFsdDntY= +github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=