git-ents.gitmain
⌘K
foforge
config.rs91 lines · 3.8 KB · rusthistorycomment on this file
1//! The verification epoch, read from `refs/meta/config` (`gate.epoch`).
2
3use facet::Facet;
4use gix_hash::ObjectId;
5use gix_object::Find;
6use gix_ref_store::RefStoreRead;
7
8use crate::error::{Error, Result};
9use crate::object::expect_commit;
10
11/// The slice of `refs/meta/config`'s typed tree the gate consults: the
12/// verification epoch (`gate.epoch`). A later, additive narrowing (e.g. a
13/// designated-worker roster for `effect.official`) would read the same
14/// tree, alongside this field.
15///
16/// `model.sdoc` defines no Config entity yet, so this struct is the
17/// first (and currently only) definition of the config tree's shape; it
18/// lives here rather than in `ents-model` because these are the only
19/// fields any crate reads today. When configuration grows non-gate fields
20/// (description, role rules, ...), the entity moves to `ents-model` and
21/// that change is a storage migration like any other struct change
22/// (`meta-ref.migration`).
23///
24/// `epoch` is `None` on a config written before verification was turned
25/// on. Once it is `Some`, the gate applies the tip invariant to every
26/// `refs/meta/*` update; the value records *when* (seconds since the
27/// Unix epoch) enforcement began, for audit tooling — the live gate only
28/// tests presence, because every proposed update is by definition after
29/// the epoch that admits it.
30///
31/// # Examples
32///
33/// ```
34/// use ents_gate::Config;
35///
36/// let config = Config { epoch: Some(1_700_000_000) };
37/// let (root, store) = facet_git_tree::serialize(&config).expect("serialize");
38/// let back: Config = facet_git_tree::deserialize(&root, &store).expect("deserialize");
39/// assert_eq!(back, config);
40/// ```
41// @relation(gate.epoch, scope=file)
42#[derive(Debug, Clone, Default, PartialEq, Eq, Facet)]
43pub struct Config {
44 /// When the tip invariant came into force, seconds since the Unix
45 /// epoch; `None` while verification has never been enabled.
46 pub epoch: Option<u64>,
47}
48
49/// The config recorded by the tree of the commit at `oid`, or an
50/// [`Error::Entity`] when the tree does not parse as [`Config`] — an
51/// unreadable config fails closed rather than silently disabling the
52/// gate.
53fn config_at_commit(objects: &dyn Find, oid: ObjectId) -> Result<Config> {
54 let commit = expect_commit(objects, oid)?;
55 facet_git_tree::deserialize(&commit.tree, objects)
56 .map_err(|source| Error::Entity { oid, source })
57}
58
59/// The epoch recorded by the config tree of the commit at `oid`, or an
60/// [`Error::Entity`] when the tree does not parse as [`Config`] — an
61/// unreadable config fails closed rather than silently disabling the
62/// gate.
63pub(crate) fn epoch_at_commit(objects: &dyn Find, oid: ObjectId) -> Result<Option<u64>> {
64 Ok(config_at_commit(objects, oid)?.epoch)
65}
66
67/// `refs/meta/config`'s current tree, or [`Config::default`] when the
68/// config ref does not exist yet — the same "absent means no narrowing in
69/// force" reading [`current_epoch`] already gives absence.
70fn current_config(refs: &dyn RefStoreRead, objects: &dyn Find) -> Result<Config> {
71 #[expect(
72 clippy::expect_used,
73 clippy::unwrap_in_result,
74 reason = "CONFIG_REF is a compile-time constant; the doctest below and \
75 ents-model's own tests pin its validity"
76 )]
77 let name: gix::refs::FullName = ents_model::namespace::CONFIG_REF
78 .try_into()
79 .expect("CONFIG_REF is a valid refname");
80 match refs.get(name.as_ref())? {
81 Some(tip) => config_at_commit(objects, tip),
82 None => Ok(Config::default()),
83 }
84}
85
86/// The epoch currently in force, read from `refs/meta/config`'s tip;
87/// `None` when the config ref does not exist or records no epoch.
88// @relation(gate.epoch, gate.policy-as-state, scope=function)
89pub(crate) fn current_epoch(refs: &dyn RefStoreRead, objects: &dyn Find) -> Result<Option<u64>> {
90 Ok(current_config(refs, objects)?.epoch)
91}