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