11from __future__ import annotations
22
3+ from contextlib import contextmanager
34import re
45import subprocess
6+ import threading
57from pathlib import Path
68
79from openvibecoding_orch .config import load_config
810
11+ try :
12+ import fcntl
13+ except Exception : # pragma: no cover - non-posix fallback
14+ fcntl = None
15+
16+
17+ _PROCESS_LOCK_GUARD = threading .Lock ()
18+ _PROCESS_LOCKS : dict [str , threading .RLock ] = {}
19+
920
1021def _run_git (args : list [str ], repo_root : Path ) -> subprocess .CompletedProcess [str ]:
1122 return subprocess .run (
@@ -27,6 +38,33 @@ def _repo_root() -> Path:
2738 return Path (result .stdout .strip ())
2839
2940
41+ def _process_lock_for (path : Path ) -> threading .RLock :
42+ key = str (path .resolve (strict = False ))
43+ with _PROCESS_LOCK_GUARD :
44+ lock = _PROCESS_LOCKS .get (key )
45+ if lock is None :
46+ lock = threading .RLock ()
47+ _PROCESS_LOCKS [key ] = lock
48+ return lock
49+
50+
51+ @contextmanager
52+ def _worktree_file_lock (repo_root : Path ):
53+ lock_path = repo_root / ".git" / "openvibecoding-worktrees.lock"
54+ lock_path .parent .mkdir (parents = True , exist_ok = True )
55+ process_lock = _process_lock_for (lock_path )
56+ with process_lock :
57+ if fcntl is None :
58+ yield None
59+ return
60+ with lock_path .open ("a+" , encoding = "utf-8" ) as lock_file :
61+ fcntl .flock (lock_file .fileno (), fcntl .LOCK_EX )
62+ try :
63+ yield lock_file
64+ finally :
65+ fcntl .flock (lock_file .fileno (), fcntl .LOCK_UN )
66+
67+
3068def _safe_segment (value : str ) -> str :
3169 cleaned = re .sub (r"[^A-Za-z0-9._-]+" , "-" , value .strip ())
3270 return cleaned or "task"
@@ -45,66 +83,69 @@ def _is_stale_worktree_metadata_error(stderr: str) -> bool:
4583def create_worktree (run_id : str , task_id : str , baseline_commit : str ) -> Path :
4684 cfg = load_config ()
4785 repo_root = _repo_root ()
86+ with _worktree_file_lock (repo_root ):
87+ prune = _run_git (["git" , "worktree" , "prune" ], repo_root )
88+ _ensure_git_ok (prune , ["git" , "worktree" , "prune" ])
4889
49- prune = _run_git (["git" , "worktree " , "prune" ], repo_root )
50- _ensure_git_ok (prune , ["git" , "worktree " , "prune" ])
90+ check = _run_git (["git" , "cat-file " , "-e" , baseline_commit ], repo_root )
91+ _ensure_git_ok (check , ["git" , "cat-file " , "-e" , baseline_commit ])
5192
52- check = _run_git (["git" , "cat-file" , "-e" , baseline_commit ], repo_root )
53- _ensure_git_ok (check , ["git" , "cat-file" , "-e" , baseline_commit ])
93+ safe_task = _safe_segment (task_id )
94+ worktree_path = cfg .worktree_root / run_id / safe_task
95+ worktree_path .parent .mkdir (parents = True , exist_ok = True )
96+ branch = _branch_name (run_id , task_id )
5497
55- safe_task = _safe_segment (task_id )
56- worktree_path = cfg .worktree_root / run_id / safe_task
57- worktree_path .parent .mkdir (parents = True , exist_ok = True )
58- branch = _branch_name (run_id , task_id )
59-
60- cmd = [
61- "git" ,
62- "worktree" ,
63- "add" ,
64- "-b" ,
65- branch ,
66- str (worktree_path ),
67- baseline_commit ,
68- ]
69- result = _run_git (cmd , repo_root )
70- _ensure_git_ok (result , cmd )
71- return worktree_path
98+ cmd = [
99+ "git" ,
100+ "worktree" ,
101+ "add" ,
102+ "-b" ,
103+ branch ,
104+ str (worktree_path ),
105+ baseline_commit ,
106+ ]
107+ result = _run_git (cmd , repo_root )
108+ _ensure_git_ok (result , cmd )
109+ return worktree_path
72110
73111
74112def remove_worktree (run_id : str , task_id : str ) -> None :
75113 cfg = load_config ()
76114 repo_root = _repo_root ()
77- safe_task = _safe_segment (task_id )
78- worktree_path = cfg .worktree_root / run_id / safe_task
79- branch = _branch_name (run_id , task_id )
80-
81- if worktree_path .exists ():
82- cmd_remove = ["git" , "worktree" , "remove" , "--force" , str (worktree_path )]
83- result = _run_git (cmd_remove , repo_root )
84- _ensure_git_ok (result , cmd_remove )
85-
86- prune = _run_git (["git" , "worktree" , "prune" ], repo_root )
87- _ensure_git_ok (prune , ["git" , "worktree" , "prune" ])
88-
89- parent = worktree_path .parent
90- if parent .exists () and parent .is_dir ():
91- try :
92- parent .rmdir ()
93- except OSError :
94- pass
95-
96- cmd_branch = ["git" , "branch" , "-D" , branch ]
97- result = _run_git (cmd_branch , repo_root )
98- if result .returncode != 0 and _is_stale_worktree_metadata_error (result .stderr ):
115+ with _worktree_file_lock (repo_root ):
116+ safe_task = _safe_segment (task_id )
117+ worktree_path = cfg .worktree_root / run_id / safe_task
118+ branch = _branch_name (run_id , task_id )
119+
120+ if worktree_path .exists ():
121+ cmd_remove = ["git" , "worktree" , "remove" , "--force" , str (worktree_path )]
122+ result = _run_git (cmd_remove , repo_root )
123+ _ensure_git_ok (result , cmd_remove )
124+
99125 prune = _run_git (["git" , "worktree" , "prune" ], repo_root )
100126 _ensure_git_ok (prune , ["git" , "worktree" , "prune" ])
127+
128+ parent = worktree_path .parent
129+ if parent .exists () and parent .is_dir ():
130+ try :
131+ parent .rmdir ()
132+ except OSError :
133+ # Best-effort cleanup only; parent may still hold other task worktrees.
134+ pass
135+
136+ cmd_branch = ["git" , "branch" , "-D" , branch ]
101137 result = _run_git (cmd_branch , repo_root )
102- if result .returncode != 0 and "not found" not in result .stderr :
103- _ensure_git_ok (result , cmd_branch )
138+ if result .returncode != 0 and _is_stale_worktree_metadata_error (result .stderr ):
139+ prune = _run_git (["git" , "worktree" , "prune" ], repo_root )
140+ _ensure_git_ok (prune , ["git" , "worktree" , "prune" ])
141+ result = _run_git (cmd_branch , repo_root )
142+ if result .returncode != 0 and "not found" not in result .stderr :
143+ _ensure_git_ok (result , cmd_branch )
104144
105145
106146def list_worktrees () -> list [str ]:
107147 repo_root = _repo_root ()
108- result = _run_git (["git" , "worktree" , "list" , "--porcelain" ], repo_root )
109- _ensure_git_ok (result , ["git" , "worktree" , "list" , "--porcelain" ])
110- return [line for line in result .stdout .splitlines () if line .startswith ("worktree " )]
148+ with _worktree_file_lock (repo_root ):
149+ result = _run_git (["git" , "worktree" , "list" , "--porcelain" ], repo_root )
150+ _ensure_git_ok (result , ["git" , "worktree" , "list" , "--porcelain" ])
151+ return [line for line in result .stdout .splitlines () if line .startswith ("worktree " )]
0 commit comments