From e064de7c9dfdc3b8b826bf726a98c4f96cef9e51 Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Sat, 24 Oct 2026 23:09:34 +0400 Subject: [PATCH] =?UTF-8?q?fix(hooks):=20wt-=D0=BF=D0=B0=D1=80=D1=8B=20?= =?UTF-8?q?=E2=80=94=20=D0=BF=D0=BE=D0=B7=D0=B8=D1=86=D0=B8=D1=8F,=20switc?= =?UTF-8?q?h-=D0=BF=D1=80=D0=B8=D0=BE=D1=80=D0=B8=D1=82=D0=B5=D1=82,=20?= =?UTF-8?q?=D0=BA=D0=BE=D1=80=D0=B5=D0=BD=D1=8C/=D0=BF=D1=80=D0=B5=D0=B4?= =?UTF-8?q?=D0=BE=D0=BA,=20--reason=20(T1.1-T1.4,=20#63)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hooks/harness_target.py | 34 ++++++++++++++++++++-------------- tests/test-hook-guard-main.sh | 8 ++++++++ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/hooks/harness_target.py b/hooks/harness_target.py index 8f21da3..abe235c 100644 --- a/hooks/harness_target.py +++ b/hooks/harness_target.py @@ -269,14 +269,14 @@ def _branch(w): class _State: def __init__(self, cwd, out, depth, wts): self.cur, self.out, self.depth, self.dirs = cwd, out, depth, [] - self.wts = wts # [(путь нового worktree, ветка)] из git worktree add -b|-B в этой же команде + self.wts = wts # [(путь, ветка, позиция в out)] из git worktree add -b|-B в этой же команде def parse(cmd, cwd, _out=None, _depth=0, _wts=None): """Цели записи команды Bash в порядке выполнения. Чистая функция: файловую систему не читает. cwd — каталог, от которого считаются относительные пути (None — неизвестен). - Новые worktree из git worktree add -b попадают в _wts (путь → ветка): decide_guard оценивает - по ним цели под ещё не созданным каталогом worktree.""" + Новые worktree из git worktree add -b попадают в _wts (путь → ветка + позиция): decide_guard + оценивает по ним только цели, идущие в команде после создания worktree.""" out = [] if _out is None else _out if _depth > 5: return out @@ -525,13 +525,13 @@ def _git(args, st, cur): if found: add(Target("switch", path, name)) elif sub == "worktree": - _worktree(rest, path, st.wts) + _worktree(rest, path, st) elif sub == "push": _push(rest, path, add) -def _worktree(rest, repo, wts): - """git worktree add <путь> -b|-B <ветка>: (путь нового worktree, ветка) для целей той же команды.""" +def _worktree(rest, repo, st): + """git worktree add <путь> -b|-B <ветка>: (путь, ветка, позиция в целях) для последующих целей команды.""" if not rest or rest[0][0] != "add": return branch, pos, k = None, [], 1 @@ -541,12 +541,15 @@ def _worktree(rest, repo, wts): branch = _branch(rest[k + 1]) k += 2 continue + if t in ("--reason",) and k + 1 < len(rest): + k += 2 + continue if not t.startswith("-"): pos.append(rest[k]) k += 1 if branch is None or not pos or pos[0][1] or repo is None: return - wts.append((_join(repo, pos[0][0]), branch)) + st.wts.append((_join(repo, pos[0][0]), branch, len(st.out))) def _push(rest, path, add): @@ -639,17 +642,17 @@ WT_HINT = "Работай в worktree на ветке (superpowers:using-git-wor def decide_guard(tool, ti, cwd): """Сообщение блокировки guard-main или None.""" cwd = cwd or None - wts = [] # [(реальный путь нового worktree, ветка)] из git worktree add -b|-B в этой же команде + wts = [] # [(реальный путь, ветка, позиция в targets)] из git worktree add -b|-B в этой же команде if tool == "Bash": targets = parse(ti.get("command") or "", cwd, _wts=wts) - wts = [(os.path.realpath(p), b) for p, b in wts] + wts = [(os.path.realpath(p), b, pos) for p, b, pos in wts] elif tool in ("Edit", "Write", "MultiEdit"): fp = ti.get("file_path") or "" targets = [Target("file", _abs(cwd, fp), tool)] if fp else [] else: return None over = {} # корень → ветка после git switch/checkout -b в этой же команде (None — не определена) - for t in targets: + for idx, t in enumerate(targets): if t.kind == "switch": r = resolve(t.path) if t.path else None if r: @@ -666,11 +669,14 @@ def decide_guard(tool, ti, cwd): if not r: continue root, branch, real = r - wt = next((b for p, b in reversed(wts) if real == p or real.startswith(p + os.sep)), None) - if wt is not None: # цель — в worktree, который эта же команда создаёт на новой ветке (#63) - branch = wt - elif root in over: + if root in over: # явный switch/checkout в этой же команде выигрывает у пары worktree (R-1) branch = over[root] + else: + wt = next((b for p, b, pos in reversed(wts) + if idx >= pos and not (p == root or root.startswith(p + os.sep)) + and (real == p or real.startswith(p + os.sep))), None) + if wt is not None: # цель идёт после worktree add и лежит в нём — ветка из add (#63) + branch = wt if t.kind == "git": if t.op.startswith("push>") or t.op == "push --all": return "BLOCKED [guard-main]: git %s — репозиторий %s. В main только через PR." % ( diff --git a/tests/test-hook-guard-main.sh b/tests/test-hook-guard-main.sh index 9a01917..85bc647 100644 --- a/tests/test-hook-guard-main.sh +++ b/tests/test-hook-guard-main.sh @@ -30,6 +30,14 @@ assert_eq "git worktree add .claude/worktrees/… на main" "$(rc "$S/n" Bash ' assert_eq "worktree add -b && cd && openspec archive (#63)" "$(rc "$S/n" Bash 'git worktree add .claude/worktrees/z -b feat/z origin/main && cd .claude/worktrees/z && openspec archive y -y')" 0 assert_eq "worktree add -C && cd && openspec archive (#63)" "$(rc "$S/ws" Bash "git -C $S/n worktree add .claude/worktrees/q -b feat/q origin/main && cd $S/n/.claude/worktrees/q && openspec archive y -y")" 0 assert_eq "worktree add -b без cd: openspec archive в main — блок (#63)" "$(rc "$S/n" Bash 'git worktree add .claude/worktrees/z -b feat/z origin/main && openspec archive y -y')" 2 +# fix-round 1 ревью PR #71 (T1.1–T1.4, R-1): пара «путь→ветка» — только для целей после add; switch и корень выигрывают +assert_eq "wt-пара не действует на цель до worktree add (T1.1)" "$(rc "$S/m" Bash 'echo x > .claude/worktrees/pre/f.md && git worktree add .claude/worktrees/pre -b feat/pre origin/main')" 2 +assert_eq "явный switch после worktree add — мутация по main (T1.2)" "$(rc "$S/m" Bash 'git worktree add .claude/worktrees/s -b feat/s origin/main && cd .claude/worktrees/s && git checkout main && git commit -m x')" 2 +assert_eq "worktree add . — пара не действует (T1.3)" "$(rc "$S/m" Bash 'git worktree add . -b feat/dot && git commit -m x')" 2 +assert_eq "worktree add .. — пара не действует (T1.3)" "$(rc "$S/m" Bash 'git worktree add .. -b feat/up || git commit -m x')" 2 +assert_eq "worktree add --reason (T1.4)" "$(rc "$S/m" Bash 'git worktree add --reason foo x -b feat/x origin/main && cd x && openspec archive y -y')" 0 +assert_eq "wt-пара не действует на основной checkout через -C (T1.6e)" "$(rc "$S/m" Bash "git worktree add .claude/worktrees/e -b feat/e origin/main && git -C $S/m commit -m x")" 2 +assert_eq "граница префикса пути пары (T1.6f)" "$(rc "$S/m" Bash 'git worktree add foo -b feat/foo && echo x > foobar/f.md')" 2 # файловые цели Bash assert_eq "echo > в main" "$(rc "$S/m" Bash 'echo x > notes.md')" 2 assert_eq "make 2>build.log в main" "$(rc "$S/m" Bash 'make 2>build.log')" 2