1use std::env;
21use std::fs;
22use std::io;
23use std::path::Path;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
28pub struct FsyncComponents(u32);
29
30impl FsyncComponents {
31 pub const NONE: Self = Self(0);
34 pub const LOOSE_OBJECT: Self = Self(1 << 0);
35 pub const PACK: Self = Self(1 << 1);
36 pub const PACK_METADATA: Self = Self(1 << 2);
37 pub const COMMIT_GRAPH: Self = Self(1 << 3);
38 pub const INDEX: Self = Self(1 << 4);
39 pub const REFERENCE: Self = Self(1 << 5);
40 pub const OBJECT_MAP: Self = Self(1 << 6);
41
42 pub const OBJECTS: Self = Self(Self::LOOSE_OBJECT.0 | Self::PACK.0);
44
45 pub const DERIVED_METADATA: Self = Self(Self::PACK_METADATA.0 | Self::COMMIT_GRAPH.0);
47
48 pub const DEFAULT: Self = Self(
50 (Self::OBJECTS.0 | Self::DERIVED_METADATA.0) & !Self::LOOSE_OBJECT.0,
51 );
52
53 pub const COMMITTED: Self = Self(Self::OBJECTS.0 | Self::REFERENCE.0);
55
56 pub const ADDED: Self = Self(Self::COMMITTED.0 | Self::INDEX.0);
58
59 pub const ALL: Self = Self(
61 Self::LOOSE_OBJECT.0
62 | Self::PACK.0
63 | Self::PACK_METADATA.0
64 | Self::COMMIT_GRAPH.0
65 | Self::INDEX.0
66 | Self::REFERENCE.0
67 | Self::OBJECT_MAP.0,
68 );
69
70 pub const PLATFORM_DEFAULT: Self = Self::DEFAULT;
75
76 const COMPONENT_TABLE: [(&str, Self); 11] = [
79 ("loose-object", Self::LOOSE_OBJECT),
80 ("pack", Self::PACK),
81 ("pack-metadata", Self::PACK_METADATA),
82 ("commit-graph", Self::COMMIT_GRAPH),
83 ("index", Self::INDEX),
84 ("objects", Self::OBJECTS),
85 ("reference", Self::REFERENCE),
86 ("derived-metadata", Self::DERIVED_METADATA),
87 ("committed", Self::COMMITTED),
88 ("added", Self::ADDED),
89 ("all", Self::ALL),
90 ];
91
92 pub fn parse(value: &str) -> Self {
103 let mut current = Self::PLATFORM_DEFAULT;
104 let mut positive = Self::NONE;
105 let mut negative = Self::NONE;
106 for raw_component in value.split(',') {
107 let component = raw_component.trim();
108 if component == "none" {
109 current = Self::NONE;
110 continue;
111 }
112 if component.is_empty() {
113 continue;
114 }
115 let Some(name) = component.strip_prefix('-') else {
116 for (table_name, bits) in Self::COMPONENT_TABLE {
117 if table_name.starts_with(component) {
118 positive = positive.union(bits);
119 }
120 }
121 continue;
122 };
123 if name.is_empty() {
124 break;
125 }
126 for (table_name, bits) in Self::COMPONENT_TABLE {
127 if table_name.starts_with(name) {
128 negative = negative.union(bits);
129 }
130 }
131 }
132 current.without(negative).union(positive)
133 }
134
135 pub const fn contains(self, other: Self) -> bool {
137 self.0 & other.0 == other.0
138 }
139
140 pub const fn union(self, other: Self) -> Self {
142 Self(self.0 | other.0)
143 }
144
145 pub const fn without(self, other: Self) -> Self {
147 Self(self.0 & !other.0)
148 }
149
150 pub const fn bits(self) -> u32 {
152 self.0
153 }
154
155 pub const fn includes_reference(self) -> bool {
158 self.contains(Self::REFERENCE)
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum FsyncMethod {
166 Fsync,
168 WriteoutOnly,
170 Batch,
173}
174
175impl FsyncMethod {
176 pub const fn platform_default() -> Self {
180 #[cfg(target_os = "windows")]
181 {
182 Self::Batch
183 }
184 #[cfg(all(not(target_os = "windows"), target_os = "macos"))]
185 {
186 Self::WriteoutOnly
187 }
188 #[cfg(not(any(target_os = "windows", target_os = "macos")))]
189 {
190 Self::Fsync
191 }
192 }
193
194 pub fn from_config(value: Option<&str>) -> Self {
197 match value {
198 Some("fsync") => Self::Fsync,
199 Some("writeout-only") => Self::WriteoutOnly,
200 Some("batch") => Self::Batch,
201 _ => Self::platform_default(),
202 }
203 }
204
205 pub fn apply(self, file: &fs::File) -> io::Result<()> {
209 match self {
210 Self::WriteoutOnly => file.sync_data(),
211 Self::Fsync | Self::Batch => file.sync_all(),
212 }
213 }
214}
215
216pub fn test_fsync_enabled() -> bool {
220 let Ok(value) = env::var("GIT_TEST_FSYNC") else {
221 return true;
222 };
223 !matches!(
224 value.to_ascii_lowercase().as_str(),
225 "0" | "false" | "no" | "off" | ""
226 )
227}
228
229pub trait FsyncConfigSource {
236 fn fsync_lookup(&self, section: &str, subsection: Option<&str>, key: &str) -> Option<&str>;
239}
240
241#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub struct Policy {
248 components: FsyncComponents,
249 method: FsyncMethod,
250 use_fsync: bool,
251}
252
253impl Default for Policy {
254 fn default() -> Self {
255 Self::from_values(None, None)
256 }
257}
258
259impl Policy {
260 pub fn from_values(core_fsync: Option<&str>, core_fsync_method: Option<&str>) -> Self {
263 Self {
264 components: core_fsync.map_or(FsyncComponents::PLATFORM_DEFAULT, FsyncComponents::parse),
265 method: FsyncMethod::from_config(core_fsync_method),
266 use_fsync: test_fsync_enabled(),
267 }
268 }
269
270 pub fn resolve(config: &impl FsyncConfigSource) -> Self {
274 Self::from_values(
275 config.fsync_lookup("core", None, "fsync"),
276 config.fsync_lookup("core", None, "fsyncMethod"),
277 )
278 }
279
280 pub fn overridden(mut self, core_fsync: Option<&str>, core_fsync_method: Option<&str>) -> Self {
285 if let Some(value) = core_fsync {
286 self.components = FsyncComponents::parse(value);
287 }
288 if let Some(value) = core_fsync_method {
289 self.method = FsyncMethod::from_config(Some(value));
290 }
291 self.use_fsync = test_fsync_enabled();
292 self
293 }
294
295 pub const fn components(&self) -> FsyncComponents {
297 self.components
298 }
299
300 pub const fn method(&self) -> FsyncMethod {
302 self.method
303 }
304
305 pub const fn syncs(&self, component: FsyncComponents) -> bool {
308 self.use_fsync && self.components.contains(component)
309 }
310
311 pub const fn method_if_enabled(&self, component: FsyncComponents) -> Option<FsyncMethod> {
315 if self.syncs(component) {
316 Some(self.method)
317 } else {
318 None
319 }
320 }
321
322 pub fn apply(&self, file: &fs::File, component: FsyncComponents) -> io::Result<()> {
325 match self.method_if_enabled(component) {
326 Some(method) => method.apply(file),
327 None => Ok(()),
328 }
329 }
330}
331
332pub fn sync_file(path: &Path, policy: &Policy, component: FsyncComponents) -> io::Result<()> {
339 let file = fs::OpenOptions::new().write(true).open(path)?;
340 policy.apply(&file, component)
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn component_bits_match_upstream_layout() {
349 assert_eq!(FsyncComponents::OBJECTS.bits(), 0b0000_0011);
350 assert_eq!(FsyncComponents::DERIVED_METADATA.bits(), 0b0000_1100);
351 assert_eq!(
353 FsyncComponents::DEFAULT.bits(),
354 FsyncComponents::PACK.bits()
355 | FsyncComponents::PACK_METADATA.bits()
356 | FsyncComponents::COMMIT_GRAPH.bits()
357 );
358 assert_eq!(
359 FsyncComponents::COMMITTED.bits(),
360 FsyncComponents::OBJECTS.union(FsyncComponents::REFERENCE).bits()
361 );
362 assert_eq!(
363 FsyncComponents::ADDED.bits(),
364 FsyncComponents::COMMITTED
365 .union(FsyncComponents::INDEX)
366 .bits()
367 );
368 assert_eq!(FsyncComponents::ALL.bits(), 0b0111_1111);
369 }
370
371 #[test]
372 fn parse_matches_upstream_groups_negation_and_prefixing() {
373 let reference = FsyncComponents::REFERENCE;
374 assert!(!FsyncComponents::parse("none").contains(reference));
375 assert!(FsyncComponents::parse("none,reference").contains(reference));
377 assert!(!FsyncComponents::parse("none,-reference").contains(reference));
378 assert!(!FsyncComponents::parse("objects,index").contains(reference));
379 assert!(!FsyncComponents::parse("-reference").contains(reference));
380 for value in ["reference", "ref", "committed", "added", "all"] {
381 assert!(
382 FsyncComponents::parse(value).contains(reference),
383 "{value} must include references"
384 );
385 }
386 assert!(FsyncComponents::parse("reference,-reference").contains(reference));
388 assert!(FsyncComponents::parse("-reference,reference").contains(reference));
389 assert!(FsyncComponents::parse("reference,none").contains(reference));
391 assert!(FsyncComponents::parse(
392 "committed,-loose-object"
393 )
394 .contains(reference));
395 assert!(FsyncComponents::parse("pack").contains(FsyncComponents::PACK_METADATA));
398 assert_eq!(
400 FsyncComponents::parse("nonsense").bits(),
401 FsyncComponents::PLATFORM_DEFAULT.bits()
402 );
403 }
404
405 #[test]
406 fn policy_gating_honors_components_and_test_switch() {
407 let enabled = Policy::from_values(Some("reference"), Some("writeout-only"));
408 assert!(enabled.syncs(FsyncComponents::REFERENCE) || !test_fsync_enabled());
409 if test_fsync_enabled() {
410 assert_eq!(
411 enabled.method_if_enabled(FsyncComponents::REFERENCE),
412 Some(FsyncMethod::WriteoutOnly)
413 );
414 assert_eq!(enabled.method_if_enabled(FsyncComponents::INDEX), None);
415 }
416
417 let disabled = Policy::from_values(Some("none"), Some("fsync"));
418 assert_eq!(disabled.method_if_enabled(FsyncComponents::REFERENCE), None);
419
420 let default = Policy::from_values(None, None);
423 assert!(!default.components().contains(FsyncComponents::REFERENCE));
424
425 let overridden = disabled.overridden(None, Some("batch"));
427 assert_eq!(overridden.method(), FsyncMethod::Batch);
428 let flipped = default.overridden(Some("all"), None);
429 if test_fsync_enabled() {
430 assert_eq!(
431 flipped.method_if_enabled(FsyncComponents::REFERENCE),
432 Some(FsyncMethod::platform_default())
433 );
434 }
435 }
436}