a73x

src/server/http/repo/diff.rs

Ref:   Size: 3.3 KiB

use std::sync::Arc;

use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response};
use chrono::{TimeZone, Utc};

use super::{AppState, collab_counts, not_found, open_repo};

#[derive(Debug)]
pub struct DiffLine {
    pub kind: String,
    pub text: String,
}

#[derive(askama::Template, askama_web::WebTemplate)]
#[template(path = "diff.html")]
pub struct DiffTemplate {
    pub site_title: String,
    pub repo_name: String,
    pub active_section: String,
    pub open_patches: usize,
    pub open_issues: usize,
    pub short_id: String,
    pub summary: String,
    pub body: String,
    pub author: String,
    pub date: String,
    pub diff_lines: Vec<DiffLine>,
}

fn compute_diff_lines(repo: &git2::Repository, oid: git2::Oid) -> Vec<DiffLine> {
    let commit = match repo.find_commit(oid) {
        Ok(c) => c,
        Err(_) => return Vec::new(),
    };

    let new_tree = match commit.tree() {
        Ok(t) => t,
        Err(_) => return Vec::new(),
    };

    let old_tree = commit
        .parent(0)
        .ok()
        .and_then(|p| p.tree().ok());

    let diff = match repo.diff_tree_to_tree(old_tree.as_ref(), Some(&new_tree), None) {
        Ok(d) => d,
        Err(_) => return Vec::new(),
    };

    let mut lines: Vec<DiffLine> = Vec::new();

    let _ = diff.print(git2::DiffFormat::Patch, |_delta, _hunk, line| {
        let text = String::from_utf8_lossy(line.content()).into_owned();
        let kind = match line.origin() {
            '+' => "add",
            '-' => "del",
            '@' => "hunk",
            'F' => "file",
            _ => "ctx",
        }
        .to_string();
        lines.push(DiffLine { kind, text });
        true
    });

    lines
}

pub async fn diff(
    Path((repo_name, oid_str)): Path<(String, String)>,
    State(state): State<Arc<AppState>>,
) -> Response {
    let (_entry, repo) = match open_repo(&state, &repo_name) {
        Ok(r) => r,
        Err(resp) => return resp,
    };

    let (open_patches, open_issues) = collab_counts(&repo);

    let oid = match git2::Oid::from_str(&oid_str) {
        Ok(o) => o,
        Err(_) => return not_found(&state, format!("Invalid OID: {}", oid_str)),
    };

    let commit = match repo.find_commit(oid) {
        Ok(c) => c,
        Err(_) => return not_found(&state, format!("Commit '{}' not found.", oid_str)),
    };

    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 %H:%M").to_string())
        .unwrap_or_default();

    let body = commit
        .message()
        .map(|m| {
            let lines: Vec<&str> = m.splitn(2, '\n').collect();
            if lines.len() > 1 { lines[1].trim_start_matches('\n').to_string() } else { String::new() }
        })
        .unwrap_or_default();

    let diff_lines = compute_diff_lines(&repo, oid);

    DiffTemplate {
        site_title: state.site_title.clone(),
        repo_name,
        active_section: "commits".to_string(),
        open_patches,
        open_issues,
        short_id,
        summary,
        body,
        author,
        date,
        diff_lines,
    }
    .into_response()
}