just playing with tangled
at diffedit3 286 lines 10 kB view raw
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::rc::Rc; 18use std::sync::Arc; 19 20use itertools::Itertools as _; 21use jj_lib::backend::{BackendResult, CommitId}; 22use jj_lib::commit::Commit; 23use jj_lib::id_prefix::IdPrefixContext; 24use jj_lib::repo::Repo; 25use jj_lib::revset::{ 26 self, DefaultSymbolResolver, Revset, RevsetAliasesMap, RevsetCommitRef, RevsetEvaluationError, 27 RevsetExpression, RevsetExtensions, RevsetIteratorExt as _, RevsetParseContext, 28 RevsetParseError, RevsetResolutionError, SymbolResolverExtension, 29}; 30use jj_lib::settings::ConfigResultExt as _; 31use thiserror::Error; 32 33use crate::command_error::{user_error, CommandError}; 34use crate::config::LayeredConfigs; 35use crate::formatter::Formatter; 36use crate::templater::TemplateRenderer; 37use crate::ui::Ui; 38 39const BUILTIN_IMMUTABLE_HEADS: &str = "immutable_heads"; 40 41#[derive(Debug, Error)] 42pub enum UserRevsetEvaluationError { 43 #[error(transparent)] 44 Resolution(RevsetResolutionError), 45 #[error(transparent)] 46 Evaluation(RevsetEvaluationError), 47} 48 49/// Wrapper around `RevsetExpression` to provide convenient methods. 50pub struct RevsetExpressionEvaluator<'repo> { 51 repo: &'repo dyn Repo, 52 extensions: Arc<RevsetExtensions>, 53 id_prefix_context: &'repo IdPrefixContext, 54 expression: Rc<RevsetExpression>, 55} 56 57impl<'repo> RevsetExpressionEvaluator<'repo> { 58 pub fn new( 59 repo: &'repo dyn Repo, 60 extensions: Arc<RevsetExtensions>, 61 id_prefix_context: &'repo IdPrefixContext, 62 expression: Rc<RevsetExpression>, 63 ) -> Self { 64 RevsetExpressionEvaluator { 65 repo, 66 extensions, 67 id_prefix_context, 68 expression, 69 } 70 } 71 72 /// Returns the underlying expression. 73 pub fn expression(&self) -> &Rc<RevsetExpression> { 74 &self.expression 75 } 76 77 /// Intersects the underlying expression with the `other` expression. 78 pub fn intersect_with(&mut self, other: &Rc<RevsetExpression>) { 79 self.expression = self.expression.intersection(other); 80 } 81 82 /// Evaluates the expression. 83 pub fn evaluate(&self) -> Result<Box<dyn Revset + 'repo>, UserRevsetEvaluationError> { 84 let symbol_resolver = default_symbol_resolver( 85 self.repo, 86 self.extensions.symbol_resolvers(), 87 self.id_prefix_context, 88 ); 89 evaluate(self.repo, &symbol_resolver, self.expression.clone()) 90 } 91 92 /// Evaluates the expression to an iterator over commit ids. Entries are 93 /// sorted in reverse topological order. 94 pub fn evaluate_to_commit_ids( 95 &self, 96 ) -> Result<Box<dyn Iterator<Item = CommitId> + 'repo>, UserRevsetEvaluationError> { 97 Ok(self.evaluate()?.iter()) 98 } 99 100 /// Evaluates the expression to an iterator over commit objects. Entries are 101 /// sorted in reverse topological order. 102 pub fn evaluate_to_commits( 103 &self, 104 ) -> Result<impl Iterator<Item = BackendResult<Commit>> + 'repo, UserRevsetEvaluationError> 105 { 106 Ok(self.evaluate()?.iter().commits(self.repo.store())) 107 } 108} 109 110pub fn load_revset_aliases( 111 ui: &Ui, 112 layered_configs: &LayeredConfigs, 113) -> Result<RevsetAliasesMap, CommandError> { 114 const TABLE_KEY: &str = "revset-aliases"; 115 let mut aliases_map = RevsetAliasesMap::new(); 116 // Load from all config layers in order. 'f(x)' in default layer should be 117 // overridden by 'f(a)' in user. 118 for (_, config) in layered_configs.sources() { 119 let table = if let Some(table) = config.get_table(TABLE_KEY).optional()? { 120 table 121 } else { 122 continue; 123 }; 124 for (decl, value) in table.into_iter().sorted_by(|a, b| a.0.cmp(&b.0)) { 125 let r = value 126 .into_string() 127 .map_err(|e| e.to_string()) 128 .and_then(|v| aliases_map.insert(&decl, v).map_err(|e| e.to_string())); 129 if let Err(s) = r { 130 writeln!( 131 ui.warning_default(), 132 r#"Failed to load "{TABLE_KEY}.{decl}": {s}"# 133 )?; 134 } 135 } 136 } 137 138 // TODO: If we add support for function overloading (#2966), this check can 139 // be removed. 140 let (params, _) = aliases_map.get_function(BUILTIN_IMMUTABLE_HEADS).unwrap(); 141 if !params.is_empty() { 142 return Err(user_error(format!( 143 "The `revset-aliases.{name}()` function must be declared without arguments", 144 name = BUILTIN_IMMUTABLE_HEADS 145 ))); 146 } 147 148 Ok(aliases_map) 149} 150 151pub fn evaluate<'a>( 152 repo: &'a dyn Repo, 153 symbol_resolver: &DefaultSymbolResolver, 154 expression: Rc<RevsetExpression>, 155) -> Result<Box<dyn Revset + 'a>, UserRevsetEvaluationError> { 156 let resolved = revset::optimize(expression) 157 .resolve_user_expression(repo, symbol_resolver) 158 .map_err(UserRevsetEvaluationError::Resolution)?; 159 resolved 160 .evaluate(repo) 161 .map_err(UserRevsetEvaluationError::Evaluation) 162} 163 164/// Wraps the given `IdPrefixContext` in `SymbolResolver` to be passed in to 165/// `evaluate()`. 166pub fn default_symbol_resolver<'a>( 167 repo: &'a dyn Repo, 168 extensions: &[impl AsRef<dyn SymbolResolverExtension>], 169 id_prefix_context: &'a IdPrefixContext, 170) -> DefaultSymbolResolver<'a> { 171 DefaultSymbolResolver::new(repo, extensions).with_id_prefix_context(id_prefix_context) 172} 173 174/// Parses user-configured expression defining the immutable set. 175pub fn parse_immutable_expression( 176 context: &RevsetParseContext, 177) -> Result<Rc<RevsetExpression>, RevsetParseError> { 178 let (params, immutable_heads_str) = context 179 .aliases_map 180 .get_function(BUILTIN_IMMUTABLE_HEADS) 181 .unwrap(); 182 assert!( 183 params.is_empty(), 184 "invalid declaration should have been rejected by load_revset_aliases()" 185 ); 186 // Negated ancestors expression `~::(<heads> | root())` is slightly easier 187 // to optimize than negated union `~(::<heads> | root())`. 188 let heads = revset::parse(immutable_heads_str, context)?; 189 Ok(heads.union(&RevsetExpression::root()).ancestors()) 190} 191 192pub(super) fn evaluate_revset_to_single_commit<'a>( 193 revision_str: &str, 194 expression: &RevsetExpressionEvaluator<'_>, 195 commit_summary_template: impl FnOnce() -> TemplateRenderer<'a, Commit>, 196 should_hint_about_all_prefix: bool, 197) -> Result<Commit, CommandError> { 198 let mut iter = expression.evaluate_to_commits()?.fuse(); 199 match (iter.next(), iter.next()) { 200 (Some(commit), None) => Ok(commit?), 201 (None, _) => Err(user_error(format!( 202 r#"Revset "{revision_str}" didn't resolve to any revisions"# 203 ))), 204 (Some(commit0), Some(commit1)) => { 205 let mut iter = [commit0, commit1].into_iter().chain(iter); 206 let commits: Vec<_> = iter.by_ref().take(5).try_collect()?; 207 let elided = iter.next().is_some(); 208 Err(format_multiple_revisions_error( 209 revision_str, 210 expression.expression(), 211 &commits, 212 elided, 213 &commit_summary_template(), 214 should_hint_about_all_prefix, 215 )) 216 } 217 } 218} 219 220fn format_multiple_revisions_error( 221 revision_str: &str, 222 expression: &RevsetExpression, 223 commits: &[Commit], 224 elided: bool, 225 template: &TemplateRenderer<'_, Commit>, 226 should_hint_about_all_prefix: bool, 227) -> CommandError { 228 assert!(commits.len() >= 2); 229 let mut cmd_err = user_error(format!( 230 r#"Revset "{revision_str}" resolved to more than one revision"# 231 )); 232 let write_commits_summary = |formatter: &mut dyn Formatter| { 233 for commit in commits { 234 write!(formatter, " ")?; 235 template.format(commit, formatter)?; 236 writeln!(formatter)?; 237 } 238 if elided { 239 writeln!(formatter, " ...")?; 240 } 241 Ok(()) 242 }; 243 if commits[0].change_id() == commits[1].change_id() { 244 // Separate hint if there's commits with same change id 245 cmd_err.add_formatted_hint_with(|formatter| { 246 writeln!( 247 formatter, 248 r#"The revset "{revision_str}" resolved to these revisions:"# 249 )?; 250 write_commits_summary(formatter) 251 }); 252 cmd_err.add_hint( 253 "Some of these commits have the same change id. Abandon one of them with `jj abandon \ 254 -r <REVISION>`.", 255 ); 256 } else if let RevsetExpression::CommitRef(RevsetCommitRef::Symbol(branch_name)) = expression { 257 // Separate hint if there's a conflicted branch 258 cmd_err.add_formatted_hint_with(|formatter| { 259 writeln!( 260 formatter, 261 "Branch {branch_name} resolved to multiple revisions because it's conflicted." 262 )?; 263 writeln!(formatter, "It resolved to these revisions:")?; 264 write_commits_summary(formatter) 265 }); 266 cmd_err.add_hint(format!( 267 "Set which revision the branch points to with `jj branch set {branch_name} -r \ 268 <REVISION>`.", 269 )); 270 } else { 271 cmd_err.add_formatted_hint_with(|formatter| { 272 writeln!( 273 formatter, 274 r#"The revset "{revision_str}" resolved to these revisions:"# 275 )?; 276 write_commits_summary(formatter) 277 }); 278 if should_hint_about_all_prefix { 279 cmd_err.add_hint(format!( 280 "Prefix the expression with 'all:' to allow any number of revisions (i.e. \ 281 'all:{revision_str}')." 282 )); 283 } 284 }; 285 cmd_err 286}