async_git/plumbing/
repo.rs

1use std::env;
2use std::path::{Path, PathBuf};
3use std::str::FromStr;
4
5use tokio::fs;
6
7use crate::errors::{Error, Result};
8use crate::plumbing::{Object, Oid, Ref};
9use crate::util;
10
11pub struct Repository {
12    pub path: PathBuf,
13}
14
15impl Repository {
16    /// Opens a git repository
17    pub async fn open(path: impl AsRef<Path>) -> Result<Self> {
18        let path = path.as_ref().to_path_buf();
19        if !Repository::is_repo(&path).await? {
20            return Err(Error::PathIsNotRepository(path));
21        }
22        Ok(Repository { path })
23    }
24
25    /// Get the head ref
26    pub async fn head(&self) -> Result<Ref> {
27        let head_path = self.path.join("HEAD");
28        let head_ref = util::file_to_string(head_path).await?;
29        Ok(Ref::parse(head_ref.trim())?)
30    }
31
32    /// Checks if the given path points to a git repository
33    pub async fn is_repo(path: impl AsRef<Path>) -> Result<bool> {
34        let path = path.as_ref();
35
36        // if it's not a directory, obviously not
37        if !path.is_dir() {
38            return Ok(false);
39        }
40
41        // look for HEAD. there's 2 possibilities:
42        // - HEAD is a symlink or a regular file and contains a "ref:" ref
43        // - HEAD is a regular file and contains an OID (detached)
44        let head_path = path.join("HEAD");
45        if !head_path.exists() {
46            return Ok(false);
47        }
48        let contents = util::file_to_string(&head_path).await?;
49        if contents.starts_with("ref:") {
50            // check the first case first
51            let rest = contents.trim_start_matches("ref:").trim();
52            if !rest.starts_with("refs/") {
53                return Ok(false);
54            }
55        } else {
56            // now try parsing as oid
57            let contents = contents.trim();
58            if !Oid::from_str(&contents).is_ok() {
59                return Ok(false);
60            }
61            // at this point, we know it's an oid. reject if it's also a symlink
62            if fs::read_link(&head_path).await.is_ok() {
63                return Ok(false);
64            }
65        }
66
67        // look for the objects directory
68        let objects_path = match env::var("GIT_OBJECT_DIRECTORY") {
69            Ok(path) => PathBuf::from(path),
70            Err(_) => path.join("objects"),
71        };
72        if !objects_path.exists() {
73            return Ok(false);
74        }
75
76        // look for the refs directory
77        let refs_path = path.join("refs");
78        if !refs_path.exists() {
79            return Ok(false);
80        }
81
82        Ok(true)
83    }
84
85    /// Find the closest repository that the current repository belongs to.
86    pub async fn find() -> Result<Option<PathBuf>> {
87        let mut cwd = env::current_dir()?;
88
89        // 3 options for each directory level:
90        // - .git file (contains "gitdir: <path>")
91        // - .git/ directory
92        // - ./ (bare repo)
93        loop {
94            let dot_git = cwd.join(".git");
95            if dot_git.exists() {
96                if dot_git.is_file() {
97                    let contents = util::file_to_string(&dot_git).await?;
98                    if contents.starts_with("gitdir:") {
99                        let path = PathBuf::from(contents.trim_start_matches("gitdir:").trim());
100                        if Repository::is_repo(&path).await? {
101                            return Ok(Some(path));
102                        }
103                    }
104                } else if dot_git.is_dir() {
105                    if Repository::is_repo(&dot_git).await? {
106                        return Ok(Some(dot_git));
107                    }
108                }
109            } else {
110                if Repository::is_repo(&cwd).await? {
111                    return Ok(Some(cwd));
112                }
113            }
114
115            if Repository::is_repo(cwd.clone()).await? {
116                return Ok(Some(cwd));
117            }
118
119            cwd = match cwd.parent() {
120                Some(parent) => parent.to_path_buf(),
121                None => return Ok(None),
122            };
123        }
124    }
125
126    pub fn get_object(&self, id: Oid) -> Object {
127        Object {
128            repo_path: self.path.clone(),
129            id,
130        }
131    }
132}