1use super::transaction_git::GitContext;
15use super::worktree_copy_error::{WorktreeCopyError, raw_failure};
16use super::worktree_copy_install::{InstallRecorder, install_snapshot};
17use super::worktree_copy_install_entry::InstalledPath;
18use super::worktree_copy_install_log::{InstallLog, open_install_log};
19use super::worktree_copy_journal::{
20 Journal, PendingJournal, Phase, create_journal, remove_pending_journal, write_journal,
21};
22use super::worktree_copy_manifest::{WorktreeCopyEntry, collect_entry_manifest};
23use super::worktree_copy_selection::CopySelection;
24use super::worktree_copy_stage::{StagedSnapshot, stage_ignored_snapshot};
25use crate::json_record::quote;
26use std::collections::BTreeSet;
27use std::path::{Path, PathBuf};
28
29pub struct TransactionState {
39 pub pending: PendingJournal,
41 pub log: InstallLog,
43 pub intended: BTreeSet<String>,
45 pub created_paths: BTreeSet<String>,
47}
48
49impl InstallRecorder for TransactionState {
50 fn record_intents(&mut self, relative_paths: &[String]) -> Result<(), WorktreeCopyError> {
52 let unclaimed: Vec<String> = relative_paths
53 .iter()
54 .filter(|relative_path| return !self.intended.contains(relative_path.as_str()))
55 .cloned()
56 .collect();
57 self.log.append_intents(unclaimed.as_slice())?;
58 self.intended.extend(unclaimed);
59 return Ok(());
60 }
61
62 fn record_creations(&mut self, entries: &[InstalledPath]) -> Result<(), WorktreeCopyError> {
64 let unrecorded: Vec<InstalledPath> = entries
65 .iter()
66 .filter(|entry| return !self.created_paths.contains(entry.relative_path.as_str()))
67 .cloned()
68 .collect();
69 self.log.append_creations(unrecorded.as_slice())?;
70 self.created_paths.extend(
71 unrecorded
72 .into_iter()
73 .map(|entry| return entry.relative_path),
74 );
75 return Ok(());
76 }
77}
78
79pub fn begin_installing(state: &mut TransactionState) -> Result<(), WorktreeCopyError> {
89 if state.pending.journal.phase == Phase::Installing {
90 return Ok(());
91 }
92 let installing: Journal = Journal {
93 phase: Phase::Installing,
94 ..state.pending.journal.clone()
95 };
96 write_journal(state.pending.path.as_path(), &installing)?;
97 state.pending.journal = installing;
98 return Ok(());
99}
100
101pub fn snapshot_from_journal(
111 pending: &PendingJournal,
112 intended: &BTreeSet<String>,
113) -> Result<StagedSnapshot, WorktreeCopyError> {
114 let stage_root: PathBuf = PathBuf::from(pending.journal.stage_root.as_str());
115 let outcome: Result<Vec<WorktreeCopyEntry>, WorktreeCopyError> = (|| {
116 let entries: Vec<WorktreeCopyEntry> = collect_entry_manifest(
117 stage_root.as_path(),
118 pending.journal.selected_roots.as_slice(),
119 &[],
120 )?;
121 let present: BTreeSet<&str> = entries
122 .iter()
123 .map(|entry| return entry.relative_path.as_str())
124 .collect();
125 for claim in intended {
126 if !present.contains(claim.as_str()) {
127 let message: String = format!(
128 "cli-git: worktree-copy journal intent is absent from private stage {}.",
129 quote(pending.journal.stage_root.as_str())
130 );
131 return Err(super::worktree_copy_error::failure_with_cause(
132 message.as_str(),
133 None,
134 ));
135 }
136 }
137 return Ok(entries);
138 })();
139 let entries: Vec<WorktreeCopyEntry> = match outcome {
141 Ok(entries) => entries,
142 Err(error) => {
143 let message: String = format!(
144 "cli-git: could not recover staged ignored state at {}.",
145 quote(pending.journal.stage_root.as_str())
146 );
147 return Err(super::worktree_copy_error::failure_with_cause(
148 message.as_str(),
149 Some(&error.0),
150 ));
151 }
152 };
153 return Ok(StagedSnapshot {
154 entries,
155 selected_roots: pending.journal.selected_roots.clone(),
156 source_root: PathBuf::from(pending.journal.source_root.as_str()),
157 stage_container: PathBuf::from(pending.journal.stage_container.as_str()),
158 stage_root,
159 });
160}
161
162#[derive(Debug)]
172pub enum Outcome {
173 Installed(usize),
175 Ended(WorktreeCopyError),
177}
178
179fn install_recorded(
189 pending: PendingJournal,
190 snapshot: &dyn Fn(&BTreeSet<String>) -> Result<StagedSnapshot, WorktreeCopyError>,
191) -> Result<(PendingJournal, Outcome), WorktreeCopyError> {
192 let stage: PathBuf = PathBuf::from(pending.journal.stage_container.as_str());
193 let opened = open_install_log(stage.as_path())?;
194 let mut intended: BTreeSet<String> = pending.journal.intended_entries.iter().cloned().collect();
195 intended.extend(opened.recorded.intended.iter().cloned());
196 let mut created_paths: BTreeSet<String> = pending
197 .journal
198 .created_entries
199 .iter()
200 .map(|entry| return entry.relative_path.clone())
201 .collect();
202 created_paths.extend(
203 opened
204 .recorded
205 .created
206 .iter()
207 .map(|entry| return entry.relative_path.clone()),
208 );
209 let mut prior: Vec<InstalledPath> = pending.journal.created_entries.clone();
210 prior.extend(opened.recorded.created.iter().cloned());
211 let staged: StagedSnapshot = snapshot(&intended)?;
212 let mut state: TransactionState = TransactionState {
213 pending,
214 log: opened.log,
215 intended,
216 created_paths,
217 };
218 begin_installing(&mut state)?;
219 let claimed: BTreeSet<String> = state
220 .intended
221 .union(&state.created_paths)
222 .cloned()
223 .collect();
224 let destination: PathBuf = PathBuf::from(state.pending.journal.destination_root.as_str());
225 let result: Result<usize, WorktreeCopyError> = install_snapshot(
226 staged.stage_root.as_path(),
227 destination.as_path(),
228 staged.entries.as_slice(),
229 &claimed,
230 prior.as_slice(),
231 &mut state,
232 );
233 let TransactionState {
234 pending: latest, ..
235 } = state;
236 match result {
238 Ok(copied) => return Ok((latest, Outcome::Installed(copied))),
239 Err(error) => return Ok((latest, Outcome::Ended(error))),
240 }
241}
242
243pub fn complete_journal(
253 pending: PendingJournal,
254 snapshot: &dyn Fn(&BTreeSet<String>) -> Result<StagedSnapshot, WorktreeCopyError>,
255) -> Result<Outcome, WorktreeCopyError> {
256 let (latest, outcome) = install_recorded(pending, snapshot)?;
257 remove_pending_journal(&latest)?;
258 return Ok(outcome);
259}
260
261pub fn synchronize_created_worktree(
271 context: &GitContext,
272 common_dir: &Path,
273 source_root: &Path,
274 destination_root: &Path,
275 registered_roots: &[PathBuf],
276 selection: &CopySelection,
277) -> Result<usize, WorktreeCopyError> {
278 let snapshot: StagedSnapshot = stage_ignored_snapshot(
279 context,
280 source_root,
281 destination_root,
282 registered_roots,
283 selection,
284 )?;
285 let pending: PendingJournal = match create_journal(common_dir, destination_root, &snapshot) {
286 Ok(pending) => pending,
287 Err(error) => {
288 std::fs::remove_dir_all(snapshot.stage_container.as_path())
290 .map_err(|removal| return raw_failure(&removal))?;
291 return Err(error);
292 }
293 };
294 let staged: StagedSnapshot = snapshot.clone();
295 let outcome: Outcome = complete_journal(pending, &|_intended| return Ok(staged.clone()))?;
296 return match outcome {
297 Outcome::Installed(copied) => Ok(copied),
298 Outcome::Ended(error) => Err(error),
299 };
300}
301
302#[cfg(test)]
304#[path = "worktree_copy_transaction_tests.rs"]
305mod tests;