use crate::{api, presentation, web}; use worker::*; use super::{load_branches, Url}; use presentation::{Action, ActionField, Hint, NegotiatedRepresentation}; struct IssueFormPage { owner: String, repo_name: String, default_branch: String, } impl IssueFormPage { fn new(sql: &SqlStorage, owner: &str, repo_name: &str) -> Result { let (default_branch, _) = web::resolve_default_branch(sql)?; Ok(Self { owner: owner.to_string(), repo_name: repo_name.to_string(), default_branch, }) } fn form_path(&self) -> String { format!("/{}/{}/issues/new", self.owner, self.repo_name) } fn submit_path(&self) -> String { format!("/{}/{}/issues", self.owner, self.repo_name) } } struct PullFormPage { owner: String, repo_name: String, default_branch: String, branches: Vec, preselect_source: String, preselect_target: String, } impl PullFormPage { fn new(sql: &SqlStorage, owner: &str, repo_name: &str, url: &Url) -> Result { let (default_branch, _) = web::resolve_default_branch(sql)?; let branches = load_branches(sql)?; let preselect_source = api::get_query(url, "source").unwrap_or_default(); let preselect_target = api::get_query(url, "target").unwrap_or_else(|| default_branch.clone()); Ok(Self { owner: owner.to_string(), repo_name: repo_name.to_string(), default_branch, branches, preselect_source, preselect_target, }) } fn form_path(&self) -> String { format!("/{}/{}/pulls/new", self.owner, self.repo_name) } fn submit_path(&self) -> String { format!("/{}/{}/pulls", self.owner, self.repo_name) } fn has_enough_branches(&self) -> bool { self.branches.len() >= 2 } } pub fn page_new_issue( sql: &SqlStorage, owner: &str, repo_name: &str, actor_name: Option<&str>, ) -> Result { let page = IssueFormPage::new(sql, owner, repo_name)?; let content = format!( r#"

New Issue

Cancel
"#, owner = web::html_escape(&page.owner), repo = web::html_escape(&page.repo_name), ); web::html_response(&web::layout( "New Issue", &page.owner, &page.repo_name, &page.default_branch, actor_name, &content, )) } pub fn page_new_issue_markdown( sql: &SqlStorage, owner: &str, repo_name: &str, _actor_name: Option<&str>, selection: &NegotiatedRepresentation, ) -> Result { let page = IssueFormPage::new(sql, owner, repo_name)?; presentation::markdown_response(&render_new_issue_markdown(&page, selection), selection) } pub fn page_new_pull( sql: &SqlStorage, owner: &str, repo_name: &str, url: &Url, actor_name: Option<&str>, ) -> Result { let page = PullFormPage::new(sql, owner, repo_name, url)?; if !page.has_enough_branches() { let content = r#"

You need at least two branches to open a pull request.

"#; return web::html_response(&web::layout( "New Pull Request", &page.owner, &page.repo_name, &page.default_branch, actor_name, content, )); } let source_options: String = page .branches .iter() .map(|branch| { let selected = if branch == &page.preselect_source { " selected" } else { "" }; format!( r#""#, value = web::html_escape(branch), selected = selected ) }) .collect(); let target_options: String = page .branches .iter() .map(|branch| { let selected = if branch == &page.preselect_target { " selected" } else { "" }; format!( r#""#, value = web::html_escape(branch), selected = selected ) }) .collect(); let content = format!( r#"

New Pull Request

Cancel
"#, owner = web::html_escape(&page.owner), repo = web::html_escape(&page.repo_name), source_options = source_options, target_options = target_options, ); web::html_response(&web::layout( "New Pull Request", &page.owner, &page.repo_name, &page.default_branch, actor_name, &content, )) } pub fn page_new_pull_markdown( sql: &SqlStorage, owner: &str, repo_name: &str, url: &Url, _actor_name: Option<&str>, selection: &NegotiatedRepresentation, ) -> Result { let page = PullFormPage::new(sql, owner, repo_name, url)?; presentation::markdown_response(&render_new_pull_markdown(&page, selection), selection) } fn render_new_issue_markdown(page: &IssueFormPage, selection: &NegotiatedRepresentation) -> String { let mut markdown = format!( "# New issue - {}/{}\n\nPOST endpoint: `{}`\nAuthentication: required\nRequired fields: `title`\nOptional fields: `body`\nCancel path: `{}`\nList path: `{}`\nResult: creates a new open issue.\n", page.owner, page.repo_name, page.submit_path(), page.submit_path(), page.submit_path(), ); markdown.push_str("\n## Related Paths (GET paths)\n"); markdown.push_str(&format!("- `{}`\n", page.form_path())); markdown.push_str(&format!("- `{}`\n", page.submit_path())); let actions = vec![ Action::post(page.submit_path(), "create a new issue") .with_fields(vec![ ActionField::required("title", "short issue summary; must be non-empty"), ActionField::optional("body", "markdown description for the issue body"), ]) .with_requires("authenticated user") .with_effect("stores an open issue and redirects to its detail page"), Action::get(page.submit_path(), "list existing issues"), Action::get(page.form_path(), "reload this form description"), ]; let hints = vec![ presentation::text_navigation_hint(*selection), Hint::new("The HTML form marks `title` as required and autofocuses it."), Hint::new("`body` accepts markdown and may be left empty."), Hint::new("Use the issues list path as the cancel destination for this form."), ]; markdown.push_str(&presentation::render_actions_section(&actions)); markdown.push_str(&presentation::render_hints_section(&hints)); markdown } fn render_new_pull_markdown(page: &PullFormPage, selection: &NegotiatedRepresentation) -> String { let mut markdown = format!( "# New pull request - {}/{}\n\nPOST endpoint: `{}`\nAuthentication: required\nRequired fields: `target`, `source`, `title`\nOptional fields: `body`\nConstraint: `source` and `target` must differ.\nCancel path: `{}`\nList path: `{}`\nDefault target branch: `{}`\nPreselected source branch: `{}`\nPreselected target branch: `{}`\n", page.owner, page.repo_name, page.submit_path(), page.submit_path(), page.submit_path(), page.default_branch, if page.preselect_source.is_empty() { "none" } else { &page.preselect_source }, if page.preselect_target.is_empty() { "none" } else { &page.preselect_target }, ); if page.has_enough_branches() { markdown.push_str("Pull request creation is available from this form.\n"); } else { markdown.push_str("Pull request creation is currently unavailable because fewer than two branches exist.\n"); } markdown.push_str("\n## Branches\n"); if page.branches.is_empty() { markdown.push_str("No branches found.\n"); } else { for branch in &page.branches { let mut suffix = String::new(); if branch == &page.default_branch { suffix.push_str(" (default target)"); } if branch == &page.preselect_source { suffix.push_str(" (preselected source)"); } if branch == &page.preselect_target { suffix.push_str(" (preselected target)"); } markdown.push_str(&format!("- `{}`{}\n", branch, suffix)); } } markdown.push_str("\n## Related Paths (GET paths)\n"); markdown.push_str(&format!("- `{}`\n", page.form_path())); markdown.push_str(&format!("- `{}`\n", page.submit_path())); let mut actions = vec![ Action::get(page.submit_path(), "list existing pull requests"), Action::get(page.form_path(), "reload this form description"), ]; let post_action = Action::post(page.submit_path(), "create a new pull request") .with_fields(vec![ ActionField::required("target", "base branch to merge into"), ActionField::required("source", "compare branch containing the proposed changes"), ActionField::required("title", "pull request summary; must be non-empty"), ActionField::optional("body", "markdown description for the pull request body"), ]) .with_requires("authenticated user") .with_effect("stores an open pull request and redirects to its detail page"); if page.has_enough_branches() { actions.insert(0, post_action); } else { actions.insert( 0, post_action.with_effect( "requires at least two existing branches before the HTML form can be used", ), ); } let hints = vec![ presentation::text_navigation_hint(*selection), Hint::new("`source` and `target` are both required, must name existing branches, and must differ."), Hint::new("The HTML form defaults `target` to the repository default branch when no query override is provided."), Hint::new("Use `?source=&target=` on the new-pull path to preselect branch choices."), Hint::new("Use the pulls list path as the cancel destination for this form."), ]; markdown.push_str(&presentation::render_actions_section(&actions)); markdown.push_str(&presentation::render_hints_section(&hints)); markdown }