git_branchless_revset/
eval.rs

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/// Evaluate the provided revset expression.
109#[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            // Check for macro hygiene: arguments from outer nested aliases
1440            // should not be available inside inner aliases.
1441            //
1442            // 1. User input: `outerAlias(a, b)` (2 arguments provided)
1443            // 2. Expands to: `innerAlias(a)` (only uses 1 arg)
1444            // 3. Expands to: `builtin(a, $2)` (uses 2 args)
1445            //
1446            // In this case, there is no $2 available for step 3, so we want to
1447            // ensure that $2 is not resolved from step 1 and that it instead
1448            // fails.
1449            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}