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 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
* When sharing the working copy with a Git repo, we used to forget to export
@ -91,11 +98,11 @@ No changes, only changed to a released version of the `thrift` crate dependency.
* Adjusted precedence of revset union/intersection/difference operators.
`x | y & z` is now equivalent to `x | (y & z)`.
* Support for open commits has been dropped. The `ui.enable-open-commits` config
that was added in 0.5.0 is no longer respected. The `jj open/close` commands
have been deleted.
* `jj commit` is now a separate command from `jj close` (which no longer
exists). The behavior has changed slightly. It now always asks for a
description, even if there already was a description set. It now also only
@ -153,7 +160,7 @@ No changes, only changed to a released version of the `thrift` crate dependency.
revset.
* The new global flag `-v/--verbose` will turn on debug logging to give
some additional insight into what is happening behind the scenes.
some additional insight into what is happening behind the scenes.
Note: This is not comprehensively supported by all operations yet.
* `jj log`, `jj show`, and `jj obslog` now all support showing relative
@ -165,7 +172,7 @@ No changes, only changed to a released version of the `thrift` crate dependency.
This typically occurred when running in a working copy colocated with Git
(created by running `jj init --git-dir=.`).
[#463](https://github.com/martinvonz/jj/issues/463)
* When exporting branches to Git, we used to fail if some branches could not be
exported (e.g. because Git doesn't allow a branch called `main` and another
branch called `main/sub`). We now print a warning about these branches
@ -417,7 +424,7 @@ No changes (just trying to get automated GitHub release to work).
has a non-empty description.
* All commands now consistently snapshot the working copy (it was missing from
e.g. `jj undo` and `jj merge` before).
e.g. `jj undo` and `jj merge` before).
## [0.4.0] - 2022-04-02

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
ID or a Git ref pointing to them).
## Symbols
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
6. Commit ID or change ID
## Operators
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
`x & (y | z)`.
## Functions
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.
* `none()`: No commits. This function is rarely useful; it is provided for
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.
* `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
possible targets are included.
* `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
in `x` doesn't exist (e.g. is an unknown branch name.)
## Aliases
New symbols and functions can be defined in the config file, by using any
combination of the predefined symbols/functions and other aliases.
For example:
```toml
[revset-aliases]
'mine' = 'author(martinvonz)'
'user(x)' = 'author(x) | committer(x)'
```
## Examples
Show the parent(s) of the working-copy commit (like `git log -1 HEAD`):
```
jj log -r @-
```
Show commits not on any remote branch:
```
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`)
```
jj log -r :@
```
Show the initial commits in the repo (the ones Git calls "root commits"):
```
jj log -r root+
```
Show some important commits (like `git --simplify-by-decoration`):
```
jj log -r 'tags() | branches()'
```
Show local commits leading up to the working copy, as well as descendants of
those commits:
```
jj log -r '(remote_branches()..@):'
```
Show commits authored by "martinvonz" and containing the word "reset" in the
description:
```
jj log -r 'author(martinvonz) & description(reset)'
```

View File

@ -378,8 +378,11 @@ pub enum RevsetExpression {
Roots(Rc<RevsetExpression>),
VisibleHeads,
PublicHeads,
Branches,
RemoteBranches,
Branches(String),
RemoteBranches {
branch_needle: String,
remote_needle: String,
},
Tags,
GitRefs,
GitHead,
@ -422,12 +425,15 @@ impl RevsetExpression {
Rc::new(RevsetExpression::PublicHeads)
}
pub fn branches() -> Rc<RevsetExpression> {
Rc::new(RevsetExpression::Branches)
pub fn branches(needle: String) -> Rc<RevsetExpression> {
Rc::new(RevsetExpression::Branches(needle))
}
pub fn remote_branches() -> Rc<RevsetExpression> {
Rc::new(RevsetExpression::RemoteBranches)
pub fn remote_branches(branch_needle: String, remote_needle: String) -> Rc<RevsetExpression> {
Rc::new(RevsetExpression::RemoteBranches {
branch_needle,
remote_needle,
})
}
pub fn tags() -> Rc<RevsetExpression> {
@ -934,12 +940,31 @@ fn parse_builtin_function(
Ok(RevsetExpression::public_heads())
}
"branches" => {
expect_no_arguments(name, arguments_pair)?;
Ok(RevsetExpression::branches())
let opt_arg = expect_one_optional_argument(name, arguments_pair)?;
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" => {
expect_no_arguments(name, arguments_pair)?;
Ok(RevsetExpression::remote_branches())
let (branch_opt_arg, remote_opt_arg) =
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" => {
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>(
name: &str,
arguments_pair: Pair<'i, Rule>,
) -> Result<Option<Pair<'i, Rule>>, RevsetParseError> {
) -> Result<OptionalArg<'i>, RevsetParseError> {
let span = arguments_pair.as_span();
let mut argument_pairs = arguments_pair.into_inner().fuse();
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(
name: &str,
pair: Pair<Rule>,
@ -1168,8 +1218,8 @@ fn transform_expression_bottom_up(
transform_rec(candidates, f).map(RevsetExpression::Roots)
}
RevsetExpression::PublicHeads => None,
RevsetExpression::Branches => None,
RevsetExpression::RemoteBranches => None,
RevsetExpression::Branches(_) => None,
RevsetExpression::RemoteBranches { .. } => None,
RevsetExpression::Tags => None,
RevsetExpression::GitRefs => None,
RevsetExpression::GitHead => None,
@ -1957,20 +2007,31 @@ pub fn evaluate_expression<'repo>(
repo,
&repo.view().public_heads().iter().cloned().collect_vec(),
)),
RevsetExpression::Branches => {
RevsetExpression::Branches(needle) => {
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 {
commit_ids.extend(local_target.adds());
}
}
Ok(revset_for_commit_ids(repo, &commit_ids))
}
RevsetExpression::RemoteBranches => {
RevsetExpression::RemoteBranches {
branch_needle,
remote_needle,
} => {
let mut commit_ids = vec![];
for branch_target in repo.view().branches().values() {
for remote_target in branch_target.remote_targets.values() {
commit_ids.extend(remote_target.adds());
for (branch_name, branch_target) in repo.view().branches() {
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());
}
}
}
Ok(revset_for_commit_ids(repo, &commit_ids))
@ -2283,6 +2344,13 @@ mod tests {
parse("foo.bar-v1+7-"),
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
assert_eq!(parse(".foo"), Err(RevsetParseErrorKind::SyntaxError));
assert_eq!(parse("foo."), Err(RevsetParseErrorKind::SyntaxError));
@ -2643,37 +2711,37 @@ mod tests {
assert_eq!(
optimize(parse("parents(branches() & all())").unwrap()),
RevsetExpression::branches().parents()
RevsetExpression::branches("".to_owned()).parents()
);
assert_eq!(
optimize(parse("children(branches() & all())").unwrap()),
RevsetExpression::branches().children()
RevsetExpression::branches("".to_owned()).children()
);
assert_eq!(
optimize(parse("ancestors(branches() & all())").unwrap()),
RevsetExpression::branches().ancestors()
RevsetExpression::branches("".to_owned()).ancestors()
);
assert_eq!(
optimize(parse("descendants(branches() & all())").unwrap()),
RevsetExpression::branches().descendants()
RevsetExpression::branches("".to_owned()).descendants()
);
assert_eq!(
optimize(parse("(branches() & all())..(all() & tags())").unwrap()),
RevsetExpression::branches().range(&RevsetExpression::tags())
RevsetExpression::branches("".to_owned()).range(&RevsetExpression::tags())
);
assert_eq!(
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!(
optimize(parse("heads(branches() & all())").unwrap()),
RevsetExpression::branches().heads()
RevsetExpression::branches("".to_owned()).heads()
);
assert_eq!(
optimize(parse("roots(branches() & all())").unwrap()),
RevsetExpression::branches().roots()
RevsetExpression::branches("".to_owned()).roots()
);
assert_eq!(
@ -2687,24 +2755,26 @@ mod tests {
);
assert_eq!(
optimize(parse("present(branches() & all())").unwrap()),
Rc::new(RevsetExpression::Present(RevsetExpression::branches()))
Rc::new(RevsetExpression::Present(RevsetExpression::branches(
"".to_owned()
)))
);
assert_eq!(
optimize(parse("~branches() & all()").unwrap()),
RevsetExpression::branches().negated()
RevsetExpression::branches("".to_owned()).negated()
);
assert_eq!(
optimize(parse("(branches() & all()) | (all() & tags())").unwrap()),
RevsetExpression::branches().union(&RevsetExpression::tags())
RevsetExpression::branches("".to_owned()).union(&RevsetExpression::tags())
);
assert_eq!(
optimize(parse("(branches() & all()) & (all() & tags())").unwrap()),
RevsetExpression::branches().intersection(&RevsetExpression::tags())
RevsetExpression::branches("".to_owned()).intersection(&RevsetExpression::tags())
);
assert_eq!(
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());
assert_eq!(
unwrap_union(&optimized).0.as_ref(),
&RevsetExpression::Branches
&RevsetExpression::Branches("".to_owned())
);
assert!(Rc::ptr_eq(
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()"),
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
// the revset
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()"),
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
// the revset
mut_repo.set_remote_branch(