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    #[cfg(windows)]
93    {
94        if directory {
95            return std::os::windows::fs::symlink_dir(source, destination);
96        }
97        return std::fs::copy(source, destination).map(|_| ());
98    }
99}
100
101/// What:
102///  Links every shared entry of one real directory into one shadow directory.
103/// Why:
104///   Entries are typed without following links,
105///  so a link in the real directory is shared as a link.
106///
107/// In TS you'd write (pseudocode):
108/// ```ts
109/// await linkDirectoryEntries({ sourceDirectory, shadowDirectory, skip });
110/// ```
111fn link_directory_entries(
112    source_directory: &Path,
113    shadow_directory: &Path,
114    skip: impl Fn(&str) -> bool,
115) -> std::io::Result<()> {
116    for entry_result in std::fs::read_dir(source_directory)? {
117        let entry: std::fs::DirEntry = entry_result?;
118        let name: String = entry.file_name().to_string_lossy().into_owned();
119        if skip(name.as_str()) {
120            continue;
121        }
122        let destination: PathBuf = shadow_directory.join(&name);
123        // `git init` may have created a template entry of the same name; the real one replaces it.
124        remove_entry(destination.as_path())?;
125        share_entry(
126            entry.path().as_path(),
127            destination.as_path(),
128            entry.file_type()?.is_dir(),
129        )?;
130    }
131    return Ok(());
132}
133
134/// What:
135///  Reports whether a regular per-worktree file exists.
136/// Why:
137///   A missing file is an answer, every other failure is an error.
138///
139/// In TS you'd write (pseudocode):
140/// ```ts
141/// await regularFileExists(path);
142/// ```
143fn regular_file_exists(path: &Path) -> std::io::Result<bool> {
144    return match std::fs::symlink_metadata(path) {
145        Ok(metadata) => Ok(metadata.is_file()),
146        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
147        Err(error) => Err(error),
148    };
149}
150
151/// What:
152///  Links the real common directory into the shadow and copies the per-worktree entries.
153/// Why:
154///   A linked worktree's `info/sparse-checkout` lives outside the common directory,
155///  so its `info` directory is private and only its shared entries are linked.
156///
157/// In TS you'd write (pseudocode):
158/// ```ts
159/// await linkShadowEntries({ shadowPath, commonDir, gitDir });
160/// ```
161pub fn link_shadow_entries(shadow: &Path, common_dir: &Path, git_dir: &Path) -> std::io::Result<()> {
162    let per_worktree_sparse: PathBuf = git_dir.join("info").join("sparse-checkout");
163    let mixed_info: bool =
164        git_dir != common_dir && regular_file_exists(per_worktree_sparse.as_path())?;
165    link_directory_entries(common_dir, shadow, |name| {
166        return is_private_shadow_entry(name) || (mixed_info && name == "info");
167    })?;
168    let worktree_config: PathBuf = git_dir.join("config.worktree");
169    if regular_file_exists(worktree_config.as_path())? {
170        std::fs::copy(&worktree_config, shadow.join("config.worktree"))?;
171    }
172    if !mixed_info {
173        return Ok(());
174    }
175    let shadow_info: PathBuf = shadow.join("info");
176    remove_entry(shadow_info.as_path())?;
177    std::fs::create_dir(&shadow_info)?;
178    set_private_directory(shadow_info.as_path())?;
179    let shared_info: PathBuf = common_dir.join("info");
180    match link_directory_entries(shared_info.as_path(), shadow_info.as_path(), |name| {
181        return name == "sparse-checkout";
182    }) {
183        Ok(()) => {}
184        Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
185        Err(error) => return Err(error),
186    }
187    std::fs::copy(&per_worktree_sparse, shadow_info.join("sparse-checkout"))?;
188    return Ok(());
189}
190
191/// What:
192///  Gives a directory the private mode of the transaction's directories.
193/// Why:
194///   The shadow's private `info` holds a copy of per-worktree state.
195///
196/// In TS you'd write (pseudocode):
197/// ```ts
198/// await mkdir(shadowInfo, { mode: 0o700 });
199/// ```
200#[cfg(unix)]
201fn set_private_directory(path: &Path) -> std::io::Result<()> {
202    use std::os::unix::fs::PermissionsExt;
203    return std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700));
204}
205
206/// Windows has no mode bits for this directory; the incumbent's ACL work is not ported.
207#[cfg(windows)]
208fn set_private_directory(_path: &Path) -> std::io::Result<()> {
209    return Ok(());
210}
211
212/// Shadow link controls stay out of the release executable.
213#[cfg(test)]
214#[path = "shadow_links_tests.rs"]
215mod tests;