Skip to main content

git_core/
repo.rs

1use std::path::{Path, PathBuf};
2
3use crate::commit::CommitObject;
4use crate::error::{GitError, Result};
5use crate::hash::GitHash;
6use crate::loose;
7use crate::object::{ObjectKind, RawObject};
8use crate::pack;
9use crate::reflog::{self, ReflogEntry};
10use crate::refs;
11use crate::tree::TreeObject;
12
13pub struct GitRepo {
14    /// The `.git` directory.
15    git_dir: PathBuf,
16    /// `.git/objects`
17    objects_dir: PathBuf,
18}
19
20impl GitRepo {
21    /// Open a git repository. `path` may be the work-tree root (contains `.git/`)
22    /// or the bare `.git` directory itself.
23    pub fn open(path: &Path) -> Result<Self> {
24        let git_dir = if path.join("HEAD").exists() {
25            path.to_owned()
26        } else if path.join(".git").join("HEAD").exists() {
27            path.join(".git")
28        } else {
29            return Err(GitError::InvalidObject(format!(
30                "not a git repository: {}",
31                path.display()
32            )));
33        };
34
35        let objects_dir = git_dir.join("objects");
36        Ok(Self {
37            git_dir,
38            objects_dir,
39        })
40    }
41
42    /// Resolve HEAD to its commit hash.
43    pub fn head(&self) -> Result<GitHash> {
44        refs::resolve_ref(&self.git_dir, "HEAD")
45    }
46
47    /// Resolve any ref name (e.g. `"HEAD"`, `"refs/heads/main"`, or a bare hex hash).
48    pub fn resolve_ref(&self, name: &str) -> Result<GitHash> {
49        refs::resolve_ref(&self.git_dir, name)
50    }
51
52    /// Read and verify an object by hash, from a loose file or a packfile.
53    ///
54    /// Loose objects are tried first; if absent, every packfile is searched
55    /// (resolving `OFS_DELTA`/`REF_DELTA` chains). A truly missing object yields
56    /// [`GitError::ObjectNotFound`]; an unsupported pack *index* version yields
57    /// the distinct [`GitError::PackfileUnsupported`] — never a misleading
58    /// not-found.
59    pub fn read_object(&self, hash: &GitHash) -> Result<RawObject> {
60        match loose::read_loose(&self.objects_dir, hash) {
61            Err(GitError::ObjectNotFound(h)) => match pack::read_packed(&self.objects_dir, hash)? {
62                Some(obj) => Ok(obj),
63                None => Err(GitError::ObjectNotFound(h)),
64            },
65            other => other,
66        }
67    }
68
69    /// Read and parse a commit object.
70    pub fn read_commit(&self, hash: &GitHash) -> Result<CommitObject> {
71        let obj = self.read_object(hash)?;
72        if obj.kind != ObjectKind::Commit {
73            return Err(GitError::InvalidObject(format!(
74                "{hash} is a {:?}, not a commit",
75                obj.kind
76            )));
77        }
78        CommitObject::parse(*hash, &obj.data)
79    }
80
81    /// Read and parse a tree object.
82    pub fn read_tree(&self, hash: &GitHash) -> Result<TreeObject> {
83        let obj = self.read_object(hash)?;
84        if obj.kind != ObjectKind::Tree {
85            return Err(GitError::InvalidObject(format!(
86                "{hash} is a {:?}, not a tree",
87                obj.kind
88            )));
89        }
90        TreeObject::parse(*hash, &obj.data)
91    }
92
93    /// Read a blob object and return its raw bytes.
94    pub fn read_blob(&self, hash: &GitHash) -> Result<Vec<u8>> {
95        let obj = self.read_object(hash)?;
96        if obj.kind != ObjectKind::Blob {
97            return Err(GitError::InvalidObject(format!(
98                "{hash} is a {:?}, not a blob",
99                obj.kind
100            )));
101        }
102        Ok(obj.data)
103    }
104
105    /// Walk the commit ancestry chain, newest-first (first-parent only).
106    pub fn walk_commits(&self, from: GitHash) -> impl Iterator<Item = Result<CommitObject>> + '_ {
107        CommitWalker {
108            repo: self,
109            next: Some(from),
110        }
111    }
112
113    /// Read the reflog for `refname` (e.g. `"HEAD"`, `"refs/heads/main"`).
114    ///
115    /// Returns an empty vec when the log file is absent (git creates one only
116    /// after the ref first moves), never an error for mere absence.
117    ///
118    /// # Errors
119    /// Propagates a non-`NotFound` I/O error encountered reading the log file.
120    pub fn reflog(&self, refname: &str) -> Result<Vec<ReflogEntry>> {
121        reflog::read_reflog(&self.git_dir, refname)
122    }
123
124    /// Every object in the store: the union of loose and packed objects, with
125    /// duplicates removed (an object may be both loose and packed mid-`gc`).
126    ///
127    /// # Errors
128    /// Propagates a [`GitError`] from packfile-index enumeration.
129    pub fn all_objects(&self) -> Result<Vec<GitHash>> {
130        let mut seen = std::collections::HashSet::new();
131        let mut out = Vec::new();
132        for hash in loose::list_loose(&self.objects_dir)
133            .into_iter()
134            .chain(pack::list_packed(&self.objects_dir)?)
135        {
136            if seen.insert(hash) {
137                out.push(hash);
138            }
139        }
140        Ok(out)
141    }
142
143    /// Every ref in the repository as `(refname, target_hash)` pairs (loose
144    /// `refs/**`, `packed-refs`, and `HEAD`).
145    #[must_use]
146    pub fn all_refs(&self) -> Vec<(String, GitHash)> {
147        refs::list_refs(&self.git_dir)
148    }
149}
150
151struct CommitWalker<'a> {
152    repo: &'a GitRepo,
153    next: Option<GitHash>,
154}
155
156impl Iterator for CommitWalker<'_> {
157    type Item = Result<CommitObject>;
158
159    fn next(&mut self) -> Option<Self::Item> {
160        let hash = self.next.take()?;
161        match self.repo.read_commit(&hash) {
162            Ok(commit) => {
163                self.next = commit.parents.first().copied();
164                Some(Ok(commit))
165            }
166            Err(e) => Some(Err(e)),
167        }
168    }
169}