Skip to content

Commit 4bb2978

Browse files
committed
Add Rust backend for split into dependent branches
- Introduced, documented, and implemented the new split_into_dependent_branch function in split_branch.rs, including dependent branch rebase and update logic. - Exported split_into_dependent_branch API in lib.rs. - Added Tauri command handler and API surface for split_branch_into_dependent_branch in workspace.rs and main.rs. This commit covers the backend/Rust implementation for splitting branches into dependent branches.
1 parent 5eb7b0d commit 4bb2978

File tree

4 files changed

+218
-12
lines changed

4 files changed

+218
-12
lines changed

crates/but-workspace/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub use tree_manipulation::{
4545
discard_worktree_changes::discard_workspace_changes,
4646
move_between_commits::move_changes_between_commits,
4747
remove_changes_from_commit_in_stack::remove_changes_from_commit_in_stack,
48-
split_branch::split_branch,
48+
split_branch::{split_branch, split_into_dependent_branch},
4949
split_commit::{CommitFiles, CommmitSplitOutcome, split_commit},
5050
};
5151
pub mod head;

crates/but-workspace/src/tree_manipulation/split_branch.rs

Lines changed: 180 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ use but_core::Reference;
33
use but_rebase::Rebase;
44
use but_rebase::RebaseStep;
55
use but_rebase::ReferenceSpec;
6+
use gitbutler_cherry_pick::GixRepositoryExt;
67
use gitbutler_command_context::CommandContext;
78
use gitbutler_oxidize::ObjectIdExt;
89
use gitbutler_oxidize::OidExt;
910
use gitbutler_repo::logging::{LogUntil, RepositoryExt as _};
11+
use gitbutler_stack::CommitOrChangeId;
12+
use gitbutler_stack::StackBranch;
1013
use gitbutler_stack::{StackId, VirtualBranchesHandle};
1114

1215
use crate::MoveChangesResult;
@@ -128,6 +131,142 @@ pub fn split_branch(
128131
Ok((new_branch_ref, move_changes_result))
129132
}
130133

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+
131270
/// Filters out the specified file changes from the branch.
132271
///
133272
/// 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(
140279
merge_base: gix::ObjectId,
141280
context_lines: u32,
142281
) -> 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> {
143313
let source_steps = source_stack.as_rebase_steps_rev(ctx, repository)?;
144314
let mut new_source_steps = Vec::new();
145315
let mut inside_branch = false;
@@ -153,6 +323,7 @@ fn filter_file_changes_in_branch(
153323
})?;
154324
let branch_ref_name = branch_ref.name().to_owned();
155325

326+
let mut inserted_steps = false;
156327
for step in source_steps {
157328
if let RebaseStep::Reference(but_core::Reference::Git(name)) = &step {
158329
if *name == branch_ref_name {
@@ -176,6 +347,14 @@ fn filter_file_changes_in_branch(
176347
continue;
177348
}
178349

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+
179358
if let RebaseStep::Pick { commit_id, .. } = &step {
180359
match remove_file_changes_from_commit(
181360
ctx,
@@ -206,16 +385,6 @@ fn filter_file_changes_in_branch(
206385
new_source_steps.push(step);
207386
}
208387
}
209-
210388
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)
221390
}

crates/gitbutler-tauri/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,7 @@ fn main() {
299299
workspace::move_changes_between_commits,
300300
workspace::uncommit_changes,
301301
workspace::split_branch,
302+
workspace::split_branch_into_dependent_branch,
302303
diff::changes_in_worktree,
303304
diff::commit_details,
304305
diff::changes_in_branch,

crates/gitbutler-tauri/src/workspace.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,42 @@ pub fn split_branch(
366366
Ok(move_changes_result.into())
367367
}
368368

369+
#[allow(clippy::too_many_arguments)]
370+
#[tauri::command(async)]
371+
#[instrument(skip(projects, settings), err(Debug))]
372+
pub fn split_branch_into_dependent_branch(
373+
projects: State<'_, projects::Controller>,
374+
settings: State<'_, AppSettingsWithDiskSync>,
375+
project_id: ProjectId,
376+
source_stack_id: StackId,
377+
source_branch_name: String,
378+
new_branch_name: String,
379+
file_changes_to_split_off: Vec<String>,
380+
) -> Result<UIMoveChangesResult, Error> {
381+
let project = projects.get(project_id)?;
382+
let ctx = CommandContext::open(&project, settings.get()?.clone())?;
383+
let mut guard = project.exclusive_worktree_access();
384+
385+
let _ = ctx.create_snapshot(
386+
SnapshotDetails::new(OperationKind::SplitBranch),
387+
guard.write_permission(),
388+
);
389+
390+
let move_changes_result = but_workspace::split_into_dependent_branch(
391+
&ctx,
392+
source_stack_id,
393+
source_branch_name,
394+
new_branch_name.clone(),
395+
&file_changes_to_split_off,
396+
settings.get()?.context_lines,
397+
)?;
398+
399+
let vb_state = VirtualBranchesHandle::new(ctx.project().gb_dir());
400+
update_workspace_commit(&vb_state, &ctx)?;
401+
402+
Ok(move_changes_result.into())
403+
}
404+
369405
/// Uncommits the changes specified in the `diffspec`.
370406
///
371407
/// If `assign_to` is provided, the changes will be assigned to the stack

0 commit comments

Comments
 (0)