src/server/http/repo/mod.rs
Ref: Size: 5.1 KiB
mod overview;
mod commits;
mod tree;
mod diff;
mod patches;
mod issues;
mod readme;
pub use overview::overview;
pub use commits::{commits, commits_ref};
pub use tree::{tree_root, tree, blob};
pub use diff::diff;
pub use patches::{patches, patch_detail};
pub use issues::{issues, issue_detail};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use chrono::{TimeZone, Utc};
use super::AppState;
#[derive(askama::Template, askama_web::WebTemplate)]
#[template(path = "error.html")]
pub struct ErrorTemplate {
pub site_title: String,
pub status_code: u16,
pub message: String,
}
fn not_found(state: &AppState, message: impl Into<String>) -> Response {
let mut response = ErrorTemplate {
site_title: state.site_title.clone(),
status_code: 404,
message: message.into(),
}
.into_response();
*response.status_mut() = StatusCode::NOT_FOUND;
response
}
fn internal_error(state: &AppState, message: impl Into<String>) -> Response {
let mut response = ErrorTemplate {
site_title: state.site_title.clone(),
status_code: 500,
message: message.into(),
}
.into_response();
*response.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
response
}
/// Count open patches and issues for sidebar badges.
/// Note: this does a full state read on each call. For repos with many collab
/// objects this could be slow — consider caching with a short TTL if needed.
fn collab_counts(repo: &git2::Repository) -> (usize, usize) {
let open_patches = git_collab::state::list_patches(repo)
.unwrap_or_default()
.into_iter()
.filter(|p| p.status == git_collab::state::PatchStatus::Open)
.count();
let open_issues = git_collab::state::list_issues(repo)
.unwrap_or_default()
.into_iter()
.filter(|i| i.status == git_collab::state::IssueStatus::Open)
.count();
(open_patches, open_issues)
}
#[derive(Debug)]
pub struct OverviewCommit {
pub id: String,
pub short_id: String,
pub summary: String,
pub author: String,
pub date: String,
}
fn recent_commits(repo: &git2::Repository, limit: usize) -> Vec<OverviewCommit> {
let mut revwalk = match repo.revwalk() {
Ok(rw) => rw,
Err(_) => return Vec::new(),
};
// Try HEAD first; if unborn, fall back to the first local branch
if revwalk.push_head().is_err() {
let branch = list_branches(repo).into_iter().next();
let pushed = branch.and_then(|name| {
let obj = repo.revparse_single(&name).ok()?;
revwalk.push(obj.id()).ok()
});
if pushed.is_none() {
return Vec::new();
}
}
revwalk
.take(limit)
.filter_map(|oid| {
let oid = oid.ok()?;
let commit = repo.find_commit(oid).ok()?;
let id = oid.to_string();
let short_id = id[..8.min(id.len())].to_string();
let summary = commit.summary().unwrap_or("").to_string();
let author = commit.author().name().unwrap_or("").to_string();
let secs = commit.time().seconds();
let date = Utc
.timestamp_opt(secs, 0)
.single()
.map(|dt| dt.format("%Y-%m-%d").to_string())
.unwrap_or_default();
Some(OverviewCommit { id, short_id, summary, author, date })
})
.collect()
}
#[derive(Debug)]
pub struct CommentView {
pub author: String,
pub body: String,
pub timestamp: String,
}
/// List local branch names, sorted alphabetically.
fn list_branches(repo: &git2::Repository) -> Vec<String> {
let mut names: Vec<String> = repo
.branches(Some(git2::BranchType::Local))
.into_iter()
.flatten()
.filter_map(|b| {
let (branch, _) = b.ok()?;
branch.name().ok()?.map(|n| n.to_string())
})
.collect();
names.sort();
names
}
/// Resolve HEAD to its short branch name (e.g. "main").
/// Falls back to the first existing branch if HEAD points to an unborn branch,
/// which happens when a bare repo is init'd with master but only main is pushed.
fn head_branch_name(repo: &git2::Repository) -> String {
if let Ok(head) = repo.head() {
if let Some(name) = head.shorthand() {
return name.to_string();
}
}
// HEAD is unborn or detached — pick the first local branch
list_branches(repo).into_iter().next().unwrap_or_else(|| "HEAD".to_string())
}
/// Open a repo by name, returning the entry and git2::Repository or an error Response.
#[allow(clippy::result_large_err)]
fn open_repo(state: &AppState, repo_name: &str) -> Result<(crate::repos::RepoEntry, git2::Repository), Response> {
let entry = match crate::repos::resolve(&state.repos_dir, repo_name) {
Some(e) => e,
None => return Err(not_found(state, format!("Repository '{}' not found.", repo_name))),
};
let repo = match crate::repos::open(&entry) {
Ok(r) => r,
Err(_) => return Err(internal_error(state, "Failed to open repository.")),
};
Ok((entry, repo))
}