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;