A file-based task manager
0
fork

Configure Feed

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

Cargo fmt

+181 -148
+2 -6
readme
··· 56 56 57 57 tsk help 58 58 59 - tsk uses plain text files for all of its functionality. A workspace is a folder 60 - that contains a .tsk/ directory created with the `tsk init` command. The 61 - presence of a .tsk/ folder is searched recursively upwards until a filesystem 62 - boundary or root is encountered. This means you can nest workspaces and use 63 - folders to namespace tasks while also using tsk commands at any location within 64 - a workspace. 59 + tsk uses virtual text files tracked by special refs in a git repo for its 60 + functionality. Any git repo can be made into a tsk workspace. 65 61 66 62 New tasks are created with the `tsk push` command. A title is always required, 67 63 but can be modified later. A unique identifier is selected automatically and a
+23 -31
src/lib.rs
··· 1 1 pub mod errors; 2 2 mod fzf; 3 - mod namespace; 4 3 mod merge; 4 + mod namespace; 5 5 mod object; 6 6 mod patch; 7 - mod propvalue; 8 7 mod properties; 8 + mod propvalue; 9 9 mod queue; 10 10 mod task; 11 11 mod workspace; ··· 162 162 remote: Option<String>, 163 163 }, 164 164 /// Push tsk refs to a git remote (default: origin). 165 - GitPush { 166 - remote: Option<String>, 167 - }, 165 + GitPush { remote: Option<String> }, 168 166 /// Fetch tsk refs from a git remote (default: origin) and reconcile 169 167 /// divergent task histories. Default strategy is merge; pass --rebase 170 168 /// to replay local-only commits onto the remote tip instead. ··· 308 306 Current, 309 307 /// Switch active namespace. With no name, fzf-picks from existing 310 308 /// namespaces (plus a `<new>` sentinel for creating one on the fly). 311 - Switch { name: Option<String> }, 309 + Switch { 310 + name: Option<String>, 311 + }, 312 312 /// List every task bound in a namespace (defaults to active), 313 313 /// regardless of which queue (if any) it's on. One row per id. 314 - Tasks { name: Option<String> }, 314 + Tasks { 315 + name: Option<String>, 316 + }, 315 317 } 316 318 317 319 #[derive(Subcommand)] ··· 326 328 }, 327 329 /// Switch active queue. With no name, fzf-picks from existing queues 328 330 /// (plus a `<new>` sentinel for creating one on the fly). 329 - Switch { name: Option<String> }, 331 + Switch { 332 + name: Option<String>, 333 + }, 330 334 } 331 335 332 336 #[derive(Subcommand)] ··· 387 391 let picked: Option<String> = fzf::select(lines, ["--prompt=task> "])?; 388 392 let picked = picked.ok_or(errors::Error::NoTasks)?; 389 393 let id_str = picked.split('\t').next().unwrap_or(""); 390 - let id: Id = 391 - parse_id(id_str).map_err(|e| errors::Error::Parse(e.to_string()))?; 394 + let id: Id = parse_id(id_str).map_err(|e| errors::Error::Parse(e.to_string()))?; 392 395 Ok(TaskIdentifier::Id(id)) 393 396 } 394 397 } ··· 446 449 Commands::Swap => Workspace::from_path(dir)?.swap_top(), 447 450 Commands::Rot => Workspace::from_path(dir)?.rot(), 448 451 Commands::Tor => Workspace::from_path(dir)?.tor(), 449 - Commands::Prioritize { task_id } => { 450 - Workspace::from_path(dir)?.prioritize(task_id.into()) 451 - } 452 + Commands::Prioritize { task_id } => Workspace::from_path(dir)?.prioritize(task_id.into()), 452 453 Commands::Deprioritize { task_id } => { 453 454 Workspace::from_path(dir)?.deprioritize(task_id.into()) 454 455 } ··· 723 724 .filter(|q| q != &cur) 724 725 .collect(); 725 726 if candidates.is_empty() { 726 - return Err(errors::Error::Parse( 727 - "No other queues to assign to".into(), 728 - )); 727 + return Err(errors::Error::Parse("No other queues to assign to".into())); 729 728 } 730 729 fzf::select::<_, String, _>(candidates, ["--prompt=assign to> "])? 731 730 .ok_or_else(|| errors::Error::Parse("No queue selected".into())) ··· 926 925 PropAction::Find { key, value } => { 927 926 let key = match key { 928 927 Some(k) => k, 929 - None => fzf::select::<_, String, _>( 930 - ws.property_keys()?, 931 - ["--prompt=key> "], 932 - )? 933 - .ok_or_else(|| errors::Error::Parse("No key selected".into()))?, 928 + None => fzf::select::<_, String, _>(ws.property_keys()?, ["--prompt=key> "])? 929 + .ok_or_else(|| errors::Error::Parse("No key selected".into()))?, 934 930 }; 935 931 let value = match value { 936 932 Some(v) if v == "<any>" => None, ··· 938 934 None => { 939 935 let mut choices = ws.property_values(&key)?; 940 936 choices.insert(0, "<any>".to_string()); 941 - let picked = fzf::select::<_, String, _>( 942 - choices, 943 - ["--prompt=value> "], 944 - )? 945 - .ok_or_else(|| errors::Error::Parse("No value selected".into()))?; 937 + let picked = fzf::select::<_, String, _>(choices, ["--prompt=value> "])? 938 + .ok_or_else(|| errors::Error::Parse("No value selected".into()))?; 946 939 if picked == "<any>" { 947 940 None 948 941 } else { ··· 1073 1066 } 1074 1067 1075 1068 fn strip_picker_marker(s: &str) -> &str { 1076 - s.strip_prefix("* ").or_else(|| s.strip_prefix(" ")).unwrap_or(s) 1069 + s.strip_prefix("* ") 1070 + .or_else(|| s.strip_prefix(" ")) 1071 + .unwrap_or(s) 1077 1072 } 1078 1073 1079 1074 fn prompt_line(prompt: &str) -> Result<String> { ··· 1103 1098 1104 1099 #[test] 1105 1100 fn picker_marks_current_and_appends_sentinel() { 1106 - let entries = picker_entries( 1107 - &["alpha".to_string(), "tsk".to_string()], 1108 - "tsk", 1109 - ); 1101 + let entries = picker_entries(&["alpha".to_string(), "tsk".to_string()], "tsk"); 1110 1102 assert_eq!(entries, vec![" alpha", "* tsk", "<new>"]); 1111 1103 } 1112 1104
+22 -17
src/merge.rs
··· 88 88 } 89 89 for r in repo.references_glob(&format!("{fetched_tasks}*"))? { 90 90 let r = r?; 91 - if let Some(name) = r.name().and_then(|n| n.strip_prefix(fetched_tasks.as_str())) { 91 + if let Some(name) = r 92 + .name() 93 + .and_then(|n| n.strip_prefix(fetched_tasks.as_str())) 94 + { 92 95 stables.insert(name.to_string()); 93 96 } 94 97 } ··· 330 333 format!("rebase-bind {name}") 331 334 }; 332 335 let parents: Vec<&Commit> = vec![&local_commit, &remote_commit]; 333 - let new_oid = repo.commit( 334 - None, 335 - &sig, 336 - &sig, 337 - &msg, 338 - &repo.find_tree(tree_oid)?, 339 - &parents, 340 - )?; 336 + let new_oid = 337 + repo.commit(None, &sig, &sig, &msg, &repo.find_tree(tree_oid)?, &parents)?; 341 338 repo.reference(&namespace::refname(name), new_oid, true, &msg)?; 342 339 Ok(Some(NamespaceReconciliation { 343 340 namespace: name.to_string(), ··· 369 366 /// 370 367 /// `can_pull`: 3-way bool. Local change wins if it differs from base; 371 368 /// otherwise take remote. 372 - pub fn reconcile_queue_refs( 373 - repo: &Repository, 374 - remote: &str, 375 - ) -> Result<Vec<QueueReconciliation>> { 369 + pub fn reconcile_queue_refs(repo: &Repository, remote: &str) -> Result<Vec<QueueReconciliation>> { 376 370 let fetched = format!("{}queues/", fetched_prefix(remote)); 377 371 let mut names: BTreeSet<String> = BTreeSet::new(); 378 372 for r in repo.references_glob(&format!("{QUEUE_REF_PREFIX}*"))? { ··· 448 442 &parents, 449 443 )?; 450 444 repo.reference(&queue::refname(name), new_oid, true, "merge")?; 451 - Ok(Some(QueueReconciliation { name: name.to_string() })) 445 + Ok(Some(QueueReconciliation { 446 + name: name.to_string(), 447 + })) 452 448 } 453 449 } 454 450 } ··· 468 464 let in_remote = remote_set.contains(*s); 469 465 // present in base → kept iff neither side removed it. 470 466 // not in base → added by either side, keep. 471 - if in_base { in_local && in_remote } else { in_local || in_remote } 467 + if in_base { 468 + in_local && in_remote 469 + } else { 470 + in_local || in_remote 471 + } 472 472 }) 473 473 .collect(); 474 474 ··· 510 510 local.can_pull 511 511 }; 512 512 513 - Queue { index, can_pull, inbox } 513 + Queue { 514 + index, 515 + can_pull, 516 + inbox, 517 + } 514 518 } 515 519 516 520 /// After task refs are reconciled, copy every other fetched ref ··· 748 752 let content_oid = repo.blob(b"v0").unwrap(); 749 753 let mut tb = repo.treebuilder(None).unwrap(); 750 754 tb.insert("content", content_oid, 0o100644).unwrap(); 751 - tb.insert("title", repo.blob(b"v0").unwrap(), 0o100644).unwrap(); 755 + tb.insert("title", repo.blob(b"v0").unwrap(), 0o100644) 756 + .unwrap(); 752 757 tb.insert("status", repo.blob(b"open\n").unwrap(), 0o100644) 753 758 .unwrap(); 754 759 let tree_oid = tb.write().unwrap();
+2 -12
src/namespace.rs
··· 136 136 137 137 /// Allocate the next human id, insert the binding, and persist. Returns the 138 138 /// human id assigned. 139 - pub fn assign_id( 140 - repo: &Repository, 141 - name: &str, 142 - stable: StableId, 143 - message: &str, 144 - ) -> Result<u32> { 139 + pub fn assign_id(repo: &Repository, name: &str, stable: StableId, message: &str) -> Result<u32> { 145 140 let mut ns = read(repo, name)?; 146 141 let human = ns.next; 147 142 ns.next += 1; ··· 178 173 } 179 174 180 175 /// Existing human id for `stable` in `name`, or a freshly-assigned one. 181 - pub fn ensure_bound( 182 - repo: &Repository, 183 - name: &str, 184 - stable: StableId, 185 - message: &str, 186 - ) -> Result<u32> { 176 + pub fn ensure_bound(repo: &Repository, name: &str, stable: StableId, message: &str) -> Result<u32> { 187 177 match human_for(repo, name, &stable)? { 188 178 Some(h) => Ok(h), 189 179 None => assign_id(repo, name, stable, message),
+17 -13
src/object.rs
··· 99 99 let stable = StableId(content_oid.to_string()); 100 100 let tree_oid = build_tree(repo, content_oid, &task.properties)?; 101 101 let sig = signature(repo); 102 - let commit = repo.commit( 103 - None, 104 - &sig, 105 - &sig, 106 - message, 107 - &repo.find_tree(tree_oid)?, 108 - &[], 109 - )?; 102 + let commit = repo.commit(None, &sig, &sig, message, &repo.find_tree(tree_oid)?, &[])?; 110 103 repo.reference(&stable.refname(), commit, true, message)?; 111 104 Ok(stable) 112 105 } ··· 207 200 let dir = tempfile::tempdir().unwrap(); 208 201 let repo = init_repo(dir.path()); 209 202 let mut t = Task::new("Hello\n\nbody text"); 210 - t.properties 211 - .insert("priority".into(), vec!["high".into()]); 203 + t.properties.insert("priority".into(), vec!["high".into()]); 212 204 t.properties 213 205 .insert("tag".into(), vec!["alpha".into(), "beta".into()]); 214 206 let id = create(&repo, &t, "create").unwrap(); ··· 229 221 t2.content = "v2".into(); 230 222 update(&repo, &id, &t2, "edit").unwrap(); 231 223 // Two commits in the chain. 232 - let head = repo.find_reference(&id.refname()).unwrap().target().unwrap(); 224 + let head = repo 225 + .find_reference(&id.refname()) 226 + .unwrap() 227 + .target() 228 + .unwrap(); 233 229 let head_commit = repo.find_commit(head).unwrap(); 234 230 assert_eq!(head_commit.parent_count(), 1); 235 231 let read_back = read(&repo, &id).unwrap().unwrap(); ··· 242 238 let repo = init_repo(dir.path()); 243 239 let t = Task::new("same"); 244 240 let id = create(&repo, &t, "create").unwrap(); 245 - let head1 = repo.find_reference(&id.refname()).unwrap().target().unwrap(); 241 + let head1 = repo 242 + .find_reference(&id.refname()) 243 + .unwrap() 244 + .target() 245 + .unwrap(); 246 246 update(&repo, &id, &t, "noop").unwrap(); 247 - let head2 = repo.find_reference(&id.refname()).unwrap().target().unwrap(); 247 + let head2 = repo 248 + .find_reference(&id.refname()) 249 + .unwrap() 250 + .target() 251 + .unwrap(); 248 252 assert_eq!(head1, head2); 249 253 } 250 254
+19 -22
src/patch.rs
··· 57 57 pub bind: Option<(String, u32)>, 58 58 } 59 59 60 - pub fn export_task( 61 - repo: &Repository, 62 - stable: &StableId, 63 - opts: &ExportOpts, 64 - ) -> Result<String> { 60 + pub fn export_task(repo: &Repository, stable: &StableId, opts: &ExportOpts) -> Result<String> { 65 61 let r = repo.find_reference(&stable.refname())?; 66 - let tip = r.target().ok_or_else(|| Error::Parse("task ref empty".into()))?; 62 + let tip = r 63 + .target() 64 + .ok_or_else(|| Error::Parse("task ref empty".into()))?; 67 65 // Collect root → tip. 68 66 let mut chain: Vec<Oid> = Vec::new(); 69 67 let mut cur = Some(repo.find_commit(tip)?); ··· 139 137 writeln!( 140 138 out, 141 139 "X-Tsk-Parent: {}", 142 - parent.map(|o| o.to_string()).unwrap_or_else(|| "none".into()) 140 + parent 141 + .map(|o| o.to_string()) 142 + .unwrap_or_else(|| "none".into()) 143 143 ) 144 144 .unwrap(); 145 145 if let Some((ns, human)) = bind { ··· 163 163 } 164 164 let blob = entry.to_object(repo)?.peel_to_blob()?; 165 165 let bytes = blob.content(); 166 - let as_str = 167 - std::str::from_utf8(bytes).map_err(|e| Error::Parse(e.to_string()))?; 166 + let as_str = std::str::from_utf8(bytes).map_err(|e| Error::Parse(e.to_string()))?; 168 167 let mangled = mangle_from(as_str); 169 168 writeln!(out, "file: {name}").unwrap(); 170 169 writeln!(out, "size: {}", mangled.len()).unwrap(); ··· 286 285 // history records who applied the import while preserving authorship. 287 286 let author = Signature::new(&e.author_name, &e.author_email, &e.when)?; 288 287 let committer = crate::object::signature(repo); 289 - let parents: Vec<git2::Commit> = prev.into_iter().map(|o| repo.find_commit(o).unwrap()).collect(); 288 + let parents: Vec<git2::Commit> = prev 289 + .into_iter() 290 + .map(|o| repo.find_commit(o).unwrap()) 291 + .collect(); 290 292 let parent_refs: Vec<&git2::Commit> = parents.iter().collect(); 291 293 let commit_oid = repo.commit( 292 294 None, ··· 438 440 if rest.len() < size + 1 { 439 441 return Err(Error::Parse("truncated file body".into())); 440 442 } 441 - let mangled = std::str::from_utf8(&rest[..size]) 442 - .map_err(|e| Error::Parse(e.to_string()))?; 443 + let mangled = 444 + std::str::from_utf8(&rest[..size]).map_err(|e| Error::Parse(e.to_string()))?; 443 445 if rest[size] != b'\n' { 444 446 return Err(Error::Parse("missing newline after file body".into())); 445 447 } ··· 530 532 fn tamper_detected_via_stable_id_check() { 531 533 let dir = tempfile::tempdir().unwrap(); 532 534 let src = init_repo(dir.path()); 533 - let stable = 534 - object::create(&src, &Task::new("original content"), "create").unwrap(); 535 + let stable = object::create(&src, &Task::new("original content"), "create").unwrap(); 535 536 let mbox = export_task(&src, &stable, &ExportOpts { bind: None }).unwrap(); 536 537 // Flip the content body without updating the stable id header. 537 538 // Equal-length substitution so size-prefix parsing still aligns; only ··· 554 555 let s = "preamble\nFrom the desk of...\n>From me\nbody\n"; 555 556 let mangled = mangle_from(s); 556 557 assert_eq!( 557 - mangled, 558 - "preamble\n>From the desk of...\n>>From me\nbody\n", 558 + mangled, "preamble\n>From the desk of...\n>>From me\nbody\n", 559 559 "every ^>*From line gets one extra '>'", 560 560 ); 561 561 assert_eq!(unmangle_from(&mangled), s); ··· 607 607 // Alice creates → exports. Bob imports. 608 608 let alice_dir = tempfile::tempdir().unwrap(); 609 609 let alice_repo = init_repo_as(alice_dir.path(), "Alice", "a@x"); 610 - let stable = 611 - object::create(&alice_repo, &Task::new("from alice"), "create").unwrap(); 610 + let stable = object::create(&alice_repo, &Task::new("from alice"), "create").unwrap(); 612 611 let mbox = export_task(&alice_repo, &stable, &ExportOpts { bind: None }).unwrap(); 613 612 614 613 let bob_dir = tempfile::tempdir().unwrap(); ··· 631 630 // second commit authored by Bob. 632 631 let alice_dir = tempfile::tempdir().unwrap(); 633 632 let alice_repo = init_repo_as(alice_dir.path(), "Alice", "a@x"); 634 - let stable = 635 - object::create(&alice_repo, &Task::new("from alice"), "create").unwrap(); 636 - let alice_mbox = 637 - export_task(&alice_repo, &stable, &ExportOpts { bind: None }).unwrap(); 633 + let stable = object::create(&alice_repo, &Task::new("from alice"), "create").unwrap(); 634 + let alice_mbox = export_task(&alice_repo, &stable, &ExportOpts { bind: None }).unwrap(); 638 635 639 636 let bob_dir = tempfile::tempdir().unwrap(); 640 637 let bob_repo = init_repo_as(bob_dir.path(), "Bob", "b@x");
+8 -1
src/properties.rs
··· 181 181 let s1 = object::create(&repo, &object::Task::new("a"), "c").unwrap(); 182 182 let s2 = object::create(&repo, &object::Task::new("b"), "c").unwrap(); 183 183 set(&repo, "priority", &s1, &["high".into()], "x").unwrap(); 184 - set(&repo, "priority", &s2, &["low".into(), "medium".into()], "x").unwrap(); 184 + set( 185 + &repo, 186 + "priority", 187 + &s2, 188 + &["low".into(), "medium".into()], 189 + "x", 190 + ) 191 + .unwrap(); 185 192 let high = find(&repo, "priority", Some("high")).unwrap(); 186 193 assert_eq!(high, vec![s1.clone()]); 187 194 let any_priority = find(&repo, "priority", None).unwrap();
+7 -12
src/queue.rs
··· 58 58 } 59 59 60 60 pub fn read(repo: &Repository, name: &str) -> Result<Queue> { 61 - let Some(target) = repo.find_reference(&refname(name)).ok().and_then(|r| r.target()) else { 61 + let Some(target) = repo 62 + .find_reference(&refname(name)) 63 + .ok() 64 + .and_then(|r| r.target()) 65 + else { 62 66 return Ok(Queue::new(name)); 63 67 }; 64 68 read_at_commit(repo, name, target) ··· 97 101 98 102 pub fn build_tree(repo: &Repository, q: &Queue) -> Result<Oid> { 99 103 let mut tb = repo.treebuilder(None)?; 100 - let index_text: String = q 101 - .index 102 - .iter() 103 - .map(|s| format!("{}\n", s.0)) 104 - .collect(); 104 + let index_text: String = q.index.iter().map(|s| format!("{}\n", s.0)).collect(); 105 105 let index_oid = repo.blob(index_text.as_bytes())?; 106 106 tb.insert(INDEX_FILE, index_oid, 0o100644)?; 107 107 let cp = if q.can_pull { "true\n" } else { "false\n" }; ··· 169 169 write(repo, name, &q, message) 170 170 } 171 171 172 - pub fn push_bottom( 173 - repo: &Repository, 174 - name: &str, 175 - stable: StableId, 176 - message: &str, 177 - ) -> Result<()> { 172 + pub fn push_bottom(repo: &Repository, name: &str, stable: StableId, message: &str) -> Result<()> { 178 173 let mut q = read(repo, name)?; 179 174 q.index.retain(|s| s != &stable); 180 175 q.index.push(stable);
+59 -29
src/workspace.rs
··· 333 333 .insert(STATUS_KEY.into(), vec![STATUS_OPEN.into()]); 334 334 let stable = object::create(&repo, &obj, "create")?; 335 335 properties::reindex_task(&repo, &stable, &obj.properties)?; 336 - let human = 337 - namespace::assign_id(&repo, &active_ns, stable.clone(), "assign-id")?; 336 + let human = namespace::assign_id(&repo, &active_ns, stable.clone(), "assign-id")?; 338 337 return Ok(Self::make_task(Id(human), stable, obj)); 339 338 } 340 339 ··· 480 479 let by_stable = ns_reverse(&namespace::read(&repo, &self.namespace())?); 481 480 let mut out = Vec::new(); 482 481 for stable in properties::find(&repo, key, value)? { 483 - let Some(&human) = by_stable.get(&stable) else { continue }; 482 + let Some(&human) = by_stable.get(&stable) else { 483 + continue; 484 + }; 484 485 out.push((Id(human), stable.clone(), Self::title_for(&repo, &stable)?)); 485 486 } 486 487 Ok(out) ··· 500 501 let mut out = Vec::new(); 501 502 for stable in queue::read(&repo, &self.queue())?.index { 502 503 // Skip tasks not visible in the active namespace (different ns owns them). 503 - let Some(&human) = by_stable.get(&stable) else { continue }; 504 + let Some(&human) = by_stable.get(&stable) else { 505 + continue; 506 + }; 504 507 let title = Self::title_for(&repo, &stable)?; 505 - out.push(StackEntry { id: Id(human), stable, title }); 508 + out.push(StackEntry { 509 + id: Id(human), 510 + stable, 511 + title, 512 + }); 506 513 } 507 514 Ok(out) 508 515 } ··· 514 521 let mut out = Vec::new(); 515 522 for (human, stable) in namespace::read(&repo, name)?.mapping { 516 523 let title = Self::title_for(&repo, &stable)?; 517 - out.push(StackEntry { id: Id(human), stable, title }); 524 + out.push(StackEntry { 525 + id: Id(human), 526 + stable, 527 + title, 528 + }); 518 529 } 519 530 Ok(out) 520 531 } ··· 557 568 /// Export multiple tasks as a single concatenated mbox stream. 558 569 /// Each task's full commit chain is emitted in order; the importer 559 570 /// groups them back by stable id. 560 - pub fn export_tasks( 561 - &self, 562 - identifiers: &[TaskIdentifier], 563 - bind: bool, 564 - ) -> Result<String> { 571 + pub fn export_tasks(&self, identifiers: &[TaskIdentifier], bind: bool) -> Result<String> { 565 572 let repo = self.repo()?; 566 573 let mut out = String::new(); 567 574 for ident in identifiers { ··· 726 733 } 727 734 } 728 735 729 - Ok((queues_pruned, prop_orphans, ghost_bindings, orphan_queue_entries)) 736 + Ok(( 737 + queues_pruned, 738 + prop_orphans, 739 + ghost_bindings, 740 + orphan_queue_entries, 741 + )) 730 742 } 731 743 732 744 /// Flip a task back to `status=open` and push it to the top of the ··· 811 823 idx.push(stable); 812 824 } 813 825 }, 814 - if to_front { "prioritize" } else { "deprioritize" }, 826 + if to_front { 827 + "prioritize" 828 + } else { 829 + "deprioritize" 830 + }, 815 831 ) 816 832 } 817 833 ··· 886 902 .map(|(s, _)| s.to_string()) 887 903 .unwrap_or_else(|| key.clone()); 888 904 let title = Self::title_for(&repo, &stable)?; 889 - out.push(InboxItem { key, source_queue, stable, title }); 905 + out.push(InboxItem { 906 + key, 907 + source_queue, 908 + stable, 909 + title, 910 + }); 890 911 } 891 912 Ok(out) 892 913 } ··· 1063 1084 /// Refs to push after `reject_inbox`: the active queue (entry left the 1064 1085 /// inbox) and the source queue (entry was bounced back into its inbox). 1065 1086 pub fn refs_for_reject_inbox(&self, source_queue: &str) -> Vec<String> { 1066 - vec![ 1067 - queue::refname(&self.queue()), 1068 - queue::refname(source_queue), 1069 - ] 1087 + vec![queue::refname(&self.queue()), queue::refname(source_queue)] 1070 1088 } 1071 1089 1072 1090 /// Refs to fetch before listing the inbox: just the active queue. ··· 1102 1120 let namespaces = merge::reconcile_namespace_refs(&repo, remote)?; 1103 1121 let queues = merge::reconcile_queue_refs(&repo, remote)?; 1104 1122 merge::fast_forward_non_task_refs(&repo, remote)?; 1105 - Ok(merge::PullOutcome { tasks, namespaces, queues }) 1123 + Ok(merge::PullOutcome { 1124 + tasks, 1125 + namespaces, 1126 + queues, 1127 + }) 1106 1128 } 1107 1129 } 1108 1130 ··· 1261 1283 ws.switch_queue("review").unwrap(); 1262 1284 ws.reject_inbox(&assign_key).unwrap(); 1263 1285 let inbox_here = ws.list_inbox().unwrap(); 1264 - assert!(inbox_here.is_empty(), "rejected item must leave receiver inbox"); 1286 + assert!( 1287 + inbox_here.is_empty(), 1288 + "rejected item must leave receiver inbox" 1289 + ); 1265 1290 ws.switch_queue("tsk").unwrap(); 1266 1291 let returned = ws.list_inbox().unwrap(); 1267 1292 assert_eq!(returned.len(), 1, "rejected item must land in sender inbox"); ··· 1281 1306 let r = ws.pull_from_queue("private", TaskIdentifier::Id(id)); 1282 1307 assert!(r.is_err(), "pull from can-pull=false queue must fail"); 1283 1308 ws.create_queue("private", Some(true)).unwrap(); 1284 - let pulled = ws.pull_from_queue("private", TaskIdentifier::Id(id)).unwrap(); 1309 + let pulled = ws 1310 + .pull_from_queue("private", TaskIdentifier::Id(id)) 1311 + .unwrap(); 1285 1312 assert_eq!(pulled.0, id.0); 1286 1313 let stack = ws.read_stack().unwrap(); 1287 1314 assert_eq!(stack.len(), 1); ··· 1407 1434 ws.push_task(t).unwrap(); 1408 1435 1409 1436 // Index reflects the new open task. 1410 - let opens = ws 1411 - .find_by_property(STATUS_KEY, Some(STATUS_OPEN)) 1412 - .unwrap(); 1437 + let opens = ws.find_by_property(STATUS_KEY, Some(STATUS_OPEN)).unwrap(); 1413 1438 assert_eq!(opens.len(), 1); 1414 1439 assert_eq!(opens[0].0, id); 1415 1440 ··· 1422 1447 Some(&vec![STATUS_DONE.to_string()]) 1423 1448 ); 1424 1449 assert!(ws.read_stack().unwrap().is_empty()); 1425 - let dones = ws 1426 - .find_by_property(STATUS_KEY, Some(STATUS_DONE)) 1427 - .unwrap(); 1450 + let dones = ws.find_by_property(STATUS_KEY, Some(STATUS_DONE)).unwrap(); 1428 1451 assert_eq!(dones.len(), 1); 1429 1452 assert_eq!(dones[0].0, id); 1430 1453 } ··· 1516 1539 namespace::unassign_id(&repo, "tsk", id.0, "test-unbind").unwrap(); 1517 1540 // Same content again should re-bind into active ns with a new id. 1518 1541 let again = ws.new_task("legacy task".into(), "".into()).unwrap(); 1519 - assert_eq!(again.stable, stable, "stable id must match the orphaned ref"); 1542 + assert_eq!( 1543 + again.stable, stable, 1544 + "stable id must match the orphaned ref" 1545 + ); 1520 1546 assert_ne!( 1521 1547 again.id, id, 1522 1548 "rebinding allocates a fresh human id from `next`" ··· 1549 1575 assert_eq!(qe, 1, "orphan queue index entry dropped"); 1550 1576 assert!(repo.find_reference(&queue::refname("empty")).is_err()); 1551 1577 assert!(repo.find_reference(&properties::refname("ghost")).is_err()); 1552 - assert!(namespace::human_for(&repo, "tsk", &orphan).unwrap().is_none()); 1578 + assert!( 1579 + namespace::human_for(&repo, "tsk", &orphan) 1580 + .unwrap() 1581 + .is_none() 1582 + ); 1553 1583 1554 1584 // Idempotent: second pass changes nothing. 1555 1585 assert_eq!(ws.gc_refs().unwrap(), (0, 0, 0, 0));
+22 -5
tests/multi_user.rs
··· 48 48 49 49 fn make_clone(origin: &Path, dest: &Path, name: &str, email: &str) { 50 50 let _ = Command::new("git") 51 - .args(["clone", "-q", origin.to_str().unwrap(), dest.to_str().unwrap()]) 51 + .args([ 52 + "clone", 53 + "-q", 54 + origin.to_str().unwrap(), 55 + dest.to_str().unwrap(), 56 + ]) 52 57 .status() 53 58 .expect("git clone"); 54 59 git(dest, &["config", "user.name", name]); ··· 111 116 tsk_ok(&alice, &["git-pull"]); 112 117 tsk_ok(&alice, &["push", "needs review"]); 113 118 let assign_out = tsk_ok(&alice, &["assign", "review", "-R", ""]); 114 - assert!(assign_out.contains("Assigned to review"), "got {assign_out}"); 119 + assert!( 120 + assign_out.contains("Assigned to review"), 121 + "got {assign_out}" 122 + ); 115 123 tsk_ok(&alice, &["git-push"]); 116 124 117 125 // Bob switches to review, pulls, sees inbox. ··· 193 201 assert!(list.contains("tag\tbeta"), "beta survives: {list}"); 194 202 195 203 // Replace whole property. 196 - tsk_ok(&alice, &["prop", "set", "-T", "tsk-1", "priority", "medium"]); 204 + tsk_ok( 205 + &alice, 206 + &["prop", "set", "-T", "tsk-1", "priority", "medium"], 207 + ); 197 208 let list = tsk_ok(&alice, &["prop", "list", "-T", "tsk-1"]); 198 209 assert!(list.contains("priority\tmedium"), "got {list}"); 199 210 assert!(!list.contains("priority\thigh"), "got {list}"); ··· 285 296 // the task. 286 297 let listing = tsk_ok(&bob, &["prop", "list", "-T", "tsk-1"]); 287 298 eprintln!("LISTING: {listing}"); 288 - assert!(listing.contains("priority\thigh"), "alice's edit lost: {listing}"); 299 + assert!( 300 + listing.contains("priority\thigh"), 301 + "alice's edit lost: {listing}" 302 + ); 289 303 assert!(listing.contains("owner\tbob"), "bob's edit lost: {listing}"); 290 304 } 291 305 ··· 309 323 310 324 // Both edits survived. 311 325 let listing = tsk_ok(&bob, &["prop", "list", "-T", "tsk-1"]); 312 - assert!(listing.contains("priority\thigh"), "alice's edit lost: {listing}"); 326 + assert!( 327 + listing.contains("priority\thigh"), 328 + "alice's edit lost: {listing}" 329 + ); 313 330 assert!(listing.contains("owner\tbob"), "bob's edit lost: {listing}"); 314 331 } 315 332