Skip to main content

git_workflow/commands/
open.rs

1//! `gw open` command - Open the PR for the current branch in the browser
2//!
3//! Resolves the PR for the current branch via `gh` and opens its URL in the
4//! browser. The command used to open the URL is configurable so that
5//! environment-specific behavior (e.g. selecting a particular Chrome profile)
6//! lives in the user's dotfiles, not in this CLI.
7//!
8//! # Browser command resolution
9//!
10//! The URL is opened by invoking a single program with the URL as its only
11//! argument (`<program> <url>`). The program is resolved in this order:
12//!
13//! 1. `$GW_OPEN_URL_CMD` - gw-specific override
14//! 2. `$OPEN_URL_CMD`     - generic override (shared with other tooling)
15//! 3. OS default          - `open` on macOS, `xdg-open` elsewhere
16//!
17//! Set `GW_OPEN_URL_CMD`/`OPEN_URL_CMD` to a script that takes the URL as `$1`.
18
19use std::process::Command;
20
21use crate::error::{GwError, Result};
22use crate::git;
23use crate::github;
24use crate::output;
25use crate::state::RepoType;
26
27/// Execute the `open` command
28pub fn run(verbose: bool) -> Result<()> {
29    // Ensure we're in a git repo
30    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    // On the home branch there is no feature PR to open
41    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    // Need gh to look up the PR for this branch
48    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(&current)? {
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
70/// Open a URL in the browser using the resolved open command.
71///
72/// Shared with `gw await --open`.
73pub(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
100/// The OS default program for opening a URL.
101fn default_open_program() -> &'static str {
102    if cfg!(target_os = "macos") {
103        "open"
104    } else {
105        "xdg-open"
106    }
107}
108
109/// Resolve which program should be used to open a URL.
110///
111/// Precedence: `GW_OPEN_URL_CMD` > `OPEN_URL_CMD` > OS default. Empty values
112/// are ignored so that an exported-but-blank variable falls through.
113fn 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        // Exported-but-blank GW var should fall through to OPEN_URL_CMD
146        assert_eq!(
147            resolve_open_program(Some(""), Some("open-url"), "open"),
148            "open-url"
149        );
150        // Both blank → OS default
151        assert_eq!(resolve_open_program(Some(""), Some(""), "open"), "open");
152    }
153}