use gix_config::file::Metadata;
use gix_error::{ErrorExt, ResultExt, bail, message, not_found, validation};
use gix_object::bstr::ByteSlice;
use gix_parallel::OwnShared;
use gix_path::RelativePath;
use std::path::Path;
use std::{
borrow::Cow,
collections::{BTreeMap, btree_map::Entry},
ffi::OsStr,
path::PathBuf,
};
use super::Options;
use crate::{
Error, Result, ThreadSafeRepository,
bstr::BString,
config,
config::{
cache::interpolate_context,
tree::{Core, Key, Safe, gitoxide},
},
open::Permissions,
};
fn not_a_repository(source: Error, path: PathBuf) -> Error {
source.and_raise(message!("\"{}\" does not appear to be a git repository", path.display()).not_found())
}
#[derive(Default, Clone)]
pub(crate) struct EnvironmentOverrides {
worktree_dir: Option<PathBuf>,
git_dir: Option<PathBuf>,
}
impl EnvironmentOverrides {
fn from_env() -> std::result::Result<Self, gix_sec::permission::Error<std::path::PathBuf>> {
let mut worktree_dir = None;
if let Some(path) = std::env::var_os(Core::WORKTREE.the_environment_override()) {
worktree_dir = PathBuf::from(path).into();
}
let mut git_dir = None;
if let Some(path) = std::env::var_os("GIT_DIR") {
git_dir = PathBuf::from(path).into();
}
Ok(EnvironmentOverrides { worktree_dir, git_dir })
}
}
impl ThreadSafeRepository {
pub fn open(path: impl Into<PathBuf>) -> Result<Self> {
Self::open_opts(path, Options::default())
}
pub fn open_opts(path: impl Into<PathBuf>, mut options: Options) -> Result<Self> {
let _span = gix_trace::coarse!("ThreadSafeRepository::open()");
let (path, kind) = {
let path = path.into();
let looks_like_dot_git_dir = path.ends_with(gix_discover::DOT_GIT_DIR);
let maybe_git_repo_path = if !options.open_path_as_is && !looks_like_dot_git_dir {
Some(path.join(gix_discover::DOT_GIT_DIR))
} else {
None
};
match maybe_git_repo_path {
Some(candidate) => match gix_discover::is_git(&candidate) {
Ok(kind) => (candidate, kind),
Err(_) => match gix_discover::is_git(&path) {
Ok(kind) => (path, kind),
Err(err) => return Err(not_a_repository(err, path)),
},
},
None => match gix_discover::is_git(&path) {
Ok(kind) => (path, kind),
Err(err) => {
return Err(not_a_repository(err, path));
}
},
}
};
let cwd = gix_fs::current_dir(false).or_error()?;
let candidate_trust = options
.git_dir_trust
.is_none()
.then(|| gix_sec::Trust::from_path_ownership(&path))
.transpose()
.or_error()?;
let (git_dir, worktree_dir) = gix_discover::repository::Path::from_dot_git_dir(path.clone(), kind, &cwd)
.expect("we have sanitized path with is_git()")
.into_repository_and_work_tree_directories();
if let Some(candidate_trust) = candidate_trust {
options.git_dir_trust = Some(gix_discover::repository::trust(
&git_dir,
worktree_dir.as_deref(),
&cwd,
(&path, Some(candidate_trust)),
)?);
}
options.current_dir = Some(cwd);
ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options, None)
}
#[doc(alias = "open_from_env", alias = "git2")]
pub fn open_with_environment_overrides(
fallback_directory: impl Into<PathBuf>,
trust_map: gix_sec::trust::Mapping<Options>,
) -> Result<Self> {
let _span = gix_trace::coarse!("ThreadSafeRepository::open_with_environment_overrides()");
let overrides = EnvironmentOverrides::from_env().or_error()?;
let (path, path_kind): (PathBuf, _) = match overrides.git_dir {
Some(git_dir) => gix_discover::is_git(&git_dir)
.map_err(|err| not_a_repository(err, git_dir.clone()))
.map(|kind| (git_dir, kind))?,
None => {
let fallback_directory = fallback_directory.into();
gix_discover::is_git(&fallback_directory)
.map_err(|err| not_a_repository(err, fallback_directory.clone()))
.map(|kind| (fallback_directory, kind))?
}
};
let cwd = gix_fs::current_dir(false).or_error()?;
let candidate_trust = gix_sec::Trust::from_path_ownership(&path).or_error()?;
let (git_dir, worktree_dir) = gix_discover::repository::Path::from_dot_git_dir(path.clone(), path_kind, &cwd)
.expect("we have sanitized path with is_git()")
.into_repository_and_work_tree_directories();
let worktree_dir = worktree_dir.or(overrides.worktree_dir);
let git_dir_trust =
gix_discover::repository::trust(&git_dir, worktree_dir.as_deref(), &cwd, (&path, Some(candidate_trust)))?;
let mut options = trust_map.into_value_by_level(git_dir_trust);
options.git_dir_trust = git_dir_trust.into();
options.current_dir = Some(cwd);
ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options, None)
}
pub(crate) fn open_from_paths(
mut git_dir: PathBuf,
mut worktree_dir: Option<PathBuf>,
mut options: Options,
known_common_dir: Option<PathBuf>,
) -> Result<Self> {
let _span = gix_trace::detail!("open_from_paths()");
options.open_path_as_is = false;
let Options {
ref mut git_dir_trust,
object_store_slots,
filter_config_section,
lossy_config,
lenient_config,
bail_if_untrusted,
open_path_as_is: _,
permissions:
Permissions {
ref env,
config,
attributes,
},
ref api_config_overrides,
ref cli_config_overrides,
ref git_installation_config_path,
ref system_config_path,
use_repository_local_environment,
ref mut current_dir,
} = options;
let git_dir_trust = git_dir_trust.as_mut().expect("trust must be determined by now");
let mut common_dir = match known_common_dir {
Some(common_dir) => Some(common_dir),
None => gix_discover::path::from_plain_file(git_dir.join("commondir").as_ref())
.transpose()
.or_error()?
.map(|cd| git_dir.join(cd)),
};
let repo_config = config::cache::StageOne::new(
common_dir.as_deref().unwrap_or(&git_dir),
git_dir.as_ref(),
*git_dir_trust,
lossy_config,
lenient_config,
)
.or_raise(|| message("Repository configuration could not be loaded"))?;
if repo_config.precompose_unicode {
git_dir = gix_utils::str::precompose_path(git_dir.into()).into_owned();
if let Some(common_dir) = common_dir.as_mut()
&& let Cow::Owned(precomposed) = gix_utils::str::precompose_path((&*common_dir).into())
{
*common_dir = precomposed;
}
if let Some(worktree_dir) = worktree_dir.as_mut()
&& let Cow::Owned(precomposed) = gix_utils::str::precompose_path((&*worktree_dir).into())
{
*worktree_dir = precomposed;
}
}
let common_dir_ref = common_dir.as_deref().unwrap_or(&git_dir);
let current_dir = {
let current_dir_ref = current_dir.as_mut().expect("BUG: current_dir must be set by caller");
if repo_config.precompose_unicode
&& let Cow::Owned(precomposed) = gix_utils::str::precompose_path((&*current_dir_ref).into())
{
*current_dir_ref = precomposed;
}
current_dir_ref.as_path()
};
let mut refs = {
let reflog = repo_config.reflog.unwrap_or(gix_ref::store::WriteReflog::Disable);
let object_hash = repo_config.object_hash;
let ref_store_init_opts = gix_ref::store::init::Options {
write_reflog: reflog,
precompose_unicode: repo_config.precompose_unicode,
prohibit_windows_device_names: repo_config.protect_windows,
};
match &common_dir {
Some(common_dir) => crate::RefStore::for_linked_worktree_opts(
git_dir.to_owned(),
common_dir.into(),
object_hash,
ref_store_init_opts,
),
None => crate::RefStore::at_opts(git_dir.to_owned(), object_hash, ref_store_init_opts),
}
};
let head = refs.find("HEAD").ok();
let git_install_dir = crate::path::install_dir().ok();
let home = gix_path::env::home_dir().and_then(|home| env.home.check_opt(home));
let mut filter_config_section = filter_config_section.unwrap_or(config::section::is_trusted);
let mut config = config::Cache::from_stage_one(
repo_config,
common_dir_ref,
head.as_ref().and_then(|head| head.target.try_name()),
filter_config_section,
git_install_dir.as_deref(),
home.as_deref(),
git_installation_config_path.as_deref(),
system_config_path.as_deref(),
*env,
attributes,
config,
lenient_config,
api_config_overrides,
cli_config_overrides,
use_repository_local_environment,
)
.or_raise(|| message("Repository configuration could not be loaded"))?;
let configured_worktree = config
.resolved
.raw_value_with_section_filter(Core::WORKTREE, |section| {
is_eligible_worktree_config_section(section, &git_dir, current_dir, &mut filter_config_section)
})
.ok()
.map(|(value, section)| (value, section.meta().source));
let worktree_from_environment = configured_worktree
.as_ref()
.is_some_and(|(_, source)| *source == gix_config::Source::EnvOverride);
let may_use_configured_worktree = config.is_bare == Some(false) || worktree_from_environment;
if let Some((worktree, source)) = configured_worktree.filter(|_| may_use_configured_worktree) {
if worktree.is_empty() {
return Err(not_found("path is missing").and_raise(
validation("The path at the 'core.worktree' configuration could not be interpolated")
.with_input(worktree),
));
}
let worktree = gix_path::from_bstr(worktree.as_bstr())?.into_owned();
let worktree = match source {
gix_config::Source::Env
| gix_config::Source::Cli
| gix_config::Source::Api
| gix_config::Source::EnvOverride => worktree,
_ => worktree_dir_from_repository_config(&git_dir, worktree, current_dir),
};
worktree_dir = if worktree_from_environment {
Some(gix_path::normalize_saturating(worktree.into(), current_dir)?.into_owned())
} else {
gix_path::normalize(worktree.into(), current_dir).map(Cow::into_owned)
};
#[allow(unused_variables, reason = "Used when tracing is enabled at compile time.")]
if let Some(worktree_path) = worktree_dir.as_deref().filter(|wtd| !wtd.is_dir()) {
gix_trace::warn!(
"The configured worktree path \"{}\" is not a directory or doesn't exist - `core.worktree` may be misleading",
worktree_path.display()
);
}
if worktree_from_environment {
config.is_bare = Some(false);
}
} else if !config.lenient_config
&& config.is_bare == Some(false)
&& config
.resolved
.boolean_filter(Core::WORKTREE, |section| {
is_eligible_worktree_config_section(section, &git_dir, current_dir, &mut filter_config_section)
})
.or_raise(|| config::key::error(&Core::WORKTREE, "Invalid configuration value"))?
.is_some()
{
bail!(config::key::error(&Core::WORKTREE, "Invalid configuration value"));
}
if worktree_dir.is_none()
&& config.is_bare == Some(false)
&& refs.git_dir().file_name() == Some(OsStr::new(gix_discover::DOT_GIT_DIR))
{
worktree_dir = Some(git_dir.parent().expect("parent is always available").to_owned());
}
let is_linked_worktree = refs.git_dir().parent().and_then(Path::file_name) == Some("worktrees".as_ref());
if config.is_bare == Some(true) && !is_linked_worktree {
worktree_dir = None;
}
if *git_dir_trust != gix_sec::Trust::Full
|| worktree_dir
.as_deref()
.is_some_and(|wd| !gix_sec::identity::is_path_owned_by_current_user(wd).unwrap_or(false))
{
let safe_dirs: Vec<BString> = config
.resolved
.strings_filter(Safe::DIRECTORY, &mut Safe::directory_filter)
.unwrap_or_default()
.into_iter()
.collect();
let test_dir = worktree_dir.as_deref().unwrap_or(git_dir.as_path());
let res = check_safe_directories(
test_dir,
git_install_dir.as_deref(),
current_dir,
home.as_deref(),
&safe_dirs,
);
if res.is_ok() {
*git_dir_trust = gix_sec::Trust::Full;
} else if bail_if_untrusted {
res?;
} else {
*git_dir_trust = gix_sec::Trust::Reduced;
}
let Ok(mut resolved) = gix_parallel::OwnShared::try_unwrap(config.resolved) else {
unreachable!("Shared ownership was just established, with one reference")
};
let section_ids: Vec<_> = resolved.section_ids().collect();
let mut is_valid_by_path = BTreeMap::new();
for id in section_ids {
let Some(mut section) = resolved.section_mut_by_id(id) else {
continue;
};
let section_trusted_by_default = Safe::directory_filter(section.meta());
if section_trusted_by_default || section.meta().trust == gix_sec::Trust::Full {
continue;
}
let Some(meta_path) = section.meta().path.as_deref() else {
continue;
};
match is_valid_by_path.entry(meta_path.to_owned()) {
Entry::Occupied(entry) => {
if *entry.get() {
section.set_trust(gix_sec::Trust::Full);
} else {
continue;
}
}
Entry::Vacant(entry) => {
let config_file_is_safe = (meta_path.strip_prefix(test_dir).is_ok()
&& *git_dir_trust == gix_sec::Trust::Full)
|| check_safe_directories(
meta_path,
git_install_dir.as_deref(),
current_dir,
home.as_deref(),
&safe_dirs,
)
.is_ok();
entry.insert(config_file_is_safe);
if config_file_is_safe {
section.set_trust(gix_sec::Trust::Full);
}
}
}
}
config.resolved = resolved.into();
}
let index_path = match config
.resolved
.string_filter(gitoxide::Core::INDEX_FILE, &mut filter_config_section)
{
Some(value) => {
gitoxide::Core::INDEX_FILE.validate(value.as_bstr())?;
gix_path::from_bstr(value)?.into_owned()
}
None => git_dir.join("index"),
};
refs.write_reflog = config::cache::util::reflog_or_default(config.reflog, worktree_dir.is_some());
refs.namespace.clone_from(&config.refs_namespace);
let prefix = replacement_objects_refs_prefix(&config.resolved, lenient_config, filter_config_section)?;
if *git_dir_trust == gix_sec::Trust::Reduced && config.alloc_limit_bytes.is_none() {
let alloc_limit_if_reduced_trust =
match gitoxide::Objects::ALLOC_LIMIT_IF_REDUCED_TRUST.try_into_usize(config.resolved.integer_filter(
gitoxide::Objects::ALLOC_LIMIT_IF_REDUCED_TRUST,
&mut filter_config_section,
)) {
Ok(Some(value)) => value,
Ok(None) => gitoxide::Objects::ALLOC_LIMIT_IF_REDUCED_TRUST_DEFAULT,
Err(_) if config.lenient_config => gitoxide::Objects::ALLOC_LIMIT_IF_REDUCED_TRUST_DEFAULT,
Err(err) => return Err(err),
};
if alloc_limit_if_reduced_trust != 0 {
config.alloc_limit_bytes = Some(alloc_limit_if_reduced_trust);
gix_trace::info!(
concat!(
"Applied a default allocation limit of {alloc_limit_bytes} ",
"bytes while opening reduced-trust repository \"{git_dir}\". ",
"Set `gitoxide.objects.allocLimitIfReducedTrust=0` to disable this fallback",
),
alloc_limit_bytes = alloc_limit_if_reduced_trust,
git_dir = git_dir.display(),
);
}
}
let replacements = match prefix {
Some(prefix) => {
let prefix: &RelativePath = prefix.as_bstr().try_into()?;
Some(prefix).and_then(|prefix| {
let _span = gix_trace::detail!("find replacement objects");
let platform = refs.iter().ok()?;
let iter = platform.prefixed(prefix).ok()?;
let replacements = iter
.filter_map(std::result::Result::ok)
.filter_map(|r: gix_ref::Reference| {
let target = r.target.try_id()?.to_owned();
let source =
gix_hash::ObjectId::from_hex(r.name.as_bstr().strip_prefix(prefix.as_ref())?).ok()?;
Some((source, target))
})
.collect::<Vec<_>>();
Some(replacements)
})
}
None => None,
};
let replacements = replacements.unwrap_or_default();
Ok(ThreadSafeRepository {
objects: OwnShared::new(
gix_odb::Store::at_opts(
common_dir_ref.join("objects"),
config.object_hash,
&mut replacements.into_iter(),
gix_odb::store::init::Options {
slots: object_store_slots,
use_multi_pack_index: config.use_multi_pack_index,
alloc_limit_bytes: config.alloc_limit_bytes,
loose_compression: config.loose_compression,
current_dir: current_dir.to_owned().into(),
},
)
.or_error()?,
),
common_dir,
refs,
work_tree: worktree_dir,
index_path,
config,
linked_worktree_options: options,
#[cfg(feature = "index")]
index: gix_fs::SharedFileSnapshotMut::new().into(),
shallow_commits: gix_fs::SharedFileSnapshotMut::new().into(),
#[cfg(feature = "attributes")]
modules: gix_fs::SharedFileSnapshotMut::new().into(),
})
}
}
fn is_eligible_worktree_config_section(
section: &Metadata,
git_dir: &Path,
current_dir: &Path,
filter_config_section: &mut fn(&Metadata) -> bool,
) -> bool {
if !filter_config_section(section) {
return false;
}
if section.source == gix_config::Source::EnvOverride {
return true;
}
section
.path
.as_deref()
.and_then(|path| gix_path::normalize(path.into(), current_dir))
.is_some_and(|config_path| config_path.starts_with(git_dir))
}
fn worktree_dir_from_repository_config(git_dir: &Path, wt_path: PathBuf, current_dir: &Path) -> PathBuf {
fn realpath(path: &Path, current_dir: &Path) -> Option<PathBuf> {
gix_path::realpath_opts(path, current_dir, crate::path::realpath::MAX_SYMLINKS).ok()
}
if wt_path.is_absolute() {
return wt_path;
}
let logical_git_dir = gix_path::normalize(
Cow::Owned(if git_dir.is_relative() {
current_dir.join(git_dir)
} else {
git_dir.to_owned()
}),
current_dir,
)
.map(Cow::into_owned);
let symlink_preserving = git_dir.join(&wt_path);
let real_git_dir = match (realpath(git_dir, current_dir), logical_git_dir) {
(Some(real_git_dir), Some(logical_git_dir)) if real_git_dir != logical_git_dir => real_git_dir,
_ => return symlink_preserving,
};
let resolved = real_git_dir.join(&wt_path);
let denotes_same_directory = gix_path::normalize(Cow::Borrowed(symlink_preserving.as_path()), current_dir)
.and_then(|normalized| realpath(&normalized, current_dir))
.zip(realpath(&resolved, current_dir))
.is_some_and(|(symlink_preserving, resolved)| symlink_preserving == resolved);
if denotes_same_directory {
symlink_preserving
} else {
resolved
}
}
fn replacement_objects_refs_prefix(
config: &gix_config::File,
lenient: bool,
mut filter_config_section: fn(&gix_config::file::Metadata) -> bool,
) -> Result<Option<BString>> {
let is_disabled = config::shared::is_replace_refs_enabled(config, lenient, filter_config_section)?.unwrap_or(true);
if is_disabled {
return Ok(None);
}
let ref_base = {
let key = "gitoxide.objects.replaceRefBase";
debug_assert_eq!(gitoxide::Objects::REPLACE_REF_BASE.logical_name(), key);
config
.string_filter(key, &mut filter_config_section)
.unwrap_or_else(|| gitoxide::Objects::REPLACE_REF_BASE.default_value_or_panic().into())
};
Ok(Some(ref_base))
}
fn check_safe_directories(
path_to_test: &std::path::Path,
git_install_dir: Option<&std::path::Path>,
current_dir: &std::path::Path,
home: Option<&std::path::Path>,
safe_dirs: &[BString],
) -> Result<()> {
let mut is_safe = false;
let realpath_or_original = |path: &std::path::Path| {
std::fs::canonicalize(path)
.or_else(|_| gix_path::realpath_opts(path, current_dir, gix_path::realpath::MAX_SYMLINKS))
.unwrap_or_else(|_| path.to_owned())
};
let path_to_test = realpath_or_original(path_to_test);
for safe_dir in safe_dirs {
let safe_dir = safe_dir.as_bstr();
if safe_dir == "*" {
is_safe = true;
continue;
}
if safe_dir.is_empty() {
is_safe = false;
continue;
}
if !is_safe {
let safe_dir =
match gix_config::Path::from(safe_dir).interpolate(interpolate_context(git_install_dir, home)) {
Ok(path) => path,
Err(_) => gix_path::from_bstr(safe_dir)?.into_owned(),
};
if !safe_dir.is_absolute() {
gix_trace::warn!(
"safe.directory \"{safe_dir}\" not absolute",
safe_dir = safe_dir.display()
);
continue;
}
if safe_dir.ends_with("*") {
let safe_dir = safe_dir.parent().expect("* is last component");
if path_to_test.strip_prefix(realpath_or_original(safe_dir)).is_ok() {
is_safe = true;
}
} else if realpath_or_original(&safe_dir) == path_to_test {
is_safe = true;
}
}
}
if is_safe {
Ok(())
} else {
Err(message!(
"The git directory at \"{}\" is considered unsafe as it's not owned by the current user.",
path_to_test.display()
)
.permission_denied_error())
}
}