Skip to main content

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