task.c (15944B)
1 #define _DEFAULT_SOURCE 2 #include <dirent.h> 3 #include <errno.h> 4 #include <fcntl.h> 5 #include <git2.h> 6 #include <git2/commit.h> 7 #include <stdint.h> 8 #include <stdio.h> 9 #include <stdlib.h> 10 #include <string.h> 11 #include <sys/random.h> 12 #include <sys/stat.h> 13 #include <unistd.h> 14 15 #define TASK_ID_BYTES 8 16 #define TASK_ID_CHARS (TASK_ID_BYTES * 2) 17 #define TASKS_DIR "tasks" 18 #define TASKS_PATH \ 19 "." \ 20 "/" TASKS_DIR 21 #define DONE_TASKS_DIR "done" 22 #define DONE_TASKS_PATH TASKS_PATH "/" DONE_TASKS_DIR 23 24 static const char reverse_hex[] = "klmnopqrstuvwxyz"; 25 26 struct options { 27 char *title; 28 char priority; 29 }; 30 31 static int filter_entries(const struct dirent *dp) { 32 if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) { 33 return 0; 34 } 35 if (strcmp(dp->d_name, DONE_TASKS_DIR) == 0) { 36 return 0; 37 } 38 return 1; 39 } 40 41 static int count_files_scandir(const char *dir_path) { 42 DIR *dirp = opendir(dir_path); 43 if (dirp == NULL) { 44 return -1; 45 } 46 47 int count = 0; 48 struct dirent *dp; 49 while ((dp = readdir(dirp)) != NULL) { 50 if (filter_entries(dp)) { 51 count++; 52 } 53 } 54 55 if (errno != 0) { 56 closedir(dirp); 57 return -1; 58 } 59 60 if (closedir(dirp) != 0) { 61 return -1; 62 } 63 64 return count; 65 } 66 67 static void print_help(const char *program) { 68 printf("usage: %s <command> [options]\n\n" 69 "commands:\n" 70 " commits <task-id> list all commits tied <task-id>.\n" 71 " new <title> [properties...] create a new task.\n" 72 " summary list contexts, projects, etc.\n" 73 "\noptions:\n" 74 " -h, --help show this help\n" 75 " -p, --priority A set priority A-Z\n", 76 program); 77 } 78 79 static int has_txt_extension(const char *name) { 80 size_t len = strlen(name); 81 return len >= 4 && strcmp(name + len - 4, ".txt") == 0; 82 } 83 84 static int generate_task_id(char out[TASK_ID_CHARS + 1]) { 85 uint8_t bytes[TASK_ID_BYTES]; 86 arc4random_buf(bytes, sizeof(bytes)); 87 88 for (size_t i = 0; i < TASK_ID_BYTES; i++) { 89 out[i * 2] = reverse_hex[bytes[i] >> 4]; 90 out[i * 2 + 1] = reverse_hex[bytes[i] & 0x0f]; 91 } 92 93 out[TASK_ID_CHARS] = '\0'; 94 return 0; 95 } 96 97 static int task_id_exists_in_dir(const char *dir, const char *id, 98 int allow_missing) { 99 struct dirent *dp; 100 FILE *fd = NULL; 101 int rc = -1; 102 103 DIR *dirp = opendir(dir); 104 if (dirp == NULL) { 105 if (allow_missing && errno == ENOENT) { 106 return 0; 107 } 108 109 fprintf(stderr, "err: failed to open %s: %s\n", dir, strerror(errno)); 110 goto cleanup; 111 } 112 113 for (;;) { 114 errno = 0; 115 dp = readdir(dirp); 116 if (dp == NULL) { 117 if (errno == 0) { 118 break; 119 } 120 perror("err: failed to iterate directory"); 121 goto cleanup; 122 } 123 124 if (has_txt_extension(dp->d_name) == 0) { 125 continue; 126 } 127 128 char path[4096]; 129 int path_len; 130 if (strcmp(dir, ".") == 0) { 131 path_len = snprintf(path, sizeof path, "%s", dp->d_name); 132 } else { 133 path_len = snprintf(path, sizeof path, "%s/%s", dir, dp->d_name); 134 } 135 136 if (path_len < 0 || (size_t)path_len >= sizeof path) { 137 fprintf(stderr, "err: task file path too long\n"); 138 goto cleanup; 139 } 140 141 fd = fopen(path, "r"); 142 if (fd == NULL) { 143 perror("err: failed to open file"); 144 goto cleanup; 145 } 146 147 char line[1024]; 148 if (fgets(line, sizeof line, fd) == NULL) { 149 if (ferror(fd)) { 150 perror("err: failed to read file"); 151 goto cleanup; 152 } 153 154 // empty file 155 if (fclose(fd) != 0) { 156 perror("err: failed to close file"); 157 goto cleanup; 158 } 159 160 fd = NULL; 161 continue; 162 } 163 164 if (fgets(line, sizeof line, fd) == NULL) { 165 if (ferror(fd)) { 166 perror("err: failed to read file"); 167 goto cleanup; 168 } 169 170 if (fclose(fd) != 0) { 171 perror("err: failed to close file"); 172 goto cleanup; 173 } 174 175 fd = NULL; 176 continue; 177 } 178 179 char expected[TASK_ID_CHARS + 6]; // "id: " + 16 + "\n" + "\0" 180 snprintf(expected, sizeof expected, "id: %s\n", id); 181 if (strcmp(line, expected) == 0) { 182 rc = 1; 183 goto cleanup; 184 } 185 186 if (fclose(fd) != 0) { 187 fd = NULL; 188 perror("err: failed to close file"); 189 goto cleanup; 190 } 191 fd = NULL; 192 } 193 194 rc = 0; 195 196 cleanup: 197 if (fd != NULL) { 198 if (fclose(fd) != 0) { 199 perror("err: failed to close file"); 200 rc = -1; 201 } 202 } 203 204 if (dirp != NULL && closedir(dirp) != 0) { 205 perror("err: failed to close directory"); 206 rc = -1; 207 } 208 209 return rc; 210 } 211 212 static int task_id_exists(const char *id) { 213 int exists = task_id_exists_in_dir(TASKS_PATH, id, 0); 214 if (exists != 0) { 215 return exists; 216 } 217 218 return task_id_exists_in_dir(DONE_TASKS_PATH, id, 1); 219 } 220 221 static int generate_unique_task_id(char id[TASK_ID_CHARS + 1]) { 222 for (;;) { 223 if (generate_task_id(id) != 0) { 224 return -1; 225 } 226 227 int exists = task_id_exists(id); 228 if (exists == -1) { 229 return -1; 230 } 231 232 if (!exists) { 233 return 0; 234 } 235 } 236 } 237 238 static void print_properties(char identifier) { 239 struct dirent *dp; 240 FILE *fd = NULL; 241 int rc = 1; 242 243 char *properties[1024]; 244 size_t properties_len = 0; 245 246 DIR *dirp = opendir(TASKS_PATH); 247 if (dirp == NULL) { 248 perror("err: failed to open tasks directory"); 249 goto cleanup; 250 } 251 252 for (;;) { 253 errno = 0; 254 dp = readdir(dirp); 255 if (dp == NULL) { 256 if (errno == 0) { 257 break; 258 } 259 perror("err: failed to iterate directory"); 260 goto cleanup; 261 } 262 263 if (has_txt_extension(dp->d_name) == 0) { 264 continue; 265 } 266 267 char path[4096]; 268 int path_len = 269 snprintf(path, sizeof(path), "%s/%s", TASKS_PATH, dp->d_name); 270 if (path_len < 0 || (size_t)path_len >= sizeof path) { 271 fprintf(stderr, "err: task file path too long\n"); 272 goto cleanup; 273 } 274 275 fd = fopen(path, "r"); 276 if (fd == NULL) { 277 perror("err: failed to open file"); 278 goto cleanup; 279 } 280 281 char line[1024]; 282 if (fgets(line, sizeof line, fd) == NULL) { 283 if (ferror(fd)) { 284 perror("err: failed to read file"); 285 goto cleanup; 286 } 287 288 // empty file 289 if (fclose(fd) != 0) { 290 perror("err: failed to close file"); 291 goto cleanup; 292 } 293 294 fd = NULL; 295 continue; 296 } 297 298 size_t byte = 0; 299 while (line[byte] != '\0') { 300 char c = line[byte]; 301 302 if (c == identifier) { 303 char property[1024]; 304 size_t property_len = 0; 305 306 while (line[byte] != ' ' && line[byte] != '\n' && line[byte] != '\0' && 307 property_len < sizeof(property) - 1) { 308 property[property_len] = line[byte]; 309 property_len++; 310 byte++; 311 } 312 property[property_len] = '\0'; 313 property_len++; 314 315 char *property_ptr = malloc(sizeof(char) * property_len); 316 if (property_ptr == NULL) { 317 perror("err: failed to alloc space for read property"); 318 goto cleanup; 319 } 320 321 memcpy(property_ptr, property, property_len); 322 323 if (properties_len == sizeof(properties) / sizeof(properties[0])) { 324 fprintf(stderr, "err: too many properties\n"); 325 free(property_ptr); 326 goto cleanup; 327 } 328 329 properties[properties_len] = property_ptr; 330 properties_len++; 331 } else { 332 byte++; 333 } 334 } 335 336 if (fclose(fd) != 0) { 337 fd = NULL; 338 perror("err: failed to close file"); 339 goto cleanup; 340 } 341 fd = NULL; 342 } 343 344 rc = 0; 345 346 for (size_t i = 0; i < properties_len; i++) { 347 int is_duplicate = 0; 348 for (size_t j = i; j > 0; j--) { 349 if (strcmp(properties[i], properties[j - 1]) == 0) { 350 is_duplicate = 1; 351 break; 352 } 353 } 354 355 if (is_duplicate == 1) { 356 continue; 357 } 358 359 printf("%s\n", properties[i]); 360 } 361 362 cleanup: 363 if (fd != NULL) { 364 fclose(fd); 365 } 366 367 for (size_t i = 0; i < properties_len; i++) { 368 free(properties[i]); 369 } 370 371 if (dirp != NULL && closedir(dirp) != 0 && rc == 0) { 372 perror("err: failed to close directory"); 373 rc = 1; 374 } 375 376 if (rc != 0) { 377 exit(1); 378 } 379 } 380 381 static int new_command(int argc, char *argv[], struct options *cli_args) { 382 if (argc == 1) { 383 fprintf(stderr, "err: title required\n"); 384 return 1; 385 } 386 387 int positionals = 0; 388 for (int idx = 1; idx < argc; idx++) { 389 if (strcmp(argv[idx], "--priority") == 0 || strcmp(argv[idx], "-p") == 0) { 390 if (cli_args->priority != '\0') { 391 fprintf(stderr, "err: priority specified multiple times\n"); 392 return 1; 393 } 394 395 if (idx + 1 == argc) { 396 fprintf(stderr, "err: priority required\n"); 397 return 1; 398 } 399 400 if (strcmp(argv[idx + 1], "") == 0) { 401 fprintf(stderr, "err: priority can not be empty\n"); 402 return 1; 403 } 404 405 if (strlen(argv[idx + 1]) != 1) { 406 fprintf(stderr, 407 "err: priority must be a single letter between [A-Z]\n"); 408 return 1; 409 } 410 411 cli_args->priority = argv[idx + 1][0]; 412 idx++; 413 continue; 414 } 415 416 if (strcmp(argv[idx], "") == 0) { 417 fprintf(stderr, "err: argument can not be empty\n"); 418 return 1; 419 } 420 421 if (positionals == 0) { 422 cli_args->title = argv[idx]; 423 } 424 425 positionals++; 426 } 427 428 if (cli_args->title == NULL) { 429 fprintf(stderr, "err: title required\n"); 430 return 1; 431 } 432 433 if (cli_args->priority != '\0') { 434 if (cli_args->priority < 'A' || cli_args->priority > 'Z') { 435 fprintf(stderr, "err: priority must be between [A-Z]\n"); 436 return 1; 437 } 438 } 439 440 size_t title_len = strlen(cli_args->title); 441 if (title_len > 251) { 442 fprintf(stderr, "err: title too long for file name\n"); 443 return 1; 444 } 445 446 char title_to_file[256]; 447 int in_leading_dots = 1; 448 for (size_t idx = 0; idx < title_len; idx++) { 449 if ((in_leading_dots && cli_args->title[idx] == '.') || 450 cli_args->title[idx] == ' ' || cli_args->title[idx] == '/' || 451 cli_args->title[idx] == '\\') { 452 title_to_file[idx] = '_'; 453 } else { 454 in_leading_dots = 0; 455 title_to_file[idx] = cli_args->title[idx]; 456 } 457 } 458 strcpy(title_to_file + title_len, ".txt"); 459 460 char path[4096]; 461 snprintf(path, sizeof(path), "%s/%s", TASKS_PATH, title_to_file); 462 463 struct stat st = {0}; 464 if (stat(TASKS_PATH, &st) == -1) { 465 if (mkdir(TASKS_PATH, 0700) == -1) { 466 perror("err: failed to create tasks directory"); 467 return 1; 468 }; 469 } 470 471 DIR *tasks_dir = opendir(TASKS_PATH); 472 if (tasks_dir == NULL) { 473 fprintf(stderr, "err: failed to check for %s: %s\n", TASKS_PATH, 474 strerror(errno)); 475 return 1; 476 } 477 closedir(tasks_dir); 478 479 int fd = open(path, O_WRONLY | O_CREAT | O_EXCL, 0666); 480 FILE *f = fdopen(fd, "w"); 481 if (f == NULL) { 482 if (fd != -1) { 483 close(fd); 484 } 485 perror("err: failed to open files"); 486 return 1; 487 } 488 489 if (cli_args->priority != '\0') { 490 if (fprintf(f, "(%c) ", cli_args->priority) < 0) { 491 fclose(f); 492 perror("err: failed to write file"); 493 return 1; 494 } 495 } 496 497 if (fprintf(f, "%s", cli_args->title) < 0) { 498 fclose(f); 499 perror("err: failed to write file"); 500 return 1; 501 } 502 503 int seen_title = 0; 504 for (int idx = 1; idx < argc; idx++) { 505 if (strcmp(argv[idx], "--priority") == 0 || strcmp(argv[idx], "-p") == 0) { 506 idx++; 507 continue; 508 } 509 510 seen_title++; 511 if (seen_title == 1) { 512 continue; 513 } 514 515 if (fprintf(f, " %s", argv[idx]) < 0) { 516 fclose(f); 517 perror("err: failed to write file"); 518 return 1; 519 } 520 } 521 522 if (fprintf(f, "\n") < 0) { 523 fclose(f); 524 perror("err: failed to write file"); 525 return 1; 526 } 527 528 char id[TASK_ID_CHARS + 1]; 529 if (generate_unique_task_id(id) != 0) { 530 fclose(f); 531 perror("err: failed to generate unique id"); 532 return 1; 533 } 534 535 if (fprintf(f, "id: %s\n", id) < 0) { 536 fclose(f); 537 perror("err: failed to write file"); 538 return 1; 539 } 540 541 fclose(f); 542 543 char *editor = getenv("VISUAL"); 544 if (editor != NULL && strcmp(editor, "") == 0) { 545 editor = NULL; 546 } 547 if (editor == NULL) { 548 editor = getenv("EDITOR"); 549 } 550 551 if (editor == NULL || strcmp(editor, "") == 0) { 552 return 0; 553 } 554 555 execlp("sh", "sh", "-c", "exec \"$1\" \"$2\"", "task", editor, path, NULL); 556 perror("err: failed to open editor"); 557 558 return 1; 559 } 560 561 static int related_commits_command(int argc, char *argv[], 562 struct options *cli_args) { 563 if (argc != 2) { 564 fprintf(stderr, "err: task id required\n"); 565 return 1; 566 } 567 568 if (strcmp(argv[1], "") == 0) { 569 fprintf(stderr, "err: task id can not be empty\n"); 570 return 1; 571 } 572 573 char *task_id = argv[1]; 574 575 int error; 576 git_libgit2_init(); 577 578 git_repository *repo = NULL; 579 if ((error = git_repository_open(&repo, ".")) < 0) { 580 const git_error *e = git_error_last(); 581 fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message); 582 return 1; 583 } 584 585 git_object *obj = NULL; 586 if ((error = git_revparse_single(&obj, repo, "HEAD")) < 0) { 587 const git_error *e = git_error_last(); 588 fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message); 589 return 1; 590 } 591 592 const git_oid *head = NULL; 593 head = git_object_id(obj); 594 git_object_free(obj); 595 596 git_revwalk *walker = NULL; 597 if ((error = git_revwalk_new(&walker, repo)) < 0) { 598 const git_error *e = git_error_last(); 599 fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message); 600 return 1; 601 } 602 603 if ((error = git_revwalk_push(walker, head)) < 0) { 604 const git_error *e = git_error_last(); 605 fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message); 606 return 1; 607 } 608 609 git_oid id; 610 while (!git_revwalk_next(&id, walker)) { 611 git_commit *commit; 612 if ((error = git_commit_lookup(&commit, repo, &id)) < 0) { 613 const git_error *e = git_error_last(); 614 fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message); 615 return 1; 616 } 617 618 const char *summary = git_commit_summary(commit); 619 const char *message = git_commit_message(commit); 620 621 git_message_trailer_array *trailers = 622 malloc(sizeof(git_message_trailer_array)); 623 if ((error = git_message_trailers(trailers, message)) < 0) { 624 const git_error *e = git_error_last(); 625 fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message); 626 return 1; 627 } 628 629 for (size_t i = 0; i < trailers->count; i++) { 630 if (strcmp(trailers->trailers[i].key, "Task-id") == 0) { 631 if (strcmp(trailers->trailers[i].value, task_id) == 0) { 632 char oid_str[GIT_OID_HEXSZ + 1]; 633 git_oid_tostr(oid_str, sizeof(oid_str), &id); 634 printf("%s: %s\n", oid_str, summary); 635 } 636 } 637 } 638 } 639 640 git_libgit2_shutdown(); 641 642 return 0; 643 } 644 645 int main(int argc, char *argv[]) { 646 struct options cli_args = {0}; 647 cli_args.priority = '\0'; 648 649 if (argc > 1 && 650 (strcmp(argv[1], "--help") == 0 || strcmp(argv[1], "-h") == 0)) { 651 print_help(argv[0]); 652 return 0; 653 } 654 655 if (argc > 1 && strcmp(argv[1], "new") == 0) { 656 if (new_command(argc - 1, argv + 1, &cli_args) != 0) { 657 return 1; 658 } 659 } 660 661 if (argc > 1 && strcmp(argv[1], "commits") == 0) { 662 if (related_commits_command(argc - 1, argv + 1, &cli_args) != 0) { 663 return 1; 664 } 665 } 666 667 if (argc > 1 && strcmp(argv[1], "summary") == 0) { 668 int open_tasks; 669 if ((open_tasks = count_files_scandir(TASKS_PATH)) < 0) { 670 perror("err: failed to read tasks/"); 671 return 1; 672 } 673 674 int closed_tasks; 675 if ((closed_tasks = count_files_scandir(DONE_TASKS_PATH)) < 0) { 676 if (errno != ENOENT) { 677 perror("err: failed to read tasks/done/"); 678 return 1; 679 } 680 closed_tasks = 0; 681 } 682 683 printf("open tasks: %d\n", open_tasks); 684 printf("closed tasks: %d\n", closed_tasks); 685 printf("\ncontexts:\n"); 686 print_properties('@'); 687 printf("\nprojects:\n"); 688 print_properties('+'); 689 } 690 691 return 0; 692 }