#![allow(clippy::result_large_err)] use std::{borrow::Cow, path::PathBuf}; use gix_features::threading::OwnShared; use super::{Error, Options}; use crate::{ config, config::{ cache::{interpolate_context, util::ApplyLeniency}, tree::{gitoxide, Core, Key, Safe}, }, open::Permissions, ThreadSafeRepository, }; #[derive(Default, Clone)] pub(crate) struct EnvironmentOverrides { /// An override of the worktree typically from the environment, and overrides even worktree dirs set as parameter. /// /// This emulates the way git handles this override. worktree_dir: Option, /// An override for the .git directory, typically from the environment. /// /// If set, the passed in `git_dir` parameter will be ignored in favor of this one. git_dir: Option, } impl EnvironmentOverrides { fn from_env() -> Result> { 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 { /// Open a git repository at the given `path`, possibly expanding it to `path/.git` if `path` is a work tree dir. pub fn open(path: impl Into) -> Result { Self::open_opts(path, Options::default()) } /// Open a git repository at the given `path`, possibly expanding it to `path/.git` if `path` is a work tree dir, and use /// `options` for fine-grained control. /// /// Note that you should use [`crate::discover()`] if security should be adjusted by ownership. pub fn open_opts(path: impl Into, mut options: Options) -> Result { let (path, kind) = { let path = path.into(); let looks_like_git_dir = path.ends_with(gix_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(gix_discover::DOT_GIT_DIR)) } else { Cow::Borrowed(&path) }; match gix_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 gix_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) = gix_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 = gix_sec::Trust::from_path_ownership(&git_dir)?.into(); } options.current_dir = Some(cwd); ThreadSafeRepository::open_from_paths(git_dir, worktree_dir, options) } /// Try to open a git repository in `fallback_directory` (can be worktree or `.git` directory) only if there is no override /// from of the `gitdir` using git environment variables. /// /// Use the `trust_map` to apply options depending in the trust level for `directory` or the directory it's overridden with. /// The `.git` directory whether given or computed is used for trust checks. /// /// Note that this will read various `GIT_*` environment variables to check for overrides, and is probably most useful when implementing /// custom hooks. // TODO: tests, with hooks, GIT_QUARANTINE for ref-log and transaction control (needs gix-sec support to remove write access in gix-ref) // TODO: The following vars should end up as overrides of the respective configuration values (see gix-config). // GIT_PROXY_SSL_CERT, GIT_PROXY_SSL_KEY, GIT_PROXY_SSL_CERT_PASSWORD_PROTECTED. // GIT_PROXY_SSL_CAINFO, GIT_SSL_CIPHER_LIST, GIT_HTTP_MAX_REQUESTS, GIT_CURL_FTP_NO_EPSV, pub fn open_with_environment_overrides( fallback_directory: impl Into, trust_map: gix_sec::trust::Mapping, ) -> Result { let overrides = EnvironmentOverrides::from_env()?; let (path, path_kind): (PathBuf, _) = match overrides.git_dir { Some(git_dir) => gix_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(); gix_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) = gix_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 = gix_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, options: Options, ) -> Result { 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, attributes, }, 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 determined by now"); // TODO: assure we handle the worktree-dir properly as we can have config per worktree with an extension. // This would be something read in later as have to first check for extensions. Also this means // that each worktree, even if accessible through this instance, has to come in its own Repository instance // as it may have its own configuration. That's fine actually. let common_dir = gix_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(gix_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 = 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 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, attributes, config, lenient_config, api_config_overrides, cli_config_overrides, )?; if bail_if_untrusted && git_dir_trust != gix_sec::Trust::Full { check_safe_directories(&git_dir, git_install_dir.as_deref(), home.as_deref(), &config)?; } // core.worktree might be used to overwrite the worktree directory 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 = { gix_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(_) => { // note that we might be bare even with a worktree directory - work trees don't have to be // the parent of a non-bare repository. } 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: 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_bytes())?).ok()?; Some((source, target)) }) .collect::>(); Some(replacements) }) .unwrap_or_default(); Ok(ThreadSafeRepository { objects: OwnShared::new(gix_odb::Store::at_opts( common_dir_ref.join("objects"), replacements, gix_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, // used when spawning new repositories off this one when following worktrees linked_worktree_options: options, index: gix_fs::SharedFileSnapshotMut::new().into(), shallow_commits: gix_fs::SharedFileSnapshotMut::new().into(), }) } } // TODO: tests fn replacement_objects_refs_prefix( config: &gix_config::File<'static>, lenient: bool, mut filter_config_section: fn(&gix_config::file::Metadata) -> bool, ) -> Result, Error> { let is_disabled = config .boolean_filter_by_key("core.useReplaceRefs", &mut filter_config_section) .map(|b| Core::USE_REPLACE_REFS.enrich_error(b)) .transpose() .with_leniency(lenient) .map_err(config::Error::ConfigBoolean)? .unwrap_or(true); if is_disabled { return Ok(None); } let ref_base = gix_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 gix_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 gix_config::Path::from(std::borrow::Cow::Borrowed(safe_dir.as_ref())) .interpolate(interpolate_context(git_install_dir, home)) { Ok(path) => path, Err(_) => gix_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 }) } }