Skip to main content

git_policy_cli/
shadow_links.rs

1//! What:
2//!  Links every non-private entry of the real common Git directory into a shadow repository,
3//!       and copies the per-worktree entries (`config.worktree`, a linked worktree's sparse checkout).
4//! Why:
5//!  Shared entries such as `hooks`, `info`, `rr-cache`, `lfs` and `modules` must be the real ones,
6//!  so hooks and native Git see them;
7//!  transaction-private entries (refs, index, objects, conclusion state, lock files) stay the shadow's own
8//!  (`src/shadow-repository/shadow-links.ts`).
9//!  On Windows the incumbent uses junctions and copies;
10//!  this port uses directory symbolic links and copies, because the standard library has no junction API.
11//!
12//! In TS you'd write (pseudocode):
13//! ```ts
14//! await linkShadowEntries({ shadowPath, commonDir, gitDir });
15//! ```
16
17/// The conclusion state names a shadow keeps privately.
18use super::conclusion_cleanup::{CONCLUSION_STATE_FILES, SEQUENCER_DIRECTORY};
19/// `Path`/`PathBuf` are borrowed/owned filesystem paths.
20use std::path::{Path, PathBuf};
21
22/// Real common-directory entries the shadow never links:
23///  its ref store, index, config, object store, message file, copied conclusion state,
24///  and cli-git's own state, which contains the shadow itself.
25pub const PRIVATE_SHADOW_ENTRIES: [&str; 13] = [
26    "HEAD",
27    "refs",
28    "packed-refs",
29    "reftable",
30    "logs",
31    "index",
32    "config",
33    "config.worktree",
34    "objects",
35    "COMMIT_EDITMSG",
36    "cli-git",
37    "cli-git-transactions",
38    "cli-git-transaction",
39];
40
41/// What:
42///  Reports whether an entry must stay private to the shadow.
43/// Why:
44///   A shadow Git that locks the same name as the real owner would collide with it,
45///  so lock files are never linked.
46///
47/// In TS you'd write (pseudocode):
48/// ```ts
49/// isPrivateShadowEntry(name)
50/// ```
51pub fn is_private_shadow_entry(name: &str) -> bool {
52    return PRIVATE_SHADOW_ENTRIES.contains(&name)
53        || CONCLUSION_STATE_FILES.contains(&name)
54        || name == SEQUENCER_DIRECTORY
55        || name.ends_with(".lock");
56}
57
58/// What:
59///  Removes whatever sits at a shadow entry name, without following a link.
60/// Why:
61///   A link in the shadow is removed itself, never its target in the real repository.
62///
63/// In TS you'd write (pseudocode):
64/// ```ts
65/// await rm(destination, { recursive: true, force: true });
66/// ```
67fn remove_entry(path: &Path) -> std::io::Result<()> {
68    return match std::fs::symlink_metadata(path) {
69        Ok(metadata) if metadata.is_dir() => std::fs::remove_dir_all(path),
70        Ok(_) => std::fs::remove_file(path),
71        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
72        Err(error) => Err(error),
73    };
74}
75
76/// What:
77///  Shares one real entry with the shadow.
78/// Why:
79///   Unix links every entry;
80///  Windows links directories and copies files, since a file link needs a privilege.
81///
82/// In TS you'd write (pseudocode):
83/// ```ts
84/// await symlink(source, destination);
85/// ```
86fn share_entry(source: &Path, destination: &Path, directory: bool) -> std::io::Result<()> {
87    #[cfg(unix)]
88    {
89        let _ = directory;
90        return std::os::unix::fs::symlink(source, destination);
91    }
92    // UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
93    #[cfg(windows)]
94    {
95        if directory {
96            return std::os::windows::fs::symlink_dir(source, destination);
97        }
98        return std::fs::copy(source, destination).map(|_| ());
99    }
100}
101
102/// What:
103///  Links every shared entry of one real directory into one shadow directory.
104/// Why:
105///   Entries are typed without following links,
106///  so a link in the real directory is shared as a link.
107///
108/// In TS you'd write (pseudocode):
109/// ```ts
110/// await linkDirectoryEntries({ sourceDirectory, shadowDirectory, skip });
111/// ```
112fn link_directory_entries(
113    source_directory: &Path,
114    shadow_directory: &Path,
115    skip: impl Fn(&str) -> bool,
116) -> std::io::Result<()> {
117    for entry_result in std::fs::read_dir(source_directory)? {
118        let entry: std::fs::DirEntry = entry_result?;
119        let name: String = entry.file_name().to_string_lossy().into_owned();
120        if skip(name.as_str()) {
121            continue;
122        }
123        let destination: PathBuf = shadow_directory.join(&name);
124        // `git init` may have created a template entry of the same name; the real one replaces it.
125        remove_entry(destination.as_path())?;
126        share_entry(
127            entry.path().as_path(),
128            destination.as_path(),
129            entry.file_type()?.is_dir(),
130        )?;
131    }
132    return Ok(());
133}
134
135/// What:
136///  Reports whether a regular per-worktree file exists.
137/// Why:
138///   A missing file is an answer, every other failure is an error.
139///
140/// In TS you'd write (pseudocode):
141/// ```ts
142/// await regularFileExists(path);
143/// ```
144fn regular_file_exists(path: &Path) -> std::io::Result<bool> {
145    return match std::fs::symlink_metadata(path) {
146        Ok(metadata) => Ok(metadata.is_file()),
147        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
148        Err(error) => Err(error),
149    };
150}
151
152/// What:
153///  Links the real common directory into the shadow and copies the per-worktree entries.
154/// Why:
155///   A linked worktree's `info/sparse-checkout` lives outside the common directory,
156///  so its `info` directory is private and only its shared entries are linked.
157///
158/// In TS you'd write (pseudocode):
159/// ```ts
160/// await linkShadowEntries({ shadowPath, commonDir, gitDir });
161/// ```
162pub fn link_shadow_entries(shadow: &Path, common_dir: &Path, git_dir: &Path) -> std::io::Result<()> {
163    let per_worktree_sparse: PathBuf = git_dir.join("info").join("sparse-checkout");
164    let mixed_info: bool =
165        git_dir != common_dir && regular_file_exists(per_worktree_sparse.as_path())?;
166    link_directory_entries(common_dir, shadow, |name| {
167        return is_private_shadow_entry(name) || (mixed_info && name == "info");
168    })?;
169    let worktree_config: PathBuf = git_dir.join("config.worktree");
170    if regular_file_exists(worktree_config.as_path())? {
171        std::fs::copy(&worktree_config, shadow.join("config.worktree"))?;
172    }
173    if !mixed_info {
174        return Ok(());
175    }
176    let shadow_info: PathBuf = shadow.join("info");
177    remove_entry(shadow_info.as_path())?;
178    std::fs::create_dir(&shadow_info)?;
179    set_private_directory(shadow_info.as_path())?;
180    let shared_info: PathBuf = common_dir.join("info");
181    match link_directory_entries(shared_info.as_path(), shadow_info.as_path(), |name| {
182        return name == "sparse-checkout";
183    }) {
184        Ok(()) => {}
185        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
186        Err(error) => return Err(error),
187    }
188    std::fs::copy(&per_worktree_sparse, shadow_info.join("sparse-checkout"))?;
189    return Ok(());
190}
191
192/// What:
193///  Gives a directory the private mode of the transaction's directories.
194/// Why:
195///   The shadow's private `info` holds a copy of per-worktree state.
196///
197/// In TS you'd write (pseudocode):
198/// ```ts
199/// await mkdir(shadowInfo, { mode: 0o700 });
200/// ```
201#[cfg(unix)]
202fn set_private_directory(path: &Path) -> std::io::Result<()> {
203    use std::os::unix::fs::PermissionsExt;
204    return std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700));
205}
206
207/// Windows has no mode bits for this directory; the incumbent's ACL work is not ported.
208// UNTESTED ON WINDOWS: no gate compiles or runs this branch (the crate does not build for Windows; README, Requirements).
209#[cfg(windows)]
210fn set_private_directory(_path: &Path) -> std::io::Result<()> {
211    return Ok(());
212}
213
214/// Shadow link controls stay out of the release executable.
215#[cfg(test)]
216#[path = "shadow_links_tests.rs"]
217mod tests;