diff --git a/src/cmd/show.rs b/src/cmd/show.rs index 25cc9f714208c6d7d10395ccc267d5e64038ac7f..755e9cbf4946f08fc409986b5b392905e4601ec2 100644 --- a/src/cmd/show.rs +++ b/src/cmd/show.rs @@ -50,13 +50,27 @@ pub fn run(root: &Path, id: &str, json: bool) -> Result<()> { detail.labels.join(",") ); } - if !detail.blockers.is_empty() { - println!( - "{} blockers{} = {}", - c.bold, - c.reset, - detail.blockers.join(",") - ); + let (open_blockers, closed_blockers) = db::load_blockers_partitioned(&conn, &t.id)?; + let total = open_blockers.len() + closed_blockers.len(); + if total > 0 { + let label = if total == 1 { "blocker" } else { "blockers" }; + let all_closed = open_blockers.is_empty(); + let mut ids: Vec = Vec::new(); + for id in &open_blockers { + ids.push(id.clone()); + } + for id in &closed_blockers { + ids.push(format!("{id} [closed]")); + } + + let value = if all_closed { + format!("[all closed] {}", ids.join(", ")) + } else { + ids.join(", ") + }; + + // Right-align the label to match other fields (12 chars wide). + println!("{}{label:>12}{} = {value}", c.bold, c.reset); } } diff --git a/src/db.rs b/src/db.rs index 59ea9ada8b361084e547557977cdbd6f77c86ae2..fa019d8cc8d59988ec7a3fbff3529a0480184091 100644 --- a/src/db.rs +++ b/src/db.rs @@ -132,6 +132,35 @@ pub fn load_blockers(conn: &Connection, task_id: &str) -> Result> { Ok(blockers) } +/// Load blockers for a task, partitioned by whether they are resolved. +/// +/// Returns `(open, resolved)` where open blockers have a non-closed status +/// and resolved blockers are closed. +pub fn load_blockers_partitioned( + conn: &Connection, + task_id: &str, +) -> Result<(Vec, Vec)> { + let mut stmt = conn.prepare( + "SELECT b.blocker_id, COALESCE(t.status, 'open') + FROM blockers b + LEFT JOIN tasks t ON b.blocker_id = t.id + WHERE b.task_id = ?1", + )?; + let mut open = Vec::new(); + let mut resolved = Vec::new(); + let rows: Vec<(String, String)> = stmt + .query_map([task_id], |r| Ok((r.get(0)?, r.get(1)?)))? + .collect::>()?; + for (id, status) in rows { + if status == "closed" { + resolved.push(id); + } else { + open.push(id); + } + } + Ok((open, resolved)) +} + /// Check whether `from` can reach `to` by following blocker edges. /// /// Returns `true` if there is a path from `from` to `to` in the blocker diff --git a/tests/cli_list_show.rs b/tests/cli_list_show.rs index f1e6d3f6bd39b3e14ed1309a84f4fc88ffb81395..347d4f8bd8c38db4fccafc01f8b45e8d123a0bb8 100644 --- a/tests/cli_list_show.rs +++ b/tests/cli_list_show.rs @@ -201,3 +201,85 @@ fn show_nonexistent_task_fails() { .failure() .stderr(predicate::str::contains("not found")); } + +#[test] +fn show_annotates_closed_blockers() { + let tmp = init_tmp(); + let task = create_task(&tmp, "Blocked task"); + let open_blocker = create_task(&tmp, "Still open"); + let closed_blocker = create_task(&tmp, "Will close"); + + td().args(["dep", "add", &task, &open_blocker]) + .current_dir(&tmp) + .assert() + .success(); + td().args(["dep", "add", &task, &closed_blocker]) + .current_dir(&tmp) + .assert() + .success(); + td().args(["done", &closed_blocker]) + .current_dir(&tmp) + .assert() + .success(); + + // Plural label, open blocker bare, closed one annotated. + td().args(["show", &task]) + .current_dir(&tmp) + .assert() + .success() + .stdout(predicate::str::contains("blockers")) + .stdout(predicate::str::contains(&open_blocker)) + .stdout(predicate::str::contains(&format!( + "{closed_blocker} [closed]" + ))); +} + +#[test] +fn show_all_closed_blockers_prefixed() { + let tmp = init_tmp(); + let task = create_task(&tmp, "Was blocked"); + let blocker = create_task(&tmp, "Done now"); + + td().args(["dep", "add", &task, &blocker]) + .current_dir(&tmp) + .assert() + .success(); + td().args(["done", &blocker]) + .current_dir(&tmp) + .assert() + .success(); + + // Singular label, [all closed] prefix. + td().args(["show", &task]) + .current_dir(&tmp) + .assert() + .success() + .stdout(predicate::str::contains("blocker")) + .stdout(predicate::str::contains("[all closed]")) + .stdout(predicate::str::contains(&blocker)); +} + +#[test] +fn show_single_open_blocker_singular_label() { + let tmp = init_tmp(); + let task = create_task(&tmp, "Blocked"); + let blocker = create_task(&tmp, "Blocking"); + + td().args(["dep", "add", &task, &blocker]) + .current_dir(&tmp) + .assert() + .success(); + + let out = td() + .args(["show", &task]) + .current_dir(&tmp) + .output() + .unwrap(); + let stdout = String::from_utf8_lossy(&out.stdout); + + // Singular "blocker", no "blockers". + assert!(stdout.contains("blocker")); + assert!(stdout.contains(&blocker)); + // Should not contain [closed] or [all closed]. + assert!(!stdout.contains("[closed]")); +}