git_policy_cli/worktree_copying/
worktree_copy_transaction.rs1use crate::diagnostic_log::debug;
15use crate::transaction_git::GitContext;
16use crate::worktree_copy_error::{WorktreeCopyError, raw_failure};
17use crate::worktree_copy_install::{InstallRecorder, install_snapshot};
18use crate::worktree_copy_install_entry::InstalledPath;
19use crate::worktree_copy_install_log::{InstallLog, open_install_log};
20use crate::worktree_copy_journal::{
21 Journal, PendingJournal, Phase, create_journal, remove_pending_journal, write_journal,
22};
23use crate::worktree_copy_manifest::{WorktreeCopyEntry, collect_entry_manifest};
24use crate::worktree_copy_selection::CopySelection;
25use crate::worktree_copy_stage::{StagedSnapshot, stage_ignored_snapshot};
26use crate::worktree_copy_stage_claim::{StageClaim, claim_stage};
27use crate::json_record::quote;
28use std::collections::BTreeSet;
29use std::path::{Path, PathBuf};
30
31pub struct TransactionState {
41 pub pending: PendingJournal,
43 pub log: InstallLog,
45 pub intended: BTreeSet<String>,
47 pub created_paths: BTreeSet<String>,
49}
50
51impl InstallRecorder for TransactionState {
52 fn record_intents(&mut self, relative_paths: &[String]) -> Result<(), WorktreeCopyError> {
54 let unclaimed: Vec<String> = relative_paths
55 .iter()
56 .filter(|relative_path| return !self.intended.contains(relative_path.as_str()))
57 .cloned()
58 .collect();
59 self.log.append_intents(unclaimed.as_slice())?;
60 self.intended.extend(unclaimed);
61 return Ok(());
62 }
63
64 fn record_creations(&mut self, entries: &[InstalledPath]) -> Result<(), WorktreeCopyError> {
66 let unrecorded: Vec<InstalledPath> = entries
67 .iter()
68 .filter(|entry| return !self.created_paths.contains(entry.relative_path.as_str()))
69 .cloned()
70 .collect();
71 self.log.append_creations(unrecorded.as_slice())?;
72 self.created_paths.extend(
73 unrecorded
74 .into_iter()
75 .map(|entry| return entry.relative_path),
76 );
77 return Ok(());
78 }
79}
80
81pub fn begin_installing(state: &mut TransactionState) -> Result<(), WorktreeCopyError> {
91 if state.pending.journal.phase == Phase::Installing {
92 return Ok(());
93 }
94 let installing: Journal = Journal {
95 phase: Phase::Installing,
96 ..state.pending.journal.clone()
97 };
98 write_journal(state.pending.path.as_path(), &installing)?;
99 state.pending.journal = installing;
100 return Ok(());
101}
102
103pub fn snapshot_from_journal(
113 pending: &PendingJournal,
114 intended: &BTreeSet<String>,
115) -> Result<StagedSnapshot, WorktreeCopyError> {
116 let stage_root: PathBuf = PathBuf::from(pending.journal.stage_root.as_str());
117 let outcome: Result<Vec<WorktreeCopyEntry>, WorktreeCopyError> = (|| {
118 let entries: Vec<WorktreeCopyEntry> = collect_entry_manifest(
119 stage_root.as_path(),
120 pending.journal.selected_roots.as_slice(),
121 &[],
122 )?;
123 let present: BTreeSet<&str> = entries
124 .iter()
125 .map(|entry| return entry.relative_path.as_str())
126 .collect();
127 for claim in intended {
128 if !present.contains(claim.as_str()) {
129 let message: String = format!(
130 "cli-git: worktree-copy journal intent is absent from private stage {}.",
131 quote(pending.journal.stage_root.as_str())
132 );
133 return Err(crate::worktree_copy_error::failure_with_cause(
134 message.as_str(),
135 None,
136 ));
137 }
138 }
139 return Ok(entries);
140 })();
141 let entries: Vec<WorktreeCopyEntry> = match outcome {
143 Ok(entries) => entries,
144 Err(error) => {
145 let message: String = format!(
146 "cli-git: could not recover staged ignored state at {}.",
147 quote(pending.journal.stage_root.as_str())
148 );
149 return Err(crate::worktree_copy_error::failure_with_cause(
150 message.as_str(),
151 Some(&error.0),
152 ));
153 }
154 };
155 return Ok(StagedSnapshot {
156 entries,
157 selected_roots: pending.journal.selected_roots.clone(),
158 source_root: PathBuf::from(pending.journal.source_root.as_str()),
159 stage_container: PathBuf::from(pending.journal.stage_container.as_str()),
160 stage_root,
161 unreadable: Vec::new(),
162 });
163}
164
165#[derive(Debug)]
175pub enum Outcome {
176 Installed(usize),
178 Ended(WorktreeCopyError),
180}
181
182fn install_recorded(
192 pending: PendingJournal,
193 snapshot: &dyn Fn(&BTreeSet<String>) -> Result<StagedSnapshot, WorktreeCopyError>,
194) -> Result<(PendingJournal, Outcome), WorktreeCopyError> {
195 let stage: PathBuf = PathBuf::from(pending.journal.stage_container.as_str());
196 let opened = open_install_log(stage.as_path())?;
197 let mut intended: BTreeSet<String> = pending.journal.intended_entries.iter().cloned().collect();
198 intended.extend(opened.recorded.intended.iter().cloned());
199 let mut created_paths: BTreeSet<String> = pending
200 .journal
201 .created_entries
202 .iter()
203 .map(|entry| return entry.relative_path.clone())
204 .collect();
205 created_paths.extend(
206 opened
207 .recorded
208 .created
209 .iter()
210 .map(|entry| return entry.relative_path.clone()),
211 );
212 let mut prior: Vec<InstalledPath> = pending.journal.created_entries.clone();
213 prior.extend(opened.recorded.created.iter().cloned());
214 let staged: StagedSnapshot = snapshot(&intended)?;
215 let mut state: TransactionState = TransactionState {
216 pending,
217 log: opened.log,
218 intended,
219 created_paths,
220 };
221 begin_installing(&mut state)?;
222 let claimed: BTreeSet<String> = state
223 .intended
224 .union(&state.created_paths)
225 .cloned()
226 .collect();
227 let destination: PathBuf = PathBuf::from(state.pending.journal.destination_root.as_str());
228 let result: Result<usize, WorktreeCopyError> = install_snapshot(
229 staged.stage_root.as_path(),
230 destination.as_path(),
231 staged.entries.as_slice(),
232 &claimed,
233 prior.as_slice(),
234 &mut state,
235 );
236 let TransactionState {
237 pending: latest, ..
238 } = state;
239 match result {
241 Ok(copied) => return Ok((latest, Outcome::Installed(copied))),
242 Err(error) => return Ok((latest, Outcome::Ended(error))),
243 }
244}
245
246pub fn complete_journal(
256 pending: PendingJournal,
257 snapshot: &dyn Fn(&BTreeSet<String>) -> Result<StagedSnapshot, WorktreeCopyError>,
258) -> Result<Outcome, WorktreeCopyError> {
259 let started: std::time::Instant = std::time::Instant::now();
260 let (latest, outcome) = install_recorded(pending, snapshot)?;
261 let recorded: std::time::Duration = started.elapsed();
262 remove_pending_journal(&latest)?;
263 debug(
264 "worktreeCopyTransaction",
265 format!(
266 "completed: install {} ms, stage and journal removal {} ms",
267 recorded.as_millis(),
268 (started.elapsed() - recorded).as_millis()
269 )
270 .as_str(),
271 );
272 return Ok(outcome);
273}
274
275pub fn synchronize_created_worktree(
286 context: &GitContext,
287 common_dir: &Path,
288 source_root: &Path,
289 destination_root: &Path,
290 registered_roots: &[PathBuf],
291 selection: &CopySelection,
292) -> Result<usize, WorktreeCopyError> {
293 return synchronize_created_worktree_noting(
294 context,
295 common_dir,
296 source_root,
297 destination_root,
298 registered_roots,
299 selection,
300 &mut std::io::sink(),
301 );
302}
303
304const NOTICE_PATHS: usize = 3;
309
310fn unreadable_notice(source_root: &Path, unreadable: &[String]) -> String {
320 let named: Vec<String> = unreadable.iter().take(NOTICE_PATHS).map(|path| return quote(path.as_str())).collect();
321 let rest: usize = unreadable.len().saturating_sub(NOTICE_PATHS);
322 let more: String = if rest == 0 { String::new() } else { format!(" and {rest} more") };
323 let noun: &str = if unreadable.len() == 1 { "entry" } else { "entries" };
324 return format!(
325 "cli-git: left out {} unreadable ignored {noun} from {}: {}{more}.\n",
326 unreadable.len(),
327 quote(source_root.to_string_lossy().as_ref()),
328 named.join(", ")
329 );
330}
331
332pub fn synchronize_created_worktree_noting(
343 context: &GitContext,
344 common_dir: &Path,
345 source_root: &Path,
346 destination_root: &Path,
347 registered_roots: &[PathBuf],
348 selection: &CopySelection,
349 notice: &mut dyn std::io::Write,
350) -> Result<usize, WorktreeCopyError> {
351 let started: std::time::Instant = std::time::Instant::now();
352 let claim: StageClaim = claim_stage(common_dir, destination_root)?;
353 let snapshot: StagedSnapshot = match stage_ignored_snapshot(
355 context,
356 source_root,
357 claim.container(),
358 registered_roots,
359 selection,
360 ) {
361 Ok(snapshot) => snapshot,
362 Err(error) => {
363 claim.release()?;
364 return Err(error);
365 }
366 };
367 let staged_done: std::time::Duration = started.elapsed();
368 let journaling: std::time::Instant = std::time::Instant::now();
369 let pending: PendingJournal = match create_journal(common_dir, destination_root, &snapshot) {
370 Ok(pending) => pending,
371 Err(error) => {
372 std::fs::remove_dir_all(snapshot.stage_container.as_path())
374 .map_err(|removal| return raw_failure(&removal))?;
375 claim.release()?;
376 return Err(error);
377 }
378 };
379 debug(
380 "worktreeCopyTransaction",
381 format!("journal created in {} ms after the stage at {} ms", journaling.elapsed().as_millis(), staged_done.as_millis()).as_str(),
382 );
383 claim.release()?;
385 let staged_after: std::time::Duration = started.elapsed();
386 let staged: StagedSnapshot = snapshot.clone();
387 let outcome: Outcome = complete_journal(pending, &|_intended| return Ok(staged.clone()))?;
388 debug(
389 "worktreeCopyTransaction",
390 format!(
391 "settled {} entries in {} ms: stage and journal {} ms, install and completion {} ms",
392 snapshot.entries.len(),
393 started.elapsed().as_millis(),
394 staged_after.as_millis(),
395 (started.elapsed() - staged_after).as_millis()
396 )
397 .as_str(),
398 );
399 return match outcome {
400 Outcome::Installed(copied) => {
401 if !snapshot.unreadable.is_empty() {
402 let _written = notice.write_all(unreadable_notice(source_root, snapshot.unreadable.as_slice()).as_bytes());
404 }
405 Ok(copied)
406 }
407 Outcome::Ended(error) => Err(error),
408 };
409}
410
411#[cfg(test)]
413#[path = "worktree_copy_transaction_tests.rs"]
414mod tests;