miyabi_github/
pull_requests.rs

1//! GitHub Pull Requests API wrapper
2//!
3//! Provides high-level interface for PR operations
4
5use crate::client::GitHubClient;
6use miyabi_types::error::{MiyabiError, Result};
7use miyabi_types::issue::{PRResult, PRState};
8use octocrab::models::pulls::PullRequest as OctoPR;
9use octocrab::params::pulls::State as PullState;
10use octocrab::params::State;
11
12impl GitHubClient {
13    /// Get a single pull request by number
14    ///
15    /// # Arguments
16    /// * `number` - Pull request number
17    pub async fn get_pull_request(&self, number: u64) -> Result<PRResult> {
18        let pr = self
19            .client
20            .pulls(&self.owner, &self.repo)
21            .get(number)
22            .await
23            .map_err(|e| {
24                MiyabiError::GitHub(format!(
25                    "Failed to get pull request #{} from {}/{}: {}",
26                    number, self.owner, self.repo, e
27                ))
28            })?;
29
30        convert_pull_request(pr)
31    }
32
33    /// List pull requests with optional filtering
34    ///
35    /// # Arguments
36    /// * `state` - Filter by state (Open/Closed/All)
37    pub async fn list_pull_requests(&self, state: Option<State>) -> Result<Vec<PRResult>> {
38        let pulls = self.client.pulls(&self.owner, &self.repo);
39        let mut handler = pulls.list();
40
41        // Apply state filter
42        if let Some(s) = state {
43            handler = handler.state(s);
44        }
45
46        let page = handler.send().await.map_err(|e| {
47            MiyabiError::GitHub(format!(
48                "Failed to list pull requests for {}/{}: {}",
49                self.owner, self.repo, e
50            ))
51        })?;
52
53        page.items.into_iter().map(convert_pull_request).collect()
54    }
55
56    /// Create a pull request
57    ///
58    /// # Arguments
59    /// * `title` - PR title
60    /// * `head` - Branch containing changes
61    /// * `base` - Base branch (e.g., "main")
62    /// * `body` - PR body (optional)
63    /// * `draft` - Create as draft PR
64    pub async fn create_pull_request(
65        &self,
66        title: &str,
67        head: &str,
68        base: &str,
69        body: Option<&str>,
70        draft: bool,
71    ) -> Result<PRResult> {
72        let pulls = self.client.pulls(&self.owner, &self.repo);
73        let mut handler = pulls.create(title, head, base);
74
75        if let Some(b) = body {
76            handler = handler.body(b);
77        }
78
79        handler = handler.draft(draft);
80
81        let pr = handler.send().await.map_err(|e| {
82            MiyabiError::GitHub(format!(
83                "Failed to create pull request in {}/{}: {}",
84                self.owner, self.repo, e
85            ))
86        })?;
87
88        convert_pull_request(pr)
89    }
90
91    /// Update a pull request
92    ///
93    /// # Arguments
94    /// * `number` - PR number to update
95    /// * `title` - New title (optional)
96    /// * `body` - New body (optional)
97    /// * `state` - New state (optional)
98    pub async fn update_pull_request(
99        &self,
100        number: u64,
101        title: Option<&str>,
102        body: Option<&str>,
103        state: Option<PullState>,
104    ) -> Result<PRResult> {
105        let pulls = self.client.pulls(&self.owner, &self.repo);
106        let mut handler = pulls.update(number);
107
108        if let Some(t) = title {
109            handler = handler.title(t);
110        }
111
112        if let Some(b) = body {
113            handler = handler.body(b);
114        }
115
116        if let Some(s) = state {
117            handler = handler.state(s);
118        }
119
120        let pr = handler.send().await.map_err(|e| {
121            MiyabiError::GitHub(format!(
122                "Failed to update pull request #{} in {}/{}: {}",
123                number, self.owner, self.repo, e
124            ))
125        })?;
126
127        convert_pull_request(pr)
128    }
129
130    /// Close a pull request (without merging)
131    pub async fn close_pull_request(&self, number: u64) -> Result<PRResult> {
132        self.update_pull_request(number, None, None, Some(PullState::Closed))
133            .await
134    }
135
136    /// Merge a pull request
137    ///
138    /// # Arguments
139    /// * `number` - PR number to merge
140    /// * `commit_title` - Merge commit title (optional)
141    /// * `commit_message` - Merge commit message (optional)
142    pub async fn merge_pull_request(
143        &self,
144        number: u64,
145        commit_title: Option<&str>,
146        commit_message: Option<&str>,
147    ) -> Result<()> {
148        let pulls = self.client.pulls(&self.owner, &self.repo);
149        let mut handler = pulls.merge(number);
150
151        if let Some(title) = commit_title {
152            handler = handler.title(title);
153        }
154
155        if let Some(message) = commit_message {
156            handler = handler.message(message);
157        }
158
159        handler.send().await.map_err(|e| {
160            MiyabiError::GitHub(format!(
161                "Failed to merge pull request #{} in {}/{}: {}",
162                number, self.owner, self.repo, e
163            ))
164        })?;
165
166        Ok(())
167    }
168
169    /// Check if a pull request is mergeable
170    pub async fn is_mergeable(&self, number: u64) -> Result<bool> {
171        let pr = self.get_pull_request(number).await?;
172
173        // A PR is mergeable if it's in Open state (not Closed or Merged)
174        Ok(matches!(pr.state, PRState::Open | PRState::Draft))
175    }
176}
177
178/// Convert octocrab PullRequest to miyabi-types PRResult
179fn convert_pull_request(pr: OctoPR) -> Result<PRResult> {
180    use octocrab::models::IssueState as OctoState;
181
182    let state = if pr.merged_at.is_some() {
183        PRState::Merged
184    } else if pr.draft.unwrap_or(false) {
185        PRState::Draft
186    } else {
187        match pr.state {
188            Some(OctoState::Open) => PRState::Open,
189            Some(OctoState::Closed) => PRState::Closed,
190            Some(ref s) => {
191                return Err(MiyabiError::GitHub(format!(
192                    "Unknown pull request state: {:?}",
193                    s
194                )))
195            }
196            None => {
197                return Err(MiyabiError::GitHub(
198                    "Pull request state is missing".to_string(),
199                ))
200            }
201        }
202    };
203
204    Ok(PRResult {
205        number: pr.number,
206        url: pr.html_url.map(|u| u.to_string()).unwrap_or_default(),
207        state,
208        created_at: pr
209            .created_at
210            .ok_or_else(|| MiyabiError::GitHub("Pull request created_at is missing".to_string()))?,
211    })
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_pr_state_conversion() {
220        // Test PRState enum (defined in miyabi-types)
221        // Just verify we can use it
222        let _draft = PRState::Draft;
223        let _open = PRState::Open;
224        let _merged = PRState::Merged;
225        let _closed = PRState::Closed;
226    }
227
228    // Integration tests requiring GitHub API are in tests/ directory
229}