aboutsummaryrefslogtreecommitdiff
path: root/src/gistatic.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gistatic.c')
-rw-r--r--src/gistatic.c2181
1 files changed, 0 insertions, 2181 deletions
diff --git a/src/gistatic.c b/src/gistatic.c
deleted file mode 100644
index 8f067b7..0000000
--- a/src/gistatic.c
+++ /dev/null
@@ -1,2181 +0,0 @@
-#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;
-}