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