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
//! What:
//! The two places where the wrapper's Git runners try the in-process answers first.
//! Why:
//! `run_metadata_git` (captured queries) and `run_git` (transaction requests) are the choke points every read-only
//! Git start goes through, so one call in each gives every caller the saving and keeps the fallback to Git in one place.
//! Setting `CLI_GIT_IN_PROCESS_READS=off` in the wrapper's environment turns the answers off, for a measurement that
//! compares both and for diagnosing a difference.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! const output = tryInProcess(args, overlay) ?? (await spawnGit(args, overlay));
//! ```
use super::git_metadata::MetadataOutput;
#[cfg(unix)]
use super::repo_read_config_scan::ScopeEnvironment;
#[cfg(unix)]
use super::repo_read_query::{Answer, Query, answer_with};
use super::transaction_git::{GitContext, GitOutput, GitRequest};
use std::ffi::OsString;
#[cfg(unix)]
use std::path::Path;
/// The wrapper environment variable that turns the in-process answers off when it holds `off`.
pub const SWITCH_VARIABLE: &str = "CLI_GIT_IN_PROCESS_READS";
/// Whether the answers are enabled in this process and for this run's added variables.
pub fn enabled(variables: &[(OsString, OsString)]) -> bool {
let added_off: bool = variables.iter().any(|(name, value)| { return name == SWITCH_VARIABLE && value == "off"; });
return !added_off && std::env::var_os(SWITCH_VARIABLE).is_none_or(|value| { return value != "off"; });
}
/// What:
/// The overlay entry that makes a runner start Git for every command line.
/// Why:
/// Controls that count the starts of a stand-in Git, or that need an unstartable Git to fail, measure the process
/// path, which stays the fallback for everything the readers decline.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// const switchOff = [['CLI_GIT_IN_PROCESS_READS', 'off']];
/// ```
#[cfg(test)]
pub fn switch_off() -> [(OsString, OsString); 1] {
return [(OsString::from(SWITCH_VARIABLE), OsString::from("off"))];
}
/// What:
/// Run a reader and turn a panic inside it into "start Git".
/// Why:
/// The readers parse bytes of any repository; a defect that panics must cost one Git start, not the wrapper's command.
/// The release profile keeps `panic = "unwind"` for this.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// try { return read(); } catch (error) { log(error); return undefined; }
/// ```
pub fn guarded<T>(read: impl FnOnce() -> Option<T>) -> Option<T> {
return match std::panic::catch_unwind(std::panic::AssertUnwindSafe(read)) {
Ok(found) => found,
Err(payload) => {
let message: &str = payload.downcast_ref::<&str>().copied().or_else(|| { return payload.downcast_ref::<String>().map(String::as_str); }).unwrap_or("a panic without text");
super::diagnostic_log::warn("repo-read-guard", format!("an in-process read panicked and Git answers instead: {message}").as_str());
None
}
};
}
/// The answer for a command line, if the readers have one and the switch allows it.
#[cfg(unix)]
fn attempt(
arguments: &[OsString],
directory: Option<&Path>,
base: Vec<(OsString, OsString)>,
variables: &[(OsString, OsString)],
input: Option<&[u8]>,
) -> Option<Answer> {
if !enabled(variables) {
return None;
}
let mut effective: Vec<(OsString, OsString)> = base.clone();
effective.extend(variables.iter().cloned());
let scope: ScopeEnvironment = ScopeEnvironment::from_variables(effective.as_slice());
return guarded(|| { return answer_with(&Query { arguments, directory, variables, input }, base, &scope); });
}
/// What:
/// The captured output of a query run in the process directory with `overlay` added to Git's environment.
/// Why:
/// `None` means "start Git".
///
/// In TS you'd write (pseudocode):
/// ```ts
/// tryMetadata(args, overlay): MetadataOutput | undefined
/// ```
#[cfg(unix)]
pub fn try_metadata(arguments: &[OsString], overlay: &[(OsString, OsString)]) -> Option<MetadataOutput> {
let found: Answer = attempt(arguments, None, std::env::vars_os().collect(), overlay, None)?;
return Some(MetadataOutput { success: found.code == 0, code: Some(found.code), stdout: found.stdout, stderr: found.stderr });
}
/// Without file system readers for this platform, every query starts Git.
#[cfg(not(unix))]
pub fn try_metadata(_arguments: &[OsString], _overlay: &[(OsString, OsString)]) -> Option<MetadataOutput> {
return None;
}
/// What:
/// The answer to a query run in `directory` with a complete `environment`, as the auto-push coordination runs them.
/// Why:
/// That runner builds the whole child environment itself, so nothing of this process's own is added to it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// tryComplete(args, directory, environment): { code: number; stdout: Uint8Array; stderr: Uint8Array } | undefined
/// ```
#[cfg(unix)]
pub fn try_complete(arguments: &[OsString], directory: &Path, environment: &[(OsString, OsString)]) -> Option<(Option<i32>, Vec<u8>, Vec<u8>)> {
let found: Answer = attempt(arguments, Some(directory), Vec::new(), environment, None)?;
return Some((Some(found.code), found.stdout, found.stderr));
}
/// Without file system readers for this platform, every query starts Git.
#[cfg(not(unix))]
pub fn try_complete(_arguments: &[OsString], _directory: &std::path::Path, _environment: &[(OsString, OsString)]) -> Option<(Option<i32>, Vec<u8>, Vec<u8>)> {
return None;
}
/// What:
/// The output of a transaction request, if the readers can answer it.
/// Why:
/// The request names its directory, its private index and object directory, the variables to add and to remove,
/// and whether the invocation's global options apply; the readers see exactly the command Git would have run.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// tryRequest(context, request): GitOutput | undefined
/// ```
#[cfg(unix)]
pub fn try_request(context: &GitContext, request: &GitRequest) -> Option<GitOutput> {
let mut arguments: Vec<OsString> = Vec::new();
if !request.without_prefix {
arguments.extend(context.global_prefix.iter().cloned());
}
arguments.extend(request.arguments.iter().cloned());
// The context names the Git it runs, which the readers need to learn the system configuration file.
// The context names the Git it runs, which the readers need to learn the system configuration file.
let mut variables: Vec<(OsString, OsString)> = super::git_system_config::with_real_git(context.overlay.as_slice(), context.real_git.as_path());
variables.extend(request.environment.iter().cloned());
if let Some(index) = &request.index {
variables.push((OsString::from("GIT_INDEX_FILE"), index.clone().into_os_string()));
}
if let Some(objects) = &request.object_directory {
variables.push((OsString::from("GIT_OBJECT_DIRECTORY"), objects.clone().into_os_string()));
}
let base: Vec<(OsString, OsString)> = std::env::vars_os()
.filter(|(name, _)| { return !request.unset.contains(name); })
.collect();
let found: Answer = attempt(arguments.as_slice(), Some(request.cwd.as_path()), base, variables.as_slice(), request.input.as_deref())?;
return Some(GitOutput { code: Some(found.code), stdout: found.stdout, stderr: found.stderr });
}
/// Without file system readers for this platform, every request starts Git.
#[cfg(not(unix))]
pub fn try_request(_context: &GitContext, _request: &GitRequest) -> Option<GitOutput> {
return None;
}
#[cfg(test)]
#[path = "repo_read_hook_tests.rs"]
mod tests;
/// Differential controls for the in-process `cat-file --batch` request.
#[cfg(test)]
#[path = "repo_read_batch_tests.rs"]
mod batch_tests;