Initial commit with basic tests
This commit is contained in:
parent
fcb78985c9
commit
82870d7bb8
|
@ -0,0 +1 @@
|
|||
/git-pre-commit-hook
|
|
@ -0,0 +1,78 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
)
|
||||
|
||||
func blacklisted() bool {
|
||||
bl, err := blacklistedAux()
|
||||
switch {
|
||||
case err != nil:
|
||||
Warn("failed to check blacklist: ", aurora.Red(err))
|
||||
return true
|
||||
case bl:
|
||||
Warn("skipping git-commit hooks in blacklisted repo")
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func blacklistedAux() (bool, error) {
|
||||
// open the user's blacklist file (and skip if ENOENT)
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
f, err := os.Open(filepath.Join(home,
|
||||
"git/hooks/pre-commit-blacklist"))
|
||||
switch {
|
||||
case os.IsNotExist(err):
|
||||
return false, nil
|
||||
case err != nil:
|
||||
return true, err
|
||||
}
|
||||
|
||||
// we're called from the root of the git repo
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
// see if it matches
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
// get a line, stripping comments / spaces / blank lines
|
||||
bl := scanner.Text()
|
||||
if pos := strings.IndexRune(bl, '#'); pos != -1 {
|
||||
bl = bl[:pos]
|
||||
}
|
||||
bl = strings.TrimSpace(bl)
|
||||
if bl == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// we know bl[0] can be dereferenced
|
||||
switch bl[0] {
|
||||
case '*':
|
||||
return strings.HasSuffix(cwd, bl[1:]), nil
|
||||
|
||||
case '~':
|
||||
return cwd == home+bl[1:], nil
|
||||
|
||||
default:
|
||||
return cwd == bl, nil
|
||||
}
|
||||
}
|
||||
if scanner.Err() != nil {
|
||||
return false, scanner.Err()
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
)
|
||||
|
||||
// goFmt checks for bad formatting. It uses the "goimports" command. As of
|
||||
// 2020-02, the command "goimports ." recurses and ignores module boundaries and
|
||||
// non-go directories. It can therefore be run from the root of the git repo.
|
||||
func goFmt() error {
|
||||
// execute goimports
|
||||
wbuf := bytes.NewBuffer(nil)
|
||||
cmd := exec.Command("goimports", "-l", ".")
|
||||
cmd.Stdout = wbuf
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
// non-zero return code = program error (not bad formatting)
|
||||
return fmt.Errorf("%s: %v", aurora.Blue("goimports -l"), aurora.Red(err))
|
||||
}
|
||||
|
||||
// iterate over list of changed files, may be empty
|
||||
var (
|
||||
scanner = bufio.NewScanner(wbuf)
|
||||
werr strings.Builder
|
||||
ok = true
|
||||
)
|
||||
fmt.Fprintln(&werr, aurora.Red("The following files need reformatting:"))
|
||||
for scanner.Scan() {
|
||||
// ignore generated .pb.go files
|
||||
if strings.HasSuffix(scanner.Text(), ".pb.go") {
|
||||
continue
|
||||
}
|
||||
|
||||
ok = false
|
||||
werr.WriteByte('\t')
|
||||
werr.WriteString(scanner.Text())
|
||||
werr.WriteByte('\n')
|
||||
}
|
||||
|
||||
if ok {
|
||||
return nil
|
||||
}
|
||||
return errors.New(werr.String())
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
)
|
||||
|
||||
// goVet executes "go vet" recursively for each module in the git repo. As of
|
||||
// go 1.13, "go vet ./..." is required to recurse but does not straddle
|
||||
// module boundaries. It must be run in a valid package directory. It is
|
||||
// significantly faster than running individual "go vet ." in each dir.
|
||||
func goVet() error {
|
||||
return filepath.Walk(".", goVetW)
|
||||
}
|
||||
|
||||
func goVetW(path string, info os.FileInfo, err error) error {
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
case info.Mode().IsDir() && strings.HasPrefix(info.Name(), "."):
|
||||
return filepath.SkipDir
|
||||
case info.Mode().IsRegular() && info.Name() == "go.mod":
|
||||
dir := filepath.Dir(path)
|
||||
Info(" running %q in %s", aurora.Blue("go vet"), aurora.Green(dir))
|
||||
cmd := exec.Command("go", "vet", "./...")
|
||||
cmd.Dir = path
|
||||
op, err := cmd.CombinedOutput()
|
||||
switch {
|
||||
case len(op) > 0 && err != nil:
|
||||
return fmt.Errorf("go vet failed: %s\n%s\n", aurora.Red(err), op)
|
||||
case len(op) > 0:
|
||||
return fmt.Errorf("%s:\n%s\n", aurora.Red("go vet reports"), op)
|
||||
case err != nil:
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
)
|
||||
|
||||
const (
|
||||
largeFileLimit = 1 << 20 // 1MiB
|
||||
)
|
||||
|
||||
func checkLargeFiles() error {
|
||||
return filepath.Walk(".", checkLargeFilesW)
|
||||
}
|
||||
|
||||
func checkLargeFilesW(path string, info os.FileInfo, err error) error {
|
||||
switch {
|
||||
case err != nil:
|
||||
return err
|
||||
case info.Size() > largeFileLimit:
|
||||
return fmt.Errorf("committing large file: %s (%d bytes)",
|
||||
aurora.Red(path), info.Size())
|
||||
case info.IsDir() && strings.HasPrefix(info.Name(), "."):
|
||||
return filepath.SkipDir
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
var gitStashName string
|
||||
|
||||
func gitStashPush() {
|
||||
// create a temporary file that _ensures_ we have something to
|
||||
// "git stash push". Without this, if all changes were staged, then
|
||||
// "git stash push" would be a no-op, with exit code 0, and the
|
||||
// final "git stash pop" would erroneously pop whatever the user had
|
||||
// stashed themselves.
|
||||
f, err := ioutil.TempFile(".", "git-pre-commit-hook-stash.")
|
||||
if err != nil {
|
||||
Err("failed to create temporary file: %v", err)
|
||||
}
|
||||
gitStashName = f.Name()
|
||||
f.Close()
|
||||
|
||||
// perform git stash
|
||||
cmd := exec.Command("git", "stash", "save",
|
||||
"--keep-index", // don't stash staged changes
|
||||
"--quiet",
|
||||
"--include-untracked", // ignores new unstaged files
|
||||
)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err = cmd.Run(); err != nil {
|
||||
Err("failed to run git stash: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func gitStashPop() {
|
||||
defer os.Remove(gitStashName)
|
||||
cmd := exec.Command("git", "stash", "pop", "--quiet")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
Err("failed to run git stash pop: %v", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
module src.lwithers.me.uk/go/git-pre-commit-hook
|
||||
|
||||
go 1.13
|
||||
|
||||
require github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381
|
|
@ -0,0 +1,2 @@
|
|||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs=
|
||||
github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
|
@ -0,0 +1,53 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
)
|
||||
|
||||
func hasGo() bool {
|
||||
h, err := hasGoAux(".", 3)
|
||||
if err != nil {
|
||||
Warn("error scanning directory for go.mod: %v", aurora.Red(err))
|
||||
return false
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func hasGoAux(dir string, depth int) (bool, error) {
|
||||
fi, err := ioutil.ReadDir(dir)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var dirs []string
|
||||
|
||||
for _, f := range fi {
|
||||
switch {
|
||||
case f.Name()[0] == '.':
|
||||
// do nothing
|
||||
|
||||
case f.IsDir():
|
||||
dirs = append(dirs, f.Name())
|
||||
|
||||
case f.Name() == "go.mod":
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
if depth == 0 {
|
||||
return false, nil
|
||||
}
|
||||
depth--
|
||||
|
||||
for _, subdir := range dirs {
|
||||
h, err := hasGoAux(filepath.Join(dir, subdir), depth)
|
||||
if h || err != nil {
|
||||
return h, err
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if blacklisted() || !hasGo() {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
var exitCode int
|
||||
|
||||
Info("Stashing unstaged changes…")
|
||||
gitStashPush()
|
||||
|
||||
Info("Checking for large files…")
|
||||
if err := checkLargeFiles(); err != nil {
|
||||
fmt.Println(err)
|
||||
exitCode = 1
|
||||
}
|
||||
|
||||
Info("Running goimports…")
|
||||
if err := goFmt(); err != nil {
|
||||
fmt.Println(err)
|
||||
exitCode = 1
|
||||
}
|
||||
|
||||
Info("Running go vet…")
|
||||
if err := goVet(); err != nil {
|
||||
fmt.Println(err)
|
||||
exitCode = 1
|
||||
}
|
||||
|
||||
Info("Restoring unstaged changes…")
|
||||
gitStashPop()
|
||||
os.Exit(exitCode)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
)
|
||||
|
||||
var (
|
||||
title = aurora.Blue("[~/git/hooks/bin/pre-commit]")
|
||||
info = aurora.Blue("INFO")
|
||||
warning = aurora.Yellow("WARN")
|
||||
errorStr = aurora.Red("ERR ")
|
||||
)
|
||||
|
||||
func Info(format string, args ...interface{}) {
|
||||
fmt.Fprintln(os.Stderr, title, info, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Warn(format string, args ...interface{}) {
|
||||
fmt.Fprintln(os.Stderr, title, warning, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Err(format string, args ...interface{}) {
|
||||
fmt.Fprintln(os.Stderr, title, errorStr, fmt.Sprintf(format, args...))
|
||||
os.Exit(1)
|
||||
}
|
Loading…
Reference in New Issue