gout

A static git page generator
git clone https://git.bracken.jp/gout.git
Log | Files | Refs | README | LICENSE

commit e45efefa2b482397162bb687ca49ab909259426c
parent f1c0362fd86ca639e2694d1343afce39d39a80d6
Author: Chris Bracken <chris@bracken.jp>
Date:   Sat,  6 Jun 2026 14:42:32 +0900

writer: refactor reference writers to accumulate branches and tags

Instead of writing references directly to the stream during iteration,
gather reference details into a list and render them sequentially at the
end. This also avoids potential null dereferences if ref->commit is
unresolved.

The existing code was fine because we generally already feed these in in
contiguous groups, but fragile because we're depending on an upstream
implementation detail in the caller.

Diffstat:
Msrc/writer/gemini/refs.c | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/writer/gemini/refs_tests.c | 32++++++++++++++++++++++++++++++++
Msrc/writer/gopher/refs.c | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/writer/gopher/refs_tests.c | 33+++++++++++++++++++++++++++++++++
Msrc/writer/html/refs.c | 105++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/writer/html/refs_tests.c | 34++++++++++++++++++++++++++++++++++
6 files changed, 308 insertions(+), 100 deletions(-)

diff --git a/src/writer/gemini/refs.c b/src/writer/gemini/refs.c @@ -8,6 +8,7 @@ #include "format.h" #include "git/commit.h" +#include "third_party/openbsd/reallocarray.h" #include "utils.h" #include "writer/gemini/page.h" @@ -16,20 +17,32 @@ typedef struct { FILE* out; } GeminiRefsTable; +typedef struct { + char* shorthand; + time_t author_time; + char* author_name; +} RefData; + +typedef struct { + RefData* items; + size_t count; + size_t capacity; +} RefList; + struct GeminiRefs { const GitRepo* repo; const FileSystem* fs; FILE* out; GeminiPage* page; - GeminiRefsTable* branches; - GeminiRefsTable* tags; + RefList branches; + RefList tags; }; static GeminiRefsTable* gemini_refstable_create(const char* title, FILE* out); static void gemini_refstable_free(GeminiRefsTable* table); static void gemini_refstable_begin(GeminiRefsTable* table); static void gemini_refstable_add_ref(GeminiRefsTable* table, - const GitReference* ref); + const RefData* ref); static void gemini_refstable_end(GeminiRefsTable* table); static GeminiRefsTable* gemini_refstable_create(const char* title, FILE* out) { @@ -60,16 +73,15 @@ static void gemini_refstable_begin(GeminiRefsTable* table) { } static void gemini_refstable_add_ref(GeminiRefsTable* table, - const GitReference* ref) { + const RefData* ref) { assert(table != NULL); assert(ref != NULL); - GitCommit* commit = ref->commit; FILE* out = table->out; print_utf8_padded(out, ref->shorthand, 32, ' '); fprintf(out, " "); - print_time_short(out, commit->author_time); - fprintf(out, " %s\n", commit->author_name); + print_time_short(out, ref->author_time); + fprintf(out, " %s\n", ref->author_name); } static void gemini_refstable_end(GeminiRefsTable* table) { @@ -77,6 +89,36 @@ static void gemini_refstable_end(GeminiRefsTable* table) { fprintf(table->out, "```\n\n"); } +static void reflist_add(RefList* list, const GitReference* ref) { + if (list->count >= list->capacity) { + list->capacity = list->capacity == 0 ? 16 : list->capacity * 2; + list->items = reallocarray(list->items, list->capacity, sizeof(RefData)); + if (!list->items) { + err(1, "reallocarray"); + } + } + RefData* item = &list->items[list->count++]; + item->shorthand = estrdup(ref->shorthand); + if (ref->commit) { + item->author_time = ref->commit->author_time; + item->author_name = estrdup(ref->commit->author_name); + } else { + item->author_time = 0; + item->author_name = estrdup(""); + } +} + +static void reflist_clear(RefList* list) { + for (size_t i = 0; i < list->count; i++) { + free(list->items[i].shorthand); + free(list->items[i].author_name); + } + free(list->items); + list->items = NULL; + list->count = 0; + list->capacity = 0; +} + GeminiRefs* gemini_refs_create(const GitRepo* repo, const FileSystem* fs) { assert(repo != NULL); assert(fs != NULL); @@ -97,8 +139,8 @@ void gemini_refs_free(GeminiRefs* refs) { } refs->fs->fclose(refs->out); gemini_page_free(refs->page); - gemini_refstable_free(refs->branches); - gemini_refstable_free(refs->tags); + reflist_clear(&refs->branches); + reflist_clear(&refs->tags); free(refs); } @@ -112,38 +154,33 @@ void gemini_refs_add_ref(GeminiRefs* refs, const GitReference* ref) { assert(ref != NULL); switch (ref->type) { case kReftypeBranch: - if (!refs->branches) { - refs->branches = gemini_refstable_create("Branches", refs->out); - gemini_refstable_begin(refs->branches); - } - gemini_refstable_add_ref(refs->branches, ref); + reflist_add(&refs->branches, ref); break; case kReftypeTag: - if (refs->branches) { - gemini_refstable_end(refs->branches); - gemini_refstable_free(refs->branches); - refs->branches = NULL; - } - if (!refs->tags) { - refs->tags = gemini_refstable_create("Tags", refs->out); - gemini_refstable_begin(refs->tags); - } - gemini_refstable_add_ref(refs->tags, ref); + reflist_add(&refs->tags, ref); break; } } void gemini_refs_end(GeminiRefs* refs) { assert(refs != NULL); - if (refs->branches) { - gemini_refstable_end(refs->branches); - gemini_refstable_free(refs->branches); - refs->branches = NULL; + if (refs->branches.count > 0) { + GeminiRefsTable* table = gemini_refstable_create("Branches", refs->out); + gemini_refstable_begin(table); + for (size_t i = 0; i < refs->branches.count; i++) { + gemini_refstable_add_ref(table, &refs->branches.items[i]); + } + gemini_refstable_end(table); + gemini_refstable_free(table); } - if (refs->tags) { - gemini_refstable_end(refs->tags); - gemini_refstable_free(refs->tags); - refs->tags = NULL; + if (refs->tags.count > 0) { + GeminiRefsTable* table = gemini_refstable_create("Tags", refs->out); + gemini_refstable_begin(table); + for (size_t i = 0; i < refs->tags.count; i++) { + gemini_refstable_add_ref(table, &refs->tags.items[i]); + } + gemini_refstable_end(table); + gemini_refstable_free(table); } gemini_page_end(refs->page); } diff --git a/src/writer/gemini/refs_tests.c b/src/writer/gemini/refs_tests.c @@ -47,3 +47,35 @@ UTEST_F(gemini_refs, branches) { "Author", "main", "2023-12-08 10:30", "User A", "```"); EXPECT_EQ(NULL, strstr(buf, "=> commit/")); } + +UTEST_F(gemini_refs, interleaved) { + GitRepo repo = {.short_name = "test-repo"}; + GeminiRefs* refs = gemini_refs_create(&repo, g_fs_inmemory); + ASSERT_NE(NULL, refs); + + GitCommit commit1 = {.author_name = "User 1", .author_time = 1000}; + GitReference branch1 = { + .type = kReftypeBranch, .shorthand = "main", .commit = &commit1}; + + GitCommit commit2 = {.author_name = "User 2", .author_time = 2000}; + GitReference tag = { + .type = kReftypeTag, .shorthand = "v1", .commit = &commit2}; + + GitCommit commit3 = {.author_name = "User 3", .author_time = 3000}; + GitReference branch2 = { + .type = kReftypeBranch, .shorthand = "dev", .commit = &commit3}; + + gemini_refs_begin(refs); + gemini_refs_add_ref(refs, &branch1); + gemini_refs_add_ref(refs, &tag); + gemini_refs_add_ref(refs, &branch2); + gemini_refs_end(refs); + gemini_refs_free(refs); + + const char* buf = inmemory_fs_get_buffer("refs.gmi"); + ASSERT_NE(NULL, buf); + + // Expect all branches to be grouped under "Branches", and all tags under + // "Tags", without duplicate "Branches" headers or formatting errors. + EXPECT_STR_SEQUENCE(buf, "## Branches", "main", "dev", "## Tags", "v1"); +} diff --git a/src/writer/gopher/refs.c b/src/writer/gopher/refs.c @@ -7,6 +7,7 @@ #include "format.h" #include "git/commit.h" +#include "third_party/openbsd/reallocarray.h" #include "utils.h" #include "writer/gopher/page.h" @@ -16,13 +17,25 @@ typedef struct { FILE* out; } GopherRefsTable; +typedef struct { + char* shorthand; + time_t author_time; + char* author_name; +} RefData; + +typedef struct { + RefData* items; + size_t count; + size_t capacity; +} RefList; + struct GopherRefs { const GitRepo* repo; const FileSystem* fs; FILE* out; GopherPage* page; - GopherRefsTable* branches; - GopherRefsTable* tags; + RefList branches; + RefList tags; }; static GopherRefsTable* gopher_refstable_create(const char* title, @@ -31,7 +44,7 @@ static GopherRefsTable* gopher_refstable_create(const char* title, static void gopher_refstable_free(GopherRefsTable* table); static void gopher_refstable_begin(GopherRefsTable* table); static void gopher_refstable_add_ref(GopherRefsTable* table, - const GitReference* ref); + const RefData* ref); static void gopher_refstable_end(GopherRefsTable* table); static GopherRefsTable* gopher_refstable_create(const char* title, @@ -66,18 +79,17 @@ static void gopher_refstable_begin(GopherRefsTable* table) { } static void gopher_refstable_add_ref(GopherRefsTable* table, - const GitReference* ref) { + const RefData* ref) { assert(table != NULL); assert(ref != NULL); - GitCommit* commit = ref->commit; FILE* out = table->out; fprintf(out, " "); print_gopher_link_padded(out, ref->shorthand, 32, ' '); fprintf(out, " "); - print_time_short(out, commit->author_time); + print_time_short(out, ref->author_time); fprintf(out, " "); - print_gopher_link_padded(out, commit->author_name, 25, '\0'); + print_gopher_link_padded(out, ref->author_name, 25, '\0'); fprintf(out, "\n"); } @@ -87,6 +99,36 @@ static void gopher_refstable_end(GopherRefsTable* table) { fprintf(out, "\n"); } +static void reflist_add(RefList* list, const GitReference* ref) { + if (list->count >= list->capacity) { + list->capacity = list->capacity == 0 ? 16 : list->capacity * 2; + list->items = reallocarray(list->items, list->capacity, sizeof(RefData)); + if (!list->items) { + err(1, "reallocarray"); + } + } + RefData* item = &list->items[list->count++]; + item->shorthand = estrdup(ref->shorthand); + if (ref->commit) { + item->author_time = ref->commit->author_time; + item->author_name = estrdup(ref->commit->author_name); + } else { + item->author_time = 0; + item->author_name = estrdup(""); + } +} + +static void reflist_clear(RefList* list) { + for (size_t i = 0; i < list->count; i++) { + free(list->items[i].shorthand); + free(list->items[i].author_name); + } + free(list->items); + list->items = NULL; + list->count = 0; + list->capacity = 0; +} + GopherRefs* gopher_refs_create(const GitRepo* repo, const FileSystem* fs) { assert(repo != NULL); assert(fs != NULL); @@ -107,8 +149,8 @@ void gopher_refs_free(GopherRefs* refs) { } refs->fs->fclose(refs->out); gopher_page_free(refs->page); - gopher_refstable_free(refs->branches); - gopher_refstable_free(refs->tags); + reflist_clear(&refs->branches); + reflist_clear(&refs->tags); free(refs); } @@ -122,39 +164,34 @@ void gopher_refs_add_ref(GopherRefs* refs, const GitReference* ref) { assert(ref != NULL); switch (ref->type) { case kReftypeBranch: - if (!refs->branches) { - refs->branches = - gopher_refstable_create("Branches", "branches", refs->out); - gopher_refstable_begin(refs->branches); - } - gopher_refstable_add_ref(refs->branches, ref); + reflist_add(&refs->branches, ref); break; case kReftypeTag: - if (refs->branches) { - gopher_refstable_end(refs->branches); - gopher_refstable_free(refs->branches); - refs->branches = NULL; - } - if (!refs->tags) { - refs->tags = gopher_refstable_create("Tags", "tags", refs->out); - gopher_refstable_begin(refs->tags); - } - gopher_refstable_add_ref(refs->tags, ref); + reflist_add(&refs->tags, ref); break; } } void gopher_refs_end(GopherRefs* refs) { assert(refs != NULL); - if (refs->branches) { - gopher_refstable_end(refs->branches); - gopher_refstable_free(refs->branches); - refs->branches = NULL; + if (refs->branches.count > 0) { + GopherRefsTable* table = + gopher_refstable_create("Branches", "branches", refs->out); + gopher_refstable_begin(table); + for (size_t i = 0; i < refs->branches.count; i++) { + gopher_refstable_add_ref(table, &refs->branches.items[i]); + } + gopher_refstable_end(table); + gopher_refstable_free(table); } - if (refs->tags) { - gopher_refstable_end(refs->tags); - gopher_refstable_free(refs->tags); - refs->tags = NULL; + if (refs->tags.count > 0) { + GopherRefsTable* table = gopher_refstable_create("Tags", "tags", refs->out); + gopher_refstable_begin(table); + for (size_t i = 0; i < refs->tags.count; i++) { + gopher_refstable_add_ref(table, &refs->tags.items[i]); + } + gopher_refstable_end(table); + gopher_refstable_free(table); } gopher_page_end(refs->page); } diff --git a/src/writer/gopher/refs_tests.c b/src/writer/gopher/refs_tests.c @@ -96,3 +96,36 @@ UTEST_F(gopher_refs, both) { ASSERT_NE(NULL, buf); EXPECT_STR_SEQUENCE(buf, "Branches", "main", "Tags", "v1"); } + +UTEST_F(gopher_refs, interleaved) { + GitRepo repo = {.short_name = "test-repo"}; + GopherRefs* refs = gopher_refs_create(&repo, g_fs_inmemory); + ASSERT_NE(NULL, refs); + + GitCommit commit1 = {.author_name = "User 1", .author_time = 1000}; + GitReference branch1 = { + .type = kReftypeBranch, .shorthand = "main", .commit = &commit1}; + + GitCommit commit2 = {.author_name = "User 2", .author_time = 2000}; + GitReference tag = { + .type = kReftypeTag, .shorthand = "v1", .commit = &commit2}; + + GitCommit commit3 = {.author_name = "User 3", .author_time = 3000}; + GitReference branch2 = { + .type = kReftypeBranch, .shorthand = "dev", .commit = &commit3}; + + gopher_refs_begin(refs); + gopher_refs_add_ref(refs, &branch1); + gopher_refs_add_ref(refs, &tag); + gopher_refs_add_ref(refs, &branch2); + gopher_refs_end(refs); + gopher_refs_free(refs); + + const char* buf = inmemory_fs_get_buffer("refs.gph"); + ASSERT_NE(NULL, buf); + + // Expect all branches to be grouped under "Branches", and all tags under + // "Tags", without duplicate "Branches" headers or mixed output order in + // files. + EXPECT_STR_SEQUENCE(buf, "Branches", "main", "dev", "Tags", "v1"); +} diff --git a/src/writer/html/refs.c b/src/writer/html/refs.c @@ -7,6 +7,7 @@ #include "format.h" #include "git/commit.h" +#include "third_party/openbsd/reallocarray.h" #include "utils.h" #include "writer/html/page.h" @@ -16,13 +17,25 @@ typedef struct { FILE* out; } HtmlRefsTable; +typedef struct { + char* shorthand; + time_t author_time; + char* author_name; +} RefData; + +typedef struct { + RefData* items; + size_t count; + size_t capacity; +} RefList; + struct HtmlRefs { const GitRepo* repo; const FileSystem* fs; FILE* out; HtmlPage* page; - HtmlRefsTable* branches; - HtmlRefsTable* tags; + RefList branches; + RefList tags; }; static HtmlRefsTable* html_refstable_create(const char* title, @@ -30,8 +43,7 @@ static HtmlRefsTable* html_refstable_create(const char* title, FILE* out); static void html_refstable_free(HtmlRefsTable* table); static void html_refstable_begin(HtmlRefsTable* table); -static void html_refstable_add_ref(HtmlRefsTable* table, - const GitReference* ref); +static void html_refstable_add_ref(HtmlRefsTable* table, const RefData* ref); static void html_refstable_end(HtmlRefsTable* table); static HtmlRefsTable* html_refstable_create(const char* title, @@ -70,17 +82,15 @@ static void html_refstable_begin(HtmlRefsTable* table) { "</thead><tbody>\n"); } -static void html_refstable_add_ref(HtmlRefsTable* table, - const GitReference* ref) { +static void html_refstable_add_ref(HtmlRefsTable* table, const RefData* ref) { assert(table != NULL); assert(ref != NULL); - GitCommit* commit = ref->commit; fprintf(table->out, "<tr><td>"); print_xml_encoded(table->out, ref->shorthand); fprintf(table->out, "</td><td>"); - print_time_short(table->out, commit->author_time); + print_time_short(table->out, ref->author_time); fprintf(table->out, "</td><td>"); - print_xml_encoded(table->out, commit->author_name); + print_xml_encoded(table->out, ref->author_name); fprintf(table->out, "</td></tr>\n"); } @@ -89,6 +99,36 @@ static void html_refstable_end(HtmlRefsTable* table) { fprintf(table->out, "</tbody></table><br/>\n"); } +static void reflist_add(RefList* list, const GitReference* ref) { + if (list->count >= list->capacity) { + list->capacity = list->capacity == 0 ? 16 : list->capacity * 2; + list->items = reallocarray(list->items, list->capacity, sizeof(RefData)); + if (!list->items) { + err(1, "reallocarray"); + } + } + RefData* item = &list->items[list->count++]; + item->shorthand = estrdup(ref->shorthand); + if (ref->commit) { + item->author_time = ref->commit->author_time; + item->author_name = estrdup(ref->commit->author_name); + } else { + item->author_time = 0; + item->author_name = estrdup(""); + } +} + +static void reflist_clear(RefList* list) { + for (size_t i = 0; i < list->count; i++) { + free(list->items[i].shorthand); + free(list->items[i].author_name); + } + free(list->items); + list->items = NULL; + list->count = 0; + list->capacity = 0; +} + HtmlRefs* html_refs_create(const GitRepo* repo, const FileSystem* fs) { assert(repo != NULL); assert(fs != NULL); @@ -109,8 +149,8 @@ void html_refs_free(HtmlRefs* refs) { } refs->fs->fclose(refs->out); html_page_free(refs->page); - html_refstable_free(refs->branches); - html_refstable_free(refs->tags); + reflist_clear(&refs->branches); + reflist_clear(&refs->tags); free(refs); } @@ -124,39 +164,34 @@ void html_refs_add_ref(HtmlRefs* refs, const GitReference* ref) { assert(ref != NULL); switch (ref->type) { case kReftypeBranch: - if (!refs->branches) { - refs->branches = - html_refstable_create("Branches", "branches", refs->out); - html_refstable_begin(refs->branches); - } - html_refstable_add_ref(refs->branches, ref); + reflist_add(&refs->branches, ref); break; case kReftypeTag: - if (refs->branches) { - html_refstable_end(refs->branches); - html_refstable_free(refs->branches); - refs->branches = NULL; - } - if (!refs->tags) { - refs->tags = html_refstable_create("Tags", "tags", refs->out); - html_refstable_begin(refs->tags); - } - html_refstable_add_ref(refs->tags, ref); + reflist_add(&refs->tags, ref); break; } } void html_refs_end(HtmlRefs* refs) { assert(refs != NULL); - if (refs->branches) { - html_refstable_end(refs->branches); - html_refstable_free(refs->branches); - refs->branches = NULL; + if (refs->branches.count > 0) { + HtmlRefsTable* table = + html_refstable_create("Branches", "branches", refs->out); + html_refstable_begin(table); + for (size_t i = 0; i < refs->branches.count; i++) { + html_refstable_add_ref(table, &refs->branches.items[i]); + } + html_refstable_end(table); + html_refstable_free(table); } - if (refs->tags) { - html_refstable_end(refs->tags); - html_refstable_free(refs->tags); - refs->tags = NULL; + if (refs->tags.count > 0) { + HtmlRefsTable* table = html_refstable_create("Tags", "tags", refs->out); + html_refstable_begin(table); + for (size_t i = 0; i < refs->tags.count; i++) { + html_refstable_add_ref(table, &refs->tags.items[i]); + } + html_refstable_end(table); + html_refstable_free(table); } html_page_end(refs->page); } diff --git a/src/writer/html/refs_tests.c b/src/writer/html/refs_tests.c @@ -96,3 +96,37 @@ UTEST_F(html_refs, both) { ASSERT_NE(NULL, buf); EXPECT_STR_SEQUENCE(buf, "<h2>Branches</h2>", "main", "<h2>Tags</h2>", "v1"); } + +UTEST_F(html_refs, interleaved) { + GitRepo repo = {.short_name = "test-repo"}; + HtmlRefs* refs = html_refs_create(&repo, g_fs_inmemory); + ASSERT_NE(NULL, refs); + + GitCommit commit1 = {.author_name = "User 1", .author_time = 1000}; + GitReference branch1 = { + .type = kReftypeBranch, .shorthand = "main", .commit = &commit1}; + + GitCommit commit2 = {.author_name = "User 2", .author_time = 2000}; + GitReference tag = { + .type = kReftypeTag, .shorthand = "v1", .commit = &commit2}; + + GitCommit commit3 = {.author_name = "User 3", .author_time = 3000}; + GitReference branch2 = { + .type = kReftypeBranch, .shorthand = "dev", .commit = &commit3}; + + html_refs_begin(refs); + html_refs_add_ref(refs, &branch1); + html_refs_add_ref(refs, &tag); + html_refs_add_ref(refs, &branch2); + html_refs_end(refs); + html_refs_free(refs); + + const char* buf = inmemory_fs_get_buffer("refs.html"); + ASSERT_NE(NULL, buf); + + // Expect all branches to be grouped under "Branches", and all tags under + // "Tags", without duplicate "Branches" headers or formatting/structural + // duplication. + EXPECT_STR_SEQUENCE(buf, "<h2>Branches</h2>", "main", "dev", "<h2>Tags</h2>", + "v1"); +}