#![allow(clippy::result_large_err)]
use std::{borrow::Cow, path::PathBuf};
use git_features::threading::OwnShared;
use super::{Error, Options};
use crate::{
config,
config::{
cache::{interpolate_context, util::ApplyLeniency},
tree::{gitoxide, Core, Key, Safe},
},
permission, Permissions, ThreadSafeRepository,
};
#[derive(Default, Clone)]
pub(crate) struct EnvironmentOverrides {
worktree_dir: Option<PathBuf>,
git_dir: Option<PathBuf>,
}
impl EnvironmentOverrides {
fn from_env() -> Result<Self, permission::env_var::resource::Error> {
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, Error> {
Self::open_opts(path, Options::default())
}
pub fn open_opts(path: impl Into<PathBuf>, mut options: Options) -> Result<Self, Error> {
let (path, kind) = {
let path = path.into();
let looks_like_git_dir =
path.ends_with(git_discover::DOT_GIT_DIR) || path.extension() == Some(std::ffi::OsStr::new("git"));
let candidate = if !options.open_path_as_is && !looks_like_git_dir {
Cow::Owned(path.join(git_discover::DOT_GIT_DIR))
} else {
Cow::Borrowed(&path)
};
match git_discover::is_git(candidate.as_ref()) {
Ok(kind) => (candidate.into_owned(), kind),
Err(err) => {
if options.open_path_as_is || matches!(candidate, Cow::Borrowed(_)) {
return Err(Error::NotARepository {
source: err,
path: candidate.into_owned(),
});
}
match git_discover::is_git(&path) {
Ok(kind) => (path, kind),
Err(err) => return Err(Error::NotARepository { source: err, path }),
}
}
}
};
let cwd = std::env::current_dir()?;
let (git_dir, worktree_dir) = git_discover::repository::Path::from_dot_git_dir(path, kind, &cwd)
.expect("we have sanitized path with is_git()")
.into_repository_and_work_tree_directories();
if options.git_dir_trust.is_none() {
options.git_dir_trust = git_sec::Trust::from_path_ownership(&git_dir)?.into();
}
options.current_dir = Some(cwd);
ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options)
}
pub fn open_with_environment_overrides(
fallback_directory: impl Into<PathBuf>,
trust_map: git_sec::trust::Mapping<Options>,
) -> Result<Self, Error> {
let overrides = EnvironmentOverrides::from_env()?;
let (path, path_kind): (PathBuf, _) = match overrides.git_dir {
Some(git_dir) => git_discover::is_git(&git_dir)
.map_err(|err| Error::NotARepository {
source: err,
path: git_dir.clone(),
})
.map(|kind| (git_dir, kind))?,
None => {
let fallback_directory = fallback_directory.into();
git_discover::is_git(&fallback_directory)
.map_err(|err| Error::NotARepository {
source: err,
path: fallback_directory.clone(),
})
.map(|kind| (fallback_directory, kind))?
}
};
let cwd = std::env::current_dir()?;
let (git_dir, worktree_dir) = git_discover::repository::Path::from_dot_git_dir(path, 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 = git_sec::Trust::from_path_ownership(&git_dir)?;
let mut options = trust_map.into_value_by_level(git_dir_trust);
options.current_dir = Some(cwd);
ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options)
}
pub(crate) fn open_from_paths(
git_dir: PathBuf,
mut worktree_dir: Option<PathBuf>,
options: Options,
) -> Result<Self, Error> {
let Options {
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 },
ref api_config_overrides,
ref cli_config_overrides,
ref current_dir,
} = options;
let current_dir = current_dir.as_deref().expect("BUG: current_dir must be set by caller");
let git_dir_trust = git_dir_trust.expect("trust must be been determined by now");
let common_dir = git_discover::path::from_plain_file(git_dir.join("commondir"))
.transpose()?
.map(|cd| git_dir.join(cd));
let common_dir_ref = common_dir.as_deref().unwrap_or(&git_dir);
let repo_config = config::cache::StageOne::new(
common_dir_ref,
git_dir.as_ref(),
git_dir_trust,
lossy_config,
lenient_config,
)?;
let mut refs = {
let reflog = repo_config.reflog.unwrap_or(git_ref::store::WriteReflog::Disable);
let object_hash = repo_config.object_hash;
match &common_dir {
Some(common_dir) => crate::RefStore::for_linked_worktree(&git_dir, common_dir, reflog, object_hash),
None => crate::RefStore::at(&git_dir, reflog, object_hash),
}
};
let head = refs.find("HEAD").ok();
let git_install_dir = crate::path::install_dir().ok();
let home = std::env::var_os("HOME")
.map(PathBuf::from)
.and_then(|home| env.home.check_opt(home));
let mut filter_config_section = filter_config_section.unwrap_or(config::section::is_trusted);
let 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(),
env.clone(),
config,
lenient_config,
api_config_overrides,
cli_config_overrides,
)?;
if bail_if_untrusted && git_dir_trust != git_sec::Trust::Full {
check_safe_directories(&git_dir, git_install_dir.as_deref(), home.as_deref(), &config)?;
}
if !config.is_bare {
if let Some(wt) = config
.resolved
.path_filter("core", None, Core::WORKTREE.name, &mut filter_config_section)
{
let wt_path = wt
.interpolate(interpolate_context(git_install_dir.as_deref(), home.as_deref()))
.map_err(config::Error::PathInterpolation)?;
worktree_dir = {
git_path::normalize(git_dir.join(wt_path), current_dir)
.and_then(|wt| wt.as_ref().is_dir().then(|| wt.into_owned()))
}
}
}
match worktree_dir {
None if !config.is_bare => {
worktree_dir = Some(git_dir.parent().expect("parent is always available").to_owned());
}
Some(_) => {
}
None => {}
}
refs.write_reflog = config::cache::util::reflog_or_default(config.reflog, worktree_dir.is_some());
let replacements = replacement_objects_refs_prefix(&config.resolved, lenient_config, filter_config_section)?
.and_then(|prefix| {
let platform = refs.iter().ok()?;
let iter = platform.prefixed(&prefix).ok()?;
let prefix = prefix.to_str()?;
let replacements = iter
.filter_map(Result::ok)
.filter_map(|r: git_ref::Reference| {
let target = r.target.try_id()?.to_owned();
let source =
git_hash::ObjectId::from_hex(r.name.as_bstr().strip_prefix(prefix.as_bytes())?).ok()?;
Some((source, target))
})
.collect::<Vec<_>>();
Some(replacements)
})
.unwrap_or_default();
Ok(ThreadSafeRepository {
objects: OwnShared::new(git_odb::Store::at_opts(
common_dir_ref.join("objects"),
replacements,
git_odb::store::init::Options {
slots: object_store_slots,
object_hash: config.object_hash,
use_multi_pack_index: config.use_multi_pack_index,
current_dir: current_dir.to_owned().into(),
},
)?),
common_dir,
refs,
work_tree: worktree_dir,
config,
linked_worktree_options: options,
index: git_features::fs::MutableSnapshot::new().into(),
})
}
}
fn replacement_objects_refs_prefix(
config: &git_config::File<'static>,
lenient: bool,
mut filter_config_section: fn(&git_config::file::Metadata) -> bool,
) -> Result<Option<PathBuf>, Error> {
let is_disabled = config
.boolean_filter_by_key("gitoxide.objects.noReplace", &mut filter_config_section)
.map(|b| gitoxide::Objects::NO_REPLACE.enrich_error(b))
.transpose()
.with_leniency(lenient)
.map_err(config::Error::ConfigBoolean)?
.unwrap_or_default();
if is_disabled {
return Ok(None);
}
let ref_base = git_path::from_bstr({
let key = "gitoxide.objects.replaceRefBase";
debug_assert_eq!(gitoxide::Objects::REPLACE_REF_BASE.logical_name(), key);
config
.string_filter_by_key(key, &mut filter_config_section)
.unwrap_or_else(|| Cow::Borrowed("refs/replace/".into()))
})
.into_owned();
Ok(ref_base.into())
}
fn check_safe_directories(
git_dir: &std::path::Path,
git_install_dir: Option<&std::path::Path>,
home: Option<&std::path::Path>,
config: &config::Cache,
) -> Result<(), Error> {
let mut is_safe = false;
let git_dir = match git_path::realpath(git_dir) {
Ok(p) => p,
Err(_) => git_dir.to_owned(),
};
for safe_dir in config
.resolved
.strings_filter("safe", None, Safe::DIRECTORY.name, &mut Safe::directory_filter)
.unwrap_or_default()
{
if safe_dir.as_ref() == "*" {
is_safe = true;
continue;
}
if safe_dir.is_empty() {
is_safe = false;
continue;
}
if !is_safe {
let safe_dir = match git_config::Path::from(std::borrow::Cow::Borrowed(safe_dir.as_ref()))
.interpolate(interpolate_context(git_install_dir, home))
{
Ok(path) => path,
Err(_) => git_path::from_bstr(safe_dir),
};
if safe_dir == git_dir {
is_safe = true;
continue;
}
}
}
if is_safe {
Ok(())
} else {
Err(Error::UnsafeGitDir { path: git_dir })
}
}