4bf31d06
feat(server): scaffold readme module + no-readme test
alex emery 2026-04-12 17:30
diff --git a/src/server/http/repo/mod.rs b/src/server/http/repo/mod.rs index 650ee0c..441e755 100644 --- a/src/server/http/repo/mod.rs +++ b/src/server/http/repo/mod.rs @@ -4,6 +4,7 @@ mod tree; mod diff; mod patches; mod issues; mod readme; pub use overview::overview; pub use commits::{commits, commits_ref}; diff --git a/src/server/http/repo/readme.rs b/src/server/http/repo/readme.rs new file mode 100644 index 0000000..7ac24c6 --- /dev/null +++ b/src/server/http/repo/readme.rs @@ -0,0 +1,85 @@ //! README lookup, decoding, and rendering for the repo overview page. //! //! Pure: no Axum / AppState dependencies, so this is unit-testable against //! fixture repos built with `git2::Repository::init`. use git2::Repository; /// Already-safe HTML ready to be rendered with `|safe` in the template. pub struct RenderedReadme { pub html: String, } /// Load and render the README at the root of HEAD's tree. /// /// Returns `None` when no README is present, the blob is binary or invalid /// UTF-8, or any unexpected git2 error occurs. The README must never break /// the overview page — failures degrade silently to "no README". pub fn load_readme(repo: &Repository) -> Option<RenderedReadme> { let head = repo.head().ok()?; let commit = head.peel_to_commit().ok()?; let tree = commit.tree().ok()?; let _entry = find_readme_entry(&tree)?; None // Task 3 will replace this } /// One root-tree entry. Carries the resolved category so the renderer can /// branch on type without re-parsing the filename. #[allow(dead_code)] #[derive(Debug)] struct ReadmeEntry { name: String, oid: git2::Oid, kind: ReadmeKind, } #[allow(dead_code)] #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum ReadmeKind { Markdown, Plain, } /// Walk the root tree (non-recursive) and pick the highest-precedence README /// blob entry. Symlinks and tree entries are ignored. #[allow(dead_code)] fn find_readme_entry(_tree: &git2::Tree) -> Option<ReadmeEntry> { None // Task 3 implements the real walk } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; /// Build an empty git repo with a single commit containing the given /// (path, contents) blobs at the root tree. Returns the repo and the /// tempdir (kept alive by the caller). fn repo_with_files(files: &[(&str, &[u8])]) -> (Repository, TempDir) { let tmp = TempDir::new().unwrap(); let repo = Repository::init(tmp.path()).unwrap(); let sig = git2::Signature::now("Test", "test@example.com").unwrap(); { let tree_oid = { let mut builder = repo.treebuilder(None).unwrap(); for (name, contents) in files { let oid = repo.blob(contents).unwrap(); builder.insert(name, oid, 0o100644).unwrap(); } builder.write().unwrap() }; let tree = repo.find_tree(tree_oid).unwrap(); repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[]).unwrap(); } (repo, tmp) } #[test] fn no_readme_returns_none() { let (repo, _tmp) = repo_with_files(&[("lib.rs", b"fn main() {}")]); assert!(load_readme(&repo).is_none()); } }