blob: b39d216c6594775586fe0b5ada3b63e10bc5bd00 [file] [log] [blame]
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 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.map_or(false, |x| current_height > x) {
return Err(Error::NoGitRepositoryWithinCeiling {
path: dir.into_owned(),
ceiling_height: current_height,
});
}
current_height += 1;
#[cfg(unix)]
if current_height != 0 && !cross_fs {
let metadata = 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),
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.parent().map_or(false, |p| p.as_os_str().is_empty()) {
cursor = cwd.to_path_buf();
dir_made_absolute = true;
}
if !cursor.pop() {
if dir_made_absolute
|| matches!(
cursor.components().next(),
Some(std::path::Component::RootDir | 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())
}
}