forks/sorcery

git clone https://git.gae.moe/forks/sorcery

Charlotte Somsort repos in catalog by author date of tip commit of HEADe759987

darwin-flake
3.9 KiB120 linesraw
1use std::collections::BTreeMap;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result};
6
7use crate::render;
8
9#[derive(Clone, Debug)]
10pub struct Repository {
11    pub user: String,
12    pub name: String,
13    pub path: PathBuf,
14    pub description: Option<String>,
15    /// Author time of the HEAD tip; `None` until the repository has commits.
16    pub updated: Option<gix::date::Time>,
17}
18
19/// Discover git repositories exactly two levels beneath `root`:
20/// `root/user/repo` or `root/user/repo.git`.
21/// Grouped by user, most recently updated first within each group.
22pub fn discover(root: &Path) -> Result<Vec<Repository>> {
23    let root = root
24        .canonicalize()
25        .with_context(|| format!("canonicalizing repository root {}", root.display()))?;
26    let mut found = BTreeMap::new();
27
28    for user in sorted_dirs(&root)? {
29        let Some(user_name) = user.file_name().and_then(|n| n.to_str()) else {
30            continue;
31        };
32        if user_name.starts_with('.') {
33            continue;
34        }
35        for path in sorted_dirs(&user)? {
36            let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
37                continue;
38            };
39            if file_name.starts_with('.') {
40                continue;
41            }
42            let name = file_name.strip_suffix(".git").unwrap_or(file_name);
43            let Ok(repo) = gix::open(&path) else {
44                continue;
45            };
46            if name.is_empty() {
47                continue;
48            }
49            let description = crate::generate::description(&repo);
50            let updated = render::list_refs(&repo).ok().and_then(|tips| {
51                let tip = render::head_tip(&repo, &tips)?;
52                render::author_time(&repo.find_commit(tip.commit_id).ok()?)
53            });
54            found.entry((user_name.to_owned(), name.to_owned())).or_insert(Repository {
55                user: user_name.to_owned(),
56                name: name.to_owned(),
57                path,
58                description,
59                updated,
60            });
61        }
62    }
63
64    let mut repositories = found.into_values().collect::<Vec<_>>();
65    repositories.sort_by(|a, b| {
66        a.user
67            .cmp(&b.user)
68            .then_with(|| b.updated.map(|t| t.seconds).cmp(&a.updated.map(|t| t.seconds)))
69            .then_with(|| a.name.cmp(&b.name))
70    });
71    Ok(repositories)
72}
73
74fn sorted_dirs(path: &Path) -> Result<Vec<PathBuf>> {
75    let mut dirs = fs::read_dir(path)
76        .with_context(|| format!("reading {}", path.display()))?
77        .filter_map(|entry| entry.ok())
78        .filter_map(|entry| entry.file_type().ok()?.is_dir().then_some(entry.path()))
79        .collect::<Vec<_>>();
80    dirs.sort();
81    Ok(dirs)
82}
83
84
85#[cfg(test)]
86mod tests {
87    use anyhow::Result;
88
89    use crate::testutil::{TempDir, git, init_repo};
90
91    use super::discover;
92
93    fn commit_at(repo: &std::path::Path, seconds: u64) -> Result<()> {
94        git(repo, &["commit", "-q", "--allow-empty", "-m", "x", &format!("--date=@{seconds} +0000")])?;
95        Ok(())
96    }
97
98    #[test]
99    fn orders_by_head_author_time_within_each_user() -> Result<()> {
100        let root = TempDir::new("catalog-order");
101        for repo in ["alice/old", "alice/new", "alice/empty", "bob/repo"] {
102            init_repo(&root.join(repo))?;
103        }
104        commit_at(&root.join("alice/new"), 2_000_000_000)?;
105        commit_at(&root.join("bob/repo"), 3_000_000_000)?;
106        let old = root.join("alice/old");
107        commit_at(&old, 1_000_000_000)?;
108        // only HEAD counts, not newer work on other branches
109        git(&old, &["switch", "-qc", "side"])?;
110        commit_at(&old, 4_000_000_000)?;
111        git(&old, &["switch", "-q", "main"])?;
112
113        let order = discover(&root)?
114            .into_iter()
115            .map(|r| format!("{}/{}", r.user, r.name))
116            .collect::<Vec<_>>();
117        assert_eq!(order, ["alice/new", "alice/old", "alice/empty", "bob/repo"]);
118        Ok(())
119    }
120}