[PATCH RFC 1/3] ui-bugs: add git-bug browsing via a Lua filter
Konstantin Ryabitsev
konstantin at linuxfoundation.org
Tue Aug 25 18:59:46 UTC 2026
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 <konstantin at linuxfoundation.org>
---
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
More information about the CGit
mailing list