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
67 changes: 50 additions & 17 deletions bin/mind_commit_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@

import json
import os
import re
import shlex
import sys
from pathlib import Path
Expand All @@ -54,19 +53,6 @@ def _deny(reason: str) -> None:
sys.exit(0)


def _mind_root(command: str, cwd: str) -> Path | None:
"""Best-effort resolution of the PyAutoMind checkout this command targets."""
m = re.search(r"(?:-C\s+|cd\s+)(\S*PyAutoMind)\b", command)
if m:
return Path(os.path.expanduser(m.group(1)))
if cwd and MIND_MARKER in cwd:
p = Path(cwd)
while p.name != MIND_MARKER and p != p.parent:
p = p.parent
return p if p.name == MIND_MARKER else None
return None


def _clauses(command: str):
"""Token-level clause split that respects quoting (v1.1).

Expand Down Expand Up @@ -94,25 +80,72 @@ def _clauses(command: str):
yield clause


def _under_mind(path: Path) -> Path | None:
"""If ``path`` is inside a PyAutoMind checkout, return that checkout root;
else None."""
p = path
while True:
if p.name == MIND_MARKER:
return p
if p == p.parent:
return None
p = p.parent


def _cd_target(tokens: list[str], cur: Path | None) -> Path | None:
"""New effective cwd after a ``cd`` clause, or ``cur`` if not a plain cd.

Honouring a leading ``cd`` is what fixes the v1.1 false positive: a command
that ``cd``s into PyAutoBuild before committing is NOT a Mind commit, even
when the session's ambient cwd (what the hook is handed) is PyAutoMind.
"""
if not tokens or tokens[0] != "cd":
return cur
args = [t for t in tokens[1:] if not t.startswith("-")]
if not args:
return cur # `cd` with no path → home; unknowable, keep current
dest = Path(os.path.expanduser(args[0]))
if dest.is_absolute() or cur is None:
return dest
return cur / dest


def check_command(command: str, cwd: str = "") -> str | None:
"""Return a denial reason, or None to allow."""
if "PYAUTO_SKIP_MIND_GUARD=1" in command:
return None
if "git" not in command or "commit" not in command:
return None
# Only reason about commands that clearly target PyAutoMind.
# Cheap pre-filter: a Mind commit needs PyAutoMind named in the command
# (a `cd`/`git -C` path) or in the ambient cwd. If neither, nothing to do.
if MIND_MARKER not in command and MIND_MARKER not in (cwd or ""):
return None
root = _mind_root(command, cwd)

# Examine each clause of the (possibly compound) command at token level.
effective_cwd: Path | None = Path(cwd) if cwd else None

# Walk clauses in order, tracking cwd so `cd`s before a commit are honoured.
for tokens in _clauses(command):
if tokens and tokens[0] == "cd":
effective_cwd = _cd_target(tokens, effective_cwd)
continue
if "git" not in tokens or "commit" not in tokens:
continue
if tokens.index("git") > tokens.index("commit"):
continue
if "--amend" in tokens or "--dry-run" in tokens:
continue
# Which repo does THIS commit target? `git -C <path>` wins; else the
# effective cwd. Only guard when that repo is a PyAutoMind checkout.
target_dir = effective_cwd
if "-C" in tokens:
ci = tokens.index("-C")
if ci + 1 < len(tokens):
cpath = Path(os.path.expanduser(tokens[ci + 1]))
target_dir = cpath if cpath.is_absolute() or effective_cwd is None else effective_cwd / cpath
mind_root = _under_mind(target_dir) if target_dir else None
if mind_root is None:
continue # not a PyAutoMind commit — e.g. a PyAutoBuild worktree
root = mind_root
if "--" not in tokens:
return (
"PyAutoMind is a SHARED checkout: concurrent sessions stage into "
Expand Down
33 changes: 33 additions & 0 deletions tests/test_mind_commit_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,3 +111,36 @@ def test_bare_commit_still_denied_in_compound_with_quotes(tmp_path):
mind.mkdir()
cmd = f'cd {mind} && git commit -q -m "msg; with semicolon" && echo ok'
assert check_command(cmd) is not None


# --- v1.2: honour a `cd` away from Mind (false positive on a PyAutoBuild commit) ---
def test_cd_to_other_repo_before_commit_is_allowed():
# The 2026-07-17 false positive: session cwd was PyAutoMind (what the hook
# is handed) but the command cd's to a PyAutoBuild worktree first. That is
# NOT a Mind commit — a bare `git commit` there is fine.
r = check_command(
'cd /home/x/wt/PyAutoBuild && git add a && git commit -m "m"',
cwd="/home/x/PyAutoMind",
)
assert r is None


def test_git_dash_C_to_other_repo_from_mind_cwd_is_allowed():
r = check_command(
'git -C /home/x/wt/PyAutoBuild commit -m "m"', cwd="/home/x/PyAutoMind"
)
assert r is None


def test_cd_into_mind_then_bare_commit_still_denied():
r = check_command(
'cd /home/x/PyAutoMind && git commit -m "m"', cwd="/tmp/elsewhere"
)
assert r is not None


def test_git_dash_C_into_mind_from_other_cwd_still_denied():
r = check_command(
'git -C /home/x/PyAutoMind commit -m "m"', cwd="/home/x/wt/PyAutoBuild"
)
assert r is not None