jj_cli/
revset_util.rs

1// Copyright 2022-2024 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Utility for parsing and evaluating user-provided revset expressions.
16
17use std::io;
18use std::sync::Arc;
19
20use itertools::Itertools as _;
21use jj_lib::backend::CommitId;
22use jj_lib::commit::Commit;
23use jj_lib::config::ConfigGetError;
24use jj_lib::config::ConfigNamePathBuf;
25use jj_lib::config::ConfigSource;
26use jj_lib::config::StackedConfig;
27use jj_lib::id_prefix::IdPrefixContext;
28use jj_lib::ref_name::RefNameBuf;
29use jj_lib::repo::Repo;
30use jj_lib::revset;
31use jj_lib::revset::ResolvedRevsetExpression;
32use jj_lib::revset::Revset;
33use jj_lib::revset::RevsetAliasesMap;
34use jj_lib::revset::RevsetDiagnostics;
35use jj_lib::revset::RevsetEvaluationError;
36use jj_lib::revset::RevsetExpression;
37use jj_lib::revset::RevsetExtensions;
38use jj_lib::revset::RevsetIteratorExt as _;
39use jj_lib::revset::RevsetParseContext;
40use jj_lib::revset::RevsetParseError;
41use jj_lib::revset::RevsetResolutionError;
42use jj_lib::revset::SymbolResolver;
43use jj_lib::revset::SymbolResolverExtension;
44use jj_lib::revset::UserRevsetExpression;
45use thiserror::Error;
46
47use crate::command_error::CommandError;
48use crate::command_error::user_error;
49use crate::formatter::Formatter;
50use crate::templater::TemplateRenderer;
51use crate::ui::Ui;
52
53const USER_IMMUTABLE_HEADS: &str = "immutable_heads";
54
55#[derive(Debug, Error)]
56pub enum UserRevsetEvaluationError {
57    #[error(transparent)]
58    Resolution(RevsetResolutionError),
59    #[error(transparent)]
60    Evaluation(RevsetEvaluationError),
61}
62
63/// Wrapper around `UserRevsetExpression` to provide convenient methods.
64pub struct RevsetExpressionEvaluator<'repo> {
65    repo: &'repo dyn Repo,
66    extensions: Arc<RevsetExtensions>,
67    id_prefix_context: &'repo IdPrefixContext,
68    expression: Arc<UserRevsetExpression>,
69}
70
71impl<'repo> RevsetExpressionEvaluator<'repo> {
72    pub fn new(
73        repo: &'repo dyn Repo,
74        extensions: Arc<RevsetExtensions>,
75        id_prefix_context: &'repo IdPrefixContext,
76        expression: Arc<UserRevsetExpression>,
77    ) -> Self {
78        Self {
79            repo,
80            extensions,
81            id_prefix_context,
82            expression,
83        }
84    }
85
86    /// Returns the underlying expression.
87    pub fn expression(&self) -> &Arc<UserRevsetExpression> {
88        &self.expression
89    }
90
91    /// Intersects the underlying expression with the `other` expression.
92    pub fn intersect_with(&mut self, other: &Arc<UserRevsetExpression>) {
93        self.expression = self.expression.intersection(other);
94    }
95
96    /// Resolves user symbols in the expression, returns new expression.
97    pub fn resolve(&self) -> Result<Arc<ResolvedRevsetExpression>, RevsetResolutionError> {
98        let symbol_resolver = default_symbol_resolver(
99            self.repo,
100            self.extensions.symbol_resolvers(),
101            self.id_prefix_context,
102        );
103        self.expression
104            .resolve_user_expression(self.repo, &symbol_resolver)
105    }
106
107    /// Evaluates the expression.
108    pub fn evaluate(&self) -> Result<Box<dyn Revset + 'repo>, UserRevsetEvaluationError> {
109        self.resolve()
110            .map_err(UserRevsetEvaluationError::Resolution)?
111            .evaluate(self.repo)
112            .map_err(UserRevsetEvaluationError::Evaluation)
113    }
114
115    /// Evaluates the expression to an iterator over commit ids. Entries are
116    /// sorted in reverse topological order.
117    pub fn evaluate_to_commit_ids(
118        &self,
119    ) -> Result<
120        Box<dyn Iterator<Item = Result<CommitId, RevsetEvaluationError>> + 'repo>,
121        UserRevsetEvaluationError,
122    > {
123        Ok(self.evaluate()?.iter())
124    }
125
126    /// Evaluates the expression to an iterator over commit objects. Entries are
127    /// sorted in reverse topological order.
128    pub fn evaluate_to_commits(
129        &self,
130    ) -> Result<
131        impl Iterator<Item = Result<Commit, RevsetEvaluationError>> + use<'repo>,
132        UserRevsetEvaluationError,
133    > {
134        Ok(self.evaluate()?.iter().commits(self.repo.store()))
135    }
136}
137
138fn warn_user_redefined_builtin(
139    ui: &Ui,
140    source: ConfigSource,
141    name: &str,
142) -> Result<(), CommandError> {
143    match source {
144        ConfigSource::Default => (),
145        ConfigSource::EnvBase
146        | ConfigSource::User
147        | ConfigSource::Repo
148        | ConfigSource::Workspace
149        | ConfigSource::EnvOverrides
150        | ConfigSource::CommandArg => {
151            let checked_mutability_builtins =
152                ["mutable()", "immutable()", "builtin_immutable_heads()"];
153
154            if checked_mutability_builtins.contains(&name) {
155                writeln!(
156                    ui.warning_default(),
157                    "Redefining `revset-aliases.{name}` is not recommended; redefine \
158                     `immutable_heads()` instead",
159                )?;
160            }
161        }
162    }
163
164    Ok(())
165}
166
167pub fn load_revset_aliases(
168    ui: &Ui,
169    stacked_config: &StackedConfig,
170) -> Result<RevsetAliasesMap, CommandError> {
171    let table_name = ConfigNamePathBuf::from_iter(["revset-aliases"]);
172    let mut aliases_map = RevsetAliasesMap::new();
173    // Load from all config layers in order. 'f(x)' in default layer should be
174    // overridden by 'f(a)' in user.
175    for layer in stacked_config.layers() {
176        let table = match layer.look_up_table(&table_name) {
177            Ok(Some(table)) => table,
178            Ok(None) => continue,
179            Err(item) => {
180                return Err(ConfigGetError::Type {
181                    name: table_name.to_string(),
182                    error: format!("Expected a table, but is {}", item.type_name()).into(),
183                    source_path: layer.path.clone(),
184                }
185                .into());
186            }
187        };
188        for (decl, item) in table.iter() {
189            warn_user_redefined_builtin(ui, layer.source, decl)?;
190
191            let r = item
192                .as_str()
193                .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
194                .and_then(|v| aliases_map.insert(decl, v).map_err(|e| e.to_string()));
195            if let Err(s) = r {
196                writeln!(
197                    ui.warning_default(),
198                    "Failed to load `{table_name}.{decl}`: {s}"
199                )?;
200            }
201        }
202    }
203    Ok(aliases_map)
204}
205
206/// Wraps the given `IdPrefixContext` in `SymbolResolver` to be passed in to
207/// `evaluate()`.
208pub fn default_symbol_resolver<'a>(
209    repo: &'a dyn Repo,
210    extensions: &[impl AsRef<dyn SymbolResolverExtension>],
211    id_prefix_context: &'a IdPrefixContext,
212) -> SymbolResolver<'a> {
213    SymbolResolver::new(repo, extensions).with_id_prefix_context(id_prefix_context)
214}
215
216/// Parses user-configured expression defining the heads of the immutable set.
217/// Includes the root commit.
218pub fn parse_immutable_heads_expression(
219    diagnostics: &mut RevsetDiagnostics,
220    context: &RevsetParseContext,
221) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
222    let (_, _, immutable_heads_str) = context
223        .aliases_map
224        .get_function(USER_IMMUTABLE_HEADS, 0)
225        .unwrap();
226    let heads = revset::parse(diagnostics, immutable_heads_str, context)?;
227    Ok(heads.union(&RevsetExpression::root()))
228}
229
230/// Prints warning if `trunk()` alias cannot be resolved. This alias could be
231/// generated by `jj git init`/`clone`.
232pub(super) fn warn_unresolvable_trunk(
233    ui: &Ui,
234    repo: &dyn Repo,
235    context: &RevsetParseContext,
236) -> io::Result<()> {
237    let (_, _, revset_str) = context
238        .aliases_map
239        .get_function("trunk", 0)
240        .expect("trunk() should be defined by default");
241    let Ok(expression) = revset::parse(&mut RevsetDiagnostics::new(), revset_str, context) else {
242        // Parse error would have been reported.
243        return Ok(());
244    };
245    // Not using IdPrefixContext since trunk() revset shouldn't contain short
246    // prefixes.
247    let symbol_resolver = SymbolResolver::new(repo, context.extensions.symbol_resolvers());
248    if let Err(err) = expression.resolve_user_expression(repo, &symbol_resolver) {
249        writeln!(
250            ui.warning_default(),
251            "Failed to resolve `revset-aliases.trunk()`: {err}"
252        )?;
253        writeln!(
254            ui.hint_default(),
255            "Use `jj config edit --repo` to adjust the `trunk()` alias."
256        )?;
257    }
258    Ok(())
259}
260
261pub(super) fn evaluate_revset_to_single_commit<'a>(
262    revision_str: &str,
263    expression: &RevsetExpressionEvaluator<'_>,
264    commit_summary_template: impl FnOnce() -> TemplateRenderer<'a, Commit>,
265) -> Result<Commit, CommandError> {
266    let mut iter = expression.evaluate_to_commits()?.fuse();
267    match (iter.next(), iter.next()) {
268        (Some(commit), None) => Ok(commit?),
269        (None, _) => Err(user_error(format!(
270            "Revset `{revision_str}` didn't resolve to any revisions"
271        ))),
272        (Some(commit0), Some(commit1)) => {
273            let mut iter = [commit0, commit1].into_iter().chain(iter);
274            let commits: Vec<_> = iter.by_ref().take(5).try_collect()?;
275            let elided = iter.next().is_some();
276            Err(format_multiple_revisions_error(
277                revision_str,
278                &commits,
279                elided,
280                &commit_summary_template(),
281            ))
282        }
283    }
284}
285
286fn format_multiple_revisions_error(
287    revision_str: &str,
288    commits: &[Commit],
289    elided: bool,
290    template: &TemplateRenderer<'_, Commit>,
291) -> CommandError {
292    assert!(commits.len() >= 2);
293    let mut cmd_err = user_error(format!(
294        "Revset `{revision_str}` resolved to more than one revision"
295    ));
296    let write_commits_summary = |formatter: &mut dyn Formatter| {
297        for commit in commits {
298            write!(formatter, "  ")?;
299            template.format(commit, formatter)?;
300            writeln!(formatter)?;
301        }
302        if elided {
303            writeln!(formatter, "  ...")?;
304        }
305        Ok(())
306    };
307    cmd_err.add_formatted_hint_with(|formatter| {
308        writeln!(
309            formatter,
310            "The revset `{revision_str}` resolved to these revisions:"
311        )?;
312        write_commits_summary(formatter)
313    });
314    cmd_err
315}
316
317#[derive(Debug, Error)]
318#[error("Failed to parse bookmark name: {}", source.kind())]
319pub struct BookmarkNameParseError {
320    pub input: String,
321    pub source: RevsetParseError,
322}
323
324/// Parses bookmark name specified in revset syntax.
325pub fn parse_bookmark_name(text: &str) -> Result<RefNameBuf, BookmarkNameParseError> {
326    revset::parse_symbol(text)
327        .map(Into::into)
328        .map_err(|source| BookmarkNameParseError {
329            input: text.to_owned(),
330            source,
331        })
332}
333
334#[derive(Debug, Error)]
335#[error("Failed to parse tag name: {}", source.kind())]
336pub struct TagNameParseError {
337    pub source: RevsetParseError,
338}
339
340/// Parses tag name specified in revset syntax.
341pub fn parse_tag_name(text: &str) -> Result<RefNameBuf, TagNameParseError> {
342    revset::parse_symbol(text)
343        .map(Into::into)
344        .map_err(|source| TagNameParseError { source })
345}