1use std::collections::HashMap;
2use std::fmt::Display;
3use std::num::ParseIntError;
4use std::sync::Arc;
5
6use eden_dag::errors::BackendError;
7use itertools::Itertools;
8use lib::core::effects::{Effects, OperationType};
9use thiserror::Error;
10
11use lib::core::dag::{CommitSet, Dag};
12use lib::core::formatting::Pluralize;
13use lib::git::{ConfigRead, Repo, RepoError, ResolvedReferenceInfo};
14use tracing::instrument;
15
16use super::Expr;
17use super::builtins::FUNCTIONS;
18use super::parser::{ParseError, parse};
19use super::pattern::{Pattern, PatternError};
20
21#[derive(Debug)]
22pub(super) struct Context<'a> {
23 pub effects: &'a Effects,
24 pub repo: &'a Repo,
25 pub dag: &'a mut Dag,
26}
27
28#[derive(Debug, Error)]
29pub enum EvalError {
30 #[error("no commit, branch, or reference with the name '{name}' could be found")]
31 UnboundName { name: String },
32
33 #[error(
34 "no function with the name '{name}' could be found; these functions are available: {}",
35 available_names.join(", "),
36 )]
37 UnboundFunction {
38 name: String,
39 available_names: Vec<&'static str>,
40 },
41
42 #[error(
43 "invalid number of arguments to {function_name}: expected {} but got {actual_arity}",
44 expected_arities.iter().map(|arity| arity.to_string()).collect::<Vec<_>>().join("/"),
45 )]
46 ArityMismatch {
47 function_name: String,
48 expected_arities: Vec<usize>,
49 actual_arity: usize,
50 },
51
52 #[error(
53 "expected '{expr}' to evaluate to {}, but got {actual_len}",
54 Pluralize {
55 determiner: None,
56 amount: *expected_len,
57 unit: ("element", "elements"),
58 }
59 )]
60 UnexpectedSetLength {
61 expr: String,
62 expected_len: usize,
63 actual_len: usize,
64 },
65
66 #[error("failed to parse alias expression '{alias}'\n{source}")]
67 ParseAlias { alias: String, source: ParseError },
68
69 #[error("not an integer: {from}")]
70 ParseInt {
71 #[from]
72 from: ParseIntError,
73 },
74
75 #[error("expected an integer, but got a call to function: {function_name}")]
76 ExpectedNumberNotFunction { function_name: String },
77
78 #[error("expected a text-matching pattern, but got a call to function: {function_name}")]
79 ExpectedPatternNotFunction { function_name: String },
80
81 #[error("there was no latest command run with `git test`; try running `git test` first")]
82 NoLatestTestCommand,
83
84 #[error(transparent)]
85 PatternError(#[from] PatternError),
86
87 #[error(transparent)]
88 RepoError(#[from] RepoError),
89
90 #[error("query error: {from}")]
91 DagError {
92 #[from]
93 from: eden_dag::Error,
94 },
95
96 #[error(transparent)]
97 OtherError(eyre::Error),
98}
99
100pub(super) fn make_dag_backend_error(error: impl Display) -> eden_dag::Error {
101 let error = format!("error: {error}");
102 let error = BackendError::Generic(error);
103 eden_dag::Error::Backend(Box::new(error))
104}
105
106pub type EvalResult = Result<CommitSet, EvalError>;
107
108#[instrument]
110pub fn eval(effects: &Effects, repo: &Repo, dag: &mut Dag, expr: &Expr) -> EvalResult {
111 let (effects, _progress) =
112 effects.start_operation(OperationType::EvaluateRevset(Arc::new(expr.to_string())));
113
114 let mut ctx = Context {
115 effects: &effects,
116 repo,
117 dag,
118 };
119 let commits = eval_inner(&mut ctx, expr, &None)?;
120 Ok(commits)
121}
122
123#[instrument]
124fn eval_inner(ctx: &mut Context, expr: &Expr, commitset_hint: &Option<&CommitSet>) -> EvalResult {
125 match expr {
126 Expr::Name(name) => eval_name(ctx, name),
127 Expr::FunctionCall(name, args) => {
128 let result = eval_fn(ctx, name, args, commitset_hint)?;
129 let result = ctx
130 .dag
131 .filter_visible_commits(result)
132 .map_err(EvalError::OtherError)?;
133 Ok(result)
134 }
135 }
136}
137
138#[instrument]
139pub(super) fn eval_name(ctx: &mut Context, name: &str) -> EvalResult {
140 if name == "." || name == "@" {
141 let head_info = ctx.repo.get_head_info()?;
142 return match head_info {
143 ResolvedReferenceInfo {
144 oid: Some(oid),
145 reference_name: _,
146 } => Ok(oid.into()),
147 ResolvedReferenceInfo {
148 oid: None,
149 reference_name: _,
150 } => Ok(CommitSet::empty()),
151 };
152 }
153
154 let commit = ctx.repo.revparse_single_commit(name);
155 let commit_set = match commit {
156 Ok(Some(commit)) => {
157 let commit_set: CommitSet = commit.get_oid().into();
158 commit_set
159 }
160 Ok(None) | Err(_) => {
161 return Err(EvalError::UnboundName {
162 name: name.to_owned(),
163 });
164 }
165 };
166
167 ctx.dag
168 .sync_from_oids(
169 ctx.effects,
170 ctx.repo,
171 CommitSet::empty(),
172 commit_set.clone(),
173 )
174 .map_err(EvalError::OtherError)?;
175 Ok(commit_set)
176}
177
178#[instrument]
179pub(super) fn eval_fn(
180 ctx: &mut Context,
181 name: &str,
182 args: &[Expr],
183 commitset_hint: &Option<&CommitSet>,
184) -> EvalResult {
185 if let Some(function) = FUNCTIONS.get(name) {
186 return function(ctx, name, args, commitset_hint);
187 }
188
189 let alias_key = format!("branchless.revsets.alias.{name}");
190 let alias_template: Option<String> = ctx
191 .repo
192 .get_readonly_config()
193 .map_err(EvalError::RepoError)?
194 .get(alias_key)
195 .map_err(EvalError::OtherError)?;
196 if let Some(alias_template) = alias_template {
197 let alias_expr = parse(&alias_template).map_err(|err| EvalError::ParseAlias {
198 alias: alias_template.clone(),
199 source: err,
200 })?;
201 let arg_map: HashMap<String, Expr> = args
202 .iter()
203 .enumerate()
204 .map(|(i, arg)| (format!("${}", i + 1), arg.clone()))
205 .collect();
206 let alias_expr = alias_expr.replace_names(&arg_map);
207 let commits = eval_inner(ctx, &alias_expr, commitset_hint)?;
208 return Ok(commits);
209 }
210
211 Err(EvalError::UnboundFunction {
212 name: name.to_owned(),
213 available_names: FUNCTIONS.keys().sorted().copied().collect(),
214 })
215}
216
217#[instrument]
218pub(super) fn eval0(
219 ctx: &mut Context,
220 function_name: &str,
221 args: &[Expr],
222) -> Result<(), EvalError> {
223 match args {
224 [] => Ok(()),
225
226 args => Err(EvalError::ArityMismatch {
227 function_name: function_name.to_string(),
228 expected_arities: vec![0],
229 actual_arity: args.len(),
230 }),
231 }
232}
233
234#[instrument]
235pub(super) fn eval0_or_1(
236 ctx: &mut Context,
237 function_name: &str,
238 args: &[Expr],
239) -> Result<Option<CommitSet>, EvalError> {
240 match args {
241 [] => Ok(None),
242 [expr] => {
243 let arg = eval_inner(ctx, expr, &None)?;
244 Ok(Some(arg))
245 }
246 args => Err(EvalError::ArityMismatch {
247 function_name: function_name.to_string(),
248 expected_arities: vec![0, 1],
249 actual_arity: args.len(),
250 }),
251 }
252}
253
254#[instrument]
255pub(super) fn eval1(ctx: &mut Context, function_name: &str, args: &[Expr]) -> EvalResult {
256 match args {
257 [arg] => {
258 let lhs = eval_inner(ctx, arg, &None)?;
259 Ok(lhs)
260 }
261
262 args => Err(EvalError::ArityMismatch {
263 function_name: function_name.to_string(),
264 expected_arities: vec![1],
265 actual_arity: args.len(),
266 }),
267 }
268}
269
270#[instrument]
271pub(super) fn eval0_or_1_pattern(
272 ctx: &mut Context,
273 function_name: &str,
274 args: &[Expr],
275) -> Result<Option<Pattern>, EvalError> {
276 match args {
277 [] => Ok(None),
278 [_] => eval1_pattern(ctx, function_name, args).map(Some),
279 args => Err(EvalError::ArityMismatch {
280 function_name: function_name.to_string(),
281 expected_arities: vec![0, 1],
282 actual_arity: args.len(),
283 }),
284 }
285}
286
287#[instrument]
288pub(super) fn eval1_pattern(
289 _ctx: &mut Context,
290 function_name: &str,
291 args: &[Expr],
292) -> Result<Pattern, EvalError> {
293 match args {
294 [Expr::Name(pattern)] => Ok(Pattern::new(pattern)?),
295
296 [Expr::FunctionCall(name, _args)] => Err(EvalError::ExpectedPatternNotFunction {
297 function_name: name.clone().into_owned(),
298 }),
299
300 args => Err(EvalError::ArityMismatch {
301 function_name: function_name.to_string(),
302 expected_arities: vec![1],
303 actual_arity: args.len(),
304 }),
305 }
306}
307
308#[instrument]
309pub(super) fn eval2(
310 ctx: &mut Context,
311 function_name: &str,
312 args: &[Expr],
313 limit_rhs: bool,
314) -> Result<(CommitSet, CommitSet), EvalError> {
315 match args {
316 [lhs, rhs] => {
317 let lhs = eval_inner(ctx, lhs, &None)?;
318 let rhs = eval_inner(ctx, rhs, &limit_rhs.then_some(&lhs))?;
319 Ok((lhs, rhs))
320 }
321
322 args => Err(EvalError::ArityMismatch {
323 function_name: function_name.to_string(),
324 expected_arities: vec![2],
325 actual_arity: args.len(),
326 }),
327 }
328}
329
330#[instrument]
331pub(super) fn eval_number_rhs(
332 ctx: &mut Context,
333 function_name: &str,
334 args: &[Expr],
335) -> Result<(CommitSet, usize), EvalError> {
336 match args {
337 [lhs, Expr::Name(name)] => {
338 let lhs = eval_inner(ctx, lhs, &None)?;
339 let number: usize = { name.parse()? };
340 Ok((lhs, number))
341 }
342
343 [_lhs, Expr::FunctionCall(name, _args)] => Err(EvalError::ExpectedNumberNotFunction {
344 function_name: name.clone().into_owned(),
345 }),
346
347 args => Err(EvalError::ArityMismatch {
348 function_name: function_name.to_string(),
349 expected_arities: vec![2],
350 actual_arity: args.len(),
351 }),
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use std::borrow::Cow;
358
359 use lib::core::eventlog::{EventLogDb, EventReplayer};
360 use lib::core::formatting::Glyphs;
361 use lib::core::repo_ext::RepoExt;
362 use lib::git::Commit;
363 use lib::testing::{GitRunOptions, make_git};
364
365 use super::*;
366
367 fn eval_and_sort<'a>(
368 effects: &Effects,
369 repo: &'a Repo,
370 dag: &mut Dag,
371 expr: &Expr,
372 ) -> eyre::Result<Vec<Commit<'a>>> {
373 let result = eval(effects, repo, dag, expr)?;
374 let mut commits: Vec<Commit> = dag
375 .commit_set_to_vec(&result)?
376 .into_iter()
377 .map(|oid| repo.find_commit_or_fail(oid))
378 .try_collect()?;
379 commits.sort_by_key(|commit| (commit.get_message_pretty(), commit.get_time()));
380 Ok(commits)
381 }
382
383 #[test]
384 fn test_eval() -> eyre::Result<()> {
385 let git = make_git()?;
386 git.init_repo()?;
387
388 let test1_oid = git.commit_file("test1", 1)?;
389 git.detach_head()?;
390 let test2_oid = git.commit_file("test2", 2)?;
391 let _test3_oid = git.commit_file("test3", 3)?;
392
393 git.run(&["checkout", "master"])?;
394 git.commit_file("test4", 4)?;
395 git.detach_head()?;
396 git.commit_file("test5", 5)?;
397 git.commit_file("test6", 6)?;
398 git.run(&["checkout", "HEAD~"])?;
399 let test7_oid = git.commit_file("test7", 7)?;
400
401 let effects = Effects::new_suppress_for_test(Glyphs::text());
402 let repo = git.get_repo()?;
403 let conn = repo.get_db_conn()?;
404 let event_log_db = EventLogDb::new(&conn)?;
405 let event_replayer = EventReplayer::from_event_log_db(&effects, &repo, &event_log_db)?;
406 let event_cursor = event_replayer.make_default_cursor();
407 let references_snapshot = repo.get_references_snapshot()?;
408 let mut dag = Dag::open_and_sync(
409 &effects,
410 &repo,
411 &event_replayer,
412 event_cursor,
413 &references_snapshot,
414 )?;
415
416 {
417 let expr = Expr::FunctionCall(Cow::Borrowed("all"), vec![]);
418 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
419 Ok(
420 [
421 Commit {
422 inner: Commit {
423 id: f777ecc9b0db5ed372b2615695191a8a17f79f24,
424 summary: "create initial.txt",
425 },
426 },
427 Commit {
428 inner: Commit {
429 id: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e,
430 summary: "create test1.txt",
431 },
432 },
433 Commit {
434 inner: Commit {
435 id: 96d1c37a3d4363611c49f7e52186e189a04c531f,
436 summary: "create test2.txt",
437 },
438 },
439 Commit {
440 inner: Commit {
441 id: 70deb1e28791d8e7dd5a1f0c871a51b91282562f,
442 summary: "create test3.txt",
443 },
444 },
445 Commit {
446 inner: Commit {
447 id: bf0d52a607f693201512a43b6b5a70b2a275e0ad,
448 summary: "create test4.txt",
449 },
450 },
451 Commit {
452 inner: Commit {
453 id: 848121cb21bf9af8b064c91bc8930bd16d624a22,
454 summary: "create test5.txt",
455 },
456 },
457 Commit {
458 inner: Commit {
459 id: f0abf649939928fe5475179fd84e738d3d3725dc,
460 summary: "create test6.txt",
461 },
462 },
463 Commit {
464 inner: Commit {
465 id: ba07500a4adc661dc06a748d200ef92120e1b355,
466 summary: "create test7.txt",
467 },
468 },
469 ],
470 )
471 "###);
472 }
473
474 {
475 let expr = Expr::FunctionCall(Cow::Borrowed("none"), vec![]);
476 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
477 Ok(
478 [],
479 )
480 "###);
481 }
482
483 {
484 let expr = Expr::FunctionCall(
485 Cow::Borrowed("union"),
486 vec![
487 Expr::Name(Cow::Owned(test1_oid.to_string())),
488 Expr::Name(Cow::Owned(test2_oid.to_string())),
489 ],
490 );
491 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
492 Ok(
493 [
494 Commit {
495 inner: Commit {
496 id: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e,
497 summary: "create test1.txt",
498 },
499 },
500 Commit {
501 inner: Commit {
502 id: 96d1c37a3d4363611c49f7e52186e189a04c531f,
503 summary: "create test2.txt",
504 },
505 },
506 ],
507 )
508 "###);
509 }
510
511 {
512 let expr = Expr::FunctionCall(
513 Cow::Borrowed("intersection"),
514 vec![
515 Expr::Name(Cow::Owned(test1_oid.to_string())),
516 Expr::Name(Cow::Owned(test2_oid.to_string())),
517 ],
518 );
519 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
520 Ok(
521 [],
522 )
523 "###);
524
525 let expr = Expr::FunctionCall(
526 Cow::Borrowed("intersection"),
527 vec![
528 Expr::Name(Cow::Owned(test1_oid.to_string())),
529 Expr::Name(Cow::Owned(test1_oid.to_string())),
530 ],
531 );
532 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
533 Ok(
534 [
535 Commit {
536 inner: Commit {
537 id: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e,
538 summary: "create test1.txt",
539 },
540 },
541 ],
542 )
543 "###);
544 }
545
546 {
547 let expr = Expr::FunctionCall(
548 Cow::Borrowed("difference"),
549 vec![
550 Expr::Name(Cow::Owned(test1_oid.to_string())),
551 Expr::Name(Cow::Owned(test2_oid.to_string())),
552 ],
553 );
554 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
555 Ok(
556 [
557 Commit {
558 inner: Commit {
559 id: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e,
560 summary: "create test1.txt",
561 },
562 },
563 ],
564 )
565 "###);
566
567 let expr = Expr::FunctionCall(
568 Cow::Borrowed("difference"),
569 vec![
570 Expr::Name(Cow::Owned(test1_oid.to_string())),
571 Expr::Name(Cow::Owned(test1_oid.to_string())),
572 ],
573 );
574 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
575 Ok(
576 [],
577 )
578 "###);
579 }
580
581 {
582 let expr = Expr::FunctionCall(
583 Cow::Borrowed("siblings"),
584 vec![Expr::Name(Cow::Owned(test2_oid.to_string()))],
585 );
586 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
587 Ok(
588 [
589 Commit {
590 inner: Commit {
591 id: bf0d52a607f693201512a43b6b5a70b2a275e0ad,
592 summary: "create test4.txt",
593 },
594 },
595 ],
596 )
597 "###);
598 }
599
600 {
601 let expr = Expr::FunctionCall(Cow::Borrowed("stack"), vec![]);
602 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
603 Ok(
604 [
605 Commit {
606 inner: Commit {
607 id: 848121cb21bf9af8b064c91bc8930bd16d624a22,
608 summary: "create test5.txt",
609 },
610 },
611 Commit {
612 inner: Commit {
613 id: f0abf649939928fe5475179fd84e738d3d3725dc,
614 summary: "create test6.txt",
615 },
616 },
617 Commit {
618 inner: Commit {
619 id: ba07500a4adc661dc06a748d200ef92120e1b355,
620 summary: "create test7.txt",
621 },
622 },
623 ],
624 )
625 "###);
626 }
627
628 {
629 let expr = Expr::FunctionCall(Cow::Borrowed("main"), vec![]);
630 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
631 Ok(
632 [
633 Commit {
634 inner: Commit {
635 id: bf0d52a607f693201512a43b6b5a70b2a275e0ad,
636 summary: "create test4.txt",
637 },
638 },
639 ],
640 )
641 "###);
642 }
643
644 {
645 let expr = Expr::FunctionCall(Cow::Borrowed("public"), vec![]);
646 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
647 Ok(
648 [
649 Commit {
650 inner: Commit {
651 id: f777ecc9b0db5ed372b2615695191a8a17f79f24,
652 summary: "create initial.txt",
653 },
654 },
655 Commit {
656 inner: Commit {
657 id: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e,
658 summary: "create test1.txt",
659 },
660 },
661 Commit {
662 inner: Commit {
663 id: bf0d52a607f693201512a43b6b5a70b2a275e0ad,
664 summary: "create test4.txt",
665 },
666 },
667 ],
668 )
669 "###);
670 }
671
672 {
673 let expr = Expr::FunctionCall(
674 Cow::Borrowed("stack"),
675 vec![Expr::Name(Cow::Owned(test2_oid.to_string()))],
676 );
677 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
678 Ok(
679 [
680 Commit {
681 inner: Commit {
682 id: 96d1c37a3d4363611c49f7e52186e189a04c531f,
683 summary: "create test2.txt",
684 },
685 },
686 Commit {
687 inner: Commit {
688 id: 70deb1e28791d8e7dd5a1f0c871a51b91282562f,
689 summary: "create test3.txt",
690 },
691 },
692 ],
693 )
694 "###);
695 }
696
697 {
698 let expr = Expr::FunctionCall(Cow::Borrowed("draft"), vec![]);
699 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
700 Ok(
701 [
702 Commit {
703 inner: Commit {
704 id: 96d1c37a3d4363611c49f7e52186e189a04c531f,
705 summary: "create test2.txt",
706 },
707 },
708 Commit {
709 inner: Commit {
710 id: 70deb1e28791d8e7dd5a1f0c871a51b91282562f,
711 summary: "create test3.txt",
712 },
713 },
714 Commit {
715 inner: Commit {
716 id: 848121cb21bf9af8b064c91bc8930bd16d624a22,
717 summary: "create test5.txt",
718 },
719 },
720 Commit {
721 inner: Commit {
722 id: f0abf649939928fe5475179fd84e738d3d3725dc,
723 summary: "create test6.txt",
724 },
725 },
726 Commit {
727 inner: Commit {
728 id: ba07500a4adc661dc06a748d200ef92120e1b355,
729 summary: "create test7.txt",
730 },
731 },
732 ],
733 )
734 "###);
735 }
736
737 {
738 let expr = Expr::FunctionCall(
739 Cow::Borrowed("not"),
740 vec![Expr::FunctionCall(Cow::Borrowed("draft"), vec![])],
741 );
742 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
743 Ok(
744 [
745 Commit {
746 inner: Commit {
747 id: f777ecc9b0db5ed372b2615695191a8a17f79f24,
748 summary: "create initial.txt",
749 },
750 },
751 Commit {
752 inner: Commit {
753 id: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e,
754 summary: "create test1.txt",
755 },
756 },
757 Commit {
758 inner: Commit {
759 id: bf0d52a607f693201512a43b6b5a70b2a275e0ad,
760 summary: "create test4.txt",
761 },
762 },
763 ],
764 )
765 "###);
766 }
767
768 {
769 let expr = Expr::FunctionCall(
770 Cow::Borrowed("parents.nth"),
771 vec![
772 Expr::Name(Cow::Owned(test7_oid.to_string())),
773 Expr::Name(Cow::Borrowed("1")),
774 ],
775 );
776 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
777 Ok(
778 [
779 Commit {
780 inner: Commit {
781 id: 848121cb21bf9af8b064c91bc8930bd16d624a22,
782 summary: "create test5.txt",
783 },
784 },
785 ],
786 )
787 "###);
788 }
789
790 {
791 let expr = Expr::FunctionCall(
792 Cow::Borrowed("ancestors.nth"),
793 vec![
794 Expr::Name(Cow::Owned(test7_oid.to_string())),
795 Expr::Name(Cow::Borrowed("2")),
796 ],
797 );
798 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
799 Ok(
800 [
801 Commit {
802 inner: Commit {
803 id: bf0d52a607f693201512a43b6b5a70b2a275e0ad,
804 summary: "create test4.txt",
805 },
806 },
807 ],
808 )
809 "###);
810 }
811
812 {
813 let expr = Expr::FunctionCall(
814 Cow::Borrowed("message"),
815 vec![Expr::Name(Cow::Borrowed("test4"))],
816 );
817 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
818 Ok(
819 [
820 Commit {
821 inner: Commit {
822 id: bf0d52a607f693201512a43b6b5a70b2a275e0ad,
823 summary: "create test4.txt",
824 },
825 },
826 ],
827 )
828 "###);
829 }
830
831 {
832 let expr = Expr::FunctionCall(
833 Cow::Borrowed("message"),
834 vec![Expr::Name(Cow::Borrowed("exact:create test4.txt"))],
835 );
836 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
837 Ok(
838 [
839 Commit {
840 inner: Commit {
841 id: bf0d52a607f693201512a43b6b5a70b2a275e0ad,
842 summary: "create test4.txt",
843 },
844 },
845 ],
846 )
847 "###);
848 }
849
850 {
851 let expr = Expr::FunctionCall(
852 Cow::Borrowed("message"),
853 vec![Expr::Name(Cow::Borrowed("regex:^create test4.txt$"))],
854 );
855 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
856 Ok(
857 [
858 Commit {
859 inner: Commit {
860 id: bf0d52a607f693201512a43b6b5a70b2a275e0ad,
861 summary: "create test4.txt",
862 },
863 },
864 ],
865 )
866 "###);
867 }
868
869 {
870 let expr = Expr::FunctionCall(
871 Cow::Borrowed("paths.changed"),
872 vec![Expr::Name(Cow::Borrowed("glob:test[1-3].txt"))],
873 );
874 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
875 Ok(
876 [
877 Commit {
878 inner: Commit {
879 id: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e,
880 summary: "create test1.txt",
881 },
882 },
883 Commit {
884 inner: Commit {
885 id: 96d1c37a3d4363611c49f7e52186e189a04c531f,
886 summary: "create test2.txt",
887 },
888 },
889 Commit {
890 inner: Commit {
891 id: 70deb1e28791d8e7dd5a1f0c871a51b91282562f,
892 summary: "create test3.txt",
893 },
894 },
895 ],
896 )
897 "###);
898 }
899
900 {
901 let expr = Expr::FunctionCall(
902 Cow::Borrowed("exactly"),
903 vec![
904 Expr::FunctionCall(Cow::Borrowed("stack"), vec![]),
905 Expr::Name(Cow::Borrowed("3")),
906 ],
907 );
908 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
909 Ok(
910 [
911 Commit {
912 inner: Commit {
913 id: 848121cb21bf9af8b064c91bc8930bd16d624a22,
914 summary: "create test5.txt",
915 },
916 },
917 Commit {
918 inner: Commit {
919 id: f0abf649939928fe5475179fd84e738d3d3725dc,
920 summary: "create test6.txt",
921 },
922 },
923 Commit {
924 inner: Commit {
925 id: ba07500a4adc661dc06a748d200ef92120e1b355,
926 summary: "create test7.txt",
927 },
928 },
929 ],
930 )
931 "###);
932 }
933
934 {
935 let expr = Expr::FunctionCall(
936 Cow::Borrowed("exactly"),
937 vec![
938 Expr::FunctionCall(Cow::Borrowed("stack"), vec![]),
939 Expr::Name(Cow::Borrowed("2")),
940 ],
941 );
942 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
943 Err(
944 UnexpectedSetLength {
945 expr: "stack()",
946 expected_len: 2,
947 actual_len: 3,
948 },
949 )
950 "###);
951 }
952
953 Ok(())
954 }
955
956 #[test]
957 fn test_eval_author_committer() -> eyre::Result<()> {
958 let git = make_git()?;
959 git.init_repo()?;
960
961 git.write_file_txt("test1", "test\n")?;
962 git.run(&["add", "test1.txt"])?;
963 git.run_with_options(
964 &["commit", "-m", "test1"],
965 &GitRunOptions {
966 env: {
967 [
968 ("GIT_AUTHOR_NAME", "Foo"),
969 ("GIT_AUTHOR_EMAIL", "foo@example.com"),
970 ("GIT_COMMITTER_NAME", "Bar"),
971 ("GIT_COMMITTER_EMAIL", "bar@example.com"),
972 ]
973 .iter()
974 .map(|(k, v)| (k.to_string(), v.to_string()))
975 .collect()
976 },
977 ..Default::default()
978 },
979 )?;
980
981 git.write_file_txt("test2", "test\n")?;
982 git.run(&["add", "test2.txt"])?;
983 git.run_with_options(
984 &["commit", "-m", "test2"],
985 &GitRunOptions {
986 env: {
987 [
988 ("GIT_AUTHOR_NAME", "Bar"),
989 ("GIT_AUTHOR_EMAIL", "bar@example.com"),
990 ("GIT_COMMITTER_NAME", "Foo"),
991 ("GIT_COMMITTER_EMAIL", "foo@example.com"),
992 ]
993 .iter()
994 .map(|(k, v)| (k.to_string(), v.to_string()))
995 .collect()
996 },
997 ..Default::default()
998 },
999 )?;
1000
1001 let effects = Effects::new_suppress_for_test(Glyphs::text());
1002 let repo = git.get_repo()?;
1003 let conn = repo.get_db_conn()?;
1004 let event_log_db = EventLogDb::new(&conn)?;
1005 let event_replayer = EventReplayer::from_event_log_db(&effects, &repo, &event_log_db)?;
1006 let event_cursor = event_replayer.make_default_cursor();
1007 let references_snapshot = repo.get_references_snapshot()?;
1008 let mut dag = Dag::open_and_sync(
1009 &effects,
1010 &repo,
1011 &event_replayer,
1012 event_cursor,
1013 &references_snapshot,
1014 )?;
1015
1016 {
1017 let expr = Expr::FunctionCall(
1018 Cow::Borrowed("author.name"),
1019 vec![Expr::Name(Cow::Borrowed("Foo"))],
1020 );
1021 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1022 Ok(
1023 [
1024 Commit {
1025 inner: Commit {
1026 id: 9ee1994c0737c221efc07acd8d73590d336ee46d,
1027 summary: "test1",
1028 },
1029 },
1030 ],
1031 )
1032 "###);
1033 }
1034
1035 {
1036 let expr = Expr::FunctionCall(
1037 Cow::Borrowed("author.email"),
1038 vec![Expr::Name(Cow::Borrowed("foo"))],
1039 );
1040 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1041 Ok(
1042 [
1043 Commit {
1044 inner: Commit {
1045 id: 9ee1994c0737c221efc07acd8d73590d336ee46d,
1046 summary: "test1",
1047 },
1048 },
1049 ],
1050 )
1051 "###);
1052 }
1053
1054 {
1055 let expr = Expr::FunctionCall(
1056 Cow::Borrowed("author.date"),
1057 vec![Expr::Name(Cow::Borrowed("before:today"))],
1058 );
1059 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1060 Ok(
1061 [
1062 Commit {
1063 inner: Commit {
1064 id: f777ecc9b0db5ed372b2615695191a8a17f79f24,
1065 summary: "create initial.txt",
1066 },
1067 },
1068 Commit {
1069 inner: Commit {
1070 id: 9ee1994c0737c221efc07acd8d73590d336ee46d,
1071 summary: "test1",
1072 },
1073 },
1074 Commit {
1075 inner: Commit {
1076 id: 05ff2fc6b3e7917ac6800b18077c211e173e8fb4,
1077 summary: "test2",
1078 },
1079 },
1080 ],
1081 )
1082 "###);
1083 }
1084
1085 {
1086 let expr = Expr::FunctionCall(
1087 Cow::Borrowed("author.date"),
1088 vec![Expr::Name(Cow::Borrowed("after:yesterday"))],
1089 );
1090 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1091 Ok(
1092 [],
1093 )
1094 "###);
1095 }
1096
1097 {
1098 let expr = Expr::FunctionCall(
1099 Cow::Borrowed("committer.name"),
1100 vec![Expr::Name(Cow::Borrowed("Foo"))],
1101 );
1102 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1103 Ok(
1104 [
1105 Commit {
1106 inner: Commit {
1107 id: 05ff2fc6b3e7917ac6800b18077c211e173e8fb4,
1108 summary: "test2",
1109 },
1110 },
1111 ],
1112 )
1113 "###);
1114 }
1115
1116 {
1117 let expr = Expr::FunctionCall(
1118 Cow::Borrowed("committer.email"),
1119 vec![Expr::Name(Cow::Borrowed("foo"))],
1120 );
1121 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1122 Ok(
1123 [
1124 Commit {
1125 inner: Commit {
1126 id: 05ff2fc6b3e7917ac6800b18077c211e173e8fb4,
1127 summary: "test2",
1128 },
1129 },
1130 ],
1131 )
1132 "###);
1133 }
1134
1135 {
1136 let expr = Expr::FunctionCall(
1137 Cow::Borrowed("committer.date"),
1138 vec![Expr::Name(Cow::Borrowed("before:today"))],
1139 );
1140 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1141 Ok(
1142 [
1143 Commit {
1144 inner: Commit {
1145 id: f777ecc9b0db5ed372b2615695191a8a17f79f24,
1146 summary: "create initial.txt",
1147 },
1148 },
1149 Commit {
1150 inner: Commit {
1151 id: 9ee1994c0737c221efc07acd8d73590d336ee46d,
1152 summary: "test1",
1153 },
1154 },
1155 Commit {
1156 inner: Commit {
1157 id: 05ff2fc6b3e7917ac6800b18077c211e173e8fb4,
1158 summary: "test2",
1159 },
1160 },
1161 ],
1162 )
1163 "###);
1164 }
1165
1166 {
1167 let expr = Expr::FunctionCall(
1168 Cow::Borrowed("committer.date"),
1169 vec![Expr::Name(Cow::Borrowed("after:yesterday"))],
1170 );
1171 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1172 Ok(
1173 [],
1174 )
1175 "###);
1176 }
1177
1178 Ok(())
1179 }
1180
1181 #[test]
1182 fn test_eval_current() -> eyre::Result<()> {
1183 let git = make_git()?;
1184 git.init_repo()?;
1185
1186 git.detach_head()?;
1187 let test1_oid = git.commit_file("test1", 1)?;
1188 let _test2_oid = git.commit_file("test2", 2)?;
1189 let test3_oid = git.commit_file("test3", 3)?;
1190 let test4_oid = git.commit_file("test4", 4)?;
1191
1192 git.branchless(
1193 "move",
1194 &["-s", &test3_oid.to_string(), "-d", &test1_oid.to_string()],
1195 )?;
1196 git.branchless("reword", &["-m", "test4 has been rewritten twice"])?;
1197
1198 let effects = Effects::new_suppress_for_test(Glyphs::text());
1199 let repo = git.get_repo()?;
1200 let conn = repo.get_db_conn()?;
1201 let event_log_db = EventLogDb::new(&conn)?;
1202 let event_replayer = EventReplayer::from_event_log_db(&effects, &repo, &event_log_db)?;
1203 let event_cursor = event_replayer.make_default_cursor();
1204 let references_snapshot = repo.get_references_snapshot()?;
1205 let mut dag = Dag::open_and_sync(
1206 &effects,
1207 &repo,
1208 &event_replayer,
1209 event_cursor,
1210 &references_snapshot,
1211 )?;
1212
1213 {
1214 let original_test3_oid = &test3_oid.to_string();
1215 let original_test4_oid = &test4_oid.to_string();
1216
1217 let expr = Expr::FunctionCall(
1218 Cow::Borrowed("union"),
1219 vec![
1220 Expr::Name(Cow::Borrowed(original_test3_oid)),
1221 Expr::Name(Cow::Borrowed(original_test4_oid)),
1222 ],
1223 );
1224 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1225 Ok(
1226 [],
1227 )
1228 "###);
1229
1230 let expr = Expr::FunctionCall(
1231 Cow::Borrowed("current"),
1232 vec![Expr::FunctionCall(
1233 Cow::Borrowed("union"),
1234 vec![
1235 Expr::Name(Cow::Borrowed(original_test3_oid)),
1236 Expr::Name(Cow::Borrowed(original_test4_oid)),
1237 ],
1238 )],
1239 );
1240 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1241 Ok(
1242 [
1243 Commit {
1244 inner: Commit {
1245 id: 4838e49b08954becdd17c0900c1179c2c654c627,
1246 summary: "create test3.txt",
1247 },
1248 },
1249 Commit {
1250 inner: Commit {
1251 id: 619162078182d2c6d80ff604b81e7c2afc3295b7,
1252 summary: "test4 has been rewritten twice",
1253 },
1254 },
1255 ],
1256 )
1257 "###);
1258 }
1259 Ok(())
1260 }
1261
1262 #[test]
1263 fn test_eval_merges() -> eyre::Result<()> {
1264 let git = make_git()?;
1265 git.init_repo()?;
1266
1267 git.detach_head()?;
1268 let test1_oid = git.commit_file("test1", 1)?;
1269 let test2_oid = git.commit_file("test2", 2)?;
1270 git.run(&["checkout", "HEAD~2"])?;
1271 git.run(&["merge", "--no-ff", &test1_oid.to_string()])?;
1272 git.run(&["merge", "--no-ff", &test2_oid.to_string()])?;
1273
1274 let effects = Effects::new_suppress_for_test(Glyphs::text());
1275 let repo = git.get_repo()?;
1276 let conn = repo.get_db_conn()?;
1277 let event_log_db = EventLogDb::new(&conn)?;
1278 let event_replayer = EventReplayer::from_event_log_db(&effects, &repo, &event_log_db)?;
1279 let event_cursor = event_replayer.make_default_cursor();
1280 let references_snapshot = repo.get_references_snapshot()?;
1281 let mut dag = Dag::open_and_sync(
1282 &effects,
1283 &repo,
1284 &event_replayer,
1285 event_cursor,
1286 &references_snapshot,
1287 )?;
1288
1289 {
1290 let expr = Expr::FunctionCall(Cow::Borrowed("merges"), vec![]);
1291 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1292 Ok(
1293 [
1294 Commit {
1295 inner: Commit {
1296 id: f486a8b756cd3a928241576aa87827284f3e14d1,
1297 summary: "Merge commit '62fc20d2a290daea0d52bdc2ed2ad4be6491010e' into HEAD",
1298 },
1299 },
1300 Commit {
1301 inner: Commit {
1302 id: 0b75bdca271fdc188e68bca6e054013bbc2a373c,
1303 summary: "Merge commit '96d1c37a3d4363611c49f7e52186e189a04c531f' into HEAD",
1304 },
1305 },
1306 ],
1307 )
1308 "###);
1309
1310 let expr = Expr::FunctionCall(
1311 Cow::Borrowed("not"),
1312 vec![Expr::FunctionCall(Cow::Borrowed("merges"), vec![])],
1313 );
1314 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1315 Ok(
1316 [
1317 Commit {
1318 inner: Commit {
1319 id: f777ecc9b0db5ed372b2615695191a8a17f79f24,
1320 summary: "create initial.txt",
1321 },
1322 },
1323 Commit {
1324 inner: Commit {
1325 id: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e,
1326 summary: "create test1.txt",
1327 },
1328 },
1329 Commit {
1330 inner: Commit {
1331 id: 96d1c37a3d4363611c49f7e52186e189a04c531f,
1332 summary: "create test2.txt",
1333 },
1334 },
1335 ],
1336 )
1337 "###);
1338 }
1339 Ok(())
1340 }
1341
1342 #[test]
1343 fn test_eval_branches_with_pattern() -> eyre::Result<()> {
1344 let git = make_git()?;
1345 git.init_repo()?;
1346
1347 git.detach_head()?;
1348 git.commit_file("test1", 1)?;
1349 git.run(&["branch", "test-1"])?;
1350 git.commit_file("test2", 2)?;
1351 git.run(&["branch", "test-2"])?;
1352
1353 let effects = Effects::new_suppress_for_test(Glyphs::text());
1354 let repo = git.get_repo()?;
1355 let conn = repo.get_db_conn()?;
1356 let event_log_db = EventLogDb::new(&conn)?;
1357 let event_replayer = EventReplayer::from_event_log_db(&effects, &repo, &event_log_db)?;
1358 let event_cursor = event_replayer.make_default_cursor();
1359 let references_snapshot = repo.get_references_snapshot()?;
1360 let mut dag = Dag::open_and_sync(
1361 &effects,
1362 &repo,
1363 &event_replayer,
1364 event_cursor,
1365 &references_snapshot,
1366 )?;
1367
1368 {
1369 let expr = Expr::FunctionCall(Cow::Borrowed("branches"), vec![]);
1370 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1371 Ok(
1372 [
1373 Commit {
1374 inner: Commit {
1375 id: f777ecc9b0db5ed372b2615695191a8a17f79f24,
1376 summary: "create initial.txt",
1377 },
1378 },
1379 Commit {
1380 inner: Commit {
1381 id: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e,
1382 summary: "create test1.txt",
1383 },
1384 },
1385 Commit {
1386 inner: Commit {
1387 id: 96d1c37a3d4363611c49f7e52186e189a04c531f,
1388 summary: "create test2.txt",
1389 },
1390 },
1391 ],
1392 )
1393 "###);
1394
1395 let expr = Expr::FunctionCall(
1396 Cow::Borrowed("branches"),
1397 vec![Expr::Name(Cow::Borrowed("glob:test*"))],
1398 );
1399 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1400 Ok(
1401 [
1402 Commit {
1403 inner: Commit {
1404 id: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e,
1405 summary: "create test1.txt",
1406 },
1407 },
1408 Commit {
1409 inner: Commit {
1410 id: 96d1c37a3d4363611c49f7e52186e189a04c531f,
1411 summary: "create test2.txt",
1412 },
1413 },
1414 ],
1415 )
1416 "###);
1417 }
1418 Ok(())
1419 }
1420
1421 #[test]
1422 fn test_eval_aliases() -> eyre::Result<()> {
1423 let git = make_git()?;
1424 git.init_repo()?;
1425
1426 git.detach_head()?;
1427 let _test1_oid = git.commit_file("test1", 1)?;
1428 let _test2_oid = git.commit_file("test2", 2)?;
1429 let _test3_oid = git.commit_file("test3", 3)?;
1430
1431 let effects = Effects::new_suppress_for_test(Glyphs::text());
1432 let repo = git.get_repo()?;
1433 let conn = repo.get_db_conn()?;
1434 let event_log_db = EventLogDb::new(&conn)?;
1435 let event_replayer = EventReplayer::from_event_log_db(&effects, &repo, &event_log_db)?;
1436 let event_cursor = event_replayer.make_default_cursor();
1437 let references_snapshot = repo.get_references_snapshot()?;
1438 let mut dag = Dag::open_and_sync(
1439 &effects,
1440 &repo,
1441 &event_replayer,
1442 event_cursor,
1443 &references_snapshot,
1444 )?;
1445
1446 {
1447 git.run(&[
1448 "config",
1449 "branchless.revsets.alias.simpleAlias",
1450 "roots($1)",
1451 ])?;
1452
1453 let expr = Expr::FunctionCall(
1454 Cow::Borrowed("simpleAlias"),
1455 vec![Expr::FunctionCall(Cow::Borrowed("stack"), vec![])],
1456 );
1457 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1458 Ok(
1459 [
1460 Commit {
1461 inner: Commit {
1462 id: 62fc20d2a290daea0d52bdc2ed2ad4be6491010e,
1463 summary: "create test1.txt",
1464 },
1465 },
1466 ],
1467 )
1468 "###);
1469 }
1470
1471 {
1472 git.run(&[
1473 "config",
1474 "branchless.revsets.alias.complexAlias",
1475 "children($1) & parents($1)",
1476 ])?;
1477
1478 let expr = Expr::FunctionCall(
1479 Cow::Borrowed("complexAlias"),
1480 vec![Expr::FunctionCall(Cow::Borrowed("stack"), vec![])],
1481 );
1482 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1483 Ok(
1484 [
1485 Commit {
1486 inner: Commit {
1487 id: 96d1c37a3d4363611c49f7e52186e189a04c531f,
1488 summary: "create test2.txt",
1489 },
1490 },
1491 ],
1492 )
1493 "###);
1494 }
1495
1496 {
1497 git.run(&["config", "branchless.revsets.alias.parseError", "foo("])?;
1498
1499 let (_stdout, _stderr) = git.branchless_with_options(
1500 "query",
1501 &["parseError()"],
1502 &GitRunOptions {
1503 expected_exit_code: 1,
1504 ..Default::default()
1505 },
1506 )?;
1507 insta::assert_snapshot!(_stderr, @r###"
1508 Evaluation error for expression 'parseError()': failed to parse alias expression 'foo('
1509 parse error: Unrecognized EOF found at 4
1510 Expected one of a commit/branch/tag, a string literal, "(", ")", "..", ":" or "::"
1511 "###);
1512 }
1513
1514 {
1515 git.run(&[
1526 "config",
1527 "branchless.revsets.alias.outerAlias",
1528 "innerAlias($1)",
1529 ])?;
1530
1531 git.run(&[
1532 "config",
1533 "branchless.revsets.alias.innerAlias",
1534 "intersection($1, $2)",
1535 ])?;
1536
1537 let expr = Expr::FunctionCall(
1538 Cow::Borrowed("outerAlias"),
1539 vec![
1540 Expr::FunctionCall(Cow::Borrowed("stack"), vec![]),
1541 Expr::FunctionCall(Cow::Borrowed("nonsense"), vec![]),
1542 ],
1543 );
1544 insta::assert_debug_snapshot!(eval_and_sort(&effects, &repo, &mut dag, &expr), @r###"
1545 Err(
1546 UnboundName {
1547 name: "$2",
1548 },
1549 )
1550 "###);
1551 }
1552
1553 Ok(())
1554 }
1555}