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
//! What:
//! One replay of a prepared commit onto a moved target, as the landing loop runs it outside both locks:
//! shared paths, capture order, subsumption, the merge, and either the replayed commit or the conflict.
//! Why:
//! This is the replay result the integrator consumes.
//! Revalidation after a clean merge (policies, `pre-commit`) and the events are the integrator's;
//! this function returns the data for `commit-replayed`, `replay-headers-dropped`, and
//! `concurrent-commit/replay-conflict`, and the replayed tree the revalidation reads.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // commit-replay.ts + commit-capture-order-replay.ts + commit-replay-subsumption.ts, composed as the landing step does
//! ```
/// Shared paths, capture order, subsumption, the merge, and the rebuild.
use super::replay_capture_history::{CaptureHistoryError, decide_shared_paths};
use super::replay_merge::{ReplayMerge, ReplayMergeError, ReplayedCommit, first_commit_touching, merge_replay_tree, replay_merge_base, write_replayed_commit};
use super::replay_object::{RawCommit, parse_raw_commit};
use super::replay_shadow::run_shadow_checked;
use super::replay_shared_paths::{SharedPathError, list_shared_paths};
use super::replay_subsumption::{SubsumptionBase, SubsumptionError, subsume_landed_changes};
use super::transaction_git::{GitContext, GitFailure};
/// Capture-order decisions keyed by path.
use std::collections::BTreeMap;
/// Filesystem paths.
use std::path::Path;
use super::replay_capture_order::CaptureOrderDecision;
/// What:
/// One replay's inputs: the Git context and directories, the transaction, the commit IDs, and the signing options.
/// Why:
/// A struct keeps the call site short; every field is named once.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ReplayRequest = { gitPath; shadowPath; objectDirectory; cwd; gitDir; directory; replay; base; emptyTree; current; prepared; signingKey; globalArgs };
/// ```
pub struct ReplayRequest<'a> {
/// Real Git executable and the variables every child receives.
pub context: &'a GitContext,
/// Shadow repository, as `--git-dir`.
pub shadow: &'a Path,
/// Shadow object store, whose alternates reach the real objects.
pub object_directory: &'a Path,
/// Worktree root, where Git reports conflicted paths from.
pub worktree_root: &'a Path,
/// Git directory of the owning worktree, holding the capture store.
pub git_dir: &'a Path,
/// This transaction's directory, holding `captured.json`.
pub transaction_directory: &'a Path,
/// Replay number, naming the private files.
pub replay: u64,
/// Preparation base commit; `None` for an unborn branch.
pub base: Option<&'a str>,
/// Empty tree of the repository's object format, for an unborn base.
pub empty_tree: &'a str,
/// Target that won.
pub current: &'a str,
/// Prepared commit.
pub prepared: &'a str,
/// Key the invocation passed to `-S`; `None` signs with the configured key.
pub signing_key: Option<&'a str>,
/// The caller's kept global options, which may configure signing.
pub global_arguments: &'a [String],
}
/// What:
/// The replay result of a clean merge:
/// the replayed commit and the data for `commit-replayed` and `replay-headers-dropped`.
/// Why:
/// The integrator runs revalidation on `tree`, then emits the events and lands `oid`.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type Replayed = { oid; tree; onto; preparedOid; fromBase; droppedHeaders; subsumedPaths; keptLandedPaths };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Replayed {
/// Replayed commit in the shadow store.
pub oid: String,
/// Merged tree the replayed commit holds.
pub tree: String,
/// The target the replay parent is.
pub onto: String,
/// The prepared commit that was replayed.
pub prepared_oid: String,
/// The preparation base, or `None` for an unborn branch.
pub from_base: Option<String>,
/// Custom headers a signed rebuild dropped.
pub dropped_headers: Vec<String>,
/// Paths whose prepared entries replay as they are.
pub subsumed_paths: Vec<String>,
/// Paths whose landed entries stay.
pub kept_landed_paths: Vec<String>,
}
/// What:
/// The replay result of an overlapping merge:
/// the data for `concurrent-commit/replay-conflict`.
/// Why:
/// The named prepared commit stays in the object store so the user can cherry-pick it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ReplayConflict = { winningOid; paths; preparedOid };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ReplayConflict {
/// First landed commit after the base that touches a conflicting path.
pub winning_oid: String,
/// Conflicting paths in Git path byte order.
pub paths: Vec<String>,
/// The prepared commit the replay could not land.
pub prepared_oid: String,
}
/// What:
/// The outcome of one replay.
/// Why:
/// The landing loop branches on it: a clean replay is revalidated and landed, a conflict fails without landing.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ReplayOutcome = { kind: 'replayed'; ... } | { kind: 'conflict'; ... };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ReplayOutcome {
/// The change applies cleanly; the replayed commit exists in the shadow store.
Replayed(Replayed),
/// The change conflicts with the target.
Conflict(ReplayConflict),
}
/// What:
/// A replay failure:
/// one of its steps failed, and the replay is `transaction-failed` rather than a conflict.
/// Why:
/// Each step reports its own failure type, so the integrator sees which step broke.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// throw new CommitTransactionGitError(message);
/// ```
#[derive(Debug)]
pub enum ReplayError {
/// Listing the shared paths failed.
Shared(SharedPathError),
/// Capture order could not read its records or history.
Capture(CaptureHistoryError),
/// Subsumption failed.
Subsumption(SubsumptionError),
/// The merge or the rebuild failed.
Merge(ReplayMergeError),
/// Reading the prepared commit failed.
Git(GitFailure),
/// The prepared commit's raw object could not be parsed.
Object(String),
}
/// What:
/// Reads the prepared commit's raw object through the shadow.
/// Why:
/// The rebuild needs its tree header, identities, encoding, message, and signature state.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// parseRawCommit(await runShadowGit({ args: ['cat-file', 'commit', prepared] }).stdout);
/// ```
fn prepared_commit(request: &ReplayRequest<'_>) -> Result<RawCommit, ReplayError> {
let output = run_shadow_checked(
request.context,
request.shadow,
request.worktree_root,
&["cat-file", "commit", request.prepared],
None,
None,
&[],
)
.map_err(ReplayError::Git)?;
return parse_raw_commit(&output.stdout).map_err(|error| { return ReplayError::Object(error.0); });
}
/// What:
/// The result of the merge half of a replay: the merged tree and what subsumption decided, or the conflict.
/// Why:
/// The incumbent revalidates the merged tree before it rebuilds the commit, so the two halves are separate steps.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type MergeStep = { kind: 'merged'; treeOid; subsumedPaths; keptLandedPaths } | { kind: 'conflict'; ... };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MergeStep {
/// The change applies cleanly.
Merged {
/// The merged tree.
tree: String,
/// Paths whose prepared entries replay as they are.
subsumed_paths: Vec<String>,
/// Paths whose landed entries stay.
kept_landed_paths: Vec<String>,
},
/// The change conflicts with the target.
Conflict(ReplayConflict),
}
/// What:
/// Lists the shared paths, decides capture order, settles subsumed paths and merges, naming the first landed commit
/// that touched a conflicting path when the merge conflicts.
/// Why:
/// Every step runs outside both landing locks in the shadow store; the merged tree is revalidated before the
/// commit is rebuilt on it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const subsumption = await subsumeLandedChanges(...); const merge = await mergeReplayTree(...);
/// ```
pub fn merge_replay(request: &ReplayRequest<'_>) -> Result<MergeStep, ReplayError> {
let merge_base: String = replay_merge_base(request.context, request.shadow, request.worktree_root, request.base, request.empty_tree)
.map_err(ReplayError::Merge)?;
let shared = list_shared_paths(request.context, request.shadow, request.worktree_root, &merge_base, request.current, request.prepared)
.map_err(ReplayError::Shared)?;
let names: Vec<String> = shared.iter().map(|entry| { return entry.path.clone(); }).collect();
let order: BTreeMap<String, CaptureOrderDecision> = decide_shared_paths(
request.context,
request.shadow,
request.worktree_root,
request.git_dir,
request.transaction_directory,
request.base,
request.current,
&names,
)
.map_err(ReplayError::Capture)?;
let subsumption: SubsumptionBase = subsume_landed_changes(
request.context,
request.shadow,
request.object_directory,
request.worktree_root,
request.transaction_directory,
request.replay,
&merge_base,
&shared,
&order,
)
.map_err(ReplayError::Subsumption)?;
let merged: ReplayMerge = merge_replay_tree(
request.context,
request.shadow,
request.worktree_root,
&subsumption.merge_base,
request.current,
request.prepared,
)
.map_err(ReplayError::Merge)?;
match merged {
ReplayMerge::Conflict { paths } => {
let winning_oid: String = first_commit_touching(request.context, request.worktree_root, request.base, request.current, &paths)
.map_err(ReplayError::Merge)?;
return Ok(MergeStep::Conflict(ReplayConflict {
winning_oid,
paths,
prepared_oid: String::from(request.prepared),
}));
}
ReplayMerge::Merged { tree_oid } => {
return Ok(MergeStep::Merged {
tree: tree_oid,
subsumed_paths: subsumption.subsumed_paths,
kept_landed_paths: subsumption.kept_landed_paths,
});
}
}
}
/// What:
/// Rebuilds the prepared commit on a tree, with the target that won as its parent.
/// Why:
/// The tree is the merged tree, or the tree revalidation settled; a signed commit is re-signed.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await writeReplayedCommit({ gitPath, shadowPath, directory, attempt, commit: prepared.raw, treeOid, parentOid: onto, globalArgs, signingKey });
/// ```
pub fn rebuild_replay(request: &ReplayRequest<'_>, tree: &str) -> Result<ReplayedCommit, ReplayError> {
let commit: RawCommit = prepared_commit(request)?;
return write_replayed_commit(
request.context,
request.shadow,
request.worktree_root,
request.transaction_directory,
request.replay,
&commit,
tree,
request.current,
request.signing_key,
request.global_arguments,
)
.map_err(ReplayError::Merge);
}
/// What:
/// Replays the prepared commit onto `current`:
/// lists the shared paths, decides capture order, settles subsumed paths, merges, and rebuilds on a clean merge;
/// on a conflict, names the first landed commit that touched a conflicting path.
/// Why:
/// Every step runs outside both landing locks in the shadow store, so the landing loop can take the locks only for the
/// revalidated commit it then lands.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// // replay in commit-replay.ts; order decisions and subsumption composed as the landing step does
/// ```
pub fn replay_commit(request: &ReplayRequest<'_>) -> Result<ReplayOutcome, ReplayError> {
let (tree, subsumed_paths, kept_landed_paths): (String, Vec<String>, Vec<String>) = match merge_replay(request)? {
MergeStep::Conflict(conflict) => return Ok(ReplayOutcome::Conflict(conflict)),
MergeStep::Merged {
tree,
subsumed_paths,
kept_landed_paths,
} => (tree, subsumed_paths, kept_landed_paths),
};
let replayed: ReplayedCommit = rebuild_replay(request, tree.as_str())?;
return Ok(ReplayOutcome::Replayed(Replayed {
oid: replayed.oid,
tree,
onto: String::from(request.current),
prepared_oid: String::from(request.prepared),
from_base: request.base.map(String::from),
dropped_headers: replayed.dropped_headers,
subsumed_paths,
kept_landed_paths,
}));
}
#[cfg(test)]
#[path = "replay_commit_tests.rs"]
mod tests;