|

feat: implement git-in-hg blocking hook (passes basic cases)

Author: Chris Tusa <chris.tusa@leafscale.com>
Date: May 03, 2026 13:13
Changeset: 2449115e2d9b0fd9b170ae6cd47d5778aac61fad
Branch: default

Changed files:

Diff

diff -r 2c2381f618b2 -r 2449115e2d9b hooks/block-git-in-hg.sh
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/hooks/block-git-in-hg.sh	Sun May 03 13:13:22 2026 -0500
@@ -0,0 +1,99 @@
+#!/usr/bin/env bash
+# PreToolUse hook: blocks `git` invocations when the working directory
+# is inside a Mercurial (.hg) repository.
+#
+# Reads tool-call JSON from stdin, exits 0 to allow, exits 2 with a
+# message on stderr to block.
+
+set -u
+
+# Fail open if jq is missing — better to allow a command than to silently
+# block all bash for a missing dependency.
+if ! command -v jq >/dev/null 2>&1; then
+    echo "isurus-hg hook: 'jq' not found; allowing command. Install jq to enable git-blocking." >&2
+    exit 0
+fi
+
+input=$(cat)
+cmd=$(jq -r '.tool_input.command // ""' <<<"$input")
+cwd=$(jq -r '.cwd // ""' <<<"$input")
+
+# Allow if no command extracted.
+if [[ -z "$cmd" ]]; then
+    exit 0
+fi
+
+# Strip leading whitespace and any leading VAR=value env assignments.
+trimmed=$(sed -E 's/^[[:space:]]+//' <<<"$cmd")
+while [[ "$trimmed" =~ ^[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+ ]]; do
+    trimmed=$(sed -E 's/^[A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+//' <<<"$trimmed")
+done
+
+# Extract the leading word.
+first=$(awk '{print $1}' <<<"$trimmed")
+if [[ "$first" != "git" ]]; then
+    exit 0
+fi
+
+# Find VCS context for cwd: the closest .hg or .git ancestor.
+# If there's no usable cwd, allow.
+if [[ -z "$cwd" || ! -d "$cwd" ]]; then
+    exit 0
+fi
+
+dir=$cwd
+hg_root=""
+git_root=""
+while [[ "$dir" != "/" && -n "$dir" ]]; do
+    if [[ -z "$hg_root" && -d "$dir/.hg" ]]; then
+        hg_root=$dir
+    fi
+    if [[ -z "$git_root" && -d "$dir/.git" ]]; then
+        git_root=$dir
+    fi
+    if [[ -n "$hg_root" || -n "$git_root" ]]; then
+        break
+    fi
+    dir=$(dirname "$dir")
+done
+
+# If a .git was found anywhere up the walk, allow. The loop breaks at
+# the first iteration where either is found, so a non-empty git_root means
+# .git is at the same level as .hg or closer — either way, the user is in
+# a git context.
+if [[ -n "$git_root" ]]; then
+    exit 0
+fi
+
+# No .hg ancestor => allow (e.g. plain `git --version` outside any repo).
+if [[ -z "$hg_root" ]]; then
+    exit 0
+fi
+
+# We are inside an .hg repo and the command starts with `git`. Block.
+# Build a suggestion based on the next word.
+sub=$(awk '{print $2}' <<<"$trimmed")
+case "$sub" in
+    status)        suggest="hg status" ;;
+    add)           suggest="hg add" ;;
+    commit)        suggest="hg commit -m \"...\" (no -u; identity comes from ~/.hgrc)" ;;
+    log)           suggest="hg log" ;;
+    diff)          suggest="hg diff" ;;
+    branch)        suggest="hg bookmarks  (in this workflow, bookmarks are feature branches)" ;;
+    checkout)      suggest="hg update <name>" ;;
+    pull)          suggest="hg pull -u" ;;
+    push)          suggest="hg push -B <bookmark>" ;;
+    blame)         suggest="hg annotate" ;;
+    *)             suggest="See the mercurial-basics skill for the full command map." ;;
+esac
+
+cat >&2 <<EOF
+git command blocked: this is a Mercurial repository (.hg/ found at $hg_root).
+
+You ran:    $trimmed
+Use:        $suggest
+
+See the mercurial-basics skill for the full command map.
+EOF
+
+exit 2