mirror of
https://github.com/herdrdev/herdr.git
synced 2026-09-22 00:01:06 +00:00
279 lines
8.0 KiB
Rust
279 lines
8.0 KiB
Rust
use std::path::{Path, PathBuf};
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct GitSpaceMetadata {
|
|
pub key: String,
|
|
pub checkout_key: String,
|
|
pub label: String,
|
|
pub repo_root: PathBuf,
|
|
pub is_linked_worktree: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct GitWorktreeInfo {
|
|
pub repo_root: PathBuf,
|
|
pub git_dir: PathBuf,
|
|
pub git_common_dir: PathBuf,
|
|
pub is_bare: bool,
|
|
pub is_linked_worktree: bool,
|
|
}
|
|
|
|
pub fn derive_label_from_cwd(cwd: &Path) -> String {
|
|
if let Some(repo_root) = git_repo_root(cwd) {
|
|
if let Some(name) = repo_root.file_name().and_then(|n| n.to_str()) {
|
|
return name.to_string();
|
|
}
|
|
}
|
|
|
|
if let Ok(home) = std::env::var("HOME") {
|
|
let home = Path::new(&home);
|
|
if cwd == home {
|
|
return "~".to_string();
|
|
}
|
|
}
|
|
|
|
cwd.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.filter(|s| !s.is_empty())
|
|
.map(|s| s.to_string())
|
|
.unwrap_or_else(|| cwd.display().to_string())
|
|
}
|
|
|
|
pub fn git_worktree_info(cwd: &Path) -> Option<GitWorktreeInfo> {
|
|
let repo_root = git_repo_root(cwd)?;
|
|
let git_dir = canonicalize_best_effort_path(&git_dir_for_repo_root(&repo_root)?);
|
|
let git_common_dir = canonicalize_best_effort_path(&git_common_dir_for_git_dir(&git_dir));
|
|
let is_linked_worktree = git_dir != git_common_dir;
|
|
|
|
Some(GitWorktreeInfo {
|
|
repo_root,
|
|
git_dir,
|
|
git_common_dir,
|
|
is_bare: false,
|
|
is_linked_worktree,
|
|
})
|
|
}
|
|
|
|
pub fn git_space_metadata(cwd: &Path) -> Option<GitSpaceMetadata> {
|
|
git_repo_root(cwd)?;
|
|
|
|
let info = git_worktree_info(cwd)?;
|
|
if info.is_bare {
|
|
return None;
|
|
}
|
|
let key = canonicalize_best_effort_path(&info.git_common_dir)
|
|
.display()
|
|
.to_string();
|
|
let checkout_key = canonicalize_best_effort_path(&info.repo_root)
|
|
.display()
|
|
.to_string();
|
|
let label_path = if info
|
|
.git_common_dir
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
== Some(".git")
|
|
{
|
|
info.git_common_dir.parent().unwrap_or(&info.repo_root)
|
|
} else {
|
|
&info.repo_root
|
|
};
|
|
let label = label_path
|
|
.file_name()
|
|
.and_then(|name| name.to_str())
|
|
.unwrap_or("repo")
|
|
.to_string();
|
|
Some(GitSpaceMetadata {
|
|
key,
|
|
checkout_key,
|
|
label,
|
|
repo_root: info.repo_root,
|
|
is_linked_worktree: info.is_linked_worktree,
|
|
})
|
|
}
|
|
|
|
pub(super) fn canonicalize_best_effort_path(path: &Path) -> PathBuf {
|
|
std::fs::canonicalize(path).unwrap_or_else(|_| path.to_path_buf())
|
|
}
|
|
|
|
fn git_common_dir_for_git_dir(git_dir: &Path) -> PathBuf {
|
|
let commondir = git_dir.join("commondir");
|
|
let Ok(contents) = std::fs::read_to_string(commondir) else {
|
|
return git_dir.to_path_buf();
|
|
};
|
|
let path = Path::new(contents.trim());
|
|
if path.is_absolute() {
|
|
path.to_path_buf()
|
|
} else {
|
|
git_dir.join(path)
|
|
}
|
|
}
|
|
|
|
pub fn git_branch(cwd: &Path) -> Option<String> {
|
|
let repo_root = git_repo_root(cwd)?;
|
|
let git_dir = git_dir_for_repo_root(&repo_root)?;
|
|
let head = std::fs::read_to_string(git_dir.join("HEAD")).ok()?;
|
|
parse_git_head_branch(&head)
|
|
}
|
|
|
|
pub(super) fn git_dir_for_repo_root(repo_root: &Path) -> Option<PathBuf> {
|
|
let git_path = repo_root.join(".git");
|
|
if git_path.is_dir() {
|
|
return Some(git_path);
|
|
}
|
|
|
|
let gitdir = std::fs::read_to_string(&git_path).ok()?;
|
|
let relative = gitdir.trim().strip_prefix("gitdir:")?.trim();
|
|
let resolved = Path::new(relative);
|
|
Some(if resolved.is_absolute() {
|
|
resolved.to_path_buf()
|
|
} else {
|
|
repo_root.join(resolved)
|
|
})
|
|
}
|
|
|
|
fn parse_git_head_branch(head: &str) -> Option<String> {
|
|
let branch = head.trim().strip_prefix("ref: refs/heads/")?;
|
|
(!branch.is_empty()).then(|| branch.to_string())
|
|
}
|
|
|
|
pub(super) fn git_repo_root(start: &Path) -> Option<PathBuf> {
|
|
let mut current = if start.is_dir() {
|
|
start.to_path_buf()
|
|
} else {
|
|
start.parent()?.to_path_buf()
|
|
};
|
|
|
|
loop {
|
|
if git_dir_for_repo_root(¤t)
|
|
.map(|git_dir| git_dir.join("HEAD").is_file())
|
|
.unwrap_or(false)
|
|
{
|
|
return Some(current);
|
|
}
|
|
if !current.pop() {
|
|
return None;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn read_ref_oid(common_dir: &Path, full_ref: &str) -> Option<String> {
|
|
let loose_ref = common_dir.join(full_ref);
|
|
if let Ok(contents) = std::fs::read_to_string(loose_ref) {
|
|
let oid = contents.trim();
|
|
if !oid.is_empty() {
|
|
return Some(oid.to_string());
|
|
}
|
|
}
|
|
|
|
let packed_refs = std::fs::read_to_string(common_dir.join("packed-refs")).ok()?;
|
|
for line in packed_refs.lines() {
|
|
let line = line.trim();
|
|
if line.is_empty() || line.starts_with('#') || line.starts_with('^') {
|
|
continue;
|
|
}
|
|
let mut parts = line.split_whitespace();
|
|
let oid = parts.next()?;
|
|
let name = parts.next()?;
|
|
if name == full_ref {
|
|
return Some(oid.to_string());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use std::path::{Path, PathBuf};
|
|
use std::time::{SystemTime, UNIX_EPOCH};
|
|
|
|
use super::*;
|
|
|
|
fn temp_test_dir(name: &str) -> PathBuf {
|
|
let unique = format!(
|
|
"herdr-workspace-tests-{}-{}-{}",
|
|
name,
|
|
std::process::id(),
|
|
SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap()
|
|
.as_nanos()
|
|
);
|
|
let path = std::env::temp_dir().join(unique);
|
|
std::fs::create_dir_all(&path).unwrap();
|
|
path
|
|
}
|
|
|
|
#[test]
|
|
fn git_branch_reads_head_from_standard_repo() {
|
|
let root = temp_test_dir("standard-repo");
|
|
std::fs::create_dir_all(root.join(".git")).unwrap();
|
|
std::fs::write(root.join(".git/HEAD"), "ref: refs/heads/main\n").unwrap();
|
|
|
|
assert_eq!(git_branch(&root).as_deref(), Some("main"));
|
|
|
|
std::fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn git_branch_reads_head_from_worktree_gitdir_file() {
|
|
let root = temp_test_dir("worktree");
|
|
let worktree_git_dir = root.join(".bare/worktrees/feature");
|
|
std::fs::create_dir_all(&worktree_git_dir).unwrap();
|
|
std::fs::write(root.join(".git"), "gitdir: .bare/worktrees/feature\n").unwrap();
|
|
std::fs::write(worktree_git_dir.join("HEAD"), "ref: refs/heads/feature\n").unwrap();
|
|
|
|
assert_eq!(git_branch(&root).as_deref(), Some("feature"));
|
|
|
|
std::fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn git_branch_returns_none_for_detached_head() {
|
|
let root = temp_test_dir("detached-head");
|
|
std::fs::create_dir_all(root.join(".git")).unwrap();
|
|
std::fs::write(root.join(".git/HEAD"), "3e1b9a8d\n").unwrap();
|
|
|
|
assert_eq!(git_branch(&root), None);
|
|
|
|
std::fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn git_repo_root_ignores_invalid_git_marker() {
|
|
let base = temp_test_dir("invalid-git-root");
|
|
let cwd = base.join("workspace");
|
|
std::fs::create_dir_all(base.join(".git")).unwrap();
|
|
std::fs::create_dir_all(&cwd).unwrap();
|
|
|
|
assert_eq!(git_repo_root(&cwd), None);
|
|
|
|
std::fs::remove_dir_all(base).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn derive_label_prefers_repo_root_name() {
|
|
let root = temp_test_dir("label-repo");
|
|
let nested = root.join("nested");
|
|
std::fs::create_dir_all(root.join(".git")).unwrap();
|
|
std::fs::write(root.join(".git/HEAD"), "ref: refs/heads/main\n").unwrap();
|
|
std::fs::create_dir_all(&nested).unwrap();
|
|
|
|
assert_eq!(
|
|
derive_label_from_cwd(&nested),
|
|
root.file_name().and_then(|name| name.to_str()).unwrap()
|
|
);
|
|
|
|
std::fs::remove_dir_all(root).unwrap();
|
|
}
|
|
|
|
#[test]
|
|
fn derive_label_uses_path_name_outside_git() {
|
|
let root = temp_test_dir("label-plain");
|
|
let label = root.file_name().and_then(|name| name.to_str()).unwrap();
|
|
|
|
assert_eq!(derive_label_from_cwd(Path::new(&root)), label);
|
|
|
|
std::fs::remove_dir_all(root).unwrap();
|
|
}
|
|
}
|