wayver's git archive


a simple self-hosted git server
git clone https://git.wayver.dev/bile

src/handlers/repo_commit.rs@6e060abc1f25c2e1b79fe06bfa8b72cd26952ee1

raw
Date Commit Message Author Files + -
2026-02-19 17:51 large refactoring wayverd 53 2153 1683
...

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        // highlight the diff
64        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        // add badge if this commit is a tag
85        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            // this can be a tag or lightweight tag, the refs path will redirect
92            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        // also add badge if this is the tip of a branch
103        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            // branch is not a reference, just a fancy name for a commit
115            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    // try to find moved/renamed files
161    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