Skip to main content

grit_lib/odb/
builder.rs

1//! Build an [`Odb`] with a custom primary store and optional read overlays.
2//!
3//! # Example
4//!
5//! ```
6//! use std::sync::Arc;
7//!
8//! use grit_lib::objects::{HashAlgo, ObjectKind};
9//! use grit_lib::objects::odb::store::{MemoryStore, WritableObjectStore};
10//! use grit_lib::objects::odb::{Odb, OdbBuilder, WriteOptions};
11//!
12//! let store = Arc::new(MemoryStore::new(HashAlgo::Sha1));
13//! let odb = OdbBuilder::files("/tmp/repo/.git/objects")
14//!     .primary(store.clone())
15//!     .alternates(false)
16//!     .build();
17//! let oid = WritableObjectStore::write(odb.primary_writable().unwrap().as_ref(), ObjectKind::Blob, b"hi", WriteOptions::default()).unwrap();
18//! assert_eq!(oid.to_hex().len(), 40);
19//! ```
20
21use std::path::{Path, PathBuf};
22use std::sync::Arc;
23
24use crate::commit_graph_file::CommitGraphChain;
25use crate::commit_graph_write::collect_reachable_commit_oids;
26use crate::config::ConfigSet;
27use crate::error::{Error, Result};
28use crate::fsck::reachability_roots;
29use crate::gc::prune_loose_unreachable;
30use crate::objects::HashAlgo;
31use crate::odb::store::{ObjectStore, WritableObjectStore};
32use crate::repo::Repository;
33
34use super::Odb;
35
36/// Configure and construct an [`Odb`] with a pluggable primary backend.
37#[derive(Debug, Clone)]
38pub struct OdbBuilder {
39    objects_dir: PathBuf,
40    custom_primary: Option<Arc<dyn WritableObjectStore>>,
41    read_sources: Vec<Arc<dyn ObjectStore>>,
42    alternates: bool,
43    commit_graph: Option<Arc<CommitGraphChain>>,
44}
45
46impl OdbBuilder {
47    /// Default files-backed layout rooted at `objects_dir` (the repository's `objects/` tree).
48    ///
49    /// When passed to [`RepositoryBuilder::odb_builder`](crate::repo::RepositoryBuilder::odb_builder),
50    /// any spelling of the repository's `objects/` directory is accepted: a path through
51    /// symlinks, or a relative path (resolved against the environment's current directory).
52    /// With a custom [`Self::primary`] the directory need not exist, but it must still name
53    /// the repository's `objects/` directory.
54    #[must_use]
55    pub fn files(objects_dir: impl AsRef<Path>) -> Self {
56        Self {
57            objects_dir: objects_dir.as_ref().to_path_buf(),
58            custom_primary: None,
59            read_sources: Vec::new(),
60            alternates: true,
61            commit_graph: None,
62        }
63    }
64
65    /// Replace the default [`super::store::FilesSource`] primary with a custom store.
66    ///
67    /// Writes and primary reads go through this backend, including commits, staging
68    /// ([`crate::porcelain::add::stage`]), merges, stash, notes, and bundle creation.
69    /// [`Repository::fsck`](crate::repo::Repository::fsck) validates the objects this
70    /// store lists. Received packs are written into it object by object:
71    /// [`ingest_received_pack`](crate::index_pack::ingest_received_pack) and
72    /// [`Bundle::unbundle`](crate::bundle::Bundle::unbundle) resolve every delta and call
73    /// [`WritableObjectStore::write`] instead of installing a pack under `objects/pack/`.
74    ///
75    /// [`Remote::fetch`](crate::remote::Remote::fetch), [`Remote::push`](crate::remote::Remote::push),
76    /// pull, bundles, and [`clone_into`](crate::clone::clone_into) read and write objects through
77    /// the custom primary. Partial-clone filters (`operation: "partial fetch"`) and filesystem
78    /// maintenance still refuse when they need on-disk pack layout:
79    ///
80    /// - maintenance: [`Odb::gc`], [`Odb::write_commit_graph`], repack, MIDX, and pack helpers
81    ///
82    /// History walks never read commit-graph files from `objects/` for a custom primary: such
83    /// a file describes the files layout, not this store. To give walks a commit-graph, build
84    /// one with [`crate::maintenance::build_commit_graph`], persist its bytes in the store, and
85    /// attach it with [`Self::commit_graph`] when opening the repository.
86    ///
87    /// The store's [`ObjectStore::hash_algo`] must match the repository's object format:
88    /// [`RepositoryBuilder::open`](crate::repo::RepositoryBuilder::open) fails with
89    /// [`Error::ObjectFormatMismatch`] otherwise. [`Self::build`] does not check, since it
90    /// has no repository config to compare against.
91    #[must_use]
92    pub fn primary(mut self, store: Arc<dyn WritableObjectStore>) -> Self {
93        self.custom_primary = Some(store);
94        self
95    }
96
97    /// Append a read-only store consulted after the primary and before alternates.
98    ///
99    /// Like [`Self::primary`], its hash algorithm must match the repository's object format
100    /// or [`RepositoryBuilder::open`](crate::repo::RepositoryBuilder::open) fails with
101    /// [`Error::ObjectFormatMismatch`].
102    #[must_use]
103    pub fn push_read_source(mut self, store: Arc<dyn ObjectStore>) -> Self {
104        self.read_sources.push(store);
105        self
106    }
107
108    /// Attach a commit-graph for history walks to consult, for any primary store.
109    ///
110    /// Rev-list, merge-base, ancestry checks, rev-parse navigation, blame and path-limited log
111    /// read parents, root trees, committer dates and generation numbers from `chain`, and test
112    /// changed-path Bloom filters when it has them, instead of reading commit objects. Commits
113    /// missing from the graph (written after it was built) are read from the store as usual,
114    /// so a graph that covers only older history stays correct.
115    ///
116    /// An attached graph replaces any graph file under `objects/info/`. It is trusted, not
117    /// checked against the store: every commit it lists must be in this database with exactly
118    /// the parents and tree it records, which holds for a graph
119    /// [`crate::maintenance::build_commit_graph`] built from this repository. Build it with
120    /// [`CommitGraphChain::from_bytes`].
121    ///
122    /// The graph's [`CommitGraphChain::hash_algo`] must match the repository's object format,
123    /// or [`RepositoryBuilder::open`](crate::repo::RepositoryBuilder::open) fails with
124    /// [`Error::ObjectFormatMismatch`].
125    #[must_use]
126    pub fn commit_graph(mut self, chain: Arc<CommitGraphChain>) -> Self {
127        self.commit_graph = Some(chain);
128        self
129    }
130
131    /// When `false`, skip `info/alternates`, environment alternates, and submodule object dirs.
132    #[must_use]
133    pub fn alternates(mut self, enabled: bool) -> Self {
134        self.alternates = enabled;
135        self
136    }
137
138    /// `objects/` directory this builder targets.
139    #[must_use]
140    pub fn objects_dir(&self) -> &Path {
141        &self.objects_dir
142    }
143
144    pub(crate) fn set_objects_dir(&mut self, objects_dir: PathBuf) {
145        self.objects_dir = objects_dir;
146    }
147
148    /// Whether [`Self::primary`] replaced the files-backed primary store.
149    pub(crate) fn has_custom_primary(&self) -> bool {
150        self.custom_primary.is_some()
151    }
152
153    /// Hash algorithms of the injected stores: the custom primary, then each read source,
154    /// then the attached commit-graph.
155    pub(crate) fn injected_hash_algos(&self) -> impl Iterator<Item = HashAlgo> + '_ {
156        self.custom_primary
157            .iter()
158            .map(|store| store.hash_algo())
159            .chain(self.read_sources.iter().map(|store| store.hash_algo()))
160            .chain(self.commit_graph.iter().map(|chain| chain.hash_algo()))
161    }
162
163    /// Construct an [`Odb`] handle (attach [`Odb::with_config_git_dir`] when opening a repository).
164    #[must_use]
165    pub fn build(self) -> Odb {
166        Odb::from_builder(self)
167    }
168}
169
170impl Odb {
171    /// Build from [`OdbBuilder`] (used by [`RepositoryBuilder`](crate::repo::RepositoryBuilder)).
172    #[must_use]
173    pub(crate) fn from_builder(builder: OdbBuilder) -> Self {
174        let mut odb = Self::new(&builder.objects_dir);
175        odb.custom_primary = builder.custom_primary;
176        odb.extra_read_sources = builder.read_sources;
177        odb.alternates_enabled = builder.alternates;
178        odb.attached_commit_graph = builder.commit_graph;
179        odb
180    }
181
182    /// Whether the primary backend is the default on-disk [`super::store::FilesSource`].
183    #[must_use]
184    pub fn uses_files_primary(&self) -> bool {
185        self.custom_primary.is_none()
186    }
187
188    /// `objects/` path when the primary is files-backed; `None` for custom primaries.
189    #[must_use]
190    pub fn files_objects_dir(&self) -> Option<&Path> {
191        if self.uses_files_primary() {
192            Some(&self.objects_dir)
193        } else {
194            None
195        }
196    }
197
198    /// Filesystem-only helper: error when the primary is not files-backed.
199    pub(crate) fn require_files_primary(&self, operation: &'static str) -> Result<()> {
200        if self.custom_primary.is_some() {
201            return Err(Error::UnsupportedObjectStore { operation });
202        }
203        Ok(())
204    }
205
206    /// Primary writable backend (files source or custom store).
207    ///
208    /// # Errors
209    ///
210    /// Propagates failures while opening the lazy files source.
211    pub fn primary_writable(&self) -> Result<Arc<dyn WritableObjectStore>> {
212        if let Some(primary) = &self.custom_primary {
213            return Ok(Arc::clone(primary));
214        }
215        Ok(self.primary()? as Arc<dyn WritableObjectStore>)
216    }
217
218    /// In-process garbage collection for the files-backed object directory.
219    ///
220    /// Custom primaries return [`Error::UnsupportedObjectStore`].
221    ///
222    /// # Errors
223    ///
224    /// Propagates object-store or I/O failures during maintenance.
225    pub fn gc(&self) -> Result<()> {
226        self.require_files_primary("gc")?;
227        let Some(git_dir) = self.config_git_dir() else {
228            return Err(Error::UnsupportedObjectStore { operation: "gc" });
229        };
230        let opts = crate::environment::RepositoryOptions::empty();
231        let repo = if git_dir.file_name().is_some_and(|n| n == ".git") {
232            let wt = git_dir
233                .parent()
234                .ok_or(Error::UnsupportedObjectStore { operation: "gc" })?;
235            Repository::open(git_dir, &opts.with_work_tree(wt))?
236        } else {
237            Repository::open(git_dir, &opts)?
238        };
239        let roots = reachability_roots(&repo)?;
240        prune_loose_unreachable(self, &roots, None)?;
241        Ok(())
242    }
243
244    /// Write `objects/info/commit-graph` from reachable commits (files primary only).
245    ///
246    /// A custom primary has no `objects/info/` to install a graph in: build it in memory with
247    /// [`crate::maintenance::build_commit_graph`] and attach it with
248    /// [`OdbBuilder::commit_graph`] instead.
249    ///
250    /// Writes nothing and deletes any existing graph in a repository that rewrites parent
251    /// lists (shallow, `info/grafts`, or replace refs unless `core.useReplaceRefs=false`), as
252    /// [`crate::maintenance::write_commit_graph`] does.
253    ///
254    /// # Errors
255    ///
256    /// Returns [`Error::UnsupportedObjectStore`] for custom primaries, or errors from graph
257    /// generation or I/O.
258    pub fn write_commit_graph(&self) -> Result<()> {
259        self.require_files_primary("commit-graph write")?;
260        let Some(git_dir) = self.config_git_dir() else {
261            return Err(Error::UnsupportedObjectStore {
262                operation: "commit-graph write",
263            });
264        };
265        let Some(objects_dir) = self.files_objects_dir() else {
266            return Err(Error::UnsupportedObjectStore {
267                operation: "commit-graph write",
268            });
269        };
270        let config = ConfigSet::load(
271            &crate::environment::Environment::empty(),
272            Some(git_dir),
273            true,
274        )
275        .unwrap_or_default();
276        let skip = crate::maintenance::CommitGraphSkipReason::detect(git_dir, || {
277            let enabled = config
278                .get_bool("core.useReplaceRefs")
279                .and_then(|r| r.ok())
280                .unwrap_or(true);
281            Ok(enabled && !crate::refs::list_refs(git_dir, "refs/replace/")?.is_empty())
282        })?;
283        if skip.is_some() {
284            crate::commit_graph_write::remove_commit_graph_files(objects_dir)?;
285            return Ok(());
286        }
287        let commits = collect_reachable_commit_oids(git_dir, self)?;
288        let existing = CommitGraphChain::try_load(objects_dir).ok().flatten();
289        let built = crate::maintenance::build_commit_graph_for_commits(
290            self,
291            commits,
292            &config,
293            true,
294            existing.as_ref(),
295        )?;
296        crate::commit_graph_write::atomic_replace_commit_graph(objects_dir, &built.bytes)?;
297        let _ = crate::commit_graph_write::remove_commit_graph_chain_dir(objects_dir);
298        Ok(())
299    }
300
301    /// Attach a resolved work tree after [`OdbBuilder::build`].
302    #[must_use]
303    pub fn with_resolved_work_tree(mut self, work_tree: Option<PathBuf>) -> Self {
304        self.work_tree = work_tree;
305        self
306    }
307}