Skip to content
Clone
b7cd2b1
187 lines 8.2 KB Rust
use std::path::PathBuf;
use export::http::{Path, Query, Response, State};
use maud::html;
use crate::handler::html::{
HtmlRepoPath, archived_class, clamp_limit, error, first_line, iso_time_unix,
relative_time_unix, resolve_view, response_html, short_sha,
};
use crate::service::GitService;
use crate::service::commits::ListCommitsCmd;
use crate::share::access::BasicAuth;
#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
pub struct CommitsQuery {
#[serde(default)]
pub revision: Option<String>,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub limit: Option<usize>,
#[serde(default)]
pub skip: Option<usize>,
}
#[utoipa::path(
get,
path = "/repos/{space_name}/{repo_name}/html/commits",
tag = "html",
description = "渲染带分页操作的提交历史列表片段。",
operation_id = "render_commits",
params(HtmlRepoPath, CommitsQuery),
responses(
(status = 200, description = "commits list html fragment", content_type = "text/html"),
(status = 404, description = "repo or ref not found")
)
)]
pub async fn render_commits(
State(service): State<GitService>,
Path(path): Path<HtmlRepoPath>,
Query(q): Query<CommitsQuery>,
auth: BasicAuth,
) -> Response {
let view = match resolve_view(service.clone(), path, auth, q.revision, q.path, false).await {
Ok(v) => v,
Err(resp) => return resp,
};
let ctx = view.ctx;
let revision = view.revision;
let clean_path = view.clean_path;
let limit = clamp_limit(q.limit).min(100);
let cmd = ListCommitsCmd {
revision: revision.clone(),
path: if clean_path.is_empty() {
None
} else {
Some(PathBuf::from(clean_path.clone()))
},
max_count: Some(limit as u32),
skip: q.skip.map(|s| s as u32),
since: None,
until: None,
first_parent: false,
};
let page = match service
.list_commits(ctx.space_name.clone(), ctx.repo_name.clone(), cmd, ctx.uid)
.await
{
Ok(p) => p,
Err(e) => {
tracing::warn!(error = %e, "commits list internal");
return error::html_err(500, "internal");
}
};
let commits = page.commits;
let base =
"git-commits w-full rounded-lg border border-zinc-200 bg-white overflow-hidden text-sm";
let cls = archived_class(&ctx.dto, base);
let space = ctx.space_name.clone();
let repo = ctx.repo_name.clone();
let skip = page.skip as usize;
let older_skip = skip + limit;
let has_older = page.has_more;
let markup = html! {
div class=(cls)
data-component="git-commits"
data-space=(space)
data-repo=(repo)
data-revision=(revision)
data-path=(clean_path.as_str())
data-count=(commits.len())
data-limit=(page.limit)
data-skip=(page.skip)
data-total=(page.total)
data-has-more=(if page.has_more { "true" } else { "false" })
data-diff-style=(page.diff_view_style)
data-archived=(if ctx.dto.is_archived { "true" } else { "false" }) {
div class="flex items-center justify-between border-b border-zinc-200 bg-zinc-50 px-4 py-2 text-xs text-zinc-500 font-medium" {
span { "Commit history" }
span class="font-mono text-[11px]" {
@if page.total == 0 {
"0 commits"
} @else {
(skip + 1) "–" (skip + commits.len()) " of " (page.total) " commits"
}
}
}
@if commits.is_empty() {
div class="p-8 text-center text-xs text-zinc-500" {
"No commits found"
}
} @else {
ul class="divide-y divide-zinc-100" {
@for c in &commits {
@let sha = c.id.clone();
@let short = short_sha(&sha);
@let subject = first_line(&c.message);
@let author_name = c.author_name.clone();
@let time_iso = iso_time_unix(c.author_time);
@let time_rel = relative_time_unix(c.author_time);
@let commit_url = format!("/{space}/{repo}/commit/{sha}");
@let tree_url = format!("/{space}/{repo}/tree/{sha}");
li class="flex items-center justify-between gap-4 p-3 hover:bg-zinc-50 transition-colors" {
div class="flex flex-col gap-1 min-w-0 flex-1" {
a class="font-medium text-zinc-900 hover:text-zinc-600 truncate text-sm"
href=(commit_url)
title=(format!("{sha}\n{subject}\n{author_name}\n{time_iso}")) {
(subject)
}
div class="flex items-center gap-2 text-xs text-zinc-500" {
span class="font-medium text-zinc-700" { (author_name) }
span { "committed" }
time datetime=(time_iso) title=(time_iso) { (time_rel) }
}
}
div class="flex items-center gap-2 shrink-0 font-mono text-xs" {
a class="rounded border border-zinc-200 bg-zinc-50 px-2 py-1 text-zinc-700 hover:bg-zinc-100 font-mono"
href=(commit_url) {
(short)
}
a class="rounded border border-zinc-200 bg-white p-1 text-zinc-500 hover:text-zinc-800 hover:bg-zinc-50"
href=(tree_url)
title="Browse repository at this point" {
svg class="h-3.5 w-3.5" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" {
path d="M4.72 3.22a.75.75 0 011.06 1.06L2.06 8l3.72 3.72a.75.75 0 11-1.06 1.06L.47 8.53a.75.75 0 010-1.06l4.25-4.25zm6.56 0a.75.75 0 011.06 0l4.25 4.25a.75.75 0 010 1.06l-4.25 4.25a.75.75 0 01-1.06-1.06L14.94 8l-3.72-3.72a.75.75 0 010-1.06z" {}
}
}
}
}
}
}
}
@if page.total > page.limit.into() {
div class="flex items-center justify-between border-t border-zinc-200 bg-zinc-50 px-4 py-2 text-xs" {
@if skip > 0 {
a class="rounded border border-zinc-200 bg-white px-3 py-1 font-medium text-zinc-700 hover:bg-zinc-100"
href=(format!("?revision={}&path={}&limit={}&skip={}", revision, clean_path, limit, skip.saturating_sub(limit))) {
"Newer"
}
} @else {
span class="rounded border border-zinc-200 bg-zinc-50 px-3 py-1 font-medium text-zinc-300 cursor-default" {
"Newer"
}
}
span class="text-zinc-500 font-mono" {
"page " (skip / limit + 1) " / " ((page.total as usize + limit - 1) / limit.max(1))
}
@if has_older {
a class="rounded border border-zinc-200 bg-white px-3 py-1 font-medium text-zinc-700 hover:bg-zinc-100"
href=(format!("?revision={}&path={}&limit={}&skip={}", revision, clean_path, limit, older_skip)) {
"Older"
}
} @else {
span class="rounded border border-zinc-200 bg-zinc-50 px-3 py-1 font-medium text-zinc-300 cursor-default" {
"Older"
}
}
}
}
}
};
response_html(markup, 30)
}