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    #[allow(dead_code)]
121    tag: Option<String>,
122}
123
124fn get_task_context(storage: &Storage, task_id: &str) -> Option<TaskContext> {
125    // Try to find the task in the active phase first
126    if let Ok(Some(tag)) = storage.get_active_group() {
127        if let Ok(phase) = storage.load_group(&tag) {
128            if let Some(task) = phase.tasks.iter().find(|t| t.id == task_id) {
129                return Some(TaskContext {
130                    title: task.title.clone(),
131                    tag: Some(tag),
132                });
133            }
134        }
135    }
136
137    // Search all phases
138    if let Ok(all_tasks) = storage.load_tasks() {
139        for (tag, phase) in all_tasks {
140            if let Some(task) = phase.tasks.iter().find(|t| t.id == task_id) {
141                return Some(TaskContext {
142                    title: task.title.clone(),
143                    tag: Some(tag),
144                });
145            }
146        }
147    }
148
149    None
150}
151
152fn build_commit_message(
153    user_message: Option<&str>,
154    task_id: Option<&str>,
155    task_context: Option<&TaskContext>,
156) -> Result<String> {
157    let mut message = String::new();
158
159    // Add task prefix if available
160    if let Some(id) = task_id {
161        message.push_str(&format!("[{}] ", id));
162    }
163
164    // Add user message or task title
165    if let Some(msg) = user_message {
166        message.push_str(msg);
167    } else if let Some(ctx) = task_context {
168        // Use task title as default message
169        message.push_str(&ctx.title);
170    } else {
171        anyhow::bail!("No commit message provided and no task context available.\nUse: scud commit -m \"your message\"");
172    }
173
174    Ok(message)
175}