@@ -3,13 +3,17 @@ use but_core::ref_metadata::StackId;
33use but_ctx:: { Context , access:: RepoExclusive } ;
44use but_rebase:: { Rebase , RebaseStep } ;
55use but_workspace:: legacy:: stack_ext:: StackExt ;
6+ use gitbutler_oplog:: {
7+ OplogExt ,
8+ entry:: { OperationKind , SnapshotDetails } ,
9+ } ;
610use gitbutler_reference:: { LocalRefname , Refname } ;
7- use gitbutler_stack:: { StackBranch , VirtualBranchesHandle } ;
11+ use gitbutler_stack:: StackBranch ;
812use gitbutler_workspace:: branch_trees:: { WorkspaceState , update_uncommitted_changes} ;
913use gix:: refs:: transaction:: PreviousValue ;
1014use serde:: Serialize ;
1115
12- use crate :: BranchManagerExt ;
16+ use crate :: { BranchManagerExt , VirtualBranchesExt as _ , move_commits :: bail_on_new_conflicts } ;
1317
1418#[ derive( Debug , Clone , Serialize ) ]
1519#[ serde( rename_all = "camelCase" ) ]
@@ -22,55 +26,141 @@ pub struct MoveBranchResult {
2226}
2327
2428pub ( crate ) fn move_branch (
25- ctx : & Context ,
29+ ctx : & mut Context ,
2630 target_stack_id : StackId ,
2731 target_branch_name : & str ,
2832 source_stack_id : StackId ,
2933 subject_branch_name : & str ,
3034 perm : & mut RepoExclusive ,
3135) -> Result < MoveBranchResult > {
3236 let old_workspace = WorkspaceState :: create ( ctx, perm. read_permission ( ) ) ?;
33- let repo = ctx. repo . get ( ) ?;
34- let mut vb_state = VirtualBranchesHandle :: new ( ctx. project_data_dir ( ) ) ;
3537
36- let source_stack = vb_state. get_stack_in_workspace ( source_stack_id) ?;
37- let source_merge_base = source_stack. merge_base ( ctx) ?;
38+ let ( source_merge_base, dest_merge_base, source_branch_pr_number) = {
39+ let vb_state = ctx. virtual_branches ( ) ;
40+ let source_stack = vb_state. get_stack_in_workspace ( source_stack_id) ?;
41+ let dest_stack = vb_state. get_stack_in_workspace ( target_stack_id) ?;
42+ let pr_number = source_stack
43+ . branches ( )
44+ . into_iter ( )
45+ . find ( |b| b. name == subject_branch_name)
46+ . context ( "Subject branch not found in source stack" ) ?
47+ . pr_number ;
48+ (
49+ source_stack. merge_base ( ctx) ?,
50+ dest_stack. merge_base ( ctx) ?,
51+ pr_number,
52+ )
53+ } ;
54+
55+ // Cross-stack move: compute both rebases (ODB-only writes), check for
56+ // conflicts, then snapshot, then apply state changes.
57+ let ( source_output, dest_output, source_will_be_deleted) = {
58+ let repo = ctx. repo . get ( ) ?;
59+ let vb_state = ctx. virtual_branches ( ) ;
60+ let source_stack = vb_state. get_stack_in_workspace ( source_stack_id) ?;
61+ let dest_stack = vb_state. get_stack_in_workspace ( target_stack_id) ?;
62+
63+ let ( subject_branch_steps, remaining_steps) =
64+ extract_branch_steps ( ctx, & repo, & source_stack, subject_branch_name) ?;
65+
66+ let source_will_be_deleted = remaining_steps. is_empty ( ) ;
67+
68+ // Source: rebase remaining commits without the moved branch (if any remain).
69+ let source_output = if !source_will_be_deleted {
70+ let mut src_rebase = Rebase :: new ( & repo, source_merge_base, None ) ?;
71+ src_rebase. steps ( remaining_steps) ?;
72+ src_rebase. rebase_noops ( false ) ;
73+ Some ( src_rebase. rebase ( & * ctx. cache . get_cache ( ) ?) ?)
74+ } else {
75+ None
76+ } ;
77+
78+ // Dest: rebase dest stack with the moved branch injected.
79+ let new_dest_steps = inject_branch_steps (
80+ ctx,
81+ & repo,
82+ & dest_stack,
83+ target_branch_name,
84+ subject_branch_steps,
85+ ) ?;
86+ let mut dst_rebase = Rebase :: new ( & repo, dest_merge_base, None ) ?;
87+ dst_rebase. steps ( new_dest_steps) ?;
88+ dst_rebase. rebase_noops ( false ) ;
89+ let dest_output = dst_rebase. rebase ( & * ctx. cache . get_cache ( ) ?) ?;
90+
91+ // Conflict check — bail before any state is written.
92+ if let Some ( ref src_out) = source_output {
93+ bail_on_new_conflicts (
94+ & repo,
95+ src_out,
96+ "This move would cause a conflict in the source stack: \
97+ other commits depend on the changes being moved.",
98+ ) ?;
99+ }
100+ bail_on_new_conflicts (
101+ & repo,
102+ & dest_output,
103+ "This move would cause a conflict in the destination stack: \
104+ the branch does not apply cleanly at the target location.",
105+ ) ?;
38106
39- let source_branch = source_stack
40- . branches ( )
41- . into_iter ( )
42- . find ( |b| b. name == subject_branch_name)
43- . context ( "Subject branch not found in source stack" ) ?;
107+ ( source_output, dest_output, source_will_be_deleted)
108+ } ;
44109
45- let destination_stack = vb_state . get_stack_in_workspace ( target_stack_id ) ? ;
46- let destination_merge_base = destination_stack . merge_base ( ctx ) ? ;
110+ // Snapshot after the conflict check, but before any state writes.
111+ let _ = ctx . create_snapshot ( SnapshotDetails :: new ( OperationKind :: MoveBranch ) , perm ) ;
47112
48- let ( subject_branch_steps, deleted_stacks) = extract_and_rebase_source_branch (
49- ctx,
50- source_stack_id,
51- subject_branch_name,
52- & repo,
53- & mut vb_state,
54- source_stack,
55- source_merge_base,
56- ) ?;
113+ // Apply source changes.
114+ let mut deleted_stacks = Vec :: new ( ) ;
115+ {
116+ let repo = ctx. repo . get ( ) ?;
117+ let mut vb_state = ctx. virtual_branches ( ) ;
118+ if source_will_be_deleted {
119+ vb_state. delete_branch_entry ( & source_stack_id) ?;
120+ deleted_stacks. push ( source_stack_id) ;
121+ } else if let Some ( src_out) = source_output {
122+ let mut source_stack = vb_state. get_stack_in_workspace ( source_stack_id) ?;
123+ let new_source_head = repo. find_commit ( src_out. top_commit ) ?;
124+ source_stack. remove_branch ( ctx, subject_branch_name) ?;
125+ source_stack. set_stack_head ( & mut vb_state, & repo, new_source_head. id ( ) . detach ( ) ) ?;
126+ source_stack. set_heads_from_rebase_output ( ctx, src_out. references ) ?;
127+ }
128+ }
57129
58- // Inject the extracted branch steps into the destination stack and rebase the stack
59- inject_branch_steps_into_destination (
60- ctx,
61- target_branch_name,
62- subject_branch_name,
63- & repo,
64- & mut vb_state,
65- destination_stack,
66- destination_merge_base,
67- subject_branch_steps,
68- source_branch. pr_number ,
69- ) ?;
130+ // Apply dest changes.
131+ {
132+ let repo = ctx. repo . get ( ) ?;
133+ let mut vb_state = ctx. virtual_branches ( ) ;
134+ let mut destination_stack = vb_state. get_stack_in_workspace ( target_stack_id) ?;
135+ let new_destination_head = repo. find_commit ( dest_output. top_commit ) ?;
136+
137+ // We initialize the new StackBranch with the target branch's current head so that
138+ // StackBranch::new's range validation passes (the rebased subject head is not yet
139+ // "in range" because the stack head hasn't been updated). The correct commit is
140+ // applied by set_heads_from_rebase_output below.
141+ let target_branch_reference = dest_output
142+ . references
143+ . iter ( )
144+ . find ( |r| r. reference . to_string ( ) == target_branch_name)
145+ . context ( "target branch not found in dest rebase output" ) ?;
146+ let target_branch_head = target_branch_reference. commit_id ;
147+
148+ let mut new_head =
149+ StackBranch :: new ( target_branch_head, subject_branch_name. to_string ( ) , & repo) ?;
150+ new_head. pr_number = source_branch_pr_number;
151+
152+ destination_stack. add_series ( ctx, new_head, Some ( target_branch_name. to_string ( ) ) ) ?;
153+ destination_stack. set_stack_head (
154+ & mut vb_state,
155+ & repo,
156+ new_destination_head. id ( ) . detach ( ) ,
157+ ) ?;
158+ destination_stack. set_heads_from_rebase_output ( ctx, dest_output. references ) ?;
159+ }
70160
71161 let new_workspace = WorkspaceState :: create ( ctx, perm. read_permission ( ) ) ?;
72162 let _ = update_uncommitted_changes ( ctx, old_workspace, new_workspace, perm) ;
73- crate :: integration:: update_workspace_commit_with_vb_state ( & vb_state , ctx, false )
163+ crate :: integration:: update_workspace_commit_with_vb_state ( & ctx . virtual_branches ( ) , ctx, false )
74164 . context ( "failed to update gitbutler workspace" ) ?;
75165
76166 Ok ( MoveBranchResult {
@@ -88,17 +178,17 @@ pub(crate) fn tear_off_branch(
88178) -> Result < MoveBranchResult > {
89179 let old_workspace = WorkspaceState :: create ( ctx, perm. read_permission ( ) ) ?;
90180 let repo = ctx. repo . get ( ) ?;
91- let mut vb_state = VirtualBranchesHandle :: new ( ctx. project_data_dir ( ) ) ;
92181
93- let source_stack = vb_state. get_stack_in_workspace ( source_stack_id) ?;
182+ let source_stack = ctx
183+ . virtual_branches ( )
184+ . get_stack_in_workspace ( source_stack_id) ?;
94185 let source_merge_base = source_stack. merge_base ( ctx) ?;
95186
96187 let ( subject_branch_steps, deleted_stacks) = extract_and_rebase_source_branch (
97188 ctx,
98189 source_stack_id,
99190 subject_branch_name,
100191 & repo,
101- & mut vb_state,
102192 source_stack,
103193 source_merge_base,
104194 ) ?;
@@ -126,7 +216,7 @@ pub(crate) fn tear_off_branch(
126216
127217 let new_workspace = WorkspaceState :: create ( ctx, perm. read_permission ( ) ) ?;
128218 let _ = update_uncommitted_changes ( ctx, old_workspace, new_workspace, perm) ;
129- crate :: integration:: update_workspace_commit_with_vb_state ( & vb_state , ctx, false )
219+ crate :: integration:: update_workspace_commit_with_vb_state ( & ctx . virtual_branches ( ) , ctx, false )
130220 . context ( "failed to update gitbutler workspace" ) ?;
131221
132222 let branch_manager = ctx. branch_manager ( ) ;
@@ -144,63 +234,12 @@ pub(crate) fn tear_off_branch(
144234 } )
145235}
146236
147- #[ expect( clippy:: too_many_arguments) ]
148- /// Injects the extracted branch steps into the destination stack and rebases it.
149- fn inject_branch_steps_into_destination (
150- ctx : & Context ,
151- target_branch_name : & str ,
152- subject_branch_name : & str ,
153- repo : & gix:: Repository ,
154- vb_state : & mut VirtualBranchesHandle ,
155- destination_stack : gitbutler_stack:: Stack ,
156- destination_merge_base : gix:: ObjectId ,
157- subject_branch_steps : Vec < RebaseStep > ,
158- subject_branch_pr_number : Option < usize > ,
159- ) -> Result < ( ) , anyhow:: Error > {
160- let new_destination_steps = inject_branch_steps (
161- ctx,
162- repo,
163- & destination_stack,
164- target_branch_name,
165- subject_branch_steps,
166- ) ?;
167-
168- let mut destination_stack_rebase = Rebase :: new ( repo, destination_merge_base, None ) ?;
169- destination_stack_rebase. steps ( new_destination_steps) ?;
170- destination_stack_rebase. rebase_noops ( false ) ;
171- let destination_rebase_result = destination_stack_rebase. rebase ( & * ctx. cache . get_cache ( ) ?) ?;
172- let new_destination_head = repo. find_commit ( destination_rebase_result. top_commit ) ?;
173- let mut destination_stack = destination_stack;
174-
175- let target_branch_reference = destination_rebase_result
176- . clone ( )
177- . references
178- . into_iter ( )
179- . find ( |r| r. reference . to_string ( ) == target_branch_name)
180- . context ( "subject branch not found in rebase output" ) ?;
181-
182- let target_branch_head = target_branch_reference. commit_id ;
183-
184- let mut new_head = StackBranch :: new ( target_branch_head, subject_branch_name. to_string ( ) , repo) ?;
185-
186- new_head. pr_number = subject_branch_pr_number;
187-
188- destination_stack. add_series ( ctx, new_head, Some ( target_branch_name. to_string ( ) ) ) ?;
189-
190- destination_stack. set_stack_head ( vb_state, repo, new_destination_head. id ( ) . detach ( ) ) ?;
191-
192- destination_stack
193- . set_heads_from_rebase_output ( ctx, destination_rebase_result. clone ( ) . references ) ?;
194- Ok ( ( ) )
195- }
196-
197237/// Extracts the steps corresponding to the branch to move, and rebases the source stack without those steps.
198238fn extract_and_rebase_source_branch (
199239 ctx : & Context ,
200240 source_stack_id : StackId ,
201241 subject_branch_name : & str ,
202242 repository : & gix:: Repository ,
203- vb_state : & mut VirtualBranchesHandle ,
204243 source_stack : gitbutler_stack:: Stack ,
205244 source_merge_base : gix:: ObjectId ,
206245) -> Result < ( Vec < RebaseStep > , Vec < StackId > ) , anyhow:: Error > {
@@ -211,7 +250,8 @@ fn extract_and_rebase_source_branch(
211250
212251 if new_source_steps. is_empty ( ) {
213252 // If there are no other branches left in the source stack, delete the stack.
214- vb_state. delete_branch_entry ( & source_stack_id) ?;
253+ ctx. virtual_branches ( )
254+ . delete_branch_entry ( & source_stack_id) ?;
215255 deleted_stacks. push ( source_stack_id) ;
216256 } else {
217257 // Rebase the source stack without the extracted branch steps
@@ -223,7 +263,11 @@ fn extract_and_rebase_source_branch(
223263
224264 source_stack. remove_branch ( ctx, subject_branch_name) ?;
225265
226- source_stack. set_stack_head ( vb_state, repository, new_source_head. id ( ) . detach ( ) ) ?;
266+ source_stack. set_stack_head (
267+ & mut ctx. virtual_branches ( ) ,
268+ repository,
269+ new_source_head. id ( ) . detach ( ) ,
270+ ) ?;
227271
228272 source_stack. set_heads_from_rebase_output ( ctx, source_rebase_result. clone ( ) . references ) ?;
229273 }
@@ -287,7 +331,7 @@ fn inject_branch_steps(
287331 branch_steps : Vec < RebaseStep > ,
288332) -> Result < Vec < RebaseStep > > {
289333 let destination_steps = destination_stack. as_rebase_steps_rev ( ctx) ?;
290- let mut branch_steps = branch_steps. clone ( ) ;
334+ let mut branch_steps = branch_steps;
291335 branch_steps. reverse ( ) ;
292336
293337 let mut new_destination_steps = Vec :: new ( ) ;
0 commit comments