A file-based task manager
0
fork

Configure Feed

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

Add tsk backfill-status for tasks created before auto-status

Walks the active namespace's mapping and sets status=open on any task
whose tree carries no status property yet. Tasks already marked done
are skipped, so re-running is a no-op.

Tests: workspace::backfill_status_marks_legacy_tasks_open_and_skips_done
covers the legacy + fresh-open + dropped mix and the idempotency
property.

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

+79
+8
src/lib.rs
··· 106 106 }, 107 107 /// Drop index entries whose stable ids no longer resolve. 108 108 Clean, 109 + /// One-shot: set status=open on every task in the active namespace that 110 + /// has no status property yet. Skips tasks already marked done. 111 + BackfillStatus, 109 112 /// Print refspec/setup hints for `git push`/`git fetch` to include `refs/tsk/*`. 110 113 GitSetup { 111 114 /// Configure push/fetch refspecs on the named remote (default: origin). ··· 312 315 Workspace::from_path(dir)?.deprioritize(task_id.into()) 313 316 } 314 317 Commands::Clean => Workspace::from_path(dir)?.clean(), 318 + Commands::BackfillStatus => { 319 + let n = Workspace::from_path(dir)?.backfill_status()?; 320 + println!("Set status=open on {n} task(s)"); 321 + Ok(()) 322 + } 315 323 Commands::GitSetup { remote } => { 316 324 let r = remote.unwrap_or_else(|| "origin".to_string()); 317 325 Workspace::from_path(dir)?.configure_git_remote_refspecs(&r)
+71
src/workspace.rs
··· 404 404 Ok(out) 405 405 } 406 406 407 + /// Set `status=open` on every task in the active namespace that has no 408 + /// status yet. Skips tasks already marked done. Returns the number of 409 + /// tasks updated. One-shot migration for tasks created before 410 + /// auto-status existed. 411 + pub fn backfill_status(&self) -> Result<usize> { 412 + let repo = self.repo()?; 413 + let ns = namespace::read(&repo, &self.namespace())?; 414 + let mut updated = 0usize; 415 + for (human, _stable) in ns.mapping.iter() { 416 + let mut task = self.task(TaskIdentifier::Id(Id(*human)))?; 417 + if task.attributes.contains_key(STATUS_KEY) { 418 + continue; 419 + } 420 + task.attributes 421 + .insert(STATUS_KEY.into(), vec![STATUS_OPEN.into()]); 422 + self.save_task(&task)?; 423 + updated += 1; 424 + } 425 + Ok(updated) 426 + } 427 + 407 428 /// Drop a task from the active queue and mark it `status=done`. The 408 429 /// namespace binding is kept so the task remains addressable by its 409 430 /// human id (and discoverable via `tsk prop find status done`); the ··· 817 838 assert_eq!(pulled.0, id.0); 818 839 let stack = ws.read_stack().unwrap(); 819 840 assert_eq!(stack.len(), 1); 841 + } 842 + 843 + #[test] 844 + fn backfill_status_marks_legacy_tasks_open_and_skips_done() { 845 + let (_d, ws) = fresh_workspace(); 846 + 847 + // Simulate a legacy task: bind a stable id with no status property. 848 + let repo = ws.repo().unwrap(); 849 + let raw = object::Task::new("legacy task"); 850 + let stable = object::create(&repo, &raw, "create").unwrap(); 851 + let h_legacy = 852 + namespace::assign_id(&repo, &ws.namespace(), stable.clone(), "assign").unwrap(); 853 + queue::push_top(&repo, &ws.queue(), stable, "push").unwrap(); 854 + 855 + // Plus a fresh task that already has status=open and a dropped one. 856 + let t_open = ws.new_task("fresh open".into(), "".into()).unwrap(); 857 + let id_open = t_open.id; 858 + ws.push_task(t_open).unwrap(); 859 + let t_done = ws.new_task("will drop".into(), "".into()).unwrap(); 860 + let id_done = t_done.id; 861 + ws.push_task(t_done).unwrap(); 862 + ws.drop(TaskIdentifier::Id(id_done)).unwrap(); 863 + 864 + // Pre-condition: the legacy task has no status. 865 + let read = ws.task(TaskIdentifier::Id(Id(h_legacy))).unwrap(); 866 + assert!(!read.attributes.contains_key(STATUS_KEY)); 867 + 868 + let n = ws.backfill_status().unwrap(); 869 + assert_eq!(n, 1, "only the legacy task gets backfilled"); 870 + 871 + // Legacy is now open, fresh-open stays open, dropped stays done. 872 + let read = ws.task(TaskIdentifier::Id(Id(h_legacy))).unwrap(); 873 + assert_eq!( 874 + read.attributes.get(STATUS_KEY), 875 + Some(&vec![STATUS_OPEN.to_string()]) 876 + ); 877 + let read = ws.task(TaskIdentifier::Id(id_open)).unwrap(); 878 + assert_eq!( 879 + read.attributes.get(STATUS_KEY), 880 + Some(&vec![STATUS_OPEN.to_string()]) 881 + ); 882 + let read = ws.task(TaskIdentifier::Id(id_done)).unwrap(); 883 + assert_eq!( 884 + read.attributes.get(STATUS_KEY), 885 + Some(&vec![STATUS_DONE.to_string()]) 886 + ); 887 + 888 + // Re-running is a no-op. 889 + let n = ws.backfill_status().unwrap(); 890 + assert_eq!(n, 0); 820 891 } 821 892 822 893 #[test]