git_policy_cli/transaction_journal.rs
1//! What:
2//! The schema-version-2 journal records of one commit transaction and their filenames;
3//! encoding and parsing live in `transaction_journal_encode.rs` and
4//! `transaction_journal_parse.rs`.
5//! Why:
6//! State files are created exclusively and never rewritten,
7//! so a crash leaves the newest
8//! complete state readable;
9//! recovery in either wrapper reads the records the other wrote
10//! (`src/policy-engine/commit-transaction-journal-states.ts` and
11//! `commit-transaction-journal-parse.ts`).
12//! Every missing or mistyped field fails closed
13//! with the record and field named.
14//!
15//! In TS you'd write (pseudocode):
16//! ```ts
17//! const preparing = parsePreparingRecord(bytes); await writeJournalRecord({ directory, filename, record });
18//! ```
19
20/// Journal schema version of every per-transaction state record.
21pub const JOURNAL_SCHEMA_VERSION: i64 = 2;
22/// Invocation capture record filename.
23pub const PREPARING_FILENAME: &str = "preparing.json";
24/// Prepared commit record filename.
25pub const PREPARED_FILENAME: &str = "prepared.json";
26/// Landed commit record filename.
27pub const REF_UPDATED_FILENAME: &str = "ref-updated.json";
28/// Index installation completion marker filename.
29pub const INDEX_INSTALLED_FILENAME: &str = "index-installed";
30
31/// What:
32/// The target at invocation,
33/// or the expected old target of a landing.
34/// Why:
35/// An unborn branch has no commit;
36/// compare-and-swap then expects the all-zero OID.
37///
38/// In TS you'd write (pseudocode):
39/// ```ts
40/// type PreparationBase = { kind: 'unborn' } | { kind: 'commit'; oid: string };
41/// ```
42#[derive(Clone, Debug, Eq, PartialEq)]
43pub enum Base {
44 /// The target names no commit yet.
45 Unborn,
46 /// The target names this commit.
47 Commit(String),
48}
49
50/// What:
51/// The symbolic `HEAD` target at invocation.
52/// Why:
53/// A branch switch between preparation and landing fails the commit.
54///
55/// In TS you'd write (pseudocode):
56/// ```ts
57/// type SymbolicHeadTarget = { kind: 'branch'; ref: string } | { kind: 'detached' };
58/// ```
59#[derive(Clone, Debug, Eq, PartialEq)]
60pub enum SymbolicHead {
61 /// `HEAD` is symbolic to this ref.
62 Branch(String),
63 /// `HEAD` is detached.
64 Detached,
65}
66
67/// What:
68/// How the commit selects its content.
69/// Why:
70/// Explicit-path commits build the tree from the base plus selected paths;
71/// index
72/// commits use the captured index.
73///
74/// In TS you'd write (pseudocode):
75/// ```ts
76/// type TransactionMode = 'explicit-path' | 'index';
77/// ```
78#[derive(Clone, Copy, Debug, Eq, PartialEq)]
79pub enum TransactionMode {
80 /// `git commit -- <paths>` with commit-only semantics.
81 ExplicitPath,
82 /// A commit of the index.
83 Index,
84}
85
86/// What:
87/// The commit kind that decides whether a moved target fails instead of replaying.
88/// Why:
89/// An amend or a conclusion cannot replay onto a moved branch.
90///
91/// In TS you'd write (pseudocode):
92/// ```ts
93/// type ConclusionKind = 'none' | 'amend' | 'merge' | 'cherry-pick' | 'revert';
94/// ```
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96pub enum Conclusion {
97 /// An ordinary commit.
98 None,
99 /// `--amend`.
100 Amend,
101 /// The conclusion of a merge.
102 Merge,
103 /// The conclusion of a cherry-pick.
104 CherryPick,
105 /// The conclusion of a revert.
106 Revert,
107}
108
109/// What:
110/// The ref storage backend of the real repository.
111/// Why:
112/// The shadow ref snapshot and the conclusion pseudorefs differ by backend.
113///
114/// In TS you'd write (pseudocode):
115/// ```ts
116/// type RefStorageFormat = 'files' | 'reftable';
117/// ```
118#[derive(Clone, Copy, Debug, Eq, PartialEq)]
119pub enum RefFormat {
120 /// Loose refs and `packed-refs`.
121 Files,
122 /// The reftable backend.
123 Reftable,
124}
125
126/// What:
127/// A file identity recorded without hashes:
128/// device and inode as decimal text.
129/// Why:
130/// Recovery proves an artifact is the exact file written,
131/// not one put in its place.
132///
133/// In TS you'd write (pseudocode):
134/// ```ts
135/// type FileIdentity = { device: string; inode: string };
136/// ```
137#[derive(Clone, Debug, Eq, PartialEq)]
138pub struct FileIdentity {
139 /// Device identity.
140 pub device: String,
141 /// Inode identity.
142 pub inode: String,
143}
144
145/// What:
146/// The identity of the real `index.lock` a transaction created.
147/// Why:
148/// Recovery removes a real `index.lock` only when it is provably the transaction's own.
149///
150/// In TS you'd write (pseudocode):
151/// ```ts
152/// type LockIdentity = FileIdentity & { fsId: string };
153/// ```
154#[derive(Clone, Debug, Eq, PartialEq)]
155pub struct LockIdentity {
156 /// Device and inode.
157 pub file: FileIdentity,
158 /// Filesystem identity containing the lock.
159 pub fs_id: String,
160}
161
162/// What:
163/// One policy-added path,
164/// or one selected path whose worktree copy receives settled
165/// bytes:
166/// its mode,
167/// the blob before the commit and the blob the commit lands.
168/// Why:
169/// After landing,
170/// the worktree copy is replaced only while it still holds the original.
171///
172/// In TS you'd write (pseudocode):
173/// ```ts
174/// type AddedPathRecord = { path: string; gitMode: '100644' | '100755'; originalOid: string; intendedOid: string };
175/// ```
176#[derive(Clone, Debug, Eq, PartialEq)]
177pub struct AddedPath {
178 /// Repository-relative path.
179 pub path: String,
180 /// `100644` or `100755`.
181 pub git_mode: String,
182 /// Blob at `HEAD`,
183 /// in the real index and in the worktree before the commit.
184 pub original_oid: String,
185 /// Blob the commit lands and the worktree receives.
186 pub intended_oid: String,
187}
188
189/// What:
190/// `preparing.json`:
191/// invocation capture facts written before any Git mutation.
192/// Why:
193/// Recovery learns from it where every piece of the transaction lives.
194///
195/// In TS you'd write (pseudocode):
196/// ```ts
197/// type PreparingRecord = { schemaVersion: 2; state: 'preparing'; transactionId; mode; base; ... };
198/// ```
199#[derive(Clone, Debug, Eq, PartialEq)]
200pub struct PreparingRecord {
201 /// Transaction ID,
202 /// also the reflog nonce.
203 pub transaction_id: String,
204 /// Commit selection mode.
205 pub mode: TransactionMode,
206 /// Target commit at invocation.
207 pub base: Base,
208 /// Symbolic `HEAD` target at invocation.
209 pub symbolic_head: SymbolicHead,
210 /// Ref advanced by compare-and-swap.
211 pub target_ref: String,
212 /// Commit kind.
213 pub conclusion: Conclusion,
214 /// Canonical worktree root.
215 pub repository_root: String,
216 /// Owning worktree Git directory.
217 pub git_dir: String,
218 /// Common Git directory.
219 pub common_dir: String,
220 /// Real index path.
221 pub real_index_path: String,
222 /// Real object directory.
223 pub object_directory: String,
224 /// Ref storage backend.
225 pub ref_format: RefFormat,
226 /// Empty tree standing in for an unborn base.
227 pub empty_tree_oid: String,
228 /// Shadow repository path derived from the transaction ID.
229 pub shadow_path: String,
230 /// Pathspecs selected at invocation.
231 pub selected_pathspecs: Vec<String>,
232 /// ISO-8601 invocation start time.
233 pub invoked_at: String,
234}
235
236/// What:
237/// `prepared.json`:
238/// the verified prepared commit in the shadow repository.
239/// Why:
240/// Landing and recovery need the commit,
241/// its tree and the worktree completions.
242///
243/// In TS you'd write (pseudocode):
244/// ```ts
245/// type PreparedRecord = { schemaVersion: 2; state: 'prepared'; shadowPath; preparedOid; signed; ... };
246/// ```
247#[derive(Clone, Debug, Eq, PartialEq)]
248pub struct PreparedRecord {
249 /// Shadow repository holding the prepared commit.
250 pub shadow_path: String,
251 /// Prepared commit.
252 pub prepared_oid: String,
253 /// Whether the prepared commit carries a signature header.
254 pub signed: bool,
255 /// Intended tree the prepared commit was verified against.
256 pub intended_tree_oid: String,
257 /// Paths the commit carries,
258 /// including policy-added paths.
259 pub committed_paths: Vec<String>,
260 /// Policy-added paths with original and intended blobs.
261 pub added_paths: Vec<AddedPath>,
262 /// Selected paths whose worktree copies receive settled bytes.
263 pub selected_worktree_paths: Vec<AddedPath>,
264}
265
266/// What:
267/// `index-lock-<n>.json`:
268/// identity of the real `index.lock` attempt `n` created,
269/// written right after creating it.
270/// Why:
271/// Recovery can prove ownership of the lock before any landing record exists.
272///
273/// In TS you'd write (pseudocode):
274/// ```ts
275/// type IndexLockRecord = { schemaVersion: 2; state: 'index-locked'; attempt: number; lock: LockIdentity };
276/// ```
277#[derive(Clone, Debug, Eq, PartialEq)]
278pub struct IndexLockRecord {
279 /// Landing attempt number,
280 /// from 1.
281 pub attempt: i64,
282 /// Created lock identity.
283 pub lock: LockIdentity,
284}
285
286/// What:
287/// What a landing attempt installs:
288/// a commit,
289/// or a normalization that installs only index
290/// and worktree bytes.
291/// Why:
292/// A normalization never moves the target,
293/// so recovery checks it did not move.
294///
295/// In TS you'd write (pseudocode):
296/// ```ts
297/// type LandingOperation = 'commit' | 'normalize-only';
298/// ```
299#[derive(Clone, Copy, Debug, Eq, PartialEq)]
300pub enum LandingOperation {
301 /// The target advances to a new commit.
302 Commit,
303 /// Only the index and worktree change.
304 NormalizeOnly,
305}
306
307/// What:
308/// `landing-<n>.json`:
309/// one landing attempt inside the critical section.
310/// Why:
311/// Recovery decides from it whether the attempt landed and completes or discards it.
312///
313/// In TS you'd write (pseudocode):
314/// ```ts
315/// type LandingRecord = { schemaVersion: 2; state: 'landing'; attempt; operation; expectedOld; newOid?; ... };
316/// ```
317#[derive(Clone, Debug, Eq, PartialEq)]
318pub struct LandingRecord {
319 /// Landing attempt number,
320 /// from 1.
321 pub attempt: i64,
322 /// Landing kind.
323 pub operation: LandingOperation,
324 /// Expected old target value for compare-and-swap.
325 pub expected_old: Base,
326 /// New target value;
327 /// absent for a normalization.
328 pub new_oid: Option<String>,
329 /// Tree the landing installs into the real index.
330 pub landed_tree_oid: String,
331 /// Exact pre-landing real index snapshot identity.
332 pub pre_landing_index: FileIdentity,
333 /// Exact post-index artifact identity.
334 pub post_index: FileIdentity,
335 /// Real `index.lock` identity.
336 pub lock: LockIdentity,
337 /// Migrated pack name;
338 /// absent for a normalization.
339 pub pack_name: Option<String>,
340 /// Policy-added path records.
341 pub added_paths: Vec<AddedPath>,
342 /// Selected worktree completion records.
343 pub selected_worktree_paths: Vec<AddedPath>,
344}
345
346/// What:
347/// The numbered artifact and record filenames of one landing attempt.
348/// Why:
349/// Every writer and reader names them the same way.
350///
351/// In TS you'd write (pseudocode):
352/// ```ts
353/// preLandingIndexFilename(1) // 'pre-landing-1.index'
354/// ```
355pub fn pre_landing_index_filename(attempt: i64) -> String {
356 return format!("pre-landing-{attempt}.index");
357}
358
359/// What:
360/// The post-index artifact filename of one attempt.
361/// Why:
362/// See `pre_landing_index_filename`.
363///
364/// In TS you'd write (pseudocode):
365/// ```ts
366/// postIndexFilename(1) // 'post-1.index'
367/// ```
368pub fn post_index_filename(attempt: i64) -> String {
369 return format!("post-{attempt}.index");
370}
371
372/// What:
373/// The landing record filename of one attempt.
374/// Why:
375/// See `pre_landing_index_filename`.
376///
377/// In TS you'd write (pseudocode):
378/// ```ts
379/// landingRecordFilename(1) // 'landing-1.json'
380/// ```
381pub fn landing_record_filename(attempt: i64) -> String {
382 return format!("landing-{attempt}.json");
383}
384
385/// What:
386/// The index-lock record filename of one attempt.
387/// Why:
388/// See `pre_landing_index_filename`.
389///
390/// In TS you'd write (pseudocode):
391/// ```ts
392/// indexLockRecordFilename(1) // 'index-lock-1.json'
393/// ```
394pub fn index_lock_record_filename(attempt: i64) -> String {
395 return format!("index-lock-{attempt}.json");
396}