mobley

C Git HTTP server
Log | Files | Refs | Submodules | README | git clone https://git.ne02ptzero.me/git/mobley

commit b8bd8cf95c6848ace11a7e1adb4e03186cd964da
parent eabe8d542e7c1db9e9ff88328ef7ce640f8ccdf5
Author: Louis Solofrizzo <lsolofrizzo@online.net>
Date:   Fri, 21 Dec 2018 10:32:57 +0100

Add file basic preview, with markdown formatting

Signed-off-by: Louis Solofrizzo <lsolofrizzo@online.net>

Diffstat:
A.gitmodules | 3+++
MCMakeLists.txt | 11+++++++++++
Acontrib/sundown | 1+
Afile.c | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afile.h | 23+++++++++++++++++++++++
Mfile_format.c | 27+++++++++++++++++++++++++++
Mfile_format.h | 2++
Mhtml.c | 18++++++++++++++++--
Mhtml.h | 2++
Mmobley.h | 17++++++++++++++++-
Mrepository.c | 60+++++++++++++++---------------------------------------------
Mrepository.h | 4----
Mrepository_handler.c | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Mstyle/main.css | 48++++++++++++++++++++++++++++++++++++++++++++++++
Mtemplate.c | 23+++++++++++++++++++++++
15 files changed, 347 insertions(+), 74 deletions(-)

diff --git a/.gitmodules b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "contrib/sundown"] + path = contrib/sundown + url = https://github.com/vmg/sundown.git diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -36,6 +36,8 @@ add_definitions(-DMOBLEY_MAJOR="${MOBLEY_MAJOR}") add_definitions(-DMOBLEY_MINOR="${MOBLEY_MINOR}") include_directories(".") +include_directories("./contrib/sundown/src") +include_directories("./contrib/sundown") link_libraries(pthread event event_openssl crypto ssl evhtp yaml git2) @@ -51,6 +53,15 @@ add_executable(${MOBLEY_NAME} main.c repository.c repository_handler.c file_format.c + file.c + ./contrib/sundown/src/autolink.c + ./contrib/sundown/src/buffer.c + ./contrib/sundown/src/markdown.c + ./contrib/sundown/html/html.c + ./contrib/sundown/html/html_smartypants.c + ./contrib/sundown/html/houdini_href_e.c + ./contrib/sundown/html/houdini_html_e.c + ./contrib/sundown/src/stack.c ) install(TARGETS ${MOBLEY_NAME} DESTINATION bin) diff --git a/contrib/sundown b/contrib/sundown @@ -0,0 +1 @@ +Subproject commit 37728fb2d7137ff7c37d0a474cb827a8d6d846d8 diff --git a/file.c b/file.c @@ -0,0 +1,86 @@ +/** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#include <file.h> +#include <file_format.h> +#include <html.h> + +static bool file_show_markdown(mobley_t *ctx, server_req_t *r, git_blob *blob) +{ + struct buf *ob = bufnew(4096); + + if (ob == NULL) + return false; + + sd_markdown_render(ob, git_blob_rawcontent(blob), git_blob_rawsize(blob), + ctx->format.md); + + evbuffer_add(r->req->buffer_out, ob->data, ob->size); + bufrelease(ob); + return true; +} + +static bool file_show_raw(mobley_t *ctx, server_req_t *r, git_blob *blob) +{ + html_open("pre", .class = "file-raw") { + string_raw_format_html(r, git_blob_rawcontent(blob), git_blob_rawsize(blob)); + } html_close("pre"); + return true; +} + +static bool file_show_header(mobley_t *ctx, server_req_t *r, git_blob *blob, const char *path) +{ + html_open("table", .class = "file-header") { + html_open("tr") { + html_open("td") { + html_nd("i", .class = __format("fa fa-%s fa-fw", file_icon(path))); + } html_close("td"); + + html_open("td") { + html("b", path); + } html_close("td"); + + html_open("td") { + html_raw("%zu Bytes", git_blob_rawsize(blob)); + } html_close("td"); + } html_close("tr"); + } html_close("table"); + html_nd("hr"); + + return true; +} + +bool file_show(mobley_t *ctx, server_req_t *r, git_tree_entry *entry, bool header) +{ + git_object *obj = NULL; + const char *path = git_tree_entry_name(entry); + bool ret = false; + + git_tree_entry_to_object(&obj, r->repo->repo, entry); + + if (header) + file_show_header(ctx, r, (git_blob *)obj, path); + + html_open("div", .class = "file-content") { + if (file_is_markdown(path)) + ret = file_show_markdown(ctx, r, (git_blob *)obj); + else + ret = file_show_raw(ctx, r, (git_blob *)obj); + } html_close("div"); + + git_object_free(obj); + git_tree_entry_free(entry); + return ret; +} diff --git a/file.h b/file.h @@ -0,0 +1,23 @@ +/** + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. +*/ + +#ifndef FILE_H +# define FILE_H + +# include <mobley.h> + +bool file_show(mobley_t *ctx, server_req_t *r, git_tree_entry *entry, bool header); + +#endif /* FILE_H */ diff --git a/file_format.c b/file_format.c @@ -18,6 +18,24 @@ #include <string.h> +const char *file_icon(const char *path) +{ + if (file_is_code(path)) + return "file-code"; + if (file_is_audio(path)) + return "file-audio"; + if (file_is_video(path)) + return "file-video"; + if (file_is_pdf(path)) + return "file-pdf"; + if (file_is_archive(path)) + return "file-archive"; + if (file_is_image(path)) + return "file-image"; + + return "file-alt"; +} + static bool format_match(const char **array, size_t size, const char *filename) { int format_cur; @@ -97,3 +115,12 @@ bool file_is_image(const char *filename) return format_match(formats, COUNT_OF(formats), filename); } + +bool file_is_markdown(const char *filename) +{ + const char *formats[] = { + ".md", ".markdown" + }; + + return format_match(formats, COUNT_OF(formats), filename); +} diff --git a/file_format.h b/file_format.h @@ -25,5 +25,7 @@ bool file_is_pdf(const char *filename); bool file_is_video(const char *filename); bool file_is_archive(const char *filename); bool file_is_image(const char *filename); +bool file_is_markdown(const char *filename); +const char *file_icon(const char *path); #endif /* FILE_FORMAT_H */ diff --git a/html.c b/html.c @@ -75,10 +75,10 @@ void _html(server_req_t *r, const mobley_t *ctx, const char *b, const char *d, if (a->width != NULL) evbuffer_add_printf(r->req->buffer_out, " width=\"%s\"", a->width); - if (d == NULL) + if (d == NULL && strcmp(b, "i") != 0) goto closing; - evbuffer_add_printf(r->req->buffer_out, ">%s</%s", d, b); + evbuffer_add_printf(r->req->buffer_out, ">%s</%s", d != NULL ? d : "", b); closing: if (strcmp(b, "img") == 0 || strcmp(b, "meta") == 0) @@ -97,3 +97,17 @@ char *__format(const char *s, ...) va_end(ap); return __buffer; } + + +void string_raw_format_html(server_req_t *r, const char *data, size_t len) +{ + for (size_t i = 0; i < len; i++) + { + if (data[i] == '<') + evbuffer_add(r->req->buffer_out, "&lt;", 3); + else if (data[i] == '>') + evbuffer_add(r->req->buffer_out, "&gt;", 3); + else + evbuffer_add(r->req->buffer_out, &data[i], 1); + } +} diff --git a/html.h b/html.h @@ -58,4 +58,6 @@ void _html(server_req_t *r, const mobley_t *ctx, __PRINTF_FUNCTION(1, 2) char *__format(const char *s, ...); +void string_raw_format_html(server_req_t *r, const char *data, size_t len); + #endif /* HTML_H */ diff --git a/mobley.h b/mobley.h @@ -20,6 +20,8 @@ # include <evhtp/evhtp.h> # include <evhtp/log.h> +# include <markdown.h> +# include <html/html.h> # include <server.h> # include <repository.h> @@ -43,6 +45,10 @@ typedef struct { list_head_t repos; } repos; + struct { + struct sd_markdown *md; + } format; + struct event_base *evbase; struct evhtp *htp; } mobley_t; @@ -64,6 +70,7 @@ static inline void mobley_dtr(mobley_t *ctx) { mobley_conf_dtr(ctx); + sd_markdown_free(ctx->format.md); evhtp_unbind_socket(ctx->htp); evhtp_free(ctx->htp); if (ctx->evbase) @@ -76,7 +83,7 @@ static inline void mobley_config(mobley_t *ctx) evhtp_enable_flag(ctx->htp, EVHTP_FLAG_ENABLE_ALL); #ifndef EVHTP_DISABLE_EVTHR - evhtp_use_threads_wexit(ctx->htp, NULL, NULL, 4, NULL); + evhtp_use_threads_wexit(ctx->htp, NULL, NULL, 8, NULL); #endif evhtp_bind_socket(ctx->htp, ctx->ip, ctx->port, 128); @@ -85,6 +92,9 @@ static inline void mobley_config(mobley_t *ctx) static inline bool mobley_init(mobley_t *ctx) { + struct html_renderopt options; + struct sd_callbacks callbacks; + ctx->evbase = event_base_new(); if (ctx->evbase == NULL) goto fail; @@ -93,6 +103,11 @@ static inline bool mobley_init(mobley_t *ctx) if (ctx->htp == NULL) goto fail; + sdhtml_renderer(&callbacks, &options, 0); + ctx->format.md = sd_markdown_new(0, 12, &callbacks, &options); + if (ctx->format.md == NULL) + goto fail; + mobley_config(ctx); return true; fail: diff --git a/repository.c b/repository.c @@ -39,7 +39,7 @@ git_commit *repository_get_last_commit(repository_t *repo, const char *ref) return NULL; } -static int match_with_parent(git_commit *commit, int i, git_diff_options *opts, git_pathspec_match_list **match_list, git_pathspec *ps) +static int match_with_parent(git_commit *commit, int i, git_diff_options *opts) { git_commit *parent; git_tree *a, *b; @@ -71,7 +71,6 @@ bool repository_get_log_from_paths(repository_t *repo, const git_oid *oid, const git_commit *commit = NULL; git_pathspec *ps = NULL; git_diff_options diffopts = GIT_DIFF_OPTIONS_INIT; - git_pathspec_match_list *match_list; commit_node_t *c; unsigned int sorting = GIT_SORT_NONE; size_t i = 0; @@ -93,11 +92,10 @@ bool repository_get_log_from_paths(repository_t *repo, const git_oid *oid, const goto fail; parents = git_commit_parentcount(commit); - match_list = NULL; unmatched = parents; if (parents == 1) - unmatched = match_with_parent(commit, 0, &diffopts, &match_list, ps) ? 0 : 1; + unmatched = match_with_parent(commit, 0, &diffopts) ? 0 : 1; else if (parents == 0) { if (git_commit_tree(&tree, commit) != 0) @@ -112,7 +110,7 @@ bool repository_get_log_from_paths(repository_t *repo, const git_oid *oid, const { for (i = 0; (int)i < parents; i++) { - if (match_with_parent(commit, i, &diffopts, &match_list, ps)) + if (match_with_parent(commit, i, &diffopts)) unmatched--; } } @@ -123,24 +121,14 @@ bool repository_get_log_from_paths(repository_t *repo, const git_oid *oid, const continue ; } - printf("Commit match %s!\n", git_commit_summary(commit)); - /*if (match_list != NULL)*/ - /*{*/ - /*for (size_t i = 0; i < git_pathspec_match_list_entrycount(match_list); i++)*/ - /*{*/ - /*printf("Match = %s\n", git_pathspec_match_list_entry(match_list, i));*/ - c = calloc(1, sizeof(*c)); - c->commit = commit; - list_add_tail(&c->node, paths); - if (list_count(paths) == max) - goto end; - /*}*/ - /*git_pathspec_match_list_free(match_list);*/ - /*}*/ - /*else*/ - /*{*/ - /*git_commit_free(commit);*/ - /*}*/ + c = calloc(1, sizeof(*c)); + if (c == NULL) + goto fail; + + c->commit = commit; + list_add_tail(&c->node, paths); + if (list_count(paths) == max) + goto end; } end: @@ -148,6 +136,10 @@ end: git_revwalk_free(walk); return true; fail: + if (commit) + git_commit_free(commit); + git_pathspec_free(ps); + git_revwalk_free(walk); return false; } @@ -174,25 +166,3 @@ bool repository_get_log_from_path_default(repository_t *repo, const char *path, return repository_get_log_from_path_ref(repo, path, repo->default_branch != NULL ? repo->default_branch : "master", out); } - -bool repository_get_log_default(repository_t *repo, list_head_t *out) -{ - return repository_get_log_ref(repo, - repo->default_branch != NULL ? repo->default_branch : "master", out); -} - -bool repository_get_log_ref(repository_t *repo, const char *ref, list_head_t *out) -{ - git_oid oid_parent_commit; - char branch[1024] = { 0 }; - - if (repo->repo == NULL) - return NULL; - - snprintf(branch, sizeof(branch), "refs/heads/%s", ref); - - if (git_reference_name_to_id(&oid_parent_commit, repo->repo, branch) == 0) - return repository_get_log_from_paths(repo, &oid_parent_commit, "", out, 1); - - return NULL; -} diff --git a/repository.h b/repository.h @@ -112,8 +112,4 @@ bool repository_get_log_from_paths(repository_t *repo, const git_oid *oid, const bool repository_get_log_from_path_ref(repository_t *repo, const char *path, const char *ref, list_head_t *out); bool repository_get_log_from_path_default(repository_t *repo, const char *path, list_head_t *out); -bool repository_get_log_default(repository_t *repo, list_head_t *out); -bool repository_get_log_ref(repository_t *repo, const char *ref, list_head_t *out); -bool repository_get_log_from_path_default(repository_t *repo, const char *path, list_head_t *out); - #endif /* REPOSITORY_H */ diff --git a/repository_handler.c b/repository_handler.c @@ -16,25 +16,13 @@ #include <repository_handler.h> #include <html.h> #include <file_format.h> +#include <file.h> -static const char *file_icon(path_commit_node_t *node) +static const char *repository_file_icon(path_commit_node_t *node) { if (node->mode & GIT_FILEMODE_TREE) return "folder"; - if (file_is_code(node->display)) - return "file-code"; - if (file_is_audio(node->display)) - return "file-audio"; - if (file_is_video(node->display)) - return "file-video"; - if (file_is_pdf(node->display)) - return "file-pdf"; - if (file_is_archive(node->display)) - return "file-archive"; - if (file_is_image(node->display)) - return "file-image"; - - return "file-alt"; + return file_icon(node->display); } static void repository_print_tree_items(mobley_t *ctx ,server_req_t *r, list_head_t *head) @@ -42,21 +30,22 @@ static void repository_print_tree_items(mobley_t *ctx ,server_req_t *r, list_hea path_commit_node_t *node, *tmp; commit_node_t *commit; git_time_t time; + list_head_t new; list_for_each_entry_safe(node, tmp, head, node) { html_open("tr") { html_open("td") { - html_raw("<i class='fa fa-%s fa-fw'></i>", file_icon(node)); + html_nd("i", + .class = __format("fa fa-%s fa-fw", repository_file_icon(node)) + ); html("a", node->display, .href = __format("tree/%s", node->path) ); } html_close("td"); - list_head_t new; - INIT_LIST_HEAD(&new); - - repository_get_log_from_path_default(r->repo, node->path, &new); + INIT_LIST_HEAD(&new); + repository_get_log_from_path_default(r->repo, node->path, &new); if (list_count(&new) != 0) { @@ -164,7 +153,10 @@ static void repository_tree_begin(mobley_t *ctx, server_req_t *r) tmp = r->path[i]; r->path[i] = '\0'; - html("a", "..", .href = __format("tree/%s", r->path)) + if (STR_NULL_OR_EMPTY(r->path)) + html("a", "..", .href = "") + else + html("a", "..", .href = __format("tree/%s", r->path)) r->path[i] = tmp; } html_close("td"); @@ -219,6 +211,10 @@ static bool repository_route_tree_handler(mobley_t *ctx, server_req_t *r) char *cur = path; char *end = path + sizeof(path); string_array_t *node; + git_commit *head = repository_get_last_commit(r->repo, r->branch); + git_tree *root = NULL; + git_tree_entry *tree = NULL; + bool ret = false; list_for_each_entry(node, r->uri.next, node) { @@ -229,7 +225,62 @@ static bool repository_route_tree_handler(mobley_t *ctx, server_req_t *r) } r->path = path; - return repository_tree_ls(ctx, r); + + if (strlen(path) == 0) + ret = repository_tree_ls(ctx, r); + else + { + if (git_commit_tree(&root, head) != 0) + return false; + + path[strlen(path) - 1] = 0; + git_tree_entry_bypath(&tree, root, path); + if (tree == NULL) + return false; + + if (git_tree_entry_filemode(tree) & GIT_FILEMODE_TREE) + { + path[strlen(path)] = '/'; + ret = repository_tree_ls(ctx, r); + } + else + { + ret = file_show(ctx, r, tree, true); + } + } + + return ret; +} + +static bool repository_show_readme(mobley_t *ctx, server_req_t *r) +{ + git_commit *head = repository_get_last_commit(r->repo, r->branch); + git_tree *root = NULL; + git_tree_entry *tree = NULL; + bool ret = false; + static const char *readmes[] = { + "README.md", + "README" + }; + + if (git_commit_tree(&root, head) != 0) + return false; + + STATIC_ARRAY_FOREACH(const char **readme, readmes) + { + git_tree_entry_bypath(&tree, root, *readme); + if (tree != NULL) + break ; + } + + if (tree == NULL) + goto end; + + ret = file_show(ctx, r, tree, true); +end: + git_commit_free(head); + git_tree_free(root); + return ret; } bool repository_route_handler(mobley_t *ctx, server_req_t *r) @@ -245,6 +296,7 @@ bool repository_route_handler(mobley_t *ctx, server_req_t *r) { repository_tree_ls(ctx, r); html_nd("hr", .closing_slash = true); + repository_show_readme(ctx, r); } else { diff --git a/style/main.css b/style/main.css @@ -129,3 +129,51 @@ table tbody .commit a { color: white; background: black; } + +.file-header { + width: auto; + margin-left: 10px; +} + +.file-header tr td { + padding: 0; + margin: 0; +} + +.file-header tr i { + margin-right: 0; +} + +.file-header tr b { + margin-right: 10px; +} + +.file-header tr:hover td { + background: transparent; +} + +.file-content { + padding: 0px 10px; +} + +.file-content pre, .file-content code { + background: black; + color: white; + font-size: .9em; + padding: 5px 8px; + border: 1px solid #9993; +} + +.file-content pre code +{ + padding: 0; + border: 0; +} + +.file-raw { + background: inherit !important; + color: inherit !important; + padding: 0 !important; + border: 0 !important; + font-size: .85em !important; +} diff --git a/template.c b/template.c @@ -84,6 +84,7 @@ typedef struct { static void template_menu(const mobley_t *ctx, server_req_t *r) { + string_array_t *node; menu_entry_t global_menu[] = { { "Git", "/" }, { "Builds", "/~builds" }, @@ -134,6 +135,28 @@ static void template_menu(const mobley_t *ctx, server_req_t *r) html_raw("%s:%s", ctx->conf.clone_ssh_url, r->repo->name); } html_close("div"); } html_close("li"); + + node = list_first_entry(&r->uri, string_array_t, node); + if (node->val != NULL && strcmp(node->val, "tree") == 0) + { + char path[PATH_MAX] = { 0 }; + char *cur = path; + + html_raw("|"); + + html_open("li") { + list_for_each_entry(node, r->uri.next, node) + { + if (node->val == NULL) + continue; + + cur += snprintf(cur, (unsigned long)(path + sizeof(path)), "/%s", node->val); + + html_raw(" / "); + html("a", node->val, .href = __format("tree%s", path)); + } + } html_close("li"); + } } } html_close("ul"); }