Add a "bugs" page that delegates rendering to a configurable Lua filter. The filter receives the path component as its argument -- empty for the bug list, or a bug id for the detail view -- and is responsible for reading git-bug data out of the repository and producing the HTML.
Keeping the reading logic outside cgit means no git-bug object format knowledge enters the C code, and the format can move without cgit following it. git-bug stores its data as ordinary git objects under refs/bugs/, so a filter using luagit2 can read it without any external database. The page is off by default and returns 403 when enable-bugs is not set, so a repository that happens to carry refs/bugs/ does not start serving a new page after an upgrade. A per-repo bugs-filter, like every other filter override, requires enable-filter-overrides. The bugs page is registered with want_vpath=0, because a bug id in the path is meaningful only to this page; letting it become the virtual path would append it to the log, tree and diff tab links. New cgitrc options: enable-bugs=1 enable the bugs tab (default: 0) bugs-filter=lua:... filter for rendering (global or per-repo) cache-bugs-ttl=15 cache TTL in minutes (default: 15) Assisted-by: LLM [codegen, review] Signed-off-by: Konstantin Ryabitsev <[email protected]> --- cgit.c | 18 +++++++++++ cgit.css | 103 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ cgit.h | 7 +++- cgit.mk | 1 + cgitrc.5.txt | 32 +++++++++++++++++++ cmd.c | 10 ++++++ filter.c | 1 + shared.c | 2 ++ ui-bugs.c | 30 +++++++++++++++++ ui-bugs.h | 6 ++++ ui-shared.c | 12 +++++++ ui-shared.h | 2 ++ 12 files changed, 223 insertions(+), 1 deletion(-) diff --git a/cgit.c b/cgit.c index ca318e8..e64fa5f 100644 --- a/cgit.c +++ b/cgit.c @@ -64,6 +64,8 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu repo->snapshots = ctx.cfg.snapshots & cgit_parse_snapshots_mask(value); else if (!strcmp(name, "enable-blame")) repo->enable_blame = atoi(value); + else if (!strcmp(name, "enable-bugs")) + repo->enable_bugs = atoi(value); else if (!strcmp(name, "enable-commit-graph")) repo->enable_commit_graph = atoi(value); else if (!strcmp(name, "enable-follow-links")) @@ -114,6 +116,8 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu else if (ctx.cfg.enable_filter_overrides) { if (!strcmp(name, "about-filter")) repo->about_filter = cgit_new_filter(value, ABOUT); + else if (!strcmp(name, "bugs-filter")) + repo->bugs_filter = cgit_new_filter(value, BUGS); else if (!strcmp(name, "commit-filter")) repo->commit_filter = cgit_new_filter(value, COMMIT); else if (!strcmp(name, "source-filter")) @@ -185,6 +189,8 @@ static void config_cb(const char *name, const char *value) ctx.cfg.enable_index_owner = atoi(value); else if (!strcmp(name, "enable-blame")) ctx.cfg.enable_blame = atoi(value); + else if (!strcmp(name, "enable-bugs")) + ctx.cfg.enable_bugs = atoi(value); else if (!strcmp(name, "enable-commit-graph")) ctx.cfg.enable_commit_graph = atoi(value); else if (!strcmp(name, "enable-log-filecount")) @@ -219,12 +225,16 @@ static void config_cb(const char *name, const char *value) ctx.cfg.cache_dynamic_ttl = atoi(value); else if (!strcmp(name, "cache-about-ttl")) ctx.cfg.cache_about_ttl = atoi(value); + else if (!strcmp(name, "cache-bugs-ttl")) + ctx.cfg.cache_bugs_ttl = atoi(value); else if (!strcmp(name, "cache-snapshot-ttl")) ctx.cfg.cache_snapshot_ttl = atoi(value); else if (!strcmp(name, "case-sensitive-sort")) ctx.cfg.case_sensitive_sort = atoi(value); else if (!strcmp(name, "about-filter")) ctx.cfg.about_filter = cgit_new_filter(value, ABOUT); + else if (!strcmp(name, "bugs-filter")) + ctx.cfg.bugs_filter = cgit_new_filter(value, BUGS); else if (!strcmp(name, "commit-filter")) ctx.cfg.commit_filter = cgit_new_filter(value, COMMIT); else if (!strcmp(name, "email-filter")) @@ -375,6 +385,7 @@ static void prepare_context(void) ctx.cfg.cache_max_create_time = 5; ctx.cfg.cache_root = CGIT_CACHE_ROOT; ctx.cfg.cache_about_ttl = 15; + ctx.cfg.cache_bugs_ttl = 15; ctx.cfg.cache_snapshot_ttl = 5; ctx.cfg.cache_repo_ttl = 5; ctx.cfg.cache_root_ttl = 5; @@ -816,6 +827,8 @@ static void print_repo(FILE *f, struct cgit_repo *repo) fprintf(f, "repo.clone-url=%s\n", repo->clone_url); fprintf(f, "repo.enable-blame=%d\n", repo->enable_blame); + fprintf(f, "repo.enable-bugs=%d\n", + repo->enable_bugs); fprintf(f, "repo.enable-commit-graph=%d\n", repo->enable_commit_graph); fprintf(f, "repo.enable-follow-links=%d\n", @@ -826,6 +839,8 @@ static void print_repo(FILE *f, struct cgit_repo *repo) repo->enable_log_linecount); if (repo->about_filter && repo->about_filter != ctx.cfg.about_filter) cgit_fprintf_filter(repo->about_filter, f, "repo.about-filter="); + if (repo->bugs_filter && repo->bugs_filter != ctx.cfg.bugs_filter) + cgit_fprintf_filter(repo->bugs_filter, f, "repo.bugs-filter="); if (repo->commit_filter && repo->commit_filter != ctx.cfg.commit_filter) cgit_fprintf_filter(repo->commit_filter, f, "repo.commit-filter="); if (repo->source_filter && repo->source_filter != ctx.cfg.source_filter) @@ -1033,6 +1048,9 @@ static int calc_ttl(void) if (!strcmp(ctx.qry.page, "about")) return ctx.cfg.cache_about_ttl; + if (!strcmp(ctx.qry.page, "bugs")) + return ctx.cfg.cache_bugs_ttl; + if (!strcmp(ctx.qry.page, "snapshot")) return ctx.cfg.cache_snapshot_ttl; diff --git a/cgit.css b/cgit.css index d10a24d..13ee509 100644 --- a/cgit.css +++ b/cgit.css @@ -907,3 +907,106 @@ div#cgit table.ssdiff td.space { div#cgit table.ssdiff td.space div { min-height: 3em; } + +/* bugs page */ + +div#cgit div#bugs .bug-counts { + margin: 0.5em 0 1em 0; +} + +div#cgit div#bugs .bug-counts a { + padding: 3px 10px; + border: 1px solid #d1d5da; + border-radius: 3px; + margin-right: 4px; + color: #333; +} + +div#cgit div#bugs .bug-counts a.active { + background: #333; + color: #fff; + border-color: #333; +} + +div#cgit div#bugs table.list td.id { + font-family: monospace; + white-space: nowrap; +} + +div#cgit div#bugs table.list td.comments { + text-align: center; + color: #666; + white-space: nowrap; +} + +div#cgit div#bugs table.list td.age { + white-space: nowrap; +} + +div#cgit div#bugs .bug-labels span { + display: inline-block; + padding: 0 4px; + margin: 0 1px; + background: #e4e4e4; + border: 1px solid #d1d5da; + border-radius: 3px; + font-size: 90%; +} + +div#cgit div#bugs .bug-status-open { + color: #fff; + background: #2cbe4e; + padding: 1px 6px; + border-radius: 3px; + font-weight: bold; + font-size: 90%; +} + +div#cgit div#bugs .bug-status-closed { + color: #fff; + background: #cb2431; + padding: 1px 6px; + border-radius: 3px; + font-weight: bold; + font-size: 90%; +} + +div#cgit div#bugs .comment { + border: 1px solid #d1d5da; + border-radius: 3px; + margin: 8px 0; +} + +div#cgit div#bugs .comment-header { + background: #f1f8ff; + padding: 6px 10px; + border-bottom: 1px solid #d1d5da; + color: #555; + font-size: 95%; +} + +div#cgit div#bugs .comment-header a.date { + float: right; + font-weight: normal; + color: #666; +} + +div#cgit div#bugs .comment-body { + padding: 8px 12px; + white-space: pre-wrap; + font-family: monospace; + font-size: 95%; + line-height: 1.45; +} + +div#cgit div#bugs .pager { + margin: 1em 0; +} + +div#cgit div#bugs .pager a { + padding: 3px 10px; + border: 1px solid #d1d5da; + border-radius: 3px; + margin-right: 4px; + color: #333; +} diff --git a/cgit.h b/cgit.h index 7d7ece7..310ee04 100644 --- a/cgit.h +++ b/cgit.h @@ -63,7 +63,7 @@ typedef enum { } diff_type; typedef enum { - ABOUT, COMMIT, SOURCE, EMAIL, AUTH, OWNER + ABOUT, COMMIT, SOURCE, EMAIL, AUTH, OWNER, BUGS } filter_type; struct cgit_filter { @@ -100,6 +100,7 @@ struct cgit_repo { char *snapshot_prefix; int snapshots; int enable_blame; + int enable_bugs; int enable_commit_graph; int enable_follow_links; int enable_log_filecount; @@ -112,6 +113,7 @@ struct cgit_repo { int commit_sort; time_t mtime; struct cgit_filter *about_filter; + struct cgit_filter *bugs_filter; struct cgit_filter *commit_filter; struct cgit_filter *source_filter; struct cgit_filter *email_filter; @@ -226,6 +228,7 @@ struct cgit_config { int cache_scanrc_ttl; int cache_static_ttl; int cache_about_ttl; + int cache_bugs_ttl; int cache_snapshot_ttl; int case_sensitive_sort; int embedded; @@ -235,6 +238,7 @@ struct cgit_config { int enable_index_links; int enable_index_owner; int enable_blame; + int enable_bugs; int enable_commit_graph; int enable_log_filecount; int enable_log_linecount; @@ -269,6 +273,7 @@ struct cgit_config { struct string_list mimetypes; struct string_list js; struct cgit_filter *about_filter; + struct cgit_filter *bugs_filter; struct cgit_filter *commit_filter; struct cgit_filter *source_filter; struct cgit_filter *email_filter; diff --git a/cgit.mk b/cgit.mk index 3fcc1ca..aa8faf4 100644 --- a/cgit.mk +++ b/cgit.mk @@ -79,6 +79,7 @@ CGIT_OBJ_NAMES += shared.o CGIT_OBJ_NAMES += ui-atom.o CGIT_OBJ_NAMES += ui-blame.o CGIT_OBJ_NAMES += ui-blob.o +CGIT_OBJ_NAMES += ui-bugs.o CGIT_OBJ_NAMES += ui-clone.o CGIT_OBJ_NAMES += ui-commit.o CGIT_OBJ_NAMES += ui-diff.o diff --git a/cgitrc.5.txt b/cgitrc.5.txt index 7c39bf9..9d5cc17 100644 --- a/cgitrc.5.txt +++ b/cgitrc.5.txt @@ -34,6 +34,15 @@ about-filter:: included verbatim on the about page. Default value: none. See also: "FILTER API". +bugs-filter:: + Specifies a command which will be invoked to render the bugs page + for repositories that use git-bug (https://github.com/git-bug/git-bug). + The filter receives the path component as its first argument: an + empty string for the bug list, or a bug ID for the detail view. + The filter is responsible for reading git-bug data from the + repository and producing HTML output. Default value: none. See + also: "enable-bugs", "FILTER API". + agefile:: Specifies a path, relative to each repository path, which can be used to specify the date and time of the youngest commit in the repository. @@ -59,6 +68,11 @@ cache-about-ttl:: version of the repository about page. See also: "CACHE". Default value: "15". +cache-bugs-ttl:: + Number which specifies the time-to-live, in minutes, for the cached + version of the repository bugs page. See also: "CACHE". Default + value: "15". + cache-dynamic-ttl:: Number which specifies the time-to-live, in minutes, for the cached version of repository pages accessed without a fixed SHA1. See also: @@ -147,6 +161,11 @@ enable-blame:: for files, and will make it generate links to that page in appropriate places. Default value: "0". +enable-bugs:: + Flag which, when set to "1", will add a "bugs" tab to the repository + navigation for browsing git-bug issues. Requires a "bugs-filter" to + be configured. Default value: "0". + enable-commit-graph:: Flag which, when set to "1", will make cgit print an ASCII-art commit history graph to the left of the commit messages in the repository @@ -491,6 +510,10 @@ repo.defbranch:: repo.desc:: The value to show as repository description. Default value: none. +repo.bugs-filter:: + Override the default bugs-filter. Default value: none. See also: + "enable-filter-overrides". See also: "FILTER API". + repo.email-filter:: Override the default email-filter. Default value: none. See also: "enable-filter-overrides". See also: "FILTER API". @@ -499,6 +522,10 @@ repo.enable-blame:: A flag which can be used to disable the global setting `enable-blame'. Default value: none. +repo.enable-bugs:: + A flag which can be used to disable the global setting + `enable-bugs'. Default value: none. + repo.enable-commit-graph:: A flag which can be used to disable the global setting `enable-commit-graph'. Default value: none. @@ -844,6 +871,11 @@ enable-index-links=1 enable-blame=1 +# Enable git-bug browsing for repos that have it +enable-bugs=1 +bugs-filter=lua:/usr/share/cgit/filters/bugs-git-bug.lua + + # Enable ASCII art commit history graph on the log pages enable-commit-graph=1 diff --git a/cmd.c b/cmd.c index 0eb75b1..73c61d0 100644 --- a/cmd.c +++ b/cmd.c @@ -13,6 +13,7 @@ #include "ui-atom.h" #include "ui-blame.h" #include "ui-blob.h" +#include "ui-bugs.h" #include "ui-clone.h" #include "ui-commit.h" #include "ui-diff.h" @@ -72,6 +73,14 @@ static void blame_fn(void) cgit_print_error_page(403, "Forbidden", "Blame is disabled"); } +static void bugs_fn(void) +{ + if (ctx.repo->enable_bugs) + cgit_print_bugs(); + else + cgit_print_error_page(403, "Forbidden", "Bug tracker view is not enabled"); +} + static void blob_fn(void) { cgit_print_blob(ctx.qry.oid, ctx.qry.path, ctx.qry.head, 0); @@ -175,6 +184,7 @@ struct cgit_cmd *cgit_get_cmd(void) def_cmd(about, 0, 0, 0), def_cmd(blame, 1, 1, 0), def_cmd(blob, 1, 0, 0), + def_cmd(bugs, 1, 0, 0), def_cmd(commit, 1, 1, 0), def_cmd(diff, 1, 1, 0), def_cmd(info, 1, 0, 1), diff --git a/filter.c b/filter.c index c778d05..9a7077f 100644 --- a/filter.c +++ b/filter.c @@ -432,6 +432,7 @@ struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype) argument_count = 0; break; + case BUGS: case SOURCE: case ABOUT: argument_count = 1; diff --git a/shared.c b/shared.c index a39394d..2632073 100644 --- a/shared.c +++ b/shared.c @@ -62,6 +62,7 @@ struct cgit_repo *cgit_add_repo(const char *url) ret->section = ctx.cfg.section; ret->snapshots = ctx.cfg.snapshots; ret->enable_blame = ctx.cfg.enable_blame; + ret->enable_bugs = ctx.cfg.enable_bugs; ret->enable_commit_graph = ctx.cfg.enable_commit_graph; ret->enable_follow_links = ctx.cfg.enable_follow_links; ret->enable_log_filecount = ctx.cfg.enable_log_filecount; @@ -76,6 +77,7 @@ struct cgit_repo *cgit_add_repo(const char *url) ret->readme = ctx.cfg.readme; ret->mtime = -1; ret->about_filter = ctx.cfg.about_filter; + ret->bugs_filter = ctx.cfg.bugs_filter; ret->commit_filter = ctx.cfg.commit_filter; ret->source_filter = ctx.cfg.source_filter; ret->email_filter = ctx.cfg.email_filter; diff --git a/ui-bugs.c b/ui-bugs.c new file mode 100644 index 0000000..268c04a --- /dev/null +++ b/ui-bugs.c @@ -0,0 +1,30 @@ +/* ui-bugs.c: git-bug browsing via Lua filter + * + * Copyright (C) 2026 by the Linux Foundation + * + * Licensed under GNU General Public License v2 + * (see COPYING for full license text) + */ + +#include "cgit.h" +#include "ui-bugs.h" +#include "html.h" +#include "ui-shared.h" + +void cgit_print_bugs(void) +{ + struct cgit_filter *f = ctx.repo->bugs_filter; + + if (!f) { + cgit_print_error_page(404, "Not Found", + "No bugs-filter configured for this repository"); + return; + } + + cgit_print_layout_start(); + html("<div id='bugs'>"); + cgit_open_filter(f, ctx.qry.path ? ctx.qry.path : ""); + cgit_close_filter(f); + html("</div>"); + cgit_print_layout_end(); +} diff --git a/ui-bugs.h b/ui-bugs.h new file mode 100644 index 0000000..ec0fdd8 --- /dev/null +++ b/ui-bugs.h @@ -0,0 +1,6 @@ +#ifndef UI_BUGS_H +#define UI_BUGS_H + +extern void cgit_print_bugs(void); + +#endif /* UI_BUGS_H */ diff --git a/ui-shared.c b/ui-shared.c index df52a9b..086e413 100644 --- a/ui-shared.c +++ b/ui-shared.c @@ -516,6 +516,12 @@ void cgit_stats_link(const char *name, const char *title, const char *class, reporevlink("stats", name, title, class, head, NULL, path); } +void cgit_bugs_link(const char *name, const char *title, const char *class, + const char *path) +{ + reporevlink("bugs", name, title, class, NULL, NULL, path); +} + static void cgit_self_link(char *name, const char *title, const char *class) { if (!strcmp(ctx.qry.page, "repolist")) @@ -567,6 +573,8 @@ static void cgit_self_link(char *name, const char *title, const char *class) else if (!strcmp(ctx.qry.page, "stats")) cgit_stats_link(name, title, class, ctx.qry.head, ctx.qry.path); + else if (!strcmp(ctx.qry.page, "bugs")) + cgit_bugs_link(name, title, class, ctx.qry.path); else { /* Don't known how to make link for this page */ repolink(title, class, ctx.qry.page, ctx.qry.head, ctx.qry.path); @@ -1107,6 +1115,10 @@ void cgit_print_pageheader(void) ctx.qry.head, ctx.qry.oid, ctx.qry.vpath); cgit_diff_link("diff", NULL, hc("diff"), ctx.qry.head, ctx.qry.oid, ctx.qry.oid2, ctx.qry.vpath); + if (ctx.repo->enable_bugs) + reporevlink("bugs", "bugs", NULL, + hc("bugs"), ctx.qry.head, NULL, + NULL); if (ctx.repo->max_stats) cgit_stats_link("stats", NULL, hc("stats"), ctx.qry.head, ctx.qry.vpath); diff --git a/ui-shared.h b/ui-shared.h index 2a3a7f5..9d98781 100644 --- a/ui-shared.h +++ b/ui-shared.h @@ -53,6 +53,8 @@ extern void cgit_diff_link(const char *name, const char *title, extern void cgit_stats_link(const char *name, const char *title, const char *class, const char *head, const char *path); +extern void cgit_bugs_link(const char *name, const char *title, + const char *class, const char *path); extern void cgit_object_link(struct object *obj); extern void cgit_submodule_link(const char *class, char *path, -- 2.55.0
