Skip to main content

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;