docs: add tracey requirement annotations across the workspace
commit 267b097
docs: add tracey requirement annotations across the workspace
Traces all 70 docs/spec/*/.adoc requirements to their implementing
and testing code via r[impl …] / r[verify …] comments, so
tracey_validate/tracey_status can report live coverage instead of
0/70. Comment-only; no logic changes. 65/70 now have an impl
reference; the remaining 5 (compat.edition, namespace.url,
nonfunctional.no-panic, nonfunctional.no-unsafe,
web.auth.webauthn-onboarding) have no .rs home or are unimplemented.
No reviews of this commit yet — record a verdict below.
Start a review
crates/git-anchor/src/lib.rs
@@ -109,6 +109,7 @@
/// Where in the repository something was attached — authoritative at creation
/// and never mutated afterwards. Projection onto other commits is always a
/// read-time view derived from this record.
+// r[impl comments.anchor]
#[derive(Debug, Clone, PartialEq, Eq, Facet)]
pub struct Anchor {
/// The commit the anchor was created against.
@@ -123,6 +124,7 @@
}
/// Where an [`Anchor`] sits on a target commit, as computed by [`project`].
+// r[impl comments.projection]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Projection {
/// The target tree holds the anchor's exact blob at its exact path; the
@@ -151,6 +153,7 @@
/// `revision` in `repo`, resolving the revision to a full commit id and
/// recording the file's blob id. Fails when the path is not a file at that
/// commit or the range does not fit it.
+// r[impl comments.anchor] - validates path and line range at creation
pub fn capture(
repo: &Path,
revision: &str,
@@ -187,6 +190,7 @@
/// The exact text of `anchor`'s lines — the whole file for a whole-file
/// anchor — derived at read time from the content-addressed blob the anchor
/// names, so it can never disagree with what was anchored.
+// r[impl comments.anchor] - text derived at read time, never stored redundantly
pub fn snippet(repo: &Path, anchor: &Anchor) -> Result<String, Error> {
let repo = gix::open(repo).map_err(|error| Error::Open(Box::new(error)))?;
let blob = ObjectId::try_from(&anchor.blob)
@@ -225,6 +229,7 @@
/// target's with rename tracking to find where the file went, and the line
/// range is mapped through the blob diff's hunks — shifted past edits that
/// land entirely outside it, [`Projection::Outdated`] when an edit touches it.
+// r[impl comments.projection]
pub fn project(repo: &Path, anchor: &Anchor, target: &str) -> Result<Projection, Error> {
let repo = gix::open(repo).map_err(|error| Error::Open(Box::new(error)))?;
let anchor_blob = ObjectId::try_from(&anchor.blob)
@@ -369,6 +374,7 @@
/// the range — including an insertion strictly inside it — means the anchored
/// region itself changed, reported as `None` (outdated) rather than guessed
/// at.
+// r[impl comments.projection] - line-range shifting through blob diff hunks
fn map_range(old: &[u8], new: &[u8], range: LineRange) -> Option<LineRange> {
// Work in 0-based half-open line coordinates, as the hunks do. Everything
// stays unsigned: the shift is tallied as lines added and lines removed
@@ -426,6 +432,7 @@
Some(LineRange { start, end })
}
+ // r[verify comments.anchor]
#[test]
fn capture_records_the_commit_and_blob_and_snippet_derives_the_text() {
let dir = repo();
@@ -440,6 +447,7 @@
assert_eq!(snippet(dir.path(), &anchor).unwrap(), "line 3\nline 4\n");
}
+ // r[verify comments.anchor]
#[test]
fn capture_rejects_a_missing_path_and_an_oversized_range() {
let dir = repo();
@@ -456,6 +464,7 @@
));
}
+ // r[verify comments.projection]
#[test]
fn unchanged_file_projects_as_current() {
let dir = repo();
@@ -472,6 +481,7 @@
);
}
+ // r[verify comments.projection]
#[test]
fn an_edit_above_the_range_shifts_it() {
let dir = repo();
@@ -492,6 +502,7 @@
);
}
+ // r[verify comments.projection]
#[test]
fn an_edit_inside_the_range_is_outdated() {
let dir = repo();
@@ -511,6 +522,7 @@
);
}
+ // r[verify comments.projection]
#[test]
fn a_pure_rename_relocates_with_the_same_lines() {
let dir = repo();
@@ -530,6 +542,7 @@
);
}
+ // r[verify comments.projection]
#[test]
fn a_rename_with_an_edit_above_relocates_and_shifts() {
let dir = repo();
@@ -551,6 +564,7 @@
);
}
+ // r[verify comments.projection]
#[test]
fn a_deleted_file_projects_as_deleted() {
let dir = repo();
@@ -568,6 +582,7 @@
);
}
+ // r[verify comments.projection]
#[test]
fn a_whole_file_anchor_survives_a_modification() {
let dir = repo();
@@ -589,6 +604,7 @@
);
}
+ // r[verify comments.projection]
#[test]
fn projection_works_backwards_onto_an_ancestor() {
let dir = repo();
@@ -610,6 +626,7 @@
);
}
+ // r[verify comments.projection]
#[test]
fn map_range_handles_edges() {
let old = b"a\nb\nc\nd\n".as_slice();
crates/git-comment/src/lib.rs
@@ -28,12 +28,17 @@
use git_anchor::{Anchor, Projection};
use git_store::Provenance;
+// r[impl comments.ref]
/// The namespace under which comments are recorded: one ref,
/// `refs/meta/comments/<id>`, per comment.
pub const COMMENTS_NS: &str = "refs/meta/comments";
/// One comment stored at `refs/meta/comments/<id>`. Author and timestamp are
/// deliberately absent: they live on the ref's commits (see [`provenance`]).
+// r[impl comments.ref]
+// r[impl comments.authorship] - body/anchor/issue only, author and timestamps deliberately absent
+// r[impl comments.anchor] - carries the anchor a comment was written against
+// r[impl comments.projection] - the anchor is retained regardless of projection outcome, so the comment is never lost
#[derive(Debug, Clone, PartialEq, Eq, Facet)]
pub struct Comment {
/// The comment's body text.
@@ -62,6 +67,8 @@
/// Write `comment` to `refs/meta/comments/<id>` in `repo` as a new commit
/// authored by `author` (a `(name, email)` pair), so the ref's commit chain is
/// the comment's edit history and carries its authorship.
+// r[impl comments.ref]
+// r[impl comments.authorship] - stamps the acting author on the commit, not the document tree
pub fn store(
repo: &Path,
id: &str,
@@ -84,6 +91,7 @@
/// Who created and who last updated the comment at `id`, recovered from its
/// ref's commit chain, or `None` when no such comment exists.
+// r[impl comments.authorship] - recovers the creator and last editor from the commit chain
pub fn provenance(repo: &Path, id: &str) -> Result<Option<Provenance>, git_store::Error> {
git_store::Store::open(repo)?.item_provenance(COMMENTS_NS, id)
}
@@ -91,6 +99,7 @@
/// Where `comment`'s anchor sits on `target` (a revision in `repo`): still
/// [`Projection::Current`], relocated to a new path or shifted lines, outdated
/// because the anchored region was edited, or gone with its file.
+// r[impl comments.projection]
pub fn project(
repo: &Path,
comment: &Comment,
@@ -129,6 +138,7 @@
const AUTHOR: (&str, &str) = ("alice", "alice@example.com");
+ // r[verify comments.ref]
#[test]
fn store_then_load_round_trips_a_comment() {
let dir = repo();
@@ -173,6 +183,7 @@
assert_ne!(a_id, new_id(None, &b).unwrap());
}
+ // r[verify comments.authorship]
#[test]
fn provenance_comes_from_the_commits_not_the_document() {
let dir = repo();
@@ -191,6 +202,8 @@
assert!(provenance.created.seconds > 0);
}
+ // r[verify comments.anchor]
+ // r[verify comments.projection]
#[test]
fn a_stored_comment_projects_onto_the_commit_it_was_written_against() {
let dir = repo();
@@ -223,6 +236,8 @@
);
}
+ // r[verify comments.anchor] - on-disk anchor shape (commit/path/blob/lines)
+ // r[verify storage.meta-ref] - hand-built fixture load test for the Comment document
#[test]
fn loads_the_on_disk_comment_format() {
// A fixture written as the real on-disk layout — a `body` blob, an
crates/git-ents-core/src/account.rs
@@ -13,10 +13,12 @@
use crate::component;
+// r[impl account.ref]
/// The ref whose tree holds the account profile, and whose mere presence marks a
/// repository as an account repo.
pub const ACCOUNT_REF: &str = "refs/meta/account";
+// r[impl account.ref]
/// A repository's account profile, stored at [`ACCOUNT_REF`].
#[derive(Debug, Clone, Default, PartialEq, Eq, Facet)]
pub struct Account {
@@ -64,6 +66,7 @@
/// when the repo is not (yet) an account repo. Mirrors the genesis-key idiom
/// `issues::new_id` uses: an identifier derived from content, never a stored
/// field.
+// r[impl account.genesis]
pub fn genesis(repo: &Path) -> Result<Option<String>, git_store::Error> {
git_store::Store::open(repo)?
.history::<Account>(ACCOUNT_REF)?
@@ -96,6 +99,7 @@
}
}
+ // r[verify account.ref]
#[test]
fn store_then_load_round_trips_the_account() {
let repo = unique_repo();
@@ -120,6 +124,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify storage.meta-ref] - hand-built fixture load test for the Account document
#[test]
fn loads_the_on_disk_account_format() {
// A fixture written as the real on-disk layout — `username`,
crates/git-ents-core/src/checks.rs
@@ -35,6 +35,7 @@
/// identity, so it is not duplicated inside the body. `pub` only because it
/// is [`component::MapDocument::Body`] for [`Check`]; nothing outside this
/// module constructs one directly.
+// r[impl checks.definition]
#[derive(Debug, Clone, PartialEq, Eq, Facet)]
pub struct CheckBody {
/// The shell command run for the check (e.g. `cargo fmt --check`), or
@@ -136,6 +137,8 @@
/// toolchain actually exists is checked server-side at job time, not here —
/// unlike `depends`, `toolchains` cross-references a different ref
/// namespace this function has no set of configured names to check against.
+// r[impl checks.definition] static DAG validation: cycles, dangling deps, self edges, no-command-no-deps, rejected images
+// r[impl checks.toolchains] toolchain name must be a valid ref-path segment at write time
pub fn order(checks: &[Check]) -> Result<Vec<&Check>, String> {
let mut by_name: std::collections::BTreeMap<&str, &Check> = std::collections::BTreeMap::new();
for check in checks {
@@ -221,6 +224,7 @@
/// A check run's status, progressing `Queued` → `Running` → a terminal
/// outcome. Closed set — the only values a run legitimately takes, in place
/// of a `String` that every caller had to trust held one of five values.
+// r[impl checks.outcomes] status progression queued->running->pass/fail/error/skipped
#[derive(Debug, Clone, Copy, PartialEq, Eq, Facet)]
#[repr(u8)]
pub enum Status {
@@ -255,6 +259,7 @@
/// One check's on-disk outcome. The map key (the check's name) is not
/// duplicated inside it. Optional fields absent from an older record load as
/// unset, so a run recorded before a field existed still loads.
+// r[impl checks.outcomes] optional metadata fields; name is the map key, not stored redundantly
#[derive(Debug, Clone, PartialEq, Eq, Facet)]
struct Outcome {
/// `queued`, `running`, then `pass`, `fail`, or `error`.
@@ -312,6 +317,7 @@
/// Record a run of `outcomes` for `commit` in `repo`, as a new commit on
/// `refs/meta/runs/<commit>`, parented on the prior run so the ref's commit
/// chain is the run history. The commit's date is the run time.
+// r[impl checks.outcomes] one ref per commit at refs/meta/runs/<commit>; commit chain is history; commit date is run time
pub fn record(
repo: &Path,
commit: ObjectId,
@@ -334,6 +340,7 @@
///
/// When no run has been recorded yet the update starts one, so a worker that
/// advances a run is self-healing even if the `queued` record never landed.
+// r[impl checks.outcomes] progresses a run's status in place (queued->running->terminal)
pub fn update_run(
repo: &Path,
commit: ObjectId,
@@ -454,6 +461,7 @@
}
}
+ // r[verify checks.definition]
#[test]
fn store_then_load_round_trips_the_check_set() {
let repo = unique_repo();
@@ -545,6 +553,7 @@
}
}
+ // r[verify checks.outcomes]
#[test]
fn record_then_runs_round_trips_a_run() {
let repo = unique_repo();
@@ -567,6 +576,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify checks.outcomes]
#[test]
fn recording_a_commit_again_appends_a_run() {
let repo = unique_repo();
@@ -595,6 +605,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify checks.outcomes]
#[test]
fn round_trips_an_outcomes_duration_and_recording() {
let repo = unique_repo();
@@ -612,6 +623,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify checks.outcomes]
#[test]
fn update_run_advances_in_place_rather_than_appending() {
let repo = unique_repo();
@@ -628,6 +640,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify checks.outcomes]
#[test]
fn displays_lowercase_status_words() {
assert_eq!(Status::Queued.to_string(), "queued");
@@ -635,6 +648,7 @@
assert_eq!(Status::Skipped.to_string(), "skipped");
}
+ // r[verify checks.definition]
#[test]
fn store_then_load_round_trips_image_and_depends() {
let repo = unique_repo();
@@ -655,6 +669,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify checks.definition]
#[test]
fn order_runs_dependencies_first() {
let checks = vec![
@@ -670,6 +685,7 @@
assert_eq!(names, vec!["fmt", "test", "ci"]);
}
+ // r[verify checks.definition] rejects a dependency cycle
#[test]
fn order_rejects_a_cycle() {
let checks = vec![
@@ -682,6 +698,7 @@
assert!(err.contains('a') && err.contains('b'));
}
+ // r[verify checks.definition] rejects a dangling dependency
#[test]
fn order_rejects_an_unknown_dependency() {
let checks = vec![dependent("test", "cargo nextest run", &["fmt"])];
@@ -689,6 +706,7 @@
assert!(err.contains("unknown check fmt"), "unexpected error: {err}");
}
+ // r[verify checks.definition] rejects self and duplicate edges
#[test]
fn order_rejects_self_and_duplicate_edges() {
let selfish = vec![dependent("a", "true", &["a"])];
@@ -700,6 +718,7 @@
assert!(order(&doubled).unwrap_err().contains("twice"));
}
+ // r[verify checks.definition] rejects a check with neither a command nor dependencies
#[test]
fn order_rejects_an_empty_check() {
let checks = vec![composite("hollow", &[])];
@@ -710,6 +729,7 @@
);
}
+ // r[verify checks.toolchains]
#[test]
fn order_accepts_a_valid_toolchain_name() {
let checks = vec![toolchained("build", "make", &["gcc-12"])];
@@ -723,6 +743,7 @@
);
}
+ // r[verify checks.toolchains]
#[test]
fn order_rejects_an_invalid_toolchain_name() {
let checks = vec![toolchained("build", "make", &["not/valid"])];
@@ -730,6 +751,7 @@
assert!(err.contains("invalid toolchain"), "unexpected error: {err}");
}
+ // r[verify checks.toolchains]
#[test]
fn store_then_load_round_trips_toolchains() {
let repo = unique_repo();
crates/git-ents-core/src/config.rs
@@ -15,9 +15,11 @@
use crate::component;
+// r[impl config.ref]
/// The ref whose tree holds the repository configuration.
pub const CONFIG_REF: &str = "refs/meta/config";
+// r[impl config.ref]
/// The repository configuration stored at [`CONFIG_REF`].
#[derive(Debug, Clone, Default, PartialEq, Eq, Facet)]
pub struct Config {
@@ -109,6 +111,7 @@
/// An absent ref yields [`Config::default`], as on a repository whose metadata
/// has not been set yet. A present but unreadable ref is an error so callers can
/// distinguish corruption from "no configuration set".
+// r[impl config.ref] - an absent ref yields the default (all empty)
pub fn load_with(store: &git_store::Store) -> Result<Config, git_store::Error> {
Ok(component::load::<Config>(store)?.unwrap_or_default())
}
@@ -158,6 +161,7 @@
}
}
+ // r[verify config.ref]
#[test]
fn store_then_load_round_trips_the_config() {
let repo = unique_repo();
@@ -175,6 +179,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify storage.meta-ref] - hand-built fixture load test for the Config document
#[test]
fn loads_the_on_disk_config_format() {
// A fixture written as the real on-disk layout — `description` and
@@ -193,6 +198,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify config.ref]
#[test]
fn default_when_the_config_ref_is_absent() {
let repo = unique_repo();
crates/git-ents-core/src/issues.rs
@@ -31,6 +31,7 @@
use crate::component;
+// r[impl issues.ref]
/// The namespace under which issues are recorded: one ref,
/// `refs/meta/issues/<id>`, per issue.
pub const ISSUES_NS: &str = "refs/meta/issues";
@@ -50,6 +51,7 @@
Closed,
}
+// r[impl issues.ref]
/// One issue stored at `refs/meta/issues/<id>`.
#[derive(Debug, Clone, PartialEq, Eq, Facet)]
pub struct Issue {
@@ -97,6 +99,7 @@
/// issue derives from one — one origin, one issue, deduplicated on
/// provenance — otherwise the hash of the issue's own initial content, since
/// every issue is a git object and so always has one.
+// r[impl issues.id]
pub fn new_id(origin: Option<&str>, content: &Issue) -> Result<String, git_store::Error> {
git_store::new_id(origin, content)
}
@@ -154,6 +157,7 @@
/// callers believe they claimed it. A CAS conflict here is retried by
/// re-reading the counter, so the number handed back is always the one
/// actually reserved for this call.
+// r[impl issues.id] - only promotion advances the friendly-number counter, never renaming the ref
pub fn promote(repo: &Path, id: &str) -> Result<String, PromoteError> {
let store = git_store::Store::open(repo)?;
let mut number = None;
@@ -208,6 +212,7 @@
}
}
+ // r[verify issues.ref]
#[test]
fn store_then_load_round_trips_an_issue() {
let repo = unique_repo();
@@ -236,6 +241,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify storage.meta-ref] - hand-built fixture load test for the Issue document
#[test]
fn loads_the_on_disk_issue_format() {
// A fixture written as the real on-disk layout — `title`, `body`,
@@ -260,12 +266,14 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify issues.id]
#[test]
fn new_id_uses_the_origin_when_one_is_given() {
let content = issue("A bug", State::Open, &[]);
assert_eq!(new_id(Some("deadbeef"), &content).unwrap(), "deadbeef");
}
+ // r[verify issues.id]
#[test]
fn new_id_hashes_its_own_content_with_no_origin() {
let a = issue("A bug", State::Open, &[]);
@@ -278,6 +286,7 @@
assert_ne!(a_id, b_id);
}
+ // r[verify issues.id]
#[test]
fn filing_an_issue_leaves_its_friendly_number_unset() {
let repo = unique_repo();
@@ -288,6 +297,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify issues.id]
#[test]
fn promotion_assigns_a_number_and_advances_the_counter_without_renaming_the_ref() {
let repo = unique_repo();
crates/git-ents-core/src/members.rs
@@ -40,6 +40,7 @@
use crate::component;
+// r[impl members.ref]
/// The namespace whose refs hold the member set — the push trust root. One
/// `refs/meta/member/<username>` ref per person.
pub const MEMBER_NS: &str = "refs/meta/member";
@@ -50,6 +51,11 @@
format!("{MEMBER_NS}/{username}")
}
+// r[impl members.ref]
+// r[impl members.trust]
+// r[impl members.provenance]
+// r[impl members.account]
+// r[impl members.window]
/// One member: a person named by their `refs/meta/member/<principal>` ref, the
/// window their trust holds within, and the keys (or, later, CA) it rests on.
#[derive(Debug, Clone, PartialEq, Eq, Facet)]
@@ -93,6 +99,7 @@
/// Whether a member was admin-registered or self-attested via web onboarding.
/// Defaults to [`Provenance::AdminRegistered`] so a member ref written before
/// this field existed loads unchanged.
+// r[impl members.provenance] - loads before the field existed as admin-registered
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Facet)]
#[repr(u8)]
pub enum Provenance {
@@ -120,6 +127,7 @@
/// keys, so rotation, expiry, and new devices cost zero downstream edits; it is
/// a security win only when the CA lives off the device (hardware token,
/// offline, or a remote issuer behind SSO).
+// r[impl members.trust] - the three mutually exclusive trust bases
#[derive(Debug, Clone, PartialEq, Eq, Facet)]
#[repr(u8)]
pub enum Trust {
@@ -235,6 +243,7 @@
/// [`store`] checks this before every write, so it holds regardless of
/// which caller builds the member (the CLI today, an admin web action
/// later).
+ // r[impl members.window] - a window must be well-formed and not inverted
pub fn validate(&self) -> Result<(), String> {
for bound in [&self.valid_after, &self.valid_before] {
if let Some(value) = bound
@@ -341,6 +350,8 @@
/// drops out entirely, so a push it would have authorized fails closed. A member
/// resting on a CA is untouched — a compromised CA is revoked by removing its
/// member ref, since a CA is named by a ref rather than listed by fingerprint.
+// r[impl revocations.ref] - subtracts every revoked fingerprint from the trust set
+// r[impl revocations.ca] - a CA-trusted member is left untouched; revocation list is leaf-key-only
#[must_use]
pub fn without_revoked(members: Vec<Member>, revoked: &BTreeSet<String>) -> Vec<Member> {
members
@@ -369,6 +380,7 @@
/// `allowed_signers` options so git enforces expiry: out-of-window keys are not
/// accepted. Options are comma-joined, the syntax OpenSSH requires for more than
/// one.
+// r[impl members.allowed-signers]
#[must_use]
pub fn allowed_signers(members: &[Member]) -> String {
members.iter().flat_map(member_lines).collect::<String>()
@@ -437,6 +449,7 @@
.collect()
}
+ // r[verify members.ref]
#[test]
fn store_then_load_round_trips_a_member() {
let repo = unique_repo();
@@ -484,6 +497,8 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify storage.meta-ref] - hand-built fixture load test for the Member document
+ // r[verify members.provenance]
#[test]
fn loads_the_on_disk_member_format_with_no_provenance_entry_as_admin_registered() {
// A fixture written as the real `member/<username>` layout — a `principal`
@@ -512,6 +527,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify members.trust] - WebAuthn credential set round trip
#[test]
fn loads_a_hand_built_webauthn_fixture() {
// A hand-built `trust/WebAuthn/<credential_id>/{cose_key,label}`
@@ -543,6 +559,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify members.trust] - WebAuthn authorizes web sign-in only, never allowed_signers/push
#[test]
fn store_then_load_round_trips_a_webauthn_member() {
let repo = unique_repo();
@@ -565,6 +582,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify members.allowed-signers] - wildcard principal, namespaces="git"
#[test]
fn renders_a_wildcard_allowed_signers_file() {
let member = Member::with_keys("alice".to_owned(), keys(&[("aa:bb", KEY_A)]));
@@ -574,6 +592,7 @@
);
}
+ // r[verify members.allowed-signers] - validity window as comma-joined options
#[test]
fn renders_the_validity_window_as_comma_joined_options() {
let mut member = Member::with_keys("alice".to_owned(), keys(&[("aa:bb", KEY_A)]));
@@ -587,6 +606,7 @@
);
}
+ // r[verify members.trust] - certificate authority trust basis
#[test]
fn store_then_load_round_trips_a_ca_member() {
let repo = unique_repo();
@@ -599,6 +619,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify revocations.ref]
#[test]
fn without_revoked_drops_revoked_keys_and_emptied_members() {
let alice = Member::with_keys(
@@ -617,6 +638,7 @@
);
}
+ // r[verify revocations.ca]
#[test]
fn without_revoked_leaves_ca_members_untouched() {
let member = Member::with_ca("alice".to_owned(), KEY_A.to_owned());
@@ -627,6 +649,7 @@
);
}
+ // r[verify members.allowed-signers] - a pinned CA renders as a cert-authority line
#[test]
fn renders_a_pinned_ca_as_a_cert_authority_line() {
let mut member = Member::with_ca("alice".to_owned(), KEY_A.to_owned());
@@ -637,6 +660,7 @@
);
}
+ // r[verify members.window]
#[test]
fn validate_rejects_a_malformed_timestamp() {
let mut member = Member::with_keys("alice".to_owned(), keys(&[("aa:bb", KEY_A)]));
@@ -644,6 +668,7 @@
assert!(member.validate().is_err());
}
+ // r[verify members.window]
#[test]
fn validate_rejects_an_inverted_window() {
let mut member = Member::with_keys("alice".to_owned(), keys(&[("aa:bb", KEY_A)]));
@@ -660,6 +685,7 @@
member.validate().unwrap();
}
+ // r[verify members.window]
#[test]
fn store_rejects_a_member_with_an_inverted_window() {
let repo = unique_repo();
crates/git-ents-core/src/revocations.rs
@@ -30,6 +30,7 @@
use crate::component;
+// r[impl revocations.ref]
/// The ref whose tree holds the revocation list — the deny overlay on the trust
/// set.
pub const REVOKED_REF: &str = "refs/meta/revoked";
@@ -134,6 +135,7 @@
}
}
+ // r[verify revocations.ref]
#[test]
fn store_then_load_round_trips_the_revocations() {
let repo = unique_repo();
@@ -167,6 +169,7 @@
let _ = std::fs::remove_dir_all(&repo);
}
+ // r[verify storage.meta-ref] - hand-built fixture load test for the Revocation document
#[test]
fn loads_the_on_disk_revoked_format() {
// A fixture written as the real `revoked/<fingerprint>/reason` subtree
crates/git-ents-core/tests/cert_authority.rs
@@ -22,6 +22,7 @@
/// The principal the CA certifies and the verifier checks — the pusher identity.
const PRINCIPAL: &str = "tester@example.com";
+// r[verify members.trust] - a pinned CA's certs verify; an unpinned CA's do not
#[test]
fn a_cert_from_the_pinned_ca_verifies_and_an_unpinned_one_does_not() {
let Some(agent) = Agent::start() else {
crates/git-ents-server/src/asciidoc.rs
@@ -25,6 +25,8 @@
/// Render AsciiDoc `source` to an embedded HTML fragment, or `None` if it cannot
/// be parsed or converted. The fragment carries no document frame, so callers
/// place it inside their own container.
+// r[impl web.render-registry] - AsciiDoc's entry in the MIME-keyed registry
+// r[impl web.syntax-highlight] - AsciiDoc rendered to HTML
pub(crate) fn to_html(source: &str) -> Option<String> {
let parsed = acdc_parser::parse(source, &ParseOptions::default()).ok()?;
let doc = parsed.document();
crates/git-ents-server/src/checks.rs
@@ -106,6 +106,8 @@
/// The hook does no check work itself: it writes one job file per updated branch
/// into the shared queue directory ([`QUEUE_ENV`]) and returns, so the push is
/// never blocked on a Sprite. The server's [`worker`] picks the jobs up.
+// r[impl checks.post-receive] enqueues one job per updated non-meta branch with a non-zero tip, records `queued` immediately, does not run checks itself, returns fast
+// r[impl nonfunctional.push-latency] - writes job files and returns without running any check
pub fn post_receive() -> Result<(), String> {
let repo = std::env::current_dir().map_err(|e| format!("cannot resolve repository: {e}"))?;
@@ -176,6 +178,8 @@
/// every other repository's checks; isolating them by repository keeps a slow
/// repository's backlog from blocking the rest. Jobs for *one* repository stay
/// serialized so concurrent runs never collide in its single Sprite.
+// r[impl checks.worker] persistent worker drains the queue; jobs grouped by repo, serialized per-repo, concurrent across repos
+// r[impl nonfunctional.concurrency] - check jobs run via `spawn_blocking`, never on the async runtime's own thread
pub async fn worker(queue: PathBuf, live: LiveRegistry) {
if let Err(e) = std::fs::create_dir_all(&queue) {
eprintln!("checks: could not create queue directory {queue:?}: {e}");
@@ -250,6 +254,7 @@
/// re-validation) finalizes the run as `error` rather than leaving it stuck at
/// `running`, then returns `Err`. Returns `Ok` even when a check fails — a
/// failing check is a recorded result, not an error.
+// r[impl checks.worker] checks settle in topological order, skipped-on-failed-dependency semantics, composite outcome derivation, re-validates the DAG before running, finalizes as error if invalid
fn process_job(job: &Job, live: &LiveRegistry) -> Result<(), String> {
let runnable = checks::load(&job.repo).map_err(|e| format!("could not read checks: {e}"))?;
if runnable.is_empty() {
@@ -351,6 +356,7 @@
/// A composite check's status, derived from its dependencies' settled
/// statuses: `pass` when everything passed, `fail` when anything failed or
/// errored, `skipped` when nothing failed but something was skipped.
+// r[impl checks.worker] composite outcome derivation from dependency statuses
fn derive_composite(deps: &[Status]) -> Status {
if deps.iter().all(|status| *status == Status::Pass) {
Status::Pass
@@ -374,6 +380,7 @@
/// Mark every check in `outcomes` `error` and record it — the terminal state for
/// a run the worker could not carry out.
+// r[impl checks.worker] finalizes a run as error when it cannot be carried out
fn finalize_error(repo: &Path, new: ObjectId, outcomes: &mut [RunOutcome]) {
for outcome in outcomes.iter_mut() {
outcome.status = Status::Error;
@@ -392,6 +399,7 @@
/// Write a job for `update` into `queue` as a three-line file (`repo`, new oid,
/// ref). The file is written under a `.tmp` name and renamed into place so the
/// worker never observes a half-written job.
+// r[impl checks.post-receive] job files are written to a tmp name then renamed into place
fn enqueue(queue: &Path, repo: &Path, update: &Update) -> Result<(), String> {
std::fs::create_dir_all(queue)
.map_err(|e| format!("could not create queue directory {queue:?}: {e}"))?;
@@ -458,6 +466,7 @@
///
/// Shared with [`crate::web`]'s debug-session broker, which targets the same
/// persistent per-repo Sprite a check run used.
+// r[impl checks.sandbox] each check runs in a Fly.io Sprite per repo
pub(crate) fn sprite_name(repo: &Path) -> String {
let stem = repo
.file_name()
@@ -485,6 +494,8 @@
/// token per call, so without this it reports "no organizations configured"
/// even with the token in the environment. `auth setup` is idempotent, so it is
/// run on every push to keep the steady state self-healing.
+// r[impl checks.sandbox] configures the sprite CLI from SPRITES_TOKEN
+// r[impl compat.sprite] - `sprite auth setup --token` from SPRITES_TOKEN, run per-push since credentials persist to a config file
pub(crate) fn ensure_auth() -> Result<(), String> {
let token = std::env::var("SPRITES_TOKEN")
.ok()
@@ -507,6 +518,8 @@
/// fails when the Sprite is already there, which is the steady state once the
/// first push has run, so its failure is tolerated and surfaces only later if
/// the Sprite turns out to be unreachable.
+// r[impl checks.sandbox] creates the repository's per-repo Sprite
+// r[impl compat.sprite] - `sprite create` on PATH at runtime
pub(crate) fn ensure_sprite(sprite: &str) -> Result<(), String> {
let _existing = Command::new("sprite")
.args(["create", "--skip-console", sprite])
@@ -519,6 +532,9 @@
/// previous contents while leaving the rest of the persistent filesystem (build
/// caches and the like) intact. `git archive` emits the tree as a tar that the
/// Sprite unpacks over stdin.
+// r[impl checks.sandbox] syncs the pushed tree via git archive | tar -x
+// r[impl compat.sprite] - `sprite exec` on PATH at runtime
+// r[impl compat.git] - invokes `git archive` as an external subprocess
fn sync_tree(repo: &Path, sprite: &str, new: ObjectId) -> Result<(), String> {
let archive = Command::new("git")
.arg("-C")
@@ -557,6 +573,8 @@
/// failed resolution (the named ref does not exist) is the one place
/// `checks::order` could not have caught it, since `refs/meta/toolchains/*`
/// is a different namespace than the check set itself.
+// r[impl checks.toolchains] existence of a named toolchain is checked server-side at job time
+// r[impl checks.sandbox] resolves toolchain bin trees and extracts into a hash-keyed dir, with caching
fn resolve_toolchains(
repo: &Path,
sprite: &str,
@@ -610,6 +628,7 @@
/// `bin` directories, declared order first (so the first-listed toolchain's
/// `bin` wins on a name collision); a check with no toolchains is returned
/// unchanged.
+// r[impl checks.sandbox] PATH prefixing of resolved toolchains in declaration order
fn activate(command: &str, toolchains: &[String], dirs: &HashMap<String, String>) -> String {
if toolchains.is_empty() {
return command.to_owned();
@@ -629,6 +648,7 @@
/// persistent filesystem is the cache. Checked before running `git archive`
/// so an already-cached toolchain never streams its (potentially large)
/// contents through a pipe the Sprite has no reason to read.
+// r[impl checks.sandbox] extracts an embedded toolchain into a hash-keyed dir, cached
fn sync_toolchain(repo: &Path, sprite: &str, tree: ObjectId) -> Result<(), String> {
let dir = format!("{TOOLCHAINS_DIR}/{tree}");
let cached = Command::new("sprite")
@@ -687,6 +707,7 @@
/// mirroring `git_toolchain::export`'s local equivalent: downloading through
/// the server first and streaming the bytes in would defeat the point of not
/// storing them.
+// r[impl checks.sandbox] extracts a downloaded toolchain into a hash-keyed dir, cached
fn sync_downloaded_toolchain(
sprite: &str,
key: &str,
@@ -738,6 +759,7 @@
/// killed and recorded `error` rather than wedging the worker (and with it every
/// other repository's checks) on the one blocking-pool thread the queue drains
/// on.
+// r[impl checks.outcomes] timeout at 30 minutes finalizes a check as error
const CHECK_TIMEOUT: Duration = Duration::from_secs(30 * 60);
/// The fixed size a check's recorded terminal session runs at. Nothing
@@ -769,6 +791,7 @@
/// as the recorded `recording`, not a separate representation of the same
/// output. Returns the check's outcome; a check that exceeds [`CHECK_TIMEOUT`]
/// or cannot be captured is [`Status::Error`].
+// r[impl compat.sprite] - `sprite exec` (with `--tty`) on PATH at runtime
fn run_one(sprite: &str, name: &str, command: &str, live: &Arc<StdMutex<String>>) -> RunResult {
let start = Instant::now();
lock(live).push_str(&asciicast_header());
@@ -926,6 +949,7 @@
use super::*;
+ // r[verify checks.post-receive] only updated non-meta branches with a non-zero tip are enqueued
#[test]
fn parse_updates_keeps_content_branches_only() {
let new = "1111111111111111111111111111111111111111";
@@ -941,12 +965,14 @@
assert_eq!(refs, vec!["refs/heads/main", "refs/heads/feature"]);
}
+ // r[verify checks.sandbox]
#[test]
fn activate_leaves_a_toolchain_free_command_unchanged() {
let dirs = HashMap::new();
assert_eq!(activate("cargo test", &[], &dirs), "cargo test");
}
+ // r[verify checks.sandbox] PATH prefixing in declaration order
#[test]
fn activate_prefixes_path_in_declared_order() {
let mut dirs = HashMap::new();
@@ -959,6 +985,7 @@
);
}
+ // r[verify checks.sandbox]
#[test]
fn activate_skips_a_toolchain_missing_from_dirs() {
let dirs = HashMap::new();
@@ -969,6 +996,7 @@
);
}
+ // r[verify checks.worker] composite outcome derivation
#[test]
fn composite_status_derives_from_its_dependencies() {
assert_eq!(
@@ -992,6 +1020,7 @@
assert_eq!(derive_composite(&[]), Status::Pass);
}
+ // r[verify checks.worker] jobs grouped by repo for per-repo draining
#[test]
fn pending_jobs_groups_by_repo_and_drops_malformed() {
let queue = tempfile::tempdir().unwrap();
crates/git-ents-server/src/http.rs
@@ -19,11 +19,13 @@
const CGI_HEADER_SEP: &[u8] = b"\r\n\r\n";
+// r[impl deploy.health]
/// A liveness probe (and the `/` root) that does not touch git.
pub async fn health() -> &'static str {
"ok"
}
+// r[impl protocol.routing] - dispatches a GET to the web UI or the git backend by path/query
/// Serve a GET: the HTML web UI for browser requests, or `git http-backend` for
/// a git wire-protocol read (the ref advertisement or a dumb-HTTP object fetch).
pub async fn get_request(State(state): State<AppState>, uri: Uri, headers: HeaderMap) -> Response {
@@ -53,6 +55,7 @@
.await
}
+// r[impl protocol.routing] - dispatches a POST to the web UI or the git backend by path
/// Serve a POST: always a git smart-HTTP RPC (`git-upload-pack` for fetch or
/// `git-receive-pack` for push). The browser UI never POSTs, so there is no web
/// branch here.
@@ -86,11 +89,16 @@
.await
}
+// r[impl protocol.routing]
/// Whether a POST is a git smart-HTTP RPC rather than a browser form submission.
fn is_git_post(path_info: &str) -> bool {
path_info.ends_with("/git-upload-pack") || path_info.ends_with("/git-receive-pack")
}
+// r[impl protocol.git] - delegates the git wire protocol to `git http-backend` as a CGI subprocess
+// r[impl compat.git] - invokes `git http-backend` as an external subprocess, GIT_PROJECT_ROOT on PATH
+// r[impl compat.cgi] - populates the CGI environment before spawning `git http-backend`
+// r[impl nonfunctional.concurrency] - writes stdin and drains stdout concurrently to avoid deadlock
/// Hand a git wire-protocol request to `git http-backend` and reply with its
/// output. A receive-pack request (push) auto-creates its bare repository before
/// the backend runs and reconciles `HEAD` after a successful push.
@@ -222,6 +230,8 @@
build_response(&stdout)
}
+// r[impl compat.git] - overrides passed via GIT_CONFIG_* rather than `git -c` so they reach receive-pack/pre-receive
+// r[impl auth.nonce]
/// The `git` config overrides applied to every backend invocation. Empty until
/// push authentication is wired: a seed enables the signed-push nonce, and the
/// hooks directory points the backend at the `pre-receive` verifier.
@@ -241,6 +251,7 @@
overrides
}
+// r[impl compat.cgi] - parses the CGI response format (header block, blank line, body) into HTTP
/// Translate a CGI response (header block, blank line, body) into HTTP.
fn build_response(stdout: &[u8]) -> Response {
let (header_block, body) = match find_subsequence(stdout, CGI_HEADER_SEP) {
@@ -282,6 +293,7 @@
/// Shared by the push gateway and the web UI's routing/discovery.
pub(crate) const MAX_REPO_DEPTH: usize = 3;
+// r[impl protocol.routing] - browse routes win over dumb-HTTP path heuristics, service requests never stolen
/// Whether a GET should be answered with the HTML web UI rather than handed to
/// `git http-backend`.
///
@@ -298,6 +310,7 @@
!is_wire || (is_browse && !is_service_request(path, query))
}
+// r[impl protocol.routing]
/// Whether `path`/`query` is an unambiguous smart-HTTP service request (the
/// ref advertisement or an upload-pack/receive-pack RPC).
fn is_service_request(path: &str, query: &str) -> bool {
@@ -325,6 +338,8 @@
.find_map(|pair| pair.strip_prefix("service="))
}
+// r[impl namespace.auto-create] - serializes creation behind `init_lock` so concurrent first pushes cannot race
+// r[impl storage.bare] - creates the bare repo automatically on first push
/// Ensure `repo` exists as a bare repository, creating it on first push.
///
/// Holds [`AppState::init_lock`] across the whole check-and-create so two
@@ -360,6 +375,7 @@
})
}
+// r[impl namespace.path] - refuses a path nested inside an existing repository
/// The ancestor of `repo` (below `data_dir`) that is itself a bare repository,
/// if any. Used to refuse creating a repository inside another one.
fn enclosing_repo(data_dir: &Path, repo: &Path) -> Option<PathBuf> {
@@ -383,6 +399,7 @@
path.join("HEAD").is_file() && path.join("objects").is_dir()
}
+// r[impl namespace.path] - one to three segments, rejected before `git http-backend` is invoked
/// The target repository of a push, as a validated path relative to the data
/// directory, or `None` if the request does not name an acceptable repository.
///
@@ -406,6 +423,7 @@
Some(segments.into_iter().collect())
}
+// r[impl namespace.path] - ASCII alphanumerics plus `.`, `_`, `-`; no leading `.`, no separator
/// Whether a single path component is a safe repository/namespace name.
///
/// Rejecting any leading `.` rules out `.`, `..`, and hidden directories; the
@@ -419,6 +437,8 @@
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
}
+// r[impl namespace.auto-create] - `git init --bare` + `http.receivepack = true` on first push
+// r[impl compat.git] - invokes `git init --bare` as an external subprocess
/// Create a bare repo that accepts pushes over smart-HTTP.
async fn init_bare_repo(repo: &Path) -> std::io::Result<()> {
let init = Command::new("git")
@@ -450,6 +470,8 @@
Ok(())
}
+// r[impl namespace.auto-create] - adopts a pushed branch (preferring main, then master) so a fresh clone checks out content
+// r[impl compat.git] - invokes `git for-each-ref`/`git symbolic-ref` as external subprocesses
/// Point `HEAD` at a real branch when it dangles after a push.
///
/// Best-effort: the push already succeeded, so failures here are ignored.
@@ -561,6 +583,7 @@
#[case("a/b", false)]
#[case("a b", false)]
#[case("a%2eb", false)]
+ // r[verify namespace.path]
fn validates_segments(#[case] segment: &str, #[case] expected: bool) {
assert_eq!(valid_segment(segment), expected);
}
@@ -579,11 +602,13 @@
}
}
+ // r[verify auth.nonce]
#[test]
fn backend_config_is_empty_without_authentication() {
assert!(backend_config(&state(None, None)).is_empty());
}
+ // r[verify auth.nonce]
#[test]
fn backend_config_injects_nonce_seed_and_hooks_path() {
assert_eq!(
@@ -605,6 +630,7 @@
#[case("/../etc/git-receive-pack", None)]
#[case("/.ssh/git-receive-pack", None)]
#[case("/git-receive-pack", None)]
+ // r[verify namespace.path]
fn extracts_repo_path(#[case] path: &str, #[case] expected: Option<&str>) {
assert_eq!(repo_path(path).as_deref(), expected.map(Path::new));
}
@@ -617,6 +643,7 @@
#[case("/repo.git/info/refs", "x=service=git-receive-pack", false)]
#[case("/repo.git/info/refs", "a=b&service=git-receive-pack", true)]
#[case("/repo.git/objects/info/packs", "", false)]
+ // r[verify protocol.routing]
fn detects_pushes(#[case] path: &str, #[case] query: &str, #[case] expected: bool) {
assert_eq!(is_receive_pack(path, query), expected);
}
@@ -638,6 +665,7 @@
#[case("/repo/info/refs", "service=git-upload-pack", false)]
#[case("/repo/git-upload-pack", "", false)]
#[case("/repo/objects/12/abcdef", "", false)]
+ // r[verify protocol.routing]
fn routes_browser_gets(#[case] path: &str, #[case] query: &str, #[case] expected: bool) {
assert_eq!(is_web_get(path, query), expected);
}
crates/git-ents-server/src/lib.rs
@@ -67,6 +67,7 @@
pub checks_queue: Option<PathBuf>,
}
+// r[impl server.embeddable] - hooks are subcommands of the server, not separate programs
/// Subcommands that run instead of serving HTTP.
#[derive(Facet, Debug)]
#[repr(u8)]
@@ -112,6 +113,8 @@
std::env::var(key).ok().filter(|value| !value.is_empty())
}
+// r[impl server.embeddable] - shared entry point run by both the standalone binary and `git ents server`
+// r[impl deploy.fly] - env-var-first config, since that is how this server is configured on Fly.io
/// Run the server: dispatch `pre-receive`/`post-receive`, or serve HTTP.
/// `args`' flags win over their matching environment variable, which in turn
/// wins over the hardcoded default.
@@ -185,11 +188,14 @@
state.live_runs.clone(),
));
+ // r[impl protocol.routing] - one listener serves both the git wire protocol and the web UI, routed by path
+ // r[impl deploy.health] - `/healthz` is wired ahead of the catch-all so it never touches a repository
// The git smart-HTTP protocol streams whole packfiles through the request
// body, so the default 2 MiB cap would reject any non-trivial push.
let app = Router::new()
.route("/healthz", get(http::health))
.route("/", get(http::get_request))
+ // r[impl checks.debug] debug shell brokered over WebSocket at /_debug/<repo>
.route("/_debug/{*path}", get(web::handshake))
.route("/{*path}", get(http::get_request).post(http::post_request))
.layer(DefaultBodyLimit::disable())
crates/git-ents-server/src/main.rs
@@ -14,9 +14,11 @@
builtins: FigueBuiltins,
}
+// r[impl server.embeddable] - the standalone binary runs the same `git_ents_server::run` as `git ents server`
fn main() -> ExitCode {
let raw_args: Vec<String> = std::env::args().skip(1).collect();
+ // r[impl deploy.fly]
// With zero CLI tokens (how Fly.io always runs this image — every setting
// arrives via env vars, which `git_ents_server::run` reads itself),
// `#[facet(flatten)]`'s all-`Option` `args` never gets a single populated
crates/git-ents-server/src/markdown.rs
@@ -19,6 +19,8 @@
/// Render Markdown `source` to an embedded HTML fragment, with the tables,
/// footnotes, strikethrough, and task-list extensions people expect from
/// forge-flavored Markdown.
+// r[impl web.render-registry] - Markdown's entry in the MIME-keyed registry
+// r[impl web.syntax-highlight] - Markdown rendered to HTML
pub(crate) fn to_html(source: &str) -> String {
let options = Options::ENABLE_TABLES
| Options::ENABLE_FOOTNOTES
crates/git-ents-server/src/render.rs
@@ -29,6 +29,7 @@
/// Render `source` (declared or inferred as `mime`) to an embedded HTML
/// fragment. Unrecognized MIME types fall through to an escaped `<pre>`
/// block rather than an error.
+// r[impl web.render-registry] - HTML path shared by the web UI
pub fn to_html(mime: &str, source: &str) -> String {
match mime {
"text/asciidoc" => asciidoc::to_html(source),
@@ -41,6 +42,7 @@
/// Render `source` (declared or inferred as `mime`) to plain text, for
/// terminal output. Unrecognized MIME types fall through to `source`
/// verbatim.
+// r[impl web.render-registry] - plain-text path shared by the CLI
pub fn to_text(mime: &str, source: &str) -> String {
match mime {
"text/asciidoc" => asciidoc::to_text(source),
crates/git-ents-server/src/verify.rs
@@ -21,11 +21,14 @@
/// Verify the push git is about to apply, returning `Ok(())` to accept it or
/// `Err(reason)` to reject it. The push certificate is read from the
/// environment git populates for the hook.
+// r[impl auth.signed-push]
+// r[impl compat.openssh-signed-push] - reads GIT_PUSH_CERT / GIT_PUSH_CERT_NONCE_STATUS, the env vars git populates for `pre-receive`
pub fn pre_receive() -> Result<(), String> {
let repo = std::env::current_dir().map_err(|e| format!("cannot resolve repository: {e}"))?;
let store = git_store::Store::open(&repo).map_err(|e| format!("cannot open store: {e}"))?;
let members = members::load_all_with(&store)
.map_err(|e| format!("could not read authorized signers: {e}"))?;
+ // r[impl auth.bootstrap]
if members.is_empty() {
// No trust list pushed yet: stay open so the first signer can be added.
// Revocation is keyed on member refs existing, so revoking every member's
@@ -35,6 +38,7 @@
}
let revoked = revocations::fingerprints_with(&store)
.map_err(|e| format!("could not read revocations: {e}"))?;
+ // r[impl auth.signed-push] trust set is the live member set minus revoked fingerprints
let authorized = members::without_revoked(members, &revoked);
let ref_updates = read_ref_updates()?;
@@ -43,6 +47,7 @@
.ok_or_else(|| {
"this repository requires a signed push: rerun with `git push --signed`".to_owned()
})?;
+ // r[impl auth.signed-push] nonce status must be OK
if env("GIT_PUSH_CERT_NONCE_STATUS").as_deref() != Some("OK") {
return Err("push certificate nonce was missing or stale".to_owned());
}
@@ -94,6 +99,8 @@
/// Split the certificate into its signed payload and SSH signature, then accept
/// it only when `ssh-keygen -Y verify` trusts the signature against one of the
/// authorized keys.
+// r[impl auth.signed-push] SSH signature block + ssh-keygen -Y verify against the trust set
+// r[impl compat.ssh-keygen] - `ssh-keygen -Y verify` against a runtime-written `allowed_signers` file
fn verify_certificate(authorized: &[Member], certificate: &str) -> Result<(), String> {
const MARKER: &str = "-----BEGIN SSH SIGNATURE-----";
let split = certificate
@@ -153,6 +160,7 @@
std::env::var(key).ok()
}
+// r[impl compat.git] - invokes `git cat-file` as an external subprocess
/// Read the blob `oid` from `repo` as text.
fn cat_blob(repo: &Path, oid: &str) -> Result<String, String> {
let output = Command::new("git")
crates/git-ents-server/tests/pre_receive.rs
@@ -220,6 +220,9 @@
.success()
}
+// r[verify auth.signed-push]
+// r[verify compat.ssh-keygen]
+// r[verify compat.openssh-signed-push]
#[test]
fn accepts_a_push_signed_by_an_authorized_key() {
let base = unique_dir("accept");
@@ -234,6 +237,8 @@
std::fs::remove_dir_all(&base).ok();
}
+// r[verify auth.signed-push]
+// r[verify compat.ssh-keygen]
#[test]
fn rejects_a_push_signed_by_an_unknown_key() {
let base = unique_dir("unknown");
@@ -249,6 +254,8 @@
std::fs::remove_dir_all(&base).ok();
}
+// r[verify auth.signed-push]
+// r[verify compat.openssh-signed-push]
#[test]
fn rejects_an_unsigned_push_when_signers_exist() {
let base = unique_dir("unsigned");
@@ -326,6 +333,7 @@
std::fs::remove_dir_all(&base).ok();
}
+// r[verify auth.signed-push] trust set excludes revoked fingerprints
#[test]
fn rejects_a_push_signed_by_a_revoked_key() {
// The key is a valid, in-window member, but its fingerprint is on the
@@ -344,6 +352,7 @@
std::fs::remove_dir_all(&base).ok();
}
+// r[verify auth.bootstrap]
#[test]
fn accepts_any_push_before_signers_are_configured() {
let base = unique_dir("bootstrap");
crates/git-ents-server/tests/server.rs
@@ -13,6 +13,9 @@
use rstest::rstest;
+// r[verify web.server-rendered] - GET / returns a rendered page, no client required
+// r[verify web.index] - GET / renders the repository index
+// r[verify server.embeddable] - exercises the standalone `git-ents-server` binary
#[test]
fn responds_to_requests() {
let port = free_port();
@@ -41,6 +44,9 @@
let _wait = child.wait();
}
+// r[verify storage.bare] - a first push auto-creates the bare repo, and it survives to be cloned
+// r[verify namespace.auto-create]
+// r[verify protocol.git]
#[test]
fn push_then_clone_round_trip() {
let data = tempfile::tempdir().unwrap();
@@ -79,6 +85,8 @@
assert_eq!(pushed, cloned, "cloned HEAD must match pushed HEAD");
}
+// r[verify namespace.path] - `org/repo` and `org/team/repo` are accepted multi-segment paths
+// r[verify namespace.auto-create]
#[rstest]
#[case("org/repo")]
#[case("org/team/repo")]
@@ -120,6 +128,7 @@
);
}
+// r[verify namespace.auto-create] - a colliding creation is refused, not raced
#[rstest]
#[case("org/repo.git/deep.git")] // nested inside an existing repository
#[case("org")] // already exists as a namespace
crates/git-ents-server/tests/web_edit.rs
@@ -20,6 +20,10 @@
const BIN: &str = env!("CARGO_BIN_EXE_git-ents-server");
const LOGIN_NAMESPACE: &str = "git.ents.cloud";
+// r[verify web.tabs] - Settings tab is editable in the browser by a signed-in member
+// r[verify web.auth.edit] - lands as a real signed push authored by the member, committed by the server
+// r[verify web.auth.challenge] - signs in via the challenge/signature flow first
+// r[verify web.auth.session] - the session cookie authorizes the edit
#[test]
fn a_member_edits_settings_through_the_browser() {
let env = Server::start();
@@ -81,6 +85,7 @@
);
}
+// r[verify web.auth.session] - a state-changing POST without the matching CSRF token is refused
#[test]
fn an_edit_without_a_valid_csrf_token_is_refused() {
let env = Server::start();
@@ -104,6 +109,7 @@
);
}
+// r[verify web.auth.edit] - require_admin_registered refuses a self-attested member's settings edit
#[test]
fn a_self_attested_member_is_refused_a_settings_edit() {
let env = Server::start();
@@ -143,6 +149,7 @@
);
}
+// r[verify web.auth.challenge] - a session proves key control, not membership or authority
#[test]
fn a_non_member_is_not_offered_an_edit_form() {
let env = Server::start();
@@ -167,6 +174,7 @@
);
}
+// r[verify web.auth.challenge] - a signature that does not match the pasted public key must not open a session
#[test]
fn a_signature_that_does_not_match_the_public_key_is_refused() {
let env = Server::start();
crates/git-ents/src/debug_session.rs
@@ -11,6 +11,7 @@
/// Open a debug session at `url`, authenticated with the session `token`
/// stored by `git ents login`.
+// r[impl checks.debug] `git ents checks debug` CLI side of the interactive shell
pub(crate) async fn run(url: &str, token: &str) -> Result<(), String> {
let mut request = url
.into_client_request()
@@ -37,6 +38,7 @@
/// straight to stdout. A dedicated thread reads stdin because raw terminal
/// input has no natural way to interrupt a blocking read when the session
/// ends from the other side.
+// r[impl checks.debug] terminal resize forwarded as control frames
async fn pump<S, R>(sink: &mut S, source: &mut R) -> Result<(), String>
where
S: futures_util::Sink<Message> + Unpin,
crates/git-ents/src/interactive.rs
@@ -6,6 +6,7 @@
use std::io::IsTerminal as _;
+// r[impl cli.interactive] - TTY detection gating prompting vs. failing fast
/// Whether prompting is possible: both stdin and stdout are a terminal.
#[must_use]
pub fn available() -> bool {
@@ -16,6 +17,7 @@
/// error naming `message` when not, so a script never hangs on a missing
/// argument. An empty string — whether passed explicitly or typed at the
/// prompt — is rejected the same as a missing value.
+// r[impl cli.interactive] - prompt for a required field at a TTY; error naming the field otherwise
pub fn text_or(existing: Option<String>, message: &str) -> Result<String, String> {
if let Some(value) = existing {
return if value.is_empty() {
@@ -41,6 +43,7 @@
/// `existing`, or an optional text prompt for `message` when interactive —
/// an empty reply is `None`. Non-interactive with no `existing` value stays
/// `None` rather than erroring, since the field is optional.
+// r[impl cli.interactive] - prompt for an optional field at a TTY, `None` otherwise
pub fn optional_text_or(existing: Option<String>, message: &str) -> Result<Option<String>, String> {
if existing.is_some() {
return Ok(existing);
@@ -56,6 +59,7 @@
/// A `Select` prompt among `options`, run only when interactive; `default`
/// otherwise.
+// r[impl cli.interactive] - prompt with a selection at a TTY, `default` otherwise
pub fn select_or(message: &str, options: &[&str], default: usize) -> Result<usize, String> {
if !available() {
return Ok(default);
crates/git-ents/src/main.rs
@@ -43,6 +43,11 @@
builtins: FigueBuiltins,
}
+// r[impl cli.members] - top-level subcommand
+// r[impl cli.account-checks] - top-level subcommands
+// r[impl cli.toolchains] - top-level subcommand
+// r[impl cli.comments] - top-level subcommand
+// r[impl cli.login] - top-level subcommand
#[derive(Facet)]
#[repr(u8)]
enum Top {
@@ -87,6 +92,7 @@
Server(git_ents_server::Args),
}
+// r[impl cli.members] - members subcommands (list/add/remove/revoke/unrevoke/check/setup)
#[derive(Facet)]
#[repr(u8)]
enum Action {
@@ -166,6 +172,7 @@
},
}
+// r[impl cli.account-checks] - `account create`
#[derive(Facet)]
#[repr(u8)]
enum AccountAction {
@@ -185,6 +192,7 @@
},
}
+// r[impl cli.account-checks] - `checks list`/`add`/`remove`
#[derive(Facet)]
#[repr(u8)]
enum ChecksAction {
@@ -227,6 +235,7 @@
Runs,
}
+// r[impl cli.toolchains] - `import`/`list`/`export`/`remove`
#[derive(Facet)]
#[repr(u8)]
enum ToolchainAction {
@@ -307,6 +316,7 @@
},
}
+// r[impl cli.comments] - `add`/`list`/`show`/`remove`
#[derive(Facet)]
#[repr(u8)]
enum CommentAction {
@@ -405,6 +415,7 @@
}
}
+// r[impl cli.members] - dispatch
fn run_members(action: Action, remote: &str) -> Result<(), String> {
match action {
Action::Setup { key, local } => setup(key.as_deref(), local),
@@ -437,6 +448,7 @@
}
}
+// r[impl cli.account-checks] - `account create` dispatch
fn run_account(action: AccountAction, remote: &str) -> Result<(), String> {
match action {
AccountAction::Create {
@@ -447,6 +459,7 @@
}
}
+// r[impl cli.account-checks] - `checks` dispatch
fn run_checks(action: ChecksAction, remote: &str) -> Result<(), String> {
match action {
ChecksAction::List => list::<Check>(remote),
@@ -492,6 +505,7 @@
Ok(())
}
+// r[impl cli.toolchains] - dispatch
fn run_toolchain(action: ToolchainAction, remote: &str) -> Result<(), String> {
match action {
ToolchainAction::Import {
@@ -521,6 +535,7 @@
/// interactive terminal, unless `from` names a recipe (`registry::resolve`)
/// to derive `bin`/`src`/`license`/`version`/`platform` from instead;
/// explicit flags still win over a recipe's values.
+// r[impl cli.toolchains] - `import` (including `--from`/`--embed`)
#[expect(clippy::too_many_arguments, reason = "one flag per import field")]
fn toolchain_import(
name: Option<String>,
@@ -607,6 +622,7 @@
Ok(())
}
+// r[impl cli.toolchains] - `list`
/// Print every toolchain configured on `remote` as
/// `<name> <bin> <version> <platform> <license>`.
fn toolchain_list(remote: &str) -> Result<(), String> {
@@ -659,6 +675,8 @@
Ok(())
}
+// r[impl cli.toolchains] - `export`
+// r[impl cli.remote-admin] - `toolchain export` is read-only
/// Export `remote`'s toolchain `name` to `dest`. Read-only per
/// `cli.remote-admin`: fetches the toolchain's tree but never pushes.
fn toolchain_export(name: &str, dest: &str, remote: &str) -> Result<(), String> {
@@ -693,6 +711,7 @@
Ok(())
}
+// r[impl cli.toolchains] - `remove`
/// Remove toolchain `name` on `remote`, deleting its ref and pushing the
/// update.
fn toolchain_remove(name: &str, remote: &str) -> Result<(), String> {
@@ -704,6 +723,7 @@
Ok(())
}
+// r[impl cli.comments] - dispatch
fn run_comment(action: CommentAction, remote: &str) -> Result<(), String> {
match action {
CommentAction::Add {
@@ -723,6 +743,7 @@
/// record it at `refs/meta/comments/<id>` authored as the configured git
/// identity, and push it. Prompts for the path and body left `None` when run
/// at an interactive terminal.
+// r[impl cli.comments] - `add`
fn comment_add(
path: Option<String>,
body: Option<String>,
@@ -753,6 +774,8 @@
Ok(())
}
+// r[impl cli.comments] - `list`
+// r[impl cli.remote-admin] - `comment list` is read-only
/// List every comment on `remote` as `<id> <author> <location> <body>`,
/// with each anchor projected onto `rev`.
fn comment_list(remote: &str, rev: &str) -> Result<(), String> {
@@ -774,6 +797,8 @@
Ok(())
}
+// r[impl cli.comments] - `show`
+// r[impl cli.remote-admin] - `comment show` is read-only
/// Show the comment `id` (or a unique prefix): who wrote and last edited it,
/// where it was anchored, where that sits on `rev`, the anchored text, and
/// the body.
@@ -827,6 +852,7 @@
Ok(())
}
+// r[impl cli.comments] - `remove`
/// Remove the comment `id` (or a unique prefix) on `remote`, deleting its ref
/// and pushing the deletion.
fn comment_remove(id: &str, remote: &str) -> Result<(), String> {
@@ -971,6 +997,8 @@
}
}
+// r[impl cli.account-checks] - `checks list`
+// r[impl cli.remote-admin] - a `Set` listing is read-only
/// Print each entry of the set `S` on `remote` as `<key> <value>`.
fn list<S: Set>(remote: &str) -> Result<(), String> {
let repo = repo()?;
@@ -986,6 +1014,7 @@
Ok(())
}
+// r[impl cli.account-checks] - `checks remove`
/// Drop the entry keyed `key` from the set `S` on `remote` and push the update.
fn remove<S: Set>(key: &str, remote: &str) -> Result<(), String> {
let repo = repo()?;
@@ -1010,6 +1039,7 @@
/// unset when run at an interactive terminal. The whole set is validated as a
/// dependency graph (`checks::order`) before it is stored, so a cycle or a
/// dangling dependency never lands on the remote.
+// r[impl cli.account-checks] - `checks add`
fn add_check(
name: Option<String>,
command: Option<String>,
@@ -1075,6 +1105,8 @@
/// (SSH-format signatures, the key, and "sign when the server asks" so pushes
/// elsewhere are untouched). Writes global config by default, since the setup
/// is per-machine.
+// r[impl cli.members] - `setup`
+// r[impl auth.client-setup]
fn setup(key: Option<&Path>, local: bool) -> Result<(), String> {
let scope = if local { "--local" } else { "--global" };
let signing_key = match key {
@@ -1102,6 +1134,7 @@
/// Ensure a usable SSH key exists at `path`, returning the public-key path to
/// record in `user.signingkey`. Generates an ed25519 keypair when neither the
/// key nor its `.pub` is present; derives a missing `.pub` from the private key.
+// r[impl cli.key-resolution] - `members setup` may generate a new `~/.ssh/id_ed25519`
fn ensure_key(path: &Path) -> Result<String, String> {
let (private, public) = key_paths(path);
if public.exists() {
@@ -1220,6 +1253,8 @@
/// `cert-authority` line per pinned-CA member — as
/// `<username>[/<fingerprint>] <label><window>`, flagging keys on the
/// `refs/meta/revoked` deny list as `[revoked]`.
+// r[impl cli.members] - `list`
+// r[impl cli.remote-admin] - `members list` is read-only
fn members_list(remote: &str) -> Result<(), String> {
let repo = repo()?;
sync_namespace(remote, MEMBER_NS)?;
@@ -1258,6 +1293,7 @@
/// Add `fingerprint` to `remote`'s `refs/meta/revoked` deny list and push the
/// update, so the key is refused before its window would expire.
+// r[impl cli.members] - `revoke`
fn members_revoke(fingerprint: &str, remote: &str, reason: String) -> Result<(), String> {
if !looks_like_fingerprint(fingerprint) {
return Err(format!(
@@ -1269,6 +1305,7 @@
// Revoking your own key fails closed against you too: if it is the last key
// that authorizes your pushes, you cannot even push the un-revoke. Warn
// before locking yourself out.
+ // r[impl cli.members] - confirmation before revoking the operator's own last key
if own_fingerprint().is_some_and(|own| own == fingerprint)
&& !confirm(&format!(
"{fingerprint} is your own signing key; \
@@ -1298,6 +1335,7 @@
/// Remove `fingerprint` from `remote`'s `refs/meta/revoked` deny list and push
/// the update.
+// r[impl cli.members] - `unrevoke`
fn members_unrevoke(fingerprint: &str, remote: &str) -> Result<(), String> {
let repo = repo()?;
let expected = sync(remote, REVOKED_REF)?;
@@ -1317,6 +1355,7 @@
/// either is already set or the terminal is non-interactive, so
/// `--key`/`--cert-authority` and scripted runs are unchanged; otherwise
/// prompts for which kind of trust to add.
+// r[impl cli.interactive] - prompt for which kind of trust to add when unset at a TTY
fn resolve_trust(
key: Option<PathBuf>,
cert_authority: Option<PathBuf>,
@@ -1344,6 +1383,7 @@
clippy::too_many_arguments,
reason = "each argument is an independent, optional member field set from its own CLI flag"
)]
+// r[impl cli.members] - `add`
fn members_add(
username: Option<String>,
remote: &str,
@@ -1431,6 +1471,7 @@
/// Revoke the member `username` on `remote`, deleting its ref and pushing the
/// deletion. Removal here is a plain signed delete; quorum-gated removal is a
/// later server-side policy.
+// r[impl cli.members] - `remove`
fn members_remove(username: &str, remote: &str) -> Result<(), String> {
let refname = member_ref(username);
let expected =
@@ -1440,6 +1481,7 @@
Ok(())
}
+// r[impl cli.account-checks] - `account create`
/// Create or update this repository's account identity on `remote` and push it.
fn account_create(
username: Option<String>,
@@ -1485,6 +1527,7 @@
/// signature back — the same proof the browser login page collects by hand.
/// The returned session token is stored locally so `checks_debug` can reuse
/// it.
+// r[impl cli.login] - challenge fetch, sign, submit
fn login(remote: &str, key: Option<&Path>) -> Result<(), String> {
let (base, _repo_path) = remote_http_base(remote)?;
let private_key = signing_key_file(key)?;
@@ -1507,6 +1550,7 @@
/// Open an interactive, read-write shell in `remote`'s persistent checks
/// Sprite, brokered by the server over a WebSocket using the session
/// `login` stored.
+// r[impl checks.debug] `git ents checks debug`
fn checks_debug(remote: &str) -> Result<(), String> {
let (base, repo_path) = remote_http_base(remote)?;
let host = host_of(&base)?;
@@ -1521,6 +1565,7 @@
/// The path to the private half of the signing key to use: `key` verbatim, or
/// the path behind `user.signingkey`, resolved the same way `setup` does.
+// r[impl cli.key-resolution] - default to `user.signingkey`
fn signing_key_file(key: Option<&Path>) -> Result<PathBuf, String> {
match key {
Some(path) => Ok(key_paths(path).0),
@@ -1535,6 +1580,7 @@
/// Sign `nonce` under [`LOGIN_NAMESPACE`] with the private key at `path`,
/// returning the armored SSH signature. `ssh-keygen -Y sign` only writes a
/// signature next to a file it read, so the nonce is staged there first.
+// r[impl cli.login] - sign the fetched challenge with the configured key
fn sign_challenge(private_key: &Path, nonce: &str) -> Result<String, String> {
let dir = tempfile::tempdir().map_err(|error| format!("could not create temp dir: {error}"))?;
let data = dir.path().join("nonce");
@@ -1724,6 +1770,8 @@
}
}
+// r[impl cli.members] - `check`
+// r[impl cli.remote-admin] - `members check` is read-only
/// Report whether `key` is a member on `remote` and how this client is
/// configured.
fn check(remote: &str, key: Option<&Path>) -> Result<(), String> {
@@ -1764,6 +1812,7 @@
/// the current value, returning the remote's current object id (or `None` when
/// it has no such ref — for the signer set, the open bootstrap window). When the
/// remote has none, clear any stale local ref so the set reads empty.
+// r[impl cli.remote-admin] - fetch the relevant `refs/meta/*` ref before a mutating command edits it
fn sync(remote: &str, refname: &str) -> Result<Option<String>, String> {
let listing = ls_remote(remote, refname)?;
let oid = listing.split_whitespace().next().map(str::to_owned);
@@ -1779,6 +1828,7 @@
/// Mirror every ref under `remote`'s `namespace` (e.g. `refs/meta/member`) into
/// the local repository, pruning local refs the remote no longer has, so the
/// glob helpers see the remote's current set.
+// r[impl cli.remote-admin] - fetch a whole `refs/meta/*` namespace for read-only listing commands
fn sync_namespace(remote: &str, namespace: &str) -> Result<(), String> {
let refspec = format!("+{namespace}/*:{namespace}/*");
git_run(&["fetch", "--quiet", "--prune", remote, &refspec])
@@ -1790,6 +1840,8 @@
/// not exist). Pushing with `--force-with-lease` pinned to that value, plus
/// `--force-if-includes`, makes the update a clean compare-and-swap: it is
/// rejected rather than clobbering a set someone changed since the fetch.
+// r[impl cli.remote-admin] - signed push of an updated typed document back to the remote
+// r[impl cli.compare-and-swap] - `--force-with-lease=<ref>:<expected>` plus `--force-if-includes`; create pins the lease to the zero object id
fn push_signed(remote: &str, refname: &str, expected: Option<&str>) -> Result<(), String> {
let lease = format!(
"--force-with-lease={refname}:{}",
@@ -1801,6 +1853,8 @@
/// Delete `refname` on `remote`, signed per the client's config and pinned with
/// `--force-with-lease` to the `expected` tip so a member changed since the
/// fetch is not clobbered.
+// r[impl cli.remote-admin] - deletion is still a signed push
+// r[impl cli.compare-and-swap] - `--force-with-lease` pinned to the observed tip
fn push_delete(remote: &str, refname: &str, expected: &str) -> Result<(), String> {
let lease = format!("--force-with-lease={refname}:{expected}");
let refspec = format!(":{refname}");
@@ -1809,6 +1863,7 @@
/// Resolve the OpenSSH public key to operate on, defaulting to the key behind
/// `user.signingkey`.
+// r[impl cli.key-resolution] - default to `user.signingkey`, accepting an inline `key::` value
fn public_key(key: Option<&Path>) -> Result<String, String> {
match key {
Some(path) => read_public_key(path),
@@ -1825,6 +1880,7 @@
/// Read an OpenSSH public key from `path`, accepting either a `.pub` file or a
/// private key (whose public half is derived with `ssh-keygen -y`).
+// r[impl cli.key-resolution] - a `.pub` file, or a private key whose public half is derived with `ssh-keygen -y`
fn read_public_key(path: &Path) -> Result<String, String> {
if let Ok(contents) = std::fs::read_to_string(path)
&& looks_like_public_key(&contents)
@@ -1873,6 +1929,7 @@
/// The key's MD5 fingerprint in colon form (`aa:bb:…`). Colon-separated pairs
/// are filesystem-safe, unlike the slashes in a base64 SHA256 fingerprint that
/// would split the `members/<name>` entry into a subtree.
+// r[impl cli.key-resolution] - MD5 colon-form fingerprint
fn fingerprint(public_key: &str) -> Result<String, String> {
let scratch =
tempfile::tempdir().map_err(|error| format!("could not create temp dir: {error}"))?;
crates/git-ents/src/registry.rs
@@ -74,6 +74,7 @@
/// bytes; by default the recipe instead points at its distributor's own
/// hosted, hash-verified archives (see [`Bin::Components`]), sparing the
/// repository the toolchain's own bytes.
+// r[impl cli.toolchains] - `import --from <recipe> --spec <name>`
pub fn resolve(recipe: &str, spec: &str, embed: bool) -> Result<Resolved, String> {
match recipe {
"rustup" => rustup(spec, embed),
@@ -128,6 +129,7 @@
/// `src` is `<sysroot>/lib/rustlib/src/rust`, unstaged, when the `rust-src`
/// component is installed, else omitted, regardless of `embed`. Rust's own
/// toolchain is dual-licensed `MIT OR Apache-2.0`.
+// r[impl cli.toolchains] - `rustup` recipe: hosted archives by default, `--embed` for local bytes
fn rustup(spec: &str, embed: bool) -> Result<Resolved, String> {
let toolchain_arg = format!("+{spec}");
let sysroot = rustc(&toolchain_arg, &["--print", "sysroot"])?;
crates/git-store/src/lib.rs
@@ -129,6 +129,8 @@
}
}
+// r[impl storage.meta-ref]
+// r[impl nonfunctional.object-store] - object IO is opened on the common git directory, never a receive-pack quarantine
/// A repository's typed `refs/meta/*` store.
///
/// Refs are read and updated through the high-level [`gix`] API, while all
@@ -142,6 +144,7 @@
}
impl Store {
+ // r[impl nonfunctional.object-store] - opens the object database at `common_dir()/objects` explicitly
/// Open the typed store for the repository at `repo`.
pub fn open(repo: &Path) -> Result<Self, Error> {
let repo = gix::open(repo).map_err(|error| Error::Open(Box::new(error)))?;
@@ -165,6 +168,7 @@
/// this attempts a schema-aware structural merge of the two documents
/// against their common base and retries, up to [`MAX_MERGE_RETRIES`]
/// times, before giving up with [`Error::Conflict`].
+ // r[impl storage.concurrency] - CAS write, retries a structural merge on conflict
pub fn store<T: for<'a> Facet<'a>>(
&self,
refname: &str,
@@ -188,6 +192,7 @@
self.store_impl(refname, value, message, Some(author))
}
+ // r[impl storage.concurrency] - CAS-and-merge retry loop
fn store_impl<T: for<'a> Facet<'a>>(
&self,
refname: &str,
@@ -229,6 +234,7 @@
/// machine advancing twice from the same state, so it fails closed with
/// [`Error::Conflict`] rather than merging — merging stale state could
/// resurrect a dead outcome.
+ // r[impl storage.concurrency] - in-place state advance fails closed on a race instead of merging
pub fn amend<T: for<'a> Facet<'a>>(
&self,
refname: &str,
@@ -935,6 +941,7 @@
tags: BTreeMap<String, String>,
}
+ // r[verify storage.concurrency] - non-overlapping struct fields merge cleanly
#[test]
fn merge_disjoint_struct_fields_combine() {
let dir = repo();
@@ -970,6 +977,7 @@
);
}
+ // r[verify storage.concurrency] - non-overlapping map entries merge cleanly
#[test]
fn merge_disjoint_map_entries_combine() {
let dir = repo();
@@ -998,6 +1006,7 @@
assert_eq!(merged.tags, entries(&[("x", "1"), ("y", "2")]));
}
+ // r[verify storage.concurrency] - a genuine conflict fails cleanly instead of picking a winner
#[test]
fn merge_same_scalar_changed_both_ways_conflicts() {
let dir = repo();
@@ -1073,6 +1082,7 @@
assert!(matches!(result, Err(Error::Conflict)));
}
+ // r[verify storage.concurrency] - CAS detects a moved ref
#[test]
fn try_set_ref_conflicts_on_a_stale_expected() {
let dir = repo();
@@ -1095,6 +1105,7 @@
assert!(matches!(result, Err(Error::Conflict)));
}
+ // r[verify storage.concurrency] - no common ancestor cannot be merged
#[test]
fn store_conflicts_on_a_fresh_ref_race_with_no_common_base() {
let dir = repo();
@@ -1114,6 +1125,7 @@
assert!(matches!(result, Err(Error::Conflict)));
}
+ // r[verify storage.concurrency] - a state advance fails closed rather than merging
#[test]
fn amend_fails_closed_on_a_race_instead_of_merging() {
let dir = repo();
crates/git-ents-server/src/web/debug.rs
@@ -30,6 +30,7 @@
/// Upgrade an authenticated member's request into an interactive shell in
/// `repo_path`'s checks Sprite.
+// r[impl checks.debug] brokered over WebSocket at /_debug/<repo>; refuses without a signed-in session
pub(crate) async fn handshake(
State(state): State<AppState>,
Path(repo_path): Path<String>,
@@ -90,6 +91,7 @@
/// pseudo-TTY, but the broker allocates its *own* local pty for the `sprite
/// exec --tty` process so a resize control frame (see below) has something to
/// apply to — plain pipes have no window size to change.
+// r[impl checks.debug] interactive read-write shell in the Sprite; server holds only the SPRITES_TOKEN credential
async fn relay(mut socket: WebSocket, sprite: String) {
let pair = match native_pty_system().openpty(INITIAL_SIZE) {
Ok(pair) => pair,
@@ -175,6 +177,7 @@
/// Parse a resize control frame, `"<cols> <rows>"`, as sent by the CLI on
/// connect and on every local `SIGWINCH`.
+// r[impl checks.debug] terminal resize forwarded as control frames
fn parse_resize(text: &str) -> Option<PtySize> {
let (cols, rows) = text.split_once(' ')?;
Some(PtySize {
crates/git-ents-server/src/web/git.rs
@@ -46,6 +46,7 @@
/// bounds the memory a single request can consume, so an arbitrarily large blob
/// or diff renders as a truncation notice instead of being slurped whole — the
/// difference between a capped response and an out-of-memory kill.
+// r[impl nonfunctional.memory-cap] - kills git and truncates once the cap is exceeded
pub(super) async fn git_output_capped(
repo: &Path,
args: &[&str],
@@ -345,6 +346,7 @@
}
}
+ // r[verify nonfunctional.memory-cap]
#[tokio::test]
async fn capped_read_flags_oversized_output() {
let dir = tempfile::tempdir().unwrap();
@@ -357,6 +359,7 @@
assert_eq!(bytes.len(), 1024);
}
+ // r[verify nonfunctional.memory-cap]
#[tokio::test]
async fn capped_read_returns_full_small_output() {
let dir = tempfile::tempdir().unwrap();
crates/git-ents-server/src/web/mod.rs
@@ -50,6 +50,8 @@
/// Render the page for `path`: the repository index at the root, a repository
/// overview, or one of its browse views (`tree`, `blob`, `commit`). `host` is
/// the request's `Host` header, used to build a copy-pasteable clone URL.
+// r[impl web.server-rendered] - entry point for every browser GET
+// r[impl web.auth.challenge] `/login` and `/login/cli` issue the one-time challenge
pub(crate) async fn render(
state: &AppState,
path: &str,
@@ -135,6 +137,8 @@
/// Handle a browser POST: signing in, signing out, or saving a settings edit.
/// Git wire POSTs never reach here — [`crate::http`] routes those to the backend.
+// r[impl web.auth.challenge] the `/login` and `/login/cli` POST endpoints complete the challenge-response
+// r[impl web.auth.session] `/login` opens the session cookie; `/logout` requires the CSRF token and clears it
pub(crate) async fn handle_post(
state: &AppState,
path: &str,
@@ -190,6 +194,7 @@
/// gate (nonce seed + hooks) and its own signing key, all of which
/// [`write::edit_config`] requires. When any is unset, edit controls are not
/// offered so a member is never told they can edit when a submit would only fail.
+// r[impl web.auth.edit] hides edit controls when the server has no signing key or nonce seed
fn editing_enabled(state: &AppState) -> bool {
state.cert_nonce_seed.is_some() && state.hooks_dir.is_some() && state.web_signing_key.is_some()
}
@@ -206,6 +211,7 @@
/// Apply a settings edit, then redirect back to the settings page on success or
/// render the reason it was rejected.
+// r[impl web.auth.edit] the settings-edit request path (CSRF check, then a real signed push)
async fn save_settings(
state: &AppState,
repo: &Path,
@@ -273,6 +279,7 @@
/// Record a code comment posted from a file view, then redirect back to that
/// file on success or render the reason it was rejected.
+// r[impl web.comments] - lands a browser comment through the same signed-push gate as a settings edit
async fn save_comment(
state: &AppState,
repo: &Path,
@@ -358,6 +365,7 @@
}
/// The `Set-Cookie` value that opens a session, marked `Secure` over HTTPS.
+// r[impl web.auth.session] issues the `ents_session` cookie
fn session_cookie(token: &str, secure: bool) -> String {
format!(
"{}={token}; Path=/; HttpOnly; SameSite=Lax{}",
@@ -367,6 +375,7 @@
}
/// The `Set-Cookie` value that clears a session.
+// r[impl web.auth.session] clears the cookie on sign-out
fn cleared_cookie(secure: bool) -> String {
format!(
"{}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax{}",
@@ -378,6 +387,8 @@
/// Dispatch the part of the path that follows the repository to a browse view.
/// Each top-level tab is its own route, since the product is server-rendered
/// with no client JavaScript.
+// r[impl web.server-rendered] - navigation is ordinary GET requests, dispatched here
+// r[impl web.tabs] - routes each tab to its page renderer
async fn route(
repo: &Path,
rel: &str,
@@ -460,6 +471,7 @@
}
/// The top-level tabs of a repository page.
+// r[impl web.tabs] - the set of tabs a repository's web UI provides
#[derive(Clone, Copy, PartialEq, Eq)]
enum Tab {
Overview,
@@ -589,6 +601,7 @@
/// The tab bar with the active tab underlined. Tabs that have no backing data
/// yet still render so the navigation matches the design.
+// r[impl web.tabs] - the tab bar navigation
fn tab_bar(meta: &RepoMeta, active: Tab) -> Markup {
let rel = &meta.rel;
html! {
@@ -610,6 +623,7 @@
}
/// The repository listing shown at `/`.
+// r[impl web.index]
fn index(state: &AppState, session: Option<&write::SessionSnapshot>) -> Markup {
let repos = discover_repos(&state.data_dir);
page(
@@ -685,6 +699,7 @@
/// The sign-in page: prove control of a member key by signing a one-time
/// challenge locally, without ever surrendering the key. `error` shows a failed
/// attempt's reason; `challenge` is the nonce to sign.
+// r[impl web.auth.challenge] points at `git ents login` as the preferred flow, manual signing as fallback
fn login_page(
session: Option<&write::SessionSnapshot>,
challenge: Option<&str>,
@@ -749,6 +764,8 @@
}
/// Wrap page `body` in the shared HTML shell, navigation, and styling.
+// r[impl web.server-rendered] - the shared shell; its two scripts (copy-to-clipboard,
+// live check polling) are progressive enhancement, not required for navigation
fn page(title: &str, body: Markup) -> Markup {
html! {
(DOCTYPE)
crates/git-ents-server/src/web/pages.rs
@@ -31,6 +31,8 @@
/// notice instead — a cap on what one page can cost. 2 MiB comfortably covers
/// real source files while ruling out the multi-hundred-MiB objects that would
/// exhaust the server.
+// r[impl web.syntax-highlight] - the 2 MiB truncation cap on blob views
+// r[impl nonfunctional.memory-cap] - no single request renders more than 2 MiB of object data
const MAX_RENDER_BYTES: usize = 2 * 1024 * 1024;
/// Render an Askama tab-body template into [`Markup`] the Maud page shell can
@@ -270,6 +272,7 @@
/// client JavaScript, expanding a folder or opening a file is a link to
/// `/<repo>/files/<path>`; the tree is rendered already expanded along the
/// selected path.
+// r[impl web.tabs] - Files tab: browsable file tree with blob/comments pane
pub(super) async fn files_page(
repo: &Path,
meta: &RepoMeta,
@@ -523,6 +526,8 @@
/// [`BlobView::Source`], with a toggle between the two; everything else is
/// syntax-highlighted source when the language is recognized and the file is
/// text.
+// r[impl web.tabs] - Files tab: blob view
+// r[impl web.syntax-highlight] - toggle between rendered document and highlighted source
pub(super) async fn blob_page(
repo: &Path,
meta: &RepoMeta,
@@ -595,6 +600,7 @@
/// Render text file `source` with a line-number gutter — each number a
/// self-linking `#L<n>` anchor — highlighting via `arborium` when the filename
/// maps to a known grammar.
+// r[impl web.syntax-highlight] - self-linking `#L<n>` gutter anchors
fn blob_body(name: &str, source: &str) -> Markup {
let lines = source.lines().count().max(1);
let highlighted = highlight(name, source);
@@ -619,6 +625,7 @@
/// (in which case the caller renders escaped plain text). The highlighter is
/// built and used synchronously so its non-`Send` grammar store is never held
/// across an `.await`.
+// r[impl web.syntax-highlight] - compile-time language registry (`arborium`)
fn highlight(name: &str, source: &str) -> Option<String> {
let language = arborium::detect_language(name)?;
let config = Config {
@@ -652,6 +659,7 @@
/// async runtime since git-comment reads the object database synchronously.
/// Comments that fail to project (say, an anchor commit the repository no
/// longer has) are skipped rather than failing the page.
+// r[impl web.comments] - projects comment anchors onto the viewed revision, flagging outdated ones
async fn file_comments(repo: &Path, path: &str) -> Vec<FileComment> {
let repo = repo.to_owned();
let path = path.to_owned();
@@ -695,6 +703,7 @@
/// when the viewer may comment; nothing when there are neither. A
/// line-anchored comment links its range to the gutter's `#L<n>` anchors; an
/// outdated one is flagged instead, since its lines no longer exist.
+// r[impl web.comments] - lists a file's anchored comments under its blob view
fn comments_card(comments: &[FileComment], form: Option<Markup>) -> Markup {
if comments.is_empty() && form.is_none() {
return html! {};
@@ -726,6 +735,7 @@
/// The add-comment form under a file view, shown when a signed-in member views
/// a server that can land edits; `None` otherwise, since a submit would only
/// fail. The comment anchors to `HEAD`'s blob at `path`.
+// r[impl web.comments] - a signed-in member can add a comment from the file view
fn comment_form(
rel: &str,
path: &str,
@@ -749,6 +759,7 @@
}
/// A single commit: its metadata and a colorized unified diff.
+// r[impl web.tabs] - Commits: commit history with diff views
pub(super) async fn commit_page(repo: &Path, meta: &RepoMeta, sha: &str) -> Response {
if sha.is_empty() || sha.len() > 64 || !sha.bytes().all(|b| b.is_ascii_hexdigit()) {
return not_found().into_response();
@@ -807,6 +818,7 @@
}
/// The Releases tab: tags presented as a changelog timeline, newest first.
+// r[impl web.tabs] - Releases: git tags browsable by version
pub(super) async fn releases_page(repo: &Path, meta: &RepoMeta) -> Markup {
let releases = releases(repo).await;
repo_shell(
@@ -855,6 +867,7 @@
/// check, its latest status against the current commit, linked to its recorded
/// terminal session when it has one; Recent runs and Configuration below it are
/// the full history and the raw set, as before.
+// r[impl web.tabs] - Checks: configured check set and recorded runs with per-check outcomes
pub(super) async fn checks_page(repo: &Path, meta: &RepoMeta) -> Markup {
let rel = &meta.rel;
let checks = component::load::<git_ents_core::checks::Check>(repo).await;
@@ -1103,6 +1116,7 @@
/// `refs/meta/issues/<id>`, split into open and closed, with the filter chips
/// derived from the labels that exist. Issue creation is a write path that does
/// not exist yet, so the "New issue" button stays disabled.
+// r[impl web.tabs] - Issues: issue list with open/closed filter and per-issue detail
pub(super) async fn issues_page(repo: &Path, meta: &RepoMeta) -> Markup {
let tpl = match component::load::<git_ents_core::issues::Issue>(repo).await {
Err(err) => IssuesTemplate {
@@ -1156,6 +1170,7 @@
/// feature and check status. The General fields are editable in place by a
/// signed-in member when `editing` is set (the server has a signing key and the
/// gate); everything else is read-only.
+// r[impl web.tabs] - Settings: repository Config fields, editable by signed-in members
pub(super) async fn settings_page(
repo: &Path,
meta: &RepoMeta,
crates/git-ents-server/src/web/write.rs
@@ -39,6 +39,7 @@
/// One browser session. It holds only the member's *public* key — enough to
/// authorize per repository — plus a display label and a CSRF token.
+// r[impl web.auth.session] holds only the public key, label, and CSRF token; nothing secret, in memory only
pub(crate) struct Session {
/// The member's public key line (`type base64`), matched against members.
public_key: String,
@@ -82,6 +83,7 @@
}
/// Issue a fresh one-time sign-in challenge, returning the nonce to sign.
+// r[impl web.auth.challenge] server issues a one-time nonce
pub(super) fn issue_challenge(challenges: &Challenges) -> Result<String, String> {
let nonce = random_token()?;
let mut table = challenges
@@ -94,6 +96,7 @@
}
/// Consume `nonce`, returning whether it was a live, unexpired challenge.
+// r[impl web.auth.challenge] a challenge is consumed on first use and expires after CHALLENGE_TTL (600s)
fn take_challenge(challenges: &Challenges, nonce: &str) -> bool {
let Ok(mut table) = challenges.lock() else {
return false;
@@ -120,6 +123,8 @@
/// against the pasted `public_key`, and on success open a session and return its
/// token. Holding a session grants nothing on its own — an edit is authorized
/// per repository against the live member list.
+// r[impl web.auth.challenge] verifies the pasted signature against the pasted public key
+// r[impl web.auth.session] on success, opens a session carrying an unguessable random token
pub(super) fn login(
sessions: &Sessions,
challenges: &Challenges,
@@ -157,6 +162,7 @@
}
/// Whether `cookie`'s session exists and its CSRF token matches `token`.
+// r[impl web.auth.session] state-changing POSTs must echo back the session's CSRF token
pub(super) fn csrf_ok(sessions: &Sessions, cookie: Option<&str>, token: &str) -> bool {
let Some(session_token) = cookie.and_then(self::token) else {
return false;
@@ -169,6 +175,7 @@
}
/// Drop the session a `Cookie` header points at, if any.
+// r[impl web.auth.session] sign-out drops the session from server memory
pub(super) fn logout(sessions: &Sessions, cookie: Option<&str>) {
let Some(token) = cookie.and_then(token) else {
return;
@@ -184,6 +191,7 @@
/// `pre_receive` cannot enforce this — it is purely key-based, and a
/// self-attested member typically has no push key to gate — so the web write
/// path is the enforcement point.
+// r[impl web.auth.edit] gates settings edits to admin-registered members
fn require_admin_registered(store: &git_store::Store, username: &str) -> Result<(), String> {
use git_ents_core::members::Provenance;
let member = git_ents_core::members::load_with(store, username)
@@ -206,6 +214,7 @@
/// `seed`, `hooks`, and `signing_key` are the server's signed-push nonce seed,
/// hooks directory, and own member key; all are required, so a web edit is never
/// a way around a server that is not enforcing the gate.
+// r[impl web.auth.edit] lands the settings edit as a signed push through the pre-receive gate
pub(super) fn edit_config(
sessions: &Sessions,
cookie: Option<&str>,
@@ -245,6 +254,7 @@
/// blob at the commented path. Any signed-in member may comment — including a
/// self-attested web member, whose allowed writes are exactly issues and
/// comments — so there is no [`require_admin_registered`] gate here.
+// r[impl web.comments] - signed push through the same pre-receive gate, no admin-registered provenance required
pub(super) fn add_comment(
sessions: &Sessions,
cookie: Option<&str>,
@@ -319,6 +329,7 @@
/// *always* deleted before returning — whether or not the push was accepted,
/// so a rejected edit never leaves a zombie ref behind — and the commit that
/// lands is authored by `username` while the server is the committer.
+// r[impl web.auth.edit] author is the signed-in member, committer is the server; staging ref always deleted
#[expect(
clippy::too_many_arguments,
reason = "the server identity a signed edit requires"
@@ -353,6 +364,7 @@
/// Point `staging` at `target_ref`'s current tip, build the new commit on it
/// authored by `username`, then push it signed with the server's key onto
/// `target_ref`.
+// r[impl web.auth.edit] the actual `git push --signed` with the server's own key
#[expect(clippy::too_many_arguments, reason = "internal step of signed_edit")]
fn stage_and_push<T: for<'a> facet::Facet<'a>>(
repo: &Path,
@@ -417,6 +429,7 @@
/// Verify an SSHSIG `signature` over `nonce` was made by `public_key` under the
/// login namespace, using `ssh-keygen -Y verify` against a one-key allowed
/// signers file.
+// r[impl web.auth.challenge] SSHSIG verification under the distinct git.ents.cloud namespace
fn verify_login_signature(public_key: &str, nonce: &str, signature: &str) -> Result<bool, String> {
let dir = tempfile::tempdir().map_err(|e| format!("could not create temp dir: {e}"))?;
let allowed = dir.path().join("allowed_signers");