How the post-receive Hook Resolves Absolute Gate Paths in no-mistakes
The post-receive hook in no-mistakes resolves absolute gate paths by first checking Git's core.hooksPath configuration or falling back to the script's own directory, then delegating final normalization to the daemon's normalizeNotifyGatePath function to handle relative inputs and symlinks.
The no-mistakes repository relies on a post-receive hook to notify the daemon which gate repository triggered a push. Since Git may invoke hooks from ambiguous working directories—where $(pwd) resolves to . for bare repositories as documented in issue #269—the system must derive an absolute gate path before the daemon can reliably match the push to its pipeline.
Why Bare Repositories Break Relative Path Resolution
When Git executes hooks in a bare repository, the current working directory often resolves ambiguously. In internal/git/hook.go, the developers documented that relying on $(pwd) alone produces unreliable results, particularly when the hook runs from paths like . that lack absolute context. This ambiguity prevents the daemon from correctly identifying which gate should trigger the pipeline, causing silent failures described in issue #269.
Step 1: Hook-Side Resolution Using core.hooksPath
The hook generation logic in internal/git/hook.go implements a two-tier fallback strategy inside the PostReceiveHookScript function. First, it attempts to read Git's core.hooksPath configuration relative to the repository's git directory. If that value is unset or unusable, it falls back to the directory containing the hook script itself using $(dirname "$0").
This approach guarantees that the hook always derives an absolute directory pointing directly at the gate repository, regardless of where Git happens to invoke the script. The InstallPostReceiveHook function then writes this logic into the actual hook file.
#!/bin/sh
# no-mistakes post-receive hook
# Resolve gate directory:
# 1. Prefer git's core.hooksPath if configured
# 2. Fallback to the directory of this script
# 3. Convert to an absolute path
gate_dir="$(git -C "$(dirname "$0")/.." config --get core.hooksPath 2>/dev/null)"
if [ -z "$gate_dir" ]; then
gate_dir="$(dirname "$0")"
fi
gate_dir="$(cd "$gate_dir" && pwd)"
# Notify the daemon (non-blocking)
"$NM_BIN" daemon notify-push --gate "$gate_dir" &
Step 2: Daemon-Side Normalization via normalizeNotifyGatePath
When the daemon receives the push notification via daemon notify-push --gate <path>, it runs normalizeNotifyGatePath defined in internal/cli/daemon_cmd.go. This function performs two critical validation steps: it checks whether the supplied path is absolute using filepath.IsAbs, and if relative, resolves it against the daemon's current working directory via filepath.Abs.
Finally, it canonicalizes the result through filepath.EvalSymlinks to eliminate any symbolic links or redundant path elements.
func normalizeNotifyGatePath(gate string) (string, error) {
// If the user supplied a relative path (e.g. "."), resolve it.
if !filepath.IsAbs(gate) {
abs, err := filepath.Abs(gate)
if err != nil {
return "", err
}
gate = abs
}
// Clean up any symlinks / redundant elements.
return filepath.EvalSymlinks(gate)
}
Step 3: Supporting Legacy Hook Installations
The system maintains backward compatibility with older hook versions that may pass relative --gate arguments such as .. The daemon's normalizer, as tested in internal/cli/daemon_cmd_test.go and internal/git/hook_test.go, automatically converts these legacy values into absolute paths. This prevents the "pipeline silently never starts" bug originally reported in issue #269, ensuring that even repaired or partially upgraded gates function correctly without requiring immediate hook reinstallation.
Summary
- The
PostReceiveHookScriptininternal/git/hook.goderives an initial absolute path by checkingcore.hooksPathor falling back to$(dirname "$0"). - The
normalizeNotifyGatePathfunction ininternal/cli/daemon_cmd.goconverts any relative daemon arguments to absolute paths and resolves symlinks viafilepath.EvalSymlinks. - Legacy hooks passing relative
--gatevalues remain supported through automatic normalization in the daemon. - Together, these mechanisms ensure reliable pipeline triggering regardless of Git's invocation directory.
Frequently Asked Questions
What happens if core.hooksPath is not configured?
If core.hooksPath is unset or returns an empty value, the hook script falls back to using $(dirname "$0") to locate the gate directory, as implemented in internal/git/hook.go. This fallback ensures the hook always produces a usable absolute path even in default Git configurations.
Why does the daemon need to normalize the path if the hook already resolved it?
The daemon normalizes paths to handle edge cases where legacy hooks pass relative values like . and to eliminate symbolic links that might cause path comparison failures. This double-validation ensures the daemon's internal gate registry matches the actual filesystem location exactly.
How does no-mistakes handle symbolic links in gate paths?
The normalizeNotifyGatePath function calls filepath.EvalSymlinks to resolve any symbolic links in the final absolute path. This canonicalization prevents scenarios where the same gate could be referenced through multiple path aliases, ensuring consistent pipeline association.
Where is the fix for issue #269 implemented?
The resolution for issue #269 spans both internal/git/hook.go where the hook generates absolute paths, and internal/cli/daemon_cmd.go where the daemon validates them. The fix is documented in CHANGELOG.md under versions addressing issues #269 and #358.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →