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