git_policy_cli/shadow/shadow_create.rs
1//! What:
2//! Creates the shadow repository of one published transaction:
3//! an empty bare repository,
4//! its config and alternates,
5//! the linked shared entries,
6//! the conclusion state,
7//! and the ref snapshot.
8//! Why:
9//! The shadow is a separate repository rather than a registered worktree,
10//! so `git worktree list` never shows it and preparation creates no ref in the real repository.
11//! Its object store receives the objects native `git commit` writes,
12//! and its alternates name the real object directory,
13//! so a real `git prune` cannot delete a pending commit
14//! (`src/shadow-repository/shadow-repository.ts`).
15//!
16//! In TS you'd write (pseudocode):
17//! ```ts
18//! await createShadowRepository({ gitPath, cwd, capture, transactionId, transactionDirectory });
19//! ```
20
21/// Shadow path, redirect variables and the shadow request.
22use crate::shadow_git::{REPOSITORY_REDIRECT_VARIABLES, shadow_repository_path};
23/// Invocation capture.
24use crate::capture_invocation::{InvocationCapture, InvocationLayout};
25/// The in-process entries of an empty bare repository.
26use crate::shadow_skeleton::create_shadow_skeleton;
27/// Format copy and config text.
28use crate::shadow_config::{format_shadow_config, read_repository_format, repository_format_from_snapshot};
29/// Entry links.
30use crate::shadow_links::link_shadow_entries;
31/// Conclusion copy and ref snapshot.
32use crate::conclusion_copy::copy_conclusion_state;
33use crate::shadow_refs::write_shadow_refs;
34/// Private directory creation and file writes.
35use crate::private_storage::create_private_directory;
36/// Where Git's words go now that the `init` carries no `--quiet`.
37use crate::inner_git_words::record_words;
38/// Process context and one required Git start.
39use crate::transaction_git::{GitContext, GitFailure, GitRequest, run_git_checked};
40/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
41use std::path::{Path, PathBuf};
42/// Strings for the redirect variable list.
43use std::ffi::OsString;
44
45/// What:
46/// Overwrites a file the shadow's `git init` already created, with private mode.
47/// Why:
48/// The incumbent's `writeFile` replaces the template `config` and `alternates`,
49/// while the exclusive writer used for new files would refuse them.
50///
51/// In TS you'd write (pseudocode):
52/// ```ts
53/// await writeFile(path, bytes, { mode: 0o600 });
54/// ```
55fn overwrite_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
56 std::fs::write(path, bytes)?;
57 return set_private_file(path);
58}
59
60/// What:
61/// Gives a file the private file mode on Unix.
62/// Why:
63/// The shadow config names the real hooks directory and worktree; it stays private to the account.
64///
65/// In TS you'd write (pseudocode):
66/// ```ts
67/// await chmod(path, 0o600);
68/// ```
69#[cfg(unix)]
70fn set_private_file(path: &Path) -> std::io::Result<()> {
71 use std::os::unix::fs::PermissionsExt;
72 return std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600));
73}
74
75/// Windows has no mode bits for this file; the incumbent's ACL work is not ported.
76// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
77#[cfg(windows)]
78fn set_private_file(_path: &Path) -> std::io::Result<()> {
79 return Ok(());
80}
81
82/// What:
83/// Creates the shadow's parent directories as private directories.
84/// Why:
85/// The incumbent creates `cli-git/shadow` with mode 0700.
86///
87/// In TS you'd write (pseudocode):
88/// ```ts
89/// await mkdir(dirname(shadowPath), { recursive: true, mode: 0o700 });
90/// ```
91fn create_parent(shadow: &Path) -> std::io::Result<()> {
92 let parent: &Path = shadow.parent().unwrap_or(shadow);
93 if let Some(grandparent) = parent.parent() {
94 ensure_private_directory(grandparent)?;
95 }
96 return ensure_private_directory(parent);
97}
98
99/// What:
100/// Create a private directory unless another process made it first.
101/// Why:
102/// Concurrent commits of one repository race to create the shared `cli-git/shadow` parents; whoever loses has
103/// nothing left to do. A path that exists as anything but a directory is still a failure.
104///
105/// In TS you'd write (pseudocode):
106/// ```ts
107/// await mkdir(directory, { mode: 0o700 }).catch((error) => { if (error.code !== 'EEXIST') throw error; });
108/// ```
109pub fn ensure_private_directory(directory: &Path) -> std::io::Result<()> {
110 match create_private_directory(directory) {
111 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists && directory.is_dir() => return Ok(()),
112 other => return other,
113 }
114}
115
116/// What:
117/// Creates an empty bare repository with `git init`, for the ref backend whose tables this crate does not write.
118/// Why:
119/// Reftable tables carry their own format, so only Git creates them.
120///
121/// In TS you'd write (pseudocode):
122/// ```ts
123/// await runGit({ args: ['init', '--bare', '--template=', `--object-format=${format}`, '--ref-format=reftable', shadow] });
124/// ```
125fn init_with_git(
126 context: &GitContext,
127 layout: &InvocationLayout,
128 shadow: &Path,
129) -> Result<(), GitFailure> {
130 let arguments: Vec<String> = vec![
131 String::from("init"),
132 String::from("--bare"),
133 String::from("--template="),
134 format!("--object-format={}", layout.object_format),
135 String::from("--ref-format=reftable"),
136 shadow.display().to_string(),
137 ];
138 let mut init: GitRequest = GitRequest::new(layout.common_dir.as_path(), &arguments);
139 init.without_prefix = true;
140 init.unset = REPOSITORY_REDIRECT_VARIABLES.iter().map(|name| { return OsString::from(*name); }).collect();
141 let made = run_git_checked(context, &init)?;
142 // The `init` carries no `--quiet`; what Git said of it is kept in the journal.
143 record_words(&["init", "--bare", "--template=", "--ref-format=reftable"], &made);
144 return Ok(());
145}
146
147/// What:
148/// Creates the shadow repository of one published transaction.
149/// Why:
150/// Every step runs before any native Git command reads the shadow,
151/// so the first read already sees the real config, objects, links and refs.
152///
153/// In TS you'd write (pseudocode):
154/// ```ts
155/// await createShadowRepository({ gitPath, cwd, capture, transactionId, transactionDirectory });
156/// ```
157pub fn create_shadow_repository(
158 context: &GitContext,
159 cwd: &Path,
160 capture: &InvocationCapture,
161 transaction_id: &str,
162 transaction_directory: &Path,
163) -> Result<PathBuf, GitFailure> {
164 let shadow: PathBuf = shadow_repository_path(capture.layout.common_dir.as_path(), transaction_id);
165 create_parent(shadow.as_path())
166 .map_err(|error| { return GitFailure(format!("creating {} failed: {error}", shadow.display())); })?;
167 create_shadow_repository_at(context, cwd, capture, shadow.as_path(), transaction_directory)?;
168 return Ok(shadow);
169}
170
171/// What:
172/// Creates a shadow repository at a given path, whose parent directory exists.
173/// Why:
174/// The commit service builds the commits Git must build (an editor, a signature) in a scratch repository inside its own
175/// scratch directory, away from the registry directory that recovery scans, so no other process mistakes it for a dead
176/// transaction's shadow.
177///
178/// In TS you'd write (pseudocode):
179/// ```ts
180/// await createShadowRepositoryAt({ gitPath, cwd, capture, shadowPath, transactionDirectory });
181/// ```
182pub fn create_shadow_repository_at(
183 context: &GitContext,
184 cwd: &Path,
185 capture: &InvocationCapture,
186 shadow: &Path,
187 transaction_directory: &Path,
188) -> Result<(), GitFailure> {
189 return create_shadow_repository_carrying(context, cwd, capture, shadow, transaction_directory, true);
190}
191
192/// What:
193/// `create_shadow_repository_at`, with the merge, cherry-pick and revert conclusion state of the worktree either copied into
194/// the scratch repository or left out of it.
195/// Why:
196/// A commit of named paths while another session's merge is unfinished must build as an ordinary commit of those paths on
197/// `HEAD` (real Git refuses it as a partial commit during a merge); a scratch repository that holds no `MERGE_HEAD` builds
198/// exactly that, and the landing then leaves the worktree's own conclusion state alone because the scratch repository
199/// never held it.
200///
201/// In TS you'd write (pseudocode):
202/// ```ts
203/// await createShadowRepositoryCarrying({ gitPath, cwd, capture, shadowPath, transactionDirectory, carryConclusion });
204/// ```
205pub fn create_shadow_repository_carrying(
206 context: &GitContext,
207 cwd: &Path,
208 capture: &InvocationCapture,
209 shadow: &Path,
210 transaction_directory: &Path,
211 carry_conclusion: bool,
212) -> Result<(), GitFailure> {
213 let layout = &capture.layout;
214 if layout.ref_format == crate::transaction_journal::RefFormat::Reftable {
215 // The reftable backend's tables are Git's to create, so this one case still starts `git init`.
216 init_with_git(context, layout, shadow)?;
217 } else {
218 create_shadow_skeleton(shadow)
219 .map_err(|error| { return GitFailure(format!("creating {} failed: {error}", shadow.display())); })?;
220 }
221 set_private_directory_mode(shadow)
222 .map_err(|error| { return GitFailure(format!("protecting {} failed: {error}", shadow.display())); })?;
223 let format = match layout.config_snapshot.as_ref().and_then(repository_format_from_snapshot) {
224 Some(from_snapshot) => from_snapshot,
225 None => read_repository_format(context, layout.common_dir.as_path())?,
226 };
227 let config_text: String =
228 format_shadow_config(&format, layout.common_dir.as_path(), layout.repository_root.as_path());
229 overwrite_private(shadow.join("config").as_path(), config_text.as_bytes())
230 .map_err(|error| { return GitFailure(format!("writing the shadow config failed: {error}")); })?;
231 overwrite_private(
232 shadow.join("objects").join("info").join("alternates").as_path(),
233 format!("{}\n", layout.object_directory.display()).as_bytes(),
234 )
235 .map_err(|error| { return GitFailure(format!("writing the shadow alternates failed: {error}")); })?;
236 link_shadow_entries(shadow, layout.common_dir.as_path(), layout.git_dir.as_path())
237 .map_err(|error| { return GitFailure(format!("linking the shadow entries failed: {error}")); })?;
238 if carry_conclusion {
239 copy_conclusion_state(
240 context,
241 cwd,
242 layout.git_dir.as_path(),
243 shadow,
244 transaction_directory,
245 layout.ref_format,
246 )?;
247 }
248 write_shadow_refs(
249 context,
250 cwd,
251 shadow,
252 layout.ref_format,
253 layout.target_ref.as_str(),
254 &layout.symbolic_head,
255 &capture.base,
256 )?;
257 return Ok(());
258}
259
260/// What:
261/// Gives the shadow directory the private mode.
262/// Why:
263/// The shadow holds the transaction's objects and refs.
264///
265/// In TS you'd write (pseudocode):
266/// ```ts
267/// await chmod(shadowPath, 0o700);
268/// ```
269#[cfg(unix)]
270fn set_private_directory_mode(path: &Path) -> std::io::Result<()> {
271 use std::os::unix::fs::PermissionsExt;
272 return std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700));
273}
274
275/// Windows has no mode bits for the shadow directory.
276// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
277#[cfg(windows)]
278fn set_private_directory_mode(_path: &Path) -> std::io::Result<()> {
279 return Ok(());
280}
281
282/// Shadow creation controls stay out of the release executable.
283#[cfg(test)]
284#[path = "shadow_create_tests.rs"]
285mod tests;