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
//! What:
//! Creates the shadow repository of one published transaction:
//! an empty bare repository,
//! its config and alternates,
//! the linked shared entries,
//! the conclusion state,
//! and the ref snapshot.
//! Why:
//! The shadow is a separate repository rather than a registered worktree,
//! so `git worktree list` never shows it and preparation creates no ref in the real repository.
//! Its object store receives the objects native `git commit` writes,
//! and its alternates name the real object directory,
//! so a real `git prune` cannot delete a pending commit
//! (`src/shadow-repository/shadow-repository.ts`).
//!
//! In TS you'd write (pseudocode):
//! ```ts
//! await createShadowRepository({ gitPath, cwd, capture, transactionId, transactionDirectory });
//! ```
/// Shadow path, redirect variables and the shadow request.
use super::shadow_git::{REPOSITORY_REDIRECT_VARIABLES, shadow_repository_path};
/// Invocation capture.
use super::capture_invocation::{InvocationCapture, InvocationLayout};
/// The in-process entries of an empty bare repository.
use super::shadow_skeleton::create_shadow_skeleton;
/// Format copy and config text.
use super::shadow_config::{format_shadow_config, read_repository_format, repository_format_from_snapshot};
/// Entry links.
use super::shadow_links::link_shadow_entries;
/// Conclusion copy and ref snapshot.
use super::conclusion_copy::copy_conclusion_state;
use super::shadow_refs::write_shadow_refs;
/// Private directory creation and file writes.
use super::private_storage::create_private_directory;
/// Process context and one required Git start.
use super::transaction_git::{GitContext, GitFailure, GitRequest, run_git_checked};
/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
use std::path::{Path, PathBuf};
/// Strings for the redirect variable list.
use std::ffi::OsString;
/// What:
/// Overwrites a file the shadow's `git init` already created, with private mode.
/// Why:
/// The incumbent's `writeFile` replaces the template `config` and `alternates`,
/// while the exclusive writer used for new files would refuse them.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await writeFile(path, bytes, { mode: 0o600 });
/// ```
fn overwrite_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
std::fs::write(path, bytes)?;
return set_private_file(path);
}
/// What:
/// Gives a file the private file mode on Unix.
/// Why:
/// The shadow config names the real hooks directory and worktree; it stays private to the account.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await chmod(path, 0o600);
/// ```
#[cfg(unix)]
fn set_private_file(path: &Path) -> std::io::Result<()> {
use std::os::unix::fs::PermissionsExt;
return std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
}
/// Windows has no mode bits for this file; the incumbent's ACL work is not ported.
// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
#[cfg(windows)]
fn set_private_file(_path: &Path) -> std::io::Result<()> {
return Ok(());
}
/// What:
/// Creates the shadow's parent directories as private directories.
/// Why:
/// The incumbent creates `cli-git/shadow` with mode 0700.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await mkdir(dirname(shadowPath), { recursive: true, mode: 0o700 });
/// ```
fn create_parent(shadow: &Path) -> std::io::Result<()> {
let parent: &Path = shadow.parent().unwrap_or(shadow);
if let Some(grandparent) = parent.parent() {
ensure_private_directory(grandparent)?;
}
return ensure_private_directory(parent);
}
/// What:
/// Create a private directory unless another process made it first.
/// Why:
/// Concurrent commits of one repository race to create the shared `cli-git/shadow` parents; whoever loses has
/// nothing left to do. A path that exists as anything but a directory is still a failure.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await mkdir(directory, { mode: 0o700 }).catch((error) => { if (error.code !== 'EEXIST') throw error; });
/// ```
pub fn ensure_private_directory(directory: &Path) -> std::io::Result<()> {
match create_private_directory(directory) {
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists && directory.is_dir() => return Ok(()),
other => return other,
}
}
/// What:
/// Creates an empty bare repository with `git init`, for the ref backend whose tables this crate does not write.
/// Why:
/// Reftable tables carry their own format, so only Git creates them.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await runGit({ args: ['init', '--bare', '--quiet', '--template=', `--object-format=${format}`, '--ref-format=reftable', shadow] });
/// ```
fn init_with_git(
context: &GitContext,
layout: &InvocationLayout,
shadow: &Path,
) -> Result<(), GitFailure> {
let arguments: Vec<String> = vec![
String::from("init"),
String::from("--bare"),
String::from("--quiet"),
String::from("--template="),
format!("--object-format={}", layout.object_format),
String::from("--ref-format=reftable"),
shadow.display().to_string(),
];
let mut init: GitRequest = GitRequest::new(layout.common_dir.as_path(), &arguments);
init.without_prefix = true;
init.unset = REPOSITORY_REDIRECT_VARIABLES.iter().map(|name| { return OsString::from(*name); }).collect();
run_git_checked(context, &init)?;
return Ok(());
}
/// What:
/// Creates the shadow repository of one published transaction.
/// Why:
/// Every step runs before any native Git command reads the shadow,
/// so the first read already sees the real config, objects, links and refs.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await createShadowRepository({ gitPath, cwd, capture, transactionId, transactionDirectory });
/// ```
pub fn create_shadow_repository(
context: &GitContext,
cwd: &Path,
capture: &InvocationCapture,
transaction_id: &str,
transaction_directory: &Path,
) -> Result<PathBuf, GitFailure> {
let shadow: PathBuf = shadow_repository_path(capture.layout.common_dir.as_path(), transaction_id);
create_parent(shadow.as_path())
.map_err(|error| { return GitFailure(format!("creating {} failed: {error}", shadow.display())); })?;
create_shadow_repository_at(context, cwd, capture, shadow.as_path(), transaction_directory)?;
return Ok(shadow);
}
/// What:
/// Creates a shadow repository at a given path, whose parent directory exists.
/// Why:
/// The commit service builds the commits Git must build (an editor, a signature) in a scratch repository inside its own
/// scratch directory, away from the registry directory that recovery scans, so no other process mistakes it for a dead
/// transaction's shadow.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await createShadowRepositoryAt({ gitPath, cwd, capture, shadowPath, transactionDirectory });
/// ```
pub fn create_shadow_repository_at(
context: &GitContext,
cwd: &Path,
capture: &InvocationCapture,
shadow: &Path,
transaction_directory: &Path,
) -> Result<(), GitFailure> {
return create_shadow_repository_carrying(context, cwd, capture, shadow, transaction_directory, true);
}
/// What:
/// `create_shadow_repository_at`, with the merge, cherry-pick and revert conclusion state of the worktree either copied into
/// the scratch repository or left out of it.
/// Why:
/// A commit of named paths while another session's merge is unfinished must build as an ordinary commit of those paths on
/// `HEAD` (real Git refuses it as a partial commit during a merge); a scratch repository that holds no `MERGE_HEAD` builds
/// exactly that, and the landing then leaves the worktree's own conclusion state alone because the scratch repository
/// never held it.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await createShadowRepositoryCarrying({ gitPath, cwd, capture, shadowPath, transactionDirectory, carryConclusion });
/// ```
pub fn create_shadow_repository_carrying(
context: &GitContext,
cwd: &Path,
capture: &InvocationCapture,
shadow: &Path,
transaction_directory: &Path,
carry_conclusion: bool,
) -> Result<(), GitFailure> {
let layout = &capture.layout;
if layout.ref_format == super::transaction_journal::RefFormat::Reftable {
// The reftable backend's tables are Git's to create, so this one case still starts `git init`.
init_with_git(context, layout, shadow)?;
} else {
create_shadow_skeleton(shadow)
.map_err(|error| { return GitFailure(format!("creating {} failed: {error}", shadow.display())); })?;
}
set_private_directory_mode(shadow)
.map_err(|error| { return GitFailure(format!("protecting {} failed: {error}", shadow.display())); })?;
let format = match layout.config_snapshot.as_ref().and_then(repository_format_from_snapshot) {
Some(from_snapshot) => from_snapshot,
None => read_repository_format(context, layout.common_dir.as_path())?,
};
let config_text: String =
format_shadow_config(&format, layout.common_dir.as_path(), layout.repository_root.as_path());
overwrite_private(shadow.join("config").as_path(), config_text.as_bytes())
.map_err(|error| { return GitFailure(format!("writing the shadow config failed: {error}")); })?;
overwrite_private(
shadow.join("objects").join("info").join("alternates").as_path(),
format!("{}\n", layout.object_directory.display()).as_bytes(),
)
.map_err(|error| { return GitFailure(format!("writing the shadow alternates failed: {error}")); })?;
link_shadow_entries(shadow, layout.common_dir.as_path(), layout.git_dir.as_path())
.map_err(|error| { return GitFailure(format!("linking the shadow entries failed: {error}")); })?;
if carry_conclusion {
copy_conclusion_state(
context,
cwd,
layout.git_dir.as_path(),
shadow,
transaction_directory,
layout.ref_format,
)?;
}
write_shadow_refs(
context,
cwd,
shadow,
layout.ref_format,
layout.target_ref.as_str(),
&layout.symbolic_head,
&capture.base,
)?;
return Ok(());
}
/// What:
/// Gives the shadow directory the private mode.
/// Why:
/// The shadow holds the transaction's objects and refs.
///
/// In TS you'd write (pseudocode):
/// ```ts
/// await chmod(shadowPath, 0o700);
/// ```
#[cfg(unix)]
fn set_private_directory_mode(path: &Path) -> std::io::Result<()> {
use std::os::unix::fs::PermissionsExt;
return std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700));
}
/// Windows has no mode bits for the shadow directory.
// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
#[cfg(windows)]
fn set_private_directory_mode(_path: &Path) -> std::io::Result<()> {
return Ok(());
}
/// Shadow creation controls stay out of the release executable.
#[cfg(test)]
#[path = "shadow_create_tests.rs"]
mod tests;