aboutsummaryrefslogtreecommitdiff
path: root/ui-ci.c
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
Diffstat (limited to 'ui-ci.c')
-rw-r--r--ui-ci.c152
1 files changed, 152 insertions, 0 deletions
diff --git a/ui-ci.c b/ui-ci.c
new file mode 100644
index 0000000..f2561a2
--- /dev/null
+++ b/ui-ci.c
@@ -0,0 +1,152 @@
+/* ui-ci.c: redirect to an external CI system
+ *
+ * Copyright (C) 2026 Saya Andy <saya.andy@posteo.com>
+ *
+ * Licensed under GNU General Public License v2
+ * (see COPYING for full license text)
+ */
+
+#define USE_THE_REPOSITORY_VARIABLE
+
+#include "cgit.h"
+#include "ui-ci.h"
+#include "html.h"
+#include "ui-shared.h"
+
+/* Memoized outcome of resolving the ci url for this request: -1 while
+ * unresolved, 0 when there is no ci url for the current ref, and 1 when
+ * ci_url holds the url to redirect to.
+ */
+static int ci_resolved = -1;
+static char *ci_url;
+
+static int ref_is_tag(const char *ref)
+{
+ struct strbuf fullref = STRBUF_INIT;
+ struct object_id oid;
+ int is_tag;
+
+ strbuf_addf(&fullref, "refs/tags/%s", ref);
+ is_tag = !repo_get_oid(the_repository, fullref.buf, &oid);
+ strbuf_release(&fullref);
+ return is_tag;
+}
+
+/* Append the repository url with any ".git" suffix removed and all
+ * slashes replaced by dashes, e.g. "sayauz/web.git" becomes
+ * "sayauz-web".
+ */
+static void add_repo_slug(struct strbuf *buf, const char *url)
+{
+ size_t len, i;
+
+ if (!strip_suffix(url, ".git", &len))
+ len = strlen(url);
+ for (i = 0; i < len; i++)
+ strbuf_addch(buf, url[i] == '/' ? '-' : url[i]);
+}
+
+static char *expand_ci_url(const char *template)
+{
+ const char *p, *rest;
+ struct strbuf buf = STRBUF_INIT;
+
+ for (p = template; *p; p++) {
+ if (*p != '$') {
+ strbuf_addch(&buf, *p);
+ continue;
+ }
+ if (skip_prefix(p, "$ref", &rest))
+ strbuf_addstr(&buf, ctx.qry.head);
+ else if (skip_prefix(p, "$repo", &rest))
+ strbuf_addstr(&buf, ctx.repo->url);
+ else if (skip_prefix(p, "$slug", &rest))
+ add_repo_slug(&buf, ctx.repo->url);
+ else if (skip_prefix(p, "$$", &rest))
+ strbuf_addch(&buf, '$');
+ else {
+ strbuf_addch(&buf, '$');
+ continue;
+ }
+ p = rest - 1;
+ }
+ return strbuf_detach(&buf, NULL);
+}
+
+/* Ask the ci filter whether the pipeline denoted by url actually exists.
+ * The filter must not write to stdout, and signals "yes" by exiting with
+ * a zero status.
+ */
+static int ci_filter_accepts(const char *refkind, const char *url)
+{
+ struct cgit_filter *filter = ctx.repo->ci_filter;
+
+ if (!filter)
+ return 1;
+ if (cgit_open_filter(filter, ctx.qry.head, refkind, url))
+ return 0;
+ return !cgit_close_filter(filter);
+}
+
+static void resolve_ci_url(void)
+{
+ const char *template, *refkind;
+
+ if (ci_resolved != -1)
+ return;
+ ci_resolved = 0;
+
+ if (!ctx.repo || !ctx.qry.head || !cgit_have_repository())
+ return;
+
+ if (ref_is_tag(ctx.qry.head)) {
+ refkind = "tag";
+ template = ctx.repo->ci_tag_url;
+ } else {
+ refkind = "branch";
+ template = ctx.repo->ci_branch_url;
+ }
+ if (!template)
+ template = ctx.repo->ci_url;
+ if (!template)
+ return;
+
+ ci_url = expand_ci_url(template);
+ if (!ci_filter_accepts(refkind, ci_url)) {
+ free(ci_url);
+ ci_url = NULL;
+ return;
+ }
+ ci_resolved = 1;
+}
+
+int cgit_ci_available(void)
+{
+ resolve_ci_url();
+ return ci_resolved == 1;
+}
+
+void cgit_print_ci(void)
+{
+ resolve_ci_url();
+
+ if (ci_resolved != 1) {
+ cgit_print_error_page(404, "Not found",
+ "No ci url available for %s",
+ ctx.qry.head);
+ return;
+ }
+
+ /* The expanded template is emitted verbatim rather than through
+ * cgit_redirect(), which percent-encodes characters such as '?',
+ * '=' and '%' that are meaningful in a ci url. Refuse anything
+ * which could be used to smuggle in extra response headers.
+ */
+ if (strpbrk(ci_url, "\r\n")) {
+ cgit_print_error_page(500, "Internal server error",
+ "Malformed ci url");
+ return;
+ }
+
+ htmlf("Status: 302 Found\nLocation: %s\n\n", ci_url);
+}