fix(grasp06): exclude /prs/ subtree from cleanup-empty-repos orphan scan

The cleanup-empty-repos subcommand scans <git_data_path> for bare repos
that have no matching kind 30617 announcement and reports them as
orphans. Every repo under <git_data_path>/prs/ would be flagged this
way because the GRASP-06 endpoint stores contributor submissions
outside the standard <npub>/<id>.git layout and never has an
announcement.

Skip the prs first-level entry up front using is_prs_repo_path so the
scan walks only standard owner directories. The /prs/ subtree has its
own lifecycle (zero-ref cleanup in the receive handler and a separate
periodic sweep) and must not be touched by this scan.
This commit is contained in:
DanConwayDev
2026-05-15 18:06:11 +00:00
parent efdb7abf96
commit edf52eea00
+80
View File
@@ -542,6 +542,13 @@ async fn find_orphan_repos(
if !npub_path.is_dir() { if !npub_path.is_dir() {
continue; continue;
} }
// Skip the GRASP-06 `/prs/` subtree: it does not follow the
// `<npub>/<identifier>.git` layout and is not described by 30617
// announcements, so every repo under it would otherwise look like
// an orphan to this scan.
if crate::grasp06::paths::is_prs_repo_path(&npub_path, git_data_path) {
continue;
}
let npub = npub_entry.file_name().to_string_lossy().into_owned(); let npub = npub_entry.file_name().to_string_lossy().into_owned();
// Iterate repo-level directories inside this npub dir // Iterate repo-level directories inside this npub dir
@@ -637,3 +644,76 @@ fn check_repo_empty(repo_path: &Path) -> (bool, bool) {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[tokio::test]
async fn find_orphan_repos_skips_prs_subtree() {
// Layout under <git_data_path>:
// prs/<hex>/<id>.git <-- must be ignored (GRASP-06 endpoint)
// <npub-shaped>/<id>.git <-- a real orphan to prove the scan still works
let tmp = tempdir().expect("tempdir");
let git_data_path = tmp.path();
// /prs/<hex>/<id>.git — bare repo so check_repo_empty doesn't complain
let prs_repo = crate::grasp06::paths::prs_repo_path(
git_data_path,
"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
"my-pr-repo",
);
std::fs::create_dir_all(&prs_repo).expect("mkdir prs repo");
let init = Command::new("git")
.args(["init", "--bare", "--quiet"])
.arg(&prs_repo)
.status()
.expect("git init prs repo");
assert!(init.success(), "git init must succeed for the test fixture");
// A standard-shaped orphan repo so we can assert the scan still finds
// real orphans after the /prs/ skip.
let orphan_owner = "npub1zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz0000000";
let orphan_repo = git_data_path.join(orphan_owner).join("standard-orphan.git");
std::fs::create_dir_all(&orphan_repo).expect("mkdir orphan repo");
let init = Command::new("git")
.args(["init", "--bare", "--quiet"])
.arg(&orphan_repo)
.status()
.expect("git init orphan");
assert!(init.success(), "git init must succeed for the test fixture");
// Empty LMDB DB so every disk repo is an "orphan" by definition.
let db_dir = tempdir().expect("tempdir db");
let database: Arc<dyn NostrDatabase> = Arc::new(
NostrLmdb::open(db_dir.path())
.await
.expect("open empty NostrLmdb"),
);
let orphans = find_orphan_repos(git_data_path, &database)
.await
.expect("scan succeeds");
assert!(
orphans.iter().all(|o| !o
.repo_path
.starts_with(prs_repo.parent().unwrap().parent().unwrap())
|| !crate::grasp06::paths::is_prs_repo_path(&o.repo_path, git_data_path)),
"/prs/ repo must not be reported as orphan: got {:?}",
orphans
.iter()
.map(|o| o.repo_path.clone())
.collect::<Vec<_>>()
);
assert!(
orphans.iter().any(|o| o.repo_path == orphan_repo),
"standard orphan must still be detected: got {:?}",
orphans
.iter()
.map(|o| o.repo_path.clone())
.collect::<Vec<_>>()
);
}
}