use std::{borrow::Cow, ffi::OsStr, path::Path}; use crate::{DOT_GIT_DIR, MODULES}; /// Returns true if the given `git_dir` seems to be a bare repository. /// /// Please note that repositories without an index generally _look_ bare, even though they might also be uninitialized. pub fn bare(git_dir_candidate: &Path) -> bool { !(git_dir_candidate.join("index").exists() || (git_dir_candidate.file_name() == Some(OsStr::new(DOT_GIT_DIR)))) } /// Parse `/config` quickly to evaluate the value of the `bare` line, or return `true` if the file doesn't exist /// similar to what`guess_repository_type` seems to be doing. /// Return `None` if the `bare` line can't be found or the value of `bare` can't be determined. fn bare_by_config(git_dir_candidate: &Path) -> std::io::Result> { match std::fs::read(git_dir_candidate.join("config")) { Ok(buf) => Ok(config::parse_bare(&buf)), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(Some(true)), Err(err) => Err(err), } } // Copied and adapted from `gix-config-value::boolean`. mod config { use bstr::{BStr, ByteSlice}; pub(crate) fn parse_bare(buf: &[u8]) -> Option { buf.lines().find_map(|line| { let line = line.trim().strip_prefix(b"bare")?; match line.first() { None => Some(true), Some(c) if *c == b'=' => parse_bool(line.get(1..)?.trim_start().as_bstr()), Some(c) if c.is_ascii_whitespace() => match line.split_once_str(b"=") { Some((_left, right)) => parse_bool(right.trim_start().as_bstr()), None => Some(true), }, Some(_other_char_) => None, } }) } fn parse_bool(value: &BStr) -> Option { Some(if parse_true(value) { true } else if parse_false(value) { false } else { use std::str::FromStr; if let Some(integer) = value.to_str().ok().and_then(|s| i64::from_str(s).ok()) { integer != 0 } else { return None; } }) } fn parse_true(value: &BStr) -> bool { value.eq_ignore_ascii_case(b"yes") || value.eq_ignore_ascii_case(b"on") || value.eq_ignore_ascii_case(b"true") } fn parse_false(value: &BStr) -> bool { value.eq_ignore_ascii_case(b"no") || value.eq_ignore_ascii_case(b"off") || value.eq_ignore_ascii_case(b"false") || value.is_empty() } #[cfg(test)] mod tests { use super::*; #[test] fn various() { for (input, expected) in [ ("bare=true", Some(true)), ("bare=1", Some(true)), ("bare =1", Some(true)), ("bare= yes", Some(true)), ("bare=false", Some(false)), ("bare=0", Some(false)), ("bare=blah", None), ("bare=", Some(false)), ("bare= \n", Some(false)), ("bare = true \n", Some(true)), ("\t bare = false \n", Some(false)), ("\n\tbare=true", Some(true)), ("\n\tbare=true\n\tfoo", Some(true)), ("\n\tbare ", Some(true)), ("\n\tbare", Some(true)), ("not found\nreally", None), ] { assert_eq!(parse_bare(input.as_bytes()), expected, "{input:?}"); } } } } /// Returns true if `git_dir` is located within a `.git/modules` directory, indicating it's a submodule clone. pub fn submodule_git_dir(git_dir: &Path) -> bool { let mut last_comp = None; git_dir.file_name() != Some(OsStr::new(DOT_GIT_DIR)) && 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 } }) && last_comp == Some(OsStr::new(MODULES)) } /// What constitutes a valid git repository, returning the guessed repository kind /// purely based on the presence of files. Note that the 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 { let git_dir_metadata = git_dir.metadata().map_err(|err| crate::is_git::Error::Metadata { source: err, path: git_dir.into(), })?; git_with_metadata(git_dir, git_dir_metadata) } pub(crate) fn git_with_metadata( git_dir: &Path, git_dir_metadata: std::fs::Metadata, ) -> Result { #[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. // TODO(reftable): use a ref-store to lookup HEAD if ref-tables should be supported, or detect ref-tables beforehand. 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 has is probably stored as part of the ref-db itself, so we can handle it from there. // In other words, it's important not to fail on detached heads here because we guessed the hash kind wrongly. let object_hash_should_not_matter_here = gix_hash::Kind::Sha1; let refs = gix_ref::file::Store::at( dot_git.as_ref().into(), gix_ref::store::WriteReflog::Normal, object_hash_should_not_matter_here, ); let head = refs.find_loose("HEAD")?; if head.name.as_bstr() != "HEAD" { return Err(crate::is_git::Error::MisplacedHead { name: head.name.into_inner(), }); } } let (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(git_dir) .map(Cow::Owned) .unwrap_or(Cow::Borrowed(git_dir)) } else { Cow::Borrowed(git_dir) }; if bare(conformed_git_dir.as_ref()) || conformed_git_dir.extension() == Some(OsStr::new("git")) { crate::repository::Kind::Bare } else if submodule_git_dir(conformed_git_dir.as_ref()) { crate::repository::Kind::SubmoduleGitDir } else if conformed_git_dir.file_name() == Some(OsStr::new(DOT_GIT_DIR)) || !bare_by_config(conformed_git_dir.as_ref()) .map_err(|err| crate::is_git::Error::Metadata { source: err, path: conformed_git_dir.join("config"), })? .ok_or(crate::is_git::Error::Inconclusive)? { crate::repository::Kind::WorkTree { linked_git_dir: None } } else { crate::repository::Kind::Bare } } }) }