use std::collections::{BTreeSet, HashSet};
use std::env;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Component, Path, PathBuf};
use std::sync::Mutex;
use crate::config::{ConfigFile, ConfigScope, ConfigSet};
use crate::error::{Error, Result};
use crate::hooks::run_hook;
use crate::index::Index;
use crate::objects::parse_commit;
use crate::odb::Odb;
use crate::rev_parse::is_inside_work_tree;
use crate::sparse_checkout::effective_cone_mode_for_sparse_file;
use crate::split_index::{write_index_file_split, WriteSplitIndexRequest};
use crate::state::resolve_head;
use crate::worktree_cwd::cwd_relative_under_work_tree;
const GIT_PREFIX_ENV: &str = "GIT_PREFIX";
fn export_git_prefix_env(repo: &Repository) {
let Some(wt) = repo.work_tree.as_ref() else {
return;
};
let Ok(cwd) = env::current_dir() else {
return;
};
let new_s = cwd_relative_under_work_tree(wt, &cwd).unwrap_or_default();
if new_s.is_empty() {
if let Ok(existing) = env::var(GIT_PREFIX_ENV) {
if !existing.trim().is_empty() {
return;
}
}
}
env::set_var(GIT_PREFIX_ENV, new_s);
}
fn read_sparse_checkout_patterns(git_dir: &Path) -> Vec<String> {
let path = git_dir.join("info").join("sparse-checkout");
let Ok(content) = fs::read_to_string(&path) else {
return Vec::new();
};
content
.lines()
.map(|l| l.trim())
.filter(|l| !l.is_empty() && !l.starts_with('#'))
.map(String::from)
.collect()
}
#[derive(Debug)]
pub struct Repository {
pub git_dir: PathBuf,
pub work_tree: Option<PathBuf>,
pub odb: Odb,
pub explicit_git_dir: bool,
pub discovery_root: Option<PathBuf>,
pub work_tree_from_env: bool,
pub discovery_via_gitfile: bool,
cached_settings: std::sync::Arc<std::sync::OnceLock<RepoCachedSettings>>,
}
#[derive(Debug, Clone)]
struct RepoCachedSettings {
use_replace_refs: bool,
replace_ref_base: String,
}
impl Repository {
fn from_canonical_git_dir(git_dir: PathBuf, work_tree: Option<&Path>) -> Result<Self> {
let head_path = git_dir.join("HEAD");
if !head_path.exists() && !head_path.is_symlink() {
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 Some(common_dir) = resolve_common_dir(&git_dir) {
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) => {
let cwd = env::current_dir().map_err(Error::Io)?;
let mut resolved = if p.is_absolute() {
p.to_path_buf()
} else {
cwd.join(p)
};
if resolved.exists() {
resolved = resolved
.canonicalize()
.map_err(|_| Error::PathError(p.display().to_string()))?;
}
Some(resolved)
}
None => None,
};
let odb = if let Some(ref wt) = work_tree {
Odb::with_work_tree(&objects_dir, wt).with_config_git_dir(git_dir.clone())
} else {
Odb::new(&objects_dir).with_config_git_dir(git_dir.clone())
};
Ok(Self {
git_dir,
work_tree,
odb,
explicit_git_dir: false,
discovery_root: None,
work_tree_from_env: false,
discovery_via_gitfile: false,
cached_settings: std::sync::Arc::new(std::sync::OnceLock::new()),
})
}
fn cached_settings(&self) -> &RepoCachedSettings {
self.cached_settings.get_or_init(|| {
let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
let use_replace_refs = cfg
.get_bool("core.useReplaceRefs")
.and_then(|r| r.ok())
.unwrap_or(true);
let replace_ref_base = std::env::var("GIT_REPLACE_REF_BASE")
.ok()
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "refs/replace/".to_owned());
let replace_ref_base = if replace_ref_base.ends_with('/') {
replace_ref_base
} else {
format!("{replace_ref_base}/")
};
RepoCachedSettings {
use_replace_refs,
replace_ref_base,
}
})
}
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()))?;
validate_repository_format(&git_dir)?;
Self::from_canonical_git_dir(git_dir, work_tree)
}
pub fn open_skipping_format_validation(
git_dir: &Path,
work_tree: Option<&Path>,
) -> Result<Self> {
let git_dir = git_dir
.canonicalize()
.map_err(|_| Error::NotARepository(git_dir.display().to_string()))?;
Self::from_canonical_git_dir(git_dir, work_tree)
}
pub fn discover(start: Option<&Path>) -> Result<Self> {
if let Ok(dir) = env::var("GIT_DIR") {
let cwd = env::current_dir()?;
let mut git_dir = PathBuf::from(&dir);
if git_dir.is_relative() {
git_dir = cwd.join(git_dir);
}
git_dir = resolve_git_dir_env_path(&git_dir)?;
let work_tree = env::var("GIT_WORK_TREE").ok().map(|wt| {
let p = PathBuf::from(wt);
if p.is_absolute() {
p
} else {
cwd.join(p)
}
});
if let Some(ref wt_path) = work_tree {
if env::var("GIT_WORK_TREE")
.ok()
.is_some_and(|raw| Path::new(&raw).is_absolute())
{
validate_git_work_tree_path(wt_path)?;
}
}
if work_tree.is_some() {
let mut repo = Self::open(&git_dir, work_tree.as_deref())?;
repo.explicit_git_dir = true;
repo.discovery_root = None;
repo.work_tree_from_env = false;
repo.discovery_via_gitfile = false;
export_git_prefix_env(&repo);
return Ok(repo);
}
let (is_bare, core_wt) = read_core_bare_and_worktree(&git_dir)?;
if is_bare && core_wt.is_some() {
warn_core_bare_worktree_conflict(&git_dir);
}
let resolved_wt = if is_bare {
None
} else if let Some(raw) = core_wt {
Some(resolve_core_worktree_path(&git_dir, &raw)?)
} else {
Some(cwd.canonicalize().unwrap_or_else(|_| cwd.clone()))
};
let mut repo = Self::open(&git_dir, resolved_wt.as_deref())?;
repo.explicit_git_dir = true;
repo.discovery_root = None;
repo.work_tree_from_env = false;
repo.discovery_via_gitfile = false;
export_git_prefix_env(&repo);
return Ok(repo);
}
let cwd = env::current_dir()?;
let env_work_tree = env::var("GIT_WORK_TREE").ok().map(|wt| {
let p = PathBuf::from(wt);
if p.is_absolute() {
p
} else {
cwd.join(p)
}
});
if let Some(ref p) = env_work_tree {
if env::var("GIT_WORK_TREE")
.ok()
.is_some_and(|raw| Path::new(&raw).is_absolute())
{
validate_git_work_tree_path(p)?;
}
}
let start = start.unwrap_or(&cwd);
let start = if start.is_absolute() {
start.to_path_buf()
} else {
cwd.join(start)
};
let (ceiling_paths, no_resolve_ceilings) = parse_ceiling_directories();
let ceiling_dirs: Vec<String> = ceiling_paths
.into_iter()
.map(|p| path_for_ceiling_compare(&p))
.collect();
let start_canon = start.canonicalize().unwrap_or_else(|_| start.clone());
let ceil_cmp_buf = if no_resolve_ceilings {
path_for_ceiling_compare(&start)
} else {
path_for_ceiling_compare(&start_canon)
};
let mut dir_buf = path_for_ceiling_compare(&start_canon);
let min_offset = offset_1st_component(&dir_buf);
let mut ceil_offset: isize = longest_ancestor_length(&ceil_cmp_buf, &ceiling_dirs)
.map(|n| n as isize)
.unwrap_or(-1);
if ceil_offset < 0 {
ceil_offset = min_offset as isize - 2;
}
loop {
let current = Path::new(&dir_buf);
if let Some(DiscoveredAt { mut repo, gitfile }) = try_open_at(current)? {
validate_repository_format(&repo.git_dir)?;
if let Some(ref wt) = env_work_tree {
repo.work_tree = Some(wt.canonicalize().unwrap_or_else(|_| wt.clone()));
repo.work_tree_from_env = true;
} else {
repo.work_tree_from_env = false;
let linked_gitfile =
repo.discovery_via_gitfile && resolve_common_dir(&repo.git_dir).is_some();
if !linked_gitfile {
let (is_bare, core_wt) = read_core_bare_and_worktree(&repo.git_dir)?;
if is_bare {
repo.work_tree = None;
} else if let Some(raw) = core_wt {
repo.work_tree = Some(resolve_core_worktree_path(&repo.git_dir, &raw)?);
}
}
}
let assume_different = env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
.ok()
.map(|v| {
let lower = v.to_ascii_lowercase();
v == "1" || lower == "true" || lower == "yes" || lower == "on"
})
.unwrap_or(false);
if assume_different {
repo.enforce_safe_directory()?;
} else {
#[cfg(unix)]
ensure_valid_ownership(
gitfile.as_deref(),
repo.work_tree.as_deref(),
&repo.git_dir,
)?;
}
export_git_prefix_env(&repo);
return Ok(repo);
}
let mut offset: isize = dir_buf.len() as isize;
if offset <= min_offset as isize {
break;
}
loop {
offset -= 1;
if offset <= ceil_offset {
break;
}
if dir_buf
.as_bytes()
.get(offset as usize)
.is_some_and(|b| *b == b'/')
{
break;
}
}
if offset <= ceil_offset {
break;
}
let off_u = offset as usize;
let new_len = if off_u > min_offset {
off_u
} else {
min_offset
};
dir_buf.truncate(new_len);
}
Err(Error::NotARepository(start.display().to_string()))
}
#[must_use]
pub fn effective_pathspec_cwd(&self) -> PathBuf {
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let Some(wt) = self.work_tree.as_ref() else {
return cwd;
};
let inside_lexical = cwd.strip_prefix(wt).is_ok();
let inside_canon = cwd
.canonicalize()
.ok()
.zip(wt.canonicalize().ok())
.is_some_and(|(c, w)| c.starts_with(&w));
if inside_lexical || inside_canon {
cwd
} else {
wt.clone()
}
}
#[must_use]
pub fn index_path(&self) -> PathBuf {
self.git_dir.join("index")
}
pub fn index_path_for_env(&self) -> Result<PathBuf> {
if let Ok(raw) = env::var("GIT_INDEX_FILE") {
if !raw.is_empty() {
let p = PathBuf::from(raw);
return Ok(if p.is_absolute() {
p
} else {
env::current_dir().map_err(Error::Io)?.join(p)
});
}
}
Ok(self.index_path())
}
pub fn load_index(&self) -> Result<Index> {
let path = self.index_path_for_env()?;
self.load_index_at(&path)
}
pub fn load_index_at(&self, path: &std::path::Path) -> Result<Index> {
let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
if let Some(res) = cfg.get_bool("index.sparse") {
res.map_err(Error::ConfigError)?;
}
let mut idx = Index::load_expand_sparse_optional(path, &self.odb)?;
crate::split_index::resolve_split_index_if_needed(&mut idx, &self.git_dir, path)?;
if let Some(ref wt) = self.work_tree {
crate::sparse_checkout::clear_skip_worktree_from_present_files(
&self.git_dir,
wt,
&mut idx,
);
}
Ok(idx)
}
pub fn write_index(&self, index: &mut Index) -> Result<()> {
self.write_index_at(&self.index_path(), index)
}
pub fn write_index_at(&self, path: &std::path::Path, index: &mut Index) -> Result<()> {
self.write_index_at_split(path, index, WriteSplitIndexRequest::default())
}
pub fn write_index_at_split(
&self,
path: &std::path::Path,
index: &mut Index,
split: WriteSplitIndexRequest,
) -> Result<()> {
self.finalize_sparse_index_if_needed(index)?;
let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
let skip_hash = crate::index::index_skip_hash_for_write(Some(&cfg));
write_index_file_split(path, &self.git_dir, index, &cfg, split, skip_hash)?;
let _ = run_hook(self, "post-index-change", &["0", "0"], None);
Ok(())
}
fn finalize_sparse_index_if_needed(&self, index: &mut Index) -> Result<()> {
let cfg = ConfigSet::load(Some(&self.git_dir), true).unwrap_or_default();
let sparse_enabled = cfg
.get("core.sparseCheckout")
.map(|v| v == "true")
.unwrap_or(false);
if !sparse_enabled {
index.sparse_directories = false;
return Ok(());
}
let cone_cfg = cfg
.get("core.sparseCheckoutCone")
.and_then(|v| v.parse::<bool>().ok())
.unwrap_or(true);
let sparse_ix = cfg
.get("index.sparse")
.map(|v| v == "true")
.unwrap_or(false);
let patterns = read_sparse_checkout_patterns(&self.git_dir);
let cone = effective_cone_mode_for_sparse_file(cone_cfg, &patterns);
let head = resolve_head(&self.git_dir)?;
let tree_oid = if let Some(oid) = head.oid() {
let obj = self.odb.read(oid)?;
let commit = parse_commit(&obj.data)?;
Some(commit.tree)
} else {
None
};
if let Some(t) = tree_oid {
index.try_collapse_sparse_directories(&self.odb, &t, &patterns, cone, sparse_ix)?;
} else {
index.sparse_directories = false;
}
Ok(())
}
#[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 bloom_pathspec_cwd(&self) -> Option<String> {
let wt = self.work_tree.as_ref()?;
let cwd = env::current_dir().ok()?;
let wt = wt.canonicalize().ok()?;
let cwd = cwd.canonicalize().ok()?;
let rel = cwd.strip_prefix(&wt).ok()?;
let s = rel.to_string_lossy().replace('\\', "/");
let s = s.trim_start_matches('/').to_string();
Some(s)
}
#[must_use]
pub fn is_bare(&self) -> bool {
if let Ok(cfg) = ConfigSet::load(Some(&self.git_dir), true) {
if let Some(Ok(bare)) = cfg.get_bool("core.bare") {
return bare;
}
}
self.work_tree.is_none()
}
pub fn read_replaced(&self, oid: &crate::objects::ObjectId) -> Result<crate::objects::Object> {
if std::env::var_os("GIT_NO_REPLACE_OBJECTS").is_some() {
return self.odb.read(oid);
}
let settings = self.cached_settings();
if !settings.use_replace_refs {
return self.odb.read(oid);
}
let replace_ref =
self.git_dir
.join(format!("{}{}", settings.replace_ref_base, oid.to_hex()));
if replace_ref.is_file() {
if let Ok(content) = std::fs::read_to_string(&replace_ref) {
let hex = content.trim();
if let Ok(replacement_oid) = hex.parse::<crate::objects::ObjectId>() {
if let Ok(obj) = self.odb.read(&replacement_oid) {
return Ok(obj);
}
}
}
}
self.odb.read(oid)
}
}
pub fn trace_repo_setup_if_requested(repo: &Repository) -> std::io::Result<()> {
let Ok(path) = env::var("GIT_TRACE_SETUP") else {
return Ok(());
};
if path.is_empty() || path == "0" {
return Ok(());
}
let trace_path = Path::new(&path);
if !trace_path.is_absolute() {
return Ok(());
}
let actual_cwd = env::current_dir()?;
let actual_cwd = actual_cwd
.canonicalize()
.unwrap_or_else(|_| actual_cwd.clone());
let (trace_cwd, prefix) = if let Some(ref wt) = repo.work_tree {
let wt_canon = wt.canonicalize().unwrap_or_else(|_| wt.clone());
if actual_cwd.starts_with(&wt_canon) {
let rel = actual_cwd
.strip_prefix(&wt_canon)
.map(|p| p.to_path_buf())
.unwrap_or_default();
let prefix = if rel.as_os_str().is_empty() {
"(null)".to_owned()
} else {
let mut s = rel.to_string_lossy().replace('\\', "/");
if !s.ends_with('/') {
s.push('/');
}
s
};
(wt_canon, prefix)
} else {
(actual_cwd.clone(), "(null)".to_owned())
}
} else {
(actual_cwd.clone(), "(null)".to_owned())
};
let git_dir_display =
display_git_dir_for_setup_trace(repo, &trace_cwd, &actual_cwd, prefix.as_str());
let common_display = display_common_dir_for_setup_trace(
repo,
&trace_cwd,
&actual_cwd,
prefix.as_str(),
&git_dir_display,
);
let worktree_display = repo
.work_tree
.as_ref()
.map(|p| {
p.canonicalize()
.unwrap_or_else(|_| lexical_normalize_path(p))
.display()
.to_string()
})
.unwrap_or_else(|| "(null)".to_owned());
let mut f = OpenOptions::new()
.create(true)
.append(true)
.open(trace_path)?;
writeln!(f, "setup: git_dir: {git_dir_display}")?;
writeln!(f, "setup: git_common_dir: {common_display}")?;
writeln!(f, "setup: worktree: {worktree_display}")?;
writeln!(f, "setup: cwd: {}", trace_cwd.display())?;
writeln!(f, "setup: prefix: {prefix}")?;
Ok(())
}
fn lexical_normalize_path(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
let mut absolute = false;
for c in path.components() {
match c {
Component::Prefix(p) => {
out.push(p.as_os_str());
}
Component::RootDir => {
absolute = true;
out.push(c.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
if absolute {
let _ = out.pop();
} else if !out.pop() {
out.push("..");
}
}
Component::Normal(s) => out.push(s),
}
}
if out.as_os_str().is_empty() {
PathBuf::from(".")
} else {
out
}
}
fn path_relative_to(target: &Path, base: &Path) -> Option<PathBuf> {
let t = target.canonicalize().ok()?;
let b = base.canonicalize().ok()?;
let tc: Vec<_> = t.components().collect();
let bc: Vec<_> = b.components().collect();
let mut i = 0usize;
while i < tc.len() && i < bc.len() && tc[i] == bc[i] {
i += 1;
}
let up = bc.len().saturating_sub(i);
let mut out = PathBuf::new();
for _ in 0..up {
out.push("..");
}
for comp in &tc[i..] {
out.push(comp.as_os_str());
}
Some(out)
}
fn rel_path_for_setup_trace(target: &Path, trace_cwd: &Path) -> String {
let t = target
.canonicalize()
.unwrap_or_else(|_| target.to_path_buf());
let tc = trace_cwd
.canonicalize()
.unwrap_or_else(|_| trace_cwd.to_path_buf());
if let Some(rel) = path_relative_to(&t, &tc) {
let s = rel.to_string_lossy().replace('\\', "/");
return if s.is_empty() || s == "." {
".".to_owned()
} else {
s
};
}
t.display().to_string()
}
fn trace_cwd_strictly_inside_git_parent(trace_cwd: &Path, git_dir: &Path) -> bool {
let tc = trace_cwd
.canonicalize()
.unwrap_or_else(|_| trace_cwd.to_path_buf());
let gd = git_dir
.canonicalize()
.unwrap_or_else(|_| git_dir.to_path_buf());
let Some(parent) = gd.parent() else {
return false;
};
let parent = parent.to_path_buf();
if tc == parent {
return false;
}
tc.starts_with(&parent) && tc != parent
}
fn display_git_dir_for_setup_trace(
repo: &Repository,
trace_cwd: &Path,
actual_cwd: &Path,
setup_prefix: &str,
) -> String {
let gd = repo
.git_dir
.canonicalize()
.unwrap_or_else(|_| repo.git_dir.clone());
let tc = trace_cwd
.canonicalize()
.unwrap_or_else(|_| trace_cwd.to_path_buf());
let ac = actual_cwd
.canonicalize()
.unwrap_or_else(|_| actual_cwd.to_path_buf());
if repo.work_tree.is_none() && !repo.explicit_git_dir {
if ac == gd {
return ".".to_owned();
}
if ac.starts_with(&gd) && ac != gd {
return gd.display().to_string();
}
}
if !repo.explicit_git_dir {
if let Some(wt) = &repo.work_tree {
let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
if ac.starts_with(&gd) && ac != wt {
return gd.display().to_string();
}
}
}
if repo.explicit_git_dir {
if repo.work_tree.is_none() {
if let Ok(raw) = env::var("GIT_DIR") {
let p = Path::new(raw.trim());
if p.is_absolute() {
return gd.display().to_string();
}
let joined = ac.join(p);
if joined.is_file() {
return gd.display().to_string();
}
if let Some(rel) = path_relative_to(&gd, &tc) {
let s = rel.to_string_lossy().replace('\\', "/");
return if s.is_empty() || s == "." {
".".to_owned()
} else {
s
};
}
}
return gd.display().to_string();
}
if let Some(wt) = &repo.work_tree {
let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
let strictly_inside_wt = ac.starts_with(&wt) && ac != wt;
if strictly_inside_wt {
return gd.display().to_string();
}
if let Ok(raw) = env::var("GIT_DIR") {
let p = Path::new(raw.trim());
if p.is_relative() {
let joined = ac.join(p);
if joined.is_file() {
return gd.display().to_string();
}
if let Some(rel) = path_relative_to(&gd, &tc) {
let s = rel.to_string_lossy().replace('\\', "/");
return if s.is_empty() || s == "." {
".".to_owned()
} else {
s
};
}
}
return gd.display().to_string();
}
}
if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
return rel_path_for_setup_trace(&gd, trace_cwd);
}
return gd.display().to_string();
}
let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
(Some(root), Some(wt)) if !repo.work_tree_from_env => {
let r = root.canonicalize().unwrap_or_else(|_| root.clone());
let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
r != w
}
_ => false,
};
if repo.work_tree_from_env {
if !repo.discovery_via_gitfile {
if setup_prefix == "(null)" {
if let (Some(root), Some(wt)) = (&repo.discovery_root, &repo.work_tree) {
let r = root.canonicalize().unwrap_or_else(|_| root.clone());
let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
if r == w {
let dot_git = r.join(".git");
let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
if gd == dot_git {
return ".git".to_owned();
}
}
}
}
if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
return rel_path_for_setup_trace(&gd, trace_cwd);
}
}
return gd.display().to_string();
}
if work_relocated {
if let Some(wt) = &repo.work_tree {
let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
if ac == wt {
return gd.display().to_string();
}
let inside_wt = ac.starts_with(&wt) && ac != wt;
if inside_wt {
if let Some(rel) = path_relative_to(&gd, &ac) {
let s = rel.to_string_lossy().replace('\\', "/");
return if s.is_empty() || s == "." {
".".to_owned()
} else {
s
};
}
}
}
}
if repo.work_tree.is_some() {
if let Some(root) = &repo.discovery_root {
let r = root.canonicalize().unwrap_or_else(|_| root.clone());
let dot_git = r.join(".git");
let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
if gd == dot_git {
return ".git".to_owned();
}
} else if let Some(wt) = &repo.work_tree {
let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
let dot_git = wt.join(".git");
let dot_git = dot_git.canonicalize().unwrap_or(dot_git);
if gd == dot_git {
return ".git".to_owned();
}
}
}
if repo.discovery_via_gitfile && !repo.explicit_git_dir {
return gd.display().to_string();
}
if repo.work_tree.is_none() && !repo.explicit_git_dir {
if let Some(gp) = gd.parent() {
let gp = gp.canonicalize().unwrap_or_else(|_| gp.to_path_buf());
let gdc = gd.canonicalize().unwrap_or_else(|_| gd.clone());
if tc.starts_with(&gp) && tc != gp && !tc.starts_with(&gdc) {
return gdc.display().to_string();
}
if tc == gp {
return rel_path_for_setup_trace(&gd, trace_cwd);
}
}
}
if trace_cwd_strictly_inside_git_parent(trace_cwd, &gd) {
rel_path_for_setup_trace(&gd, trace_cwd)
} else {
gd.display().to_string()
}
}
fn display_common_dir_for_setup_trace(
repo: &Repository,
trace_cwd: &Path,
actual_cwd: &Path,
_setup_prefix: &str,
git_dir_display: &str,
) -> String {
let gd = repo
.git_dir
.canonicalize()
.unwrap_or_else(|_| repo.git_dir.clone());
let Some(common) = resolve_common_dir(&gd) else {
return git_dir_display.to_owned();
};
let common = common.canonicalize().unwrap_or(common);
if common == gd {
return git_dir_display.to_owned();
}
let ac = actual_cwd
.canonicalize()
.unwrap_or_else(|_| actual_cwd.to_path_buf());
if repo.work_tree.is_none() && !repo.explicit_git_dir {
if ac == common {
return ".".to_owned();
}
if ac.starts_with(&common) && ac != common {
return common.display().to_string();
}
}
let work_relocated = match (&repo.discovery_root, &repo.work_tree) {
(Some(root), Some(wt)) if !repo.work_tree_from_env => {
let r = root.canonicalize().unwrap_or_else(|_| root.clone());
let w = wt.canonicalize().unwrap_or_else(|_| wt.clone());
r != w
}
_ => false,
};
if work_relocated {
if let Some(wt) = &repo.work_tree {
let wt = wt.canonicalize().unwrap_or_else(|_| wt.clone());
if ac == wt {
return common.display().to_string();
}
let inside_wt = ac.starts_with(&wt) && ac != wt;
if inside_wt {
if let Some(rel) = path_relative_to(&common, &ac) {
let s = rel.to_string_lossy().replace('\\', "/");
return if s.is_empty() || s == "." {
".".to_owned()
} else {
s
};
}
}
}
}
if repo.discovery_via_gitfile && !repo.explicit_git_dir {
return common.display().to_string();
}
if repo.work_tree.is_none() && !repo.explicit_git_dir {
let tc = trace_cwd
.canonicalize()
.unwrap_or_else(|_| trace_cwd.to_path_buf());
if let Some(cp) = common.parent() {
let cp = cp.canonicalize().unwrap_or_else(|_| cp.to_path_buf());
let comc = common.canonicalize().unwrap_or_else(|_| common.clone());
if tc.starts_with(&cp) && tc != cp && !tc.starts_with(&comc) {
return comc.display().to_string();
}
if tc == cp {
return rel_path_for_setup_trace(&common, trace_cwd);
}
}
}
if trace_cwd_strictly_inside_git_parent(trace_cwd, &common) {
rel_path_for_setup_trace(&common, trace_cwd)
} else {
common.display().to_string()
}
}
fn resolve_common_dir(git_dir: &Path) -> Option<PathBuf> {
let common_raw = fs::read_to_string(git_dir.join("commondir")).ok()?;
let common_rel = common_raw.trim();
if common_rel.is_empty() {
return None;
}
let common_dir = if Path::new(common_rel).is_absolute() {
PathBuf::from(common_rel)
} else {
git_dir.join(common_rel)
};
Some(common_dir.canonicalize().unwrap_or(common_dir))
}
#[must_use]
pub fn common_git_dir_for_config(git_dir: &Path) -> PathBuf {
resolve_common_dir(git_dir).unwrap_or_else(|| git_dir.to_path_buf())
}
pub fn worktree_config_enabled(common_dir: &Path) -> bool {
let path = common_dir.join("config");
let Ok(content) = fs::read_to_string(&path) else {
return false;
};
let mut in_extensions = false;
for raw_line in content.lines() {
let mut line = raw_line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
continue;
}
if line.starts_with('[') {
let Some(end_idx) = line.find(']') else {
continue;
};
let section = line[1..end_idx].trim();
let section_name = section
.split_whitespace()
.next()
.unwrap_or_default()
.to_ascii_lowercase();
in_extensions = section_name == "extensions";
let remainder = line[end_idx + 1..].trim();
if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
continue;
}
line = remainder;
}
if in_extensions {
let Some((key, value)) = line.split_once('=') else {
continue;
};
if key.trim().eq_ignore_ascii_case("worktreeconfig") {
let v = value.trim();
return v.eq_ignore_ascii_case("true")
|| v.eq_ignore_ascii_case("yes")
|| v.eq_ignore_ascii_case("on")
|| v == "1";
}
}
}
false
}
fn open_or_create_config_file(path: &Path, scope: ConfigScope) -> Result<ConfigFile> {
match ConfigFile::from_path(path, scope)? {
Some(f) => Ok(f),
None => {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(Error::Io)?;
}
ConfigFile::parse(path, "", scope)
}
}
}
fn config_file_bool_true(cfg: &ConfigFile, key: &str) -> bool {
cfg.get(key).is_some_and(|v| {
matches!(
v.trim().to_ascii_lowercase().as_str(),
"true" | "yes" | "on" | "1"
)
})
}
pub fn init_worktree_config(git_dir: &Path) -> Result<()> {
let common_dir = common_git_dir_for_config(git_dir);
let common_config_path = common_dir.join("config");
let worktree_config_path = git_dir.join("config.worktree");
if worktree_config_enabled(&common_dir) {
if !worktree_config_path.exists() {
if let Some(parent) = worktree_config_path.parent() {
fs::create_dir_all(parent).map_err(Error::Io)?;
}
fs::write(&worktree_config_path, "").map_err(Error::Io)?;
}
return Ok(());
}
let mut common_cfg = open_or_create_config_file(&common_config_path, ConfigScope::Local)?;
common_cfg.set("extensions.worktreeConfig", "true")?;
let mut wt_cfg = open_or_create_config_file(&worktree_config_path, ConfigScope::Worktree)?;
if config_file_bool_true(&common_cfg, "core.bare") {
wt_cfg.set("core.bare", "true")?;
common_cfg.unset("core.bare")?;
}
if let Some(worktree) = common_cfg.get("core.worktree") {
wt_cfg.set("core.worktree", &worktree)?;
common_cfg.unset("core.worktree")?;
}
common_cfg.write()?;
wt_cfg.write()?;
Ok(())
}
pub fn early_config_ignore_repo_reason(common_dir: &Path) -> Option<String> {
const GIT_REPO_VERSION_READ: u32 = 1;
let path = common_dir.join("config");
let content = fs::read_to_string(&path).ok()?;
let mut version = 0u32;
let mut in_core = false;
for raw_line in content.lines() {
let mut line = raw_line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
continue;
}
if line.starts_with('[') {
let Some(end_idx) = line.find(']') else {
continue;
};
let section = line[1..end_idx].trim();
let section_name = section
.split_whitespace()
.next()
.unwrap_or_default()
.to_ascii_lowercase();
in_core = section_name == "core";
let remainder = line[end_idx + 1..].trim();
if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
continue;
}
line = remainder;
}
if in_core {
if let Some((key, value)) = line.split_once('=') {
if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
if let Ok(v) = value.trim().parse::<u32>() {
version = v;
}
}
}
}
}
if version > GIT_REPO_VERSION_READ {
Some(format!(
"Expected git repo version <= {GIT_REPO_VERSION_READ}, found {version}"
))
} else {
None
}
}
fn path_for_ceiling_compare(path: &Path) -> String {
let path = path.to_string_lossy();
#[cfg(windows)]
{
path.replace('\\', "/")
}
#[cfg(not(windows))]
{
path.into_owned()
}
}
fn offset_1st_component(path: &str) -> usize {
if path.starts_with('/') {
1
} else {
0
}
}
fn longest_ancestor_length(path: &str, ceilings: &[String]) -> Option<usize> {
if path == "/" {
return None;
}
let mut max_len: Option<usize> = None;
for ceil in ceilings {
let mut len = ceil.len();
while len > 0 && ceil.as_bytes().get(len - 1) == Some(&b'/') {
len -= 1;
}
if len == 0 {
continue;
}
if path.len() <= len + 1 {
continue;
}
if !path.starts_with(&ceil[..len]) {
continue;
}
if path.as_bytes().get(len) != Some(&b'/') {
continue;
}
if path.as_bytes().get(len + 1).is_none() {
continue;
}
max_len = Some(max_len.map_or(len, |m| m.max(len)));
}
max_len
}
fn repository_config_path(git_dir: &Path) -> Option<PathBuf> {
let local = git_dir.join("config");
if local.exists() {
return Some(local);
}
let common = resolve_common_dir(git_dir)?;
let shared = common.join("config");
if shared.exists() {
Some(shared)
} else {
None
}
}
pub fn validate_repo_format(git_dir: &Path) -> Result<()> {
validate_repository_format(git_dir)
}
fn validate_repository_format(git_dir: &Path) -> Result<()> {
let Some(config_path) = repository_config_path(git_dir) else {
return Ok(());
};
let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
let mut in_core = false;
let mut in_extensions = false;
let mut repo_version = 0u32;
let mut extensions = BTreeSet::new();
let mut ref_storage: Option<String> = None;
for raw_line in content.lines() {
let mut line = raw_line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
continue;
}
if line.starts_with('[') {
let Some(end_idx) = line.find(']') else {
return Err(Error::ConfigError(format!(
"invalid config in {}",
config_path.display()
)));
};
let section = line[1..end_idx].trim();
let section_name = section
.split_whitespace()
.next()
.unwrap_or_default()
.to_ascii_lowercase();
in_core = section_name == "core";
in_extensions = section_name == "extensions";
let remainder = line[end_idx + 1..].trim();
if remainder.is_empty() || remainder.starts_with('#') || remainder.starts_with(';') {
continue;
}
line = remainder;
}
if in_core {
if let Some((key, value)) = line.split_once('=') {
if key.trim().eq_ignore_ascii_case("repositoryformatversion") {
if let Ok(v) = value.trim().parse::<u32>() {
repo_version = v;
}
}
}
}
if in_extensions {
let (key, value) = if let Some((key, value)) = line.split_once('=') {
(key.trim(), Some(value.trim()))
} else {
(line, None)
};
if key.eq_ignore_ascii_case("refstorage") {
ref_storage = value.map(str::to_owned);
}
let key = if let Some((key, _)) = line.split_once('=') {
key.trim()
} else {
line
};
if !key.is_empty() {
extensions.insert(key.to_ascii_lowercase());
}
}
}
if repo_version > 1 {
return Err(Error::UnsupportedRepositoryFormatVersion(repo_version));
}
if let Some(raw) = ref_storage.as_deref() {
let lower = raw.to_ascii_lowercase();
let name = lower
.split_once(':')
.map(|(prefix, _)| prefix)
.unwrap_or(lower.as_str());
if !matches!(name, "files" | "reftable") {
return Err(Error::Message(format!(
"error: invalid value for 'extensions.refstorage': '{raw}'"
)));
}
}
let mut v1_only_found: Vec<String> = Vec::new();
let mut unknown_found: Vec<String> = Vec::new();
for extension in extensions {
match extension.as_str() {
"noop" | "preciousobjects" | "partialclone" | "worktreeconfig" => {}
"noop-v1"
| "objectformat"
| "compatobjectformat"
| "refstorage"
| "relativeworktrees"
| "submodulepathconfig" => {
if repo_version == 0 {
v1_only_found.push(extension);
}
}
_ => {
if repo_version >= 1 {
unknown_found.push(extension);
}
}
}
}
if !unknown_found.is_empty() {
let mut msg = if unknown_found.len() == 1 {
"unknown repository extension found:".to_owned()
} else {
"unknown repository extensions found:".to_owned()
};
for ext in &unknown_found {
msg.push_str(&format!("\n\t{ext}"));
}
return Err(Error::Message(msg));
}
if !v1_only_found.is_empty() {
let mut msg = if v1_only_found.len() == 1 {
"repo version is 0, but v1-only extension found:".to_owned()
} else {
"repo version is 0, but v1-only extensions found:".to_owned()
};
for ext in &v1_only_found {
msg.push_str(&format!("\n\t{ext}"));
}
return Err(Error::Message(msg));
}
Ok(())
}
struct DiscoveredAt {
repo: Repository,
gitfile: Option<PathBuf>,
}
fn try_open_at(dir: &Path) -> Result<Option<DiscoveredAt>> {
let dot_git = dir.join(".git");
#[cfg(unix)]
{
use std::os::unix::fs::FileTypeExt;
if let Ok(meta) = fs::symlink_metadata(&dot_git) {
let ft = meta.file_type();
if ft.is_fifo() || ft.is_socket() || ft.is_block_device() || ft.is_char_device() {
return Err(Error::NotARepository(format!(
"invalid gitfile format: {} is not a regular file",
dot_git.display()
)));
}
if ft.is_symlink() {
if let Ok(target_meta) = fs::metadata(&dot_git) {
let tft = target_meta.file_type();
if tft.is_fifo()
|| tft.is_socket()
|| tft.is_block_device()
|| tft.is_char_device()
{
return Err(Error::NotARepository(format!(
"invalid gitfile format: {} is not a regular file",
dot_git.display()
)));
}
}
}
}
}
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 mut repo = Repository::open_skipping_format_validation(&git_dir, Some(dir))?;
if resolve_common_dir(&git_dir).is_some() {
let cwd = env::current_dir().map_err(Error::Io)?;
if repo.work_tree.is_some() && !is_inside_work_tree(&repo, &cwd) {
let root = if dir.is_absolute() {
dir.to_path_buf()
} else {
cwd.join(dir)
};
repo.work_tree = Some(root.canonicalize().unwrap_or(root));
}
}
let root = if dir.is_absolute() {
dir.to_path_buf()
} else {
env::current_dir().map_err(Error::Io)?.join(dir)
};
repo.discovery_root = Some(root.canonicalize().unwrap_or(root));
repo.discovery_via_gitfile = true;
warn_core_bare_worktree_conflict(&git_dir);
return Ok(Some(DiscoveredAt {
repo,
gitfile: Some(dot_git.clone()),
}));
}
if dot_git.is_dir() {
let open_path = if dot_git.is_symlink() {
dot_git.read_link().unwrap_or_else(|_| dot_git.clone())
} else {
dot_git.clone()
};
match Repository::open_skipping_format_validation(&open_path, Some(dir)) {
Ok(mut repo) => {
if dot_git.is_symlink() {
let abs_dot_git = if dot_git.is_absolute() {
dot_git
} else {
dir.join(".git")
};
repo.git_dir = abs_dot_git;
}
let root = if dir.is_absolute() {
dir.to_path_buf()
} else {
env::current_dir().map_err(Error::Io)?.join(dir)
};
repo.discovery_root = Some(root.canonicalize().unwrap_or(root));
repo.discovery_via_gitfile = false;
return Ok(Some(DiscoveredAt {
repo,
gitfile: None,
}));
}
Err(Error::NotARepository(_)) | Err(Error::ConfigError(_)) => return Ok(None),
Err(Error::Message(ref msg)) if msg.contains("bad config") => return Ok(None),
Err(e) => return Err(e),
}
}
if dir.join("HEAD").is_file() && dir.join("commondir").is_file() {
maybe_trace_implicit_bare_repository(dir);
let repo = Repository::open(dir, None)?;
warn_core_bare_worktree_conflict(dir);
return Ok(Some(DiscoveredAt {
repo,
gitfile: None,
}));
}
if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
maybe_trace_implicit_bare_repository(dir);
if !is_inside_dot_git(dir) {
if let Ok(cfg) = crate::config::ConfigSet::load(None, true) {
if let Some(val) = cfg.get("safe.bareRepository") {
if val.eq_ignore_ascii_case("explicit") {
return Err(Error::ForbiddenBareRepository(dir.display().to_string()));
}
}
}
}
let repo = Repository::open(dir, None)?;
warn_core_bare_worktree_conflict(dir);
return Ok(Some(DiscoveredAt {
repo,
gitfile: None,
}));
}
Ok(None)
}
fn is_inside_dot_git(path: &Path) -> bool {
path.components().any(|c| c.as_os_str() == ".git")
}
fn maybe_trace_implicit_bare_repository(dir: &Path) {
let path = match std::env::var("GIT_TRACE2_PERF") {
Ok(p) if !p.is_empty() => p,
_ => return,
};
if let Ok(mut file) = OpenOptions::new().create(true).append(true).open(path) {
let _ = writeln!(file, "setup: implicit-bare-repository:{}", dir.display());
}
}
fn safe_directory_effective_values(git_dir: &Path) -> Vec<String> {
let cfg = crate::config::ConfigSet::load(Some(git_dir), true)
.unwrap_or_else(|_| crate::config::ConfigSet::new());
let mut values: Vec<String> = Vec::new();
for e in cfg.entries() {
if e.key == "safe.directory"
&& e.scope != crate::config::ConfigScope::Local
&& e.scope != crate::config::ConfigScope::Worktree
{
values.push(e.value.clone().unwrap_or_else(|| "true".to_owned()));
}
}
let mut effective: Vec<String> = Vec::new();
for v in values {
if v.is_empty() {
effective.clear();
} else {
effective.push(v);
}
}
effective
}
fn ensure_safe_directory_allows(git_dir: &Path, checked: &Path) -> Result<()> {
let effective = safe_directory_effective_values(git_dir);
let checked_s = checked.to_string_lossy().to_string();
if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
eprintln!("debug-safe-directory values={:?}", effective);
}
if effective
.iter()
.any(|v| safe_directory_matches(v, &checked_s))
{
return Ok(());
}
Err(Error::DubiousOwnership(checked_s))
}
#[cfg(unix)]
fn path_lstat_uid(path: &Path) -> std::io::Result<u32> {
use std::os::unix::fs::MetadataExt;
let meta = fs::symlink_metadata(path)?;
Ok(meta.uid())
}
#[cfg(unix)]
fn extract_uid_from_env(name: &str) -> Option<u32> {
let raw = std::env::var(name).ok()?;
if raw.is_empty() {
return None;
}
raw.parse::<u32>().ok()
}
#[cfg(unix)]
fn ensure_valid_ownership(
gitfile: Option<&Path>,
worktree: Option<&Path>,
gitdir: &Path,
) -> Result<()> {
const ROOT_UID: u32 = 0;
fn owned_by_effective_user(path: &Path) -> std::io::Result<bool> {
let st_uid = path_lstat_uid(path)?;
let mut euid = unsafe { libc::geteuid() };
if euid == ROOT_UID {
if st_uid == ROOT_UID {
return Ok(true);
}
if let Some(sudo_uid) = extract_uid_from_env("SUDO_UID") {
euid = sudo_uid;
}
}
Ok(st_uid == euid)
}
let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
.ok()
.map(|v| {
let lower = v.to_ascii_lowercase();
v == "1" || lower == "true" || lower == "yes" || lower == "on"
})
.unwrap_or(false);
if !assume_different {
let gitfile_ok = gitfile
.map(owned_by_effective_user)
.transpose()?
.unwrap_or(true);
let wt_ok = match worktree {
None => true,
Some(wt) => match owned_by_effective_user(wt) {
Ok(ok) => ok,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => true,
Err(e) => return Err(Error::Io(e)),
},
};
let gd_ok = owned_by_effective_user(gitdir)?;
if gitfile_ok && wt_ok && gd_ok {
return Ok(());
}
}
let data_path = if let Some(wt) = worktree {
wt.canonicalize().unwrap_or_else(|_| wt.to_path_buf())
} else {
gitdir
.canonicalize()
.unwrap_or_else(|_| gitdir.to_path_buf())
};
ensure_safe_directory_allows(gitdir, &data_path)
}
#[cfg(not(unix))]
fn ensure_valid_ownership(
_gitfile: Option<&Path>,
_worktree: Option<&Path>,
_gitdir: &Path,
) -> Result<()> {
Ok(())
}
impl Repository {
pub fn enforce_safe_directory(&self) -> Result<()> {
let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
.ok()
.map(|v| {
let lower = v.to_ascii_lowercase();
v == "1" || lower == "true" || lower == "yes" || lower == "on"
})
.unwrap_or(false);
if !assume_different {
return Ok(());
}
if self.explicit_git_dir {
return Ok(());
}
let checked = if let Some(wt) = &self.work_tree {
let cwd = std::env::current_dir().ok();
if let Some(cwd) = cwd {
if cwd
.canonicalize()
.ok()
.is_some_and(|c| c.starts_with(&self.git_dir))
{
self.git_dir
.canonicalize()
.unwrap_or_else(|_| self.git_dir.clone())
} else {
wt.canonicalize().unwrap_or_else(|_| wt.clone())
}
} else {
wt.canonicalize().unwrap_or_else(|_| wt.clone())
}
} else {
self.git_dir
.canonicalize()
.unwrap_or_else(|_| self.git_dir.clone())
};
if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
eprintln!(
"debug-safe-directory checked={} git_dir={} work_tree={:?} cwd={:?}",
checked.display(),
self.git_dir.display(),
self.work_tree,
std::env::current_dir().ok()
);
}
self.enforce_safe_directory_checked(&checked)
}
pub fn enforce_safe_directory_git_dir(&self) -> Result<()> {
let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
.ok()
.map(|v| {
let lower = v.to_ascii_lowercase();
v == "1" || lower == "true" || lower == "yes" || lower == "on"
})
.unwrap_or(false);
if !assume_different {
return Ok(());
}
let checked = self
.git_dir
.canonicalize()
.unwrap_or_else(|_| self.git_dir.clone());
if std::env::var("GRIT_DEBUG_SAFE_DIR").is_ok() {
eprintln!(
"debug-safe-directory(gitdir) checked={} git_dir={} work_tree={:?}",
checked.display(),
self.git_dir.display(),
self.work_tree
);
}
self.enforce_safe_directory_checked(&checked)
}
pub fn enforce_safe_directory_git_dir_with_path(&self, checked: &Path) -> Result<()> {
let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
.ok()
.map(|v| {
let lower = v.to_ascii_lowercase();
v == "1" || lower == "true" || lower == "yes" || lower == "on"
})
.unwrap_or(false);
if !assume_different {
return Ok(());
}
self.enforce_safe_directory_checked(checked)
}
fn enforce_safe_directory_checked(&self, checked: &Path) -> Result<()> {
ensure_safe_directory_allows(&self.git_dir, checked)
}
pub fn verify_safe_for_clone_source(&self) -> Result<()> {
let assume_different = std::env::var("GIT_TEST_ASSUME_DIFFERENT_OWNER")
.ok()
.map(|v| {
let lower = v.to_ascii_lowercase();
v == "1" || lower == "true" || lower == "yes" || lower == "on"
})
.unwrap_or(false);
if assume_different {
self.enforce_safe_directory_git_dir()
} else {
#[cfg(unix)]
{
ensure_valid_ownership(None, None, &self.git_dir)
}
#[cfg(not(unix))]
{
Ok(())
}
}
}
}
fn normalize_fs_path(raw: &str) -> String {
use std::path::Component;
let p = std::path::Path::new(raw);
let mut parts: Vec<String> = Vec::new();
let mut absolute = false;
for c in p.components() {
match c {
Component::RootDir => {
absolute = true;
parts.clear();
}
Component::CurDir => {}
Component::ParentDir => {
if !parts.is_empty() {
parts.pop();
}
}
Component::Normal(s) => parts.push(s.to_string_lossy().to_string()),
Component::Prefix(_) => {}
}
}
let mut out = if absolute {
String::from("/")
} else {
String::new()
};
out.push_str(&parts.join("/"));
out
}
fn safe_directory_matches(config_value: &str, checked: &str) -> bool {
if config_value == "*" {
return true;
}
if config_value == "." {
if let Ok(cwd) = std::env::current_dir() {
let cwd_s = normalize_fs_path(&cwd.to_string_lossy());
let checked_s = normalize_fs_path(checked);
return cwd_s == checked_s;
}
return false;
}
let canonicalize_or_normalize = |raw: &str| -> String {
let p = std::path::Path::new(raw);
if p.exists() {
p.canonicalize()
.map(|c| c.to_string_lossy().to_string())
.map(|s| normalize_fs_path(&s))
.unwrap_or_else(|_| normalize_fs_path(raw))
} else {
normalize_fs_path(raw)
}
};
let config_norm = canonicalize_or_normalize(config_value);
let checked_norm = normalize_fs_path(checked);
if config_norm.ends_with("/*") {
let prefix_raw = &config_norm[..config_norm.len() - 2];
let prefix_norm = canonicalize_or_normalize(prefix_raw);
let mut prefix = prefix_norm;
if !prefix.ends_with('/') {
prefix.push('/');
}
return checked_norm.starts_with(&prefix);
}
config_norm == checked_norm
}
fn warn_core_bare_worktree_conflict(git_dir: &Path) {
if env::var("GIT_WORK_TREE")
.ok()
.filter(|s| !s.trim().is_empty())
.is_some()
{
return;
}
static WARNED_DIRS: Mutex<Option<HashSet<String>>> = Mutex::new(None);
if let Ok((bare, wt)) = read_core_bare_and_worktree(git_dir) {
if bare && wt.is_some() {
let key = git_dir
.canonicalize()
.unwrap_or_else(|_| git_dir.to_path_buf())
.to_string_lossy()
.to_string();
let mut guard = WARNED_DIRS.lock().unwrap_or_else(|e| e.into_inner());
let set = guard.get_or_insert_with(HashSet::new);
if set.insert(key) {
eprintln!("warning: core.bare and core.worktree do not make sense");
}
}
}
}
fn read_core_bare_and_worktree(git_dir: &Path) -> Result<(bool, Option<String>)> {
let Some(config_path) = repository_config_path(git_dir) else {
return Ok((false, None));
};
let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
let mut in_core = false;
let mut bare = false;
let mut worktree: Option<String> = None;
for raw_line in content.lines() {
let line = raw_line.trim();
if line.is_empty() || line.starts_with('#') || line.starts_with(';') {
continue;
}
if line.starts_with('[') {
in_core = line.eq_ignore_ascii_case("[core]");
continue;
}
if !in_core {
continue;
}
if let Some((k, v)) = line.split_once('=') {
let key = k.trim();
let val = v.trim();
if key.eq_ignore_ascii_case("bare") {
bare = val.eq_ignore_ascii_case("true");
} else if key.eq_ignore_ascii_case("worktree") {
worktree = Some(val.to_owned());
}
}
}
Ok((bare, worktree))
}
fn validate_git_work_tree_path(path: &Path) -> Result<()> {
if !path.is_absolute() {
return Ok(());
}
let comps: Vec<Component<'_>> = path.components().collect();
let Some(last_normal_idx) = comps
.iter()
.enumerate()
.rev()
.find_map(|(i, c)| matches!(c, Component::Normal(_)).then_some(i))
else {
return Ok(());
};
let mut cur = PathBuf::new();
for (i, comp) in comps.iter().enumerate() {
match comp {
Component::Prefix(p) => cur.push(p.as_os_str()),
Component::RootDir => cur.push(comp.as_os_str()),
Component::CurDir => {}
Component::ParentDir => {
let _ = cur.pop();
}
Component::Normal(seg) => {
cur.push(seg);
if i != last_normal_idx && !cur.exists() {
return Err(Error::PathError(format!(
"Invalid path '{}': No such file or directory",
cur.display()
)));
}
}
}
}
Ok(())
}
fn resolve_core_worktree_path(git_dir: &Path, raw: &str) -> Result<PathBuf> {
let p = Path::new(raw);
if p.is_absolute() {
return Ok(p.canonicalize().unwrap_or_else(|_| p.to_path_buf()));
}
let old = env::current_dir().map_err(Error::Io)?;
env::set_current_dir(git_dir).map_err(Error::Io)?;
env::set_current_dir(raw).map_err(Error::Io)?;
let resolved = env::current_dir().map_err(Error::Io)?;
env::set_current_dir(&old).map_err(Error::Io)?;
Ok(resolved.canonicalize().unwrap_or(resolved))
}
fn resolve_git_dir_env_path(git_dir: &Path) -> Result<PathBuf> {
if git_dir.is_file() {
let content =
fs::read_to_string(git_dir).map_err(|e| Error::NotARepository(e.to_string()))?;
let base = git_dir
.parent()
.ok_or_else(|| Error::NotARepository(git_dir.display().to_string()))?;
return parse_gitfile(&content, base);
}
Ok(git_dir.to_path_buf())
}
pub fn resolve_git_directory_arg(git_dir: &Path) -> Result<PathBuf> {
resolve_git_dir_env_path(git_dir)
}
pub fn resolve_dot_git(dot_git: &Path) -> Result<PathBuf> {
if dot_git.is_dir() {
return dot_git
.canonicalize()
.map_err(|_| Error::NotARepository(dot_git.display().to_string()));
}
if dot_git.is_file() {
let content =
fs::read_to_string(dot_git).map_err(|e| Error::NotARepository(e.to_string()))?;
let base = dot_git
.parent()
.ok_or_else(|| Error::NotARepository(dot_git.display().to_string()))?;
return parse_gitfile(&content, base);
}
Err(Error::NotARepository(dot_git.display().to_string()))
}
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)
};
if !path.exists() {
return Err(Error::NotARepository(path.display().to_string()));
}
return Ok(path);
}
}
Err(Error::NotARepository("invalid gitfile format".to_owned()))
}
fn write_fresh_git_directory(
git_dir: &Path,
bare: bool,
initial_branch: &str,
template_dir: Option<&Path>,
ref_storage: &str,
skip_hooks_and_info: bool,
) -> Result<()> {
let mut subs = vec![
"objects",
"objects/info",
"objects/pack",
"refs",
"refs/heads",
"refs/tags",
];
if !bare && !skip_hooks_and_info {
subs.push("info");
subs.push("hooks");
}
for sub in subs {
fs::create_dir_all(git_dir.join(sub))?;
}
if ref_storage == "reftable" {
let reftable_dir = git_dir.join("reftable");
fs::create_dir_all(&reftable_dir)?;
let tables_list = reftable_dir.join("tables.list");
if !tables_list.exists() {
fs::write(&tables_list, "")?;
}
}
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 needs_extensions = ref_storage == "reftable";
let repo_version = if needs_extensions { 1 } else { 0 };
let mut config_content = String::from("[core]\n");
config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
config_content.push_str("\tfilemode = true\n");
if bare {
config_content.push_str("\tbare = true\n");
} else {
config_content.push_str("\tbare = false\n");
config_content.push_str("\tlogallrefupdates = true\n");
}
if needs_extensions {
config_content.push_str("[extensions]\n");
config_content.push_str("\trefStorage = reftable\n");
}
fs::write(git_dir.join("config"), config_content)?;
if let Some(tmpl) = template_dir {
if tmpl.is_dir() {
let tmpl_config = tmpl.join("config");
if tmpl_config.is_file() {
let tmpl_text = fs::read_to_string(&tmpl_config)?;
let tmpl_parsed = ConfigFile::parse(&tmpl_config, &tmpl_text, ConfigScope::Local)?;
let dest_path = git_dir.join("config");
let dest_text = fs::read_to_string(&dest_path)?;
let mut dest_parsed =
ConfigFile::parse(&dest_path, &dest_text, ConfigScope::Local)?;
for e in &tmpl_parsed.entries {
if e.key == "core.bare" {
continue;
}
if let Some(v) = &e.value {
let _ = dest_parsed.set(&e.key, v);
} else {
let _ = dest_parsed.set(&e.key, "true");
}
}
dest_parsed.write()?;
}
}
}
fs::write(
git_dir.join("description"),
"Unnamed repository; edit this file 'description' to name the repository.\n",
)?;
Ok(())
}
pub fn init_repository_separate_git_dir(
work_tree: &Path,
git_dir: &Path,
initial_branch: &str,
template_dir: Option<&Path>,
ref_storage: &str,
) -> Result<Repository> {
let skip_hooks_info = template_dir.is_some_and(|p| p.as_os_str().is_empty());
fs::create_dir_all(work_tree)?;
fs::create_dir_all(git_dir)?;
write_fresh_git_directory(
git_dir,
false,
initial_branch,
template_dir,
ref_storage,
skip_hooks_info,
)?;
let gitfile = work_tree.join(".git");
let rel_git_dir = pathdiff_relative_gitfile(work_tree, git_dir);
fs::write(gitfile, format!("gitdir: {rel_git_dir}\n"))?;
Repository::open(git_dir, Some(work_tree))
}
fn pathdiff_relative_gitfile(from: &Path, to: &Path) -> String {
let from_c = fs::canonicalize(from).unwrap_or_else(|_| from.to_path_buf());
let to_c = fs::canonicalize(to).unwrap_or_else(|_| to.to_path_buf());
let from_comp: Vec<Component<'_>> = from_c.components().collect();
let to_comp: Vec<Component<'_>> = to_c.components().collect();
let mut i = 0usize;
while i < from_comp.len() && i < to_comp.len() && from_comp[i] == to_comp[i] {
i += 1;
}
let mut out = PathBuf::new();
for _ in i..from_comp.len() {
out.push("..");
}
for c in &to_comp[i..] {
out.push(c.as_os_str());
}
out.to_string_lossy().replace('\\', "/")
}
pub fn ensure_core_bare(git_dir: &Path) -> Result<()> {
let path = git_dir.join("config");
let text = fs::read_to_string(&path).unwrap_or_default();
if text.lines().any(|l| {
let t = l.trim();
t == "bare = true" || t == "bare=true"
}) {
return Ok(());
}
let mut out = text;
if !out.ends_with('\n') && !out.is_empty() {
out.push('\n');
}
if !out.contains("[core]") {
out.push_str("[core]\n");
}
out.push_str("\tbare = true\n");
fs::write(path, out).map_err(Error::Io)
}
pub fn init_bare_clone_minimal(
git_dir: &Path,
initial_branch: &str,
ref_storage: &str,
) -> Result<()> {
for sub in &[
"objects",
"objects/info",
"objects/pack",
"refs",
"refs/heads",
"refs/tags",
] {
fs::create_dir_all(git_dir.join(sub))?;
}
if ref_storage == "reftable" {
let reftable_dir = git_dir.join("reftable");
fs::create_dir_all(&reftable_dir)?;
let tables_list = reftable_dir.join("tables.list");
if !tables_list.exists() {
fs::write(&tables_list, "")?;
}
}
let head_content = format!("ref: refs/heads/{initial_branch}\n");
fs::write(git_dir.join("HEAD"), head_content)?;
let needs_extensions = ref_storage == "reftable";
let repo_version = if needs_extensions { 1 } else { 0 };
let mut config_content = String::from("[core]\n");
config_content.push_str(&format!("\trepositoryformatversion = {repo_version}\n"));
config_content.push_str("\tfilemode = true\n");
config_content.push_str("\tbare = true\n");
if needs_extensions {
config_content.push_str("[extensions]\n");
config_content.push_str("\trefStorage = reftable\n");
}
fs::write(git_dir.join("config"), config_content)?;
fs::write(
git_dir.join("packed-refs"),
"# pack-refs with: peeled fully-peeled sorted\n",
)?;
Ok(())
}
pub fn init_repository(
path: &Path,
bare: bool,
initial_branch: &str,
template_dir: Option<&Path>,
ref_storage: &str,
) -> Result<Repository> {
let skip_hooks_info = !bare && template_dir.is_some_and(|p| p.as_os_str().is_empty());
let git_dir = if bare {
path.to_path_buf()
} else {
path.join(".git")
};
if !bare {
fs::create_dir_all(path)?;
}
fs::create_dir_all(&git_dir)?;
write_fresh_git_directory(
&git_dir,
bare,
initial_branch,
template_dir,
ref_storage,
skip_hooks_info,
)?;
let work_tree = if bare { None } else { Some(path) };
Repository::open(&git_dir, work_tree)
}
pub fn init_bare_with_env_worktree(
git_dir: &Path,
work_tree: &Path,
initial_branch: &str,
template_dir: Option<&Path>,
ref_storage: &str,
) -> Result<Repository> {
fs::create_dir_all(git_dir)?;
fs::create_dir_all(work_tree)?;
write_fresh_git_directory(
git_dir,
true,
initial_branch,
template_dir,
ref_storage,
false,
)?;
let work_tree_abs = fs::canonicalize(work_tree).unwrap_or_else(|_| work_tree.to_path_buf());
let config_path = git_dir.join("config");
let mut config = match ConfigFile::from_path(&config_path, ConfigScope::Local)? {
Some(c) => c,
None => ConfigFile::parse(&config_path, "", ConfigScope::Local)?,
};
config.set("core.worktree", &work_tree_abs.to_string_lossy())?;
config.write()?;
Repository::open(git_dir, Some(work_tree))
}
pub fn init_repository_separate(
work_tree: &Path,
git_dir: &Path,
initial_branch: &str,
template_dir: Option<&Path>,
) -> Result<Repository> {
fs::create_dir_all(work_tree)?;
if git_dir.exists() {
return Err(Error::PathError(format!(
"git directory '{}' already exists",
git_dir.display()
)));
}
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)?;
}
}
fs::write(
git_dir.join("HEAD"),
format!("ref: refs/heads/{initial_branch}\n"),
)?;
let work_tree_abs = fs::canonicalize(work_tree).unwrap_or_else(|_| work_tree.to_path_buf());
let git_dir_abs = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
let config_content = format!(
"[core]\n\trepositoryformatversion = 0\n\tfilemode = true\n\tbare = false\n\tlogallrefupdates = true\n\tworktree = {}\n",
work_tree_abs.display()
);
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 gitfile = work_tree.join(".git");
fs::write(&gitfile, format!("gitdir: {}\n", git_dir_abs.display()))?;
Repository::open(git_dir, Some(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>, bool) {
let raw = match env::var("GIT_CEILING_DIRECTORIES") {
Ok(val) => val,
Err(_) => return (Vec::new(), false),
};
if raw.is_empty() {
return (Vec::new(), false);
}
let (no_resolve, effective) = if raw.starts_with(':') {
(true, &raw[1..])
} else {
(false, raw.as_str())
};
let paths = effective
.split(':')
.filter(|s| !s.is_empty())
.filter_map(|s| {
let p = PathBuf::from(s);
if !p.is_absolute() {
return None;
}
if no_resolve {
let s = s.trim_end_matches('/');
Some(PathBuf::from(s))
} else {
Some(p.canonicalize().unwrap_or_else(|_| {
let s = s.trim_end_matches('/');
PathBuf::from(s)
}))
}
})
.collect();
(paths, no_resolve)
}
pub fn validate_repo_config(config_text: &str) -> std::result::Result<(), String> {
let mut version: u32 = 0;
let mut in_core = false;
for line in config_text.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_core = trimmed.to_lowercase().starts_with("[core");
continue;
}
if in_core {
if let Some(rest) = trimmed.strip_prefix("repositoryformatversion") {
let val = rest.trim_start_matches([' ', '=']).trim();
if let Ok(v) = val.parse::<u32>() {
version = v;
}
}
}
}
if version >= 2 {
return Err(format!("unknown repository format version: {version}"));
}
Ok(())
}