weavatrix-worktree 0.2.1

Bounded, crash-recoverable multi-file worktree edits for source refactoring
Documentation
use crate::{
    error::{TransactionPhase, WorktreeError, WorktreeErrorCode},
    journal::{FinishOutcome, JournalRecord},
};

use super::{
    PreparedTransaction,
    stage::cleanup_staged,
    util::{fs_error, journal_error},
    verify::{verify_artifact, verify_target},
};

pub(crate) fn rollback_committed(
    transaction: &mut PreparedTransaction,
    committed: &[usize],
) -> Result<(), WorktreeError> {
    for &index in committed.iter().rev() {
        rollback_one(transaction, index)?;
    }
    transaction
        .journal
        .append(&JournalRecord::Finished {
            outcome: FinishOutcome::RolledBack,
        })
        .map_err(|error| {
            journal_error(
                TransactionPhase::Rollback,
                "failed to record completed rollback",
                error,
            )
            .requiring_recovery()
        })?;
    cleanup_staged(&transaction.files)?;
    transaction.control.remove_journal().map_err(|error| {
        WorktreeError::with_source(
            WorktreeErrorCode::RecoveryRequired,
            TransactionPhase::Cleanup,
            "rollback succeeded but journal cleanup failed",
            error,
        )
        .requiring_recovery()
    })
}

fn rollback_one(transaction: &mut PreparedTransaction, index: usize) -> Result<(), WorktreeError> {
    let file = &transaction.files[index];
    verify_target(
        &file.access,
        file.new_hash,
        None,
        transaction.options.limits.max_output_bytes_per_file,
        file.original_index,
        TransactionPhase::Rollback,
    )
    .map_err(|error| recovery_error(&error))?;
    verify_artifact(
        &file.access,
        &file.backup_name,
        file.old_hash,
        transaction.options.limits.max_source_bytes_per_file,
        file.original_index,
        TransactionPhase::Rollback,
    )?;
    transaction
        .journal
        .append(&JournalRecord::RollbackIntent {
            index: file.stable_index,
        })
        .map_err(|error| {
            journal_error(
                TransactionPhase::Rollback,
                "failed to synchronize rollback intent",
                error,
            )
            .requiring_recovery()
        })?;
    file.access
        .rename_from(&file.backup_name)
        .and_then(|()| file.access.sync_parent())
        .map_err(|error| {
            fs_error(
                TransactionPhase::Rollback,
                file.access.path(),
                file.original_index,
                "failed to restore a backup",
                error,
            )
            .requiring_recovery()
        })?;
    transaction
        .journal
        .append(&JournalRecord::RolledBack {
            index: file.stable_index,
        })
        .map_err(|error| {
            journal_error(
                TransactionPhase::Rollback,
                "failed to record restored target",
                error,
            )
            .requiring_recovery()
        })?;
    Ok(())
}

fn recovery_error(error: &WorktreeError) -> WorktreeError {
    WorktreeError::new(
        WorktreeErrorCode::RollbackFailed,
        TransactionPhase::Rollback,
        error.to_string(),
    )
    .requiring_recovery()
}