just playing with tangled
1// Copyright 2022-2024 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Utility for parsing and evaluating user-provided revset expressions.
16
17use std::rc::Rc;
18use std::sync::Arc;
19
20use itertools::Itertools as _;
21use jj_lib::backend::{BackendResult, CommitId};
22use jj_lib::commit::Commit;
23use jj_lib::id_prefix::IdPrefixContext;
24use jj_lib::repo::Repo;
25use jj_lib::revset::{
26 self, DefaultSymbolResolver, Revset, RevsetAliasesMap, RevsetCommitRef, RevsetEvaluationError,
27 RevsetExpression, RevsetExtensions, RevsetIteratorExt as _, RevsetParseContext,
28 RevsetParseError, RevsetResolutionError, SymbolResolverExtension,
29};
30use jj_lib::settings::ConfigResultExt as _;
31use thiserror::Error;
32
33use crate::command_error::{user_error, CommandError};
34use crate::config::LayeredConfigs;
35use crate::formatter::Formatter;
36use crate::templater::TemplateRenderer;
37use crate::ui::Ui;
38
39const BUILTIN_IMMUTABLE_HEADS: &str = "immutable_heads";
40
41#[derive(Debug, Error)]
42pub enum UserRevsetEvaluationError {
43 #[error(transparent)]
44 Resolution(RevsetResolutionError),
45 #[error(transparent)]
46 Evaluation(RevsetEvaluationError),
47}
48
49/// Wrapper around `RevsetExpression` to provide convenient methods.
50pub struct RevsetExpressionEvaluator<'repo> {
51 repo: &'repo dyn Repo,
52 extensions: Arc<RevsetExtensions>,
53 id_prefix_context: &'repo IdPrefixContext,
54 expression: Rc<RevsetExpression>,
55}
56
57impl<'repo> RevsetExpressionEvaluator<'repo> {
58 pub fn new(
59 repo: &'repo dyn Repo,
60 extensions: Arc<RevsetExtensions>,
61 id_prefix_context: &'repo IdPrefixContext,
62 expression: Rc<RevsetExpression>,
63 ) -> Self {
64 RevsetExpressionEvaluator {
65 repo,
66 extensions,
67 id_prefix_context,
68 expression,
69 }
70 }
71
72 /// Returns the underlying expression.
73 pub fn expression(&self) -> &Rc<RevsetExpression> {
74 &self.expression
75 }
76
77 /// Intersects the underlying expression with the `other` expression.
78 pub fn intersect_with(&mut self, other: &Rc<RevsetExpression>) {
79 self.expression = self.expression.intersection(other);
80 }
81
82 /// Evaluates the expression.
83 pub fn evaluate(&self) -> Result<Box<dyn Revset + 'repo>, UserRevsetEvaluationError> {
84 let symbol_resolver = default_symbol_resolver(
85 self.repo,
86 self.extensions.symbol_resolvers(),
87 self.id_prefix_context,
88 );
89 evaluate(self.repo, &symbol_resolver, self.expression.clone())
90 }
91
92 /// Evaluates the expression to an iterator over commit ids. Entries are
93 /// sorted in reverse topological order.
94 pub fn evaluate_to_commit_ids(
95 &self,
96 ) -> Result<Box<dyn Iterator<Item = CommitId> + 'repo>, UserRevsetEvaluationError> {
97 Ok(self.evaluate()?.iter())
98 }
99
100 /// Evaluates the expression to an iterator over commit objects. Entries are
101 /// sorted in reverse topological order.
102 pub fn evaluate_to_commits(
103 &self,
104 ) -> Result<impl Iterator<Item = BackendResult<Commit>> + 'repo, UserRevsetEvaluationError>
105 {
106 Ok(self.evaluate()?.iter().commits(self.repo.store()))
107 }
108}
109
110pub fn load_revset_aliases(
111 ui: &Ui,
112 layered_configs: &LayeredConfigs,
113) -> Result<RevsetAliasesMap, CommandError> {
114 const TABLE_KEY: &str = "revset-aliases";
115 let mut aliases_map = RevsetAliasesMap::new();
116 // Load from all config layers in order. 'f(x)' in default layer should be
117 // overridden by 'f(a)' in user.
118 for (_, config) in layered_configs.sources() {
119 let table = if let Some(table) = config.get_table(TABLE_KEY).optional()? {
120 table
121 } else {
122 continue;
123 };
124 for (decl, value) in table.into_iter().sorted_by(|a, b| a.0.cmp(&b.0)) {
125 let r = value
126 .into_string()
127 .map_err(|e| e.to_string())
128 .and_then(|v| aliases_map.insert(&decl, v).map_err(|e| e.to_string()));
129 if let Err(s) = r {
130 writeln!(
131 ui.warning_default(),
132 r#"Failed to load "{TABLE_KEY}.{decl}": {s}"#
133 )?;
134 }
135 }
136 }
137
138 // TODO: If we add support for function overloading (#2966), this check can
139 // be removed.
140 let (params, _) = aliases_map.get_function(BUILTIN_IMMUTABLE_HEADS).unwrap();
141 if !params.is_empty() {
142 return Err(user_error(format!(
143 "The `revset-aliases.{name}()` function must be declared without arguments",
144 name = BUILTIN_IMMUTABLE_HEADS
145 )));
146 }
147
148 Ok(aliases_map)
149}
150
151pub fn evaluate<'a>(
152 repo: &'a dyn Repo,
153 symbol_resolver: &DefaultSymbolResolver,
154 expression: Rc<RevsetExpression>,
155) -> Result<Box<dyn Revset + 'a>, UserRevsetEvaluationError> {
156 let resolved = revset::optimize(expression)
157 .resolve_user_expression(repo, symbol_resolver)
158 .map_err(UserRevsetEvaluationError::Resolution)?;
159 resolved
160 .evaluate(repo)
161 .map_err(UserRevsetEvaluationError::Evaluation)
162}
163
164/// Wraps the given `IdPrefixContext` in `SymbolResolver` to be passed in to
165/// `evaluate()`.
166pub fn default_symbol_resolver<'a>(
167 repo: &'a dyn Repo,
168 extensions: &[impl AsRef<dyn SymbolResolverExtension>],
169 id_prefix_context: &'a IdPrefixContext,
170) -> DefaultSymbolResolver<'a> {
171 DefaultSymbolResolver::new(repo, extensions).with_id_prefix_context(id_prefix_context)
172}
173
174/// Parses user-configured expression defining the immutable set.
175pub fn parse_immutable_expression(
176 context: &RevsetParseContext,
177) -> Result<Rc<RevsetExpression>, RevsetParseError> {
178 let (params, immutable_heads_str) = context
179 .aliases_map
180 .get_function(BUILTIN_IMMUTABLE_HEADS)
181 .unwrap();
182 assert!(
183 params.is_empty(),
184 "invalid declaration should have been rejected by load_revset_aliases()"
185 );
186 // Negated ancestors expression `~::(<heads> | root())` is slightly easier
187 // to optimize than negated union `~(::<heads> | root())`.
188 let heads = revset::parse(immutable_heads_str, context)?;
189 Ok(heads.union(&RevsetExpression::root()).ancestors())
190}
191
192pub(super) fn evaluate_revset_to_single_commit<'a>(
193 revision_str: &str,
194 expression: &RevsetExpressionEvaluator<'_>,
195 commit_summary_template: impl FnOnce() -> TemplateRenderer<'a, Commit>,
196 should_hint_about_all_prefix: bool,
197) -> Result<Commit, CommandError> {
198 let mut iter = expression.evaluate_to_commits()?.fuse();
199 match (iter.next(), iter.next()) {
200 (Some(commit), None) => Ok(commit?),
201 (None, _) => Err(user_error(format!(
202 r#"Revset "{revision_str}" didn't resolve to any revisions"#
203 ))),
204 (Some(commit0), Some(commit1)) => {
205 let mut iter = [commit0, commit1].into_iter().chain(iter);
206 let commits: Vec<_> = iter.by_ref().take(5).try_collect()?;
207 let elided = iter.next().is_some();
208 Err(format_multiple_revisions_error(
209 revision_str,
210 expression.expression(),
211 &commits,
212 elided,
213 &commit_summary_template(),
214 should_hint_about_all_prefix,
215 ))
216 }
217 }
218}
219
220fn format_multiple_revisions_error(
221 revision_str: &str,
222 expression: &RevsetExpression,
223 commits: &[Commit],
224 elided: bool,
225 template: &TemplateRenderer<'_, Commit>,
226 should_hint_about_all_prefix: bool,
227) -> CommandError {
228 assert!(commits.len() >= 2);
229 let mut cmd_err = user_error(format!(
230 r#"Revset "{revision_str}" resolved to more than one revision"#
231 ));
232 let write_commits_summary = |formatter: &mut dyn Formatter| {
233 for commit in commits {
234 write!(formatter, " ")?;
235 template.format(commit, formatter)?;
236 writeln!(formatter)?;
237 }
238 if elided {
239 writeln!(formatter, " ...")?;
240 }
241 Ok(())
242 };
243 if commits[0].change_id() == commits[1].change_id() {
244 // Separate hint if there's commits with same change id
245 cmd_err.add_formatted_hint_with(|formatter| {
246 writeln!(
247 formatter,
248 r#"The revset "{revision_str}" resolved to these revisions:"#
249 )?;
250 write_commits_summary(formatter)
251 });
252 cmd_err.add_hint(
253 "Some of these commits have the same change id. Abandon one of them with `jj abandon \
254 -r <REVISION>`.",
255 );
256 } else if let RevsetExpression::CommitRef(RevsetCommitRef::Symbol(branch_name)) = expression {
257 // Separate hint if there's a conflicted branch
258 cmd_err.add_formatted_hint_with(|formatter| {
259 writeln!(
260 formatter,
261 "Branch {branch_name} resolved to multiple revisions because it's conflicted."
262 )?;
263 writeln!(formatter, "It resolved to these revisions:")?;
264 write_commits_summary(formatter)
265 });
266 cmd_err.add_hint(format!(
267 "Set which revision the branch points to with `jj branch set {branch_name} -r \
268 <REVISION>`.",
269 ));
270 } else {
271 cmd_err.add_formatted_hint_with(|formatter| {
272 writeln!(
273 formatter,
274 r#"The revset "{revision_str}" resolved to these revisions:"#
275 )?;
276 write_commits_summary(formatter)
277 });
278 if should_hint_about_all_prefix {
279 cmd_err.add_hint(format!(
280 "Prefix the expression with 'all:' to allow any number of revisions (i.e. \
281 'all:{revision_str}')."
282 ));
283 }
284 };
285 cmd_err
286}