use super::{helpers::*, *}; pub fn issue_page(details: IssueDetails) -> IssuePage { IssuePage { title: details .issue .title .clone() .unwrap_or_else(|| "Untitled issue".into()), state: work_item_state(details.issue.state.as_deref()), meta: issue_meta(&details.issue), milestone: issue_milestone(&details.issue), labels: details .issue .labels .as_deref() .unwrap_or_default() .iter() .filter_map(label_row) .collect(), body: details .issue .body .filter(|body| !body.is_empty()) .unwrap_or_else(|| "No description provided.".into()), comments: details .comments .iter() .map(|comment| comment_row(comment, details.viewer_id)) .collect(), has_more: details.has_more, } } pub fn issue_editor_page(data: IssueEditorData) -> IssueEditorPage { let selected_labels: std::collections::BTreeSet<_> = data .issue .as_ref() .and_then(|issue| issue.labels.as_deref()) .unwrap_or_default() .iter() .filter_map(|label| label.id) .collect(); let selected_milestone = data .issue .as_ref() .and_then(|issue| issue.milestone.as_ref()) .and_then(|milestone| milestone.id); let mut labels: Vec<_> = data .labels .into_iter() .filter_map(|label| { let id = label.id?; Some(IssueEditorLabel { id, name: label.name?, selected: selected_labels.contains(&id), }) }) .collect(); labels.sort_by_key(|label| label.name.to_lowercase()); let mut milestones: Vec<_> = data .milestones .into_iter() .filter(|milestone| { milestone.state.as_deref() != Some("closed") || milestone.id == selected_milestone }) .filter_map(|milestone| { let id = milestone.id?; Some(IssueEditorMilestone { id, title: milestone.title?, selected: Some(id) == selected_milestone, }) }) .collect(); milestones.sort_by_key(|milestone| milestone.title.to_lowercase()); IssueEditorPage { title: data .issue .as_ref() .and_then(|issue| issue.title.clone()) .unwrap_or_default(), body: data .issue .as_ref() .and_then(|issue| issue.body.clone()) .unwrap_or_default(), due_date: data .issue .as_ref() .and_then(|issue| issue.due_date.as_deref()) .and_then(parse_api_date), closed: data.issue.as_ref().and_then(|issue| issue.state.as_deref()) == Some("closed"), labels, milestones, } } pub fn pull_page(details: PullDetails) -> PullPage { let pull = &details.pull; let author = pull .user .as_ref() .and_then(|user| user.login.as_deref()) .unwrap_or("unknown"); let head = pull .head .as_ref() .and_then(|branch| branch.label.as_deref().or(branch.r#ref.as_deref())) .unwrap_or("head"); let base = pull .base .as_ref() .and_then(|branch| branch.label.as_deref().or(branch.r#ref.as_deref())) .unwrap_or("base"); let state = pull_state(pull); PullPage { title: pull .title .clone() .unwrap_or_else(|| "Untitled pull request".into()), state: work_item_state(pull.state.as_deref()), meta: format!( "#{} · {state} · {author} · {head} → {base}\n{} files · +{} −{} · {} comments", pull.number.unwrap_or_default(), pull.changed_files.unwrap_or_default(), pull.additions.unwrap_or_default(), pull.deletions.unwrap_or_default(), pull.comments.unwrap_or_default(), ), body: pull .body .clone() .filter(|body| !body.is_empty()) .unwrap_or_else(|| "No description provided.".into()), files_ref: pull_files_ref(pull), files: details.files.iter().filter_map(file_row).collect(), comments: details .comments .iter() .map(|comment| comment_row(comment, None)) .collect(), has_more: details.has_more, } } pub fn file_rows(files: Vec) -> Vec { files.iter().filter_map(file_row).collect() } pub fn commit_file_rows(files: Vec) -> Vec { files .into_iter() .filter_map(|file| { Some(FileRow { path: file.filename?, status: file.status.unwrap_or_else(|| "modified".into()), }) }) .collect() } pub fn commit_details_page(commit: models::Commit, branch: Option) -> CommitDetailsPage { let details = commit.commit.as_deref(); let message = details .and_then(|details| details.message.as_deref()) .unwrap_or("Commit"); let mut lines = message.lines(); let title = lines.next().unwrap_or("Commit").to_string(); let description = lines.collect::>().join("\n").trim().to_string(); let author = details .and_then(|details| details.author.as_deref()) .and_then(commit_user) .or_else(|| { commit .author .as_deref() .and_then(|author| author.login.clone()) }); let committer = details .and_then(|details| details.committer.as_deref()) .and_then(commit_user) .or_else(|| { commit .committer .as_deref() .and_then(|committer| committer.login.clone()) }); let date = details .and_then(|details| details.committer.as_deref()) .and_then(|committer| committer.date.as_deref()) .or_else(|| { details .and_then(|details| details.author.as_deref()) .and_then(|author| author.date.as_deref()) }) .or(commit.created.as_deref()); let files = commit.files.unwrap_or_default(); let mut metadata = Vec::new(); if let Some(author) = author { metadata.push(metadata_row("Author", author, false)); } if let Some(committer) = committer { metadata.push(metadata_row("Committer", committer, false)); } if date.is_some() { metadata.push(metadata_row("Committed", compact_date(date), false)); } if let Some(branch) = branch.filter(|branch| !branch.is_empty()) { metadata.push(metadata_row("Branch", branch, false)); } metadata.push(metadata_row( "Commit", commit.sha.clone().unwrap_or_else(|| "unknown".into()), true, )); let verification = details.and_then(|details| details.verification.as_deref()); metadata.push(metadata_row( "Signature", match verification.filter(|verification| { verification .signature .as_deref() .is_some_and(|signature| !signature.is_empty()) }) { Some(verification) if verification.verified.unwrap_or(false) => "Verified".into(), Some(_) => "Unverified".into(), None => "Unsigned".into(), }, false, )); CommitDetailsPage { title, description, metadata, files: commit_file_rows(files), } } fn commit_user(user: &models::CommitUser) -> Option { match (user.name.as_deref(), user.email.as_deref()) { (Some(name), Some(email)) => Some(format!("{name} <{email}>")), (Some(name), None) => Some(name.into()), (None, Some(email)) => Some(email.into()), (None, None) => None, } } fn metadata_row(label: &str, value: String, monospaced: bool) -> CommitMetadataRow { CommitMetadataRow { label: label.into(), value, monospaced, } }