opys_engine/commands/
block.rs1use crate::commands::{expand_ids, for_each_id, maybe_sync, touch};
10use crate::error::{usage, OpysError, Result};
11use crate::frontmatter::Frontmatter;
12use crate::project::Project;
13use crate::refs;
14use crate::store::Store;
15use crate::Ctx;
16
17pub fn block_core(prj: &Project, store: &mut Store, id: &str, by: &str) -> Result<()> {
20 if id == by {
21 return Err(usage("an item cannot block itself"));
22 }
23 let id_title = store
24 .title_of(id)?
25 .ok_or_else(|| OpysError::NotFound { id: id.to_string() })?;
26 let by_title = store
27 .title_of(by)?
28 .ok_or_else(|| OpysError::NotFound { id: by.to_string() })?;
29
30 let id_blockable = has_status(prj, id, "blocked");
31 edit_doc(prj, store, id, true, |fm| {
32 let mut changed = refs::add_to_map(fm, refs::BLOCKED_BY, by, &by_title);
33 if id_blockable && fm.status() != Some("blocked") {
34 fm.set_str("status", "blocked");
35 changed = true;
36 }
37 if changed {
38 touch(fm);
39 }
40 changed
41 })?;
42 edit_doc(prj, store, by, false, |fm| {
45 refs::add_to_map(fm, refs::BLOCKS, id, &id_title)
46 })?;
47 Ok(())
48}
49
50pub fn block(ctx: &Ctx, ids: &str, by: &str) -> Result<()> {
52 let prj = ctx.open()?;
53 let ids = expand_ids(ids)?;
54 let (mut store, _) = ctx.load(&prj)?;
55 let res = for_each_id(&ids, |id| {
56 block_core(&prj, &mut store, id, by)?;
57 println!("{id} blocked by {by}");
58 Ok(())
59 });
60 ctx.flush(&prj, store)?;
61 maybe_sync(ctx, &prj);
62 res
63}
64
65pub fn unblock_core(prj: &Project, store: &mut Store, id: &str, by: &str) -> Result<()> {
70 if store.dkey_opt(id)?.is_none() {
71 return Err(OpysError::NotFound { id: id.to_string() });
72 }
73 let linked = store.has_relation(id, refs::BLOCKED_BY, by)?
74 || store.has_relation(by, refs::BLOCKS, id)?;
75 if !linked {
76 return Err(usage(format!("no blocker '{by}' recorded on '{id}'")));
77 }
78
79 let id_blockable = has_status(prj, id, "blocked") && has_status(prj, id, "in-progress");
80 edit_doc(prj, store, id, true, |fm| {
81 let mut changed = refs::remove_from_map(fm, refs::BLOCKED_BY, by);
82 if id_blockable
83 && fm.status() == Some("blocked")
84 && refs::parse_in(fm, refs::BLOCKED_BY).is_empty()
85 && fm.get_str("blocked_reason").is_none()
86 {
87 fm.set_str("status", "in-progress");
88 changed = true;
89 }
90 if changed {
91 touch(fm);
92 }
93 changed
94 })?;
95 match edit_doc(prj, store, by, false, |fm| {
98 refs::remove_from_map(fm, refs::BLOCKS, id)
99 }) {
100 Ok(()) | Err(OpysError::NotFound { .. }) => {}
101 Err(e) => return Err(e),
102 }
103 Ok(())
104}
105
106pub fn unblock(ctx: &Ctx, ids: &str, by: &str) -> Result<()> {
108 let prj = ctx.open()?;
109 let ids = expand_ids(ids)?;
110 let (mut store, _) = ctx.load(&prj)?;
111 let res = for_each_id(&ids, |id| {
112 unblock_core(&prj, &mut store, id, by)?;
113 println!("{id} no longer blocked by {by}");
114 Ok(())
115 });
116 ctx.flush(&prj, store)?;
117 maybe_sync(ctx, &prj);
118 res
119}
120
121fn has_status(prj: &Project, id: &str, status: &str) -> bool {
123 prj.pcfg
124 .type_name_for_id(id)
125 .map(|n| prj.pcfg.types[n].statuses.iter().any(|s| s == status))
126 .unwrap_or(false)
127}
128
129fn edit_doc<F>(prj: &Project, store: &mut Store, id: &str, relocate: bool, f: F) -> Result<()>
133where
134 F: FnOnce(&mut Frontmatter) -> bool,
135{
136 let dkey = store.dkey_of(id)?;
137 let mut doc = store.doc(dkey)?;
138 if f(&mut doc.frontmatter) {
139 store.put_doc(&prj.pcfg, Some(dkey), &doc)?;
140 if relocate {
141 store.set_canonical_path(&prj.pcfg, dkey)?;
142 }
143 }
144 Ok(())
145}