commit f9623b0a9797e21e95e61784e5091f69cd94b991
parent 868dbf75c016b6c2b949a0e1828476e0b8d7bb7b
Author: brookjeynes <me@brookjeynes.dev>
Date: Thu, 18 Jun 2026 10:12:37 +1000
feat: iterate all git repos in dir
Signed-off-by: brookjeynes <me@brookjeynes.dev>
Diffstat:
| M | Makefile | | | 9 | +++++---- |
| A | index.html | | | 19 | +++++++++++++++++++ |
| M | main.c | | | 213 | ++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------- |
| D | style.css | | | 29 | ----------------------------- |
4 files changed, 166 insertions(+), 104 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,13 +1,14 @@
.PHONY: build clean
-CC := gcc
-CFLAGS := -Wall -Wextra -Wpedantic -std=c11 $(shell pkg-config --cflags libgit2)
+CC = gcc
+CFLAGS = -Wall -Wextra -Wpedantic -std=c11 $(shell pkg-config --cflags libgit2)
LDFLAGS = $(shell pkg-config --libs libgit2)
-BUILD_DIR := build
+BUILD_DIR = build
+NAME = commit-heatmap-gen
build:
mkdir -p ./$(BUILD_DIR)
- $(CC) $(CFLAGS) $(LDFLAGS) -o ./$(BUILD_DIR)/gs main.c
+ $(CC) $(CFLAGS) $(LDFLAGS) -o ./$(BUILD_DIR)/$(NAME) main.c
clean:
rm -rf ./$(BUILD_DIR)
diff --git a/index.html b/index.html
@@ -0,0 +1,19 @@
+<html lang="en">
+
+<head>
+ <script src="commit-heatmap.js"></script>
+</head>
+
+<style>
+ body {
+ width: 900px;
+ }
+</style>
+
+<body>
+ <div>
+ <commit-heatmap></commit-heatmap>
+ </div>
+</body>
+
+</html>
diff --git a/main.c b/main.c
@@ -1,5 +1,10 @@
+#define _DEFAULT_SOURCE
+
+#include <dirent.h>
+#include <errno.h>
#include <git2.h>
#include <git2/commit.h>
+#include <git2/repository.h>
#include <git2/signature.h>
#include <stdio.h>
#include <string.h>
@@ -48,99 +53,159 @@ int main(int argc, char *argv[]) {
fprintf(stderr, "err: abs repo path required");
exit(1);
}
- char *repo_path = argv[2];
+ char *repos_dir = argv[2];
commits_by_date commit_tracker[DAYS_PER_YEAR];
int unique_dates = 0;
+ const time_t now = time(NULL);
+ const time_t year_behind = now - (DAYS_PER_YEAR * HOURS_PER_DAY *
+ MINUTES_PER_HOUR * SECONDS_PER_MINUTE);
+
git_libgit2_init();
- git_repository *repo = NULL;
- if ((error = git_repository_open(&repo, repo_path)) < 0) {
- const git_error *e = git_error_last();
- printf("err: %d/%d: %s\n", error, e->klass, e->message);
- exit(error);
+ DIR *dirp = opendir(repos_dir);
+ if (dirp == NULL) {
+ fprintf(stderr, "err: failed to open directory: %s\n", strerror(errno));
+ exit(1);
}
- git_object *obj = NULL;
- if ((error = git_revparse_single(&obj, repo, "HEAD")) < 0) {
- const git_error *e = git_error_last();
- printf("err: %d/%d: %s\n", error, e->klass, e->message);
- exit(error);
- }
+ for (;;) {
+ errno = 0;
- const git_oid *head = NULL;
- head = git_object_id(obj);
- git_object_free(obj);
+ struct dirent *dp;
+ dp = readdir(dirp);
+ if (dp == NULL) {
+ if (errno == 0) {
+ break;
+ }
+ fprintf(stderr, "err: failed to iterate directory: %s\n",
+ strerror(errno));
+ exit(1);
+ }
- git_revwalk *walker = NULL;
- if ((error = git_revwalk_new(&walker, repo)) < 0) {
- const git_error *e = git_error_last();
- printf("err: %d/%d: %s\n", error, e->klass, e->message);
- exit(error);
- }
+ if (dp->d_type != DT_DIR) {
+ continue;
+ }
- if ((error = git_revwalk_push(walker, head)) < 0) {
- const git_error *e = git_error_last();
- printf("err: %d/%d: %s\n", error, e->klass, e->message);
- exit(error);
- }
+ if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) {
+ continue;
+ }
- const time_t now = time(NULL);
- const time_t year_behind = now - (DAYS_PER_YEAR * HOURS_PER_DAY *
- MINUTES_PER_HOUR * SECONDS_PER_MINUTE);
+ char path[1024];
+ int path_len = snprintf(path, sizeof(path), "%s/%s", repos_dir, dp->d_name);
+
+ if (path_len < 0 || (size_t)path_len >= sizeof(path)) {
+ fprintf(stderr, "err: path too long\n");
+ exit(1);
+ }
+
+ git_repository *repo = NULL;
+ if ((error = git_repository_open_ext(
+ &repo, path, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL)) < 0) {
+ if (error == GIT_ENOTFOUND) {
+ continue;
+ }
+ const git_error *e = git_error_last();
+ fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message);
+ exit(error);
+ }
- git_oid id;
- while (!git_revwalk_next(&id, walker)) {
- git_commit *commit;
- if ((error = git_commit_lookup(&commit, repo, &id)) < 0) {
+ git_object *obj = NULL;
+ if ((error = git_revparse_single(&obj, repo, "HEAD")) < 0) {
const git_error *e = git_error_last();
- printf("err: %d/%d: %s\n", error, e->klass, e->message);
+ fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message);
exit(error);
}
- const git_signature *author = git_commit_author(commit);
+ const git_oid *head = NULL;
+ head = git_object_id(obj);
+ git_object_free(obj);
- if (strcmp(author_email, author->email) != 0) {
- continue;
+ git_revwalk *walker = NULL;
+ if ((error = git_revwalk_new(&walker, repo)) < 0) {
+ const git_error *e = git_error_last();
+ fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message);
+ exit(error);
}
- if (author->when.time < year_behind) {
- break;
+ if ((error = git_revwalk_push(walker, head)) < 0) {
+ const git_error *e = git_error_last();
+ fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message);
+ exit(error);
}
- struct tm *lt;
- char buf[11];
+ git_oid id;
+ while (!git_revwalk_next(&id, walker)) {
+ git_commit *commit;
+ if ((error = git_commit_lookup(&commit, repo, &id)) < 0) {
+ const git_error *e = git_error_last();
+ fprintf(stderr, "err: %d/%d: %s\n", error, e->klass, e->message);
+ exit(error);
+ }
- lt = localtime(&author->when.time);
- if (strftime(buf, sizeof(buf), "%Y-%m-%d", lt) == 0) {
- fprintf(stderr, "err: failed to get print time\n");
- exit(1);
- }
+ const git_signature *author = git_commit_author(commit);
- int found = 0;
- for (int j = 0; j < unique_dates; j++) {
- if (strcmp(commit_tracker[j].date, buf) == 0) {
- commit_tracker[j].count++;
- found = 1;
+ if (strcmp(author_email, author->email) != 0) {
+ continue;
+ }
+
+ if (author->when.time < year_behind) {
break;
}
- }
- if (found == 0) {
- if (unique_dates < DAYS_PER_YEAR) {
- strcpy(commit_tracker[unique_dates].date, buf);
- commit_tracker[unique_dates].count = 1;
- unique_dates++;
+ struct tm *lt;
+ char buf[11];
+
+ lt = localtime(&author->when.time);
+ if (strftime(buf, sizeof(buf), "%Y-%m-%d", lt) == 0) {
+ fprintf(stderr, "err: failed to get print time\n");
+ exit(1);
+ }
+
+ int found = 0;
+ for (int j = 0; j < unique_dates; j++) {
+ if (strcmp(commit_tracker[j].date, buf) == 0) {
+ commit_tracker[j].count++;
+ found = 1;
+ break;
+ }
+ }
+
+ if (found == 0) {
+ if (unique_dates < DAYS_PER_YEAR) {
+ strcpy(commit_tracker[unique_dates].date, buf);
+ commit_tracker[unique_dates].count = 1;
+ unique_dates++;
+ }
}
}
}
- printf("<html lang=\"en\">");
- printf("<head>");
- printf("<link rel=\"stylesheet\" href=\"./style.css\" type=\"text/css\"/>");
- printf("</head>");
- printf("<body>");
+ if (dirp != NULL) {
+ closedir(dirp);
+ }
+
+ printf("const template = document.createElement(\"template\");"
+ "template.innerHTML = `<style>"
+ ".heatmap-grid {"
+ "display: grid;"
+ "grid-template-rows: repeat(7, minmax(0, 1fr));"
+ "grid-auto-flow: column;"
+ "gap: 0.25rem;"
+ "}"
+ ".heatmap-bubble {"
+ "width: 1rem;"
+ "height: 1rem;"
+ "border-radius: 0.25rem;"
+ "background-color: #eff2f5;"
+ "&.-lightest { background-color: #aceebb; }"
+ "&.-light { background-color: #4ac26b; }"
+ "&.- medium { background-color: #2da44e; }"
+ "&.-heavy { background-color: #116329; }"
+ "}"
+ "</style>");
+
printf("<div class=\"heatmap-grid\">");
time_t day = year_behind;
@@ -163,22 +228,28 @@ int main(int argc, char *argv[]) {
}
if (found == -1) {
- printf(
- "<div class=\"heatmap-bubble %s\" title=\"0 sessions on %s\"></div>",
- bubble_variant(0), buf);
+ printf("<div class=\"heatmap-bubble %s\" title=\"0 sessions on "
+ "%s\"></div>",
+ bubble_variant(0), buf);
} else {
int commits = commit_tracker[found].count;
- printf(
- "<div class=\"heatmap-bubble %s\" title=\"%d sessions on %s\"></div>",
- bubble_variant(commits), commits, buf);
+ printf("<div class=\"heatmap-bubble %s\" title=\"%d sessions on "
+ "%s\"></div>",
+ bubble_variant(commits), commits, buf);
}
day += (24 * 60 * 60);
}
- printf("</div>");
- printf("</body>");
- printf("</html>");
+ printf("</div>`;");
+
+ printf("class CommitHeatmap extends HTMLElement {"
+ "constructor() {"
+ "super();"
+ "const shadowRoot = this.attachShadow({ mode: \"closed\" });"
+ "shadowRoot.appendChild(document.importNode(template.content, true));"
+ "}}"
+ "customElements.define(\"commit-heatmap\", CommitHeatmap);");
git_libgit2_shutdown();
diff --git a/style.css b/style.css
@@ -1,29 +0,0 @@
-.heatmap-grid {
- display: grid;
- grid-template-rows: repeat(7, minmax(0, 1fr));
- grid-auto-flow: column;
- gap: 0.25rem;
-}
-
-.heatmap-bubble {
- width: 1rem;
- height: 1rem;
- border-radius: 0.25rem;
- background-color: #eff2f5;
-
- &.-lightest {
- background-color: #aceebb;
- }
-
- &.-light {
- background-color: #4ac26b;
- }
-
- &.-medium {
- background-color: #2da44e;
- }
-
- &.-heavy {
- background-color: #116329;
- }
-}