482 lines · 15.9 KB · Rust
RawDownload
// Copyright 2022-2024 The Jujutsu Authors//// Licensed under the Apache License, Version 2.0 (the "License");// you may not use this file except in compliance with the License.// You may obtain a copy of the License at//// https://www.apache.org/licenses/LICENSE-2.0//// Unless required by applicable law or agreed to in writing, software// distributed under the License is distributed on an "AS IS" BASIS,// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.// See the License for the specific language governing permissions and// limitations under the License. //! Utility for parsing and evaluating user-provided revset expressions. use std::collections::HashMap;use std::io;use std::sync::Arc; use futures::StreamExt as _;use futures::TryStreamExt as _;use futures::stream::LocalBoxStream;use itertools::Itertools as _;use jj_lib::backend::CommitId;use jj_lib::commit::Commit;use jj_lib::config::ConfigNamePathBuf;use jj_lib::config::ConfigSource;use jj_lib::config::StackedConfig;use jj_lib::id_prefix::IdPrefixContext;use jj_lib::ref_name::RefNameBuf;use jj_lib::ref_name::RemoteName;use jj_lib::ref_name::RemoteNameBuf;use jj_lib::ref_name::RemoteRefSymbolBuf;use jj_lib::repo::Repo;use jj_lib::revset;use jj_lib::revset::ResolvedRevsetExpression;use jj_lib::revset::Revset;use jj_lib::revset::RevsetDiagnostics;use jj_lib::revset::RevsetEvaluationError;use jj_lib::revset::RevsetExpression;use jj_lib::revset::RevsetExtensions;use jj_lib::revset::RevsetParseContext;use jj_lib::revset::RevsetParseError;use jj_lib::revset::RevsetResolutionError;use jj_lib::revset::RevsetStreamExt as _;use jj_lib::revset::SymbolResolver;use jj_lib::revset::SymbolResolverExtension;use jj_lib::revset::UserRevsetExpression;use jj_lib::settings::RemoteSettingsMap;use jj_lib::str_util::StringExpression;use jj_lib::str_util::StringMatcher;use thiserror::Error; use crate::command_error::CommandError;use crate::command_error::config_error_with_message;use crate::command_error::print_parse_diagnostics;use crate::command_error::revset_parse_error_hint;use crate::command_error::user_error;use crate::command_error::user_error_with_message;use crate::formatter::Formatter;use crate::templater::TemplateRenderer;use crate::ui::Ui; const USER_IMMUTABLE_HEADS: &str = "immutable_heads"; #[derive(Debug, Error)]pub enum UserRevsetEvaluationError {    #[error(transparent)]    Resolution(RevsetResolutionError),    #[error(transparent)]    Evaluation(RevsetEvaluationError),} /// Wrapper around `UserRevsetExpression` to provide convenient methods.pub struct RevsetExpressionEvaluator<'repo> {    repo: &'repo dyn Repo,    extensions: Arc<RevsetExtensions>,    id_prefix_context: &'repo IdPrefixContext,    expression: Arc<UserRevsetExpression>,} impl<'repo> RevsetExpressionEvaluator<'repo> {    pub fn new(        repo: &'repo dyn Repo,        extensions: Arc<RevsetExtensions>,        id_prefix_context: &'repo IdPrefixContext,        expression: Arc<UserRevsetExpression>,    ) -> Self {        Self {            repo,            extensions,            id_prefix_context,            expression,        }    }     /// Returns the underlying expression.    pub fn expression(&self) -> &Arc<UserRevsetExpression> {        &self.expression    }     /// Intersects the underlying expression with the `other` expression.    pub fn intersect_with(&mut self, other: &Arc<UserRevsetExpression>) {        self.expression = self.expression.intersection(other);    }     /// Resolves user symbols in the expression, returns new expression.    pub fn resolve(&self) -> Result<Arc<ResolvedRevsetExpression>, RevsetResolutionError> {        let symbol_resolver = default_symbol_resolver(            self.repo,            self.extensions.symbol_resolvers(),            self.id_prefix_context,        );        self.expression            .resolve_user_expression(self.repo, &symbol_resolver)    }     /// Evaluates the expression.    pub fn evaluate(&self) -> Result<Box<dyn Revset + 'repo>, UserRevsetEvaluationError> {        self.resolve()            .map_err(UserRevsetEvaluationError::Resolution)?            .evaluate(self.repo)            .map_err(UserRevsetEvaluationError::Evaluation)    }     /// Evaluates the expression to an iterator over commit ids. Entries are    /// sorted in reverse topological order.    pub fn evaluate_to_commit_ids(        &self,    ) -> Result<        LocalBoxStream<'repo, Result<CommitId, RevsetEvaluationError>>,        UserRevsetEvaluationError,    > {        Ok(self.evaluate()?.stream())    }     /// Evaluates the expression to an iterator over commit objects. Entries are    /// sorted in reverse topological order.    pub fn evaluate_to_commits(        &self,    ) -> Result<        LocalBoxStream<'repo, Result<Commit, RevsetEvaluationError>>,        UserRevsetEvaluationError,    > {        Ok(self            .evaluate()?            .stream()            .commits(self.repo.store())            .boxed_local())    }} pub(super) fn warn_user_redefined_builtin(    ui: &Ui,    config: &StackedConfig,    table_name: &ConfigNamePathBuf,) -> io::Result<()> {    let checked_mutability_builtins = ["mutable()", "immutable()", "builtin_immutable_heads()"];    for layer in config        .layers()        .iter()        .skip_while(|layer| layer.source == ConfigSource::Default)    {        let Ok(Some(table)) = layer.look_up_table(table_name) else {            continue;        };        for decl in checked_mutability_builtins            .iter()            .filter(|decl| table.contains_key(decl))        {            writeln!(                ui.warning_default(),                "Redefining `{table_name}.{decl}` is not recommended; redefine \                 `immutable_heads()` instead",            )?;        }    }    Ok(())} /// Wraps the given `IdPrefixContext` in `SymbolResolver` to be passed in to/// `evaluate()`.pub fn default_symbol_resolver<'a>(    repo: &'a dyn Repo,    extensions: &[impl AsRef<dyn SymbolResolverExtension>],    id_prefix_context: &'a IdPrefixContext,) -> SymbolResolver<'a> {    SymbolResolver::new(repo, extensions).with_id_prefix_context(id_prefix_context)} /// Parses user-configured expression defining the heads of the immutable set./// Includes the root commit.pub fn parse_immutable_heads_expression(    diagnostics: &mut RevsetDiagnostics,    context: &RevsetParseContext,) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {    let (_, _, immutable_heads_str, _) = context        .aliases_map        .get_function(USER_IMMUTABLE_HEADS, 0)        .unwrap();    let heads = revset::parse(diagnostics, immutable_heads_str, context)?;    Ok(heads.union(&RevsetExpression::root()))} /// Parses and resolves `trunk()` alias to detect name resolution error in it.////// Returns `None` if the alias couldn't be parsed. Returns `Err` if the parsed/// expression had name resolution error.pub(super) fn try_resolve_trunk_alias(    repo: &dyn Repo,    context: &RevsetParseContext,) -> Result<Option<Arc<ResolvedRevsetExpression>>, RevsetResolutionError> {    let (_, _, revset_str, _) = context        .aliases_map        .get_function("trunk", 0)        .expect("trunk() should be defined by default");    let Ok(expression) = revset::parse(&mut RevsetDiagnostics::new(), revset_str, context) else {        return Ok(None);    };    // Not using IdPrefixContext since trunk() revset shouldn't contain short    // prefixes.    let symbol_resolver = SymbolResolver::new(repo, context.extensions.symbol_resolvers());    let resolved = expression.resolve_user_expression(repo, &symbol_resolver)?;    Ok(Some(resolved))} pub(super) async fn evaluate_revset_to_single_commit<'a>(    revision_str: &str,    expression: &RevsetExpressionEvaluator<'_>,    commit_summary_template: impl FnOnce() -> TemplateRenderer<'a, Commit>,) -> Result<Commit, CommandError> {    let commits: Vec<_> = expression        .evaluate_to_commits()?        .take(6)        .try_collect()        .await?;    match commits.as_slice() {        [commit] => Ok(commit.clone()),        [] => Err(user_error(format!(            "Revset `{revision_str}` didn't resolve to any revisions"        ))),        _ => {            let elided = commits.len() > 5;            Err(format_multiple_revisions_error(                revision_str,                &commits[..std::cmp::min(5, commits.len())],                elided,                &commit_summary_template(),            ))        }    }} fn format_multiple_revisions_error(    revision_str: &str,    commits: &[Commit],    elided: bool,    template: &TemplateRenderer<'_, Commit>,) -> CommandError {    assert!(commits.len() >= 2);    let mut cmd_err = user_error(format!(        "Revset `{revision_str}` resolved to more than one revision"    ));    let write_commits_summary = |formatter: &mut dyn Formatter| {        for commit in commits {            write!(formatter, "  ")?;            template.format(commit, formatter)?;            writeln!(formatter)?;        }        if elided {            writeln!(formatter, "  ...")?;        }        Ok(())    };    cmd_err.add_formatted_hint_with(|formatter| {        writeln!(            formatter,            "The revset `{revision_str}` resolved to these revisions:"        )?;        write_commits_summary(formatter)    });    cmd_err} #[derive(Debug, Error)]#[error("Failed to parse bookmark name: {}", source.kind())]pub struct BookmarkNameParseError {    pub input: String,    pub source: RevsetParseError,} /// Parses bookmark name specified in revset syntax.pub fn parse_bookmark_name(text: &str) -> Result<RefNameBuf, BookmarkNameParseError> {    revset::parse_symbol(text)        .map(Into::into)        .map_err(|source| BookmarkNameParseError {            input: text.to_owned(),            source,        })} #[derive(Debug, Error)]#[error("Failed to parse tag name: {}", source.kind())]pub struct TagNameParseError {    pub source: RevsetParseError,} /// Parses tag name specified in revset syntax.pub fn parse_tag_name(text: &str) -> Result<RefNameBuf, TagNameParseError> {    revset::parse_symbol(text)        .map(Into::into)        .map_err(|source| TagNameParseError { source })} /// Parses bookmark/tag/remote name patterns and unions them all.pub fn parse_union_name_patterns<I>(ui: &Ui, texts: I) -> Result<StringExpression, CommandError>where    I: IntoIterator,    I::Item: AsRef<str>,{    let mut diagnostics = RevsetDiagnostics::new();    let expressions = texts        .into_iter()        .map(|text| revset::parse_string_expression(&mut diagnostics, text.as_ref()))        .try_collect()        .map_err(|err| {            // From<RevsetParseError>, but with different message            let hint = revset_parse_error_hint(&err);            let message = format!("Failed to parse name pattern: {}", err.kind());            let mut cmd_err = user_error_with_message(message, err);            cmd_err.extend_hints(hint);            cmd_err        })?;    print_parse_diagnostics(ui, "In name pattern", &diagnostics)?;    Ok(StringExpression::union_all(expressions))} /// Parses bookmark/tag name patterns or remote symbols.pub fn parse_name_patterns_or_remote_symbols<I>(    ui: &Ui,    texts: I,) -> Result<(Vec<StringExpression>, Vec<RemoteRefSymbolBuf>), CommandError>where    I: IntoIterator,    I::Item: AsRef<str>,{    let wrap_err = |err| {        // From<RevsetParseError>, but with different message        let hint = revset_parse_error_hint(&err);        let message = format!(            "Failed to parse name pattern or remote symbol: {}",            err.kind()        );        let mut cmd_err = user_error_with_message(message, err);        cmd_err.extend_hints(hint);        cmd_err    };    let mut diagnostics = RevsetDiagnostics::new();    let mut name_expressions = Vec::new();    let mut remote_symbols = Vec::new();    for text in texts {        let node = revset::parse_program(text.as_ref()).map_err(wrap_err)?;        if let revset::ExpressionKind::RemoteSymbol(symbol) = node.kind {            remote_symbols.push(symbol);        } else {            let expr =                revset::expect_string_expression(&mut diagnostics, &node).map_err(wrap_err)?;            name_expressions.push(expr);        }    }    print_parse_diagnostics(ui, "In name pattern", &diagnostics)?;    Ok((name_expressions, remote_symbols))} /// Parses the given `remotes.<name>.auto-track-bookmarks` settings into a map/// of string matchers.pub fn parse_remote_auto_track_bookmarks_map(    ui: &Ui,    remote_settings: &RemoteSettingsMap,) -> Result<HashMap<RemoteNameBuf, StringMatcher>, CommandError> {    let mut matchers = HashMap::new();    for (name, settings) in remote_settings {        let Some(text) = &settings.auto_track_bookmarks else {            continue;        };        let expr = parse_remote_string_expression(ui, name, text, "auto-track-bookmarks")?;        matchers.insert(name.clone(), expr.to_matcher());    }    Ok(matchers)} /// Parses the given `remotes.<name>.auto-track-bookmarks` and/// `remotes.<name>.auto-track-created-bookmarks` settings into a map of string/// matchers. If both settings exist for the same remote, the union of the/// settings will be matched.pub fn parse_remote_auto_track_bookmarks_map_for_new_bookmarks(    ui: &Ui,    remote_settings: &RemoteSettingsMap,) -> Result<HashMap<RemoteNameBuf, StringMatcher>, CommandError> {    let mut matchers = HashMap::new();    for (name, settings) in remote_settings {        let mut exprs = Vec::new();        if let Some(text) = &settings.auto_track_bookmarks {            exprs.push(parse_remote_string_expression(                ui,                name,                text,                "auto-track-bookmarks",            )?);        }        if let Some(text) = &settings.auto_track_created_bookmarks {            exprs.push(parse_remote_string_expression(                ui,                name,                text,                "auto-track-created-bookmarks",            )?);        }        if exprs.is_empty() {            continue;        }        matchers.insert(            name.clone(),            StringExpression::union_all(exprs).to_matcher(),        );    }    Ok(matchers)} /// Parses the given `remotes.<name>.fetch-bookmarks` setting.pub fn parse_remote_fetch_bookmarks(    ui: &Ui,    remote_settings: &RemoteSettingsMap,    name: &RemoteName,) -> Result<Option<StringExpression>, CommandError> {    remote_settings        .get(name)        .and_then(|settings| settings.fetch_bookmarks.as_ref())        .map(|text| parse_remote_string_expression(ui, name, text, "fetch-bookmarks"))        .transpose()} /// Parses the given `remotes.<name>.fetch-tags` setting.pub fn parse_remote_fetch_tags(    ui: &Ui,    remote_settings: &RemoteSettingsMap,    name: &RemoteName,) -> Result<Option<StringExpression>, CommandError> {    remote_settings        .get(name)        .and_then(|settings| settings.fetch_tags.as_ref())        .map(|text| parse_remote_string_expression(ui, name, text, "fetch-tags"))        .transpose()} fn parse_remote_string_expression(    ui: &Ui,    name: &RemoteName,    text: &str,    field_name: &str,) -> Result<StringExpression, CommandError> {    let mut diagnostics = RevsetDiagnostics::new();    let expr = revset::parse_string_expression(&mut diagnostics, text).map_err(|err| {        // From<RevsetParseError>, but with different message and error kind        let hint = revset_parse_error_hint(&err);        let message = format!(            "Invalid `remotes.{}.{field_name}`: {}",            name.as_symbol(),            err.kind()        );        let mut cmd_err = config_error_with_message(message, err);        cmd_err.extend_hints(hint);        cmd_err    })?;    print_parse_diagnostics(        ui,        &format!("In `remotes.{}.{field_name}`", name.as_symbol()),        &diagnostics,    )?;    Ok(expr)}