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(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
48// applies topologically order on paths sorting
49fn 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
73/// will only work on utf8 content
74pub 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
90///
91fn 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}