Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,3 +107,7 @@ pre-push:
- "go.mod"
- "go.sum"
run: tools/githooks/.bin/githooks test {push_files}

pr-size-guard:
tags: [guard, diff]
run: tools/githooks/.bin/githooks pr-size
25 changes: 25 additions & 0 deletions tools/githooks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- **Parallel Module Tidy:** Runs `go mod tidy` in parallel across all affected modules.
- **Targeted Linting:** Runs `golangci-lint` only against the exact changed packages within affected modules instead of scanning whole modules or the entire repository.
- **Targeted Unit Testing:** Discovers changed test packages and executes `tools/test` with `-short` directly on those packages (aligned with CI unit test scope).
- **PR Diff Size Guard:** Calculates branch diff against the default branch merge-base, classifies size (`small`, `medium`, `large`), warns or fails on large diffs, and prompts developers to break PRs into smaller chunks.
- **Dependency Changes:** Automatically runs on all packages (`./...`) if a module's `go.mod` or `go.sum` is modified.
- **Lefthook Integration:** Seamlessly works with Lefthook staged/push file filters and `stage_fixed`.

Expand Down Expand Up @@ -115,6 +116,30 @@ go -C tools/githooks run . test core/logger/logger_test.go tools/ci-testshard/ma
go -C tools/githooks run . test --short=false
```

### `pr-size` (aliases: `big-pr-guard`, `pr-guard`, `diff-guard`, `diff-size`)

Calculates the git diff of the current branch against the default branch (the PR diff), classifies it into size categories (`small`, `medium`, `large`), and prompts the developer with recommendations to split large PRs.

```bash
# Check PR diff size with default settings
go -C tools/githooks run . pr-size

# Check with custom thresholds
go -C tools/githooks run . pr-size --small-limit=100 --medium-limit=300

# Fail the command on large PRs (useful in CI or strict pre-push mode)
go -C tools/githooks run . pr-size --fail-on-large

# Select calculation strategy: per-file-max (default), sum, max, or weighted
go -C tools/githooks run . pr-size --strategy=per-file-max

# Diff against a specific base branch or ref
go -C tools/githooks run . pr-size --base=origin/develop

# Automatically ignores generated files defined in .gitattributes (linguist-generated=true)
go -C tools/githooks run . pr-size --ignore-generated=true
```

## Running Tests & Benchmarks

```bash
Expand Down
1 change: 1 addition & 0 deletions tools/githooks/cmd/cmd_bench_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ func BenchmarkCommandsHelp(b *testing.B) {
{"generate", "--help"},
{"end-of-file-fixer", "--help"},
{"whitespace-fixer", "--help"},
{"pr-size", "--help"},
}

for _, args := range commands {
Expand Down
1 change: 1 addition & 0 deletions tools/githooks/cmd/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func TestRootCmd(t *testing.T) {
assert.Contains(t, buf.String(), "generate")
assert.Contains(t, buf.String(), "end-of-file-fixer")
assert.Contains(t, buf.String(), "whitespace-fixer")
assert.Contains(t, buf.String(), "pr-size")
}

func TestLintCmdHelp(t *testing.T) {
Expand Down
69 changes: 69 additions & 0 deletions tools/githooks/cmd/pr_size.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"

"github.com/smartcontractkit/chainlink/v2/tools/githooks/internal/prsize"
)

func newPRSizeCmd() *cobra.Command {
var (
strategy string
smallLimit int
mediumLimit int
failOnLarge bool
baseRef string
ignoreLockfiles bool
ignoreGenerated bool
includeUncommitted bool
)

cmd := &cobra.Command{
Use: "pr-size",
Aliases: []string{"big-pr-guard", "pr-guard", "diff-guard", "diff-size"},
Short: "Check PR diff size against default branch and guard against oversized PRs",
Long: "Calculates the git diff of the current branch against the default branch (the PR diff), " +
"classifies the diff as small, medium, or large, and prompts developers with recommendations " +
"to split large PRs into smaller, reviewable chunks.",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context()

repoRoot, err := findRepoRoot(ctx)
if err != nil {
return fmt.Errorf("could not find repo root: %w", err)
}

cfg := prsize.Config{
RepoRoot: repoRoot,
BaseRef: baseRef,
Strategy: prsize.Strategy(strategy),
SmallLimit: smallLimit,
MediumLimit: mediumLimit,
FailOnLarge: failOnLarge,
IgnoreLockfiles: ignoreLockfiles,
IgnoreGenerated: ignoreGenerated,
IncludeUncommitted: includeUncommitted,
Stdout: cmd.OutOrStdout(),
Stderr: cmd.ErrOrStderr(),
}

return prsize.Run(ctx, cfg)
},
}

cmd.Flags().StringVar(&strategy, "strategy", string(prsize.StrategyPerFileMax), "Diff size strategy: per-file-max, sum, max, weighted")
cmd.Flags().IntVar(&smallLimit, "small-limit", prsize.DefaultSmallLimit, "Maximum effective lines for small classification")
cmd.Flags().IntVar(&smallLimit, "max-small", prsize.DefaultSmallLimit, "Alias for --small-limit")
cmd.Flags().IntVar(&mediumLimit, "medium-limit", prsize.DefaultMediumLimit, "Maximum effective lines for medium classification")
cmd.Flags().IntVar(&mediumLimit, "max-medium", prsize.DefaultMediumLimit, "Alias for --medium-limit")
cmd.Flags().BoolVar(&failOnLarge, "fail-on-large", false, "Exit with error code if PR is classified as large")
cmd.Flags().StringVar(&baseRef, "base", "", "Base branch or ref to diff against (default: origin default branch merge-base)")
cmd.Flags().StringVar(&baseRef, "rev", "", "Alias for --base")
cmd.Flags().BoolVar(&ignoreLockfiles, "ignore-lockfiles", true, "Ignore lockfiles (go.sum, package-lock.json, etc.) from diff line count")
cmd.Flags().BoolVar(&ignoreGenerated, "ignore-generated", true, "Ignore generated changes marked in .gitattributes (linguist-generated)")
cmd.Flags().BoolVar(&includeUncommitted, "include-uncommitted", false, "Include uncommitted staged and unstaged working-tree changes")

return cmd
}
54 changes: 54 additions & 0 deletions tools/githooks/cmd/pr_size_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package cmd_test

import (
"bytes"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/smartcontractkit/chainlink/v2/tools/githooks/cmd"
)

func TestPRSizeCmdHelp(t *testing.T) {
t.Parallel()

root := cmd.NewRootCmd()
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetErr(buf)
root.SetArgs([]string{"pr-size", "--help"})

err := root.Execute()
require.NoError(t, err)

out := buf.String()
assert.Contains(t, buf.String(), "Calculates the git diff of the current branch against the default branch")
assert.Contains(t, out, "--strategy")
assert.Contains(t, out, "--small-limit")
assert.Contains(t, out, "--medium-limit")
assert.Contains(t, out, "--fail-on-large")
assert.Contains(t, out, "--base")
assert.Contains(t, out, "--ignore-lockfiles")
assert.Contains(t, out, "--ignore-generated")
}

func TestPRSizeCmdAliases(t *testing.T) {
t.Parallel()

aliases := []string{"big-pr-guard", "pr-guard", "diff-guard", "diff-size"}
for _, alias := range aliases {
t.Run(alias, func(t *testing.T) {
t.Parallel()
root := cmd.NewRootCmd()
buf := new(bytes.Buffer)
root.SetOut(buf)
root.SetErr(buf)
root.SetArgs([]string{alias, "--help"})

err := root.Execute()
require.NoError(t, err)
assert.Contains(t, buf.String(), "Calculates the git diff of the current branch against the default branch")
})
}
}
1 change: 1 addition & 0 deletions tools/githooks/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func NewRootCmd() *cobra.Command {
rootCmd.AddCommand(newGenerateCmd())
rootCmd.AddCommand(newEOFCmd())
rootCmd.AddCommand(newWhitespaceCmd())
rootCmd.AddCommand(newPRSizeCmd())
return rootCmd
}

Expand Down
Loading
Loading