hooks

package
v0.179.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 2, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

Documentation

Overview

Package hooks manages declarative, user-owned Git hook templates.

Index

Constants

View Source
const (
	BuiltinPreCommit = "builtin:pre-commit"
	BuiltinPrePush   = "builtin:pre-push"
)
View Source
const (
	BuiltinGoPreCommit   = "builtin:go-pre-commit"
	BuiltinGoPrePush     = "builtin:go-pre-push"
	BuiltinNodePreCommit = "builtin:node-pre-commit"
	BuiltinNodePrePush   = "builtin:node-pre-push"
	BuiltinWorktreeGuard = "builtin:worktree-guard"
)
View Source
const (

	// TierSkip runs no language validation.
	TierSkip pushTier = 0
	// TierLint runs lint/vet only. This is the fast lane: a feature branch
	// with no open pull request yet.
	TierLint pushTier = 1
	// TierPublication marks the default branch, a tag, or a branch with an
	// open pull request. The hook still runs only static validation.
	TierPublication pushTier = 2
)
View Source
const CheckpointRefPrefix = "refs/wb/checkpoints/"

CheckpointRefPrefix marks a WB checkpoint ref. A push confined to this namespace is a fast-persistence path, never a landing receipt: nothing about a checkpoint ref implies the work merged or landed anywhere.

View Source
const DefaultBranchEnv = "WB_DEFAULT_BRANCH"

DefaultBranchEnv lets a repository or a fleet policy assert its default branch explicitly, skipping local detection entirely. It never makes a network call either way; this is purely for the rare local checkout where origin/HEAD was never recorded.

View Source
const EventSchemaVersion = 1
View Source
const PolicyVersion = 1
View Source
const SecureHooksGitHelperArgument = "--wb-internal-hooks-git"

SecureHooksGitHelperArgument selects the private WB child-process path that enters an inherited repository descriptor before writing core.hooksPath. It is handled before normal CLI parsing and is not a user command.

Variables

This section is empty.

Functions

func AppendEvents added in v0.4.0

func AppendEvents(path string, events []Event) error

func DetectDefaultBranch added in v0.164.7

func DetectDefaultBranch(repoRoot string) string

DetectDefaultBranch resolves the repository's default branch using only local Git state: an explicit override, the recorded origin/HEAD symref (set by a full clone or `git remote set-head origin -a`), or, failing both, the first of the conventional names that already exists as a local remote-tracking ref. It never fetches and never contacts the network, so it can never be the reason a hook hangs; an unresolved default branch simply means the default-branch publication test never matches, not that classification fails.

func RefreshManagedShims added in v0.22.2

func RefreshManagedShims(repoPath, configPath, wbExecutable, projectsRoot string) (bool, error)

RefreshManagedShims upgrades an already-managed hook installation before a command creates a new worktree. It deliberately leaves repositories without WB-managed hooks alone; a conflicting or malformed managed installation fails the caller before it can create a split-layout checkout.

func RepositoryRoot

func RepositoryRoot(path string) (string, error)

RepositoryRoot resolves path to the enclosing non-bare Git worktree.

func RunSecureHooksGitHelper added in v0.22.2

func RunSecureHooksGitHelper(args []string) int

RunSecureHooksGitHelper is the child-side handoff for configuring core.hooksPath. The parent supplies the already-open repository as fd 3; only this short-lived child changes directory via fchdir before executing an absolute Git path, so a path substitution cannot redirect the config write.

Types

type ActiveProfile added in v0.4.0

type ActiveProfile struct {
	Name   string `json:"name"`
	Order  int    `json:"order"`
	Reason string `json:"reason"`
}

type AgentConfig added in v0.123.0

type AgentConfig struct {
	// AutoTags declares that this repository's own CI already tags it (see
	// lesson l3-check-existing-tags-before-tagging-a-lower-version-with-newer-code-hides-the-fix
	// and lesson l11-find-out-whether-the-repo-auto-tags-before-you-hand-tag-it),
	// so a hand-pushed `git tag`/`git push --tags` is refused. A pointer so an
	// explicit `false` at the repository can override a `true` global policy.
	AutoTags *bool `yaml:"autoTags" json:"autoTags,omitempty"`
}

AgentConfig declares policy for `wb hooks agent pre-tool-use` — the PreToolUse guard, not a Git hook. It lives in the same file as the Git hooks policy because both are "how strict is this repository", but this package only carries the field through decode/validate so the guard's own package (internal/agentguard, which never spawns a process and reads this file itself) can read it; nothing here acts on it.

type AgentPolicy added in v0.123.0

type AgentPolicy struct {
	AutoTags bool
}

AgentPolicy is the resolved (global-then-repository-layered) form of AgentConfig — see AgentConfig for what AutoTags means.

type ApplyOptions

type ApplyOptions struct {
	RepoPath     string
	ConfigPath   string
	WBExecutable string
	// ProjectsRoot is persisted in every shim so hooks keep the same checkout
	// policy when WB was invoked with a non-default --projects-root.
	ProjectsRoot string
	// WBHome is persisted in every shim so hooks preserve an explicit WB_HOME
	// and do not recreate mixed-home state after a later shell invocation.
	WBHome string
	// WBHomeAllowsLegacy marks a shim installed from the normal default layout.
	// Such a shim pins its default write home but must still guard legacy linked
	// worktrees until migration completes.
	WBHomeAllowsLegacy bool
	Repair             bool
	Force              bool
	Now                func() time.Time
	// contains filtered or unexported fields
}

type ApplyResult

type ApplyResult struct {
	Report  CheckReport
	Actions []string
}

func Apply

func Apply(options ApplyOptions) (ApplyResult, error)

Apply installs or repairs WB's local shims. It never overwrites unmanaged hook files unless Force is set, and forced replacements are backed up.

type BlockMetrics added in v0.4.0

type BlockMetrics struct {
	ID                string `json:"id"`
	Profile           string `json:"profile"`
	Hook              string `json:"hook"`
	Runs              int    `json:"runs"`
	Failures          int    `json:"failures"`
	TotalDurationMS   int64  `json:"total_duration_ms"`
	AverageDurationMS int64  `json:"average_duration_ms"`
}

type BlockRunResult added in v0.4.0

type BlockRunResult struct {
	ID       string
	Profile  string
	ExitCode int
	Duration time.Duration
}

type CachedGHPRLookup added in v0.65.0

type CachedGHPRLookup struct {
	RepoRoot  string
	RepoSlug  string
	CachePath string
	TTL       time.Duration
	Timeout   time.Duration
	Now       func() time.Time
	RunGH     runGHFunc
}

CachedGHPRLookup is the enrichment path in push-tier's PR-status decision. It is deliberately the SECOND thing consulted, never the first: a fresh positive cache entry (a signal WB itself already wrote) wins over asking GitHub again. Negative answers are revalidated because PR creation makes them stale immediately. Every gh call has a hard timeout.

func NewCachedGHPRLookup added in v0.65.0

func NewCachedGHPRLookup(repoRoot string) *CachedGHPRLookup

NewCachedGHPRLookup builds the production lookup for repoRoot: cache under the user's XDG state directory (matching hook-metrics' own convention), repository slug from the configured origin remote, gh invoked through console.Env() so it can never block on a prompt or a pager.

func (*CachedGHPRLookup) OpenPullRequest added in v0.65.0

func (l *CachedGHPRLookup) OpenPullRequest(branch string) (open bool, known bool)

OpenPullRequest answers whether branch has an open pull request in RepoSlug. known is false whenever the answer cannot be established without exceeding the bounded, offline-safe budget above: no cache entry, an expired one, gh missing, gh erroring, or the timeout firing. A failed or unknown lookup is never cached, so the next push tries again rather than freezing on a bad answer.

type CheckReport

type CheckReport struct {
	RepoRoot       string          `json:"repo_root"`
	ManagedPath    string          `json:"managed_path"`
	ConfigPaths    []string        `json:"config_paths,omitempty"`
	Hooks          []string        `json:"hooks"`
	ProfilesAuto   bool            `json:"profiles_auto"`
	ActiveProfiles []ActiveProfile `json:"active_profiles,omitempty"`
	// ExcludedProfiles makes explicit policy exceptions auditable even though
	// they intentionally contribute no hook blocks. In particular, disabling
	// the default worktree admission guard must never look like an ordinary
	// healthy installation with no explanation.
	ExcludedProfiles []string            `json:"excluded_profiles,omitempty"`
	HookBlocks       map[string][]string `json:"hook_blocks,omitempty"`
	MetricsPath      string              `json:"metrics_path,omitempty"`
	Findings         []Finding           `json:"findings,omitempty"`
}

func Check

func Check(repoPath, configPath, wbExecutable, projectsRoot string) (CheckReport, error)

Check validates config, core.hooksPath, generated shims, and executability without changing repository state.

type Classification added in v0.65.0

type Classification struct {
	Tier   pushTier
	Reason string
}

Classification is the tier decision for one pre-push invocation, always paired with a one-line, human-readable Reason so an agent can tell "fast tier, no PR" from "hung" or "full tier, publication push" without guessing.

func ClassifyPendingPush added in v0.65.0

func ClassifyPendingPush(stdin io.Reader, repoRoot string) (Classification, error)

ClassifyPendingPush is the entry point `wb hooks push-tier` calls: it reads Git's pre-push ref list from stdin (unless stdin is a terminal, in which case there is no ref list to read and reading it would hang forever waiting for a human who was never asked to type anything — see shouldReplicateStdin for the same hazard), determines the repository's default branch from purely local Git state, and classifies the push.

A terminal stdin (an agent or a human running the hook by hand) has no ref list to classify from, so it reports the tier as explicitly unknown rather than guessing: Tier 1, with a reason that says why, exactly like any other unresolved PR-status case. CI remains the real gate for a publication push either way.

func ClassifyPushTier added in v0.65.0

func ClassifyPushTier(updates []RefUpdate, defaultBranch string, lookup PRLookup) Classification

ClassifyPushTier decides the tier for one composed set of pushed-ref updates. It implements the founder-approved publication rule (option b): a publication push is one whose remote ref is the repository's default branch, OR is a tag, OR names a branch with an open pull request. Anything else pushed to refs/heads/* is the fast lane: lint only. A deletion or a WB checkpoint-ref push carries no requirement of its own; if every pushed ref is one of those two, the whole push skips both lint and test.

defaultBranch may be empty when it cannot be determined locally (never fetched, detached remote HEAD); an empty value simply never matches, so an unresolvable default branch degrades to the same publication test as any other branch (PR lookup) rather than guessing.

func (Classification) ExitCode added in v0.65.0

func (c Classification) ExitCode() int

ExitCode is the fixed process-exit encoding `wb hooks push-tier` uses to hand its decision to the calling shell template: 0, 1, or 2.

type DailyMetrics

type DailyMetrics struct {
	Date              string `json:"date"`
	Commits           int    `json:"commits"`
	PushAttempts      int    `json:"push_attempts"`
	CommitChecks      int    `json:"commit_checks"`
	HookFailures      int    `json:"hook_failures"`
	HookRuns          int    `json:"hook_runs"`
	TotalDurationMS   int64  `json:"total_duration_ms"`
	AverageDurationMS int64  `json:"average_duration_ms"`
}

type Event

type Event struct {
	SchemaVersion int       `json:"schema_version"`
	Timestamp     time.Time `json:"timestamp"`
	Repository    string    `json:"repository"`
	Hook          string    `json:"hook"`
	Profile       string    `json:"profile,omitempty"`
	Block         string    `json:"block,omitempty"`
	Action        string    `json:"action"`
	Outcome       string    `json:"outcome"`
	DurationMS    int64     `json:"duration_ms"`
	Commit        string    `json:"commit,omitempty"`
	Branch        string    `json:"branch,omitempty"`
	// Ref is the REMOTE ref a push targeted. It is what a stream push must be
	// attributed to: the checked-out branch is not necessarily the pushed
	// one, so keying on Branch reported zero stream pushes after real ones.
	Ref string `json:"ref,omitempty"`
	// Tier is the classification the push hook acted on: 0 skip, 1 lint,
	// 2 lint and test. It is recorded so the saving is measured from what
	// actually ran rather than inferred from a branch name.
	Tier   *int              `json:"tier,omitempty"`
	OS     string            `json:"os"`
	Arch   string            `json:"arch"`
	Labels map[string]string `json:"labels,omitempty"`
}

func ReadEvents

func ReadEvents(path string) ([]Event, error)

type ExecutionLayout added in v0.67.11

type ExecutionLayout struct {
	Root               string
	ReportRoot         string
	PendingMetricsRoot string
}

ExecutionLayout is the private writable state a hook may use without touching the repository under test or requiring authority to the whole home directory.

func ResolveExecutionLayout added in v0.67.11

func ResolveExecutionLayout(repoRoot, projectsRoot string) (ExecutionLayout, error)

type Finding

type Finding struct {
	Code    string `json:"code"`
	Message string `json:"message"`
	Path    string `json:"path,omitempty"`
}

type HookBlock added in v0.4.0

type HookBlock struct {
	ID      string
	Profile string
	Hook    ResolvedHook
}

type HookConfig

type HookConfig struct {
	Template string `yaml:"template" json:"template,omitempty"`
	Disabled bool   `yaml:"disabled" json:"disabled,omitempty"`
}

HookConfig selects a script template for one Git hook. Relative template paths are resolved from the YAML file that declares them.

type MetricsConfig

type MetricsConfig struct {
	Enabled *bool             `yaml:"enabled" json:"enabled,omitempty"`
	Path    string            `yaml:"path" json:"path,omitempty"`
	Labels  map[string]string `yaml:"labels" json:"labels,omitempty"`
}

MetricsConfig controls local hook-event collection. Enabled is a pointer so a repository policy can explicitly override a global true/false value.

type MetricsPolicy

type MetricsPolicy struct {
	Enabled bool
	Path    string
	Labels  map[string]string
}

type MetricsSummary

type MetricsSummary struct {
	From              string         `json:"from"`
	Through           string         `json:"through"`
	RepositoryFilter  string         `json:"repository_filter,omitempty"`
	Commits           int            `json:"commits"`
	PushAttempts      int            `json:"push_attempts"`
	CommitChecks      int            `json:"commit_checks"`
	HookFailures      int            `json:"hook_failures"`
	HookRuns          int            `json:"hook_runs"`
	AverageDurationMS int64          `json:"average_duration_ms"`
	Days              []DailyMetrics `json:"days"`
	Blocks            []BlockMetrics `json:"blocks,omitempty"`
}

func Summarize

func Summarize(events []Event, days int, repositoryFilter string, now time.Time) MetricsSummary

type PRLookup added in v0.65.0

type PRLookup interface {
	OpenPullRequest(branch string) (open bool, known bool)
}

PRLookup answers whether one branch has an open pull request. known is false whenever the answer cannot be established locally or within a bounded, offline-safe check; a caller must treat that as "unknown", never as "no", and must never let "unknown" silently escalate to the full tier.

type PendingMetricsReceipt added in v0.67.11

type PendingMetricsReceipt struct {
	SchemaVersion int       `json:"schema_version"`
	RecordedAt    time.Time `json:"recorded_at"`
	TargetPath    string    `json:"target_path"`
	AppendError   string    `json:"append_error"`
	Events        []Event   `json:"events"`
}

type Policy

type Policy struct {
	RepoRoot           string
	ConfigPaths        []string
	Hooks              map[string]ResolvedHook
	ProfilesAuto       bool
	ProfileSelections  map[string]bool
	ProfileDefinitions map[string]ProfileDefinition
	ActiveProfiles     []ActiveProfile
	Metrics            MetricsPolicy
	Agent              AgentPolicy
	ExplicitPath       string
}

Policy is the effective configuration after built-ins, the user's global policy, and the repository policy have been layered in that order.

func LoadPolicy

func LoadPolicy(repoPath, explicitPath string) (Policy, error)

LoadPolicy loads the git_hooks section from ~/.config/wb/wb.yaml and the repository's .wb/hooks.yaml when present. An explicit path replaces those discovery locations but still layers on top of WB's conservative built-in templates.

type ProfileCost added in v0.90.0

type ProfileCost struct {
	Runs              int   `json:"runs"`
	Failures          int   `json:"failures"`
	TotalDurationMS   int64 `json:"total_duration_ms"`
	AverageDurationMS int64 `json:"average_duration_ms"`
	// MaxDurationMS is the measured budget: the slowest observed run.
	MaxDurationMS int64 `json:"max_duration_ms"`
}

ProfileCost is the observed cost of one hook profile.

`every-profile-declares-a-measured-budget` requires a profile to carry a measured cold wall-time budget before it becomes a default. MaxDurationMS is that budget: the worst run actually observed, not an estimate.

type ProfileDefinition added in v0.4.0

type ProfileDefinition struct {
	Name       string
	Order      int
	Detection  ProfileDetection
	Hooks      map[string]ResolvedHook
	ConfigPath string
}

type ProfileDefinitionConfig added in v0.4.0

type ProfileDefinitionConfig struct {
	Order  *int                  `yaml:"order"`
	Detect *ProfileDetection     `yaml:"detect"`
	Hooks  map[string]HookConfig `yaml:"hooks"`
}

ProfileDefinitionConfig describes a language, toolchain, product, or other repository profile. Detection paths are repository-relative and may contain filepath.Glob patterns.

type ProfileDelta added in v0.90.0

type ProfileDelta struct {
	From             string `json:"from"`
	Through          string `json:"through"`
	RepositoryFilter string `json:"repository_filter,omitempty"`
	// Commit is the pre-commit profile: formatting and static checks over the
	// files changed in that commit, never a test suite.
	Commit ProfileCost `json:"commit"`
	// StreamPush is every pre-push on a `stream/<name>` branch. These run no
	// local verification: CI on the stream pull request is the gate.
	StreamPush ProfileCost `json:"stream_push"`
	// OtherPush is every pre-push elsewhere, which runs the current full
	// profile unchanged.
	OtherPush ProfileCost `json:"other_push"`
	// SavedRuns is the number of stream-branch pushes that ran no local
	// verification.
	SavedRuns int `json:"saved_runs"`
	// SavedDurationMS is SavedRuns priced at the measured average cost of a
	// non-stream push. It is an estimate *derived from measurement*, and the
	// basis is reported alongside it so a reader can check the arithmetic.
	SavedDurationMS int64 `json:"saved_duration_ms"`
	// SavedBasisMS is the average non-stream push duration the estimate used.
	SavedBasisMS int64 `json:"saved_basis_ms"`
	// Blocks are the per-profile block costs, so a slow profile can be named
	// rather than guessed at.
	Blocks []BlockMetrics `json:"blocks,omitempty"`
	// Unmeasured names what this window could not price, so a zero saving is
	// never readable as "the stream profile saved nothing".
	Unmeasured []string `json:"unmeasured,omitempty"`
}

ProfileDelta is the evidence behind the hook-profile claim: what a commit costs, what a push costs, and what pushing to a stream branch saved.

The saving is measured rather than asserted, because the whole reason the push hook defers to CI on a stream branch is cost — and a cost claim with no measurement behind it is the assurance this Feature refuses to print.

Implements: dependency-streams#req:commit-hook-is-fast-and-scoped, dependency-streams#req:push-hook-defers-to-ci-on-stream-branches, dependency-streams#req:every-profile-declares-a-measured-budget.

func Measure added in v0.90.0

func Measure(events []Event, days int, repositoryFilter string, now time.Time) ProfileDelta

type ProfileDetection added in v0.4.0

type ProfileDetection struct {
	AnyFiles []string `yaml:"any_files" json:"any_files,omitempty"`
	AllFiles []string `yaml:"all_files" json:"all_files,omitempty"`
}

type ProfilesConfig added in v0.4.0

type ProfilesConfig struct {
	Auto        *bool                              `yaml:"auto"`
	Include     []string                           `yaml:"include"`
	Exclude     []string                           `yaml:"exclude"`
	Definitions map[string]ProfileDefinitionConfig `yaml:"definitions"`
}

ProfilesConfig controls automatic detection and profile composition. Each active profile contributes at most one block to each configured hook type.

type RefUpdate added in v0.65.0

type RefUpdate struct {
	LocalRef  string
	LocalSHA  string
	RemoteRef string
	RemoteSHA string
}

RefUpdate is one line of the pushed-ref list Git streams on pre-push stdin: "<local ref> <local sha1> <remote ref> <remote sha1>".

func ParseRefUpdates added in v0.65.0

func ParseRefUpdates(r io.Reader) ([]RefUpdate, error)

ParseRefUpdates reads Git's pre-push stdin protocol. Blank lines are skipped; anything else that does not carry exactly four fields is rejected rather than silently ignored, so a malformed invocation is never misclassified as "nothing to push".

type ResolvedHook

type ResolvedHook struct {
	Name       string
	Template   string
	Builtin    bool
	Disabled   bool
	ConfigPath string
}

ResolvedHook is a validated hook entry ready to execute.

type RunOptions

type RunOptions struct {
	RepoPath     string
	ConfigPath   string
	Hook         string
	Args         []string
	Stdin        io.Reader
	Stdout       io.Writer
	Stderr       io.Writer
	Now          func() time.Time
	WBExecutable string
	ProjectsRoot string
}

type RunResult

type RunResult struct {
	ExitCode     int
	Duration     time.Duration
	Blocks       []BlockRunResult
	MetricsError error
}

func Run

func Run(options RunOptions) (RunResult, error)

Run executes the configured base and active-profile blocks in the repository root and records compact local events. Hook arguments and streams are passed through unchanged; composed pre-push blocks each receive the complete stdin.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL