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 thiserror::Error;
46
47use crate::command_error::CommandError;
48use crate::command_error::user_error;
49use crate::formatter::Formatter;
50use crate::templater::TemplateRenderer;
51use crate::ui::Ui;
52
53const USER_IMMUTABLE_HEADS: &str = "immutable_heads";
54
55#[derive(Debug, Error)]
56pub enum UserRevsetEvaluationError {
57 #[error(transparent)]
58 Resolution(RevsetResolutionError),
59 #[error(transparent)]
60 Evaluation(RevsetEvaluationError),
61}
62
63pub struct RevsetExpressionEvaluator<'repo> {
65 repo: &'repo dyn Repo,
66 extensions: Arc<RevsetExtensions>,
67 id_prefix_context: &'repo IdPrefixContext,
68 expression: Arc<UserRevsetExpression>,
69}
70
71impl<'repo> RevsetExpressionEvaluator<'repo> {
72 pub fn new(
73 repo: &'repo dyn Repo,
74 extensions: Arc<RevsetExtensions>,
75 id_prefix_context: &'repo IdPrefixContext,
76 expression: Arc<UserRevsetExpression>,
77 ) -> Self {
78 Self {
79 repo,
80 extensions,
81 id_prefix_context,
82 expression,
83 }
84 }
85
86 pub fn expression(&self) -> &Arc<UserRevsetExpression> {
88 &self.expression
89 }
90
91 pub fn intersect_with(&mut self, other: &Arc<UserRevsetExpression>) {
93 self.expression = self.expression.intersection(other);
94 }
95
96 pub fn resolve(&self) -> Result<Arc<ResolvedRevsetExpression>, RevsetResolutionError> {
98 let symbol_resolver = default_symbol_resolver(
99 self.repo,
100 self.extensions.symbol_resolvers(),
101 self.id_prefix_context,
102 );
103 self.expression
104 .resolve_user_expression(self.repo, &symbol_resolver)
105 }
106
107 pub fn evaluate(&self) -> Result<Box<dyn Revset + 'repo>, UserRevsetEvaluationError> {
109 self.resolve()
110 .map_err(UserRevsetEvaluationError::Resolution)?
111 .evaluate(self.repo)
112 .map_err(UserRevsetEvaluationError::Evaluation)
113 }
114
115 pub fn evaluate_to_commit_ids(
118 &self,
119 ) -> Result<
120 Box<dyn Iterator<Item = Result<CommitId, RevsetEvaluationError>> + 'repo>,
121 UserRevsetEvaluationError,
122 > {
123 Ok(self.evaluate()?.iter())
124 }
125
126 pub fn evaluate_to_commits(
129 &self,
130 ) -> Result<
131 impl Iterator<Item = Result<Commit, RevsetEvaluationError>> + use<'repo>,
132 UserRevsetEvaluationError,
133 > {
134 Ok(self.evaluate()?.iter().commits(self.repo.store()))
135 }
136}
137
138fn warn_user_redefined_builtin(
139 ui: &Ui,
140 source: ConfigSource,
141 name: &str,
142) -> Result<(), CommandError> {
143 match source {
144 ConfigSource::Default => (),
145 ConfigSource::EnvBase
146 | ConfigSource::User
147 | ConfigSource::Repo
148 | ConfigSource::Workspace
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) -> SymbolResolver<'a> {
213 SymbolResolver::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<Arc<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 = SymbolResolver::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) -> 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}