1use libgrite_core::{
2 hash::compute_event_id,
3 lock::LockCheckResult,
4 store::IssueFilter,
5 types::event::{Event, EventKind, IssueState},
6 types::ids::{generate_issue_id, id_to_hex},
7 GriteError,
8};
9
10use crate::context::GriteContext;
11use crate::event_helper::insert_and_append;
12use crate::types::*;
13
14fn current_ts() -> u64 {
15 std::time::SystemTime::now()
16 .duration_since(std::time::UNIX_EPOCH)
17 .unwrap_or_default()
18 .as_millis() as u64
19}
20
21struct LockGuard<'a> {
23 ctx: &'a GriteContext,
24 resource: String,
25 acquired: bool,
26}
27
28impl<'a> LockGuard<'a> {
29 fn acquire(
30 ctx: &'a GriteContext,
31 issue_id_hex: &str,
32 should_lock: bool,
33 ) -> Result<Self, GriteError> {
34 let resource = format!("issue:{}", issue_id_hex);
35 if should_lock {
36 let lock_manager = ctx.open_lock_manager()?;
37 lock_manager
38 .acquire(&resource, &ctx.actor_id, None)
39 .map_err(|e| match e {
40 libgrite_git::GitError::LockConflict {
41 resource,
42 owner,
43 expires_in_ms,
44 } => GriteError::Conflict(format!(
45 "Cannot acquire lock on {} - held by {} (expires in {}s)",
46 resource,
47 owner,
48 expires_in_ms / 1000
49 )),
50 _ => GriteError::Internal(e.to_string()),
51 })?;
52 Ok(Self {
53 ctx,
54 resource,
55 acquired: true,
56 })
57 } else {
58 Ok(Self {
59 ctx,
60 resource,
61 acquired: false,
62 })
63 }
64 }
65}
66
67impl<'a> Drop for LockGuard<'a> {
68 fn drop(&mut self) {
69 if self.acquired {
70 if let Ok(lock_manager) = self.ctx.open_lock_manager() {
71 let _ = lock_manager.release(&self.resource, &self.ctx.actor_id);
72 }
73 }
74 }
75}
76
77pub fn issue_create(
79 ctx: &GriteContext,
80 opts: &IssueCreateOptions,
81) -> Result<IssueCreateResult, GriteError> {
82 match ctx.check_lock("repo:global")? {
84 LockCheckResult::Clear => {}
85 LockCheckResult::Warning(_) => {}
86 LockCheckResult::Blocked(_) => {
87 return Err(GriteError::Conflict(
88 "Repository is locked by another process".to_string(),
89 ));
90 }
91 }
92
93 let store = ctx.open_store()?;
94 let wal = ctx.open_wal()?;
95 let actor = ctx.actor_config.actor_id_bytes()?;
96
97 let issue_id = generate_issue_id();
98 let ts = current_ts();
99 let kind = EventKind::IssueCreated {
100 title: opts.title.clone(),
101 body: opts.body.clone(),
102 labels: opts.labels.clone(),
103 };
104 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
105 let event = Event::new(event_id, issue_id, actor, ts, None, kind);
106 let event = ctx.sign_event(event);
107
108 insert_and_append(&store, &wal, &actor, &event)?;
109
110 Ok(IssueCreateResult {
111 issue_id: id_to_hex(&issue_id),
112 event_id: id_to_hex(&event_id),
113 })
114}
115
116pub fn issue_list(
118 ctx: &GriteContext,
119 opts: &IssueListOptions,
120) -> Result<IssueListResult, GriteError> {
121 let store = ctx.open_store()?;
122
123 let state_filter = opts
124 .state
125 .as_ref()
126 .map(|s| match s.to_lowercase().as_str() {
127 "open" => IssueState::Open,
128 "closed" => IssueState::Closed,
129 _ => IssueState::Open,
130 });
131
132 let filter = IssueFilter {
133 state: state_filter,
134 label: opts.label.clone(),
135 };
136
137 let issues = store.list_issues(&filter)?;
138
139 Ok(IssueListResult { issues })
140}
141
142pub fn issue_show(
144 ctx: &GriteContext,
145 opts: &IssueShowOptions,
146) -> Result<IssueShowResult, GriteError> {
147 let store = ctx.open_store()?;
148
149 let issue_id = store.resolve_issue_id(&opts.issue_id)?;
150 let proj = store
151 .get_issue(&issue_id)?
152 .ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", opts.issue_id)))?;
153
154 let events = store.get_issue_events(&issue_id)?;
155
156 Ok(IssueShowResult {
157 issue: proj,
158 events,
159 })
160}
161
162pub fn issue_update(
164 ctx: &GriteContext,
165 opts: &IssueUpdateOptions,
166) -> Result<IssueUpdateResult, GriteError> {
167 if opts.title.is_none() && opts.body.is_none() {
168 return Err(GriteError::InvalidArgs(
169 "Either --title or --body must be provided".to_string(),
170 ));
171 }
172
173 let _guard = LockGuard::acquire(ctx, &opts.issue_id, opts.acquire_lock)?;
174
175 let store = ctx.open_store()?;
176 let wal = ctx.open_wal()?;
177 let actor = ctx.actor_config.actor_id_bytes()?;
178
179 let issue_id = store.resolve_issue_id(&opts.issue_id)?;
180 let _existing = store
181 .get_issue(&issue_id)?
182 .ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", opts.issue_id)))?;
183
184 let title = opts.title.clone();
185 let body = opts.body.clone();
186
187 let ts = current_ts();
188 let kind = EventKind::IssueUpdated { title, body };
189 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
190 let event = Event::new(event_id, issue_id, actor, ts, None, kind);
191 let event = ctx.sign_event(event);
192
193 insert_and_append(&store, &wal, &actor, &event)?;
194
195 Ok(IssueUpdateResult {
196 issue_id: id_to_hex(&issue_id),
197 event_id: id_to_hex(&event_id),
198 })
199}
200
201pub fn issue_comment(
203 ctx: &GriteContext,
204 opts: &IssueCommentOptions,
205) -> Result<IssueCommentResult, GriteError> {
206 let _guard = LockGuard::acquire(ctx, &opts.issue_id, opts.acquire_lock)?;
207
208 let store = ctx.open_store()?;
209 let wal = ctx.open_wal()?;
210 let actor = ctx.actor_config.actor_id_bytes()?;
211
212 let issue_id = store.resolve_issue_id(&opts.issue_id)?;
213 let _existing = store
214 .get_issue(&issue_id)?
215 .ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", opts.issue_id)))?;
216
217 let ts = current_ts();
218 let kind = EventKind::CommentAdded {
219 body: opts.body.clone(),
220 };
221 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
222 let event = Event::new(event_id, issue_id, actor, ts, None, kind);
223 let event = ctx.sign_event(event);
224
225 insert_and_append(&store, &wal, &actor, &event)?;
226
227 Ok(IssueCommentResult {
228 issue_id: id_to_hex(&issue_id),
229 event_id: id_to_hex(&event_id),
230 })
231}
232
233pub fn issue_close(
235 ctx: &GriteContext,
236 opts: &IssueStateOptions,
237) -> Result<IssueStateResult, GriteError> {
238 let _guard = LockGuard::acquire(ctx, &opts.issue_id, opts.acquire_lock)?;
239
240 let store = ctx.open_store()?;
241 let wal = ctx.open_wal()?;
242 let actor = ctx.actor_config.actor_id_bytes()?;
243
244 let issue_id = store.resolve_issue_id(&opts.issue_id)?;
245 let _existing = store
246 .get_issue(&issue_id)?
247 .ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", opts.issue_id)))?;
248
249 let ts = current_ts();
250 let kind = EventKind::StateChanged {
251 state: IssueState::Closed,
252 };
253 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
254 let event = Event::new(event_id, issue_id, actor, ts, None, kind);
255 let event = ctx.sign_event(event);
256
257 insert_and_append(&store, &wal, &actor, &event)?;
258
259 Ok(IssueStateResult {
260 issue_id: id_to_hex(&issue_id),
261 event_id: id_to_hex(&event_id),
262 action: "closed".to_string(),
263 })
264}
265
266pub fn issue_reopen(
268 ctx: &GriteContext,
269 opts: &IssueStateOptions,
270) -> Result<IssueStateResult, GriteError> {
271 let _guard = LockGuard::acquire(ctx, &opts.issue_id, opts.acquire_lock)?;
272
273 let store = ctx.open_store()?;
274 let wal = ctx.open_wal()?;
275 let actor = ctx.actor_config.actor_id_bytes()?;
276
277 let issue_id = store.resolve_issue_id(&opts.issue_id)?;
278 let _existing = store
279 .get_issue(&issue_id)?
280 .ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", opts.issue_id)))?;
281
282 let ts = current_ts();
283 let kind = EventKind::StateChanged {
284 state: IssueState::Open,
285 };
286 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
287 let event = Event::new(event_id, issue_id, actor, ts, None, kind);
288 let event = ctx.sign_event(event);
289
290 insert_and_append(&store, &wal, &actor, &event)?;
291
292 Ok(IssueStateResult {
293 issue_id: id_to_hex(&issue_id),
294 event_id: id_to_hex(&event_id),
295 action: "reopened".to_string(),
296 })
297}
298
299pub fn issue_label(
301 ctx: &GriteContext,
302 opts: &IssueLabelOptions,
303) -> Result<IssueLabelResult, GriteError> {
304 let _guard = LockGuard::acquire(ctx, &opts.issue_id, opts.acquire_lock)?;
305
306 let store = ctx.open_store()?;
307 let wal = ctx.open_wal()?;
308 let actor = ctx.actor_config.actor_id_bytes()?;
309
310 let issue_id = store.resolve_issue_id(&opts.issue_id)?;
311 let _existing = store
312 .get_issue(&issue_id)?
313 .ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", opts.issue_id)))?;
314
315 let ts = current_ts();
316 let kind = if !opts.add.is_empty() {
317 EventKind::LabelAdded {
318 label: opts.add[0].clone(),
319 }
320 } else if !opts.remove.is_empty() {
321 EventKind::LabelRemoved {
322 label: opts.remove[0].clone(),
323 }
324 } else {
325 return Err(GriteError::InvalidArgs(
326 "No labels to add or remove".to_string(),
327 ));
328 };
329
330 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
331 let event = Event::new(event_id, issue_id, actor, ts, None, kind);
332 let event = ctx.sign_event(event);
333
334 insert_and_append(&store, &wal, &actor, &event)?;
335
336 Ok(IssueLabelResult {
337 issue_id: id_to_hex(&issue_id),
338 event_id: id_to_hex(&event_id),
339 })
340}
341
342pub fn issue_assign(
344 ctx: &GriteContext,
345 opts: &IssueAssignOptions,
346) -> Result<IssueAssignResult, GriteError> {
347 let _guard = LockGuard::acquire(ctx, &opts.issue_id, opts.acquire_lock)?;
348
349 let store = ctx.open_store()?;
350 let wal = ctx.open_wal()?;
351 let actor = ctx.actor_config.actor_id_bytes()?;
352
353 let issue_id = store.resolve_issue_id(&opts.issue_id)?;
354 let _existing = store
355 .get_issue(&issue_id)?
356 .ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", opts.issue_id)))?;
357
358 let ts = current_ts();
359 let kind = if !opts.add.is_empty() {
360 EventKind::AssigneeAdded {
361 user: opts.add[0].clone(),
362 }
363 } else if !opts.remove.is_empty() {
364 EventKind::AssigneeRemoved {
365 user: opts.remove[0].clone(),
366 }
367 } else {
368 return Err(GriteError::InvalidArgs(
369 "No assignees to add or remove".to_string(),
370 ));
371 };
372
373 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
374 let event = Event::new(event_id, issue_id, actor, ts, None, kind);
375 let event = ctx.sign_event(event);
376
377 insert_and_append(&store, &wal, &actor, &event)?;
378
379 Ok(IssueAssignResult {
380 issue_id: id_to_hex(&issue_id),
381 event_id: id_to_hex(&event_id),
382 })
383}
384
385pub fn issue_link(
387 ctx: &GriteContext,
388 opts: &IssueLinkOptions,
389) -> Result<IssueLinkResult, GriteError> {
390 let _guard = LockGuard::acquire(ctx, &opts.issue_id, opts.acquire_lock)?;
391
392 let store = ctx.open_store()?;
393 let wal = ctx.open_wal()?;
394 let actor = ctx.actor_config.actor_id_bytes()?;
395
396 let issue_id = store.resolve_issue_id(&opts.issue_id)?;
397 let _existing = store
398 .get_issue(&issue_id)?
399 .ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", opts.issue_id)))?;
400
401 let ts = current_ts();
402 let kind = EventKind::LinkAdded {
403 url: opts.url.clone(),
404 note: opts.note.clone(),
405 };
406 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
407 let event = Event::new(event_id, issue_id, actor, ts, None, kind);
408 let event = ctx.sign_event(event);
409
410 insert_and_append(&store, &wal, &actor, &event)?;
411
412 Ok(IssueLinkResult {
413 issue_id: id_to_hex(&issue_id),
414 event_id: id_to_hex(&event_id),
415 })
416}
417
418pub fn issue_attach(
420 ctx: &GriteContext,
421 opts: &IssueAttachOptions,
422) -> Result<IssueAttachResult, GriteError> {
423 let _guard = LockGuard::acquire(ctx, &opts.issue_id, opts.acquire_lock)?;
424
425 let store = ctx.open_store()?;
426 let wal = ctx.open_wal()?;
427 let actor = ctx.actor_config.actor_id_bytes()?;
428
429 let issue_id = store.resolve_issue_id(&opts.issue_id)?;
430 let _existing = store
431 .get_issue(&issue_id)?
432 .ok_or_else(|| GriteError::NotFound(format!("Issue {} not found", opts.issue_id)))?;
433
434 let ts = current_ts();
435 let sha256_bytes = hex::decode(&opts.sha256)
436 .map_err(|e| GriteError::InvalidArgs(format!("Invalid sha256 hex: {}", e)))?
437 .try_into()
438 .map_err(|_| GriteError::InvalidArgs("sha256 must be 32 bytes".to_string()))?;
439 let kind = EventKind::AttachmentAdded {
440 name: opts.name.clone(),
441 sha256: sha256_bytes,
442 mime: opts.mime.clone(),
443 };
444 let event_id = compute_event_id(&issue_id, &actor, ts, None, &kind);
445 let event = Event::new(event_id, issue_id, actor, ts, None, kind);
446 let event = ctx.sign_event(event);
447
448 insert_and_append(&store, &wal, &actor, &event)?;
449
450 Ok(IssueAttachResult {
451 issue_id: id_to_hex(&issue_id),
452 event_id: id_to_hex(&event_id),
453 })
454}