use super::*; struct HomeTreeEntry { name: String, path: String, is_tree: bool, } struct HomeReadme { name: String, path: String, content: String, is_markdown: bool, } enum HomePageState { Populated { commit_hash: String, entries: Vec, recent_commits: Vec, readme: Option, }, Empty, } struct HomePage { route_path: String, owner: String, repo_name: String, ref_name: String, branches: Vec, viewer_is_owner: bool, scheme: String, host: String, state: HomePageState, } impl HomePage { fn home_path(&self, ref_name: &str) -> String { format!("{}?ref={}", self.route_path, ref_name) } fn current_path(&self) -> String { self.home_path(&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 commits_path(&self) -> String { format!( "/{}/{}/commits?ref={}", self.owner, self.repo_name, self.ref_name ) } fn search_path(&self) -> String { format!("/{}/{}/search-ui?scope=code", self.owner, self.repo_name) } fn issues_path(&self) -> String { format!("/{}/{}/issues", self.owner, self.repo_name) } fn pulls_path(&self) -> String { format!("/{}/{}/pulls", self.owner, self.repo_name) } fn new_issue_path(&self) -> String { format!("/{}/{}/issues/new", self.owner, self.repo_name) } fn new_pull_path(&self) -> String { format!("/{}/{}/pulls/new", self.owner, self.repo_name) } fn settings_path(&self) -> String { format!("/{}/{}/settings", self.owner, self.repo_name) } fn push_remote_url(&self) -> String { format!( "{}://{}:TOKEN@{}/{}/{}", self.scheme, self.owner, self.host, self.owner, self.repo_name ) } } fn build_home_page( sql: &SqlStorage, owner: &str, repo_name: &str, url: &Url, actor_name: Option<&str>, ) -> Result { let (ref_name, head_hash) = if let Some(reference) = api::get_query(url, "ref") { let hash = api::resolve_ref(sql, &reference)?; (reference, hash) } else { resolve_default_branch(sql)? }; let state = match head_hash { Some(hash) => { let tree_hash = load_tree_for_commit(sql, &hash)?; let entries = load_sorted_tree(sql, &tree_hash)? .into_iter() .map(|entry| HomeTreeEntry { path: entry.name.clone(), name: entry.name, is_tree: entry.is_tree, }) .collect(); let recent_commits = summarize_commits(walk_commits(sql, &hash, 5, 0)?); let readme = load_root_readme(sql, &tree_hash)?; HomePageState::Populated { commit_hash: hash, entries, recent_commits, readme, } } None => HomePageState::Empty, }; Ok(HomePage { route_path: url.path().to_string(), owner: owner.to_string(), repo_name: repo_name.to_string(), ref_name, branches: load_branches(sql)?, viewer_is_owner: actor_name == Some(owner), scheme: url.scheme().to_string(), host: url.host_str().unwrap_or("your-worker.dev").to_string(), state, }) } fn render_home_branch_selector(page: &HomePage) -> String { if page.branches.len() <= 1 { return format!( r#"
branch: {}
"#, html_escape(&page.ref_name) ); } let mut html = String::from( r#"
branch:
"); html } fn render_home_html(page: &HomePage, actor_name: Option<&str>, sql: &SqlStorage) -> String { let content = match &page.state { HomePageState::Populated { commit_hash, entries, recent_commits, readme, } => { let mut html = String::new(); html.push_str(&render_home_branch_selector(page)); html.push_str(r#""#); for entry in entries { let icon = if entry.is_tree { "📁" } else { "📄" }; let link = if entry.is_tree { page.tree_path(&entry.path) } else { page.blob_path(&entry.path) }; html.push_str(&format!( r#""#, icon = icon, link = html_escape(&link), name = html_escape(&entry.name), )); } html.push_str("
{icon}{name}
"); html.push_str(r#"

Recent commits

"#); html.push_str(&render_commit_list( recent_commits, &page.owner, &page.repo_name, false, )); if let Some(readme) = readme { html.push_str(&render_home_readme_html(sql, page, commit_hash, readme)); } html } HomePageState::Empty => { if page.viewer_is_owner { format!( r#"

This repository is empty

Push your first commit to get started:

cd my-project
git init
git add .
git commit -m "initial commit"
git remote add origin {remote}
git push origin main

Replace TOKEN with an access token from Settings.

"#, remote = page.push_remote_url(), ) } else { "

Empty repository.

".to_string() } } }; layout( "Home", &page.owner, &page.repo_name, &page.ref_name, actor_name, &content, ) } fn render_home_markdown(page: &HomePage, selection: &NegotiatedRepresentation) -> String { let mut markdown = format!( "# {}/{}\n\nBranch: `{}`\n", page.owner, page.repo_name, page.ref_name ); match &page.state { HomePageState::Populated { commit_hash: _, entries, recent_commits, readme, } => { markdown.push_str("\n## Files (GET paths)\n"); if entries.is_empty() { markdown.push_str("No files at this ref.\n"); } else { for entry in entries { let label = if entry.is_tree { "dir" } else { "file" }; let display_name = if entry.is_tree { format!("{}/", entry.name) } else { entry.name.clone() }; let path = if entry.is_tree { page.tree_path(&entry.path) } else { page.blob_path(&entry.path) }; markdown.push_str(&format!("- {} `{}` - `{}`\n", label, display_name, path)); } } markdown.push_str("\n## Recent Commits (GET paths)\n"); if recent_commits.is_empty() { markdown.push_str("No commits yet.\n"); } else { for commit in recent_commits { markdown.push_str(&format!( "- `{}` - {} - {} - {} - `{}`\n", commit.short_hash, commit.subject, commit.author, commit.relative_time, format!("/{}/{}/commit/{}", page.owner, page.repo_name, commit.hash) )); } } if let Some(readme) = readme { markdown.push_str(&format!( "\n## README (`{}`)\nSource path: `{}`\n", readme.name, page.blob_path(&readme.path) )); if readme.is_markdown { markdown.push_str("Format: source markdown\n"); } markdown.push('\n'); markdown.push_str(&markdown_literal_block(&readme.content)); } } HomePageState::Empty => { markdown.push_str("\nRepository is empty.\n"); if page.viewer_is_owner { markdown.push_str("\nPush your first commit with:\n\n"); markdown.push_str(&markdown_literal_block(&format!( "cd my-project\ngit init\ngit add .\ngit commit -m \"initial commit\"\ngit remote add origin {}\ngit push origin main", page.push_remote_url() ))); markdown.push_str("\nReplace `TOKEN` with an access token from `/settings`.\n"); } } } 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.home_path(branch) )); } } let mut actions = vec![ Action::get(page.current_path(), "reload this repository view"), Action::get(page.issues_path(), "list issues"), Action::get(page.pulls_path(), "list pull requests"), Action::get(page.search_path(), "search the default branch code index"), Action::get(page.new_issue_path(), "open the new issue form") .with_requires("authenticated user"), Action::get(page.new_pull_path(), "open the new pull request form") .with_requires("authenticated user"), Action::get(page.settings_path(), "open repository settings").with_requires("repo owner"), ]; if matches!(page.state, HomePageState::Populated { .. }) { actions.insert( 1, Action::get( page.commits_path(), "browse the full commit history for this ref", ), ); } let hints = vec![ presentation::text_navigation_hint(*selection), Hint::new("Directories use `/tree/{ref}/{path}` and files use `/blob/{ref}/{path}`."), Hint::new( "Search currently follows the default branch index rather than the `ref=` query.", ), ]; markdown.push_str(&presentation::render_actions_section(&actions)); markdown.push_str(&presentation::render_hints_section(&hints)); markdown } fn load_root_readme(sql: &SqlStorage, tree_hash: &str) -> Result> { let entries = load_sorted_tree(sql, tree_hash)?; let readme = entries.iter().find(|entry| { let lower = entry.name.to_lowercase(); lower == "readme.md" || lower == "readme" || lower == "readme.txt" }); let entry = match readme { Some(entry) => entry, None => return Ok(None), }; if entry.is_tree { return Ok(None); } let content = load_blob(sql, &entry.hash)?; let text = match std::str::from_utf8(&content) { Ok(text) => text.to_string(), Err(_) => return Ok(None), }; Ok(Some(HomeReadme { name: entry.name.clone(), path: entry.name.clone(), is_markdown: entry.name.to_lowercase().ends_with(".md"), content: text, })) } fn render_home_readme_html( sql: &SqlStorage, page: &HomePage, commit_hash: &str, readme: &HomeReadme, ) -> String { let mut html = String::new(); html.push_str(r#"
"#); html.push_str(&format!( r#""#, html_escape(&page.blob_path(&readme.path)), html_escape(&readme.name), )); html.push_str(r#"
"#); if readme.is_markdown { html.push_str(&render_repo_markdown( sql, &readme.content, &page.owner, &page.repo_name, &page.ref_name, commit_hash, &readme.path, )); } else { html.push_str(&format!("
{}
", html_escape(&readme.content))); } html.push_str("
"); html } 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 } pub fn page_home( sql: &SqlStorage, owner: &str, repo_name: &str, url: &Url, actor_name: Option<&str>, ) -> Result { let page = build_home_page(sql, owner, repo_name, url, actor_name)?; html_response(&render_home_html(&page, actor_name, sql)) } pub fn page_home_markdown( sql: &SqlStorage, owner: &str, repo_name: &str, url: &Url, actor_name: Option<&str>, selection: &NegotiatedRepresentation, ) -> Result { let page = build_home_page(sql, owner, repo_name, url, actor_name)?; presentation::markdown_response(&render_home_markdown(&page, selection), selection) }