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