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