Skip to main content

anodizer_core/git/
remote.rs

1use anyhow::Result;
2
3use super::git_output;
4use crate::redact::redact_url_credentials;
5
6/// Parse owner and repo name from a GitHub remote URL.
7/// Supports HTTPS (`https://github.com/owner/repo.git`) and SSH (`git@github.com:owner/repo.git`).
8pub fn parse_github_remote(url: &str) -> Option<(String, String)> {
9    let url = url.trim();
10    if url.is_empty() {
11        return None;
12    }
13
14    // Strip trailing ".git" if present
15    let url = url.strip_suffix(".git").unwrap_or(url);
16
17    // HTTPS: https://github.com/owner/repo
18    if let Some(path) = url.strip_prefix("https://github.com/") {
19        let parts: Vec<&str> = path.splitn(3, '/').collect();
20        if parts.len() >= 2 && !parts[0].is_empty() && !parts[1].is_empty() {
21            return Some((parts[0].to_string(), parts[1].to_string()));
22        }
23    }
24
25    // SSH: git@github.com:owner/repo
26    if let Some(path) = url.strip_prefix("git@github.com:") {
27        let parts: Vec<&str> = path.splitn(3, '/').collect();
28        if parts.len() >= 2 && !parts[0].is_empty() && !parts[1].is_empty() {
29            return Some((parts[0].to_string(), parts[1].to_string()));
30        }
31    }
32
33    None
34}
35
36/// Get the GitHub owner/name from the `origin` remote.
37pub fn detect_github_repo() -> Result<(String, String)> {
38    let url = git_output(&["remote", "get-url", "origin"])?;
39    parse_github_remote(&url).ok_or_else(|| {
40        // Strip inline `<scheme>://<userinfo>@...` userinfo before surfacing
41        // the URL in a user-visible error.
42        let safe = redact_url_credentials(&url);
43        anyhow::anyhow!(
44            "could not parse GitHub owner/repo from remote URL: {}",
45            safe
46        )
47    })
48}
49
50/// Parse owner and repo from any git remote URL, regardless of host.
51///
52/// Supports HTTPS (`https://host/owner/repo.git`) and SSH (`git@host:owner/repo.git`)
53/// formats. Returns `(owner, repo)` with `.git` suffix stripped.
54///
55/// This is a host-agnostic version of [`parse_github_remote`], suitable for
56/// GitLab, Gitea, and other SCM providers.
57pub fn parse_remote_owner_repo(url: &str) -> Option<(String, String)> {
58    let url = url.trim();
59    if url.is_empty() {
60        return None;
61    }
62
63    // Strip trailing ".git" if present
64    let url = url.strip_suffix(".git").unwrap_or(url);
65
66    // HTTPS: https://host/owner/repo or https://host/group/subgroup/repo
67    if url.starts_with("https://") || url.starts_with("http://") {
68        // Strip scheme and host
69        let after_scheme = if let Some(rest) = url.strip_prefix("https://") {
70            rest
71        } else {
72            url.strip_prefix("http://")?
73        };
74        // Strip any credentials (user:pass@host or user@host)
75        let after_host = after_scheme.find('/').map(|i| &after_scheme[i + 1..])?;
76        // For nested groups (e.g. group/subgroup/repo), the owner is everything
77        // up to the last slash.
78        let last_slash = after_host.rfind('/')?;
79        let owner = &after_host[..last_slash];
80        let repo = &after_host[last_slash + 1..];
81        if !owner.is_empty() && !repo.is_empty() {
82            return Some((owner.to_string(), repo.to_string()));
83        }
84    }
85
86    // SSH: git@host:owner/repo or git@host:group/subgroup/repo
87    if let Some(colon_pos) = url.find(':') {
88        let before_colon = &url[..colon_pos];
89        // Ensure it looks like an SSH URL (contains @, no //)
90        if before_colon.contains('@') && !before_colon.contains("//") {
91            let path = &url[colon_pos + 1..];
92            let last_slash = path.rfind('/')?;
93            let owner = &path[..last_slash];
94            let repo = &path[last_slash + 1..];
95            if !owner.is_empty() && !repo.is_empty() {
96                return Some((owner.to_string(), repo.to_string()));
97            }
98        }
99    }
100
101    None
102}
103
104/// Get the owner/repo from the `origin` remote, regardless of SCM host.
105///
106/// Uses [`parse_remote_owner_repo`] which works with any git hosting provider
107/// (GitHub, GitLab, Gitea, etc.).
108pub fn detect_owner_repo() -> Result<(String, String)> {
109    let url = git_output(&["remote", "get-url", "origin"])?;
110    parse_remote_owner_repo(&url).ok_or_else(|| {
111        // Strip inline userinfo before surfacing the URL.
112        let safe = redact_url_credentials(&url);
113        anyhow::anyhow!("could not parse owner/repo from remote URL: {}", safe)
114    })
115}