oxi/cli/commands/
issue.rs1use crate::cli::IssueCommands;
4use anyhow::Result;
5
6pub async fn handle_issue(action: &IssueCommands) -> Result<()> {
9 use crate::store::issues::{IssueFilter, Priority, Status};
10 use crate::tools::format_issue_full;
11
12 let cwd = std::env::current_dir()?;
13
14 if let IssueCommands::Reap = action {
18 let dir = crate::store::issues::issues_dir(&cwd);
19 let removed = crate::store::issues::liveness::reap_orphans(&dir)
20 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
21 println!("reaped {removed} dead alive-lock file(s)");
22 return Ok(());
23 }
24
25 let store = crate::store::issues::FileIssueStore::open_from_cwd(&cwd)?;
26
27 match action {
28 IssueCommands::List { all, label, text } => {
29 let filter = IssueFilter {
30 status: if *all { None } else { Some(Status::Open) },
31 priority: None,
32 label: label.clone(),
33 assigned_to_session: None,
34 text: text.clone(),
35 };
36 let issues = store.list(&filter)?;
37 if issues.is_empty() {
38 println!("(no issues)");
39 } else {
40 for i in &issues {
41 println!("{}", crate::tools::format_issue_line(i));
42 }
43 }
44 }
45 IssueCommands::Show { id } => {
46 let (issue, hash) = store.read(*id)?;
47 println!("{}", format_issue_full(&issue, &hash));
48 }
49 IssueCommands::New {
50 title,
51 body,
52 priority,
53 labels,
54 } => {
55 let body = body.clone().unwrap_or_default();
56 let prio = match priority.as_deref() {
57 Some("low") => Priority::Low,
58 Some("medium") | None => Priority::Medium,
59 Some("high") => Priority::High,
60 Some("critical") => Priority::Critical,
61 Some(other) => anyhow::bail!("invalid priority: {other}"),
62 };
63 let labels: Vec<String> = labels
64 .as_deref()
65 .map(|s| s.split(',').map(|l| l.trim().to_string()).collect())
66 .unwrap_or_default();
67 let issue = store.create(title.clone(), body, prio, labels, None)?;
68 println!("created issue #{}: {}", issue.meta.id, issue.meta.title);
69 }
70 IssueCommands::Close { id, hash } => {
71 let session = format!(
72 "cli-{}-{}",
73 std::process::id(),
74 std::time::SystemTime::now()
75 .duration_since(std::time::UNIX_EPOCH)
76 .map(|d| d.as_nanos())
77 .unwrap_or(0)
78 );
79 let _guard =
80 crate::store::issues::liveness::acquire(&store.issues_dir(), &session).ok();
81 let (issue, current_hash) = store.read(*id)?;
82 if let Some(a) = &issue.meta.assigned_to
83 && a.session != session
84 && crate::store::issues::liveness::is_session_alive(&store.issues_dir(), &a.session)
85 {
86 anyhow::bail!(
87 "issue #{id} is currently being worked on by session {} (since {}); cannot close from CLI",
88 a.session,
89 a.acquired_at,
90 );
91 }
92 let effective_hash = hash.clone().unwrap_or(current_hash);
93 store
94 .start(*id, &session, Some(effective_hash))
95 .await
96 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
97 let (_, fresh_hash) = store.read(*id)?;
98 let closed = store
99 .close(*id, &session, Some(fresh_hash))
100 .await
101 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
102 println!("closed issue #{}: {}", closed.meta.id, closed.meta.title);
103 }
104 IssueCommands::Reopen { id, hash } => {
105 let effective_hash = match hash.clone() {
106 Some(h) => h,
107 None => store.read(*id)?.1,
108 };
109 let reopened = store
110 .reopen(*id, Some(effective_hash))
111 .await
112 .map_err(|e| anyhow::anyhow!(e.to_string()))?;
113 println!(
114 "reopened issue #{}: {}",
115 reopened.meta.id, reopened.meta.title
116 );
117 }
118 IssueCommands::Reap => unreachable!("reap handled before store open"),
119 }
120 Ok(())
121}