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, 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    #[must_use]
86    pub fn primary(mut self, store: Arc<dyn WritableObjectStore>) -> Self {
87        self.custom_primary = Some(store);
88        self
89    }
90
91    /// Append a read-only store consulted after the primary and before alternates.
92    #[must_use]
93    pub fn push_read_source(mut self, store: Arc<dyn ObjectStore>) -> Self {
94        self.read_sources.push(store);
95        self
96    }
97
98    /// When `false`, skip `info/alternates`, environment alternates, and submodule object dirs.
99    #[must_use]
100    pub fn alternates(mut self, enabled: bool) -> Self {
101        self.alternates = enabled;
102        self
103    }
104
105    /// `objects/` directory this builder targets.
106    #[must_use]
107    pub fn objects_dir(&self) -> &Path {
108        &self.objects_dir
109    }
110
111    pub(crate) fn set_objects_dir(&mut self, objects_dir: PathBuf) {
112        self.objects_dir = objects_dir;
113    }
114
115    /// Whether [`Self::primary`] replaced the files-backed primary store.
116    pub(crate) fn has_custom_primary(&self) -> bool {
117        self.custom_primary.is_some()
118    }
119
120    /// Construct an [`Odb`] handle (attach [`Odb::with_config_git_dir`] when opening a repository).
121    #[must_use]
122    pub fn build(self) -> Odb {
123        Odb::from_builder(self)
124    }
125}
126
127impl Odb {
128    /// Build from [`OdbBuilder`] (used by [`RepositoryBuilder`](crate::repo::RepositoryBuilder)).
129    #[must_use]
130    pub(crate) fn from_builder(builder: OdbBuilder) -> Self {
131        let mut odb = Self::new(&builder.objects_dir);
132        odb.custom_primary = builder.custom_primary;
133        odb.extra_read_sources = builder.read_sources;
134        odb.alternates_enabled = builder.alternates;
135        odb
136    }
137
138    /// Whether the primary backend is the default on-disk [`super::store::FilesSource`].
139    #[must_use]
140    pub fn uses_files_primary(&self) -> bool {
141        self.custom_primary.is_none()
142    }
143
144    /// `objects/` path when the primary is files-backed; `None` for custom primaries.
145    #[must_use]
146    pub fn files_objects_dir(&self) -> Option<&Path> {
147        if self.uses_files_primary() {
148            Some(&self.objects_dir)
149        } else {
150            None
151        }
152    }
153
154    /// Filesystem-only helper: error when the primary is not files-backed.
155    pub(crate) fn require_files_primary(&self, operation: &'static str) -> Result<()> {
156        if self.custom_primary.is_some() {
157            return Err(Error::UnsupportedObjectStore { operation });
158        }
159        Ok(())
160    }
161
162    /// Primary writable backend (files source or custom store).
163    ///
164    /// # Errors
165    ///
166    /// Propagates failures while opening the lazy files source.
167    pub fn primary_writable(&self) -> Result<Arc<dyn WritableObjectStore>> {
168        if let Some(primary) = &self.custom_primary {
169            return Ok(Arc::clone(primary));
170        }
171        Ok(self.primary()? as Arc<dyn WritableObjectStore>)
172    }
173
174    /// In-process garbage collection for the files-backed object directory.
175    ///
176    /// Custom primaries return [`Error::UnsupportedObjectStore`].
177    ///
178    /// # Errors
179    ///
180    /// Propagates object-store or I/O failures during maintenance.
181    pub fn gc(&self) -> Result<()> {
182        self.require_files_primary("gc")?;
183        let Some(git_dir) = self.config_git_dir() else {
184            return Err(Error::UnsupportedObjectStore { operation: "gc" });
185        };
186        let opts = crate::environment::RepositoryOptions::empty();
187        let repo = if git_dir.file_name().is_some_and(|n| n == ".git") {
188            let wt = git_dir
189                .parent()
190                .ok_or(Error::UnsupportedObjectStore { operation: "gc" })?;
191            Repository::open(git_dir, &opts.with_work_tree(wt))?
192        } else {
193            Repository::open(git_dir, &opts)?
194        };
195        let roots = reachability_roots(&repo)?;
196        prune_loose_unreachable(self, &roots, None)?;
197        Ok(())
198    }
199
200    /// Write `objects/info/commit-graph` from reachable commits (files primary only).
201    ///
202    /// Writes nothing and deletes any existing graph in a repository that rewrites parent
203    /// lists (shallow, `info/grafts`, or replace refs unless `core.useReplaceRefs=false`), as
204    /// [`crate::maintenance::write_commit_graph`] does.
205    ///
206    /// # Errors
207    ///
208    /// Returns [`Error::UnsupportedObjectStore`] for custom primaries, or errors from graph
209    /// generation or I/O.
210    pub fn write_commit_graph(&self) -> Result<()> {
211        self.require_files_primary("commit-graph write")?;
212        let Some(git_dir) = self.config_git_dir() else {
213            return Err(Error::UnsupportedObjectStore {
214                operation: "commit-graph write",
215            });
216        };
217        let Some(objects_dir) = self.files_objects_dir() else {
218            return Err(Error::UnsupportedObjectStore {
219                operation: "commit-graph write",
220            });
221        };
222        let config = ConfigSet::load(
223            &crate::environment::Environment::empty(),
224            Some(git_dir),
225            true,
226        )
227        .unwrap_or_default();
228        let skip = crate::maintenance::CommitGraphSkipReason::detect(git_dir, || {
229            let enabled = config
230                .get_bool("core.useReplaceRefs")
231                .and_then(|r| r.ok())
232                .unwrap_or(true);
233            Ok(enabled && !crate::refs::list_refs(git_dir, "refs/replace/")?.is_empty())
234        })?;
235        if skip.is_some() {
236            crate::commit_graph_write::remove_commit_graph_files(objects_dir)?;
237            return Ok(());
238        }
239        let bloom = crate::maintenance::bloom_settings_for_commit_graph_write(&config, true);
240        let write_generation = crate::maintenance::commit_graph_writes_generation_data(&config);
241        let commits = collect_reachable_commit_oids(git_dir, self)?;
242        let mut sorted: Vec<ObjectId> = commits.into_iter().collect();
243        sorted.sort();
244        let mut infos = oid_hash_map();
245        for oid in &sorted {
246            infos.insert(*oid, load_commit_graph_commit_info(self, *oid)?);
247        }
248        let chain = CommitGraphChain::try_load(objects_dir).ok().flatten();
249        let mut existing_filters = oid_hash_map();
250        let mut upgraded_filters = oid_hash_map();
251        if let Some(ref chain) = chain {
252            for oid in &sorted {
253                if let Some(bytes) = chain.existing_filter_bytes(oid, &bloom) {
254                    existing_filters.insert(*oid, bytes);
255                } else if let Some(bytes) = chain.upgradable_filter_bytes(oid, &bloom) {
256                    upgraded_filters.insert(*oid, bytes);
257                }
258            }
259        }
260        use std::collections::HashMap;
261        let infos_map: HashMap<_, _> = infos.iter().map(|(k, v)| (*k, v.clone())).collect();
262        let existing_map: HashMap<_, _> = existing_filters
263            .iter()
264            .map(|(k, v)| (*k, v.clone()))
265            .collect();
266        let upgraded_map: HashMap<_, _> = upgraded_filters
267            .iter()
268            .map(|(k, v)| (*k, v.clone()))
269            .collect();
270        let (bytes, _) = build_commit_graph_bytes(
271            &sorted,
272            &infos_map,
273            self,
274            true,
275            &bloom,
276            None,
277            &[],
278            None,
279            &existing_map,
280            &upgraded_map,
281            write_generation,
282        )?;
283        crate::commit_graph_write::atomic_replace_commit_graph(objects_dir, &bytes)?;
284        let _ = crate::commit_graph_write::remove_commit_graph_chain_dir(objects_dir);
285        Ok(())
286    }
287
288    /// Attach a resolved work tree after [`OdbBuilder::build`].
289    #[must_use]
290    pub fn with_resolved_work_tree(mut self, work_tree: Option<PathBuf>) -> Self {
291        self.work_tree = work_tree;
292        self
293    }
294}