neocgit

a more 'modern' version of cgit
Log | Files | Refs | Submodules | README | LICENSE | git clone https://git.ne02ptzero.me/git/neocgit

commit 88d5499be6e26f1b803935dac31aee1572736b91
parent 575258adc88471fe7ba267b16f2246a34cc1f77e
Author: Louis Solofrizzo <lsolofrizzo@online.net>
Date:   Wed,  3 Oct 2018 12:10:50 +0200

Issues: Now supporting issues / bugs with git-bug

neocgit now supports issues / bugs / tickets with git-bug[1]
This is the first (major) patch on this integration, and more will
follow with mainly formatting stuff, and maybe some work on more
integrations like git-appraise. For this commit though, issue listing is
coded, aswell as issue specific urls.

[1] https://github.com/MichaelMure/git-bug

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

Diffstat:
M.gitmodules | 3+++
AcJSON | 1+
Mcgit.css | 207+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcgit.mk | 4++++
Mcmd.c | 8++++++++
Alist_helpers.h | 161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mui-blob.c | 93+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Mui-blob.h | 1+
Aui-issues.c | 613+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aui-issues.h | 6++++++
Mui-log.c | 24++++++++++++++++++++++++
Mui-log.h | 1+
Mui-shared.c | 3++-
13 files changed, 1086 insertions(+), 39 deletions(-)

diff --git a/.gitmodules b/.gitmodules @@ -1,3 +1,6 @@ [submodule "git"] url = https://git.kernel.org/pub/scm/git/git.git path = git +[submodule "cJSON"] + path = cJSON + url = https://github.com/DaveGamble/cJSON.git diff --git a/cJSON b/cJSON @@ -0,0 +1 @@ +Subproject commit 08103f048e5f54c8f60aaefda16761faf37114f2 diff --git a/cgit.css b/cgit.css @@ -743,6 +743,213 @@ option:not(:checked) { width: calc(100% - 100px); } +.issue-header strong { + margin-right: 20px; +} + +.issue-header span { + color: #292626cc; + margin-right: 10px; +} + +.issue-header span i { + margin-right: 5px !important; +} + +.issue-list { + list-style: none; + padding: 0; + margin: 0; + width: 100%; +} + +.issue-list .issue-entry { + padding: 0; + margin: 0; + border-bottom: 1px solid #eee; + width: 100%; + padding-bottom: 10px; + padding-top: 10px; +} + +.issue-list .issue-entry:hover { + cursor: pointer; + background: #fafafa; + border-bottom: 1px solid #7575ea4d; +} + +.issue-list .issue-entry ul { + list-style: none; + padding: 0; + margin: 0; + width: 100%; + height: 100%; +} + +.issue-list .issue-entry ul li { + display: inline-block; + margin: 0; +} + +.issue-list .issue-entry ul .issue-icon { + width: 5%; + text-align: center; + font-size: 2em; + height: 100%; + vertical-align: top; + padding-top: 5px; +} + +.issue-list .issue-entry ul .issue-comment { + width: calc(5% - 5px); + text-align: center; + vertical-align: top; + color: #000c; + padding-right: 5px; +} + +.issue-list .issue-entry ul .issue-opened { + color: #349c34; +} + +.issue-list .issue-entry ul .issue-closed { + color: #b42727; +} + +.issue-list .issue-entry ul .issue-infos { + width: 90%; +} + +.issue-list .issue-entry ul .issue-infos ul li { + width: 100%; +} + +.issue-list .issue-entry ul .issue-infos ul .issue-title { + padding-bottom: 5px; +} + +.issue-list .issue-entry ul .issue-infos ul .issue-title .main-title { + font-size: 1.3em; + font-weight: bold; + margin-right: 10px; +} + +.label { + margin-right: 5px; + padding: 2px 10px; + background: #919adb; + border-radius: 3px; + font-size: 0.9em; +} + +.issue-list .issue-entry ul .issue-infos ul .issue-desc { + color: #0009; +} + +.issue-list .issue-entry ul .issue-infos ul .issue-desc a { + color: black !important; +} + +.issue-main-title { + padding-bottom: 20px; + border-bottom: 1px solid #eee; + margin-bottom: 20px; +} +.issue-main-title .issue-status { + padding: 5px 10px; + margin-right: 10px; + color: white; + font-weight: bold; + border-radius: 3px; +} + +.issue-main-title .issue-opened { + background: #115f11; +} + +.issue-main-title .issue-closed { + background: #bd1e1e; +} + +.issue-main-title .issue-status i { + margin-right: 5px; +} + +.issue-main-title .label { + margin-top: 10px; + font-size: 1em; + font-weight: bold; + color: white; + background: #2f397d; +} + +.issue-comment { + list-style: none; + padding: 0; + margin: 0; +} + +.issue-comment li { + display: inline-block; +} + +.issue-comment .issue-comment-picture { + width: 3%; + text-align: center; +} + +.issue-comment .issue-comment-content { + width: 97%; +} + +.issue-content { + margin-bottom: 0px; + margin-top: 0px; +} + +.issue-separator { + border-left: 3px solid #eee; + width: 1px; + margin: 0; + height: 15px; + margin-left: 5%; +} + +.issue-change { + margin-left: 5%; + margin-top: 9px; +} + +.issue-change img { + vertical-align: middle; + margin-right: 5px; +} + +.issue-change .icon-status { + padding: 5px 5px; + border-radius: 20px; + color: white; + border: 3px solid #eee; + margin-left: -14px; + margin-right: 10px; +} + +.icon-rename { + background: #5f5fd8; +} + +.icon-label { + background: #290d83; +} + +.icon-close { + background: #bd1e1e; +} + +.icon-open { + background: #57a194; +} + div#cgit div.path { margin: 0px; padding: 5px 2em 2px 2em; diff --git a/cgit.mk b/cgit.mk @@ -96,6 +96,9 @@ CGIT_OBJ_NAMES += ui-stats.o CGIT_OBJ_NAMES += ui-summary.o CGIT_OBJ_NAMES += ui-tag.o CGIT_OBJ_NAMES += ui-tree.o +CGIT_OBJ_NAMES += ui-issues.o +CGIT_OBJ_NAMES += cJSON/cJSON.o +CGIT_OBJ_NAMES += cJSON/cJSON_Utils.o CGIT_OBJS := $(addprefix $(CGIT_PREFIX),$(CGIT_OBJ_NAMES)) @@ -120,6 +123,7 @@ endif $(CGIT_PREFIX).depend: @mkdir -p $@ + @mkdir -p ../cJSON/.depend $(CGIT_PREFIX)CGIT-CFLAGS: FORCE @FLAGS='$(subst ','\'',$(CGIT_CFLAGS))'; \ diff --git a/cmd.c b/cmd.c @@ -26,6 +26,7 @@ #include "ui-summary.h" #include "ui-tag.h" #include "ui-tree.h" +#include "ui-issues.h" static void HEAD_fn(void) { @@ -164,6 +165,12 @@ static void tree_fn(void) cgit_print_tree(ctx.qry.sha1, ctx.qry.path, true); } +static void issues_fn(void) +{ + cgit_print_issues(); + return ; +} + #define def_cmd(name, want_repo, want_vpath, is_clone) \ {#name, name##_fn, want_repo, want_vpath, is_clone} @@ -191,6 +198,7 @@ struct cgit_cmd *cgit_get_cmd(void) def_cmd(summary, 1, 0, 0), def_cmd(tag, 1, 0, 0), def_cmd(tree, 1, 1, 0), + def_cmd(issues, 1, 0, 0) }; int i; diff --git a/list_helpers.h b/list_helpers.h @@ -0,0 +1,161 @@ +#ifndef LIST_HELPERS_H +# define LIST_HELPERS_H + +# include "git/list.h" + +# define COUNT_OF(x) (sizeof(x) / sizeof(x[0])) +# define unlikely(x) __builtin_expect((x), 0) + +# define list_for_each_entry(pos, head, member) \ + for (pos = list_entry((head)->next, typeof(*pos), member); \ + &pos->member != (head); \ + pos = list_entry(pos->member.next, typeof(*pos), member)) + +/*! + * \brief Count the number of elements in a list + * + * \param[in] head Head of the list + * + * \return Size of the list + */ +static inline size_t list_count(const struct list_head *head) +{ + struct list_head *iter; + size_t i = 0; + + list_for_each(iter, head) + i++; + + return i; +} + +#define MAX_LIST_LENGTH_BITS 20 + +/** + * Returns a list organized in an intermediate format suited + * to chaining of merge() calls: null-terminated, no reserved or + * sentinel head node, "prev" links not maintained. + */ +static inline struct list_head *merge(void *priv, + int (*cmp)(void *priv, struct list_head *a, struct list_head *b), + struct list_head *a, struct list_head *b) +{ + struct list_head head, *tail = &head; + + while (a && b) + { + /* if equal, take 'a' -- important for sort stability */ + if ((*cmp)(priv, a, b) <= 0) + { + tail->next = a; + a = a->next; + } + else + { + tail->next = b; + b = b->next; + } + tail = tail->next; + } + tail->next = a?:b; + + return head.next; +} + +/** + * Combine final list merge with restoration of standard doubly-linked + * list structure. This approach duplicates code from merge(), but + * runs faster than the tidier alternatives of either a separate final + * prev-link restoration pass, or maintaining the prev links + * throughout. + */ +static inline void merge_and_restore_back_links(void *priv, + int (*cmp)(void *priv, struct list_head *a, struct list_head *b), + struct list_head *head, struct list_head *a, struct list_head *b) +{ + struct list_head *tail = head; + uint8_t count = 0; + + while (a && b) + { + /* if equal, take 'a' -- important for sort stability */ + if ((*cmp)(priv, a, b) <= 0) + { + tail->next = a; + a->prev = tail; + a = a->next; + } + else + { + tail->next = b; + b->prev = tail; + b = b->next; + } + tail = tail->next; + } + tail->next = a ? : b; + + do + { + /* + * In worst cases this loop may run many iterations. + * Continue callbacks to the client even though no + * element comparison is needed, so the client's cmp() + * routine can invoke cond_resched() periodically. + */ + if (unlikely(!(++count))) + (*cmp)(priv, tail->next, tail->next); + + tail->next->prev = tail; + tail = tail->next; + + } while (tail->next); + + tail->next = head; + head->prev = tail; +} + +static inline void list_sort(void *priv, struct list_head *head, int (*cmp)(void *priv, struct list_head *a, struct list_head *b)) +{ + struct list_head *part[MAX_LIST_LENGTH_BITS + 1] = { 0 }; /* sorted partial lists last slot is a sentinel */ + int lev; /* index into part[] */ + int max_lev = 0; + struct list_head *list; + + if (list_empty(head)) + return; + + head->prev->next = NULL; + list = head->next; + + while (list) + { + struct list_head *cur = list; + list = list->next; + cur->next = NULL; + + for (lev = 0; part[lev]; lev++) + { + cur = merge(priv, cmp, part[lev], cur); + part[lev] = NULL; + } + + if (lev > max_lev) + { + if (unlikely(lev >= (int)(COUNT_OF(part) - 1))) + lev--; + + max_lev = lev; + } + part[lev] = cur; + } + + for (lev = 0; lev < max_lev; lev++) + { + if (part[lev]) + list = merge(priv, cmp, part[lev], list); + } + + merge_and_restore_back_links(priv, cmp, head, part[max_lev], list); +} +#endif /* LIST_HELPERS_H */ diff --git a/ui-blob.c b/ui-blob.c @@ -63,47 +63,64 @@ done: return walk_tree_ctx.found_path; } +char *cgit_get_file(char *path, const char *head, int file_only) +{ + struct object_id oid; + enum object_type type; + char *buf = NULL; + unsigned long size; + struct commit *commit; + struct pathspec_item path_items = { + .match = path, + .len = strlen(path) + }; + struct pathspec paths = { + .nr = 1, + .items = &path_items + }; + struct walk_tree_context walk_tree_ctx = { + .match_path = path, + .matched_oid = &oid, + .found_path = 0, + .file_only = file_only + }; + + if (get_oid(head, &oid)) + return NULL; + + type = oid_object_info(the_repository, &oid, &size); + + if (type == OBJ_COMMIT) { + commit = lookup_commit_reference(&oid); + read_tree_recursive(commit->maybe_tree, "", 0, 0, &paths, walk_tree, + &walk_tree_ctx); + + if (!walk_tree_ctx.found_path) + return NULL; + + type = oid_object_info(the_repository, &oid, &size); + } + + if (type == OBJ_BAD) + return NULL; + + buf = read_object_file(&oid, &type, &size); + if (!buf) + return NULL; + + buf[size] = '\0'; + return buf; +} + int cgit_print_file(char *path, const char *head, int file_only) { - struct object_id oid; - enum object_type type; - char *buf; - unsigned long size; - struct commit *commit; - struct pathspec_item path_items = { - .match = path, - .len = strlen(path) - }; - struct pathspec paths = { - .nr = 1, - .items = &path_items - }; - struct walk_tree_context walk_tree_ctx = { - .match_path = path, - .matched_oid = &oid, - .found_path = 0, - .file_only = file_only - }; + char *buf = cgit_get_file(path, head, file_only); + if (buf == NULL) + return -1; - if (get_oid(head, &oid)) - return -1; - type = oid_object_info(the_repository, &oid, &size); - if (type == OBJ_COMMIT) { - commit = lookup_commit_reference(&oid); - read_tree_recursive(commit->maybe_tree, "", 0, 0, &paths, walk_tree, &walk_tree_ctx); - if (!walk_tree_ctx.found_path) - return -1; - type = oid_object_info(the_repository, &oid, &size); - } - if (type == OBJ_BAD) - return -1; - buf = read_object_file(&oid, &type, &size); - if (!buf) - return -1; - buf[size] = '\0'; - html_raw(buf, size); - free(buf); - return 0; + html_raw(buf, strlen(buf)); + free(buf); + return 0; } void cgit_print_blob(const char *hex, char *path, const char *head, int file_only) diff --git a/ui-blob.h b/ui-blob.h @@ -4,5 +4,6 @@ extern int cgit_ref_path_exists(const char *path, const char *ref, int file_only); extern int cgit_print_file(char *path, const char *head, int file_only); extern void cgit_print_blob(const char *hex, char *path, const char *head, int file_only); +char *cgit_get_file(char *path, const char *head, int file_only); #endif /* UI_BLOB_H */ diff --git a/ui-issues.c b/ui-issues.c @@ -0,0 +1,613 @@ +#include "cgit.h" +#include "html.h" +#include "ui-shared.h" +#include "ui-blob.h" +#include "ui-log.h" +#include "cJSON/cJSON.h" +#include "list_helpers.h" + +typedef enum { + BUG_CREATE = 1, + BUG_RENAME = 2, + BUG_COMMENT = 3, + BUG_STATUS = 4, + BUG_LABEL = 5 +} gitbug_type_t; + +typedef struct { + char *name; + struct list_head node; +} cgit_gitbug_label_t; + +typedef struct { + gitbug_type_t type; + char *author; + char *author_email; + time_t timestamp; + union { + char *body; + int status; + struct { + struct list_head added; + struct list_head removed; + } labels; + struct { + char *old; + char *new; + } rename; + }; + struct list_head node; +} cgit_gitbug_entry_t; + +typedef struct { + char *hash; + char *title; + bool opened; + int comments; + cgit_gitbug_entry_t *body; + struct list_head content; + struct list_head labels; + struct list_head node; +} cgit_gitbug_issue_t; + +static cgit_gitbug_entry_t *gitbug_entry_new(cJSON *entry, bool get_message) +{ + cgit_gitbug_entry_t *ret = NULL; + cJSON *author; + cJSON *timestamp; + cJSON *message; + cJSON *author_name; + cJSON *author_email; + + author = cJSON_GetObjectItemCaseSensitive(entry, "author"); + timestamp = cJSON_GetObjectItemCaseSensitive(entry, "timestamp"); + author_name = cJSON_GetObjectItemCaseSensitive(author, "name"); + author_email = cJSON_GetObjectItemCaseSensitive(author, "email"); + + ret = calloc(1, sizeof(*ret)); + if (ret == NULL) + return NULL; + + ret->author = strdup(author_name->valuestring); + ret->author_email = strdup(author_email->valuestring); + ret->timestamp = timestamp->valuedouble; + + if (get_message) + { + message = cJSON_GetObjectItemCaseSensitive(entry, "message"); + ret->body = strdup(message->valuestring); + } + return ret; +} + +static bool gitbug_parse_creation(cgit_gitbug_issue_t *ptr, cJSON *entry) +{ + cgit_gitbug_entry_t *com; + cJSON *title; + + com = gitbug_entry_new(entry, true); + if (com == NULL) + return false; + + title = cJSON_GetObjectItemCaseSensitive(entry, "title"); + ptr->title = strdup(title->valuestring); + ptr->body = com; + com->type = BUG_CREATE; + + return true; +} + +static bool gitbug_parse_rename(cgit_gitbug_issue_t *ptr, cJSON *entry) +{ + cgit_gitbug_entry_t *com; + cJSON *title; + cJSON *was; + + com = gitbug_entry_new(entry, false); + if (com == NULL) + return false; + + title = cJSON_GetObjectItemCaseSensitive(entry, "title"); + com->rename.new = strdup(title->valuestring); + + was = cJSON_GetObjectItemCaseSensitive(entry, "was"); + com->rename.old = strdup(was->valuestring); + com->type = BUG_RENAME; + list_add_tail(&com->node, &ptr->content); + + return true; +} + +static bool gitbug_parse_comment(cgit_gitbug_issue_t *ptr, cJSON *entry) +{ + cgit_gitbug_entry_t *com; + + com = gitbug_entry_new(entry, true); + if (com == NULL) + return false; + + com->type = BUG_COMMENT; + list_add_tail(&com->node, &ptr->content); + return true; +} + +static bool gitbug_parse_status(cgit_gitbug_issue_t *ptr, cJSON *entry) +{ + cgit_gitbug_entry_t *com; + cJSON *status; + + com = gitbug_entry_new(entry, false); + if (com == NULL) + return false; + + com->type = BUG_STATUS; + status = cJSON_GetObjectItemCaseSensitive(entry, "status"); + com->status = status->valuedouble; + list_add_tail(&com->node, &ptr->content); + return true; + +} + +static bool gitbug_parse_label(cgit_gitbug_issue_t *ptr, cJSON *entry) +{ + cgit_gitbug_entry_t *com; + cJSON *added; + cJSON *removed; + cJSON *iter; + cgit_gitbug_label_t *tmp; + + com = gitbug_entry_new(entry, false); + if (com == NULL) + return false; + + INIT_LIST_HEAD(&com->labels.added); + INIT_LIST_HEAD(&com->labels.removed); + + added = cJSON_GetObjectItemCaseSensitive(entry, "added"); + removed = cJSON_GetObjectItemCaseSensitive(entry, "removed"); + + cJSON_ArrayForEach(iter, added) + { + tmp = calloc(1, sizeof(*tmp)); + if (tmp == NULL) + goto fail; + + tmp->name = strdup(iter->valuestring); + list_add_tail(&tmp->node, &com->labels.added); + } + + cJSON_ArrayForEach(iter, removed) + { + tmp = calloc(1, sizeof(*tmp)); + if (tmp == NULL) + goto fail; + + tmp->name = strdup(iter->valuestring); + list_add_tail(&tmp->node, &com->labels.removed); + } + + com->type = BUG_LABEL; + list_add_tail(&com->node, &ptr->content); + + return true; +fail: + return false; +} + +static int gitbug_entry_cmp(void *priv, struct list_head *a, struct list_head *b) +{ + cgit_gitbug_entry_t *one = list_entry(a, typeof(*one), node); + cgit_gitbug_entry_t *two = list_entry(b, typeof(*one), node); + + if (one->timestamp > two->timestamp) + return 1; + else if (one->timestamp == two->timestamp) + return 0; + return -1; +} + +static const bool (*parse_callbacks[])(cgit_gitbug_issue_t *, cJSON *) = { + [BUG_CREATE] = gitbug_parse_creation, + [BUG_RENAME] = gitbug_parse_rename, + [BUG_COMMENT] = gitbug_parse_comment, + [BUG_STATUS] = gitbug_parse_status, + [BUG_LABEL] = gitbug_parse_label +}; + +static bool parse_bug_json(cgit_gitbug_issue_t *ptr, cJSON *entry) +{ + bool ret = false; + cJSON *obj_type; + int type; + + obj_type = cJSON_GetObjectItemCaseSensitive(entry, "type"); + if (!cJSON_IsNumber(obj_type)) + goto end; + + type = obj_type->valuedouble; + for (size_t i = 0; i < sizeof(parse_callbacks) / sizeof(parse_callbacks[0]); i++) + { + if (i == type && parse_callbacks[i] != NULL) + { + ret = parse_callbacks[i](ptr, entry); + goto end; + } + } + + printf("Unknown type %d\n", type); + assert(!"Unhandled type!"); + +end: + return ret; +} + +static bool handle_commit_bug(cgit_gitbug_issue_t *ptr, struct commit *commit) +{ + char *hex = oid_to_hex(&commit->object.oid); + char *buf = NULL; + cJSON *entry; + cJSON *ops; + cJSON *details; + bool ret = false; + + buf = cgit_get_file("ops", hex, 1); + if (buf == NULL) + return false; + + entry = cJSON_Parse(buf); + if (entry == NULL) + goto end; + + ops = cJSON_GetObjectItemCaseSensitive(entry, "ops"); + if (ops == NULL) + goto end; + + for (int i = 0; i < cJSON_GetArraySize(ops); i++) + { + details = cJSON_GetArrayItem(ops, i); + if (details == NULL) + goto end; + + if (!parse_bug_json(ptr, details)) + goto end; + } + + ret = true; +end: + free(buf); + if (entry != NULL) + cJSON_Delete(entry); + return ret; +} + +static void rebuild_bug_status(cgit_gitbug_issue_t *ptr) +{ + cgit_gitbug_entry_t *iter; + cgit_gitbug_label_t *tmp, *label, *add; + + ptr->opened = true; + list_for_each_entry(iter, &ptr->content, node) + { + if (iter->type == BUG_STATUS) + { + if (iter->status == 2) + ptr->opened = false; + else + ptr->opened = true; + } + else if (iter->type == BUG_RENAME) + { + free(ptr->title); + ptr->title = strdup(iter->rename.new); + } + else if (iter->type == BUG_COMMENT) + { + ptr->comments++; + } + else if (iter->type == BUG_LABEL) + { + list_for_each_entry(tmp, &iter->labels.added, node) + { + add = calloc(1, sizeof(*add)); + memcpy(add, tmp, sizeof(*add)); + list_add_tail(&add->node, &ptr->labels); + } + + list_for_each_entry(tmp, &iter->labels.removed, node) + { + list_for_each_entry(label, &ptr->labels, node) + { + if (strcmp(label->name, tmp->name) == 0) + { + list_del(&label->node); + free(label); + } + } + } + } + } +} + +static cgit_gitbug_issue_t *get_issue_info(const char *hash) +{ + cgit_gitbug_issue_t *ptr = NULL; + struct rev_info *rev; + struct commit *commit = NULL; + char bug_ref[1024] = { 0 }; + + ptr = calloc(1, sizeof(*ptr)); + if (ptr == NULL) + return NULL; + INIT_LIST_HEAD(&ptr->content); + INIT_LIST_HEAD(&ptr->labels); + + ptr->hash = strdup(hash); + snprintf(bug_ref, sizeof(bug_ref), "refs/bugs/%s", hash); + + rev = cgit_get_commit_list_from_ref(bug_ref); + if (rev == NULL) + goto fail; + + while ((commit = get_revision(rev))) + { + if (!handle_commit_bug(ptr, commit)) + { + htmlf("Could not parse commit bug<br />"); + goto fail; + } + } + + list_sort(NULL, &ptr->content, &gitbug_entry_cmp); + rebuild_bug_status(ptr); + free(rev); + return ptr; + +fail: + free(ptr); + return NULL; +} + +static void get_issue_stats(struct list_head *head, int *opened, int *closed) +{ + cgit_gitbug_issue_t *tmp; + + list_for_each_entry(tmp, head, node) + { + if (tmp->opened) + (*opened)++; + else + (*closed)++; + } +} + +static void print_issue_list(const char *path) +{ + DIR *fd = opendir(path); + struct dirent *tmp; + cgit_gitbug_issue_t *issue; + cgit_gitbug_label_t *label; + struct list_head issues; + int opened = 0, closed = 0; + + if (fd == NULL) + { + htmlf("<center><h3>No issues :)</h3></center>"); + return ; + } + + INIT_LIST_HEAD(&issues); + + while ((tmp = readdir(fd))) + { + if (tmp->d_name[0] == '.') + continue ; + + issue = get_issue_info(tmp->d_name); + list_add_tail(&issue->node, &issues); + } + closedir(fd); + get_issue_stats(&issues, &opened, &closed); + + html("<div class='repo-file'>"); + _html("<div class='header issue-header'>") { + html("<strong>Issues</strong>"); + htmlf("<span><i class='fa fa-exclamation-circle'></i>%d Open</span>", opened); + htmlf("<span><i class='fa fa-check'></i>%d Closed</span>", closed); + } _html("</div>"); + html("<div class='content' style='padding: 0'>"); + html("<ul class='issue-list'>"); + + list_for_each_entry(issue, &issues, node) + { + htmlf("<li class='issue-entry' onclick=\"location.href='" \ + "/%s/issues/%s'\">", ctx.repo->name, issue->hash); { + html("<ul>"); { + html("<li class='issue-icon'>"); { + htmlf("<div><i class='fa fa-exclamation-circle %s'></i></div>", + issue->opened ? "issue-opened" : "issue-closed"); + } html("</li>"); + + html("<li class='issue-infos'><ul>"); { + html("<li class='issue-title'>"); { + htmlf("<span class='main-title'>%s</span>", issue->title); + list_for_each_entry(label, &issue->labels, node) + { + htmlf("<span class='label'>%s</span>", label->name); + } + } html("</li>"); + html("<li class='issue-desc'>"); { + char *nice_name = strdup(issue->hash); + nice_name[9] = 0; + + htmlf("<a href='/%s/issues/%s'>%s</a> opened ", + ctx.repo->name, issue->hash, nice_name); + cgit_print_age(issue->body->timestamp, -1, -1); + htmlf(" ago by %s", issue->body->author); + free(nice_name); + + } html("</li>"); + } html("</ul></li>"); + html("<li class='issue-comment'>"); { + htmlf("<i class='fa fa-comment-alt'></i> %d", issue->comments); + } html("</li>"); + } html("</ul>"); + } html("</li>"); + } + html("</ul></div></div>"); +} + +static void dump_entry(const cgit_gitbug_entry_t *entry) +{ + if (entry->type == BUG_COMMENT || entry->type == BUG_CREATE) + { + html("<ul class='issue-comment'>"); + html("<li class='issue-comment-picture'>"); + gravatar_img(entry->author_email, 40); + html("</li>"); + + html("<li class='issue-comment-content'>"); + html("<div class='repo-file issue-content'>"); + _html("<div class='header'>") { + htmlf("<strong>%s</strong> commented ", entry->author); + cgit_print_age(entry->timestamp, -1, -1); + html(" ago"); + } _html("</div>"); + htmlf("<div class='content' style='padding: 20px'>%s</div>", entry->body); + html("</div>"); + html("</li>"); + html("</ul>"); + } + else + { + html("<div class='issue-change'>"); + if (entry->type == BUG_RENAME) + html("<span class='icon-status icon-rename'><i class='fa fa-fw fa-pen'></i></span>"); + else if (entry->type == BUG_LABEL) + html("<span class='icon-status icon-label'><i class='fa fa-fw fa-tag'></i></span>"); + else if (entry->type == BUG_STATUS) + { + if (entry->status == 2) + html("<span class='icon-status icon-close'><i class='fa fa-fw fa-ban'></i></span>"); + else + html("<span class='icon-status icon-open'><i class='fa fa-fw fa-exclamation-circle'></i></span>"); + } + + gravatar_img(entry->author_email, 20); + + if (entry->type == BUG_RENAME) + { + htmlf("<strong>%s</strong> renamed the issue from" \ + " '<strong>%s</strong>' to '<strong>%s</strong>' ", + entry->author, entry->rename.old, entry->rename.new); + } + else if (entry->type == BUG_LABEL) + { + const struct list_head *head; + cgit_gitbug_label_t *label; + + htmlf("<strong>%s</strong> ", entry->author); + if (list_count(&entry->labels.added) > 0) + { + html(" added "); + head = &entry->labels.added; + } + else + { + html(" removed "); + head = &entry->labels.removed; + } + html("the label "); + list_for_each_entry(label, head, node) + { + htmlf("<span class='label'>%s</span>", label->name); + } + } + else if (entry->type == BUG_STATUS) + { + htmlf("<strong>%s</strong> ", entry->author); + if (entry->status == 2) + html("closed the issue "); + else + html("re-opened the issue "); + } + + cgit_print_age(entry->timestamp, -1, -1); + html(" ago"); + html("</div>"); + } +} + +static void print_single_issue(char *hash) +{ + cgit_gitbug_issue_t *issue = get_issue_info(hash); + cgit_gitbug_label_t *label = NULL; + cgit_gitbug_entry_t *entry = NULL; + char *nice_name; + + html("<div class='repo-file'>"); + _html("<div class='header'>") { + nice_name = strdup(hash); + nice_name[9] = 0; + htmlf("<strong>Issue %s</strong>", nice_name); + free(issue); + } _html("</div>"); + html("<div class='content' style='padding-top: 0'>"); + + _html("<div class='issue-main-title'>") { + htmlf("<h1>%s</h1>", issue->title); + if (issue->opened) + { + html("<span class='issue-status issue-opened'>" \ + "<i class='fa fa-exclamation-circle'></i>Open</span>"); + } + else + { + html("<span class='issue-status issue-closed'>" \ + "<i class='fa fa-check'></i>Closed</span>"); + } + + htmlf("<strong>%s</strong> opened this issue ", issue->body->author); + cgit_print_age(issue->body->timestamp, -1, -1); + htmlf(" ago · %d comments", issue->comments); + + if (list_count(&issue->labels) > 0) + { + htmlf(" · "); + list_for_each_entry(label, &issue->labels, node) + htmlf("<span class='label'>%s</span>", label->name); + } + } _html("</div>"); + + dump_entry(issue->body); + html("<div class='issue-separator'></div>"); + list_for_each_entry(entry, &issue->content, node) + { + dump_entry(entry); + if (entry->node.next != &issue->content) + { + html("<div class='issue-separator'></div>"); + } + } + + html("</div>"); + html("</div>"); + + free(issue); +} + +void cgit_print_issues(void) +{ + char path[PATH_MAX] = { 0 }; + + cgit_print_layout_start(); + if (ctx.qry.path == NULL) + { + snprintf(path, sizeof(path), "%s/refs/bugs", ctx.repo->path); + print_issue_list(path); + } + else + { + print_single_issue(ctx.qry.path); + } + cgit_print_layout_end(); +} diff --git a/ui-issues.h b/ui-issues.h @@ -0,0 +1,6 @@ +#ifndef UI_ISSUES +# define UI_ISSUES + +void cgit_print_issues(void); + +#endif /* UI_ISSUES */ diff --git a/ui-log.c b/ui-log.c @@ -375,6 +375,30 @@ struct commit *cgit_get_last_commit_from_path(const char *path) return commit; } +struct rev_info *cgit_get_commit_list_from_ref(const char *ref) +{ + struct rev_info *rev; + struct commit *commit = NULL; + struct argv_array rev_argv = ARGV_ARRAY_INIT; + + rev = calloc(1, sizeof(*rev)); + if (rev == NULL) + return NULL; + + argv_array_push(&rev_argv, "log_rev_setup"); + argv_array_push(&rev_argv, ref); + + init_revisions(rev, NULL); + + setup_revisions(rev_argv.argc, rev_argv.argv, rev, NULL); + load_ref_decorations(NULL, DECORATE_FULL_REFS); + + if (prepare_revision_walk(rev) != 0) + die("Walk error"); + + return rev; +} + static char *next_token(char **src) { char *result; diff --git a/ui-log.h b/ui-log.h @@ -7,5 +7,6 @@ extern void cgit_print_log(const char *tip, int ofs, int cnt, char *grep, extern void show_commit_decorations(struct commit *commit); extern int cgit_count_commits(void); extern struct commit *cgit_get_last_commit_from_path(const char *path); +extern struct rev_info *cgit_get_commit_list_from_ref(const char *path); #endif /* UI_LOG_H */ diff --git a/ui-shared.c b/ui-shared.c @@ -981,6 +981,7 @@ static void print_header(void) HTML_LINK("<li><a href='" URL("log") "'>Commits (%d)</a></li>", cgit_count_commits()); HTML_LINK("<li><a href='" URL("refs") "'>Branches (%d)</a></li>", cgit_count_branches()); htmlf("<li><a href='#'>Releases (%d)</a></li>", cgit_count_tags()); + HTML_LINK("<li><a href='" URL("issues") "'>Issues</a></li>"); } else { @@ -1008,7 +1009,7 @@ void cgit_print_pageheader(void) cgit_add_clone_urls(NULL); } _html("</div>"); - if (strcmp("commit", ctx.qry.page) == 0) + if (strcmp("commit", ctx.qry.page) == 0 || strcmp("issues", ctx.qry.page) == 0) goto end; _html("<div class='summary-branches'>") {