let branches and remote_branches revset functions take needles as arguments

- branches has the signature branches([needle]), meaning the needle is optional (branches() is equivalent to branches("")) and it matches all branches whose name contains needle as a substring
- remote_branches has the signature remote_branches([branch_needle[, remote_needle]]), meaning it can be called with no arguments, or one argument (in which case, it's similar to branches), or two arguments where the first argument matches branch names and the second argument matches remote names (similar to branches, remote_branches(), remote_branches("") and remote_branches("", "") are all equivalent)
This commit is contained in:
Vamsi Avula 2023-01-12 15:31:35 +05:30 committed by Vamsi Avula
parent 24ccd80a5c
commit 60d1537731
4 changed files with 202 additions and 46 deletions

View File

@ -52,6 +52,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
* The `empty` condition in templates is true when the commit makes no change to * The `empty` condition in templates is true when the commit makes no change to
the three compared to its parents. the three compared to its parents.
* `branches([needle])` revset function now takes `needle` as an optional
argument and matches just the branches whose name contains `needle`.
* `remote_branches([branch_needle[, remote_needle]])` now takes `branch_needle`
and `remote_needle` as optional arguments and matches just the branches whose
name contains `branch_needle` and remote contains `remote_needle`.
### Fixed bugs ### Fixed bugs
* When sharing the working copy with a Git repo, we used to forget to export * When sharing the working copy with a Git repo, we used to forget to export

View File

@ -16,7 +16,6 @@ The commits listed by `jj log` without arguments are called "visible commits".
Other commits are only included if you explicitly mention them (e.g. by commit Other commits are only included if you explicitly mention them (e.g. by commit
ID or a Git ref pointing to them). ID or a Git ref pointing to them).
## Symbols ## Symbols
The symbol `root` refers to the virtual commit that is the oldest ancestor of The symbol `root` refers to the virtual commit that is the oldest ancestor of
@ -48,7 +47,6 @@ Jujutsu attempts to resolve a symbol in the following order:
5. Git ref 5. Git ref
6. Commit ID or change ID 6. Commit ID or change ID
## Operators ## Operators
The following operators are supported. `x` and `y` below can be any revset, not The following operators are supported. `x` and `y` below can be any revset, not
@ -74,7 +72,6 @@ only symbols.
You can use parentheses to control evaluation order, such as `(x & y) | z` or You can use parentheses to control evaluation order, such as `(x & y) | z` or
`x & (y | z)`. `x & (y | z)`.
## Functions ## Functions
You can also specify revisions by using functions. Some functions take other You can also specify revisions by using functions. Some functions take other
@ -88,10 +85,20 @@ revsets (expressions) as arguments.
* `all()`: All visible commits in the repo. * `all()`: All visible commits in the repo.
* `none()`: No commits. This function is rarely useful; it is provided for * `none()`: No commits. This function is rarely useful; it is provided for
completeness. completeness.
* `branches()`: All local branch targets. If a branch is in a conflicted state, * `branches([needle])`: All local branch targets. If `needle` is specified,
branches whose name contains the given string are selected. For example,
`branches(push)` would match the branches `push-123` and `repushed` but not
the branch `main`. If a branch is in a conflicted state, all its possible
targets are included.
* `remote_branches([branch_needle[, remote_needle]])`: All remote branch
targets across all remotes. If just the `branch_needle` is specificed,
branches whose name contains the given string across all remotes are
selected. If both `branch_needle` and `remote_needle` are specified, the
selection is further restricted to just the remotes whose name contains
`remote_needle`. For example, `remote_branches(push, ri)` would match the
branches `push-123@origin` and `repushed@private` but not `push-123@upstream`
or `main@origin` or `main@upstream`. If a branch is in a conflicted state,
all its possible targets are included. all its possible targets are included.
* `remote_branches()`: All remote branch targets across all remotes. If a
branch is in a conflicted state, all its possible targets are included.
* `tags()`: All tag targets. If a tag is in a conflicted state, all its * `tags()`: All tag targets. If a tag is in a conflicted state, all its
possible targets are included. possible targets are included.
* `git_refs()`: All Git ref targets as of the last import. If a Git ref * `git_refs()`: All Git ref targets as of the last import. If a Git ref
@ -114,55 +121,67 @@ revsets (expressions) as arguments.
* `present(x)`: Same as `x`, but evaluated to `none()` if any of the commits * `present(x)`: Same as `x`, but evaluated to `none()` if any of the commits
in `x` doesn't exist (e.g. is an unknown branch name.) in `x` doesn't exist (e.g. is an unknown branch name.)
## Aliases ## Aliases
New symbols and functions can be defined in the config file, by using any New symbols and functions can be defined in the config file, by using any
combination of the predefined symbols/functions and other aliases. combination of the predefined symbols/functions and other aliases.
For example: For example:
```toml ```toml
[revset-aliases] [revset-aliases]
'mine' = 'author(martinvonz)' 'mine' = 'author(martinvonz)'
'user(x)' = 'author(x) | committer(x)' 'user(x)' = 'author(x) | committer(x)'
``` ```
## Examples ## Examples
Show the parent(s) of the working-copy commit (like `git log -1 HEAD`): Show the parent(s) of the working-copy commit (like `git log -1 HEAD`):
``` ```
jj log -r @- jj log -r @-
``` ```
Show commits not on any remote branch: Show commits not on any remote branch:
``` ```
jj log -r 'remote_branches()..' jj log -r 'remote_branches()..'
``` ```
Show commits not on `origin` (if you have other remotes like `fork`):
```
jj log -r 'remote_branches("", origin)..'
```
Show all ancestors of the working copy (almost like plain `git log`) Show all ancestors of the working copy (almost like plain `git log`)
``` ```
jj log -r :@ jj log -r :@
``` ```
Show the initial commits in the repo (the ones Git calls "root commits"): Show the initial commits in the repo (the ones Git calls "root commits"):
``` ```
jj log -r root+ jj log -r root+
``` ```
Show some important commits (like `git --simplify-by-decoration`): Show some important commits (like `git --simplify-by-decoration`):
``` ```
jj log -r 'tags() | branches()' jj log -r 'tags() | branches()'
``` ```
Show local commits leading up to the working copy, as well as descendants of Show local commits leading up to the working copy, as well as descendants of
those commits: those commits:
``` ```
jj log -r '(remote_branches()..@):' jj log -r '(remote_branches()..@):'
``` ```
Show commits authored by "martinvonz" and containing the word "reset" in the Show commits authored by "martinvonz" and containing the word "reset" in the
description: description:
``` ```
jj log -r 'author(martinvonz) & description(reset)' jj log -r 'author(martinvonz) & description(reset)'
``` ```

View File

@ -378,8 +378,11 @@ pub enum RevsetExpression {
Roots(Rc<RevsetExpression>), Roots(Rc<RevsetExpression>),
VisibleHeads, VisibleHeads,
PublicHeads, PublicHeads,
Branches, Branches(String),
RemoteBranches, RemoteBranches {
branch_needle: String,
remote_needle: String,
},
Tags, Tags,
GitRefs, GitRefs,
GitHead, GitHead,
@ -422,12 +425,15 @@ impl RevsetExpression {
Rc::new(RevsetExpression::PublicHeads) Rc::new(RevsetExpression::PublicHeads)
} }
pub fn branches() -> Rc<RevsetExpression> { pub fn branches(needle: String) -> Rc<RevsetExpression> {
Rc::new(RevsetExpression::Branches) Rc::new(RevsetExpression::Branches(needle))
} }
pub fn remote_branches() -> Rc<RevsetExpression> { pub fn remote_branches(branch_needle: String, remote_needle: String) -> Rc<RevsetExpression> {
Rc::new(RevsetExpression::RemoteBranches) Rc::new(RevsetExpression::RemoteBranches {
branch_needle,
remote_needle,
})
} }
pub fn tags() -> Rc<RevsetExpression> { pub fn tags() -> Rc<RevsetExpression> {
@ -934,12 +940,31 @@ fn parse_builtin_function(
Ok(RevsetExpression::public_heads()) Ok(RevsetExpression::public_heads())
} }
"branches" => { "branches" => {
expect_no_arguments(name, arguments_pair)?; let opt_arg = expect_one_optional_argument(name, arguments_pair)?;
Ok(RevsetExpression::branches()) let needle = if let Some(arg) = opt_arg {
parse_function_argument_to_string(name, arg, state)?
} else {
"".to_owned()
};
Ok(RevsetExpression::branches(needle))
} }
"remote_branches" => { "remote_branches" => {
expect_no_arguments(name, arguments_pair)?; let (branch_opt_arg, remote_opt_arg) =
Ok(RevsetExpression::remote_branches()) expect_two_optional_argument(name, arguments_pair)?;
let branch_needle = if let Some(branch_arg) = branch_opt_arg {
parse_function_argument_to_string(name, branch_arg, state)?
} else {
"".to_owned()
};
let remote_needle = if let Some(remote_arg) = remote_opt_arg {
parse_function_argument_to_string(name, remote_arg, state)?
} else {
"".to_owned()
};
Ok(RevsetExpression::remote_branches(
branch_needle,
remote_needle,
))
} }
"tags" => { "tags" => {
expect_no_arguments(name, arguments_pair)?; expect_no_arguments(name, arguments_pair)?;
@ -1068,10 +1093,12 @@ fn expect_one_argument<'i>(
} }
} }
type OptionalArg<'i> = Option<Pair<'i, Rule>>;
fn expect_one_optional_argument<'i>( fn expect_one_optional_argument<'i>(
name: &str, name: &str,
arguments_pair: Pair<'i, Rule>, arguments_pair: Pair<'i, Rule>,
) -> Result<Option<Pair<'i, Rule>>, RevsetParseError> { ) -> Result<OptionalArg<'i>, RevsetParseError> {
let span = arguments_pair.as_span(); let span = arguments_pair.as_span();
let mut argument_pairs = arguments_pair.into_inner().fuse(); let mut argument_pairs = arguments_pair.into_inner().fuse();
if let (opt_arg, None) = (argument_pairs.next(), argument_pairs.next()) { if let (opt_arg, None) = (argument_pairs.next(), argument_pairs.next()) {
@ -1087,6 +1114,29 @@ fn expect_one_optional_argument<'i>(
} }
} }
fn expect_two_optional_argument<'i>(
name: &str,
arguments_pair: Pair<'i, Rule>,
) -> Result<(OptionalArg<'i>, OptionalArg<'i>), RevsetParseError> {
let span = arguments_pair.as_span();
let mut argument_pairs = arguments_pair.into_inner().fuse();
if let (opt_arg1, opt_arg2, None) = (
argument_pairs.next(),
argument_pairs.next(),
argument_pairs.next(),
) {
Ok((opt_arg1, opt_arg2))
} else {
Err(RevsetParseError::with_span(
RevsetParseErrorKind::InvalidFunctionArguments {
name: name.to_owned(),
message: "Expected 0 to 2 arguments".to_string(),
},
span,
))
}
}
fn parse_function_argument_to_string( fn parse_function_argument_to_string(
name: &str, name: &str,
pair: Pair<Rule>, pair: Pair<Rule>,
@ -1168,8 +1218,8 @@ fn transform_expression_bottom_up(
transform_rec(candidates, f).map(RevsetExpression::Roots) transform_rec(candidates, f).map(RevsetExpression::Roots)
} }
RevsetExpression::PublicHeads => None, RevsetExpression::PublicHeads => None,
RevsetExpression::Branches => None, RevsetExpression::Branches(_) => None,
RevsetExpression::RemoteBranches => None, RevsetExpression::RemoteBranches { .. } => None,
RevsetExpression::Tags => None, RevsetExpression::Tags => None,
RevsetExpression::GitRefs => None, RevsetExpression::GitRefs => None,
RevsetExpression::GitHead => None, RevsetExpression::GitHead => None,
@ -1957,22 +2007,33 @@ pub fn evaluate_expression<'repo>(
repo, repo,
&repo.view().public_heads().iter().cloned().collect_vec(), &repo.view().public_heads().iter().cloned().collect_vec(),
)), )),
RevsetExpression::Branches => { RevsetExpression::Branches(needle) => {
let mut commit_ids = vec![]; let mut commit_ids = vec![];
for branch_target in repo.view().branches().values() { for (branch_name, branch_target) in repo.view().branches() {
if !branch_name.contains(needle) {
continue;
}
if let Some(local_target) = &branch_target.local_target { if let Some(local_target) = &branch_target.local_target {
commit_ids.extend(local_target.adds()); commit_ids.extend(local_target.adds());
} }
} }
Ok(revset_for_commit_ids(repo, &commit_ids)) Ok(revset_for_commit_ids(repo, &commit_ids))
} }
RevsetExpression::RemoteBranches => { RevsetExpression::RemoteBranches {
branch_needle,
remote_needle,
} => {
let mut commit_ids = vec![]; let mut commit_ids = vec![];
for branch_target in repo.view().branches().values() { for (branch_name, branch_target) in repo.view().branches() {
for remote_target in branch_target.remote_targets.values() { if !branch_name.contains(branch_needle) {
continue;
}
for (remote_name, remote_target) in branch_target.remote_targets.iter() {
if remote_name.contains(remote_needle) {
commit_ids.extend(remote_target.adds()); commit_ids.extend(remote_target.adds());
} }
} }
}
Ok(revset_for_commit_ids(repo, &commit_ids)) Ok(revset_for_commit_ids(repo, &commit_ids))
} }
RevsetExpression::Tags => { RevsetExpression::Tags => {
@ -2283,6 +2344,13 @@ mod tests {
parse("foo.bar-v1+7-"), parse("foo.bar-v1+7-"),
Ok(RevsetExpression::symbol("foo.bar-v1+7".to_string()).parents()) Ok(RevsetExpression::symbol("foo.bar-v1+7".to_string()).parents())
); );
// Default arguments for *branches() are all ""
assert_eq!(parse("branches()"), parse(r#"branches("")"#));
assert_eq!(parse("remote_branches()"), parse(r#"remote_branches("")"#));
assert_eq!(
parse("remote_branches()"),
parse(r#"remote_branches("", "")"#)
);
// '.' is not allowed at the beginning or end // '.' is not allowed at the beginning or end
assert_eq!(parse(".foo"), Err(RevsetParseErrorKind::SyntaxError)); assert_eq!(parse(".foo"), Err(RevsetParseErrorKind::SyntaxError));
assert_eq!(parse("foo."), Err(RevsetParseErrorKind::SyntaxError)); assert_eq!(parse("foo."), Err(RevsetParseErrorKind::SyntaxError));
@ -2643,37 +2711,37 @@ mod tests {
assert_eq!( assert_eq!(
optimize(parse("parents(branches() & all())").unwrap()), optimize(parse("parents(branches() & all())").unwrap()),
RevsetExpression::branches().parents() RevsetExpression::branches("".to_owned()).parents()
); );
assert_eq!( assert_eq!(
optimize(parse("children(branches() & all())").unwrap()), optimize(parse("children(branches() & all())").unwrap()),
RevsetExpression::branches().children() RevsetExpression::branches("".to_owned()).children()
); );
assert_eq!( assert_eq!(
optimize(parse("ancestors(branches() & all())").unwrap()), optimize(parse("ancestors(branches() & all())").unwrap()),
RevsetExpression::branches().ancestors() RevsetExpression::branches("".to_owned()).ancestors()
); );
assert_eq!( assert_eq!(
optimize(parse("descendants(branches() & all())").unwrap()), optimize(parse("descendants(branches() & all())").unwrap()),
RevsetExpression::branches().descendants() RevsetExpression::branches("".to_owned()).descendants()
); );
assert_eq!( assert_eq!(
optimize(parse("(branches() & all())..(all() & tags())").unwrap()), optimize(parse("(branches() & all())..(all() & tags())").unwrap()),
RevsetExpression::branches().range(&RevsetExpression::tags()) RevsetExpression::branches("".to_owned()).range(&RevsetExpression::tags())
); );
assert_eq!( assert_eq!(
optimize(parse("(branches() & all()):(all() & tags())").unwrap()), optimize(parse("(branches() & all()):(all() & tags())").unwrap()),
RevsetExpression::branches().dag_range_to(&RevsetExpression::tags()) RevsetExpression::branches("".to_owned()).dag_range_to(&RevsetExpression::tags())
); );
assert_eq!( assert_eq!(
optimize(parse("heads(branches() & all())").unwrap()), optimize(parse("heads(branches() & all())").unwrap()),
RevsetExpression::branches().heads() RevsetExpression::branches("".to_owned()).heads()
); );
assert_eq!( assert_eq!(
optimize(parse("roots(branches() & all())").unwrap()), optimize(parse("roots(branches() & all())").unwrap()),
RevsetExpression::branches().roots() RevsetExpression::branches("".to_owned()).roots()
); );
assert_eq!( assert_eq!(
@ -2687,24 +2755,26 @@ mod tests {
); );
assert_eq!( assert_eq!(
optimize(parse("present(branches() & all())").unwrap()), optimize(parse("present(branches() & all())").unwrap()),
Rc::new(RevsetExpression::Present(RevsetExpression::branches())) Rc::new(RevsetExpression::Present(RevsetExpression::branches(
"".to_owned()
)))
); );
assert_eq!( assert_eq!(
optimize(parse("~branches() & all()").unwrap()), optimize(parse("~branches() & all()").unwrap()),
RevsetExpression::branches().negated() RevsetExpression::branches("".to_owned()).negated()
); );
assert_eq!( assert_eq!(
optimize(parse("(branches() & all()) | (all() & tags())").unwrap()), optimize(parse("(branches() & all()) | (all() & tags())").unwrap()),
RevsetExpression::branches().union(&RevsetExpression::tags()) RevsetExpression::branches("".to_owned()).union(&RevsetExpression::tags())
); );
assert_eq!( assert_eq!(
optimize(parse("(branches() & all()) & (all() & tags())").unwrap()), optimize(parse("(branches() & all()) & (all() & tags())").unwrap()),
RevsetExpression::branches().intersection(&RevsetExpression::tags()) RevsetExpression::branches("".to_owned()).intersection(&RevsetExpression::tags())
); );
assert_eq!( assert_eq!(
optimize(parse("(branches() & all()) ~ (all() & tags())").unwrap()), optimize(parse("(branches() & all()) ~ (all() & tags())").unwrap()),
RevsetExpression::branches().minus(&RevsetExpression::tags()) RevsetExpression::branches("".to_owned()).minus(&RevsetExpression::tags())
); );
} }
@ -2737,7 +2807,7 @@ mod tests {
let optimized = optimize(parsed.clone()); let optimized = optimize(parsed.clone());
assert_eq!( assert_eq!(
unwrap_union(&optimized).0.as_ref(), unwrap_union(&optimized).0.as_ref(),
&RevsetExpression::Branches &RevsetExpression::Branches("".to_owned())
); );
assert!(Rc::ptr_eq( assert!(Rc::ptr_eq(
unwrap_union(&parsed).1, unwrap_union(&parsed).1,

View File

@ -1355,6 +1355,20 @@ fn test_evaluate_expression_branches(use_git: bool) {
resolve_commit_ids(mut_repo.as_repo_ref(), "branches()"), resolve_commit_ids(mut_repo.as_repo_ref(), "branches()"),
vec![commit2.id().clone(), commit1.id().clone()] vec![commit2.id().clone(), commit1.id().clone()]
); );
// Can get branches with matching names
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "branches(branch1)"),
vec![commit1.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "branches(branch)"),
vec![commit2.id().clone(), commit1.id().clone()]
);
// Can silently resolve to an empty set if there's no matches
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "branches(branch3)"),
vec![]
);
// Two branches pointing to the same commit does not result in a duplicate in // Two branches pointing to the same commit does not result in a duplicate in
// the revset // the revset
mut_repo.set_local_branch( mut_repo.set_local_branch(
@ -1426,6 +1440,52 @@ fn test_evaluate_expression_remote_branches(use_git: bool) {
resolve_commit_ids(mut_repo.as_repo_ref(), "remote_branches()"), resolve_commit_ids(mut_repo.as_repo_ref(), "remote_branches()"),
vec![commit2.id().clone(), commit1.id().clone()] vec![commit2.id().clone(), commit1.id().clone()]
); );
// Can get branches with matching names
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "remote_branches(branch1)"),
vec![commit1.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "remote_branches(branch)"),
vec![commit2.id().clone(), commit1.id().clone()]
);
// Can get branches from matching remotes
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), r#"remote_branches("", origin)"#),
vec![commit1.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), r#"remote_branches("", ri)"#),
vec![commit2.id().clone(), commit1.id().clone()]
);
// Can get branches with matching names from matching remotes
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "remote_branches(branch1, ri)"),
vec![commit1.id().clone()]
);
assert_eq!(
resolve_commit_ids(
mut_repo.as_repo_ref(),
r#"remote_branches(branch, private)"#
),
vec![commit2.id().clone()]
);
// Can silently resolve to an empty set if there's no matches
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), "remote_branches(branch3)"),
vec![]
);
assert_eq!(
resolve_commit_ids(mut_repo.as_repo_ref(), r#"remote_branches("", upstream)"#),
vec![]
);
assert_eq!(
resolve_commit_ids(
mut_repo.as_repo_ref(),
r#"remote_branches(branch1, private)"#
),
vec![]
);
// Two branches pointing to the same commit does not result in a duplicate in // Two branches pointing to the same commit does not result in a duplicate in
// the revset // the revset
mut_repo.set_remote_branch( mut_repo.set_remote_branch(