anodizer_core/git/
remote.rs1use anyhow::Result;
2use std::path::Path;
3use std::process::Command;
4
5use super::git_output_in;
6use crate::redact::redact_url_credentials;
7
8pub fn has_remote_in(cwd: &Path, remote: &str) -> bool {
15 Command::new("git")
16 .current_dir(cwd)
17 .args(["remote", "get-url", remote])
18 .env("GIT_TERMINAL_PROMPT", "0")
19 .env("LC_ALL", "C")
20 .output()
21 .map(|o| o.status.success())
22 .unwrap_or(false)
23}
24
25pub(crate) fn parse_github_remote(url: &str) -> Option<(String, String)> {
28 let url = url.trim();
29 if url.is_empty() {
30 return None;
31 }
32
33 let url = url.strip_suffix(".git").unwrap_or(url);
35
36 if let Some(path) = url.strip_prefix("https://github.com/") {
38 let parts: Vec<&str> = path.splitn(3, '/').collect();
39 if parts.len() >= 2 && !parts[0].is_empty() && !parts[1].is_empty() {
40 return Some((parts[0].to_string(), parts[1].to_string()));
41 }
42 }
43
44 if let Some(path) = url.strip_prefix("git@github.com:") {
46 let parts: Vec<&str> = path.splitn(3, '/').collect();
47 if parts.len() >= 2 && !parts[0].is_empty() && !parts[1].is_empty() {
48 return Some((parts[0].to_string(), parts[1].to_string()));
49 }
50 }
51
52 None
53}
54
55pub(crate) fn detect_github_repo_in(cwd: &Path) -> Result<(String, String)> {
61 let url = git_output_in(cwd, &["remote", "get-url", "origin"])?;
62 parse_github_remote(&url).ok_or_else(|| {
63 let safe = redact_url_credentials(&url);
66 anyhow::anyhow!(
67 "could not parse GitHub owner/repo from remote URL: {}",
68 safe
69 )
70 })
71}
72
73pub(crate) fn parse_remote_owner_repo(url: &str) -> Option<(String, String)> {
81 let url = url.trim();
82 if url.is_empty() {
83 return None;
84 }
85
86 let url = url.strip_suffix(".git").unwrap_or(url);
88
89 if url.starts_with("https://") || url.starts_with("http://") {
91 let after_scheme = if let Some(rest) = url.strip_prefix("https://") {
93 rest
94 } else {
95 url.strip_prefix("http://")?
96 };
97 let after_host = after_scheme.find('/').map(|i| &after_scheme[i + 1..])?;
99 let last_slash = after_host.rfind('/')?;
102 let owner = &after_host[..last_slash];
103 let repo = &after_host[last_slash + 1..];
104 if !owner.is_empty() && !repo.is_empty() {
105 return Some((owner.to_string(), repo.to_string()));
106 }
107 }
108
109 if let Some(colon_pos) = url.find(':') {
111 let before_colon = &url[..colon_pos];
112 if before_colon.contains('@') && !before_colon.contains("//") {
114 let path = &url[colon_pos + 1..];
115 let last_slash = path.rfind('/')?;
116 let owner = &path[..last_slash];
117 let repo = &path[last_slash + 1..];
118 if !owner.is_empty() && !repo.is_empty() {
119 return Some((owner.to_string(), repo.to_string()));
120 }
121 }
122 }
123
124 None
125}
126
127pub fn parse_remote_web_base(url: &str) -> Option<String> {
141 let url = url.trim();
142 if url.is_empty() {
143 return None;
144 }
145 let url = url.strip_suffix(".git").unwrap_or(url);
146
147 if let Some(rest) = url
149 .strip_prefix("https://")
150 .or_else(|| url.strip_prefix("http://"))
151 {
152 let slash = rest.find('/')?;
154 let host_seg = &rest[..slash];
155 let path = &rest[slash + 1..];
156 let host = host_seg.rsplit('@').next().unwrap_or(host_seg);
157 if host.is_empty() || path.is_empty() {
158 return None;
159 }
160 return Some(format!("https://{}/{}", host, path));
161 }
162
163 if let Some(colon_pos) = url.find(':') {
165 let before_colon = &url[..colon_pos];
166 if before_colon.contains('@') && !before_colon.contains("//") {
167 let host = before_colon.rsplit('@').next().unwrap_or(before_colon);
168 let path = &url[colon_pos + 1..];
169 if !host.is_empty() && !path.is_empty() {
170 return Some(format!("https://{}/{}", host, path));
171 }
172 }
173 }
174
175 None
176}
177
178pub fn detect_remote_web_base_in(cwd: &Path) -> Result<String> {
184 let url = git_output_in(cwd, &["remote", "get-url", "origin"])?;
185 parse_remote_web_base(&url).ok_or_else(|| {
186 let safe = redact_url_credentials(&url);
187 anyhow::anyhow!("could not parse web base from remote URL: {}", safe)
188 })
189}
190
191pub(crate) fn detect_owner_repo_in(cwd: &Path) -> Result<(String, String)> {
197 let url = git_output_in(cwd, &["remote", "get-url", "origin"])?;
198 parse_remote_owner_repo(&url).ok_or_else(|| {
199 let safe = redact_url_credentials(&url);
201 anyhow::anyhow!("could not parse owner/repo from remote URL: {}", safe)
202 })
203}