mobley

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

commit eabe8d542e7c1db9e9ff88328ef7ce640f8ccdf5
parent 3863febb166dce364a599d6d806ff06df7a5b7c4
Author: Louis Solofrizzo <lsolofrizzo@online.net>
Date:   Wed, 19 Dec 2018 22:53:42 +0100

Add tree list with last commits and icons on files:

This is a WIP work. It basically works, but it is _very_ slow. We're
talking about several minutes of the processing for something like the
linux tree. I'll need to patch libgit2, because they are doing way too
much commit parsing [1].
The API of repository walk is a mess right now, since I designed it to
get all the commits in on rev_walk, but the matchspec libgit2 API is not
meant to do that, so I've reverted it by a revwalk by file.

[1] https://github.com/libgit2/libgit2/issues/3507

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

 ________________________________________
/ Ok, I'm just uploading the new version \
| of the kernel, v1.3.33, also known as  |
| "the buggiest kernel ever". -- Linus   |
\ Torvalds                               /
 ----------------------------------------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

Diffstat:
MCMakeLists.txt | 2++
Afile_format.c | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afile_format.h | 29+++++++++++++++++++++++++++++
Mhtml.c | 15+++++++++++++++
Mhtml.h | 6++++++
Mindex.c | 4++--
Mrepository.c | 171+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mrepository.h | 43++++++++++++++++++++++++++++++++++++++++++-
Mrepository_handler.c | 242+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mserver.h | 4++++
Mstyle/main.css | 15+++++++++++++++
Mtemplate.c | 9++++++++-
12 files changed, 631 insertions(+), 8 deletions(-)

diff --git a/CMakeLists.txt b/CMakeLists.txt @@ -26,6 +26,7 @@ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_FORTIFY_SOURCE=2") # Run-time buffer over set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Werror=format-security") # Reject potentially unsafe format string arguents set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wunreachable-code") # Warn if the compiler detects that code will never be executed set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wswitch-enum") # Force a switch on an enum to have all the cases possible +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-missing-field-initializers") # Force a switch on an enum to have all the cases possible set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -49,6 +50,7 @@ add_executable(${MOBLEY_NAME} main.c config.c repository.c repository_handler.c + file_format.c ) install(TARGETS ${MOBLEY_NAME} DESTINATION bin) diff --git a/file_format.c b/file_format.c @@ -0,0 +1,99 @@ +/** + * 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_format.h> +#include <utils.h> + +#include <string.h> + +static bool format_match(const char **array, size_t size, const char *filename) +{ + int format_cur; + + for (format_cur = strlen(filename); format_cur > 0 && filename[format_cur] != '.'; format_cur--) + ; + + if (format_cur == 0) + return false; + + for (size_t i = 0; i < size; i++) + { + if (strcmp(array[i], filename + format_cur) == 0) + return true; + } + + return false; +} + +bool file_is_code(const char *filename) +{ + const char *formats[] = { + ".c", ".cpp", ".h", ".hpp", ".cxx", ".cc", + ".js", ".html", ".css", ".py", ".php" // XXX + }; + + return format_match(formats, COUNT_OF(formats), filename); +} + +bool file_is_audio(const char *filename) +{ + const char *formats[] = { + ".3gp", ".aa", ".aac", ".aax", ".act", ".aiff", ".amr", ".ape", ".au", + ".awb", ".dct", ".dss", ".dvf", ".flac", ".gsm", ".m4a", ".m4b", ".m4p", + ".mmf", ".mp3", ".mpc", ".msv", ".nsf", ".ogg", ".oga", ".mogg", ".opus", + ".ra", ".rm", ".sln", ".tta", ".vox", ".wav", ".wma", ".wv", ".webm" + }; + + return format_match(formats, COUNT_OF(formats), filename); +} + +bool file_is_pdf(const char *filename) +{ + const char *formats[] = { + ".pdf" + }; + + return format_match(formats, COUNT_OF(formats), filename); +} + +bool file_is_video(const char *filename) +{ + const char *formats[] = { + ".yuv", ".wmv", ".vob", ".svi", ".roq", ".rmvb", ".mpg", ".mpeg", ".m2v", + ".mp2", ".mpe", ".mpv", ".mp4", ".m4p", ".m4v", ".mov", ".qt", ".mng", + ".mkv", ".m4v", ".gifv", ".gif", ".flv", ".drc", ".avi", ".asf", ".amv", + ".3gp", ".3g2" + }; + + return format_match(formats, COUNT_OF(formats), filename); +} + +bool file_is_archive(const char *filename) +{ + const char *formats[] = { + ".tar", ".gz", ".zip", ".ar", ".a" + }; + + return format_match(formats, COUNT_OF(formats), filename); +} + +bool file_is_image(const char *filename) +{ + const char *formats[] = { + ".png", ".jpg", ".jpeg", ".webp", ".svg", ".ai", ".eps" + }; + + return format_match(formats, COUNT_OF(formats), filename); +} diff --git a/file_format.h b/file_format.h @@ -0,0 +1,29 @@ +/** + * 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_FORMAT_H +# define FILE_FORMAT_H + +# include <stdbool.h> +# include <stdlib.h> + +bool file_is_code(const char *filename); +bool file_is_audio(const char *filename); +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); + +#endif /* FILE_FORMAT_H */ diff --git a/html.c b/html.c @@ -70,6 +70,10 @@ void _html(server_req_t *r, const mobley_t *ctx, const char *b, const char *d, evbuffer_add_printf(r->req->buffer_out, " charset=\"%s\"", a->charset); if (a->align != NULL) evbuffer_add_printf(r->req->buffer_out, " align=\"%s\"", a->align); + if (a->colspan != NULL) + evbuffer_add_printf(r->req->buffer_out, " colspan=\"%s\"", a->colspan); + if (a->width != NULL) + evbuffer_add_printf(r->req->buffer_out, " width=\"%s\"", a->width); if (d == NULL) goto closing; @@ -82,3 +86,14 @@ closing: else evbuffer_add_printf(r->req->buffer_out, ">"); } + +char __buffer[2048] = { 0 }; +char *__format(const char *s, ...) +{ + va_list ap; + + va_start(ap, s); + vsnprintf(__buffer, sizeof(__buffer), s, ap); + va_end(ap); + return __buffer; +} diff --git a/html.h b/html.h @@ -32,6 +32,8 @@ typedef struct { const char *charset; const char *id; const char *align; + const char *colspan; + const char *width; bool closing_slash; bool is_static; } html_args_t; @@ -52,4 +54,8 @@ void _html(server_req_t *r, const mobley_t *ctx, html_raw("%s", asctime(__timeinfo)); \ } while (0) + +__PRINTF_FUNCTION(1, 2) +char *__format(const char *s, ...); + #endif /* HTML_H */ diff --git a/index.c b/index.c @@ -45,15 +45,15 @@ bool index_route_handler(const mobley_t *ctx, server_req_t *r) html("td", repo->owner, .align = "right"); html_open("td", .align = "right") { - commit = repository_get_last_commit(repo); + commit = repository_get_last_commit_from_default(repo); if (commit != NULL) { time = git_commit_time(commit); html_git_date(&time); - git_commit_free(commit); } else html_raw("None"); + git_commit_free(commit); } html_close("td"); } html_close("tr"); diff --git a/repository.c b/repository.c @@ -15,17 +15,22 @@ #include <repository.h> -git_commit *repository_get_last_commit(repository_t *repo) +git_commit *repository_get_last_commit_from_default(repository_t *repo) +{ + return repository_get_last_commit(repo, + repo->default_branch != NULL ? repo->default_branch : "master"); +} + +git_commit *repository_get_last_commit(repository_t *repo, const char *ref) { git_commit *ret = NULL; - git_oid oid_parent_commit = { 0 }; + git_oid oid_parent_commit/* = { 0 }*/; char branch[1024] = { 0 }; if (repo->repo == NULL) return NULL; - snprintf(branch, sizeof(branch), "refs/heads/%s", - repo->default_branch != NULL ? repo->default_branch : "master"); + snprintf(branch, sizeof(branch), "refs/heads/%s", ref); if (git_reference_name_to_id(&oid_parent_commit, repo->repo, branch) == 0) if (git_commit_lookup(&ret, repo->repo, &oid_parent_commit) == 0) @@ -33,3 +38,161 @@ git_commit *repository_get_last_commit(repository_t *repo) 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) +{ + git_commit *parent; + git_tree *a, *b; + git_diff *diff; + int ndeltas; + + + git_commit_parent(&parent, commit, (size_t)i); + git_commit_tree(&a, parent); + git_commit_tree(&b, commit); + + git_diff_tree_to_tree(&diff, git_commit_owner(commit), a, b, opts); + + ndeltas = (int)git_diff_num_deltas(diff); + + git_diff_free(diff); + git_tree_free(a); + git_tree_free(b); + git_commit_free(parent); + + return ndeltas > 0; +} + +bool repository_get_log_from_paths(repository_t *repo, const git_oid *oid, const char *path, list_head_t *paths, size_t max) +{ + git_revwalk *walk = NULL; + git_oid tmp; + git_tree *tree = NULL; + 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; + int parents, unmatched; + + diffopts.pathspec.strings = (char **)&path; + diffopts.pathspec.count = 1; + + git_revwalk_new(&walk, repo->repo); + sorting = GIT_SORT_TIME | (sorting & GIT_SORT_REVERSE); + git_revwalk_push(walk, oid); + git_revwalk_sorting(walk, sorting); + + git_pathspec_new(&ps, &diffopts.pathspec); + + for (int index = 0; !git_revwalk_next(&tmp, walk); index++) + { + if (git_commit_lookup(&commit, repo->repo, &tmp) != 0) + 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; + else if (parents == 0) + { + if (git_commit_tree(&tree, commit) != 0) + goto fail; + + if (git_pathspec_match_tree(NULL, tree, GIT_PATHSPEC_NO_MATCH_ERROR, ps) != 0) + unmatched = 1; + + git_tree_free(tree); + } + else + { + for (i = 0; (int)i < parents; i++) + { + if (match_with_parent(commit, i, &diffopts, &match_list, ps)) + unmatched--; + } + } + + if (unmatched > 0) + { + git_commit_free(commit); + 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);*/ + /*}*/ + } + +end: + git_pathspec_free(ps); + git_revwalk_free(walk); + return true; +fail: + return false; +} + +bool repository_get_log_from_path_ref(repository_t *repo, const char *path, 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, path, out, 1); + } + + return NULL; +} + +bool repository_get_log_from_path_default(repository_t *repo, const char *path, list_head_t *out) +{ + 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 @@ -73,6 +73,47 @@ static inline int repository_name_cmp(void *u, list_head_t *a, list_head_t *b) return strcmp(one->name, two->name); } -git_commit *repository_get_last_commit(repository_t *repo); +git_commit *repository_get_last_commit(repository_t *repo, const char *ref); +git_commit *repository_get_last_commit_from_default(repository_t *repo); + +typedef struct { + git_commit *commit; + list_head_t node; +} commit_node_t; + +typedef struct { + char *display; + char *path; + git_filemode_t mode; + list_head_t commits; + list_head_t node; +} path_commit_node_t; + +static inline bool path_commit_add_node(list_head_t *head, const char *path) +{ + path_commit_node_t *ptr = calloc(1, sizeof(*ptr)); + + if (ptr == NULL) + return false; + + INIT_LIST_HEAD(&ptr->commits); + ptr->path = strdup(path); + if (ptr->path == NULL) + { + free(ptr); + return false; + } + + list_add_tail(&ptr->node, head); + return true; +} + +bool repository_get_log_from_paths(repository_t *repo, const git_oid *oid, const char *path, list_head_t *paths, size_t max); +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 @@ -14,8 +14,250 @@ */ #include <repository_handler.h> +#include <html.h> +#include <file_format.h> + +static const char *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"; +} + +static void repository_print_tree_items(mobley_t *ctx ,server_req_t *r, list_head_t *head) +{ + path_commit_node_t *node, *tmp; + commit_node_t *commit; + git_time_t time; + + 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("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); + + if (list_count(&new) != 0) + { + char hash[GIT_OID_HEXSZ + 1] = { 0 }; + + commit = list_first_entry(&new, commit_node_t, node); + + git_oid_tostr(hash, sizeof(hash), git_commit_id(commit->commit)); + html_open("td", .class = "commit") { + html("a", git_commit_summary(commit->commit), + .href = __format("log/%s", hash) + ); + } html_close("td"); + + time = git_commit_time(commit->commit); + html_open("td", .align = "right") { + html_git_date(&time); + } html_close("td"); + git_commit_free(commit->commit); + free(commit); + } + else + { + html("td", "None"); + html("td", "None", .align = "right"); + } + } html_close("tr"); + + free(node->display); + free(node->path); + free(node); + } +} + +static int repository_item_cmp(void *priv, list_head_t *a, list_head_t *b) +{ + path_commit_node_t *one = list_entry(a, path_commit_node_t, node); + path_commit_node_t *two = list_entry(b, path_commit_node_t, node); + + if (one->mode & GIT_FILEMODE_TREE && !(two->mode & GIT_FILEMODE_TREE)) + return -1; + else if (!(one->mode & GIT_FILEMODE_TREE) && two->mode & GIT_FILEMODE_TREE) + return 1; + + return strcmp(one->display, two->display); +} + +static int repository_add_tree_item(const char *root, const git_tree_entry *entry, + void *payload) +{ + server_req_t *r = payload; + list_head_t *head = &r->items; + path_commit_node_t *node; + + if (r->path != NULL) + { + if (strcmp(r->path, root) == 0) + goto add; + else if (strncmp(r->path, root, strlen(root)) != 0 && strlen(root) != 0) + return 1; + + return 0; + } + else + { + if (strlen(root) != 0) + return 1; + } + +add: + node = calloc(1, sizeof(*node)); + if (node == NULL) + return 1; + + INIT_LIST_HEAD(&node->commits); + node->display = strdup(git_tree_entry_name(entry)); + node->path = strdup(__format("%s%s", root, git_tree_entry_name(entry))); + node->mode = git_tree_entry_filemode(entry); + list_add_tail(&node->node, head); + + return 0; +} + +static void repository_tree_begin(mobley_t *ctx, server_req_t *r) +{ + html_open("table") { + html_open("thead") { + html_open("tr") { + html("td", "Name", .width = "10%"); + html("td", "Last commit"); + html("td", "Date", .align = "right"); + } html_close("tr"); + } html_close("thead"); + html_open("tbody"); + if (r->path != NULL && strlen(r->path) > 0) + { + html_open("tr") { + html_open("td", .colspan = "3") { + size_t i = strlen(r->path) - 2; + char tmp; + + for (; i > 0 && r->path[i] != '/'; i--) + ; + + tmp = r->path[i]; + r->path[i] = '\0'; + + html("a", "..", .href = __format("tree/%s", r->path)) + + r->path[i] = tmp; + } html_close("td"); + } html_close("tr"); + } + } +} + +static void repository_tree_end(mobley_t *ctx, server_req_t *r) +{ + html_close("tbody"); + html_close("table"); +} + +bool repository_tree_ls(mobley_t *ctx, server_req_t *r) +{ + git_commit *head = repository_get_last_commit(r->repo, r->branch); + git_tree *tree = NULL; + + if (head == NULL) + return false; + + if (git_commit_tree(&tree, head) != 0) + goto fail; + + repository_tree_begin(ctx, r); + + r->ctx = ctx; + INIT_LIST_HEAD(&r->items); + + if (git_tree_walk(tree, GIT_TREEWALK_PRE, &repository_add_tree_item, r) != 0) + goto fail; + + list_sort(NULL, &r->items, &repository_item_cmp); + repository_print_tree_items(ctx, r, &r->items); + + repository_tree_end(ctx, r); + + git_tree_free(tree); + git_commit_free(head); + + return true; + +fail: + git_commit_free(head); + return false; +} + +static bool repository_route_tree_handler(mobley_t *ctx, server_req_t *r) +{ + char path[PATH_MAX] = { 0 }; + char *cur = path; + char *end = path + sizeof(path); + string_array_t *node; + + list_for_each_entry(node, r->uri.next, node) + { + if (node->val == NULL) + continue; + + cur += snprintf(cur, end - cur, "%s/", node->val); + } + + r->path = path; + return repository_tree_ls(ctx, r); +} bool repository_route_handler(mobley_t *ctx, server_req_t *r) { + if (r->repo->default_branch == NULL) + r->branch = "master"; + else + r->branch = r->repo->default_branch; + /* Branch from URL */ + + /* Index */ + if (r->uri.next == &r->uri) + { + repository_tree_ls(ctx, r); + html_nd("hr", .closing_slash = true); + } + else + { + string_array_t *first = list_first_entry(&r->uri, string_array_t, node); + + if (strcmp(first->val, "tree") == 0) + { + if (!repository_route_tree_handler(ctx, r)) + goto fail; + } + } return true; + +fail: + return false; } diff --git a/server.h b/server.h @@ -31,6 +31,10 @@ typedef struct { list_head_t uri; char *route; repository_t *repo; + char *path; + char *branch; + void *ctx; + list_head_t items; } server_req_t; static inline void server_req_dtr(server_req_t *req) diff --git a/style/main.css b/style/main.css @@ -109,6 +109,21 @@ table thead td { font-weight: bold; } +table tbody i { + font-size: .85em; + text-align: left; + margin-right: 4px; + color: #ffffff4d; +} + +table tbody .fa-folder { + color: #b5e853; +} + +table tbody .commit a { + color: #999; +} + .url { padding: 1px 8px; color: white; diff --git a/template.c b/template.c @@ -94,7 +94,7 @@ static void template_menu(const mobley_t *ctx, server_req_t *r) }; menu_entry_t repo_menu[] = { - { "Summary", "summary" }, + { "Summary", "" }, { "Commits", "commits" }, { "Branches", "branches" }, { "Releases", "releases" }, @@ -160,6 +160,13 @@ void template_begin(const mobley_t *ctx, server_req_t *r) ); } + html_nd("link", + .rel = "stylesheet", + .type = "text/css", + .href = "https://use.fontawesome.com/releases/v5.1.0/css/all.css", + .closing_slash = true + ); + } html_close("head"); html_open("body") {