Scope key prefix resolution to default backlog
When RANGER_DEFAULT_BACKLOG is set, key lookups resolve within
that backlog so the shorter prefixes shown in backlog-scoped
listings actually work in subsequent commands.
Assisted-by: Claude Opus 4.6 via pi
diff --git a/src/bin/ranger/commands/comment.rs b/src/bin/ranger/commands/comment.rs
index 946e083..9a5bf08 100644
--- a/src/bin/ranger/commands/comment.rs
+++ b/src/bin/ranger/commands/comment.rs
@@ -24,18 +24,19 @@ pub enum CommentCommands {
}
pub async fn run(pool: &SqlitePool, command: CommentCommands, json: bool) -> Result<()> {
+ let backlog_scope = super::task::default_backlog_id(pool).await;
let mut conn = pool.acquire().await?;
match command {
CommentCommands::Add { task, body } => {
- let t = ops::task::get_by_key_prefix(&mut conn, &task).await?;
+ let t = ops::task::get_by_key_prefix(&mut conn, &task, backlog_scope).await?;
let comment = ops::comment::add(&mut conn, t.id, &body).await?;
output::print(&comment, json, |c| {
println!("[{}] {}", c.created_at, c.body);
});
}
CommentCommands::List { task } => {
- let t = ops::task::get_by_key_prefix(&mut conn, &task).await?;
+ let t = ops::task::get_by_key_prefix(&mut conn, &task, backlog_scope).await?;
let comments = ops::comment::list(&mut conn, t.id).await?;
output::print_list(&comments, json, |c| {
println!("[{}] {}", c.created_at, c.body);
diff --git a/src/bin/ranger/commands/task.rs b/src/bin/ranger/commands/task.rs
index 01487c6..5cf05b8 100644
--- a/src/bin/ranger/commands/task.rs
+++ b/src/bin/ranger/commands/task.rs
@@ -22,20 +22,24 @@ pub struct PositionArgs {
}
impl PositionArgs {
- async fn resolve(self, conn: &mut SqliteConnection) -> Result<Option<PositionAnchors>> {
+ async fn resolve(
+ self,
+ conn: &mut SqliteConnection,
+ backlog_id: Option<i64>,
+ ) -> Result<Option<PositionAnchors>> {
match (self.before, self.after) {
(None, None) => Ok(None),
(Some(b), None) => {
- let before = ops::task::get_by_key_prefix(conn, &b).await?;
+ let before = ops::task::get_by_key_prefix(conn, &b, backlog_id).await?;
Ok(Some(PositionAnchors::Before(before)))
}
(None, Some(a)) => {
- let after = ops::task::get_by_key_prefix(conn, &a).await?;
+ let after = ops::task::get_by_key_prefix(conn, &a, backlog_id).await?;
Ok(Some(PositionAnchors::After(after)))
}
(Some(b), Some(a)) => {
- let before = ops::task::get_by_key_prefix(conn, &b).await?;
- let after = ops::task::get_by_key_prefix(conn, &a).await?;
+ let before = ops::task::get_by_key_prefix(conn, &b, backlog_id).await?;
+ let after = ops::task::get_by_key_prefix(conn, &a, backlog_id).await?;
Ok(Some(PositionAnchors::Between { before, after }))
}
}
@@ -145,7 +149,20 @@ pub enum TaskCommands {
},
}
+/// Resolve `RANGER_DEFAULT_BACKLOG` to a backlog ID, if set.
+/// Returns `None` when the env var is absent or the backlog doesn't exist.
+pub async fn default_backlog_id(pool: &SqlitePool) -> Option<i64> {
+ let name = std::env::var("RANGER_DEFAULT_BACKLOG").ok()?;
+ let mut conn = pool.acquire().await.ok()?;
+ ops::backlog::get_by_name(&mut conn, &name)
+ .await
+ .ok()
+ .map(|b| b.id)
+}
+
pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result<()> {
+ let backlog_scope = default_backlog_id(pool).await;
+
match command {
TaskCommands::Create {
title,
@@ -159,11 +176,15 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
let bl = ops::backlog::get_by_name(&mut tx, &backlog).await?;
let parent_id = if let Some(parent_key) = &parent {
- Some(ops::task::get_by_key_prefix(&mut tx, parent_key).await?.id)
+ Some(
+ ops::task::get_by_key_prefix(&mut tx, parent_key, Some(bl.id))
+ .await?
+ .id,
+ )
} else {
None
};
- let anchors = position.resolve(&mut tx).await?;
+ let anchors = position.resolve(&mut tx, Some(bl.id)).await?;
let state = state.map(|s| s.parse::<State>()).transpose()?;
let task = ops::task::create(
@@ -225,7 +246,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
}
TaskCommands::Show { key } => {
let mut conn = pool.acquire().await?;
- let task = ops::task::get_by_key_prefix(&mut conn, &key).await?;
+ let task = ops::task::get_by_key_prefix(&mut conn, &key, backlog_scope).await?;
let comments = ops::comment::list(&mut conn, task.id).await?;
if json {
@@ -257,9 +278,9 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
} => {
let mut conn = pool.acquire().await?;
let state = state.map(|s| s.parse::<State>()).transpose()?;
- let anchors = position.resolve(&mut conn).await?;
+ let anchors = position.resolve(&mut conn, backlog_scope).await?;
- let task = ops::task::get_by_key_prefix(&mut conn, &key).await?;
+ let task = ops::task::get_by_key_prefix(&mut conn, &key, backlog_scope).await?;
let updated = ops::task::edit(
&mut conn,
task.id,
@@ -279,8 +300,8 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
}
TaskCommands::Move { key, position } => {
let mut conn = pool.acquire().await?;
- let task = ops::task::get_by_key_prefix(&mut conn, &key).await?;
- let anchors = position.resolve(&mut conn).await?;
+ let task = ops::task::get_by_key_prefix(&mut conn, &key, backlog_scope).await?;
+ let anchors = position.resolve(&mut conn, backlog_scope).await?;
match anchors {
Some(anchors) => {
@@ -298,7 +319,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
}
TaskCommands::Delete { key } => {
let mut conn = pool.acquire().await?;
- let task = ops::task::get_by_key_prefix(&mut conn, &key).await?;
+ let task = ops::task::get_by_key_prefix(&mut conn, &key, backlog_scope).await?;
let all_keys = ops::task::all_keys(&mut conn).await?;
let prefixes = key::unique_prefix_lengths(&all_keys);
ops::task::delete(&mut conn, task.id).await?;
@@ -310,7 +331,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
}
TaskCommands::Archive { key } => {
let mut conn = pool.acquire().await?;
- let task = ops::task::get_by_key_prefix(&mut conn, &key).await?;
+ let task = ops::task::get_by_key_prefix(&mut conn, &key, backlog_scope).await?;
let updated = ops::task::set_archived(&mut conn, task.id, true).await?;
let all_keys = ops::task::all_keys(&mut conn).await?;
let prefixes = key::unique_prefix_lengths(&all_keys);
@@ -324,7 +345,7 @@ pub async fn run(pool: &SqlitePool, command: TaskCommands, json: bool) -> Result
}
TaskCommands::Unarchive { key } => {
let mut conn = pool.acquire().await?;
- let task = ops::task::get_by_key_prefix(&mut conn, &key).await?;
+ let task = ops::task::get_by_key_prefix(&mut conn, &key, backlog_scope).await?;
let updated = ops::task::set_archived(&mut conn, task.id, false).await?;
let all_keys = ops::task::all_keys(&mut conn).await?;
let prefixes = key::unique_prefix_lengths(&all_keys);
diff --git a/src/ops/task.rs b/src/ops/task.rs
index 5addaf9..ffa9e6b 100644
--- a/src/ops/task.rs
+++ b/src/ops/task.rs
@@ -124,13 +124,23 @@ pub async fn get_by_id(conn: &mut SqliteConnection, id: i64) -> Result<Task, Ran
pub async fn get_by_key_prefix(
conn: &mut SqliteConnection,
prefix: &str,
+ backlog_id: Option<i64>,
) -> Result<Task, RangerError> {
let pattern = format!("{prefix}%");
- let query = format!("SELECT {TASK_COLUMNS} FROM tasks WHERE key LIKE ?");
- let matches = sqlx::query_as::<_, Task>(&query)
- .bind(&pattern)
- .fetch_all(&mut *conn)
- .await?;
+ let matches = if let Some(bid) = backlog_id {
+ let query = format!("SELECT {TASK_COLUMNS} FROM tasks WHERE key LIKE ? AND backlog_id = ?");
+ sqlx::query_as::<_, Task>(&query)
+ .bind(&pattern)
+ .bind(bid)
+ .fetch_all(&mut *conn)
+ .await?
+ } else {
+ let query = format!("SELECT {TASK_COLUMNS} FROM tasks WHERE key LIKE ?");
+ sqlx::query_as::<_, Task>(&query)
+ .bind(&pattern)
+ .fetch_all(&mut *conn)
+ .await?
+ };
match matches.len() {
0 => Err(RangerError::KeyNotFound(prefix.to_string())),
@@ -613,7 +623,9 @@ mod tests {
.await
.unwrap();
- let found = get_by_key_prefix(&mut conn, &task.key[..3]).await.unwrap();
+ let found = get_by_key_prefix(&mut conn, &task.key[..3], None)
+ .await
+ .unwrap();
assert_eq!(found.id, task.id);
}
@@ -670,7 +682,7 @@ mod tests {
delete(&mut conn, task.id).await.unwrap();
- let result = get_by_key_prefix(&mut conn, &task.key).await;
+ let result = get_by_key_prefix(&mut conn, &task.key, None).await;
assert!(result.is_err());
let tasks = list(&mut conn, bl.id, &ListFilter::default())
@@ -720,8 +732,43 @@ mod tests {
.await
.unwrap();
- let result = get_by_key_prefix(&mut conn, "kkkk").await;
+ let result = get_by_key_prefix(&mut conn, "kkkk", None).await;
+ assert!(result.is_err());
+ }
+
+ #[tokio::test]
+ async fn get_by_key_prefix_scoped_to_backlog() {
+ let pool = test_pool().await;
+ let mut conn = pool.acquire().await.unwrap();
+ let bl1 = backlog::create(&mut conn, "Alpha").await.unwrap();
+ let bl2 = backlog::create(&mut conn, "Beta").await.unwrap();
+
+ // Two tasks with the same key prefix in different backlogs
+ sqlx::query("INSERT INTO tasks (key, backlog_id, title, state, position) VALUES ('kkkkaaaa', ?, 'In Alpha', 'icebox', 'a')")
+ .bind(bl1.id)
+ .execute(&mut *conn)
+ .await
+ .unwrap();
+ sqlx::query("INSERT INTO tasks (key, backlog_id, title, state, position) VALUES ('kkkkbbbb', ?, 'In Beta', 'icebox', 'a')")
+ .bind(bl2.id)
+ .execute(&mut *conn)
+ .await
+ .unwrap();
+
+ // Globally ambiguous
+ let result = get_by_key_prefix(&mut conn, "kkkk", None).await;
assert!(result.is_err());
+
+ // Scoped to each backlog resolves uniquely
+ let t1 = get_by_key_prefix(&mut conn, "kkkk", Some(bl1.id))
+ .await
+ .unwrap();
+ assert_eq!(t1.title, "In Alpha");
+
+ let t2 = get_by_key_prefix(&mut conn, "kkkk", Some(bl2.id))
+ .await
+ .unwrap();
+ assert_eq!(t2.title, "In Beta");
}
#[tokio::test]