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
//! 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> {
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}")); })?;
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;