aboutsummaryrefslogtreecommitdiff
diff options
from:
to:
context:
space:
mode:
-rw-r--r--cgit.c22
-rw-r--r--cgit.h6
-rw-r--r--cgit.mk1
-rw-r--r--cgitrc.5.txt70
-rw-r--r--cmd.c7
-rw-r--r--shared.c3
-rw-r--r--tests/t0112-ci.sh83
-rw-r--r--ui-ci.c129
-rw-r--r--ui-ci.h7
-rw-r--r--ui-shared.c9
-rw-r--r--ui-shared.h2
11 files changed, 339 insertions, 0 deletions
diff --git a/cgit.c b/cgit.c
index 5bf52f8..c21117e 100644
--- a/cgit.c
+++ b/cgit.c
@@ -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",
diff --git a/cgit.h b/cgit.h
index b8b7712..709fc0c 100644
--- a/cgit.h
+++ b/cgit.h
@@ -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;
diff --git a/cgit.mk b/cgit.mk
index 3fcc1ca..c4b735d 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-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
diff --git a/cmd.c b/cmd.c
index 0eb75b1..4c249c8 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-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),
diff --git a/shared.c b/shared.c
index e79c3da..d839071 100644
--- a/shared.c
+++ b/shared.c
@@ -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
diff --git a/ui-ci.c b/ui-ci.c
new file mode 100644
index 0000000..1c9f39c
--- /dev/null
+++ b/ui-ci.c
@@ -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);
+}
diff --git a/ui-ci.h b/ui-ci.h
new file mode 100644
index 0000000..3d7ce02
--- /dev/null
+++ b/ui-ci.h
@@ -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);