//! Repository/worktree discovery helpers. use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; /// Find the git directory for a path using gix (handles both repos and worktrees). /// /// For a regular repo, returns the `.git` directory. For a linked worktree, /// returns the worktree's git dir under `.git/worktrees/`. /// /// Note: currently unused in production code — retained for future use and /// tested below. See `find_worktree_git_dir` for the version used by /// `copy_git_index`. #[allow(dead_code)] pub(crate) fn find_git_dir(path: &Path) -> Result { let repo = gix::discover(path) .with_context(|| format!("failed to discover git repo at {}", path.display()))?; Ok(repo.git_dir().to_path_buf()) } /// Find the worktree's git directory from its `.git` file. /// /// Worktrees have a `.git` file (not directory) that points to the actual git dir. /// For regular repos, returns the `.git` directory. pub(crate) fn find_worktree_git_dir(worktree_path: &Path) -> Result { let git_path = worktree_path.join(".git"); if git_path.is_file() { // Worktree: .git is a file containing "gitdir: " let content = std::fs::read_to_string(&git_path) .with_context(|| format!("failed to read .git file at {}", git_path.display()))?; let raw = content .strip_prefix("gitdir: ") .ok_or_else(|| anyhow::anyhow!("invalid .git file format: {}", content.trim()))? .trim(); // git may write a RELATIVE pointer (worktrees added with a relative // path). Resolve it against the worktree dir — otherwise downstream // index lookups join it against the CWD and break (mirrors // `read_worktree_gitdir` in api.rs). let raw_path = Path::new(raw); let resolved = if raw_path.is_relative() { worktree_path.join(raw_path) } else { raw_path.to_path_buf() }; Ok(dunce::canonicalize(&resolved).unwrap_or(resolved)) } else if git_path.is_dir() { // Regular repository Ok(git_path) } else { anyhow::bail!( "no .git file or directory found at {}", worktree_path.display() ) } } /// Find the worktree root (working directory root) for a path. /// /// This handles both regular repositories and worktrees correctly. /// For a regular repo at `/repo`, returns `/repo`. /// For a worktree at `/worktrees/wt1`, returns `/worktrees/wt1`. /// For a subdirectory `/repo/subdir`, returns `/repo`. pub(crate) fn find_worktree_root(path: &Path) -> Result { let repo = gix::discover(path) .with_context(|| format!("failed to discover git repo at {}", path.display()))?; // workdir() returns the working directory root for both repos and worktrees let work_dir = repo .workdir() .ok_or_else(|| anyhow::anyhow!("bare repository has no working directory"))?; Ok(work_dir.to_path_buf()) } /// Get the HEAD commit hash using gix. pub(crate) fn get_head_commit(path: &Path) -> Result { let repo = gix::discover(path) .with_context(|| format!("failed to discover git repo at {}", path.display()))?; let head = repo .head() .context("failed to get HEAD")? .peel_to_commit() .context("failed to peel HEAD to commit")?; Ok(head.id().to_string()) } #[cfg(test)] mod tests { use super::*; use tempfile::TempDir; use xai_test_utils::git::{git_commit_all, init_git_repo}; #[test] fn test_find_git_dir() { xai_test_utils::require_git!(); let temp = TempDir::new().unwrap(); init_git_repo(temp.path()); let git_dir = find_git_dir(temp.path()).unwrap(); assert!(git_dir.ends_with(".git")); assert!(git_dir.is_dir()); } #[test] fn test_find_worktree_git_dir_resolves_relative_gitdir() { // A worktree `.git` file can hold a RELATIVE `gitdir:` pointer; it must // resolve against the worktree dir, not be returned as-is (which would // break index copy for relative-gitdir worktrees). let temp = TempDir::new().unwrap(); let worktree = temp.path().join("wt"); std::fs::create_dir_all(&worktree).unwrap(); let real_git = temp.path().join("repo/.git/worktrees/wt"); std::fs::create_dir_all(&real_git).unwrap(); std::fs::write(worktree.join(".git"), "gitdir: ../repo/.git/worktrees/wt\n").unwrap(); let resolved = find_worktree_git_dir(&worktree).unwrap(); assert_eq!(resolved, dunce::canonicalize(&real_git).unwrap()); } #[test] fn test_find_worktree_git_dir_regular_repo() { xai_test_utils::require_git!(); let temp = TempDir::new().unwrap(); init_git_repo(temp.path()); let git_dir = find_worktree_git_dir(temp.path()).unwrap(); assert!(git_dir.ends_with(".git")); } #[test] fn test_get_head_commit() { xai_test_utils::require_git!(); let temp = TempDir::new().unwrap(); init_git_repo(temp.path()); // Create a commit std::fs::write(temp.path().join("file.txt"), "content").unwrap(); git_commit_all(temp.path(), "initial"); let commit = get_head_commit(temp.path()).unwrap(); assert_eq!(commit.len(), 40); // SHA-1 hex string assert!(commit.chars().all(|c| c.is_ascii_hexdigit())); } }