@@ -140,6 +140,14 @@ pub struct RefUpdate {
140140 tree_id : ObjectHash ,
141141}
142142
143+ struct ApplyChangeContext < ' a > {
144+ components : & ' a [ String ] ,
145+ chain_paths : & ' a [ PathBuf ] ,
146+ chain_trees : & ' a [ Tree ] ,
147+ tree_cache : & ' a mut HashMap < PathBuf , Tree > ,
148+ new_trees : & ' a mut HashMap < ObjectHash , Tree > ,
149+ }
150+
143151/// `MonoServiceLogic` is a helper struct for `MonoApiService` containing stateless logic.
144152///
145153/// It encapsulates the pure logic methods of `MonoApiService` that do not depend on
@@ -1479,7 +1487,7 @@ impl MonoApiService {
14791487 } ;
14801488
14811489 // Build chain of trees from root to parent, using updated cache when available
1482- let mut components: Vec < String > = parent_path
1490+ let components: Vec < String > = parent_path
14831491 . components ( )
14841492 . filter_map ( |c| match c {
14851493 std:: path:: Component :: RootDir => None ,
@@ -1496,13 +1504,12 @@ impl MonoApiService {
14961504 ] ;
14971505
14981506 let mut cursor = PathBuf :: from ( "/" ) ;
1499- for comp in & components {
1500- cursor = cursor . join ( comp ) ;
1507+ let mut missing_components : Option < Vec < String > > = None ;
1508+ for ( idx , comp ) in components . iter ( ) . enumerate ( ) {
15011509 let parent_tree = chain_trees
15021510 . last ( )
15031511 . ok_or_else ( || GitError :: CustomError ( "Empty tree chain" . to_string ( ) ) ) ?;
15041512
1505- // Try to find existing child directory; if missing, create an empty tree placeholder
15061513 let maybe_child = parent_tree. tree_items . iter ( ) . find ( |it| it. name == * comp) ;
15071514 let child_tree = if let Some ( child_item) = maybe_child {
15081515 if child_item. mode != TreeItemMode :: Tree {
@@ -1518,6 +1525,7 @@ impl MonoApiService {
15181525 comp
15191526 ) ) ) ;
15201527 }
1528+ cursor = cursor. join ( comp) ;
15211529 let child_hash = child_item. id ;
15221530 if let Some ( cached) = tree_cache. get ( & cursor) {
15231531 cached. clone ( )
@@ -1542,26 +1550,30 @@ impl MonoApiService {
15421550 Tree :: from_mega_model ( model)
15431551 }
15441552 } else {
1545- // Absent in target head: create an empty directory tree and continue
1546- let empty = Tree :: from_tree_items ( Vec :: new ( ) )
1547- . map_err ( |e| GitError :: CustomError ( e. to_string ( ) ) ) ?;
1548- // Cache the newly created empty directory so subsequent diffs within
1549- // the same path chain reuse the same tree instance.
1550- debug ! (
1551- cl_link = %cl. link,
1552- path_component = %comp,
1553- cursor = %cursor. to_string_lossy( ) ,
1554- "apply_changes: synthesize missing directory"
1555- ) ;
1556- tree_cache. insert ( cursor. clone ( ) , empty. clone ( ) ) ;
1557- new_trees. insert ( empty. id , empty. clone ( ) ) ;
1558- empty
1553+ missing_components = Some ( components[ idx..] . to_vec ( ) ) ;
1554+ break ;
15591555 } ;
15601556
15611557 chain_paths. push ( cursor. clone ( ) ) ;
15621558 chain_trees. push ( child_tree) ;
15631559 }
15641560
1561+ if let Some ( missing) = missing_components {
1562+ let mut ctx = ApplyChangeContext {
1563+ components : & components,
1564+ chain_paths : & chain_paths,
1565+ chain_trees : & chain_trees,
1566+ tree_cache : & mut tree_cache,
1567+ new_trees : & mut new_trees,
1568+ } ;
1569+ if let Some ( updated_root) =
1570+ Self :: apply_missing_path_update ( & cl. link , missing, op, file_name, & mut ctx) ?
1571+ {
1572+ root_tree = updated_root;
1573+ }
1574+ continue ;
1575+ }
1576+
15651577 let parent_dir_abs = cursor. clone ( ) ;
15661578
15671579 // Update parent tree with the file change
@@ -1588,7 +1600,7 @@ impl MonoApiService {
15881600 }
15891601 }
15901602
1591- let mut updated_tree =
1603+ let updated_tree =
15921604 Tree :: from_tree_items ( items) . map_err ( |e| GitError :: CustomError ( e. to_string ( ) ) ) ?;
15931605 // If parent tree id did not change (no-op), skip propagation for this diff.
15941606 if updated_tree. id == parent_tree. id {
@@ -1601,54 +1613,23 @@ impl MonoApiService {
16011613 ) ;
16021614 continue ;
16031615 }
1604- tree_cache. insert ( parent_dir_abs, updated_tree. clone ( ) ) ;
1605- new_trees. insert ( updated_tree. id , updated_tree. clone ( ) ) ;
1616+ Self :: record_tree (
1617+ parent_dir_abs,
1618+ & updated_tree,
1619+ & mut tree_cache,
1620+ & mut new_trees,
1621+ ) ;
16061622
16071623 // Propagate updated hashes up to root
1608- components. reverse ( ) ;
1609- for ( idx, comp) in components. iter ( ) . enumerate ( ) {
1610- let parent_index = chain_trees
1611- . len ( )
1612- . checked_sub ( idx + 1 )
1613- . ok_or_else ( || GitError :: CustomError ( "Tree chain underflow" . to_string ( ) ) ) ?;
1614-
1615- let mut parent_tree = chain_trees[ parent_index] . clone ( ) ;
1616- let mut parent_items = parent_tree. tree_items . clone ( ) ;
1617- if let Some ( pos) = parent_items. iter ( ) . position ( |it| it. name == * comp) {
1618- parent_items[ pos] . id = updated_tree. id ;
1619- } else {
1620- // Missing entry: create a new directory item pointing to updated child
1621- parent_items. push ( TreeItem :: new (
1622- TreeItemMode :: Tree ,
1623- updated_tree. id ,
1624- comp. clone ( ) ,
1625- ) ) ;
1626- // Keep deterministic ordering
1627- parent_items. sort_by ( |a, b| a. name . cmp ( & b. name ) ) ;
1628- debug ! (
1629- cl_link = %cl. link,
1630- parent_path = %chain_paths
1631- . get( parent_index)
1632- . map( |p| p. to_string_lossy( ) . to_string( ) )
1633- . unwrap_or_else( || "<missing>" . into( ) ) ,
1634- created_entry = %comp,
1635- "apply_changes: inserted missing parent entry"
1636- ) ;
1637- }
1638-
1639- parent_tree = Tree :: from_tree_items ( parent_items)
1640- . map_err ( |e| GitError :: CustomError ( e. to_string ( ) ) ) ?;
1641-
1642- let parent_path_idx = chain_paths. get ( parent_index) . cloned ( ) . ok_or_else ( || {
1643- GitError :: CustomError ( "Tree path chain underflow" . to_string ( ) )
1644- } ) ?;
1645- tree_cache. insert ( parent_path_idx. clone ( ) , parent_tree. clone ( ) ) ;
1646- new_trees. insert ( parent_tree. id , parent_tree. clone ( ) ) ;
1647- updated_tree = parent_tree;
1648- }
1649-
1650- // After propagation, updated_tree is the new root
1651- root_tree = updated_tree;
1624+ root_tree = Self :: propagate_up (
1625+ & cl. link ,
1626+ updated_tree,
1627+ & components,
1628+ & chain_paths,
1629+ & chain_trees,
1630+ & mut tree_cache,
1631+ & mut new_trees,
1632+ ) ?;
16521633 }
16531634
16541635 let result = TreeUpdateResult {
@@ -1673,6 +1654,205 @@ impl MonoApiService {
16731654 . await
16741655 }
16751656
1657+ fn apply_missing_path_update (
1658+ cl_link : & str ,
1659+ missing : Vec < String > ,
1660+ op : Option < ObjectHash > ,
1661+ file_name : & str ,
1662+ ctx : & mut ApplyChangeContext < ' _ > ,
1663+ ) -> Result < Option < Tree > , GitError > {
1664+ debug_assert ! (
1665+ !missing. iter( ) . any( |c| c == file_name) ,
1666+ "missing path components should not include file name"
1667+ ) ;
1668+ if op. is_none ( ) {
1669+ debug ! (
1670+ cl_link,
1671+ missing_path = %missing. join( "/" ) ,
1672+ "apply_changes: delete on missing path (no-op)"
1673+ ) ;
1674+ return Ok ( None ) ;
1675+ }
1676+
1677+ let new_hash = op. ok_or_else ( || {
1678+ GitError :: CustomError ( "Missing blob hash for new/modified file" . to_string ( ) )
1679+ } ) ?;
1680+
1681+ if missing. is_empty ( ) {
1682+ // No missing directories: update directly under the last existing parent.
1683+ let parent_path = ctx. chain_paths . last ( ) . cloned ( ) . unwrap_or_else ( PathBuf :: new) ;
1684+ let parent_tree = ctx
1685+ . chain_trees
1686+ . last ( )
1687+ . cloned ( )
1688+ . ok_or_else ( || GitError :: CustomError ( "Root tree missing" . to_string ( ) ) ) ?;
1689+ let updated_tree = Self :: update_parent_tree (
1690+ cl_link,
1691+ & parent_tree,
1692+ file_name,
1693+ TreeItemMode :: Blob ,
1694+ new_hash,
1695+ None ,
1696+ ) ?;
1697+ Self :: record_tree ( parent_path, & updated_tree, ctx. tree_cache , ctx. new_trees ) ;
1698+
1699+ return Ok ( Some ( Self :: propagate_up (
1700+ cl_link,
1701+ updated_tree,
1702+ ctx. components ,
1703+ ctx. chain_paths ,
1704+ ctx. chain_trees ,
1705+ ctx. tree_cache ,
1706+ ctx. new_trees ,
1707+ ) ?) ) ;
1708+ }
1709+
1710+ // Build missing subtree from leaf (parent dir) upward without empty trees.
1711+ let file_item = TreeItem :: new ( TreeItemMode :: Blob , new_hash, file_name. to_string ( ) ) ;
1712+ let mut updated_tree = Tree :: from_tree_items ( vec ! [ file_item] )
1713+ . map_err ( |e| GitError :: CustomError ( e. to_string ( ) ) ) ?;
1714+
1715+ let mut missing_paths: Vec < PathBuf > = Vec :: new ( ) ;
1716+ let mut base = ctx. chain_paths . last ( ) . cloned ( ) . unwrap_or_else ( PathBuf :: new) ;
1717+ for comp in & missing {
1718+ base = base. join ( comp) ;
1719+ missing_paths. push ( base. clone ( ) ) ;
1720+ }
1721+
1722+ if let Some ( parent_path) = missing_paths. last ( ) {
1723+ Self :: record_tree (
1724+ parent_path. clone ( ) ,
1725+ & updated_tree,
1726+ ctx. tree_cache ,
1727+ ctx. new_trees ,
1728+ ) ;
1729+ } else {
1730+ Self :: record_tree ( PathBuf :: new ( ) , & updated_tree, ctx. tree_cache , ctx. new_trees ) ;
1731+ }
1732+
1733+ for ( child_name, path) in missing
1734+ . iter ( )
1735+ . rev ( )
1736+ . skip ( 1 )
1737+ . zip ( missing_paths. iter ( ) . rev ( ) . skip ( 1 ) )
1738+ {
1739+ let wrapper = Tree :: from_tree_items ( vec ! [ TreeItem :: new(
1740+ TreeItemMode :: Tree ,
1741+ updated_tree. id,
1742+ child_name. clone( ) ,
1743+ ) ] )
1744+ . map_err ( |e| GitError :: CustomError ( e. to_string ( ) ) ) ?;
1745+ updated_tree = wrapper;
1746+ Self :: record_tree ( path. clone ( ) , & updated_tree, ctx. tree_cache , ctx. new_trees ) ;
1747+ }
1748+
1749+ // Attach the newly built subtree to the last existing parent.
1750+ let parent_tree = ctx
1751+ . chain_trees
1752+ . last ( )
1753+ . cloned ( )
1754+ . ok_or_else ( || GitError :: CustomError ( "Root tree missing" . to_string ( ) ) ) ?;
1755+ let attach_name = missing
1756+ . first ( )
1757+ . ok_or_else ( || GitError :: CustomError ( "Missing component chain empty" . to_string ( ) ) ) ?;
1758+ updated_tree = Self :: update_parent_tree (
1759+ cl_link,
1760+ & parent_tree,
1761+ attach_name,
1762+ TreeItemMode :: Tree ,
1763+ updated_tree. id ,
1764+ None ,
1765+ ) ?;
1766+ let parent_path = ctx. chain_paths . last ( ) . cloned ( ) . unwrap_or_else ( PathBuf :: new) ;
1767+ Self :: record_tree ( parent_path, & updated_tree, ctx. tree_cache , ctx. new_trees ) ;
1768+
1769+ Ok ( Some ( Self :: propagate_up (
1770+ cl_link,
1771+ updated_tree,
1772+ ctx. components ,
1773+ ctx. chain_paths ,
1774+ ctx. chain_trees ,
1775+ ctx. tree_cache ,
1776+ ctx. new_trees ,
1777+ ) ?) )
1778+ }
1779+
1780+ fn update_parent_tree (
1781+ cl_link : & str ,
1782+ parent_tree : & Tree ,
1783+ name : & str ,
1784+ mode : TreeItemMode ,
1785+ id : ObjectHash ,
1786+ debug_parent_path : Option < & PathBuf > ,
1787+ ) -> Result < Tree , GitError > {
1788+ let mut parent_items = parent_tree. tree_items . clone ( ) ;
1789+ if let Some ( pos) = parent_items. iter ( ) . position ( |it| it. name == name) {
1790+ parent_items[ pos] . id = id;
1791+ } else {
1792+ parent_items. push ( TreeItem :: new ( mode, id, name. to_string ( ) ) ) ;
1793+ parent_items. sort_by ( |a, b| a. name . cmp ( & b. name ) ) ;
1794+ if let Some ( path) = debug_parent_path {
1795+ debug ! (
1796+ cl_link,
1797+ parent_path = %path. to_string_lossy( ) ,
1798+ created_entry = %name,
1799+ "apply_changes: inserted missing parent entry"
1800+ ) ;
1801+ }
1802+ }
1803+
1804+ Tree :: from_tree_items ( parent_items) . map_err ( |e| GitError :: CustomError ( e. to_string ( ) ) )
1805+ }
1806+
1807+ fn record_tree (
1808+ path : PathBuf ,
1809+ tree : & Tree ,
1810+ tree_cache : & mut HashMap < PathBuf , Tree > ,
1811+ new_trees : & mut HashMap < ObjectHash , Tree > ,
1812+ ) {
1813+ tree_cache. insert ( path, tree. clone ( ) ) ;
1814+ new_trees. insert ( tree. id , tree. clone ( ) ) ;
1815+ }
1816+
1817+ fn propagate_up (
1818+ cl_link : & str ,
1819+ mut updated_tree : Tree ,
1820+ components : & [ String ] ,
1821+ chain_paths : & [ PathBuf ] ,
1822+ chain_trees : & [ Tree ] ,
1823+ tree_cache : & mut HashMap < PathBuf , Tree > ,
1824+ new_trees : & mut HashMap < ObjectHash , Tree > ,
1825+ ) -> Result < Tree , GitError > {
1826+ debug_assert ! (
1827+ components. len( ) >= chain_trees. len( ) . saturating_sub( 1 ) ,
1828+ "components length must cover parent chain"
1829+ ) ;
1830+
1831+ for parent_index in ( 0 ..chain_trees. len ( ) . saturating_sub ( 1 ) ) . rev ( ) {
1832+ let comp = components
1833+ . get ( parent_index)
1834+ . ok_or_else ( || GitError :: CustomError ( "Tree path chain underflow" . to_string ( ) ) ) ?;
1835+
1836+ let parent_tree = Self :: update_parent_tree (
1837+ cl_link,
1838+ & chain_trees[ parent_index] ,
1839+ comp,
1840+ TreeItemMode :: Tree ,
1841+ updated_tree. id ,
1842+ chain_paths. get ( parent_index) ,
1843+ ) ?;
1844+
1845+ let parent_path_idx = chain_paths
1846+ . get ( parent_index)
1847+ . cloned ( )
1848+ . ok_or_else ( || GitError :: CustomError ( "Tree path chain underflow" . to_string ( ) ) ) ?;
1849+ Self :: record_tree ( parent_path_idx, & parent_tree, tree_cache, new_trees) ;
1850+ updated_tree = parent_tree;
1851+ }
1852+
1853+ Ok ( updated_tree)
1854+ }
1855+
16761856 /// Merges a CL without checking for conflicts.
16771857 /// Caller is responsible for ensuring no conflicts exist before calling this method.
16781858 async fn merge_cl_unchecked ( & self , username : & str , cl : mega_cl:: Model ) -> Result < ( ) , GitError > {
0 commit comments