feat: add the refs/meta/config repository metadata document
commit
b3fbe43feat: add the refs/meta/config repository metadata document
Promote the repository’s description, homepage, and topics to a typed,
members-gated Config document on its own refs/meta/config ref, read
and written through git-store. Pinned against its on-disk layout with a
fixed-format load test built by a new write_config_doc testutil helper.
feat: add git_ents::config with Config/load/store
test: pin the on-disk config format against a fixed fixture
Assisted-by: Claude:claude-opus-4-8
Reviews
No reviews of this commit yet — record a verdict below.
Start a review
crates/git-ents/src/lib.rs
@@ -1,6 +1,7 @@
//! Git Ents — helpful guardians of your git trees.
pub mod checks;
+pub mod config;
pub mod signers;
#[cfg(test)]
mod testutil;
crates/git-ents/src/testutil.rs
@@ -61,6 +61,44 @@
assert!(status.success());
}
+/// Lay a `Config` document out at `refname` as the real on-disk format: a
+/// `description` blob, a `homepage` blob, and a `topics/` subtree of index-keyed
+/// (`0000`, `0001`, …) blobs, committed and pointed to by the ref. Asserts the
+/// loader still reads the format independent of the writer.
+pub(crate) fn write_config_doc(
+ repo: &Path,
+ refname: &str,
+ description: &str,
+ homepage: &str,
+ topics: &[&str],
+) {
+ let description_blob = git_with_stdin(repo, &["hash-object", "-w", "--stdin"], description);
+ let homepage_blob = git_with_stdin(repo, &["hash-object", "-w", "--stdin"], homepage);
+ let mut topic_entries = String::new();
+ for (index, topic) in topics.iter().enumerate() {
+ let blob = git_with_stdin(repo, &["hash-object", "-w", "--stdin"], topic);
+ topic_entries.push_str(&format!("100644 blob {blob}\t{index:04}\n"));
+ }
+ let topics_tree = git_with_stdin(repo, &["mktree"], &topic_entries);
+ let root = git_with_stdin(
+ repo,
+ &["mktree"],
+ &format!(
+ "100644 blob {description_blob}\tdescription\n\
+ 100644 blob {homepage_blob}\thomepage\n\
+ 040000 tree {topics_tree}\ttopics\n"
+ ),
+ );
+ let commit = git_with_stdin(repo, &["commit-tree", &root, "-m", "fixture"], "");
+ let status = Command::new("git")
+ .arg("-C")
+ .arg(repo)
+ .args(["update-ref", refname, &commit])
+ .status()
+ .unwrap();
+ assert!(status.success());
+}
+
/// Run git in `repo` with `input` on stdin, returning its trimmed stdout.
fn git_with_stdin(repo: &Path, args: &[&str], input: &str) -> String {
let mut child = Command::new("git")
crates/git-ents/src/config.rs
@@ -1,0 +1,119 @@
+//! The repository's metadata, sourced from the `refs/meta/config` ref.
+//!
+//! A repository's loose metadata — its description, homepage, and topics — is
+//! first-class, members-gated, versioned data rather than worktree content or
+//! a loose git file. It lives on exactly one ref, `refs/meta/config`, whose
+//! tree is a [`Config`] document read and written through [`git_store`]. Keeping
+//! it on a meta ref (not in the worktree) means anyone who can push content
+//! cannot rewrite the repository's metadata, and the metadata carries its own
+//! independent history.
+
+use std::path::Path;
+
+use facet::Facet;
+
+/// The ref whose tree holds the repository configuration.
+pub const CONFIG_REF: &str = "refs/meta/config";
+
+/// The repository configuration stored at [`CONFIG_REF`].
+#[derive(Debug, Clone, Default, PartialEq, Eq, Facet)]
+pub struct Config {
+ /// The repository's description (was git's `.git/description` file).
+ pub description: String,
+ /// The repository's homepage URL; `""` when unset.
+ pub homepage: String,
+ /// The repository's topics (migrated off the tracked `HEAD:.gitents/topics`).
+ pub topics: Vec<String>,
+}
+
+/// A failure reading or writing the configuration.
+#[derive(Debug, thiserror::Error)]
+pub enum Error {
+ /// The configuration could not be read from or written to its ref.
+ #[error(transparent)]
+ Store(#[from] git_store::Error),
+}
+
+/// Load the configuration recorded at [`CONFIG_REF`] in `repo`.
+///
+/// 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".
+pub fn load(repo: &Path) -> Result<Config, Error> {
+ Ok(git_store::Store::open(repo)?
+ .load::<Config>(CONFIG_REF)?
+ .unwrap_or_default())
+}
+
+/// Write `config` to [`CONFIG_REF`], replacing any existing value, as a new
+/// commit.
+pub fn store(repo: &Path, config: &Config) -> Result<(), Error> {
+ git_store::Store::open(repo)?.store(CONFIG_REF, config, "Update configuration")?;
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ #![allow(
+ clippy::unwrap_used,
+ clippy::let_underscore_must_use,
+ reason = "unit test"
+ )]
+
+ use super::*;
+ use crate::testutil::{unique_repo as new_repo, write_config_doc};
+
+ fn unique_repo() -> std::path::PathBuf {
+ new_repo("config")
+ }
+
+ fn config() -> Config {
+ Config {
+ description: "A repository".to_owned(),
+ homepage: "https://example.com".to_owned(),
+ topics: vec!["rust".to_owned(), "git".to_owned()],
+ }
+ }
+
+ #[test]
+ fn store_then_load_round_trips_the_config() {
+ let repo = unique_repo();
+ store(&repo, &config()).unwrap();
+ assert_eq!(load(&repo).unwrap(), config());
+ let _ = std::fs::remove_dir_all(&repo);
+ }
+
+ #[test]
+ fn store_replaces_the_previous_config() {
+ let repo = unique_repo();
+ store(&repo, &config()).unwrap();
+ store(&repo, &Config::default()).unwrap();
+ assert_eq!(load(&repo).unwrap(), Config::default());
+ let _ = std::fs::remove_dir_all(&repo);
+ }
+
+ #[test]
+ fn loads_the_on_disk_config_format() {
+ // A fixture written as the real on-disk layout — `description` and
+ // `homepage` blobs plus an index-keyed `topics/` subtree — must keep
+ // loading, guarding the Config document's shape against an incompatible
+ // change to data already on a ref.
+ let repo = unique_repo();
+ write_config_doc(
+ &repo,
+ CONFIG_REF,
+ "A repository",
+ "https://example.com",
+ &["rust", "git"],
+ );
+ assert_eq!(load(&repo).unwrap(), config());
+ let _ = std::fs::remove_dir_all(&repo);
+ }
+
+ #[test]
+ fn default_when_the_config_ref_is_absent() {
+ let repo = unique_repo();
+ assert_eq!(load(&repo).unwrap(), Config::default());
+ let _ = std::fs::remove_dir_all(&repo);
+ }
+}