Skip to main content

track/use_cases/
archive_task.rs

1use crate::db::Database;
2use crate::models::Task;
3use crate::services::{TaskService, WorktreeService};
4use crate::utils::{Result, TrackError};
5
6/// A workspace with uncommitted JJ changes blocking archive.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct DirtyWorkspace {
9    pub id: i64,
10    pub path: String,
11}
12
13/// Result of archiving a task and cleaning up workspaces.
14#[derive(Debug, Clone)]
15pub struct ArchiveTaskOutcome {
16    pub task: Task,
17    pub removed_workspaces: Vec<(i64, String)>,
18    pub workspace_errors: Vec<String>,
19}
20
21/// Archives a task after optionally removing JJ workspaces.
22pub struct ArchiveTaskUseCase<'a> {
23    db: &'a Database,
24}
25
26impl<'a> ArchiveTaskUseCase<'a> {
27    pub fn new(db: &'a Database) -> Self {
28        Self { db }
29    }
30
31    pub fn resolve_task_id(&self, task_ref: Option<&str>) -> Result<i64> {
32        let task_service = TaskService::new(self.db);
33        match task_ref {
34            Some(r) => task_service.resolve_task_id(r),
35            None => self
36                .db
37                .get_current_task_id()?
38                .ok_or(TrackError::NoActiveTask),
39        }
40    }
41
42    pub fn find_dirty_workspaces(&self, task_id: i64) -> Result<Vec<DirtyWorkspace>> {
43        let worktree_service = WorktreeService::new(self.db);
44        let worktrees = worktree_service.list_worktrees(task_id)?;
45
46        Ok(worktrees
47            .into_iter()
48            .filter(|worktree| {
49                std::path::Path::new(&worktree.path).exists()
50                    && worktree_service
51                        .has_uncommitted_changes(&worktree.path)
52                        .unwrap_or(false)
53            })
54            .map(|worktree| DirtyWorkspace {
55                id: worktree.id,
56                path: worktree.path,
57            })
58            .collect())
59    }
60
61    /// Removes workspaces and archives the task.
62    ///
63    /// When `allow_dirty` is false and dirty workspaces exist, returns
64    /// [`TrackError::UncommittedWorkspaces`] so the caller can prompt the user.
65    pub fn execute(&self, task_id: i64, allow_dirty: bool) -> Result<ArchiveTaskOutcome> {
66        let task_service = TaskService::new(self.db);
67        let worktree_service = WorktreeService::new(self.db);
68
69        let task = task_service.get_task(task_id)?;
70        let dirty = self.find_dirty_workspaces(task_id)?;
71
72        if !dirty.is_empty() && !allow_dirty {
73            return Err(TrackError::UncommittedWorkspaces(
74                dirty
75                    .into_iter()
76                    .map(|ws| format!("#{} {}", ws.id, ws.path))
77                    .collect(),
78            ));
79        }
80
81        let worktrees = worktree_service.list_worktrees(task_id)?;
82        let mut removed_workspaces = Vec::new();
83        let mut workspace_errors = Vec::new();
84
85        for worktree in worktrees {
86            match worktree_service.remove_worktree(worktree.id, false) {
87                Ok(()) => removed_workspaces.push((worktree.id, worktree.path)),
88                Err(err) => workspace_errors.push(format!("#{}: {}", worktree.id, err)),
89            }
90        }
91
92        task_service.archive_task(task_id)?;
93
94        Ok(ArchiveTaskOutcome {
95            task,
96            removed_workspaces,
97            workspace_errors,
98        })
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use crate::models::TaskStatus;
106
107    #[test]
108    fn archive_task_marks_task_archived() {
109        let db = Database::new_in_memory().unwrap();
110        let task_service = TaskService::new(&db);
111        let task = task_service
112            .create_task("Archive me", None, None, None)
113            .unwrap();
114
115        let outcome = ArchiveTaskUseCase::new(&db).execute(task.id, true).unwrap();
116
117        assert_eq!(outcome.task.id, task.id);
118        let archived = task_service.get_task(task.id).unwrap();
119        assert_eq!(archived.status, TaskStatus::Archived);
120    }
121}