[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