diff options
| -rw-r--r-- | cgit.c | 22 | ||||
| -rw-r--r-- | cgit.h | 6 | ||||
| -rw-r--r-- | cgit.mk | 1 | ||||
| -rw-r--r-- | cgitrc.5.txt | 70 | ||||
| -rw-r--r-- | cmd.c | 7 | ||||
| -rw-r--r-- | shared.c | 3 | ||||
| -rw-r--r-- | tests/t0112-ci.sh | 83 | ||||
| -rw-r--r-- | ui-ci.c | 129 | ||||
| -rw-r--r-- | ui-ci.h | 7 | ||||
| -rw-r--r-- | ui-shared.c | 9 | ||||
| -rw-r--r-- | ui-shared.h | 2 |
11 files changed, 339 insertions, 0 deletions
@@ -56,6 +56,16 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu repo->owner = strdup_first_line(value); else if (!strcmp(name, "homepage")) repo->homepage = strdup_first_line(value); + else if (!strcmp(name, "ci-url")) { + repo->ci_url = strdup_first_line(value); + if (repo->ci_branch_url == ctx.cfg.ci_branch_url) + repo->ci_branch_url = NULL; + if (repo->ci_tag_url == ctx.cfg.ci_tag_url) + repo->ci_tag_url = NULL; + } else if (!strcmp(name, "ci-branch-url")) + repo->ci_branch_url = strdup_first_line(value); + else if (!strcmp(name, "ci-tag-url")) + repo->ci_tag_url = strdup_first_line(value); else if (!strcmp(name, "defbranch")) repo->defbranch = strdup_first_line(value); else if (!strcmp(name, "extra-head-content")) @@ -163,6 +173,12 @@ static void config_cb(const char *name, const char *value) ctx.cfg.logo_link = strdup_first_line(value); else if (!strcmp(name, "module-link")) ctx.cfg.module_link = strdup_first_line(value); + else if (!strcmp(name, "ci-url")) + ctx.cfg.ci_url = strdup_first_line(value); + else if (!strcmp(name, "ci-branch-url")) + ctx.cfg.ci_branch_url = strdup_first_line(value); + else if (!strcmp(name, "ci-tag-url")) + ctx.cfg.ci_tag_url = strdup_first_line(value); else if (!strcmp(name, "strict-export")) ctx.cfg.strict_export = strdup_first_line(value); else if (!strcmp(name, "virtual-root")) @@ -815,6 +831,12 @@ static void print_repo(FILE *f, struct cgit_repo *repo) fprintf(f, "repo.section=%s\n", repo->section); if (repo->homepage) fprintf(f, "repo.homepage=%s\n", repo->homepage); + if (repo->ci_url) + fprintf(f, "repo.ci-url=%s\n", repo->ci_url); + if (repo->ci_branch_url) + fprintf(f, "repo.ci-branch-url=%s\n", repo->ci_branch_url); + if (repo->ci_tag_url) + fprintf(f, "repo.ci-tag-url=%s\n", repo->ci_tag_url); if (repo->clone_url) fprintf(f, "repo.clone-url=%s\n", repo->clone_url); fprintf(f, "repo.enable-blame=%d\n", @@ -90,6 +90,9 @@ struct cgit_repo { char *extra_head_content; char *owner; char *homepage; + char *ci_url; + char *ci_branch_url; + char *ci_tag_url; char *defbranch; char *module_link; struct string_list readme; @@ -196,6 +199,9 @@ struct cgit_query { struct cgit_config { char *agefile; char *cache_root; + char *ci_url; + char *ci_branch_url; + char *ci_tag_url; char *clone_prefix; char *clone_url; char *favicon; @@ -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-ci.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..145a17e 100644 --- a/cgitrc.5.txt +++ b/cgitrc.5.txt @@ -100,6 +100,23 @@ cache-static-ttl:: version of repository pages accessed with a fixed SHA1. See also: "CACHE". Default value: -1". +ci-branch-url:: + Url template used by the "ci" tab when a branch is being viewed. If + unspecified, `ci-url` is used instead. Default value: none. See also: + "CI URL EXPANSION". + +ci-tag-url:: + Url template used by the "ci" tab when a tag is being viewed. If + unspecified, `ci-url' is used instead. Default value: none. See also: + "CI URL EXPANSION". + +ci-url:: + Url template used by the "ci" tab for refs which are not covered by + `ci-branch-url` or `ci-tag-url`. When at least one of the three ci url + settings is specified, cgit adds a "ci" tab which redirects to the + expanded url for the ref currently being viewed. Default value: none. + See also: "CI URL EXPANSION". + clone-prefix:: Space-separated list of common prefixes which, when combined with a repository url, generates valid clone urls for the repository. This @@ -468,6 +485,18 @@ repo.branch-sort:: list, and when set to "name" enables ordering by branch name. Default value: "name". +repo.ci-branch-url:: + Override the global setting `ci-branch-url' for this repository. + Default value: <ci-branch-url>. See also: "CI URL EXPANSION". + +repo.ci-tag-url:: + Override the global setting `ci-tag-url' for this repository. Default + value: <ci-tag-url>. See also: "CI URL EXPANSION". + +repo.ci-url:: + Override the global setting `ci-url' for this repository. Default + value: <ci-url>. See also: "CI URL EXPANSION". + repo.clone-url:: A list of space-separated urls which can be used to clone this repo. Default value: none. See also: "MACRO EXPANSION". @@ -776,6 +805,42 @@ the environment variables defined in "FILTER API": - repo.clone-url +CI URL EXPANSION +---------------- +The options `ci-url', `ci-branch-url' and `ci-tag-url' (and their +`repo.'-prefixed counterparts) are url templates in which the following +tokens are substituted when the "ci" tab is followed: + +$ref:: + The name of the branch or tag currently being viewed. + +$repo:: + The url of the repository, i.e. the value of `repo.url'. + +$slug:: + $repo with any ".git" suffix removed and all slashes replaced by + dashes, e.g. "sayauz/web.git" becomes "sayauz-web". + +Two consecutive dollar signs produce a single literal one, and any other +"$" is left untouched. Tokens are substituted verbatim, so the resulting +url must already be properly encoded. + +When a tag is being viewed, `ci-tag-url' is used; for anything else +`ci-branch-url' is used. Whichever of the two applies falls back to +`ci-url' when it is unset, and the "ci" tab yields "404 Not found" when +neither is set for the current ref. This makes it possible to point at a +Jenkins multibranch pipeline, where branches and tags live at different +locations: + + ci-branch-url=https://ci.example.org/job/$slug/job/$ref + ci-tag-url=https://ci.example.org/job/$slug/view/tags/job/$ref + +Setting `repo.ci-url' discards the values which the repository would +otherwise inherit from the global `ci-branch-url' and `ci-tag-url', so +that a single repository can be pointed at a different ci system without +having to override both of them. + + CACHE ----- @@ -828,6 +893,11 @@ clone-url=git://foo.org/$CGIT_REPO_URL git@foo.org:$CGIT_REPO_URL css=/css/cgit.css +# Add a "ci" tab pointing at a Jenkins multibranch pipeline +ci-branch-url=https://ci.foo.org/job/$slug/job/$ref +ci-tag-url=https://ci.foo.org/job/$slug/view/tags/job/$ref + + # Show owner on index page enable-index-owner=1 @@ -13,6 +13,7 @@ #include "ui-atom.h" #include "ui-blame.h" #include "ui-blob.h" +#include "ui-ci.h" #include "ui-clone.h" #include "ui-commit.h" #include "ui-diff.h" @@ -77,6 +78,11 @@ static void blob_fn(void) cgit_print_blob(ctx.qry.oid, ctx.qry.path, ctx.qry.head, 0); } +static void ci_fn(void) +{ + cgit_print_ci(); +} + static void commit_fn(void) { cgit_print_commit(ctx.qry.oid, ctx.qry.path); @@ -175,6 +181,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(ci, 1, 0, 0), def_cmd(commit, 1, 1, 0), def_cmd(diff, 1, 1, 0), def_cmd(info, 1, 0, 1), @@ -71,6 +71,9 @@ struct cgit_repo *cgit_add_repo(const char *url) ret->extra_head_content = NULL; ret->owner = NULL; ret->homepage = NULL; + ret->ci_url = ctx.cfg.ci_url; + ret->ci_branch_url = ctx.cfg.ci_branch_url; + ret->ci_tag_url = ctx.cfg.ci_tag_url; ret->section = ctx.cfg.section; ret->snapshots = ctx.cfg.snapshots; ret->enable_blame = ctx.cfg.enable_blame; diff --git a/tests/t0112-ci.sh b/tests/t0112-ci.sh new file mode 100644 index 0000000..d8673e4 --- /dev/null +++ b/tests/t0112-ci.sh @@ -0,0 +1,83 @@ +#!/bin/sh + +test_description='Check content on ci page' +. ./setup.sh + +# The ci settings are only inherited by repositories which are declared +# after them, which is used below to get a repository with no ci url and +# one with a tag-only ci url. +cat >cgitrc.ci <<EOF +virtual-root=/ +cache-root=$PWD/cache +cache-size=0 + +repo.url=tagonly +repo.path=$PWD/repos/foo/.git +repo.ci-tag-url=https://ci.example.org/tagonly/\$ref + +ci-branch-url=https://ci.example.org/job/\$slug/job/\$ref +ci-tag-url=https://ci.example.org/job/\$slug/view/tags/job/\$ref + +repo.url=foo +repo.path=$PWD/repos/foo/.git + +repo.url=sayauz/web.git +repo.path=$PWD/repos/foo/.git + +repo.url=override +repo.path=$PWD/repos/foo/.git +repo.ci-url=https://ci.example.org/override/\$repo/\$\$/\$ref +EOF + +git --git-dir="$PWD/repos/foo/.git" tag v0.17.3.2 master + +cgit_ci() +{ + CGIT_CONFIG="$PWD/cgitrc.ci" QUERY_STRING="url=$1" cgit +} + +test_expect_success 'redirect to the branch ci url' ' + cgit_ci "foo/ci&h=master" >tmp && + grep "^Status: 302 Found$" tmp && + grep "^Location: https://ci.example.org/job/foo/job/master$" tmp +' + +test_expect_success 'redirect to the tag ci url' ' + cgit_ci "foo/ci&h=v0.17.3.2" >tmp && + grep "^Status: 302 Found$" tmp && + grep "^Location: https://ci.example.org/job/foo/view/tags/job/v0.17.3.2$" tmp +' + +test_expect_success 'redirect to the default branch ci url' ' + cgit_ci "foo/ci" >tmp && + grep "^Location: https://ci.example.org/job/foo/job/master$" tmp +' + +test_expect_success '$slug strips ".git" and replaces slashes' ' + cgit_ci "sayauz/web.git/ci&h=v0.17.3.2" >tmp && + grep "^Location: https://ci.example.org/job/sayauz-web/view/tags/job/v0.17.3.2$" tmp +' + +test_expect_success 'repo.ci-url overrides and expands $repo and $$' ' + cgit_ci "override/ci&h=master" >tmp && + grep -F "Location: https://ci.example.org/override/override/\$/master" tmp +' + +test_expect_success 'repo.ci-tag-url does not apply to branches' ' + cgit_ci "tagonly/ci&h=v0.17.3.2" >tmp && + grep "^Location: https://ci.example.org/tagonly/v0.17.3.2$" tmp && + cgit_ci "tagonly/ci&h=master" >tmp && + grep "^Status: 404 Not found$" tmp +' + +test_expect_success 'find ci tab' ' + cgit_ci "foo/refs&h=master" >tmp && + grep "href=./foo/ci/.>ci</a>" tmp +' + +test_expect_success 'no ci tab without a ci url' ' + cgit_url "foo/refs" >tmp && + ! grep ">ci</a>" tmp +' + +test_done @@ -0,0 +1,129 @@ +/* ui-ci.c: redirect to an external CI system + * + * Copyright (C) 2006-2025 cgit Development Team <cgit@lists.zx2c4.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); +} + +static void resolve_ci_url(void) +{ + const char *template; + + 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)) + template = ctx.repo->ci_tag_url; + else + template = ctx.repo->ci_branch_url; + if (!template) + template = ctx.repo->ci_url; + if (!template) + return; + + ci_url = expand_ci_url(template); + 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); +} @@ -0,0 +1,7 @@ +#ifndef UI_CI_H +#define UI_CI_H + +extern int cgit_ci_available(void); +extern void cgit_print_ci(void); + +#endif /* UI_CI_H */ diff --git a/ui-shared.c b/ui-shared.c index a6a828d..9a9d014 100644 --- a/ui-shared.c +++ b/ui-shared.c @@ -12,6 +12,7 @@ #include "ui-shared.h" #include "cmd.h" #include "html.h" +#include "ui-ci.h" #include "version.h" static const char cgit_doctype[] = @@ -342,6 +343,12 @@ void cgit_tag_link(const char *name, const char *title, const char *class, reporevlink("tag", name, title, class, tag, NULL, NULL); } +void cgit_ci_link(const char *name, const char *title, const char *class, + const char *head) +{ + reporevlink("ci", name, title, class, head, NULL, NULL); +} + void cgit_tree_link(const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path) { @@ -1110,6 +1117,8 @@ void cgit_print_pageheader(void) if (ctx.repo->max_stats) cgit_stats_link("stats", NULL, hc("stats"), ctx.qry.head, ctx.qry.vpath); + if (cgit_ci_available()) + cgit_ci_link("ci", NULL, hc("ci"), ctx.qry.head); if (ctx.repo->homepage) { html("<a href='"); html_attr(ctx.repo->homepage); diff --git a/ui-shared.h b/ui-shared.h index 2a3a7f5..a6627d4 100644 --- a/ui-shared.h +++ b/ui-shared.h @@ -21,6 +21,8 @@ extern void cgit_summary_link(const char *name, const char *title, const char *class, const char *head); extern void cgit_tag_link(const char *name, const char *title, const char *class, const char *tag); +extern void cgit_ci_link(const char *name, const char *title, + const char *class, const char *head); extern void cgit_tree_link(const char *name, const char *title, const char *class, const char *head, const char *rev, const char *path); |