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
//! What:
//! The fixed argument transforms of a forwarded command,
//! in their fixed order:
//! atomic push,
//! commit-only,
//! status hints off.
//! Why:
//! These are not policies:
//! they have no severity and cannot be configured.
//! They add
//! `--atomic` to `git push`,
//! `-o` to `git commit` and `-c advice.statusHints=false`
//! before `git status`,
//! and the commit-only transform can reject a commit.
//! The
//! decisions belong to the pure rule cores;
//! this module runs them in order and
//! fetches the two repository facts commit-only may ask for.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // const { args, events, complete } = await applyFixedTransforms({ args, rawArgs, sequence });
//! ```
/// What:
/// `use` brings names from sibling files into this file;
/// `super::` means "the parent
/// module",
/// where every sibling file of this crate is declared.
/// Why:
/// Each transform is one rule core;
/// the facts interface answers what a core asks.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// import { atomicPush } from '../rule/atomic-push.ts';
/// ```
use super::command_options::OptionError;
use super::diagnostics::EngineFailureCode;
use super::policy_events::PolicyEvent;
use super::repository_facts::RepositoryFacts;
use super::rule_argument_rewrite::ArgumentRewrite;
use super::rule_atomic_push::atomic_push;
use super::rule_commit_only::{
CommitOnlyDecision, decide_commit_only, resolve_index_state, resolve_sequencer_state,
violation_code_text,
};
use super::rule_status_hints::status_hints_off;
/// What:
/// `OsString` is owned operating-system text of raw bytes.
/// Sibling the reader might
/// expect:
/// `String`,
/// which must be valid UTF-8.
/// Why:
/// Arguments are forwarded with their exact bytes.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// // string, but byte-preserving.
/// ```
use std::ffi::OsString;
/// What:
/// The identifier of the commit-only transform in its events.
/// `&str` is borrowed
/// text baked into the program.
/// Why:
/// A fixed-transform rejection names its transform where a finding names its policy.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const COMMIT_ONLY_CORE = 'commit-only';
/// ```
pub const COMMIT_ONLY_CORE: &str = "commit-only";
/// What:
/// What the transform stage produced.
/// A `struct` is a record with named fields;
/// `Vec<T>` is an owned list;
/// `bool` is true or false.
/// `#[derive(...)]` asks the
/// compiler to generate cloning,
/// debug printing and `==`.
/// Why:
/// The caller forwards `arguments` only when no event blocks and `complete` is true.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type TransformResult = { args: string[]; events: PolicyEvent[]; complete: boolean };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TransformResult {
/// The arguments to forward;
/// the stage's input when a transform rejected or failed.
pub arguments: Vec<OsString>,
/// Nothing,
/// one rejection,
/// or one engine failure.
pub events: Vec<PolicyEvent>,
/// False when a transform could not decide.
pub complete: bool,
}
/// What:
/// The result of a transform that could not read a fact.
/// `&[OsString]` borrows the
/// stage's input;
/// `String` is the owned explanation.
/// Why:
/// A transform that cannot decide stops the command with an engine failure.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// return { args, events: [createEngineFailureEvent({ code: 'core-incomplete', message })], complete: false };
/// ```
fn incomplete(arguments: &[OsString], message: String) -> TransformResult {
return TransformResult {
// `.to_vec()` copies the borrowed list into an owned one.
arguments: arguments.to_vec(),
// `vec![...]` is a macro building an owned list from literal items.
events: vec![PolicyEvent::EngineFailure {
code: EngineFailureCode::CoreIncomplete,
message,
// `None` is the "absent" case of `Option`.
trigger: None,
policy: None,
path: None,
}],
complete: false,
};
}
/// What:
/// Ask for the repository fact a commit-only decision still needs,
/// and finish the
/// decision.
/// `&mut dyn RepositoryFacts` lends "any facts provider" for writing;
/// `Result<A, B>` is "either success `A` or failure `B`".
/// Why:
/// Most commits are decided from their arguments;
/// only a pathless commit asks
/// whether an operation awaits its conclusion or whether the index is dirty.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function settleCommitOnly(decision, args, facts): Promise<CommitOnlyDecision>; // throws
/// ```
fn settle_commit_only(
decision: CommitOnlyDecision,
arguments: &[OsString],
facts: &mut dyn RepositoryFacts,
) -> Result<CommitOnlyDecision, String> {
// `match` picks one arm per variant; `other` binds every decision that needs no fact.
match decision {
CommitOnlyDecision::NeedsSequencerState => {
// A trailing `?` returns the failure to our caller, or unwraps the state.
return Ok(resolve_sequencer_state(facts.sequencer_state()?));
}
CommitOnlyDecision::NeedsIndexState(pending) => {
// `&pending` lends the pending insertion read-only.
return Ok(resolve_index_state(
arguments,
&pending,
facts.index_vs_head()?,
));
}
other => return Ok(other),
}
}
/// What:
/// Run the commit-only transform.
/// `bool` says whether `--no-enforce-only` was given.
/// Why:
/// The hatch skips the transform entirely;
/// its token is already gone from the
/// arguments.
/// A region Git itself refuses is left to Git.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function commitOnlyStage(args, escaped, facts): Promise<TransformResult>;
/// ```
fn commit_only_stage(
arguments: &[OsString],
escaped: bool,
facts: &mut dyn RepositoryFacts,
) -> TransformResult {
let unchanged: TransformResult = TransformResult {
arguments: arguments.to_vec(),
// `Vec::<PolicyEvent>::new()` is an empty owned list.
events: Vec::<PolicyEvent>::new(),
complete: true,
};
if escaped {
return unchanged;
}
// `&[]` is an empty flag list: wrapper controls were removed before this point.
let decided: Result<CommitOnlyDecision, OptionError> = decide_commit_only(arguments, &[]);
// `let Ok(x) = ... else { ... };` unwraps the decision or returns for a refused region.
let Ok(first) = decided else {
return unchanged;
};
let settled: CommitOnlyDecision = match settle_commit_only(first, arguments, facts) {
Ok(decision) => decision,
Err(message) => return incomplete(arguments, message),
};
match settled {
CommitOnlyDecision::Unchanged => return unchanged,
CommitOnlyDecision::Rewritten(rewritten) => {
return TransformResult {
arguments: rewritten,
events: Vec::<PolicyEvent>::new(),
complete: true,
};
}
CommitOnlyDecision::Rejected(violation) => {
return TransformResult {
arguments: arguments.to_vec(),
events: vec![PolicyEvent::CoreFinding {
core_id: COMMIT_ONLY_CORE,
code: violation_code_text(violation.code),
message: violation.message,
}],
complete: true,
};
}
// A finished decision never asks again; one that did is refused, not guessed.
CommitOnlyDecision::NeedsSequencerState | CommitOnlyDecision::NeedsIndexState(_) => {
return incomplete(
arguments,
String::from(
"cli-git: the commit-only transform asked for a second repository fact \
and cannot decide.",
),
);
}
}
}
/// What:
/// Apply the three transforms in order.
/// `&[OsString]` borrows the arguments,
/// already
/// free of wrapper controls.
/// Why:
/// The order is the incumbent's:
/// atomic push,
/// commit-only,
/// status hints.
/// A rejection
/// or failure of commit-only ends the stage with the stage's input unchanged.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function applyFixedTransforms({ args, rawArgs, sequence }): Promise<FixedTransformResult>;
/// ```
pub fn apply_fixed_transforms(
arguments: &[OsString],
commit_only_escaped: bool,
facts: &mut dyn RepositoryFacts,
) -> TransformResult {
// `match` unpacks the rewrite; a region Git refuses (`Err`) is left as it is.
let after_push: Vec<OsString> = match atomic_push(arguments, &[]) {
Ok(ArgumentRewrite::Rewritten(rewritten)) => rewritten,
Ok(ArgumentRewrite::Unchanged) | Err(_) => arguments.to_vec(),
};
// `.as_slice()` lends the owned list as a borrowed view.
let commit: TransformResult =
commit_only_stage(after_push.as_slice(), commit_only_escaped, facts);
if !commit.complete || !commit.events.is_empty() {
return TransformResult {
arguments: arguments.to_vec(),
events: commit.events,
complete: commit.complete,
};
}
let after_status: Vec<OsString> = match status_hints_off(commit.arguments.as_slice()) {
ArgumentRewrite::Rewritten(rewritten) => rewritten,
ArgumentRewrite::Unchanged => commit.arguments,
};
return TransformResult {
arguments: after_status,
events: Vec::<PolicyEvent>::new(),
complete: true,
};
}
/// Order,
/// rejection and failure controls stay out of the release executable.
#[cfg(test)]
#[path = "policy_transforms_tests.rs"]
mod tests;