Skip to main content

bb_cli/commands/
browse.rs

1use crate::error::Result;
2use crate::output::{self, Format};
3use crate::repo::{self, RepoSlug};
4
5#[derive(Debug, Clone, Copy)]
6pub enum BrowseTarget {
7    Pr(u64),
8    Branches,
9}
10
11pub fn url_for(slug: &RepoSlug, target: Option<&BrowseTarget>) -> String {
12    let base = slug.browse_url();
13    match target {
14        Some(BrowseTarget::Pr(id)) => format!("{base}/pull-requests/{id}"),
15        Some(BrowseTarget::Branches) => format!("{base}/branches"),
16        None => base,
17    }
18}
19
20pub fn browse(
21    repo_arg: Option<&str>,
22    target: Option<BrowseTarget>,
23    print_only: bool,
24    format: Format,
25) -> Result<()> {
26    // Parsing happens first: an invalid or hostile value never reaches a spawn.
27    let slug = repo::resolve(repo_arg)?;
28    let url = url_for(&slug, target.as_ref());
29
30    if format.is_json() {
31        return output::print_json(&serde_json::json!({ "url": url }));
32    }
33
34    println!("{url}");
35
36    if !print_only {
37        // The url is passed as a single argument, not through a shell.
38        if let Err(err) = open::that_detached(&url) {
39            output::warn(&format!("cannot open a browser: {err}"));
40        }
41    }
42
43    Ok(())
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49
50    fn slug() -> RepoSlug {
51        RepoSlug {
52            workspace: "acme".into(),
53            repo: "widgets".into(),
54        }
55    }
56
57    #[test]
58    fn repo_url_has_no_suffix() {
59        assert_eq!(url_for(&slug(), None), "https://bitbucket.org/acme/widgets");
60    }
61
62    #[test]
63    fn pr_url_includes_the_id() {
64        assert_eq!(
65            url_for(&slug(), Some(&BrowseTarget::Pr(7))),
66            "https://bitbucket.org/acme/widgets/pull-requests/7"
67        );
68    }
69
70    #[test]
71    fn branches_url() {
72        assert!(url_for(&slug(), Some(&BrowseTarget::Branches)).ends_with("/branches"));
73    }
74
75    /// Pinning the leading-dash analysis: a workspace beginning with `-` is
76    /// accepted by `valid_segment`, but `browse_url` always prefixes it with
77    /// `https://bitbucket.org/`, so the dash is never the first character of
78    /// the string handed to `open::that_detached`, and it is never its own
79    /// argv element (the whole url is one argument). It cannot be mistaken
80    /// for a flag by `open`/`xdg-open`.
81    #[test]
82    fn dash_prefixed_workspace_produces_a_well_formed_url() {
83        let slug = RepoSlug {
84            workspace: "-rf".into(),
85            repo: "widgets".into(),
86        };
87        let url = url_for(&slug, None);
88        assert_eq!(url, "https://bitbucket.org/-rf/widgets");
89        assert!(url.starts_with("https://"));
90    }
91}