22054c427463519346462843ea625a242a2e8124 / frogue / src / views / commit.rs
Raw
//! The commit diff viewer: shows the changes in a single commit.
//!
//! This module provides both a full commit diff page and reusable maud
//! components for displaying diffs, which can be shared with other views
//! like commit comparison pages.
use actix_web::HttpResponse;
use maud::{Markup, Render, html};
use crate::repositories::git;
use crate::views::{
Page,
components::{RepositoryChrome, RepositoryTab},
repositories::CommitSummary,
};
pub const MAX_DIFF_LINES_TO_AUTOPEN: usize = 100;
// ============================================================================
// Reusable maud components for displaying diffs
// ============================================================================
/// Renders a summary of diff statistics: file count, insertions, and deletions.
///
/// This component can be reused in commit lists, comparison pages, etc.
pub fn diff_stats_summary(files: &[git::DiffFile]) -> Markup {
let total_files = files.len();
let total_insertions: u64 = files.iter().map(|f| f.added).sum();
let total_deletions: u64 = files.iter().map(|f| f.deleted).sum();
html! {
span class="stats" {
(total_files)
@if total_files == 1 {
" file changed"
} @else {
" files changed"
}
", "
span class="insertions" { "+" (total_insertions) }
", "
span class="deletions" { "−" (total_deletions) }
}
}
}
/// Renders a single file's diff: header with path and stats, plus hunks.
///
/// This component can be reused anywhere a file diff needs to be displayed.
pub fn file_diff(file: &git::DiffFile) -> Markup {
let line_count = file
.hunks
.iter()
.map(|f| f.lines.len())
.fold(0, |acc, i| acc + i);
html! {
details class="file-diff" open[line_count<MAX_DIFF_LINES_TO_AUTOPEN] {
summary {
span class="file-path" {
@if file.old_path != file.new_path {
(file.old_path) " → " (file.new_path)
} @else {
(file.new_path)
}
}
span class="file-stats" {
span class="insertions" { "+" (file.added) }
span class="deletions" { "−" (file.deleted) }
}
}
@if file.binary {
p class="binary-notice" { "Binary file" }
} @else {
div class="hunks" {
@for hunk in &file.hunks {
div class="hunk" {
div class="hunk-header" { (hunk.header) }
@for line in &hunk.lines {
@match line.kind {
git::DiffLineKind::Context => {
div class="diff-line context" { " " (line.text) }
}
git::DiffLineKind::Added => {
div class="diff-line added" { "+" (line.text) }
}
git::DiffLineKind::Removed => {
div class="diff-line removed" { "−" (line.text) }
}
}
}
}
}
}
}
}
}
}
/// Renders a list of file diffs.
///
/// Convenience wrapper around multiple file_diff calls.
pub fn file_diffs(files: &[git::DiffFile]) -> Markup {
html! {
@for file in files {
(file_diff(file))
}
}
}
/// Renders a commit header: subject, hash, date, and author.
///
/// This component can be reused in commit lists, diff pages, etc.
pub fn commit_header(commit: &git::Commit) -> Markup {
html! {
div class="commit-header" {
h3 { (commit.subject) }
p class="commit-meta" {
code class="hash" { (commit.hash) }
code class="date" { (commit.date) }
span class="author" { (commit.author) }
}
}
}
}
/// Renders inline stats for a single commit: insertions and deletions.
///
/// This component is designed for use in commit lists where space is limited.
pub fn commit_stats_inline(commit: &CommitSummary) -> Markup {
html! {
@if commit.insertions > 0 || commit.deletions > 0 {
span class="commit-stats" {
span class="insertions" { "+" (commit.insertions) }
" "
span class="deletions" { "−" (commit.deletions) }
}
}
}
}
// ============================================================================
// Full commit diff page
// ============================================================================
pub(super) struct CommitBody<'a> {
owner: &'a str,
slug: &'a str,
visibility: &'a str,
default_branch: &'a str,
commit: &'a git::Commit,
files: &'a [git::DiffFile],
viewer: Option<&'a str>,
}
impl Render for CommitBody<'_> {
fn render(&self) -> Markup {
html! {
(RepositoryChrome {
owner: self.owner,
slug: self.slug,
visibility: self.visibility,
default_branch: self.default_branch,
current_tab: RepositoryTab::Commits,
is_owner: self.viewer == Some(self.owner),
})
div class="commit-detail" {
(commit_header(self.commit))
p class="commit-links" {
a href=(format!(
"/~{}/{}/tree/{}",
self.owner, self.slug, self.commit.full_hash
)) {
"Browse the tree at this commit"
}
}
@if !self.files.is_empty() {
div class="commit-stats" {
(diff_stats_summary(self.files))
}
div class="commit-diff" {
(file_diffs(self.files))
}
} @else {
p class="empty" { "No changes in this commit." }
}
}
}
}
}
/// The page showing the diff of a single commit.
pub fn commit(
viewer: Option<&str>,
owner: &str,
slug: &str,
visibility: &'static str,
default_branch: &str,
commit: &git::Commit,
files: &[git::DiffFile],
) -> HttpResponse {
Page {
title: format!("~{owner}/{slug}: {} - Frogue", commit.subject),
viewer: viewer.map(str::to_string),
extra_head: None,
body: CommitBody {
owner,
slug,
visibility,
default_branch,
commit,
files,
viewer,
},
}
.into_response()
}