git_policy_cli/shadow/shadow_git.rs
1//! What:
2//! Git commands run against a transaction's shadow repository,
3//! and its location and
4//! removal.
5//! Why:
6//! The shadow lives at `<git-common-dir>/cli-git/shadow/<transaction-id>`,
7//! derived from the
8//! transaction ID so recovery finds it before `prepared.json` exists.
9//! cli-git's own shadow
10//! commands remove every variable that would redirect Git elsewhere and point hooks at an
11//! absent directory,
12//! so no hook observes private shadow writes
13//! (`src/shadow-repository/shadow-refs.ts`,
14//! `runShadowGit`).
15//!
16//! In TS you'd write (pseudocode):
17//! ```ts
18//! await runShadowGit({ gitPath, shadowPath, args: ['rev-parse', 'HEAD'] });
19//! ```
20
21/// Debug diagnostics.
22use crate::diagnostic_log::debug;
23/// Tree removal.
24use crate::private_storage::remove_tree;
25/// The fail-closed recovery failure.
26use crate::recovery_error::{RecoveryError, io_failure};
27/// One Git start.
28use crate::transaction_git::GitRequest;
29/// `OsString` is owned operating-system text of raw bytes.
30use std::ffi::OsString;
31/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
32use std::path::{Path, PathBuf};
33
34/// Variables that would redirect a shadow command to another repository,
35/// index or store.
36pub const REPOSITORY_REDIRECT_VARIABLES: [&str; 5] = [
37 "GIT_DIR",
38 "GIT_WORK_TREE",
39 "GIT_COMMON_DIR",
40 "GIT_OBJECT_DIRECTORY",
41 "GIT_INDEX_FILE",
42];
43
44/// Name of the absent directory inside a shadow, named as the hooks path of cli-git's own shadow commands.
45pub const SHADOW_NO_HOOKS_NAME: &str = "no-hooks";
46
47/// Name of the directory under `<git-common-dir>/cli-git` that holds one shadow repository per transaction.
48pub const SHADOW_ROOT_NAME: &str = "shadow";
49
50/// What:
51/// The shadow repository path of a transaction.
52/// Why:
53/// `shadowRepositoryPath`:
54/// derived from the ID alone.
55///
56/// In TS you'd write (pseudocode):
57/// ```ts
58/// shadowRepositoryPath({ commonDir, transactionId }) // `${commonDir}/cli-git/shadow/${transactionId}`
59/// ```
60pub fn shadow_repository_path(common_dir: &Path, transaction_id: &str) -> PathBuf {
61 return common_dir
62 .join("cli-git")
63 .join(SHADOW_ROOT_NAME)
64 .join(transaction_id);
65}
66
67/// What:
68/// A request running `arguments` against the shadow:
69/// `--git-dir=<shadow>`,
70/// hooks pointed
71/// at an absent directory,
72/// redirecting variables removed,
73/// in `cwd`.
74/// Why:
75/// Every cli-git shadow command looks the same;
76/// the caller adds input or variables.
77///
78/// In TS you'd write (pseudocode):
79/// ```ts
80/// runShadowGit({ gitPath, shadowPath, args, cwd })
81/// ```
82pub fn shadow_request<S: AsRef<std::ffi::OsStr>>(
83 shadow: &Path,
84 cwd: &Path,
85 arguments: &[S],
86) -> GitRequest {
87 // `mut` allows prefixing the shadow selection.
88 let mut git_dir: OsString = OsString::from("--git-dir=");
89 git_dir.push(shadow.as_os_str());
90 let mut hooks: OsString = OsString::from("core.hooksPath=");
91 hooks.push(shadow.join(SHADOW_NO_HOOKS_NAME).as_os_str());
92 let mut full: Vec<OsString> = vec![git_dir, OsString::from("-c"), hooks];
93 for argument in arguments {
94 full.push(argument.as_ref().to_os_string());
95 }
96 let mut request: GitRequest = GitRequest::new(cwd, full.as_slice());
97 request.without_prefix = true;
98 for name in REPOSITORY_REDIRECT_VARIABLES {
99 request.unset.push(OsString::from(name));
100 }
101 return request;
102}
103
104/// What:
105/// Remove a shadow repository,
106/// treating an absent one as removed.
107/// Why:
108/// `removeShadowRepository`:
109/// a shadow never outlives its transaction directory.
110///
111/// In TS you'd write (pseudocode):
112/// ```ts
113/// await rm(shadowPath, { recursive: true, force: true });
114/// ```
115pub fn remove_shadow_repository(shadow: &Path) -> Result<(), RecoveryError> {
116 if let Err(error) = remove_tree(shadow) {
117 return Err(io_failure("removing the shadow repository", shadow, &error));
118 }
119 debug(
120 "removeShadowRepository",
121 format!("removed shadow repository {}", shadow.display()).as_str(),
122 );
123 return Ok(());
124}
125
126/// Shadow request controls stay out of the release executable.
127#[cfg(test)]
128#[path = "shadow_git_tests.rs"]
129mod tests;