forked from mirrors/jj
cli: allow to select branches to track/untrack by string pattern
This commit is contained in:
parent
d43704c978
commit
d7f504c98c
2 changed files with 143 additions and 29 deletions
|
@ -7,7 +7,7 @@ use clap::builder::NonEmptyStringValueParser;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use jj_lib::backend::{CommitId, ObjectId};
|
use jj_lib::backend::{CommitId, ObjectId};
|
||||||
use jj_lib::git;
|
use jj_lib::git;
|
||||||
use jj_lib::op_store::RefTarget;
|
use jj_lib::op_store::{RefTarget, RemoteRef};
|
||||||
use jj_lib::repo::Repo;
|
use jj_lib::repo::Repo;
|
||||||
use jj_lib::revset::{self, RevsetExpression};
|
use jj_lib::revset::{self, RevsetExpression};
|
||||||
use jj_lib::str_util::{StringPattern, StringPatternParseError};
|
use jj_lib::str_util::{StringPattern, StringPatternParseError};
|
||||||
|
@ -136,8 +136,12 @@ pub struct BranchSetArgs {
|
||||||
#[derive(clap::Args, Clone, Debug)]
|
#[derive(clap::Args, Clone, Debug)]
|
||||||
pub struct BranchTrackArgs {
|
pub struct BranchTrackArgs {
|
||||||
/// Remote branches to track
|
/// 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.
|
||||||
#[arg(required = true)]
|
#[arg(required = true)]
|
||||||
pub names: Vec<RemoteBranchName>,
|
pub names: Vec<RemoteBranchNamePattern>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stop tracking given remote branches
|
/// Stop tracking given remote branches
|
||||||
|
@ -147,8 +151,12 @@ pub struct BranchTrackArgs {
|
||||||
#[derive(clap::Args, Clone, Debug)]
|
#[derive(clap::Args, Clone, Debug)]
|
||||||
pub struct BranchUntrackArgs {
|
pub struct BranchUntrackArgs {
|
||||||
/// Remote branches to untrack
|
/// 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.
|
||||||
#[arg(required = true)]
|
#[arg(required = true)]
|
||||||
pub names: Vec<RemoteBranchName>,
|
pub names: Vec<RemoteBranchNamePattern>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||||
|
@ -157,24 +165,57 @@ pub struct RemoteBranchName {
|
||||||
pub remote: String,
|
pub remote: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for RemoteBranchName {
|
impl fmt::Display for RemoteBranchName {
|
||||||
type Err = &'static str;
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let RemoteBranchName { branch, remote } = self;
|
||||||
|
write!(f, "{branch}@{remote}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct RemoteBranchNamePattern {
|
||||||
|
pub branch: StringPattern,
|
||||||
|
pub remote: StringPattern,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for RemoteBranchNamePattern {
|
||||||
|
type Err = String;
|
||||||
|
|
||||||
|
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
||||||
|
// The kind prefix applies to both branch and remote fragments. It's
|
||||||
|
// weird that unanchored patterns like substring:branch@remote is split
|
||||||
|
// into two, but I can't think of a better syntax.
|
||||||
|
// TODO: should we disable substring pattern? what if we added regex?
|
||||||
|
let (maybe_kind, pat) = src
|
||||||
|
.split_once(':')
|
||||||
|
.map_or((None, src), |(kind, pat)| (Some(kind), pat));
|
||||||
|
let to_pattern = |pat: &str| {
|
||||||
|
if let Some(kind) = maybe_kind {
|
||||||
|
StringPattern::from_str_kind(pat, kind).map_err(|err| err.to_string())
|
||||||
|
} else {
|
||||||
|
Ok(StringPattern::exact(pat))
|
||||||
|
}
|
||||||
|
};
|
||||||
// TODO: maybe reuse revset parser to handle branch/remote name containing @
|
// TODO: maybe reuse revset parser to handle branch/remote name containing @
|
||||||
let (branch, remote) = s
|
let (branch, remote) = pat
|
||||||
.rsplit_once('@')
|
.rsplit_once('@')
|
||||||
.ok_or("remote branch must be specified in branch@remote form")?;
|
.ok_or_else(|| "remote branch must be specified in branch@remote form".to_owned())?;
|
||||||
Ok(RemoteBranchName {
|
Ok(RemoteBranchNamePattern {
|
||||||
branch: branch.to_owned(),
|
branch: to_pattern(branch)?,
|
||||||
remote: remote.to_owned(),
|
remote: to_pattern(remote)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for RemoteBranchName {
|
impl RemoteBranchNamePattern {
|
||||||
|
pub fn is_exact(&self) -> bool {
|
||||||
|
self.branch.is_exact() && self.remote.is_exact()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for RemoteBranchNamePattern {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let RemoteBranchName { branch, remote } = self;
|
let RemoteBranchNamePattern { branch, remote } = self;
|
||||||
write!(f, "{branch}@{remote}")
|
write!(f, "{branch}@{remote}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -340,6 +381,44 @@ fn find_branches_with<'a, I: Iterator<Item = String>>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
fn cmd_branch_delete(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
command: &CommandHelper,
|
command: &CommandHelper,
|
||||||
|
@ -403,19 +482,15 @@ fn cmd_branch_track(
|
||||||
let mut workspace_command = command.workspace_helper(ui)?;
|
let mut workspace_command = command.workspace_helper(ui)?;
|
||||||
let view = workspace_command.repo().view();
|
let view = workspace_command.repo().view();
|
||||||
let mut names = Vec::new();
|
let mut names = Vec::new();
|
||||||
for name in &args.names {
|
for (name, remote_ref) in find_remote_branches(view, &args.names)? {
|
||||||
let remote_ref = view.get_remote_branch(&name.branch, &name.remote);
|
|
||||||
if remote_ref.is_absent() {
|
|
||||||
return Err(user_error(format!("No such remote branch: {name}")));
|
|
||||||
}
|
|
||||||
if remote_ref.is_tracking() {
|
if remote_ref.is_tracking() {
|
||||||
writeln!(ui.warning(), "Remote branch already tracked: {name}")?;
|
writeln!(ui.warning(), "Remote branch already tracked: {name}")?;
|
||||||
} else {
|
} else {
|
||||||
names.push(name.clone());
|
names.push(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut tx = workspace_command
|
let mut tx =
|
||||||
.start_transaction(&format!("track remote {}", make_branch_term(&names)));
|
workspace_command.start_transaction(&format!("track remote {}", make_branch_term(&names)));
|
||||||
for name in &names {
|
for name in &names {
|
||||||
tx.mut_repo()
|
tx.mut_repo()
|
||||||
.track_remote_branch(&name.branch, &name.remote);
|
.track_remote_branch(&name.branch, &name.remote);
|
||||||
|
@ -432,18 +507,17 @@ fn cmd_branch_untrack(
|
||||||
let mut workspace_command = command.workspace_helper(ui)?;
|
let mut workspace_command = command.workspace_helper(ui)?;
|
||||||
let view = workspace_command.repo().view();
|
let view = workspace_command.repo().view();
|
||||||
let mut names = Vec::new();
|
let mut names = Vec::new();
|
||||||
for name in &args.names {
|
for (name, remote_ref) in find_remote_branches(view, &args.names)? {
|
||||||
let remote_ref = view.get_remote_branch(&name.branch, &name.remote);
|
|
||||||
if remote_ref.is_absent() {
|
|
||||||
return Err(user_error(format!("No such remote branch: {name}")));
|
|
||||||
}
|
|
||||||
if name.remote == git::REMOTE_NAME_FOR_LOCAL_GIT_REPO {
|
if name.remote == git::REMOTE_NAME_FOR_LOCAL_GIT_REPO {
|
||||||
// This restriction can be lifted if we want to support untracked @git branches.
|
// This restriction can be lifted if we want to support untracked @git branches.
|
||||||
writeln!(ui.warning(), "Git-tracking branch cannot be untracked: {name}")?;
|
writeln!(
|
||||||
|
ui.warning(),
|
||||||
|
"Git-tracking branch cannot be untracked: {name}"
|
||||||
|
)?;
|
||||||
} else if !remote_ref.is_tracking() {
|
} else if !remote_ref.is_tracking() {
|
||||||
writeln!(ui.warning(), "Remote branch not tracked yet: {name}")?;
|
writeln!(ui.warning(), "Remote branch not tracked yet: {name}")?;
|
||||||
} else {
|
} else {
|
||||||
names.push(name.clone());
|
names.push(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let mut tx = workspace_command
|
let mut tx = workspace_command
|
||||||
|
|
|
@ -663,7 +663,7 @@ fn test_branch_track_untrack() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_branch_track_untrack_bad_branches() {
|
fn test_branch_track_untrack_patterns() {
|
||||||
let test_env = TestEnvironment::default();
|
let test_env = TestEnvironment::default();
|
||||||
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
|
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
|
||||||
let repo_path = test_env.env_root().join("repo");
|
let repo_path = test_env.env_root().join("repo");
|
||||||
|
@ -717,6 +717,17 @@ fn test_branch_track_untrack_bad_branches() {
|
||||||
test_env.jj_cmd_failure(&repo_path, &["branch", "untrack", "main@origin"]), @r###"
|
test_env.jj_cmd_failure(&repo_path, &["branch", "untrack", "main@origin"]), @r###"
|
||||||
Error: No such remote branch: main@origin
|
Error: No such remote branch: main@origin
|
||||||
"###);
|
"###);
|
||||||
|
insta::assert_snapshot!(
|
||||||
|
test_env.jj_cmd_failure(&repo_path, &["branch", "track", "glob:maine@*"]), @r###"
|
||||||
|
Error: No matching remote branches for patterns: maine@*
|
||||||
|
"###);
|
||||||
|
insta::assert_snapshot!(
|
||||||
|
test_env.jj_cmd_failure(
|
||||||
|
&repo_path,
|
||||||
|
&["branch", "untrack", "main@origin", "glob:main@o*"],
|
||||||
|
), @r###"
|
||||||
|
Error: No matching remote branches for patterns: main@origin, main@o*
|
||||||
|
"###);
|
||||||
|
|
||||||
// Track already tracked branch
|
// Track already tracked branch
|
||||||
test_env.jj_cmd_ok(&repo_path, &["branch", "track", "feature1@origin"]);
|
test_env.jj_cmd_ok(&repo_path, &["branch", "track", "feature1@origin"]);
|
||||||
|
@ -748,6 +759,35 @@ fn test_branch_track_untrack_bad_branches() {
|
||||||
main: qpvuntsm 230dd059 (empty) (no description set)
|
main: qpvuntsm 230dd059 (empty) (no description set)
|
||||||
@git: qpvuntsm 230dd059 (empty) (no description set)
|
@git: qpvuntsm 230dd059 (empty) (no description set)
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
|
// Untrack by pattern
|
||||||
|
let (_, stderr) = test_env.jj_cmd_ok(&repo_path, &["branch", "untrack", "glob:*@*"]);
|
||||||
|
insta::assert_snapshot!(stderr, @r###"
|
||||||
|
Git-tracking branch cannot be untracked: feature1@git
|
||||||
|
Remote branch not tracked yet: feature2@origin
|
||||||
|
Git-tracking branch cannot be untracked: main@git
|
||||||
|
"###);
|
||||||
|
insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###"
|
||||||
|
feature1: omvolwpu 1336caed commit
|
||||||
|
@git: omvolwpu 1336caed commit
|
||||||
|
feature1@origin: omvolwpu 1336caed commit
|
||||||
|
feature2@origin: omvolwpu 1336caed commit
|
||||||
|
main: qpvuntsm 230dd059 (empty) (no description set)
|
||||||
|
@git: qpvuntsm 230dd059 (empty) (no description set)
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Track by pattern
|
||||||
|
let (_, stderr) = test_env.jj_cmd_ok(&repo_path, &["branch", "track", "glob:feature?@origin"]);
|
||||||
|
insta::assert_snapshot!(stderr, @"");
|
||||||
|
insta::assert_snapshot!(get_branch_output(&test_env, &repo_path), @r###"
|
||||||
|
feature1: omvolwpu 1336caed commit
|
||||||
|
@git: omvolwpu 1336caed commit
|
||||||
|
@origin: omvolwpu 1336caed commit
|
||||||
|
feature2: omvolwpu 1336caed commit
|
||||||
|
@origin: omvolwpu 1336caed commit
|
||||||
|
main: qpvuntsm 230dd059 (empty) (no description set)
|
||||||
|
@git: qpvuntsm 230dd059 (empty) (no description set)
|
||||||
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
Loading…
Reference in a new issue