summaryrefslogtreecommitdiffstats
path: root/.gitlab-ci
diff options
context:
space:
mode:
Diffstat (limited to '.gitlab-ci')
-rw-r--r--.gitlab-ci/check-potfiles.js207
-rwxr-xr-x.gitlab-ci/check-potfiles.sh38
-rwxr-xr-x.gitlab-ci/checkout-mutter.sh70
-rw-r--r--.gitlab-ci/commit-rules.yml16
-rwxr-xr-x.gitlab-ci/download-coverity-tarball.sh38
-rwxr-xr-x.gitlab-ci/install-meson-project.sh82
-rwxr-xr-x.gitlab-ci/run-eslint128
7 files changed, 579 insertions, 0 deletions
diff --git a/.gitlab-ci/check-potfiles.js b/.gitlab-ci/check-potfiles.js
new file mode 100644
index 0000000..0c8885e
--- /dev/null
+++ b/.gitlab-ci/check-potfiles.js
@@ -0,0 +1,207 @@
+const gettextFuncs = new Set([
+ '_',
+ 'N_',
+ 'C_',
+ 'NC_',
+ 'dcgettext',
+ 'dgettext',
+ 'dngettext',
+ 'dpgettext',
+ 'gettext',
+ 'ngettext',
+ 'pgettext',
+]);
+
+function dirname(file) {
+ const split = file.split('/');
+ split.pop();
+ return split.join('/');
+}
+
+const scriptDir = dirname(import.meta.url);
+const root = dirname(scriptDir);
+
+const excludedFiles = new Set();
+const foundFiles = new Set()
+
+function addExcludes(filename) {
+ const contents = os.file.readFile(filename);
+ const lines = contents.split('\n')
+ .filter(l => l && !l.startsWith('#'));
+ lines.forEach(line => excludedFiles.add(line));
+}
+
+addExcludes(`${root}/po/POTFILES.in`);
+addExcludes(`${root}/po/POTFILES.skip`);
+
+function walkAst(node, func) {
+ func(node);
+ nodesToWalk(node).forEach(n => walkAst(n, func));
+}
+
+function findGettextCalls(node) {
+ switch(node.type) {
+ case 'CallExpression':
+ if (node.callee.type === 'Identifier' &&
+ gettextFuncs.has(node.callee.name))
+ throw new Error();
+ if (node.callee.type === 'MemberExpression' &&
+ node.callee.object.type === 'Identifier' &&
+ node.callee.object.name === 'Gettext' &&
+ node.callee.property.type === 'Identifier' &&
+ gettextFuncs.has(node.callee.property.name))
+ throw new Error();
+ break;
+ }
+ return true;
+}
+
+function nodesToWalk(node) {
+ switch(node.type) {
+ case 'ArrayPattern':
+ case 'BreakStatement':
+ case 'CallSiteObject': // i.e. strings passed to template
+ case 'ContinueStatement':
+ case 'DebuggerStatement':
+ case 'EmptyStatement':
+ case 'Identifier':
+ case 'Literal':
+ case 'MetaProperty': // i.e. new.target
+ case 'Super':
+ case 'ThisExpression':
+ return [];
+ case 'ArrowFunctionExpression':
+ case 'FunctionDeclaration':
+ case 'FunctionExpression':
+ return [...node.defaults, node.body].filter(n => !!n);
+ case 'AssignmentExpression':
+ case 'BinaryExpression':
+ case 'ComprehensionBlock':
+ case 'LogicalExpression':
+ return [node.left, node.right];
+ case 'ArrayExpression':
+ case 'TemplateLiteral':
+ return node.elements.filter(n => !!n);
+ case 'BlockStatement':
+ case 'Program':
+ return node.body;
+ case 'StaticClassBlock':
+ return [node.body];
+ case 'ClassField':
+ return [node.name, node.init];
+ case 'CallExpression':
+ case 'NewExpression':
+ case 'OptionalCallExpression':
+ case 'TaggedTemplate':
+ return [node.callee, ...node.arguments];
+ case 'CatchClause':
+ return [node.body, node.guard].filter(n => !!n);
+ case 'ClassExpression':
+ case 'ClassStatement':
+ return [...node.body, node.superClass].filter(n => !!n);
+ case 'ClassMethod':
+ return [node.name, node.body];
+ case 'ComprehensionExpression':
+ case 'GeneratorExpression':
+ return [node.body, ...node.blocks, node.filter].filter(n => !!n);
+ case 'ComprehensionIf':
+ return [node.test];
+ case 'ComputedName':
+ return [node.name];
+ case 'ConditionalExpression':
+ case 'IfStatement':
+ return [node.test, node.consequent, node.alternate].filter(n => !!n);
+ case 'DoWhileStatement':
+ case 'WhileStatement':
+ return [node.body, node.test];
+ case 'ExportDeclaration':
+ return [node.declaration, node.source].filter(n => !!n);
+ case 'ImportDeclaration':
+ return [...node.specifiers, node.source];
+ case 'LetStatement':
+ return [...node.head, node.body];
+ case 'ExpressionStatement':
+ return [node.expression];
+ case 'ForInStatement':
+ case 'ForOfStatement':
+ return [node.body, node.left, node.right];
+ case 'ForStatement':
+ return [node.init, node.test, node.update, node.body].filter(n => !!n);
+ case 'LabeledStatement':
+ return [node.body];
+ case 'MemberExpression':
+ return [node.object, node.property];
+ case 'ObjectExpression':
+ case 'ObjectPattern':
+ return node.properties;
+ case 'OptionalExpression':
+ return [node.expression];
+ case 'OptionalMemberExpression':
+ return [node.object, node.property];
+ case 'Property':
+ case 'PrototypeMutation':
+ return [node.value];
+ case 'ReturnStatement':
+ case 'ThrowStatement':
+ case 'UnaryExpression':
+ case 'UpdateExpression':
+ case 'YieldExpression':
+ return node.argument ? [node.argument] : [];
+ case 'SequenceExpression':
+ return node.expressions;
+ case 'SpreadExpression':
+ return [node.expression];
+ case 'SwitchCase':
+ return [node.test, ...node.consequent].filter(n => !!n);
+ case 'SwitchStatement':
+ return [node.discriminant, ...node.cases];
+ case 'TryStatement':
+ return [node.block, node.handler, node.finalizer].filter(n => !!n);
+ case 'VariableDeclaration':
+ return node.declarations;
+ case 'VariableDeclarator':
+ return node.init ? [node.init] : [];
+ case 'WithStatement':
+ return [node.object, node.body];
+ default:
+ print(`Ignoring ${node.type}, you should probably fix this in the script`);
+ }
+}
+
+function walkDir(dir) {
+ os.file.listDir(dir).forEach(child => {
+ if (child.startsWith('.'))
+ return;
+
+ const path = os.path.join(dir, child);
+ const relativePath = path.replace(`${root}/`, '');
+ if (excludedFiles.has(relativePath))
+ return;
+
+ if (!child.endsWith('.js')) {
+ try {
+ walkDir(path);
+ } catch (e) {
+ // not a directory
+ }
+ return;
+ }
+
+ try {
+ const script = os.file.readFile(path);
+ const ast = Reflect.parse(script);
+ walkAst(ast, findGettextCalls);
+ } catch (e) {
+ foundFiles.add(path);
+ }
+ });
+}
+
+walkDir(root);
+
+if (foundFiles.size === 0)
+ quit(0);
+
+print('The following files are missing from po/POTFILES.in:')
+foundFiles.forEach(f => print(` ${f}`));
+quit(1);
diff --git a/.gitlab-ci/check-potfiles.sh b/.gitlab-ci/check-potfiles.sh
new file mode 100755
index 0000000..0969da1
--- /dev/null
+++ b/.gitlab-ci/check-potfiles.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/env bash
+
+srcdirs="src subprojects/extensions-tool"
+uidirs="js subprojects/extensions-app"
+desktopdirs="data subprojects/extensions-app/ subprojects/extensions-tool"
+
+# find source files that contain gettext keywords
+files=$(grep -lR --include='*.c' '\(gettext\|[^I_)]_\)(' $srcdirs)
+
+# find ui files that contain translatable string
+files="$files "$(grep -lRi --include='*.ui' 'translatable="[ty1]' $uidirs)
+
+# find .desktop files
+files="$files "$(find $desktopdirs -name '*.desktop*')
+
+# filter out excluded files
+if [ -f po/POTFILES.skip ]; then
+ files=$(for f in $files; do ! grep -q ^$f po/POTFILES.skip && echo $f; done)
+fi
+
+# find those that aren't listed in POTFILES.in
+missing=$(for f in $files; do ! grep -q ^$f po/POTFILES.in && echo $f; done)
+
+if [ ${#missing} -eq 0 ]; then
+ exit 0
+fi
+
+cat >&2 <<EOT
+
+The following files are missing from po/POTFILES.po:
+
+EOT
+for f in $missing; do
+ echo " $f" >&2
+done
+echo >&2
+
+exit 1
diff --git a/.gitlab-ci/checkout-mutter.sh b/.gitlab-ci/checkout-mutter.sh
new file mode 100755
index 0000000..76375fd
--- /dev/null
+++ b/.gitlab-ci/checkout-mutter.sh
@@ -0,0 +1,70 @@
+#!/usr/bin/bash
+
+fetch() {
+ local remote=$1
+ local ref=$2
+
+ git fetch --quiet --depth=1 $remote $ref 2>/dev/null
+}
+
+mutter_target=
+
+echo -n Cloning into mutter ...
+if git clone --quiet --depth=1 https://gitlab.gnome.org/GNOME/mutter.git; then
+ echo \ done
+else
+ echo \ failed
+ exit 1
+fi
+
+cd mutter
+
+if [ "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" ]; then
+ merge_request_remote=${CI_MERGE_REQUEST_SOURCE_PROJECT_URL//gnome-shell/mutter}
+ merge_request_branch=$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME
+
+ echo -n Looking for $merge_request_branch on remote ...
+ if fetch $merge_request_remote $merge_request_branch; then
+ echo \ found
+ mutter_target=FETCH_HEAD
+ else
+ echo \ not found
+
+ echo -n Looking for $CI_MERGE_REQUEST_TARGET_BRANCH_NAME instead ...
+ if fetch origin $CI_MERGE_REQUEST_TARGET_BRANCH_NAME; then
+ echo \ found
+ mutter_target=FETCH_HEAD
+ else
+ echo \ not found
+ fi
+ fi
+fi
+
+if [ -z "$mutter_target" ]; then
+ ref_remote=${CI_PROJECT_URL//gnome-shell/mutter}
+ echo -n Looking for $CI_COMMIT_REF_NAME on remote ...
+ if fetch $ref_remote $CI_COMMIT_REF_NAME; then
+ echo \ found
+ mutter_target=FETCH_HEAD
+ else
+ echo \ not found
+ fi
+fi
+
+fallback_branch=${CI_COMMIT_TAG:+gnome-}${CI_COMMIT_TAG%%.*}
+if [ -z "$mutter_target" -a "$fallback_branch" ]; then
+ echo -n Looking for $fallback_branch instead ...
+ if fetch origin $fallback_branch; then
+ echo \ found
+ mutter_target=FETCH_HEAD
+ else
+ echo \ not found
+ fi
+fi
+
+if [ -z "$mutter_target" ]; then
+ mutter_target=HEAD
+ echo Using $mutter_target instead
+fi
+
+git checkout -q $mutter_target
diff --git a/.gitlab-ci/commit-rules.yml b/.gitlab-ci/commit-rules.yml
new file mode 100644
index 0000000..5828f8a
--- /dev/null
+++ b/.gitlab-ci/commit-rules.yml
@@ -0,0 +1,16 @@
+patterns:
+ deny:
+ - regex: '^$CI_MERGE_REQUEST_PROJECT_URL/(-/)?merge_requests/$CI_MERGE_REQUEST_IID$'
+ message: Commit message must not contain a link to its own merge request
+ - regex: '^(st-|St)'
+ message: Commit message subject should not be prefixed with 'st-' or 'St', use 'st/' instead
+ where: subject
+ - regex: '^[^:]+: [a-z]'
+ message: "Commit message subject should be properly Capitalized. E.g. 'window: Marginalize extradicity'"
+ where: subject
+ - regex: '^\S*\.(js|c|h):'
+ message: Commit message subject prefix should not include .c, .h etc.
+ where: subject
+ - regex: '([^.]\.|[:,;])\s*$'
+ message: Commit message subject should not end with punctuation
+ where: subject
diff --git a/.gitlab-ci/download-coverity-tarball.sh b/.gitlab-ci/download-coverity-tarball.sh
new file mode 100755
index 0000000..e2afc5d
--- /dev/null
+++ b/.gitlab-ci/download-coverity-tarball.sh
@@ -0,0 +1,38 @@
+#!/usr/bin/bash
+
+# We need a coverity token to fetch the tarball
+if [ -x $COVERITY_TOKEN ]
+then
+ echo "No coverity token. Run this job from a protected branch."
+ exit -1
+fi
+
+mkdir -p coverity
+
+# Download and check MD5 first
+curl https://scan.coverity.com/download/linux64 \
+ --data "token=$COVERITY_TOKEN&project=GNOME+Shell&md5=1" \
+ --output /tmp/coverity_tool.md5
+
+diff /tmp/coverity_tool.md5 coverity/coverity_tool.md5 >/dev/null 2>&1
+
+if [ $? -eq 0 -a -d coverity/cov-analysis* ]
+then
+ echo "Coverity tarball is up-to-date"
+ exit 0
+fi
+
+# Download and extract coverity tarball
+curl https://scan.coverity.com/download/linux64 \
+ --data "token=$COVERITY_TOKEN&project=GNOME+Shell" \
+ --output /tmp/coverity_tool.tgz
+
+rm -rf ./coverity/cov-analysis*
+
+tar zxf /tmp/coverity_tool.tgz -C coverity/
+if [ $? -eq 0 ]
+then
+ mv /tmp/coverity_tool.md5 coverity/
+fi
+
+rm /tmp/coverity_tool.tgz
diff --git a/.gitlab-ci/install-meson-project.sh b/.gitlab-ci/install-meson-project.sh
new file mode 100755
index 0000000..8ecf8a3
--- /dev/null
+++ b/.gitlab-ci/install-meson-project.sh
@@ -0,0 +1,82 @@
+#!/bin/bash
+
+set -e
+
+usage() {
+ cat <<-EOF
+ Usage: $(basename $0) [OPTION…] REPO_URL COMMIT
+
+ Check out and install a meson project
+
+ Options:
+ -Dkey=val Option to pass on to meson
+ --subdir Build subdirectory instead of whole project
+ --prepare Script to run before build
+
+ -h, --help Display this help
+
+ EOF
+}
+
+TEMP=$(getopt \
+ --name=$(basename $0) \
+ --options='D:h' \
+ --longoptions='subdir:' \
+ --longoptions='prepare:' \
+ --longoptions='help' \
+ -- "$@")
+
+eval set -- "$TEMP"
+unset TEMP
+
+MESON_OPTIONS=()
+SUBDIR=.
+PREPARE=:
+
+while true; do
+ case "$1" in
+ -D)
+ MESON_OPTIONS+=( -D$2 )
+ shift 2
+ ;;
+
+ --subdir)
+ SUBDIR=$2
+ shift 2
+ ;;
+
+ --prepare)
+ PREPARE=$2
+ shift 2
+ ;;
+
+ -h|--help)
+ usage
+ exit 0
+ ;;
+
+ --)
+ shift
+ break
+ ;;
+ esac
+done
+
+if [[ $# -lt 2 ]]; then
+ usage
+ exit 1
+fi
+
+REPO_URL="$1"
+COMMIT="$2"
+
+CHECKOUT_DIR=$(mktemp --directory)
+trap "rm -rf $CHECKOUT_DIR" EXIT
+
+git clone --depth 1 "$REPO_URL" -b "$COMMIT" "$CHECKOUT_DIR"
+
+pushd "$CHECKOUT_DIR/$SUBDIR"
+sh -c "$PREPARE"
+meson setup --prefix=/usr _build "${MESON_OPTIONS[@]}"
+meson install -C _build
+popd
diff --git a/.gitlab-ci/run-eslint b/.gitlab-ci/run-eslint
new file mode 100755
index 0000000..2a8f60d
--- /dev/null
+++ b/.gitlab-ci/run-eslint
@@ -0,0 +1,128 @@
+#!/usr/bin/env node
+
+const { ESLint } = require('eslint');
+const fs = require('fs');
+const path = require('path');
+const { spawn } = require('child_process');
+
+function createConfig(config) {
+ const options = {
+ cache: true,
+ cacheLocation: `.eslintcache-${config}`,
+ };
+
+ if (config === 'legacy')
+ options.overrideConfigFile='lint/eslintrc-legacy.yml';
+
+ return new ESLint(options);
+}
+
+function git(...args) {
+ const git = spawn('git', args, { stdio: ['ignore', null, 'ignore'] });
+ git.stdout.setEncoding('utf8');
+
+ return new Promise(resolve => {
+ let out = '';
+ git.stdout.on('data', chunk => out += chunk);
+ git.stdout.on('end', () => resolve(out.trim()));
+ });
+}
+
+function createCommon(report1, report2, ignoreColumn=false) {
+ return report1.map(result => {
+ const { filePath, messages } = result;
+ const match =
+ report2.find(r => r.filePath === filePath) || { messages: [] };
+
+ const filteredMessages = messages.filter(
+ msg => match.messages.some(
+ m => m.line === msg.line && (ignoreColumn || m.column === msg.column)));
+
+ const [errorCount, warningCount] = filteredMessages.reduce(
+ ([e, w], msg) => {
+ return [
+ e + Number(msg.severity === 2),
+ w + Number(msg.severity === 1)];
+ }, [0, 0]);
+
+ return {
+ filePath,
+ messages: filteredMessages,
+ errorCount,
+ warningCount,
+ };
+ });
+}
+
+async function getMergeRequestChanges(remote, branch) {
+ await git('fetch', remote, branch);
+ const branchPoint = await git('merge-base', 'HEAD', 'FETCH_HEAD');
+ const diff = await git('diff', '-U0', `${branchPoint}...HEAD`);
+
+ const report = [];
+ let messages = null;
+ for (const line of diff.split('\n')) {
+ if (line.startsWith('+++ b/')) {
+ const filePath = path.resolve(line.substring(6));
+ messages = filePath.endsWith('.js') ? [] : null;
+ if (messages)
+ report.push({ filePath, messages });
+ } else if (messages && line.startsWith('@@ ')) {
+ [, , changes] = line.split(' ');
+ [start, count] = `${changes},1`.split(',').map(i => parseInt(i));
+ for (let i = start; i < start + count; i++)
+ messages.push({ line: i });
+ }
+ }
+
+ return report;
+}
+
+function getOption(...names) {
+ const optIndex =
+ process.argv.findIndex(arg => names.includes(arg)) + 1;
+
+ if (optIndex === 0)
+ return undefined;
+
+ return process.argv[optIndex];
+}
+
+(async function main() {
+ const outputOption = getOption('--output-file', '-o');
+ const outputPath = outputOption ? path.resolve(outputOption) : null;
+
+ const sourceDir = path.dirname(process.argv[1]);
+ process.chdir(path.resolve(sourceDir, '..'));
+
+ const remote = getOption('--remote') || 'origin';
+ const branch = getOption('--branch', '-b');
+
+ const sources = ['js', 'subprojects/extensions-app/js'];
+ const regular = createConfig('regular');
+
+ const ops = [];
+ ops.push(regular.lintFiles(sources));
+ if (branch)
+ ops.push(getMergeRequestChanges(remote, branch));
+ else
+ ops.push(createConfig('legacy').lintFiles(sources));
+
+ const results = await Promise.all(ops);
+ const commonResults = createCommon(...results, branch !== undefined);
+
+ const formatter = await regular.loadFormatter(getOption('--format', '-f'));
+ const resultText = formatter.format(commonResults);
+
+ if (outputPath) {
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
+ fs.writeFileSync(outputPath, resultText);
+ } else {
+ console.log(resultText);
+ }
+
+ process.exitCode = commonResults.some(r => r.errorCount > 0) ? 1 : 0;
+})().catch((error) => {
+ process.exitCode = 1;
+ console.error(error);
+});