Skip to content
Clone
927a8b2
201 lines 7.2 KB Rust
use crate::rpcgit::commit::types::TreeEntryMode;
use crate::rpcgit::tree::types::TreeEntryWithLatest;
use export::http::{Path, Query, Response, State};
use maud::{Markup, html};
use crate::handler::html::{
HtmlRepoPath, archived_class, clamp_limit, error, first_line, iso_time, relative_time,
resolve_view, response_html, short_sha,
};
use crate::service::GitService;
use crate::share::access::BasicAuth;
#[derive(Debug, Clone, serde::Deserialize, utoipa::IntoParams)]
pub struct TreeQuery {
#[serde(default)]
pub revision: Option<String>,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub limit: Option<usize>,
}
#[utoipa::path(
get,
path = "/repos/{space_name}/{repo_name}/html/tree",
tag = "html",
description = "渲染目录列表表格及每个条目的最近提交信息。",
operation_id = "render_tree",
params(HtmlRepoPath, TreeQuery),
responses(
(status = 200, description = "tree list html fragment", content_type = "text/html"),
(status = 404, description = "repo, ref or path not found")
)
)]
pub async fn render_tree(
State(service): State<GitService>,
Path(path): Path<HtmlRepoPath>,
Query(q): Query<TreeQuery>,
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);
let rows = match ctx
.service
.tree_with_commits_html(&ctx.ctx, &revision, &clean_path)
.await
{
Ok(r) => r,
Err(crate::share::errors::GitError::NotFound) => {
return error::html_err(404, "ref not found");
}
Err(e) => {
tracing::warn!(error = %e, "tree internal");
return error::html_err(500, "internal");
}
};
let total_count = rows.len();
let mut entries = rows;
entries.sort_by(|a, b| {
let dir_a = matches!(a.entry.mode, TreeEntryMode::Tree);
let dir_b = matches!(b.entry.mode, TreeEntryMode::Tree);
dir_b
.cmp(&dir_a)
.then_with(|| a.entry.name.cmp(&b.entry.name))
});
let truncated = entries.len() > limit;
if truncated {
entries.truncate(limit);
}
let base = "git-tree w-full border-collapse text-sm";
let cls = archived_class(&ctx.dto, base);
let space = ctx.space_name.clone();
let repo = ctx.repo_name.clone();
let markup = html! {
table class=(cls)
data-component="git-tree"
data-space=(space)
data-repo=(repo)
data-revision=(revision)
data-path=(clean_path.as_str())
data-count=(total_count)
data-truncated=(if truncated { "true" } else { "false" })
data-archived=(if ctx.dto.is_archived { "true" } else { "false" }) {
thead class="bg-zinc-50 text-xs uppercase text-zinc-500" {
tr {
th class="px-2 py-1 text-left" { "name" }
th class="px-2 py-1 text-left" { "last commit" }
th class="px-2 py-1 text-right" { "date" }
}
}
tbody {
@if !clean_path.is_empty() {
@let parent_dir = if clean_path.contains('/') {
clean_path.rsplit_once('/').map(|(p, _)| p).unwrap_or("")
} else {
""
};
@let parent_href = if parent_dir.is_empty() {
format!("/{space}/{repo}")
} else {
format!("/{space}/{repo}/tree/{revision}/{parent_dir}/")
};
tr class="border-b border-zinc-100 bg-zinc-50/50 hover:bg-zinc-50" {
td class="px-2 py-1 font-mono text-xs text-zinc-500" colspan="3" {
a class="text-zinc-700 hover:underline flex items-center gap-1 font-semibold" href=(parent_href) {
span { ".." }
}
}
}
}
@if entries.is_empty() {
tr {
td class="px-2 py-3 text-center text-xs text-zinc-500"
colspan="3" {
"empty directory"
}
}
} @else {
@for row in &entries {
(render_row(&ctx, &revision, &clean_path, row))
}
}
}
}
};
response_html(markup, 30)
}
fn render_row(
ctx: &crate::handler::html::HtmlCtx,
revision: &str,
parent_path: &str,
row: &TreeEntryWithLatest,
) -> Markup {
let mode = match row.entry.mode {
TreeEntryMode::Tree => "tree",
TreeEntryMode::Submodule => "submodule",
TreeEntryMode::Commit => "commit",
TreeEntryMode::Symlink => "symlink",
TreeEntryMode::Exec => "exec",
TreeEntryMode::Blob => "blob",
};
let entry_path = if parent_path.is_empty() {
row.entry.name.clone()
} else {
format!("{parent_path}/{}", row.entry.name)
};
let href_path = if mode == "tree" || mode == "submodule" {
format!("{entry_path}/")
} else {
entry_path.clone()
};
let href = format!(
"/{}/{}/{}",
ctx.space_name,
ctx.repo_name,
if mode == "tree" || mode == "submodule" {
format!("tree/{revision}/{href_path}")
} else {
format!("blob/{revision}/{href_path}")
}
);
let oid_str = row.entry.oid.to_string();
let short_oid = short_sha(&oid_str);
let sha = row.latest.id.to_string();
let short_sha_str = short_sha(&sha);
let subject = first_line(&row.latest.message);
let author = row.latest.author.name.clone();
let iso = iso_time(row.latest.author.time);
let rel = relative_time(row.latest.author.time);
let commit_url = format!("/{}/{}/commit/{}", ctx.space_name, ctx.repo_name, sha);
html! {
tr class="border-b border-zinc-100 hover:bg-zinc-50"
data-mode=(mode)
data-name=(row.entry.name)
data-path=(entry_path)
data-oid=(short_oid)
data-commit=(short_sha_str) {
td class="px-2 py-1 font-mono" {
a class="text-zinc-900 hover:underline" href=(href) { (row.entry.name) }
}
td class="px-2 py-1" {
a class="flex items-center gap-2 text-zinc-700 hover:text-zinc-900"
href=(commit_url)
title=(format!("{}\n{}\n{}\n{}", sha, subject, author, iso)) {
code class="rounded bg-zinc-100 px-1 text-xs" { (short_sha_str) }
span class="truncate max-w-md text-xs" { (subject) }
}
}
td class="px-2 py-1 text-right text-zinc-500 text-xs" data-time=(row.latest.author.time.unix_timestamp()) {
(rel)
}
}
}
}