wayver's git archive


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

src/handlers/repo_commit.rs@34150f75160e9b2d55638b0e49fd5cab218f71d3

raw
Date Commit Message Author Files + -
2026-02-20 20:25 remove unneeded result type alias wayverd 13 27 31
...

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        // highlight the diff
65        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        // add badge if this commit is a tag
86        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            // this can be a tag or lightweight tag, the refs path will redirect
93            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        // also add badge if this is the tip of a branch
104        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            // branch is not a reference, just a fancy name for a commit
116            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    // try to find moved/renamed files
162    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