create src

This commit is contained in:
awfixer
2026-03-11 02:04:19 -07:00
commit 52f7a22bf2
2595 changed files with 402870 additions and 0 deletions

1945
src-discover/CHANGELOG.md Normal file

File diff suppressed because it is too large Load Diff

45
src-discover/Cargo.toml Normal file
View File

@@ -0,0 +1,45 @@
lints.workspace = true
[package]
name = "src-discover"
version = "0.48.0"
repository = "https://github.com/GitoxideLabs/gitoxide"
license = "MIT OR Apache-2.0"
description = "Discover git repositories and check if a directory is a git repository"
authors = ["Sebastian Thiel <sebastian.thiel@icloud.com>"]
edition = "2021"
include = ["src/**/*", "LICENSE-*"]
rust-version = "1.82"
[lib]
doctest = false
[features]
## Enable support for the SHA-1 hash by forwarding the feature to dependencies.
sha1 = ["src-ref/sha1"]
[dependencies]
src-sec = { version = "^0.13.1", path = "../src-sec" }
src-path = { version = "^0.11.1", path = "../src-path" }
src-ref = { version = "^0.60.0", path = "../src-ref" }
src-fs = { version = "^0.19.1", path = "../src-fs" }
bstr = { version = "1.12.0", default-features = false, features = ["std", "unicode"] }
thiserror = "2.0.18"
[target.'cfg(windows)'.dependencies]
dunce = "1.0.3"
[dev-dependencies]
src-testtools = { path = "../tests/tools" }
serial_test = { version = "3.4.0", default-features = false }
is_ci = "1.1.1"
[target.'cfg(target_os = "macos")'.dev-dependencies]
defer = "0.2.1"
[target.'cfg(any(unix, windows))'.dev-dependencies]
tempfile = "3.26.0"
[package.metadata.docs.rs]
features = ["sha1"]

1
src-discover/LICENSE-APACHE Symbolic link
View File

@@ -0,0 +1 @@
../LICENSE-APACHE

1
src-discover/LICENSE-MIT Symbolic link
View File

@@ -0,0 +1 @@
../LICENSE-MIT

163
src-discover/src/is.rs Normal file
View File

@@ -0,0 +1,163 @@
use std::{borrow::Cow, ffi::OsStr, path::Path};
use crate::path::RepositoryKind;
use crate::DOT_GIT_DIR;
/// 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: &Path) -> bool {
!(git_dir_candidate.join("index").exists() || (git_dir_candidate.file_name() == Some(OsStr::new(DOT_GIT_DIR))))
}
/// Returns true if `git_dir` is located within a `.git/modules` directory, indicating it's a submodule clone.
#[deprecated = "use path::repository_kind() instead"]
pub fn submodule_git_dir(git_dir: &Path) -> bool {
crate::path::repository_kind(git_dir).is_some_and(|kind| matches!(kind, RepositoryKind::Submodule))
}
/// What constitutes a valid git repository, returning the guessed repository kind
/// purely based on the presence of files. Note that the git-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: &Path) -> Result<crate::repository::Kind, crate::is_git::Error> {
let git_dir_metadata = git_dir.metadata().map_err(|err| crate::is_git::Error::Metadata {
source: err,
path: git_dir.into(),
})?;
// precompose-unicode can't be known here, so we just default it to false, hoping it won't matter.
let cwd = gix_fs::current_dir(false)?;
git_with_metadata(git_dir, git_dir_metadata, &cwd)
}
pub(crate) fn git_with_metadata(
git_dir: &Path,
git_dir_metadata: std::fs::Metadata,
cwd: &Path,
) -> Result<crate::repository::Kind, crate::is_git::Error> {
#[derive(Eq, PartialEq)]
enum Kind {
MaybeRepo,
Submodule,
LinkedWorkTreeDir,
WorkTreeGitDir { work_dir: std::path::PathBuf },
}
let dot_git = if git_dir_metadata.is_file() {
let private_git_dir = crate::path::from_gitdir_file(git_dir)?;
Cow::Owned(private_git_dir)
} else {
Cow::Borrowed(git_dir)
};
{
// Fast-path: avoid doing the complete search if HEAD is already not there.
if !dot_git.join("HEAD").exists() {
return Err(crate::is_git::Error::MissingHead);
}
// 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 hash 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 refs = gix_ref::file::Store::at(dot_git.as_ref().into(), Default::default());
match refs.find_loose("HEAD") {
Ok(head) => {
if head.name.as_bstr() != "HEAD" {
return Err(crate::is_git::Error::MisplacedHead {
name: head.name.into_inner(),
});
}
}
Err(gix_ref::file::find::existing::Error::Find(gix_ref::file::find::Error::ReferenceCreation {
source: _,
relative_path,
})) if relative_path == Path::new("HEAD") => {
// It's fine as long as the reference is found is `HEAD`.
}
Err(err) => {
return Err(err.into());
}
}
}
let (common_dir, kind) = if git_dir_metadata.is_file() {
let common_dir = dot_git.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 = dot_git.join(common_dir);
(Cow::Owned(common_dir), Kind::LinkedWorkTreeDir)
}
None => (dot_git.clone(), Kind::Submodule),
}
} else {
let common_dir = dot_git.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(&dot_git.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 = dot_git.join(common_dir);
(Cow::Owned(common_dir), Kind::WorkTreeGitDir { work_dir })
}
None => (dot_git.clone(), Kind::MaybeRepo),
}
};
{
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 => {
let conformed_git_dir = if git_dir == Path::new(".") {
gix_path::realpath_opts(git_dir, cwd, gix_path::realpath::MAX_SYMLINKS)
.map(Cow::Owned)
.unwrap_or(Cow::Borrowed(git_dir))
} else {
gix_path::normalize(git_dir.into(), cwd).unwrap_or(Cow::Borrowed(git_dir))
};
if bare(conformed_git_dir.as_ref()) || conformed_git_dir.extension() == Some(OsStr::new("git")) {
crate::repository::Kind::PossiblyBare
} else if crate::path::repository_kind(conformed_git_dir.as_ref())
.is_some_and(|kind| matches!(kind, RepositoryKind::Submodule))
{
crate::repository::Kind::SubmoduleGitDir
} else if conformed_git_dir.file_name() == Some(OsStr::new(DOT_GIT_DIR)) {
crate::repository::Kind::WorkTree { linked_git_dir: None }
} else {
crate::repository::Kind::PossiblyBare
}
}
})
}

60
src-discover/src/lib.rs Normal file
View File

@@ -0,0 +1,60 @@
//! 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("Missing HEAD at '.git/HEAD'")]
MissingHead,
#[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 },
#[error("The repository's config file doesn't exist or didn't have a 'bare' configuration or contained core.worktree without value")]
Inconclusive,
#[error("Could not obtain current directory for resolving the '.' repository path")]
CurrentDir(#[from] std::io::Error),
}
}
mod is;
#[allow(deprecated)]
pub use is::submodule_git_dir as is_submodule_git_dir;
pub use is::{bare as is_bare, git as is_git};
///
pub mod upwards;
pub use upwards::function::{discover as upwards, discover_opts as upwards_opts};
///
pub mod path;
///
pub mod parse;

33
src-discover/src/parse.rs Normal file
View File

@@ -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())
}

104
src-discover/src/path.rs Normal file
View File

@@ -0,0 +1,104 @@
use crate::{DOT_GIT_DIR, MODULES};
use std::ffi::OsStr;
use std::path::Path;
use std::{io::Read, path::PathBuf};
/// The kind of repository by looking exclusively at its `git_dir`.
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub enum RepositoryKind {
/// The repository resides in `.git/modules/`.
Submodule,
/// The repository resides in `.git/worktrees/`.
LinkedWorktree,
/// The repository is in a `.git` directory.
Common,
}
///
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: &std::path::Path) -> std::io::Result<Vec<u8>> {
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::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)
}
/// Guess the kind of repository by looking at its `git_dir` path and return it.
/// Return `None` if `git_dir` isn't called `.git` or isn't within `.git/worktrees` or `.git/modules`, or if it's
/// a `.git` suffix like in `foo.git`.
/// The check for markers is case-sensitive under the assumption that nobody meddles with standard directories.
pub fn repository_kind(git_dir: &Path) -> Option<RepositoryKind> {
if git_dir.file_name() == Some(OsStr::new(DOT_GIT_DIR)) {
return Some(RepositoryKind::Common);
}
let mut last_comp = None;
git_dir.components().rev().skip(1).any(|c| {
if c.as_os_str() == OsStr::new(DOT_GIT_DIR) {
true
} else {
last_comp = Some(c.as_os_str());
false
}
});
let last_comp = last_comp?;
if last_comp == OsStr::new(MODULES) {
RepositoryKind::Submodule.into()
} else if last_comp == OsStr::new("worktrees") {
RepositoryKind::LinkedWorktree.into()
} else {
None
}
}
/// Reads a plain path from a file that contains it as its only content, with trailing newlines trimmed.
pub fn from_plain_file(path: &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: &std::path::Path) -> Result<PathBuf, from_gitdir_file::Error> {
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(std::ffi::OsStr::to_str) == Some(DOT_GIT_DIR) {
path.pop();
}
path
}

View File

@@ -0,0 +1,153 @@
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.
/// It could also be non-bare with a worktree configured using git configuration, or no worktree at all despite
/// not being bare (due to mis-configuration for example).
///
/// Note that it might still have linked work-trees which can be accessed later, 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: PathBuf, kind: Kind, current_dir: &std::path::Path) -> Option<Self> {
let cwd = current_dir;
let normalize_on_trailing_dot_dot = |dir: PathBuf| -> Option<PathBuf> {
if !matches!(dir.components().next_back(), Some(std::path::Component::ParentDir)) {
dir
} else {
gix_path::normalize(dir.into(), cwd)?.into_owned()
}
.into()
};
match kind {
Kind::Submodule { git_dir } => Path::LinkedWorkTree {
git_dir: gix_path::normalize(git_dir.into(), 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 = if dir.as_os_str().is_empty() {
PathBuf::from(".")
} else {
dir
};
Path::WorkTree(work_dir)
}
},
Kind::PossiblyBare => 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::PossiblyBare,
}
}
/// 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.
///
/// Also note that due to optimizing for performance and *just* making an educated *guess in some situations*,
/// we may consider a non-bare repository bare if it doesn't have an index yet due to be freshly initialized.
/// The caller has to handle this, typically by reading the configuration.
///
/// It could also be a directory which is non-bare by configuration, but is *not* named `.git`.
/// Unusual, but it's possible that a worktree is configured via `core.worktree`.
PossiblyBare,
/// 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::PossiblyBare)
}
}

View File

@@ -0,0 +1,204 @@
mod types;
pub use types::{Error, Options};
mod util;
pub(crate) mod function {
use std::{borrow::Cow, ffi::OsStr, path::Path};
use gix_sec::Trust;
use super::{Error, Options};
#[cfg(unix)]
use crate::upwards::util::device_id;
use crate::{
is::git_with_metadata as is_git_with_metadata,
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: &Path,
Options {
required_trust,
ceiling_dirs,
match_ceiling_dir_or_error,
cross_fs,
current_dir,
dot_git_only,
}: 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 that contain '..'.)
let cwd = current_dir.map_or_else(
|| {
// The paths we return are relevant to the repository, but at this time it's impossible to know
// what `core.precomposeUnicode` is going to be. Hence, the one using these paths will have to
// transform the paths as needed, because we can't. `false` means to leave the obtained path as is.
gix_fs::current_dir(false).map(Cow::Owned)
},
|cwd| Ok(Cow::Borrowed(cwd)),
)?;
#[cfg(windows)]
let directory = dunce::simplified(directory);
let dir = gix_path::normalize(directory.into(), 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;
let mut cursor_metadata = Some(dir_metadata);
'outer: loop {
if max_height.is_some_and(|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 = cursor_metadata.take().map_or_else(
|| {
if cursor.as_os_str().is_empty() {
Path::new(".")
} else {
cursor.as_ref()
}
.metadata()
.map_err(|_| Error::InaccessibleDirectory { path: cursor.clone() })
},
Ok,
)?;
if device_id(&metadata) != initial_device {
return Err(Error::NoGitRepositoryWithinFs {
path: dir.into_owned(),
limit: cursor.clone(),
});
}
cursor_metadata = Some(metadata);
}
let mut cursor_metadata_backup = None;
let started_as_dot_git = cursor.file_name() == Some(OsStr::new(DOT_GIT_DIR));
let dir_manipulation = if dot_git_only { &[true] as &[_] } else { &[true, false] };
for append_dot_git in dir_manipulation {
if *append_dot_git && !started_as_dot_git {
cursor.push(DOT_GIT_DIR);
cursor_metadata_backup = cursor_metadata.take();
}
if let Ok(kind) = match cursor_metadata.take() {
Some(metadata) => is_git_with_metadata(&cursor, metadata, &cwd),
None => 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.as_ref()).ok_or_else(
|| Error::InvalidInput {
directory: directory.into(),
},
)?,
trust,
));
}
None => {
break 'outer Err(Error::NoTrustedGitRepository {
path: dir.into_owned(),
candidate: cursor,
required: required_trust,
})
}
}
}
// Usually `.git` (started_as_dot_git == true) will be a git dir, but if not we can quickly skip over it.
if *append_dot_git || started_as_dot_git {
cursor.pop();
if let Some(metadata) = cursor_metadata_backup.take() {
cursor_metadata = Some(metadata);
}
}
}
if cursor.as_os_str().is_empty() || cursor.as_os_str() == OsStr::new(".") {
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 | 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.clone().into(), 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: &Path) -> Result<(crate::repository::Path, Trust), Error> {
discover_opts(directory, Default::default())
}
}

View File

@@ -0,0 +1,197 @@
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 not find a git repository in '{}' or in any of its parents", .path.display())]
NoGitRepository { path: PathBuf },
#[error("Could not 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 not 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 not 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 true, limit discovery to `.git` directories.
///
/// This will fail to find typical bare repositories, but would find them if they happen to be named `.git`.
/// Use this option if repos with worktrees are the only kind of repositories you are interested in for
/// optimal discovery performance.
pub dot_git_only: 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.
/// Note that the path here might or might not contained decomposed unicode, which may end up in a path
/// relevant us, like the git-dir or the worktree-dir. However, when opening the repository, it will
/// change decomposed unicode to precomposed unicode based on the value of `core.precomposeUnicode`, and we
/// don't have to deal with that value here just yet.
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,
dot_git_only: 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 `git-config` style boolean values.
// 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
}
}
/// 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()
}
}

View File

@@ -0,0 +1,77 @@
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(std::ffi::OsStr::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_n("..", 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.into(), cwd)?;
if !ceiling_dir.is_absolute() {
ceiling_dir = gix_path::normalize(cwd.join(ceiling_dir.as_ref()).into(), 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()
}

View File

@@ -0,0 +1,147 @@
use crate::upwards::repo_path;
#[cfg(target_os = "macos")]
#[test]
fn verify_on_exfat() -> crate::Result<()> {
use std::process::Command;
use gix_discover::repository::Kind;
let fixtures = gix_testtools::scripted_fixture_read_only("make_exfat_repo_darwin.sh")?;
let mount_point = tempfile::tempdir()?;
let _cleanup = {
// Mount dmg file
Command::new("hdiutil")
.args(["attach", "-nobrowse", "-mountpoint"])
.arg(mount_point.path())
.arg(fixtures.as_path().join("exfat_repo.dmg"))
.status()?;
// Ensure that the mount point is always cleaned up
defer::defer({
let mount_point = mount_point.path().to_owned();
move || {
Command::new("hdiutil")
.arg("detach")
.arg(&mount_point)
.status()
.expect("detach temporary test dmg filesystem successfully");
}
})
};
let is_git = gix_discover::is_git(&mount_point.path().join(".git"));
assert!(
matches!(is_git, Ok(Kind::WorkTree { linked_git_dir: None })),
"repo on exFAT is recognized as a valid worktree repo"
);
Ok(())
}
#[test]
fn missing_configuration_file_is_not_a_dealbreaker_in_bare_repo() -> crate::Result {
for name in ["bare-no-config-after-init.git", "bare-no-config.git"] {
let repo = repo_path()?.join(name);
let kind = gix_discover::is_git(&repo)?;
assert_eq!(kind, gix_discover::repository::Kind::PossiblyBare);
}
Ok(())
}
#[test]
fn bare_repo_with_index_file_looks_still_looks_like_bare() -> crate::Result {
let repo = repo_path()?.join("bare-with-index.git");
let kind = gix_discover::is_git(&repo)?;
assert_eq!(kind, gix_discover::repository::Kind::PossiblyBare);
Ok(())
}
#[test]
fn non_bare_repo_without_workdir() -> crate::Result {
let repo = repo_path()?.join("non-bare-without-worktree");
let kind = gix_discover::is_git(&repo)?;
assert_eq!(
kind,
gix_discover::repository::Kind::PossiblyBare,
"typically due to misconfiguration, but worktrees could also be configured in Git configuration"
);
Ok(())
}
#[test]
fn non_bare_repo_without_workdir_with_index() -> crate::Result {
let repo = repo_path()?.join("non-bare-without-worktree-with-index");
let kind = gix_discover::is_git(&repo)?;
assert_eq!(
kind,
gix_discover::repository::Kind::PossiblyBare,
"this means it has to be validated later"
);
Ok(())
}
#[test]
fn bare_repo_with_index_file_looks_still_looks_like_bare_if_it_was_renamed() -> crate::Result {
for repo_name in ["bare-with-index-bare", "bare-with-index-no-config-bare"] {
let repo = repo_path()?.join(repo_name);
let kind = gix_discover::is_git(&repo)?;
assert_eq!(kind, gix_discover::repository::Kind::PossiblyBare);
}
Ok(())
}
#[test]
fn no_bare_repo_without_index_file_looks_like_worktree() -> crate::Result {
let repo = repo_path()?.join("non-bare-without-index").join(".git");
let kind = gix_discover::is_git(&repo)?;
assert_eq!(kind, gix_discover::repository::Kind::WorkTree { linked_git_dir: None });
Ok(())
}
#[test]
fn missing_configuration_file_is_not_a_dealbreaker_in_nonbare_repo() -> crate::Result {
for name in ["worktree-no-config-after-init/.git", "worktree-no-config/.git"] {
let repo = repo_path()?.join(name);
let kind = gix_discover::is_git(&repo)?;
assert_eq!(kind, gix_discover::repository::Kind::WorkTree { linked_git_dir: None });
}
Ok(())
}
#[test]
fn split_worktree_using_configuration() -> crate::Result {
for name in [
"repo-with-worktree-in-config",
"repo-with-worktree-in-config-unborn",
"repo-with-worktree-in-config-unborn-no-worktreedir",
"repo-with-worktree-in-config-unborn-empty-worktreedir",
"repo-with-worktree-in-config-unborn-worktreedir-missing-value",
] {
let repo = repo_path()?.join(name);
let kind = gix_discover::is_git(&repo)?;
assert_eq!(
kind,
gix_discover::repository::Kind::PossiblyBare,
"{name}: we think these are bare as we don't read the configuration in this case - \
a shortcoming to favor performance which still comes out correct in `src`"
);
}
Ok(())
}
#[test]
fn reftable() -> crate::Result {
let repo_path = match gix_testtools::scripted_fixture_read_only("make_reftable_repo.sh") {
Ok(root) => root.join("reftable-clone/.git"),
Err(_) if *gix_testtools::GIT_VERSION < (2, 44, 0) => {
eprintln!("Fixture script failure ignored as it looks like Git isn't recent enough.");
return Ok(());
}
Err(err) => panic!("{err}"),
};
let kind = gix_discover::is_git(&repo_path)?;
assert_eq!(kind, gix_discover::repository::Kind::WorkTree { linked_git_dir: None });
Ok(())
}

View File

@@ -0,0 +1,6 @@
pub use gix_testtools::Result;
mod is_git;
mod parse;
mod path;
mod upwards;

View File

@@ -0,0 +1,42 @@
use std::path::Path;
use gix_discover::parse;
#[test]
fn valid() -> crate::Result {
assert_eq!(parse::gitdir(b"gitdir: a")?, Path::new("a"));
assert_eq!(parse::gitdir(b"gitdir: relative/path")?, Path::new("relative/path"));
assert_eq!(parse::gitdir(b"gitdir: ./relative/path")?, Path::new("./relative/path"));
assert_eq!(parse::gitdir(b"gitdir: /absolute/path\n")?, Path::new("/absolute/path"));
assert_eq!(
parse::gitdir(b"gitdir: C:/hello/there\r\n")?,
Path::new("C:/hello/there")
);
Ok(())
}
#[test]
fn invalid() {
assert!(
matches!(
parse::gitdir(b"gitdir:"),
Err(parse::gitdir::Error::InvalidFormat { .. })
),
"missing prefix"
);
assert!(
matches!(
parse::gitdir(b"bogus: foo"),
Err(parse::gitdir::Error::InvalidFormat { .. })
),
"invalid prefix"
);
assert!(
matches!(
parse::gitdir(b"gitdir: "),
Err(parse::gitdir::Error::InvalidFormat { .. })
),
"empty path"
);
}

View File

@@ -0,0 +1,67 @@
mod from_git_dir_file {
use std::{
io::Write,
path::{Path, PathBuf},
};
use gix_testtools::tempfile::NamedTempFile;
#[cfg(not(windows))]
#[test]
fn absolute_path_unix() -> crate::Result {
let (path, _) = write_and_read(b"gitdir: /absolute/path/.git")?;
assert_eq!(path, Path::new("/absolute/path/.git"));
Ok(())
}
#[cfg(windows)]
#[test]
fn absolute_path_windows() -> crate::Result {
let (path, _) = write_and_read(b"gitdir: C:/absolute/path/.git")?;
assert_eq!(path, Path::new("C:/absolute/path/.git"));
let (path, _) = write_and_read(br"gitdir: C:\absolute\path\.git")?;
assert_eq!(path, Path::new(r"C:\absolute\path\.git"));
Ok(())
}
#[test]
fn relative_path_is_made_absolute_relative_to_containing_dir() -> crate::Result {
let (path, gitdir_file) = write_and_read(b"gitdir: relative/path")?;
assert_eq!(path, gitdir_file.parent().unwrap().join(Path::new("relative/path")));
Ok(())
}
fn write_and_read(content: &[u8]) -> crate::Result<(PathBuf, PathBuf)> {
let file = gitdir_with_content(content)?;
Ok((gix_discover::path::from_gitdir_file(file.path())?, file.path().into()))
}
fn gitdir_with_content(content: &[u8]) -> std::io::Result<NamedTempFile> {
let mut file = tempfile::NamedTempFile::new()?;
file.write_all(content)?;
Ok(file)
}
}
#[test]
fn repository_kind() {
use gix_discover::path::{repository_kind, RepositoryKind::*};
assert_eq!(repository_kind("hello".as_ref()), None);
assert_eq!(repository_kind(".git".as_ref()), Some(Common));
assert_eq!(repository_kind("foo/.git".as_ref()), Some(Common));
assert_eq!(
repository_kind("foo/other.git".as_ref()),
None,
"it makes no assumption beyond the standard name, nor does it consider suffixes"
);
assert_eq!(repository_kind(".git/modules".as_ref()), None);
assert_eq!(
repository_kind(".git/modules/actual-submodule".as_ref()),
Some(Submodule)
);
assert_eq!(
repository_kind(".git/worktrees/actual-worktree".as_ref()),
Some(LinkedWorktree)
);
}

View File

@@ -0,0 +1,209 @@
use std::path::Path;
use gix_discover::upwards::Options;
use crate::upwards::repo_path;
fn assert_repo_is_current_workdir(path: gix_discover::repository::Path, work_dir: &Path) {
assert_eq!(
path.into_repository_and_work_tree_directories()
.1
.expect("work dir")
.file_name(),
work_dir.file_name()
);
}
#[test]
fn git_dir_candidate_within_ceiling_allows_discovery() -> crate::Result {
let work_dir = repo_path()?;
let dir = work_dir.join("some/very/deeply/nested/subdir");
let (repo_path, _trust) = gix_discover::upwards_opts(
&dir,
Options {
ceiling_dirs: vec![work_dir.clone()],
..Default::default()
},
)
.expect("ceiling dir should allow us to discover the repo");
assert_repo_is_current_workdir(repo_path, &work_dir);
Ok(())
}
#[test]
fn ceiling_dir_is_ignored_if_we_are_standing_on_the_ceiling_and_no_match_is_required() -> crate::Result {
let work_dir = repo_path()?;
let dir = work_dir.join("some/very/deeply/nested/subdir");
// the ceiling dir is equal to the input dir, which itself doesn't contain a repository.
// But we can ignore that just like git does (see https://github.com/GitoxideLabs/gitoxide/pull/723 for more information)
// and imagine us to 'stand on the ceiling', hence we are already past it.
let (repo_path, _trust) = gix_discover::upwards_opts(
&dir.clone(),
Options {
ceiling_dirs: vec![dir],
match_ceiling_dir_or_error: false,
..Default::default()
},
)
.expect("ceiling dir should be skipped");
assert_repo_is_current_workdir(repo_path, &work_dir);
Ok(())
}
#[test]
fn discovery_fails_if_we_require_a_matching_ceiling_dir_but_are_standing_on_it() -> crate::Result {
let work_dir = repo_path()?;
let dir = work_dir.join("some/very/deeply/nested/subdir");
let err = gix_discover::upwards_opts(
&dir.clone(),
Options {
ceiling_dirs: vec![dir],
match_ceiling_dir_or_error: true,
..Default::default()
},
)
.unwrap_err();
assert!(
matches!(err, gix_discover::upwards::Error::NoMatchingCeilingDir),
"since standing on the ceiling dir doesn't match it, we get exactly the semantically correct error"
);
Ok(())
}
#[test]
fn ceiling_dir_limits_are_respected_and_prevent_discovery() -> crate::Result {
let work_dir = repo_path()?;
let dir = work_dir.join("some/very/deeply/nested/subdir");
let err = gix_discover::upwards_opts(
&dir,
Options {
ceiling_dirs: vec![work_dir.join("some/../some")],
..Default::default()
},
)
.expect_err("ceiling dir prevents discovery as it ends on level too early, and they are also absolutized");
assert!(matches!(
err,
gix_discover::upwards::Error::NoGitRepositoryWithinCeiling { ceiling_height: 5, .. }
));
Ok(())
}
#[test]
fn no_matching_ceiling_dir_error_can_be_suppressed() -> crate::Result {
let work_dir = repo_path()?;
let dir = work_dir.join("some/very/deeply/nested/subdir");
let (repo_path, _trust) = gix_discover::upwards_opts(
&dir,
Options {
match_ceiling_dir_or_error: false,
ceiling_dirs: vec![
work_dir.canonicalize()?,
work_dir.join("some/very/deeply/nested/subdir/too-deep"),
work_dir.join("some/very/deeply/nested/unrelated-dir"),
work_dir.join("a/completely/unrelated/dir"),
],
..Default::default()
},
)
.expect("ceiling dir should allow us to discover the repo");
assert_repo_is_current_workdir(repo_path, &work_dir);
Ok(())
}
#[test]
fn more_restrictive_ceiling_dirs_overrule_less_restrictive_ones() -> crate::Result {
let work_dir = repo_path()?;
let dir = work_dir.join("some/very/deeply/nested/subdir");
let err = gix_discover::upwards_opts(
&dir,
Options {
ceiling_dirs: vec![work_dir.clone(), work_dir.join("some")],
..Default::default()
},
)
.expect_err("more restrictive ceiling dirs overrule less restrictive ones");
assert!(matches!(
err,
gix_discover::upwards::Error::NoGitRepositoryWithinCeiling { ceiling_height: 5, .. }
));
Ok(())
}
#[test]
fn ceiling_dirs_are_not_processed_differently_than_the_git_dir_candidate() -> crate::Result {
let work_dir = repo_path()?;
let dir = work_dir.join("some/very/deeply/nested/subdir/../../../../../..");
let (repo_path, _trust) = gix_discover::upwards_opts(
&dir,
Options {
match_ceiling_dir_or_error: false,
ceiling_dirs: vec![Path::new("./some").into()],
..Default::default()
},
)
.expect("the repo can be discovered because the relative ceiling doesn't _look_ like it has something to do with the git dir candidate");
assert_ne!(
&repo_path.as_ref().canonicalize()?,
&work_dir,
"a relative path that climbs above the test repo should yield the gitoxide repo"
);
Ok(())
}
#[test]
fn no_matching_ceiling_dirs_errors_by_default() -> crate::Result {
let relative_work_dir = repo_path()?;
let dir = relative_work_dir.join("some");
let res = gix_discover::upwards_opts(
&dir,
Options {
ceiling_dirs: vec!["/something/somewhere".into()],
..Default::default()
},
);
assert!(
matches!(res, Err(gix_discover::upwards::Error::NoMatchingCeilingDir)),
"the canonicalized ceiling dir doesn't have the same root as the git dir candidate, and can never match."
);
Ok(())
}
#[test]
fn ceilings_are_adjusted_to_match_search_dir() -> crate::Result {
let relative_work_dir = repo_path()?;
let cwd = std::env::current_dir()?;
let absolute_ceiling_dir = gix_path::realpath_opts(&relative_work_dir, &cwd, 8)?;
let dir = relative_work_dir.join("some");
assert!(dir.is_relative());
let (repo_path, _trust) = gix_discover::upwards_opts(
&dir,
Options {
ceiling_dirs: vec![absolute_ceiling_dir],
..Default::default()
},
)?;
assert_repo_is_current_workdir(repo_path, &relative_work_dir);
assert!(relative_work_dir.is_relative());
let absolute_dir = gix_path::realpath_opts(relative_work_dir.join("some").as_ref(), &cwd, 8)?;
let (repo_path, _trust) = gix_discover::upwards_opts(
&absolute_dir,
Options {
ceiling_dirs: vec![relative_work_dir.clone()],
..Default::default()
},
)?;
assert_repo_is_current_workdir(repo_path, &relative_work_dir);
Ok(())
}

View File

@@ -0,0 +1,430 @@
use std::path::PathBuf;
use gix_discover::repository::Kind;
fn expected_trust() -> gix_sec::Trust {
if std::env::var_os("GIX_TEST_EXPECT_REDUCED_TRUST").is_some() {
gix_sec::Trust::Reduced
} else {
gix_sec::Trust::Full
}
}
mod ceiling_dirs;
#[test]
fn from_bare_git_dir() -> crate::Result {
let dir = repo_path()?.join("bare.git");
let (path, trust) = gix_discover::upwards(&dir)?;
assert_eq!(path.as_ref(), dir, "the bare .git dir is directly returned");
assert_eq!(path.kind(), Kind::PossiblyBare);
assert_eq!(trust, expected_trust());
Ok(())
}
#[test]
fn from_bare_with_index() -> crate::Result {
let dir = repo_path()?.join("bare-with-index.git");
let (path, trust) = gix_discover::upwards(&dir)?;
assert_eq!(path.as_ref(), dir, "the bare .git dir is directly returned");
assert_eq!(path.kind(), Kind::PossiblyBare);
assert_eq!(trust, expected_trust());
Ok(())
}
#[test]
fn from_non_bare_without_index() -> crate::Result {
let dir = repo_path()?.join("non-bare-without-index");
let (path, trust) = gix_discover::upwards(&dir)?;
assert_eq!(path.as_ref(), dir, "now we refer to a worktree");
assert_eq!(path.kind(), Kind::WorkTree { linked_git_dir: None });
assert_eq!(trust, expected_trust());
Ok(())
}
#[test]
fn from_bare_git_dir_without_config_file() -> crate::Result {
for name in ["bare-no-config.git", "bare-no-config-after-init.git"] {
let dir = repo_path()?.join(name);
let (path, trust) = gix_discover::upwards(&dir)?;
assert_eq!(path.as_ref(), dir, "the bare .git dir is directly returned");
assert_eq!(path.kind(), Kind::PossiblyBare);
assert_eq!(trust, expected_trust());
}
Ok(())
}
#[test]
fn from_inside_bare_git_dir() -> crate::Result {
let git_dir = repo_path()?.join("bare.git");
let dir = git_dir.join("objects");
let (path, trust) = gix_discover::upwards(&dir)?;
assert_eq!(
path.as_ref(),
git_dir,
"the bare .git dir is found while traversing upwards"
);
assert_eq!(path.kind(), Kind::PossiblyBare);
assert_eq!(trust, expected_trust());
Ok(())
}
#[test]
fn from_git_dir() -> crate::Result {
let dir = repo_path()?.join(".git");
let (path, trust) = gix_discover::upwards(&dir)?;
assert_eq!(path.kind(), Kind::WorkTree { linked_git_dir: None });
assert_eq!(
path.into_repository_and_work_tree_directories().0,
dir,
"the .git dir is directly returned if valid"
);
assert_eq!(trust, expected_trust());
Ok(())
}
#[test]
fn from_working_dir() -> crate::Result {
let dir = repo_path()?;
let (path, trust) = gix_discover::upwards(&dir)?;
assert_eq!(path.as_ref(), dir, "a working tree dir yields the git dir");
assert_eq!(path.kind(), Kind::WorkTree { linked_git_dir: None });
assert_eq!(trust, expected_trust());
Ok(())
}
#[test]
fn from_working_dir_no_config() -> crate::Result {
for name in ["worktree-no-config-after-init", "worktree-no-config"] {
let dir = repo_path()?.join(name);
let (path, trust) = gix_discover::upwards(&dir)?;
assert_eq!(path.kind(), Kind::WorkTree { linked_git_dir: None });
assert_eq!(path.as_ref(), dir, "a working tree dir yields the git dir");
assert_eq!(trust, expected_trust());
}
Ok(())
}
#[test]
fn from_nested_dir() -> crate::Result {
let working_dir = repo_path()?;
let dir = working_dir.join("some/very/deeply/nested/subdir");
let (path, trust) = gix_discover::upwards(&dir)?;
assert_eq!(path.kind(), Kind::WorkTree { linked_git_dir: None });
assert_eq!(path.as_ref(), working_dir, "a working tree dir yields the git dir");
assert_eq!(trust, expected_trust());
Ok(())
}
#[test]
fn from_dir_with_dot_dot() -> crate::Result {
// This would be neater if we could just change the actual working directory,
// but Rust tests run in parallel by default so we'd interfere with other tests.
// Instead ensure it finds the gitoxide repo instead of a test repo if we crawl
// up far enough. (This tests that `discover::existing` canonicalizes paths before
// exploring ancestors.)
let working_dir = repo_path()?;
let dir = working_dir.join("some/very/deeply/nested/subdir/../../../../../..");
let (path, trust) = gix_discover::upwards(&dir)?;
assert_ne!(
path.as_ref().canonicalize()?,
working_dir.canonicalize()?,
"a relative path that climbs above the test repo should yield the parent-gitoxide repo"
);
// If the parent repo is actually a main worktree, we can make more assertions. If it is not,
// it will use an absolute paths and we have to bail.
if path.as_ref() == std::path::Path::new("..") {
assert_eq!(path.kind(), Kind::WorkTree { linked_git_dir: None });
assert_eq!(
path.as_ref(),
std::path::Path::new(".."),
"there is only the minimal amount of relative path components to see this worktree"
);
} else {
assert!(
path.as_ref().is_absolute(),
"worktree paths are absolute and the parent repo is one"
);
assert!(matches!(
path.kind(),
Kind::WorkTree {
linked_git_dir: Some(_)
}
));
}
assert_eq!(trust, expected_trust());
Ok(())
}
#[test]
fn from_nested_dir_inside_a_git_dir() -> crate::Result {
let working_dir = repo_path()?;
let dir = working_dir.join(".git").join("objects");
let (path, trust) = gix_discover::upwards(&dir)?;
assert_eq!(path.kind(), Kind::WorkTree { linked_git_dir: None });
assert_eq!(path.as_ref(), working_dir, "we find .git directories on the way");
assert_eq!(trust, expected_trust());
Ok(())
}
#[test]
fn from_non_existing_worktree() {
let top_level_repo = repo_path().unwrap();
let (path, _trust) = gix_discover::upwards(&top_level_repo.join("worktrees/b-private-dir-deleted")).unwrap();
assert_eq!(path, gix_discover::repository::Path::WorkTree(top_level_repo.clone()));
let (path, _trust) =
gix_discover::upwards(&top_level_repo.join("worktrees/from-bare/d-private-dir-deleted")).unwrap();
assert_eq!(path, gix_discover::repository::Path::WorkTree(top_level_repo));
}
#[test]
fn from_existing_worktree_inside_dot_git() {
let top_level_repo = repo_path().unwrap();
let (path, _trust) = gix_discover::upwards(&top_level_repo.join(".git/worktrees/a")).unwrap();
let suffix = std::path::Path::new(top_level_repo.file_name().unwrap())
.join("worktrees")
.join("a");
assert!(
matches!(path, gix_discover::repository::Path::LinkedWorkTree { work_dir, .. } if work_dir.ends_with(suffix)),
"we can handle to start from within a (somewhat partial) worktree git dir"
);
}
#[test]
fn from_non_existing_worktree_inside_dot_git() {
let top_level_repo = repo_path().unwrap();
let (path, _trust) = gix_discover::upwards(&top_level_repo.join(".git/worktrees/c-worktree-deleted")).unwrap();
let suffix = std::path::Path::new(top_level_repo.file_name().unwrap())
.join("worktrees")
.join("c-worktree-deleted");
assert!(
matches!(path, gix_discover::repository::Path::LinkedWorkTree { work_dir, .. } if work_dir.ends_with(suffix)),
"it's no problem if work-dirs don't exist - this can be discovered later and a lot of operations are possible anyway."
);
}
#[test]
fn from_existing_worktree() -> crate::Result {
let top_level_repo = repo_path()?;
for (discover_path, expected_worktree_path, expected_git_dir) in [
(top_level_repo.join("worktrees/a"), "worktrees/a", ".git/worktrees/a"),
(
top_level_repo.join("worktrees/from-bare/c"),
"worktrees/from-bare/c",
"bare.git/worktrees/c",
),
] {
let (path, trust) = gix_discover::upwards(&discover_path)?;
assert!(matches!(path, gix_discover::repository::Path::LinkedWorkTree { .. }));
assert_eq!(trust, expected_trust());
let (git_dir, worktree) = path.into_repository_and_work_tree_directories();
assert_eq!(
git_dir.strip_prefix(gix_path::realpath(&top_level_repo).unwrap()),
Ok(std::path::Path::new(expected_git_dir)),
"we don't skip over worktrees and discover their git dir (gitdir is absolute in file)"
);
let worktree = worktree.expect("linked worktree is set");
assert_eq!(
worktree.strip_prefix(&top_level_repo),
Ok(std::path::Path::new(expected_worktree_path)),
"the worktree path is the .git file's directory"
);
}
Ok(())
}
#[cfg(target_os = "macos")]
#[test]
fn cross_fs() -> crate::Result {
use std::{os::unix::fs::symlink, process::Command};
use gix_discover::upwards::Options;
if gix_testtools::is_ci::cached() {
// Don't run on CI as it's too slow there, resource busy, it fails more often than it succeeds by now.
return Ok(());
}
let top_level_repo = gix_testtools::scripted_fixture_writable("make_basic_repo.sh")?;
let _cleanup = {
// Create an empty dmg file
let dmg_location = tempfile::tempdir()?;
let dmg_file = dmg_location.path().join("temp.dmg");
Command::new("hdiutil")
.args(["create", "-size", "1m"])
.arg(&dmg_file)
.status()?;
// Mount dmg file into temporary location
let mount_point = tempfile::tempdir()?;
Command::new("hdiutil")
.args(["attach", "-nobrowse", "-mountpoint"])
.arg(mount_point.path())
.arg(&dmg_file)
.status()?;
// Symlink the mount point into the repo
symlink(mount_point.path(), top_level_repo.path().join("remote"))?;
// Ensure that the mount point is always cleaned up
defer::defer({
let arg = mount_point.path().to_owned();
move || {
Command::new("hdiutil")
.arg("detach")
.arg(arg)
.status()
.expect("detach temporary test dmg filesystem successfully");
}
})
};
let res = gix_discover::upwards(&top_level_repo.path().join("remote"))
.expect_err("the cross-fs option should prevent us from discovering the repo");
assert!(matches!(
res,
gix_discover::upwards::Error::NoGitRepositoryWithinFs { .. }
));
let (repo_path, _trust) = gix_discover::upwards_opts(
&top_level_repo.path().join("remote"),
Options {
cross_fs: true,
..Default::default()
},
)
.expect("the cross-fs option should allow us to discover the repo");
assert_eq!(
repo_path
.into_repository_and_work_tree_directories()
.1
.expect("work dir")
.file_name(),
top_level_repo.path().file_name()
);
Ok(())
}
#[test]
fn do_not_shorten_absolute_paths() -> crate::Result {
let top_level_repo = repo_path()?.canonicalize().expect("repo path exists");
let (repo_path, _trust) = gix_discover::upwards(&top_level_repo).expect("we can discover the repo");
match repo_path {
gix_discover::repository::Path::WorkTree(work_dir) => {
assert!(work_dir.is_absolute());
}
_ => panic!("expected worktree path"),
}
Ok(())
}
mod dot_git_only {
use crate::upwards::repo_path;
fn find_dot_git(base: impl AsRef<std::path::Path>) -> gix_discover::repository::Path {
gix_discover::upwards_opts(
base.as_ref(),
gix_discover::upwards::Options {
dot_git_only: true,
..Default::default()
},
)
.expect("we can discover the repo")
.0
}
fn assert_is_worktree_at(repo_path: gix_discover::repository::Path, expected: impl AsRef<std::path::Path>) {
match repo_path {
gix_discover::repository::Path::WorkTree(work_dir) => {
assert_eq!(work_dir, expected.as_ref());
}
_ => panic!("expected worktree path"),
}
}
#[test]
fn succeeds_in_worktree_dir() -> crate::Result {
let top_level_repo = repo_path()?;
for base in [
top_level_repo.join("some/very/deeply/nested/subdir"),
top_level_repo.clone(),
] {
let repo_path = find_dot_git(base);
assert_is_worktree_at(repo_path, &top_level_repo);
}
Ok(())
}
#[test]
fn succeeds_from_within_dot_git_dir() -> crate::Result {
let top_level_repo = repo_path()?;
for inside_git_dir in [top_level_repo.join(".git"), top_level_repo.join(".git").join("refs")] {
let repo_path = find_dot_git(inside_git_dir);
assert_is_worktree_at(repo_path, &top_level_repo);
}
Ok(())
}
#[test]
fn bare_repos_are_ignored() -> crate::Result {
let top_level_repo = repo_path()?;
for bare_dir in [
top_level_repo.join("bare.git"),
top_level_repo.join("bare.git").join("refs"),
] {
let repo_path = find_dot_git(bare_dir);
assert_is_worktree_at(repo_path, &top_level_repo);
}
Ok(())
}
}
mod submodules {
#[test]
fn by_their_worktree_checkout() -> crate::Result {
let dir = gix_testtools::scripted_fixture_read_only("make_submodules.sh")?;
let parent = dir.join("with-submodules");
let modules = parent.join(".git").join("modules");
for module in ["m1", "dir/m1"] {
let submodule_m1_workdir = parent.join(module);
let submodule_m1_gitdir = modules.join(module);
let (path, _trust) = gix_discover::upwards(&submodule_m1_workdir)?;
assert!(
matches!(path, gix_discover::repository::Path::LinkedWorkTree{ref work_dir, ref git_dir} if work_dir == &submodule_m1_workdir && git_dir == &submodule_m1_gitdir),
"{path:?} should match {submodule_m1_workdir:?} {submodule_m1_gitdir:?}"
);
let (path, _trust) = gix_discover::upwards(&submodule_m1_workdir.join("subdir"))?;
assert!(
matches!(path, gix_discover::repository::Path::LinkedWorkTree{ref work_dir, ref git_dir} if work_dir == &submodule_m1_workdir && git_dir == &submodule_m1_gitdir),
"{path:?} should match {submodule_m1_workdir:?} {submodule_m1_gitdir:?}"
);
}
Ok(())
}
#[test]
fn by_their_module_git_dir() -> crate::Result {
let dir = gix_testtools::scripted_fixture_read_only("make_submodules.sh")?;
let modules = dir.join("with-submodules").join(".git").join("modules");
for module in ["m1", "dir/m1"] {
let submodule_m1_gitdir = modules.join(module);
let (path, _trust) = gix_discover::upwards(&submodule_m1_gitdir)?;
assert!(
matches!(path, gix_discover::repository::Path::Repository(ref dir) if dir == &submodule_m1_gitdir),
"{path:?} should match {submodule_m1_gitdir:?}"
);
}
Ok(())
}
}
pub(crate) fn repo_path() -> crate::Result<PathBuf> {
gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")
}

View File

@@ -0,0 +1 @@
/make_basic_repo.tar

Binary file not shown.

View File

@@ -0,0 +1,105 @@
#!/usr/bin/env bash
set -eu -o pipefail
git init -q
git checkout -b main
touch this
git add this
git commit -q -m c1
echo hello >> this
git commit -q -am c2
mkdir subdir
mkdir -p some/very/deeply/nested/subdir
git clone --bare --shared . bare.git
git clone --bare --shared . non-bare-without-worktree
(cd non-bare-without-worktree
git config core.bare false
)
git clone --bare --shared . non-bare-without-worktree-with-index
(cd non-bare-without-worktree
git config core.bare false
cp ../.git/index .
)
git worktree add worktrees/a
git worktree add worktrees/b-private-dir-deleted
rm -R .git/worktrees/b-private-dir-deleted
git worktree add worktrees/c-worktree-deleted
rm -R worktrees/c-worktree-deleted
(cd bare.git
git worktree add ../worktrees/from-bare/c
git worktree add ../worktrees/from-bare/d-private-dir-deleted
rm -R ./worktrees/d-private-dir-deleted
)
git clone --bare --shared . bare-no-config.git
(cd bare-no-config.git
rm config
)
git init --bare bare-no-config-after-init.git
(cd bare-no-config-after-init.git
rm config
)
git clone --shared . worktree-no-config
(cd worktree-no-config
rm .git/config
)
git init worktree-no-config-after-init
(cd worktree-no-config-after-init
rm .git/config
)
git init --bare bare-with-index.git
(cd bare-with-index.git
touch index
)
git init --bare bare-with-index-bare
(cd bare-with-index-bare
touch index
)
git init --bare bare-with-index-no-config-bare
(cd bare-with-index-no-config-bare
touch index
rm config
)
git init non-bare-without-index
(cd non-bare-without-index
touch this
git add this
git commit -m "init"
rm .git/index
)
git --git-dir=repo-with-worktree-in-config-unborn-no-worktreedir --work-tree=does-not-exist-yet init
worktree=repo-with-worktree-in-config-unborn-worktree
git --git-dir=repo-with-worktree-in-config-unborn --work-tree=$worktree init && mkdir $worktree
repo=repo-with-worktree-in-config-unborn-empty-worktreedir
git --git-dir=$repo --work-tree="." init
touch $repo/index
git -C $repo config core.worktree ''
repo=repo-with-worktree-in-config-unborn-worktreedir-missing-value
git --git-dir=$repo init
touch $repo/index
echo " worktree" >> $repo/config
worktree=repo-with-worktree-in-config-worktree
git --git-dir=repo-with-worktree-in-config --work-tree=$worktree init
mkdir $worktree && touch $worktree/file
(cd repo-with-worktree-in-config
git add file
git commit -m "make sure na index exists"
)

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
set -eu -o pipefail
[[ $(uname) == Darwin ]] || exit 1
dmg_file=exfat_repo.dmg
hdiutil create -size 10m -fs exfat -volname "test" $dmg_file
mount=exfat_mount
mkdir $mount
hdiutil attach $dmg_file -nobrowse -mountpoint $mount
(cd $mount
git init -q
git checkout -b main
touch this
git add this
git commit -q -m c1
)
hdiutil detach $mount
rm -R $mount

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -eu -o pipefail
git init -q
git checkout -b main
touch this
git add this
git commit -q -m c1
echo hello >> this
git commit -q -am c2
git clone --ref-format=reftable . reftable-clone

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env bash
set -eu -o pipefail
git init -q module1
(cd module1
touch this
mkdir subdir
touch subdir/that
git add .
git commit -q -m c1
echo hello >> this
git commit -q -am c2
)
git init with-submodules
(cd with-submodules
mkdir dir
touch dir/file
git add dir
git commit -m "init"
git submodule add ../module1 m1
git commit -m "add module 1"
git submodule add ../module1 dir/m1
)

View File

@@ -0,0 +1,193 @@
use std::path::{Path, PathBuf};
use gix_discover::upwards::Options;
use serial_test::serial;
#[test]
#[serial]
fn in_cwd_upwards_from_nested_dir() -> gix_testtools::Result {
let repo = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?;
let _keep = gix_testtools::set_current_dir(repo)?;
for dir in ["subdir", "some/very/deeply/nested/subdir"] {
let (repo_path, _trust) = gix_discover::upwards(Path::new(dir))?;
assert_eq!(
repo_path.kind(),
gix_discover::repository::Kind::WorkTree { linked_git_dir: None },
);
assert_eq!(repo_path.as_ref(), Path::new("."), "{dir}");
}
Ok(())
}
#[test]
#[serial]
fn upwards_bare_repo_with_index() -> gix_testtools::Result {
let repo = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?;
let _keep = gix_testtools::set_current_dir(repo.join("bare-with-index.git"))?;
let (repo_path, _trust) = gix_discover::upwards(".".as_ref())?;
assert_eq!(
repo_path.kind(),
gix_discover::repository::Kind::PossiblyBare,
"bare stays bare, even with index, as it resolves the path as needed in this special case"
);
Ok(())
}
#[test]
#[serial]
fn in_cwd_upwards_bare_repo_without_index() -> gix_testtools::Result {
let repo = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?;
let _keep = gix_testtools::set_current_dir(repo.join("bare.git"))?;
let (repo_path, _trust) = gix_discover::upwards(".".as_ref())?;
assert_eq!(repo_path.kind(), gix_discover::repository::Kind::PossiblyBare);
Ok(())
}
#[test]
#[serial]
fn in_cwd_upwards_nonbare_repo_without_index() -> gix_testtools::Result {
let repo = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?;
let _keep = gix_testtools::set_current_dir(repo.join("non-bare-without-index"))?;
let (repo_path, _trust) = gix_discover::upwards(".".as_ref())?;
assert_eq!(
repo_path.kind(),
gix_discover::repository::Kind::WorkTree { linked_git_dir: None },
);
Ok(())
}
#[test]
#[serial]
fn upwards_with_relative_directories_and_optional_ceiling() -> gix_testtools::Result {
let repo = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh")?;
let _keep = gix_testtools::set_current_dir(repo.join("some"))?;
let cwd = std::env::current_dir()?;
for (search_dir, ceiling_dir_component) in [
(".", ".."),
(".", "./.."),
("./.", "./.."),
(".", "./does-not-exist/../.."),
("./././very/deeply/nested/subdir", ".."),
("very/deeply/nested/subdir", ".."),
] {
let search_dir = Path::new(search_dir);
let ceiling_dir = cwd.join(ceiling_dir_component);
let (repo_path, _trust) = gix_discover::upwards_opts(
search_dir,
Options {
ceiling_dirs: vec![ceiling_dir],
..Default::default()
},
)
.expect("ceiling dir should allow us to discover the repo");
assert_repo_is_current_workdir(repo_path, Path::new(".."));
let (repo_path, _trust) =
gix_discover::upwards_opts(search_dir, Default::default()).expect("without ceiling dir we see the same");
assert_repo_is_current_workdir(repo_path, Path::new(".."));
let (repo_path, _trust) = gix_discover::upwards_opts(
search_dir,
Options {
ceiling_dirs: vec![PathBuf::from("..")],
..Default::default()
},
)
.expect("purely relative ceiling dirs work as well");
assert_repo_is_current_workdir(repo_path, Path::new(".."));
let err = gix_discover::upwards_opts(
search_dir,
Options {
ceiling_dirs: vec![PathBuf::from(".")],
..Default::default()
},
)
.unwrap_err();
if search_dir.parent() == Some(".".as_ref()) || search_dir.parent() == Some("".as_ref()) {
assert!(matches!(err, gix_discover::upwards::Error::NoMatchingCeilingDir));
} else {
assert!(matches!(
err,
gix_discover::upwards::Error::NoGitRepositoryWithinCeiling { .. }
));
}
}
Ok(())
}
#[test]
#[serial]
fn unc_paths_are_handled_on_windows() -> gix_testtools::Result {
let repo = gix_testtools::scripted_fixture_read_only("make_basic_repo.sh").unwrap();
let _keep = gix_testtools::set_current_dir(repo.join("some/very/deeply/nested/subdir")).unwrap();
let cwd = std::env::current_dir().unwrap();
let parent = cwd.parent().unwrap();
// all discoveries should fail, as they'll hit `parent` before finding a git repository.
// dir: normal, ceiling: normal
let res = gix_discover::upwards_opts(
&cwd,
Options {
ceiling_dirs: vec![parent.to_path_buf()],
match_ceiling_dir_or_error: false,
..Default::default()
},
);
assert!(res.is_err(), "{res:?}");
let parent = parent.canonicalize().unwrap();
// dir: normal, ceiling: extended
let res = gix_discover::upwards_opts(
&cwd,
Options {
ceiling_dirs: vec![parent],
match_ceiling_dir_or_error: false,
..Default::default()
},
);
assert!(res.is_err(), "{res:?}");
let cwd = cwd.canonicalize().unwrap();
let parent = cwd.parent().unwrap();
// dir: extended, ceiling: normal
let res = gix_discover::upwards_opts(
&cwd,
Options {
ceiling_dirs: vec![parent.to_path_buf()],
match_ceiling_dir_or_error: false,
..Default::default()
},
);
assert!(res.is_err(), "{res:?}");
let parent = parent.canonicalize().unwrap();
// dir: extended, ceiling: extended
let res = gix_discover::upwards_opts(
&cwd,
Options {
ceiling_dirs: vec![parent],
match_ceiling_dir_or_error: false,
..Default::default()
},
);
assert!(res.is_err(), "{res:?}");
Ok(())
}
fn assert_repo_is_current_workdir(path: gix_discover::repository::Path, work_dir: &Path) {
assert_eq!(
path.into_repository_and_work_tree_directories().1.expect("work dir"),
work_dir,
);
}