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