scud/commands/
commit.rs

1use anyhow::{Context, Result};
2use colored::Colorize;
3use std::path::PathBuf;
4use std::process::Command;
5
6use crate::storage::Storage;
7
8pub fn run(project_root: Option<PathBuf>, message: Option<&str>, all: bool) -> Result<()> {
9    let storage = Storage::new(project_root.clone());
10
11    // Get current task ID from environment or .scud/current-task
12    let task_id = get_current_task_id(&storage)?;
13
14    // Get task details if we have a task ID
15    let task_context = if let Some(ref id) = task_id {
16        get_task_context(&storage, id)
17    } else {
18        None
19    };
20
21    // Build commit message
22    let commit_message = build_commit_message(message, task_id.as_deref(), task_context.as_ref())?;
23
24    // Show what we're about to do
25    println!("{}", "SCUD Commit".cyan().bold());
26    println!("{}", "-".repeat(40).dimmed());
27
28    if let Some(ref id) = task_id {
29        println!("Task: {}", id.cyan());
30    }
31    println!("Message: {}", commit_message.lines().next().unwrap_or(""));
32
33    // Stage files if --all
34    if all {
35        println!("\n{}", "Staging all changes...".dimmed());
36        let status = Command::new("git")
37            .args(["add", "-A"])
38            .status()
39            .context("Failed to run git add")?;
40
41        if !status.success() {
42            anyhow::bail!("git add failed");
43        }
44    }
45
46    // Check if there are staged changes
47    let staged = Command::new("git")
48        .args(["diff", "--cached", "--quiet"])
49        .status()
50        .context("Failed to check staged changes")?;
51
52    if staged.success() {
53        println!("\n{}", "No staged changes to commit.".yellow());
54        println!(
55            "Use {} to stage changes, or {} to stage all.",
56            "git add <files>".cyan(),
57            "scud commit --all".cyan()
58        );
59        return Ok(());
60    }
61
62    // Show staged files
63    println!("\n{}", "Staged files:".bold());
64    let staged_output = Command::new("git")
65        .args(["diff", "--cached", "--name-status"])
66        .output()
67        .context("Failed to get staged files")?;
68
69    for line in String::from_utf8_lossy(&staged_output.stdout).lines() {
70        println!("  {}", line.dimmed());
71    }
72
73    // Create commit
74    println!("\n{}", "Creating commit...".dimmed());
75    let status = Command::new("git")
76        .args(["commit", "-m", &commit_message])
77        .status()
78        .context("Failed to run git commit")?;
79
80    if !status.success() {
81        anyhow::bail!("git commit failed");
82    }
83
84    println!("\n{} Commit created successfully", "✓".green());
85
86    // Show the commit
87    let log = Command::new("git")
88        .args(["log", "-1", "--oneline"])
89        .output()
90        .context("Failed to get commit info")?;
91
92    println!("  {}", String::from_utf8_lossy(&log.stdout).trim().dimmed());
93
94    Ok(())
95}
96
97fn get_current_task_id(storage: &Storage) -> Result<Option<String>> {
98    // First check environment variable
99    if let Ok(id) = std::env::var("SCUD_TASK_ID") {
100        if !id.is_empty() {
101            return Ok(Some(id));
102        }
103    }
104
105    // Then check .scud/current-task file
106    let current_task_file = storage.scud_dir().join("current-task");
107    if current_task_file.exists() {
108        let content = std::fs::read_to_string(&current_task_file)?;
109        let id = content.trim();
110        if !id.is_empty() {
111            return Ok(Some(id.to_string()));
112        }
113    }
114
115    Ok(None)
116}
117
118struct TaskContext {
119    title: String,
120}
121
122fn get_task_context(storage: &Storage, task_id: &str) -> Option<TaskContext> {
123    // Try to find the task in the active phase first
124    if let Ok(Some(tag)) = storage.get_active_group() {
125        if let Ok(phase) = storage.load_group(&tag) {
126            if let Some(task) = phase.tasks.iter().find(|t| t.id == task_id) {
127                return Some(TaskContext {
128                    title: task.title.clone(),
129                });
130            }
131        }
132    }
133
134    // Search all phases
135    if let Ok(all_tasks) = storage.load_tasks() {
136        for (_tag, phase) in all_tasks {
137            if let Some(task) = phase.tasks.iter().find(|t| t.id == task_id) {
138                return Some(TaskContext {
139                    title: task.title.clone(),
140                });
141            }
142        }
143    }
144
145    None
146}
147
148fn build_commit_message(
149    user_message: Option<&str>,
150    task_id: Option<&str>,
151    task_context: Option<&TaskContext>,
152) -> Result<String> {
153    let mut message = String::new();
154
155    // Add task prefix if available
156    if let Some(id) = task_id {
157        message.push_str(&format!("[{}] ", id));
158    }
159
160    // Add user message or task title
161    if let Some(msg) = user_message {
162        message.push_str(msg);
163    } else if let Some(ctx) = task_context {
164        // Use task title as default message
165        message.push_str(&ctx.title);
166    } else {
167        anyhow::bail!("No commit message provided and no task context available.\nUse: scud commit -m \"your message\"");
168    }
169
170    Ok(message)
171}