1use std::io;
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::repo::Repo;
30use jj_lib::revset;
31use jj_lib::revset::ResolvedRevsetExpression;
32use jj_lib::revset::Revset;
33use jj_lib::revset::RevsetAliasesMap;
34use jj_lib::revset::RevsetDiagnostics;
35use jj_lib::revset::RevsetEvaluationError;
36use jj_lib::revset::RevsetExpression;
37use jj_lib::revset::RevsetExtensions;
38use jj_lib::revset::RevsetIteratorExt as _;
39use jj_lib::revset::RevsetParseContext;
40use jj_lib::revset::RevsetParseError;
41use jj_lib::revset::RevsetResolutionError;
42use jj_lib::revset::SymbolResolver;
43use jj_lib::revset::SymbolResolverExtension;
44use jj_lib::revset::UserRevsetExpression;
45use jj_lib::str_util::StringExpression;
46use thiserror::Error;
47
48use crate::command_error::CommandError;
49use crate::command_error::print_parse_diagnostics;
50use crate::command_error::revset_parse_error_hint;
51use crate::command_error::user_error;
52use crate::command_error::user_error_with_message;
53use crate::formatter::Formatter;
54use crate::templater::TemplateRenderer;
55use crate::ui::Ui;
56
57const USER_IMMUTABLE_HEADS: &str = "immutable_heads";
58
59#[derive(Debug, Error)]
60pub enum UserRevsetEvaluationError {
61 #[error(transparent)]
62 Resolution(RevsetResolutionError),
63 #[error(transparent)]
64 Evaluation(RevsetEvaluationError),
65}
66
67pub struct RevsetExpressionEvaluator<'repo> {
69 repo: &'repo dyn Repo,
70 extensions: Arc<RevsetExtensions>,
71 id_prefix_context: &'repo IdPrefixContext,
72 expression: Arc<UserRevsetExpression>,
73}
74
75impl<'repo> RevsetExpressionEvaluator<'repo> {
76 pub fn new(
77 repo: &'repo dyn Repo,
78 extensions: Arc<RevsetExtensions>,
79 id_prefix_context: &'repo IdPrefixContext,
80 expression: Arc<UserRevsetExpression>,
81 ) -> Self {
82 Self {
83 repo,
84 extensions,
85 id_prefix_context,
86 expression,
87 }
88 }
89
90 pub fn expression(&self) -> &Arc<UserRevsetExpression> {
92 &self.expression
93 }
94
95 pub fn intersect_with(&mut self, other: &Arc<UserRevsetExpression>) {
97 self.expression = self.expression.intersection(other);
98 }
99
100 pub fn resolve(&self) -> Result<Arc<ResolvedRevsetExpression>, RevsetResolutionError> {
102 let symbol_resolver = default_symbol_resolver(
103 self.repo,
104 self.extensions.symbol_resolvers(),
105 self.id_prefix_context,
106 );
107 self.expression
108 .resolve_user_expression(self.repo, &symbol_resolver)
109 }
110
111 pub fn evaluate(&self) -> Result<Box<dyn Revset + 'repo>, UserRevsetEvaluationError> {
113 self.resolve()
114 .map_err(UserRevsetEvaluationError::Resolution)?
115 .evaluate(self.repo)
116 .map_err(UserRevsetEvaluationError::Evaluation)
117 }
118
119 pub fn evaluate_to_commit_ids(
122 &self,
123 ) -> Result<
124 Box<dyn Iterator<Item = Result<CommitId, RevsetEvaluationError>> + 'repo>,
125 UserRevsetEvaluationError,
126 > {
127 Ok(self.evaluate()?.iter())
128 }
129
130 pub fn evaluate_to_commits(
133 &self,
134 ) -> Result<
135 impl Iterator<Item = Result<Commit, RevsetEvaluationError>> + use<'repo>,
136 UserRevsetEvaluationError,
137 > {
138 Ok(self.evaluate()?.iter().commits(self.repo.store()))
139 }
140}
141
142fn warn_user_redefined_builtin(
143 ui: &Ui,
144 source: ConfigSource,
145 name: &str,
146) -> Result<(), CommandError> {
147 match source {
148 ConfigSource::Default => (),
149 ConfigSource::EnvBase
150 | ConfigSource::User
151 | ConfigSource::Repo
152 | ConfigSource::Workspace
153 | ConfigSource::EnvOverrides
154 | ConfigSource::CommandArg => {
155 let checked_mutability_builtins =
156 ["mutable()", "immutable()", "builtin_immutable_heads()"];
157
158 if checked_mutability_builtins.contains(&name) {
159 writeln!(
160 ui.warning_default(),
161 "Redefining `revset-aliases.{name}` is not recommended; redefine \
162 `immutable_heads()` instead",
163 )?;
164 }
165 }
166 }
167
168 Ok(())
169}
170
171pub fn load_revset_aliases(
172 ui: &Ui,
173 stacked_config: &StackedConfig,
174) -> Result<RevsetAliasesMap, CommandError> {
175 let table_name = ConfigNamePathBuf::from_iter(["revset-aliases"]);
176 let mut aliases_map = RevsetAliasesMap::new();
177 for layer in stacked_config.layers() {
180 let table = match layer.look_up_table(&table_name) {
181 Ok(Some(table)) => table,
182 Ok(None) => continue,
183 Err(item) => {
184 return Err(ConfigGetError::Type {
185 name: table_name.to_string(),
186 error: format!("Expected a table, but is {}", item.type_name()).into(),
187 source_path: layer.path.clone(),
188 }
189 .into());
190 }
191 };
192 for (decl, item) in table.iter() {
193 warn_user_redefined_builtin(ui, layer.source, decl)?;
194
195 let r = item
196 .as_str()
197 .ok_or_else(|| format!("Expected a string, but is {}", item.type_name()))
198 .and_then(|v| aliases_map.insert(decl, v).map_err(|e| e.to_string()));
199 if let Err(s) = r {
200 writeln!(
201 ui.warning_default(),
202 "Failed to load `{table_name}.{decl}`: {s}"
203 )?;
204 }
205 }
206 }
207 Ok(aliases_map)
208}
209
210pub fn default_symbol_resolver<'a>(
213 repo: &'a dyn Repo,
214 extensions: &[impl AsRef<dyn SymbolResolverExtension>],
215 id_prefix_context: &'a IdPrefixContext,
216) -> SymbolResolver<'a> {
217 SymbolResolver::new(repo, extensions).with_id_prefix_context(id_prefix_context)
218}
219
220pub fn parse_immutable_heads_expression(
223 diagnostics: &mut RevsetDiagnostics,
224 context: &RevsetParseContext,
225) -> Result<Arc<UserRevsetExpression>, RevsetParseError> {
226 let (_, _, immutable_heads_str) = context
227 .aliases_map
228 .get_function(USER_IMMUTABLE_HEADS, 0)
229 .unwrap();
230 let heads = revset::parse(diagnostics, immutable_heads_str, context)?;
231 Ok(heads.union(&RevsetExpression::root()))
232}
233
234pub(super) fn warn_unresolvable_trunk(
237 ui: &Ui,
238 repo: &dyn Repo,
239 context: &RevsetParseContext,
240) -> io::Result<()> {
241 let (_, _, revset_str) = context
242 .aliases_map
243 .get_function("trunk", 0)
244 .expect("trunk() should be defined by default");
245 let Ok(expression) = revset::parse(&mut RevsetDiagnostics::new(), revset_str, context) else {
246 return Ok(());
248 };
249 let symbol_resolver = SymbolResolver::new(repo, context.extensions.symbol_resolvers());
252 if let Err(err) = expression.resolve_user_expression(repo, &symbol_resolver) {
253 writeln!(
254 ui.warning_default(),
255 "Failed to resolve `revset-aliases.trunk()`: {err}"
256 )?;
257 writeln!(
258 ui.hint_default(),
259 "Use `jj config edit --repo` to adjust the `trunk()` alias."
260 )?;
261 }
262 Ok(())
263}
264
265pub(super) fn evaluate_revset_to_single_commit<'a>(
266 revision_str: &str,
267 expression: &RevsetExpressionEvaluator<'_>,
268 commit_summary_template: impl FnOnce() -> TemplateRenderer<'a, Commit>,
269) -> Result<Commit, CommandError> {
270 let mut iter = expression.evaluate_to_commits()?.fuse();
271 match (iter.next(), iter.next()) {
272 (Some(commit), None) => Ok(commit?),
273 (None, _) => Err(user_error(format!(
274 "Revset `{revision_str}` didn't resolve to any revisions"
275 ))),
276 (Some(commit0), Some(commit1)) => {
277 let mut iter = [commit0, commit1].into_iter().chain(iter);
278 let commits: Vec<_> = iter.by_ref().take(5).try_collect()?;
279 let elided = iter.next().is_some();
280 Err(format_multiple_revisions_error(
281 revision_str,
282 &commits,
283 elided,
284 &commit_summary_template(),
285 ))
286 }
287 }
288}
289
290fn format_multiple_revisions_error(
291 revision_str: &str,
292 commits: &[Commit],
293 elided: bool,
294 template: &TemplateRenderer<'_, Commit>,
295) -> CommandError {
296 assert!(commits.len() >= 2);
297 let mut cmd_err = user_error(format!(
298 "Revset `{revision_str}` resolved to more than one revision"
299 ));
300 let write_commits_summary = |formatter: &mut dyn Formatter| {
301 for commit in commits {
302 write!(formatter, " ")?;
303 template.format(commit, formatter)?;
304 writeln!(formatter)?;
305 }
306 if elided {
307 writeln!(formatter, " ...")?;
308 }
309 Ok(())
310 };
311 cmd_err.add_formatted_hint_with(|formatter| {
312 writeln!(
313 formatter,
314 "The revset `{revision_str}` resolved to these revisions:"
315 )?;
316 write_commits_summary(formatter)
317 });
318 cmd_err
319}
320
321#[derive(Debug, Error)]
322#[error("Failed to parse bookmark name: {}", source.kind())]
323pub struct BookmarkNameParseError {
324 pub input: String,
325 pub source: RevsetParseError,
326}
327
328pub fn parse_bookmark_name(text: &str) -> Result<RefNameBuf, BookmarkNameParseError> {
330 revset::parse_symbol(text)
331 .map(Into::into)
332 .map_err(|source| BookmarkNameParseError {
333 input: text.to_owned(),
334 source,
335 })
336}
337
338#[derive(Debug, Error)]
339#[error("Failed to parse tag name: {}", source.kind())]
340pub struct TagNameParseError {
341 pub source: RevsetParseError,
342}
343
344pub fn parse_tag_name(text: &str) -> Result<RefNameBuf, TagNameParseError> {
346 revset::parse_symbol(text)
347 .map(Into::into)
348 .map_err(|source| TagNameParseError { source })
349}
350
351pub fn parse_union_name_patterns<I>(ui: &Ui, texts: I) -> Result<StringExpression, CommandError>
353where
354 I: IntoIterator,
355 I::Item: AsRef<str>,
356{
357 let mut diagnostics = RevsetDiagnostics::new();
358 let expressions = texts
359 .into_iter()
360 .map(|text| revset::parse_string_expression(&mut diagnostics, text.as_ref()))
361 .try_collect()
362 .map_err(|err| {
363 let hint = revset_parse_error_hint(&err);
365 let message = format!("Failed to parse name pattern: {}", err.kind());
366 let mut cmd_err = user_error_with_message(message, err);
367 cmd_err.extend_hints(hint);
368 cmd_err
369 })?;
370 print_parse_diagnostics(ui, "In name pattern", &diagnostics)?;
371 Ok(StringExpression::union_all(expressions))
372}