raw
1use std::fmt::Write as _;
2
3use axum::{
4 extract::{Path, 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 _,
19 git::Repository,
20 http::{
21 extractor::{Commit, RepoName},
22 response::{ErrorPage, Html, Result},
23 },
24 utils::filters,
25};
26
27#[derive(askama::Template)]
28#[template(path = "commit.html")]
29struct RepoCommitTemplate<'a> {
30 config: &'a Config,
31 syntaxes: &'a SyntaxSet,
32 repo: &'a Repository,
33 commit: git2::Commit<'a>,
34 diff: &'a git2::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 = self
65 .syntaxes
66 .find_syntax_by_name("Diff")
67 .expect("diff syntax missing");
68
69 let mut highlighter =
70 ClassedHTMLGenerator::new_with_class_style(syntax, self.syntaxes, ClassStyle::Spaced);
71
72 LinesWithEndings::from(&buf).for_each(|line| {
73 if let Err(err) = highlighter.parse_html_for_line_which_includes_newline(line) {
74 tracing::error!(err=?err, "failed to highlight code");
75 }
76 });
77
78 highlighter.finalize()
79 }
80
81 fn refs(&self) -> String {
82 let mut html = String::new();
83
84 let descr = self.commit.as_object().describe(
86 DescribeOptions::new()
87 .describe_tags()
88 .max_candidates_tags(0),
89 );
90 if let Ok(descr) = descr {
91 let _ = write!(
93 &mut html,
94 r#"<a href="/{0}/refs/{1}" class="badge tag">{1}</a>"#,
95 self.repo.name().unwrap_or("<unknown>"),
96 descr
97 .format(Some(DescribeFormatOptions::new().abbreviated_size(0)))
98 .unwrap_or_else(|_| "<unknown>".to_string()),
99 );
100 }
101
102 let branches = match self.repo.branches_of_type(BranchType::Local) {
104 Ok(branches) => branches,
105 Err(err) => {
106 tracing::error!(err=?err, "failed to create branches iterator");
107 return html;
108 }
109 };
110 let branches = branches.into_iter().filter(|branch| {
111 branch.get().peel_to_commit().map(|commit| commit.id()) == Ok(self.commit.id())
112 });
113 for branch in branches {
114 let _ = write!(
116 &mut html,
117 r#" <a href="/{0}/log/{1}" class="badge branch">{1}</a>"#,
118 self.repo.name().unwrap_or("<unknown>"),
119 branch
120 .name()
121 .unwrap_or(Some("<unknown>"))
122 .unwrap_or("<unknown>"),
123 );
124 }
125
126 html
127 }
128}
129
130#[tracing::instrument(skip_all)]
131pub(crate) async fn get(
132 state: State<BileState>,
133 Path((repo_name, commit)): Path<(RepoName, Commit)>,
134) -> Response {
135 state
136 .spawn(move |state| inner(&state, &repo_name, &commit))
137 .await
138}
139
140#[tracing::instrument(skip_all)]
141fn inner(state: &BileState, repo_name: &RepoName, commit: &Commit) -> Result<Response> {
142 let Some(repo) = Repository::open(&state.config, repo_name).context("opening repository")?
143 else {
144 return Ok(ErrorPage::new(&state.config)
145 .with_status(StatusCode::NOT_FOUND)
146 .into_response());
147 };
148
149 let Some(commit) = repo.commit(&commit.0).context("failed to get commit")? else {
150 return Ok(ErrorPage::new(&state.config)
151 .with_status(StatusCode::NOT_FOUND)
152 .into_response());
153 };
154
155 let mut diff = repo
156 .commit_diff(&commit)
157 .context("failed to get commits diff")?;
158
159 let mut find_options = DiffFindOptions::new();
160 find_options.all(true);
162 if let Err(err) = diff.find_similar(Some(&mut find_options)) {
163 tracing::error!(err=?err, "failed to mark similar files in diff");
164 }
165
166 Ok(Html(RepoCommitTemplate {
167 config: &state.config,
168 syntaxes: &state.syntax,
169 repo: &repo,
170 commit,
171 diff: &diff,
172 })
173 .into_response())
174}
175