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
//! What:
//! Running a forwarded index writer as a child of the wrapper:
//! wait for the commit service's own hold on the index, complete an isolated merge when one applies, run real Git
//! (again when the service took the index first), and report its status.
//! Why:
//! A writer competes for the index only through Git's own `index.lock`, as it does without the wrapper; the one holder
//! that is waited for is the commit service, whose hold ends by itself. The wrapper stays alive around Git so that a
//! run the service beat to the lock can be started again and so that a merge the staged index would make Git refuse can
//! be completed. A command that needs neither replaces the process with Git.
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! await waitForServiceHold({ realIndex, command });
//! await runGitWithWorktreeCopy({ args, gitPath, environment });
//! ```
/// The wait for the commit service's own hold on the index.
use super::service_index_wait::wait_for_service_hold_here;
/// Starting a command again when the commit service took the index first.
use super::service_hold_retry::{may_run_again, run_again_while_service_holds};
/// Running real Git, settling the linked-worktree copy around it when one applies.
use super::worktree_copy_forward::{CopyFacts, ForwardedRun, run_forwarded_git};
/// The resolved forwarded command.
use super::forwarded_command::ResolvedCommand;
/// The classification of index writers.
use super::index_writer_commands::is_index_writer;
/// The index a command writes.
use super::landing_location::{LandingLocation, resolve_landing_location};
/// The failure event rendered for standard error.
use super::diagnostics::{EngineFailureCode, render_engine_failure};
/// The once-per-process Git version check.
use super::git_version::check_git_version;
/// The repository shape the invocation selects.
use super::worktree_identity::WorktreeIdentity;
/// A merge the staged index would make native Git refuse, completed without disturbing the staged work.
use super::merge_isolation::{IsolatedMerge, IsolatedMergeInput, run_isolated_merge};
/// Running real Git for the location query.
use super::transaction_git::GitContext;
/// `OsString` is owned operating-system text of raw bytes.
use std::ffi::OsString;
/// Writing the merge's own output.
use std::io::Write;
/// `Path` and `PathBuf` are borrowed and owned filesystem paths.
use std::path::PathBuf;
/// What:
/// Everything a forward that stays alive around Git needs, owned, so the planned action can carry it.
/// Why:
/// The plan decides that the wrapper stays alive; the run performs it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ForwardToRealGit = { realGit: string; args: string[]; overlay: [string, string][]; identity; command; environment };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CoordinatedForward {
/// Real Git executable.
pub real_git: PathBuf,
/// Arguments Git receives,
/// as the caller gave them.
pub arguments: Vec<OsString>,
/// Environment variables added to Git's inherited environment.
pub overlay: Vec<(OsString, OsString)>,
/// Repository shape of the invocation.
pub identity: WorktreeIdentity,
/// The command after alias expansion,
/// which decides what runs around Git.
pub command: ResolvedCommand,
/// The environment the invocation inherited.
pub environment: Vec<(OsString, OsString)>,
/// The linked-worktree copy to settle around Git, when it applies to this invocation.
pub copy: Option<CopyFacts>,
}
/// What:
/// How a forward that stays alive around Git ended.
/// Why:
/// Only a run that did not stay alive may replace the process.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// type ForwardOutcome = 'not-coordinated' | { exitCode: number } | { stderr: string; exitCode: number };
/// ```
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum CoordinatedRun {
/// Nothing needs the wrapper alive;
/// the caller replaces its process with Git.
NotCoordinated,
/// Git ran as a child and exited with this status.
Ran(i32),
/// The command did not run;
/// its standard error and exit status.
Failed {
/// Text for standard error.
stderr: String,
/// Exit status.
code: i32,
},
}
/// What:
/// The location of the real index a command writes, or nothing when the command writes none that the commit service holds.
/// Why:
/// Only a command that writes the worktree's own index competes with the service's landing; a command outside a
/// repository, or one writing another index file, has nothing to wait for.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// if (!isIndexWriter(command) || location.indexPath !== location.realIndexPath) return undefined;
/// ```
fn written_real_index(context: &GitContext, command: &ResolvedCommand) -> Result<Option<LandingLocation>, String> {
if !is_index_writer(command) {
return Ok(None);
}
let global: &[OsString] = &command.arguments[..command.subcommand_index];
let Some(location) = resolve_landing_location(context, global)? else {
return Ok(None);
};
if location.index_path != location.real_index_path {
return Ok(None);
}
return Ok(Some(location));
}
/// What:
/// Run the forward when it stays alive around Git, and say when it does not.
/// Why:
/// `forwardToRealGit`: a writer waits for the service's hold, runs Git as a child, and returns Git's status; a merge
/// that native Git would refuse only for the staged index is completed first.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// async function forwardToRealGit({ args, gitPath, identity }): Promise<void>;
/// ```
pub fn run_coordinated_forward(forward: &CoordinatedForward) -> CoordinatedRun {
// The context runs the location query with the same overlay as the command.
let context: GitContext = GitContext {
real_git: forward.real_git.clone(),
overlay: forward.overlay.clone(),
global_prefix: Vec::new(),
// The location query sends no input, so no scratch directory is needed.
common_dir: None,
};
let location: LandingLocation = match written_real_index(&context, &forward.command) {
Ok(Some(found)) => found,
Ok(None) => {
// A copy keeps the wrapper alive around Git; otherwise the caller may replace the process.
let Some(facts) = forward.copy.as_ref() else {
return CoordinatedRun::NotCoordinated;
};
return from_forwarded(run_forwarded_git(
forward.real_git.as_path(),
forward.arguments.as_slice(),
forward.overlay.as_slice(),
Some(facts),
));
}
Err(reason) => return failed_with(EngineFailureCode::ContentUnavailable, reason.as_str()),
};
// The version check runs once per process, before the first work that depends on Git's behavior.
if let Err(reason) = check_git_version(&context) {
return failed_with(EngineFailureCode::ContentUnavailable, reason.as_str());
}
let name: String = String::from_utf8_lossy(forward.command.word()).into_owned();
// The commit service holds the index while it lands a commit, which ends by itself: the command waits for it, naming
// it, before Git starts. A lock that is not the service's is a foreign program's and keeps the behavior of native Git.
wait_for_service_hold_here(location.real_index_path.as_path(), name.as_str());
// A merge that native Git would refuse only because the index holds staged changes is completed in isolation.
if forward.command.word() == b"merge" {
let merge_input: IsolatedMergeInput<'_> = IsolatedMergeInput {
real_git: forward.real_git.as_path(),
overlay: forward.overlay.as_slice(),
identity: &forward.identity,
command: &forward.command,
environment: forward.environment.as_slice(),
};
match run_isolated_merge(&merge_input) {
IsolatedMerge::Forward { notice } => {
if let Some(text) = notice {
let _ = std::io::stderr().write_all(text.as_bytes());
}
}
IsolatedMerge::Finished { code, stdout, stderr } => {
let _ = std::io::stdout().write_all(stdout.as_bytes());
let _ = std::io::stderr().write_all(stderr.as_bytes());
return CoordinatedRun::Ran(code);
}
}
}
// A command that takes the index lock before it changes anything is run again when the commit service took the index first.
let outcome: ForwardedRun = if forward.copy.is_none() && may_run_again(forward.command.word(), forward.command.region()) {
run_again_while_service_holds(
forward.real_git.as_path(),
forward.arguments.as_slice(),
forward.overlay.as_slice(),
forward.environment.as_slice(),
location.real_index_path.as_path(),
name.as_str(),
&mut std::io::stderr(),
)
} else {
run_forwarded_git(
forward.real_git.as_path(),
forward.arguments.as_slice(),
forward.overlay.as_slice(),
forward.copy.as_ref(),
)
};
return from_forwarded(outcome);
}
/// What:
/// The coordinated run that a forwarded run stands for.
/// Why:
/// Both report the same two endings, so the status and the failure text pass through unchanged.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// // the same exit status or the same failure text
/// ```
fn from_forwarded(run: ForwardedRun) -> CoordinatedRun {
// `match` on the ending: a status, or a failure with its own text.
match run {
ForwardedRun::Ran(code) => return CoordinatedRun::Ran(code),
ForwardedRun::Failed { stderr, code } => return CoordinatedRun::Failed { stderr, code },
}
}
/// What:
/// The engine failure a command that could not start reports.
/// Why:
/// A location or version that could not be read stops the command with the engine failure's status.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// renderPolicyEvents([createEngineFailureEvent({ code, message })])
/// ```
fn failed_with(code: EngineFailureCode, message: &str) -> CoordinatedRun {
return CoordinatedRun::Failed {
stderr: render_engine_failure(0, code, message),
code: super::action::ENGINE_FAILURE_EXIT_CODE,
};
}
/// Coordinated forwarding controls stay out of the release executable.
#[cfg(test)]
#[path = "index_writer_forward_tests.rs"]
mod tests;