Initial commit with basic tests

This commit is contained in:
Laurence Withers 2020-02-16 09:48:17 +00:00
parent fcb78985c9
commit 82870d7bb8
11 changed files with 378 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/git-pre-commit-hook

78
blacklist.go Normal file
View File

@ -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
}

52
check_gofmt.go Normal file
View File

@ -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())
}

43
check_govet.go Normal file
View File

@ -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
}

32
check_largefiles.go Normal file
View File

@ -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
}
}

45
git_stash.go Normal file
View File

@ -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)
}
}

5
go.mod Normal file
View File

@ -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

2
go.sum Normal file
View File

@ -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=

53
has_go.go Normal file
View File

@ -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
}

39
main.go Normal file
View File

@ -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)
}

28
output.go Normal file
View File

@ -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)
}