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 let task_id = get_current_task_id(&storage)?;
13
14 let task_context = if let Some(ref id) = task_id {
16 get_task_context(&storage, id)
17 } else {
18 None
19 };
20
21 let commit_message = build_commit_message(message, task_id.as_deref(), task_context.as_ref())?;
23
24 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 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 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 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 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 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 if let Ok(id) = std::env::var("SCUD_TASK_ID") {
100 if !id.is_empty() {
101 return Ok(Some(id));
102 }
103 }
104
105 let current_task_file = storage.scud_dir().join("current-task");
107 if current_task_file.exists() {
108 let content = std::fs::read_to_string(¤t_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 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 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 if let Some(id) = task_id {
157 message.push_str(&format!("[{}] ", id));
158 }
159
160 if let Some(msg) = user_message {
162 message.push_str(msg);
163 } else if let Some(ctx) = task_context {
164 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}