From fdc98eaf1ce061a89b1376a7548176a96d7ddd98 Mon Sep 17 00:00:00 2001 From: iton0 Date: Sun, 27 Oct 2024 23:41:25 -0400 Subject: [PATCH] initial commit --- .github/ISSUE_TEMPLATE/bug_report.md | 34 +++++++++ .github/ISSUE_TEMPLATE/feature_request.md | 20 +++++ .gitignore | 13 ++++ .hkup/pre-commit | 15 ++++ .hkup/pre-push | 25 +++++++ LICENSE | 21 ++++++ README.md | 90 +++++++++++++++++++++++ cmd/add.go | 23 ++++++ cmd/add_test.go | 55 ++++++++++++++ cmd/doc.go | 23 ++++++ cmd/doc_test.go | 55 ++++++++++++++ cmd/init.go | 20 +++++ cmd/init_test.go | 55 ++++++++++++++ cmd/list.go | 21 ++++++ cmd/list_test.go | 66 +++++++++++++++++ cmd/main.go | 8 ++ cmd/remove.go | 23 ++++++ cmd/remove_test.go | 55 ++++++++++++++ cmd/root.go | 23 ++++++ go.mod | 10 +++ go.sum | 10 +++ internal/git/main.go | 87 ++++++++++++++++++++++ internal/logic/add.go | 65 ++++++++++++++++ internal/logic/doc.go | 43 +++++++++++ internal/logic/init.go | 46 ++++++++++++ internal/logic/list.go | 31 ++++++++ internal/logic/main.go | 14 ++++ internal/logic/remove.go | 36 +++++++++ internal/util/main.go | 66 +++++++++++++++++ main.go | 10 +++ scripts/build | 7 ++ scripts/install | 45 ++++++++++++ 32 files changed, 1115 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .gitignore create mode 100755 .hkup/pre-commit create mode 100755 .hkup/pre-push create mode 100644 LICENSE create mode 100644 README.md create mode 100644 cmd/add.go create mode 100644 cmd/add_test.go create mode 100644 cmd/doc.go create mode 100644 cmd/doc_test.go create mode 100644 cmd/init.go create mode 100644 cmd/init_test.go create mode 100644 cmd/list.go create mode 100644 cmd/list_test.go create mode 100644 cmd/main.go create mode 100644 cmd/remove.go create mode 100644 cmd/remove_test.go create mode 100644 cmd/root.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/git/main.go create mode 100644 internal/logic/add.go create mode 100644 internal/logic/doc.go create mode 100644 internal/logic/init.go create mode 100644 internal/logic/list.go create mode 100644 internal/logic/main.go create mode 100644 internal/logic/remove.go create mode 100644 internal/util/main.go create mode 100644 main.go create mode 100755 scripts/build create mode 100755 scripts/install diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..a6adc65 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG] " +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Version [e.g. 22] + +**HkUp (please complete the following information).** +- Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..c8bebdb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEAT] " +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b742136 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/ + +# Test binary, built with `go test -c` +*.test + +# Go workspace file +go.work diff --git a/.hkup/pre-commit b/.hkup/pre-commit new file mode 100755 index 0000000..e4f22e3 --- /dev/null +++ b/.hkup/pre-commit @@ -0,0 +1,15 @@ +#!/bin/sh + +# Check for changes in Go files +if git diff --cached --name-only | grep -q '\.go$'; then + echo "Formatting Go files..." + gofmt -w . + echo "" + + # Add formatted files to the staging area + git add . +else + echo "No Go files changed. Skipping formatting." + echo "" +fi + diff --git a/.hkup/pre-push b/.hkup/pre-push new file mode 100755 index 0000000..76e2ee0 --- /dev/null +++ b/.hkup/pre-push @@ -0,0 +1,25 @@ +#!/bin/bash + +# Check for modified Go files in the staged changes +echo "Checking for modified Go files..." + +# Store the output of the git command in a variable +modified_go_files=$(git diff --name-only HEAD^ HEAD) + +# Check if there are any Go files in the modified files +if echo "$modified_go_files" | grep -q '\.go$'; then + echo "Go files detected. Running tests..." + + # Run tests + if go test ./cmd; then + echo "Tests passed." + echo "" + else + echo "Tests failed. Push cancelled." + exit 1 + fi +else + echo "No Go files changed. Skipping tests." + echo "" +fi + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a0496a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 iton0 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..901ddff --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +# HkUp +Your CLI tool with benefits built by [iton0](https://github.com/iton0) in [Go](https://go.dev/)! + +[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/iton0/hkup-cli)](https://github.com/iton0/hkup-cli/releases/latest) +[![godoc](https://godoc.org/github.com/iton0/hkup-cli?status.svg)](http://godoc.org/github.com/iton0/hkup-cli) +[![Go Report Card](https://goreportcard.com/badge/github.com/iton0/hkup-cli)](https://goreportcard.com/report/github.com/iton0/hkup-cli) + +![GitHub watchers](https://img.shields.io/github/watchers/iton0/hkup-cli?style=social) +![GitHub stars](https://img.shields.io/github/stars/iton0/hkup-cli?style=social) + +## Introduction +Git hooks automate and implement processes in your workflow, increasing code quality and consistency. + +However, many developers avoid git hooks due to a lack of awareness and the perceived complexity of setup, discouraging them from using this feature. + +**HkUp** simplifies the management of git hooks, allowing you to focus on the logic and usage of your hooks instead. + +## Installation +External Dependencies: +- `git` +- `curl` +- `grep` + +Run the script below (supports Linux and macOS): + +```sh +curl -sSL https://raw.githubusercontent.com/iton0/hkup-cli/main/scripts/install | sh +``` +> [!Tip] +> To update HkUp, rerun the above script. +> It will replace the current version. + +#### Uninstalling hkup + +```sh +# Locates and deletes the HkUp binary +sh -c 'rm "$(command -v 'hkup')"' +``` + + + +## Usage Quickstart +This section provides basic information about core usage. For detailed options run `hkup --help`. + +#### Initializing hkup +Run the following command in your git repository to initialize HkUp: +```sh +hkup init +``` + +This command creates a **.hkup** folder and sets the local **core.hooksPath** variable. If the folder already exists, it will simply update the path variable. The path is relative, ensuring that moving your repository won’t affect hook sourcing. + +#### Adding & Removing hooks +Add or remove hooks easily with: +```sh +hkup add +hkup remove +``` + +#### Info & Docs +There are two commands that will help you with both HkUp and git hooks: + +**`hkup list {hook|lang}`** +Outputs list of either available hooks or supported languages. + +**`hkup doc `** +Opens your browser with Git documentation for the specified git hook, helping you understand its usage. + +## Future TODOs +- [ ] add either flags or subcommand for init to specify dir and worktree; also if you want the hkup folder to be hidden or not +- [ ] functionality to save custom setups (ie gitdir and workdir are not in same location) +- [ ] make an update subcommand +- [ ] store custom git hooks as templates for future use (via add template subcmd) + - Allow users to create, store, and share templates for common hooks. Users can fetch these templates over the network. +- [ ] branch-specific hooks +- [ ] logo maybe? + +## Contributing +HkUp welcomes contributions to enhance this CLI application! Before submitting a pull request (PR) for a new feature, please follow these steps: + +1. **Create an Issue**: + If you have an idea for a new feature, please create a new issue in the repository using the **feature_request** template. Provide a clear description of the feature and its potential benefits. Please note that issues submitted without using the template may be closed without warning. + +2. **Wait for Approval**: + Once you submit your issue, I’ll review it and provide feedback. If I approve the feature request, I will let you know that you're free to proceed with your PR. + +3. **Submit Your PR**: + After receiving approval, you can create your PR. Be sure to reference the issue in your PR description. + +Please note that PRs submitted without prior approval through an issue may be closed without merging. This process helps us manage feature requests effectively and ensures that contributions align with the project’s goals. diff --git a/cmd/add.go b/cmd/add.go new file mode 100644 index 0000000..a1d5586 --- /dev/null +++ b/cmd/add.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "github.com/iton0/hkup-cli/internal/git" + "github.com/iton0/hkup-cli/internal/logic" + "github.com/iton0/hkup-cli/internal/util" + "github.com/spf13/cobra" +) + +var ( + addCmd = &cobra.Command{ + Use: "add ", + Short: "Add git hook", + ValidArgs: util.ConvertMapKeysToSlice(git.Hooks()), + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + RunE: logic.Add, + } +) + +func init() { + addCmd.Flags().StringVar(&logic.Lang, "lang", "", "supported languages for git hooks") + rootCmd.AddCommand(addCmd) +} diff --git a/cmd/add_test.go b/cmd/add_test.go new file mode 100644 index 0000000..036cd50 --- /dev/null +++ b/cmd/add_test.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" +) + +func TestAddCmd(t *testing.T) { + var buf bytes.Buffer + rootCmd.SetOut(&buf) + + // Change directory to the parent + originalDir, err := os.Getwd() + if err != nil { + t.Fatalf("could not get current working directory: %v", err) + } + defer os.Chdir(originalDir) // Restore original directory after test + + if err := os.Chdir(filepath.Join(originalDir, "..")); err != nil { + t.Fatalf("could not change to parent directory: %v", err) + } + + tests := []struct { + args []string + want string + err error + }{ + { + args: []string{"add", "test"}, + want: "Usage:\n hkup add [flags]\n\nFlags:\n -h, --help help for add\n --lang string supported languages for git hooks\n\n", + err: fmt.Errorf("invalid argument \"test\" for \"hkup add\""), + }, + // Add more test cases here if necessary, e.g., for error conditions + } + + for _, tt := range tests { + buf.Reset() // Reset the buffer before each command execution + rootCmd.SetArgs(tt.args) + + err := rootCmd.Execute() + + // Check for expected error + if (err != nil) != (tt.err != nil) || (err != nil && err.Error() != tt.err.Error()) { + t.Fatalf("Command failed for args %v: got error %v, want %v", tt.args, err, tt.err) + } + + got := buf.String() + if got != tt.want { + t.Errorf("got output %q, want %q for args %v", got, tt.want, tt.args) + } + } +} diff --git a/cmd/doc.go b/cmd/doc.go new file mode 100644 index 0000000..f2c8a83 --- /dev/null +++ b/cmd/doc.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "github.com/iton0/hkup-cli/internal/git" + "github.com/iton0/hkup-cli/internal/logic" + "github.com/iton0/hkup-cli/internal/util" + "github.com/spf13/cobra" +) + +var ( + docCmd = &cobra.Command{ + Use: "doc ", + Aliases: []string{"docs"}, + Short: "Documentation for git hook", + ValidArgs: util.ConvertMapKeysToSlice(git.Hooks()), + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + RunE: logic.Doc, + } +) + +func init() { + rootCmd.AddCommand(docCmd) +} diff --git a/cmd/doc_test.go b/cmd/doc_test.go new file mode 100644 index 0000000..c3eace8 --- /dev/null +++ b/cmd/doc_test.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" +) + +func TestDocCmd(t *testing.T) { + var buf bytes.Buffer + rootCmd.SetOut(&buf) + + // Change directory to the parent + originalDir, err := os.Getwd() + if err != nil { + t.Fatalf("could not get current working directory: %v", err) + } + defer os.Chdir(originalDir) // Restore original directory after test + + if err := os.Chdir(filepath.Join(originalDir, "..")); err != nil { + t.Fatalf("could not change to parent directory: %v", err) + } + + tests := []struct { + args []string + want string + err error + }{ + { + args: []string{"doc", "test"}, + want: "Usage:\n hkup doc [flags]\n\nAliases:\n doc, docs\n\nFlags:\n -h, --help help for doc\n\n", + err: fmt.Errorf("invalid argument \"test\" for \"hkup doc\""), + }, + // Add more test cases here if necessary, e.g., for error conditions + } + + for _, tt := range tests { + buf.Reset() // Reset the buffer before each command execution + rootCmd.SetArgs(tt.args) + + err := rootCmd.Execute() + + // Check for expected error + if (err != nil) != (tt.err != nil) || (err != nil && err.Error() != tt.err.Error()) { + t.Fatalf("Command failed for args %v: got error %v, want %v", tt.args, err, tt.err) + } + + got := buf.String() + if got != tt.want { + t.Errorf("got %q, want %q for args %v", got, tt.want, tt.args) + } + } +} diff --git a/cmd/init.go b/cmd/init.go new file mode 100644 index 0000000..69f9fca --- /dev/null +++ b/cmd/init.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "github.com/iton0/hkup-cli/internal/logic" + "github.com/spf13/cobra" +) + +var ( + initCmd = &cobra.Command{ + Use: "init", + Short: "Initialize hkup", + Long: "Create an empty hkup folder or reinitialize an existing one", + Args: cobra.NoArgs, + RunE: logic.Init, + } +) + +func init() { + rootCmd.AddCommand(initCmd) +} diff --git a/cmd/init_test.go b/cmd/init_test.go new file mode 100644 index 0000000..7caf875 --- /dev/null +++ b/cmd/init_test.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" +) + +func TestInitCmd(t *testing.T) { + var buf bytes.Buffer + rootCmd.SetOut(&buf) + + // Change directory to the parent + originalDir, err := os.Getwd() + if err != nil { + t.Fatalf("could not get current working directory: %v", err) + } + defer os.Chdir(originalDir) // Restore original directory after test + + if err := os.Chdir(filepath.Join(originalDir, "..")); err != nil { + t.Fatalf("could not change to parent directory: %v", err) + } + + tests := []struct { + args []string + want string + err error + }{ + { + args: []string{"init"}, + want: "Usage:\n hkup init [flags]\n\nFlags:\n -h, --help help for init\n\n", + err: fmt.Errorf("hooksPath already set to .hkup\n"), + }, + // Add more test cases here if necessary, e.g., for error conditions + } + + for _, tt := range tests { + buf.Reset() // Reset the buffer before each command execution + rootCmd.SetArgs(tt.args) + + err := rootCmd.Execute() + + // Check for expected error + if (err != nil) != (tt.err != nil) || (err != nil && err.Error() != tt.err.Error()) { + t.Fatalf("Command failed for args %v: got error %v, want %v", tt.args, err, tt.err) + } + + got := buf.String() + if got != tt.want { + t.Errorf("got last line %q, want %q for args %v", got, tt.want, tt.args) + } + } +} diff --git a/cmd/list.go b/cmd/list.go new file mode 100644 index 0000000..ab8abf3 --- /dev/null +++ b/cmd/list.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "github.com/iton0/hkup-cli/internal/logic" + "github.com/spf13/cobra" +) + +var ( + listCmd = &cobra.Command{ + Use: "list {hook|lang}", + Aliases: []string{"ls"}, + Short: "List git hooks information", + ValidArgs: []string{"hook", "lang"}, + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + RunE: logic.List, + } +) + +func init() { + rootCmd.AddCommand(listCmd) +} diff --git a/cmd/list_test.go b/cmd/list_test.go new file mode 100644 index 0000000..1a5ca30 --- /dev/null +++ b/cmd/list_test.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + + "testing" +) + +func TestListCmd(t *testing.T) { + var buf bytes.Buffer + rootCmd.SetOut(&buf) + + // Change directory to the parent + originalDir, err := os.Getwd() + if err != nil { + t.Fatalf("could not get current working directory: %v", err) + } + defer os.Chdir(originalDir) // Restore original directory after test + + if err := os.Chdir(filepath.Join(originalDir, "..")); err != nil { + t.Fatalf("could not change to parent directory: %v", err) + } + + tests := []struct { + args []string + want string + err error + }{ + { + args: []string{"list"}, + want: "Usage:\n hkup list {hook|lang} [flags]\n\nAliases:\n list, ls\n\nFlags:\n -h, --help help for list\n\n", + err: fmt.Errorf("accepts 1 arg(s), received 0"), + }, + { + args: []string{"list", "test"}, + want: "Usage:\n hkup list {hook|lang} [flags]\n\nAliases:\n list, ls\n\nFlags:\n -h, --help help for list\n\n", + err: fmt.Errorf("invalid argument \"test\" for \"hkup list\""), + }, + { + args: []string{"ls", "hook", "lang"}, + want: "Usage:\n hkup list {hook|lang} [flags]\n\nAliases:\n list, ls\n\nFlags:\n -h, --help help for list\n\n", + err: fmt.Errorf("accepts 1 arg(s), received 2"), + }, + // Add more test cases here if necessary, e.g., for error conditions + } + + for _, tt := range tests { + buf.Reset() // Reset the buffer before each command execution + rootCmd.SetArgs(tt.args) + + err := rootCmd.Execute() + + // Check for expected error + if (err != nil) != (tt.err != nil) || (err != nil && err.Error() != tt.err.Error()) { + t.Fatalf("Command failed for args %v: got error %v, want %v", tt.args, err, tt.err) + } + + got := buf.String() + if got != tt.want { + t.Errorf("got %q, want %q for args %v", got, tt.want, tt.args) + } + } +} diff --git a/cmd/main.go b/cmd/main.go new file mode 100644 index 0000000..ee9fc84 --- /dev/null +++ b/cmd/main.go @@ -0,0 +1,8 @@ +/* +Package cmd initializes all commands (including root command) for the HkUp CLI. + +Additionally, the package holds all tests for commands. +*/ +package cmd + +// Note: This file should be kept empty. diff --git a/cmd/remove.go b/cmd/remove.go new file mode 100644 index 0000000..92f6fcb --- /dev/null +++ b/cmd/remove.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "github.com/iton0/hkup-cli/internal/git" + "github.com/iton0/hkup-cli/internal/logic" + "github.com/iton0/hkup-cli/internal/util" + "github.com/spf13/cobra" +) + +var ( + removeCmd = &cobra.Command{ + Use: "remove ", + Aliases: []string{"rm"}, + Short: "Remove git hook", + ValidArgs: util.ConvertMapKeysToSlice(git.Hooks()), + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + RunE: logic.Remove, + } +) + +func init() { + rootCmd.AddCommand(removeCmd) +} diff --git a/cmd/remove_test.go b/cmd/remove_test.go new file mode 100644 index 0000000..9a30ddb --- /dev/null +++ b/cmd/remove_test.go @@ -0,0 +1,55 @@ +package cmd + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" +) + +func TestRemoveCmd(t *testing.T) { + var buf bytes.Buffer + rootCmd.SetOut(&buf) + + // Change directory to the parent + originalDir, err := os.Getwd() + if err != nil { + t.Fatalf("could not get current working directory: %v", err) + } + defer os.Chdir(originalDir) // Restore original directory after test + + if err := os.Chdir(filepath.Join(originalDir, "..")); err != nil { + t.Fatalf("could not change to parent directory: %v", err) + } + + tests := []struct { + args []string + want string + err error + }{ + { + args: []string{"rm", "test"}, + want: "Usage:\n hkup remove [flags]\n\nAliases:\n remove, rm\n\nFlags:\n -h, --help help for remove\n\n", + err: fmt.Errorf("invalid argument \"test\" for \"hkup remove\""), + }, + // Add more test cases here if necessary, e.g., for error conditions + } + + for _, tt := range tests { + buf.Reset() // Reset the buffer before each command execution + rootCmd.SetArgs(tt.args) + + err := rootCmd.Execute() + + // Check for expected error + if (err != nil) != (tt.err != nil) || (err != nil && err.Error() != tt.err.Error()) { + t.Fatalf("Command failed for args %v: got error %v, want %v", tt.args, err, tt.err) + } + + got := buf.String() + if got != tt.want { + t.Errorf("got %q, want %q for args %v", got, tt.want, tt.args) + } + } +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..ba5fe7a --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,23 @@ +package cmd + +import ( + "github.com/spf13/cobra" +) + +var ( + rootCmd = &cobra.Command{ + Use: "hkup", + Short: "hkup CLI", + Long: `hkup is a management tool for git hooks`, + Args: cobra.MinimumNArgs(1), + Version: "0.1.0", + } +) + +func init() {} + +// Execute serves as a wrapper for the Cobra API's Execute function, +// allowing it to be called from the main package. +func Execute() { + rootCmd.Execute() +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..3af5e59 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/iton0/hkup-cli + +go 1.23.2 + +require github.com/spf13/cobra v1.8.1 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..912390a --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/git/main.go b/internal/git/main.go new file mode 100644 index 0000000..a5a0963 --- /dev/null +++ b/internal/git/main.go @@ -0,0 +1,87 @@ +/* +Package git provides utilities related to Git hooks and supported scripting languages for those hooks. + +This package includes functionality for retrieving information about Git hooks and determining the supported languages for writing hooks. + +This package is designed to facilitate the use and understanding of Git hooks in various programming environments. +*/ +package git + +import ( + "fmt" +) + +// HookDocSite is a constant of the base URL for the Git hooks documentation. +const HookDocSite = "https://git-scm.com/docs/githooks#" + +var ( + // hooks is a map of Git hook names to their corresponding sections in the Git hooks documentation. This map is kept up to date as of 10/24/2024. + // source: https://git-scm.com/docs/githooks + hooks = map[string]string{ + "applypatch-msg": "_applypatch_msg", + "pre-applypatch": "_pre_applypatch", + "post-applypatch": "_post_applypatch", + "pre-commit": "_pre_commit", + "pre-merge-commit": "_pre_merge_commit", + "prepare-commit-msg": "_prepare_commit_msg", + "commit-msg": "_commit_msg", + "post-commit": "_post_commit", + "pre-rebase": "_pre_rebase", + "post-checkout": "_post_checkout", + "post-merge": "_post_merge", + "pre-push": "_pre_push", + "pre-receive": "pre-receive", + "update": "update", + "proc-receive": "proc-receive", + "post-receive": "post-receive", + "post-update": "post-update", + "reference-transaction": "_reference_transaction", + "push-to-checkout": "_push_to_checkout", + "pre-auto-gc": "_pre_auto_gc", + "post-rewrite": "_post_rewrite", + "sendemail-validate": "_sendemail_validate", + "fsmonitor-watchman": "_fsmonitor_watchman", + "p4-changelist": "_p4_changelist", + "p4-prepare-changelist": "_p4_prepare_changelist", + "p4-post-changelist": "_p4_post_changelist", + "p4-pre-submit": "_p4_pre_submit", + "post-index-change": "_post_index_change", + } + + // supportedLangs is a map indicating which programming languages are supported for Git hooks, excluding the default bash. + supportedLangs = map[string]bool{ + "python": true, + "ruby": true, + "node": true, + "perl": true, + "php": true, + } +) + +// GetHook retrieves the URL section of the documentation for a specified Git hook. Returns an error if the hook is not found. +func GetHook(key string) (string, error) { + value, exists := hooks[key] + if !exists { + return "", fmt.Errorf("hook not found: %s", key) + } + return value, nil +} + +// Hooks returns the complete map of all defined Git hooks. +func Hooks() map[string]string { + return hooks +} + +// GetLang reports if a specified language is supported for Git hooks. Returns an error if the language is not recognized. +func GetLang(key string) (bool, error) { + value, exists := supportedLangs[key] + if !exists { + return false, fmt.Errorf("language not supported: %s", key) + } + return value, nil +} + +// SupportedLangs returns the map of supported programming languages for Git hooks. +func SupportedLangs() map[string]bool { + return supportedLangs +} diff --git a/internal/logic/add.go b/internal/logic/add.go new file mode 100644 index 0000000..2b84dec --- /dev/null +++ b/internal/logic/add.go @@ -0,0 +1,65 @@ +package logic + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/iton0/hkup-cli/internal/git" + "github.com/iton0/hkup-cli/internal/util" + "github.com/spf13/cobra" +) + +var ( + // Lang is a flag indicating the programming language to use for the hook script. Defaults to sh. + Lang string +) + +// Add adds a new Git hook with the specified name and optional programming language. +// It creates a new file in the designated `.hkup` directory, setting the appropriate shebang line based on the provided language. +// Returns an error if any of the steps fail, including directory existence, file creation, or permission setting. +func Add(cmd *cobra.Command, args []string) error { + var sheBangLine = "#!/bin/sh\n\n\n\n\n" + hook := args[0] + + if Lang != "" { + if _, err := git.GetLang(Lang); err != nil { + return err + } + sheBangLine = fmt.Sprintf("#!/usr/bin/env %s\n\n\n\n\n", Lang) + } + + if !util.DoesDirectoryExist(FullPath) { + return fmt.Errorf("failed running \"hkup add\"\n%s does not exist", FullPath) + } + + filePath := filepath.Join(FullPath, hook) + + if util.DoesFileExist(filePath) { + return fmt.Errorf("%s hook already exists", hook) + } + + file, err := os.Create(filePath) + if err != nil { + return err + } + + defer func(file *os.File) { + err := file.Close() + if err != nil { + panic(err) + } + }(file) + + _, err = file.WriteString(sheBangLine) + if err != nil { + return fmt.Errorf("failed writing to file: %w", err) + } + + err = os.Chmod(filePath, 0755) + if err != nil { + return fmt.Errorf("failed changing permissions of file: %w", err) + } + + return nil +} diff --git a/internal/logic/doc.go b/internal/logic/doc.go new file mode 100644 index 0000000..ae6321e --- /dev/null +++ b/internal/logic/doc.go @@ -0,0 +1,43 @@ +package logic + +import ( + "fmt" + "os/exec" + "runtime" + + "github.com/iton0/hkup-cli/internal/git" + "github.com/spf13/cobra" +) + +// Doc opens the documentation for a specified Git hook in the default web browser. +// The command takes a single argument, which is the key (name) of the hook. +// It constructs the URL for the documentation based on the hook name and attempts to open it using the appropriate command for the operating system. +// +// Returns: +// - error: Returns an error if the hook key is invalid, if the platform is unsupported, or if there is an issue starting the command; otherwise, it returns nil. +func Doc(cmd *cobra.Command, args []string) error { + key := args[0] + var termCmd *exec.Cmd + var url string + + if hook, err := git.GetHook(key); err != nil { + return err + } else { + url = git.HookDocSite + hook + } + + switch runtime.GOOS { + case "linux": + termCmd = exec.Command("xdg-open", url) + case "darwin": + termCmd = exec.Command("open", url) + case "windows": + termCmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) + default: + return fmt.Errorf("unsupported platform: %s", runtime.GOOS) + } + + termCmd.Start() + + return termCmd.Wait() +} diff --git a/internal/logic/init.go b/internal/logic/init.go new file mode 100644 index 0000000..afb7c11 --- /dev/null +++ b/internal/logic/init.go @@ -0,0 +1,46 @@ +package logic + +import ( + "fmt" + "os/exec" + "path/filepath" + + "github.com/iton0/hkup-cli/internal/util" + "github.com/spf13/cobra" +) + +var ( + // FullPath defines the local repository folder name to hold Git hooks via a relative path. + // It is treated as a constant and points to the ".hkup" directory within the current working directory. + FullPath = filepath.Join(".", ".hkup") +) + +// Init initializes the hkup folder for storing Git hooks in the current repository. +// It checks if the current working directory is a Git repository, creates the hkup folder if it doesn't exist, +// and sets the Git configuration for `core.hooksPath` to point to the hkup folder. +// Returns an error if the current directory is not a Git repository, if the folder creation fails, +// or if there is an issue setting the Git hooks path. +// +// Returns: +// - error: Returns an error if any of the steps fail; otherwise, it returns nil. +func Init(cmd *cobra.Command, args []string) error { + if err := exec.Command("git", "-C", ".", "rev-parse", "--is-inside-work-tree").Run(); err != nil { + return fmt.Errorf("failed to check if cwd is git repo: %w", err) + } + + if !util.DoesDirectoryExist(FullPath) { + if err := util.CreateFolder(FullPath); err != nil { + return err + } + cmd.Printf("Initialized hkup folder at %s\n", FullPath) + } + + if out, _ := exec.Command("git", "config", "--local", "core.hooksPath").Output(); len(out) != 0 { + return fmt.Errorf("hooksPath already set to %s", out) + } else { + if err := exec.Command("git", "config", "--local", "core.hooksPath", FullPath).Run(); err != nil { + return fmt.Errorf("failed to set hooksPath: %w", err) + } + return nil + } +} diff --git a/internal/logic/list.go b/internal/logic/list.go new file mode 100644 index 0000000..ebb75c6 --- /dev/null +++ b/internal/logic/list.go @@ -0,0 +1,31 @@ +package logic + +import ( + "github.com/iton0/hkup-cli/internal/git" + "github.com/iton0/hkup-cli/internal/util" + "github.com/spf13/cobra" +) + +// List displays a list of available Git hooks or supported languages based on the provided argument. +// It takes a single argument, which determines whether to list hooks or languages. +// +// Returns: +// - error: Returns an error if the argument is invalid; otherwise, it returns nil. +func List(cmd *cobra.Command, args []string) error { + arg := args[0] + var output []string + + // NOTE: default case is handled by cobra framework + switch { + case arg == "hook": + output = util.ConvertMapKeysToSlice(git.Hooks()) + case arg == "lang": + output = util.ConvertMapKeysToSlice(git.SupportedLangs()) + } + + for _, key := range output { + cmd.Printf(" %s\n", key) + } + + return nil +} diff --git a/internal/logic/main.go b/internal/logic/main.go new file mode 100644 index 0000000..4a11fb0 --- /dev/null +++ b/internal/logic/main.go @@ -0,0 +1,14 @@ +/* +Package logic provides functionality for managing Git hooks, including commands to add, remove, and list hooks. +This package utilizes the cobra library for command-line interaction and is implemented in the respective commands of the [cmd] package. + +Commands: +- Init: Initialize HkUp. +- Add: Adds a new Git hook with the specified name and optional programming language. +- Remove: Removes an existing Git hook with the specified name. +- List: Lists all available Git hooks or supported Git hook languages. +- Doc: Opens browser for specified Git hook documentation. +*/ +package logic + +// Note: This file should be kept empty. diff --git a/internal/logic/remove.go b/internal/logic/remove.go new file mode 100644 index 0000000..a58d8dd --- /dev/null +++ b/internal/logic/remove.go @@ -0,0 +1,36 @@ +package logic + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/iton0/hkup-cli/internal/util" + "github.com/spf13/cobra" +) + +// Remove deletes a specified Git hook from the hkup folder. +// It takes a single argument, which is the name of the hook to be removed. +// +// Returns: +// - error: Returns an error if the hkup folder does not exist, if the specified hook is not found, +// or if there is an issue deleting the file; otherwise, it returns nil. +func Remove(cmd *cobra.Command, args []string) error { + hook := args[0] + + if !util.DoesDirectoryExist(FullPath) { + return fmt.Errorf("failed running \"hkup remove\"\n%s folder does not exist", FullPath) + } + + filePath := filepath.Join(FullPath, hook) + + if !util.DoesFileExist(filePath) { + return fmt.Errorf("not supported hook: %s", hook) + } + + if err := os.Remove(filePath); err != nil { + return fmt.Errorf("failed deleting file: %w", err) + } + + return nil +} diff --git a/internal/util/main.go b/internal/util/main.go new file mode 100644 index 0000000..04595b6 --- /dev/null +++ b/internal/util/main.go @@ -0,0 +1,66 @@ +/* +Package util provides utility functions for file and directory operations, including creating folders and files, checking existence of files and directories, and converting map keys to slices. + +This package is designed to simplify common file and directory management tasks for hkup related commands in the [internal/logic] package. +*/ +package util + +import ( + "fmt" + "os" +) + +// CreateFolder creates a new directory at the specified path. Returns an error if the operation fails. +func CreateFolder(dirPath string) error { + if err := os.Mkdir(dirPath, os.ModePerm); err != nil { + return fmt.Errorf("failed to create directory %s: %w", dirPath, err) + } + return nil +} + +// CreateFile creates a new file in the specified directory with the given name. Returns an error if the operation fails. +func CreateFile(dirPath string, name string) error { + filePath := dirPath + name + file, err := os.Create(filePath) + if err != nil { + return fmt.Errorf("failed to create file %s: %w", filePath, err) + } + + defer func(file *os.File) { + err := file.Close() + if err != nil { + panic(err) + } + }(file) + + return nil +} + +// DoesDirectoryExist reports if a directory exists at the specified path. +func DoesDirectoryExist(dirPath string) bool { + info, err := os.Stat(dirPath) + if os.IsNotExist(err) { + return false + } + return info.IsDir() +} + +// DoesFileExist reports if a file exists at the specified path. +func DoesFileExist(filePath string) bool { + info, err := os.Stat(filePath) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +// ConvertMapKeysToSlice converts the keys of a map into a returned slice of strings. +func ConvertMapKeysToSlice[T comparable](m map[string]T) []string { + var keys []string + + for key := range m { + keys = append(keys, key) + } + + return keys +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..7a97bb0 --- /dev/null +++ b/main.go @@ -0,0 +1,10 @@ +// Package main initializes Cobra +package main + +import ( + "github.com/iton0/hkup-cli/cmd" +) + +func main() { + cmd.Execute() +} diff --git a/scripts/build b/scripts/build new file mode 100755 index 0000000..4979386 --- /dev/null +++ b/scripts/build @@ -0,0 +1,7 @@ +#!/bin/bash + +mkdir -p ./bin + +GOOS=linux GOARCH=amd64 go build -o ./bin/hkup-linux -ldflags="-s -w" . +GOOS=darwin GOARCH=amd64 go build -o ./bin/hkup-darwin -ldflags="-s -w" . +# GOOS=windows GOARCH=amd64 go build -o ./bin/hkup.exe -ldflags="-s -w" . diff --git a/scripts/install b/scripts/install new file mode 100755 index 0000000..80b6486 --- /dev/null +++ b/scripts/install @@ -0,0 +1,45 @@ +#!/bin/sh + +# Define the GitHub repository +REPO="iton0/hkup-cli" + +# Determine the binary name and installation path based on the OS +case "$(uname)" in + Linux) + BINARY_NAME="hkup-linux" + INSTALL_PATH="/usr/local/bin/hkup" + ;; + Darwin) + BINARY_NAME="hkup-darwin" + INSTALL_PATH="/usr/local/bin/hkup" + ;; + # CYGWIN*|MINGW32*|MSYS*|MINGW*) + # BINARY_NAME="hkup.exe" + # INSTALL_PATH="$USERPROFILE/bin/hkup.exe" # Use a user-specific directory + # mkdir -p "$USERPROFILE/bin" # Create the bin directory if it doesn't exist + # ;; + *) + echo "Unsupported OS: $(uname)" + exit 1 + ;; +esac + +# Get the latest release version from GitHub +LATEST_RELEASE=$(curl -s "https://api.github.com/repos/$REPO/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') +echo "Latest version: $LATEST_RELEASE" + +# Download the binary for the latest release to a temporary location +TEMP_PATH=$(mktemp) +echo "Downloading $BINARY_NAME version $LATEST_RELEASE..." +curl -L "https://github.com/$REPO/releases/download/$LATEST_RELEASE/$BINARY_NAME" -o "$TEMP_PATH" + +# Move the downloaded binary to the installation path +sudo mv "$TEMP_PATH" "$INSTALL_PATH" + +# Make it executable (Linux and macOS) +if [ "$(uname)" != "CYGWIN" ] && [ "$(uname)" != "MINGW" ]; then + sudo chmod +x "$INSTALL_PATH" # Use sudo to change permissions +fi + +echo "hkup installed successfully!" +