use super::configured_url::{normalize, parse};
use super::user_url::NormalizedUrl;
use super::{
ConfiguredRefspec, ConfiguredRefspecError, ConfiguredUrlError, ConfiguredUrlParts, Direction,
};
use crate::Config;
#[derive(Clone)]
pub struct ConfiguredRemote {
fetch_url: Option<NormalizedUrl>,
push_url: Option<NormalizedUrl>,
fetch_refspecs: Vec<ConfiguredRefspec>,
push_refspecs: Vec<ConfiguredRefspec>,
}
impl std::fmt::Debug for ConfiguredRemote {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConfiguredRemote")
.field("has_fetch_url", &self.fetch_url.is_some())
.field("has_push_url", &self.push_url.is_some())
.field("fetch_refspec_count", &self.fetch_refspecs.len())
.field("push_refspec_count", &self.push_refspecs.len())
.finish()
}
}
#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ConfiguredRemoteError {
#[error("remote tagOpt must be --tags or --no-tags")]
TagOption,
#[error("unsupported remote tagOpt inheritance across configuration sections")]
UnsupportedTagOptionInheritance,
#[error(
"invalid remote {key} (value {occurrence}{})",
if *rewritten { ", after an insteadOf rewrite" } else { "" }
)]
Url {
key: &'static str,
occurrence: usize,
rewritten: bool,
#[source]
source: ConfiguredUrlError,
},
#[error("invalid remote {key} refspec (value {occurrence})")]
Refspec {
key: &'static str,
occurrence: usize,
#[source]
source: ConfiguredRefspecError,
},
#[error(
"unsupported remote {key} syntax (value {occurrence}{})",
if *rewritten { ", after an insteadOf rewrite" } else { "" }
)]
UnsupportedUrlSyntax {
key: &'static str,
occurrence: usize,
rewritten: bool,
},
}
impl ConfiguredRemote {
pub fn find(config: &Config, name: &[u8]) -> Result<Option<Self>, ConfiguredRemoteError> {
let Some(record) = ConfiguredRemoteRecord::find(config, name)? else {
return Ok(None);
};
let ConfiguredRemoteRecord {
fetch,
push,
fetch_refspecs,
push_refspecs,
..
} = record;
let rewritten_fetch = rewrite_urls(config, &fetch, "url")?;
let rewritten_push = if push.is_empty() {
push_fallback_urls(config, &fetch, &rewritten_fetch)?
} else {
rewrite_urls(config, &push, "pushurl")?
};
if fetch.is_empty() && push.is_empty() {
return Ok(None);
}
Ok(Some(Self {
fetch_url: rewritten_fetch.into_iter().next(),
push_url: rewritten_push.into_iter().next(),
fetch_refspecs,
push_refspecs,
}))
}
pub fn fetch_url(&self) -> Option<&[u8]> {
self.fetch_url.as_ref().map(|url| url.bytes.as_slice())
}
pub fn push_url(&self) -> Option<&[u8]> {
self.push_url.as_ref().map(|url| url.bytes.as_slice())
}
pub fn fetch_url_parts(&self) -> Option<&ConfiguredUrlParts> {
self.fetch_url.as_ref().map(|url| &url.parts)
}
pub fn push_url_parts(&self) -> Option<&ConfiguredUrlParts> {
self.push_url.as_ref().map(|url| &url.parts)
}
pub fn fetch_refspecs(&self) -> &[ConfiguredRefspec] {
&self.fetch_refspecs
}
pub fn push_refspecs(&self) -> &[ConfiguredRefspec] {
&self.push_refspecs
}
}
#[derive(Clone)]
pub struct ConfiguredRemoteRecord {
fetch: Vec<UrlOccurrence>,
push: Vec<UrlOccurrence>,
fetch_refspecs: Vec<ConfiguredRefspec>,
push_refspecs: Vec<ConfiguredRefspec>,
tag_option: Option<&'static [u8]>,
}
impl std::fmt::Debug for ConfiguredRemoteRecord {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ConfiguredRemoteRecord")
.field("fetch_url_count", &self.fetch.len())
.field("push_url_count", &self.push.len())
.field("fetch_refspec_count", &self.fetch_refspecs.len())
.field("push_refspec_count", &self.push_refspecs.len())
.field("tag_option", &self.tag_option)
.finish()
}
}
impl ConfiguredRemoteRecord {
pub fn find(config: &Config, name: &[u8]) -> Result<Option<Self>, ConfiguredRemoteError> {
if !config.contains_section("remote", Some(name)) {
return Ok(None);
}
let selected_tag_option = config
.section_occurrences()
.filter(|section| {
section.name().eq_ignore_ascii_case(b"remote") && section.subsection() == Some(name)
})
.filter_map(|section| {
section
.entry_indices()
.iter()
.rev()
.map(|&index| &config.entries()[index])
.find(|entry| entry.name.eq_ignore_ascii_case(b"tagopt"))
.and_then(|entry| entry.value.as_deref())
})
.last();
let tag_option = match selected_tag_option {
None => None,
Some(b"--tags") => Some(b"--tags".as_slice()),
Some(b"--no-tags") => Some(b"--no-tags".as_slice()),
_ => return Err(ConfiguredRemoteError::TagOption),
};
let fetch = configured_urls(config, name, "url")?;
let push = configured_urls(config, name, "pushurl")?;
let fetch_refspecs = configured_refspecs(config, name, "fetch", Direction::Fetch)?;
let push_refspecs = configured_refspecs(config, name, "push", Direction::Push)?;
Ok(Some(Self {
fetch,
push,
fetch_refspecs,
push_refspecs,
tag_option,
}))
}
pub fn fetch_urls(&self) -> impl ExactSizeIterator<Item = &[u8]> {
self.fetch.iter().map(|url| url.url.bytes.as_slice())
}
pub fn push_urls(&self) -> impl ExactSizeIterator<Item = &[u8]> {
self.push.iter().map(|url| url.url.bytes.as_slice())
}
pub fn fetch_refspecs(&self) -> &[ConfiguredRefspec] {
&self.fetch_refspecs
}
pub fn push_refspecs(&self) -> &[ConfiguredRefspec] {
&self.push_refspecs
}
pub fn tag_option(&self) -> Option<&[u8]> {
self.tag_option
}
}
#[derive(Clone)]
struct UrlOccurrence {
index: usize,
url: NormalizedUrl,
}
fn configured_urls(
config: &Config,
name: &[u8],
key: &'static str,
) -> Result<Vec<UrlOccurrence>, ConfiguredRemoteError> {
let mut surviving = Vec::new();
for (index, value) in config.values("remote", Some(name), key).enumerate() {
match value {
None | Some(b"") => surviving.clear(),
Some(bytes) => surviving.push((index + 1, bytes)),
}
}
surviving
.into_iter()
.map(|(index, bytes)| {
parse(bytes)
.map(|url| UrlOccurrence { index, url })
.map_err(|err| url_error(err, key, index, false))
})
.collect()
}
fn configured_refspecs(
config: &Config,
name: &[u8],
key: &'static str,
direction: Direction,
) -> Result<Vec<ConfiguredRefspec>, ConfiguredRemoteError> {
config
.values("remote", Some(name), key)
.enumerate()
.map(|(index, value)| {
ConfiguredRefspec::parse(direction, value.unwrap_or_default()).map_err(|source| {
ConfiguredRemoteError::Refspec {
key,
occurrence: index + 1,
source,
}
})
})
.collect()
}
pub fn rewrite_configured_url(
config: &Config,
bytes: &[u8],
) -> Result<Vec<u8>, ConfiguredUrlError> {
let original = parse(bytes)?;
match rewrite(config, &original, "insteadof")? {
Some(rewritten) => normalize(&rewritten),
None => Ok(original.bytes),
}
}
fn rewrite_urls(
config: &Config,
urls: &[UrlOccurrence],
key: &'static str,
) -> Result<Vec<NormalizedUrl>, ConfiguredRemoteError> {
urls.iter()
.map(|url| {
let rewritten = rewrite(config, &url.url, "insteadof")
.map_err(|err| url_error(err, key, url.index, true))?;
match rewritten {
Some(bytes) => parse(&bytes).map_err(|err| url_error(err, key, url.index, true)),
None => Ok(url.url.clone()),
}
})
.collect()
}
fn push_fallback_urls(
config: &Config,
originals: &[UrlOccurrence],
ordinary: &[NormalizedUrl],
) -> Result<Vec<NormalizedUrl>, ConfiguredRemoteError> {
originals
.iter()
.zip(ordinary)
.map(|(url, ordinary)| {
let rewritten = rewrite(config, &url.url, "pushinsteadof")
.map_err(|err| url_error(err, "url", url.index, true))?;
let Some(bytes) = rewritten else {
return Ok(ordinary.clone());
};
match parse(&bytes) {
Ok(url) => Ok(url),
Err(ConfiguredUrlError::Unsupported) => Err(url_error(
ConfiguredUrlError::Unsupported,
"url",
url.index,
true,
)),
Err(_) => Ok(url.url.clone()),
}
})
.collect()
}
fn rewrite(
config: &Config,
url: &NormalizedUrl,
key: &str,
) -> Result<Option<Vec<u8>>, ConfiguredUrlError> {
let ssh_ipv6 = url.bytes.starts_with(b"ssh://")
&& url.parts.host().is_some_and(|host| {
std::str::from_utf8(host)
.ok()
.and_then(|host| host.parse::<std::net::Ipv6Addr>().ok())
.is_some()
});
let mut selected: Option<(&[u8], &[u8])> = None;
for entry in config.entries() {
if !entry.section.eq_ignore_ascii_case(b"url")
|| !entry.name.eq_ignore_ascii_case(key.as_bytes())
{
continue;
}
let Some(base) = entry.subsection.as_deref() else {
continue;
};
if ssh_ipv6 {
return Err(ConfiguredUrlError::Unsupported);
}
let prefix = entry.value.as_deref().unwrap_or_default();
if url.bytes.starts_with(prefix) && selected.is_none_or(|(_, old)| prefix.len() > old.len())
{
selected = Some((base, prefix));
}
}
Ok(selected.map(|(base, prefix)| [base, &url.bytes[prefix.len()..]].concat()))
}
fn url_error(
source: ConfiguredUrlError,
key: &'static str,
occurrence: usize,
rewritten: bool,
) -> ConfiguredRemoteError {
if source == ConfiguredUrlError::Unsupported {
ConfiguredRemoteError::UnsupportedUrlSyntax {
key,
occurrence,
rewritten,
}
} else {
ConfiguredRemoteError::Url {
key,
occurrence,
rewritten,
source,
}
}
}
#[cfg(test)]
#[path = "configured_remote_tests.rs"]
mod tests;