Skip to main content

storage/checkpoint/
base.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! A building's base checkpoint, written as one pack.
7
8use std::io::Write;
9
10use kernel::{Payload, TimeMs};
11
12use crate::error::StorageError;
13
14use super::commit::{Checkpoint, checkpoint_ref, committed, git_err, subject_of};
15use super::provenance::Provenance;
16use super::scan::CommitPlan;
17
18/// How far a base checkpoint has got, reported as each step lands.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum BaseProgress {
21    /// The building's files are in the index and their blobs in memory.
22    Staged { files: usize },
23    /// The one pack holding every object the checkpoint wrote is on disk.
24    Packed { bytes: usize },
25}
26
27impl Checkpoint {
28    /// Stages `scopes`, scans them and commits, with every object held
29    /// in memory until the end and then written as one pack.
30    ///
31    /// Takes the handle by value: the in-memory store is installed on
32    /// this handle's object database and cannot be taken off again, so
33    /// the handle goes with it, and no later checkpoint can write an object
34    /// into a store nobody dumps (`crates/storage/spec/Checkpoint.lean` ยง8-8).
35    ///
36    /// The commit moves HEAD when the city has none, and is otherwise
37    /// filed under `refs/sprawling/runs/<run>/<oid>` like a wave checkpoint.
38    ///
39    /// # Errors
40    /// Propagates staging, the staged-secret scan, the commit, a pack the
41    /// object database refuses, and a reference it will not write.
42    pub fn base_checkpoint(
43        mut self,
44        scopes: &[String],
45        t: TimeMs,
46        of: &Provenance,
47        progress: &mut dyn FnMut(BaseProgress),
48    ) -> Result<Payload, StorageError> {
49        let objects = self.repo.path().join("objects");
50        let objects = objects.to_str().ok_or_else(|| StorageError::Checkpoint {
51            op: "open the object store",
52            detail: format!("{} is not valid UTF-8", objects.display()),
53        })?;
54        // The disk store is an alternate, and libgit2 writes to no
55        // alternate: every object this checkpoint makes lands in memory.
56        let odb = git2::Odb::new().map_err(git_err("open an in-memory object store"))?;
57        let held = odb
58            .add_new_mempack_backend(1)
59            .map_err(git_err("open an in-memory object store"))?;
60        odb.add_disk_alternate(objects)
61            .map_err(git_err("read the object store"))?;
62        self.repo
63            .set_odb(&odb)
64            .map_err(git_err("hold the base checkpoint in memory"))?;
65
66        let files = self.stage_scopes_held(scopes)?;
67        progress(BaseProgress::Staged { files: files.len() });
68        self.scan_staged()?;
69        // The same question `ensure_base` asks: a city with no HEAD has
70        // no base yet, and this commit becomes it.
71        let onto_head = self.repo.head().is_err();
72        let oid = self.commit(&CommitPlan {
73            t,
74            of,
75            subject: &subject_of(scopes),
76            onto_head: false,
77        })?;
78
79        let mut pack = git2::Buf::new();
80        held.dump(&self.repo, &mut pack)
81            .map_err(git_err("dump the base checkpoint"))?;
82        let disk =
83            git2::Repository::open(self.repo.path()).map_err(git_err("open the object store"))?;
84        let disk_odb = disk.odb().map_err(git_err("open the object store"))?;
85        let mut writer = disk_odb
86            .packwriter()
87            .map_err(git_err("write the base checkpoint pack"))?;
88        writer
89            .write_all(&pack)
90            .map_err(|err| StorageError::Checkpoint {
91                op: "write the base checkpoint pack",
92                detail: err.to_string(),
93            })?;
94        writer
95            .commit()
96            .map_err(git_err("write the base checkpoint pack"))?;
97        progress(BaseProgress::Packed { bytes: pack.len() });
98
99        // Only now are the objects on disk, so only now may the index and
100        // a reference name them: a failure above leaves both as they were.
101        super::scan::write_index(&mut self.repo.index().map_err(git_err("read index"))?)?;
102        let name = if onto_head {
103            self.repo
104                .find_reference("HEAD")
105                .map_err(git_err("read HEAD"))?
106                .symbolic_target()
107                .map_err(|err| StorageError::Checkpoint {
108                    op: "read HEAD",
109                    detail: err.to_string(),
110                })?
111                .map(str::to_owned)
112                .ok_or_else(|| StorageError::Checkpoint {
113                    op: "move HEAD to the base checkpoint",
114                    detail: "HEAD names no branch".to_owned(),
115                })?
116        } else {
117            checkpoint_ref(of, oid)
118        };
119        self.repo
120            .reference(&name, oid, true, "sprawling: a base checkpoint")
121            .map_err(git_err("file a base checkpoint"))?;
122        held.reset()
123            .map_err(git_err("release the base checkpoint"))?;
124        committed(oid, of, scopes, files)
125    }
126}
127
128#[cfg(test)]
129#[allow(
130    clippy::unwrap_used,
131    clippy::expect_used,
132    clippy::panic,
133    clippy::indexing_slicing,
134    reason = "test code"
135)]
136mod tests;