Skip to main content

git_policy_cli/
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 super::worktree_copy_compare::lstat_or_absent;
18use super::worktree_copy_error::{WorktreeCopyError, failure_with_cause, raw_failure};
19use super::worktree_copy_manifest::{EntryKind, WorktreeCopyEntry};
20use super::worktree_copy_selection::filesystem_path;
21use super::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    let components: Vec<&str> = entry.relative_path.split('/').collect();
115    let parent_count: usize = components.len().saturating_sub(1);
116    for depth in 1..=parent_count {
117        let current: String = components[..depth].join("/");
118        let destination_path: PathBuf = filesystem_path(destination_root, current.as_str())?;
119        // `match` on the existing entry: a directory is kept, anything else is a refusal.
120        match lstat_or_absent(destination_path.as_path())? {
121            Some(metadata) => {
122                if !metadata.is_dir() {
123                    let message: String = format!(
124                        "cli-git: ignored-state parent is not a directory: {} in {}.",
125                        quote(current.as_str()),
126                        quote(destination_root.to_string_lossy().as_ref())
127                    );
128                    return Err(failure_with_cause(message.as_str(), None));
129                }
130            }
131            None => {
132                std::fs::create_dir(destination_path.as_path())
133                    .map_err(|error| return raw_failure(&error))?;
134                created.push(capture_installed_path(
135                    destination_path.as_path(),
136                    current.as_str(),
137                    false,
138                )?);
139            }
140        }
141    }
142    return Ok(());
143}
144
145/// What:
146///  Whether a hard-link failure means the filesystem cannot link the staged file.
147/// Why:
148///   Only a refusal of the link itself falls back to a copy; a missing source or a full disk does not.
149///
150/// In TS you'd write (pseudocode):
151/// ```ts
152/// function isLinkUnsupported(error: unknown): boolean;
153/// ```
154fn is_link_unsupported(error: &std::io::Error) -> bool {
155    return error
156        .raw_os_error()
157        .is_some_and(|code| return LINK_UNSUPPORTED_CODES.contains(&code));
158}
159
160/// What:
161///  Installs one staged regular file: a hard link when the filesystem allows it, otherwise an exclusive copy with the entry's mode.
162/// Why:
163///   The link shares the staged bytes and mode in one step; the copy is the fallback the contract requires.
164///
165/// In TS you'd write (pseudocode):
166/// ```ts
167/// async function installFile({ stagePath, destinationPath, mode }): Promise<void>;
168/// ```
169fn install_file(
170    stage_path: &Path,
171    destination_path: &Path,
172    mode: u32,
173) -> Result<(), WorktreeCopyError> {
174    // `match` on the link result: success ends here, a refusal copies, and any other failure is reported.
175    match std::fs::hard_link(stage_path, destination_path) {
176        Ok(()) => return Ok(()),
177        Err(error) if is_link_unsupported(&error) => {}
178        Err(error) => return Err(raw_failure(&error)),
179    }
180    copy_file_exclusive(stage_path, destination_path)?;
181    let applied: std::io::Result<()> = std::fs::set_permissions(
182        destination_path,
183        std::os::unix::fs::PermissionsExt::from_mode(mode),
184    );
185    if let Err(error) = applied {
186        // A copy that cannot take its mode is removed, so the next attempt starts from the absent path.
187        let _ignored_removal: std::io::Result<()> = std::fs::remove_file(destination_path);
188        return Err(raw_failure(&error));
189    }
190    return Ok(());
191}
192
193/// What:
194///  Creates one absent selected entry from the private stage, as its kind requires.
195/// Why:
196///   Directories are created private and changed later; links are recreated with their exact target text.
197///
198/// In TS you'd write (pseudocode):
199/// ```ts
200/// async function createSelectedEntry({ snapshot, destinationRoot, entry }): Promise<void>;
201/// ```
202pub fn create_selected_entry(
203    stage_root: &Path,
204    destination_root: &Path,
205    entry: &WorktreeCopyEntry,
206) -> Result<(), WorktreeCopyError> {
207    let stage_path: PathBuf = filesystem_path(stage_root, entry.relative_path.as_str())?;
208    let destination_path: PathBuf =
209        filesystem_path(destination_root, entry.relative_path.as_str())?;
210    // `match` on the kind selects the one creation that applies to it.
211    match entry.kind {
212        EntryKind::Directory => {
213            return std::fs::DirBuilder::new()
214                .mode(PRIVATE_DIRECTORY_MODE)
215                .create(destination_path.as_path())
216                .map_err(|error| return raw_failure(&error));
217        }
218        EntryKind::File => {
219            return install_file(stage_path.as_path(), destination_path.as_path(), entry.mode);
220        }
221        EntryKind::Symlink => {
222            let target: PathBuf = std::fs::read_link(stage_path.as_path())
223                .map_err(|error| return raw_failure(&error))?;
224            return std::os::unix::fs::symlink(target, destination_path.as_path())
225                .map_err(|error| return raw_failure(&error));
226        }
227    }
228}
229
230/// Entry-creation controls stay out of the release executable.
231#[cfg(test)]
232#[path = "worktree_copy_install_entry_tests.rs"]
233mod tests;