Skip to main content

gix_discover/
is.rs

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