gnostr_asyncgit/sync/
tree.rs

1use 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/// `tree_files` returns a list of `FileTree`
16#[derive(Debug, PartialEq, Eq, Clone)]
17pub struct TreeFile {
18	/// path of this file
19	pub path: PathBuf,
20	/// unix filemode
21	pub filemode: i32,
22	// internal object id
23	id: Oid,
24}
25
26/// guarantees sorting the result
27pub 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
51// applies topologically order on paths sorting
52fn 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
76/// will only work on utf8 content
77pub 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
96///
97fn 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}