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
70pub(crate) fn open_url(url: &str, verbose: bool) -> Result<()> {
74 let gw_cmd = std::env::var("GW_OPEN_URL_CMD").ok();
75 let open_cmd = std::env::var("OPEN_URL_CMD").ok();
76 let program = resolve_open_program(
77 gw_cmd.as_deref(),
78 open_cmd.as_deref(),
79 default_open_program(),
80 );
81
82 if verbose {
83 output::action(&format!("{} {}", program, url));
84 }
85
86 let status = Command::new(&program)
87 .arg(url)
88 .status()
89 .map_err(|e| GwError::Other(format!("Failed to run open command '{program}': {e}")))?;
90
91 if !status.success() {
92 return Err(GwError::Other(format!(
93 "Open command '{program}' exited with a non-zero status"
94 )));
95 }
96
97 Ok(())
98}
99
100fn default_open_program() -> &'static str {
102 if cfg!(target_os = "macos") {
103 "open"
104 } else {
105 "xdg-open"
106 }
107}
108
109fn resolve_open_program(gw_cmd: Option<&str>, open_cmd: Option<&str>, os_default: &str) -> String {
114 gw_cmd
115 .filter(|s| !s.is_empty())
116 .or_else(|| open_cmd.filter(|s| !s.is_empty()))
117 .unwrap_or(os_default)
118 .to_string()
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[test]
126 fn gw_cmd_takes_precedence() {
127 let program = resolve_open_program(Some("gw-open"), Some("open-url"), "open");
128 assert_eq!(program, "gw-open");
129 }
130
131 #[test]
132 fn falls_back_to_open_url_cmd() {
133 let program = resolve_open_program(None, Some("open-url"), "open");
134 assert_eq!(program, "open-url");
135 }
136
137 #[test]
138 fn falls_back_to_os_default() {
139 let program = resolve_open_program(None, None, "xdg-open");
140 assert_eq!(program, "xdg-open");
141 }
142
143 #[test]
144 fn empty_values_are_ignored() {
145 assert_eq!(
147 resolve_open_program(Some(""), Some("open-url"), "open"),
148 "open-url"
149 );
150 assert_eq!(resolve_open_program(Some(""), Some(""), "open"), "open");
152 }
153}