use crate::worktree_copy_compare::{entry_matches, lstat_or_absent};
use crate::worktree_copy_error::WorktreeCopyError;
use crate::worktree_copy_install_entry::InstalledPath;
use crate::worktree_copy_manifest::{EntryKind, WorktreeCopyEntry};
use crate::worktree_copy_selection::filesystem_path;
use std::collections::BTreeMap;
use std::os::unix::fs::MetadataExt;
use std::path::Path;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum IdentityState {
Absent,
Changed,
Match,
}
fn identity_state(
path: &Path,
installed: &InstalledPath,
) -> Result<IdentityState, WorktreeCopyError> {
return match lstat_or_absent(path)? {
None => Ok(IdentityState::Absent),
Some(metadata) => {
if metadata.dev() == installed.device && metadata.ino() == installed.inode {
Ok(IdentityState::Match)
} else {
Ok(IdentityState::Changed)
}
}
};
}
fn remove_proven(path: &Path, is_directory: bool) -> std::io::Result<()> {
if is_directory {
return std::fs::remove_dir(path);
}
return std::fs::remove_file(path);
}
pub fn rollback_created(
stage_root: &Path,
destination_root: &Path,
entries: &[WorktreeCopyEntry],
created: &[InstalledPath],
) -> Result<Vec<String>, WorktreeCopyError> {
let selected_by_path: BTreeMap<&str, &WorktreeCopyEntry> = entries
.iter()
.map(|entry| return (entry.relative_path.as_str(), entry))
.collect();
let mut retained: Vec<String> = Vec::new();
for installed in created.iter().rev() {
let destination_path: std::path::PathBuf =
filesystem_path(destination_root, installed.relative_path.as_str())?;
let expected: Option<&&WorktreeCopyEntry> =
selected_by_path.get(installed.relative_path.as_str());
let outcome: Result<Option<String>, WorktreeCopyError> = rollback_one(
stage_root,
destination_root,
destination_path.as_path(),
installed,
expected.copied(),
);
match outcome {
Ok(None) => {}
Ok(Some(reason)) => retained.push(reason),
Err(error) => retained.push(format!("{}: {}", installed.relative_path, error.0)),
}
}
return Ok(retained);
}
fn rollback_one(
stage_root: &Path,
destination_root: &Path,
destination_path: &Path,
installed: &InstalledPath,
expected: Option<&WorktreeCopyEntry>,
) -> Result<Option<String>, WorktreeCopyError> {
let initial: IdentityState = identity_state(destination_path, installed)?;
match initial {
IdentityState::Absent => return Ok(None),
IdentityState::Changed => {
return Ok(Some(format!(
"{}: filesystem identity changed",
installed.relative_path
)));
}
IdentityState::Match => {}
}
let content_differs: bool = match expected {
Some(entry) if entry.kind != EntryKind::Directory => {
!entry_matches(stage_root, destination_root, entry)?
}
_ => false,
};
if content_differs {
return Ok(Some(installed.relative_path.clone()));
}
let final_state: IdentityState = identity_state(destination_path, installed)?;
match final_state {
IdentityState::Match => {}
IdentityState::Absent => return Ok(None),
IdentityState::Changed => {
return Ok(Some(format!(
"{}: filesystem identity changed",
installed.relative_path
)));
}
}
let is_directory: bool = expected
.is_some_and(|entry| return entry.kind == EntryKind::Directory)
|| !installed.selected;
remove_proven(destination_path, is_directory)
.map_err(|error| return WorktreeCopyError(error.to_string()))?;
return Ok(None);
}
#[cfg(test)]
#[path = "worktree_copy_rollback_tests.rs"]
mod tests;