From d00548b8d3f835b00a1cc402abd115f21df51a66 Mon Sep 17 00:00:00 2001 From: John Wang Date: Sun, 4 Jan 2026 18:17:09 -0800 Subject: [PATCH 1/4] fix: don't skip current directory when walking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the directory to scan is ".", filepath.WalkDir returns "." as the first entry. The previous code checked `name[0] == '.'` which matched the current directory and skipped the entire tree. Fix by explicitly allowing "." while still skipping other hidden directories like .git, .github, etc. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- detect/detect.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/detect/detect.go b/detect/detect.go index de44c18..2fb9f94 100644 --- a/detect/detect.go +++ b/detect/detect.go @@ -36,9 +36,10 @@ func Detect(dir string) ([]Detection, error) { } // Skip hidden directories and common non-source directories + // Note: don't skip "." itself (current directory) if d.IsDir() { name := d.Name() - if name[0] == '.' || name == "node_modules" || name == "vendor" || name == "__pycache__" { + if name != "." && (name[0] == '.' || name == "node_modules" || name == "vendor" || name == "__pycache__") { return filepath.SkipDir } return nil From 7d15a92249c056d4d774372538e2c20c1e26fa82 Mon Sep 17 00:00:00 2001 From: John Wang Date: Sun, 4 Jan 2026 18:58:56 -0800 Subject: [PATCH 2/4] feat: add new checks and soft warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New checks: - go mod tidy: verify go.mod/go.sum are up to date - go build: ensure project compiles Soft checks (warnings, don't fail build): - untracked references: detect if tracked files reference untracked files - coverage: now marked as warning (informational only) Also adds Warning field to Result struct for soft checks that report issues but don't fail the pre-push validation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- checks/checks.go | 27 +++++- checks/checks_test.go | 28 ++++++- checks/golang.go | 189 ++++++++++++++++++++++++++++++++++++++++-- main.go | 14 +++- 4 files changed, 247 insertions(+), 11 deletions(-) diff --git a/checks/checks.go b/checks/checks.go index a4328b8..fe814b7 100644 --- a/checks/checks.go +++ b/checks/checks.go @@ -16,6 +16,7 @@ type Result struct { Error error Skipped bool Reason string + Warning bool // Soft check: reported but doesn't fail the build } // Checker is the interface for language-specific checks. @@ -70,7 +71,8 @@ func CommandExists(command string) bool { } // PrintResults prints check results to stdout. -func PrintResults(results []Result, verbose bool) (passed int, failed int, skipped int) { +// Returns counts: passed, failed, skipped, warnings +func PrintResults(results []Result, verbose bool) (passed int, failed int, skipped int, warnings int) { for _, r := range results { if r.Skipped { fmt.Printf("⊘ %s (skipped: %s)\n", r.Name, r.Reason) @@ -78,6 +80,27 @@ func PrintResults(results []Result, verbose bool) (passed int, failed int, skipp continue } + if r.Warning { + // Soft check: show warning but count as passed + if r.Passed { + fmt.Printf("✓ %s\n", r.Name) + } else { + fmt.Printf("⚠ %s (warning)\n", r.Name) + warnings++ + } + // Always show output for warnings + if r.Output != "" { + lines := strings.Split(r.Output, "\n") + for _, line := range lines { + fmt.Printf(" %s\n", line) + } + } + if r.Passed { + passed++ + } + continue + } + if r.Passed { fmt.Printf("✓ %s\n", r.Name) passed++ @@ -100,7 +123,7 @@ func PrintResults(results []Result, verbose bool) (passed int, failed int, skipp } } - return passed, failed, skipped + return passed, failed, skipped, warnings } // FileExists checks if a file exists. diff --git a/checks/checks_test.go b/checks/checks_test.go index 8d3bdd9..bc88e0f 100644 --- a/checks/checks_test.go +++ b/checks/checks_test.go @@ -101,7 +101,7 @@ func TestPrintResults(t *testing.T) { {Name: "test3", Skipped: true, Reason: "not configured"}, } - passed, failed, skipped := PrintResults(results, false) + passed, failed, skipped, warnings := PrintResults(results, false) if passed != 1 { t.Errorf("expected 1 passed, got %d", passed) @@ -112,4 +112,30 @@ func TestPrintResults(t *testing.T) { if skipped != 1 { t.Errorf("expected 1 skipped, got %d", skipped) } + if warnings != 0 { + t.Errorf("expected 0 warnings, got %d", warnings) + } +} + +func TestPrintResults_Warnings(t *testing.T) { + results := []Result{ + {Name: "test1", Passed: true}, + {Name: "test2", Warning: true, Passed: false, Output: "something to check"}, + {Name: "test3", Warning: true, Passed: true}, + } + + passed, failed, skipped, warnings := PrintResults(results, false) + + if passed != 2 { + t.Errorf("expected 2 passed, got %d", passed) + } + if failed != 0 { + t.Errorf("expected 0 failed, got %d", failed) + } + if skipped != 0 { + t.Errorf("expected 0 skipped, got %d", skipped) + } + if warnings != 1 { + t.Errorf("expected 1 warning, got %d", warnings) + } } diff --git a/checks/golang.go b/checks/golang.go index 3bd370d..83c5700 100644 --- a/checks/golang.go +++ b/checks/golang.go @@ -22,6 +22,12 @@ func (c *GoChecker) Check(dir string, opts Options) []Result { // Check for local replace directives results = append(results, c.checkNoLocalReplace(dir)) + // Check go mod tidy + results = append(results, c.checkModTidy(dir)) + + // Check build + results = append(results, c.checkBuild(dir)) + // Format check if opts.Format { results = append(results, c.checkFormat(dir)) @@ -37,7 +43,11 @@ func (c *GoChecker) Check(dir string, opts Options) []Result { results = append(results, c.checkTest(dir)) } - // Coverage check (informational) + // Soft checks (warnings, don't fail build) + // Untracked file references + results = append(results, c.checkUntrackedReferences(dir)) + + // Coverage check if opts.Coverage { results = append(results, c.checkCoverage(dir, opts.GoExcludeCoverage)) } @@ -142,10 +152,179 @@ func (c *GoChecker) checkCoverage(dir string, exclude string) Result { } result := RunCommand(name, dir, "gocoverbadge", args...) - // Coverage is informational, always passes + // Coverage is informational (soft check) + result.Warning = true result.Passed = true - if result.Error == nil { - fmt.Printf(" %s\n", result.Output) - } return result } + +func (c *GoChecker) checkModTidy(dir string) Result { + name := "Go: mod tidy" + + // Run go mod tidy -diff to check if go.mod/go.sum need updating + // This is available in Go 1.21+ + cmd := exec.Command("go", "mod", "tidy", "-diff") + cmd.Dir = dir + output, err := cmd.CombinedOutput() + + // If -diff is not supported, fall back to checking manually + if err != nil && strings.Contains(string(output), "unknown flag") { + // Fall back: run go mod tidy and check for changes + return c.checkModTidyFallback(dir) + } + + outputStr := strings.TrimSpace(string(output)) + if outputStr != "" { + return Result{ + Name: name, + Passed: false, + Output: "go.mod or go.sum needs updating. Run: go mod tidy", + } + } + + return Result{ + Name: name, + Passed: true, + } +} + +func (c *GoChecker) checkModTidyFallback(dir string) Result { + name := "Go: mod tidy" + + // Get current state of go.mod and go.sum + cmd := exec.Command("git", "diff", "--quiet", "go.mod", "go.sum") + cmd.Dir = dir + err := cmd.Run() + if err != nil { + return Result{ + Name: name, + Passed: false, + Output: "go.mod or go.sum has uncommitted changes", + } + } + + // Run go mod tidy + tidyCmd := exec.Command("go", "mod", "tidy") + tidyCmd.Dir = dir + if err := tidyCmd.Run(); err != nil { + return Result{ + Name: name, + Passed: false, + Error: err, + } + } + + // Check if anything changed + checkCmd := exec.Command("git", "diff", "--quiet", "go.mod", "go.sum") + checkCmd.Dir = dir + err = checkCmd.Run() + if err != nil { + // Restore the original files + restoreCmd := exec.Command("git", "checkout", "go.mod", "go.sum") + restoreCmd.Dir = dir + _ = restoreCmd.Run() // Best effort restore, ignore error + + return Result{ + Name: name, + Passed: false, + Output: "go.mod or go.sum needs updating. Run: go mod tidy", + } + } + + return Result{ + Name: name, + Passed: true, + } +} + +func (c *GoChecker) checkBuild(dir string) Result { + name := "Go: build" + return RunCommand(name, dir, "go", "build", "./...") +} + +func (c *GoChecker) checkUntrackedReferences(dir string) Result { + name := "Go: untracked references" + + // Get list of untracked files + cmd := exec.Command("git", "ls-files", "--others", "--exclude-standard") + cmd.Dir = dir + output, err := cmd.Output() + if err != nil { + return Result{ + Name: name, + Warning: true, + Passed: true, // Can't check, so pass + } + } + + untrackedFiles := strings.Split(strings.TrimSpace(string(output)), "\n") + if len(untrackedFiles) == 0 || (len(untrackedFiles) == 1 && untrackedFiles[0] == "") { + return Result{ + Name: name, + Warning: true, + Passed: true, + } + } + + // Get list of tracked files + trackedCmd := exec.Command("git", "ls-files") + trackedCmd.Dir = dir + trackedOutput, err := trackedCmd.Output() + if err != nil { + return Result{ + Name: name, + Warning: true, + Passed: true, + } + } + + trackedFiles := strings.Split(strings.TrimSpace(string(trackedOutput)), "\n") + + // Check if any tracked file references an untracked file + var references []string + for _, tracked := range trackedFiles { + // Only check Go files and go.mod + if !strings.HasSuffix(tracked, ".go") && tracked != "go.mod" { + continue + } + + for _, untracked := range untrackedFiles { + if untracked == "" { + continue + } + // Simple check: see if the untracked filename appears in the tracked file + // This is a heuristic and may have false positives + baseName := strings.TrimSuffix(untracked, ".go") + if strings.Contains(baseName, "/") { + parts := strings.Split(baseName, "/") + baseName = parts[len(parts)-1] + } + + // Skip common patterns that are likely false positives + if baseName == "main" || baseName == "test" || baseName == "doc" { + continue + } + + grepCmd := exec.Command("grep", "-l", baseName, tracked) + grepCmd.Dir = dir + if grepOutput, err := grepCmd.Output(); err == nil && len(grepOutput) > 0 { + references = append(references, fmt.Sprintf("%s may reference untracked %s", tracked, untracked)) + } + } + } + + if len(references) > 0 { + return Result{ + Name: name, + Warning: true, + Passed: false, + Output: strings.Join(references, "\n"), + } + } + + return Result{ + Name: name, + Warning: true, + Passed: true, + } +} diff --git a/main.go b/main.go index bcc328f..427a931 100644 --- a/main.go +++ b/main.go @@ -157,9 +157,13 @@ func main() { // Print summary fmt.Println("=== Summary ===") - passed, failed, skipped := checks.PrintResults(allResults, cfg.Verbose) + passed, failed, skipped, warnings := checks.PrintResults(allResults, cfg.Verbose) fmt.Println() - fmt.Printf("Passed: %d, Failed: %d, Skipped: %d\n", passed, failed, skipped) + if warnings > 0 { + fmt.Printf("Passed: %d, Failed: %d, Skipped: %d, Warnings: %d\n", passed, failed, skipped, warnings) + } else { + fmt.Printf("Passed: %d, Failed: %d, Skipped: %d\n", passed, failed, skipped) + } if failed > 0 { fmt.Println() @@ -168,5 +172,9 @@ func main() { } fmt.Println() - fmt.Println("All pre-push checks passed!") + if warnings > 0 { + fmt.Println("Pre-push checks passed with warnings.") + } else { + fmt.Println("All pre-push checks passed!") + } } From 01205fa24c2f9d9e94dd72c8414dcf1f6ac215d1 Mon Sep 17 00:00:00 2001 From: John Wang Date: Sun, 4 Jan 2026 19:17:26 -0800 Subject: [PATCH 3/4] docs: expand git hook setup and update examples MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add three options for git hook setup (script, symlink, shared hooks) - Add bypass instructions (--no-verify) - Document hard vs soft checks for Go - Update example output to show new checks (mod tidy, build, untracked refs) - Add example showing warnings output 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- README.md | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 80 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 62223ae..444514a 100644 --- a/README.md +++ b/README.md @@ -48,30 +48,76 @@ prepush --coverage ### As Git Hook -Create `.git/hooks/pre-push`: +To run prepush automatically before every `git push`: + +**Option 1: Create hook script** + +```bash +# Create the hook +cat > .git/hooks/pre-push << 'EOF' +#!/bin/bash +exec prepush +EOF + +# Make it executable +chmod +x .git/hooks/pre-push +``` + +**Option 2: Symlink (simpler)** + +```bash +ln -sf $(which prepush) .git/hooks/pre-push +``` + +**Option 3: Shared hooks (team setup)** + +Since `.git/hooks/` isn't tracked by git, teams can use a shared hooks directory: ```bash +# Create tracked hooks directory +mkdir -p .githooks +cat > .githooks/pre-push << 'EOF' #!/bin/bash exec prepush +EOF +chmod +x .githooks/pre-push + +# Configure git to use it (each team member runs this once) +git config core.hooksPath .githooks ``` -Or symlink: +**Bypassing the hook** + +When needed (e.g., WIP branches), bypass with: ```bash -ln -s $(which prepush) .git/hooks/pre-push +git push --no-verify ``` ## Supported Languages | Language | Detection | Checks | |----------|-----------|--------| -| **Go** | `go.mod` | `gofmt`, `golangci-lint`, `go test`, local replace check | +| **Go** | `go.mod` | `go build`, `go mod tidy`, `gofmt`, `golangci-lint`, `go test`, local replace, untracked refs | | **TypeScript** | `package.json` + `tsconfig.json` | `eslint`, `prettier`, `tsc --noEmit`, `npm test` | | **JavaScript** | `package.json` | `eslint`, `prettier`, `npm test` | | **Python** | `pyproject.toml`, `setup.py`, `requirements.txt` | Coming soon | | **Rust** | `Cargo.toml` | Coming soon | | **Swift** | `Package.swift` | Coming soon | +### Go Checks Detail + +| Check | Type | Description | +|-------|------|-------------| +| no local replace | Hard | Fails if go.mod has local replace directives | +| mod tidy | Hard | Fails if go.mod/go.sum need updating | +| build | Hard | Fails if project doesn't compile | +| gofmt | Hard | Fails if code isn't formatted | +| golangci-lint | Hard | Fails if linter reports issues | +| tests | Hard | Fails if tests fail | +| untracked refs | Soft | Warns if tracked files reference untracked files | +| coverage | Soft | Reports coverage (requires `gocoverbadge`) | + ## Configuration Create `.prepush.yaml` in your repository root: @@ -127,15 +173,44 @@ Running Go checks... === Summary === ✓ Go: no local replace directives +✓ Go: mod tidy +✓ Go: build ✓ Go: gofmt ✓ Go: golangci-lint ✓ Go: tests +✓ Go: untracked references -Passed: 4, Failed: 0, Skipped: 0 +Passed: 7, Failed: 0, Skipped: 0 All pre-push checks passed! ``` +### With Warnings + +``` +$ prepush +=== Pre-push Checks === + +Detecting languages... + Found: go in . + +Running Go checks... + +=== Summary === +✓ Go: no local replace directives +✓ Go: mod tidy +✓ Go: build +✓ Go: gofmt +✓ Go: golangci-lint +✓ Go: tests +⚠ Go: untracked references (warning) + main.go may reference untracked utils.go + +Passed: 6, Failed: 0, Skipped: 0, Warnings: 1 + +Pre-push checks passed with warnings. +``` + ### Monorepo (Go + TypeScript) ``` From e90fa661cfca2c720b1e767c7f764cbc151b62aa Mon Sep 17 00:00:00 2001 From: John Wang Date: Sun, 4 Jan 2026 19:42:00 -0800 Subject: [PATCH 4/4] docs: add v0.2.0 changelog and release notes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add structured changelog entries for v0.2.0 with highlights section for both releases. Generated CHANGELOG.md using structured-changelog. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- CHANGELOG.json | 25 +++++++++++++ CHANGELOG.md | 29 ++++++++++++++ RELEASE_NOTES_v0.2.0.md | 83 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 137 insertions(+) create mode 100644 RELEASE_NOTES_v0.2.0.md diff --git a/CHANGELOG.json b/CHANGELOG.json index 52423ea..3f7bbe9 100644 --- a/CHANGELOG.json +++ b/CHANGELOG.json @@ -3,9 +3,34 @@ "project": "prepush", "repository": "https://github.com/grokify/prepush", "releases": [ + { + "version": "0.2.0", + "date": "2026-01-04", + "highlights": [ + { "description": "Catch more issues locally with new `go build` and `go mod tidy` checks, plus soft warnings that inform without blocking" } + ], + "added": [ + { "description": "Go check: `go mod tidy` to verify go.mod/go.sum are up to date" }, + { "description": "Go check: `go build` to ensure project compiles" }, + { "description": "Go check: untracked references detection (soft warning)" }, + { "description": "Soft warnings system: checks that report issues but don't fail the build" }, + { "description": "Git hook setup instructions with three options (script, symlink, shared hooks)" } + ], + "fixed": [ + { "description": "Directory walking now correctly handles current directory (`.`)" } + ], + "documentation": [ + { "description": "Expanded README with git hook setup options and bypass instructions" }, + { "description": "Added Go checks detail table documenting hard vs soft checks" }, + { "description": "Updated example output to show all 7 checks including warnings" } + ] + }, { "version": "0.1.0", "date": "2026-01-04", + "highlights": [ + { "description": "One command to run all pre-push checks across Go, TypeScript, and JavaScript - with auto-detection and monorepo support" } + ], "added": [ { "description": "Multi-language pre-push hook with auto-detection" }, { "description": "Go checks: `gofmt`, `golangci-lint`, `go test`, local replace directive detection" }, diff --git a/CHANGELOG.md b/CHANGELOG.md index 0471a35..3035b11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,36 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), and this changelog is generated by [Structured Changelog](https://github.com/grokify/structured-changelog). +## [0.2.0] - 2026-01-04 + +### Highlights + +- Catch more issues locally with new `go build` and `go mod tidy` checks, plus soft warnings that inform without blocking + +### Added + +- Go check: `go mod tidy` to verify go.mod/go.sum are up to date +- Go check: `go build` to ensure project compiles +- Go check: untracked references detection (soft warning) +- Soft warnings system: checks that report issues but don't fail the build +- Git hook setup instructions with three options (script, symlink, shared hooks) + +### Fixed + +- Directory walking now correctly handles current directory (`.`) + +### Documentation + +- Expanded README with git hook setup options and bypass instructions +- Added Go checks detail table documenting hard vs soft checks +- Updated example output to show all 7 checks including warnings + ## [0.1.0] - 2026-01-04 +### Highlights + +- One command to run all pre-push checks across Go, TypeScript, and JavaScript - with auto-detection and monorepo support + ### Added - Multi-language pre-push hook with auto-detection @@ -26,4 +54,5 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro - Unit tests for detect, config, and checks packages +[0.2.0]: https://github.com/grokify/prepush/compare/v0.1.0...v0.2.0 [0.1.0]: https://github.com/grokify/prepush/releases/tag/v0.1.0 diff --git a/RELEASE_NOTES_v0.2.0.md b/RELEASE_NOTES_v0.2.0.md new file mode 100644 index 0000000..2f72e9f --- /dev/null +++ b/RELEASE_NOTES_v0.2.0.md @@ -0,0 +1,83 @@ +# Release Notes - v0.2.0 + +**Release Date:** 2026-01-04 + +## Overview + +This release adds new Go checks, introduces a soft warnings system for non-blocking issues, and fixes a critical bug in directory detection. The README has been expanded with comprehensive git hook setup instructions. + +## Highlights + +### New Go Checks + +Three new checks for Go projects: + +| Check | Type | Description | +|-------|------|-------------| +| `go mod tidy` | Hard | Fails if go.mod/go.sum need updating | +| `go build` | Hard | Fails if project doesn't compile | +| untracked refs | Soft | Warns if tracked files reference untracked files | + +### Soft Warnings System + +New `Warning` field in check results allows checks to report issues without failing the build: + +``` +=== Summary === +✓ Go: no local replace directives +✓ Go: mod tidy +✓ Go: build +⚠ Go: untracked references (warning) + main.go may reference untracked utils.go + +Passed: 6, Failed: 0, Skipped: 0, Warnings: 1 + +Pre-push checks passed with warnings. +``` + +Soft checks are useful for: + +- Informational checks (coverage reporting) +- Heuristic checks that may have false positives (untracked references) +- Checks you want visibility into but don't want to block on + +### Expanded Git Hook Documentation + +Three options for setting up prepush as a git hook: + +1. **Script**: Create `.git/hooks/pre-push` manually +2. **Symlink**: `ln -sf $(which prepush) .git/hooks/pre-push` +3. **Shared hooks**: Use `.githooks/` directory with `git config core.hooksPath` + +## Bug Fixes + +### Directory Walking Fix + +Fixed a critical bug where prepush would skip the current directory when run with `.` as the target. The issue was that `filepath.WalkDir` returns `.` as the first entry, and the hidden directory check (`name[0] == '.'`) incorrectly matched it. + +**Before:** `prepush` in a Go project reported "No supported languages detected" +**After:** Correctly detects Go and runs all checks + +## Breaking Changes + +None. This release is fully backwards compatible. + +## Installation + +```bash +go install github.com/grokify/prepush@v0.2.0 +``` + +## What's Next + +Planned for future releases: + +- Python checks (pytest, black, ruff) +- Rust checks (cargo build, cargo test, cargo clippy) +- Configuration for soft vs hard check behavior + +## Links + +- [Full Changelog](CHANGELOG.md) +- [README](README.md) +- [GitHub Repository](https://github.com/grokify/prepush)