Skip to main content

gix_discover/
path.rs

1use crate::{DOT_GIT_DIR, MODULES};
2use std::ffi::OsStr;
3use std::path::Path;
4use std::{io::Read, path::PathBuf};
5
6/// The kind of repository by looking exclusively at its `git_dir`.
7#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash)]
8pub enum RepositoryKind {
9    /// The repository resides in `.git/modules/`.
10    Submodule,
11    /// The repository resides in `.git/worktrees/`.
12    LinkedWorktree,
13    /// The repository is in a `.git` directory.
14    Common,
15}
16
17///
18pub mod from_gitdir_file {
19    /// The error returned by [`from_gitdir_file()`][crate::path::from_gitdir_file()].
20    #[derive(Debug, thiserror::Error)]
21    #[expect(missing_docs)]
22    pub enum Error {
23        #[error(transparent)]
24        Io(#[from] std::io::Error),
25        #[error(transparent)]
26        Parse(#[from] crate::parse::gitdir::Error),
27    }
28}
29
30fn read_regular_file_content_with_size_limit(path: &std::path::Path) -> std::io::Result<Vec<u8>> {
31    let mut file = std::fs::File::open(path)?;
32    let max_file_size = 1024 * 64; // NOTE: git allows 1MB here
33    let file_size = file.metadata()?.len();
34    if file_size > max_file_size {
35        return Err(std::io::Error::other(format!(
36            "Refusing to open files larger than {} bytes, '{}' was {} bytes large",
37            max_file_size,
38            path.display(),
39            file_size
40        )));
41    }
42    let mut buf = Vec::with_capacity(512);
43    file.read_to_end(&mut buf)?;
44    Ok(buf)
45}
46
47/// Read a plain path file, returning `None` if the file is missing.
48///
49/// Linked-worktree `gitdir` files are plain path files in Git, not `gitdir:`
50/// files. Match Git's `get_linked_worktree()` behavior by trimming trailing
51/// whitespace before interpreting the content. Empty or whitespace-only path
52/// files are invalid.
53fn read_plain_file_content(path: &std::path::Path) -> Option<std::io::Result<Vec<u8>>> {
54    use bstr::ByteSlice;
55    let mut buf = match read_regular_file_content_with_size_limit(path) {
56        Ok(buf) => buf,
57        Err(err) if err.kind() == std::io::ErrorKind::NotFound => return None,
58        Err(err) => return Some(Err(err)),
59    };
60    let trimmed_len = buf.trim_end().len();
61    buf.truncate(trimmed_len);
62    if buf.is_empty() {
63        return Some(Err(std::io::Error::new(
64            std::io::ErrorKind::InvalidData,
65            format!("Refusing to read an empty path from '{}'", path.display()),
66        )));
67    }
68    Some(Ok(buf))
69}
70
71/// Guess the kind of repository by looking at its `git_dir` path and return it.
72/// Return `None` if `git_dir` isn't called `.git` or isn't within `.git/worktrees` or `.git/modules`, or if it's
73/// a `.git` suffix like in `foo.git`.
74/// The check for markers is case-sensitive under the assumption that nobody meddles with standard directories.
75///
76/// As this considers only the path, it cannot recognize linked worktrees of repositories whose Git directory isn't
77/// named `.git`, such as natively bare repositories. Inspect the worktree's `commondir` file to identify those.
78pub fn repository_kind(git_dir: &Path) -> Option<RepositoryKind> {
79    if git_dir.file_name() == Some(OsStr::new(DOT_GIT_DIR)) {
80        return Some(RepositoryKind::Common);
81    }
82
83    let mut last_comp = None;
84    git_dir.components().rev().skip(1).any(|c| {
85        if c.as_os_str() == OsStr::new(DOT_GIT_DIR) {
86            true
87        } else {
88            last_comp = Some(c.as_os_str());
89            false
90        }
91    });
92    let last_comp = last_comp?;
93    if last_comp == OsStr::new(MODULES) {
94        RepositoryKind::Submodule.into()
95    } else if last_comp == OsStr::new("worktrees") {
96        RepositoryKind::LinkedWorktree.into()
97    } else {
98        None
99    }
100}
101
102/// Reads a plain path from a file that contains it as its only content, with trailing whitespace trimmed.
103///
104/// Empty or whitespace-only path files are invalid.
105pub fn from_plain_file(path: &std::path::Path) -> Option<std::io::Result<PathBuf>> {
106    read_plain_file_content(path).map(|res| res.map(gix_path::from_bstring))
107}
108
109/// Reads a plain path from a file like [`from_plain_file()`], resolving relative paths against
110/// the file's containing directory as needed.
111///
112/// The `path` argument is expected to name the path file itself, which is always supposed to be
113/// a file path.
114pub fn from_plain_file_relative_to_file(path: &std::path::Path) -> Option<std::io::Result<PathBuf>> {
115    read_plain_file_content(path).map(|res| {
116        res.and_then(|buf| {
117            let plain_path = gix_path::from_bstring(buf);
118            if !plain_path.is_relative() {
119                return Ok(plain_path);
120            }
121            match path.parent() {
122                Some(parent) => Ok(parent.join(plain_path)),
123                _ => Err(std::io::Error::other(format!(
124                    "'{path}' has no parent, but '{plain_path}' is relative. It's impossible",
125                    path = path.display(),
126                    plain_path = plain_path.display()
127                ))),
128            }
129        })
130    })
131}
132
133/// Reads typical `gitdir: ` files from disk as used by worktrees and submodules.
134pub fn from_gitdir_file(path: &std::path::Path) -> Result<PathBuf, from_gitdir_file::Error> {
135    let buf = read_regular_file_content_with_size_limit(path)?;
136    let mut gitdir = crate::parse::gitdir(&buf)?;
137    if let Some(parent) = path.parent() {
138        gitdir = parent.join(gitdir);
139    }
140    Ok(gitdir)
141}
142
143/// Conditionally pop a trailing `.git` dir if present.
144pub fn without_dot_git_dir(mut path: PathBuf) -> PathBuf {
145    if path.file_name().and_then(std::ffi::OsStr::to_str) == Some(DOT_GIT_DIR) {
146        path.pop();
147    }
148    path
149}