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