Skip to main content

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;