@@ -6,27 +6,49 @@ use std::{
66
77use git_internal:: {
88 errors:: GitError ,
9+ hash:: SHA1 ,
910 internal:: object:: {
1011 commit:: Commit ,
11- tree:: { Tree , TreeItem } ,
12+ tree:: { Tree , TreeItem , TreeItemMode } ,
1213 } ,
1314} ;
1415
1516use crate :: api_service:: ApiHandler ;
1617
18+ const MAX_ITERATIONS : usize = 10_000 ;
19+
20+ /// Parent commit info with pre-computed item hash map for batch lookup.
21+ struct ParentInfo {
22+ commit : Arc < Commit > ,
23+ /// Map of item name -> (hash, mode) for quick TREESAME check
24+ item_hashes : HashMap < String , ( SHA1 , TreeItemMode ) > ,
25+ }
26+
27+ /// State for tracking a single item's traversal through commit history.
28+ /// Used by the batch traversal algorithm in `item_to_commit_map`.
29+ struct ItemTraversalState {
30+ /// The tree item being tracked
31+ item : TreeItem ,
32+ /// Current commit being examined for this item
33+ current_commit_id : SHA1 ,
34+ /// Hash of the item in the current commit
35+ current_hash : SHA1 ,
36+ /// Whether this item's last modification has been determined
37+ determined : bool ,
38+ /// The commit where this item was last modified (set when determined)
39+ result_commit : Option < Commit > ,
40+ }
41+
1742/// Builds a mapping from each `TreeItem` under a given path to the commit
1843/// where that item was last modified.
1944///
20- /// This function retrieves the tree corresponding to the given `path`, then for each entry
21- /// (file or subdirectory) in that tree, it traverses the commit history backwards to find
22- /// the most recent commit where the item's hash changed (i.e., was modified).
45+ /// This function retrieves the tree corresponding to the given `path`, then uses a
46+ /// batch traversal algorithm to find the most recent commit where each item's hash
47+ /// changed (i.e., was modified).
2348///
2449/// If a `ref` (reference, such as a tag) is provided, traversal starts from the commit
2550/// that the reference points to. Otherwise, it starts from the repository's root commit.
2651///
27- /// Internally, it leverages [`traverse_commit_history_for_last_modification`] to perform
28- /// the traversal and uses a shared [`GitObjectCache`] to minimize redundant commit/tree lookups.
29- ///
3052/// # Arguments
3153/// - `path`: The path to the target directory or subtree to analyze.
3254/// - `reference`: Optional reference (e.g., tag name) to start traversal from.
@@ -43,21 +65,26 @@ use crate::api_service::ApiHandler;
4365/// # Algorithm
4466/// 1. Resolve the starting commit (from `reference` if provided, otherwise root commit).
4567/// 2. Resolve the tree at the given `path` at the starting commit.
46- /// 3. For each entry (`TreeItem`) in the resolved tree:
47- /// - Call [`traverse_commit_history_for_last_modification`] to find the most recent commit
48- /// where that item's hash changed, starting from the resolved commit and traversing backwards.
49- /// - Store the mapping in the result map.
50- /// 4. Return the complete mapping.
68+ /// 3. Initialize traversal state for all items, grouping them by current commit.
69+ /// 4. For each commit group:
70+ /// - Load parent trees once (shared by all items in the group).
71+ /// - For each item, check if any parent has TREESAME content (same hash and mode).
72+ /// - If TREESAME: continue traversing to that parent.
73+ /// - If no TREESAME parent: this commit modified the item.
74+ /// 5. Return the complete mapping.
5175///
5276/// # Performance Notes
53- /// - Uses a shared `Arc<Mutex<GitObjectCache>>` for caching to avoid redundant
54- /// lookups of commits and trees.
55- /// - Each item is processed sequentially, so total runtime scales linearly
56- /// with the number of items in the directory.
77+ /// - Uses batch processing to group items by their current commit, reducing redundant
78+ /// parent tree loads when multiple items are at the same commit.
79+ /// - Uses a commit cache to avoid reloading the same commits.
80+ /// - Uses TREESAME pruning similar to `git log --follow` for efficiency.
81+ ///
82+ /// # Limitations
83+ /// - If `MAX_ITERATIONS` is exceeded, remaining undetermined items fall back to
84+ /// the start commit.
5785///
5886/// # See Also
59- /// - [`traverse_commit_history_for_last_modification`] — for the logic of commit traversal
60- /// and last modification detection.
87+ /// - [`traverse_commit_history_for_last_modification`] — for single-item traversal.
6188pub async fn item_to_commit_map < T : ApiHandler + ?Sized > (
6289 handler : & T ,
6390 path : PathBuf ,
@@ -68,7 +95,6 @@ pub async fn item_to_commit_map<T: ApiHandler + ?Sized>(
6895 crate :: api_service:: commit_ops:: resolve_start_commit ( handler, reference) . await ?;
6996
7097 // Get the tree at the specified path from the resolved start commit
71- // Use the start commit's tree to ensure consistency
7298 let start_tree = handler
7399 . get_tree_by_hash ( & start_commit_arc. tree_id . to_string ( ) )
74100 . await ?;
@@ -78,19 +104,186 @@ pub async fn item_to_commit_map<T: ApiHandler + ?Sized>(
78104 return Ok ( HashMap :: new ( ) ) ;
79105 } ;
80106
81- // For each item in the tree, traverse commit history to find its last modification
82107 let tree_items = tree. tree_items . clone ( ) ;
83- let mut result = HashMap :: with_capacity ( tree_items. len ( ) ) ;
84-
85- for item in tree_items {
86- let commit = traverse_commit_history_for_last_modification (
87- handler,
88- & path,
89- start_commit_arc. clone ( ) ,
90- & item,
91- )
92- . await ?;
93- result. insert ( item, Some ( commit) ) ;
108+ let item_count = tree_items. len ( ) ;
109+
110+ if item_count == 0 {
111+ return Ok ( HashMap :: new ( ) ) ;
112+ }
113+
114+ // Initialize traversal state for all items
115+ let mut item_states: Vec < ItemTraversalState > = tree_items
116+ . into_iter ( )
117+ . map ( |item| ItemTraversalState {
118+ current_hash : item. id ,
119+ item,
120+ current_commit_id : start_commit_arc. id ,
121+ determined : false ,
122+ result_commit : None ,
123+ } )
124+ . collect ( ) ;
125+
126+ let mut pending_count = item_count;
127+
128+ // Cache for commits we've loaded
129+ let mut commit_cache: HashMap < SHA1 , Arc < Commit > > = HashMap :: new ( ) ;
130+ commit_cache. insert ( start_commit_arc. id , start_commit_arc. clone ( ) ) ;
131+
132+ let mut iteration = 0 ;
133+
134+ while pending_count > 0 {
135+ iteration += 1 ;
136+ if iteration > MAX_ITERATIONS {
137+ tracing:: warn!(
138+ "[HISTORY] Exceeded max iterations ({}) for path {:?}, {} items still pending" ,
139+ MAX_ITERATIONS ,
140+ path,
141+ pending_count
142+ ) ;
143+ break ;
144+ }
145+
146+ // Group undetermined items by their current commit
147+ let mut commit_groups: HashMap < SHA1 , Vec < usize > > = HashMap :: new ( ) ;
148+ for ( idx, state) in item_states. iter ( ) . enumerate ( ) {
149+ if !state. determined {
150+ commit_groups
151+ . entry ( state. current_commit_id )
152+ . or_default ( )
153+ . push ( idx) ;
154+ }
155+ }
156+
157+ if commit_groups. is_empty ( ) {
158+ break ;
159+ }
160+
161+ let mut any_progress = false ;
162+
163+ // Process each commit group
164+ for ( commit_id, item_indices) in commit_groups {
165+ // Get the commit (from cache or load)
166+ let commit = if let Some ( c) = commit_cache. get ( & commit_id) {
167+ c. clone ( )
168+ } else {
169+ let c = handler
170+ . object_cache ( )
171+ . get_commit ( commit_id, |id| async move {
172+ handler. get_commit_by_hash ( & id. to_string ( ) ) . await
173+ } )
174+ . await ?;
175+ commit_cache. insert ( commit_id, c. clone ( ) ) ;
176+ c
177+ } ;
178+
179+ // No parents: all items in this group were introduced in this commit
180+ if commit. parent_commit_ids . is_empty ( ) {
181+ for & idx in & item_indices {
182+ if !item_states[ idx] . determined {
183+ item_states[ idx] . determined = true ;
184+ item_states[ idx] . result_commit = Some ( ( * commit) . clone ( ) ) ;
185+ pending_count -= 1 ;
186+ any_progress = true ;
187+ }
188+ }
189+ continue ;
190+ }
191+
192+ // Load all parent trees once (shared by all items in this group)
193+ let mut parent_data: Vec < ParentInfo > = Vec :: new ( ) ;
194+
195+ for & parent_id in & commit. parent_commit_ids {
196+ let parent_commit = if let Some ( c) = commit_cache. get ( & parent_id) {
197+ c. clone ( )
198+ } else {
199+ let c = handler
200+ . object_cache ( )
201+ . get_commit ( parent_id, |id| async move {
202+ handler. get_commit_by_hash ( & id. to_string ( ) ) . await
203+ } )
204+ . await ?;
205+ commit_cache. insert ( parent_id, c. clone ( ) ) ;
206+ c
207+ } ;
208+
209+ let parent_tree = handler
210+ . object_cache ( )
211+ . get_tree ( parent_commit. tree_id , |id| async move {
212+ handler. get_tree_by_hash ( & id. to_string ( ) ) . await
213+ } )
214+ . await ?;
215+
216+ let parent_target_tree_opt = navigate_to_tree ( handler, parent_tree, & path) . await ?;
217+
218+ // Build hash map for this parent's items
219+ let parent_item_hashes: HashMap < String , ( SHA1 , TreeItemMode ) > =
220+ if let Some ( ref t) = parent_target_tree_opt {
221+ t. tree_items
222+ . iter ( )
223+ . map ( |item| ( item. name . clone ( ) , ( item. id , item. mode ) ) )
224+ . collect ( )
225+ } else {
226+ HashMap :: new ( )
227+ } ;
228+
229+ parent_data. push ( ParentInfo {
230+ commit : parent_commit,
231+ item_hashes : parent_item_hashes,
232+ } ) ;
233+ }
234+
235+ // Process each item in this commit group
236+ for & idx in & item_indices {
237+ let state = & mut item_states[ idx] ;
238+ if state. determined {
239+ continue ;
240+ }
241+
242+ let item_name = & state. item . name ;
243+ let item_mode = state. item . mode ;
244+ let current_hash = state. current_hash ;
245+
246+ // Find a parent that has the same hash (TREESAME)
247+ let mut found_matching_parent = false ;
248+
249+ for parent in & parent_data {
250+ if let Some ( & ( parent_hash, parent_mode) ) = parent. item_hashes . get ( item_name)
251+ && parent_hash == current_hash
252+ && parent_mode == item_mode
253+ {
254+ // TREESAME: continue traversing to this parent
255+ state. current_commit_id = parent. commit . id ;
256+ state. current_hash = parent_hash;
257+ found_matching_parent = true ;
258+ any_progress = true ;
259+
260+ break ;
261+ }
262+ }
263+
264+ if !found_matching_parent {
265+ // No parent has the same hash: this commit modified the item
266+ state. determined = true ;
267+ state. result_commit = Some ( ( * commit) . clone ( ) ) ;
268+ pending_count -= 1 ;
269+ any_progress = true ;
270+ }
271+ }
272+ }
273+
274+ if !any_progress {
275+ break ;
276+ }
277+ }
278+
279+ // Build the result map
280+ let mut result = HashMap :: with_capacity ( item_count) ;
281+ for state in item_states {
282+ // Use result_commit, or fallback to start_commit if undetermined
283+ let commit = state
284+ . result_commit
285+ . or_else ( || Some ( ( * start_commit_arc) . clone ( ) ) ) ;
286+ result. insert ( state. item , commit) ;
94287 }
95288
96289 Ok ( result)
@@ -103,7 +296,6 @@ pub async fn item_to_commit_map<T: ApiHandler + ?Sized>(
103296/// - `handler`: The API handler for Git operations.
104297/// - `root_tree`: The root tree to start navigation from.
105298/// - `path`: The path to navigate to.
106- /// - `cache`: A shared cache for tree lookups.
107299///
108300/// # Returns
109301/// - `Ok(Some(Arc<Tree>))` if the path exists and the target tree is found.
@@ -313,8 +505,6 @@ pub async fn traverse_commit_history_for_last_modification<T: ApiHandler + ?Size
313505 visited. insert ( start_commit. id ) ;
314506 commit_queue. push_back ( ( start_commit. clone ( ) , current_hash) ) ;
315507
316- // Safety limit to prevent unbounded memory usage in complex histories
317- const MAX_ITERATIONS : usize = 10_000 ;
318508 let mut iterations = 0 ;
319509
320510 while let Some ( ( commit, commit_hash) ) = commit_queue. pop_front ( ) {
0 commit comments