Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 47 additions & 13 deletions crates/but-workspace/src/branch/move_branch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,20 +375,10 @@ fn get_disconnect_parameters<'ws, 'meta, M: RefMetadata>(

let parents_to_disconnect = if let Some(stack_base_segment) = stack_base_segment {
// Base segment is part of the source stack.
let base_segment_ref_name = stack_base_segment
.ref_name()
.context("Base segment doesn't have a ref name.")?;
let reference_selector = editor.select_reference(base_segment_ref_name)?;
let selectors = SomeSelectors::new(vec![reference_selector])?;
SelectorSet::Some(selectors)
select_segment(editor, stack_base_segment)?
} else if let Some(graph_base_segment) = graph_base_segment {
// Base segment is outside of workspace (probably target branch).
let ref_name = graph_base_segment
.ref_name()
.context("Graph base segment doesn't have a ref name.")?;
let reference_selector = editor.select_reference(ref_name)?;
let selectors = SomeSelectors::new(vec![reference_selector])?;
SelectorSet::Some(selectors)
// Base segment is outside the stack (e.g. the target branch, or an unnamed fork-point segment).
select_segment(editor, graph_base_segment)?
} else if subject_segment.base_segment_id.is_some() {
// Base segment could not be found, but there is an ID defined. Error out.
bail!(
Expand Down Expand Up @@ -437,3 +427,47 @@ fn get_disconnect_parameters<'ws, 'meta, M: RefMetadata>(

Ok((delimiter, children_to_disconnect, parents_to_disconnect))
}

/// Select a segment for use as a disconnect point.
///
/// Prefers the ref name when available, otherwise falls back to the tip commit.
/// Fails if the segment has neither (empty and unnamed).
fn select_segment<M: RefMetadata>(
editor: &Editor<'_, '_, M>,
segment: &impl SegmentLike,
) -> anyhow::Result<SelectorSet> {
let selector = if let Some(ref_name) = segment.ref_name() {
editor.select_reference(ref_name)?
} else if let Some(tip) = segment.tip() {
editor.select_commit(tip)?
} else {
bail!("Base segment has neither a ref name nor any commits.");
};
let selectors = SomeSelectors::new(vec![selector])?;
Ok(SelectorSet::Some(selectors))
}

/// Common interface for selecting a graph segment, abstracting over
/// [`StackSegment`] (workspace projection) and [`but_graph::Segment`] (raw graph).
trait SegmentLike {
fn ref_name(&self) -> Option<&gix::refs::FullNameRef>;
fn tip(&self) -> Option<gix::ObjectId>;
}

impl SegmentLike for StackSegment {
fn ref_name(&self) -> Option<&gix::refs::FullNameRef> {
self.ref_name()
}
fn tip(&self) -> Option<gix::ObjectId> {
self.tip()
}
}

impl SegmentLike for but_graph::Segment {
fn ref_name(&self) -> Option<&gix::refs::FullNameRef> {
self.ref_name()
}
fn tip(&self) -> Option<gix::ObjectId> {
self.tip()
}
}
10 changes: 10 additions & 0 deletions e2e/playwright/scripts/fetch-in-clone.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash

echo "GIT CONFIG $GIT_CONFIG_GLOBAL"
echo "DATA DIR $E2E_TEST_APP_DATA_DIR"
echo "DIRECTORY: $1"

# Fetch updates from the remote.
pushd "$1"
git fetch origin
popd
38 changes: 38 additions & 0 deletions e2e/playwright/tests/branches.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,3 +643,41 @@ test("should be able to unapply a stack", async ({ page, context }, testInfo) =>
branchHeaders = getByTestId(page, "branch-header").filter({ hasText: "branch1" });
await expect(branchHeaders).toHaveCount(0);
});

test("should be able to move a branch when origin/master has advanced past the fork point", async ({
page,
context,
}, testInfo) => {
const workdir = testInfo.outputPath("workdir");
const configdir = testInfo.outputPath("config");
gitbutler = await startGitButler(workdir, configdir, context);

// Set up project with branches (branch1 and branch3 fork from initial master).
await gitbutler.runScript("project-with-remote-branches.sh");
// Apply branch1 and branch3 as two separate stacks.
await gitbutler.runScript("apply-upstream-branch.sh", ["branch1", "local-clone"]);
await gitbutler.runScript("apply-upstream-branch.sh", ["branch3", "local-clone"]);

// Advance remote master past the fork point. This creates a commit on
// origin/master that is ahead of where branch1 and branch3 diverge.
await gitbutler.runScript("project-with-remote-branches__add-commit-to-base.sh");
// Fetch so that origin/master advances in the local clone, making the
// old fork point an unnamed segment in the graph.
await gitbutler.runScript("fetch-in-clone.sh", ["local-clone"]);

// Move branch3 on top of branch1. This should succeed even though the
// base segment at the old fork point has no ref name.
await gitbutler.runScript("move-branch.sh", ["branch3", "branch1", "local-clone"]);

await page.goto("/");

// Should load the workspace
await waitForTestId(page, "workspace-view");

// There should be one stack with both branches
const stacks = getByTestId(page, "stack");
await expect(stacks).toHaveCount(1);

const branchHeaders = getByTestId(page, "branch-header");
await expect(branchHeaders).toHaveCount(2);
});
Loading