git_workflow/commands/
open.rs1use std::process::Command;
20
21use crate::error::{GwError, Result};
22use crate::git;
23use crate::github;
24use crate::output;
25use crate::state::RepoType;
26
27pub fn run(verbose: bool) -> Result<()> {
29 if !git::is_git_repo() {
31 return Err(GwError::NotAGitRepository);
32 }
33
34 let repo_type = RepoType::detect()?;
35 let home_branch = repo_type.home_branch();
36 let current = git::current_branch()?;
37
38 println!();
39
40 if current == home_branch {
42 output::warn(&format!("On home branch '{}'. No PR to open.", home_branch));
43 output::hints(&["gw new feature/your-feature # Start a branch first"]);
44 return Ok(());
45 }
46
47 if !github::is_gh_available() {
49 return Err(GwError::Other(
50 "GitHub CLI (gh) is not available. Install it from https://cli.github.com/".into(),
51 ));
52 }
53
54 let pr = match github::get_pr_for_branch(¤t)? {
55 Some(pr) => pr,
56 None => {
57 output::warn(&format!("No PR found for branch '{}'.", current));
58 output::hints(&["gh pr create -a \"@me\" -t \"...\" # Create a PR first"]);
59 return Ok(());
60 }
61 };
62
63 output::info(&format!("PR: #{} {}", pr.number, pr.title));
64 open_url(&pr.url, verbose)?;
65 output::success(&format!("Opened {}", pr.url));
66
67 Ok(())
68}
69
70fn open_url(url: &str, verbose: bool) -> Result<()> {
72 let gw_cmd = std::env::var("GW_OPEN_URL_CMD").ok();
73 let open_cmd = std::env::var("OPEN_URL_CMD").ok();
74 let program = resolve_open_program(
75 gw_cmd.as_deref(),
76 open_cmd.as_deref(),
77 default_open_program(),
78 );
79
80 if verbose {
81 output::action(&format!("{} {}", program, url));
82 }
83
84 let status = Command::new(&program)
85 .arg(url)
86 .status()
87 .map_err(|e| GwError::Other(format!("Failed to run open command '{program}': {e}")))?;
88
89 if !status.success() {
90 return Err(GwError::Other(format!(
91 "Open command '{program}' exited with a non-zero status"
92 )));
93 }
94
95 Ok(())
96}
97
98fn default_open_program() -> &'static str {
100 if cfg!(target_os = "macos") {
101 "open"
102 } else {
103 "xdg-open"
104 }
105}
106
107fn resolve_open_program(gw_cmd: Option<&str>, open_cmd: Option<&str>, os_default: &str) -> String {
112 gw_cmd
113 .filter(|s| !s.is_empty())
114 .or_else(|| open_cmd.filter(|s| !s.is_empty()))
115 .unwrap_or(os_default)
116 .to_string()
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn gw_cmd_takes_precedence() {
125 let program = resolve_open_program(Some("gw-open"), Some("open-url"), "open");
126 assert_eq!(program, "gw-open");
127 }
128
129 #[test]
130 fn falls_back_to_open_url_cmd() {
131 let program = resolve_open_program(None, Some("open-url"), "open");
132 assert_eq!(program, "open-url");
133 }
134
135 #[test]
136 fn falls_back_to_os_default() {
137 let program = resolve_open_program(None, None, "xdg-open");
138 assert_eq!(program, "xdg-open");
139 }
140
141 #[test]
142 fn empty_values_are_ignored() {
143 assert_eq!(
145 resolve_open_program(Some(""), Some("open-url"), "open"),
146 "open-url"
147 );
148 assert_eq!(resolve_open_program(Some(""), Some(""), "open"), "open");
150 }
151}