add repo detail view
This commit is contained in:
@@ -10,3 +10,6 @@ signed_core = { path = "../signed_core" }
|
||||
nostr.workspace = true
|
||||
gix.workspace = true
|
||||
anyhow.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -140,6 +140,94 @@ fn sanitize_path_component(id: &str) -> String {
|
||||
sanitized
|
||||
}
|
||||
|
||||
/// Relative paths of all entries in the worktree (files and directories),
|
||||
/// directories first, then alphabetically within each group. The `.git`
|
||||
/// directory is skipped.
|
||||
pub fn worktree_entries(repo: &gix::Repository) -> Result<Vec<PathBuf>> {
|
||||
let workdir = repo.workdir().context("repository has no worktree")?;
|
||||
|
||||
let mut entries: Vec<(PathBuf, bool)> = Vec::new();
|
||||
collect_entries(workdir, workdir, &mut entries)?;
|
||||
|
||||
entries.sort_by(|(a, a_is_dir), (b, b_is_dir)| {
|
||||
b_is_dir
|
||||
.cmp(a_is_dir)
|
||||
.then_with(|| a.as_os_str().cmp(b.as_os_str()))
|
||||
});
|
||||
Ok(entries.into_iter().map(|(path, _)| path).collect())
|
||||
}
|
||||
|
||||
/// Read a file from the worktree. Returns `Ok(None)` if the path is missing
|
||||
/// or not a regular file.
|
||||
pub fn worktree_read(repo: &gix::Repository, rel: &Path) -> Result<Option<Vec<u8>>> {
|
||||
let workdir = repo.workdir().context("repository has no worktree")?;
|
||||
let path = workdir.join(rel);
|
||||
|
||||
match std::fs::read(&path) {
|
||||
Ok(bytes) => Ok(Some(bytes)),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::IsADirectory => Ok(None),
|
||||
Err(e) => Err(e).with_context(|| format!("failed to read {}", path.display())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the README file in the repository root (returned as a path relative
|
||||
/// to the worktree). Case-insensitive; prefers `README.md`, then `.markdown`,
|
||||
/// `.mdown`, `.mkdn`, then any other file whose name starts with `readme`.
|
||||
pub fn find_readme(repo: &gix::Repository) -> Result<Option<PathBuf>> {
|
||||
let Some(workdir) = repo.workdir() else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let mut candidates: Vec<PathBuf> = Vec::new();
|
||||
for entry in std::fs::read_dir(workdir)? {
|
||||
let entry = entry?;
|
||||
let name = entry.file_name();
|
||||
let Some(name) = name.to_str() else { continue };
|
||||
if name.to_ascii_lowercase().starts_with("readme") {
|
||||
candidates.push(entry.path());
|
||||
}
|
||||
}
|
||||
|
||||
candidates.sort_by_key(|path| {
|
||||
let ext = path
|
||||
.extension()
|
||||
.map(|e| e.to_string_lossy().to_ascii_lowercase());
|
||||
match ext.as_deref() {
|
||||
Some("md") => 0,
|
||||
Some("markdown") => 1,
|
||||
Some("mdown") => 2,
|
||||
Some("mkdn") => 3,
|
||||
Some(_) => 5,
|
||||
None => 4,
|
||||
}
|
||||
});
|
||||
|
||||
Ok(candidates
|
||||
.into_iter()
|
||||
.next()
|
||||
.and_then(|path| path.strip_prefix(workdir).ok().map(Path::to_path_buf)))
|
||||
}
|
||||
|
||||
fn collect_entries(root: &Path, dir: &Path, out: &mut Vec<(PathBuf, bool)>) -> Result<()> {
|
||||
for entry in std::fs::read_dir(dir)? {
|
||||
let entry = entry?;
|
||||
if entry.file_name() == ".git" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let is_dir = entry.file_type()?.is_dir();
|
||||
let path = entry.path();
|
||||
let rel = path.strip_prefix(root)?.to_path_buf();
|
||||
out.push((rel, is_dir));
|
||||
|
||||
if is_dir {
|
||||
collect_entries(root, &path, out)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use nostr::prelude::*;
|
||||
@@ -181,4 +269,92 @@ mod tests {
|
||||
Some("_".into())
|
||||
);
|
||||
}
|
||||
|
||||
/// Build a throwaway non-bare repository with the given files (rel → bytes).
|
||||
fn fixture(files: &[(&str, &[u8])]) -> (tempfile::TempDir, gix::Repository) {
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let repo = gix::init(&dir).expect("init");
|
||||
|
||||
for (rel, bytes) in files {
|
||||
let path = dir.path().join(rel);
|
||||
std::fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
|
||||
std::fs::write(&path, bytes).expect("write");
|
||||
}
|
||||
|
||||
(dir, repo)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worktree_entries_lists_all_files_and_dirs() {
|
||||
let (_dir, repo) = fixture(&[
|
||||
("README.md", b"# Hi"),
|
||||
("src/main.rs", b"fn main() {}"),
|
||||
("src/lib.rs", b""),
|
||||
("docs/guide.md", b"guide"),
|
||||
]);
|
||||
|
||||
let entries = worktree_entries(&repo).expect("entries");
|
||||
let entries: Vec<String> = entries
|
||||
.iter()
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
entries,
|
||||
vec![
|
||||
"docs",
|
||||
"src",
|
||||
"README.md",
|
||||
"docs/guide.md",
|
||||
"src/lib.rs",
|
||||
"src/main.rs"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worktree_read_returns_bytes_or_none() {
|
||||
let (_dir, repo) = fixture(&[("a.txt", b"hello"), ("sub/b.bin", b"\x00\x01")]);
|
||||
|
||||
assert_eq!(
|
||||
worktree_read(&repo, Path::new("a.txt")).expect("read"),
|
||||
Some(b"hello".to_vec())
|
||||
);
|
||||
assert_eq!(
|
||||
worktree_read(&repo, Path::new("sub/b.bin")).expect("read"),
|
||||
Some(vec![0x00, 0x01])
|
||||
);
|
||||
assert_eq!(
|
||||
worktree_read(&repo, Path::new("missing.txt")).expect("read"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_readme_prefers_markdown() {
|
||||
let (_dir, repo) = fixture(&[("readme.txt", b"txt"), ("README.md", b"md")]);
|
||||
|
||||
let readme = find_readme(&repo).expect("find");
|
||||
assert_eq!(
|
||||
readme.map(|p| p.to_string_lossy().into_owned()),
|
||||
Some("README.md".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_readme_falls_back_to_any_readme() {
|
||||
let (_dir, repo) = fixture(&[("README.rst", b"rst")]);
|
||||
|
||||
let readme = find_readme(&repo).expect("find");
|
||||
assert_eq!(
|
||||
readme.map(|p| p.to_string_lossy().into_owned()),
|
||||
Some("README.rst".into())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_readme_returns_none_without_one() {
|
||||
let (_dir, repo) = fixture(&[("main.rs", b"")]);
|
||||
assert!(find_readme(&repo).expect("find").is_none());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user