pub fn save_rebase_checkpoint(checkpoint: &RebaseCheckpoint) -> io::Result<()> {
let json = serde_json::to_string_pretty(checkpoint).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to serialize rebase checkpoint: {e}"),
)
})?;
fs::create_dir_all(AGENT_DIR)?;
let checkpoint_existed = Path::new(&rebase_checkpoint_path()).exists();
let _ = backup_checkpoint();
let checkpoint_path_str = rebase_checkpoint_path();
let temp_path = format!("{checkpoint_path_str}.tmp");
let write_result = fs::write(&temp_path, &json);
if write_result.is_err() {
let _ = fs::remove_file(&temp_path);
return write_result;
}
let rename_result = fs::rename(&temp_path, &checkpoint_path_str);
if rename_result.is_err() {
let _ = fs::remove_file(&temp_path);
return rename_result;
}
if !checkpoint_existed {
let _ = backup_checkpoint();
}
Ok(())
}
pub fn load_rebase_checkpoint() -> io::Result<Option<RebaseCheckpoint>> {
let checkpoint = rebase_checkpoint_path();
let path = Path::new(&checkpoint);
if !path.exists() {
return Ok(None);
}
let content = fs::read_to_string(path)?;
let loaded_checkpoint: RebaseCheckpoint = match serde_json::from_str(&content) {
Ok(cp) => cp,
Err(e) => {
let backup_result = restore_from_backup();
return if backup_result.is_err() {
Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Checkpoint corrupted: {e}; backup restore failed: {}",
backup_result.unwrap_err()
),
))
} else {
backup_result
};
}
};
if let Err(e) = validate_checkpoint(&loaded_checkpoint) {
let backup_result = restore_from_backup();
return if backup_result.is_err() {
Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Checkpoint validation failed: {e}; backup restore failed: {}",
backup_result.unwrap_err()
),
))
} else {
backup_result
};
}
Ok(Some(loaded_checkpoint))
}
pub fn clear_rebase_checkpoint() -> io::Result<()> {
let checkpoint = rebase_checkpoint_path();
let path = Path::new(&checkpoint);
if path.exists() {
fs::remove_file(path)?;
}
Ok(())
}
#[must_use]
pub fn rebase_checkpoint_exists() -> bool {
Path::new(&rebase_checkpoint_path()).exists()
}
#[cfg(any(test, feature = "test-utils"))]
pub fn validate_checkpoint(checkpoint: &RebaseCheckpoint) -> io::Result<()> {
validate_checkpoint_impl(checkpoint)
}
#[cfg(not(any(test, feature = "test-utils")))]
fn validate_checkpoint(checkpoint: &RebaseCheckpoint) -> io::Result<()> {
validate_checkpoint_impl(checkpoint)
}
fn validate_checkpoint_impl(checkpoint: &RebaseCheckpoint) -> io::Result<()> {
if checkpoint.phase != RebasePhase::NotStarted && checkpoint.upstream_branch.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Checkpoint has empty upstream branch",
));
}
if chrono::DateTime::parse_from_rfc3339(&checkpoint.timestamp).is_err() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"Checkpoint has invalid timestamp format",
));
}
checkpoint.resolved_files.iter().try_for_each(|resolved| {
if checkpoint.conflicted_files.contains(resolved) {
return Ok(());
}
Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("Resolved file '{resolved}' not found in conflicted files list"),
))
})?;
Ok(())
}
fn backup_checkpoint() -> io::Result<()> {
let checkpoint_path = rebase_checkpoint_path();
let backup_path = rebase_checkpoint_backup_path();
let checkpoint = Path::new(&checkpoint_path);
let backup = Path::new(&backup_path);
if !checkpoint.exists() {
return Ok(());
}
if backup.exists() {
fs::remove_file(backup)?;
}
fs::copy(checkpoint, backup)?;
Ok(())
}
fn restore_from_backup() -> io::Result<Option<RebaseCheckpoint>> {
let backup_path = rebase_checkpoint_backup_path();
let backup = Path::new(&backup_path);
if !backup.exists() {
return Ok(None);
}
let content = fs::read_to_string(backup)?;
let checkpoint: RebaseCheckpoint = serde_json::from_str(&content).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to parse backup checkpoint: {e}"),
)
})?;
validate_checkpoint(&checkpoint)?;
let checkpoint_path = rebase_checkpoint_path();
fs::copy(backup, checkpoint_path)?;
Ok(Some(checkpoint))
}
pub fn save_rebase_checkpoint_with_workspace(
checkpoint: &RebaseCheckpoint,
workspace: &dyn Workspace,
) -> io::Result<()> {
let json = serde_json::to_string_pretty(checkpoint).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to serialize rebase checkpoint: {e}"),
)
})?;
let agent_dir = Path::new(AGENT_DIR);
let checkpoint_path = Path::new(AGENT_DIR).join(REBASE_CHECKPOINT_FILE);
let backup_path = Path::new(AGENT_DIR).join(format!("{REBASE_CHECKPOINT_FILE}.bak"));
workspace.create_dir_all(agent_dir)?;
let checkpoint_existed = workspace.exists(&checkpoint_path);
if checkpoint_existed {
let _ = backup_checkpoint_with_workspace(workspace);
}
workspace.write_atomic(&checkpoint_path, &json)?;
if !checkpoint_existed {
let _ = backup_checkpoint_with_workspace(workspace);
}
if workspace.exists(&backup_path) {
if let Ok(content) = workspace.read(&backup_path) {
if content.trim().is_empty() {
let _ = workspace.remove(&backup_path);
}
}
}
Ok(())
}
pub fn load_rebase_checkpoint_with_workspace(
workspace: &dyn Workspace,
) -> io::Result<Option<RebaseCheckpoint>> {
let checkpoint_path = Path::new(AGENT_DIR).join(REBASE_CHECKPOINT_FILE);
if !workspace.exists(&checkpoint_path) {
return Ok(None);
}
let content = workspace.read(&checkpoint_path)?;
let loaded_checkpoint: RebaseCheckpoint = match serde_json::from_str(&content) {
Ok(cp) => cp,
Err(e) => {
let backup_result = restore_from_backup_with_workspace(workspace);
return if backup_result.is_err() {
Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Checkpoint corrupted: {e}; backup restore failed: {}",
backup_result.unwrap_err()
),
))
} else {
backup_result
};
}
};
if let Err(e) = validate_checkpoint_impl(&loaded_checkpoint) {
let backup_result = restore_from_backup_with_workspace(workspace);
return if backup_result.is_err() {
Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"Checkpoint validation failed: {e}; backup restore failed: {}",
backup_result.unwrap_err()
),
))
} else {
backup_result
};
}
Ok(Some(loaded_checkpoint))
}
pub fn clear_rebase_checkpoint_with_workspace(workspace: &dyn Workspace) -> io::Result<()> {
let checkpoint_path = Path::new(AGENT_DIR).join(REBASE_CHECKPOINT_FILE);
if workspace.exists(&checkpoint_path) {
workspace.remove(&checkpoint_path)?;
}
Ok(())
}
pub fn rebase_checkpoint_exists_with_workspace(workspace: &dyn Workspace) -> bool {
let checkpoint_path = Path::new(AGENT_DIR).join(REBASE_CHECKPOINT_FILE);
workspace.exists(&checkpoint_path)
}
fn backup_checkpoint_with_workspace(workspace: &dyn Workspace) -> io::Result<()> {
let checkpoint_path = Path::new(AGENT_DIR).join(REBASE_CHECKPOINT_FILE);
let backup_path = Path::new(AGENT_DIR).join(format!("{REBASE_CHECKPOINT_FILE}.bak"));
if !workspace.exists(&checkpoint_path) {
return Ok(());
}
if workspace.exists(&backup_path) {
workspace.remove(&backup_path)?;
}
let content = workspace.read(&checkpoint_path)?;
workspace.write(&backup_path, &content)?;
Ok(())
}
fn restore_from_backup_with_workspace(
workspace: &dyn Workspace,
) -> io::Result<Option<RebaseCheckpoint>> {
let checkpoint_path = Path::new(AGENT_DIR).join(REBASE_CHECKPOINT_FILE);
let backup_path = Path::new(AGENT_DIR).join(format!("{REBASE_CHECKPOINT_FILE}.bak"));
if !workspace.exists(&backup_path) {
return Ok(None);
}
let content = workspace.read(&backup_path)?;
let checkpoint: RebaseCheckpoint = serde_json::from_str(&content).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to parse backup checkpoint: {e}"),
)
})?;
validate_checkpoint_impl(&checkpoint)?;
workspace.write(&checkpoint_path, &content)?;
Ok(Some(checkpoint))
}