1use std::path::Path;
2
3use anyhow::{Context, Result};
4
5use super::GhClient;
6use crate::process::CommandRunner;
7
8impl<R: CommandRunner> GhClient<R> {
9 pub async fn create_draft_pr(&self, title: &str, branch: &str, body: &str) -> Result<u32> {
11 self.create_draft_pr_in(title, branch, body, &self.repo_dir).await
12 }
13
14 pub async fn create_draft_pr_in(
18 &self,
19 title: &str,
20 branch: &str,
21 body: &str,
22 repo_dir: &Path,
23 ) -> Result<u32> {
24 let output = self
25 .runner
26 .run_gh(
27 &Self::s(&[
28 "pr", "create", "--title", title, "--body", body, "--head", branch, "--draft",
29 ]),
30 repo_dir,
31 )
32 .await
33 .context("creating draft PR")?;
34 Self::check_output(&output, "create draft PR")?;
35
36 let url = output.stdout.trim();
38 let pr_number = url
39 .rsplit('/')
40 .next()
41 .and_then(|s| s.parse::<u32>().ok())
42 .context("parsing PR number from gh output")?;
43
44 Ok(pr_number)
45 }
46
47 pub async fn comment_on_pr(&self, pr_number: u32, body: &str) -> Result<()> {
49 let output = self
50 .runner
51 .run_gh(
52 &Self::s(&["pr", "comment", &pr_number.to_string(), "--body", body]),
53 &self.repo_dir,
54 )
55 .await
56 .context("commenting on PR")?;
57 Self::check_output(&output, "comment on PR")?;
58 Ok(())
59 }
60
61 pub async fn mark_pr_ready(&self, pr_number: u32) -> Result<()> {
63 let output = self
64 .runner
65 .run_gh(&Self::s(&["pr", "ready", &pr_number.to_string()]), &self.repo_dir)
66 .await
67 .context("marking PR ready")?;
68 Self::check_output(&output, "mark PR ready")?;
69 Ok(())
70 }
71
72 pub async fn merge_pr(&self, pr_number: u32) -> Result<()> {
74 let output = self
75 .runner
76 .run_gh(
77 &Self::s(&["pr", "merge", &pr_number.to_string(), "--squash", "--delete-branch"]),
78 &self.repo_dir,
79 )
80 .await
81 .context("merging PR")?;
82 Self::check_output(&output, "merge PR")?;
83 Ok(())
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use std::path::Path;
90
91 use crate::{
92 github::GhClient,
93 process::{CommandOutput, MockCommandRunner},
94 };
95
96 #[tokio::test]
97 async fn create_draft_pr_returns_number() {
98 let mut mock = MockCommandRunner::new();
99 mock.expect_run_gh().returning(|_, _| {
100 Box::pin(async {
101 Ok(CommandOutput {
102 stdout: "https://github.com/user/repo/pull/99\n".to_string(),
103 stderr: String::new(),
104 success: true,
105 })
106 })
107 });
108
109 let client = GhClient::new(mock, Path::new("/tmp"));
110 let pr_number = client.create_draft_pr("title", "branch", "body").await.unwrap();
111 assert_eq!(pr_number, 99);
112 }
113
114 #[tokio::test]
115 async fn comment_on_pr_succeeds() {
116 let mut mock = MockCommandRunner::new();
117 mock.expect_run_gh().returning(|_, _| {
118 Box::pin(async {
119 Ok(CommandOutput { stdout: String::new(), stderr: String::new(), success: true })
120 })
121 });
122
123 let client = GhClient::new(mock, Path::new("/tmp"));
124 let result = client.comment_on_pr(42, "looks good").await;
125 assert!(result.is_ok());
126 }
127
128 #[tokio::test]
129 async fn mark_pr_ready_succeeds() {
130 let mut mock = MockCommandRunner::new();
131 mock.expect_run_gh().returning(|_, _| {
132 Box::pin(async {
133 Ok(CommandOutput { stdout: String::new(), stderr: String::new(), success: true })
134 })
135 });
136
137 let client = GhClient::new(mock, Path::new("/tmp"));
138 let result = client.mark_pr_ready(42).await;
139 assert!(result.is_ok());
140 }
141
142 #[tokio::test]
143 async fn merge_pr_succeeds() {
144 let mut mock = MockCommandRunner::new();
145 mock.expect_run_gh().returning(|_, _| {
146 Box::pin(async {
147 Ok(CommandOutput { stdout: String::new(), stderr: String::new(), success: true })
148 })
149 });
150
151 let client = GhClient::new(mock, Path::new("/tmp"));
152 let result = client.merge_pr(42).await;
153 assert!(result.is_ok());
154 }
155
156 #[tokio::test]
157 async fn merge_pr_failure_propagates() {
158 let mut mock = MockCommandRunner::new();
159 mock.expect_run_gh().returning(|_, _| {
160 Box::pin(async {
161 Ok(CommandOutput {
162 stdout: String::new(),
163 stderr: "merge conflict".to_string(),
164 success: false,
165 })
166 })
167 });
168
169 let client = GhClient::new(mock, Path::new("/tmp"));
170 let result = client.merge_pr(42).await;
171 assert!(result.is_err());
172 assert!(result.unwrap_err().to_string().contains("merge conflict"));
173 }
174}