use crate::schema::schema;
use crate::api::client::{ApiError, GitHub};
use crate::api::model::{NodeId, RepoId};
use crate::api::notifications::ThreadAt;
use crate::api::queries::{
AddCommentPayload, AutoMergePayload, CloseIssuePayload, ClosePrPayload, DequeuePayload,
EnqueuePayload, GitObjectId, MergePayload, NoAutoMergePayload, ReadyPayload,
ReopenIssuePayload, ReopenPrPayload, StarPayload, UnstarPayload, UpdateBranchPayload,
};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Change {
Comment {
subject: NodeId,
body: String,
},
CommitComment {
repo: RepoId,
oid: String,
body: String,
},
Reply {
thread: NodeId,
body: String,
},
Star {
repo: RepoId,
id: NodeId,
starred: bool,
},
Merge {
pr: NodeId,
method: MergeMethod,
head: String,
},
EnableAutoMerge {
pr: NodeId,
method: MergeMethod,
head: String,
},
DisableAutoMerge {
pr: NodeId,
},
Enqueue {
pr: NodeId,
head: String,
},
Dequeue {
pr: NodeId,
},
SetPrOpen {
pr: NodeId,
open: bool,
},
CloseIssue {
issue: NodeId,
reason: CloseReason,
},
ReopenIssue {
issue: NodeId,
},
ReadyForReview {
pr: NodeId,
},
UpdateBranch {
pr: NodeId,
rebase: bool,
head: String,
},
Rerun {
repo: RepoId,
run: u64,
failed_only: bool,
},
RerunJob {
repo: RepoId,
job: u64,
},
CancelRun {
repo: RepoId,
run: u64,
},
MarkRead {
thread: String,
},
MarkDone {
thread: ThreadAt,
},
MarkAllRead {
repo: Option<RepoId>,
last_read_at: String,
},
MarkEach {
threads: Vec<ThreadAt>,
done: bool,
},
Edit(crate::api::edit::Edit),
Discussion(crate::api::discussion::Discuss),
Project(crate::api::project::ProjectChange),
SetProjectField {
project: NodeId,
item: NodeId,
field: NodeId,
value: Option<crate::api::project::FieldValue>,
said: String,
},
Alert(crate::api::security::AlertChange),
Manage(crate::api::manage::Manage),
Settings(crate::api::settings::SettingsChange),
}
#[derive(cynic::Enum, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cynic(graphql_type = "PullRequestMergeMethod")]
pub enum MergeMethod {
Merge,
Squash,
Rebase,
}
impl MergeMethod {
pub fn describe(self) -> &'static str {
match self {
Self::Merge => "Create a merge commit",
Self::Squash => "Squash and merge",
Self::Rebase => "Rebase and merge",
}
}
}
#[derive(cynic::Enum, Debug, Clone, Copy, PartialEq, Eq)]
#[cynic(graphql_type = "IssueClosedStateReason")]
pub enum CloseReason {
Completed,
NotPlanned,
Duplicate,
}
#[derive(cynic::Enum, Debug, Clone, Copy, PartialEq, Eq)]
#[cynic(graphql_type = "PullRequestBranchUpdateMethod")]
pub enum UpdateMethod {
Merge,
Rebase,
}
impl GitHub {
pub async fn change(&self, change: &Change) -> Result<(), ApiError> {
match change {
Change::Comment { subject, body } => {
self.run::<AddComment, _>(AddCommentVariables {
subject: subject.gql(),
body: body.clone(),
})
.await
}
Change::CommitComment { repo, oid, body } => {
self.comment_on_commit(repo, oid, body).await
}
Change::Reply { thread, body } => {
self.run::<crate::api::queries::Reply, _>(crate::api::queries::ReplyVariables {
thread_id: thread.gql(),
body: body.clone(),
})
.await
}
Change::Star {
id, starred: true, ..
} => self.run_on::<AddStar>(id).await,
Change::Star { id, .. } => self.run_on::<RemoveStar>(id).await,
Change::Merge { pr, method, head } | Change::EnableAutoMerge { pr, method, head } => {
let vars = MergeVariables {
pr: pr.gql(),
method: *method,
head: GitObjectId(head.clone()),
};
if let Change::Merge { .. } = change {
self.run::<MergePr, _>(vars).await
} else {
self.run::<EnableAutoMerge, _>(vars).await
}
}
Change::DisableAutoMerge { pr } => self.run_on::<DisableAutoMerge>(pr).await,
Change::Enqueue { pr, head } => {
self.run::<Enqueue, _>(EnqueueVariables {
pr: pr.gql(),
head: GitObjectId(head.clone()),
})
.await
}
Change::Dequeue { pr } => self.run_on::<Dequeue>(pr).await,
Change::SetPrOpen { pr, open: true } => self.run_on::<ReopenPr>(pr).await,
Change::SetPrOpen { pr, .. } => self.run_on::<ClosePr>(pr).await,
Change::CloseIssue { issue, reason } => {
self.run::<CloseIssue, _>(CloseIssueVariables {
issue: issue.gql(),
reason: *reason,
})
.await
}
Change::ReopenIssue { issue } => self.run_on::<ReopenIssue>(issue).await,
Change::ReadyForReview { pr } => self.run_on::<MarkReady>(pr).await,
Change::UpdateBranch { pr, rebase, head } => {
self.run::<UpdateBranch, _>(UpdateBranchVariables {
pr: pr.gql(),
method: if *rebase {
UpdateMethod::Rebase
} else {
UpdateMethod::Merge
},
head: GitObjectId(head.clone()),
})
.await
}
Change::Rerun {
repo,
run,
failed_only,
} => {
let what = if *failed_only {
"rerun-failed-jobs"
} else {
"rerun"
};
self.post(&format!("/repos/{repo}/actions/runs/{run}/{what}"))
.await
}
Change::RerunJob { repo, job } => {
self.post(&format!("/repos/{repo}/actions/jobs/{job}/rerun"))
.await
}
Change::CancelRun { repo, run } => {
self.post(&format!("/repos/{repo}/actions/runs/{run}/cancel"))
.await
}
Change::MarkRead { thread } => self.mark(thread, false).await,
Change::MarkDone { thread } => {
self.mark(&thread.id, true).await?;
self.remember_done(std::slice::from_ref(thread)).await;
Ok(())
}
Change::MarkAllRead { repo, last_read_at } => {
let path = match repo {
Some(repo) => format!("/repos/{repo}/notifications"),
None => "/notifications".to_owned(),
};
let body = serde_json::json!({ "last_read_at": last_read_at, "read": true });
self.rest_write(http::Method::PUT, &path, Some(&body)).await
}
Change::MarkEach { threads, done } => {
let mut marked = 0;
let mut sent = Ok(());
for thread in threads {
if let Err(err) = self.mark(&thread.id, *done).await {
sent = Err(match err {
ApiError::Http { status, message } if marked > 0 => ApiError::Http {
status,
message: format!("{message} (after {marked} of {})", threads.len()),
},
err => err,
});
break;
}
marked += 1;
}
if *done {
self.remember_done(threads.get(..marked).unwrap_or_default())
.await;
}
sent
}
Change::Edit(edit) => self.edit(edit).await,
Change::Discussion(change) => self.discuss(change).await,
Change::Project(change) => self.change_project(change).await,
Change::SetProjectField {
project,
item,
field,
value,
..
} => {
self.set_project_field(project, item, field, value.as_ref())
.await
}
Change::Alert(change) => self.set_alert(change).await,
Change::Manage(change) => self.manage(change).await,
Change::Settings(change) => self.change_setting(change).await,
}
}
}
impl GitHub {
async fn mark(&self, thread: &str, done: bool) -> Result<(), ApiError> {
let path = format!("/notifications/threads/{thread}");
let method = if done {
http::Method::DELETE
} else {
http::Method::PATCH
};
self.rest_write(method, &path, None).await
}
async fn post(&self, path: &str) -> Result<(), ApiError> {
self.rest_write(http::Method::POST, path, None).await
}
pub(crate) async fn run<M, V>(&self, vars: V) -> Result<(), ApiError>
where
M: cynic::MutationBuilder<V> + serde::de::DeserializeOwned,
V: serde::Serialize,
{
self.mutate(M::build(vars)).await.map(drop)
}
pub(crate) async fn run_on<M>(&self, id: &NodeId) -> Result<(), ApiError>
where
M: cynic::MutationBuilder<IdVariables> + serde::de::DeserializeOwned,
{
self.run::<M, _>(IdVariables { id: id.gql() }).await
}
}
#[derive(cynic::QueryVariables, Debug)]
pub struct IdVariables {
pub id: cynic::Id,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct AddCommentVariables {
pub subject: cynic::Id,
pub body: String,
}
macro_rules! mutations {
($($(#[$doc:meta])* $name:ident($vars:literal) $field:ident: $payload:ident { $($input:tt)* })*) => {
$(
$(#[$doc])*
#[derive(cynic::QueryFragment, Debug)]
#[cynic(graphql_type = "Mutation", variables = $vars)]
pub struct $name {
#[arguments(input: { $($input)* })]
pub $field: Option<$payload>,
}
)*
#[cfg(test)]
pub(crate) const MUTATIONS: usize = [$(stringify!($name)),*].len();
};
}
pub(crate) use mutations;
mutations! {
AddComment("AddCommentVariables") add_comment: AddCommentPayload { subjectId: $subject, body: $body }
AddStar("IdVariables") add_star: StarPayload { starrableId: $id }
RemoveStar("IdVariables") remove_star: UnstarPayload { starrableId: $id }
MergePr("MergeVariables") merge_pull_request: MergePayload { pullRequestId: $pr, mergeMethod: $method, expectedHeadOid: $head }
EnableAutoMerge("MergeVariables") enable_pull_request_auto_merge: AutoMergePayload { pullRequestId: $pr, mergeMethod: $method, expectedHeadOid: $head }
DisableAutoMerge("IdVariables") disable_pull_request_auto_merge: NoAutoMergePayload { pullRequestId: $id }
Enqueue("EnqueueVariables") enqueue_pull_request: EnqueuePayload { pullRequestId: $pr, expectedHeadOid: $head }
Dequeue("IdVariables") dequeue_pull_request: DequeuePayload { id: $id }
ClosePr("IdVariables") close_pull_request: ClosePrPayload { pullRequestId: $id }
ReopenPr("IdVariables") reopen_pull_request: ReopenPrPayload { pullRequestId: $id }
CloseIssue("CloseIssueVariables") close_issue: CloseIssuePayload { issueId: $issue, stateReason: $reason }
ReopenIssue("IdVariables") reopen_issue: ReopenIssuePayload { issueId: $id }
MarkReady("IdVariables") mark_pull_request_ready_for_review: ReadyPayload { pullRequestId: $id }
UpdateBranch("UpdateBranchVariables") update_pull_request_branch: UpdateBranchPayload { pullRequestId: $pr, updateMethod: $method, expectedHeadOid: $head }
}
#[derive(cynic::QueryVariables, Debug)]
pub struct MergeVariables {
pub pr: cynic::Id,
pub method: MergeMethod,
pub head: GitObjectId,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct EnqueueVariables {
pub pr: cynic::Id,
pub head: GitObjectId,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct CloseIssueVariables {
pub issue: cynic::Id,
pub reason: CloseReason,
}
#[derive(cynic::QueryVariables, Debug)]
pub struct UpdateBranchVariables {
pub pr: cynic::Id,
pub method: UpdateMethod,
pub head: GitObjectId,
}