Files
sausage/userland/git/src/repo.rs
T
surma 4b27e9afd5 userland/git: git-compatible CLI on v2's gix code (T10)
Local workflow, smart-HTTP clone/fetch/push and public HTTPS clone/fetch match
native git in stdout and exit codes. Only WASI imports (wasi:http, exit-with-code).
2026-11-06 16:07:57 +00:00

113 lines
4.4 KiB
Rust

//! 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<PathBuf> {
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<PathBuf> {
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: <path>"))?;
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<PathBuf> {
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<gix::Repository> {
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(())
}