use super::*; struct TreePage { owner: String, repo_name: String, ref_name: String, path: String, commit_hash: String, branches: Vec, entries: Vec, } impl TreePage { fn current_path(&self) -> String { format!( "/{}/{}/tree/{}/{}", self.owner, self.repo_name, self.ref_name, self.path ) } fn repo_home_path(&self) -> String { format!("/{}/{}/?ref={}", self.owner, self.repo_name, self.ref_name) } fn commits_path(&self) -> String { format!( "/{}/{}/commits?ref={}", self.owner, self.repo_name, self.ref_name ) } fn tree_path(&self, path: &str) -> String { format!( "/{}/{}/tree/{}/{}", self.owner, self.repo_name, self.ref_name, path ) } fn blob_path(&self, path: &str) -> String { format!( "/{}/{}/blob/{}/{}", self.owner, self.repo_name, self.ref_name, path ) } fn display_path(&self) -> &str { if self.path.is_empty() { "/" } else { &self.path } } fn up_path(&self) -> Option { if self.path.is_empty() { return None; } let parent = parent_path(&self.path); Some(if parent.is_empty() { format!("/{}/{}/", self.owner, self.repo_name) } else { self.tree_path(&parent) }) } fn child_path(&self, entry: &SortedEntry) -> String { let full_path = if self.path.is_empty() { entry.name.clone() } else { format!("{}/{}", self.path, entry.name) }; if entry.is_tree { self.tree_path(&full_path) } else { self.blob_path(&full_path) } } fn breadcrumb_paths(&self) -> Vec<(String, String, bool)> { let mut crumbs = vec![("/".to_string(), self.repo_home_path(), self.path.is_empty())]; if self.path.is_empty() { return crumbs; } let parts: Vec<&str> = self .path .split('/') .filter(|segment| !segment.is_empty()) .collect(); for (index, part) in parts.iter().enumerate() { let sub_path = parts[..=index].join("/"); crumbs.push(( (*part).to_string(), self.tree_path(&sub_path), index == parts.len() - 1, )); } crumbs } fn branch_path(&self, branch: &str) -> String { format!( "/{}/{}/tree/{}/{}", self.owner, self.repo_name, branch, self.path ) } } struct BlobPage { owner: String, repo_name: String, ref_name: String, path: String, commit_hash: String, blob_hash: String, branches: Vec, content: Vec, } impl BlobPage { fn current_path(&self) -> String { format!( "/{}/{}/blob/{}/{}", self.owner, self.repo_name, self.ref_name, self.path ) } fn repo_home_path(&self) -> String { format!("/{}/{}/?ref={}", self.owner, self.repo_name, self.ref_name) } fn commits_path(&self) -> String { format!( "/{}/{}/commits?ref={}", self.owner, self.repo_name, self.ref_name ) } fn tree_path(&self, path: &str) -> String { format!( "/{}/{}/tree/{}/{}", self.owner, self.repo_name, self.ref_name, path ) } fn raw_path(&self) -> String { format!( "/{}/{}/raw/{}/{}", self.owner, self.repo_name, self.ref_name, self.path ) } fn filename(&self) -> &str { self.path.rsplit('/').next().unwrap_or(&self.path) } fn parent_tree_path(&self) -> String { let parent = parent_path(&self.path); if parent.is_empty() { format!("/{}/{}/", self.owner, self.repo_name) } else { self.tree_path(&parent) } } fn branch_path(&self, branch: &str) -> String { format!( "/{}/{}/blob/{}/{}", self.owner, self.repo_name, branch, self.path ) } fn is_binary(&self) -> bool { let limit = self.content.len().min(8192); limit > 0 && self.content[..limit].contains(&0) } fn renderable_text(&self) -> Option<(String, bool)> { if self.is_binary() { return None; } match String::from_utf8(self.content.clone()) { Ok(text) => Some((text, true)), Err(_) => Some((String::from_utf8_lossy(&self.content).into_owned(), false)), } } fn breadcrumb_paths(&self) -> Vec<(String, String, bool)> { let mut crumbs = vec![("/".to_string(), self.repo_home_path(), false)]; let parts: Vec<&str> = self .path .split('/') .filter(|segment| !segment.is_empty()) .collect(); for (index, part) in parts.iter().enumerate() { let is_last = index == parts.len() - 1; let sub_path = parts[..=index].join("/"); let path = if is_last { self.current_path() } else { self.tree_path(&sub_path) }; crumbs.push(((*part).to_string(), path, is_last)); } crumbs } } fn build_tree_page( sql: &SqlStorage, owner: &str, repo_name: &str, ref_name: &str, path: &str, commit_hash: String, ) -> Result { let tree_hash = resolve_path_to_tree(sql, &commit_hash, path)?; let entries = load_sorted_tree(sql, &tree_hash)?; Ok(TreePage { owner: owner.to_string(), repo_name: repo_name.to_string(), ref_name: ref_name.to_string(), path: path.to_string(), commit_hash, branches: load_branches(sql)?, entries, }) } fn build_blob_page( sql: &SqlStorage, owner: &str, repo_name: &str, ref_name: &str, path: &str, commit_hash: String, ) -> Result { let blob_hash = resolve_path_to_blob(sql, &commit_hash, path)?; let content = load_blob(sql, &blob_hash)?; Ok(BlobPage { owner: owner.to_string(), repo_name: repo_name.to_string(), ref_name: ref_name.to_string(), path: path.to_string(), commit_hash, blob_hash, branches: load_branches(sql)?, content, }) } fn render_tree_html( page: &TreePage, actor_name: Option<&str>, sql: &SqlStorage, ) -> Result { let mut html = String::new(); html.push_str(&render_branch_selector( sql, &page.owner, &page.repo_name, &page.ref_name, "tree", &page.path, )?); html.push_str(&render_breadcrumb( &page.owner, &page.repo_name, &page.ref_name, &page.path, )); html.push_str(r#""#); if let Some(parent_link) = page.up_path() { html.push_str(&format!( r#""#, parent_link )); } for entry in &page.entries { let icon = if entry.is_tree { "📁" } else { "📄" }; let link = page.child_path(entry); html.push_str(&format!( r#""#, icon = icon, link = link, name = html_escape(&entry.name), )); } html.push_str("
📁..
{icon}{name}
"); html_response(&layout( &page.path, &page.owner, &page.repo_name, &page.ref_name, actor_name, &html, )) } fn render_tree_markdown(page: &TreePage, selection: &NegotiatedRepresentation) -> String { let mut markdown = format!( "# {}/{} tree\n\nBranch: `{}`\nLocation: `{}`\nCurrent path: `{}`\nCommit: `{}`\n", page.owner, page.repo_name, page.ref_name, page.display_path(), page.current_path(), page.commit_hash, ); markdown.push_str("\n## Breadcrumb (GET paths)\n"); for (label, path, current) in page.breadcrumb_paths() { let suffix = if current { " (current)" } else { "" }; markdown.push_str(&format!("- `{}`{} - `{}`\n", label, suffix, path)); } if let Some(up_path) = page.up_path() { markdown.push_str("\n## Navigation (GET paths)\n"); markdown.push_str(&format!("- up - `{}`\n", up_path)); markdown.push_str(&format!( "- repository home - `{}`\n", page.repo_home_path() )); } markdown.push_str("\n## Files (GET paths)\n"); if page.entries.is_empty() { markdown.push_str("Directory is empty.\n"); } else { for entry in &page.entries { let kind = if entry.is_tree { "dir" } else { "file" }; let name = if entry.is_tree { format!("{}/", entry.name) } else { entry.name.clone() }; markdown.push_str(&format!( "- {} `{}` - `{}`\n", kind, name, page.child_path(entry) )); } } if !page.branches.is_empty() { markdown.push_str("\n## Branches (GET paths)\n"); for branch in &page.branches { let current = if branch == &page.ref_name { " (current)" } else { "" }; markdown.push_str(&format!( "- `{}`{} - `{}`\n", branch, current, page.branch_path(branch) )); } } let mut actions = vec![ Action::get(page.current_path(), "reload this tree view"), Action::get( page.repo_home_path(), "open the repository home at this ref", ), Action::get(page.commits_path(), "browse commit history for this ref"), ]; if let Some(up_path) = page.up_path() { actions.push(Action::get(up_path, "move up to the parent directory")); } let hints = vec![ presentation::text_navigation_hint(*selection), Hint::new("Root breadcrumb paths go to the repository home, matching the HTML breadcrumb."), Hint::new("Directory entries sort with subdirectories first, then files."), ]; markdown.push_str(&presentation::render_actions_section(&actions)); markdown.push_str(&presentation::render_hints_section(&hints)); markdown } fn render_blob_html( page: &BlobPage, actor_name: Option<&str>, sql: &SqlStorage, ) -> Result { let mut html = String::new(); html.push_str(&render_branch_selector( sql, &page.owner, &page.repo_name, &page.ref_name, "blob", &page.path, )?); html.push_str(&render_breadcrumb( &page.owner, &page.repo_name, &page.ref_name, &page.path, )); let filename = page.filename(); let size = page.content.len(); html.push_str(&format!( r#"
{filename}
{size} bytesRaw
"#, owner = html_escape(&page.owner), repo = html_escape(&page.repo_name), ref_name = html_escape(&page.ref_name), path = page.path, filename = html_escape(filename), size = size, )); if page.is_binary() { html.push_str(r#"
Binary file not shown.
"#); } else { let text = String::from_utf8_lossy(&page.content); let lang_class = lang_from_filename(filename); html.push_str(&format!( r#"
{code}
"#, lang = lang_class, code = html_escape(&text), )); } html_response(&layout( filename, &page.owner, &page.repo_name, &page.ref_name, actor_name, &html, )) } fn render_blob_markdown(page: &BlobPage, selection: &NegotiatedRepresentation) -> String { let is_binary = page.is_binary(); let text = page.renderable_text(); let mut markdown = format!( "# {}/{} blob\n\nBranch: `{}`\nFile: `{}`\nCurrent path: `{}`\nParent tree: `{}`\nRaw path: `{}`\nCommit: `{}`\nBlob: `{}`\nSize: `{}` bytes\nBinary: `{}`\n", page.owner, page.repo_name, page.ref_name, page.path, page.current_path(), page.parent_tree_path(), page.raw_path(), page.commit_hash, page.blob_hash, page.content.len(), if is_binary { "yes" } else { "no" }, ); markdown.push_str("\n## Breadcrumb (GET paths)\n"); for (label, path, current) in page.breadcrumb_paths() { let suffix = if current { " (current)" } else { "" }; markdown.push_str(&format!("- `{}`{} - `{}`\n", label, suffix, path)); } if !page.branches.is_empty() { markdown.push_str("\n## Branches (GET paths)\n"); for branch in &page.branches { let current = if branch == &page.ref_name { " (current)" } else { "" }; markdown.push_str(&format!( "- `{}`{} - `{}`\n", branch, current, page.branch_path(branch) )); } } markdown.push_str("\n## Content\n"); if is_binary { markdown .push_str("Binary file not shown in text mode. Use the raw path for exact bytes.\n"); } else if let Some((text, is_utf8)) = text { let encoding_note = if is_utf8 { "UTF-8 text" } else { "lossy UTF-8 view of non-UTF-8 text bytes" }; markdown.push_str(&format!("Rendering: {}\n\n", encoding_note)); markdown.push_str(&markdown_literal_block(&text)); } let actions = vec![ Action::get(page.current_path(), "reload this blob view"), Action::get(page.parent_tree_path(), "open the containing directory"), Action::get(page.raw_path(), "download or stream the raw file bytes"), Action::get(page.commits_path(), "browse commit history for this ref"), ]; let hints = vec![ presentation::text_navigation_hint(*selection), Hint::new( "Use the raw path for exact bytes, especially for binary files or original encodings.", ), Hint::new("Line anchors like `#L42` remain available on the HTML blob route."), ]; markdown.push_str(&presentation::render_actions_section(&actions)); markdown.push_str(&presentation::render_hints_section(&hints)); markdown } pub fn page_tree( sql: &SqlStorage, owner: &str, repo_name: &str, ref_name: &str, path: &str, actor_name: Option<&str>, ) -> Result { let commit_hash = match api::resolve_ref(sql, ref_name)? { Some(hash) => hash, None => return Response::error("ref not found", 404), }; let page = build_tree_page(sql, owner, repo_name, ref_name, path, commit_hash)?; render_tree_html(&page, actor_name, sql) } pub fn page_tree_markdown( sql: &SqlStorage, owner: &str, repo_name: &str, ref_name: &str, path: &str, selection: &NegotiatedRepresentation, ) -> Result { let commit_hash = match api::resolve_ref(sql, ref_name)? { Some(hash) => hash, None => return Response::error("ref not found", 404), }; let page = build_tree_page(sql, owner, repo_name, ref_name, path, commit_hash)?; presentation::markdown_response(&render_tree_markdown(&page, selection), selection) } pub fn page_blob( sql: &SqlStorage, owner: &str, repo_name: &str, ref_name: &str, path: &str, actor_name: Option<&str>, ) -> Result { let commit_hash = match api::resolve_ref(sql, ref_name)? { Some(hash) => hash, None => return Response::error("ref not found", 404), }; let page = build_blob_page(sql, owner, repo_name, ref_name, path, commit_hash)?; render_blob_html(&page, actor_name, sql) } pub fn page_blob_markdown( sql: &SqlStorage, owner: &str, repo_name: &str, ref_name: &str, path: &str, selection: &NegotiatedRepresentation, ) -> Result { let commit_hash = match api::resolve_ref(sql, ref_name)? { Some(hash) => hash, None => return Response::error("ref not found", 404), }; let page = build_blob_page(sql, owner, repo_name, ref_name, path, commit_hash)?; presentation::markdown_response(&render_blob_markdown(&page, selection), selection) } fn render_breadcrumb(owner: &str, repo_name: &str, ref_name: &str, path: &str) -> String { let mut html = String::from(r#""); return html; } let parts: Vec<&str> = path .split('/') .filter(|segment| !segment.is_empty()) .collect(); for (index, part) in parts.iter().enumerate() { if index < parts.len() - 1 { let sub_path = parts[..=index].join("/"); html.push_str(&format!( r#"{} / "#, owner, repo_name, ref_name, sub_path, html_escape(part), )); } else { html.push_str(&format!("{}", html_escape(part))); } } html.push_str(""); html } fn render_branch_selector( sql: &SqlStorage, owner: &str, repo_name: &str, current_ref: &str, page_type: &str, path: &str, ) -> Result { let branches = load_branches(sql)?; if branches.len() <= 1 { return Ok(format!( r#"
branch: {}
"#, html_escape(current_ref) )); } let mut html = String::from( r#"
branch:
"); Ok(html) } fn parent_path(path: &str) -> String { match path.rfind('/') { Some(position) => path[..position].to_string(), None => String::new(), } } fn lang_from_filename(name: &str) -> String { let ext = name.rsplit('.').next().unwrap_or(""); let lang = match ext { "rs" => "language-rust", "js" | "mjs" | "cjs" => "language-javascript", "ts" | "mts" | "cts" => "language-typescript", "py" => "language-python", "rb" => "language-ruby", "go" => "language-go", "java" => "language-java", "c" | "h" => "language-c", "cpp" | "cc" | "cxx" | "hpp" => "language-cpp", "cs" => "language-csharp", "swift" => "language-swift", "kt" | "kts" => "language-kotlin", "php" => "language-php", "sh" | "bash" | "zsh" => "language-bash", "json" => "language-json", "yaml" | "yml" => "language-yaml", "toml" => "language-toml", "xml" | "svg" | "html" | "htm" => "language-xml", "css" => "language-css", "scss" | "sass" => "language-scss", "sql" => "language-sql", "md" | "markdown" => "language-markdown", "dockerfile" | "Dockerfile" => "language-dockerfile", "makefile" | "Makefile" => "language-makefile", "zig" => "language-zig", "lua" => "language-lua", "r" | "R" => "language-r", "ex" | "exs" => "language-elixir", "erl" | "hrl" => "language-erlang", "hs" => "language-haskell", "ml" | "mli" => "language-ocaml", "nix" => "language-nix", _ => "", }; lang.to_string() } fn markdown_literal_block(text: &str) -> String { let mut output = String::new(); for line in text.lines() { output.push_str(" "); output.push_str(line); output.push('\n'); } if output.is_empty() { output.push_str(" \n"); } output }