mirror of
https://github.com/awfixers-stuff/src.git
synced 2026-03-27 12:56:19 +00:00
create src
This commit is contained in:
1945
src-discover/CHANGELOG.md
Normal file
1945
src-discover/CHANGELOG.md
Normal file
File diff suppressed because it is too large
Load Diff
45
src-discover/Cargo.toml
Normal file
45
src-discover/Cargo.toml
Normal 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
1
src-discover/LICENSE-APACHE
Symbolic link
@@ -0,0 +1 @@
|
||||
../LICENSE-APACHE
|
||||
1
src-discover/LICENSE-MIT
Symbolic link
1
src-discover/LICENSE-MIT
Symbolic link
@@ -0,0 +1 @@
|
||||
../LICENSE-MIT
|
||||
163
src-discover/src/is.rs
Normal file
163
src-discover/src/is.rs
Normal 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
60
src-discover/src/lib.rs
Normal 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
33
src-discover/src/parse.rs
Normal 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
104
src-discover/src/path.rs
Normal 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
|
||||
}
|
||||
153
src-discover/src/repository.rs
Normal file
153
src-discover/src/repository.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
204
src-discover/src/upwards/mod.rs
Normal file
204
src-discover/src/upwards/mod.rs
Normal 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())
|
||||
}
|
||||
}
|
||||
197
src-discover/src/upwards/types.rs
Normal file
197
src-discover/src/upwards/types.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
77
src-discover/src/upwards/util.rs
Normal file
77
src-discover/src/upwards/util.rs
Normal 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()
|
||||
}
|
||||
147
src-discover/tests/discover/is_git.rs
Normal file
147
src-discover/tests/discover/is_git.rs
Normal 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(())
|
||||
}
|
||||
6
src-discover/tests/discover/main.rs
Normal file
6
src-discover/tests/discover/main.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub use gix_testtools::Result;
|
||||
|
||||
mod is_git;
|
||||
mod parse;
|
||||
mod path;
|
||||
mod upwards;
|
||||
42
src-discover/tests/discover/parse.rs
Normal file
42
src-discover/tests/discover/parse.rs
Normal 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"
|
||||
);
|
||||
}
|
||||
67
src-discover/tests/discover/path.rs
Normal file
67
src-discover/tests/discover/path.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
209
src-discover/tests/discover/upwards/ceiling_dirs.rs
Normal file
209
src-discover/tests/discover/upwards/ceiling_dirs.rs
Normal 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(())
|
||||
}
|
||||
430
src-discover/tests/discover/upwards/mod.rs
Normal file
430
src-discover/tests/discover/upwards/mod.rs
Normal 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")
|
||||
}
|
||||
1
src-discover/tests/fixtures/generated-archives/.gitignore
vendored
Normal file
1
src-discover/tests/fixtures/generated-archives/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/make_basic_repo.tar
|
||||
BIN
src-discover/tests/fixtures/generated-archives/make_exfat_repo_darwin.tar
vendored
Normal file
BIN
src-discover/tests/fixtures/generated-archives/make_exfat_repo_darwin.tar
vendored
Normal file
Binary file not shown.
BIN
src-discover/tests/fixtures/generated-archives/make_reftable_repo.tar
vendored
Normal file
BIN
src-discover/tests/fixtures/generated-archives/make_reftable_repo.tar
vendored
Normal file
Binary file not shown.
BIN
src-discover/tests/fixtures/generated-archives/make_submodules.tar
vendored
Normal file
BIN
src-discover/tests/fixtures/generated-archives/make_submodules.tar
vendored
Normal file
Binary file not shown.
105
src-discover/tests/fixtures/make_basic_repo.sh
Executable file
105
src-discover/tests/fixtures/make_basic_repo.sh
Executable 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"
|
||||
)
|
||||
22
src-discover/tests/fixtures/make_exfat_repo_darwin.sh
Executable file
22
src-discover/tests/fixtures/make_exfat_repo_darwin.sh
Executable 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
|
||||
13
src-discover/tests/fixtures/make_reftable_repo.sh
Executable file
13
src-discover/tests/fixtures/make_reftable_repo.sh
Executable 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
|
||||
26
src-discover/tests/fixtures/make_submodules.sh
Executable file
26
src-discover/tests/fixtures/make_submodules.sh
Executable 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
|
||||
)
|
||||
193
src-discover/tests/isolated.rs
Normal file
193
src-discover/tests/isolated.rs
Normal 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,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user