From 736c234622b3abab582215954db7bd52e542294b Mon Sep 17 00:00:00 2001 From: EuAndreh Date: Sun, 22 Aug 2021 11:46:56 -0300 Subject: Rename src/gistatic.* src/lib.* --- src/lib.c | 2181 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 2181 insertions(+) create mode 100644 src/lib.c (limited to 'src/lib.c') diff --git a/src/lib.c b/src/lib.c new file mode 100644 index 0000000..8f067b7 --- /dev/null +++ b/src/lib.c @@ -0,0 +1,2181 @@ +#include "config.h" +#include "tar.h" +#include "gistatic.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + + +#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 = "" + "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\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 = + "" PROGNAME "."; + + +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("link"); + 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, + "" + "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " %s\n" + " \n" + " \n" + "
\n" + "
\n" + " \"%s\"\n" + "

\n" + " %s\n" + "

\n" + "
\n" + "
\n" + "
\n" + "
\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \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, + "" + " \n" + "
\n" + " %s\n" + " \n" + " %s\n" + " \n" + " %s\n" + "
\n" + "
\n" + "
\n" + "
\n" + "

\n" + " %s\n" + "

\n" + "
\n" + " \n" + "\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, + "" + " \n" + " \n" + " \n" + " %s\n" + " \n" + " \n" + " \n" + " %s\n" + " \n" + " \n" + " %s\n" + " \n" + " \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, + "" + " \n" + " \n" + " \n" + " %s\n" + " \n" + " \n" + " \n" + " \n" + " %s\n" + " \n" + " \n" + " \n" + " %s\n" + " \n" + " \n" + " %s\n" + " \n" + " \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, + "" + " \n" + " \n" + " \n" + " %s\n" + " \n" + " (tarball, sig)\n" + " \n" + " \n" + " %s\n" + " \n" + " \n" + " %s\n" + " \n" + " \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, + "" + "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " %s\n" + " \n" + " \n" + "
\n" + "
\n" + " \n" + " \"%s\"\n" + " \n" + "
\n" + "

\n" + " %s\n" + "

\n" + "

\n" + " %s\n" + "

\n" + " \n" + " git clone %s\n" + " \n" + "
\n" + "
\n" + " \n" + "
\n" + "
\n" + "
\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \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, + "" + " \n" + "
\n" + " %s\n" + " \n" + " %s\n" + " \n" + " %s\n" + " \n" + " %s\n" + "
\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \n" + " \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, + "" + " \n" + "
\n" + " %s\n" + " \n" + " %s\n" + " \n" + " %s\n" + "
\n" + "
\n" + " \n" + " \n" + "\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; +} -- cgit v1.2.3