Skip to main content

radicle_surf/
tree.rs

1//! Represents git object type 'tree', i.e. like directory entries in Unix.
2//! See git [doc](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects) for more details.
3
4use std::cmp::Ordering;
5use std::path::PathBuf;
6
7use radicle_git_ext::Oid;
8#[cfg(feature = "serde")]
9use serde::{
10    ser::{SerializeStruct as _, Serializer},
11    Serialize,
12};
13use url::Url;
14
15use crate::{fs, Commit, Error, Repository};
16
17/// Represents a tree object as in git. It is essentially the content of
18/// one directory. Note that multiple directories can have the same content,
19/// i.e. have the same tree object. Hence this struct does not embed its path.
20#[derive(Clone, Debug)]
21pub struct Tree {
22    /// The object id of this tree.
23    id: Oid,
24    /// The first descendant entries for this tree.
25    entries: Vec<Entry>,
26    /// The commit object that created this tree object.
27    commit: Commit,
28    /// The root path this tree was constructed from.
29    root: PathBuf,
30}
31
32#[derive(Debug, thiserror::Error)]
33pub enum LastCommitError {
34    #[error(transparent)]
35    Repo(#[from] Error),
36    #[error("could not get the last commit for this entry")]
37    Missing,
38}
39
40impl Tree {
41    /// Creates a new tree, ensuring the `entries` are sorted.
42    pub(crate) fn new(id: Oid, mut entries: Vec<Entry>, commit: Commit, root: PathBuf) -> Self {
43        entries.sort();
44        Self {
45            id,
46            entries,
47            commit,
48            root,
49        }
50    }
51
52    pub fn object_id(&self) -> Oid {
53        self.id
54    }
55
56    /// Returns the commit for which this [`Tree`] was constructed from.
57    pub fn commit(&self) -> &Commit {
58        &self.commit
59    }
60
61    /// Returns the commit that last touched this [`Tree`].
62    pub fn last_commit(&self, repo: &Repository) -> Result<Commit, LastCommitError> {
63        repo.last_commit(&self.root, self.commit().clone())?
64            .ok_or(LastCommitError::Missing)
65    }
66
67    /// Returns the entries of the tree.
68    pub fn entries(&self) -> &Vec<Entry> {
69        &self.entries
70    }
71}
72
73#[cfg(feature = "serde")]
74impl Serialize for Tree {
75    /// Sample output:
76    /// (for `<entry_1>` and `<entry_2>` sample output, see [`Entry`])
77    /// ```
78    /// {
79    ///   "entries": [
80    ///     { <entry_1> },
81    ///     { <entry_2> },
82    ///   ],
83    ///   "root": "src/foo",
84    ///   "commit": {
85    ///     "author": {
86    ///       "email": "foobar@gmail.com",
87    ///       "name": "Foo Bar"
88    ///     },
89    ///     "committer": {
90    ///       "email": "noreply@github.com",
91    ///       "name": "GitHub"
92    ///     },
93    ///     "committerTime": 1582198877,
94    ///     "description": "A sample commit.",
95    ///     "sha1": "b57846bbc8ced6587bf8329fc4bce970eb7b757e",
96    ///     "summary": "Add a new sample"
97    ///   },
98    ///   "oid": "dd52e9f8dfe1d8b374b2a118c25235349a743dd2"
99    /// }
100    /// ```
101    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
102    where
103        S: Serializer,
104    {
105        const FIELDS: usize = 4;
106        let mut state = serializer.serialize_struct("Tree", FIELDS)?;
107        state.serialize_field("oid", &self.id)?;
108        state.serialize_field("entries", &self.entries)?;
109        state.serialize_field("commit", &self.commit)?;
110        state.serialize_field("root", &self.root)?;
111        state.end()
112    }
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
116pub enum EntryKind {
117    Tree(Oid),
118    Blob(Oid),
119    Submodule { id: Oid, url: Option<Url> },
120}
121
122impl PartialOrd for EntryKind {
123    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
124        Some(self.cmp(other))
125    }
126}
127
128impl Ord for EntryKind {
129    fn cmp(&self, other: &Self) -> Ordering {
130        match (self, other) {
131            (EntryKind::Submodule { .. }, EntryKind::Submodule { .. }) => Ordering::Equal,
132            (EntryKind::Submodule { .. }, EntryKind::Tree(_)) => Ordering::Equal,
133            (EntryKind::Tree(_), EntryKind::Submodule { .. }) => Ordering::Equal,
134            (EntryKind::Tree(_), EntryKind::Tree(_)) => Ordering::Equal,
135            (EntryKind::Tree(_), EntryKind::Blob(_)) => Ordering::Less,
136            (EntryKind::Blob(_), EntryKind::Tree(_)) => Ordering::Greater,
137            (EntryKind::Submodule { .. }, EntryKind::Blob(_)) => Ordering::Less,
138            (EntryKind::Blob(_), EntryKind::Submodule { .. }) => Ordering::Greater,
139            (EntryKind::Blob(_), EntryKind::Blob(_)) => Ordering::Equal,
140        }
141    }
142}
143
144/// An entry that can be found in a tree.
145///
146/// # Ordering
147///
148/// The ordering of a [`Entry`] is first by its `entry` where
149/// [`EntryKind::Tree`]s come before [`EntryKind::Blob`]. If both kinds
150/// are equal then they are next compared by the lexicographical ordering
151/// of their `name`s.
152#[derive(Clone, Debug)]
153pub struct Entry {
154    name: String,
155    entry: EntryKind,
156    path: PathBuf,
157    /// The commit from which this entry was constructed from.
158    commit: Commit,
159}
160
161impl Entry {
162    pub(crate) fn new(name: String, path: PathBuf, entry: EntryKind, commit: Commit) -> Self {
163        Self {
164            name,
165            entry,
166            path,
167            commit,
168        }
169    }
170
171    pub fn name(&self) -> &str {
172        &self.name
173    }
174
175    /// The full path to this entry from the root of the Git repository
176    pub fn path(&self) -> &PathBuf {
177        &self.path
178    }
179
180    pub fn entry(&self) -> &EntryKind {
181        &self.entry
182    }
183
184    pub fn is_tree(&self) -> bool {
185        matches!(self.entry, EntryKind::Tree(_))
186    }
187
188    pub fn commit(&self) -> &Commit {
189        &self.commit
190    }
191
192    pub fn object_id(&self) -> Oid {
193        match self.entry {
194            EntryKind::Blob(id) => id,
195            EntryKind::Tree(id) => id,
196            EntryKind::Submodule { id, .. } => id,
197        }
198    }
199
200    /// Returns the commit that last touched this [`Entry`].
201    pub fn last_commit(&self, repo: &Repository) -> Result<Commit, LastCommitError> {
202        repo.last_commit(&self.path, self.commit.clone())?
203            .ok_or(LastCommitError::Missing)
204    }
205}
206
207// To support `sort`.
208impl Ord for Entry {
209    fn cmp(&self, other: &Self) -> Ordering {
210        self.entry
211            .cmp(&other.entry)
212            .then(self.name.cmp(&other.name))
213    }
214}
215
216impl PartialOrd for Entry {
217    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
218        Some(self.cmp(other))
219    }
220}
221
222impl PartialEq for Entry {
223    fn eq(&self, other: &Self) -> bool {
224        self.entry == other.entry && self.name == other.name
225    }
226}
227
228impl Eq for Entry {}
229
230impl From<fs::Entry> for EntryKind {
231    fn from(entry: fs::Entry) -> Self {
232        match entry {
233            fs::Entry::File(f) => EntryKind::Blob(f.id()),
234            fs::Entry::Directory(d) => EntryKind::Tree(d.id()),
235            fs::Entry::Submodule(u) => EntryKind::Submodule {
236                id: u.id(),
237                url: u.url().clone(),
238            },
239        }
240    }
241}
242
243#[cfg(feature = "serde")]
244impl Serialize for Entry {
245    /// Sample output:
246    /// ```json
247    ///  {
248    ///     "kind": "blob",
249    ///     "commit": {
250    ///       "author": {
251    ///         "email": "foobar@gmail.com",
252    ///         "name": "Foo Bar"
253    ///       },
254    ///       "committer": {
255    ///         "email": "noreply@github.com",
256    ///         "name": "GitHub"
257    ///       },
258    ///       "committerTime": 1578309972,
259    ///       "description": "This is a sample file",
260    ///       "sha1": "2873745c8f6ffb45c990eb23b491d4b4b6182f95",
261    ///       "summary": "Add a new sample"
262    ///     },
263    ///     "path": "src/foo/Sample.rs",
264    ///     "name": "Sample.rs",
265    ///     "oid": "6d6240123a8d8ea8a8376610168a0a4bcb96afd0"
266    ///   },
267    /// ```
268    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
269    where
270        S: Serializer,
271    {
272        const FIELDS: usize = 5;
273        let mut state = serializer.serialize_struct("TreeEntry", FIELDS)?;
274        state.serialize_field("name", &self.name)?;
275        state.serialize_field(
276            "kind",
277            match self.entry {
278                EntryKind::Blob(_) => "blob",
279                EntryKind::Tree(_) => "tree",
280                EntryKind::Submodule { .. } => "submodule",
281            },
282        )?;
283        if let EntryKind::Submodule { url: Some(url), .. } = &self.entry {
284            state.serialize_field("url", url)?;
285        };
286        state.serialize_field("oid", &self.object_id())?;
287        state.serialize_field("commit", &self.path)?;
288        state.end()
289    }
290}