commit-heatmap-gen

commit heatmap generator
git clone git://brookjeynes.dev/bjeynes/commit-heatmap-gen.git
Log | Files | Refs | README | LICENSE

main.c (6330B)


      1 #define _DEFAULT_SOURCE
      2 
      3 #include <dirent.h>
      4 #include <errno.h>
      5 #include <git2.h>
      6 #include <git2/commit.h>
      7 #include <git2/repository.h>
      8 #include <git2/signature.h>
      9 #include <stdio.h>
     10 #include <string.h>
     11 #include <time.h>
     12 
     13 #define DAYS_PER_YEAR 365
     14 #define HOURS_PER_DAY 24
     15 #define MINUTES_PER_HOUR 60
     16 #define SECONDS_PER_MINUTE 60
     17 
     18 char *bubble_variant(int count) {
     19   if (count > 13) {
     20     return "-heavy";
     21   }
     22 
     23   if (count > 8) {
     24     return "-medium";
     25   }
     26 
     27   if (count > 4) {
     28     return "-light";
     29   }
     30 
     31   if (count > 0) {
     32     return "-lightest";
     33   }
     34 
     35   return "";
     36 }
     37 
     38 typedef struct {
     39   char date[11]; // "YYYY-MM-DD\0"
     40   int count;
     41 } commits_by_date;
     42 
     43 int main(int argc, char *argv[]) {
     44   int error;
     45 
     46   if (argc == 1) {
     47     fprintf(stderr, "err: author email required\n");
     48     exit(1);
     49   }
     50   char *author_email = argv[1];
     51 
     52   if (argc == 2) {
     53     fprintf(stderr, "err: abs repo path required");
     54     exit(1);
     55   }
     56   char *repos_dir = argv[2];
     57 
     58   commits_by_date commit_tracker[DAYS_PER_YEAR];
     59   int unique_dates = 0;
     60 
     61   const time_t now = time(NULL);
     62   const time_t year_behind = now - (DAYS_PER_YEAR * HOURS_PER_DAY *
     63                                     MINUTES_PER_HOUR * SECONDS_PER_MINUTE);
     64 
     65   git_libgit2_init();
     66 
     67   DIR *dirp = opendir(repos_dir);
     68   if (dirp == NULL) {
     69     fprintf(stderr, "err: failed to open directory: %s\n", strerror(errno));
     70     exit(1);
     71   }
     72 
     73   for (;;) {
     74     errno = 0;
     75 
     76     struct dirent *dp;
     77     dp = readdir(dirp);
     78     if (dp == NULL) {
     79       if (errno == 0) {
     80         break;
     81       }
     82       fprintf(stderr, "err: failed to iterate directory: %s\n",
     83               strerror(errno));
     84       exit(1);
     85     }
     86 
     87     if (dp->d_type != DT_DIR) {
     88       continue;
     89     }
     90 
     91     if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) {
     92       continue;
     93     }
     94 
     95     char path[1024];
     96     int path_len = snprintf(path, sizeof(path), "%s/%s", repos_dir, dp->d_name);
     97 
     98     if (path_len < 0 || (size_t)path_len >= sizeof(path)) {
     99       fprintf(stderr, "err: path too long\n");
    100       exit(1);
    101     }
    102 
    103     git_repository *repo = NULL;
    104     if ((error = git_repository_open_ext(
    105              &repo, path, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) < 0) {
    106       if (error == GIT_ENOTFOUND) {
    107         continue;
    108       }
    109       const git_error *e = git_error_last();
    110       fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message);
    111       exit(error);
    112     }
    113 
    114     git_object *obj = NULL;
    115     if ((error = git_revparse_single(&obj, repo, "HEAD")) < 0) {
    116       const git_error *e = git_error_last();
    117       fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message);
    118       exit(error);
    119     }
    120 
    121     const git_oid *head = NULL;
    122     head = git_object_id(obj);
    123     git_object_free(obj);
    124 
    125     git_revwalk *walker = NULL;
    126     if ((error = git_revwalk_new(&walker, repo)) < 0) {
    127       const git_error *e = git_error_last();
    128       fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message);
    129       exit(error);
    130     }
    131 
    132     if ((error = git_revwalk_push(walker, head)) < 0) {
    133       const git_error *e = git_error_last();
    134       fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message);
    135       exit(error);
    136     }
    137 
    138     git_oid id;
    139     while (!git_revwalk_next(&id, walker)) {
    140       git_commit *commit;
    141       if ((error = git_commit_lookup(&commit, repo, &id)) < 0) {
    142         const git_error *e = git_error_last();
    143         fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message);
    144         exit(error);
    145       }
    146 
    147       const git_signature *author = git_commit_author(commit);
    148 
    149       if (strcmp(author_email, author->email) != 0) {
    150         continue;
    151       }
    152 
    153       if (author->when.time < year_behind) {
    154         break;
    155       }
    156 
    157       struct tm *lt;
    158       char buf[11];
    159 
    160       lt = localtime(&author->when.time);
    161       if (strftime(buf, sizeof(buf), "%Y-%m-%d", lt) == 0) {
    162         fprintf(stderr, "err: failed to get print time\n");
    163         exit(1);
    164       }
    165 
    166       int found = 0;
    167       for (int j = 0; j < unique_dates; j++) {
    168         if (strcmp(commit_tracker[j].date, buf) == 0) {
    169           commit_tracker[j].count++;
    170           found = 1;
    171           break;
    172         }
    173       }
    174 
    175       if (found == 0) {
    176         if (unique_dates < DAYS_PER_YEAR) {
    177           strcpy(commit_tracker[unique_dates].date, buf);
    178           commit_tracker[unique_dates].count = 1;
    179           unique_dates++;
    180         }
    181       }
    182     }
    183   }
    184 
    185   if (dirp != NULL) {
    186     closedir(dirp);
    187   }
    188 
    189   printf("const template = document.createElement(\"template\");"
    190          "template.innerHTML = `<style>"
    191          ".heatmap-grid {"
    192          "display: grid;"
    193          "grid-template-rows: repeat(7, minmax(0, 1fr));"
    194          "grid-auto-flow: column;"
    195          "gap: 0.25rem;"
    196          "}"
    197          ".heatmap-bubble {"
    198          "width: 1rem;"
    199          "height: 1rem;"
    200          "border-radius: 0.25rem;"
    201          "background-color: #eff2f5;"
    202          "&.-lightest { background-color: #aceebb; }"
    203          "&.-light { background-color: #4ac26b; }"
    204          "&.- medium { background-color: #2da44e; }"
    205          "&.-heavy { background-color: #116329; }"
    206          "}"
    207          "</style>");
    208 
    209   printf("<div class=\"heatmap-grid\">");
    210 
    211   time_t day = year_behind;
    212   while (day < now) {
    213     struct tm *lt;
    214     char buf[11];
    215 
    216     lt = localtime(&day);
    217     if (strftime(buf, sizeof(buf), "%Y-%m-%d", lt) == 0) {
    218       fprintf(stderr, "err: failed to get print time\n");
    219       exit(1);
    220     }
    221 
    222     int found = -1;
    223     for (int j = 0; j < unique_dates; j++) {
    224       if (strcmp(commit_tracker[j].date, buf) == 0) {
    225         found = j;
    226         break;
    227       }
    228     }
    229 
    230     if (found == -1) {
    231       printf("<div class=\"heatmap-bubble %s\" title=\"0 sessions on "
    232              "%s\"></div>",
    233              bubble_variant(0), buf);
    234     } else {
    235       int commits = commit_tracker[found].count;
    236       printf("<div class=\"heatmap-bubble %s\" title=\"%d sessions on "
    237              "%s\"></div>",
    238              bubble_variant(commits), commits, buf);
    239     }
    240 
    241     day += (24 * 60 * 60);
    242   }
    243 
    244   printf("</div>`;");
    245 
    246   printf("class CommitHeatmap extends HTMLElement {"
    247          "constructor() {"
    248          "super();"
    249          "const shadowRoot = this.attachShadow({ mode: \"closed\" });"
    250          "shadowRoot.appendChild(document.importNode(template.content, true));"
    251          "}}"
    252          "customElements.define(\"commit-heatmap\", CommitHeatmap);");
    253 
    254   git_libgit2_shutdown();
    255 
    256   return 0;
    257 }