commit b21912c284e8044119272d0ffeeb9efff50e0df7
parent 867d17666ea89d440406ec38833cbcdf2e94931f
Author: Michael Neuper <michael@michaelneuper.com>
Date: Sun, 25 Oct 2026 21:55:21 +0200
browse files by directory instead of one flat list
files.html lists the top level, directories first. Each directory gets a
page at tree/<path>.html with a .. row, and directory and file pages start
with the path, each parent directory linked to its listing.
Diffstat:
| M | README.md | | | 3 | +++ |
| M | stagit.c | | | 152 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------- |
2 files changed, 131 insertions(+), 24 deletions(-)
diff --git a/README.md b/README.md
@@ -12,6 +12,9 @@ mirror of my public GitHub repos. The upstream README is in `README`.
log.
- stagit-index links to each repo's front page instead of its log.
- The menu reads README | Files | Log | Refs.
+- Files are browsed by directory: `files.html` lists the top level, each
+ directory gets a page at `tree/<path>.html`, and file and directory pages
+ start with a linked path.
- cgit-flavoured `style.css`, and my own logo and favicon.
- The Makefile also finds libraries installed by Homebrew.
diff --git a/stagit.c b/stagit.c
@@ -980,6 +980,26 @@ writereadme(FILE *fp)
git_object_free(obj);
}
+/* path with the repository and each parent directory linked to its listing */
+void
+writepath(FILE *fp, const char *path)
+{
+ const char *s, *e;
+
+ fprintf(fp, "<a href=\"%sfiles.html\">", relpath);
+ xmlencode(fp, strippedname, strlen(strippedname));
+ fputs("</a>", fp);
+ for (s = path; (e = strchr(s, '/')); s = e + 1) {
+ fprintf(fp, "/<a href=\"%stree/", relpath);
+ percentencode(fp, path, e - path);
+ fputs(".html\">", fp);
+ xmlencode(fp, s, e - s);
+ fputs("</a>", fp);
+ }
+ putc('/', fp);
+ xmlencode(fp, s, strlen(s));
+}
+
size_t
writeblob(git_object *obj, const char *fpath, const char *filename, size_t filesize)
{
@@ -1004,7 +1024,7 @@ writeblob(git_object *obj, const char *fpath, const char *filename, size_t files
fp = efopen(fpath, "w");
writeheader(fp, filename);
fputs("<p> ", fp);
- xmlencode(fp, filename, strlen(filename));
+ writepath(fp, filename);
fprintf(fp, " (%zuB)", filesize);
fputs("</p><hr/>", fp);
@@ -1064,21 +1084,120 @@ filemode(git_filemode_t m)
return mode;
}
+int writefilestree(FILE *fp, git_tree *tree, const char *path, const char *rel);
+
int
-writefilestree(FILE *fp, git_tree *tree, const char *path)
+writefilestable(FILE *fp, git_tree *tree, const char *path, const char *rel)
+{
+ int ret;
+
+ 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);
+ ret = writefilestree(fp, tree, path, rel);
+ fputs("</tbody></table>", fp);
+
+ return ret;
+}
+
+/* listing of a directory, written to tree/path.html */
+int
+writetree(git_tree *tree, const char *path)
+{
+ char fpath[PATH_MAX], tmp[PATH_MAX], rel[PATH_MAX] = "", *d;
+ const char *p;
+ FILE *fp;
+ int r, ret;
+
+ r = snprintf(fpath, sizeof(fpath), "tree/%s.html", path);
+ if (r < 0 || (size_t)r >= sizeof(fpath))
+ errx(1, "path truncated: 'tree/%s.html'", path);
+
+ 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; *p; p++) {
+ if (*p == '/' && strlcat(rel, "../", sizeof(rel)) >= sizeof(rel))
+ errx(1, "path truncated: '../%s'", rel);
+ }
+ relpath = rel;
+
+ fp = efopen(fpath, "w");
+ writeheader(fp, path);
+ fputs("<p> ", fp);
+ writepath(fp, path);
+ fputs("/</p><hr/>", fp);
+ relpath = "";
+ ret = writefilestable(fp, tree, path, rel);
+ writefooter(fp);
+ checkfileerror(fp, fpath, 'w');
+ fclose(fp);
+
+ return ret;
+}
+
+/* rows for the entries of a directory, with links relative to rel */
+int
+writefilestree(FILE *fp, git_tree *tree, const char *path, const char *rel)
{
const git_tree_entry *entry = NULL;
git_object *obj = NULL;
- const char *entryname;
+ const char *entryname, *p;
char filepath[PATH_MAX], entrypath[PATH_MAX], oid[8];
size_t count, i, lc, filesize;
int r, ret;
+ if (path[0]) {
+ fprintf(fp, "<tr><td></td><td><a href=\"%s", rel);
+ if ((p = strrchr(path, '/'))) {
+ fputs("tree/", fp);
+ percentencode(fp, path, p - path);
+ fputs(".html", fp);
+ } else {
+ fputs("files.html", fp);
+ }
+ fputs("\">..</a></td><td class=\"num\" align=\"right\"></td></tr>\n", fp);
+ }
+
count = git_tree_entrycount(tree);
+
+ /* directories first */
+ for (i = 0; i < count; i++) {
+ if (!(entry = git_tree_entry_byindex(tree, i)) ||
+ !(entryname = git_tree_entry_name(entry)))
+ return -1;
+ if (git_tree_entry_type(entry) != GIT_OBJ_TREE)
+ continue;
+ joinpath(entrypath, sizeof(entrypath), path, entryname);
+
+ if (git_tree_entry_to_object(&obj, repo, entry))
+ return -1;
+ /* NOTE: recurses */
+ ret = writetree((git_tree *)obj, entrypath);
+ git_object_free(obj);
+ if (ret)
+ return ret;
+
+ fputs("<tr><td>", fp);
+ fputs(filemode(git_tree_entry_filemode(entry)), fp);
+ fprintf(fp, "</td><td><a href=\"%stree/", rel);
+ percentencode(fp, entrypath, strlen(entrypath));
+ fputs(".html\">", fp);
+ xmlencode(fp, entryname, strlen(entryname));
+ fputs("/</a></td><td class=\"num\" align=\"right\"></td></tr>\n", fp);
+ }
+
for (i = 0; i < count; i++) {
if (!(entry = git_tree_entry_byindex(tree, i)) ||
!(entryname = git_tree_entry_name(entry)))
return -1;
+ if (git_tree_entry_type(entry) == GIT_OBJ_TREE)
+ continue;
joinpath(entrypath, sizeof(entrypath), path, entryname);
r = snprintf(filepath, sizeof(filepath), "file/%s.html",
@@ -1090,28 +1209,20 @@ writefilestree(FILE *fp, git_tree *tree, const char *path)
switch (git_object_type(obj)) {
case GIT_OBJ_BLOB:
break;
- case GIT_OBJ_TREE:
- /* NOTE: recurses */
- ret = writefilestree(fp, (git_tree *)obj,
- entrypath);
- git_object_free(obj);
- if (ret)
- return ret;
- continue;
default:
git_object_free(obj);
continue;
}
filesize = git_blob_rawsize((git_blob *)obj);
- lc = writeblob(obj, filepath, entryname, filesize);
+ lc = writeblob(obj, filepath, entrypath, filesize);
fputs("<tr><td>", fp);
fputs(filemode(git_tree_entry_filemode(entry)), fp);
- fprintf(fp, "</td><td><a href=\"%s", relpath);
+ fprintf(fp, "</td><td><a href=\"%s", rel);
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);
@@ -1122,8 +1233,8 @@ writefilestree(FILE *fp, git_tree *tree, const char *path)
} else if (git_tree_entry_type(entry) == GIT_OBJ_COMMIT) {
/* commit object in tree is a submodule */
fprintf(fp, "<tr><td>m---------</td><td><a href=\"%sfile/.gitmodules.html\">",
- relpath);
- xmlencode(fp, entrypath, strlen(entrypath));
+ rel);
+ xmlencode(fp, entryname, strlen(entryname));
fputs("</a> @ ", fp);
git_oid_tostr(oid, sizeof(oid), git_tree_entry_id(entry));
xmlencode(fp, oid, strlen(oid));
@@ -1141,16 +1252,9 @@ 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);
+ ret = writefilestable(fp, tree, "", "");
git_commit_free(commit);
git_tree_free(tree);