raw
1use std::fmt::Write as _;
2
3use axum::{
4 extract::Path,
5 http::StatusCode,
6 response::{IntoResponse as _, Response},
7};
8use git2::{
9 BranchType, Commit, DescribeFormatOptions, DescribeOptions, Diff, DiffFindOptions, DiffFormat,
10};
11use syntect::{
12 html::{ClassStyle, ClassedHTMLGenerator},
13 util::LinesWithEndings,
14};
15
16use crate::{
17 SYNTAXES,
18 utils::{
19 Error, Result,
20 error::Context as _,
21 extractor::{commit_checks, repo_name_checks},
22 filters,
23 git::Repository,
24 response::Html,
25 spawn_blocking,
26 },
27};
28
29#[derive(askama::Template)]
30#[template(path = "commit.html")]
31struct RepoCommitTemplate<'a> {
32 repo: &'a Repository,
33 commit: Commit<'a>,
34 diff: &'a Diff<'a>,
35}
36
37impl RepoCommitTemplate<'_> {
38 fn parent_ids(&self) -> Vec<git2::Oid> {
39 self.commit.parent_ids().collect()
40 }
41
42 fn diff(&self) -> String {
43 let mut buf = String::new();
44
45 let _ = self.diff.print(DiffFormat::Patch, |_, _, line| {
46 let Ok(content) = str::from_utf8(line.content()) else {
47 buf.push_str("Cannot display diff for binary file.");
48
49 return false;
50 };
51
52 match line.origin() {
53 'F' | 'H' => {}
54 c if matches!(c, ' ' | '+' | '-' | '=' | '<' | '>') => buf.push(c),
55 _ => unreachable!(),
56 }
57
58 buf.push_str(content);
59
60 true
61 });
62
63 let syntax = SYNTAXES
65 .find_syntax_by_name("Diff")
66 .expect("diff syntax missing");
67
68 let mut highlighter =
69 ClassedHTMLGenerator::new_with_class_style(syntax, &SYNTAXES, ClassStyle::Spaced);
70
71 LinesWithEndings::from(&buf).for_each(|line| {
72 if let Err(err) = highlighter.parse_html_for_line_which_includes_newline(line) {
73 tracing::error!(err=?err, "failed to highlight code");
74 }
75 });
76
77 highlighter.finalize()
78 }
79
80 fn refs(&self) -> String {
81 let mut html = String::new();
82
83 let descr = self.commit.as_object().describe(
85 DescribeOptions::new()
86 .describe_tags()
87 .max_candidates_tags(0),
88 );
89 if let Ok(descr) = descr {
90 let _ = write!(
92 &mut html,
93 r#"<a href="/{0}/refs/{1}" class="badge tag">{1}</a>"#,
94 self.repo.name().unwrap_or("<unknown>"),
95 descr
96 .format(Some(DescribeFormatOptions::new().abbreviated_size(0)))
97 .unwrap_or_else(|_| "<unknown>".to_string()),
98 );
99 }
100
101 let branches = match self.repo.branches_of_type(BranchType::Local) {
103 Ok(branches) => branches,
104 Err(err) => {
105 tracing::error!(err=?err, "failed to create branches iterator");
106 return html;
107 }
108 };
109 let branches = branches.into_iter().filter(|branch| {
110 branch.get().peel_to_commit().map(|commit| commit.id()) == Ok(self.commit.id())
111 });
112 for branch in branches {
113 let _ = write!(
115 &mut html,
116 r#" <a href="/{0}/log/{1}" class="badge branch">{1}</a>"#,
117 self.repo.name().unwrap_or("<unknown>"),
118 branch
119 .name()
120 .unwrap_or(Some("<unknown>"))
121 .unwrap_or("<unknown>"),
122 );
123 }
124
125 html
126 }
127}
128
129#[tracing::instrument(skip_all)]
130pub async fn get(Path((repo_name, commit)): Path<(String, String)>) -> Response {
131 spawn_blocking(move || inner(&repo_name, &commit).into_response()).await
132}
133
134#[tracing::instrument(skip_all)]
135fn inner(repo_name: &str, commit: &str) -> Result {
136 repo_name_checks(repo_name)?;
137 commit_checks(commit)?;
138
139 let Some(repo) = Repository::open(repo_name).context("opening repository")? else {
140 return Err(Error::new(StatusCode::NOT_FOUND, "repo does not exist"));
141 };
142
143 let Some(commit) = repo.commit(commit).context("failed to get commit")? else {
144 return Err(Error::new(
145 StatusCode::NOT_FOUND,
146 "commit does not exist in repo",
147 ));
148 };
149
150 let mut diff = repo
151 .commit_diff(&commit)
152 .context("failed to get commits diff")?;
153
154 let mut find_options = DiffFindOptions::new();
155 find_options.all(true);
157 if let Err(err) = diff.find_similar(Some(&mut find_options)) {
158 tracing::error!(err=?err, "failed to mark similar files in diff");
159 }
160
161 Ok(Html(RepoCommitTemplate {
162 repo: &repo,
163 commit,
164 diff: &diff,
165 })
166 .into_response())
167}
168