use std::path::Path;
use std::process::Command;
use crate::Error;
#[derive(Debug, Default, Clone)]
pub struct HttpOptions {
pub ssl_ca_info: Option<String>,
pub ssl_verify: Option<bool>,
pub ssl_cert: Option<String>,
pub ssl_key: Option<String>,
pub cookie_file: Option<String>,
}
impl HttpOptions {
pub fn for_url(cwd: &Path, url: &str) -> Result<Self, Error> {
let scoped = scoped_keys(cwd, url)?;
Ok(Self {
ssl_ca_info: scoped
.lookup("sslcainfo")
.or_else(|| get_global(cwd, "http.sslcainfo").ok().flatten()),
ssl_verify: scoped
.lookup("sslverify")
.or_else(|| get_global(cwd, "http.sslVerify").ok().flatten())
.map(|v| parse_bool(&v)),
ssl_cert: scoped
.lookup("sslcert")
.or_else(|| get_global(cwd, "http.sslCert").ok().flatten()),
ssl_key: scoped
.lookup("sslkey")
.or_else(|| get_global(cwd, "http.sslKey").ok().flatten()),
cookie_file: scoped
.lookup("cookiefile")
.or_else(|| get_global(cwd, "http.cookieFile").ok().flatten()),
})
}
}
struct Scoped(Vec<(String, String, String)>);
impl Scoped {
fn lookup(&self, key: &str) -> Option<String> {
let key = key.to_ascii_lowercase();
self.0
.iter()
.find(|(_, k, _)| k.to_ascii_lowercase() == key)
.map(|(_, _, v)| v.clone())
}
}
fn scoped_keys(cwd: &Path, url: &str) -> Result<Scoped, Error> {
let out = Command::new("git")
.arg("-C")
.arg(cwd)
.args([
"config",
"--includes",
"--null",
"--get-regexp",
r"^http\..+\..+$",
])
.output()?;
if !out.status.success() {
return Ok(Scoped(Vec::new()));
}
let raw = String::from_utf8_lossy(&out.stdout);
let mut entries: Vec<(String, String, String)> = Vec::new();
for record in raw.split('\0').filter(|s| !s.is_empty()) {
let (key_full, value) = match record.split_once('\n') {
Some((k, v)) => (k, v),
None => (record, ""),
};
let parts: Vec<&str> = key_full.splitn(2, '.').collect();
if parts.len() != 2 || parts[0] != "http" {
continue;
}
let rest = parts[1];
let last_dot = rest.rfind('.').unwrap_or(rest.len());
if last_dot == rest.len() {
continue;
}
let prefix = &rest[..last_dot];
let subkey = &rest[last_dot + 1..];
if url_matches(prefix, url) {
entries.push((prefix.to_owned(), subkey.to_owned(), value.to_owned()));
}
}
entries.sort_by_key(|e| std::cmp::Reverse(e.0.len()));
Ok(Scoped(entries))
}
fn get_global(cwd: &Path, key: &str) -> Result<Option<String>, Error> {
let out = Command::new("git")
.arg("-C")
.arg(cwd)
.args(["config", "--includes", "--get", key])
.output()?;
match out.status.code() {
Some(0) => Ok(Some(String::from_utf8_lossy(&out.stdout).trim().to_owned())),
Some(1) | Some(128) | Some(129) => Ok(None),
_ => Err(Error::Failed(
String::from_utf8_lossy(&out.stderr).trim().to_owned(),
)),
}
}
fn get_all_global(cwd: &Path, key: &str) -> Vec<String> {
let out = Command::new("git")
.arg("-C")
.arg(cwd)
.args(["config", "--includes", "--get-all", key])
.output();
match out {
Ok(o) if o.status.success() => String::from_utf8_lossy(&o.stdout)
.lines()
.map(str::to_owned)
.collect(),
_ => Vec::new(),
}
}
pub fn extra_headers_for(cwd: &Path, url: &str) -> Vec<(String, String)> {
let mut out: Vec<(String, String)> = Vec::new();
if let Ok(scoped) = scoped_keys(cwd, url) {
for (_prefix, subkey, value) in &scoped.0 {
if !subkey.eq_ignore_ascii_case("extraheader") {
continue;
}
if let Some(pair) = parse_header_line(value) {
out.push(pair);
}
}
}
for value in get_all_global(cwd, "http.extraHeader") {
if let Some(pair) = parse_header_line(&value) {
out.push(pair);
}
}
out
}
fn parse_header_line(s: &str) -> Option<(String, String)> {
let (name, value) = s.split_once(':')?;
let name = name.trim();
if name.is_empty() {
return None;
}
Some((name.to_owned(), value.trim().to_owned()))
}
pub fn lfs_url_bool(cwd: &Path, url: &str, subkey: &str, default: bool) -> bool {
let scoped = lfs_scoped_keys(cwd, url).unwrap_or(Scoped(Vec::new()));
if let Some(v) = scoped.lookup(subkey) {
return parse_bool(&v);
}
let global_key = format!("lfs.{subkey}");
match get_global(cwd, &global_key) {
Ok(Some(v)) => parse_bool(&v),
_ => default,
}
}
fn lfs_scoped_keys(cwd: &Path, url: &str) -> Result<Scoped, Error> {
let out = Command::new("git")
.arg("-C")
.arg(cwd)
.args([
"config",
"--includes",
"--null",
"--get-regexp",
r"^lfs\..+\..+$",
])
.output()?;
if !out.status.success() {
return Ok(Scoped(Vec::new()));
}
let raw = String::from_utf8_lossy(&out.stdout);
let mut entries: Vec<(String, String, String)> = Vec::new();
for record in raw.split('\0').filter(|s| !s.is_empty()) {
let (key_full, value) = match record.split_once('\n') {
Some((k, v)) => (k, v),
None => (record, ""),
};
let parts: Vec<&str> = key_full.splitn(2, '.').collect();
if parts.len() != 2 || parts[0] != "lfs" {
continue;
}
let rest = parts[1];
let Some(last_dot) = rest.rfind('.') else {
continue;
};
let prefix = &rest[..last_dot];
let subkey = &rest[last_dot + 1..];
if url_matches(prefix, url) {
entries.push((prefix.to_owned(), subkey.to_owned(), value.to_owned()));
}
}
entries.sort_by_key(|e| std::cmp::Reverse(e.0.len()));
Ok(Scoped(entries))
}
fn url_matches(prefix: &str, url: &str) -> bool {
let p = prefix.trim_end_matches('/').to_ascii_lowercase();
let u = url.trim_end_matches('/').to_ascii_lowercase();
u == p || u.starts_with(&format!("{p}/"))
}
fn parse_bool(s: &str) -> bool {
matches!(
s.trim().to_ascii_lowercase().as_str(),
"true" | "1" | "yes" | "on"
)
}