use std::collections::BTreeSet;
use std::fs;
use std::fs::OpenOptions;
use std::io::Write;
use std::path::{Component, Path, PathBuf};
use std::sync::{Arc, Mutex};
use crate::change_id::ChangeIdGenerator;
use crate::check_ref_format::validate_branch_short_name;
use crate::command_runner::CommandRunner;
use crate::config::{
config_file_stamps, stamp_for_path, ConfigCacheKey, ConfigFile, ConfigScope, ConfigSet,
LoadConfigOptions,
};
use crate::diagnostics::{self, DiagnosticsHandle};
use crate::environment::{Environment, RepositoryOptions};
use crate::error::{Error, Result};
use crate::hooks::run_hook;
use crate::index::Index;
use crate::init_filesystem::{apply_init_filesystem_config, InitFilesystemConfigOptions};
use crate::objects::{parse_commit, HashAlgo};
use crate::odb::Odb;
use crate::repo_caches::RepoCaches;
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;
fn compute_git_prefix(env: &Environment, work_tree: &Path) -> Option<String> {
if let Some(prefix) = env.git_prefix.as_deref() {
let p = prefix.trim();
if !p.is_empty() {
return Some(p.to_owned());
}
}
let rel = cwd_relative_under_work_tree(work_tree, &env.cwd)?;
if rel.is_empty() {
None
} else {
Some(rel)
}
}
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, Clone, PartialEq, Eq)]
pub struct DiscoveryInfo {
pub explicit_git_dir: bool,
pub discovery_root: Option<PathBuf>,
pub work_tree_from_env: bool,
pub discovery_via_gitfile: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InitOutcome {
pub reinitialized: bool,
pub initial_branch: String,
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct InitOptions {
pub bare: bool,
pub initial_branch: Option<String>,
pub template_dir: Option<PathBuf>,
pub ref_storage: crate::ref_storage::RefStorageFormat,
pub separate_git_dir: Option<PathBuf>,
pub bare_with_worktree_at_path: bool,
pub object_format: Option<HashAlgo>,
pub environment: Option<Environment>,
}
impl Default for InitOptions {
fn default() -> Self {
Self {
bare: false,
initial_branch: None,
template_dir: None,
ref_storage: crate::ref_storage::RefStorageFormat::Files,
separate_git_dir: None,
bare_with_worktree_at_path: false,
object_format: None,
environment: None,
}
}
}
impl InitOptions {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_bare(mut self, bare: bool) -> Self {
self.bare = bare;
self
}
#[must_use]
pub fn with_initial_branch(mut self, initial_branch: impl Into<String>) -> Self {
self.initial_branch = Some(initial_branch.into());
self
}
#[must_use]
pub fn with_object_format(mut self, object_format: HashAlgo) -> Self {
self.object_format = Some(object_format);
self
}
#[must_use]
pub fn with_environment(mut self, environment: Environment) -> Self {
self.environment = Some(environment);
self
}
#[must_use]
pub fn with_ref_storage(mut self, ref_storage: crate::ref_storage::RefStorageFormat) -> Self {
self.ref_storage = ref_storage;
self
}
#[must_use]
pub fn with_template_dir(mut self, template_dir: Option<PathBuf>) -> Self {
self.template_dir = template_dir;
self
}
#[must_use]
pub fn with_separate_git_dir(mut self, separate_git_dir: Option<PathBuf>) -> Self {
self.separate_git_dir = separate_git_dir;
self
}
#[must_use]
pub fn with_bare_with_worktree_at_path(mut self, bare_with_worktree_at_path: bool) -> Self {
self.bare_with_worktree_at_path = bare_with_worktree_at_path;
self
}
}
#[must_use]
pub struct RepositoryBuilder {
options: RepositoryOptions,
odb_builder: Option<crate::odb::OdbBuilder>,
ref_store: Option<Arc<dyn crate::refs::store::RefStore>>,
}
impl RepositoryBuilder {
pub fn new(options: RepositoryOptions) -> Self {
Self {
options,
odb_builder: None,
ref_store: None,
}
}
pub fn odb_builder(mut self, builder: crate::odb::OdbBuilder) -> Self {
self.odb_builder = Some(builder);
self
}
pub fn ref_store(mut self, store: Arc<dyn crate::refs::store::RefStore>) -> Self {
self.ref_store = Some(store);
self
}
pub fn open(self, git_dir: &Path, work_tree: Option<&Path>) -> Result<Repository> {
let mut options = self.options;
if let Some(wt) = work_tree {
options.work_tree = Some(wt.to_path_buf());
}
let mut repo = if let Some(builder) = self.odb_builder {
Repository::open_with_odb_internal(&options, git_dir, builder)?
} else {
Repository::open_internal(&options, git_dir)?
};
if let Some(store) = self.ref_store {
repo = repo.with_ref_store(store);
}
Ok(repo)
}
}
pub struct Repository {
git_dir: PathBuf,
common_git_dir: PathBuf,
work_tree: Option<PathBuf>,
odb: Odb,
explicit_git_dir: bool,
discovery_root: Option<PathBuf>,
work_tree_from_env: bool,
discovery_via_gitfile: bool,
config_session: Mutex<Option<ConfigSession>>,
caches: Arc<RepoCaches>,
ref_store: Arc<dyn crate::refs::store::RefStore>,
environment: Arc<Environment>,
git_prefix: Option<String>,
command_runner: Arc<dyn CommandRunner>,
change_id_generator: Arc<dyn ChangeIdGenerator>,
diagnostics: DiagnosticsHandle,
network_trace: bool,
reference_unix_time: Option<i64>,
test_assume_different_owner: bool,
force_split_index: bool,
thread_policy: crate::parallel::ThreadPolicy,
}
const _: () = {
fn assert_send_sync<T: Send + Sync>() {}
let _ = assert_send_sync::<Repository>;
};
impl std::fmt::Debug for Repository {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Repository")
.field("git_dir", &self.git_dir())
.field("work_tree", &self.work_tree())
.field("explicit_git_dir", &self.discovery_info().explicit_git_dir)
.field("network_trace", &self.network_trace)
.finish_non_exhaustive()
}
}
pub(crate) type RepositoryConfigSnapshot = Arc<RepoCaches>;
#[derive(Debug, Clone)]
struct RepoCachedSettings {
use_replace_refs: bool,
replace_ref_base: String,
}
#[derive(Clone)]
struct ConfigSessionValidation {
key: ConfigCacheKey,
env_fingerprint: Vec<(String, Option<String>)>,
base_stamps: Vec<(PathBuf, Option<(std::time::SystemTime, u64)>)>,
extra_stamps: Vec<(PathBuf, Option<(std::time::SystemTime, u64)>)>,
}
struct ConfigSession {
config: Arc<ConfigSet>,
validation: ConfigSessionValidation,
cached_settings: RepoCachedSettings,
replace_lookup_inactive: bool,
}
impl ConfigSessionValidation {
fn still_valid(&self, env: &Environment, git_dir: &Path, opts: &LoadConfigOptions) -> bool {
let Some(env_fp) = env.config_fingerprint() else {
return false;
};
if self.env_fingerprint.as_slice() != env_fp.as_slice() {
return false;
}
let key = ConfigCacheKey::new(Some(git_dir), opts);
if key != self.key {
return false;
}
let base_stamps = config_file_stamps(env, Some(git_dir), opts);
if self.base_stamps.as_slice() != base_stamps.as_slice() {
return false;
}
for (path, stamp) in &self.extra_stamps {
if stamp_for_path(path) != *stamp {
return false;
}
}
true
}
}
impl Repository {
fn config_load_options(&self, include_system: bool) -> LoadConfigOptions {
LoadConfigOptions {
include_system,
include_ctx: crate::config::IncludeContext {
git_dir: Some(self.git_dir.clone()),
cwd: self.environment.cwd.clone(),
pwd: self.environment.pwd.clone(),
..Default::default()
},
diagnostics: Some(self.diagnostics()),
repo_caches: Some(Arc::clone(&self.caches)),
..Default::default()
}
}
fn from_canonical_git_dir(
options: &RepositoryOptions,
git_dir: PathBuf,
work_tree: Option<&Path>,
) -> Result<Self> {
let environment = Arc::new(options.environment.clone());
let command_runner = Arc::clone(&options.command_runner);
let change_id_generator = Arc::clone(&options.change_id_generator);
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 common_git_dir = resolve_common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
let objects_dir = if git_dir.join("objects").exists() {
git_dir.join("objects")
} else {
common_git_dir.join("objects")
};
if !objects_dir.exists() {
return Err(Error::NotARepository(git_dir.display().to_string()));
}
let work_tree = match work_tree {
Some(p) => {
let cwd = environment.discovery_cwd();
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 caches = RepoCaches::with_diagnostics(
Arc::clone(&command_runner),
Arc::clone(&options.diagnostics),
);
let odb = if let Some(ref wt) = work_tree {
Odb::with_work_tree(&objects_dir, wt)
.with_config_git_dir(git_dir.clone())
.with_shared_config_state(caches.clone(), environment.clone())
} else {
Odb::new(&objects_dir)
.with_config_git_dir(git_dir.clone())
.with_shared_config_state(caches.clone(), environment.clone())
};
let git_prefix = work_tree
.as_ref()
.and_then(|wt| compute_git_prefix(environment.as_ref(), wt));
let ref_store = caches.open_ref_store(&git_dir, environment.as_ref())?;
Ok(Self {
git_dir,
common_git_dir,
work_tree,
odb,
explicit_git_dir: false,
discovery_root: None,
work_tree_from_env: false,
discovery_via_gitfile: false,
config_session: Mutex::new(None),
caches,
ref_store,
environment,
git_prefix,
command_runner,
change_id_generator,
diagnostics: Arc::clone(&options.diagnostics),
network_trace: options.network_trace,
reference_unix_time: options.reference_unix_time,
test_assume_different_owner: options.test_assume_different_owner,
force_split_index: options.force_split_index,
thread_policy: options.thread_policy,
})
}
#[must_use]
pub fn thread_policy(&self) -> crate::parallel::ThreadPolicy {
self.thread_policy
}
#[must_use]
pub fn with_thread_policy(mut self, policy: crate::parallel::ThreadPolicy) -> Self {
self.thread_policy = policy;
self
}
#[must_use]
pub(crate) fn caches(&self) -> &Arc<RepoCaches> {
&self.caches
}
#[must_use]
pub(crate) fn promisor_hydrate_hook(&self) -> Option<crate::blame::PromisorHydrateHook> {
self.caches.promisor_hydrate_hook()
}
#[must_use]
pub fn environment(&self) -> &Environment {
&self.environment
}
#[must_use]
pub fn reference_unix_time(&self) -> i64 {
self.wall_clock_epoch()
}
#[must_use]
pub fn wall_clock_epoch(&self) -> i64 {
if let Some(ts) = self.reference_unix_time {
return ts;
}
if let Some(ts) = self.environment.git_now_date_override() {
return ts;
}
crate::git_date::tm::process_wall_clock_sec()
}
#[must_use]
pub fn identity_now(&self) -> time::OffsetDateTime {
crate::environment::offset_from_epoch_and_tz(
self.wall_clock_epoch(),
self.environment.tz.as_deref(),
)
}
#[must_use]
pub fn command_runner(&self) -> Arc<dyn CommandRunner> {
Arc::clone(&self.command_runner)
}
#[must_use]
pub fn change_id_generator(&self) -> Arc<dyn ChangeIdGenerator> {
Arc::clone(&self.change_id_generator)
}
#[must_use]
pub fn linked_worktree_discover_options(&self) -> RepositoryOptions {
let mut environment = (*self.environment).clone();
environment.git_dir = None;
environment.git_work_tree = None;
environment.git_index_file = None;
environment.git_prefix = None;
environment.git_object_directory = None;
RepositoryOptions {
environment,
command_runner: Arc::clone(&self.command_runner),
change_id_generator: Arc::clone(&self.change_id_generator),
diagnostics: Arc::clone(&self.diagnostics),
network_trace: self.network_trace,
test_assume_different_owner: self.test_assume_different_owner,
force_split_index: self.force_split_index,
reference_unix_time: self.reference_unix_time,
thread_policy: self.thread_policy,
work_tree: None,
validate_repository_format: true,
open_for_serving: false,
}
}
fn repo_cached_settings_from_config(
cfg: &ConfigSet,
environment: &Environment,
) -> RepoCachedSettings {
let use_replace_refs = cfg
.get_bool("core.useReplaceRefs")
.and_then(|r| r.ok())
.unwrap_or(true);
let replace_ref_base = environment
.git_replace_ref_base
.clone()
.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,
}
}
fn ensure_config_arc(&self) -> Result<Arc<ConfigSet>> {
self.config_arc_with_validation(true)
}
pub(crate) fn ensure_config_arc_fast(&self) -> Result<Arc<ConfigSet>> {
self.config_arc_with_validation(false)
}
fn config_arc_with_validation(&self, validate_stamps: bool) -> Result<Arc<ConfigSet>> {
if validate_stamps {
if let Ok(guard) = self.config_session.lock() {
if let Some(session) = guard.as_ref() {
let opts = self.config_load_options(true);
if session.validation.still_valid(
self.environment.as_ref(),
&self.git_dir,
&opts,
) {
return Ok(Arc::clone(&session.config));
}
}
}
} else if let Ok(guard) = self.config_session.lock() {
if let Some(session) = guard.as_ref() {
return Ok(Arc::clone(&session.config));
}
}
self.refresh_config_session()?;
self.config_session
.lock()
.map_err(|e| Error::Message(format!("config session lock poisoned: {e}")))?
.as_ref()
.map(|s| Arc::clone(&s.config))
.ok_or_else(|| Error::Message("config session missing after refresh".to_owned()))
}
fn refresh_config_session(&self) -> Result<()> {
crate::trace::trace_span!("grit.config.load");
let opts = self.config_load_options(true);
if let Ok(guard) = self.config_session.lock() {
if let Some(session) = guard.as_ref() {
if session
.validation
.still_valid(self.environment.as_ref(), &self.git_dir, &opts)
{
return Ok(());
}
let env = self.environment.as_ref();
let base_stamps = config_file_stamps(env, Some(&self.git_dir), &opts);
if session.validation.base_stamps.as_slice() != base_stamps.as_slice() {
self.caches.invalidate_config_file_derived();
}
}
}
self.caches.clear_session_config();
let env = self.environment.as_ref();
let cfg = self
.caches
.load_config_with_options(env, Some(&self.git_dir), &opts)?;
let key = ConfigCacheKey::new(Some(&self.git_dir), &opts);
let (env_fingerprint, base_stamps, extra_stamps) =
self.caches.config_entry_stamps(&key).unwrap_or_else(|| {
(
env.config_fingerprint().unwrap_or_default(),
config_file_stamps(env, Some(&self.git_dir), &opts),
Vec::new(),
)
});
let cached_settings =
Self::repo_cached_settings_from_config(cfg.as_ref(), self.environment.as_ref());
let replace_lookup_inactive = self.replace_lookup_inactive(cfg.as_ref());
self.caches
.set_session_config(key.clone(), Arc::clone(&cfg));
let session = ConfigSession {
config: Arc::clone(&cfg),
validation: ConfigSessionValidation {
key,
env_fingerprint,
base_stamps,
extra_stamps,
},
cached_settings,
replace_lookup_inactive,
};
*self
.config_session
.lock()
.map_err(|e| Error::Message(format!("config session lock poisoned: {e}")))? =
Some(session);
Ok(())
}
fn replace_lookup_inactive(&self, cfg: &ConfigSet) -> bool {
if self.environment.git_no_replace_objects {
return true;
}
if !cfg
.get_bool("core.useReplaceRefs")
.and_then(|r| r.ok())
.unwrap_or(true)
{
return true;
}
let replace_dir = self.common_git_dir.join("refs/replace");
!replace_dir_is_non_empty(&replace_dir)
}
#[must_use]
pub fn diagnostics(&self) -> DiagnosticsHandle {
Arc::clone(&self.diagnostics)
}
pub fn set_diagnostics(&mut self, sink: DiagnosticsHandle) {
self.caches.set_diagnostics(Arc::clone(&sink));
self.diagnostics = sink;
}
#[must_use]
pub fn network_trace_enabled(&self) -> bool {
self.network_trace
}
pub fn set_network_trace(&mut self, enabled: bool) {
self.network_trace = enabled;
}
pub(crate) fn warn(&self, warning: diagnostics::Warning) {
self.diagnostics.warn(warning);
}
pub fn config(&self) -> Result<Arc<ConfigSet>> {
self.ensure_config_arc()
}
pub fn reload_config(&self) -> Result<()> {
self.caches.invalidate_config_cache();
if let Ok(mut guard) = self.config_session.lock() {
*guard = None;
}
self.refresh_config_session()?;
Ok(())
}
pub(crate) fn install_config_snapshot(&self) {
let _ = self
.caches
.open_ref_store(&self.git_dir, self.environment.as_ref());
}
#[must_use]
pub fn common_git_dir(&self) -> &Path {
&self.common_git_dir
}
fn validate_repository_format_cached(&self) -> Result<()> {
let Some(config_path) = repository_config_path(&self.git_dir) else {
return Ok(());
};
let parsed = self.caches.repository_format(&self.git_dir, &config_path)?;
validate_repository_format_parsed(&parsed)
}
#[must_use]
pub fn refs(&self) -> &dyn crate::refs::store::RefStore {
self.ref_store.as_ref()
}
#[inline]
#[must_use]
pub fn objects(&self) -> crate::objects::Objects<'_> {
crate::objects::Objects::new(self)
}
#[inline]
#[must_use]
pub fn references(&self) -> crate::refs::References<'_> {
crate::refs::References::new(self)
}
#[inline]
#[must_use]
pub fn index_handle(&self) -> crate::index::IndexHandle<'_> {
crate::index::IndexHandle::new(self)
}
#[inline]
#[must_use]
pub fn config_handle(&self) -> crate::config::ConfigHandle<'_> {
crate::config::ConfigHandle::new(self)
}
#[inline]
#[must_use]
pub fn rev_walk(&self) -> crate::revwalk::RevWalk<'_> {
crate::revwalk::RevWalk::new(self)
}
#[inline]
#[must_use]
pub fn diff(&self) -> crate::diff::Diff<'_> {
crate::diff::Diff::new(self)
}
#[must_use]
pub fn remotes(&self) -> crate::repo_handles::RepositoryRemotes<'_> {
crate::repo_handles::RepositoryRemotes::new(self)
}
#[must_use]
pub fn worktrees(&self) -> crate::repo_handles::RepositoryWorktrees<'_> {
crate::repo_handles::RepositoryWorktrees::new(self)
}
#[must_use]
pub fn stash(&self) -> crate::repo_handles::RepositoryStash<'_> {
crate::repo_handles::RepositoryStash::new(self)
}
#[must_use]
pub fn notes(&self) -> crate::repo_handles::RepositoryNotes<'_> {
crate::repo_handles::RepositoryNotes::new(self)
}
#[must_use]
pub fn hooks(&self) -> crate::repo_handles::RepositoryHooks<'_> {
crate::repo_handles::RepositoryHooks::new(self)
}
pub fn fsck(
&self,
options: &crate::fsck::FsckOptions,
progress: &mut dyn crate::progress::ProgressSink,
cancel: &dyn crate::progress::Cancel,
) -> crate::error::Result<crate::fsck::FsckReport> {
crate::fsck::fsck(self, options, progress, cancel)
}
pub fn tree_sha256(
&self,
oid: crate::objects::ObjectId,
options: &crate::tree_sha256::TreeSha256Options,
) -> crate::error::Result<crate::tree_sha256::TreeSha256Report> {
crate::tree_sha256::tree_sha256(self, oid, options)
}
pub fn verify_tag(
&self,
oid: crate::objects::ObjectId,
options: &crate::tree_sha256::TagVerifyOptions,
) -> crate::error::Result<crate::tree_sha256::TagVerification> {
crate::tree_sha256::verify_tag(self, oid, options)
}
pub fn patch_id(
&self,
commit: &crate::objects::ObjectId,
mode: crate::patch_ids::PatchIdMode,
) -> crate::error::Result<Option<crate::objects::ObjectId>> {
crate::patch_ids::compute_patch_id(self.odb().hash_algo(), self.odb(), commit, mode)
}
#[must_use]
pub fn reflogs(&self) -> crate::repo_handles::RepositoryReflogs<'_> {
crate::repo_handles::RepositoryReflogs::new(self)
}
#[must_use]
pub fn bundles(&self) -> crate::repo_handles::RepositoryBundles<'_> {
crate::repo_handles::RepositoryBundles::new(self)
}
pub fn repack(
&self,
opts: &crate::maintenance::RepackOptions,
) -> crate::error::Result<crate::maintenance::RepackReport> {
crate::maintenance::repack(self, opts)
}
pub(crate) fn resolve_ref_name(&self, refname: &str) -> Result<crate::objects::ObjectId> {
self.ref_store.resolve(refname)
}
#[must_use]
pub fn with_ref_store(mut self, store: Arc<dyn crate::refs::store::RefStore>) -> Self {
let key = self
.git_dir
.canonicalize()
.unwrap_or_else(|_| self.git_dir.clone());
if let Ok(mut guard) = self.caches.ref_stores.lock() {
guard.insert(key, Arc::clone(&store));
}
self.ref_store = store;
self
}
#[must_use]
pub(crate) fn pathspec_precompose_enabled(&self) -> bool {
self.caches
.pathspec_precompose_enabled(self.environment.as_ref(), &self.git_dir)
}
#[must_use]
pub(crate) fn pathspec_match_context(&self) -> crate::pathspec::PathspecMatchContext {
crate::pathspec::pathspec_match_context_with_precompose(self.pathspec_precompose_enabled())
}
#[must_use]
pub fn paths_match_pathspecs(&self, path: &str, specs: &[String]) -> bool {
crate::pathspec::matches_pathspec_list_with_context(
path,
specs,
self.pathspec_match_context(),
)
}
#[must_use]
pub fn pathspec_cwd_prefix(&self) -> Option<&str> {
self.git_prefix.as_deref().filter(|p| !p.is_empty())
}
pub fn pathspec_argv_prefix<'a>(&'a self, pathspec: &str) -> Option<&'a str> {
if crate::pathspec::pathspec_has_top(pathspec) {
None
} else {
self.pathspec_cwd_prefix()
}
}
pub fn resolve_rev_walk_pathspecs(&self, paths: &[String]) -> Result<Vec<String>> {
if paths.is_empty() {
return Ok(Vec::new());
}
let prefix = self.pathspec_cwd_prefix();
let cwd_for_implicit = prefix.map(str::to_owned);
let mut resolved = Vec::with_capacity(paths.len());
if let Some(wt) = self.work_tree() {
for pathspec in paths {
let prefix = Self::pathspec_argv_prefix(self, pathspec);
let spec = crate::pathspec::resolve_pathspec_in_worktree_with_git_dir(
pathspec,
pathspec,
wt,
prefix,
Some(self.git_dir()),
)
.map_err(|e| Error::PathError(e.to_string()))?;
resolved.push(spec);
}
} else {
let root = self.git_dir();
for pathspec in paths {
let prefix = Self::pathspec_argv_prefix(self, pathspec);
resolved.push(crate::pathspec::resolve_pathspec(pathspec, root, prefix));
}
}
Ok(crate::pathspec::extend_pathspec_list_implicit_cwd(
&resolved,
cwd_for_implicit.as_deref(),
))
}
#[must_use]
pub fn git_dir(&self) -> &Path {
&self.git_dir
}
#[must_use]
pub fn work_tree(&self) -> Option<&Path> {
self.work_tree.as_deref()
}
#[must_use]
pub fn odb(&self) -> &Odb {
&self.odb
}
#[must_use]
pub fn discovery_info(&self) -> DiscoveryInfo {
DiscoveryInfo {
explicit_git_dir: self.explicit_git_dir,
discovery_root: self.discovery_root.clone(),
work_tree_from_env: self.work_tree_from_env,
discovery_via_gitfile: self.discovery_via_gitfile,
}
}
fn cached_settings(&self) -> RepoCachedSettings {
if let Ok(guard) = self.config_session.lock() {
if let Some(session) = guard.as_ref() {
return session.cached_settings.clone();
}
}
self.ensure_config_arc_fast()
.map(|cfg| {
Self::repo_cached_settings_from_config(cfg.as_ref(), self.environment.as_ref())
})
.unwrap_or_else(|_| {
Self::repo_cached_settings_from_config(&ConfigSet::new(), self.environment.as_ref())
})
}
pub fn init(path: &Path, options: &InitOptions) -> Result<Self> {
Self::init_with_outcome(path, options).map(|(repo, _)| repo)
}
pub fn init_with_outcome(path: &Path, options: &InitOptions) -> Result<(Self, InitOutcome)> {
if options.bare && options.separate_git_dir.is_some() {
return Err(Error::InitBareWithSeparateGitDir);
}
let env = options
.environment
.clone()
.unwrap_or_else(Environment::empty);
let git_dir = resolve_git_dir_for_init(path, options)?;
let reinitialized = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
let existing_format = hash_algo_from_git_dir(&git_dir)?;
let object_format = match (reinitialized, options.object_format) {
(true, None) => existing_format,
(true, Some(requested)) if requested != existing_format => {
return Err(Error::InitObjectFormatChange);
}
(true, Some(requested)) => requested,
(false, None) => HashAlgo::Sha1,
(false, Some(requested)) => requested,
};
let initial_branch = if reinitialized {
head_branch_name_for_outcome(&git_dir)?
} else {
resolve_initial_branch(&env, options.initial_branch.as_deref())?
};
let separate_layout = options.separate_git_dir.is_some()
|| (!options.bare
&& !options.bare_with_worktree_at_path
&& path.join(".git").is_file());
let repo = if options.bare_with_worktree_at_path {
init_bare_with_env_worktree(
path,
path,
&initial_branch,
options.template_dir.as_deref(),
options.ref_storage,
object_format,
)?
} else if separate_layout {
init_repository_separate_git_dir(
path,
&git_dir,
&initial_branch,
options.template_dir.as_deref(),
options.ref_storage,
object_format,
)?
} else {
init_repository_with_object_format(
path,
options.bare,
&initial_branch,
options.template_dir.as_deref(),
options.ref_storage,
object_format,
)?
};
Ok((
repo,
InitOutcome {
reinitialized,
initial_branch,
},
))
}
pub fn open(path: &Path, options: &RepositoryOptions) -> Result<Self> {
if options.open_for_serving {
return Self::open_for_serving_internal(path, options);
}
Self::open_internal(options, path)
}
fn open_with_work_tree(
options: &RepositoryOptions,
git_dir: &Path,
work_tree: Option<&Path>,
) -> Result<Self> {
let mut opts = options.clone();
opts.work_tree = work_tree.map(|p| p.to_path_buf());
Self::open_internal(&opts, git_dir)
}
fn open_internal(options: &RepositoryOptions, git_dir: &Path) -> Result<Self> {
crate::trace::trace_span!("grit.repository.open");
let work_tree = options.work_tree.as_deref();
let git_dir = git_dir
.canonicalize()
.map_err(|_| Error::NotARepository(git_dir.display().to_string()))?;
let repo = Self::from_canonical_git_dir(options, git_dir, work_tree)?;
repo.refresh_config_session()?;
if options.validate_repository_format {
repo.validate_repository_format_cached()?;
}
repo.install_config_snapshot();
warn_core_bare_worktree_conflict(options, repo.git_dir());
let _ = trace_repo_setup_if_requested(&repo);
Ok(repo)
}
fn open_for_serving_internal(path: &Path, options: &RepositoryOptions) -> Result<Self> {
let mut with_git_suffix = path.as_os_str().to_owned();
with_git_suffix.push(".git");
let candidates = [
(path.join(".git"), Some(path.to_path_buf())),
(path.to_path_buf(), None),
(PathBuf::from(with_git_suffix), None),
];
for (git_dir, work_tree) in candidates {
if !git_dir.join("HEAD").is_file() {
continue;
}
let mut opts = options.clone();
opts.work_tree = work_tree;
opts.open_for_serving = false;
return Self::open_internal(&opts, &git_dir);
}
Err(Error::NotARepository(path.display().to_string()))
}
fn open_with_odb_internal(
options: &RepositoryOptions,
git_dir: &Path,
builder: crate::odb::OdbBuilder,
) -> Result<Self> {
let work_tree = options.work_tree.as_deref();
let git_dir = git_dir
.canonicalize()
.map_err(|_| Error::NotARepository(git_dir.display().to_string()))?;
let environment = Arc::new(options.environment.clone());
let command_runner = Arc::clone(&options.command_runner);
let change_id_generator = Arc::clone(&options.change_id_generator);
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 common_git_dir = resolve_common_dir(&git_dir).unwrap_or_else(|| git_dir.clone());
let objects_dir = if git_dir.join("objects").exists() {
git_dir.join("objects")
} else {
common_git_dir.join("objects")
};
if !objects_dir.exists() {
return Err(Error::NotARepository(git_dir.display().to_string()));
}
if builder.objects_dir() != objects_dir.as_path() {
return Err(Error::PathError(format!(
"OdbBuilder objects dir '{}' does not match repository objects '{}'",
builder.objects_dir().display(),
objects_dir.display()
)));
}
let work_tree = match work_tree {
Some(p) => {
let cwd = environment.discovery_cwd();
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 caches = RepoCaches::with_diagnostics(
Arc::clone(&command_runner),
Arc::clone(&options.diagnostics),
);
let odb = builder
.build()
.with_config_git_dir(git_dir.clone())
.with_shared_config_state(caches.clone(), environment.clone())
.with_resolved_work_tree(work_tree.clone());
let git_prefix = work_tree
.as_ref()
.and_then(|wt| compute_git_prefix(environment.as_ref(), wt));
let ref_store = caches.open_ref_store(&git_dir, environment.as_ref())?;
let repo = Self {
git_dir,
common_git_dir,
work_tree,
odb,
explicit_git_dir: false,
discovery_root: None,
work_tree_from_env: false,
discovery_via_gitfile: false,
config_session: Mutex::new(None),
caches,
ref_store,
environment,
git_prefix,
command_runner,
change_id_generator,
diagnostics: Arc::clone(&options.diagnostics),
network_trace: options.network_trace,
reference_unix_time: options.reference_unix_time,
test_assume_different_owner: options.test_assume_different_owner,
force_split_index: options.force_split_index,
thread_policy: options.thread_policy,
};
repo.refresh_config_session()?;
if options.validate_repository_format {
repo.validate_repository_format_cached()?;
}
warn_core_bare_worktree_conflict(options, repo.git_dir());
let _ = trace_repo_setup_if_requested(&repo);
Ok(repo)
}
pub fn discover(start: Option<&Path>, options: &RepositoryOptions) -> Result<Self> {
let env = &options.environment;
let cwd = env.discovery_cwd();
if let Some(dir) = env.git_dir.as_deref() {
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.git_work_tree.as_deref().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
.git_work_tree
.as_deref()
.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_with_work_tree(options, &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;
return Ok(repo);
}
let (is_bare, core_wt) = read_core_bare_and_worktree(&git_dir)?;
let bare_worktree_conflict = is_bare && core_wt.is_some();
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_with_work_tree(options, &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;
let _ = bare_worktree_conflict;
return Ok(repo);
}
let env_work_tree = env.git_work_tree.as_deref().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
.git_work_tree
.as_deref()
.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(env);
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 ceiling_dirs.contains(&dir_buf) {
ceil_offset = dir_buf.len() as isize;
} else 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(options, current)? {
repo.environment = Arc::new(env.clone());
repo.refresh_config_session()?;
repo.validate_repository_format_cached()?;
repo.install_config_snapshot();
let cfg = repo.config()?;
repo.set_diagnostics(Arc::clone(&options.diagnostics));
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_from_config(cfg.as_ref());
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 =
options.test_assume_different_owner || env.test_assume_different_owner();
if assume_different {
repo.enforce_safe_directory()?;
} else {
ensure_valid_ownership(
env,
gitfile.as_deref(),
repo.work_tree.as_deref(),
&repo.git_dir,
)?;
}
if let Some(wt) = repo.work_tree.as_ref() {
repo.git_prefix = compute_git_prefix(env, wt);
}
let _ = trace_repo_setup_if_requested(&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(crate) fn index_path(&self) -> PathBuf {
self.git_dir.join("index")
}
pub(crate) fn index_path_for_env(&self) -> Result<PathBuf> {
if let Some(raw) = self.environment.git_index_file.as_deref() {
if !raw.is_empty() {
let p = PathBuf::from(raw);
return Ok(if p.is_absolute() {
p
} else {
self.environment.discovery_cwd().join(p)
});
}
}
Ok(self.index_path())
}
pub(crate) fn load_index(&self) -> Result<Index> {
let path = self.index_path_for_env()?;
self.load_index_at(&path)
}
pub(crate) fn load_index_at(&self, path: &std::path::Path) -> Result<Index> {
self.load_index_at_with_context(
path,
crate::index::IndexParseContext::from_policy(self.thread_policy()),
)
}
pub(crate) fn load_index_at_with_context(
&self,
path: &std::path::Path,
parse_ctx: crate::index::IndexParseContext,
) -> Result<Index> {
crate::trace::trace_span!("grit.index.read");
let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
if let Some(res) = cfg.as_ref().get_bool("index.sparse") {
res.map_err(|s| Error::Config(s.into()))?;
}
let mut idx = Index::load_expand_sparse_optional_with_context(
path,
&self.odb,
Some(cfg.as_ref()),
parse_ctx,
)?;
crate::split_index::resolve_split_index_if_needed(&mut idx, &self.git_dir, path)?;
if idx.source_mtime.is_none() {
idx.source_mtime = crate::index::index_file_mtime(path);
}
if let Some(ref wt) = self.work_tree {
crate::sparse_checkout::clear_skip_worktree_from_present_files(
&self.git_dir,
wt,
&mut idx,
Some(cfg.as_ref()),
);
}
Ok(idx)
}
pub(crate) fn write_index(&self, index: &mut Index) -> Result<()> {
self.write_index_with_post_index_change(index, false, false)
}
pub(crate) fn begin_index_update(&self) -> Result<crate::index::IndexLock> {
test_inject_index_write_fail()?;
let path = self.index_path();
let cfg = self.config()?;
crate::index::IndexLock::acquire(&path, Some(cfg.as_ref()))
}
pub(crate) fn commit_index_update(
&self,
lock: &mut crate::index::IndexLock,
index: &mut Index,
updated_workdir: bool,
) -> Result<()> {
self.flush_index_update(lock, index)?;
self.finish_index_update(lock, updated_workdir)
}
pub(crate) fn flush_index_update(
&self,
lock: &mut crate::index::IndexLock,
index: &mut Index,
) -> Result<()> {
test_inject_index_write_fail()?;
index.hash_algo = self.odb.hash_algo();
self.finalize_sparse_index_if_needed(index)?;
let path = self.index_path();
let prev_index_mtime = crate::index::index_file_mtime(&path);
let cfg = self.config()?;
if let Some(work_tree) = self.work_tree.as_deref() {
if !index.suppress_pre_write_index_housekeeping {
crate::untracked_cache::ensure_untracked_cache_shell(
index,
work_tree,
cfg.as_ref(),
);
if !index.skip_racy_smudge_on_write {
crate::diff::smudge_racily_clean_entries(
&self.odb,
&self.git_dir,
index,
work_tree,
prev_index_mtime,
Some(cfg.as_ref()),
Some(self.caches().filters()),
);
}
}
}
let skip_hash = crate::index::index_skip_hash_for_write(Some(cfg.as_ref()));
index.write_to_path_with_config_and_lock_mode(
&path,
skip_hash,
Some(cfg.as_ref()),
Some(lock),
false,
)
}
pub(crate) fn finish_index_update(
&self,
lock: &mut crate::index::IndexLock,
updated_workdir: bool,
) -> Result<()> {
lock.commit_replace_index()?;
let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
let _ = run_hook(self, "post-index-change", &[updated_workdir_arg, "0"], None);
Ok(())
}
pub fn write_largest_pack_bitmap(
&self,
options: &crate::pack_bitmap::PackBitmapWriteOptions,
now: std::time::SystemTime,
) -> std::result::Result<std::path::PathBuf, crate::pack_bitmap::PackBitmapWriteError> {
let path = self
.odb
.largest_pack_index_path()
.map_err(|e| crate::pack_bitmap::PackBitmapWriteError::Object(e.to_string()))?
.ok_or(crate::pack_bitmap::PackBitmapWriteError::InvalidPackPath(
"no pack index in object store".to_owned(),
))?;
crate::pack_bitmap::PackBitmapWriter::write(self, &path, options, now)
}
pub(crate) fn try_write_index(&self, index: &mut Index) -> Result<bool> {
self.try_write_index_at(&self.index_path(), index)
}
pub(crate) fn try_write_index_at(&self, path: &Path, index: &mut Index) -> Result<bool> {
match self.write_index_at(path, index) {
Ok(()) => Ok(true),
Err(e) if Self::opportunistic_index_write_skippable(&e) => Ok(false),
Err(e) => Err(e),
}
}
fn opportunistic_index_write_skippable(err: &crate::error::Error) -> bool {
use crate::error::Error;
match err {
Error::Io(e) => matches!(
e.kind(),
std::io::ErrorKind::AlreadyExists
| std::io::ErrorKind::PermissionDenied
| std::io::ErrorKind::ReadOnlyFilesystem
),
_ => false,
}
}
pub(crate) fn write_index_with_post_index_change(
&self,
index: &mut Index,
updated_workdir: bool,
updated_skipworktree: bool,
) -> Result<()> {
self.write_index_at_with_post_index_change(
&self.index_path(),
index,
updated_workdir,
updated_skipworktree,
)
}
pub(crate) fn write_index_at(&self, path: &std::path::Path, index: &mut Index) -> Result<()> {
self.write_index_at_with_post_index_change(path, index, false, false)
}
pub(crate) fn write_index_at_with_split(
&self,
path: &std::path::Path,
index: &mut Index,
split: WriteSplitIndexRequest,
) -> Result<()> {
self.write_index_at_split(path, index, split)
}
#[must_use]
pub(crate) fn split_index_would_force_write(&self, index: &Index) -> bool {
if index.split_index_base_oid().is_some() {
return false;
}
let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
matches!(
crate::split_index::split_index_config(cfg.as_ref()),
crate::split_index::SplitIndexConfig::Enabled
) || self.force_split_index
|| crate::split_index::git_test_split_index_env()
}
pub(crate) fn write_index_at_with_post_index_change(
&self,
path: &std::path::Path,
index: &mut Index,
updated_workdir: bool,
updated_skipworktree: bool,
) -> Result<()> {
self.write_index_at_split_with_post_index_change(
path,
index,
WriteSplitIndexRequest::default(),
updated_workdir,
updated_skipworktree,
)
}
pub(crate) fn write_index_at_split(
&self,
path: &std::path::Path,
index: &mut Index,
split: WriteSplitIndexRequest,
) -> Result<()> {
self.write_index_at_split_with_post_index_change(path, index, split, false, false)
}
pub(crate) fn write_index_at_split_with_post_index_change(
&self,
path: &std::path::Path,
index: &mut Index,
split: WriteSplitIndexRequest,
updated_workdir: bool,
updated_skipworktree: bool,
) -> Result<()> {
crate::trace::trace_span!("grit.index.write");
test_inject_index_write_fail()?;
index.hash_algo = self.odb.hash_algo();
self.finalize_sparse_index_if_needed(index)?;
let prev_index_mtime = crate::index::index_file_mtime(path);
let cfg = self.config()?;
if let Some(work_tree) = self.work_tree.as_deref() {
if !index.suppress_pre_write_index_housekeeping {
crate::untracked_cache::ensure_untracked_cache_shell(
index,
work_tree,
cfg.as_ref(),
);
if !index.skip_racy_smudge_on_write {
crate::diff::smudge_racily_clean_entries(
&self.odb,
&self.git_dir,
index,
work_tree,
prev_index_mtime,
Some(cfg.as_ref()),
Some(self.caches().filters()),
);
}
}
}
let skip_hash = crate::index::index_skip_hash_for_write(Some(cfg.as_ref()));
write_index_file_split(
path,
&self.git_dir,
index,
cfg.as_ref(),
split,
skip_hash,
self.thread_policy(),
)?;
let updated_workdir_arg = if updated_workdir { "1" } else { "0" };
let updated_skipworktree_arg = if updated_skipworktree { "1" } else { "0" };
let _ = run_hook(
self,
"post-index-change",
&[updated_workdir_arg, updated_skipworktree_arg],
None,
);
Ok(())
}
fn finalize_sparse_index_if_needed(&self, index: &mut Index) -> Result<()> {
let cfg = self.config().unwrap_or_else(|_| Arc::new(ConfigSet::new()));
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 {
let protect = crate::path_validate::ProtectOptions::from_config(cfg.as_ref());
index.try_collapse_sparse_directories(
&self.odb, &t, &patterns, cone, sparse_ix, protect,
)?;
} else {
index.sparse_directories = false;
}
Ok(())
}
#[must_use]
pub(crate) fn bloom_pathspec_cwd(&self) -> Option<String> {
let wt = self.work_tree.as_ref()?;
let cwd = self.environment.discovery_cwd();
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) = self.config() {
if let Some(Ok(bare)) = cfg.get_bool("core.bare") {
return bare;
}
}
self.work_tree.is_none()
}
pub(crate) fn object_id_after_replace(
&self,
oid: &crate::objects::ObjectId,
) -> crate::objects::ObjectId {
self.resolve_replace_redirect(oid).unwrap_or(*oid)
}
pub(crate) fn read_info_replaced(
&self,
oid: &crate::objects::ObjectId,
) -> Result<crate::objects::ObjectInfo> {
let effective = self.object_id_after_replace(oid);
self.odb.read_info(&effective)
}
pub(crate) fn read_replaced(
&self,
oid: &crate::objects::ObjectId,
) -> Result<crate::objects::Object> {
let effective = self.object_id_after_replace(oid);
self.odb.read(&effective)
}
fn resolve_replace_redirect(
&self,
oid: &crate::objects::ObjectId,
) -> Option<crate::objects::ObjectId> {
if self.environment.git_no_replace_objects {
return None;
}
if let Ok(guard) = self.config_session.lock() {
if guard.as_ref().is_some_and(|s| s.replace_lookup_inactive) {
return None;
}
}
let settings = self.cached_settings();
if !settings.use_replace_refs {
return None;
}
let replace_ref =
self.common_git_dir
.join(format!("{}{}", settings.replace_ref_base, oid.to_hex()));
if !replace_ref.is_file() {
return None;
}
let content = std::fs::read_to_string(&replace_ref).ok()?;
let hex = content.trim();
let replacement_oid = hex.parse::<crate::objects::ObjectId>().ok()?;
if self.odb.read_info(&replacement_oid).is_ok() {
Some(replacement_oid)
} else {
None
}
}
}
pub(crate) fn trace_repo_setup_if_requested(repo: &Repository) -> std::io::Result<()> {
let Some(path) = repo.environment.git_trace_setup.as_deref() 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 = repo.environment.discovery_cwd();
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 Some(raw) = repo.environment.git_dir.as_deref() {
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 Some(raw) = repo.environment.git_dir.as_deref() {
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 replace_dir_is_non_empty(replace_dir: &Path) -> bool {
let Ok(entries) = fs::read_dir(replace_dir) else {
return false;
};
entries.filter_map(|e| e.ok()).any(|e| {
let Ok(ft) = e.file_type() else {
return false;
};
ft.is_file()
})
}
fn resolve_common_dir(git_dir: &Path) -> Option<PathBuf> {
let commondir_file = git_dir.join("commondir");
if !commondir_file.is_file() {
return None;
}
let common_raw = fs::read_to_string(commondir_file).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) = crate::repo_caches::read_local_config_file(&path) else {
return false;
};
worktree_config_enabled_for_text(Some(content.as_str()))
}
pub fn worktree_config_enabled_for_text(content: Option<&str>) -> bool {
let Some(content) = content 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> {
let path = common_dir.join("config");
let content = crate::repo_caches::read_local_config_file(&path).ok()?;
early_config_ignore_repo_reason_for_text(Some(content.as_str()))
}
pub fn early_config_ignore_repo_reason_for_text(content: Option<&str>) -> Option<String> {
const GIT_REPO_VERSION_READ: u32 = 1;
let content = content?;
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
}
pub fn local_config_write_path(git_dir: &Path) -> PathBuf {
if let Some(path) = repository_config_path(git_dir) {
return path;
}
if let Some(common) = resolve_common_dir(git_dir) {
return common.join("config");
}
git_dir.join("config")
}
fn repository_config_path(git_dir: &Path) -> Option<PathBuf> {
repository_config_path_for_caches(git_dir)
}
pub(crate) fn repository_config_path_for_caches(git_dir: &Path) -> Option<PathBuf> {
let local = git_dir.join("config");
if local.is_file() {
return Some(local);
}
let commondir = git_dir.join("commondir");
if !commondir.is_file() {
return None;
}
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)
}
pub(crate) fn validate_repository_format_parsed(parsed: &RepositoryFormat) -> Result<()> {
if parsed.repo_version > 1 {
return Err(Error::UnsupportedRepositoryFormatVersion(
parsed.repo_version,
));
}
if let Some(raw) = parsed.ref_storage.as_deref() {
crate::ref_storage::RefStorageFormat::validate_config_extension(raw)?;
}
if let Some(msg) = parsed.format_error_message() {
return Err(Error::Message(msg));
}
Ok(())
}
fn validate_repository_format(git_dir: &Path) -> Result<()> {
let Some(config_path) = repository_config_path(git_dir) else {
return Ok(());
};
let content = crate::repo_caches::read_local_config_file(&config_path)?;
let parsed = parse_repository_format(&content, &config_path)?;
validate_repository_format_parsed(&parsed)
}
#[derive(Clone)]
pub(crate) struct RepositoryFormat {
pub(crate) repo_version: u32,
pub(crate) extensions: BTreeSet<String>,
pub(crate) ref_storage: Option<String>,
pub(crate) object_format: Option<String>,
}
pub(crate) fn read_repository_format_from_git_dir(git_dir: &Path) -> Result<RepositoryFormat> {
let Some(config_path) = repository_config_path(git_dir) else {
return Ok(RepositoryFormat {
repo_version: 0,
extensions: BTreeSet::new(),
ref_storage: None,
object_format: None,
});
};
let content = crate::repo_caches::read_local_config_file(&config_path)?;
parse_repository_format(&content, &config_path)
}
impl RepositoryFormat {
pub(crate) fn hash_algo(&self) -> crate::objects::HashAlgo {
self.object_format
.as_deref()
.and_then(crate::objects::HashAlgo::from_name)
.unwrap_or(crate::objects::HashAlgo::Sha1)
}
fn format_error_message(&self) -> Option<String> {
let mut v1_only_found: Vec<&str> = Vec::new();
let mut unknown_found: Vec<&str> = Vec::new();
for extension in &self.extensions {
match extension.as_str() {
"noop" | "preciousobjects" | "partialclone" | "worktreeconfig" => {}
"noop-v1"
| "objectformat"
| "compatobjectformat"
| "refstorage"
| "relativeworktrees"
| "submodulepathconfig" => {
if self.repo_version == 0 {
v1_only_found.push(extension);
}
}
_ => {
if self.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 Some(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 Some(msg);
}
None
}
}
pub(crate) fn parse_repository_format(
content: &str,
config_path: &Path,
) -> Result<RepositoryFormat> {
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;
let mut object_format: 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::Config(
format!("invalid config in {}", config_path.display()).into(),
));
};
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);
}
if key.eq_ignore_ascii_case("objectformat") {
object_format = value.map(str::to_owned);
}
if !key.is_empty() {
extensions.insert(key.to_ascii_lowercase());
}
}
}
Ok(RepositoryFormat {
repo_version,
extensions,
ref_storage,
object_format,
})
}
pub fn repository_format_warning(git_dir: &Path) -> Result<Option<String>> {
const GIT_REPO_VERSION_READ: u32 = 1;
let Some(config_path) = repository_config_path(git_dir) else {
return Ok(None);
};
let content = fs::read_to_string(&config_path).map_err(Error::Io)?;
let parsed = parse_repository_format(&content, &config_path)?;
if parsed.repo_version > GIT_REPO_VERSION_READ {
return Ok(Some(format!(
"Expected git repo version <= {GIT_REPO_VERSION_READ}, found {}",
parsed.repo_version
)));
}
Ok(parsed.format_error_message())
}
struct DiscoveredAt {
repo: Repository,
gitfile: Option<PathBuf>,
}
fn try_open_at(options: &RepositoryOptions, dir: &Path) -> Result<Option<DiscoveredAt>> {
let env = &options.environment;
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 opts = options.clone();
opts.validate_repository_format = false;
opts.work_tree = Some(dir.to_path_buf());
let mut repo = Repository::open_internal(&opts, &git_dir)?;
if resolve_common_dir(&git_dir).is_some() {
let cwd = env.discovery_cwd();
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.discovery_cwd().join(dir)
};
repo.discovery_root = Some(root.canonicalize().unwrap_or(root));
repo.discovery_via_gitfile = true;
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()
};
let mut skip_opts = options.clone();
skip_opts.validate_repository_format = false;
skip_opts.work_tree = Some(dir.to_path_buf());
match Repository::open_internal(&skip_opts, &open_path) {
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.discovery_cwd().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::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, env);
let repo = Repository::open_with_work_tree(options, dir, None)?;
return Ok(Some(DiscoveredAt {
repo,
gitfile: None,
}));
}
if dir.join("objects").is_dir() && dir.join("HEAD").is_file() {
maybe_trace_implicit_bare_repository(dir, env);
if !is_inside_dot_git(dir) {
if let Ok(cfg) = crate::config::ConfigSet::load(env, 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_with_work_tree(options, dir, None)?;
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, environment: &Environment) {
let Some(path) = environment
.git_trace2_perf
.as_deref()
.filter(|p| !p.is_empty())
else {
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, env: &Environment) -> Vec<String> {
let cfg = crate::config::ConfigSet::load(env, 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,
environment: &Environment,
) -> Result<()> {
let effective = safe_directory_effective_values(git_dir, environment);
let checked_s = checked.to_string_lossy().to_string();
let _ = environment.grit_debug_safe_dir;
if effective
.iter()
.any(|v| safe_directory_matches(v, &checked_s, &environment.discovery_cwd()))
{
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 ensure_valid_ownership(
environment: &Environment,
gitfile: Option<&Path>,
worktree: Option<&Path>,
gitdir: &Path,
) -> Result<()> {
const ROOT_UID: u32 = 0;
let sudo_euid = environment
.sudo_uid
.as_deref()
.and_then(|s| s.parse::<u32>().ok());
fn owned_by_effective_user(path: &Path, sudo_euid: Option<u32>) -> std::io::Result<bool> {
let st_uid = path_lstat_uid(path)?;
let mut euid = nix::unistd::geteuid().as_raw();
if euid == ROOT_UID {
if st_uid == ROOT_UID {
return Ok(true);
}
if let Some(sudo_uid) = sudo_euid {
euid = sudo_uid;
}
}
Ok(st_uid == euid)
}
let assume_different = environment.test_assume_different_owner();
if !assume_different {
let gitfile_ok = gitfile
.map(|p| owned_by_effective_user(p, sudo_euid))
.transpose()?
.unwrap_or(true);
let wt_ok = match worktree {
None => true,
Some(wt) => match owned_by_effective_user(wt, sudo_euid) {
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, sudo_euid)?;
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, environment)
}
#[cfg(not(unix))]
fn ensure_valid_ownership(
_environment: &Environment,
_gitfile: Option<&Path>,
_worktree: Option<&Path>,
_gitdir: &Path,
) -> Result<()> {
Ok(())
}
impl Repository {
pub fn enforce_safe_directory(&self) -> Result<()> {
let assume_different =
self.test_assume_different_owner || self.environment.test_assume_different_owner();
if !assume_different {
return Ok(());
}
if self.explicit_git_dir {
return Ok(());
}
let checked = if let Some(wt) = &self.work_tree {
let cwd = Some(self.environment.discovery_cwd());
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())
};
let _ = self.environment.grit_debug_safe_dir;
self.enforce_safe_directory_checked(&checked)
}
pub(crate) fn enforce_safe_directory_git_dir(&self) -> Result<()> {
let assume_different =
self.test_assume_different_owner || self.environment.test_assume_different_owner();
if !assume_different {
return Ok(());
}
let checked = self
.git_dir
.canonicalize()
.unwrap_or_else(|_| self.git_dir.clone());
let _ = self.environment.grit_debug_safe_dir;
self.enforce_safe_directory_checked(&checked)
}
fn enforce_safe_directory_checked(&self, checked: &Path) -> Result<()> {
ensure_safe_directory_allows(&self.git_dir, checked, self.environment.as_ref())
}
pub fn verify_safe_for_clone_source(&self) -> Result<()> {
let assume_different =
self.test_assume_different_owner || self.environment.test_assume_different_owner();
if assume_different {
self.enforce_safe_directory_git_dir()
} else {
#[cfg(unix)]
{
ensure_valid_ownership(self.environment.as_ref(), 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, cwd: &Path) -> bool {
if config_value == "*" {
return true;
}
if config_value == "." {
let cwd_s = normalize_fs_path(&cwd.to_string_lossy());
let checked_s = normalize_fs_path(checked);
return cwd_s == checked_s;
}
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(options: &RepositoryOptions, git_dir: &Path) {
if options
.environment
.git_work_tree
.as_deref()
.is_some_and(|s| !s.trim().is_empty())
{
return;
}
if let Ok((bare, wt)) = read_core_bare_and_worktree(git_dir) {
if bare && wt.is_some() {
options
.diagnostics
.warn(diagnostics::Warning::CoreBareWithWorktree);
}
}
}
fn read_core_bare_and_worktree_from_config(cfg: &ConfigSet) -> (bool, Option<String>) {
let bare = cfg
.get_bool("core.bare")
.and_then(|r| r.ok())
.unwrap_or(false);
let worktree = cfg.get("core.worktree").map(|s| s.to_owned());
(bare, worktree)
}
fn read_core_bare_and_worktree(git_dir: &Path) -> Result<(bool, Option<String>)> {
let cfg = ConfigSet::load(
&crate::environment::Environment::empty(),
Some(git_dir),
true,
)
.unwrap_or_default();
Ok(read_core_bare_and_worktree_from_config(&cfg))
}
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 joined = git_dir.join(p);
Ok(joined.canonicalize().unwrap_or(joined))
}
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()))
}
pub fn resolve_initial_branch(env: &Environment, explicit: Option<&str>) -> Result<String> {
let name = match explicit {
Some(name) => name.to_owned(),
None => ConfigSet::load(env, None, true)?
.get_last_entry("init.defaultBranch")
.and_then(|e| e.value.clone())
.filter(|s| !s.is_empty())
.unwrap_or_else(|| "main".to_owned()),
};
validate_branch_short_name(&name).map_err(|e| Error::InvalidRef(e.to_string()))?;
Ok(name)
}
fn resolve_git_dir_for_init(path: &Path, options: &InitOptions) -> Result<PathBuf> {
if options.bare_with_worktree_at_path || options.bare {
return Ok(path.to_path_buf());
}
if let Some(separate) = &options.separate_git_dir {
return Ok(separate.clone());
}
let dot_git = path.join(".git");
if dot_git.is_file() {
let content = fs::read_to_string(&dot_git)?;
return parse_gitfile(&content, path);
}
Ok(dot_git)
}
fn head_branch_name_for_outcome(git_dir: &Path) -> Result<String> {
if let Some(target) = crate::refs::read_symbolic_ref(git_dir, "HEAD")? {
if let Some(short) = target.strip_prefix("refs/heads/") {
return Ok(short.to_owned());
}
return Ok(target
.rsplit('/')
.next()
.unwrap_or(target.as_str())
.to_owned());
}
let head_text = fs::read_to_string(git_dir.join("HEAD"))?;
let oid = head_text.trim();
if crate::objects::ObjectId::from_hex(oid).is_ok() {
let end = oid.len().min(7);
return Ok(oid[..end].to_owned());
}
Ok("HEAD".to_owned())
}
fn hash_algo_from_git_dir(git_dir: &Path) -> Result<HashAlgo> {
let config_path = git_dir.join("config");
if !config_path.is_file() {
return Ok(HashAlgo::Sha1);
}
let content = fs::read_to_string(&config_path)?;
Ok(parse_repository_format(&content, &config_path)?.hash_algo())
}
fn ensure_git_directory_layout(
git_dir: &Path,
bare: bool,
ref_storage: crate::ref_storage::RefStorageFormat,
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.is_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, "")?;
}
}
Ok(())
}
fn write_fresh_git_directory(
git_dir: &Path,
bare: bool,
initial_branch: &str,
template_dir: Option<&Path>,
ref_storage: crate::ref_storage::RefStorageFormat,
object_format: HashAlgo,
skip_hooks_and_info: bool,
) -> Result<()> {
ensure_git_directory_layout(git_dir, bare, ref_storage, skip_hooks_and_info)?;
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.is_reftable() || object_format != HashAlgo::Sha1;
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");
if ref_storage.is_reftable() {
config_content.push_str("\trefStorage = reftable\n");
}
if object_format != HashAlgo::Sha1 {
config_content.push_str(&format!("\tobjectformat = {}\n", object_format.name()));
}
}
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()?;
}
}
}
apply_init_filesystem_config(
git_dir,
InitFilesystemConfigOptions::default(),
&Environment::empty(),
)?;
fs::write(
git_dir.join("description"),
"Unnamed repository; edit this file 'description' to name the repository.\n",
)?;
Ok(())
}
pub(crate) fn init_repository_separate_git_dir(
work_tree: &Path,
git_dir: &Path,
initial_branch: &str,
template_dir: Option<&Path>,
ref_storage: crate::ref_storage::RefStorageFormat,
object_format: HashAlgo,
) -> 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)?;
let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
if existing {
ensure_git_directory_layout(git_dir, false, ref_storage, skip_hooks_info)?;
} else {
write_fresh_git_directory(
git_dir,
false,
initial_branch,
template_dir,
ref_storage,
object_format,
skip_hooks_info,
)?;
}
let gitfile = work_tree.join(".git");
let abs_git_dir = fs::canonicalize(git_dir).unwrap_or_else(|_| git_dir.to_path_buf());
let abs_git_dir = abs_git_dir.to_string_lossy().replace('\\', "/");
fs::write(gitfile, format!("gitdir: {abs_git_dir}\n"))?;
Repository::open(
git_dir,
&RepositoryOptions::empty().with_work_tree(work_tree),
)
}
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(crate) fn init_repository(
path: &Path,
bare: bool,
initial_branch: &str,
template_dir: Option<&Path>,
ref_storage: crate::ref_storage::RefStorageFormat,
) -> Result<Repository> {
init_repository_with_object_format(
path,
bare,
initial_branch,
template_dir,
ref_storage,
HashAlgo::Sha1,
)
}
pub(crate) fn init_repository_with_object_format(
path: &Path,
bare: bool,
initial_branch: &str,
template_dir: Option<&Path>,
ref_storage: crate::ref_storage::RefStorageFormat,
object_format: HashAlgo,
) -> 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)?;
let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
if existing {
ensure_git_directory_layout(&git_dir, bare, ref_storage, skip_hooks_info)?;
} else {
write_fresh_git_directory(
&git_dir,
bare,
initial_branch,
template_dir,
ref_storage,
object_format,
skip_hooks_info,
)?;
}
if bare {
Repository::open(&git_dir, &RepositoryOptions::empty())
} else {
Repository::open(
&git_dir,
&RepositoryOptions::empty().with_work_tree(path.to_path_buf()),
)
}
}
pub(crate) fn init_bare_with_env_worktree(
git_dir: &Path,
work_tree: &Path,
initial_branch: &str,
template_dir: Option<&Path>,
ref_storage: crate::ref_storage::RefStorageFormat,
object_format: HashAlgo,
) -> Result<Repository> {
fs::create_dir_all(git_dir)?;
fs::create_dir_all(work_tree)?;
let existing = git_dir.join("config").is_file() && git_dir.join("HEAD").is_file();
if existing {
ensure_git_directory_layout(git_dir, true, ref_storage, false)?;
} else {
write_fresh_git_directory(
git_dir,
true,
initial_branch,
template_dir,
ref_storage,
object_format,
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,
&RepositoryOptions::empty().with_work_tree(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(env: &Environment) -> (Vec<PathBuf>, bool) {
let Some(raw) = env.git_ceiling_directories.as_deref() else {
return (Vec::new(), false);
};
if raw.is_empty() {
return (Vec::new(), false);
}
let (no_resolve, effective) = match raw.strip_prefix(':') {
Some(rest) => (true, rest),
None => (false, raw),
};
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(())
}
pub mod caches {
pub use crate::repo_caches::*;
}
mod index_write_fail_inject {
use std::cell::RefCell;
thread_local! {
pub static INJECT: RefCell<bool> = const { RefCell::new(false) };
pub static INJECT_REMAINING: RefCell<Option<usize>> = const { RefCell::new(None) };
}
}
#[doc(hidden)]
pub fn set_test_inject_index_write_fail(enabled: bool) {
index_write_fail_inject::INJECT.with(|c| *c.borrow_mut() = enabled);
if !enabled {
index_write_fail_inject::INJECT_REMAINING.with(|c| *c.borrow_mut() = None);
}
}
#[doc(hidden)]
pub fn set_test_inject_index_write_fail_remaining(remaining: usize) {
index_write_fail_inject::INJECT.with(|c| *c.borrow_mut() = true);
index_write_fail_inject::INJECT_REMAINING.with(|c| *c.borrow_mut() = Some(remaining));
}
fn test_inject_index_write_fail() -> Result<()> {
index_write_fail_inject::INJECT.with(|enabled| {
if !*enabled.borrow() {
return Ok(());
}
index_write_fail_inject::INJECT_REMAINING.with(|remaining| {
if let Some(left) = remaining.borrow_mut().as_mut() {
if *left == 0 {
return Ok(());
}
*left -= 1;
}
Err(Error::Io(std::io::Error::other(
"injected index write failure",
)))
})
})
}
#[cfg(test)]
#[path = "repo_line_coverage_tests.rs"]
mod line_coverage_tests;