radicle_surf/
tree.rs

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