Skip to main content

thin_fetch/
checkout.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::object::{Commit, Object, Tree};
4use crate::{Error, WithError as _, bail, error};
5
6/// Commit reference with all entries collected from the repo.
7#[derive(Debug)]
8pub struct Checkout<'a> {
9    pub commit: &'a Commit,
10    pub entries: HashMap<String, DirEntry<'a>>,
11}
12
13impl<'a> Checkout<'a> {
14    /// Collect checkout from a repository of objects.
15    ///
16    /// # Errors
17    ///
18    /// Returns an error when an object cannot be found.
19    pub fn from_pack(pack: &'a HashMap<String, Object>, oid: &str) -> Result<Self, Error> {
20        let commit_obj = pack
21            .get(oid)
22            .with_err(|| error!("commit {oid} not found"))?;
23
24        let Object::Commit(commit) = commit_obj else {
25            bail!("expected {oid} to be a commit, got {commit_obj:?}")
26        };
27
28        let tree_id = commit.commit.tree.to_string();
29
30        let tree_obj = pack
31            .get(&tree_id)
32            .with_err(|| error!("root tree {tree_id} not found"))?;
33
34        let mut entries = HashMap::new();
35
36        let Object::Tree(tree) = tree_obj else {
37            bail!("expected {tree_id} to be a tree, got {tree_obj:?}")
38        };
39
40        collect_entries(pack, tree, ".", &mut entries);
41
42        Ok(Self { commit, entries })
43    }
44
45    /// Get difference between checkouts.
46    #[must_use]
47    pub fn get_diff(&self, other: &Checkout) -> Vec<BlobDiff> {
48        let all_paths = self
49            .entries
50            .values()
51            .chain(other.entries.values())
52            .filter(|entry| !entry.dir)
53            .map(|entry| entry.path.as_str())
54            .collect::<HashSet<&str>>();
55
56        let mut result = vec![];
57
58        for path in all_paths {
59            let this = self.entries.get(path);
60            let other = other.entries.get(path);
61
62            if this != other {
63                let old = other.map(|a| a.hash.clone());
64                let new = this.map(|a| a.hash.clone());
65
66                result.push(BlobDiff {
67                    path: path.to_owned(),
68                    old_oid: old,
69                    new_oid: new,
70                });
71            }
72        }
73
74        result.sort_by(|a, b| a.path.cmp(&b.path));
75
76        result
77    }
78}
79
80fn collect_entries<'a>(
81    pack: &'a HashMap<String, Object>,
82    tree: &'a Tree,
83    path: &str,
84    entries: &mut HashMap<String, DirEntry<'a>>,
85) {
86    for entry in &tree.entries {
87        let hash = &entry.hash;
88        let path = format!("{path}/{}", entry.name);
89
90        let object = pack.get(hash);
91
92        if let Some(Object::Tree(tree)) = object {
93            collect_entries(pack, tree, &path, entries);
94        }
95
96        entries.insert(
97            path.clone(),
98            DirEntry {
99                path,
100                hash: hash.clone(),
101                dir: entry.is_dir(),
102                object,
103            },
104        );
105    }
106}
107
108#[derive(Debug, Clone)]
109pub struct BlobDiff {
110    pub path: String,
111
112    pub old_oid: Option<String>,
113    pub new_oid: Option<String>,
114}
115
116#[derive(Debug, Clone)]
117pub struct DirEntry<'a> {
118    pub path: String,
119    pub hash: String,
120    pub dir: bool,
121    pub object: Option<&'a Object>,
122}
123
124impl PartialEq<DirEntry<'_>> for DirEntry<'_> {
125    fn eq(&self, other: &DirEntry) -> bool {
126        self.path == other.path && self.hash == other.hash
127    }
128}