From 1ed362424dafec690f919154f5116471951cda9c Mon Sep 17 00:00:00 2001 From: soryu Date: Thu, 22 Jan 2026 22:32:46 +0000 Subject: Add patch checkpointing --- makima/src/daemon/worktree/manager.rs | 135 ++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) (limited to 'makima/src/daemon/worktree') diff --git a/makima/src/daemon/worktree/manager.rs b/makima/src/daemon/worktree/manager.rs index 5edd7b1..04cb307 100644 --- a/makima/src/daemon/worktree/manager.rs +++ b/makima/src/daemon/worktree/manager.rs @@ -1697,6 +1697,141 @@ impl WorktreeManager { pub async fn target_directory_exists(&self, target_dir: &Path) -> bool { target_dir.exists() } + + /// Restore a worktree from a stored patch. + /// + /// This is used for task recovery when the local worktree has been lost. + /// 1. Clone/fetch the source repo to get the base commit + /// 2. Create a new worktree at the base commit + /// 3. Apply the patch to restore the task's state + pub async fn restore_from_patch( + &self, + source_repo: &str, + task_id: Uuid, + task_name: &str, + base_commit_sha: &str, + patch_data: &[u8], + ) -> Result { + use crate::daemon::storage; + + // Generate directory and branch names + let dir_name = format!("{}-{}", short_uuid(task_id), sanitize_name(task_name)); + let worktree_path = self.base_dir.join(&dir_name); + let branch_name = format!( + "{}{}-{}", + self.branch_prefix, + sanitize_name(task_name), + short_uuid(task_id) + ); + + // Ensure base directory exists + tokio::fs::create_dir_all(&self.base_dir).await?; + + // Remove existing worktree if present (we're restoring from scratch) + if worktree_path.exists() { + tracing::info!( + task_id = %task_id, + worktree_path = %worktree_path.display(), + "Removing existing worktree before restore" + ); + tokio::fs::remove_dir_all(&worktree_path).await?; + } + + // Clone the source repo if needed + let repo_path = self.ensure_repo(source_repo).await?; + + // Create worktree at the base commit + // First, we need to make sure the base commit is available + let fetch_output = Command::new("git") + .args(["fetch", "--all"]) + .current_dir(&repo_path) + .output() + .await?; + + if !fetch_output.status.success() { + tracing::warn!( + task_id = %task_id, + stderr = %String::from_utf8_lossy(&fetch_output.stderr), + "git fetch failed, continuing anyway" + ); + } + + // Create the worktree from the base commit + let output = Command::new("git") + .args([ + "worktree", + "add", + "-b", + &branch_name, + worktree_path.to_str().ok_or_else(|| { + WorktreeError::InvalidPath("Invalid worktree path".to_string()) + })?, + base_commit_sha, + ]) + .current_dir(&repo_path) + .output() + .await?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + // If branch already exists, try without -b flag + if stderr.contains("already exists") { + // Remove the branch and try again + let _ = Command::new("git") + .args(["branch", "-D", &branch_name]) + .current_dir(&repo_path) + .output() + .await; + + let retry_output = Command::new("git") + .args([ + "worktree", + "add", + "-b", + &branch_name, + worktree_path.to_str().unwrap(), + base_commit_sha, + ]) + .current_dir(&repo_path) + .output() + .await?; + + if !retry_output.status.success() { + return Err(WorktreeError::GitCommand(format!( + "Failed to create worktree after retry: {}", + String::from_utf8_lossy(&retry_output.stderr) + ))); + } + } else { + return Err(WorktreeError::GitCommand(format!( + "Failed to create worktree: {}", + stderr + ))); + } + } + + // Apply the patch to restore the task's state + if let Err(e) = storage::apply_patch(&worktree_path, patch_data).await { + tracing::error!( + task_id = %task_id, + error = %e, + "Failed to apply patch, worktree is at base commit" + ); + // Don't fail - the worktree is usable, just at the base commit + } else { + tracing::info!( + task_id = %task_id, + worktree_path = %worktree_path.display(), + "Successfully restored worktree from patch" + ); + } + + Ok(WorktreeInfo { + path: worktree_path, + branch: branch_name, + source_repo: repo_path, + }) + } } /// Check if repo_source is a "new repo" request. -- cgit v1.2.3