aboutsummaryrefslogtreecommitdiff
path: root/src/gistatic.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gistatic.c')
-rw-r--r--src/gistatic.c1176
1 files changed, 1175 insertions, 1 deletions
diff --git a/src/gistatic.c b/src/gistatic.c
index 061ed7e..8ab9aa0 100644
--- a/src/gistatic.c
+++ b/src/gistatic.c
@@ -1,3 +1,1177 @@
-int main(void) {
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <stdbool.h>
+#include <libgen.h>
+#include <string.h>
+#include <assert.h>
+#include <sys/stat.h>
+#include <errno.h>
+
+#include <git2.h>
+
+
+#ifdef TEST
+#include "unit-test.h"
+#endif
+
+// FIXME: translate
+static const char *const DEFAULT_TITLE = "Repositories";
+static const char *const LOGO_ALT = "Logo image of the repository list";
+static const char *const FOOTER_TEMPLATE = "Generated with %s";
+static const char *const LANG = "en";
+static const char *const NAME = "Name";
+static const char *const DESCRIPTION = "Description";
+static const char *const LAST_COMMIT = "Last commit";
+static const char *const USAGE = "Usage: -h -o -i -x...\n";
+
+
+static const char *const LOGO_STR = ""
+ "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n"
+ "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\">\n"
+ " <path d=\"M 0 8 L 1 8 L 1 9 L 0 9 L 0 8 Z\" />\n"
+ " <path d=\"M 0 13 L 1 13 L 1 14 L 0 14 L 0 13 Z\" />\n"
+ " <path d=\"M 1 8 L 2 8 L 2 9 L 1 9 L 1 8 Z\" />\n"
+ " <path d=\"M 1 13 L 2 13 L 2 14 L 1 14 L 1 13 Z\" />\n"
+ " <path d=\"M 2 8 L 3 8 L 3 9 L 2 9 L 2 8 Z\" />\n"
+ " <path d=\"M 2 13 L 3 13 L 3 14 L 2 14 L 2 13 Z\" />\n"
+ " <path d=\"M 3 8 L 4 8 L 4 9 L 3 9 L 3 8 Z\" />\n"
+ " <path d=\"M 3 13 L 4 13 L 4 14 L 3 14 L 3 13 Z\" />\n"
+ " <path d=\"M 4 7 L 5 7 L 5 8 L 4 8 L 4 7 Z\" />\n"
+ " <path d=\"M 4 8 L 5 8 L 5 9 L 4 9 L 4 8 Z\" />\n"
+ " <path d=\"M 4 13 L 5 13 L 5 14 L 4 14 L 4 13 Z\" />\n"
+ " <path d=\"M 5 6 L 6 6 L 6 7 L 5 7 L 5 6 Z\" />\n"
+ " <path d=\"M 5 7 L 6 7 L 6 8 L 5 8 L 5 7 Z\" />\n"
+ " <path d=\"M 5 13 L 6 13 L 6 14 L 5 14 L 5 13 Z\" />\n"
+ " <path d=\"M 6 5 L 7 5 L 7 6 L 6 6 L 6 5 Z\" />\n"
+ " <path d=\"M 6 6 L 7 6 L 7 7 L 6 7 L 6 6 Z\" />\n"
+ " <path d=\"M 6 14 L 7 14 L 7 15 L 6 15 L 6 14 Z\" />\n"
+ " <path d=\"M 7 1 L 8 1 L 8 2 L 7 2 L 7 1 Z\" />\n"
+ " <path d=\"M 7 14 L 8 14 L 8 15 L 7 15 L 7 14 Z\" />\n"
+ " <path d=\"M 7 15 L 8 15 L 8 16 L 7 16 L 7 15 Z\" />\n"
+ " <path d=\"M 7 2 L 8 2 L 8 3 L 7 3 L 7 2 Z\" />\n"
+ " <path d=\"M 7 3 L 8 3 L 8 4 L 7 4 L 7 3 Z\" />\n"
+ " <path d=\"M 7 4 L 8 4 L 8 5 L 7 5 L 7 4 Z\" />\n"
+ " <path d=\"M 7 5 L 8 5 L 8 6 L 7 6 L 7 5 Z\" />\n"
+ " <path d=\"M 8 1 L 9 1 L 9 2 L 8 2 L 8 1 Z\" />\n"
+ " <path d=\"M 8 15 L 9 15 L 9 16 L 8 16 L 8 15 Z\" />\n"
+ " <path d=\"M 9 1 L 10 1 L 10 2 L 9 2 L 9 1 Z\" />\n"
+ " <path d=\"M 9 2 L 10 2 L 10 3 L 9 3 L 9 2 Z\" />\n"
+ " <path d=\"M 9 6 L 10 6 L 10 7 L 9 7 L 9 6 Z\" />\n"
+ " <path d=\"M 9 15 L 10 15 L 10 16 L 9 16 L 9 15 Z\" />\n"
+ " <path d=\"M 10 2 L 11 2 L 11 3 L 10 3 L 10 2 Z\" />\n"
+ " <path d=\"M 10 3 L 11 3 L 11 4 L 10 4 L 10 3 Z\" />\n"
+ " <path d=\"M 10 4 L 11 4 L 11 5 L 10 5 L 10 4 Z\" />\n"
+ " <path d=\"M 10 5 L 11 5 L 11 6 L 10 6 L 10 5 Z\" />\n"
+ " <path d=\"M 10 6 L 11 6 L 11 7 L 10 7 L 10 6 Z\" />\n"
+ " <path d=\"M 11 6 L 12 6 L 12 7 L 11 7 L 11 6 Z\" />\n"
+ " <path d=\"M 11 8 L 12 8 L 12 9 L 11 9 L 11 8 Z\" />\n"
+ " <path d=\"M 10 15 L 11 15 L 11 16 L 10 16 L 10 15 Z\" />\n"
+ " <path d=\"M 11 10 L 12 10 L 12 11 L 11 11 L 11 10 Z\" />\n"
+ " <path d=\"M 11 12 L 12 12 L 12 13 L 11 13 L 11 12 Z\" />\n"
+ " <path d=\"M 11 14 L 12 14 L 12 15 L 11 15 L 11 14 Z\" />\n"
+ " <path d=\"M 11 15 L 12 15 L 12 16 L 11 16 L 11 15 Z\" />\n"
+ " <path d=\"M 12 6 L 13 6 L 13 7 L 12 7 L 12 6 Z\" />\n"
+ " <path d=\"M 12 8 L 13 8 L 13 9 L 12 9 L 12 8 Z\" />\n"
+ " <path d=\"M 12 10 L 13 10 L 13 11 L 12 11 L 12 10 Z\" />\n"
+ " <path d=\"M 12 12 L 13 12 L 13 13 L 12 13 L 12 12 Z\" />\n"
+ " <path d=\"M 12 14 L 13 14 L 13 15 L 12 15 L 12 14 Z\" />\n"
+ " <path d=\"M 13 6 L 14 6 L 14 7 L 13 7 L 13 6 Z\" />\n"
+ " <path d=\"M 13 8 L 14 8 L 14 9 L 13 9 L 13 8 Z\" />\n"
+ " <path d=\"M 13 10 L 14 10 L 14 11 L 13 11 L 13 10 Z\" />\n"
+ " <path d=\"M 13 12 L 14 12 L 14 13 L 13 13 L 13 12 Z\" />\n"
+ " <path d=\"M 13 13 L 14 13 L 14 14 L 13 14 L 13 13 Z\" />\n"
+ " <path d=\"M 13 14 L 14 14 L 14 15 L 13 15 L 13 14 Z\" />\n"
+ " <path d=\"M 14 7 L 15 7 L 15 8 L 14 8 L 14 7 Z\" />\n"
+ " <path d=\"M 14 8 L 15 8 L 15 9 L 14 9 L 14 8 Z\" />\n"
+ " <path d=\"M 14 9 L 15 9 L 15 10 L 14 10 L 14 9 Z\" />\n"
+ " <path d=\"M 14 10 L 15 10 L 15 11 L 14 11 L 14 10 Z\" />\n"
+ " <path d=\"M 14 11 L 15 11 L 15 12 L 14 12 L 14 11 Z\" />\n"
+ " <path d=\"M 14 12 L 15 12 L 15 13 L 14 13 L 14 12 Z\" />\n"
+ "</svg>\n";
+
+static const char *const STYLE_STR = ""
+ "body {\n"
+ " font-family: monospace;\n"
+ " max-width: 1100px;\n"
+ " margin: 0 auto 0 auto;\n"
+ "}\n"
+ "\n"
+ "img.logo {\n"
+ " height: 3em;\n"
+ " width: 3em;\n"
+ "}\n"
+ "\n"
+ ".idx-header {\n"
+ " display: flex;\n"
+ " align-items: center;\n"
+ " margin-top: 1em;\n"
+ " margin-bottom: 1em;\n"
+ "}\n"
+ "\n"
+ ".idx-header-description {\n"
+ " margin-left: 1em;\n"
+ "}\n"
+ "\n"
+ "hr {\n"
+ " border: 0;\n"
+ " border-top: 3px solid hsl(0, 0%, 67%);\n"
+ "}\n"
+ "\n"
+ "table {\n"
+ " margin-top: 2em;\n"
+ " margin-bottom: 2em;\n"
+ "}\n"
+ "\n"
+ "thead td {\n"
+ " font-weight: bold;\n"
+ " padding-bottom: 1em;\n"
+ "}\n"
+ "\n"
+ "tbody tr:hover {\n"
+ " background-color: hsl(0, 0%, 93%);"
+ "}\n"
+ "\n"
+ "td {\n"
+ " padding-left: 1em;\n"
+ " padding-right: 1em;\n"
+ "}\n"
+ "\n"
+ "footer { \n"
+ " text-align: center;\n"
+ "}\n"
+ "";
+
+
+#define PROGNAME "gistatic"
+static const char *const GIT_SUFFIX = ".git";
+static const char *const PROJECT_HOMEPAGE_LINK =
+ "<a href=\"https://euandreh.xyz/" PROGNAME "\">" PROGNAME "</a>.";
+
+static const int EXIT_ERROR = 1;
+static const int EXIT_USAGE = 2;
+
+
+static int usage(FILE *const fd) {
+ if (fprintf(fd, "%s", USAGE) < 0) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fprintf(): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ strerror(errno)
+ );
+ return -1;
+ }
return 0;
}
+
+static char *remove_suffix(char *const s, const char *const suffix) {
+ if (!s || !suffix) {
+ return NULL;
+ }
+
+ char *const name = strdup(s);
+ if (!name) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: strdup(\"s\"): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ strerror(errno)
+ );
+ return NULL;
+ }
+
+ const char *const match_suffix = strrchr(name, suffix[0]);
+ if (match_suffix && (strcmp(match_suffix, suffix) == 0)) {
+ const size_t suffix_idx = strlen(name) - strlen(suffix);
+ name[suffix_idx] = '\0';
+ }
+ return name;
+}
+
+#ifdef TEST
+static void test_remove_suffix() {
+ test_start("test_remove_suffix");
+ {
+ testing("empty string");
+ char *const s = remove_suffix("", "suffix");
+ assert(s);
+ assert(strcmp(s, "") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("empty suffix");
+ char *const s = remove_suffix("a string", "");
+ assert(s);
+ assert(strcmp(s, "a string") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("with no suffix match");
+ char *const s = remove_suffix("another string", "NO MATCH!");
+ assert(s);
+ assert(strcmp(s, "another string") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("with suffix bigger than string");
+ char *const s = remove_suffix("str", "a string");
+ assert(s);
+ assert(strcmp(s, "str") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("with a NULL string");
+ char *const s = remove_suffix(NULL, "a non-null suffix");
+ assert(s == NULL);
+ test_ok();
+ }
+ {
+ testing("with a NULL suffix");
+ char *const s = remove_suffix("not null", NULL);
+ assert(s == NULL);;
+ test_ok();
+ }
+ {
+ testing("both are NULL");
+ char *const s = remove_suffix(NULL, NULL);
+ assert(s == NULL);
+ test_ok();
+ }
+ {
+ testing("matching prefix");
+ char *const s = remove_suffix("aaa bbb ccc", "aaa");
+ assert(s);
+ assert(strcmp(s, "aaa bbb ccc") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("matching infix");
+ char *const s = remove_suffix("aaa bbb ccc", "bbb");
+ assert(s);
+ assert(strcmp(s, "aaa bbb ccc") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("with suffix equal to string");
+ char *const s = remove_suffix("string", "string");
+ assert(s);
+ assert(strcmp(s, "") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("with matching suffix");
+ char *const s = remove_suffix("string", "ing");
+ assert(s);
+ assert(strcmp(s, "str") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("example usage: with .git suffix");
+ char *const s = remove_suffix("reponame.git", GIT_SUFFIX);
+ assert(s);
+ assert(strcmp(s, "reponame") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("example usage: with full path");
+ char *const s = remove_suffix("an/example/path", "ath");
+ assert(s);
+ assert(strcmp(s, "an/example/p") == 0);
+ free(s);
+ test_ok();
+ }
+}
+#endif
+
+static char *strjoin(const char *const s1, const char *const s2) {
+ if (!s1 || !s2) {
+ return NULL;
+ }
+
+ const size_t size = strlen(s1) + strlen(s2) + sizeof('\0');
+ char *const s = malloc(size);
+ if (!s) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: malloc(%ld): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ size,
+ strerror(errno)
+ );
+ return NULL;
+ }
+
+ strcpy(s, s1);
+ strcat(s, s2);
+ return s;
+}
+
+#ifdef TEST
+static void test_strjoin() {
+ test_start("test_strjoin");
+ {
+ testing("joining empty strings");
+ char *const s = strjoin("", "");
+ assert(s);
+ assert(strcmp(s, "") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("joining NULL strings");
+ assert(strjoin(NULL, NULL) == NULL);
+ assert(strjoin("", NULL) == NULL);
+ assert(strjoin(NULL, "") == NULL);
+ test_ok();
+ }
+ {
+ testing("first string is empty");
+ char *const s = strjoin("", "second not empty");
+ assert(s);
+ assert(strcmp(s, "second not empty") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("second string is empty");
+ char *const s = strjoin("first not empty", "");
+ assert(s);
+ assert(strcmp(s, "first not empty") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("two non-empty strings");
+ char *const s = strjoin("abc", "def");
+ assert(s);
+ assert(strcmp(s, "abcdef") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("example usage: with file names");
+ char *const s = strjoin("../repository.git", "/description");
+ assert(s);
+ assert(strcmp(s, "../repository.git/description") == 0);
+ free(s);
+ test_ok();
+ }
+}
+#endif
+
+static char *formatted_date(const time_t time_sec) {
+ struct tm *time_utc = gmtime(&time_sec);
+ if (!time_utc) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: gmtime(%ld): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ time_sec,
+ strerror(errno)
+ );
+ return NULL;
+ }
+
+ // Expected size, plus breathing room
+ const size_t size = strlen("XXX-XX-XX XX:XX") * 2;
+ char *const formatted = malloc(size);
+ if (!formatted) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: malloc(%ld): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ size,
+ strerror(errno)
+ );
+ return NULL;
+ }
+
+ strftime(formatted, size, "%Y-%m-%d %H:%M", time_utc);
+ return formatted;
+}
+
+#ifdef TEST
+static void test_formatted_date() {
+ test_start("test_formatted_date");
+ {
+ testing("when given 0");
+ char *const s = formatted_date(0);
+ assert(s);
+ assert(strcmp(s, "1970-01-01 00:00") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("when given negative values");
+ char *s;
+
+ s = formatted_date(-1);
+ assert(s);
+ assert(strcmp(s, "1969-12-31 23:59") == 0);
+ free(s);
+
+ s = formatted_date(-123456789);
+ assert(s);
+ assert(strcmp(s, "1966-02-02 02:26") == 0);
+ free(s);
+
+ s = formatted_date(-999999999);
+ assert(s);
+ assert(strcmp(s, "1938-04-24 22:13") == 0);
+ free(s);
+
+ test_ok();
+ }
+ {
+ testing("when given recent date values");
+ char *s;
+
+ s = formatted_date(1627645522);
+ assert(s);
+ assert(strcmp(s, "2021-07-30 11:45") == 0);
+ free(s);
+
+ s = formatted_date(1500000000);
+ assert(s);
+ assert(strcmp(s, "2017-07-14 02:40") == 0);
+ free(s);
+
+ s = formatted_date(1000000000);
+ assert(s);
+ assert(strcmp(s, "2001-09-09 01:46") == 0);
+ free(s);
+
+ test_ok();
+ }
+}
+#endif
+
+static char *escape_html(const char *s) {
+ if (!s) {
+ return NULL;
+ }
+
+ #define AMPCHAR '&'
+ #define LTCHAR '<'
+ #define GTCHAR '>'
+ #define DQUOTCHAR '"'
+ #define SQUOTCHAR '\''
+
+ static const char *const AMP = "&amp;";
+ static const char *const LT = "&lt;";
+ static const char *const GT = "&gt;";
+ static const char *const DQUOT = "&quot;";
+ static const char *const SQUOT = "&#39;";
+
+ const size_t AMP_SIZE = strlen(AMP);
+ const size_t LT_SIZE = strlen(LT);
+ const size_t GT_SIZE = strlen(GT);
+ const size_t DQUOT_SIZE = strlen(DQUOT);
+ const size_t SQUOT_SIZE = strlen(SQUOT);
+
+ const size_t input_size = strlen(s);
+
+ size_t escaped_size = 0;
+ for (size_t i = 0; i < input_size; i++) {
+ switch (s[i]) {
+ case AMPCHAR:
+ escaped_size += AMP_SIZE;
+ break;
+ case LTCHAR:
+ escaped_size += LT_SIZE;
+ break;
+ case GTCHAR:
+ escaped_size += GT_SIZE;
+ break;
+ case DQUOTCHAR:
+ escaped_size += DQUOT_SIZE;
+ break;
+ case SQUOTCHAR:
+ escaped_size += SQUOT_SIZE;
+ break;
+ default:
+ escaped_size += sizeof(s[i]);
+ break;
+ }
+ }
+
+ const size_t size = escaped_size + sizeof('\0');
+ char *const escaped = malloc(size);
+ if (!escaped) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: malloc(%ld): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ size,
+ strerror(errno)
+ );
+ return NULL;
+ }
+ escaped[0] = '\0';
+
+ char c;
+ size_t escaped_idx = 0;
+ while ((c = *s++)) {
+ switch (c) {
+ case AMPCHAR:
+ strcat(escaped, AMP);
+ escaped_idx += AMP_SIZE;
+ break;
+ case LTCHAR:
+ strcat(escaped, LT);
+ escaped_idx += LT_SIZE;
+ break;
+ case GTCHAR:
+ strcat(escaped, GT);
+ escaped_idx += GT_SIZE;
+ break;
+ case DQUOTCHAR:
+ strcat(escaped, DQUOT);
+ escaped_idx += DQUOT_SIZE;
+ break;
+ case SQUOTCHAR:
+ strcat(escaped, SQUOT);
+ escaped_idx += SQUOT_SIZE;
+ break;
+ default:
+ escaped[escaped_idx] = c;
+ escaped[escaped_idx + 1] = '\0';
+ escaped_idx += sizeof(c);
+ break;
+ }
+ }
+
+ return escaped;
+}
+
+#ifdef TEST
+static void test_escape_html() {
+ test_start("test_escape_html");
+ {
+ testing("string with no escapable chars");
+ char *const s = escape_html("a plain string");
+ assert(s);
+ assert(strcmp(s, "a plain string") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("an empty string");
+ char *const s = escape_html("");
+ assert(s);
+ assert(strcmp(s, "") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("a NULL value");
+ char *const s = escape_html(NULL);
+ assert(s == NULL);
+ test_ok();
+ }
+ {
+ testing("string with a single & character");
+ char *const s = escape_html("&");
+ assert(s);
+ assert(strcmp(s, "&amp;") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("a string with many escapable characters in sequence");
+ char *const s = escape_html("&& >> text <<");
+ assert(s);
+ assert(strcmp(s, "&amp;&amp; &gt;&gt; text &lt;&lt;") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("all escapable characters");
+ char *const s = escape_html("&<>\"'");
+ assert(s);
+ assert(strcmp(s, "&amp;&lt;&gt;&quot;&#39;") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("example usage: /path/edn->json.git/");
+ char *const s = escape_html("/path/edn->json.git/");
+ assert(s);
+ assert(strcmp(s, "/path/edn-&gt;json.git/") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("example usage: Description"
+ " with \"quotes\" && 'quotes'");
+ char *const s = escape_html(
+ "Description with \"quotes\" && 'quotes'"
+ );
+ assert(s);
+ const char *const expected = "Description with"
+ " &quot;quotes&quot; &amp;&amp; &#39;quotes&#39;";
+ assert(strcmp(s, expected) == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("example usage: plain HTML");
+ char *const s = escape_html("<a href=\"#\">link</a>");
+ assert(s);
+ const char *const expected = "&lt;a"
+ " href=&quot;#&quot;&gt;link&lt;/a&gt;";
+ assert(strcmp(s, expected) == 0);
+ free(s);
+ test_ok();
+ }
+}
+#endif
+
+static int write_header(FILE *const fd, const char *const idx_title) {
+ const int e = fprintf(
+ fd,
+ ""
+ "<!DOCTYPE html>\n"
+ "<html lang=\"%s\">\n"
+ " <head>\n"
+ " <meta charset=\"UTF-8\" />\n"
+ " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n"
+ " <link rel=\"icon\" type=\"image/svg+xml\" href=\"logo.svg\" />\n"
+ " <link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\" />\n"
+ " <title>%s</title>\n"
+ " </head>\n"
+ " <body>\n"
+ " <header>\n"
+ " <h1 class=\"idx-header\">\n"
+ " <img alt=\"%s\" class=\"logo\" src=\"logo.svg\" />\n"
+ " <span class=\"idx-header-description\">\n"
+ " %s\n"
+ " </span>\n"
+ " </h1>\n"
+ " <hr />\n"
+ " </header>\n"
+ " <main>\n"
+ " <table>\n"
+ " <thead>\n"
+ " <tr>\n"
+ " <td>\n"
+ " %s\n"
+ " </td>\n"
+ " <td>\n"
+ " %s\n"
+ " </td>\n"
+ " <td>\n"
+ " %s\n"
+ " </td>\n"
+ " </tr>\n"
+ " </thead>\n"
+ " <tbody>\n",
+ LANG,
+ idx_title,
+ LOGO_ALT,
+ idx_title,
+ NAME,
+ DESCRIPTION,
+ LAST_COMMIT
+ );
+ if (e < 0) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fprintf(): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ strerror(errno)
+ );
+ return -1;
+ }
+ return 0;
+}
+
+static int write_footer(FILE *const fd) {
+ const size_t footer_size = strlen(FOOTER_TEMPLATE) - strlen("%s")
+ + strlen(PROJECT_HOMEPAGE_LINK) + sizeof('\0');
+ char *const footer_text = malloc(footer_size);
+ if (!footer_text) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: malloc(%ld): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ footer_size,
+ strerror(errno)
+ );
+ return -1;
+ }
+ sprintf(footer_text, FOOTER_TEMPLATE, PROJECT_HOMEPAGE_LINK);
+
+ const int e = fprintf(
+ fd,
+ ""
+ " </tbody>\n"
+ " </table>\n"
+ " </main>\n"
+ " <footer>\n"
+ " <hr />\n"
+ " <p>\n"
+ " %s\n"
+ " </p>\n"
+ " </footer>\n"
+ " </body>\n"
+ "</html>\n",
+ footer_text
+ );
+ free(footer_text);
+ if (e < 0) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fprintf(): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ strerror(errno)
+ );
+ return -1;
+ }
+
+ return 0;
+}
+
+static char *last_commit_date(struct git_repository *repo) {
+ struct git_commit *commit = NULL;
+ struct git_revwalk *walker = NULL;
+ struct git_oid oid;
+ char *ret;
+
+ if (
+ git_revwalk_new(&walker, repo)
+ || git_revwalk_push_head(walker)
+ || git_revwalk_next(&oid, walker)
+ || git_commit_lookup(&commit, repo, &oid)
+ ) {
+ char *const default_return = strdup("");
+ if (!default_return) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: strdup(\"\"): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ strerror(errno)
+ );
+ }
+ ret = default_return;
+ goto cleanup;
+ }
+
+ const struct git_signature *const author = git_commit_author(commit);
+ assert(author);
+ ret = formatted_date(author->when.time);
+
+cleanup:
+ git_commit_free(commit);
+ git_revwalk_free(walker);
+ return ret;
+}
+
+#ifdef TEST
+static void test_last_commit_date() {
+ test_start("test_last_commit_date");
+ {
+ testing("embedded Git repository");
+ struct git_repository *repo;
+ const int e = git_repository_open_ext(
+ &repo,
+ "tests/resources/embedded-repo",
+ GIT_REPOSITORY_OPEN_NO_SEARCH,
+ NULL
+ );
+ assert(e == 0);
+ char *const date = last_commit_date(repo);
+ assert(date);
+ assert(strcmp(date, "2021-07-30 14:18") == 0);
+ free(date);
+ git_repository_free(repo);
+ test_ok();
+ }
+}
+#endif
+
+static int write_logo(FILE *const fd) {
+ if (fprintf(fd, "%s", LOGO_STR) < 0) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fprintf(): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ strerror(errno)
+ );
+ return -1;
+ }
+ return 0;
+}
+
+static int write_style(FILE *const fd) {
+ if (fprintf(fd, "%s", STYLE_STR) < 0) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fprintf(): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ strerror(errno)
+ );
+ return -1;
+ }
+ return 0;
+}
+
+static int write_index_row(FILE *const fd, char *const repopath) {
+ int ret = 0;
+ char description[81] = "";
+ int e;
+
+ struct git_repository *repo = NULL;
+ char *name = NULL;
+ char *description_path = NULL;
+ char *date = NULL;
+ char *encoded_name = NULL;
+ char *encoded_description = NULL;
+ char *encoded_date = NULL;
+
+ e = git_repository_open_ext(
+ &repo,
+ repopath,
+ GIT_REPOSITORY_OPEN_NO_SEARCH,
+ NULL
+ );
+ if (e) {
+ fprintf(stderr, "%s: cannot open repository\n", repopath);
+ ret = 1;
+ goto cleanup;
+ }
+
+ if (
+ !(name = remove_suffix(basename(repopath), GIT_SUFFIX))
+ || !(description_path = strjoin(repopath, "/description"))
+ || !(date = last_commit_date(repo))
+ ) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ FILE *const f = fopen(description_path, "r");
+ if (f) {
+ if (!fgets(description, sizeof(description), f)) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fgets(\"%s\"): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ description_path,
+ strerror(errno)
+ );
+ }
+ if (fclose(f)) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fclose(\"%s\"): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ description_path,
+ strerror(errno)
+ );
+ ret = -1;
+ goto cleanup;
+ }
+ }
+
+ if (
+ !(encoded_name = escape_html(name))
+ || !(encoded_description = escape_html(description))
+ || !(encoded_date = escape_html(date))
+ ) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ e = fprintf(
+ fd,
+ ""
+ " <tr>\n"
+ " <td>\n"
+ " <a href=\"%s/\">\n"
+ " %s\n"
+ " </a>\n"
+ " </td>\n"
+ " <td>\n"
+ " %s\n"
+ " </td>\n"
+ " <td>\n"
+ " %s\n"
+ " </td>\n"
+ " </tr>\n",
+ encoded_name,
+ encoded_name,
+ encoded_description,
+ encoded_date
+ );
+ if (e < 0) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fprintf(): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ strerror(errno)
+ );
+ ret = -1;
+ goto cleanup;
+ }
+
+cleanup:
+ free(encoded_date);
+ free(encoded_description);
+ free(encoded_name);
+ free(date);
+ free(description_path);
+ free(name);
+ git_repository_free(repo);
+ return ret;
+}
+
+static int write_index(
+ const char *const outdir,
+ const char *const idx_description,
+ const int repoc,
+ char *const repov[]
+) {
+ int ret = 0;
+ char *index_path = NULL;
+ char *logo_path = NULL;
+ char *style_path = NULL;
+ FILE *index_fd = NULL;
+ FILE *logo_fd = NULL;
+ FILE *style_fd = NULL;
+
+ if (
+ !(index_path = strjoin(outdir, "/index.html"))
+ || !(logo_path = strjoin(outdir, "/logo.svg"))
+ || !(style_path = strjoin(outdir, "/style.css"))
+ ) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ if (!(index_fd = fopen(index_path, "w"))) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fopen(\"%s\"): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ index_path,
+ strerror(errno)
+ );
+ ret = -1;
+ goto cleanup;
+ }
+ if (!(logo_fd = fopen(logo_path, "w"))) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fopen(\"%s\"): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ logo_path,
+ strerror(errno)
+ );
+ ret = -1;
+ goto cleanup;
+ }
+ if (!(style_fd = fopen(style_path, "w"))) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fopen(\"%s\"): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ style_path,
+ strerror(errno)
+ );
+ ret = -1;
+ goto cleanup;
+ }
+
+ if (write_header(index_fd, idx_description)) {
+ ret = -1;
+ goto cleanup;
+ }
+ for (int i = 0; i < repoc; i++) {
+ const int e = write_index_row(index_fd, repov[i]);
+ if (e == -1) {
+ ret = -1;
+ goto cleanup;
+ }
+ if (e) {
+ ret = 1;
+ }
+ }
+
+ if (
+ write_footer(index_fd)
+ || write_logo(logo_fd)
+ || write_style(style_fd)
+ ) {
+ ret = -1;
+ goto cleanup;
+ }
+
+cleanup:
+ if (style_fd && fclose(style_fd)) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fclose(\"%s\"): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ style_path,
+ strerror(errno)
+ );
+ ret = -1;
+ }
+ if (logo_fd && fclose(logo_fd)) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fclose(\"%s\"): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ logo_path,
+ strerror(errno)
+ );
+ ret = -1;
+ }
+ if (index_fd && fclose(index_fd)) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: fclose(\"%s\"): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ index_path,
+ strerror(errno)
+ );
+ ret = -1;
+ }
+ free(style_path);
+ free(logo_path);
+ free(index_path);
+ return ret;
+}
+
+#ifdef TEST
+static void unit_tests(){
+ git_libgit2_init();
+ test_remove_suffix();
+ test_strjoin();
+ test_formatted_date();
+ test_escape_html();
+ test_last_commit_date();
+ git_libgit2_shutdown();
+}
+#endif
+
+int main(int argc, char *argv[]) {
+#ifdef TEST
+ unit_tests();
+ return EXIT_SUCCESS;
+#endif
+
+ if (argc < 2) {
+ usage(stderr);
+ return EXIT_USAGE;
+ }
+
+ int ret = 0;
+ int flag;
+ bool index = false;
+ const char *idx_title = DEFAULT_TITLE;
+ const char *outdir = ".";
+ while ((flag = getopt(argc, argv, "o:t:ihV")) != -1) {
+ switch (flag) {
+ case 'i':
+ index = true;
+ break;
+ case 't':
+ idx_title = optarg;
+ break;
+ case 'o':
+ outdir = optarg;
+ break;
+ default:
+ if (usage(stderr)) {
+ return EXIT_ERROR;
+ }
+ return EXIT_USAGE;
+ }
+ }
+
+ git_libgit2_init();
+
+ errno = 0;
+ if (
+ mkdir(outdir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)
+ && errno != EEXIST
+ ) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: mkdir(\"%s\"): %s\n",
+ PROGNAME,
+ __FILE__,
+ __LINE__,
+ outdir,
+ strerror(errno)
+ );
+ ret = 1;
+ goto cleanup;
+ }
+
+ if (index) {
+ if (write_index(outdir, idx_title,
+ argc - optind, argv + optind)) {
+ ret = 1;
+ goto cleanup;
+ }
+ }
+
+cleanup:
+ git_libgit2_shutdown();
+ return ret;
+}