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