1use anyhow::{Context, Result};
2use serde::Deserialize;
3
4use super::Client;
5
6#[derive(Debug, Clone, Deserialize)]
7pub struct PullRequest {
8 pub number: u64,
9 pub html_url: String,
10 pub state: String,
11 pub title: String,
12 pub head: PullRequestHead,
13}
14
15#[derive(Debug, Clone, Deserialize)]
16pub struct PullRequestHead {
17 #[serde(rename = "ref")]
18 pub branch: String,
19}
20
21impl Client {
22 pub async fn create_pull_request(
24 &self,
25 repo: &str,
26 title: &str,
27 body: &str,
28 head: &str,
29 base: &str,
30 reviewers: &[String],
31 ) -> Result<PullRequest> {
32 if let Some(existing) = self.find_open_pr(repo, head).await? {
34 tracing::info!(
35 "PR already exists for {head} in {repo}: {}",
36 existing.html_url
37 );
38 return Ok(existing);
39 }
40
41 let pr_body = serde_json::json!({
42 "title": title,
43 "body": body,
44 "head": head,
45 "base": base,
46 });
47
48 let resp = self
49 .post_json(&format!("/repos/{}/{repo}/pulls", self.org), &pr_body)
50 .await?;
51
52 let status = resp.status();
53 if !status.is_success() {
54 let body = resp.text().await.unwrap_or_default();
55 anyhow::bail!("Failed to create PR in {repo} (HTTP {status}): {body}");
56 }
57
58 let pr: PullRequest = resp.json().await.context("Failed to parse PR response")?;
59
60 if !reviewers.is_empty() {
62 let review_body = serde_json::json!({
63 "reviewers": reviewers,
64 });
65
66 let _ = self
67 .post_json(
68 &format!(
69 "/repos/{}/{repo}/pulls/{}/requested_reviewers",
70 self.org, pr.number
71 ),
72 &review_body,
73 )
74 .await;
75 }
76
77 Ok(pr)
78 }
79
80 async fn find_open_pr(&self, repo: &str, head_branch: &str) -> Result<Option<PullRequest>> {
82 let resp = self
83 .get(&format!(
84 "/repos/{org}/{repo}/pulls?state=open&head={org}:{head_branch}",
85 org = self.org,
86 ))
87 .await?;
88
89 if !resp.status().is_success() {
90 return Ok(None);
91 }
92
93 let prs: Vec<PullRequest> = resp.json().await.unwrap_or_default();
94 Ok(prs.into_iter().next())
95 }
96}