git-ents.gitmain
⌘K
foforge
commit ea8a28f
docs: refactor tracey annotations to StrictDoc's @relation syntax

Replaces the r[impl …​] / r[verify …​] markdown-style markers with StrictDoc’s @relation(…​) form, which tracey recognizes identically. impl references move into a /// doc comment under a "## Requirements" heading so they read as part of the item’s documentation; verify references and impl sites that aren’t attached to an item (mid-chain routes, statements) stay as // line comments. tracey_validate reports the same 0 errors as before; comment-only, no logic changes.

Assisted-by: Claude:claude-sonnet-5

Joseph D. Carpinelli · 1 month ago

Reviews

No reviews of this commit yet — record a verdict below.

Start a review

verdict

crates/git-anchor/src/lib.rs @@ -109,7 +109,10 @@ /// 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] +/// +/// ## Requirements +/// +/// @relation(comments.anchor) #[derive(Debug, Clone, PartialEq, Eq, Facet)] pub struct Anchor { /// The commit the anchor was created against. @@ -124,7 +127,10 @@ } /// Where an [`Anchor`] sits on a target commit, as computed by [`project`]. -// r[impl comments.projection] +/// +/// ## Requirements +/// +/// @relation(comments.projection) #[derive(Debug, Clone, PartialEq, Eq)] pub enum Projection { /// The target tree holds the anchor's exact blob at its exact path; the @@ -153,7 +159,10 @@ /// `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 +/// +/// ## Requirements +/// +/// @relation(comments.anchor) pub fn capture( repo: &Path, revision: &str, @@ -190,7 +199,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(comments.anchor) 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) @@ -229,7 +241,10 @@ /// 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] +/// +/// ## Requirements +/// +/// @relation(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) @@ -374,7 +389,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(comments.projection) 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 @@ -432,7 +450,7 @@ Some(LineRange { start, end }) } - // r[verify comments.anchor] + // @relation(comments.anchor, role=Verifies) #[test] fn capture_records_the_commit_and_blob_and_snippet_derives_the_text() { let dir = repo(); @@ -447,7 +465,7 @@ assert_eq!(snippet(dir.path(), &anchor).unwrap(), "line 3\nline 4\n"); } - // r[verify comments.anchor] + // @relation(comments.anchor, role=Verifies) #[test] fn capture_rejects_a_missing_path_and_an_oversized_range() { let dir = repo(); @@ -464,7 +482,7 @@ )); } - // r[verify comments.projection] + // @relation(comments.projection, role=Verifies) #[test] fn unchanged_file_projects_as_current() { let dir = repo(); @@ -481,7 +499,7 @@ ); } - // r[verify comments.projection] + // @relation(comments.projection, role=Verifies) #[test] fn an_edit_above_the_range_shifts_it() { let dir = repo(); @@ -502,7 +520,7 @@ ); } - // r[verify comments.projection] + // @relation(comments.projection, role=Verifies) #[test] fn an_edit_inside_the_range_is_outdated() { let dir = repo(); @@ -522,7 +540,7 @@ ); } - // r[verify comments.projection] + // @relation(comments.projection, role=Verifies) #[test] fn a_pure_rename_relocates_with_the_same_lines() { let dir = repo(); @@ -542,7 +560,7 @@ ); } - // r[verify comments.projection] + // @relation(comments.projection, role=Verifies) #[test] fn a_rename_with_an_edit_above_relocates_and_shifts() { let dir = repo(); @@ -564,7 +582,7 @@ ); } - // r[verify comments.projection] + // @relation(comments.projection, role=Verifies) #[test] fn a_deleted_file_projects_as_deleted() { let dir = repo(); @@ -582,7 +600,7 @@ ); } - // r[verify comments.projection] + // @relation(comments.projection, role=Verifies) #[test] fn a_whole_file_anchor_survives_a_modification() { let dir = repo(); @@ -604,7 +622,7 @@ ); } - // r[verify comments.projection] + // @relation(comments.projection, role=Verifies) #[test] fn projection_works_backwards_onto_an_ancestor() { let dir = repo(); @@ -626,7 +644,7 @@ ); } - // r[verify comments.projection] + // @relation(comments.projection, role=Verifies) #[test] fn map_range_handles_edges() { let old = b"a\nb\nc\nd\n".as_slice();
crates/git-comment/src/lib.rs @@ -28,17 +28,17 @@ use git_anchor::{Anchor, Projection}; use git_store::Provenance; -// r[impl comments.ref] +// @relation(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 +/// +/// ## Requirements +/// +/// @relation(comments.ref, comments.authorship, comments.anchor, comments.projection) #[derive(Debug, Clone, PartialEq, Eq, Facet)] pub struct Comment { /// The comment's body text. @@ -67,8 +67,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(comments.ref, comments.authorship) pub fn store( repo: &Path, id: &str, @@ -91,7 +93,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(comments.authorship) pub fn provenance(repo: &Path, id: &str) -> Result<Option<Provenance>, git_store::Error> { git_store::Store::open(repo)?.item_provenance(COMMENTS_NS, id) } @@ -99,7 +104,10 @@ /// 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] +/// +/// ## Requirements +/// +/// @relation(comments.projection) pub fn project( repo: &Path, comment: &Comment, @@ -138,7 +146,7 @@ const AUTHOR: (&str, &str) = ("alice", "alice@example.com"); - // r[verify comments.ref] + // @relation(comments.ref, role=Verifies) #[test] fn store_then_load_round_trips_a_comment() { let dir = repo(); @@ -183,7 +191,7 @@ assert_ne!(a_id, new_id(None, &b).unwrap()); } - // r[verify comments.authorship] + // @relation(comments.authorship, role=Verifies) #[test] fn provenance_comes_from_the_commits_not_the_document() { let dir = repo(); @@ -202,8 +210,7 @@ assert!(provenance.created.seconds > 0); } - // r[verify comments.anchor] - // r[verify comments.projection] + // @relation(comments.anchor, comments.projection, role=Verifies) #[test] fn a_stored_comment_projects_onto_the_commit_it_was_written_against() { let dir = repo(); @@ -236,8 +243,7 @@ ); } - // 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 + // @relation(comments.anchor, storage.meta-ref, role=Verifies) #[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,12 +13,12 @@ use crate::component; -// r[impl account.ref] +// @relation(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] +// @relation(account.ref) /// A repository's account profile, stored at [`ACCOUNT_REF`]. #[derive(Debug, Clone, Default, PartialEq, Eq, Facet)] pub struct Account { @@ -66,7 +66,10 @@ /// 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] +/// +/// ## Requirements +/// +/// @relation(account.genesis) pub fn genesis(repo: &Path) -> Result<Option<String>, git_store::Error> { git_store::Store::open(repo)? .history::<Account>(ACCOUNT_REF)? @@ -99,7 +102,7 @@ } } - // r[verify account.ref] + // @relation(account.ref, role=Verifies) #[test] fn store_then_load_round_trips_the_account() { let repo = unique_repo(); @@ -124,7 +127,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify storage.meta-ref] - hand-built fixture load test for the Account document + // @relation(storage.meta-ref, role=Verifies) #[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,7 +35,10 @@ /// 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] +/// +/// ## Requirements +/// +/// @relation(checks.definition) #[derive(Debug, Clone, PartialEq, Eq, Facet)] pub struct CheckBody { /// The shell command run for the check (e.g. `cargo fmt --check`), or @@ -137,8 +140,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.definition, checks.toolchains) 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 { @@ -224,7 +229,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.outcomes) #[derive(Debug, Clone, Copy, PartialEq, Eq, Facet)] #[repr(u8)] pub enum Status { @@ -259,7 +267,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.outcomes) #[derive(Debug, Clone, PartialEq, Eq, Facet)] struct Outcome { /// `queued`, `running`, then `pass`, `fail`, or `error`. @@ -317,7 +328,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.outcomes) pub fn record( repo: &Path, commit: ObjectId, @@ -340,7 +354,10 @@ /// /// 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) +/// +/// ## Requirements +/// +/// @relation(checks.outcomes) pub fn update_run( repo: &Path, commit: ObjectId, @@ -461,7 +478,7 @@ } } - // r[verify checks.definition] + // @relation(checks.definition, role=Verifies) #[test] fn store_then_load_round_trips_the_check_set() { let repo = unique_repo(); @@ -553,7 +570,7 @@ } } - // r[verify checks.outcomes] + // @relation(checks.outcomes, role=Verifies) #[test] fn record_then_runs_round_trips_a_run() { let repo = unique_repo(); @@ -576,7 +593,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify checks.outcomes] + // @relation(checks.outcomes, role=Verifies) #[test] fn recording_a_commit_again_appends_a_run() { let repo = unique_repo(); @@ -605,7 +622,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify checks.outcomes] + // @relation(checks.outcomes, role=Verifies) #[test] fn round_trips_an_outcomes_duration_and_recording() { let repo = unique_repo(); @@ -623,7 +640,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify checks.outcomes] + // @relation(checks.outcomes, role=Verifies) #[test] fn update_run_advances_in_place_rather_than_appending() { let repo = unique_repo(); @@ -640,7 +657,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify checks.outcomes] + // @relation(checks.outcomes, role=Verifies) #[test] fn displays_lowercase_status_words() { assert_eq!(Status::Queued.to_string(), "queued"); @@ -648,7 +665,7 @@ assert_eq!(Status::Skipped.to_string(), "skipped"); } - // r[verify checks.definition] + // @relation(checks.definition, role=Verifies) #[test] fn store_then_load_round_trips_image_and_depends() { let repo = unique_repo(); @@ -669,7 +686,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify checks.definition] + // @relation(checks.definition, role=Verifies) #[test] fn order_runs_dependencies_first() { let checks = vec![ @@ -685,7 +702,7 @@ assert_eq!(names, vec!["fmt", "test", "ci"]); } - // r[verify checks.definition] rejects a dependency cycle + // @relation(checks.definition, role=Verifies) #[test] fn order_rejects_a_cycle() { let checks = vec![ @@ -698,7 +715,7 @@ assert!(err.contains('a') && err.contains('b')); } - // r[verify checks.definition] rejects a dangling dependency + // @relation(checks.definition, role=Verifies) #[test] fn order_rejects_an_unknown_dependency() { let checks = vec![dependent("test", "cargo nextest run", &["fmt"])]; @@ -706,7 +723,7 @@ assert!(err.contains("unknown check fmt"), "unexpected error: {err}"); } - // r[verify checks.definition] rejects self and duplicate edges + // @relation(checks.definition, role=Verifies) #[test] fn order_rejects_self_and_duplicate_edges() { let selfish = vec![dependent("a", "true", &["a"])]; @@ -718,7 +735,7 @@ assert!(order(&doubled).unwrap_err().contains("twice")); } - // r[verify checks.definition] rejects a check with neither a command nor dependencies + // @relation(checks.definition, role=Verifies) #[test] fn order_rejects_an_empty_check() { let checks = vec![composite("hollow", &[])]; @@ -729,7 +746,7 @@ ); } - // r[verify checks.toolchains] + // @relation(checks.toolchains, role=Verifies) #[test] fn order_accepts_a_valid_toolchain_name() { let checks = vec![toolchained("build", "make", &["gcc-12"])]; @@ -743,7 +760,7 @@ ); } - // r[verify checks.toolchains] + // @relation(checks.toolchains, role=Verifies) #[test] fn order_rejects_an_invalid_toolchain_name() { let checks = vec![toolchained("build", "make", &["not/valid"])]; @@ -751,7 +768,7 @@ assert!(err.contains("invalid toolchain"), "unexpected error: {err}"); } - // r[verify checks.toolchains] + // @relation(checks.toolchains, role=Verifies) #[test] fn store_then_load_round_trips_toolchains() { let repo = unique_repo();
crates/git-ents-core/src/config.rs @@ -15,11 +15,11 @@ use crate::component; -// r[impl config.ref] +// @relation(config.ref) /// The ref whose tree holds the repository configuration. pub const CONFIG_REF: &str = "refs/meta/config"; -// r[impl config.ref] +// @relation(config.ref) /// The repository configuration stored at [`CONFIG_REF`]. #[derive(Debug, Clone, Default, PartialEq, Eq, Facet)] pub struct Config { @@ -111,7 +111,10 @@ /// 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) +/// +/// ## Requirements +/// +/// @relation(config.ref) pub fn load_with(store: &git_store::Store) -> Result<Config, git_store::Error> { Ok(component::load::<Config>(store)?.unwrap_or_default()) } @@ -161,7 +164,7 @@ } } - // r[verify config.ref] + // @relation(config.ref, role=Verifies) #[test] fn store_then_load_round_trips_the_config() { let repo = unique_repo(); @@ -179,7 +182,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify storage.meta-ref] - hand-built fixture load test for the Config document + // @relation(storage.meta-ref, role=Verifies) #[test] fn loads_the_on_disk_config_format() { // A fixture written as the real on-disk layout — `description` and @@ -198,7 +201,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify config.ref] + // @relation(config.ref, role=Verifies) #[test] fn default_when_the_config_ref_is_absent() { let repo = unique_repo();
crates/git-ents-core/src/issues.rs @@ -31,7 +31,7 @@ use crate::component; -// r[impl issues.ref] +// @relation(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"; @@ -51,7 +51,7 @@ Closed, } -// r[impl issues.ref] +// @relation(issues.ref) /// One issue stored at `refs/meta/issues/<id>`. #[derive(Debug, Clone, PartialEq, Eq, Facet)] pub struct Issue { @@ -99,7 +99,10 @@ /// 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] +/// +/// ## Requirements +/// +/// @relation(issues.id) pub fn new_id(origin: Option<&str>, content: &Issue) -> Result<String, git_store::Error> { git_store::new_id(origin, content) } @@ -157,7 +160,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(issues.id) pub fn promote(repo: &Path, id: &str) -> Result<String, PromoteError> { let store = git_store::Store::open(repo)?; let mut number = None; @@ -212,7 +218,7 @@ } } - // r[verify issues.ref] + // @relation(issues.ref, role=Verifies) #[test] fn store_then_load_round_trips_an_issue() { let repo = unique_repo(); @@ -241,7 +247,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify storage.meta-ref] - hand-built fixture load test for the Issue document + // @relation(storage.meta-ref, role=Verifies) #[test] fn loads_the_on_disk_issue_format() { // A fixture written as the real on-disk layout — `title`, `body`, @@ -266,14 +272,14 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify issues.id] + // @relation(issues.id, role=Verifies) #[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] + // @relation(issues.id, role=Verifies) #[test] fn new_id_hashes_its_own_content_with_no_origin() { let a = issue("A bug", State::Open, &[]); @@ -286,7 +292,7 @@ assert_ne!(a_id, b_id); } - // r[verify issues.id] + // @relation(issues.id, role=Verifies) #[test] fn filing_an_issue_leaves_its_friendly_number_unset() { let repo = unique_repo(); @@ -297,7 +303,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify issues.id] + // @relation(issues.id, role=Verifies) #[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,7 +40,7 @@ use crate::component; -// r[impl members.ref] +// @relation(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"; @@ -51,11 +51,7 @@ format!("{MEMBER_NS}/{username}") } -// r[impl members.ref] -// r[impl members.trust] -// r[impl members.provenance] -// r[impl members.account] -// r[impl members.window] +// @relation(members.ref, members.trust, members.provenance, members.account, 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)] @@ -99,7 +95,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(members.provenance) #[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Facet)] #[repr(u8)] pub enum Provenance { @@ -127,7 +126,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(members.trust) #[derive(Debug, Clone, PartialEq, Eq, Facet)] #[repr(u8)] pub enum Trust { @@ -243,7 +245,10 @@ /// [`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 + /// + /// ## Requirements + /// + /// @relation(members.window) pub fn validate(&self) -> Result<(), String> { for bound in [&self.valid_after, &self.valid_before] { if let Some(value) = bound @@ -350,8 +355,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(revocations.ref, revocations.ca) #[must_use] pub fn without_revoked(members: Vec<Member>, revoked: &BTreeSet<String>) -> Vec<Member> { members @@ -380,7 +387,10 @@ /// `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] +/// +/// ## Requirements +/// +/// @relation(members.allowed-signers) #[must_use] pub fn allowed_signers(members: &[Member]) -> String { members.iter().flat_map(member_lines).collect::<String>() @@ -449,7 +459,7 @@ .collect() } - // r[verify members.ref] + // @relation(members.ref, role=Verifies) #[test] fn store_then_load_round_trips_a_member() { let repo = unique_repo(); @@ -497,8 +507,7 @@ 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] + // @relation(storage.meta-ref, members.provenance, role=Verifies) #[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` @@ -527,7 +536,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify members.trust] - WebAuthn credential set round trip + // @relation(members.trust, role=Verifies) #[test] fn loads_a_hand_built_webauthn_fixture() { // A hand-built `trust/WebAuthn/<credential_id>/{cose_key,label}` @@ -559,7 +568,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify members.trust] - WebAuthn authorizes web sign-in only, never allowed_signers/push + // @relation(members.trust, role=Verifies) #[test] fn store_then_load_round_trips_a_webauthn_member() { let repo = unique_repo(); @@ -582,7 +591,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify members.allowed-signers] - wildcard principal, namespaces="git" + // @relation(members.allowed-signers, role=Verifies) #[test] fn renders_a_wildcard_allowed_signers_file() { let member = Member::with_keys("alice".to_owned(), keys(&[("aa:bb", KEY_A)])); @@ -592,7 +601,7 @@ ); } - // r[verify members.allowed-signers] - validity window as comma-joined options + // @relation(members.allowed-signers, role=Verifies) #[test] fn renders_the_validity_window_as_comma_joined_options() { let mut member = Member::with_keys("alice".to_owned(), keys(&[("aa:bb", KEY_A)])); @@ -606,7 +615,7 @@ ); } - // r[verify members.trust] - certificate authority trust basis + // @relation(members.trust, role=Verifies) #[test] fn store_then_load_round_trips_a_ca_member() { let repo = unique_repo(); @@ -619,7 +628,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify revocations.ref] + // @relation(revocations.ref, role=Verifies) #[test] fn without_revoked_drops_revoked_keys_and_emptied_members() { let alice = Member::with_keys( @@ -638,7 +647,7 @@ ); } - // r[verify revocations.ca] + // @relation(revocations.ca, role=Verifies) #[test] fn without_revoked_leaves_ca_members_untouched() { let member = Member::with_ca("alice".to_owned(), KEY_A.to_owned()); @@ -649,7 +658,7 @@ ); } - // r[verify members.allowed-signers] - a pinned CA renders as a cert-authority line + // @relation(members.allowed-signers, role=Verifies) #[test] fn renders_a_pinned_ca_as_a_cert_authority_line() { let mut member = Member::with_ca("alice".to_owned(), KEY_A.to_owned()); @@ -660,7 +669,7 @@ ); } - // r[verify members.window] + // @relation(members.window, role=Verifies) #[test] fn validate_rejects_a_malformed_timestamp() { let mut member = Member::with_keys("alice".to_owned(), keys(&[("aa:bb", KEY_A)])); @@ -668,7 +677,7 @@ assert!(member.validate().is_err()); } - // r[verify members.window] + // @relation(members.window, role=Verifies) #[test] fn validate_rejects_an_inverted_window() { let mut member = Member::with_keys("alice".to_owned(), keys(&[("aa:bb", KEY_A)])); @@ -685,7 +694,7 @@ member.validate().unwrap(); } - // r[verify members.window] + // @relation(members.window, role=Verifies) #[test] fn store_rejects_a_member_with_an_inverted_window() { let repo = unique_repo();
crates/git-ents-core/src/revocations.rs @@ -30,7 +30,7 @@ use crate::component; -// r[impl revocations.ref] +// @relation(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"; @@ -135,7 +135,7 @@ } } - // r[verify revocations.ref] + // @relation(revocations.ref, role=Verifies) #[test] fn store_then_load_round_trips_the_revocations() { let repo = unique_repo(); @@ -169,7 +169,7 @@ let _ = std::fs::remove_dir_all(&repo); } - // r[verify storage.meta-ref] - hand-built fixture load test for the Revocation document + // @relation(storage.meta-ref, role=Verifies) #[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,7 +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 +// @relation(members.trust, role=Verifies) #[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,8 +25,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.render-registry, web.syntax-highlight) 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,8 +106,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.post-receive, nonfunctional.push-latency) pub fn post_receive() -> Result<(), String> { let repo = std::env::current_dir().map_err(|e| format!("cannot resolve repository: {e}"))?; @@ -178,8 +180,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.worker, nonfunctional.concurrency) 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}"); @@ -254,7 +258,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.worker) 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() { @@ -356,7 +363,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.worker) fn derive_composite(deps: &[Status]) -> Status { if deps.iter().all(|status| *status == Status::Pass) { Status::Pass @@ -380,7 +390,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.worker) fn finalize_error(repo: &Path, new: ObjectId, outcomes: &mut [RunOutcome]) { for outcome in outcomes.iter_mut() { outcome.status = Status::Error; @@ -399,7 +412,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.post-receive) 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}"))?; @@ -466,7 +482,10 @@ /// /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.sandbox) pub(crate) fn sprite_name(repo: &Path) -> String { let stem = repo .file_name() @@ -494,8 +513,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.sandbox, compat.sprite) pub(crate) fn ensure_auth() -> Result<(), String> { let token = std::env::var("SPRITES_TOKEN") .ok() @@ -518,8 +539,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.sandbox, compat.sprite) pub(crate) fn ensure_sprite(sprite: &str) -> Result<(), String> { let _existing = Command::new("sprite") .args(["create", "--skip-console", sprite]) @@ -532,9 +555,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.sandbox, compat.sprite, compat.git) fn sync_tree(repo: &Path, sprite: &str, new: ObjectId) -> Result<(), String> { let archive = Command::new("git") .arg("-C") @@ -573,8 +597,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.toolchains, checks.sandbox) fn resolve_toolchains( repo: &Path, sprite: &str, @@ -628,7 +654,10 @@ /// `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 +/// +/// ## Requirements +/// +/// @relation(checks.sandbox) fn activate(command: &str, toolchains: &[String], dirs: &HashMap<String, String>) -> String { if toolchains.is_empty() { return command.to_owned(); @@ -648,7 +677,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.sandbox) fn sync_toolchain(repo: &Path, sprite: &str, tree: ObjectId) -> Result<(), String> { let dir = format!("{TOOLCHAINS_DIR}/{tree}"); let cached = Command::new("sprite") @@ -707,7 +739,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.sandbox) fn sync_downloaded_toolchain( sprite: &str, key: &str, @@ -759,7 +794,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.outcomes) const CHECK_TIMEOUT: Duration = Duration::from_secs(30 * 60); /// The fixed size a check's recorded terminal session runs at. Nothing @@ -791,7 +829,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(compat.sprite) fn run_one(sprite: &str, name: &str, command: &str, live: &Arc<StdMutex<String>>) -> RunResult { let start = Instant::now(); lock(live).push_str(&asciicast_header()); @@ -949,7 +990,7 @@ use super::*; - // r[verify checks.post-receive] only updated non-meta branches with a non-zero tip are enqueued + // @relation(checks.post-receive, role=Verifies) #[test] fn parse_updates_keeps_content_branches_only() { let new = "1111111111111111111111111111111111111111"; @@ -965,14 +1006,14 @@ assert_eq!(refs, vec!["refs/heads/main", "refs/heads/feature"]); } - // r[verify checks.sandbox] + // @relation(checks.sandbox, role=Verifies) #[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 + // @relation(checks.sandbox, role=Verifies) #[test] fn activate_prefixes_path_in_declared_order() { let mut dirs = HashMap::new(); @@ -985,7 +1026,7 @@ ); } - // r[verify checks.sandbox] + // @relation(checks.sandbox, role=Verifies) #[test] fn activate_skips_a_toolchain_missing_from_dirs() { let dirs = HashMap::new(); @@ -996,7 +1037,7 @@ ); } - // r[verify checks.worker] composite outcome derivation + // @relation(checks.worker, role=Verifies) #[test] fn composite_status_derives_from_its_dependencies() { assert_eq!( @@ -1020,7 +1061,7 @@ assert_eq!(derive_composite(&[]), Status::Pass); } - // r[verify checks.worker] jobs grouped by repo for per-repo draining + // @relation(checks.worker, role=Verifies) #[test] fn pending_jobs_groups_by_repo_and_drops_malformed() { let queue = tempfile::tempdir().unwrap();
crates/git-ents-server/src/http.rs @@ -19,13 +19,13 @@ const CGI_HEADER_SEP: &[u8] = b"\r\n\r\n"; -// r[impl deploy.health] +// @relation(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 +// @relation(protocol.routing) /// 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 { @@ -55,7 +55,7 @@ .await } -// r[impl protocol.routing] - dispatches a POST to the web UI or the git backend by path +// @relation(protocol.routing) /// 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. @@ -89,16 +89,13 @@ .await } -// r[impl protocol.routing] +// @relation(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 +// @relation(protocol.git, compat.git, compat.cgi, nonfunctional.concurrency) /// 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. @@ -230,8 +227,7 @@ 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] +// @relation(compat.git, 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. @@ -251,7 +247,7 @@ overrides } -// r[impl compat.cgi] - parses the CGI response format (header block, blank line, body) into HTTP +// @relation(compat.cgi) /// 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) { @@ -293,7 +289,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 +// @relation(protocol.routing) /// Whether a GET should be answered with the HTML web UI rather than handed to /// `git http-backend`. /// @@ -310,7 +306,7 @@ !is_wire || (is_browse && !is_service_request(path, query)) } -// r[impl protocol.routing] +// @relation(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 { @@ -338,8 +334,7 @@ .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 +// @relation(namespace.auto-create, storage.bare) /// Ensure `repo` exists as a bare repository, creating it on first push. /// /// Holds [`AppState::init_lock`] across the whole check-and-create so two @@ -375,7 +370,7 @@ }) } -// r[impl namespace.path] - refuses a path nested inside an existing repository +// @relation(namespace.path) /// 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> { @@ -399,7 +394,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 +// @relation(namespace.path) /// 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. /// @@ -423,7 +418,7 @@ Some(segments.into_iter().collect()) } -// r[impl namespace.path] - ASCII alphanumerics plus `.`, `_`, `-`; no leading `.`, no separator +// @relation(namespace.path) /// Whether a single path component is a safe repository/namespace name. /// /// Rejecting any leading `.` rules out `.`, `..`, and hidden directories; the @@ -437,8 +432,7 @@ .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 +// @relation(namespace.auto-create, compat.git) /// 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") @@ -470,8 +464,7 @@ 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 +// @relation(namespace.auto-create, compat.git) /// Point `HEAD` at a real branch when it dangles after a push. /// /// Best-effort: the push already succeeded, so failures here are ignored. @@ -583,7 +576,7 @@ #[case("a/b", false)] #[case("a b", false)] #[case("a%2eb", false)] - // r[verify namespace.path] + // @relation(namespace.path, role=Verifies) fn validates_segments(#[case] segment: &str, #[case] expected: bool) { assert_eq!(valid_segment(segment), expected); } @@ -602,13 +595,13 @@ } } - // r[verify auth.nonce] + // @relation(auth.nonce, role=Verifies) #[test] fn backend_config_is_empty_without_authentication() { assert!(backend_config(&state(None, None)).is_empty()); } - // r[verify auth.nonce] + // @relation(auth.nonce, role=Verifies) #[test] fn backend_config_injects_nonce_seed_and_hooks_path() { assert_eq!( @@ -630,7 +623,7 @@ #[case("/../etc/git-receive-pack", None)] #[case("/.ssh/git-receive-pack", None)] #[case("/git-receive-pack", None)] - // r[verify namespace.path] + // @relation(namespace.path, role=Verifies) fn extracts_repo_path(#[case] path: &str, #[case] expected: Option<&str>) { assert_eq!(repo_path(path).as_deref(), expected.map(Path::new)); } @@ -643,7 +636,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] + // @relation(protocol.routing, role=Verifies) fn detects_pushes(#[case] path: &str, #[case] query: &str, #[case] expected: bool) { assert_eq!(is_receive_pack(path, query), expected); } @@ -665,7 +658,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] + // @relation(protocol.routing, role=Verifies) 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,7 +67,7 @@ pub checks_queue: Option<PathBuf>, } -// r[impl server.embeddable] - hooks are subcommands of the server, not separate programs +// @relation(server.embeddable) /// Subcommands that run instead of serving HTTP. #[derive(Facet, Debug)] #[repr(u8)] @@ -113,8 +113,7 @@ 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 +// @relation(server.embeddable, deploy.fly) /// 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. @@ -188,14 +187,13 @@ 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 + // @relation(protocol.routing, deploy.health) // 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> + // @relation(checks.debug) .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,11 +14,13 @@ builtins: FigueBuiltins, } -// r[impl server.embeddable] - the standalone binary runs the same `git_ents_server::run` as `git ents server` +/// ## Requirements +/// +/// @relation(server.embeddable) fn main() -> ExitCode { let raw_args: Vec<String> = std::env::args().skip(1).collect(); - // r[impl deploy.fly] + // @relation(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,8 +19,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.render-registry, web.syntax-highlight) pub(crate) fn to_html(source: &str) -> String { let options = Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES
crates/git-ents-server/src/render.rs @@ -29,7 +29,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.render-registry) pub fn to_html(mime: &str, source: &str) -> String { match mime { "text/asciidoc" => asciidoc::to_html(source), @@ -42,7 +45,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.render-registry) 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,14 +21,16 @@ /// 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` +/// +/// ## Requirements +/// +/// @relation(auth.signed-push, compat.openssh-signed-push) 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] + // @relation(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 @@ -38,7 +40,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 + // @relation(auth.signed-push) let authorized = members::without_revoked(members, &revoked); let ref_updates = read_ref_updates()?; @@ -47,7 +49,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 + // @relation(auth.signed-push) if env("GIT_PUSH_CERT_NONCE_STATUS").as_deref() != Some("OK") { return Err("push certificate nonce was missing or stale".to_owned()); } @@ -99,8 +101,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(auth.signed-push, compat.ssh-keygen) fn verify_certificate(authorized: &[Member], certificate: &str) -> Result<(), String> { const MARKER: &str = "-----BEGIN SSH SIGNATURE-----"; let split = certificate @@ -160,7 +164,7 @@ std::env::var(key).ok() } -// r[impl compat.git] - invokes `git cat-file` as an external subprocess +// @relation(compat.git) /// 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,9 +220,7 @@ .success() } -// r[verify auth.signed-push] -// r[verify compat.ssh-keygen] -// r[verify compat.openssh-signed-push] +// @relation(auth.signed-push, compat.ssh-keygen, compat.openssh-signed-push, role=Verifies) #[test] fn accepts_a_push_signed_by_an_authorized_key() { let base = unique_dir("accept"); @@ -237,8 +235,7 @@ std::fs::remove_dir_all(&base).ok(); } -// r[verify auth.signed-push] -// r[verify compat.ssh-keygen] +// @relation(auth.signed-push, compat.ssh-keygen, role=Verifies) #[test] fn rejects_a_push_signed_by_an_unknown_key() { let base = unique_dir("unknown"); @@ -254,8 +251,7 @@ std::fs::remove_dir_all(&base).ok(); } -// r[verify auth.signed-push] -// r[verify compat.openssh-signed-push] +// @relation(auth.signed-push, compat.openssh-signed-push, role=Verifies) #[test] fn rejects_an_unsigned_push_when_signers_exist() { let base = unique_dir("unsigned"); @@ -333,7 +329,7 @@ std::fs::remove_dir_all(&base).ok(); } -// r[verify auth.signed-push] trust set excludes revoked fingerprints +// @relation(auth.signed-push, role=Verifies) #[test] fn rejects_a_push_signed_by_a_revoked_key() { // The key is a valid, in-window member, but its fingerprint is on the @@ -352,7 +348,7 @@ std::fs::remove_dir_all(&base).ok(); } -// r[verify auth.bootstrap] +// @relation(auth.bootstrap, role=Verifies) #[test] fn accepts_any_push_before_signers_are_configured() { let base = unique_dir("bootstrap");
crates/git-ents-server/tests/server.rs @@ -13,9 +13,7 @@ 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 +// @relation(web.server-rendered, web.index, server.embeddable, role=Verifies) #[test] fn responds_to_requests() { let port = free_port(); @@ -44,9 +42,7 @@ 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] +// @relation(storage.bare, namespace.auto-create, protocol.git, role=Verifies) #[test] fn push_then_clone_round_trip() { let data = tempfile::tempdir().unwrap(); @@ -85,8 +81,7 @@ 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] +// @relation(namespace.path, namespace.auto-create, role=Verifies) #[rstest] #[case("org/repo")] #[case("org/team/repo")] @@ -128,7 +123,7 @@ ); } -// r[verify namespace.auto-create] - a colliding creation is refused, not raced +// @relation(namespace.auto-create, role=Verifies) #[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,10 +20,7 @@ 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 +// @relation(web.tabs, web.auth.edit, web.auth.challenge, web.auth.session, role=Verifies) #[test] fn a_member_edits_settings_through_the_browser() { let env = Server::start(); @@ -85,7 +82,7 @@ ); } -// r[verify web.auth.session] - a state-changing POST without the matching CSRF token is refused +// @relation(web.auth.session, role=Verifies) #[test] fn an_edit_without_a_valid_csrf_token_is_refused() { let env = Server::start(); @@ -109,7 +106,7 @@ ); } -// r[verify web.auth.edit] - require_admin_registered refuses a self-attested member's settings edit +// @relation(web.auth.edit, role=Verifies) #[test] fn a_self_attested_member_is_refused_a_settings_edit() { let env = Server::start(); @@ -149,7 +146,7 @@ ); } -// r[verify web.auth.challenge] - a session proves key control, not membership or authority +// @relation(web.auth.challenge, role=Verifies) #[test] fn a_non_member_is_not_offered_an_edit_form() { let env = Server::start(); @@ -174,7 +171,7 @@ ); } -// r[verify web.auth.challenge] - a signature that does not match the pasted public key must not open a session +// @relation(web.auth.challenge, role=Verifies) #[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,7 +11,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.debug) pub(crate) async fn run(url: &str, token: &str) -> Result<(), String> { let mut request = url .into_client_request() @@ -38,7 +41,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.debug) 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,7 +6,7 @@ use std::io::IsTerminal as _; -// r[impl cli.interactive] - TTY detection gating prompting vs. failing fast +// @relation(cli.interactive) /// Whether prompting is possible: both stdin and stdout are a terminal. #[must_use] pub fn available() -> bool { @@ -17,7 +17,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(cli.interactive) pub fn text_or(existing: Option<String>, message: &str) -> Result<String, String> { if let Some(value) = existing { return if value.is_empty() { @@ -43,7 +46,10 @@ /// `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 +/// +/// ## Requirements +/// +/// @relation(cli.interactive) pub fn optional_text_or(existing: Option<String>, message: &str) -> Result<Option<String>, String> { if existing.is_some() { return Ok(existing); @@ -59,7 +65,10 @@ /// A `Select` prompt among `options`, run only when interactive; `default` /// otherwise. -// r[impl cli.interactive] - prompt with a selection at a TTY, `default` otherwise +/// +/// ## Requirements +/// +/// @relation(cli.interactive) 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,11 +43,9 @@ 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 +/// ## Requirements +/// +/// @relation(cli.members, cli.account-checks, cli.toolchains, cli.comments, cli.login) #[derive(Facet)] #[repr(u8)] enum Top { @@ -92,7 +90,9 @@ Server(git_ents_server::Args), } -// r[impl cli.members] - members subcommands (list/add/remove/revoke/unrevoke/check/setup) +/// ## Requirements +/// +/// @relation(cli.members) #[derive(Facet)] #[repr(u8)] enum Action { @@ -172,7 +172,9 @@ }, } -// r[impl cli.account-checks] - `account create` +/// ## Requirements +/// +/// @relation(cli.account-checks) #[derive(Facet)] #[repr(u8)] enum AccountAction { @@ -192,7 +194,9 @@ }, } -// r[impl cli.account-checks] - `checks list`/`add`/`remove` +/// ## Requirements +/// +/// @relation(cli.account-checks) #[derive(Facet)] #[repr(u8)] enum ChecksAction { @@ -235,7 +239,9 @@ Runs, } -// r[impl cli.toolchains] - `import`/`list`/`export`/`remove` +/// ## Requirements +/// +/// @relation(cli.toolchains) #[derive(Facet)] #[repr(u8)] enum ToolchainAction { @@ -316,7 +322,9 @@ }, } -// r[impl cli.comments] - `add`/`list`/`show`/`remove` +/// ## Requirements +/// +/// @relation(cli.comments) #[derive(Facet)] #[repr(u8)] enum CommentAction { @@ -415,7 +423,9 @@ } } -// r[impl cli.members] - dispatch +/// ## Requirements +/// +/// @relation(cli.members) fn run_members(action: Action, remote: &str) -> Result<(), String> { match action { Action::Setup { key, local } => setup(key.as_deref(), local), @@ -448,7 +458,9 @@ } } -// r[impl cli.account-checks] - `account create` dispatch +/// ## Requirements +/// +/// @relation(cli.account-checks) fn run_account(action: AccountAction, remote: &str) -> Result<(), String> { match action { AccountAction::Create { @@ -459,7 +471,9 @@ } } -// r[impl cli.account-checks] - `checks` dispatch +/// ## Requirements +/// +/// @relation(cli.account-checks) fn run_checks(action: ChecksAction, remote: &str) -> Result<(), String> { match action { ChecksAction::List => list::<Check>(remote), @@ -505,7 +519,9 @@ Ok(()) } -// r[impl cli.toolchains] - dispatch +/// ## Requirements +/// +/// @relation(cli.toolchains) fn run_toolchain(action: ToolchainAction, remote: &str) -> Result<(), String> { match action { ToolchainAction::Import { @@ -535,7 +551,10 @@ /// 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`) +/// +/// ## Requirements +/// +/// @relation(cli.toolchains) #[expect(clippy::too_many_arguments, reason = "one flag per import field")] fn toolchain_import( name: Option<String>, @@ -622,7 +641,7 @@ Ok(()) } -// r[impl cli.toolchains] - `list` +// @relation(cli.toolchains) /// Print every toolchain configured on `remote` as /// `<name> <bin> <version> <platform> <license>`. fn toolchain_list(remote: &str) -> Result<(), String> { @@ -675,8 +694,7 @@ Ok(()) } -// r[impl cli.toolchains] - `export` -// r[impl cli.remote-admin] - `toolchain export` is read-only +// @relation(cli.toolchains, cli.remote-admin) /// 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> { @@ -711,7 +729,7 @@ Ok(()) } -// r[impl cli.toolchains] - `remove` +// @relation(cli.toolchains) /// Remove toolchain `name` on `remote`, deleting its ref and pushing the /// update. fn toolchain_remove(name: &str, remote: &str) -> Result<(), String> { @@ -723,7 +741,9 @@ Ok(()) } -// r[impl cli.comments] - dispatch +/// ## Requirements +/// +/// @relation(cli.comments) fn run_comment(action: CommentAction, remote: &str) -> Result<(), String> { match action { CommentAction::Add { @@ -743,7 +763,10 @@ /// 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` +/// +/// ## Requirements +/// +/// @relation(cli.comments) fn comment_add( path: Option<String>, body: Option<String>, @@ -774,8 +797,7 @@ Ok(()) } -// r[impl cli.comments] - `list` -// r[impl cli.remote-admin] - `comment list` is read-only +// @relation(cli.comments, cli.remote-admin) /// 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> { @@ -797,8 +819,7 @@ Ok(()) } -// r[impl cli.comments] - `show` -// r[impl cli.remote-admin] - `comment show` is read-only +// @relation(cli.comments, cli.remote-admin) /// 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. @@ -852,7 +873,7 @@ Ok(()) } -// r[impl cli.comments] - `remove` +// @relation(cli.comments) /// 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> { @@ -997,8 +1018,7 @@ } } -// r[impl cli.account-checks] - `checks list` -// r[impl cli.remote-admin] - a `Set` listing is read-only +// @relation(cli.account-checks, cli.remote-admin) /// Print each entry of the set `S` on `remote` as `<key> <value>`. fn list<S: Set>(remote: &str) -> Result<(), String> { let repo = repo()?; @@ -1014,7 +1034,7 @@ Ok(()) } -// r[impl cli.account-checks] - `checks remove` +// @relation(cli.account-checks) /// 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()?; @@ -1039,7 +1059,10 @@ /// 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` +/// +/// ## Requirements +/// +/// @relation(cli.account-checks) fn add_check( name: Option<String>, command: Option<String>, @@ -1105,8 +1128,10 @@ /// (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] +/// +/// ## Requirements +/// +/// @relation(cli.members, auth.client-setup) fn setup(key: Option<&Path>, local: bool) -> Result<(), String> { let scope = if local { "--local" } else { "--global" }; let signing_key = match key { @@ -1134,7 +1159,10 @@ /// 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` +/// +/// ## Requirements +/// +/// @relation(cli.key-resolution) fn ensure_key(path: &Path) -> Result<String, String> { let (private, public) = key_paths(path); if public.exists() { @@ -1253,8 +1281,10 @@ /// `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 +/// +/// ## Requirements +/// +/// @relation(cli.members, cli.remote-admin) fn members_list(remote: &str) -> Result<(), String> { let repo = repo()?; sync_namespace(remote, MEMBER_NS)?; @@ -1293,7 +1323,10 @@ /// 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` +/// +/// ## Requirements +/// +/// @relation(cli.members) fn members_revoke(fingerprint: &str, remote: &str, reason: String) -> Result<(), String> { if !looks_like_fingerprint(fingerprint) { return Err(format!( @@ -1305,7 +1338,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 + // @relation(cli.members) if own_fingerprint().is_some_and(|own| own == fingerprint) && !confirm(&format!( "{fingerprint} is your own signing key; \ @@ -1335,7 +1368,10 @@ /// Remove `fingerprint` from `remote`'s `refs/meta/revoked` deny list and push /// the update. -// r[impl cli.members] - `unrevoke` +/// +/// ## Requirements +/// +/// @relation(cli.members) fn members_unrevoke(fingerprint: &str, remote: &str) -> Result<(), String> { let repo = repo()?; let expected = sync(remote, REVOKED_REF)?; @@ -1355,7 +1391,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(cli.interactive) fn resolve_trust( key: Option<PathBuf>, cert_authority: Option<PathBuf>, @@ -1383,7 +1422,9 @@ clippy::too_many_arguments, reason = "each argument is an independent, optional member field set from its own CLI flag" )] -// r[impl cli.members] - `add` +/// ## Requirements +/// +/// @relation(cli.members) fn members_add( username: Option<String>, remote: &str, @@ -1471,7 +1512,10 @@ /// 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` +/// +/// ## Requirements +/// +/// @relation(cli.members) fn members_remove(username: &str, remote: &str) -> Result<(), String> { let refname = member_ref(username); let expected = @@ -1481,7 +1525,7 @@ Ok(()) } -// r[impl cli.account-checks] - `account create` +// @relation(cli.account-checks) /// Create or update this repository's account identity on `remote` and push it. fn account_create( username: Option<String>, @@ -1527,7 +1571,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(cli.login) fn login(remote: &str, key: Option<&Path>) -> Result<(), String> { let (base, _repo_path) = remote_http_base(remote)?; let private_key = signing_key_file(key)?; @@ -1550,7 +1597,10 @@ /// 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` +/// +/// ## Requirements +/// +/// @relation(checks.debug) fn checks_debug(remote: &str) -> Result<(), String> { let (base, repo_path) = remote_http_base(remote)?; let host = host_of(&base)?; @@ -1565,7 +1615,10 @@ /// 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` +/// +/// ## Requirements +/// +/// @relation(cli.key-resolution) fn signing_key_file(key: Option<&Path>) -> Result<PathBuf, String> { match key { Some(path) => Ok(key_paths(path).0), @@ -1580,7 +1633,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(cli.login) 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"); @@ -1770,8 +1826,7 @@ } } -// r[impl cli.members] - `check` -// r[impl cli.remote-admin] - `members check` is read-only +// @relation(cli.members, cli.remote-admin) /// Report whether `key` is a member on `remote` and how this client is /// configured. fn check(remote: &str, key: Option<&Path>) -> Result<(), String> { @@ -1812,7 +1867,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(cli.remote-admin) 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); @@ -1828,7 +1886,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(cli.remote-admin) fn sync_namespace(remote: &str, namespace: &str) -> Result<(), String> { let refspec = format!("+{namespace}/*:{namespace}/*"); git_run(&["fetch", "--quiet", "--prune", remote, &refspec]) @@ -1840,8 +1901,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(cli.remote-admin, cli.compare-and-swap) fn push_signed(remote: &str, refname: &str, expected: Option<&str>) -> Result<(), String> { let lease = format!( "--force-with-lease={refname}:{}", @@ -1853,8 +1916,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(cli.remote-admin, cli.compare-and-swap) fn push_delete(remote: &str, refname: &str, expected: &str) -> Result<(), String> { let lease = format!("--force-with-lease={refname}:{expected}"); let refspec = format!(":{refname}"); @@ -1863,7 +1928,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(cli.key-resolution) fn public_key(key: Option<&Path>) -> Result<String, String> { match key { Some(path) => read_public_key(path), @@ -1880,7 +1948,10 @@ /// 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` +/// +/// ## Requirements +/// +/// @relation(cli.key-resolution) fn read_public_key(path: &Path) -> Result<String, String> { if let Ok(contents) = std::fs::read_to_string(path) && looks_like_public_key(&contents) @@ -1929,7 +2000,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(cli.key-resolution) 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,7 +74,10 @@ /// 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>` +/// +/// ## Requirements +/// +/// @relation(cli.toolchains) pub fn resolve(recipe: &str, spec: &str, embed: bool) -> Result<Resolved, String> { match recipe { "rustup" => rustup(spec, embed), @@ -129,7 +132,10 @@ /// `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 +/// +/// ## Requirements +/// +/// @relation(cli.toolchains) 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,8 +129,7 @@ } } -// r[impl storage.meta-ref] -// r[impl nonfunctional.object-store] - object IO is opened on the common git directory, never a receive-pack quarantine +// @relation(storage.meta-ref, nonfunctional.object-store) /// A repository's typed `refs/meta/*` store. /// /// Refs are read and updated through the high-level [`gix`] API, while all @@ -144,7 +143,7 @@ } impl Store { - // r[impl nonfunctional.object-store] - opens the object database at `common_dir()/objects` explicitly + // @relation(nonfunctional.object-store) /// 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)))?; @@ -168,7 +167,10 @@ /// 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 + /// + /// ## Requirements + /// + /// @relation(storage.concurrency) pub fn store<T: for<'a> Facet<'a>>( &self, refname: &str, @@ -192,7 +194,9 @@ self.store_impl(refname, value, message, Some(author)) } - // r[impl storage.concurrency] - CAS-and-merge retry loop + /// ## Requirements + /// + /// @relation(storage.concurrency) fn store_impl<T: for<'a> Facet<'a>>( &self, refname: &str, @@ -234,7 +238,10 @@ /// 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 + /// + /// ## Requirements + /// + /// @relation(storage.concurrency) pub fn amend<T: for<'a> Facet<'a>>( &self, refname: &str, @@ -941,7 +948,7 @@ tags: BTreeMap<String, String>, } - // r[verify storage.concurrency] - non-overlapping struct fields merge cleanly + // @relation(storage.concurrency, role=Verifies) #[test] fn merge_disjoint_struct_fields_combine() { let dir = repo(); @@ -977,7 +984,7 @@ ); } - // r[verify storage.concurrency] - non-overlapping map entries merge cleanly + // @relation(storage.concurrency, role=Verifies) #[test] fn merge_disjoint_map_entries_combine() { let dir = repo(); @@ -1006,7 +1013,7 @@ assert_eq!(merged.tags, entries(&[("x", "1"), ("y", "2")])); } - // r[verify storage.concurrency] - a genuine conflict fails cleanly instead of picking a winner + // @relation(storage.concurrency, role=Verifies) #[test] fn merge_same_scalar_changed_both_ways_conflicts() { let dir = repo(); @@ -1082,7 +1089,7 @@ assert!(matches!(result, Err(Error::Conflict))); } - // r[verify storage.concurrency] - CAS detects a moved ref + // @relation(storage.concurrency, role=Verifies) #[test] fn try_set_ref_conflicts_on_a_stale_expected() { let dir = repo(); @@ -1105,7 +1112,7 @@ assert!(matches!(result, Err(Error::Conflict))); } - // r[verify storage.concurrency] - no common ancestor cannot be merged + // @relation(storage.concurrency, role=Verifies) #[test] fn store_conflicts_on_a_fresh_ref_race_with_no_common_base() { let dir = repo(); @@ -1125,7 +1132,7 @@ assert!(matches!(result, Err(Error::Conflict))); } - // r[verify storage.concurrency] - a state advance fails closed rather than merging + // @relation(storage.concurrency, role=Verifies) #[test] fn amend_fails_closed_on_a_race_instead_of_merging() { let dir = repo();
crates/git-ents-server/src/web/debug.rs @@ -30,7 +30,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.debug) pub(crate) async fn handshake( State(state): State<AppState>, Path(repo_path): Path<String>, @@ -91,7 +94,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.debug) async fn relay(mut socket: WebSocket, sprite: String) { let pair = match native_pty_system().openpty(INITIAL_SIZE) { Ok(pair) => pair, @@ -177,7 +183,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(checks.debug) fn parse_resize(text: &str) -> Option<PtySize> { let (cols, rows) = text.split_once(' ')?; Some(PtySize {
crates/git-ents-server/src/web/git.rs @@ -46,7 +46,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(nonfunctional.memory-cap) pub(super) async fn git_output_capped( repo: &Path, args: &[&str], @@ -346,7 +349,7 @@ } } - // r[verify nonfunctional.memory-cap] + // @relation(nonfunctional.memory-cap, role=Verifies) #[tokio::test] async fn capped_read_flags_oversized_output() { let dir = tempfile::tempdir().unwrap(); @@ -359,7 +362,7 @@ assert_eq!(bytes.len(), 1024); } - // r[verify nonfunctional.memory-cap] + // @relation(nonfunctional.memory-cap, role=Verifies) #[tokio::test] async fn capped_read_returns_full_small_output() { let dir = tempfile::tempdir().unwrap();
crates/git-ents-server/src/web/mod.rs @@ -50,8 +50,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.server-rendered, web.auth.challenge) pub(crate) async fn render( state: &AppState, path: &str, @@ -137,8 +139,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.auth.challenge, web.auth.session) pub(crate) async fn handle_post( state: &AppState, path: &str, @@ -194,7 +198,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.auth.edit) fn editing_enabled(state: &AppState) -> bool { state.cert_nonce_seed.is_some() && state.hooks_dir.is_some() && state.web_signing_key.is_some() } @@ -211,7 +218,10 @@ /// 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) +/// +/// ## Requirements +/// +/// @relation(web.auth.edit) async fn save_settings( state: &AppState, repo: &Path, @@ -279,7 +289,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.comments) async fn save_comment( state: &AppState, repo: &Path, @@ -365,7 +378,10 @@ } /// The `Set-Cookie` value that opens a session, marked `Secure` over HTTPS. -// r[impl web.auth.session] issues the `ents_session` cookie +/// +/// ## Requirements +/// +/// @relation(web.auth.session) fn session_cookie(token: &str, secure: bool) -> String { format!( "{}={token}; Path=/; HttpOnly; SameSite=Lax{}", @@ -375,7 +391,10 @@ } /// The `Set-Cookie` value that clears a session. -// r[impl web.auth.session] clears the cookie on sign-out +/// +/// ## Requirements +/// +/// @relation(web.auth.session) fn cleared_cookie(secure: bool) -> String { format!( "{}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax{}", @@ -387,8 +406,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.server-rendered, web.tabs) async fn route( repo: &Path, rel: &str, @@ -471,7 +492,10 @@ } /// The top-level tabs of a repository page. -// r[impl web.tabs] - the set of tabs a repository's web UI provides +/// +/// ## Requirements +/// +/// @relation(web.tabs) #[derive(Clone, Copy, PartialEq, Eq)] enum Tab { Overview, @@ -601,7 +625,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.tabs) fn tab_bar(meta: &RepoMeta, active: Tab) -> Markup { let rel = &meta.rel; html! { @@ -623,7 +650,10 @@ } /// The repository listing shown at `/`. -// r[impl web.index] +/// +/// ## Requirements +/// +/// @relation(web.index) fn index(state: &AppState, session: Option<&write::SessionSnapshot>) -> Markup { let repos = discover_repos(&state.data_dir); page( @@ -699,7 +729,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.auth.challenge) fn login_page( session: Option<&write::SessionSnapshot>, challenge: Option<&str>, @@ -764,7 +797,7 @@ } /// 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, +// @relation(web.server-rendered) // live check polling) are progressive enhancement, not required for navigation fn page(title: &str, body: Markup) -> Markup { html! {
crates/git-ents-server/src/web/pages.rs @@ -31,8 +31,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.syntax-highlight, nonfunctional.memory-cap) const MAX_RENDER_BYTES: usize = 2 * 1024 * 1024; /// Render an Askama tab-body template into [`Markup`] the Maud page shell can @@ -272,7 +274,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.tabs) pub(super) async fn files_page( repo: &Path, meta: &RepoMeta, @@ -526,8 +531,10 @@ /// [`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 +/// +/// ## Requirements +/// +/// @relation(web.tabs, web.syntax-highlight) pub(super) async fn blob_page( repo: &Path, meta: &RepoMeta, @@ -600,7 +607,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.syntax-highlight) fn blob_body(name: &str, source: &str) -> Markup { let lines = source.lines().count().max(1); let highlighted = highlight(name, source); @@ -625,7 +635,10 @@ /// (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`) +/// +/// ## Requirements +/// +/// @relation(web.syntax-highlight) fn highlight(name: &str, source: &str) -> Option<String> { let language = arborium::detect_language(name)?; let config = Config { @@ -659,7 +672,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.comments) async fn file_comments(repo: &Path, path: &str) -> Vec<FileComment> { let repo = repo.to_owned(); let path = path.to_owned(); @@ -703,7 +719,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.comments) fn comments_card(comments: &[FileComment], form: Option<Markup>) -> Markup { if comments.is_empty() && form.is_none() { return html! {}; @@ -735,7 +754,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.comments) fn comment_form( rel: &str, path: &str, @@ -759,7 +781,10 @@ } /// A single commit: its metadata and a colorized unified diff. -// r[impl web.tabs] - Commits: commit history with diff views +/// +/// ## Requirements +/// +/// @relation(web.tabs) 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(); @@ -818,7 +843,10 @@ } /// The Releases tab: tags presented as a changelog timeline, newest first. -// r[impl web.tabs] - Releases: git tags browsable by version +/// +/// ## Requirements +/// +/// @relation(web.tabs) pub(super) async fn releases_page(repo: &Path, meta: &RepoMeta) -> Markup { let releases = releases(repo).await; repo_shell( @@ -867,7 +895,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.tabs) 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; @@ -1116,7 +1147,10 @@ /// `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 +/// +/// ## Requirements +/// +/// @relation(web.tabs) 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 { @@ -1170,7 +1204,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.tabs) pub(super) async fn settings_page( repo: &Path, meta: &RepoMeta,
crates/git-ents-server/src/web/write.rs @@ -39,7 +39,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.auth.session) pub(crate) struct Session { /// The member's public key line (`type base64`), matched against members. public_key: String, @@ -83,7 +86,10 @@ } /// Issue a fresh one-time sign-in challenge, returning the nonce to sign. -// r[impl web.auth.challenge] server issues a one-time nonce +/// +/// ## Requirements +/// +/// @relation(web.auth.challenge) pub(super) fn issue_challenge(challenges: &Challenges) -> Result<String, String> { let nonce = random_token()?; let mut table = challenges @@ -96,7 +102,10 @@ } /// 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) +/// +/// ## Requirements +/// +/// @relation(web.auth.challenge) fn take_challenge(challenges: &Challenges, nonce: &str) -> bool { let Ok(mut table) = challenges.lock() else { return false; @@ -123,8 +132,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.auth.challenge, web.auth.session) pub(super) fn login( sessions: &Sessions, challenges: &Challenges, @@ -162,7 +173,10 @@ } /// 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 +/// +/// ## Requirements +/// +/// @relation(web.auth.session) 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; @@ -175,7 +189,10 @@ } /// Drop the session a `Cookie` header points at, if any. -// r[impl web.auth.session] sign-out drops the session from server memory +/// +/// ## Requirements +/// +/// @relation(web.auth.session) pub(super) fn logout(sessions: &Sessions, cookie: Option<&str>) { let Some(token) = cookie.and_then(token) else { return; @@ -191,7 +208,10 @@ /// `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 +/// +/// ## Requirements +/// +/// @relation(web.auth.edit) 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) @@ -214,7 +234,10 @@ /// `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 +/// +/// ## Requirements +/// +/// @relation(web.auth.edit) pub(super) fn edit_config( sessions: &Sessions, cookie: Option<&str>, @@ -254,7 +277,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.comments) pub(super) fn add_comment( sessions: &Sessions, cookie: Option<&str>, @@ -329,7 +355,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 +// @relation(web.auth.edit) #[expect( clippy::too_many_arguments, reason = "the server identity a signed edit requires" @@ -364,7 +390,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.auth.edit) #[expect(clippy::too_many_arguments, reason = "internal step of signed_edit")] fn stage_and_push<T: for<'a> facet::Facet<'a>>( repo: &Path, @@ -429,7 +458,10 @@ /// 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 +/// +/// ## Requirements +/// +/// @relation(web.auth.challenge) 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");
Diff truncated (over 1 MiB).