gitscope
← rgit / main / src/methods/repo/tree.rs
https://dev.kronoz.win/r/rgit.git
src/methods/repo/tree.rs · Rust
Blame Raw
  1use anyhow::{Context, bail};
  2use askama::Template;
  3use axum::{Extension, extract::Query, response::IntoResponse};
  4use gix::ObjectId;
  5use itertools::Itertools;
  6use serde::Deserialize;
  7use std::path::PathBuf;
  8use std::{
  9    fmt::{Display, Formatter},
 10    path::Path,
 11    sync::Arc,
 12};
 13
 14use crate::database::schema::tree::{
 15    ArchivedSortedTree, ArchivedSortedTreeItem, ArchivedTreeItemKind, SortedTree, Tree, TreeItem,
 16    YokedSortedTree, YokedTreeItem, YokedTreeItemKeyUtf8,
 17};
 18use crate::{
 19    Git, ResponseEither,
 20    git::FileWithContent,
 21    into_response,
 22    methods::{
 23        filters,
 24        repo::{ChildPath, Repository, RepositoryPath, Result},
 25    },
 26};
 27
 28use super::log::get_branch_commits;
 29
 30#[derive(Deserialize)]
 31pub struct UriQuery {
 32    id: Option<String>,
 33    #[serde(default)]
 34    raw: bool,
 35    #[serde(rename = "h")]
 36    branch: Option<Arc<str>>,
 37}
 38
 39impl Display for UriQuery {
 40    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
 41        let mut prefix = "?";
 42
 43        if let Some(id) = self.id.as_deref() {
 44            write!(f, "{prefix}id={id}")?;
 45            prefix = "&";
 46        }
 47
 48        if let Some(branch) = self.branch.as_deref() {
 49            write!(f, "{prefix}h={branch}")?;
 50        }
 51
 52        Ok(())
 53    }
 54}
 55
 56#[derive(Template)]
 57#[template(path = "partials/file_tree.html")]
 58pub struct FileTree<'a> {
 59    pub inner: &'a ArchivedSortedTree,
 60    pub base: &'a Repository,
 61    pub path_stack: String,
 62    pub query: &'a UriQuery,
 63    pub repo_path: Option<&'a Path>,
 64}
 65
 66impl<'a> FileTree<'a> {
 67    pub fn new(
 68        inner: &'a ArchivedSortedTree,
 69        base: &'a Repository,
 70        path_stack: String,
 71        query: &'a UriQuery,
 72        repo_path: Option<&'a Path>,
 73    ) -> Self {
 74        Self {
 75            inner,
 76            base,
 77            path_stack,
 78            query,
 79            repo_path,
 80        }
 81    }
 82
 83    pub fn get_next_repo_path_if_parent(&self, name: &str) -> Option<&Path> {
 84        self.repo_path.and_then(|v| v.strip_prefix(name).ok())
 85    }
 86}
 87
 88#[derive(Template)]
 89#[template(path = "repo/tree.html")]
 90#[allow(clippy::module_name_repetitions)]
 91pub struct TreeView {
 92    pub repo: Repository,
 93    pub items: Vec<(YokedTreeItemKeyUtf8, usize, YokedTreeItem)>,
 94    pub query: UriQuery,
 95    pub repo_path: PathBuf,
 96    pub branch: Option<Arc<str>>,
 97    pub full_tree: YokedSortedTree,
 98}
 99
100#[derive(Template)]
101#[template(path = "repo/file.html")]
102pub struct FileView {
103    pub repo: Repository,
104    pub repo_path: PathBuf,
105    pub query: UriQuery,
106    pub file: FileWithContent,
107    pub branch: Option<Arc<str>>,
108    pub full_tree: YokedSortedTree,
109}
110
111enum LookupResult {
112    RealPath,
113    Children(Vec<(YokedTreeItemKeyUtf8, usize, YokedTreeItem)>),
114}
115
116pub async fn handle(
117    Extension(repo): Extension<Repository>,
118    Extension(RepositoryPath(repository_path)): Extension<RepositoryPath>,
119    Extension(ChildPath(child_path)): Extension<ChildPath>,
120    Extension(git): Extension<Arc<Git>>,
121    Extension(db_orig): Extension<Arc<rocksdb::DB>>,
122    Query(query): Query<UriQuery>,
123) -> Result<impl IntoResponse> {
124    let db = db_orig.clone();
125    let (query, repo, tree_id) = tokio::task::spawn_blocking(move || {
126        let tree_id = if let Some(id) = query.id.as_deref() {
127            let hex = const_hex::decode_to_array(id).context("Failed to parse tree hash")?;
128            Tree::find(&db, ObjectId::Sha1(hex))
129                .context("Failed to lookup tree")?
130                .context("Couldn't find tree with given id")?
131        } else {
132            let repository = crate::database::schema::repository::Repository::open(&db, &*repo)?
133                .context("Repository does not exist")?;
134            let commit = get_branch_commits(&repository, &db, query.branch.as_deref(), 1, 0)?
135                .into_iter()
136                .next()
137                .context("Branch not found")?;
138            commit.get().tree.to_native()
139        };
140
141        Ok::<_, anyhow::Error>((query, repo, tree_id))
142    })
143    .await
144    .context("failed to join tree_id task")??;
145
146    let db = db_orig.clone();
147    let (repo, child_path, lookup_result) = tokio::task::spawn_blocking(move || {
148        if let Some(path) = &child_path {
149            if let Some(item) =
150                TreeItem::find_exact(&db, tree_id, path.as_os_str().as_encoded_bytes())?
151            {
152                if let ArchivedTreeItemKind::File = item.get().kind {
153                    return Ok((repo, child_path, LookupResult::RealPath));
154                }
155            }
156        }
157
158        let path = child_path
159            .as_ref()
160            .map(|v| v.as_os_str().as_encoded_bytes())
161            .unwrap_or_default();
162
163        let tree_items = TreeItem::find_prefix(&db, tree_id, Some(path))
164            // don't take the current path the user is on
165            .filter_ok(|(k, _)| !k.get()[path.len()..].is_empty())
166            // only take direct descendents
167            .filter_ok(|(k, _)| {
168                memchr::memrchr(b'/', &k.get()[path.len()..]).is_none_or(|v| v == 0)
169            })
170            .map_ok(|(k, v)| {
171                (
172                    k.try_map_project(|v, _| simdutf8::basic::from_utf8(v))
173                        .expect("invalid utf8"),
174                    path.len(),
175                    v,
176                )
177            })
178            .try_collect::<_, Vec<_>, _>()?;
179
180        if tree_items.is_empty() {
181            bail!("Path doesn't exist in tree");
182        }
183
184        Ok::<_, anyhow::Error>((repo, child_path, LookupResult::Children(tree_items)))
185    })
186    .await
187    .context("failed to join on tokio task")??;
188
189    Ok(match lookup_result {
190        LookupResult::RealPath => {
191            let open_repo = git.repo(repository_path, query.branch.clone()).await?;
192            let file = open_repo
193                .path(child_path.clone(), query.id.as_deref(), !query.raw)
194                .await?;
195
196            if query.raw {
197                ResponseEither::Right(file.content)
198            } else {
199                let db = db_orig.clone();
200                let full_tree = tokio::task::spawn_blocking(move || SortedTree::get(tree_id, &db))
201                    .await
202                    .context("failed to join on tokio task")??
203                    .context("missing file tree")?;
204
205                ResponseEither::Left(ResponseEither::Right(into_response(FileView {
206                    repo,
207                    file,
208                    branch: query.branch.clone(),
209                    repo_path: child_path.unwrap_or_default(),
210                    full_tree,
211                    query,
212                })))
213            }
214        }
215        LookupResult::Children(items) => {
216            let db = db_orig.clone();
217            let full_tree = tokio::task::spawn_blocking(move || SortedTree::get(tree_id, &db))
218                .await
219                .context("failed to join on tokio task")??
220                .context("missing file tree")?;
221
222            ResponseEither::Left(ResponseEither::Left(into_response(TreeView {
223                repo,
224                items,
225                branch: query.branch.clone(),
226                query,
227                repo_path: child_path.unwrap_or_default(),
228                full_tree,
229            })))
230        }
231    })
232}