From 9dda8780626ff22289f9949b33233bf0789ade4b Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 21 Jun 2026 13:34:54 +0800 Subject: [PATCH 1/3] feat(task): support numeric short ids --- Cargo.lock | 61 +------------------- src/CLI2.cpp | 32 +++++++---- src/TDB2.cpp | 28 +++++++++- src/Task.cpp | 7 ++- src/columns/ColID.cpp | 2 +- src/taskchampion-cpp/Cargo.toml | 4 +- src/taskchampion-cpp/src/lib.rs | 13 +++++ test/CMakeLists.txt | 1 + test/basetest/task.py | 16 +++--- test/bash_tap_tw.sh | 16 +++--- test/short_id.test.py | 99 +++++++++++++++++++++++++++++++++ 11 files changed, 190 insertions(+), 89 deletions(-) create mode 100644 test/short_id.test.py diff --git a/Cargo.lock b/Cargo.lock index 46b868331..4588dd014 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,18 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.4" @@ -503,18 +491,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fallible-streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" - [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -683,15 +659,6 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", -] - [[package]] name = "hashbrown" version = "0.15.5" @@ -709,15 +676,6 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -[[package]] -name = "hashlink" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" -dependencies = [ - "hashbrown 0.14.5", -] - [[package]] name = "hashlink" version = "0.10.0" @@ -1424,20 +1382,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "rusqlite" -version = "0.32.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" -dependencies = [ - "bitflags", - "fallible-iterator", - "fallible-streaming-iterator", - "hashlink 0.9.1", - "libsqlite3-sys", - "smallvec", -] - [[package]] name = "rustc-hash" version = "2.1.2" @@ -1669,7 +1613,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.15.5", - "hashlink 0.10.0", + "hashlink", "indexmap", "log", "memchr", @@ -1917,7 +1861,7 @@ dependencies = [ [[package]] name = "taskchampion" version = "3.0.2-pre" -source = "git+https://github.com/GuionAI/taskchampion.git?tag=v3.0.2-guion.49#f51b0cacf0049748d5d91516634666df1363620b" +source = "git+https://github.com/GuionAI/taskchampion.git?tag=v3.0.2-guion.58#de999bb20ea80e7c01026c8276f1873ad66a9956" dependencies = [ "anyhow", "async-trait", @@ -1925,7 +1869,6 @@ dependencies = [ "fractional_index", "log", "powersync_core", - "rusqlite", "serde", "serde_json", "sqlx", diff --git a/src/CLI2.cpp b/src/CLI2.cpp index 145891e85..2a28a51a6 100644 --- a/src/CLI2.cpp +++ b/src/CLI2.cpp @@ -1300,14 +1300,25 @@ static bool looksLikeHexPrefix(const std::string& s) { return true; } -// Pushes all hex-prefix elements from a comma-separated string into _uuid_list. +static bool looksLikeNumericShortId(const std::string& s) { + if (s.empty()) return false; + for (char c : s) + if (!std::isdigit(static_cast(c))) return false; + return true; +} + +static bool looksLikeTaskRef(const std::string& s) { + return looksLikeNumericShortId(s) || looksLikeHexPrefix(s); +} + +// Pushes all task-ref elements from a comma-separated string into _uuid_list. // Returns true if any were added. static bool pushHexPrefixesFromSet(const std::string& raw, std::vector& uuid_list) { auto elements = split(raw, ','); bool any = false; for (auto& element : elements) { - if (looksLikeHexPrefix(element)) { + if (looksLikeTaskRef(element)) { uuid_list.push_back(element); any = true; } @@ -1328,11 +1339,12 @@ void CLI2::findIDs() { std::string raw = a.attribute("raw"); - // A hex-only word/identifier token is treated as a UUID prefix. + // Numeric short IDs and hex-only words are treated as task refs. bool isWordOrIdent = (a._lextype == Lexer::Type::word || - a._lextype == Lexer::Type::identifier); + a._lextype == Lexer::Type::identifier || + a._lextype == Lexer::Type::number); if (isWordOrIdent && !previousFilterArgWasAnOperator && - looksLikeHexPrefix(raw)) { + looksLikeTaskRef(raw)) { changes = true; _uuid_list.push_back(raw); } else if (a._lextype == Lexer::Type::set) { @@ -1354,8 +1366,9 @@ void CLI2::findIDs() { if (a.hasTag("MODIFICATION")) { std::string raw = a.attribute("raw"); - if ((a._lextype == Lexer::Type::word || a._lextype == Lexer::Type::identifier) && - looksLikeHexPrefix(raw)) { + if ((a._lextype == Lexer::Type::word || a._lextype == Lexer::Type::identifier || + a._lextype == Lexer::Type::number) && + looksLikeTaskRef(raw)) { changes = true; a.unTag("MODIFICATION"); a.tag("FILTER"); @@ -1494,9 +1507,6 @@ void CLI2::insertIDExpr() { A2 opSimilar("=", Lexer::Type::op); opSimilar.tag("FILTER"); - A2 argUUID("uuid", Lexer::Type::dom); - argUUID.tag("FILTER"); - reconstructed.push_back(openParen); // Add all UUID prefix items. @@ -1504,6 +1514,8 @@ void CLI2::insertIDExpr() { if (u != _uuid_list.begin()) reconstructed.push_back(opOr); reconstructed.push_back(openParen); + A2 argUUID(looksLikeNumericShortId(*u) ? "id" : "uuid", Lexer::Type::dom); + argUUID.tag("FILTER"); reconstructed.push_back(argUUID); reconstructed.push_back(opSimilar); diff --git a/src/TDB2.cpp b/src/TDB2.cpp index 9be7bc5bd..70414f1ba 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -63,6 +63,13 @@ bool looksLikeFullUuid(const std::string& s) { return true; } +bool looksLikeShortId(const std::string& s) { + if (s.empty()) return false; + for (char c : s) + if (!std::isdigit(static_cast(c))) return false; + return true; +} + void apply_depmap(Task& t, tc::DependencyMapWrapper& depmap) { auto uuid_str = t.get("uuid"); if (!looksLikeFullUuid(uuid_str)) return; @@ -74,10 +81,11 @@ void apply_depmap(Task& t, tc::DependencyMapWrapper& depmap) { } // namespace // Keys that must never be written to TaskChampion storage: -// uuid/id — synthetic keys managed by tch itself +// uuid/id/short_id — synthetic keys managed by tch or the backing database // tags — legacy comma-separated representation; tch-native tag_* keys carry the same data // depends — legacy comma-separated representation; tch-native dep_* keys carry the same data -static const std::unordered_set kTCSkippedKeys = {"uuid", "id", "tags", "depends"}; +static const std::unordered_set kTCSkippedKeys = {"uuid", "id", "short_id", "tags", + "depends"}; //////////////////////////////////////////////////////////////////////////////// void TDB2::open_replica(const std::string& db_path) { @@ -339,6 +347,22 @@ void TDB2::invalidate_cached_info() { bool TDB2::get(const std::string& uuid, Task& task) { auto depmap = replica()->dependency_map(); + // Numeric task refs are per-user short IDs. Prefer them over numeric UUID + // prefixes; if no short ID matches, keep the historical UUID-prefix fallback. + if (looksLikeShortId(uuid)) { + try { + auto resolved = replica()->resolve_task_ref(uuid); + auto maybe = replica()->get_task_data(resolved); + if (maybe.is_some()) { + auto tctask = maybe.take(); + task = Task{std::move(tctask)}; + apply_depmap(task, *depmap); + return true; + } + } catch (...) { + } + } + // Tier 1: full-UUID PK fast path. if (looksLikeFullUuid(uuid)) { auto maybe = replica()->get_task_data(tc::uuid_from_string(uuid)); diff --git a/src/Task.cpp b/src/Task.cpp index 8be57848c..a9d47c5d1 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -734,7 +734,12 @@ void Task::parseTC(rust::Box task) { } data["uuid"] = static_cast(task->get_uuid().to_string()); - id = (data["uuid"].length() >= 8) ? data["uuid"].substr(0, 8) : ""; + if (has("short_id")) { + id = get("short_id"); + remove("short_id"); + } else { + id = (data["uuid"].length() >= 8) ? data["uuid"].substr(0, 8) : ""; + } } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColID.cpp b/src/columns/ColID.cpp index 3a0490c49..a0f2a8f7b 100644 --- a/src/columns/ColID.cpp +++ b/src/columns/ColID.cpp @@ -36,7 +36,7 @@ ColumnID::ColumnID() { _label = "ID"; _modifiable = false; _styles = {"short"}; - _examples = {"a1b2c3d4"}; + _examples = {"42"}; } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/taskchampion-cpp/Cargo.toml b/src/taskchampion-cpp/Cargo.toml index ec03748fa..6226f884e 100644 --- a/src/taskchampion-cpp/Cargo.toml +++ b/src/taskchampion-cpp/Cargo.toml @@ -9,7 +9,7 @@ rust-version = "1.91.1" # MSRV (taskchampion@3.0.2-pre requirement) crate-type = ["staticlib"] [dependencies] -taskchampion = { git = "https://github.com/GuionAI/taskchampion.git", tag = "v3.0.2-guion.49", features = ["storage-powersync", "storage-pgwire", "test-utils"] } +taskchampion = { git = "https://github.com/GuionAI/taskchampion.git", tag = "v3.0.2-guion.58", features = ["storage-powersync", "storage-pgwire", "test-utils"] } anyhow = "1" async-trait = "0.1" cxx = "1.0.133" @@ -21,4 +21,4 @@ tokio = { version = "1", features = [ "rt" ] } cxx-build = "1.0.133" [dev-dependencies] -taskchampion = { git = "https://github.com/GuionAI/taskchampion.git", tag = "v3.0.2-guion.49", features = ["storage-powersync", "storage-pgwire", "test-utils"] } +taskchampion = { git = "https://github.com/GuionAI/taskchampion.git", tag = "v3.0.2-guion.58", features = ["storage-powersync", "storage-pgwire", "test-utils"] } diff --git a/src/taskchampion-cpp/src/lib.rs b/src/taskchampion-cpp/src/lib.rs index 64716eb85..0b53ff01b 100644 --- a/src/taskchampion-cpp/src/lib.rs +++ b/src/taskchampion-cpp/src/lib.rs @@ -137,6 +137,9 @@ mod ffi { /// Get an existing task by its UUID. fn get_task_data(&mut self, uuid: Uuid) -> Result; + /// Resolve a full UUID or numeric short ID to a task UUID. + fn resolve_task_ref(&mut self, task_ref: String) -> Result; + /// Get the operations for a task task by its UUID. fn get_task_operations(&mut self, uuid: Uuid) -> Result>; @@ -696,6 +699,16 @@ impl Replica { rt().block_on(async { Ok(self.0.get_task_data(uuid.into()).await?.into()) }) } + fn resolve_task_ref(&mut self, task_ref: String) -> Result { + rt().block_on(async { + self.0 + .resolve_task_ref(&task_ref) + .await? + .map(ffi::Uuid::from) + .ok_or_else(|| CppError::from(anyhow::anyhow!("task reference not found"))) + }) + } + fn get_task_operations(&mut self, uuid: ffi::Uuid) -> Result, CppError> { rt().block_on(async { Ok(from_tc_operations( diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9a9f46251..a9970df17 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -161,6 +161,7 @@ set (pythonTests reports.test.py search.test.py sequence.test.py + short_id.test.py shell.test.py show.test.py sorting.test.py diff --git a/test/basetest/task.py b/test/basetest/task.py index 6a341fba9..cf0f29e45 100644 --- a/test/basetest/task.py +++ b/test/basetest/task.py @@ -85,31 +85,33 @@ def _init_test_db(self): ); INSERT OR IGNORE INTO settings (id, tc_config) VALUES ('tc_config', '{"tags":[]}'); CREATE TABLE IF NOT EXISTS tc_tasks_data ( - id TEXT PRIMARY KEY, user_id TEXT, data TEXT NOT NULL DEFAULT '{}', + id TEXT PRIMARY KEY, short_id INTEGER, user_id TEXT, data TEXT NOT NULL DEFAULT '{}', entry_at TEXT, status TEXT, description TEXT, priority TEXT, modified_at TEXT, due_at TEXT, scheduled_at TEXT, start_at TEXT, end_at TEXT, wait_at TEXT, parent_id TEXT, position TEXT, project_id TEXT, note_id TEXT ); CREATE VIEW IF NOT EXISTS tc_tasks AS - SELECT id, user_id, data, entry_at, status, description, priority, + SELECT id, short_id, user_id, data, entry_at, status, description, priority, modified_at, due_at, scheduled_at, start_at, end_at, wait_at, parent_id, position, project_id, note_id FROM tc_tasks_data; CREATE TRIGGER IF NOT EXISTS tc_tasks_insert INSTEAD OF INSERT ON tc_tasks BEGIN INSERT OR REPLACE INTO tc_tasks_data - (id, user_id, data, entry_at, status, description, priority, + (id, short_id, user_id, data, entry_at, status, description, priority, modified_at, due_at, scheduled_at, start_at, end_at, wait_at, parent_id, position, project_id, note_id) - VALUES (NEW.id, NEW.user_id, COALESCE(NEW.data, '{}'), NEW.entry_at, - NEW.status, NEW.description, NEW.priority, NEW.modified_at, - NEW.due_at, NEW.scheduled_at, NEW.start_at, NEW.end_at, - NEW.wait_at, NEW.parent_id, NEW.position, NEW.project_id, NEW.note_id); + VALUES (NEW.id, NEW.short_id, NEW.user_id, COALESCE(NEW.data, '{}'), + NEW.entry_at, NEW.status, NEW.description, NEW.priority, + NEW.modified_at, NEW.due_at, NEW.scheduled_at, NEW.start_at, + NEW.end_at, NEW.wait_at, NEW.parent_id, NEW.position, + NEW.project_id, NEW.note_id); END; CREATE TRIGGER IF NOT EXISTS tc_tasks_update INSTEAD OF UPDATE ON tc_tasks BEGIN UPDATE tc_tasks_data SET + short_id = NEW.short_id, user_id = NEW.user_id, data = COALESCE(NEW.data, '{}'), entry_at = NEW.entry_at, status = NEW.status, description = NEW.description, priority = NEW.priority, diff --git a/test/bash_tap_tw.sh b/test/bash_tap_tw.sh index 99fec78a4..3fd695ee1 100644 --- a/test/bash_tap_tw.sh +++ b/test/bash_tap_tw.sh @@ -34,30 +34,32 @@ function setup_taskrc { sqlite3 "$POWERSYNC_DB_PATH" <<'SCHEMA' CREATE TABLE IF NOT EXISTS tc_tasks_data ( - id TEXT PRIMARY KEY, user_id TEXT, data TEXT NOT NULL DEFAULT '{}', + id TEXT PRIMARY KEY, short_id INTEGER, user_id TEXT, data TEXT NOT NULL DEFAULT '{}', entry_at TEXT, status TEXT, description TEXT, priority TEXT, modified_at TEXT, due_at TEXT, scheduled_at TEXT, start_at TEXT, end_at TEXT, wait_at TEXT, parent_id TEXT, position TEXT, project_id TEXT ); CREATE VIEW IF NOT EXISTS tc_tasks AS - SELECT id, user_id, data, entry_at, status, description, priority, + SELECT id, short_id, user_id, data, entry_at, status, description, priority, modified_at, due_at, scheduled_at, start_at, end_at, wait_at, parent_id, position, project_id FROM tc_tasks_data; CREATE TRIGGER IF NOT EXISTS tc_tasks_insert INSTEAD OF INSERT ON tc_tasks BEGIN INSERT OR REPLACE INTO tc_tasks_data - (id, user_id, data, entry_at, status, description, priority, + (id, short_id, user_id, data, entry_at, status, description, priority, modified_at, due_at, scheduled_at, start_at, end_at, wait_at, parent_id, position, project_id) - VALUES (NEW.id, NEW.user_id, COALESCE(NEW.data, '{}'), NEW.entry_at, - NEW.status, NEW.description, NEW.priority, NEW.modified_at, - NEW.due_at, NEW.scheduled_at, NEW.start_at, NEW.end_at, - NEW.wait_at, NEW.parent_id, NEW.position, NEW.project_id); + VALUES (NEW.id, NEW.short_id, NEW.user_id, COALESCE(NEW.data, '{}'), + NEW.entry_at, NEW.status, NEW.description, NEW.priority, + NEW.modified_at, NEW.due_at, NEW.scheduled_at, NEW.start_at, + NEW.end_at, NEW.wait_at, NEW.parent_id, NEW.position, + NEW.project_id); END; CREATE TRIGGER IF NOT EXISTS tc_tasks_update INSTEAD OF UPDATE ON tc_tasks BEGIN UPDATE tc_tasks_data SET + short_id = NEW.short_id, user_id = NEW.user_id, data = COALESCE(NEW.data, '{}'), entry_at = NEW.entry_at, status = NEW.status, description = NEW.description, priority = NEW.priority, diff --git a/test/short_id.test.py b/test/short_id.test.py new file mode 100644 index 000000000..dcfad4832 --- /dev/null +++ b/test/short_id.test.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +############################################################################### +# +# Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, modify, merge, publish, distribute, sublicense, and/or sell copies of +# the Software, and to permit persons to whom the Software is furnished to do +# so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# +# https://www.opensource.org/licenses/mit-license.php +# +############################################################################### + +import os +import sqlite3 +import sys +import unittest + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from basetest import Task, TestCase + + +SHORT_ID_UUID = "aaaaaaaa-1111-4111-8111-111111111111" +NUMERIC_PREFIX_UUID = "12345678-2222-4222-8222-222222222222" + + +class TestShortID(TestCase): + def setUp(self): + self.t = Task() + self.t( + "import -", + input="""[ + {"description":"short-id target","entry":"1700000000","status":"pending","uuid":"aaaaaaaa-1111-4111-8111-111111111111"}, + {"description":"numeric-prefix target","entry":"1700000000","status":"pending","uuid":"12345678-2222-4222-8222-222222222222"} + ]""", + ) + self._set_short_id(SHORT_ID_UUID, 12345678) + self._set_short_id(NUMERIC_PREFIX_UUID, 77) + + def _set_short_id(self, uuid, short_id): + conn = sqlite3.connect(self.t.db_path) + try: + conn.execute( + "UPDATE tc_tasks_data SET short_id = ? WHERE id = ?", + (short_id, uuid), + ) + conn.commit() + finally: + conn.close() + + def test_id_column_displays_short_id(self): + code, out, err = self.t( + "aaaaaaaa list rc.report.list.columns:id,description rc.report.list.labels:ID,Description" + ) + self.assertIn("12345678", out) + self.assertIn("short-id target", out) + + def test_numeric_short_id_filters_task(self): + code, out, err = self.t("12345678 export") + self.assertIn('"description":"short-id target"', out) + self.assertNotIn('"description":"numeric-prefix target"', out) + self.assertNotIn('"short_id"', out) + + def test_numeric_short_id_modifies_task(self): + self.t("12345678 modify priority:H") + code, out, err = self.t("aaaaaaaa export") + self.assertIn('"priority":"H"', out) + + def test_numeric_ambiguity_prefers_short_id_over_uuid_prefix(self): + code, out, err = self.t("12345678 info") + self.assertIn("short-id target", out) + self.assertNotIn("numeric-prefix target", out) + + def test_uuid_prefix_falls_back_when_no_short_id_matches(self): + code, out, err = self.t("aaaaaaaa export") + self.assertIn('"description":"short-id target"', out) + + +if __name__ == "__main__": + from simpletap import TAPTestRunner + + unittest.main(testRunner=TAPTestRunner()) + +# vim: ai sts=4 et sw=4 ft=python From 7df5ced727cbfa3faf193b314350d128045f2a8a Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 21 Jun 2026 13:39:48 +0800 Subject: [PATCH 2/3] fix(task): preserve short id lookup errors --- src/TDB2.cpp | 16 ++++++---------- src/taskchampion-cpp/src/lib.rs | 15 +++++++-------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/src/TDB2.cpp b/src/TDB2.cpp index 70414f1ba..0dba277bc 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -350,16 +350,12 @@ bool TDB2::get(const std::string& uuid, Task& task) { // Numeric task refs are per-user short IDs. Prefer them over numeric UUID // prefixes; if no short ID matches, keep the historical UUID-prefix fallback. if (looksLikeShortId(uuid)) { - try { - auto resolved = replica()->resolve_task_ref(uuid); - auto maybe = replica()->get_task_data(resolved); - if (maybe.is_some()) { - auto tctask = maybe.take(); - task = Task{std::move(tctask)}; - apply_depmap(task, *depmap); - return true; - } - } catch (...) { + auto maybe = replica()->get_task_data_by_ref(uuid); + if (maybe.is_some()) { + auto tctask = maybe.take(); + task = Task{std::move(tctask)}; + apply_depmap(task, *depmap); + return true; } } diff --git a/src/taskchampion-cpp/src/lib.rs b/src/taskchampion-cpp/src/lib.rs index 0b53ff01b..0a6241c70 100644 --- a/src/taskchampion-cpp/src/lib.rs +++ b/src/taskchampion-cpp/src/lib.rs @@ -137,8 +137,8 @@ mod ffi { /// Get an existing task by its UUID. fn get_task_data(&mut self, uuid: Uuid) -> Result; - /// Resolve a full UUID or numeric short ID to a task UUID. - fn resolve_task_ref(&mut self, task_ref: String) -> Result; + /// Get an existing task by its full UUID or numeric short ID. + fn get_task_data_by_ref(&mut self, task_ref: String) -> Result; /// Get the operations for a task task by its UUID. fn get_task_operations(&mut self, uuid: Uuid) -> Result>; @@ -699,13 +699,12 @@ impl Replica { rt().block_on(async { Ok(self.0.get_task_data(uuid.into()).await?.into()) }) } - fn resolve_task_ref(&mut self, task_ref: String) -> Result { + fn get_task_data_by_ref(&mut self, task_ref: String) -> Result { rt().block_on(async { - self.0 - .resolve_task_ref(&task_ref) - .await? - .map(ffi::Uuid::from) - .ok_or_else(|| CppError::from(anyhow::anyhow!("task reference not found"))) + let Some(uuid) = self.0.resolve_task_ref(&task_ref).await? else { + return Ok(None.into()); + }; + Ok(self.0.get_task_data(uuid).await?.into()) }) } From 8a0114471af2ba61ac386959ca514423a31b9658 Mon Sep 17 00:00:00 2001 From: neil Date: Sun, 21 Jun 2026 14:00:01 +0800 Subject: [PATCH 3/3] refactor(task): share task ref predicates --- src/CLI2.cpp | 27 ++++----------------- src/TDB2.cpp | 10 ++------ src/TaskRef.h | 56 +++++++++++++++++++++++++++++++++++++++++++ src/columns/ColID.cpp | 2 +- 4 files changed, 64 insertions(+), 31 deletions(-) create mode 100644 src/TaskRef.h diff --git a/src/CLI2.cpp b/src/CLI2.cpp index 2a28a51a6..bc28585b9 100644 --- a/src/CLI2.cpp +++ b/src/CLI2.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -1293,24 +1294,6 @@ void CLI2::desugarFilterPatterns() { // a single prefix: a1b2c3d4 // a comma-separated list: a1b2c3d4,e5f6a7b8 // -static bool looksLikeHexPrefix(const std::string& s) { - if (s.length() != 8) return false; - for (char c : s) - if (!std::isxdigit(static_cast(c))) return false; - return true; -} - -static bool looksLikeNumericShortId(const std::string& s) { - if (s.empty()) return false; - for (char c : s) - if (!std::isdigit(static_cast(c))) return false; - return true; -} - -static bool looksLikeTaskRef(const std::string& s) { - return looksLikeNumericShortId(s) || looksLikeHexPrefix(s); -} - // Pushes all task-ref elements from a comma-separated string into _uuid_list. // Returns true if any were added. static bool pushHexPrefixesFromSet(const std::string& raw, @@ -1318,7 +1301,7 @@ static bool pushHexPrefixesFromSet(const std::string& raw, auto elements = split(raw, ','); bool any = false; for (auto& element : elements) { - if (looksLikeTaskRef(element)) { + if (taskref::looksLikeTaskRef(element)) { uuid_list.push_back(element); any = true; } @@ -1344,7 +1327,7 @@ void CLI2::findIDs() { a._lextype == Lexer::Type::identifier || a._lextype == Lexer::Type::number); if (isWordOrIdent && !previousFilterArgWasAnOperator && - looksLikeTaskRef(raw)) { + taskref::looksLikeTaskRef(raw)) { changes = true; _uuid_list.push_back(raw); } else if (a._lextype == Lexer::Type::set) { @@ -1368,7 +1351,7 @@ void CLI2::findIDs() { if ((a._lextype == Lexer::Type::word || a._lextype == Lexer::Type::identifier || a._lextype == Lexer::Type::number) && - looksLikeTaskRef(raw)) { + taskref::looksLikeTaskRef(raw)) { changes = true; a.unTag("MODIFICATION"); a.tag("FILTER"); @@ -1514,7 +1497,7 @@ void CLI2::insertIDExpr() { if (u != _uuid_list.begin()) reconstructed.push_back(opOr); reconstructed.push_back(openParen); - A2 argUUID(looksLikeNumericShortId(*u) ? "id" : "uuid", Lexer::Type::dom); + A2 argUUID(taskref::looksLikeNumericShortId(*u) ? "id" : "uuid", Lexer::Type::dom); argUUID.tag("FILTER"); reconstructed.push_back(argUUID); reconstructed.push_back(opSimilar); diff --git a/src/TDB2.cpp b/src/TDB2.cpp index 0dba277bc..2d345f69f 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -63,13 +64,6 @@ bool looksLikeFullUuid(const std::string& s) { return true; } -bool looksLikeShortId(const std::string& s) { - if (s.empty()) return false; - for (char c : s) - if (!std::isdigit(static_cast(c))) return false; - return true; -} - void apply_depmap(Task& t, tc::DependencyMapWrapper& depmap) { auto uuid_str = t.get("uuid"); if (!looksLikeFullUuid(uuid_str)) return; @@ -349,7 +343,7 @@ bool TDB2::get(const std::string& uuid, Task& task) { // Numeric task refs are per-user short IDs. Prefer them over numeric UUID // prefixes; if no short ID matches, keep the historical UUID-prefix fallback. - if (looksLikeShortId(uuid)) { + if (taskref::looksLikeNumericShortId(uuid)) { auto maybe = replica()->get_task_data_by_ref(uuid); if (maybe.is_some()) { auto tctask = maybe.take(); diff --git a/src/TaskRef.h b/src/TaskRef.h new file mode 100644 index 000000000..f263a7f47 --- /dev/null +++ b/src/TaskRef.h @@ -0,0 +1,56 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// https://www.opensource.org/licenses/mit-license.php +// +//////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDED_TASK_REF +#define INCLUDED_TASK_REF + +#include +#include + +namespace taskref { + +inline bool looksLikeHexPrefix(const std::string& s) { + if (s.length() != 8) return false; + for (char c : s) + if (!std::isxdigit(static_cast(c))) return false; + return true; +} + +inline bool looksLikeNumericShortId(const std::string& s) { + if (s.empty()) return false; + for (char c : s) + if (!std::isdigit(static_cast(c))) return false; + return true; +} + +inline bool looksLikeTaskRef(const std::string& s) { + return looksLikeNumericShortId(s) || looksLikeHexPrefix(s); +} + +} // namespace taskref + +#endif +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColID.cpp b/src/columns/ColID.cpp index a0f2a8f7b..6b1a1f79a 100644 --- a/src/columns/ColID.cpp +++ b/src/columns/ColID.cpp @@ -36,7 +36,7 @@ ColumnID::ColumnID() { _label = "ID"; _modifiable = false; _styles = {"short"}; - _examples = {"42"}; + _examples = {"42", "a1b2c3d4"}; } ////////////////////////////////////////////////////////////////////////////////