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}