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::{
26    build_commit_graph_bytes, collect_reachable_commit_oids, load_commit_graph_commit_info,
27};
28use crate::config::ConfigSet;
29use crate::error::{Error, Result};
30use crate::fsck::reachability_roots;
31use crate::gc::prune_loose_unreachable;
32use crate::objects::{oid_hash_map, HashAlgo, ObjectId};
33use crate::odb::store::{ObjectStore, WritableObjectStore};
34use crate::repo::Repository;
35
36use super::Odb;
37
38/// Configure and construct an [`Odb`] with a pluggable primary backend.
39#[derive(Debug, Clone)]
40pub struct OdbBuilder {
41    objects_dir: PathBuf,
42    custom_primary: Option<Arc<dyn WritableObjectStore>>,
43    read_sources: Vec<Arc<dyn ObjectStore>>,
44    alternates: bool,
45}
46
47impl OdbBuilder {
48    /// Default files-backed layout rooted at `objects_dir` (the repository's `objects/` tree).
49    ///
50    /// When passed to [`RepositoryBuilder::odb_builder`](crate::repo::RepositoryBuilder::odb_builder),
51    /// any spelling of the repository's `objects/` directory is accepted: a path through
52    /// symlinks, or a relative path (resolved against the environment's current directory).
53    /// With a custom [`Self::primary`] the directory need not exist, but it must still name
54    /// the repository's `objects/` directory.
55    #[must_use]
56    pub fn files(objects_dir: impl AsRef<Path>) -> Self {
57        Self {
58            objects_dir: objects_dir.as_ref().to_path_buf(),
59            custom_primary: None,
60            read_sources: Vec::new(),
61            alternates: true,
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    /// Operations that still need the on-disk `objects/` directory refuse up front with
76    /// [`Error::UnsupportedObjectStore`] (its `operation` names the refused call) and leave
77    /// refs and `objects/` untouched:
78    ///
79    /// - `"fetch"`: [`Remote::fetch`](crate::remote::Remote::fetch) and everything built on it
80    ///   ([`RepositoryRemotes::fetch`](crate::repo_handles::RepositoryRemotes::fetch),
81    ///   `fetch_all`, pull, promisor lazy fetch), for every transport including bundles
82    /// - `"push"`: [`Remote::push`](crate::remote::Remote::push) and
83    ///   [`RepositoryRemotes::push`](crate::repo_handles::RepositoryRemotes::push)
84    /// - maintenance: [`Odb::gc`], [`Odb::write_commit_graph`], repack, MIDX, and pack helpers
85    ///
86    /// The store's [`ObjectStore::hash_algo`] must match the repository's object format:
87    /// [`RepositoryBuilder::open`](crate::repo::RepositoryBuilder::open) fails with
88    /// [`Error::ObjectFormatMismatch`] otherwise. [`Self::build`] does not check, since it
89    /// has no repository config to compare against.
90    #[must_use]
91    pub fn primary(mut self, store: Arc<dyn WritableObjectStore>) -> Self {
92        self.custom_primary = Some(store);
93        self
94    }
95
96    /// Append a read-only store consulted after the primary and before alternates.
97    ///
98    /// Like [`Self::primary`], its hash algorithm must match the repository's object format
99    /// or [`RepositoryBuilder::open`](crate::repo::RepositoryBuilder::open) fails with
100    /// [`Error::ObjectFormatMismatch`].
101    #[must_use]
102    pub fn push_read_source(mut self, store: Arc<dyn ObjectStore>) -> Self {
103        self.read_sources.push(store);
104        self
105    }
106
107    /// When `false`, skip `info/alternates`, environment alternates, and submodule object dirs.
108    #[must_use]
109    pub fn alternates(mut self, enabled: bool) -> Self {
110        self.alternates = enabled;
111        self
112    }
113
114    /// `objects/` directory this builder targets.
115    #[must_use]
116    pub fn objects_dir(&self) -> &Path {
117        &self.objects_dir
118    }
119
120    pub(crate) fn set_objects_dir(&mut self, objects_dir: PathBuf) {
121        self.objects_dir = objects_dir;
122    }
123
124    /// Whether [`Self::primary`] replaced the files-backed primary store.
125    pub(crate) fn has_custom_primary(&self) -> bool {
126        self.custom_primary.is_some()
127    }
128
129    /// Hash algorithms of the injected stores: the custom primary, then each read source.
130    pub(crate) fn injected_hash_algos(&self) -> impl Iterator<Item = HashAlgo> + '_ {
131        self.custom_primary
132            .iter()
133            .map(|store| store.hash_algo())
134            .chain(self.read_sources.iter().map(|store| store.hash_algo()))
135    }
136
137    /// Construct an [`Odb`] handle (attach [`Odb::with_config_git_dir`] when opening a repository).
138    #[must_use]
139    pub fn build(self) -> Odb {
140        Odb::from_builder(self)
141    }
142}
143
144impl Odb {
145    /// Build from [`OdbBuilder`] (used by [`RepositoryBuilder`](crate::repo::RepositoryBuilder)).
146    #[must_use]
147    pub(crate) fn from_builder(builder: OdbBuilder) -> Self {
148        let mut odb = Self::new(&builder.objects_dir);
149        odb.custom_primary = builder.custom_primary;
150        odb.extra_read_sources = builder.read_sources;
151        odb.alternates_enabled = builder.alternates;
152        odb
153    }
154
155    /// Whether the primary backend is the default on-disk [`super::store::FilesSource`].
156    #[must_use]
157    pub fn uses_files_primary(&self) -> bool {
158        self.custom_primary.is_none()
159    }
160
161    /// `objects/` path when the primary is files-backed; `None` for custom primaries.
162    #[must_use]
163    pub fn files_objects_dir(&self) -> Option<&Path> {
164        if self.uses_files_primary() {
165            Some(&self.objects_dir)
166        } else {
167            None
168        }
169    }
170
171    /// Filesystem-only helper: error when the primary is not files-backed.
172    pub(crate) fn require_files_primary(&self, operation: &'static str) -> Result<()> {
173        if self.custom_primary.is_some() {
174            return Err(Error::UnsupportedObjectStore { operation });
175        }
176        Ok(())
177    }
178
179    /// Primary writable backend (files source or custom store).
180    ///
181    /// # Errors
182    ///
183    /// Propagates failures while opening the lazy files source.
184    pub fn primary_writable(&self) -> Result<Arc<dyn WritableObjectStore>> {
185        if let Some(primary) = &self.custom_primary {
186            return Ok(Arc::clone(primary));
187        }
188        Ok(self.primary()? as Arc<dyn WritableObjectStore>)
189    }
190
191    /// In-process garbage collection for the files-backed object directory.
192    ///
193    /// Custom primaries return [`Error::UnsupportedObjectStore`].
194    ///
195    /// # Errors
196    ///
197    /// Propagates object-store or I/O failures during maintenance.
198    pub fn gc(&self) -> Result<()> {
199        self.require_files_primary("gc")?;
200        let Some(git_dir) = self.config_git_dir() else {
201            return Err(Error::UnsupportedObjectStore { operation: "gc" });
202        };
203        let opts = crate::environment::RepositoryOptions::empty();
204        let repo = if git_dir.file_name().is_some_and(|n| n == ".git") {
205            let wt = git_dir
206                .parent()
207                .ok_or(Error::UnsupportedObjectStore { operation: "gc" })?;
208            Repository::open(git_dir, &opts.with_work_tree(wt))?
209        } else {
210            Repository::open(git_dir, &opts)?
211        };
212        let roots = reachability_roots(&repo)?;
213        prune_loose_unreachable(self, &roots, None)?;
214        Ok(())
215    }
216
217    /// Write `objects/info/commit-graph` from reachable commits (files primary only).
218    ///
219    /// Writes nothing and deletes any existing graph in a repository that rewrites parent
220    /// lists (shallow, `info/grafts`, or replace refs unless `core.useReplaceRefs=false`), as
221    /// [`crate::maintenance::write_commit_graph`] does.
222    ///
223    /// # Errors
224    ///
225    /// Returns [`Error::UnsupportedObjectStore`] for custom primaries, or errors from graph
226    /// generation or I/O.
227    pub fn write_commit_graph(&self) -> Result<()> {
228        self.require_files_primary("commit-graph write")?;
229        let Some(git_dir) = self.config_git_dir() else {
230            return Err(Error::UnsupportedObjectStore {
231                operation: "commit-graph write",
232            });
233        };
234        let Some(objects_dir) = self.files_objects_dir() else {
235            return Err(Error::UnsupportedObjectStore {
236                operation: "commit-graph write",
237            });
238        };
239        let config = ConfigSet::load(
240            &crate::environment::Environment::empty(),
241            Some(git_dir),
242            true,
243        )
244        .unwrap_or_default();
245        let skip = crate::maintenance::CommitGraphSkipReason::detect(git_dir, || {
246            let enabled = config
247                .get_bool("core.useReplaceRefs")
248                .and_then(|r| r.ok())
249                .unwrap_or(true);
250            Ok(enabled && !crate::refs::list_refs(git_dir, "refs/replace/")?.is_empty())
251        })?;
252        if skip.is_some() {
253            crate::commit_graph_write::remove_commit_graph_files(objects_dir)?;
254            return Ok(());
255        }
256        let bloom = crate::maintenance::bloom_settings_for_commit_graph_write(&config, true);
257        let write_generation = crate::maintenance::commit_graph_writes_generation_data(&config);
258        let commits = collect_reachable_commit_oids(git_dir, self)?;
259        let mut sorted: Vec<ObjectId> = commits.into_iter().collect();
260        sorted.sort();
261        let mut infos = oid_hash_map();
262        for oid in &sorted {
263            infos.insert(*oid, load_commit_graph_commit_info(self, *oid)?);
264        }
265        let chain = CommitGraphChain::try_load(objects_dir).ok().flatten();
266        let mut existing_filters = oid_hash_map();
267        let mut upgraded_filters = oid_hash_map();
268        if let Some(ref chain) = chain {
269            for oid in &sorted {
270                if let Some(bytes) = chain.existing_filter_bytes(oid, &bloom) {
271                    existing_filters.insert(*oid, bytes);
272                } else if let Some(bytes) = chain.upgradable_filter_bytes(oid, &bloom) {
273                    upgraded_filters.insert(*oid, bytes);
274                }
275            }
276        }
277        use std::collections::HashMap;
278        let infos_map: HashMap<_, _> = infos.iter().map(|(k, v)| (*k, v.clone())).collect();
279        let existing_map: HashMap<_, _> = existing_filters
280            .iter()
281            .map(|(k, v)| (*k, v.clone()))
282            .collect();
283        let upgraded_map: HashMap<_, _> = upgraded_filters
284            .iter()
285            .map(|(k, v)| (*k, v.clone()))
286            .collect();
287        let (bytes, _) = build_commit_graph_bytes(
288            &sorted,
289            &infos_map,
290            self,
291            true,
292            &bloom,
293            None,
294            &[],
295            None,
296            &existing_map,
297            &upgraded_map,
298            write_generation,
299        )?;
300        crate::commit_graph_write::atomic_replace_commit_graph(objects_dir, &bytes)?;
301        let _ = crate::commit_graph_write::remove_commit_graph_chain_dir(objects_dir);
302        Ok(())
303    }
304
305    /// Attach a resolved work tree after [`OdbBuilder::build`].
306    #[must_use]
307    pub fn with_resolved_work_tree(mut self, work_tree: Option<PathBuf>) -> Self {
308        self.work_tree = work_tree;
309        self
310    }
311}