Skip to main content

ward/github/
pulls.rs

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    /// Create a pull request. Returns the created PR.
23    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        // Check for existing PR from the same branch
33        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        // Request reviewers (best-effort)
61        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    /// Find an open PR from the given branch.
81    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}