radicle_source/object/
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
18use std::{convert::TryFrom as _, str::FromStr as _};
19
20use serde::{
21    ser::{SerializeStruct as _, Serializer},
22    Serialize,
23};
24
25use radicle_surf::{
26    file_system,
27    vcs::git::{Browser, Rev},
28};
29
30use crate::{
31    commit,
32    error::Error,
33    object::{Info, ObjectType},
34    revision::Revision,
35};
36
37/// Result of a directory listing, carries other trees and blobs.
38pub struct Tree {
39    /// Absolute path to the tree object from the repo root.
40    pub path: String,
41    /// Entries listed in that tree result.
42    pub entries: Vec<TreeEntry>,
43    /// Extra info for the tree object.
44    pub info: Info,
45}
46
47impl Serialize for Tree {
48    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
49    where
50        S: Serializer,
51    {
52        let mut state = serializer.serialize_struct("Tree", 3)?;
53        state.serialize_field("path", &self.path)?;
54        state.serialize_field("entries", &self.entries)?;
55        state.serialize_field("info", &self.info)?;
56        state.end()
57    }
58}
59
60// TODO(xla): Ensure correct by construction.
61/// Entry in a Tree result.
62pub struct TreeEntry {
63    /// Extra info for the entry.
64    pub info: Info,
65    /// Absolute path to the object from the root of the repo.
66    pub path: String,
67}
68
69impl Serialize for TreeEntry {
70    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
71    where
72        S: Serializer,
73    {
74        let mut state = serializer.serialize_struct("Tree", 2)?;
75        state.serialize_field("path", &self.path)?;
76        state.serialize_field("info", &self.info)?;
77        state.end()
78    }
79}
80
81/// Retrieve the [`Tree`] for the given `revision` and directory `prefix`.
82///
83/// # Errors
84///
85/// Will return [`Error`] if any of the surf interactions fail.
86pub fn tree<P>(
87    browser: &mut Browser<'_>,
88    maybe_revision: Option<Revision<P>>,
89    maybe_prefix: Option<String>,
90) -> Result<Tree, Error>
91where
92    P: ToString,
93{
94    let maybe_revision = maybe_revision.map(Rev::try_from).transpose()?;
95    let prefix = maybe_prefix.unwrap_or_default();
96
97    if let Some(revision) = maybe_revision {
98        browser.rev(revision)?;
99    }
100
101    let path = if prefix == "/" || prefix.is_empty() {
102        file_system::Path::root()
103    } else {
104        file_system::Path::from_str(&prefix)?
105    };
106
107    let root_dir = browser.get_directory()?;
108    let prefix_dir = if path.is_root() {
109        root_dir
110    } else {
111        root_dir
112            .find_directory(path.clone())
113            .ok_or_else(|| Error::PathNotFound(path.clone()))?
114    };
115    let mut prefix_contents = prefix_dir.list_directory();
116    prefix_contents.sort();
117
118    let entries_results: Result<Vec<TreeEntry>, Error> = prefix_contents
119        .iter()
120        .map(|(label, system_type)| {
121            let entry_path = if path.is_root() {
122                file_system::Path::new(label.clone())
123            } else {
124                let mut p = path.clone();
125                p.push(label.clone());
126                p
127            };
128            let mut commit_path = file_system::Path::root();
129            commit_path.append(entry_path.clone());
130
131            let info = Info {
132                name: label.to_string(),
133                object_type: match system_type {
134                    file_system::SystemType::Directory => ObjectType::Tree,
135                    file_system::SystemType::File => ObjectType::Blob,
136                },
137                last_commit: None,
138            };
139
140            Ok(TreeEntry {
141                info,
142                path: entry_path.to_string(),
143            })
144        })
145        .collect();
146
147    let mut entries = entries_results?;
148
149    // We want to ensure that in the response Tree entries come first. `Ord` being
150    // derived on the enum ensures Variant declaration order.
151    //
152    // https://doc.rust-lang.org/std/cmp/trait.Ord.html#derivable
153    entries.sort_by(|a, b| a.info.object_type.cmp(&b.info.object_type));
154
155    let last_commit = if path.is_root() {
156        Some(commit::Header::from(browser.get().first()))
157    } else {
158        None
159    };
160    let name = if path.is_root() {
161        "".into()
162    } else {
163        let (_first, last) = path.split_last();
164        last.to_string()
165    };
166    let info = Info {
167        name,
168        object_type: ObjectType::Tree,
169        last_commit,
170    };
171
172    Ok(Tree {
173        path: prefix,
174        entries,
175        info,
176    })
177}