forked from mirrors/jj
cli: add string pattern support to "git push --branch"
Since "jj git fetch --branch" supports glob patterns, users would expect that "jj git push --branch glob:.." also works. The error handling bits are copied from "branch" sub commands. We might want to extract it to a common helper function, but I haven't figured out a reasonable boundary point yet.
This commit is contained in:
parent
fcd02a6091
commit
1bfe5b5b56
4 changed files with 102 additions and 19 deletions
|
@ -59,9 +59,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
* `branches()`/`remote_branches()`/`author()`/`committer()`/`description()`
|
||||
revsets now support glob matching.
|
||||
|
||||
* `jj branch delete`/`forget` now support [string pattern
|
||||
syntax](docs/revsets.md#string-patterns). The `--glob` option is deprecated in
|
||||
favor of `glob:` pattern.
|
||||
* `jj branch delete`/`forget`, and `jj git push --branch` now support [string
|
||||
pattern syntax](docs/revsets.md#string-patterns). The `--glob` option is
|
||||
deprecated in favor of `glob:` pattern.
|
||||
|
||||
### Fixed bugs
|
||||
|
||||
|
|
|
@ -24,6 +24,7 @@ use jj_lib::revset::{self, RevsetExpression, RevsetIteratorExt as _};
|
|||
use jj_lib::settings::{ConfigResultExt as _, UserSettings};
|
||||
use jj_lib::store::Store;
|
||||
use jj_lib::str_util::StringPattern;
|
||||
use jj_lib::view::View;
|
||||
use jj_lib::workspace::Workspace;
|
||||
use maplit::hashset;
|
||||
|
||||
|
@ -141,8 +142,12 @@ pub struct GitPushArgs {
|
|||
#[arg(long)]
|
||||
remote: Option<String>,
|
||||
/// Push only this branch (can be repeated)
|
||||
#[arg(long, short)]
|
||||
branch: Vec<String>,
|
||||
///
|
||||
/// 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(long, short, value_parser = parse_string_pattern)]
|
||||
branch: Vec<StringPattern>,
|
||||
/// Push all branches (including deleted branches)
|
||||
#[arg(long)]
|
||||
all: bool,
|
||||
|
@ -716,19 +721,11 @@ fn cmd_git_push(
|
|||
tx_description = format!("push all deleted branches to git remote {remote}");
|
||||
} else {
|
||||
let mut seen_branches = hashset! {};
|
||||
for branch_name in &args.branch {
|
||||
if !seen_branches.insert(branch_name.clone()) {
|
||||
continue;
|
||||
}
|
||||
let targets = TrackingRefPair {
|
||||
local_target: repo.view().get_local_branch(branch_name),
|
||||
remote_ref: repo.view().get_remote_branch(branch_name, &remote),
|
||||
};
|
||||
let branches_by_name =
|
||||
find_branches_to_push(repo.view(), &args.branch, &remote, &mut seen_branches)?;
|
||||
for (branch_name, targets) in branches_by_name {
|
||||
match classify_branch_update(branch_name, &remote, targets) {
|
||||
Ok(Some(update)) => branch_updates.push((branch_name.clone(), update)),
|
||||
Ok(None) if targets.local_target.is_absent() => {
|
||||
return Err(user_error(format!("Branch {branch_name} doesn't exist")));
|
||||
}
|
||||
Ok(Some(update)) => branch_updates.push((branch_name.to_owned(), update)),
|
||||
Ok(None) => writeln!(
|
||||
ui.stderr(),
|
||||
"Branch {branch_name}@{remote} already matches {branch_name}",
|
||||
|
@ -1047,6 +1044,39 @@ fn classify_branch_update(
|
|||
}
|
||||
}
|
||||
|
||||
fn find_branches_to_push<'a>(
|
||||
view: &'a View,
|
||||
branch_patterns: &[StringPattern],
|
||||
remote_name: &str,
|
||||
seen_branches: &mut HashSet<String>,
|
||||
) -> Result<Vec<(&'a str, TrackingRefPair<'a>)>, CommandError> {
|
||||
let mut matching_branches = vec![];
|
||||
let mut unmatched_patterns = vec![];
|
||||
for pattern in branch_patterns {
|
||||
let mut matches = view
|
||||
.local_remote_branches_matching(pattern, remote_name)
|
||||
.filter(|(_, targets)| {
|
||||
// If the remote exists but is not tracking, the absent local shouldn't
|
||||
// be considered a deleted branch.
|
||||
targets.local_target.is_present() || targets.remote_ref.is_tracking()
|
||||
})
|
||||
.peekable();
|
||||
if matches.peek().is_none() {
|
||||
unmatched_patterns.push(pattern);
|
||||
}
|
||||
matching_branches
|
||||
.extend(matches.filter(|&(name, _)| seen_branches.insert(name.to_owned())));
|
||||
}
|
||||
match &unmatched_patterns[..] {
|
||||
[] => 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 cmd_git_import(
|
||||
ui: &mut Ui,
|
||||
command: &CommandHelper,
|
||||
|
|
|
@ -318,7 +318,7 @@ fn test_git_push_multiple() {
|
|||
"-b=branch1",
|
||||
"-b=my-branch",
|
||||
"-b=branch1",
|
||||
"-b=my-branch",
|
||||
"-b=glob:my-*",
|
||||
"--dry-run",
|
||||
],
|
||||
);
|
||||
|
@ -329,6 +329,32 @@ fn test_git_push_multiple() {
|
|||
Add branch my-branch to 15dcdaa4f12f
|
||||
Dry-run requested, not pushing.
|
||||
"###);
|
||||
// Dry run with glob pattern
|
||||
let (stdout, stderr) = test_env.jj_cmd_ok(
|
||||
&workspace_root,
|
||||
&["git", "push", "-b=glob:branch?", "--dry-run"],
|
||||
);
|
||||
insta::assert_snapshot!(stdout, @"");
|
||||
insta::assert_snapshot!(stderr, @r###"
|
||||
Branch changes to push to origin:
|
||||
Delete branch branch1 from 45a3aa29e907
|
||||
Force branch branch2 from 8476341eb395 to 15dcdaa4f12f
|
||||
Dry-run requested, not pushing.
|
||||
"###);
|
||||
|
||||
// Unmatched branch name is error
|
||||
let stderr = test_env.jj_cmd_failure(&workspace_root, &["git", "push", "-b=foo"]);
|
||||
insta::assert_snapshot!(stderr, @r###"
|
||||
Error: No such branch: foo
|
||||
"###);
|
||||
let stderr = test_env.jj_cmd_failure(
|
||||
&workspace_root,
|
||||
&["git", "push", "-b=foo", "-b=glob:?branch"],
|
||||
);
|
||||
insta::assert_snapshot!(stderr, @r###"
|
||||
Error: No matching branches for patterns: foo, ?branch
|
||||
"###);
|
||||
|
||||
let (stdout, stderr) = test_env.jj_cmd_ok(&workspace_root, &["git", "push", "--all"]);
|
||||
insta::assert_snapshot!(stdout, @"");
|
||||
insta::assert_snapshot!(stderr, @r###"
|
||||
|
@ -740,7 +766,7 @@ fn test_git_push_deleted_untracked() {
|
|||
"###);
|
||||
let stderr = test_env.jj_cmd_failure(&workspace_root, &["git", "push", "--branch=branch1"]);
|
||||
insta::assert_snapshot!(stderr, @r###"
|
||||
Error: Branch branch1 doesn't exist
|
||||
Error: No such branch: branch1
|
||||
"###);
|
||||
}
|
||||
|
||||
|
|
|
@ -240,6 +240,33 @@ impl View {
|
|||
})
|
||||
}
|
||||
|
||||
/// Iterates local/remote branch `(name, remote_ref)`s of the specified
|
||||
/// remote, matching the given branch name pattern. Entries are sorted by
|
||||
/// `name`.
|
||||
pub fn local_remote_branches_matching<'a: 'b, 'b>(
|
||||
&'a self,
|
||||
branch_pattern: &'b StringPattern,
|
||||
remote_name: &str,
|
||||
) -> impl Iterator<Item = (&'a str, TrackingRefPair<'a>)> + 'b {
|
||||
// Change remote_name to StringPattern if needed, but merge-join adapter won't
|
||||
// be usable.
|
||||
let maybe_remote_view = self.data.remote_views.get(remote_name);
|
||||
refs::iter_named_local_remote_refs(
|
||||
branch_pattern.filter_btree_map(&self.data.local_branches),
|
||||
maybe_remote_view
|
||||
.map(|remote_view| branch_pattern.filter_btree_map(&remote_view.branches))
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.map(|(name, (local_target, remote_ref))| {
|
||||
let targets = TrackingRefPair {
|
||||
local_target,
|
||||
remote_ref,
|
||||
};
|
||||
(name.as_ref(), targets)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn remove_remote(&mut self, remote_name: &str) {
|
||||
self.data.remote_views.remove(remote_name);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue