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