#!/usr/bin/env python3 """解出一條指令裡的 `git checkout` / `git switch` **真正會落在哪個目錄**。 管什麼 line-needs-own-worktree.sh 要回答「這次切分支動的是哪個 repo」。舊版拿 payload 的 `cwd` 當答案,於是 `cd <別的 repo> && git checkout <分支>` 這個 形狀會**認錯 repo**(inkstone/ISEP#109 → comment 5398 實測): 指令:cd .../matrix/arcrun && git checkout fix/library-lifecycle-187 舊版訊息:目錄:.../InkStoneCo 它現在在:feat/ticket-bell-webhook ↑ 動的是 matrix/arcrun,講的卻是 InkStoneCo 🔴 擋是擋對了,但**印給人的補救指令是另一個 repo 的**——照著做會在 InkStoneCo 開一份用不到的 worktree,真正要隔離的 matrix/arcrun 沒開到, 而人以為自己隔離好了。**一道閘給錯下一步,比不擋更糟。** 而 `cd X && git checkout` 正是這條線最常出現的寫法(本票的來由那次就是它)。 為什麼接既有的解析器,不另寫一支 `push_target_dir.py` 2026-08-23 已經替 `git push` 解過同一個問題 (inkstone/ISEP#30 comment 3949),連多層 cd 鏈與**子殼的 cd 不外洩** (`(cd A && …); git checkout` 落在殼外而不是 A)都算過了。 ⇒ 本檔只加「哪個動詞」與「哪些形狀不動 HEAD」,tokenize 與路徑接合一律沿用 那支,不養第二套會漂的 parser。那支的 42 條既有測試在本次改動後全數重跑通過。 回傳三種,呼叫端要分開處理 "" 這條指令裡沒有會**移動 HEAD** 的 checkout/switch ⇒ 不關這道閘的事 "?" 有,但目錄**解不出來**(`cd $VAR`、`cd -`、引號壞掉) ⇒ 🔴 呼叫端必須放行。寧可漏擋,也不要指著錯的 repo 叫人去開 worktree ——那正是本檔要修的病,猜一個回去等於原地打轉。 其他 目錄運算式,可能是相對的;呼叫端自己 `cd` 進去問 git(唯讀)。 "." =「就是呼叫端自己的 cwd」(指令裡沒有 cd 也沒有 -C)。 用法:printf '%s' "$CMD" | python3 checkout_target_dir.py """ import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from push_target_dir import _events, _join # noqa: E402 VERBS = ("checkout", "switch") UNKNOWN = "?" def _step(base, path): """走一步 cd/-C。不確定就一路標 unknown,不要往回猜成呼叫端的 cwd。""" if base == UNKNOWN: return UNKNOWN if not path or path == "-" or path.startswith("$") or path.startswith("`"): return UNKNOWN return _join(base, path) def _moves_head(tokens): """tokens 是整串 `git …`。回傳 (會不會動 HEAD, -C 指的路徑 or None)。 判準只有「這個動作動不動 HEAD」,不看任何措辭: git checkout -- <檔>/git checkout HEAD -- <檔> ⇒ 還原檔案,不動 git checkout -p ⇒ 挑 hunk,不動 git checkout(沒有任何目標) ⇒ git 自己會報錯 """ tok = list(tokens[1:]) # 去掉 "git" c_path = None while tok and tok[0].startswith("-"): if tok[0] == "-C" and len(tok) > 1: c_path = tok[1] if c_path is None else c_path + "/" + tok[1] tok = tok[2:] continue if tok[0].startswith("-C") and len(tok[0]) > 2: seg = tok[0][2:] c_path = seg if c_path is None else c_path + "/" + seg tok.pop(0) continue if tok[0].startswith("--git-dir"): return (False, None) # 自己指 git-dir 的花式用法,不猜 tok.pop(0) if not tok or tok[0] not in VERBS: return (False, None) rest = tok[1:] if "--" in rest or "-p" in rest or "--patch" in rest: return (False, None) if not [a for a in rest if not a.startswith("-")] and \ not any(a in ("-b", "-B", "-c", "-C") for a in rest): return (False, None) return (True, c_path) def find_checkout_target(cmd): events = _events(cmd, VERBS, strip_env=True) if not events: return "" # 引號壞掉之類——沿用「不擋在不確定上」 stack = [None] # 每層括號各自的 cwd;None = 同呼叫端的 cwd for kind, val in events: if kind == "enter": stack.append(stack[-1]) # 子殼繼承「進去那一刻」的位置 elif kind == "exit": if len(stack) > 1: stack.pop() # 子殼自己 cd 到哪,出來不算數 elif kind == "cd": stack[-1] = _step(stack[-1], val) elif kind == "verb": moves, c_path = _moves_head(val) if not moves: continue where = stack[-1] if c_path is not None: # -C 贏過 cd:git 自己就是這個優先序 # (而且 -C 若是相對路徑,是接在 cd 之後算,不是接在 cwd) where = _step(where, c_path) if where == UNKNOWN: return UNKNOWN return where or "." return "" def main(): sys.stdout.write(find_checkout_target(sys.stdin.read())) if __name__ == "__main__": main()