hooks

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jul 19, 2026 License: GPL-3.0 Imports: 10 Imported by: 0

Documentation

Overview

Package hooks is dstow's hook engine (DESIGN.md §5 + A11): discovery of the eight per-event executables in one hooks directory, the DSTOW_HOOK_* environment contract each hook runs under, direct exec of a hook, and the per-invocation sequencer that fires the nested/LIFO lifecycle (REQUIREMENTS §9.1) once per scope.

The package returns data, never output (A4): diagnostics come back as Warning values and failures as typed errors; the caller (ui) decides how to render them, and only ui touches process streams. The package never references os.Stdout/os.Stderr — the Runner's streams are injected — and it reads os.Getenv/os.Environ only at the point of use (A2): once when building a hook's inherited environment, and once in InHook.

hooks owns ordering and once-per-invocation firing (A11); ops owns the iteration loop. The no-op contract (§9.1.5) is the CALLER's: ops invokes the Invocation methods only for scopes that actually change something, and the once-per-invocation firing then makes repeated bulk runs quiet and idempotent. hooks does not second-guess the caller — a hook file that simply does not exist is a silent no-op (hooks are optional).

Index

Constants

View Source
const (
	EnvLevel      = "DSTOW_HOOK_LEVEL"
	EnvAction     = "DSTOW_HOOK_ACTION"
	EnvPhase      = "DSTOW_HOOK_PHASE"
	EnvFQN        = "DSTOW_HOOK_FQN"
	EnvScheme     = "DSTOW_HOOK_SCHEME"
	EnvCoordinate = "DSTOW_HOOK_COORDINATE"
	EnvPackage    = "DSTOW_HOOK_PACKAGE"
	EnvPackageDir = "DSTOW_HOOK_PACKAGE_DIR"
	EnvTarget     = "DSTOW_HOOK_TARGET"
	EnvRepoFQN    = "DSTOW_HOOK_REPO_FQN"
	EnvRepoDir    = "DSTOW_HOOK_REPO_DIR"
	EnvPackages   = "DSTOW_HOOK_PACKAGES"
)

The twelve DSTOW_HOOK_* variable names (H2). Each is a named constant so no other file spells a variable name as a literal.

Variables

This section is empty.

Functions

func Discover

func Discover(dir string) (Set, []Warning, error)

Discover scans ONE hooks directory, non-recursively (M6). It returns the Set of present executable hooks plus any M6/M7 warnings the directory's contents draw. An absent directory is an empty Set with no warnings and no error (hooks are optional); any other ReadDir failure is returned as the error.

Executability is tested with os.Stat, not Lstat (mode&0111 != 0): a symlink to an executable is a hook.

func InHook

func InHook() bool

InHook reports whether this process is running inside a dstow hook — the entire H7 surface in this package. Detection is DSTOW_HOOK_ACTION present in the environment (H7), read at the point of use (A2). The actual write refusals live in the write commands (later tickets); this predicate only answers the question.

func ScopeHooksDir

func ScopeHooksDir(scopeRoot string) string

ScopeHooksDir returns the hooks directory of a repo or package root: <scopeRoot>/.dstow/hooks (M6). Discovery goes through config's metadata accessor (A11), so hooks and config can never disagree about where metadata lives. The global level's hooks directory is not this — it is GlobalScope.Dir/hooks (the caller supplies the global config dir for testability; see GlobalScope).

Types

type Action

type Action int

Action is one of the four lifecycle actions (§5 H2). Restow is its own pair, not a stow+unstow composition: M6 pins "Restow fires only the restow pair", so the caller passes ActionRestow and the restow hooks fire, nothing else.

const (
	ActionStow Action = iota
	ActionUnstow
	ActionRestow
	ActionAdopt
)

func (Action) String

func (a Action) String() string

String yields the DSTOW_HOOK_ACTION spelling (H2).

type GlobalScope

type GlobalScope struct {
	Dir      string     // global config dir; hooks dir = Dir/hooks; also the hook cwd (H5)
	Packages []name.FQN // all packages acting this invocation
}

GlobalScope carries a global-level firing's context (H2). Dir is the global config dir ($XDG_CONFIG_HOME/dstow in production, supplied by the caller for testability); the hooks directory is Dir/hooks and the hook cwd is Dir (H5). Packages are all packages acting this invocation, for DSTOW_HOOK_PACKAGES.

type Hook

type Hook struct {
	Phase  Phase
	Action Action
}

Hook identifies one of the eight lifecycle hooks: a (Phase, Action) pair (M6). It is the key of a Set.

func (Hook) FileName

func (h Hook) FileName() string

FileName is the on-disk hook file name, Phase.String()+"-"+Action.String() (M6) — one of {pre,post}-{stow,unstow,restow,adopt}.

type HookError

type HookError struct {
	Level  Level
	Action Action
	Phase  Phase
	Path   string // the hook file that failed, or the hooks dir when discovery failed
	Err    error
}

HookError reports that one firing failed — a non-zero exit, an exec error, or the scope's hooks directory being unreadable at discovery. hooks only classifies: it names the Level and Phase off which the caller (ops, ticket #44) applies REQUIREMENTS §9.1.4 blocking (a failed package-pre blocks that package; a failed repo/global-pre blocks everything under it; a failed post marks its scope failed but completed work stays).

func (*HookError) Error

func (e *HookError) Error() string

func (*HookError) Unwrap

func (e *HookError) Unwrap() error

type Invocation

type Invocation struct {
	// contains filtered or unexported fields
}

Invocation drives the nested/LIFO hook lifecycle for one dstow invocation (A11 + REQUIREMENTS §9.1.3): global-pre → repo-pre → package-pre → action → package-post → repo-post → global-post. It owns ordering and once-per-invocation firing; ops owns the iteration loop and calls the methods below in nesting order for each acting scope.

Each hooks directory is discovered lazily, at most once per invocation, and its warnings are surfaced exactly once — from whichever method first reaches it. Each pre hook fires at most once per scope: once fired is fired, whether it succeeded or failed, so a later BeforePackage never retries a pre hook that already ran (a failed global/repo-pre means ops blocks everything under it per §9.1.4, and hooks must not undo that by re-firing).

func NewInvocation

func NewInvocation(action Action, r Runner, global GlobalScope) *Invocation

NewInvocation begins a hook invocation for one action, over the injected Runner and the global scope (its Dir supplies the global hooks dir and cwd).

func (*Invocation) AfterPackage

func (inv *Invocation) AfterPackage(pkg PackageScope) ([]Warning, error)

AfterPackage fires this package's package-post hook.

func (*Invocation) AfterRepo

func (inv *Invocation) AfterRepo(repo RepoScope) ([]Warning, error)

AfterRepo fires a repo's repo-post hook, at most once per repo (keyed by FQN). A second AfterRepo for the same repo is a no-op with no error.

func (*Invocation) BeforePackage

func (inv *Invocation) BeforePackage(pkg PackageScope) ([]Warning, error)

BeforePackage fires the pre hooks nesting down to one package, each at most once per invocation and in order: global-pre (first BeforePackage only), repo-pre (first BeforePackage for this repo, keyed by FQN.Repo()), then this package's package-pre. It stops at the first failure and returns it — the firing that failed is still marked fired, so a later BeforePackage will not retry it. Returned warnings are those newly incurred by directories this call discovered for the first time.

func (*Invocation) Finish

func (inv *Invocation) Finish() ([]Warning, error)

Finish fires the global-post hook, at most once, and only if the global scope ever activated — i.e. at least one BeforePackage happened. With no package having acted, global-pre never fired and global-post must not either (§9.1.5: nothing changed, so the global scope stays quiet).

type Level

type Level int

Level identifies which of the three hook scopes is firing (§9.1.2).

const (
	LevelPackage Level = iota
	LevelRepo
	LevelGlobal
)

func (Level) String

func (l Level) String() string

String yields the DSTOW_HOOK_LEVEL spelling (H2).

type PackageScope

type PackageScope struct {
	FQN     name.FQN // the package FQN
	Dir     string   // absolute package dir; also the hook cwd (H5)
	Target  string   // effective target root, absolute
	RepoDir string   // absolute repo dir
}

PackageScope carries a package-level firing's context (H2). Every field is caller-supplied; hooks derives the env values from it. Dir is the package directory and also the hook cwd (H5). The repo's FQN is not a field: a package FQN's repo is FQN.Repo() by construction, so carrying it separately could only ever disagree.

type Phase

type Phase int

Phase distinguishes the pre and post firing around an action (§9.1.1).

const (
	PhasePre Phase = iota
	PhasePost
)

func (Phase) String

func (p Phase) String() string

String yields the DSTOW_HOOK_PHASE spelling (H2).

type RepoScope

type RepoScope struct {
	FQN      name.FQN
	Dir      string     // absolute repo dir; also the hook cwd (H5)
	Packages []name.FQN // all packages acting under this repo
}

RepoScope carries a repo-level firing's context (H2). Dir is the repo directory and also the hook cwd (H5); Packages are all packages acting under this repo this invocation, for DSTOW_HOOK_PACKAGES.

type Runner

type Runner struct {
	Stdin  io.Reader // passed through to the hook (H6)
	Stderr io.Writer // both hook streams land here (H6)
}

Runner carries the injected streams a hook runs with (A4: only ui touches the process streams; hooks receives them). Both hook output streams land on Stderr (H6) — nothing a hook prints is dstow's answer to a question, so hook output is commentary definitionally — and Stdin passes through to the hook.

type Set

type Set map[Hook]string

Set maps each of the eight (Phase, Action) pairs that is present and executable to the absolute path of its hook file (M6). A pair with no present, executable file is simply absent from the map — its firing is a silent no-op.

type Warning

type Warning struct {
	Source string
	Detail string
	Fix    string
}

Warning is a discovery diagnostic as data (A4), mirroring config.Warning: hooks returns warnings, the caller decides when and how to print them. Source is the offending entry's path; Detail is complete prose; Fix, when set, is the O2 remedy line the caller renders after it.

Jump to

Keyboard shortcuts

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