From 2dd75b5c53aa97a75b13826d4adc667e90177126 Mon Sep 17 00:00:00 2001 From: Scott Taylor Date: Sun, 7 Jul 2024 18:38:29 -0500 Subject: [PATCH] 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. --- CHANGELOG.md | 3 ++ cli/src/commands/git/push.rs | 1 + docs/revsets.md | 8 ++++ lib/src/revset.rs | 90 ++++++++++++++++++++++++++++-------- lib/tests/test_revset.rs | 63 +++++++++++++++++++++---- 5 files changed, 137 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ace2489aa8..03c113f404 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/cli/src/commands/git/push.rs b/cli/src/commands/git/push.rs index 7e317710b8..632a3f4e38 100644 --- a/cli/src/commands/git/push.rs +++ b/cli/src/commands/git/push.rs @@ -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())); diff --git a/docs/revsets.md b/docs/revsets.md index 8bebbac8c9..2d4e2f11e9 100644 --- a/docs/revsets.md +++ b/docs/revsets.md @@ -217,6 +217,14 @@ revsets (expressions) as arguments. While Git-tracking branches can be selected by `@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. diff --git a/lib/src/revset.rs b/lib/src/revset.rs index 357cc48a55..19cb6374b6 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -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, }, Tags, GitRefs, @@ -239,11 +240,13 @@ impl RevsetExpression { pub fn remote_branches( branch_pattern: StringPattern, remote_pattern: StringPattern, + remote_ref_state: Option, ) -> Rc { Rc::new(RevsetExpression::CommitRef( RevsetCommitRef::RemoteBranches { branch_pattern, remote_pattern, + remote_ref_state, }, )) } @@ -626,22 +629,13 @@ static BUILTIN_FUNCTION_MAP: Lazy> = 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, +) -> Result, 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), }, ) "###); diff --git a/lib/tests/test_revset.rs b/lib/tests/test_revset.rs index 91e091f6b6..75716ed1e7 100644 --- a/lib/tests/test_revset.rs +++ b/lib/tests/test_revset.rs @@ -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()], )),