a73x

4d3a606f

support description

a73x   2026-04-04 07:09


diff --git a/src/server/http/repo_list.rs b/src/server/http/repo_list.rs
index cdc2fdc..111ea63 100644
--- a/src/server/http/repo_list.rs
+++ b/src/server/http/repo_list.rs
@@ -35,8 +35,9 @@ fn build_repo_list(state: &AppState) -> Vec<RepoListItem> {
    entries
        .into_iter()
        .map(|entry| {
            let description = read_description(&entry);
            let last_commit = read_last_commit(&entry);
            let repo = crate::repos::open(&entry).ok();
            let description = resolve_description(&entry, repo.as_ref());
            let last_commit = repo.as_ref().map(read_last_commit).unwrap_or_default();
            RepoListItem {
                name: entry.name,
                description,
@@ -46,34 +47,68 @@ fn build_repo_list(state: &AppState) -> Vec<RepoListItem> {
        .collect()
}

fn read_description(entry: &crate::repos::RepoEntry) -> String {
fn normalize_description(description: &str) -> Option<String> {
    let description = description.trim();
    if description.is_empty() || description.starts_with("Unnamed repository") {
        None
    } else {
        Some(description.to_string())
    }
}

fn resolve_display_commit(repo: &git2::Repository) -> Option<git2::Commit<'_>> {
    if let Ok(head) = repo.head() {
        if let Ok(commit) = head.peel_to_commit() {
            return Some(commit);
        }
    }

    let branches = repo.branches(Some(git2::BranchType::Local)).ok()?;
    for branch in branches.filter_map(Result::ok) {
        let (branch, _) = branch;
        if let Ok(commit) = branch.into_reference().peel_to_commit() {
            return Some(commit);
        }
    }

    None
}

fn read_tracked_description(repo: &git2::Repository) -> Option<String> {
    let commit = resolve_display_commit(repo)?;
    let tree = commit.tree().ok()?;
    let entry = tree.get_path(std::path::Path::new(".description")).ok()?;
    let blob = entry.to_object(repo).ok()?.into_blob().ok()?;
    let description = std::str::from_utf8(blob.content()).ok()?;
    normalize_description(description)
}

fn read_local_description(entry: &crate::repos::RepoEntry) -> Option<String> {
    let desc_path = if entry.bare {
        entry.path.join("description")
    } else {
        entry.path.join(".git").join("description")
    };

    std::fs::read_to_string(&desc_path)
        .ok()
        .map(|s| s.trim().to_string())
        .filter(|s| !s.is_empty() && !s.starts_with("Unnamed repository"))
        .unwrap_or_default()
    let description = std::fs::read_to_string(&desc_path).ok()?;
    normalize_description(&description)
}

fn read_last_commit(entry: &crate::repos::RepoEntry) -> String {
    let repo = match crate::repos::open(entry) {
        Ok(r) => r,
        Err(_) => return String::new(),
    };
fn resolve_description(entry: &crate::repos::RepoEntry, repo: Option<&git2::Repository>) -> String {
    repo.and_then(read_tracked_description)
        .or_else(|| read_local_description(entry))
        .unwrap_or_default()
}

    let head = match repo.head() {
        Ok(h) => h,
        Err(_) => return String::new(),
    };
fn read_description(entry: &crate::repos::RepoEntry) -> String {
    let repo = crate::repos::open(entry).ok();
    resolve_description(entry, repo.as_ref())
}

    let commit = match head.peel_to_commit() {
        Ok(c) => c,
        Err(_) => return String::new(),
fn read_last_commit(repo: &git2::Repository) -> String {
    let commit = match resolve_display_commit(repo) {
        Some(commit) => commit,
        None => return String::new(),
    };

    let time = commit.time();
@@ -85,3 +120,77 @@ fn read_last_commit(entry: &crate::repos::RepoEntry) -> String {
        .map(|dt| dt.format("%Y-%m-%d").to_string())
        .unwrap_or_default()
}

#[cfg(test)]
mod tests {
    use super::{read_description, read_local_description};
    use std::path::Path;
    use std::process::Command;
    use tempfile::TempDir;

    fn git(cwd: &Path, args: &[&str]) {
        let output = Command::new("git")
            .args(args)
            .current_dir(cwd)
            .output()
            .expect("git command should run");
        assert!(
            output.status.success(),
            "git {:?} failed: stdout={} stderr={}",
            args,
            String::from_utf8_lossy(&output.stdout),
            String::from_utf8_lossy(&output.stderr)
        );
    }

    fn make_non_bare_entry(path: &Path) -> crate::repos::RepoEntry {
        crate::repos::RepoEntry {
            name: "repo".to_string(),
            path: path.to_path_buf(),
            bare: false,
        }
    }

    fn make_bare_entry(path: &Path) -> crate::repos::RepoEntry {
        crate::repos::RepoEntry {
            name: "repo".to_string(),
            path: path.to_path_buf(),
            bare: true,
        }
    }

    #[test]
    fn read_description_prefers_tracked_description_for_bare_repo() {
        let tmp = TempDir::new().unwrap();
        let bare_repo = tmp.path().join("repo.git");
        let work_repo = tmp.path().join("work");

        git(tmp.path(), &["init", "--bare", "repo.git"]);
        git(tmp.path(), &["clone", bare_repo.to_str().unwrap(), "work"]);
        git(&work_repo, &["config", "user.name", "Test User"]);
        git(&work_repo, &["config", "user.email", "test@example.com"]);
        git(&work_repo, &["checkout", "-b", "main"]);
        std::fs::write(work_repo.join(".description"), "Tracked description\n").unwrap();
        git(&work_repo, &["add", ".description"]);
        git(&work_repo, &["commit", "-m", "Add tracked description"]);
        git(&work_repo, &["push", "-u", "origin", "main"]);

        std::fs::write(bare_repo.join("description"), "Local bare description\n").unwrap();

        let entry = make_bare_entry(&bare_repo);
        assert_eq!(read_description(&entry), "Tracked description");
    }

    #[test]
    fn read_local_description_falls_back_to_git_metadata_file() {
        let tmp = TempDir::new().unwrap();
        let repo_path = tmp.path().join("repo");

        git(tmp.path(), &["init", "repo"]);
        std::fs::write(repo_path.join(".git").join("description"), "Local repo description\n").unwrap();

        let entry = make_non_bare_entry(&repo_path);
        assert_eq!(read_local_description(&entry), Some("Local repo description".to_string()));
        assert_eq!(read_description(&entry), "Local repo description");
    }
}