1use crate::json_record::quote;
18use crate::parallel_budget::{HostResources, sliced_workers};
19use crate::private_storage::{sync_directory, write_exclusive_scratch, write_exclusive_synced};
20use crate::random_id::random_uuid;
21use crate::worktree_copy_error::{WorktreeCopyError, failure_with_cause, raw_failure};
22use crate::worktree_copy_install_entry::InstalledPath;
23use crate::worktree_copy_owner::{CopyOwner, current_copy_owner, token_owner_is_dead};
24use crate::worktree_copy_selection::{STAGE_PREFIX, assert_safe_repository_path, js_order};
25use crate::worktree_copy_stage::{PARALLEL_COPY_MAX_WORKERS, StagedSnapshot};
26use std::path::{Path, PathBuf};
27
28pub const JOURNAL_PARENT_NAME: &str = "cli-git-worktree-copy";
38
39pub const JOURNAL_VERSION_NAME: &str = "v1";
49
50const JOURNAL_SUFFIX: &str = ".json";
60
61pub const TEMPORARY_SUFFIX: &str = ".tmp";
71
72const NON_PRIVATE_MODE_BITS: u32 = 0o077;
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
93pub enum Phase {
94 Staged,
96 Installing,
98 Complete,
100}
101
102impl Phase {
103 fn as_str(self) -> &'static str {
105 match self {
107 Phase::Staged => return "staged",
108 Phase::Installing => return "installing",
109 Phase::Complete => return "complete",
110 }
111 }
112}
113
114#[derive(Clone, Debug, Eq, PartialEq)]
124pub struct Journal {
125 pub created_entries: Vec<InstalledPath>,
127 pub intended_entries: Vec<String>,
129 pub destination_root: String,
131 pub phase: Phase,
133 pub selected_roots: Vec<String>,
135 pub source_root: String,
137 pub stage_container: String,
139 pub stage_root: String,
141}
142
143#[derive(Clone, Debug, Eq, PartialEq)]
153pub struct PendingJournal {
154 pub path: PathBuf,
156 pub journal: Journal,
158}
159
160impl PendingJournal {
161 pub fn owner_token(&self) -> Option<String> {
163 return owner_token_of(self.path.as_path());
164 }
165}
166
167pub fn owner_token_of(path: &Path) -> Option<String> {
177 let name: String = path.file_name()?.to_string_lossy().into_owned();
178 let stem: &str = name.strip_suffix(JOURNAL_SUFFIX)?;
179 let (_identifier, token) = stem.rsplit_once('.')?;
180 return Some(token.to_string());
181}
182
183pub fn encode_journal(journal: &Journal) -> String {
193 let created: Vec<String> = journal
194 .created_entries
195 .iter()
196 .map(encode_installed_entry)
197 .collect();
198 return format!(
199 "{{\"createdEntries\":[{}],\"intendedEntries\":{},\"destinationRoot\":{},\"phase\":{},\"selectedRoots\":{},\"sourceRoot\":{},\"stageContainer\":{},\"stageRoot\":{},\"version\":1}}\n",
200 created.join(","),
201 string_array(journal.intended_entries.as_slice()),
202 quote(journal.destination_root.as_str()),
203 quote(journal.phase.as_str()),
204 string_array(journal.selected_roots.as_slice()),
205 quote(journal.source_root.as_str()),
206 quote(journal.stage_container.as_str()),
207 quote(journal.stage_root.as_str()),
208 );
209}
210
211pub fn encode_installed_entry(entry: &InstalledPath) -> String {
221 return format!(
222 "{{\"device\":\"{}\",\"inode\":\"{}\",\"relativePath\":{},\"selected\":{}}}",
223 entry.device,
224 entry.inode,
225 quote(entry.relative_path.as_str()),
226 entry.selected
227 );
228}
229
230pub fn string_array(items: &[String]) -> String {
240 let quoted: Vec<String> = items
241 .iter()
242 .map(|item| return quote(item.as_str()))
243 .collect();
244 return format!("[{}]", quoted.join(","));
245}
246
247fn is_canonical_absolute(value: &str) -> bool {
257 if !value.starts_with('/') {
258 return false;
259 }
260 if value == "/" {
261 return true;
262 }
263 return value[1..]
264 .split('/')
265 .all(|component| return !component.is_empty() && component != "." && component != "..");
266}
267
268fn decimal_identity(value: &serde_json::Value) -> Option<u64> {
278 let text: &str = value.as_str()?;
279 if text.is_empty() || (text.len() > 1 && text.starts_with('0')) {
280 return None;
281 }
282 if !text.bytes().all(|byte| return byte.is_ascii_digit()) {
283 return None;
284 }
285 return text.parse::<u64>().ok();
286}
287
288pub fn installed_entry(value: &serde_json::Value) -> Option<InstalledPath> {
298 let object = value.as_object()?;
299 return Some(InstalledPath {
300 device: decimal_identity(object.get("device")?)?,
301 inode: decimal_identity(object.get("inode")?)?,
302 relative_path: object.get("relativePath")?.as_str()?.to_string(),
303 selected: object.get("selected")?.as_bool()?,
304 });
305}
306
307fn string_list(value: Option<&serde_json::Value>) -> Option<Vec<String>> {
317 let array = value?.as_array()?;
318 return array
319 .iter()
320 .map(|item| return item.as_str().map(str::to_string))
321 .collect();
322}
323
324fn read_shape(value: &serde_json::Value) -> Option<Journal> {
334 let object = value.as_object()?;
335 if object.get("version")?.as_f64()? != 1.0 {
336 return None;
337 }
338 let phase: Phase = match object.get("phase")?.as_str()? {
339 "staged" => Phase::Staged,
340 "installing" => Phase::Installing,
341 "complete" => Phase::Complete,
342 _ => return None,
343 };
344 let created_entries: Vec<InstalledPath> = object
345 .get("createdEntries")?
346 .as_array()?
347 .iter()
348 .map(installed_entry)
349 .collect::<Option<Vec<InstalledPath>>>()?;
350 return Some(Journal {
351 created_entries,
352 intended_entries: string_list(object.get("intendedEntries"))?,
353 destination_root: object.get("destinationRoot")?.as_str()?.to_string(),
354 phase,
355 selected_roots: string_list(object.get("selectedRoots"))?,
356 source_root: object.get("sourceRoot")?.as_str()?.to_string(),
357 stage_container: object.get("stageContainer")?.as_str()?.to_string(),
358 stage_root: object.get("stageRoot")?.as_str()?.to_string(),
359 });
360}
361
362pub fn parse_journal(
372 text: &str,
373 path: &Path,
374 journal_root: &Path,
375) -> Result<Journal, WorktreeCopyError> {
376 let value: serde_json::Value = serde_json::from_str(text).map_err(|error| {
377 let message: String = format!(
378 "cli-git: could not read worktree-copy journal {}.",
379 quote(path.to_string_lossy().as_ref())
380 );
381 return failure_with_cause(message.as_str(), Some(&error));
382 })?;
383 let Some(journal) = read_shape(&value) else {
384 return Err(corrupt(path));
385 };
386 if path.parent() != Some(journal_root) {
387 return Err(failure_with_cause(
388 format!(
389 "cli-git: worktree-copy journal escaped private root: {}.",
390 quote(path.to_string_lossy().as_ref())
391 )
392 .as_str(),
393 None,
394 ));
395 }
396 for (field, field_value) in [
397 ("destinationRoot", journal.destination_root.as_str()),
398 ("sourceRoot", journal.source_root.as_str()),
399 ("stageContainer", journal.stage_container.as_str()),
400 ("stageRoot", journal.stage_root.as_str()),
401 ] {
402 if !is_canonical_absolute(field_value) {
403 return Err(failure_with_cause(
404 format!(
405 "cli-git: worktree-copy journal has unsafe {field}: {}.",
406 quote(field_value)
407 )
408 .as_str(),
409 None,
410 ));
411 }
412 }
413 let stage_relation: bool = Path::new(journal.stage_container.as_str()).parent()
414 == Path::new(journal.destination_root.as_str()).parent()
415 && Path::new(journal.stage_container.as_str())
416 .file_name()
417 .is_some_and(|name| return name.to_string_lossy().starts_with(STAGE_PREFIX))
418 && journal.stage_root == format!("{}/payload", journal.stage_container);
419 if !stage_relation {
420 return Err(failure_with_cause(
421 format!(
422 "cli-git: worktree-copy journal has unsafe private stage relation: {}.",
423 quote(path.to_string_lossy().as_ref())
424 )
425 .as_str(),
426 None,
427 ));
428 }
429 for selected in &journal.selected_roots {
430 assert_safe_repository_path(selected)?;
431 }
432 for intended in &journal.intended_entries {
433 assert_safe_repository_path(intended)?;
434 }
435 for created in &journal.created_entries {
436 assert_safe_repository_path(created.relative_path.as_str())?;
437 }
438 let created_paths: Vec<&str> = journal
439 .created_entries
440 .iter()
441 .map(|entry| return entry.relative_path.as_str())
442 .collect();
443 let duplicated: bool = has_duplicates(journal.selected_roots.as_slice())
444 || has_duplicates(journal.intended_entries.as_slice())
445 || has_duplicates(created_paths.as_slice());
446 if duplicated {
447 return Err(failure_with_cause(
448 format!(
449 "cli-git: worktree-copy journal contains duplicate repository paths: {}.",
450 quote(path.to_string_lossy().as_ref())
451 )
452 .as_str(),
453 None,
454 ));
455 }
456 return Ok(journal);
457}
458
459fn has_duplicates<T: AsRef<str>>(items: &[T]) -> bool {
469 let mut seen: std::collections::BTreeSet<&str> = std::collections::BTreeSet::new();
470 return items.iter().any(|item| return !seen.insert(item.as_ref()));
471}
472
473fn corrupt(path: &Path) -> WorktreeCopyError {
483 return failure_with_cause(
484 format!(
485 "cli-git: worktree-copy journal is corrupt: {}.",
486 quote(path.to_string_lossy().as_ref())
487 )
488 .as_str(),
489 None,
490 );
491}
492
493#[derive(Clone, Copy, Debug, Eq, PartialEq)]
503pub enum Role {
504 JournalDirectory,
506 JournalFile,
508 PrivateStage,
510}
511
512impl Role {
513 fn name(self) -> &'static str {
515 match self {
517 Role::JournalDirectory => return "journal directory",
518 Role::JournalFile => return "journal file",
519 Role::PrivateStage => return "private stage",
520 }
521 }
522}
523
524pub fn assert_private_path(path: &Path, role: Role) -> Result<(), WorktreeCopyError> {
534 use std::os::unix::fs::MetadataExt;
535 let metadata: std::fs::Metadata =
536 std::fs::symlink_metadata(path).map_err(|error| return raw_failure(&error))?;
537 let shape: bool = match role {
538 Role::JournalFile => metadata.is_file() && metadata.nlink() == 1,
539 Role::JournalDirectory | Role::PrivateStage => metadata.is_dir(),
540 };
541 let effective_user: u32 = unsafe { libc::geteuid() };
543 let canonical: bool = std::fs::canonicalize(path)
544 .map_err(|error| return raw_failure(&error))?
545 .as_path()
546 == path;
547 if !shape
548 || (metadata.mode() & NON_PRIVATE_MODE_BITS) != 0
549 || metadata.uid() != effective_user
550 || !canonical
551 {
552 return Err(failure_with_cause(
553 format!(
554 "cli-git: worktree-copy {} is unsafe: {}.",
555 role.name(),
556 quote(path.to_string_lossy().as_ref())
557 )
558 .as_str(),
559 None,
560 ));
561 }
562 return Ok(());
563}
564
565pub fn journal_root(common_dir: &Path) -> PathBuf {
575 return common_dir
576 .join(JOURNAL_PARENT_NAME)
577 .join(JOURNAL_VERSION_NAME);
578}
579
580fn ensure_private_directory(path: &Path) -> Result<(), WorktreeCopyError> {
590 use std::os::unix::fs::DirBuilderExt;
591 match std::fs::DirBuilder::new().mode(0o700).create(path) {
593 Ok(()) => {}
594 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {}
595 Err(error) => return Err(raw_failure(&error)),
596 }
597 return assert_private_path(path, Role::JournalDirectory);
598}
599
600pub fn ensure_journal_root(common_dir: &Path) -> Result<PathBuf, WorktreeCopyError> {
610 ensure_private_directory(common_dir.join(JOURNAL_PARENT_NAME).as_path())?;
611 let root: PathBuf = journal_root(common_dir);
612 ensure_private_directory(root.as_path())?;
613 return Ok(root);
614}
615
616pub fn write_journal(path: &Path, journal: &Journal) -> Result<(), WorktreeCopyError> {
626 return write_atomic(path, encode_journal(journal).as_bytes(), "journal");
627}
628
629pub fn temporary_path(path: &Path, owner: &CopyOwner, unique: &str) -> PathBuf {
640 return PathBuf::from(format!("{}.{}.{unique}{TEMPORARY_SUFFIX}", path.to_string_lossy(), owner.token()));
641}
642
643pub fn temporary_writer_token(file_name: &str) -> Option<&str> {
653 let stem: &str = file_name.strip_suffix(TEMPORARY_SUFFIX)?;
654 let (rest, _unique) = stem.rsplit_once('.')?;
655 let (_item, token) = rest.rsplit_once('.')?;
656 return Some(token);
657}
658
659pub fn write_atomic(path: &Path, bytes: &[u8], subject: &str) -> Result<(), WorktreeCopyError> {
669 return write_atomic_flushing(path, bytes, subject, Flush::ToDisk);
670}
671
672#[derive(Clone, Copy, Debug, Eq, PartialEq)]
674enum Flush {
675 ToDisk,
677 Never,
679}
680
681pub fn write_atomic_unsynced(path: &Path, bytes: &[u8], subject: &str) -> Result<(), WorktreeCopyError> {
694 return write_atomic_flushing(path, bytes, subject, Flush::Never);
695}
696
697fn write_atomic_flushing(path: &Path, bytes: &[u8], subject: &str, flush: Flush) -> Result<(), WorktreeCopyError> {
700 let unique: String = random_uuid().map_err(|error| {
701 return failure_with_cause(
702 format!(
703 "cli-git: could not persist worktree-copy {subject} {}.",
704 quote(path.to_string_lossy().as_ref())
705 )
706 .as_str(),
707 Some(&error.0),
708 );
709 })?;
710 let temporary: PathBuf = temporary_path(path, ¤t_copy_owner()?, unique.as_str());
711 let written: std::io::Result<()> = match flush {
712 Flush::ToDisk => write_exclusive_synced(temporary.as_path(), bytes),
713 Flush::Never => write_exclusive_scratch(temporary.as_path(), bytes),
714 };
715 let persisted: std::io::Result<()> =
716 written
717 .and_then(|()| return std::fs::rename(temporary.as_path(), path))
718 .and_then(|()| match (flush, path.parent()) {
719 (Flush::ToDisk, Some(parent)) => return sync_directory(parent),
720 _ => return Ok(()),
721 });
722 if let Err(error) = persisted {
724 let _ignored_removal: std::io::Result<()> = std::fs::remove_file(temporary.as_path());
725 return Err(failure_with_cause(
726 format!(
727 "cli-git: could not persist worktree-copy {subject} {}.",
728 quote(path.to_string_lossy().as_ref())
729 )
730 .as_str(),
731 Some(&error),
732 ));
733 }
734 return Ok(());
735}
736
737pub fn create_journal(
747 common_dir: &Path,
748 destination_root: &Path,
749 snapshot: &StagedSnapshot,
750) -> Result<PendingJournal, WorktreeCopyError> {
751 return create_journal_for(common_dir, destination_root, snapshot, ¤t_copy_owner()?);
752}
753
754pub fn create_journal_for(
764 common_dir: &Path,
765 destination_root: &Path,
766 snapshot: &StagedSnapshot,
767 owner: &CopyOwner,
768) -> Result<PendingJournal, WorktreeCopyError> {
769 let root: PathBuf = ensure_journal_root(common_dir)?;
770 let unique: String = random_uuid().map_err(|error| return raw_failure(&error.0))?;
771 let path: PathBuf = root.join(format!("{unique}.{}{JOURNAL_SUFFIX}", owner.token()));
772 let journal: Journal = Journal {
773 created_entries: Vec::new(),
774 intended_entries: Vec::new(),
775 destination_root: destination_root.to_string_lossy().into_owned(),
776 phase: Phase::Staged,
777 selected_roots: snapshot.selected_roots.clone(),
778 source_root: snapshot.source_root.to_string_lossy().into_owned(),
779 stage_container: snapshot.stage_container.to_string_lossy().into_owned(),
780 stage_root: snapshot.stage_root.to_string_lossy().into_owned(),
781 };
782 write_journal(path.as_path(), &journal)?;
783 return Ok(PendingJournal { path, journal });
784}
785
786pub fn adopt_journal(pending: &PendingJournal, owner: &CopyOwner) -> Result<Option<PendingJournal>, WorktreeCopyError> {
798 let Some(parent) = pending.path.parent() else {
799 return Ok(None);
800 };
801 let name: String = pending.path.file_name().unwrap_or_default().to_string_lossy().into_owned();
802 let identifier: &str = name.split('.').next().unwrap_or_default();
803 let adopted: PathBuf = parent.join(format!("{identifier}.{}{JOURNAL_SUFFIX}", owner.token()));
804 match std::fs::rename(pending.path.as_path(), adopted.as_path()) {
806 Ok(()) => {}
807 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
808 Err(error) => return Err(raw_failure(&error)),
809 }
810 sync_directory(parent).map_err(|error| return raw_failure(&error))?;
811 return Ok(Some(PendingJournal { path: adopted, journal: pending.journal.clone() }));
812}
813
814pub fn journal_owner_is_dead(pending: &PendingJournal) -> Result<bool, WorktreeCopyError> {
824 return token_owner_is_dead(pending.owner_token().as_deref(), pending.path.as_path());
825}
826
827fn read_listed_journal(root: &Path, name: &str) -> Result<Option<PendingJournal>, WorktreeCopyError> {
838 let path: PathBuf = root.join(name);
839 let loaded: Result<Journal, WorktreeCopyError> = (|| {
840 assert_private_path(path.as_path(), Role::JournalFile)?;
841 let text: String = read_journal_text(path.as_path())?;
842 return parse_journal(text.as_str(), path.as_path(), root);
843 })();
844 match loaded {
846 Ok(journal) => return Ok(Some(PendingJournal { path, journal })),
847 Err(error) => {
848 let vanished: bool = matches!(std::fs::symlink_metadata(path.as_path()), Err(ref lookup) if lookup.kind() == std::io::ErrorKind::NotFound);
849 if vanished {
850 return Ok(None);
851 }
852 return Err(error);
853 }
854 }
855}
856
857pub fn read_pending_journals(common_dir: &Path) -> Result<Vec<PendingJournal>, WorktreeCopyError> {
867 let root: PathBuf = journal_root(common_dir);
868 match std::fs::symlink_metadata(root.as_path()) {
870 Ok(_) => {}
871 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
872 Err(error) => return Err(raw_failure(&error)),
873 }
874 assert_private_path(
875 root.parent().unwrap_or(root.as_path()),
876 Role::JournalDirectory,
877 )?;
878 assert_private_path(root.as_path(), Role::JournalDirectory)?;
879 let mut names: Vec<String> = Vec::new();
880 let listing = std::fs::read_dir(root.as_path()).map_err(|error| {
881 let message: String = format!(
882 "cli-git: could not inspect worktree-copy journals under {}.",
883 quote(root.to_string_lossy().as_ref())
884 );
885 return failure_with_cause(message.as_str(), Some(&error));
886 })?;
887 for listed in listing {
888 let entry: std::fs::DirEntry = listed.map_err(|error| return raw_failure(&error))?;
889 let name: String = entry.file_name().to_string_lossy().into_owned();
890 if name.ends_with(JOURNAL_SUFFIX) {
891 names.push(name);
892 }
893 }
894 names.sort_by(|left, right| return js_order(left, right));
895 let mut pending: Vec<PendingJournal> = Vec::new();
896 for name in names {
897 if let Some(found) = read_listed_journal(root.as_path(), name.as_str())? {
898 pending.push(found);
899 }
900 }
901 return Ok(pending);
902}
903
904fn read_journal_text(path: &Path) -> Result<String, WorktreeCopyError> {
914 let bytes: Vec<u8> = std::fs::read(path).map_err(|error| {
915 let message: String = format!(
916 "cli-git: could not read worktree-copy journal {}.",
917 quote(path.to_string_lossy().as_ref())
918 );
919 return failure_with_cause(message.as_str(), Some(&error));
920 })?;
921 return Ok(String::from_utf8_lossy(bytes.as_slice()).into_owned());
922}
923
924pub fn prepare_stage_removal(stage_container: &Path) -> Result<(), WorktreeCopyError> {
934 use std::os::unix::fs::PermissionsExt;
935 let mut pending: Vec<PathBuf> = vec![stage_container.to_path_buf()];
936 while let Some(directory) = pending.pop() {
937 let metadata: std::fs::Metadata = match std::fs::symlink_metadata(directory.as_path()) {
939 Ok(metadata) => metadata,
940 Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
941 Err(error) => return Err(raw_failure(&error)),
942 };
943 if !metadata.is_dir() {
944 continue;
945 }
946 std::fs::set_permissions(directory.as_path(), std::fs::Permissions::from_mode(0o700))
947 .map_err(|error| return raw_failure(&error))?;
948 for listed in
949 std::fs::read_dir(directory.as_path()).map_err(|error| return raw_failure(&error))?
950 {
951 let child: std::fs::DirEntry = listed.map_err(|error| return raw_failure(&error))?;
952 let child_type: std::fs::FileType = child
953 .file_type()
954 .map_err(|error| return raw_failure(&error))?;
955 if child_type.is_dir() {
956 pending.push(child.path());
957 }
958 }
959 }
960 return Ok(());
961}
962
963pub fn remove_stage(stage_container: &Path) -> Result<(), WorktreeCopyError> {
973 #[cfg(target_os = "linux")]
975 if crate::fs_kind::is_subvolume_root(stage_container).unwrap_or(false) && crate::fs_btrfs_ioctl::subvolume_destroy_at(stage_container).is_ok() {
976 return Ok(());
977 }
978 prepare_stage_removal(stage_container)?;
979 return remove_tree_if_present(stage_container);
980}
981
982pub fn remove_pending_journal(pending: &PendingJournal) -> Result<(), WorktreeCopyError> {
992 if pending.journal.phase != Phase::Complete {
993 let completed: Journal = Journal {
994 phase: Phase::Complete,
995 ..pending.journal.clone()
996 };
997 write_journal(pending.path.as_path(), &completed)?;
998 }
999 remove_stage(Path::new(pending.journal.stage_container.as_str()))?;
1000 remove_file_if_present(pending.path.as_path())?;
1001 remove_unfinished_writes(pending.path.as_path())?;
1002 if let Some(parent) = pending.path.parent() {
1003 sync_directory(parent).map_err(|error| return raw_failure(&error))?;
1004 }
1005 return Ok(());
1006}
1007
1008const REMOVAL_SPLIT_DEPTH: usize = 3;
1023
1024fn remove_tree_split(path: &Path) -> std::io::Result<()> {
1036 let mut shallow: Vec<PathBuf> = vec![path.to_path_buf()];
1037 let mut level: Vec<PathBuf> = vec![path.to_path_buf()];
1038 let mut subtrees: Vec<PathBuf> = Vec::new();
1039 for depth in 0..REMOVAL_SPLIT_DEPTH {
1040 let mut next: Vec<PathBuf> = Vec::new();
1041 for directory in &level {
1042 for listed in std::fs::read_dir(directory)? {
1043 let entry: std::fs::DirEntry = listed?;
1044 let child: PathBuf = entry.path();
1045 if !entry.file_type()?.is_dir() {
1046 std::fs::remove_file(child.as_path())?;
1047 } else if depth + 1 == REMOVAL_SPLIT_DEPTH {
1048 subtrees.push(child);
1049 } else {
1050 shallow.push(child.clone());
1051 next.push(child);
1052 }
1053 }
1054 }
1055 level = next;
1056 }
1057 let workers: usize = sliced_workers(usize::MAX, PARALLEL_COPY_MAX_WORKERS, &HostResources::read());
1058 let cursor: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
1059 let outcomes: Vec<std::io::Result<()>> = std::thread::scope(|scope| {
1060 let handles: Vec<_> = (0..workers)
1061 .map(|_worker| {
1062 return scope.spawn(|| -> std::io::Result<()> {
1063 loop {
1064 let index: usize = cursor.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
1065 let Some(subtree) = subtrees.get(index) else {
1066 return Ok(());
1067 };
1068 std::fs::remove_dir_all(subtree.as_path())?;
1069 }
1070 });
1071 })
1072 .collect();
1073 return handles
1074 .into_iter()
1075 .map(|handle| return handle.join().unwrap_or_else(|_panic| return Err(std::io::Error::other("a removal worker panicked"))))
1076 .collect();
1077 });
1078 for outcome in outcomes {
1079 outcome?;
1080 }
1081 for directory in shallow.iter().rev() {
1082 std::fs::remove_dir(directory.as_path())?;
1083 }
1084 return Ok(());
1085}
1086
1087fn remove_tree_if_present(path: &Path) -> Result<(), WorktreeCopyError> {
1098 match remove_tree_split(path).or_else(|_split_failure| return std::fs::remove_dir_all(path)) {
1100 Ok(()) => return Ok(()),
1101 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
1102 Err(error) => return Err(raw_failure(&error)),
1103 }
1104}
1105
1106pub fn remove_file_if_present(path: &Path) -> Result<(), WorktreeCopyError> {
1116 match std::fs::remove_file(path) {
1118 Ok(()) => return Ok(()),
1119 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
1120 Err(error) => return Err(raw_failure(&error)),
1121 }
1122}
1123
1124fn remove_unfinished_writes(path: &Path) -> Result<(), WorktreeCopyError> {
1134 let Some(parent) = path.parent() else {
1135 return Ok(());
1136 };
1137 let prefix: String = format!(
1138 "{}.",
1139 path.file_name().unwrap_or_default().to_string_lossy()
1140 );
1141 for listed in std::fs::read_dir(parent).map_err(|error| return raw_failure(&error))? {
1142 let entry: std::fs::DirEntry = listed.map_err(|error| return raw_failure(&error))?;
1143 let name: String = entry.file_name().to_string_lossy().into_owned();
1144 if name.starts_with(prefix.as_str()) && name.ends_with(TEMPORARY_SUFFIX) {
1145 remove_file_if_present(entry.path().as_path())?;
1146 }
1147 }
1148 return Ok(());
1149}
1150
1151#[cfg(test)]
1153#[path = "worktree_copy_journal_tests.rs"]
1154mod tests;