@@ -3,15 +3,15 @@ use std::{
33 path:: PathBuf ,
44 str:: FromStr ,
55 sync:: {
6- Arc ,
6+ Arc , Mutex ,
77 atomic:: { AtomicUsize , Ordering } ,
88 } ,
99} ;
1010
1111use async_recursion:: async_recursion;
1212use async_trait:: async_trait;
1313use callisto:: sea_orm_active_enums:: RefTypeEnum ;
14- use common:: errors:: MegaError ;
14+ use common:: { errors:: MegaError , utils :: ZERO_ID } ;
1515use futures:: { StreamExt , TryStreamExt } ;
1616use git_internal:: {
1717 errors:: GitError ,
@@ -44,7 +44,7 @@ use crate::{
4444pub struct ImportRepo {
4545 pub storage : Storage ,
4646 pub repo : Repo ,
47- pub command_list : Vec < RefCommand > ,
47+ pub command_list : Mutex < Vec < RefCommand > > ,
4848 pub unpack_redlock : Arc < RedLock > ,
4949 pub git_object_cache : Arc < GitObjectCache > ,
5050}
@@ -55,6 +55,13 @@ impl RepoHandler for ImportRepo {
5555 false
5656 }
5757
58+ fn sync_commands_after_unpack ( & self , commands : & [ RefCommand ] ) {
59+ * self
60+ . command_list
61+ . lock ( )
62+ . expect ( "command_list lock poisoned" ) = commands. to_vec ( ) ;
63+ }
64+
5865 async fn refs_with_head_hash ( & self ) -> ( String , Vec < Refs > ) {
5966 let result = self
6067 . storage
@@ -67,7 +74,7 @@ impl RepoHandler for ImportRepo {
6774 self . find_head_hash ( refs)
6875 }
6976
70- async fn post_receive_pack ( & self ) -> Result < ( ) , MegaError > {
77+ async fn finalize_receive_pack ( & self ) -> Result < ( ) , MegaError > {
7178 let guard = self . unpack_redlock . clone ( ) . lock ( ) . await ?;
7279 self . traverses_tree_and_update_filepath ( ) . await ?;
7380 self . attach_to_monorepo_parent ( ) . await ?;
@@ -280,23 +287,27 @@ impl RepoHandler for ImportRepo {
280287 }
281288
282289 async fn update_refs ( & self , refs : & RefCommand ) -> Result < ( ) , GitError > {
290+ if refs. ref_type != RefTypeEnum :: Tag {
291+ // Branch `import_refs` rows are written in the same DB transaction as monorepo attach.
292+ return Ok ( ( ) ) ;
293+ }
283294 let storage = self . storage . git_db_storage ( ) ;
284295 match refs. command_type {
285296 CommandType :: Create => {
286297 storage
287298 . save_ref ( self . repo . repo_id , refs. clone ( ) . into ( ) )
288299 . await
289- . unwrap ( ) ;
300+ . map_err ( |e| GitError :: CustomError ( e . to_string ( ) ) ) ? ;
290301 }
291302 CommandType :: Delete => storage
292303 . remove_ref ( self . repo . repo_id , & refs. ref_name )
293304 . await
294- . unwrap ( ) ,
305+ . map_err ( |e| GitError :: CustomError ( e . to_string ( ) ) ) ? ,
295306 CommandType :: Update => {
296307 storage
297308 . update_ref ( self . repo . repo_id , & refs. ref_name , & refs. new_id )
298309 . await
299- . unwrap ( ) ;
310+ . map_err ( |e| GitError :: CustomError ( e . to_string ( ) ) ) ? ;
300311 }
301312 }
302313 Ok ( ( ) )
@@ -320,8 +331,22 @@ impl RepoHandler for ImportRepo {
320331 }
321332
322333 async fn traverses_tree_and_update_filepath ( & self ) -> Result < ( ) , MegaError > {
323- //let (current_head, refs) = self.head_hash().await;
324- let ( current_head, _refs) = self . refs_with_head_hash ( ) . await ;
334+ // Prefer the branch tip from this receive-pack (same as `attach_to_monorepo_parent`).
335+ // DB `import_refs` is not updated until the attach transaction, so reading HEAD only
336+ // from the DB would still see the pre-push tip during finalize.
337+ let from_commands = {
338+ let cmds = self
339+ . command_list
340+ . lock ( )
341+ . expect ( "command_list lock poisoned" ) ;
342+ cmds. iter ( )
343+ . find ( |c| c. ref_type == RefTypeEnum :: Branch && c. new_id != ZERO_ID )
344+ . map ( |c| c. new_id . clone ( ) )
345+ } ;
346+ let current_head = match from_commands {
347+ Some ( h) => h,
348+ None => self . refs_with_head_hash ( ) . await . 0 ,
349+ } ;
325350 let commit = Commit :: from_git_model (
326351 self . storage
327352 . git_db_storage ( )
@@ -379,53 +404,115 @@ impl ImportRepo {
379404
380405 // attach import repo to monorepo parent tree
381406 pub ( crate ) async fn attach_to_monorepo_parent ( & self ) -> Result < ( ) , MegaError > {
382- // 1. find branch command
383- let commit_id = match self
407+ // Snapshot commands without holding the mutex across await (Send + avoids deadlocks).
408+ let commands_snapshot : Vec < RefCommand > = self
384409 . command_list
410+ . lock ( )
411+ . expect ( "command_list lock poisoned" )
412+ . clone ( ) ;
413+ let commit_id = match commands_snapshot
385414 . iter ( )
386415 . find ( |c| c. ref_type == RefTypeEnum :: Branch )
387416 {
388417 Some ( cmd) => cmd. new_id . clone ( ) ,
389418 None => return Ok ( ( ) ) ,
390419 } ;
391420
392- // 2. search and create tree
393421 let path = PathBuf :: from ( self . repo . repo_path . clone ( ) ) ;
394-
395422 let mono_api_service: MonoApiService = self . into ( ) ;
396423 let storage = self . storage . mono_storage ( ) ;
397- let save_trees = tree_ops:: search_and_create_tree ( & mono_api_service, & path) . await ?;
398424
399- // 3. get root ref
400- let root_ref = storage
401- . get_main_ref ( "/" )
402- . await ?
403- . ok_or_else ( || MegaError :: Other ( "root ref not found" . to_string ( ) ) ) ?;
425+ // Concurrent attaches need CAS on root mega_refs; retry when head moved.
426+ const MAX_ATTACH_ATTEMPTS : u32 = 64 ;
404427
405- // 4. get latest commit
406- let latest_commit: Commit = Commit :: from_git_model (
407- self . storage
408- . git_db_storage ( )
409- . get_commit_by_hash ( self . repo . repo_id , & commit_id)
428+ for attempt in 0 ..MAX_ATTACH_ATTEMPTS {
429+ let root_ref = storage
430+ . get_main_ref ( "/" )
410431 . await ?
411- . ok_or_else ( || MegaError :: Other ( format ! ( "commit {} not found" , commit_id) ) ) ?,
412- ) ;
432+ . ok_or_else ( || MegaError :: Other ( "root ref not found" . to_string ( ) ) ) ?;
433+ let expected_commit = root_ref. ref_commit_hash . clone ( ) ;
434+ let expected_tree = root_ref. ref_tree_hash . clone ( ) ;
435+ let root_ref_id = root_ref. id ;
413436
414- // 5. generate commit
415- let commit_msg = latest_commit. format_message ( ) ;
416- let new_commit = Commit :: from_tree_id (
417- save_trees
418- . back ( )
419- . ok_or_else ( || MegaError :: Other ( "no tree generated" . to_string ( ) ) ) ?
420- . id ,
421- vec ! [ ObjectHash :: from_str( & root_ref. ref_commit_hash) . unwrap( ) ] ,
422- & format ! ( "\n {commit_msg}" ) ,
423- ) ;
437+ let save_trees = tree_ops:: search_and_create_tree ( & mono_api_service, & path) . await ?;
424438
425- storage
426- . attach_to_monorepo_parent_with_txn ( root_ref, new_commit, save_trees. into ( ) )
427- . await ?;
428- Ok ( ( ) )
439+ let latest_commit: Commit = Commit :: from_git_model (
440+ self . storage
441+ . git_db_storage ( )
442+ . get_commit_by_hash ( self . repo . repo_id , & commit_id)
443+ . await ?
444+ . ok_or_else ( || MegaError :: Other ( format ! ( "commit {} not found" , commit_id) ) ) ?,
445+ ) ;
446+
447+ let commit_msg = latest_commit. format_message ( ) ;
448+ let new_commit = Commit :: from_tree_id (
449+ save_trees
450+ . back ( )
451+ . ok_or_else ( || MegaError :: Other ( "no tree generated" . to_string ( ) ) ) ?
452+ . id ,
453+ vec ! [ ObjectHash :: from_str( & expected_commit) . unwrap( ) ] ,
454+ & format ! ( "\n {commit_msg}" ) ,
455+ ) ;
456+
457+ let txn = self . storage . begin_db_transaction ( ) . await ?;
458+ let git_db = self . storage . git_db_storage ( ) ;
459+ for cmd in & commands_snapshot {
460+ if cmd. ref_type != RefTypeEnum :: Branch {
461+ continue ;
462+ }
463+ match cmd. command_type {
464+ CommandType :: Create => {
465+ git_db
466+ . save_ref_in_txn ( self . repo . repo_id , cmd. clone ( ) . into ( ) , & txn)
467+ . await ?;
468+ }
469+ CommandType :: Delete => {
470+ git_db
471+ . remove_ref_in_txn ( self . repo . repo_id , & cmd. ref_name , & txn)
472+ . await ?;
473+ }
474+ CommandType :: Update => {
475+ git_db
476+ . update_ref_in_txn ( self . repo . repo_id , & cmd. ref_name , & cmd. new_id , & txn)
477+ . await ?;
478+ }
479+ }
480+ }
481+
482+ match storage
483+ . attach_to_monorepo_parent_in_txn (
484+ & txn,
485+ root_ref_id,
486+ & expected_commit,
487+ & expected_tree,
488+ new_commit,
489+ save_trees. into ( ) ,
490+ )
491+ . await
492+ {
493+ Ok ( ( ) ) => {
494+ txn. commit ( ) . await . map_err ( MegaError :: Db ) ?;
495+ return Ok ( ( ) ) ;
496+ }
497+ Err ( MegaError :: StaleMonorepoRootRef ) if attempt + 1 < MAX_ATTACH_ATTEMPTS => {
498+ let _ = txn. rollback ( ) . await ;
499+ tracing:: warn!(
500+ attempt = attempt,
501+ repo_path = %self . repo. repo_path,
502+ "attach_to_monorepo_parent: root ref moved, retrying"
503+ ) ;
504+ tokio:: task:: yield_now ( ) . await ;
505+ }
506+ Err ( e) => {
507+ let _ = txn. rollback ( ) . await ;
508+ return Err ( e) ;
509+ }
510+ }
511+ }
512+
513+ Err ( MegaError :: Other (
514+ "attach_to_monorepo_parent: exceeded retry limit for concurrent root updates" . into ( ) ,
515+ ) )
429516 }
430517}
431518
0 commit comments