wayver's git archive


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

src/handlers/repo_commit.rs@f3f2b40f0ffae5de2e6d3f661e32b582274bae49

raw
Date Commit Message Author Files + -
2026-02-17 21:07 initial mvp wayverd 74 10800 0
...

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