raw
1use std::{fmt::Write as _, path};
2
3use axum::{
4 extract::Path,
5 http::StatusCode,
6 response::{IntoResponse as _, Response},
7};
8use git2::{Blob, Commit, Tree};
9use syntect::{
10 html::{ClassStyle, ClassedHTMLGenerator},
11 util::LinesWithEndings,
12};
13
14use crate::{
15 SYNTAXES,
16 utils::{
17 Error, Result, blob_mime,
18 error::Context as _,
19 extractor::repo_name_checks,
20 filters,
21 git::Repository,
22 response::{Html, Redirect},
23 spawn_blocking,
24 },
25};
26
27#[derive(askama::Template)]
28#[template(path = "tree.html")]
29struct RepoTreeTemplate<'a> {
30 repo: &'a Repository,
31 tree: Tree<'a>,
32 path: &'a path::Path,
33 spec: &'a str,
34 last_commit: Commit<'a>,
35}
36
37#[derive(askama::Template)]
38#[template(path = "file.html")]
39struct RepoFileTemplate<'a> {
40 repo: &'a Repository,
41 path: &'a path::Path,
42 file_text: &'a str,
43 spec: &'a str,
44 last_commit: Commit<'a>,
45}
46
47#[tracing::instrument(skip_all)]
48pub async fn get_1(Path(repo_name): Path<String>) -> Response {
49 spawn_blocking(move || inner(&repo_name, None, None).into_response()).await
50}
51
52#[tracing::instrument(skip_all)]
53pub async fn get_2(Path((repo_name, r#ref)): Path<(String, String)>) -> Response {
54 spawn_blocking(move || inner(&repo_name, Some(&r#ref), None).into_response()).await
55}
56
57#[tracing::instrument(skip_all)]
58pub async fn get_3(
59 Path((repo_name, r#ref, object_name)): Path<(String, String, String)>,
60) -> Response {
61 spawn_blocking(move || inner(&repo_name, Some(&r#ref), Some(&object_name)).into_response())
62 .await
63}
64
65fn inner(repo_name: &str, r#ref: Option<&str>, object_name: Option<&str>) -> Result {
66 repo_name_checks(repo_name)?;
67
68 let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
69 return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
70 };
71
72 if repo.is_empty()? {
73 return Ok(Redirect::permanent(&format!("/{repo_name}"))
74 .unwrap_or(Redirect::PERMANENT_ROOT)
75 .into_response());
76 }
77
78 let spec = repo.ref_or_head_shorthand(r#ref)?;
79 let Some((commit, tree)) = repo
80 .commit_tree(&spec)
81 .context("failed to get commit tree")?
82 else {
83 return Err(Error::new(
84 StatusCode::NOT_FOUND,
85 "commit does not exist in repo",
86 ));
87 };
88
89 let (path, tree_obj) = if let Some(path) = object_name {
90 let path = path::Path::new(path);
91
92 (path, repo.tree_object(&tree, path)?)
93 } else {
94 (path::Path::new(""), Some(tree.into_object()))
95 };
96
97 let Some(tree_obj) = tree_obj else {
98 return Err(Error::new(
99 StatusCode::NOT_FOUND,
100 "file does not exist into repo",
101 ));
102 };
103
104 let Some(last_commit) = repo.file_last_commit(&spec, path)? else {
105 return Err(Error::new(
106 StatusCode::NOT_FOUND,
107 "commit does not exist in repo",
108 ));
109 };
110
111 let tree_obj = match tree_obj.into_tree() {
112 Ok(sub_tree) => {
114 return Ok(Html(RepoTreeTemplate {
115 repo: &repo,
116 tree: sub_tree,
117 path,
118 spec: &spec,
119 last_commit,
120 })
121 .into_response());
122 }
123 Err(tree_obj) => tree_obj,
125 };
126
127 let Some(blob) = tree_obj.as_blob() else {
128 return Err(Error::new(StatusCode::NOT_FOUND, "File not found"));
129 };
130
131 let output = render(repo_name, path, &spec, &commit, blob)?;
132
133 Ok(Html(RepoFileTemplate {
134 repo: &repo,
135 path,
136 file_text: &output,
137 spec: &spec,
138 last_commit,
139 })
140 .into_response())
141}
142
143fn render(
147 repo_name: &str,
148 path: &path::Path,
149 spec: &str,
150 commit: &Commit<'_>,
151 blob: &Blob<'_>,
152) -> crate::utils::error::Result<String> {
153 let extension = path
154 .extension()
155 .and_then(std::ffi::OsStr::to_str)
156 .unwrap_or_default();
157
158 if blob.is_binary() {
159 let mime = blob_mime(blob, extension);
162
163 let output = match mime.type_() {
164 mime::TEXT => unreachable!("git detected this file as binary"),
165 mime::IMAGE => format!(
166 "<img src=\"/{}/tree/{}/raw/{}\" />",
167 repo_name,
168 spec,
169 path.display()
170 ),
171 tag @ (mime::AUDIO | mime::VIDEO) => format!(
172 "<{} src=\"/{}/tree/{}/raw/{}\" controls>Your browser does not have support for playing this {0} file.</{0}>",
173 tag,
174 repo_name,
175 spec,
176 path.display()
177 ),
178 _ => "Cannot display binary file.".to_string(),
179 };
180
181 return Ok(output);
182 }
183
184 let syntax = SYNTAXES
185 .find_syntax_by_extension(extension)
186 .unwrap_or_else(|| SYNTAXES.find_syntax_plain_text());
187
188 let file_string = str::from_utf8(blob.content())?;
190
191 let mut highlighter =
193 ClassedHTMLGenerator::new_with_class_style(syntax, &SYNTAXES, ClassStyle::Spaced);
194 LinesWithEndings::from(file_string).for_each(|line| {
195 if let Err(err) = highlighter.parse_html_for_line_which_includes_newline(line) {
196 tracing::error!(err=?err, "failed to highlight code");
197 }
198 });
199
200 let prefix = format!(
202 "/{}/tree/{}/item/{}",
203 repo_name,
204 commit.id(),
205 path.display()
206 );
207
208 let mut output = String::from("<pre>\n");
209 for (n, line) in highlighter.finalize().lines().enumerate() {
210 let _ = writeln!(
211 &mut output,
212 "<a href='{1}#L{0}' id='L{0}' class='line'>{0}</a>{2}",
213 n + 1,
214 prefix,
215 line,
216 );
217 }
218 output.push_str("</pre>\n");
219
220 Ok(output)
221}
222