Documentation
¶
Overview ¶
Package githooks provides utilities shared between the mdsmith CLI and the git-hook-sync rule for managing the pre-merge-commit hook, merge-driver assignments in .gitattributes, and discovery of files that contain generated-section directives.
Index ¶
- Constants
- func BuildHookScript(exe string) string
- func DefaultIncludes() []string
- func DiscoverFiles(repoRoot string, maxBytes int64) []string
- func DiscoverFilesForInstall(repoRoot string, maxBytes int64) []string
- func EnableRuleSnippet(ruleName string) string
- func ExtractGitattributesFiles(content string) []string
- func ExtractHookFiles(content string) []string
- func FilesMatch(a, b []string) bool
- func GitRepoRoot(dir string) (string, error)
- func GlobsEqual(a, b Globs) bool
- func HasMdsmithMergeDriver(repoRoot string) bool
- func HookMatchesCanonical(hook string) bool
- func NormalizeManagedPath(repoRoot, p string) (string, error)
- func NormalizeManagedPaths(repoRoot string, paths []string) ([]string, error)
- func RenderManagedBlock(globs Globs) string
- func ResolveHooksDir(repoRoot string) string
- func StageGitattributes(repoRoot string) error
- func WriteGitattributes(path string, globs Globs) error
- type Globs
Constants ¶
const PreMergeCommitMarker = "# mdsmith merge-driver pre-merge-commit hook"
PreMergeCommitMarker is the comment line written into the pre-merge-commit hook so that mdsmith (and the git-hook-sync rule) can recognise hooks it manages without stomping on user-authored hooks of the same name.
Variables ¶
This section is empty.
Functions ¶
func BuildHookScript ¶ added in v0.9.0
BuildHookScript returns the canonical pre-merge-commit hook content. The script runs `mdsmith fix` once on the entire repo after git resolves every per-file merge, so generated sections reflect the final merged state. mdsmith fix walks the worktree respecting `.mdsmith.yml` ignore patterns, matching the same set of files marked with `merge=mdsmith` in `.gitattributes`. Modified markdown files are then staged so the merge commit captures them.
The script embeds the absolute path of the mdsmith binary, so one line is machine-specific. The rule's drift detection therefore re-renders the canonical template and validates the stable hook lines (chdir, fix invocation, staging) rather than requiring a full byte-for-byte match.
`mdsmith fix` exit code 1 means unfixed diagnostics remain — the hook still allows the merge to proceed in that case so reviewers can resolve the remaining issues in a follow-up commit. Any other non-zero exit (e.g. config errors, panics, exit 2) is propagated out of the hook so the merge commit aborts on genuine errors.
The staging loop reads `git diff --name-only` newline-by-newline inside a POSIX `while read` loop. `xargs -r` is a GNU extension (BSD xargs on macOS does not support it), so an empty pipeline would otherwise invoke `git add --` with no arguments and abort the merge. The loop also avoids splitting on filename whitespace (read uses IFS= -r) at the cost of mishandling the rare filename that contains literal newlines — an acceptable trade for portability.
func DefaultIncludes ¶ added in v0.9.0
func DefaultIncludes() []string
DefaultIncludes is the canonical include pattern set: every markdown extension mdsmith processes. Kept as a function so callers always get a fresh slice rather than sharing a package-level value.
func DiscoverFiles ¶
DiscoverFiles scans repoRoot for Markdown files containing a generated-section directive (catalog, include, toc, …). Returned paths are relative to repoRoot and use forward-slash separators on every platform so they compare correctly against entries written into .gitattributes and the pre-merge-commit hook.
Hidden directories (names starting with ".") are skipped. The returned slice is sorted and may be empty: the caller decides whether to apply a fallback (the install commands do; the git-hook-sync rule does not).
func DiscoverFilesForInstall ¶
DiscoverFilesForInstall is the install-time variant of DiscoverFiles that supplies a sensible default file list when the repository has no directive-bearing files. It returns ["PLAN.md", "README.md"] in that case so a fresh repo still gets a useful hook/.gitattributes configuration after `mdsmith merge-driver install` or `mdsmith pre-merge-commit install`.
The git-hook-sync rule must not use this variant: when the user has no directive-bearing files, the rule should report nothing rather than reference fictional PLAN.md/README.md paths.
func EnableRuleSnippet ¶
EnableRuleSnippet returns the YAML the user can paste into .mdsmith.yml to enable the given rule. mdsmith never rewrites the user's config file automatically; the snippet is printed instead.
func ExtractGitattributesFiles ¶
ExtractGitattributesFiles returns the list of paths assigned to the mdsmith merge driver in .gitattributes content. Each entry is the pathname token from a line of the form `<pathname> merge=mdsmith`. Comment lines (`#`) and lines without a `merge=mdsmith` attribute are ignored.
The parser splits on whitespace, so it does not support pathnames that themselves contain whitespace. NormalizeManagedPath rejects such paths at install time so the installer and the drift checker stay consistent.
func ExtractHookFiles ¶
ExtractHookFiles parses a pre-merge-commit hook script and returns the list of files it invokes `mdsmith fix --` on. Files appear in the order they occur in the hook. Each `fix --` line contributes at most one entry: the first single-quoted token that follows. Comment and blank lines are skipped so a commented-out example or note in the hook does not produce a false managed-file entry.
func FilesMatch ¶
FilesMatch reports whether a and b contain the same set of files, ignoring order and duplicates. A repeated entry on either side is treated the same as a single occurrence so that a `.gitattributes` or hook script that lists the same path twice still compares equal to a deduplicated list.
func GitRepoRoot ¶
GitRepoRoot returns the absolute path of the git repository that contains dir. The lookup runs `git -C dir rev-parse --show-toplevel` so it works correctly when invoked from any subdirectory or when linting an absolute path outside the process working directory.
func GlobsEqual ¶ added in v0.9.0
GlobsEqual reports whether two glob sets are identical. Comparison is order-sensitive because .gitattributes uses last-match-wins: reordering Include vs Exclude (or shuffling Exclude entries that might overlap) changes which paths the merge driver applies to.
func HasMdsmithMergeDriver ¶
HasMdsmithMergeDriver reports whether the repository's local git config defines `merge.mdsmith.driver` (i.e. the merge driver itself has been registered for this repo). The lookup is scoped to the repo's local config (`--local`), not global/system config, so a user with a personal merge driver elsewhere cannot accidentally opt every clone into MDS048's drift checks. A missing driver is reported as false rather than as an error so callers can treat the merge-driver setup as "not installed".
func HookMatchesCanonical ¶ added in v0.9.0
HookMatchesCanonical reports whether hook content looks like the current glob-based pre-merge-commit template. The mdsmith binary path is repo-specific, so canonical comparison checks for the stable lines that carry the runtime behaviour: cd to the repo root, run `mdsmith fix .` inside the exit-1-tolerant guard, and stage modified markdown files via the POSIX `while read` loop. Both the CLI status output and the git-hook-sync rule call this so they cannot disagree on what counts as in-sync.
Required fragments are matched only on non-comment lines so a drifted hook with the canonical commands sitting in a comment (or otherwise inert text) is reliably detected as drift.
func NormalizeManagedPath ¶
NormalizeManagedPath converts p (which may be absolute, relative, or use OS-specific separators) into the canonical form used in .gitattributes and the pre-merge-commit hook: a non-empty repo-relative path with forward-slash separators that does not escape repoRoot.
Whitespace inside the *resulting* repo-relative path is rejected because .gitattributes splits attributes on whitespace and the rule's Fields-based parser cannot recover the original token. The check runs after Rel/ToSlash so an absolute input rooted at a repo whose own path contains whitespace (e.g. a Windows or macOS home dir with spaces) is still accepted, as long as the repo-relative tail is whitespace-free.
Glob and pathspec metacharacters (`*`, `?`, `[`) are also rejected. The install commands write each managed entry into a `[ -e <path> ]` guard inside the pre-merge-commit hook script, and `[ -e ]` treats its argument as a literal filename rather than a glob, so a pattern like `docs/*.md` would always be skipped even when files match. The drift checker likewise compares exact paths.
func NormalizeManagedPaths ¶
NormalizeManagedPaths normalizes each entry via NormalizeManagedPath. It returns the first error encountered, so callers can surface a single clear message rather than a list of failures.
func RenderManagedBlock ¶ added in v0.9.0
RenderManagedBlock returns the .gitattributes managed block content for globs, including the BEGIN/END markers and a trailing newline. Output is deterministic so drift detection compares it byte-for-byte against the installed block.
func ResolveHooksDir ¶
ResolveHooksDir returns the directory where git hooks live for the repository at repoRoot. It uses `git rev-parse --git-path hooks` so that worktrees, submodules, and core.hooksPath all resolve correctly. Falls back to <repoRoot>/.git/hooks when git cannot be queried.
func StageGitattributes ¶ added in v0.9.0
StageGitattributes runs `git add -- .gitattributes` against repoRoot so updates written by Fix end up in the index. Without this, the pre-merge-commit hook flow stages only the markdown file passed to `mdsmith fix`, leaving the regenerated .gitattributes in the working tree but absent from the resulting merge commit. Errors are surfaced so callers can decide whether to roll back; the working-tree write itself is already done at the point this is called. CombinedOutput is used so git's stderr (e.g. `fatal: Unable to create '/.../.git/index.lock': File exists.`) is preserved in the error returned to the caller — without it MDS048's "staging failed" diagnostic would only carry an `exit status N` and nothing actionable.
func WriteGitattributes ¶ added in v0.9.0
WriteGitattributes updates .gitattributes to assign the mdsmith merge driver to the patterns described by globs. It preserves all non-mdsmith entries and replaces only the BEGIN/END managed block. Stray `merge=mdsmith` lines outside the managed block (left behind by older append-only installs or hand-edited files) are removed so the resulting file matches globs exactly.
If the file does not exist, it is created with only the managed block. If the file exists but has no managed block, one is appended. If a managed block exists, it is replaced.
This approach ensures that other .gitattributes entries (e.g. text, eol=lf, linguist settings, other merge drivers) are never dropped.
Types ¶
type Globs ¶ added in v0.9.0
Globs describes the set of paths the mdsmith merge driver applies to. Each Include pattern is written as `<pattern> merge=mdsmith` and each Exclude pattern is written after them as `<pattern> -merge`. .gitattributes uses last-match-wins, so an exclude line after the include lines effectively removes the merge driver from any path the include patterns matched.
`.gitattributes` itself does not support negative patterns (`!*.md` is a syntax error there). Order-sensitive override via -merge is the supported way to express exclusions, which is why Globs keeps Include and Exclude as separate ordered slices.
func ExtractGlobs ¶ added in v0.9.0
ExtractGlobs parses the managed block from .gitattributes content and returns the include and exclude patterns. The second return is true when a managed block was found. Content outside the BEGIN/END markers is ignored — stale `merge=mdsmith` lines outside the block are handled by stripStaleMergeMdsmithLines at write time.
func GlobsFromConfig ¶ added in v0.9.0
GlobsFromConfig returns the canonical merge-driver glob set for a repository: every markdown extension is included, and the project's .mdsmith.yml ignore patterns are translated as exclude patterns. Last-match-wins in .gitattributes lets the excludes override the broader markdown includes. cfg may be nil (no exclusions then).
Patterns that cannot be represented directly in .gitattributes are dropped from the exclude set so MDS048's auto-fix never produces a broken managed block:
- .gitattributes splits attribute lines on whitespace, so a pattern containing a space or tab would be parsed as a path plus a stray attribute.
- .gitattributes does not support `!`-prefixed negation. A pattern like `!docs/*.md` written verbatim would be silently ignored by git (or treated as a literal path starting with `!`), which is misleading.
The returned `skipped` slice lists any ignore patterns that were dropped, in input order. Callers that have an error channel (notably the install commands) surface them on stderr; the rule's auto-fix path silently discards the list because it runs per-file and would otherwise flood diagnostic output.
func LoadGlobs ¶ added in v0.9.0
LoadGlobs reads .mdsmith.yml from repoRoot and returns the merge- driver glob set. A missing or unparseable config falls back to the default include set with no exclusions. Skipped (unrepresentable) ignore patterns are silently discarded — callers that need to surface them should use GlobsFromConfig directly.