Skip to main content

storage/checkpoint/
sweep.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//! The post-wave sweep: what a checkpoint's tree holds and the working
7//! tree no longer does, as `file_discarded` payloads.
8//!
9//! Specified by `crates/storage/spec/Checkpoint.lean` ยง8-8.
10
11use kernel::Payload;
12use serde_json::{Map, Value};
13
14use crate::error::StorageError;
15
16use super::commit::{Checkpoint, git_err};
17
18impl Checkpoint {
19    /// The post-wave sweep: every path present at `pre_oid` and gone
20    /// from the working tree becomes a `file_discarded` payload whose
21    /// restoration points back into that commit.
22    ///
23    /// **The question is existence, and only existence.** The tree is
24    /// walked once and each blob is looked for on disk; a path that is
25    /// not there is a deletion. Asking git for the tree-to-workdir diff
26    /// instead would hash every path the tree and the worktree agree on,
27    /// and the city itself keeps writing some of those paths after the
28    /// checkpoint (a room's session projection, a worktree under a lease); a
29    /// hash taken through a Windows directory entry that trails the open
30    /// handle refuses the whole sweep. A sweep needs `Deleted` deltas,
31    /// and a deletion is answered without reading any content.
32    ///
33    /// **The tree is the checkpoint's own write domain**, not the whole city:
34    /// `wave_pre` walked exactly these paths before the wave began, so
35    /// the walk costs what staging already cost.
36    ///
37    /// # Errors
38    /// Propagates a `pre_oid` that is not an object, a commit whose tree
39    /// cannot be read, and a working-tree path that cannot be examined.
40    pub fn wave_post(&mut self, pre_oid: &str) -> Result<Vec<Payload>, StorageError> {
41        let oid = git2::Oid::from_str(pre_oid).map_err(git_err("parse checkpoint oid"))?;
42        let commit = self
43            .repo
44            .find_commit(oid)
45            .map_err(git_err("find checkpoint commit"))?;
46        let tree = commit.tree().map_err(git_err("read checkpoint tree"))?;
47        let workdir = self
48            .repo
49            .workdir()
50            .ok_or_else(|| StorageError::Checkpoint {
51                op: "sweep the working tree",
52                detail: "the city repository is bare".to_owned(),
53            })?
54            .to_path_buf();
55        let mut deleted: Vec<String> = Vec::new();
56        let mut fault: Option<StorageError> = None;
57        let walked = tree.walk(git2::TreeWalkMode::PreOrder, |dir, entry| {
58            if entry.kind() != Some(git2::ObjectType::Blob) {
59                return git2::TreeWalkResult::Ok;
60            }
61            // An address is UTF-8, so a name that is not cannot be swept
62            // to a `file:` address; reading it as its directory would hide
63            // a deletion.
64            let Ok(name) = entry.name() else {
65                fault = Some(StorageError::Checkpoint {
66                    op: "sweep the working tree",
67                    detail: format!(
68                        "{dir}{}: the checkpoint holds a name that is not UTF-8",
69                        String::from_utf8_lossy(entry.name_bytes())
70                    ),
71                });
72                return git2::TreeWalkResult::Abort;
73            };
74            let path = format!("{dir}{name}");
75            match std::fs::symlink_metadata(workdir.join(&path)) {
76                Ok(_) => {}
77                Err(err) if err.kind() == std::io::ErrorKind::NotFound => deleted.push(path),
78                Err(err) => {
79                    fault = Some(StorageError::Checkpoint {
80                        op: "sweep the working tree",
81                        detail: format!("{path}: {err}"),
82                    });
83                    return git2::TreeWalkResult::Abort;
84                }
85            }
86            git2::TreeWalkResult::Ok
87        });
88        walked.map_err(git_err("walk the checkpoint tree"))?;
89        if let Some(err) = fault {
90            return Err(err);
91        }
92        // Sorted here rather than trusted from the walk: the order these
93        // records land in is part of what a replay reproduces.
94        deleted.sort();
95        let mut payloads = Vec::new();
96        for path in deleted {
97            let mut map = Map::new();
98            map.insert(
99                "paths".to_owned(),
100                Value::Array(vec![Value::String(format!("file:{path}"))]),
101            );
102            let mut restoration = Map::new();
103            restoration.insert(
104                "tracked".to_owned(),
105                Value::String(format!("file:{path}@{pre_oid}")),
106            );
107            map.insert("restoration".to_owned(), Value::Object(restoration));
108            payloads.push(Payload::new(map).map_err(|source| StorageError::Draft { source })?);
109        }
110        Ok(payloads)
111    }
112}