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