ok/jj
1
0
Fork 0
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:
Yuya Nishihara 2023-10-24 15:31:26 +09:00
parent fcd02a6091
commit 1bfe5b5b56
4 changed files with 102 additions and 19 deletions

View file

@ -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

View file

@ -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,

View file

@ -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
"###);
}

View file

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