Skip to main content

objects/store/
shallow.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Shallow state management for incomplete history.
3//!
4//! Shallow clones have "grafted" states where parent history is not available.
5//! This module threads which states are shallow and their grafted parents.
6
7use std::{collections::HashSet, fs, path::Path};
8
9use crate::{fs_atomic::write_file_atomic, object::ChangeId, store::Result};
10
11/// Manages shallow state information.
12pub struct ShallowInfo {
13    path: std::path::PathBuf,
14    shallow_states: HashSet<ChangeId>,
15}
16
17impl ShallowInfo {
18    /// Load shallow info from a repository.
19    pub fn load(heddle_dir: &Path) -> Result<Self> {
20        let path = heddle_dir.join("shallow");
21        let shallow_states = if path.exists() {
22            let contents = fs::read_to_string(&path)?;
23            contents
24                .lines()
25                .filter_map(|line| {
26                    let line = line.trim();
27                    if line.is_empty() || line.starts_with('#') {
28                        None
29                    } else {
30                        ChangeId::parse(line).ok()
31                    }
32                })
33                .collect()
34        } else {
35            HashSet::new()
36        };
37
38        Ok(Self {
39            path,
40            shallow_states,
41        })
42    }
43
44    /// Check if a state is shallow (has grafted parents).
45    pub fn is_shallow(&self, id: &ChangeId) -> bool {
46        self.shallow_states.contains(id)
47    }
48
49    /// Get all shallow states.
50    pub fn shallow_states(&self) -> &HashSet<ChangeId> {
51        &self.shallow_states
52    }
53
54    /// Add a shallow state.
55    pub fn add_shallow(&mut self, id: ChangeId) -> Result<()> {
56        if self.shallow_states.insert(id) {
57            self.save()?;
58        }
59        Ok(())
60    }
61
62    /// Remove a shallow state (when history is unshallowed).
63    pub fn remove_shallow(&mut self, id: &ChangeId) -> Result<()> {
64        if self.shallow_states.remove(id) {
65            self.save()?;
66        }
67        Ok(())
68    }
69
70    /// Save shallow info to disk.
71    fn save(&self) -> Result<()> {
72        let contents: String = self
73            .shallow_states
74            .iter()
75            .map(|id| format!("{}\n", id.to_string_full()))
76            .collect();
77
78        write_file_atomic(&self.path, contents.as_bytes())?;
79        Ok(())
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use tempfile::TempDir;
86
87    use super::*;
88
89    #[test]
90    fn test_shallow_save_rewrites_file_without_temp_residue() {
91        let temp_dir = TempDir::new().unwrap();
92        let heddle_dir = temp_dir.path().join(".heddle");
93        fs::create_dir_all(&heddle_dir).unwrap();
94
95        let mut shallow = ShallowInfo::load(&heddle_dir).unwrap();
96        let id = ChangeId::generate();
97        shallow.add_shallow(id).unwrap();
98
99        let reloaded = ShallowInfo::load(&heddle_dir).unwrap();
100        assert!(reloaded.is_shallow(&id));
101
102        let temp_entries = fs::read_dir(&heddle_dir)
103            .unwrap()
104            .filter_map(|entry| entry.ok())
105            .filter(|entry| entry.file_name().to_string_lossy().contains(".tmp-"))
106            .count();
107        assert_eq!(temp_entries, 0);
108    }
109}