diff options
| author | soryu <soryu@soryu.co> | 2026-01-15 17:59:37 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-15 17:59:37 +0000 |
| commit | 11c78ade600a2d74b8f033f18045a0c28fac4362 (patch) | |
| tree | 19a62408769292cefd2f990f9fd8d9fff43becdf /makima/src/daemon/task | |
| parent | 3efdab36ca61a6795454668881d5b925abe22bd3 (diff) | |
| download | soryu-11c78ade600a2d74b8f033f18045a0c28fac4362.tar.gz soryu-11c78ade600a2d74b8f033f18045a0c28fac4362.zip | |
Implement simple git checkpoint command for supervisor
Diffstat (limited to 'makima/src/daemon/task')
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 239 |
1 files changed, 239 insertions, 0 deletions
diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index 75c884b..427a9d1 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -1431,6 +1431,13 @@ impl TaskManager { tracing::info!(task_id = %task_id, "Getting task diff"); self.handle_get_task_diff(task_id).await?; } + DaemonCommand::CreateCheckpoint { + task_id, + message, + } => { + tracing::info!(task_id = %task_id, "Creating checkpoint"); + self.handle_create_checkpoint(task_id, message).await?; + } DaemonCommand::CleanupWorktree { task_id, delete_branch, @@ -2498,6 +2505,238 @@ impl TaskManager { let _ = self.ws_tx.send(msg).await; Ok(()) } + + /// Handle CreateCheckpoint command - stage all changes, commit, and get stats. + async fn handle_create_checkpoint( + &self, + task_id: Uuid, + message: String, + ) -> Result<(), DaemonError> { + // Get task's worktree path and branch name + let task_info = { + let tasks = self.tasks.read().await; + tasks.get(&task_id).map(|t| ( + t.worktree.as_ref().map(|w| w.path.clone()), + t.worktree.as_ref().map(|w| w.branch.clone()), + )) + }; + + let (worktree_path, branch_name) = match task_info { + Some((Some(path), Some(branch))) => (path, branch), + Some((Some(path), None)) => { + // Try to get current branch from git + let branch = self.get_current_branch(&path).await.unwrap_or_else(|| "unknown".to_string()); + (path, branch) + } + _ => { + let msg = DaemonMessage::CheckpointCreated { + task_id, + success: false, + commit_sha: None, + branch_name: None, + checkpoint_number: None, + files_changed: None, + lines_added: None, + lines_removed: None, + error: Some(format!("Task {} not found or has no worktree", task_id)), + message, + }; + let _ = self.ws_tx.send(msg).await; + return Ok(()); + } + }; + + // Step 1: Check if there are changes to commit + let status_output = tokio::process::Command::new("git") + .current_dir(&worktree_path) + .args(["status", "--porcelain"]) + .output() + .await; + + let has_changes = match &status_output { + Ok(output) => !output.stdout.is_empty(), + Err(_) => false, + }; + + if !has_changes { + let msg = DaemonMessage::CheckpointCreated { + task_id, + success: false, + commit_sha: None, + branch_name: Some(branch_name), + checkpoint_number: None, + files_changed: None, + lines_added: None, + lines_removed: None, + error: Some("No changes to checkpoint".to_string()), + message, + }; + let _ = self.ws_tx.send(msg).await; + return Ok(()); + } + + // Step 2: Stage all changes + let add_result = tokio::process::Command::new("git") + .current_dir(&worktree_path) + .args(["add", "-A"]) + .output() + .await; + + if let Err(e) = add_result { + let msg = DaemonMessage::CheckpointCreated { + task_id, + success: false, + commit_sha: None, + branch_name: Some(branch_name), + checkpoint_number: None, + files_changed: None, + lines_added: None, + lines_removed: None, + error: Some(format!("Failed to stage changes: {}", e)), + message, + }; + let _ = self.ws_tx.send(msg).await; + return Ok(()); + } + + // Step 3: Get diff stats before commit + let (lines_added, lines_removed, files_changed) = self.get_staged_diff_stats(&worktree_path).await; + + // Step 4: Create commit + let commit_result = tokio::process::Command::new("git") + .current_dir(&worktree_path) + .args(["commit", "-m", &message]) + .output() + .await; + + let commit_sha = match commit_result { + Ok(output) if output.status.success() => { + // Get the commit SHA + let sha_output = tokio::process::Command::new("git") + .current_dir(&worktree_path) + .args(["rev-parse", "HEAD"]) + .output() + .await; + + match sha_output { + Ok(o) => Some(String::from_utf8_lossy(&o.stdout).trim().to_string()), + Err(_) => None, + } + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + let msg = DaemonMessage::CheckpointCreated { + task_id, + success: false, + commit_sha: None, + branch_name: Some(branch_name), + checkpoint_number: None, + files_changed: Some(files_changed), + lines_added: Some(lines_added), + lines_removed: Some(lines_removed), + error: Some(format!("Commit failed: {}", stderr)), + message, + }; + let _ = self.ws_tx.send(msg).await; + return Ok(()); + } + Err(e) => { + let msg = DaemonMessage::CheckpointCreated { + task_id, + success: false, + commit_sha: None, + branch_name: Some(branch_name), + checkpoint_number: None, + files_changed: None, + lines_added: None, + lines_removed: None, + error: Some(format!("Failed to execute git commit: {}", e)), + message, + }; + let _ = self.ws_tx.send(msg).await; + return Ok(()); + } + }; + + // Success - send response (checkpoint_number will be assigned by server on DB insert) + let msg = DaemonMessage::CheckpointCreated { + task_id, + success: true, + commit_sha, + branch_name: Some(branch_name), + checkpoint_number: None, // Server will assign from DB + files_changed: Some(files_changed), + lines_added: Some(lines_added), + lines_removed: Some(lines_removed), + error: None, + message, + }; + let _ = self.ws_tx.send(msg).await; + Ok(()) + } + + /// Get the current branch name from a worktree. + async fn get_current_branch(&self, worktree_path: &std::path::PathBuf) -> Option<String> { + let output = tokio::process::Command::new("git") + .current_dir(worktree_path) + .args(["branch", "--show-current"]) + .output() + .await + .ok()?; + + if output.status.success() { + Some(String::from_utf8_lossy(&output.stdout).trim().to_string()) + } else { + None + } + } + + /// Get diff stats for staged changes. + async fn get_staged_diff_stats(&self, worktree_path: &std::path::PathBuf) -> (i32, i32, serde_json::Value) { + // Get numstat for lines added/removed + let numstat = tokio::process::Command::new("git") + .current_dir(worktree_path) + .args(["diff", "--cached", "--numstat"]) + .output() + .await; + + let (mut total_added, mut total_removed) = (0i32, 0i32); + if let Ok(output) = numstat { + for line in String::from_utf8_lossy(&output.stdout).lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + if let Ok(added) = parts[0].parse::<i32>() { + total_added += added; + } + if let Ok(removed) = parts[1].parse::<i32>() { + total_removed += removed; + } + } + } + } + + // Get name-status for file changes + let name_status = tokio::process::Command::new("git") + .current_dir(worktree_path) + .args(["diff", "--cached", "--name-status"]) + .output() + .await; + + let mut files = Vec::new(); + if let Ok(output) = name_status { + for line in String::from_utf8_lossy(&output.stdout).lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + files.push(serde_json::json!({ + "action": parts[0], + "path": parts[1] + })); + } + } + } + + (total_added, total_removed, serde_json::json!(files)) + } } /// Inner state for spawned tasks (cloneable). |
