/* ui-ci.c: redirect to an external CI system * * Copyright (C) 2026 Saya Andy * * 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); }