1use std::borrow::ToOwned;
2use std::collections::HashMap;
3use std::ops::Deref;
4use std::time::{SystemTime, UNIX_EPOCH};
5use git2::*;
6use serde_json;
7use serde::{Deserialize, Serialize};
8
9const NAME: &'static str = "name";
10const DESCRIPTION: &'static str = "description";
11const STATUS: &'static str = "status";
12const CREATED: &'static str = "created";
13
14#[derive(Clone, Serialize, Deserialize)]
15pub struct Task {
16 id: Option<String>,
17 props: HashMap<String, String>,
18 comments: Option<Vec<Comment>>,
19 labels: Option<Vec<Label>>,
20}
21
22#[derive(Clone, PartialEq, Serialize, Deserialize)]
23pub struct Comment {
24 id: Option<String>,
25 props: HashMap<String, String>,
26 text: String,
27}
28
29#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30pub struct Label {
31 name: String,
32 color: Option<String>,
33 description: Option<String>,
34}
35
36impl Task {
37 pub fn new(name: String, description: String, status: String) -> Result<Task, &'static str> {
38 if !name.is_empty() && !status.is_empty() {
39 Ok(Self::construct_task(name, description, status, None))
40 } else {
41 Err("Name or status is empty")
42 }
43 }
44
45 pub fn from_properties(id: String, mut props: HashMap<String, String>) -> Result<Task, &'static str> {
46 let name = props.get(NAME).unwrap_or(&"".to_owned()).to_owned();
47 let status = props.get(STATUS).unwrap_or(&"".to_owned()).to_owned();
48
49 if !name.is_empty() && !status.is_empty() {
50 if !props.contains_key("created") {
51 props.insert("created".to_string(), get_current_timestamp().to_string());
52 }
53
54 Ok(Task{ id: Some(id), props, comments: None, labels: None })
55 } else {
56 Err("Name or status is empty")
57 }
58 }
59
60 fn construct_task(name: String, description: String, status: String, created: Option<u64>) -> Task {
61 let mut props = HashMap::from([
62 (NAME.to_owned(), name),
63 (DESCRIPTION.to_owned(), description),
64 (STATUS.to_owned(), status),
65 (CREATED.to_owned(), created.unwrap_or(get_current_timestamp()).to_string()),
66 ]);
67
68 if let Ok(Some(current_user)) = get_current_user() {
69 props.insert("author".to_string(), current_user);
70 }
71
72 Task {
73 id: None,
74 props,
75 comments: None,
76 labels: None,
77 }
78 }
79
80 pub fn get_id(&self) -> Option<String> {
81 match &self.id {
82 Some(id) => Some(id.clone()),
83 _ => None
84 }
85 }
86
87 pub fn set_id(&mut self, id: String) {
88 self.id = Some(id);
89 }
90
91 pub fn get_property(&self, prop: &str) -> Option<&String> {
92 self.props.get(prop)
93 }
94
95 pub fn get_all_properties(&self) -> &HashMap<String, String> {
96 &self.props
97 }
98
99 pub fn set_property(&mut self, prop: &str, value: &str) {
100 self.props.insert(prop.to_string(), value.to_string());
101 }
102
103 pub fn has_property(&self, prop: &str) -> bool {
104 self.props.contains_key(prop)
105 }
106
107 pub fn delete_property(&mut self, prop: &str) -> bool {
108 self.props.remove(prop).is_some()
109 }
110
111 pub fn get_comments(&self) -> &Option<Vec<Comment>> {
112 &self.comments
113 }
114
115 pub fn add_comment(&mut self, id: Option<String>, mut props: HashMap<String, String>, text: String) -> Comment {
116 if self.comments.is_none() {
117 self.comments = Some(vec![]);
118 }
119
120 let id = Some(id.unwrap_or_else(|| (self.comments.as_ref().unwrap().len() + 1).to_string()));
121
122 if !props.contains_key("created") {
123 props.insert("created".to_string(), get_current_timestamp().to_string());
124 }
125
126 if !props.contains_key("author") {
127 if let Ok(Some(current_user)) = get_current_user() {
128 props.insert("author".to_string(), current_user);
129 }
130 }
131
132 let comment = Comment {
133 id,
134 props,
135 text,
136 };
137
138 self.comments.as_mut().unwrap().push(comment.clone());
139
140 comment
141 }
142
143 pub fn set_comments(&mut self, comments: Vec<Comment>) {
144 self.comments = Some(comments);
145 }
146
147 pub fn delete_comment(&mut self, id: &String) -> Result<(), String> {
148 if self.comments.is_none() {
149 return Err("Task has no comments".to_string());
150 }
151
152 let index = self.comments.as_ref().unwrap().iter().position(|comment| comment.get_id().unwrap() == id.deref());
153
154 if index.is_none() {
155 return Err(format!("Comment ID {id} not found"));
156 }
157
158 self.comments.as_mut().unwrap().remove(index.unwrap());
159
160 Ok(())
161 }
162
163 pub fn get_labels(&self) -> &Option<Vec<Label>> {
164 &self.labels
165 }
166
167 pub fn add_label(&mut self, name: String, description: Option<String>, color: Option<String>) -> Label {
168 if self.labels.is_none() {
169 self.labels = Some(vec![]);
170 }
171
172 let label = Label {
173 name: name.clone(),
174 description,
175 color,
176 };
177
178 self.labels.as_mut().unwrap().push(label.clone());
179
180 label
181 }
182
183 pub fn set_labels(&mut self, labels: Vec<Label>) {
184 self.labels = Some(labels);
185 }
186
187 pub fn delete_label(&mut self, name: &str) -> Result<(), String> {
188 if self.labels.is_none() {
189 return Err("Task has no labels".to_string());
190 }
191
192 let index = self.labels.as_ref().unwrap().iter().position(|label| label.name == name);
193
194 if index.is_none() {
195 return Err(format!("Label with name '{name}' not found"));
196 }
197
198 self.labels.as_mut().unwrap().remove(index.unwrap());
199
200 Ok(())
201 }
202
203 pub fn get_label_by_name(&self, name: &str) -> Option<&Label> {
204 self.labels
205 .as_ref()
206 .and_then(|labels| labels.iter().find(|label| label.name == name))
207 }
208}
209
210impl Comment {
211 pub fn new(id: String, props: HashMap<String, String>, text: String) -> Comment {
212 Comment {
213 id: Some(id),
214 props,
215 text,
216 }
217 }
218
219 pub fn get_id(&self) -> Option<String> {
220 match &self.id {
221 Some(id) => Some(id.clone()),
222 _ => None
223 }
224 }
225
226 pub fn set_id(&mut self, id: String) {
227 self.id = Some(id);
228 }
229
230 pub fn get_all_properties(&self) -> &HashMap<String, String> {
231 &self.props
232 }
233
234 pub fn get_text(&self) -> String {
235 self.text.to_string()
236 }
237
238 pub fn set_text(&mut self, text: String) {
239 self.text = text;
240 }
241}
242
243impl Label {
244 pub fn new(name: String, color: Option<String>, description: Option<String>) -> Label {
245 Label {
246 name, color, description
247 }
248 }
249
250 pub fn get_name(&self) -> String {
251 self.name.to_string()
252 }
253
254 pub fn get_color(&self) -> String {
255 self.color.clone().unwrap_or_else(|| String::from(""))
256 }
257
258 pub fn set_color(&mut self, color: String) {
259 self.color = Some(color);
260 }
261
262 pub fn get_description(&self) -> Option<String> {
263 self.description.clone()
264 }
265
266 pub fn set_description(&mut self, description: String) {
267 self.description = Some(description);
268 }
269}
270
271macro_rules! map_err {
272 ($expr:expr) => {
273 $expr.map_err(|e| e.message().to_owned())?
274 }
275}
276
277pub fn list_tasks() -> Result<Vec<Task>, String> {
278 let repo = map_err!(Repository::discover("."));
279 let task_ref = map_err!(repo.find_reference(&get_ref_path()));
280 let task_tree = map_err!(task_ref.peel_to_tree());
281
282 let mut result = vec![];
283
284 let _ = map_err!(task_tree.walk(TreeWalkMode::PreOrder, |_, entry| {
285 let oid = entry.id();
286 let blob = repo.find_blob(oid).unwrap();
287 let content = blob.content();
288
289 let task = serde_json::from_slice(content).unwrap();
290 result.push(task);
291
292 TreeWalkResult::Ok
293 }));
294
295 Ok(result)
296}
297
298pub fn find_task(id: &str) -> Result<Option<Task>, String> {
299 let repo = map_err!(Repository::discover("."));
300 let task_ref = repo.find_reference(&get_ref_path());
301 match task_ref {
302 Ok(task_ref) => {
303 let task_tree = map_err!(task_ref.peel_to_tree());
304 let result = match task_tree.get_name(id) {
305 Some(entry) => {
306 let oid = entry.id();
307 let blob = map_err!(repo.find_blob(oid));
308 let content = blob.content();
309 let task = serde_json::from_slice(content).unwrap();
310
311 Some(task)
312 },
313 None => None,
314 };
315
316 Ok(result)
317 },
318 Err(_) => Ok(None)
319 }
320}
321
322pub fn delete_tasks(ids: &[&str]) -> Result<(), String> {
323 let repo = map_err!(Repository::discover("."));
324 let task_ref = map_err!(repo.find_reference(&get_ref_path()));
325 let task_tree = map_err!(task_ref.peel_to_tree());
326
327 let mut treebuilder = map_err!(repo.treebuilder(Some(&task_tree)));
328 for id in ids {
329 map_err!(treebuilder.remove(id));
330 }
331 let tree_oid = map_err!(treebuilder.write());
332
333 let parent_commit = map_err!(task_ref.peel_to_commit());
334 let parents = vec![parent_commit];
335 let me = &map_err!(repo.signature());
336
337 let mut ids = ids.iter().map(|id| id.parse::<u64>().unwrap()).collect::<Vec<_>>();
338 ids.sort();
339 let ids = ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(", ");
340 map_err!(repo.commit(Some(&get_ref_path()), me, me, format!("Delete task {}", ids).as_str(), &map_err!(repo.find_tree(tree_oid)), &parents.iter().collect::<Vec<_>>()));
341
342 Ok(())
343}
344
345pub fn clear_tasks() -> Result<u64, String> {
346 let repo = map_err!(Repository::discover("."));
347 let task_ref = map_err!(repo.find_reference(&get_ref_path()));
348 let task_tree = map_err!(task_ref.peel_to_tree());
349
350 let mut treebuilder = map_err!(repo.treebuilder(Some(&task_tree)));
351 let task_count = treebuilder.len() as u64;
352 map_err!(treebuilder.clear());
353 let tree_oid = map_err!(treebuilder.write());
354
355 let parent_commit = map_err!(task_ref.peel_to_commit());
356 let parents = vec![parent_commit];
357 let me = &map_err!(repo.signature());
358
359 map_err!(repo.commit(Some(&get_ref_path()), me, me, "Clear tasks", &map_err!(repo.find_tree(tree_oid)), &parents.iter().collect::<Vec<_>>()));
360
361 Ok(task_count)
362}
363
364pub fn create_task(mut task: Task) -> Result<Task, String> {
365 let repo = map_err!(Repository::discover("."));
366 let task_ref_result = repo.find_reference(&get_ref_path());
367 let source_tree = match task_ref_result {
368 Ok(ref reference) => {
369 match reference.peel_to_tree() {
370 Ok(tree) => Some(tree),
371 _ => None
372 }
373 }
374 _ => { None }
375 };
376
377 if task.get_id().is_none() {
378 let id = get_next_id().unwrap_or_else(|_| "1".to_string());
379 task.set_id(id);
380 }
381 let string_content = serde_json::to_string(&task).unwrap();
382 let content = string_content.as_bytes();
383 let oid = map_err!(repo.blob(content));
384 let mut treebuilder = map_err!(repo.treebuilder(source_tree.as_ref()));
385 map_err!(treebuilder.insert(&task.get_id().unwrap(), oid, FileMode::Blob.into()));
386 let tree_oid = map_err!(treebuilder.write());
387
388 let me = &map_err!(repo.signature());
389 let mut parents = vec![];
390 if task_ref_result.is_ok() {
391 let parent_commit = map_err!(task_ref_result).peel_to_commit();
392 if parent_commit.is_ok() {
393 parents.push(map_err!(parent_commit));
394 }
395 }
396 map_err!(repo.commit(Some(&get_ref_path()), me, me, format!("Create task {}", &task.get_id().unwrap_or_else(|| String::from("?"))).as_str(), &map_err!(repo.find_tree(tree_oid)), &parents.iter().collect::<Vec<_>>()));
397
398 Ok(task)
399}
400
401pub fn update_task(task: Task) -> Result<String, String> {
402 let repo = map_err!(Repository::discover("."));
403 let task_ref_result = map_err!(repo.find_reference(&get_ref_path()));
404 let parent_commit = map_err!(task_ref_result.peel_to_commit());
405 let source_tree = map_err!(task_ref_result.peel_to_tree());
406 let string_content = serde_json::to_string(&task).unwrap();
407 let content = string_content.as_bytes();
408 let oid = map_err!(repo.blob(content));
409 let mut treebuilder = map_err!(repo.treebuilder(Some(&source_tree)));
410 map_err!(treebuilder.insert(&task.get_id().unwrap(), oid, FileMode::Blob.into()));
411 let tree_oid = map_err!(treebuilder.write());
412
413 let me = &map_err!(repo.signature());
414 let parents = vec![parent_commit];
415 map_err!(repo.commit(Some(&get_ref_path()), me, me, format!("Update task {}", &task.get_id().unwrap()).as_str(), &map_err!(repo.find_tree(tree_oid)), &parents.iter().collect::<Vec<_>>()));
416
417 Ok(task.get_id().unwrap())
418}
419
420fn get_next_id() -> Result<String, String> {
421 let repo = map_err!(Repository::discover("."));
422 let task_ref = map_err!(repo.find_reference(&get_ref_path()));
423 let task_tree = map_err!(task_ref.peel_to_tree());
424
425 let mut result = 0;
426
427 let _ = map_err!(task_tree.walk(TreeWalkMode::PreOrder, |_, entry| {
428 let entry_name = entry.name().unwrap();
429 match entry_name.parse::<i64>() {
430 Ok(id) => {
431 if id > result {
432 result = id;
433 }
434 },
435 _ => return TreeWalkResult::Skip
436 };
437
438 TreeWalkResult::Ok
439 }));
440
441 Ok((result + 1).to_string())
442}
443
444pub fn update_task_id(id: &str, new_id: &str) -> Result<(), String> {
445 let mut task = find_task(&id)?.unwrap();
446 task.set_id(new_id.to_string());
447 create_task(task)?;
448 delete_tasks(&[&id])?;
449
450 Ok(())
451}
452
453pub fn update_comment_id(task_id: &str, id: &str, new_id: &str) -> Result<(), String> {
454 let mut task = find_task(&task_id)?.unwrap().clone();
455 let comments = task.get_comments();
456 match comments {
457 Some(comments) => {
458 let updated_comments = comments.iter().map(|c| {
459 if c.get_id().unwrap() == id {
460 let mut c = c.clone();
461 c.set_id(new_id.to_string());
462 c
463 } else {
464 c.clone()
465 }
466 }).collect::<Vec<_>>();
467 task.set_comments(updated_comments);
468 update_task(task)?;
469 },
470 None => {}
471 }
472
473 Ok(())
474}
475
476pub fn list_remotes(remote: &Option<String>) -> Result<Vec<String>, String> {
477 let repo = map_err!(Repository::discover("."));
478 let remotes = map_err!(repo.remotes());
479 Ok(remotes.iter()
480 .filter(|s| remote.is_none() || remote.as_ref().unwrap().as_str() == s.unwrap())
481 .map(|s| repo.find_remote(s.unwrap()).unwrap().url().unwrap().to_owned())
482 .collect())
483}
484
485fn get_current_timestamp() -> u64 {
486 SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs()
487}
488
489fn get_current_user() -> Result<Option<String>, String> {
490 let repo = map_err!(Repository::discover("."));
491 let me = &map_err!(repo.signature());
492 match me.name() {
493 Some(name) => Ok(Some(String::from(name))),
494 _ => match me.email() {
495 Some(email) => Ok(Some(String::from(email))),
496 _ => Ok(None),
497 }
498 }
499}
500
501pub fn get_ref_path() -> String {
502 get_config_value("task.ref").unwrap_or_else(|_| "refs/tasks/tasks".to_string())
503}
504
505pub fn get_config_value(key: &str) -> Result<String, String> {
506 let repo = map_err!(Repository::discover("."));
507 let config = map_err!(repo.config());
508 Ok(map_err!(config.get_string(key)))
509}
510
511pub fn set_config_value(key: &str, value: &str) -> Result<(), String> {
512 let repo = map_err!(Repository::discover("."));
513 let mut config = map_err!(repo.config());
514 map_err!(config.set_str(key, value));
515 Ok(())
516}
517
518pub fn set_ref_path(ref_path: &str, move_ref: bool) -> Result<(), String> {
519 let repo = map_err!(Repository::discover("."));
520
521 let current_reference = repo.find_reference(&get_ref_path());
522 if let Ok(current_reference) = ¤t_reference {
523 let commit = map_err!(current_reference.peel_to_commit());
524 map_err!(repo.reference(ref_path, commit.id(), true, "task.ref migrated"));
525 }
526
527 let mut config = map_err!(repo.config());
528 map_err!(config.set_str("task.ref", ref_path));
529
530 if move_ref && current_reference.is_ok() {
531 map_err!(current_reference.unwrap().delete());
532 }
533
534 Ok(())
535}
536
537#[cfg(test)]
538mod test {
539 use std::collections::HashMap;
540 use crate::*;
541
542 #[test]
543 fn test_ref_path() {
544 let ref_path = get_ref_path();
545 assert!(set_ref_path("refs/heads/test-git-task", true).is_ok());
546 assert_eq!(get_ref_path(), "refs/heads/test-git-task");
547 assert!(set_ref_path(&ref_path, true).is_ok());
548 assert_eq!(get_ref_path(), ref_path);
549 }
550
551 #[test]
552 fn test_create_update_delete_task() {
553 let id = get_next_id().unwrap_or_else(|_| "1".to_string());
554 let task = Task::construct_task("Test task".to_string(), "Description goes here".to_string(), "OPEN".to_string(), Some(get_current_timestamp()));
555 let create_result = create_task(task);
556 assert!(create_result.is_ok());
557 let mut task = create_result.unwrap();
558 assert_eq!(task.get_id(), Some(id.clone()));
559 assert_eq!(task.get_property("name").unwrap(), "Test task");
560 assert_eq!(task.get_property("description").unwrap(), "Description goes here");
561 assert_eq!(task.get_property("status").unwrap(), "OPEN");
562 assert!(task.has_property("created"));
563
564 task.set_property("description", "Updated description");
565 let comment_props = HashMap::from([("author".to_string(), "Some developer".to_string())]);
566 task.add_comment(None, comment_props, "This is a comment".to_string());
567 task.set_property("custom_prop", "Custom content");
568 let update_result = update_task(task);
569 assert!(update_result.is_ok());
570 assert_eq!(update_result.unwrap(), id.clone());
571
572 let find_result = find_task(&id);
573 assert!(find_result.is_ok());
574 let task = find_result.unwrap();
575 assert!(task.is_some());
576 let task = task.unwrap();
577 assert_eq!(task.get_id(), Some(id.clone()));
578 assert_eq!(task.get_property("description").unwrap(), "Updated description");
579 let comments = task.get_comments().clone();
580 assert!(comments.is_some());
581 let comments = comments.unwrap();
582 assert_eq!(comments.len(), 1);
583 let comment = comments.first().unwrap();
584 assert_eq!(comment.get_text(), "This is a comment".to_string());
585 let comment_props = comment.clone().props;
586 assert_eq!(comment_props.get("author").unwrap(), &"Some developer".to_string());
587 assert_eq!(task.get_property("custom_prop").unwrap(), "Custom content");
588
589 let delete_result = delete_tasks(&[&id]);
590 assert!(delete_result.is_ok());
591
592 let find_result = find_task(&id);
593 assert!(find_result.is_ok());
594 let task = find_result.unwrap();
595 assert!(task.is_none());
596 }
597
598 #[test]
599 fn test_update_comment_id() {
600 let id = get_next_id().unwrap_or_else(|_| "1".to_string());
602 let task = Task::construct_task(
603 "Test task".to_string(),
604 "Description goes here".to_string(),
605 "OPEN".to_string(),
606 Some(get_current_timestamp())
607 );
608 let create_result = create_task(task);
609 assert!(create_result.is_ok());
610 let mut task = create_result.unwrap();
611
612 let comment_props = HashMap::from([("author".to_string(), "Some developer".to_string())]);
614 let comment = task.add_comment(Some("1".to_string()), comment_props, "Test comment".to_string());
615 assert_eq!(comment.get_id().unwrap(), "1");
616 let update_result = update_task(task);
617 assert!(update_result.is_ok());
618
619 let result = update_comment_id(&id, "1", "2");
621 assert!(result.is_ok());
622
623 let updated_task = find_task(&id).unwrap().unwrap();
625 let updated_comments = updated_task.get_comments().as_ref().unwrap();
626 assert_eq!(updated_comments.len(), 1);
627 assert_eq!(updated_comments[0].get_id().unwrap(), "2");
628
629 let delete_result = delete_tasks(&[&id]);
631 assert!(delete_result.is_ok());
632 }
633
634 #[test]
635 fn test_clear_tasks() {
636 let id = get_next_id().unwrap_or_else(|_| "1".to_string());
637 let task = Task::construct_task("Test task".to_string(), "Description goes here".to_string(), "OPEN".to_string(), Some(get_current_timestamp()));
638 let create_result = create_task(task);
639 assert!(create_result.is_ok());
640 let task = create_result.unwrap();
641 assert_eq!(task.get_id(), Some(id.clone()));
642
643 let id = get_next_id().unwrap_or_else(|_| "2".to_string());
644 let task2 = Task::construct_task("Another task".to_string(), "Another description".to_string(), "IN_PROGRESS".to_string(), Some(get_current_timestamp()));
645 let create_result2 = create_task(task2);
646 assert!(create_result2.is_ok());
647 let task2 = create_result2.unwrap();
648 assert_eq!(task2.get_id(), Some(id.clone()));
649
650 let id = get_next_id().unwrap_or_else(|_| "3".to_string());
651 let task3 = Task::construct_task("Third task".to_string(), "Third description".to_string(), "CLOSED".to_string(), Some(get_current_timestamp()));
652 let create_result3 = create_task(task3);
653 assert!(create_result3.is_ok());
654 let task3 = create_result3.unwrap();
655 assert_eq!(task3.get_id(), Some(id.clone()));
656
657 let clear_result = crate::clear_tasks();
658 assert!(clear_result.is_ok());
659 assert_eq!(clear_result.unwrap(), 3);
660
661 let find_result = find_task(&id);
662 assert!(find_result.is_ok());
663 let task = find_result.unwrap();
664 assert!(task.is_none());
665
666 let find_result = find_task(&task2.get_id().unwrap());
667 assert!(find_result.is_ok());
668 let task = find_result.unwrap();
669 assert!(task.is_none());
670
671 let find_result = find_task(&task3.get_id().unwrap());
672 assert!(find_result.is_ok());
673 let task = find_result.unwrap();
674 assert!(task.is_none());
675 }
676}