use std::io;
use crate::{
error::{TransactionPhase, WorktreeError, WorktreeErrorCode},
filesystem::{SlotEvidence, TargetAccess},
options::WorktreeOptions,
};
use super::UndoReplay;
use crate::operation::journal::{Record, Writer};
use crate::operation::undo::receipt::{ReceiptPath, StoredReceipt};
use crate::operation::undo::rollback::{
restore_path, restored_evidence, state_bytes, verify_restored,
};
pub(super) fn verify_finished(
receipt: &StoredReceipt,
accesses: &[TargetAccess],
replay: &UndoReplay,
options: WorktreeOptions,
) -> Result<(), WorktreeError> {
for (position, path) in receipt.paths().iter().enumerate() {
let access = &accesses[position];
if current(access, path, replay, options)? != restored(path, replay)? {
return Err(foreign(
path,
replay,
"finished undo no longer matches exact before evidence",
));
}
access.sync_parent().map_err(|error| {
path_io(
path,
replay,
"finished undo path did not synchronize",
error,
)
})?;
}
Ok(())
}
pub(super) fn complete(
receipt: &StoredReceipt,
accesses: &[TargetAccess],
replay: &UndoReplay,
writer: &mut Writer,
options: WorktreeOptions,
) -> Result<usize, WorktreeError> {
let mut removed = 0;
for (position, path) in receipt.paths().iter().enumerate().rev() {
removed += converge_path(&accesses[position], path, replay, writer, options)?;
}
Ok(removed)
}
fn converge_path(
access: &TargetAccess,
path: &ReceiptPath,
replay: &UndoReplay,
writer: &mut Writer,
options: WorktreeOptions,
) -> Result<usize, WorktreeError> {
if linked_restore_is_exact(access, path, replay, options)? {
return finish_linked_restore(access, path, replay, writer, options);
}
let actual = current(access, path, replay, options)?;
let restored = restored(path, replay)?;
if replay.rolled_back.contains(&path.index) {
if actual == restored {
return Ok(0);
}
return Err(foreign(
path,
replay,
"a journaled restored path no longer matches its before state",
));
}
if actual == restored {
if !replay.intents.contains(&path.index) {
return Err(foreign(
path,
replay,
"path was restored without a durable undo intent",
));
}
record(writer, replay, &Record::RolledBack { index: path.index })?;
return Ok(consumed(path));
}
if actual != path.after {
return Err(foreign(
path,
replay,
"path matches neither its committed nor its restored evidence",
));
}
if !replay.intents.contains(&path.index) {
record(
writer,
replay,
&Record::RollbackIntent { index: path.index },
)?;
}
restore_path(access, path, options)
.map_err(|error| error.in_transaction(replay.rollback_id.clone()))?;
record(writer, replay, &Record::RolledBack { index: path.index })?;
Ok(consumed(path))
}
fn finish_linked_restore(
access: &TargetAccess,
path: &ReceiptPath,
replay: &UndoReplay,
writer: &mut Writer,
options: WorktreeOptions,
) -> Result<usize, WorktreeError> {
if replay.rolled_back.contains(&path.index) || !replay.intents.contains(&path.index) {
return Err(foreign(
path,
replay,
"linked undo restore contradicts the journaled intents",
));
}
let (Some(name), Some(backup)) = (path.backup_name.as_deref(), path.backup) else {
return Err(foreign(
path,
replay,
"linked undo restore lacks its backup",
));
};
access
.finish_linked_install(name, backup.identity)
.map_err(|error| {
path_io(
path,
replay,
"failed to finish a linked undo restore",
error,
)
})?;
verify_restored(access, path, options)
.map_err(|error| error.in_transaction(replay.rollback_id.clone()))?;
record(writer, replay, &Record::RolledBack { index: path.index })?;
Ok(consumed(path))
}
fn linked_restore_is_exact(
access: &TargetAccess,
path: &ReceiptPath,
replay: &UndoReplay,
options: WorktreeOptions,
) -> Result<bool, WorktreeError> {
let (SlotEvidence::Present(_), Some(name), Some(backup)) =
(path.before, path.backup_name.as_deref(), path.backup)
else {
return Ok(false);
};
match access.same_file_as_artifact(name) {
Ok(false) => Ok(false),
Ok(true) => {
let evidence = access
.linked_artifact_evidence(name, state_bytes(options))
.map_err(|error| {
path_io(
path,
replay,
"failed to verify a linked undo restore",
error,
)
})?;
if evidence == backup {
Ok(true)
} else {
Err(foreign(
path,
replay,
"linked undo restore does not match retained evidence",
))
}
}
Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
Err(error) => Err(path_io(
path,
replay,
"failed to inspect a linked undo restore",
error,
)),
}
}
fn record(writer: &mut Writer, replay: &UndoReplay, record: &Record) -> Result<(), WorktreeError> {
writer.append(record).map(drop).map_err(|error| {
WorktreeError::with_source(
WorktreeErrorCode::RecoveryRequired,
TransactionPhase::Recover,
"failed to synchronize an undo recovery record",
error,
)
.in_transaction(replay.rollback_id.clone())
.requiring_recovery()
})
}
fn current(
access: &TargetAccess,
path: &ReceiptPath,
replay: &UndoReplay,
options: WorktreeOptions,
) -> Result<SlotEvidence, WorktreeError> {
access.slot_evidence(state_bytes(options)).map_err(|error| {
path_io(
path,
replay,
"failed to inspect an undo recovery target",
error,
)
})
}
fn restored(path: &ReceiptPath, replay: &UndoReplay) -> Result<SlotEvidence, WorktreeError> {
restored_evidence(path).map_err(|error| error.in_transaction(replay.rollback_id.clone()))
}
fn consumed(path: &ReceiptPath) -> usize {
usize::from(path.backup_name.is_some())
}
fn foreign(path: &ReceiptPath, replay: &UndoReplay, message: &str) -> WorktreeError {
WorktreeError::new(
WorktreeErrorCode::UndoConflict,
TransactionPhase::Recover,
message,
)
.at_path(path.path.clone())
.at_file(path.index as usize)
.in_transaction(replay.rollback_id.clone())
.requiring_recovery()
}
fn path_io(
path: &ReceiptPath,
replay: &UndoReplay,
message: &str,
error: io::Error,
) -> WorktreeError {
WorktreeError::with_source(
WorktreeErrorCode::RecoveryRequired,
TransactionPhase::Recover,
message,
error,
)
.at_path(path.path.clone())
.at_file(path.index as usize)
.in_transaction(replay.rollback_id.clone())
.requiring_recovery()
}