@@ -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 } ;
711use gitbutler_stack:: { StackBranch , VirtualBranchesHandle } ;
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 , move_commits :: bail_on_new_conflicts } ;
1317
1418#[ derive( Debug , Clone , Serialize ) ]
1519#[ serde( rename_all = "camelCase" ) ]
@@ -22,54 +26,137 @@ 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 = VirtualBranchesHandle :: new ( ctx. project_data_dir ( ) ) ;
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 = VirtualBranchesHandle :: new ( ctx. project_data_dir ( ) ) ;
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 = VirtualBranchesHandle :: new ( ctx. project_data_dir ( ) ) ;
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 = VirtualBranchesHandle :: new ( ctx. project_data_dir ( ) ) ;
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+ let target_branch_reference = dest_output
138+ . references
139+ . iter ( )
140+ . find ( |r| r. reference . to_string ( ) == target_branch_name)
141+ . context ( "target branch not found in dest rebase output" ) ?;
142+ let target_branch_head = target_branch_reference. commit_id ;
143+
144+ let mut new_head =
145+ StackBranch :: new ( target_branch_head, subject_branch_name. to_string ( ) , & repo) ?;
146+ new_head. pr_number = source_branch_pr_number;
147+
148+ destination_stack. add_series ( ctx, new_head, Some ( target_branch_name. to_string ( ) ) ) ?;
149+ destination_stack. set_stack_head (
150+ & mut vb_state,
151+ & repo,
152+ new_destination_head. id ( ) . detach ( ) ,
153+ ) ?;
154+ destination_stack. set_heads_from_rebase_output ( ctx, dest_output. references ) ?;
155+ }
70156
71157 let new_workspace = WorkspaceState :: create ( ctx, perm. read_permission ( ) ) ?;
72158 let _ = update_uncommitted_changes ( ctx, old_workspace, new_workspace, perm) ;
159+ let vb_state = VirtualBranchesHandle :: new ( ctx. project_data_dir ( ) ) ;
73160 crate :: integration:: update_workspace_commit_with_vb_state ( & vb_state, ctx, false )
74161 . context ( "failed to update gitbutler workspace" ) ?;
75162
@@ -144,56 +231,6 @@ pub(crate) fn tear_off_branch(
144231 } )
145232}
146233
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-
197234/// Extracts the steps corresponding to the branch to move, and rebases the source stack without those steps.
198235fn extract_and_rebase_source_branch (
199236 ctx : & Context ,
@@ -287,7 +324,7 @@ fn inject_branch_steps(
287324 branch_steps : Vec < RebaseStep > ,
288325) -> Result < Vec < RebaseStep > > {
289326 let destination_steps = destination_stack. as_rebase_steps_rev ( ctx) ?;
290- let mut branch_steps = branch_steps. clone ( ) ;
327+ let mut branch_steps = branch_steps;
291328 branch_steps. reverse ( ) ;
292329
293330 let mut new_destination_steps = Vec :: new ( ) ;
0 commit comments