1use std::path::{Path, PathBuf};
2
3use crate::config::Config;
4use anyhow::Result;
5use fs_err as fs;
6
7pub const CONFIG_FILE_NAME: &str = "dvs.toml";
8pub const DEFAULT_FOLDER_NAME: &str = ".dvs";
9
10#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum AddPathStatus {
13 Valid,
15 NotFound,
17 IsDirectory,
19 OutsideProject,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum GetPathStatus {
26 Tracked,
28 NotFound,
30 NotTracked,
32}
33
34pub fn find_repo_root(start_dir: impl AsRef<Path>) -> PathBuf {
39 let mut dir = start_dir.as_ref();
40 log::debug!("Searching for repo root starting from {}", dir.display());
41
42 loop {
43 if dir.join(CONFIG_FILE_NAME).exists() {
44 log::debug!("Found repo root at {}", dir.display());
45 return dir.to_path_buf();
46 }
47
48 if let Some(parent) = dir.parent() {
49 dir = parent;
50 } else {
51 break;
52 }
53 }
54
55 start_dir.as_ref().to_path_buf()
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct DvsPaths {
63 cwd: PathBuf,
65 repo_root: PathBuf,
67 metadata_folder_name: String,
69}
70
71impl DvsPaths {
72 pub fn new(
74 cwd: PathBuf,
75 repo_root: PathBuf,
76 metadata_folder_name: impl Into<String>,
77 ) -> Result<Self> {
78 Ok(Self {
79 cwd,
80 repo_root: fs::canonicalize(&repo_root)?,
81 metadata_folder_name: metadata_folder_name.into(),
82 })
83 }
84
85 pub fn from_cwd(config: &Config) -> Result<Self> {
86 let cwd = fs::canonicalize(std::env::current_dir()?)?;
87 let repo_root = fs::canonicalize(find_repo_root(&cwd))?;
88
89 log::debug!(
90 "Resolved paths: cwd={}, repo_root={}",
91 cwd.display(),
92 repo_root.display()
93 );
94 Ok(Self {
95 cwd,
96 repo_root,
97 metadata_folder_name: config.metadata_folder_name().to_owned(),
98 })
99 }
100
101 pub fn metadata_folder(&self) -> PathBuf {
102 self.repo_root.join(&self.metadata_folder_name)
103 }
104
105 pub fn cache_folder(&self) -> PathBuf {
106 self.metadata_folder().join(".cache")
107 }
108
109 pub fn metadata_path(&self, relative: &Path) -> PathBuf {
110 let dvs_path = self.metadata_folder().join(relative);
111 let mut s = dvs_path.into_os_string();
112 s.push(".dvs");
113 PathBuf::from(s)
114 }
115
116 pub fn repo_root(&self) -> &Path {
117 &self.repo_root
118 }
119
120 pub fn cwd(&self) -> &Path {
121 &self.cwd
122 }
123
124 pub fn cwd_relative_to_root(&self) -> Option<&Path> {
126 self.cwd
127 .strip_prefix(&self.repo_root)
128 .ok()
129 .filter(|p| !p.as_os_str().is_empty())
130 }
131
132 pub fn file_path(&self, relative: &Path) -> PathBuf {
134 self.repo_root.join(relative)
135 }
136
137 pub fn validate_for_add(&self, paths: &[PathBuf]) -> Vec<(PathBuf, AddPathStatus)> {
138 let mut found = Vec::new();
139 for path in paths {
140 let file_path = self.file_path(path);
141 let status = match file_path.canonicalize() {
142 Ok(canonical) => {
143 if !canonical.starts_with(&self.repo_root) {
144 AddPathStatus::OutsideProject
145 } else if canonical.is_dir() {
146 AddPathStatus::IsDirectory
147 } else if canonical.is_file() {
148 AddPathStatus::Valid
149 } else {
150 AddPathStatus::NotFound
151 }
152 }
153 Err(_) => AddPathStatus::NotFound,
154 };
155 found.push((path.clone(), status));
156 }
157 found
158 }
159
160 pub fn validate_for_get(&self, paths: &[PathBuf]) -> Vec<(PathBuf, GetPathStatus)> {
161 let mut found = Vec::new();
162 for path in paths {
163 let metadata_path = self.metadata_path(path);
164 let validation = if metadata_path.is_file() {
165 GetPathStatus::Tracked
166 } else if self.file_path(path).is_file() {
167 GetPathStatus::NotTracked
168 } else {
169 GetPathStatus::NotFound
170 };
171 found.push((path.clone(), validation));
172 }
173 found
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::testutil::create_temp_git_repo;
181
182 #[test]
183 fn metadata_path_returns_dvs_file_path() {
184 let (_tmp, root) = create_temp_git_repo();
185 let paths = DvsPaths::new(root.clone(), root.clone(), ".meta").unwrap();
186
187 let result = paths.metadata_path(Path::new("sub/file.txt"));
188 assert_eq!(result, root.join(".meta/sub/file.txt.dvs"));
189 }
190
191 #[test]
192 fn validate_for_add() {
193 let (_tmp, root) = create_temp_git_repo();
194 let paths = DvsPaths::new(root.clone(), root.clone(), ".dvs").unwrap();
195
196 fs_err::write(root.join("test.txt"), b"content").unwrap();
198
199 let outside_tmp = tempfile::tempdir().unwrap();
201 let outside_path = fs::canonicalize(outside_tmp.path()).unwrap();
202 fs_err::write(outside_path.join("outside.txt"), b"outside").unwrap();
203 let outside_relative = PathBuf::from("..").join(
204 outside_path
205 .join("outside.txt")
206 .strip_prefix(root.parent().unwrap())
207 .unwrap(),
208 );
209
210 fs_err::create_dir(root.join("subdir")).unwrap();
212
213 let result = paths.validate_for_add(&[
214 PathBuf::from("test.txt"),
215 PathBuf::from("nonexistent.txt"),
216 outside_relative,
217 PathBuf::from("subdir"),
218 ]);
219
220 assert_eq!(result[0].1, AddPathStatus::Valid);
221 assert_eq!(result[1].1, AddPathStatus::NotFound);
222 assert_eq!(result[2].1, AddPathStatus::OutsideProject);
223 assert_eq!(result[3].1, AddPathStatus::IsDirectory);
224 }
225}