1use std::process::Command;
4
5use crate::error::Result;
6use crate::git::{self, PushResult};
7use crate::output::{
8 print_push_already_up_to_date, print_push_success, print_pushing_branch, print_warning,
9};
10use crate::spec::Spec;
11
12use super::detection::{get_existing_pr_number, get_existing_pr_url, pr_exists_for_branch};
13use super::format::{format_pr_description, format_pr_title};
14use super::template::{detect_pr_template, run_template_agent, TemplateAgentResult};
15use super::types::PRResult;
16
17pub fn is_gh_installed() -> bool {
19 Command::new("gh")
20 .arg("--version")
21 .output()
22 .map(|o| o.status.success())
23 .unwrap_or(false)
24}
25
26pub fn is_gh_authenticated() -> bool {
28 Command::new("gh")
29 .args(["auth", "status"])
30 .output()
31 .map(|o| o.status.success())
32 .unwrap_or(false)
33}
34
35pub fn update_pr_description(spec: &Spec, pr_number: u32) -> Result<PRResult> {
37 let repo_root = std::env::current_dir().unwrap_or_default();
39 if let Some(template_content) = detect_pr_template(&repo_root) {
40 let title = format_pr_title(spec);
42 match run_template_agent(
44 spec,
45 &template_content,
46 &title,
47 Some(pr_number),
48 false,
49 |_| {},
50 ) {
51 Ok(TemplateAgentResult::Success(url)) => {
52 return Ok(PRResult::Updated(url));
53 }
54 Ok(TemplateAgentResult::Error(error_info)) => {
55 print_warning(&format!(
57 "Template agent failed ({}), using generated description",
58 error_info.message
59 ));
60 }
61 Err(e) => {
62 print_warning(&format!(
64 "Template agent error ({}), using generated description",
65 e
66 ));
67 }
68 }
69 }
70
71 update_pr_description_direct(spec, pr_number)
73}
74
75fn update_pr_description_direct(spec: &Spec, pr_number: u32) -> Result<PRResult> {
77 let body = format_pr_description(spec);
78
79 let output = Command::new("gh")
80 .args(["pr", "edit", &pr_number.to_string(), "--body", &body])
81 .output()?;
82
83 if !output.status.success() {
84 let stderr = String::from_utf8_lossy(&output.stderr);
85 return Ok(PRResult::Error(format!(
86 "Failed to update PR: {}",
87 stderr.trim()
88 )));
89 }
90
91 let url_output = Command::new("gh")
92 .args(["pr", "view", &pr_number.to_string(), "--json", "url"])
93 .output()?;
94
95 if url_output.status.success() {
96 let stdout = String::from_utf8_lossy(&url_output.stdout);
97 if let Ok(parsed) = serde_json::from_str::<serde_json::Value>(stdout.trim()) {
98 if let Some(url) = parsed.get("url").and_then(|v| v.as_str()) {
99 return Ok(PRResult::Updated(url.to_string()));
100 }
101 }
102 }
103
104 Ok(PRResult::Updated(format!("PR #{}", pr_number)))
105}
106
107pub fn create_pull_request(spec: &Spec, commits_were_made: bool, draft: bool) -> Result<PRResult> {
109 if !commits_were_made {
110 return Ok(PRResult::Skipped(
111 "No commits were made in this session".to_string(),
112 ));
113 }
114
115 if !git::is_git_repo() {
116 return Ok(PRResult::Skipped("Not in a git repository".to_string()));
117 }
118
119 if !is_gh_installed() {
120 return Ok(PRResult::Skipped(
121 "GitHub CLI (gh) not installed. Install from https://cli.github.com".to_string(),
122 ));
123 }
124
125 if !is_gh_authenticated() {
126 return Ok(PRResult::Skipped(
127 "Not authenticated with GitHub CLI. Run 'gh auth login' first".to_string(),
128 ));
129 }
130
131 let branch = match git::current_branch() {
132 Ok(b) => b,
133 Err(e) => {
134 return Ok(PRResult::Error(format!(
135 "Failed to get current branch: {}",
136 e
137 )))
138 }
139 };
140
141 if branch == "main" || branch == "master" {
142 return Ok(PRResult::Skipped(format!(
143 "Cannot create PR from {} branch",
144 branch
145 )));
146 }
147
148 if pr_exists_for_branch(&branch)? {
150 if let Some(pr_number) = get_existing_pr_number(&branch)? {
152 return update_pr_description(spec, pr_number);
153 } else if let Some(url) = get_existing_pr_url(&branch)? {
154 return Ok(PRResult::AlreadyExists(url));
155 }
156 return Ok(PRResult::AlreadyExists(format!("PR exists for {}", branch)));
157 }
158
159 let push_result = ensure_branch_pushed(&branch)?;
161 if let PushResult::Error(e) = push_result {
162 return Ok(PRResult::Error(format!("Failed to push branch: {}", e)));
163 }
164
165 let repo_root = std::env::current_dir().unwrap_or_default();
167 if let Some(template_content) = detect_pr_template(&repo_root) {
168 let title = format_pr_title(spec);
170 match run_template_agent(spec, &template_content, &title, None, draft, |_| {}) {
171 Ok(TemplateAgentResult::Success(url)) => {
172 return Ok(PRResult::Success(url));
173 }
174 Ok(TemplateAgentResult::Error(error_info)) => {
175 print_warning(&format!(
177 "Template agent failed ({}), using generated description",
178 error_info.message
179 ));
180 }
181 Err(e) => {
182 print_warning(&format!(
184 "Template agent error ({}), using generated description",
185 e
186 ));
187 }
188 }
189 }
190
191 create_pull_request_direct(spec, draft)
193}
194
195#[cfg(test)]
196fn build_pr_create_args<'a>(title: &'a str, body: &'a str, draft: bool) -> Vec<&'a str> {
197 let mut args = vec!["pr", "create", "--title", title, "--body", body];
198 if draft {
199 args.push("--draft");
200 }
201 args
202}
203
204fn create_pull_request_direct(spec: &Spec, draft: bool) -> Result<PRResult> {
206 let title = format_pr_title(spec);
207 let body = format_pr_description(spec);
208
209 let mut args = vec!["pr", "create", "--title", &title, "--body", &body];
210 if draft {
211 args.push("--draft");
212 }
213
214 let output = Command::new("gh").args(&args).output()?;
215
216 if !output.status.success() {
217 let stderr = String::from_utf8_lossy(&output.stderr);
218 return Ok(PRResult::Error(format!(
219 "Failed to create PR: {}",
220 stderr.trim()
221 )));
222 }
223
224 let stdout = String::from_utf8_lossy(&output.stdout);
225 let url = stdout.trim().to_string();
226
227 Ok(PRResult::Success(url))
228}
229
230pub fn ensure_branch_pushed(branch: &str) -> Result<PushResult> {
232 print_pushing_branch(branch);
233 let result = git::push_branch(branch)?;
234
235 match &result {
236 PushResult::Success => print_push_success(),
237 PushResult::AlreadyUpToDate => print_push_already_up_to_date(),
238 PushResult::Error(_) => {}
239 }
240
241 Ok(result)
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247 use crate::spec::UserStory;
248
249 fn make_test_spec() -> Spec {
250 Spec {
251 project: "TestProject".to_string(),
252 branch_name: "feature/test".to_string(),
253 description: "Test feature description.".to_string(),
254 user_stories: vec![UserStory {
255 id: "US-001".to_string(),
256 title: "Test Story".to_string(),
257 description: "Test story description".to_string(),
258 acceptance_criteria: vec!["Criterion 1".to_string()],
259 priority: 1,
260 passes: true,
261 notes: String::new(),
262 }],
263 }
264 }
265
266 #[test]
271 fn test_pr_result_success_variant() {
272 let result = PRResult::Success("https://github.com/owner/repo/pull/1".to_string());
273 assert!(matches!(result, PRResult::Success(_)));
274 }
275
276 #[test]
277 fn test_pr_result_updated_variant() {
278 let result = PRResult::Updated("https://github.com/owner/repo/pull/1".to_string());
279 assert!(matches!(result, PRResult::Updated(_)));
280 }
281
282 #[test]
283 fn test_pr_result_already_exists_variant() {
284 let result = PRResult::AlreadyExists("https://github.com/owner/repo/pull/1".to_string());
285 assert!(matches!(result, PRResult::AlreadyExists(_)));
286 }
287
288 #[test]
289 fn test_pr_result_skipped_variant() {
290 let result = PRResult::Skipped("reason".to_string());
291 assert!(matches!(result, PRResult::Skipped(_)));
292 }
293
294 #[test]
295 fn test_pr_result_error_variant() {
296 let result = PRResult::Error("error message".to_string());
297 assert!(matches!(result, PRResult::Error(_)));
298 }
299
300 #[test]
305 fn test_create_pr_skips_when_no_commits() {
306 let spec = make_test_spec();
307 let result = create_pull_request(&spec, false, false);
308 assert!(result.is_ok());
309 match result.unwrap() {
310 PRResult::Skipped(msg) => {
311 assert!(msg.contains("No commits"));
312 }
313 _ => panic!("Expected Skipped result"),
314 }
315 }
316
317 #[test]
322 fn test_detect_pr_template_integration_no_template_in_test_dir() {
323 use tempfile::TempDir;
326 let temp_dir = TempDir::new().unwrap();
327 let result = detect_pr_template(temp_dir.path());
328 assert!(result.is_none());
329 }
330
331 #[test]
332 fn test_detect_pr_template_integration_with_template() {
333 use std::fs;
334 use tempfile::TempDir;
335
336 let temp_dir = TempDir::new().unwrap();
337 let github_dir = temp_dir.path().join(".github");
338 fs::create_dir_all(&github_dir).unwrap();
339 fs::write(
340 github_dir.join("pull_request_template.md"),
341 "## Description\n\nPlease describe your changes.",
342 )
343 .unwrap();
344
345 let result = detect_pr_template(temp_dir.path());
346 assert!(result.is_some());
347 assert!(result.unwrap().contains("Description"));
348 }
349
350 #[test]
355 fn test_create_pull_request_direct_builds_correct_command_args() {
356 let spec = make_test_spec();
360 let title = format_pr_title(&spec);
361 let body = format_pr_description(&spec);
362
363 assert!(title.contains("TestProject"));
364 assert!(body.contains("Summary"));
365 assert!(body.contains("Test feature description"));
366 }
367
368 #[test]
369 fn test_update_pr_description_direct_builds_correct_command_args() {
370 let spec = make_test_spec();
372 let body = format_pr_description(&spec);
373
374 assert!(body.contains("Summary"));
375 assert!(body.contains("US-001"));
376 assert!(body.contains("Test Story"));
377 }
378
379 #[test]
384 fn test_build_pr_create_args_without_draft() {
385 let title = "Test PR Title";
386 let body = "Test PR body content";
387 let args = build_pr_create_args(title, body, false);
388
389 assert_eq!(args, vec!["pr", "create", "--title", title, "--body", body]);
390 assert!(!args.contains(&"--draft"));
391 }
392
393 #[test]
394 fn test_build_pr_create_args_with_draft() {
395 let title = "Test PR Title";
396 let body = "Test PR body content";
397 let args = build_pr_create_args(title, body, true);
398
399 assert_eq!(
400 args,
401 vec!["pr", "create", "--title", title, "--body", body, "--draft"]
402 );
403 assert!(args.contains(&"--draft"));
404 }
405
406 #[test]
411 fn test_is_gh_installed_returns_bool() {
412 let _ = is_gh_installed();
415 }
416
417 #[test]
418 fn test_is_gh_authenticated_returns_bool() {
419 let _ = is_gh_authenticated();
422 }
423}