diff options
Diffstat (limited to 'vendor/gix-discover/src')
-rw-r--r-- | vendor/gix-discover/src/is.rs | 151 | ||||
-rw-r--r-- | vendor/gix-discover/src/lib.rs | 52 | ||||
-rw-r--r-- | vendor/gix-discover/src/parse.rs | 33 | ||||
-rw-r--r-- | vendor/gix-discover/src/path.rs | 69 | ||||
-rw-r--r-- | vendor/gix-discover/src/repository.rs | 145 | ||||
-rw-r--r-- | vendor/gix-discover/src/upwards/mod.rs | 178 | ||||
-rw-r--r-- | vendor/gix-discover/src/upwards/types.rs | 190 | ||||
-rw-r--r-- | vendor/gix-discover/src/upwards/util.rs | 78 |
8 files changed, 896 insertions, 0 deletions
diff --git a/vendor/gix-discover/src/is.rs b/vendor/gix-discover/src/is.rs new file mode 100644 index 000000000..e5feb16da --- /dev/null +++ b/vendor/gix-discover/src/is.rs @@ -0,0 +1,151 @@ +use std::{borrow::Cow, ffi::OsStr, path::Path}; + +use crate::{DOT_GIT_DIR, MODULES}; + +/// Returns true if the given `git_dir` seems to be a bare repository. +/// +/// Please note that repositories without an index generally _look_ bare, even though they might also be uninitialized. +pub fn bare(git_dir_candidate: impl AsRef<Path>) -> bool { + let git_dir = git_dir_candidate.as_ref(); + !(git_dir.join("index").exists() || (git_dir.file_name() == Some(OsStr::new(DOT_GIT_DIR)))) +} + +/// Returns true if `git_dir` is is located within a `.git/modules` directory, indicating it's a submodule clone. +pub fn submodule_git_dir(git_dir: impl AsRef<Path>) -> bool { + let git_dir = git_dir.as_ref(); + + let mut last_comp = None; + git_dir.file_name() != Some(OsStr::new(DOT_GIT_DIR)) + && git_dir.components().rev().any(|c| { + if c.as_os_str() == OsStr::new(DOT_GIT_DIR) { + true + } else { + last_comp = Some(c.as_os_str()); + false + } + }) + && last_comp == Some(OsStr::new(MODULES)) +} + +/// What constitutes a valid git repository, returning the guessed repository kind +/// purely based on the presence of files. Note that the gix-config ultimately decides what's bare. +/// +/// Returns the `Kind` of git directory that was passed, possibly alongside the supporting private worktree git dir. +/// +/// Note that `.git` files are followed to a valid git directory, which then requires… +/// +/// * …a valid head +/// * …an objects directory +/// * …a refs directory +/// +pub fn git(git_dir: impl AsRef<Path>) -> Result<crate::repository::Kind, crate::is_git::Error> { + #[derive(Eq, PartialEq)] + enum Kind { + MaybeRepo, + Submodule, + LinkedWorkTreeDir, + WorkTreeGitDir { work_dir: std::path::PathBuf }, + } + let git_dir = git_dir.as_ref(); + let (dot_git, common_dir, kind) = if git_dir + .metadata() + .map_err(|err| crate::is_git::Error::Metadata { + source: err, + path: git_dir.into(), + })? + .is_file() + { + let private_git_dir = crate::path::from_gitdir_file(git_dir)?; + let common_dir = private_git_dir.join("commondir"); + match crate::path::from_plain_file(&common_dir) { + Some(Err(err)) => { + return Err(crate::is_git::Error::MissingCommonDir { + missing: common_dir, + source: err, + }) + } + Some(Ok(common_dir)) => { + let common_dir = private_git_dir.join(common_dir); + ( + Cow::Owned(private_git_dir), + Cow::Owned(common_dir), + Kind::LinkedWorkTreeDir, + ) + } + None => ( + Cow::Owned(private_git_dir.clone()), + Cow::Owned(private_git_dir), + Kind::Submodule, + ), + } + } else { + let common_dir = git_dir.join("commondir"); + let worktree_and_common_dir = crate::path::from_plain_file(common_dir) + .and_then(Result::ok) + .and_then(|cd| { + crate::path::from_plain_file(git_dir.join("gitdir")) + .and_then(Result::ok) + .map(|worktree_gitfile| (crate::path::without_dot_git_dir(worktree_gitfile), cd)) + }); + match worktree_and_common_dir { + Some((work_dir, common_dir)) => { + let common_dir = git_dir.join(common_dir); + ( + Cow::Borrowed(git_dir), + Cow::Owned(common_dir), + Kind::WorkTreeGitDir { work_dir }, + ) + } + None => (Cow::Borrowed(git_dir), Cow::Borrowed(git_dir), Kind::MaybeRepo), + } + }; + + { + // We expect to be able to parse any ref-hash, so we shouldn't have to know the repos hash here. + // With ref-table, the has is probably stored as part of the ref-db itself, so we can handle it from there. + // In other words, it's important not to fail on detached heads here because we guessed the hash kind wrongly. + let object_hash_should_not_matter_here = gix_hash::Kind::Sha1; + let refs = gix_ref::file::Store::at( + dot_git.as_ref(), + gix_ref::store::WriteReflog::Normal, + object_hash_should_not_matter_here, + ); + let head = refs.find_loose("HEAD")?; + if head.name.as_bstr() != "HEAD" { + return Err(crate::is_git::Error::MisplacedHead { + name: head.name.into_inner(), + }); + } + } + + { + let objects_path = common_dir.join("objects"); + if !objects_path.is_dir() { + return Err(crate::is_git::Error::MissingObjectsDirectory { missing: objects_path }); + } + } + { + let refs_path = common_dir.join("refs"); + if !refs_path.is_dir() { + return Err(crate::is_git::Error::MissingRefsDirectory { missing: refs_path }); + } + } + Ok(match kind { + Kind::LinkedWorkTreeDir => crate::repository::Kind::WorkTree { + linked_git_dir: Some(dot_git.into_owned()), + }, + Kind::WorkTreeGitDir { work_dir } => crate::repository::Kind::WorkTreeGitDir { work_dir }, + Kind::Submodule => crate::repository::Kind::Submodule { + git_dir: dot_git.into_owned(), + }, + Kind::MaybeRepo => { + if bare(git_dir) { + crate::repository::Kind::Bare + } else if submodule_git_dir(git_dir) { + crate::repository::Kind::SubmoduleGitDir + } else { + crate::repository::Kind::WorkTree { linked_git_dir: None } + } + } + }) +} diff --git a/vendor/gix-discover/src/lib.rs b/vendor/gix-discover/src/lib.rs new file mode 100644 index 000000000..b522ae1c2 --- /dev/null +++ b/vendor/gix-discover/src/lib.rs @@ -0,0 +1,52 @@ +//! Find git repositories or search them upwards from a starting point, or determine if a directory looks like a git repository. +//! +//! Note that detection methods are educated guesses using the presence of files, without looking too much into the details. +#![deny(missing_docs, rust_2018_idioms)] +#![forbid(unsafe_code)] + +/// The name of the `.git` directory. +pub const DOT_GIT_DIR: &str = ".git"; + +/// The name of the `modules` sub-directory within a `.git` directory for keeping submodule checkouts. +pub const MODULES: &str = "modules"; + +/// +pub mod repository; + +/// +pub mod is_git { + use std::path::PathBuf; + + /// The error returned by [`crate::is_git()`]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("Could not find a valid HEAD reference")] + FindHeadRef(#[from] gix_ref::file::find::existing::Error), + #[error("Expected HEAD at '.git/HEAD', got '.git/{}'", .name)] + MisplacedHead { name: bstr::BString }, + #[error("Expected an objects directory at '{}'", .missing.display())] + MissingObjectsDirectory { missing: PathBuf }, + #[error("The worktree's private repo's commondir file at '{}' or it could not be read", .missing.display())] + MissingCommonDir { missing: PathBuf, source: std::io::Error }, + #[error("Expected a refs directory at '{}'", .missing.display())] + MissingRefsDirectory { missing: PathBuf }, + #[error(transparent)] + GitFile(#[from] crate::path::from_gitdir_file::Error), + #[error("Could not retrieve metadata of \"{path}\"")] + Metadata { source: std::io::Error, path: PathBuf }, + } +} + +mod is; +pub use is::{bare as is_bare, git as is_git, submodule_git_dir as is_submodule_git_dir}; + +/// +pub mod upwards; +pub use upwards::function::{discover as upwards, discover_opts as upwards_opts}; + +/// +pub mod path; + +/// +pub mod parse; diff --git a/vendor/gix-discover/src/parse.rs b/vendor/gix-discover/src/parse.rs new file mode 100644 index 000000000..589718327 --- /dev/null +++ b/vendor/gix-discover/src/parse.rs @@ -0,0 +1,33 @@ +use std::path::PathBuf; + +use bstr::ByteSlice; + +/// +pub mod gitdir { + use bstr::BString; + + /// The error returned by [`parse::gitdir()`][super::gitdir()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error("Format should be 'gitdir: <path>', but got: {:?}", .input)] + InvalidFormat { input: BString }, + #[error("Couldn't decode {:?} as UTF8", .input)] + IllformedUtf8 { input: BString }, + } +} + +/// Parse typical `gitdir` files as seen in worktrees and submodules. +pub fn gitdir(input: &[u8]) -> Result<PathBuf, gitdir::Error> { + let path = input + .strip_prefix(b"gitdir: ") + .ok_or_else(|| gitdir::Error::InvalidFormat { input: input.into() })? + .as_bstr(); + let path = path.trim_end().as_bstr(); + if path.is_empty() { + return Err(gitdir::Error::InvalidFormat { input: input.into() }); + } + Ok(gix_path::try_from_bstr(path) + .map_err(|_| gitdir::Error::IllformedUtf8 { input: input.into() })? + .into_owned()) +} diff --git a/vendor/gix-discover/src/path.rs b/vendor/gix-discover/src/path.rs new file mode 100644 index 000000000..89d260fe9 --- /dev/null +++ b/vendor/gix-discover/src/path.rs @@ -0,0 +1,69 @@ +use std::{io::Read, path::PathBuf}; + +use crate::DOT_GIT_DIR; + +/// +pub mod from_gitdir_file { + /// The error returned by [`from_gitdir_file()`][crate::path::from_gitdir_file()]. + #[derive(Debug, thiserror::Error)] + #[allow(missing_docs)] + pub enum Error { + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Parse(#[from] crate::parse::gitdir::Error), + } +} + +fn read_regular_file_content_with_size_limit(path: impl AsRef<std::path::Path>) -> std::io::Result<Vec<u8>> { + let path = path.as_ref(); + let mut file = std::fs::File::open(path)?; + let max_file_size = 1024 * 64; // NOTE: git allows 1MB here + let file_size = file.metadata()?.len(); + if file_size > max_file_size { + return Err(std::io::Error::new( + std::io::ErrorKind::Other, + format!( + "Refusing to open files larger than {} bytes, '{}' was {} bytes large", + max_file_size, + path.display(), + file_size + ), + )); + } + let mut buf = Vec::with_capacity(512); + file.read_to_end(&mut buf)?; + Ok(buf) +} + +/// Reads a plain path from a file that contains it as its only content, with trailing newlines trimmed. +pub fn from_plain_file(path: impl AsRef<std::path::Path>) -> Option<std::io::Result<PathBuf>> { + use bstr::ByteSlice; + let mut buf = match read_regular_file_content_with_size_limit(path) { + Ok(buf) => buf, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return None, + Err(err) => return Some(Err(err)), + }; + let trimmed_len = buf.trim_end().len(); + buf.truncate(trimmed_len); + Some(Ok(gix_path::from_bstring(buf))) +} + +/// Reads typical `gitdir: ` files from disk as used by worktrees and submodules. +pub fn from_gitdir_file(path: impl AsRef<std::path::Path>) -> Result<PathBuf, from_gitdir_file::Error> { + let path = path.as_ref(); + let buf = read_regular_file_content_with_size_limit(path)?; + let mut gitdir = crate::parse::gitdir(&buf)?; + if let Some(parent) = path.parent() { + gitdir = parent.join(gitdir); + } + Ok(gitdir) +} + +/// Conditionally pop a trailing `.git` dir if present. +pub fn without_dot_git_dir(mut path: PathBuf) -> PathBuf { + if path.file_name().and_then(|n| n.to_str()) == Some(DOT_GIT_DIR) { + path.pop(); + } + path +} diff --git a/vendor/gix-discover/src/repository.rs b/vendor/gix-discover/src/repository.rs new file mode 100644 index 000000000..0c0318cdc --- /dev/null +++ b/vendor/gix-discover/src/repository.rs @@ -0,0 +1,145 @@ +use std::path::PathBuf; + +/// A repository path which either points to a work tree or the `.git` repository itself. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Path { + /// The currently checked out linked worktree along with its connected and existing git directory, or the worktree checkout of a + /// submodule. + LinkedWorkTree { + /// The base of the work tree. + work_dir: PathBuf, + /// The worktree-private git dir, located within the main git directory which holds most of the information. + git_dir: PathBuf, + }, + /// The currently checked out or nascent work tree of a git repository + WorkTree(PathBuf), + /// The git repository itself, typically bare and without known worktree. + /// + /// Note that it might still have linked work-trees which can be accessed later, weather bare or not, or it might be a + /// submodule git directory in the `.git/modules/**/<name>` directory of the parent repository. + Repository(PathBuf), +} + +mod path { + use std::path::PathBuf; + + use crate::{ + path::without_dot_git_dir, + repository::{Kind, Path}, + DOT_GIT_DIR, + }; + + impl AsRef<std::path::Path> for Path { + fn as_ref(&self) -> &std::path::Path { + match self { + Path::WorkTree(path) + | Path::Repository(path) + | Path::LinkedWorkTree { + work_dir: _, + git_dir: path, + } => path, + } + } + } + + impl Path { + /// Instantiate a new path from `dir` which is expected to be the `.git` directory, with `kind` indicating + /// whether it's a bare repository or not, with `current_dir` being used to normalize relative paths + /// as needed. + /// + /// `None` is returned if `dir` could not be resolved due to being relative and trying to reach outside of the filesystem root. + pub fn from_dot_git_dir( + dir: impl Into<PathBuf>, + kind: Kind, + current_dir: impl AsRef<std::path::Path>, + ) -> Option<Self> { + let cwd = current_dir.as_ref(); + let normalize_on_trailing_dot_dot = |dir: PathBuf| -> Option<PathBuf> { + if !matches!(dir.components().rev().next(), Some(std::path::Component::ParentDir)) { + dir + } else { + gix_path::normalize(&dir, cwd)?.into_owned() + } + .into() + }; + + let dir = dir.into(); + match kind { + Kind::Submodule { git_dir } => Path::LinkedWorkTree { + git_dir: gix_path::normalize(git_dir, cwd)?.into_owned(), + work_dir: without_dot_git_dir(normalize_on_trailing_dot_dot(dir)?), + }, + Kind::SubmoduleGitDir => Path::Repository(dir), + Kind::WorkTreeGitDir { work_dir } => Path::LinkedWorkTree { git_dir: dir, work_dir }, + Kind::WorkTree { linked_git_dir } => match linked_git_dir { + Some(git_dir) => Path::LinkedWorkTree { + git_dir, + work_dir: without_dot_git_dir(normalize_on_trailing_dot_dot(dir)?), + }, + None => { + let mut dir = normalize_on_trailing_dot_dot(dir)?; + dir.pop(); // ".git" suffix + let work_dir = dir.as_os_str().is_empty().then(|| PathBuf::from(".")).unwrap_or(dir); + Path::WorkTree(work_dir) + } + }, + Kind::Bare => Path::Repository(dir), + } + .into() + } + /// Returns the [kind][Kind] of this repository path. + pub fn kind(&self) -> Kind { + match self { + Path::LinkedWorkTree { work_dir: _, git_dir } => Kind::WorkTree { + linked_git_dir: Some(git_dir.to_owned()), + }, + Path::WorkTree(_) => Kind::WorkTree { linked_git_dir: None }, + Path::Repository(_) => Kind::Bare, + } + } + + /// Consume and split this path into the location of the `.git` directory as well as an optional path to the work tree. + pub fn into_repository_and_work_tree_directories(self) -> (PathBuf, Option<PathBuf>) { + match self { + Path::LinkedWorkTree { work_dir, git_dir } => (git_dir, Some(work_dir)), + Path::WorkTree(working_tree) => (working_tree.join(DOT_GIT_DIR), Some(working_tree)), + Path::Repository(repository) => (repository, None), + } + } + } +} + +/// The kind of repository path. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Kind { + /// A bare repository does not have a work tree, that is files on disk beyond the `git` repository itself. + /// + /// Note that this is merely a guess at this point as we didn't read the configuration yet. + Bare, + /// A `git` repository along with checked out files in a work tree. + WorkTree { + /// If set, this is the git dir associated with this _linked_ worktree. + /// If `None`, the git_dir is the `.git` directory inside the _main_ worktree we represent. + linked_git_dir: Option<PathBuf>, + }, + /// A worktree's git directory in the common`.git` directory in `worktrees/<name>`. + WorkTreeGitDir { + /// Path to the worktree directory. + work_dir: PathBuf, + }, + /// The directory is a `.git` dir file of a submodule worktree. + Submodule { + /// The git repository itself that is referenced by the `.git` dir file, typically in the `.git/modules/**/<name>` directory of the parent + /// repository. + git_dir: PathBuf, + }, + /// The git directory in the `.git/modules/**/<name>` directory tree of the parent repository + SubmoduleGitDir, +} + +impl Kind { + /// Returns true if this is a bare repository, one without a work tree. + pub fn is_bare(&self) -> bool { + matches!(self, Kind::Bare) + } +} diff --git a/vendor/gix-discover/src/upwards/mod.rs b/vendor/gix-discover/src/upwards/mod.rs new file mode 100644 index 000000000..8b041f443 --- /dev/null +++ b/vendor/gix-discover/src/upwards/mod.rs @@ -0,0 +1,178 @@ +mod types; +pub use types::{Error, Options}; + +mod util; + +pub(crate) mod function { + use std::{borrow::Cow, path::Path}; + + use gix_sec::Trust; + + use super::{Error, Options}; + #[cfg(unix)] + use crate::upwards::util::device_id; + use crate::{ + is_git, + upwards::util::{find_ceiling_height, shorten_path_with_cwd}, + DOT_GIT_DIR, + }; + + /// Find the location of the git repository directly in `directory` or in any of its parent directories and provide + /// an associated Trust level by looking at the git directory's ownership, and control discovery using `options`. + /// + /// Fail if no valid-looking git repository could be found. + // TODO: tests for trust-based discovery + #[cfg_attr(not(unix), allow(unused_variables))] + pub fn discover_opts( + directory: impl AsRef<Path>, + Options { + required_trust, + ceiling_dirs, + match_ceiling_dir_or_error, + cross_fs, + current_dir, + }: Options<'_>, + ) -> Result<(crate::repository::Path, Trust), Error> { + // Normalize the path so that `Path::parent()` _actually_ gives + // us the parent directory. (`Path::parent` just strips off the last + // path component, which means it will not do what you expect when + // working with paths paths that contain '..'.) + let cwd = current_dir + .map(|cwd| Ok(Cow::Borrowed(cwd))) + .unwrap_or_else(|| std::env::current_dir().map(Cow::Owned))?; + let directory = directory.as_ref(); + #[cfg(windows)] + let directory = dunce::simplified(directory); + let dir = gix_path::normalize(directory, cwd.as_ref()).ok_or_else(|| Error::InvalidInput { + directory: directory.into(), + })?; + let dir_metadata = dir.metadata().map_err(|_| Error::InaccessibleDirectory { + path: dir.to_path_buf(), + })?; + + if !dir_metadata.is_dir() { + return Err(Error::InaccessibleDirectory { path: dir.into_owned() }); + } + let mut dir_made_absolute = !directory.is_absolute() + && cwd + .as_ref() + .strip_prefix(dir.as_ref()) + .or_else(|_| dir.as_ref().strip_prefix(cwd.as_ref())) + .is_ok(); + + let filter_by_trust = |x: &Path| -> Result<Option<Trust>, Error> { + let trust = Trust::from_path_ownership(x).map_err(|err| Error::CheckTrust { path: x.into(), err })?; + Ok((trust >= required_trust).then_some(trust)) + }; + + let max_height = if !ceiling_dirs.is_empty() { + let max_height = find_ceiling_height(&dir, &ceiling_dirs, cwd.as_ref()); + if max_height.is_none() && match_ceiling_dir_or_error { + return Err(Error::NoMatchingCeilingDir); + } + max_height + } else { + None + }; + + #[cfg(unix)] + let initial_device = device_id(&dir_metadata); + + let mut cursor = dir.clone().into_owned(); + let mut current_height = 0; + 'outer: loop { + if max_height.map_or(false, |x| current_height > x) { + return Err(Error::NoGitRepositoryWithinCeiling { + path: dir.into_owned(), + ceiling_height: current_height, + }); + } + current_height += 1; + + #[cfg(unix)] + if current_height != 0 && !cross_fs { + let metadata = if cursor.as_os_str().is_empty() { + Path::new(".") + } else { + cursor.as_ref() + } + .metadata() + .map_err(|_| Error::InaccessibleDirectory { path: cursor.clone() })?; + + if device_id(&metadata) != initial_device { + return Err(Error::NoGitRepositoryWithinFs { + path: dir.into_owned(), + limit: cursor.clone(), + }); + } + } + + for append_dot_git in &[false, true] { + if *append_dot_git { + cursor.push(DOT_GIT_DIR); + } + if let Ok(kind) = is_git(&cursor) { + match filter_by_trust(&cursor)? { + Some(trust) => { + // TODO: test this more, it definitely doesn't always find the shortest path to a directory + let path = if dir_made_absolute { + shorten_path_with_cwd(cursor, cwd.as_ref()) + } else { + cursor + }; + break 'outer Ok(( + crate::repository::Path::from_dot_git_dir(path, kind, cwd).ok_or_else(|| { + Error::InvalidInput { + directory: directory.into(), + } + })?, + trust, + )); + } + None => { + break 'outer Err(Error::NoTrustedGitRepository { + path: dir.into_owned(), + candidate: cursor, + required: required_trust, + }) + } + } + } + if *append_dot_git { + cursor.pop(); + } + } + if cursor.parent().map_or(false, |p| p.as_os_str().is_empty()) { + cursor = cwd.to_path_buf(); + dir_made_absolute = true; + } + if !cursor.pop() { + if dir_made_absolute + || matches!( + cursor.components().next(), + Some(std::path::Component::RootDir) | Some(std::path::Component::Prefix(_)) + ) + { + break Err(Error::NoGitRepository { path: dir.into_owned() }); + } else { + dir_made_absolute = true; + debug_assert!(!cursor.as_os_str().is_empty()); + // TODO: realpath or normalize? No test runs into this. + cursor = gix_path::normalize(&cursor, cwd.as_ref()) + .ok_or_else(|| Error::InvalidInput { + directory: cursor.clone(), + })? + .into_owned(); + } + } + } + } + + /// Find the location of the git repository directly in `directory` or in any of its parent directories, and provide + /// the trust level derived from Path ownership. + /// + /// Fail if no valid-looking git repository could be found. + pub fn discover(directory: impl AsRef<Path>) -> Result<(crate::repository::Path, Trust), Error> { + discover_opts(directory, Default::default()) + } +} diff --git a/vendor/gix-discover/src/upwards/types.rs b/vendor/gix-discover/src/upwards/types.rs new file mode 100644 index 000000000..52ce9db9c --- /dev/null +++ b/vendor/gix-discover/src/upwards/types.rs @@ -0,0 +1,190 @@ +use std::{env, ffi::OsStr, path::PathBuf}; + +/// The error returned by [gix_discover::upwards()][crate::upwards()]. +#[derive(Debug, thiserror::Error)] +#[allow(missing_docs)] +pub enum Error { + #[error("Could not obtain the current working directory")] + CurrentDir(#[from] std::io::Error), + #[error("Relative path \"{}\"tries to reach beyond root filesystem", directory.display())] + InvalidInput { directory: PathBuf }, + #[error("Failed to access a directory, or path is not a directory: '{}'", .path.display())] + InaccessibleDirectory { path: PathBuf }, + #[error("Could find a git repository in '{}' or in any of its parents", .path.display())] + NoGitRepository { path: PathBuf }, + #[error("Could find a git repository in '{}' or in any of its parents within ceiling height of {}", .path.display(), .ceiling_height)] + NoGitRepositoryWithinCeiling { path: PathBuf, ceiling_height: usize }, + #[error("Could find a git repository in '{}' or in any of its parents within device limits below '{}'", .path.display(), .limit.display())] + NoGitRepositoryWithinFs { path: PathBuf, limit: PathBuf }, + #[error("None of the passed ceiling directories prefixed the git-dir candidate, making them ineffective.")] + NoMatchingCeilingDir, + #[error("Could find a trusted git repository in '{}' or in any of its parents, candidate at '{}' discarded", .path.display(), .candidate.display())] + NoTrustedGitRepository { + path: PathBuf, + candidate: PathBuf, + required: gix_sec::Trust, + }, + #[error("Could not determine trust level for path '{}'.", .path.display())] + CheckTrust { + path: PathBuf, + #[source] + err: std::io::Error, + }, +} + +/// Options to help guide the [discovery][crate::upwards()] of repositories, along with their options +/// when instantiated. +pub struct Options<'a> { + /// When discovering a repository, assure it has at least this trust level or ignore it otherwise. + /// + /// This defaults to [`Reduced`][gix_sec::Trust::Reduced] as our default settings are geared towards avoiding abuse. + /// Set it to `Full` to only see repositories that [are owned by the current user][gix_sec::Trust::from_path_ownership()]. + pub required_trust: gix_sec::Trust, + /// When discovering a repository, ignore any repositories that are located in these directories or any of their parents. + /// + /// Note that we ignore ceiling directories if the search directory is directly on top of one, which by default is an error + /// if `match_ceiling_dir_or_error` is true, the default. + pub ceiling_dirs: Vec<PathBuf>, + /// If true, default true, and `ceiling_dirs` is not empty, we expect at least one ceiling directory to + /// contain our search dir or else there will be an error. + pub match_ceiling_dir_or_error: bool, + /// if `true` avoid crossing filesystem boundaries. + /// Only supported on Unix-like systems. + // TODO: test on Linux + // TODO: Handle WASI once https://github.com/rust-lang/rust/issues/71213 is resolved + pub cross_fs: bool, + /// If set, the _current working directory_ (absolute path) to use when resolving relative paths. Note that + /// that this is merely an optimization for those who discover a lot of repositories in the same process. + /// + /// If unset, the current working directory will be obtained automatically. + pub current_dir: Option<&'a std::path::Path>, +} + +impl Default for Options<'_> { + fn default() -> Self { + Options { + required_trust: gix_sec::Trust::Reduced, + ceiling_dirs: vec![], + match_ceiling_dir_or_error: true, + cross_fs: false, + current_dir: None, + } + } +} + +impl Options<'_> { + /// Loads discovery options overrides from the environment. + /// + /// The environment variables are: + /// - `GIT_CEILING_DIRECTORIES` for `ceiling_dirs` + /// + /// Note that `GIT_DISCOVERY_ACROSS_FILESYSTEM` for `cross_fs` is **not** read, + /// as it requires parsing of `gix-config` style boolean values. + /// + /// In addition, this function disables `match_ceiling_dir_or_error` to allow + /// discovery if an outside environment variable sets non-matching ceiling directories. + // TODO: test + pub fn apply_environment(mut self) -> Self { + let name = "GIT_CEILING_DIRECTORIES"; + if let Some(ceiling_dirs) = env::var_os(name) { + self.ceiling_dirs = parse_ceiling_dirs(&ceiling_dirs); + } + self.match_ceiling_dir_or_error = false; + self + } +} + +/// Parse a byte-string of `:`-separated paths into `Vec<PathBuf>`. +/// On Windows, paths are separated by `;`. +/// Non-absolute paths are discarded. +/// To match git, all paths are normalized, until an empty path is encountered. +pub(crate) fn parse_ceiling_dirs(ceiling_dirs: &OsStr) -> Vec<PathBuf> { + let mut should_normalize = true; + let mut out = Vec::new(); + for ceiling_dir in std::env::split_paths(ceiling_dirs) { + if ceiling_dir.as_os_str().is_empty() { + should_normalize = false; + continue; + } + + // Only absolute paths are allowed + if ceiling_dir.is_relative() { + continue; + } + + let mut dir = ceiling_dir; + if should_normalize { + if let Ok(normalized) = gix_path::realpath(&dir) { + dir = normalized; + } + } + out.push(dir); + } + out +} + +#[cfg(test)] +mod tests { + + #[test] + #[cfg(unix)] + fn parse_ceiling_dirs_from_environment_format() -> std::io::Result<()> { + use std::{fs, os::unix::fs::symlink}; + + use super::*; + + // Setup filesystem + let dir = tempfile::tempdir().expect("success creating temp dir"); + let direct_path = dir.path().join("direct"); + let symlink_path = dir.path().join("symlink"); + fs::create_dir(&direct_path)?; + symlink(&direct_path, &symlink_path)?; + + // Parse & build ceiling dirs string + let symlink_str = symlink_path.to_str().expect("symlink path is valid utf8"); + let ceiling_dir_string = format!("{symlink_str}:relative::{symlink_str}"); + let ceiling_dirs = parse_ceiling_dirs(OsStr::new(ceiling_dir_string.as_str())); + + assert_eq!(ceiling_dirs.len(), 2, "Relative path is discarded"); + assert_eq!( + ceiling_dirs[0], + symlink_path.canonicalize().expect("symlink path exists"), + "Symlinks are resolved" + ); + assert_eq!( + ceiling_dirs[1], symlink_path, + "Symlink are not resolved after empty item" + ); + + dir.close() + } + + #[test] + #[cfg(windows)] + fn parse_ceiling_dirs_from_environment_format() -> std::io::Result<()> { + use std::{fs, os::windows::fs::symlink_dir}; + + use super::*; + + // Setup filesystem + let dir = tempfile::tempdir().expect("success creating temp dir"); + let direct_path = dir.path().join("direct"); + let symlink_path = dir.path().join("symlink"); + fs::create_dir(&direct_path)?; + symlink_dir(&direct_path, &symlink_path)?; + + // Parse & build ceiling dirs string + let symlink_str = symlink_path.to_str().expect("symlink path is valid utf8"); + let ceiling_dir_string = format!("{};relative;;{}", symlink_str, symlink_str); + let ceiling_dirs = parse_ceiling_dirs(OsStr::new(ceiling_dir_string.as_str())); + + assert_eq!(ceiling_dirs.len(), 2, "Relative path is discarded"); + assert_eq!(ceiling_dirs[0], direct_path, "Symlinks are resolved"); + assert_eq!( + ceiling_dirs[1], symlink_path, + "Symlink are not resolved after empty item" + ); + + dir.close() + } +} diff --git a/vendor/gix-discover/src/upwards/util.rs b/vendor/gix-discover/src/upwards/util.rs new file mode 100644 index 000000000..93c6e993e --- /dev/null +++ b/vendor/gix-discover/src/upwards/util.rs @@ -0,0 +1,78 @@ +use std::path::{Path, PathBuf}; + +use crate::DOT_GIT_DIR; + +pub(crate) fn shorten_path_with_cwd(cursor: PathBuf, cwd: &Path) -> PathBuf { + fn comp_len(c: std::path::Component<'_>) -> usize { + use std::path::Component::*; + match c { + Prefix(p) => p.as_os_str().len(), + CurDir => 1, + ParentDir => 2, + Normal(p) => p.len(), + RootDir => 1, + } + } + + debug_assert_eq!(cursor.file_name().and_then(|f| f.to_str()), Some(DOT_GIT_DIR)); + let parent = cursor.parent().expect(".git appended"); + cwd.strip_prefix(parent) + .ok() + .and_then(|path_relative_to_cwd| { + let relative_path_components = path_relative_to_cwd.components().count(); + let current_component_len = cursor.components().map(comp_len).sum::<usize>(); + (relative_path_components * "..".len() < current_component_len).then(|| { + std::iter::repeat("..") + .take(relative_path_components) + .chain(Some(DOT_GIT_DIR)) + .collect() + }) + }) + .unwrap_or(cursor) +} + +/// Find the number of components parenting the `search_dir` before the first directory in `ceiling_dirs`. +/// `search_dir` needs to be normalized, and we normalize every ceiling as well. +pub(crate) fn find_ceiling_height(search_dir: &Path, ceiling_dirs: &[PathBuf], cwd: &Path) -> Option<usize> { + if ceiling_dirs.is_empty() { + return None; + } + + let search_realpath; + let search_dir = if search_dir.is_absolute() { + search_dir + } else { + search_realpath = gix_path::realpath_opts(search_dir, cwd, gix_path::realpath::MAX_SYMLINKS).ok()?; + search_realpath.as_path() + }; + ceiling_dirs + .iter() + .filter_map(|ceiling_dir| { + #[cfg(windows)] + let ceiling_dir = dunce::simplified(ceiling_dir); + let mut ceiling_dir = gix_path::normalize(ceiling_dir, cwd)?; + if !ceiling_dir.is_absolute() { + ceiling_dir = gix_path::normalize(cwd.join(ceiling_dir.as_ref()), cwd)?; + } + search_dir + .strip_prefix(ceiling_dir.as_ref()) + .ok() + .map(|path_relative_to_ceiling| path_relative_to_ceiling.components().count()) + .filter(|height| *height > 0) + }) + .min() +} + +/// Returns the device ID of the directory. +#[cfg(target_os = "linux")] +pub(crate) fn device_id(m: &std::fs::Metadata) -> u64 { + use std::os::linux::fs::MetadataExt; + m.st_dev() +} + +/// Returns the device ID of the directory. +#[cfg(all(unix, not(target_os = "linux")))] +pub(crate) fn device_id(m: &std::fs::Metadata) -> u64 { + use std::os::unix::fs::MetadataExt; + m.dev() +} |