// Copyright 2021 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. use std::collections::HashMap;use std::iter;use std::path::Path;use std::sync::Arc; use assert_matches::assert_matches;use chrono::DateTime;use itertools::Itertools as _;use jj_lib::backend::ChangeId;use jj_lib::backend::CommitId;use jj_lib::backend::MillisSinceEpoch;use jj_lib::backend::Signature;use jj_lib::backend::Timestamp;use jj_lib::commit::Commit;use jj_lib::default_index::DefaultIndexStore;use jj_lib::fileset::FilesetExpression;use jj_lib::git;use jj_lib::graph::GraphEdge;use jj_lib::graph::reverse_graph;use jj_lib::id_prefix::IdPrefixContext;use jj_lib::object_id::ObjectId as _;use jj_lib::op_store::RefTarget;use jj_lib::op_store::RemoteRef;use jj_lib::op_store::RemoteRefState;use jj_lib::ref_name::RefName;use jj_lib::ref_name::RemoteName;use jj_lib::ref_name::RemoteRefSymbol;use jj_lib::ref_name::WorkspaceName;use jj_lib::ref_name::WorkspaceNameBuf;use jj_lib::repo::ReadonlyRepo;use jj_lib::repo::Repo;use jj_lib::repo_path::RepoPath;use jj_lib::repo_path::RepoPathUiConverter;use jj_lib::revset::ResolvedRevsetExpression;use jj_lib::revset::Revset;use jj_lib::revset::RevsetAliasesMap;use jj_lib::revset::RevsetDiagnostics;use jj_lib::revset::RevsetEvaluationError;use jj_lib::revset::RevsetExpression;use jj_lib::revset::RevsetExtensions;use jj_lib::revset::RevsetFilterPredicate;use jj_lib::revset::RevsetParseContext;use jj_lib::revset::RevsetResolutionError;use jj_lib::revset::RevsetWorkspaceContext;use jj_lib::revset::SymbolResolver;use jj_lib::revset::SymbolResolverExtension;use jj_lib::revset::parse;use jj_lib::signing::SignBehavior;use jj_lib::signing::Signer;use jj_lib::test_signing_backend::TestSigningBackend;use jj_lib::workspace::Workspace;use pollster::FutureExt as _;use test_case::test_case;use testutils::TestRepo;use testutils::TestRepoBackend;use testutils::TestWorkspace;use testutils::create_random_commit;use testutils::create_tree;use testutils::create_tree_with;use testutils::repo_path;use testutils::write_random_commit;use testutils::write_random_commit_with_parents; fn remote_symbol<'a, N, M>(name: &'a N, remote: &'a M) -> RemoteRefSymbol<'a>where N: AsRef<RefName> + ?Sized, M: AsRef<RemoteName> + ?Sized,{ RemoteRefSymbol { name: name.as_ref(), remote: remote.as_ref(), }} fn default_symbol_resolver(repo: &dyn Repo) -> SymbolResolver<'_> { SymbolResolver::new(repo, &([] as [&Box<dyn SymbolResolverExtension>; 0]))} fn resolve_symbol(repo: &dyn Repo, symbol: &str) -> Result<Vec<CommitId>, RevsetResolutionError> { let context = RevsetParseContext { aliases_map: &RevsetAliasesMap::default(), local_variables: HashMap::new(), user_email: "", date_pattern_context: chrono::Local::now().into(), extensions: &RevsetExtensions::default(), workspace: None, }; let expression = parse(&mut RevsetDiagnostics::new(), symbol, &context).unwrap(); assert_matches!(*expression, RevsetExpression::CommitRef(_)); let symbol_resolver = default_symbol_resolver(repo); match expression .resolve_user_expression(repo, &symbol_resolver)? .as_ref() { RevsetExpression::Commits(commits) => Ok(commits.clone()), expression => panic!("symbol resolved to compound expression: {expression:?}"), }} fn revset_for_commits<'index>( repo: &'index dyn Repo, commits: &[&Commit],) -> Box<dyn Revset + 'index> { let symbol_resolver = default_symbol_resolver(repo); RevsetExpression::commits(commits.iter().map(|commit| commit.id().clone()).collect()) .resolve_user_expression(repo, &symbol_resolver) .unwrap() .evaluate(repo) .unwrap()} fn build_changed_path_index(repo: &ReadonlyRepo) -> Arc<ReadonlyRepo> { let default_index_store: &DefaultIndexStore = repo.index_store().downcast_ref().unwrap(); default_index_store .build_changed_path_index_at_operation(repo.op_id(), repo.store(), u32::MAX) .block_on() .unwrap(); repo.reload_at(repo.operation()).unwrap()} #[test]fn test_resolve_symbol_empty_string() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; assert_matches!( resolve_symbol(repo.as_ref(), r#""""#), Err(RevsetResolutionError::EmptyString) );} #[test]fn test_resolve_symbol_commit_id() { let settings = testutils::user_settings(); // Test only with git so we can get predictable commit ids let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; let mut tx = repo.start_transaction(); let mut_repo = tx.repo_mut(); let signature = Signature { name: "test".to_string(), email: "test".to_string(), timestamp: Timestamp { timestamp: MillisSinceEpoch(0), tz_offset: 0, }, }; let mut commits = vec![]; for i in [156, 268, 869] { let commit = mut_repo .new_commit( vec![repo.store().root_commit_id().clone()], repo.store().empty_merged_tree_id(), ) // An arbitrary change id that doesn't start with "01" .set_change_id(ChangeId::from_hex("781199f9d55d18e855a7aa84c5e4b40d")) .set_description(format!("test {i}")) .set_author(signature.clone()) .set_committer(signature.clone()) .write() .unwrap(); commits.push(commit); } let repo = tx.commit("test").unwrap(); // Test the test setup insta::assert_snapshot!(commits.iter().map(|c| c.id().hex()).join("\n"), @r" 019f179b4479a4f3d1373b772866037929e4f63c 019fd357eb2a4904c348b62d1f4cc2ac222cdbc7 017dc442a1d77bb1620a1a32863580ae81543d7d "); // Test lookup by full commit id assert_eq!( resolve_symbol(repo.as_ref(), "019f179b4479a4f3d1373b772866037929e4f63c",).unwrap(), vec![commits[0].id().clone()] ); assert_eq!( resolve_symbol(repo.as_ref(), "019fd357eb2a4904c348b62d1f4cc2ac222cdbc7",).unwrap(), vec![commits[1].id().clone()] ); assert_eq!( resolve_symbol(repo.as_ref(), "017dc442a1d77bb1620a1a32863580ae81543d7d",).unwrap(), vec![commits[2].id().clone()] ); // Test commit id prefix assert_eq!( resolve_symbol(repo.as_ref(), "017").unwrap(), vec![commits[2].id().clone()] ); assert_matches!( resolve_symbol(repo.as_ref(), "01"), Err(RevsetResolutionError::AmbiguousCommitIdPrefix(s)) if s == "01" ); assert_matches!( resolve_symbol(repo.as_ref(), "010"), Err(RevsetResolutionError::NoSuchRevision{name, candidates}) if name == "010" && candidates.is_empty() ); // Test non-hex string assert_matches!( resolve_symbol(repo.as_ref(), "foo"), Err(RevsetResolutionError::NoSuchRevision{name, candidates}) if name == "foo" && candidates.is_empty() ); // Test present() suppresses only NoSuchRevision error assert_eq!(resolve_commit_ids(repo.as_ref(), "present(foo)"), []); let symbol_resolver = default_symbol_resolver(repo.as_ref()); let context = RevsetParseContext { aliases_map: &RevsetAliasesMap::default(), local_variables: HashMap::new(), user_email: settings.user_email(), date_pattern_context: chrono::Utc::now().fixed_offset().into(), extensions: &RevsetExtensions::default(), workspace: None, }; assert_matches!( parse(&mut RevsetDiagnostics::new(), "present(01)", &context).unwrap() .resolve_user_expression(repo.as_ref(), &symbol_resolver), Err(RevsetResolutionError::AmbiguousCommitIdPrefix(s)) if s == "01" ); assert_eq!( resolve_commit_ids(repo.as_ref(), "present(017)"), vec![commits[2].id().clone()] ); // Test commit_id() function, which is roughly equivalent to present(id) assert_eq!( resolve_symbol( repo.as_ref(), "commit_id(019f179b4479a4f3d1373b772866037929e4f63c)", ) .unwrap(), vec![commits[0].id().clone()] ); assert_eq!( resolve_symbol(repo.as_ref(), "commit_id(019f1)").unwrap(), vec![commits[0].id().clone()] ); assert_eq!( resolve_symbol(repo.as_ref(), "commit_id(12345)").unwrap(), vec![] ); assert_matches!( resolve_symbol(repo.as_ref(), "commit_id('')"), Err(RevsetResolutionError::AmbiguousCommitIdPrefix(s)) if s.is_empty() ); assert_matches!( resolve_symbol(repo.as_ref(), "commit_id(0)"), Err(RevsetResolutionError::AmbiguousCommitIdPrefix(s)) if s == "0" );} #[test_case(false ; "mutable")]#[test_case(true ; "readonly")]fn test_resolve_symbol_change_id(readonly: bool) { // Test only with git so we can get predictable commit ids let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; // Add some commits that will end up having change ids with common prefixes let author = Signature { name: "git author".to_owned(), email: "git.author@example.com".to_owned(), timestamp: Timestamp { timestamp: MillisSinceEpoch(1_000_000), tz_offset: 60, }, }; let committer = Signature { name: "git committer".to_owned(), email: "git.committer@example.com".to_owned(), timestamp: Timestamp { timestamp: MillisSinceEpoch(2_000_000), tz_offset: -480, }, }; let root_commit_id = repo.store().root_commit_id(); let empty_tree_id = repo.store().empty_merged_tree_id(); // These are change ids that would be generated for the imported commits, // but that isn't important. Here we have common prefixes "04", "040", // "04e1" across commit and change ids. let change_ids = [ "04e12a5467bba790efb88a9870894ec2", "040b3ba3a51d8edbc4c5855cbd09de71", "04e1c7082e4e34f3f371d8a1a46770b8", "911d7e52fd5ba04b8f289e14c3d30b52", ] .map(ChangeId::from_hex); let mut commits = vec![]; let mut tx = repo.start_transaction(); for (i, change_id) in iter::zip([0, 1, 2, 5359], change_ids) { let commit = tx .repo_mut() .new_commit(vec![root_commit_id.clone()], empty_tree_id.clone()) .set_change_id(change_id) .set_description(format!("test {i}")) .set_author(author.clone()) .set_committer(committer.clone()) .write() .unwrap(); commits.push(commit); } // Test the test setup insta::allow_duplicates! { insta::assert_snapshot!( commits.iter().map(|c| format!("{} {}\n", c.id(), c.change_id())).join(""), @r" cd741d7f2c542e443df3c5bf2d4f8a15a2759e77 zvlyxpuvtsoopsqzlkorrpqrszrqvlnx 0af32dcddbdf49c132ad39c3623a6196c6c987a5 zvzowopwpuymrlmonvnuruunomzqmlsy 553ee869e64329d1022f5c00c63dff6621924c18 zvlynszrxlvlwvkwkwsymrpypvtsszor 0407d5eb08231b546a42518a50a835f17282eaef qyymsluxkmuopzvorkxrqlyvnwmwzoux "); } let _readonly_repo; let repo: &dyn Repo = if readonly { _readonly_repo = tx.commit("test").unwrap(); _readonly_repo.as_ref() } else { tx.repo_mut() }; // Test lookup by full change id assert_eq!( resolve_symbol(repo, "zvlyxpuvtsoopsqzlkorrpqrszrqvlnx").unwrap(), vec![commits[0].id().clone()] ); assert_eq!( resolve_symbol(repo, "zvzowopwpuymrlmonvnuruunomzqmlsy").unwrap(), vec![commits[1].id().clone()] ); assert_eq!( resolve_symbol(repo, "zvlynszrxlvlwvkwkwsymrpypvtsszor").unwrap(), vec![commits[2].id().clone()] ); // Test change id prefix assert_eq!( resolve_symbol(repo, "zvlyx").unwrap(), vec![commits[0].id().clone()] ); assert_eq!( resolve_symbol(repo, "zvlyn").unwrap(), vec![commits[2].id().clone()] ); assert_matches!( resolve_symbol(repo, "zvly"), Err(RevsetResolutionError::AmbiguousChangeIdPrefix(s)) if s == "zvly" ); assert_matches!( resolve_symbol(repo, "zvlyw"), Err(RevsetResolutionError::NoSuchRevision{name, candidates}) if name == "zvlyw" && candidates.is_empty() ); // Test that commit and changed id don't conflict ("040" and "zvz" are the // same). assert_eq!( resolve_symbol(repo, "040").unwrap(), vec![commits[3].id().clone()] ); assert_eq!( resolve_symbol(repo, "zvz").unwrap(), vec![commits[1].id().clone()] ); // Test non-hex string assert_matches!( resolve_symbol(repo, "foo"), Err(RevsetResolutionError::NoSuchRevision{ name, candidates }) if name == "foo" && candidates.is_empty() ); // Test change_id() function, which is roughly equivalent to present(id) assert_eq!( resolve_symbol(repo, "change_id(zvlyxpuvtsoopsqzlkorrpqrszrqvlnx)").unwrap(), vec![commits[0].id().clone()] ); assert_eq!( resolve_symbol(repo, "change_id(zvlyx)").unwrap(), vec![commits[0].id().clone()] ); assert_eq!(resolve_symbol(repo, "change_id(xyzzy)").unwrap(), vec![]); assert_matches!( resolve_symbol(repo, "change_id('')"), Err(RevsetResolutionError::AmbiguousChangeIdPrefix(s)) if s.is_empty() ); assert_matches!( resolve_symbol(repo, "change_id(z)"), Err(RevsetResolutionError::AmbiguousChangeIdPrefix(s)) if s == "z" );} #[test]fn test_resolve_symbol_divergent_change_id() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let mut tx = repo.start_transaction(); let commit1 = write_random_commit(tx.repo_mut()); let commit2 = create_random_commit(tx.repo_mut()) .set_change_id(commit1.change_id().clone()) .write() .unwrap(); let change_id = commit1.change_id(); assert_matches!( resolve_symbol(tx.repo(), &format!("{change_id}")), Err(RevsetResolutionError::DivergentChangeId { symbol, targets }) if symbol == change_id.to_string() && targets == vec![commit1.id().clone(), commit2.id().clone()] ); assert_eq!( resolve_symbol(tx.repo(), &format!("change_id({change_id})")).unwrap(), // The order is unspecified at resolution stage, but the current index // implementation returns commit ids in ascending position order. vec![commit1.id().clone(), commit2.id().clone()] );} #[test]fn test_resolve_symbol_in_different_disambiguation_context() { let test_repo = TestRepo::init(); let repo0 = &test_repo.repo; let mut tx = repo0.start_transaction(); let commit1 = write_random_commit(tx.repo_mut()); // Create more commits that are likely to conflict with 1-char hex prefix. for _ in 0..50 { write_random_commit(tx.repo_mut()); } let repo1 = tx.commit("test").unwrap(); let mut tx = repo1.start_transaction(); let commit2 = tx.repo_mut().rewrite_commit(&commit1).write().unwrap(); tx.repo_mut().rebase_descendants().unwrap(); let repo2 = tx.commit("test").unwrap(); // Set up disambiguation index which only contains the commit2.id(). let id_prefix_context = IdPrefixContext::new(Arc::new(RevsetExtensions::default())) .disambiguate_within(RevsetExpression::commit(commit2.id().clone())); let symbol_resolver = default_symbol_resolver(repo2.as_ref()).with_id_prefix_context(&id_prefix_context); // Sanity check let change_hex = commit2.change_id().reverse_hex(); assert_eq!( symbol_resolver .resolve_symbol(repo2.as_ref(), &change_hex[0..1]) .unwrap(), commit2.id().clone() ); assert_eq!( symbol_resolver .resolve_symbol(repo2.as_ref(), &commit2.id().hex()[0..1]) .unwrap(), commit2.id().clone() ); // Change ID is disambiguated within repo2, then resolved in repo1. assert_eq!( symbol_resolver .resolve_symbol(repo1.as_ref(), &change_hex[0..1]) .unwrap(), commit1.id().clone() ); // Commit ID can be found in the disambiguation index, but doesn't exist in // repo1. assert_matches!( symbol_resolver.resolve_symbol(repo1.as_ref(), &commit2.id().hex()[0..1]), Err(RevsetResolutionError::NoSuchRevision { .. }) );} #[test]fn test_resolve_working_copy() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let mut tx = repo.start_transaction();Lines 1–500 of 4753. Load more
// Copyright 2021 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::collections::HashMap; use std::iter; use std::path::Path; use std::sync::Arc; use assert_matches::assert_matches; use chrono::DateTime; use itertools::Itertools as _; use jj_lib::backend::ChangeId; use jj_lib::backend::CommitId; use jj_lib::backend::MillisSinceEpoch; use jj_lib::backend::Signature; use jj_lib::backend::Timestamp; use jj_lib::commit::Commit; use jj_lib::default_index::DefaultIndexStore; use jj_lib::fileset::FilesetExpression; use jj_lib::git; use jj_lib::graph::GraphEdge; use jj_lib::graph::reverse_graph; use jj_lib::id_prefix::IdPrefixContext; use jj_lib::object_id::ObjectId as _; use jj_lib::op_store::RefTarget; use jj_lib::op_store::RemoteRef; use jj_lib::op_store::RemoteRefState; use jj_lib::ref_name::RefName; use jj_lib::ref_name::RemoteName; use jj_lib::ref_name::RemoteRefSymbol; use jj_lib::ref_name::WorkspaceName; use jj_lib::ref_name::WorkspaceNameBuf; use jj_lib::repo::ReadonlyRepo; use jj_lib::repo::Repo; use jj_lib::repo_path::RepoPath; use jj_lib::repo_path::RepoPathUiConverter; use jj_lib::revset::ResolvedRevsetExpression; use jj_lib::revset::Revset; use jj_lib::revset::RevsetAliasesMap; use jj_lib::revset::RevsetDiagnostics; use jj_lib::revset::RevsetEvaluationError; use jj_lib::revset::RevsetExpression; use jj_lib::revset::RevsetExtensions; use jj_lib::revset::RevsetFilterPredicate; use jj_lib::revset::RevsetParseContext; use jj_lib::revset::RevsetResolutionError; use jj_lib::revset::RevsetWorkspaceContext; use jj_lib::revset::SymbolResolver; use jj_lib::revset::SymbolResolverExtension; use jj_lib::revset::parse; use jj_lib::signing::SignBehavior; use jj_lib::signing::Signer; use jj_lib::test_signing_backend::TestSigningBackend; use jj_lib::workspace::Workspace; use pollster::FutureExt as _; use test_case::test_case; use testutils::TestRepo; use testutils::TestRepoBackend; use testutils::TestWorkspace; use testutils::create_random_commit; use testutils::create_tree; use testutils::create_tree_with; use testutils::repo_path; use testutils::write_random_commit; use testutils::write_random_commit_with_parents; fn remote_symbol<'a, N, M>(name: &'a N, remote: &'a M) -> RemoteRefSymbol<'a> where N: AsRef<RefName> + ?Sized, M: AsRef<RemoteName> + ?Sized, { RemoteRefSymbol { name: name.as_ref(), remote: remote.as_ref(), } } fn default_symbol_resolver(repo: &dyn Repo) -> SymbolResolver<'_> { SymbolResolver::new(repo, &([] as [&Box<dyn SymbolResolverExtension>; 0])) } fn resolve_symbol(repo: &dyn Repo, symbol: &str) -> Result<Vec<CommitId>, RevsetResolutionError> { let context = RevsetParseContext { aliases_map: &RevsetAliasesMap::default(), local_variables: HashMap::new(), user_email: "", date_pattern_context: chrono::Local::now().into(), extensions: &RevsetExtensions::default(), workspace: None, }; let expression = parse(&mut RevsetDiagnostics::new(), symbol, &context).unwrap(); assert_matches!(*expression, RevsetExpression::CommitRef(_)); let symbol_resolver = default_symbol_resolver(repo); match expression .resolve_user_expression(repo, &symbol_resolver)? .as_ref() { RevsetExpression::Commits(commits) => Ok(commits.clone()), expression => panic!("symbol resolved to compound expression: {expression:?}"), } } fn revset_for_commits<'index>( repo: &'index dyn Repo, commits: &[&Commit], ) -> Box<dyn Revset + 'index> { let symbol_resolver = default_symbol_resolver(repo); RevsetExpression::commits(commits.iter().map(|commit| commit.id().clone()).collect()) .resolve_user_expression(repo, &symbol_resolver) .unwrap() .evaluate(repo) .unwrap() } fn build_changed_path_index(repo: &ReadonlyRepo) -> Arc<ReadonlyRepo> { let default_index_store: &DefaultIndexStore = repo.index_store().downcast_ref().unwrap(); default_index_store .build_changed_path_index_at_operation(repo.op_id(), repo.store(), u32::MAX) .block_on() .unwrap(); repo.reload_at(repo.operation()).unwrap() } #[test] fn test_resolve_symbol_empty_string() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; assert_matches!( resolve_symbol(repo.as_ref(), r#""""#), Err(RevsetResolutionError::EmptyString) ); } #[test] fn test_resolve_symbol_commit_id() { let settings = testutils::user_settings(); // Test only with git so we can get predictable commit ids let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; let mut tx = repo.start_transaction(); let mut_repo = tx.repo_mut(); let signature = Signature { name: "test".to_string(), email: "test".to_string(), timestamp: Timestamp { timestamp: MillisSinceEpoch(0), tz_offset: 0, }, }; let mut commits = vec![]; for i in [156, 268, 869] { let commit = mut_repo .new_commit( vec![repo.store().root_commit_id().clone()], repo.store().empty_merged_tree_id(), ) // An arbitrary change id that doesn't start with "01" .set_change_id(ChangeId::from_hex("781199f9d55d18e855a7aa84c5e4b40d")) .set_description(format!("test {i}")) .set_author(signature.clone()) .set_committer(signature.clone()) .write() .unwrap(); commits.push(commit); } let repo = tx.commit("test").unwrap(); // Test the test setup insta::assert_snapshot!(commits.iter().map(|c| c.id().hex()).join("\n"), @r" 019f179b4479a4f3d1373b772866037929e4f63c 019fd357eb2a4904c348b62d1f4cc2ac222cdbc7 017dc442a1d77bb1620a1a32863580ae81543d7d "); // Test lookup by full commit id assert_eq!( resolve_symbol(repo.as_ref(), "019f179b4479a4f3d1373b772866037929e4f63c",).unwrap(), vec![commits[0].id().clone()] ); assert_eq!( resolve_symbol(repo.as_ref(), "019fd357eb2a4904c348b62d1f4cc2ac222cdbc7",).unwrap(), vec![commits[1].id().clone()] ); assert_eq!( resolve_symbol(repo.as_ref(), "017dc442a1d77bb1620a1a32863580ae81543d7d",).unwrap(), vec![commits[2].id().clone()] ); // Test commit id prefix assert_eq!( resolve_symbol(repo.as_ref(), "017").unwrap(), vec![commits[2].id().clone()] ); assert_matches!( resolve_symbol(repo.as_ref(), "01"), Err(RevsetResolutionError::AmbiguousCommitIdPrefix(s)) if s == "01" ); assert_matches!( resolve_symbol(repo.as_ref(), "010"), Err(RevsetResolutionError::NoSuchRevision{name, candidates}) if name == "010" && candidates.is_empty() ); // Test non-hex string assert_matches!( resolve_symbol(repo.as_ref(), "foo"), Err(RevsetResolutionError::NoSuchRevision{name, candidates}) if name == "foo" && candidates.is_empty() ); // Test present() suppresses only NoSuchRevision error assert_eq!(resolve_commit_ids(repo.as_ref(), "present(foo)"), []); let symbol_resolver = default_symbol_resolver(repo.as_ref()); let context = RevsetParseContext { aliases_map: &RevsetAliasesMap::default(), local_variables: HashMap::new(), user_email: settings.user_email(), date_pattern_context: chrono::Utc::now().fixed_offset().into(), extensions: &RevsetExtensions::default(), workspace: None, }; assert_matches!( parse(&mut RevsetDiagnostics::new(), "present(01)", &context).unwrap() .resolve_user_expression(repo.as_ref(), &symbol_resolver), Err(RevsetResolutionError::AmbiguousCommitIdPrefix(s)) if s == "01" ); assert_eq!( resolve_commit_ids(repo.as_ref(), "present(017)"), vec![commits[2].id().clone()] ); // Test commit_id() function, which is roughly equivalent to present(id) assert_eq!( resolve_symbol( repo.as_ref(), "commit_id(019f179b4479a4f3d1373b772866037929e4f63c)", ) .unwrap(), vec![commits[0].id().clone()] ); assert_eq!( resolve_symbol(repo.as_ref(), "commit_id(019f1)").unwrap(), vec![commits[0].id().clone()] ); assert_eq!( resolve_symbol(repo.as_ref(), "commit_id(12345)").unwrap(), vec![] ); assert_matches!( resolve_symbol(repo.as_ref(), "commit_id('')"), Err(RevsetResolutionError::AmbiguousCommitIdPrefix(s)) if s.is_empty() ); assert_matches!( resolve_symbol(repo.as_ref(), "commit_id(0)"), Err(RevsetResolutionError::AmbiguousCommitIdPrefix(s)) if s == "0" ); } #[test_case(false ; "mutable")] #[test_case(true ; "readonly")] fn test_resolve_symbol_change_id(readonly: bool) { // Test only with git so we can get predictable commit ids let test_repo = TestRepo::init_with_backend(TestRepoBackend::Git); let repo = &test_repo.repo; // Add some commits that will end up having change ids with common prefixes let author = Signature { name: "git author".to_owned(), email: "git.author@example.com".to_owned(), timestamp: Timestamp { timestamp: MillisSinceEpoch(1_000_000), tz_offset: 60, }, }; let committer = Signature { name: "git committer".to_owned(), email: "git.committer@example.com".to_owned(), timestamp: Timestamp { timestamp: MillisSinceEpoch(2_000_000), tz_offset: -480, }, }; let root_commit_id = repo.store().root_commit_id(); let empty_tree_id = repo.store().empty_merged_tree_id(); // These are change ids that would be generated for the imported commits, // but that isn't important. Here we have common prefixes "04", "040", // "04e1" across commit and change ids. let change_ids = [ "04e12a5467bba790efb88a9870894ec2", "040b3ba3a51d8edbc4c5855cbd09de71", "04e1c7082e4e34f3f371d8a1a46770b8", "911d7e52fd5ba04b8f289e14c3d30b52", ] .map(ChangeId::from_hex); let mut commits = vec![]; let mut tx = repo.start_transaction(); for (i, change_id) in iter::zip([0, 1, 2, 5359], change_ids) { let commit = tx .repo_mut() .new_commit(vec![root_commit_id.clone()], empty_tree_id.clone()) .set_change_id(change_id) .set_description(format!("test {i}")) .set_author(author.clone()) .set_committer(committer.clone()) .write() .unwrap(); commits.push(commit); } // Test the test setup insta::allow_duplicates! { insta::assert_snapshot!( commits.iter().map(|c| format!("{} {}\n", c.id(), c.change_id())).join(""), @r" cd741d7f2c542e443df3c5bf2d4f8a15a2759e77 zvlyxpuvtsoopsqzlkorrpqrszrqvlnx 0af32dcddbdf49c132ad39c3623a6196c6c987a5 zvzowopwpuymrlmonvnuruunomzqmlsy 553ee869e64329d1022f5c00c63dff6621924c18 zvlynszrxlvlwvkwkwsymrpypvtsszor 0407d5eb08231b546a42518a50a835f17282eaef qyymsluxkmuopzvorkxrqlyvnwmwzoux "); } let _readonly_repo; let repo: &dyn Repo = if readonly { _readonly_repo = tx.commit("test").unwrap(); _readonly_repo.as_ref() } else { tx.repo_mut() }; // Test lookup by full change id assert_eq!( resolve_symbol(repo, "zvlyxpuvtsoopsqzlkorrpqrszrqvlnx").unwrap(), vec![commits[0].id().clone()] ); assert_eq!( resolve_symbol(repo, "zvzowopwpuymrlmonvnuruunomzqmlsy").unwrap(), vec![commits[1].id().clone()] ); assert_eq!( resolve_symbol(repo, "zvlynszrxlvlwvkwkwsymrpypvtsszor").unwrap(), vec![commits[2].id().clone()] ); // Test change id prefix assert_eq!( resolve_symbol(repo, "zvlyx").unwrap(), vec![commits[0].id().clone()] ); assert_eq!( resolve_symbol(repo, "zvlyn").unwrap(), vec![commits[2].id().clone()] ); assert_matches!( resolve_symbol(repo, "zvly"), Err(RevsetResolutionError::AmbiguousChangeIdPrefix(s)) if s == "zvly" ); assert_matches!( resolve_symbol(repo, "zvlyw"), Err(RevsetResolutionError::NoSuchRevision{name, candidates}) if name == "zvlyw" && candidates.is_empty() ); // Test that commit and changed id don't conflict ("040" and "zvz" are the // same). assert_eq!( resolve_symbol(repo, "040").unwrap(), vec![commits[3].id().clone()] ); assert_eq!( resolve_symbol(repo, "zvz").unwrap(), vec![commits[1].id().clone()] ); // Test non-hex string assert_matches!( resolve_symbol(repo, "foo"), Err(RevsetResolutionError::NoSuchRevision{ name, candidates }) if name == "foo" && candidates.is_empty() ); // Test change_id() function, which is roughly equivalent to present(id) assert_eq!( resolve_symbol(repo, "change_id(zvlyxpuvtsoopsqzlkorrpqrszrqvlnx)").unwrap(), vec![commits[0].id().clone()] ); assert_eq!( resolve_symbol(repo, "change_id(zvlyx)").unwrap(), vec![commits[0].id().clone()] ); assert_eq!(resolve_symbol(repo, "change_id(xyzzy)").unwrap(), vec![]); assert_matches!( resolve_symbol(repo, "change_id('')"), Err(RevsetResolutionError::AmbiguousChangeIdPrefix(s)) if s.is_empty() ); assert_matches!( resolve_symbol(repo, "change_id(z)"), Err(RevsetResolutionError::AmbiguousChangeIdPrefix(s)) if s == "z" ); } #[test] fn test_resolve_symbol_divergent_change_id() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let mut tx = repo.start_transaction(); let commit1 = write_random_commit(tx.repo_mut()); let commit2 = create_random_commit(tx.repo_mut()) .set_change_id(commit1.change_id().clone()) .write() .unwrap(); let change_id = commit1.change_id(); assert_matches!( resolve_symbol(tx.repo(), &format!("{change_id}")), Err(RevsetResolutionError::DivergentChangeId { symbol, targets }) if symbol == change_id.to_string() && targets == vec![commit1.id().clone(), commit2.id().clone()] ); assert_eq!( resolve_symbol(tx.repo(), &format!("change_id({change_id})")).unwrap(), // The order is unspecified at resolution stage, but the current index // implementation returns commit ids in ascending position order. vec![commit1.id().clone(), commit2.id().clone()] ); } #[test] fn test_resolve_symbol_in_different_disambiguation_context() { let test_repo = TestRepo::init(); let repo0 = &test_repo.repo; let mut tx = repo0.start_transaction(); let commit1 = write_random_commit(tx.repo_mut()); // Create more commits that are likely to conflict with 1-char hex prefix. for _ in 0..50 { write_random_commit(tx.repo_mut()); } let repo1 = tx.commit("test").unwrap(); let mut tx = repo1.start_transaction(); let commit2 = tx.repo_mut().rewrite_commit(&commit1).write().unwrap(); tx.repo_mut().rebase_descendants().unwrap(); let repo2 = tx.commit("test").unwrap(); // Set up disambiguation index which only contains the commit2.id(). let id_prefix_context = IdPrefixContext::new(Arc::new(RevsetExtensions::default())) .disambiguate_within(RevsetExpression::commit(commit2.id().clone())); let symbol_resolver = default_symbol_resolver(repo2.as_ref()).with_id_prefix_context(&id_prefix_context); // Sanity check let change_hex = commit2.change_id().reverse_hex(); assert_eq!( symbol_resolver .resolve_symbol(repo2.as_ref(), &change_hex[0..1]) .unwrap(), commit2.id().clone() ); assert_eq!( symbol_resolver .resolve_symbol(repo2.as_ref(), &commit2.id().hex()[0..1]) .unwrap(), commit2.id().clone() ); // Change ID is disambiguated within repo2, then resolved in repo1. assert_eq!( symbol_resolver .resolve_symbol(repo1.as_ref(), &change_hex[0..1]) .unwrap(), commit1.id().clone() ); // Commit ID can be found in the disambiguation index, but doesn't exist in // repo1. assert_matches!( symbol_resolver.resolve_symbol(repo1.as_ref(), &commit2.id().hex()[0..1]), Err(RevsetResolutionError::NoSuchRevision { .. }) ); } #[test] fn test_resolve_working_copy() { let test_repo = TestRepo::init(); let repo = &test_repo.repo; let mut tx = repo.start_transaction();