Skip to content
Merged
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

**Collective memory, encrypted.**

[![tests: 457](https://img.shields.io/badge/tests-457-brightgreen?style=flat)](test/)
[![tests: 459](https://img.shields.io/badge/tests-459-brightgreen?style=flat)](test/)
![lints: 8](https://img.shields.io/badge/lints-8-blue?style=flat)
[![license: MIT](https://img.shields.io/badge/license-MIT-blue?style=flat)](LICENSE)

Expand Down
46 changes: 31 additions & 15 deletions lib/hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,27 @@ _render_notes_hook_template() {
"$template"
}

# Ensure a hook dispatcher is installed for the given hook type.
_active_git_hooks_dir() {
local repo_root hooks_dir
repo_root=$(git -C "$TARGET_DIR" rev-parse --show-toplevel) || return 1
hooks_dir=$(git -C "$TARGET_DIR" rev-parse --git-path hooks) || return 1
case "$hooks_dir" in
/*) printf '%s\n' "$hooks_dir" ;;
*) printf '%s/%s\n' "$repo_root" "$hooks_dir" ;;
esac
}

# Ensure the exact Notes dispatcher is installed in Git's active hooks path.
# Usage: ensure_hook_dispatcher <pre-commit|post-commit|post-merge|post-checkout>
ensure_hook_dispatcher() {
local hook_type="${1:?usage: ensure_hook_dispatcher <pre-commit|post-commit|post-merge|post-checkout>}"
local hooks_dir="$TARGET_DIR/.git/hooks"
local hooks_dir
hooks_dir=$(_active_git_hooks_dir) || return 1
local dispatcher="$hooks_dir/$hook_type"

mkdir -p "$hooks_dir/${hook_type}.d"

# Only install if the dispatcher isn't already in place
if ! grep -q "${hook_type}.d" "$dispatcher" 2>/dev/null; then
if ! cmp -s "$HOOKS_DIR/dispatcher" "$dispatcher"; then
cp "$HOOKS_DIR/dispatcher" "$dispatcher"
chmod +x "$dispatcher"
fi
Expand All @@ -48,7 +58,9 @@ ensure_hook_dispatcher() {
# installed it rather than whichever `notes` command appears first on PATH.
install_encryption_hook() {
ensure_hook_dispatcher pre-commit
local target="$TARGET_DIR/.git/hooks/pre-commit.d/encryption"
local hooks_dir
hooks_dir=$(_active_git_hooks_dir) || return 1
local target="$hooks_dir/pre-commit.d/encryption"
_render_notes_hook_template "$HOOKS_DIR/encryption.template" "." > "$target"
chmod +x "$target"
}
Expand All @@ -58,7 +70,9 @@ install_encryption_hook() {
install_obfuscation_hook() {
local notes_dir="${1:-notes}"
ensure_hook_dispatcher pre-commit
local target="$TARGET_DIR/.git/hooks/pre-commit.d/obfuscation"
local hooks_dir
hooks_dir=$(_active_git_hooks_dir) || return 1
local target="$hooks_dir/pre-commit.d/obfuscation"
_render_notes_hook_template "$HOOKS_DIR/obfuscation.template" "$notes_dir" > "$target"
chmod +x "$target"
}
Expand All @@ -69,9 +83,11 @@ install_obfuscation_hook() {
install_double_tracking_hook() {
local notes_dir="${1:-notes}"
ensure_hook_dispatcher pre-commit
local hooks_dir
hooks_dir=$(_active_git_hooks_dir) || return 1
# Name sorts after `obfuscation` so the auto-obfuscation hook can fix a
# naively-staged readable before this guard runs.
local target="$TARGET_DIR/.git/hooks/pre-commit.d/verify-double-tracking"
local target="$hooks_dir/pre-commit.d/verify-double-tracking"
_render_notes_hook_template "$HOOKS_DIR/verify-double-tracking.template" "$notes_dir" > "$target"
chmod +x "$target"
}
Expand Down Expand Up @@ -100,17 +116,15 @@ _installed_hook_matches() {

required_pre_commit_hooks_ready() {
local notes_dir="${1:-notes}"
local hooks_dir="$TARGET_DIR/.git/hooks"
local hooks_dir
hooks_dir=$(_active_git_hooks_dir) || return 1
local dispatcher="$hooks_dir/pre-commit"
local fragments="$hooks_dir/pre-commit.d"
local obfuscation_hook="$fragments/obfuscation"
local installed_mise_bin

[ -x "$dispatcher" ] || return 1
if ! cmp -s "$HOOKS_DIR/dispatcher" "$dispatcher" \
&& ! grep -qF 'pre-commit.d' "$dispatcher" 2>/dev/null; then
return 1
fi
cmp -s "$HOOKS_DIR/dispatcher" "$dispatcher" || return 1
[ -x "$obfuscation_hook" ] || return 1
installed_mise_bin=$(sed -n 's/^MISE_BIN="\(.*\)"$/\1/p' \
"$obfuscation_hook")
Expand Down Expand Up @@ -151,25 +165,27 @@ install_manifest_merge_driver() {
# After a branch checkout changes the manifest, post-checkout reconciles stale names.
install_deobfuscation_hook() {
local notes_dir="${1:-notes}"
local hooks_dir
hooks_dir=$(_active_git_hooks_dir) || return 1
local commit_template="$HOOKS_DIR/post-commit-deobfuscate.template"
local merge_template="$HOOKS_DIR/post-merge-deobfuscate.template"
local checkout_template="$HOOKS_DIR/post-checkout-deobfuscate.template"

# Install for post-commit (deobfuscate after committing)
ensure_hook_dispatcher post-commit
local target="$TARGET_DIR/.git/hooks/post-commit.d/deobfuscation"
local target="$hooks_dir/post-commit.d/deobfuscation"
_render_notes_hook_template "$commit_template" "$notes_dir" > "$target"
chmod +x "$target"

# Install for post-merge (deobfuscate after pulling)
ensure_hook_dispatcher post-merge
local merge_target="$TARGET_DIR/.git/hooks/post-merge.d/deobfuscation"
local merge_target="$hooks_dir/post-merge.d/deobfuscation"
_render_notes_hook_template "$merge_template" "$notes_dir" > "$merge_target"
chmod +x "$merge_target"

# Install for post-checkout (deobfuscate after branch checkout)
ensure_hook_dispatcher post-checkout
local checkout_target="$TARGET_DIR/.git/hooks/post-checkout.d/deobfuscation"
local checkout_target="$hooks_dir/post-checkout.d/deobfuscation"
_render_notes_hook_template "$checkout_template" "$notes_dir" > "$checkout_target"
chmod +x "$checkout_target"
}
44 changes: 44 additions & 0 deletions test/stage.bats
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,50 @@ setup() {
[ -z "$output" ]
}

@test "notes stage refuses a dispatcher that does not run required fragments" {
cat > "$NOTES_CALLER_PWD/.git/hooks/pre-commit" <<'HOOK'
#!/usr/bin/env bash
# Looks compatible by mentioning pre-commit.d, but executes nothing.
exit 0
HOOK
chmod +x "$NOTES_CALLER_PWD/.git/hooks/pre-commit"
echo "# Alpha modified" > "$NOTES_CALLER_PWD/notes/alpha.md"

run notes stage alpha.md

[ "$status" -ne 0 ]
[[ "$output" == *"pre-commit hooks are missing or stale"* ]]
run git -C "$NOTES_CALLER_PWD" diff --cached --name-only
[ -z "$output" ]

notes install-hooks --yes
run notes stage alpha.md
[ "$status" -eq 0 ]
git -C "$NOTES_CALLER_PWD" commit -q -m "repaired dispatcher"
run git -C "$NOTES_CALLER_PWD" show --name-only --format= HEAD
[[ "$output" != *"notes/alpha.md"* ]]
}

@test "notes stage checks and repairs the active core.hooksPath" {
git -C "$NOTES_CALLER_PWD" config core.hooksPath .active-hooks
echo "# Alpha modified" > "$NOTES_CALLER_PWD/notes/alpha.md"

run notes stage alpha.md

[ "$status" -ne 0 ]
[[ "$output" == *"pre-commit hooks are missing or stale"* ]]
run git -C "$NOTES_CALLER_PWD" diff --cached --name-only
[ -z "$output" ]

notes install-hooks --yes
[ -x "$NOTES_CALLER_PWD/.active-hooks/pre-commit" ]
run notes stage alpha.md
[ "$status" -eq 0 ]
git -C "$NOTES_CALLER_PWD" commit -q -m "active hooks path"
run git -C "$NOTES_CALLER_PWD" show --name-only --format= HEAD
[[ "$output" != *"notes/alpha.md"* ]]
}

@test "notes stage dry-run remains available without required hooks" {
rm -rf "$NOTES_CALLER_PWD/.git/hooks/pre-commit" \
"$NOTES_CALLER_PWD/.git/hooks/pre-commit.d"
Expand Down
Loading