diff options
Diffstat (limited to '.gitlab-ci')
-rw-r--r-- | .gitlab-ci/check-potfiles.js | 207 | ||||
-rwxr-xr-x | .gitlab-ci/check-potfiles.sh | 38 | ||||
-rwxr-xr-x | .gitlab-ci/checkout-mutter.sh | 70 | ||||
-rw-r--r-- | .gitlab-ci/commit-rules.yml | 16 | ||||
-rwxr-xr-x | .gitlab-ci/download-coverity-tarball.sh | 38 | ||||
-rwxr-xr-x | .gitlab-ci/install-meson-project.sh | 82 | ||||
-rwxr-xr-x | .gitlab-ci/run-eslint | 128 |
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); +}); |