summaryrefslogtreecommitdiffstats
path: root/.gitlab
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.gitlab-ci.yml289
-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
-rw-r--r--.gitlab/issue_templates/Bug.md55
-rw-r--r--.gitlab/issue_templates/Feature.md30
10 files changed, 953 insertions, 0 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..468549f
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,289 @@
+include:
+ - remote: 'https://gitlab.gnome.org/GNOME/citemplates/raw/HEAD/flatpak/flatpak_ci_initiative.yml'
+ - remote: 'https://gitlab.freedesktop.org/freedesktop/ci-templates/-/raw/34f4ade99434043f88e164933f570301fd18b125/templates/fedora.yml'
+ - remote: 'https://gitlab.freedesktop.org/freedesktop/ci-templates/-/raw/34f4ade99434043f88e164933f570301fd18b125/templates/ci-fairy.yml'
+
+stages:
+ - pre_review
+ - prep
+ - review
+ - build
+ - test
+ - analyze
+ - deploy
+
+default:
+ image: registry.gitlab.gnome.org/gnome/mutter/fedora/36:x86_64-2022-09-01.0
+ # Cancel jobs if newer commits are pushed to the branch
+ interruptible: true
+ # Auto-retry jobs in case of infra failures
+ retry:
+ max: 1
+ when:
+ - 'runner_system_failure'
+ - 'stuck_or_timeout_failure'
+ - 'scheduler_failure'
+ - 'api_failure'
+
+variables:
+ FDO_UPSTREAM_REPO: GNOME/gnome-shell
+ BUNDLE: "extensions-git.flatpak"
+ JS_LOG: "js-report.txt"
+ LINT_LOG: "eslint-report.xml"
+ LINT_MR_LOG: "eslint-mr-report.xml"
+
+workflow:
+ rules:
+ - if: '$CI_MERGE_REQUEST_IID'
+ - if: '$CI_COMMIT_TAG'
+ - if: '$CI_COMMIT_BRANCH'
+
+.pipeline_guard: &pipeline_guard
+ rules:
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
+ - if: '$CI_COMMIT_TAG'
+ - if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
+ - if: '$CI_COMMIT_BRANCH =~ /^gnome-[0-9-]+$/'
+ - when: 'manual'
+
+.gnome-shell.fedora:35:
+ variables:
+ FDO_DISTRIBUTION_VERSION: 35
+ FDO_DISTRIBUTION_TAG: '2022-01-18.0'
+ FDO_DISTRIBUTION_PACKAGES:
+ findutils
+ mozjs91-devel
+ nodejs
+ npm
+ meson
+ pkgconfig(gio-2.0)
+ pkgconfig(gio-unix-2.0)
+ pkgconfig(gnome-autoar-0)
+ pkgconfig(json-glib-1.0)
+ FDO_DISTRIBUTION_EXEC: |
+ # For static analysis with eslint
+ npm install -g eslint eslint-plugin-jsdoc &&
+
+ dnf group install -y 'Development Tools' \
+ 'C Development Tools and Libraries' &&
+
+ ./.gitlab-ci/install-meson-project.sh \
+ --subdir subprojects/extensions-tool/ \
+ --prepare ./generate-translations.sh \
+ -Dman=false \
+ https://gitlab.gnome.org/GNOME/gnome-shell.git \
+ main &&
+
+ dnf clean all
+
+check_commit_log:
+ extends:
+ - .fdo.ci-fairy
+ stage: pre_review
+ variables:
+ GIT_DEPTH: "100"
+ script:
+ - if [[ x"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" != "x" ]] ;
+ then
+ ci-fairy check-commits --junit-xml=commit-message-junit-report.xml ;
+ else
+ echo "Not a merge request" ;
+ fi
+ <<: *pipeline_guard
+ artifacts:
+ expire_in: 1 week
+ paths:
+ - commit-message-junit-report.xml
+ reports:
+ junit: commit-message-junit-report.xml
+
+check-merge-request:
+ extends:
+ - .fdo.ci-fairy
+ stage: pre_review
+ script:
+ - if [[ x"$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" != "x" ]] ;
+ then
+ ci-fairy check-merge-request --require-allow-collaboration --junit-xml=check-merge-request-report.xml ;
+ else
+ echo "Not a merge request" ;
+ fi
+ <<: *pipeline_guard
+ artifacts:
+ expire_in: 1 week
+ paths:
+ - check-merge-request-report.xml
+ reports:
+ junit: check-merge-request-report.xml
+
+build-fedora-container:
+ extends:
+ - .fdo.container-build@fedora@x86_64
+ - .gnome-shell.fedora:35
+ stage: prep
+
+js_check:
+ extends:
+ - .fdo.distribution-image@fedora
+ - .gnome-shell.fedora:35
+ stage: review
+ script:
+ - find js -name '*.js' $(printf "! -wholename %s " $(cat .jscheckignore)) -exec js91 -c '{}' ';' 2>&1 | tee $JS_LOG
+ - (! grep -q . $JS_LOG)
+ artifacts:
+ paths:
+ - ${JS_LOG}
+ when: on_failure
+
+eslint:
+ extends:
+ - .fdo.distribution-image@fedora
+ - .gnome-shell.fedora:35
+ stage: review
+ script:
+ - export NODE_PATH=$(npm root -g)
+ - ./.gitlab-ci/run-eslint --output-file ${LINT_LOG} --format junit
+ artifacts:
+ reports:
+ junit: ${LINT_LOG}
+ when: always
+
+eslint_mr:
+ extends:
+ - .fdo.distribution-image@fedora
+ - .gnome-shell.fedora:35
+ stage: review
+ script:
+ - export NODE_PATH=$(npm root -g)
+ - ./.gitlab-ci/run-eslint --output-file ${LINT_MR_LOG} --format junit
+ --remote ${CI_MERGE_REQUEST_PROJECT_URL}.git
+ --branch ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}
+ only:
+ - merge_requests
+ artifacts:
+ reports:
+ junit: ${LINT_MR_LOG}
+ when: always
+
+potfile_c_check:
+ extends:
+ - .fdo.distribution-image@fedora
+ - .gnome-shell.fedora:35
+ stage: review
+ script:
+ - ./.gitlab-ci/check-potfiles.sh
+
+potfile_js_check:
+ extends:
+ - .fdo.distribution-image@fedora
+ - .gnome-shell.fedora:35
+ stage: review
+ script:
+ - js91 -m .gitlab-ci/check-potfiles.js
+
+build:
+ stage: build
+ needs: ["check_commit_log"]
+ variables:
+ GIT_SUBMODULE_STRATEGY: normal
+ before_script:
+ - .gitlab-ci/checkout-mutter.sh
+ - meson mutter mutter/build --prefix=/usr
+ - meson install -C mutter/build
+ script:
+ - meson . build -Dbuildtype=debugoptimized -Dman=false --werror
+ - meson compile -C build
+ - meson install -C build
+ artifacts:
+ expire_in: 1 day
+ paths:
+ - mutter
+ - build
+
+test:
+ stage: test
+ needs: ["build"]
+ variables:
+ GIT_SUBMODULE_STRATEGY: normal
+ XDG_RUNTIME_DIR: "$CI_PROJECT_DIR/runtime-dir"
+ NO_AT_BRIDGE: "1"
+ before_script:
+ - meson install -C mutter/build
+ script:
+ - dbus-run-session -- xvfb-run meson test -C build --no-rebuild
+ artifacts:
+ expire_in: 1 day
+ paths:
+ - build/meson-logs/testlog.txt
+ reports:
+ junit: build/meson-logs/testlog.junit.xml
+ when: on_failure
+
+test-coverity:
+ rules:
+ - if: '$CI_PIPELINE_SOURCE == "schedule" && $GNOME_SHELL_SCHEDULED_JOB == "coverity"'
+ when: always
+ - when: manual
+ needs: ["build"]
+ stage: analyze
+ allow_failure: true
+ variables:
+ GIT_SUBMODULE_STRATEGY: normal
+ before_script:
+ - meson install -C mutter/build
+ script:
+ - .gitlab-ci/download-coverity-tarball.sh
+ - CC=clang meson coverity-build -Dman=false
+ - ./coverity/cov-analysis-linux64-*/bin/cov-build --fs-capture-search js --dir cov-int meson compile -C coverity-build
+ - tar czf cov-int.tar.gz cov-int
+ - curl https://scan.coverity.com/builds?project=GNOME+Shell
+ --form token=$COVERITY_TOKEN --form email=carlosg@gnome.org
+ --form file=@cov-int.tar.gz --form version="`git describe --tags`"
+ --form description="GitLab CI build"
+ cache:
+ key: coverity-tarball
+ paths:
+ - coverity
+
+flatpak:
+ stage: build
+ needs: ["check_commit_log"]
+ variables:
+ SUBPROJECT: "subprojects/extensions-app"
+ # Your manifest path
+ MANIFEST_PATH: "$SUBPROJECT/build-aux/flatpak/org.gnome.Extensions.json"
+ RUNTIME_REPO: "https://nightly.gnome.org/gnome-nightly.flatpakrepo"
+ FLATPAK_MODULE: "gnome-extensions-app"
+ APP_ID: "org.gnome.Extensions.Devel"
+ extends: .flatpak
+
+nightly:
+ extends: '.publish_nightly'
+
+dist:
+ variables:
+ XDG_RUNTIME_DIR: "$CI_PROJECT_DIR/runtime-dir"
+ NO_AT_BRIDGE: "1"
+ GIT_SUBMODULE_STRATEGY: normal
+ stage: deploy
+ needs: ["build"]
+ before_script:
+ - meson install -C mutter/build
+ - mkdir -m 700 $XDG_RUNTIME_DIR
+ script:
+ - dbus-run-session xvfb-run meson dist -C build
+ rules:
+ - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
+ changes:
+ - "**/meson.build"
+ - meson/*
+
+
+dist-tarball:
+ extends: dist
+ artifacts:
+ expose_as: 'Get tarball here'
+ paths:
+ - build/meson-dist/$CI_PROJECT_NAME-$CI_COMMIT_TAG.tar.xz
+ rules:
+ - if: '$CI_COMMIT_TAG'
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);
+});
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
new file mode 100644
index 0000000..2043fe4
--- /dev/null
+++ b/.gitlab/issue_templates/Bug.md
@@ -0,0 +1,55 @@
+<!--
+Please read https://wiki.gnome.org/Community/GettingInTouch/BugReportingGuidelines
+first to ensure that you create a clear and specific issue.
+-->
+
+### Affected version
+
+<!--
+Provide at least the following information:
+* Your OS and version
+* Affected GNOME Shell version (see https://wiki.gnome.org/Schedule for currently supported versions)
+* Does this issue appear in XOrg and/or Wayland
+-->
+
+### Bug summary
+
+<!--
+Provide a short summary of the bug you encountered.
+-->
+
+### Steps to reproduce
+
+<!--
+1. Step one
+2. Step two
+3. ...
+-->
+
+### What happened
+
+<!--
+What did GNOME Shell do that was unexpected?
+-->
+
+### What did you expect to happen
+
+<!--
+What did you expect GNOME Shell to do?
+-->
+
+### Relevant logs, screenshots, screencasts etc.
+
+<!--
+If you have further information, such as technical documentation, logs,
+screenshots or screencasts related, please provide them here.
+
+If the bug is a crash, please obtain a stack trace with installed debug
+symbols (at least for GNOME Shell and Mutter) and attach it to
+this issue following the instructions on
+https://wiki.gnome.org/Community/GettingInTouch/Bugzilla/GettingTraces.
+-->
+
+
+<!-- Do not remove the following line. -->
+/label ~"1. Bug"
diff --git a/.gitlab/issue_templates/Feature.md b/.gitlab/issue_templates/Feature.md
new file mode 100644
index 0000000..8443b5a
--- /dev/null
+++ b/.gitlab/issue_templates/Feature.md
@@ -0,0 +1,30 @@
+<!--
+Please read https://wiki.gnome.org/Community/GettingInTouch/BugReportingGuidelines
+first to ensure that you create a clear and specific issue.
+-->
+
+### Feature summary
+
+<!--
+Describe what you would like to be able to do with GNOME Shell
+that you currently cannot do.
+-->
+
+### How would you like it to work
+
+<!--
+If you can think of a way GNOME Shell might be able to do this,
+let us know here.
+-->
+
+### Relevant links, screenshots, screencasts etc.
+
+<!--
+If you have further information, such as technical documentation,
+code, mockups or a similar feature in another desktop environments,
+please provide them here.
+-->
+
+
+<!-- Do not remove the following line. -->
+/label ~"1. Feature"