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::EnvOverrides
149        | ConfigSource::CommandArg => {
150            let checked_mutability_builtins =
151                ["mutable()", "immutable()", "builtin_immutable_heads()"];
152
153            if checked_mutability_builtins.contains(&name) {
154                writeln!(
155                    ui.warning_default(),
156                    "Redefining `revset-aliases.{name}` is not recommended; redefine \
157                     `immutable_heads()` instead",
158                )?;
159            }
160        }
161    }
162
163    Ok(())
164}
165
166pub fn load_revset_aliases(
167    ui: &Ui,
168    stacked_config: &StackedConfig,
169) -> Result<RevsetAliasesMap, CommandError> {
170    let table_name = ConfigNamePathBuf::from_iter(["revset-aliases"]);
171    let mut aliases_map = RevsetAliasesMap::new();
172    // Load from all config layers in order. 'f(x)' in default layer should be
173    // overridden by 'f(a)' in user.
174    for layer in stacked_config.layers() {
175        let table = match layer.look_up_table(&table_name) {
176            Ok(Some(table)) => table,
177            Ok(None) => continue,
178            Err(item) => {
179                return Err(ConfigGetError::Type {
180                    name: table_name.to_string(),
181                    error: format!("Expected a table, but is {}", item.type_name()).into(),
182                    source_path: layer.path.clone(),
183                }
184                .into());
185            }
186        };
187        for (decl, item) in table.iter() {
188            warn_user_redefined_builtin(ui, layer.source, decl)?;
189
190            let r = item
191                .as_str()
192                .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
193                .and_then(|v| aliases_map.insert(decl, v).map_err(|e| e.to_string()));
194            if let Err(s) = r {
195                writeln!(
196                    ui.warning_default(),
197                    "Failed to load `{table_name}.{decl}`: {s}"
198                )?;
199            }
200        }
201    }
202    Ok(aliases_map)
203}
204
205/// Wraps the given `IdPrefixContext` in `SymbolResolver` to be passed in to
206/// `evaluate()`.
207pub fn default_symbol_resolver<'a>(
208    repo: &'a dyn Repo,
209    extensions: &[impl AsRef<dyn SymbolResolverExtension>],
210    id_prefix_context: &'a IdPrefixContext,
211) -> SymbolResolver<'a> {
212    SymbolResolver::new(repo, extensions).with_id_prefix_context(id_prefix_context)
213}
214
215/// Parses user-configured expression defining the heads of the immutable set.
216/// Includes the root commit.
217pub fn parse_immutable_heads_expression(
218    diagnostics: &mut RevsetDiagnostics,
219    context: &RevsetParseContext,
220) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
221    let (_, _, immutable_heads_str) = context
222        .aliases_map
223        .get_function(USER_IMMUTABLE_HEADS, 0)
224        .unwrap();
225    let heads = revset::parse(diagnostics, immutable_heads_str, context)?;
226    Ok(heads.union(&RevsetExpression::root()))
227}
228
229/// Prints warning if `trunk()` alias cannot be resolved. This alias could be
230/// generated by `jj git init`/`clone`.
231pub(super) fn warn_unresolvable_trunk(
232    ui: &Ui,
233    repo: &dyn Repo,
234    context: &RevsetParseContext,
235) -> io::Result<()> {
236    let (_, _, revset_str) = context
237        .aliases_map
238        .get_function("trunk", 0)
239        .expect("trunk() should be defined by default");
240    let Ok(expression) = revset::parse(&mut RevsetDiagnostics::new(), revset_str, context) else {
241        // Parse error would have been reported.
242        return Ok(());
243    };
244    // Not using IdPrefixContext since trunk() revset shouldn't contain short
245    // prefixes.
246    let symbol_resolver = SymbolResolver::new(repo, context.extensions.symbol_resolvers());
247    if let Err(err) = expression.resolve_user_expression(repo, &symbol_resolver) {
248        writeln!(
249            ui.warning_default(),
250            "Failed to resolve `revset-aliases.trunk()`: {err}"
251        )?;
252        writeln!(
253            ui.hint_default(),
254            "Use `jj config edit --repo` to adjust the `trunk()` alias."
255        )?;
256    }
257    Ok(())
258}
259
260pub(super) fn evaluate_revset_to_single_commit<'a>(
261    revision_str: &str,
262    expression: &RevsetExpressionEvaluator<'_>,
263    commit_summary_template: impl FnOnce() -> TemplateRenderer<'a, Commit>,
264) -> Result<Commit, CommandError> {
265    let mut iter = expression.evaluate_to_commits()?.fuse();
266    match (iter.next(), iter.next()) {
267        (Some(commit), None) => Ok(commit?),
268        (None, _) => Err(user_error(format!(
269            "Revset `{revision_str}` didn't resolve to any revisions"
270        ))),
271        (Some(commit0), Some(commit1)) => {
272            let mut iter = [commit0, commit1].into_iter().chain(iter);
273            let commits: Vec<_> = iter.by_ref().take(5).try_collect()?;
274            let elided = iter.next().is_some();
275            Err(format_multiple_revisions_error(
276                revision_str,
277                &commits,
278                elided,
279                &commit_summary_template(),
280            ))
281        }
282    }
283}
284
285fn format_multiple_revisions_error(
286    revision_str: &str,
287    commits: &[Commit],
288    elided: bool,
289    template: &TemplateRenderer<'_, Commit>,
290) -> CommandError {
291    assert!(commits.len() >= 2);
292    let mut cmd_err = user_error(format!(
293        "Revset `{revision_str}` resolved to more than one revision"
294    ));
295    let write_commits_summary = |formatter: &mut dyn Formatter| {
296        for commit in commits {
297            write!(formatter, "  ")?;
298            template.format(commit, formatter)?;
299            writeln!(formatter)?;
300        }
301        if elided {
302            writeln!(formatter, "  ...")?;
303        }
304        Ok(())
305    };
306    cmd_err.add_formatted_hint_with(|formatter| {
307        writeln!(
308            formatter,
309            "The revset `{revision_str}` resolved to these revisions:"
310        )?;
311        write_commits_summary(formatter)
312    });
313    cmd_err
314}
315
316#[derive(Debug, Error)]
317#[error("Failed to parse bookmark name: {}", source.kind())]
318pub struct BookmarkNameParseError {
319    pub input: String,
320    pub source: RevsetParseError,
321}
322
323/// Parses bookmark name specified in revset syntax.
324pub fn parse_bookmark_name(text: &str) -> Result<RefNameBuf, BookmarkNameParseError> {
325    revset::parse_symbol(text)
326        .map(Into::into)
327        .map_err(|source| BookmarkNameParseError {
328            input: text.to_owned(),
329            source,
330        })
331}