scan local repo
This commit is contained in:
@@ -63,6 +63,65 @@ impl GitCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// Maximum directory nesting depth when scanning for local repositories,
|
||||
/// so pathological trees can't stall the scan.
|
||||
const SCAN_MAX_DEPTH: usize = 12;
|
||||
|
||||
/// Directories never descended into during a scan: dependency caches that
|
||||
/// can be enormous without ever containing user repositories.
|
||||
const SCAN_SKIPPED_DIRS: [&str; 1] = ["node_modules"];
|
||||
|
||||
/// Walk `root` recursively and collect the paths of git repositories
|
||||
/// (directories containing a `.git` entry) below it.
|
||||
///
|
||||
/// Hidden entries and symlinks are skipped, and directories that are
|
||||
/// themselves repositories are not descended into (so nested repositories,
|
||||
/// like submodule worktrees, are not reported). Results are canonicalized,
|
||||
/// deduplicated and sorted by path.
|
||||
pub fn find_git_repos(root: &Path) -> Vec<PathBuf> {
|
||||
let mut repos = Vec::new();
|
||||
if !root.is_dir() {
|
||||
return repos;
|
||||
}
|
||||
|
||||
let mut stack = vec![(root.to_path_buf(), 0usize)];
|
||||
while let Some((dir, depth)) = stack.pop() {
|
||||
if depth > SCAN_MAX_DEPTH {
|
||||
continue;
|
||||
}
|
||||
// A directory containing a `.git` entry is a repository (a linked
|
||||
// worktree has a `.git` file instead of a directory); don't descend.
|
||||
if dir.join(".git").exists() {
|
||||
if let Ok(path) = dir.canonicalize() {
|
||||
repos.push(path);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
let Ok(entries) = std::fs::read_dir(&dir) else {
|
||||
continue;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let Ok(file_type) = entry.file_type() else {
|
||||
continue;
|
||||
};
|
||||
if !file_type.is_dir() || file_type.is_symlink() {
|
||||
continue;
|
||||
}
|
||||
let file_name = entry.file_name();
|
||||
let name = file_name.to_string_lossy();
|
||||
if name.starts_with('.') || SCAN_SKIPPED_DIRS.contains(&name.as_ref()) {
|
||||
continue;
|
||||
}
|
||||
stack.push((entry.path(), depth + 1));
|
||||
}
|
||||
}
|
||||
|
||||
repos.sort();
|
||||
repos.dedup();
|
||||
repos
|
||||
}
|
||||
|
||||
/// Clone a repository into `path` from the first working URL in
|
||||
/// `clone_urls` (the announcement's `clone` tag), then fetch the
|
||||
/// `refs/nostr/*` PR refs like the cache clone does. The destination must
|
||||
@@ -1552,6 +1611,48 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn find_git_repos_discovers_repositories_recursively() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let root = temp.path();
|
||||
|
||||
// Repositories are found at any depth; a linked worktree (a `.git`
|
||||
// file instead of a directory) counts too.
|
||||
let nested = root.join("a/b/project");
|
||||
std::fs::create_dir_all(nested.join(".git")).unwrap();
|
||||
let worktree = root.join("wt");
|
||||
std::fs::create_dir_all(&worktree).unwrap();
|
||||
std::fs::write(
|
||||
worktree.join(".git"),
|
||||
"gitdir: ../a/b/project/.git/worktrees/wt",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Plain directories are not repositories.
|
||||
std::fs::create_dir_all(root.join("plain")).unwrap();
|
||||
|
||||
// Hidden entries and dependency caches are skipped.
|
||||
std::fs::create_dir_all(root.join(".hidden/repo/.git")).unwrap();
|
||||
std::fs::create_dir_all(root.join("node_modules/pkg/.git")).unwrap();
|
||||
|
||||
// A repository is not descended into, so repositories inside it
|
||||
// (submodule worktrees) are not reported.
|
||||
let outer = root.join("outer");
|
||||
std::fs::create_dir_all(outer.join(".git")).unwrap();
|
||||
std::fs::create_dir_all(outer.join("sub/other/.git")).unwrap();
|
||||
|
||||
let mut found = find_git_repos(root);
|
||||
found.sort();
|
||||
|
||||
let mut expected = vec![
|
||||
nested.canonicalize().unwrap(),
|
||||
worktree.canonicalize().unwrap(),
|
||||
outer.canonicalize().unwrap(),
|
||||
];
|
||||
expected.sort();
|
||||
assert_eq!(found, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn repo_ref_state_lists_branches_tags_and_head() {
|
||||
let (_dir, repo) = fixture(&[("a.txt", b"hello")]);
|
||||
|
||||
Reference in New Issue
Block a user