but: switch command#14278
Conversation
9371b86 to
96871ac
Compare
|
Good to go |
96871ac to
8ca6d40
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new but switch subcommand to the but CLI, enabling users to switch to an existing local branch (by name, full ref, or workspace CLI ID), switch back to the GitButler workspace ref, or create-and-switch to a new branch. This fits into the CLI’s branching workflow alongside existing branch/workspace operations, and wires the command into help text and metrics.
Changes:
- Introduces
but switchcommand implementation and clap argument wiring (--workspace,--new, optional target). - Extends
but-apibranch checkout helpers with a workspace checkout entrypoint (ref-based checkout refactor). - Adds integration tests covering the main switch scenarios and several input validation cases.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/but/tests/but/command/switch.rs | New integration tests for but switch behavior (existing branch, workspace, new branch, invalid inputs). |
| crates/but/tests/but/command/mod.rs | Registers the new switch test module. |
| crates/but/src/utils/metrics.rs | Maps Subcommands::Switch to the Switch metrics command name. |
| crates/but/src/lib.rs | Dispatches the Switch subcommand to command::switch::handle. |
| crates/but/src/command/switch.rs | Implements but switch behavior (workspace checkout, new branch checkout, existing local branch resolution). |
| crates/but/src/command/mod.rs | Exposes the new switch command module. |
| crates/but/src/command/help.rs | Adds switch into grouped help output. |
| crates/but/src/args/mod.rs | Defines the Switch clap subcommand, flags, argument grouping, and help text. |
| crates/but/src/args/metrics.rs | Adds CommandName::Switch for telemetry identifiers. |
| crates/but/src/args/atoms/branch_arg.rs | Gates try_resolve_stack behind legacy since atoms are now used outside legacy. |
| crates/but-api/src/branch.rs | Adds workspace_checkout_with_perm and factors checkout into a shared ref-based helper. |
8ca6d40 to
7baca4e
Compare
7baca4e to
be4fea3
Compare
| /// Branch name, full local branch ref, or workspace CLI branch ID | ||
| target: Option<CliIdArg>, |
| /// Create a new branch at the target and switch to it | ||
| #[clap(long = "new", short = 'n')] | ||
| new: bool, |
| .stderr_eq(str![[r#" | ||
| Error: Invalid branch. '94' is a commit | ||
|
|
||
| "#]]); |
be4fea3 to
0b84953
Compare
0b84953 to
95f8f4c
Compare
| if matches.len() > 1 { | ||
| return Err(anyhow::anyhow!( | ||
| "Branch '{}' is ambiguous. Try using more characters to disambiguate.", | ||
| target.0 | ||
| ) | ||
| .into()); | ||
| } |
| if target.0.starts_with("refs/remotes/") || looks_like_remote_branch(&repo, &target.0) { | ||
| return Err(bad_input(format!( | ||
| "Can only switch to local branches, got '{}'", | ||
| target.0 | ||
| )) | ||
| .into()); | ||
| } | ||
|
|
||
| if let Ok(short_name) = Category::LocalBranch.to_full_name(target.0.as_str()) | ||
| && repo.try_find_reference(short_name.as_ref())?.is_some() | ||
| { | ||
| return Ok(short_name); | ||
| } |
|
Notes for reviewer: This command should be hidden for now, even if we add it. Since switching away from the workspace and then running other actions will trigger an error until something like: #14276 lands |
There was a problem hiding this comment.
Looks fine to add as a hidden command, but argument parsing should be consolidated with CLI atoms.
Note that hidden commands are added to auto completion, and users are likely to find switch very quickly. If we want it to be less discoverable, consider prepending an unusual character (e.g. _) to the command name until we're ready to release it to the masses.
| fn resolve_existing_local_branch( | ||
| ctx: &but_ctx::Context, | ||
| perm: &but_core::sync::RepoShared, | ||
| target: &CliIdArg, | ||
| ) -> CliResult<FullName> { | ||
| let repo = ctx.repo.get()?; | ||
|
|
||
| if target.0.starts_with("refs/heads/") { | ||
| let full_name = FullName::try_from(target.0.as_str()) | ||
| .map_err(|_| bad_input(format!("Invalid branch ref '{}'", target.0)))?; | ||
| ensure_existing_local_branch(&repo, &full_name)?; | ||
| return Ok(full_name); | ||
| } | ||
|
|
||
| if target.0.starts_with("refs/remotes/") || looks_like_remote_branch(&repo, &target.0) { | ||
| return Err(bad_input(format!( | ||
| "Can only switch to local branches, got '{}'", | ||
| target.0 | ||
| )) | ||
| .into()); | ||
| } | ||
|
|
||
| if let Ok(short_name) = Category::LocalBranch.to_full_name(target.0.as_str()) | ||
| && repo.try_find_reference(short_name.as_ref())?.is_some() | ||
| { | ||
| return Ok(short_name); | ||
| } | ||
|
|
||
| let id_map = IdMap::new_from_context(ctx, None, perm)?; | ||
| let matches = id_map.parse_using_context(&target.0, ctx)?; | ||
| if matches.is_empty() { | ||
| return Err(bad_input(format!("Could not find branch: '{}'", target.0)).into()); | ||
| } | ||
| if matches.len() > 1 { | ||
| return Err(anyhow::anyhow!( | ||
| "Branch '{}' is ambiguous. Try using more characters to disambiguate.", | ||
| target.0 | ||
| ) | ||
| .into()); | ||
| } | ||
|
|
||
| match &matches[0] { | ||
| CliId::Branch { name, .. } => { | ||
| let branch = Category::LocalBranch.to_full_name(name.as_str())?; | ||
| ensure_existing_local_branch(&repo, &branch)?; | ||
| Ok(branch) | ||
| } | ||
| other => { | ||
| let kind = match other { | ||
| CliId::Branch { .. } => unreachable!("handled above"), | ||
| CliId::Commit { .. } => "a commit", | ||
| CliId::Uncommitted(..) => "an uncommitted file", | ||
| CliId::PathPrefix { .. } => "a path", | ||
| CliId::CommittedFile { .. } => "a committed file", | ||
| CliId::Unassigned { .. } => "unassigned changes", | ||
| CliId::Stack { .. } => "a stack", | ||
| }; | ||
| Err(bad_input(format!("Invalid branch. '{}' is {kind}", target.0)).into()) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
We try to contain this kind of resolution to CLI atoms, this duplicates lot of that code. I think BranchArg::resolve_branch() should do the trick, see if you can use that instead.
I don't know if we have any existing code path from CliIdArg all the way to an unapplied branch, but you should be able to cobble it together from CLI atoms, perhaps with some minimal extension.
Thanks! I'll address the changes and hide the command. |
Add a command to switch to an new or existing ref. And back to the workspace.
95f8f4c to
142012e
Compare
|
Addressed the feedback |
Usage
Switch to a local branch, workspace branch ID, or the GitButler workspace.
Examples
Switch to a branch:
but switch my-featureSwitch back to the GitButler workspace:
but switch --workspaceCreate a new branch at the target and switch to it (canned name if no name is specified):
but switch --new my-featurebut switch --new