@@ -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<String> = 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);
}
}
@@ -132,6 +132,35 @@ pub fn load_blockers(conn: &Connection, task_id: &str) -> Result<Vec<String>> {
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<String>, Vec<String>)> {
+ 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::<rusqlite::Result<_>>()?;
+ 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
@@ -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]"));
+}