Skip to main content

storage/worktree/
lease.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//! Worktree leases: a held tree and its measure.
7
8use std::path::{Path, PathBuf};
9
10use kernel::event::record::WorktreeOpened;
11use kernel::{ByteLen, Payload};
12
13use crate::error::StorageError;
14
15use super::name::WorktreeName;
16
17/// A live tree: what it is called, where it is, and what it costs.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct WorktreeLease {
20    pub(crate) name: WorktreeName,
21    pub(crate) path: PathBuf,
22    pub(crate) disk: ByteLen,
23    pub(crate) work: FileWork,
24}
25
26/// What putting a tree in place cost the filesystem, counted by the code
27/// that did the work (`crates/storage/spec/Worktree.lean` ยง8-31).
28///
29/// A wall-clock reading of the same work moves with the machine and its
30/// real-time scanner, which waits on every file created; these four
31/// numbers do not, so they, and not the clock, are what a regression is
32/// judged by. Directories made along the way are not counted: the
33/// scanner waits on files.
34#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
35pub struct FileWork {
36    /// Files written where no file stood.
37    pub created: u64,
38    /// Files written over because they differed from what the tree
39    /// should hold.
40    pub rewritten: u64,
41    /// Entries taken away because the tree should not hold them; a
42    /// directory counts once, with whatever it held.
43    pub removed: u64,
44    /// Directory entries read.
45    pub walked: u64,
46}
47
48impl WorktreeLease {
49    #[must_use]
50    pub fn name(&self) -> &WorktreeName {
51        &self.name
52    }
53
54    /// The root a run in this node writes against.
55    #[must_use]
56    pub fn path(&self) -> &Path {
57        &self.path
58    }
59
60    /// What the tree occupied when it was opened. `status` reports this,
61    /// which is why it is measured rather than estimated.
62    #[must_use]
63    pub fn disk(&self) -> ByteLen {
64        self.disk
65    }
66
67    /// What claiming this tree cost the filesystem.
68    #[must_use]
69    pub fn work(&self) -> FileWork {
70        self.work
71    }
72
73    /// The `worktree_opened` payload: a name and a size, no path. An
74    /// absolute path is a fact about this machine, and a history that
75    /// carries one does not survive being moved to another.
76    ///
77    /// # Errors
78    /// Propagates the payload's refusal to hold what it was given.
79    pub fn opened_payload(&self) -> Result<Payload, StorageError> {
80        Payload::of(&WorktreeOpened {
81            name: self.name.as_str().to_owned(),
82            disk_bytes: self.disk,
83        })
84        .map_err(|source| StorageError::Draft { source })
85    }
86}
87
88#[cfg(test)]
89#[allow(
90    clippy::unwrap_used,
91    clippy::expect_used,
92    clippy::panic,
93    clippy::indexing_slicing,
94    reason = "test code"
95)]
96mod tests {
97    use super::super::trees::Worktrees;
98    use serde_json::Value;
99
100    use super::*;
101    use crate::checkpoint::Checkpoint;
102    use kernel::TimeMs;
103    use kernel::consts_policy::WORKTREE_MAX_BYTES;
104    fn city(dir: &Path) -> Worktrees {
105        std::fs::create_dir_all(dir.join("lab")).unwrap();
106        std::fs::write(dir.join("lab").join("notes.md"), b"first\n").unwrap();
107        let mut checkpoint = Checkpoint::open(dir).unwrap();
108        checkpoint
109            .ensure_base(
110                &["lab".to_owned()],
111                TimeMs::new(1_000),
112                &super::super::trees::tests::owner(),
113            )
114            .unwrap();
115        Worktrees::open(dir).unwrap()
116    }
117    fn name(raw: &str) -> WorktreeName {
118        WorktreeName::parse(raw).unwrap()
119    }
120
121    #[test]
122    fn the_tree_is_measured_rather_than_estimated() {
123        let dir = tempfile::tempdir().unwrap();
124        let trees = city(dir.path());
125        let lease = trees.claim(&name("node-1"), &[]).unwrap();
126        assert!(
127            lease.disk().get() >= 6,
128            "the checked-out file has six bytes"
129        );
130        assert!(lease.disk().get() < WORKTREE_MAX_BYTES);
131
132        let payload = lease.opened_payload().unwrap();
133        let map = payload.as_map();
134        assert_eq!(map.get("name").and_then(Value::as_str), Some("node-1"));
135        assert!(map.contains_key("disk_bytes"));
136        assert!(
137            !map.contains_key("path"),
138            "an absolute path is a fact about this machine, not about the city"
139        );
140    }
141}