use anyhow::Context;
use super::{GitRemoteUrl, Repository};
impl Repository {
pub fn primary_remote(&self) -> anyhow::Result<String> {
if let Some(default_remote) = self.config_last("checkout.defaultRemote")? {
let default_remote = default_remote.trim();
if !default_remote.is_empty() && self.remote_url(default_remote).is_some() {
return Ok(default_remote.to_string());
}
}
let guard = self.all_config()?.read().unwrap();
let first_remote = guard.keys().find_map(|k| {
let rest = k.strip_prefix("remote.")?;
let name = rest.strip_suffix(".url")?;
Some(name.to_string())
});
first_remote.ok_or_else(|| anyhow::anyhow!("No remotes configured"))
}
pub fn remote_url(&self, remote: &str) -> Option<String> {
self.config_last(&format!("remote.{remote}.url"))
.ok()
.flatten()
.filter(|url| !url.is_empty())
}
pub fn effective_remote_url(&self, remote: &str) -> Option<String> {
self.cache
.effective_remote_urls
.entry(remote.to_string())
.or_insert_with(|| {
self.run_command(&["remote", "get-url", remote])
.ok()
.map(|url| url.trim().to_string())
.filter(|url| !url.is_empty())
})
.clone()
}
pub fn find_remote_for_repo(
&self,
host: Option<&str>,
owner: &str,
repo: &str,
) -> Option<String> {
let matches = |url: &str| -> bool {
let Some(parsed) = GitRemoteUrl::parse(url) else {
return false;
};
parsed.owner().eq_ignore_ascii_case(owner)
&& parsed.repo().eq_ignore_ascii_case(repo)
&& host.is_none_or(|h| parsed.host().eq_ignore_ascii_case(h))
};
for (remote_name, raw_url) in self.all_remote_urls() {
if matches(&raw_url) {
return Some(remote_name);
}
if let Some(effective_url) = self.effective_remote_url(&remote_name)
&& effective_url != raw_url
&& matches(&effective_url)
{
return Some(remote_name);
}
}
None
}
pub fn find_remote_by_url(&self, target_url: &str) -> Option<String> {
let parsed = GitRemoteUrl::parse(target_url)?;
self.find_remote_for_repo(Some(parsed.host()), parsed.owner(), parsed.repo())
}
pub fn all_remote_urls(&self) -> Vec<(String, String)> {
let Ok(lock) = self.all_config() else {
return Vec::new();
};
let guard = lock.read().unwrap();
guard
.iter()
.filter_map(|(k, values)| {
let rest = k.strip_prefix("remote.")?;
let name = rest.strip_suffix(".url")?;
let url = values.last()?.trim();
if url.is_empty() {
return None;
}
Some((name.to_string(), url.to_string()))
})
.collect()
}
pub fn primary_remote_url(&self) -> Option<String> {
self.primary_remote()
.ok()
.and_then(|remote| self.remote_url(&remote))
}
pub fn primary_remote_parsed_url(&self) -> Option<GitRemoteUrl> {
self.primary_remote_url()
.as_deref()
.and_then(GitRemoteUrl::parse)
}
pub fn project_identifier(&self) -> anyhow::Result<String> {
self.cache
.project_identifier
.get_or_try_init(|| {
if let Some(url) = self.primary_remote_url() {
if let Some(parsed) = GitRemoteUrl::parse(url.trim()) {
return Ok(parsed.project_identifier());
}
let url = url.strip_suffix(".git").unwrap_or(url.as_str());
return Ok(url.to_string());
}
let repo_root = self.repo_path()?;
let canonical =
dunce::canonicalize(repo_root).unwrap_or_else(|_| repo_root.to_path_buf());
let path_str = canonical
.to_str()
.context("Repository path is not valid UTF-8")?;
Ok(path_str.to_string())
})
.cloned()
}
pub fn url_template(&self) -> Option<String> {
self.load_project_config()
.ok()
.flatten()
.and_then(|config| config.list.url)
}
pub fn is_remote_tracking_branch(&self, ref_name: &str) -> bool {
self.run_command(&[
"rev-parse",
"--verify",
&format!("refs/remotes/{}", ref_name),
])
.is_ok()
}
pub fn strip_remote_prefix(&self, ref_name: &str) -> Option<String> {
if !self.is_remote_tracking_branch(ref_name) {
return None;
}
let output = self.run_command(&["remote"]).ok()?;
output.lines().find_map(|remote| {
let prefix = format!("{}/", remote.trim());
ref_name
.strip_prefix(&prefix)
.filter(|branch| !branch.is_empty())
.map(|branch| branch.to_string())
})
}
}