specfold_core/workspace/vcs/
repo.rs1use std::path::{Path, PathBuf};
4use std::process::Command;
5
6pub(super) fn git(root: &Path, args: &[&str]) -> Option<Vec<u8>> {
7 let out = Command::new("git").arg("-C").arg(root).args(args).output().ok()?;
8 if !out.status.success() {
9 return None;
10 }
11 Some(out.stdout)
12}
13
14pub(super) fn split_nul(bytes: Vec<u8>) -> Vec<String> {
15 bytes.split(|b| *b == 0).filter(|s| !s.is_empty()).map(|s| String::from_utf8_lossy(s).into_owned()).collect()
16}
17
18pub fn git_root(dir: &Path) -> Option<PathBuf> {
19 let out = git(dir, &["rev-parse", "--show-toplevel"])?;
20 let s = String::from_utf8(out).ok()?;
21 Some(PathBuf::from(s.trim()))
22}
23
24pub fn is_repo(dir: &Path) -> bool {
25 git(dir, &["rev-parse", "--git-dir"]).is_some()
26}
27
28pub(super) fn nearest_repo(root: &Path, path: &str) -> (PathBuf, String, String) {
32 let mut prefix = Path::new(path).parent();
33 while let Some(dir) = prefix.filter(|d| !d.as_os_str().is_empty()) {
34 if root.join(dir).join(".git").exists() {
35 let p = dir.to_string_lossy().replace('\\', "/");
36 let rel = path.strip_prefix(&format!("{p}/")).unwrap_or(path).to_string();
37 return (root.join(dir), rel, format!("{p}/"));
38 }
39 prefix = dir.parent();
40 }
41 (root.to_path_buf(), path.to_string(), String::new())
42}
43
44#[cfg(test)]
45mod tests {
46 use super::*;
47
48 #[test]
49 fn nested_checkout_owns_its_paths() {
50 let tmp = tempfile::tempdir().unwrap();
51 let r = tmp.path();
52 std::fs::create_dir_all(r.join("libs/sub/.git")).unwrap();
53 std::fs::create_dir_all(r.join("libs/sub/docs")).unwrap();
54 let (repo, rel, prefix) = nearest_repo(r, "libs/sub/docs/a.md");
55 assert_eq!((repo, rel.as_str(), prefix.as_str()), (r.join("libs/sub"), "docs/a.md", "libs/sub/"));
56 let (repo, rel, prefix) = nearest_repo(r, "docs/a.md");
57 assert_eq!((repo, rel.as_str(), prefix.as_str()), (r.to_path_buf(), "docs/a.md", ""));
58 }
59}