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 jj_lib::backend::{CommitId, ObjectId};
|
||||
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::revset::{self, RevsetExpression};
|
||||
use jj_lib::str_util::{StringPattern, StringPatternParseError};
|
||||
|
@ -136,8 +136,12 @@ pub struct BranchSetArgs {
|
|||
#[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.
|
||||
#[arg(required = true)]
|
||||
pub names: Vec<RemoteBranchName>,
|
||||
pub names: Vec<RemoteBranchNamePattern>,
|
||||
}
|
||||
|
||||
/// Stop tracking given remote branches
|
||||
|
@ -147,8 +151,12 @@ pub struct BranchTrackArgs {
|
|||
#[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.
|
||||
#[arg(required = true)]
|
||||
pub names: Vec<RemoteBranchName>,
|
||||
pub names: Vec<RemoteBranchNamePattern>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
|
@ -157,24 +165,57 @@ pub struct RemoteBranchName {
|
|||
pub remote: String,
|
||||
}
|
||||
|
||||
impl FromStr for RemoteBranchName {
|
||||
type Err = &'static str;
|
||||
impl fmt::Display for RemoteBranchName {
|
||||
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 @
|
||||
let (branch, remote) = s
|
||||
let (branch, remote) = pat
|
||||
.rsplit_once('@')
|
||||
.ok_or("remote branch must be specified in branch@remote form")?;
|
||||
Ok(RemoteBranchName {
|
||||
branch: branch.to_owned(),
|
||||
remote: remote.to_owned(),
|
||||
.ok_or_else(|| "remote branch must be specified in branch@remote form".to_owned())?;
|
||||
Ok(RemoteBranchNamePattern {
|
||||
branch: to_pattern(branch)?,
|
||||
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 {
|
||||
let RemoteBranchName { branch, remote } = self;
|
||||
let RemoteBranchNamePattern { branch, remote } = self;
|
||||
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(
|
||||
ui: &mut Ui,
|
||||
command: &CommandHelper,
|
||||
|
@ -403,19 +482,15 @@ fn cmd_branch_track(
|
|||
let mut workspace_command = command.workspace_helper(ui)?;
|
||||
let view = workspace_command.repo().view();
|
||||
let mut names = Vec::new();
|
||||
for name in &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}")));
|
||||
}
|
||||
for (name, remote_ref) in find_remote_branches(view, &args.names)? {
|
||||
if remote_ref.is_tracking() {
|
||||
writeln!(ui.warning(), "Remote branch already tracked: {name}")?;
|
||||
} else {
|
||||
names.push(name.clone());
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
let mut tx = workspace_command
|
||||
.start_transaction(&format!("track remote {}", make_branch_term(&names)));
|
||||
let mut tx =
|
||||
workspace_command.start_transaction(&format!("track remote {}", make_branch_term(&names)));
|
||||
for name in &names {
|
||||
tx.mut_repo()
|
||||
.track_remote_branch(&name.branch, &name.remote);
|
||||
|
@ -432,18 +507,17 @@ fn cmd_branch_untrack(
|
|||
let mut workspace_command = command.workspace_helper(ui)?;
|
||||
let view = workspace_command.repo().view();
|
||||
let mut names = Vec::new();
|
||||
for name in &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}")));
|
||||
}
|
||||
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(), "Git-tracking branch cannot be untracked: {name}")?;
|
||||
writeln!(
|
||||
ui.warning(),
|
||||
"Git-tracking branch cannot be untracked: {name}"
|
||||
)?;
|
||||
} else if !remote_ref.is_tracking() {
|
||||
writeln!(ui.warning(), "Remote branch not tracked yet: {name}")?;
|
||||
} else {
|
||||
names.push(name.clone());
|
||||
names.push(name);
|
||||
}
|
||||
}
|
||||
let mut tx = workspace_command
|
||||
|
|
|
@ -663,7 +663,7 @@ fn test_branch_track_untrack() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_branch_track_untrack_bad_branches() {
|
||||
fn test_branch_track_untrack_patterns() {
|
||||
let test_env = TestEnvironment::default();
|
||||
test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]);
|
||||
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###"
|
||||
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
|
||||
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)
|
||||
@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]
|
||||
|
|
Loading…
Reference in a new issue