just playing with tangled
at ig/vimdiffwarn 372 lines 13 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::io; 18use std::rc::Rc; 19use std::sync::Arc; 20 21use itertools::Itertools as _; 22use jj_lib::backend::CommitId; 23use jj_lib::commit::Commit; 24use jj_lib::config::ConfigGetError; 25use jj_lib::config::ConfigNamePathBuf; 26use jj_lib::config::ConfigSource; 27use jj_lib::config::StackedConfig; 28use jj_lib::id_prefix::IdPrefixContext; 29use jj_lib::ref_name::RefNameBuf; 30use jj_lib::repo::Repo; 31use jj_lib::revset; 32use jj_lib::revset::DefaultSymbolResolver; 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::SymbolResolverExtension; 45use jj_lib::revset::UserRevsetExpression; 46use thiserror::Error; 47 48use crate::command_error::user_error; 49use crate::command_error::CommandError; 50use crate::formatter::Formatter; 51use crate::templater::TemplateRenderer; 52use crate::ui::Ui; 53 54const USER_IMMUTABLE_HEADS: &str = "immutable_heads"; 55 56#[derive(Debug, Error)] 57pub enum UserRevsetEvaluationError { 58 #[error(transparent)] 59 Resolution(RevsetResolutionError), 60 #[error(transparent)] 61 Evaluation(RevsetEvaluationError), 62} 63 64/// Wrapper around `UserRevsetExpression` to provide convenient methods. 65pub struct RevsetExpressionEvaluator<'repo> { 66 repo: &'repo dyn Repo, 67 extensions: Arc<RevsetExtensions>, 68 id_prefix_context: &'repo IdPrefixContext, 69 expression: Rc<UserRevsetExpression>, 70} 71 72impl<'repo> RevsetExpressionEvaluator<'repo> { 73 pub fn new( 74 repo: &'repo dyn Repo, 75 extensions: Arc<RevsetExtensions>, 76 id_prefix_context: &'repo IdPrefixContext, 77 expression: Rc<UserRevsetExpression>, 78 ) -> Self { 79 RevsetExpressionEvaluator { 80 repo, 81 extensions, 82 id_prefix_context, 83 expression, 84 } 85 } 86 87 /// Returns the underlying expression. 88 pub fn expression(&self) -> &Rc<UserRevsetExpression> { 89 &self.expression 90 } 91 92 /// Intersects the underlying expression with the `other` expression. 93 pub fn intersect_with(&mut self, other: &Rc<UserRevsetExpression>) { 94 self.expression = self.expression.intersection(other); 95 } 96 97 /// Resolves user symbols in the expression, returns new expression. 98 pub fn resolve(&self) -> Result<Rc<ResolvedRevsetExpression>, RevsetResolutionError> { 99 let symbol_resolver = default_symbol_resolver( 100 self.repo, 101 self.extensions.symbol_resolvers(), 102 self.id_prefix_context, 103 ); 104 self.expression 105 .resolve_user_expression(self.repo, &symbol_resolver) 106 } 107 108 /// Evaluates the expression. 109 pub fn evaluate(&self) -> Result<Box<dyn Revset + 'repo>, UserRevsetEvaluationError> { 110 self.resolve() 111 .map_err(UserRevsetEvaluationError::Resolution)? 112 .evaluate(self.repo) 113 .map_err(UserRevsetEvaluationError::Evaluation) 114 } 115 116 /// Evaluates the expression to an iterator over commit ids. Entries are 117 /// sorted in reverse topological order. 118 pub fn evaluate_to_commit_ids( 119 &self, 120 ) -> Result< 121 Box<dyn Iterator<Item = Result<CommitId, RevsetEvaluationError>> + 'repo>, 122 UserRevsetEvaluationError, 123 > { 124 Ok(self.evaluate()?.iter()) 125 } 126 127 /// Evaluates the expression to an iterator over commit objects. Entries are 128 /// sorted in reverse topological order. 129 pub fn evaluate_to_commits( 130 &self, 131 ) -> Result< 132 impl Iterator<Item = Result<Commit, RevsetEvaluationError>> + use<'repo>, 133 UserRevsetEvaluationError, 134 > { 135 Ok(self.evaluate()?.iter().commits(self.repo.store())) 136 } 137} 138 139fn warn_user_redefined_builtin( 140 ui: &Ui, 141 source: ConfigSource, 142 name: &str, 143) -> Result<(), CommandError> { 144 match source { 145 ConfigSource::Default => (), 146 ConfigSource::EnvBase 147 | ConfigSource::User 148 | ConfigSource::Repo 149 | ConfigSource::EnvOverrides 150 | ConfigSource::CommandArg => { 151 let checked_mutability_builtins = 152 ["mutable()", "immutable()", "builtin_immutable_heads()"]; 153 154 if checked_mutability_builtins.contains(&name) { 155 writeln!( 156 ui.warning_default(), 157 "Redefining `revset-aliases.{name}` is not recommended; redefine \ 158 `immutable_heads()` instead", 159 )?; 160 } 161 } 162 } 163 164 Ok(()) 165} 166 167pub fn load_revset_aliases( 168 ui: &Ui, 169 stacked_config: &StackedConfig, 170) -> Result<RevsetAliasesMap, CommandError> { 171 let table_name = ConfigNamePathBuf::from_iter(["revset-aliases"]); 172 let mut aliases_map = RevsetAliasesMap::new(); 173 // Load from all config layers in order. 'f(x)' in default layer should be 174 // overridden by 'f(a)' in user. 175 for layer in stacked_config.layers() { 176 let table = match layer.look_up_table(&table_name) { 177 Ok(Some(table)) => table, 178 Ok(None) => continue, 179 Err(item) => { 180 return Err(ConfigGetError::Type { 181 name: table_name.to_string(), 182 error: format!("Expected a table, but is {}", item.type_name()).into(), 183 source_path: layer.path.clone(), 184 } 185 .into()); 186 } 187 }; 188 for (decl, item) in table.iter() { 189 warn_user_redefined_builtin(ui, layer.source, decl)?; 190 191 let r = item 192 .as_str() 193 .ok_or_else(|| format!("Expected a string, but is {}", item.type_name())) 194 .and_then(|v| aliases_map.insert(decl, v).map_err(|e| e.to_string())); 195 if let Err(s) = r { 196 writeln!( 197 ui.warning_default(), 198 "Failed to load `{table_name}.{decl}`: {s}" 199 )?; 200 } 201 } 202 } 203 Ok(aliases_map) 204} 205 206/// Wraps the given `IdPrefixContext` in `SymbolResolver` to be passed in to 207/// `evaluate()`. 208pub fn default_symbol_resolver<'a>( 209 repo: &'a dyn Repo, 210 extensions: &[impl AsRef<dyn SymbolResolverExtension>], 211 id_prefix_context: &'a IdPrefixContext, 212) -> DefaultSymbolResolver<'a> { 213 DefaultSymbolResolver::new(repo, extensions).with_id_prefix_context(id_prefix_context) 214} 215 216/// Parses user-configured expression defining the heads of the immutable set. 217/// Includes the root commit. 218pub fn parse_immutable_heads_expression( 219 diagnostics: &mut RevsetDiagnostics, 220 context: &RevsetParseContext, 221) -> Result<Rc<UserRevsetExpression>, RevsetParseError> { 222 let (_, _, immutable_heads_str) = context 223 .aliases_map 224 .get_function(USER_IMMUTABLE_HEADS, 0) 225 .unwrap(); 226 let heads = revset::parse(diagnostics, immutable_heads_str, context)?; 227 Ok(heads.union(&RevsetExpression::root())) 228} 229 230/// Prints warning if `trunk()` alias cannot be resolved. This alias could be 231/// generated by `jj git init`/`clone`. 232pub(super) fn warn_unresolvable_trunk( 233 ui: &Ui, 234 repo: &dyn Repo, 235 context: &RevsetParseContext, 236) -> io::Result<()> { 237 let (_, _, revset_str) = context 238 .aliases_map 239 .get_function("trunk", 0) 240 .expect("trunk() should be defined by default"); 241 let Ok(expression) = revset::parse(&mut RevsetDiagnostics::new(), revset_str, context) else { 242 // Parse error would have been reported. 243 return Ok(()); 244 }; 245 // Not using IdPrefixContext since trunk() revset shouldn't contain short 246 // prefixes. 247 let symbol_resolver = DefaultSymbolResolver::new(repo, context.extensions.symbol_resolvers()); 248 if let Err(err) = expression.resolve_user_expression(repo, &symbol_resolver) { 249 writeln!( 250 ui.warning_default(), 251 "Failed to resolve `revset-aliases.trunk()`: {err}" 252 )?; 253 writeln!( 254 ui.hint_default(), 255 "Use `jj config edit --repo` to adjust the `trunk()` alias." 256 )?; 257 } 258 Ok(()) 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 should_hint_about_all_prefix: bool, 266) -> Result<Commit, CommandError> { 267 let mut iter = expression.evaluate_to_commits()?.fuse(); 268 match (iter.next(), iter.next()) { 269 (Some(commit), None) => Ok(commit?), 270 (None, _) => Err(user_error(format!( 271 "Revset `{revision_str}` didn't resolve to any revisions" 272 ))), 273 (Some(commit0), Some(commit1)) => { 274 let mut iter = [commit0, commit1].into_iter().chain(iter); 275 let commits: Vec<_> = iter.by_ref().take(5).try_collect()?; 276 let elided = iter.next().is_some(); 277 Err(format_multiple_revisions_error( 278 revision_str, 279 expression.expression(), 280 &commits, 281 elided, 282 &commit_summary_template(), 283 should_hint_about_all_prefix, 284 )) 285 } 286 } 287} 288 289fn format_multiple_revisions_error( 290 revision_str: &str, 291 expression: &UserRevsetExpression, 292 commits: &[Commit], 293 elided: bool, 294 template: &TemplateRenderer<'_, Commit>, 295 should_hint_about_all_prefix: bool, 296) -> CommandError { 297 assert!(commits.len() >= 2); 298 let mut cmd_err = user_error(format!( 299 "Revset `{revision_str}` resolved to more than one revision" 300 )); 301 let write_commits_summary = |formatter: &mut dyn Formatter| { 302 for commit in commits { 303 write!(formatter, " ")?; 304 template.format(commit, formatter)?; 305 writeln!(formatter)?; 306 } 307 if elided { 308 writeln!(formatter, " ...")?; 309 } 310 Ok(()) 311 }; 312 if commits[0].change_id() == commits[1].change_id() { 313 // Separate hint if there's commits with same change id 314 cmd_err.add_formatted_hint_with(|formatter| { 315 writeln!( 316 formatter, 317 "The revset `{revision_str}` resolved to these revisions:" 318 )?; 319 write_commits_summary(formatter) 320 }); 321 cmd_err.add_hint( 322 "Some of these commits have the same change id. Abandon one of them with `jj abandon \ 323 -r <REVISION>`.", 324 ); 325 } else if let Some(bookmark_name) = expression.as_symbol() { 326 // Separate hint if there's a conflicted bookmark 327 cmd_err.add_formatted_hint_with(|formatter| { 328 writeln!( 329 formatter, 330 "Bookmark {bookmark_name} resolved to multiple revisions because it's conflicted." 331 )?; 332 writeln!(formatter, "It resolved to these revisions:")?; 333 write_commits_summary(formatter) 334 }); 335 cmd_err.add_hint(format!( 336 "Set which revision the bookmark points to with `jj bookmark set {bookmark_name} -r \ 337 <REVISION>`.", 338 )); 339 } else { 340 cmd_err.add_formatted_hint_with(|formatter| { 341 writeln!( 342 formatter, 343 "The revset `{revision_str}` resolved to these revisions:" 344 )?; 345 write_commits_summary(formatter) 346 }); 347 if should_hint_about_all_prefix { 348 cmd_err.add_hint(format!( 349 "Prefix the expression with `all:` to allow any number of revisions (i.e. \ 350 `all:{revision_str}`)." 351 )); 352 } 353 }; 354 cmd_err 355} 356 357#[derive(Debug, Error)] 358#[error("Failed to parse bookmark name: {}", source.kind())] 359pub struct BookmarkNameParseError { 360 pub input: String, 361 pub source: RevsetParseError, 362} 363 364/// Parses bookmark name specified in revset syntax. 365pub fn parse_bookmark_name(text: &str) -> Result<RefNameBuf, BookmarkNameParseError> { 366 revset::parse_symbol(text) 367 .map(Into::into) 368 .map_err(|source| BookmarkNameParseError { 369 input: text.to_owned(), 370 source, 371 }) 372}