Skip to content

Commit 86e0cd0

Browse files
authored
Merge pull request #138 from PyAutoLabs/feature/mind-guard-cd-fix
mind_commit_guard v1.2: honour a leading cd away from Mind
2 parents 8db0c8e + 771b34f commit 86e0cd0

2 files changed

Lines changed: 83 additions & 17 deletions

File tree

bin/mind_commit_guard.py

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727

2828
import json
2929
import os
30-
import re
3130
import shlex
3231
import sys
3332
from pathlib import Path
@@ -54,19 +53,6 @@ def _deny(reason: str) -> None:
5453
sys.exit(0)
5554

5655

57-
def _mind_root(command: str, cwd: str) -> Path | None:
58-
"""Best-effort resolution of the PyAutoMind checkout this command targets."""
59-
m = re.search(r"(?:-C\s+|cd\s+)(\S*PyAutoMind)\b", command)
60-
if m:
61-
return Path(os.path.expanduser(m.group(1)))
62-
if cwd and MIND_MARKER in cwd:
63-
p = Path(cwd)
64-
while p.name != MIND_MARKER and p != p.parent:
65-
p = p.parent
66-
return p if p.name == MIND_MARKER else None
67-
return None
68-
69-
7056
def _clauses(command: str):
7157
"""Token-level clause split that respects quoting (v1.1).
7258
@@ -94,25 +80,72 @@ def _clauses(command: str):
9480
yield clause
9581

9682

83+
def _under_mind(path: Path) -> Path | None:
84+
"""If ``path`` is inside a PyAutoMind checkout, return that checkout root;
85+
else None."""
86+
p = path
87+
while True:
88+
if p.name == MIND_MARKER:
89+
return p
90+
if p == p.parent:
91+
return None
92+
p = p.parent
93+
94+
95+
def _cd_target(tokens: list[str], cur: Path | None) -> Path | None:
96+
"""New effective cwd after a ``cd`` clause, or ``cur`` if not a plain cd.
97+
98+
Honouring a leading ``cd`` is what fixes the v1.1 false positive: a command
99+
that ``cd``s into PyAutoBuild before committing is NOT a Mind commit, even
100+
when the session's ambient cwd (what the hook is handed) is PyAutoMind.
101+
"""
102+
if not tokens or tokens[0] != "cd":
103+
return cur
104+
args = [t for t in tokens[1:] if not t.startswith("-")]
105+
if not args:
106+
return cur # `cd` with no path → home; unknowable, keep current
107+
dest = Path(os.path.expanduser(args[0]))
108+
if dest.is_absolute() or cur is None:
109+
return dest
110+
return cur / dest
111+
112+
97113
def check_command(command: str, cwd: str = "") -> str | None:
98114
"""Return a denial reason, or None to allow."""
99115
if "PYAUTO_SKIP_MIND_GUARD=1" in command:
100116
return None
101117
if "git" not in command or "commit" not in command:
102118
return None
103-
# Only reason about commands that clearly target PyAutoMind.
119+
# Cheap pre-filter: a Mind commit needs PyAutoMind named in the command
120+
# (a `cd`/`git -C` path) or in the ambient cwd. If neither, nothing to do.
104121
if MIND_MARKER not in command and MIND_MARKER not in (cwd or ""):
105122
return None
106-
root = _mind_root(command, cwd)
107123

108-
# Examine each clause of the (possibly compound) command at token level.
124+
effective_cwd: Path | None = Path(cwd) if cwd else None
125+
126+
# Walk clauses in order, tracking cwd so `cd`s before a commit are honoured.
109127
for tokens in _clauses(command):
128+
if tokens and tokens[0] == "cd":
129+
effective_cwd = _cd_target(tokens, effective_cwd)
130+
continue
110131
if "git" not in tokens or "commit" not in tokens:
111132
continue
112133
if tokens.index("git") > tokens.index("commit"):
113134
continue
114135
if "--amend" in tokens or "--dry-run" in tokens:
115136
continue
137+
# Which repo does THIS commit target? `git -C <path>` wins; else the
138+
# effective cwd. Only guard when that repo is a PyAutoMind checkout.
139+
target_dir = effective_cwd
140+
if "-C" in tokens:
141+
ci = tokens.index("-C")
142+
if ci + 1 < len(tokens):
143+
cpath = Path(os.path.expanduser(tokens[ci + 1]))
144+
target_dir = cpath if cpath.is_absolute() or effective_cwd is None else effective_cwd / cpath
145+
mind_root = _under_mind(target_dir) if target_dir else None
146+
if mind_root is None:
147+
continue # not a PyAutoMind commit — e.g. a PyAutoBuild worktree
148+
root = mind_root
116149
if "--" not in tokens:
117150
return (
118151
"PyAutoMind is a SHARED checkout: concurrent sessions stage into "

tests/test_mind_commit_guard.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,36 @@ def test_bare_commit_still_denied_in_compound_with_quotes(tmp_path):
111111
mind.mkdir()
112112
cmd = f'cd {mind} && git commit -q -m "msg; with semicolon" && echo ok'
113113
assert check_command(cmd) is not None
114+
115+
116+
# --- v1.2: honour a `cd` away from Mind (false positive on a PyAutoBuild commit) ---
117+
def test_cd_to_other_repo_before_commit_is_allowed():
118+
# The 2026-07-17 false positive: session cwd was PyAutoMind (what the hook
119+
# is handed) but the command cd's to a PyAutoBuild worktree first. That is
120+
# NOT a Mind commit — a bare `git commit` there is fine.
121+
r = check_command(
122+
'cd /home/x/wt/PyAutoBuild && git add a && git commit -m "m"',
123+
cwd="/home/x/PyAutoMind",
124+
)
125+
assert r is None
126+
127+
128+
def test_git_dash_C_to_other_repo_from_mind_cwd_is_allowed():
129+
r = check_command(
130+
'git -C /home/x/wt/PyAutoBuild commit -m "m"', cwd="/home/x/PyAutoMind"
131+
)
132+
assert r is None
133+
134+
135+
def test_cd_into_mind_then_bare_commit_still_denied():
136+
r = check_command(
137+
'cd /home/x/PyAutoMind && git commit -m "m"', cwd="/tmp/elsewhere"
138+
)
139+
assert r is not None
140+
141+
142+
def test_git_dash_C_into_mind_from_other_cwd_still_denied():
143+
r = check_command(
144+
'git -C /home/x/PyAutoMind commit -m "m"', cwd="/home/x/wt/PyAutoBuild"
145+
)
146+
assert r is not None

0 commit comments

Comments
 (0)