Remove tags and blockers
Unused features adding complexity without value. Drops the
tags, task_tags, and blockers tables plus all associated ops,
CLI commands, models, and test coverage.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/migrations/005_drop_tags_blockers.sql b/migrations/005_drop_tags_blockers.sql
new file mode 100644
index 0000000..15bb21b
--- /dev/null
+++ b/migrations/005_drop_tags_blockers.sql
@@ -0,0 +1,3 @@
+DROP TABLE IF EXISTS task_tags;
+DROP TABLE IF EXISTS tags;
+DROP TABLE IF EXISTS blockers;
diff --git a/src/bin/ranger/commands/blocker.rs b/src/bin/ranger/commands/blocker.rs
deleted file mode 100644
index 38781c9..0000000
--- a/src/bin/ranger/commands/blocker.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use clap::Subcommand;
-use color_eyre::eyre::Result;
-use ranger::db::SqlitePool;
-use ranger::key;
-use ranger::ops;
-
-use crate::output;
-
-#[derive(Subcommand)]
-pub enum BlockerCommands {
- /// Add a blocker to a task
- #[command(visible_alias = "a")]
- Add {
- /// Task key or prefix (the blocked task)
- task: String,
- /// Blocking task key or prefix
- blocked_by: String,
- },
- /// Remove a blocker from a task
- #[command(visible_alias = "rm")]
- Remove {
- /// Task key or prefix (the blocked task)
- task: String,
- /// Blocking task key or prefix
- blocked_by: String,
- },
-}
-
-pub async fn run(pool: &SqlitePool, command: BlockerCommands, json: bool) -> Result<()> {
- let mut conn = pool.acquire().await?;
-
- match command {
- BlockerCommands::Add { task, blocked_by } => {
- let t = ops::task::get_by_key_prefix(&mut conn, &task).await?;
- let bt = ops::task::get_by_key_prefix(&mut conn, &blocked_by).await?;
- let all_keys = ops::task::all_keys(&mut conn).await?;
- let prefixes = key::unique_prefix_lengths(&all_keys);
- let blocker = ops::blocker::add(&mut conn, t.id, bt.id).await?;
- output::print(&blocker, json, |_| {
- println!(
- "{} blocked by {} {}",
- output::format_key_from_map(&t.key, &prefixes),
- output::format_key_from_map(&bt.key, &prefixes),
- bt.title
- );
- });
- }
- BlockerCommands::Remove { task, blocked_by } => {
- let t = ops::task::get_by_key_prefix(&mut conn, &task).await?;
- let bt = ops::task::get_by_key_prefix(&mut conn, &blocked_by).await?;
- let all_keys = ops::task::all_keys(&mut conn).await?;
- let prefixes = key::unique_prefix_lengths(&all_keys);
- ops::blocker::remove(&mut conn, t.id, bt.id).await?;
- println!(
- "Removed blocker {} from {}",
- output::format_key_from_map(&bt.key, &prefixes),
- output::format_key_from_map(&t.key, &prefixes)
- );
- }
- }
- Ok(())
-}
diff --git a/src/bin/ranger/commands/mod.rs b/src/bin/ranger/commands/mod.rs
index 3674cef..e0afbd2 100644
--- a/src/bin/ranger/commands/mod.rs
+++ b/src/bin/ranger/commands/mod.rs
@@ -1,5 +1,3 @@
pub mod backlog;
-pub mod blocker;
pub mod comment;
-pub mod tag;
pub mod task;
diff --git a/src/bin/ranger/commands/tag.rs b/src/bin/ranger/commands/tag.rs
deleted file mode 100644
index 07edc1f..0000000
--- a/src/bin/ranger/commands/tag.rs
+++ /dev/null
@@ -1,27 +0,0 @@
-use clap::Subcommand;
-use color_eyre::eyre::Result;
-use ranger::db::SqlitePool;
-use ranger::ops;
-
-use crate::output;
-
-#[derive(Subcommand)]
-pub enum TagCommands {
- /// List all tags
- #[command(visible_alias = "ls")]
- List,
-}
-
-pub async fn run(pool: &SqlitePool, command: TagCommands, json: bool) -> Result<()> {
- let mut conn = pool.acquire().await?;
-
- match command {
- TagCommands::List => {
- let tags = ops::tag::list(&mut conn).await?;
- output::print_list(&tags, json, |t| {
- println!("{}", t.name);
- });
- }
- }
- Ok(())
-}
diff --git a/src/bin/ranger/commands/task.rs b/src/bin/ranger/commands/task.rs
index ba72eff..417694f 100644
--- a/src/bin/ranger/commands/task.rs
+++ b/src/bin/ranger/commands/task.rs
@@ -77,9 +77,6 @@ pub enum TaskCommands {
/// Parent task key or prefix (makes this a subtask)
#[arg(long)]
parent: Option<String>,
- /// Tags to add (comma-separated)
- #[arg(long)]
- tag: Option<String>,
#[command(flatten)]
position: PositionArgs,
},
@@ -141,7 +138,6 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
description,
state,
parent,
- tag,
position,
} => {
let mut tx = pool.begin().await?;
@@ -171,13 +167,6 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
ops::task::move_task(&mut tx, &task, anchors.as_placement()).await?;
}
- if let Some(tags) = &tag {
- for tag_name in tags.split(',').map(str::trim) {
- let t = ops::tag::get_or_create(&mut tx, tag_name).await?;
- ops::tag::add_to_task(&mut tx, task.id, t.id).await?;
- }
- }
-
tx.commit().await?;
let mut conn = pool.acquire().await?;
@@ -215,15 +204,11 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
let mut conn = pool.acquire().await?;
let task = ops::task::get_by_key_prefix(&mut conn, &key).await?;
let comments = ops::comment::list(&mut conn, task.id).await?;
- let tags = ops::tag::list_for_task(&mut conn, task.id).await?;
- let blockers = ops::blocker::list_for_task(&mut conn, task.id).await?;
if json {
let detail = serde_json::json!({
"task": task,
"comments": comments,
- "tags": tags,
- "blockers": blockers,
});
println!("{}", serde_json::to_string_pretty(&detail).unwrap());
} else {
@@ -231,23 +216,6 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
let prefixes = key::unique_prefix_lengths(&all_keys);
print_task_detail(&task, &prefixes);
- if !tags.is_empty() {
- let tag_names: Vec<&str> = tags.iter().map(|t| t.name.as_str()).collect();
- println!("Tags: {}", tag_names.join(", "));
- }
- if !blockers.is_empty() {
- println!("Blocked by:");
- for b in &blockers {
- if let Ok(bt) = ops::task::get_by_id(&mut conn, b.blocked_by_task_id).await
- {
- println!(
- " {} {}",
- output::format_key_from_map(&bt.key, &prefixes),
- bt.title
- );
- }
- }
- }
if !comments.is_empty() {
println!();
for c in &comments {
diff --git a/src/bin/ranger/main.rs b/src/bin/ranger/main.rs
index 687275d..0a39733 100644
--- a/src/bin/ranger/main.rs
+++ b/src/bin/ranger/main.rs
@@ -39,16 +39,6 @@ enum Commands {
#[command(subcommand)]
command: commands::comment::CommentCommands,
},
- /// Manage tags
- Tag {
- #[command(subcommand)]
- command: commands::tag::TagCommands,
- },
- /// Manage blockers
- Blocker {
- #[command(subcommand)]
- command: commands::blocker::BlockerCommands,
- },
}
fn resolve_db_path(cli_path: Option<PathBuf>) -> PathBuf {
@@ -81,12 +71,6 @@ async fn main() -> color_eyre::Result<()> {
Commands::Comment { command } => {
commands::comment::run(&pool, command, cli.json).await?;
}
- Commands::Tag { command } => {
- commands::tag::run(&pool, command, cli.json).await?;
- }
- Commands::Blocker { command } => {
- commands::blocker::run(&pool, command, cli.json).await?;
- }
}
Ok(())
diff --git a/src/db.rs b/src/db.rs
index b6444df..6ec8125 100644
--- a/src/db.rs
+++ b/src/db.rs
@@ -51,9 +51,9 @@ mod tests {
assert!(table_names.contains(&"backlogs".to_string()));
assert!(table_names.contains(&"tasks".to_string()));
assert!(table_names.contains(&"comments".to_string()));
- assert!(table_names.contains(&"blockers".to_string()));
- assert!(table_names.contains(&"tags".to_string()));
- assert!(table_names.contains(&"task_tags".to_string()));
+ assert!(!table_names.contains(&"blockers".to_string()));
+ assert!(!table_names.contains(&"tags".to_string()));
+ assert!(!table_names.contains(&"task_tags".to_string()));
assert!(!table_names.contains(&"backlog_tasks".to_string()));
}
}
diff --git a/src/models.rs b/src/models.rs
index a080c97..b1f83c1 100644
--- a/src/models.rs
+++ b/src/models.rs
@@ -108,19 +108,6 @@ pub struct Comment {
pub created_at: Timestamp,
}
-#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
-pub struct Tag {
- pub id: i64,
- pub name: String,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
-pub struct Blocker {
- pub id: i64,
- pub task_id: i64,
- pub blocked_by_task_id: i64,
-}
-
#[cfg(test)]
mod tests {
use super::*;
diff --git a/src/ops/blocker.rs b/src/ops/blocker.rs
deleted file mode 100644
index e05bc09..0000000
--- a/src/ops/blocker.rs
+++ /dev/null
@@ -1,133 +0,0 @@
-use crate::error::RangerError;
-use crate::models::Blocker;
-use sqlx::sqlite::SqliteConnection;
-
-pub async fn add(
- conn: &mut SqliteConnection,
- task_id: i64,
- blocked_by_task_id: i64,
-) -> Result<Blocker, RangerError> {
- let blocker = sqlx::query_as::<_, Blocker>(
- "INSERT INTO blockers (task_id, blocked_by_task_id) VALUES (?, ?) \
- RETURNING id, task_id, blocked_by_task_id",
- )
- .bind(task_id)
- .bind(blocked_by_task_id)
- .fetch_one(&mut *conn)
- .await?;
- Ok(blocker)
-}
-
-pub async fn remove(
- conn: &mut SqliteConnection,
- task_id: i64,
- blocked_by_task_id: i64,
-) -> Result<(), RangerError> {
- sqlx::query("DELETE FROM blockers WHERE task_id = ? AND blocked_by_task_id = ?")
- .bind(task_id)
- .bind(blocked_by_task_id)
- .execute(&mut *conn)
- .await?;
- Ok(())
-}
-
-pub async fn list_for_task(
- conn: &mut SqliteConnection,
- task_id: i64,
-) -> Result<Vec<Blocker>, RangerError> {
- let blockers = sqlx::query_as::<_, Blocker>(
- "SELECT id, task_id, blocked_by_task_id FROM blockers WHERE task_id = ?",
- )
- .bind(task_id)
- .fetch_all(&mut *conn)
- .await?;
- Ok(blockers)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::db;
- use crate::ops::{backlog, task};
- use tempfile::tempdir;
-
- async fn test_pool() -> sqlx::SqlitePool {
- let dir = tempdir().unwrap();
- let dir = Box::leak(Box::new(dir));
- db::connect(&dir.path().join("test.db")).await.unwrap()
- }
-
- #[tokio::test]
- async fn add_and_list_blockers() {
- let pool = test_pool().await;
- let mut conn = pool.acquire().await.unwrap();
- let bl = backlog::create(&mut conn, "Test").await.unwrap();
- let t1 = task::create(
- &mut conn,
- task::CreateTask {
- title: "Blocked",
- backlog_id: bl.id,
- state: None,
- parent_id: None,
- description: None,
- },
- )
- .await
- .unwrap();
- let t2 = task::create(
- &mut conn,
- task::CreateTask {
- title: "Blocker",
- backlog_id: bl.id,
- state: None,
- parent_id: None,
- description: None,
- },
- )
- .await
- .unwrap();
-
- add(&mut conn, t1.id, t2.id).await.unwrap();
-
- let blockers = list_for_task(&mut conn, t1.id).await.unwrap();
- assert_eq!(blockers.len(), 1);
- assert_eq!(blockers[0].blocked_by_task_id, t2.id);
- }
-
- #[tokio::test]
- async fn remove_blocker() {
- let pool = test_pool().await;
- let mut conn = pool.acquire().await.unwrap();
- let bl = backlog::create(&mut conn, "Test").await.unwrap();
- let t1 = task::create(
- &mut conn,
- task::CreateTask {
- title: "Blocked",
- backlog_id: bl.id,
- state: None,
- parent_id: None,
- description: None,
- },
- )
- .await
- .unwrap();
- let t2 = task::create(
- &mut conn,
- task::CreateTask {
- title: "Blocker",
- backlog_id: bl.id,
- state: None,
- parent_id: None,
- description: None,
- },
- )
- .await
- .unwrap();
-
- add(&mut conn, t1.id, t2.id).await.unwrap();
- remove(&mut conn, t1.id, t2.id).await.unwrap();
-
- let blockers = list_for_task(&mut conn, t1.id).await.unwrap();
- assert_eq!(blockers.len(), 0);
- }
-}
diff --git a/src/ops/mod.rs b/src/ops/mod.rs
index 3674cef..e0afbd2 100644
--- a/src/ops/mod.rs
+++ b/src/ops/mod.rs
@@ -1,5 +1,3 @@
pub mod backlog;
-pub mod blocker;
pub mod comment;
-pub mod tag;
pub mod task;
diff --git a/src/ops/tag.rs b/src/ops/tag.rs
deleted file mode 100644
index 9c74076..0000000
--- a/src/ops/tag.rs
+++ /dev/null
@@ -1,141 +0,0 @@
-use crate::error::RangerError;
-use crate::models::Tag;
-use sqlx::sqlite::SqliteConnection;
-
-/// Get or create a tag by name.
-pub async fn get_or_create(conn: &mut SqliteConnection, name: &str) -> Result<Tag, RangerError> {
- // Try insert, ignore conflict
- sqlx::query("INSERT OR IGNORE INTO tags (name) VALUES (?)")
- .bind(name)
- .execute(&mut *conn)
- .await?;
-
- let tag = sqlx::query_as::<_, Tag>("SELECT id, name FROM tags WHERE name = ?")
- .bind(name)
- .fetch_one(&mut *conn)
- .await?;
- Ok(tag)
-}
-
-pub async fn list(conn: &mut SqliteConnection) -> Result<Vec<Tag>, RangerError> {
- let tags = sqlx::query_as::<_, Tag>("SELECT id, name FROM tags ORDER BY name")
- .fetch_all(&mut *conn)
- .await?;
- Ok(tags)
-}
-
-pub async fn add_to_task(
- conn: &mut SqliteConnection,
- task_id: i64,
- tag_id: i64,
-) -> Result<(), RangerError> {
- sqlx::query("INSERT OR IGNORE INTO task_tags (task_id, tag_id) VALUES (?, ?)")
- .bind(task_id)
- .bind(tag_id)
- .execute(&mut *conn)
- .await?;
- Ok(())
-}
-
-pub async fn list_for_task(
- conn: &mut SqliteConnection,
- task_id: i64,
-) -> Result<Vec<Tag>, RangerError> {
- let tags = sqlx::query_as::<_, Tag>(
- "SELECT t.id, t.name FROM tags t \
- JOIN task_tags tt ON tt.tag_id = t.id \
- WHERE tt.task_id = ? ORDER BY t.name",
- )
- .bind(task_id)
- .fetch_all(&mut *conn)
- .await?;
- Ok(tags)
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use crate::db;
- use crate::ops::{backlog, task};
- use tempfile::tempdir;
-
- async fn test_pool() -> sqlx::SqlitePool {
- let dir = tempdir().unwrap();
- let dir = Box::leak(Box::new(dir));
- db::connect(&dir.path().join("test.db")).await.unwrap()
- }
-
- #[tokio::test]
- async fn get_or_create_is_idempotent() {
- let pool = test_pool().await;
- let mut conn = pool.acquire().await.unwrap();
- let t1 = get_or_create(&mut conn, "urgent").await.unwrap();
- let t2 = get_or_create(&mut conn, "urgent").await.unwrap();
- assert_eq!(t1.id, t2.id);
- }
-
- #[tokio::test]
- async fn list_tags() {
- let pool = test_pool().await;
- let mut conn = pool.acquire().await.unwrap();
- get_or_create(&mut conn, "beta").await.unwrap();
- get_or_create(&mut conn, "alpha").await.unwrap();
-
- let tags = list(&mut conn).await.unwrap();
- assert_eq!(tags.len(), 2);
- assert_eq!(tags[0].name, "alpha");
- assert_eq!(tags[1].name, "beta");
- }
-
- #[tokio::test]
- async fn add_tag_to_task_and_list() {
- let pool = test_pool().await;
- let mut conn = pool.acquire().await.unwrap();
- let bl = backlog::create(&mut conn, "Test").await.unwrap();
- let t = task::create(
- &mut conn,
- task::CreateTask {
- title: "Task",
- backlog_id: bl.id,
- state: None,
- parent_id: None,
- description: None,
- },
- )
- .await
- .unwrap();
- let tag = get_or_create(&mut conn, "important").await.unwrap();
-
- add_to_task(&mut conn, t.id, tag.id).await.unwrap();
-
- let tags = list_for_task(&mut conn, t.id).await.unwrap();
- assert_eq!(tags.len(), 1);
- assert_eq!(tags[0].name, "important");
- }
-
- #[tokio::test]
- async fn add_tag_to_task_is_idempotent() {
- let pool = test_pool().await;
- let mut conn = pool.acquire().await.unwrap();
- let bl = backlog::create(&mut conn, "Test").await.unwrap();
- let t = task::create(
- &mut conn,
- task::CreateTask {
- title: "Task",
- backlog_id: bl.id,
- state: None,
- parent_id: None,
- description: None,
- },
- )
- .await
- .unwrap();
- let tag = get_or_create(&mut conn, "dup").await.unwrap();
-
- add_to_task(&mut conn, t.id, tag.id).await.unwrap();
- add_to_task(&mut conn, t.id, tag.id).await.unwrap();
-
- let tags = list_for_task(&mut conn, t.id).await.unwrap();
- assert_eq!(tags.len(), 1);
- }
-}
diff --git a/tests/cli.rs b/tests/cli.rs
index fb3df8e..a9e2aff 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -41,7 +41,7 @@ fn full_workflow() {
assert!(output.status.success());
let output = ranger(db_path)
- .args(["task", "create", "Second task", "--tag", "urgent"])
+ .args(["task", "create", "Second task"])
.output()
.unwrap();
assert!(output.status.success());
@@ -86,19 +86,6 @@ fn full_workflow() {
let stdout = String::from_utf8(output.stdout).unwrap();
assert!(stdout.contains("Started working on this"));
- // Add a blocker
- let output = ranger(db_path)
- .args(["blocker", "add", &t2_key[..4], &t1_key[..4]])
- .output()
- .unwrap();
- assert!(output.status.success());
-
- // List tags
- let output = ranger(db_path).args(["tag", "list"]).output().unwrap();
- assert!(output.status.success());
- let stdout = String::from_utf8(output.stdout).unwrap();
- assert!(stdout.contains("urgent"));
-
// Show task (JSON) — verify all data present
let output = ranger(db_path)
.args(["task", "show", &t2_key[..4], "--json"])
@@ -107,8 +94,6 @@ fn full_workflow() {
assert!(output.status.success());
let detail: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
assert_eq!(detail["task"]["title"], "Second task");
- assert_eq!(detail["tags"][0]["name"], "urgent");
- assert_eq!(detail["blockers"].as_array().unwrap().len(), 1);
// Create two queued tasks and use edit --before to reposition within the same state
let output = ranger(db_path)