#include #include #include #include #include #include #include #include #include #include #include #include #ifdef TEST #define COLOUR_RESET "\033[0m" #define COLOUR_GREEN "\033[0;32m" #define COLOUR_YELLOW "\033[0;33m" static void test_start(const char *const name) { assert(fprintf(stderr, "%s():\n", name) > 0); } static void testing(const char *const message) { assert( fprintf( stderr, COLOUR_YELLOW "testing" COLOUR_RESET ": %s...", message ) > 0 ); } static void test_ok() { assert(fprintf(stderr, " " COLOUR_GREEN "OK" COLOUR_RESET ".\n") > 0); } #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 2 #define MSG_FOOTER_TEMPLATE 3 #define MSG_LANG 4 #define MSG_NAME 5 #define MSG_DESCRIPTION 6 #define MSG_LAST_COMMIT 7 #define MSG_USAGE 8 #define MSG_HELP 9 #define MSG_COMMIT_FEED 10 #define MSG_TAGS_FEED 11 #define MSG_NAV_FILES 12 #define MSG_NAV_LOG 13 #define MSG_NAV_REFS 14 #define MSG_THEAD_BRANCH 15 #define MSG_THEAD_COMMITMSG 16 #define MSG_THEAD_AUTHOR 17 #define MSG_THEAD_DATE 18 #define MSG_THEAD_TAG 19 #define MSG_THEAD_DOWNLOAD 20 #define MSG_MISSING_URL 21 #define MSG_MISSING_ARGS 22 #define MSG_INCOMPATIBLE_OPTIONS 23 #define MSG_ERR_NONDIRECT_REF 24 static const char *const MSGS[] = { "", [MSG_DEFAULT_TITLE]="Repositories", [MSG_LOGO_ALT]="Logo image of the repository list", [MSG_FOOTER_TEMPLATE]="Generated with %s", [MSG_LANG]="en", [MSG_NAME]="Name", [MSG_DESCRIPTION]="Description", [MSG_LAST_COMMIT]="Last commit", [MSG_USAGE]="Usage: -h -o -i\n-z -x...\n", [MSG_HELP]="help: ...\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_THEAD_DOWNLOAD]="Download", [MSG_MISSING_URL]="Missing '-u CLONE_URL'", [MSG_MISSING_ARGS]="Missing [PATH | [PATHS]]", [MSG_INCOMPATIBLE_OPTIONS]="Incompatible options -u and -i", [MSG_ERR_NONDIRECT_REF]="Git reference is not direct", NULL }; #ifdef TEST static void dump_translatable_strings() { 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 = "" "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 {\n" " padding: 2px 8px 0px 8px;\n" " text-decoration: none;\n" " color: black;\n" "}\n" "\n" "nav a:hover {\n" " text-decoration: underline;\n" "}\n" "\n" ".selected-nav-item {\n" " background-color: hsl(0, 0%, 87%);\n" "}\n" "\n" "hr {\n" " margin-top: 0;\n" " border: 0;\n" " border-top: 3px solid hsl(0, 0%, 87%);\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: hsl(0, 0%, 93%);\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() { 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() { 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) { logerrl("malloc(", size, ")", strerror(errno), __LINE__); 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) { 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 */ 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() { 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) { 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() { 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 int index_write_header(FILE *const fd, const char *const idx_title) { const int e = fprintf( fd, "" "\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), idx_title, _(MSG_LOGO_ALT), 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() { const size_t signature_size = strlen(_(MSG_FOOTER_TEMPLATE)) - strlen("%s") + strlen(PROJECT_HOMEPAGE_LINK) + sizeof('\0'); char *const signature_text = malloc(signature_size); if (!signature_text) { logerrl("malloc(", signature_size, ")", strerror(errno), __LINE__); return NULL; } sprintf(signature_text, _(MSG_FOOTER_TEMPLATE), PROJECT_HOMEPAGE_LINK); return signature_text; } static int index_write_footer(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() { test_start("test_last_commit_date"); { testing("embedded Git repository tests/submodules/repo-1"); struct git_repository *repo; const int e = git_repository_open_ext( &repo, "tests/submodules/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:24") == 0); free(date); git_repository_free(repo); test_ok(); } { testing("embedded Git repository tests/submodules/repo-2"); struct git_repository *repo; const int e = git_repository_open_ext( &repo, "tests/submodules/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 write_logo(FILE *const fd) { if (fprintf(fd, "%s", LOGO_STR) < 0) { logerr("fprintf()", strerror(errno), __LINE__); return -1; } return 0; } static int write_style(FILE *const fd) { if (fprintf(fd, "%s", STYLE_STR) < 0) { logerr("fprintf()", strerror(errno), __LINE__); return -1; } return 0; } static int index_write_row(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__); } 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_write_header(index_fd, title)) { ret = -1; goto cleanup; } for (int i = 0; i < repoc; i++) { const int e = index_write_row(index_fd, repov[i]); if (e == -1) { ret = -1; goto cleanup; } if (e) { ret = 1; } } if ( index_write_footer(index_fd) || write_logo(logo_fd) || write_style(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_write_refs( 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" " %s\n" " \n" " \n" "
\n" "
\n" " \"%s\"\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), project_name, _(MSG_COMMIT_FEED), _(MSG_LANG), project_name, _(MSG_TAGS_FEED), _(MSG_LANG), project_name, _(MSG_LOGO_ALT), 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))) { const char *const name = git_reference_shorthand(ref); assert(name); const struct git_oid *const oid = git_reference_target(ref); if (!oid) { logerrs("git_reference_target(\"", name, "\")", _(MSG_ERR_NONDIRECT_REF), __LINE__); git_reference_free(ref); ret = -1; goto cleanup; } struct git_commit *commit; if (git_commit_lookup(&commit, repo, oid)) { const git_error *const error = git_error_last(); assert(error); logerr("git_commit_lookup()", error->message, __LINE__); git_reference_free(ref); 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); char *const date = formatted_date(author->when.time); if (!date) { git_commit_free(commit); git_reference_free(ref); ret = -1; goto cleanup; } e = fprintf( refs_fd, "" " \n" " \n" " \n" " \n" " \n" " \n" "", name, name, sha, summary, author->name, date ); if (e < 0) { logerr("fprintf()", strerror(errno), __LINE__); free(date); git_reference_free(ref); ret = -1; goto cleanup; } free(date); git_commit_free(commit); git_reference_free(ref); } 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" " %s\n" " \n" " \n" " \n" " %s\n" " \n" " \n" " %s\n" " \n" " %s\n" "
\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n", _(MSG_THEAD_TAG), _(MSG_THEAD_COMMITMSG), _(MSG_THEAD_DOWNLOAD), _(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))) { if (!git_reference_is_tag(ref)) { git_reference_free(ref); continue; } const char *const name = git_reference_shorthand(ref); assert(name); const struct git_oid *const oid = git_reference_target(ref); if (!oid) { logerrs("git_reference_target(\"", name, "\")", _(MSG_ERR_NONDIRECT_REF), __LINE__); git_reference_free(ref); ret = -1; goto cleanup; } struct git_commit *commit; if (git_commit_lookup(&commit, repo, oid)) { const git_error *const error = git_error_last(); assert(error); logerr("git_commit_lookup()", error->message, __LINE__); git_reference_free(ref); 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); char *const date = formatted_date(author->when.time); if (!date) { git_commit_free(commit); git_reference_free(ref); ret = -1; goto cleanup; } e = fprintf( refs_fd, "" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "", name, name, sha, summary, project_name, name, project_name, name, project_name, name, author->name, date ); if (e < 0) { logerr("fprintf()", strerror(errno), __LINE__); free(date); git_reference_free(ref); ret = -1; goto cleanup; } free(date); git_commit_free(commit); 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; } e = fprintf( refs_fd, "" " \n" "
\n" " %s\n" " \n" " %s\n" " \n" " %s\n" " \n" " %s\n" " \n" " %s\n" "
\n" " \n" " %s\n" " \n" " \n" " \n" " %s\n" " \n" " \n" " %s-%s.tar.xz\n" " (sig)\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_write_snapshots( const char *const outdir, struct git_repository *const repo, const char *const project_name ) { int ret = 0; int e; (void)outdir; (void)repo; (void)project_name; char *snapshots_dir = NULL; struct git_reference_iterator *ref_iter = NULL; if (!(snapshots_dir = strjoin(outdir, "/snapshots"))) { ret = -1; goto cleanup; } errno = 0; if (mkdir(snapshots_dir, S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH) && errno != EEXIST) { logerrs("mkdir(\"", snapshots_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(snapshots_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 (write_logo(logo_fd) || write_style(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__); } 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_write_refs(outdir, repo, encoded_name, encoded_description, clone_url) || repo_write_snapshots(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 static void unit_tests(){ dump_translatable_strings(); git_libgit2_init(); test_underscore(); 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 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_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; }