Skip to content

Commit 8f33869

Browse files
committed
branch upstream integration: merge commit
Add the ability to integrate changes by merge commit WIP wip: Merge by creating a full new commit merge correctly
1 parent ca100b8 commit 8f33869

3 files changed

Lines changed: 480 additions & 56 deletions

File tree

crates/but-workspace/src/branch/integrate_branch_upstream.rs

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ use but_core::{
1111
commit::{add_conflict_markers, write_conflicted_tree},
1212
};
1313
use but_rebase::graph_rebase::{
14-
Editor, LookupStep, Selector, Step, SuccessfulRebase, ToSelector,
15-
mutate::{InsertSide, SegmentDelimiter, SelectorSet},
14+
Editor, ExtraRef, GraphEditorOptions, LookupStep, Selector, Step, SuccessfulRebase, ToSelector,
15+
mutate::{SegmentDelimiter, SelectorSet},
1616
};
1717
use but_rebase::{commit::DateMode, graph_rebase::commit::MergeCommitChangesOutcome};
1818
use gix::{prelude::ObjectIdExt as _, remote::Direction};
@@ -44,6 +44,11 @@ pub enum InteractiveIntegrationStep {
4444
/// Optionally, the message to use for the squash commit.
4545
message: Option<String>,
4646
},
47+
/// Merge a commit into the previous one.
48+
Merge {
49+
/// The SHA of the commit to squash.
50+
commit_id: gix::ObjectId,
51+
},
4752
}
4853

4954
impl fmt::Display for InteractiveIntegrationStep {
@@ -52,6 +57,7 @@ impl fmt::Display for InteractiveIntegrationStep {
5257
Self::Skip { commit_id } => write!(f, "skip {commit_id}"),
5358
Self::Pick { commit_id } => write!(f, "pick {commit_id}"),
5459
Self::PickUpstream { commit_id } => write!(f, "pick-upstream {commit_id}"),
60+
Self::Merge { commit_id } => write!(f, "merge {commit_id}"),
5561
Self::Squash { commits, message } => {
5662
write!(f, "squash")?;
5763
for commit_id in commits {
@@ -294,21 +300,28 @@ fn parse_quoted_string(input: &str, line_no: usize) -> Result<String> {
294300

295301
/// Integrate the upstream changes in the order of the provided steps.
296302
///
297-
/// `editor` - The graph editor handle.
298-
///
299303
/// `ref_name` - The full reference name of the local branch we're integrating the upstream changes into.
300304
///
301305
/// `steps` - The vector of steps in the application order (parent to child) that describe the actions to perform
302306
/// for the integration of the changes.
303307
pub fn integrate_branch_with_steps<'ws, 'meta, M: RefMetadata>(
304-
mut editor: Editor<'ws, 'meta, M>,
305308
ref_name: &gix::refs::FullNameRef,
306309
integration: InteractiveIntegration,
310+
workspace: &'ws mut but_graph::Workspace,
311+
meta: &'meta mut M,
312+
repo: &gix::Repository,
307313
) -> Result<SuccessfulRebase<'ws, 'meta, M>> {
308314
if integration.steps.is_empty() {
309315
bail!("Integration steps cannot be empty")
310316
}
317+
let (_, upstream_ref_name, _) = get_branch_tips_and_upstream(ref_name, repo)?;
311318

319+
let upstream_ref_name = upstream_ref_name.as_ref();
320+
let editor_options = GraphEditorOptions {
321+
extra_refs: vec![ExtraRef::immutable(upstream_ref_name)],
322+
..GraphEditorOptions::default()
323+
};
324+
let mut editor = Editor::create_with_opts(workspace, meta, repo, &editor_options)?;
312325
let delimiter_child = editor.select_reference(ref_name)?;
313326
let delimiter_parent = editor.select_commit(integration.merge_base)?;
314327
let segment_delimiter = SegmentDelimiter {
@@ -383,7 +396,7 @@ fn integration_steps_into_segment_nodes<M: RefMetadata>(
383396
continue;
384397
}
385398

386-
parent_most = editor.insert(parent_most, step, InsertSide::Below)?;
399+
parent_most = connect_parent_step(editor, parent_most, step)?;
387400
}
388401

389402
// Step 3: Append the merge base at the bottom
@@ -394,7 +407,7 @@ fn integration_steps_into_segment_nodes<M: RefMetadata>(
394407
{
395408
existing_parent
396409
} else {
397-
editor.insert(parent_most, merge_base_step, InsertSide::Below)?
410+
connect_parent_step(editor, parent_most, merge_base_step)?
398411
};
399412

400413
Ok(SegmentDelimiter {
@@ -424,6 +437,40 @@ fn already_connected_parent_for_step<M: RefMetadata>(
424437
.find_map(|(parent, _)| (parent == existing_pick).then_some(parent)))
425438
}
426439

440+
/// Connects `child` to `parent_step`, choosing the smallest available edge order.
441+
///
442+
/// Prefers order `0` when free; otherwise picks the next smallest unused order.
443+
fn connect_parent_step<M: RefMetadata>(
444+
editor: &mut Editor<'_, '_, M>,
445+
child: Selector,
446+
parent_step: Step,
447+
) -> Result<Selector> {
448+
let parent = match parent_step {
449+
Step::Pick(pick) => {
450+
if let Some(existing_pick) = editor.try_select_commit(pick.id) {
451+
existing_pick
452+
} else {
453+
editor.add_step(Step::Pick(pick))?
454+
}
455+
}
456+
Step::Reference { refname } => editor.select_reference(refname.as_ref())?,
457+
Step::None => bail!("BUG: trying to connect to none"),
458+
};
459+
460+
let used_orders = editor
461+
.direct_parents(child)?
462+
.into_iter()
463+
.map(|(_, order)| order)
464+
.collect::<HashSet<_>>();
465+
let mut order = 0;
466+
while used_orders.contains(&order) {
467+
order += 1;
468+
}
469+
470+
editor.add_edge(child, parent, order)?;
471+
Ok(parent)
472+
}
473+
427474
/// Converts user-provided integration steps into graph `Step`s in insertion order.
428475
///
429476
/// While translating, it applies graph detachments for skip instructions and prepares
@@ -451,6 +498,28 @@ fn integration_steps_to_segment_steps_for_editor<M: RefMetadata>(
451498
InteractiveIntegrationStep::Squash { commits, message } => {
452499
out.push(squash_step_for_editor(editor, commits, message.as_deref())?);
453500
}
501+
InteractiveIntegrationStep::Merge { commit_id } => {
502+
let mut merge_commit = editor.empty_commit()?;
503+
merge_commit.message = format!("Merge {commit_id} into previous commit").into();
504+
let merge_commit =
505+
editor.new_commit_untracked(merge_commit, DateMode::CommitterKeepAuthorKeep)?;
506+
let preserved_parents = editor
507+
.find_commit(*commit_id)?
508+
.inner
509+
.parents
510+
.iter()
511+
.copied()
512+
.collect::<Vec<_>>();
513+
let mut commit_to_merge = Step::new_untracked_pick(*commit_id);
514+
let Step::Pick(pick) = &mut commit_to_merge else {
515+
bail!("BUG: expected merge side parent to be a pick step");
516+
};
517+
pick.preserved_parents = Some(preserved_parents);
518+
let commit_to_merge = editor.add_step(commit_to_merge)?;
519+
let merge_commit = editor.add_step(Step::new_untracked_pick(merge_commit))?;
520+
editor.add_edge(merge_commit, commit_to_merge, 1)?;
521+
out.push(editor.lookup_step(merge_commit)?);
522+
}
454523
}
455524
}
456525

@@ -564,7 +633,7 @@ fn existing_or_new_pick_step<M: RefMetadata>(
564633
child: existing,
565634
parent: existing,
566635
},
567-
SelectorSet::All,
636+
SelectorSet::None,
568637
parents_to_disconnect,
569638
true,
570639
)?;

crates/but-workspace/src/branch/segment_disconnect.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use anyhow::{Context, bail};
22
use but_core::RefMetadata;
3-
use but_graph::projection::{Stack, StackSegment};
3+
use but_graph::workspace::{Stack, StackSegment};
44
use but_rebase::graph_rebase::{
55
Editor, LookupStep, Selector, Step,
66
mutate::{SegmentDelimiter, SelectorSet, SomeSelectors},
@@ -18,7 +18,7 @@ pub(crate) struct DisconnectParameters {
1818
/// as well as the right segment delimiter to move.
1919
pub(crate) fn get_disconnect_parameters<'ws, 'meta, M: RefMetadata>(
2020
editor: &Editor<'ws, 'meta, M>,
21-
workspace: &but_graph::projection::Workspace,
21+
workspace: &but_graph::Workspace,
2222
source_stack: &Stack,
2323
subject_segment: &StackSegment,
2424
workspace_head: gix::ObjectId,

0 commit comments

Comments
 (0)