aboutsummaryrefslogtreecommitdiff
path: root/src/lib.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib.c')
-rw-r--r--src/lib.c2181
1 files changed, 2181 insertions, 0 deletions
diff --git a/src/lib.c b/src/lib.c
new file mode 100644
index 0000000..8f067b7
--- /dev/null
+++ b/src/lib.c
@@ -0,0 +1,2181 @@
+#include "config.h"
+#include "tar.h"
+#include "gistatic.h"
+
+#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 <nl_types.h>
+#include <locale.h>
+
+#include <git2.h>
+
+
+#ifdef TEST
+#include "tests-lib.h"
+#endif
+
+
+static const int EXIT_ERROR = 1;
+static const int EXIT_USAGE = 2;
+
+
+#define PROGNAME "gistatic"
+static const char *const CATALOG_NAME = PROGNAME;
+static nl_catd catalog_descriptor = NULL;
+
+#define MSG_DEFAULT_TITLE 1
+#define MSG_LOGO_ALT_INDEX 2
+#define MSG_LOGO_ALT_REPOSITORY 3
+#define MSG_FOOTER_TEMPLATE 4
+#define MSG_LANG 5
+#define MSG_NAME 6
+#define MSG_DESCRIPTION 7
+#define MSG_LAST_COMMIT 8
+#define MSG_USAGE 9
+#define MSG_HELP 10
+#define MSG_COMMIT_FEED 11
+#define MSG_TAGS_FEED 12
+#define MSG_NAV_FILES 13
+#define MSG_NAV_LOG 14
+#define MSG_NAV_REFS 15
+#define MSG_THEAD_BRANCH 16
+#define MSG_THEAD_COMMITMSG 17
+#define MSG_THEAD_AUTHOR 18
+#define MSG_THEAD_DATE 19
+#define MSG_THEAD_TAG 20
+#define MSG_MISSING_URL 21
+#define MSG_MISSING_ARGS 22
+#define MSG_INCOMPATIBLE_OPTIONS 23
+#define MSG_INDEX_DESCRIPTION 24
+
+static const char *const MSGS[] = {
+ "",
+ [MSG_DEFAULT_TITLE]="Repositories",
+ [MSG_LOGO_ALT_INDEX]="Logo image of the repository list",
+ [MSG_LOGO_ALT_REPOSITORY]="Logo image of the repository",
+ [MSG_FOOTER_TEMPLATE]="Generated with %s",
+ [MSG_LANG]="en",
+ [MSG_NAME]="Name",
+ [MSG_DESCRIPTION]="Description",
+ [MSG_LAST_COMMIT]="Last commit",
+ [MSG_USAGE]=""
+ "Usage:\n"
+ " " PROGNAME " -i -o DIRECTORY REPOSITORY...\n"
+ " " PROGNAME " -o DIRECTORY -u CLONE_URL REPOSITORY\n"
+ " " PROGNAME " [-hV]\n"
+ "",
+ [MSG_HELP]=""
+ "\n"
+ "Options:\n"
+ " -i build the index page of the repositories\n"
+ " -u CLONE_URL address to be shown alongside \"git clone\"\n"
+ " -o DIRECTORY output where to place the generated files\n"
+ " -h, --help show this help\n"
+ " -V, --version print the version number\n"
+ "\n"
+ "See \"man gistatic\" for more information.\n"
+ "",
+ [MSG_COMMIT_FEED]="commit feed",
+ [MSG_TAGS_FEED]="tags feed",
+ [MSG_NAV_FILES]="files",
+ [MSG_NAV_LOG]="log",
+ [MSG_NAV_REFS]="refs",
+ [MSG_THEAD_BRANCH]="Branch",
+ [MSG_THEAD_COMMITMSG]="Commit message",
+ [MSG_THEAD_AUTHOR]="Author",
+ [MSG_THEAD_DATE]="Date",
+ [MSG_THEAD_TAG]="Tag",
+ [MSG_MISSING_URL]="Missing '-u CLONE_URL'",
+ [MSG_MISSING_ARGS]="Missing [PATH | [PATHS]]",
+ [MSG_INCOMPATIBLE_OPTIONS]="Incompatible options -u and -i",
+ [MSG_INDEX_DESCRIPTION]="Index of repositories",
+ NULL
+};
+
+#ifdef TEST
+static void dump_translatable_strings(void) {
+ const size_t size =
+ strlen(__FILE__) - strlen(".c") + strlen(".msg") + sizeof('\0');
+ char *const catalog_path = malloc(size);
+ assert(catalog_path);
+
+ strcpy(catalog_path, __FILE__);
+ catalog_path[strlen(__FILE__) - strlen(".c")] = '\0';
+ strcat(catalog_path, ".msg");
+
+ FILE *const f = fopen(catalog_path, "w");
+ assert(f);
+ for (size_t i = 1; MSGS[i] != NULL; i++) {
+ assert(fprintf(f, "%ld %s\n\n", i, MSGS[i]) > 0);
+ }
+
+ assert(fclose(f) == 0);
+ free(catalog_path);
+}
+#endif
+
+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 = ""
+ ":root {\n"
+ " --color: black;\n"
+ " --background-color: white;\n"
+ " --hover-color: hsl(0, 0%, 93%);\n"
+ " --nav-color: hsl(0, 0%, 87%);\n"
+ "}\n"
+ "\n"
+ "@media(prefers-color-scheme: dark) {\n"
+ " :root {\n"
+ " --color: white;\n"
+ " --background-color: black;\n"
+ " --hover-color: hsl(0, 0%, 7%);\n"
+ " --nav-color: hsl(0, 0%, 13%);\n"
+ " }\n"
+ "\n"
+ " body {\n"
+ " color: var(--color);\n"
+ " background-color: var(--background-color);\n"
+ " }\n"
+ "\n"
+ " a {\n"
+ " color: hsl(211, 100%, 60%);\n"
+ " }\n"
+ "\n"
+ " a:visited {\n"
+ " color: hsl(242, 100%, 80%);\n"
+ " }\n"
+ "}\n"
+ "\n"
+ "body {\n"
+ " font-family: monospace;\n"
+ " max-width: 1100px;\n"
+ " margin: 0 auto 0 auto;\n"
+ "}\n"
+ "\n"
+ ".logo {\n"
+ " height: 6em;\n"
+ " width: 6em;\n"
+ "}\n"
+ "\n"
+ ".header-horizontal-grouping {\n"
+ " display: flex;\n"
+ " align-items: center;\n"
+ " margin-top: 1em;\n"
+ " margin-bottom: 1em;\n"
+ "}\n"
+ "\n"
+ ".header-description {\n"
+ " margin-left: 2em;\n"
+ "}\n"
+ "\n"
+ "nav {\n"
+ " margin-top: 2em;\n"
+ "}\n"
+ "\n"
+ "nav ul {\n"
+ " display: flex;\n"
+ " list-style-type: none;\n"
+ " margin-bottom: 0;\n"
+ "}\n"
+ "\n"
+ "nav li {\n"
+ " margin-left: 10px;\n"
+ "}\n"
+ "\n"
+ "nav a, nav a:visited {\n"
+ " padding: 2px 8px 0px 8px;\n"
+ " color: var(--color);\n"
+ "}\n"
+ "\n"
+ ".selected-nav-item {\n"
+ " background-color: var(--nav-color);\n"
+ "}\n"
+ "\n"
+ "hr {\n"
+ " margin-top: 0;\n"
+ " border: 0;\n"
+ " border-top: 3px solid var(--nav-color);\n"
+ "}\n"
+ "\n"
+ "table {\n"
+ " margin: 2em auto;\n"
+ "}\n"
+ "\n"
+ "th {\n"
+ " padding-bottom: 1em;\n"
+ "}\n"
+ "\n"
+ "tbody tr:hover {\n"
+ " background-color: var(--hover-color);\n"
+ "}\n"
+ "\n"
+ "td {\n"
+ " padding-left: 1em;\n"
+ " padding-right: 1em;\n"
+ "}\n"
+ "\n"
+ "footer {\n"
+ " text-align: center;\n"
+ "}\n"
+ "";
+
+
+#define DESCRIPTION_MAXLENGTH 81
+static bool verbose = false;
+static const char *const GIT_SUFFIX = ".git";
+static const char *const PROJECT_HOMEPAGE_LINK =
+ "<a href=\"https://euandreh.xyz/" PROGNAME "\">" PROGNAME "</a>.";
+
+
+static void logerr(const char *const s, const char *const msg, int lineno) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: %s: %s\n",
+ PROGNAME,
+ __FILE__,
+ lineno,
+ s,
+ msg
+ );
+}
+
+static void logerrs(
+ const char *const pre,
+ const char *const mid,
+ const char *const post,
+ const char *const msg,
+ int lineno
+) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: %s%s%s: %s\n",
+ PROGNAME,
+ __FILE__,
+ lineno,
+ pre,
+ mid,
+ post,
+ msg
+ );
+}
+
+static void logerrl(
+ const char *const pre,
+ const size_t mid,
+ const char *const post,
+ const char *const msg,
+ int lineno
+) {
+ fprintf(
+ stderr,
+ "%s:%s:%d: %s%ld%s: %s\n",
+ PROGNAME,
+ __FILE__,
+ lineno,
+ pre,
+ mid,
+ post,
+ msg
+ );
+}
+
+static const char *_(int msg_id) {
+ if (!catalog_descriptor || catalog_descriptor == (nl_catd)-1) {
+ return MSGS[msg_id];
+ }
+ errno = 0;
+ const char *const ret =
+ catgets(catalog_descriptor, NL_SETD, msg_id, MSGS[msg_id]);
+ if (errno && verbose) {
+ logerrl("catgets(", msg_id, ")", strerror(errno), __LINE__);
+ }
+ return ret;
+}
+
+#ifdef TEST
+static void test_underscore(void) {
+ test_start("test_underscore");
+ const char *const original_locale = setlocale(LC_ALL, NULL);
+ setlocale(LC_ALL, "");
+ setenv("NLSPATH", "", 1);
+ {
+ testing("a NULL value for catalog_descriptor");
+ test_ok();
+ }
+ setlocale(LC_ALL, original_locale);
+}
+#endif
+
+static int print_msg(FILE *const fd, const char *const msg) {
+ if (fprintf(fd, "%s", msg) < 0) {
+ logerr("fprintf()", strerror(errno), __LINE__);
+ return -1;
+ }
+ return 0;
+}
+
+static int print_help(FILE *const fd) {
+ return print_msg(fd, _(MSG_HELP));
+}
+
+static int print_version(FILE *const fd) {
+ return print_msg(fd, PROGNAME "-" VERSION " " DATE "\n");
+}
+
+static int print_usage(FILE *const fd) {
+ return print_msg(fd, _(MSG_USAGE));
+}
+
+
+static char *remove_suffix(char *const s, const char *const suffix) {
+ if (!s || !suffix) {
+ return NULL;
+ }
+
+ char *const name = strdup(s);
+ if (!name) {
+ logerrs("strdup(\"", s, "\")", strerror(errno), __LINE__);
+ 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(void) {
+ 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 size1 = strnlen(s1, SIZE_MAX);
+ const size_t size2 = strnlen(s2, SIZE_MAX - sizeof('\0'))
+ + sizeof('\0');
+ if (SIZE_MAX - size1 < size2) {
+ errno = EOVERFLOW;
+ logerr("strjoin()", strerror(errno), __LINE__);
+ return NULL;
+ }
+ const size_t size = size1 + size2;
+ char *const s = malloc(size);
+ if (!s) {
+ logerrl("malloc(", size, ")", strerror(errno), __LINE__);
+ return NULL;
+ }
+
+ strcpy(s, s1);
+ strcat(s, s2);
+ return s;
+}
+
+#ifdef TEST
+static void test_strjoin(void) {
+ 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) {
+ const struct tm *const time_utc = gmtime(&time_sec);
+ if (!time_utc) {
+ logerrl("gmtime(", time_sec, ")", strerror(errno), __LINE__);
+ return NULL;
+ }
+
+ /* Expected size, plus breathing room for date format variations */
+ const size_t size = (strlen("XXX-XX-XX XX:XX") + sizeof('\0')) * 2;
+ char *const formatted = malloc(size);
+ if (!formatted) {
+ logerrl("malloc(", size, ")", strerror(errno), __LINE__);
+ return NULL;
+ }
+
+ strftime(formatted, size, "%Y-%m-%d %H:%M", time_utc);
+ return formatted;
+}
+
+#ifdef TEST
+static void test_formatted_date(void) {
+ 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 size_t max(const size_t size1, const size_t size2) {
+ return size1 > size2 ? size1 : size2;
+}
+
+#ifdef TEST
+static void test_max(void) {
+ test_start("max()");
+ {
+ testing("equal values");
+ assert(max(1, 1) == 1);
+ assert(max(3, 3) == 3);
+ assert(max(0, 0) == 0);
+ assert(max(999, 999) == 999);
+ assert(max(SIZE_MAX, SIZE_MAX) == SIZE_MAX);
+ test_ok();
+ }
+ {
+ testing("first is bigger");
+ assert(max(SIZE_MAX, SIZE_MAX - 1) == SIZE_MAX);
+ assert(max(SIZE_MAX - 1, SIZE_MAX - 2) == SIZE_MAX - 1);
+ assert(max(1, 0) == 1);
+ assert(max(999, 3) == 999);
+ test_ok();
+ }
+ {
+ testing("second is bigger");
+ assert(max(123, 321) == 321);
+ assert(max(1, 999) == 999);
+ 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 BIGGEST = max(AMP_SIZE, max(LT_SIZE, max(GT_SIZE,
+ max(DQUOT_SIZE, SQUOT_SIZE))));
+
+ const size_t input_size = strnlen(s, SIZE_MAX);
+ size_t escaped_size = 0;
+ for (size_t i = 0; i < input_size; i++) {
+ if (SIZE_MAX - escaped_size < BIGGEST) {
+ errno = EOVERFLOW;
+ logerr("escape_html()", strerror(errno), __LINE__);
+ return NULL;
+ }
+ 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) {
+ logerrl("malloc(", size, ")", strerror(errno), __LINE__);
+ 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(void) {
+ 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 bool should_trim(const char c) {
+ return c == '\n' || c == '\t' || c == '\r' || c == ' ';
+}
+
+#ifdef TEST
+static void test_should_trim(void) {
+ test_start("test_should_trim");
+ {
+ testing("the \\0 null character");
+ assert(should_trim('\0') == false);
+ test_ok();
+ }
+ {
+ testing("the \\b character");
+ assert(should_trim('\b') == false);
+ test_ok();
+ }
+ {
+ testing("the space character");
+ assert(should_trim(' ') == true);
+ test_ok();
+ }
+}
+#endif
+
+static void strtrim(char *const s) {
+ if (s == NULL) {
+ return;
+ }
+
+ if (s[0] == '\0') {
+ return;
+ }
+
+ size_t len = strnlen(s, SIZE_MAX);
+ while (len != 0 && should_trim(s[len - 1])) {
+ s[len - 1] = '\0';
+ len--;
+ }
+
+ size_t offset_count = 0;
+ while (should_trim(s[offset_count])) {
+ offset_count++;
+ };
+
+ if (offset_count == 0) {
+ return;
+ }
+
+ for (size_t i = 0; s[i]; i++) {
+ s[i] = s[i + offset_count];
+ }
+}
+
+#ifdef TEST
+static void test_strtrim(void) {
+ test_start("test_strtrim");
+ {
+ testing("empty string");
+ char *const s = "";
+ strtrim(s);
+ assert(strcmp(s, "") == 0);
+ test_ok();
+ }
+ {
+ testing("NULL string");
+ char *const s = NULL;
+ strtrim(s);
+ assert(s == NULL);
+ test_ok();
+ }
+ {
+ testing("string without a newline");
+ char *const s = "a string without a newline";
+ strtrim(s);
+ assert(strcmp(s, "a string without a newline") == 0);
+ test_ok();
+ }
+ {
+ testing("string with a newline");
+ char *const s = strdup("a string with an ending newline\n");
+ assert(s);
+ strtrim(s);
+ assert(strcmp(s, "a string with an ending newline") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("a single newline");
+ char *const s = strdup("\n");
+ assert(s);
+ strtrim(s);
+ assert(strcmp(s, "") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("multiple newlines at the end");
+ char *const s = strdup("a string\n\n\n\n\n");
+ assert(s);
+ strtrim(s);
+ assert(strcmp(s, "a string") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("a single space at the end");
+ char *const s = strdup("a string ");
+ assert(s);
+ strtrim(s);
+ assert(strcmp(s, "a string") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("a single space at the beginning");
+ char *const s = strdup(" a string");
+ assert(s);
+ strtrim(s);
+ assert(strcmp(s, "a string") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("multiple newlines");
+ char *const s = strdup("\n\na string\n\n");
+ assert(s);
+ strtrim(s);
+ assert(strcmp(s, "a string") == 0);
+ free(s);
+ test_ok();
+ }
+ {
+ testing("newline on the middle of the string");
+ char *const s = "a\nstring";
+ strtrim(s);
+ assert(strcmp(s, "a\nstring") == 0);
+ test_ok();
+ }
+ {
+ testing("multiple trimmable characters");
+ char *const s = strdup(" \t \n \r a string \n \t \r ");
+ assert(s);
+ strtrim(s);
+ assert(strcmp(s, "a string") == 0);
+ free(s);
+ test_ok();
+ }
+}
+#endif
+
+static int index_header_write(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"
+ " <meta name=\"description\" content=\"%s\" />\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"
+ " <div class=\"header-horizontal-grouping\">\n"
+ " <img alt=\"%s\" class=\"logo\" src=\"logo.svg\" />\n"
+ " <h1 class=\"header-description\">\n"
+ " %s\n"
+ " </h1>\n"
+ " </div>\n"
+ " <hr />\n"
+ " </header>\n"
+ " <main>\n"
+ " <table>\n"
+ " <thead>\n"
+ " <tr>\n"
+ " <th>\n"
+ " %s\n"
+ " </th>\n"
+ " <th>\n"
+ " %s\n"
+ " </th>\n"
+ " <th>\n"
+ " %s\n"
+ " </th>\n"
+ " </tr>\n"
+ " </thead>\n"
+ " <tbody>\n",
+ _(MSG_LANG),
+ _(MSG_INDEX_DESCRIPTION),
+ idx_title,
+ _(MSG_LOGO_ALT_INDEX),
+ idx_title,
+ _(MSG_NAME),
+ _(MSG_DESCRIPTION),
+ _(MSG_LAST_COMMIT)
+ );
+ if (e < 0) {
+ logerr("fprintf()", strerror(errno), __LINE__);
+ return -1;
+ }
+ return 0;
+}
+
+static char *footer_signature_string(void) {
+ const size_t template_size = strnlen(
+ _(MSG_FOOTER_TEMPLATE),
+ SIZE_MAX
+ ) - strlen("%s");
+ const size_t link_size = strnlen(
+ PROJECT_HOMEPAGE_LINK,
+ SIZE_MAX - sizeof('\0')
+ ) + sizeof('\0');
+ if (SIZE_MAX - template_size < link_size) {
+ errno = EOVERFLOW;
+ logerr("footer_signature_string()", strerror(errno), __LINE__);
+ return NULL;
+ }
+ const size_t signature_size = template_size + link_size;
+
+ char *const signature_text = malloc(signature_size);
+ if (!signature_text) {
+ logerrl("malloc(", signature_size, ")", strerror(errno),
+ __LINE__);
+ return NULL;
+ }
+
+ snprintf(
+ signature_text,
+ signature_size,
+ _(MSG_FOOTER_TEMPLATE),
+ PROJECT_HOMEPAGE_LINK
+ );
+ return signature_text;
+}
+
+static int index_footer_write(FILE *const fd) {
+ char *const signature_text = footer_signature_string();
+ if (!signature_text) {
+ return -1;
+ }
+
+ 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",
+ signature_text
+ );
+ free(signature_text);
+ if (e < 0) {
+ logerr("fprintf()", strerror(errno), __LINE__);
+ return -1;
+ }
+
+ return 0;
+}
+
+static char *last_commit_date(struct git_repository *const 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) {
+ logerr("strdup(\"\")", strerror(errno), __LINE__);
+ }
+ 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(void) {
+ test_start("test_last_commit_date");
+ {
+ testing("embedded Git repository"
+ " tests/resources/repositories/repo-1");
+ struct git_repository *repo;
+ const int e = git_repository_open_ext(
+ &repo,
+ "tests/resources/repositories/repo-1",
+ GIT_REPOSITORY_OPEN_NO_SEARCH,
+ NULL
+ );
+ assert(e == 0);
+ char *const date = last_commit_date(repo);
+ assert(date);
+ assert(strcmp(date, "2021-07-31 19:29") == 0);
+ free(date);
+ git_repository_free(repo);
+ test_ok();
+ }
+ {
+ testing("embedded Git repository"
+ " tests/resources/repositories/repo-2");
+ struct git_repository *repo;
+ const int e = git_repository_open_ext(
+ &repo,
+ "tests/resources/repositories/repo-2",
+ GIT_REPOSITORY_OPEN_NO_SEARCH,
+ NULL
+ );
+ assert(e == 0);
+ char *const date = last_commit_date(repo);
+ assert(date);
+ assert(strcmp(date, "2021-07-31 19:27") == 0);
+ free(date);
+ git_repository_free(repo);
+ test_ok();
+ }
+}
+#endif
+
+static int logo_write(FILE *const fd) {
+ if (fprintf(fd, "%s", LOGO_STR) < 0) {
+ logerr("fprintf()", strerror(errno), __LINE__);
+ return -1;
+ }
+ return 0;
+}
+
+static int style_write(FILE *const fd) {
+ if (fprintf(fd, "%s", STYLE_STR) < 0) {
+ logerr("fprintf()", strerror(errno), __LINE__);
+ return -1;
+ }
+ return 0;
+}
+
+static int index_row_write(FILE *const fd, char *const repopath) {
+ int ret = 0;
+ 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;
+ }
+
+ char description[DESCRIPTION_MAXLENGTH] = "";
+ FILE *const f = fopen(description_path, "r");
+ if (f) {
+ if (!fgets(description, sizeof(description), f)) {
+ logerrs("fgets(\"", description_path, "\")",
+ strerror(errno), __LINE__);
+ } else {
+ strtrim(description);
+ }
+ if (fclose(f)) {
+ logerrs("fclose(\"", description_path, "\")",
+ strerror(errno), __LINE__);
+ 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) {
+ logerr("fprintf()", strerror(errno), __LINE__);
+ 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 index_write(
+ const char *const outdir,
+ const char *const title,
+ 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"))) {
+ logerrs("fopen(\"", index_path, "\")", strerror(errno),
+ __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+ if (!(logo_fd = fopen(logo_path, "w"))) {
+ logerrs("fopen(\"", logo_path, "\")", strerror(errno),
+ __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+ if (!(style_fd = fopen(style_path, "w"))) {
+ logerrs("fopen(\"", style_path, "\")", strerror(errno),
+ __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+ if (index_header_write(index_fd, title)) {
+ ret = -1;
+ goto cleanup;
+ }
+ for (int i = 0; i < repoc; i++) {
+ const int e = index_row_write(index_fd, repov[i]);
+ if (e == -1) {
+ ret = -1;
+ goto cleanup;
+ }
+ if (e) {
+ ret = 1;
+ }
+ }
+
+ if (
+ index_footer_write(index_fd)
+ || logo_write(logo_fd)
+ || style_write(style_fd)
+ ) {
+ ret = -1;
+ goto cleanup;
+ }
+
+cleanup:
+ if (style_fd && fclose(style_fd)) {
+ logerrs("fclose(\"", style_path, "\")", strerror(errno),
+ __LINE__);
+ ret = -1;
+ }
+ if (logo_fd && fclose(logo_fd)) {
+ logerrs("fclose(\"", logo_path, "\")", strerror(errno),
+ __LINE__);
+ ret = -1;
+ }
+ if (index_fd && fclose(index_fd)) {
+ logerrs("fclose(\"", index_path, "\")", strerror(errno),
+ __LINE__);
+ ret = -1;
+ }
+ free(style_path);
+ free(logo_path);
+ free(index_path);
+ return ret;
+}
+
+static int repo_refs_branches_each(
+ FILE *const refs_fd,
+ struct git_repository *const repo,
+ struct git_reference *const ref
+) {
+ int ret = 0;
+ struct git_commit *commit = NULL;
+ char *encoded_name = NULL;
+ char *encoded_summary = NULL;
+ char *encoded_author = NULL;
+ char *date = NULL;
+ int e;
+
+ const char *const name = git_reference_shorthand(ref);
+ assert(name);
+ const struct git_oid *const oid = git_reference_target(ref);
+ assert(oid);
+
+ if (git_commit_lookup(&commit, repo, oid)) {
+ const git_error *const error = git_error_last();
+ assert(error);
+ logerrs("git_commit_lookup(", name, ")", error->message,
+ __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+ const char *const sha = git_oid_tostr_s(oid);
+ assert(sha);
+ const char *const summary = git_commit_summary(commit);
+ assert(summary);
+ const struct git_signature *const author = git_commit_author(commit);
+ assert(author);
+
+ if (
+ !(encoded_name = escape_html(name))
+ || !(encoded_summary = escape_html(summary))
+ || !(encoded_author = escape_html(author->name))
+ || !(date = formatted_date(author->when.time))
+ ) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ e = fprintf(
+ refs_fd,
+ ""
+ " <tr>\n"
+ " <td>\n"
+ " <a href=\"log/%s.html\">\n"
+ " %s\n"
+ " </a>\n"
+ " </td>\n"
+ " <td>\n"
+ " <a href=\"commit/%s.html\">\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,
+ sha,
+ encoded_summary,
+ encoded_author,
+ date
+ );
+ if (e < 0) {
+ logerr("fprintf()", strerror(errno), __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+cleanup:
+ free(date);
+ free(encoded_author);
+ free(encoded_summary);
+ free(encoded_name);
+ git_commit_free(commit);
+ return ret;
+}
+
+static int repo_refs_tags_each(
+ FILE *const refs_fd,
+ struct git_repository *const repo,
+ struct git_reference *const ref,
+ const char *const project_name
+) {
+ int ret = 0;
+ struct git_commit *commit = NULL;
+ char *encoded_name = NULL;
+ char *encoded_author = NULL;
+ char *date = NULL;
+ int e;
+
+ if (!git_reference_is_tag(ref)) {
+ goto cleanup;
+ }
+
+ const char *const name = git_reference_shorthand(ref);
+ assert(name);
+ const struct git_oid *const oid = git_reference_target(ref);
+ assert(oid);
+
+ if (git_commit_lookup(&commit, repo, oid)) {
+ const git_error *const error = git_error_last();
+ assert(error);
+ logerrs("git_commit_lookup(", name, ")", error->message, __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+ const char *const summary = git_commit_summary(commit);
+ assert(summary);
+ const struct git_signature *const author = git_commit_author(commit);
+ assert(author);
+
+ if (
+ !(encoded_name = escape_html(name))
+ || !(encoded_author = escape_html(author->name))
+ || !(date = formatted_date(author->when.time))
+ ) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ e = fprintf(
+ refs_fd,
+ ""
+ " <tr>\n"
+ " <td>\n"
+ " <a href=\"tag/%s.html\">\n"
+ " %s\n"
+ " </a>\n"
+ " (<a href=\"tarballs/%s-%s.tar.gz\">tarball</a>, <a href=\"tarballs/%s-%s.tar.gz.asc\">sig</a>)\n"
+ " </td>\n"
+ " <td>\n"
+ " %s\n"
+ " </td>\n"
+ " <td>\n"
+ " %s\n"
+ " </td>\n"
+ " </tr>\n"
+ "",
+ encoded_name,
+ encoded_name,
+ project_name,
+ encoded_name,
+ project_name,
+ encoded_name,
+ encoded_author,
+ date
+ );
+ if (e < 0) {
+ logerr("fprintf()", strerror(errno), __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+cleanup:
+ free(date);
+ free(encoded_author);
+ free(encoded_name);
+ git_commit_free(commit);
+ return ret;
+}
+
+static int repo_refs_write(
+ const char *const outdir,
+ struct git_repository *const repo,
+ const char *const project_name,
+ const char *const description,
+ const char *const clone_url
+) {
+ int ret = 0;
+ char *signature_text = NULL;
+ char *refs_path = NULL;
+ FILE *refs_fd = NULL;
+ struct git_branch_iterator *branch_iter = NULL;
+ struct git_reference_iterator *ref_iter = NULL;
+ int e;
+
+
+ if (
+ !(signature_text = footer_signature_string())
+ || !(refs_path = strjoin(outdir, "/refs.html"))
+ ) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ if (!(refs_fd = fopen(refs_path, "w"))) {
+ logerrs("fopen(\"", refs_path, "\")", strerror(errno),
+ __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+ if (
+ git_branch_iterator_new(&branch_iter, repo, GIT_BRANCH_LOCAL)
+ || git_reference_iterator_new(&ref_iter, repo)
+ ) {
+ const git_error *const error = git_error_last();
+ assert(error);
+ const char *const fn = branch_iter == NULL
+ ? "git_branch_iterator_new()"
+ : "git_reference_iterator_new()";
+ logerr(fn, error->message, __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+
+ e = fprintf(
+ refs_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"
+ " <meta name=\"description\" content=\"%s\" />\n"
+ " <link rel=\"icon\" type=\"image/svg+xml\" href=\"logo.svg\" />\n"
+ " <link rel=\"stylesheet\" type=\"text/css\" href=\"style.css\" />\n"
+ " <link rel=\"alternate\" type=\"application/atom+xml\" href=\"commits.xml\" title=\"%s - %s\" hreflang=\"%s\" />\n"
+ " <link rel=\"alternate\" type=\"application/atom+xml\" href=\"tags.xml\" title=\"%s - %s\" hreflang=\"%s\" />\n"
+ " <title>%s</title>\n"
+ " </head>\n"
+ " <body>\n"
+ " <header>\n"
+ " <div class=\"header-horizontal-grouping\">\n"
+ " <a href=\"../\">\n"
+ " <img alt=\"%s\" class=\"logo\" src=\"logo.svg\" />\n"
+ " </a>\n"
+ " <div class=\"header-description\">\n"
+ " <h1>\n"
+ " %s\n"
+ " </h1>\n"
+ " <h2>\n"
+ " %s\n"
+ " </h2>\n"
+ " <code>\n"
+ " git clone %s\n"
+ " </code>\n"
+ " </div>\n"
+ " </div>\n"
+ " <nav>\n"
+ " <ul>\n"
+ " <li>\n"
+ " <a href=\"files.html\">\n"
+ " %s\n"
+ " </a>\n"
+ " </li>\n"
+ " <li>\n"
+ " <a href=\"log.html\">\n"
+ " %s\n"
+ " </a>\n"
+ " </li>\n"
+ " <li class=\"selected-nav-item\">\n"
+ " <a href=\"refs.html\">\n"
+ " %s\n"
+ " </a>\n"
+ " </li>\n"
+ " </ul>\n"
+ " </nav>\n"
+ " <hr />\n"
+ " </header>\n"
+ " <main>\n"
+ " <table>\n"
+ " <thead>\n"
+ " <tr>\n"
+ " <th>\n"
+ " %s\n"
+ " </th>\n"
+ " <th>\n"
+ " %s\n"
+ " </th>\n"
+ " <th>\n"
+ " %s\n"
+ " </th>\n"
+ " <th>\n"
+ " %s\n"
+ " </th>\n"
+ " </tr>\n"
+ " </thead>\n"
+ " <tbody>\n",
+ _(MSG_LANG),
+ description,
+ project_name,
+ _(MSG_COMMIT_FEED),
+ _(MSG_LANG),
+ project_name,
+ _(MSG_TAGS_FEED),
+ _(MSG_LANG),
+ project_name,
+ _(MSG_LOGO_ALT_REPOSITORY),
+ project_name,
+ description,
+ clone_url,
+ _(MSG_NAV_FILES),
+ _(MSG_NAV_LOG),
+ _(MSG_NAV_REFS),
+ _(MSG_THEAD_BRANCH),
+ _(MSG_THEAD_COMMITMSG),
+ _(MSG_THEAD_AUTHOR),
+ _(MSG_THEAD_DATE)
+ );
+ if (e < 0) {
+ logerr("fprintf()", strerror(errno), __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+ struct git_reference *ref;
+ git_branch_t _btype;
+ while (!(e = git_branch_next(&ref, &_btype, branch_iter))) {
+ e = repo_refs_branches_each(refs_fd, repo, ref);
+ git_reference_free(ref);
+ if (e) {
+ goto cleanup;
+ }
+ }
+ if (e != GIT_ITEROVER) {
+ const git_error *const error = git_error_last();
+ assert(error);
+ logerr("git_branch_next()", error->message, __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+ e = fprintf(
+ refs_fd,
+ ""
+ " </tbody>\n"
+ " </table>\n"
+ " <table>\n"
+ " <thead>\n"
+ " <tr>\n"
+ " <th>\n"
+ " %s\n"
+ " </th>\n"
+ " <th>\n"
+ " %s\n"
+ " </th>\n"
+ " <th>\n"
+ " %s\n"
+ " </th>\n"
+ " </tr>\n"
+ " </thead>\n"
+ " <tbody>\n",
+ _(MSG_THEAD_TAG),
+ _(MSG_THEAD_AUTHOR),
+ _(MSG_THEAD_DATE)
+ );
+ if (e < 0) {
+ logerr("fprintf()", strerror(errno), __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+ while (!(e = git_reference_next(&ref, ref_iter))) {
+ e = repo_refs_tags_each(refs_fd, repo, ref, project_name);
+ git_reference_free(ref);
+ if (e) {
+ goto cleanup;
+ }
+ }
+ if (e != GIT_ITEROVER) {
+ const git_error *const error = git_error_last();
+ assert(error);
+ logerr("git_reference_next()", error->message, __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+ e = fprintf(
+ refs_fd,
+ ""
+ " </tbody>\n"
+ " </table>\n"
+ " </main>\n"
+ " <footer>\n"
+ " <hr />\n"
+ " <p>\n"
+ " %s\n"
+ " </p>\n"
+ " </footer>\n"
+ " </body>\n"
+ "</html>\n",
+ signature_text
+ );
+ if (e < 0) {
+ logerr("fprintf()", strerror(errno), __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+cleanup:
+ git_branch_iterator_free(branch_iter);
+ git_reference_iterator_free(ref_iter);
+ if (refs_fd && fclose(refs_fd)) {
+ logerrs("fclose(\"", refs_path, "\")", strerror(errno),
+ __LINE__);
+ ret = -1;
+ }
+ free(refs_path);
+ free(signature_text);
+ return ret;
+}
+
+static int repo_tarballs_write(
+ const char *const outdir,
+ struct git_repository *const repo,
+ const char *const project_name
+) {
+ int ret = 0;
+ int e;
+ char *tarballs_dir = NULL;
+ struct git_reference_iterator *ref_iter = NULL;
+
+ if (!(tarballs_dir = strjoin(outdir, "/tarballs"))) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ errno = 0;
+ if (mkdir(tarballs_dir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)
+ && errno != EEXIST) {
+ logerrs("mkdir(\"", tarballs_dir, "\")", strerror(errno),
+ __LINE__);
+ ret = EXIT_ERROR;
+ goto cleanup;
+ }
+
+ if (git_reference_iterator_new(&ref_iter, repo)) {
+ const git_error *const error = git_error_last();
+ assert(error);
+ logerr("git_reference_iterator_new()", error->message,
+ __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+ struct git_reference *ref;
+ while (!(e = git_reference_next(&ref, ref_iter))) {
+ const char *const name = git_reference_shorthand(ref);
+ assert(name);
+ const bool is_tag = git_reference_is_tag(ref);
+ const bool is_branch = git_reference_is_branch(ref);
+ const bool is_note = git_reference_is_note(ref);
+
+ if (!is_tag && !is_branch && !is_note) {
+ goto loop_cleanup;
+ }
+ if (is_note && strcmp(name, "notes/signatures/tar.xz") != 0) {
+ goto loop_cleanup;
+ }
+
+ // printf("reference name: %s\n", git_reference_shorthand(ref));
+
+ // https://github.com/ionescu007/minlzma
+ // https://git.tukaani.org/?p=xz.git;a=tree;f=src;h=665b39e6439f1bb5afd9181ec0890c2ed26d047e;hb=HEAD
+ // git archive --format tar.xz --prefix "$PROJECT-$tag/" "$tag"
+ loop_cleanup:
+ git_reference_free(ref);
+ }
+ if (e != GIT_ITEROVER) {
+ const git_error *const error = git_error_last();
+ assert(error);
+ logerr("git_reference_next()", error->message, __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+cleanup:
+ git_reference_iterator_free(ref_iter);
+ free(tarballs_dir);
+ return ret;
+}
+
+static int repo_write(
+ const char *const outdir,
+ char *const reporelpath,
+ const char *const clone_url
+) {
+ int ret = 0;
+ int e;
+
+ char *repopath = NULL;
+ char *description_path = NULL;
+ char *logo_path = NULL;
+ char *style_path = NULL;
+ char *name = NULL;
+ char *encoded_name = NULL;
+ char *encoded_description = NULL;
+ FILE *logo_fd = NULL;
+ FILE *style_fd = NULL;
+ struct git_repository *repo = NULL;
+
+ if (!(repopath = realpath(reporelpath, NULL))) {
+ logerrs("realpath(\"", reporelpath, "\")", strerror(errno),
+ __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+ if (
+ !(description_path = strjoin(repopath, "/description"))
+ || !(logo_path = strjoin(outdir, "/logo.svg"))
+ || !(style_path = strjoin(outdir, "/style.css"))
+ || !(name = remove_suffix(basename(repopath), GIT_SUFFIX))
+ ) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ if (!(logo_fd = fopen(logo_path, "w"))) {
+ logerrs("fopen(\"", logo_path, "\")", strerror(errno),
+ __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+ if (!(style_fd = fopen(style_path, "w"))) {
+ logerrs("fopen(\"", style_path, "\")", strerror(errno),
+ __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+ if (logo_write(logo_fd) || style_write(style_fd)) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ e = git_repository_open_ext(
+ &repo,
+ repopath,
+ GIT_REPOSITORY_OPEN_NO_SEARCH,
+ NULL
+ );
+ if (e) {
+ const git_error *const error = git_error_last();
+ assert(error);
+ logerr("git_repository_open_ext()", error->message, __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+
+ char description[DESCRIPTION_MAXLENGTH] = "";
+ FILE *f = fopen(description_path, "r");
+ if (f) {
+ if (!fgets(description, sizeof(description), f)) {
+ logerrs("fget(\"", description_path, "\")",
+ strerror(errno), __LINE__);
+ } else {
+ strtrim(description);
+ }
+ if (fclose(f)) {
+ logerrs("fclose(\"", description_path, "\")",
+ strerror(errno), __LINE__);
+ ret = -1;
+ goto cleanup;
+ }
+ }
+
+ if (
+ !(encoded_name = escape_html(name))
+ || !(encoded_description = escape_html(description))
+ ) {
+ ret = -1;
+ goto cleanup;
+ }
+
+ if (
+ repo_refs_write(outdir, repo, encoded_name,
+ encoded_description, clone_url)
+ || repo_tarballs_write(outdir, repo, encoded_name)
+ ) {
+ ret = -1;
+ goto cleanup;
+ }
+
+cleanup:
+ free(encoded_description);
+ free(encoded_name);
+ if (repo) {
+ git_repository_free(repo);
+ }
+ if (style_fd && fclose(style_fd)) {
+ logerrs("fclose(\"", style_path, "\")", strerror(errno),
+ __LINE__);
+ ret = -1;
+ }
+ if (logo_fd && fclose(logo_fd)) {
+ logerrs("fclose(\"", style_path, "\")", strerror(errno),
+ __LINE__);
+ ret = -1;
+ }
+ free(name);
+ free(style_path);
+ free(logo_path);
+ free(description_path);
+ free(repopath);
+ return ret;
+}
+
+#ifdef TEST
+void unit_tests_gistatic(void) {
+ dump_translatable_strings();
+
+ git_libgit2_init();
+ test_underscore();
+ test_remove_suffix();
+ test_strjoin();
+ test_formatted_date();
+ test_max();
+ test_escape_html();
+ test_should_trim();
+ test_strtrim();
+ test_last_commit_date();
+ git_libgit2_shutdown();
+}
+#endif
+
+int gistatic_main(int argc, char *argv[]) {
+ int ret = EXIT_SUCCESS;
+ bool cleanup_libgit = false;
+ catalog_descriptor = catopen(CATALOG_NAME, NL_CAT_LOCALE);
+
+ if (argc < 2) {
+ if (print_usage(stderr)) {
+ ret = EXIT_ERROR;
+ goto cleanup;
+ }
+ ret = EXIT_USAGE;
+ goto cleanup;
+ }
+
+ int flag;
+ bool index = false;
+ const char *idx_title = _(MSG_DEFAULT_TITLE);
+ const char *outdir = ".";
+ const char *clone_url = NULL;
+ while ((flag = getopt(argc, argv, "o:t:u:ivhV")) != -1) {
+ switch (flag) {
+ case 'i':
+ index = true;
+ break;
+ case 't':
+ idx_title = optarg;
+ break;
+ case 'o':
+ outdir = optarg;
+ break;
+ case 'v':
+ verbose = true;
+ break;
+ case 'u':
+ clone_url = optarg;
+ break;
+ case 'h':
+ if (print_usage(stdout) || print_help(stdout)) {
+ ret = EXIT_ERROR;
+ }
+ goto cleanup;
+ case 'V':
+ if (print_version(stdout)) {
+ ret = EXIT_ERROR;
+ }
+ goto cleanup;
+ default:
+ if (print_usage(stderr)) {
+ ret = EXIT_ERROR;
+ }
+ goto cleanup;
+ }
+ }
+
+ if (!index && !clone_url) {
+ if (fprintf(stderr, "%s\n", _(MSG_MISSING_URL)) < 0
+ || print_usage(stderr)) {
+ ret = EXIT_ERROR;
+ goto cleanup;
+ }
+ ret = EXIT_USAGE;
+ goto cleanup;
+ }
+ if (index && clone_url) {
+ if (fprintf(stderr, "%s\n", _(MSG_INCOMPATIBLE_OPTIONS)) < 0
+ || print_usage(stderr)) {
+ ret = EXIT_ERROR;
+ goto cleanup;
+ }
+ ret = EXIT_USAGE;
+ goto cleanup;
+ }
+ if (optind == argc) {
+ if (fprintf(stderr, "%s\n", _(MSG_MISSING_ARGS)) < 0
+ || print_usage(stderr)) {
+ ret = EXIT_ERROR;
+ goto cleanup;
+ }
+ ret = EXIT_USAGE;
+ goto cleanup;
+ }
+
+
+ errno = 0;
+ if (mkdir(outdir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH)
+ && errno != EEXIST) {
+ logerrs("mkdir(\"", outdir, "\")", strerror(errno), __LINE__);
+ ret = EXIT_ERROR;
+ goto cleanup;
+ }
+
+ git_libgit2_init();
+ cleanup_libgit = true;
+
+ if (index) {
+ if (index_write(outdir, idx_title,
+ argc - optind, argv + optind)) {
+ ret = EXIT_ERROR;
+ goto cleanup;
+ }
+ } else {
+ if (repo_write(outdir, argv[optind], clone_url)) {
+ ret = EXIT_ERROR;
+ goto cleanup;
+ }
+ }
+
+cleanup:
+ if (cleanup_libgit) {
+ git_libgit2_shutdown();
+ }
+ if (catalog_descriptor && catalog_descriptor != (nl_catd)-1) {
+ if (catclose(catalog_descriptor)) {
+ logerr("catclose()", strerror(errno), __LINE__);
+ ret = EXIT_ERROR;
+ }
+ }
+ return ret;
+}