diff --git a/CHANGELOG.md b/CHANGELOG.md index 625a01622..fb11afbf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Sparse checkouts are now supported. In fact, all working copies are now "sparse", only to different degrees. Use the `jj sparse` command to manage - the paths included in the sparse checkout. + the paths included in the sparse checkout. * The `$JJ_CONFIG` environment variable can now point to a directory. If it does, all files in the directory will be read, in alphabetical order. @@ -73,6 +73,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 program = "kdiff3" edit-args = ["--merge", "--cs", "CreateBakFiles=0"] +* `jj branch` can accept any number of branches to update, rather than just one. + ### Fixed bugs * When rebasing a conflict where one side modified a file and the other side @@ -83,7 +85,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Updating the working copy to a commit where a file's executable bit changed but the contents was the same used to lead to a crash. That has now been - fixed. + fixed. * If one side of a merge modified a directory and the other side deleted it, it used to be considered a conflict. The same was true if both sides added a diff --git a/src/commands.rs b/src/commands.rs index bab70a336..2d83b5dcb 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -1604,7 +1604,8 @@ struct BranchArgs { #[clap(long, group = "action")] forget: bool, - name: String, + /// The branches to update. + names: Vec, } /// List branches and their targets @@ -3966,53 +3967,76 @@ fn is_fast_forward(repo: RepoRef, branch_name: &str, new_target_id: &CommitId) - fn cmd_branch(ui: &mut Ui, command: &CommandHelper, args: &BranchArgs) -> Result<(), CommandError> { let mut workspace_command = command.workspace_helper(ui)?; - let branch_name = &args.name; + let branch_names: Vec<&str> = if args.delete || args.forget { + let view = workspace_command.repo().view(); + args.names + .iter() + .map(|branch_name| match view.get_local_branch(branch_name) { + Some(_) => Ok(branch_name.as_str()), + None => Err(CommandError::UserError(format!( + "No such branch: {}", + branch_name + ))), + }) + .try_collect()? + } else { + args.names.iter().map(|name| name.as_str()).collect() + }; + + if branch_names.is_empty() { + ui.write_warn("warning: No branches provided.\n")?; + } + let branch_term = if branch_names.len() == 1 { + "branch" + } else { + "branches" + }; + let branch_term = format!("{branch_term} {}", branch_names.join(", ")); + if args.delete { - if workspace_command - .repo() - .view() - .get_local_branch(branch_name) - .is_none() - { - return Err(CommandError::UserError("No such branch".to_string())); + let mut tx = workspace_command.start_transaction(&format!("delete {branch_term}")); + for branch_name in branch_names { + tx.mut_repo().remove_local_branch(branch_name); } - let mut tx = workspace_command.start_transaction(&format!("delete branch {}", branch_name)); - tx.mut_repo().remove_local_branch(branch_name); workspace_command.finish_transaction(ui, tx)?; } else if args.forget { - if workspace_command - .repo() - .view() - .get_local_branch(branch_name) - .is_none() - { - return Err(CommandError::UserError("No such branch".to_string())); + let mut tx = workspace_command.start_transaction(&format!("forget {branch_term}")); + for branch_name in branch_names { + tx.mut_repo().remove_branch(branch_name); } - let mut tx = workspace_command.start_transaction(&format!("forget branch {}", branch_name)); - tx.mut_repo().remove_branch(branch_name); workspace_command.finish_transaction(ui, tx)?; } else { + if branch_names.len() > 1 { + ui.write_warn(format!( + "warning: Updating multiple branches ({}).\n", + branch_names.len() + ))?; + } + let target_commit = workspace_command.resolve_single_rev(ui, &args.revision)?; if !args.allow_backwards - && !is_fast_forward( - workspace_command.repo().as_repo_ref(), - branch_name, - target_commit.id(), - ) + && !branch_names.iter().all(|branch_name| { + is_fast_forward( + workspace_command.repo().as_repo_ref(), + branch_name, + target_commit.id(), + ) + }) { return Err(CommandError::UserError( "Use --allow-backwards to allow moving a branch backwards or sideways".to_string(), )); } let mut tx = workspace_command.start_transaction(&format!( - "point branch {} to commit {}", - branch_name, + "point {branch_term} to commit {}", target_commit.id().hex() )); - tx.mut_repo().set_local_branch( - branch_name.to_string(), - RefTarget::Normal(target_commit.id().clone()), - ); + for branch_name in branch_names { + tx.mut_repo().set_local_branch( + branch_name.to_string(), + RefTarget::Normal(target_commit.id().clone()), + ); + } workspace_command.finish_transaction(ui, tx)?; } diff --git a/src/formatter.rs b/src/formatter.rs index 9a0aeb08e..1b644ea39 100644 --- a/src/formatter.rs +++ b/src/formatter.rs @@ -80,7 +80,8 @@ pub struct ColorFormatter<'output> { fn config_colors(user_settings: &UserSettings) -> HashMap { let mut result = HashMap::new(); result.insert(String::from("error"), String::from("red")); - result.insert(String::from("hint"), String::from("yellow")); + result.insert(String::from("warning"), String::from("yellow")); + result.insert(String::from("hint"), String::from("blue")); result.insert(String::from("commit_id"), String::from("blue")); result.insert(String::from("commit_id open"), String::from("green")); diff --git a/src/ui.rs b/src/ui.rs index bdebc6a0c..bdd0d9730 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -128,6 +128,14 @@ impl<'stdout> Ui<'stdout> { Ok(()) } + pub fn write_warn(&mut self, text: impl AsRef) -> io::Result<()> { + let mut formatter = self.stderr_formatter(); + formatter.add_label(String::from("warning"))?; + formatter.write_str(text.as_ref())?; + formatter.remove_label()?; + Ok(()) + } + pub fn write_error(&mut self, text: &str) -> io::Result<()> { let mut formatter = self.stderr_formatter(); formatter.add_label(String::from("error"))?; diff --git a/tests/test_branch_command.rs b/tests/test_branch_command.rs index da986d8e4..a82fdca3b 100644 --- a/tests/test_branch_command.rs +++ b/tests/test_branch_command.rs @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::common::TestEnvironment; +use std::path::Path; + +use crate::common::{get_stderr_string, get_stdout_string, TestEnvironment}; pub mod common; @@ -28,3 +30,49 @@ fn test_branch_mutually_exclusive_actions() { &["branch", "--delete", "--allow-backwards", "foo"], ); } + +#[test] +fn test_branch_multiple_names() { + 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 assert = test_env + .jj_cmd(&repo_path, &["branch", "foo", "bar"]) + .assert() + .success(); + insta::assert_snapshot!(get_stdout_string(&assert), @""); + insta::assert_snapshot!(get_stderr_string(&assert), @"warning: Updating multiple branches (2). +"); + + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" + @ bar foo 230dd059e1b0 + o 000000000000 + "###); + + let stdout = test_env.jj_cmd_success(&repo_path, &["branch", "--delete", "foo", "bar"]); + insta::assert_snapshot!(stdout, @""); + insta::assert_snapshot!(get_log_output(&test_env, &repo_path), @r###" + @ 230dd059e1b0 + o 000000000000 + "###); +} + +#[test] +fn test_branch_hint_no_branches() { + 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 assert = test_env + .jj_cmd(&repo_path, &["branch", "--delete"]) + .assert() + .success(); + let stderr = get_stderr_string(&assert); + insta::assert_snapshot!(stderr, @"warning: No branches provided. +"); +} + +fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String { + test_env.jj_cmd_success(cwd, &["log", "-T", r#"branches " " commit_id.short()"#]) +}