use crate::schema::schema;
use crate::api::client::{ApiError, GitHub};
use crate::api::model::{Capped, NodeId, RepoId};
use crate::git::local::CommitMessage;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewForm {
pub repo_id: NodeId,
pub default_branch: Option<String>,
pub can_create_issues: bool,
pub has_issues: bool,
pub archived: bool,
pub can_triage: bool,
pub labels: Capped<Choice>,
pub assignees: Capped<Choice>,
pub milestones: Capped<Choice>,
pub issue_templates: Vec<(String, String)>,
pub pr_template: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Choice {
pub id: NodeId,
pub name: String,
pub about: String,
pub color: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewIssue {
pub repo: NodeId,
pub title: String,
pub body: String,
pub labels: Vec<NodeId>,
pub assignees: Vec<NodeId>,
pub milestone: Option<NodeId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NewPr {
pub repo: NodeId,
pub base: String,
pub head: String,
pub title: String,
pub body: String,
pub draft: bool,
}
mod wire {
use serde::Deserialize;
pub use crate::api::browse::wire::{Counted, Name};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Repo {
pub id: String,
pub viewer_can_create_issues: bool,
pub has_issues_enabled: bool,
pub is_archived: bool,
pub viewer_permission: Option<String>,
pub default_branch_ref: Option<Name>,
pub labels: Option<Counted<Label>>,
pub assignable_users: Counted<User>,
pub milestones: Option<Counted<Milestone>>,
pub issue_templates: Option<Tree>,
pub pull_request_templates: Option<Vec<Template>>,
}
#[derive(Deserialize)]
pub struct Label {
pub id: String,
pub name: String,
pub color: String,
pub description: Option<String>,
}
#[derive(Deserialize)]
pub struct User {
pub id: String,
pub login: String,
pub name: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Milestone {
pub id: String,
pub title: String,
pub due_on: Option<String>,
}
#[derive(Deserialize)]
pub struct Tree {
pub entries: Option<Vec<Entry>>,
}
#[derive(Deserialize)]
pub struct Entry {
pub name: String,
pub object: Option<Blob>,
}
#[derive(Deserialize)]
pub struct Blob {
pub text: Option<String>,
}
#[derive(Deserialize)]
pub struct Template {
pub filename: Option<String>,
pub body: Option<String>,
}
#[derive(Deserialize)]
pub struct Compare {
pub total_commits: u64,
pub commits: Vec<Commit>,
}
#[derive(Deserialize)]
pub struct Commit {
pub sha: String,
pub commit: Message,
}
#[derive(Deserialize)]
pub struct Message {
pub message: String,
}
}
fn capped<T, U>(c: Option<wire::Counted<T>>, mut f: impl FnMut(T) -> U) -> Capped<U> {
c.map_or_else(Capped::default, |c| c.into_capped(|t| Some(f(t))))
}
impl GitHub {
pub async fn new_form(&self, repo: &RepoId) -> Result<NewForm, ApiError> {
let vars = serde_json::json!({ "owner": repo.owner, "name": repo.name });
let r: wire::Repo =
(self.graphql_json(crate::api::raw::NEW_FORM, vars, repo, "/repository")).await?;
let can_triage = matches!(
r.viewer_permission.as_deref(),
Some("ADMIN" | "MAINTAIN" | "WRITE" | "TRIAGE")
);
let entries = r
.issue_templates
.and_then(|t| t.entries)
.unwrap_or_default();
let issue_templates = (entries.into_iter())
.filter_map(|e| Some((e.name, e.object?.text?)))
.collect();
let pr_template = (r.pull_request_templates.unwrap_or_default().into_iter())
.filter(|t| t.filename.is_some())
.find_map(|t| t.body);
Ok(NewForm {
repo_id: NodeId::new(r.id),
default_branch: r.default_branch_ref.map(|b| b.name),
can_create_issues: r.viewer_can_create_issues,
has_issues: r.has_issues_enabled,
archived: r.is_archived,
can_triage,
labels: capped(r.labels, |l| Choice {
id: NodeId::new(l.id),
name: l.name,
about: l.description.unwrap_or_default(),
color: Some(l.color),
}),
assignees: capped(Some(r.assignable_users), |u| Choice {
id: NodeId::new(u.id),
name: u.login,
about: u.name.unwrap_or_default(),
color: None,
}),
milestones: capped(r.milestones, |m| Choice {
id: NodeId::new(m.id),
name: m.title,
about: (m.due_on.as_deref())
.and_then(|d| d.get(..10))
.map(|d| format!("due {d}"))
.unwrap_or_default(),
color: None,
}),
issue_templates,
pr_template,
})
}
pub async fn commit_messages(
&self,
repo: &RepoId,
base: &str,
head: &str,
) -> Result<Capped<CommitMessage>, ApiError> {
use crate::ui::pages::url::encode_path;
let head = match head.split_once(':') {
Some((owner, branch)) => format!("{}:{}", encode_path(owner), encode_path(branch)),
None => encode_path(head),
};
let path = format!(
"/repos/{}/{}/compare/{}...{head}",
repo.owner,
repo.name,
encode_path(base)
);
let what = format!("{repo}'s comparison {base}...{head}");
let wire: wire::Compare = serde_json::from_str(&self.rest_get(&path, what).await?)?;
let commits = (wire.commits.into_iter())
.map(|c| CommitMessage {
oid: c.sha,
message: c.commit.message,
})
.collect();
Ok(Capped::new(commits, wire.total_commits))
}
pub async fn create_issue(&self, new: &NewIssue) -> Result<u64, ApiError> {
use cynic::MutationBuilder;
let gql = |ids: &[NodeId]| Some(ids.iter().map(NodeId::gql).collect());
let op = CreateIssue::build(CreateIssueVariables {
repo: new.repo.gql(),
title: new.title.clone(),
body: new.body.clone(),
labels: gql(&new.labels),
assignees: gql(&new.assignees),
milestone: new.milestone.as_ref().map(NodeId::gql),
});
let made = self.mutate(op).await?;
let issue = made.create_issue.and_then(|p| p.issue);
number(issue.map(|i| i.number), "issue")
}
pub async fn create_pull_request(&self, new: &NewPr) -> Result<u64, ApiError> {
use cynic::MutationBuilder;
let op = CreatePr::build(CreatePrVariables {
repo: new.repo.gql(),
base: new.base.clone(),
head: new.head.clone(),
title: new.title.clone(),
body: new.body.clone(),
draft: new.draft,
});
let made = self.mutate(op).await?;
let pr = made.create_pull_request.and_then(|p| p.pull_request);
number(pr.map(|p| p.number), "pull request")
}
}
fn number(n: Option<i32>, what: &str) -> Result<u64, ApiError> {
let n = n.ok_or_else(|| ApiError::Decode(format!("GitHub didn't say which {what} it made")))?;
u64::try_from(n).map_err(|_| ApiError::Decode(format!("a {what} numbered {n}")))
}
#[derive(cynic::QueryVariables, Debug)]
pub struct CreateIssueVariables {
pub repo: cynic::Id,
pub title: String,
pub body: String,
pub labels: Option<Vec<cynic::Id>>,
pub assignees: Option<Vec<cynic::Id>>,
pub milestone: Option<cynic::Id>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "CreateIssueVariables")]
pub struct CreateIssue {
#[arguments(input: { repositoryId: $repo, title: $title, body: $body, labelIds: $labels, assigneeIds: $assignees, milestoneId: $milestone })]
pub create_issue: Option<CreateIssuePayload>,
}
#[derive(cynic::QueryFragment, Debug)]
pub struct CreateIssuePayload {
pub issue: Option<Numbered>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Issue")]
pub struct Numbered {
pub number: i32,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct CreatePrVariables {
pub repo: cynic::Id,
pub base: String,
pub head: String,
pub title: String,
pub body: String,
pub draft: bool,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = "CreatePrVariables")]
pub struct CreatePr {
#[arguments(input: { repositoryId: $repo, baseRefName: $base, headRefName: $head, title: $title, body: $body, draft: $draft })]
pub create_pull_request: Option<CreatePrPayload>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "CreatePullRequestPayload")]
pub struct CreatePrPayload {
pub pull_request: Option<NumberedPr>,
}
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "PullRequest")]
pub struct NumberedPr {
pub number: i32,
}