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()], )),