diff options
author | EuAndreh <eu@euandre.org> | 2021-08-22 11:46:56 -0300 |
---|---|---|
committer | EuAndreh <eu@euandre.org> | 2021-08-22 11:46:56 -0300 |
commit | 736c234622b3abab582215954db7bd52e542294b (patch) | |
tree | 37675e889dbade08acba135b5f03cf6354a3c336 /src/gistatic.c | |
parent | TODOs.md: Add #task-c71e7a3a-076d-3553-e245-0ad2b7a64231 (diff) | |
download | gistatic-736c234622b3abab582215954db7bd52e542294b.tar.gz gistatic-736c234622b3abab582215954db7bd52e542294b.tar.xz |
Rename src/gistatic.* src/lib.*
Diffstat (limited to 'src/gistatic.c')
-rw-r--r-- | src/gistatic.c | 2181 |
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 = "&"; - static const char *const LT = "<"; - static const char *const GT = ">"; - static const char *const DQUOT = """; - static const char *const SQUOT = "'"; - - 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, "&") == 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, "&& >> text <<") == 0); - free(s); - test_ok(); - } - { - testing("all escapable characters"); - char *const s = escape_html("&<>\"'"); - assert(s); - assert(strcmp(s, "&<>"'") == 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->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" - " "quotes" && 'quotes'"; - 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 = "<a" - " href="#">link</a>"; - 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; -} |