1use std::collections::HashMap;
18use std::sync::Arc;
19
20use itertools::Itertools as _;
21use jj_lib::backend::CommitId;
22use jj_lib::commit::Commit;
23use jj_lib::config::ConfigGetError;
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::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::SymbolResolver;
45use jj_lib::revset::SymbolResolverExtension;
46use jj_lib::revset::UserRevsetExpression;
47use jj_lib::settings::RemoteSettingsMap;
48use jj_lib::str_util::StringExpression;
49use jj_lib::str_util::StringMatcher;
50use thiserror::Error;
51
52use crate::command_error::CommandError;
53use crate::command_error::config_error_with_message;
54use crate::command_error::print_parse_diagnostics;
55use crate::command_error::revset_parse_error_hint;
56use crate::command_error::user_error;
57use crate::command_error::user_error_with_message;
58use crate::formatter::Formatter;
59use crate::templater::TemplateRenderer;
60use crate::ui::Ui;
61
62const USER_IMMUTABLE_HEADS: &str = "immutable_heads";
63
64#[derive(Debug, Error)]
65pub enum UserRevsetEvaluationError {
66 #[error(transparent)]
67 Resolution(RevsetResolutionError),
68 #[error(transparent)]
69 Evaluation(RevsetEvaluationError),
70}
71
72pub struct RevsetExpressionEvaluator<'repo> {
74 repo: &'repo dyn Repo,
75 extensions: Arc<RevsetExtensions>,
76 id_prefix_context: &'repo IdPrefixContext,
77 expression: Arc<UserRevsetExpression>,
78}
79
80impl<'repo> RevsetExpressionEvaluator<'repo> {
81 pub fn new(
82 repo: &'repo dyn Repo,
83 extensions: Arc<RevsetExtensions>,
84 id_prefix_context: &'repo IdPrefixContext,
85 expression: Arc<UserRevsetExpression>,
86 ) -> Self {
87 Self {
88 repo,
89 extensions,
90 id_prefix_context,
91 expression,
92 }
93 }
94
95 pub fn expression(&self) -> &Arc<UserRevsetExpression> {
97 &self.expression
98 }
99
100 pub fn intersect_with(&mut self, other: &Arc<UserRevsetExpression>) {
102 self.expression = self.expression.intersection(other);
103 }
104
105 pub fn resolve(&self) -> Result<Arc<ResolvedRevsetExpression>, RevsetResolutionError> {
107 let symbol_resolver = default_symbol_resolver(
108 self.repo,
109 self.extensions.symbol_resolvers(),
110 self.id_prefix_context,
111 );
112 self.expression
113 .resolve_user_expression(self.repo, &symbol_resolver)
114 }
115
116 pub fn evaluate(&self) -> Result<Box<dyn Revset + 'repo>, UserRevsetEvaluationError> {
118 self.resolve()
119 .map_err(UserRevsetEvaluationError::Resolution)?
120 .evaluate(self.repo)
121 .map_err(UserRevsetEvaluationError::Evaluation)
122 }
123
124 pub fn evaluate_to_commit_ids(
127 &self,
128 ) -> Result<
129 Box<dyn Iterator<Item = Result<CommitId, RevsetEvaluationError>> + 'repo>,
130 UserRevsetEvaluationError,
131 > {
132 Ok(self.evaluate()?.iter())
133 }
134
135 pub fn evaluate_to_commits(
138 &self,
139 ) -> Result<
140 impl Iterator<Item = Result<Commit, RevsetEvaluationError>> + use<'repo>,
141 UserRevsetEvaluationError,
142 > {
143 Ok(self.evaluate()?.iter().commits(self.repo.store()))
144 }
145}
146
147fn warn_user_redefined_builtin(
148 ui: &Ui,
149 source: ConfigSource,
150 name: &str,
151) -> Result<(), CommandError> {
152 match source {
153 ConfigSource::Default => (),
154 ConfigSource::EnvBase
155 | ConfigSource::User
156 | ConfigSource::Repo
157 | ConfigSource::Workspace
158 | ConfigSource::EnvOverrides
159 | ConfigSource::CommandArg => {
160 let checked_mutability_builtins =
161 ["mutable()", "immutable()", "builtin_immutable_heads()"];
162
163 if checked_mutability_builtins.contains(&name) {
164 writeln!(
165 ui.warning_default(),
166 "Redefining `revset-aliases.{name}` is not recommended; redefine \
167 `immutable_heads()` instead",
168 )?;
169 }
170 }
171 }
172
173 Ok(())
174}
175
176pub fn load_revset_aliases(
177 ui: &Ui,
178 stacked_config: &StackedConfig,
179) -> Result<RevsetAliasesMap, CommandError> {
180 let table_name = ConfigNamePathBuf::from_iter(["revset-aliases"]);
181 let mut aliases_map = RevsetAliasesMap::new();
182 for layer in stacked_config.layers() {
185 let table = match layer.look_up_table(&table_name) {
186 Ok(Some(table)) => table,
187 Ok(None) => continue,
188 Err(item) => {
189 return Err(ConfigGetError::Type {
190 name: table_name.to_string(),
191 error: format!("Expected a table, but is {}", item.type_name()).into(),
192 source_path: layer.path.clone(),
193 }
194 .into());
195 }
196 };
197 for (decl, item) in table.iter() {
198 warn_user_redefined_builtin(ui, layer.source, decl)?;
199
200 let r = item
201 .as_str()
202 .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
203 .and_then(|v| aliases_map.insert(decl, v).map_err(|e| e.to_string()));
204 if let Err(s) = r {
205 writeln!(
206 ui.warning_default(),
207 "Failed to load `{table_name}.{decl}`: {s}"
208 )?;
209 }
210 }
211 }
212 Ok(aliases_map)
213}
214
215pub fn default_symbol_resolver<'a>(
218 repo: &'a dyn Repo,
219 extensions: &[impl AsRef<dyn SymbolResolverExtension>],
220 id_prefix_context: &'a IdPrefixContext,
221) -> SymbolResolver<'a> {
222 SymbolResolver::new(repo, extensions).with_id_prefix_context(id_prefix_context)
223}
224
225pub fn parse_immutable_heads_expression(
228 diagnostics: &mut RevsetDiagnostics,
229 context: &RevsetParseContext,
230) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
231 let (_, _, immutable_heads_str) = context
232 .aliases_map
233 .get_function(USER_IMMUTABLE_HEADS, 0)
234 .unwrap();
235 let heads = revset::parse(diagnostics, immutable_heads_str, context)?;
236 Ok(heads.union(&RevsetExpression::root()))
237}
238
239pub(super) fn try_resolve_trunk_alias(
244 repo: &dyn Repo,
245 context: &RevsetParseContext,
246) -> Result<Option<Arc<ResolvedRevsetExpression>>, RevsetResolutionError> {
247 let (_, _, revset_str) = context
248 .aliases_map
249 .get_function("trunk", 0)
250 .expect("trunk() should be defined by default");
251 let Ok(expression) = revset::parse(&mut RevsetDiagnostics::new(), revset_str, context) else {
252 return Ok(None);
253 };
254 let symbol_resolver = SymbolResolver::new(repo, context.extensions.symbol_resolvers());
257 let resolved = expression.resolve_user_expression(repo, &symbol_resolver)?;
258 Ok(Some(resolved))
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) -> Result<Commit, CommandError> {
266 let mut iter = expression.evaluate_to_commits()?.fuse();
267 match (iter.next(), iter.next()) {
268 (Some(commit), None) => Ok(commit?),
269 (None, _) => Err(user_error(format!(
270 "Revset `{revision_str}` didn't resolve to any revisions"
271 ))),
272 (Some(commit0), Some(commit1)) => {
273 let mut iter = [commit0, commit1].into_iter().chain(iter);
274 let commits: Vec<_> = iter.by_ref().take(5).try_collect()?;
275 let elided = iter.next().is_some();
276 Err(format_multiple_revisions_error(
277 revision_str,
278 &commits,
279 elided,
280 &commit_summary_template(),
281 ))
282 }
283 }
284}
285
286fn format_multiple_revisions_error(
287 revision_str: &str,
288 commits: &[Commit],
289 elided: bool,
290 template: &TemplateRenderer<'_, Commit>,
291) -> CommandError {
292 assert!(commits.len() >= 2);
293 let mut cmd_err = user_error(format!(
294 "Revset `{revision_str}` resolved to more than one revision"
295 ));
296 let write_commits_summary = |formatter: &mut dyn Formatter| {
297 for commit in commits {
298 write!(formatter, " ")?;
299 template.format(commit, formatter)?;
300 writeln!(formatter)?;
301 }
302 if elided {
303 writeln!(formatter, " ...")?;
304 }
305 Ok(())
306 };
307 cmd_err.add_formatted_hint_with(|formatter| {
308 writeln!(
309 formatter,
310 "The revset `{revision_str}` resolved to these revisions:"
311 )?;
312 write_commits_summary(formatter)
313 });
314 cmd_err
315}
316
317#[derive(Debug, Error)]
318#[error("Failed to parse bookmark name: {}", source.kind())]
319pub struct BookmarkNameParseError {
320 pub input: String,
321 pub source: RevsetParseError,
322}
323
324pub fn parse_bookmark_name(text: &str) -> Result<RefNameBuf, BookmarkNameParseError> {
326 revset::parse_symbol(text)
327 .map(Into::into)
328 .map_err(|source| BookmarkNameParseError {
329 input: text.to_owned(),
330 source,
331 })
332}
333
334#[derive(Debug, Error)]
335#[error("Failed to parse tag name: {}", source.kind())]
336pub struct TagNameParseError {
337 pub source: RevsetParseError,
338}
339
340pub fn parse_tag_name(text: &str) -> Result<RefNameBuf, TagNameParseError> {
342 revset::parse_symbol(text)
343 .map(Into::into)
344 .map_err(|source| TagNameParseError { source })
345}
346
347pub fn parse_union_name_patterns<I>(ui: &Ui, texts: I) -> Result<StringExpression, CommandError>
349where
350 I: IntoIterator,
351 I::Item: AsRef<str>,
352{
353 let mut diagnostics = RevsetDiagnostics::new();
354 let expressions = texts
355 .into_iter()
356 .map(|text| revset::parse_string_expression(&mut diagnostics, text.as_ref()))
357 .try_collect()
358 .map_err(|err| {
359 let hint = revset_parse_error_hint(&err);
361 let message = format!("Failed to parse name pattern: {}", err.kind());
362 let mut cmd_err = user_error_with_message(message, err);
363 cmd_err.extend_hints(hint);
364 cmd_err
365 })?;
366 print_parse_diagnostics(ui, "In name pattern", &diagnostics)?;
367 Ok(StringExpression::union_all(expressions))
368}
369
370pub fn parse_remote_auto_track_bookmarks_map(
373 ui: &Ui,
374 remote_settings: &RemoteSettingsMap,
375) -> Result<HashMap<RemoteNameBuf, StringMatcher>, CommandError> {
376 let mut matchers = HashMap::new();
377 for (name, settings) in remote_settings {
378 let Some(text) = &settings.auto_track_bookmarks else {
379 continue;
380 };
381 let expr = parse_remote_auto_track_text(ui, name, text, "auto-track-bookmarks")?;
382 matchers.insert(name.clone(), expr.to_matcher());
383 }
384 Ok(matchers)
385}
386
387pub fn parse_remote_auto_track_bookmarks_map_for_new_bookmarks(
392 ui: &Ui,
393 remote_settings: &RemoteSettingsMap,
394) -> Result<HashMap<RemoteNameBuf, StringMatcher>, CommandError> {
395 let mut matchers = HashMap::new();
396 for (name, settings) in remote_settings {
397 let mut exprs = Vec::new();
398 if let Some(text) = &settings.auto_track_bookmarks {
399 exprs.push(parse_remote_auto_track_text(
400 ui,
401 name,
402 text,
403 "auto-track-bookmarks",
404 )?);
405 }
406 if let Some(text) = &settings.auto_track_created_bookmarks {
407 exprs.push(parse_remote_auto_track_text(
408 ui,
409 name,
410 text,
411 "auto-track-created-bookmarks",
412 )?);
413 }
414 matchers.insert(
415 name.clone(),
416 StringExpression::union_all(exprs).to_matcher(),
417 );
418 }
419 Ok(matchers)
420}
421
422fn parse_remote_auto_track_text(
423 ui: &Ui,
424 name: &RemoteName,
425 text: &str,
426 field_name: &str,
427) -> Result<StringExpression, CommandError> {
428 let mut diagnostics = RevsetDiagnostics::new();
429 let expr = revset::parse_string_expression(&mut diagnostics, text).map_err(|err| {
430 let hint = revset_parse_error_hint(&err);
432 let message = format!(
433 "Invalid `remotes.{}.{field_name}`: {}",
434 name.as_symbol(),
435 err.kind()
436 );
437 let mut cmd_err = config_error_with_message(message, err);
438 cmd_err.extend_hints(hint);
439 cmd_err
440 })?;
441 print_parse_diagnostics(
442 ui,
443 &format!("In `remotes.{}.{field_name}`", name.as_symbol()),
444 &diagnostics,
445 )?;
446 Ok(expr)
447}