Skip to main content

aster/github/
pr.rs

1//! GitHub PR 管理
2//!
3//! 提供 PR 信息获取、评论、创建等功能
4
5use serde::{Deserialize, Serialize};
6use tokio::process::Command;
7
8/// PR 信息
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct PRInfo {
11    /// 标题
12    pub title: String,
13    /// 描述
14    pub body: String,
15    /// 作者
16    pub author: String,
17    /// 状态
18    pub state: String,
19    /// 新增行数
20    pub additions: u32,
21    /// 删除行数
22    pub deletions: u32,
23    /// 变更文件数
24    pub changed_files: u32,
25}
26
27/// PR 评论
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct PRComment {
30    /// 作者
31    pub author: String,
32    /// 内容
33    pub body: String,
34    /// 创建时间
35    pub created_at: String,
36}
37
38/// 获取 PR 信息
39pub 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
91/// 获取 PR 评论
92pub 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
141/// 添加 PR 评论
142pub 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/// 创建 PR 选项
152#[derive(Debug, Clone, Default)]
153pub struct CreatePROptions {
154    /// 标题
155    pub title: String,
156    /// 描述
157    pub body: String,
158    /// 基础分支
159    pub base: Option<String>,
160    /// 头分支
161    pub head: Option<String>,
162    /// 是否为草稿
163    pub draft: bool,
164}
165
166/// 创建 PR 结果
167#[derive(Debug, Clone)]
168pub struct CreatePRResult {
169    /// 是否成功
170    pub success: bool,
171    /// PR URL
172    pub url: Option<String>,
173    /// 错误信息
174    pub error: Option<String>,
175}
176
177/// 创建 PR
178pub 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}