ok/jj
1
0
Fork 0
forked from mirrors/jj

cli: branch: split to per-command module files

This is another big subcommand module. Let's split it up.

I'm not a big fan of r#move syntax, but we already have one in src/commands,
so there's no point to avoid it.
This commit is contained in:
Yuya Nishihara 2024-06-20 12:16:30 +09:00
parent 494de23ea5
commit 9e35b9b218
11 changed files with 1112 additions and 906 deletions

View file

@ -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<RevisionArg>,
/// The branches to create.
#[arg(required = true, value_parser=NonEmptyStringValueParser::new())]
names: Vec<String>,
}
/// 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<StringPattern>,
/// Deprecated. Please prefix the pattern with `glob:` instead.
#[arg(long, hide = true, value_parser = StringPattern::glob)]
pub glob: Vec<StringPattern>,
}
/// 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<StringPattern>,
/// 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<RevisionArg>,
/// 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<String>,
}
/// 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<StringPattern>,
/// Deprecated. Please prefix the pattern with `glob:` instead.
#[arg(long, hide = true, value_parser = StringPattern::glob)]
pub glob: Vec<StringPattern>,
}
/// 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<RevisionArg>,
/// 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<String>,
}
/// 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<RevisionArg>,
/// 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<StringPattern>,
}
/// 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<RemoteBranchNamePattern>,
}
/// 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<RemoteBranchNamePattern>,
}
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<Vec<String>, 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<Vec<String>, 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<Item = String>>(
name_patterns: &'a [StringPattern],
mut find_matches: impl FnMut(&'a StringPattern) -> I,
) -> Result<Vec<String>, CommandError> {
let mut matching_branches: Vec<String> = 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<Vec<(RemoteBranchName, &'a RemoteRef)>, 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::<String>("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::<Vec<_>, _>(|&(_, 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
}
}

View file

@ -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<RevisionArg>,
/// The branches to create.
#[arg(required = true, value_parser=NonEmptyStringValueParser::new())]
names: Vec<String>,
}
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(())
}

View file

@ -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<StringPattern>,
/// Deprecated. Please prefix the pattern with `glob:` instead.
#[arg(long, hide = true, value_parser = StringPattern::glob)]
pub glob: Vec<StringPattern>,
}
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(())
}

View file

@ -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<StringPattern>,
/// Deprecated. Please prefix the pattern with `glob:` instead.
#[arg(long, hide = true, value_parser = StringPattern::glob)]
pub glob: Vec<StringPattern>,
}
fn find_forgettable_branches(
view: &View,
name_patterns: &[StringPattern],
) -> Result<Vec<String>, 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(())
}

View file

@ -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<StringPattern>,
/// 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<RevisionArg>,
/// 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<String>,
}
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::<Vec<_>, _>(|&(_, 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(())
}

View file

@ -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<Vec<String>, CommandError> {
find_branches_with(name_patterns, |pattern| {
view.local_branches_matching(pattern)
.map(|(name, _)| name.to_owned())
})
}
fn find_branches_with<'a, I: Iterator<Item = String>>(
name_patterns: &'a [StringPattern],
mut find_matches: impl FnMut(&'a StringPattern) -> I,
) -> Result<Vec<String>, CommandError> {
let mut matching_branches: Vec<String> = 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<Vec<(RemoteBranchName, &'a RemoteRef)>, 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
}
}

View file

@ -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<RevisionArg>,
/// 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<StringPattern>,
}
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(())
}

View file

@ -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(())
}

View file

@ -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<RevisionArg>,
/// 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<String>,
}
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(())
}

View file

@ -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<RemoteBranchNamePattern>,
}
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::<String>("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(())
}

View file

@ -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<RemoteBranchNamePattern>,
}
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(())
}