a73x

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());
    }
}