use anyhow::Result;
use std::path::Path;
use std::process::Command;
use super::git_output_in;
use crate::redact::redact_url_credentials;
pub fn has_remote_in(cwd: &Path, remote: &str) -> bool {
Command::new("git")
.current_dir(cwd)
.args(["remote", "get-url", remote])
.env("GIT_TERMINAL_PROMPT", "0")
.env("LC_ALL", "C")
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
pub(crate) fn parse_github_remote(url: &str) -> Option<(String, String)> {
let url = url.trim();
if url.is_empty() {
return None;
}
let url = url.strip_suffix(".git").unwrap_or(url);
if let Some(path) = url.strip_prefix("https://github.com/") {
let parts: Vec<&str> = path.splitn(3, '/').collect();
if parts.len() >= 2 && !parts[0].is_empty() && !parts[1].is_empty() {
return Some((parts[0].to_string(), parts[1].to_string()));
}
}
if let Some(path) = url.strip_prefix("git@github.com:") {
let parts: Vec<&str> = path.splitn(3, '/').collect();
if parts.len() >= 2 && !parts[0].is_empty() && !parts[1].is_empty() {
return Some((parts[0].to_string(), parts[1].to_string()));
}
}
None
}
pub(crate) fn detect_github_repo_in(cwd: &Path) -> Result<(String, String)> {
let url = git_output_in(cwd, &["remote", "get-url", "origin"])?;
parse_github_remote(&url).ok_or_else(|| {
let safe = redact_url_credentials(&url);
anyhow::anyhow!(
"could not parse GitHub owner/repo from remote URL: {}",
safe
)
})
}
pub(crate) fn parse_remote_owner_repo(url: &str) -> Option<(String, String)> {
let url = url.trim();
if url.is_empty() {
return None;
}
let url = url.strip_suffix(".git").unwrap_or(url);
if url.starts_with("https://") || url.starts_with("http://") {
let after_scheme = if let Some(rest) = url.strip_prefix("https://") {
rest
} else {
url.strip_prefix("http://")?
};
let after_host = after_scheme.find('/').map(|i| &after_scheme[i + 1..])?;
let last_slash = after_host.rfind('/')?;
let owner = &after_host[..last_slash];
let repo = &after_host[last_slash + 1..];
if !owner.is_empty() && !repo.is_empty() {
return Some((owner.to_string(), repo.to_string()));
}
}
if let Some(colon_pos) = url.find(':') {
let before_colon = &url[..colon_pos];
if before_colon.contains('@') && !before_colon.contains("//") {
let path = &url[colon_pos + 1..];
let last_slash = path.rfind('/')?;
let owner = &path[..last_slash];
let repo = &path[last_slash + 1..];
if !owner.is_empty() && !repo.is_empty() {
return Some((owner.to_string(), repo.to_string()));
}
}
}
None
}
pub fn parse_remote_web_base(url: &str) -> Option<String> {
let url = url.trim();
if url.is_empty() {
return None;
}
let url = url.strip_suffix(".git").unwrap_or(url);
if let Some(rest) = url
.strip_prefix("https://")
.or_else(|| url.strip_prefix("http://"))
{
let slash = rest.find('/')?;
let host_seg = &rest[..slash];
let path = &rest[slash + 1..];
let host = host_seg.rsplit('@').next().unwrap_or(host_seg);
if host.is_empty() || path.is_empty() {
return None;
}
return Some(format!("https://{}/{}", host, path));
}
if let Some(colon_pos) = url.find(':') {
let before_colon = &url[..colon_pos];
if before_colon.contains('@') && !before_colon.contains("//") {
let host = before_colon.rsplit('@').next().unwrap_or(before_colon);
let path = &url[colon_pos + 1..];
if !host.is_empty() && !path.is_empty() {
return Some(format!("https://{}/{}", host, path));
}
}
}
None
}
pub fn detect_remote_web_base_in(cwd: &Path) -> Result<String> {
let url = git_output_in(cwd, &["remote", "get-url", "origin"])?;
parse_remote_web_base(&url).ok_or_else(|| {
let safe = redact_url_credentials(&url);
anyhow::anyhow!("could not parse web base from remote URL: {}", safe)
})
}
pub(crate) fn detect_owner_repo_in(cwd: &Path) -> Result<(String, String)> {
let url = git_output_in(cwd, &["remote", "get-url", "origin"])?;
parse_remote_owner_repo(&url).ok_or_else(|| {
let safe = redact_url_credentials(&url);
anyhow::anyhow!("could not parse owner/repo from remote URL: {}", safe)
})
}