gitdataai/libs/git/hook/sync/fsck.rs
2026-04-14 19:02:01 +08:00

141 lines
4.8 KiB
Rust

use crate::GitError;
use crate::hook::sync::HookMetaDataSync;
use db::database::AppTransaction;
use models::system::notify;
use sea_orm::*;
use std::collections::HashMap;
use std::process::Command;
impl HookMetaDataSync {
pub async fn run_fsck_and_rollback_if_corrupt(
&self,
txn: &AppTransaction,
) -> Result<(), GitError> {
let snapshot = self.snapshot_refs();
let storage_path = self.repo.storage_path.clone();
let logger = self.logger.clone();
let fsck_errors = tokio::task::spawn_blocking(move || {
let output = Command::new("git")
.arg("-C")
.arg(&storage_path)
.arg("fsck")
.arg("--full")
.output()
.map_err(|e| GitError::IoError(format!("git fsck failed: {}", e)))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
slog::warn!(
logger,
"git fsck failed with code {:?}. stdout: {}, stderr: {}",
output.status.code(),
stdout,
stderr
);
return Ok(Some(format!("{}\n{}", stdout, stderr)));
}
Ok::<Option<String>, GitError>(None)
})
.await
.map_err(|e| GitError::Internal(format!("spawn_blocking join error: {}", e)))??;
if let Some(errors) = fsck_errors {
self.rollback_refs(&snapshot).await;
let notification = notify::ActiveModel {
user: Set(self.repo.created_by),
title: Set(format!(
"Repository sync rollback triggered: {}",
self.repo.repo_name
)),
description: Set(Some("Repository integrity check failed".to_string())),
content: Set(format!(
"Repository {} sync failed and has been rolled back.\nError: {}",
self.repo.repo_name, errors
)),
kind: Set(1),
created_at: Set(chrono::Utc::now()),
..Default::default()
};
notify::Entity::insert(notification)
.exec(txn)
.await
.map_err(|e| GitError::IoError(format!("failed to insert notification: {}", e)))?;
return Err(GitError::Internal(format!(
"repository corruption detected: {}",
errors
)));
}
Ok(())
}
fn snapshot_refs(&self) -> HashMap<String, String> {
let mut snapshot = HashMap::new();
let repo = self.domain.repo();
if let Ok(refs) = repo.references() {
for r in refs.flatten() {
let name = match r.name() {
Some(n) => n.to_string(),
None => continue,
};
let oid = match r.target() {
Some(o) => o.to_string(),
None => continue,
};
if name.starts_with("refs/heads/") || name.starts_with("refs/tags/") {
snapshot.insert(name, oid);
}
}
}
snapshot
}
async fn rollback_refs(&self, snapshot: &HashMap<String, String>) {
let storage_path = self.repo.storage_path.clone();
let logger = self.logger.clone();
let refs: Vec<(String, String)> = snapshot
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect();
let _ = tokio::task::spawn_blocking(move || {
for (ref_name, oid) in &refs {
let status = Command::new("git")
.arg("-C")
.arg(&storage_path)
.arg("update-ref")
.arg("-m")
.arg("rollback: integrity check failed")
.arg(ref_name)
.arg(oid)
.arg("HEAD")
.status();
match status {
Ok(s) if s.success() => {
slog::info!(logger, "rolled back ref {} to {}", ref_name, oid);
}
Ok(s) => {
slog::error!(
logger,
"failed to rollback ref {}: git exited with {:?}",
ref_name,
s.code()
);
}
Err(e) => {
slog::error!(logger, "failed to rollback ref {}: {}", ref_name, e);
}
}
}
})
.await;
}
}