branch delete: allow deleting globs of branches

This commit is contained in:
Ilya Grigoriev 2023-06-23 23:27:48 -07:00
parent 97815851f7
commit f6ddd775b9
2 changed files with 112 additions and 9 deletions

View file

@ -49,8 +49,12 @@ pub struct BranchCreateArgs {
#[derive(clap::Args, Clone, Debug)] #[derive(clap::Args, Clone, Debug)]
pub struct BranchDeleteArgs { pub struct BranchDeleteArgs {
/// The branches to delete. /// The branches to delete.
#[arg(required = true)] #[arg(required_unless_present_any(& ["glob"]))]
names: Vec<String>, names: Vec<String>,
/// A glob pattern indicating branches to delete.
#[arg(long)]
pub glob: Vec<String>,
} }
/// List branches and their targets /// List branches and their targets
@ -202,7 +206,11 @@ fn cmd_branch_set(
} }
/// This function may return the same branch more than once /// This function may return the same branch more than once
fn find_globs(view: &View, globs: &[String]) -> Result<Vec<String>, CommandError> { fn find_globs(
view: &View,
globs: &[String],
allow_deleted: bool,
) -> Result<Vec<String>, CommandError> {
let mut matching_branches: Vec<String> = vec![]; let mut matching_branches: Vec<String> = vec![];
let mut failed_globs = vec![]; let mut failed_globs = vec![];
for glob_str in globs { for glob_str in globs {
@ -210,8 +218,15 @@ fn find_globs(view: &View, globs: &[String]) -> Result<Vec<String>, CommandError
let names = view let names = view
.branches() .branches()
.iter() .iter()
.map(|(branch_name, _branch_target)| branch_name) .filter_map(|(branch_name, branch_target)| {
.filter(|branch_name| glob.matches(branch_name)) if glob.matches(branch_name)
&& (allow_deleted || branch_target.local_target.is_some())
{
Some(branch_name)
} else {
None
}
})
.cloned() .cloned()
.collect_vec(); .collect_vec();
if names.is_empty() { if names.is_empty() {
@ -242,6 +257,7 @@ fn cmd_branch_delete(
args: &BranchDeleteArgs, args: &BranchDeleteArgs,
) -> Result<(), CommandError> { ) -> Result<(), CommandError> {
let mut workspace_command = command.workspace_helper(ui)?; let mut workspace_command = command.workspace_helper(ui)?;
let view = workspace_command.repo().view();
for branch_name in &args.names { for branch_name in &args.names {
if workspace_command if workspace_command
.repo() .repo()
@ -252,10 +268,12 @@ fn cmd_branch_delete(
return Err(user_error(format!("No such branch: {branch_name}"))); return Err(user_error(format!("No such branch: {branch_name}")));
} }
} }
let mut tx = let globbed_names = find_globs(view, &args.glob, false)?;
workspace_command.start_transaction(&format!("delete {}", make_branch_term(&args.names))); let names: BTreeSet<String> = args.names.iter().cloned().chain(globbed_names).collect();
for branch_name in &args.names { let branch_term = make_branch_term(names.iter().collect_vec().as_slice());
tx.mut_repo().remove_local_branch(branch_name); let mut tx = workspace_command.start_transaction(&format!("delete {branch_term}"));
for branch_name in names {
tx.mut_repo().remove_local_branch(&branch_name);
} }
tx.finish(ui)?; tx.finish(ui)?;
Ok(()) Ok(())
@ -273,7 +291,7 @@ fn cmd_branch_forget(
return Err(user_error(format!("No such branch: {branch_name}"))); return Err(user_error(format!("No such branch: {branch_name}")));
} }
} }
let globbed_names = find_globs(view, &args.glob)?; let globbed_names = find_globs(view, &args.glob, true)?;
let names: BTreeSet<String> = args.names.iter().cloned().chain(globbed_names).collect(); let names: BTreeSet<String> = args.names.iter().cloned().chain(globbed_names).collect();
let branch_term = make_branch_term(names.iter().collect_vec().as_slice()); let branch_term = make_branch_term(names.iter().collect_vec().as_slice());
let mut tx = workspace_command.start_transaction(&format!("forget {branch_term}")); let mut tx = workspace_command.start_transaction(&format!("forget {branch_term}"));

View file

@ -129,6 +129,91 @@ fn test_branch_forget_glob() {
"###); "###);
} }
#[test]
fn test_branch_delete_glob() {
// Set up a git repo with a branch and a jj repo that has it as a remote.
let test_env = TestEnvironment::default();
test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]);
let repo_path = test_env.env_root().join("repo");
let git_repo_path = test_env.env_root().join("git-repo");
let git_repo = git2::Repository::init_bare(git_repo_path).unwrap();
let mut tree_builder = git_repo.treebuilder(None).unwrap();
let file_oid = git_repo.blob(b"content").unwrap();
tree_builder
.insert("file", file_oid, git2::FileMode::Blob.into())
.unwrap();
test_env.jj_cmd_success(
&repo_path,
&["git", "remote", "add", "origin", "../git-repo"],
);
test_env.jj_cmd_success(&repo_path, &["describe", "-m=commit"]);
test_env.jj_cmd_success(&repo_path, &["branch", "set", "foo-1"]);
test_env.jj_cmd_success(&repo_path, &["branch", "set", "bar-2"]);
test_env.jj_cmd_success(&repo_path, &["branch", "set", "foo-3"]);
test_env.jj_cmd_success(&repo_path, &["branch", "set", "foo-4"]);
// Push to create remote-tracking branches
test_env.jj_cmd_success(&repo_path, &["git", "push", "--all"]);
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
@ bar-2 foo-1 foo-3 foo-4 6fbf398c2d59
~
"###);
let stdout = test_env.jj_cmd_success(&repo_path, &["branch", "delete", "--glob", "foo-[1-3]"]);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
@ bar-2 foo-1@origin foo-3@origin foo-4 6fbf398c2d59
~
"###);
// We get an error if none of the globs match live branches. Unlike `jj branch
// forget`, it's not allowed to delete already deleted branches.
let stderr = test_env.jj_cmd_failure(&repo_path, &["branch", "delete", "--glob=foo-[1-3]"]);
insta::assert_snapshot!(stderr, @r###"
Error: The provided glob 'foo-[1-3]' did not match any branches
"###);
// Deleting a branch via both explicit name and glob pattern, or with
// multiple glob patterns, shouldn't produce an error.
let stdout = test_env.jj_cmd_success(
&repo_path,
&[
"branch", "delete", "foo-4", "--glob", "foo-*", "--glob", "foo-*",
],
);
insta::assert_snapshot!(stdout, @"");
insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###"
@ bar-2 foo-1@origin foo-3@origin foo-4@origin 6fbf398c2d59
~
"###);
// The deleted branches are still there
insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###"
bar-2: 6fbf398c2d59 commit
foo-1 (deleted)
@origin: 6fbf398c2d59 commit
(this branch will be *deleted permanently* on the remote on the
next `jj git push`. Use `jj branch forget` to prevent this)
foo-3 (deleted)
@origin: 6fbf398c2d59 commit
(this branch will be *deleted permanently* on the remote on the
next `jj git push`. Use `jj branch forget` to prevent this)
foo-4 (deleted)
@origin: 6fbf398c2d59 commit
(this branch will be *deleted permanently* on the remote on the
next `jj git push`. Use `jj branch forget` to prevent this)
"###);
// Malformed glob
let stderr = test_env.jj_cmd_failure(&repo_path, &["branch", "delete", "--glob", "foo-[1-3"]);
insta::assert_snapshot!(stderr, @r###"
Error: Failed to compile glob: Pattern syntax error near position 4: invalid range pattern
"###);
}
#[test] #[test]
fn test_branch_forget_export() { fn test_branch_forget_export() {
let test_env = TestEnvironment::default(); let test_env = TestEnvironment::default();