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
//! What:
//! The capture-order inputs of one replay:
//! the first-parent commits landed since the preparation base with their changed paths,
//! and the decisions for the shared paths read from `captured.json` and the landed-capture records.
//! Why:
//! Two Git processes list the history whatever its length; each landed commit's record says which worktree
//! captured which path and when, which is what the decision compares.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! await decideSharedPaths({ gitPath, shadowPath, gitDir, transactionDirectory, base, current, paths });
//! ```
/// The capture-order decision and its parsers.
use super::replay_capture_order::{CaptureOrderDecision, CaptureView, LandedChange, capture_view, decide_capture_order, parse_commit_changes};
/// The captured and landed record readers.
use super::capture_records::{landed_record_directory, parse_landed_capture_record, read_captured_record};
use super::capture_store::read_store_file;
use super::replay_shadow::run_shadow_checked;
use super::transaction_git::{GitContext, GitFailure};
/// Decisions and records keyed by path or commit.
use std::collections::BTreeMap;
/// `Path` names the shadow, the git directory and the transaction.
use std::path::Path;
/// What:
/// A capture-order failure:
/// a Git command failed, or a record or listing could not be read or parsed.
/// Why:
/// Replay reports each as `transaction-failed`; a malformed record is never treated as "unordered".
///
/// In TS you'd write (pseudocode):
/// ```ts
/// throw new CaptureOrderRecordError(message);
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CaptureHistoryError {
/// A Git command failed.
Git(GitFailure),
/// A record file could not be read, or the listing did not parse.
Record(String),
}
/// What:
/// Lists the commits landed since the preparation base on the first-parent line with the paths each changed.
/// Why:
/// `rev-list --first-parent` gives the line; one `diff-tree --stdin` gives every commit's changes,
/// so the process count does not depend on the history length.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await listLandedChanges({ gitPath, shadowPath, base, current });
/// ```
pub fn list_landed_changes(
context: &GitContext,
shadow: &Path,
cwd: &Path,
base: Option<&str>,
current: &str,
) -> Result<Vec<LandedChange>, CaptureHistoryError> {
let exclusion: String = base.map(|oid| { return format!("^{oid}"); }).unwrap_or_default();
// `mut` lets the exclusion be included only for a committed base.
let mut arguments: Vec<&str> = vec!["rev-list", "--first-parent", current];
if base.is_some() {
arguments.push(exclusion.as_str());
}
arguments.push("--");
let listed = run_shadow_checked(context, shadow, cwd, &arguments, None, None, &[]).map_err(CaptureHistoryError::Git)?;
if listed.stdout.is_empty() {
return Ok(Vec::new());
}
let changes = run_shadow_checked(
context,
shadow,
cwd,
&[
"diff-tree",
"--stdin",
"--root",
"-r",
"-z",
"--raw",
"--no-renames",
"--no-abbrev",
"-m",
"--first-parent",
],
Some(listed.stdout.as_slice()),
None,
&[],
)
.map_err(CaptureHistoryError::Git)?;
let text: String = changes.stdout.iter().map(|byte| { return char::from(*byte); }).collect();
return parse_commit_changes(text.as_str()).map_err(|error| { return CaptureHistoryError::Record(error.0); });
}
/// What:
/// Reads the landed-capture record of each listed commit that has one, as capture views keyed by commit.
/// Why:
/// A commit without a record leaves the path unordered, so a missing record is skipped, not an error;
/// a malformed record fails the replay.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await readLandedCaptures({ gitDir, commits });
/// ```
fn read_landed_captures(git_dir: &Path, commits: &[LandedChange]) -> Result<BTreeMap<String, CaptureView>, CaptureHistoryError> {
let directory = landed_record_directory(git_dir);
// `mut` lets the views be collected one commit at a time.
let mut views: BTreeMap<String, CaptureView> = BTreeMap::new();
for change in commits {
let name: String = format!("{}.json", change.commit);
let text: Option<String> = read_store_file(directory.join(&name).as_path())
.map_err(|error| { return CaptureHistoryError::Record(error.0); })?;
let Some(found) = text else {
continue;
};
let record = parse_landed_capture_record(found.as_str(), &format!("landed/{name}"))
.map_err(|error| { return CaptureHistoryError::Record(error.0); })?;
views.insert(
change.commit.clone(),
capture_view(record.worktree_id.as_str(), record.sequence, &record.worktree_paths),
);
}
return Ok(views);
}
/// What:
/// Decides every shared path of one replay: the transaction's `captured.json` and the landed records
/// of the first-parent history since the base.
/// Why:
/// When the transaction's capture names none of the shared paths, or the transaction has no capture,
/// every path is unordered and no history process runs.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await decideSharedPaths({ gitPath, shadowPath, gitDir, transactionDirectory, base, current, paths });
/// ```
pub fn decide_shared_paths(
context: &GitContext,
shadow: &Path,
cwd: &Path,
git_dir: &Path,
transaction_directory: &Path,
base: Option<&str>,
current: &str,
paths: &[String],
) -> Result<BTreeMap<String, CaptureOrderDecision>, CaptureHistoryError> {
let captured = match read_captured_record(transaction_directory).map_err(|error| { return CaptureHistoryError::Record(error.0); })? {
Some(found) => found,
None => return Ok(BTreeMap::new()),
};
let own: CaptureView = capture_view(captured.worktree_id.as_str(), captured.sequence, &captured.worktree_paths);
let candidates: Vec<&String> = paths.iter().filter(|path| { return own.paths.contains(path.as_str()); }).collect();
if candidates.is_empty() {
return Ok(BTreeMap::new());
}
let history: Vec<LandedChange> = list_landed_changes(context, shadow, cwd, base, current)?;
let landed: BTreeMap<String, CaptureView> = read_landed_captures(git_dir, &history)?;
return Ok(candidates
.into_iter()
.map(|path| {
let decision: CaptureOrderDecision = decide_capture_order(path.as_str(), &own, &history, &landed);
return (path.to_string(), decision);
})
.collect());
}
#[cfg(test)]
#[path = "replay_capture_history_tests.rs"]
mod tests;