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
//! What:
//! The values every commit of the service route shares: the invocation, how it can stop, what it came to, the selection
//! the command line makes, and the policy checks over the private index.
//! Why:
//! The preparation (`service_prepare.rs`) and the commit command (`commit_command.rs`) pass these between them. The
//! transaction that once landed a commit itself, with its registry, recovery and replay, is gone: the resident service is the
//! only writer of a commit (`doc/planning/cli-git-resident-service.md`, "Landing every command line").
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const selection = decideSelection(region, conclusion);
//! ```
use super::candidate_object::{ObjectId, parse_object_id};
use super::candidate_prediction::CandidateRequest;
use super::capture_invocation::InvocationCapture;
use super::command_commit::CommitRegion;
use super::commit_facts::PrivateIndexFacts;
use super::config_schema::CliGitConfig;
use super::diagnostics::EngineFailureCode;
use super::path_bytes::os_from_path_bytes;
use super::markdown_linter::embedded_linter;
use super::policy_checks::{ShippedChecks, shipped_checks};
use super::policy_forbidden_strings::default_scanner_settings;
use super::policy_markdown::markdown_state;
use super::policy_content::LifecycleContent;
use super::policy_convergence::Convergence;
use super::policy_events::PolicyEvent;
use super::repository_facts::RepositoryFacts;
use super::transaction_git::{GitContext, GitFailure};
use super::transaction_journal::{Base, Conclusion, TransactionMode};
use super::transaction_phase::PhaseMarkerError;
use super::wrapper_controls::Controls;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
/// What:
/// Everything the invocation fixes before the transaction starts.
/// Why:
/// The steps share one identity, one parsed commit region and one configuration; passing them together keeps a
/// step from reading another invocation's state.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type CommitInvocation = { args; gitPath; cwd; region; policyOptions; concurrency };
/// ```
pub struct CommitInvocation<'a> {
/// Real Git, the child environment overlay, and the invocation's global options.
pub context: &'a GitContext,
/// The invocation's environment, read for leases and inherited configuration.
pub environment: &'a [(OsString, OsString)],
/// The effective invocation directory (the process directory moved by every `-C`).
pub cwd: &'a Path,
/// The arguments with the wrapper's controls removed.
pub arguments: &'a [OsString],
/// The index of `commit` within `arguments`.
pub subcommand_index: usize,
/// The commit options Git's table read.
pub region: &'a CommitRegion,
/// The configuration of the worktree.
pub config: &'a CliGitConfig,
/// The wrapper controls given on the command line.
pub controls: &'a Controls,
}
/// What:
/// Why the transaction stopped without a policy result.
/// Why:
/// The incumbent's boundary maps each to a different ending: Git's own exit code with no wrapper text, a message
/// with exit status 1, a finding with exit status 2, or a verbatim stop text.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// NativeCommitFailedError | CommitTransactionGitError | IndexLockUnprovenOwnerError | Error
/// ```
#[derive(Debug)]
pub enum TransactionStop {
/// Native `git commit` exited nonzero during preparation: its status is the invocation's, and Git printed why.
NativeExit(i32),
/// A Git step failed: the message reaches standard error and the status is 1.
Message(String),
/// A transaction failure reported as an engine failure event with status 2.
Failed {
/// The failure's stable code.
code: EngineFailureCode,
/// What went wrong.
message: String,
},
}
impl From<GitFailure> for TransactionStop {
/// A Git step failure keeps its message and ends the command with status 1.
fn from(failure: GitFailure) -> Self {
return TransactionStop::Message(failure.0);
}
}
impl From<PhaseMarkerError> for TransactionStop {
/// A malformed test marker fails the transaction.
fn from(failure: PhaseMarkerError) -> Self {
return failed(failure.to_string());
}
}
/// What:
/// A transaction failure with the generic code.
/// Why:
/// Most unexpected failures are reported the same way.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// initialTransactionFailure({ args, message })
/// ```
pub fn failed(message: String) -> TransactionStop {
return TransactionStop::Failed {
code: EngineFailureCode::TransactionFailed,
message,
};
}
/// What:
/// What the transaction decided.
/// Why:
/// The command prints the events, runs the post-commit lifecycle for a landed commit, and exits with the code.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type CommitTransactionResult = { policyResult: PolicyEngineResult; committed: boolean; landedOid?: string };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TransactionResult {
/// The events the invocation reports, in order.
pub events: Vec<PolicyEvent>,
/// The exit status when nothing landed; 0 after a landed commit.
pub exit_code: i32,
/// The landed commit, when the transaction landed one.
pub landed_oid: Option<String>,
/// Whether the Git configuration read when the commit began holds LFS keys, so the commit's auto-push must upload
/// LFS objects first; `false` when nothing landed.
pub uses_lfs: bool,
}
/// What:
/// The commit-region facts the transaction decides from, derived once.
/// Why:
/// The mode and the selection flags are read in several steps.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const { concludesSequencer, readOnlySelection, selectsPaths, stagedAll, mode } = ...;
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Selection {
/// Whether a pathless commit concludes a merge, cherry-pick or revert.
pub concludes_sequencer: bool,
/// Whether the selection UI (include, interactive, patch) leaves the policies read-only.
pub read_only: bool,
/// Whether any path was selected.
pub selects_paths: bool,
/// Whether `-a` stages privately.
pub staged_all: bool,
/// The private index mode.
pub mode: TransactionMode,
}
/// What:
/// Decide the selection facts of one commit invocation.
/// Why:
/// Invalid combinations reach native Git unchanged, so it rejects them.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const mode = region.hasNoOnlyFlag || concludesSequencer || readOnlySelection || region.hasAllFlag || (!selectsPaths) ? 'index' : 'explicit-path';
/// ```
pub fn decide_selection(region: &CommitRegion, conclusion: Conclusion) -> Selection {
let concludes_sequencer: bool = conclusion != Conclusion::None && conclusion != Conclusion::Amend;
let read_only: bool = region.include || region.interactive || region.patch;
let selects_paths: bool = !region.pathspecs.is_empty() || region.pathspec_from_file.is_some();
let staged_all: bool = region.all && !selects_paths && !read_only && region.only != Some(true);
let index_mode: bool = region.only == Some(false)
|| concludes_sequencer
|| read_only
|| region.all
|| !selects_paths;
return Selection {
concludes_sequencer,
read_only,
selects_paths,
staged_all,
mode: if index_mode { TransactionMode::Index } else { TransactionMode::ExplicitPath },
};
}
/// What:
/// Rebuild the shipped checks over the private commit index: rule facts stay real, the candidates come from the
/// private index against the recorded base.
/// Why:
/// The incumbent's rules ask real Git about the real index, while the content policies read what the transaction
/// corrects.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const facts = createPrivateIndexFacts({ gitPath, cwd, indexPath, paths, baseRevision, objectDirectory });
/// ```
pub fn private_checks<'a, F: RepositoryFacts>(
checks: &'a mut ShippedChecks<F>,
index: &Path,
object_directory: &Path,
candidates: CandidateRequest,
) -> ShippedChecks<PrivateIndexFacts<&'a mut F>> {
// The scanner settings and the Markdown linter move to the rebuilt checks; the originals are placeholders that
// nothing reads again, because the rebuilt checks run every policy of this transaction.
let scanner_settings = std::mem::replace(&mut checks.scanner_settings, default_scanner_settings());
let markdown = std::mem::replace(&mut checks.markdown, markdown_state(Box::new(embedded_linter())));
let arguments: Vec<OsString> = checks.arguments.clone();
let allowed_worktree_dirs: Vec<PathBuf> = checks.allowed_worktree_dirs.clone();
let mut rebuilt: ShippedChecks<PrivateIndexFacts<&'a mut F>> = shipped_checks(
PrivateIndexFacts::new(&mut checks.facts, index, object_directory),
arguments,
LifecycleContent::Requested(candidates),
allowed_worktree_dirs,
);
rebuilt.scanner_settings = scanner_settings;
rebuilt.markdown = markdown;
return rebuilt;
}
/// What:
/// How many passes changed the state when the convergence settled; any other ending counts none.
/// Why:
/// The fix summary names the passes of a settled convergence only.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// convergence.kind === 'settled' ? convergence.changedPasses : 0
/// ```
pub fn settled_passes(convergence: &Convergence) -> u64 {
return match convergence {
Convergence::Settled { changed_passes } => *changed_passes as u64,
_ => 0,
};
}
/// What:
/// The candidate request for the paths a private index changes against the recorded base.
/// Why:
/// The policies check exactly the paths the commit changes; each path is literal and rooted at the top level, so a
/// name with glob characters or a directory below the current one still names one file.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// createPrivateIndexFacts({ paths: candidatePaths, baseRevision })
/// ```
pub fn staged_at_base(base: Option<ObjectId>, paths: &[Vec<u8>]) -> CandidateRequest {
return CandidateRequest::StagedAtBase {
base,
pathspecs: paths
.iter()
.map(|path| {
let mut pathspec: Vec<u8> = b":(top,literal)".to_vec();
pathspec.extend_from_slice(path.as_slice());
return os_from_path_bytes(pathspec.as_slice());
})
.collect(),
};
}
/// What:
/// The recorded base as an object ID, or nothing for an unborn branch.
/// Why:
/// The candidate listing compares the private index with this commit.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// capture.base.kind === 'commit' ? capture.base.oid : undefined
/// ```
pub fn base_object(capture: &InvocationCapture) -> Result<Option<ObjectId>, TransactionStop> {
return match &capture.base {
Base::Unborn => Ok(None),
Base::Commit(oid) => match parse_object_id(oid.as_bytes()) {
Some(found) => Ok(Some(found)),
None => Err(failed(format!("the preparation base {oid} is not an object name"))),
},
};
}
/// Controls for this module stay out of the release executable.
#[cfg(test)]
#[path = "commit_transaction_tests.rs"]
mod tests;