diff --git a/cli/src/commands/branch.rs b/cli/src/commands/branch.rs deleted file mode 100644 index a9cd70e4c..000000000 --- a/cli/src/commands/branch.rs +++ /dev/null @@ -1,906 +0,0 @@ -// Copyright 2020-2023 The Jujutsu Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use std::collections::{HashMap, HashSet}; -use std::fmt; -use std::io::Write as _; - -use clap::builder::NonEmptyStringValueParser; -use itertools::Itertools; -use jj_lib::backend::CommitId; -use jj_lib::git; -use jj_lib::object_id::ObjectId; -use jj_lib::op_store::{RefTarget, RemoteRef}; -use jj_lib::repo::Repo; -use jj_lib::revset::RevsetExpression; -use jj_lib::str_util::StringPattern; -use jj_lib::view::View; - -use crate::cli_util::{CommandHelper, RemoteBranchName, RemoteBranchNamePattern, RevisionArg}; -use crate::command_error::{user_error, user_error_with_hint, CommandError}; -use crate::commit_templater::{CommitTemplateLanguage, RefName}; -use crate::ui::Ui; - -/// Manage branches. -/// -/// For information about branches, see -/// https://github.com/martinvonz/jj/blob/main/docs/branches.md. -#[derive(clap::Subcommand, Clone, Debug)] -pub enum BranchCommand { - #[command(visible_alias("c"))] - Create(BranchCreateArgs), - #[command(visible_alias("d"))] - Delete(BranchDeleteArgs), - #[command(visible_alias("f"))] - Forget(BranchForgetArgs), - #[command(visible_alias("l"))] - List(BranchListArgs), - #[command(visible_alias("m"))] - Move(BranchMoveArgs), - #[command(visible_alias("r"))] - Rename(BranchRenameArgs), - #[command(visible_alias("s"))] - Set(BranchSetArgs), - #[command(visible_alias("t"))] - Track(BranchTrackArgs), - Untrack(BranchUntrackArgs), -} - -/// Create a new branch. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchCreateArgs { - /// The branch's target revision. - #[arg(long, short)] - revision: Option, - - /// The branches to create. - #[arg(required = true, value_parser=NonEmptyStringValueParser::new())] - names: Vec, -} - -/// Delete an existing branch and propagate the deletion to remotes on the -/// next push. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchDeleteArgs { - /// The branches to delete - /// - /// By default, the specified name matches exactly. Use `glob:` prefix to - /// select branches by wildcard pattern. For details, see - /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. - #[arg(required_unless_present_any(&["glob"]), value_parser = StringPattern::parse)] - pub names: Vec, - - /// Deprecated. Please prefix the pattern with `glob:` instead. - #[arg(long, hide = true, value_parser = StringPattern::glob)] - pub glob: Vec, -} - -/// List branches and their targets -/// -/// By default, a tracking remote branch will be included only if its target is -/// different from the local target. A non-tracking remote branch won't be -/// listed. For a conflicted branch (both local and remote), old target -/// revisions are preceded by a "-" and new target revisions are preceded by a -/// "+". -/// -/// For information about branches, see -/// https://github.com/martinvonz/jj/blob/main/docs/branches.md. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchListArgs { - /// Show all tracking and non-tracking remote branches including the ones - /// whose targets are synchronized with the local branches. - #[arg(long, short, alias = "all")] - all_remotes: bool, - - /// Show remote tracked branches only. Omits local Git-tracking branches by - /// default. - #[arg(long, short, conflicts_with_all = ["all_remotes"])] - tracked: bool, - - /// Show conflicted branches only. - #[arg(long, short, conflicts_with_all = ["all_remotes"])] - conflicted: bool, - - /// Show branches whose local name matches - /// - /// By default, the specified name matches exactly. Use `glob:` prefix to - /// select branches by wildcard pattern. For details, see - /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. - #[arg(value_parser = StringPattern::parse)] - pub names: Vec, - - /// Show branches whose local targets are in the given revisions. - /// - /// Note that `-r deleted_branch` will not work since `deleted_branch` - /// wouldn't have a local target. - #[arg(long, short)] - revisions: Vec, - - /// Render each branch using the given template - /// - /// All 0-argument methods of the `RefName` type are available as keywords. - /// - /// For the syntax, see https://github.com/martinvonz/jj/blob/main/docs/templates.md - #[arg(long, short = 'T')] - template: Option, -} - -/// Forget everything about a branch, including its local and remote -/// targets. -/// -/// A forgotten branch will not impact remotes on future pushes. It will be -/// recreated on future pulls if it still exists in the remote. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchForgetArgs { - /// The branches to forget - /// - /// By default, the specified name matches exactly. Use `glob:` prefix to - /// select branches by wildcard pattern. For details, see - /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. - #[arg(required_unless_present_any(&["glob"]), value_parser = StringPattern::parse)] - pub names: Vec, - - /// Deprecated. Please prefix the pattern with `glob:` instead. - #[arg(long, hide = true, value_parser = StringPattern::glob)] - 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 { - /// The branch's target revision. - #[arg(long, short)] - pub revision: Option, - - /// Allow moving the branch backwards or sideways. - #[arg(long, short = 'B')] - pub allow_backwards: bool, - - /// The branches to update. - #[arg(required = true)] - pub names: Vec, -} - -/// Move existing branches to target revision -/// -/// If branch names are given, the specified branches will be updated to point -/// to the target revision. -/// -/// If `--from` options are given, branches currently pointing to the specified -/// revisions will be updated. The branches can also be filtered by names. -/// -/// Example: pull up the nearest branches to the working-copy parent -/// -/// $ jj branch move --from 'heads(::@- & branches())' --to @- -#[derive(clap::Args, Clone, Debug)] -#[command(group(clap::ArgGroup::new("source").multiple(true).required(true)))] -pub struct BranchMoveArgs { - /// Move branches from the given revisions - #[arg(long, group = "source", value_name = "REVISIONS")] - from: Vec, - - /// Move branches to this revision - #[arg(long, default_value = "@", value_name = "REVISION")] - to: RevisionArg, - - /// Allow moving branches backwards or sideways - #[arg(long, short = 'B')] - allow_backwards: bool, - - /// Move branches matching the given name patterns - /// - /// By default, the specified name matches exactly. Use `glob:` prefix to - /// select branches by wildcard pattern. For details, see - /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. - #[arg(group = "source", value_parser = StringPattern::parse)] - names: Vec, -} - -/// Start tracking given remote branches -/// -/// A tracking remote branch will be imported as a local branch of the same -/// name. Changes to it will propagate to the existing local branch on future -/// pulls. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchTrackArgs { - /// Remote branches to track - /// - /// By default, the specified name matches exactly. Use `glob:` prefix to - /// select branches by wildcard pattern. For details, see - /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. - /// - /// Examples: branch@remote, glob:main@*, glob:jjfan-*@upstream - #[arg(required = true, value_name = "BRANCH@REMOTE")] - pub names: Vec, -} - -/// Stop tracking given remote branches -/// -/// A non-tracking remote branch is just a pointer to the last-fetched remote -/// branch. It won't be imported as a local branch on future pulls. -#[derive(clap::Args, Clone, Debug)] -pub struct BranchUntrackArgs { - /// Remote branches to untrack - /// - /// By default, the specified name matches exactly. Use `glob:` prefix to - /// select branches by wildcard pattern. For details, see - /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. - /// - /// Examples: branch@remote, glob:main@*, glob:jjfan-*@upstream - #[arg(required = true, value_name = "BRANCH@REMOTE")] - pub names: Vec, -} - -fn make_branch_term(branch_names: &[impl fmt::Display]) -> String { - match branch_names { - [branch_name] => format!("branch {}", branch_name), - branch_names => format!("branches {}", branch_names.iter().join(", ")), - } -} - -pub fn cmd_branch( - ui: &mut Ui, - command: &CommandHelper, - subcommand: &BranchCommand, -) -> Result<(), CommandError> { - match subcommand { - BranchCommand::Create(sub_args) => cmd_branch_create(ui, command, sub_args), - BranchCommand::Rename(sub_args) => cmd_branch_rename(ui, command, sub_args), - BranchCommand::Set(sub_args) => cmd_branch_set(ui, command, sub_args), - BranchCommand::Move(sub_args) => cmd_branch_move(ui, command, sub_args), - BranchCommand::Delete(sub_args) => cmd_branch_delete(ui, command, sub_args), - BranchCommand::Forget(sub_args) => cmd_branch_forget(ui, command, sub_args), - BranchCommand::Track(sub_args) => cmd_branch_track(ui, command, sub_args), - BranchCommand::Untrack(sub_args) => cmd_branch_untrack(ui, command, sub_args), - BranchCommand::List(sub_args) => cmd_branch_list(ui, command, sub_args), - } -} - -fn cmd_branch_create( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchCreateArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let target_commit = - 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.", - )); - } - - if branch_names.len() > 1 { - writeln!( - ui.warning_default(), - "Creating multiple branches: {}", - branch_names.join(", "), - )?; - } - - let mut tx = workspace_command.start_transaction(); - for branch_name in branch_names { - tx.mut_repo() - .set_local_branch_target(branch_name, RefTarget::normal(target_commit.id().clone())); - } - tx.finish( - ui, - format!( - "create {} pointing to commit {}", - make_branch_term(branch_names), - target_commit.id().hex() - ), - )?; - Ok(()) -} - -fn cmd_branch_rename( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchRenameArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let view = workspace_command.repo().view(); - let old_branch = &args.old; - let ref_target = view.get_local_branch(old_branch).clone(); - if ref_target.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, ref_target); - 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]), - ), - )?; - - let view = workspace_command.repo().view(); - if view - .remote_branches_matching( - &StringPattern::exact(old_branch), - &StringPattern::everything(), - ) - .any(|(_, remote_ref)| remote_ref.is_tracking()) - { - writeln!( - ui.warning_default(), - "Branch {old_branch} has tracking remote branches which were not renamed." - )?; - writeln!( - ui.hint_default(), - "to rename the branch on the remote, you can `jj git push --branch {old_branch}` \ - first (to delete it on the remote), and then `jj git push --branch {new_branch}`. \ - `jj git push --all` would also be sufficient." - )?; - } - - Ok(()) -} - -fn cmd_branch_set( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchSetArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let target_commit = - workspace_command.resolve_single_rev(args.revision.as_ref().unwrap_or(&RevisionArg::AT))?; - let repo = workspace_command.repo().as_ref(); - let branch_names = &args.names; - for name in branch_names { - let old_target = repo.view().get_local_branch(name); - if old_target.is_absent() { - return Err(user_error_with_hint( - format!("No such branch: {name}"), - "Use `jj branch create` to create it.", - )); - } - if !args.allow_backwards && !is_fast_forward(repo, old_target, target_commit.id()) { - return Err(user_error_with_hint( - format!("Refusing to move branch backwards or sideways: {name}"), - "Use --allow-backwards to allow it.", - )); - } - } - - if branch_names.len() > 1 { - writeln!( - ui.warning_default(), - "Updating multiple branches: {}", - branch_names.join(", "), - )?; - } - - let mut tx = workspace_command.start_transaction(); - for branch_name in branch_names { - tx.mut_repo() - .set_local_branch_target(branch_name, RefTarget::normal(target_commit.id().clone())); - } - tx.finish( - ui, - format!( - "point {} to commit {}", - make_branch_term(branch_names), - target_commit.id().hex() - ), - )?; - Ok(()) -} - -fn cmd_branch_move( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchMoveArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let repo = workspace_command.repo().as_ref(); - let view = repo.view(); - - let branch_names = { - let is_source_commit = if !args.from.is_empty() { - workspace_command - .parse_union_revsets(&args.from)? - .evaluate()? - .containing_fn() - } else { - Box::new(|_: &CommitId| true) - }; - if !args.names.is_empty() { - find_branches_with(&args.names, |pattern| { - view.local_branches_matching(pattern) - .filter(|(_, target)| target.added_ids().any(&is_source_commit)) - .map(|(name, _)| name.to_owned()) - })? - } else { - view.local_branches() - .filter(|(_, target)| target.added_ids().any(&is_source_commit)) - .map(|(name, _)| name.to_owned()) - .collect() - } - }; - let target_commit = workspace_command.resolve_single_rev(&args.to)?; - - if branch_names.is_empty() { - writeln!(ui.status(), "No branches to update.")?; - return Ok(()); - } - if branch_names.len() > 1 { - writeln!( - ui.warning_default(), - "Updating multiple branches: {}", - branch_names.join(", "), - )?; - if args.names.is_empty() { - writeln!(ui.hint_default(), "Specify branch by name to update one.")?; - } - } - - if !args.allow_backwards { - if let Some(name) = branch_names - .iter() - .find(|name| !is_fast_forward(repo, view.get_local_branch(name), target_commit.id())) - { - return Err(user_error_with_hint( - format!("Refusing to move branch backwards or sideways: {name}"), - "Use --allow-backwards to allow it.", - )); - } - } - - let mut tx = workspace_command.start_transaction(); - for name in &branch_names { - tx.mut_repo() - .set_local_branch_target(name, RefTarget::normal(target_commit.id().clone())); - } - tx.finish( - ui, - format!( - "point {} to commit {}", - make_branch_term(&branch_names), - target_commit.id().hex() - ), - )?; - - Ok(()) -} - -fn find_local_branches( - view: &View, - name_patterns: &[StringPattern], -) -> Result, CommandError> { - find_branches_with(name_patterns, |pattern| { - view.local_branches_matching(pattern) - .map(|(name, _)| name.to_owned()) - }) -} - -fn find_forgettable_branches( - view: &View, - name_patterns: &[StringPattern], -) -> Result, CommandError> { - find_branches_with(name_patterns, |pattern| { - view.branches() - .filter(|(name, _)| pattern.matches(name)) - .map(|(name, _)| name.to_owned()) - }) -} - -fn find_branches_with<'a, I: Iterator>( - name_patterns: &'a [StringPattern], - mut find_matches: impl FnMut(&'a StringPattern) -> I, -) -> Result, CommandError> { - let mut matching_branches: Vec = vec![]; - let mut unmatched_patterns = vec![]; - for pattern in name_patterns { - let mut names = find_matches(pattern).peekable(); - if names.peek().is_none() { - unmatched_patterns.push(pattern); - } - matching_branches.extend(names); - } - match &unmatched_patterns[..] { - [] => { - matching_branches.sort_unstable(); - matching_branches.dedup(); - Ok(matching_branches) - } - [pattern] if pattern.is_exact() => Err(user_error(format!("No such branch: {pattern}"))), - patterns => Err(user_error(format!( - "No matching branches for patterns: {}", - patterns.iter().join(", ") - ))), - } -} - -fn find_remote_branches<'a>( - view: &'a View, - name_patterns: &[RemoteBranchNamePattern], -) -> Result, CommandError> { - let mut matching_branches = vec![]; - let mut unmatched_patterns = vec![]; - for pattern in name_patterns { - let mut matches = view - .remote_branches_matching(&pattern.branch, &pattern.remote) - .map(|((branch, remote), remote_ref)| { - let name = RemoteBranchName { - branch: branch.to_owned(), - remote: remote.to_owned(), - }; - (name, remote_ref) - }) - .peekable(); - if matches.peek().is_none() { - unmatched_patterns.push(pattern); - } - matching_branches.extend(matches); - } - match &unmatched_patterns[..] { - [] => { - matching_branches.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2)); - matching_branches.dedup_by(|(name1, _), (name2, _)| name1 == name2); - Ok(matching_branches) - } - [pattern] if pattern.is_exact() => { - Err(user_error(format!("No such remote branch: {pattern}"))) - } - patterns => Err(user_error(format!( - "No matching remote branches for patterns: {}", - patterns.iter().join(", ") - ))), - } -} - -fn cmd_branch_delete( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchDeleteArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let view = workspace_command.repo().view(); - if !args.glob.is_empty() { - writeln!( - ui.warning_default(), - "--glob has been deprecated. Please prefix the pattern with `glob:` instead." - )?; - } - let name_patterns = [&args.names[..], &args.glob[..]].concat(); - let names = find_local_branches(view, &name_patterns)?; - let mut tx = workspace_command.start_transaction(); - for branch_name in names.iter() { - tx.mut_repo() - .set_local_branch_target(branch_name, RefTarget::absent()); - } - tx.finish(ui, format!("delete {}", make_branch_term(&names)))?; - if names.len() > 1 { - writeln!(ui.status(), "Deleted {} branches.", names.len())?; - } - Ok(()) -} - -fn cmd_branch_forget( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchForgetArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let view = workspace_command.repo().view(); - if !args.glob.is_empty() { - writeln!( - ui.warning_default(), - "--glob has been deprecated. Please prefix the pattern with `glob:` instead." - )?; - } - let name_patterns = [&args.names[..], &args.glob[..]].concat(); - let names = find_forgettable_branches(view, &name_patterns)?; - let mut tx = workspace_command.start_transaction(); - for branch_name in names.iter() { - tx.mut_repo().remove_branch(branch_name); - } - tx.finish(ui, format!("forget {}", make_branch_term(&names)))?; - if names.len() > 1 { - writeln!(ui.status(), "Forgot {} branches.", names.len())?; - } - Ok(()) -} - -fn cmd_branch_track( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchTrackArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let view = workspace_command.repo().view(); - let mut names = Vec::new(); - for (name, remote_ref) in find_remote_branches(view, &args.names)? { - if remote_ref.is_tracking() { - writeln!( - ui.warning_default(), - "Remote branch already tracked: {name}" - )?; - } else { - names.push(name); - } - } - let mut tx = workspace_command.start_transaction(); - for name in &names { - tx.mut_repo() - .track_remote_branch(&name.branch, &name.remote); - } - tx.finish(ui, format!("track remote {}", make_branch_term(&names)))?; - if names.len() > 1 { - writeln!( - ui.status(), - "Started tracking {} remote branches.", - names.len() - )?; - } - - //show conflicted branches if there are some - - if let Some(mut formatter) = ui.status_formatter() { - let template = { - let language = workspace_command.commit_template_language()?; - let text = command - .settings() - .config() - .get::("templates.branch_list")?; - workspace_command - .parse_template(&language, &text, CommitTemplateLanguage::wrap_ref_name)? - .labeled("branch_list") - }; - - let mut remote_per_branch: HashMap<&str, Vec<&str>> = HashMap::new(); - for n in names.iter() { - remote_per_branch - .entry(&n.branch) - .or_default() - .push(&n.remote); - } - let branches_to_list = - workspace_command - .repo() - .view() - .branches() - .filter(|(name, target)| { - remote_per_branch.contains_key(name) && target.local_target.has_conflict() - }); - - for (name, branch_target) in branches_to_list { - let local_target = branch_target.local_target; - let ref_name = RefName::local( - name, - local_target.clone(), - branch_target.remote_refs.iter().map(|x| x.1), - ); - template.format(&ref_name, formatter.as_mut())?; - - for (remote_name, remote_ref) in branch_target.remote_refs { - if remote_per_branch[name].contains(&remote_name) { - let ref_name = - RefName::remote(name, remote_name, remote_ref.clone(), local_target); - template.format(&ref_name, formatter.as_mut())?; - } - } - } - } - Ok(()) -} - -fn cmd_branch_untrack( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchUntrackArgs, -) -> Result<(), CommandError> { - let mut workspace_command = command.workspace_helper(ui)?; - let view = workspace_command.repo().view(); - let mut names = Vec::new(); - for (name, remote_ref) in find_remote_branches(view, &args.names)? { - if name.remote == git::REMOTE_NAME_FOR_LOCAL_GIT_REPO { - // This restriction can be lifted if we want to support untracked @git branches. - writeln!( - ui.warning_default(), - "Git-tracking branch cannot be untracked: {name}" - )?; - } else if !remote_ref.is_tracking() { - writeln!( - ui.warning_default(), - "Remote branch not tracked yet: {name}" - )?; - } else { - names.push(name); - } - } - let mut tx = workspace_command.start_transaction(); - for name in &names { - tx.mut_repo() - .untrack_remote_branch(&name.branch, &name.remote); - } - tx.finish(ui, format!("untrack remote {}", make_branch_term(&names)))?; - if names.len() > 1 { - writeln!( - ui.status(), - "Stopped tracking {} remote branches.", - names.len() - )?; - } - Ok(()) -} - -fn cmd_branch_list( - ui: &mut Ui, - command: &CommandHelper, - args: &BranchListArgs, -) -> Result<(), CommandError> { - let workspace_command = command.workspace_helper(ui)?; - let repo = workspace_command.repo(); - let view = repo.view(); - - // Like cmd_git_push(), names and revisions are OR-ed. - let branch_names_to_list = if !args.names.is_empty() || !args.revisions.is_empty() { - let mut branch_names: HashSet<&str> = HashSet::new(); - if !args.names.is_empty() { - branch_names.extend( - view.branches() - .filter(|&(name, _)| args.names.iter().any(|pattern| pattern.matches(name))) - .map(|(name, _)| name), - ); - } - if !args.revisions.is_empty() { - // Match against local targets only, which is consistent with "jj git push". - let mut expression = workspace_command.parse_union_revsets(&args.revisions)?; - // Intersects with the set of local branch targets to minimize the lookup space. - expression.intersect_with(&RevsetExpression::branches(StringPattern::everything())); - let filtered_targets: HashSet<_> = expression.evaluate_to_commit_ids()?.collect(); - branch_names.extend( - view.local_branches() - .filter(|(_, target)| { - target.added_ids().any(|id| filtered_targets.contains(id)) - }) - .map(|(name, _)| name), - ); - } - Some(branch_names) - } else { - None - }; - - let template = { - let language = workspace_command.commit_template_language()?; - let text = match &args.template { - Some(value) => value.to_owned(), - None => command.settings().config().get("templates.branch_list")?, - }; - workspace_command - .parse_template(&language, &text, CommitTemplateLanguage::wrap_ref_name)? - .labeled("branch_list") - }; - - ui.request_pager(); - let mut formatter = ui.stdout_formatter(); - - let mut found_deleted_local_branch = false; - let mut found_deleted_tracking_local_branch = false; - let branches_to_list = view.branches().filter(|(name, target)| { - branch_names_to_list - .as_ref() - .map_or(true, |branch_names| branch_names.contains(name)) - && (!args.conflicted || target.local_target.has_conflict()) - }); - for (name, branch_target) in branches_to_list { - let local_target = branch_target.local_target; - let remote_refs = branch_target.remote_refs; - let (mut tracking_remote_refs, untracked_remote_refs) = remote_refs - .iter() - .copied() - .partition::, _>(|&(_, remote_ref)| remote_ref.is_tracking()); - - if args.tracked { - tracking_remote_refs - .retain(|&(remote, _)| remote != git::REMOTE_NAME_FOR_LOCAL_GIT_REPO); - } else if !args.all_remotes { - tracking_remote_refs.retain(|&(_, remote_ref)| remote_ref.target != *local_target); - } - - if !args.tracked && local_target.is_present() || !tracking_remote_refs.is_empty() { - let ref_name = RefName::local( - name, - local_target.clone(), - remote_refs.iter().map(|&(_, remote_ref)| remote_ref), - ); - template.format(&ref_name, formatter.as_mut())?; - } - - for &(remote, remote_ref) in &tracking_remote_refs { - let ref_name = RefName::remote(name, remote, remote_ref.clone(), local_target); - template.format(&ref_name, formatter.as_mut())?; - } - - if local_target.is_absent() && !tracking_remote_refs.is_empty() { - found_deleted_local_branch = true; - found_deleted_tracking_local_branch |= tracking_remote_refs - .iter() - .any(|&(remote, _)| remote != git::REMOTE_NAME_FOR_LOCAL_GIT_REPO); - } - - if args.all_remotes { - for &(remote, remote_ref) in &untracked_remote_refs { - let ref_name = RefName::remote_only(name, remote, remote_ref.target.clone()); - template.format(&ref_name, formatter.as_mut())?; - } - } - } - - drop(formatter); - - // Print only one of these hints. It's not important to mention unexported - // branches, but user might wonder why deleted branches are still listed. - if found_deleted_tracking_local_branch { - writeln!( - ui.hint_default(), - "Branches marked as deleted will be *deleted permanently* on the remote on the next \ - `jj git push`. Use `jj branch forget` to prevent this." - )?; - } else if found_deleted_local_branch { - writeln!( - ui.hint_default(), - "Branches marked as deleted will be deleted from the underlying Git repo on the next \ - `jj git export`." - )?; - } - - Ok(()) -} - -fn is_fast_forward(repo: &dyn Repo, old_target: &RefTarget, new_target_id: &CommitId) -> bool { - if old_target.is_present() { - // Strictly speaking, "all" old targets should be ancestors, but we allow - // conflict resolution by setting branch to "any" of the old target descendants. - old_target - .added_ids() - .any(|old| repo.index().is_ancestor(old, new_target_id)) - } else { - true - } -} diff --git a/cli/src/commands/branch/create.rs b/cli/src/commands/branch/create.rs new file mode 100644 index 000000000..de527e165 --- /dev/null +++ b/cli/src/commands/branch/create.rs @@ -0,0 +1,78 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use clap::builder::NonEmptyStringValueParser; +use jj_lib::object_id::ObjectId as _; +use jj_lib::op_store::RefTarget; + +use super::make_branch_term; +use crate::cli_util::{CommandHelper, RevisionArg}; +use crate::command_error::{user_error_with_hint, CommandError}; +use crate::ui::Ui; + +/// Create a new branch. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchCreateArgs { + /// The branch's target revision. + #[arg(long, short)] + revision: Option, + + /// The branches to create. + #[arg(required = true, value_parser=NonEmptyStringValueParser::new())] + names: Vec, +} + +pub fn cmd_branch_create( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchCreateArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let target_commit = + 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.", + )); + } + + if branch_names.len() > 1 { + writeln!( + ui.warning_default(), + "Creating multiple branches: {}", + branch_names.join(", "), + )?; + } + + let mut tx = workspace_command.start_transaction(); + for branch_name in branch_names { + tx.mut_repo() + .set_local_branch_target(branch_name, RefTarget::normal(target_commit.id().clone())); + } + tx.finish( + ui, + format!( + "create {} pointing to commit {}", + make_branch_term(branch_names), + target_commit.id().hex() + ), + )?; + Ok(()) +} diff --git a/cli/src/commands/branch/delete.rs b/cli/src/commands/branch/delete.rs new file mode 100644 index 000000000..088393f10 --- /dev/null +++ b/cli/src/commands/branch/delete.rs @@ -0,0 +1,65 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::op_store::RefTarget; +use jj_lib::str_util::StringPattern; + +use super::{find_local_branches, make_branch_term}; +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Delete an existing branch and propagate the deletion to remotes on the +/// next push. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchDeleteArgs { + /// The branches to delete + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// select branches by wildcard pattern. For details, see + /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. + #[arg(required_unless_present_any(&["glob"]), value_parser = StringPattern::parse)] + pub names: Vec, + + /// Deprecated. Please prefix the pattern with `glob:` instead. + #[arg(long, hide = true, value_parser = StringPattern::glob)] + pub glob: Vec, +} + +pub fn cmd_branch_delete( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchDeleteArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let view = workspace_command.repo().view(); + if !args.glob.is_empty() { + writeln!( + ui.warning_default(), + "--glob has been deprecated. Please prefix the pattern with `glob:` instead." + )?; + } + let name_patterns = [&args.names[..], &args.glob[..]].concat(); + let names = find_local_branches(view, &name_patterns)?; + let mut tx = workspace_command.start_transaction(); + for branch_name in names.iter() { + tx.mut_repo() + .set_local_branch_target(branch_name, RefTarget::absent()); + } + tx.finish(ui, format!("delete {}", make_branch_term(&names)))?; + if names.len() > 1 { + writeln!(ui.status(), "Deleted {} branches.", names.len())?; + } + Ok(()) +} diff --git a/cli/src/commands/branch/forget.rs b/cli/src/commands/branch/forget.rs new file mode 100644 index 000000000..3baed1939 --- /dev/null +++ b/cli/src/commands/branch/forget.rs @@ -0,0 +1,78 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::str_util::StringPattern; +use jj_lib::view::View; + +use super::{find_branches_with, make_branch_term}; +use crate::cli_util::CommandHelper; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Forget everything about a branch, including its local and remote +/// targets. +/// +/// A forgotten branch will not impact remotes on future pushes. It will be +/// recreated on future pulls if it still exists in the remote. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchForgetArgs { + /// The branches to forget + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// select branches by wildcard pattern. For details, see + /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. + #[arg(required_unless_present_any(&["glob"]), value_parser = StringPattern::parse)] + pub names: Vec, + + /// Deprecated. Please prefix the pattern with `glob:` instead. + #[arg(long, hide = true, value_parser = StringPattern::glob)] + pub glob: Vec, +} + +fn find_forgettable_branches( + view: &View, + name_patterns: &[StringPattern], +) -> Result, CommandError> { + find_branches_with(name_patterns, |pattern| { + view.branches() + .filter(|(name, _)| pattern.matches(name)) + .map(|(name, _)| name.to_owned()) + }) +} + +pub fn cmd_branch_forget( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchForgetArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let view = workspace_command.repo().view(); + if !args.glob.is_empty() { + writeln!( + ui.warning_default(), + "--glob has been deprecated. Please prefix the pattern with `glob:` instead." + )?; + } + let name_patterns = [&args.names[..], &args.glob[..]].concat(); + let names = find_forgettable_branches(view, &name_patterns)?; + let mut tx = workspace_command.start_transaction(); + for branch_name in names.iter() { + tx.mut_repo().remove_branch(branch_name); + } + tx.finish(ui, format!("forget {}", make_branch_term(&names)))?; + if names.len() > 1 { + writeln!(ui.status(), "Forgot {} branches.", names.len())?; + } + Ok(()) +} diff --git a/cli/src/commands/branch/list.rs b/cli/src/commands/branch/list.rs new file mode 100644 index 000000000..a150bbd25 --- /dev/null +++ b/cli/src/commands/branch/list.rs @@ -0,0 +1,199 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashSet; + +use jj_lib::git; +use jj_lib::revset::RevsetExpression; +use jj_lib::str_util::StringPattern; + +use crate::cli_util::{CommandHelper, RevisionArg}; +use crate::command_error::CommandError; +use crate::commit_templater::{CommitTemplateLanguage, RefName}; +use crate::ui::Ui; + +/// List branches and their targets +/// +/// By default, a tracking remote branch will be included only if its target is +/// different from the local target. A non-tracking remote branch won't be +/// listed. For a conflicted branch (both local and remote), old target +/// revisions are preceded by a "-" and new target revisions are preceded by a +/// "+". +/// +/// For information about branches, see +/// https://github.com/martinvonz/jj/blob/main/docs/branches.md. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchListArgs { + /// Show all tracking and non-tracking remote branches including the ones + /// whose targets are synchronized with the local branches. + #[arg(long, short, alias = "all")] + all_remotes: bool, + + /// Show remote tracked branches only. Omits local Git-tracking branches by + /// default. + #[arg(long, short, conflicts_with_all = ["all_remotes"])] + tracked: bool, + + /// Show conflicted branches only. + #[arg(long, short, conflicts_with_all = ["all_remotes"])] + conflicted: bool, + + /// Show branches whose local name matches + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// select branches by wildcard pattern. For details, see + /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. + #[arg(value_parser = StringPattern::parse)] + pub names: Vec, + + /// Show branches whose local targets are in the given revisions. + /// + /// Note that `-r deleted_branch` will not work since `deleted_branch` + /// wouldn't have a local target. + #[arg(long, short)] + revisions: Vec, + + /// Render each branch using the given template + /// + /// All 0-argument methods of the `RefName` type are available as keywords. + /// + /// For the syntax, see https://github.com/martinvonz/jj/blob/main/docs/templates.md + #[arg(long, short = 'T')] + template: Option, +} + +pub fn cmd_branch_list( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchListArgs, +) -> Result<(), CommandError> { + let workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo(); + let view = repo.view(); + + // Like cmd_git_push(), names and revisions are OR-ed. + let branch_names_to_list = if !args.names.is_empty() || !args.revisions.is_empty() { + let mut branch_names: HashSet<&str> = HashSet::new(); + if !args.names.is_empty() { + branch_names.extend( + view.branches() + .filter(|&(name, _)| args.names.iter().any(|pattern| pattern.matches(name))) + .map(|(name, _)| name), + ); + } + if !args.revisions.is_empty() { + // Match against local targets only, which is consistent with "jj git push". + let mut expression = workspace_command.parse_union_revsets(&args.revisions)?; + // Intersects with the set of local branch targets to minimize the lookup space. + expression.intersect_with(&RevsetExpression::branches(StringPattern::everything())); + let filtered_targets: HashSet<_> = expression.evaluate_to_commit_ids()?.collect(); + branch_names.extend( + view.local_branches() + .filter(|(_, target)| { + target.added_ids().any(|id| filtered_targets.contains(id)) + }) + .map(|(name, _)| name), + ); + } + Some(branch_names) + } else { + None + }; + + let template = { + let language = workspace_command.commit_template_language()?; + let text = match &args.template { + Some(value) => value.to_owned(), + None => command.settings().config().get("templates.branch_list")?, + }; + workspace_command + .parse_template(&language, &text, CommitTemplateLanguage::wrap_ref_name)? + .labeled("branch_list") + }; + + ui.request_pager(); + let mut formatter = ui.stdout_formatter(); + + let mut found_deleted_local_branch = false; + let mut found_deleted_tracking_local_branch = false; + let branches_to_list = view.branches().filter(|(name, target)| { + branch_names_to_list + .as_ref() + .map_or(true, |branch_names| branch_names.contains(name)) + && (!args.conflicted || target.local_target.has_conflict()) + }); + for (name, branch_target) in branches_to_list { + let local_target = branch_target.local_target; + let remote_refs = branch_target.remote_refs; + let (mut tracking_remote_refs, untracked_remote_refs) = remote_refs + .iter() + .copied() + .partition::, _>(|&(_, remote_ref)| remote_ref.is_tracking()); + + if args.tracked { + tracking_remote_refs + .retain(|&(remote, _)| remote != git::REMOTE_NAME_FOR_LOCAL_GIT_REPO); + } else if !args.all_remotes { + tracking_remote_refs.retain(|&(_, remote_ref)| remote_ref.target != *local_target); + } + + if !args.tracked && local_target.is_present() || !tracking_remote_refs.is_empty() { + let ref_name = RefName::local( + name, + local_target.clone(), + remote_refs.iter().map(|&(_, remote_ref)| remote_ref), + ); + template.format(&ref_name, formatter.as_mut())?; + } + + for &(remote, remote_ref) in &tracking_remote_refs { + let ref_name = RefName::remote(name, remote, remote_ref.clone(), local_target); + template.format(&ref_name, formatter.as_mut())?; + } + + if local_target.is_absent() && !tracking_remote_refs.is_empty() { + found_deleted_local_branch = true; + found_deleted_tracking_local_branch |= tracking_remote_refs + .iter() + .any(|&(remote, _)| remote != git::REMOTE_NAME_FOR_LOCAL_GIT_REPO); + } + + if args.all_remotes { + for &(remote, remote_ref) in &untracked_remote_refs { + let ref_name = RefName::remote_only(name, remote, remote_ref.target.clone()); + template.format(&ref_name, formatter.as_mut())?; + } + } + } + + drop(formatter); + + // Print only one of these hints. It's not important to mention unexported + // branches, but user might wonder why deleted branches are still listed. + if found_deleted_tracking_local_branch { + writeln!( + ui.hint_default(), + "Branches marked as deleted will be *deleted permanently* on the remote on the next \ + `jj git push`. Use `jj branch forget` to prevent this." + )?; + } else if found_deleted_local_branch { + writeln!( + ui.hint_default(), + "Branches marked as deleted will be deleted from the underlying Git repo on the next \ + `jj git export`." + )?; + } + + Ok(()) +} diff --git a/cli/src/commands/branch/mod.rs b/cli/src/commands/branch/mod.rs new file mode 100644 index 000000000..fb7513939 --- /dev/null +++ b/cli/src/commands/branch/mod.rs @@ -0,0 +1,182 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +mod create; +mod delete; +mod forget; +mod list; +mod r#move; +mod rename; +mod set; +mod track; +mod untrack; + +use std::fmt; + +use itertools::Itertools as _; +use jj_lib::backend::CommitId; +use jj_lib::op_store::{RefTarget, RemoteRef}; +use jj_lib::repo::Repo; +use jj_lib::str_util::StringPattern; +use jj_lib::view::View; + +use self::create::{cmd_branch_create, BranchCreateArgs}; +use self::delete::{cmd_branch_delete, BranchDeleteArgs}; +use self::forget::{cmd_branch_forget, BranchForgetArgs}; +use self::list::{cmd_branch_list, BranchListArgs}; +use self::r#move::{cmd_branch_move, BranchMoveArgs}; +use self::rename::{cmd_branch_rename, BranchRenameArgs}; +use self::set::{cmd_branch_set, BranchSetArgs}; +use self::track::{cmd_branch_track, BranchTrackArgs}; +use self::untrack::{cmd_branch_untrack, BranchUntrackArgs}; +use crate::cli_util::{CommandHelper, RemoteBranchName, RemoteBranchNamePattern}; +use crate::command_error::{user_error, CommandError}; +use crate::ui::Ui; + +/// Manage branches. +/// +/// For information about branches, see +/// https://github.com/martinvonz/jj/blob/main/docs/branches.md. +#[derive(clap::Subcommand, Clone, Debug)] +pub enum BranchCommand { + #[command(visible_alias("c"))] + Create(BranchCreateArgs), + #[command(visible_alias("d"))] + Delete(BranchDeleteArgs), + #[command(visible_alias("f"))] + Forget(BranchForgetArgs), + #[command(visible_alias("l"))] + List(BranchListArgs), + #[command(visible_alias("m"))] + Move(BranchMoveArgs), + #[command(visible_alias("r"))] + Rename(BranchRenameArgs), + #[command(visible_alias("s"))] + Set(BranchSetArgs), + #[command(visible_alias("t"))] + Track(BranchTrackArgs), + Untrack(BranchUntrackArgs), +} + +fn make_branch_term(branch_names: &[impl fmt::Display]) -> String { + match branch_names { + [branch_name] => format!("branch {}", branch_name), + branch_names => format!("branches {}", branch_names.iter().join(", ")), + } +} + +pub fn cmd_branch( + ui: &mut Ui, + command: &CommandHelper, + subcommand: &BranchCommand, +) -> Result<(), CommandError> { + match subcommand { + BranchCommand::Create(sub_args) => cmd_branch_create(ui, command, sub_args), + BranchCommand::Rename(sub_args) => cmd_branch_rename(ui, command, sub_args), + BranchCommand::Set(sub_args) => cmd_branch_set(ui, command, sub_args), + BranchCommand::Move(sub_args) => cmd_branch_move(ui, command, sub_args), + BranchCommand::Delete(sub_args) => cmd_branch_delete(ui, command, sub_args), + BranchCommand::Forget(sub_args) => cmd_branch_forget(ui, command, sub_args), + BranchCommand::Track(sub_args) => cmd_branch_track(ui, command, sub_args), + BranchCommand::Untrack(sub_args) => cmd_branch_untrack(ui, command, sub_args), + BranchCommand::List(sub_args) => cmd_branch_list(ui, command, sub_args), + } +} + +fn find_local_branches( + view: &View, + name_patterns: &[StringPattern], +) -> Result, CommandError> { + find_branches_with(name_patterns, |pattern| { + view.local_branches_matching(pattern) + .map(|(name, _)| name.to_owned()) + }) +} + +fn find_branches_with<'a, I: Iterator>( + name_patterns: &'a [StringPattern], + mut find_matches: impl FnMut(&'a StringPattern) -> I, +) -> Result, CommandError> { + let mut matching_branches: Vec = vec![]; + let mut unmatched_patterns = vec![]; + for pattern in name_patterns { + let mut names = find_matches(pattern).peekable(); + if names.peek().is_none() { + unmatched_patterns.push(pattern); + } + matching_branches.extend(names); + } + match &unmatched_patterns[..] { + [] => { + matching_branches.sort_unstable(); + matching_branches.dedup(); + Ok(matching_branches) + } + [pattern] if pattern.is_exact() => Err(user_error(format!("No such branch: {pattern}"))), + patterns => Err(user_error(format!( + "No matching branches for patterns: {}", + patterns.iter().join(", ") + ))), + } +} + +fn find_remote_branches<'a>( + view: &'a View, + name_patterns: &[RemoteBranchNamePattern], +) -> Result, CommandError> { + let mut matching_branches = vec![]; + let mut unmatched_patterns = vec![]; + for pattern in name_patterns { + let mut matches = view + .remote_branches_matching(&pattern.branch, &pattern.remote) + .map(|((branch, remote), remote_ref)| { + let name = RemoteBranchName { + branch: branch.to_owned(), + remote: remote.to_owned(), + }; + (name, remote_ref) + }) + .peekable(); + if matches.peek().is_none() { + unmatched_patterns.push(pattern); + } + matching_branches.extend(matches); + } + match &unmatched_patterns[..] { + [] => { + matching_branches.sort_unstable_by(|(name1, _), (name2, _)| name1.cmp(name2)); + matching_branches.dedup_by(|(name1, _), (name2, _)| name1 == name2); + Ok(matching_branches) + } + [pattern] if pattern.is_exact() => { + Err(user_error(format!("No such remote branch: {pattern}"))) + } + patterns => Err(user_error(format!( + "No matching remote branches for patterns: {}", + patterns.iter().join(", ") + ))), + } +} + +fn is_fast_forward(repo: &dyn Repo, old_target: &RefTarget, new_target_id: &CommitId) -> bool { + if old_target.is_present() { + // Strictly speaking, "all" old targets should be ancestors, but we allow + // conflict resolution by setting branch to "any" of the old target descendants. + old_target + .added_ids() + .any(|old| repo.index().is_ancestor(old, new_target_id)) + } else { + true + } +} diff --git a/cli/src/commands/branch/move.rs b/cli/src/commands/branch/move.rs new file mode 100644 index 000000000..e0cd00348 --- /dev/null +++ b/cli/src/commands/branch/move.rs @@ -0,0 +1,135 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::backend::CommitId; +use jj_lib::object_id::ObjectId as _; +use jj_lib::op_store::RefTarget; +use jj_lib::str_util::StringPattern; + +use super::{find_branches_with, is_fast_forward, make_branch_term}; +use crate::cli_util::{CommandHelper, RevisionArg}; +use crate::command_error::{user_error_with_hint, CommandError}; +use crate::ui::Ui; + +/// Move existing branches to target revision +/// +/// If branch names are given, the specified branches will be updated to point +/// to the target revision. +/// +/// If `--from` options are given, branches currently pointing to the specified +/// revisions will be updated. The branches can also be filtered by names. +/// +/// Example: pull up the nearest branches to the working-copy parent +/// +/// $ jj branch move --from 'heads(::@- & branches())' --to @- +#[derive(clap::Args, Clone, Debug)] +#[command(group(clap::ArgGroup::new("source").multiple(true).required(true)))] +pub struct BranchMoveArgs { + /// Move branches from the given revisions + #[arg(long, group = "source", value_name = "REVISIONS")] + from: Vec, + + /// Move branches to this revision + #[arg(long, default_value = "@", value_name = "REVISION")] + to: RevisionArg, + + /// Allow moving branches backwards or sideways + #[arg(long, short = 'B')] + allow_backwards: bool, + + /// Move branches matching the given name patterns + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// select branches by wildcard pattern. For details, see + /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. + #[arg(group = "source", value_parser = StringPattern::parse)] + names: Vec, +} + +pub fn cmd_branch_move( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchMoveArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let repo = workspace_command.repo().as_ref(); + let view = repo.view(); + + let branch_names = { + let is_source_commit = if !args.from.is_empty() { + workspace_command + .parse_union_revsets(&args.from)? + .evaluate()? + .containing_fn() + } else { + Box::new(|_: &CommitId| true) + }; + if !args.names.is_empty() { + find_branches_with(&args.names, |pattern| { + view.local_branches_matching(pattern) + .filter(|(_, target)| target.added_ids().any(&is_source_commit)) + .map(|(name, _)| name.to_owned()) + })? + } else { + view.local_branches() + .filter(|(_, target)| target.added_ids().any(&is_source_commit)) + .map(|(name, _)| name.to_owned()) + .collect() + } + }; + let target_commit = workspace_command.resolve_single_rev(&args.to)?; + + if branch_names.is_empty() { + writeln!(ui.status(), "No branches to update.")?; + return Ok(()); + } + if branch_names.len() > 1 { + writeln!( + ui.warning_default(), + "Updating multiple branches: {}", + branch_names.join(", "), + )?; + if args.names.is_empty() { + writeln!(ui.hint_default(), "Specify branch by name to update one.")?; + } + } + + if !args.allow_backwards { + if let Some(name) = branch_names + .iter() + .find(|name| !is_fast_forward(repo, view.get_local_branch(name), target_commit.id())) + { + return Err(user_error_with_hint( + format!("Refusing to move branch backwards or sideways: {name}"), + "Use --allow-backwards to allow it.", + )); + } + } + + let mut tx = workspace_command.start_transaction(); + for name in &branch_names { + tx.mut_repo() + .set_local_branch_target(name, RefTarget::normal(target_commit.id().clone())); + } + tx.finish( + ui, + format!( + "point {} to commit {}", + make_branch_term(&branch_names), + target_commit.id().hex() + ), + )?; + + Ok(()) +} diff --git a/cli/src/commands/branch/rename.rs b/cli/src/commands/branch/rename.rs new file mode 100644 index 000000000..2082b68f7 --- /dev/null +++ b/cli/src/commands/branch/rename.rs @@ -0,0 +1,89 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::op_store::RefTarget; +use jj_lib::str_util::StringPattern; + +use super::make_branch_term; +use crate::cli_util::CommandHelper; +use crate::command_error::{user_error, CommandError}; +use crate::ui::Ui; + +/// 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, +} + +pub fn cmd_branch_rename( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchRenameArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let view = workspace_command.repo().view(); + let old_branch = &args.old; + let ref_target = view.get_local_branch(old_branch).clone(); + if ref_target.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, ref_target); + 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]), + ), + )?; + + let view = workspace_command.repo().view(); + if view + .remote_branches_matching( + &StringPattern::exact(old_branch), + &StringPattern::everything(), + ) + .any(|(_, remote_ref)| remote_ref.is_tracking()) + { + writeln!( + ui.warning_default(), + "Branch {old_branch} has tracking remote branches which were not renamed." + )?; + writeln!( + ui.hint_default(), + "to rename the branch on the remote, you can `jj git push --branch {old_branch}` \ + first (to delete it on the remote), and then `jj git push --branch {new_branch}`. \ + `jj git push --all` would also be sufficient." + )?; + } + + Ok(()) +} diff --git a/cli/src/commands/branch/set.rs b/cli/src/commands/branch/set.rs new file mode 100644 index 000000000..18ba43042 --- /dev/null +++ b/cli/src/commands/branch/set.rs @@ -0,0 +1,87 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::object_id::ObjectId as _; +use jj_lib::op_store::RefTarget; + +use super::{is_fast_forward, make_branch_term}; +use crate::cli_util::{CommandHelper, RevisionArg}; +use crate::command_error::{user_error_with_hint, CommandError}; +use crate::ui::Ui; + +/// Update an existing branch to point to a certain commit. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchSetArgs { + /// The branch's target revision. + #[arg(long, short)] + pub revision: Option, + + /// Allow moving the branch backwards or sideways. + #[arg(long, short = 'B')] + pub allow_backwards: bool, + + /// The branches to update. + #[arg(required = true)] + pub names: Vec, +} + +pub fn cmd_branch_set( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchSetArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let target_commit = + workspace_command.resolve_single_rev(args.revision.as_ref().unwrap_or(&RevisionArg::AT))?; + let repo = workspace_command.repo().as_ref(); + let branch_names = &args.names; + for name in branch_names { + let old_target = repo.view().get_local_branch(name); + if old_target.is_absent() { + return Err(user_error_with_hint( + format!("No such branch: {name}"), + "Use `jj branch create` to create it.", + )); + } + if !args.allow_backwards && !is_fast_forward(repo, old_target, target_commit.id()) { + return Err(user_error_with_hint( + format!("Refusing to move branch backwards or sideways: {name}"), + "Use --allow-backwards to allow it.", + )); + } + } + + if branch_names.len() > 1 { + writeln!( + ui.warning_default(), + "Updating multiple branches: {}", + branch_names.join(", "), + )?; + } + + let mut tx = workspace_command.start_transaction(); + for branch_name in branch_names { + tx.mut_repo() + .set_local_branch_target(branch_name, RefTarget::normal(target_commit.id().clone())); + } + tx.finish( + ui, + format!( + "point {} to commit {}", + make_branch_term(branch_names), + target_commit.id().hex() + ), + )?; + Ok(()) +} diff --git a/cli/src/commands/branch/track.rs b/cli/src/commands/branch/track.rs new file mode 100644 index 000000000..b392aba1b --- /dev/null +++ b/cli/src/commands/branch/track.rs @@ -0,0 +1,122 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::collections::HashMap; + +use super::{find_remote_branches, make_branch_term}; +use crate::cli_util::{CommandHelper, RemoteBranchNamePattern}; +use crate::command_error::CommandError; +use crate::commit_templater::{CommitTemplateLanguage, RefName}; +use crate::ui::Ui; + +/// Start tracking given remote branches +/// +/// A tracking remote branch will be imported as a local branch of the same +/// name. Changes to it will propagate to the existing local branch on future +/// pulls. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchTrackArgs { + /// Remote branches to track + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// select branches by wildcard pattern. For details, see + /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. + /// + /// Examples: branch@remote, glob:main@*, glob:jjfan-*@upstream + #[arg(required = true, value_name = "BRANCH@REMOTE")] + pub names: Vec, +} + +pub fn cmd_branch_track( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchTrackArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let view = workspace_command.repo().view(); + let mut names = Vec::new(); + for (name, remote_ref) in find_remote_branches(view, &args.names)? { + if remote_ref.is_tracking() { + writeln!( + ui.warning_default(), + "Remote branch already tracked: {name}" + )?; + } else { + names.push(name); + } + } + let mut tx = workspace_command.start_transaction(); + for name in &names { + tx.mut_repo() + .track_remote_branch(&name.branch, &name.remote); + } + tx.finish(ui, format!("track remote {}", make_branch_term(&names)))?; + if names.len() > 1 { + writeln!( + ui.status(), + "Started tracking {} remote branches.", + names.len() + )?; + } + + //show conflicted branches if there are some + + if let Some(mut formatter) = ui.status_formatter() { + let template = { + let language = workspace_command.commit_template_language()?; + let text = command + .settings() + .config() + .get::("templates.branch_list")?; + workspace_command + .parse_template(&language, &text, CommitTemplateLanguage::wrap_ref_name)? + .labeled("branch_list") + }; + + let mut remote_per_branch: HashMap<&str, Vec<&str>> = HashMap::new(); + for n in names.iter() { + remote_per_branch + .entry(&n.branch) + .or_default() + .push(&n.remote); + } + let branches_to_list = + workspace_command + .repo() + .view() + .branches() + .filter(|(name, target)| { + remote_per_branch.contains_key(name) && target.local_target.has_conflict() + }); + + for (name, branch_target) in branches_to_list { + let local_target = branch_target.local_target; + let ref_name = RefName::local( + name, + local_target.clone(), + branch_target.remote_refs.iter().map(|x| x.1), + ); + template.format(&ref_name, formatter.as_mut())?; + + for (remote_name, remote_ref) in branch_target.remote_refs { + if remote_per_branch[name].contains(&remote_name) { + let ref_name = + RefName::remote(name, remote_name, remote_ref.clone(), local_target); + template.format(&ref_name, formatter.as_mut())?; + } + } + } + } + Ok(()) +} diff --git a/cli/src/commands/branch/untrack.rs b/cli/src/commands/branch/untrack.rs new file mode 100644 index 000000000..ca90650ba --- /dev/null +++ b/cli/src/commands/branch/untrack.rs @@ -0,0 +1,77 @@ +// Copyright 2020-2023 The Jujutsu Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use jj_lib::git; + +use super::{find_remote_branches, make_branch_term}; +use crate::cli_util::{CommandHelper, RemoteBranchNamePattern}; +use crate::command_error::CommandError; +use crate::ui::Ui; + +/// Stop tracking given remote branches +/// +/// A non-tracking remote branch is just a pointer to the last-fetched remote +/// branch. It won't be imported as a local branch on future pulls. +#[derive(clap::Args, Clone, Debug)] +pub struct BranchUntrackArgs { + /// Remote branches to untrack + /// + /// By default, the specified name matches exactly. Use `glob:` prefix to + /// select branches by wildcard pattern. For details, see + /// https://github.com/martinvonz/jj/blob/main/docs/revsets.md#string-patterns. + /// + /// Examples: branch@remote, glob:main@*, glob:jjfan-*@upstream + #[arg(required = true, value_name = "BRANCH@REMOTE")] + pub names: Vec, +} + +pub fn cmd_branch_untrack( + ui: &mut Ui, + command: &CommandHelper, + args: &BranchUntrackArgs, +) -> Result<(), CommandError> { + let mut workspace_command = command.workspace_helper(ui)?; + let view = workspace_command.repo().view(); + let mut names = Vec::new(); + for (name, remote_ref) in find_remote_branches(view, &args.names)? { + if name.remote == git::REMOTE_NAME_FOR_LOCAL_GIT_REPO { + // This restriction can be lifted if we want to support untracked @git branches. + writeln!( + ui.warning_default(), + "Git-tracking branch cannot be untracked: {name}" + )?; + } else if !remote_ref.is_tracking() { + writeln!( + ui.warning_default(), + "Remote branch not tracked yet: {name}" + )?; + } else { + names.push(name); + } + } + let mut tx = workspace_command.start_transaction(); + for name in &names { + tx.mut_repo() + .untrack_remote_branch(&name.branch, &name.remote); + } + tx.finish(ui, format!("untrack remote {}", make_branch_term(&names)))?; + if names.len() > 1 { + writeln!( + ui.status(), + "Stopped tracking {} remote branches.", + names.len() + )?; + } + Ok(()) +}