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