git_policy_cli/
worktree_copy_install.rs1use super::worktree_copy_compare::{entry_matches, lstat_or_absent};
16use super::worktree_copy_error::{WorktreeCopyError, failure_with_cause};
17use super::worktree_copy_install_entry::{
18 InstalledPath, capture_installed_path, create_selected_entry, ensure_parents,
19};
20use super::worktree_copy_manifest::{EntryKind, WorktreeCopyEntry, apply_entry_modes};
21use super::worktree_copy_rollback::rollback_created;
22use super::worktree_copy_selection::filesystem_path;
23use crate::json_record::quote;
24use std::collections::BTreeSet;
25use std::path::{Path, PathBuf};
26
27pub const INSTALL_BATCH_ENTRIES: usize = 512;
37
38pub trait InstallRecorder {
49 fn record_intents(&mut self, relative_paths: &[String]) -> Result<(), WorktreeCopyError>;
51
52 fn record_creations(&mut self, entries: &[InstalledPath]) -> Result<(), WorktreeCopyError>;
54}
55
56#[derive(Debug, Default)]
66pub struct NoRecord;
67
68impl InstallRecorder for NoRecord {
69 fn record_intents(&mut self, _relative_paths: &[String]) -> Result<(), WorktreeCopyError> {
71 return Ok(());
72 }
73
74 fn record_creations(&mut self, _entries: &[InstalledPath]) -> Result<(), WorktreeCopyError> {
76 return Ok(());
77 }
78}
79
80pub fn preflight_destination(
90 stage_root: &Path,
91 destination_root: &Path,
92 entries: &[WorktreeCopyEntry],
93 claimed: &BTreeSet<String>,
94) -> Result<BTreeSet<String>, WorktreeCopyError> {
95 let mut absent: BTreeSet<String> = BTreeSet::new();
96 for entry in entries {
97 let destination_path: PathBuf =
98 filesystem_path(destination_root, entry.relative_path.as_str())?;
99 match lstat_or_absent(destination_path.as_path())? {
101 None => {
102 absent.insert(entry.relative_path.clone());
103 }
104 Some(metadata) => {
105 let claimed_directory: bool = entry.kind == EntryKind::Directory
106 && metadata.is_dir()
107 && claimed.contains(&entry.relative_path);
108 if claimed_directory {
109 continue;
110 }
111 if !entry_matches(stage_root, destination_root, entry)? {
112 let message: String = format!(
113 "cli-git: ignored-state copy would overwrite differing destination entry {} in {}.",
114 quote(entry.relative_path.as_str()),
115 quote(destination_root.to_string_lossy().as_ref())
116 );
117 return Err(failure_with_cause(message.as_str(), None));
118 }
119 }
120 }
121 }
122 return Ok(absent);
123}
124
125fn install_batch(
135 stage_root: &Path,
136 destination_root: &Path,
137 batch: &[WorktreeCopyEntry],
138 absent: &BTreeSet<String>,
139 created: &mut Vec<InstalledPath>,
140 recorder: &mut dyn InstallRecorder,
141) -> Result<(), WorktreeCopyError> {
142 let claims: Vec<String> = batch
143 .iter()
144 .map(|entry| return entry.relative_path.clone())
145 .filter(|relative_path| return absent.contains(relative_path))
146 .collect();
147 recorder.record_intents(claims.as_slice())?;
148 let batch_start: usize = created.len();
149 for entry in batch {
150 ensure_parents(destination_root, entry, created)?;
151 let destination_path: PathBuf =
152 filesystem_path(destination_root, entry.relative_path.as_str())?;
153 if lstat_or_absent(destination_path.as_path())?.is_some() {
155 continue;
156 }
157 create_selected_entry(stage_root, destination_root, entry)?;
158 created.push(capture_installed_path(
159 destination_path.as_path(),
160 entry.relative_path.as_str(),
161 true,
162 )?);
163 }
164 recorder.record_creations(&created[batch_start..])?;
165 return Ok(());
166}
167
168pub fn install_snapshot(
178 stage_root: &Path,
179 destination_root: &Path,
180 entries: &[WorktreeCopyEntry],
181 claimed: &BTreeSet<String>,
182 prior: &[InstalledPath],
183 recorder: &mut dyn InstallRecorder,
184) -> Result<usize, WorktreeCopyError> {
185 let mut created: Vec<InstalledPath> = prior.to_vec();
186 match install_all(
187 stage_root,
188 destination_root,
189 entries,
190 claimed,
191 &mut created,
192 recorder,
193 ) {
194 Ok(copied) => return Ok(copied),
195 Err(error) => {
196 let retained: Vec<String> =
197 rollback_created(stage_root, destination_root, entries, created.as_slice())?;
198 let suffix: String = if retained.is_empty() {
199 String::new()
200 } else {
201 let names: Vec<String> = retained
202 .iter()
203 .map(|path| return quote(path.as_str()))
204 .collect();
205 format!(" Rollback retained: {}.", names.join(", "))
206 };
207 let message: String = format!(
208 "cli-git: ignored-state installation failed for {}.{suffix}",
209 quote(destination_root.to_string_lossy().as_ref())
210 );
211 return Err(failure_with_cause(message.as_str(), Some(&error.0)));
212 }
213 }
214}
215
216fn install_all(
226 stage_root: &Path,
227 destination_root: &Path,
228 entries: &[WorktreeCopyEntry],
229 claimed: &BTreeSet<String>,
230 created: &mut Vec<InstalledPath>,
231 recorder: &mut dyn InstallRecorder,
232) -> Result<usize, WorktreeCopyError> {
233 let absent: BTreeSet<String> =
234 preflight_destination(stage_root, destination_root, entries, claimed)?;
235 for batch in entries.chunks(INSTALL_BATCH_ENTRIES) {
236 install_batch(
237 stage_root,
238 destination_root,
239 batch,
240 &absent,
241 created,
242 recorder,
243 )?;
244 }
245 apply_entry_modes(destination_root, entries)?;
246 let selected: usize = created
247 .iter()
248 .filter(|installed| return installed.selected)
249 .count();
250 return Ok(selected);
251}
252
253#[cfg(test)]
255#[path = "worktree_copy_install_tests.rs"]
256mod tests;