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 }