revset: add tracked/untracked_remote_branches()

Adds support for revset functions `tracked_remote_branches()` and
`untracked_remote_branches()`. I think this would be especially useful
for configuring `immutable_heads()` because rewriting untracked remote
branches usually wouldn't be desirable (since it wouldn't update the
remote branch). It also makes it easy to hide branches that you don't
care about from the log, since you could hide untracked branches and
then only track branches that you care about.
This commit is contained in:
Scott Taylor 2024-07-07 18:38:29 -05:00 committed by Scott Taylor
parent 35b2136c68
commit 2dd75b5c53
5 changed files with 137 additions and 28 deletions

View file

@ -46,6 +46,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
address unconditionally. Only ASCII case folding is currently implemented,
but this will likely change in the future.
* New `tracked_remote_branches()` and `untracked_remote_branches()` revset
functions can be used to select tracked/untracked remote branches.
### Fixed bugs
## [0.19.0] - 2024-07-03

View file

@ -553,6 +553,7 @@ fn find_branches_targeted_by_revisions<'a>(
let current_branches_expression = RevsetExpression::remote_branches(
StringPattern::everything(),
StringPattern::exact(remote_name),
None,
)
.range(&RevsetExpression::commit(wc_commit_id))
.intersection(&RevsetExpression::branches(StringPattern::everything()));

View file

@ -217,6 +217,14 @@ revsets (expressions) as arguments.
While Git-tracking branches can be selected by `<name>@git`, these branches
aren't included in `remote_branches()`.
* `tracked_remote_branches([branch_pattern[, [remote=]remote_pattern]])`: All
targets of tracked remote branches. Supports the same optional arguments as
`remote_branches()`.
* `untracked_remote_branches([branch_pattern[, [remote=]remote_pattern]])`:
All targets of untracked remote branches. Supports the same optional arguments
as `remote_branches()`.
* `tags()`: All tag targets. If a tag is in a conflicted state, all its
possible targets are included.

View file

@ -34,7 +34,7 @@ use crate::graph::GraphEdge;
use crate::hex_util::to_forward_hex;
use crate::id_prefix::IdPrefixContext;
use crate::object_id::{HexPrefix, PrefixResolution};
use crate::op_store::WorkspaceId;
use crate::op_store::{RemoteRefState, WorkspaceId};
use crate::repo::Repo;
use crate::repo_path::RepoPathUiConverter;
pub use crate::revset_parser::{
@ -107,6 +107,7 @@ pub enum RevsetCommitRef {
RemoteBranches {
branch_pattern: StringPattern,
remote_pattern: StringPattern,
remote_ref_state: Option<RemoteRefState>,
},
Tags,
GitRefs,
@ -239,11 +240,13 @@ impl RevsetExpression {
pub fn remote_branches(
branch_pattern: StringPattern,
remote_pattern: StringPattern,
remote_ref_state: Option<RemoteRefState>,
) -> Rc<RevsetExpression> {
Rc::new(RevsetExpression::CommitRef(
RevsetCommitRef::RemoteBranches {
branch_pattern,
remote_pattern,
remote_ref_state,
},
))
}
@ -626,22 +629,13 @@ static BUILTIN_FUNCTION_MAP: Lazy<HashMap<&'static str, RevsetFunction>> = Lazy:
Ok(RevsetExpression::branches(pattern))
});
map.insert("remote_branches", |function, _context| {
let ([], [branch_opt_arg, remote_opt_arg]) =
function.expect_named_arguments(&["", "remote"])?;
let branch_pattern = if let Some(branch_arg) = branch_opt_arg {
expect_string_pattern(branch_arg)?
} else {
StringPattern::everything()
};
let remote_pattern = if let Some(remote_arg) = remote_opt_arg {
expect_string_pattern(remote_arg)?
} else {
StringPattern::everything()
};
Ok(RevsetExpression::remote_branches(
branch_pattern,
remote_pattern,
))
parse_remote_branches_arguments(function, None)
});
map.insert("tracked_remote_branches", |function, _context| {
parse_remote_branches_arguments(function, Some(RemoteRefState::Tracking))
});
map.insert("untracked_remote_branches", |function, _context| {
parse_remote_branches_arguments(function, Some(RemoteRefState::New))
});
map.insert("tags", |function, _context| {
function.expect_no_arguments()?;
@ -752,6 +746,29 @@ pub fn expect_string_pattern(node: &ExpressionNode) -> Result<StringPattern, Rev
revset_parser::expect_pattern_with("string pattern", node, parse_pattern)
}
fn parse_remote_branches_arguments(
function: &FunctionCallNode,
remote_ref_state: Option<RemoteRefState>,
) -> Result<Rc<RevsetExpression>, RevsetParseError> {
let ([], [branch_opt_arg, remote_opt_arg]) =
function.expect_named_arguments(&["", "remote"])?;
let branch_pattern = if let Some(branch_arg) = branch_opt_arg {
expect_string_pattern(branch_arg)?
} else {
StringPattern::everything()
};
let remote_pattern = if let Some(remote_arg) = remote_opt_arg {
expect_string_pattern(remote_arg)?
} else {
StringPattern::everything()
};
Ok(RevsetExpression::remote_branches(
branch_pattern,
remote_pattern,
remote_ref_state,
))
}
/// Resolves function call by using the given function map.
fn lower_function_call(
function: &FunctionCallNode,
@ -1609,11 +1626,15 @@ fn resolve_commit_ref(
RevsetCommitRef::RemoteBranches {
branch_pattern,
remote_pattern,
remote_ref_state,
} => {
// TODO: should we allow to select @git branches explicitly?
let commit_ids = repo
.view()
.remote_branches_matching(branch_pattern, remote_pattern)
.filter(|(_, remote_ref)| {
remote_ref_state.map_or(true, |state| remote_ref.state == state)
})
.filter(|&((_, remote_name), _)| {
#[cfg(feature = "git")]
{
@ -2313,14 +2334,25 @@ mod tests {
RemoteBranches {
branch_pattern: Substring(""),
remote_pattern: Substring(""),
remote_ref_state: None,
},
)
"###);
insta::assert_debug_snapshot!(parse("remote_branches()").unwrap(), @r###"
insta::assert_debug_snapshot!(parse("tracked_remote_branches()").unwrap(), @r###"
CommitRef(
RemoteBranches {
branch_pattern: Substring(""),
remote_pattern: Substring(""),
remote_ref_state: Some(Tracking),
},
)
"###);
insta::assert_debug_snapshot!(parse("untracked_remote_branches()").unwrap(), @r###"
CommitRef(
RemoteBranches {
branch_pattern: Substring(""),
remote_pattern: Substring(""),
remote_ref_state: Some(New),
},
)
"###);
@ -2566,6 +2598,7 @@ mod tests {
RemoteBranches {
branch_pattern: Substring(""),
remote_pattern: Substring("foo"),
remote_ref_state: None,
},
)
"###);
@ -2575,6 +2608,27 @@ mod tests {
RemoteBranches {
branch_pattern: Substring("foo"),
remote_pattern: Substring("bar"),
remote_ref_state: None,
},
)
"###);
insta::assert_debug_snapshot!(
parse("tracked_remote_branches(foo, remote=bar)").unwrap(), @r###"
CommitRef(
RemoteBranches {
branch_pattern: Substring("foo"),
remote_pattern: Substring("bar"),
remote_ref_state: Some(Tracking),
},
)
"###);
insta::assert_debug_snapshot!(
parse("untracked_remote_branches(foo, remote=bar)").unwrap(), @r###"
CommitRef(
RemoteBranches {
branch_pattern: Substring("foo"),
remote_pattern: Substring("bar"),
remote_ref_state: Some(New),
},
)
"###);

View file

@ -2059,11 +2059,12 @@ fn test_evaluate_expression_remote_branches() {
let settings = testutils::user_settings();
let test_repo = TestRepo::init();
let repo = &test_repo.repo;
let remote_ref = |target| RemoteRef {
let tracking_remote_ref = |target| RemoteRef {
target,
state: RemoteRefState::Tracking, // doesn't matter
state: RemoteRefState::Tracking,
};
let normal_remote_ref = |id: &CommitId| remote_ref(RefTarget::normal(id.clone()));
let normal_tracking_remote_ref =
|id: &CommitId| tracking_remote_ref(RefTarget::normal(id.clone()));
let mut tx = repo.start_transaction(&settings);
let mut_repo = tx.mut_repo();
@ -2076,15 +2077,28 @@ fn test_evaluate_expression_remote_branches() {
// Can get branches when there are none
assert_eq!(resolve_commit_ids(mut_repo, "remote_branches()"), vec![]);
// Can get a few branches
mut_repo.set_remote_branch("branch1", "origin", normal_remote_ref(commit1.id()));
mut_repo.set_remote_branch("branch2", "private", normal_remote_ref(commit2.id()));
// Branch 1 is untracked on remote origin
mut_repo.set_remote_branch(
"branch1",
"origin",
RemoteRef {
target: RefTarget::normal(commit1.id().clone()),
state: RemoteRefState::New,
},
);
// Branch 2 is tracked on remote private
mut_repo.set_remote_branch(
"branch2",
"private",
normal_tracking_remote_ref(commit2.id()),
);
// Git-tracking branches aren't included
mut_repo.set_remote_branch(
"branch",
git::REMOTE_NAME_FOR_LOCAL_GIT_REPO,
normal_remote_ref(commit_git_remote.id()),
normal_tracking_remote_ref(commit_git_remote.id()),
);
// Can get a few branches
assert_eq!(
resolve_commit_ids(mut_repo, "remote_branches()"),
vec![commit2.id().clone(), commit1.id().clone()]
@ -2128,6 +2142,23 @@ fn test_evaluate_expression_remote_branches() {
resolve_commit_ids(mut_repo, r#"remote_branches(exact:branch1, exact:origin)"#),
vec![commit1.id().clone()]
);
// Can filter branches by tracked and untracked
assert_eq!(
resolve_commit_ids(mut_repo, "tracked_remote_branches()"),
vec![commit2.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, "untracked_remote_branches()"),
vec![commit1.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, "untracked_remote_branches(branch1, origin)"),
vec![commit1.id().clone()]
);
assert_eq!(
resolve_commit_ids(mut_repo, "tracked_remote_branches(branch2, private)"),
vec![commit2.id().clone()]
);
// Can silently resolve to an empty set if there's no matches
assert_eq!(
resolve_commit_ids(mut_repo, "remote_branches(branch3)"),
@ -2149,9 +2180,21 @@ fn test_evaluate_expression_remote_branches() {
resolve_commit_ids(mut_repo, r#"remote_branches(exact:branch1, exact:orig)"#),
vec![]
);
assert_eq!(
resolve_commit_ids(mut_repo, "tracked_remote_branches(branch1)"),
vec![]
);
assert_eq!(
resolve_commit_ids(mut_repo, "untracked_remote_branches(branch2)"),
vec![]
);
// Two branches pointing to the same commit does not result in a duplicate in
// the revset
mut_repo.set_remote_branch("branch3", "origin", normal_remote_ref(commit2.id()));
mut_repo.set_remote_branch(
"branch3",
"origin",
normal_tracking_remote_ref(commit2.id()),
);
assert_eq!(
resolve_commit_ids(mut_repo, "remote_branches()"),
vec![commit2.id().clone(), commit1.id().clone()]
@ -2166,7 +2209,7 @@ fn test_evaluate_expression_remote_branches() {
mut_repo.set_remote_branch(
"branch1",
"origin",
remote_ref(RefTarget::from_legacy_form(
tracking_remote_ref(RefTarget::from_legacy_form(
[commit1.id().clone()],
[commit2.id().clone(), commit3.id().clone()],
)),
@ -2174,7 +2217,7 @@ fn test_evaluate_expression_remote_branches() {
mut_repo.set_remote_branch(
"branch2",
"private",
remote_ref(RefTarget::from_legacy_form(
tracking_remote_ref(RefTarget::from_legacy_form(
[commit2.id().clone()],
[commit3.id().clone(), commit4.id().clone()],
)),