A file-based task manager
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

save_task / object::update return Result<bool> ("did it write")

object::update was already idempotent — it short-circuits when the
proposed tree equals the current tip's. Surfacing that fact through
the return type lets migrate_property_encoding drop its before/after
ref-OID dance:

let head_before = repo.find_reference(...).target();
self.save_task(&task)?;
let head_after = repo.find_reference(...).target();
if head_before != head_after { rewritten += 1; }

becomes

if self.save_task(&task)? { rewritten += 1; }

The three property-mutation methods (add/set/unset_property) want
unit results, so they now `?;` the bool and return `Ok(())`. Tests
that called `ws.save_task(...).unwrap();` keep compiling unchanged
(the returned bool is discarded as a statement).

Net −4 src lines; 98 tests still green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+24 -28
+6 -5
src/object.rs
··· 112 112 Ok(stable) 113 113 } 114 114 115 - /// Append a new commit to a task's history. No-op when the resulting tree 116 - /// matches the parent's tree (idempotent saves). 117 - pub fn update(repo: &Repository, id: &StableId, task: &Task, message: &str) -> Result<()> { 115 + /// Append a new commit to a task's history. Returns `true` if a commit 116 + /// was actually written; `false` when the resulting tree matches the 117 + /// parent's (idempotent no-op). 118 + pub fn update(repo: &Repository, id: &StableId, task: &Task, message: &str) -> Result<bool> { 118 119 let content_oid = repo.blob(task.content.as_bytes())?; 119 120 let tree_oid = build_tree(repo, content_oid, task.title(), &task.properties)?; 120 121 let parent = repo ··· 125 126 if let Some(p) = &parent 126 127 && p.tree_id() == tree_oid 127 128 { 128 - return Ok(()); 129 + return Ok(false); 129 130 } 130 131 let sig = signature(repo); 131 132 let parents: Vec<&git2::Commit> = parent.iter().collect(); ··· 138 139 &parents, 139 140 )?; 140 141 repo.reference(&id.refname(), commit, true, message)?; 141 - Ok(()) 142 + Ok(true) 142 143 } 143 144 144 145 pub fn read(repo: &Repository, id: &StableId) -> Result<Option<Task>> {
+18 -23
src/workspace.rs
··· 328 328 Ok(Self::make_task(id, stable, obj)) 329 329 } 330 330 331 - pub fn save_task(&self, task: &Task) -> Result<()> { 331 + /// Persist any in-memory edits to a task. Returns `true` when the 332 + /// underlying `object::update` actually wrote a new commit (i.e. the 333 + /// resulting tree differs from the current tip); `false` on a no-op. 334 + pub fn save_task(&self, task: &Task) -> Result<bool> { 332 335 let repo = self.repo()?; 333 336 let content = if task.body.is_empty() { 334 337 task.title.trim().to_string() ··· 339 342 content, 340 343 properties: task.attributes.clone(), 341 344 }; 342 - object::update(&repo, &task.stable, &task_obj, "edit")?; 345 + let wrote = object::update(&repo, &task.stable, &task_obj, "edit")?; 343 346 properties::reindex_task(&repo, &task.stable, &task.attributes)?; 344 - Ok(()) 347 + Ok(wrote) 345 348 } 346 349 347 350 /// Append a value to a property on a task. If the value is already ··· 357 360 if !entry.iter().any(|v| v == value) { 358 361 entry.push(value.to_string()); 359 362 } 360 - self.save_task(&task) 363 + self.save_task(&task)?; 364 + Ok(()) 361 365 } 362 366 363 367 /// Replace the entire value list for a property. ··· 373 377 } else { 374 378 task.attributes.insert(key.to_string(), values); 375 379 } 376 - self.save_task(&task) 380 + self.save_task(&task)?; 381 + Ok(()) 377 382 } 378 383 379 384 /// Remove a single value from a property, or the whole property if ··· 398 403 } 399 404 } 400 405 } 401 - self.save_task(&task) 406 + self.save_task(&task)?; 407 + Ok(()) 402 408 } 403 409 404 410 pub fn property_keys(&self) -> Result<Vec<String>> { ··· 580 586 581 587 /// Re-save every task in the active namespace whose property blobs are 582 588 /// in the legacy line-split encoding, rewriting them as size-prefixed. 583 - /// Returns the number of tasks rewritten. Idempotent — already-migrated 584 - /// tasks have a tree that matches the rewrite, so `object::update` 585 - /// no-ops for them. 589 + /// Returns the number of tasks rewritten. Idempotent — `save_task` 590 + /// no-ops on already-migrated tasks. 586 591 pub fn migrate_property_encoding(&self) -> Result<usize> { 587 - let repo = self.repo()?; 588 - let ns = namespace::read(&repo, &self.namespace())?; 589 - let mut rewritten = 0usize; 590 - for (human, _stable) in ns.mapping.iter() { 592 + let ns = namespace::read(&self.repo()?, &self.namespace())?; 593 + let mut rewritten = 0; 594 + for human in ns.mapping.keys() { 591 595 let task = self.task(TaskIdentifier::Id(Id(*human)))?; 592 - let head_before = repo 593 - .find_reference(&task.stable.refname()) 594 - .ok() 595 - .and_then(|r| r.target()); 596 - self.save_task(&task)?; 597 - let head_after = repo 598 - .find_reference(&task.stable.refname()) 599 - .ok() 600 - .and_then(|r| r.target()); 601 - if head_before != head_after { 596 + if self.save_task(&task)? { 602 597 rewritten += 1; 603 598 } 604 599 }