stagit

static git page generator
git clone https://git.awy.one/stagit
Log | Files | Refs | README | LICENSE

commit 60f688cb8923c79ad682d6cf47bcc6807bceeebf
parent 9a1a424b3ae7af21968c2dce62b9d54562003b81
Author: awy <awy@awy.one>
Date:   Fri, 10 Apr 2026 21:42:21 +0300

feat: markdown parsing and directory listing

Diffstat:
MMakefile | 2+-
Mstagit.c | 199++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
2 files changed, 167 insertions(+), 34 deletions(-)

diff --git a/Makefile b/Makefile @@ -9,7 +9,7 @@ MANPREFIX = ${PREFIX}/man DOCPREFIX = ${PREFIX}/share/doc/${NAME} LIBGIT_INC = -I/usr/local/include -LIBGIT_LIB = -L/usr/local/lib -lgit2 +LIBGIT_LIB = -L/usr/local/lib -lgit2 -lmd4c-html # use system flags. STAGIT_CFLAGS = ${LIBGIT_INC} ${CFLAGS} diff --git a/stagit.c b/stagit.c @@ -13,6 +13,7 @@ #include <unistd.h> #include <git2.h> +#include <md4c-html.h> #include "compat.h" @@ -449,6 +450,20 @@ mkdirp(const char *path) return 0; } +int +mkdirfile(const char *path) +{ + char *d; + char tmp[PATH_MAX]; + if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp)) + errx(1, "path truncated: '%s'", path); + if (!(d = dirname(tmp))) + err(1, "dirname"); + if (mkdirp(d)) + return -1; + return 0; +} + void printtimez(FILE *fp, const git_time *intime) { @@ -542,8 +557,7 @@ writeheader(FILE *fp, const char *title) fprintf(fp, " | <a href=\"%sfile/%s.html\">Submodules</a>", relpath, submodules); if (readme) - fprintf(fp, " | <a href=\"%sfile/%s.html\">README</a>", - relpath, readme); + fprintf(fp, " | <a href=\"%sreadme.html\">README</a>", relpath); if (license) fprintf(fp, " | <a href=\"%sfile/%s.html\">LICENSE</a>", relpath, license); @@ -950,25 +964,48 @@ writeatom(FILE *fp, int all) return 0; } -size_t -writeblob(git_object *obj, const char *fpath, const char *filename, size_t filesize) +void +writeblobraw(const git_blob *blob, const char *fpath, const char *filename, git_off_t filesize) { - char tmp[PATH_MAX] = "", *d; + char tmp[PATH_MAX] = ""; const char *p; size_t lc = 0; FILE *fp; + mkdirfile(fpath); + if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp)) errx(1, "path truncated: '%s'", fpath); - if (!(d = dirname(tmp))) - err(1, "dirname"); - if (mkdirp(d)) - return -1; for (p = fpath, tmp[0] = '\0'; *p; p++) { if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp)) errx(1, "path truncated: '../%s'", tmp); } + + fp = efopen(fpath, "w"); + fwrite(git_blob_rawcontent(blob), (size_t)git_blob_rawsize(blob), 1, fp); + fclose(fp); +} + +size_t +writeblob(git_object *obj, const char *fpath, const char *rpath, const char *filename, size_t filesize) +{ + char tmp[PATH_MAX] = ""; + const char *p, *oldrelpath; + int lc = 0; + FILE *fp; + + mkdirfile(fpath); + + if (strlcpy(tmp, fpath, sizeof(tmp)) >= sizeof(tmp)) + errx(1, "path truncated: '%s'", fpath); + + for (p = fpath, tmp[0] = '\0'; *p; p++) { + if (*p == '/' && strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp)) + errx(1, "path truncated: '../%s'", tmp); + } + + oldrelpath = relpath; relpath = tmp; fp = efopen(fpath, "w"); @@ -976,7 +1013,7 @@ writeblob(git_object *obj, const char *fpath, const char *filename, size_t files fputs("<p> ", fp); xmlencode(fp, filename, strlen(filename)); fprintf(fp, " (%zuB)", filesize); - fputs("</p><hr/>", fp); + fprintf(fp, " - <a href=\"%s%s\">raw</a></p><hr/>", relpath, rpath); if (git_blob_is_binary((git_blob *)obj)) { @@ -988,7 +1025,7 @@ writeblob(git_object *obj, const char *fpath, const char *filename, size_t files !strcmp(fext, ".webp") || !strcmp(fext, ".avif"))) fprintf(fp, "<a href=\"/%s/plain/%s\"><img id=\"blob-img\" src=\"/%s/plain/%s\" /></a>\n", - name, entrypath, name, entrypath); + relpath, rpath, relpath, rpath); else fputs("<p>Binary file.</p>\n", fp); } @@ -999,7 +1036,7 @@ writeblob(git_object *obj, const char *fpath, const char *filename, size_t files checkfileerror(fp, fpath, 'w'); fclose(fp); - relpath = ""; + relpath = oldrelpath; return lc; } @@ -1051,10 +1088,51 @@ writefilestree(FILE *fp, git_tree *tree, const char *path) { const git_tree_entry *entry = NULL; git_object *obj = NULL; - const char *entryname; - char filepath[PATH_MAX], entrypath[PATH_MAX], oid[8]; + FILE *fp_subtree; + const char *entryname, *oldrelpath; + char filepath[PATH_MAX], rawpath[PATH_MAX], entrypath[PATH_MAX], tmp[PATH_MAX], tmp2[PATH_MAX], oid[8]; + char* parent; size_t count, i, lc, filesize; - int r, ret; + int r, rf, ret, is_obj_tree; + + if (strlen(path) > 0) { + fputs("<h2>Directory: ", fp); + xmlencode(fp, path, strlen(path)); + fputs("</h2>\n", fp); + } + + fputs("<table id=\"files\"><thead>\n<tr>" + "<td id=\"file-mode\"><b>Mode</b></td><td><b>Name</b></td>" + "<td id=\"file-size\" class=\"num\" align=\"right\"><b>Size</b></td>" + "</tr>\n</thead><tbody>\n", fp); + + if (strlen(path) > 0) { + if (strlcpy(tmp, path, sizeof(tmp)) >= sizeof(tmp)) + errx(1, "path truncated: '%s'", path); + parent = strrchr(tmp, '/'); + if (parent == NULL) + parent = "files"; + else { + *parent = '\0'; + parent = strrchr(tmp, '/'); + if (parent == NULL) + parent = tmp; + else + ++parent; + } + /* fputs("<tr><td>d---------</td><td><a class=\"dir\" href=\"../", fp); */ + /* xmlencode(fp, parent, strlen(parent)); */ + + fprintf(fp, "<tr style=\"cursor: pointer; cursor: hand;\" onclick=\"window.location.href=\'../"); + percentencode(fp, parent, strlen(parent)); + fputs(".html\'\">", fp); + + fputs("<td id=\"file-mode\">d---------</td><td><a class=\"dir\" href=\"../", fp); + xmlencode(fp, parent, strlen(parent)); + + /* fputs(".html\">..</a></td><td class=\"num\" align=\"right\"></td></tr>\n", fp); */ + fputs(".html\">..</a></td><td class=\"num\" align=\"right\"></td></tr>\n", fp); + } count = git_tree_entrycount(tree); for (i = 0; i < count; i++) { @@ -1067,37 +1145,68 @@ writefilestree(FILE *fp, git_tree *tree, const char *path) entrypath); if (r < 0 || (size_t)r >= sizeof(filepath)) errx(1, "path truncated: 'file/%s.html'", entrypath); + rf = snprintf(rawpath, sizeof(rawpath), "raw/%s", + entrypath); + if (rf < 0 || (size_t)rf >= sizeof(rawpath)) + errx(1, "path truncated: 'raw/%s'", entrypath); if (!git_tree_entry_to_object(&obj, repo, entry)) { switch (git_object_type(obj)) { case GIT_OBJ_BLOB: + is_obj_tree = 0; + filesize = git_blob_rawsize((git_blob *)obj); + lc = writeblob(obj, filepath, rawpath, entryname, filesize); + writeblobraw((git_blob *)obj, rawpath, entryname, filesize); break; case GIT_OBJ_TREE: + mkdirfile(filepath); + + if (strlcpy(tmp, relpath, sizeof(tmp)) >= sizeof(tmp)) + errx(1, "path truncated: '%s'", relpath); + if (strlcat(tmp, "../", sizeof(tmp)) >= sizeof(tmp)) + errx(1, "path truncated: '../%s'", tmp); + + oldrelpath = relpath; + relpath = tmp; + fp_subtree = efopen(filepath, "w"); + strlcpy(tmp2, "Files - ", sizeof(tmp2)); + if (strlcat(tmp2, entrypath, sizeof(tmp2)) >= sizeof(tmp2)) + errx(1, "path truncated: '%s'", tmp2); + writeheader(fp_subtree, tmp2); /* NOTE: recurses */ - ret = writefilestree(fp, (git_tree *)obj, + ret = writefilestree(fp_subtree, (git_tree *)obj, entrypath); - git_object_free(obj); + writefooter(fp_subtree); + relpath = oldrelpath; + lc = 0; + is_obj_tree = 1; if (ret) return ret; - continue; + break; default: git_object_free(obj); continue; } - - filesize = git_blob_rawsize((git_blob *)obj); - lc = writeblob(obj, filepath, entryname, filesize); - fputs("<tr><td>", fp); fputs(filemode(git_tree_entry_filemode(entry)), fp); - fprintf(fp, "</td><td><a href=\"%s", relpath); + fprintf(fp, "</td>"); + + + if (git_object_type(obj) == GIT_OBJ_TREE) + fprintf(fp, "<td id=\"dir-name\"><a href=\"%s", relpath); + else + fprintf(fp, "<td id=\"file-name\"><a href=\"%s", relpath); + /* fputs("id=\"files-dir\" ", fp); */ + + /* fprintf(fp, "href=\"%s", relpath); */ + percentencode(fp, filepath, strlen(filepath)); fputs("\">", fp); - xmlencode(fp, entrypath, strlen(entrypath)); + xmlencode(fp, entryname, strlen(entryname)); fputs("</a></td><td class=\"num\" align=\"right\">", fp); if (lc > 0) fprintf(fp, "%zuL", lc); - else + else if (!is_obj_tree) fprintf(fp, "%zuB", filesize); fputs("</td></tr>\n", fp); git_object_free(obj); @@ -1113,6 +1222,7 @@ writefilestree(FILE *fp, git_tree *tree, const char *path) } } + fputs("</tbody></table>", fp); return 0; } @@ -1123,17 +1233,10 @@ writefiles(FILE *fp, const git_oid *id) git_commit *commit = NULL; int ret = -1; - fputs("<table id=\"files\"><thead>\n<tr>" - "<td><b>Mode</b></td><td><b>Name</b></td>" - "<td class=\"num\" align=\"right\"><b>Size</b></td>" - "</tr>\n</thead><tbody>\n", fp); - if (!git_commit_lookup(&commit, repo, id) && !git_commit_tree(&tree, commit)) ret = writefilestree(fp, tree, ""); - fputs("</tbody></table>", fp); - git_commit_free(commit); git_tree_free(tree); @@ -1205,6 +1308,12 @@ usage(char *argv0) exit(1); } +void +process_output_md(const char* text, unsigned int size, void* fp) +{ + fprintf((FILE *)fp, "%.*s", size, text); +} + int main(int argc, char *argv[]) { @@ -1215,7 +1324,7 @@ main(int argc, char *argv[]) char path[PATH_MAX], repodirabs[PATH_MAX + 1], *p; char tmppath[64] = "cache.XXXXXXXXXXXX", buf[BUFSIZ]; size_t n; - int i, fd; + int i, fd, r; for (i = 1; i < argc; i++) { if (argv[i][0] != '-') { @@ -1335,6 +1444,7 @@ main(int argc, char *argv[]) if (!git_revparse_single(&obj, repo, readmefiles[i]) && git_object_type(obj) == GIT_OBJ_BLOB) readme = readmefiles[i] + strlen("HEAD:"); + r = i; git_object_free(obj); } @@ -1343,6 +1453,29 @@ main(int argc, char *argv[]) submodules = ".gitmodules"; git_object_free(obj); + /* about page */ + if (readme) { + fp = efopen("readme.html", "w"); + writeheader(fp, "README"); + git_revparse_single(&obj, repo, readmefiles[r]); + const char *s = git_blob_rawcontent((git_blob *)obj); + if (r == 1) { + git_off_t len = git_blob_rawsize((git_blob *)obj); + fputs("<div id=\"readme\">", fp); + if (md_html(s, len, process_output_md, fp, MD_FLAG_TABLES | MD_FLAG_TASKLISTS | + MD_FLAG_PERMISSIVEEMAILAUTOLINKS | MD_FLAG_PERMISSIVEURLAUTOLINKS, 0)) + err(1, "error parsing markdown"); + fputs("</div>\n", fp); + } else { + fputs("<pre id=\"readme\">", fp); + xmlencode(fp, s, strlen(s)); + fputs("</pre>\n", fp); + } + git_object_free(obj); + writefooter(fp); + fclose(fp); + } + /* log for HEAD */ fp = efopen("log.html", "w"); relpath = "";