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.client.issues(&self.owner, &self.repo).get(number).await.map_err(|e| {
21 MiyabiError::GitHub(format!(
22 "Failed to get issue #{} from {}/{}: {}",
23 number, self.owner, self.repo, e
24 ))
25 })?;
26
27 convert_issue(issue)
28 }
29
30 pub async fn list_issues(
39 &self,
40 state: Option<State>,
41 labels: Vec<String>,
42 ) -> Result<Vec<Issue>> {
43 let issues = self.client.issues(&self.owner, &self.repo);
44 let mut handler = issues.list();
45
46 if let Some(s) = state {
48 handler = handler.state(s);
49 }
50
51 if !labels.is_empty() {
52 handler = handler.labels(&labels);
53 }
54
55 let page = handler.send().await.map_err(|e| {
56 MiyabiError::GitHub(format!(
57 "Failed to list issues for {}/{}: {}",
58 self.owner, self.repo, e
59 ))
60 })?;
61
62 page.items.into_iter().map(convert_issue).collect()
63 }
64
65 pub async fn create_issue(&self, title: &str, body: Option<&str>) -> Result<Issue> {
74 let issues = self.client.issues(&self.owner, &self.repo);
75 let mut handler = issues.create(title);
76
77 if let Some(b) = body {
78 handler = handler.body(b);
79 }
80
81 let issue = handler.send().await.map_err(|e| {
82 MiyabiError::GitHub(format!(
83 "Failed to create issue in {}/{}: {}",
84 self.owner, self.repo, e
85 ))
86 })?;
87
88 convert_issue(issue)
89 }
90
91 pub async fn update_issue(
102 &self,
103 number: u64,
104 title: Option<&str>,
105 body: Option<&str>,
106 state: Option<State>,
107 ) -> Result<Issue> {
108 use octocrab::models::IssueState as OctoState;
109
110 let issues = self.client.issues(&self.owner, &self.repo);
111 let mut handler = issues.update(number);
112
113 if let Some(t) = title {
114 handler = handler.title(t);
115 }
116
117 if let Some(b) = body {
118 handler = handler.body(b);
119 }
120
121 if let Some(s) = state {
122 let issue_state = match s {
123 State::Open => OctoState::Open,
124 State::Closed => OctoState::Closed,
125 State::All => {
126 return Err(MiyabiError::GitHub(
127 "Cannot update issue to 'All' state".to_string(),
128 ))
129 },
130 _ => return Err(MiyabiError::GitHub(format!("Unknown state: {:?}", s))),
131 };
132 handler = handler.state(issue_state);
133 }
134
135 let issue = handler.send().await.map_err(|e| {
136 MiyabiError::GitHub(format!(
137 "Failed to update issue #{} in {}/{}: {}",
138 number, self.owner, self.repo, e
139 ))
140 })?;
141
142 convert_issue(issue)
143 }
144
145 pub async fn close_issue(&self, number: u64) -> Result<Issue> {
147 self.update_issue(number, None, None, Some(State::Closed)).await
148 }
149
150 pub async fn reopen_issue(&self, number: u64) -> Result<Issue> {
152 self.update_issue(number, None, None, Some(State::Open)).await
153 }
154
155 pub async fn add_labels(&self, number: u64, labels: &[String]) -> Result<Vec<String>> {
161 let labels_result = self
162 .client
163 .issues(&self.owner, &self.repo)
164 .add_labels(number, labels)
165 .await
166 .map_err(|e| {
167 MiyabiError::GitHub(format!(
168 "Failed to add labels to issue #{} in {}/{}: {}",
169 number, self.owner, self.repo, e
170 ))
171 })?;
172
173 Ok(labels_result.into_iter().map(|l| l.name).collect())
174 }
175
176 pub async fn remove_label(&self, number: u64, label: &str) -> Result<()> {
182 self.client
183 .issues(&self.owner, &self.repo)
184 .remove_label(number, label)
185 .await
186 .map_err(|e| {
187 MiyabiError::GitHub(format!(
188 "Failed to remove label '{}' from issue #{} in {}/{}: {}",
189 label, number, self.owner, self.repo, e
190 ))
191 })?;
192
193 Ok(())
194 }
195
196 pub async fn replace_labels(&self, number: u64, labels: &[String]) -> Result<Vec<String>> {
202 let labels_result = self
203 .client
204 .issues(&self.owner, &self.repo)
205 .replace_all_labels(number, labels)
206 .await
207 .map_err(|e| {
208 MiyabiError::GitHub(format!(
209 "Failed to replace labels on issue #{} in {}/{}: {}",
210 number, self.owner, self.repo, e
211 ))
212 })?;
213
214 Ok(labels_result.into_iter().map(|l| l.name).collect())
215 }
216
217 pub async fn get_issues_by_state(&self, state: IssueState) -> Result<Vec<Issue>> {
225 let label = state.to_label().to_string();
226 self.list_issues(Some(State::Open), vec![label]).await
227 }
228}
229
230fn convert_issue(issue: OctoIssue) -> Result<Issue> {
232 use octocrab::models::IssueState as OctoState;
233
234 let state = match issue.state {
235 OctoState::Open => IssueStateGithub::Open,
236 OctoState::Closed => IssueStateGithub::Closed,
237 _ => return Err(MiyabiError::GitHub(format!("Unknown issue state: {:?}", issue.state))),
238 };
239
240 let assignee = issue.assignee.map(|a| a.login);
241
242 let labels = issue.labels.into_iter().map(|l| l.name).collect::<Vec<String>>();
243
244 Ok(Issue {
245 number: issue.number,
246 title: issue.title,
247 body: issue.body.unwrap_or_default(),
248 state,
249 labels,
250 assignee,
251 created_at: issue.created_at,
252 updated_at: issue.updated_at,
253 url: issue.html_url.to_string(),
254 })
255}
256
257#[cfg(test)]
258mod tests {
259 use super::*;
260
261 #[test]
264 fn test_issue_state_conversion() {
265 assert_eq!(IssueState::Pending.to_label(), "📥 state:pending");
267 assert_eq!(IssueState::Analyzing.to_label(), "🔍 state:analyzing");
268 assert_eq!(IssueState::Implementing.to_label(), "🏗️ state:implementing");
269 assert_eq!(IssueState::Reviewing.to_label(), "👀 state:reviewing");
270 assert_eq!(IssueState::Deploying.to_label(), "🚀 state:deploying");
271 assert_eq!(IssueState::Done.to_label(), "✅ state:done");
272 assert_eq!(IssueState::Blocked.to_label(), "🚫 state:blocked");
273 assert_eq!(IssueState::Failed.to_label(), "❌ state:failed");
274 }
275
276 }