use std::{fs, path::Path};
use anyhow::{bail, ensure, Context, Result};
use crate::{
commands::{checkout, rm},
repository::Repository,
};
pub(crate) async fn execute(abort: bool) -> Result<()> {
let current_dir = std::env::current_dir().context("无法确定当前工作目录")?;
let repository = Repository::discover(¤t_dir)?;
let outcome = tokio::task::spawn_blocking(move || recover_repository(&repository, abort))
.await
.context("Recover 任务异常终止")??;
match outcome {
RecoveryReport::Nothing => println!("No unfinished transaction"),
RecoveryReport::Checkout { target_id, aborted } => {
if aborted {
println!("Aborted checkout transaction for {target_id}");
} else {
println!("Recovered checkout transaction to {target_id}");
}
}
RecoveryReport::Removal { id, aborted } => {
if aborted {
println!("Aborted rm transaction {id}");
} else {
println!("Recovered rm transaction {id}");
}
}
}
Ok(())
}
enum RecoveryReport {
Nothing,
Checkout { target_id: String, aborted: bool },
Removal { id: String, aborted: bool },
}
fn recover_repository(repository: &Repository, abort: bool) -> Result<RecoveryReport> {
let mut lock = repository.acquire_recovery_lock()?;
let transactions = repository.unfinished_transactions()?;
if transactions.is_empty() {
return Ok(RecoveryReport::Nothing);
}
ensure!(
transactions.len() == 1,
"检测到多个未完成事务,无法安全推断恢复顺序: {}",
transactions
.iter()
.map(|path| path.display().to_string())
.collect::<Vec<_>>()
.join(", ")
);
let transaction = transactions.first().context("内部错误:事务列表为空")?;
let operation = read_operation(transaction)?;
match operation.as_str() {
"checkout" => {
let outcome = checkout::recover_transaction(repository, transaction, abort, &mut lock)?;
Ok(RecoveryReport::Checkout {
target_id: outcome.target_id,
aborted: outcome.aborted,
})
}
"rm" => {
let id = transaction
.file_name()
.and_then(|name| name.to_str())
.context("rm 事务目录名不是有效 UTF-8")?
.to_owned();
lock.set_operation("rm-recover", Some(&id))?;
rm::recover_transaction(repository, transaction, abort)?;
lock.set_operation("rm-recover", None)?;
Ok(RecoveryReport::Removal { id, aborted: abort })
}
other => bail!("不支持的事务类型 {other:?}: {}", transaction.display()),
}
}
fn read_operation(transaction: &Path) -> Result<String> {
let path = transaction.join("OPERATION");
let metadata = fs::symlink_metadata(&path)
.with_context(|| format!("无法检查事务类型文件: {}", path.display()))?;
ensure!(
metadata.is_file() && !metadata.file_type().is_symlink(),
"事务类型路径不是普通文件: {}",
path.display()
);
let operation = fs::read_to_string(&path)
.with_context(|| format!("无法读取事务类型: {}", path.display()))?;
ensure!(!operation.trim().is_empty(), "事务类型不能为空");
Ok(operation.trim().to_owned())
}