gitow 1.0.0

Open Git remotes and repository pages in your browser from the CLI
Documentation
use std::path::Path;

use crate::{
    browser,
    cli::{Cli, OpenTarget},
    error::{GitowError, Result},
    git::{Repository, default_ssh_config_path},
    providers::{self, RepoPage},
    remote::parse_remote_url,
};

pub fn run(cli: Cli, cwd: &Path) -> Result<()> {
    let urls = resolve_urls(&cli, cwd)?;
    browser::open_urls(&urls, cli.print)
}

pub fn resolve_urls(cli: &Cli, cwd: &Path) -> Result<Vec<String>> {
    let repository = Repository::new(cwd);
    repository.ensure_work_tree()?;
    let target = cli.target();

    let branch = match &cli.branch {
        Some(branch) => Some(branch.clone()),
        None => repository.current_branch()?,
    };

    let upstream_branch = match branch.as_deref() {
        Some(branch) => repository.branch_upstream(branch)?,
        None => None,
    };
    let tracked_remote = match branch.as_deref() {
        Some(branch) => repository.branch_remote(branch)?,
        None => None,
    };
    let default_remote = repository.default_open_remote()?;

    let remote_names = if cli.all_remotes {
        let mut remotes = repository.remotes()?;
        remotes.sort_by(|left, right| match (left == "origin", right == "origin") {
            (true, false) => std::cmp::Ordering::Less,
            (false, true) => std::cmp::Ordering::Greater,
            _ => left.cmp(right),
        });

        if remotes.is_empty() {
            return Err(GitowError::NoRemotesConfigured);
        }

        remotes
    } else {
        vec![
            cli.remote
                .clone()
                .or(default_remote)
                .or(tracked_remote)
                .unwrap_or_else(|| "origin".to_string()),
        ]
    };

    let remote_ref = if let Some(reference) = upstream_branch.clone().or_else(|| branch.clone()) {
        reference
    } else if let Some(tag) = repository.exact_tag()? {
        tag
    } else {
        repository.head_sha()?
    };

    remote_names
        .into_iter()
        .map(|remote_name| {
            build_url_for_remote(
                &repository,
                cli,
                &remote_name,
                &remote_ref,
                branch.as_deref(),
                target,
            )
        })
        .collect()
}

fn build_url_for_remote(
    repository: &Repository,
    cli: &Cli,
    remote_name: &str,
    remote_ref: &str,
    branch: Option<&str>,
    target: OpenTarget,
) -> Result<String> {
    let git_url = repository.resolve_remote_url(remote_name)?;
    let ssh_config_path = default_ssh_config_path();
    let parsed_remote = parse_remote_url(&git_url, ssh_config_path.as_deref());
    let domain_override = repository.open_urlmatch("domain", &parsed_remote.config_base_url)?;
    let protocol_override = repository.open_urlmatch("protocol", &parsed_remote.config_base_url)?;
    let forge_override = repository.open_urlmatch("forge", &parsed_remote.config_base_url)?;
    let remote = parsed_remote.with_overrides(domain_override, protocol_override, forge_override);

    let mut open_url = match target {
        OpenTarget::Branch => {
            let parts = providers::build_branch_url_parts(&remote, remote_ref, false)?;
            let mut open_url = parts.base_url;
            let repository_file = if let Some(file) = &cli.file {
                let reference = branch.unwrap_or(remote_ref);
                let repository_file = repository
                    .repository_file(reference, file)?
                    .ok_or_else(|| GitowError::FileNotTracked(file.clone()))?;
                Some(repository_file)
            } else {
                None
            };

            if remote_ref != "master" || repository_file.is_some() {
                open_url.push_str(&parts.branch_ref);
            }

            if let Some(repository_file) = repository_file {
                open_url.push('/');
                open_url.push_str(&repository_file);
            }

            open_url
        }
        OpenTarget::CurrentCommit => {
            let sha = repository.head_sha()?;
            providers::build_commit_url(&remote, &sha)
        }
        OpenTarget::Issue => {
            let parts = providers::build_branch_url_parts(&remote, remote_ref, true)?;
            format!("{}{}", parts.base_url, parts.branch_ref)
        }
        OpenTarget::PullRequests => {
            providers::build_page_url(&remote, RepoPage::PullRequests, remote_ref)?
        }
        OpenTarget::Commits => providers::build_page_url(&remote, RepoPage::Commits, remote_ref)?,
        OpenTarget::Releases => providers::build_page_url(&remote, RepoPage::Releases, remote_ref)?,
    };

    if let Some(suffix) = &cli.suffix {
        open_url.push('/');
        open_url.push_str(suffix);
    }

    Ok(open_url)
}