use std::path::Path;
use thiserror::Error;
use crate::config::{ConfigFile, ConfigScope, ConfigSet};
use crate::error::{Error, Result};
use crate::rev_list::{expand_object_filter_for_protocol, ObjectFilter};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ObjectFilterSpec {
BlobNone,
BlobLimit(u64),
}
#[derive(Debug, Error)]
pub enum PartialCloneError {
#[error("{0}")]
InvalidFilter(String),
#[error(transparent)]
Library(#[from] Error),
}
impl ObjectFilterSpec {
pub fn parse(spec: &str) -> std::result::Result<Self, PartialCloneError> {
let filter = ObjectFilter::parse(spec.trim()).map_err(PartialCloneError::InvalidFilter)?;
Self::from_object_filter(&filter)
}
pub fn from_object_filter(
filter: &ObjectFilter,
) -> std::result::Result<Self, PartialCloneError> {
match filter {
ObjectFilter::BlobNone => Ok(Self::BlobNone),
ObjectFilter::BlobLimit(n) => Ok(Self::BlobLimit(*n)),
other => Err(PartialCloneError::InvalidFilter(format!(
"partial clone filter must be blob:none or blob:limit, not {other:?}"
))),
}
}
#[must_use]
pub fn to_wire_spec(&self) -> String {
match self {
Self::BlobNone => "blob:none".to_owned(),
Self::BlobLimit(n) => format!("blob:limit={n}"),
}
}
#[must_use]
pub fn to_config_value(&self) -> String {
self.to_wire_spec()
}
pub fn expand_for_protocol(&self) -> std::result::Result<String, PartialCloneError> {
expand_object_filter_for_protocol(&self.to_wire_spec())
.map_err(PartialCloneError::InvalidFilter)
}
}
pub struct PartialCloneConfig;
impl PartialCloneConfig {
pub fn write(git_dir: &Path, remote: &str, spec: &ObjectFilterSpec) -> Result<()> {
let path = git_dir.join("config");
let content = std::fs::read_to_string(&path).map_err(Error::Io)?;
let mut config = ConfigFile::parse(&path, &content, ConfigScope::Local)?;
let filter = spec.to_config_value();
config.set(&format!("remote.{remote}.promisor"), "true")?;
config.set(
&format!("remote.{remote}.partialclonefilter"),
filter.as_str(),
)?;
config.set("extensions.partialclone", remote)?;
config.set("core.repositoryformatversion", "1")?;
config.write()?;
Ok(())
}
}
#[must_use]
pub fn stored_remote_filter(config: &ConfigSet, remote: &str) -> Option<ObjectFilterSpec> {
config
.get(&format!("remote.{remote}.partialclonefilter"))
.and_then(|s| ObjectFilterSpec::parse(&s).ok())
}
#[must_use]
pub fn remote_is_promisor(config: &ConfigSet, remote: &str) -> bool {
config
.get(&format!("remote.{remote}.promisor"))
.is_some_and(|v| v.eq_ignore_ascii_case("true"))
|| config
.get(&format!("remote.{remote}.partialclonefilter"))
.is_some()
}
#[must_use]
pub fn wire_filter_for_capabilities(
spec: Option<&ObjectFilterSpec>,
v2_fetch_caps: Option<&[String]>,
v0_v1_caps: Option<&str>,
) -> (Option<String>, bool) {
let Some(spec) = spec else {
return (None, false);
};
let Ok(wire) = spec.expand_for_protocol() else {
return (None, false);
};
let supported = match v2_fetch_caps {
Some(caps) => crate::protocol_v2::fetch_supports_filter(caps),
None => {
v0_v1_caps.is_some_and(|caps| caps.split_whitespace().any(|token| token == "filter"))
}
};
if supported {
(Some(wire), true)
} else {
(None, false)
}
}
#[must_use]
pub fn fetch_requires_upload_pack(
opts_filter: Option<&ObjectFilterSpec>,
config: &ConfigSet,
remote_name: Option<&str>,
) -> bool {
if opts_filter.is_some() {
return true;
}
remote_name.is_some_and(|name| remote_is_promisor(config, name))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_blob_none_and_limit() {
assert_eq!(
ObjectFilterSpec::parse("blob:none").expect("none"),
ObjectFilterSpec::BlobNone
);
assert_eq!(
ObjectFilterSpec::parse("blob:limit=512").expect("limit"),
ObjectFilterSpec::BlobLimit(512)
);
assert!(ObjectFilterSpec::parse("tree:0").is_err());
}
#[test]
fn wire_filter_dropped_without_server_cap() {
let spec = ObjectFilterSpec::BlobNone;
let (wire, send) = wire_filter_for_capabilities(Some(&spec), Some(&[]), None);
assert!(!send);
assert!(wire.is_none());
let (wire, send) =
wire_filter_for_capabilities(Some(&spec), None, Some("multi_ack side-band-64k"));
assert!(!send);
assert!(wire.is_none());
let (wire, send) =
wire_filter_for_capabilities(Some(&spec), Some(&["fetch=filter".to_owned()]), None);
assert!(send);
assert_eq!(wire.as_deref(), Some("blob:none"));
}
}