Skip to main content

git_policy_cli/worktree_copying/
worktree_copy_install_entry.rs

1//! What:
2//!  The exclusive creation of one destination entry from the private stage,
3//!  the identity captured right after it exists,
4//!  and the creation of the unselected parent directories an entry needs.
5//! Why:
6//!  A regular file is installed as a hard link to its staged copy,
7//!  so it appears with its final bytes and mode in one step;
8//!  the identity captured after creation is the only proof later rollback accepts.
9//!
10//! In TS you'd write (pseudocode):
11//! ```ts
12//! await ensureParents({ destinationRoot, entry, created });
13//! await createSelectedEntry({ snapshot, destinationRoot, entry });
14//! created.push(await captureInstalledPath({ destinationPath, relativePath, selected: true }));
15//! ```
16
17use crate::worktree_copy_compare::lstat_or_absent;
18use crate::worktree_copy_error::{WorktreeCopyError, failure_with_cause, raw_failure};
19use crate::worktree_copy_manifest::{EntryKind, WorktreeCopyEntry};
20use crate::worktree_copy_selection::filesystem_path;
21use crate::worktree_copy_stage::copy_file_exclusive;
22use crate::json_record::quote;
23use std::os::unix::fs::{DirBuilderExt, MetadataExt};
24use std::path::{Path, PathBuf};
25
26/// What:
27///  The permission bits of a directory cli-git creates for a selected entry, before modes are applied.
28/// Why:
29///   Restrictive modes can block the children installation still has to create, so modes come last.
30///
31/// In TS you'd write (pseudocode):
32/// ```ts
33/// const PRIVATE_DIRECTORY_MODE = 0o700;
34/// ```
35const PRIVATE_DIRECTORY_MODE: u32 = 0o700;
36
37/// What:
38///  The operating-system errors that mean the filesystem cannot hard-link the staged file.
39/// Why:
40///   Only these refusals fall back to a copy; any other failure is reported as it is.
41///
42/// In TS you'd write (pseudocode):
43/// ```ts
44/// const LINK_UNSUPPORTED_CODES = new Set(['EMLINK', 'ENOSYS', 'ENOTSUP', 'EOPNOTSUPP', 'EPERM', 'EXDEV']);
45/// ```
46const LINK_UNSUPPORTED_CODES: [i32; 6] = [
47    libc::EMLINK,
48    libc::ENOSYS,
49    libc::ENOTSUP,
50    libc::EOPNOTSUPP,
51    libc::EPERM,
52    libc::EXDEV,
53];
54
55/// What:
56///  One path cli-git created, with the identity the filesystem gave it.
57/// Why:
58///   Rollback removes a path only when its device and inode still match this record.
59///
60/// In TS you'd write (pseudocode):
61/// ```ts
62/// type InstalledWorktreePath = { device: string; inode: string; relativePath: string; selected: boolean };
63/// ```
64#[derive(Clone, Debug, Eq, PartialEq)]
65pub struct InstalledPath {
66    /// Device number reported by the filesystem after creation.
67    pub device: u64,
68    /// Inode number reported by the filesystem after creation.
69    pub inode: u64,
70    /// Slash-separated repository path.
71    pub relative_path: String,
72    /// Whether the path came from the selected source manifest rather than being parent scaffolding.
73    pub selected: bool,
74}
75
76/// What:
77///  The no-follow identity of a path, read right after its creation.
78/// Why:
79///   The identity names the exact object cli-git made, not whatever later sits at the same path.
80///
81/// In TS you'd write (pseudocode):
82/// ```ts
83/// async function captureInstalledPath({ destinationPath, relativePath, selected }): Promise<InstalledWorktreePath>;
84/// ```
85pub fn capture_installed_path(
86    destination_path: &Path,
87    relative_path: &str,
88    selected: bool,
89) -> Result<InstalledPath, WorktreeCopyError> {
90    let metadata: std::fs::Metadata =
91        std::fs::symlink_metadata(destination_path).map_err(|error| return raw_failure(&error))?;
92    return Ok(InstalledPath {
93        device: metadata.dev(),
94        inode: metadata.ino(),
95        relative_path: String::from(relative_path),
96        selected,
97    });
98}
99
100/// What:
101///  Creates each missing unselected parent directory of one entry, shallow first, recording what it created.
102/// Why:
103///   An entry's parents are scaffolding that rollback must be able to remove again, so each one is recorded.
104///
105/// In TS you'd write (pseudocode):
106/// ```ts
107/// async function ensureParents({ destinationRoot, entry, created }): Promise<void>;
108/// ```
109pub fn ensure_parents(
110    destination_root: &Path,
111    entry: &WorktreeCopyEntry,
112    created: &mut Vec<InstalledPath>,
113) -> Result<(), WorktreeCopyError> {
114    return ensure_parents_remembering(destination_root, entry, created, &mut std::collections::HashSet::new());
115}
116
117/// What:
118///  Creates each missing unselected parent directory of one entry, skipping every parent an earlier call already proved.
119/// Why:
120///   Entries arrive parent first, so almost every entry's parent was just proved or created; asking the filesystem about
121///  each ancestor of each of hundreds of thousands of entries costs more than the clones themselves.
122///
123/// In TS you'd write (pseudocode):
124/// ```ts
125/// async function ensureParentsRemembering({ destinationRoot, entry, created, known }): Promise<void>;
126/// ```
127pub fn ensure_parents_remembering(
128    destination_root: &Path,
129    entry: &WorktreeCopyEntry,
130    created: &mut Vec<InstalledPath>,
131    known: &mut std::collections::HashSet<String>,
132) -> Result<(), WorktreeCopyError> {
133    let Some((parent, _name)) = entry.relative_path.rsplit_once('/') else {
134        return Ok(());
135    };
136    if known.contains(parent) {
137        return Ok(());
138    }
139    let components: Vec<&str> = entry.relative_path.split('/').collect();
140    let parent_count: usize = components.len().saturating_sub(1);
141    for depth in 1..=parent_count {
142        let current: String = components[..depth].join("/");
143        let destination_path: PathBuf = filesystem_path(destination_root, current.as_str())?;
144        // `match` on the existing entry: a directory is kept, anything else is a refusal.
145        match lstat_or_absent(destination_path.as_path())? {
146            Some(metadata) => {
147                if !metadata.is_dir() {
148                    let message: String = format!(
149                        "cli-git: ignored-state parent is not a directory: {} in {}.",
150                        quote(current.as_str()),
151                        quote(destination_root.to_string_lossy().as_ref())
152                    );
153                    return Err(failure_with_cause(message.as_str(), None));
154                }
155            }
156            None => {
157                std::fs::create_dir(destination_path.as_path())
158                    .map_err(|error| return raw_failure(&error))?;
159                created.push(capture_installed_path(
160                    destination_path.as_path(),
161                    current.as_str(),
162                    false,
163                )?);
164            }
165        }
166    }
167    for depth in 1..=parent_count {
168        known.insert(components[..depth].join("/"));
169    }
170    return Ok(());
171}
172
173/// What:
174///  Whether a hard-link failure means the filesystem cannot link the staged file.
175/// Why:
176///   Only a refusal of the link itself falls back to a copy; a missing source or a full disk does not.
177///
178/// In TS you'd write (pseudocode):
179/// ```ts
180/// function isLinkUnsupported(error: unknown): boolean;
181/// ```
182fn is_link_unsupported(error: &std::io::Error) -> bool {
183    return error
184        .raw_os_error()
185        .is_some_and(|code| return LINK_UNSUPPORTED_CODES.contains(&code));
186}
187
188/// What:
189///  Installs one staged regular file: a hard link when the filesystem allows it, otherwise an exclusive copy with the entry's mode.
190/// Why:
191///   The link shares the staged bytes and mode in one step; the copy is the fallback the contract requires.
192///
193/// In TS you'd write (pseudocode):
194/// ```ts
195/// async function installFile({ stagePath, destinationPath, mode }): Promise<void>;
196/// ```
197fn install_file(
198    stage_path: &Path,
199    destination_path: &Path,
200    mode: u32,
201) -> Result<(), WorktreeCopyError> {
202    // `match` on the link result: success ends here, a refusal copies, and any other failure is reported.
203    match std::fs::hard_link(stage_path, destination_path) {
204        Ok(()) => return Ok(()),
205        Err(error) if is_link_unsupported(&error) => {}
206        Err(error) => return Err(raw_failure(&error)),
207    }
208    copy_file_exclusive(stage_path, destination_path)?;
209    let applied: std::io::Result<()> = std::fs::set_permissions(
210        destination_path,
211        std::os::unix::fs::PermissionsExt::from_mode(mode),
212    );
213    if let Err(error) = applied {
214        // A copy that cannot take its mode is removed, so the next attempt starts from the absent path.
215        let _ignored_removal: std::io::Result<()> = std::fs::remove_file(destination_path);
216        return Err(raw_failure(&error));
217    }
218    return Ok(());
219}
220
221/// What:
222///  Creates one absent selected entry from the private stage, as its kind requires.
223/// Why:
224///   Directories are created private and changed later; links are recreated with their exact target text.
225///
226/// In TS you'd write (pseudocode):
227/// ```ts
228/// async function createSelectedEntry({ snapshot, destinationRoot, entry }): Promise<void>;
229/// ```
230pub fn create_selected_entry(
231    stage_root: &Path,
232    destination_root: &Path,
233    entry: &WorktreeCopyEntry,
234) -> Result<(), WorktreeCopyError> {
235    let stage_path: PathBuf = filesystem_path(stage_root, entry.relative_path.as_str())?;
236    let destination_path: PathBuf =
237        filesystem_path(destination_root, entry.relative_path.as_str())?;
238    // `match` on the kind selects the one creation that applies to it.
239    match entry.kind {
240        EntryKind::Directory => {
241            return std::fs::DirBuilder::new()
242                .mode(PRIVATE_DIRECTORY_MODE)
243                .create(destination_path.as_path())
244                .map_err(|error| return raw_failure(&error));
245        }
246        EntryKind::File => {
247            return install_file(stage_path.as_path(), destination_path.as_path(), entry.mode);
248        }
249        EntryKind::Symlink => {
250            let target: PathBuf = std::fs::read_link(stage_path.as_path())
251                .map_err(|error| return raw_failure(&error))?;
252            return std::os::unix::fs::symlink(target, destination_path.as_path())
253                .map_err(|error| return raw_failure(&error));
254        }
255    }
256}
257
258/// Entry-creation controls stay out of the release executable.
259#[cfg(test)]
260#[path = "worktree_copy_install_entry_tests.rs"]
261mod tests;