@@ -3,10 +3,13 @@ use but_core::Reference;
3
3
use but_rebase:: Rebase ;
4
4
use but_rebase:: RebaseStep ;
5
5
use but_rebase:: ReferenceSpec ;
6
+ use gitbutler_cherry_pick:: GixRepositoryExt ;
6
7
use gitbutler_command_context:: CommandContext ;
7
8
use gitbutler_oxidize:: ObjectIdExt ;
8
9
use gitbutler_oxidize:: OidExt ;
9
10
use gitbutler_repo:: logging:: { LogUntil , RepositoryExt as _} ;
11
+ use gitbutler_stack:: CommitOrChangeId ;
12
+ use gitbutler_stack:: StackBranch ;
10
13
use gitbutler_stack:: { StackId , VirtualBranchesHandle } ;
11
14
12
15
use crate :: MoveChangesResult ;
@@ -128,6 +131,142 @@ pub fn split_branch(
128
131
Ok ( ( new_branch_ref, move_changes_result) )
129
132
}
130
133
134
+ /// Splits a branch into a dependent branch.
135
+ ///
136
+ /// This function splits a branch into two dependent branches,
137
+ /// moving the specified changes to the new branch (dependent branch).
138
+ ///
139
+ /// In steps:
140
+ ///
141
+ /// 1. Create a new branch from the source branch's head.
142
+ /// 2. Remove all the specified changes from the source branch.
143
+ /// 3. Remove all but the specified changes from the new branch.
144
+ /// 4. Insert the new branch as a dependent branch in the stack.
145
+ /// 5. Update the stack
146
+ pub fn split_into_dependent_branch (
147
+ ctx : & CommandContext ,
148
+ stack_id : StackId ,
149
+ source_branch_name : String ,
150
+ new_branch_name : String ,
151
+ file_changes_to_split_off : & [ String ] ,
152
+ context_lines : u32 ,
153
+ ) -> Result < MoveChangesResult > {
154
+ let repository = ctx. gix_repo ( ) ?;
155
+ let vb_state = VirtualBranchesHandle :: new ( ctx. project ( ) . gb_dir ( ) ) ;
156
+
157
+ let source_stack = vb_state. get_stack_in_workspace ( stack_id) ?;
158
+ let merge_base = source_stack. merge_base ( ctx) ?;
159
+
160
+ // Create a new branch from the source branch's head
161
+ let push_details = source_stack. push_details ( ctx, source_branch_name. clone ( ) ) ?;
162
+ let branch_head = push_details. head ;
163
+
164
+ // Create a new branch reference
165
+ let new_branch_ref_name = format ! ( "refs/heads/{}" , new_branch_name) ;
166
+ let new_branch_log_message = format ! (
167
+ "Split off changes from branch '{}' into new dependent branch '{}'" ,
168
+ source_branch_name, new_branch_name
169
+ ) ;
170
+
171
+ let new_ref = repository. reference (
172
+ new_branch_ref_name. clone ( ) ,
173
+ branch_head. to_gix ( ) ,
174
+ gix:: refs:: transaction:: PreviousValue :: Any ,
175
+ new_branch_log_message. clone ( ) ,
176
+ ) ?;
177
+
178
+ // Remove all but the specified changes from the new branch
179
+ let new_branch_commits =
180
+ ctx. repo ( )
181
+ . l ( branch_head, LogUntil :: Commit ( merge_base. to_git2 ( ) ) , false ) ?;
182
+
183
+ // Branch as rebase steps
184
+ let mut dependent_branch_steps: Vec < RebaseStep > = Vec :: new ( ) ;
185
+
186
+ let reference_step = RebaseStep :: Reference ( but_core:: Reference :: Git ( new_ref. name ( ) . to_owned ( ) ) ) ;
187
+ dependent_branch_steps. push ( reference_step) ;
188
+
189
+ for commit in new_branch_commits {
190
+ let commit_id = commit. to_gix ( ) ;
191
+ if let Some ( new_commit_id) = keep_only_file_changes_in_commit (
192
+ ctx,
193
+ commit_id,
194
+ file_changes_to_split_off,
195
+ context_lines,
196
+ true ,
197
+ ) ? {
198
+ let pick_step = RebaseStep :: Pick {
199
+ commit_id : new_commit_id,
200
+ new_message : None ,
201
+ } ;
202
+ dependent_branch_steps. push ( pick_step) ;
203
+ }
204
+ }
205
+
206
+ println ! (
207
+ "Dependent branch steps before filtering: {:?}" ,
208
+ dependent_branch_steps
209
+ ) ;
210
+
211
+ let steps = construct_source_steps (
212
+ ctx,
213
+ & repository,
214
+ file_changes_to_split_off,
215
+ & source_stack,
216
+ source_branch_name. clone ( ) ,
217
+ context_lines,
218
+ Some ( & dependent_branch_steps) ,
219
+ ) ?;
220
+
221
+ println ! (
222
+ "Rebasing source branch '{}' with steps: {:?}" ,
223
+ source_branch_name, steps
224
+ ) ;
225
+
226
+ let mut source_rebase = Rebase :: new ( & repository, merge_base, None ) ?;
227
+ source_rebase. steps ( steps) ?;
228
+ source_rebase. rebase_noops ( false ) ;
229
+ let source_result = source_rebase. rebase ( ) ?;
230
+ // let new_head = repo_git2.find_commit(source_result.top_commit.to_git2())?;
231
+ let new_head = repository. find_commit ( source_result. top_commit ) ?;
232
+
233
+ let mut source_stack = source_stack;
234
+
235
+ source_stack. add_series (
236
+ ctx,
237
+ StackBranch :: new (
238
+ CommitOrChangeId :: CommitId ( branch_head. to_string ( ) ) ,
239
+ new_branch_name,
240
+ None ,
241
+ & repository,
242
+ ) ?,
243
+ Some ( source_branch_name) ,
244
+ ) ?;
245
+
246
+ source_stack. set_stack_head (
247
+ & vb_state,
248
+ & repository,
249
+ new_head. id ( ) . to_git2 ( ) ,
250
+ Some (
251
+ repository
252
+ . find_real_tree ( & new_head. id ( ) , Default :: default ( ) ) ?
253
+ . to_git2 ( ) ,
254
+ ) ,
255
+ ) ?;
256
+ source_stack. set_heads_from_rebase_output ( ctx, source_result. clone ( ) . references ) ?;
257
+
258
+ let move_changes_result = MoveChangesResult {
259
+ replaced_commits : source_result
260
+ . commit_mapping
261
+ . iter ( )
262
+ . filter ( |( _, old, new) | old != new)
263
+ . map ( |( _, old, new) | ( * old, * new) )
264
+ . collect ( ) ,
265
+ } ;
266
+
267
+ Ok ( move_changes_result)
268
+ }
269
+
131
270
/// Filters out the specified file changes from the branch.
132
271
///
133
272
/// All commits that end up empty after removing the specified file changes will be dropped.
@@ -140,6 +279,37 @@ fn filter_file_changes_in_branch(
140
279
merge_base : gix:: ObjectId ,
141
280
context_lines : u32 ,
142
281
) -> Result < but_rebase:: RebaseOutput , anyhow:: Error > {
282
+ let source_steps = construct_source_steps (
283
+ ctx,
284
+ repository,
285
+ file_changes_to_split_off,
286
+ & source_stack,
287
+ source_branch_name,
288
+ context_lines,
289
+ None ,
290
+ ) ?;
291
+
292
+ let mut source_rebase = Rebase :: new ( repository, merge_base, None ) ?;
293
+ source_rebase. steps ( source_steps) ?;
294
+ source_rebase. rebase_noops ( false ) ;
295
+ let source_result = source_rebase. rebase ( ) ?;
296
+
297
+ let mut source_stack = source_stack;
298
+
299
+ source_stack. set_heads_from_rebase_output ( ctx, source_result. clone ( ) . references ) ?;
300
+
301
+ Ok ( source_result)
302
+ }
303
+
304
+ fn construct_source_steps (
305
+ ctx : & CommandContext ,
306
+ repository : & gix:: Repository ,
307
+ file_changes_to_split_off : & [ String ] ,
308
+ source_stack : & gitbutler_stack:: Stack ,
309
+ source_branch_name : String ,
310
+ context_lines : u32 ,
311
+ steps_to_insert : Option < & [ RebaseStep ] > ,
312
+ ) -> Result < Vec < RebaseStep > , anyhow:: Error > {
143
313
let source_steps = source_stack. as_rebase_steps_rev ( ctx, repository) ?;
144
314
let mut new_source_steps = Vec :: new ( ) ;
145
315
let mut inside_branch = false ;
@@ -153,6 +323,7 @@ fn filter_file_changes_in_branch(
153
323
} ) ?;
154
324
let branch_ref_name = branch_ref. name ( ) . to_owned ( ) ;
155
325
326
+ let mut inserted_steps = false ;
156
327
for step in source_steps {
157
328
if let RebaseStep :: Reference ( but_core:: Reference :: Git ( name) ) = & step {
158
329
if * name == branch_ref_name {
@@ -176,6 +347,14 @@ fn filter_file_changes_in_branch(
176
347
continue ;
177
348
}
178
349
350
+ // Insert steps just above the branch reference step
351
+ if !inserted_steps {
352
+ if let Some ( steps_to_insert) = steps_to_insert {
353
+ inserted_steps = true ;
354
+ new_source_steps. extend ( steps_to_insert. iter ( ) . cloned ( ) ) ;
355
+ }
356
+ }
357
+
179
358
if let RebaseStep :: Pick { commit_id, .. } = & step {
180
359
match remove_file_changes_from_commit (
181
360
ctx,
@@ -206,16 +385,6 @@ fn filter_file_changes_in_branch(
206
385
new_source_steps. push ( step) ;
207
386
}
208
387
}
209
-
210
388
new_source_steps. reverse ( ) ;
211
-
212
- let mut source_rebase = Rebase :: new ( repository, merge_base, None ) ?;
213
- source_rebase. steps ( new_source_steps) ?;
214
- source_rebase. rebase_noops ( false ) ;
215
- let source_result = source_rebase. rebase ( ) ?;
216
-
217
- let mut source_stack = source_stack;
218
- source_stack. set_heads_from_rebase_output ( ctx, source_result. clone ( ) . references ) ?;
219
-
220
- Ok ( source_result)
389
+ Ok ( new_source_steps)
221
390
}
0 commit comments