summaryrefslogtreecommitdiffstats
path: root/vendor/gix-discover/src
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/gix-discover/src')
-rw-r--r--vendor/gix-discover/src/is.rs151
-rw-r--r--vendor/gix-discover/src/lib.rs52
-rw-r--r--vendor/gix-discover/src/parse.rs33
-rw-r--r--vendor/gix-discover/src/path.rs69
-rw-r--r--vendor/gix-discover/src/repository.rs145
-rw-r--r--vendor/gix-discover/src/upwards/mod.rs178
-rw-r--r--vendor/gix-discover/src/upwards/types.rs190
-rw-r--r--vendor/gix-discover/src/upwards/util.rs78
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()
+}