@@ -11,7 +11,10 @@ use but_hunk_assignment::{
1111 AbsorptionReason , AbsorptionTarget , CommitAbsorption , CommitMap , FileAbsorption ,
1212 GroupedChanges , HunkAssignment , convert_assignments_to_diff_specs,
1313} ;
14- use but_hunk_dependency:: ui:: { HunkLock , HunkLockTarget } ;
14+ use but_hunk_dependency:: ui:: {
15+ HunkDependencies , HunkLock , HunkLockTarget ,
16+ hunk_dependencies_for_workspace_changes_by_worktree_dir,
17+ } ;
1518use but_rebase:: graph_rebase:: mutate:: { InsertSide , RelativeTo } ;
1619use but_workspace:: ui:: StackDetails ;
1720use gitbutler_oplog:: {
@@ -97,13 +100,14 @@ pub fn absorption_plan(
97100 ctx : & mut Context ,
98101 target : AbsorptionTarget ,
99102) -> anyhow:: Result < Vec < CommitAbsorption > > {
100- let assignments = match target {
103+ let ( assignments, dependencies ) = match target {
101104 AbsorptionTarget :: Branch { branch_name } => {
102105 // Get all worktree changes, assignments, and dependencies
103106 // TODO: Ideally, there's a simpler way of getting the worktree changes without passing the context to it.
104107 // At this time, the context is passed pretty deep into the function.
105108 let worktree_changes = changes_in_worktree ( ctx) ?;
106109 let all_assignments = worktree_changes. assignments ;
110+ let dependencies = worktree_changes. dependencies ;
107111
108112 // Get the stack ID for this branch
109113 let stacks = crate :: legacy:: workspace:: stacks ( ctx, None ) ?;
@@ -131,7 +135,7 @@ pub fn absorption_plan(
131135 anyhow:: bail!( "No uncommitted changes assigned to branch: {branch_name}" ) ;
132136 }
133137
134- stack_assignments
138+ ( stack_assignments, dependencies )
135139 }
136140 AbsorptionTarget :: TreeChanges {
137141 changes,
@@ -140,6 +144,7 @@ pub fn absorption_plan(
140144 // Get all worktree changes, assignments, and dependencies
141145 let worktree_changes = changes_in_worktree ( ctx) ?;
142146 let all_assignments = worktree_changes. assignments ;
147+ let dependencies = worktree_changes. dependencies ;
143148
144149 // Filter assignments to just this stack
145150 let stack_assignments: Vec < _ > = all_assignments
@@ -155,23 +160,34 @@ pub fn absorption_plan(
155160 anyhow:: bail!( "No uncommitted changes assigned to stack: {assigned_stack_id:?}" ) ;
156161 }
157162
158- stack_assignments
163+ ( stack_assignments, dependencies)
164+ }
165+ AbsorptionTarget :: HunkAssignments { assignments } => {
166+ // Compute hunk dependencies only for this target since changes_in_worktree isn't called
167+ let ( _read_guard, repo, ws, _db) = ctx. workspace_and_db ( ) ?;
168+ let dependencies =
169+ hunk_dependencies_for_workspace_changes_by_worktree_dir ( & repo, & ws, None ) . ok ( ) ;
170+ drop ( ( _read_guard, repo, ws, _db) ) ;
171+ ( assignments, dependencies)
159172 }
160- AbsorptionTarget :: HunkAssignments { assignments } => assignments,
161173 AbsorptionTarget :: All => {
162174 // Get all worktree changes, assignments, and dependencies
163175 // TODO: Ideally, there's a simpler way of getting the worktree changes without passing the context to it.
164176 // At this time, the context is passed pretty deep into the function.
165177 let worktree_changes = changes_in_worktree ( ctx) ?;
166- worktree_changes. assignments
178+ ( worktree_changes. assignments , worktree_changes . dependencies )
167179 }
168180 } ;
169181
170182 let mut guard = ctx. exclusive_worktree_access ( ) ;
171183
172184 // Group all changes by their target commit
173- let changes_by_commit =
174- group_changes_by_target_commit ( ctx, & assignments, guard. write_permission ( ) ) ?;
185+ let changes_by_commit = group_changes_by_target_commit (
186+ ctx,
187+ & assignments,
188+ dependencies. as_ref ( ) ,
189+ guard. write_permission ( ) ,
190+ ) ?;
175191
176192 // Prepare commit absorptions for display
177193 let commit_absorptions = prepare_commit_absorptions ( ctx, changes_by_commit) ?;
@@ -183,17 +199,30 @@ pub fn absorption_plan(
183199fn group_changes_by_target_commit (
184200 ctx : & mut Context ,
185201 assignments : & [ HunkAssignment ] ,
202+ dependencies : Option < & HunkDependencies > ,
186203 perm : & mut RepoExclusive ,
187204) -> anyhow:: Result < GroupedChanges > {
188205 let mut changes_by_commit: GroupedChanges = BTreeMap :: new ( ) ;
189206
190207 let mut stack_details_cache = HashMap :: < StackId , StackDetails > :: new ( ) ;
191208
209+ // Build an index for O(1) lock lookups per assignment
210+ let lock_index = dependencies. map ( build_lock_index) ;
211+
192212 // Process each assignment
193213 for assignment in assignments {
194214 // Determine the target commit for this assignment
195- let ( stack_id, commit_id, reason) =
196- determine_target_commit ( ctx, assignment, & mut stack_details_cache, perm) ?;
215+ let locks = lock_index
216+ . as_ref ( )
217+ . map ( |idx| locks_for_assignment ( idx, assignment) )
218+ . filter ( |l| !l. is_empty ( ) ) ;
219+ let ( stack_id, commit_id, reason) = determine_target_commit (
220+ ctx,
221+ assignment,
222+ locks. as_deref ( ) ,
223+ & mut stack_details_cache,
224+ perm,
225+ ) ?;
197226
198227 let entry = changes_by_commit
199228 . entry ( ( stack_id, commit_id) )
@@ -209,6 +238,71 @@ fn group_changes_by_target_commit(
209238 Ok ( changes_by_commit)
210239}
211240
241+ /// Per-file entries of `(DiffHunk, locks)` for range-based lock matching.
242+ type LockIndex = HashMap < String , Vec < ( but_core:: unified_diff:: DiffHunk , Vec < HunkLock > ) > > ;
243+
244+ /// Build a lookup index from hunk dependencies, grouped by file path.
245+ ///
246+ /// Each entry retains the original `DiffHunk` range so that lookups can match
247+ /// by range overlap rather than exact header equality. This is necessary because
248+ /// dependency hunks are computed with 0 context lines while assignment hunks use
249+ /// the user's `context_lines` setting, so their headers differ.
250+ fn build_lock_index ( dependencies : & HunkDependencies ) -> LockIndex {
251+ let mut index = LockIndex :: new ( ) ;
252+ for ( path, diff_hunk, locks) in & dependencies. diffs {
253+ index
254+ . entry ( path. clone ( ) )
255+ . or_default ( )
256+ . push ( ( diff_hunk. clone ( ) , locks. clone ( ) ) ) ;
257+ }
258+ index
259+ }
260+
261+ /// Check whether two line ranges overlap.
262+ /// Ranges are `[start, start + lines)` (1-based start, length in lines).
263+ /// A range with 0 lines (pure insertion/deletion) is treated as a point at `start`.
264+ fn ranges_overlap ( start_a : u32 , lines_a : u32 , start_b : u32 , lines_b : u32 ) -> bool {
265+ let end_a = start_a + lines_a. max ( 1 ) ;
266+ let end_b = start_b + lines_b. max ( 1 ) ;
267+ start_a < end_b && start_b < end_a
268+ }
269+
270+ /// Look up the dependency locks for an assignment by finding dependency hunks
271+ /// whose ranges overlap with the assignment's hunk header.
272+ ///
273+ /// When the assignment has no hunk header (binary/too-large diffs), all locks
274+ /// for the file are returned as a fallback.
275+ fn locks_for_assignment ( index : & LockIndex , assignment : & HunkAssignment ) -> Vec < HunkLock > {
276+ let Some ( file_entries) = index. get ( & assignment. path ) else {
277+ return Vec :: new ( ) ;
278+ } ;
279+
280+ match assignment. hunk_header {
281+ Some ( hunk_header) => {
282+ let mut locks = Vec :: new ( ) ;
283+ for ( dep_hunk, dep_locks) in file_entries {
284+ // Match on the new-file side: assignment hunks describe worktree
285+ // state (new), and dependency hunks record which committed ranges
286+ // they depend on.
287+ if ranges_overlap (
288+ dep_hunk. new_start ,
289+ dep_hunk. new_lines ,
290+ hunk_header. new_start ,
291+ hunk_header. new_lines ,
292+ ) {
293+ locks. extend ( dep_locks. iter ( ) . cloned ( ) ) ;
294+ }
295+ }
296+ locks
297+ }
298+ // No hunk header (binary/too-large) — return all locks for the file.
299+ None => file_entries
300+ . iter ( )
301+ . flat_map ( |( _, locks) | locks. iter ( ) . cloned ( ) )
302+ . collect ( ) ,
303+ }
304+ }
305+
212306// Find the lock that is highest in the application order (child-most commit)
213307fn find_top_most_lock < ' a > (
214308 locks : & ' a [ HunkLock ] ,
@@ -252,6 +346,7 @@ fn find_top_most_lock<'a>(
252346fn determine_target_commit (
253347 ctx : & mut Context ,
254348 assignment : & HunkAssignment ,
349+ locks : Option < & [ HunkLock ] > ,
255350 stack_details_cache : & mut HashMap < StackId , StackDetails > ,
256351 perm : & mut RepoExclusive ,
257352) -> anyhow:: Result < (
@@ -260,7 +355,7 @@ fn determine_target_commit(
260355 AbsorptionReason ,
261356) > {
262357 // Priority 1: Check if there's a dependency lock for this hunk
263- if let Some ( locks) = & assignment . hunk_locks {
358+ if let Some ( locks) = locks {
264359 if let Some ( lock) = find_top_most_lock ( locks, ctx, stack_details_cache) {
265360 if let HunkLockTarget :: Stack ( stack_id) = lock. target {
266361 return Ok ( ( stack_id, lock. commit_id , AbsorptionReason :: HunkDependency ) ) ;
0 commit comments