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
228pub(super) async fn evaluate_revset_to_single_commit<'a>(
229    revision_str: &str,
230    expression: &RevsetExpressionEvaluator<'_>,
231    commit_summary_template: impl FnOnce() -> TemplateRenderer<'a, Commit>,
232) -> Result<Commit, CommandError> {
233    let commits: Vec<_> = expression
234        .evaluate_to_commits()?
235        .take(6)
236        .try_collect()
237        .await?;
238    match commits.as_slice() {
239        [commit] => Ok(commit.clone()),
240        [] => Err(user_error(format!(
241            "Revset `{revision_str}` didn't resolve to any revisions"
242        ))),
243        _ => {
244            let elided = commits.len() > 5;
245            Err(format_multiple_revisions_error(
246                revision_str,
247                &commits[..std::cmp::min(5, commits.len())],
248                elided,
249                &commit_summary_template(),
250            ))
251        }
252    }
253}
254
255fn format_multiple_revisions_error(
256    revision_str: &str,
257    commits: &[Commit],
258    elided: bool,
259    template: &TemplateRenderer<'_, Commit>,
260) -> CommandError {
261    assert!(commits.len() >= 2);
262    let mut cmd_err = user_error(format!(
263        "Revset `{revision_str}` resolved to more than one revision"
264    ));
265    let write_commits_summary = |formatter: &mut dyn Formatter| {
266        for commit in commits {
267            write!(formatter, "  ")?;
268            template.format(commit, formatter)?;
269            writeln!(formatter)?;
270        }
271        if elided {
272            writeln!(formatter, "  ...")?;
273        }
274        Ok(())
275    };
276    cmd_err.add_formatted_hint_with(|formatter| {
277        writeln!(
278            formatter,
279            "The revset `{revision_str}` resolved to these revisions:"
280        )?;
281        write_commits_summary(formatter)
282    });
283    cmd_err
284}
285
286#[derive(Debug, Error)]
287#[error("Failed to parse bookmark name: {}", source.kind())]
288pub struct BookmarkNameParseError {
289    pub input: String,
290    pub source: RevsetParseError,
291}
292
293/// Parses bookmark name specified in revset syntax.
294pub fn parse_bookmark_name(text: &str) -> Result<RefNameBuf, BookmarkNameParseError> {
295    revset::parse_symbol(text)
296        .map(Into::into)
297        .map_err(|source| BookmarkNameParseError {
298            input: text.to_owned(),
299            source,
300        })
301}
302
303#[derive(Debug, Error)]
304#[error("Failed to parse tag name: {}", source.kind())]
305pub struct TagNameParseError {
306    pub source: RevsetParseError,
307}
308
309/// Parses tag name specified in revset syntax.
310pub fn parse_tag_name(text: &str) -> Result<RefNameBuf, TagNameParseError> {
311    revset::parse_symbol(text)
312        .map(Into::into)
313        .map_err(|source| TagNameParseError { source })
314}
315
316/// Parses bookmark/tag/remote name patterns and unions them all.
317pub fn parse_union_name_patterns<I>(ui: &Ui, texts: I) -> Result<StringExpression, CommandError>
318where
319    I: IntoIterator,
320    I::Item: AsRef<str>,
321{
322    let mut diagnostics = RevsetDiagnostics::new();
323    let expressions = texts
324        .into_iter()
325        .map(|text| revset::parse_string_expression(&mut diagnostics, text.as_ref()))
326        .try_collect()
327        .map_err(|err| {
328            // From<RevsetParseError>, but with different message
329            let hint = revset_parse_error_hint(&err);
330            let message = format!("Failed to parse name pattern: {}", err.kind());
331            let mut cmd_err = user_error_with_message(message, err);
332            cmd_err.extend_hints(hint);
333            cmd_err
334        })?;
335    print_parse_diagnostics(ui, "In name pattern", &diagnostics)?;
336    Ok(StringExpression::union_all(expressions))
337}
338
339/// Parses bookmark/tag name patterns or remote symbols.
340pub fn parse_name_patterns_or_remote_symbols<I>(
341    ui: &Ui,
342    texts: I,
343) -> Result<(Vec<StringExpression>, Vec<RemoteRefSymbolBuf>), CommandError>
344where
345    I: IntoIterator,
346    I::Item: AsRef<str>,
347{
348    let wrap_err = |err| {
349        // From<RevsetParseError>, but with different message
350        let hint = revset_parse_error_hint(&err);
351        let message = format!(
352            "Failed to parse name pattern or remote symbol: {}",
353            err.kind()
354        );
355        let mut cmd_err = user_error_with_message(message, err);
356        cmd_err.extend_hints(hint);
357        cmd_err
358    };
359    let mut diagnostics = RevsetDiagnostics::new();
360    let mut name_expressions = Vec::new();
361    let mut remote_symbols = Vec::new();
362    for text in texts {
363        let node = revset::parse_program(text.as_ref()).map_err(wrap_err)?;
364        if let revset::ExpressionKind::RemoteSymbol(symbol) = node.kind {
365            remote_symbols.push(symbol);
366        } else {
367            let expr =
368                revset::expect_string_expression(&mut diagnostics, &node).map_err(wrap_err)?;
369            name_expressions.push(expr);
370        }
371    }
372    print_parse_diagnostics(ui, "In name pattern", &diagnostics)?;
373    Ok((name_expressions, remote_symbols))
374}
375
376/// Parses the given `remotes.<name>.auto-track-bookmarks` settings into a map
377/// of string matchers.
378pub fn parse_remote_auto_track_bookmarks_map(
379    ui: &Ui,
380    remote_settings: &RemoteSettingsMap,
381) -> Result<HashMap<RemoteNameBuf, StringMatcher>, CommandError> {
382    let mut matchers = HashMap::new();
383    for (name, settings) in remote_settings {
384        let Some(text) = &settings.auto_track_bookmarks else {
385            continue;
386        };
387        let expr = parse_remote_string_expression(ui, name, text, "auto-track-bookmarks")?;
388        matchers.insert(name.clone(), expr.to_matcher());
389    }
390    Ok(matchers)
391}
392
393/// Parses the given `remotes.<name>.auto-track-bookmarks` and
394/// `remotes.<name>.auto-track-created-bookmarks` settings into a map of string
395/// matchers. If both settings exist for the same remote, the union of the
396/// settings will be matched.
397pub fn parse_remote_auto_track_bookmarks_map_for_new_bookmarks(
398    ui: &Ui,
399    remote_settings: &RemoteSettingsMap,
400) -> Result<HashMap<RemoteNameBuf, StringMatcher>, CommandError> {
401    let mut matchers = HashMap::new();
402    for (name, settings) in remote_settings {
403        let mut exprs = Vec::new();
404        if let Some(text) = &settings.auto_track_bookmarks {
405            exprs.push(parse_remote_string_expression(
406                ui,
407                name,
408                text,
409                "auto-track-bookmarks",
410            )?);
411        }
412        if let Some(text) = &settings.auto_track_created_bookmarks {
413            exprs.push(parse_remote_string_expression(
414                ui,
415                name,
416                text,
417                "auto-track-created-bookmarks",
418            )?);
419        }
420        if exprs.is_empty() {
421            continue;
422        }
423        matchers.insert(
424            name.clone(),
425            StringExpression::union_all(exprs).to_matcher(),
426        );
427    }
428    Ok(matchers)
429}
430
431/// Parses the given `remotes.<name>.fetch-bookmarks` setting.
432pub fn parse_remote_fetch_bookmarks(
433    ui: &Ui,
434    remote_settings: &RemoteSettingsMap,
435    name: &RemoteName,
436) -> Result<Option<StringExpression>, CommandError> {
437    remote_settings
438        .get(name)
439        .and_then(|settings| settings.fetch_bookmarks.as_ref())
440        .map(|text| parse_remote_string_expression(ui, name, text, "fetch-bookmarks"))
441        .transpose()
442}
443
444/// Parses the given `remotes.<name>.fetch-tags` setting.
445pub fn parse_remote_fetch_tags(
446    ui: &Ui,
447    remote_settings: &RemoteSettingsMap,
448    name: &RemoteName,
449) -> Result<Option<StringExpression>, CommandError> {
450    remote_settings
451        .get(name)
452        .and_then(|settings| settings.fetch_tags.as_ref())
453        .map(|text| parse_remote_string_expression(ui, name, text, "fetch-tags"))
454        .transpose()
455}
456
457fn parse_remote_string_expression(
458    ui: &Ui,
459    name: &RemoteName,
460    text: &str,
461    field_name: &str,
462) -> Result<StringExpression, CommandError> {
463    let mut diagnostics = RevsetDiagnostics::new();
464    let expr = revset::parse_string_expression(&mut diagnostics, text).map_err(|err| {
465        // From<RevsetParseError>, but with different message and error kind
466        let hint = revset_parse_error_hint(&err);
467        let message = format!(
468            "Invalid `remotes.{}.{field_name}`: {}",
469            name.as_symbol(),
470            err.kind()
471        );
472        let mut cmd_err = config_error_with_message(message, err);
473        cmd_err.extend_hints(hint);
474        cmd_err
475    })?;
476    print_parse_diagnostics(
477        ui,
478        &format!("In `remotes.{}.{field_name}`", name.as_symbol()),
479        &diagnostics,
480    )?;
481    Ok(expr)
482}