Skip to main content

git_side/
tracked.rs

1use std::collections::BTreeSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4
5use crate::error::{Error, Result};
6use crate::side_repo::SideRepo;
7
8/// Manages the .side-tracked file.
9pub struct TrackedPaths {
10    file_path: PathBuf,
11    paths: BTreeSet<PathBuf>,
12}
13
14impl TrackedPaths {
15    /// Load tracked paths from the side repo.
16    ///
17    /// # Errors
18    ///
19    /// Returns an error if the tracked file exists but cannot be read.
20    pub fn load(repo: &SideRepo) -> Result<Self> {
21        let file_path = repo.tracked_file();
22        let paths = if file_path.exists() {
23            let content = fs::read_to_string(&file_path).map_err(|e| Error::ReadFile {
24                path: file_path.clone(),
25                source: e,
26            })?;
27            content
28                .lines()
29                .filter(|line| !line.trim().is_empty())
30                .map(PathBuf::from)
31                .collect()
32        } else {
33            BTreeSet::new()
34        };
35
36        Ok(Self { file_path, paths })
37    }
38
39    /// Save tracked paths to disk.
40    ///
41    /// # Errors
42    ///
43    /// Returns an error if the file cannot be written.
44    pub fn save(&self) -> Result<()> {
45        // Ensure parent directory exists
46        if let Some(parent) = self.file_path.parent()
47            && !parent.exists()
48        {
49            fs::create_dir_all(parent).map_err(|e| Error::CreateDir {
50                path: parent.to_path_buf(),
51                source: e,
52            })?;
53        }
54
55        let content: String = self
56            .paths
57            .iter()
58            .map(|p| p.to_string_lossy().to_string())
59            .collect::<Vec<_>>()
60            .join("\n");
61
62        fs::write(&self.file_path, content).map_err(|e| Error::WriteFile {
63            path: self.file_path.clone(),
64            source: e,
65        })
66    }
67
68    /// Add a path to track.
69    pub fn add(&mut self, path: &Path) -> bool {
70        self.paths.insert(path.to_path_buf())
71    }
72
73    /// Remove a path from tracking.
74    pub fn remove(&mut self, path: &Path) -> bool {
75        self.paths.remove(path)
76    }
77
78    /// Check if a path is tracked.
79    #[must_use]
80    pub fn contains(&self, path: &Path) -> bool {
81        self.paths.contains(path)
82    }
83
84    /// Check if there are any tracked paths.
85    #[must_use]
86    pub fn is_empty(&self) -> bool {
87        self.paths.is_empty()
88    }
89
90    /// Get all tracked paths.
91    #[must_use]
92    pub const fn paths(&self) -> &BTreeSet<PathBuf> {
93        &self.paths
94    }
95
96    /// Expand all tracked paths to actual files on disk.
97    /// Directories are walked recursively.
98    #[must_use]
99    pub fn expand(&self, work_tree: &Path) -> Vec<PathBuf> {
100        let mut files = Vec::new();
101
102        for path in &self.paths {
103            let full_path = work_tree.join(path);
104            if full_path.is_file() {
105                files.push(path.clone());
106            } else if full_path.is_dir() {
107                Self::walk_dir(&full_path, path, &mut files);
108            }
109            // If path doesn't exist, skip it (will be handled as deletion)
110        }
111
112        files
113    }
114
115    /// Recursively walk a directory and collect all files.
116    fn walk_dir(dir: &Path, relative_base: &Path, files: &mut Vec<PathBuf>) {
117        if let Ok(entries) = fs::read_dir(dir) {
118            for entry in entries.flatten() {
119                let entry_path = entry.path();
120                let relative = relative_base.join(entry.file_name());
121
122                if entry_path.is_file() {
123                    files.push(relative);
124                } else if entry_path.is_dir() {
125                    Self::walk_dir(&entry_path, &relative, files);
126                }
127            }
128        }
129    }
130}