summaryrefslogtreecommitdiff
path: root/makima/src/daemon/task/manager.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/daemon/task/manager.rs')
-rw-r--r--makima/src/daemon/task/manager.rs338
1 files changed, 307 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,