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