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