docs: refactor tracey annotations to StrictDoc's @relation syntax
commit
ea8a28fdocs: 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
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
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).