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 jj_lib::str_util::StringExpression;
46use thiserror::Error;
47
48use crate::command_error::CommandError;
49use crate::command_error::print_parse_diagnostics;
50use crate::command_error::revset_parse_error_hint;
51use crate::command_error::user_error;
52use crate::command_error::user_error_with_message;
53use crate::formatter::Formatter;
54use crate::templater::TemplateRenderer;
55use crate::ui::Ui;
56
57const USER_IMMUTABLE_HEADS: &str = "immutable_heads";
58
59#[derive(Debug, Error)]
60pub enum UserRevsetEvaluationError {
61    #[error(transparent)]
62    Resolution(RevsetResolutionError),
63    #[error(transparent)]
64    Evaluation(RevsetEvaluationError),
65}
66
67/// Wrapper around `UserRevsetExpression` to provide convenient methods.
68pub struct RevsetExpressionEvaluator<'repo> {
69    repo: &'repo dyn Repo,
70    extensions: Arc<RevsetExtensions>,
71    id_prefix_context: &'repo IdPrefixContext,
72    expression: Arc<UserRevsetExpression>,
73}
74
75impl<'repo> RevsetExpressionEvaluator<'repo> {
76    pub fn new(
77        repo: &'repo dyn Repo,
78        extensions: Arc<RevsetExtensions>,
79        id_prefix_context: &'repo IdPrefixContext,
80        expression: Arc<UserRevsetExpression>,
81    ) -> Self {
82        Self {
83            repo,
84            extensions,
85            id_prefix_context,
86            expression,
87        }
88    }
89
90    /// Returns the underlying expression.
91    pub fn expression(&self) -> &Arc<UserRevsetExpression> {
92        &self.expression
93    }
94
95    /// Intersects the underlying expression with the `other` expression.
96    pub fn intersect_with(&mut self, other: &Arc<UserRevsetExpression>) {
97        self.expression = self.expression.intersection(other);
98    }
99
100    /// Resolves user symbols in the expression, returns new expression.
101    pub fn resolve(&self) -> Result<Arc<ResolvedRevsetExpression>, RevsetResolutionError> {
102        let symbol_resolver = default_symbol_resolver(
103            self.repo,
104            self.extensions.symbol_resolvers(),
105            self.id_prefix_context,
106        );
107        self.expression
108            .resolve_user_expression(self.repo, &symbol_resolver)
109    }
110
111    /// Evaluates the expression.
112    pub fn evaluate(&self) -> Result<Box<dyn Revset + 'repo>, UserRevsetEvaluationError> {
113        self.resolve()
114            .map_err(UserRevsetEvaluationError::Resolution)?
115            .evaluate(self.repo)
116            .map_err(UserRevsetEvaluationError::Evaluation)
117    }
118
119    /// Evaluates the expression to an iterator over commit ids. Entries are
120    /// sorted in reverse topological order.
121    pub fn evaluate_to_commit_ids(
122        &self,
123    ) -> Result<
124        Box<dyn Iterator<Item = Result<CommitId, RevsetEvaluationError>> + 'repo>,
125        UserRevsetEvaluationError,
126    > {
127        Ok(self.evaluate()?.iter())
128    }
129
130    /// Evaluates the expression to an iterator over commit objects. Entries are
131    /// sorted in reverse topological order.
132    pub fn evaluate_to_commits(
133        &self,
134    ) -> Result<
135        impl Iterator<Item = Result<Commit, RevsetEvaluationError>> + use<'repo>,
136        UserRevsetEvaluationError,
137    > {
138        Ok(self.evaluate()?.iter().commits(self.repo.store()))
139    }
140}
141
142fn warn_user_redefined_builtin(
143    ui: &Ui,
144    source: ConfigSource,
145    name: &str,
146) -> Result<(), CommandError> {
147    match source {
148        ConfigSource::Default => (),
149        ConfigSource::EnvBase
150        | ConfigSource::User
151        | ConfigSource::Repo
152        | ConfigSource::Workspace
153        | ConfigSource::EnvOverrides
154        | ConfigSource::CommandArg => {
155            let checked_mutability_builtins =
156                ["mutable()", "immutable()", "builtin_immutable_heads()"];
157
158            if checked_mutability_builtins.contains(&name) {
159                writeln!(
160                    ui.warning_default(),
161                    "Redefining `revset-aliases.{name}` is not recommended; redefine \
162                     `immutable_heads()` instead",
163                )?;
164            }
165        }
166    }
167
168    Ok(())
169}
170
171pub fn load_revset_aliases(
172    ui: &Ui,
173    stacked_config: &StackedConfig,
174) -> Result<RevsetAliasesMap, CommandError> {
175    let table_name = ConfigNamePathBuf::from_iter(["revset-aliases"]);
176    let mut aliases_map = RevsetAliasesMap::new();
177    // Load from all config layers in order. 'f(x)' in default layer should be
178    // overridden by 'f(a)' in user.
179    for layer in stacked_config.layers() {
180        let table = match layer.look_up_table(&table_name) {
181            Ok(Some(table)) => table,
182            Ok(None) => continue,
183            Err(item) => {
184                return Err(ConfigGetError::Type {
185                    name: table_name.to_string(),
186                    error: format!("Expected a table, but is {}", item.type_name()).into(),
187                    source_path: layer.path.clone(),
188                }
189                .into());
190            }
191        };
192        for (decl, item) in table.iter() {
193            warn_user_redefined_builtin(ui, layer.source, decl)?;
194
195            let r = item
196                .as_str()
197                .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
198                .and_then(|v| aliases_map.insert(decl, v).map_err(|e| e.to_string()));
199            if let Err(s) = r {
200                writeln!(
201                    ui.warning_default(),
202                    "Failed to load `{table_name}.{decl}`: {s}"
203                )?;
204            }
205        }
206    }
207    Ok(aliases_map)
208}
209
210/// Wraps the given `IdPrefixContext` in `SymbolResolver` to be passed in to
211/// `evaluate()`.
212pub fn default_symbol_resolver<'a>(
213    repo: &'a dyn Repo,
214    extensions: &[impl AsRef<dyn SymbolResolverExtension>],
215    id_prefix_context: &'a IdPrefixContext,
216) -> SymbolResolver<'a> {
217    SymbolResolver::new(repo, extensions).with_id_prefix_context(id_prefix_context)
218}
219
220/// Parses user-configured expression defining the heads of the immutable set.
221/// Includes the root commit.
222pub fn parse_immutable_heads_expression(
223    diagnostics: &mut RevsetDiagnostics,
224    context: &RevsetParseContext,
225) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
226    let (_, _, immutable_heads_str) = context
227        .aliases_map
228        .get_function(USER_IMMUTABLE_HEADS, 0)
229        .unwrap();
230    let heads = revset::parse(diagnostics, immutable_heads_str, context)?;
231    Ok(heads.union(&RevsetExpression::root()))
232}
233
234/// Prints warning if `trunk()` alias cannot be resolved. This alias could be
235/// generated by `jj git init`/`clone`.
236pub(super) fn warn_unresolvable_trunk(
237    ui: &Ui,
238    repo: &dyn Repo,
239    context: &RevsetParseContext,
240) -> io::Result<()> {
241    let (_, _, revset_str) = context
242        .aliases_map
243        .get_function("trunk", 0)
244        .expect("trunk() should be defined by default");
245    let Ok(expression) = revset::parse(&mut RevsetDiagnostics::new(), revset_str, context) else {
246        // Parse error would have been reported.
247        return Ok(());
248    };
249    // Not using IdPrefixContext since trunk() revset shouldn't contain short
250    // prefixes.
251    let symbol_resolver = SymbolResolver::new(repo, context.extensions.symbol_resolvers());
252    if let Err(err) = expression.resolve_user_expression(repo, &symbol_resolver) {
253        writeln!(
254            ui.warning_default(),
255            "Failed to resolve `revset-aliases.trunk()`: {err}"
256        )?;
257        writeln!(
258            ui.hint_default(),
259            "Use `jj config edit --repo` to adjust the `trunk()` alias."
260        )?;
261    }
262    Ok(())
263}
264
265pub(super) fn evaluate_revset_to_single_commit<'a>(
266    revision_str: &str,
267    expression: &RevsetExpressionEvaluator<'_>,
268    commit_summary_template: impl FnOnce() -> TemplateRenderer<'a, Commit>,
269) -> Result<Commit, CommandError> {
270    let mut iter = expression.evaluate_to_commits()?.fuse();
271    match (iter.next(), iter.next()) {
272        (Some(commit), None) => Ok(commit?),
273        (None, _) => Err(user_error(format!(
274            "Revset `{revision_str}` didn't resolve to any revisions"
275        ))),
276        (Some(commit0), Some(commit1)) => {
277            let mut iter = [commit0, commit1].into_iter().chain(iter);
278            let commits: Vec<_> = iter.by_ref().take(5).try_collect()?;
279            let elided = iter.next().is_some();
280            Err(format_multiple_revisions_error(
281                revision_str,
282                &commits,
283                elided,
284                &commit_summary_template(),
285            ))
286        }
287    }
288}
289
290fn format_multiple_revisions_error(
291    revision_str: &str,
292    commits: &[Commit],
293    elided: bool,
294    template: &TemplateRenderer<'_, Commit>,
295) -> CommandError {
296    assert!(commits.len() >= 2);
297    let mut cmd_err = user_error(format!(
298        "Revset `{revision_str}` resolved to more than one revision"
299    ));
300    let write_commits_summary = |formatter: &mut dyn Formatter| {
301        for commit in commits {
302            write!(formatter, "  ")?;
303            template.format(commit, formatter)?;
304            writeln!(formatter)?;
305        }
306        if elided {
307            writeln!(formatter, "  ...")?;
308        }
309        Ok(())
310    };
311    cmd_err.add_formatted_hint_with(|formatter| {
312        writeln!(
313            formatter,
314            "The revset `{revision_str}` resolved to these revisions:"
315        )?;
316        write_commits_summary(formatter)
317    });
318    cmd_err
319}
320
321#[derive(Debug, Error)]
322#[error("Failed to parse bookmark name: {}", source.kind())]
323pub struct BookmarkNameParseError {
324    pub input: String,
325    pub source: RevsetParseError,
326}
327
328/// Parses bookmark name specified in revset syntax.
329pub fn parse_bookmark_name(text: &str) -> Result<RefNameBuf, BookmarkNameParseError> {
330    revset::parse_symbol(text)
331        .map(Into::into)
332        .map_err(|source| BookmarkNameParseError {
333            input: text.to_owned(),
334            source,
335        })
336}
337
338#[derive(Debug, Error)]
339#[error("Failed to parse tag name: {}", source.kind())]
340pub struct TagNameParseError {
341    pub source: RevsetParseError,
342}
343
344/// Parses tag name specified in revset syntax.
345pub fn parse_tag_name(text: &str) -> Result<RefNameBuf, TagNameParseError> {
346    revset::parse_symbol(text)
347        .map(Into::into)
348        .map_err(|source| TagNameParseError { source })
349}
350
351/// Parses bookmark/tag/remote name patterns and unions them all.
352pub fn parse_union_name_patterns<I>(ui: &Ui, texts: I) -> Result<StringExpression, CommandError>
353where
354    I: IntoIterator,
355    I::Item: AsRef<str>,
356{
357    let mut diagnostics = RevsetDiagnostics::new();
358    let expressions = texts
359        .into_iter()
360        .map(|text| revset::parse_string_expression(&mut diagnostics, text.as_ref()))
361        .try_collect()
362        .map_err(|err| {
363            // From<RevsetParseError>, but with different message
364            let hint = revset_parse_error_hint(&err);
365            let message = format!("Failed to parse name pattern: {}", err.kind());
366            let mut cmd_err = user_error_with_message(message, err);
367            cmd_err.extend_hints(hint);
368            cmd_err
369        })?;
370    print_parse_diagnostics(ui, "In name pattern", &diagnostics)?;
371    Ok(StringExpression::union_all(expressions))
372}