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