@@ -38,6 +38,22 @@ fn cached_workstation_id(workspace_name: &str) -> Option<u64> {
3838 . and_then ( |cache| cache. get ( workspace_name) . copied ( ) )
3939}
4040
41+ /// Per-branch lock that serializes worktree setup so that `setup_worktree`
42+ /// (frontend) and `setup_worktree_sync` (backend/MCP) do not race each other.
43+ /// Follows the same pattern as `REPO_CLONE_LOCKS` in `git/github.rs`.
44+ static WORKTREE_SETUP_LOCKS : OnceLock < Mutex < HashMap < String , Arc < Mutex < ( ) > > > > > = OnceLock :: new ( ) ;
45+
46+ fn worktree_setup_lock_for ( branch_id : & str ) -> Arc < Mutex < ( ) > > {
47+ let locks = WORKTREE_SETUP_LOCKS . get_or_init ( || Mutex :: new ( HashMap :: new ( ) ) ) ;
48+ let mut map = locks. lock ( ) . unwrap_or_else ( |p| p. into_inner ( ) ) ;
49+ // Opportunistically drop lock entries that are no longer referenced by any
50+ // active setup operation so the map does not grow without bound.
51+ map. retain ( |_, v| Arc :: strong_count ( v) > 1 ) ;
52+ map. entry ( branch_id. to_string ( ) )
53+ . or_insert_with ( || Arc :: new ( Mutex :: new ( ( ) ) ) )
54+ . clone ( )
55+ }
56+
4157fn get_store ( store : & tauri:: State < ' _ , Mutex < Option < Arc < Store > > > > ) -> Result < Arc < Store > , String > {
4258 store
4359 . lock ( )
@@ -823,6 +839,20 @@ pub async fn setup_worktree(
823839 git:: project_worktree_path_for ( & branch. project_id , & repo_slug, & branch. branch_name )
824840 . map_err ( |e| e. to_string ( ) ) ?;
825841
842+ // Serialize worktree creation per branch so that concurrent callers
843+ // (frontend `setup_worktree` vs backend `setup_worktree_sync`) do not race.
844+ let lock = worktree_setup_lock_for ( & branch. id ) ;
845+ let _guard = lock. lock ( ) . unwrap_or_else ( |p| p. into_inner ( ) ) ;
846+
847+ // Re-check the DB fast-path under the lock — the other caller may have
848+ // finished while we were waiting.
849+ if let Some ( existing) = store
850+ . get_workdir_for_branch ( & branch. id )
851+ . map_err ( |e| e. to_string ( ) ) ?
852+ {
853+ return Ok ( to_branch_with_workdir ( branch, Some ( existing. path ) ) ) ;
854+ }
855+
826856 // Reuse any existing worktree for this branch; otherwise create one.
827857 let existing_worktree_path =
828858 find_existing_worktree_for_branch ( & repo_path, & branch. branch_name ) ?;
@@ -923,6 +953,7 @@ pub async fn setup_worktree(
923953 }
924954 }
925955
956+ // _guard dropped here, releasing the per-branch lock.
926957 Ok ( to_branch_with_workdir ( branch, Some ( worktree_str) ) )
927958}
928959
@@ -2109,27 +2140,35 @@ pub(crate) fn setup_worktree_sync(
21092140 crate :: git:: project_worktree_path_for ( & branch. project_id , & repo_slug, & branch. branch_name )
21102141 . map_err ( |e| e. to_string ( ) ) ?;
21112142
2143+ // Serialize worktree creation per branch so that concurrent callers
2144+ // (frontend `setup_worktree` vs backend `setup_worktree_sync`) do not race.
2145+ let lock = worktree_setup_lock_for ( & branch. id ) ;
2146+ let _guard = lock. lock ( ) . unwrap_or_else ( |p| p. into_inner ( ) ) ;
2147+
2148+ // Re-check the DB fast-path under the lock — the other caller may have
2149+ // finished while we were waiting.
2150+ if let Some ( existing) = store
2151+ . get_workdir_for_branch ( & branch. id )
2152+ . map_err ( |e| e. to_string ( ) ) ?
2153+ {
2154+ return Ok ( existing. path ) ;
2155+ }
2156+
21122157 emit_progress ( "creating_worktree" , None ) ;
21132158 // Reuse any existing worktree for this branch; otherwise create one.
2114- let existing_worktree_path = crate :: git:: list_worktrees ( & repo_path)
2115- . map_err ( |e| e. to_string ( ) ) ?
2116- . into_iter ( )
2117- . find_map ( |( path, wt_branch) | match wt_branch. as_deref ( ) {
2118- Some ( name) if name == branch. branch_name => Some ( path) ,
2119- _ => None ,
2120- } ) ;
2159+ let existing_worktree_path =
2160+ find_existing_worktree_for_branch ( & repo_path, & branch. branch_name ) ?;
21212161
21222162 let worktree_path = if let Some ( path) = existing_worktree_path {
21232163 path
21242164 } else if crate :: git:: branch_exists ( & repo_path, & branch. branch_name )
21252165 . map_err ( |e| e. to_string ( ) ) ?
21262166 {
2127- crate :: git :: create_worktree_for_existing_branch_at_path (
2167+ create_worktree_for_existing_branch_with_fallback (
21282168 & repo_path,
21292169 & branch. branch_name ,
21302170 & desired_worktree_path,
2131- )
2132- . map_err ( |e| e. to_string ( ) ) ?
2171+ ) ?
21332172 } else {
21342173 // If the branch exists on the remote (e.g. from an existing PR),
21352174 // start the new local branch from the remote tracking ref so it
@@ -2143,29 +2182,37 @@ pub(crate) fn setup_worktree_sync(
21432182 } else {
21442183 & branch. base_branch
21452184 } ;
2146- match crate :: git :: create_worktree_at_path (
2185+ match create_worktree_with_fallback (
21472186 & repo_path,
21482187 & branch. branch_name ,
21492188 start_point,
21502189 & desired_worktree_path,
21512190 ) {
21522191 Ok ( path) => path,
21532192 Err ( create_err) => {
2154- if crate :: git:: branch_exists ( & repo_path, & branch. branch_name )
2193+ if let Some ( path) =
2194+ find_existing_worktree_for_branch ( & repo_path, & branch. branch_name ) ?
2195+ {
2196+ log:: warn!(
2197+ "[setup_worktree_sync] Reusing existing worktree '{}' for branch '{}' after create failure" ,
2198+ path. display( ) ,
2199+ branch. branch_name
2200+ ) ;
2201+ path
2202+ } else if crate :: git:: branch_exists ( & repo_path, & branch. branch_name )
21552203 . map_err ( |e| e. to_string ( ) ) ?
21562204 {
21572205 log:: warn!(
2158- "[project_mcp ] Branch '{}' already exists after create attempt; retrying with existing branch" ,
2206+ "[setup_worktree_sync ] Branch '{}' already exists after create attempt; retrying with existing branch" ,
21592207 branch. branch_name
21602208 ) ;
2161- crate :: git :: create_worktree_for_existing_branch_at_path (
2209+ create_worktree_for_existing_branch_with_fallback (
21622210 & repo_path,
21632211 & branch. branch_name ,
21642212 & desired_worktree_path,
2165- )
2166- . map_err ( |e| e. to_string ( ) ) ?
2213+ ) ?
21672214 } else {
2168- return Err ( create_err. to_string ( ) ) ;
2215+ return Err ( create_err) ;
21692216 }
21702217 }
21712218 }
@@ -2205,6 +2252,7 @@ pub(crate) fn setup_worktree_sync(
22052252 }
22062253 }
22072254
2255+ // _guard dropped here, releasing the per-branch lock.
22082256 Ok ( worktree_str)
22092257}
22102258
@@ -2249,11 +2297,6 @@ pub(crate) async fn run_prerun_actions_for_branch(
22492297
22502298 // If actions haven't been detected yet for this repo+subpath, detect now
22512299 if !context. has_detected_actions {
2252- log:: info!(
2253- "[project_mcp] detecting actions for repo {} (subpath: {:?})" ,
2254- github_repo,
2255- subpath
2256- ) ;
22572300 store
22582301 . set_action_context_detecting ( & context. id , true )
22592302 . map_err ( |e| format ! ( "Failed to set detection status: {e}" ) ) ?;
@@ -2268,12 +2311,22 @@ pub(crate) async fn run_prerun_actions_for_branch(
22682311 ) ;
22692312
22702313 // Run detection (may call out to AI)
2271- let detected = crate :: actions:: commands:: detect_actions_for_repo_context (
2314+ let detected = match crate :: actions:: commands:: detect_actions_for_repo_context (
22722315 & github_repo,
22732316 subpath. as_deref ( ) ,
22742317 )
22752318 . await
2276- . unwrap_or_default ( ) ;
2319+ {
2320+ Ok ( actions) => actions,
2321+ Err ( e) => {
2322+ log:: warn!(
2323+ "[run_prerun_actions_for_branch] action detection failed for repo {} (subpath: {:?}): {e}" ,
2324+ github_repo,
2325+ subpath
2326+ ) ;
2327+ Vec :: new ( )
2328+ }
2329+ } ;
22772330
22782331 // Persist detected actions (skip duplicates)
22792332 let existing_actions = store
@@ -2375,11 +2428,6 @@ pub(crate) async fn run_prerun_actions_for_branch(
23752428 {
23762429 Ok ( _execution_id) => {
23772430 count += 1 ;
2378- log:: info!(
2379- "[project_mcp] prerun action '{}' completed for branch {}" ,
2380- action. id,
2381- branch_id
2382- ) ;
23832431 }
23842432 Err ( e) => {
23852433 log:: warn!(
0 commit comments