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 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 fn detect_github_repo() -> Result<(String, String)> {
57 detect_github_repo_in(&std::env::current_dir()?)
58}
59
60pub fn detect_github_repo_in(cwd: &Path) -> Result<(String, String)> {
67 let url = git_output_in(cwd, &["remote", "get-url", "origin"])?;
68 parse_github_remote(&url).ok_or_else(|| {
69 let safe = redact_url_credentials(&url);
72 anyhow::anyhow!(
73 "could not parse GitHub owner/repo from remote URL: {}",
74 safe
75 )
76 })
77}
78
79pub fn parse_remote_owner_repo(url: &str) -> Option<(String, String)> {
87 let url = url.trim();
88 if url.is_empty() {
89 return None;
90 }
91
92 let url = url.strip_suffix(".git").unwrap_or(url);
94
95 if url.starts_with("https://") || url.starts_with("http://") {
97 let after_scheme = if let Some(rest) = url.strip_prefix("https://") {
99 rest
100 } else {
101 url.strip_prefix("http://")?
102 };
103 let after_host = after_scheme.find('/').map(|i| &after_scheme[i + 1..])?;
105 let last_slash = after_host.rfind('/')?;
108 let owner = &after_host[..last_slash];
109 let repo = &after_host[last_slash + 1..];
110 if !owner.is_empty() && !repo.is_empty() {
111 return Some((owner.to_string(), repo.to_string()));
112 }
113 }
114
115 if let Some(colon_pos) = url.find(':') {
117 let before_colon = &url[..colon_pos];
118 if before_colon.contains('@') && !before_colon.contains("//") {
120 let path = &url[colon_pos + 1..];
121 let last_slash = path.rfind('/')?;
122 let owner = &path[..last_slash];
123 let repo = &path[last_slash + 1..];
124 if !owner.is_empty() && !repo.is_empty() {
125 return Some((owner.to_string(), repo.to_string()));
126 }
127 }
128 }
129
130 None
131}
132
133pub fn parse_remote_web_base(url: &str) -> Option<String> {
147 let url = url.trim();
148 if url.is_empty() {
149 return None;
150 }
151 let url = url.strip_suffix(".git").unwrap_or(url);
152
153 if let Some(rest) = url
155 .strip_prefix("https://")
156 .or_else(|| url.strip_prefix("http://"))
157 {
158 let slash = rest.find('/')?;
160 let host_seg = &rest[..slash];
161 let path = &rest[slash + 1..];
162 let host = host_seg.rsplit('@').next().unwrap_or(host_seg);
163 if host.is_empty() || path.is_empty() {
164 return None;
165 }
166 return Some(format!("https://{}/{}", host, path));
167 }
168
169 if let Some(colon_pos) = url.find(':') {
171 let before_colon = &url[..colon_pos];
172 if before_colon.contains('@') && !before_colon.contains("//") {
173 let host = before_colon.rsplit('@').next().unwrap_or(before_colon);
174 let path = &url[colon_pos + 1..];
175 if !host.is_empty() && !path.is_empty() {
176 return Some(format!("https://{}/{}", host, path));
177 }
178 }
179 }
180
181 None
182}
183
184pub fn detect_remote_web_base_in(cwd: &Path) -> Result<String> {
190 let url = git_output_in(cwd, &["remote", "get-url", "origin"])?;
191 parse_remote_web_base(&url).ok_or_else(|| {
192 let safe = redact_url_credentials(&url);
193 anyhow::anyhow!("could not parse web base from remote URL: {}", safe)
194 })
195}
196
197pub fn detect_owner_repo() -> Result<(String, String)> {
202 detect_owner_repo_in(&std::env::current_dir()?)
203}
204
205pub fn detect_owner_repo_in(cwd: &Path) -> Result<(String, String)> {
211 let url = git_output_in(cwd, &["remote", "get-url", "origin"])?;
212 parse_remote_owner_repo(&url).ok_or_else(|| {
213 let safe = redact_url_credentials(&url);
215 anyhow::anyhow!("could not parse owner/repo from remote URL: {}", safe)
216 })
217}