use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use crate::error::{Error, Result};
use crate::odb::Odb;
#[derive(Debug)]
pub struct Repository {
pub git_dir: PathBuf,
pub work_tree: Option<PathBuf>,
pub odb: Odb,
}
impl Repository {
pub fn open(git_dir: &Path, work_tree: Option<&Path>) -> Result<Self> {
let git_dir = git_dir
.canonicalize()
.map_err(|_| Error::NotARepository(git_dir.display().to_string()))?;
if !git_dir.join("HEAD").exists() {
return Err(Error::NotARepository(git_dir.display().to_string()));
}
let objects_dir = if git_dir.join("objects").exists() {
git_dir.join("objects")
} else if let Ok(common_raw) = fs::read_to_string(git_dir.join("commondir")) {
let common_rel = common_raw.trim();
let common_dir = if Path::new(common_rel).is_absolute() {
PathBuf::from(common_rel)
} else {
git_dir.join(common_rel)
};
let common_dir = common_dir
.canonicalize()
.map_err(|_| Error::NotARepository(git_dir.display().to_string()))?;
common_dir.join("objects")
} else {
return Err(Error::NotARepository(git_dir.display().to_string()));
};
if !objects_dir.exists() {
return Err(Error::NotARepository(git_dir.display().to_string()));
}
let work_tree = match work_tree {
Some(p) => Some(
p.canonicalize()
.map_err(|_| Error::PathError(p.display().to_string()))?,
),
None => None,
};
let odb = Odb::new(&objects_dir);
Ok(Self {
git_dir,
work_tree,
odb,
})
}
pub fn discover(start: Option<&Path>) -> Result<Self> {
if let Ok(dir) = env::var("GIT_DIR") {
let git_dir = PathBuf::from(dir);
let work_tree = env::var("GIT_WORK_TREE").ok().map(PathBuf::from);
return Self::open(&git_dir, work_tree.as_deref());
}
let cwd = env::current_dir()?;
let start = start.unwrap_or(&cwd);
let start = if start.is_absolute() {
start.to_path_buf()
} else {
cwd.join(start)
};
let ceiling_dirs = parse_ceiling_directories();
let mut current = start.as_path();
let mut first = true;
loop {
if !first && is_ceiling_blocked(current, &ceiling_dirs) {
break;
}
first = false;
if let Some(repo) = try_open_at(current)? {
return Ok(repo);
}
match current.parent() {
Some(p) => current = p,
None => break,
}
}
Err(Error::NotARepository(start.display().to_string()))
}
#[must_use]
pub fn index_path(&self) -> PathBuf {
self.git_dir.join("index")
}
#[must_use]
pub fn refs_dir(&self) -> PathBuf {
self.git_dir.join("refs")
}
#[must_use]
pub fn head_path(&self) -> PathBuf {
self.git_dir.join("HEAD")
}
#[must_use]
pub fn is_bare(&self) -> bool {
self.work_tree.is_none()
}
}
fn try_open_at(dir: &Path) -> Result<Option<Repository>> {
let dot_git = dir.join(".git");
if dot_git.is_file() {
let content =
fs::read_to_string(&dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
let git_dir = parse_gitfile(&content, dir)?;
let repo = Repository::open(&git_dir, Some(dir))?;
return Ok(Some(repo));
}
if dot_git.is_dir() {
let repo = Repository::open(&dot_git, Some(dir))?;
return Ok(Some(repo));
}
if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
let repo = Repository::open(dir, None)?;
return Ok(Some(repo));
}
Ok(None)
}
fn parse_gitfile(content: &str, base: &Path) -> Result<PathBuf> {
for line in content.lines() {
if let Some(rest) = line.strip_prefix("gitdir:") {
let rel = rest.trim();
let path = if Path::new(rel).is_absolute() {
PathBuf::from(rel)
} else {
base.join(rel)
};
return Ok(path);
}
}
Err(Error::NotARepository(
"gitfile does not contain 'gitdir:' line".to_owned(),
))
}
pub fn init_repository(
path: &Path,
bare: bool,
initial_branch: &str,
template_dir: Option<&Path>,
) -> Result<Repository> {
let git_dir = if bare {
path.to_path_buf()
} else {
path.join(".git")
};
for sub in &[
"objects",
"objects/info",
"objects/pack",
"refs",
"refs/heads",
"refs/tags",
"info",
"hooks",
] {
fs::create_dir_all(git_dir.join(sub))?;
}
if let Some(tmpl) = template_dir {
if tmpl.is_dir() {
copy_template(tmpl, &git_dir)?;
}
}
let head_content = format!("ref: refs/heads/{initial_branch}\n");
fs::write(git_dir.join("HEAD"), head_content)?;
let config_content = if bare {
"[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = true\n"
} else {
"[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallrefupdates = true\n"
};
fs::write(git_dir.join("config"), config_content)?;
fs::write(
git_dir.join("description"),
"Unnamed repository; edit this file 'description' to name the repository.\n",
)?;
let work_tree = if bare { None } else { Some(path) };
Repository::open(&git_dir, work_tree)
}
fn copy_template(src: &Path, dst: &Path) -> Result<()> {
for entry in fs::read_dir(src)? {
let entry = entry?;
let src_path = entry.path();
let dst_path = dst.join(entry.file_name());
if src_path.is_dir() {
fs::create_dir_all(&dst_path)?;
copy_template(&src_path, &dst_path)?;
} else {
fs::copy(&src_path, &dst_path)?;
}
}
Ok(())
}
fn parse_ceiling_directories() -> Vec<PathBuf> {
let raw = match env::var("GIT_CEILING_DIRECTORIES") {
Ok(val) => val,
Err(_) => return Vec::new(),
};
if raw.is_empty() {
return Vec::new();
}
raw.split(':')
.filter(|s| !s.is_empty())
.filter_map(|s| {
let p = PathBuf::from(s);
if !p.is_absolute() {
return None;
}
Some(p.canonicalize().unwrap_or_else(|_| {
let s = s.trim_end_matches('/');
PathBuf::from(s)
}))
})
.collect()
}
fn is_ceiling_blocked(dir: &Path, ceilings: &[PathBuf]) -> bool {
if ceilings.is_empty() {
return false;
}
let canon = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
for ceil in ceilings {
if canon == *ceil {
return true;
}
}
false
}