aboutsummaryrefslogtreecommitdiff
diff options
from:
to:
context:
space:
mode:
-rw-r--r--cgit.c10
-rw-r--r--cgit.h4
-rw-r--r--cgitrc.5.txt27
-rw-r--r--filter.c6
-rwxr-xr-xfilters/ci-jenkins.sh58
-rw-r--r--shared.c1
-rw-r--r--tests/t0112-ci.sh44
-rw-r--r--tests/t0113-ci-lua.sh78
-rw-r--r--ui-ci.c29
9 files changed, 253 insertions, 4 deletions
diff --git a/cgit.c b/cgit.c
index c21117e..97a4180 100644
--- a/cgit.c
+++ b/cgit.c
@@ -58,6 +58,10 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu
repo->homepage = strdup_first_line(value);
else if (!strcmp(name, "ci-url")) {
repo->ci_url = strdup_first_line(value);
+ /* An explicit repo.ci-url would otherwise never be used
+ * when the more specific urls are inherited from the
+ * global settings, so discard those.
+ */
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)
@@ -132,6 +136,8 @@ void cgit_repo_config(struct cgit_repo *repo, const char *name, const char *valu
repo->email_filter = cgit_new_filter(value, EMAIL);
else if (!strcmp(name, "owner-filter"))
repo->owner_filter = cgit_new_filter(value, OWNER);
+ else if (!strcmp(name, "ci-filter"))
+ repo->ci_filter = cgit_new_filter(value, CI);
}
}
@@ -249,6 +255,8 @@ static void config_cb(const char *name, const char *value)
ctx.cfg.owner_filter = cgit_new_filter(value, OWNER);
else if (!strcmp(name, "auth-filter"))
ctx.cfg.auth_filter = cgit_new_filter(value, AUTH);
+ else if (!strcmp(name, "ci-filter"))
+ ctx.cfg.ci_filter = cgit_new_filter(value, CI);
else if (!strcmp(name, "embedded"))
ctx.cfg.embedded = atoi(value);
else if (!strcmp(name, "max-atom-items"))
@@ -859,6 +867,8 @@ static void print_repo(FILE *f, struct cgit_repo *repo)
cgit_fprintf_filter(repo->email_filter, f, "repo.email-filter=");
if (repo->owner_filter && repo->owner_filter != ctx.cfg.owner_filter)
cgit_fprintf_filter(repo->owner_filter, f, "repo.owner-filter=");
+ if (repo->ci_filter && repo->ci_filter != ctx.cfg.ci_filter)
+ cgit_fprintf_filter(repo->ci_filter, f, "repo.ci-filter=");
if (repo->snapshots != ctx.cfg.snapshots) {
char *tmp = build_snapshot_setting(repo->snapshots);
fprintf(f, "repo.snapshots=%s\n", tmp ? tmp : "");
diff --git a/cgit.h b/cgit.h
index 709fc0c..847ab4e 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, CI
} filter_type;
struct cgit_filter {
@@ -119,6 +119,7 @@ struct cgit_repo {
struct cgit_filter *source_filter;
struct cgit_filter *email_filter;
struct cgit_filter *owner_filter;
+ struct cgit_filter *ci_filter;
struct string_list submodules;
int hide;
int ignore;
@@ -280,6 +281,7 @@ struct cgit_config {
struct cgit_filter *email_filter;
struct cgit_filter *owner_filter;
struct cgit_filter *auth_filter;
+ struct cgit_filter *ci_filter;
};
struct cgit_page {
diff --git a/cgitrc.5.txt b/cgitrc.5.txt
index 145a17e..c03579f 100644
--- a/cgitrc.5.txt
+++ b/cgitrc.5.txt
@@ -105,6 +105,11 @@ ci-branch-url::
unspecified, `ci-url` is used instead. Default value: none. See also:
"CI URL EXPANSION".
+ci-filter::
+ Specifies a command which decides whether the "ci" tab is shown for
+ the ref currently being viewed, which is useful when only some refs
+ have a pipeline. Default value: none. See also: "FILTER API".
+
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:
@@ -489,6 +494,10 @@ 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-filter::
+ Override the default ci-filter. Default value: none. See also:
+ "enable-filter-overrides". See also: "FILTER API".
+
repo.ci-tag-url::
Override the global setting `ci-tag-url' for this repository. Default
value: <ci-tag-url>. See also: "CI URL EXPANSION".
@@ -739,6 +748,20 @@ auth filter::
Please see `filters/simple-authentication.lua` for a clear example
script that may be modified.
+ci filter::
+ This filter is given three parameters: the name of the branch or tag
+ being viewed, the string "branch" or "tag" to say which of the two it
+ is, and the ci url that the "ci" tab would redirect to. It decides
+ whether that tab is shown at all, by returning zero from the exit code
+ / close function to show it and non-zero to hide it. Nothing is written
+ to its standard input, and it must not write to standard output, since
+ that would land in the middle of the page being rendered.
+
+ The filter is consulted while rendering every page of the repository,
+ not only ref pages, so a filter which contacts the ci system should
+ cache its verdict and use a short timeout. Please see
+ `filters/ci-jenkins.sh` for an example.
+
commit filter::
This filter is given no arguments. The commit message text that is to
be filtered is available on standard input and the filtered text is
@@ -840,6 +863,10 @@ 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.
+Note that cgit itself never contacts the ci system, so a tab shown for a
+ref which has no pipeline only reveals that after being followed. Use
+`ci-filter' to suppress the tab in that case.
+
CACHE
-----
diff --git a/filter.c b/filter.c
index c778d05..3b452cb 100644
--- a/filter.c
+++ b/filter.c
@@ -30,12 +30,14 @@ void cgit_cleanup_filters(void)
reap_filter(ctx.cfg.email_filter);
reap_filter(ctx.cfg.owner_filter);
reap_filter(ctx.cfg.auth_filter);
+ reap_filter(ctx.cfg.ci_filter);
for (i = 0; i < cgit_repolist.count; ++i) {
reap_filter(cgit_repolist.repos[i].about_filter);
reap_filter(cgit_repolist.repos[i].commit_filter);
reap_filter(cgit_repolist.repos[i].source_filter);
reap_filter(cgit_repolist.repos[i].email_filter);
reap_filter(cgit_repolist.repos[i].owner_filter);
+ reap_filter(cgit_repolist.repos[i].ci_filter);
}
}
@@ -424,6 +426,10 @@ struct cgit_filter *cgit_new_filter(const char *cmd, filter_type filtertype)
argument_count = 12;
break;
+ case CI:
+ argument_count = 3;
+ break;
+
case EMAIL:
argument_count = 2;
break;
diff --git a/filters/ci-jenkins.sh b/filters/ci-jenkins.sh
new file mode 100755
index 0000000..842af21
--- /dev/null
+++ b/filters/ci-jenkins.sh
@@ -0,0 +1,58 @@
+#!/bin/sh
+# This script may be used with the ci-filter or repo.ci-filter setting in
+# cgitrc to hide the "ci" tab for refs which have no pipeline on a Jenkins
+# instance.
+#
+# Arguments:
+# $1 the name of the branch or tag being viewed
+# $2 "branch" or "tag"
+# $3 the ci url which the "ci" tab would redirect to
+#
+# Exit with a zero status to show the tab, non-zero to hide it. This script
+# must not write anything to standard output, as that would end up in the
+# middle of the page cgit is rendering.
+#
+# The filter is consulted while rendering every repository page, so the
+# verdict is cached on disk to keep Jenkins from being hammered, and the
+# probe is given a short timeout so that an unreachable Jenkins degrades
+# into a missing tab rather than a hanging web server.
+#
+# Set CI_NETRC to a netrc(5) file if the Jenkins instance requires
+# authentication; without it a private job answers 403 and the tab is
+# hidden even though the pipeline exists.
+
+CI_CACHE_DIR="${CI_CACHE_DIR:-/var/cache/cgit/ci-filter}"
+CI_CACHE_TTL_MINUTES="${CI_CACHE_TTL_MINUTES:-5}"
+CI_TIMEOUT="${CI_TIMEOUT:-2}"
+
+url="$3"
+test -n "$url" || exit 1
+
+# Jenkins job pages are often not readable anonymously, so query the REST
+# API rather than the page the tab points at.
+probe="$url/api/json?tree=name"
+
+key="$(printf '%s' "$url" | cksum | tr -cd '0-9')"
+cache="$CI_CACHE_DIR/$key"
+
+mkdir -p "$CI_CACHE_DIR" 2>/dev/null
+
+if test -f "$cache" &&
+ test -z "$(find "$cache" -mmin "+$CI_CACHE_TTL_MINUTES" 2>/dev/null)"
+then
+ exit "$(cat "$cache")"
+fi
+
+status=0
+curl --silent --fail --head --output /dev/null \
+ --max-time "$CI_TIMEOUT" \
+ ${CI_NETRC:+--netrc-file "$CI_NETRC"} \
+ "$probe" >/dev/null 2>&1 || status=1
+
+if test -d "$CI_CACHE_DIR"
+then
+ printf '%s\n' "$status" >"$cache.$$" 2>/dev/null &&
+ mv "$cache.$$" "$cache" 2>/dev/null
+fi
+
+exit "$status"
diff --git a/shared.c b/shared.c
index d839071..83b5626 100644
--- a/shared.c
+++ b/shared.c
@@ -95,6 +95,7 @@ struct cgit_repo *cgit_add_repo(const char *url)
ret->source_filter = ctx.cfg.source_filter;
ret->email_filter = ctx.cfg.email_filter;
ret->owner_filter = ctx.cfg.owner_filter;
+ ret->ci_filter = ctx.cfg.ci_filter;
ret->clone_url = ctx.cfg.clone_url;
ret->submodules.strdup_strings = 1;
ret->hide = ret->ignore = 0;
diff --git a/tests/t0112-ci.sh b/tests/t0112-ci.sh
index d8673e4..33609d6 100644
--- a/tests/t0112-ci.sh
+++ b/tests/t0112-ci.sh
@@ -10,6 +10,7 @@ cat >cgitrc.ci <<EOF
virtual-root=/
cache-root=$PWD/cache
cache-size=0
+enable-filter-overrides=1
repo.url=tagonly
repo.path=$PWD/repos/foo/.git
@@ -27,8 +28,20 @@ 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
+
+repo.url=filtered
+repo.path=$PWD/repos/foo/.git
+repo.ci-filter=exec:$PWD/ci-filter.sh
EOF
+# Records its arguments and only accepts tags.
+cat >ci-filter.sh <<EOF
+#!/bin/sh
+printf '%s|%s|%s\n' "\$1" "\$2" "\$3" >>"$PWD/ci-filter.log"
+test "\$2" = tag
+EOF
+chmod +x ci-filter.sh
+
git --git-dir="$PWD/repos/foo/.git" tag v0.17.3.2 master
cgit_ci()
@@ -80,4 +93,35 @@ test_expect_success 'no ci tab without a ci url' '
! grep ">ci</a>" tmp
'
+test_expect_success 'ci filter accepts a tag' '
+ rm -f ci-filter.log &&
+ cgit_ci "filtered/ci&h=v0.17.3.2" >tmp &&
+ grep "^Location: https://ci.example.org/job/filtered/view/tags/job/v0.17.3.2$" tmp &&
+ echo "v0.17.3.2|tag|https://ci.example.org/job/filtered/view/tags/job/v0.17.3.2" >expected &&
+ test_cmp expected ci-filter.log
+'
+
+test_expect_success 'ci filter rejects a branch' '
+ rm -f ci-filter.log &&
+ cgit_ci "filtered/ci&h=master" >tmp &&
+ grep "^Status: 404 Not found$" tmp &&
+ echo "master|branch|https://ci.example.org/job/filtered/job/master" >expected &&
+ test_cmp expected ci-filter.log
+'
+
+test_expect_success "ci filter hides the tab and is consulted once" '
+ rm -f ci-filter.log &&
+ cgit_ci "filtered/refs&h=master" >tmp &&
+ ! grep ">ci</a>" tmp &&
+ test_line_count = 1 ci-filter.log &&
+ cgit_ci "filtered/refs&h=v0.17.3.2" >tmp &&
+ grep "href=./filtered/ci/?h=v0.17.3.2.>ci</a>" tmp
+'
+
+test_expect_success 'ci filter is not consulted without a ci url' '
+ rm -f ci-filter.log &&
+ cgit_url "foo/refs" >tmp &&
+ test_path_is_missing ci-filter.log
+'
+
test_done
diff --git a/tests/t0113-ci-lua.sh b/tests/t0113-ci-lua.sh
new file mode 100644
index 0000000..1e095b1
--- /dev/null
+++ b/tests/t0113-ci-lua.sh
@@ -0,0 +1,78 @@
+#!/bin/sh
+
+test_description='Check ci filter written in Lua'
+. ./setup.sh
+
+if test $CGIT_HAS_LUA -ne 1
+then
+ skip_all='Skipping Lua ci filter tests: Lua support not compiled in'
+ test_done
+ exit
+fi
+
+cat >cgitrc.cilua <<EOF
+virtual-root=/
+cache-root=$PWD/cache
+cache-size=0
+
+ci-url=https://ci.example.org/job/\$slug/job/\$ref
+ci-filter=lua:$PWD/ci-filter.lua
+
+repo.url=foo
+repo.path=$PWD/repos/foo/.git
+EOF
+
+# Accepts tags only, and records its arguments.
+cat >ci-filter.lua <<EOF
+function filter_open(ref, refkind, url)
+ local log = io.open("$PWD/ci-filter.log", "a")
+ log:write(ref .. "|" .. refkind .. "|" .. url .. "\n")
+ log:close()
+ accepted = (refkind == "tag")
+end
+
+function filter_write(buffer)
+end
+
+function filter_close()
+ if accepted then
+ return 0
+ end
+ return 1
+end
+EOF
+
+git --git-dir="$PWD/repos/foo/.git" tag v1.0 master
+
+cgit_ci()
+{
+ CGIT_CONFIG="$PWD/cgitrc.cilua" QUERY_STRING="url=$1" cgit
+}
+
+test_expect_success 'Lua ci filter accepts a tag' '
+ rm -f ci-filter.log &&
+ cgit_ci "foo/ci&h=v1.0" >tmp &&
+ grep "^Location: https://ci.example.org/job/foo/job/v1.0$" tmp &&
+ echo "v1.0|tag|https://ci.example.org/job/foo/job/v1.0" >expected &&
+ test_cmp expected ci-filter.log
+'
+
+test_expect_success 'Lua ci filter rejects a branch' '
+ cgit_ci "foo/ci&h=master" >tmp &&
+ grep "^Status: 404 Not found$" tmp
+'
+
+test_expect_success 'Lua ci filter controls the tab' '
+ cgit_ci "foo/refs&h=v1.0" >tmp &&
+ grep "href=./foo/ci/?h=v1.0.>ci</a>" tmp &&
+ cgit_ci "foo/refs&h=master" >tmp &&
+ ! grep ">ci</a>" tmp
+'
+
+test_expect_success 'page is still rendered correctly around the filter' '
+ cgit_ci "foo/refs&h=v1.0" >tmp &&
+ grep "</html>" tmp &&
+ ! grep "|tag|" tmp
+'
+
+test_done
diff --git a/ui-ci.c b/ui-ci.c
index 1c9f39c..ffd3a65 100644
--- a/ui-ci.c
+++ b/ui-ci.c
@@ -73,9 +73,24 @@ static char *expand_ci_url(const char *template)
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;
+ const char *template, *refkind;
if (ci_resolved != -1)
return;
@@ -84,16 +99,24 @@ static void resolve_ci_url(void)
if (!ctx.repo || !ctx.qry.head || !cgit_have_repository())
return;
- if (ref_is_tag(ctx.qry.head))
+ if (ref_is_tag(ctx.qry.head)) {
+ refkind = "tag";
template = ctx.repo->ci_tag_url;
- else
+ } 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;
}