1use lago_core::ManifestEntry;
2use serde::{Deserialize, Serialize};
3use std::collections::BTreeSet;
4
5use crate::manifest::Manifest;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub enum TreeEntry {
10 File { name: String, entry: ManifestEntry },
12 Directory { name: String },
14}
15
16pub fn list_directory(manifest: &Manifest, path: &str) -> Vec<TreeEntry> {
23 let prefix = if path.ends_with('/') {
24 path.to_string()
25 } else {
26 format!("{path}/")
27 };
28
29 let mut files: Vec<TreeEntry> = Vec::new();
30 let mut dirs: BTreeSet<String> = BTreeSet::new();
31
32 for (entry_path, entry) in manifest.entries() {
33 if entry_path == path {
35 continue;
36 }
37
38 let Some(suffix) = entry_path.strip_prefix(&prefix) else {
39 continue;
40 };
41
42 if suffix.is_empty() {
43 continue;
44 }
45
46 if let Some(slash_pos) = suffix.find('/') {
47 let dir_name = &suffix[..slash_pos];
49 dirs.insert(dir_name.to_string());
50 } else {
51 if entry
54 .content_type
55 .as_deref()
56 .is_some_and(|ct| ct == "inode/directory")
57 {
58 dirs.insert(suffix.to_string());
59 } else {
60 files.push(TreeEntry::File {
61 name: suffix.to_string(),
62 entry: entry.clone(),
63 });
64 }
65 }
66 }
67
68 let mut result: Vec<TreeEntry> = dirs
69 .into_iter()
70 .map(|name| TreeEntry::Directory { name })
71 .collect();
72 result.append(&mut files);
73 result
74}
75
76pub fn walk<'a>(manifest: &'a Manifest, path: &str) -> Vec<&'a ManifestEntry> {
80 let prefix = if path.ends_with('/') {
81 path.to_string()
82 } else {
83 format!("{path}/")
84 };
85
86 manifest
87 .entries()
88 .range(prefix.clone()..)
89 .take_while(|(k, _)| k.starts_with(&prefix))
90 .filter(|(_, entry)| {
91 entry
92 .content_type
93 .as_deref()
94 .is_none_or(|ct| ct != "inode/directory")
95 })
96 .map(|(_, v)| v)
97 .collect()
98}
99
100pub fn parent_dirs(path: &str) -> Vec<String> {
105 let mut dirs = Vec::new();
106 let mut current = String::new();
107
108 let trimmed = path.strip_prefix('/').unwrap_or(path);
110
111 let parts: Vec<&str> = trimmed.split('/').collect();
112 for part in &parts[..parts.len().saturating_sub(1)] {
114 current.push('/');
115 current.push_str(part);
116 dirs.push(current.clone());
117 }
118
119 dirs
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use lago_core::BlobHash;
126
127 fn make_manifest() -> Manifest {
128 let mut m = Manifest::new();
129 m.apply_write(
130 "/src/main.rs".to_string(),
131 BlobHash::from_hex("aaa"),
132 100,
133 Some("text/x-rust".to_string()),
134 1,
135 );
136 m.apply_write(
137 "/src/lib.rs".to_string(),
138 BlobHash::from_hex("bbb"),
139 200,
140 Some("text/x-rust".to_string()),
141 2,
142 );
143 m.apply_write(
144 "/src/util/helpers.rs".to_string(),
145 BlobHash::from_hex("ccc"),
146 50,
147 Some("text/x-rust".to_string()),
148 3,
149 );
150 m.apply_write(
151 "/README.md".to_string(),
152 BlobHash::from_hex("ddd"),
153 300,
154 Some("text/markdown".to_string()),
155 4,
156 );
157 m
158 }
159
160 #[test]
161 fn list_root_directory() {
162 let m = make_manifest();
163 let entries = list_directory(&m, "/");
164 let names: Vec<String> = entries
165 .iter()
166 .map(|e| match e {
167 TreeEntry::File { name, .. } => name.clone(),
168 TreeEntry::Directory { name } => format!("{name}/"),
169 })
170 .collect();
171 assert!(names.contains(&"src/".to_string()));
172 assert!(names.contains(&"README.md".to_string()));
173 }
174
175 #[test]
176 fn list_src_directory() {
177 let m = make_manifest();
178 let entries = list_directory(&m, "/src");
179 let names: Vec<String> = entries
180 .iter()
181 .map(|e| match e {
182 TreeEntry::File { name, .. } => name.clone(),
183 TreeEntry::Directory { name } => format!("{name}/"),
184 })
185 .collect();
186 assert!(names.contains(&"main.rs".to_string()));
187 assert!(names.contains(&"lib.rs".to_string()));
188 assert!(names.contains(&"util/".to_string()));
189 }
190
191 #[test]
192 fn walk_all_files() {
193 let m = make_manifest();
194 let files = walk(&m, "/");
195 assert_eq!(files.len(), 4);
197 }
198
199 #[test]
200 fn walk_subdirectory() {
201 let m = make_manifest();
202 let files = walk(&m, "/src");
203 assert_eq!(files.len(), 3);
205 }
206
207 #[test]
208 fn parent_dirs_deep_path() {
209 let dirs = parent_dirs("/a/b/c/d.txt");
210 assert_eq!(dirs, vec!["/a", "/a/b", "/a/b/c"]);
211 }
212
213 #[test]
214 fn parent_dirs_shallow_path() {
215 let dirs = parent_dirs("/file.txt");
216 assert!(dirs.is_empty());
217 }
218
219 #[test]
220 fn parent_dirs_two_levels() {
221 let dirs = parent_dirs("/src/main.rs");
222 assert_eq!(dirs, vec!["/src"]);
223 }
224}