commit-heatmap-gen

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

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:
MMakefile | 9+++++----
Aindex.html | 19+++++++++++++++++++
Mmain.c | 213++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Dstyle.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; - } -}