gnostr_asyncgit/sync/
tree.rs1use std::{
2 cmp::Ordering,
3 path::{Path, PathBuf},
4};
5
6use git2::{Oid, Repository, Tree};
7use scopetime::scope_time;
8
9use super::{CommitId, RepoPath};
10use crate::{
11 error::{Error, Result},
12 sync::repository::repo,
13};
14
15#[derive(Debug, PartialEq, Eq, Clone)]
17pub struct TreeFile {
18 pub path: PathBuf,
20 pub filemode: i32,
22 id: Oid,
24}
25
26pub fn tree_files(
28 repo_path: &RepoPath,
29 commit: CommitId,
30) -> Result<Vec<TreeFile>> {
31 scope_time!("tree_files");
32
33 let repo = repo(repo_path)?;
34
35 let commit = repo.find_commit(commit.into())?;
36 let tree = commit.tree()?;
37
38 let mut files: Vec<TreeFile> = Vec::new();
39
40 tree_recurse(&repo, &PathBuf::from("./"), &tree, &mut files)?;
41
42 sort_file_list(&mut files);
43
44 Ok(files)
45}
46
47fn sort_file_list(files: &mut [TreeFile]) {
48 files.sort_by(|a, b| path_cmp(&a.path, &b.path));
49}
50
51fn path_cmp(a: &Path, b: &Path) -> Ordering {
53 let mut comp_a = a.components().peekable();
54 let mut comp_b = b.components().peekable();
55
56 loop {
57 let a = comp_a.next();
58 let b = comp_b.next();
59
60 let a_is_file = comp_a.peek().is_none();
61 let b_is_file = comp_b.peek().is_none();
62
63 if a_is_file && !b_is_file {
64 return Ordering::Greater;
65 } else if !a_is_file && b_is_file {
66 return Ordering::Less;
67 }
68
69 let cmp = a.cmp(&b);
70 if cmp != Ordering::Equal {
71 return cmp;
72 }
73 }
74}
75
76pub fn tree_file_content(
78 repo_path: &RepoPath,
79 file: &TreeFile,
80) -> Result<String> {
81 scope_time!("tree_file_content");
82
83 let repo = repo(repo_path)?;
84
85 let blob = repo.find_blob(file.id)?;
86
87 if blob.is_binary() {
88 return Err(Error::BinaryFile);
89 }
90
91 let content = String::from_utf8_lossy(blob.content()).to_string();
92
93 Ok(content)
94}
95
96fn tree_recurse(
98 repo: &Repository,
99 path: &Path,
100 tree: &Tree,
101 out: &mut Vec<TreeFile>,
102) -> Result<()> {
103 out.reserve(tree.len());
104
105 for e in tree {
106 let p = String::from_utf8_lossy(e.name_bytes());
107 let path = path.join(p.to_string());
108 match e.kind() {
109 Some(git2::ObjectType::Blob) => {
110 let id = e.id();
111 let filemode = e.filemode();
112 out.push(TreeFile { path, filemode, id });
113 }
114 Some(git2::ObjectType::Tree) => {
115 let obj = e.to_object(repo)?;
116 let tree = obj.peel_to_tree()?;
117 tree_recurse(repo, &path, &tree, out)?;
118 }
119 Some(_) | None => (),
120 }
121 }
122 Ok(())
123}
124
125#[cfg(test)]
126mod tests {
127 use pretty_assertions::{assert_eq, assert_ne};
128
129 use super::*;
130 use crate::sync::tests::{repo_init, write_commit_file};
131
132 #[test]
133 fn test_smoke() {
134 let (_td, repo) = repo_init().unwrap();
135 let root = repo.path().parent().unwrap();
136 let repo_path: &RepoPath =
137 &root.as_os_str().to_str().unwrap().into();
138
139 let c1 =
140 write_commit_file(&repo, "test.txt", "content", "c1");
141
142 let files = tree_files(repo_path, c1).unwrap();
143
144 assert_eq!(files.len(), 1);
145 assert_eq!(files[0].path, PathBuf::from("./test.txt"));
146
147 let c2 =
148 write_commit_file(&repo, "test.txt", "content2", "c2");
149
150 let content =
151 tree_file_content(repo_path, &files[0]).unwrap();
152 assert_eq!(&content, "content");
153
154 let files_c2 = tree_files(repo_path, c2).unwrap();
155
156 assert_eq!(files_c2.len(), 1);
157 assert_ne!(files_c2[0], files[0]);
158 }
159
160 #[test]
161 fn test_sorting() {
162 let mut list = vec!["file", "folder/file", "folder/afile"]
163 .iter()
164 .map(|f| TreeFile {
165 path: PathBuf::from(f),
166 filemode: 0,
167 id: Oid::zero(),
168 })
169 .collect::<Vec<_>>();
170
171 sort_file_list(&mut list);
172
173 assert_eq!(
174 list.iter()
175 .map(|f| f.path.to_string_lossy())
176 .collect::<Vec<_>>(),
177 vec![
178 String::from("folder/afile"),
179 String::from("folder/file"),
180 String::from("file")
181 ]
182 );
183 }
184
185 #[test]
186 fn test_sorting_folders() {
187 let mut list = vec!["bfolder/file", "afolder/file"]
188 .iter()
189 .map(|f| TreeFile {
190 path: PathBuf::from(f),
191 filemode: 0,
192 id: Oid::zero(),
193 })
194 .collect::<Vec<_>>();
195
196 sort_file_list(&mut list);
197
198 assert_eq!(
199 list.iter()
200 .map(|f| f.path.to_string_lossy())
201 .collect::<Vec<_>>(),
202 vec![
203 String::from("afolder/file"),
204 String::from("bfolder/file"),
205 ]
206 );
207 }
208
209 #[test]
210 fn test_sorting_folders2() {
211 let mut list = vec!["bfolder/sub/file", "afolder/file"]
212 .iter()
213 .map(|f| TreeFile {
214 path: PathBuf::from(f),
215 filemode: 0,
216 id: Oid::zero(),
217 })
218 .collect::<Vec<_>>();
219
220 sort_file_list(&mut list);
221
222 assert_eq!(
223 list.iter()
224 .map(|f| f.path.to_string_lossy())
225 .collect::<Vec<_>>(),
226 vec![
227 String::from("afolder/file"),
228 String::from("bfolder/sub/file"),
229 ]
230 );
231 }
232
233 #[test]
234 fn test_path_cmp() {
235 assert_eq!(
236 path_cmp(
237 &PathBuf::from("bfolder/sub/file"),
238 &PathBuf::from("afolder/file")
239 ),
240 Ordering::Greater
241 );
242 }
243
244 #[test]
245 fn test_path_file_cmp() {
246 assert_eq!(
247 path_cmp(
248 &PathBuf::from("a"),
249 &PathBuf::from("afolder/file")
250 ),
251 Ordering::Greater
252 );
253 }
254}