diff --git a/Makefile b/Makefile index bdc99977a6..1551f57a88 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,11 @@ ssvsigner-golangci-lint: deadcode-lint: ./scripts/deadcode.sh +.PHONY: install-hooks +install-hooks: + git config core.hooksPath scripts/git-hooks + @echo "git hooks installed (core.hooksPath -> scripts/git-hooks)" + .PHONY: ssvsigner-boundary-lint ssvsigner-boundary-lint: ./scripts/ssvsigner_boundary.sh diff --git a/scripts/git-hooks/pre-push b/scripts/git-hooks/pre-push new file mode 100755 index 0000000000..32b6081030 --- /dev/null +++ b/scripts/git-hooks/pre-push @@ -0,0 +1,145 @@ +#!/bin/sh +# Pre-push hook: lint what this push introduces or modifies, so lint failures +# surface locally instead of on the PR's lint check. Two stages: +# +# 1. misspell (locale US, same as .golangci.yaml's misspell config) on the +# changed .go/.md blobs at the pushed commit. +# 2. golangci-lint (the same config CI's `make lint` uses) on the packages +# containing changed .go files. Unlike misspell it needs full packages on +# disk for type-checking, so it lints the checkout rather than the pushed +# blobs and only runs when pushing the checked-out HEAD. +# +# Install with: make install-hooks +# Bypass for a one-off push: git push --no-verify + +root=$(git rev-parse --show-toplevel) || exit 0 + +# go tool compiles the linters from tool.mod on first use and caches them; go +# may not be on PATH in GUI-spawned shells, so fall back to the default install. +if command -v go >/dev/null 2>&1; then + GO=go +elif [ -x /usr/local/go/bin/go ]; then + GO=/usr/local/go/bin/go +else + exit 0 +fi + +ZERO=0000000000000000000000000000000000000000 +status=0 + +# Changed paths may contain spaces or glob characters: iterate $files with a +# newline-only IFS (restored to the default around the outer read, which needs +# it to split its fields) and keep pathname expansion off for the whole run. +set -f +nl=' +' +oIFS=$IFS +head_sha=$(git rev-parse HEAD) +lint_dirs="" +ssvsigner_lint_dirs="" +misspell_ok="" # probed lazily on the first push with changed files + +while read -r local_ref local_sha remote_ref remote_sha; do + [ "$local_sha" = "$ZERO" ] && continue # deleting a remote ref + + if [ "$remote_sha" = "$ZERO" ]; then + # New remote branch: diff against the merge-base with stage (or main). + base=$(git merge-base "$local_sha" origin/stage 2>/dev/null) || + base=$(git merge-base "$local_sha" origin/main 2>/dev/null) || { + echo "pre-push: no merge-base with origin/stage or origin/main — skipping lint for $local_ref" >&2 + continue + } + else + base=$remote_sha + fi + + files=$(git diff --name-only --diff-filter=ACMR "$base" "$local_sha" -- '*.go' '*.md') + [ -z "$files" ] && continue + + # go tool failures (resolution, compile, module download) must not be + # mistaken for spelling findings, so probe misspell once up front; if it + # can't run, warn and skip this stage instead of blocking the push. + if [ -z "$misspell_ok" ]; then + if printf '' | GOWORK=off "$GO" tool -modfile="$root/tool.mod" misspell -locale US -error >/dev/null 2>&1; then + misspell_ok=y + else + misspell_ok=n + echo "pre-push: cannot run misspell via 'go tool' — skipping the spelling stage" >&2 + fi + fi + + # Check the blobs at local_sha, not the working tree, so uncommitted edits + # don't mask (or fake) findings in what's actually being pushed. + if [ "$misspell_ok" = y ]; then + IFS=$nl + for f in $files; do + out=$(git show "$local_sha:$f" 2>/dev/null | + GOWORK=off "$GO" tool -modfile="$root/tool.mod" misspell -locale US -error 2>&1) + if [ -n "$out" ]; then + echo "pre-push: misspell found issues in $f:" >&2 + echo "$out" | sed "s|^stdin| $f|" >&2 + status=1 + fi + done + IFS=$oIFS + fi + + # Collect the packages holding changed .go files for the golangci-lint + # stage. Only when this ref is the checked-out HEAD — the checkout is what + # gets linted, so for any other ref the results wouldn't describe the push. + [ "$local_sha" = "$head_sha" ] || continue + IFS=$nl + for f in $files; do + case $f in + *.go) ;; + *) continue ;; + esac + d=./$(dirname "$f") + [ -d "$root/${d#./}" ] || continue # dir may be gone (renames) + case $d in + # ssvsigner is a nested module CI lints too (make lint runs + # ssvsigner-golangci-lint from ssvsigner/), and its packages can't be + # loaded from the root, so collect its dirs for a separate run. + ./ssvsigner | ./ssvsigner/*) + sd=.${d#./ssvsigner} + case " $ssvsigner_lint_dirs " in + *" $sd "*) ;; + *) ssvsigner_lint_dirs="$ssvsigner_lint_dirs $sd" ;; + esac + continue + ;; + # Nested module CI's `make lint` doesn't cover. + ./scripts/differ | ./scripts/differ/*) continue ;; + esac + case " $lint_dirs " in + *" $d "*) ;; + *) lint_dirs="$lint_dirs $d" ;; + esac + done + IFS=$oIFS +done + +# golangci-lint stage: staticcheck & friends on the changed packages, scoped so +# a push costs seconds, not CI's full-repo minutes. It sees the working tree, +# so uncommitted edits in these packages show up here. +if [ -n "$lint_dirs" ]; then + # shellcheck disable=SC2086 # lint_dirs is word-split on purpose + if ! (cd "$root" && GOWORK=off "$GO" tool -modfile="$root/tool.mod" golangci-lint run $lint_dirs >&2); then + status=1 + fi +fi +if [ -n "$ssvsigner_lint_dirs" ]; then + # Mirrors make lint's ssvsigner-golangci-lint: the nested module is linted + # from its own directory, with the root's config and tool.mod. + # shellcheck disable=SC2086 # ssvsigner_lint_dirs is word-split on purpose + if ! (cd "$root/ssvsigner" && GOWORK=off "$GO" tool -modfile="$root/tool.mod" golangci-lint run -c "$root/.golangci.yaml" $ssvsigner_lint_dirs >&2); then + status=1 + fi +fi + +if [ "$status" -ne 0 ]; then + echo "" >&2 + echo "pre-push: fix the issues above and amend/commit — the CI lint check (make lint) fails on them." >&2 + echo "pre-push: bypass once with: git push --no-verify" >&2 +fi +exit $status diff --git a/tool.mod b/tool.mod index 08abb9b3ec..fcddbaf74d 100644 --- a/tool.mod +++ b/tool.mod @@ -7,6 +7,7 @@ tool ( github.com/ethereum/go-ethereum/cmd/abigen github.com/ferranbt/fastssz/sszgen github.com/golangci/golangci-lint/v2/cmd/golangci-lint + github.com/golangci/misspell/cmd/misspell github.com/swaggo/swag/cmd/swag go.uber.org/mock/mockgen golang.org/x/tools/cmd/deadcode