Skip to main content

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, the exact bytes Git reported.
179    pub path: Vec<u8>,
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, as the exact bytes of the command line.
231    pub selected_pathspecs: Vec<Vec<u8>>,
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    ///  as the exact bytes Git reported.
260    pub committed_paths: Vec<Vec<u8>>,
261    /// Policy-added paths with original and intended blobs.
262    pub added_paths: Vec<AddedPath>,
263    /// Selected paths whose worktree copies receive settled bytes.
264    pub selected_worktree_paths: Vec<AddedPath>,
265}
266
267/// What:
268///  `index-lock-<n>.json`:
269///  identity of the real `index.lock` attempt `n` created,
270///       written right after creating it.
271/// Why:
272///   Recovery can prove ownership of the lock before any landing record exists.
273///
274/// In TS you'd write (pseudocode):
275/// ```ts
276/// type IndexLockRecord = { schemaVersion: 2; state: 'index-locked'; attempt: number; lock: LockIdentity };
277/// ```
278#[derive(Clone, Debug, Eq, PartialEq)]
279pub struct IndexLockRecord {
280    /// Landing attempt number,
281    ///  from 1.
282    pub attempt: i64,
283    /// Created lock identity.
284    pub lock: LockIdentity,
285}
286
287/// What:
288///  What a landing attempt installs:
289///  a commit,
290///  or a normalization that installs only index
291///       and worktree bytes.
292/// Why:
293///   A normalization never moves the target,
294///  so recovery checks it did not move.
295///
296/// In TS you'd write (pseudocode):
297/// ```ts
298/// type LandingOperation = 'commit' | 'normalize-only';
299/// ```
300#[derive(Clone, Copy, Debug, Eq, PartialEq)]
301pub enum LandingOperation {
302    /// The target advances to a new commit.
303    Commit,
304    /// Only the index and worktree change.
305    NormalizeOnly,
306}
307
308/// What:
309///  `landing-<n>.json`:
310///  one landing attempt inside the critical section.
311/// Why:
312///   Recovery decides from it whether the attempt landed and completes or discards it.
313///
314/// In TS you'd write (pseudocode):
315/// ```ts
316/// type LandingRecord = { schemaVersion: 2; state: 'landing'; attempt; operation; expectedOld; newOid?; ... };
317/// ```
318#[derive(Clone, Debug, Eq, PartialEq)]
319pub struct LandingRecord {
320    /// Landing attempt number,
321    ///  from 1.
322    pub attempt: i64,
323    /// Landing kind.
324    pub operation: LandingOperation,
325    /// Expected old target value for compare-and-swap.
326    pub expected_old: Base,
327    /// New target value;
328    ///  absent for a normalization.
329    pub new_oid: Option<String>,
330    /// Tree the landing installs into the real index.
331    pub landed_tree_oid: String,
332    /// Exact pre-landing real index snapshot identity.
333    pub pre_landing_index: FileIdentity,
334    /// Exact post-index artifact identity.
335    pub post_index: FileIdentity,
336    /// Real `index.lock` identity.
337    pub lock: LockIdentity,
338    /// Migrated pack name;
339    ///  absent for a normalization.
340    pub pack_name: Option<String>,
341    /// Policy-added path records.
342    pub added_paths: Vec<AddedPath>,
343    /// Selected worktree completion records.
344    pub selected_worktree_paths: Vec<AddedPath>,
345}
346
347/// What:
348///  The numbered artifact and record filenames of one landing attempt.
349/// Why:
350///   Every writer and reader names them the same way.
351///
352/// In TS you'd write (pseudocode):
353/// ```ts
354/// preLandingIndexFilename(1) // 'pre-landing-1.index'
355/// ```
356pub fn pre_landing_index_filename(attempt: i64) -> String {
357    return format!("pre-landing-{attempt}.index");
358}
359
360/// What:
361///  The post-index artifact filename of one attempt.
362/// Why:
363///   See `pre_landing_index_filename`.
364///
365/// In TS you'd write (pseudocode):
366/// ```ts
367/// postIndexFilename(1) // 'post-1.index'
368/// ```
369pub fn post_index_filename(attempt: i64) -> String {
370    return format!("post-{attempt}.index");
371}
372
373/// What:
374///  The landing record filename of one attempt.
375/// Why:
376///   See `pre_landing_index_filename`.
377///
378/// In TS you'd write (pseudocode):
379/// ```ts
380/// landingRecordFilename(1) // 'landing-1.json'
381/// ```
382pub fn landing_record_filename(attempt: i64) -> String {
383    return format!("landing-{attempt}.json");
384}
385
386/// What:
387///  The index-lock record filename of one attempt.
388/// Why:
389///   See `pre_landing_index_filename`.
390///
391/// In TS you'd write (pseudocode):
392/// ```ts
393/// indexLockRecordFilename(1) // 'index-lock-1.json'
394/// ```
395pub fn index_lock_record_filename(attempt: i64) -> String {
396    return format!("index-lock-{attempt}.json");
397}