use crate::rpcgit::commit::types::TreeEntryMode; use export::http::{Path, Query, Response, State}; use maud::html; use crate::handler::html::{ HtmlRepoPath, 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 TreeRowQuery { #[serde(default)] pub revision: Option, #[serde(default)] pub path: Option, #[serde(default)] pub name: Option, } #[utoipa::path( get, path = "/repos/{space_name}/{repo_name}/html/tree-row", tag = "html", description = "渲染目录列表中单个条目的 HTML 行片段。", operation_id = "render_tree_row", params(HtmlRepoPath, TreeRowQuery), responses( (status = 200, description = "tree row html fragment", content_type = "text/html"), (status = 404, description = "repo, ref or path not found") ) )] pub async fn render_tree_row( State(service): State, Path(path): Path, Query(q): Query, 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 parent_clean = view.clean_path; let name = match q.name.as_deref() { Some(n) if !n.is_empty() && n != ".." && !n.contains('/') && !n.contains('\\') => { n.to_string() } _ => return error::html_err(404, "name invalid"), }; if name.is_empty() || name.chars().count() > crate::handler::html::MAX_NAME_LEN { return error::html_err(404, "name invalid"); } let lookup_path = if parent_clean.is_empty() { format!("{name}/") } else { format!("{parent_clean}/{name}/") }; let lookup_file = if parent_clean.is_empty() { name.clone() } else { format!("{parent_clean}/{name}") }; let rows = match ctx .service .tree_with_commits_html(&ctx.ctx, &revision, &parent_clean) .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-row internal"); return error::html_err(500, "internal"); } }; let matched = rows.iter().find(|r| { let entry_path = if r.entry.mode == TreeEntryMode::Tree { format!("{}/", r.entry.name) } else { r.entry.name.clone() }; if parent_clean.is_empty() { r.entry.name == name || entry_path == lookup_path } else { entry_path == lookup_file || entry_path == lookup_path } }); let row = match matched { Some(r) => r, None => return error::html_err(404, "entry not found"), }; 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 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); let blob_url = format!( "/{}/{}/blob/{}/{}", ctx.space_name, ctx.repo_name, revision, lookup_path ); let href = if mode == "tree" || mode == "submodule" { format!( "/{}/{}/tree/{}/{}", ctx.space_name, ctx.repo_name, revision, lookup_path ) } else { blob_url.clone() }; let base = "git-tree-row border-b border-zinc-100 hover:bg-zinc-50"; let cls = if ctx.dto.is_archived { format!("{base} opacity-60") } else { base.to_string() }; let markup = html! { tr class=(cls) data-component="git-tree-row" data-space=(ctx.space_name) data-repo=(ctx.repo_name) data-revision=(revision) data-path=(lookup_path) data-mode=(mode) data-name=(name.as_str()) data-oid=(short_oid) data-commit=(short_sha_str) data-archived=(if ctx.dto.is_archived { "true" } else { "false" }) { td class="w-8 px-2 py-1 text-zinc-400" { @if mode == "tree" { svg class="h-4 w-4" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" { path d="M1.75 1A1.75 1.75 0 000 2.75v10.5C0 14.216.784 15 1.75 15h12.5A1.75 1.75 0 0016 13.25V2.75A1.75 1.75 0 0014.25 1H1.75zM1.5 2.75a.25.25 0 01.25-.25h12.5a.25.25 0 01.25.25v.857l-4.61 4.61a.25.25 0 01-.354 0L5.61 4.354a.25.25 0 00-.354 0L1.5 8.111V2.75zm0 7.268l3.405-3.405 2.79 2.79a.75.75 0 001.06 0l3.245-3.245V13.25a.25.25 0 01-.25.25H1.75a.25.25 0 01-.25-.25V10.018z" {} } } @else if mode == "submodule" { svg class="h-4 w-4" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" { path d="M4.25 2.5a.75.75 0 000 1.5h7.5a.75.75 0 000-1.5h-7.5zm0 4a.75.75 0 000 1.5h7.5a.75.75 0 000-1.5h-7.5zm0 4a.75.75 0 000 1.5h4.5a.75.75 0 000-1.5h-4.5z" {} } } @else if mode == "symlink" { svg class="h-4 w-4" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" { path d="M2.5 4.5v7a1 1 0 001 1h7a1 1 0 001-1v-3.379a1.5 1.5 0 00-.44-1.061L8.94 5.94a1.5 1.5 0 00-1.061-.439H3.5a1 1 0 00-1 1z" {} } } @else if mode == "exec" { svg class="h-4 w-4" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" { path d="M2.75 2.5a.25.25 0 00-.25.25v10.5c0 .138.112.25.25.25h10.5a.25.25 0 00.25-.25V2.75a.25.25 0 00-.25-.25H2.75z" {} } } @else { svg class="h-4 w-4" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg" { path d="M2 1.75C2 .784 2.784 0 3.75 0h6.586c.464 0 .909.184 1.237.513l2.914 2.914c.329.328.513.773.513 1.237v9.586A1.75 1.75 0 0113.25 16h-9.5A1.75 1.75 0 012 14.25V1.75z" {} } } } td class="px-2 py-1 font-mono" { a class="text-zinc-900 hover:underline" href=(href) { (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) } } }; response_html(markup, 30) }