Skip to main content

git_policy_cli/
transaction_journal_encode.rs

1//! What:
2//!  The exact encoding of every schema-version-2 journal record:
3//!  compact JSON in the
4//!       incumbent's field order,
5//!  one record per line.
6//! Why:
7//!  `writeJournalRecord` writes `JSON.stringify(record)` and a newline;
8//!  the native wrapper
9//!      writes the same bytes for the same values so either wrapper can recover the other's
10//!      transactions.
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! `${JSON.stringify(record)}\n`
15//! ```
16
17/// The ordered writer and array encoding.
18use super::json_record::{ObjectWriter, string_array};
19/// The record types and their wire names.
20use super::transaction_journal::{
21    AddedPath, Base, Conclusion, FileIdentity, IndexLockRecord, JOURNAL_SCHEMA_VERSION,
22    LandingOperation, LandingRecord, LockIdentity, PreparedRecord, PreparingRecord, RefFormat,
23    SymbolicHead, TransactionMode,
24};
25
26/// What:
27///  Encode a base as its tagged object.
28/// Why:
29///   Shared by `preparing.json` and `landing-<n>.json`.
30///
31/// In TS you'd write (pseudocode):
32/// ```ts
33/// JSON.stringify(base)
34/// ```
35fn encode_base(base: &Base) -> String {
36    let mut writer: ObjectWriter = ObjectWriter::new();
37    match base {
38        Base::Unborn => {
39            writer.string("kind", "unborn");
40        }
41        Base::Commit(oid) => {
42            writer.string("kind", "commit").string("oid", oid.as_str());
43        }
44    }
45    return writer.finish();
46}
47
48/// What:
49///  Encode a file identity.
50/// Why:
51///   Shared by landing artifacts and locks.
52///
53/// In TS you'd write (pseudocode):
54/// ```ts
55/// JSON.stringify({ device, inode })
56/// ```
57fn encode_identity(identity: &FileIdentity) -> String {
58    let mut writer: ObjectWriter = ObjectWriter::new();
59    writer
60        .string("device", identity.device.as_str())
61        .string("inode", identity.inode.as_str());
62    return writer.finish();
63}
64
65/// What:
66///  Encode a lock identity.
67/// Why:
68///   Shared by `index-lock-<n>.json` and `landing-<n>.json`.
69///
70/// In TS you'd write (pseudocode):
71/// ```ts
72/// JSON.stringify({ device, inode, fsId })
73/// ```
74fn encode_lock(lock: &LockIdentity) -> String {
75    let mut writer: ObjectWriter = ObjectWriter::new();
76    writer
77        .string("device", lock.file.device.as_str())
78        .string("inode", lock.file.inode.as_str())
79        .string("fsId", lock.fs_id.as_str());
80    return writer.finish();
81}
82
83/// What:
84///  Encode a list of added-path records.
85/// Why:
86///   Shared by `prepared.json` and `landing-<n>.json`.
87///
88/// In TS you'd write (pseudocode):
89/// ```ts
90/// JSON.stringify(records)
91/// ```
92fn encode_added_paths(records: &[AddedPath]) -> String {
93    let mut text: String = String::from("[");
94    for (index, record) in records.iter().enumerate() {
95        if index > 0 {
96            text.push(',');
97        }
98        let mut writer: ObjectWriter = ObjectWriter::new();
99        writer
100            .string("path", record.path.as_str())
101            .string("gitMode", record.git_mode.as_str())
102            .string("originalOid", record.original_oid.as_str())
103            .string("intendedOid", record.intended_oid.as_str());
104        text.push_str(writer.finish().as_str());
105    }
106    text.push(']');
107    return text;
108}
109
110/// What:
111///  The common head of every record:
112///  schema version and state discriminator.
113/// Why:
114///   Every record starts with the same two fields.
115///
116/// In TS you'd write (pseudocode):
117/// ```ts
118/// { schemaVersion: 2, state }
119/// ```
120fn record_head(state: &str) -> ObjectWriter {
121    let mut writer: ObjectWriter = ObjectWriter::new();
122    writer
123        .integer("schemaVersion", JOURNAL_SCHEMA_VERSION)
124        .string("state", state);
125    return writer;
126}
127
128/// What:
129///  The wire spelling of a mode.
130/// Why:
131///   Shared by encoding and its tests.
132///
133/// In TS you'd write (pseudocode):
134/// ```ts
135/// mode
136/// ```
137pub fn mode_name(mode: TransactionMode) -> &'static str {
138    match mode {
139        TransactionMode::ExplicitPath => return "explicit-path",
140        TransactionMode::Index => return "index",
141    }
142}
143
144/// What:
145///  The wire spelling of a conclusion kind.
146/// Why:
147///   Shared by encoding and its tests.
148///
149/// In TS you'd write (pseudocode):
150/// ```ts
151/// conclusion
152/// ```
153pub fn conclusion_name(conclusion: Conclusion) -> &'static str {
154    match conclusion {
155        Conclusion::None => return "none",
156        Conclusion::Amend => return "amend",
157        Conclusion::Merge => return "merge",
158        Conclusion::CherryPick => return "cherry-pick",
159        Conclusion::Revert => return "revert",
160    }
161}
162
163/// What:
164///  The wire spelling of a ref format.
165/// Why:
166///   Shared by encoding and its tests.
167///
168/// In TS you'd write (pseudocode):
169/// ```ts
170/// refFormat
171/// ```
172pub fn ref_format_name(format: RefFormat) -> &'static str {
173    match format {
174        RefFormat::Files => return "files",
175        RefFormat::Reftable => return "reftable",
176    }
177}
178
179/// What:
180///  Encode `preparing.json` with its trailing newline.
181/// Why:
182///   `writeJournalRecord` writes `JSON.stringify(record)` and a newline.
183///
184/// In TS you'd write (pseudocode):
185/// ```ts
186/// `${JSON.stringify(preparing)}\n`
187/// ```
188pub fn encode_preparing(record: &PreparingRecord) -> String {
189    let mut writer: ObjectWriter = record_head("preparing");
190    writer
191        .string("transactionId", record.transaction_id.as_str())
192        .string("mode", mode_name(record.mode))
193        .raw("base", encode_base(&record.base).as_str());
194    let head: String = match &record.symbolic_head {
195        SymbolicHead::Branch(name) => {
196            let mut nested: ObjectWriter = ObjectWriter::new();
197            nested.string("kind", "branch").string("ref", name.as_str());
198            nested.finish()
199        }
200        SymbolicHead::Detached => String::from("{\"kind\":\"detached\"}"),
201    };
202    writer
203        .raw("symbolicHead", head.as_str())
204        .string("targetRef", record.target_ref.as_str())
205        .string("conclusion", conclusion_name(record.conclusion))
206        .string("repositoryRoot", record.repository_root.as_str())
207        .string("gitDir", record.git_dir.as_str())
208        .string("commonDir", record.common_dir.as_str())
209        .string("realIndexPath", record.real_index_path.as_str())
210        .string("objectDirectory", record.object_directory.as_str())
211        .string("refFormat", ref_format_name(record.ref_format))
212        .string("emptyTreeOid", record.empty_tree_oid.as_str())
213        .string("shadowPath", record.shadow_path.as_str())
214        .raw(
215            "selectedPathspecs",
216            string_array(record.selected_pathspecs.as_slice()).as_str(),
217        )
218        .string("invokedAt", record.invoked_at.as_str());
219    return format!("{}\n", writer.finish());
220}
221
222/// What:
223///  Encode `prepared.json` with its trailing newline.
224/// Why:
225///   See `encode_preparing`.
226///
227/// In TS you'd write (pseudocode):
228/// ```ts
229/// `${JSON.stringify(prepared)}\n`
230/// ```
231pub fn encode_prepared(record: &PreparedRecord) -> String {
232    let mut writer: ObjectWriter = record_head("prepared");
233    writer
234        .string("shadowPath", record.shadow_path.as_str())
235        .string("preparedOid", record.prepared_oid.as_str())
236        .boolean("signed", record.signed)
237        .string("intendedTreeOid", record.intended_tree_oid.as_str())
238        .raw(
239            "committedPaths",
240            string_array(record.committed_paths.as_slice()).as_str(),
241        )
242        .raw(
243            "addedPaths",
244            encode_added_paths(record.added_paths.as_slice()).as_str(),
245        )
246        .raw(
247            "selectedWorktreePaths",
248            encode_added_paths(record.selected_worktree_paths.as_slice()).as_str(),
249        );
250    return format!("{}\n", writer.finish());
251}
252
253/// What:
254///  Encode `index-lock-<n>.json` with its trailing newline.
255/// Why:
256///   See `encode_preparing`.
257///
258/// In TS you'd write (pseudocode):
259/// ```ts
260/// `${JSON.stringify(indexLock)}\n`
261/// ```
262pub fn encode_index_lock(record: &IndexLockRecord) -> String {
263    let mut writer: ObjectWriter = record_head("index-locked");
264    writer
265        .integer("attempt", record.attempt)
266        .raw("lock", encode_lock(&record.lock).as_str());
267    return format!("{}\n", writer.finish());
268}
269
270/// What:
271///  Encode `landing-<n>.json` with its trailing newline;
272///  optional fields are omitted when
273///       absent,
274///  as `JSON.stringify` omits `undefined`.
275/// Why:
276///   See `encode_preparing`.
277///
278/// In TS you'd write (pseudocode):
279/// ```ts
280/// `${JSON.stringify(landing)}\n`
281/// ```
282pub fn encode_landing(record: &LandingRecord) -> String {
283    let mut writer: ObjectWriter = record_head("landing");
284    writer.integer("attempt", record.attempt).string(
285        "operation",
286        match record.operation {
287            LandingOperation::Commit => "commit",
288            LandingOperation::NormalizeOnly => "normalize-only",
289        },
290    );
291    writer.raw("expectedOld", encode_base(&record.expected_old).as_str());
292    if let Some(oid) = &record.new_oid {
293        writer.string("newOid", oid.as_str());
294    }
295    writer
296        .string("landedTreeOid", record.landed_tree_oid.as_str())
297        .raw(
298            "preLandingIndex",
299            encode_identity(&record.pre_landing_index).as_str(),
300        )
301        .raw("postIndex", encode_identity(&record.post_index).as_str())
302        .raw("lock", encode_lock(&record.lock).as_str());
303    if let Some(pack) = &record.pack_name {
304        writer.string("packName", pack.as_str());
305    }
306    writer
307        .raw(
308            "addedPaths",
309            encode_added_paths(record.added_paths.as_slice()).as_str(),
310        )
311        .raw(
312            "selectedWorktreePaths",
313            encode_added_paths(record.selected_worktree_paths.as_slice()).as_str(),
314        );
315    return format!("{}\n", writer.finish());
316}
317
318/// What:
319///  Encode `ref-updated.json` with its trailing newline.
320/// Why:
321///   See `encode_preparing`.
322///
323/// In TS you'd write (pseudocode):
324/// ```ts
325/// `${JSON.stringify({ schemaVersion: 2, state: 'ref-updated', landedOid })}\n`
326/// ```
327pub fn encode_ref_updated(landed_oid: &str) -> String {
328    let mut writer: ObjectWriter = record_head("ref-updated");
329    writer.string("landedOid", landed_oid);
330    return format!("{}\n", writer.finish());
331}
332
333/// Encoding controls stay out of the release executable.
334#[cfg(test)]
335#[path = "transaction_journal_encode_tests.rs"]
336pub(crate) mod tests;