diff --git a/cli/src/commands/branch/create.rs b/cli/src/commands/branch/create.rs index 9c6cbe7157..6bdaaed3f9 100644 --- a/cli/src/commands/branch/create.rs +++ b/cli/src/commands/branch/create.rs @@ -16,6 +16,7 @@ use clap::builder::NonEmptyStringValueParser; use jj_lib::object_id::ObjectId as _; use jj_lib::op_store::RefTarget; +use super::has_tracked_remote_branches; use crate::cli_util::{CommandHelper, RevisionArg}; use crate::command_error::{user_error_with_hint, CommandError}; use crate::ui::Ui; @@ -42,14 +43,22 @@ pub fn cmd_branch_create( workspace_command.resolve_single_rev(args.revision.as_ref().unwrap_or(&RevisionArg::AT))?; let view = workspace_command.repo().view(); let branch_names = &args.names; - if let Some(branch_name) = branch_names - .iter() - .find(|&name| view.get_local_branch(name).is_present()) - { - return Err(user_error_with_hint( - format!("Branch already exists: {branch_name}"), - "Use `jj branch set` to update it.", - )); + for name in branch_names { + if view.get_local_branch(name).is_present() { + return Err(user_error_with_hint( + format!("Branch already exists: {name}"), + "Use `jj branch set` to update it.", + )); + } + if has_tracked_remote_branches(view, name) { + return Err(user_error_with_hint( + format!("Tracked remote branches exist for deleted branch: {name}"), + format!( + "Use `jj branch set` to recreate the local branch. Run `jj branch untrack \ + 'glob:{name}@*'` to disassociate them." + ), + )); + } } if branch_names.len() > 1 { diff --git a/cli/src/commands/branch/rename.rs b/cli/src/commands/branch/rename.rs index 9dbd9b1c2a..4d2ae459e2 100644 --- a/cli/src/commands/branch/rename.rs +++ b/cli/src/commands/branch/rename.rs @@ -69,6 +69,19 @@ pub fn cmd_branch_rename( `jj git push --all` would also be sufficient." )?; } + if has_tracked_remote_branches(view, new_branch) { + // This isn't an error because branch renaming can't be propagated to + // the remote immediately. "rename old new && rename new old" should be + // allowed even if the original old branch had tracked remotes. + writeln!( + ui.warning_default(), + "Tracked remote branches for branch {new_branch} exist." + )?; + writeln!( + ui.hint_default(), + "Run `jj branch untrack 'glob:{new_branch}@*'` to disassociate them." + )?; + } Ok(()) } diff --git a/cli/src/commands/branch/set.rs b/cli/src/commands/branch/set.rs index 2dfbf4d942..267e98b592 100644 --- a/cli/src/commands/branch/set.rs +++ b/cli/src/commands/branch/set.rs @@ -16,7 +16,7 @@ use clap::builder::NonEmptyStringValueParser; use jj_lib::object_id::ObjectId as _; use jj_lib::op_store::RefTarget; -use super::is_fast_forward; +use super::{has_tracked_remote_branches, is_fast_forward}; use crate::cli_util::{CommandHelper, RevisionArg}; use crate::command_error::{user_error_with_hint, CommandError}; use crate::ui::Ui; @@ -50,7 +50,9 @@ pub fn cmd_branch_set( let mut new_branch_names: Vec<&str> = Vec::new(); for name in branch_names { let old_target = repo.view().get_local_branch(name); - if old_target.is_absent() { + // If a branch is absent locally but is still tracking remote branches, + // we are resurrecting the local branch, not "creating" a new branch. + if old_target.is_absent() && !has_tracked_remote_branches(repo.view(), name) { new_branch_names.push(name); } if !args.allow_backwards && !is_fast_forward(repo, old_target, target_commit.id()) { diff --git a/cli/tests/test_branch_command.rs b/cli/tests/test_branch_command.rs index 7136f0884f..34ee9b068d 100644 --- a/cli/tests/test_branch_command.rs +++ b/cli/tests/test_branch_command.rs @@ -103,6 +103,14 @@ fn test_branch_move() { test_env.jj_cmd_ok(test_env.env_root(), &["git", "init", "repo"]); let repo_path = test_env.env_root().join("repo"); + // Set up remote + let git_repo_path = test_env.env_root().join("git-repo"); + git2::Repository::init_bare(git_repo_path).unwrap(); + test_env.jj_cmd_ok( + &repo_path, + &["git", "remote", "add", "origin", "../git-repo"], + ); + let stderr = test_env.jj_cmd_failure(&repo_path, &["branch", "move", "foo"]); insta::assert_snapshot!(stderr, @r###" Error: No such branch: foo @@ -150,6 +158,40 @@ fn test_branch_move() { &["branch", "move", "--to=@-", "--allow-backwards", "foo"], ); insta::assert_snapshot!(stderr, @""); + + // Delete branch locally, but is still tracking remote + test_env.jj_cmd_ok(&repo_path, &["describe", "@-", "-mcommit"]); + test_env.jj_cmd_ok(&repo_path, &["git", "push", "-r@-"]); + test_env.jj_cmd_ok(&repo_path, &["branch", "delete", "foo"]); + insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" + foo (deleted) + @origin: qpvuntsm 29a62310 (empty) commit + "###); + + // Deleted tracking branch name should still be allocated + let stderr = test_env.jj_cmd_failure(&repo_path, &["branch", "create", "foo"]); + insta::assert_snapshot!(stderr, @r###" + Error: Tracked remote branches exist for deleted branch: foo + Hint: Use `jj branch set` to recreate the local branch. Run `jj branch untrack 'glob:foo@*'` to disassociate them. + "###); + + // Restoring local target shouldn't invalidate tracking state + let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["branch", "set", "foo"]); + insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" + foo: mzvwutvl d5f17aba (empty) (no description set) + @origin (behind by 1 commits): qpvuntsm 29a62310 (empty) commit + "###); + + // Untracked remote branch shouldn't block creation of local branch + test_env.jj_cmd_ok(&repo_path, &["branch", "untrack", "foo@origin"]); + test_env.jj_cmd_ok(&repo_path, &["branch", "delete", "foo"]); + let (_stdout, stderr) = test_env.jj_cmd_ok(&repo_path, &["branch", "create", "foo"]); + insta::assert_snapshot!(stderr, @""); + insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###" + foo: mzvwutvl d5f17aba (empty) (no description set) + foo@origin: qpvuntsm 29a62310 (empty) commit + "###); } #[test] @@ -357,6 +399,12 @@ fn test_branch_rename() { Warning: Tracked remote branches for branch bremote were not renamed. Hint: To rename the branch on the remote, you can `jj git push --branch bremote` first (to delete it on the remote), and then `jj git push --branch bremote2`. `jj git push --all` would also be sufficient. "###); + let (_stdout, stderr) = + test_env.jj_cmd_ok(&repo_path, &["branch", "rename", "bremote2", "bremote"]); + insta::assert_snapshot!(stderr, @r###" + Warning: Tracked remote branches for branch bremote exist. + Hint: Run `jj branch untrack 'glob:bremote@*'` to disassociate them. + "###); } #[test]