1use serde::{Deserialize, Serialize};
6use tokio::process::Command;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PRInfo {
11 pub title: String,
13 pub body: String,
15 pub author: String,
17 pub state: String,
19 pub additions: u32,
21 pub deletions: u32,
23 pub changed_files: u32,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct PRComment {
30 pub author: String,
32 pub body: String,
34 pub created_at: String,
36}
37
38pub async fn get_pr_info(pr_number: u32) -> Option<PRInfo> {
40 let output = Command::new("gh")
41 .args([
42 "pr",
43 "view",
44 &pr_number.to_string(),
45 "--json",
46 "title,body,author,state,additions,deletions,changedFiles",
47 ])
48 .output()
49 .await
50 .ok()?;
51
52 if !output.status.success() {
53 return None;
54 }
55
56 let stdout = String::from_utf8_lossy(&output.stdout);
57
58 #[derive(Deserialize)]
59 struct GhPRInfo {
60 title: String,
61 body: Option<String>,
62 author: Option<GhAuthor>,
63 state: String,
64 additions: u32,
65 deletions: u32,
66 #[serde(rename = "changedFiles")]
67 changed_files: u32,
68 }
69
70 #[derive(Deserialize)]
71 struct GhAuthor {
72 login: String,
73 }
74
75 let data: GhPRInfo = serde_json::from_str(&stdout).ok()?;
76
77 Some(PRInfo {
78 title: data.title,
79 body: data.body.unwrap_or_default(),
80 author: data
81 .author
82 .map(|a| a.login)
83 .unwrap_or_else(|| "unknown".to_string()),
84 state: data.state,
85 additions: data.additions,
86 deletions: data.deletions,
87 changed_files: data.changed_files,
88 })
89}
90
91pub async fn get_pr_comments(pr_number: u32) -> Vec<PRComment> {
93 let output = Command::new("gh")
94 .args(["pr", "view", &pr_number.to_string(), "--json", "comments"])
95 .output()
96 .await;
97
98 let output = match output {
99 Ok(o) if o.status.success() => o,
100 _ => return Vec::new(),
101 };
102
103 let stdout = String::from_utf8_lossy(&output.stdout);
104
105 #[derive(Deserialize)]
106 struct GhComments {
107 comments: Vec<GhComment>,
108 }
109
110 #[derive(Deserialize)]
111 struct GhComment {
112 author: Option<GhAuthor>,
113 body: String,
114 #[serde(rename = "createdAt")]
115 created_at: String,
116 }
117
118 #[derive(Deserialize)]
119 struct GhAuthor {
120 login: String,
121 }
122
123 let data: GhComments = match serde_json::from_str(&stdout) {
124 Ok(d) => d,
125 Err(_) => return Vec::new(),
126 };
127
128 data.comments
129 .into_iter()
130 .map(|c| PRComment {
131 author: c
132 .author
133 .map(|a| a.login)
134 .unwrap_or_else(|| "unknown".to_string()),
135 body: c.body,
136 created_at: c.created_at,
137 })
138 .collect()
139}
140
141pub async fn add_pr_comment(pr_number: u32, body: &str) -> bool {
143 let output = Command::new("gh")
144 .args(["pr", "comment", &pr_number.to_string(), "--body", body])
145 .output()
146 .await;
147
148 output.map(|o| o.status.success()).unwrap_or(false)
149}
150
151#[derive(Debug, Clone, Default)]
153pub struct CreatePROptions {
154 pub title: String,
156 pub body: String,
158 pub base: Option<String>,
160 pub head: Option<String>,
162 pub draft: bool,
164}
165
166#[derive(Debug, Clone)]
168pub struct CreatePRResult {
169 pub success: bool,
171 pub url: Option<String>,
173 pub error: Option<String>,
175}
176
177pub async fn create_pr(options: CreatePROptions) -> CreatePRResult {
179 let mut args = vec![
180 "pr".to_string(),
181 "create".to_string(),
182 "--title".to_string(),
183 options.title,
184 "--body".to_string(),
185 options.body,
186 ];
187
188 if let Some(base) = options.base {
189 args.push("--base".to_string());
190 args.push(base);
191 }
192
193 if let Some(head) = options.head {
194 args.push("--head".to_string());
195 args.push(head);
196 }
197
198 if options.draft {
199 args.push("--draft".to_string());
200 }
201
202 let output = Command::new("gh").args(&args).output().await;
203
204 match output {
205 Ok(o) if o.status.success() => {
206 let url = String::from_utf8_lossy(&o.stdout).trim().to_string();
207 CreatePRResult {
208 success: true,
209 url: Some(url),
210 error: None,
211 }
212 }
213 Ok(o) => {
214 let stderr = String::from_utf8_lossy(&o.stderr).to_string();
215 CreatePRResult {
216 success: false,
217 url: None,
218 error: Some(stderr),
219 }
220 }
221 Err(e) => CreatePRResult {
222 success: false,
223 url: None,
224 error: Some(format!("执行 gh 命令失败: {}", e)),
225 },
226 }
227}