//! Repository layout and shared operation helpers. Persistent state is ordinary Git files only. use crate::{Error, Result}; use std::path::PathBuf; /// Add operation context to Git and filesystem errors. pub fn error(operation: &str, error: impl std::fmt::Display) -> Error { Error::new(-32000, format!("{operation}: {error}")) } /// Resolve a legacy name or a literal absolute path under /plugins/ or /repos/. pub fn repo_path(name: &str) -> Result { Ok(PathBuf::from(name)) } /// Resolve metadata without opening another repository or acquiring another lock. /// Missing .git is retained for the existing non-bare init/clone lock key. fn git_dir(path: &std::path::Path) -> Result { let dot_git = path.join(".git"); if dot_git.is_file() { let text = std::fs::read_to_string(&dot_git).map_err(|e| error("gitfile", e))?; let target = text .trim_end_matches(['\r', '\n']) .strip_prefix("gitdir: ") .filter(|p| !p.is_empty() && !p.contains(['\r', '\n', '\0'])) .ok_or_else(|| error("gitfile", "expected gitdir: "))?; return std::fs::canonicalize(path.join(target)).map_err(|e| error("gitfile target", e)); } if !dot_git.exists() && path.join("HEAD").is_file() && path.join("objects").is_dir() { return Ok(path.to_owned()); } Ok(dot_git) } /// Common-dir advisory lock key, shared by the main and all linked worktrees. /// A plain .git directory retains exactly the legacy lock path. pub fn lock_path(name: &str) -> Result { let git_dir = git_dir(&repo_path(name)?)?; let common = match std::fs::read_to_string(git_dir.join("commondir")) { Ok(text) => text, Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(git_dir), Err(e) => return Err(error("commondir", e)), }; let common = common.trim_end_matches(['\r', '\n']); if common.is_empty() || common.contains(['\r', '\n', '\0']) { return Err(error("commondir", "expected one nonempty path")); } std::fs::canonicalize(git_dir.join(common)).map_err(|e| error("commondir target", e)) } /// Open only the addressed worktree or bare directory, never discover an ancestor. pub fn open(name: &str) -> Result { let path = repo_path(name)?; let dot_git = path.join(".git"); let mut repo = if dot_git.is_file() { // gix follows gitfiles and commondir, including the private HEAD/index. git_dir(&path)?; // Give malformed gitfiles a useful error before opening. let repo = gix::open(&path).map_err(|e| error("open", e))?; // gix 0.90 reports is_bare()==true from the common config even though // it resolves the workdir and private index correctly. Check workdir, // not that cached flag; status and our tree writer work without config edits. worktree(&repo)?; repo } else if dot_git.is_dir() { let repo = gix::open(&dot_git).map_err(|e| error("open", e))?; if repo.workdir().is_none() || repo.is_bare() { return Err(error( "open", "expected worktree metadata in .git; address bare repositories by their bare directory", )); } repo } else { let repo = gix::open_opts(&path, gix::open::Options::default().open_path_as_is(true)) .map_err(|e| error("open bare", e))?; if !repo.is_bare() { return Err(error( "open", "expected a worktree root or bare repository directory", )); } repo }; // gix-ref otherwise uses unsupported mmap for packed-refs above 32 KiB. #[cfg(target_os = "wasi")] repo.refs.set_packed_buffer_mmap_threshold(u64::MAX); Ok(repo) } /// Require a worktree before any status/index/worktree-style operation. pub fn worktree(repo: &gix::Repository) -> Result<&std::path::Path> { repo.workdir().ok_or_else(|| { error( "worktree", "no worktree: operation is not available on a bare repository", ) }) } /// Validate a literal repository-relative path (not a pathspec). pub fn validate_path(path: &str) -> Result<()> { if path.is_empty() || path .split('/') .any(|c| c.is_empty() || c == "." || c == "..") { return Err(error( "path", "expected a nonempty repository-relative literal path", )); } Ok(()) }