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
//! What:
//! The repository facts policies and fixed transforms need,
//! behind one small
//! interface,
//! with the provider that asks real Git.
//! Why:
//! The rule cores decide from arguments alone and name the fact they still need.
//! This module fetches those facts lazily and at most once:
//! an invocation whose rules
//! need nothing starts no Git process,
//! and several rules that need the repository's
//! location share one query.
//! Tests drive the rules with a scripted provider.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! // const facts = createGitFacts({ gitPath, globalPrefix }); await facts.location();
//! ```
/// 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 fact has a pure core that builds its query and reads its answer;
/// this module
/// only runs the query.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// import { indexQueryArguments, indexStateFromExit } from './rule_commit_index.ts';
/// ```
use super::candidate_error::CandidateError;
/// Candidate preparation on a private index.
use super::candidate_prediction::{CandidateRequest, PreparedCandidates, prepare_candidates};
/// The captured-query runner and its byte helpers.
use super::git_metadata::{MetadataOutput, path_from_git_bytes, run_metadata_git};
use super::repository_location::{
RepositoryLocation, location_query_arguments, parse_location_output,
};
use super::rule_branch_worktree::{remote_guess_creates_branch, remote_guess_query_arguments};
use super::rule_commit_index::{IndexVsHead, index_query_arguments, index_state_from_exit};
use super::rule_commit_sequencer::{
SequencerFacts, SequencerState, sequencer_head_paths, sequencer_query_arguments,
sequencer_state, sequencer_state_when_query_fails,
};
/// The transaction Git context.
use super::transaction_git::GitContext;
/// What:
/// `OsStr`/`OsString` are borrowed/owned operating-system text of raw bytes.
/// Sibling
/// the reader might expect:
/// `&str`/`String`,
/// which must be valid UTF-8.
/// Why:
/// Global options and branch names are passed to Git unchanged.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// // string, but byte-preserving.
/// ```
use std::ffi::{OsStr, OsString};
/// `Path`/`PathBuf` are borrowed/owned filesystem paths of raw bytes.
use std::path::{Path, PathBuf};
/// What:
/// The facts a rule may ask for.
/// A `trait` is a named set of methods a type promises
/// to provide,
/// like a TS `interface`;
/// `&mut self` lends the provider for writing,
/// because it remembers answers.
/// `Result<T, String>` is "a value,
/// or a message
/// saying why Git could not be asked or understood".
/// Why:
/// Two providers exist:
/// real Git for the executable,
/// a script for tests.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// interface RepositoryFacts { location(): Promise<RepositoryLocation>; indexVsHead(): Promise<IndexVsHead>; /* ... */ }
/// ```
pub trait RepositoryFacts {
/// Repository identity,
/// worktree top level and the path below it;
/// asked once.
fn location(&mut self) -> Result<RepositoryLocation, String>;
/// Whether the index differs from `HEAD`.
fn index_vs_head(&mut self) -> Result<IndexVsHead, String>;
/// Whether a merge,
/// cherry-pick or revert awaits its concluding commit.
fn sequencer_state(&mut self) -> Result<SequencerState, String>;
/// Whether `git switch <target>` or `git checkout <target>` would create a local
/// branch from the one remote branch of that name.
fn remote_guess_creates_branch(&mut self, target: &OsStr) -> Result<bool, String>;
/// The candidates a content policy reads for this request,
/// prepared on a private
/// index;
/// a failure carries the candidate layer's cause.
fn candidates(
&mut self,
request: &CandidateRequest,
) -> Result<PreparedCandidates, CandidateError>;
/// The location when it was already asked for,
/// without asking.
fn asked_location(&self) -> Option<Result<RepositoryLocation, String>>;
/// Real Git,
/// the child environment and the invocation's global options,
/// for transaction
/// and recovery work.
fn transaction_context(&self) -> GitContext;
}
/// What:
/// The provider that asks real Git.
/// A `struct` is a record with named fields;
/// `Option<T>` is "a value or nothing";
/// `usize` is the unsigned integer of counts.
/// Why:
/// It owns copies of the executable path,
/// the caller's global options and the child
/// environment additions,
/// so it can be handed around without borrowing rules.
/// The
/// query count lets a test prove how many Git processes an invocation started.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type GitFacts = { realGit: string; globalPrefix: string[]; overlay: [string, string][]; location?: Result; queries: number };
/// ```
#[derive(Clone, Debug)]
pub struct GitFacts {
/// The selected real Git executable.
real_git: PathBuf,
/// The arguments before the subcommand,
/// without wrapper controls.
global_prefix: Vec<OsString>,
/// Variables added to every Git child.
overlay: Vec<(OsString, OsString)>,
/// The remembered location answer,
/// once asked.
location: Option<Result<RepositoryLocation, String>>,
/// How many Git processes this provider started.
pub queries: usize,
}
/// What:
/// Build a provider that has asked nothing yet.
/// `&Path` and `&[...]` borrow the
/// caller's values;
/// the provider keeps its own copies.
/// Why:
/// Creating a provider costs no process;
/// only a rule that needs a fact pays for it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// function gitFacts(realGit: string, globalPrefix: string[], overlay: [string, string][]): GitFacts;
/// ```
pub fn git_facts(
real_git: &Path,
global_prefix: &[OsString],
overlay: &[(OsString, OsString)],
) -> GitFacts {
return GitFacts {
// `.to_path_buf()` and `.to_vec()` copy the borrowed values into owned ones.
real_git: real_git.to_path_buf(),
global_prefix: global_prefix.to_vec(),
overlay: overlay.to_vec(),
// `None` is the "absent" case of `Option`: nothing has been asked.
location: None,
queries: 0,
};
}
/// What:
/// Say that real Git could not be started for a fact.
/// `&std::io::Error` borrows the
/// operating system's error.
/// Why:
/// The message names the executable and the fact,
/// so the reader knows what failed.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const message = `cli-git could not start ${realGit} to read ${fact}: ${error}`;
/// ```
fn not_started(real_git: &Path, fact: &str, error: &std::io::Error) -> String {
// `format!` builds owned text; `.display()` shows a path, replacing undecodable bytes.
return format!(
"cli-git could not start {} to read {fact}: {error}",
real_git.display()
);
}
/// Private helpers of the real-Git provider.
impl GitFacts {
/// What:
/// Run one captured Git query after the caller's global options and count it.
/// `&mut self` lends the provider for writing;
/// `Vec<OsString>` is the owned
/// argument list,
/// already starting with the global options.
/// Why:
/// Every fact goes through here,
/// so the count cannot miss a process.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async query(args: string[], fact: string): Promise<MetadataOutput>;
/// ```
fn query(&mut self, arguments: Vec<OsString>, fact: &str) -> Result<MetadataOutput, String> {
self.queries += 1;
// `match` unpacks the start result; `Ok`/`Err` are the success/failure variants.
match run_metadata_git(
self.real_git.as_path(),
arguments.as_slice(),
self.overlay.as_slice(),
) {
Ok(output) => return Ok(output),
// `&error` borrows the error for the message.
Err(error) => return Err(not_started(self.real_git.as_path(), fact, &error)),
}
}
}
/// What:
/// `impl RepositoryFacts for GitFacts` provides the interface's methods for the
/// real-Git provider.
/// Why:
/// Each method is one query whose arguments and reading belong to a pure core.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// class GitFacts implements RepositoryFacts { /* ... */ }
/// ```
impl RepositoryFacts for GitFacts {
fn location(&mut self) -> Result<RepositoryLocation, String> {
// `if let Some(known) = &self.location` borrows the remembered answer when present.
if let Some(known) = &self.location {
// `.clone()` copies the remembered answer for the caller.
return known.clone();
}
let arguments: Vec<OsString> = location_query_arguments(self.global_prefix.as_slice());
let answer: Result<RepositoryLocation, String> =
match self.query(arguments, "the repository location") {
Ok(output) => read_location(&output),
Err(message) => Err(message),
};
// `Some(x)` is the "present" case of `Option`.
self.location = Some(answer.clone());
return answer;
}
fn index_vs_head(&mut self) -> Result<IndexVsHead, String> {
let arguments: Vec<OsString> = index_query_arguments(self.global_prefix.as_slice());
// A trailing `?` returns the failure to our caller, or unwraps the output.
let output: MetadataOutput = self.query(arguments, "the index state")?;
return Ok(index_state_from_exit(output.code));
}
fn sequencer_state(&mut self) -> Result<SequencerState, String> {
let arguments: Vec<OsString> = sequencer_query_arguments(self.global_prefix.as_slice());
let output: MetadataOutput = self.query(arguments, "the Git directory")?;
if !output.success {
return Ok(sequencer_state_when_query_fails());
}
let paths: Vec<Vec<u8>> = match sequencer_head_paths(output.stdout.as_slice()) {
Ok(found) => found,
Err(error) => return Err(error.to_string()),
};
return Ok(sequencer_state(SequencerFacts {
merge_head: head_file_exists(paths[0].as_slice()),
cherry_pick_head: head_file_exists(paths[1].as_slice()),
revert_head: head_file_exists(paths[2].as_slice()),
}));
}
fn remote_guess_creates_branch(&mut self, target: &OsStr) -> Result<bool, String> {
let arguments: Vec<OsString> =
remote_guess_query_arguments(self.global_prefix.as_slice(), target);
let output: MetadataOutput = self.query(arguments, "branch names")?;
if !output.success {
return Ok(false);
}
// `.as_encoded_bytes()` lends the raw bytes of the branch name.
return Ok(remote_guess_creates_branch(
output.stdout.as_slice(),
target.as_encoded_bytes(),
));
}
/// Prepare the candidates with this provider's Git,
/// prefix and environment.
fn candidates(
&mut self,
request: &CandidateRequest,
) -> Result<PreparedCandidates, CandidateError> {
// The same Git, prefix and environment as every other question, so the prediction
// looks at the repository the command will change.
return prepare_candidates(
self.real_git.as_path(),
self.global_prefix.as_slice(),
self.overlay.as_slice(),
request,
);
}
fn asked_location(&self) -> Option<Result<RepositoryLocation, String>> {
return self.location.clone();
}
fn transaction_context(&self) -> GitContext {
return GitContext {
real_git: self.real_git.clone(),
overlay: self.overlay.clone(),
global_prefix: self.global_prefix.clone(),
};
}
}
/// What:
/// Read the location answer,
/// turning an unreadable one into a message.
/// `&MetadataOutput` borrows the captured query result.
/// Why:
/// Callers of the facts interface report failures as text.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// try { return parseLocationOutput(output.success, output.stdout); } catch (e) { throw String(e); }
/// ```
fn read_location(output: &MetadataOutput) -> Result<RepositoryLocation, String> {
match parse_location_output(output.success, output.stdout.as_slice()) {
Ok(location) => return Ok(location),
// `.to_string()` renders the input/output error as owned text.
Err(error) => return Err(error.to_string()),
}
}
/// What:
/// Whether the file at the path Git printed exists,
/// without following a final link.
/// `&[u8]` borrows the path bytes.
/// Why:
/// Git itself tests these head files with `lstat`,
/// so a dangling link counts as
/// present here too.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const exists = await lstat(path).then(() => true, () => false);
/// ```
fn head_file_exists(path: &[u8]) -> bool {
// `match` unpacks "a path or nothing": empty bytes are not a path.
match path_from_git_bytes(path) {
// `.is_ok()` is true when the file's metadata could be read.
Some(found) => return std::fs::symlink_metadata(found).is_ok(),
None => return false,
}
}
/// Query-count and disposable-repository controls stay out of the release executable.
#[cfg(test)]
#[path = "repository_facts_tests.rs"]
mod tests;