diff --git a/crates/but-api/src/branch.rs b/crates/but-api/src/branch.rs index 32dfb21d0e9..fd7c6bc43bc 100644 --- a/crates/but-api/src/branch.rs +++ b/crates/but-api/src/branch.rs @@ -843,6 +843,7 @@ pub fn apply_only_with_perm( workspace_reference_naming: WorkspaceReferenceNaming::default(), order: None, new_stack_id: None, + allow_applying_already_applied_branch_when_outside_workspace: false, }, )?; diff --git a/crates/but-debug/src/command/workspace.rs b/crates/but-debug/src/command/workspace.rs index fd646d85bae..99497fe2732 100644 --- a/crates/but-debug/src/command/workspace.rs +++ b/crates/but-debug/src/command/workspace.rs @@ -34,6 +34,7 @@ pub(crate) fn apply( workspace_reference_naming: WorkspaceReferenceNaming::Default, order: None, new_stack_id: None, + ..Default::default() }, )?; diff --git a/crates/but-workspace/src/branch/apply.rs b/crates/but-workspace/src/branch/apply.rs index 5d1de22edbb..5fc14571324 100644 --- a/crates/but-workspace/src/branch/apply.rs +++ b/crates/but-workspace/src/branch/apply.rs @@ -1,10 +1,34 @@ +use anyhow::{Context as _, bail}; use bstr::ByteSlice as _; +use but_core::{ + ObjectStorageExt, RefMetadata, RepositoryExt, extract_remote_name_and_short_name, ref_metadata, + ref_metadata::{ + Workspace, + WorkspaceCommitRelation::{Merged, Outside}, + }, +}; use but_core::{ WORKSPACE_REF_NAME, ref_metadata::{StackId, StackKind}, }; +use but_graph::{SegmentIndex, init::Overlay, petgraph::Direction, workspace::WorkspaceKind}; +use gix::{ + prelude::ObjectIdExt, + reference::Category, + refs::{ + FullNameRef, Target, + transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, + }, +}; +use tracing::instrument; use crate::branch::{OnWorkspaceMergeConflict, try_find_validated_ref}; +use crate::{ + WorkspaceCommit, + branch::{anon_stacks, ensure_no_missing_stacks}, + commit::merge::Tip, + ref_info::WorkspaceExt, +}; /// A stack that conflicted while applying a branch. #[derive(Clone)] @@ -167,34 +191,20 @@ pub struct Options { pub order: Option, /// Create new stack id, which by default is a function that generates a new StackId. pub new_stack_id: Option StackId>, + /// By default applying branches that are already applied is considered an error. Setting this + /// to `true` changes that so if we're not in a workspace, applying already applied branches is + /// allowed. + /// + /// The apply follows the normal apply path, as if the branch wasn't applied. + /// + /// The use case for this is to go from single branch mode to workspace mode. If we're in SBM + /// with branch `foo` applied and we apply `foo` with + /// `allow_applying_already_applied_branch_when_outside_workspace` set to `true`, we'll be put + /// into a workspace with only `foo` applied, regardless which branches were previously + /// applied. + pub allow_applying_already_applied_branch_when_outside_workspace: bool, } -use anyhow::{Context as _, bail}; -use but_core::{ - ObjectStorageExt, RefMetadata, RepositoryExt, extract_remote_name_and_short_name, ref_metadata, - ref_metadata::{ - Workspace, - WorkspaceCommitRelation::{Merged, Outside}, - }, -}; -use but_graph::{SegmentIndex, init::Overlay, petgraph::Direction, workspace::WorkspaceKind}; -use gix::{ - prelude::ObjectIdExt, - reference::Category, - refs::{ - FullNameRef, Target, - transaction::{Change, LogChange, PreviousValue, RefEdit, RefLog}, - }, -}; -use tracing::instrument; - -use crate::{ - WorkspaceCommit, - branch::{anon_stacks, ensure_no_missing_stacks}, - commit::merge::Tip, - ref_info::WorkspaceExt, -}; - /// Apply `branch` to the given `workspace`, and possibly create the workspace reference in `repo` /// along with its `meta`-data if it doesn't exist yet. /// The changed workspace will be checked out. @@ -228,6 +238,7 @@ pub fn apply( workspace_reference_naming, order, new_stack_id, + allow_applying_already_applied_branch_when_outside_workspace, }: Options, ) -> anyhow::Result { let ws = workspace; @@ -256,11 +267,22 @@ pub fn apply( branch = upstream_branch_name; branch_ref = try_find_validated_ref(repo, branch.as_ref(), "apply")?; } + let head_ref_name = repo.head_name()?.map(|name| name.to_owned()); + let head_on_managed_workspace_ref = match &ws.kind { + WorkspaceKind::Managed { ref_info } + | WorkspaceKind::ManagedMissingWorkspaceCommit { ref_info } => head_ref_name + .as_ref() + .is_some_and(|head| head.as_ref() == ref_info.ref_name.as_ref()), + WorkspaceKind::AdHoc => false, + }; let branch_has_applied_metadata = branch_has_applied_workspace_metadata(branch.as_ref(), &ws, meta)?; let branch_already_applied = ws.is_reachable_from_entrypoint(branch.as_ref()) && branch_has_applied_metadata; - if branch_already_applied { + if branch_already_applied + && (!allow_applying_already_applied_branch_when_outside_workspace + || head_on_managed_workspace_ref) + { let workspace_ref_created = false; // When exiting early, don't try to adjust the ws commit. return Ok(Outcome { @@ -382,7 +404,6 @@ pub fn apply( }; // Whether HEAD already points at the workspace ref, or sits directly on a branch. When it's on a // branch we move HEAD onto the workspace ref and rebuild the workspace around the branches we keep. - let head_ref_name = repo.head_name()?.map(|rn| rn.to_owned()); let head_on_workspace_ref = head_ref_name .as_ref() .is_some_and(|head| head.as_bstr() == workspace_ref_name_to_update.as_bstr()); diff --git a/crates/but-workspace/src/branch/create_reference.rs b/crates/but-workspace/src/branch/create_reference.rs index cb9f3bd4a55..51294a897c5 100644 --- a/crates/but-workspace/src/branch/create_reference.rs +++ b/crates/but-workspace/src/branch/create_reference.rs @@ -330,30 +330,29 @@ pub(super) mod function { AnchorResolution::positioned(ref_target_id, validate_id, instruction) } - Some(Anchor::AtSegment { ref_name, position }) => { + Some(Anchor::AtSegment { + ref_name: anchor_ref, + position, + }) => { let mut validate_id = true; let ref_target_id = if workspace.has_metadata() { let (stack_idx, seg_idx) = - workspace.try_find_segment_owner_indexes_by_refname(ref_name.as_ref())?; + workspace.try_find_segment_owner_indexes_by_refname(anchor_ref.as_ref())?; let segment = &workspace.stacks[stack_idx].segments[seg_idx]; - let id = workspace + workspace .tip_commit_by_segment_id(segment.id) .map(|commit| position.resolve_commit(commit.into(), ws_base)) .context( "BUG: we should always see through to the base or eligible commits", - )??; - if Some(id) == ws_base { - validate_id = false - } - id + )?? } else { let Some((_stack, segment)) = - workspace.find_segment_and_stack_by_refname(ref_name.as_ref()) + workspace.find_segment_and_stack_by_refname(anchor_ref.as_ref()) else { bail!( "Could not find a segment named '{}' in workspace", - ref_name.shorten() + anchor_ref.shorten() ); }; position.resolve_commit( @@ -365,11 +364,42 @@ pub(super) mod function { ws_base, )? }; - AnchorResolution::positioned( + let points_to_workspace_base = Some(ref_target_id) == ws_base; + if points_to_workspace_base { + validate_id = false; + } + // The lower bound owns no commits, so an ad-hoc workspace needs explicit ref + // ordering to project the new empty segment at that boundary. + let branch_stack_order = if !workspace.has_metadata() && points_to_workspace_base { + if !meta.can_persist_branch_stack_order() { + bail_precondition!( + "Cannot position '{new}' relative to local reference '{anchor}' at the workspace base without branch order metadata", + new = ref_name.shorten(), + anchor = anchor_ref.shorten() + ); + } + let existing_order = meta + .branch_stack_order(anchor_ref.as_ref())? + .unwrap_or_default(); + Some(insert_into_branch_stack_order( + existing_order, + anchor_ref.as_ref(), + ref_name, + position, + )) + } else { + None + }; + let mut resolution = AnchorResolution::positioned( ref_target_id, validate_id, - Some(Instruction::Dependent { ref_name, position }), - ) + Some(Instruction::Dependent { + ref_name: anchor_ref, + position, + }), + ); + resolution.branch_stack_order = branch_stack_order; + resolution } // Position relative to another *reference* on the same commit. Managed workspaces // order these in workspace metadata; ad-hoc workspaces record the order in the diff --git a/crates/but-workspace/tests/workspace/branch/apply_unapply.rs b/crates/but-workspace/tests/workspace/branch/apply_unapply.rs index 4a6d6ef35e8..10a999ef0f4 100644 --- a/crates/but-workspace/tests/workspace/branch/apply_unapply.rs +++ b/crates/but-workspace/tests/workspace/branch/apply_unapply.rs @@ -6226,6 +6226,7 @@ fn apply_options() -> but_workspace::branch::apply::Options { workspace_reference_naming: WorkspaceReferenceNaming::Default, order: None, new_stack_id: Some(stack_id_for_name), + allow_applying_already_applied_branch_when_outside_workspace: false, } } diff --git a/crates/but/src/command/legacy/branch/new.rs b/crates/but/src/command/legacy/branch/new.rs index 108eef8e618..1d64ca6aaa6 100644 --- a/crates/but/src/command/legacy/branch/new.rs +++ b/crates/but/src/command/legacy/branch/new.rs @@ -1,9 +1,9 @@ use std::borrow::Cow; -use anyhow::Context as _; +use anyhow::{Context as _, bail}; use but_core::{ DryRun, RefMetadata, - ref_metadata::StackId, + ref_metadata::{ProjectMeta, StackId}, sync::{RepoExclusive, RepoShared}, }; use but_ctx::Context; @@ -36,7 +36,6 @@ pub fn new( args: NewPlatform, ) -> CliResult { let mut guard = ctx.exclusive_worktree_access(); - let mut meta = ctx.meta()?; let id_map = IdMap::new_from_context(ctx, guard.read_permission())?; let operation = { @@ -44,6 +43,7 @@ pub fn new( resolve(ctx, guard.read_permission(), args, &head_info, &id_map)? }; + let mut meta = ctx.meta()?; Ok(run(ctx, &mut meta, guard.write_permission(), operation)?) } @@ -85,7 +85,9 @@ fn resolve( }; match (above, below) { - (None, None) => Ok(NewOperation::NewUnstackedBranch { name }), + (None, None) => Ok(NewOperation::NewUnstackedBranch( + NewUnstackedBranchOperation { name }, + )), (None, Some(target_below)) => { let target = resolve_above_below_target(&repo, id_map, target_below)?; @@ -98,19 +100,19 @@ fn resolve( } } - Ok(NewOperation::NewStackedBranch { + Ok(NewOperation::NewStackedBranch(NewStackedBranchOperation { name, target, side: Side::Below, - }) + })) } (Some(target_above), None) => { let target = resolve_above_below_target(&repo, id_map, target_above)?; - Ok(NewOperation::NewStackedBranch { + Ok(NewOperation::NewStackedBranch(NewStackedBranchOperation { name, target, side: Side::Above, - }) + })) } (Some(_), Some(_)) => { unreachable!("--above and --below are mutually exclusive in the clap args") @@ -141,14 +143,18 @@ fn resolve_above_below_target( } pub enum NewOperation { - NewUnstackedBranch { - name: Option, - }, - NewStackedBranch { - name: Option, - target: NewStackedBranchTarget, - side: Side, - }, + NewUnstackedBranch(NewUnstackedBranchOperation), + NewStackedBranch(NewStackedBranchOperation), +} + +pub struct NewUnstackedBranchOperation { + pub name: Option, +} + +pub struct NewStackedBranchOperation { + pub name: Option, + pub target: NewStackedBranchTarget, + pub side: Side, } pub enum NewStackedBranchTarget { @@ -162,89 +168,270 @@ pub fn run( perm: &mut RepoExclusive, operation: NewOperation, ) -> anyhow::Result { - let in_single_branch_mode = ctx.settings.feature_flags.single_branch - && gitbutler_operating_modes::in_outside_workspace_mode(ctx, perm.read_permission())?; - let mut checkout_after_create = false; - - let snapshot_details = SnapshotDetails::new(OperationKind::CreateBranch); - let (name, _ws) = but_transaction::with_transaction_with_perm( - ctx, - meta, - perm, - snapshot_details, - DryRun::No, - |mut tx| { - let new_ref = match &operation { - NewOperation::NewStackedBranch { name, .. } - | NewOperation::NewUnstackedBranch { name } => { - if let Some(name) = name { - name.clone() - } else { - but_core::branch::unique_canned_refname(tx.repo())? - } + match operation { + NewOperation::NewUnstackedBranch(op) => op.execute(ctx, meta, perm), + NewOperation::NewStackedBranch(op) => op.execute(ctx, meta, perm), + } +} + +impl NewUnstackedBranchOperation { + fn execute( + self, + ctx: &mut Context, + meta: &mut impl RefMetadata, + perm: &mut RepoExclusive, + ) -> anyhow::Result { + let in_single_branch_mode = ctx.settings.feature_flags.single_branch + && gitbutler_operating_modes::in_outside_workspace_mode(ctx, perm.read_permission())?; + + if in_single_branch_mode { + self.execute_single_branch_mode(ctx, meta, perm) + } else { + self.execute_workspace_mode(ctx, meta, perm) + } + } + + fn execute_workspace_mode( + self, + ctx: &mut Context, + meta: &mut impl RefMetadata, + perm: &mut RepoExclusive, + ) -> anyhow::Result { + let Self { name } = self; + + let snapshot_details = SnapshotDetails::new(OperationKind::CreateBranch); + + let (new_ref, _ws) = but_transaction::with_transaction_with_perm( + ctx, + meta, + perm, + snapshot_details, + DryRun::No, + |mut tx| { + let new_ref = if let Some(name) = name { + name.clone() + } else { + but_core::branch::unique_canned_refname(tx.repo())? + }; + + tx.create_reference(new_ref.as_ref(), None, |_| StackId::generate(), Some(0))?; + + Ok(but_transaction::Commit(new_ref)) + }, + )?; + + Ok(NewOutcome { + name: new_ref, + target: None, + }) + } + + fn execute_single_branch_mode( + self, + ctx: &mut Context, + meta: &mut impl RefMetadata, + perm: &mut RepoExclusive, + ) -> anyhow::Result { + let Self { name } = self; + + let snapshot_details = SnapshotDetails::new(OperationKind::CreateBranch); + + let repo = ctx.repo.get()?; + let project_meta = ProjectMeta::resolve(&repo)?; + let head_name = head_name(&repo)?; + + let new_ref = if let Some(name) = name { + name.clone() + } else { + but_core::branch::unique_canned_refname(&repo)? + }; + + let target_ref = project_meta + .target_ref + .as_ref() + .context("BUG: target ref is missing")?; + + let is_on_target = + but_core::branch::resolve_tracking_branch_ref_name(head_name.as_ref(), &repo) + .is_ok_and(|upstream| &*upstream == target_ref.as_ref()); + + if is_on_target { + // we're directly on the target then we haven't created any branches yet so + // create the branch on top of the target then check it out + + drop(repo); + + but_transaction::with_transaction_with_perm( + ctx, + meta, + perm, + snapshot_details, + DryRun::No, + |mut tx| { + let anchor = Some(Anchor::AtReference { + ref_name: Cow::Owned(head_name), + position: Side::Above.into(), + }); + + tx.create_reference( + new_ref.as_ref(), + anchor, + |_| StackId::generate(), + Some(0), + )?; + + Ok(()) + }, + )?; + + but_api::branch::branch_checkout_with_perm(ctx, new_ref.clone(), perm)?; + } else { + // if we're not on the target then enter a workspace and create the branch + + if repo + .try_find_reference(but_core::WORKSPACE_REF_NAME)? + .is_none() + { + // the workspace doesn't exist, create it + drop(repo); + let target_ref = target_ref.to_string().parse()?; + gitbutler_branch_actions::set_base_branch(ctx, &target_ref, perm)?; + } else { + drop(repo); + } + + // make sure the previous branch is applied + // if the branch had no commits `set_base_branch` doesn't apply it + // + // this also has the effect of entering the workspace with one branch applied + { + let (repo, mut ws, _db) = ctx.workspace_mut_and_db_with_perm(perm)?; + let outcome = but_workspace::branch::apply( + head_name.as_ref(), + ws.clone(), + &repo, + meta, + but_workspace::branch::apply::Options { + allow_applying_already_applied_branch_when_outside_workspace: true, + ..Default::default() + }, + )?; + if outcome.status.persisted_mutation() { + *ws = outcome.workspace.clone(); + } else { + bail!( + "BUG: failed to apply head ref ({head_name}). Failed with {:?}", + outcome.status + ) } }; - let anchor = match &operation { - NewOperation::NewUnstackedBranch { name: _ } => None, - NewOperation::NewStackedBranch { - name: _, - target, - side, - } => Some(match target { + but_transaction::with_transaction_with_perm( + ctx, + meta, + perm, + snapshot_details, + DryRun::No, + |mut tx| { + tx.create_reference(new_ref.as_ref(), None, |_| StackId::generate(), Some(0))?; + + Ok(()) + }, + )?; + } + + Ok(NewOutcome { + name: new_ref, + target: None, + }) + } +} + +impl NewStackedBranchOperation { + fn execute( + self, + ctx: &mut Context, + meta: &mut impl RefMetadata, + perm: &mut RepoExclusive, + ) -> anyhow::Result { + let Self { name, target, side } = self; + + let in_single_branch_mode = ctx.settings.feature_flags.single_branch + && gitbutler_operating_modes::in_outside_workspace_mode(ctx, perm.read_permission())?; + + let mut checkout_after_create = false; + + let snapshot_details = SnapshotDetails::new(OperationKind::CreateBranch); + + let (new_ref, _ws) = but_transaction::with_transaction_with_perm( + ctx, + meta, + perm, + snapshot_details, + DryRun::No, + |mut tx| { + let new_ref = if let Some(name) = name { + name.clone() + } else { + but_core::branch::unique_canned_refname(tx.repo())? + }; + + let anchor = match &target { NewStackedBranchTarget::Commit(commit_target) => Anchor::AtCommit { commit_id: commit_target.commit_id, - position: (*side).into(), + position: side.into(), }, NewStackedBranchTarget::Branch(branch_target) => { - Anchor::at_segment(branch_target.as_ref(), (*side).into()) + Anchor::at_segment(branch_target.as_ref(), side.into()) } - }), - }; - - let anchor = if let Some(anchor) = anchor { - match anchor { - Anchor::AtSegment { position, ref_name } - if matches!(position, Position::Above) && in_single_branch_mode => - { - let head_name = head_name(tx.repo())?; - if &*ref_name == head_name.as_ref() { - Some(single_branch_mode_anchor( - head_name, - &mut checkout_after_create, - )?) - } else { - Some(Anchor::AtReference { ref_name, position }) + }; + + let anchor = if in_single_branch_mode + && let Anchor::AtSegment { + position: position @ Position::Above, + ref_name, + } = anchor + { + // creating a new branch above HEAD works differently in single branch mode + // have to use a different anchor type and manually checkout the newly created + // branch + let head_name = head_name(tx.repo())?; + if &*ref_name == head_name.as_ref() { + checkout_after_create = true; + Anchor::AtReference { + ref_name: Cow::Owned(head_name), + position: Side::Above.into(), } + } else { + Anchor::AtReference { ref_name, position } } - _ => Some(anchor), - } - } else if in_single_branch_mode { - let head_name = head_name(tx.repo())?; - Some(single_branch_mode_anchor( - head_name, - &mut checkout_after_create, - )?) - } else { - None - }; - - tx.create_reference(new_ref.as_ref(), anchor, |_| StackId::generate(), Some(0))?; - - Ok(but_transaction::Commit(new_ref)) - }, - )?; + } else { + anchor + }; + + tx.create_reference( + new_ref.as_ref(), + anchor.clone(), + |_| StackId::generate(), + Some(0), + ) + .with_context(|| { + format!("failed to create reference. anchor={anchor:?}; new_ref={new_ref:?}") + })?; + + Ok(but_transaction::Commit(new_ref)) + }, + )?; + + if checkout_after_create { + but_api::branch::branch_checkout_with_perm(ctx, new_ref.clone(), perm)?; + } - if checkout_after_create { - but_api::branch::branch_checkout_with_perm(ctx, name.clone(), perm)?; + Ok(NewOutcome { + name: new_ref, + target: Some((target, side)), + }) } - - let target = match operation { - NewOperation::NewUnstackedBranch { .. } => None, - NewOperation::NewStackedBranch { target, side, .. } => Some((target, side)), - }; - - Ok(NewOutcome { name, target }) } fn head_name(repo: &gix::Repository) -> anyhow::Result { @@ -256,18 +443,6 @@ fn head_name(repo: &gix::Repository) -> anyhow::Result { .to_owned()) } -fn single_branch_mode_anchor( - head_name: FullName, - checkout_after_create: &mut bool, -) -> anyhow::Result> { - *checkout_after_create = true; - - Ok(Anchor::AtReference { - ref_name: Cow::Owned(head_name), - position: Side::Above.into(), - }) -} - #[must_use] pub struct NewOutcome { pub name: FullName, diff --git a/crates/but/src/command/legacy/push.rs b/crates/but/src/command/legacy/push.rs index 56cfade77f9..ab7265ce12c 100644 --- a/crates/but/src/command/legacy/push.rs +++ b/crates/but/src/command/legacy/push.rs @@ -7,8 +7,8 @@ use gitbutler_git::PushResult; use serde::Serialize; use crate::{ - CliId, CliResultExt as _, IdMap, - args::{push, push::Command}, + CliId, CliResult, IdMap, + args::push::{self, Command}, command::legacy::workspace_target, theme::{self, Paint}, utils::{ @@ -47,7 +47,7 @@ pub async fn handle( args: push::Command, ctx: &mut Context, out: &mut OutputChannel, -) -> anyhow::Result<()> { +) -> CliResult<()> { // Check gerrit mode early let gerrit_mode = { let repo = ctx.repo.get()?; @@ -56,7 +56,7 @@ pub async fn handle( // If dry-run, show what would be pushed if args.dry_run { - return handle_dry_run(ctx, &args.branch_id, out); + return Ok(handle_dry_run(ctx, &args.branch_id, out)?); } let id_map = { @@ -95,9 +95,7 @@ pub async fn handle( let merged = MergedUpstream::from_ctx(ctx, args.allow_merged)?; for name in names { let full_name = gix::refs::FullName::try_from(format!("refs/heads/{name}"))?; - merged - .ensure_branch_not_merged(full_name.as_ref()) - .into_internal_error()?; + merged.ensure_branch_not_merged(full_name.as_ref())?; } } diff --git a/crates/but/src/command/legacy/status/tui/app/mod.rs b/crates/but/src/command/legacy/status/tui/app/mod.rs index c85878f172d..62811e68924 100644 --- a/crates/but/src/command/legacy/status/tui/app/mod.rs +++ b/crates/but/src/command/legacy/status/tui/app/mod.rs @@ -22,7 +22,10 @@ use crate::{ legacy::{ branch::{ self, - new::{NewOperation, NewStackedBranchTarget}, + new::{ + NewOperation, NewStackedBranchOperation, NewStackedBranchTarget, + NewUnstackedBranchOperation, + }, }, commit::{ self, CommitAtOperation, CommitOperation, CommitRelativeToTarget, CommitSelection, @@ -136,6 +139,7 @@ pub struct App { pub head_sha: String, pub clipboard: Clipboard, pub operating_mode: OperatingMode, + pub is_in_single_branch_mode: bool, } pub(super) fn changed_paths_affect_uncommitted_details<'a>( @@ -402,7 +406,7 @@ impl App { let file_browser = show_file_browser.then(FileBrowser::default); - Ok(Self { + let mut app = Self { status_lines, flags, cursor, @@ -431,7 +435,12 @@ impl App { head_sha, clipboard, operating_mode, - }) + is_in_single_branch_mode: false, + }; + + app.reload_is_in_single_branch_mode(ctx)?; + + Ok(app) } pub fn active_key_binds(&self) -> &KeyBinds { @@ -1394,6 +1403,17 @@ impl App { } } + self.reload_is_in_single_branch_mode(ctx)?; + + Ok(()) + } + + fn reload_is_in_single_branch_mode(&mut self, ctx: &Context) -> anyhow::Result<()> { + let guard = ctx.shared_worktree_access(); + + self.is_in_single_branch_mode = ctx.settings.feature_flags.single_branch + && gitbutler_operating_modes::in_outside_workspace_mode(ctx, guard.read_permission())?; + Ok(()) } @@ -1556,13 +1576,13 @@ impl App { ctx, &mut meta, guard.write_permission(), - NewOperation::NewStackedBranch { + NewOperation::NewStackedBranch(NewStackedBranchOperation { name: None, target: NewStackedBranchTarget::Branch( Category::LocalBranch.to_full_name(&*branch.name)?, ), side: Side::Above, - }, + }), )?; outcome.name.shorten().to_string() @@ -1577,7 +1597,7 @@ impl App { ctx, &mut meta, guard.write_permission(), - NewOperation::NewUnstackedBranch { name: None }, + NewOperation::NewUnstackedBranch(NewUnstackedBranchOperation { name: None }), )?; outcome.name.shorten().to_string() diff --git a/crates/but/src/command/legacy/status/tui/render.rs b/crates/but/src/command/legacy/status/tui/render.rs index ade1842cd65..7bef7bc6ff9 100644 --- a/crates/but/src/command/legacy/status/tui/render.rs +++ b/crates/but/src/command/legacy/status/tui/render.rs @@ -1031,9 +1031,26 @@ fn render_hot_bar(app: &App, area: Rect, frame: &mut Frame) { frame.render_widget(" ", layout[1]); - app.mode - .as_mode_render() - .render_hot_bar_content(app, layout[2], frame); + if app.is_in_single_branch_mode { + let content_layout = + Layout::horizontal([Constraint::Min(1), Constraint::Length(5)]).split(layout[2]); + + app.mode + .as_mode_render() + .render_hot_bar_content(app, content_layout[0], frame); + + frame.render_widget( + Span::styled( + " SBM ", + Style::default().bg(ModeDiscriminant::Normal.bg(app.theme)), + ), + content_layout[1], + ); + } else { + app.mode + .as_mode_render() + .render_hot_bar_content(app, layout[2], frame); + } if let Some(started_at) = loading_spinner_started_at { let mut line = RenderSingleLineSpans::new(frame, layout[3]); diff --git a/crates/but/src/command/legacy/status/tui/tests/mod.rs b/crates/but/src/command/legacy/status/tui/tests/mod.rs index 04b2760b8bb..efcb6257d4e 100644 --- a/crates/but/src/command/legacy/status/tui/tests/mod.rs +++ b/crates/but/src/command/legacy/status/tui/tests/mod.rs @@ -29,6 +29,7 @@ mod marking_tests; mod move_tests; mod open_tests; mod pick_tests; +mod single_branch_mode; mod squash_tests; mod stack_tests; mod utils; diff --git a/crates/but/src/command/legacy/status/tui/tests/single_branch_mode.rs b/crates/but/src/command/legacy/status/tui/tests/single_branch_mode.rs new file mode 100644 index 00000000000..7b359eb902a --- /dev/null +++ b/crates/but/src/command/legacy/status/tui/tests/single_branch_mode.rs @@ -0,0 +1,15 @@ +use but_testsupport::Sandbox; +use snapbox::file; + +use crate::command::legacy::status::tui::tests::utils::test_status_tui; + +#[test] +fn shows_single_branch_mode_label_in_hot_bar() { + let env = Sandbox::init_scenario_with_target_and_default_settings("single-branch-mode"); + + let mut tui = test_status_tui(env); + + tui.reload().assert_rendered_term_svg_eq(file![ + "snapshots/shows_single_branch_mode_label_in_hot_bar_001.svg" + ]); +} diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_from_unstaged_changes_to_new_branch_checks_out_branch_in_single_branch_mode_final.svg b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_from_unstaged_changes_to_new_branch_checks_out_branch_in_single_branch_mode_final.svg index 1b98943cf98..4c6145d925c 100644 --- a/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_from_unstaged_changes_to_new_branch_checks_out_branch_in_single_branch_mode_final.svg +++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/commit_from_unstaged_changes_to_new_branch_checks_out_branch_in_single_branch_mode_final.svg @@ -2,6 +2,7 @@ + ╭┄ zz @@ -61,13 +62,11 @@ b branch - s - stack - - ? - help - - q - quit + ? + help + + q + quit + SBM diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/marks_spanning_checkouts_are_refused.svg b/crates/but/src/command/legacy/status/tui/tests/snapshots/marks_spanning_checkouts_are_refused.svg index c1af737edd4..2957703df47 100644 --- a/crates/but/src/command/legacy/status/tui/tests/snapshots/marks_spanning_checkouts_are_refused.svg +++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/marks_spanning_checkouts_are_refused.svg @@ -48,36 +48,19 @@ 2000-01-02 add M - ┌───────────────────────────────────────────────────────────────────────┐ - - Cannot - use - changes - from - the - uncommitted - area - and - worktree - wt - together - - - + ┌───────────────────────────────────────────────────────────────────────┐ - Hint: - An - operation - can - only - take - changes - from - one - checkout - at - a - time + Cannot + use + changes + from + the + uncommitted + area + and + worktree + wt + together └───────────────────────────────────────────────────────────────────────┘ normal diff --git a/crates/but/src/command/legacy/status/tui/tests/snapshots/shows_single_branch_mode_label_in_hot_bar_001.svg b/crates/but/src/command/legacy/status/tui/tests/snapshots/shows_single_branch_mode_label_in_hot_bar_001.svg new file mode 100644 index 00000000000..45ce89339b6 --- /dev/null +++ b/crates/but/src/command/legacy/status/tui/tests/snapshots/shows_single_branch_mode_label_in_hot_bar_001.svg @@ -0,0 +1,56 @@ + + + + + + + ╭┄ + zz + [ + uncommitted + ] + (no + changes) + + ┊╭┄ + ma + [ + main + ] + (no + commits) + ├╯ + + + b1540e5 + (common + base) + 2000-01-02 + M + normal + ↑/k + up + + ↓/j + down + + c + commit + + r + squash + + m + move + + b + branch + + ? + help + + q + quit + SBM + + diff --git a/crates/but/src/command/legacy/status/tui/tests/utils.rs b/crates/but/src/command/legacy/status/tui/tests/utils.rs index 554cb6846a1..6f8da7bb392 100644 --- a/crates/but/src/command/legacy/status/tui/tests/utils.rs +++ b/crates/but/src/command/legacy/status/tui/tests/utils.rs @@ -95,6 +95,10 @@ pub fn test_status_tui_with_options(mut env: Sandbox, options: TestTuiOptions) - StatusRenderMode::Tui(launch_options.clone()), ) .expect("failed to build status context"); + + // App::new acquires a new guard so have to drop this before calling that + drop(guard); + let initial_target = resolve_tui_target( &ctx.repo.get().unwrap(), &status_ctx.id_map, diff --git a/crates/but/src/error.rs b/crates/but/src/error.rs index e683277101e..a8f36d6f8a8 100644 --- a/crates/but/src/error.rs +++ b/crates/but/src/error.rs @@ -172,7 +172,14 @@ impl CliError { pub fn into_internal(self) -> anyhow::Error { match self { - Self::BadInput(..) | Self::ExternalCommandNotFound(..) | Self::CommandRejection => { + Self::BadInput(mut err) => { + // the hints are unlikely to be useful for internal errors so lets just discard + // them + err.hint = None; + + anyhow::anyhow!("{err}") + } + Self::ExternalCommandNotFound(..) | Self::CommandRejection => { anyhow::anyhow!("{self}") } Self::Internal(error) => error, diff --git a/crates/but/tests/but/command/branch/new.rs b/crates/but/tests/but/command/branch/new.rs index cb7601f62b8..7eb36723d1c 100644 --- a/crates/but/tests/but/command/branch/new.rs +++ b/crates/but/tests/but/command/branch/new.rs @@ -175,8 +175,8 @@ fn with_json_output() { } #[test] -fn create_new_branch_in_single_branch_mode() { - let env = Sandbox::open_with_default_settings("one-fork"); +fn in_single_branch_mode_creating_stacked_branches() { + let env = Sandbox::open_with_default_settings("single-branch-mode"); env.but("status") .assert() @@ -185,11 +185,10 @@ fn create_new_branch_in_single_branch_mode() { .stdout_eq(str![[r#" ╭┄ zz [uncommitted] (no changes) ┊ -┊╭┄ ma [main] -┊● nmy M (no changes) +┊╭┄ ma [main] (no commits) ├╯ ┊ -┴ e31e6ca (common base) 2000-01-02 add init +┴ b1540e5 (common base) 2000-01-02 M Hint: run `but help` for all commands @@ -214,12 +213,9 @@ Created branch 'middle' ╭┄ zz [uncommitted] (no changes) ┊ ┊╭┄ mi [middle] (no commits) -┊│ -┊├┄ ma [main] -┊● nmy M (no changes) ├╯ ┊ -┴ e31e6ca (common base) 2000-01-02 add init +┴ b1540e5 (common base) 2000-01-02 M Hint: run `but help` for all commands @@ -244,12 +240,9 @@ Created branch 'bottom' below branch 'middle' ┊╭┄ mi [middle] (no commits) ┊│ ┊├┄ bo [bottom] (no commits) -┊│ -┊├┄ ma [main] -┊● nmy M (no changes) ├╯ ┊ -┴ e31e6ca (common base) 2000-01-02 add init +┴ b1540e5 (common base) 2000-01-02 M Hint: run `but help` for all commands @@ -276,12 +269,9 @@ Created branch 'top' above branch 'middle' ┊├┄ mi [middle] (no commits) ┊│ ┊├┄ bo [bottom] (no commits) -┊│ -┊├┄ ma [main] -┊● nmy M (no changes) ├╯ ┊ -┴ e31e6ca (common base) 2000-01-02 add init +┴ b1540e5 (common base) 2000-01-02 M Hint: run `but help` for all commands @@ -310,42 +300,27 @@ Created branch 'between-middle-and-top' above branch 'middle' ┊├┄ mi [middle] (no commits) ┊│ ┊├┄ bo [bottom] (no commits) -┊│ -┊├┄ ma [main] -┊● nmy M (no changes) ├╯ ┊ -┴ e31e6ca (common base) 2000-01-02 add init +┴ b1540e5 (common base) 2000-01-02 M Hint: run `but help` for all commands "#]]); - let repo = env.open_repo(); - - // ensure the branches exist for real - for branch_name in ["top", "between-middle-and-top", "middle", "bottom"] { - let reference_name = format!("refs/heads/{branch_name}"); - assert!( - repo.try_find_reference(reference_name.as_str()) - .unwrap() - .is_some(), - "single-branch creation writes the branch reference" - ); - } + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* b1540e5 (HEAD -> top, origin/main, origin/HEAD, middle, main, gitbutler/target, bottom, between-middle-and-top) M +* e31e6ca add init - // ensure we didn't create the workspace ref - assert!( - repo.try_find_reference(but_core::WORKSPACE_REF_NAME) - .unwrap() - .is_none(), - "single-branch creation does not create a managed workspace reference" +"#]] ); } #[test] -fn create_new_branches_with_commits_in_single_branch_mode() { - let env = Sandbox::open_with_default_settings("one-fork"); +fn in_single_branch_mode_create_new_branches_with_commits() { + let env = Sandbox::open_with_default_settings("single-branch-mode"); env.but("status") .assert() @@ -354,11 +329,10 @@ fn create_new_branches_with_commits_in_single_branch_mode() { .stdout_eq(str![[r#" ╭┄ zz [uncommitted] (no changes) ┊ -┊╭┄ ma [main] -┊● nmy M (no changes) +┊╭┄ ma [main] (no commits) ├╯ ┊ -┴ e31e6ca (common base) 2000-01-02 add init +┴ b1540e5 (common base) 2000-01-02 M Hint: run `but help` for all commands @@ -385,12 +359,9 @@ Created branch 'middle' ┊ ┊╭┄ mi [middle] ┊● 1 on middle (no changes) -┊│ -┊├┄ ma [main] -┊● nmy M (no changes) ├╯ ┊ -┴ e31e6ca (common base) 2000-01-02 add init +┴ b1540e5 (common base) 2000-01-02 M Hint: run `but help` for all commands @@ -420,23 +391,53 @@ Created branch 'top' above branch 'middle' ┊│ ┊├┄ mi [middle] ┊● 1#1 on middle (no changes) -┊│ -┊├┄ ma [main] -┊● nmy M (no changes) ├╯ ┊ -┴ e31e6ca (common base) 2000-01-02 add init +┴ b1540e5 (common base) 2000-01-02 M Hint: run `but help` for all commands "#]]); + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* 278079d (HEAD -> top) on top +* 8e3d31a (middle) on middle +* b1540e5 (origin/main, origin/HEAD, main, gitbutler/target) M +* e31e6ca add init + +"#]] + ); + env.but("branch new bottom --below middle") .assert() .success() .stdout_eq(str![[r#" Created branch 'bottom' below branch 'middle' +"#]]); + + env.but("status") + .assert() + .success() + .stderr_eq(str![]) + .stdout_eq(str![[r#" +╭┄ zz [uncommitted] (no changes) +┊ +┊╭┄ to [top] +┊● 1#0 on top (no changes) +┊│ +┊├┄ mi [middle] +┊● 1#1 on middle (no changes) +┊│ +┊├┄ bo [bottom] (no commits) +├╯ +┊ +┴ b1540e5 (common base) 2000-01-02 M + +Hint: run `but help` for all commands + "#]]); env.but("commit --empty -b bottom -m 'on bottom'") @@ -458,12 +459,9 @@ Created branch 'bottom' below branch 'middle' ┊│ ┊├┄ bo [bottom] ┊● 1#2 on bottom (no changes) -┊│ -┊├┄ ma [main] -┊● nmy M (no changes) ├╯ ┊ -┴ e31e6ca (common base) 2000-01-02 add init +┴ b1540e5 (common base) 2000-01-02 M Hint: run `but help` for all commands @@ -499,16 +497,26 @@ Created branch 'between-middle-and-top' above branch 'middle' ┊│ ┊├┄ bo [bottom] ┊● 1#3 on bottom (no changes) -┊│ -┊├┄ ma [main] -┊● nmy M (no changes) ├╯ ┊ -┴ e31e6ca (common base) 2000-01-02 add init +┴ b1540e5 (common base) 2000-01-02 M Hint: run `but help` for all commands "#]]); + + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* a54744f (HEAD -> top) on top +* 8406d60 (between-middle-and-top) on between-middle-and-top +* 5614135 (middle) on middle +* 9133168 (bottom) on bottom +* b1540e5 (origin/main, origin/HEAD, main, gitbutler/target) M +* e31e6ca add init + +"#]] + ); } #[test] @@ -1088,3 +1096,354 @@ Hint: run `but help` for all commands "#]]); } + +#[test] +fn in_single_branch_mode_creating_new_independent_branch_takes_you_to_workspace_mode() { + let env = Sandbox::open_with_default_settings("single-branch-mode"); + + // at first we're not on a workspace + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* b1540e5 (HEAD -> main, origin/main, origin/HEAD) M +* e31e6ca add init + +"#]] + ); + + env.but("status").assert().success().stdout_eq(str![[r#" +╭┄ zz [uncommitted] (no changes) +┊ +┊╭┄ ma [main] (no commits) +├╯ +┊ +┴ b1540e5 (common base) 2000-01-02 M + +Hint: run `but help` for all commands + +"#]]); + + // creating a new branch just puts us on that branch + env.but("branch new one") + .assert() + .success() + .stdout_eq(str![[r#" +Created branch 'one' + +"#]]); + + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* b1540e5 (HEAD -> one, origin/main, origin/HEAD, main, gitbutler/target) M +* e31e6ca add init + +"#]] + ); + + env.but("status").assert().success().stdout_eq(str![[r#" +╭┄ zz [uncommitted] (no changes) +┊ +┊╭┄ on [one] (no commits) +├╯ +┊ +┴ b1540e5 (common base) 2000-01-02 M + +Hint: run `but help` for all commands + +"#]]); + + // creating a second branch puts us into a workspace with both branches applied + env.but("branch new two") + .assert() + .success() + .stderr_eq(str![]) + .stdout_eq(str![[r#" +Created branch 'two' + +"#]]); + + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* 8ad759d (HEAD -> gitbutler/workspace) GitButler Workspace Commit +|/ +* b1540e5 (origin/main, origin/HEAD, two, one, main, gitbutler/target) M +* e31e6ca add init + +"#]] + ); + + env.but("status").assert().success().stdout_eq(str![[r#" +╭┄ zz [uncommitted] (no changes) +┊ +┊╭┄ tw [two] (no commits) +├╯ +┊ +┊╭┄ on [one] (no commits) +├╯ +┊ +┴ b1540e5 (common base) 2000-01-02 M + +Hint: run `but help` for all commands + +"#]]); + + // switching to a branch removes the workspace and checks out the branch + env.but("switch one").assert().success(); + + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* 8ad759d (gitbutler/workspace) GitButler Workspace Commit +|/ +* b1540e5 (HEAD -> one, origin/main, origin/HEAD, two, main, gitbutler/target) M +* e31e6ca add init + +"#]] + ); + + env.but("status").assert().success().stdout_eq(str![[r#" +╭┄ zz [uncommitted] (no changes) +┊ +┊╭┄ on [one] (no commits) +├╯ +┊ +┴ b1540e5 (common base) 2000-01-02 M + +Hint: run `but help` for all commands + +"#]]); + + // creating a new branch puts us back on a workspace with the previous and new branches applied + env.but("branch new three") + .assert() + .success() + .stderr_eq(str![]) + .stdout_eq(str![[r#" +Created branch 'three' + +"#]]); + + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* 9e991f4 (HEAD -> gitbutler/workspace) GitButler Workspace Commit +|/ +* b1540e5 (origin/main, origin/HEAD, two, three, one, main, gitbutler/target) M +* e31e6ca add init + +"#]] + ); + + env.but("status").assert().success().stdout_eq(str![[r#" +╭┄ zz [uncommitted] (no changes) +┊ +┊╭┄ th [three] (no commits) +├╯ +┊ +┊╭┄ on [one] (no commits) +├╯ +┊ +┴ b1540e5 (common base) 2000-01-02 M + +Hint: run `but help` for all commands + +"#]]); +} + +#[test] +fn in_single_branch_mode_switching_to_stacked_branches_works() { + let env = Sandbox::open_with_default_settings("single-branch-mode"); + + env.but("branch new bottom").assert().success(); + + env.but("branch new middle --above bottom") + .assert() + .success(); + + env.but("status").assert().success().stdout_eq(str![[r#" +╭┄ zz [uncommitted] (no changes) +┊ +┊╭┄ mi [middle] (no commits) +┊│ +┊├┄ bo [bottom] (no commits) +├╯ +┊ +┴ b1540e5 (common base) 2000-01-02 M + +Hint: run `but help` for all commands + +"#]]); + + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* b1540e5 (HEAD -> middle, origin/main, origin/HEAD, main, gitbutler/target, bottom) M +* e31e6ca add init + +"#]] + ); + + env.but("switch bottom").assert().success(); + + env.but("status").assert().success().stdout_eq(str![[r#" +╭┄ zz [uncommitted] (no changes) +┊ +┊╭┄ bo [bottom] (no commits) +├╯ +┊ +┴ b1540e5 (common base) 2000-01-02 M + +Hint: run `but help` for all commands + +"#]]); + + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* b1540e5 (HEAD -> bottom, origin/main, origin/HEAD, middle, main, gitbutler/target) M +* e31e6ca add init + +"#]] + ); + + env.but("branch new new-branch").assert().success(); + + env.but("status").assert().success().stdout_eq(str![[r#" +╭┄ zz [uncommitted] (no changes) +┊ +┊╭┄ ne [new-branch] (no commits) +├╯ +┊ +┊╭┄ bo [bottom] (no commits) +├╯ +┊ +┴ b1540e5 (common base) 2000-01-02 M + +Hint: run `but help` for all commands + +"#]]); + + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* 10e74ab (HEAD -> gitbutler/workspace) GitButler Workspace Commit +|/ +* b1540e5 (origin/main, origin/HEAD, new-branch, middle, main, gitbutler/target, bottom) M +* e31e6ca add init + +"#]] + ); +} + +#[test] +fn in_single_branch_mode_switching_to_stacked_branches_with_commits_works() { + let env = Sandbox::open_with_default_settings("single-branch-mode"); + + env.but("branch new bottom").assert().success(); + + env.but("commit -m 'on bottom' -b bottom") + .assert() + .success(); + + env.but("branch new middle --above bottom") + .assert() + .success(); + + env.but("commit -m 'on middle' -b middle") + .assert() + .success(); + + env.but("status").assert().success().stdout_eq(str![[r#" +╭┄ zz [uncommitted] (no changes) +┊ +┊╭┄ mi [middle] +┊● 1#0 on middle (no changes) +┊│ +┊├┄ bo [bottom] +┊● 1#1 on bottom (no changes) +├╯ +┊ +┴ b1540e5 (common base) 2000-01-02 M + +Hint: run `but help` for all commands + +"#]]); + + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* 5614135 (HEAD -> middle) on middle +* 9133168 (bottom) on bottom +* b1540e5 (origin/main, origin/HEAD, main, gitbutler/target) M +* e31e6ca add init + +"#]] + ); + + env.but("switch bottom").assert().success(); + + env.but("status").assert().success().stdout_eq(str![[r#" +╭┄ zz [uncommitted] (no changes) +┊ +┊╭┄ bo [bottom] +┊● 1 on bottom (no changes) +├╯ +┊ +┴ b1540e5 (common base) 2000-01-02 M + +Hint: run `but help` for all commands + +"#]]); + + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* 5614135 (middle) on middle +* 9133168 (HEAD -> bottom) on bottom +* b1540e5 (origin/main, origin/HEAD, main, gitbutler/target) M +* e31e6ca add init + +"#]] + ); + + env.but("branch new new-branch").assert().success(); + + env.but("commit -m 'on new-branch' -b new-branch") + .assert() + .success(); + + env.but("status").assert().success().stdout_eq(str![[r#" +╭┄ zz [uncommitted] (no changes) +┊ +┊╭┄ ne [new-branch] +┊● 1#0 on new-branch (no changes) +├╯ +┊ +┊╭┄ bo [bottom] +┊● 1#1 on bottom (no changes) +├╯ +┊ +┴ b1540e5 (common base) 2000-01-02 M + +Hint: run `but help` for all commands + +"#]]); + + snapbox::assert_data_eq!( + env.git_log(), + snapbox::str![[r#" +* ca16105 (HEAD -> gitbutler/workspace) GitButler Workspace Commit +|/ +* | f6cc4a5 (new-branch) on new-branch +| | * 5614135 (middle) on middle +| |/ +| * 9133168 (bottom) on bottom +|/ +* b1540e5 (origin/main, origin/HEAD, main, gitbutler/target) M +* e31e6ca add init + +"#]] + ); +} diff --git a/crates/but/tests/but/command/commit.rs b/crates/but/tests/but/command/commit.rs index e85d93fed6c..c0211ad6943 100644 --- a/crates/but/tests/but/command/commit.rs +++ b/crates/but/tests/but/command/commit.rs @@ -186,7 +186,7 @@ fn commits_at_each_branch_in_an_existing_single_branch_stack() { env.but("commit --empty -b middle -m 'middle base'") .assert() .success(); - env.but("branch new top").assert().success(); + env.but("branch new top --above middle").assert().success(); env.but("commit --empty -b top -m 'top base'") .assert() .success(); diff --git a/crates/but/tests/but/command/pull.rs b/crates/but/tests/but/command/pull.rs index dd3fa39a5b8..b008b18519a 100644 --- a/crates/but/tests/but/command/pull.rs +++ b/crates/but/tests/but/command/pull.rs @@ -140,7 +140,7 @@ fn single_branch_pull_prunes_an_integrated_lower_branch() { let env = single_branch_integration_scenario(); env.but("branch new C").assert().success(); commit_file(&env, "C"); - env.but("branch new A").assert().success(); + env.but("branch new A --above C").assert().success(); commit_file(&env, "A"); let old_head = rev_parse(&env, "A"); let old_lower = rev_parse(&env, "C"); @@ -232,7 +232,7 @@ fn single_branch_pull_keeps_an_empty_branch_above_an_integrated_branch() { let env = single_branch_integration_scenario(); env.but("branch new bottom").assert().success(); commit_file(&env, "bottom"); - env.but("branch new top").assert().success(); + env.but("branch new top --above bottom").assert().success(); let old_tip = rev_parse(&env, "top"); merge_into_upstream(&env, "bottom", false); diff --git a/crates/but/tests/but/command/push.rs b/crates/but/tests/but/command/push.rs index 818b5721e5d..12c002093ee 100644 --- a/crates/but/tests/but/command/push.rs +++ b/crates/but/tests/but/command/push.rs @@ -327,6 +327,5 @@ Error: Branch 'A' is merged upstream Hint: Most likely you want `but pull`, which updates the workspace and removes landed work. In rare cases `--allow-merged` can bypass this check - "#]]); } diff --git a/crates/but/tests/fixtures/scenario/single-branch-mode.sh b/crates/but/tests/fixtures/scenario/single-branch-mode.sh new file mode 100644 index 00000000000..6a02dda6810 --- /dev/null +++ b/crates/but/tests/fixtures/scenario/single-branch-mode.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash + +set -eu -o pipefail + +source "${BASH_SOURCE[0]%/*}/shared.sh" + +### General Description + +# A repo in single branch mode whose main and origin/main tips match. + +git-init-frozen + +commit-file init + +git checkout main + commit M + +setup_target_to_match_main