A minimal self-hosted git browser on Rust axum: bare repos rendered as a website with commits, diffs, syntax-highlighted blobs, atom feeds, and clone over HTTPS.
axumdockergitgit-browsergitoxidegixrustself-hosted
1use axum::{
2 extract::{Path, State},
3 response::{IntoResponse, Response},
4 routing::get,
5 Router,
6};
7
8use crate::render::{not_found, render};
9use crate::AppState;
10
11pub fn router() -> Router<AppState> {
12 Router::new().route("/{name}", get(repo_page))
13}
14
15async fn repo_page(Path(name): Path<String>, State(state): State<AppState>) -> Response {
16 let repo_root = state.config.repo_root.clone();
17 let clone_base = state.config.clone_base.clone();
18 let name_for_blocking = name.clone();
19 let result = tokio::task::spawn_blocking(move || -> anyhow::Result<_> {
20 let repo = crate::git::open(&repo_root, &name_for_blocking)?;
21 let summary = crate::git::repo_summary(
22 &crate::git::resolve_path(&repo_root, &name_for_blocking)?,
23 &clone_base,
24 )?;
25 let head = repo.head_commit().ok();
26 let commits = if let Some(h) = head.as_ref() {
27 crate::git::recent_commits(&repo, h.id, 10).unwrap_or_default()
28 } else {
29 Vec::new()
30 };
31 let readme_html = head.as_ref().and_then(|h| {
32 crate::git::read_readme(&repo, h.id).map(|(_, bytes)| {
33 let text = String::from_utf8_lossy(&bytes).into_owned();
34 let link_base =
35 format!("/{}/blob/{}", name_for_blocking, summary.default_branch);
36 let image_base =
37 format!("/{}/raw/{}", name_for_blocking, summary.default_branch);
38 crate::markdown::render(&text, &link_base, &image_base)
39 })
40 });
41 Ok((summary, commits, readme_html))
42 })
43 .await;
44
45 let (summary, commits, readme_html) = match result {
46 Ok(Ok(v)) => v,
47 Ok(Err(e)) => return not_found(&state, &name, e),
48 Err(e) => {
49 tracing::error!("repo_page join: {e}");
50 return (axum::http::StatusCode::INTERNAL_SERVER_ERROR, "render error")
51 .into_response();
52 }
53 };
54
55 render(
56 &state,
57 "repo.html",
58 &format!("/{name}"),
59 minijinja::context! {
60 repo => &summary,
61 commits => &commits,
62 readme_html => readme_html.map(minijinja::Value::from_safe_string),
63 },
64 )
65}