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