OXYGENHUB

Coverage / crates/oxy-core/src/repo/revset.rs 89.8%

593 of 660 lines run, 68 of 150 functions · run #253 on main · run missed

1
//! Evaluating revsets (docs/revset_query_language.md §4) over a repository.
2
//!
3
//! The graph is the change metadata the state index already holds: a
4
//! symbol loads the closure of the heads it names, and `all()`, `X::`,
5
//! `dependents()` and `roots()` load every bookmark, remote bookmark, tag
6
//! and the working copy. Evaluation is read-only and has a time budget;
7
//! a query past it fails and says what to narrow.
8
9
use super::*;
10
use crate::ids::NodeId;
11
use crate::revset::{self, Expr};
12
use crate::state::ChangeMeta;
13
use std::time::{Duration, Instant};
14
15
type Set = BTreeSet<ChangeHash>;
16
17
/// The default budget for a command-line evaluation.
18
pub const REVSET_BUDGET: Duration = Duration::from_secs(10);
19
20
/// A revset's result with the metadata of every change it names.
21
#[derive(Clone, Debug)]
22
pub struct Revset {
23
    /// Newest first, as a log is ordered.
24
    pub hashes: Vec<ChangeHash>,
25
    pub metas: HashMap<ChangeHash, ChangeMeta>,
26
}
27
28
struct Eval<'a> {
29
    repo: &'a Repo,
30
    graph: HashMap<ChangeHash, ChangeMeta>,
31
    /// Which heads have had their closure loaded.
32
    loaded: HashSet<ChangeHash>,
33
    all_loaded: bool,
34
    dependents: Option<HashMap<ChangeHash, Vec<ChangeHash>>>,
35
    names: HashMap<KeyId, Option<(String, String)>>,
36
    /// Who authenticated each change, once asked (signature checks cost).
37
    auth: HashMap<ChangeHash, SignatureStatus>,
38
    deadline: Instant,
39
    budget: Duration,
40
    now_ms: u64,
41
    depth: usize,
42
}
43
445×
fn err(msg: impl Into<String>) -> RepoError {
455×
    RepoError::Msg(msg.into())
465×
}
47
48
impl<'a> Eval<'a> {
49220×
    fn tick(&self) -> Result<()> {
50220×
        if Instant::now() > self.deadline {
511×
            return Err(err(format!(
521×
                "the revset went past its budget of {:.0} s; narrow it with a range such as main.. or a date()",
531×
                self.budget.as_secs_f64()
541×
            )));
55219×
        }
56219×
        Ok(())
57220×
    }
58
59
    /// Load the closure of `heads` into the graph.
6081×
    fn ensure(&mut self, heads: &[ChangeHash]) -> Result<()> {
6181×
        let fresh: Vec<ChangeHash> = heads
6281×
            .iter()
6381×
            .copied()
64207×
            .filter(|h| !self.loaded.contains(h))
6581×
            .collect();
6681×
        if fresh.is_empty() {
678×
            return Ok(());
6873×
        }
69280×
        for (h, m) in self.repo.metas_of(&fresh)? {
70280×
            self.graph.insert(h, m);
71280×
        }
7273×
        self.loaded.extend(fresh);
7373×
        self.dependents = None;
7473×
        self.tick()
7581×
    }
76
77
    /// Load every change the repository names: bookmarks, remote
78
    /// bookmarks, tags, and the working copy.
7924×
    fn load_all(&mut self) -> Result<()> {
8024×
        if self.all_loaded {
810×
            return Ok(());
8224×
        }
8324×
        let view = self.repo.view()?;
8424×
        let mut heads: Vec<ChangeHash> = Vec::new();
8524×
        for st in view.bookmarks.values() {
8624×
            heads.extend(self.repo.store().get_state(st)?.heads);
87
        }
8824×
        for bookmarks in view.remotes.values() {
891×
            for st in bookmarks.values() {
901×
                heads.extend(self.repo.store().get_state(st)?.heads);
91
            }
92
        }
9324×
        for st in view.tags.values() {
944×
            heads.extend(self.repo.store().get_state(st)?.heads);
95
        }
9624×
        if let Some(wc) = self.working_copy() {
9724×
            heads.push(wc);
9824×
        }
9924×
        heads.sort();
10024×
        heads.dedup();
10124×
        self.ensure(&heads)?;
10224×
        self.all_loaded = true;
10324×
        Ok(())
10424×
    }
105
10635×
    fn working_copy(&self) -> Option<ChangeHash> {
10735×
        if self.repo.is_bare() {
1080×
            None
109
        } else {
11035×
            self.repo.working_copy().ok().map(|(h, _)| h)
111
        }
11235×
    }
113
11418×
    fn all(&mut self) -> Result<Set> {
11518×
        self.load_all()?;
11618×
        Ok(self.graph.keys().copied().collect())
11718×
    }
118
1196×
    fn dependents_map(&mut self) -> Result<&HashMap<ChangeHash, Vec<ChangeHash>>> {
1206×
        self.load_all()?;
1216×
        if self.dependents.is_none() {
1226×
            let mut map: HashMap<ChangeHash, Vec<ChangeHash>> = HashMap::new();
12330×
            for (h, m) in &self.graph {
12430×
                for d in &m.deps {
12530×
                    map.entry(*d).or_default().push(*h);
12630×
                }
127
            }
1286×
            self.dependents = Some(map);
1290×
        }
1306×
        Ok(self.dependents.as_ref().unwrap())
1316×
    }
132
133
    /// `::X`: the members and their dependency closures.
13435×
    fn ancestors(&mut self, set: &Set) -> Result<Set> {
13535×
        let mut out = set.clone();
13635×
        let mut stack: Vec<ChangeHash> = set.iter().copied().collect();
13735×
        let mut n = 0usize;
138149×
        while let Some(h) = stack.pop() {
139114×
            if let Some(m) = self.graph.get(&h) {
140114×
                for d in &m.deps {
14166×
                    if out.insert(*d) {
14238×
                        stack.push(*d);
14338×
                    }
144
                }
1450×
            }
146114×
            n += 1;
147114×
            if n.is_multiple_of(4096) {
1480×
                self.tick()?;
149114×
            }
150
        }
15135×
        Ok(out)
15235×
    }
153
154
    /// `X::`: the members and everything that transitively depends on them.
1554×
    fn descendants(&mut self, set: &Set) -> Result<Set> {
1564×
        let mut out = set.clone();
1574×
        let mut stack: Vec<ChangeHash> = set.iter().copied().collect();
1584×
        let deps = self.dependents_map()?;
15918×
        while let Some(h) = stack.pop() {
16014×
            if let Some(ds) = deps.get(&h) {
16113×
                for d in ds {
16213×
                    if out.insert(*d) {
16310×
                        stack.push(*d);
16410×
                    }
165
                }
1664×
            }
167
        }
1684×
        Ok(out)
1694×
    }
170
171
    /// Members no other member depends on, directly or through others.
1722×
    fn heads(&mut self, set: &Set) -> Result<Set> {
1732×
        let mut below: HashSet<ChangeHash> = HashSet::new();
1742×
        let mut stack: Vec<ChangeHash> = set
1752×
            .iter()
1767×
            .filter_map(|h| self.graph.get(h))
1777×
            .flat_map(|m| m.deps.iter().copied())
1782×
            .collect();
1795×
        while let Some(h) = stack.pop() {
1803×
            if below.insert(h) {
1812×
                if let Some(m) = self.graph.get(&h) {
1822×
                    stack.extend(m.deps.iter().copied());
1832×
                }
1841×
            }
185
        }
1867×
        Ok(set.iter().copied().filter(|h| !below.contains(h)).collect())
1872×
    }
188
189
    /// Members that depend on no other member, directly or through others.
1901×
    fn roots(&mut self, set: &Set) -> Result<Set> {
1911×
        let deps = self.dependents_map()?;
1921×
        let mut above: HashSet<ChangeHash> = HashSet::new();
1931×
        let mut stack: Vec<ChangeHash> = set
1941×
            .iter()
1954×
            .filter_map(|h| deps.get(h))
1964×
            .flat_map(|ds| ds.iter().copied())
1971×
            .collect();
1988×
        while let Some(h) = stack.pop() {
1997×
            if above.insert(h) {
2003×
                if let Some(ds) = deps.get(&h) {
2012×
                    stack.extend(ds.iter().copied());
2022×
                }
2034×
            }
204
        }
2054×
        Ok(set.iter().copied().filter(|h| !above.contains(h)).collect())
2061×
    }
207
20845×
    fn symbol(&mut self, name: &str) -> Result<Set> {
20945×
        if name == "@" {
2104×
            let Some(wc) = self.working_copy() else {
2110×
                return Err(err(
2120×
                    "@ names the working copy, and a bare repository has none",
2130×
                ));
214
            };
2154×
            self.ensure(&[wc])?;
2164×
            return Ok([wc].into_iter().collect());
21741×
        }
21841×
        let heads = self.repo.resolve_heads(name)?;
21940×
        self.ensure(&heads)?;
22040×
        Ok(heads.into_iter().collect())
22145×
    }
222
223
    /// The identity's name and email, if its object is known.
2245×
    fn who(&mut self, key: &KeyId) -> Option<(String, String)> {
2255×
        if let Some(w) = self.names.get(key) {
2260×
            return w.clone();
2275×
        }
2285×
        let w = self.repo.identity(key).ok().map(|id| (id.name, id.email));
2295×
        self.names.insert(*key, w.clone());
2305×
        w
2315×
    }
232
233
    /// The set a predicate ranges over: what it is intersected with when
234
    /// that is known, else everything.
23530×
    fn universe(&mut self, within: Option<&Set>) -> Result<Set> {
23630×
        match within {
23712×
            Some(s) => Ok(s.clone()),
23818×
            None => self.all(),
239
        }
24030×
    }
241
2425×
    fn filter_metas(
2435×
        &mut self,
2445×
        within: Option<&Set>,
2455×
        mut keep: impl FnMut(&ChangeHash, &ChangeMeta) -> bool,
2465×
    ) -> Result<Set> {
2475×
        let candidates = self.universe(within)?;
2485×
        let mut out = Set::new();
24924×
        for (i, h) in candidates.iter().enumerate() {
25024×
            if let Some(m) = self.graph.get(h) {
25124×
                if keep(h, m) {
2527×
                    out.insert(*h);
25317×
                }
2540×
            }
25524×
            if i % 8192 == 8191 {
2560×
                self.tick()?;
25724×
            }
258
        }
2595×
        Ok(out)
2605×
    }
261
262
    /// Changes touching any node whose path matches `glob` in the
263
    /// evaluation state: the working copy's base, or the bookmark's state
264
    /// in a bare repository. Nodes keep their identity through renames.
2654×
    fn touches(&mut self, within: Option<&Set>, glob: &str) -> Result<Set> {
2664×
        let matcher = globset::GlobBuilder::new(glob)
2674×
            .literal_separator(false)
2684×
            .build()
2694×
            .map_err(|e| err(format!("touches({glob:?}): {e}")))?
2704×
            .compile_matcher();
2714×
        let heads = match self.working_copy() {
2724×
            Some(wc) => self
2734×
                .graph
2744×
                .get(&wc)
2754×
                .map(|m| m.deps.clone())
2764×
                .or_else(|| self.repo.working_copy().ok().map(|(_, c)| c.deps))
2774×
                .unwrap_or_default(),
2780×
            None => self.repo.bookmark_heads()?,
279
        };
2804×
        let state = self.repo.load_state(&heads)?;
2814×
        let nodes: HashSet<NodeId> = state
2824×
            .tree()
2834×
            .view()
2844×
            .entries
2854×
            .iter()
28636×
            .filter(|(path, _)| matcher.is_match(path))
2874×
            .map(|(_, e)| e.node)
2884×
            .collect();
2894×
        if nodes.is_empty() {
2901×
            return Ok(Set::new());
2913×
        }
2923×
        let candidates = self.universe(within)?;
2933×
        let mut out = Set::new();
29414×
        for (i, h) in candidates.iter().enumerate() {
29514×
            let Ok(c) = self.repo.store().get_change(h) else {
2960×
                continue;
297
            };
29830×
            if c.tree_ops.iter().any(|o| nodes.contains(&o.node()))
2999×
                || c.edits.iter().any(|e| nodes.contains(&e.node()))
3007×
            {
3017×
                out.insert(*h);
3027×
            }
30314×
            if i % 256 == 255 {
3040×
                self.tick()?;
30514×
            }
306
        }
3073×
        Ok(out)
3084×
    }
309
31030×
    fn text_arg<'e>(&self, name: &str, args: &'e [Expr], i: usize) -> Result<&'e str> {
31130×
        match args.get(i) {
31230×
            Some(Expr::Symbol(s)) => Ok(s),
3130×
            _ => Err(err(format!("{name}() takes text, quoted if it has spaces"))),
314
        }
31530×
    }
316
31749×
    fn arity(&self, name: &str, args: &[Expr], n: usize) -> Result<()> {
31849×
        if args.len() != n {
3190×
            return Err(err(format!(
320
                "{name}() takes {n} argument{}, not {}",
3210×
                if n == 1 { "" } else { "s" },
3220×
                args.len()
323
            )));
32449×
        }
32549×
        Ok(())
32649×
    }
327
32854×
    fn call(&mut self, name: &str, args: &[Expr], within: Option<&Set>) -> Result<Set> {
32954×
        match name {
33054×
            "all" => {
3312×
                self.arity(name, args, 0)?;
3322×
                self.universe(within)
333
            }
33452×
            "none" => {
3351×
                self.arity(name, args, 0)?;
3361×
                Ok(Set::new())
337
            }
33851×
            "deps" => {
3391×
                self.arity(name, args, 1)?;
3401×
                let x = self.eval(&args[0], None)?;
3411×
                let mut out = Set::new();
3423×
                for h in &x {
3433×
                    if let Some(m) = self.graph.get(h) {
3443×
                        out.extend(m.deps.iter().copied());
3453×
                    }
346
                }
3471×
                Ok(out)
348
            }
34950×
            "dependents" => {
3501×
                self.arity(name, args, 1)?;
3511×
                let x = self.eval(&args[0], None)?;
3521×
                let deps = self.dependents_map()?;
3531×
                let mut out = Set::new();
3541×
                for h in &x {
3551×
                    if let Some(ds) = deps.get(h) {
3561×
                        out.extend(ds.iter().copied());
3571×
                    }
358
                }
3591×
                Ok(out)
360
            }
36149×
            "heads" => {
3622×
                self.arity(name, args, 1)?;
3632×
                let x = self.eval(&args[0], None)?;
3642×
                self.heads(&x)
365
            }
36647×
            "roots" => {
3671×
                self.arity(name, args, 1)?;
3681×
                let x = self.eval(&args[0], None)?;
3691×
                self.roots(&x)
370
            }
37146×
            "independent" => {
3722×
                self.arity(name, args, 2)?;
3732×
                let x = self.eval(&args[0], None)?;
3742×
                let inn = self.eval(&args[1], None)?;
3752×
                let below = self.ancestors(&x)?;
3762×
                let above = self.descendants(&x)?;
3772×
                Ok(inn
3782×
                    .into_iter()
3799×
                    .filter(|h| !below.contains(h) && !above.contains(h))
3802×
                    .collect())
381
            }
38244×
            "conflicts" => {
3834×
                if args.len() > 1 {
3840×
                    return Err(err(
3850×
                        "conflicts() takes a state, or nothing for the working copy's",
3860×
                    ));
3874×
                }
3884×
                let heads: Vec<ChangeHash> = match args.first() {
3891×
                    Some(e) => self.eval(e, None)?.into_iter().collect(),
3903×
                    None => match self.working_copy() {
3913×
                        Some(wc) => {
3923×
                            self.ensure(&[wc])?;
3933×
                            self.graph
3943×
                                .get(&wc)
3953×
                                .map(|m| m.deps.clone())
3963×
                                .unwrap_or_default()
397
                        }
3980×
                        None => self.repo.bookmark_heads()?,
399
                    },
400
                };
4014×
                self.conflicts(within, &heads)
402
            }
40340×
            "latest" => {
4041×
                self.arity(name, args, 2)?;
4051×
                let x = self.eval(&args[0], None)?;
4061×
                let n: usize = self
4071×
                    .text_arg(name, args, 1)?
4081×
                    .parse()
4091×
                    .map_err(|_| err("latest(X, n) takes a count"))?;
4101×
                let mut members: Vec<ChangeHash> = x.into_iter().collect();
4111×
                self.sort_newest_first(&mut members);
4121×
                Ok(members.into_iter().take(n).collect())
413
            }
41439×
            "author" => {
4157×
                self.arity(name, args, 1)?;
4167×
                let who = self.text_arg(name, args, 0)?.to_string();
4177×
                let mut matches: HashMap<KeyId, bool> = HashMap::new();
41827×
                self.filter_metas_with(within, |ev, _, m| {
41927×
                    if let Some(hit) = matches.get(&m.author) {
42020×
                        return *hit;
4217×
                    }
4227×
                    let hit = ev.names_key(&who, &m.author);
4237×
                    matches.insert(m.author, hit);
4247×
                    hit
42527×
                })
426
            }
42732×
            "description" => {
4282×
                self.arity(name, args, 1)?;
4292×
                let text = self.text_arg(name, args, 0)?.to_string();
4302×
                if text.starts_with("re:") {
4310×
                    return Err(err(
4320×
                        "description(re:…) is not supported yet; give a substring",
4330×
                    ));
4342×
                }
4352×
                let needle = text.to_lowercase();
43610×
                self.filter_metas(within, |_, m| m.message.to_lowercase().contains(&needle))
437
            }
43830×
            "date" => {
4392×
                self.arity(name, args, 1)?;
4402×
                let range = revset::parse_date_range(self.text_arg(name, args, 0)?, self.now_ms)
4412×
                    .map_err(err)?;
4429×
                self.filter_metas(within, |_, m| range.contains(m.hlc.wall_ms))
443
            }
44428×
            "kind" => {
4451×
                self.arity(name, args, 1)?;
4461×
                let kind = match self.text_arg(name, args, 0)? {
4471×
                    "normal" => ChangeKind::Normal,
4481×
                    "resolution" => ChangeKind::Resolution,
4491×
                    "working-copy" | "working_copy" => ChangeKind::WorkingCopy,
4500×
                    other => {
4510×
                        return Err(err(format!(
4520×
                            "kind({other:?}): use normal, resolution or working-copy"
4530×
                        )))
454
                    }
455
                };
4565×
                self.filter_metas(within, |_, m| m.kind == kind)
457
            }
45827×
            "touches" => {
4594×
                self.arity(name, args, 1)?;
4604×
                let glob = self.text_arg(name, args, 0)?.to_string();
4614×
                self.touches(within, &glob)
462
            }
463
            // Trust (§2.4): what authenticated a change, and what vouched
464
            // for the states it is in.
46523×
            "signed_by" => {
4665×
                self.arity(name, args, 1)?;
4675×
                let who = self.text_arg(name, args, 0)?.to_string();
46825×
                self.filter_metas_with(within, |ev, h, m| match ev.auth(h, m) {
4694×
                    SignatureStatus::Valid { key, .. } => ev.names_key(&who, &key),
47021×
                    _ => false,
47125×
                })
472
            }
47318×
            "delegated" => {
4742×
                self.arity(name, args, 0)?;
4758×
                self.filter_metas_with(within, |ev, h, m| {
4768×
                    !m.author.is_zero()
4778×
                        && matches!(ev.auth(h, m), SignatureStatus::Valid { key, .. } if key != m.author)
4788×
                })
479
            }
48016×
            "unsigned" => {
4813×
                self.arity(name, args, 0)?;
48210×
                self.filter_metas_with(within, |ev, h, m| {
48310×
                    m.kind != ChangeKind::WorkingCopy
48410×
                        && !matches!(ev.auth(h, m), SignatureStatus::Valid { .. })
48510×
                })
486
            }
48713×
            "revoked" => {
4883×
                self.arity(name, args, 0)?;
48911×
                self.filter_metas_with(within, |ev, h, m| {
49011×
                    matches!(ev.auth(h, m), SignatureStatus::Valid { revoked: true, .. })
49111×
                })
492
            }
49310×
            "attested" => {
4945×
                self.arity(name, args, 1)?;
4955×
                let kind = self.text_arg(name, args, 0)?.to_string();
4965×
                self.attested(within, &kind)
497
            }
498
            // A bookmark's heads by name: what a bare bookmark symbol means,
499
            // without falling through to tags and change prefixes.
5005×
            "bookmark" => {
5012×
                self.arity(name, args, 1)?;
5022×
                let b = self.text_arg(name, args, 0)?;
5032×
                let view = self.repo.view()?;
5042×
                let st = match view.bookmarks.get(b) {
5051×
                    Some(st) => *st,
5061×
                    None => match remote_bookmark(&view, b) {
5070×
                        Some((_, _, st)) => st,
5081×
                        None => return Err(err(format!("bookmark({b}): no such bookmark"))),
509
                    },
510
                };
5111×
                let heads = self.repo.store().get_state(&st)?.heads;
5121×
                self.ensure(&heads)?;
5131×
                Ok(heads.into_iter().collect())
514
            }
5153×
            "built" => {
5161×
                self.arity(name, args, 1)?;
5171×
                let job = self.text_arg(name, args, 0)?.to_string();
5181×
                self.built(within, &job)
519
            }
5202×
            other => {
521
                // A saved revset: `oxy config revset.NAME 'EXPR'` makes NAME().
5222×
                if let Some(saved) = self.repo.config.revsets.get(other).cloned() {
5231×
                    self.arity(name, args, 0)?;
5241×
                    if self.depth >= 16 {
5250×
                        return Err(err(format!("revset.{other} refers to itself")));
5261×
                    }
5271×
                    let e = revset::parse(&saved)
5281×
                        .map_err(|e| err(format!("revset.{other} = {saved:?}: {e}")))?;
5291×
                    self.depth += 1;
5301×
                    let r = self.eval(&e, within);
5311×
                    self.depth -= 1;
5321×
                    return r;
5331×
                }
5341×
                Err(err(format!(
5351×
                    "unknown function {other}(); see docs/revset_query_language.md"
5361×
                )))
537
            }
538
        }
53954×
    }
540
541
    /// The authentication of a change: its signature or seal, checked once.
54254×
    fn auth(&mut self, h: &ChangeHash, m: &ChangeMeta) -> SignatureStatus {
54354×
        if let Some(s) = self.auth.get(h) {
5441×
            return s.clone();
54553×
        }
54653×
        let s = self
54753×
            .repo
54853×
            .authenticate_change_known(h, Some(m))
54953×
            .unwrap_or_else(|e| SignatureStatus::Invalid(e.to_string()));
55053×
        self.auth.insert(*h, s.clone());
55153×
        s
55254×
    }
553
554
    /// Whether `who` names key `key`: by hex prefix, `me`, or the name or
555
    /// email of its identity.
55611×
    fn names_key(&mut self, who: &str, key: &KeyId) -> bool {
55711×
        (who == "me" && Some(*key) == self.repo.config.author_key)
5587×
            || key.starts_with_hex(who)
5595×
            || self.who(key).is_some_and(|(n, e)| {
5605×
                let needle = who.to_lowercase();
5615×
                n.to_lowercase().contains(&needle) || e.to_lowercase().contains(&needle)
5625×
            })
56311×
    }
564
565
    /// The closure of the states whose heads these are, within `within`.
5665×
    fn closure_of(&mut self, mut heads: Vec<ChangeHash>, within: Option<&Set>) -> Result<Set> {
5675×
        heads.sort();
5685×
        heads.dedup();
5695×
        self.ensure(&heads)?;
5705×
        let set: Set = heads.into_iter().collect();
5715×
        let closure = self.ancestors(&set)?;
5725×
        Ok(match within {
5730×
            Some(w) => closure.intersection(w).copied().collect(),
5745×
            None => closure,
575
        })
5765×
    }
577
578
    /// The closure of every state that carries an attestation of `kind`:
579
    /// `tag` (tagged states), `receipt` (states a runner reported on) or
580
    /// `ref` (states a remote attested, as its tracking bookmarks hold).
5815×
    fn attested(&mut self, within: Option<&Set>, kind: &str) -> Result<Set> {
5825×
        let mut heads: Vec<ChangeHash> = Vec::new();
5835×
        match kind {
5845×
            "tag" => {
5853×
                for t in self.repo.tags()? {
5862×
                    heads.extend(t.heads);
5872×
                }
588
            }
5892×
            "receipt" => {
5901×
                for st in self.repo.store().receipt_states()? {
5910×
                    if let Ok(s) = self.repo.store().get_state(&st) {
5920×
                        heads.extend(s.heads);
5930×
                    }
594
                }
595
            }
5961×
            "ref" => {
5970×
                let view = self.repo.view()?;
5980×
                for bookmarks in view.remotes.values() {
5990×
                    for st in bookmarks.values() {
6000×
                        heads.extend(self.repo.store().get_state(st)?.heads);
601
                    }
602
                }
603
            }
6041×
            other => return Err(err(format!("attested({other:?}): use tag, receipt or ref"))),
605
        }
6064×
        self.closure_of(heads, within)
6075×
    }
608
609
    /// The closure of every state with a valid receipt for Flow job `job`
610
    /// from a runner whose key is not revoked.
6111×
    fn built(&mut self, within: Option<&Set>, job: &str) -> Result<Set> {
6121×
        let mut heads: Vec<ChangeHash> = Vec::new();
6131×
        for st in self.repo.store().receipt_states()? {
6140×
            let Ok(state) = self.repo.store().get_state(&st) else {
6150×
                continue;
616
            };
6170×
            let good = self
6180×
                .repo
6190×
                .receipts(&state.heads)?
6200×
                .into_iter()
6210×
                .any(|r| r.signed.receipt.job_name == job && r.problem.is_none() && !r.revoked);
6220×
            if good {
6230×
                heads.extend(state.heads);
6240×
            }
6250×
            self.tick()?;
626
        }
6271×
        self.closure_of(heads, within)
6281×
    }
629
630
    /// `conflicts(S)`: the changes that contributed atoms to a conflict in
631
    /// the state `heads` name (conflicts belong to states, not changes).
6324×
    fn conflicts(&mut self, within: Option<&Set>, heads: &[ChangeHash]) -> Result<Set> {
6334×
        let state = self.repo.load_state(heads)?;
6344×
        let snapshot = state.materialize();
6354×
        let mut out = Set::new();
63636×
        for f in snapshot.files.values() {
63736×
            if !f.is_conflicted() {
63834×
                continue;
6392×
            }
6402×
            match &f.content {
6412×
                crate::state::FileContent::Text { .. } => {
6422×
                    out.extend(state.conflict_regions(&f.node).contributors);
6432×
                }
6440×
                crate::state::FileContent::Opaque { blobs } => {
6450×
                    out.extend(state.blob_contributors(&f.node, blobs));
6460×
                }
6470×
                crate::state::FileContent::Dir => {}
648
            }
6492×
            self.tick()?;
650
        }
6514×
        self.ensure(heads)?;
6524×
        Ok(match within {
6530×
            Some(w) => out.intersection(w).copied().collect(),
6544×
            None => out,
655
        })
6564×
    }
657
658
    /// `filter_metas` for a test that needs the evaluator (identity lookups).
65920×
    fn filter_metas_with(
66020×
        &mut self,
66120×
        within: Option<&Set>,
66220×
        mut keep: impl FnMut(&mut Eval<'a>, &ChangeHash, &ChangeMeta) -> bool,
66320×
    ) -> Result<Set> {
66420×
        let candidates = self.universe(within)?;
66520×
        let mut out = Set::new();
66681×
        for (i, h) in candidates.iter().enumerate() {
66781×
            if let Some(m) = self.graph.get(h).cloned() {
66881×
                if keep(self, h, &m) {
66936×
                    out.insert(*h);
67045×
                }
6710×
            }
67281×
            if i % 8192 == 8191 {
6730×
                self.tick()?;
67481×
            }
675
        }
67620×
        Ok(out)
67720×
    }
678
679
    /// Whether an expression selects by a property, so that evaluating it
680
    /// within the other side of an intersection saves scanning everything.
68123×
    fn is_predicate(e: &Expr) -> bool {
68223×
        match e {
68313×
            Expr::Call(name, _) => matches!(
68413×
                name.as_str(),
68513×
                "author"
6867×
                    | "description"
6877×
                    | "date"
6886×
                    | "kind"
6896×
                    | "touches"
6905×
                    | "all"
6915×
                    | "signed_by"
6925×
                    | "delegated"
6935×
                    | "unsigned"
6942×
                    | "revoked"
6952×
                    | "attested"
6961×
                    | "built"
6971×
                    | "conflicts"
698
            ),
6990×
            Expr::Intersection(a, b) => Self::is_predicate(a) || Self::is_predicate(b),
7000×
            Expr::Union(a, b) => Self::is_predicate(a) && Self::is_predicate(b),
7010×
            Expr::Difference(a, _) => Self::is_predicate(a),
70210×
            _ => false,
703
        }
70423×
    }
705
706145×
    fn eval(&mut self, e: &Expr, within: Option<&Set>) -> Result<Set> {
707145×
        self.tick()?;
708144×
        match e {
70945×
            Expr::Symbol(s) => self.symbol(s),
71023×
            Expr::Ancestors(x) => {
71123×
                let x = self.eval(x, None)?;
71223×
                self.ancestors(&x)
713
            }
7141×
            Expr::Descendants(x) => {
7151×
                let x = self.eval(x, None)?;
7161×
                self.descendants(&x)
717
            }
7181×
            Expr::Between(x, y) => {
7191×
                let x = self.eval(x, None)?;
7201×
                let y = self.eval(y, None)?;
7211×
                let down = self.descendants(&x)?;
7221×
                let up = self.ancestors(&y)?;
7231×
                Ok(down.intersection(&up).copied().collect())
724
            }
7252×
            Expr::Range(x, y) => {
7262×
                let x = self.eval(x, None)?;
7272×
                let y = self.eval(y, None)?;
7282×
                let from = self.ancestors(&x)?;
7292×
                let to = self.ancestors(&y)?;
7302×
                Ok(to.difference(&from).copied().collect())
731
            }
7323×
            Expr::Union(a, b) => {
7333×
                let mut a = self.eval(a, within)?;
7343×
                a.extend(self.eval(b, within)?);
7353×
                Ok(a)
736
            }
73712×
            Expr::Intersection(a, b) => {
73812×
                let (first, second) = if Self::is_predicate(a) && !Self::is_predicate(b) {
7399×
                    (b, a)
740
                } else {
7413×
                    (a, b)
742
                };
74312×
                let left = self.eval(first, within)?;
74412×
                let right = self.eval(second, Some(&left))?;
74512×
                Ok(left.intersection(&right).copied().collect())
746
            }
7473×
            Expr::Difference(a, b) => {
7483×
                let left = self.eval(a, within)?;
7493×
                let right = self.eval(b, Some(&left))?;
7503×
                Ok(left.difference(&right).copied().collect())
751
            }
75254×
            Expr::Call(name, args) => self.call(name, args, within),
753
        }
754145×
    }
755
75663×
    fn sort_newest_first(&self, hashes: &mut [ChangeHash]) {
75774×
        hashes.sort_by(|a, b| {
75874×
            let (ma, mb) = (self.graph.get(a), self.graph.get(b));
75974×
            mb.map(|m| m.hlc).cmp(&ma.map(|m| m.hlc)).then(b.cmp(a))
76074×
        });
76163×
    }
762
}
763
764
impl Repo {
765
    /// Evaluate a revset within [`REVSET_BUDGET`].
76667×
    pub fn revset(&self, src: &str) -> Result<Revset> {
76767×
        self.revset_with_budget(src, REVSET_BUDGET)
76867×
    }
769
770
    /// Evaluate a revset: the changes it selects, newest first, with
771
    /// their metadata. Read-only. Fails past `budget`.
77268×
    pub fn revset_with_budget(&self, src: &str, budget: Duration) -> Result<Revset> {
77368×
        let expr = revset::parse(src).map_err(|e| err(format!("revset {src:?}: {e}")))?;
77467×
        let mut ev = Eval {
77567×
            repo: self,
77667×
            graph: HashMap::new(),
77767×
            loaded: HashSet::new(),
77867×
            all_loaded: false,
77967×
            dependents: None,
78067×
            names: HashMap::new(),
78167×
            auth: HashMap::new(),
78267×
            deadline: Instant::now() + budget,
78367×
            budget,
78467×
            now_ms: Hlc::now().wall_ms,
78567×
            depth: 0,
78667×
        };
78767×
        let set = ev.eval(&expr, None)?;
78862×
        let mut hashes: Vec<ChangeHash> = set.into_iter().collect();
78962×
        ev.sort_newest_first(&mut hashes);
79062×
        let metas = hashes
79162×
            .iter()
792117×
            .filter_map(|h| ev.graph.get(h).map(|m| (*h, m.clone())))
79362×
            .collect();
79462×
        Ok(Revset { hashes, metas })
79568×
    }
796
797
    /// The log entries a revset selects, newest first. A head of the
798
    /// current bookmark is marked as in `log`.
7991×
    pub fn log_revset(&self, src: &str) -> Result<Vec<LogEntry>> {
8001×
        let r = self.revset(src)?;
8011×
        let heads: BTreeSet<ChangeHash> = self.bookmark_heads()?.into_iter().collect();
8021×
        let wc = if self.is_bare() {
8030×
            None
804
        } else {
8051×
            self.working_copy().ok().map(|(h, _)| h)
806
        };
8071×
        let mut entries: Vec<LogEntry> = r
8081×
            .hashes
8091×
            .iter()
8105×
            .filter_map(|h| {
8115×
                r.metas.get(h).map(|m| LogEntry {
8125×
                    hash: *h,
8135×
                    change: m.clone(),
8145×
                    is_working_copy: Some(*h) == wc,
8155×
                    is_head: heads.contains(h),
8165×
                })
8175×
            })
8181×
            .collect();
819
        // The working copy first, as `log` shows it.
8208×
        entries.sort_by_key(|e| !e.is_working_copy);
8211×
        Ok(entries)
8221×
    }
823
}