gix_discover/
is.rs

1use std::{borrow::Cow, ffi::OsStr, path::Path};
2
3use crate::{DOT_GIT_DIR, MODULES};
4
5/// Returns true if the given `git_dir` seems to be a bare repository.
6///
7/// Please note that repositories without an index generally _look_ bare, even though they might also be uninitialized.
8pub fn bare(git_dir_candidate: &Path) -> bool {
9    !(git_dir_candidate.join("index").exists() || (git_dir_candidate.file_name() == Some(OsStr::new(DOT_GIT_DIR))))
10}
11
12/// Returns true if `git_dir` is located within a `.git/modules` directory, indicating it's a submodule clone.
13pub fn submodule_git_dir(git_dir: &Path) -> bool {
14    let mut last_comp = None;
15    git_dir.file_name() != Some(OsStr::new(DOT_GIT_DIR))
16        && git_dir.components().rev().skip(1).any(|c| {
17            if c.as_os_str() == OsStr::new(DOT_GIT_DIR) {
18                true
19            } else {
20                last_comp = Some(c.as_os_str());
21                false
22            }
23        })
24        && last_comp == Some(OsStr::new(MODULES))
25}
26
27/// What constitutes a valid git repository, returning the guessed repository kind
28/// purely based on the presence of files. Note that the git-config ultimately decides what's bare.
29///
30/// Returns the `Kind` of git directory that was passed, possibly alongside the supporting private worktree git dir.
31///
32/// Note that `.git` files are followed to a valid git directory, which then requires…
33///
34///   * …a valid head
35///   * …an objects directory
36///   * …a refs directory
37///
38pub fn git(git_dir: &Path) -> Result<crate::repository::Kind, crate::is_git::Error> {
39    let git_dir_metadata = git_dir.metadata().map_err(|err| crate::is_git::Error::Metadata {
40        source: err,
41        path: git_dir.into(),
42    })?;
43    // precompose-unicode can't be known here, so we just default it to false, hoping it won't matter.
44    let cwd = gix_fs::current_dir(false)?;
45    git_with_metadata(git_dir, git_dir_metadata, &cwd)
46}
47
48pub(crate) fn git_with_metadata(
49    git_dir: &Path,
50    git_dir_metadata: std::fs::Metadata,
51    cwd: &Path,
52) -> Result<crate::repository::Kind, crate::is_git::Error> {
53    #[derive(Eq, PartialEq)]
54    enum Kind {
55        MaybeRepo,
56        Submodule,
57        LinkedWorkTreeDir,
58        WorkTreeGitDir { work_dir: std::path::PathBuf },
59    }
60
61    let dot_git = if git_dir_metadata.is_file() {
62        let private_git_dir = crate::path::from_gitdir_file(git_dir)?;
63        Cow::Owned(private_git_dir)
64    } else {
65        Cow::Borrowed(git_dir)
66    };
67
68    {
69        // Fast-path: avoid doing the complete search if HEAD is already not there.
70        if !dot_git.join("HEAD").exists() {
71            return Err(crate::is_git::Error::MissingHead);
72        }
73        // We expect to be able to parse any ref-hash, so we shouldn't have to know the repos hash here.
74        // With ref-table, the hash is probably stored as part of the ref-db itself, so we can handle it from there.
75        // In other words, it's important not to fail on detached heads here because we guessed the hash kind wrongly.
76        let refs = gix_ref::file::Store::at(dot_git.as_ref().into(), Default::default());
77        match refs.find_loose("HEAD") {
78            Ok(head) => {
79                if head.name.as_bstr() != "HEAD" {
80                    return Err(crate::is_git::Error::MisplacedHead {
81                        name: head.name.into_inner(),
82                    });
83                }
84            }
85            Err(gix_ref::file::find::existing::Error::Find(gix_ref::file::find::Error::ReferenceCreation {
86                source: _,
87                relative_path,
88            })) if relative_path == Path::new("HEAD") => {
89                // It's fine as long as the reference is found is `HEAD`.
90            }
91            Err(err) => {
92                return Err(err.into());
93            }
94        }
95    }
96
97    let (common_dir, kind) = if git_dir_metadata.is_file() {
98        let common_dir = dot_git.join("commondir");
99        match crate::path::from_plain_file(&common_dir) {
100            Some(Err(err)) => {
101                return Err(crate::is_git::Error::MissingCommonDir {
102                    missing: common_dir,
103                    source: err,
104                })
105            }
106            Some(Ok(common_dir)) => {
107                let common_dir = dot_git.join(common_dir);
108                (Cow::Owned(common_dir), Kind::LinkedWorkTreeDir)
109            }
110            None => (dot_git.clone(), Kind::Submodule),
111        }
112    } else {
113        let common_dir = dot_git.join("commondir");
114        let worktree_and_common_dir = crate::path::from_plain_file(&common_dir)
115            .and_then(Result::ok)
116            .and_then(|cd| {
117                crate::path::from_plain_file(&dot_git.join("gitdir"))
118                    .and_then(Result::ok)
119                    .map(|worktree_gitfile| (crate::path::without_dot_git_dir(worktree_gitfile), cd))
120            });
121        match worktree_and_common_dir {
122            Some((work_dir, common_dir)) => {
123                let common_dir = dot_git.join(common_dir);
124                (Cow::Owned(common_dir), Kind::WorkTreeGitDir { work_dir })
125            }
126            None => (dot_git.clone(), Kind::MaybeRepo),
127        }
128    };
129
130    {
131        let objects_path = common_dir.join("objects");
132        if !objects_path.is_dir() {
133            return Err(crate::is_git::Error::MissingObjectsDirectory { missing: objects_path });
134        }
135    }
136    {
137        let refs_path = common_dir.join("refs");
138        if !refs_path.is_dir() {
139            return Err(crate::is_git::Error::MissingRefsDirectory { missing: refs_path });
140        }
141    }
142    Ok(match kind {
143        Kind::LinkedWorkTreeDir => crate::repository::Kind::WorkTree {
144            linked_git_dir: Some(dot_git.into_owned()),
145        },
146        Kind::WorkTreeGitDir { work_dir } => crate::repository::Kind::WorkTreeGitDir { work_dir },
147        Kind::Submodule => crate::repository::Kind::Submodule {
148            git_dir: dot_git.into_owned(),
149        },
150        Kind::MaybeRepo => {
151            let conformed_git_dir = if git_dir == Path::new(".") {
152                gix_path::realpath_opts(git_dir, cwd, gix_path::realpath::MAX_SYMLINKS)
153                    .map(Cow::Owned)
154                    .unwrap_or(Cow::Borrowed(git_dir))
155            } else {
156                gix_path::normalize(git_dir.into(), cwd).unwrap_or(Cow::Borrowed(git_dir))
157            };
158            if bare(conformed_git_dir.as_ref()) || conformed_git_dir.extension() == Some(OsStr::new("git")) {
159                crate::repository::Kind::PossiblyBare
160            } else if submodule_git_dir(conformed_git_dir.as_ref()) {
161                crate::repository::Kind::SubmoduleGitDir
162            } else if conformed_git_dir.file_name() == Some(OsStr::new(DOT_GIT_DIR)) {
163                crate::repository::Kind::WorkTree { linked_git_dir: None }
164            // } else if !bare_by_config(conformed_git_dir.as_ref())
165            //     .map_err(|err| crate::is_git::Error::Metadata {
166            //         source: err,
167            //         path: conformed_git_dir.join("config"),
168            //     })?
169            //     .ok_or(crate::is_git::Error::Inconclusive)?
170            // {
171            //     crate::repository::Kind::WorktreePossiblyInConfiguration
172            } else {
173                crate::repository::Kind::PossiblyBare
174            }
175        }
176    })
177}