1use 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
64pub 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 pub fn expression(&self) -> &Rc<UserRevsetExpression> {
89 &self.expression
90 }
91
92 pub fn intersect_with(&mut self, other: &Rc<UserRevsetExpression>) {
94 self.expression = self.expression.intersection(other);
95 }
96
97 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 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 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 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 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
206pub 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
216pub 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
230pub(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 return Ok(());
244 };
245 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 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 the unneeded commits with `jj \
323 abandon <commit_id>`.",
324 );
325 } else if let Some(bookmark_name) = expression.as_symbol() {
326 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
364pub 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}