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::rc::Rc;
19use std::sync::Arc;
20
21use itertools::Itertools as _;
22use jj_lib::backend::CommitId;
23use jj_lib::commit::Commit;
24use jj_lib::config::ConfigGetError;
25use jj_lib::config::ConfigNamePathBuf;
26use jj_lib::config::ConfigSource;
27use jj_lib::config::StackedConfig;
28use jj_lib::id_prefix::IdPrefixContext;
29use jj_lib::ref_name::RefNameBuf;
30use jj_lib::repo::Repo;
31use jj_lib::revset;
32use jj_lib::revset::DefaultSymbolResolver;
33use jj_lib::revset::ResolvedRevsetExpression;
34use jj_lib::revset::Revset;
35use jj_lib::revset::RevsetAliasesMap;
36use jj_lib::revset::RevsetDiagnostics;
37use jj_lib::revset::RevsetEvaluationError;
38use jj_lib::revset::RevsetExpression;
39use jj_lib::revset::RevsetExtensions;
40use jj_lib::revset::RevsetIteratorExt as _;
41use jj_lib::revset::RevsetParseContext;
42use jj_lib::revset::RevsetParseError;
43use jj_lib::revset::RevsetResolutionError;
44use jj_lib::revset::SymbolResolverExtension;
45use jj_lib::revset::UserRevsetExpression;
46use thiserror::Error;
47
48use crate::command_error::user_error;
49use crate::command_error::CommandError;
50use crate::formatter::Formatter;
51use crate::templater::TemplateRenderer;
52use crate::ui::Ui;
53
54const USER_IMMUTABLE_HEADS: &str = "immutable_heads";
55
56#[derive(Debug, Error)]
57pub enum UserRevsetEvaluationError {
58    #[error(transparent)]
59    Resolution(RevsetResolutionError),
60    #[error(transparent)]
61    Evaluation(RevsetEvaluationError),
62}
63
64/// Wrapper around `UserRevsetExpression` to provide convenient methods.
65pub struct RevsetExpressionEvaluator<'repo> {
66    repo: &'repo dyn Repo,
67    extensions: Arc<RevsetExtensions>,
68    id_prefix_context: &'repo IdPrefixContext,
69    expression: Rc<UserRevsetExpression>,
70}
71
72impl<'repo> RevsetExpressionEvaluator<'repo> {
73    pub fn new(
74        repo: &'repo dyn Repo,
75        extensions: Arc<RevsetExtensions>,
76        id_prefix_context: &'repo IdPrefixContext,
77        expression: Rc<UserRevsetExpression>,
78    ) -> Self {
79        RevsetExpressionEvaluator {
80            repo,
81            extensions,
82            id_prefix_context,
83            expression,
84        }
85    }
86
87    /// Returns the underlying expression.
88    pub fn expression(&self) -> &Rc<UserRevsetExpression> {
89        &self.expression
90    }
91
92    /// Intersects the underlying expression with the `other` expression.
93    pub fn intersect_with(&mut self, other: &Rc<UserRevsetExpression>) {
94        self.expression = self.expression.intersection(other);
95    }
96
97    /// Resolves user symbols in the expression, returns new expression.
98    pub fn resolve(&self) -> Result<Rc<ResolvedRevsetExpression>, RevsetResolutionError> {
99        let symbol_resolver = default_symbol_resolver(
100            self.repo,
101            self.extensions.symbol_resolvers(),
102            self.id_prefix_context,
103        );
104        self.expression
105            .resolve_user_expression(self.repo, &symbol_resolver)
106    }
107
108    /// Evaluates the expression.
109    pub fn evaluate(&self) -> Result<Box<dyn Revset + 'repo>, UserRevsetEvaluationError> {
110        self.resolve()
111            .map_err(UserRevsetEvaluationError::Resolution)?
112            .evaluate(self.repo)
113            .map_err(UserRevsetEvaluationError::Evaluation)
114    }
115
116    /// Evaluates the expression to an iterator over commit ids. Entries are
117    /// sorted in reverse topological order.
118    pub fn evaluate_to_commit_ids(
119        &self,
120    ) -> Result<
121        Box<dyn Iterator<Item = Result<CommitId, RevsetEvaluationError>> + 'repo>,
122        UserRevsetEvaluationError,
123    > {
124        Ok(self.evaluate()?.iter())
125    }
126
127    /// Evaluates the expression to an iterator over commit objects. Entries are
128    /// sorted in reverse topological order.
129    pub fn evaluate_to_commits(
130        &self,
131    ) -> Result<
132        impl Iterator<Item = Result<Commit, RevsetEvaluationError>> + use<'repo>,
133        UserRevsetEvaluationError,
134    > {
135        Ok(self.evaluate()?.iter().commits(self.repo.store()))
136    }
137}
138
139fn warn_user_redefined_builtin(
140    ui: &Ui,
141    source: ConfigSource,
142    name: &str,
143) -> Result<(), CommandError> {
144    match source {
145        ConfigSource::Default => (),
146        ConfigSource::EnvBase
147        | ConfigSource::User
148        | ConfigSource::Repo
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) -> DefaultSymbolResolver<'a> {
213    DefaultSymbolResolver::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<Rc<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 = DefaultSymbolResolver::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    should_hint_about_all_prefix: bool,
266) -> Result<Commit, CommandError> {
267    let mut iter = expression.evaluate_to_commits()?.fuse();
268    match (iter.next(), iter.next()) {
269        (Some(commit), None) => Ok(commit?),
270        (None, _) => Err(user_error(format!(
271            "Revset `{revision_str}` didn't resolve to any revisions"
272        ))),
273        (Some(commit0), Some(commit1)) => {
274            let mut iter = [commit0, commit1].into_iter().chain(iter);
275            let commits: Vec<_> = iter.by_ref().take(5).try_collect()?;
276            let elided = iter.next().is_some();
277            Err(format_multiple_revisions_error(
278                revision_str,
279                expression.expression(),
280                &commits,
281                elided,
282                &commit_summary_template(),
283                should_hint_about_all_prefix,
284            ))
285        }
286    }
287}
288
289fn format_multiple_revisions_error(
290    revision_str: &str,
291    expression: &UserRevsetExpression,
292    commits: &[Commit],
293    elided: bool,
294    template: &TemplateRenderer<'_, Commit>,
295    should_hint_about_all_prefix: bool,
296) -> CommandError {
297    assert!(commits.len() >= 2);
298    let mut cmd_err = user_error(format!(
299        "Revset `{revision_str}` resolved to more than one revision"
300    ));
301    let write_commits_summary = |formatter: &mut dyn Formatter| {
302        for commit in commits {
303            write!(formatter, "  ")?;
304            template.format(commit, formatter)?;
305            writeln!(formatter)?;
306        }
307        if elided {
308            writeln!(formatter, "  ...")?;
309        }
310        Ok(())
311    };
312    if commits[0].change_id() == commits[1].change_id() {
313        // Separate hint if there's commits with same change id
314        cmd_err.add_formatted_hint_with(|formatter| {
315            writeln!(
316                formatter,
317                "The revset `{revision_str}` resolved to these revisions:"
318            )?;
319            write_commits_summary(formatter)
320        });
321        cmd_err.add_hint(
322            "Some of these commits have the same change id. Abandon the unneeded commits with `jj \
323             abandon <commit_id>`.",
324        );
325    } else if let Some(bookmark_name) = expression.as_symbol() {
326        // Separate hint if there's a conflicted bookmark
327        cmd_err.add_formatted_hint_with(|formatter| {
328            writeln!(
329                formatter,
330                "Bookmark {bookmark_name} resolved to multiple revisions because it's conflicted."
331            )?;
332            writeln!(formatter, "It resolved to these revisions:")?;
333            write_commits_summary(formatter)
334        });
335        cmd_err.add_hint(format!(
336            "Set which revision the bookmark points to with `jj bookmark set {bookmark_name} -r \
337             <REVISION>`.",
338        ));
339    } else {
340        cmd_err.add_formatted_hint_with(|formatter| {
341            writeln!(
342                formatter,
343                "The revset `{revision_str}` resolved to these revisions:"
344            )?;
345            write_commits_summary(formatter)
346        });
347        if should_hint_about_all_prefix {
348            cmd_err.add_hint(format!(
349                "Prefix the expression with `all:` to allow any number of revisions (i.e. \
350                 `all:{revision_str}`)."
351            ));
352        }
353    };
354    cmd_err
355}
356
357#[derive(Debug, Error)]
358#[error("Failed to parse bookmark name: {}", source.kind())]
359pub struct BookmarkNameParseError {
360    pub input: String,
361    pub source: RevsetParseError,
362}
363
364/// Parses bookmark name specified in revset syntax.
365pub fn parse_bookmark_name(text: &str) -> Result<RefNameBuf, BookmarkNameParseError> {
366    revset::parse_symbol(text)
367        .map(Into::into)
368        .map_err(|source| BookmarkNameParseError {
369            input: text.to_owned(),
370            source,
371        })
372}