use crate::cli::handlers::CommandCtx;
use crate::cli::TodoCommands;
use crate::models::{TaskRepo, TodoAction, TodoStatus};
use crate::services::{RepoService, TaskService, TodoService, WorktreeService};
use crate::use_cases::ApplyTodoActionUseCase;
use crate::utils::{Result, TrackError};
use prettytable::{format, Cell, Row, Table};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
pub fn handle_todo(ctx: &CommandCtx, command: TodoCommands) -> Result<()> {
let current_task_id = ctx
.db
.get_current_task_id()?
.ok_or(TrackError::NoActiveTask)?;
let todo_service = TodoService::new(ctx.db);
match command {
TodoCommands::Add { text, worktree } => {
let todo = todo_service.add_todo(current_task_id, &text, worktree)?;
println!("Added TODO #{}: {}", todo.task_index, todo.content);
if worktree {
println!("Workspace creation scheduled for 'track sync'");
}
}
TodoCommands::List => {
let todos = todo_service.list_todos(current_task_id)?;
let mut table = Table::new();
table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE);
table.set_titles(Row::new(vec![
Cell::new("ID"),
Cell::new("Status"),
Cell::new("Content"),
]));
for todo in todos {
table.add_row(Row::new(vec![
Cell::new(&todo.task_index.to_string()),
Cell::new(todo.status.as_str()),
Cell::new(&todo.content),
]));
}
table.printstd();
}
TodoCommands::Update { id, status } => {
let todo = todo_service.get_todo_by_index(current_task_id, id)?;
if status == TodoStatus::PENDING {
todo_service.update_status(todo.id, &status)?;
} else {
let action = TodoAction::from_cli_update_status(&status)?;
ApplyTodoActionUseCase::new(ctx.db).execute(current_task_id, id, action)?;
}
println!("Updated TODO #{} status to '{}'", id, status);
}
TodoCommands::Done { id } => {
let outcome = ApplyTodoActionUseCase::new(ctx.db)
.execute(current_task_id, id, TodoAction::Complete)?
.expect("complete action returns outcome");
if let Some(branch) = outcome.merged_bookmark {
println!(
"Rebased and removed workspace for TODO #{} (bookmark: {}).",
id, branch
);
}
println!("Marked TODO #{} as done.", id);
}
TodoCommands::Workspace {
id,
recreate,
force,
all,
} => {
let todo = todo_service.get_todo_by_index(current_task_id, id)?;
let worktree_service = WorktreeService::new(ctx.db);
let repo_service = RepoService::new(ctx.db);
let repos = repo_service.list_repos(current_task_id)?;
if repos.is_empty() {
return Err(TrackError::Other(
"No repositories registered for this task.".to_string(),
));
}
let target_repos = if all {
repos
} else {
let current_path = std::env::current_dir()
.and_then(|path| path.canonicalize())
.map_err(|e| {
TrackError::Other(format!("Failed to resolve current path: {}", e))
})?;
let mut matching: Vec<(TaskRepo, PathBuf)> = repos
.into_iter()
.filter_map(|repo| {
let repo_path = PathBuf::from(&repo.repo_path);
let repo_path = repo_path.canonicalize().ok()?;
if current_path.starts_with(&repo_path) {
Some((repo, repo_path))
} else {
None
}
})
.collect();
matching.sort_by_key(|(_, repo_path)| repo_path.to_string_lossy().len());
let repo = matching.pop().map(|(repo, _)| repo).ok_or_else(|| {
TrackError::Other(
"Current directory is not a registered repo for this task.".to_string(),
)
})?;
vec![repo]
};
let worktrees = worktree_service.list_worktrees(current_task_id)?;
let task_service = TaskService::new(ctx.db);
let task = task_service.get_task(current_task_id)?;
let branch_name = worktree_service.get_todo_branch_name(
current_task_id,
task.ticket_id.as_deref(),
todo.task_index,
)?;
let mut output_paths: Vec<String> = Vec::new();
for repo in target_repos {
let mut todo_worktrees: Vec<_> = worktrees
.iter()
.filter(|wt| {
wt.todo_id == Some(todo.id)
&& wt.base_repo.as_deref() == Some(repo.repo_path.as_str())
})
.cloned()
.collect();
if !todo_worktrees.is_empty() {
if !recreate {
output_paths
.extend(todo_worktrees.iter().map(|worktree| worktree.path.clone()));
continue;
}
if !force {
for worktree in &todo_worktrees {
if Path::new(&worktree.path).exists()
&& worktree_service.has_uncommitted_changes(&worktree.path)?
{
return Err(TrackError::Other(format!(
"Workspace {} has uncommitted changes. Use --force to recreate.",
worktree.path
)));
}
}
}
for worktree in todo_worktrees.drain(..) {
if !worktree_service
.bookmark_exists_in_repo(repo.repo_path.as_str(), &worktree.branch)?
{
if Path::new(&worktree.path).exists() {
output_paths.push(worktree.path.clone());
}
if all {
eprintln!(
"Skipping recreate for {} (missing branch/bookmark).",
worktree.path
);
continue;
}
return Err(TrackError::Other(format!(
"Bookmark {} not found in {}.",
worktree.branch, repo.repo_path
)));
}
let recreated = worktree_service.recreate_worktree(&worktree, force)?;
output_paths.push(recreated.path);
}
} else {
if recreate
&& !worktree_service
.bookmark_exists_in_repo(repo.repo_path.as_str(), &branch_name)?
{
if all {
eprintln!(
"Skipping create for {} (missing branch/bookmark).",
repo.repo_path
);
continue;
}
return Err(TrackError::Other(format!(
"Bookmark {} not found in {}.",
branch_name, repo.repo_path
)));
}
let created = match worktree_service.add_worktree(
current_task_id,
&repo.repo_path,
None,
task.ticket_id.as_deref(),
Some(todo.id),
false,
) {
Ok(worktree) => worktree,
Err(TrackError::BookmarkExists(_)) => worktree_service
.add_existing_worktree(
current_task_id,
&repo.repo_path,
&branch_name,
Some(todo.id),
false,
None,
)?,
Err(e) => return Err(e),
};
output_paths.push(created.path);
}
}
if output_paths.is_empty() {
return Err(TrackError::Other(
"No workspace paths available for this TODO.".to_string(),
));
}
if all {
for path in output_paths {
println!("{}", path);
}
} else {
println!("{}", output_paths[0]);
if output_paths.len() > 1 {
eprintln!(
"Multiple workspaces exist for TODO #{}. Using first path.",
id
);
}
}
}
TodoCommands::Delete { id, force } => {
let todo = todo_service.get_todo_by_index(current_task_id, id)?;
if !force {
print!("Delete TODO #{}: \"{}\"? [y/N]: ", id, todo.content);
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
if !matches!(input.trim().to_lowercase().as_str(), "y" | "yes") {
println!("Cancelled.");
return Ok(());
}
}
todo_service.delete_todo(todo.id)?;
println!("Deleted TODO #{}", id);
}
TodoCommands::Next { id } => {
todo_service.move_to_next(current_task_id, id)?;
println!("Moved TODO #{} to the front (next todo to work on)", id);
}
}
Ok(())
}