Skip to main content

git_policy_cli/config/
config_fsync.rs

1//! What:
2//!  Git's `core.fsync` value read into the set of components it flushes, and the value that keeps every component of a set
3//!  except the pack's own.
4//! Why:
5//!  Git flushes only the components `core.fsync` names, and the wrapper asks the same question for two writes it makes
6//!  itself: whether the index it writes in process is flushed, and whether the pack that `git fast-import` writes for a
7//!  landing is flushed. Read from Git 2.56.0 (`environment.c`, `parse_fsync_components`; `write-or-die.h`).
8//!
9//! In TS you'd write (pseudocode):
10//! ```ts
11//! const set = parseFsyncComponents('added,-pack'); // a bit set of components
12//! set.has('index'); // true
13//! ```
14
15/// A component of the repository that Git can flush, one bit each as in Git's `enum fsync_component`.
16const LOOSE_OBJECT: u8 = 1 << 0;
17/// A pack file.
18const PACK: u8 = 1 << 1;
19/// A pack's index and reverse index.
20const PACK_METADATA: u8 = 1 << 2;
21/// The commit-graph files.
22const COMMIT_GRAPH: u8 = 1 << 3;
23/// The index file.
24const INDEX: u8 = 1 << 4;
25/// The files of the reference store.
26const REFERENCE: u8 = 1 << 5;
27/// The mapping between hash algorithms; no `core.fsync` word names it alone, only `all`.
28const OBJECT_MAP: u8 = 1 << 6;
29
30/// What `objects` names.
31const OBJECTS: u8 = LOOSE_OBJECT | PACK;
32/// What `derived-metadata` names.
33const DERIVED_METADATA: u8 = PACK_METADATA | COMMIT_GRAPH;
34/// What Git flushes when `core.fsync` is not set: the objects and the derived metadata, but not the loose objects.
35const DEFAULT: u8 = (OBJECTS | DERIVED_METADATA) & !LOOSE_OBJECT;
36/// What `committed` names.
37const COMMITTED: u8 = OBJECTS | REFERENCE;
38/// What `added` names.
39const ADDED: u8 = COMMITTED | INDEX;
40/// What `all` names.
41const ALL: u8 = LOOSE_OBJECT | PACK | PACK_METADATA | COMMIT_GRAPH | INDEX | REFERENCE | OBJECT_MAP;
42
43/// The words `core.fsync` accepts, in the order Git lists them, with the components each names.
44const WORDS: [(&str, u8); 11] = [
45    ("loose-object", LOOSE_OBJECT),
46    ("pack", PACK),
47    ("pack-metadata", PACK_METADATA),
48    ("commit-graph", COMMIT_GRAPH),
49    ("index", INDEX),
50    ("objects", OBJECTS),
51    ("reference", REFERENCE),
52    ("derived-metadata", DERIVED_METADATA),
53    ("committed", COMMITTED),
54    ("added", ADDED),
55    ("all", ALL),
56];
57
58/// The separator characters Git skips before a word.
59const SEPARATORS: [char; 5] = [',', ' ', '\t', '\n', '\r'];
60
61/// What:
62///  The components one `core.fsync` value makes Git flush.
63/// Why:
64///   One value type for both questions, so the index writer and the import step cannot disagree about how Git reads the
65///  setting.
66///
67/// In TS you'd write (pseudocode):
68/// ```ts
69/// type FsyncComponents = { bits: number };
70/// ```
71#[derive(Clone, Copy, Debug, Eq, PartialEq)]
72pub struct FsyncComponents {
73    /// The bits of the components that are flushed.
74    bits: u8,
75}
76
77/// Reading and asking.
78impl FsyncComponents {
79    /// What:
80    ///  The set Git flushes when `core.fsync` is not set.
81    /// Why:
82    ///   An unset key is the default, which flushes packs and their metadata but not loose objects, references or the index.
83    ///
84    /// In TS you'd write (pseudocode):
85    /// ```ts
86    /// FsyncComponents.default()
87    /// ```
88    pub fn default_set() -> Self {
89        return Self { bits: DEFAULT };
90    }
91
92    /// What:
93    ///  The set one `core.fsync` value makes Git flush.
94    /// Why:
95    ///   The value starts again from the default at each entry. In one value `none` (only as the whole rest of the value)
96    ///  clears the set, a word with a `-` is removed and any other is added, a word names every component whose name it is a
97    ///  prefix of (`ind` is `index`, `pack` also names `pack-metadata`), a word that names none is ignored, and the result
98    ///  is the set without the removed components plus the added ones, so an added component stays even when it is also
99    ///  removed.
100    ///
101    /// In TS you'd write (pseudocode):
102    /// ```ts
103    /// FsyncComponents.parse('added,-pack')
104    /// ```
105    pub fn parse(value: &str) -> Self {
106        let mut current: u8 = DEFAULT;
107        let (mut positive, mut negative): (u8, u8) = (0, 0);
108        let mut rest: &str = value;
109        loop {
110            rest = rest.trim_start_matches(SEPARATORS);
111            let end: usize = rest.find(',').unwrap_or(rest.len());
112            if rest == "none" {
113                current = 0;
114            } else {
115                let (negated, word): (bool, &str) = match rest[..end].strip_prefix('-') {
116                    Some(removed) => (true, removed),
117                    None => (false, &rest[..end]),
118                };
119                // Git stops at an empty word, which is also how the end of the value reads.
120                if word.is_empty() {
121                    break;
122                }
123                for (name, bits) in WORDS {
124                    if name.starts_with(word) {
125                        if negated {
126                            negative |= bits;
127                        } else {
128                            positive |= bits;
129                        }
130                    }
131                }
132            }
133            if end >= rest.len() {
134                break;
135            }
136            rest = &rest[end..];
137        }
138        return Self { bits: (current & !negative) | positive };
139    }
140
141    /// Whether loose objects are flushed.
142    pub fn names_loose_object(self) -> bool {
143        return self.bits & LOOSE_OBJECT != 0;
144    }
145
146    /// Whether the index file is flushed.
147    pub fn names_index(self) -> bool {
148        return self.bits & INDEX != 0;
149    }
150
151    /// Whether a pack file or a pack's metadata is flushed.
152    pub fn names_pack(self) -> bool {
153        return self.bits & (PACK | PACK_METADATA) != 0;
154    }
155
156    /// Whether both a pack file and a pack's metadata are flushed.
157    pub fn names_pack_and_metadata(self) -> bool {
158        return self.bits & (PACK | PACK_METADATA) == PACK | PACK_METADATA;
159    }
160
161    /// Whether the files of the reference store are flushed.
162    pub fn names_reference(self) -> bool {
163        return self.bits & REFERENCE != 0;
164    }
165
166    /// What:
167    ///  This set without the pack file and the pack's metadata.
168    /// Why:
169    ///   The import step of a landing writes a temporary pack that it deletes when the batch is small, and native Git writes
170    ///  loose objects, so a repository that does not flush loose objects gets no pack flush for it.
171    ///
172    /// In TS you'd write (pseudocode):
173    /// ```ts
174    /// set.without('pack', 'pack-metadata')
175    /// ```
176    pub fn without_packs(self) -> Self {
177        return Self { bits: self.bits & !(PACK | PACK_METADATA) };
178    }
179
180    /// What:
181    ///  This set with the pack file and the pack's metadata added.
182    /// Why:
183    ///   Where loose objects are flushed, a pack that the import keeps instead of unpacking holds the same objects and is
184    ///  flushed with its index.
185    ///
186    /// In TS you'd write (pseudocode):
187    /// ```ts
188    /// set.with('pack', 'pack-metadata')
189    /// ```
190    pub fn with_packs(self) -> Self {
191        return Self { bits: self.bits | PACK | PACK_METADATA };
192    }
193
194    /// What:
195    ///  A `core.fsync` value that makes Git flush exactly this set.
196    /// Why:
197    ///   Each word is spelled in full, because a word is a prefix match (`pack` alone would also name `pack-metadata`), and
198    ///  `none` ends the value, which starts the set empty. A component that no single word names (the mapping between hash
199    ///  algorithms) is left out.
200    ///
201    /// In TS you'd write (pseudocode):
202    /// ```ts
203    /// set.toValue() // 'commit-graph,reference,none'
204    /// ```
205    pub fn value(self) -> String {
206        let mut words: Vec<&str> = Vec::new();
207        for (name, bits) in WORDS {
208            // Only the words that name one component are spelled; the grouped words would bring back what is not in the set.
209            if bits.is_power_of_two() && self.bits & bits != 0 {
210                words.push(name);
211            }
212        }
213        words.push("none");
214        return words.join(",");
215    }
216}
217
218/// Controls for reading the value, each against real Git's flushes.
219#[cfg(test)]
220#[path = "config_fsync_tests.rs"]
221mod tests;