grit_lib/
partial_clone.rs1use std::path::Path;
4
5use thiserror::Error;
6
7use crate::config::{ConfigFile, ConfigScope, ConfigSet};
8use crate::error::{Error, Result};
9use crate::rev_list::{expand_object_filter_for_protocol, ObjectFilter};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ObjectFilterSpec {
17 BlobNone,
19 BlobLimit(u64),
21}
22
23#[derive(Debug, Error)]
25pub enum PartialCloneError {
26 #[error("{0}")]
28 InvalidFilter(String),
29 #[error(transparent)]
31 Library(#[from] Error),
32}
33
34impl ObjectFilterSpec {
35 pub fn parse(spec: &str) -> std::result::Result<Self, PartialCloneError> {
41 let filter = ObjectFilter::parse(spec.trim()).map_err(PartialCloneError::InvalidFilter)?;
42 Self::from_object_filter(&filter)
43 }
44
45 pub fn from_object_filter(
51 filter: &ObjectFilter,
52 ) -> std::result::Result<Self, PartialCloneError> {
53 match filter {
54 ObjectFilter::BlobNone => Ok(Self::BlobNone),
55 ObjectFilter::BlobLimit(n) => Ok(Self::BlobLimit(*n)),
56 other => Err(PartialCloneError::InvalidFilter(format!(
57 "partial clone filter must be blob:none or blob:limit, not {other:?}"
58 ))),
59 }
60 }
61
62 #[must_use]
64 pub fn to_wire_spec(&self) -> String {
65 match self {
66 Self::BlobNone => "blob:none".to_owned(),
67 Self::BlobLimit(n) => format!("blob:limit={n}"),
68 }
69 }
70
71 #[must_use]
73 pub fn to_config_value(&self) -> String {
74 self.to_wire_spec()
75 }
76
77 pub fn expand_for_protocol(&self) -> std::result::Result<String, PartialCloneError> {
83 expand_object_filter_for_protocol(&self.to_wire_spec())
84 .map_err(PartialCloneError::InvalidFilter)
85 }
86
87 pub fn from_blobs_cli(value: &str) -> std::result::Result<Self, PartialCloneError> {
95 let trimmed = value.trim();
96 if trimmed.eq_ignore_ascii_case("none") {
97 return Ok(Self::BlobNone);
98 }
99 Self::parse(&format!("blob:limit={trimmed}"))
100 }
101
102 #[must_use]
104 pub const fn blob_limit_bytes(&self) -> Option<u64> {
105 match self {
106 Self::BlobNone => None,
107 Self::BlobLimit(n) => Some(*n),
108 }
109 }
110}
111
112pub struct PartialCloneConfig;
114
115impl PartialCloneConfig {
116 pub fn write(git_dir: &Path, remote: &str, spec: &ObjectFilterSpec) -> Result<()> {
125 let path = git_dir.join("config");
126 let content = std::fs::read_to_string(&path).map_err(Error::Io)?;
127 let mut config = ConfigFile::parse(&path, &content, ConfigScope::Local)?;
128 let filter = spec.to_config_value();
129 config.set(&format!("remote.{remote}.promisor"), "true")?;
130 config.set(
131 &format!("remote.{remote}.partialclonefilter"),
132 filter.as_str(),
133 )?;
134 config.set("extensions.partialclone", remote)?;
135 config.set("core.repositoryformatversion", "1")?;
136 config.write()?;
137 Ok(())
138 }
139}
140
141#[must_use]
143pub fn stored_remote_filter(config: &ConfigSet, remote: &str) -> Option<ObjectFilterSpec> {
144 config
145 .get(&format!("remote.{remote}.partialclonefilter"))
146 .and_then(|s| ObjectFilterSpec::parse(&s).ok())
147}
148
149#[must_use]
151pub fn remote_is_promisor(config: &ConfigSet, remote: &str) -> bool {
152 config
153 .get(&format!("remote.{remote}.promisor"))
154 .is_some_and(|v| v.eq_ignore_ascii_case("true"))
155 || config
156 .get(&format!("remote.{remote}.partialclonefilter"))
157 .is_some()
158}
159
160#[must_use]
165pub fn wire_filter_for_capabilities(
166 spec: Option<&ObjectFilterSpec>,
167 v2_fetch_caps: Option<&[String]>,
168 v0_v1_caps: Option<&str>,
169) -> (Option<String>, bool) {
170 let Some(spec) = spec else {
171 return (None, false);
172 };
173 let Ok(wire) = spec.expand_for_protocol() else {
174 return (None, false);
175 };
176 let supported = match v2_fetch_caps {
177 Some(caps) => crate::protocol_v2::fetch_supports_filter(caps),
178 None => {
179 v0_v1_caps.is_some_and(|caps| caps.split_whitespace().any(|token| token == "filter"))
180 }
181 };
182 if supported {
183 (Some(wire), true)
184 } else {
185 (None, false)
186 }
187}
188
189#[must_use]
192pub fn fetch_requires_upload_pack(
193 opts_filter: Option<&ObjectFilterSpec>,
194 config: &ConfigSet,
195 remote_name: Option<&str>,
196) -> bool {
197 if opts_filter.is_some() {
198 return true;
199 }
200 remote_name.is_some_and(|name| remote_is_promisor(config, name))
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206
207 #[test]
208 fn from_blobs_cli_rejects_overflowing_suffix() {
209 assert!(ObjectFilterSpec::from_blobs_cli("18446744073709551615g").is_err());
210 }
211
212 #[test]
213 fn from_blobs_cli_none_and_suffixes() {
214 assert_eq!(
215 ObjectFilterSpec::from_blobs_cli("none").expect("none"),
216 ObjectFilterSpec::BlobNone
217 );
218 assert_eq!(
219 ObjectFilterSpec::from_blobs_cli("1k").expect("1k"),
220 ObjectFilterSpec::BlobLimit(1024)
221 );
222 assert!(ObjectFilterSpec::from_blobs_cli("not-a-size").is_err());
223 }
224
225 #[test]
226 fn parse_blob_none_and_limit() {
227 assert_eq!(
228 ObjectFilterSpec::parse("blob:none").expect("none"),
229 ObjectFilterSpec::BlobNone
230 );
231 assert_eq!(
232 ObjectFilterSpec::parse("blob:limit=512").expect("limit"),
233 ObjectFilterSpec::BlobLimit(512)
234 );
235 assert!(ObjectFilterSpec::parse("tree:0").is_err());
236 }
237
238 #[test]
239 fn wire_filter_dropped_without_server_cap() {
240 let spec = ObjectFilterSpec::BlobNone;
241 let (wire, send) = wire_filter_for_capabilities(Some(&spec), Some(&[]), None);
242 assert!(!send);
243 assert!(wire.is_none());
244
245 let (wire, send) =
246 wire_filter_for_capabilities(Some(&spec), None, Some("multi_ack side-band-64k"));
247 assert!(!send);
248 assert!(wire.is_none());
249
250 let (wire, send) =
251 wire_filter_for_capabilities(Some(&spec), Some(&["fetch=filter".to_owned()]), None);
252 assert!(send);
253 assert_eq!(wire.as_deref(), Some("blob:none"));
254 }
255}