fix(hooks): цель после git worktree add в той же команде — по новой ветке (O-4, #63)
This commit is contained in:
parent
2eaaa99399
commit
8d8dd89b8a
@ -267,17 +267,20 @@ def _branch(w):
|
|||||||
|
|
||||||
|
|
||||||
class _State:
|
class _State:
|
||||||
def __init__(self, cwd, out, depth):
|
def __init__(self, cwd, out, depth, wts):
|
||||||
self.cur, self.out, self.depth, self.dirs = cwd, out, depth, []
|
self.cur, self.out, self.depth, self.dirs = cwd, out, depth, []
|
||||||
|
self.wts = wts # [(путь нового worktree, ветка)] из git worktree add -b|-B в этой же команде
|
||||||
|
|
||||||
|
|
||||||
def parse(cmd, cwd, _out=None, _depth=0):
|
def parse(cmd, cwd, _out=None, _depth=0, _wts=None):
|
||||||
"""Цели записи команды Bash в порядке выполнения. Чистая функция: файловую систему не читает.
|
"""Цели записи команды Bash в порядке выполнения. Чистая функция: файловую систему не читает.
|
||||||
cwd — каталог, от которого считаются относительные пути (None — неизвестен)."""
|
cwd — каталог, от которого считаются относительные пути (None — неизвестен).
|
||||||
|
Новые worktree из git worktree add -b попадают в _wts (путь → ветка): decide_guard оценивает
|
||||||
|
по ним цели под ещё не созданным каталогом worktree."""
|
||||||
out = [] if _out is None else _out
|
out = [] if _out is None else _out
|
||||||
if _depth > 5:
|
if _depth > 5:
|
||||||
return out
|
return out
|
||||||
st = _State(cwd or None, out, _depth)
|
st = _State(cwd or None, out, _depth, [] if _wts is None else _wts)
|
||||||
toks = Lexer(cmd).tokens()
|
toks = Lexer(cmd).tokens()
|
||||||
stack, words, redirs, prev = [], [], [], None
|
stack, words, redirs, prev = [], [], [], None
|
||||||
i = 0
|
i = 0
|
||||||
@ -378,11 +381,11 @@ def _simple(words, redirs, st):
|
|||||||
for k, (t, d) in enumerate(args):
|
for k, (t, d) in enumerate(args):
|
||||||
if t.startswith("-") and not t.startswith("--") and "c" in t[1:]:
|
if t.startswith("-") and not t.startswith("--") and "c" in t[1:]:
|
||||||
if k + 1 < len(args) and not args[k + 1][1]:
|
if k + 1 < len(args) and not args[k + 1][1]:
|
||||||
parse(args[k + 1][0], cur, out, st.depth + 1)
|
parse(args[k + 1][0], cur, out, st.depth + 1, st.wts)
|
||||||
break
|
break
|
||||||
return _SAME
|
return _SAME
|
||||||
if head == "eval":
|
if head == "eval":
|
||||||
parse(" ".join(t for t, d in args), cur, out, st.depth + 1)
|
parse(" ".join(t for t, d in args), cur, out, st.depth + 1, st.wts)
|
||||||
return _SAME
|
return _SAME
|
||||||
if xargs and head in ALL_PATHS + DEST + ("chmod", "chown", "chgrp", "sed", "perl"):
|
if xargs and head in ALL_PATHS + DEST + ("chmod", "chown", "chgrp", "sed", "perl"):
|
||||||
out.append(Target("file", None, head)) # пути придут из stdin
|
out.append(Target("file", None, head)) # пути придут из stdin
|
||||||
@ -521,10 +524,31 @@ def _git(args, st, cur):
|
|||||||
name, found = (None if ops[0] == ("-", False) else _branch(ops[0])), True # «-» — предыдущая
|
name, found = (None if ops[0] == ("-", False) else _branch(ops[0])), True # «-» — предыдущая
|
||||||
if found:
|
if found:
|
||||||
add(Target("switch", path, name))
|
add(Target("switch", path, name))
|
||||||
|
elif sub == "worktree":
|
||||||
|
_worktree(rest, path, st.wts)
|
||||||
elif sub == "push":
|
elif sub == "push":
|
||||||
_push(rest, path, add)
|
_push(rest, path, add)
|
||||||
|
|
||||||
|
|
||||||
|
def _worktree(rest, repo, wts):
|
||||||
|
"""git worktree add <путь> -b|-B <ветка>: (путь нового worktree, ветка) для целей той же команды."""
|
||||||
|
if not rest or rest[0][0] != "add":
|
||||||
|
return
|
||||||
|
branch, pos, k = None, [], 1
|
||||||
|
while k < len(rest):
|
||||||
|
t = rest[k][0]
|
||||||
|
if t in ("-b", "-B") and k + 1 < len(rest):
|
||||||
|
branch = _branch(rest[k + 1])
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
def _push(rest, path, add):
|
def _push(rest, path, add):
|
||||||
delete, everything, tags, pos, k = False, False, False, [], 0
|
delete, everything, tags, pos, k = False, False, False, [], 0
|
||||||
while k < len(rest):
|
while k < len(rest):
|
||||||
@ -615,8 +639,10 @@ WT_HINT = "Работай в worktree на ветке (superpowers:using-git-wor
|
|||||||
def decide_guard(tool, ti, cwd):
|
def decide_guard(tool, ti, cwd):
|
||||||
"""Сообщение блокировки guard-main или None."""
|
"""Сообщение блокировки guard-main или None."""
|
||||||
cwd = cwd or None
|
cwd = cwd or None
|
||||||
|
wts = [] # [(реальный путь нового worktree, ветка)] из git worktree add -b|-B в этой же команде
|
||||||
if tool == "Bash":
|
if tool == "Bash":
|
||||||
targets = parse(ti.get("command") or "", cwd)
|
targets = parse(ti.get("command") or "", cwd, _wts=wts)
|
||||||
|
wts = [(os.path.realpath(p), b) for p, b in wts]
|
||||||
elif tool in ("Edit", "Write", "MultiEdit"):
|
elif tool in ("Edit", "Write", "MultiEdit"):
|
||||||
fp = ti.get("file_path") or ""
|
fp = ti.get("file_path") or ""
|
||||||
targets = [Target("file", _abs(cwd, fp), tool)] if fp else []
|
targets = [Target("file", _abs(cwd, fp), tool)] if fp else []
|
||||||
@ -640,7 +666,10 @@ def decide_guard(tool, ti, cwd):
|
|||||||
if not r:
|
if not r:
|
||||||
continue
|
continue
|
||||||
root, branch, real = r
|
root, branch, real = r
|
||||||
if root in over:
|
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:
|
||||||
branch = over[root]
|
branch = over[root]
|
||||||
if t.kind == "git":
|
if t.kind == "git":
|
||||||
if t.op.startswith("push>") or t.op == "push --all":
|
if t.op.startswith("push>") or t.op == "push --all":
|
||||||
|
|||||||
@ -176,6 +176,20 @@ guard-main SHALL блокировать git-мутации — `commit`, `merge`
|
|||||||
- **WHEN** cwd — worktree на ветке, команда `git switch - && git merge x`
|
- **WHEN** cwd — worktree на ветке, команда `git switch - && git merge x`
|
||||||
- **THEN** guard-main завершается с кодом 2
|
- **THEN** guard-main завершается с кодом 2
|
||||||
|
|
||||||
|
### Requirement: Новый worktree внутри той же команды
|
||||||
|
`git worktree add <путь> -b|-B <ветка>` в вызове Bash SHALL задавать ветку `<ветка>` для целей записи под `<путь>` в
|
||||||
|
последующих командах того же вызова — в том числе когда каталога worktree на момент проверки ещё нет, а цель задана
|
||||||
|
относительно него после `cd <путь>` или через `git -C <путь>`. Цель вне `<путь>` MUST оцениваться по ветке объемлющего
|
||||||
|
репозитория.
|
||||||
|
|
||||||
|
#### Scenario: Создание worktree и openspec archive в одной цепочке
|
||||||
|
- **WHEN** cwd — репозиторий на main, команда `git worktree add .claude/worktrees/x -b feat/x origin/main && cd .claude/worktrees/x && openspec archive y -y`
|
||||||
|
- **THEN** guard-main завершается с кодом 0
|
||||||
|
|
||||||
|
#### Scenario: openspec archive без cd после создания worktree
|
||||||
|
- **WHEN** cwd — репозиторий на main, команда `git worktree add .claude/worktrees/x -b feat/x origin/main && openspec archive y -y`
|
||||||
|
- **THEN** guard-main завершается с кодом 2
|
||||||
|
|
||||||
### Requirement: Префиксы команды
|
### Requirement: Префиксы команды
|
||||||
Голова команды SHALL определяться после присваиваний `VAR=x` и префиксов `sudo`, `env`, `nice`, `timeout`, `ionice`,
|
Голова команды SHALL определяться после присваиваний `VAR=x` и префиксов `sudo`, `env`, `nice`, `timeout`, `ionice`,
|
||||||
`stdbuf`, `xargs`, `nohup`, `command`, `exec`, `time`, включая значения их опций (`nice -n 5`, `sudo -u u`,
|
`stdbuf`, `xargs`, `nohup`, `command`, `exec`, `time`, включая значения их опций (`nice -n 5`, `sudo -u u`,
|
||||||
|
|||||||
@ -26,6 +26,10 @@ assert_eq "вложенный worktree: git commit в нём" "$(rc "$W" Bash 'g
|
|||||||
assert_eq "вложенный worktree: запись в основной checkout на main" "$(rc "$W" Bash "echo x > $S/n/a.md")" 2
|
assert_eq "вложенный worktree: запись в основной checkout на main" "$(rc "$W" Bash "echo x > $S/n/a.md")" 2
|
||||||
assert_eq "вложенный worktree: Write в основной checkout рядом" "$(rc "$W" Write "$S/n/.claude/x.md")" 2
|
assert_eq "вложенный worktree: Write в основной checkout рядом" "$(rc "$W" Write "$S/n/.claude/x.md")" 2
|
||||||
assert_eq "git worktree add .claude/worktrees/… на main" "$(rc "$S/n" Bash 'git worktree add .claude/worktrees/y -b feat/y origin/main')" 0
|
assert_eq "git worktree add .claude/worktrees/… на main" "$(rc "$S/n" Bash 'git worktree add .claude/worktrees/y -b feat/y origin/main')" 0
|
||||||
|
# цепочка создания worktree и записи в нём одной командой (#63, O-4): цель под новым путём — по новой ветке
|
||||||
|
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
|
||||||
# файловые цели Bash
|
# файловые цели Bash
|
||||||
assert_eq "echo > в main" "$(rc "$S/m" Bash 'echo x > notes.md')" 2
|
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
|
assert_eq "make 2>build.log в main" "$(rc "$S/m" Bash 'make 2>build.log')" 2
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user