diff options
Diffstat (limited to 'makima/src/daemon')
| -rw-r--r-- | makima/src/daemon/task/manager.rs | 338 | ||||
| -rw-r--r-- | makima/src/daemon/ws/protocol.rs | 17 |
2 files changed, 324 insertions, 31 deletions
diff --git a/makima/src/daemon/task/manager.rs b/makima/src/daemon/task/manager.rs index dd7df8a..acdf4ad 100644 --- a/makima/src/daemon/task/manager.rs +++ b/makima/src/daemon/task/manager.rs @@ -1925,6 +1925,10 @@ impl TaskManager { tracing::info!(task_id = %task_id, "Getting worktree info"); self.handle_get_worktree_info(task_id).await?; } + DaemonCommand::CommitWorktree { task_id, message } => { + tracing::info!(task_id = %task_id, "Committing worktree changes"); + self.handle_commit_worktree(task_id, message).await?; + } DaemonCommand::CreateCheckpoint { task_id, message, @@ -3322,6 +3326,96 @@ impl TaskManager { Ok(()) } + /// Handle CommitWorktree command - stage and commit changes in a task's worktree. + async fn handle_commit_worktree( + &self, + task_id: Uuid, + message: Option<String>, + ) -> Result<(), DaemonError> { + // Get task's worktree path + let worktree_path = { + let tasks = self.tasks.read().await; + tasks.get(&task_id) + .and_then(|t| t.worktree.as_ref()) + .map(|w| w.path.clone()) + }; + + let (success, commit_sha, error) = if let Some(path) = worktree_path { + // Step 1: Check if there are changes to commit + let status_output = tokio::process::Command::new("git") + .current_dir(&path) + .args(["status", "--porcelain"]) + .output() + .await; + + let has_changes = match &status_output { + Ok(output) => !output.stdout.is_empty(), + Err(_) => false, + }; + + if !has_changes { + (true, None, Some("No changes to commit".to_string())) + } else { + // Step 2: Stage all changes + let add_result = tokio::process::Command::new("git") + .current_dir(&path) + .args(["add", "-A"]) + .output() + .await; + + match add_result { + Ok(output) if output.status.success() => { + // Step 3: Commit + let commit_msg = message.unwrap_or_else(|| "Worktree commit".to_string()); + let commit_result = tokio::process::Command::new("git") + .current_dir(&path) + .args(["commit", "-m", &commit_msg]) + .output() + .await; + + match commit_result { + Ok(output) if output.status.success() => { + // Step 4: Get commit SHA + let sha_output = tokio::process::Command::new("git") + .current_dir(&path) + .args(["rev-parse", "HEAD"]) + .output() + .await; + + let sha = sha_output.ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string()); + + (true, sha, None) + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + (false, None, Some(format!("Git commit failed: {}", stderr))) + } + Err(e) => (false, None, Some(format!("Failed to run git commit: {}", e))), + } + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr).to_string(); + (false, None, Some(format!("Failed to stage changes: {}", stderr))) + } + Err(e) => (false, None, Some(format!("Failed to run git add: {}", e))), + } + } + } else { + (false, None, Some(format!("Task {} not found or has no worktree", task_id))) + }; + + let msg = DaemonMessage::WorktreeCommitResult { + task_id, + success, + commit_sha, + error, + }; + let _ = self.ws_tx.send(msg).await; + Ok(()) + } + /// Handle GetWorktreeInfo command - get worktree files, stats, branch info. async fn handle_get_worktree_info( &self, @@ -3451,35 +3545,57 @@ impl TaskManager { }; if let Some(ref base) = effective_base_branch { - // Get committed changes using git diff --name-status - let diff_base = format!("origin/{}...HEAD", base); - let name_status_output = tokio::process::Command::new("git") - .current_dir(&path) - .args(["diff", "--name-status", &diff_base]) - .output() - .await; + // Resolve the best diff base reference, handling missing remote refs + let resolved_diff_base = Self::resolve_diff_base(&path, base).await; + + if let Some(ref diff_base) = resolved_diff_base { + // Get committed changes using git diff --name-status + let name_status_output = tokio::process::Command::new("git") + .current_dir(&path) + .args(["diff", "--name-status", diff_base]) + .output() + .await; + + let committed_status_lines: Vec<(String, String)> = match name_status_output { + Ok(output) if output.status.success() => { + String::from_utf8_lossy(&output.stdout) + .lines() + .filter_map(|line| { + let parts: Vec<&str> = line.splitn(2, '\t').collect(); + if parts.len() >= 2 { + let status = parts[0].trim().to_string(); + let file_path = parts[1].to_string(); + Some((file_path, status)) + } else { + None + } + }) + .collect() + } + Ok(output) => { + let stderr = String::from_utf8_lossy(&output.stderr); + tracing::warn!( + diff_base = %diff_base, + stderr = %stderr, + "git diff --name-status failed with resolved diff base", + ); + vec![] + } + Err(e) => { + tracing::warn!( + error = %e, + diff_base = %diff_base, + "Failed to execute git diff --name-status", + ); + vec![] + } + }; - let committed_status_lines: Vec<(String, String)> = match name_status_output { - Ok(output) if output.status.success() => { - String::from_utf8_lossy(&output.stdout) - .lines() - .filter_map(|line| { - let parts: Vec<&str> = line.splitn(2, '\t').collect(); - if parts.len() >= 2 { - let status = parts[0].trim().to_string(); - let file_path = parts[1].to_string(); - Some((file_path, status)) - } else { - None - } - }) - .collect() + if !committed_status_lines.is_empty() { + (committed_status_lines, resolved_diff_base) + } else { + (vec![], None) } - _ => vec![], - }; - - if !committed_status_lines.is_empty() { - (committed_status_lines, Some(base.clone())) } else { (vec![], None) } @@ -3489,15 +3605,14 @@ impl TaskManager { }; // Get numstat for line counts - // If we have effective_base_for_diff, compare against origin/{base_branch} + // If we have effective_base_for_diff (a resolved diff base string), use it directly // Otherwise compare against HEAD for uncommitted changes let mut file_stats: std::collections::HashMap<String, (i32, i32)> = std::collections::HashMap::new(); - let numstat_output = if let Some(ref base) = effective_base_for_diff { - let diff_base = format!("origin/{}...HEAD", base); + let numstat_output = if let Some(ref diff_base) = effective_base_for_diff { tokio::process::Command::new("git") .current_dir(&path) - .args(["diff", "--numstat", &diff_base]) + .args(["diff", "--numstat", diff_base]) .output() .await } else { @@ -3557,6 +3672,167 @@ impl TaskManager { Ok(()) } + /// Handle GetWorktreeDiff command - get git diff for a task's worktree. + async fn handle_get_worktree_diff( + &self, + task_id: Uuid, + file_path: Option<String>, + ) -> Result<(), DaemonError> { + // Get task's worktree path, branch, and base_branch + // If the task shares a supervisor's worktree, use the supervisor's worktree info + let task_info = { + let tasks = self.tasks.read().await; + if let Some(task) = tasks.get(&task_id) { + if let Some(supervisor_task_id) = task.supervisor_worktree_task_id { + tasks.get(&supervisor_task_id).map(|supervisor| ( + supervisor.worktree.as_ref().map(|w| w.path.clone()), + supervisor.base_branch.clone(), + )) + } else { + Some(( + task.worktree.as_ref().map(|w| w.path.clone()), + task.base_branch.clone(), + )) + } + } else { + None + } + }; + + let (worktree_path, base_branch) = match task_info { + Some((Some(path), base_branch)) => (path, base_branch), + _ => { + let msg = DaemonMessage::WorktreeDiffResult { + task_id, + success: true, + diff: Some(String::new()), + error: None, + }; + let _ = self.ws_tx.send(msg).await; + return Ok(()); + } + }; + + if !worktree_path.exists() { + let msg = DaemonMessage::WorktreeDiffResult { + task_id, + success: false, + diff: None, + error: Some("Worktree path does not exist".to_string()), + }; + let _ = self.ws_tx.send(msg).await; + return Ok(()); + } + + // Check for uncommitted changes first + let status_output = tokio::process::Command::new("git") + .current_dir(&worktree_path) + .args(["status", "--porcelain"]) + .output() + .await; + + let has_uncommitted = match &status_output { + Ok(output) if output.status.success() => { + !String::from_utf8_lossy(&output.stdout).trim().is_empty() + } + _ => false, + }; + + let diff_result = if has_uncommitted { + // Get diff for uncommitted changes (both staged and unstaged) + let mut args = vec!["diff".to_string(), "HEAD".to_string()]; + if let Some(ref fp) = file_path { + args.push("--".to_string()); + args.push(fp.clone()); + } + let output = tokio::process::Command::new("git") + .current_dir(&worktree_path) + .args(&args) + .output() + .await; + + match output { + Ok(out) if out.status.success() => { + let diff = String::from_utf8_lossy(&out.stdout).to_string(); + // If diff is empty (e.g., for new untracked files), try git diff (no HEAD) + // and also try to show untracked file content + if diff.is_empty() { + // Try to show untracked files as diffs + let mut args2 = vec!["diff".to_string()]; + if let Some(ref fp) = file_path { + args2.push("--".to_string()); + args2.push(fp.clone()); + } + let output2 = tokio::process::Command::new("git") + .current_dir(&worktree_path) + .args(&args2) + .output() + .await; + match output2 { + Ok(out2) if out2.status.success() => { + Ok(String::from_utf8_lossy(&out2.stdout).to_string()) + } + _ => Ok(diff), + } + } else { + Ok(diff) + } + } + Ok(out) => Err(String::from_utf8_lossy(&out.stderr).to_string()), + Err(e) => Err(format!("Failed to run git diff: {}", e)), + } + } else { + // No uncommitted changes - compare against base branch + let effective_base_branch = if let Some(ref base) = base_branch { + Some(base.clone()) + } else { + self.worktree_manager.detect_default_branch(&worktree_path).await.ok() + }; + + if let Some(ref base) = effective_base_branch { + let diff_base = format!("origin/{}...HEAD", base); + let mut args = vec!["diff".to_string(), diff_base]; + if let Some(ref fp) = file_path { + args.push("--".to_string()); + args.push(fp.clone()); + } + let output = tokio::process::Command::new("git") + .current_dir(&worktree_path) + .args(&args) + .output() + .await; + + match output { + Ok(out) if out.status.success() => { + Ok(String::from_utf8_lossy(&out.stdout).to_string()) + } + Ok(out) => Err(String::from_utf8_lossy(&out.stderr).to_string()), + Err(e) => Err(format!("Failed to run git diff: {}", e)), + } + } else { + Ok(String::new()) + } + }; + + let msg = match diff_result { + Ok(diff) => DaemonMessage::WorktreeDiffResult { + task_id, + success: true, + diff: Some(diff), + error: None, + }, + Err(e) => DaemonMessage::WorktreeDiffResult { + task_id, + success: false, + diff: None, + error: Some(e), + }, + }; + + let _ = self.ws_tx.send(msg).await; + Ok(()) + } + /// Handle CreateCheckpoint command - stage all changes, commit, and get stats. async fn handle_create_checkpoint( &self, diff --git a/makima/src/daemon/ws/protocol.rs b/makima/src/daemon/ws/protocol.rs index 1611f52..0583783 100644 --- a/makima/src/daemon/ws/protocol.rs +++ b/makima/src/daemon/ws/protocol.rs @@ -310,6 +310,16 @@ pub enum DaemonMessage { error: Option<String>, }, + /// Response to CommitWorktree command. + WorktreeCommitResult { + #[serde(rename = "taskId")] + task_id: Uuid, + success: bool, + #[serde(rename = "commitSha")] + commit_sha: Option<String>, + error: Option<String>, + }, + /// Response to GetWorktreeInfo command. WorktreeInfoResult { #[serde(rename = "taskId")] @@ -758,6 +768,13 @@ pub enum DaemonCommand { task_id: Uuid, }, + /// Commit changes in a task worktree. + CommitWorktree { + #[serde(rename = "taskId")] + task_id: Uuid, + message: Option<String>, + }, + /// Create a checkpoint (stage changes, commit, get stats). CreateCheckpoint { #[serde(rename = "taskId")] |
