From ceccabd9ee241d1cd2d44d8db30bc2b4baa7dac8 Mon Sep 17 00:00:00 2001 From: Pavan Kumar Sunkara Date: Thu, 2 Jul 2026 17:34:12 +0530 Subject: [PATCH] meta: Extract revset parser as a separate crate --- Cargo.lock | 21 +- Cargo.toml | 3 +- lib/Cargo.toml | 2 +- lib/proc-macros/src/content_hash.rs | 14 +- lib/proc-macros/src/lib.rs | 4 +- lib/revset-parser/Cargo.toml | 35 +++ lib/revset-parser/LICENSE | 202 ++++++++++++++++++ lib/{ => revset-parser}/src/content_hash.rs | 0 lib/{ => revset-parser}/src/dsl_util.rs | 0 lib/revset-parser/src/fmt.rs | 84 ++++++++ lib/{ => revset-parser}/src/hex_util.rs | 0 lib/revset-parser/src/lib.rs | 29 +++ .../src/parser.rs} | 19 +- lib/{ => revset-parser}/src/ref_name.rs | 13 +- lib/{ => revset-parser}/src/revset.pest | 0 lib/src/lib.rs | 11 +- lib/src/revset.rs | 64 +----- 17 files changed, 407 insertions(+), 94 deletions(-) create mode 100644 lib/revset-parser/Cargo.toml create mode 100644 lib/revset-parser/LICENSE rename lib/{ => revset-parser}/src/content_hash.rs (100%) rename lib/{ => revset-parser}/src/dsl_util.rs (100%) create mode 100644 lib/revset-parser/src/fmt.rs rename lib/{ => revset-parser}/src/hex_util.rs (100%) create mode 100644 lib/revset-parser/src/lib.rs rename lib/{src/revset_parser.rs => revset-parser/src/parser.rs} (99%) rename lib/{ => revset-parser}/src/ref_name.rs (97%) rename lib/{ => revset-parser}/src/revset.pest (100%) diff --git a/Cargo.lock b/Cargo.lock index 61e8ea183e3..e0772573b9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2592,6 +2592,7 @@ dependencies = [ "interim", "itertools 0.15.0", "jj-lib-proc-macros", + "jj-revset-parser", "maplit", "nix 0.31.3", "num_cpus", @@ -2613,7 +2614,6 @@ dependencies = [ "sapling-renderdag", "serde", "smallvec", - "strsim", "tempfile", "test-case", "testutils", @@ -2634,6 +2634,25 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "jj-revset-parser" +version = "0.43.0" +dependencies = [ + "assert_matches", + "blake2", + "digest", + "eyre", + "insta", + "itertools 0.15.0", + "jj-lib-proc-macros", + "pest", + "pest_derive", + "ref-cast", + "serde", + "strsim", + "thiserror 2.0.18", +] + [[package]] name = "js-sys" version = "0.3.103" diff --git a/Cargo.toml b/Cargo.toml index b65d1fd23da..c346d236166 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ cargo-features = [] [workspace] resolver = "3" -members = ["cli", "lib", "lib/gen-protos", "lib/proc-macros", "lib/testutils"] +members = ["cli", "lib", "lib/gen-protos", "lib/proc-macros", "lib/revset-parser", "lib/testutils"] [workspace.package] version = "0.43.0" @@ -138,6 +138,7 @@ winreg = "0.56" # their own (alphabetically sorted) block jj-lib = { path = "lib", version = "0.43.0", default-features = false } jj-lib-proc-macros = { path = "lib/proc-macros", version = "0.43.0" } +jj-revset-parser = { path = "lib/revset-parser", version = "0.43.0" } testutils = { path = "lib/testutils" } [workspace.lints.clippy] diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 7825be2aa2d..ab8a26fca53 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -48,6 +48,7 @@ indexmap = { workspace = true } interim = { workspace = true } itertools = { workspace = true } jj-lib-proc-macros = { workspace = true } +jj-revset-parser = { workspace = true } maplit = { workspace = true } once_cell = { workspace = true } pest = { workspace = true } @@ -61,7 +62,6 @@ ref-cast = { workspace = true } regex = { workspace = true } serde = { workspace = true } smallvec = { workspace = true } -strsim = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, optional = true } diff --git a/lib/proc-macros/src/content_hash.rs b/lib/proc-macros/src/content_hash.rs index e7d3d31b9ec..4ecedc49efc 100644 --- a/lib/proc-macros/src/content_hash.rs +++ b/lib/proc-macros/src/content_hash.rs @@ -18,7 +18,7 @@ pub fn add_trait_bounds(mut generics: Generics) -> Generics { if let GenericParam::Type(type_param) = param { type_param .bounds - .push(parse_quote!(::jj_lib::content_hash::ContentHash)); + .push(parse_quote!(::jj_revset_parser::content_hash::ContentHash)); } } generics @@ -32,7 +32,7 @@ pub fn generate_hash_impl(data: &Data) -> TokenStream { let field_name = &f.ident; let ty = &f.ty; quote_spanned! {ty.span()=> - <#ty as ::jj_lib::content_hash::ContentHash>::hash( + <#ty as ::jj_revset_parser::content_hash::ContentHash>::hash( &self.#field_name, state); } }); @@ -45,7 +45,8 @@ pub fn generate_hash_impl(data: &Data) -> TokenStream { let index = Index::from(i); let ty = &f.ty; quote_spanned! {ty.span() => - <#ty as ::jj_lib::content_hash::ContentHash>::hash(&self.#index, state); + <#ty as ::jj_revset_parser::content_hash::ContentHash>::hash( + &self.#index, state); } }); quote! { @@ -86,7 +87,7 @@ pub fn generate_hash_impl(data: &Data) -> TokenStream { let ix = index_to_ordinal(i); quote_spanned! {v.span() => Self::#variant_id => { - ::jj_lib::content_hash::ContentHash::hash(&#ix, state); + ::jj_revset_parser::content_hash::ContentHash::hash(&#ix, state); } } } @@ -137,10 +138,11 @@ fn hash_statements_for_enum_fields<'a>( let ix = index_to_ordinal(index); let typed_bindings = enum_bindings_with_type(fields); let mut hash_statements = Vec::with_capacity(typed_bindings.len() + 1); - hash_statements.push(quote! {::jj_lib::content_hash::ContentHash::hash(&#ix, state);}); + hash_statements + .push(quote! {::jj_revset_parser::content_hash::ContentHash::hash(&#ix, state);}); for (ty, b) in &typed_bindings { hash_statements.push(quote_spanned! {b.span() => - <#ty as ::jj_lib::content_hash::ContentHash>::hash(#b, state); + <#ty as ::jj_revset_parser::content_hash::ContentHash>::hash(#b, state); }); } diff --git a/lib/proc-macros/src/lib.rs b/lib/proc-macros/src/lib.rs index 83937aece71..f2b8591b478 100644 --- a/lib/proc-macros/src/lib.rs +++ b/lib/proc-macros/src/lib.rs @@ -27,9 +27,9 @@ pub fn derive_content_hash(input: proc_macro::TokenStream) -> proc_macro::TokenS let expanded = quote! { #[automatically_derived] - impl #impl_generics ::jj_lib::content_hash::ContentHash for #name #ty_generics + impl #impl_generics ::jj_revset_parser::content_hash::ContentHash for #name #ty_generics #where_clause { - fn hash(&self, state: &mut impl ::jj_lib::content_hash::DigestUpdate) { + fn hash(&self, state: &mut impl ::jj_revset_parser::content_hash::DigestUpdate) { #hash_impl } } diff --git a/lib/revset-parser/Cargo.toml b/lib/revset-parser/Cargo.toml new file mode 100644 index 00000000000..a44696ef37d --- /dev/null +++ b/lib/revset-parser/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "jj-revset-parser" +description = "Revset language parser for the jj-lib crate" +autotests = false + +version = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +license = { workspace = true } +homepage = { workspace = true } +repository = { workspace = true } +documentation = { workspace = true } +readme = { workspace = true } + +include = ["/LICENSE", "/src/"] + +[dependencies] +blake2 = { workspace = true } +digest = { workspace = true } +itertools = { workspace = true } +jj-lib-proc-macros = { workspace = true } +pest = { workspace = true } +pest_derive = { workspace = true } +ref-cast = { workspace = true } +serde = { workspace = true } +strsim = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +assert_matches = { workspace = true } +eyre = { workspace = true } +insta = { workspace = true } + +[lints] +workspace = true diff --git a/lib/revset-parser/LICENSE b/lib/revset-parser/LICENSE new file mode 100644 index 00000000000..d6456956733 --- /dev/null +++ b/lib/revset-parser/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/lib/src/content_hash.rs b/lib/revset-parser/src/content_hash.rs similarity index 100% rename from lib/src/content_hash.rs rename to lib/revset-parser/src/content_hash.rs diff --git a/lib/src/dsl_util.rs b/lib/revset-parser/src/dsl_util.rs similarity index 100% rename from lib/src/dsl_util.rs rename to lib/revset-parser/src/dsl_util.rs diff --git a/lib/revset-parser/src/fmt.rs b/lib/revset-parser/src/fmt.rs new file mode 100644 index 00000000000..98e546acfad --- /dev/null +++ b/lib/revset-parser/src/fmt.rs @@ -0,0 +1,84 @@ +// Copyright 2026 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. + +//! Formatting of symbols and strings for the revset language. + +use crate::dsl_util::escape_string; +use crate::parser::is_identifier; + +/// Formats a string as symbol by quoting and escaping it if necessary. +/// +/// Note that symbols may be substituted to user aliases. Use +/// [`format_string()`] to ensure that the provided string is resolved as a +/// tag/bookmark name, commit/change ID prefix, etc. +pub fn format_symbol(literal: &str) -> String { + if is_identifier(literal) { + literal.to_string() + } else { + format_string(literal) + } +} + +/// Formats a string by quoting and escaping it. +pub fn format_string(literal: &str) -> String { + format!(r#""{}""#, escape_string(literal)) +} + +/// Formats a `name@remote` symbol, applies quoting and escaping if necessary. +pub fn format_remote_symbol(name: &str, remote: &str) -> String { + let name = format_symbol(name); + let remote = format_symbol(remote); + format!("{name}@{remote}") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_escape_string_literal() { + // Valid identifiers don't need quoting + assert_eq!(format_symbol("foo"), "foo"); + assert_eq!(format_symbol("foo.bar"), "foo.bar"); + + // Invalid identifiers need quoting + assert_eq!(format_symbol("foo@bar"), r#""foo@bar""#); + assert_eq!(format_symbol("foo bar"), r#""foo bar""#); + assert_eq!(format_symbol(" foo "), r#"" foo ""#); + assert_eq!(format_symbol("(foo)"), r#""(foo)""#); + assert_eq!(format_symbol("all:foo"), r#""all:foo""#); + + // Some characters also need escaping + assert_eq!(format_symbol("foo\"bar"), r#""foo\"bar""#); + assert_eq!(format_symbol("foo\\bar"), r#""foo\\bar""#); + assert_eq!(format_symbol("foo\\\"bar"), r#""foo\\\"bar""#); + assert_eq!(format_symbol("foo\nbar"), r#""foo\nbar""#); + + // Some characters don't technically need escaping, but we escape them for + // clarity + assert_eq!(format_symbol("foo\"bar"), r#""foo\"bar""#); + assert_eq!(format_symbol("foo\\bar"), r#""foo\\bar""#); + assert_eq!(format_symbol("foo\\\"bar"), r#""foo\\\"bar""#); + assert_eq!(format_symbol("foo \x01 bar"), r#""foo \x01 bar""#); + } + + #[test] + fn test_escape_remote_symbol() { + assert_eq!(format_remote_symbol("foo", "bar"), "foo@bar"); + assert_eq!( + format_remote_symbol(" foo ", "bar:baz"), + r#"" foo "@"bar:baz""# + ); + } +} diff --git a/lib/src/hex_util.rs b/lib/revset-parser/src/hex_util.rs similarity index 100% rename from lib/src/hex_util.rs rename to lib/revset-parser/src/hex_util.rs diff --git a/lib/revset-parser/src/lib.rs b/lib/revset-parser/src/lib.rs new file mode 100644 index 00000000000..19581412d25 --- /dev/null +++ b/lib/revset-parser/src/lib.rs @@ -0,0 +1,29 @@ +// Copyright 2026 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. + +//! Revset language parser and shared DSL/name utilities. + +// Needed so that proc macros can be used inside jj_lib and by external crates +// that depend on it. +// See: +// - https://github.com/rust-lang/rust/issues/54647#issuecomment-432015102 +// - https://github.com/rust-lang/rust/issues/54363 +extern crate self as jj_revset_parser; + +pub mod content_hash; +pub mod dsl_util; +pub mod fmt; +pub mod hex_util; +pub mod parser; +pub mod ref_name; diff --git a/lib/src/revset_parser.rs b/lib/revset-parser/src/parser.rs similarity index 99% rename from lib/src/revset_parser.rs rename to lib/revset-parser/src/parser.rs index 163f6f2b585..41e57697248 100644 --- a/lib/src/revset_parser.rs +++ b/lib/revset-parser/src/parser.rs @@ -44,6 +44,7 @@ use crate::dsl_util::FunctionCallParser; use crate::dsl_util::InvalidArguments; use crate::dsl_util::StringLiteralParser; use crate::dsl_util::collect_similar; +pub use crate::fmt; use crate::ref_name::RefNameBuf; use crate::ref_name::RemoteNameBuf; use crate::ref_name::RemoteRefSymbolBuf; @@ -198,7 +199,7 @@ pub enum RevsetParseErrorKind { } impl RevsetParseError { - pub(super) fn with_span(kind: RevsetParseErrorKind, span: pest::Span<'_>) -> Self { + pub fn with_span(kind: RevsetParseErrorKind, span: pest::Span<'_>) -> Self { let message = kind.to_string(); let pest_error = Box::new(pest::error::Error::new_from_span( pest::error::ErrorVariant::CustomError { message }, @@ -211,10 +212,7 @@ impl RevsetParseError { } } - pub(super) fn with_source( - mut self, - source: impl Into>, - ) -> Self { + pub fn with_source(mut self, source: impl Into>) -> Self { self.source = Some(source.into()); self } @@ -226,7 +224,7 @@ impl RevsetParseError { /// If this is a `NoSuchFunction` error, expands the candidates list with /// the given `other_functions`. - pub(super) fn extend_function_candidates(mut self, other_functions: I) -> Self + pub fn extend_function_candidates(mut self, other_functions: I) -> Self where I: IntoIterator, I::Item: AsRef, @@ -733,7 +731,7 @@ impl AliasDefinitionParser for RevsetAliasParser { } } -pub(super) fn expect_string_pattern<'a>( +pub fn expect_string_pattern<'a>( type_name: &str, node: &'a ExpressionNode<'_>, ) -> Result<(&'a str, Option<&'a str>), RevsetParseError> { @@ -763,7 +761,7 @@ pub fn expect_literal( }) } -pub(super) fn expect_string_literal<'a>( +pub fn expect_string_literal<'a>( type_name: &str, node: &'a ExpressionNode<'_>, ) -> Result<&'a str, RevsetParseError> { @@ -779,7 +777,7 @@ pub(super) fn expect_string_literal<'a>( /// Applies the given function to the innermost `node` by unwrapping alias /// expansion nodes. Appends alias expansion stack to error and diagnostics. -pub(super) fn catch_aliases<'a, 'i, T>( +pub fn catch_aliases<'a, 'i, T>( diagnostics: &mut RevsetDiagnostics, node: &'a ExpressionNode<'i>, f: impl FnOnce(&mut RevsetDiagnostics, &'a ExpressionNode<'i>) -> Result, @@ -831,7 +829,8 @@ mod tests { use super::*; use crate::dsl_util::KeywordArgument; - use crate::tests::TestResult; + + type TestResult = eyre::Result; #[derive(Debug)] struct WithRevsetAliasesMap<'i> { diff --git a/lib/src/ref_name.rs b/lib/revset-parser/src/ref_name.rs similarity index 97% rename from lib/src/ref_name.rs rename to lib/revset-parser/src/ref_name.rs index 1b8926e4599..101e0b54375 100644 --- a/lib/src/ref_name.rs +++ b/lib/revset-parser/src/ref_name.rs @@ -16,18 +16,18 @@ //! //! Name types can be constructed from a string: //! ``` -//! # use jj_lib::ref_name::*; +//! # use jj_revset_parser::ref_name::*; //! let _: RefNameBuf = "main".into(); //! let _: &RemoteName = "origin".as_ref(); //! ``` //! //! However, they cannot be converted to other name types: //! ```compile_fail -//! # use jj_lib::ref_name::*; +//! # use jj_revset_parser::ref_name::*; //! let _: RefNameBuf = RemoteName::new("origin").into(); //! ``` //! ```compile_fail -//! # use jj_lib::ref_name::*; +//! # use jj_revset_parser::ref_name::*; //! let _: &RemoteName = RefName::new("main").as_ref(); //! ``` @@ -40,7 +40,8 @@ use ref_cast::RefCastCustom; use ref_cast::ref_cast_custom; use crate::content_hash::ContentHash; -use crate::revset; +use crate::fmt::format_remote_symbol; +use crate::fmt::format_symbol; /// Owned Git ref name in fully-qualified form (e.g. `refs/heads/main`.) /// @@ -333,7 +334,7 @@ impl RefSymbol { impl Display for RefSymbol { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.pad(&revset::format_symbol(&self.0)) + f.pad(&format_symbol(&self.0)) } } @@ -420,6 +421,6 @@ impl Display for RemoteRefSymbolBuf { impl Display for RemoteRefSymbol<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let RemoteRefSymbol { name, remote } = self; - f.pad(&revset::format_remote_symbol(&name.0, &remote.0)) + f.pad(&format_remote_symbol(&name.0, &remote.0)) } } diff --git a/lib/src/revset.pest b/lib/revset-parser/src/revset.pest similarity index 100% rename from lib/src/revset.pest rename to lib/revset-parser/src/revset.pest diff --git a/lib/src/lib.rs b/lib/src/lib.rs index c97831a80ad..eee8e9b49e0 100644 --- a/lib/src/lib.rs +++ b/lib/src/lib.rs @@ -25,8 +25,7 @@ // - https://github.com/rust-lang/rust/issues/54363 extern crate self as jj_lib; -#[macro_use] -pub mod content_hash; +pub use jj_revset_parser::content_hash; pub mod absorb; pub mod annotate; @@ -45,7 +44,7 @@ pub mod default_index; pub mod default_submodule_store; pub mod diff; pub mod diff_presentation; -pub mod dsl_util; +pub use jj_revset_parser::dsl_util; pub(crate) mod eol; pub mod evolution; pub mod extensions_map; @@ -66,7 +65,7 @@ pub mod gitignore; pub mod gpg_signing; pub mod graph; pub mod graph_dominators; -pub mod hex_util; +pub use jj_revset_parser::hex_util; pub mod id_prefix; pub mod index; pub mod iter_util; @@ -83,12 +82,12 @@ pub mod op_walk; pub mod operation; #[expect(missing_docs)] pub mod protos; -pub mod ref_name; +pub use jj_revset_parser::ref_name; pub mod refs; pub mod repo; pub mod repo_path; pub mod revset; -mod revset_parser; +use jj_revset_parser::parser as revset_parser; pub mod rewrite; #[cfg(feature = "testing")] pub mod secret_backend; diff --git a/lib/src/revset.rs b/lib/src/revset.rs index 97845455ee3..516bc830b88 100644 --- a/lib/src/revset.rs +++ b/lib/src/revset.rs @@ -28,6 +28,9 @@ use futures::Stream; use futures::StreamExt as _; use futures::stream::LocalBoxStream; use itertools::Itertools as _; +pub use jj_revset_parser::fmt::format_remote_symbol; +pub use jj_revset_parser::fmt::format_string; +pub use jj_revset_parser::fmt::format_symbol; use pollster::FutureExt as _; use thiserror::Error; @@ -3540,31 +3543,6 @@ pub struct RevsetWorkspaceContext<'a> { pub workspace_name: &'a WorkspaceName, } -/// Formats a string as symbol by quoting and escaping it if necessary. -/// -/// Note that symbols may be substituted to user aliases. Use -/// [`format_string()`] to ensure that the provided string is resolved as a -/// tag/bookmark name, commit/change ID prefix, etc. -pub fn format_symbol(literal: &str) -> String { - if revset_parser::is_identifier(literal) { - literal.to_string() - } else { - format_string(literal) - } -} - -/// Formats a string by quoting and escaping it. -pub fn format_string(literal: &str) -> String { - format!(r#""{}""#, dsl_util::escape_string(literal)) -} - -/// Formats a `name@remote` symbol, applies quoting and escaping if necessary. -pub fn format_remote_symbol(name: &str, remote: &str) -> String { - let name = format_symbol(name); - let remote = format_symbol(remote); - format!("{name}@{remote}") -} - #[cfg(test)] #[rustversion::attr( since(1.89), @@ -6395,40 +6373,4 @@ mod tests { "#); Ok(()) } - - #[test] - fn test_escape_string_literal() { - // Valid identifiers don't need quoting - assert_eq!(format_symbol("foo"), "foo"); - assert_eq!(format_symbol("foo.bar"), "foo.bar"); - - // Invalid identifiers need quoting - assert_eq!(format_symbol("foo@bar"), r#""foo@bar""#); - assert_eq!(format_symbol("foo bar"), r#""foo bar""#); - assert_eq!(format_symbol(" foo "), r#"" foo ""#); - assert_eq!(format_symbol("(foo)"), r#""(foo)""#); - assert_eq!(format_symbol("all:foo"), r#""all:foo""#); - - // Some characters also need escaping - assert_eq!(format_symbol("foo\"bar"), r#""foo\"bar""#); - assert_eq!(format_symbol("foo\\bar"), r#""foo\\bar""#); - assert_eq!(format_symbol("foo\\\"bar"), r#""foo\\\"bar""#); - assert_eq!(format_symbol("foo\nbar"), r#""foo\nbar""#); - - // Some characters don't technically need escaping, but we escape them for - // clarity - assert_eq!(format_symbol("foo\"bar"), r#""foo\"bar""#); - assert_eq!(format_symbol("foo\\bar"), r#""foo\\bar""#); - assert_eq!(format_symbol("foo\\\"bar"), r#""foo\\\"bar""#); - assert_eq!(format_symbol("foo \x01 bar"), r#""foo \x01 bar""#); - } - - #[test] - fn test_escape_remote_symbol() { - assert_eq!(format_remote_symbol("foo", "bar"), "foo@bar"); - assert_eq!( - format_remote_symbol(" foo ", "bar:baz"), - r#"" foo "@"bar:baz""# - ); - } }