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

cli: add "branch list [NAMES]..." filter

Like "jj log PATHS...", unmatched name isn't an error. I don't think
"jj branch list glob:'push-*'" should fail just because there are no in-flight
PR branches.
This commit is contained in:
Yuya Nishihara 2023-11-01 12:01:17 +09:00 committed by Martin von Zweigbergk
parent 749d8bb15a
commit f89f2f9e7d
3 changed files with 87 additions and 36 deletions

View file

@ -61,9 +61,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* `branches()`/`remote_branches()`/`author()`/`committer()`/`description()` * `branches()`/`remote_branches()`/`author()`/`committer()`/`description()`
revsets now support glob matching. revsets now support glob matching.
* `jj branch delete`/`forget`, and `jj git push --branch` now support [string * `jj branch delete`/`forget`/`list`, and `jj git push --branch` now support
pattern syntax](docs/revsets.md#string-patterns). The `--glob` option is [string pattern syntax](docs/revsets.md#string-patterns). The `--glob` option
deprecated in favor of `glob:` pattern. is deprecated in favor of `glob:` pattern.
* The `branches`/`tags`/`git_refs`/`git_head` template keywords now return a * The `branches`/`tags`/`git_refs`/`git_head` template keywords now return a
list of `RefName`s. They were previously pre-formatted strings. list of `RefName`s. They were previously pre-formatted strings.

View file

@ -84,9 +84,17 @@ pub struct BranchDeleteArgs {
pub struct BranchListArgs { pub struct BranchListArgs {
/// Show all tracking and non-tracking remote branches including the ones /// Show all tracking and non-tracking remote branches including the ones
/// whose targets are synchronized with the local branches. /// whose targets are synchronized with the local branches.
#[arg(long, short, conflicts_with = "revisions")] #[arg(long, short, conflicts_with_all = ["names", "revisions"])]
all: bool, all: 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 = parse_string_pattern)]
pub names: Vec<StringPattern>,
/// Show branches whose local targets are in the given revisions. /// Show branches whose local targets are in the given revisions.
/// ///
/// Note that `-r deleted_branch` will not work since `deleted_branch` /// Note that `-r deleted_branch` will not work since `deleted_branch`
@ -547,29 +555,39 @@ fn cmd_branch_list(
let workspace_command = command.workspace_helper(ui)?; let workspace_command = command.workspace_helper(ui)?;
let repo = workspace_command.repo(); let repo = workspace_command.repo();
let view = repo.view(); let view = repo.view();
let branch_names_to_list: Option<HashSet<&str>> = if !args.revisions.is_empty() {
// Match against local targets only, which is consistent with "jj git push". // Like cmd_git_push(), names and revisions are OR-ed.
let filter_expressions: Vec<_> = args let branch_names_to_list = if !args.names.is_empty() || !args.revisions.is_empty() {
.revisions let mut branch_names: HashSet<&str> = HashSet::new();
.iter() if !args.names.is_empty() {
.map(|revision_str| workspace_command.parse_revset(revision_str, Some(ui))) branch_names.extend(
.try_collect()?; view.branches()
let filter_expression = RevsetExpression::union_all(&filter_expressions); .filter(|&(name, _)| args.names.iter().any(|pattern| pattern.matches(name)))
// Intersects with the set of local branch targets to minimize the lookup space. .map(|(name, _)| name),
let revset_expression = RevsetExpression::branches(StringPattern::everything()) );
.intersection(&filter_expression); }
let revset_expression = revset::optimize(revset_expression); if !args.revisions.is_empty() {
let revset = workspace_command.evaluate_revset(revset_expression)?; // Match against local targets only, which is consistent with "jj git push".
let filtered_targets: HashSet<CommitId> = revset.iter().collect(); let filter_expressions: Vec<_> = args
// TODO: Suppose we have name-based filter like --glob, should these filters .revisions
// be AND-ed or OR-ed? Maybe OR as "jj git push" would do. Perhaps, we .iter()
// can consider these options as producers of branch names, not filters .map(|revision_str| workspace_command.parse_revset(revision_str, Some(ui)))
// of different kind (which are typically intersected.) .try_collect()?;
let branch_names = view let filter_expression = RevsetExpression::union_all(&filter_expressions);
.local_branches() // Intersects with the set of local branch targets to minimize the lookup space.
.filter(|(_, target)| target.added_ids().any(|id| filtered_targets.contains(id))) let revset_expression = RevsetExpression::branches(StringPattern::everything())
.map(|(name, _)| name) .intersection(&filter_expression);
.collect(); let revset_expression = revset::optimize(revset_expression);
let revset = workspace_command.evaluate_revset(revset_expression)?;
let filtered_targets: HashSet<CommitId> = revset.iter().collect();
branch_names.extend(
view.local_branches()
.filter(|(_, target)| {
target.added_ids().any(|id| filtered_targets.contains(id))
})
.map(|(name, _)| name),
);
}
Some(branch_names) Some(branch_names)
} else { } else {
None None

View file

@ -861,7 +861,7 @@ fn test_branch_list() {
} }
#[test] #[test]
fn test_branch_list_filtered_by_revset() { fn test_branch_list_filtered() {
let test_env = TestEnvironment::default(); let test_env = TestEnvironment::default();
test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#); test_env.add_config(r#"revset-aliases."immutable_heads()" = "none()""#);
@ -921,13 +921,14 @@ fn test_branch_list_filtered_by_revset() {
@origin (ahead by 1 commits, behind by 1 commits): xyxluytn hidden 3e9a5af6 (empty) remote-rewrite @origin (ahead by 1 commits, behind by 1 commits): xyxluytn hidden 3e9a5af6 (empty) remote-rewrite
"###); "###);
let query = |revset| test_env.jj_cmd_success(&local_path, &["branch", "list", "-r", revset]); let query =
|args: &[&str]| test_env.jj_cmd_success(&local_path, &[&["branch", "list"], args].concat());
let query_error = let query_error =
|revset| test_env.jj_cmd_failure(&local_path, &["branch", "list", "-r", revset]); |args: &[&str]| test_env.jj_cmd_failure(&local_path, &[&["branch", "list"], args].concat());
// "all()" doesn't include deleted branches since they have no local targets. // "all()" doesn't include deleted branches since they have no local targets.
// So "all()" is identical to "branches()". // So "all()" is identical to "branches()".
insta::assert_snapshot!(query("all()"), @r###" insta::assert_snapshot!(query(&["-rall()"]), @r###"
local-keep: kpqxywon c7b4c09c (empty) local-keep local-keep: kpqxywon c7b4c09c (empty) local-keep
remote-keep: nlwprzpn 911e9120 (empty) remote-keep remote-keep: nlwprzpn 911e9120 (empty) remote-keep
remote-rewrite: xyxluytn e31634b6 (empty) rewritten remote-rewrite: xyxluytn e31634b6 (empty) rewritten
@ -936,7 +937,7 @@ fn test_branch_list_filtered_by_revset() {
// Exclude remote-only branches. "remote-rewrite@origin" is included since // Exclude remote-only branches. "remote-rewrite@origin" is included since
// local "remote-rewrite" target matches. // local "remote-rewrite" target matches.
insta::assert_snapshot!(query("branches()"), @r###" insta::assert_snapshot!(query(&["-rbranches()"]), @r###"
local-keep: kpqxywon c7b4c09c (empty) local-keep local-keep: kpqxywon c7b4c09c (empty) local-keep
remote-keep: nlwprzpn 911e9120 (empty) remote-keep remote-keep: nlwprzpn 911e9120 (empty) remote-keep
remote-rewrite: xyxluytn e31634b6 (empty) rewritten remote-rewrite: xyxluytn e31634b6 (empty) rewritten
@ -944,18 +945,50 @@ fn test_branch_list_filtered_by_revset() {
"###); "###);
// Select branches by name. // Select branches by name.
insta::assert_snapshot!(query("branches(remote-rewrite)"), @r###" insta::assert_snapshot!(query(&["remote-rewrite"]), @r###"
remote-rewrite: xyxluytn e31634b6 (empty) rewritten
@origin (ahead by 1 commits, behind by 1 commits): xyxluytn hidden 3e9a5af6 (empty) remote-rewrite
"###);
insta::assert_snapshot!(query(&["-rbranches(remote-rewrite)"]), @r###"
remote-rewrite: xyxluytn e31634b6 (empty) rewritten remote-rewrite: xyxluytn e31634b6 (empty) rewritten
@origin (ahead by 1 commits, behind by 1 commits): xyxluytn hidden 3e9a5af6 (empty) remote-rewrite @origin (ahead by 1 commits, behind by 1 commits): xyxluytn hidden 3e9a5af6 (empty) remote-rewrite
"###); "###);
// Can't select deleted branch. // Can select deleted branch by name pattern, but not by revset.
insta::assert_snapshot!(query("branches(remote-delete)"), @r###" insta::assert_snapshot!(query(&["remote-delete"]), @r###"
remote-delete (deleted)
@origin: yxusvupt dad5f298 (empty) remote-delete
(this branch will be *deleted permanently* on the remote on the
next `jj git push`. Use `jj branch forget` to prevent this)
"###); "###);
insta::assert_snapshot!(query_error("remote-delete"), @r###" insta::assert_snapshot!(query(&["-rbranches(remote-delete)"]), @r###"
"###);
insta::assert_snapshot!(query_error(&["-rremote-delete"]), @r###"
Error: Revision "remote-delete" doesn't exist Error: Revision "remote-delete" doesn't exist
Hint: Did you mean "remote-delete@origin", "remote-keep", "remote-rewrite", "remote-rewrite@origin"? Hint: Did you mean "remote-delete@origin", "remote-keep", "remote-rewrite", "remote-rewrite@origin"?
"###); "###);
// Name patterns are OR-ed.
insta::assert_snapshot!(query(&["glob:*-keep", "remote-delete"]), @r###"
local-keep: kpqxywon c7b4c09c (empty) local-keep
remote-delete (deleted)
@origin: yxusvupt dad5f298 (empty) remote-delete
(this branch will be *deleted permanently* on the remote on the
next `jj git push`. Use `jj branch forget` to prevent this)
remote-keep: nlwprzpn 911e9120 (empty) remote-keep
"###);
// Unmatched name pattern shouldn't be an error. A warning can be added later.
insta::assert_snapshot!(query(&["local-keep", "glob:push-*"]), @r###"
local-keep: kpqxywon c7b4c09c (empty) local-keep
"###);
// Name pattern and revset are OR-ed.
insta::assert_snapshot!(query(&["local-keep", "-rbranches(remote-rewrite)"]), @r###"
local-keep: kpqxywon c7b4c09c (empty) local-keep
remote-rewrite: xyxluytn e31634b6 (empty) rewritten
@origin (ahead by 1 commits, behind by 1 commits): xyxluytn hidden 3e9a5af6 (empty) remote-rewrite
"###);
} }
fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String { fn get_log_output(test_env: &TestEnvironment, cwd: &Path) -> String {