diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b48d6c3787..04c5bf7af16 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### New features * Information about new and resolved conflicts is now printed by every command. +* `jj branch` has gained a new `rename` subcommand that allows changing a branch + name atomically. `jj branch help rename` for details. ### Fixed bugs diff --git a/cli/src/commands/branch.rs b/cli/src/commands/branch.rs index b8699268214..027d78b0de9 100644 --- a/cli/src/commands/branch.rs +++ b/cli/src/commands/branch.rs @@ -34,6 +34,8 @@ pub enum BranchSubcommand { Forget(BranchForgetArgs), #[command(visible_alias("l"))] List(BranchListArgs), + #[command(visible_alias("r"))] + Rename(BranchRenameArgs), #[command(visible_alias("s"))] Set(BranchSetArgs), Track(BranchTrackArgs), @@ -122,6 +124,22 @@ pub struct BranchForgetArgs { pub glob: Vec, } +/// Rename an existing branch pointed to by a specified commit. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchRenameArgs { + /// The old name of the branch. + #[arg(required = true)] + pub old: String, + + /// The old branch's target revision. + #[arg(long, short)] + pub revision: Option, + + /// The new name of the branch. + #[arg(required = true)] + pub new: String, +} + /// Update an existing branch to point to a certain commit. #[derive(clap::Args, Clone, Debug)] pub struct BranchSetArgs { @@ -248,6 +266,7 @@ pub fn cmd_branch( ) -> Result<(), CommandError> { match subcommand { BranchSubcommand::Create(sub_args) => cmd_branch_create(ui, command, sub_args), + BranchSubcommand::Rename(sub_args) => cmd_branch_rename(ui, command, sub_args), BranchSubcommand::Set(sub_args) => cmd_branch_set(ui, command, sub_args), BranchSubcommand::Delete(sub_args) => cmd_branch_delete(ui, command, sub_args), BranchSubcommand::Forget(sub_args) => cmd_branch_forget(ui, command, sub_args), @@ -301,6 +320,42 @@ fn cmd_branch_create( Ok(()) } +fn cmd_branch_rename( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchRenameArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let target_commit = + workspace_command.resolve_single_rev(args.revision.as_deref().unwrap_or("@"), ui)?; + let repo = workspace_command.repo().as_ref(); + let new_branch = &args.new; + let new_target = repo.view().get_local_branch(new_branch); + if !new_target.is_absent() { + return Err(user_error_with_hint( + format!("Branch name conflict: {new_branch}"), + "Target branch name already exists.", + )); + } + + let old_branch = &args.old; + let mut tx = workspace_command.start_transaction(); + tx.mut_repo() + .set_local_branch_target(new_branch, RefTarget::normal(target_commit.id().clone())); + tx.mut_repo() + .set_local_branch_target(old_branch, RefTarget::absent()); + tx.finish( + ui, + format!( + "rename {} to {}; still point to commit {}", + make_branch_term(&[old_branch]), + make_branch_term(&[new_branch]), + target_commit.id().hex() + ), + )?; + Ok(()) +} + fn cmd_branch_set( ui: &mut Ui, command: &CommandHelper,