diff options
| -rw-r--r-- | .gitignore | 3 | ||||
| -rw-r--r-- | Makefile.am | 55 | ||||
| -rwxr-xr-x | build-aux/travis-build.sh | 3 | ||||
| -rw-r--r-- | configure.ac | 42 | ||||
| -rw-r--r-- | src/blogc-git-receiver.c | 502 | ||||
| -rw-r--r-- | src/blogc.c (renamed from src/main.c) | 0 | 
6 files changed, 589 insertions, 16 deletions
| @@ -44,8 +44,9 @@ Makefile.in  /blogc*.[17]  blogc*.html -# blogc +# binaries  /blogc +/blogc-git-receiver  # tests  /tests/check_content_parser diff --git a/Makefile.am b/Makefile.am index 8237508..934e1cc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -6,6 +6,7 @@ AM_DISTCHECK_CONFIGURE_FLAGS = \  	--enable-tests \  	--enable-ronn \  	--disable-valgrind \ +	--enable-git-receiver \  	$(NULL) @@ -43,6 +44,7 @@ noinst_HEADERS = \  noinst_LTLIBRARIES = \  	libblogc.la \ +	libblogc_utils.la \  	$(NULL)  noinst_PROGRAMS = \ @@ -52,6 +54,12 @@ bin_PROGRAMS = \  	blogc \  	$(NULL) +if BUILD_GIT_RECEIVER +bin_PROGRAMS += \ +	blogc-git-receiver \ +	$(NULL) +endif +  check_PROGRAMS = \  	$(NULL) @@ -65,7 +73,6 @@ libblogc_la_SOURCES = \  	src/renderer.c \  	src/source-parser.c \  	src/template-parser.c \ -	src/utils.c \  	$(NULL)  libblogc_la_CFLAGS = \ @@ -78,8 +85,18 @@ libblogc_la_LIBADD = \  	$(NULL) +libblogc_utils_la_SOURCES = \ +	src/utils.c \ +	$(NULL) + +libblogc_utils_la_CFLAGS = \ +	$(AM_CFLAGS) \ +	-I$(top_srcdir)/src \ +	$(NULL) + +  blogc_SOURCES = \ -	src/main.c \ +	src/blogc.c \  	$(NULL)  blogc_CFLAGS = \ @@ -89,9 +106,26 @@ blogc_CFLAGS = \  blogc_LDADD = \  	libblogc.la \ +	libblogc_utils.la \  	$(NULL) +if BUILD_GIT_RECEIVER +blogc_git_receiver_SOURCES = \ +	src/blogc-git-receiver.c \ +	$(NULL) + +blogc_git_receiver_CFLAGS = \ +	$(AM_CFLAGS) \ +	-I$(top_srcdir)/src \ +	$(NULL) + +blogc_git_receiver_LDADD = \ +	libblogc_utils.la \ +	$(NULL) +endif + +  ## Build rules: man pages  EXTRA_DIST += \ @@ -107,6 +141,14 @@ dist_man_MANS = \  	blogc-template.7 \  	$(NULL) +if BUILD_GIT_RECEIVER +EXTRA_DIST += \ +	$(NULL) + +dist_man_MANS += \ +	$(NULL) +endif +  MAINTAINERCLEANFILES += \  	$(dist_man_MANS) \  	$(NULL) @@ -181,6 +223,7 @@ tests_check_error_LDFLAGS = \  tests_check_error_LDADD = \  	$(CMOCKA_LIBS) \  	libblogc.la \ +	libblogc_utils.la \  	$(NULL)  tests_check_loader_SOURCES = \ @@ -200,6 +243,7 @@ tests_check_loader_LDFLAGS = \  tests_check_loader_LDADD = \  	$(CMOCKA_LIBS) \  	libblogc.la \ +	libblogc_utils.la \  	$(NULL)  tests_check_content_parser_SOURCES = \ @@ -217,6 +261,7 @@ tests_check_content_parser_LDFLAGS = \  tests_check_content_parser_LDADD = \  	$(CMOCKA_LIBS) \  	libblogc.la \ +	libblogc_utils.la \  	$(NULL)  tests_check_datetime_parser_SOURCES = \ @@ -234,6 +279,7 @@ tests_check_datetime_parser_LDFLAGS = \  tests_check_datetime_parser_LDADD = \  	$(CMOCKA_LIBS) \  	libblogc.la \ +	libblogc_utils.la \  	$(NULL)  tests_check_renderer_SOURCES = \ @@ -251,6 +297,7 @@ tests_check_renderer_LDFLAGS = \  tests_check_renderer_LDADD = \  	$(CMOCKA_LIBS) \  	libblogc.la \ +	libblogc_utils.la \  	$(NULL)  tests_check_source_parser_SOURCES = \ @@ -268,6 +315,7 @@ tests_check_source_parser_LDFLAGS = \  tests_check_source_parser_LDADD = \  	$(CMOCKA_LIBS) \  	libblogc.la \ +	libblogc_utils.la \  	$(NULL)  tests_check_template_parser_SOURCES = \ @@ -285,6 +333,7 @@ tests_check_template_parser_LDFLAGS = \  tests_check_template_parser_LDADD = \  	$(CMOCKA_LIBS) \  	libblogc.la \ +	libblogc_utils.la \  	$(NULL)  tests_check_utils_SOURCES = \ @@ -301,7 +350,7 @@ tests_check_utils_LDFLAGS = \  tests_check_utils_LDADD = \  	$(CMOCKA_LIBS) \ -	libblogc.la \ +	libblogc_utils.la \  	$(NULL)  endif diff --git a/build-aux/travis-build.sh b/build-aux/travis-build.sh index 1fcac10..4187575 100755 --- a/build-aux/travis-build.sh +++ b/build-aux/travis-build.sh @@ -13,9 +13,10 @@ if [[ "x${TARGET}" = xw* ]]; then      export CHOST="x86_64-w64-mingw32"      [[ "x${TARGET}" = "xw32" ]] && export CHOST="i686-w64-mingw32"      MAKE_TARGET="all" +    CONFIGURE_ARGS="--disable-git-receiver"  else      export CFLAGS="-Wall -g" -    export CONFIGURE_ARGS="--enable-tests --enable-valgrind" +    CONFIGURE_ARGS="--enable-tests --enable-valgrind"  fi  pushd build > /dev/null diff --git a/configure.ac b/configure.ac index 1e7366e..858db59 100644 --- a/configure.ac +++ b/configure.ac @@ -119,6 +119,24 @@ AS_IF([test "x$have_cmocka" = "xyes"], , [  ])  AM_CONDITIONAL([USE_CMOCKA], [test "x$have_cmocka" = "xyes"]) +GIT_RECEIVER="disabled" +AC_ARG_ENABLE([git-receiver], AS_HELP_STRING([--disable-git-receiver], +              [disable blogc-git-receiver build])) +AS_IF([test "x$enable_git_receiver" != "xno"], [ +  AC_CHECK_HEADERS([sys/types.h sys/stat.h time.h libgen.h unistd.h errno.h dirent.h], [ +    GIT_RECEIVER="enabled" +    have_git_receiver=yes +  ], [ +    have_git_receiver=no +  ]) +]) +AS_IF([test "x$have_git_receiver" = "xyes"], , [ +  AS_IF([test "x$enable_git_receiver" = "xyes"], [ +    AC_MSG_ERROR([blogc-git-receiver requested but required headers not found]) +  ]) +]) +AM_CONDITIONAL([BUILD_GIT_RECEIVER], [test "x$have_git_receiver" = "xyes"]) +  AC_CHECK_HEADERS([sys/types.h sys/stat.h time.h])  LT_LIB_M @@ -131,19 +149,21 @@ AC_CONFIG_FILES([  AC_OUTPUT  AS_ECHO(" -        ==== ${PACKAGE_STRING} ==== +        ====== ${PACKAGE_STRING} ====== + +        prefix:              ${prefix} +        exec_prefix:         ${exec_prefix} +        bindir:              ${bindir} -        prefix:       ${prefix} -        exec_prefix:  ${exec_prefix} -        bindir:       ${bindir} +        compiler:            ${CC} +        cflags:              ${CFLAGS} +        ldflags:             ${LDFLAGS} -        compiler:     ${CC} -        cflags:       ${CFLAGS} -        ldflags:      ${LDFLAGS} +        blogc-git-receiver:  ${GIT_RECEIVER} -        tests:        ${TESTS} +        tests:               ${TESTS} -        ronn:         ${RONN} -        valgrind:     ${VALGRIND} -        rpmbuild:     ${RPMBUILD} +        ronn:                ${RONN} +        valgrind:            ${VALGRIND} +        rpmbuild:            ${RPMBUILD}  ") diff --git a/src/blogc-git-receiver.c b/src/blogc-git-receiver.c new file mode 100644 index 0000000..ffeac61 --- /dev/null +++ b/src/blogc-git-receiver.c @@ -0,0 +1,502 @@ +/* + * blogc: A blog compiler. + * Copyright (C) 2015-2016 Rafael G. Martins <rafael@rafaelmartins.eng.br> + * + * This program can be distributed under the terms of the BSD License. + * See the file LICENSE. + */ + +#ifdef HAVE_CONFIG_H +#include <config.h> +#endif /* HAVE_CONFIG_H */ + +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <libgen.h> +#include <unistd.h> +#include <errno.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <dirent.h> +#include <time.h> + +#include "utils.h" + +#ifndef BUFFER_SIZE +#define BUFFER_SIZE 4096 +#endif + + +static unsigned int +cpu_count(void) +{ +#ifdef _SC_NPROCESSORS_ONLN +    long num = sysconf(_SC_NPROCESSORS_ONLN); +    if (num >= 1) +        return (unsigned int) num; +#endif +    return 1; +} + + +static void +rmdir_recursive(const char *dir) +{ +    struct stat buf; +    if (0 != stat(dir, &buf)) { +        fprintf(stderr, "warning: failed to remove directory (%s): %s\n", dir, +            strerror(errno)); +        return; +    } +    if (!S_ISDIR(buf.st_mode)) { +        fprintf(stderr, "error: trying to remove invalid directory: %s\n", dir); +        exit(2); +    } +    DIR *d = opendir(dir); +    if (d == NULL) { +        fprintf(stderr, "error: failed to open directory: %s\n", +            strerror(errno)); +        exit(2); +    } +    struct dirent *e; +    while (NULL != (e = readdir(d))) { +        if ((0 == strcmp(e->d_name, ".")) || (0 == strcmp(e->d_name, ".."))) +            continue; +        char *f = sb_strdup_printf("%s/%s", dir, e->d_name); +        if (0 != stat(f, &buf)) { +            fprintf(stderr, "error: failed to stat directory entry (%s): %s\n", +                e->d_name, strerror(errno)); +            free(f); +            exit(2); +        } +        if (S_ISDIR(buf.st_mode)) { +            rmdir_recursive(f); +        } +        else if (0 != unlink(f)) { +            fprintf(stderr, "error: failed to remove file (%s): %s\n", f, +                strerror(errno)); +            free(f); +            exit(2); +        } +        free(f); +    } +    if (0 != closedir(d)) { +        fprintf(stderr, "error: failed to close directory: %s\n", +            strerror(errno)); +        exit(2); +    } +    if (0 != rmdir(dir)) { +        fprintf(stderr, "error: failed to remove directory: %s\n", +            strerror(errno)); +        exit(2); +    } +} + + +static int +git_shell(int argc, char *argv[]) +{ +    int rv = 0; + +    char *repo = NULL; +    char *command_orig = NULL; +    char *command_name = NULL; +    char command_new[BUFFER_SIZE]; + +    bool exec_git = false; + +    // validate git command +    size_t len = strlen(argv[2]); +    if (!((len > 17 && (0 == strncmp(argv[2], "git-receive-pack ", 17))) || +          (len > 16 && (0 == strncmp(argv[2], "git-upload-pack ", 16))) || +          (len > 19 && (0 == strncmp(argv[2], "git-upload-archive ", 19))))) +    { +        fprintf(stderr, "error: unsupported git command: %s\n", argv[2]); +        rv = 1; +        goto cleanup; +    } + +    // get shell path +    char *self = getenv("SHELL"); +    if (self == NULL) { +        fprintf(stderr, "error: failed to find blogc-git-receiver path\n"); +        rv = 1; +        goto cleanup; +    } + +    // get home path +    char *home = getenv("HOME"); +    if (home == NULL) { +        fprintf(stderr, "error: failed to find user home path\n"); +        rv = 1; +        goto cleanup; +    } + +    // get git repository +    command_orig = sb_strdup(argv[2]); +    char *p, *r; +    for (p = command_orig; *p != ' ' && *p != '\0'; p++); +    if (*p == ' ') +        p++; +    if (*p == '\'' || *p == '"') +        p++; +    if (*p == '/') +        p++; +    for (r = p; *p != '\'' && *p != '"' && *p != '\0'; p++); +    if (*p == '\'' || *p == '"') +        *p = '\0'; +    if (*--p == '/') +        *p = '\0'; + +    repo = sb_strdup_printf("repos/%s", r); + +    // check if repository is sane +    if (0 == strlen(repo)) { +        fprintf(stderr, "error: invalid repository\n"); +        rv = 1; +        goto cleanup; +    } + +    if (0 == strncmp(argv[2], "git-upload-", 11))  // no need to check len here +        goto git_exec; + +    if (0 != chdir(home)) { +        fprintf(stderr, "error: failed to chdir (%s): %s\n", home, +            strerror(errno)); +        rv = 1; +        goto cleanup; +    } + +    if (0 != access(repo, F_OK)) { +        char *git_init_cmd = sb_strdup_printf( +            "git init --bare \"%s\" > /dev/null", repo); +        if (0 != system(git_init_cmd)) { +            fprintf(stderr, "error: failed to create git repository: %s\n", +                repo); +            rv = 1; +            free(git_init_cmd); +            goto cleanup; +        } +        free(git_init_cmd); +    } + +    if (0 != chdir(repo)) { +        fprintf(stderr, "error: failed to chdir (%s/%s): %s\n", home, repo, +            strerror(errno)); +        rv = 1; +        goto cleanup; +    } + +    if (0 != access("hooks", F_OK)) { +        // openwrt git package won't install git templates, then the git +        // repositories created with it won't have the hooks/ directory. +        if (0 != mkdir("hooks", 0777)) {  // mkdir honors umask for us. +            fprintf(stderr, "error: failed to create directory (%s/%s/hooks): " +                "%s\n", home, repo, strerror(errno)); +            rv = 1; +            goto cleanup; +        } +    } + +    if (0 != chdir("hooks")) { +        fprintf(stderr, "error: failed to chdir (%s/%s/hooks): %s\n", home, +            repo, strerror(errno)); +        rv = 1; +        goto cleanup; +    } + +    if (0 == access("pre-receive", F_OK)) { +        if (0 != unlink("pre-receive")) { +            fprintf(stderr, "error: failed to remove old symlink " +                "(%s/%s/hooks/pre-receive): %s\n", home, repo, strerror(errno)); +            rv = 1; +            goto cleanup; +        } +    } + +    if (0 != symlink(self, "pre-receive")) { +        fprintf(stderr, "error: failed to create symlink " +            "(%s/%s/hooks/pre-receive): %s\n", home, repo, strerror(errno)); +        rv = 1; +        goto cleanup; +    } + +    if (0 == access("post-receive", F_OK)) { +        if (0 != unlink("post-receive")) { +            fprintf(stderr, "error: failed to remove old symlink " +                "(%s/%s/hooks/post-receive): %s\n", home, repo, strerror(errno)); +            rv = 1; +            goto cleanup; +        } +    } + +    if (0 != symlink(self, "post-receive")) { +        fprintf(stderr, "error: failed to create symlink " +            "(%s/%s/hooks/post-receive): %s\n", home, repo, strerror(errno)); +        rv = 1; +        goto cleanup; +    } + +    if (0 != chdir(home)) { +        fprintf(stderr, "error: failed to chdir (%s): %s\n", home, +            strerror(errno)); +        rv = 1; +        goto cleanup; +    } + +git_exec: +    command_name = sb_strdup(argv[2]); +    for (p = command_name; *p != ' ' && *p != '\0'; p++); +    if (*p == ' ') +        *p = '\0'; + +    if (BUFFER_SIZE < (strlen(command_name) + strlen(repo) + 4)) { +        fprintf(stderr, "error: git-shell command is too big\n"); +        rv = 1; +        goto cleanup; +    } + +    if (snprintf(command_new, BUFFER_SIZE, "%s '%s'", command_name, repo)) +        exec_git = true; + +cleanup: +    free(repo); +    free(command_orig); +    free(command_name); + +    if (exec_git) { +        execlp("git-shell", "git-shell", "-c", command_new, NULL); + +        // execlp only returns on error, then something bad happened +        fprintf(stderr, "error: failed to execute git-shell\n"); +        rv = 1; +    } + +    return rv; +} + + +static int +git_post_receive_hook(int argc, char *argv[]) +{ +    if (0 != system("git remote get-url --push mirror &> /dev/null")) +        return 0; + +    // at this point we know that we have a remote called mirror, we can just +    // push to it. +    if (0 != system("git push --mirror mirror")) +        fprintf(stderr, "warning: failed push to git mirror\n"); + +    return 0; +} + + +typedef enum { +    START_OLD = 1, +    OLD, +    START_NEW, +    NEW, +    START_REF, +    REF +} input_state_t; + + +static int +git_pre_receive_hook(int argc, char *argv[]) +{ +    int c; +    char buffer[BUFFER_SIZE]; + +    input_state_t state = START_OLD; +    size_t i = 0; +    size_t start = 0; + +    int rv = 0; +    char *new = NULL; +    char *master = NULL; + +    while (EOF != (c = getc(stdin))) { + +        buffer[i] = (char) c; + +        switch (state) { +            case START_OLD: +                start = i; +                state = OLD; +                break; +            case OLD: +                if (c != ' ') +                    break; +                // no need to store old +                state = START_NEW; +                break; +            case START_NEW: +                start = i; +                state = NEW; +                break; +            case NEW: +                if (c != ' ') +                    break; +                state = START_REF; +                new = strndup(buffer + start, i - start); +                break; +            case START_REF: +                start = i; +                state = REF; +                break; +            case REF: +                if (c != '\n') +                    break; +                state = START_OLD; +                // we just care about a ref (refs/heads/master), everything +                // else is disposable :) +                if (!((i - start == 17) && +                      (0 == strncmp("refs/heads/master", buffer + start, 17)))) +                { +                    free(new); +                    new = NULL; +                    break; +                } +                master = new; +                break; +        } + +        if (++i >= BUFFER_SIZE) { +            fprintf(stderr, "error: pre-receive hook payload is too big.\n"); +            rv = 1; +            goto cleanup2; +        } +    } + +    if (master == NULL) { +        fprintf(stderr, "warning: no reference to master branch found. " +            "nothing to deploy.\n"); +        goto cleanup2; +    } + +    char *repo_dir = NULL; +    char *output_dir = NULL; + +    if (NULL == getcwd(buffer, BUFFER_SIZE)) { +        fprintf(stderr, "error: failed to get repository remote path: %s\n", +            strerror(errno)); +        rv = 1; +        goto cleanup; +    } + +    repo_dir = sb_strdup(buffer); + +    char dir[] = "/tmp/blogc_XXXXXX"; +    if (NULL == mkdtemp(dir)) { +        rv = 1; +        goto cleanup; +    } + +    char *git_archive_cmd = sb_strdup_printf( +        "git archive \"%s\" | tar -x -C \"%s\" -f -", master, dir); +    if (0 != system(git_archive_cmd)) { +        fprintf(stderr, "error: failed to extract git content to temporary " +            "directory: %s\n", dir); +        rv = 1; +        free(git_archive_cmd); +        goto cleanup; +    } +    free(git_archive_cmd); + +    if (0 != chdir(dir)) { +        fprintf(stderr, "error: failed to chdir (%s): %s\n", dir, +            strerror(errno)); +        rv = 1; +        goto cleanup; +    } + +    if ((0 != access("Makefile", F_OK)) && (0 != access("GNUMakefile", F_OK))) { +        fprintf(stderr, "warning: no makefile found. skipping ...\n"); +        goto cleanup; +    } + +    char *home = getenv("HOME"); +    if (home == NULL) { +        fprintf(stderr, "error: failed to find user home path\n"); +        rv = 1; +        goto cleanup; +    } + +    unsigned long epoch = time(NULL); +    output_dir = sb_strdup_printf("%s/builds/%s-%lu", home, master, epoch); +    char *gmake_cmd = sb_strdup_printf( +        "gmake -j%d OUTPUT_DIR=\"%s\" BLOGC_GIT_RECEIVER=1", +        cpu_count(), output_dir); +    fprintf(stdout, "running command: %s\n\n", gmake_cmd); +    fflush(stdout); +    if (0 != system(gmake_cmd)) { +        fprintf(stderr, "error: failed to build website ...\n"); +        rmdir_recursive(output_dir); +        free(gmake_cmd); +        rv = 1; +        goto cleanup; +    } +    free(gmake_cmd); + +    if (0 != chdir(repo_dir)) { +        fprintf(stderr, "error: failed to chdir (%s): %s\n", repo_dir, +            strerror(errno)); +        rmdir_recursive(output_dir); +        rv = 1; +        goto cleanup; +    } + +    char *htdocs_sym = NULL; +    ssize_t htdocs_sym_len = readlink("htdocs", buffer, BUFFER_SIZE); +    if (0 < htdocs_sym_len) { +        if (0 != unlink("htdocs")) { +            fprintf(stderr, "error: failed to remove symlink (%s/htdocs): %s\n", +                repo_dir, strerror(errno)); +            rmdir_recursive(output_dir); +            rv = 1; +            goto cleanup; +        } +        buffer[htdocs_sym_len] = '\0'; +        htdocs_sym = buffer; +    } + +    if (0 != symlink(output_dir, "htdocs")) { +        fprintf(stderr, "error: failed to create symlink (%s/htdocs): %s\n", +            repo_dir, strerror(errno)); +        rmdir_recursive(output_dir); +        rv = 1; +        goto cleanup; +    } + +    if (htdocs_sym != NULL) +        rmdir_recursive(htdocs_sym); + +cleanup: +    free(output_dir); +    rmdir_recursive(dir); +    free(repo_dir); +cleanup2: +    free(new); +    return rv; +} + + +int +main(int argc, char *argv[]) +{ +    if (argc > 0) { +        if (0 == strcmp(basename(argv[0]), "pre-receive")) +            return git_pre_receive_hook(argc, argv); +        if (0 == strcmp(basename(argv[0]), "post-receive")) +            return git_post_receive_hook(argc, argv); +    } + +    if (argc == 3 && (0 == strcmp(argv[1], "-c"))) +        return git_shell(argc, argv); + +    fprintf(stderr, "error: this is a special shell, go away!\n"); +    return 1; +} | 
