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
41 changes: 41 additions & 0 deletions crates/but-core/src/ref_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,27 @@ pub struct Workspace {
pub push_remote: Option<String>,
}

impl Workspace {}

/// Mutations
impl Workspace {
/// Remove the named segment `branch`, which removes the whole stack if it's empty after removing a segment
/// of that name.
/// Returns `true` if it was removed or `false` if it wasn't found.
pub fn remove_segment(&mut self, branch: &FullNameRef) -> bool {
let Some((stack_idx, segment_idx)) = self.find_owner_indexes_by_name(branch) else {
return false;
};

let stack = &mut self.stacks[stack_idx];
stack.branches.remove(segment_idx);

if stack.branches.is_empty() {
self.stacks.remove(stack_idx);
}
true
}

/// Insert `branch` as new stack if it's not yet contained in the workspace and if `order` is not `None` or push
/// it to the end of the stack list.
/// Note that `order` is only relevant at insertion time.
Expand Down Expand Up @@ -68,6 +87,28 @@ impl Workspace {
}
true
}

/// Insert `branch` as new segment if it's not yet contained in the workspace,
/// and insert it above the given `anchor` segment name, which maybe the tip of a stack or any segment within one
/// Returns `true` if the ref was newly added, or `false` if it already existed, or `None` if `anchor` didn't exist.
pub fn insert_new_segment_above_anchor_if_not_present(
&mut self,
branch: &FullNameRef,
anchor: &FullNameRef,
) -> Option<bool> {
if self.contains_ref(branch) {
return Some(false);
};
let (stack_idx, segment_idx) = self.find_owner_indexes_by_name(anchor)?;
self.stacks[stack_idx].branches.insert(
segment_idx,
WorkspaceStackBranch {
ref_name: branch.to_owned(),
archived: false,
},
);
Some(true)
}
}

/// Access
Expand Down
96 changes: 95 additions & 1 deletion crates/but-core/tests/core/ref_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ mod workspace {
use but_core::ref_metadata::Workspace;

#[test]
fn add_new_stack_if_not_present() {
fn add_new_stack_if_not_present_journey() {
let mut ws = Workspace::default();
assert_eq!(ws.stacks.len(), 0);

Expand All @@ -18,6 +18,100 @@ mod workspace {
let c_ref = r("refs/heads/C");
assert!(ws.add_or_insert_new_stack_if_not_present(c_ref, None));
assert_eq!(ws.stack_names().collect::<Vec<_>>(), [b_ref, a_ref, c_ref]);

assert!(ws.remove_segment(a_ref));
assert!(ws.remove_segment(b_ref));
assert!(!ws.remove_segment(b_ref));
assert!(ws.remove_segment(c_ref));
assert!(!ws.remove_segment(c_ref));

// Everything should be removed.
insta::assert_debug_snapshot!(ws, @r"
Workspace {
ref_info: RefInfo { created_at: None, updated_at: None },
stacks: [],
target_ref: None,
push_remote: None,
}
");
}

#[test]
fn insert_new_segment_above_anchor_if_not_present_journey() {
let mut ws = Workspace::default();
assert_eq!(ws.stacks.len(), 0);

let a_ref = r("refs/heads/A");
let b_ref = r("refs/heads/B");
assert_eq!(
ws.insert_new_segment_above_anchor_if_not_present(b_ref, a_ref),
None,
"anchor doesn't exist"
);
assert!(ws.add_or_insert_new_stack_if_not_present(a_ref, None));
assert_eq!(
ws.insert_new_segment_above_anchor_if_not_present(b_ref, a_ref),
Some(true),
"anchor existed and it was added"
);
assert_eq!(
ws.insert_new_segment_above_anchor_if_not_present(b_ref, a_ref),
Some(false),
"anchor existed and it was NOT added as it already existed"
);

let c_ref = r("refs/heads/C");
assert_eq!(
ws.insert_new_segment_above_anchor_if_not_present(c_ref, a_ref),
Some(true)
);

insta::assert_snapshot!(but_testsupport::sanitize_uuids_and_timestamps(format!("{ws:#?}")), @r#"
Workspace {
ref_info: RefInfo { created_at: None, updated_at: None },
stacks: [
WorkspaceStack {
id: 1,
branches: [
WorkspaceStackBranch {
ref_name: FullName(
"refs/heads/B",
),
archived: false,
},
WorkspaceStackBranch {
ref_name: FullName(
"refs/heads/C",
),
archived: false,
},
WorkspaceStackBranch {
ref_name: FullName(
"refs/heads/A",
),
archived: false,
},
],
},
],
target_ref: None,
push_remote: None,
}
"#);

assert!(ws.remove_segment(b_ref));
assert!(ws.remove_segment(a_ref));
assert!(ws.remove_segment(c_ref));

// Everything should be removed.
insta::assert_debug_snapshot!(ws, @r"
Workspace {
ref_info: RefInfo { created_at: None, updated_at: None },
stacks: [],
target_ref: None,
push_remote: None,
}
");
}

fn r(name: &str) -> &gix::refs::FullNameRef {
Expand Down
8 changes: 8 additions & 0 deletions crates/but-testing/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ pub struct Args {

#[derive(Debug, clap::Subcommand)]
pub enum Subcommands {
/// Make an existing branch appear in the workspace.
Apply {
/// The 0-based place in the worktree into which the branch should be inserted.
#[clap(long, short = 'o')]
order: Option<usize>,
/// The name of the branch to apply to the workspace, like `feature` or `origin/other`.
branch_name: String,
},
/// Add the given Git repository as project for use with GitButler.
AddProject {
/// The long name of the remote reference to track, like `refs/remotes/origin/main`,
Expand Down
29 changes: 29 additions & 0 deletions crates/but-testing/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ use but_core::UnifiedDiff;
use but_db::poll::ItemKind;
use but_graph::VirtualBranchesTomlMetadata;
use but_settings::AppSettings;
use but_workspace::branch::OnWorkspaceMergeConflict;
use but_workspace::branch::apply::{IntegrationMode, WorkspaceReferenceNaming};
use but_workspace::branch::checkout::UncommitedWorktreeChanges;
use but_workspace::branch::create_reference::{Anchor, Position};
use but_workspace::{DiffSpec, HunkHeader};
use gitbutler_project::{Project, ProjectId};
Expand Down Expand Up @@ -634,6 +637,32 @@ pub fn remove_reference(
Ok(())
}

pub fn apply(args: &super::Args, short_name: &str, order: Option<usize>) -> anyhow::Result<()> {
let (repo, project, graph, mut meta) =
repo_and_maybe_project_and_graph(args, RepositoryOpenMode::Merge)?;
let branch = repo.find_reference(short_name)?;
let ws = graph.to_workspace()?;
_ = but_workspace::branch::apply(
branch.name(),
&ws,
&repo,
&mut *meta,
but_workspace::branch::apply::Options {
integration_mode: IntegrationMode::AlwaysMerge,
on_workspace_conflict: OnWorkspaceMergeConflict::MaterializeAndReportConflictingStacks,
workspace_reference_naming: WorkspaceReferenceNaming::Default,
uncommitted_changes: UncommitedWorktreeChanges::KeepAndAbortOnConflict,
order,
},
)?;

if project.is_some() {
// write metadata if there are projects - this is a special case while we use vb.toml.
ManuallyDrop::into_inner(meta);
}
Ok(())
}

pub fn create_reference(
args: &super::Args,
short_name: &str,
Expand Down
3 changes: 2 additions & 1 deletion crates/but-testing/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use command::parse_diff_spec;
use gix::bstr::BString;

mod args;
use crate::args::Subcommands;
use crate::command::{RepositoryOpenMode, repo_and_maybe_project};
use args::Args;

Expand All @@ -31,6 +32,7 @@ async fn main() -> Result<()> {
}

match &args.cmd {
Subcommands::Apply { branch_name, order } => command::apply(&args, branch_name, *order),
args::Subcommands::AddProject {
switch_to_workspace,
path,
Expand All @@ -39,7 +41,6 @@ async fn main() -> Result<()> {
path.to_owned(),
switch_to_workspace.to_owned(),
),

args::Subcommands::RemoveReference {
permit_empty_stacks,
keep_metadata,
Expand Down
Loading
Loading