1use crate::client::GitHubClient;
6use miyabi_types::error::{MiyabiError, Result};
7use miyabi_types::issue::{Issue, IssueState, IssueStateGithub};
8use octocrab::models::issues::Issue as OctoIssue;
9use octocrab::params::State;
10
11impl GitHubClient {
12 pub async fn get_issue(&self, number: u64) -> Result<Issue> {
20 let issue = self
21 .client
22 .issues(&self.owner, &self.repo)
23 .get(number)
24 .await
25 .map_err(|e| {
26 MiyabiError::GitHub(format!(
27 "Failed to get issue #{} from {}/{}: {}",
28 number, self.owner, self.repo, e
29 ))
30 })?;
31
32 convert_issue(issue)
33 }
34
35 pub async fn list_issues(
44 &self,
45 state: Option<State>,
46 labels: Vec<String>,
47 ) -> Result<Vec<Issue>> {
48 let issues = self.client.issues(&self.owner, &self.repo);
49 let mut handler = issues.list();
50
51 if let Some(s) = state {
53 handler = handler.state(s);
54 }
55
56 if !labels.is_empty() {
57 handler = handler.labels(&labels);
58 }
59
60 let page = handler.send().await.map_err(|e| {
61 MiyabiError::GitHub(format!(
62 "Failed to list issues for {}/{}: {}",
63 self.owner, self.repo, e
64 ))
65 })?;
66
67 page.items.into_iter().map(convert_issue).collect()
68 }
69
70 pub async fn create_issue(&self, title: &str, body: Option<&str>) -> Result<Issue> {
79 let issues = self.client.issues(&self.owner, &self.repo);
80 let mut handler = issues.create(title);
81
82 if let Some(b) = body {
83 handler = handler.body(b);
84 }
85
86 let issue = handler.send().await.map_err(|e| {
87 MiyabiError::GitHub(format!(
88 "Failed to create issue in {}/{}: {}",
89 self.owner, self.repo, e
90 ))
91 })?;
92
93 convert_issue(issue)
94 }
95
96 pub async fn update_issue(
107 &self,
108 number: u64,
109 title: Option<&str>,
110 body: Option<&str>,
111 state: Option<State>,
112 ) -> Result<Issue> {
113 use octocrab::models::IssueState as OctoState;
114
115 let issues = self.client.issues(&self.owner, &self.repo);
116 let mut handler = issues.update(number);
117
118 if let Some(t) = title {
119 handler = handler.title(t);
120 }
121
122 if let Some(b) = body {
123 handler = handler.body(b);
124 }
125
126 if let Some(s) = state {
127 let issue_state = match s {
128 State::Open => OctoState::Open,
129 State::Closed => OctoState::Closed,
130 State::All => {
131 return Err(MiyabiError::GitHub(
132 "Cannot update issue to 'All' state".to_string(),
133 ))
134 }
135 _ => return Err(MiyabiError::GitHub(format!("Unknown state: {:?}", s))),
136 };
137 handler = handler.state(issue_state);
138 }
139
140 let issue = handler.send().await.map_err(|e| {
141 MiyabiError::GitHub(format!(
142 "Failed to update issue #{} in {}/{}: {}",
143 number, self.owner, self.repo, e
144 ))
145 })?;
146
147 convert_issue(issue)
148 }
149
150 pub async fn close_issue(&self, number: u64) -> Result<Issue> {
152 self.update_issue(number, None, None, Some(State::Closed))
153 .await
154 }
155
156 pub async fn reopen_issue(&self, number: u64) -> Result<Issue> {
158 self.update_issue(number, None, None, Some(State::Open))
159 .await
160 }
161
162 pub async fn add_labels(&self, number: u64, labels: &[String]) -> Result<Vec<String>> {
168 let labels_result = self
169 .client
170 .issues(&self.owner, &self.repo)
171 .add_labels(number, labels)
172 .await
173 .map_err(|e| {
174 MiyabiError::GitHub(format!(
175 "Failed to add labels to issue #{} in {}/{}: {}",
176 number, self.owner, self.repo, e
177 ))
178 })?;
179
180 Ok(labels_result.into_iter().map(|l| l.name).collect())
181 }
182
183 pub async fn remove_label(&self, number: u64, label: &str) -> Result<()> {
189 self.client
190 .issues(&self.owner, &self.repo)
191 .remove_label(number, label)
192 .await
193 .map_err(|e| {
194 MiyabiError::GitHub(format!(
195 "Failed to remove label '{}' from issue #{} in {}/{}: {}",
196 label, number, self.owner, self.repo, e
197 ))
198 })?;
199
200 Ok(())
201 }
202
203 pub async fn replace_labels(&self, number: u64, labels: &[String]) -> Result<Vec<String>> {
209 let labels_result = self
210 .client
211 .issues(&self.owner, &self.repo)
212 .replace_all_labels(number, labels)
213 .await
214 .map_err(|e| {
215 MiyabiError::GitHub(format!(
216 "Failed to replace labels on issue #{} in {}/{}: {}",
217 number, self.owner, self.repo, e
218 ))
219 })?;
220
221 Ok(labels_result.into_iter().map(|l| l.name).collect())
222 }
223
224 pub async fn get_issues_by_state(&self, state: IssueState) -> Result<Vec<Issue>> {
232 let label = state.to_label().to_string();
233 self.list_issues(Some(State::Open), vec![label]).await
234 }
235}
236
237fn convert_issue(issue: OctoIssue) -> Result<Issue> {
239 use octocrab::models::IssueState as OctoState;
240
241 let state = match issue.state {
242 OctoState::Open => IssueStateGithub::Open,
243 OctoState::Closed => IssueStateGithub::Closed,
244 _ => {
245 return Err(MiyabiError::GitHub(format!(
246 "Unknown issue state: {:?}",
247 issue.state
248 )))
249 }
250 };
251
252 let assignee = issue.assignee.map(|a| a.login);
253
254 let labels = issue
255 .labels
256 .into_iter()
257 .map(|l| l.name)
258 .collect::<Vec<String>>();
259
260 Ok(Issue {
261 number: issue.number,
262 title: issue.title,
263 body: issue.body.unwrap_or_default(),
264 state,
265 labels,
266 assignee,
267 created_at: issue.created_at,
268 updated_at: issue.updated_at,
269 url: issue.html_url.to_string(),
270 })
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
280 fn test_issue_state_conversion() {
281 assert_eq!(IssueState::Pending.to_label(), "📥 state:pending");
283 assert_eq!(IssueState::Analyzing.to_label(), "🔍 state:analyzing");
284 assert_eq!(IssueState::Implementing.to_label(), "🏗️ state:implementing");
285 assert_eq!(IssueState::Reviewing.to_label(), "👀 state:reviewing");
286 assert_eq!(IssueState::Deploying.to_label(), "🚀 state:deploying");
287 assert_eq!(IssueState::Done.to_label(), "✅ state:done");
288 assert_eq!(IssueState::Blocked.to_label(), "🚫 state:blocked");
289 assert_eq!(IssueState::Failed.to_label(), "❌ state:failed");
290 }
291
292 }