diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b48d6c378..04c5bf7af1 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 35a5a91448..3f592f49de 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,19 @@ pub struct BranchForgetArgs { pub glob: Vec, } +/// Rename `old` branch name to `new` branch name. +/// +/// The new branch name points at the same commit as the old +/// branch name. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchRenameArgs { + /// The old name of the branch. + pub old: String, + + /// The new name of the branch. + pub new: String, +} + /// Update an existing branch to point to a certain commit. #[derive(clap::Args, Clone, Debug)] pub struct BranchSetArgs { @@ -248,6 +263,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 +317,44 @@ 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 repo = workspace_command.repo().as_ref(); + let view = repo.view(); + let old_branch = &args.old; + // call clone because .set_local_branch_target takes ownership + // of the target_commit, so we need to create a copy that is no + // longer owned by the workspace. + let target_commit = view.get_local_branch(old_branch).clone(); + if target_commit.is_absent() { + return Err(user_error(format!("No such branch: {old_branch}"))); + } + + let new_branch = &args.new; + if view.get_local_branch(new_branch).is_present() { + return Err(user_error(format!("Branch already exists: {new_branch}"))); + } + + let mut tx = workspace_command.start_transaction(); + tx.mut_repo() + .set_local_branch_target(new_branch, target_commit); + tx.mut_repo() + .set_local_branch_target(old_branch, RefTarget::absent()); + tx.finish( + ui, + format!( + "rename {} to {}", + make_branch_term(&[old_branch]), + make_branch_term(&[new_branch]), + ), + )?; + Ok(()) +} + fn cmd_branch_set( ui: &mut Ui, command: &CommandHelper, diff --git a/cli/tests/test_branch_command.rs b/cli/tests/test_branch_command.rs index 7a0881673b..6220f8661f 100644 --- a/cli/tests/test_branch_command.rs +++ b/cli/tests/test_branch_command.rs @@ -189,6 +189,31 @@ fn test_branch_move_conflicting() { "###); } +#[test] +fn test_branch_rename() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + + let stderr = test_env.jj_cmd_failure(&repo_path, &["branch", "rename", "foo", "bar"]); + insta::assert_snapshot!(stderr, @r###" + Error: No such branch: foo + "###); + + // we will conflict with this in a later test. + test_env.jj_cmd_ok(&repo_path, &["branch", "create", "conflictfoo"]); + + test_env.jj_cmd_ok(&repo_path, &["new"]); + test_env.jj_cmd_ok(&repo_path, &["branch", "create", "foo"]); + let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["branch", "rename", "foo", "bar"]); + insta::assert_snapshot!(stderr, @""); + + let stderr = test_env.jj_cmd_failure(&repo_path, &["branch", "rename", "bar", "conflictfoo"]); + insta::assert_snapshot!(stderr, @r###" + Error: Branch already exists: conflictfoo + "###); +} + #[test] fn test_branch_forget_glob() { let test_env = TestEnvironment::default();