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