From f6ddd775b96f174ad27c5c000d519ba928bf6777 Mon Sep 17 00:00:00 2001 From: Ilya Grigoriev Date: Fri, 23 Jun 2023 23:27:48 -0700 Subject: [PATCH] `branch delete`: allow deleting globs of branches --- src/commands/branch.rs | 36 +++++++++++---- tests/test_branch_command.rs | 85 ++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 9 deletions(-) diff --git a/src/commands/branch.rs b/src/commands/branch.rs index b0b22219f..69f84a55e 100644 --- a/src/commands/branch.rs +++ b/src/commands/branch.rs @@ -49,8 +49,12 @@ pub struct BranchCreateArgs { #[derive(clap::Args, Clone, Debug)] pub struct BranchDeleteArgs { /// The branches to delete. - #[arg(required = true)] + #[arg(required_unless_present_any(& ["glob"]))] names: Vec, + + /// A glob pattern indicating branches to delete. + #[arg(long)] + pub glob: Vec, } /// List branches and their targets @@ -202,7 +206,11 @@ fn cmd_branch_set( } /// This function may return the same branch more than once -fn find_globs(view: &View, globs: &[String]) -> Result, CommandError> { +fn find_globs( + view: &View, + globs: &[String], + allow_deleted: bool, +) -> Result, CommandError> { let mut matching_branches: Vec = vec![]; let mut failed_globs = vec![]; for glob_str in globs { @@ -210,8 +218,15 @@ fn find_globs(view: &View, globs: &[String]) -> Result, CommandError let names = view .branches() .iter() - .map(|(branch_name, _branch_target)| branch_name) - .filter(|branch_name| glob.matches(branch_name)) + .filter_map(|(branch_name, branch_target)| { + if glob.matches(branch_name) + && (allow_deleted || branch_target.local_target.is_some()) + { + Some(branch_name) + } else { + None + } + }) .cloned() .collect_vec(); if names.is_empty() { @@ -242,6 +257,7 @@ fn cmd_branch_delete( args: &BranchDeleteArgs, ) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; + let view = workspace_command.repo().view(); for branch_name in &args.names { if workspace_command .repo() @@ -252,10 +268,12 @@ fn cmd_branch_delete( return Err(user_error(format!("No such branch: {branch_name}"))); } } - let mut tx = - workspace_command.start_transaction(&format!("delete {}", make_branch_term(&args.names))); - for branch_name in &args.names { - tx.mut_repo().remove_local_branch(branch_name); + let globbed_names = find_globs(view, &args.glob, false)?; + let names: BTreeSet = args.names.iter().cloned().chain(globbed_names).collect(); + let branch_term = make_branch_term(names.iter().collect_vec().as_slice()); + 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)?; Ok(()) @@ -273,7 +291,7 @@ fn cmd_branch_forget( 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 = args.names.iter().cloned().chain(globbed_names).collect(); let branch_term = make_branch_term(names.iter().collect_vec().as_slice()); let mut tx = workspace_command.start_transaction(&format!("forget {branch_term}")); diff --git a/tests/test_branch_command.rs b/tests/test_branch_command.rs index f27a33bcb..005a3695d 100644 --- a/tests/test_branch_command.rs +++ b/tests/test_branch_command.rs @@ -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] fn test_branch_forget_export() { let test_env = TestEnvironment::default();