task

create a plain-text task file in tasks/
git clone git://brookjeynes.dev/bjeynes/task.git
Log | Files | Refs | README | LICENSE

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 }