Skip to main content

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::collections::HashMap;
18use std::io;
19use std::sync::Arc;
20
21use futures::StreamExt as _;
22use futures::TryStreamExt as _;
23use futures::stream::LocalBoxStream;
24use itertools::Itertools as _;
25use jj_lib::backend::CommitId;
26use jj_lib::commit::Commit;
27use jj_lib::config::ConfigNamePathBuf;
28use jj_lib::config::ConfigSource;
29use jj_lib::config::StackedConfig;
30use jj_lib::id_prefix::IdPrefixContext;
31use jj_lib::ref_name::RefNameBuf;
32use jj_lib::ref_name::RemoteName;
33use jj_lib::ref_name::RemoteNameBuf;
34use jj_lib::ref_name::RemoteRefSymbolBuf;
35use jj_lib::repo::Repo;
36use jj_lib::revset;
37use jj_lib::revset::ResolvedRevsetExpression;
38use jj_lib::revset::Revset;
39use jj_lib::revset::RevsetDiagnostics;
40use jj_lib::revset::RevsetEvaluationError;
41use jj_lib::revset::RevsetExpression;
42use jj_lib::revset::RevsetExtensions;
43use jj_lib::revset::RevsetParseContext;
44use jj_lib::revset::RevsetParseError;
45use jj_lib::revset::RevsetResolutionError;
46use jj_lib::revset::RevsetStreamExt as _;
47use jj_lib::revset::SymbolResolver;
48use jj_lib::revset::SymbolResolverExtension;
49use jj_lib::revset::UserRevsetExpression;
50use jj_lib::settings::RemoteSettingsMap;
51use jj_lib::str_util::StringExpression;
52use jj_lib::str_util::StringMatcher;
53use thiserror::Error;
54
55use crate::command_error::CommandError;
56use crate::command_error::config_error_with_message;
57use crate::command_error::print_parse_diagnostics;
58use crate::command_error::revset_parse_error_hint;
59use crate::command_error::user_error;
60use crate::command_error::user_error_with_message;
61use crate::formatter::Formatter;
62use crate::templater::TemplateRenderer;
63use crate::ui::Ui;
64
65const USER_IMMUTABLE_HEADS: &str = "immutable_heads";
66
67#[derive(Debug, Error)]
68pub enum UserRevsetEvaluationError {
69    #[error(transparent)]
70    Resolution(RevsetResolutionError),
71    #[error(transparent)]
72    Evaluation(RevsetEvaluationError),
73}
74
75/// Wrapper around `UserRevsetExpression` to provide convenient methods.
76pub struct RevsetExpressionEvaluator<'repo> {
77    repo: &'repo dyn Repo,
78    extensions: Arc<RevsetExtensions>,
79    id_prefix_context: &'repo IdPrefixContext,
80    expression: Arc<UserRevsetExpression>,
81}
82
83impl<'repo> RevsetExpressionEvaluator<'repo> {
84    pub fn new(
85        repo: &'repo dyn Repo,
86        extensions: Arc<RevsetExtensions>,
87        id_prefix_context: &'repo IdPrefixContext,
88        expression: Arc<UserRevsetExpression>,
89    ) -> Self {
90        Self {
91            repo,
92            extensions,
93            id_prefix_context,
94            expression,
95        }
96    }
97
98    /// Returns the underlying expression.
99    pub fn expression(&self) -> &Arc<UserRevsetExpression> {
100        &self.expression
101    }
102
103    /// Intersects the underlying expression with the `other` expression.
104    pub fn intersect_with(&mut self, other: &Arc<UserRevsetExpression>) {
105        self.expression = self.expression.intersection(other);
106    }
107
108    /// Resolves user symbols in the expression, returns new expression.
109    pub fn resolve(&self) -> Result<Arc<ResolvedRevsetExpression>, RevsetResolutionError> {
110        let symbol_resolver = default_symbol_resolver(
111            self.repo,
112            self.extensions.symbol_resolvers(),
113            self.id_prefix_context,
114        );
115        self.expression
116            .resolve_user_expression(self.repo, &symbol_resolver)
117    }
118
119    /// Evaluates the expression.
120    pub fn evaluate(&self) -> Result<Box<dyn Revset + 'repo>, UserRevsetEvaluationError> {
121        self.resolve()
122            .map_err(UserRevsetEvaluationError::Resolution)?
123            .evaluate(self.repo)
124            .map_err(UserRevsetEvaluationError::Evaluation)
125    }
126
127    /// Evaluates the expression to an iterator over commit ids. Entries are
128    /// sorted in reverse topological order.
129    pub fn evaluate_to_commit_ids(
130        &self,
131    ) -> Result<
132        LocalBoxStream<'repo, Result<CommitId, RevsetEvaluationError>>,
133        UserRevsetEvaluationError,
134    > {
135        Ok(self.evaluate()?.stream())
136    }
137
138    /// Evaluates the expression to an iterator over commit objects. Entries are
139    /// sorted in reverse topological order.
140    pub fn evaluate_to_commits(
141        &self,
142    ) -> Result<
143        LocalBoxStream<'repo, Result<Commit, RevsetEvaluationError>>,
144        UserRevsetEvaluationError,
145    > {
146        Ok(self
147            .evaluate()?
148            .stream()
149            .commits(self.repo.store())
150            .boxed_local())
151    }
152}
153
154pub(super) fn warn_user_redefined_builtin(
155    ui: &Ui,
156    config: &StackedConfig,
157    table_name: &ConfigNamePathBuf,
158) -> io::Result<()> {
159    let checked_mutability_builtins = ["mutable()", "immutable()", "builtin_immutable_heads()"];
160    for layer in config
161        .layers()
162        .iter()
163        .skip_while(|layer| layer.source == ConfigSource::Default)
164    {
165        let Ok(Some(table)) = layer.look_up_table(table_name) else {
166            continue;
167        };
168        for decl in checked_mutability_builtins
169            .iter()
170            .filter(|decl| table.contains_key(decl))
171        {
172            writeln!(
173                ui.warning_default(),
174                "Redefining `{table_name}.{decl}` is not recommended; redefine \
175                 `immutable_heads()` instead.",
176            )?;
177        }
178    }
179    Ok(())
180}
181
182/// Wraps the given `IdPrefixContext` in `SymbolResolver` to be passed in to
183/// `evaluate()`.
184pub fn default_symbol_resolver<'a>(
185    repo: &'a dyn Repo,
186    extensions: &[impl AsRef<dyn SymbolResolverExtension>],
187    id_prefix_context: &'a IdPrefixContext,
188) -> SymbolResolver<'a> {
189    SymbolResolver::new(repo, extensions).with_id_prefix_context(id_prefix_context)
190}
191
192/// Parses user-configured expression defining the heads of the immutable set.
193/// Includes the root commit.
194pub fn parse_immutable_heads_expression(
195    diagnostics: &mut RevsetDiagnostics,
196    context: &RevsetParseContext,
197) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
198    let (_, _, immutable_heads_str, _) = context
199        .aliases_map
200        .get_function(USER_IMMUTABLE_HEADS, 0)
201        .unwrap();
202    let heads = revset::parse(diagnostics, immutable_heads_str, context)?;
203    Ok(heads.union(&RevsetExpression::root()))
204}
205
206/// Parses and resolves `trunk()` alias to detect name resolution error in it.
207///
208/// Returns `None` if the alias couldn't be parsed. Returns `Err` if the parsed
209/// expression had name resolution error.
210pub(super) fn try_resolve_trunk_alias(
211    repo: &dyn Repo,
212    context: &RevsetParseContext,
213) -> Result<Option<Arc<ResolvedRevsetExpression>>, RevsetResolutionError> {
214    let (_, _, revset_str, _) = context
215        .aliases_map
216        .get_function("trunk", 0)
217        .expect("trunk() should be defined by default");
218    let Ok(expression) = revset::parse(&mut RevsetDiagnostics::new(), revset_str, context) else {
219        return Ok(None);
220    };
221    // Not using IdPrefixContext since trunk() revset shouldn't contain short
222    // prefixes.
223    let symbol_resolver = SymbolResolver::new(repo, context.extensions.symbol_resolvers());
224    let resolved = expression.resolve_user_expression(repo, &symbol_resolver)?;
225    Ok(Some(resolved))
226}
227
228/// Error when evaluating a revset into a single commit.
229#[derive(Debug)]
230pub enum RevsetEvaluationSizeError {
231    /// The revset evaluated to no commits.
232    Empty,
233    /// The revset evaluated to multiple commits. The vector only contains a few
234    /// commits (enough for an error message), and the bool indicates whether
235    /// there were more; do not expect this to be the entire evaluated revset.
236    Multiple(Vec<Commit>, bool),
237    /// An error occurred during revset evaluation; size unknown.
238    Other(UserRevsetEvaluationError),
239}
240
241impl RevsetEvaluationSizeError {
242    pub fn to_command_error(
243        self,
244        revision_str: &str,
245        commit_summary_template: &TemplateRenderer<'_, Commit>,
246    ) -> CommandError {
247        match self {
248            Self::Empty => user_error(format!(
249                "Revset `{revision_str}` didn't resolve to any revisions"
250            )),
251            Self::Multiple(commits, has_more) => format_multiple_revisions_error(
252                revision_str,
253                &commits,
254                has_more,
255                commit_summary_template,
256            ),
257            Self::Other(error) => error.into(),
258        }
259    }
260}
261
262pub(super) async fn evaluate_revset_to_single_commit(
263    expression: &RevsetExpressionEvaluator<'_>,
264) -> Result<Commit, RevsetEvaluationSizeError> {
265    // The number of commits to pass to the error (will be shown in the error
266    // message).
267    let max_commits = 5;
268    let mut commits: Vec<_> = expression
269        .evaluate_to_commits()
270        .map_err(RevsetEvaluationSizeError::Other)?
271        .take(max_commits + 1)
272        .try_collect()
273        .await
274        .map_err(UserRevsetEvaluationError::Evaluation)
275        .map_err(RevsetEvaluationSizeError::Other)?;
276    match commits.as_slice() {
277        [commit] => Ok(commit.clone()),
278        [] => Err(RevsetEvaluationSizeError::Empty),
279        _ => {
280            let has_more = commits.len() > max_commits;
281            commits.truncate(max_commits);
282            Err(RevsetEvaluationSizeError::Multiple(commits, has_more))
283        }
284    }
285}
286
287fn format_multiple_revisions_error(
288    revision_str: &str,
289    commits: &[Commit],
290    elided: bool,
291    template: &TemplateRenderer<'_, Commit>,
292) -> CommandError {
293    assert!(commits.len() >= 2);
294    let mut cmd_err = user_error(format!(
295        "Revset `{revision_str}` resolved to more than one revision"
296    ));
297    let write_commits_summary = |formatter: &mut dyn Formatter| {
298        for commit in commits {
299            write!(formatter, "  ")?;
300            template.format(commit, formatter)?;
301            writeln!(formatter)?;
302        }
303        if elided {
304            writeln!(formatter, "  ...")?;
305        }
306        Ok(())
307    };
308    cmd_err.add_formatted_hint_with(|formatter| {
309        writeln!(
310            formatter,
311            "The revset `{revision_str}` resolved to these revisions:"
312        )?;
313        write_commits_summary(formatter)
314    });
315    cmd_err
316}
317
318#[derive(Debug, Error)]
319#[error("Failed to parse bookmark name: {}", source.kind())]
320pub struct BookmarkNameParseError {
321    pub input: String,
322    pub source: RevsetParseError,
323}
324
325/// Parses bookmark name specified in revset syntax.
326pub fn parse_bookmark_name(text: &str) -> Result<RefNameBuf, BookmarkNameParseError> {
327    revset::parse_symbol(text)
328        .map(Into::into)
329        .map_err(|source| BookmarkNameParseError {
330            input: text.to_owned(),
331            source,
332        })
333}
334
335#[derive(Debug, Error)]
336#[error("Failed to parse tag name: {}", source.kind())]
337pub struct TagNameParseError {
338    pub source: RevsetParseError,
339}
340
341/// Parses tag name specified in revset syntax.
342pub fn parse_tag_name(text: &str) -> Result<RefNameBuf, TagNameParseError> {
343    revset::parse_symbol(text)
344        .map(Into::into)
345        .map_err(|source| TagNameParseError { source })
346}
347
348/// Parses bookmark/tag/remote name patterns and unions them all.
349pub fn parse_union_name_patterns<I>(ui: &Ui, texts: I) -> Result<StringExpression, CommandError>
350where
351    I: IntoIterator,
352    I::Item: AsRef<str>,
353{
354    let mut diagnostics = RevsetDiagnostics::new();
355    let expressions = texts
356        .into_iter()
357        .map(|text| revset::parse_string_expression(&mut diagnostics, text.as_ref()))
358        .try_collect()
359        .map_err(|err| {
360            // From<RevsetParseError>, but with different message
361            let hint = revset_parse_error_hint(&err);
362            let message = format!("Failed to parse name pattern: {}", err.kind());
363            let mut cmd_err = user_error_with_message(message, err);
364            cmd_err.extend_hints(hint);
365            cmd_err
366        })?;
367    print_parse_diagnostics(ui, "In name pattern", &diagnostics)?;
368    Ok(StringExpression::union_all(expressions))
369}
370
371/// Parses bookmark/tag name patterns or remote symbols.
372pub fn parse_name_patterns_or_remote_symbols<I>(
373    ui: &Ui,
374    texts: I,
375) -> Result<(Vec<StringExpression>, Vec<RemoteRefSymbolBuf>), CommandError>
376where
377    I: IntoIterator,
378    I::Item: AsRef<str>,
379{
380    let wrap_err = |err| {
381        // From<RevsetParseError>, but with different message
382        let hint = revset_parse_error_hint(&err);
383        let message = format!(
384            "Failed to parse name pattern or remote symbol: {}",
385            err.kind()
386        );
387        let mut cmd_err = user_error_with_message(message, err);
388        cmd_err.extend_hints(hint);
389        cmd_err
390    };
391    let mut diagnostics = RevsetDiagnostics::new();
392    let mut name_expressions = Vec::new();
393    let mut remote_symbols = Vec::new();
394    for text in texts {
395        let node = revset::parse_program(text.as_ref()).map_err(wrap_err)?;
396        if let revset::ExpressionKind::RemoteSymbol(symbol) = node.kind {
397            remote_symbols.push(symbol);
398        } else {
399            let expr =
400                revset::expect_string_expression(&mut diagnostics, &node).map_err(wrap_err)?;
401            name_expressions.push(expr);
402        }
403    }
404    print_parse_diagnostics(ui, "In name pattern", &diagnostics)?;
405    Ok((name_expressions, remote_symbols))
406}
407
408/// Parses the given `remotes.<name>.auto-track-bookmarks` settings into a map
409/// of string matchers.
410pub fn parse_remote_auto_track_bookmarks_map(
411    ui: &Ui,
412    remote_settings: &RemoteSettingsMap,
413) -> Result<HashMap<RemoteNameBuf, StringMatcher>, CommandError> {
414    let mut matchers = HashMap::new();
415    for (name, settings) in remote_settings {
416        let Some(text) = &settings.auto_track_bookmarks else {
417            continue;
418        };
419        let expr = parse_remote_string_expression(ui, name, text, "auto-track-bookmarks")?;
420        matchers.insert(name.clone(), expr.to_matcher());
421    }
422    Ok(matchers)
423}
424
425/// Parses the given `remotes.<name>.auto-track-bookmarks` and
426/// `remotes.<name>.auto-track-created-bookmarks` settings into a map of string
427/// matchers. If both settings exist for the same remote, the union of the
428/// settings will be matched.
429pub fn parse_remote_auto_track_bookmarks_map_for_new_bookmarks(
430    ui: &Ui,
431    remote_settings: &RemoteSettingsMap,
432) -> Result<HashMap<RemoteNameBuf, StringMatcher>, CommandError> {
433    let mut matchers = HashMap::new();
434    for (name, settings) in remote_settings {
435        let mut exprs = Vec::new();
436        if let Some(text) = &settings.auto_track_bookmarks {
437            exprs.push(parse_remote_string_expression(
438                ui,
439                name,
440                text,
441                "auto-track-bookmarks",
442            )?);
443        }
444        if let Some(text) = &settings.auto_track_created_bookmarks {
445            exprs.push(parse_remote_string_expression(
446                ui,
447                name,
448                text,
449                "auto-track-created-bookmarks",
450            )?);
451        }
452        if exprs.is_empty() {
453            continue;
454        }
455        matchers.insert(
456            name.clone(),
457            StringExpression::union_all(exprs).to_matcher(),
458        );
459    }
460    Ok(matchers)
461}
462
463/// Parses the given `remotes.<name>.fetch-bookmarks` setting.
464pub fn parse_remote_fetch_bookmarks(
465    ui: &Ui,
466    remote_settings: &RemoteSettingsMap,
467    name: &RemoteName,
468) -> Result<Option<StringExpression>, CommandError> {
469    remote_settings
470        .get(name)
471        .and_then(|settings| settings.fetch_bookmarks.as_ref())
472        .map(|text| parse_remote_string_expression(ui, name, text, "fetch-bookmarks"))
473        .transpose()
474}
475
476/// Parses the given `remotes.<name>.fetch-tags` setting.
477pub fn parse_remote_fetch_tags(
478    ui: &Ui,
479    remote_settings: &RemoteSettingsMap,
480    name: &RemoteName,
481) -> Result<Option<StringExpression>, CommandError> {
482    remote_settings
483        .get(name)
484        .and_then(|settings| settings.fetch_tags.as_ref())
485        .map(|text| parse_remote_string_expression(ui, name, text, "fetch-tags"))
486        .transpose()
487}
488
489fn parse_remote_string_expression(
490    ui: &Ui,
491    name: &RemoteName,
492    text: &str,
493    field_name: &str,
494) -> Result<StringExpression, CommandError> {
495    let mut diagnostics = RevsetDiagnostics::new();
496    let expr = revset::parse_string_expression(&mut diagnostics, text).map_err(|err| {
497        // From<RevsetParseError>, but with different message and error kind
498        let hint = revset_parse_error_hint(&err);
499        let message = format!(
500            "Invalid `remotes.{}.{field_name}`: {}",
501            name.as_symbol(),
502            err.kind()
503        );
504        let mut cmd_err = config_error_with_message(message, err);
505        cmd_err.extend_hints(hint);
506        cmd_err
507    })?;
508    print_parse_diagnostics(
509        ui,
510        &format!("In `remotes.{}.{field_name}`", name.as_symbol()),
511        &diagnostics,
512    )?;
513    Ok(expr)
514}