diff --git a/.flayignore b/.flayignore
index 87cb3507b0598e16a70ac89790c9e27d29e5a77d..3d69bb2c985b36cb0c59db7e69fcc5e7802e53f7 100644
--- a/.flayignore
+++ b/.flayignore
@@ -8,3 +8,4 @@ lib/gitlab/redis/*.rb
 lib/gitlab/gitaly_client/operation_service.rb
 lib/gitlab/background_migration/*
 app/models/project_services/kubernetes_service.rb
+lib/gitlab/workhorse.rb
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 70f41e4dc981bc43bba2b21faf83752b69cbbe3d..4890738aa3db223f46256f5183e028602db96b77 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -264,8 +264,17 @@ package-and-qa:
   stage: build
   cache: {}
   when: manual
+  variables:
+    GIT_STRATEGY: none
+  before_script:
+    # We need to download the script rather than clone the repo since the
+    # package-and-qa job will not be able to run when the branch gets
+    # deleted (when merging the MR).
+    - apk add --update openssl
+    - wget https://gitlab.com/gitlab-org/gitlab-ce/raw/$CI_COMMIT_SHA/scripts/trigger-build-omnibus
+    - chmod 755 trigger-build-omnibus
   script:
-    - scripts/trigger-build-omnibus
+    - ./trigger-build-omnibus
   only:
     - //@gitlab-org/gitlab-ce
     - //@gitlab-org/gitlab-ee
diff --git a/CHANGELOG.md b/CHANGELOG.md
index adb0ec9f5b16e89c8f7e50d05070d4c2bb05bd26..8a90a7fcdc2cca4d9beeb2f368b87bb58663a4f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,14 @@
 documentation](doc/development/changelog.md) for instructions on adding your own
 entry.
 
+## 10.6.2 (2018-03-29)
+
+### Fixed (2 changes, 1 of them is from the community)
+
+- Don't capture trailing punctuation when autolinking. !17965
+- Cloning a repository over HTTPS with LDAP credentials causes a HTTP 401 Access denied. (Horatiu Eugen Vlad)
+
+
 ## 10.6.1 (2018-03-27)
 
 ### Security (1 change)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 8f63f4f9a10c8a2525dddeff0809e7b5d34beac7..36545ad338e5f19fa893efe276125089bc8dd932 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.91.0
+0.92.0
diff --git a/Gemfile b/Gemfile
index 035c86efff3b436dfd1581d99a2c9de3562aec6d..fd174a60c666a03d24579dd244c0c2baf5d06df2 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,7 +6,6 @@ end
 gem_versions = {}
 gem_versions['activerecord_sane_schema_dumper'] = rails5? ? '1.0'      : '0.2'
 gem_versions['default_value_for']               = rails5? ? '~> 3.0.5' : '~> 3.0.0'
-gem_versions['html-pipeline']                   = rails5? ? '~> 2.6.0' : '~> 1.11.0'
 gem_versions['rails']                           = rails5? ? '5.0.6'    : '4.2.10'
 gem_versions['rails-i18n']                      = rails5? ? '~> 5.1'   : '~> 4.0.9'
 # --- The end of special code for migrating to Rails 5.0 ---
@@ -28,7 +27,7 @@ gem 'default_value_for', gem_versions['default_value_for']
 gem 'mysql2', '~> 0.4.10', group: :mysql
 gem 'pg', '~> 0.18.2', group: :postgres
 
-gem 'rugged', '~> 0.26.0'
+gem 'rugged', '~> 0.27'
 gem 'grape-route-helpers', '~> 2.1.0'
 
 gem 'faraday', '~> 0.12'
@@ -44,7 +43,7 @@ gem 'omniauth-cas3', '~> 1.1.4'
 gem 'omniauth-facebook', '~> 4.0.0'
 gem 'omniauth-github', '~> 1.1.1'
 gem 'omniauth-gitlab', '~> 1.0.2'
-gem 'omniauth-google-oauth2', '~> 0.5.2'
+gem 'omniauth-google-oauth2', '~> 0.5.3'
 gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
 gem 'omniauth-oauth2-generic', '~> 0.2.2'
 gem 'omniauth-saml', '~> 1.10'
@@ -136,7 +135,7 @@ gem 'unf', '~> 0.1.4'
 gem 'seed-fu', '~> 2.3.7'
 
 # Markdown and HTML processing
-gem 'html-pipeline', gem_versions['html-pipeline']
+gem 'html-pipeline', '~> 2.7.1'
 gem 'deckar01-task_list', '2.0.0'
 gem 'gitlab-markup', '~> 1.6.2'
 gem 'redcarpet', '~> 3.4'
@@ -310,7 +309,7 @@ end
 
 group :development do
   gem 'foreman', '~> 0.84.0'
-  gem 'brakeman', '~> 3.6.0', require: false
+  gem 'brakeman', '~> 4.2', require: false
 
   gem 'letter_opener_web', '~> 1.3.0'
   gem 'rblineprof', '~> 0.3.6', platform: :mri, require: false
@@ -376,6 +375,8 @@ group :development, :test do
   gem 'stackprof', '~> 0.2.10', require: false
 
   gem 'simple_po_parser', '~> 1.1.2', require: false
+
+  gem 'timecop', '~> 0.8.0'
 end
 
 group :test do
@@ -385,7 +386,6 @@ group :test do
   gem 'webmock', '~> 2.3.2'
   gem 'test_after_commit', '~> 1.1'
   gem 'sham_rack', '~> 1.3.6'
-  gem 'timecop', '~> 0.8.0'
   gem 'concurrent-ruby', '~> 1.0.5'
   gem 'test-prof', '~> 0.2.5'
 end
@@ -421,7 +421,7 @@ group :ed25519 do
 end
 
 # Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.91.0', require: 'gitaly'
 gem 'grpc', '~> 1.10.0'
 
 # Locked until https://github.com/google/protobuf/issues/4210 is closed
diff --git a/Gemfile.lock b/Gemfile.lock
index 7d8b22359b2a2e161efd52384a94e3a685143e82..55e7bd9492aed3a67b664e92295a21b7b3239bbd 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -95,7 +95,7 @@ GEM
       autoprefixer-rails (>= 5.2.1)
       sass (>= 3.3.4)
     bootstrap_form (2.7.0)
-    brakeman (3.6.1)
+    brakeman (4.2.1)
     browser (2.2.0)
     builder (3.2.3)
     bullet (5.5.1)
@@ -120,7 +120,7 @@ GEM
       activesupport (>= 4.0.0)
       mime-types (>= 1.16)
     cause (0.1)
-    charlock_holmes (0.7.5)
+    charlock_holmes (0.7.6)
     childprocess (0.7.0)
       ffi (~> 1.0, >= 1.0.11)
     chronic (0.10.2)
@@ -290,7 +290,7 @@ GEM
       po_to_json (>= 1.0.0)
       rails (>= 3.2.0)
     gherkin-ruby (0.3.2)
-    gitaly-proto (0.88.0)
+    gitaly-proto (0.91.0)
       google-protobuf (~> 3.1)
       grpc (~> 1.0)
     github-linguist (5.3.3)
@@ -399,9 +399,9 @@ GEM
     hipchat (1.5.2)
       httparty
       mimemagic
-    html-pipeline (1.11.0)
+    html-pipeline (2.7.1)
       activesupport (>= 2)
-      nokogiri (~> 1.4)
+      nokogiri (>= 1.4)
     html2text (0.2.0)
       nokogiri (~> 1.6)
     htmlentities (4.3.4)
@@ -550,11 +550,10 @@ GEM
     omniauth-gitlab (1.0.2)
       omniauth (~> 1.0)
       omniauth-oauth2 (~> 1.0)
-    omniauth-google-oauth2 (0.5.2)
-      jwt (~> 1.5)
-      multi_json (~> 1.3)
+    omniauth-google-oauth2 (0.5.3)
+      jwt (>= 1.5)
       omniauth (>= 1.1.1)
-      omniauth-oauth2 (>= 1.3.1)
+      omniauth-oauth2 (>= 1.5)
     omniauth-jwt (0.0.2)
       jwt
       omniauth (~> 1.1)
@@ -566,8 +565,8 @@ GEM
     omniauth-oauth (1.1.0)
       oauth
       omniauth (~> 1.0)
-    omniauth-oauth2 (1.4.0)
-      oauth2 (~> 1.0)
+    omniauth-oauth2 (1.5.0)
+      oauth2 (~> 1.1)
       omniauth (~> 1.2)
     omniauth-oauth2-generic (0.2.2)
       omniauth-oauth2 (~> 1.0)
@@ -814,7 +813,7 @@ GEM
     rubyzip (1.2.1)
     rufus-scheduler (3.4.0)
       et-orbi (~> 1.0)
-    rugged (0.26.0)
+    rugged (0.27.0)
     safe_yaml (1.0.4)
     sanitize (2.1.0)
       nokogiri (>= 1.4.4)
@@ -1013,7 +1012,7 @@ DEPENDENCIES
   binding_of_caller (~> 0.7.2)
   bootstrap-sass (~> 3.3.0)
   bootstrap_form (~> 2.7.0)
-  brakeman (~> 3.6.0)
+  brakeman (~> 4.2)
   browser (~> 2.2)
   bullet (~> 5.5.0)
   bundler-audit (~> 0.5.0)
@@ -1062,7 +1061,7 @@ DEPENDENCIES
   gettext (~> 3.2.2)
   gettext_i18n_rails (~> 1.8.0)
   gettext_i18n_rails_js (~> 1.3)
-  gitaly-proto (~> 0.88.0)
+  gitaly-proto (~> 0.91.0)
   github-linguist (~> 5.3.3)
   gitlab-flowdock-git-hook (~> 1.0.1)
   gitlab-markup (~> 1.6.2)
@@ -1084,7 +1083,7 @@ DEPENDENCIES
   hashie-forbidden_attributes
   health_check (~> 2.6.0)
   hipchat (~> 1.5.0)
-  html-pipeline (~> 1.11.0)
+  html-pipeline (~> 2.7.1)
   html2text
   httparty (~> 0.13.3)
   influxdb (~> 0.2)
@@ -1118,7 +1117,7 @@ DEPENDENCIES
   omniauth-facebook (~> 4.0.0)
   omniauth-github (~> 1.1.1)
   omniauth-gitlab (~> 1.0.2)
-  omniauth-google-oauth2 (~> 0.5.2)
+  omniauth-google-oauth2 (~> 0.5.3)
   omniauth-jwt (~> 0.0.2)
   omniauth-kerberos (~> 0.3.0)
   omniauth-oauth2-generic (~> 0.2.2)
@@ -1174,7 +1173,7 @@ DEPENDENCIES
   ruby-prof (~> 0.17.0)
   ruby_parser (~> 3.8)
   rufus-scheduler (~> 3.4)
-  rugged (~> 0.26.0)
+  rugged (~> 0.27)
   sanitize (~> 2.0)
   sass-rails (~> 5.0.6)
   scss_lint (~> 0.56.0)
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_canceled.ico b/app/assets/images/ci_favicons/canary/favicon_status_canceled.ico
new file mode 100644
index 0000000000000000000000000000000000000000..48b1095370d446df1f3107565b1c5e72c0128c7d
Binary files /dev/null and b/app/assets/images/ci_favicons/canary/favicon_status_canceled.ico differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_created.ico b/app/assets/images/ci_favicons/canary/favicon_status_created.ico
new file mode 100644
index 0000000000000000000000000000000000000000..623c728faf6981b0b21af20b6b351ddd277fffb7
Binary files /dev/null and b/app/assets/images/ci_favicons/canary/favicon_status_created.ico differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_failed.ico b/app/assets/images/ci_favicons/canary/favicon_status_failed.ico
new file mode 100644
index 0000000000000000000000000000000000000000..3073fe5a761ba1825bfbab4337a0eacf36b848c9
Binary files /dev/null and b/app/assets/images/ci_favicons/canary/favicon_status_failed.ico differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_manual.ico b/app/assets/images/ci_favicons/canary/favicon_status_manual.ico
new file mode 100644
index 0000000000000000000000000000000000000000..6c713d7b67557ea33f2b8cfccd51f3343c29fdbb
Binary files /dev/null and b/app/assets/images/ci_favicons/canary/favicon_status_manual.ico differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_not_found.ico b/app/assets/images/ci_favicons/canary/favicon_status_not_found.ico
new file mode 100644
index 0000000000000000000000000000000000000000..dbf855fdafd3b9465e3df615223739b2c03c1abf
Binary files /dev/null and b/app/assets/images/ci_favicons/canary/favicon_status_not_found.ico differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_pending.ico b/app/assets/images/ci_favicons/canary/favicon_status_pending.ico
new file mode 100644
index 0000000000000000000000000000000000000000..ccd00606aeb39546a35b0fc902765d0a085f394d
Binary files /dev/null and b/app/assets/images/ci_favicons/canary/favicon_status_pending.ico differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_running.ico b/app/assets/images/ci_favicons/canary/favicon_status_running.ico
new file mode 100644
index 0000000000000000000000000000000000000000..968e7c4c2d4a95852ef4215b88ea9a2c030e8c28
Binary files /dev/null and b/app/assets/images/ci_favicons/canary/favicon_status_running.ico differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_skipped.ico b/app/assets/images/ci_favicons/canary/favicon_status_skipped.ico
new file mode 100644
index 0000000000000000000000000000000000000000..7e3be35cc3a5f869c574290f58ca1bbc6f4095ef
Binary files /dev/null and b/app/assets/images/ci_favicons/canary/favicon_status_skipped.ico differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_success.ico b/app/assets/images/ci_favicons/canary/favicon_status_success.ico
new file mode 100644
index 0000000000000000000000000000000000000000..a1fb6e91d653f60418ab8d1c1925b7f99aa69c2f
Binary files /dev/null and b/app/assets/images/ci_favicons/canary/favicon_status_success.ico differ
diff --git a/app/assets/images/ci_favicons/canary/favicon_status_warning.ico b/app/assets/images/ci_favicons/canary/favicon_status_warning.ico
new file mode 100644
index 0000000000000000000000000000000000000000..5d931619fb29fc424d6506565e160709e12e1e8a
Binary files /dev/null and b/app/assets/images/ci_favicons/canary/favicon_status_warning.ico differ
diff --git a/app/assets/images/favicon-yellow.ico b/app/assets/images/favicon-yellow.ico
new file mode 100644
index 0000000000000000000000000000000000000000..b650f277fb66d6193e27e1f2bfe59f751159270b
Binary files /dev/null and b/app/assets/images/favicon-yellow.ico differ
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index cbcefb2c18f49e27bf86fb3c5bb529df4fb615b3..8ad3d18b30223b5cfbf01df6391e9bec54173fbe 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -10,6 +10,9 @@ const Api = {
   projectsPath: '/api/:version/projects.json',
   projectPath: '/api/:version/projects/:id',
   projectLabelsPath: '/:namespace_path/:project_path/labels',
+  mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
+  mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
+  mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
   groupLabelsPath: '/groups/:namespace_path/-/labels',
   licensePath: '/api/:version/templates/licenses/:key',
   gitignorePath: '/api/:version/templates/gitignores/:key',
@@ -22,25 +25,27 @@ const Api = {
   createBranchPath: '/api/:version/projects/:id/repository/branches',
 
   group(groupId, callback) {
-    const url = Api.buildUrl(Api.groupPath)
-      .replace(':id', groupId);
-    return axios.get(url)
-      .then(({ data }) => {
-        callback(data);
+    const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
+    return axios.get(url).then(({ data }) => {
+      callback(data);
 
-        return data;
-      });
+      return data;
+    });
   },
 
   // Return groups list. Filtered by query
   groups(query, options, callback = $.noop) {
     const url = Api.buildUrl(Api.groupsPath);
-    return axios.get(url, {
-      params: Object.assign({
-        search: query,
-        per_page: 20,
-      }, options),
-    })
+    return axios
+      .get(url, {
+        params: Object.assign(
+          {
+            search: query,
+            per_page: 20,
+          },
+          options,
+        ),
+      })
       .then(({ data }) => {
         callback(data);
 
@@ -51,12 +56,13 @@ const Api = {
   // Return namespaces list. Filtered by query
   namespaces(query, callback) {
     const url = Api.buildUrl(Api.namespacesPath);
-    return axios.get(url, {
-      params: {
-        search: query,
-        per_page: 20,
-      },
-    })
+    return axios
+      .get(url, {
+        params: {
+          search: query,
+          per_page: 20,
+        },
+      })
       .then(({ data }) => callback(data));
   },
 
@@ -73,9 +79,10 @@ const Api = {
       defaults.membership = true;
     }
 
-    return axios.get(url, {
-      params: Object.assign(defaults, options),
-    })
+    return axios
+      .get(url, {
+        params: Object.assign(defaults, options),
+      })
       .then(({ data }) => {
         callback(data);
 
@@ -85,8 +92,32 @@ const Api = {
 
   // Return single project
   project(projectPath) {
-    const url = Api.buildUrl(Api.projectPath)
-            .replace(':id', encodeURIComponent(projectPath));
+    const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
+
+    return axios.get(url);
+  },
+
+  // Return Merge Request for project
+  mergeRequest(projectPath, mergeRequestId) {
+    const url = Api.buildUrl(Api.mergeRequestPath)
+      .replace(':id', encodeURIComponent(projectPath))
+      .replace(':mrid', mergeRequestId);
+
+    return axios.get(url);
+  },
+
+  mergeRequestChanges(projectPath, mergeRequestId) {
+    const url = Api.buildUrl(Api.mergeRequestChangesPath)
+      .replace(':id', encodeURIComponent(projectPath))
+      .replace(':mrid', mergeRequestId);
+
+    return axios.get(url);
+  },
+
+  mergeRequestVersions(projectPath, mergeRequestId) {
+    const url = Api.buildUrl(Api.mergeRequestVersionsPath)
+      .replace(':id', encodeURIComponent(projectPath))
+      .replace(':mrid', mergeRequestId);
 
     return axios.get(url);
   },
@@ -102,30 +133,30 @@ const Api = {
       url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
     }
 
-    return axios.post(url, {
-      label: data,
-    })
+    return axios
+      .post(url, {
+        label: data,
+      })
       .then(res => callback(res.data))
       .catch(e => callback(e.response.data));
   },
 
   // Return group projects list. Filtered by query
   groupProjects(groupId, query, callback) {
-    const url = Api.buildUrl(Api.groupProjectsPath)
-      .replace(':id', groupId);
-    return axios.get(url, {
-      params: {
-        search: query,
-        per_page: 20,
-      },
-    })
+    const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
+    return axios
+      .get(url, {
+        params: {
+          search: query,
+          per_page: 20,
+        },
+      })
       .then(({ data }) => callback(data));
   },
 
   commitMultiple(id, data) {
     // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
-    const url = Api.buildUrl(Api.commitPath)
-      .replace(':id', encodeURIComponent(id));
+    const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id));
     return axios.post(url, JSON.stringify(data), {
       headers: {
         'Content-Type': 'application/json; charset=utf-8',
@@ -136,39 +167,34 @@ const Api = {
   branchSingle(id, branch) {
     const url = Api.buildUrl(Api.branchSinglePath)
       .replace(':id', encodeURIComponent(id))
-      .replace(':branch', branch);
+      .replace(':branch', encodeURIComponent(branch));
 
     return axios.get(url);
   },
 
   // Return text for a specific license
   licenseText(key, data, callback) {
-    const url = Api.buildUrl(Api.licensePath)
-      .replace(':key', key);
-    return axios.get(url, {
-      params: data,
-    })
+    const url = Api.buildUrl(Api.licensePath).replace(':key', key);
+    return axios
+      .get(url, {
+        params: data,
+      })
       .then(res => callback(res.data));
   },
 
   gitignoreText(key, callback) {
-    const url = Api.buildUrl(Api.gitignorePath)
-      .replace(':key', key);
-    return axios.get(url)
-      .then(({ data }) => callback(data));
+    const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
+    return axios.get(url).then(({ data }) => callback(data));
   },
 
   gitlabCiYml(key, callback) {
-    const url = Api.buildUrl(Api.gitlabCiYmlPath)
-      .replace(':key', key);
-    return axios.get(url)
-      .then(({ data }) => callback(data));
+    const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
+    return axios.get(url).then(({ data }) => callback(data));
   },
 
   dockerfileYml(key, callback) {
     const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
-    return axios.get(url)
-      .then(({ data }) => callback(data));
+    return axios.get(url).then(({ data }) => callback(data));
   },
 
   issueTemplate(namespacePath, projectPath, key, type, callback) {
@@ -177,7 +203,8 @@ const Api = {
       .replace(':type', type)
       .replace(':project_path', projectPath)
       .replace(':namespace_path', namespacePath);
-    return axios.get(url)
+    return axios
+      .get(url)
       .then(({ data }) => callback(null, data))
       .catch(callback);
   },
@@ -185,10 +212,13 @@ const Api = {
   users(query, options) {
     const url = Api.buildUrl(this.usersPath);
     return axios.get(url, {
-      params: Object.assign({
-        search: query,
-        per_page: 20,
-      }, options),
+      params: Object.assign(
+        {
+          search: query,
+          per_page: 20,
+        },
+        options,
+      ),
     });
   },
 
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index 7dcf1aeed17bb15cc760c1135254d642d04e49c2..eb4e59d12b148d99abd45c81d71e4ae5f41044ff 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -31,7 +31,7 @@ export default function renderMath($els) {
   if (!$els.length) return;
   Promise.all([
     import(/* webpackChunkName: 'katex' */ 'katex'),
-    import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.css'),
+    import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
   ]).then(([katex]) => {
     renderWithKaTeX($els, katex);
   }).catch(() => flash(__('An error occurred while rendering KaTeX')));
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 8259133c95bc3ae2381d3e4f71121fc7ef1b8c7f..7e9770a9ea2ff93228cca243fd1d312344981965 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -54,6 +54,7 @@ class GfmAutoComplete {
       alias: 'commands',
       searchKey: 'search',
       skipSpecialCharacterTest: true,
+      skipMarkdownCharacterTest: true,
       data: GfmAutoComplete.defaultLoadingData,
       displayTpl(value) {
         if (GfmAutoComplete.isLoading(value)) return GfmAutoComplete.Loading.template;
@@ -376,15 +377,23 @@ class GfmAutoComplete {
         return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
       },
       beforeInsert(value) {
-        let resultantValue = value;
+        let withoutAt = value.substring(1);
+        const at = value.charAt();
+
         if (value && !this.setting.skipSpecialCharacterTest) {
-          const withoutAt = value.substring(1);
-          const regex = value.charAt() === '~' ? /\W|^\d+$/ : /\W/;
+          const regex = at === '~' ? /\W|^\d+$/ : /\W/;
           if (withoutAt && regex.test(withoutAt)) {
-            resultantValue = `${value.charAt()}"${withoutAt}"`;
+            withoutAt = `"${withoutAt}"`;
           }
         }
-        return resultantValue;
+
+        // We can ignore this for quick actions because they are processed
+        // before Markdown.
+        if (!this.setting.skipMarkdownCharacterTest) {
+          withoutAt = withoutAt.replace(/([~\-_*`])/g, '\\$&');
+        }
+
+        return `${at}${withoutAt}`;
       },
       matcher(flag, subtext) {
         const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 0c54c992e51e076d55ab8fc1edbd3029ed81cce0..037e3efb4ce687f3dd0343b89e7fb92932adbbbc 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -1,25 +1,25 @@
 <script>
-  import icon from '~/vue_shared/components/icon.vue';
+import icon from '~/vue_shared/components/icon.vue';
 
-  export default {
-    components: {
-      icon,
+export default {
+  components: {
+    icon,
+  },
+  props: {
+    file: {
+      type: Object,
+      required: true,
     },
-    props: {
-      file: {
-        type: Object,
-        required: true,
-      },
+  },
+  computed: {
+    changedIcon() {
+      return this.file.tempFile ? 'file-addition' : 'file-modified';
     },
-    computed: {
-      changedIcon() {
-        return this.file.tempFile ? 'file-addition' : 'file-modified';
-      },
-      changedIconClass() {
-        return `multi-${this.changedIcon}`;
-      },
+    changedIconClass() {
+      return `multi-${this.changedIcon}`;
     },
-  };
+  },
+};
 </script>
 
 <template>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 170347881e0f20539a44454f1d44bcbf85416a55..0c44a755f56f3dfafa39e630d86107a81b7dc1a0 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,31 +1,44 @@
 <script>
-  import Icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __, sprintf } from '~/locale';
 
-  export default {
-    components: {
-      Icon,
+export default {
+  components: {
+    Icon,
+  },
+  props: {
+    hasChanges: {
+      type: Boolean,
+      required: false,
+      default: false,
     },
-    props: {
-      hasChanges: {
-        type: Boolean,
-        required: false,
-        default: false,
-      },
-      viewer: {
-        type: String,
-        required: true,
-      },
-      showShadow: {
-        type: Boolean,
-        required: true,
-      },
+    mergeRequestId: {
+      type: String,
+      required: false,
+      default: '',
     },
-    methods: {
-      changeMode(mode) {
-        this.$emit('click', mode);
-      },
+    viewer: {
+      type: String,
+      required: true,
     },
-  };
+    showShadow: {
+      type: Boolean,
+      required: true,
+    },
+  },
+  computed: {
+    mergeReviewLine() {
+      return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
+        mergeRequestId: this.mergeRequestId,
+      });
+    },
+  },
+  methods: {
+    changeMode(mode) {
+      this.$emit('click', mode);
+    },
+  },
+};
 </script>
 
 <template>
@@ -43,7 +56,10 @@
       }"
       data-toggle="dropdown"
     >
-      <template v-if="viewer === 'editor'">
+      <template v-if="viewer === 'mrdiff' && mergeRequestId">
+        {{ mergeReviewLine }}
+      </template>
+      <template v-else-if="viewer === 'editor'">
         {{ __('Editing') }}
       </template>
       <template v-else>
@@ -57,6 +73,29 @@
     </button>
     <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
       <ul>
+        <template v-if="mergeRequestId">
+          <li>
+            <a
+              href="#"
+              @click.prevent="changeMode('mrdiff')"
+              :class="{
+                'is-active': viewer === 'mrdiff',
+              }"
+            >
+              <strong class="dropdown-menu-inner-title">
+                {{ mergeReviewLine }}
+              </strong>
+              <span class="dropdown-menu-inner-content">
+                {{ __('Compare changes with the merge request target branch') }}
+              </span>
+            </a>
+          </li>
+          <li
+            role="separator"
+            class="divider"
+          >
+          </li>
+        </template>
         <li>
           <a
             href="#"
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 1581e015c39a00e1aaa45c45a67727c44ad10a1c..d22869466c9ce4fb7c1cba93b48bb6060473b372 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -31,7 +31,7 @@ export default {
     },
   },
   computed: {
-    ...mapState(['changedFiles', 'openFiles', 'viewer']),
+    ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
     ...mapGetters(['activeFile', 'hasChanges']),
   },
   mounted() {
@@ -64,6 +64,7 @@ export default {
           :files="openFiles"
           :viewer="viewer"
           :has-changes="hasChanges"
+          :merge-request-id="currentMergeRequestId"
         />
         <repo-editor
           class="multi-file-edit-pane-content"
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8a440902dfc7af539798db3383e42ca7e067fef6
--- /dev/null
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -0,0 +1,23 @@
+<script>
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+  components: {
+    icon,
+  },
+  directives: {
+    tooltip,
+  },
+};
+</script>
+
+<template>
+  <icon
+    name="git-merge"
+    v-tooltip
+    title="__('Part of merge request changes')"
+    css-classes="ide-file-changed-icon"
+    :size="12"
+  />
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 5cf1d9f09c642249c034ffcdf18da838c0916625..0a61b49c950ce25eeb4f3300a8c62dbf82b45d54 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,6 +1,6 @@
 <script>
 /* global monaco */
-import { mapState, mapActions } from 'vuex';
+import { mapState, mapGetters, mapActions } from 'vuex';
 import flash from '~/flash';
 import monacoLoader from '../monaco_loader';
 import Editor from '../lib/editor';
@@ -13,12 +13,8 @@ export default {
     },
   },
   computed: {
-    ...mapState([
-      'leftPanelCollapsed',
-      'rightPanelCollapsed',
-      'viewer',
-      'delayViewerUpdated',
-    ]),
+    ...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']),
+    ...mapGetters(['currentMergeRequest']),
     shouldHideEditor() {
       return this.file && this.file.binary && !this.file.raw;
     },
@@ -68,7 +64,10 @@ export default {
 
       this.editor.clearEditor();
 
-      this.getRawFileData(this.file)
+      this.getRawFileData({
+        path: this.file.path,
+        baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
+      })
         .then(() => {
           const viewerPromise = this.delayViewerUpdated
             ? this.updateViewer(this.file.pending ? 'diff' : 'editor')
@@ -81,14 +80,7 @@ export default {
           this.createEditorInstance();
         })
         .catch(err => {
-          flash(
-            'Error setting up monaco. Please try again.',
-            'alert',
-            document,
-            null,
-            false,
-            true,
-          );
+          flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
           throw err;
         });
     },
@@ -110,7 +102,11 @@ export default {
 
       this.model = this.editor.createModel(this.file);
 
-      this.editor.attachModel(this.model);
+      if (this.viewer === 'mrdiff') {
+        this.editor.attachMergeRequestModel(this.model);
+      } else {
+        this.editor.attachModel(this.model);
+      }
 
       this.model.onChange(model => {
         const { file } = model;
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index fedb13339b8691f05e5216082f3da0d8f62a1ec3..3b5068d4910c99ca93db81120c21372a79907578 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -6,6 +6,7 @@ import router from '../ide_router';
 import newDropdown from './new_dropdown/index.vue';
 import fileStatusIcon from './repo_file_status_icon.vue';
 import changedFileIcon from './changed_file_icon.vue';
+import mrFileIcon from './mr_file_icon.vue';
 
 export default {
   name: 'RepoFile',
@@ -15,6 +16,7 @@ export default {
     fileStatusIcon,
     fileIcon,
     changedFileIcon,
+    mrFileIcon,
   },
   props: {
     file: {
@@ -56,10 +58,7 @@ export default {
     ...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
     clickFile() {
       // Manual Action if a tree is selected/opened
-      if (
-        this.isTree &&
-        this.$router.currentRoute.path === `/project${this.file.url}`
-      ) {
+      if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
         this.toggleTreeOpen(this.file.path);
       }
 
@@ -98,11 +97,15 @@ export default {
             :file="file"
           />
         </span>
-        <changed-file-icon
-          :file="file"
-          v-if="file.changed || file.tempFile"
-          class="prepend-top-5 pull-right"
-        />
+        <span class="pull-right">
+          <mr-file-icon
+            v-if="file.mrChange"
+          />
+          <changed-file-icon
+            :file="file"
+            v-if="file.changed || file.tempFile"
+          />
+        </span>
         <new-dropdown
           v-if="isTree"
           :project-id="file.projectId"
diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
index 25d311142d54200e15843ab1e954c84d42308376..97589e116c58be138d8524ca5a660450ac6d152f 100644
--- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -1,27 +1,27 @@
 <script>
-  import icon from '~/vue_shared/components/icon.vue';
-  import tooltip from '~/vue_shared/directives/tooltip';
-  import '~/lib/utils/datetime_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import '~/lib/utils/datetime_utility';
 
-  export default {
-    components: {
-      icon,
+export default {
+  components: {
+    icon,
+  },
+  directives: {
+    tooltip,
+  },
+  props: {
+    file: {
+      type: Object,
+      required: true,
     },
-    directives: {
-      tooltip,
+  },
+  computed: {
+    lockTooltip() {
+      return `Locked by ${this.file.file_lock.user.name}`;
     },
-    props: {
-      file: {
-        type: Object,
-        required: true,
-      },
-    },
-    computed: {
-      lockTooltip() {
-        return `Locked by ${this.file.file_lock.user.name}`;
-      },
-    },
-  };
+  },
+};
 </script>
 
 <template>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index acbb43e867d34f76dc4bd88aa203c06104b20269..7bd646ba9b0597ba6a4c9eacfd7279058118ab9e 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -26,6 +26,11 @@ export default {
       type: Boolean,
       required: true,
     },
+    mergeRequestId: {
+      type: String,
+      required: false,
+      default: '',
+    },
   },
   data() {
     return {
@@ -70,6 +75,7 @@ export default {
       :viewer="viewer"
       :show-shadow="showShadow"
       :has-changes="hasChanges"
+      :merge-request-id="mergeRequestId"
       @click="openFileViewer"
     />
   </div>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index a6013784677417c625474f04221f01b9d4eff1d8..20983666b4a4fedbf8c55fcb50a553311c09cbfc 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -44,7 +44,7 @@ const router = new VueRouter({
           component: EmptyRouterComponent,
         },
         {
-          path: 'mr/:mrid',
+          path: 'merge_requests/:mrid',
           component: EmptyRouterComponent,
         },
       ],
@@ -98,6 +98,60 @@ router.beforeEach((to, from, next) => {
               );
               throw e;
             });
+        } else if (to.params.mrid) {
+          store.dispatch('updateViewer', 'mrdiff');
+
+          store
+            .dispatch('getMergeRequestData', {
+              projectId: fullProjectId,
+              mergeRequestId: to.params.mrid,
+            })
+            .then(mr => {
+              store.dispatch('getBranchData', {
+                projectId: fullProjectId,
+                branchId: mr.source_branch,
+              });
+
+              return store.dispatch('getFiles', {
+                projectId: fullProjectId,
+                branchId: mr.source_branch,
+              });
+            })
+            .then(() =>
+              store.dispatch('getMergeRequestVersions', {
+                projectId: fullProjectId,
+                mergeRequestId: to.params.mrid,
+              }),
+            )
+            .then(() =>
+              store.dispatch('getMergeRequestChanges', {
+                projectId: fullProjectId,
+                mergeRequestId: to.params.mrid,
+              }),
+            )
+            .then(mrChanges => {
+              mrChanges.changes.forEach((change, ind) => {
+                const changeTreeEntry = store.state.entries[change.new_path];
+
+                if (changeTreeEntry) {
+                  store.dispatch('setFileMrChange', {
+                    file: changeTreeEntry,
+                    mrChange: change,
+                  });
+
+                  if (ind < 10) {
+                    store.dispatch('getFileData', {
+                      path: change.new_path,
+                      makeFileActive: ind === 0,
+                    });
+                  }
+                }
+              });
+            })
+            .catch(e => {
+              flash('Error while loading the merge request. Please try again.');
+              throw e;
+            });
         }
       })
       .catch(e => {
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index e659a6868bab927a903c758c2e6baeec9f394807..e47adae99ed1cabd418be099798a23ea8ca4a0be 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -21,6 +21,15 @@ export default class Model {
         new this.monaco.Uri(null, null, this.file.key),
       )),
     );
+    if (this.file.mrChange) {
+      this.disposable.add(
+        (this.baseModel = this.monaco.editor.createModel(
+          this.file.baseRaw,
+          undefined,
+          new this.monaco.Uri(null, null, `target/${this.file.path}`),
+        )),
+      );
+    }
 
     this.events = new Map();
 
@@ -55,6 +64,10 @@ export default class Model {
     return this.originalModel;
   }
 
+  getBaseModel() {
+    return this.baseModel;
+  }
+
   setValue(value) {
     this.getModel().setValue(value);
   }
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 887dd7e39b1a65617f324876c64894503ca6f070..6b4ba30e08679b871ceeaf9502085c79d563cb84 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -109,11 +109,19 @@ export default class Editor {
     if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
   }
 
+  attachMergeRequestModel(model) {
+    this.instance.setModel({
+      original: model.getBaseModel(),
+      modified: model.getModel(),
+    });
+
+    this.monaco.editor.createDiffNavigator(this.instance, {
+      alwaysRevealFirst: true,
+    });
+  }
+
   setupMonacoTheme() {
-    this.monaco.editor.defineTheme(
-      gitlabTheme.themeName,
-      gitlabTheme.monacoTheme,
-    );
+    this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
 
     this.monaco.editor.setTheme('gitlab');
   }
@@ -161,8 +169,6 @@ export default class Editor {
   onPositionChange(cb) {
     if (!this.instance.onDidChangeCursorPosition) return;
 
-    this.disposable.add(
-      this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
-    );
+    this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
   }
 }
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 5f1fb6cf843d66732e85cd2863d436d6a3fbaefd..a12e637616a70d05472ac352b3637fadb23ceaf8 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -20,12 +20,35 @@ export default {
       return Promise.resolve(file.raw);
     }
 
-    return Vue.http.get(file.rawPath, { params: { format: 'json' } })
+    return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
+  },
+  getBaseRawFileData(file, sha) {
+    if (file.tempFile) {
+      return Promise.resolve(file.baseRaw);
+    }
+
+    if (file.baseRaw) {
+      return Promise.resolve(file.baseRaw);
+    }
+
+    return Vue.http
+      .get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
+        params: { format: 'json' },
+      })
       .then(res => res.text());
   },
   getProjectData(namespace, project) {
     return Api.project(`${namespace}/${project}`);
   },
+  getProjectMergeRequestData(projectId, mergeRequestId) {
+    return Api.mergeRequest(projectId, mergeRequestId);
+  },
+  getProjectMergeRequestChanges(projectId, mergeRequestId) {
+    return Api.mergeRequestChanges(projectId, mergeRequestId);
+  },
+  getProjectMergeRequestVersions(projectId, mergeRequestId) {
+    return Api.mergeRequestVersions(projectId, mergeRequestId);
+  },
   getBranchData(projectId, currentBranchId) {
     return Api.branchSingle(projectId, currentBranchId);
   },
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 373ea3f5c08288e15ff0127f837de799364970b5..c6ba679d99c44d97e27d7c59c2cd5e744e30d930 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -115,3 +115,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
 export * from './actions/tree';
 export * from './actions/file';
 export * from './actions/project';
+export * from './actions/merge_request';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 7aacec891168e621859817a40cfcff547437c820..6b034ea1e8227643757b79ac9d9eda05e03e5244 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -56,22 +56,21 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
   commit(types.SET_CURRENT_BRANCH, file.branchId);
 };
 
-export const getFileData = ({ state, commit, dispatch }, file) => {
+export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
+  const file = state.entries[path];
   commit(types.TOGGLE_LOADING, { entry: file });
-
   return service
     .getFileData(file.url)
     .then(res => {
       const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
-
       setPageTitle(pageTitle);
 
       return res.json();
     })
     .then(data => {
       commit(types.SET_FILE_DATA, { data, file });
-      commit(types.TOGGLE_FILE_OPEN, file.path);
-      dispatch('setFileActive', file.path);
+      commit(types.TOGGLE_FILE_OPEN, path);
+      if (makeFileActive) dispatch('setFileActive', path);
       commit(types.TOGGLE_LOADING, { entry: file });
     })
     .catch(() => {
@@ -80,15 +79,40 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
     });
 };
 
-export const getRawFileData = ({ commit, dispatch }, file) =>
-  service
-    .getRawFileData(file)
-    .then(raw => {
-      commit(types.SET_FILE_RAW_DATA, { file, raw });
-    })
-    .catch(() =>
-      flash('Error loading file content. Please try again.', 'alert', document, null, false, true),
-    );
+export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
+  commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
+};
+
+export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
+  const file = state.entries[path];
+  return new Promise((resolve, reject) => {
+    service
+      .getRawFileData(file)
+      .then(raw => {
+        commit(types.SET_FILE_RAW_DATA, { file, raw });
+        if (file.mrChange && file.mrChange.new_file === false) {
+          service
+            .getBaseRawFileData(file, baseSha)
+            .then(baseRaw => {
+              commit(types.SET_FILE_BASE_RAW_DATA, {
+                file,
+                baseRaw,
+              });
+              resolve(raw);
+            })
+            .catch(e => {
+              reject(e);
+            });
+        } else {
+          resolve(raw);
+        }
+      })
+      .catch(() => {
+        flash('Error loading file content. Please try again.');
+        reject();
+      });
+  });
+};
 
 export const changeFileContent = ({ state, commit }, { path, content }) => {
   const file = state.entries[path];
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
new file mode 100644
index 0000000000000000000000000000000000000000..da73034fd7d3a70e91d0a25247e05cb1d788fc1e
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -0,0 +1,84 @@
+import flash from '~/flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+
+export const getMergeRequestData = (
+  { commit, state, dispatch },
+  { projectId, mergeRequestId, force = false } = {},
+) =>
+  new Promise((resolve, reject) => {
+    if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
+      service
+        .getProjectMergeRequestData(projectId, mergeRequestId)
+        .then(res => res.data)
+        .then(data => {
+          commit(types.SET_MERGE_REQUEST, {
+            projectPath: projectId,
+            mergeRequestId,
+            mergeRequest: data,
+          });
+          if (!state.currentMergeRequestId) {
+            commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
+          }
+          resolve(data);
+        })
+        .catch(() => {
+          flash('Error loading merge request data. Please try again.');
+          reject(new Error(`Merge Request not loaded ${projectId}`));
+        });
+    } else {
+      resolve(state.projects[projectId].mergeRequests[mergeRequestId]);
+    }
+  });
+
+export const getMergeRequestChanges = (
+  { commit, state, dispatch },
+  { projectId, mergeRequestId, force = false } = {},
+) =>
+  new Promise((resolve, reject) => {
+    if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
+      service
+        .getProjectMergeRequestChanges(projectId, mergeRequestId)
+        .then(res => res.data)
+        .then(data => {
+          commit(types.SET_MERGE_REQUEST_CHANGES, {
+            projectPath: projectId,
+            mergeRequestId,
+            changes: data,
+          });
+          resolve(data);
+        })
+        .catch(() => {
+          flash('Error loading merge request changes. Please try again.');
+          reject(new Error(`Merge Request Changes not loaded ${projectId}`));
+        });
+    } else {
+      resolve(state.projects[projectId].mergeRequests[mergeRequestId].changes);
+    }
+  });
+
+export const getMergeRequestVersions = (
+  { commit, state, dispatch },
+  { projectId, mergeRequestId, force = false } = {},
+) =>
+  new Promise((resolve, reject) => {
+    if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) {
+      service
+        .getProjectMergeRequestVersions(projectId, mergeRequestId)
+        .then(res => res.data)
+        .then(data => {
+          commit(types.SET_MERGE_REQUEST_VERSIONS, {
+            projectPath: projectId,
+            mergeRequestId,
+            versions: data,
+          });
+          resolve(data);
+        })
+        .catch(() => {
+          flash('Error loading merge request versions. Please try again.');
+          reject(new Error(`Merge Request Versions not loaded ${projectId}`));
+        });
+    } else {
+      resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions);
+    }
+  });
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 70a969a03253744c26a1d366bca499c26595590d..6536be04f0a1d1a47a5b8a50f238759acd3c5b94 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
 import flash from '~/flash';
 import service from '../../services';
 import * as types from '../mutation_types';
-import {
-  findEntry,
-} from '../utils';
+import { findEntry } from '../utils';
 import FilesDecoratorWorker from '../workers/files_decorator_worker';
 
 export const toggleTreeOpen = ({ commit, dispatch }, path) => {
@@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
 
     dispatch('setFileActive', row.path);
   } else {
-    dispatch('getFileData', row);
+    dispatch('getFileData', { path: row.path });
   }
 };
 
 export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
   if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
 
-  service.getTreeLastCommit(tree.lastCommitPath)
-    .then((res) => {
+  service
+    .getTreeLastCommit(tree.lastCommitPath)
+    .then(res => {
       const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
 
       commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
 
       return res.json();
     })
-    .then((data) => {
-      data.forEach((lastCommit) => {
+    .then(data => {
+      data.forEach(lastCommit => {
         const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
 
         if (entry) {
@@ -50,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
     .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
 };
 
-export const getFiles = (
-  { state, commit, dispatch },
-  { projectId, branchId } = {},
-) => new Promise((resolve, reject) => {
-  if (!state.trees[`${projectId}/${branchId}`]) {
-    const selectedProject = state.projects[projectId];
-    commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
-
-    service
-      .getFiles(selectedProject.web_url, branchId)
-      .then(res => res.json())
-      .then((data) => {
-        const worker = new FilesDecoratorWorker();
-        worker.addEventListener('message', (e) => {
-          const { entries, treeList } = e.data;
-          const selectedTree = state.trees[`${projectId}/${branchId}`];
-
-          commit(types.SET_ENTRIES, entries);
-          commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
-          commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
-
-          worker.terminate();
-
-          resolve();
-        });
-
-        worker.postMessage({
-          data,
-          projectId,
-          branchId,
+export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
+  new Promise((resolve, reject) => {
+    if (!state.trees[`${projectId}/${branchId}`]) {
+      const selectedProject = state.projects[projectId];
+      commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
+
+      service
+        .getFiles(selectedProject.web_url, branchId)
+        .then(res => res.json())
+        .then(data => {
+          const worker = new FilesDecoratorWorker();
+          worker.addEventListener('message', e => {
+            const { entries, treeList } = e.data;
+            const selectedTree = state.trees[`${projectId}/${branchId}`];
+
+            commit(types.SET_ENTRIES, entries);
+            commit(types.SET_DIRECTORY_DATA, {
+              treePath: `${projectId}/${branchId}`,
+              data: treeList,
+            });
+            commit(types.TOGGLE_LOADING, {
+              entry: selectedTree,
+              forceValue: false,
+            });
+
+            worker.terminate();
+
+            resolve();
+          });
+
+          worker.postMessage({
+            data,
+            projectId,
+            branchId,
+          });
+        })
+        .catch(e => {
+          flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
+          reject(e);
         });
-      })
-      .catch((e) => {
-        flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
-        reject(e);
-      });
-  } else {
-    resolve();
-  }
-});
-
+    } else {
+      resolve();
+    }
+  });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index eba325a31df27773e48b82a66841df4c148ecd45..a77cdbc13c8fd036d67ff995742945177ce1aa7f 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,10 +1,8 @@
-export const activeFile = state =>
-  state.openFiles.find(file => file.active) || null;
+export const activeFile = state => state.openFiles.find(file => file.active) || null;
 
 export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
 
-export const modifiedFiles = state =>
-  state.changedFiles.filter(f => !f.tempFile);
+export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
 
 export const projectsWithTrees = state =>
   Object.keys(state.projects).map(projectId => {
@@ -23,8 +21,17 @@ export const projectsWithTrees = state =>
     };
   });
 
+export const currentMergeRequest = state => {
+  if (state.projects[state.currentProjectId]) {
+    return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId];
+  }
+  return null;
+};
+
 // eslint-disable-next-line no-confusing-arrow
 export const currentIcon = state =>
   state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
 
 export const hasChanges = state => !!state.changedFiles.length;
+
+export const hasMergeRequest = state => !!state.currentMergeRequestId;
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index fa2fbaf868353adbb1645254835c462a4f50b42d..ee759bff51650decd285a0e64983c87bdaedcd1b 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
 export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
 export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
 
+// Merge Request Mutation Types
+export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
+export const SET_CURRENT_MERGE_REQUEST = 'SET_CURRENT_MERGE_REQUEST';
+export const SET_MERGE_REQUEST_CHANGES = 'SET_MERGE_REQUEST_CHANGES';
+export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
+
 // Branch Mutation Types
 export const SET_BRANCH = 'SET_BRANCH';
 export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
 export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
 export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
 export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
+export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
 export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
 export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
 export const SET_FILE_POSITION = 'SET_FILE_POSITION';
@@ -39,6 +46,7 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
 export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
 export const SET_ENTRIES = 'SET_ENTRIES';
 export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
+export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
 export const UPDATE_VIEWER = 'UPDATE_VIEWER';
 export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
 
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index da41fc9285c7c5951e026d6965fef5c6b394aac0..5e5eb83166209d7af29d79a1aad0519ea50052da 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,5 +1,6 @@
 import * as types from './mutation_types';
 import projectMutations from './mutations/project';
+import mergeRequestMutation from './mutations/merge_request';
 import fileMutations from './mutations/file';
 import treeMutations from './mutations/tree';
 import branchMutations from './mutations/branch';
@@ -11,10 +12,7 @@ export default {
   [types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
     if (entry.path) {
       Object.assign(state.entries[entry.path], {
-        loading:
-          forceValue !== undefined
-            ? forceValue
-            : !state.entries[entry.path].loading,
+        loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
       });
     } else {
       Object.assign(entry, {
@@ -83,9 +81,7 @@ export default {
 
     if (!foundEntry) {
       Object.assign(state.trees[`${projectId}/${branchId}`], {
-        tree: state.trees[`${projectId}/${branchId}`].tree.concat(
-          data.treeList,
-        ),
+        tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
       });
     }
   },
@@ -100,6 +96,7 @@ export default {
     });
   },
   ...projectMutations,
+  ...mergeRequestMutation,
   ...fileMutations,
   ...treeMutations,
   ...branchMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 4fafcfd0ea1d42b870ad17391713e5bd0dbc2604..926b6f66d788a0a9559af8306011bf7274a9c178 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -40,6 +40,8 @@ export default {
       rawPath: data.raw_path,
       binary: data.binary,
       renderError: data.render_error,
+      raw: null,
+      baseRaw: null,
     });
   },
   [types.SET_FILE_RAW_DATA](state, { file, raw }) {
@@ -47,6 +49,11 @@ export default {
       raw,
     });
   },
+  [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
+    Object.assign(state.entries[file.path], {
+      baseRaw,
+    });
+  },
   [types.UPDATE_FILE_CONTENT](state, { path, content }) {
     const changed = content !== state.entries[path].raw;
 
@@ -71,6 +78,11 @@ export default {
       editorColumn,
     });
   },
+  [types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
+    Object.assign(state.entries[file.path], {
+      mrChange,
+    });
+  },
   [types.DISCARD_FILE_CHANGES](state, path) {
     Object.assign(state.entries[path], {
       content: state.entries[path].raw,
diff --git a/app/assets/javascripts/ide/stores/mutations/merge_request.js b/app/assets/javascripts/ide/stores/mutations/merge_request.js
new file mode 100644
index 0000000000000000000000000000000000000000..334819fe702e4e1c5aeb4c25b21e672ad37ad901
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/merge_request.js
@@ -0,0 +1,33 @@
+import * as types from '../mutation_types';
+
+export default {
+  [types.SET_CURRENT_MERGE_REQUEST](state, currentMergeRequestId) {
+    Object.assign(state, {
+      currentMergeRequestId,
+    });
+  },
+  [types.SET_MERGE_REQUEST](state, { projectPath, mergeRequestId, mergeRequest }) {
+    Object.assign(state.projects[projectPath], {
+      mergeRequests: {
+        [mergeRequestId]: {
+          ...mergeRequest,
+          active: true,
+          changes: [],
+          versions: [],
+          baseCommitSha: null,
+        },
+      },
+    });
+  },
+  [types.SET_MERGE_REQUEST_CHANGES](state, { projectPath, mergeRequestId, changes }) {
+    Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
+      changes,
+    });
+  },
+  [types.SET_MERGE_REQUEST_VERSIONS](state, { projectPath, mergeRequestId, versions }) {
+    Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
+      versions,
+      baseCommitSha: versions.length ? versions[0].base_commit_sha : null,
+    });
+  },
+};
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
index 2816562a91968985a64559bce570abf75dd882a0..284b39a2c72a81278937f1a95d782af9dcebd580 100644
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -11,6 +11,7 @@ export default {
     Object.assign(project, {
       tree: [],
       branches: {},
+      mergeRequests: {},
       active: true,
     });
 
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 6110f54951c8926de95c756d6bc31555408d22a0..e5cc8814000238fa361f674c3a14f5c68a3e9472 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,6 +1,7 @@
 export default () => ({
   currentProjectId: '',
   currentBranchId: '',
+  currentMergeRequestId: '',
   changedFiles: [],
   endpoints: {},
   lastCommitMsg: '',
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 487ea1ead8e6aee72040a6260f9e25043d02bdde..3389eeeaa2ea453258cc0bc912c98a4b7ecca58f 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -38,7 +38,7 @@ export const dataStructure = () => ({
   eol: '',
 });
 
-export const decorateData = (entity) => {
+export const decorateData = entity => {
   const {
     id,
     projectId,
@@ -57,7 +57,6 @@ export const decorateData = (entity) => {
     base64 = false,
 
     file_lock,
-
   } = entity;
 
   return {
@@ -80,17 +79,15 @@ export const decorateData = (entity) => {
     base64,
 
     file_lock,
-
   };
 };
 
-export const findEntry = (tree, type, name, prop = 'name') => tree.find(
-  f => f.type === type && f[prop] === name,
-);
+export const findEntry = (tree, type, name, prop = 'name') =>
+  tree.find(f => f.type === type && f[prop] === name);
 
 export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
 
-export const setPageTitle = (title) => {
+export const setPageTitle = title => {
   document.title = title;
 };
 
@@ -120,6 +117,11 @@ const sortTreesByTypeAndName = (a, b) => {
   return 0;
 };
 
-export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
-  tree: entity.tree.length ? sortTree(entity.tree) : [],
-})).sort(sortTreesByTypeAndName);
+export const sortTree = sortedTree =>
+  sortedTree
+    .map(entity =>
+      Object.assign(entity, {
+        tree: entity.tree.length ? sortTree(entity.tree) : [],
+      }),
+    )
+    .sort(sortTreesByTypeAndName);
diff --git a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
index a6819aaeb123c828fd59153f38aa5ec80c778094..dfe87d89a3990dd065e7ce8ef77ad797cde276d3 100644
--- a/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_detail_row.vue
@@ -11,11 +11,19 @@
         type: String,
         required: true,
       },
+      helpUrl: {
+        type: String,
+        required: false,
+        default: '',
+      },
     },
     computed: {
       hasTitle() {
         return this.title.length > 0;
       },
+      hasHelpURL() {
+        return this.helpUrl.length > 0;
+      },
     },
   };
 </script>
@@ -28,5 +36,21 @@
       {{ title }}:
     </span>
     {{ value }}
+
+    <span
+      v-if="hasHelpURL"
+      class="help-button pull-right"
+    >
+      <a
+        :href="helpUrl"
+        target="_blank"
+        rel="noopener noreferrer nofollow"
+      >
+        <i
+          class="fa fa-question-circle"
+          aria-hidden="true"
+        ></i>
+      </a>
+    </span>
   </p>
 </template>
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index 56814a525255223ee8d6b5c663c7bb3e2bf0faca..172de6b36791aeafdc5c54ec0cd01c93a005eb05 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -22,6 +22,11 @@
         type: Boolean,
         required: true,
       },
+      runnerHelpUrl: {
+        type: String,
+        required: false,
+        default: '',
+      },
     },
     computed: {
       shouldRenderContent() {
@@ -39,6 +44,21 @@
       runnerId() {
         return `#${this.job.runner.id}`;
       },
+      hasTimeout() {
+        return this.job.metadata != null && this.job.metadata.timeout_human_readable !== '';
+      },
+      timeout() {
+        if (this.job.metadata == null) {
+          return '';
+        }
+
+        let t = this.job.metadata.timeout_human_readable;
+        if (this.job.metadata.timeout_source !== '') {
+          t += ` (from ${this.job.metadata.timeout_source})`;
+        }
+
+        return t;
+      },
       renderBlock() {
         return this.job.merge_request ||
           this.job.duration ||
@@ -114,6 +134,13 @@
           title="Queued"
           :value="queued"
         />
+        <detail-row
+          class="js-job-timeout"
+          v-if="hasTimeout"
+          title="Timeout"
+          :help-url="runnerHelpUrl"
+          :value="timeout"
+        />
         <detail-row
           class="js-job-runner"
           v-if="job.runner"
diff --git a/app/assets/javascripts/jobs/job_details_bundle.js b/app/assets/javascripts/jobs/job_details_bundle.js
index 85a88ae409b463291a3269a7e2b1e004f40c49b4..656676ead91c82f1eb7b022633cb035ad4318bf7 100644
--- a/app/assets/javascripts/jobs/job_details_bundle.js
+++ b/app/assets/javascripts/jobs/job_details_bundle.js
@@ -51,6 +51,7 @@ export default () => {
         props: {
           isLoading: this.mediator.state.isLoading,
           job: this.mediator.store.state.job,
+          runnerHelpUrl: dataset.runnerHelpUrl,
         },
       });
     },
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index a266bb6771fa6c5ab3faeda6ffe12ea1404876fc..dd17544b656be8f7b046e15fde63d54d91580641 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -51,7 +51,7 @@ export function removeParams(params) {
   const url = document.createElement('a');
   url.href = window.location.href;
 
-  params.forEach((param) => {
+  params.forEach(param => {
     url.search = removeParamQueryString(url.search, param);
   });
 
@@ -83,3 +83,11 @@ export function refreshCurrentPage() {
 export function redirectTo(url) {
   return window.location.assign(url);
 }
+
+export function webIDEUrl(route = undefined) {
+  let returnUrl = `${gon.relative_url_root}/-/ide/`;
+  if (route) {
+    returnUrl += `project${route}`;
+  }
+  return returnUrl;
+}
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index cf579c5d4dce71d8bc22a111aeca5058a0fc4744..e0f883a8e08b095740d4109ab735c6611d653f6b 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -292,10 +292,12 @@ Please check your network connection and try again.`;
                         </button>
                       </div>
                       <div
+                        v-if="note.resolvable"
                         class="btn-group discussion-actions"
-                        role="group">
+                        role="group"
+                      >
                         <div
-                          v-if="note.resolvable && !discussionResolved"
+                          v-if="!discussionResolved"
                           class="btn-group"
                           role="group">
                           <a
diff --git a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
index 22248418c419059cf5a0145d47477fa0f0ec6380..2bda2aeb3a18468e75dd3f6fc1fe2b7eca5816ba 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
@@ -19,15 +19,19 @@
         type: String,
         required: true,
       },
+      groupName: {
+        type: String,
+        required: true,
+      },
     },
     computed: {
       title() {
         return sprintf(s__('Milestones|Promote %{milestoneTitle} to group milestone?'), { milestoneTitle: this.milestoneTitle });
       },
       text() {
-        return s__(`Milestones|Promoting this milestone will make it available for all projects inside the group.
+        return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
         Existing project milestones with the same title will be merged.
-        This action cannot be reversed.`);
+        This action cannot be reversed.`), { milestoneTitle: this.milestoneTitle, groupName: this.groupName });
       },
     },
     methods: {
diff --git a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
index d00f81c909489033c85148e573227ab8b60978e7..8e79341e96abbca87556b17e3fe070b184aeacf9 100644
--- a/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
+++ b/app/assets/javascripts/pages/milestones/shared/promote_milestone_modal_init.js
@@ -25,6 +25,7 @@ export default () => {
     const modalProps = {
       milestoneTitle: button.dataset.milestoneTitle,
       url: button.dataset.url,
+      groupName: button.dataset.groupName,
     };
     eventHub.$once('promoteMilestoneModal.requestStarted', onRequestStarted);
     eventHub.$emit('promoteMilestoneModal.props', modalProps);
@@ -54,6 +55,7 @@ export default () => {
         return {
           modalProps: {
             milestoneTitle: '',
+            groupName: '',
             url: '',
           },
         };
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index 54695dfeb995b6ba42983e32b3717a96c2210c48..ad6df51bb7a44213ae204047d7d7e65657043a83 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -1,4 +1,5 @@
 <script>
+  import _ from 'underscore';
   import axios from '~/lib/utils/axios_utils';
   import createFlash from '~/flash';
   import GlModal from '~/vue_shared/components/gl_modal.vue';
@@ -27,19 +28,26 @@
         type: String,
         required: true,
       },
+      groupName: {
+        type: String,
+        required: true,
+      },
     },
     computed: {
       text() {
-        return s__(`Milestones|Promoting this label will make it available for all projects inside the group. 
-        Existing project labels with the same title will be merged. This action cannot be reversed.`);
+        return sprintf(s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. 
+        Existing project labels with the same title will be merged. This action cannot be reversed.`), {
+          labelTitle: this.labelTitle,
+          groupName: this.groupName,
+        });
       },
       title() {
         const label = `<span
           class="label color-label"
           style="background-color: ${this.labelColor}; color: ${this.labelTextColor};"
-        >${this.labelTitle}</span>`;
+        >${_.escape(this.labelTitle)}</span>`;
 
-        return sprintf(s__('Labels|Promote label %{labelTitle} to Group Label?'), {
+        return sprintf(s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), {
           labelTitle: label,
         }, false);
       },
@@ -69,6 +77,7 @@
   >
     <div
       slot="title"
+      class="modal-title-with-label"
       v-html="title"
     >
       {{ title }}
diff --git a/app/assets/javascripts/pages/projects/labels/index/index.js b/app/assets/javascripts/pages/projects/labels/index/index.js
index 2abcbfab1eda99353c1b6ef3e4f25b12fa3401bc..03cfef61311a22d86b59e18c18fcd4595b54970b 100644
--- a/app/assets/javascripts/pages/projects/labels/index/index.js
+++ b/app/assets/javascripts/pages/projects/labels/index/index.js
@@ -30,6 +30,7 @@ const initLabelIndex = () => {
       labelColor: button.dataset.labelColor,
       labelTextColor: button.dataset.labelTextColor,
       url: button.dataset.url,
+      groupName: button.dataset.groupName,
     };
     eventHub.$once('promoteLabelModal.requestStarted', onRequestStarted);
     eventHub.$emit('promoteLabelModal.props', modalProps);
@@ -62,6 +63,7 @@ const initLabelIndex = () => {
             labelColor: '',
             labelTextColor: '',
             url: '',
+            groupName: '',
           },
         };
       },
diff --git a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
index 8a86c409b6292841468049d7a55c928a0006038d..ceb023099595c8481315aa48915b6491004fe861 100644
--- a/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/confidential_issue_sidebar.vue
@@ -1,59 +1,73 @@
 <script>
-  import Flash from '../../../flash';
-  import editForm from './edit_form.vue';
-  import Icon from '../../../vue_shared/components/icon.vue';
-  import { __ } from '../../../locale';
+import Flash from '../../../flash';
+import editForm from './edit_form.vue';
+import Icon from '../../../vue_shared/components/icon.vue';
+import { __ } from '../../../locale';
+import eventHub from '../../event_hub';
 
-  export default {
-    components: {
-      editForm,
-      Icon,
+export default {
+  components: {
+    editForm,
+    Icon,
+  },
+  props: {
+    isConfidential: {
+      required: true,
+      type: Boolean,
     },
-    props: {
-      isConfidential: {
-        required: true,
-        type: Boolean,
-      },
-      isEditable: {
-        required: true,
-        type: Boolean,
-      },
-      service: {
-        required: true,
-        type: Object,
-      },
+    isEditable: {
+      required: true,
+      type: Boolean,
     },
-    data() {
-      return {
-        edit: false,
-      };
+    service: {
+      required: true,
+      type: Object,
     },
-    computed: {
-      confidentialityIcon() {
-        return this.isConfidential ? 'eye-slash' : 'eye';
-      },
+  },
+  data() {
+    return {
+      edit: false,
+    };
+  },
+  computed: {
+    confidentialityIcon() {
+      return this.isConfidential ? 'eye-slash' : 'eye';
     },
-    methods: {
-      toggleForm() {
-        this.edit = !this.edit;
-      },
-      updateConfidentialAttribute(confidential) {
-        this.service.update('issue', { confidential })
-          .then(() => location.reload())
-          .catch(() => {
-            Flash(__('Something went wrong trying to change the confidentiality of this issue'));
-          });
-      },
+  },
+  created() {
+    eventHub.$on('closeConfidentialityForm', this.toggleForm);
+  },
+  beforeDestroy() {
+    eventHub.$off('closeConfidentialityForm', this.toggleForm);
+  },
+  methods: {
+    toggleForm() {
+      this.edit = !this.edit;
     },
-  };
+    updateConfidentialAttribute(confidential) {
+      this.service
+        .update('issue', { confidential })
+        .then(() => location.reload())
+        .catch(() => {
+          Flash(
+            __(
+              'Something went wrong trying to change the confidentiality of this issue',
+            ),
+          );
+        });
+    },
+  },
+};
 </script>
 
 <template>
   <div class="block issuable-sidebar-item confidentiality">
-    <div class="sidebar-collapsed-icon">
+    <div
+      class="sidebar-collapsed-icon"
+      @click="toggleForm"
+    >
       <icon
         :name="confidentialityIcon"
-        :size="16"
         aria-hidden="true"
       />
     </div>
@@ -71,7 +85,6 @@
     <div class="value sidebar-item-value hide-collapsed">
       <editForm
         v-if="edit"
-        :toggle-form="toggleForm"
         :is-confidential="isConfidential"
         :update-confidential-attribute="updateConfidentialAttribute"
       />
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
index c569843b05fd9311c46b00d159a7699514da836f..3783f71a848640d7dca62f5b254eaafd6ffa3bb2 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form.vue
@@ -1,34 +1,34 @@
 <script>
-  import editFormButtons from './edit_form_buttons.vue';
-  import { s__ } from '../../../locale';
+import editFormButtons from './edit_form_buttons.vue';
+import { s__ } from '../../../locale';
 
-  export default {
-    components: {
-      editFormButtons,
+export default {
+  components: {
+    editFormButtons,
+  },
+  props: {
+    isConfidential: {
+      required: true,
+      type: Boolean,
     },
-    props: {
-      isConfidential: {
-        required: true,
-        type: Boolean,
-      },
-      toggleForm: {
-        required: true,
-        type: Function,
-      },
-      updateConfidentialAttribute: {
-        required: true,
-        type: Function,
-      },
+    updateConfidentialAttribute: {
+      required: true,
+      type: Function,
     },
-    computed: {
-      confidentialityOnWarning() {
-        return s__('confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.');
-      },
-      confidentialityOffWarning() {
-        return s__('confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.');
-      },
+  },
+  computed: {
+    confidentialityOnWarning() {
+      return s__(
+        'confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue.',
+      );
     },
-  };
+    confidentialityOffWarning() {
+      return s__(
+        'confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue.',
+      );
+    },
+  },
+};
 </script>
 
 <template>
@@ -45,7 +45,6 @@
         </p>
         <edit-form-buttons
           :is-confidential="isConfidential"
-          :toggle-form="toggleForm"
           :update-confidential-attribute="updateConfidentialAttribute"
         />
       </div>
diff --git a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
index 49d5dfeea1a8ac3734930749087b063796d1e399..38b1ddbfd5b34d195971cb9c0ee8fe658dd330d5 100644
--- a/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/edit_form_buttons.vue
@@ -1,14 +1,13 @@
 <script>
+import $ from 'jquery';
+import eventHub from '../../event_hub';
+
 export default {
   props: {
     isConfidential: {
       required: true,
       type: Boolean,
     },
-    toggleForm: {
-      required: true,
-      type: Function,
-    },
     updateConfidentialAttribute: {
       required: true,
       type: Function,
@@ -22,6 +21,16 @@ export default {
       return !this.isConfidential;
     },
   },
+  methods: {
+    closeForm() {
+      eventHub.$emit('closeConfidentialityForm');
+      $(this.$el).trigger('hidden.gl.dropdown');
+    },
+    submitForm() {
+      this.closeForm();
+      this.updateConfidentialAttribute(this.updateConfidentialBool);
+    },
+  },
 };
 </script>
 
@@ -30,14 +39,14 @@ export default {
     <button
       type="button"
       class="btn btn-default append-right-10"
-      @click="toggleForm"
+      @click="closeForm"
     >
       {{ __('Cancel') }}
     </button>
     <button
       type="button"
       class="btn btn-close"
-      @click.prevent="updateConfidentialAttribute(updateConfidentialBool)"
+      @click.prevent="submitForm"
     >
       {{ toggleButtonText }}
     </button>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form.vue b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
index bc32e974bc379b00b993f9a16b6b0ca6c5deb773..e1e4715826a27bdbedca3534fa2ba93851775c90 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form.vue
@@ -1,40 +1,43 @@
 <script>
-  import editFormButtons from './edit_form_buttons.vue';
-  import issuableMixin from '../../../vue_shared/mixins/issuable';
-  import { __, sprintf } from '../../../locale';
+import editFormButtons from './edit_form_buttons.vue';
+import issuableMixin from '../../../vue_shared/mixins/issuable';
+import { __, sprintf } from '../../../locale';
 
-  export default {
-    components: {
-      editFormButtons,
+export default {
+  components: {
+    editFormButtons,
+  },
+  mixins: [issuableMixin],
+  props: {
+    isLocked: {
+      required: true,
+      type: Boolean,
     },
-    mixins: [
-      issuableMixin,
-    ],
-    props: {
-      isLocked: {
-        required: true,
-        type: Boolean,
-      },
 
-      toggleForm: {
-        required: true,
-        type: Function,
-      },
-
-      updateLockedAttribute: {
-        required: true,
-        type: Function,
-      },
+    updateLockedAttribute: {
+      required: true,
+      type: Function,
+    },
+  },
+  computed: {
+    lockWarning() {
+      return sprintf(
+        __(
+          'Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.',
+        ),
+        { issuableDisplayName: this.issuableDisplayName },
+      );
     },
-    computed: {
-      lockWarning() {
-        return sprintf(__('Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
-      },
-      unlockWarning() {
-        return sprintf(__('Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.'), { issuableDisplayName: this.issuableDisplayName });
-      },
+    unlockWarning() {
+      return sprintf(
+        __(
+          'Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment.',
+        ),
+        { issuableDisplayName: this.issuableDisplayName },
+      );
     },
-  };
+  },
+};
 </script>
 
 <template>
@@ -54,7 +57,6 @@
 
       <edit-form-buttons
         :is-locked="isLocked"
-        :toggle-form="toggleForm"
         :update-locked-attribute="updateLockedAttribute"
       />
     </div>
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index c3a553a76059d1fc87f88882a8193263bbda83e0..5e7b8f9698f9f0d9044a47224f363c9f7a9f8f27 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -1,4 +1,7 @@
 <script>
+import $ from 'jquery';
+import eventHub from '../../event_hub';
+
 export default {
   props: {
     isLocked: {
@@ -6,11 +9,6 @@ export default {
       type: Boolean,
     },
 
-    toggleForm: {
-      required: true,
-      type: Function,
-    },
-
     updateLockedAttribute: {
       required: true,
       type: Function,
@@ -26,6 +24,17 @@ export default {
       return !this.isLocked;
     },
   },
+
+  methods: {
+    closeForm() {
+      eventHub.$emit('closeLockForm');
+      $(this.$el).trigger('hidden.gl.dropdown');
+    },
+    submitForm() {
+      this.closeForm();
+      this.updateLockedAttribute(this.toggleLock);
+    },
+  },
 };
 </script>
 
@@ -34,7 +43,7 @@ export default {
     <button
       type="button"
       class="btn btn-default append-right-10"
-      @click="toggleForm"
+      @click="closeForm"
     >
       {{ __('Cancel') }}
     </button>
@@ -42,7 +51,7 @@ export default {
     <button
       type="button"
       class="btn btn-close"
-      @click.prevent="updateLockedAttribute(toggleLock)"
+      @click.prevent="submitForm"
     >
       {{ buttonText }}
     </button>
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index 0686910fc7e3f451b8128b2600950ff7c251f049..e4893451af34872e02fc6c115b65e186efa484d5 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -1,70 +1,93 @@
 <script>
-  import Flash from '~/flash';
-  import editForm from './edit_form.vue';
-  import issuableMixin from '../../../vue_shared/mixins/issuable';
-  import Icon from '../../../vue_shared/components/icon.vue';
+import Flash from '~/flash';
+import editForm from './edit_form.vue';
+import issuableMixin from '../../../vue_shared/mixins/issuable';
+import Icon from '../../../vue_shared/components/icon.vue';
+import eventHub from '../../event_hub';
 
-  export default {
-    components: {
-      editForm,
-      Icon,
-    },
-    mixins: [
-      issuableMixin,
-    ],
+export default {
+  components: {
+    editForm,
+    Icon,
+  },
+  mixins: [issuableMixin],
 
-    props: {
-      isLocked: {
-        required: true,
-        type: Boolean,
-      },
+  props: {
+    isLocked: {
+      required: true,
+      type: Boolean,
+    },
 
-      isEditable: {
-        required: true,
-        type: Boolean,
-      },
+    isEditable: {
+      required: true,
+      type: Boolean,
+    },
 
-      mediator: {
-        required: true,
-        type: Object,
-        validator(mediatorObject) {
-          return mediatorObject.service && mediatorObject.service.update && mediatorObject.store;
-        },
+    mediator: {
+      required: true,
+      type: Object,
+      validator(mediatorObject) {
+        return (
+          mediatorObject.service &&
+          mediatorObject.service.update &&
+          mediatorObject.store
+        );
       },
     },
+  },
 
-    computed: {
-      lockIcon() {
-        return this.isLocked ? 'lock' : 'lock-open';
-      },
+  computed: {
+    lockIcon() {
+      return this.isLocked ? 'lock' : 'lock-open';
+    },
 
-      isLockDialogOpen() {
-        return this.mediator.store.isLockDialogOpen;
-      },
+    isLockDialogOpen() {
+      return this.mediator.store.isLockDialogOpen;
     },
+  },
 
-    methods: {
-      toggleForm() {
-        this.mediator.store.isLockDialogOpen = !this.mediator.store.isLockDialogOpen;
-      },
+  created() {
+    eventHub.$on('closeLockForm', this.toggleForm);
+  },
+
+  beforeDestroy() {
+    eventHub.$off('closeLockForm', this.toggleForm);
+  },
 
-      updateLockedAttribute(locked) {
-        this.mediator.service.update(this.issuableType, {
+  methods: {
+    toggleForm() {
+      this.mediator.store.isLockDialogOpen = !this.mediator.store
+        .isLockDialogOpen;
+    },
+
+    updateLockedAttribute(locked) {
+      this.mediator.service
+        .update(this.issuableType, {
           discussion_locked: locked,
         })
         .then(() => location.reload())
-        .catch(() => Flash(this.__(`Something went wrong trying to change the locked state of this ${this.issuableDisplayName}`)));
-      },
+        .catch(() =>
+          Flash(
+            this.__(
+              `Something went wrong trying to change the locked state of this ${
+                this.issuableDisplayName
+              }`,
+            ),
+          ),
+        );
     },
-  };
+  },
+};
 </script>
 
 <template>
   <div class="block issuable-sidebar-item lock">
-    <div class="sidebar-collapsed-icon">
+    <div
+      class="sidebar-collapsed-icon"
+      @click="toggleForm"
+    >
       <icon
         :name="lockIcon"
-        :size="16"
         aria-hidden="true"
         class="sidebar-item-icon is-active"
       />
@@ -85,7 +108,6 @@
     <div class="value sidebar-item-value hide-collapsed">
       <edit-form
         v-if="isLockDialogOpen"
-        :toggle-form="toggleForm"
         :is-locked="isLocked"
         :update-locked-attribute="updateLockedAttribute"
         :issuable-type="issuableType"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 3d886e7d628a6f1dd450e110163453a3c66175b3..18ee4c62bf11aa7ab8d4451bd0daec3cb03931ab 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,53 +1,57 @@
 <script>
-  import tooltip from '~/vue_shared/directives/tooltip';
-  import { n__ } from '~/locale';
-  import icon from '~/vue_shared/components/icon.vue';
-  import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { n__ } from '~/locale';
+import { webIDEUrl } from '~/lib/utils/url_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
 
-  export default {
-    name: 'MRWidgetHeader',
-    directives: {
-      tooltip,
+export default {
+  name: 'MRWidgetHeader',
+  directives: {
+    tooltip,
+  },
+  components: {
+    icon,
+    clipboardButton,
+  },
+  props: {
+    mr: {
+      type: Object,
+      required: true,
     },
-    components: {
-      icon,
-      clipboardButton,
+  },
+  computed: {
+    shouldShowCommitsBehindText() {
+      return this.mr.divergedCommitsCount > 0;
     },
-    props: {
-      mr: {
-        type: Object,
-        required: true,
-      },
+    commitsText() {
+      return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
     },
-    computed: {
-      shouldShowCommitsBehindText() {
-        return this.mr.divergedCommitsCount > 0;
-      },
-      commitsText() {
-        return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
-      },
-      branchNameClipboardData() {
-        // This supports code in app/assets/javascripts/copy_to_clipboard.js that
-        // works around ClipboardJS limitations to allow the context-specific
-        // copy/pasting of plain text or GFM.
-        return JSON.stringify({
-          text: this.mr.sourceBranch,
-          gfm: `\`${this.mr.sourceBranch}\``,
-        });
-      },
-      isSourceBranchLong() {
-        return this.isBranchTitleLong(this.mr.sourceBranch);
-      },
-      isTargetBranchLong() {
-        return this.isBranchTitleLong(this.mr.targetBranch);
-      },
+    branchNameClipboardData() {
+      // This supports code in app/assets/javascripts/copy_to_clipboard.js that
+      // works around ClipboardJS limitations to allow the context-specific
+      // copy/pasting of plain text or GFM.
+      return JSON.stringify({
+        text: this.mr.sourceBranch,
+        gfm: `\`${this.mr.sourceBranch}\``,
+      });
     },
-    methods: {
-      isBranchTitleLong(branchTitle) {
-        return branchTitle.length > 32;
-      },
+    isSourceBranchLong() {
+      return this.isBranchTitleLong(this.mr.sourceBranch);
     },
-  };
+    isTargetBranchLong() {
+      return this.isBranchTitleLong(this.mr.targetBranch);
+    },
+    webIdePath() {
+      return webIDEUrl(this.mr.statusPath.replace('.json', ''));
+    },
+  },
+  methods: {
+    isBranchTitleLong(branchTitle) {
+      return branchTitle.length > 32;
+    },
+  },
+};
 </script>
 <template>
   <div class="mr-source-target">
@@ -96,6 +100,13 @@
     </div>
 
     <div v-if="mr.isOpen">
+      <a
+        v-if="!mr.sourceBranchRemoved"
+        :href="webIdePath"
+        class="btn btn-sm btn-default inline js-web-ide"
+      >
+        {{ s__("mrWidget|Web IDE") }}
+      </a>
       <button
         data-target="#modal_merge_info"
         data-toggle="modal"
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index 7f3f7e67d761e938475c662df711fdb8e74ad829..05cb0196cedf285c9ebf05456287ea203134c64d 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -199,6 +199,10 @@
   .branch-header-title {
     color: $color-700;
   }
+
+  .ide-file-list .file.file-active {
+    color: $color-700;
+  }
 }
 
 body {
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 48b981dd31fcae2fcd2c950f4e03148d76e87000..eb789cc64b0a8d9f261e0049d406cdf08ffd5fff 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -4,9 +4,15 @@
 
   .page-title,
   .modal-title {
+    .modal-title-with-label span {
+      vertical-align: middle;
+      display: inline-block;
+    }
+
     .color-label {
       font-size: $gl-font-size;
       padding: $gl-vert-padding $label-padding-modal;
+      vertical-align: middle;
     }
   }
 
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 3dd4a6137897eab528ded901b25240d92b1b9a19..798f248dad44143dce49e0c481c2ab1e230edf3f 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -88,7 +88,6 @@
 
 .right-sidebar {
   border-left: 1px solid $border-color;
-  height: calc(100% - #{$header-height});
 }
 
 .with-performance-bar .right-sidebar.affix {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index e21a9f0afc9e3f37217ef69c9bdef443dba56ddd..2c0ed97630160dd271d07f8d0b69c3a754b15f7d 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -522,10 +522,6 @@
 
 .with-performance-bar .right-sidebar {
   top: $header-height + $performance-bar-height;
-
-  .issuable-sidebar {
-    height: calc(100% - #{$performance-bar-height});
-  }
 }
 
 .sidebar-move-issue-confirmation-button {
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 65046f6665ed045c9243983cd219cf20c871513e..1f6f7138e1f605e10cd111aaf394ff6958de73d6 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -19,8 +19,7 @@
 .ide-view {
   display: flex;
   height: calc(100vh - #{$header-height});
-  margin-top: 40px;
-  color: $almost-black;
+  margin-top: 0;
   border-top: 1px solid $white-dark;
   border-bottom: 1px solid $white-dark;
 
@@ -43,13 +42,18 @@
     cursor: pointer;
 
     &.file-open {
-      background: $white-normal;
+      background: $link-active-background;
+    }
+
+    &.file-active {
+      font-weight: $gl-font-weight-bold;
     }
 
     .ide-file-name {
       flex: 1;
       white-space: nowrap;
       text-overflow: ellipsis;
+      max-width: inherit;
 
       svg {
         vertical-align: middle;
@@ -72,7 +76,10 @@
       margin-right: -8px;
     }
 
-    &:hover {
+    &:hover,
+    &:focus {
+      background: $link-active-background;
+
       .ide-new-btn {
         display: block;
       }
@@ -450,6 +457,8 @@
   display: flex;
   flex-direction: column;
   flex: 1;
+  max-height: 100%;
+  overflow: auto;
 }
 
 .multi-file-commit-empty-state-container {
@@ -460,7 +469,7 @@
 .multi-file-commit-panel-header {
   display: flex;
   align-items: center;
-  margin-bottom: 12px;
+  margin-bottom: 0;
   border-bottom: 1px solid $white-dark;
   padding: $gl-btn-padding 0;
 
@@ -667,8 +676,14 @@
   overflow: hidden;
 
   &.nav-only {
+    padding-top: $header-height;
+
+    .with-performance-bar & {
+      padding-top: $header-height + $performance-bar-height;
+    }
+
     .flash-container {
-      margin-top: $header-height;
+      margin-top: 0;
       margin-bottom: 0;
     }
 
@@ -678,7 +693,7 @@
     }
 
     .content-wrapper {
-      margin-top: $header-height;
+      margin-top: 0;
       padding-bottom: 0;
     }
 
@@ -702,11 +717,11 @@
 
 .with-performance-bar .ide.nav-only {
   .flash-container {
-    margin-top: #{$header-height + $performance-bar-height};
+    margin-top: 0;
   }
 
   .content-wrapper {
-    margin-top: #{$header-height + $performance-bar-height};
+    margin-top: 0;
     padding-bottom: 0;
   }
 
@@ -715,14 +730,8 @@
   }
 
   &.flash-shown {
-    .content-wrapper {
-      margin-top: 0;
-    }
-
     .ide-view {
-      height: calc(
-        100vh - #{$header-height + $performance-bar-height + $flash-height}
-      );
+      height: calc(100vh - #{$header-height + $performance-bar-height + $flash-height});
     }
   }
 }
diff --git a/app/assets/stylesheets/pages/repo.scss.orig b/app/assets/stylesheets/pages/repo.scss.orig
new file mode 100644
index 0000000000000000000000000000000000000000..57b995adb64129e3f9c2268e5a074722409b8d00
--- /dev/null
+++ b/app/assets/stylesheets/pages/repo.scss.orig
@@ -0,0 +1,786 @@
+.project-refs-form,
+.project-refs-target-form {
+  display: inline-block;
+}
+
+.fade-enter,
+.fade-leave-to {
+  opacity: 0;
+}
+
+.commit-message {
+  @include str-truncated(250px);
+}
+
+.editable-mode {
+  display: inline-block;
+}
+
+.ide-view {
+  display: flex;
+  height: calc(100vh - #{$header-height});
+  margin-top: 40px;
+  color: $almost-black;
+  border-top: 1px solid $white-dark;
+  border-bottom: 1px solid $white-dark;
+
+  &.is-collapsed {
+    .ide-file-list {
+      max-width: 250px;
+    }
+  }
+
+  .file-status-icon {
+    width: 10px;
+    height: 10px;
+  }
+}
+
+.ide-file-list {
+  flex: 1;
+
+  .file {
+    cursor: pointer;
+
+    &.file-open {
+      background: $white-normal;
+    }
+
+    .ide-file-name {
+      flex: 1;
+      white-space: nowrap;
+      text-overflow: ellipsis;
+
+      svg {
+        vertical-align: middle;
+        margin-right: 2px;
+      }
+
+      .loading-container {
+        margin-right: 4px;
+        display: inline-block;
+      }
+    }
+
+    .ide-file-changed-icon {
+      margin-left: auto;
+    }
+
+    .ide-new-btn {
+      display: none;
+      margin-bottom: -4px;
+      margin-right: -8px;
+    }
+
+    &:hover {
+      .ide-new-btn {
+        display: block;
+      }
+    }
+
+    &.folder {
+      svg {
+        fill: $gl-text-color-secondary;
+      }
+    }
+  }
+
+  a {
+    color: $gl-text-color;
+  }
+
+  th {
+    position: sticky;
+    top: 0;
+  }
+}
+
+.file-name,
+.file-col-commit-message {
+  display: flex;
+  overflow: visible;
+  padding: 6px 12px;
+}
+
+.multi-file-loading-container {
+  margin-top: 10px;
+  padding: 10px;
+
+  .animation-container {
+    background: $gray-light;
+
+    div {
+      background: $gray-light;
+    }
+  }
+}
+
+.multi-file-table-col-commit-message {
+  white-space: nowrap;
+  width: 50%;
+}
+
+.multi-file-edit-pane {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+  border-left: 1px solid $white-dark;
+  overflow: hidden;
+}
+
+.multi-file-tabs {
+  display: flex;
+  background-color: $white-normal;
+  box-shadow: inset 0 -1px $white-dark;
+
+  > ul {
+    display: flex;
+    overflow-x: auto;
+  }
+
+  li {
+    position: relative;
+  }
+
+  .dropdown {
+    display: flex;
+    margin-left: auto;
+    margin-bottom: 1px;
+    padding: 0 $grid-size;
+    border-left: 1px solid $white-dark;
+    background-color: $white-light;
+
+    &.shadow {
+      box-shadow: 0 0 10px $dropdown-shadow-color;
+    }
+
+    .btn {
+      margin-top: auto;
+      margin-bottom: auto;
+    }
+  }
+}
+
+.multi-file-tab {
+  @include str-truncated(150px);
+  padding: ($gl-padding / 2) ($gl-padding + 12) ($gl-padding / 2) $gl-padding;
+  background-color: $gray-normal;
+  border-right: 1px solid $white-dark;
+  border-bottom: 1px solid $white-dark;
+  cursor: pointer;
+
+  svg {
+    vertical-align: middle;
+  }
+
+  &.active {
+    background-color: $white-light;
+    border-bottom-color: $white-light;
+  }
+}
+
+.multi-file-tab-close {
+  position: absolute;
+  right: 8px;
+  top: 50%;
+  width: 16px;
+  height: 16px;
+  padding: 0;
+  background: none;
+  border: 0;
+  border-radius: $border-radius-default;
+  color: $theme-gray-900;
+  transform: translateY(-50%);
+
+  svg {
+    position: relative;
+    top: -1px;
+  }
+
+  &:hover {
+    background-color: $theme-gray-200;
+  }
+
+  &:focus {
+    background-color: $blue-500;
+    color: $white-light;
+    outline: 0;
+
+    svg {
+      fill: currentColor;
+    }
+  }
+}
+
+.multi-file-edit-pane-content {
+  flex: 1;
+  height: 0;
+}
+
+.blob-editor-container {
+  flex: 1;
+  height: 0;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+
+  .vertical-center {
+    min-height: auto;
+  }
+
+  .monaco-editor .lines-content .cigr {
+    display: none;
+  }
+
+  .monaco-diff-editor.vs {
+    .editor.modified {
+      box-shadow: none;
+    }
+
+    .diagonal-fill {
+      display: none !important;
+    }
+
+    .diffOverview {
+      background-color: $white-light;
+      border-left: 1px solid $white-dark;
+      cursor: ns-resize;
+    }
+
+    .diffViewport {
+      display: none;
+    }
+
+    .char-insert {
+      background-color: $line-added-dark;
+    }
+
+    .char-delete {
+      background-color: $line-removed-dark;
+    }
+
+    .line-numbers {
+      color: $black-transparent;
+    }
+
+    .view-overlays {
+      .line-insert {
+        background-color: $line-added;
+      }
+
+      .line-delete {
+        background-color: $line-removed;
+      }
+    }
+
+    .margin {
+      background-color: $gray-light;
+      border-right: 1px solid $white-normal;
+
+      .line-insert {
+        border-right: 1px solid $line-added-dark;
+      }
+
+      .line-delete {
+        border-right: 1px solid $line-removed-dark;
+      }
+    }
+
+    .margin-view-overlays .insert-sign,
+    .margin-view-overlays .delete-sign {
+      opacity: 0.4;
+    }
+
+    .cursors-layer {
+      display: none;
+    }
+  }
+}
+
+.multi-file-editor-holder {
+  height: 100%;
+}
+
+.multi-file-editor-btn-group {
+  padding: $gl-bar-padding $gl-padding;
+  border-top: 1px solid $white-dark;
+  border-bottom: 1px solid $white-dark;
+  background: $white-light;
+}
+
+.ide-status-bar {
+  padding: $gl-bar-padding $gl-padding;
+  background: $white-light;
+  display: flex;
+  justify-content: space-between;
+
+  svg {
+    vertical-align: middle;
+  }
+}
+
+// Not great, but this is to deal with our current output
+.multi-file-preview-holder {
+  height: 100%;
+  overflow: scroll;
+
+  .file-content.code {
+    display: flex;
+
+    i {
+      margin-left: -10px;
+    }
+  }
+
+  .line-numbers {
+    min-width: 50px;
+  }
+
+  .file-content,
+  .line-numbers,
+  .blob-content,
+  .code {
+    min-height: 100%;
+  }
+}
+
+.file-content.blob-no-preview {
+  a {
+    margin-left: auto;
+    margin-right: auto;
+  }
+}
+
+.multi-file-commit-panel {
+  display: flex;
+  position: relative;
+  flex-direction: column;
+  width: 340px;
+  padding: 0;
+  background-color: $gray-light;
+  padding-right: 3px;
+
+  .projects-sidebar {
+    display: flex;
+    flex-direction: column;
+
+    .context-header {
+      width: auto;
+      margin-right: 0;
+    }
+  }
+
+  .multi-file-commit-panel-inner {
+    display: flex;
+    flex: 1;
+    flex-direction: column;
+  }
+
+  .multi-file-commit-panel-inner-scroll {
+    display: flex;
+    flex: 1;
+    flex-direction: column;
+    overflow: auto;
+  }
+
+  &.is-collapsed {
+    width: 60px;
+
+    .multi-file-commit-list {
+      padding-top: $gl-padding;
+      overflow: hidden;
+    }
+
+    .multi-file-context-bar-icon {
+      align-items: center;
+
+      svg {
+        float: none;
+        margin: 0;
+      }
+    }
+  }
+
+  .branch-container {
+    border-left: 4px solid $indigo-700;
+    margin-bottom: $gl-bar-padding;
+  }
+
+  .branch-header {
+    background: $white-dark;
+    display: flex;
+  }
+
+  .branch-header-title {
+    flex: 1;
+    padding: $grid-size $gl-padding;
+    color: $indigo-700;
+    font-weight: $gl-font-weight-bold;
+
+    svg {
+      vertical-align: middle;
+    }
+  }
+
+  .branch-header-btns {
+    padding: $gl-vert-padding $gl-padding;
+  }
+
+  .left-collapse-btn {
+    display: none;
+    background: $gray-light;
+    text-align: left;
+    border-top: 1px solid $white-dark;
+
+    svg {
+      vertical-align: middle;
+    }
+  }
+}
+
+.multi-file-context-bar-icon {
+  padding: 10px;
+
+  svg {
+    margin-right: 10px;
+    float: left;
+  }
+}
+
+.multi-file-commit-panel-section {
+  display: flex;
+  flex-direction: column;
+  flex: 1;
+}
+
+.multi-file-commit-empty-state-container {
+  align-items: center;
+  justify-content: center;
+}
+
+.multi-file-commit-panel-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 12px;
+  border-bottom: 1px solid $white-dark;
+  padding: $gl-btn-padding 0;
+
+  &.is-collapsed {
+    border-bottom: 1px solid $white-dark;
+
+    svg {
+      margin-left: auto;
+      margin-right: auto;
+    }
+
+    .multi-file-commit-panel-collapse-btn {
+      margin-right: auto;
+      margin-left: auto;
+      border-left: 0;
+    }
+  }
+}
+
+.multi-file-commit-panel-header-title {
+  display: flex;
+  flex: 1;
+  padding: 0 $gl-btn-padding;
+
+  svg {
+    margin-right: $gl-btn-padding;
+  }
+}
+
+.multi-file-commit-panel-collapse-btn {
+  border-left: 1px solid $white-dark;
+}
+
+.multi-file-commit-list {
+  flex: 1;
+  overflow: auto;
+  padding: $gl-padding 0;
+  min-height: 60px;
+}
+
+.multi-file-commit-list-item {
+  display: flex;
+  padding: 0;
+  align-items: center;
+
+  .multi-file-discard-btn {
+    display: none;
+    margin-left: auto;
+    color: $gl-link-color;
+    padding: 0 2px;
+
+    &:focus,
+    &:hover {
+      text-decoration: underline;
+    }
+  }
+
+  &:hover {
+    background: $white-normal;
+
+    .multi-file-discard-btn {
+      display: block;
+    }
+  }
+}
+
+.multi-file-addition {
+  fill: $green-500;
+}
+
+.multi-file-modified {
+  fill: $orange-500;
+}
+
+.multi-file-commit-list-collapsed {
+  display: flex;
+  flex-direction: column;
+
+  > svg {
+    margin-left: auto;
+    margin-right: auto;
+  }
+
+  .file-status-icon {
+    width: 10px;
+    height: 10px;
+    margin-left: 3px;
+  }
+}
+
+.multi-file-commit-list-path {
+  padding: $grid-size / 2;
+  padding-left: $gl-padding;
+  background: none;
+  border: 0;
+  text-align: left;
+  width: 100%;
+  min-width: 0;
+
+  svg {
+    min-width: 16px;
+    vertical-align: middle;
+    display: inline-block;
+  }
+
+  &:hover,
+  &:focus {
+    outline: 0;
+  }
+}
+
+.multi-file-commit-list-file-path {
+  @include str-truncated(100%);
+
+  &:hover {
+    text-decoration: underline;
+  }
+
+  &:active {
+    text-decoration: none;
+  }
+}
+
+.multi-file-commit-form {
+  padding: $gl-padding;
+  border-top: 1px solid $white-dark;
+
+  .btn {
+    font-size: $gl-font-size;
+  }
+}
+
+.multi-file-commit-message.form-control {
+  height: 160px;
+  resize: none;
+}
+
+.dirty-diff {
+  // !important need to override monaco inline style
+  width: 4px !important;
+  left: 0 !important;
+
+  &-modified {
+    background-color: $blue-500;
+  }
+
+  &-added {
+    background-color: $green-600;
+  }
+
+  &-removed {
+    height: 0 !important;
+    width: 0 !important;
+    bottom: -2px;
+    border-style: solid;
+    border-width: 5px;
+    border-color: transparent transparent transparent $red-500;
+
+    &::before {
+      content: '';
+      position: absolute;
+      left: 0;
+      top: 0;
+      width: 100px;
+      height: 1px;
+      background-color: rgba($red-500, 0.5);
+    }
+  }
+}
+
+.ide-loading {
+  display: flex;
+  height: 100vh;
+  align-items: center;
+  justify-content: center;
+}
+
+.ide-empty-state {
+  display: flex;
+  height: 100vh;
+  align-items: center;
+  justify-content: center;
+}
+
+.ide-new-btn {
+  .dropdown-toggle svg {
+    margin-top: -2px;
+    margin-bottom: 2px;
+  }
+
+  .dropdown-menu {
+    left: auto;
+    right: 0;
+
+    label {
+      font-weight: $gl-font-weight-normal;
+      padding: 5px 8px;
+      margin-bottom: 0;
+    }
+  }
+}
+
+.ide {
+  overflow: hidden;
+
+  &.nav-only {
+    .flash-container {
+      margin-top: $header-height;
+      margin-bottom: 0;
+    }
+
+    .alert-wrapper .flash-container .flash-alert:last-child,
+    .alert-wrapper .flash-container .flash-notice:last-child {
+      margin-bottom: 0;
+    }
+
+    .content-wrapper {
+      margin-top: $header-height;
+      padding-bottom: 0;
+    }
+
+    &.flash-shown {
+      .content-wrapper {
+        margin-top: 0;
+      }
+
+      .ide-view {
+        height: calc(100vh - #{$header-height + $flash-height});
+      }
+    }
+
+    .projects-sidebar {
+      .multi-file-commit-panel-inner-scroll {
+        flex: 1;
+      }
+    }
+  }
+}
+
+.with-performance-bar .ide.nav-only {
+  .flash-container {
+    margin-top: #{$header-height + $performance-bar-height};
+  }
+
+  .content-wrapper {
+    margin-top: #{$header-height + $performance-bar-height};
+    padding-bottom: 0;
+  }
+
+  .ide-view {
+    height: calc(100vh - #{$header-height + $performance-bar-height});
+  }
+
+  &.flash-shown {
+    .content-wrapper {
+      margin-top: 0;
+    }
+
+    .ide-view {
+      height: calc(
+        100vh - #{$header-height + $performance-bar-height + $flash-height}
+      );
+    }
+  }
+}
+
+.dragHandle {
+  position: absolute;
+  top: 0;
+  bottom: 0;
+  width: 3px;
+  background-color: $white-dark;
+
+  &.dragright {
+    right: 0;
+  }
+
+  &.dragleft {
+    left: 0;
+  }
+}
+
+.ide-commit-radios {
+  label {
+    font-weight: normal;
+  }
+
+  .help-block {
+    margin-top: 0;
+    line-height: 0;
+  }
+}
+
+.ide-commit-new-branch {
+  margin-left: 25px;
+}
+
+.ide-external-links {
+  p {
+    margin: 0;
+  }
+}
+
+.ide-sidebar-link {
+  padding: $gl-padding-8 $gl-padding;
+  background: $indigo-700;
+  color: $white-light;
+  text-decoration: none;
+  display: flex;
+  align-items: center;
+
+  &:focus,
+  &:hover {
+    color: $white-light;
+    text-decoration: underline;
+    background: $indigo-500;
+  }
+
+  &:active {
+    background: $indigo-800;
+  }
+}
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index dd0b38970bd870896572e3c33c6891625ff43930..ea302f17d16dab67eb7668b34b30a73f06587940 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -50,9 +50,19 @@ class Admin::AppearancesController < Admin::ApplicationController
 
   # Only allow a trusted parameter "white list" through.
   def appearance_params
-    params.require(:appearance).permit(
-      :title, :description, :logo, :logo_cache, :header_logo, :header_logo_cache,
-      :new_project_guidelines, :updated_by
-    )
+    params.require(:appearance).permit(allowed_appearance_params)
+  end
+
+  def allowed_appearance_params
+    %i[
+      title
+      description
+      logo
+      logo_cache
+      header_logo
+      header_logo_cache
+      new_project_guidelines
+      updated_by
+    ]
   end
 end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 965cece600e5cf8d700e2dd585f2f42de5abd70b..176679f08494a785bef0d7bf4753488d978cff75 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -21,17 +21,13 @@ class Projects::BranchesController < Projects::ApplicationController
         fetch_branches_by_mode
 
         @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
-        @merged_branch_names =
-          repository.merged_branch_names(@branches.map(&:name))
-        # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37429
-        Gitlab::GitalyClient.allow_n_plus_1_calls do
-          @max_commits = @branches.reduce(0) do |memo, branch|
-            diverging_commit_counts = repository.diverging_commit_counts(branch)
-            [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
-          end
-
-          render
+        @merged_branch_names = repository.merged_branch_names(@branches.map(&:name))
+        @max_commits = @branches.reduce(0) do |memo, branch|
+          diverging_commit_counts = repository.diverging_commit_counts(branch)
+          [memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
         end
+
+        render
       end
       format.json do
         branches = BranchesFinder.new(@repository, params).execute
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 99790b8e7e85dab7a320b49edca7ad2cddb01a46..516198b1b8a175d008b69d413173661fa45c9fe9 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -112,7 +112,7 @@ class Projects::LabelsController < Projects::ApplicationController
     begin
       return render_404 unless promote_service.execute(@label)
 
-      flash[:notice] = "#{@label.title} promoted to group label."
+      flash[:notice] = "#{@label.title} promoted to <a href=\"#{group_labels_path(@project.group)}\">group label</a>.".html_safe
       respond_to do |format|
         format.html do
           redirect_to(project_labels_path(@project), status: 303)
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index cf84629fadc051a4d411227259f77a2accadf30e..e898136d203ff7ac7f76c246d68b6abbcc2688de 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -74,9 +74,9 @@ class Projects::MilestonesController < Projects::ApplicationController
   end
 
   def promote
-    Milestones::PromoteService.new(project, current_user).execute(milestone)
+    promoted_milestone = Milestones::PromoteService.new(project, current_user).execute(milestone)
 
-    flash[:notice] = "#{milestone.title} promoted to group milestone"
+    flash[:notice] = "#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, promoted_milestone.iid)}\">group milestone</a>.".html_safe
     respond_to do |format|
       format.html do
         redirect_to project_milestones_path(project)
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index f14cb5f6a9fc6c0cd44fcccad746fb29e602374c..a5ea9ff7ed7b767289128b1c36656e1dfa936c1e 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -46,6 +46,8 @@ class Projects::ServicesController < Projects::ApplicationController
     else
       { error: true, message: 'Validations failed.', service_response: @service.errors.full_messages.join(',') }
     end
+  rescue Gitlab::HTTP::BlockedUrlError => e
+    { error: true, message: 'Test failed.', service_response: e.message }
   end
 
   def success_message
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index c037de33c22fc27abcf81a1b808854edc2071d71..f48db024e3fdc0f6d054a32056e98b05b4396fd1 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -1,27 +1,27 @@
 module AppearancesHelper
   def brand_title
-    brand_item&.title.presence || 'GitLab Community Edition'
+    current_appearance&.title.presence || 'GitLab Community Edition'
   end
 
   def brand_image
-    image_tag(brand_item.logo) if brand_item&.logo?
+    image_tag(current_appearance.logo) if current_appearance&.logo?
   end
 
   def brand_text
-    markdown_field(brand_item, :description)
+    markdown_field(current_appearance, :description)
   end
 
   def brand_new_project_guidelines
-    markdown_field(brand_item, :new_project_guidelines)
+    markdown_field(current_appearance, :new_project_guidelines)
   end
 
-  def brand_item
+  def current_appearance
     @appearance ||= Appearance.current
   end
 
   def brand_header_logo
-    if brand_item&.header_logo?
-      image_tag brand_item.header_logo
+    if current_appearance&.header_logo?
+      image_tag current_appearance.header_logo
     else
       render 'shared/logo.svg'
     end
@@ -29,7 +29,7 @@ module AppearancesHelper
 
   # Skip the 'GitLab' type logo when custom brand logo is set
   def brand_header_logo_type
-    unless brand_item&.header_logo?
+    unless current_appearance&.header_logo?
       render 'shared/logo_type.svg'
     end
   end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 701be97ee96c20d0c2c6206d313ed50258c69a34..86ec500ceb3c06d88a7044d2146a2b6ec4ab3ac8 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -285,6 +285,10 @@ module ApplicationHelper
     class_names
   end
 
+  # EE feature: System header and footer, unavailable in CE
+  def system_message_class
+  end
+
   # Returns active css class when condition returns true
   # otherwise returns nil.
   #
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 4ddc1dbed49b30836f3484d844111b07c1520d8c..c86a26ac30ffeec53f87a678a6e009449d5316d5 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -54,9 +54,9 @@ module EmailsHelper
   end
 
   def header_logo
-    if brand_item && brand_item.header_logo?
+    if current_appearance&.header_logo?
       image_tag(
-        brand_item.header_logo,
+        current_appearance.header_logo,
         style: 'height: 50px'
       )
     else
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 40ca666f1bf5985e2e31c073a59fd954945455ca..9be93fa69ae41dbc1109932c4a6065736b100719 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -31,7 +31,7 @@ module NamespacesHelper
 
   def namespace_icon(namespace, size = 40)
     if namespace.is_a?(Group)
-      group_icon(namespace)
+      group_icon_url(namespace)
     else
       avatar_icon_for_user(namespace.owner, size)
     end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 18b9bf214a32e5f5dcf8a972cd6e15d2e1d98671..a8397b03d63dd56bd660d44e7671468ee63dcd90 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -39,7 +39,10 @@ module PageLayoutHelper
   end
 
   def favicon
-    Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico'
+    return 'favicon-yellow.ico' if Gitlab::Utils.to_boolean(ENV['CANARY'])
+    return 'favicon-blue.ico' if Rails.env.development?
+
+    'favicon.ico'
   end
 
   def page_image
diff --git a/app/models/ci/artifact_blob.rb b/app/models/ci/artifact_blob.rb
index ec56cc53aea7bfacf5925202283e4f81e35370b6..760f01f225bffd53187fdbaa122655c8020b7d91 100644
--- a/app/models/ci/artifact_blob.rb
+++ b/app/models/ci/artifact_blob.rb
@@ -36,16 +36,15 @@ module Ci
     def external_url(project, job)
       return unless external_link?(job)
 
-      full_path_parts = project.full_path_components
-      top_level_group = full_path_parts.shift
+      url_project_path = project.full_path.partition('/').last
 
       artifact_path = [
-        '-', *full_path_parts, '-',
+        '-', url_project_path, '-',
         'jobs', job.id,
         'artifacts', path
       ].join('/')
 
-      "#{pages_config.protocol}://#{top_level_group}.#{pages_config.host}/#{artifact_path}"
+      "#{project.pages_group_url}/#{artifact_path}"
     end
 
     def external_link?(job)
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 08bb5915d10e4e4f6a6bb77ecd7de17b95fe5a9a..18e963891996b4b5b6c4d8dd45409c142b5214eb 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -6,6 +6,7 @@ module Ci
     include ObjectStorage::BackgroundMove
     include Presentable
     include Importable
+    include Gitlab::Utils::StrongMemoize
 
     MissingDependenciesError = Class.new(StandardError)
 
@@ -24,12 +25,18 @@ module Ci
     has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
     has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
 
-    # The "environment" field for builds is a String, and is the unexpanded name
+    has_one :metadata, class_name: 'Ci::BuildMetadata'
+    delegate :timeout, to: :metadata, prefix: true, allow_nil: true
+
+    ##
+    # The "environment" field for builds is a String, and is the unexpanded name!
+    #
     def persisted_environment
-      @persisted_environment ||= Environment.find_by(
-        name: expanded_environment_name,
-        project: project
-      )
+      return unless has_environment?
+
+      strong_memoize(:persisted_environment) do
+        Environment.find_by(name: expanded_environment_name, project: project)
+      end
     end
 
     serialize :options # rubocop:disable Cop/ActiveRecordSerialize
@@ -153,6 +160,14 @@ module Ci
       before_transition any => [:running] do |build|
         build.validates_dependencies! unless Feature.enabled?('ci_disable_validates_dependencies')
       end
+
+      before_transition pending: :running do |build|
+        build.ensure_metadata.update_timeout_state
+      end
+    end
+
+    def ensure_metadata
+      metadata || build_metadata(project: project)
     end
 
     def detailed_status(current_user)
@@ -200,7 +215,11 @@ module Ci
     end
 
     def expanded_environment_name
-      ExpandVariables.expand(environment, simple_variables) if environment
+      return unless has_environment?
+
+      strong_memoize(:expanded_environment_name) do
+        ExpandVariables.expand(environment, simple_variables)
+      end
     end
 
     def has_environment?
@@ -231,10 +250,6 @@ module Ci
       latest_builds.where('stage_idx < ?', stage_idx)
     end
 
-    def timeout
-      project.build_timeout
-    end
-
     def triggered_by?(current_user)
       user == current_user
     end
@@ -250,31 +265,52 @@ module Ci
       Gitlab::Utils.slugify(ref.to_s)
     end
 
-    # Variables whose value does not depend on environment
-    def simple_variables
-      variables(environment: nil)
-    end
-
-    # All variables, including those dependent on environment, which could
-    # contain unexpanded variables.
-    def variables(environment: persisted_environment)
-      collection = Gitlab::Ci::Variables::Collection.new.tap do |variables|
+    ##
+    # Variables in the environment name scope.
+    #
+    def scoped_variables(environment: expanded_environment_name)
+      Gitlab::Ci::Variables::Collection.new.tap do |variables|
         variables.concat(predefined_variables)
         variables.concat(project.predefined_variables)
         variables.concat(pipeline.predefined_variables)
         variables.concat(runner.predefined_variables) if runner
-        variables.concat(project.deployment_variables(environment: environment)) if has_environment?
+        variables.concat(project.deployment_variables(environment: environment)) if environment
         variables.concat(yaml_variables)
         variables.concat(user_variables)
-        variables.concat(project.group.secret_variables_for(ref, project)) if project.group
-        variables.concat(secret_variables(environment: environment))
+        variables.concat(secret_group_variables)
+        variables.concat(secret_project_variables(environment: environment))
         variables.concat(trigger_request.user_variables) if trigger_request
         variables.concat(pipeline.variables)
         variables.concat(pipeline.pipeline_schedule.job_variables) if pipeline.pipeline_schedule
-        variables.concat(persisted_environment_variables) if environment
       end
+    end
+
+    ##
+    # Variables that do not depend on the environment name.
+    #
+    def simple_variables
+      strong_memoize(:simple_variables) do
+        scoped_variables(environment: nil).to_runner_variables
+      end
+    end
 
-      collection.to_runner_variables
+    ##
+    # All variables, including persisted environment variables.
+    #
+    def variables
+      Gitlab::Ci::Variables::Collection.new
+        .concat(persisted_variables)
+        .concat(scoped_variables)
+        .concat(persisted_environment_variables)
+        .to_runner_variables
+    end
+
+    ##
+    # Regular Ruby hash of scoped variables, without duplicates that are
+    # possible to be present in an array of hashes returned from `variables`.
+    #
+    def scoped_variables_hash
+      scoped_variables.to_hash
     end
 
     def features
@@ -451,9 +487,14 @@ module Ci
       end
     end
 
-    def secret_variables(environment: persisted_environment)
+    def secret_group_variables
+      return [] unless project.group
+
+      project.group.secret_variables_for(ref, project)
+    end
+
+    def secret_project_variables(environment: persisted_environment)
       project.secret_variables_for(ref: ref, environment: environment)
-        .map(&:to_runner_variable)
     end
 
     def steps
@@ -550,6 +591,21 @@ module Ci
 
     CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
 
+    def persisted_variables
+      Gitlab::Ci::Variables::Collection.new.tap do |variables|
+        return variables unless persisted?
+
+        variables
+          .append(key: 'CI_JOB_ID', value: id.to_s)
+          .append(key: 'CI_JOB_TOKEN', value: token, public: false)
+          .append(key: 'CI_BUILD_ID', value: id.to_s)
+          .append(key: 'CI_BUILD_TOKEN', value: token, public: false)
+          .append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
+          .append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
+          .append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
+      end
+    end
+
     def predefined_variables
       Gitlab::Ci::Variables::Collection.new.tap do |variables|
         variables.append(key: 'CI', value: 'true')
@@ -558,16 +614,11 @@ module Ci
         variables.append(key: 'CI_SERVER_NAME', value: 'GitLab')
         variables.append(key: 'CI_SERVER_VERSION', value: Gitlab::VERSION)
         variables.append(key: 'CI_SERVER_REVISION', value: Gitlab::REVISION)
-        variables.append(key: 'CI_JOB_ID', value: id.to_s)
         variables.append(key: 'CI_JOB_NAME', value: name)
         variables.append(key: 'CI_JOB_STAGE', value: stage)
-        variables.append(key: 'CI_JOB_TOKEN', value: token, public: false)
         variables.append(key: 'CI_COMMIT_SHA', value: sha)
         variables.append(key: 'CI_COMMIT_REF_NAME', value: ref)
         variables.append(key: 'CI_COMMIT_REF_SLUG', value: ref_slug)
-        variables.append(key: 'CI_REGISTRY_USER', value: CI_REGISTRY_USER)
-        variables.append(key: 'CI_REGISTRY_PASSWORD', value: token, public: false)
-        variables.append(key: 'CI_REPOSITORY_URL', value: repo_url, public: false)
         variables.append(key: "CI_COMMIT_TAG", value: ref) if tag?
         variables.append(key: "CI_PIPELINE_TRIGGERED", value: 'true') if trigger_request
         variables.append(key: "CI_JOB_MANUAL", value: 'true') if action?
@@ -575,23 +626,8 @@ module Ci
       end
     end
 
-    def persisted_environment_variables
-      Gitlab::Ci::Variables::Collection.new.tap do |variables|
-        return variables unless persisted_environment
-
-        variables.concat(persisted_environment.predefined_variables)
-
-        # Here we're passing unexpanded environment_url for runner to expand,
-        # and we need to make sure that CI_ENVIRONMENT_NAME and
-        # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
-        variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
-      end
-    end
-
     def legacy_variables
       Gitlab::Ci::Variables::Collection.new.tap do |variables|
-        variables.append(key: 'CI_BUILD_ID', value: id.to_s)
-        variables.append(key: 'CI_BUILD_TOKEN', value: token, public: false)
         variables.append(key: 'CI_BUILD_REF', value: sha)
         variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
         variables.append(key: 'CI_BUILD_REF_NAME', value: ref)
@@ -604,6 +640,19 @@ module Ci
       end
     end
 
+    def persisted_environment_variables
+      Gitlab::Ci::Variables::Collection.new.tap do |variables|
+        return variables unless persisted? && persisted_environment.present?
+
+        variables.concat(persisted_environment.predefined_variables)
+
+        # Here we're passing unexpanded environment_url for runner to expand,
+        # and we need to make sure that CI_ENVIRONMENT_NAME and
+        # CI_ENVIRONMENT_SLUG so on are available for the URL be expanded.
+        variables.append(key: 'CI_ENVIRONMENT_URL', value: environment_url) if environment_url
+      end
+    end
+
     def environment_url
       options&.dig(:environment, :url) || persisted_environment&.external_url
     end
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
new file mode 100644
index 0000000000000000000000000000000000000000..96762f8845ccaa6539f4387325d9da89fbc54da9
--- /dev/null
+++ b/app/models/ci/build_metadata.rb
@@ -0,0 +1,35 @@
+module Ci
+  # The purpose of this class is to store Build related data that can be disposed.
+  # Data that should be persisted forever, should be stored with Ci::Build model.
+  class BuildMetadata < ActiveRecord::Base
+    extend Gitlab::Ci::Model
+    include Presentable
+    include ChronicDurationAttribute
+
+    self.table_name = 'ci_builds_metadata'
+
+    belongs_to :build, class_name: 'Ci::Build'
+    belongs_to :project
+
+    validates :build, presence: true
+    validates :project, presence: true
+
+    chronic_duration_attr_reader :timeout_human_readable, :timeout
+
+    enum timeout_source: {
+        unknown_timeout_source: 1,
+        project_timeout_source: 2,
+        runner_timeout_source: 3
+    }
+
+    def update_timeout_state
+      return unless build.runner.present?
+
+      project_timeout = project&.build_timeout
+      timeout = [project_timeout, build.runner.maximum_timeout].compact.min
+      timeout_source = timeout < project_timeout ? :runner_timeout_source : :project_timeout_source
+
+      update(timeout: timeout, timeout_source: timeout_source)
+    end
+  end
+end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 7173f88f1c71339fd8f88fa3a9c2f8ceea2ebb14..5a4c56ec0dc5f8fd786a7517d0341eeb55f08d69 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -3,12 +3,13 @@ module Ci
     extend Gitlab::Ci::Model
     include Gitlab::SQL::Pattern
     include RedisCacheable
+    include ChronicDurationAttribute
 
     RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
     ONLINE_CONTACT_TIMEOUT = 1.hour
     UPDATE_DB_RUNNER_INFO_EVERY = 40.minutes
     AVAILABLE_SCOPES = %w[specific shared active paused online].freeze
-    FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level].freeze
+    FORM_EDITABLE = %i[description tag_list active run_untagged locked access_level maximum_timeout_human_readable].freeze
 
     has_many :builds
     has_many :runner_projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -51,6 +52,12 @@ module Ci
 
     cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
 
+    chronic_duration_attr :maximum_timeout_human_readable, :maximum_timeout
+
+    validates :maximum_timeout, allow_nil: true,
+                                numericality: { greater_than_or_equal_to: 600,
+                                                message: 'needs to be at least 10 minutes' }
+
     # Searches for runners matching the given query.
     #
     # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index bfdfc5ae6fe99eca3bdb9dde0828095e701517e3..77947d515c1c32468dd55faae93be9b9c3604123 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -51,6 +51,10 @@ module Clusters
 
     scope :enabled, -> { where(enabled: true) }
     scope :disabled, -> { where(enabled: false) }
+    scope :user_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:user]) }
+    scope :gcp_provided, -> { where(provider_type: ::Clusters::Cluster.provider_types[:gcp]) }
+    scope :gcp_installed, -> { gcp_provided.includes(:provider_gcp).where(cluster_providers_gcp: { status: ::Clusters::Providers::Gcp.state_machines[:status].states[:created].value }) }
+
     scope :default_environment, -> { where(environment_scope: DEFAULT_ENVIRONMENT) }
 
     def status_name
diff --git a/app/models/clusters/concerns/application_status.rb b/app/models/clusters/concerns/application_status.rb
index 7b7c8eac773cead666a1db90df1062dcbd0ddf41..8f3eb75bfa9d587f03127a6f7fe4ee43641677c6 100644
--- a/app/models/clusters/concerns/application_status.rb
+++ b/app/models/clusters/concerns/application_status.rb
@@ -4,6 +4,8 @@ module Clusters
       extend ActiveSupport::Concern
 
       included do
+        scope :installed, -> { where(status: self.state_machines[:status].states[:installed].value) }
+
         state_machine :status, initial: :not_installable do
           state :not_installable, value: -2
           state :errored, value: -1
diff --git a/app/models/concerns/chronic_duration_attribute.rb b/app/models/concerns/chronic_duration_attribute.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fa1eafb1d7a2eb9dfb478c82dafa388dac3aae49
--- /dev/null
+++ b/app/models/concerns/chronic_duration_attribute.rb
@@ -0,0 +1,39 @@
+module ChronicDurationAttribute
+  extend ActiveSupport::Concern
+
+  class_methods do
+    def chronic_duration_attr_reader(virtual_attribute, source_attribute)
+      define_method(virtual_attribute) do
+        chronic_duration_attributes[virtual_attribute] || output_chronic_duration_attribute(source_attribute)
+      end
+    end
+
+    def chronic_duration_attr_writer(virtual_attribute, source_attribute)
+      chronic_duration_attr_reader(virtual_attribute, source_attribute)
+
+      define_method("#{virtual_attribute}=") do |value|
+        chronic_duration_attributes[virtual_attribute] = value.presence || ''
+
+        begin
+          new_value = ChronicDuration.parse(value).to_i if value.present?
+          assign_attributes(source_attribute => new_value)
+        rescue ChronicDuration::DurationParseError
+          # ignore error as it will be caught by validation
+        end
+      end
+
+      validates virtual_attribute, allow_nil: true, duration: true
+    end
+
+    alias_method :chronic_duration_attr, :chronic_duration_attr_writer
+  end
+
+  def chronic_duration_attributes
+    @chronic_duration_attributes ||= {}
+  end
+
+  def output_chronic_duration_attribute(source_attribute)
+    value = attributes[source_attribute.to_s]
+    ChronicDuration.output(value, format: :short) if value
+  end
+end
diff --git a/app/models/deploy_key.rb b/app/models/deploy_key.rb
index c2e0a5fa12688a219bc5c6b1ad0a94dbe435b912..89a74b7dcb1d94a8fb612121454d1ce1698b4219 100644
--- a/app/models/deploy_key.rb
+++ b/app/models/deploy_key.rb
@@ -27,6 +27,10 @@ class DeployKey < Key
     self.private?
   end
 
+  def user
+    super || User.ghost
+  end
+
   def has_access_to?(project)
     deploy_keys_project_for(project).present?
   end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 7bfc45c1f43ae3606eb6c57bd7828cfd3818bc49..6a94d60c828fd13c959343b57e62d2ede25b890b 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -23,6 +23,7 @@ class Issue < ActiveRecord::Base
 
   belongs_to :project
   belongs_to :moved_to, class_name: 'Issue'
+  belongs_to :closed_by, class_name: 'User'
 
   has_internal_id :iid, scope: :project, init: ->(s) { s&.project&.issues&.maximum(:iid) }
 
@@ -78,6 +79,11 @@ class Issue < ActiveRecord::Base
     before_transition any => :closed do |issue|
       issue.closed_at = Time.zone.now
     end
+
+    before_transition closed: :opened do |issue|
+      issue.closed_at = nil
+      issue.closed_by = nil
+    end
   end
 
   class << self
diff --git a/app/models/project.rb b/app/models/project.rb
index 6a420663644cca542036f634a01216dd8eeb005a..b343786d2c9b3c5a4ac80a5c375e0908c3501298 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1346,20 +1346,19 @@ class Project < ActiveRecord::Base
     Dir.exist?(public_pages_path)
   end
 
-  def pages_url
-    subdomain, _, url_path = full_path.partition('/')
-
-    # The hostname always needs to be in downcased
-    # All web servers convert hostname to lowercase
-    host = "#{subdomain}.#{Settings.pages.host}".downcase
-
+  def pages_group_url
     # The host in URL always needs to be downcased
-    url = Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
-      "#{prefix}#{subdomain}."
+    Gitlab.config.pages.url.sub(%r{^https?://}) do |prefix|
+      "#{prefix}#{pages_subdomain}."
     end.downcase
+  end
+
+  def pages_url
+    url = pages_group_url
+    url_path = full_path.partition('/').last
 
     # If the project path is the same as host, we serve it as group page
-    return url if host == url_path
+    return url if url == "#{Settings.pages.protocol}://#{url_path}"
 
     "#{url}/#{url_path}"
   end
@@ -1545,8 +1544,8 @@ class Project < ActiveRecord::Base
     @errors = original_errors
   end
 
-  def add_export_job(current_user:, params: {})
-    job_id = ProjectExportWorker.perform_async(current_user.id, self.id, params)
+  def add_export_job(current_user:, after_export_strategy: nil, params: {})
+    job_id = ProjectExportWorker.perform_async(current_user.id, self.id, after_export_strategy, params)
 
     if job_id
       Rails.logger.info "Export job started for project ID #{self.id} with job ID #{job_id}"
@@ -1572,6 +1571,8 @@ class Project < ActiveRecord::Base
   def export_status
     if export_in_progress?
       :started
+    elsif after_export_in_progress?
+      :after_export_action
     elsif export_project_path
       :finished
     else
@@ -1583,12 +1584,22 @@ class Project < ActiveRecord::Base
     import_export_shared.active_export_count > 0
   end
 
+  def after_export_in_progress?
+    import_export_shared.after_export_in_progress?
+  end
+
   def remove_exports
     return nil unless export_path.present?
 
     FileUtils.rm_rf(export_path)
   end
 
+  def remove_exported_project_file
+    return unless export_project_path.present?
+
+    FileUtils.rm_f(export_project_path)
+  end
+
   def full_path_slug
     Gitlab::Utils.slugify(full_path.to_s)
   end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 2ba1c6cb8c97d0552bc8fab019455df92620bf59..fd1afafe4dfc3d436afe44407c98073c4d32261d 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -249,13 +249,13 @@ class Repository
   end
 
   def diverging_commit_counts(branch)
-    root_ref_hash = raw_repository.commit(root_ref).id
+    @root_ref_hash ||= raw_repository.commit(root_ref).id
     cache.fetch(:"diverging_commit_counts_#{branch.name}") do
       # Rugged seems to throw a `ReferenceError` when given branch_names rather
       # than SHA-1 hashes
       number_commits_behind, number_commits_ahead =
         raw_repository.count_commits_between(
-          root_ref_hash,
+          @root_ref_hash,
           branch.dereferenced_target.sha,
           left_right: true,
           max_count: MAX_DIVERGING_COUNT)
diff --git a/app/models/service.rb b/app/models/service.rb
index 1dcb79157a2c6f541e7babc6e96c4cc47d8b1692..7424cef0fc05b8ee01f1baac5d19b5e9cca14d08 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -273,6 +273,7 @@ class Service < ActiveRecord::Base
 
   def self.build_from_template(project_id, template)
     service = template.dup
+    service.active = false unless service.valid?
     service.template = false
     service.project_id = project_id
     service
diff --git a/app/models/user.rb b/app/models/user.rb
index 187878f4fb516745a5a129766a5353a40f2fd0b2..f934b6542257cc36a321dae0a312fd9bd370f4c7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -82,11 +82,8 @@ class User < ActiveRecord::Base
   has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
 
   # Profile
-  has_many :keys, -> do
-    type = Key.arel_table[:type]
-    where(type.not_eq('DeployKey').or(type.eq(nil)))
-  end, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-  has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+  has_many :keys, -> { where(type: ['Key', nil]) }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+  has_many :deploy_keys, -> { where(type: 'DeployKey') }, dependent: :nullify # rubocop:disable Cop/ActiveRecordDependent
   has_many :gpg_keys
 
   has_many :emails, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
diff --git a/app/presenters/ci/build_metadata_presenter.rb b/app/presenters/ci/build_metadata_presenter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5048f967ea862cafce4653c82904a64708b7dce2
--- /dev/null
+++ b/app/presenters/ci/build_metadata_presenter.rb
@@ -0,0 +1,18 @@
+module Ci
+  class BuildMetadataPresenter < Gitlab::View::Presenter::Delegated
+    TIMEOUT_SOURCES = {
+        unknown_timeout_source: nil,
+        project_timeout_source: 'project',
+        runner_timeout_source: 'runner'
+    }.freeze
+
+    presents :metadata
+
+    def timeout_source
+      return unless metadata.timeout_source?
+
+      TIMEOUT_SOURCES[metadata.timeout_source.to_sym] ||
+        metadata.timeout_source
+    end
+  end
+end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 69d46f5ec14d91387e3c96e10fcc6234f8dc3cf0..ca4480fe2b18de06ea8550d6f99d5f5b3b57e076 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -5,6 +5,8 @@ class BuildDetailsEntity < JobEntity
   expose :runner, using: RunnerEntity
   expose :pipeline, using: PipelineEntity
 
+  expose :metadata, using: BuildMetadataEntity
+
   expose :erased_by, if: -> (*) { build.erased? }, using: UserEntity
   expose :erase_path, if: -> (*) { build.erasable? && can?(current_user, :erase_build, build) } do |build|
     erase_project_job_path(project, build)
diff --git a/app/serializers/build_metadata_entity.rb b/app/serializers/build_metadata_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..39f429aa6c37b8ffd3302613217c5ead3e7ca1cc
--- /dev/null
+++ b/app/serializers/build_metadata_entity.rb
@@ -0,0 +1,9 @@
+class BuildMetadataEntity < Grape::Entity
+  expose :timeout_human_readable do |metadata|
+    metadata.timeout_human_readable unless metadata.timeout.nil?
+  end
+
+  expose :timeout_source do |metadata|
+    metadata.present.timeout_source
+  end
+end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
index 3e40ecf1c1c092cba15f3d3c4606c044cbb570a1..a7c2e21e92bc0a3a37ff03a54d9455a4aaf6aecb 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -7,8 +7,14 @@ class StatusEntity < Grape::Entity
   expose :details_path
 
   expose :favicon do |status|
-    dir = 'ci_favicons'
-    dir = File.join(dir, 'dev') if Rails.env.development?
+    dir =
+      if Gitlab::Utils.to_boolean(ENV['CANARY'])
+        File.join('ci_favicons', 'canary')
+      elsif Rails.env.development?
+        File.join('ci_favicons', 'dev')
+      else
+        'ci_favicons'
+      end
 
     ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
   end
diff --git a/app/services/boards/list_service.rb b/app/services/boards/list_service.rb
index 6d0dd0a9f99559b107e516b322692c0ac5dedcf1..9269b8d26202b7bb6ced46831de1c47346168085 100644
--- a/app/services/boards/list_service.rb
+++ b/app/services/boards/list_service.rb
@@ -2,11 +2,15 @@ module Boards
   class ListService < Boards::BaseService
     def execute
       create_board! if parent.boards.empty?
-      parent.boards
+      boards
     end
 
     private
 
+    def boards
+      parent.boards
+    end
+
     def create_board!
       Boards::CreateService.new(parent, current_user).execute
     end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index 0c5cf2c62ade5809f8d39f62c53e7dcf1c4569f4..fee5bc38f7b4bb3fc4313b50aebd76e6be5d42d2 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -23,6 +23,7 @@ module Issues
       end
 
       if project.issues_enabled? && issue.close
+        issue.update(closed_by: current_user)
         event_service.close_issue(issue, current_user)
         create_note(issue, commit) if system_note
         notification_service.close_issue(issue, current_user) if notifications
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 7fa1387084cd6c2fd1bafd902bc3442cc34fd227..633e2c8236cd234a4632748d6a6e805a63147f97 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -90,9 +90,6 @@ module Projects
       unless @project.gitlab_project_import?
         @project.write_repository_config
         @project.create_wiki unless skip_wiki?
-        create_services_from_active_templates(@project)
-
-        @project.create_labels
       end
 
       event_service.create_project(@project, current_user)
@@ -121,21 +118,29 @@ module Projects
       Project.transaction do
         @project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
 
-        if @project.save && !@project.import?
-          raise 'Failed to create repository' unless @project.create_repository
+        if @project.save
+          unless @project.gitlab_project_import?
+            create_services_from_active_templates(@project)
+            @project.create_labels
+          end
+
+          unless @project.import?
+            raise 'Failed to create repository' unless @project.create_repository
+          end
         end
       end
     end
 
     def fail(error:)
       message = "Unable to save project. Error: #{error}"
-      message << "Project ID: #{@project.id}" if @project && @project.id
+      log_message = message.dup
 
-      Rails.logger.error(message)
+      log_message << " Project ID: #{@project.id}" if @project&.id
+      Rails.logger.error(log_message)
 
-      if @project && @project.import?
+      if @project
         @project.errors.add(:base, message)
-        @project.mark_import_as_failed(message)
+        @project.mark_import_as_failed(message) if @project.import?
       end
 
       @project
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index d16aa3de63984e107a5924ff0290c3a7e7c6031a..402cddd3ec1b69f4848d38b7b2d68eb758d9d392 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -1,22 +1,36 @@
 module Projects
   module ImportExport
     class ExportService < BaseService
-      def execute(_options = {})
+      def execute(after_export_strategy = nil, options = {})
         @shared = project.import_export_shared
-        save_all
+
+        save_all!
+        execute_after_export_action(after_export_strategy)
       end
 
       private
 
-      def save_all
-        if [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
+      def execute_after_export_action(after_export_strategy)
+        return unless after_export_strategy
+
+        unless after_export_strategy.execute(current_user, project)
+          cleanup_and_notify_error
+        end
+      end
+
+      def save_all!
+        if save_services
           Gitlab::ImportExport::Saver.save(project: project, shared: @shared)
           notify_success
         else
-          cleanup_and_notify
+          cleanup_and_notify_error!
         end
       end
 
+      def save_services
+        [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
+      end
+
       def version_saver
         Gitlab::ImportExport::VersionSaver.new(shared: @shared)
       end
@@ -41,19 +55,22 @@ module Projects
         Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
       end
 
-      def cleanup_and_notify
+      def cleanup_and_notify_error
         Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
 
         FileUtils.rm_rf(@shared.export_path)
 
         notify_error
+      end
+
+      def cleanup_and_notify_error!
+        cleanup_and_notify_error
+
         raise Gitlab::ImportExport::Error.new(@shared.errors.join(', '))
       end
 
       def notify_success
         Rails.logger.info("Import/Export - Project #{project.name} with ID: #{project.id} successfully exported")
-
-        notification_service.project_exported(@project, @current_user)
       end
 
       def notify_error
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index a34024f4f807b309692e7a6f4e27be891e811346..a3828acc50b408995ccb6986ff07e01f6801f61c 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -28,7 +28,11 @@ module Projects
 
     def add_repository_to_project
       if project.external_import? && !unknown_url?
-        raise Error, 'Blocked import URL.' if Gitlab::UrlBlocker.blocked_url?(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
+        begin
+          Gitlab::UrlBlocker.validate!(project.import_url, valid_ports: Project::VALID_IMPORT_PORTS)
+        rescue Gitlab::UrlBlocker::BlockedUrlError => e
+          raise Error, "Blocked import URL: #{e.message}"
+        end
       end
 
       # We should skip the repository for a GitHub import or GitLab project import,
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 5bf8208e0357da86160167664ff234827bc66c98..9c8877be14e4f342068effd01391c5cc762dcb1a 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -178,6 +178,9 @@ module Projects
 
     def latest_sha
       project.commit(build.ref).try(:sha).to_s
+    ensure
+      # Close any file descriptors that were opened and free libgit2 buffers
+      project.cleanup
     end
 
     def sha
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index 30cc4425ae4097c41b000909e0caa147ca4d96c7..4028b0527681fa17b6410e21634581ebeb03c08b 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -228,16 +228,9 @@ module ObjectStorage
       raise 'Failed to update object store' unless updated
     end
 
-    def use_file
-      if file_storage?
-        return yield path
-      end
-
-      begin
-        cache_stored_file!
-        yield cache_path
-      ensure
-        cache_storage.delete_dir!(cache_path(nil))
+    def use_file(&blk)
+      with_exclusive_lease do
+        unsafe_use_file(&blk)
       end
     end
 
@@ -247,12 +240,9 @@ module ObjectStorage
     #   new_store: Enum (Store::LOCAL, Store::REMOTE)
     #
     def migrate!(new_store)
-      uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
-      raise 'Already running' unless uuid
-
-      unsafe_migrate!(new_store)
-    ensure
-      Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid)
+      with_exclusive_lease do
+        unsafe_migrate!(new_store)
+      end
     end
 
     def schedule_background_upload(*args)
@@ -384,6 +374,15 @@ module ObjectStorage
       "object_storage_migrate:#{model.class}:#{model.id}"
     end
 
+    def with_exclusive_lease
+      uuid = Gitlab::ExclusiveLease.new(exclusive_lease_key, timeout: 1.hour.to_i).try_obtain
+      raise 'exclusive lease already taken' unless uuid
+
+      yield uuid
+    ensure
+      Gitlab::ExclusiveLease.cancel(exclusive_lease_key, uuid)
+    end
+
     #
     # Move the file to another store
     #
@@ -418,4 +417,18 @@ module ObjectStorage
       raise e
     end
   end
+
+  def unsafe_use_file
+    if file_storage?
+      return yield path
+    end
+
+    begin
+      cache_stored_file!
+      yield cache_path
+    ensure
+      FileUtils.rm_f(cache_path)
+      cache_storage.delete_dir!(cache_path(nil))
+    end
+  end
 end
diff --git a/app/validators/certificate_fingerprint_validator.rb b/app/validators/certificate_fingerprint_validator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..17df756183a994b413aa3bfbfa20ba367f95370e
--- /dev/null
+++ b/app/validators/certificate_fingerprint_validator.rb
@@ -0,0 +1,9 @@
+class CertificateFingerprintValidator < ActiveModel::EachValidator
+  FINGERPRINT_PATTERN = /\A([a-zA-Z0-9]{2}[\s\-:]?){16,}\z/.freeze
+
+  def validate_each(record, attribute, value)
+    unless value.try(:match, FINGERPRINT_PATTERN)
+      record.errors.add(attribute, "must be a hash containing only letters, numbers, spaces, : and -")
+    end
+  end
+end
diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb
index 3ec1594e20217aacc01e31d3209478cc098f7ed5..612d3c71913d6497e27075d82436209b87c9bc81 100644
--- a/app/validators/importable_url_validator.rb
+++ b/app/validators/importable_url_validator.rb
@@ -4,8 +4,8 @@
 # protect against Server-side Request Forgery (SSRF).
 class ImportableUrlValidator < ActiveModel::EachValidator
   def validate_each(record, attribute, value)
-    if Gitlab::UrlBlocker.blocked_url?(value, valid_ports: Project::VALID_IMPORT_PORTS)
-      record.errors.add(attribute, "imports are not allowed from that URL")
-    end
+    Gitlab::UrlBlocker.validate!(value, valid_ports: Project::VALID_IMPORT_PORTS)
+  rescue Gitlab::UrlBlocker::BlockedUrlError => e
+    record.errors.add(attribute, "is blocked: #{e.message}")
   end
 end
diff --git a/app/validators/top_level_group_validator.rb b/app/validators/top_level_group_validator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7e2e735e0cf605dc5e94b2ddb8472cb40ee3e4c7
--- /dev/null
+++ b/app/validators/top_level_group_validator.rb
@@ -0,0 +1,7 @@
+class TopLevelGroupValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    if value&.subgroup?
+      record.errors.add(attribute, "must be a top level Group")
+    end
+  end
+end
diff --git a/app/views/admin/application_settings/_background_jobs.html.haml b/app/views/admin/application_settings/_background_jobs.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..8198a822a10321fd090c797ef06e943a4bd9e064
--- /dev/null
+++ b/app/views/admin/application_settings/_background_jobs.html.haml
@@ -0,0 +1,30 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+  = form_errors(@application_setting)
+
+  %fieldset
+    %p
+      These settings require a
+      = link_to 'restart', help_page_path('administration/restart_gitlab')
+      to take effect.
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :sidekiq_throttling_enabled do
+            = f.check_box :sidekiq_throttling_enabled
+            Enable Sidekiq Job Throttling
+          .help-block
+            Limit the amount of resources slow running jobs are assigned.
+    .form-group
+      = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
+        .help-block
+          Choose which queues you wish to throttle.
+    .form-group
+      = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
+        .help-block
+          The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 636535fba84a98791c1cff97315ac37c54f3cc72..309c7ed5dfa1d9187baa5f7d90754845b276e667 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -9,111 +9,6 @@
         .col-sm-10
           = f.number_field :container_registry_token_expire_delay, class: 'form-control'
 
-  %fieldset
-    %legend Profiling - Performance Bar
-    %p
-      Enable the Performance Bar for a given group.
-      = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :performance_bar_enabled do
-            = f.check_box :performance_bar_enabled
-            Enable the Performance Bar
-    .form-group
-      = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
-
-  %fieldset
-    %legend Background Jobs
-    %p
-      These settings require a
-      = link_to 'restart', help_page_path('administration/restart_gitlab')
-      to take effect.
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :sidekiq_throttling_enabled do
-            = f.check_box :sidekiq_throttling_enabled
-            Enable Sidekiq Job Throttling
-          .help-block
-            Limit the amount of resources slow running jobs are assigned.
-    .form-group
-      = f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
-        .help-block
-          Choose which queues you wish to throttle.
-    .form-group
-      = f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
-        .help-block
-          The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
-
-  %fieldset
-    %legend Spam and Anti-bot Protection
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :recaptcha_enabled do
-            = f.check_box :recaptcha_enabled
-            Enable reCAPTCHA
-          %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
-
-    .form-group
-      = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.text_field :recaptcha_site_key, class: 'form-control'
-        .help-block
-          Generate site and private keys at
-          %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
-
-    .form-group
-      = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.text_field :recaptcha_private_key, class: 'form-control'
-
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :akismet_enabled do
-            = f.check_box :akismet_enabled
-            Enable Akismet
-          %span.help-block#akismet_help_block Helps prevent bots from creating issues
-
-    .form-group
-      = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.text_field :akismet_api_key, class: 'form-control'
-        .help-block
-          Generate API key at
-          %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
-
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :unique_ips_limit_enabled do
-            = f.check_box :unique_ips_limit_enabled
-            Limit sign in from multiple ips
-          %span.help-block#unique_ip_help_block
-            Helps prevent malicious users hide their activity
-
-    .form-group
-      = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :unique_ips_limit_per_user, class: 'form-control'
-        .help-block
-          Maximum number of unique IPs per user
-
-    .form-group
-      = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :unique_ips_limit_time_window, class: 'form-control'
-        .help-block
-          How many seconds an IP will be counted towards the limit
-
   %fieldset
     %legend Abuse reports
     .form-group
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..5344f030c970b6241346bdf3b832076aff89d42b
--- /dev/null
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -0,0 +1,16 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+  = form_errors(@application_setting)
+
+  %fieldset
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :performance_bar_enabled do
+            = f.check_box :performance_bar_enabled
+            Enable the Performance Bar
+    .form-group
+      = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..25e89097dfef9df0b5c2f81529654b4991f8dd25
--- /dev/null
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -0,0 +1,65 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+  = form_errors(@application_setting)
+
+  %fieldset
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :recaptcha_enabled do
+            = f.check_box :recaptcha_enabled
+            Enable reCAPTCHA
+          %span.help-block#recaptcha_help_block Helps prevent bots from creating accounts
+
+    .form-group
+      = f.label :recaptcha_site_key, 'reCAPTCHA Site Key', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.text_field :recaptcha_site_key, class: 'form-control'
+        .help-block
+          Generate site and private keys at
+          %a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
+
+    .form-group
+      = f.label :recaptcha_private_key, 'reCAPTCHA Private Key', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.text_field :recaptcha_private_key, class: 'form-control'
+
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :akismet_enabled do
+            = f.check_box :akismet_enabled
+            Enable Akismet
+          %span.help-block#akismet_help_block Helps prevent bots from creating issues
+
+    .form-group
+      = f.label :akismet_api_key, 'Akismet API Key', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.text_field :akismet_api_key, class: 'form-control'
+        .help-block
+          Generate API key at
+          %a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
+
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :unique_ips_limit_enabled do
+            = f.check_box :unique_ips_limit_enabled
+            Limit sign in from multiple ips
+          %span.help-block#unique_ip_help_block
+            Helps prevent malicious users hide their activity
+
+    .form-group
+      = f.label :unique_ips_limit_per_user, 'IPs per user', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :unique_ips_limit_per_user, class: 'form-control'
+        .help-block
+          Maximum number of unique IPs per user
+
+    .form-group
+      = f.label :unique_ips_limit_time_window, 'IP expiration time', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :unique_ips_limit_time_window, class: 'form-control'
+        .help-block
+          How many seconds an IP will be counted towards the limit
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index 17f2f37d24e22d4650084b1dda1fbef4f87d89b1..d0e612e62e524a236f6f9b3ad159bb22fcfc74af 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -7,7 +7,7 @@
   .settings-header
     %h4
       = _('Visibility and access controls')
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       = _('Set default and restrict visibility levels. Configure import sources and git access protocol.')
@@ -18,7 +18,7 @@
   .settings-header
     %h4
       = _('Account and limit settings')
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       = _('Session expiration, projects limit and attachment size.')
@@ -29,7 +29,7 @@
   .settings-header
     %h4
       = _('Sign-up restrictions')
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       = _('Configure the way a user creates a new account.')
@@ -40,7 +40,7 @@
   .settings-header
     %h4
       = _('Sign-in restrictions')
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       = _('Set requirements for a user to sign-in. Enable mandatory two-factor authentication.')
@@ -51,7 +51,7 @@
   .settings-header
     %h4
       = _('Help page')
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       = _('Help page text and support page url.')
@@ -62,7 +62,7 @@
   .settings-header
     %h4
       = _('Pages')
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       = _('Size and domain settings for static websites')
@@ -73,7 +73,7 @@
   .settings-header
     %h4
       = _('Continuous Integration and Deployment')
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       = _('Auto DevOps, runners amd job artifacts')
@@ -84,7 +84,7 @@
   .settings-header
     %h4
       = _('Metrics - Influx')
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       = _('Enable and configure InfluxDB metrics.')
@@ -95,12 +95,46 @@
   .settings-header
     %h4
       = _('Metrics - Prometheus')
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       = _('Enable and configure Prometheus metrics.')
   .settings-content
     = render 'prometheus'
 
+%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Profiling - Performance bar')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Enable the Performance Bar for a given group.')
+      = link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
+  .settings-content
+    = render 'performance_bar'
+
+%section.settings.as-background.no-animate#js-background-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Background jobs')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Configure Sidekiq job throttling.')
+  .settings-content
+    = render 'background_jobs'
+
+%section.settings.as-spam.no-animate#js-spam-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Spam and Anti-bot Protection')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Enable reCAPTCHA or Akismet and set IP limits.')
+  .settings-content
+    = render 'spam'
+
 .prepend-top-20
   = render 'form'
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index 5d4229c80af72e6671e35fb8e2cda5cfe7ba46b2..440623b34f58fe2ad82048016ef3a4095c61e793 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -43,7 +43,5 @@
           %span.toggle-icon
             = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked')
             = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked')
-      -# EE-specific start
-      -# EE-specific end
   %button.js-row-remove-button.ci-variable-row-remove-button{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
     = icon('minus-circle')
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 7f9486d08d9fa2594b1fb81e059801640947cbd4..8e1dea4afc1cee3c45db871caa7162365248c081 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,5 +1,5 @@
 - @no_container = true
-- breadcrumb_title "Details"
+- breadcrumb_title _("Details")
 - can_create_subgroups = can?(current_user, :create_subgroup, @group)
 
 = content_for :meta_tags do
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
index 54ef51b30e3cf20930ddaa075774a1988304b215..c63cf2b31cb454b3882a194d41714a05b3a99dcd 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -22,9 +22,6 @@
     = text_field_tag :personal_access_token, '', class: 'form-control', placeholder: _('Personal Access Token'), size: 40
     = submit_tag _('List your GitHub repositories'), class: 'btn btn-success'
 
-  -# EE-specific start
-  -# EE-specific end
-
 - unless github_import_configured?
   %hr
   %p
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 257f7326409eaf1984ef069c27ecf5cf9d658241..6513b7191995b38c729666ca17134ad41fc071ca 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -1,5 +1,5 @@
 !!! 5
-%html.devise-layout-html
+%html.devise-layout-html{ class: system_message_class }
   = render "layouts/head"
   %body.ui_indigo.login-page.application.navless{ data: { page: body_data_page } }
     .page-wrap
@@ -16,7 +16,7 @@
               %h1
                 = brand_title
               = brand_image
-              - if brand_item&.description?
+              - if current_appearance&.description?
                 = brand_text
               - else
                 %h3 Open source software to collaborate on code
diff --git a/app/views/layouts/devise_empty.html.haml b/app/views/layouts/devise_empty.html.haml
index 8718bb3db1a5e8955ccad9b9ac4d1ac37fc4852c..adf90cb7667c2b6139edfcfa7f73168352bfd917 100644
--- a/app/views/layouts/devise_empty.html.haml
+++ b/app/views/layouts/devise_empty.html.haml
@@ -1,5 +1,5 @@
 !!! 5
-%html{ lang: "en" }
+%html{ lang: "en", class: system_message_class }
   = render "layouts/head"
   %body.ui_indigo.login-page.application.navless
     = render "layouts/header/empty"
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 059571f795f3f83fcbf07e37627e841f24241fe7..5c90d13420f9499b853c3c8fbed51dec5c15362a 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -80,14 +80,6 @@
               = link_to charts_project_graph_path(@project, current_ref) do
                 #{ _('Charts') }
 
-      - if project_nav_tab? :container_registry
-        = nav_link(controller: %w[projects/registry/repositories]) do
-          = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
-            .nav-icon-container
-              = sprite_icon('disk')
-            %span.nav-item-name
-              Registry
-
       - if project_nav_tab? :issues
         = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
           = link_to project_issues_path(@project), class: 'shortcuts-issues' do
@@ -231,6 +223,14 @@
                   %span
                     Charts
 
+      - if project_nav_tab? :container_registry
+        = nav_link(controller: %w[projects/registry/repositories]) do
+          = link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
+            .nav-icon-container
+              = sprite_icon('disk')
+            %span.nav-item-name
+              Registry
+
       - if project_nav_tab? :wiki
         = nav_link(controller: :wikis) do
           = link_to get_project_wiki_path(@project), class: 'shortcuts-wiki' do
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 5dfe973f33cfe1754f49ae0a451ff6d545a50e41..825bfd0707ff4e0bb9758d1788f90a490cfe3430 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -7,7 +7,7 @@
   .settings-header
     %h4
       Export project
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index 2ee0eafcf1af341df963ac2ce9360881d331a798..4c510293204aa66b81cea241663c28bfdd5f166b 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -31,7 +31,7 @@
   %section.settings#js-cluster-details{ class: ('expanded' if expanded) }
     .settings-header
       %h4= s_('ClusterIntegration|Kubernetes cluster details')
-      %button.btn.js-settings-toggle
+      %button.btn.js-settings-toggle{ type: 'button' }
         = expanded ? 'Collapse' : 'Expand'
       %p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
     .settings-content
@@ -43,7 +43,7 @@
   %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
     .settings-header
       %h4= _('Advanced settings')
-      %button.btn.js-settings-toggle
+      %button.btn.js-settings-toggle{ type: 'button' }
         = expanded ? 'Collapse' : 'Expand'
       %p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration")
     .settings-content
diff --git a/app/views/projects/deploy_keys/_index.html.haml b/app/views/projects/deploy_keys/_index.html.haml
index 75dd4c9ae15fb1a096a46389f9c669a7bb700f51..7dd8dc28e5b58c5ac8e9a80530f882341b704b8f 100644
--- a/app/views/projects/deploy_keys/_index.html.haml
+++ b/app/views/projects/deploy_keys/_index.html.haml
@@ -3,7 +3,7 @@
   .settings-header
     %h4
       Deploy Keys
-    %button.btn.js-settings-toggle.qa-expand-deploy-keys
+    %button.btn.js-settings-toggle.qa-expand-deploy-keys{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index a96485ab1553051044db6543a903fe69e6263388..99eeb9551e3695dcc280434383f3146f1cd4e9a2 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -8,7 +8,7 @@
     .settings-header
       %h4
         General project settings
-      %button.btn.js-settings-toggle
+      %button.btn.js-settings-toggle{ type: 'button' }
         = expanded ? 'Collapse' : 'Expand'
       %p
         Update your project name, description, avatar, and other general settings.
@@ -64,7 +64,7 @@
     .settings-header
       %h4
         Permissions
-      %button.btn.js-settings-toggle
+      %button.btn.js-settings-toggle{ type: 'button' }
         = expanded ? 'Collapse' : 'Expand'
       %p
         Enable or disable certain project features and choose access levels.
@@ -79,7 +79,7 @@
     .settings-header
       %h4
         Merge request settings
-      %button.btn.js-settings-toggle
+      %button.btn.js-settings-toggle{ type: 'button' }
         = expanded ? 'Collapse' : 'Expand'
       %p
         Customize your merge request restrictions.
@@ -94,7 +94,7 @@
     .settings-header
       %h4
         Advanced settings
-      %button.btn.js-settings-toggle
+      %button.btn.js-settings-toggle{ type: 'button' }
         = expanded ? 'Collapse' : 'Expand'
       %p
         Perform advanced options such as housekeeping, archiving, renaming, transferring, or removing your project.
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 8a36fada3892a843b0dd68b097d13a45d0d6096d..b15fe514a0812e7bf006667440c8e63580dd4818 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,5 +1,5 @@
 - @no_container = true
-- breadcrumb_title "Details"
+- breadcrumb_title _("Details")
 
 = render partial: 'flash_messages', locals: { project: @project }
 
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index 849c273db8c14668fb4ab685316aae795093843a..fa27ded7cc25c2312ba2507cb3413aa2ac85b198 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -111,4 +111,4 @@
 
 .js-build-options{ data: javascript_build_options }
 
-#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json) } }
+#js-job-details-vue{ data: { endpoint: project_job_path(@project, @build, format: :json), runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner') } }
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index b423888c8759921e3c3755a13b7771c78ba60ff1..5ec219fdf009f9a389175d9f53f919d248180afa 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -30,6 +30,7 @@
           %button.js-promote-project-milestone-button.btn.btn-grouped{ data: { toggle: 'modal',
             target: '#promote-milestone-modal',
             milestone_title: @milestone.title,
+            group_name: @project.group.name,
             url: promote_project_milestone_path(@milestone.project, @milestone),
             container: 'body' },
             disabled: true,
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 8cdb0a6aff44e7138f3469b5a5ab70d51981a684..b66e05596038d0ad600fe7f9bc8c11461caee8cd 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -18,8 +18,6 @@
         = _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link }
       %p
         = _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.')
-      -# EE-specific start
-      -# EE-specific end
       .md
         = brand_new_project_guidelines
       %p
@@ -43,8 +41,6 @@
           %a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' }
             %span.hidden-xs Import project
             %span.visible-xs Import
-        -# EE-specific start
-        -# EE-specific end
 
       .tab-content.gitlab-tab-content
         .tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
@@ -110,10 +106,6 @@
                       = render "shared/import_form", f: f
                       = render 'new_project_fields', f: f, project_name_id: "import-url-name"
 
-
-        -# EE-specific start
-        -# EE-specific end
-
 .save-project-loader.hide
   .center
     %h2
diff --git a/app/views/projects/no_repo.html.haml b/app/views/projects/no_repo.html.haml
index ba5845877e54fc25ee6b38bcda47dfeef1fcf055..14d880028c7ba762771858d6f2fc260c56f5d3f7 100644
--- a/app/views/projects/no_repo.html.haml
+++ b/app/views/projects/no_repo.html.haml
@@ -1,3 +1,5 @@
+- breadcrumb_title _("Details")
+
 %h2
   %i.fa.fa-warning
   #{ _('No repository') }
@@ -10,7 +12,7 @@
 
 .no-repo-actions
   = link_to project_repository_path(@project), method: :post, class: 'btn btn-primary' do
-    #{ _('Create empty bare repository') }
+    #{ _('Create empty repository') }
 
   %strong.prepend-left-10.append-right-10 or
 
@@ -19,4 +21,4 @@
 
 - if can? current_user, :remove_project, @project
   .prepend-top-20
-    = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
+    = link_to _('Remove project'), project_path(@project), data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-inverted btn-remove pull-right"
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/projects/protected_branches/shared/_index.html.haml
index e662b877fbb4b5f85055fb0ddffc0249ea9faf38..55d87c35a80f323d2c9b39f4015c41872192eeed 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/projects/protected_branches/shared/_index.html.haml
@@ -4,7 +4,7 @@
   .settings-header
     %h4
       Protected Branches
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       Keep stable branches secure and force developers to use merge requests.
diff --git a/app/views/projects/protected_tags/shared/_index.html.haml b/app/views/projects/protected_tags/shared/_index.html.haml
index 24baf1cfc892e4e0e629505b5353b5d9e5a7b658..c33723d8072232b103e0b2532bfe6dd3404d406b 100644
--- a/app/views/projects/protected_tags/shared/_index.html.haml
+++ b/app/views/projects/protected_tags/shared/_index.html.haml
@@ -4,7 +4,7 @@
   .settings-header
     %h4
       Protected Tags
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       Limit access to creating and updating tags.
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index 49c9086914605c7b9177a4cafc3a436b91e2bf1c..6a681736b6f662a74189c8f2527337b202867c69 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -39,6 +39,12 @@
       Description
     .col-sm-10
       = f.text_field :description, class: 'form-control'
+  .form-group
+    = label_tag :maximum_timeout_human_readable, class: 'control-label' do
+      Maximum job timeout
+    .col-sm-10
+      = f.text_field :maximum_timeout_human_readable, class: 'form-control'
+      .help-block This timeout will take precedence when lower than Project-defined timeout
   .form-group
     = label_tag :tag_list, class: 'control-label' do
       Tags
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
index 4e57f5f844d3602602a212c1792a6836afbebc6a..f33e7e25b682d3216e327f298c052101eedb7981 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/projects/runners/show.html.haml
@@ -55,6 +55,9 @@
     %tr
       %td Description
       %td= @runner.description
+    %tr
+      %td Maximum job timeout
+      %td= @runner.maximum_timeout_human_readable
     %tr
       %td Last contact
       %td
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 756f31f91d942a461e154a46284149f36f815ffc..d65341dbd409a9d40f62ea9e869d8be2d70ec36f 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -8,7 +8,7 @@
   .settings-header
     %h4
       General pipelines settings
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       Update your CI/CD configuration, like job timeout or Auto DevOps.
@@ -19,7 +19,7 @@
   .settings-header
     %h4
       Runners settings
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       Register and see your runners for this project.
@@ -31,7 +31,7 @@
     %h4
       = _('Secret variables')
       = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p.append-bottom-0
       = render "ci/variables/content"
@@ -42,7 +42,7 @@
   .settings-header
     %h4
       Pipeline triggers
-    %button.btn.js-settings-toggle
+    %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
       Triggers can force a specific branch or tag to get rebuilt with an API call.  These tokens will
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index fa281327eb70332dd4fbeb0ee5bebe4c1032fe03..94331a16abd6863c533e007958466d83f84328fd 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -1,5 +1,5 @@
 - @no_container = true
-- breadcrumb_title "Details"
+- breadcrumb_title _("Details")
 - @content_class = "limit-container-width" unless fluid_layout
 - show_auto_devops_callout = show_auto_devops_callout?(@project)
 
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 5eaaa1448d5d072eb5e4f986d8846f5d3744432d..3806ead6c873e8ee2255f22225afc80ed31752f7 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -17,6 +17,3 @@
         = import_will_timeout_message(ci_cd_only)
       %li
         = import_svn_message(ci_cd_only)
-
--# EE-specific start
--# EE-specific end
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 5afbc78df5310680ab10b62c5061cdd9848bf798..5640390784475674da8bc4e5e97b6c16f7e423b7 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -55,6 +55,7 @@
                 label_title: label.title,
                 label_color: label.color,
                 label_text_color: label.text_color,
+                group_name: label.project.group.name,
                 target: '#promote-label-modal',
                 container: 'body',
                 toggle: 'modal' } }
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index adaddda13ebf93f84ff39ed7d759a429cbba6332..6afcd447f28efae4526b98c0de5c5589647a84ce 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -84,7 +84,7 @@
                   = dropdown_content do
                     .js-due-date-calendar
 
-      - if @labels && @labels.any?
+      - if @labels
         - selected_labels = issuable.labels
         .block.labels
           .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index 5926867e2d744b91181d896f611becc51618b587..ac494814f55d47dad9c37f03acb8a27972f66ba7 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -56,6 +56,7 @@
               type: 'button',
               data: { url: promote_project_milestone_path(milestone.project, milestone),
                       milestone_title: milestone.title,
+                      group_name: @project.group.name,
                       target: '#promote-milestone-modal',
                       container: 'body',
                       toggle: 'modal' } }
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 6006ab8b43fe46833ff3ada4fac5d41dbaacb444..f302299eb24f3340575409e3943fd3b11d65bba9 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -1,4 +1,5 @@
-- page_title milestone.title, "Milestones"
+- page_title @milestone.title
+- @breadcrumb_link = dashboard_milestone_path(milestone.safe_title, title: milestone.title)
 
 - group = local_assigns[:group]
 
@@ -17,7 +18,7 @@
     Milestone #{milestone.title}
   - if milestone.due_date || milestone.start_date
     %span.creator
-      &middot;
+      &nbsp;&middot;
       = milestone_date_range(milestone)
   - if group
     .pull-right
diff --git a/app/workers/object_storage/migrate_uploads_worker.rb b/app/workers/object_storage/migrate_uploads_worker.rb
index 01ed123e6c8baadec02da1b9759a9765d3607daf..a6b2c25125416f892ea564a723d04b7268c8a62f 100644
--- a/app/workers/object_storage/migrate_uploads_worker.rb
+++ b/app/workers/object_storage/migrate_uploads_worker.rb
@@ -138,21 +138,18 @@ module ObjectStorage
 
     include Report
 
-    def self.enqueue!(uploads, mounted_as, to_store)
-      sanity_check!(uploads, mounted_as)
+    def self.enqueue!(uploads, model_class, mounted_as, to_store)
+      sanity_check!(uploads, model_class, mounted_as)
 
-      perform_async(uploads.ids, mounted_as, to_store)
+      perform_async(uploads.ids, model_class.to_s, mounted_as, to_store)
     end
 
     # We need to be sure all the uploads are for the same uploader and model type
     # and that the mount point exists if provided.
     #
-    def self.sanity_check!(uploads, mounted_as)
+    def self.sanity_check!(uploads, model_class, mounted_as)
       upload = uploads.first
-
       uploader_class = upload.uploader.constantize
-      model_class = uploads.first.model_type.constantize
-
       uploader_types = uploads.map(&:uploader).uniq
       model_types = uploads.map(&:model_type).uniq
       model_has_mount = mounted_as.nil? || model_class.uploaders[mounted_as] == uploader_class
@@ -162,7 +159,12 @@ module ObjectStorage
       raise(SanityCheckError, "Mount point #{mounted_as} not found in #{model_class}.") unless model_has_mount
     end
 
-    def perform(ids, mounted_as, to_store)
+    def perform(*args)
+      args_check!(args)
+
+      (ids, model_type, mounted_as, to_store) = args
+
+      @model_class = model_type.constantize
       @mounted_as = mounted_as&.to_sym
       @to_store = to_store
 
@@ -178,7 +180,17 @@ module ObjectStorage
     end
 
     def sanity_check!(uploads)
-      self.class.sanity_check!(uploads, @mounted_as)
+      self.class.sanity_check!(uploads, @model_class, @mounted_as)
+    end
+
+    def args_check!(args)
+      return if args.count == 4
+
+      case args.count
+      when 3 then raise SanityCheckError, "Job is missing the `model_type` argument."
+      else
+        raise SanityCheckError, "Job has wrong arguments format."
+      end
     end
 
     def build_uploaders(uploads)
diff --git a/app/workers/project_export_worker.rb b/app/workers/project_export_worker.rb
index 0b502143e5d0b36046d659124bed6f73ce1173b5..c3d84bb0b93bf18bc6865d4024163ca6aa6189c2 100644
--- a/app/workers/project_export_worker.rb
+++ b/app/workers/project_export_worker.rb
@@ -4,11 +4,19 @@ class ProjectExportWorker
 
   sidekiq_options retry: 3
 
-  def perform(current_user_id, project_id, params = {})
-    params = params.with_indifferent_access
+  def perform(current_user_id, project_id, after_export_strategy = {}, params = {})
     current_user = User.find(current_user_id)
     project = Project.find(project_id)
+    after_export = build!(after_export_strategy)
 
-    ::Projects::ImportExport::ExportService.new(project, current_user, params).execute
+    ::Projects::ImportExport::ExportService.new(project, current_user, params).execute(after_export)
+  end
+
+  private
+
+  def build!(after_export_strategy)
+    strategy_klass = after_export_strategy&.delete('klass')
+
+    Gitlab::ImportExport::AfterExportStrategyBuilder.build!(strategy_klass, after_export_strategy)
   end
 end
diff --git a/changelogs/unreleased/17516-nested-restore-changelog.yml b/changelogs/unreleased/17516-nested-restore-changelog.yml
new file mode 100644
index 0000000000000000000000000000000000000000..89753f45457ec1bca21d849a54db04f3a6387fb7
--- /dev/null
+++ b/changelogs/unreleased/17516-nested-restore-changelog.yml
@@ -0,0 +1,5 @@
+---
+title: Enable restore rake task to handle nested storage directories
+merge_request: 17516
+author: Balasankar C
+type: fixed
diff --git a/changelogs/unreleased/41967_issue_api_closed_by_info.yml b/changelogs/unreleased/41967_issue_api_closed_by_info.yml
new file mode 100644
index 0000000000000000000000000000000000000000..436574c3638edc2fb9be92ed985d45d5a15226dd
--- /dev/null
+++ b/changelogs/unreleased/41967_issue_api_closed_by_info.yml
@@ -0,0 +1,5 @@
+---
+title: adds closed by informations in issue api
+merge_request: 17042
+author: haseebeqx
+type: added
diff --git a/changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml b/changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b5c12d8f40eb547e610dea5e7d9e7fe8b694557b
--- /dev/null
+++ b/changelogs/unreleased/44291-usage-ping-for-kubernetes-integration.yml
@@ -0,0 +1,5 @@
+---
+title: Add additional cluster usage metrics to usage ping.
+merge_request: 17922
+author:
+type: changed
diff --git a/changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml b/changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3bbd5a05b98b4a5c4ab244e66f26c94b976c73bb
--- /dev/null
+++ b/changelogs/unreleased/44392-resolve-projects-creation-silently-failing-on-after-create-error.yml
@@ -0,0 +1,5 @@
+---
+title: Project creation will now raise an error if a service template is invalid
+merge_request: 18013
+author:
+type: fixed
diff --git a/changelogs/unreleased/44508-fix-fork-namespace-images.yml b/changelogs/unreleased/44508-fix-fork-namespace-images.yml
new file mode 100644
index 0000000000000000000000000000000000000000..63b4b9a5e560816dc322e95f2fec494c020f6f1f
--- /dev/null
+++ b/changelogs/unreleased/44508-fix-fork-namespace-images.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug rendering group icons when forking
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/44587-autolinking-includes-trailing-exclamation-marks.yml b/changelogs/unreleased/44587-autolinking-includes-trailing-exclamation-marks.yml
deleted file mode 100644
index 636fde601ee1ee7872e1ddc6ea58693a9f9fe22d..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/44587-autolinking-includes-trailing-exclamation-marks.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't capture trailing punctuation when autolinking
-merge_request: 17965
-author:
-type: fixed
diff --git a/changelogs/unreleased/44608-Cloning-a-repository-over-HTTPS-with-LDAP-credentials-causes-a-HTTP-401-Access-denied.yml b/changelogs/unreleased/44608-Cloning-a-repository-over-HTTPS-with-LDAP-credentials-causes-a-HTTP-401-Access-denied.yml
deleted file mode 100644
index 5afb1e3d90813cef51c0e6560dde119cdec76aa1..0000000000000000000000000000000000000000
--- a/changelogs/unreleased/44608-Cloning-a-repository-over-HTTPS-with-LDAP-credentials-causes-a-HTTP-401-Access-denied.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: 'Cloning a repository over HTTPS with LDAP credentials causes a HTTP 401 Access denied'
-merge_request: !17988
-author: Horatiu Eugen Vlad
-type: fixed
diff --git a/changelogs/unreleased/44649-reference-parsing-conflicting-with-auto-linking.yml b/changelogs/unreleased/44649-reference-parsing-conflicting-with-auto-linking.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a64b0efa1edf7cc4f7f5fbae05dc945f56ff4a11
--- /dev/null
+++ b/changelogs/unreleased/44649-reference-parsing-conflicting-with-auto-linking.yml
@@ -0,0 +1,5 @@
+---
+title: Fix autolinking URLs containing ampersands
+merge_request: 18045
+author:
+type: fixed
diff --git a/changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml b/changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4f21aadd86b1cbb8a7cf4b1a04fcb5f88113931d
--- /dev/null
+++ b/changelogs/unreleased/44657-reuse-root_ref_hash-on-branches.yml
@@ -0,0 +1,5 @@
+---
+title: Reuse root_ref_hash for performance on Branches
+merge_request: 17998
+author: Takuya Noguchi
+type: performance
diff --git a/changelogs/unreleased/44717-no-resolve-issue.yml b/changelogs/unreleased/44717-no-resolve-issue.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ce23f4e6e9fa50ab45278222342045241b0db5fe
--- /dev/null
+++ b/changelogs/unreleased/44717-no-resolve-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Don't show Jump to Discussion button on Issues
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml b/changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml
new file mode 100644
index 0000000000000000000000000000000000000000..372f42939642613789cb6ff3182e4c97f17a996e
--- /dev/null
+++ b/changelogs/unreleased/44774-migrate-upload-task-fails-for-upload-with-store-nil.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed gitlab:uploads:migrate task ignoring some uploads.
+merge_request: 18082
+author:
+type: fixed
diff --git a/changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml b/changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6094fcd0b3e276fc070a77055c6e56f2b18d4ca0
--- /dev/null
+++ b/changelogs/unreleased/44776-fix-upload-migrate-fails-for-group.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed gitlab:uploads:migrate task failing for Groups' avatar.
+merge_request: 18088
+author:
+type: fixed
diff --git a/changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml b/changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f5710cf4f7f0a34b8a329601f2d2b59dc7f0ab6d
--- /dev/null
+++ b/changelogs/unreleased/44878-update-brakeman-3-6-1-to-4-2-1.yml
@@ -0,0 +1,5 @@
+---
+title: Update brakeman 3.6.1 to 4.2.1
+merge_request: 18122
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/Link_to_project_labels_page.yml b/changelogs/unreleased/Link_to_project_labels_page.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7bdeec423fcd380a3c0b7b923e2c4fc2a8b36289
--- /dev/null
+++ b/changelogs/unreleased/Link_to_project_labels_page.yml
@@ -0,0 +1,5 @@
+---
+title: Always display Labels section in issuable sidebar, even when the project has no labels
+merge_request: 18081
+author: Branka Martinovic
+type: fixed
diff --git a/changelogs/unreleased/ac-fix-use_file-race.yml b/changelogs/unreleased/ac-fix-use_file-race.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f1315d5d50e9ba77393c23e1e1a64902f05026ba
--- /dev/null
+++ b/changelogs/unreleased/ac-fix-use_file-race.yml
@@ -0,0 +1,5 @@
+---
+title: Fix data race between ObjectStorage background_upload and Pages publishing
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/ac-pages-port.yml b/changelogs/unreleased/ac-pages-port.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4f7257b47989f80c5d29593f4e1e0e64a1e64f28
--- /dev/null
+++ b/changelogs/unreleased/ac-pages-port.yml
@@ -0,0 +1,5 @@
+---
+title: Add missing port to artifact links
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-canary-favicon.yml b/changelogs/unreleased/add-canary-favicon.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1af6572588d409fe689ac5b023eae3f69bc790fb
--- /dev/null
+++ b/changelogs/unreleased/add-canary-favicon.yml
@@ -0,0 +1,5 @@
+---
+title: Add yellow favicon when `CANARY=true` to differientate canary environment
+merge_request: 12477
+author:
+type: changed
diff --git a/changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml b/changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml
new file mode 100644
index 0000000000000000000000000000000000000000..015bee99170b7a3f465c1ac33913133721c402cc
--- /dev/null
+++ b/changelogs/unreleased/add-milestone-path-to-dashboard-milestones-breadcrumb-link.yml
@@ -0,0 +1,5 @@
+---
+title: Update dashboard milestones breadcrumb link
+merge_request: 17933
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/add-per-runner-job-timeout.yml b/changelogs/unreleased/add-per-runner-job-timeout.yml
new file mode 100644
index 0000000000000000000000000000000000000000..336b4d15ddfa2a9b867521b7f29ce76f375ab978
--- /dev/null
+++ b/changelogs/unreleased/add-per-runner-job-timeout.yml
@@ -0,0 +1,5 @@
+---
+title: Add per-runner configured job timeout
+merge_request: 17221
+author:
+type: added
diff --git a/changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml b/changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9885c8853cc06dfac29d4c3229db8a45316993bd
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-bump-html-pipeline-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Bump html-pipeline to 2.7.1
+merge_request: 18132
+author: "@blackst0ne"
+type: other
diff --git a/changelogs/unreleased/dm-deploy-keys-default-user.yml b/changelogs/unreleased/dm-deploy-keys-default-user.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b82d67d028ce49333d69dfa4bfd4e7593e8c14e6
--- /dev/null
+++ b/changelogs/unreleased/dm-deploy-keys-default-user.yml
@@ -0,0 +1,5 @@
+---
+title: Ensure hooks run when a deploy key without a user pushes
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/escape-autocomplete-values-for-markdown.yml b/changelogs/unreleased/escape-autocomplete-values-for-markdown.yml
new file mode 100644
index 0000000000000000000000000000000000000000..eea9da4c579046b0e9ce00f9d4f93ef8f2c22fc2
--- /dev/null
+++ b/changelogs/unreleased/escape-autocomplete-values-for-markdown.yml
@@ -0,0 +1,5 @@
+---
+title: Escape Markdown characters properly when using autocomplete
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/expose-commits-mr-api.yml b/changelogs/unreleased/expose-commits-mr-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..77ea2f274312d0218d383686636fca01e35dbc2a
--- /dev/null
+++ b/changelogs/unreleased/expose-commits-mr-api.yml
@@ -0,0 +1,5 @@
+---
+title: Allow merge requests related to a commit to be found via API
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml b/changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml
new file mode 100644
index 0000000000000000000000000000000000000000..84977ce11c8c00f5e018e3b43a63936fe04d17cc
--- /dev/null
+++ b/changelogs/unreleased/feature-gb-variables-expressions-in-only-except.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for pipeline variables expressions in only/except
+merge_request: 17316
+author:
+type: added
diff --git a/changelogs/unreleased/fix-gb-fix-background-pipeline-stages-migration.yml b/changelogs/unreleased/fix-gb-fix-background-pipeline-stages-migration.yml
new file mode 100644
index 0000000000000000000000000000000000000000..63948f0c1963b479cd4b4353630ee22f72a719a2
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-fix-background-pipeline-stages-migration.yml
@@ -0,0 +1,5 @@
+---
+title: Fix exceptions raised when migrating pipeline stages in the background
+merge_request: 18076
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-projects-no-repository-placeholder.yml b/changelogs/unreleased/fix-projects-no-repository-placeholder.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3d11c8970209ddbff639e8221f762ad01c82aebc
--- /dev/null
+++ b/changelogs/unreleased/fix-projects-no-repository-placeholder.yml
@@ -0,0 +1,5 @@
+---
+title: Update no repository placeholder
+merge_request: 17964
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml b/changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a06499d821a0f8baea024e0d3a56cd34981cd677
--- /dev/null
+++ b/changelogs/unreleased/fj-42685-extend-project-export-endpoint.yml
@@ -0,0 +1,5 @@
+---
+title: Extend API for exporting a project with direct upload URL
+merge_request: 17686
+author:
+type: added
diff --git a/changelogs/unreleased/ide-file-row-hover-style.yml b/changelogs/unreleased/ide-file-row-hover-style.yml
new file mode 100644
index 0000000000000000000000000000000000000000..158379a5aefec76fd862a78ac19118072d2e61f6
--- /dev/null
+++ b/changelogs/unreleased/ide-file-row-hover-style.yml
@@ -0,0 +1,5 @@
+---
+title: Added hover background color to IDE file list rows
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/jivl-change-copy-text-promote-milestones-labels.yml b/changelogs/unreleased/jivl-change-copy-text-promote-milestones-labels.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fb3095552d328fb4d30382dbe9696f6e6c276d3b
--- /dev/null
+++ b/changelogs/unreleased/jivl-change-copy-text-promote-milestones-labels.yml
@@ -0,0 +1,5 @@
+---
+title: Correct copy text for the promote milestone and label modals
+merge_request: 17726
+author:
+type: fixed
diff --git a/changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml b/changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml
new file mode 100644
index 0000000000000000000000000000000000000000..03a6fd42228435991d400b378fc048ceac483ab8
--- /dev/null
+++ b/changelogs/unreleased/move-registry-after-cicd-project-nav-sidebar.yml
@@ -0,0 +1,5 @@
+---
+  title: Move 'Registry' after 'CI/CD' in project navigation sidebar
+  merge_request: 18018
+  author: Elias Werberich
+  type: changed
diff --git a/changelogs/unreleased/sh-cleanup-pages-worker.yml b/changelogs/unreleased/sh-cleanup-pages-worker.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c26e1342dd28fa242fae25d14f9b1f06b50f0017
--- /dev/null
+++ b/changelogs/unreleased/sh-cleanup-pages-worker.yml
@@ -0,0 +1,5 @@
+---
+title: Free open file descriptors and libgit2 buffers in UpdatePagesService
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/workhorse-gitaly-mandatory.yml b/changelogs/unreleased/workhorse-gitaly-mandatory.yml
new file mode 100644
index 0000000000000000000000000000000000000000..77b62302e863b0cc80dae9717d6395445f3ff8c8
--- /dev/null
+++ b/changelogs/unreleased/workhorse-gitaly-mandatory.yml
@@ -0,0 +1,5 @@
+---
+title: Make all workhorse gitaly calls opt-out, take 2
+merge_request: 18043
+author:
+type: other
diff --git a/changelogs/unreleased/zj-remote-repo-exists.yml b/changelogs/unreleased/zj-remote-repo-exists.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f024b83159bba50ba16d1cf6d8bc29fb937ff2fa
--- /dev/null
+++ b/changelogs/unreleased/zj-remote-repo-exists.yml
@@ -0,0 +1,5 @@
+---
+title: Test if remote repository exists when importing wikis
+merge_request:
+author:
+type: fixed
diff --git a/config/webpack.config.js b/config/webpack.config.js
index b74d9dde494bd747e257e0ecd9322d9ff31e7e67..39e9fbbd5304eee4a54b022d54087e2ace428c4c 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -104,7 +104,7 @@ const config = {
         },
       },
       {
-        test: /katex.css$/,
+        test: /katex.min.css$/,
         include: /node_modules\/katex\/dist/,
         use: [
           { loader: 'style-loader' },
diff --git a/db/migrate/20180209165249_add_closed_by_to_issues.rb b/db/migrate/20180209165249_add_closed_by_to_issues.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e251afd7b49894facb79425e66a06efc1c3ffa5d
--- /dev/null
+++ b/db/migrate/20180209165249_add_closed_by_to_issues.rb
@@ -0,0 +1,20 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddClosedByToIssues < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  disable_ddl_transaction!
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  def up
+    add_column :issues, :closed_by_id, :integer
+    add_concurrent_foreign_key :issues, :users, column: :closed_by_id, on_delete: :nullify
+  end
+
+  def down
+    remove_foreign_key :issues, column: :closed_by_id
+    remove_column :issues, :closed_by_id
+  end
+end
diff --git a/db/migrate/20180219153455_add_maximum_timeout_to_ci_runners.rb b/db/migrate/20180219153455_add_maximum_timeout_to_ci_runners.rb
new file mode 100644
index 0000000000000000000000000000000000000000..072e696a43e5bffd04a0624a0a5112449f2938e5
--- /dev/null
+++ b/db/migrate/20180219153455_add_maximum_timeout_to_ci_runners.rb
@@ -0,0 +1,9 @@
+class AddMaximumTimeoutToCiRunners < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    add_column :ci_runners, :maximum_timeout, :integer
+  end
+end
diff --git a/db/migrate/20180301010859_create_ci_builds_metadata_table.rb b/db/migrate/20180301010859_create_ci_builds_metadata_table.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ce73744409233ce04672f370f11baa5d6835a8a5
--- /dev/null
+++ b/db/migrate/20180301010859_create_ci_builds_metadata_table.rb
@@ -0,0 +1,20 @@
+class CreateCiBuildsMetadataTable < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def change
+    create_table :ci_builds_metadata do |t|
+      t.integer :build_id, null: false
+      t.integer :project_id, null: false
+      t.integer :timeout
+      t.integer :timeout_source, null: false, default: 1
+
+      t.foreign_key :ci_builds, column: :build_id, on_delete: :cascade
+      t.foreign_key :projects, column: :project_id, on_delete: :cascade
+
+      t.index :build_id, unique: true
+      t.index :project_id
+    end
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 77b3d836287db5df45fd2c6ec5a39fac1c92fa94..06fc1a9d7e923ec878d88e7c033bd3cfc413e884 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -329,6 +329,16 @@ ActiveRecord::Schema.define(version: 20180327101207) do
   add_index "ci_builds", ["updated_at"], name: "index_ci_builds_on_updated_at", using: :btree
   add_index "ci_builds", ["user_id"], name: "index_ci_builds_on_user_id", using: :btree
 
+  create_table "ci_builds_metadata", force: :cascade do |t|
+    t.integer "build_id", null: false
+    t.integer "project_id", null: false
+    t.integer "timeout"
+    t.integer "timeout_source", default: 1, null: false
+  end
+
+  add_index "ci_builds_metadata", ["build_id"], name: "index_ci_builds_metadata_on_build_id", unique: true, using: :btree
+  add_index "ci_builds_metadata", ["project_id"], name: "index_ci_builds_metadata_on_project_id", using: :btree
+
   create_table "ci_group_variables", force: :cascade do |t|
     t.string "key", null: false
     t.text "value"
@@ -459,6 +469,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
     t.boolean "locked", default: false, null: false
     t.integer "access_level", default: 0, null: false
     t.string "ip_address"
+    t.integer "maximum_timeout"
   end
 
   add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
@@ -921,6 +932,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
     t.integer "last_edited_by_id"
     t.boolean "discussion_locked"
     t.datetime_with_timezone "closed_at"
+    t.integer "closed_by_id"
   end
 
   add_index "issues", ["author_id"], name: "index_issues_on_author_id", using: :btree
@@ -2027,6 +2039,8 @@ ActiveRecord::Schema.define(version: 20180327101207) do
   add_foreign_key "ci_builds", "ci_pipelines", column: "auto_canceled_by_id", name: "fk_a2141b1522", on_delete: :nullify
   add_foreign_key "ci_builds", "ci_stages", column: "stage_id", name: "fk_3a9eaa254d", on_delete: :cascade
   add_foreign_key "ci_builds", "projects", name: "fk_befce0568a", on_delete: :cascade
+  add_foreign_key "ci_builds_metadata", "ci_builds", column: "build_id", on_delete: :cascade
+  add_foreign_key "ci_builds_metadata", "projects", on_delete: :cascade
   add_foreign_key "ci_group_variables", "namespaces", column: "group_id", name: "fk_33ae4d58d8", on_delete: :cascade
   add_foreign_key "ci_job_artifacts", "ci_builds", column: "job_id", on_delete: :cascade
   add_foreign_key "ci_job_artifacts", "projects", on_delete: :cascade
@@ -2082,6 +2096,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
   add_foreign_key "issues", "milestones", name: "fk_96b1dd429c", on_delete: :nullify
   add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade
   add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify
+  add_foreign_key "issues", "users", column: "closed_by_id", name: "fk_c63cbf6c25", on_delete: :nullify
   add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify
   add_foreign_key "label_priorities", "labels", on_delete: :cascade
   add_foreign_key "label_priorities", "projects", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index be805a2ccc4f9de81ac9a74d86b8b8c64643163a..604f7244a346dab48809088b26b71761a6a6d224 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -211,9 +211,9 @@ straight away.
 
 ### GitLab self-hosted
 
-With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Libre, Starter, Premium, and Ultimate.
+With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Core, Starter, Premium, and Ultimate.
 
-Every feature available in Libre is also available in Starter, Premium, and Ultimate.
+Every feature available in Core is also available in Starter, Premium, and Ultimate.
 Starter features are also available in Premium and Ultimate, and Premium features are also
 available in Ultimate.
 
@@ -227,7 +227,7 @@ GitLab.com subscriptions grants access
 to the same features available in GitLab self-hosted, **expect
 [administration](administration/index.md) tools and settings**:
 
-- GitLab.com Free includes the same features available in GitLab Libre
+- GitLab.com Free includes the same features available in Core
 - GitLab.com Bronze includes the same features available in GitLab Starter
 - GitLab.com Silver includes the same features available in GitLab Premium
 - GitLab.com Gold includes the same features available in GitLab Ultimate
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 4366590578a9410612fff5d05bcf8f0458c4424f..60a45426636d2ac22cf8395588041c8f0216ebfa 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -11,8 +11,8 @@ available through [different subscriptions](https://about.gitlab.com/products/).
 
 You can [install GitLab CE or GitLab EE](https://about.gitlab.com/installation/ce-or-ee/),
 but the features you'll have access to depend on the subscription you choose
-(Libre, Starter, Premium, or Ultimate). GitLab Community Edition installations
-only have access to Libre features.
+(Core, Starter, Premium, or Ultimate). GitLab Community Edition installations
+only have access to Core features.
 
 GitLab.com is administered by GitLab, Inc., therefore, only GitLab team members have
 access to its admin configurations. If you're a GitLab.com user, please check the
diff --git a/doc/administration/issue_closing_pattern.md b/doc/administration/issue_closing_pattern.md
index 28e1fd4e12e2d09b628a4a560a7634f392cdd125..466bb1f851e9058e5cb3fa5bc14edb6036c87d62 100644
--- a/doc/administration/issue_closing_pattern.md
+++ b/doc/administration/issue_closing_pattern.md
@@ -24,11 +24,11 @@ Because Rubular doesn't understand `%{issue_ref}`, you can replace this by
 **For Omnibus installations**
 
 1. Open `/etc/gitlab/gitlab.rb` with your editor.
-1. Change the value of `gitlab_rails['issue_closing_pattern']` to a regular
+1. Change the value of `gitlab_rails['gitlab_issue_closing_pattern']` to a regular
    expression of your liking:
 
     ```ruby
-    gitlab_rails['issue_closing_pattern'] = "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
+    gitlab_rails['gitlab_issue_closing_pattern'] = "((?:[Cc]los(?:e[sd]|ing)|[Ff]ix(?:e[sd]|ing)?) +(?:(?:issues? +)?%{issue_ref}(?:(?:, *| +and +)?))+)"
     ```
 1. [Reconfigure] GitLab for the changes to take effect.
 
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 55c673fd06a167792d1a65e234e07c2e2de2a70d..db0a80d04d916cb1e1331b879c53be5433196ecd 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -412,9 +412,10 @@ Example response:
 
 Since GitLab 8.1, this is the new commit status API.
 
-### Get the status of a commit
+### List the statuses of a commit
 
-Get the statuses of a commit in a project.
+List the statuses of a commit in a project.
+The pagination parameters `page` and `per_page` can be used to restrict the list of references.
 
 ```
 GET /projects/:id/repository/commits/:sha/statuses
@@ -536,6 +537,74 @@ Example response:
 }
 ```
 
+## List Merge Requests associated with a commit
+
+Get a list of Merge Requests related to the specified commit.
+
+```
+GET /projects/:id/repository/commits/:sha/merge_requests
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id`      | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
+| `sha`     | string  | yes   | The commit SHA
+
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/af5b13261899fb2c0db30abdd0af8b07cb44fdc5/merge_requests"
+```
+
+Example response:
+
+```json
+[
+   {
+      "id":45,
+      "iid":1,
+      "project_id":35,
+      "title":"Add new file",
+      "description":"",
+      "state":"opened",
+      "created_at":"2018-03-26T17:26:30.916Z",
+      "updated_at":"2018-03-26T17:26:30.916Z",
+      "target_branch":"master",
+      "source_branch":"test-branch",
+      "upvotes":0,
+      "downvotes":0,
+      "author" : {
+        "web_url" : "https://gitlab.example.com/thedude",
+        "name" : "Jeff Lebowski",
+        "avatar_url" : "https://gitlab.example.com/uploads/user/avatar/28/The-Big-Lebowski-400-400.png",
+        "username" : "thedude",
+        "state" : "active",
+        "id" : 28
+      },
+      "assignee":null,
+      "source_project_id":35,
+      "target_project_id":35,
+      "labels":[ ],
+      "work_in_progress":false,
+      "milestone":null,
+      "merge_when_pipeline_succeeds":false,
+      "merge_status":"can_be_merged",
+      "sha":"af5b13261899fb2c0db30abdd0af8b07cb44fdc5",
+      "merge_commit_sha":null,
+      "user_notes_count":0,
+      "discussion_locked":null,
+      "should_remove_source_branch":null,
+      "force_remove_source_branch":false,
+      "web_url":"http://https://gitlab.example.com/root/test-project/merge_requests/1",
+      "time_stats":{
+         "time_estimate":0,
+         "total_time_spent":0,
+         "human_time_estimate":null,
+         "human_total_time_spent":null
+      }
+   }
+]
+```
+
 [ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
 [ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047
 [ce-15026]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15026
diff --git a/doc/api/events.md b/doc/api/events.md
index 129af0afa35faa58061ec5af1b654347411f4b80..f4d26c4de1cfa8f7f6d04973ab10d5a1456bbaec 100644
--- a/doc/api/events.md
+++ b/doc/api/events.md
@@ -42,6 +42,10 @@ Dates for the `before` and `after` parameters should be supplied in the followin
 YYYY-MM-DD
 ```
 
+### Event Time Period Limit
+
+GitLab removes events older than 1 year from the events table for performance reasons. The range of 1 year was chosen because user contribution calendars only show contributions of the past year.
+
 ## List currently authenticated user's events
 
 >**Note:** This endpoint was introduced in GitLab 9.3.
diff --git a/doc/api/issues.md b/doc/api/issues.md
index a4a511012973da6a82b18d0def41a9e08a12521b..7479c1d2f93bd93de3aef5c203a525ced3784f55 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -100,6 +100,7 @@ Example response:
       },
       "updated_at" : "2016-01-04T15:31:51.081Z",
       "closed_at" : null,
+      "closed_by" : null,
       "id" : 76,
       "title" : "Consequatur vero maxime deserunt laboriosam est voluptas dolorem.",
       "created_at" : "2016-01-04T15:31:51.081Z",
@@ -122,6 +123,8 @@ Example response:
 
 **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
 
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
 ## List group issues
 
 Get a list of a group's issues.
@@ -216,6 +219,7 @@ Example response:
       "updated_at" : "2016-01-04T15:31:46.176Z",
       "created_at" : "2016-01-04T15:31:46.176Z",
       "closed_at" : null,
+      "closed_by" : null,
       "user_notes_count": 1,
       "due_date": null,
       "web_url": "http://example.com/example/example/issues/1",
@@ -233,6 +237,8 @@ Example response:
 
 **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
 
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
 ## List project issues
 
 Get a list of a project's issues.
@@ -326,6 +332,14 @@ Example response:
       "updated_at" : "2016-01-04T15:31:46.176Z",
       "created_at" : "2016-01-04T15:31:46.176Z",
       "closed_at" : "2016-01-05T15:31:46.176Z",
+      "closed_by" : {
+         "state" : "active",
+         "web_url" : "https://gitlab.example.com/root",
+         "avatar_url" : null,
+         "username" : "root",
+         "id" : 1,
+         "name" : "Administrator"
+      },
       "user_notes_count": 1,
       "due_date": "2016-07-22",
       "web_url": "http://example.com/example/example/issues/1",
@@ -343,6 +357,8 @@ Example response:
 
 **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
 
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
 ## Single issue
 
 Get a single project issue.
@@ -409,6 +425,8 @@ Example response:
    "title" : "Ut commodi ullam eos dolores perferendis nihil sunt.",
    "updated_at" : "2016-01-04T15:31:46.176Z",
    "created_at" : "2016-01-04T15:31:46.176Z",
+   "closed_at" : null,
+   "closed_by" : null,
    "subscribed": false,
    "user_notes_count": 1,
    "due_date": null,
@@ -432,6 +450,8 @@ Example response:
 
 **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
 
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
 ## New issue
 
 Creates a new project issue.
@@ -484,6 +504,7 @@ Example response:
    "description" : null,
    "updated_at" : "2016-01-07T12:44:33.959Z",
    "closed_at" : null,
+   "closed_by" : null,
    "milestone" : null,
    "subscribed" : true,
    "user_notes_count": 0,
@@ -508,6 +529,8 @@ Example response:
 
 **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
 
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
 ## Edit issue
 
 Updates an existing project issue. This call is also used to mark an issue as
@@ -556,6 +579,14 @@ Example response:
    "description" : null,
    "updated_at" : "2016-01-07T12:55:16.213Z",
    "closed_at" : "2016-01-08T12:55:16.213Z",
+   "closed_by" : {
+      "state" : "active",
+      "web_url" : "https://gitlab.example.com/root",
+      "avatar_url" : null,
+      "username" : "root",
+      "id" : 1,
+      "name" : "Administrator"
+    },
    "iid" : 15,
    "labels" : [
       "bug"
@@ -587,6 +618,8 @@ Example response:
 
 **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
 
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
 ## Delete an issue
 
 Only for admins and project owners. Soft deletes the issue in question.
@@ -640,6 +673,7 @@ Example response:
   "created_at": "2016-04-05T21:41:45.652Z",
   "updated_at": "2016-04-07T12:20:17.596Z",
   "closed_at": null,
+  "closed_by": null,
   "labels": [],
   "milestone": null,
   "assignees": [{
@@ -687,6 +721,8 @@ Example response:
 
 **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
 
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
 ## Subscribe to an issue
 
 Subscribes the authenticated user to an issue to receive notifications.
@@ -719,6 +755,7 @@ Example response:
   "created_at": "2016-04-05T21:41:45.652Z",
   "updated_at": "2016-04-07T12:20:17.596Z",
   "closed_at": null,
+  "closed_by": null,
   "labels": [],
   "milestone": null,
   "assignees": [{
@@ -766,6 +803,9 @@ Example response:
 
 **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
 
+
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
 ## Unsubscribe from an issue
 
 Unsubscribes the authenticated user from the issue to not receive notifications
@@ -807,6 +847,8 @@ Example response:
     "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon",
     "web_url": "https://gitlab.example.com/keyon"
   },
+  "closed_at": null,
+  "closed_by": null,
   "author": {
     "name": "Vivian Hermann",
     "username": "orville",
@@ -927,6 +969,9 @@ Example response:
 
 **Note**: `assignee` column is deprecated, now we show it as a single-sized array `assignees` to conform to the GitLab EE API.
 
+
+**Note**: The `closed_by` attribute was [introduced in GitLab 10.6][ce-17042]. This value will only be present for issues which were closed after GitLab 10.6 and when the user account that closed the issue still exists.
+
 ## Set a time estimate for an issue
 
 Sets an estimated time of work for this issue.
@@ -1112,6 +1157,8 @@ Example response:
     "assignee": null,
     "source_project_id": 1,
     "target_project_id": 1,
+    "closed_at": null,
+    "closed_by": null,
     "labels": [],
     "work_in_progress": false,
     "milestone": null,
@@ -1206,3 +1253,4 @@ Example response:
 
 [ce-13004]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13004
 [ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016
+[ce-17042]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17042
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index e7060e154f414300c1e915e249a30eb6da57e8dc..db4fe2f688012a5cbe4c178fc0e1f39e5cd61442 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -294,9 +294,10 @@ Example of response
 
 ## Get job artifacts
 
-> [Introduced][ce-2893] in GitLab 8.5
+> **Notes**:
+- [Introduced][ce-2893] in GitLab 8.5.
 
-Get job artifacts of a project
+Get job artifacts of a project.
 
 ```
 GET /projects/:id/jobs/:job_id/artifacts
@@ -307,8 +308,10 @@ GET /projects/:id/jobs/:job_id/artifacts
 | `id`       | integer/string | yes      | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
 | `job_id` | integer | yes      | The ID of a job   |
 
+Example requests:
+
 ```
-curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/artifacts"
+curl --location --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/8/artifacts"
 ```
 
 Response:
@@ -322,7 +325,8 @@ Response:
 
 ## Download the artifacts archive
 
-> [Introduced][ce-5347] in GitLab 8.10.
+> **Notes**:
+- [Introduced][ce-5347] in GitLab 8.10.
 
 Download the artifacts archive from the given reference name and job provided the
 job finished successfully.
@@ -339,7 +343,7 @@ Parameters
 | `ref_name`  | string  | yes      | The ref from a repository (can only be branch or tag name, not HEAD or SHA) |
 | `job`       | string  | yes      | The name of the job       |
 
-Example request:
+Example requests:
 
 ```
 curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/jobs/artifacts/master/download?job=test"
diff --git a/doc/api/project_import_export.md b/doc/api/project_import_export.md
index de5207fc5e4e52f5727bc644519e64c0d9282e77..5467187788a5432f3b25c3e1c78ae660671c58ae 100644
--- a/doc/api/project_import_export.md
+++ b/doc/api/project_import_export.md
@@ -8,6 +8,14 @@
 
 Start a new export.
 
+The endpoint also accepts an `upload` param. This param is a hash that contains
+all the necessary information to upload the exported project to a web server or
+to any S3-compatible platform. At the moment we only support binary
+data file uploads to the final server.
+
+If the `upload` params is present, `upload[url]` param is required.
+ (**Note:** This feature was introduced in GitLab 10.7)
+
 ```http
 POST /projects/:id/export
 ```
@@ -16,9 +24,12 @@ POST /projects/:id/export
 | --------- | -------------- | -------- | ---------------------------------------- |
 | `id`      | integer/string | yes      | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
 | `description`      | string | no | Overrides the project description |
+| `upload`      | hash | no | Hash that contains the information to upload the exported project to a web server |
+| `upload[url]`      | string | yes      | The URL to upload the project |
+| `upload[http_method]`      | string | no      | The HTTP method to upload the exported project. Only `PUT` and `POST` methods allowed. Default is `PUT` |
 
 ```console
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --form "description=Foo Bar" https://gitlab.example.com/api/v4/projects/1/export
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/export --data "description=FooBar&upload[http_method]=PUT&upload[url]=https://example-bucket.s3.eu-west-3.amazonaws.com/backup?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAIMBJHN2O62W8IELQ%2F20180312%2Feu-west-3%2Fs3%2Faws4_request&X-Amz-Date=20180312T110328Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=8413facb20ff33a49a147a0b4abcff4c8487cc33ee1f7e450c46e8f695569dbd"
 ```
 
 ```json
@@ -43,7 +54,11 @@ GET /projects/:id/export
 curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/export
 ```
 
-Status can be one of `none`, `started`, or `finished`.
+Status can be one of `none`, `started`, `after_export_action` or `finished`. The
+`after_export_action` state represents that the export process has been completed successfully and
+the platform is performing some actions on the resulted file. For example, sending
+an email notifying the user to download the file, uploading the exported file
+to a web server, etc.
 
 `_links` are only present when export has finished.
 
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 271ee91dc72069b53a25e49b2917e9647f91fb91..f388fae42a9478e42afdeaa54cf9087df1286012 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1344,3 +1344,7 @@ Read more in the [Project members](members.md) documentation.
 ## Project badges
 
 Read more in the [Project Badges](project_badges.md) documentation.
+
+## Issue and merge request description templates
+
+The non-default [issue and merge request description templates](../user/project/description_templates.md) are managed inside the project's repository. So you can manage them via the API through the [Repositories API](repositories.md) and the [Repository Files API](repository_files.md).
\ No newline at end of file
diff --git a/doc/api/runners.md b/doc/api/runners.md
index 7495c6cdedb0f8d4ad2ced1a5f9fc5c913673181..f384ac57bfe3e03b12aa8af46e0da3b3b8b07760 100644
--- a/doc/api/runners.md
+++ b/doc/api/runners.md
@@ -153,7 +153,8 @@ Example response:
         "mysql"
     ],
     "version": null,
-    "access_level": "ref_protected"
+    "access_level": "ref_protected",
+    "maximum_timeout": 3600
 }
 ```
 
@@ -174,6 +175,7 @@ PUT /runners/:id
 | `run_untagged`    | boolean   | no       | Flag indicating the runner can execute untagged jobs |
 | `locked`    | boolean   | no       | Flag indicating the runner is locked |
 | `access_level`    | string   | no       | The access_level of the runner; `not_protected` or `ref_protected` |
+| `maximum_timeout` | integer | no | Maximum timeout set when this Runner will handle the job |
 
 ```
 curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/runners/6" --form "description=test-1-20150125-test" --form "tag_list=ruby,mysql,tag1,tag2"
@@ -211,7 +213,8 @@ Example response:
         "tag2"
     ],
     "version": null,
-    "access_level": "ref_protected"
+    "access_level": "ref_protected",
+    "maximum_timeout": null
 }
 ```
 
diff --git a/doc/ci/examples/browser_performance.md b/doc/ci/examples/browser_performance.md
index 42dc6ef36ba20ccf90ccfd5f12ec01bdcb2693a8..691370d71959b64d614637db304f7297e0e7f231 100644
--- a/doc/ci/examples/browser_performance.md
+++ b/doc/ci/examples/browser_performance.md
@@ -1,22 +1,28 @@
 # Browser Performance Testing with the Sitespeed.io container
 
-This example shows how to run the [Sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) on your code by using
-GitLab CI/CD and [Sitespeed.io](https://www.sitespeed.io) using Docker-in-Docker.
+This example shows how to run the
+[Sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) on
+your code by using GitLab CI/CD and [Sitespeed.io](https://www.sitespeed.io)
+using Docker-in-Docker.
 
-First, you need a GitLab Runner with the [docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
-
-Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `performance`:
+First, you need a GitLab Runner with the
+[docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
+Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called
+`performance`:
 
 ```yaml
+performance:
   stage: performance
   image: docker:git
+  variables:
+    URL: https://example.com
   services:
     - docker:dind
   script:
     - mkdir gitlab-exporter
-    - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/10-5/index.js
+    - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
     - mkdir sitespeed-results
-    - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results https://my.website.com
+    - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results $URL
     - mv sitespeed-results/data/performance.json performance.json
   artifacts:
     paths:
@@ -24,37 +30,84 @@ Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `performan
     - sitespeed-results/
 ```
 
-This will create a `performance` job in your CI/CD pipeline and will run Sitespeed.io against the webpage you define. The GitLab plugin for Sitespeed.io is downloaded in order to export key metrics to JSON. The full HTML Sitespeed.io report will also be saved as an artifact, and if you have Pages enabled it can be viewed directly in your browser. For further customization options of Sitespeed.io, including the ability to provide a list of URLs to test, please consult their [documentation](https://www.sitespeed.io/documentation/sitespeed.io/configuration/).
+The above example will:
+
+1. Create a `performance` job in your CI/CD pipeline and will run
+   Sitespeed.io against the webpage you defined in `URL`.
+1. The [GitLab plugin](https://gitlab.com/gitlab-org/gl-performance) for
+   Sitespeed.io is downloaded in order to export key metrics to JSON. The full
+   HTML Sitespeed.io report will also be saved as an artifact, and if you have
+   [GitLab Pages](../../user/project/pages/index.md) enabled, it can be viewed
+   directly in your browser.
+
+For further customization options of Sitespeed.io, including the ability to
+provide a list of URLs to test, please consult
+[their documentation](https://www.sitespeed.io/documentation/sitespeed.io/configuration/).
 
-For [GitLab Premium](https://about.gitlab.com/products/) users, key metrics are automatically
-extracted and shown right in the merge request widget. Learn more about [Browser Performance Testing](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html).
+TIP: **Tip:**
+For [GitLab Premium](https://about.gitlab.com/pricing/) users, key metrics are automatically
+extracted and shown right in the merge request widget. Learn more about
+[Browser Performance Testing](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html).
 
 ## Performance testing on Review Apps
 
-The above CI YML is great for testing against static environments, and it can be extended for dynamic environments. There are a few extra steps to take to set this up:
-1. The `performance` job should run after the environment has started.
-1. In the `deploy` job, persist the hostname so it is available to the `performance` job. The same can be done for static environments like staging and production to unify the code path. Saving it as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt`.
-1. In the `performance` job read the artifact into an environment variable, like `$CI_ENVIRONMENT_URL`, and use it to parameterize the test URL's.
-1. Now you can run the Sitespeed.io container against the desired hostname and paths.
+The above CI YML is great for testing against static environments, and it can
+be extended for dynamic environments. There are a few extra steps to take to
+set this up:
 
-A simple `performance` job would look like:
+1. The `performance` job should run after the dynamic environment has started.
+1. In the `review` job, persist the hostname and upload it as an artifact so
+   it's available to the `performance` job (the same can be done for static
+   environments like staging and production to unify the code path). Saving it
+   as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt`
+   in your job's `script`.
+1. In the `performance` job, read the previous artifact into an environment
+   variable, like `$CI_ENVIRONMENT_URL`, and use it to parameterize the test
+   URLs.
+1. You can now run the Sitespeed.io container against the desired hostname and
+   paths.
+
+Your `.gitlab-ci.yml` file would look like:
 
 ```yaml
+stages:
+  - deploy
+  - performance
+
+review:
+  stage: deploy
+  environment:
+    name: review/$CI_COMMIT_REF_SLUG
+    url: http://$CI_COMMIT_REF_SLUG.$APPS_DOMAIN
+  script:
+    - run_deploy_script
+    - echo $CI_ENVIRONMENT_URL > environment_url.txt
+  artifacts:
+    paths:
+      - environment_url.txt
+  only:
+    - branches
+  except:
+    - master
+
+performance:
   stage: performance
   image: docker:git
   services:
     - docker:dind
+  dependencies:
+    - review
   script:
     - export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
     - mkdir gitlab-exporter
-    - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/10-5/index.js
+    - wget -O ./gitlab-exporter/index.js https://gitlab.com/gitlab-org/gl-performance/raw/master/index.js
     - mkdir sitespeed-results
     - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:6.3.1 --plugins.add ./gitlab-exporter --outputFolder sitespeed-results "$CI_ENVIRONMENT_URL"
     - mv sitespeed-results/data/performance.json performance.json
   artifacts:
     paths:
-    - performance.json
-    - sitespeed-results/
+      - performance.json
+      - sitespeed-results/
 ```
 
-A complete example can be found in our [Auto DevOps CI YML](https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml).
\ No newline at end of file
+A complete example can be found in our [Auto DevOps CI YML](https://gitlab.com/gitlab-org/gitlab-ci-yml/blob/master/Auto-DevOps.gitlab-ci.yml).
diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md
index ec5e5afb8c6647eeb68a881595ae24d96543151b..64a759a9a99741a4784aa7ee03206c14c898295a 100644
--- a/doc/ci/examples/code_climate.md
+++ b/doc/ci/examples/code_climate.md
@@ -15,13 +15,8 @@ codequality:
   services:
     - docker:dind
   script:
-    - docker pull codeclimate/codeclimate
     - export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
-    - docker run
-      --env SOURCE_CODE="$PWD" \
-      --volume "$PWD":/code \
-      --volume /var/run/docker.sock:/var/run/docker.sock \
-      "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
+    - docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
   artifacts:
     paths: [codeclimate.json]
 ```
diff --git a/doc/ci/examples/deployment/README.md b/doc/ci/examples/deployment/README.md
index e80e246c5ddf6913a6174ba1e6f4160af6483594..2dcdc2d41ecbf55aff6aa8b559724f017617373b 100644
--- a/doc/ci/examples/deployment/README.md
+++ b/doc/ci/examples/deployment/README.md
@@ -111,7 +111,7 @@ We also use two secure variables:
 ## Storing API keys
 
 Secure Variables can added by going to your project's
-**Settings âž” Pipelines âž” Secret variables**. The variables that are defined
+**Settings âž” CI / CD âž” Secret variables**. The variables that are defined
 in the project settings are sent along with the build script to the Runner.
 The secure variables are stored out of the repository. Never store secrets in
 your project's `.gitlab-ci.yml`. It is also important that the secret's value
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 856d7f264e4269b4a82e5f938b39b5a28b33c682..301cccc80a314a8c1b5f5f3f544fe8ae6dcd9825 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -2,6 +2,11 @@
 
 > Introduced in GitLab 8.8.
 
+NOTE: **Note:**
+If you have a [mirrored repository where GitLab pulls from](https://docs.gitlab.com/ee/workflow/repository_mirroring.html#pulling-from-a-remote-repository),
+you may need to enable pipeline triggering in your project's
+**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
+
 ## Pipelines
 
 A pipeline is a group of [jobs][] that get executed in [stages][](batches).
@@ -121,9 +126,8 @@ The basic requirements is that there are two numbers separated with one of
 the following (you can even use them interchangeably):
 
 - a space
-- a forward slash (`/`)
+- a slash (`/`)
 - a colon (`:`)
-- a dot (`.`)
 
 >**Note:**
 More specifically, [it uses][regexp] this regular expression: `\d+[\s:\/\\]+\d+\s*`.
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index f64e868d390067045922f07ebaec5aa85ff3c674..fec0ff87326449d8ceccfda6361773e719e30e5b 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -126,6 +126,11 @@ git push origin master
 Now if you go to the **Pipelines** page you will see that the pipeline is
 pending.
 
+NOTE: **Note:**
+If you have a [mirrored repository where GitLab pulls from](https://docs.gitlab.com/ee/workflow/repository_mirroring.html#pulling-from-a-remote-repository),
+you may need to enable pipeline triggering in your project's
+**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
+
 You can also go to the **Commits** page and notice the little pause icon next
 to the commit SHA.
 
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 7a7b50b294d7763da26c0d69442defb1eade2571..60dc2ef9ac5f124fec52cb1d2f41dd9e0da48100 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -35,7 +35,7 @@ are:
 
 A Runner that is specific only runs for the specified project(s). A shared Runner
 can run jobs for every project that has enabled the option **Allow shared Runners**
-under **Settings âž” CI/CD**.
+under **Settings > CI/CD**.
 
 Projects with high demand of CI activity can also benefit from using specific
 Runners. By having dedicated Runners you are guaranteed that the Runner is not
@@ -76,7 +76,7 @@ Registering a specific can be done in two ways:
 To create a specific Runner without having admin rights to the GitLab instance,
 visit the project you want to make the Runner work for in GitLab:
 
-1. Go to **Settings âž” CI/CD** to obtain the token
+1. Go to **Settings > CI/CD** to obtain the token
 1. [Register the Runner][register]
 
 ### Making an existing shared Runner specific
@@ -85,7 +85,7 @@ If you are an admin on your GitLab instance, you can turn any shared Runner into
 a specific one, but not the other way around. Keep in mind that this is a one
 way transition.
 
-1. Go to the Runners in the admin area **Overview âž” Runners** (`/admin/runners`)
+1. Go to the Runners in the admin area **Overview > Runners** (`/admin/runners`)
    and find your Runner
 1. Enable any projects under **Restrict projects for this Runner** to be used
    with the Runner
@@ -101,7 +101,7 @@ can be changed afterwards under each Runner's settings.
 
 To lock/unlock a Runner:
 
-1. Visit your project's **Settings âž” CI/CD**
+1. Visit your project's **Settings > CI/CD**
 1. Find the Runner you wish to lock/unlock and make sure it's enabled
 1. Click the pencil button
 1. Check the **Lock to current projects** option
@@ -115,7 +115,7 @@ you can enable the Runner also on any other project where you have Master permis
 
 To enable/disable a Runner in your project:
 
-1. Visit your project's **Settings âž” CI/CD**
+1. Visit your project's **Settings > CI/CD**
 1. Find the Runner you wish to enable/disable
 1. Click **Enable for this project** or **Disable for this project**
 
@@ -124,6 +124,13 @@ Consider that if you don't lock your specific Runner to a specific project, any
 user with Master role in you project can assign your runner to another arbitrary
 project without requiring your authorization, so use it with caution.
 
+An admin can enable/disable a specific Runner for projects:
+
+1. Navigate to **Admin > Runners**
+2. Find the Runner you wish to enable/disable
+3. Click edit on the Runner
+4. Click **Enable** or **Disable** on the project
+
 ## Protected Runners
 
 >
@@ -136,7 +143,7 @@ Whenever a Runner is protected, the Runner picks only jobs created on
 
 To protect/unprotect Runners:
 
-1. Visit your project's **Settings âž” CI/CD**
+1. Visit your project's **Settings > CI/CD**
 1. Find a Runner you want to protect/unprotect and make sure it's enabled
 1. Click the pencil button besides the Runner name
 1. Check the **Protected** option
@@ -231,6 +238,38 @@ To make a Runner pick tagged/untagged jobs:
 1. Check the **Run untagged jobs** option
 1. Click **Save changes** for the changes to take effect
 
+### Setting maximum job timeout for a Runner
+
+For each Runner you can specify a _maximum job timeout_. Such timeout,
+if smaller than [project defined timeout], will take the precedence. This
+feature can be used to prevent Shared Runner from being appropriated
+by a project by setting a ridiculous big timeout (e.g. one week).
+
+When not configured, Runner will not override project timeout.
+
+How this feature will work:
+
+**Example 1 - Runner timeout bigger than project timeout**
+
+1. You set the _maximum job timeout_ for a Runner to 24 hours
+1. You set the _CI/CD Timeout_ for a project to **2 hours**
+1. You start a job
+1. The job, if running longer, will be timeouted after **2 hours**
+
+**Example 2 - Runner timeout not configured**
+
+1. You remove the _maximum job timeout_ configuration from a Runner
+1. You set the _CI/CD Timeout_ for a project to **2 hours**
+1. You start a job
+1. The job, if running longer, will be timeouted after **2 hours**
+
+**Example 3 - Runner timeout smaller than project timeout**
+
+1. You set the _maximum job timeout_ for a Runner to **30 minutes**
+1. You set the _CI/CD Timeout_ for a project to 2 hours
+1. You start a job
+1. The job, if running longer, will be timeouted after **30 minutes**
+
 ### Be careful with sensitive information
 
 With some [Runner Executors](https://docs.gitlab.com/runner/executors/README.html),
@@ -259,12 +298,6 @@ Mentioned briefly earlier, but the following things of Runners can be exploited.
 We're always looking for contributions that can mitigate these
 [Security Considerations](https://docs.gitlab.com/runner/security/).
 
-[install]: http://docs.gitlab.com/runner/install/
-[fifo]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)
-[register]: http://docs.gitlab.com/runner/register/
-[protected branches]: ../../user/project/protected_branches.md
-[protected tags]: ../../user/project/protected_tags.md
-
 ## Determining the IP address of a Runner
 
 > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17286) in GitLab 10.6.
@@ -297,3 +330,10 @@ You can find the IP address of a Runner for a specific project by:
 1. On the details page you should see a row for "IP Address"
 
 ![specific Runner IP address](img/specific_runner_ip_address.png)
+
+[install]: http://docs.gitlab.com/runner/install/
+[fifo]: https://en.wikipedia.org/wiki/FIFO_(computing_and_electronics)
+[register]: http://docs.gitlab.com/runner/register/
+[protected branches]: ../../user/project/protected_branches.md
+[protected tags]: ../../user/project/protected_tags.md
+[project defined timeout]: ../../user/project/pipelines/settings.html#timeout
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index bd4aeb006bdb2e4a6eb6887859f578a15efa77d0..9f268f47e6fe421532a88d40387e7b156df2e8d8 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -449,6 +449,72 @@ export CI_REGISTRY_USER="gitlab-ci-token"
 export CI_REGISTRY_PASSWORD="longalfanumstring"
 ```
 
+## Variables expressions
+
+> Variables expressions were added in GitLab 10.7.
+
+It is possible to use variables expressions with only / except policies in
+`.gitlab-ci.yml`. By using this approach you can limit what builds are going to
+be created within a pipeline after pushing code to GitLab.
+
+This is particularly useful in combination with secret variables and triggered
+pipeline variables.
+
+```yaml
+deploy:
+  script: cap staging deploy
+  environment: staging
+  only:
+    variables:
+      - $RELEASE == "staging"
+      - $STAGING
+```
+
+Each provided variables expression is going to be evaluated before creating
+a pipeline.
+
+If any of the conditions in `variables` evaluates to truth when using `only`,
+a new job is going to be created. If any of the expressions evaluates to truth
+when `except` is being used, a job is not going to be created.
+
+This follows usual rules for `only` / `except` policies.
+
+### Supported syntax
+
+Below you can find currently supported syntax reference:
+
+1. Equality matching using a string
+
+    Example: `$VARIABLE == "some value"`
+
+    You can use equality operator `==` to compare a variable content to a
+    string. We support both, double quotes and single quotes to define a string
+    value, so both `$VARIABLE == "some value"` and `$VARIABLE == 'some value'`
+    are supported. `"some value" == $VARIABLE` is correct too.
+
+1. Checking for an undefined value
+
+    It sometimes happens that you want to check whether variable is defined or
+    not. To do that, you can compare variable to `null` value, like
+    `$VARIABLE == null`. This expression is going to evaluate to truth if
+    variable is not set.
+
+1. Checking for an empty variable
+
+    If you want to check whether a variable is defined, but is empty, you can
+    simply compare it against an empty string, like `$VAR == ''`.
+
+1. Comparing two variables
+
+    It is possible to compare two variables. `$VARIABLE_1 == $VARIABLE_2`.
+
+1. Variable presence check
+
+    If you only want to create a job when there is some variable present,
+    which means that it is defined and non-empty, you can simply use
+    variable name as an expression, like `$STAGING`. If `$STAGING` variable
+    is defined, and is non empty, expression will evaluate to truth.
+
 [ce-13784]: https://gitlab.com/gitlab-org/gitlab-ce/issues/13784 "Simple protection of CI secret variables"
 [eep]: https://about.gitlab.com/products/ "Available only in GitLab Premium"
 [envs]: ../environments.md
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index c2b06e53c2ffa71d22441457fe2754f4ead4b5f9..be114e7008e13a140bf2423f8eabdab469e0abb8 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -10,6 +10,11 @@ of your repository and contains definitions of how your project should be built.
 If you want a quick introduction to GitLab CI, follow our
 [quick start guide](../quick_start/README.md).
 
+NOTE: **Note:**
+If you have a [mirrored repository where GitLab pulls from](https://docs.gitlab.com/ee/workflow/repository_mirroring.html#pulling-from-a-remote-repository),
+you may need to enable pipeline triggering in your project's
+**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
+
 ## Jobs
 
 The YAML file defines a set of jobs with constraints stating when they should
@@ -315,9 +320,14 @@ policy configuration.
 GitLab now supports both, simple and complex strategies, so it is possible to
 use an array and a hash configuration scheme.
 
-Two keys are now available: `refs` and `kubernetes`. Refs strategy equals to
-simplified only/except configuration, whereas kubernetes strategy accepts only
-`active` keyword.
+Three keys are now available: `refs`, `kubernetes` and `variables`.
+Refs strategy equals to simplified only/except configuration, whereas
+kubernetes strategy accepts only `active` keyword.
+
+`variables` keyword is used to define variables expressions. In other words
+you can use predefined variables / secret variables / project / group or
+environment-scoped variables to define an expression GitLab is going to
+evaluate in order to decide whether a job should be created or not.
 
 See the example below. Job is going to be created only when pipeline has been
 scheduled or runs for a `master` branch, and only if kubernetes service is
@@ -332,6 +342,20 @@ job:
     kubernetes: active
 ```
 
+Example of using variables expressions:
+
+```yaml
+deploy:
+  only:
+    refs:
+      - branches
+    variables:
+      - $RELEASE == "staging"
+      - $STAGING
+```
+
+Learn more about variables expressions on a separate page.
+
 ## `tags`
 
 `tags` is used to select specific Runners from the list of all Runners that are
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index fea92e740cba1bf9529d1edcd5ec4d581d282e37..3ba03d2d5912209e9f3ea76763e8763b374f2aad 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -33,6 +33,26 @@ rest of the code should be as close to the CE files as possible.
 
 [single code base]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2952#note_41016454
 
+### EE-specific comments
+
+When complete separation can't be achieved with the `ee/` directory, you can wrap
+code in EE specific comments to designate the difference from CE/EE and add
+some context for someone resolving a conflict.
+
+```rb
+# EE-specific start
+stub_licensed_features(variable_environment_scope: true)
+# EE specific end
+```
+
+```haml
+-# EE-specific start
+= render 'ci/variables/environment_scope', form_field: form_field, variable: variable
+-# EE-specific end
+```
+
+EE-specific comments should not be backported to CE.
+
 ### Detection of EE-only files
 
 For each commit (except on `master`), the `ee-files-location-check` CI job tries
@@ -350,6 +370,255 @@ class beneath the `EE` module just as you would normally.
 For example, if CE has LDAP classes in `lib/gitlab/ldap/` then you would place
 EE-specific LDAP classes in `ee/lib/ee/gitlab/ldap`.
 
+### Code in `lib/api/`
+
+It can be very tricky to extend EE features by a single line of `prepend`,
+and for each different [Grape](https://github.com/ruby-grape/grape) feature,
+we might need different strategies to extend it. To apply different strategies
+easily, we would use `extend ActiveSupport::Concern` in the EE module.
+
+Put the EE module files following
+[EE features based on CE features](#ee-features-based-on-ce-features).
+
+#### EE API routes
+
+For EE API routes, we put them in a `prepended` block:
+
+``` ruby
+module EE
+  module API
+    module MergeRequests
+      extend ActiveSupport::Concern
+
+      prepended do
+        params do
+          requires :id, type: String, desc: 'The ID of a project'
+        end
+        resource :projects, requirements: ::API::API::PROJECT_ENDPOINT_REQUIREMENTS do
+          # ...
+        end
+      end
+    end
+  end
+end
+```
+
+Note that due to namespace differences, we need to use the full qualifier for some
+constants.
+
+#### EE params
+
+We can define `params` and utilize `use` in another `params` definition to
+include params defined in EE. However, we need to define the "interface" first
+in CE in order for EE to override it. We don't have to do this in other places
+due to `prepend`, but Grape is complex internally and we couldn't easily do
+that, so we'll follow regular object-oriented practices that we define the
+interface first here.
+
+For example, suppose we have a few more optional params for EE, given this CE
+API code:
+
+``` ruby
+module API
+  class MergeRequests < Grape::API
+    # EE::API::MergeRequests would override the following helpers
+    helpers do
+      params :optional_params_ee do
+      end
+    end
+
+    prepend EE::API::MergeRequests
+
+    params :optional_params do
+      # CE specific params go here...
+
+      use :optional_params_ee
+    end
+  end
+end
+```
+
+And then we could override it in EE module:
+
+``` ruby
+module EE
+  module API
+    module MergeRequests
+      extend ActiveSupport::Concern
+
+      prepended do
+        helpers do
+          params :optional_params_ee do
+            # EE specific params go here...
+          end
+        end
+      end
+    end
+  end
+end
+```
+
+This way, the only difference between CE and EE for that API file would be
+`prepend EE::API::MergeRequests`.
+
+#### EE helpers
+
+To make it easy for an EE module to override the CE helpers, we need to define
+those helpers we want to extend first. Try to do that immediately after the
+class definition to make it easy and clear:
+
+``` ruby
+module API
+  class JobArtifacts < Grape::API
+    # EE::API::JobArtifacts would override the following helpers
+    helpers do
+      def authorize_download_artifacts!
+        authorize_read_builds!
+      end
+    end
+
+    prepend EE::API::JobArtifacts
+  end
+end
+```
+
+And then we can follow regular object-oriented practices to override it:
+
+``` ruby
+module EE
+  module API
+    module JobArtifacts
+      extend ActiveSupport::Concern
+
+      prepended do
+        helpers do
+          def authorize_download_artifacts!
+            super
+            check_cross_project_pipelines_feature!
+          end
+        end
+      end
+    end
+  end
+end
+```
+
+#### EE-specific behaviour
+
+Sometimes we need EE-specific behaviour in some of the APIs. Normally we could
+use EE methods to override CE methods, however API routes are not methods and
+therefore can't be simply overridden. We need to extract them into a standalone
+method, or introduce some "hooks" where we could inject behavior in the CE
+route. Something like this:
+
+``` ruby
+module API
+  class MergeRequests < Grape::API
+    helpers do
+      # EE::API::MergeRequests would override the following helpers
+      def update_merge_request_ee(merge_request)
+      end
+    end
+
+    prepend EE::API::MergeRequests
+
+    put ':id/merge_requests/:merge_request_iid/merge' do
+      merge_request = find_project_merge_request(params[:merge_request_iid])
+
+      # ...
+
+      update_merge_request_ee(merge_request)
+
+      # ...
+    end
+  end
+end
+```
+
+Note that `update_merge_request_ee` doesn't do anything in CE, but
+then we could override it in EE:
+
+``` ruby
+module EE
+  module API
+    module MergeRequests
+      extend ActiveSupport::Concern
+
+      prepended do
+        helpers do
+          def update_merge_request_ee(merge_request)
+            # ...
+          end
+        end
+      end
+    end
+  end
+end
+```
+
+#### EE `route_setting`
+
+It's very hard to extend this in an EE module, and this is simply storing
+some meta-data for a particular route. Given that, we could simply leave the
+EE `route_setting` in CE as it won't hurt and we are just not going to use
+those meta-data in CE.
+
+We could revisit this policy when we're using `route_setting` more and whether
+or not we really need to extend it from EE. For now we're not using it much.
+
+#### Utilizing class methods for setting up EE-specific data
+
+Sometimes we need to use different arguments for a particular API route, and we
+can't easily extend it with an EE module because Grape has different context in
+different blocks. In order to overcome this, we could use class methods from the
+API class.
+
+For example, in one place we need to pass an extra argument to
+`at_least_one_of` so that the API could consider an EE-only argument as the
+least argument. This is not quite beautiful but it's working:
+
+``` ruby
+module API
+  class MergeRequests < Grape::API
+    def self.update_params_at_least_one_of
+      %i[
+        assignee_id
+        description
+      ]
+    end
+
+    prepend EE::API::MergeRequests
+
+    params do
+      at_least_one_of(*::API::MergeRequests.update_params_at_least_one_of)
+    end
+  end
+end
+```
+
+And then we could easily extend that argument in the EE class method:
+
+``` ruby
+module EE
+  module API
+    module MergeRequests
+      extend ActiveSupport::Concern
+
+      class_methods do
+        def update_params_at_least_one_of
+          super.push(*%i[
+            squash
+          ])
+        end
+      end
+    end
+  end
+end
+```
+
+It could be annoying if we need this for a lot of routes, but it might be the
+simplest solution right now.
+
 ### Code in `spec/`
 
 When you're testing EE-only features, avoid adding examples to the
@@ -405,12 +674,13 @@ to avoid conflicts during CE to EE merge.
   }
 }
 
-/* EE-specific styles */
+// EE-specific start
 .section-body.ee-section-body {
   .section-title {
     background: $gl-header-color-cyan;
   }
 }
+// EE-specific end
 ```
 
 ## gitlab-svgs
diff --git a/doc/development/new_fe_guide/style/javascript.md b/doc/development/new_fe_guide/style/javascript.md
index 480d50a211fb43e428bf57a979aaa67dccfe1fe6..57efd9353bc8f75e7585dc6c12cb493121d30387 100644
--- a/doc/development/new_fe_guide/style/javascript.md
+++ b/doc/development/new_fe_guide/style/javascript.md
@@ -1,3 +1,195 @@
 # JavaScript style guide
 
-> TODO: Add content
+We use [Airbnb's JavaScript Style Guide][airbnb-style-guide] and it's accompanying linter to manage most of our JavaScript style guidelines.
+
+In addition to the style guidelines set by Airbnb, we also have a few specific rules listed below.
+
+> **Tip:**
+You can run eslint locally by running `yarn eslint`
+
+## Arrays
+
+<a name="avoid-foreach"></a><a name="1.1"></a>
+- [1.1](#avoid-foreach) **Avoid ForEach when mutating data** Use `map`, `reduce` or `filter` instead of `forEach` when mutating data. This will minimize mutations in functions ([which is aligned with Airbnb's style guide][airbnb-minimize-mutations])
+
+```
+// bad
+users.forEach((user, index) => {
+  user.id = index;
+});
+
+// good
+const usersWithId = users.map((user, index) => {
+  return Object.assign({}, user, { id: index });
+});
+```
+
+## Functions
+
+<a name="limit-params"></a><a name="2.1"></a>
+- [2.1](#limit-params) **Limit number of parameters** If your function or method has more than 3 parameters, use an object as a parameter instead.
+
+```
+// bad
+function a(p1, p2, p3) {
+  // ...
+};
+
+// good
+function a(p) {
+  // ...
+};
+```
+
+## Classes & constructors
+
+<a name="avoid-constructor-side-effects"></a><a name="3.1"></a>
+- [3.1](#avoid-constructor-side-effects) **Avoid side effects in constructors** Avoid making some operations in the `constructor`, such as asynchronous calls, API requests and DOM manipulations. Prefer moving them into separate functions. This will make tests easier to write and code easier to maintain.
+
+  ```javascript
+  // bad
+  class myClass {
+    constructor(config) {
+      this.config = config;
+      axios.get(this.config.endpoint)
+    }
+  }
+
+  // good
+  class myClass {
+    constructor(config) {
+      this.config = config;
+    }
+
+    makeRequest() {
+      axios.get(this.config.endpoint)
+    }
+  }
+  const instance = new myClass();
+  instance.makeRequest();
+
+  ```
+
+<a name="avoid-classes-to-handle-dom-events"></a><a name="3.2"></a>
+- [3.2](#avoid-classes-to-handle-dom-events) **Avoid classes to handle DOM events** If the only purpose of the class is to bind a DOM event and handle the callback, prefer using a function.
+
+```
+// bad
+class myClass {
+  constructor(config) {
+    this.config = config;
+  }
+
+  init() {
+    document.addEventListener('click', () => {});
+  }
+}
+
+// good
+
+const myFunction = () => {
+  document.addEventListener('click', () => {
+    // handle callback here
+  });
+}
+```
+
+<a name="element-container"></a><a name="3.3"></a>
+- [3.3](#element-container) **Pass element container to constructor** When your class manipulates the DOM, receive the element container as a parameter.
+This is more maintainable and performant.
+
+```
+// bad
+class a {
+  constructor() {
+    document.querySelector('.b');
+  }
+}
+
+// good
+class a {
+  constructor(options) {
+    options.container.querySelector('.b');
+  }
+}
+```
+
+## Type Casting & Coercion
+
+<a name="use-parseint"></a><a name="4.1"></a>
+- [4.1](#use-parseint) **Use ParseInt** Use `ParseInt` when converting a numeric string into a number.
+
+```
+// bad
+Number('10')
+
+
+// good
+parseInt('10', 10);
+```
+
+## CSS Selectors
+
+<a name="use-js-prefix"></a><a name="5.1"></a>
+- [5.1](#use-js-prefix) **Use js prefix** If a CSS class is only being used in JavaScript as a reference to the element, prefix the class name with `js-`
+
+```
+// bad
+<button class="add-user"></button>
+
+// good
+<button class="js-add-user"></button>
+```
+
+## Modules
+
+<a name="use-absolute-paths"></a><a name="6.1"></a>
+- [6.1](#use-absolute-paths) **Use absolute paths for nearby modules** Use absolute paths if the module you are importing is less than two levels up.
+
+```
+// bad
+import GitLabStyleGuide from '~/guides/GitLabStyleGuide';
+
+// good
+import GitLabStyleGuide from '../GitLabStyleGuide';
+```
+
+<a name="use-relative-paths"></a><a name="6.2"></a>
+- [6.2](#use-relative-paths) **Use relative paths for distant modules** If the module you are importing is two or more levels up, use a relative path instead of an absolute path.
+
+```
+// bad
+import GitLabStyleGuide from '../../../guides/GitLabStyleGuide';
+
+// good
+import GitLabStyleGuide from '~/GitLabStyleGuide';
+```
+
+<a name="global-namespace"></a><a name="6.3"></a>
+- [6.3](#global-namespace) **Do not add to global namespace**
+
+<a name="domcontentloaded"></a><a name="6.4"></a>
+- [6.4](#domcontentloaded) **Do not use DOMContentLoaded in non-page modules** Imported modules should act the same each time they are loaded. `DOMContentLoaded` events are only allowed on modules loaded in the `/pages/*` directory because those are loaded dynamically with webpack.
+
+## Security
+
+<a name="avoid-xss"></a><a name="7.1"></a>
+- [7.1](#avoid-xss) **Avoid XSS** Do not use `innerHTML`, `append()` or `html()` to set content. It opens up too many vulnerabilities.
+
+## ESLint
+
+<a name="disable-eslint-file"></a><a name="8.1"></a>
+- [8.1](#disable-eslint-file) **Disabling ESLint in new files** Do not disable ESLint when creating new files. Existing files may have existing rules disabled due to legacy compatibility reasons but they are in the process of being refactored.
+
+<a name="disable-eslint-rule"></a><a name="8.2"></a>
+- [8.2](#disable-eslint-rule) **Disabling ESLint rule** Do not disable specific ESLint rules. Due to technical debt, you may disable the following rules only if you are invoking/instantiating existing code modules
+
+  - [no-new][no-new]
+  - [class-method-use-this][class-method-use-this]
+
+> Note: Disable these rules on a per line basis. This makes it easier to refactor in the future. E.g. use `eslint-disable-next-line` or `eslint-disable-line`
+
+[airbnb-style-guide]: https://github.com/airbnb/javascript
+[airbnb-minimize-mutations]: https://github.com/airbnb/javascript#testing--for-real
+[no-new]: http://eslint.org/docs/rules/no-new
+[class-method-use-this]: http://eslint.org/docs/rules/class-methods-use-this
diff --git a/doc/install/README.md b/doc/install/README.md
index 87f6969b415c9dc937d6cded22dbd47f8a7884ea..9724b56910dfbed96fc9954c8f7f407f7dcde8db 100644
--- a/doc/install/README.md
+++ b/doc/install/README.md
@@ -36,7 +36,7 @@ the full process of installing GitLab on Google Container Engine (GKE), pushing
 - [Install on AWS](https://about.gitlab.com/aws/)
 - _Testing only!_ [DigitalOcean and Docker Machine](digitaloceandocker.md) -
   Quickly test any version of GitLab on DigitalOcean using Docker Machine.
-- [Getting started with GitLab and DigitalOcean](ttps://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/): requirements, installation process, updates.
+- [Getting started with GitLab and DigitalOcean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/): requirements, installation process, updates.
 - [Demo: Cloud Native Development with GitLab](https://about.gitlab.com/2017/04/18/cloud-native-demo/): video demonstration on how to install GitLab on Kubernetes, build a project, create Review Apps, store Docker images in Container Registry, deploy to production on Kubernetes, and monitor with Prometheus.
 
 ## Database
diff --git a/doc/integration/google.md b/doc/integration/google.md
index 07a700f7b64ea758865293bc0d92e42207401619..ae1d848f4394a75702dce6eade36a19314fb8b1a 100644
--- a/doc/integration/google.md
+++ b/doc/integration/google.md
@@ -35,7 +35,7 @@ In Google's side:
 
 1. You should now be able to see a Client ID and Client secret. Note them down
    or keep this page open as you will need them later.
-1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Compute > Google Container Engine API > Enable**
+1. From the **Dashboard** select **ENABLE APIS AND SERVICES > Compute > Google Kubernetes Engine API > Enable**
 
 On your GitLab server:
 
diff --git a/doc/policy/maintenance.md b/doc/policy/maintenance.md
index 8d0afa9e69234fe0338e2d8547967e4e62faff43..7f0285654129786e33eae291fb61e71ddabf25d5 100644
--- a/doc/policy/maintenance.md
+++ b/doc/policy/maintenance.md
@@ -44,7 +44,7 @@ This decision is made on a case-by-case basis.
 
 ## Upgrade recommendations
 
-We encourage everyone to run the latest stable release to ensure that you can
+We encourage everyone to run the [latest stable release](https://about.gitlab.com/blog/categories/release/) to ensure that you can
 easily upgrade to the most secure and feature-rich GitLab experience. In order
 to make sure you can easily run the most recent stable release, we are working
 hard to keep the update process simple and reliable.
diff --git a/doc/update/10.5-to-10.6.md b/doc/update/10.5-to-10.6.md
index f5c5c3057261bd9391b0c1475957523433f96f9f..2f90fb62c4a2aff89f9c81f551370daddc2cc264 100644
--- a/doc/update/10.5-to-10.6.md
+++ b/doc/update/10.5-to-10.6.md
@@ -103,7 +103,7 @@ rm go1.8.3.linux-amd64.tar.gz
 ```bash
 cd /home/git/gitlab
 
-sudo -u git -H git fetch --all
+sudo -u git -H git fetch --all --prune
 sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
 sudo -u git -H git checkout -- locale
 ```
@@ -131,7 +131,7 @@ sudo -u git -H git checkout 10-6-stable-ee
 ```bash
 cd /home/git/gitlab-shell
 
-sudo -u git -H git fetch --all --tags
+sudo -u git -H git fetch --all --tags --prune
 sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_SHELL_VERSION)
 sudo -u git -H bin/compile
 ```
@@ -146,7 +146,7 @@ If you are not using Linux you may have to run `gmake` instead of
 ```bash
 cd /home/git/gitlab-workhorse
 
-sudo -u git -H git fetch --all --tags
+sudo -u git -H git fetch --all --tags --prune
 sudo -u git -H git checkout v$(</home/git/gitlab/GITLAB_WORKHORSE_VERSION)
 sudo -u git -H make
 ```
@@ -182,7 +182,7 @@ sudo -u git -H sed -i.pre-10.1 's/\[\[storages\]\]/[[storage]]/' /home/git/gital
 
 ```shell
 cd /home/git/gitaly
-sudo -u git -H git fetch --all --tags
+sudo -u git -H git fetch --all --tags --prune
 sudo -u git -H git checkout v$(</home/git/gitlab/GITALY_SERVER_VERSION)
 sudo -u git -H make
 ```
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index 83eb7a225b293dda4b5e1abbcb6e4e3dbf668ccd..d5f77191938bacfe8ce0f8d17f227369aa954b32 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -51,7 +51,7 @@ Below are the settings for [GitLab Pages].
 | TLS certificates support| yes               | no            |
 
 The maximum size of your Pages site is regulated by the artifacts maximum size
-which is part of [GitLab CI](#gitlab-ci).
+which is part of [GitLab CI/CD](#gitlab-ci-cd).
 
 ## GitLab CI/CD
 
@@ -61,6 +61,14 @@ Below are the current settings regarding [GitLab CI/CD](../../ci/README.md).
 | -----------             | ----------------- | ------------- |
 | Artifacts maximum size  | 1G                | 100M          |
 
+## Repository size limit
+
+The maximum size your Git repository is allowed to be including LFS.
+
+| Setting                 | GitLab.com        | Default       |
+| -----------             | ----------------- | ------------- |
+| Repository size including LFS | 10G         | Unlimited     |
+
 ## Shared Runners
 
 Shared Runners on GitLab.com run in [autoscale mode] and powered by
diff --git a/doc/user/project/integrations/img/jira_workflow_screenshot.png b/doc/user/project/integrations/img/jira_workflow_screenshot.png
deleted file mode 100644
index e62fb202613febb8cc0a681944aad7e028f12db6..0000000000000000000000000000000000000000
Binary files a/doc/user/project/integrations/img/jira_workflow_screenshot.png and /dev/null differ
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
index fc527663db052ee033bb6b9bbbc2721c00e35aaa..5933bcedc8b612394055503827a847d5dc1d108f 100644
--- a/doc/user/project/integrations/jira.md
+++ b/doc/user/project/integrations/jira.md
@@ -113,7 +113,20 @@ in the table below.
 | `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. |
 | `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
 | `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
-| `Transition ID` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
+| `Transition ID` | This is the ID of a transition that moves issues to the desired state.  **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
+
+### Getting a transition ID
+
+In the most recent JIRA UI, you can no longer see transition IDs in the workflow
+administration UI. You can get the ID you need in either of the following ways:
+
+1. By using the API, with a request like `https://yourcompany.atlassian.net/rest/api/2/issue/ISSUE-123/transitions`
+   using an issue that is in the appropriate "open" state
+1. By mousing over the link for the transition you want and looking for the
+   "action" parameter in the URL
+
+Note that the transition ID may vary between workflows (e.g., bug vs. story),
+even if the status you are changing to is the same.
 
 After saving the configuration, your GitLab project will be able to interact
 with all JIRA projects in your JIRA instance and you'll see the JIRA link on the GitLab project pages that takes you to the appropriate JIRA project.
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index d403d5698a98163da7b5f490a2498c2b1e7c6ed9..b4a842f33d638ba7c8403fde8bd3c1ce66adaf38 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -251,7 +251,7 @@ Different issue board features are available in different [GitLab tiers](https:/
 
 | Tier | Number of project issue boards | Board with configuration in project issue boards | Number of group issue boards | Board with configuration in group issue boards |
 | --- | --- | --- | --- | --- |
-| Libre    | 1        | No  | 1        | No  |
+| Core    | 1        | No  | 1        | No  |
 | Starter  | Multiple | Yes | 1        | No  |
 | Premium  | Multiple | Yes | Multiple | Yes |
 | Ultimate | Multiple | Yes | Multiple | Yes |
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
index 43451844f2de8630d7478d097260994b3a4448e6..6cead7b9961d99a9b9ff4e2eec0953f6f8f37726 100644
--- a/doc/user/project/pipelines/settings.md
+++ b/doc/user/project/pipelines/settings.md
@@ -27,6 +27,13 @@ The default value is 60 minutes. Decrease the time limit if you want to impose
 a hard limit on your jobs' running time or increase it otherwise. In any case,
 if the job surpasses the threshold, it is marked as failed.
 
+### Timeout overriding on Runner level
+
+> - [Introduced][ce-17221] in GitLab 10.7.
+
+Project defined timeout (either specific timeout set by user or the default
+60 minutes timeout) may be [overridden on Runner level][timeout overriding].
+
 ## Custom CI config path
 
 >  - [Introduced][ce-12509] in GitLab 9.4.
@@ -152,5 +159,7 @@ into your `README.md`:
 
 [var]: ../../../ci/yaml/README.md#git-strategy
 [coverage report]: #test-coverage-parsing
+[timeout overriding]: ../../../ci/runners/README.html#setting-maximum-job-timeout-for-a-runner
 [ce-9362]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9362
 [ce-12509]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12509
+[ce-17221]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17221
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index ae131d51305f44a0fb9a0dd370cc2318bd750580..376f4e3cbe4ae2aff142f9ce2cbf7d5e47126caa 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -132,8 +132,9 @@ Use GPG to [sign your commits](gpg_signed_commits/index.md).
 
 ## Repository size
 
-In GitLab.com, your repository size limit it 10GB. For other instances,
-the repository size is limited by your system administrators.
+On GitLab.com, your [repository size limit is 10GB](../../gitlab_com/index.md#repository-size-limit)
+(including LFS). For other instances, the repository size is limited by your
+system administrators.
 
 You can also [reduce a repository size using Git](reducing_the_repo_size_using_git.md).
 
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index cac3cb599ddea23158e063663c55d6dd5009d553..f824756c10ca96f375f5e0ad73953aec4390d266 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -19,7 +19,7 @@ There are various configuration options to help GitLab server administrators:
 * Changing the location of LFS object storage
 * Setting up AWS S3 compatible object storage
 
-### Omnibus packages
+### Configuration for Omnibus installations
 
 In `/etc/gitlab/gitlab.rb`:
 
@@ -33,7 +33,7 @@ gitlab_rails['lfs_enabled'] = false
 gitlab_rails['lfs_storage_path'] = "/mnt/storage/lfs-objects"
 ```
 
-### Installations from source
+### Configuration for installations from source
 
 In `config/gitlab.yml`:
 
@@ -44,20 +44,21 @@ In `config/gitlab.yml`:
     storage_path: /mnt/storage/lfs-objects
 ```
 
-## Setting up S3 compatible object storage
+## Storing the LFS objects in an S3-compatible object storage
 
-> **Note:** [Introduced][ee-2760] in [GitLab Premium][eep] 10.0.
-> Available in [GitLab CE][ce] 10.7
+> [Introduced][ee-2760] in [GitLab Premium][eep] 10.0. Brought to GitLab Core
+in 10.7.
 
-It is possible to store LFS objects on remote object storage instead of on a local disk.
+It is possible to store LFS objects on a remote object storage which allows you
+to offload storage to an external AWS S3 compatible service, freeing up disk
+space locally. You can also host your own S3 compatible storage decoupled from
+GitLab, with with a service such as [Minio](https://www.minio.io/).
 
-This allows you to offload storage to an external AWS S3 compatible service, freeing up disk space locally. You can also host your own S3 compatible storage decoupled from GitLab, with with a service such as [Minio](https://www.minio.io/).
+Object storage currently transfers files first to GitLab, and then on the
+object storage in a second stage. This can be done either by using a rake task
+to transfer existing objects, or in a background job after each file is received.
 
-Object storage currently transfers files first to GitLab, and then on the object storage in a second stage. This can be done either by using a rake task to transfer existing objects, or in a background job after each file is received.
-
-### Object Storage Settings
-
-For source installations the following settings are nested under `lfs:` and then `object_store:`. On omnibus installs they are prefixed by `lfs_object_store_`.
+The following general settings are supported.
 
 | Setting | Description | Default |
 |---------|-------------|---------|
@@ -68,9 +69,7 @@ For source installations the following settings are nested under `lfs:` and then
 | `proxy_download` | Set to true to enable proxying all files served. Option allows to reduce egress traffic as this allows clients to download directly from remote storage instead of proxying all data | `false` |
 | `connection` | Various connection options described below | |
 
-#### S3 compatible connection settings
-
-The connection settings match those provided by [Fog](https://github.com/fog), and are as follows:
+The `connection` settings match those provided by [Fog](https://github.com/fog).
 
 | Setting | Description | Default |
 |---------|-------------|---------|
@@ -82,8 +81,43 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
 | `endpoint` | Can be used when configuring an S3 compatible service such as [Minio](https://www.minio.io), by entering a URL such as `http://127.0.0.1:9000` | (optional) |
 | `path_style` | Set to true to use `host/bucket_name/object` style paths instead of `bucket_name.host/object`. Leave as false for AWS S3 | false |
 
+### S3 for Omnibus installations
+
+On Omnibus installations, the settings are prefixed by `lfs_object_store_`:
+
+1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with
+   the values you want:
+
+	```ruby
+	gitlab_rails['lfs_object_store_enabled'] = true
+	gitlab_rails['lfs_object_store_remote_directory'] = "lfs-objects"
+	gitlab_rails['lfs_object_store_connection'] = {
+	  'provider' => 'AWS',
+	  'region' => 'eu-central-1',
+	  'aws_access_key_id' => '1ABCD2EFGHI34JKLM567N',
+	  'aws_secret_access_key' => 'abcdefhijklmnopQRSTUVwxyz0123456789ABCDE',
+	  # The below options configure an S3 compatible host instead of AWS
+	  'host' => 'localhost',
+	  'endpoint' => 'http://127.0.0.1:9000',
+	  'path_style' => true
+	}
+	```
+
+1. Save the file and [reconfigure GitLab]s for the changes to take effect.
+1. Migrate any existing local LFS objects to the object storage:
+
+    ```bash
+    gitlab-rake gitlab:lfs:migrate
+    ```
+
+    This will migrate existing LFS objects to object storage. New LFS objects
+    will be forwarded to object storage unless
+    `gitlab_rails['lfs_object_store_background_upload']` is set to false.
 
-### From source
+### S3 for installations from source
+
+For source installations the settings are nested under `lfs:` and then
+`object_store:`:
 
 1. Edit `/home/git/gitlab/config/gitlab.yml` and add or amend the following
    lines:
@@ -108,44 +142,13 @@ The connection settings match those provided by [Fog](https://github.com/fog), a
 1. Save the file and [restart GitLab][] for the changes to take effect.
 1. Migrate any existing local LFS objects to the object storage:
 
-	```bash
-	sudo -u git -H bundle exec rake gitlab:lfs:migrate RAILS_ENV=production
-	```
-
-	This will migrate existing LFS objects to object storage. New LFS objects
-	will be forwarded to object storage unless
-	`gitlab_rails['lfs_object_store_background_upload']` is set to false.
-
-### In Omnibus
-
-1. Edit `/etc/gitlab/gitlab.rb` and add the following lines by replacing with
-   the values you want:
-
-	```ruby
-	gitlab_rails['lfs_object_store_enabled'] = true
-	gitlab_rails['lfs_object_store_remote_directory'] = "lfs-objects"
-	gitlab_rails['lfs_object_store_connection'] = {
-	  'provider' => 'AWS',
-	  'region' => 'eu-central-1',
-	  'aws_access_key_id' => '1ABCD2EFGHI34JKLM567N',
-	  'aws_secret_access_key' => 'abcdefhijklmnopQRSTUVwxyz0123456789ABCDE',
-	  # The below options configure an S3 compatible host instead of AWS
-	  'host' => 'localhost',
-	  'endpoint' => 'http://127.0.0.1:9000',
-	  'path_style' => true
-	}
-	```
-
-1. Save the file and [reconfigure GitLab]s for the changes to take effect.
-1. Migrate any existing local LFS objects to the object storage:
-
-      ```bash
-      gitlab-rake gitlab:lfs:migrate
-      ```
+    ```bash
+    sudo -u git -H bundle exec rake gitlab:lfs:migrate RAILS_ENV=production
+    ```
 
-      This will migrate existing LFS objects to object storage. New LFS objects
-      will be forwarded to object storage unless
-      `gitlab_rails['lfs_object_store_background_upload']` is set to false.
+    This will migrate existing LFS objects to object storage. New LFS objects
+    will be forwarded to object storage unless `background_upload` is set to
+    false.
 
 ## Storage statistics
 
diff --git a/doc/workflow/todos.md b/doc/workflow/todos.md
index 3d8d3ce8f1322842cde1688fb7f4dc5819918d2e..e612646cfbcf72cadf426074398d5c817e5ba7fc 100644
--- a/doc/workflow/todos.md
+++ b/doc/workflow/todos.md
@@ -28,11 +28,10 @@ A Todo appears in your Todos dashboard when:
 - an issue or merge request is assigned to you,
 - you are `@mentioned` in an issue or merge request, be it the description of
   the issue/merge request or in a comment,
+- you are `@mentioned` in a comment on a commit,
 - a job in the CI pipeline running for your merge request failed, but this
   job is not allowed to fail.
 
->**Note:** Commenting on a commit will _not_ trigger a Todo.
-
 ### Directly addressed Todos
 
 > [Introduced][ce-7926] in GitLab 9.0.
diff --git a/features/groups.feature b/features/groups.feature
deleted file mode 100644
index 4044bd9be79c93a142b414283b1b465c4234cdbb..0000000000000000000000000000000000000000
--- a/features/groups.feature
+++ /dev/null
@@ -1,73 +0,0 @@
-Feature: Groups
-  Background:
-    Given I sign in as "John Doe"
-    And "John Doe" is owner of group "Owned"
-
-  Scenario: I should not see a group if it does not exist
-    When I visit group "NonExistentGroup" page
-    Then page status code should be 404
-
-  @javascript
-  Scenario: I should see group "Owned" dashboard list
-    When I visit group "Owned" page
-    Then I should see group "Owned" projects list
-
-  @javascript
-  Scenario: I should see group "Owned" activity feed
-    When I visit group "Owned" activity page
-    And I should see projects activity feed
-
-  Scenario: I should see group "Owned" issues list
-    Given project from group "Owned" has issues assigned to me
-    When I visit group "Owned" issues page
-    Then I should see issues from group "Owned" assigned to me
-
-  Scenario: I should not see issues from archived project in "Owned" group issues list
-    Given Group "Owned" has archived project
-    And the archived project have some issues
-    When I visit group "Owned" issues page
-    Then I should not see issues from the archived project
-
-  Scenario: I should see group "Owned" merge requests list
-    Given project from group "Owned" has merge requests assigned to me
-    When I visit group "Owned" merge requests page
-    Then I should see merge requests from group "Owned" assigned to me
-
-  Scenario: I should not see merge requests from archived project in "Owned" group merge requests list
-    Given Group "Owned" has archived project
-    And the archived project have some merge_requests
-    When I visit group "Owned" merge requests page
-    Then I should not see merge requests from the archived project
-
-  Scenario: I edit group "Owned" avatar
-    When I visit group "Owned" settings page
-    And I change group "Owned" avatar
-    And I visit group "Owned" settings page
-    Then I should see new group "Owned" avatar
-    And I should see the "Remove avatar" button
-
-  Scenario: I remove group "Owned" avatar
-    When I visit group "Owned" settings page
-    And I have group "Owned" avatar
-    And I visit group "Owned" settings page
-    And I remove group "Owned" avatar
-    Then I should not see group "Owned" avatar
-    And I should not see the "Remove avatar" button
-
-  # Group projects in settings
-  Scenario: I should see all projects in the project list in settings
-    Given Group "Owned" has archived project
-    When I visit group "Owned" projects page
-    Then I should see group "Owned" projects list
-    And I should see "archived" label
-
-  # Public group
-  @javascript
-  Scenario: Signed out user should see group
-    Given "Mary Jane" is owner of group "Owned"
-    And I am a signed out user
-    And Group "Owned" has a public project "Public-project"
-    When I visit group "Owned" page
-    Then I should see group "Owned"
-    Then I should see project "Public-project"
-
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
deleted file mode 100644
index 753694a5392232ed8c5fba2fa4d2ef1126870f9a..0000000000000000000000000000000000000000
--- a/features/steps/groups.rb
+++ /dev/null
@@ -1,147 +0,0 @@
-class Spinach::Features::Groups < Spinach::FeatureSteps
-  include SharedAuthentication
-  include SharedPaths
-  include SharedGroup
-  include SharedUser
-
-  step 'I should see group "Owned"' do
-    expect(page).to have_content 'Owned'
-  end
-
-  step 'I am a signed out user' do
-    logout
-  end
-
-  step 'Group "Owned" has a public project "Public-project"' do
-    group = owned_group
-
-    @project = create :project, :public,
-                 group: group,
-                 name: "Public-project"
-  end
-
-  step 'I should see project "Public-project"' do
-    expect(page).to have_content 'Public-project'
-  end
-
-  step 'I should see group "Owned" projects list' do
-    owned_group.projects.each do |project|
-      expect(page).to have_link project.name
-    end
-  end
-
-  step 'I should see projects activity feed' do
-    expect(page).to have_content 'joined project'
-  end
-
-  step 'I should see issues from group "Owned" assigned to me' do
-    assigned_to_me(:issues).each do |issue|
-      expect(page).to have_content issue.title
-    end
-  end
-
-  step 'I should not see issues from the archived project' do
-    @archived_project.issues.each do |issue|
-      expect(page).not_to have_content issue.title
-    end
-  end
-
-  step 'I should not see merge requests from the archived project' do
-    @archived_project.merge_requests.each do |mr|
-      expect(page).not_to have_content mr.title
-    end
-  end
-
-  step 'I should see merge requests from group "Owned" assigned to me' do
-    assigned_to_me(:merge_requests).each do |issue|
-      expect(page).to have_content issue.title[0..80]
-    end
-  end
-
-  step 'project from group "Owned" has issues assigned to me' do
-    create :issue,
-      project: project,
-      assignees: [current_user],
-      author: current_user
-  end
-
-  step 'project from group "Owned" has merge requests assigned to me' do
-    create :merge_request,
-      source_project: project,
-      target_project: project,
-      assignee: current_user,
-      author: current_user
-  end
-
-  step 'I change group "Owned" avatar' do
-    attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
-    click_button "Save group"
-    owned_group.reload
-  end
-
-  step 'I should see new group "Owned" avatar' do
-    expect(owned_group.avatar).to be_instance_of AvatarUploader
-    expect(owned_group.avatar.url).to eq "/uploads/-/system/group/avatar/#{Group.find_by(name: "Owned").id}/banana_sample.gif"
-  end
-
-  step 'I should see the "Remove avatar" button' do
-    expect(page).to have_link("Remove avatar")
-  end
-
-  step 'I have group "Owned" avatar' do
-    attach_file(:group_avatar, File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif'))
-    click_button "Save group"
-    owned_group.reload
-  end
-
-  step 'I remove group "Owned" avatar' do
-    click_link "Remove avatar"
-    owned_group.reload
-  end
-
-  step 'I should not see group "Owned" avatar' do
-    expect(owned_group.avatar?).to eq false
-  end
-
-  step 'I should not see the "Remove avatar" button' do
-    expect(page).not_to have_link("Remove avatar")
-  end
-
-  step 'Group "Owned" has archived project' do
-    group = Group.find_by(name: 'Owned')
-    @archived_project = create(:project, :archived, namespace: group, path: "archived-project")
-  end
-
-  step 'I should see "archived" label' do
-    expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
-  end
-
-  step 'I visit group "NonExistentGroup" page' do
-    visit group_path("NonExistentGroup")
-  end
-
-  step 'the archived project have some issues' do
-    create :issue,
-      project: @archived_project,
-      assignees: [current_user],
-      author: current_user
-  end
-
-  step 'the archived project have some merge requests' do
-    create :merge_request,
-      source_project: @archived_project,
-      target_project: @archived_project,
-      assignee: current_user,
-      author: current_user
-  end
-
-  private
-
-  def assigned_to_me(key)
-    project.send(key).assigned_to(current_user)
-  end
-
-  def project
-    owned_group.projects.first
-  end
-end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 3a762be8f1f5df290fd3b505ba64cbe24dbb10ba..bba30a723252172f0451f61fa1d3518c3c1d0913 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -143,7 +143,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
   end
 
   step 'I create bare repo' do
-    click_link 'Create empty bare repository'
+    click_link 'Create empty repository'
   end
 
   step 'I should see command line instructions' do
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 982f45425a3402454e98394b5fced30e1749899a..684955a1b248814fb1395a4072e7027eea21018e 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -231,6 +231,20 @@ module API
           render_api_error!("Failed to save note #{note.errors.messages}", 400)
         end
       end
+
+      desc 'Get Merge Requests associated with a commit' do
+        success Entities::MergeRequestBasic
+      end
+      params do
+        requires :sha, type: String, desc: 'A commit sha, or the name of a branch or tag on which to find Merge Requests'
+        use :pagination
+      end
+      get ':id/repository/commits/:sha/merge_requests', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
+        commit = user_project.commit(params[:sha])
+        not_found! 'Commit' unless commit
+
+        present paginate(commit.merge_requests), with: Entities::MergeRequestBasic
+      end
     end
   end
 end
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index b0b7b50998f5cde4146bbcd17ad85eac7502406b..70d43ac1d79c777e92e9415595777ceef5d0ec65 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -54,7 +54,7 @@ module API
         present key, with: Entities::DeployKeysProject
       end
 
-      desc 'Add new deploy key to currently authenticated user' do
+      desc 'Add new deploy key to a project' do
         success Entities::DeployKeysProject
       end
       params do
@@ -66,33 +66,32 @@ module API
         params[:key].strip!
 
         # Check for an existing key joined to this project
-        key = user_project.deploy_keys_projects
+        deploy_key_project = user_project.deploy_keys_projects
                           .joins(:deploy_key)
                           .find_by(keys: { key: params[:key] })
 
-        if key
-          present key, with: Entities::DeployKeysProject
+        if deploy_key_project
+          present deploy_key_project, with: Entities::DeployKeysProject
           break
         end
 
         # Check for available deploy keys in other projects
         key = current_user.accessible_deploy_keys.find_by(key: params[:key])
         if key
-          added_key = add_deploy_keys_project(user_project, deploy_key: key, can_push: !!params[:can_push])
+          deploy_key_project = add_deploy_keys_project(user_project, deploy_key: key, can_push: !!params[:can_push])
 
-          present added_key, with: Entities::DeployKeysProject
+          present deploy_key_project, with: Entities::DeployKeysProject
           break
         end
 
         # Create a new deploy key
-        key_attributes = { can_push: !!params[:can_push],
-                           deploy_key_attributes: declared_params.except(:can_push) }
-        key = add_deploy_keys_project(user_project, key_attributes)
+        deploy_key_attributes = declared_params.except(:can_push).merge(user: current_user)
+        deploy_key_project = add_deploy_keys_project(user_project, deploy_key_attributes: deploy_key_attributes, can_push: !!params[:can_push])
 
-        if key.valid?
-          present key, with: Entities::DeployKeysProject
+        if deploy_key_project.valid?
+          present deploy_key_project, with: Entities::DeployKeysProject
         else
-          render_validation_error!(key)
+          render_validation_error!(deploy_key_project)
         end
       end
 
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 16147ee90c96c52ca897f4ab6eef9ffd92f176a3..b7a390696c78727681d6acc4f599515eb0e1f151 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -405,6 +405,7 @@ module API
 
     class IssueBasic < ProjectEntity
       expose :closed_at
+      expose :closed_by, using: Entities::UserBasic
       expose :labels do |issue, options|
         # Avoids an N+1 query since labels are preloaded
         issue.labels.map(&:title).sort
@@ -951,6 +952,7 @@ module API
       expose :tag_list
       expose :run_untagged
       expose :locked
+      expose :maximum_timeout
       expose :access_level
       expose :version, :revision, :platform, :architecture
       expose :contacted_at
@@ -1119,7 +1121,7 @@ module API
       end
 
       class RunnerInfo < Grape::Entity
-        expose :timeout
+        expose :metadata_timeout, as: :timeout
       end
 
       class Step < Grape::Entity
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index 14648588dfd1e00c7e1a684d2c254ed44103303e..abe3d3539843f6c511e9ef51816be6920b0887d4 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -29,18 +29,6 @@ module API
         {}
       end
 
-      def fix_git_env_repository_paths(env, repository_path)
-        if obj_dir_relative = env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence
-          env['GIT_OBJECT_DIRECTORY'] = File.join(repository_path, obj_dir_relative)
-        end
-
-        if alt_obj_dirs_relative = env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE'].presence
-          env['GIT_ALTERNATE_OBJECT_DIRECTORIES'] = alt_obj_dirs_relative.map { |dir| File.join(repository_path, dir) }
-        end
-
-        env
-      end
-
       def log_user_activity(actor)
         commands = Gitlab::GitAccess::DOWNLOAD_COMMANDS
 
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index b3660e4a1d0bbc6bc241b895d38fa1bb452e4f51..fcbc248fc3bd371f8d632678f11165047a6a3b23 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -21,8 +21,7 @@ module API
 
         # Stores some Git-specific env thread-safely
         env = parse_env
-        env = fix_git_env_repository_paths(env, repository_path) if project
-        Gitlab::Git::Env.set(env)
+        Gitlab::Git::HookEnv.set(gl_repository, env) if project
 
         actor =
           if params[:key_id]
diff --git a/lib/api/project_export.rb b/lib/api/project_export.rb
index efc4a33ae1b2e6cadf772b6b1305d1c868398035..5ef4e9d530c201e0b72517b8aa7ec280fe95ff77 100644
--- a/lib/api/project_export.rb
+++ b/lib/api/project_export.rb
@@ -33,11 +33,28 @@ module API
       end
       params do
         optional :description, type: String, desc: 'Override the project description'
+        optional :upload, type: Hash do
+          optional :url, type: String, desc: 'The URL to upload the project'
+          optional :http_method, type: String, default: 'PUT', desc: 'HTTP method to upload the exported project'
+        end
       end
       post ':id/export' do
         project_export_params = declared_params(include_missing: false)
+        after_export_params = project_export_params.delete(:upload) || {}
 
-        user_project.add_export_job(current_user: current_user, params: project_export_params)
+        export_strategy = if after_export_params[:url].present?
+                            params = after_export_params.slice(:url, :http_method).symbolize_keys
+
+                            Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy.new(params)
+                          end
+
+        if export_strategy&.invalid?
+          render_validation_error!(export_strategy)
+        else
+          user_project.add_export_job(current_user: current_user,
+                                      after_export_strategy: export_strategy,
+                                      params: project_export_params)
+        end
 
         accepted!
       end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 8da97a97754d0246f21425c0848747214d734442..57c0a7295351b5fd915953b3933e8f0a549e7083 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -14,9 +14,10 @@ module API
         optional :locked, type: Boolean, desc: 'Should Runner be locked for current project'
         optional :run_untagged, type: Boolean, desc: 'Should Runner handle untagged jobs'
         optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
+        optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
       end
       post '/' do
-        attributes = attributes_for_keys([:description, :locked, :run_untagged, :tag_list])
+        attributes = attributes_for_keys([:description, :locked, :run_untagged, :tag_list, :maximum_timeout])
           .merge(get_runner_details_from_request)
 
         runner =
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 996457c5dfecc1f5c4e9f560f40429b5f0ba0fa6..5f2a95676051e4d725ee5eaa8ec03e9187641d68 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -57,6 +57,7 @@ module API
         optional :locked, type: Boolean, desc: 'Flag indicating the runner is locked'
         optional :access_level, type: String, values: Ci::Runner.access_levels.keys,
                                 desc: 'The access_level of the runner'
+        optional :maximum_timeout, type: Integer, desc: 'Maximum timeout set when this Runner will handle the job'
         at_least_one_of :description, :active, :tag_list, :run_untagged, :locked, :access_level
       end
       put ':id' do
diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb
index 4383124d150b67dc80713bb67197b6c4c4d407e3..6a5a223a614117fd08cb3a17e47be6e7d5cd250b 100644
--- a/lib/backup/artifacts.rb
+++ b/lib/backup/artifacts.rb
@@ -5,9 +5,5 @@ module Backup
     def initialize
       super('artifacts', JobArtifactUploader.root)
     end
-
-    def create_files_dir
-      Dir.mkdir(app_files_dir, 0700)
-    end
   end
 end
diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb
index 635967f4bd465949847382d6e0b030b07c9144e5..f869916e199b5e26983f1bf94fc7dcce3d6f7f8e 100644
--- a/lib/backup/builds.rb
+++ b/lib/backup/builds.rb
@@ -5,9 +5,5 @@ module Backup
     def initialize
       super('builds', Settings.gitlab_ci.builds_path)
     end
-
-    def create_files_dir
-      Dir.mkdir(app_files_dir, 0700)
-    end
   end
 end
diff --git a/lib/backup/files.rb b/lib/backup/files.rb
index 287d591e88d27d48bb0076774d57f8303ecde17b..88cb7e7b5a43ef3224c8b6381a5cef75841fc802 100644
--- a/lib/backup/files.rb
+++ b/lib/backup/files.rb
@@ -1,7 +1,10 @@
 require 'open3'
+require_relative 'helper'
 
 module Backup
   class Files
+    include Backup::Helper
+
     attr_reader :name, :app_files_dir, :backup_tarball, :files_parent_dir
 
     def initialize(name, app_files_dir)
@@ -35,15 +38,22 @@ module Backup
 
     def restore
       backup_existing_files_dir
-      create_files_dir
 
-      run_pipeline!([%w(gzip -cd), %W(tar -C #{app_files_dir} -xf -)], in: backup_tarball)
+      run_pipeline!([%w(gzip -cd), %W(tar --unlink-first --recursive-unlink -C #{app_files_dir} -xf -)], in: backup_tarball)
     end
 
     def backup_existing_files_dir
-      timestamped_files_path = File.join(files_parent_dir, "#{name}.#{Time.now.to_i}")
+      timestamped_files_path = File.join(Gitlab.config.backup.path, "tmp", "#{name}.#{Time.now.to_i}")
       if File.exist?(app_files_dir)
-        FileUtils.mv(app_files_dir, File.expand_path(timestamped_files_path))
+        # Move all files in the existing repos directory except . and .. to
+        # repositories.old.<timestamp> directory
+        FileUtils.mkdir_p(timestamped_files_path, mode: 0700)
+        files = Dir.glob(File.join(app_files_dir, "*"), File::FNM_DOTMATCH) - [File.join(app_files_dir, "."), File.join(app_files_dir, "..")]
+        begin
+          FileUtils.mv(files, timestamped_files_path)
+        rescue Errno::EACCES
+          access_denied_error(app_files_dir)
+        end
       end
     end
 
diff --git a/lib/backup/helper.rb b/lib/backup/helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a1ee0faefe9e84d9e0340d6d81b6c6e0e5a3d201
--- /dev/null
+++ b/lib/backup/helper.rb
@@ -0,0 +1,17 @@
+module Backup
+  module Helper
+    def access_denied_error(path)
+      message = <<~EOS
+
+      ### NOTICE ###
+      As part of restore, the task tried to move existing content from #{path}.
+      However, it seems that directory contains files/folders that are not owned
+      by the user #{Gitlab.config.gitlab.user}. To proceed, please move the files
+      or folders inside #{path} to a secure location so that #{path} is empty and
+      run restore task again.
+
+      EOS
+      raise message
+    end
+  end
+end
diff --git a/lib/backup/lfs.rb b/lib/backup/lfs.rb
index 4153467fbeeea29b981762f667939aa0440ed5b2..4e234e50a7a9a42bd8caac0669b9e38410696507 100644
--- a/lib/backup/lfs.rb
+++ b/lib/backup/lfs.rb
@@ -5,9 +5,5 @@ module Backup
     def initialize
       super('lfs', Settings.lfs.storage_path)
     end
-
-    def create_files_dir
-      Dir.mkdir(app_files_dir, 0700)
-    end
   end
 end
diff --git a/lib/backup/pages.rb b/lib/backup/pages.rb
index 215ded93bfe04ce84fe5b325a4be23b4827bf631..5830b209d6ebb0251af3f5ff2b0a5ade50a30912 100644
--- a/lib/backup/pages.rb
+++ b/lib/backup/pages.rb
@@ -5,9 +5,5 @@ module Backup
     def initialize
       super('pages', Gitlab.config.pages.path)
     end
-
-    def create_files_dir
-      Dir.mkdir(app_files_dir, 0700)
-    end
   end
 end
diff --git a/lib/backup/registry.rb b/lib/backup/registry.rb
index 67fe023108726beb9e7f29886d3f68aa95d52e4b..916986694027b9874d660a7b3fad22b4a3d289ac 100644
--- a/lib/backup/registry.rb
+++ b/lib/backup/registry.rb
@@ -5,9 +5,5 @@ module Backup
     def initialize
       super('registry', Settings.registry.path)
     end
-
-    def create_files_dir
-      Dir.mkdir(app_files_dir, 0700)
-    end
   end
 end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 88a7f2a4235391ff024c9b14e33ed9b2ed9c0087..89e3f1d907662a9cb1346637d65f7acdcb3c4e27 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -1,8 +1,11 @@
 require 'yaml'
+require_relative 'helper'
 
 module Backup
   class Repository
+    include Backup::Helper
     # rubocop:disable Metrics/AbcSize
+
     def dump
       prepare
 
@@ -63,18 +66,27 @@ module Backup
       end
     end
 
-    def restore
+    def prepare_directories
       Gitlab.config.repositories.storages.each do |name, repository_storage|
         path = repository_storage.legacy_disk_path
         next unless File.exist?(path)
 
-        # Move repos dir to 'repositories.old' dir
-        bk_repos_path = File.join(path, '..', 'repositories.old.' + Time.now.to_i.to_s)
-        FileUtils.mv(path, bk_repos_path)
-        # This is expected from gitlab:check
-        FileUtils.mkdir_p(path, mode: 02770)
+        # Move all files in the existing repos directory except . and .. to
+        # repositories.old.<timestamp> directory
+        bk_repos_path = File.join(Gitlab.config.backup.path, "tmp", "#{name}-repositories.old." + Time.now.to_i.to_s)
+        FileUtils.mkdir_p(bk_repos_path, mode: 0700)
+        files = Dir.glob(File.join(path, "*"), File::FNM_DOTMATCH) - [File.join(path, "."), File.join(path, "..")]
+
+        begin
+          FileUtils.mv(files, bk_repos_path)
+        rescue Errno::EACCES
+          access_denied_error(path)
+        end
       end
+    end
 
+    def restore
+      prepare_directories
       Project.find_each(batch_size: 1000) do |project|
         progress.print " * #{display_repo_path(project)} ... "
         path_to_project_repo = path_to_repo(project)
diff --git a/lib/backup/uploads.rb b/lib/backup/uploads.rb
index 35118375499f53e553d8577af47b909c5b21574d..d46e2cd869d3300e41949723cb87c6366896786f 100644
--- a/lib/backup/uploads.rb
+++ b/lib/backup/uploads.rb
@@ -5,9 +5,5 @@ module Backup
     def initialize
       super('uploads', Rails.root.join('public/uploads'))
     end
-
-    def create_files_dir
-      Dir.mkdir(app_files_dir)
-    end
   end
 end
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index ce401c1c31c2baa3bf5a45da7f305a54fe2aabda..4a143baeef6bb6a990b859f1db6a5637b01a4b01 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -105,8 +105,12 @@ module Banzai
           end
         end
 
-        options = link_options.merge(href: match)
-        content_tag(:a, match.html_safe, options) + dropped
+        # match has come from node.to_html above, so we know it's encoded
+        # correctly.
+        html_safe_match = match.html_safe
+        options = link_options.merge(href: html_safe_match)
+
+        content_tag(:a, html_safe_match, options) + dropped
       end
 
       def autolink_filter(text)
diff --git a/lib/banzai/filter/emoji_filter.rb b/lib/banzai/filter/emoji_filter.rb
index b82c6ca639371a233338c4c506da12cf2c6280ee..e1261e7bbbe341cafb060480c57c6221c216acd6 100644
--- a/lib/banzai/filter/emoji_filter.rb
+++ b/lib/banzai/filter/emoji_filter.rb
@@ -11,7 +11,7 @@ module Banzai
       IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
 
       def call
-        search_text_nodes(doc).each do |node|
+        doc.search(".//text()").each do |node|
           content = node.to_html
           next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
 
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index c2b426733769fb01f7909ae38c303d370bc1f580..f2e9a5a1116f3eb2d0d9e984ca9d348f934e81d7 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -57,7 +57,7 @@ module Banzai
       ALLOWED_IMAGE_EXTENSIONS = /.+(jpg|png|gif|svg|bmp)\z/i.freeze
 
       def call
-        search_text_nodes(doc).each do |node|
+        doc.search(".//text()").each do |node|
           # A Gollum ToC tag is `[[_TOC_]]`, but due to MarkdownFilter running
           # before this one, it will be converted into `[[<em>TOC</em>]]`, so it
           # needs special-case handling
diff --git a/lib/banzai/filter/inline_diff_filter.rb b/lib/banzai/filter/inline_diff_filter.rb
index beb21b19ab3b60f075b038b23a6728380174e6ce..73e82a4d7e3a8f306fffca102101b852da9d3826 100644
--- a/lib/banzai/filter/inline_diff_filter.rb
+++ b/lib/banzai/filter/inline_diff_filter.rb
@@ -4,7 +4,7 @@ module Banzai
       IGNORED_ANCESTOR_TAGS = %w(pre code tt).to_set
 
       def call
-        search_text_nodes(doc).each do |node|
+        doc.search(".//text()").each do |node|
           next if has_ancestor?(node, IGNORED_ANCESTOR_TAGS)
 
           content = node.to_html
diff --git a/lib/gitlab/background_migration/migrate_build_stage.rb b/lib/gitlab/background_migration/migrate_build_stage.rb
index 8fe4f1a2289d098914ad5fdb3ac2a2c4c293c3c3..242e3143e71db7ced0fb9ee0d6df721756630077 100644
--- a/lib/gitlab/background_migration/migrate_build_stage.rb
+++ b/lib/gitlab/background_migration/migrate_build_stage.rb
@@ -12,6 +12,7 @@ module Gitlab
 
         class Build < ActiveRecord::Base
           self.table_name = 'ci_builds'
+          self.inheritance_column = :_type_disabled
 
           def ensure_stage!(attempts: 2)
             find_stage || create_stage!
diff --git a/lib/gitlab/ci/build/policy/kubernetes.rb b/lib/gitlab/ci/build/policy/kubernetes.rb
index b20d374288fa7a3651defeaba3e29d4c4c9c85b9..782f6c4c0af28741b43fc7e7eef73fb0602c2638 100644
--- a/lib/gitlab/ci/build/policy/kubernetes.rb
+++ b/lib/gitlab/ci/build/policy/kubernetes.rb
@@ -9,7 +9,7 @@ module Gitlab
             end
           end
 
-          def satisfied_by?(pipeline)
+          def satisfied_by?(pipeline, seed = nil)
             pipeline.has_kubernetes_active?
           end
         end
diff --git a/lib/gitlab/ci/build/policy/refs.rb b/lib/gitlab/ci/build/policy/refs.rb
index eadc0948d2f7b2f83190983839a111acb835535f..4aa5dc89f47aaf012a2d89b11bb22b2c5145f76b 100644
--- a/lib/gitlab/ci/build/policy/refs.rb
+++ b/lib/gitlab/ci/build/policy/refs.rb
@@ -7,7 +7,7 @@ module Gitlab
             @patterns = Array(refs)
           end
 
-          def satisfied_by?(pipeline)
+          def satisfied_by?(pipeline, seed = nil)
             @patterns.any? do |pattern|
               pattern, path = pattern.split('@', 2)
 
diff --git a/lib/gitlab/ci/build/policy/specification.rb b/lib/gitlab/ci/build/policy/specification.rb
index c317291f29df332b45188caaf599fdcdaf573067..f09ba42c074276f9e8ac8e12c5ab3ae5294613b2 100644
--- a/lib/gitlab/ci/build/policy/specification.rb
+++ b/lib/gitlab/ci/build/policy/specification.rb
@@ -15,7 +15,7 @@ module Gitlab
             @spec = spec
           end
 
-          def satisfied_by?(pipeline)
+          def satisfied_by?(pipeline, seed = nil)
             raise NotImplementedError
           end
         end
diff --git a/lib/gitlab/ci/build/policy/variables.rb b/lib/gitlab/ci/build/policy/variables.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9d2a362b7d4b63cf84078b1929bfca6b1cf46395
--- /dev/null
+++ b/lib/gitlab/ci/build/policy/variables.rb
@@ -0,0 +1,24 @@
+module Gitlab
+  module Ci
+    module Build
+      module Policy
+        class Variables < Policy::Specification
+          def initialize(expressions)
+            @expressions = Array(expressions)
+          end
+
+          def satisfied_by?(pipeline, seed)
+            variables = seed.to_resource.scoped_variables_hash
+
+            statements = @expressions.map do |statement|
+              ::Gitlab::Ci::Pipeline::Expression::Statement
+                .new(statement, variables)
+            end
+
+            statements.any?(&:truthful?)
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/ci/build/step.rb b/lib/gitlab/ci/build/step.rb
index 411f67f8ce7ccfc08768c1363c2eaa77415abe51..0b1ebe4e048b8a5c67c1afcfc60472ba4d048c00 100644
--- a/lib/gitlab/ci/build/step.rb
+++ b/lib/gitlab/ci/build/step.rb
@@ -14,7 +14,7 @@ module Gitlab
             self.new(:script).tap do |step|
               step.script = job.options[:before_script].to_a + job.options[:script].to_a
               step.script = job.commands.split("\n") if step.script.empty?
-              step.timeout = job.timeout
+              step.timeout = job.metadata_timeout
               step.when = WHEN_ON_SUCCESS
             end
           end
@@ -25,7 +25,7 @@ module Gitlab
 
             self.new(:after_script).tap do |step|
               step.script = after_script
-              step.timeout = job.timeout
+              step.timeout = job.metadata_timeout
               step.when = WHEN_ALWAYS
               step.allow_failure = true
             end
diff --git a/lib/gitlab/ci/config/entry/policy.rb b/lib/gitlab/ci/config/entry/policy.rb
index 0027e9ec8c5d89a2e62c776406a84a26bc141450..09e8e52b60fb4a355323716348779b90c6d96464 100644
--- a/lib/gitlab/ci/config/entry/policy.rb
+++ b/lib/gitlab/ci/config/entry/policy.rb
@@ -25,15 +25,31 @@ module Gitlab
             include Entry::Validatable
             include Entry::Attributable
 
-            attributes :refs, :kubernetes
+            attributes :refs, :kubernetes, :variables
 
             validations do
               validates :config, presence: true
-              validates :config, allowed_keys: %i[refs kubernetes]
+              validates :config, allowed_keys: %i[refs kubernetes variables]
+              validate :variables_expressions_syntax
 
               with_options allow_nil: true do
                 validates :refs, array_of_strings_or_regexps: true
                 validates :kubernetes, allowed_values: %w[active]
+                validates :variables, array_of_strings: true
+              end
+
+              def variables_expressions_syntax
+                return unless variables.is_a?(Array)
+
+                statements = variables.map do |statement|
+                  ::Gitlab::Ci::Pipeline::Expression::Statement.new(statement)
+                end
+
+                statements.each do |statement|
+                  unless statement.valid?
+                    errors.add(:variables, "Invalid expression syntax")
+                  end
+                end
               end
             end
           end
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index b2b00c8cb4b8ab521db85f7d907cba9042775c68..d299a5677deaf221b71eaa2d3a84270a76643696 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -17,8 +17,6 @@ module Gitlab
             # Populate pipeline with all stages and builds from pipeline seeds.
             #
             pipeline.stage_seeds.each do |stage|
-              stage.user = current_user
-
               pipeline.stages << stage.to_resource
 
               stage.seeds.each do |build|
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
index 48bde213d44dc42dccd9c374cfc5b78f6bb5ed40..346c92dc51ea3f8bbdb5900dedd7cb0ce305ccdc 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
@@ -4,7 +4,7 @@ module Gitlab
       module Expression
         module Lexeme
           class String < Lexeme::Value
-            PATTERN = /("(?<string>.+?)")|('(?<string>.+?)')/.freeze
+            PATTERN = /("(?<string>.*?)")|('(?<string>.*?)')/.freeze
 
             def initialize(value)
               @value = value
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
index b781c15fd67b73b79ca54465a2ea211b03ae2804..37643c8ef53c03e32a151c1acdf12f82c6a22b93 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
@@ -11,7 +11,7 @@ module Gitlab
             end
 
             def evaluate(variables = {})
-              HashWithIndifferentAccess.new(variables).fetch(@name, nil)
+              variables.with_indifferent_access.fetch(@name, nil)
             end
 
             def self.build(string)
diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb
index 4f0e101b7301fe9eec4f66d1c68a0561991928ab..09a7c98464b52d49f80588d895b0000054015200 100644
--- a/lib/gitlab/ci/pipeline/expression/statement.rb
+++ b/lib/gitlab/ci/pipeline/expression/statement.rb
@@ -14,12 +14,9 @@ module Gitlab
             %w[variable]
           ].freeze
 
-          def initialize(statement, pipeline)
+          def initialize(statement, variables = {})
             @lexer = Expression::Lexer.new(statement)
-
-            @variables = pipeline.variables.map do |variable|
-              [variable.key, variable.value]
-            end
+            @variables = variables.with_indifferent_access
           end
 
           def parse_tree
@@ -35,6 +32,16 @@ module Gitlab
           def evaluate
             parse_tree.evaluate(@variables.to_h)
           end
+
+          def truthful?
+            evaluate.present?
+          end
+
+          def valid?
+            parse_tree.is_a?(Lexeme::Base)
+          rescue StatementError
+            false
+          end
         end
       end
     end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 7cd7c864448dcad57b185590144d33f1c589fb63..6980b0b7aff29ae56e510b9917d19443cc52c2d7 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -11,21 +11,16 @@ module Gitlab
             @pipeline = pipeline
             @attributes = attributes
 
-            @only = attributes.delete(:only)
-            @except = attributes.delete(:except)
-          end
-
-          def user=(current_user)
-            @attributes.merge!(user: current_user)
+            @only = Gitlab::Ci::Build::Policy
+              .fabricate(attributes.delete(:only))
+            @except = Gitlab::Ci::Build::Policy
+              .fabricate(attributes.delete(:except))
           end
 
           def included?
             strong_memoize(:inclusion) do
-              only_specs = Gitlab::Ci::Build::Policy.fabricate(@only)
-              except_specs = Gitlab::Ci::Build::Policy.fabricate(@except)
-
-              only_specs.all? { |spec| spec.satisfied_by?(@pipeline) } &&
-                except_specs.none? { |spec| spec.satisfied_by?(@pipeline) }
+              @only.all? { |spec| spec.satisfied_by?(@pipeline, self) } &&
+                @except.none? { |spec| spec.satisfied_by?(@pipeline, self) }
             end
           end
 
@@ -33,6 +28,7 @@ module Gitlab
             @attributes.merge(
               pipeline: @pipeline,
               project: @pipeline.project,
+              user: @pipeline.user,
               ref: @pipeline.ref,
               tag: @pipeline.tag,
               trigger_request: @pipeline.legacy_trigger,
diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb
index 1fcbdc1b15ace6479356655523e384c0b0f19856..c101f30d6e81ce252501b8c593fcdd6ae78b32c7 100644
--- a/lib/gitlab/ci/pipeline/seed/stage.rb
+++ b/lib/gitlab/ci/pipeline/seed/stage.rb
@@ -17,10 +17,6 @@ module Gitlab
             end
           end
 
-          def user=(current_user)
-            @builds.each { |seed| seed.user = current_user }
-          end
-
           def attributes
             { name: @attributes.fetch(:name),
               pipeline: @pipeline,
diff --git a/lib/gitlab/ci/variables/collection.rb b/lib/gitlab/ci/variables/collection.rb
index 0deca55fe8fd62cce1524ed07578c852616eedb1..ad30b3f427c21e858d0d119aa24c735d909bb3db 100644
--- a/lib/gitlab/ci/variables/collection.rb
+++ b/lib/gitlab/ci/variables/collection.rb
@@ -30,7 +30,13 @@ module Gitlab
         end
 
         def to_runner_variables
-          self.map(&:to_hash)
+          self.map(&:to_runner_variable)
+        end
+
+        def to_hash
+          self.to_runner_variables
+            .map { |env| [env.fetch(:key), env.fetch(:value)] }
+            .to_h.with_indifferent_access
         end
       end
     end
diff --git a/lib/gitlab/ci/variables/collection/item.rb b/lib/gitlab/ci/variables/collection/item.rb
index 939912981e658c51c39800ab36b709d08216f8c9..23ed71db8b0e1bdc15b9c699466b957f6148af5f 100644
--- a/lib/gitlab/ci/variables/collection/item.rb
+++ b/lib/gitlab/ci/variables/collection/item.rb
@@ -17,7 +17,7 @@ module Gitlab
           end
 
           def ==(other)
-            to_hash == self.class.fabricate(other).to_hash
+            to_runner_variable == self.class.fabricate(other).to_runner_variable
           end
 
           ##
@@ -25,7 +25,7 @@ module Gitlab
           # don't expose `file` attribute at all (stems from what the runner
           # expects).
           #
-          def to_hash
+          def to_runner_variable
             @variable.reject do |hash_key, hash_value|
               hash_key == :file && hash_value == false
             end
diff --git a/lib/gitlab/ee_compat_check.rb b/lib/gitlab/ee_compat_check.rb
index 0fb71976883c7030012a215de279e6c2c09f7e95..5fdd5dcd374ca6fe908fed388d909fe029e1cb8b 100644
--- a/lib/gitlab/ee_compat_check.rb
+++ b/lib/gitlab/ee_compat_check.rb
@@ -2,8 +2,8 @@
 module Gitlab
   # Checks if a set of migrations requires downtime or not.
   class EeCompatCheck
-    DEFAULT_CE_PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-ce'.freeze
-    EE_REPO_URL = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
+    CANONICAL_CE_PROJECT_URL = 'https://gitlab.com/gitlab-org/gitlab-ce'.freeze
+    CANONICAL_EE_REPO_URL = 'https://gitlab.com/gitlab-org/gitlab-ee.git'.freeze
     CHECK_DIR = Rails.root.join('ee_compat_check')
     IGNORED_FILES_REGEX = %r{VERSION|CHANGELOG\.md|db/schema\.rb}i.freeze
     PLEASE_READ_THIS_BANNER = %Q{
@@ -11,57 +11,81 @@ module Gitlab
       ===================== PLEASE READ THIS =====================
       ============================================================
     }.freeze
+    STAY_STRONG_LINK_TO_DOCS = %Q{
+      Stay 💪! For more information, see
+      https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
+    }.freeze
     THANKS_FOR_READING_BANNER = %Q{
       ============================================================
       ==================== THANKS FOR READING ====================
       ============================================================\n
     }.freeze
 
-    attr_reader :ee_repo_dir, :patches_dir, :ce_project_url, :ce_repo_url, :ce_branch, :ee_branch_found
+    attr_reader :ee_repo_dir, :patches_dir
+    attr_reader :ce_project_url, :ee_repo_url
+    attr_reader :ce_branch, :ee_remote_with_branch, :ee_branch_found
     attr_reader :job_id, :failed_files
 
-    def initialize(branch:, ce_project_url: DEFAULT_CE_PROJECT_URL, job_id: nil)
+    def initialize(branch:, ce_project_url: CANONICAL_CE_PROJECT_URL, job_id: nil)
       @ee_repo_dir = CHECK_DIR.join('ee-repo')
       @patches_dir = CHECK_DIR.join('patches')
       @ce_branch = branch
       @ce_project_url = ce_project_url
-      @ce_repo_url = "#{ce_project_url}.git"
+      @ee_repo_url = ce_public_repo_url.sub('gitlab-ce', 'gitlab-ee')
       @job_id = job_id
     end
 
     def check
       ensure_patches_dir
-      add_remote('canonical-ce', "#{DEFAULT_CE_PROJECT_URL}.git")
-      generate_patch(branch: ce_branch, patch_path: ce_patch_full_path, remote: 'canonical-ce')
+      # We're generating the patch against the canonical-ce remote since forks'
+      # master branch are not necessarily up-to-date.
+      add_remote('canonical-ce', "#{CANONICAL_CE_PROJECT_URL}.git")
+      generate_patch(branch: ce_branch, patch_path: ce_patch_full_path, branch_remote: 'origin', master_remote: 'canonical-ce')
 
       ensure_ee_repo
       Dir.chdir(ee_repo_dir) do
         step("In the #{ee_repo_dir} directory")
 
-        add_remote('canonical-ee', EE_REPO_URL)
+        ee_remotes.each do |key, url|
+          add_remote(key, url)
+        end
+        fetch(branch: 'master', depth: 20, remote: 'canonical-ee')
 
         status = catch(:halt_check) do
           ce_branch_compat_check!
           delete_ee_branches_locally!
           ee_branch_presence_check!
 
-          step("Checking out #{ee_branch_found}", %W[git checkout -b #{ee_branch_found} canonical-ee/#{ee_branch_found}])
-          generate_patch(branch: ee_branch_found, patch_path: ee_patch_full_path, remote: 'canonical-ee')
+          step("Checking out #{ee_remote_with_branch}/#{ee_branch_found}", %W[git checkout -b #{ee_branch_found} #{ee_remote_with_branch}/#{ee_branch_found}])
+          generate_patch(branch: ee_branch_found, patch_path: ee_patch_full_path, branch_remote: ee_remote_with_branch, master_remote: 'canonical-ee')
           ee_branch_compat_check!
         end
 
         delete_ee_branches_locally!
 
-        if status.nil?
-          true
-        else
-          false
-        end
+        status.nil?
       end
     end
 
     private
 
+    def fork?
+      ce_project_url != CANONICAL_CE_PROJECT_URL
+    end
+
+    def ee_remotes
+      return @ee_remotes if defined?(@ee_remotes)
+
+      remotes =
+        {
+          'ee' => ee_repo_url,
+          'canonical-ee' => CANONICAL_EE_REPO_URL
+        }
+      remotes.delete('ee') unless fork?
+
+      @ee_remotes = remotes
+    end
+
     def add_remote(name, url)
       step(
         "Adding the #{name} remote (#{url})",
@@ -70,28 +94,32 @@ module Gitlab
     end
 
     def ensure_ee_repo
-      if Dir.exist?(ee_repo_dir)
-        step("#{ee_repo_dir} already exists")
-      else
-        step(
-          "Cloning #{EE_REPO_URL} into #{ee_repo_dir}",
-          %W[git clone --branch master --single-branch --depth=200 #{EE_REPO_URL} #{ee_repo_dir}]
-        )
+      unless clone_repo(ee_repo_url, ee_repo_dir)
+        # Fallback to using the canonical EE if there is no forked EE
+        clone_repo(CANONICAL_EE_REPO_URL, ee_repo_dir)
       end
     end
 
+    def clone_repo(url, dir)
+      _, status = step(
+        "Cloning #{url} into #{dir}",
+        %W[git clone --branch master --single-branch --depth=200 #{url} #{dir}]
+      )
+      status.zero?
+    end
+
     def ensure_patches_dir
       FileUtils.mkdir_p(patches_dir)
     end
 
-    def generate_patch(branch:, patch_path:, remote:)
+    def generate_patch(branch:, patch_path:, branch_remote:, master_remote:)
       FileUtils.rm(patch_path, force: true)
 
-      find_merge_base_with_master(branch: branch, master_remote: remote)
+      find_merge_base_with_master(branch: branch, branch_remote: branch_remote, master_remote: master_remote)
 
       step(
-        "Generating the patch against #{remote}/master in #{patch_path}",
-        %W[git diff --binary #{remote}/master...origin/#{branch}]
+        "Generating the patch against #{master_remote}/master in #{patch_path}",
+        %W[git diff --binary #{master_remote}/master...#{branch_remote}/#{branch}]
       ) do |output, status|
         throw(:halt_check, :ko) unless status.zero?
 
@@ -109,23 +137,22 @@ module Gitlab
     end
 
     def ee_branch_presence_check!
-      _, status = step("Fetching origin/#{ee_branch_prefix}", %W[git fetch canonical-ee #{ee_branch_prefix}])
-
-      if status.zero?
-        @ee_branch_found = ee_branch_prefix
-        return
+      ee_remotes.keys.each do |remote|
+        [ee_branch_prefix, ee_branch_suffix].each do |branch|
+          _, status = step("Fetching #{remote}/#{ee_branch_prefix}", %W[git fetch #{remote} #{branch}])
+
+          if status.zero?
+            @ee_remote_with_branch = remote
+            @ee_branch_found = branch
+            return true
+          end
+        end
       end
 
-      _, status = step("Fetching origin/#{ee_branch_suffix}", %W[git fetch canonical-ee #{ee_branch_suffix}])
-
-      if status.zero?
-        @ee_branch_found = ee_branch_suffix
-      else
-        puts
-        puts ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
+      puts
+      puts ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
 
-        throw(:halt_check, :ko)
-      end
+      throw(:halt_check, :ko)
     end
 
     def ee_branch_compat_check!
@@ -181,10 +208,10 @@ module Gitlab
       command(%W[git branch --delete --force #{ee_branch_suffix}])
     end
 
-    def merge_base_found?(master_remote:, branch:)
+    def merge_base_found?(branch:, branch_remote:, master_remote:)
       step(
         "Finding merge base with #{master_remote}/master",
-        %W[git merge-base #{master_remote}/master origin/#{branch}]
+        %W[git merge-base #{master_remote}/master #{branch_remote}/#{branch}]
       ) do |output, status|
         if status.zero?
           puts "Merge base was found: #{output}"
@@ -193,7 +220,7 @@ module Gitlab
       end
     end
 
-    def find_merge_base_with_master(branch:, master_remote:)
+    def find_merge_base_with_master(branch:, branch_remote:, master_remote:)
       # Start with (Math.exp(3).to_i = 20) until (Math.exp(6).to_i = 403)
       # In total we go (20 + 54 + 148 + 403 = 625) commits deeper
       depth = 20
@@ -202,10 +229,10 @@ module Gitlab
           depth += Math.exp(factor).to_i
           # Repository is initially cloned with a depth of 20 so we need to fetch
           # deeper in the case the branch has more than 20 commits on top of master
-          fetch(branch: branch, depth: depth, remote: 'origin')
+          fetch(branch: branch, depth: depth, remote: branch_remote)
           fetch(branch: 'master', depth: depth, remote: master_remote)
 
-          merge_base_found?(master_remote: master_remote, branch: branch)
+          merge_base_found?(branch: branch, branch_remote: branch_remote, master_remote: master_remote)
         end
 
       raise "\n#{branch} is too far behind #{master_remote}/master, please rebase it!\n" unless success
@@ -274,6 +301,13 @@ module Gitlab
       Gitlab::Popen.popen(cmd)
     end
 
+    # We're "re-creating" the repo URL because ENV['CI_REPOSITORY_URL'] contains
+    # redacted credentials (e.g. "***:****") which are useless in instructions
+    # the job gives.
+    def ce_public_repo_url
+      "#{ce_project_url}.git"
+    end
+
     def applies_cleanly_msg(branch)
       %Q{
         #{PLEASE_READ_THIS_BANNER}
@@ -288,13 +322,15 @@ module Gitlab
     end
 
     def ce_branch_doesnt_apply_cleanly_and_no_ee_branch_msg
+      ee_repos = ee_remotes.values.uniq
+
       %Q{
         #{PLEASE_READ_THIS_BANNER}
         💥 Oh no! 💥
 
         The `#{ce_branch}` branch does not apply cleanly to the current
         EE/master, and no `#{ee_branch_prefix}` or `#{ee_branch_suffix}` branch
-        was found in the EE repository.
+        was found in #{ee_repos.join(' nor in ')}.
 
         If you're a community contributor, don't worry, someone from
         GitLab Inc. will take care of this, and you don't have to do anything.
@@ -314,17 +350,17 @@ module Gitlab
         1. Create a new branch from master and cherry-pick your CE commits
 
           # In the EE repo
-          $ git fetch #{EE_REPO_URL} master
+          $ git fetch #{CANONICAL_EE_REPO_URL} master
           $ git checkout -b #{ee_branch_prefix} FETCH_HEAD
-          $ git fetch #{ce_repo_url} #{ce_branch}
+          $ git fetch #{ce_public_repo_url} #{ce_branch}
           $ git cherry-pick SHA # Repeat for all the commits you want to pick
 
-          You can squash the `#{ce_branch}` commits into a single "Port of #{ce_branch} to EE" commit.
+          Note: You can squash the `#{ce_branch}` commits into a single "Port of #{ce_branch} to EE" commit.
 
         2. Apply your branch's patch to EE
 
           # In the EE repo
-          $ git fetch #{EE_REPO_URL} master
+          $ git fetch #{CANONICAL_EE_REPO_URL} master
           $ git checkout -b #{ee_branch_prefix} FETCH_HEAD
           $ wget #{patch_url} && git apply --3way #{ce_patch_name}
 
@@ -356,10 +392,9 @@ module Gitlab
         ⚠️ Also, don't forget to create a new merge request on gitlab-ee and
         cross-link it with the CE merge request.
 
-        Once this is done, you can retry this failed build, and it should pass.
+        Once this is done, you can retry this failed job, and it should pass.
 
-        Stay 💪 ! For more information, see
-        https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
+        #{STAY_STRONG_LINK_TO_DOCS}
         #{THANKS_FOR_READING_BANNER}
       }
     end
@@ -371,16 +406,15 @@ module Gitlab
 
         The `#{ce_branch}` does not apply cleanly to the current EE/master, and
         even though a `#{ee_branch_found}` branch
-        exists in the EE repository, it does not apply cleanly either to
+        exists in #{ee_repo_url}, it does not apply cleanly either to
         EE/master!
 
         #{conflicting_files_msg}
 
         Please update the `#{ee_branch_found}`, push it again to gitlab-ee, and
-        retry this build.
+        retry this job.
 
-        Stay 💪 ! For more information, see
-        https://docs.gitlab.com/ce/development/automatic_ce_ee_merge.html
+        #{STAY_STRONG_LINK_TO_DOCS}
         #{THANKS_FOR_READING_BANNER}
       }
     end
diff --git a/lib/gitlab/git/gitmodules_parser.rb b/lib/gitlab/git/gitmodules_parser.rb
index 4a43b9b444d41c0115cf0a8c157daf519d006792..4b505312f60d03dad953870ea79f901798f1ef52 100644
--- a/lib/gitlab/git/gitmodules_parser.rb
+++ b/lib/gitlab/git/gitmodules_parser.rb
@@ -46,6 +46,8 @@ module Gitlab
         iterator = State.new
 
         @content.split("\n").each_with_object(iterator) do |text, iterator|
+          text.chomp!
+
           next if text =~ /^\s*#/
 
           if text =~ /\A\[submodule "(?<name>[^"]+)"\]\z/
@@ -55,7 +57,7 @@ module Gitlab
 
             next unless text =~ /\A\s*(?<key>\w+)\s*=\s*(?<value>.*)\z/
 
-            value = $~[:value].chomp
+            value = $~[:value]
             iterator.set_attribute($~[:key], value)
           end
         end
diff --git a/lib/gitlab/git/env.rb b/lib/gitlab/git/hook_env.rb
similarity index 64%
rename from lib/gitlab/git/env.rb
rename to lib/gitlab/git/hook_env.rb
index 9d0b47a1a6d98fd0e1d7925d3ff8400d1333066e..455e8451c10d1b6419e7d68f13cf9ba980ef9681 100644
--- a/lib/gitlab/git/env.rb
+++ b/lib/gitlab/git/hook_env.rb
@@ -3,37 +3,39 @@
 module Gitlab
   module Git
     # Ephemeral (per request) storage for environment variables that some Git
-    # commands may need.
+    # commands need during internal API calls made from Git push hooks.
     #
     # For example, in pre-receive hooks, new objects are put in a temporary
     # $GIT_OBJECT_DIRECTORY. Without it set, the new objects cannot be retrieved
     # (this would break push rules for instance).
     #
     # This class is thread-safe via RequestStore.
-    class Env
+    class HookEnv
       WHITELISTED_VARIABLES = %w[
-        GIT_OBJECT_DIRECTORY
         GIT_OBJECT_DIRECTORY_RELATIVE
-        GIT_ALTERNATE_OBJECT_DIRECTORIES
         GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
       ].freeze
 
-      def self.set(env)
+      def self.set(gl_repository, env)
         return unless RequestStore.active?
 
-        RequestStore.store[:gitlab_git_env] = whitelist_git_env(env)
+        raise "missing gl_repository" if gl_repository.blank?
+
+        RequestStore.store[:gitlab_git_env] ||= {}
+        RequestStore.store[:gitlab_git_env][gl_repository] = whitelist_git_env(env)
       end
 
-      def self.all
+      def self.all(gl_repository)
         return {} unless RequestStore.active?
 
-        RequestStore.fetch(:gitlab_git_env) { {} }
+        h = RequestStore.fetch(:gitlab_git_env) { {} }
+        h.fetch(gl_repository, {})
       end
 
-      def self.to_env_hash
+      def self.to_env_hash(gl_repository)
         env = {}
 
-        all.compact.each do |key, value|
+        all(gl_repository).compact.each do |key, value|
           value = value.join(File::PATH_SEPARATOR) if value.is_a?(Array)
           env[key.to_s] = value
         end
@@ -41,10 +43,6 @@ module Gitlab
         env
       end
 
-      def self.[](key)
-        all[key]
-      end
-
       def self.whitelist_git_env(env)
         env.select { |key, _| WHITELISTED_VARIABLES.include?(key.to_s) }.with_indifferent_access
       end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 2d16a81c8881557a9b1c06de9fc574b411e20f6f..e692c9ce342dcdfd7fd811872ca5bf4999fa1cff 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -1745,21 +1745,11 @@ module Gitlab
       end
 
       def alternate_object_directories
-        relative_paths = relative_object_directories
-
-        if relative_paths.any?
-          relative_paths.map { |d| File.join(path, d) }
-        else
-          absolute_object_directories.flat_map { |d| d.split(File::PATH_SEPARATOR) }
-        end
+        relative_object_directories.map { |d| File.join(path, d) }
       end
 
       def relative_object_directories
-        Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact
-      end
-
-      def absolute_object_directories
-        Gitlab::Git::Env.all.values_at(*ALLOWED_OBJECT_DIRECTORIES_VARIABLES).flatten.compact
+        Gitlab::Git::HookEnv.all(gl_repository).values_at(*ALLOWED_OBJECT_RELATIVE_DIRECTORIES_VARIABLES).flatten.compact
       end
 
       # Get the content of a blob for a given commit.  If the blob is a commit
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index 0a2a23e835bac0401e4c75bdaf711169d1cf646c..ed0644f6cf16f82476d569e0e5e7cb1af324ea17 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -99,8 +99,6 @@ module Gitlab
     end
 
     def check_active_user!
-      return if deploy_key?
-
       if user && !user_access.allowed?
         raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
       end
@@ -215,7 +213,7 @@ module Gitlab
         raise UnauthorizedError, ERROR_MESSAGES[:read_only]
       end
 
-      if deploy_key
+      if deploy_key?
         unless deploy_key.can_push_to?(project)
           raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload]
         end
@@ -305,8 +303,10 @@ module Gitlab
         case actor
         when User
           actor
+        when DeployKey
+          nil
         when Key
-          actor.user unless actor.is_a?(DeployKey)
+          actor.user
         when :ci
           nil
         end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 8ca30ffc2328c63319768daa17774c4d7a44b9dc..0abae70c443cbc2288f7a54bab6320fd1180c442 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -83,6 +83,10 @@ module Gitlab
       end
     end
 
+    def self.random_storage
+      Gitlab.config.repositories.storages.keys.sample
+    end
+
     def self.address(storage)
       params = Gitlab.config.repositories.storages[storage]
       raise "storage not found: #{storage.inspect}" if params.nil?
diff --git a/lib/gitlab/gitaly_client/remote_service.rb b/lib/gitlab/gitaly_client/remote_service.rb
index 58c356edfd1e58bd786369375d074c535ca48a26..f2d699d9dfb1e0edeb0e648f05fac4f7a7bc9502 100644
--- a/lib/gitlab/gitaly_client/remote_service.rb
+++ b/lib/gitlab/gitaly_client/remote_service.rb
@@ -3,6 +3,17 @@ module Gitlab
     class RemoteService
       MAX_MSG_SIZE = 128.kilobytes.freeze
 
+      def self.exists?(remote_url)
+        request = Gitaly::FindRemoteRepositoryRequest.new(remote: remote_url)
+
+        response = GitalyClient.call(GitalyClient.random_storage,
+                                     :remote_service,
+                                     :find_remote_repository, request,
+                                     timeout: GitalyClient.medium_timeout)
+
+        response.exists
+      end
+
       def initialize(repository)
         @repository = repository
         @gitaly_repo = repository.gitaly_repository
diff --git a/lib/gitlab/gitaly_client/util.rb b/lib/gitlab/gitaly_client/util.rb
index a8c6d478de8f36f5fb69497b87603cbcdbc37a26..405567db94aff76806d0df4ef1d26f76a26fd673 100644
--- a/lib/gitlab/gitaly_client/util.rb
+++ b/lib/gitlab/gitaly_client/util.rb
@@ -3,11 +3,9 @@ module Gitlab
     module Util
       class << self
         def repository(repository_storage, relative_path, gl_repository)
-          git_object_directory = Gitlab::Git::Env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence ||
-            Gitlab::Git::Env['GIT_OBJECT_DIRECTORY'].presence
-          git_alternate_object_directories =
-            Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE']).presence ||
-            Array.wrap(Gitlab::Git::Env['GIT_ALTERNATE_OBJECT_DIRECTORIES']).flat_map { |d| d.split(File::PATH_SEPARATOR) }
+          git_env = Gitlab::Git::HookEnv.all(gl_repository)
+          git_object_directory = git_env['GIT_OBJECT_DIRECTORY_RELATIVE'].presence
+          git_alternate_object_directories = Array.wrap(git_env['GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE'])
 
           Gitaly::Repository.new(
             storage_name: repository_storage,
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
index ab0b751fe242e73afcff3301666f9e63402d5a2e..b1b283e98b5bf9ef96bbcbc52019192ebe7225ac 100644
--- a/lib/gitlab/github_import/importer/repository_importer.rb
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -16,7 +16,8 @@ module Gitlab
         # Returns true if we should import the wiki for the project.
         def import_wiki?
           client.repository(project.import_source)&.has_wiki &&
-            !project.wiki_repository_exists?
+            !project.wiki_repository_exists? &&
+            Gitlab::GitalyClient::RemoteService.exists?(wiki_url)
         end
 
         # Imports the repository data.
@@ -55,7 +56,6 @@ module Gitlab
 
         def import_wiki_repository
           wiki_path = "#{project.disk_path}.wiki"
-          wiki_url = project.import_url.sub(/\.git\z/, '.wiki.git')
           storage_path = project.repository_storage_path
 
           gitlab_shell.import_repository(storage_path, wiki_path, wiki_url)
@@ -70,6 +70,10 @@ module Gitlab
           end
         end
 
+        def wiki_url
+          project.import_url.sub(/\.git\z/, '.wiki.git')
+        end
+
         def update_clone_time
           project.update_column(:last_repository_updated_at, Time.zone.now)
         end
diff --git a/lib/gitlab/http.rb b/lib/gitlab/http.rb
index 96558872a378db463aa8eccb40beec218a0b9851..9aca3b0fb264e007f5a5c8235fdb62a88d16bf8f 100644
--- a/lib/gitlab/http.rb
+++ b/lib/gitlab/http.rb
@@ -4,6 +4,8 @@
 # calling internal IP or services.
 module Gitlab
   class HTTP
+    BlockedUrlError = Class.new(StandardError)
+
     include HTTParty # rubocop:disable Gitlab/HTTParty
 
     connection_adapter ProxyHTTPConnectionAdapter
diff --git a/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..aef371d81ebce961140401392eeb2183775b5037
--- /dev/null
+++ b/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy.rb
@@ -0,0 +1,83 @@
+module Gitlab
+  module ImportExport
+    module AfterExportStrategies
+      class BaseAfterExportStrategy
+        include ActiveModel::Validations
+        extend Forwardable
+
+        StrategyError = Class.new(StandardError)
+
+        AFTER_EXPORT_LOCK_FILE_NAME = '.after_export_action'.freeze
+
+        private
+
+        attr_reader :project, :current_user
+
+        public
+
+        def initialize(attributes = {})
+          @options = OpenStruct.new(attributes)
+
+          self.class.instance_eval do
+            def_delegators :@options, *attributes.keys
+          end
+        end
+
+        def execute(current_user, project)
+          return unless project&.export_project_path
+
+          @project = project
+          @current_user = current_user
+
+          if invalid?
+            log_validation_errors
+
+            return
+          end
+
+          create_or_update_after_export_lock
+          strategy_execute
+
+          true
+        rescue => e
+          project.import_export_shared.error(e)
+          false
+        ensure
+          delete_after_export_lock
+        end
+
+        def to_json(options = {})
+          @options.to_h.merge!(klass: self.class.name).to_json
+        end
+
+        def self.lock_file_path(project)
+          return unless project&.export_path
+
+          File.join(project.export_path, AFTER_EXPORT_LOCK_FILE_NAME)
+        end
+
+        protected
+
+        def strategy_execute
+          raise NotImplementedError
+        end
+
+        private
+
+        def create_or_update_after_export_lock
+          FileUtils.touch(self.class.lock_file_path(project))
+        end
+
+        def delete_after_export_lock
+          lock_file = self.class.lock_file_path(project)
+
+          FileUtils.rm(lock_file) if lock_file.present? && File.exist?(lock_file)
+        end
+
+        def log_validation_errors
+          errors.full_messages.each { |msg| project.import_export_shared.add_error_message(msg) }
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb b/lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4371a7eff5663fd28fa41c02e434214fae60cc63
--- /dev/null
+++ b/lib/gitlab/import_export/after_export_strategies/download_notification_strategy.rb
@@ -0,0 +1,17 @@
+module Gitlab
+  module ImportExport
+    module AfterExportStrategies
+      class DownloadNotificationStrategy < BaseAfterExportStrategy
+        private
+
+        def strategy_execute
+          notification_service.project_exported(project, current_user)
+        end
+
+        def notification_service
+          @notification_service ||= NotificationService.new
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
new file mode 100644
index 0000000000000000000000000000000000000000..938664a95a1503d1646bb5db9492b2df666c4fa7
--- /dev/null
+++ b/lib/gitlab/import_export/after_export_strategies/web_upload_strategy.rb
@@ -0,0 +1,61 @@
+module Gitlab
+  module ImportExport
+    module AfterExportStrategies
+      class WebUploadStrategy < BaseAfterExportStrategy
+        PUT_METHOD = 'PUT'.freeze
+        POST_METHOD = 'POST'.freeze
+        INVALID_HTTP_METHOD = 'invalid. Only PUT and POST methods allowed.'.freeze
+
+        validates :url, url: true
+
+        validate do
+          unless [PUT_METHOD, POST_METHOD].include?(http_method.upcase)
+            errors.add(:http_method, INVALID_HTTP_METHOD)
+          end
+        end
+
+        def initialize(url:, http_method: PUT_METHOD)
+          super
+        end
+
+        protected
+
+        def strategy_execute
+          handle_response_error(send_file)
+
+          project.remove_exported_project_file
+        end
+
+        def handle_response_error(response)
+          unless response.success?
+            error_code = response.dig('Error', 'Code') || response.code
+            error_message = response.dig('Error', 'Message') || response.message
+
+            raise StrategyError.new("Error uploading the project. Code #{error_code}: #{error_message}")
+          end
+        end
+
+        private
+
+        def send_file
+          export_file = File.open(project.export_project_path)
+
+          Gitlab::HTTP.public_send(http_method.downcase, url, send_file_options(export_file)) # rubocop:disable GitlabSecurity/PublicSend
+        ensure
+          export_file.close if export_file
+        end
+
+        def send_file_options(export_file)
+          {
+            body_stream: export_file,
+            headers: headers
+          }
+        end
+
+        def headers
+          { 'Content-Length' => File.size(project.export_project_path).to_s }
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/import_export/after_export_strategy_builder.rb b/lib/gitlab/import_export/after_export_strategy_builder.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7eabcae23805bab217e00925e0ff2ec8290cab15
--- /dev/null
+++ b/lib/gitlab/import_export/after_export_strategy_builder.rb
@@ -0,0 +1,24 @@
+module Gitlab
+  module ImportExport
+    class AfterExportStrategyBuilder
+      StrategyNotFoundError = Class.new(StandardError)
+
+      def self.build!(strategy_klass, attributes = {})
+        return default_strategy.new unless strategy_klass
+
+        attributes ||= {}
+        klass = strategy_klass.constantize rescue nil
+
+        unless klass && klass < AfterExportStrategies::BaseAfterExportStrategy
+          raise StrategyNotFoundError.new("Strategy #{strategy_klass} not found")
+        end
+
+        klass.new(**attributes.symbolize_keys)
+      end
+
+      def self.default_strategy
+        AfterExportStrategies::DownloadNotificationStrategy
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 791a54e1b698b6a05489cdb26adc76924eed4081..598832fb2dff6badd5d94e5544c2e8598cfc2c58 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -19,7 +19,7 @@ module Gitlab
                     custom_attributes: 'ProjectCustomAttribute',
                     project_badges: 'Badge' }.freeze
 
-      USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id].freeze
+      USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id closed_by_id].freeze
 
       PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze
 
diff --git a/lib/gitlab/import_export/shared.rb b/lib/gitlab/import_export/shared.rb
index 3d3d998a6a321bdbde87231fbf80f84e0824732b..6d7c36ce38b36a2ecac822153cb8ab40ad8fe38a 100644
--- a/lib/gitlab/import_export/shared.rb
+++ b/lib/gitlab/import_export/shared.rb
@@ -22,7 +22,7 @@ module Gitlab
 
       def error(error)
         error_out(error.message, caller[0].dup)
-        @errors << error.message
+        add_error_message(error.message)
 
         # Debug:
         if error.backtrace
@@ -32,6 +32,14 @@ module Gitlab
         end
       end
 
+      def add_error_message(error_message)
+        @errors << error_message
+      end
+
+      def after_export_in_progress?
+        File.exist?(after_export_lock_file)
+      end
+
       private
 
       def relative_path
@@ -45,6 +53,10 @@ module Gitlab
       def error_out(message, caller)
         Rails.logger.error("Import/Export error raised on #{caller}: #{message}")
       end
+
+      def after_export_lock_file
+        AfterExportStrategies::BaseAfterExportStrategy.lock_file_path(project)
+      end
     end
   end
 end
diff --git a/lib/gitlab/proxy_http_connection_adapter.rb b/lib/gitlab/proxy_http_connection_adapter.rb
index c70d6f4cd8443b3c081f81de84c502e223097678..d682289b632cd4ff121ef1fd40a0d9e058306b22 100644
--- a/lib/gitlab/proxy_http_connection_adapter.rb
+++ b/lib/gitlab/proxy_http_connection_adapter.rb
@@ -10,8 +10,12 @@
 module Gitlab
   class ProxyHTTPConnectionAdapter < HTTParty::ConnectionAdapter
     def connection
-      if !allow_local_requests? && blocked_url?
-        raise URI::InvalidURIError
+      unless allow_local_requests?
+        begin
+          Gitlab::UrlBlocker.validate!(uri, allow_local_network: false)
+        rescue Gitlab::UrlBlocker::BlockedUrlError => e
+          raise Gitlab::HTTP::BlockedUrlError, "URL '#{uri}' is blocked: #{e.message}"
+        end
       end
 
       super
@@ -19,10 +23,6 @@ module Gitlab
 
     private
 
-    def blocked_url?
-      Gitlab::UrlBlocker.blocked_url?(uri, allow_private_networks: false)
-    end
-
     def allow_local_requests?
       options.fetch(:allow_local_requests, allow_settings_local_requests?)
     end
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
index 0f9f939e20443bc30b79f4f4d7b5ef4608f9a388..db97f65bd546ccfcf23f2552480ddcf4be0da450 100644
--- a/lib/gitlab/url_blocker.rb
+++ b/lib/gitlab/url_blocker.rb
@@ -2,48 +2,84 @@ require 'resolv'
 
 module Gitlab
   class UrlBlocker
-    class << self
-      def blocked_url?(url, allow_private_networks: true, valid_ports: [])
-        return false if url.nil?
+    BlockedUrlError = Class.new(StandardError)
 
-        blocked_ips = ["127.0.0.1", "::1", "0.0.0.0"]
-        blocked_ips.concat(Socket.ip_address_list.map(&:ip_address))
+    class << self
+      def validate!(url, allow_localhost: false, allow_local_network: true, valid_ports: [])
+        return true if url.nil?
 
         begin
           uri = Addressable::URI.parse(url)
-          # Allow imports from the GitLab instance itself but only from the configured ports
-          return false if internal?(uri)
+        rescue Addressable::URI::InvalidURIError
+          raise BlockedUrlError, "URI is invalid"
+        end
 
-          return true if blocked_port?(uri.port, valid_ports)
-          return true if blocked_user_or_hostname?(uri.user)
-          return true if blocked_user_or_hostname?(uri.hostname)
+        # Allow imports from the GitLab instance itself but only from the configured ports
+        return true if internal?(uri)
 
-          addrs_info = Addrinfo.getaddrinfo(uri.hostname, 80, nil, :STREAM)
-          server_ips = addrs_info.map(&:ip_address)
+        port = uri.port || uri.default_port
+        validate_port!(port, valid_ports) if valid_ports.any?
+        validate_user!(uri.user)
+        validate_hostname!(uri.hostname)
 
-          return true if (blocked_ips & server_ips).any?
-          return true if !allow_private_networks && private_network?(addrs_info)
-        rescue Addressable::URI::InvalidURIError
-          return true
+        begin
+          addrs_info = Addrinfo.getaddrinfo(uri.hostname, port, nil, :STREAM)
         rescue SocketError
-          return false
+          return true
         end
 
+        validate_localhost!(addrs_info) unless allow_localhost
+        validate_local_network!(addrs_info) unless allow_local_network
+
+        true
+      end
+
+      def blocked_url?(*args)
+        validate!(*args)
+
         false
+      rescue BlockedUrlError
+        true
       end
 
       private
 
-      def blocked_port?(port, valid_ports)
-        return false if port.blank? || valid_ports.blank?
+      def validate_port!(port, valid_ports)
+        return if port.blank?
+        # Only ports under 1024 are restricted
+        return if port >= 1024
+        return if valid_ports.include?(port)
 
-        port < 1024 && !valid_ports.include?(port)
+        raise BlockedUrlError, "Only allowed ports are #{valid_ports.join(', ')}, and any over 1024"
       end
 
-      def blocked_user_or_hostname?(value)
-        return false if value.blank?
+      def validate_user!(value)
+        return if value.blank?
+        return if value =~ /\A\p{Alnum}/
 
-        value !~ /\A\p{Alnum}/
+        raise BlockedUrlError, "Username needs to start with an alphanumeric character"
+      end
+
+      def validate_hostname!(value)
+        return if value.blank?
+        return if value =~ /\A\p{Alnum}/
+
+        raise BlockedUrlError, "Hostname needs to start with an alphanumeric character"
+      end
+
+      def validate_localhost!(addrs_info)
+        local_ips = ["127.0.0.1", "::1", "0.0.0.0"]
+        local_ips.concat(Socket.ip_address_list.map(&:ip_address))
+
+        return if (local_ips & addrs_info.map(&:ip_address)).empty?
+
+        raise BlockedUrlError, "Requests to localhost are not allowed"
+      end
+
+      def validate_local_network!(addrs_info)
+        return unless addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? }
+
+        raise BlockedUrlError, "Requests to the local network are not allowed"
       end
 
       def internal?(uri)
@@ -60,10 +96,6 @@ module Gitlab
           (uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
       end
 
-      def private_network?(addrs_info)
-        addrs_info.any? { |addr| addr.ipv4_private? || addr.ipv6_sitelocal? }
-      end
-
       def config
         Gitlab.config
       end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 37d3512990edd509c18cd02792acbc1648b708a1..8c0a4d55ea2f5f7abdf0a981f0fefe831dc6e94b 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -30,6 +30,7 @@ module Gitlab
         usage_data
       end
 
+      # rubocop:disable Metrics/AbcSize
       def system_usage_data
         {
           counts: {
@@ -50,6 +51,12 @@ module Gitlab
             clusters: ::Clusters::Cluster.count,
             clusters_enabled: ::Clusters::Cluster.enabled.count,
             clusters_disabled: ::Clusters::Cluster.disabled.count,
+            clusters_platforms_gke: ::Clusters::Cluster.gcp_installed.enabled.count,
+            clusters_platforms_user: ::Clusters::Cluster.user_provided.enabled.count,
+            clusters_applications_helm: ::Clusters::Applications::Helm.installed.count,
+            clusters_applications_ingress: ::Clusters::Applications::Ingress.installed.count,
+            clusters_applications_prometheus: ::Clusters::Applications::Prometheus.installed.count,
+            clusters_applications_runner: ::Clusters::Applications::Runner.installed.count,
             in_review_folder: ::Environment.in_review_folder.count,
             groups: Group.count,
             issues: Issue.count,
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 5619130c263f59618402bf2f94b3f0bce02857c5..b102812ec12f694b4a4141d762988945b186db4e 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -21,20 +21,19 @@ module Gitlab
         raise "Unsupported action: #{action}" unless ALLOWED_GIT_HTTP_ACTIONS.include?(action.to_s)
 
         project = repository.project
-        params = {
+
+        {
           GL_ID: Gitlab::GlId.gl_id(user),
           GL_REPOSITORY: Gitlab::GlRepository.gl_repository(project, is_wiki),
           GL_USERNAME: user&.username,
-          ShowAllRefs: show_all_refs
-        }
-        server = {
-          address: Gitlab::GitalyClient.address(project.repository_storage),
-          token: Gitlab::GitalyClient.token(project.repository_storage)
+          ShowAllRefs: show_all_refs,
+          Repository: repository.gitaly_repository.to_h,
+          RepoPath: 'ignored but not allowed to be empty in gitlab-workhorse',
+          GitalyServer: {
+            address: Gitlab::GitalyClient.address(project.repository_storage),
+            token: Gitlab::GitalyClient.token(project.repository_storage)
+          }
         }
-        params[:Repository] = repository.gitaly_repository.to_h
-        params[:GitalyServer] = server
-
-        params
       end
 
       def artifact_upload_ok
@@ -42,7 +41,7 @@ module Gitlab
       end
 
       def send_git_blob(repository, blob)
-        params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show)
+        params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
                    {
                      'GitalyServer' => gitaly_server_hash(repository),
                      'GetBlobRequest' => {
@@ -70,7 +69,7 @@ module Gitlab
         params = repository.archive_metadata(ref, Gitlab.config.gitlab.repository_downloads_path, format)
         raise "Repository or ref not found" if params.empty?
 
-        if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive)
+        if Gitlab::GitalyClient.feature_enabled?(:workhorse_archive, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
           params.merge!(
             'GitalyServer' => gitaly_server_hash(repository),
             'GitalyRepository' => repository.gitaly_repository.to_h
@@ -87,7 +86,7 @@ module Gitlab
       end
 
       def send_git_diff(repository, diff_refs)
-        params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff)
+        params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_diff, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
                    {
                      'GitalyServer' => gitaly_server_hash(repository),
                      'RawDiffRequest' => Gitaly::RawDiffRequest.new(
@@ -105,7 +104,7 @@ module Gitlab
       end
 
       def send_git_patch(repository, diff_refs)
-        params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch)
+        params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_send_git_patch, status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT)
                    {
                      'GitalyServer' => gitaly_server_hash(repository),
                      'RawPatchRequest' => Gitaly::RawPatchRequest.new(
diff --git a/lib/tasks/gitlab/uploads/migrate.rake b/lib/tasks/gitlab/uploads/migrate.rake
index c26c3ccb3be0ba2f466dccea365467a718fb70f7..78e18992a8e5845fdedeeb68fc2058d7c1fda91d 100644
--- a/lib/tasks/gitlab/uploads/migrate.rake
+++ b/lib/tasks/gitlab/uploads/migrate.rake
@@ -13,6 +13,7 @@ namespace :gitlab do
 
     def enqueue_batch(batch, index)
       job = ObjectStorage::MigrateUploadsWorker.enqueue!(batch,
+                                                         @model_class,
                                                          @mounted_as,
                                                          @to_store)
       puts "Enqueued job ##{index}: #{job}"
@@ -25,8 +26,8 @@ namespace :gitlab do
       Upload.class_eval { include EachBatch } unless Upload < EachBatch
 
       Upload
-        .where.not(store: @to_store)
-        .where(uploader: @uploader_class.to_s,
+        .where(store: [nil, ObjectStorage::Store::LOCAL],
+               uploader: @uploader_class.to_s,
                model_type: @model_class.base_class.sti_name)
     end
   end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a04f869f2bbcaa68758428a544fbbd7030835965..68d0c0c8854bac81b30ecf7ece6f7d1bb5634fa2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
 msgstr ""
 "Project-Id-Version: gitlab 1.0.0\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-03-12 19:50+0100\n"
-"PO-Revision-Date: 2018-03-12 19:50+0100\n"
+"POT-Creation-Date: 2018-03-27 14:40+0300\n"
+"PO-Revision-Date: 2018-03-27 14:40+0300\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 "Language: \n"
@@ -226,6 +226,9 @@ msgstr ""
 msgid "All"
 msgstr ""
 
+msgid "All changes are committed"
+msgstr ""
+
 msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
 msgstr ""
 
@@ -340,6 +343,9 @@ msgstr ""
 msgid "Assign to"
 msgstr ""
 
+msgid "Assigned to :name"
+msgstr ""
+
 msgid "Assignee"
 msgstr ""
 
@@ -417,6 +423,9 @@ msgstr[1] ""
 msgid "Branch <strong>%{branch_name}</strong> was created. To set up auto deploy, choose a GitLab CI Yaml template and commit your changes. %{link_to_autodeploy_doc}"
 msgstr ""
 
+msgid "Branch has changed"
+msgstr ""
+
 msgid "Branch is already taken"
 msgstr ""
 
@@ -570,6 +579,9 @@ msgstr ""
 msgid "Cancel"
 msgstr ""
 
+msgid "Cannot be merged automatically"
+msgstr ""
+
 msgid "Cannot modify managed Kubernetes cluster"
 msgstr ""
 
@@ -1039,6 +1051,9 @@ msgstr ""
 msgid "Commit statistics for %{ref} %{start_time} - %{end_time}"
 msgstr ""
 
+msgid "Commit to %{branchName} branch"
+msgstr ""
+
 msgid "CommitBoxTitle|Commit"
 msgstr ""
 
@@ -1084,6 +1099,9 @@ msgstr ""
 msgid "Compare Revisions"
 msgstr ""
 
+msgid "Compare changes with the last commit"
+msgstr ""
+
 msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
 msgstr ""
 
@@ -1099,6 +1117,9 @@ msgstr ""
 msgid "CompareBranches|There isn't anything to compare."
 msgstr ""
 
+msgid "Confidential"
+msgstr ""
+
 msgid "Confidentiality"
 msgstr ""
 
@@ -1195,6 +1216,12 @@ msgstr ""
 msgid "Create New Directory"
 msgstr ""
 
+msgid "Create a new branch"
+msgstr ""
+
+msgid "Create a new branch and merge request"
+msgstr ""
+
 msgid "Create a personal access token on your account to pull or push via %{protocol}."
 msgstr ""
 
@@ -1204,7 +1231,10 @@ msgstr ""
 msgid "Create directory"
 msgstr ""
 
-msgid "Create empty bare repository"
+msgid "Create empty repository"
+msgstr ""
+
+msgid "Create file"
 msgstr ""
 
 msgid "Create group label"
@@ -1219,6 +1249,15 @@ msgstr ""
 msgid "Create merge request and branch"
 msgstr ""
 
+msgid "Create new branch"
+msgstr ""
+
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
 msgid "Create new label"
 msgstr ""
 
@@ -1237,6 +1276,12 @@ msgstr ""
 msgid "CreateTokenToCloneLink|create a personal access token"
 msgstr ""
 
+msgid "Creates a new branch from %{branchName}"
+msgstr ""
+
+msgid "Creates a new branch from %{branchName} and re-directs to create a new merge request"
+msgstr ""
+
 msgid "Cron Timezone"
 msgstr ""
 
@@ -1311,6 +1356,9 @@ msgstr ""
 msgid "Directory name"
 msgstr ""
 
+msgid "Discard draft"
+msgstr ""
+
 msgid "Dismiss Cycle Analytics introduction box"
 msgstr ""
 
@@ -1347,6 +1395,9 @@ msgstr ""
 msgid "DownloadSource|Download"
 msgstr ""
 
+msgid "Downvotes"
+msgstr ""
+
 msgid "Due date"
 msgstr ""
 
@@ -1356,6 +1407,12 @@ msgstr ""
 msgid "Edit Pipeline Schedule %{id}"
 msgstr ""
 
+msgid "Edit files in the editor and commit changes here"
+msgstr ""
+
+msgid "Editing"
+msgstr ""
+
 msgid "Emails"
 msgstr ""
 
@@ -1410,6 +1467,12 @@ msgstr ""
 msgid "Environments|You don't have any environments right now."
 msgstr ""
 
+msgid "Error checking branch data. Please try again."
+msgstr ""
+
+msgid "Error committing changes. Please try again."
+msgstr ""
+
 msgid "Error fetching contributors data."
 msgstr ""
 
@@ -1497,6 +1560,9 @@ msgstr ""
 msgid "Fields on this page are now uneditable, you can configure"
 msgstr ""
 
+msgid "File name"
+msgstr ""
+
 msgid "Files"
 msgstr ""
 
@@ -1628,9 +1694,6 @@ msgstr ""
 msgid "GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group."
 msgstr ""
 
-msgid "GroupsTree|Are you sure you want to leave the \"${group.fullName}\" group?"
-msgstr ""
-
 msgid "GroupsTree|Create a project in this group."
 msgstr ""
 
@@ -1676,6 +1739,9 @@ msgstr ""
 msgid "HealthCheck|Unhealthy"
 msgstr ""
 
+msgid "Help"
+msgstr ""
+
 msgid "Hide value"
 msgid_plural "Hide values"
 msgstr[0] ""
@@ -1875,6 +1941,9 @@ msgstr ""
 msgid "List your GitHub repositories"
 msgstr ""
 
+msgid "Loading the GitLab IDE..."
+msgstr ""
+
 msgid "Lock"
 msgstr ""
 
@@ -1890,6 +1959,9 @@ msgstr ""
 msgid "Login"
 msgstr ""
 
+msgid "Manage all notifications"
+msgstr ""
+
 msgid "Manage group labels"
 msgstr ""
 
@@ -2042,6 +2114,9 @@ msgstr ""
 msgid "No assignee"
 msgstr ""
 
+msgid "No changes"
+msgstr ""
+
 msgid "No connection could be made to a Gitaly Server, please check your logs!"
 msgstr ""
 
@@ -2633,6 +2708,9 @@ msgstr ""
 msgid "Related Merged Requests"
 msgstr ""
 
+msgid "Related merge requests"
+msgstr ""
+
 msgid "Remind later"
 msgstr ""
 
@@ -2671,6 +2749,12 @@ msgstr ""
 msgid "Revert this merge request"
 msgstr ""
 
+msgid "Reviewing"
+msgstr ""
+
+msgid "Runners"
+msgstr ""
+
 msgid "Running"
 msgstr ""
 
@@ -2805,21 +2889,12 @@ msgstr ""
 msgid "Something went wrong when toggling the button"
 msgstr ""
 
-msgid "Something went wrong while closing the %{issuable}. Please try again later"
-msgstr ""
-
 msgid "Something went wrong while fetching the projects."
 msgstr ""
 
 msgid "Something went wrong while fetching the registry list."
 msgstr ""
 
-msgid "Something went wrong while reopening the %{issuable}. Please try again later"
-msgstr ""
-
-msgid "Something went wrong while resolving this discussion. Please try again."
-msgstr ""
-
 msgid "Something went wrong. Please try again."
 msgstr ""
 
@@ -3044,6 +3119,9 @@ msgstr ""
 msgid "Target Branch"
 msgstr ""
 
+msgid "Target branch"
+msgstr ""
+
 msgid "Team"
 msgstr ""
 
@@ -3435,6 +3513,9 @@ msgstr ""
 msgid "UploadLink|click to upload"
 msgstr ""
 
+msgid "Upvotes"
+msgstr ""
+
 msgid "Use the following registration token during setup:"
 msgstr ""
 
@@ -3444,6 +3525,9 @@ msgstr ""
 msgid "Variables are applied to environments via the runner. They can be protected by only exposing them to protected branches or tags. You can use variables for passwords, secret keys, or whatever you want."
 msgstr ""
 
+msgid "View and edit lines"
+msgstr ""
+
 msgid "View file @ "
 msgstr ""
 
@@ -3486,6 +3570,9 @@ msgstr ""
 msgid "We want to be sure it is you, please confirm you are not a robot."
 msgstr ""
 
+msgid "Web IDE"
+msgstr ""
+
 msgid "Wiki"
 msgstr ""
 
@@ -3597,6 +3684,9 @@ msgstr ""
 msgid "Withdraw Access Request"
 msgstr ""
 
+msgid "Write a commit message..."
+msgstr ""
+
 msgid "You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?"
 msgstr ""
 
@@ -3669,12 +3759,18 @@ msgstr ""
 msgid "You'll need to use different branch names to get a valid comparison."
 msgstr ""
 
+msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}"
+msgstr ""
+
 msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
 msgstr ""
 
 msgid "Your changes can be committed to %{branch_name} because a merge request is open."
 msgstr ""
 
+msgid "Your changes have been committed. Commit %{commitId} %{commitStats}"
+msgstr ""
+
 msgid "Your comment will not be visible to the public."
 msgstr ""
 
@@ -3902,3 +3998,6 @@ msgstr ""
 
 msgid "uses Kubernetes clusters to deploy your code!"
 msgstr ""
+
+msgid "with %{additions} additions, %{deletions} deletions."
+msgstr ""
diff --git a/package.json b/package.json
index 31edc3a8016e95f50ac9f442b7716022e0e053d4..56fd2575e91ecde88313d110c195d0d59eb1e125 100644
--- a/package.json
+++ b/package.json
@@ -121,8 +121,5 @@
     "nodemon": "^1.15.1",
     "prettier": "1.11.1",
     "webpack-dev-server": "^2.11.2"
-  },
-  "optionalDependencies": {
-    "fsevents": "^1.1.3"
   }
 }
diff --git a/qa/qa/scenario/bootable.rb b/qa/qa/scenario/bootable.rb
index d6de4d404c86dff675744cd1cdeeafee58417d82..dd12ea6d49275194749b4da229e8b1d797bd7d1c 100644
--- a/qa/qa/scenario/bootable.rb
+++ b/qa/qa/scenario/bootable.rb
@@ -23,7 +23,7 @@ module QA
 
           arguments.parse!(argv)
 
-          self.perform(**Runtime::Scenario.attributes)
+          self.perform(Runtime::Scenario.attributes, *arguments.default_argv)
         end
 
         private
diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb
index 0af9afd1ea4f5afb0df8b9b16a456f43cf15f6fb..567e5fd6ccab5fc2b09e68cbe39c8248bc59a419 100644
--- a/qa/qa/scenario/test/instance.rb
+++ b/qa/qa/scenario/test/instance.rb
@@ -11,7 +11,7 @@ module QA
 
         tags :core
 
-        def perform(address, *files)
+        def perform(address, *rspec_options)
           Runtime::Scenario.define(:gitlab_address, address)
 
           ##
@@ -22,9 +22,9 @@ module QA
           Specs::Runner.perform do |specs|
             specs.tty = true
             specs.tags = self.class.focus
-            specs.files =
-              if files.any?
-                files
+            specs.options =
+              if rspec_options.any?
+                rspec_options
               else
                 File.expand_path('../../specs/features', __dir__)
               end
diff --git a/qa/qa/scenario/test/integration/mattermost.rb b/qa/qa/scenario/test/integration/mattermost.rb
index d939f52ab16e93823b09c183a3eb5c4a2bc136eb..13bfad28b0ba15496e0c54637e94800f7c84bd98 100644
--- a/qa/qa/scenario/test/integration/mattermost.rb
+++ b/qa/qa/scenario/test/integration/mattermost.rb
@@ -9,10 +9,10 @@ module QA
         class Mattermost < Test::Instance
           tags :core, :mattermost
 
-          def perform(address, mattermost, *files)
+          def perform(address, mattermost, *rspec_options)
             Runtime::Scenario.define(:mattermost_address, mattermost)
 
-            super(address, *files)
+            super(address, *rspec_options)
           end
         end
       end
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index 752e3e60b8c3d6ef71d0493d872be55400622f08..f8f6fe6559932e9879d12219f68ce2947f302d94 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -3,19 +3,19 @@ require 'rspec/core'
 module QA
   module Specs
     class Runner < Scenario::Template
-      attr_accessor :tty, :tags, :files
+      attr_accessor :tty, :tags, :options
 
       def initialize
         @tty = false
         @tags = []
-        @files = [File.expand_path('./features', __dir__)]
+        @options = [File.expand_path('./features', __dir__)]
       end
 
       def perform
         args = []
         args.push('--tty') if tty
         tags.to_a.each { |tag| args.push(['-t', tag.to_s]) }
-        args.push(files)
+        args.push(options)
 
         Runtime::Browser.configure!
 
diff --git a/qa/spec/scenario/test/instance_spec.rb b/qa/spec/scenario/test/instance_spec.rb
index bd09c28e924bcc43fc4f27d897fa3eedb3560f63..a74a9538be8cf98c92dd0681aea00c26a0183e5d 100644
--- a/qa/spec/scenario/test/instance_spec.rb
+++ b/qa/spec/scenario/test/instance_spec.rb
@@ -29,7 +29,7 @@ describe QA::Scenario::Test::Instance do
       it 'should call runner with default arguments' do
         subject.perform("test")
 
-        expect(runner).to have_received(:files=)
+        expect(runner).to have_received(:options=)
           .with(File.expand_path('../../../qa/specs/features', __dir__))
       end
     end
@@ -38,7 +38,7 @@ describe QA::Scenario::Test::Instance do
       it 'should call runner with paths' do
         subject.perform('test', 'path1', 'path2')
 
-        expect(runner).to have_received(:files=).with(%w[path1 path2])
+        expect(runner).to have_received(:options=).with(%w[path1 path2])
       end
     end
   end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 5b2614163ff010c5a768d4203c716d9b153478b3..548c5ef36e7977f1d3663ea87606d304c8d0d696 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -107,7 +107,7 @@ describe Projects::MilestonesController do
       it 'shows group milestone' do
         post :promote, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid
 
-        expect(flash[:notice]).to eq("#{milestone.title} promoted to group milestone")
+        expect(flash[:notice]).to eq("#{milestone.title} promoted to <a href=\"#{group_milestone_path(project.group, milestone.iid)}\">group milestone</a>.")
         expect(response).to redirect_to(project_milestones_path(project))
       end
     end
diff --git a/spec/factories/ci/build_metadata.rb b/spec/factories/ci/build_metadata.rb
new file mode 100644
index 0000000000000000000000000000000000000000..66bbd977b88f6de50127af9e050cac531f1e5866
--- /dev/null
+++ b/spec/factories/ci/build_metadata.rb
@@ -0,0 +1,9 @@
+FactoryBot.define do
+  factory :ci_build_metadata, class: Ci::BuildMetadata do
+    build factory: :ci_build
+
+    after(:build) do |build_metadata, _|
+      build_metadata.project ||= build_metadata.build.project
+    end
+  end
+end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index c89bc54cad4acb8d389ee05ad14de063eb4cf315..3005d74c3cf4f389eeea8b7ab6749514e6825a94 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -120,6 +120,53 @@ feature 'Admin updates settings' do
     expect(page).to have_content "Application settings saved successfully"
   end
 
+  scenario 'Change Performance bar settings' do
+    group = create(:group)
+
+    page.within('.as-performance') do
+      check 'Enable the Performance Bar'
+      fill_in 'Allowed group', with: group.path
+      click_on 'Save changes'
+    end
+
+    expect(page).to have_content "Application settings saved successfully"
+    expect(find_field('Enable the Performance Bar')).to be_checked
+    expect(find_field('Allowed group').value).to eq group.path
+
+    page.within('.as-performance') do
+      uncheck 'Enable the Performance Bar'
+      click_on 'Save changes'
+    end
+
+    expect(page).to have_content 'Application settings saved successfully'
+    expect(find_field('Enable the Performance Bar')).not_to be_checked
+    expect(find_field('Allowed group').value).to be_nil
+  end
+
+  scenario 'Change Background jobs settings' do
+    page.within('.as-background') do
+      fill_in 'Throttling Factor', with: 1
+      click_button 'Save changes'
+    end
+
+    expect(Gitlab::CurrentSettings.sidekiq_throttling_factor).to eq(1)
+    expect(page).to have_content "Application settings saved successfully"
+  end
+
+  scenario 'Change Spam settings' do
+    page.within('.as-spam') do
+      check 'Enable reCAPTCHA'
+      fill_in 'reCAPTCHA Site Key', with: 'key'
+      fill_in 'reCAPTCHA Private Key', with: 'key'
+      fill_in 'IPs per user', with: 15
+      click_button 'Save changes'
+    end
+
+    expect(page).to have_content "Application settings saved successfully"
+    expect(Gitlab::CurrentSettings.recaptcha_enabled).to be true
+    expect(Gitlab::CurrentSettings.unique_ips_limit_per_user).to eq(15)
+  end
+
   scenario 'Change Slack Notifications Service template settings' do
     first(:link, 'Service Templates').click
     click_link 'Slack notifications'
@@ -172,29 +219,6 @@ feature 'Admin updates settings' do
     expect(find_field('ED25519 SSH keys').value).to eq(forbidden)
   end
 
-  scenario 'Change Performance Bar settings' do
-    group = create(:group)
-
-    check 'Enable the Performance Bar'
-    fill_in 'Allowed group', with: group.path
-
-    click_on 'Save'
-
-    expect(page).to have_content 'Application settings saved successfully'
-
-    expect(find_field('Enable the Performance Bar')).to be_checked
-    expect(find_field('Allowed group').value).to eq group.path
-
-    uncheck 'Enable the Performance Bar'
-
-    click_on 'Save'
-
-    expect(page).to have_content 'Application settings saved successfully'
-
-    expect(find_field('Enable the Performance Bar')).not_to be_checked
-    expect(find_field('Allowed group').value).to be_nil
-  end
-
   def check_all_events
     page.check('Active')
     page.check('Push')
diff --git a/spec/features/groups/activity_spec.rb b/spec/features/groups/activity_spec.rb
index d3b25ec3d6c4b99c345377f5f4cae60e81911c9a..7bc809b310409511c6eda6c8c9eecba5f74eb409 100644
--- a/spec/features/groups/activity_spec.rb
+++ b/spec/features/groups/activity_spec.rb
@@ -8,11 +8,30 @@ feature 'Group activity page' do
   context 'when signed in' do
     before do
       sign_in(user)
-      visit path
     end
 
-    it_behaves_like "it has an RSS button with current_user's RSS token"
-    it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+    describe 'RSS' do
+      before do
+        visit path
+      end
+
+      it_behaves_like "it has an RSS button with current_user's RSS token"
+      it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+    end
+
+    context 'when project is in the group', :js do
+      let(:project) { create(:project, :public, namespace: group) }
+
+      before do
+        project.add_master(user)
+
+        visit path
+      end
+
+      it 'renders user joined to project event' do
+        expect(page).to have_content 'joined project'
+      end
+    end
   end
 
   context 'when signed out' do
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index b83bad3befb52811e0fe5dd5d70c49f9f035009a..1ce30015e81592b3d6de15ca6ffbfe754431802f 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -76,6 +76,27 @@ feature 'Edit group settings' do
       end
     end
   end
+
+  describe 'edit group avatar' do
+    before do
+      visit edit_group_path(group)
+
+      attach_file(:group_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif'))
+
+      expect { click_button 'Save group' }.to change { group.reload.avatar? }.to(true)
+    end
+
+    it 'uploads new group avatar' do
+      expect(group.avatar).to be_instance_of AvatarUploader
+      expect(group.avatar.url).to eq "/uploads/-/system/group/avatar/#{group.id}/banana_sample.gif"
+      expect(page).to have_link('Remove avatar')
+    end
+
+    it 'removes group avatar' do
+      expect { click_link 'Remove avatar' }.to change { group.reload.avatar? }.to(false)
+      expect(page).not_to have_link('Remove avatar')
+    end
+  end
 end
 
 def update_path(new_group_path)
diff --git a/spec/features/groups/issues_spec.rb b/spec/features/groups/issues_spec.rb
index 450bc0ff8cf95d23418c8a9e0c253931004aa83d..90bf7ba49f6eda39c733726d58421b81d33f5524 100644
--- a/spec/features/groups/issues_spec.rb
+++ b/spec/features/groups/issues_spec.rb
@@ -3,8 +3,11 @@ require 'spec_helper'
 feature 'Group issues page' do
   include FilteredSearchHelpers
 
+  let(:group) { create(:group) }
+  let(:project) { create(:project, :public, group: group)}
+  let(:path) { issues_group_path(group) }
+
   context 'with shared examples' do
-    let(:path) { issues_group_path(group) }
     let(:issuable) { create(:issue, project: project, title: "this is my created issuable")}
 
     include_examples 'project features apply to issuables', Issue
@@ -31,7 +34,6 @@ feature 'Group issues page' do
       let(:access_level) { ProjectFeature::ENABLED }
       let(:user) { user_in_group }
       let(:user2) { user_outside_group }
-      let(:path) { issues_group_path(group) }
 
       it 'filters by only group users' do
         filtered_search.set('assignee:')
@@ -43,9 +45,7 @@ feature 'Group issues page' do
   end
 
   context 'issues list', :nested_groups do
-    let(:group) { create(:group)}
     let(:subgroup) { create(:group, parent: group) }
-    let(:project) { create(:project, :public, group: group)}
     let(:subgroup_project) { create(:project, :public, group: subgroup)}
     let!(:issue) { create(:issue, project: project, title: 'root group issue') }
     let!(:subgroup_issue) { create(:issue, project: subgroup_project, title: 'subgroup issue') }
@@ -59,5 +59,17 @@ feature 'Group issues page' do
         expect(page).to have_content('subgroup issue')
       end
     end
+
+    context 'when project is archived' do
+      before do
+        project.archive!
+      end
+
+      it 'does not render issue' do
+        visit path
+
+        expect(page).not_to have_content issue.title[0..80]
+      end
+    end
   end
 end
diff --git a/spec/features/groups/merge_requests_spec.rb b/spec/features/groups/merge_requests_spec.rb
index 7ce6a61d50ca405f56055ff548165bcf3a71989b..672ae785c2dbf23df324c3094acf4473e58a323c 100644
--- a/spec/features/groups/merge_requests_spec.rb
+++ b/spec/features/groups/merge_requests_spec.rb
@@ -5,14 +5,14 @@ feature 'Group merge requests page' do
 
   let(:path) { merge_requests_group_path(group) }
   let(:issuable) { create(:merge_request, source_project: project, target_project: project, title: 'this is my created issuable') }
+  let(:access_level) { ProjectFeature::ENABLED }
+  let(:user) { user_in_group }
 
   include_examples 'project features apply to issuables', MergeRequest
 
   context 'archived issuable' do
     let(:project_archived) { create(:project, :archived, :merge_requests_enabled, :repository, group: group) }
     let(:issuable_archived) { create(:merge_request, source_project: project_archived, target_project: project_archived, title: 'issuable of an archived project') }
-    let(:access_level) { ProjectFeature::ENABLED }
-    let(:user) { user_in_group }
 
     before do
       issuable_archived
@@ -36,9 +36,17 @@ feature 'Group merge requests page' do
     end
   end
 
+  context 'when merge request assignee to user' do
+    before do
+      issuable.update!(assignee: user)
+
+      visit path
+    end
+
+    it { expect(page).to have_content issuable.title[0..80] }
+  end
+
   context 'group filtered search', :js do
-    let(:access_level) { ProjectFeature::ENABLED }
-    let(:user) { user_in_group }
     let(:user2) { user_outside_group }
 
     it 'filters by assignee only group users' do
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index ceccc4714057b0217cd285816fba37b8a7db296e..4ffadbbcd3582e7b5e2bac75bbeae4923e789b37 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -15,14 +15,44 @@ feature 'Group show page' do
     end
 
     it_behaves_like "an autodiscoverable RSS feed with current_user's RSS token"
+
+    context 'when group does not exist' do
+      let(:path) { group_path('not-exist') }
+
+      it { expect(status_code).to eq(404) }
+    end
   end
 
   context 'when signed out' do
-    before do
-      visit path
+    describe 'RSS' do
+      before do
+        visit path
+      end
+
+      it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+    end
+
+    context 'when group has a public project', :js do
+      let!(:project) { create(:project, :public, namespace: group) }
+
+      it 'renders public project' do
+        visit path
+
+        expect(page).to have_link group.name
+        expect(page).to have_link project.name
+      end
     end
 
-    it_behaves_like "an autodiscoverable RSS feed without an RSS token"
+    context 'when group has a private project', :js do
+      let!(:project) { create(:project, :private, namespace: group) }
+
+      it 'does not render private project' do
+        visit path
+
+        expect(page).to have_link group.name
+        expect(page).not_to have_link project.name
+      end
+    end
   end
 
   context 'subgroup support' do
diff --git a/spec/features/groups/user_browse_projects_group_page_spec.rb b/spec/features/groups/user_browse_projects_group_page_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e81c3180e78102db0a09f0953abda41817956fe7
--- /dev/null
+++ b/spec/features/groups/user_browse_projects_group_page_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+describe 'User browse group projects page' do
+  let(:user) { create :user }
+  let(:group) { create :group }
+
+  context 'when user is owner' do
+    before do
+      group.add_owner(user)
+    end
+
+    context 'when user signed in' do
+      before do
+        sign_in(user)
+      end
+
+      context 'when group has archived project', :js do
+        let!(:project) { create :project, :archived, namespace: group }
+
+        it 'renders projects list' do
+          visit projects_group_path(group)
+
+          expect(page).to have_link project.name
+          expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived')
+        end
+      end
+    end
+  end
+end
diff --git a/spec/features/issuables/discussion_lock_spec.rb b/spec/features/issuables/discussion_lock_spec.rb
index ecbe51a7bc2540e7beb923b9ef21ad72412ad6e1..7ea29ff252b448e1c883944752b23d836107f52a 100644
--- a/spec/features/issuables/discussion_lock_spec.rb
+++ b/spec/features/issuables/discussion_lock_spec.rb
@@ -14,7 +14,7 @@ describe 'Discussion Lock', :js do
       project.add_developer(user)
     end
 
-    context 'when the discussion is   unlocked' do
+    context 'when the discussion is unlocked' do
       it 'the user can lock the issue' do
         visit project_issue_path(project, issue)
 
diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb
index b835558b1427f586c3f9149658f41aab8079850f..27551bb70eef06fbfb597c41a1586fed94112b2e 100644
--- a/spec/features/issues/issue_sidebar_spec.rb
+++ b/spec/features/issues/issue_sidebar_spec.rb
@@ -161,6 +161,50 @@ feature 'Issue Sidebar' do
         end
       end
     end
+
+    context 'interacting with collapsed sidebar', :js do
+      collapsed_sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed'
+      expanded_sidebar_selector = 'aside.right-sidebar.right-sidebar-expanded'
+      confidentiality_sidebar_block = '.block.confidentiality'
+      lock_sidebar_block = '.block.lock'
+      collapsed_sidebar_block_icon = '.sidebar-collapsed-icon'
+
+      before do
+        resize_screen_sm
+      end
+
+      it 'confidentiality block expands then collapses sidebar' do
+        expect(page).to have_css(collapsed_sidebar_selector)
+
+        page.within(confidentiality_sidebar_block) do
+          find(collapsed_sidebar_block_icon).click
+        end
+
+        expect(page).to have_css(expanded_sidebar_selector)
+
+        page.within(confidentiality_sidebar_block) do
+          page.find('button', text: 'Cancel').click
+        end
+
+        expect(page).to have_css(collapsed_sidebar_selector)
+      end
+
+      it 'lock block expands then collapses sidebar' do
+        expect(page).to have_css(collapsed_sidebar_selector)
+
+        page.within(lock_sidebar_block) do
+          find(collapsed_sidebar_block_icon).click
+        end
+
+        expect(page).to have_css(expanded_sidebar_selector)
+
+        page.within(lock_sidebar_block) do
+          page.find('button', text: 'Cancel').click
+        end
+
+        expect(page).to have_css(collapsed_sidebar_selector)
+      end
+    end
   end
 
   context 'as a guest' do
diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb
index 171e061e60e84afe506ff8bb19d9cf882e11eac1..e8eb0d17ca481ab5a89a99a1f6aa2e6469bdfed7 100644
--- a/spec/features/profiles/account_spec.rb
+++ b/spec/features/profiles/account_spec.rb
@@ -43,14 +43,14 @@ feature 'Profile > Account' do
         update_username(new_username)
         visit new_project_path
         expect(current_path).to eq(new_project_path)
-        expect(find('.breadcrumbs-sub-title')).to have_content(project.path)
+        expect(find('.breadcrumbs-sub-title')).to have_content('Details')
       end
 
       scenario 'the old project path redirects to the new path' do
         update_username(new_username)
         visit old_project_path
         expect(current_path).to eq(new_project_path)
-        expect(find('.breadcrumbs-sub-title')).to have_content(project.path)
+        expect(find('.breadcrumbs-sub-title')).to have_content('Details')
       end
     end
   end
diff --git a/spec/fixtures/api/schemas/public_api/v4/project/export_status.json b/spec/fixtures/api/schemas/public_api/v4/project/export_status.json
index d24a6f93f4bb6a95d4158ae3458b2ac3b7f91252..81c8815caf6f10d7b27ac2829e2b67f5196edb9b 100644
--- a/spec/fixtures/api/schemas/public_api/v4/project/export_status.json
+++ b/spec/fixtures/api/schemas/public_api/v4/project/export_status.json
@@ -1,7 +1,9 @@
 {
   "type": "object",
   "allOf": [
-    { "$ref": "identity.json" },
+    {
+      "$ref": "identity.json"
+    },
     {
       "required": [
         "export_status"
@@ -9,7 +11,12 @@
       "properties": {
         "export_status": {
           "type": "string",
-          "enum": ["none", "started", "finished"]
+          "enum": [
+            "none",
+            "started",
+            "finished",
+            "after_export_action"
+          ]
         }
       }
     }
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index baf927a9acc9af87451442c714a1ef55654b6feb..b77114a8152edd4026c3c5104b7229ac9564a0cc 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -50,6 +50,11 @@ describe PageLayoutHelper do
       allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
       expect(helper.favicon).to eq 'favicon-blue.ico'
     end
+
+    it 'has yellow favicon for canary' do
+      stub_env('CANARY', 'true')
+      expect(helper.favicon).to eq 'favicon-yellow.ico'
+    end
   end
 
   describe 'page_image' do
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 5477581c1b961ff7a3e3a49ecffd3d0318158448..3d7ccf432be91adfa50d71a50e886cbdc099dd70 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -35,14 +35,14 @@ describe('Api', () => {
   });
 
   describe('group', () => {
-    it('fetches a group', (done) => {
+    it('fetches a group', done => {
       const groupId = '123456';
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`;
       mock.onGet(expectedUrl).reply(200, {
         name: 'test',
       });
 
-      Api.group(groupId, (response) => {
+      Api.group(groupId, response => {
         expect(response.name).toBe('test');
         done();
       });
@@ -50,15 +50,17 @@ describe('Api', () => {
   });
 
   describe('groups', () => {
-    it('fetches groups', (done) => {
+    it('fetches groups', done => {
       const query = 'dummy query';
       const options = { unused: 'option' };
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`;
-      mock.onGet(expectedUrl).reply(200, [{
-        name: 'test',
-      }]);
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          name: 'test',
+        },
+      ]);
 
-      Api.groups(query, options, (response) => {
+      Api.groups(query, options, response => {
         expect(response.length).toBe(1);
         expect(response[0].name).toBe('test');
         done();
@@ -67,14 +69,16 @@ describe('Api', () => {
   });
 
   describe('namespaces', () => {
-    it('fetches namespaces', (done) => {
+    it('fetches namespaces', done => {
       const query = 'dummy query';
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`;
-      mock.onGet(expectedUrl).reply(200, [{
-        name: 'test',
-      }]);
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          name: 'test',
+        },
+      ]);
 
-      Api.namespaces(query, (response) => {
+      Api.namespaces(query, response => {
         expect(response.length).toBe(1);
         expect(response[0].name).toBe('test');
         done();
@@ -83,31 +87,35 @@ describe('Api', () => {
   });
 
   describe('projects', () => {
-    it('fetches projects with membership when logged in', (done) => {
+    it('fetches projects with membership when logged in', done => {
       const query = 'dummy query';
       const options = { unused: 'option' };
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
       window.gon.current_user_id = 1;
-      mock.onGet(expectedUrl).reply(200, [{
-        name: 'test',
-      }]);
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          name: 'test',
+        },
+      ]);
 
-      Api.projects(query, options, (response) => {
+      Api.projects(query, options, response => {
         expect(response.length).toBe(1);
         expect(response[0].name).toBe('test');
         done();
       });
     });
 
-    it('fetches projects without membership when not logged in', (done) => {
+    it('fetches projects without membership when not logged in', done => {
       const query = 'dummy query';
       const options = { unused: 'option' };
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
-      mock.onGet(expectedUrl).reply(200, [{
-        name: 'test',
-      }]);
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          name: 'test',
+        },
+      ]);
 
-      Api.projects(query, options, (response) => {
+      Api.projects(query, options, response => {
         expect(response.length).toBe(1);
         expect(response[0].name).toBe('test');
         done();
@@ -115,8 +123,65 @@ describe('Api', () => {
     });
   });
 
+  describe('mergerequest', () => {
+    it('fetches a merge request', done => {
+      const projectPath = 'abc';
+      const mergeRequestId = '123456';
+      const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}`;
+      mock.onGet(expectedUrl).reply(200, {
+        title: 'test',
+      });
+
+      Api.mergeRequest(projectPath, mergeRequestId)
+        .then(({ data }) => {
+          expect(data.title).toBe('test');
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+  });
+
+  describe('mergerequest changes', () => {
+    it('fetches the changes of a merge request', done => {
+      const projectPath = 'abc';
+      const mergeRequestId = '123456';
+      const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/changes`;
+      mock.onGet(expectedUrl).reply(200, {
+        title: 'test',
+      });
+
+      Api.mergeRequestChanges(projectPath, mergeRequestId)
+        .then(({ data }) => {
+          expect(data.title).toBe('test');
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+  });
+
+  describe('mergerequest versions', () => {
+    it('fetches the versions of a merge request', done => {
+      const projectPath = 'abc';
+      const mergeRequestId = '123456';
+      const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/versions`;
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          id: 123,
+        },
+      ]);
+
+      Api.mergeRequestVersions(projectPath, mergeRequestId)
+        .then(({ data }) => {
+          expect(data.length).toBe(1);
+          expect(data[0].id).toBe(123);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+  });
+
   describe('newLabel', () => {
-    it('creates a new label', (done) => {
+    it('creates a new label', done => {
       const namespace = 'some namespace';
       const project = 'some project';
       const labelData = { some: 'data' };
@@ -124,36 +189,42 @@ describe('Api', () => {
       const expectedData = {
         label: labelData,
       };
-      mock.onPost(expectedUrl).reply((config) => {
+      mock.onPost(expectedUrl).reply(config => {
         expect(config.data).toBe(JSON.stringify(expectedData));
 
-        return [200, {
-          name: 'test',
-        }];
+        return [
+          200,
+          {
+            name: 'test',
+          },
+        ];
       });
 
-      Api.newLabel(namespace, project, labelData, (response) => {
+      Api.newLabel(namespace, project, labelData, response => {
         expect(response.name).toBe('test');
         done();
       });
     });
 
-    it('creates a group label', (done) => {
+    it('creates a group label', done => {
       const namespace = 'group/subgroup';
       const labelData = { some: 'data' };
       const expectedUrl = `${dummyUrlRoot}/groups/${namespace}/-/labels`;
       const expectedData = {
         label: labelData,
       };
-      mock.onPost(expectedUrl).reply((config) => {
+      mock.onPost(expectedUrl).reply(config => {
         expect(config.data).toBe(JSON.stringify(expectedData));
 
-        return [200, {
-          name: 'test',
-        }];
+        return [
+          200,
+          {
+            name: 'test',
+          },
+        ];
       });
 
-      Api.newLabel(namespace, undefined, labelData, (response) => {
+      Api.newLabel(namespace, undefined, labelData, response => {
         expect(response.name).toBe('test');
         done();
       });
@@ -161,15 +232,17 @@ describe('Api', () => {
   });
 
   describe('groupProjects', () => {
-    it('fetches group projects', (done) => {
+    it('fetches group projects', done => {
       const groupId = '123456';
       const query = 'dummy query';
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
-      mock.onGet(expectedUrl).reply(200, [{
-        name: 'test',
-      }]);
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          name: 'test',
+        },
+      ]);
 
-      Api.groupProjects(groupId, query, (response) => {
+      Api.groupProjects(groupId, query, response => {
         expect(response.length).toBe(1);
         expect(response[0].name).toBe('test');
         done();
@@ -178,13 +251,13 @@ describe('Api', () => {
   });
 
   describe('licenseText', () => {
-    it('fetches a license text', (done) => {
+    it('fetches a license text', done => {
       const licenseKey = "driver's license";
       const data = { unused: 'option' };
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`;
       mock.onGet(expectedUrl).reply(200, 'test');
 
-      Api.licenseText(licenseKey, data, (response) => {
+      Api.licenseText(licenseKey, data, response => {
         expect(response).toBe('test');
         done();
       });
@@ -192,12 +265,12 @@ describe('Api', () => {
   });
 
   describe('gitignoreText', () => {
-    it('fetches a gitignore text', (done) => {
+    it('fetches a gitignore text', done => {
       const gitignoreKey = 'ignore git';
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`;
       mock.onGet(expectedUrl).reply(200, 'test');
 
-      Api.gitignoreText(gitignoreKey, (response) => {
+      Api.gitignoreText(gitignoreKey, response => {
         expect(response).toBe('test');
         done();
       });
@@ -205,12 +278,12 @@ describe('Api', () => {
   });
 
   describe('gitlabCiYml', () => {
-    it('fetches a .gitlab-ci.yml', (done) => {
+    it('fetches a .gitlab-ci.yml', done => {
       const gitlabCiYmlKey = 'Y CI ML';
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`;
       mock.onGet(expectedUrl).reply(200, 'test');
 
-      Api.gitlabCiYml(gitlabCiYmlKey, (response) => {
+      Api.gitlabCiYml(gitlabCiYmlKey, response => {
         expect(response).toBe('test');
         done();
       });
@@ -218,12 +291,12 @@ describe('Api', () => {
   });
 
   describe('dockerfileYml', () => {
-    it('fetches a Dockerfile', (done) => {
+    it('fetches a Dockerfile', done => {
       const dockerfileYmlKey = 'a giant whale';
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`;
       mock.onGet(expectedUrl).reply(200, 'test');
 
-      Api.dockerfileYml(dockerfileYmlKey, (response) => {
+      Api.dockerfileYml(dockerfileYmlKey, response => {
         expect(response).toBe('test');
         done();
       });
@@ -231,12 +304,14 @@ describe('Api', () => {
   });
 
   describe('issueTemplate', () => {
-    it('fetches an issue template', (done) => {
+    it('fetches an issue template', done => {
       const namespace = 'some namespace';
       const project = 'some project';
       const templateKey = ' template #%?.key ';
       const templateType = 'template type';
-      const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(templateKey)}`;
+      const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(
+        templateKey,
+      )}`;
       mock.onGet(expectedUrl).reply(200, 'test');
 
       Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => {
@@ -247,13 +322,15 @@ describe('Api', () => {
   });
 
   describe('users', () => {
-    it('fetches users', (done) => {
+    it('fetches users', done => {
       const query = 'dummy query';
       const options = { unused: 'option' };
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`;
-      mock.onGet(expectedUrl).reply(200, [{
-        name: 'test',
-      }]);
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          name: 'test',
+        },
+      ]);
 
       Api.users(query, options)
         .then(({ data }) => {
diff --git a/spec/javascripts/boards/mock_data.js b/spec/javascripts/boards/mock_data.js
index 0671facb285611b7827dc4de7f1806c11c4ae3be..81f1a97112fc8b7603f6523018de9eb7bf480853 100644
--- a/spec/javascripts/boards/mock_data.js
+++ b/spec/javascripts/boards/mock_data.js
@@ -1,7 +1,4 @@
 /* global BoardService */
-/* eslint-disable comma-dangle, no-unused-vars, quote-props */
-import _ from 'underscore';
-
 export const listObj = {
   id: 300,
   position: 0,
@@ -11,8 +8,8 @@ export const listObj = {
     id: 5000,
     title: 'Testing',
     color: 'red',
-    description: 'testing;'
-  }
+    description: 'testing;',
+  },
 };
 
 export const listObjDuplicate = {
@@ -24,35 +21,37 @@ export const listObjDuplicate = {
     id: listObj.label.id,
     title: 'Testing',
     color: 'red',
-    description: 'testing;'
-  }
+    description: 'testing;',
+  },
 };
 
 export const BoardsMockData = {
-  'GET': {
+  GET: {
     '/test/-/boards/1/lists/300/issues?id=300&page=1&=': {
-      issues: [{
-        title: 'Testing',
-        id: 1,
-        iid: 1,
-        confidential: false,
-        labels: [],
-        assignees: [],
-      }],
-    }
+      issues: [
+        {
+          title: 'Testing',
+          id: 1,
+          iid: 1,
+          confidential: false,
+          labels: [],
+          assignees: [],
+        },
+      ],
+    },
+  },
+  POST: {
+    '/test/-/boards/1/lists': listObj,
   },
-  'POST': {
-    '/test/-/boards/1/lists': listObj
+  PUT: {
+    '/test/issue-boards/board/1/lists{/id}': {},
   },
-  'PUT': {
-    '/test/issue-boards/board/1/lists{/id}': {}
+  DELETE: {
+    '/test/issue-boards/board/1/lists{/id}': {},
   },
-  'DELETE': {
-    '/test/issue-boards/board/1/lists{/id}': {}
-  }
 };
 
-export const boardsMockInterceptor = (config) => {
+export const boardsMockInterceptor = config => {
   const body = BoardsMockData[config.method.toUpperCase()][config.url];
   return [200, body];
 };
diff --git a/spec/javascripts/droplab/constants_spec.js b/spec/javascripts/droplab/constants_spec.js
index b9d28db74cc5b6ac8f80b2f864b1230b73ee992a..23b69defec617eb4cb8986ce8658fa1271324c12 100644
--- a/spec/javascripts/droplab/constants_spec.js
+++ b/spec/javascripts/droplab/constants_spec.js
@@ -1,39 +1,37 @@
-/* eslint-disable */
-
 import * as constants from '~/droplab/constants';
 
-describe('constants', function () {
-  describe('DATA_TRIGGER', function () {
+describe('constants', function() {
+  describe('DATA_TRIGGER', function() {
     it('should be `data-dropdown-trigger`', function() {
       expect(constants.DATA_TRIGGER).toBe('data-dropdown-trigger');
     });
   });
 
-  describe('DATA_DROPDOWN', function () {
+  describe('DATA_DROPDOWN', function() {
     it('should be `data-dropdown`', function() {
       expect(constants.DATA_DROPDOWN).toBe('data-dropdown');
     });
   });
 
-  describe('SELECTED_CLASS', function () {
+  describe('SELECTED_CLASS', function() {
     it('should be `droplab-item-selected`', function() {
       expect(constants.SELECTED_CLASS).toBe('droplab-item-selected');
     });
   });
 
-  describe('ACTIVE_CLASS', function () {
+  describe('ACTIVE_CLASS', function() {
     it('should be `droplab-item-active`', function() {
       expect(constants.ACTIVE_CLASS).toBe('droplab-item-active');
     });
   });
 
-  describe('TEMPLATE_REGEX', function () {
+  describe('TEMPLATE_REGEX', function() {
     it('should be a handlebars templating syntax regex', function() {
       expect(constants.TEMPLATE_REGEX).toEqual(/\{\{(.+?)\}\}/g);
     });
   });
 
-  describe('IGNORE_CLASS', function () {
+  describe('IGNORE_CLASS', function() {
     it('should be `droplab-item-ignore`', function() {
       expect(constants.IGNORE_CLASS).toBe('droplab-item-ignore');
     });
diff --git a/spec/javascripts/fixtures/projects.rb b/spec/javascripts/fixtures/projects.rb
index b344b389241d5c6e2250178fac59c179307fb8ef..e8865b04874b5132dd1e5e433b43664c3f6b7489 100644
--- a/spec/javascripts/fixtures/projects.rb
+++ b/spec/javascripts/fixtures/projects.rb
@@ -17,8 +17,6 @@ describe 'Projects (JavaScript fixtures)', type: :controller do
   end
 
   before do
-    # EE-specific start
-    # EE specific end
     project.add_master(admin)
     sign_in(admin)
   end
diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js
index dc0a5bc275cea95ac2a65b43d95322a980c06912..1cb20a1e7ff38a5a56ce2b0714e4bcbb82c932e5 100644
--- a/spec/javascripts/gfm_auto_complete_spec.js
+++ b/spec/javascripts/gfm_auto_complete_spec.js
@@ -81,13 +81,21 @@ describe('GfmAutoComplete', function () {
     });
 
     it('should quote if value contains any non-alphanumeric characters', () => {
-      expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label-20"');
+      expect(beforeInsert(atwhoInstance, '~label-20')).toBe('~"label\\-20"');
       expect(beforeInsert(atwhoInstance, '~label 20')).toBe('~"label 20"');
     });
 
     it('should quote integer labels', () => {
       expect(beforeInsert(atwhoInstance, '~1234')).toBe('~"1234"');
     });
+
+    it('should escape Markdown emphasis characters, except in the first character', () => {
+      expect(beforeInsert(atwhoInstance, '@_group')).toEqual('@\\_group');
+      expect(beforeInsert(atwhoInstance, '~_bug')).toEqual('~\\_bug');
+      expect(beforeInsert(atwhoInstance, '~a `bug`')).toEqual('~"a \\`bug\\`"');
+      expect(beforeInsert(atwhoInstance, '~a ~bug')).toEqual('~"a \\~bug"');
+      expect(beforeInsert(atwhoInstance, '~a **bug')).toEqual('~"a \\*\\*bug"');
+    });
   });
 
   describe('DefaultOptions.matcher', function () {
diff --git a/spec/javascripts/helpers/vuex_action_helper.js b/spec/javascripts/helpers/vuex_action_helper.js
index 2d386fe1da568811cd17947d1a01e6aed1f6727a..83f29d1b0c266a6c21d975bc37b84e87001ea1dc 100644
--- a/spec/javascripts/helpers/vuex_action_helper.js
+++ b/spec/javascripts/helpers/vuex_action_helper.js
@@ -1,37 +1,71 @@
-/* eslint-disable */
-
 /**
- * helper for testing action with expected mutations
+ * helper for testing action with expected mutations inspired in
  * https://vuex.vuejs.org/en/testing.html
+ *
+ * @example
+ * testAction(
+ *   actions.actionName, // action
+ *   { }, // mocked response
+ *   state, // state
+ *   [
+ *    { type: types.MUTATION}
+ *    { type: types.MUTATION_1, payload: {}}
+ *   ], // mutations
+ *   [
+ *    { type: 'actionName', payload: {}},
+ *    { type: 'actionName1', payload: {}}
+ *   ] //actions
+ *   done,
+ * );
  */
-export default (action, payload, state, expectedMutations, done) => {
-  let count = 0;
+export default (action, payload, state, expectedMutations, expectedActions, done) => {
+  let mutationsCount = 0;
+  let actionsCount = 0;
 
   // mock commit
-  const commit = (type, payload) => {
-    const mutation = expectedMutations[count];
-
-    try {
-      expect(mutation.type).to.equal(type);
-      if (payload) {
-        expect(mutation.payload).to.deep.equal(payload);
-      }
-    } catch (error) {
-      done(error);
+  const commit = (type, mutationPayload) => {
+    const mutation = expectedMutations[mutationsCount];
+
+    expect(mutation.type).toEqual(type);
+
+    if (mutation.payload) {
+      expect(mutation.payload).toEqual(mutationPayload);
     }
 
-    count++;
-    if (count >= expectedMutations.length) {
+    mutationsCount += 1;
+    if (mutationsCount >= expectedMutations.length) {
+      done();
+    }
+  };
+
+  // mock dispatch
+  const dispatch = (type, actionPayload) => {
+    const actionExpected = expectedActions[actionsCount];
+
+    expect(actionExpected.type).toEqual(type);
+
+    if (actionExpected.payload) {
+      expect(actionExpected.payload).toEqual(actionPayload);
+    }
+
+    actionsCount += 1;
+    if (actionsCount >= expectedActions.length) {
       done();
     }
   };
 
   // call the action with mocked store and arguments
-  action({ commit, state }, payload);
+  action({ commit, state, dispatch }, payload);
 
   // check if no mutations should have been dispatched
   if (expectedMutations.length === 0) {
-    expect(count).to.equal(0);
+    expect(mutationsCount).toEqual(0);
+    done();
+  }
+
+  // check if no mutations should have been dispatched
+  if (expectedActions.length === 0) {
+    expect(actionsCount).toEqual(0);
     done();
   }
 };
diff --git a/spec/javascripts/ide/components/changed_file_icon_spec.js b/spec/javascripts/ide/components/changed_file_icon_spec.js
index 987aea7befcea2401a63174aa6ae7259a5b0201b..541864e912ecc08d4fe6bcac859468d970512482 100644
--- a/spec/javascripts/ide/components/changed_file_icon_spec.js
+++ b/spec/javascripts/ide/components/changed_file_icon_spec.js
@@ -11,6 +11,7 @@ describe('IDE changed file icon', () => {
     vm = createComponent(component, {
       file: {
         tempFile: false,
+        changed: true,
       },
     });
   });
@@ -20,7 +21,7 @@ describe('IDE changed file icon', () => {
   });
 
   describe('changedIcon', () => {
-    it('equals file-modified when not a temp file', () => {
+    it('equals file-modified when not a temp file and has changes', () => {
       expect(vm.changedIcon).toBe('file-modified');
     });
 
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index ae657e8c881a8b796bf9b0e11de0667e11f9b4ee..9d3fa1280f4b17019e5c165becef4a92d391400b 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -89,6 +89,20 @@ describe('RepoEditor', () => {
         done();
       });
     });
+
+    it('calls createDiffInstance when viewer is a merge request diff', done => {
+      vm.$store.state.viewer = 'mrdiff';
+
+      spyOn(vm.editor, 'createDiffInstance');
+
+      vm.createEditorInstance();
+
+      vm.$nextTick(() => {
+        expect(vm.editor.createDiffInstance).toHaveBeenCalled();
+
+        done();
+      });
+    });
   });
 
   describe('setupEditor', () => {
@@ -134,4 +148,48 @@ describe('RepoEditor', () => {
       });
     });
   });
+
+  describe('setup editor for merge request viewing', () => {
+    beforeEach(done => {
+      // Resetting as the main test setup has already done it
+      vm.$destroy();
+      resetStore(vm.$store);
+      Editor.editorInstance.modelManager.dispose();
+
+      const f = {
+        ...file(),
+        active: true,
+        tempFile: true,
+        html: 'testing',
+        mrChange: { diff: 'ABC' },
+        baseRaw: 'testing',
+        content: 'test',
+      };
+      const RepoEditor = Vue.extend(repoEditor);
+      vm = createComponentWithStore(RepoEditor, store, {
+        file: f,
+      });
+
+      vm.$store.state.openFiles.push(f);
+      vm.$store.state.entries[f.path] = f;
+
+      vm.$store.state.viewer = 'mrdiff';
+
+      vm.monaco = true;
+
+      vm.$mount();
+
+      monacoLoader(['vs/editor/editor.main'], () => {
+        setTimeout(done, 0);
+      });
+    });
+
+    it('attaches merge request model to editor when merge request diff', () => {
+      spyOn(vm.editor, 'attachMergeRequestModel').and.callThrough();
+
+      vm.setupEditor();
+
+      expect(vm.editor.attachMergeRequestModel).toHaveBeenCalledWith(vm.model);
+    });
+  });
 });
diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js
index 1cd2e362f508b6a571e37d94d94bd867c0903a5b..cb785ba2cd3c4eeded0965435da48538cc102835 100644
--- a/spec/javascripts/ide/components/repo_tabs_spec.js
+++ b/spec/javascripts/ide/components/repo_tabs_spec.js
@@ -18,6 +18,7 @@ describe('RepoTabs', () => {
       viewer: 'editor',
       hasChanges: false,
       activeFile: file('activeFile'),
+      hasMergeRequest: false,
     });
     openedFiles[0].active = true;
 
@@ -58,6 +59,7 @@ describe('RepoTabs', () => {
           viewer: 'editor',
           hasChanges: false,
           activeFile: file('activeFile'),
+          hasMergeRequest: false,
         },
         '#test-app',
       );
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
index f6979e32cb3dbe48b210737f1d2180715ed5d8fa..8fc2fccb64c8067a24dad9f02ef6f70f2bd9f75e 100644
--- a/spec/javascripts/ide/lib/common/model_spec.js
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -11,7 +11,10 @@ describe('Multi-file editor library model', () => {
     spyOn(eventHub, '$on').and.callThrough();
 
     monacoLoader(['vs/editor/editor.main'], () => {
-      model = new Model(monaco, file('path'));
+      const f = file('path');
+      f.mrChange = { diff: 'ABC' };
+      f.baseRaw = 'test';
+      model = new Model(monaco, f);
 
       done();
     });
@@ -21,9 +24,10 @@ describe('Multi-file editor library model', () => {
     model.dispose();
   });
 
-  it('creates original model & new model', () => {
+  it('creates original model & base model & new model', () => {
     expect(model.originalModel).not.toBeNull();
     expect(model.model).not.toBeNull();
+    expect(model.baseModel).not.toBeNull();
   });
 
   it('adds eventHub listener', () => {
@@ -51,6 +55,12 @@ describe('Multi-file editor library model', () => {
     });
   });
 
+  describe('getBaseModel', () => {
+    it('returns base model', () => {
+      expect(model.getBaseModel()).toBe(model.baseModel);
+    });
+  });
+
   describe('setValue', () => {
     it('updates models value', () => {
       model.setValue('testing 123');
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
index 2ccd87de1a76f433c25f1f4a01030839ab6c42ce..ec56ebc03412a7a43247fb98e2664ae0847ebffa 100644
--- a/spec/javascripts/ide/lib/editor_spec.js
+++ b/spec/javascripts/ide/lib/editor_spec.js
@@ -143,6 +143,31 @@ describe('Multi-file editor library', () => {
     });
   });
 
+  describe('attachMergeRequestModel', () => {
+    let model;
+
+    beforeEach(() => {
+      instance.createDiffInstance(document.createElement('div'));
+
+      const f = file();
+      f.mrChanges = { diff: 'ABC' };
+      f.baseRaw = 'testing';
+
+      model = instance.createModel(f);
+    });
+
+    it('sets original & modified', () => {
+      spyOn(instance.instance, 'setModel');
+
+      instance.attachMergeRequestModel(model);
+
+      expect(instance.instance.setModel).toHaveBeenCalledWith({
+        original: model.getBaseModel(),
+        modified: model.getModel(),
+      });
+    });
+  });
+
   describe('clearEditor', () => {
     it('resets the editor model', () => {
       instance.createInstance(document.createElement('div'));
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index eb8933b2b3f9db6e4ea336a95f2d137a10f2c423..479ed7ce49ee6e170b80daf185b7204554b30e27 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -5,7 +5,7 @@ import router from '~/ide/ide_router';
 import eventHub from '~/ide/eventhub';
 import { file, resetStore } from '../../helpers';
 
-describe('Multi-file store file actions', () => {
+describe('IDE store file actions', () => {
   beforeEach(() => {
     spyOn(router, 'push');
   });
@@ -205,7 +205,7 @@ describe('Multi-file store file actions', () => {
 
     it('calls the service', done => {
       store
-        .dispatch('getFileData', localFile)
+        .dispatch('getFileData', { path: localFile.path })
         .then(() => {
           expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
 
@@ -216,7 +216,7 @@ describe('Multi-file store file actions', () => {
 
     it('sets the file data', done => {
       store
-        .dispatch('getFileData', localFile)
+        .dispatch('getFileData', { path: localFile.path })
         .then(() => {
           expect(localFile.blamePath).toBe('blame_path');
 
@@ -227,7 +227,7 @@ describe('Multi-file store file actions', () => {
 
     it('sets document title', done => {
       store
-        .dispatch('getFileData', localFile)
+        .dispatch('getFileData', { path: localFile.path })
         .then(() => {
           expect(document.title).toBe('testing getFileData');
 
@@ -238,7 +238,7 @@ describe('Multi-file store file actions', () => {
 
     it('sets the file as active', done => {
       store
-        .dispatch('getFileData', localFile)
+        .dispatch('getFileData', { path: localFile.path })
         .then(() => {
           expect(localFile.active).toBeTruthy();
 
@@ -247,9 +247,20 @@ describe('Multi-file store file actions', () => {
         .catch(done.fail);
     });
 
+    it('sets the file not as active if we pass makeFileActive false', done => {
+      store
+        .dispatch('getFileData', { path: localFile.path, makeFileActive: false })
+        .then(() => {
+          expect(localFile.active).toBeFalsy();
+
+          done();
+        })
+        .catch(done.fail);
+    });
+
     it('adds the file to open files', done => {
       store
-        .dispatch('getFileData', localFile)
+        .dispatch('getFileData', { path: localFile.path })
         .then(() => {
           expect(store.state.openFiles.length).toBe(1);
           expect(store.state.openFiles[0].name).toBe(localFile.name);
@@ -272,7 +283,7 @@ describe('Multi-file store file actions', () => {
 
     it('calls getRawFileData service method', done => {
       store
-        .dispatch('getRawFileData', tmpFile)
+        .dispatch('getRawFileData', { path: tmpFile.path })
         .then(() => {
           expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
 
@@ -283,7 +294,7 @@ describe('Multi-file store file actions', () => {
 
     it('updates file raw data', done => {
       store
-        .dispatch('getRawFileData', tmpFile)
+        .dispatch('getRawFileData', { path: tmpFile.path })
         .then(() => {
           expect(tmpFile.raw).toBe('raw');
 
@@ -291,6 +302,22 @@ describe('Multi-file store file actions', () => {
         })
         .catch(done.fail);
     });
+
+    it('calls also getBaseRawFileData service method', done => {
+      spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
+
+      tmpFile.mrChange = { new_file: false };
+
+      store
+        .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' })
+        .then(() => {
+          expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
+          expect(tmpFile.baseRaw).toBe('baseraw');
+
+          done();
+        })
+        .catch(done.fail);
+    });
   });
 
   describe('changeFileContent', () => {
diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b4ec4a0b1737fe36bb161fe91b176807a34fe0d8
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js
@@ -0,0 +1,110 @@
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import { resetStore } from '../../helpers';
+
+describe('IDE store merge request actions', () => {
+  beforeEach(() => {
+    store.state.projects.abcproject = {
+      mergeRequests: {},
+    };
+  });
+
+  afterEach(() => {
+    resetStore(store);
+  });
+
+  describe('getMergeRequestData', () => {
+    beforeEach(() => {
+      spyOn(service, 'getProjectMergeRequestData').and.returnValue(
+        Promise.resolve({ data: { title: 'mergerequest' } }),
+      );
+    });
+
+    it('calls getProjectMergeRequestData service method', done => {
+      store
+        .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+        .then(() => {
+          expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1);
+
+          done();
+        })
+        .catch(done.fail);
+    });
+
+    it('sets the Merge Request Object', done => {
+      store
+        .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+        .then(() => {
+          expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest');
+          expect(store.state.currentMergeRequestId).toBe(1);
+
+          done();
+        })
+        .catch(done.fail);
+    });
+  });
+
+  describe('getMergeRequestChanges', () => {
+    beforeEach(() => {
+      spyOn(service, 'getProjectMergeRequestChanges').and.returnValue(
+        Promise.resolve({ data: { title: 'mergerequest' } }),
+      );
+
+      store.state.projects.abcproject.mergeRequests['1'] = { changes: [] };
+    });
+
+    it('calls getProjectMergeRequestChanges service method', done => {
+      store
+        .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+        .then(() => {
+          expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1);
+
+          done();
+        })
+        .catch(done.fail);
+    });
+
+    it('sets the Merge Request Changes Object', done => {
+      store
+        .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+        .then(() => {
+          expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe(
+            'mergerequest',
+          );
+          done();
+        })
+        .catch(done.fail);
+    });
+  });
+
+  describe('getMergeRequestVersions', () => {
+    beforeEach(() => {
+      spyOn(service, 'getProjectMergeRequestVersions').and.returnValue(
+        Promise.resolve({ data: [{ id: 789 }] }),
+      );
+
+      store.state.projects.abcproject.mergeRequests['1'] = { versions: [] };
+    });
+
+    it('calls getProjectMergeRequestVersions service method', done => {
+      store
+        .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+        .then(() => {
+          expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1);
+
+          done();
+        })
+        .catch(done.fail);
+    });
+
+    it('sets the Merge Request Versions Object', done => {
+      store
+        .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+        .then(() => {
+          expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1);
+          done();
+        })
+        .catch(done.fail);
+    });
+  });
+});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index 381f038067b1c4aa96012aafb1a6b49bf37dd3c7..e0ef57a39666d59824d922caf1beb4707b2f0cf4 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -68,9 +68,7 @@ describe('Multi-file store tree actions', () => {
           expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
           expect(projectTree.tree[1].type).toBe('blob');
           expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
-          expect(projectTree.tree[0].tree[0].tree[0].name).toBe(
-            'fileinsubfolder.js',
-          );
+          expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js');
 
           done();
         })
@@ -132,9 +130,7 @@ describe('Multi-file store tree actions', () => {
       store
         .dispatch('getLastCommitData', projectTree)
         .then(() => {
-          expect(service.getTreeLastCommit).toHaveBeenCalledWith(
-            'lastcommitpath',
-          );
+          expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
 
           done();
         })
@@ -160,9 +156,7 @@ describe('Multi-file store tree actions', () => {
         .dispatch('getLastCommitData', projectTree)
         .then(Vue.nextTick)
         .then(() => {
-          expect(projectTree.tree[0].lastCommit.message).not.toBe(
-            'commit message',
-          );
+          expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message');
 
           done();
         })
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index a613f3a21cc8f387f058b5ee6f38d74c7edd2f58..33733b97dff1b72f7949bcc6662edb8a5cc9d097 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -2,7 +2,7 @@ import * as getters from '~/ide/stores/getters';
 import state from '~/ide/stores/state';
 import { file } from '../helpers';
 
-describe('Multi-file store getters', () => {
+describe('IDE store getters', () => {
   let localState;
 
   beforeEach(() => {
@@ -52,4 +52,24 @@ describe('Multi-file store getters', () => {
       expect(modifiedFiles[0].name).toBe('added');
     });
   });
+
+  describe('currentMergeRequest', () => {
+    it('returns Current Merge Request', () => {
+      localState.currentProjectId = 'abcproject';
+      localState.currentMergeRequestId = 1;
+      localState.projects.abcproject = {
+        mergeRequests: {
+          1: { mergeId: 1 },
+        },
+      };
+
+      expect(getters.currentMergeRequest(localState).mergeId).toBe(1);
+    });
+
+    it('returns null if no active Merge Request was found', () => {
+      localState.currentProjectId = 'otherproject';
+
+      expect(getters.currentMergeRequest(localState)).toBeNull();
+    });
+  });
 });
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index 4f9e00b8543612e79fe0fe62d13a6a24908e8591..88285ee409fb16a907fc8e0ad934d5b774b0ee43 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -2,7 +2,7 @@ import mutations from '~/ide/stores/mutations/file';
 import state from '~/ide/stores/state';
 import { file } from '../../helpers';
 
-describe('Multi-file store file mutations', () => {
+describe('IDE store file mutations', () => {
   let localState;
   let localFile;
 
@@ -77,6 +77,8 @@ describe('Multi-file store file mutations', () => {
       expect(localFile.rawPath).toBe('raw');
       expect(localFile.binary).toBeTruthy();
       expect(localFile.renderError).toBe('render_error');
+      expect(localFile.raw).toBeNull();
+      expect(localFile.baseRaw).toBeNull();
     });
   });
 
@@ -91,6 +93,17 @@ describe('Multi-file store file mutations', () => {
     });
   });
 
+  describe('SET_FILE_BASE_RAW_DATA', () => {
+    it('sets raw data from base branch', () => {
+      mutations.SET_FILE_BASE_RAW_DATA(localState, {
+        file: localFile,
+        baseRaw: 'testing',
+      });
+
+      expect(localFile.baseRaw).toBe('testing');
+    });
+  });
+
   describe('UPDATE_FILE_CONTENT', () => {
     beforeEach(() => {
       localFile.raw = 'test';
@@ -127,6 +140,17 @@ describe('Multi-file store file mutations', () => {
     });
   });
 
+  describe('SET_FILE_MERGE_REQUEST_CHANGE', () => {
+    it('sets file mr change', () => {
+      mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, {
+        file: localFile,
+        mrChange: { diff: 'ABC' },
+      });
+
+      expect(localFile.mrChange.diff).toBe('ABC');
+    });
+  });
+
   describe('DISCARD_FILE_CHANGES', () => {
     beforeEach(() => {
       localFile.content = 'test';
diff --git a/spec/javascripts/ide/stores/mutations/merge_request_spec.js b/spec/javascripts/ide/stores/mutations/merge_request_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f724bf464f583b042eb7d161083d17479c1e8e85
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations/merge_request_spec.js
@@ -0,0 +1,65 @@
+import mutations from '~/ide/stores/mutations/merge_request';
+import state from '~/ide/stores/state';
+
+describe('IDE store merge request mutations', () => {
+  let localState;
+
+  beforeEach(() => {
+    localState = state();
+    localState.projects = { abcproject: { mergeRequests: {} } };
+
+    mutations.SET_MERGE_REQUEST(localState, {
+      projectPath: 'abcproject',
+      mergeRequestId: 1,
+      mergeRequest: {
+        title: 'mr',
+      },
+    });
+  });
+
+  describe('SET_CURRENT_MERGE_REQUEST', () => {
+    it('sets current merge request', () => {
+      mutations.SET_CURRENT_MERGE_REQUEST(localState, 2);
+
+      expect(localState.currentMergeRequestId).toBe(2);
+    });
+  });
+
+  describe('SET_MERGE_REQUEST', () => {
+    it('setsmerge request data', () => {
+      const newMr = localState.projects.abcproject.mergeRequests[1];
+
+      expect(newMr.title).toBe('mr');
+      expect(newMr.active).toBeTruthy();
+    });
+  });
+
+  describe('SET_MERGE_REQUEST_CHANGES', () => {
+    it('sets merge request changes', () => {
+      mutations.SET_MERGE_REQUEST_CHANGES(localState, {
+        projectPath: 'abcproject',
+        mergeRequestId: 1,
+        changes: {
+          diff: 'abc',
+        },
+      });
+
+      const newMr = localState.projects.abcproject.mergeRequests[1];
+      expect(newMr.changes.diff).toBe('abc');
+    });
+  });
+
+  describe('SET_MERGE_REQUEST_VERSIONS', () => {
+    it('sets merge request versions', () => {
+      mutations.SET_MERGE_REQUEST_VERSIONS(localState, {
+        projectPath: 'abcproject',
+        mergeRequestId: 1,
+        versions: [{ id: 123 }],
+      });
+
+      const newMr = localState.projects.abcproject.mergeRequests[1];
+      expect(newMr.versions.length).toBe(1);
+      expect(newMr.versions[0].id).toBe(123);
+    });
+  });
+});
diff --git a/spec/javascripts/jobs/mock_data.js b/spec/javascripts/jobs/mock_data.js
index 43589d54be44cf5afa5632469f0f3cdedb6874a9..25ca8eb6c0b429155a8d45ca35e7f820a1e5eb5a 100644
--- a/spec/javascripts/jobs/mock_data.js
+++ b/spec/javascripts/jobs/mock_data.js
@@ -115,6 +115,10 @@ export default {
       commit_path: '/root/ci-mock/commit/c58647773a6b5faf066d4ad6ff2c9fbba5f180f6',
     },
   },
+  metadata: {
+    timeout_human_readable: '1m 40s',
+    timeout_source: 'runner',
+  },
   merge_request: {
     iid: 2,
     path: '/root/ci-mock/merge_requests/2',
diff --git a/spec/javascripts/jobs/sidebar_detail_row_spec.js b/spec/javascripts/jobs/sidebar_detail_row_spec.js
index 3ac65709c4a4a76143adbc92623b8a51ff6b7f6c..e6bfb0c4adcabcec0c916c194e0f630ca7b3e45d 100644
--- a/spec/javascripts/jobs/sidebar_detail_row_spec.js
+++ b/spec/javascripts/jobs/sidebar_detail_row_spec.js
@@ -37,4 +37,25 @@ describe('Sidebar detail row', () => {
       vm.$el.textContent.replace(/\s+/g, ' ').trim(),
     ).toEqual('this is the title: this is the value');
   });
+
+  describe('when helpUrl not provided', () => {
+    it('should not render help', () => {
+      expect(vm.$el.querySelector('.help-button')).toBeNull();
+    });
+  });
+
+  describe('when helpUrl provided', () => {
+    beforeEach(() => {
+      vm = new SidebarDetailRow({
+        propsData: {
+          helpUrl: 'help url',
+          value: 'foo',
+        },
+      }).$mount();
+    });
+
+    it('should render help', () => {
+      expect(vm.$el.querySelector('.help-button a').getAttribute('href')).toEqual('help url');
+    });
+  });
 });
diff --git a/spec/javascripts/jobs/sidebar_details_block_spec.js b/spec/javascripts/jobs/sidebar_details_block_spec.js
index 95532ef5382521663071e7f00a6259a14a0fa6c7..602dae514b1d69ba5f83a5d39181858742f992c1 100644
--- a/spec/javascripts/jobs/sidebar_details_block_spec.js
+++ b/spec/javascripts/jobs/sidebar_details_block_spec.js
@@ -96,6 +96,12 @@ describe('Sidebar details block', () => {
       ).toEqual('Runner: #1');
     });
 
+    it('should render timeout information', () => {
+      expect(
+        trimWhitespace(vm.$el.querySelector('.js-job-timeout')),
+      ).toEqual('Timeout: 1m 40s (from runner)');
+    });
+
     it('should render coverage', () => {
       expect(
         trimWhitespace(vm.$el.querySelector('.js-job-coverage')),
diff --git a/spec/javascripts/notes/components/noteable_discussion_spec.js b/spec/javascripts/notes/components/noteable_discussion_spec.js
index 19504e4f7c895172bb90e5ec50f6d5aad83a99a1..cda550760fe97d3b744ef4cff56c4251b9bcf232 100644
--- a/spec/javascripts/notes/components/noteable_discussion_spec.js
+++ b/spec/javascripts/notes/components/noteable_discussion_spec.js
@@ -25,26 +25,34 @@ describe('issue_discussion component', () => {
   });
 
   it('should render user avatar', () => {
-    expect(vm.$el.querySelector('.user-avatar-link')).toBeDefined();
+    expect(vm.$el.querySelector('.user-avatar-link')).not.toBeNull();
   });
 
   it('should render discussion header', () => {
-    expect(vm.$el.querySelector('.discussion-header')).toBeDefined();
+    expect(vm.$el.querySelector('.discussion-header')).not.toBeNull();
     expect(vm.$el.querySelector('.notes').children.length).toEqual(discussionMock.notes.length);
   });
 
   describe('actions', () => {
     it('should render reply button', () => {
-      expect(vm.$el.querySelector('.js-vue-discussion-reply').textContent.trim()).toEqual('Reply...');
+      expect(vm.$el.querySelector('.js-vue-discussion-reply').textContent.trim()).toEqual(
+        'Reply...',
+      );
     });
 
-    it('should toggle reply form', (done) => {
+    it('should toggle reply form', done => {
       vm.$el.querySelector('.js-vue-discussion-reply').click();
       Vue.nextTick(() => {
-        expect(vm.$refs.noteForm).toBeDefined();
+        expect(vm.$refs.noteForm).not.toBeNull();
         expect(vm.isReplying).toEqual(true);
         done();
       });
     });
+
+    it('does not render jump to discussion button', () => {
+      expect(
+        vm.$el.querySelector('*[data-original-title="Jump to next unresolved discussion"]'),
+      ).toBeNull();
+    });
   });
 });
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index 5be13ed0dfe83d31353ae20b9c29d755360e6ef7..2d88cee61f1660426503a196bf8bae5d79018974 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -1,4 +1,3 @@
-/* eslint-disable */
 export const notesDataMock = {
   discussionsPath: '/gitlab-org/gitlab-ce/issues/26/discussions.json',
   lastFetchedAt: 1501862675,
@@ -43,7 +42,8 @@ export const noteableDataMock = {
   milestone: null,
   milestone_id: null,
   moved_to_id: null,
-  preview_note_path: '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
+  preview_note_path:
+    '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
   project_id: 2,
   state: 'opened',
   time_estimate: 0,
@@ -60,465 +60,504 @@ export const individualNote = {
   expanded: true,
   id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
   individual_note: true,
-  notes: [{
-    id: 1390,
-    attachment: {
-      url: null,
-      filename: null,
-      image: false,
-    },
-    author: {
-      id: 1,
-      name: 'Root',
-      username: 'root',
-      state: 'active',
-      avatar_url: 'test',
-      path: '/root',
+  notes: [
+    {
+      id: 1390,
+      attachment: {
+        url: null,
+        filename: null,
+        image: false,
+      },
+      author: {
+        id: 1,
+        name: 'Root',
+        username: 'root',
+        state: 'active',
+        avatar_url: 'test',
+        path: '/root',
+      },
+      created_at: '2017-08-01T17: 09: 33.762Z',
+      updated_at: '2017-08-01T17: 09: 33.762Z',
+      system: false,
+      noteable_id: 98,
+      noteable_type: 'Issue',
+      type: null,
+      human_access: 'Owner',
+      note: 'sdfdsaf',
+      note_html: "<p dir='auto'>sdfdsaf</p>",
+      current_user: { can_edit: true },
+      discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+      emoji_awardable: true,
+      award_emoji: [
+        { name: 'baseball', user: { id: 1, name: 'Root', username: 'root' } },
+        { name: 'art', user: { id: 1, name: 'Root', username: 'root' } },
+      ],
+      toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji',
+      report_abuse_path:
+        '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1',
+      path: '/gitlab-org/gitlab-ce/notes/1390',
     },
-    created_at: '2017-08-01T17: 09: 33.762Z',
-    updated_at: '2017-08-01T17: 09: 33.762Z',
-    system: false,
-    noteable_id: 98,
-    noteable_type: 'Issue',
-    type: null,
-    human_access: 'Owner',
-    note: 'sdfdsaf',
-    note_html: '<p dir=\'auto\'>sdfdsaf</p>',
-    current_user: { can_edit: true },
-    discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
-    emoji_awardable: true,
-    award_emoji: [
-      { name: 'baseball', user: { id: 1, name: 'Root', username: 'root' } },
-      { name: 'art', user: { id: 1, name: 'Root', username: 'root' } },
-    ],
-    toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji',
-    report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390&user_id=1',
-    path: '/gitlab-org/gitlab-ce/notes/1390',
-  }],
+  ],
   reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
 };
 
 export const note = {
-  "id": 546,
-  "attachment": {
-    "url": null,
-    "filename": null,
-    "image": false
+  id: 546,
+  attachment: {
+    url: null,
+    filename: null,
+    image: false,
   },
-  "author": {
-    "id": 1,
-    "name": "Administrator",
-    "username": "root",
-    "state": "active",
-    "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-    "path": "/root"
+  author: {
+    id: 1,
+    name: 'Administrator',
+    username: 'root',
+    state: 'active',
+    avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+    path: '/root',
   },
-  "created_at": "2017-08-10T15:24:03.087Z",
-  "updated_at": "2017-08-10T15:24:03.087Z",
-  "system": false,
-  "noteable_id": 67,
-  "noteable_type": "Issue",
-  "noteable_iid": 7,
-  "type": null,
-  "human_access": "Owner",
-  "note": "Vel id placeat reprehenderit sit numquam.",
-  "note_html": "<p dir=\"auto\">Vel id placeat reprehenderit sit numquam.</p>",
-  "current_user": {
-    "can_edit": true
+  created_at: '2017-08-10T15:24:03.087Z',
+  updated_at: '2017-08-10T15:24:03.087Z',
+  system: false,
+  noteable_id: 67,
+  noteable_type: 'Issue',
+  noteable_iid: 7,
+  type: null,
+  human_access: 'Owner',
+  note: 'Vel id placeat reprehenderit sit numquam.',
+  note_html: '<p dir="auto">Vel id placeat reprehenderit sit numquam.</p>',
+  current_user: {
+    can_edit: true,
   },
-  "discussion_id": "d3842a451b7f3d9a5dfce329515127b2d29a4cd0",
-  "emoji_awardable": true,
-  "award_emoji": [{
-    "name": "baseball",
-    "user": {
-      "id": 1,
-      "name": "Administrator",
-      "username": "root"
-    }
-  }, {
-    "name": "bath_tone3",
-    "user": {
-      "id": 1,
-      "name": "Administrator",
-      "username": "root"
-    }
-  }],
-  "toggle_award_path": "/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji",
-  "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1",
-  "path": "/gitlab-org/gitlab-ce/notes/546"
-  }
+  discussion_id: 'd3842a451b7f3d9a5dfce329515127b2d29a4cd0',
+  emoji_awardable: true,
+  award_emoji: [
+    {
+      name: 'baseball',
+      user: {
+        id: 1,
+        name: 'Administrator',
+        username: 'root',
+      },
+    },
+    {
+      name: 'bath_tone3',
+      user: {
+        id: 1,
+        name: 'Administrator',
+        username: 'root',
+      },
+    },
+  ],
+  toggle_award_path: '/gitlab-org/gitlab-ce/notes/546/toggle_award_emoji',
+  report_abuse_path:
+    '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_546&user_id=1',
+  path: '/gitlab-org/gitlab-ce/notes/546',
+};
 
 export const discussionMock = {
   id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
   reply_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
   expanded: true,
-  notes: [{
-    id: 1395,
-    attachment: {
-      url: null,
-      filename: null,
-      image: false,
-    },
-    author: {
-      id: 1,
-      name: 'Root',
-      username: 'root',
-      state: 'active',
-      avatar_url: null,
-      path: '/root',
-    },
-    created_at: '2017-08-02T10:51:58.559Z',
-    updated_at: '2017-08-02T10:51:58.559Z',
-    system: false,
-    noteable_id: 98,
-    noteable_type: 'Issue',
-    type: 'DiscussionNote',
-    human_access: 'Owner',
-    note: 'THIS IS A DICUSSSION!',
-    note_html: '<p dir=\'auto\'>THIS IS A DICUSSSION!</p>',
-    current_user: {
-      can_edit: true,
-    },
-    discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
-    emoji_awardable: true,
-    award_emoji: [],
-    toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji',
-    report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1',
-    path: '/gitlab-org/gitlab-ce/notes/1395',
-  }, {
-    id: 1396,
-    attachment: {
-      url: null,
-      filename: null,
-      image: false,
-    },
-    author: {
-      id: 1,
-      name: 'Root',
-      username: 'root',
-      state: 'active',
-      avatar_url: null,
-      path: '/root',
-    },
-    created_at: '2017-08-02T10:56:50.980Z',
-    updated_at: '2017-08-03T14:19:35.691Z',
-    system: false,
-    noteable_id: 98,
-    noteable_type: 'Issue',
-    type: 'DiscussionNote',
-    human_access: 'Owner',
-    note: 'sadfasdsdgdsf',
-    note_html: '<p dir=\'auto\'>sadfasdsdgdsf</p>',
-    last_edited_at: '2017-08-03T14:19:35.691Z',
-    last_edited_by: {
-      id: 1,
-      name: 'Root',
-      username: 'root',
-      state: 'active',
-      avatar_url: null,
-      path: '/root',
-    },
-    current_user: {
-      can_edit: true,
+  notes: [
+    {
+      id: 1395,
+      attachment: {
+        url: null,
+        filename: null,
+        image: false,
+      },
+      author: {
+        id: 1,
+        name: 'Root',
+        username: 'root',
+        state: 'active',
+        avatar_url: null,
+        path: '/root',
+      },
+      created_at: '2017-08-02T10:51:58.559Z',
+      updated_at: '2017-08-02T10:51:58.559Z',
+      system: false,
+      noteable_id: 98,
+      noteable_type: 'Issue',
+      type: 'DiscussionNote',
+      human_access: 'Owner',
+      note: 'THIS IS A DICUSSSION!',
+      note_html: "<p dir='auto'>THIS IS A DICUSSSION!</p>",
+      current_user: {
+        can_edit: true,
+      },
+      discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+      emoji_awardable: true,
+      award_emoji: [],
+      toggle_award_path: '/gitlab-org/gitlab-ce/notes/1395/toggle_award_emoji',
+      report_abuse_path:
+        '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1395&user_id=1',
+      path: '/gitlab-org/gitlab-ce/notes/1395',
     },
-    discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
-    emoji_awardable: true,
-    award_emoji: [],
-    toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji',
-    report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1',
-    path: '/gitlab-org/gitlab-ce/notes/1396',
-  }, {
-    id: 1437,
-    attachment: {
-      url: null,
-      filename: null,
-      image: false,
+    {
+      id: 1396,
+      attachment: {
+        url: null,
+        filename: null,
+        image: false,
+      },
+      author: {
+        id: 1,
+        name: 'Root',
+        username: 'root',
+        state: 'active',
+        avatar_url: null,
+        path: '/root',
+      },
+      created_at: '2017-08-02T10:56:50.980Z',
+      updated_at: '2017-08-03T14:19:35.691Z',
+      system: false,
+      noteable_id: 98,
+      noteable_type: 'Issue',
+      type: 'DiscussionNote',
+      human_access: 'Owner',
+      note: 'sadfasdsdgdsf',
+      note_html: "<p dir='auto'>sadfasdsdgdsf</p>",
+      last_edited_at: '2017-08-03T14:19:35.691Z',
+      last_edited_by: {
+        id: 1,
+        name: 'Root',
+        username: 'root',
+        state: 'active',
+        avatar_url: null,
+        path: '/root',
+      },
+      current_user: {
+        can_edit: true,
+      },
+      discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+      emoji_awardable: true,
+      award_emoji: [],
+      toggle_award_path: '/gitlab-org/gitlab-ce/notes/1396/toggle_award_emoji',
+      report_abuse_path:
+        '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1396&user_id=1',
+      path: '/gitlab-org/gitlab-ce/notes/1396',
     },
-    author: {
-      id: 1,
-      name: 'Root',
-      username: 'root',
-      state: 'active',
-      avatar_url: null,
-      path: '/root',
+    {
+      id: 1437,
+      attachment: {
+        url: null,
+        filename: null,
+        image: false,
+      },
+      author: {
+        id: 1,
+        name: 'Root',
+        username: 'root',
+        state: 'active',
+        avatar_url: null,
+        path: '/root',
+      },
+      created_at: '2017-08-03T18:11:18.780Z',
+      updated_at: '2017-08-04T09:52:31.062Z',
+      system: false,
+      noteable_id: 98,
+      noteable_type: 'Issue',
+      type: 'DiscussionNote',
+      human_access: 'Owner',
+      note: 'adsfasf Should disappear',
+      note_html: "<p dir='auto'>adsfasf Should disappear</p>",
+      last_edited_at: '2017-08-04T09:52:31.062Z',
+      last_edited_by: {
+        id: 1,
+        name: 'Root',
+        username: 'root',
+        state: 'active',
+        avatar_url: null,
+        path: '/root',
+      },
+      current_user: {
+        can_edit: true,
+      },
+      discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
+      emoji_awardable: true,
+      award_emoji: [],
+      toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji',
+      report_abuse_path:
+        '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1',
+      path: '/gitlab-org/gitlab-ce/notes/1437',
     },
-    created_at: '2017-08-03T18:11:18.780Z',
-    updated_at: '2017-08-04T09:52:31.062Z',
-    system: false,
-    noteable_id: 98,
-    noteable_type: 'Issue',
-    type: 'DiscussionNote',
-    human_access: 'Owner',
-    note: 'adsfasf Should disappear',
-    note_html: '<p dir=\'auto\'>adsfasf Should disappear</p>',
-    last_edited_at: '2017-08-04T09:52:31.062Z',
-    last_edited_by: {
+  ],
+  individual_note: false,
+};
+
+export const loggedOutnoteableData = {
+  id: 98,
+  iid: 26,
+  author_id: 1,
+  description: '',
+  lock_version: 1,
+  milestone_id: null,
+  state: 'opened',
+  title: 'asdsa',
+  updated_by_id: 1,
+  created_at: '2017-02-07T10:11:18.395Z',
+  updated_at: '2017-08-08T10:22:51.564Z',
+  time_estimate: 0,
+  total_time_spent: 0,
+  human_time_estimate: null,
+  human_total_time_spent: null,
+  milestone: null,
+  labels: [],
+  branch_name: null,
+  confidential: false,
+  assignees: [
+    {
       id: 1,
       name: 'Root',
       username: 'root',
       state: 'active',
       avatar_url: null,
-      path: '/root',
+      web_url: 'http://localhost:3000/root',
     },
-    current_user: {
-      can_edit: true,
-    },
-    discussion_id: '9e3bd2f71a01de45fd166e6719eb380ad9f270b1',
-    emoji_awardable: true,
-    award_emoji: [],
-    toggle_award_path: '/gitlab-org/gitlab-ce/notes/1437/toggle_award_emoji',
-    report_abuse_path: '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1437&user_id=1',
-    path: '/gitlab-org/gitlab-ce/notes/1437',
-  }],
-  individual_note: false,
-};
-
-export const loggedOutnoteableData = {
-  "id": 98,
-  "iid": 26,
-  "author_id": 1,
-  "description": "",
-  "lock_version": 1,
-  "milestone_id": null,
-  "state": "opened",
-  "title": "asdsa",
-  "updated_by_id": 1,
-  "created_at": "2017-02-07T10:11:18.395Z",
-  "updated_at": "2017-08-08T10:22:51.564Z",
-  "time_estimate": 0,
-  "total_time_spent": 0,
-  "human_time_estimate": null,
-  "human_total_time_spent": null,
-  "milestone": null,
-  "labels": [],
-  "branch_name": null,
-  "confidential": false,
-  "assignees": [{
-    "id": 1,
-    "name": "Root",
-    "username": "root",
-    "state": "active",
-    "avatar_url": null,
-    "web_url": "http://localhost:3000/root"
-  }],
-  "due_date": null,
-  "moved_to_id": null,
-  "project_id": 2,
-  "web_url": "/gitlab-org/gitlab-ce/issues/26",
-  "current_user": {
-    "can_create_note": false,
-    "can_update": false
+  ],
+  due_date: null,
+  moved_to_id: null,
+  project_id: 2,
+  web_url: '/gitlab-org/gitlab-ce/issues/26',
+  current_user: {
+    can_create_note: false,
+    can_update: false,
   },
-  "create_note_path": "/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue",
-  "preview_note_path": "/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue"
-}
+  create_note_path: '/gitlab-org/gitlab-ce/notes?target_id=98&target_type=issue',
+  preview_note_path:
+    '/gitlab-org/gitlab-ce/preview_markdown?quick_actions_target_id=98&quick_actions_target_type=Issue',
+};
 
 export const INDIVIDUAL_NOTE_RESPONSE_MAP = {
-  'GET': {
-    '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{
-      "id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
-      "reply_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
-      "expanded": true,
-      "notes": [{
-        "id": 1390,
-        "attachment": {
-          "url": null,
-          "filename": null,
-          "image": false
-        },
-        "author": {
-          "id": 1,
-          "name": "Root",
-          "username": "root",
-          "state": "active",
-          "avatar_url": null,
-          "path": "/root"
-        },
-        "created_at": "2017-08-01T17:09:33.762Z",
-        "updated_at": "2017-08-01T17:09:33.762Z",
-        "system": false,
-        "noteable_id": 98,
-        "noteable_type": "Issue",
-        "type": null,
-        "human_access": "Owner",
-        "note": "sdfdsaf",
-        "note_html": "\u003cp dir=\"auto\"\u003esdfdsaf\u003c/p\u003e",
-        "current_user": {
-          "can_edit": true
-        },
-        "discussion_id": "0fb4e0e3f9276e55ff32eb4195add694aece4edd",
-        "emoji_awardable": true,
-        "award_emoji": [{
-          "name": "baseball",
-          "user": {
-            "id": 1,
-            "name": "Root",
-            "username": "root"
-          }
-        }, {
-          "name": "art",
-          "user": {
-            "id": 1,
-            "name": "Root",
-            "username": "root"
-          }
-        }],
-        "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji",
-        "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1",
-        "path": "/gitlab-org/gitlab-ce/notes/1390"
-      }],
-      "individual_note": true
-      }, {
-      "id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
-      "reply_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
-      "expanded": true,
-      "notes": [{
-        "id": 1391,
-        "attachment": {
-          "url": null,
-          "filename": null,
-          "image": false
-        },
-        "author": {
-          "id": 1,
-          "name": "Root",
-          "username": "root",
-          "state": "active",
-          "avatar_url": null,
-          "path": "/root"
-        },
-        "created_at": "2017-08-02T10:51:38.685Z",
-        "updated_at": "2017-08-02T10:51:38.685Z",
-        "system": false,
-        "noteable_id": 98,
-        "noteable_type": "Issue",
-        "type": null,
-        "human_access": "Owner",
-        "note": "New note!",
-        "note_html": "\u003cp dir=\"auto\"\u003eNew note!\u003c/p\u003e",
-        "current_user": {
-          "can_edit": true
-        },
-        "discussion_id": "70d5c92a4039a36c70100c6691c18c27e4b0a790",
-        "emoji_awardable": true,
-        "award_emoji": [],
-        "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji",
-        "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1",
-        "path": "/gitlab-org/gitlab-ce/notes/1391"
-      }],
-      "individual_note": true
-    }],
+  GET: {
+    '/gitlab-org/gitlab-ce/issues/26/discussions.json': [
+      {
+        id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+        reply_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+        expanded: true,
+        notes: [
+          {
+            id: 1390,
+            attachment: {
+              url: null,
+              filename: null,
+              image: false,
+            },
+            author: {
+              id: 1,
+              name: 'Root',
+              username: 'root',
+              state: 'active',
+              avatar_url: null,
+              path: '/root',
+            },
+            created_at: '2017-08-01T17:09:33.762Z',
+            updated_at: '2017-08-01T17:09:33.762Z',
+            system: false,
+            noteable_id: 98,
+            noteable_type: 'Issue',
+            type: null,
+            human_access: 'Owner',
+            note: 'sdfdsaf',
+            note_html: '\u003cp dir="auto"\u003esdfdsaf\u003c/p\u003e',
+            current_user: {
+              can_edit: true,
+            },
+            discussion_id: '0fb4e0e3f9276e55ff32eb4195add694aece4edd',
+            emoji_awardable: true,
+            award_emoji: [
+              {
+                name: 'baseball',
+                user: {
+                  id: 1,
+                  name: 'Root',
+                  username: 'root',
+                },
+              },
+              {
+                name: 'art',
+                user: {
+                  id: 1,
+                  name: 'Root',
+                  username: 'root',
+                },
+              },
+            ],
+            toggle_award_path: '/gitlab-org/gitlab-ce/notes/1390/toggle_award_emoji',
+            report_abuse_path:
+              '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1390\u0026user_id=1',
+            path: '/gitlab-org/gitlab-ce/notes/1390',
+          },
+        ],
+        individual_note: true,
+      },
+      {
+        id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
+        reply_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
+        expanded: true,
+        notes: [
+          {
+            id: 1391,
+            attachment: {
+              url: null,
+              filename: null,
+              image: false,
+            },
+            author: {
+              id: 1,
+              name: 'Root',
+              username: 'root',
+              state: 'active',
+              avatar_url: null,
+              path: '/root',
+            },
+            created_at: '2017-08-02T10:51:38.685Z',
+            updated_at: '2017-08-02T10:51:38.685Z',
+            system: false,
+            noteable_id: 98,
+            noteable_type: 'Issue',
+            type: null,
+            human_access: 'Owner',
+            note: 'New note!',
+            note_html: '\u003cp dir="auto"\u003eNew note!\u003c/p\u003e',
+            current_user: {
+              can_edit: true,
+            },
+            discussion_id: '70d5c92a4039a36c70100c6691c18c27e4b0a790',
+            emoji_awardable: true,
+            award_emoji: [],
+            toggle_award_path: '/gitlab-org/gitlab-ce/notes/1391/toggle_award_emoji',
+            report_abuse_path:
+              '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F26%23note_1391\u0026user_id=1',
+            path: '/gitlab-org/gitlab-ce/notes/1391',
+          },
+        ],
+        individual_note: true,
+      },
+    ],
     '/gitlab-org/gitlab-ce/noteable/issue/98/notes': {
       last_fetched_at: 1512900838,
       notes: [],
     },
   },
-  'PUT': {
+  PUT: {
     '/gitlab-org/gitlab-ce/notes/1471': {
-      "commands_changes": null,
-      "valid": true,
-      "id": 1471,
-      "attachment": null,
-      "author": {
-        "id": 1,
-        "name": "Root",
-        "username": "root",
-        "state": "active",
-        "avatar_url": null,
-        "path": "/root"
+      commands_changes: null,
+      valid: true,
+      id: 1471,
+      attachment: null,
+      author: {
+        id: 1,
+        name: 'Root',
+        username: 'root',
+        state: 'active',
+        avatar_url: null,
+        path: '/root',
       },
-      "created_at": "2017-08-08T16:53:00.666Z",
-      "updated_at": "2017-12-10T11:03:21.876Z",
-      "system": false,
-      "noteable_id": 124,
-      "noteable_type": "Issue",
-      "noteable_iid": 29,
-      "type": "DiscussionNote",
-      "human_access": "Owner",
-      "note": "Adding a comment",
-      "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e",
-      "last_edited_at": "2017-12-10T11:03:21.876Z",
-      "last_edited_by": {
-        "id": 1,
-        "name": 'Root',
-        "username": 'root',
-        "state": 'active',
-        "avatar_url": null,
-        "path": '/root',
+      created_at: '2017-08-08T16:53:00.666Z',
+      updated_at: '2017-12-10T11:03:21.876Z',
+      system: false,
+      noteable_id: 124,
+      noteable_type: 'Issue',
+      noteable_iid: 29,
+      type: 'DiscussionNote',
+      human_access: 'Owner',
+      note: 'Adding a comment',
+      note_html: '\u003cp dir="auto"\u003eAdding a comment\u003c/p\u003e',
+      last_edited_at: '2017-12-10T11:03:21.876Z',
+      last_edited_by: {
+        id: 1,
+        name: 'Root',
+        username: 'root',
+        state: 'active',
+        avatar_url: null,
+        path: '/root',
       },
-      "current_user": {
-        "can_edit": true
+      current_user: {
+        can_edit: true,
       },
-      "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
-      "emoji_awardable": true,
-      "award_emoji": [],
-      "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji",
-      "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1",
-      "path": "/gitlab-org/gitlab-ce/notes/1471"
+      discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
+      emoji_awardable: true,
+      award_emoji: [],
+      toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji',
+      report_abuse_path:
+        '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1',
+      path: '/gitlab-org/gitlab-ce/notes/1471',
     },
-  }
+  },
 };
 
 export const DISCUSSION_NOTE_RESPONSE_MAP = {
   ...INDIVIDUAL_NOTE_RESPONSE_MAP,
-  'GET': {
+  GET: {
     ...INDIVIDUAL_NOTE_RESPONSE_MAP.GET,
-    '/gitlab-org/gitlab-ce/issues/26/discussions.json': [{
-      "id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
-      "reply_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
-      "expanded": true,
-      "notes": [{
-        "id": 1471,
-        "attachment": {
-          "url": null,
-          "filename": null,
-          "image": false
-        },
-        "author": {
-          "id": 1,
-          "name": "Root",
-          "username": "root",
-          "state": "active",
-          "avatar_url": null,
-          "path": "/root"
-        },
-        "created_at": "2017-08-08T16:53:00.666Z",
-        "updated_at": "2017-08-08T16:53:00.666Z",
-        "system": false,
-        "noteable_id": 124,
-        "noteable_type": "Issue",
-        "noteable_iid": 29,
-        "type": "DiscussionNote",
-        "human_access": "Owner",
-        "note": "Adding a comment",
-        "note_html": "\u003cp dir=\"auto\"\u003eAdding a comment\u003c/p\u003e",
-        "current_user": {
-          "can_edit": true
-        },
-        "discussion_id": "a3ed36e29b1957efb3b68c53e2d7a2b24b1df052",
-        "emoji_awardable": true,
-        "award_emoji": [],
-        "toggle_award_path": "/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji",
-        "report_abuse_path": "/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1",
-        "path": "/gitlab-org/gitlab-ce/notes/1471"
-      }],
-      "individual_note": false
-    }],
+    '/gitlab-org/gitlab-ce/issues/26/discussions.json': [
+      {
+        id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
+        reply_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
+        expanded: true,
+        notes: [
+          {
+            id: 1471,
+            attachment: {
+              url: null,
+              filename: null,
+              image: false,
+            },
+            author: {
+              id: 1,
+              name: 'Root',
+              username: 'root',
+              state: 'active',
+              avatar_url: null,
+              path: '/root',
+            },
+            created_at: '2017-08-08T16:53:00.666Z',
+            updated_at: '2017-08-08T16:53:00.666Z',
+            system: false,
+            noteable_id: 124,
+            noteable_type: 'Issue',
+            noteable_iid: 29,
+            type: 'DiscussionNote',
+            human_access: 'Owner',
+            note: 'Adding a comment',
+            note_html: '\u003cp dir="auto"\u003eAdding a comment\u003c/p\u003e',
+            current_user: {
+              can_edit: true,
+            },
+            discussion_id: 'a3ed36e29b1957efb3b68c53e2d7a2b24b1df052',
+            emoji_awardable: true,
+            award_emoji: [],
+            toggle_award_path: '/gitlab-org/gitlab-ce/notes/1471/toggle_award_emoji',
+            report_abuse_path:
+              '/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F29%23note_1471\u0026user_id=1',
+            path: '/gitlab-org/gitlab-ce/notes/1471',
+          },
+        ],
+        individual_note: false,
+      },
+    ],
   },
 };
 
 export function individualNoteInterceptor(request, next) {
   const body = INDIVIDUAL_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url];
 
-  next(request.respondWith(JSON.stringify(body), {
-    status: 200,
-  }));
+  next(
+    request.respondWith(JSON.stringify(body), {
+      status: 200,
+    }),
+  );
 }
 
 export function discussionNoteInterceptor(request, next) {
   const body = DISCUSSION_NOTE_RESPONSE_MAP[request.method.toUpperCase()][request.url];
 
-  next(request.respondWith(JSON.stringify(body), {
-    status: 200,
-  }));
+  next(
+    request.respondWith(JSON.stringify(body), {
+      status: 200,
+    }),
+  );
 }
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 91249b2c79e36159a1a0fc62a69f7e7ddef5afd9..520a25cc5c6b8b40406d032f689f0cf995fac9cf 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -5,7 +5,13 @@ import * as actions from '~/notes/stores/actions';
 import store from '~/notes/stores';
 import testAction from '../../helpers/vuex_action_helper';
 import { resetStore } from '../helpers';
-import { discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
+import {
+  discussionMock,
+  notesDataMock,
+  userDataMock,
+  noteableDataMock,
+  individualNote,
+} from '../mock_data';
 
 describe('Actions Notes Store', () => {
   afterEach(() => {
@@ -13,66 +19,103 @@ describe('Actions Notes Store', () => {
   });
 
   describe('setNotesData', () => {
-    it('should set received notes data', (done) => {
-      testAction(actions.setNotesData, null, { notesData: {} }, [
-        { type: 'SET_NOTES_DATA', payload: notesDataMock },
-      ], done);
+    it('should set received notes data', done => {
+      testAction(
+        actions.setNotesData,
+        notesDataMock,
+        { notesData: {} },
+        [{ type: 'SET_NOTES_DATA', payload: notesDataMock }],
+        [],
+        done,
+      );
     });
   });
 
   describe('setNoteableData', () => {
-    it('should set received issue data', (done) => {
-      testAction(actions.setNoteableData, null, { noteableData: {} }, [
-        { type: 'SET_NOTEABLE_DATA', payload: noteableDataMock },
-      ], done);
+    it('should set received issue data', done => {
+      testAction(
+        actions.setNoteableData,
+        noteableDataMock,
+        { noteableData: {} },
+        [{ type: 'SET_NOTEABLE_DATA', payload: noteableDataMock }],
+        [],
+        done,
+      );
     });
   });
 
   describe('setUserData', () => {
-    it('should set received user data', (done) => {
-      testAction(actions.setUserData, null, { userData: {} }, [
-        { type: 'SET_USER_DATA', payload: userDataMock },
-      ], done);
+    it('should set received user data', done => {
+      testAction(
+        actions.setUserData,
+        userDataMock,
+        { userData: {} },
+        [{ type: 'SET_USER_DATA', payload: userDataMock }],
+        [],
+        done,
+      );
     });
   });
 
   describe('setLastFetchedAt', () => {
-    it('should set received timestamp', (done) => {
-      testAction(actions.setLastFetchedAt, null, { lastFetchedAt: {} }, [
-        { type: 'SET_LAST_FETCHED_AT', payload: 'timestamp' },
-      ], done);
+    it('should set received timestamp', done => {
+      testAction(
+        actions.setLastFetchedAt,
+        'timestamp',
+        { lastFetchedAt: {} },
+        [{ type: 'SET_LAST_FETCHED_AT', payload: 'timestamp' }],
+        [],
+        done,
+      );
     });
   });
 
   describe('setInitialNotes', () => {
-    it('should set initial notes', (done) => {
-      testAction(actions.setInitialNotes, null, { notes: [] }, [
-        { type: 'SET_INITIAL_NOTES', payload: [individualNote] },
-      ], done);
+    it('should set initial notes', done => {
+      testAction(
+        actions.setInitialNotes,
+        [individualNote],
+        { notes: [] },
+        [{ type: 'SET_INITIAL_NOTES', payload: [individualNote] }],
+        [],
+        done,
+      );
     });
   });
 
   describe('setTargetNoteHash', () => {
-    it('should set target note hash', (done) => {
-      testAction(actions.setTargetNoteHash, null, { notes: [] }, [
-        { type: 'SET_TARGET_NOTE_HASH', payload: 'hash' },
-      ], done);
+    it('should set target note hash', done => {
+      testAction(
+        actions.setTargetNoteHash,
+        'hash',
+        { notes: [] },
+        [{ type: 'SET_TARGET_NOTE_HASH', payload: 'hash' }],
+        [],
+        done,
+      );
     });
   });
 
   describe('toggleDiscussion', () => {
-    it('should toggle discussion', (done) => {
-      testAction(actions.toggleDiscussion, null, { notes: [discussionMock] }, [
-        { type: 'TOGGLE_DISCUSSION', payload: { discussionId: discussionMock.id } },
-      ], done);
+    it('should toggle discussion', done => {
+      testAction(
+        actions.toggleDiscussion,
+        { discussionId: discussionMock.id },
+        { notes: [discussionMock] },
+        [{ type: 'TOGGLE_DISCUSSION', payload: { discussionId: discussionMock.id } }],
+        [],
+        done,
+      );
     });
   });
 
   describe('async methods', () => {
     const interceptor = (request, next) => {
-      next(request.respondWith(JSON.stringify({}), {
-        status: 200,
-      }));
+      next(
+        request.respondWith(JSON.stringify({}), {
+          status: 200,
+        }),
+      );
     };
 
     beforeEach(() => {
@@ -84,8 +127,9 @@ describe('Actions Notes Store', () => {
     });
 
     describe('closeIssue', () => {
-      it('sets state as closed', (done) => {
-        store.dispatch('closeIssue', { notesData: { closeIssuePath: '' } })
+      it('sets state as closed', done => {
+        store
+          .dispatch('closeIssue', { notesData: { closeIssuePath: '' } })
           .then(() => {
             expect(store.state.noteableData.state).toEqual('closed');
             expect(store.state.isToggleStateButtonLoading).toEqual(false);
@@ -96,8 +140,9 @@ describe('Actions Notes Store', () => {
     });
 
     describe('reopenIssue', () => {
-      it('sets state as reopened', (done) => {
-        store.dispatch('reopenIssue', { notesData: { reopenIssuePath: '' } })
+      it('sets state as reopened', done => {
+        store
+          .dispatch('reopenIssue', { notesData: { reopenIssuePath: '' } })
           .then(() => {
             expect(store.state.noteableData.state).toEqual('reopened');
             expect(store.state.isToggleStateButtonLoading).toEqual(false);
@@ -110,7 +155,7 @@ describe('Actions Notes Store', () => {
 
   describe('emitStateChangedEvent', () => {
     it('emits an event on the document', () => {
-      document.addEventListener('issuable_vue_app:change', (event) => {
+      document.addEventListener('issuable_vue_app:change', event => {
         expect(event.detail.data).toEqual({ id: '1', state: 'closed' });
         expect(event.detail.isClosed).toEqual(false);
       });
@@ -120,40 +165,47 @@ describe('Actions Notes Store', () => {
   });
 
   describe('toggleStateButtonLoading', () => {
-    it('should set loading as true', (done) => {
-      testAction(actions.toggleStateButtonLoading, true, {}, [
-        { type: 'TOGGLE_STATE_BUTTON_LOADING', payload: true },
-      ], done);
+    it('should set loading as true', done => {
+      testAction(
+        actions.toggleStateButtonLoading,
+        true,
+        {},
+        [{ type: 'TOGGLE_STATE_BUTTON_LOADING', payload: true }],
+        [],
+        done,
+      );
     });
 
-    it('should set loading as false', (done) => {
-      testAction(actions.toggleStateButtonLoading, false, {}, [
-        { type: 'TOGGLE_STATE_BUTTON_LOADING', payload: false },
-      ], done);
+    it('should set loading as false', done => {
+      testAction(
+        actions.toggleStateButtonLoading,
+        false,
+        {},
+        [{ type: 'TOGGLE_STATE_BUTTON_LOADING', payload: false }],
+        [],
+        done,
+      );
     });
   });
 
   describe('toggleIssueLocalState', () => {
-    it('sets issue state as closed', (done) => {
-      testAction(actions.toggleIssueLocalState, 'closed', {}, [
-        { type: 'CLOSE_ISSUE', payload: 'closed' },
-      ], done);
+    it('sets issue state as closed', done => {
+      testAction(actions.toggleIssueLocalState, 'closed', {}, [{ type: 'CLOSE_ISSUE' }], [], done);
     });
 
-    it('sets issue state as reopened', (done) => {
-      testAction(actions.toggleIssueLocalState, 'reopened', {}, [
-        { type: 'REOPEN_ISSUE', payload: 'reopened' },
-      ], done);
+    it('sets issue state as reopened', done => {
+      testAction(actions.toggleIssueLocalState, 'reopened', {}, [{ type: 'REOPEN_ISSUE' }], [], done);
     });
   });
 
   describe('poll', () => {
-    beforeEach((done) => {
+    beforeEach(done => {
       jasmine.clock().install();
 
       spyOn(Vue.http, 'get').and.callThrough();
 
-      store.dispatch('setNotesData', notesDataMock)
+      store
+        .dispatch('setNotesData', notesDataMock)
         .then(done)
         .catch(done.fail);
     });
@@ -162,23 +214,29 @@ describe('Actions Notes Store', () => {
       jasmine.clock().uninstall();
     });
 
-    it('calls service with last fetched state', (done) => {
+    it('calls service with last fetched state', done => {
       const interceptor = (request, next) => {
-        next(request.respondWith(JSON.stringify({
-          notes: [],
-          last_fetched_at: '123456',
-        }), {
-          status: 200,
-          headers: {
-            'poll-interval': '1000',
-          },
-        }));
+        next(
+          request.respondWith(
+            JSON.stringify({
+              notes: [],
+              last_fetched_at: '123456',
+            }),
+            {
+              status: 200,
+              headers: {
+                'poll-interval': '1000',
+              },
+            },
+          ),
+        );
       };
 
       Vue.http.interceptors.push(interceptor);
       Vue.http.interceptors.push(headersInterceptor);
 
-      store.dispatch('poll')
+      store
+        .dispatch('poll')
         .then(() => new Promise(resolve => requestAnimationFrame(resolve)))
         .then(() => {
           expect(Vue.http.get).toHaveBeenCalledWith(jasmine.anything(), {
@@ -192,9 +250,12 @@ describe('Actions Notes Store', () => {
 
           jasmine.clock().tick(1500);
         })
-        .then(() => new Promise((resolve) => {
-          requestAnimationFrame(resolve);
-        }))
+        .then(
+          () =>
+            new Promise(resolve => {
+              requestAnimationFrame(resolve);
+            }),
+        )
         .then(() => {
           expect(Vue.http.get.calls.count()).toBe(2);
           expect(Vue.http.get.calls.mostRecent().args[1].headers).toEqual({
diff --git a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
index 080158a8ee01460c171dd14d7a9542e8fd1da41d..a24f8204fe11d69888f6fa6b493bfa378f374b07 100644
--- a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
+++ b/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
@@ -12,6 +12,7 @@ describe('Promote label modal', () => {
     labelColor: '#5cb85c',
     labelTextColor: '#ffffff',
     url: `${gl.TEST_HOST}/dummy/promote/labels`,
+    groupName: 'group',
   };
 
   describe('Modal title and description', () => {
@@ -24,7 +25,7 @@ describe('Promote label modal', () => {
     });
 
     it('contains the proper description', () => {
-      expect(vm.text).toContain('Promoting this label will make it available for all projects inside the group');
+      expect(vm.text).toContain(`Promoting ${labelMockData.labelTitle} will make it available for all projects inside ${labelMockData.groupName}`);
     });
 
     it('contains a label span with the color', () => {
diff --git a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
index 22956929e7bd89ad8418c0320052eb6a9fdfb232..8b220423637caf1a3412f5fe96b8fcca26efcf3b 100644
--- a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
+++ b/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
@@ -10,6 +10,7 @@ describe('Promote milestone modal', () => {
   const milestoneMockData = {
     milestoneTitle: 'v1.0',
     url: `${gl.TEST_HOST}/dummy/promote/milestones`,
+    groupName: 'group',
   };
 
   describe('Modal title and description', () => {
@@ -22,7 +23,7 @@ describe('Promote milestone modal', () => {
     });
 
     it('contains the proper description', () => {
-      expect(vm.text).toContain('Promoting this milestone will make it available for all projects inside the group.');
+      expect(vm.text).toContain(`Promoting ${milestoneMockData.milestoneTitle} will make it available for all projects inside ${milestoneMockData.groupName}.`);
     });
 
     it('contains the correct title', () => {
diff --git a/spec/javascripts/pipelines/graph/mock_data.js b/spec/javascripts/pipelines/graph/mock_data.js
index b9494f86d745ba7ee63c14c379b44fe22bd9134a..70eba98e939bc12f53e48e7f2699aa8a7c46f711 100644
--- a/spec/javascripts/pipelines/graph/mock_data.js
+++ b/spec/javascripts/pipelines/graph/mock_data.js
@@ -1,232 +1,261 @@
-/* eslint-disable quote-props, quotes, comma-dangle */
 export default {
-  "id": 123,
-  "user": {
-    "name": "Root",
-    "username": "root",
-    "id": 1,
-    "state": "active",
-    "avatar_url": null,
-    "web_url": "http://localhost:3000/root"
+  id: 123,
+  user: {
+    name: 'Root',
+    username: 'root',
+    id: 1,
+    state: 'active',
+    avatar_url: null,
+    web_url: 'http://localhost:3000/root',
   },
-  "active": false,
-  "coverage": null,
-  "path": "/root/ci-mock/pipelines/123",
-  "details": {
-    "status": {
-      "icon": "icon_status_success",
-      "text": "passed",
-      "label": "passed",
-      "group": "success",
-      "has_details": true,
-      "details_path": "/root/ci-mock/pipelines/123",
-      "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico"
+  active: false,
+  coverage: null,
+  path: '/root/ci-mock/pipelines/123',
+  details: {
+    status: {
+      icon: 'icon_status_success',
+      text: 'passed',
+      label: 'passed',
+      group: 'success',
+      has_details: true,
+      details_path: '/root/ci-mock/pipelines/123',
+      favicon:
+        '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
     },
-    "duration": 9,
-    "finished_at": "2017-04-19T14:30:27.542Z",
-    "stages": [{
-      "name": "test",
-      "title": "test: passed",
-      "groups": [{
-        "name": "test",
-        "size": 1,
-        "status": {
-          "icon": "icon_status_success",
-          "text": "passed",
-          "label": "passed",
-          "group": "success",
-          "has_details": true,
-          "details_path": "/root/ci-mock/builds/4153",
-          "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
-          "action": {
-            "icon": "retry",
-            "title": "Retry",
-            "path": "/root/ci-mock/builds/4153/retry",
-            "method": "post"
-          }
+    duration: 9,
+    finished_at: '2017-04-19T14:30:27.542Z',
+    stages: [
+      {
+        name: 'test',
+        title: 'test: passed',
+        groups: [
+          {
+            name: 'test',
+            size: 1,
+            status: {
+              icon: 'icon_status_success',
+              text: 'passed',
+              label: 'passed',
+              group: 'success',
+              has_details: true,
+              details_path: '/root/ci-mock/builds/4153',
+              favicon:
+                '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+              action: {
+                icon: 'retry',
+                title: 'Retry',
+                path: '/root/ci-mock/builds/4153/retry',
+                method: 'post',
+              },
+            },
+            jobs: [
+              {
+                id: 4153,
+                name: 'test',
+                build_path: '/root/ci-mock/builds/4153',
+                retry_path: '/root/ci-mock/builds/4153/retry',
+                playable: false,
+                created_at: '2017-04-13T09:25:18.959Z',
+                updated_at: '2017-04-13T09:25:23.118Z',
+                status: {
+                  icon: 'icon_status_success',
+                  text: 'passed',
+                  label: 'passed',
+                  group: 'success',
+                  has_details: true,
+                  details_path: '/root/ci-mock/builds/4153',
+                  favicon:
+                    '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+                  action: {
+                    icon: 'retry',
+                    title: 'Retry',
+                    path: '/root/ci-mock/builds/4153/retry',
+                    method: 'post',
+                  },
+                },
+              },
+            ],
+          },
+        ],
+        status: {
+          icon: 'icon_status_success',
+          text: 'passed',
+          label: 'passed',
+          group: 'success',
+          has_details: true,
+          details_path: '/root/ci-mock/pipelines/123#test',
+          favicon:
+            '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
         },
-        "jobs": [{
-          "id": 4153,
-          "name": "test",
-          "build_path": "/root/ci-mock/builds/4153",
-          "retry_path": "/root/ci-mock/builds/4153/retry",
-          "playable": false,
-          "created_at": "2017-04-13T09:25:18.959Z",
-          "updated_at": "2017-04-13T09:25:23.118Z",
-          "status": {
-            "icon": "icon_status_success",
-            "text": "passed",
-            "label": "passed",
-            "group": "success",
-            "has_details": true,
-            "details_path": "/root/ci-mock/builds/4153",
-            "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
-            "action": {
-              "icon": "retry",
-              "title": "Retry",
-              "path": "/root/ci-mock/builds/4153/retry",
-              "method": "post"
-            }
-          }
-        }]
-      }],
-      "status": {
-        "icon": "icon_status_success",
-        "text": "passed",
-        "label": "passed",
-        "group": "success",
-        "has_details": true,
-        "details_path": "/root/ci-mock/pipelines/123#test",
-        "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico"
+        path: '/root/ci-mock/pipelines/123#test',
+        dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=test',
       },
-      "path": "/root/ci-mock/pipelines/123#test",
-      "dropdown_path": "/root/ci-mock/pipelines/123/stage.json?stage=test"
-    }, {
-      "name": "deploy",
-      "title": "deploy: passed",
-      "groups": [{
-        "name": "deploy to production",
-        "size": 1,
-        "status": {
-          "icon": "icon_status_success",
-          "text": "passed",
-          "label": "passed",
-          "group": "success",
-          "has_details": true,
-          "details_path": "/root/ci-mock/builds/4166",
-          "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
-          "action": {
-            "icon": "retry",
-            "title": "Retry",
-            "path": "/root/ci-mock/builds/4166/retry",
-            "method": "post"
-          }
+      {
+        name: 'deploy',
+        title: 'deploy: passed',
+        groups: [
+          {
+            name: 'deploy to production',
+            size: 1,
+            status: {
+              icon: 'icon_status_success',
+              text: 'passed',
+              label: 'passed',
+              group: 'success',
+              has_details: true,
+              details_path: '/root/ci-mock/builds/4166',
+              favicon:
+                '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+              action: {
+                icon: 'retry',
+                title: 'Retry',
+                path: '/root/ci-mock/builds/4166/retry',
+                method: 'post',
+              },
+            },
+            jobs: [
+              {
+                id: 4166,
+                name: 'deploy to production',
+                build_path: '/root/ci-mock/builds/4166',
+                retry_path: '/root/ci-mock/builds/4166/retry',
+                playable: false,
+                created_at: '2017-04-19T14:29:46.463Z',
+                updated_at: '2017-04-19T14:30:27.498Z',
+                status: {
+                  icon: 'icon_status_success',
+                  text: 'passed',
+                  label: 'passed',
+                  group: 'success',
+                  has_details: true,
+                  details_path: '/root/ci-mock/builds/4166',
+                  favicon:
+                    '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+                  action: {
+                    icon: 'retry',
+                    title: 'Retry',
+                    path: '/root/ci-mock/builds/4166/retry',
+                    method: 'post',
+                  },
+                },
+              },
+            ],
+          },
+          {
+            name: 'deploy to staging',
+            size: 1,
+            status: {
+              icon: 'icon_status_success',
+              text: 'passed',
+              label: 'passed',
+              group: 'success',
+              has_details: true,
+              details_path: '/root/ci-mock/builds/4159',
+              favicon:
+                '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+              action: {
+                icon: 'retry',
+                title: 'Retry',
+                path: '/root/ci-mock/builds/4159/retry',
+                method: 'post',
+              },
+            },
+            jobs: [
+              {
+                id: 4159,
+                name: 'deploy to staging',
+                build_path: '/root/ci-mock/builds/4159',
+                retry_path: '/root/ci-mock/builds/4159/retry',
+                playable: false,
+                created_at: '2017-04-18T16:32:08.420Z',
+                updated_at: '2017-04-18T16:32:12.631Z',
+                status: {
+                  icon: 'icon_status_success',
+                  text: 'passed',
+                  label: 'passed',
+                  group: 'success',
+                  has_details: true,
+                  details_path: '/root/ci-mock/builds/4159',
+                  favicon:
+                    '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
+                  action: {
+                    icon: 'retry',
+                    title: 'Retry',
+                    path: '/root/ci-mock/builds/4159/retry',
+                    method: 'post',
+                  },
+                },
+              },
+            ],
+          },
+        ],
+        status: {
+          icon: 'icon_status_success',
+          text: 'passed',
+          label: 'passed',
+          group: 'success',
+          has_details: true,
+          details_path: '/root/ci-mock/pipelines/123#deploy',
+          favicon:
+            '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico',
         },
-        "jobs": [{
-          "id": 4166,
-          "name": "deploy to production",
-          "build_path": "/root/ci-mock/builds/4166",
-          "retry_path": "/root/ci-mock/builds/4166/retry",
-          "playable": false,
-          "created_at": "2017-04-19T14:29:46.463Z",
-          "updated_at": "2017-04-19T14:30:27.498Z",
-          "status": {
-            "icon": "icon_status_success",
-            "text": "passed",
-            "label": "passed",
-            "group": "success",
-            "has_details": true,
-            "details_path": "/root/ci-mock/builds/4166",
-            "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
-            "action": {
-              "icon": "retry",
-              "title": "Retry",
-              "path": "/root/ci-mock/builds/4166/retry",
-              "method": "post"
-            }
-          }
-        }]
-      }, {
-        "name": "deploy to staging",
-        "size": 1,
-        "status": {
-          "icon": "icon_status_success",
-          "text": "passed",
-          "label": "passed",
-          "group": "success",
-          "has_details": true,
-          "details_path": "/root/ci-mock/builds/4159",
-          "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
-          "action": {
-            "icon": "retry",
-            "title": "Retry",
-            "path": "/root/ci-mock/builds/4159/retry",
-            "method": "post"
-          }
-        },
-        "jobs": [{
-          "id": 4159,
-          "name": "deploy to staging",
-          "build_path": "/root/ci-mock/builds/4159",
-          "retry_path": "/root/ci-mock/builds/4159/retry",
-          "playable": false,
-          "created_at": "2017-04-18T16:32:08.420Z",
-          "updated_at": "2017-04-18T16:32:12.631Z",
-          "status": {
-            "icon": "icon_status_success",
-            "text": "passed",
-            "label": "passed",
-            "group": "success",
-            "has_details": true,
-            "details_path": "/root/ci-mock/builds/4159",
-            "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico",
-            "action": {
-              "icon": "retry",
-              "title": "Retry",
-              "path": "/root/ci-mock/builds/4159/retry",
-              "method": "post"
-            }
-          }
-        }]
-      }],
-      "status": {
-        "icon": "icon_status_success",
-        "text": "passed",
-        "label": "passed",
-        "group": "success",
-        "has_details": true,
-        "details_path": "/root/ci-mock/pipelines/123#deploy",
-        "favicon": "/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico"
+        path: '/root/ci-mock/pipelines/123#deploy',
+        dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=deploy',
+      },
+    ],
+    artifacts: [],
+    manual_actions: [
+      {
+        name: 'deploy to production',
+        path: '/root/ci-mock/builds/4166/play',
+        playable: false,
       },
-      "path": "/root/ci-mock/pipelines/123#deploy",
-      "dropdown_path": "/root/ci-mock/pipelines/123/stage.json?stage=deploy"
-    }],
-    "artifacts": [],
-    "manual_actions": [{
-      "name": "deploy to production",
-      "path": "/root/ci-mock/builds/4166/play",
-      "playable": false
-    }]
+    ],
   },
-  "flags": {
-    "latest": true,
-    "triggered": false,
-    "stuck": false,
-    "yaml_errors": false,
-    "retryable": false,
-    "cancelable": false
+  flags: {
+    latest: true,
+    triggered: false,
+    stuck: false,
+    yaml_errors: false,
+    retryable: false,
+    cancelable: false,
   },
-  "ref": {
-    "name": "master",
-    "path": "/root/ci-mock/tree/master",
-    "tag": false,
-    "branch": true
+  ref: {
+    name: 'master',
+    path: '/root/ci-mock/tree/master',
+    tag: false,
+    branch: true,
   },
-  "commit": {
-    "id": "798e5f902592192afaba73f4668ae30e56eae492",
-    "short_id": "798e5f90",
-    "title": "Merge branch 'new-branch' into 'master'\r",
-    "created_at": "2017-04-13T10:25:17.000+01:00",
-    "parent_ids": ["54d483b1ed156fbbf618886ddf7ab023e24f8738", "c8e2d38a6c538822e81c57022a6e3a0cfedebbcc"],
-    "message": "Merge branch 'new-branch' into 'master'\r\n\r\nAdd new file\r\n\r\nSee merge request !1",
-    "author_name": "Root",
-    "author_email": "admin@example.com",
-    "authored_date": "2017-04-13T10:25:17.000+01:00",
-    "committer_name": "Root",
-    "committer_email": "admin@example.com",
-    "committed_date": "2017-04-13T10:25:17.000+01:00",
-    "author": {
-      "name": "Root",
-      "username": "root",
-      "id": 1,
-      "state": "active",
-      "avatar_url": null,
-      "web_url": "http://localhost:3000/root"
+  commit: {
+    id: '798e5f902592192afaba73f4668ae30e56eae492',
+    short_id: '798e5f90',
+    title: "Merge branch 'new-branch' into 'master'\r",
+    created_at: '2017-04-13T10:25:17.000+01:00',
+    parent_ids: [
+      '54d483b1ed156fbbf618886ddf7ab023e24f8738',
+      'c8e2d38a6c538822e81c57022a6e3a0cfedebbcc',
+    ],
+    message:
+      "Merge branch 'new-branch' into 'master'\r\n\r\nAdd new file\r\n\r\nSee merge request !1",
+    author_name: 'Root',
+    author_email: 'admin@example.com',
+    authored_date: '2017-04-13T10:25:17.000+01:00',
+    committer_name: 'Root',
+    committer_email: 'admin@example.com',
+    committed_date: '2017-04-13T10:25:17.000+01:00',
+    author: {
+      name: 'Root',
+      username: 'root',
+      id: 1,
+      state: 'active',
+      avatar_url: null,
+      web_url: 'http://localhost:3000/root',
     },
-    "author_gravatar_url": null,
-    "commit_url": "http://localhost:3000/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492",
-    "commit_path": "/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492"
+    author_gravatar_url: null,
+    commit_url:
+      'http://localhost:3000/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492',
+    commit_path: '/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492',
   },
-  "created_at": "2017-04-13T09:25:18.881Z",
-  "updated_at": "2017-04-19T14:30:27.561Z"
+  created_at: '2017-04-13T09:25:18.881Z',
+  updated_at: '2017-04-19T14:30:27.561Z',
 };
diff --git a/spec/javascripts/registry/stores/actions_spec.js b/spec/javascripts/registry/stores/actions_spec.js
index 3c9da4f107b27c8b0787e73c170190dd2a199fbe..bc4c444655a518d0bcd4380553d5ac23f619bd23 100644
--- a/spec/javascripts/registry/stores/actions_spec.js
+++ b/spec/javascripts/registry/stores/actions_spec.js
@@ -29,57 +29,96 @@ describe('Actions Registry Store', () => {
     describe('fetchRepos', () => {
       beforeEach(() => {
         interceptor = (request, next) => {
-          next(request.respondWith(JSON.stringify(reposServerResponse), {
-            status: 200,
-          }));
+          next(
+            request.respondWith(JSON.stringify(reposServerResponse), {
+              status: 200,
+            }),
+          );
         };
 
         Vue.http.interceptors.push(interceptor);
       });
 
-      it('should set receveived repos', (done) => {
-        testAction(actions.fetchRepos, null, mockedState, [
-          { type: types.TOGGLE_MAIN_LOADING },
-          { type: types.SET_REPOS_LIST, payload: reposServerResponse },
-        ], done);
+      it('should set receveived repos', done => {
+        testAction(
+          actions.fetchRepos,
+          null,
+          mockedState,
+          [
+            { type: types.TOGGLE_MAIN_LOADING },
+            { type: types.TOGGLE_MAIN_LOADING },
+            { type: types.SET_REPOS_LIST, payload: reposServerResponse },
+          ],
+          [],
+          done,
+        );
       });
     });
 
     describe('fetchList', () => {
       beforeEach(() => {
         interceptor = (request, next) => {
-          next(request.respondWith(JSON.stringify(registryServerResponse), {
-            status: 200,
-          }));
+          next(
+            request.respondWith(JSON.stringify(registryServerResponse), {
+              status: 200,
+            }),
+          );
         };
 
         Vue.http.interceptors.push(interceptor);
       });
 
-      it('should set received list', (done) => {
+      it('should set received list', done => {
         mockedState.repos = parsedReposServerResponse;
 
-        testAction(actions.fetchList, { repo: mockedState.repos[1] }, mockedState, [
-          { type: types.TOGGLE_REGISTRY_LIST_LOADING },
-          { type: types.SET_REGISTRY_LIST, payload: registryServerResponse },
-        ], done);
+        const repo = mockedState.repos[1];
+
+        testAction(
+          actions.fetchList,
+          { repo },
+          mockedState,
+          [
+            { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
+            { type: types.TOGGLE_REGISTRY_LIST_LOADING, payload: repo },
+            {
+              type: types.SET_REGISTRY_LIST,
+              payload: {
+                repo,
+                resp: registryServerResponse,
+                headers: jasmine.anything(),
+              },
+            },
+          ],
+          [],
+          done,
+        );
       });
     });
   });
 
   describe('setMainEndpoint', () => {
-    it('should commit set main endpoint', (done) => {
-      testAction(actions.setMainEndpoint, 'endpoint', mockedState, [
-        { type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' },
-      ], done);
+    it('should commit set main endpoint', done => {
+      testAction(
+        actions.setMainEndpoint,
+        'endpoint',
+        mockedState,
+        [{ type: types.SET_MAIN_ENDPOINT, payload: 'endpoint' }],
+        [],
+        done,
+      );
     });
   });
 
   describe('toggleLoading', () => {
-    it('should commit toggle main loading', (done) => {
-      testAction(actions.toggleLoading, null, mockedState, [
-        { type: types.TOGGLE_MAIN_LOADING },
-      ], done);
+    it('should commit toggle main loading', done => {
+      testAction(
+        actions.toggleLoading,
+        null,
+        mockedState,
+        [{ type: types.TOGGLE_MAIN_LOADING }],
+        [],
+        done,
+      );
     });
   });
 });
diff --git a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
index 88a33caf2e388b4534adde03fed9d0e93b194294..0c173062835d830b97336be99888c2e311b76864 100644
--- a/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
+++ b/spec/javascripts/sidebar/confidential_issue_sidebar_spec.js
@@ -62,4 +62,22 @@ describe('Confidential Issue Sidebar Block', () => {
       done();
     });
   });
+
+  it('displays the edit form when opened from collapsed state', (done) => {
+    expect(vm1.edit).toBe(false);
+
+    vm1.$el.querySelector('.sidebar-collapsed-icon').click();
+
+    expect(vm1.edit).toBe(true);
+
+    setTimeout(() => {
+      expect(
+        vm1.$el
+          .innerHTML
+          .includes('You are going to turn off the confidentiality.'),
+      ).toBe(true);
+
+      done();
+    });
+  });
 });
diff --git a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
index 696fca516bc11b188dd5239b9c4e3b3920d7bf5e..9abc3daf221bb56d21b3dffd03024abed5b2019a 100644
--- a/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
+++ b/spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js
@@ -68,4 +68,22 @@ describe('LockIssueSidebar', () => {
       done();
     });
   });
+
+  it('displays the edit form when opened from collapsed state', (done) => {
+    expect(vm1.isLockDialogOpen).toBe(false);
+
+    vm1.$el.querySelector('.sidebar-collapsed-icon').click();
+
+    expect(vm1.isLockDialogOpen).toBe(true);
+
+    setTimeout(() => {
+      expect(
+        vm1.$el
+          .innerHTML
+          .includes('Unlock this issue?'),
+      ).toBe(true);
+
+      done();
+    });
+  });
 });
diff --git a/spec/javascripts/sidebar/mock_data.js b/spec/javascripts/sidebar/mock_data.js
index d9e84e35f69805edf3088c9116c7bdfab8d3ac76..8b6e8b24f006af8d265b181bf8c58e3860abbbbf 100644
--- a/spec/javascripts/sidebar/mock_data.js
+++ b/spec/javascripts/sidebar/mock_data.js
@@ -1,7 +1,5 @@
-/* eslint-disable quote-props*/
-
 const RESPONSE_MAP = {
-  'GET': {
+  GET: {
     '/gitlab-org/gitlab-shell/issues/5.json': {
       id: 45,
       iid: 5,
@@ -27,7 +25,8 @@ const RESPONSE_MAP = {
           username: 'user0',
           id: 22,
           state: 'active',
-          avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+          avatar_url:
+            'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
           web_url: 'http: //localhost:3001/user0',
         },
         {
@@ -35,7 +34,8 @@ const RESPONSE_MAP = {
           username: 'tajuana',
           id: 18,
           state: 'active',
-          avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+          avatar_url:
+            'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
           web_url: 'http: //localhost:3001/tajuana',
         },
         {
@@ -43,7 +43,8 @@ const RESPONSE_MAP = {
           username: 'michaele.will',
           id: 16,
           state: 'active',
-          avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+          avatar_url:
+            'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
           web_url: 'http: //localhost:3001/michaele.will',
         },
       ],
@@ -72,7 +73,8 @@ const RESPONSE_MAP = {
           username: 'user0',
           id: 22,
           state: 'active',
-          avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+          avatar_url:
+            'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
           web_url: 'http://localhost:3001/user0',
         },
         {
@@ -80,7 +82,8 @@ const RESPONSE_MAP = {
           username: 'tajuana',
           id: 18,
           state: 'active',
-          avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+          avatar_url:
+            'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
           web_url: 'http://localhost:3001/tajuana',
         },
         {
@@ -88,7 +91,8 @@ const RESPONSE_MAP = {
           username: 'michaele.will',
           id: 16,
           state: 'active',
-          avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+          avatar_url:
+            'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
           web_url: 'http://localhost:3001/michaele.will',
         },
       ],
@@ -100,7 +104,8 @@ const RESPONSE_MAP = {
           username: 'user0',
           id: 22,
           state: 'active',
-          avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
+          avatar_url:
+            'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80\u0026d=identicon',
           web_url: 'http://localhost:3001/user0',
         },
         {
@@ -108,7 +113,8 @@ const RESPONSE_MAP = {
           username: 'tajuana',
           id: 18,
           state: 'active',
-          avatar_url: 'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
+          avatar_url:
+            'https://www.gravatar.com/avatar/4852a41fb41616bf8f140d3701673f53?s=80\u0026d=identicon',
           web_url: 'http://localhost:3001/tajuana',
         },
         {
@@ -116,7 +122,8 @@ const RESPONSE_MAP = {
           username: 'michaele.will',
           id: 16,
           state: 'active',
-          avatar_url: 'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
+          avatar_url:
+            'https://www.gravatar.com/avatar/e301827eb03be955c9c172cb9a8e4e8a?s=80\u0026d=identicon',
           web_url: 'http://localhost:3001/michaele.will',
         },
       ],
@@ -126,20 +133,21 @@ const RESPONSE_MAP = {
     },
     '/autocomplete/projects?project_id=15': [
       {
-        'id': 0,
-        'name_with_namespace': 'No project',
-      }, {
-        'id': 20,
-        'name_with_namespace': 'foo / bar',
+        id: 0,
+        name_with_namespace: 'No project',
+      },
+      {
+        id: 20,
+        name_with_namespace: 'foo / bar',
       },
     ],
   },
-  'PUT': {
+  PUT: {
     '/gitlab-org/gitlab-shell/issues/5.json': {
       data: {},
     },
   },
-  'POST': {
+  POST: {
     '/gitlab-org/gitlab-shell/issues/5/move': {
       id: 123,
       iid: 5,
@@ -182,7 +190,8 @@ const mockData = {
       id: 1,
       name: 'Administrator',
       username: 'root',
-      avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+      avatar_url:
+        'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
     },
     rootPath: '/',
     fullPath: '/gitlab-org/gitlab-shell',
@@ -201,12 +210,14 @@ const mockData = {
   },
 };
 
-mockData.sidebarMockInterceptor = function (request, next) {
+mockData.sidebarMockInterceptor = function(request, next) {
   const body = this.responseMap[request.method.toUpperCase()][request.url];
 
-  next(request.respondWith(JSON.stringify(body), {
-    status: 200,
-  }));
+  next(
+    request.respondWith(JSON.stringify(body), {
+      status: 200,
+    }),
+  );
 }.bind(mockData);
 
 export default mockData;
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index 235c33fac0d5ec452c4ab033b68b41512328f7c6..9b9c9656979c6ebe22fa53490a82033fea774eec 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -17,46 +17,58 @@ describe('MRWidgetHeader', () => {
   describe('computed', () => {
     describe('shouldShowCommitsBehindText', () => {
       it('return true when there are divergedCommitsCount', () => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 12,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
-          targetBranch: 'master',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 12,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+            targetBranch: 'master',
+            statusPath: 'abc',
+          },
+        });
 
         expect(vm.shouldShowCommitsBehindText).toEqual(true);
       });
 
       it('returns false where there are no divergedComits count', () => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 0,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
-          targetBranch: 'master',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 0,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+            targetBranch: 'master',
+            statusPath: 'abc',
+          },
+        });
         expect(vm.shouldShowCommitsBehindText).toEqual(false);
       });
     });
 
     describe('commitsText', () => {
       it('returns singular when there is one commit', () => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 1,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
-          targetBranch: 'master',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 1,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+            targetBranch: 'master',
+            statusPath: 'abc',
+          },
+        });
 
         expect(vm.commitsText).toEqual('1 commit behind');
       });
 
       it('returns plural when there is more than one commit', () => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 2,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
-          targetBranch: 'master',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 2,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+            targetBranch: 'master',
+            statusPath: 'abc',
+          },
+        });
 
         expect(vm.commitsText).toEqual('2 commits behind');
       });
@@ -66,24 +78,27 @@ describe('MRWidgetHeader', () => {
   describe('template', () => {
     describe('common elements', () => {
       beforeEach(() => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 12,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
-          sourceBranchRemoved: false,
-          targetBranchPath: 'foo/bar/commits-path',
-          targetBranchTreePath: 'foo/bar/tree/path',
-          targetBranch: 'master',
-          isOpen: true,
-          emailPatchesPath: '/mr/email-patches',
-          plainDiffPath: '/mr/plainDiffPath',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 12,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+            sourceBranchRemoved: false,
+            targetBranchPath: 'foo/bar/commits-path',
+            targetBranchTreePath: 'foo/bar/tree/path',
+            targetBranch: 'master',
+            isOpen: true,
+            emailPatchesPath: '/mr/email-patches',
+            plainDiffPath: '/mr/plainDiffPath',
+            statusPath: 'abc',
+          },
+        });
       });
 
       it('renders source branch link', () => {
-        expect(
-          vm.$el.querySelector('.js-source-branch').innerHTML,
-        ).toEqual('<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>');
+        expect(vm.$el.querySelector('.js-source-branch').innerHTML).toEqual(
+          '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+        );
       });
 
       it('renders clipboard button', () => {
@@ -101,18 +116,21 @@ describe('MRWidgetHeader', () => {
       });
 
       beforeEach(() => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 12,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
-          sourceBranchRemoved: false,
-          targetBranchPath: 'foo/bar/commits-path',
-          targetBranchTreePath: 'foo/bar/tree/path',
-          targetBranch: 'master',
-          isOpen: true,
-          emailPatchesPath: '/mr/email-patches',
-          plainDiffPath: '/mr/plainDiffPath',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 12,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+            sourceBranchRemoved: false,
+            targetBranchPath: 'foo/bar/commits-path',
+            targetBranchTreePath: 'foo/bar/tree/path',
+            targetBranch: 'master',
+            isOpen: true,
+            emailPatchesPath: '/mr/email-patches',
+            plainDiffPath: '/mr/plainDiffPath',
+            statusPath: 'abc',
+          },
+        });
       });
 
       it('renders checkout branch button with modal trigger', () => {
@@ -123,39 +141,49 @@ describe('MRWidgetHeader', () => {
         expect(button.getAttribute('data-toggle')).toEqual('modal');
       });
 
+      it('renders web ide button', () => {
+        const button = vm.$el.querySelector('.js-web-ide');
+
+        expect(button.textContent.trim()).toEqual('Web IDE');
+        expect(button.getAttribute('href')).toEqual('undefined/-/ide/projectabc');
+      });
+
       it('renders download dropdown with links', () => {
-        expect(
-          vm.$el.querySelector('.js-download-email-patches').textContent.trim(),
-        ).toEqual('Email patches');
+        expect(vm.$el.querySelector('.js-download-email-patches').textContent.trim()).toEqual(
+          'Email patches',
+        );
 
-        expect(
-          vm.$el.querySelector('.js-download-email-patches').getAttribute('href'),
-        ).toEqual('/mr/email-patches');
+        expect(vm.$el.querySelector('.js-download-email-patches').getAttribute('href')).toEqual(
+          '/mr/email-patches',
+        );
 
-        expect(
-          vm.$el.querySelector('.js-download-plain-diff').textContent.trim(),
-        ).toEqual('Plain diff');
+        expect(vm.$el.querySelector('.js-download-plain-diff').textContent.trim()).toEqual(
+          'Plain diff',
+        );
 
-        expect(
-          vm.$el.querySelector('.js-download-plain-diff').getAttribute('href'),
-        ).toEqual('/mr/plainDiffPath');
+        expect(vm.$el.querySelector('.js-download-plain-diff').getAttribute('href')).toEqual(
+          '/mr/plainDiffPath',
+        );
       });
     });
 
     describe('with a closed merge request', () => {
       beforeEach(() => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 12,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
-          sourceBranchRemoved: false,
-          targetBranchPath: 'foo/bar/commits-path',
-          targetBranchTreePath: 'foo/bar/tree/path',
-          targetBranch: 'master',
-          isOpen: false,
-          emailPatchesPath: '/mr/email-patches',
-          plainDiffPath: '/mr/plainDiffPath',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 12,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+            sourceBranchRemoved: false,
+            targetBranchPath: 'foo/bar/commits-path',
+            targetBranchTreePath: 'foo/bar/tree/path',
+            targetBranch: 'master',
+            isOpen: false,
+            emailPatchesPath: '/mr/email-patches',
+            plainDiffPath: '/mr/plainDiffPath',
+            statusPath: 'abc',
+          },
+        });
       });
 
       it('does not render checkout branch button with modal trigger', () => {
@@ -165,30 +193,29 @@ describe('MRWidgetHeader', () => {
       });
 
       it('does not render download dropdown with links', () => {
-        expect(
-          vm.$el.querySelector('.js-download-email-patches'),
-        ).toEqual(null);
+        expect(vm.$el.querySelector('.js-download-email-patches')).toEqual(null);
 
-        expect(
-          vm.$el.querySelector('.js-download-plain-diff'),
-        ).toEqual(null);
+        expect(vm.$el.querySelector('.js-download-plain-diff')).toEqual(null);
       });
     });
 
     describe('without diverged commits', () => {
       beforeEach(() => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 0,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
-          sourceBranchRemoved: false,
-          targetBranchPath: 'foo/bar/commits-path',
-          targetBranchTreePath: 'foo/bar/tree/path',
-          targetBranch: 'master',
-          isOpen: true,
-          emailPatchesPath: '/mr/email-patches',
-          plainDiffPath: '/mr/plainDiffPath',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 0,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+            sourceBranchRemoved: false,
+            targetBranchPath: 'foo/bar/commits-path',
+            targetBranchTreePath: 'foo/bar/tree/path',
+            targetBranch: 'master',
+            isOpen: true,
+            emailPatchesPath: '/mr/email-patches',
+            plainDiffPath: '/mr/plainDiffPath',
+            statusPath: 'abc',
+          },
+        });
       });
 
       it('does not render diverged commits info', () => {
@@ -198,22 +225,27 @@ describe('MRWidgetHeader', () => {
 
     describe('with diverged commits', () => {
       beforeEach(() => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 12,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
-          sourceBranchRemoved: false,
-          targetBranchPath: 'foo/bar/commits-path',
-          targetBranchTreePath: 'foo/bar/tree/path',
-          targetBranch: 'master',
-          isOpen: true,
-          emailPatchesPath: '/mr/email-patches',
-          plainDiffPath: '/mr/plainDiffPath',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 12,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+            sourceBranchRemoved: false,
+            targetBranchPath: 'foo/bar/commits-path',
+            targetBranchTreePath: 'foo/bar/tree/path',
+            targetBranch: 'master',
+            isOpen: true,
+            emailPatchesPath: '/mr/email-patches',
+            plainDiffPath: '/mr/plainDiffPath',
+            statusPath: 'abc',
+          },
+        });
       });
 
       it('renders diverged commits info', () => {
-        expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual('(12 commits behind)');
+        expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual(
+          '(12 commits behind)',
+        );
       });
     });
   });
diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js
index 3dd7530748429d189789e4af924865cdf1b6140e..3fc7663b9c2f0d36d2d2ad613d9db15c36e98500 100644
--- a/spec/javascripts/vue_mr_widget/mock_data.js
+++ b/spec/javascripts/vue_mr_widget/mock_data.js
@@ -1,213 +1,218 @@
-/* eslint-disable */
-
 export default {
-  "id": 132,
-  "iid": 22,
-  "assignee_id": null,
-  "author_id": 1,
-  "description": "",
-  "lock_version": null,
-  "milestone_id": null,
-  "position": 0,
-  "state": "merged",
-  "title": "Update README.md",
-  "updated_by_id": null,
-  "created_at": "2017-04-07T12:27:26.718Z",
-  "updated_at": "2017-04-07T15:39:25.852Z",
-  "time_estimate": 0,
-  "total_time_spent": 0,
-  "human_time_estimate": null,
-  "human_total_time_spent": null,
-  "in_progress_merge_commit_sha": null,
-  "merge_commit_sha": "53027d060246c8f47e4a9310fb332aa52f221775",
-  "merge_error": null,
-  "merge_params": {
-    "force_remove_source_branch": null
+  id: 132,
+  iid: 22,
+  assignee_id: null,
+  author_id: 1,
+  description: '',
+  lock_version: null,
+  milestone_id: null,
+  position: 0,
+  state: 'merged',
+  title: 'Update README.md',
+  updated_by_id: null,
+  created_at: '2017-04-07T12:27:26.718Z',
+  updated_at: '2017-04-07T15:39:25.852Z',
+  time_estimate: 0,
+  total_time_spent: 0,
+  human_time_estimate: null,
+  human_total_time_spent: null,
+  in_progress_merge_commit_sha: null,
+  merge_commit_sha: '53027d060246c8f47e4a9310fb332aa52f221775',
+  merge_error: null,
+  merge_params: {
+    force_remove_source_branch: null,
   },
-  "merge_status": "can_be_merged",
-  "merge_user_id": null,
-  "merge_when_pipeline_succeeds": false,
-  "source_branch": "daaaa",
-  "source_branch_link": "daaaa",
-  "source_project_id": 19,
-  "target_branch": "master",
-  "target_project_id": 19,
-  "metrics": {
-    "merged_by": {
-      "name": "Administrator",
-      "username": "root",
-      "id": 1,
-      "state": "active",
-      "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-      "web_url": "http://localhost:3000/root"
+  merge_status: 'can_be_merged',
+  merge_user_id: null,
+  merge_when_pipeline_succeeds: false,
+  source_branch: 'daaaa',
+  source_branch_link: 'daaaa',
+  source_project_id: 19,
+  target_branch: 'master',
+  target_project_id: 19,
+  metrics: {
+    merged_by: {
+      name: 'Administrator',
+      username: 'root',
+      id: 1,
+      state: 'active',
+      avatar_url:
+        'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+      web_url: 'http://localhost:3000/root',
     },
-    "merged_at": "2017-04-07T15:39:25.696Z",
-    "closed_by": null,
-    "closed_at": null
+    merged_at: '2017-04-07T15:39:25.696Z',
+    closed_by: null,
+    closed_at: null,
   },
-  "author": {
-    "name": "Administrator",
-    "username": "root",
-    "id": 1,
-    "state": "active",
-    "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-    "web_url": "http://localhost:3000/root"
+  author: {
+    name: 'Administrator',
+    username: 'root',
+    id: 1,
+    state: 'active',
+    avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+    web_url: 'http://localhost:3000/root',
   },
-  "merge_user": null,
-  "diff_head_sha": "104096c51715e12e7ae41f9333e9fa35b73f385d",
-  "diff_head_commit_short_id": "104096c5",
-  "merge_commit_message": "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
-  "pipeline": {
-    "id": 172,
-    "user": {
-      "name": "Administrator",
-      "username": "root",
-      "id": 1,
-      "state": "active",
-      "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-      "web_url": "http://localhost:3000/root"
+  merge_user: null,
+  diff_head_sha: '104096c51715e12e7ae41f9333e9fa35b73f385d',
+  diff_head_commit_short_id: '104096c5',
+  merge_commit_message:
+    "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
+  pipeline: {
+    id: 172,
+    user: {
+      name: 'Administrator',
+      username: 'root',
+      id: 1,
+      state: 'active',
+      avatar_url:
+        'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+      web_url: 'http://localhost:3000/root',
     },
-    "active": false,
-    "coverage": "92.16",
-    "path": "/root/acets-app/pipelines/172",
-    "details": {
-      "status": {
-        "icon": "icon_status_success",
-        "favicon": "favicon_status_success",
-        "text": "passed",
-        "label": "passed",
-        "group": "success",
-        "has_details": true,
-        "details_path": "/root/acets-app/pipelines/172"
+    active: false,
+    coverage: '92.16',
+    path: '/root/acets-app/pipelines/172',
+    details: {
+      status: {
+        icon: 'icon_status_success',
+        favicon: 'favicon_status_success',
+        text: 'passed',
+        label: 'passed',
+        group: 'success',
+        has_details: true,
+        details_path: '/root/acets-app/pipelines/172',
       },
-      "duration": null,
-      "finished_at": "2017-04-07T14:00:14.256Z",
-      "stages": [
+      duration: null,
+      finished_at: '2017-04-07T14:00:14.256Z',
+      stages: [
         {
-          "name": "build",
-          "title": "build: failed",
-          "status": {
-            "icon": "icon_status_failed",
-            "favicon": "favicon_status_failed",
-            "text": "failed",
-            "label": "failed",
-            "group": "failed",
-            "has_details": true,
-            "details_path": "/root/acets-app/pipelines/172#build"
+          name: 'build',
+          title: 'build: failed',
+          status: {
+            icon: 'icon_status_failed',
+            favicon: 'favicon_status_failed',
+            text: 'failed',
+            label: 'failed',
+            group: 'failed',
+            has_details: true,
+            details_path: '/root/acets-app/pipelines/172#build',
           },
-          "path": "/root/acets-app/pipelines/172#build",
-          "dropdown_path": "/root/acets-app/pipelines/172/stage.json?stage=build"
+          path: '/root/acets-app/pipelines/172#build',
+          dropdown_path: '/root/acets-app/pipelines/172/stage.json?stage=build',
         },
         {
-          "name": "review",
-          "title": "review: skipped",
-          "status": {
-            "icon": "icon_status_skipped",
-            "favicon": "favicon_status_skipped",
-            "text": "skipped",
-            "label": "skipped",
-            "group": "skipped",
-            "has_details": true,
-            "details_path": "/root/acets-app/pipelines/172#review"
+          name: 'review',
+          title: 'review: skipped',
+          status: {
+            icon: 'icon_status_skipped',
+            favicon: 'favicon_status_skipped',
+            text: 'skipped',
+            label: 'skipped',
+            group: 'skipped',
+            has_details: true,
+            details_path: '/root/acets-app/pipelines/172#review',
           },
-          "path": "/root/acets-app/pipelines/172#review",
-          "dropdown_path": "/root/acets-app/pipelines/172/stage.json?stage=review"
-        }
-      ],
-      "artifacts": [
-
+          path: '/root/acets-app/pipelines/172#review',
+          dropdown_path: '/root/acets-app/pipelines/172/stage.json?stage=review',
+        },
       ],
-      "manual_actions": [
+      artifacts: [],
+      manual_actions: [
         {
-          "name": "stop_review",
-          "path": "/root/acets-app/builds/1427/play",
-          "playable": false
-        }
-      ]
+          name: 'stop_review',
+          path: '/root/acets-app/builds/1427/play',
+          playable: false,
+        },
+      ],
     },
-    "flags": {
-      "latest": false,
-      "triggered": false,
-      "stuck": false,
-      "yaml_errors": false,
-      "retryable": true,
-      "cancelable": false
+    flags: {
+      latest: false,
+      triggered: false,
+      stuck: false,
+      yaml_errors: false,
+      retryable: true,
+      cancelable: false,
     },
-    "ref": {
-      "name": "daaaa",
-      "path": "/root/acets-app/tree/daaaa",
-      "tag": false,
-      "branch": true
+    ref: {
+      name: 'daaaa',
+      path: '/root/acets-app/tree/daaaa',
+      tag: false,
+      branch: true,
     },
-    "commit": {
-      "id": "104096c51715e12e7ae41f9333e9fa35b73f385d",
-      "short_id": "104096c5",
-      "title": "Update README.md",
-      "created_at": "2017-04-07T15:27:18.000+03:00",
-      "parent_ids": [
-        "2396536178668d8930c29d904e53bd4d06228b32"
-      ],
-      "message": "Update README.md",
-      "author_name": "Administrator",
-      "author_email": "admin@example.com",
-      "authored_date": "2017-04-07T15:27:18.000+03:00",
-      "committer_name": "Administrator",
-      "committer_email": "admin@example.com",
-      "committed_date": "2017-04-07T15:27:18.000+03:00",
-      "author": {
-        "name": "Administrator",
-        "username": "root",
-        "id": 1,
-        "state": "active",
-        "avatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-        "web_url": "http://localhost:3000/root"
+    commit: {
+      id: '104096c51715e12e7ae41f9333e9fa35b73f385d',
+      short_id: '104096c5',
+      title: 'Update README.md',
+      created_at: '2017-04-07T15:27:18.000+03:00',
+      parent_ids: ['2396536178668d8930c29d904e53bd4d06228b32'],
+      message: 'Update README.md',
+      author_name: 'Administrator',
+      author_email: 'admin@example.com',
+      authored_date: '2017-04-07T15:27:18.000+03:00',
+      committer_name: 'Administrator',
+      committer_email: 'admin@example.com',
+      committed_date: '2017-04-07T15:27:18.000+03:00',
+      author: {
+        name: 'Administrator',
+        username: 'root',
+        id: 1,
+        state: 'active',
+        avatar_url:
+          'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+        web_url: 'http://localhost:3000/root',
       },
-      "author_gravatar_url": "https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
-      "commit_url": "http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d",
-      "commit_path": "/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d"
+      author_gravatar_url:
+        'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+      commit_url:
+        'http://localhost:3000/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d',
+      commit_path: '/root/acets-app/commit/104096c51715e12e7ae41f9333e9fa35b73f385d',
     },
-    "retry_path": "/root/acets-app/pipelines/172/retry",
-    "created_at": "2017-04-07T12:27:19.520Z",
-    "updated_at": "2017-04-07T15:28:44.800Z"
+    retry_path: '/root/acets-app/pipelines/172/retry',
+    created_at: '2017-04-07T12:27:19.520Z',
+    updated_at: '2017-04-07T15:28:44.800Z',
   },
-  "work_in_progress": false,
-  "source_branch_exists": false,
-  "mergeable_discussions_state": true,
-  "conflicts_can_be_resolved_in_ui": false,
-  "branch_missing": true,
-  "commits_count": 1,
-  "has_conflicts": false,
-  "can_be_merged": true,
-  "has_ci": true,
-  "ci_status": "success",
-  "pipeline_status_path": "/root/acets-app/merge_requests/22/pipeline_status",
-  "issues_links": {
-    "closing": "",
-    "mentioned_but_not_closing": ""
+  work_in_progress: false,
+  source_branch_exists: false,
+  mergeable_discussions_state: true,
+  conflicts_can_be_resolved_in_ui: false,
+  branch_missing: true,
+  commits_count: 1,
+  has_conflicts: false,
+  can_be_merged: true,
+  has_ci: true,
+  ci_status: 'success',
+  pipeline_status_path: '/root/acets-app/merge_requests/22/pipeline_status',
+  issues_links: {
+    closing: '',
+    mentioned_but_not_closing: '',
   },
-  "current_user": {
-    "can_resolve_conflicts": true,
-    "can_remove_source_branch": false,
-    "can_revert_on_current_merge_request": true,
-    "can_cherry_pick_on_current_merge_request": true
+  current_user: {
+    can_resolve_conflicts: true,
+    can_remove_source_branch: false,
+    can_revert_on_current_merge_request: true,
+    can_cherry_pick_on_current_merge_request: true,
   },
-  "target_branch_path": "/root/acets-app/branches/master",
-  "source_branch_path": "/root/acets-app/branches/daaaa",
-  "conflict_resolution_ui_path": "/root/acets-app/merge_requests/22/conflicts",
-  "remove_wip_path": "/root/acets-app/merge_requests/22/remove_wip",
-  "cancel_merge_when_pipeline_succeeds_path": "/root/acets-app/merge_requests/22/cancel_merge_when_pipeline_succeeds",
-  "create_issue_to_resolve_discussions_path": "/root/acets-app/issues/new?merge_request_to_resolve_discussions_of=22",
-  "merge_path": "/root/acets-app/merge_requests/22/merge",
-  "cherry_pick_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+revert+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1",
-  "revert_in_fork_path": "/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1",
-  "email_patches_path": "/root/acets-app/merge_requests/22.patch",
-  "plain_diff_path": "/root/acets-app/merge_requests/22.diff",
-  "status_path": "/root/acets-app/merge_requests/22.json",
-  "merge_check_path": "/root/acets-app/merge_requests/22/merge_check",
-  "ci_environments_status_url": "/root/acets-app/merge_requests/22/ci_environments_status",
-  "project_archived": false,
-  "merge_commit_message_with_description": "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
-  "diverged_commits_count": 0,
-  "only_allow_merge_if_pipeline_succeeds": false,
-  "commit_change_content_path": "/root/acets-app/merge_requests/22/commit_change_content"
-}
+  target_branch_path: '/root/acets-app/branches/master',
+  source_branch_path: '/root/acets-app/branches/daaaa',
+  conflict_resolution_ui_path: '/root/acets-app/merge_requests/22/conflicts',
+  remove_wip_path: '/root/acets-app/merge_requests/22/remove_wip',
+  cancel_merge_when_pipeline_succeeds_path:
+    '/root/acets-app/merge_requests/22/cancel_merge_when_pipeline_succeeds',
+  create_issue_to_resolve_discussions_path:
+    '/root/acets-app/issues/new?merge_request_to_resolve_discussions_of=22',
+  merge_path: '/root/acets-app/merge_requests/22/merge',
+  cherry_pick_in_fork_path:
+    '/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+revert+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1',
+  revert_in_fork_path:
+    '/root/acets-app/forks?continue%5Bnotice%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+has+been+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.+Try+to+cherry-pick+this+commit+again.&continue%5Bnotice_now%5D=You%27re+not+allowed+to+make+changes+to+this+project+directly.+A+fork+of+this+project+is+being+created+that+you+can+make+changes+in%2C+so+you+can+submit+a+merge+request.&continue%5Bto%5D=%2Froot%2Facets-app%2Fmerge_requests%2F22&namespace_key=1',
+  email_patches_path: '/root/acets-app/merge_requests/22.patch',
+  plain_diff_path: '/root/acets-app/merge_requests/22.diff',
+  status_path: '/root/acets-app/merge_requests/22.json',
+  merge_check_path: '/root/acets-app/merge_requests/22/merge_check',
+  ci_environments_status_url: '/root/acets-app/merge_requests/22/ci_environments_status',
+  project_archived: false,
+  merge_commit_message_with_description:
+    "Merge branch 'daaaa' into 'master'\n\nUpdate README.md\n\nSee merge request !22",
+  diverged_commits_count: 0,
+  only_allow_merge_if_pipeline_succeeds: false,
+  commit_change_content_path: '/root/acets-app/merge_requests/22/commit_change_content',
+};
diff --git a/spec/javascripts/vue_shared/components/mock_data.js b/spec/javascripts/vue_shared/components/mock_data.js
index 0d781bdca74751bd2ca90e7c3e0f589009822aab..15b56c58c332db73203250ee2ae6467924f4b87c 100644
--- a/spec/javascripts/vue_shared/components/mock_data.js
+++ b/spec/javascripts/vue_shared/components/mock_data.js
@@ -1,5 +1,3 @@
-/* eslint-disable */
-
 export const mockMetrics = [
   [1493716685, '4.30859375'],
   [1493716745, '4.30859375'],
diff --git a/spec/lib/backup/files_spec.rb b/spec/lib/backup/files_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..14d055cbcc109b38ba1a1b0af5b633dbde0553ef
--- /dev/null
+++ b/spec/lib/backup/files_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Backup::Files do
+  let(:progress) { StringIO.new }
+  let!(:project) { create(:project) }
+
+  before do
+    allow(progress).to receive(:puts)
+    allow(progress).to receive(:print)
+    allow(FileUtils).to receive(:mkdir_p).and_return(true)
+    allow(FileUtils).to receive(:mv).and_return(true)
+    allow(File).to receive(:exist?).and_return(true)
+    allow(File).to receive(:realpath).with("/var/gitlab-registry").and_return("/var/gitlab-registry")
+    allow(File).to receive(:realpath).with("/var/gitlab-registry/..").and_return("/var")
+
+    allow_any_instance_of(String).to receive(:color) do |string, _color|
+      string
+    end
+
+    allow_any_instance_of(described_class).to receive(:progress).and_return(progress)
+  end
+
+  describe '#restore' do
+    subject { described_class.new('registry', '/var/gitlab-registry') }
+    let(:timestamp) { Time.utc(2017, 3, 22) }
+
+    around do |example|
+      Timecop.freeze(timestamp) { example.run }
+    end
+
+    describe 'folders with permission' do
+      before do
+        allow(subject).to receive(:run_pipeline!).and_return(true)
+        allow(subject).to receive(:backup_existing_files).and_return(true)
+        allow(Dir).to receive(:glob).with("/var/gitlab-registry/*", File::FNM_DOTMATCH).and_return(["/var/gitlab-registry/.", "/var/gitlab-registry/..", "/var/gitlab-registry/sample1"])
+      end
+
+      it 'moves all necessary files' do
+        allow(subject).to receive(:backup_existing_files).and_call_original
+        expect(FileUtils).to receive(:mv).with(["/var/gitlab-registry/sample1"], File.join(Gitlab.config.backup.path, "tmp", "registry.#{Time.now.to_i}"))
+        subject.restore
+      end
+
+      it 'raises no errors' do
+        expect { subject.restore }.not_to raise_error
+      end
+
+      it 'calls tar command with unlink' do
+        expect(subject).to receive(:run_pipeline!).with([%w(gzip -cd), %w(tar --unlink-first --recursive-unlink -C /var/gitlab-registry -xf -)], any_args)
+        subject.restore
+      end
+    end
+
+    describe 'folders without permissions' do
+      before do
+        allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES)
+        allow(subject).to receive(:run_pipeline!).and_return(true)
+      end
+
+      it 'shows error message' do
+        expect(subject).to receive(:access_denied_error).with("/var/gitlab-registry")
+        subject.restore
+      end
+    end
+  end
+end
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index 03573c304aa8458043ad3b4d10bae96a0b679568..e4c1c9bafc0e802e09319801495024568b3f93ad 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -7,6 +7,8 @@ describe Backup::Repository do
   before do
     allow(progress).to receive(:puts)
     allow(progress).to receive(:print)
+    allow(FileUtils).to receive(:mkdir_p).and_return(true)
+    allow(FileUtils).to receive(:mv).and_return(true)
 
     allow_any_instance_of(String).to receive(:color) do |string, _color|
       string
@@ -68,6 +70,17 @@ describe Backup::Repository do
         end
       end
     end
+
+    describe 'folders without permissions' do
+      before do
+        allow(FileUtils).to receive(:mv).and_raise(Errno::EACCES)
+      end
+
+      it 'shows error message' do
+        expect(subject).to receive(:access_denied_error)
+        subject.restore
+      end
+    end
   end
 
   describe '#empty_repo?' do
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index cbb0089bde7362aec3066404628adc14f5608f71..a50329473addab11298956e0a839b6d97fd9edb5 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -167,6 +167,15 @@ describe Banzai::Filter::AutolinkFilter do
       expect(actual).to eq(expected_complicated_link)
     end
 
+    it 'does not double-encode HTML entities' do
+      encoded_link = "#{link}?foo=bar&amp;baz=quux"
+      expected_encoded_link = %Q{<a href="#{encoded_link}">#{encoded_link}</a>}
+      actual = unescape(filter(encoded_link).to_html)
+
+      expect(actual).to eq(Rinku.auto_link(encoded_link))
+      expect(actual).to eq(expected_encoded_link)
+    end
+
     it 'does not include trailing HTML entities' do
       doc = filter("See &lt;&lt;&lt;#{link}&gt;&gt;&gt;")
 
diff --git a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
index e112e9e9e3d0b6cf53de3bd974683c61f965072b..5ce84c610423c69d3cf94bb8951e0faace78a154 100644
--- a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
@@ -51,4 +51,20 @@ describe Gitlab::BackgroundMigration::MigrateBuildStage, :migration, schema: 201
     expect { described_class.new.perform(1, 6) }
       .to raise_error ActiveRecord::RecordNotUnique
   end
+
+  context 'when invalid class can be loaded due to single table inheritance' do
+    let(:commit_status) do
+      jobs.create!(id: 7, commit_id: 1, project_id: 123, stage_idx: 4,
+                   stage: 'post-deploy', status: :failed)
+    end
+
+    before do
+      commit_status.update_column(:type, 'SomeClass')
+    end
+
+    it 'does ignore single table inheritance type' do
+      expect { described_class.new.perform(1, 7) }.not_to raise_error
+      expect(jobs.find(7)).to have_attributes(stage_id: (a_value > 0))
+    end
+  end
 end
diff --git a/spec/lib/gitlab/ci/build/policy/variables_spec.rb b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2ce858836e3975d1f641f8ccc23f97c6da39511d
--- /dev/null
+++ b/spec/lib/gitlab/ci/build/policy/variables_spec.rb
@@ -0,0 +1,72 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Build::Policy::Variables do
+  set(:project) { create(:project) }
+
+  let(:pipeline) do
+    build(:ci_empty_pipeline, project: project, ref: 'master', source: :push)
+  end
+
+  let(:ci_build) do
+    build(:ci_build, pipeline: pipeline, project: project, ref: 'master')
+  end
+
+  let(:seed) { double('build seed', to_resource: ci_build) }
+
+  before do
+    pipeline.variables.build(key: 'CI_PROJECT_NAME', value: '')
+  end
+
+  describe '#satisfied_by?' do
+    it 'is satisfied by at least one matching statement' do
+      policy = described_class.new(['$CI_PROJECT_ID', '$UNDEFINED'])
+
+      expect(policy).to be_satisfied_by(pipeline, seed)
+    end
+
+    it 'is not satisfied by an overriden empty variable' do
+      policy = described_class.new(['$CI_PROJECT_NAME'])
+
+      expect(policy).not_to be_satisfied_by(pipeline, seed)
+    end
+
+    it 'is satisfied by a truthy pipeline expression' do
+      policy = described_class.new([%($CI_PIPELINE_SOURCE == "push")])
+
+      expect(policy).to be_satisfied_by(pipeline, seed)
+    end
+
+    it 'is not satisfied by a falsy pipeline expression' do
+      policy = described_class.new([%($CI_PIPELINE_SOURCE == "invalid source")])
+
+      expect(policy).not_to be_satisfied_by(pipeline, seed)
+    end
+
+    it 'is satisfied by a truthy expression using undefined variable' do
+      policy = described_class.new(['$UNDEFINED == null'])
+
+      expect(policy).to be_satisfied_by(pipeline, seed)
+    end
+
+    it 'is not satisfied by a falsy expression using undefined variable' do
+      policy = described_class.new(['$UNDEFINED'])
+
+      expect(policy).not_to be_satisfied_by(pipeline, seed)
+    end
+
+    it 'allows to evaluate regular secret variables' do
+      create(:ci_variable, project: project, key: 'SECRET', value: 'my secret')
+
+      policy = described_class.new(["$SECRET == 'my secret'"])
+
+      expect(policy).to be_satisfied_by(pipeline, seed)
+    end
+
+    it 'does not persist neither pipeline nor build' do
+      described_class.new('$VAR').satisfied_by?(pipeline, seed)
+
+      expect(pipeline).not_to be_persisted
+      expect(seed.to_resource).not_to be_persisted
+    end
+  end
+end
diff --git a/spec/lib/gitlab/ci/build/step_spec.rb b/spec/lib/gitlab/ci/build/step_spec.rb
index 5a21282712a79e516b1739fc5a27c30b57fee68d..cce4efaa069852714d898325122b7e5277add302 100644
--- a/spec/lib/gitlab/ci/build/step_spec.rb
+++ b/spec/lib/gitlab/ci/build/step_spec.rb
@@ -5,10 +5,14 @@ describe Gitlab::Ci::Build::Step do
     shared_examples 'has correct script' do
       subject { described_class.from_commands(job) }
 
+      before do
+        job.run!
+      end
+
       it 'fabricates an object' do
         expect(subject.name).to eq(:script)
         expect(subject.script).to eq(script)
-        expect(subject.timeout).to eq(job.timeout)
+        expect(subject.timeout).to eq(job.metadata_timeout)
         expect(subject.when).to eq('on_success')
         expect(subject.allow_failure).to be_falsey
       end
@@ -47,6 +51,10 @@ describe Gitlab::Ci::Build::Step do
 
     subject { described_class.from_after_script(job) }
 
+    before do
+      job.run!
+    end
+
     context 'when after_script is empty' do
       it 'doesn not fabricate an object' do
         is_expected.to be_nil
@@ -59,7 +67,7 @@ describe Gitlab::Ci::Build::Step do
       it 'fabricates an object' do
         expect(subject.name).to eq(:after_script)
         expect(subject.script).to eq(['ls -la', 'date'])
-        expect(subject.timeout).to eq(job.timeout)
+        expect(subject.timeout).to eq(job.metadata_timeout)
         expect(subject.when).to eq('always')
         expect(subject.allow_failure).to be_truthy
       end
diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
index 5e83abf645b87f1532aa0fdc5009876030ae4037..08718c382b9675054163f09bf0d95ffb2258ea7a 100644
--- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb
+++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb
@@ -83,6 +83,39 @@ describe Gitlab::Ci::Config::Entry::Policy do
       end
     end
 
+    context 'when specifying valid variables expressions policy' do
+      let(:config) { { variables: ['$VAR == null'] } }
+
+      it 'is a correct configuraton' do
+        expect(entry).to be_valid
+        expect(entry.value).to eq(config)
+      end
+    end
+
+    context 'when specifying variables expressions in invalid format' do
+      let(:config) { { variables: '$MY_VAR' } }
+
+      it 'reports an error about invalid format' do
+        expect(entry.errors).to include /should be an array of strings/
+      end
+    end
+
+    context 'when specifying invalid variables expressions statement' do
+      let(:config) { { variables: ['$MY_VAR =='] } }
+
+      it 'reports an error about invalid statement' do
+        expect(entry.errors).to include /invalid expression syntax/
+      end
+    end
+
+    context 'when specifying invalid variables expressions token' do
+      let(:config) { { variables: ['$MY_VAR == 123'] } }
+
+      it 'reports an error about invalid statement' do
+        expect(entry.errors).to include /invalid expression syntax/
+      end
+    end
+
     context 'when specifying unknown policy' do
       let(:config) { { refs: ['master'], invalid: :something } }
 
diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
index 2258ae83f382925374c43f1d1b4b7d809c57a41c..8312fa47cfa5a23c9b5f4fc4837e60911d3d9ab0 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb
@@ -6,7 +6,8 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
 
   let(:pipeline) do
     build(:ci_pipeline_with_one_job, project: project,
-                                     ref: 'master')
+                                     ref: 'master',
+                                     user: user)
   end
 
   let(:command) do
@@ -42,6 +43,10 @@ describe Gitlab::Ci::Pipeline::Chain::Populate do
       expect(pipeline.stages.first.builds).to be_one
       expect(pipeline.stages.first.builds.first).not_to be_persisted
     end
+
+    it 'correctly assigns user' do
+      expect(pipeline.builds).to all(have_attributes(user: user))
+    end
   end
 
   context 'when pipeline is empty' do
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb
index 86234dfb9e53c8261263611af54ac5cfa2814474..1ccb792d1dade3e4b769b1ef99b16b11796eb344 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb
@@ -73,6 +73,22 @@ describe Gitlab::Ci::Pipeline::Expression::Lexeme::String do
         expect(token).not_to be_nil
         expect(token.build.evaluate).to eq 'some " string'
       end
+
+      it 'allows to use an empty string inside single quotes' do
+        scanner = StringScanner.new(%(''))
+
+        token = described_class.scan(scanner)
+
+        expect(token.build.evaluate).to eq ''
+      end
+
+      it 'allow to use an empty string inside double quotes' do
+        scanner = StringScanner.new(%(""))
+
+        token = described_class.scan(scanner)
+
+        expect(token.build.evaluate).to eq ''
+      end
     end
   end
 
diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
index 472a58599d8eb7de0858dfe7143897c9de971541..6685bf5385b272c1812497b4393697d91d83c9db 100644
--- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
@@ -1,14 +1,23 @@
 require 'spec_helper'
 
 describe Gitlab::Ci::Pipeline::Expression::Statement do
-  let(:pipeline) { build(:ci_pipeline) }
-
   subject do
-    described_class.new(text, pipeline)
+    described_class.new(text, variables)
+  end
+
+  let(:variables) do
+    { 'PRESENT_VARIABLE' => 'my variable',
+      EMPTY_VARIABLE: '' }
   end
 
-  before do
-    pipeline.variables.build([key: 'VARIABLE', value: 'my variable'])
+  describe '.new' do
+    context 'when variables are not provided' do
+      it 'allows to properly initializes the statement' do
+        statement = described_class.new('$PRESENT_VARIABLE')
+
+        expect(statement.evaluate).to be_nil
+      end
+    end
   end
 
   describe '#parse_tree' do
@@ -23,18 +32,26 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
 
     context 'when expression grammar is incorrect' do
       table = [
-        '$VAR "text"',      # missing operator
-        '== "123"',         # invalid right side
-        "'single quotes'",  # single quotes string
-        '$VAR ==',          # invalid right side
-        '12345',            # unknown syntax
-        ''                  # empty statement
+        '$VAR "text"',   # missing operator
+        '== "123"',      # invalid left side
+        '"some string"', # only string provided
+        '$VAR ==',       # invalid right side
+        '12345',         # unknown syntax
+        ''               # empty statement
       ]
 
       table.each do |syntax|
-        it "raises an error when syntax is `#{syntax}`" do
-          expect { described_class.new(syntax, pipeline).parse_tree }
-            .to raise_error described_class::StatementError
+        context "when expression grammar is #{syntax.inspect}" do
+          let(:text) { syntax }
+
+          it 'aises a statement error exception' do
+            expect { subject.parse_tree }
+              .to raise_error described_class::StatementError
+          end
+
+          it 'is an invalid statement' do
+            expect(subject).not_to be_valid
+          end
         end
       end
     end
@@ -47,10 +64,14 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
           expect(subject.parse_tree)
             .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
         end
+
+        it 'is a valid statement' do
+          expect(subject).to be_valid
+        end
       end
 
       context 'when using a single token' do
-        let(:text) { '$VARIABLE' }
+        let(:text) { '$PRESENT_VARIABLE' }
 
         it 'returns a single token instance' do
           expect(subject.parse_tree)
@@ -62,14 +83,17 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
 
   describe '#evaluate' do
     statements = [
-      ['$VARIABLE == "my variable"', true],
-      ["$VARIABLE == 'my variable'", true],
-      ['"my variable" == $VARIABLE', true],
-      ['$VARIABLE == null', false],
-      ['$VAR == null', true],
-      ['null == $VAR', true],
-      ['$VARIABLE', 'my variable'],
-      ['$VAR', nil]
+      ['$PRESENT_VARIABLE == "my variable"', true],
+      ["$PRESENT_VARIABLE == 'my variable'", true],
+      ['"my variable" == $PRESENT_VARIABLE', true],
+      ['$PRESENT_VARIABLE == null', false],
+      ['$EMPTY_VARIABLE == null', false],
+      ['"" == $EMPTY_VARIABLE', true],
+      ['$EMPTY_VARIABLE', ''],
+      ['$UNDEFINED_VARIABLE == null', true],
+      ['null == $UNDEFINED_VARIABLE', true],
+      ['$PRESENT_VARIABLE', 'my variable'],
+      ['$UNDEFINED_VARIABLE', nil]
     ]
 
     statements.each do |expression, value|
@@ -82,4 +106,25 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do
       end
     end
   end
+
+  describe '#truthful?' do
+    statements = [
+      ['$PRESENT_VARIABLE == "my variable"', true],
+      ["$PRESENT_VARIABLE == 'no match'", false],
+      ['$UNDEFINED_VARIABLE == null', true],
+      ['$PRESENT_VARIABLE', true],
+      ['$UNDEFINED_VARIABLE', false],
+      ['$EMPTY_VARIABLE', false]
+    ]
+
+    statements.each do |expression, value|
+      context "when using expression `#{expression}`" do
+        let(:text) { expression }
+
+        it "returns `#{value.inspect}`" do
+          expect(subject.truthful?).to eq value
+        end
+      end
+    end
+  end
 end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 116573379e0c2b54535547389c9308dc262b2e22..fffa727c2ed80ee1269ff4a0d9c32e0548b12cee 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -21,16 +21,6 @@ describe Gitlab::Ci::Pipeline::Seed::Build do
     end
   end
 
-  describe '#user=' do
-    let(:user) { build(:user) }
-
-    it 'assignes user to a build' do
-      subject.user = user
-
-      expect(subject.attributes).to include(user: user)
-    end
-  end
-
   describe '#to_resource' do
     it 'returns a valid build resource' do
       expect(subject.to_resource).to be_a(::Ci::Build)
diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
index 8f0bf40d624eb28347a6a18d3734ad06bde8505c..eb1b285c7bd32f44b3e400690f5393329336e1e4 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb
@@ -95,16 +95,6 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do
     end
   end
 
-  describe '#user=' do
-    let(:user) { build(:user) }
-
-    it 'assignes relevant pipeline attributes' do
-      subject.user = user
-
-      expect(subject.seeds.map(&:attributes)).to all(include(user: user))
-    end
-  end
-
   describe '#to_resource' do
     it 'builds a valid stage object with all builds' do
       subject.to_resource.save!
diff --git a/spec/lib/gitlab/ci/variables/collection/item_spec.rb b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
index cc1257484d263ef6d7bfc5872b820f541c74fa22..bf9208f1ff4e5d55235198770aabb876c1524fcc 100644
--- a/spec/lib/gitlab/ci/variables/collection/item_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection/item_spec.rb
@@ -46,9 +46,13 @@ describe Gitlab::Ci::Variables::Collection::Item do
     end
   end
 
-  describe '#to_hash' do
-    it 'returns a hash representation of a collection item' do
-      expect(described_class.new(**variable).to_hash).to eq variable
+  describe '#to_runner_variable' do
+    it 'returns a runner-compatible hash representation' do
+      runner_variable = described_class
+        .new(**variable)
+        .to_runner_variable
+
+      expect(runner_variable).to eq variable
     end
   end
 end
diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb
index 90b6e1782420ed4213f26d01f252f574891aa29c..cb2f7718c9cf67770da45a5625d341ddd2497df2 100644
--- a/spec/lib/gitlab/ci/variables/collection_spec.rb
+++ b/spec/lib/gitlab/ci/variables/collection_spec.rb
@@ -7,7 +7,7 @@ describe Gitlab::Ci::Variables::Collection do
 
       collection = described_class.new([variable])
 
-      expect(collection.first.to_hash).to eq variable
+      expect(collection.first.to_runner_variable).to eq variable
     end
 
     it 'can be initialized without an argument' do
@@ -96,4 +96,19 @@ describe Gitlab::Ci::Variables::Collection do
         .to eq [{ key: 'TEST', value: 1, public: true }]
     end
   end
+
+  describe '#to_hash' do
+    it 'returns regular hash in valid order without duplicates' do
+      collection = described_class.new
+        .append(key: 'TEST1', value: 'test-1')
+        .append(key: 'TEST2', value: 'test-2')
+        .append(key: 'TEST1', value: 'test-3')
+
+      expect(collection.to_hash).to eq('TEST1' => 'test-3',
+                                       'TEST2' => 'test-2')
+
+      expect(collection.to_hash).to include(TEST1: 'test-3')
+      expect(collection.to_hash).not_to include(TEST1: 'test-1')
+    end
+  end
 end
diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb
index fbc2af29b98150e22070c9586f0b6d6861a23d19..ecb16daec960a19e119ab7d9020c38efad3a41bf 100644
--- a/spec/lib/gitlab/ci/yaml_processor_spec.rb
+++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb
@@ -1311,6 +1311,14 @@ module Gitlab
             Gitlab::Ci::YamlProcessor.new(config)
           end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
         end
+
+        it 'returns errors if pipeline variables expression is invalid' do
+          config = YAML.dump({ rspec: { script: 'test', only: { variables: ['== null'] } } })
+
+          expect { Gitlab::Ci::YamlProcessor.new(config) }
+            .to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
+                            'jobs:rspec:only variables invalid expression syntax')
+        end
       end
 
       describe "Validate configuration templates" do
diff --git a/spec/lib/gitlab/git/gitmodules_parser_spec.rb b/spec/lib/gitlab/git/gitmodules_parser_spec.rb
index 143aa2218c9a4771d01d353d2273e02b6d1833ea..6fd2b33486b20fad5475fea11950614517616188 100644
--- a/spec/lib/gitlab/git/gitmodules_parser_spec.rb
+++ b/spec/lib/gitlab/git/gitmodules_parser_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
 
 describe Gitlab::Git::GitmodulesParser do
   it 'should parse a .gitmodules file correctly' do
-    parser = described_class.new(<<-'GITMODULES'.strip_heredoc)
+    data = <<~GITMODULES
       [submodule "vendor/libgit2"]
          path = vendor/libgit2
       [submodule "vendor/libgit2"]
@@ -16,6 +16,7 @@ describe Gitlab::Git::GitmodulesParser do
           url = https://example.com/another/project
     GITMODULES
 
+    parser = described_class.new(data.gsub("\n", "\r\n"))
     modules = parser.parse
 
     expect(modules).to eq({
diff --git a/spec/lib/gitlab/git/env_spec.rb b/spec/lib/gitlab/git/hook_env_spec.rb
similarity index 54%
rename from spec/lib/gitlab/git/env_spec.rb
rename to spec/lib/gitlab/git/hook_env_spec.rb
index 03836d49518998836bbceadeccce51013f2ee5d8..e6aa5ad8c903fca39647ad07063be112c6c15cf6 100644
--- a/spec/lib/gitlab/git/env_spec.rb
+++ b/spec/lib/gitlab/git/hook_env_spec.rb
@@ -1,6 +1,8 @@
 require 'spec_helper'
 
-describe Gitlab::Git::Env do
+describe Gitlab::Git::HookEnv do
+  let(:gl_repository) { 'project-123' }
+
   describe ".set" do
     context 'with RequestStore.store disabled' do
       before do
@@ -8,9 +10,9 @@ describe Gitlab::Git::Env do
       end
 
       it 'does not store anything' do
-        described_class.set(GIT_OBJECT_DIRECTORY: 'foo')
+        described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'foo')
 
-        expect(described_class.all).to be_empty
+        expect(described_class.all(gl_repository)).to be_empty
       end
     end
 
@@ -21,15 +23,19 @@ describe Gitlab::Git::Env do
 
       it 'whitelist some `GIT_*` variables and stores them using RequestStore' do
         described_class.set(
-          GIT_OBJECT_DIRECTORY: 'foo',
-          GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar',
+          gl_repository,
+          GIT_OBJECT_DIRECTORY_RELATIVE: 'foo',
+          GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: 'bar',
           GIT_EXEC_PATH: 'baz',
           PATH: '~/.bin:/bin')
 
-        expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo')
-        expect(described_class[:GIT_ALTERNATE_OBJECT_DIRECTORIES]).to eq('bar')
-        expect(described_class[:GIT_EXEC_PATH]).to be_nil
-        expect(described_class[:bar]).to be_nil
+        git_env = described_class.all(gl_repository)
+
+        expect(git_env[:GIT_OBJECT_DIRECTORY_RELATIVE]).to eq('foo')
+        expect(git_env[:GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE]).to eq('bar')
+        expect(git_env[:GIT_EXEC_PATH]).to be_nil
+        expect(git_env[:PATH]).to be_nil
+        expect(git_env[:bar]).to be_nil
       end
     end
   end
@@ -39,14 +45,15 @@ describe Gitlab::Git::Env do
       before do
         allow(RequestStore).to receive(:active?).and_return(true)
         described_class.set(
-          GIT_OBJECT_DIRECTORY: 'foo',
-          GIT_ALTERNATE_OBJECT_DIRECTORIES: ['bar'])
+          gl_repository,
+          GIT_OBJECT_DIRECTORY_RELATIVE: 'foo',
+          GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: ['bar'])
       end
 
       it 'returns an env hash' do
-        expect(described_class.all).to eq({
-          'GIT_OBJECT_DIRECTORY' => 'foo',
-          'GIT_ALTERNATE_OBJECT_DIRECTORIES' => ['bar']
+        expect(described_class.all(gl_repository)).to eq({
+          'GIT_OBJECT_DIRECTORY_RELATIVE' => 'foo',
+          'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['bar']
         })
       end
     end
@@ -56,8 +63,8 @@ describe Gitlab::Git::Env do
     context 'with RequestStore.store enabled' do
       using RSpec::Parameterized::TableSyntax
 
-      let(:key) { 'GIT_OBJECT_DIRECTORY' }
-      subject { described_class.to_env_hash }
+      let(:key) { 'GIT_OBJECT_DIRECTORY_RELATIVE' }
+      subject { described_class.to_env_hash(gl_repository) }
 
       where(:input, :output) do
         nil         | nil
@@ -70,7 +77,7 @@ describe Gitlab::Git::Env do
       with_them do
         before do
           allow(RequestStore).to receive(:active?).and_return(true)
-          described_class.set(key.to_sym => input)
+          described_class.set(gl_repository, key.to_sym => input)
         end
 
         it 'puts the right value in the hash' do
@@ -84,47 +91,25 @@ describe Gitlab::Git::Env do
     end
   end
 
-  describe ".[]" do
-    context 'with RequestStore.store enabled' do
-      before do
-        allow(RequestStore).to receive(:active?).and_return(true)
-      end
-
-      before do
-        described_class.set(
-          GIT_OBJECT_DIRECTORY: 'foo',
-          GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar')
-      end
-
-      it 'returns a stored value for an existing key' do
-        expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo')
-      end
-
-      it 'returns nil for an non-existing key' do
-        expect(described_class[:foo]).to be_nil
-      end
-    end
-  end
-
   describe 'thread-safety' do
     context 'with RequestStore.store enabled' do
       before do
         allow(RequestStore).to receive(:active?).and_return(true)
-        described_class.set(GIT_OBJECT_DIRECTORY: 'foo')
+        described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'foo')
       end
 
       it 'is thread-safe' do
         another_thread = Thread.new do
-          described_class.set(GIT_OBJECT_DIRECTORY: 'bar')
+          described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'bar')
 
           Thread.stop
-          described_class[:GIT_OBJECT_DIRECTORY]
+          described_class.all(gl_repository)[:GIT_OBJECT_DIRECTORY_RELATIVE]
         end
 
         # Ensure another_thread runs first
         sleep 0.1 until another_thread.stop?
 
-        expect(described_class[:GIT_OBJECT_DIRECTORY]).to eq('foo')
+        expect(described_class.all(gl_repository)[:GIT_OBJECT_DIRECTORY_RELATIVE]).to eq('foo')
 
         another_thread.run
         expect(another_thread.value).to eq('bar')
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 0e315b3f49ebda54c8d0664b6927379bb6f36dd2..5cbe2808d0b5286480fa4902f41bb1cf23b8755a 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -120,7 +120,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
     describe 'alternates keyword argument' do
       context 'with no Git env stored' do
         before do
-          allow(Gitlab::Git::Env).to receive(:all).and_return({})
+          allow(Gitlab::Git::HookEnv).to receive(:all).and_return({})
         end
 
         it "is passed an empty array" do
@@ -132,7 +132,7 @@ describe Gitlab::Git::Repository, seed_helper: true do
 
       context 'with absolute and relative Git object dir envvars stored' do
         before do
-          allow(Gitlab::Git::Env).to receive(:all).and_return({
+          allow(Gitlab::Git::HookEnv).to receive(:all).and_return({
             'GIT_OBJECT_DIRECTORY_RELATIVE' => './objects/foo',
             'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => ['./objects/bar', './objects/baz'],
             'GIT_OBJECT_DIRECTORY' => 'ignored',
@@ -148,22 +148,6 @@ describe Gitlab::Git::Repository, seed_helper: true do
           repository.rugged
         end
       end
-
-      context 'with only absolute Git object dir envvars stored' do
-        before do
-          allow(Gitlab::Git::Env).to receive(:all).and_return({
-            'GIT_OBJECT_DIRECTORY' => 'foo',
-            'GIT_ALTERNATE_OBJECT_DIRECTORIES' => %w[bar baz],
-            'GIT_OTHER' => 'another_env'
-          })
-        end
-
-        it "is passed the absolute object dir envvars as is" do
-          expect(Rugged::Repository).to receive(:new).with(repository.path, alternates: %w[foo bar baz])
-
-          repository.rugged
-        end
-      end
     end
   end
 
diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb
index 4e0ee206219e50852277f8d26be2123bae486aac..32ec1e029c855f7b4230676f46c03cb29205ac86 100644
--- a/spec/lib/gitlab/git/rev_list_spec.rb
+++ b/spec/lib/gitlab/git/rev_list_spec.rb
@@ -3,17 +3,6 @@ require 'spec_helper'
 describe Gitlab::Git::RevList do
   let(:repository) { create(:project, :repository).repository.raw }
   let(:rev_list) { described_class.new(repository, newrev: 'newrev') }
-  let(:env_hash) do
-    {
-      'GIT_OBJECT_DIRECTORY' => 'foo',
-      'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
-    }
-  end
-  let(:command_env) { { 'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'foo:bar' } }
-
-  before do
-    allow(Gitlab::Git::Env).to receive(:all).and_return(env_hash)
-  end
 
   def args_for_popen(args_list)
     [Gitlab.config.git.bin_path, 'rev-list', *args_list]
@@ -23,7 +12,7 @@ describe Gitlab::Git::RevList do
     params = [
       args_for_popen(additional_args),
       repository.path,
-      command_env,
+      {},
       hash_including(lazy_block: with_lazy_block ? anything : nil)
     ]
 
diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
index 872377c93d8ab121f67709c732076d7ccf0c97ae..f03c7e3f04b423cd442dc069b3d56261ceffeaef 100644
--- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
@@ -58,4 +58,14 @@ describe Gitlab::GitalyClient::RemoteService do
       client.update_remote_mirror(ref_name, only_branches_matching)
     end
   end
+
+  describe '.exists?' do
+    context "when the remote doesn't exist" do
+      let(:url) { 'https://gitlab.com/gitlab-org/ik-besta-niet-of-ik-word-geplaagd.git' }
+
+      it 'returns false' do
+        expect(described_class.exists?(url)).to be(false)
+      end
+    end
+  end
 end
diff --git a/spec/lib/gitlab/gitaly_client/util_spec.rb b/spec/lib/gitlab/gitaly_client/util_spec.rb
index d1e0136f8c1ff435cd1515c5d982e727be17034b..550db6db6d9f0226d8c27de3a93e8091cf36b060 100644
--- a/spec/lib/gitlab/gitaly_client/util_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/util_spec.rb
@@ -7,16 +7,19 @@ describe Gitlab::GitalyClient::Util do
     let(:gl_repository) { 'project-1' }
     let(:git_object_directory) { '.git/objects' }
     let(:git_alternate_object_directory) { ['/dir/one', '/dir/two'] }
+    let(:git_env) do
+      {
+        'GIT_OBJECT_DIRECTORY_RELATIVE' => git_object_directory,
+        'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => git_alternate_object_directory
+      }
+    end
 
     subject do
       described_class.repository(repository_storage, relative_path, gl_repository)
     end
 
     it 'creates a Gitaly::Repository with the given data' do
-      allow(Gitlab::Git::Env).to receive(:[]).with('GIT_OBJECT_DIRECTORY_RELATIVE')
-        .and_return(git_object_directory)
-      allow(Gitlab::Git::Env).to receive(:[]).with('GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE')
-        .and_return(git_alternate_object_directory)
+      allow(Gitlab::Git::HookEnv).to receive(:all).with(gl_repository).and_return(git_env)
 
       expect(subject).to be_a(Gitaly::Repository)
       expect(subject.storage_name).to eq(repository_storage)
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index 5bedfc79dd31ab67dea58ebb8a07cdf0bc222bdc..1f0f1fdd7da18a683c7da6f4c171a5e50793e545 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -38,8 +38,12 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
       expect(project)
         .to receive(:wiki_repository_exists?)
         .and_return(false)
+      expect(Gitlab::GitalyClient::RemoteService)
+        .to receive(:exists?)
+        .with("foo.wiki.git")
+        .and_return(true)
 
-      expect(importer.import_wiki?).to eq(true)
+      expect(importer.import_wiki?).to be(true)
     end
 
     it 'returns false if the GitHub wiki is disabled' do
diff --git a/spec/lib/gitlab/http_spec.rb b/spec/lib/gitlab/http_spec.rb
index b0bc081a3c81207443340e6fec7a8040466fb443..d0dadfa78da2ba678a0001f4ca2da16012e90cb6 100644
--- a/spec/lib/gitlab/http_spec.rb
+++ b/spec/lib/gitlab/http_spec.rb
@@ -12,11 +12,11 @@ describe Gitlab::HTTP do
       end
 
       it 'deny requests to localhost' do
-        expect { described_class.get('http://localhost:3003') }.to raise_error(URI::InvalidURIError)
+        expect { described_class.get('http://localhost:3003') }.to raise_error(Gitlab::HTTP::BlockedUrlError)
       end
 
       it 'deny requests to private network' do
-        expect { described_class.get('http://192.168.1.2:3003') }.to raise_error(URI::InvalidURIError)
+        expect { described_class.get('http://192.168.1.2:3003') }.to raise_error(Gitlab::HTTP::BlockedUrlError)
       end
 
       context 'if allow_local_requests set to true' do
@@ -41,7 +41,7 @@ describe Gitlab::HTTP do
 
       context 'if allow_local_requests set to false' do
         it 'override the global value and ban requests to localhost or private network' do
-          expect { described_class.get('http://localhost:3003', allow_local_requests: false) }.to raise_error(URI::InvalidURIError)
+          expect { described_class.get('http://localhost:3003', allow_local_requests: false) }.to raise_error(Gitlab::HTTP::BlockedUrlError)
         end
       end
     end
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ed54d87de4aa9121c0813c45902a603a3a8ca5b2
--- /dev/null
+++ b/spec/lib/gitlab/import_export/after_export_strategies/base_after_export_strategy_spec.rb
@@ -0,0 +1,104 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy do
+  let!(:service) { described_class.new }
+  let!(:project) { create(:project, :with_export) }
+  let(:shared) { project.import_export_shared }
+  let!(:user) { create(:user) }
+
+  describe '#execute' do
+    before do
+      allow(service).to receive(:strategy_execute)
+    end
+
+    it 'returns if project exported file is not found' do
+      allow(project).to receive(:export_project_path).and_return(nil)
+
+      expect(service).not_to receive(:strategy_execute)
+
+      service.execute(user, project)
+    end
+
+    it 'creates a lock file in the export dir' do
+      allow(service).to receive(:delete_after_export_lock)
+
+      service.execute(user, project)
+
+      expect(lock_path_exist?).to be_truthy
+    end
+
+    context 'when the method succeeds' do
+      it 'removes the lock file' do
+        service.execute(user, project)
+
+        expect(lock_path_exist?).to be_falsey
+      end
+    end
+
+    context 'when the method fails' do
+      before do
+        allow(service).to receive(:strategy_execute).and_call_original
+      end
+
+      context 'when validation fails' do
+        before do
+          allow(service).to receive(:invalid?).and_return(true)
+        end
+
+        it 'does not create the lock file' do
+          expect(service).not_to receive(:create_or_update_after_export_lock)
+
+          service.execute(user, project)
+        end
+
+        it 'does not execute main logic' do
+          expect(service).not_to receive(:strategy_execute)
+
+          service.execute(user, project)
+        end
+
+        it 'logs validation errors in shared context' do
+          expect(service).to receive(:log_validation_errors)
+
+          service.execute(user, project)
+        end
+      end
+
+      context 'when an exception is raised' do
+        it 'removes the lock' do
+          expect { service.execute(user, project) }.to raise_error(NotImplementedError)
+
+          expect(lock_path_exist?).to be_falsey
+        end
+      end
+    end
+  end
+
+  describe '#log_validation_errors' do
+    it 'add the message to the shared context' do
+      errors = %w(test_message test_message2)
+
+      allow(service).to receive(:invalid?).and_return(true)
+      allow(service.errors).to receive(:full_messages).and_return(errors)
+
+      expect(shared).to receive(:add_error_message).twice.and_call_original
+
+      service.execute(user, project)
+
+      expect(shared.errors).to eq errors
+    end
+  end
+
+  describe '#to_json' do
+    it 'adds the current strategy class to the serialized attributes' do
+      params = { param1: 1 }
+      result = params.merge(klass: described_class.to_s).to_json
+
+      expect(described_class.new(params).to_json).to eq result
+    end
+  end
+
+  def lock_path_exist?
+    File.exist?(described_class.lock_file_path(project))
+  end
+end
diff --git a/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..5fe57d9987b992c35e99a1e736712d42949c2f3c
--- /dev/null
+++ b/spec/lib/gitlab/import_export/after_export_strategies/web_upload_strategy_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy do
+  let(:example_url) { 'http://www.example.com' }
+  let(:strategy) { subject.new(url: example_url, http_method: 'post') }
+  let!(:project) { create(:project, :with_export) }
+  let!(:user) { build(:user) }
+
+  subject { described_class }
+
+  describe 'validations' do
+    it 'only POST and PUT method allowed' do
+      %w(POST post PUT put).each do |method|
+        expect(subject.new(url: example_url, http_method: method)).to be_valid
+      end
+
+      expect(subject.new(url: example_url, http_method: 'whatever')).not_to be_valid
+    end
+
+    it 'onyl allow urls as upload urls' do
+      expect(subject.new(url: example_url)).to be_valid
+      expect(subject.new(url: 'whatever')).not_to be_valid
+    end
+  end
+
+  describe '#execute' do
+    it 'removes the exported project file after the upload' do
+      allow(strategy).to receive(:send_file)
+      allow(strategy).to receive(:handle_response_error)
+
+      expect(project).to receive(:remove_exported_project_file)
+
+      strategy.execute(user, project)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb b/spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bf727285a9fe2a2e8b415ce16beb6e3f8237c6b7
--- /dev/null
+++ b/spec/lib/gitlab/import_export/after_export_strategy_builder_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AfterExportStrategyBuilder do
+  let!(:strategies_namespace) { 'Gitlab::ImportExport::AfterExportStrategies' }
+
+  describe '.build!' do
+    context 'when klass param is' do
+      it 'null it returns the default strategy' do
+        expect(described_class.build!(nil).class).to eq described_class.default_strategy
+      end
+
+      it 'not a valid class it raises StrategyNotFoundError exception' do
+        expect { described_class.build!('Whatever') }.to raise_error(described_class::StrategyNotFoundError)
+      end
+
+      it 'not a descendant of AfterExportStrategy' do
+        expect { described_class.build!('User') }.to raise_error(described_class::StrategyNotFoundError)
+      end
+    end
+
+    it 'initializes strategy with attributes param' do
+      params = { param1: 1, param2: 2, param3: 3 }
+
+      strategy = described_class.build!("#{strategies_namespace}::DownloadNotificationStrategy", params)
+
+      params.each { |k, v| expect(strategy.public_send(k)).to eq v }
+    end
+  end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index a204a8f1ffe8d0dde4ca25da7c1f816a0d3d49a1..b675d5dc0318a16be4b44ce0c47af08677dc0286 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -18,6 +18,7 @@ issues:
 - metrics
 - timelogs
 - issue_assignees
+- closed_by
 events:
 - author
 - project
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 0716852f57f67a7be897885a70035e6a1f9369e7..f949a23ffbb0ee408383d0696ffb45dce8f117cc 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -15,6 +15,7 @@ Issue:
 - updated_by_id
 - confidential
 - closed_at
+- closed_by_id
 - due_date
 - moved_to_id
 - lock_version
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
index 2d35b02648564337219339e157327d51aad9b148..a3b3dc3be6dd4984bc83f9c4923cc62cada2eb93 100644
--- a/spec/lib/gitlab/url_blocker_spec.rb
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -74,13 +74,13 @@ describe Gitlab::UrlBlocker do
       expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
     end
 
-    context 'when allow_private_networks is' do
-      let(:private_networks) { ['192.168.1.2', '10.0.0.2', '172.16.0.2'] }
+    context 'when allow_local_network is' do
+      let(:local_ips) { ['192.168.1.2', '10.0.0.2', '172.16.0.2'] }
       let(:fake_domain) { 'www.fakedomain.fake' }
 
       context 'true (default)' do
         it 'does not block urls from private networks' do
-          private_networks.each do |ip|
+          local_ips.each do |ip|
             stub_domain_resolv(fake_domain, ip)
 
             expect(described_class).not_to be_blocked_url("http://#{fake_domain}")
@@ -94,14 +94,14 @@ describe Gitlab::UrlBlocker do
 
       context 'false' do
         it 'blocks urls from private networks' do
-          private_networks.each do |ip|
+          local_ips.each do |ip|
             stub_domain_resolv(fake_domain, ip)
 
-            expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_private_networks: false)
+            expect(described_class).to be_blocked_url("http://#{fake_domain}", allow_local_network: false)
 
             unstub_domain_resolv
 
-            expect(described_class).to be_blocked_url("http://#{ip}", allow_private_networks: false)
+            expect(described_class).to be_blocked_url("http://#{ip}", allow_local_network: false)
           end
         end
       end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 138d21ede9712aee533642041862989ec8e32200..9e6aa109a4b4a9e9caa846ed317eaf565885d417 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -12,6 +12,14 @@ describe Gitlab::UsageData do
       create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
       create(:service, project: projects[1], type: 'SlackService', active: true)
       create(:service, project: projects[2], type: 'SlackService', active: true)
+
+      gcp_cluster = create(:cluster, :provided_by_gcp)
+      create(:cluster, :provided_by_user)
+      create(:cluster, :provided_by_user, :disabled)
+      create(:clusters_applications_helm, :installed, cluster: gcp_cluster)
+      create(:clusters_applications_ingress, :installed, cluster: gcp_cluster)
+      create(:clusters_applications_prometheus, :installed, cluster: gcp_cluster)
+      create(:clusters_applications_runner, :installed, cluster: gcp_cluster)
     end
 
     subject { described_class.data }
@@ -64,6 +72,12 @@ describe Gitlab::UsageData do
         clusters
         clusters_enabled
         clusters_disabled
+        clusters_platforms_gke
+        clusters_platforms_user
+        clusters_applications_helm
+        clusters_applications_ingress
+        clusters_applications_prometheus
+        clusters_applications_runner
         in_review_folder
         groups
         issues
@@ -97,6 +111,15 @@ describe Gitlab::UsageData do
       expect(count_data[:projects_jira_active]).to eq(2)
       expect(count_data[:projects_slack_notifications_active]).to eq(2)
       expect(count_data[:projects_slack_slash_active]).to eq(1)
+
+      expect(count_data[:clusters_enabled]).to eq(6)
+      expect(count_data[:clusters_disabled]).to eq(1)
+      expect(count_data[:clusters_platforms_gke]).to eq(1)
+      expect(count_data[:clusters_platforms_user]).to eq(1)
+      expect(count_data[:clusters_applications_helm]).to eq(1)
+      expect(count_data[:clusters_applications_ingress]).to eq(1)
+      expect(count_data[:clusters_applications_prometheus]).to eq(1)
+      expect(count_data[:clusters_applications_runner]).to eq(1)
     end
   end
 
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 1d0faf56f7c13aeba91958ddfb8be5474d7f940e..2b3ffb2d7c0f29d17ca91052f8367c87b0ad14cf 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -55,7 +55,7 @@ describe Gitlab::Workhorse do
       end
     end
 
-    context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do
+    context 'when Gitaly workhorse_archive feature is disabled', :disable_gitaly do
       it 'sets the header correctly' do
         key, command, params = decode_workhorse_header(subject)
 
@@ -100,7 +100,7 @@ describe Gitlab::Workhorse do
       end
     end
 
-    context 'when Gitaly workhorse_send_git_patch feature is disabled', :skip_gitaly_mock do
+    context 'when Gitaly workhorse_send_git_patch feature is disabled', :disable_gitaly do
       it 'sets the header correctly' do
         key, command, params = decode_workhorse_header(subject)
 
@@ -173,7 +173,7 @@ describe Gitlab::Workhorse do
       end
     end
 
-    context 'when Gitaly workhorse_send_git_diff feature is disabled', :skip_gitaly_mock do
+    context 'when Gitaly workhorse_send_git_diff feature is disabled', :disable_gitaly do
       it 'sets the header correctly' do
         key, command, params = decode_workhorse_header(subject)
 
@@ -275,12 +275,14 @@ describe Gitlab::Workhorse do
 
   describe '.git_http_ok' do
     let(:user) { create(:user) }
+    let(:repo_path) { 'ignored but not allowed to be empty in gitlab-workhorse' }
     let(:action) { 'info_refs' }
     let(:params) do
       {
         GL_ID: "user-#{user.id}",
         GL_USERNAME: user.username,
         GL_REPOSITORY: "project-#{project.id}",
+        RepoPath: repo_path,
         ShowAllRefs: false
       }
     end
@@ -295,6 +297,7 @@ describe Gitlab::Workhorse do
           GL_ID: "user-#{user.id}",
           GL_USERNAME: user.username,
           GL_REPOSITORY: "wiki-#{project.id}",
+          RepoPath: repo_path,
           ShowAllRefs: false
         }
       end
@@ -452,7 +455,7 @@ describe Gitlab::Workhorse do
       end
     end
 
-    context 'when Gitaly workhorse_raw_show feature is disabled', :skip_gitaly_mock do
+    context 'when Gitaly workhorse_raw_show feature is disabled', :disable_gitaly do
       it 'sets the header correctly' do
         key, command, params = decode_workhorse_header(subject)
 
diff --git a/spec/mailers/previews/notify_preview.rb b/spec/mailers/previews/notify_preview.rb
index 580f0d56a929aedc48cc11309f1af7794bce605e..43c3c89f1408757345f41efe6b6815c560dfcda6 100644
--- a/spec/mailers/previews/notify_preview.rb
+++ b/spec/mailers/previews/notify_preview.rb
@@ -65,7 +65,7 @@ class NotifyPreview < ActionMailer::Preview
   end
 
   def merge_request
-    @merge_request ||= project.merge_requests.find_by(source_branch: 'master', target_branch: 'feature')
+    @merge_request ||= project.merge_requests.first
   end
 
   def user
diff --git a/spec/models/ci/artifact_blob_spec.rb b/spec/models/ci/artifact_blob_spec.rb
index 4e72d9d748e1bbf60544f0f5a24667c837a30d4d..0014bbcf9f502b2bbed59bb00538dcad04acfa20 100644
--- a/spec/models/ci/artifact_blob_spec.rb
+++ b/spec/models/ci/artifact_blob_spec.rb
@@ -65,6 +65,19 @@ describe Ci::ArtifactBlob do
         expect(url).not_to be_nil
         expect(url).to eq("http://#{project.namespace.path}.#{Gitlab.config.pages.host}/-/#{project.path}/-/jobs/#{build.id}/artifacts/#{path}")
       end
+
+      context 'when port is configured' do
+        let(:port) { 1234 }
+
+        it 'returns an URL with port number' do
+          allow(Gitlab.config.pages).to receive(:url).and_return("#{Gitlab.config.pages.url}:#{port}")
+
+          url = subject.external_url(build.project, build)
+
+          expect(url).not_to be_nil
+          expect(url).to eq("http://#{project.namespace.path}.#{Gitlab.config.pages.host}:#{port}/-/#{project.path}/-/jobs/#{build.id}/artifacts/#{path}")
+        end
+      end
     end
   end
 
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..268561ee9412cbc7c842f8dc2f504334b855032c
--- /dev/null
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe Ci::BuildMetadata do
+  set(:user) { create(:user) }
+  set(:group) { create(:group, :access_requestable) }
+  set(:project) { create(:project, :repository, group: group, build_timeout: 2000) }
+
+  set(:pipeline) do
+    create(:ci_pipeline, project: project,
+                         sha: project.commit.id,
+                         ref: project.default_branch,
+                         status: 'success')
+  end
+
+  let(:build) { create(:ci_build, pipeline: pipeline) }
+  let(:build_metadata) { create(:ci_build_metadata, build: build) }
+
+  describe '#update_timeout_state' do
+    subject { build_metadata }
+
+    context 'when runner is not assigned to the job' do
+      it "doesn't change timeout value" do
+        expect { subject.update_timeout_state }.not_to change { subject.reload.timeout }
+      end
+
+      it "doesn't change timeout_source value" do
+        expect { subject.update_timeout_state }.not_to change { subject.reload.timeout_source }
+      end
+    end
+
+    context 'when runner is assigned to the job' do
+      before do
+        build.update_attributes(runner: runner)
+      end
+
+      context 'when runner timeout is lower than project timeout' do
+        let(:runner) { create(:ci_runner, maximum_timeout: 1900) }
+
+        it 'sets runner timeout' do
+          expect { subject.update_timeout_state }.to change { subject.reload.timeout }.to(1900)
+        end
+
+        it 'sets runner_timeout_source' do
+          expect { subject.update_timeout_state }.to change { subject.reload.timeout_source }.to('runner_timeout_source')
+        end
+      end
+
+      context 'when runner timeout is higher than project timeout' do
+        let(:runner) { create(:ci_runner, maximum_timeout: 2100) }
+
+        it 'sets project timeout' do
+          expect { subject.update_timeout_state }.to change { subject.reload.timeout }.to(2000)
+        end
+
+        it 'sets project_timeout_source' do
+          expect { subject.update_timeout_state }.to change { subject.reload.timeout_source }.to('project_timeout_source')
+        end
+      end
+    end
+  end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 7d935cf8d766239f3cc77122fdcb1a50cbe6d2d3..a12717835b0d2489f7a1ac8b7cb039f7e10af865 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1271,12 +1271,6 @@ describe Ci::Build do
   end
 
   describe 'project settings' do
-    describe '#timeout' do
-      it 'returns project timeout configuration' do
-        expect(build.timeout).to eq(project.build_timeout)
-      end
-    end
-
     describe '#allow_git_fetch' do
       it 'return project allow_git_fetch configuration' do
         expect(build.allow_git_fetch).to eq(project.build_allow_git_fetch)
@@ -1469,24 +1463,24 @@ describe Ci::Build do
     let(:container_registry_enabled) { false }
     let(:predefined_variables) do
       [
+        { key: 'CI_JOB_ID', value: build.id.to_s, public: true },
+        { key: 'CI_JOB_TOKEN', value: build.token, public: false },
+        { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
+        { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
+        { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
+        { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
+        { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
         { key: 'CI', value: 'true', public: true },
         { key: 'GITLAB_CI', value: 'true', public: true },
         { key: 'GITLAB_FEATURES', value: project.namespace.features.join(','), public: true },
         { key: 'CI_SERVER_NAME', value: 'GitLab', public: true },
         { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true },
         { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true },
-        { key: 'CI_JOB_ID', value: build.id.to_s, public: true },
         { key: 'CI_JOB_NAME', value: 'test', public: true },
         { key: 'CI_JOB_STAGE', value: 'test', public: true },
-        { key: 'CI_JOB_TOKEN', value: build.token, public: false },
         { key: 'CI_COMMIT_SHA', value: build.sha, public: true },
         { key: 'CI_COMMIT_REF_NAME', value: build.ref, public: true },
         { key: 'CI_COMMIT_REF_SLUG', value: build.ref_slug, public: true },
-        { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true },
-        { key: 'CI_REGISTRY_PASSWORD', value: build.token, public: false },
-        { key: 'CI_REPOSITORY_URL', value: build.repo_url, public: false },
-        { key: 'CI_BUILD_ID', value: build.id.to_s, public: true },
-        { key: 'CI_BUILD_TOKEN', value: build.token, public: false },
         { key: 'CI_BUILD_REF', value: build.sha, public: true },
         { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true },
         { key: 'CI_BUILD_REF_NAME', value: build.ref, public: true },
@@ -1951,6 +1945,7 @@ describe Ci::Build do
         before do
           allow(build).to receive(:predefined_variables) { [build_pre_var] }
           allow(build).to receive(:yaml_variables) { [build_yaml_var] }
+          allow(build).to receive(:persisted_variables) { [] }
 
           allow_any_instance_of(Project)
             .to receive(:predefined_variables) { [project_pre_var] }
@@ -1999,6 +1994,106 @@ describe Ci::Build do
         end
       end
     end
+
+    context 'when build has not been persisted yet' do
+      let(:build) do
+        described_class.new(
+          name: 'rspec',
+          stage: 'test',
+          ref: 'feature',
+          project: project,
+          pipeline: pipeline
+        )
+      end
+
+      it 'returns static predefined variables' do
+        expect(build.variables.size).to be >= 28
+        expect(build.variables)
+          .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true)
+        expect(build).not_to be_persisted
+      end
+    end
+  end
+
+  describe '#scoped_variables' do
+    context 'when build has not been persisted yet' do
+      let(:build) do
+        described_class.new(
+          name: 'rspec',
+          stage: 'test',
+          ref: 'feature',
+          project: project,
+          pipeline: pipeline
+        )
+      end
+
+      it 'does not persist the build' do
+        expect(build).to be_valid
+        expect(build).not_to be_persisted
+
+        build.scoped_variables
+
+        expect(build).not_to be_persisted
+      end
+
+      it 'returns static predefined variables' do
+        keys = %w[CI_JOB_NAME
+                  CI_COMMIT_SHA
+                  CI_COMMIT_REF_NAME
+                  CI_COMMIT_REF_SLUG
+                  CI_JOB_STAGE]
+
+        variables = build.scoped_variables
+
+        variables.map { |env| env[:key] }.tap do |names|
+          expect(names).to include(*keys)
+        end
+
+        expect(variables)
+          .to include(key: 'CI_COMMIT_REF_NAME', value: 'feature', public: true)
+      end
+
+      it 'does not return prohibited variables' do
+        keys = %w[CI_JOB_ID
+                  CI_JOB_TOKEN
+                  CI_BUILD_ID
+                  CI_BUILD_TOKEN
+                  CI_REGISTRY_USER
+                  CI_REGISTRY_PASSWORD
+                  CI_REPOSITORY_URL
+                  CI_ENVIRONMENT_URL]
+
+        build.scoped_variables.map { |env| env[:key] }.tap do |names|
+          expect(names).not_to include(*keys)
+        end
+      end
+    end
+  end
+
+  describe '#scoped_variables_hash' do
+    context 'when overriding secret variables' do
+      before do
+        project.variables.create!(key: 'MY_VAR', value: 'my value 1')
+        pipeline.variables.create!(key: 'MY_VAR', value: 'my value 2')
+      end
+
+      it 'returns a regular hash created using valid ordering' do
+        expect(build.scoped_variables_hash).to include('MY_VAR': 'my value 2')
+        expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1')
+      end
+    end
+
+    context 'when overriding user-provided variables' do
+      before do
+        pipeline.variables.build(key: 'MY_VAR', value: 'pipeline value')
+        build.yaml_variables = [{ key: 'MY_VAR', value: 'myvar', public: true }]
+      end
+
+      it 'returns a hash including variable with higher precedence' do
+        expect(build.scoped_variables_hash).to include('MY_VAR': 'pipeline value')
+        expect(build.scoped_variables_hash).not_to include('MY_VAR': 'myvar')
+      end
+    end
   end
 
   describe 'state transition: any => [:pending]' do
@@ -2011,6 +2106,70 @@ describe Ci::Build do
     end
   end
 
+  describe 'state transition: pending: :running' do
+    let(:runner) { create(:ci_runner) }
+    let(:job) { create(:ci_build, :pending, runner: runner) }
+
+    before do
+      job.project.update_attribute(:build_timeout, 1800)
+    end
+
+    def run_job_without_exception
+      job.run!
+    rescue StateMachines::InvalidTransition
+    end
+
+    shared_examples 'saves data on transition' do
+      it 'saves timeout' do
+        expect { job.run! }.to change { job.reload.ensure_metadata.timeout }.from(nil).to(expected_timeout)
+      end
+
+      it 'saves timeout_source' do
+        expect { job.run! }.to change { job.reload.ensure_metadata.timeout_source }.from('unknown_timeout_source').to(expected_timeout_source)
+      end
+
+      context 'when Ci::BuildMetadata#update_timeout_state fails update' do
+        before do
+          allow_any_instance_of(Ci::BuildMetadata).to receive(:update_timeout_state).and_return(false)
+        end
+
+        it "doesn't save timeout" do
+          expect { run_job_without_exception }.not_to change { job.reload.ensure_metadata.timeout_source }
+        end
+
+        it "doesn't save timeout_source" do
+          expect { run_job_without_exception }.not_to change { job.reload.ensure_metadata.timeout_source }
+        end
+
+        it 'raises an exception' do
+          expect { job.run! }.to raise_error(StateMachines::InvalidTransition)
+        end
+      end
+    end
+
+    context 'when runner timeout overrides project timeout' do
+      let(:expected_timeout) { 900 }
+      let(:expected_timeout_source) { 'runner_timeout_source' }
+
+      before do
+        runner.update_attribute(:maximum_timeout, 900)
+      end
+
+      it_behaves_like 'saves data on transition'
+    end
+
+    context "when runner timeout doesn't override project timeout" do
+      let(:expected_timeout) { 1800 }
+      let(:expected_timeout_source) { 'project_timeout_source' }
+
+      before do
+        runner.update_attribute(:maximum_timeout, 3600)
+      end
+
+      it_behaves_like 'saves data on transition'
+    end
+  end
+
   describe 'state transition: any => [:running]' do
     shared_examples 'validation is active' do
       context 'when depended job has not been completed yet' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 92f00cfbc1916943226fdf3490536cd38b8ce63f..dd94515b0a454d38e81074ec65ecdaa715ebf5bd 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -346,6 +346,20 @@ describe Ci::Pipeline, :mailer do
           end
         end
       end
+
+      context 'when variables policy is specified' do
+        let(:config) do
+          { unit: { script: 'minitest', only: { variables: ['$CI_PIPELINE_SOURCE'] } },
+            feature: { script: 'spinach', only: { variables: ['$UNDEFINED'] } } }
+        end
+
+        it 'returns stage seeds only when variables expression is truthy' do
+          seeds = pipeline.stage_seeds
+
+          expect(seeds.size).to eq 1
+          expect(seeds.dig(0, 0, :name)).to eq 'unit'
+        end
+      end
     end
 
     describe '#seeds_size' do
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
index ba7bad617b44215bd24abbf8ae4dff8a9ab1e8dd..0eb1e3876e287249713aa9c2d7ce61894bcf41fc 100644
--- a/spec/models/clusters/applications/helm_spec.rb
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -3,6 +3,18 @@ require 'rails_helper'
 describe Clusters::Applications::Helm do
   include_examples 'cluster application core specs', :clusters_applications_helm
 
+  describe '.installed' do
+    subject { described_class.installed }
+
+    let!(:cluster) { create(:clusters_applications_helm, :installed) }
+
+    before do
+      create(:clusters_applications_helm, :errored)
+    end
+
+    it { is_expected.to contain_exactly(cluster) }
+  end
+
   describe '#install_command' do
     let(:helm) { create(:clusters_applications_helm) }
 
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index 03f5b88a5250e7bc9a1923bd6f3cbc7b2adc531f..a47a07d908dc2e7cf7d6dd0e9090bf5d2443059b 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -11,6 +11,18 @@ describe Clusters::Applications::Ingress do
     allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
   end
 
+  describe '.installed' do
+    subject { described_class.installed }
+
+    let!(:cluster) { create(:clusters_applications_ingress, :installed) }
+
+    before do
+      create(:clusters_applications_ingress, :errored)
+    end
+
+    it { is_expected.to contain_exactly(cluster) }
+  end
+
   describe '#make_installed!' do
     before do
       application.make_installed!
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index 2905b58066beeb9df025ecd213c34eebd826d77f..aeca6ee903ad0cb4d7d3cfd536ccb93de332e1e0 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -4,6 +4,18 @@ describe Clusters::Applications::Prometheus do
   include_examples 'cluster application core specs', :clusters_applications_prometheus
   include_examples 'cluster application status specs', :cluster_application_prometheus
 
+  describe '.installed' do
+    subject { described_class.installed }
+
+    let!(:cluster) { create(:clusters_applications_prometheus, :installed) }
+
+    before do
+      create(:clusters_applications_prometheus, :errored)
+    end
+
+    it { is_expected.to contain_exactly(cluster) }
+  end
+
   describe 'transition to installed' do
     let(:project) { create(:project) }
     let(:cluster) { create(:cluster, projects: [project]) }
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
index a574779e39d62f041fa65e728127d4c2db2f5a06..64d995a73c11a03c22d4eaa59813ff0aeb495058 100644
--- a/spec/models/clusters/applications/runner_spec.rb
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -8,6 +8,18 @@ describe Clusters::Applications::Runner do
 
   it { is_expected.to belong_to(:runner) }
 
+  describe '.installed' do
+    subject { described_class.installed }
+
+    let!(:cluster) { create(:clusters_applications_runner, :installed) }
+
+    before do
+      create(:clusters_applications_runner, :errored)
+    end
+
+    it { is_expected.to contain_exactly(cluster) }
+  end
+
   describe '#install_command' do
     let(:kubeclient) { double('kubernetes client') }
     let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 8f12a0e308553ed09a7f24492e5bd998e5b02267..b942554d67b3a13f833a7e04ee4be7b924d1bd9f 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -39,6 +39,42 @@ describe Clusters::Cluster do
     it { is_expected.to contain_exactly(cluster) }
   end
 
+  describe '.user_provided' do
+    subject { described_class.user_provided }
+
+    let!(:cluster) { create(:cluster, :provided_by_user) }
+
+    before do
+      create(:cluster, :provided_by_gcp)
+    end
+
+    it { is_expected.to contain_exactly(cluster) }
+  end
+
+  describe '.gcp_provided' do
+    subject { described_class.gcp_provided }
+
+    let!(:cluster) { create(:cluster, :provided_by_gcp) }
+
+    before do
+      create(:cluster, :provided_by_user)
+    end
+
+    it { is_expected.to contain_exactly(cluster) }
+  end
+
+  describe '.gcp_installed' do
+    subject { described_class.gcp_installed }
+
+    let!(:cluster) { create(:cluster, :provided_by_gcp) }
+
+    before do
+      create(:cluster, :providing_by_gcp)
+    end
+
+    it { is_expected.to contain_exactly(cluster) }
+  end
+
   describe 'validation' do
     subject { cluster.valid? }
 
diff --git a/spec/models/concerns/chronic_duration_attribute_spec.rb b/spec/models/concerns/chronic_duration_attribute_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..27c86e60e60237540da207f79c22fad027c6fe1e
--- /dev/null
+++ b/spec/models/concerns/chronic_duration_attribute_spec.rb
@@ -0,0 +1,115 @@
+require 'spec_helper'
+
+shared_examples 'ChronicDurationAttribute reader' do
+  it 'contains dynamically created reader method' do
+    expect(subject.class).to be_public_method_defined(virtual_field)
+  end
+
+  it 'outputs chronic duration formatted value' do
+    subject.send("#{source_field}=", 120)
+
+    expect(subject.send(virtual_field)).to eq('2m')
+  end
+
+  context 'when value is set to nil' do
+    it 'outputs nil' do
+      subject.send("#{source_field}=", nil)
+
+      expect(subject.send(virtual_field)).to be_nil
+    end
+  end
+end
+
+shared_examples 'ChronicDurationAttribute writer' do
+  it 'contains dynamically created writer method' do
+    expect(subject.class).to be_public_method_defined("#{virtual_field}=")
+  end
+
+  before do
+    subject.send("#{virtual_field}=", '10m')
+  end
+
+  it 'parses chronic duration input' do
+    expect(subject.send(source_field)).to eq(600)
+  end
+
+  it 'passes validation' do
+    expect(subject.valid?).to be_truthy
+  end
+
+  context 'when negative input is used' do
+    before do
+      subject.send("#{source_field}=", 3600)
+    end
+
+    it "doesn't raise exception" do
+      expect { subject.send("#{virtual_field}=", '-10m') }.not_to raise_error(ChronicDuration::DurationParseError)
+    end
+
+    it "doesn't change value" do
+      expect { subject.send("#{virtual_field}=", '-10m') }.not_to change { subject.send(source_field) }
+    end
+
+    it "doesn't pass validation" do
+      subject.send("#{virtual_field}=", '-10m')
+
+      expect(subject.valid?).to be_falsey
+      expect(subject.errors&.messages).to include(virtual_field => ['is not a correct duration'])
+    end
+  end
+
+  context 'when empty input is used' do
+    before do
+      subject.send("#{virtual_field}=", '')
+    end
+
+    it 'writes nil' do
+      expect(subject.send(source_field)).to be_nil
+    end
+
+    it 'passes validation' do
+      expect(subject.valid?).to be_truthy
+    end
+  end
+
+  context 'when nil input is used' do
+    before do
+      subject.send("#{virtual_field}=", nil)
+    end
+
+    it 'writes nil' do
+      expect(subject.send(source_field)).to be_nil
+    end
+
+    it 'passes validation' do
+      expect(subject.valid?).to be_truthy
+    end
+
+    it "doesn't raise exception" do
+      expect { subject.send("#{virtual_field}=", nil) }.not_to raise_error(NoMethodError)
+    end
+  end
+end
+
+describe 'ChronicDurationAttribute' do
+  let(:source_field) {:maximum_timeout}
+  let(:virtual_field) {:maximum_timeout_human_readable}
+
+  subject { Ci::Runner.new }
+
+  it_behaves_like 'ChronicDurationAttribute reader'
+  it_behaves_like 'ChronicDurationAttribute writer'
+end
+
+describe 'ChronicDurationAttribute - reader' do
+  let(:source_field) {:timeout}
+  let(:virtual_field) {:timeout_human_readable}
+
+  subject {Ci::BuildMetadata.new}
+
+  it "doesn't contain dynamically created writer method" do
+    expect(subject.class).not_to be_public_method_defined("#{virtual_field}=")
+  end
+
+  it_behaves_like 'ChronicDurationAttribute reader'
+end
diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb
index 3d7283e216488896133023351d051ab364bbc0e0..41440c6d288d029c70cc687a31e7e427ee188548 100644
--- a/spec/models/deploy_key_spec.rb
+++ b/spec/models/deploy_key_spec.rb
@@ -17,4 +17,25 @@ describe DeployKey, :mailer do
       should_not_email(user)
     end
   end
+
+  describe '#user' do
+    let(:deploy_key) { create(:deploy_key) }
+    let(:user) { create(:user) }
+
+    context 'when user is set' do
+      before do
+        deploy_key.user = user
+      end
+
+      it 'returns the user' do
+        expect(deploy_key.user).to be(user)
+      end
+    end
+
+    context 'when user is not set' do
+      it 'returns the ghost user' do
+        expect(deploy_key.user).to eq(User.ghost)
+      end
+    end
+  end
 end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 1a7a6e035eab0ec6310bb3ad80211123cad4b647..fef868ac0f2b737259a7d44d0d4161eba7063db0 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -224,14 +224,14 @@ describe Project do
       project2 = build(:project, import_url: 'http://localhost:9000/t.git')
 
       expect(project2).to be_invalid
-      expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+      expect(project2.errors[:import_url].first).to include('Requests to localhost are not allowed')
     end
 
     it "does not allow blocked import_url port" do
       project2 = build(:project, import_url: 'http://github.com:25/t.git')
 
       expect(project2).to be_invalid
-      expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+      expect(project2.errors[:import_url].first).to include('Only allowed ports are 22, 80, 443')
     end
 
     describe 'project pending deletion' do
@@ -1265,6 +1265,34 @@ describe Project do
     end
   end
 
+  describe '#pages_group_url' do
+    let(:group) { create :group, name: group_name }
+    let(:project) { create :project, namespace: group, name: project_name }
+    let(:domain) { 'Example.com' }
+    let(:port) { 1234 }
+
+    subject { project.pages_group_url }
+
+    before do
+      allow(Settings.pages).to receive(:host).and_return(domain)
+      allow(Gitlab.config.pages).to receive(:url).and_return("http://example.com:#{port}")
+    end
+
+    context 'group page' do
+      let(:group_name) { 'Group' }
+      let(:project_name) { 'group.example.com' }
+
+      it { is_expected.to eq("http://group.example.com:#{port}") }
+    end
+
+    context 'project page' do
+      let(:group_name) { 'Group' }
+      let(:project_name) { 'Project' }
+
+      it { is_expected.to eq("http://group.example.com:#{port}") }
+    end
+  end
+
   describe '.search' do
     let(:project) { create(:project, description: 'kitten mittens') }
 
@@ -2532,7 +2560,7 @@ describe Project do
     end
   end
 
-  describe '#remove_exports' do
+  describe '#remove_export' do
     let(:legacy_project) { create(:project, :legacy_storage, :with_export) }
     let(:project) { create(:project, :with_export) }
 
@@ -2580,6 +2608,23 @@ describe Project do
     end
   end
 
+  describe '#remove_exported_project_file' do
+    let(:project) { create(:project, :with_export) }
+
+    it 'removes the exported project file' do
+      exported_file = project.export_project_path
+
+      expect(File.exist?(exported_file)).to be_truthy
+
+      allow(FileUtils).to receive(:rm_f).and_call_original
+      expect(FileUtils).to receive(:rm_f).with(exported_file).and_call_original
+
+      project.remove_exported_project_file
+
+      expect(File.exist?(exported_file)).to be_falsy
+    end
+  end
+
   describe '#forks_count' do
     it 'returns the number of forks' do
       project = build(:project)
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 79f25dc4360117a6369745b4f037d0532b42e3c5..83ed3b203e696245b65c697b73f67996fef0416c 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -58,6 +58,21 @@ describe Service do
   end
 
   describe "Template" do
+    describe '.build_from_template' do
+      context 'when template is invalid' do
+        it 'sets service template to inactive when template is invalid' do
+          project = create(:project)
+          template = JiraService.new(template: true, active: true)
+          template.save(validate: false)
+
+          service = described_class.build_from_template(project.id, template)
+
+          expect(service).to be_valid
+          expect(service.active).to be false
+        end
+      end
+    end
+
     describe "for pushover service" do
       let!(:service_template) do
         PushoverService.create(
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index bbfdda23a317dcf612e6d0d3a54adb72aafac892..100418da804741a78c3e40a452a4757c7686e474 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -25,7 +25,7 @@ describe User do
     it { is_expected.to have_many(:group_members) }
     it { is_expected.to have_many(:groups) }
     it { is_expected.to have_many(:keys).dependent(:destroy) }
-    it { is_expected.to have_many(:deploy_keys).dependent(:destroy) }
+    it { is_expected.to have_many(:deploy_keys).dependent(:nullify) }
     it { is_expected.to have_many(:events).dependent(:destroy) }
     it { is_expected.to have_many(:issues).dependent(:destroy) }
     it { is_expected.to have_many(:notes).dependent(:destroy) }
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 852f67db9582ee59e8176985825b194b88509c95..8ad19e3f0f5686e2397536f30c36e406fa0adc2b 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -1141,4 +1141,33 @@ describe API::Commits do
       end
     end
   end
+
+  describe 'GET /projects/:id/repository/commits/:sha/merge_requests' do
+    let!(:project) { create(:project, :repository, :private) }
+    let!(:merged_mr) { create(:merge_request, source_project: project, source_branch: 'master', target_branch: 'feature') }
+    let(:commit) { merged_mr.merge_request_diff.commits.last }
+
+    it 'returns the correct merge request' do
+      get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", user)
+
+      expect(response).to have_gitlab_http_status(200)
+      expect(response).to include_pagination_headers
+      expect(json_response.length).to eq(1)
+      expect(json_response[0]['id']).to eq(merged_mr.id)
+    end
+
+    it 'returns 403 for an unauthorized user' do
+      project.add_guest(user)
+
+      get api("/projects/#{project.id}/repository/commits/#{commit.id}/merge_requests", user)
+
+      expect(response).to have_gitlab_http_status(403)
+    end
+
+    it 'responds 404 when the commit does not exist' do
+      get api("/projects/#{project.id}/repository/commits/a7d26f00c35b/merge_requests", user)
+
+      expect(response).to have_gitlab_http_status(404)
+    end
+  end
 end
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 0772b3f2e6462dccf6a378ddb8d14ef875024086..ae9c0e9c3047b93c5a0598050e1312d5b3cc0973 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -91,6 +91,10 @@ describe API::DeployKeys do
       expect do
         post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
       end.to change { project.deploy_keys.count }.by(1)
+
+      new_key = project.deploy_keys.last
+      expect(new_key.key).to eq(key_attrs[:key])
+      expect(new_key.user).to eq(admin)
     end
 
     it 'returns an existing ssh key when attempting to add a duplicate' do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 3cb90a1b8ef60c2ce013256b7dfa908dd3829ccb..db8c5f963d64aa908e0248d0769694fc1b4522ba 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -251,44 +251,23 @@ describe API::Internal do
       end
 
       context 'with env passed as a JSON' do
-        context 'when relative path envs are not set' do
-          it 'sets env in RequestStore' do
-            expect(Gitlab::Git::Env).to receive(:set).with({
-              'GIT_OBJECT_DIRECTORY' => 'foo',
-              'GIT_ALTERNATE_OBJECT_DIRECTORIES' => 'bar'
-            })
-
-            push(key, project.wiki, env: {
-              GIT_OBJECT_DIRECTORY: 'foo',
-              GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar'
-            }.to_json)
+        let(:gl_repository) { project.gl_repository(is_wiki: true) }
 
-            expect(response).to have_gitlab_http_status(200)
-          end
-        end
+        it 'sets env in RequestStore' do
+          obj_dir_relative = './objects'
+          alt_obj_dirs_relative = ['./alt-objects-1', './alt-objects-2']
 
-        context 'when relative path envs are set' do
-          it 'sets env in RequestStore' do
-            obj_dir_relative = './objects'
-            alt_obj_dirs_relative = ['./alt-objects-1', './alt-objects-2']
-            repo_path = project.wiki.repository.path_to_repo
-
-            expect(Gitlab::Git::Env).to receive(:set).with({
-              'GIT_OBJECT_DIRECTORY' => File.join(repo_path, obj_dir_relative),
-              'GIT_ALTERNATE_OBJECT_DIRECTORIES' => alt_obj_dirs_relative.map { |d| File.join(repo_path, d) },
-              'GIT_OBJECT_DIRECTORY_RELATIVE' => obj_dir_relative,
-              'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => alt_obj_dirs_relative
-            })
-
-            push(key, project.wiki, env: {
-              GIT_OBJECT_DIRECTORY: 'foo',
-              GIT_ALTERNATE_OBJECT_DIRECTORIES: 'bar',
-              GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative,
-              GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative
-            }.to_json)
+          expect(Gitlab::Git::HookEnv).to receive(:set).with(gl_repository, {
+            'GIT_OBJECT_DIRECTORY_RELATIVE' => obj_dir_relative,
+            'GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE' => alt_obj_dirs_relative
+          })
 
-            expect(response).to have_gitlab_http_status(200)
-          end
+          push(key, project.wiki, env: {
+            GIT_OBJECT_DIRECTORY_RELATIVE: obj_dir_relative,
+            GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE: alt_obj_dirs_relative
+          }.to_json)
+
+          expect(response).to have_gitlab_http_status(200)
         end
       end
 
diff --git a/spec/requests/api/project_export_spec.rb b/spec/requests/api/project_export_spec.rb
index 12583109b5960cce300a849520502404cdcc9317..3834d27d0a9b0da56a7e6a7477906ef91cb2d7cb 100644
--- a/spec/requests/api/project_export_spec.rb
+++ b/spec/requests/api/project_export_spec.rb
@@ -5,6 +5,7 @@ describe API::ProjectExport do
   set(:project_none) { create(:project) }
   set(:project_started) { create(:project) }
   set(:project_finished) { create(:project) }
+  set(:project_after_export) { create(:project) }
   set(:user) { create(:user) }
   set(:admin) { create(:admin) }
 
@@ -12,11 +13,13 @@ describe API::ProjectExport do
   let(:path_none) { "/projects/#{project_none.id}/export" }
   let(:path_started) { "/projects/#{project_started.id}/export" }
   let(:path_finished) { "/projects/#{project_finished.id}/export" }
+  let(:path_after_export) { "/projects/#{project_after_export.id}/export" }
 
   let(:download_path) { "/projects/#{project.id}/export/download" }
   let(:download_path_none) { "/projects/#{project_none.id}/export/download" }
   let(:download_path_started) { "/projects/#{project_started.id}/export/download" }
   let(:download_path_finished) { "/projects/#{project_finished.id}/export/download" }
+  let(:download_path_export_action) { "/projects/#{project_after_export.id}/export/download" }
 
   let(:export_path) { "#{Dir.tmpdir}/project_export_spec" }
 
@@ -29,6 +32,11 @@ describe API::ProjectExport do
     # simulate exported
     FileUtils.mkdir_p project_finished.export_path
     FileUtils.touch File.join(project_finished.export_path, '_export.tar.gz')
+
+    # simulate in after export action
+    FileUtils.mkdir_p project_after_export.export_path
+    FileUtils.touch File.join(project_after_export.export_path, '_export.tar.gz')
+    FileUtils.touch Gitlab::ImportExport::AfterExportStrategies::BaseAfterExportStrategy.lock_file_path(project_after_export)
   end
 
   after do
@@ -73,6 +81,14 @@ describe API::ProjectExport do
         expect(json_response['export_status']).to eq('started')
       end
 
+      it 'is after_export' do
+        get api(path_after_export, user)
+
+        expect(response).to have_gitlab_http_status(200)
+        expect(response).to match_response_schema('public_api/v4/project/export_status')
+        expect(json_response['export_status']).to eq('after_export_action')
+      end
+
       it 'is finished' do
         get api(path_finished, user)
 
@@ -99,6 +115,7 @@ describe API::ProjectExport do
           project_none.add_master(user)
           project_started.add_master(user)
           project_finished.add_master(user)
+          project_after_export.add_master(user)
         end
 
         it_behaves_like 'get project export status ok'
@@ -163,6 +180,36 @@ describe API::ProjectExport do
       end
     end
 
+    shared_examples_for 'get project export upload after action' do
+      context 'and is uploading' do
+        it 'downloads' do
+          get api(download_path_export_action, user)
+
+          expect(response).to have_gitlab_http_status(200)
+        end
+      end
+
+      context 'when upload complete' do
+        before do
+          FileUtils.rm_rf(project_after_export.export_path)
+        end
+
+        it_behaves_like '404 response' do
+          let(:request) { get api(download_path_export_action, user) }
+        end
+      end
+    end
+
+    shared_examples_for 'get project download by strategy' do
+      context 'when upload strategy set' do
+        it_behaves_like 'get project export upload after action'
+      end
+
+      context 'when download strategy set' do
+        it_behaves_like 'get project export download'
+      end
+    end
+
     it_behaves_like 'when project export is disabled' do
       let(:request) { get api(download_path, admin) }
     end
@@ -171,7 +218,7 @@ describe API::ProjectExport do
       context 'when user is an admin' do
         let(:user) { admin }
 
-        it_behaves_like 'get project export download'
+        it_behaves_like 'get project download by strategy'
       end
 
       context 'when user is a master' do
@@ -180,9 +227,10 @@ describe API::ProjectExport do
           project_none.add_master(user)
           project_started.add_master(user)
           project_finished.add_master(user)
+          project_after_export.add_master(user)
         end
 
-        it_behaves_like 'get project export download'
+        it_behaves_like 'get project download by strategy'
       end
 
       context 'when user is a developer' do
@@ -229,10 +277,30 @@ describe API::ProjectExport do
     end
 
     shared_examples_for 'post project export start' do
-      it 'starts' do
-        post api(path, user)
+      context 'with upload strategy' do
+        context 'when params invalid' do
+          it_behaves_like '400 response' do
+            let(:request) { post(api(path, user), 'upload[url]' => 'whatever') }
+          end
+        end
+
+        it 'starts' do
+          allow_any_instance_of(Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy).to receive(:send_file)
+
+          post(api(path, user), 'upload[url]' => 'http://gitlab.com')
 
-        expect(response).to have_gitlab_http_status(202)
+          expect(response).to have_gitlab_http_status(202)
+        end
+      end
+
+      context 'with download strategy' do
+        it 'starts' do
+          expect_any_instance_of(Gitlab::ImportExport::AfterExportStrategies::WebUploadStrategy).not_to receive(:send_file)
+
+          post api(path, user)
+
+          expect(response).to have_gitlab_http_status(202)
+        end
       end
     end
 
@@ -253,6 +321,7 @@ describe API::ProjectExport do
           project_none.add_master(user)
           project_started.add_master(user)
           project_finished.add_master(user)
+          project_after_export.add_master(user)
         end
 
         it_behaves_like 'post project export start'
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index f3dd121faa9842d087506be942e36e5aa1d182a3..5084b36c7616951c8ebbe6e2cc7871bd2eb4f930 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -109,6 +109,26 @@ describe API::Runner do
         end
       end
 
+      context 'when maximum job timeout is specified' do
+        it 'creates runner' do
+          post api('/runners'), token: registration_token,
+                                maximum_timeout: 9000
+
+          expect(response).to have_gitlab_http_status 201
+          expect(Ci::Runner.first.maximum_timeout).to eq(9000)
+        end
+
+        context 'when maximum job timeout is empty' do
+          it 'creates runner' do
+            post api('/runners'), token: registration_token,
+                                  maximum_timeout: ''
+
+            expect(response).to have_gitlab_http_status 201
+            expect(Ci::Runner.first.maximum_timeout).to be_nil
+          end
+        end
+      end
+
       %w(name version revision platform architecture).each do |param|
         context "when info parameter '#{param}' info is present" do
           let(:value) { "#{param}_value" }
@@ -340,12 +360,12 @@ describe API::Runner do
           let(:expected_steps) do
             [{ 'name' => 'script',
                'script' => %w(ls date),
-               'timeout' => job.timeout,
+               'timeout' => job.metadata_timeout,
                'when' => 'on_success',
                'allow_failure' => false },
              { 'name' => 'after_script',
                'script' => %w(ls date),
-               'timeout' => job.timeout,
+               'timeout' => job.metadata_timeout,
                'when' => 'always',
                'allow_failure' => true }]
           end
@@ -648,6 +668,41 @@ describe API::Runner do
               end
             end
           end
+
+          describe 'timeout support' do
+            context 'when project specifies job timeout' do
+              let(:project) { create(:project, shared_runners_enabled: false, build_timeout: 1234) }
+
+              it 'contains info about timeout taken from project' do
+                request_job
+
+                expect(response).to have_gitlab_http_status(201)
+                expect(json_response['runner_info']).to include({ 'timeout' => 1234 })
+              end
+
+              context 'when runner specifies lower timeout' do
+                let(:runner) { create(:ci_runner, maximum_timeout: 1000) }
+
+                it 'contains info about timeout overridden by runner' do
+                  request_job
+
+                  expect(response).to have_gitlab_http_status(201)
+                  expect(json_response['runner_info']).to include({ 'timeout' => 1000 })
+                end
+              end
+
+              context 'when runner specifies bigger timeout' do
+                let(:runner) { create(:ci_runner, maximum_timeout: 2000) }
+
+                it 'contains info about timeout not overridden by runner' do
+                  request_job
+
+                  expect(response).to have_gitlab_http_status(201)
+                  expect(json_response['runner_info']).to include({ 'timeout' => 1234 })
+                end
+              end
+            end
+          end
         end
 
         def request_job(token = runner.token, **params)
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index ec5cad4f4fd66a50e9f16f66e42cc74462daf8fb..d30f0cf36e2a0486a8d0fb388cd30941960235e7 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -123,6 +123,7 @@ describe API::Runners do
 
           expect(response).to have_gitlab_http_status(200)
           expect(json_response['description']).to eq(shared_runner.description)
+          expect(json_response['maximum_timeout']).to be_nil
         end
       end
 
@@ -192,7 +193,8 @@ describe API::Runners do
                                                  tag_list: ['ruby2.1', 'pgsql', 'mysql'],
                                                  run_untagged: 'false',
                                                  locked: 'true',
-                                                 access_level: 'ref_protected')
+                                                 access_level: 'ref_protected',
+                                                 maximum_timeout: 1234)
           shared_runner.reload
 
           expect(response).to have_gitlab_http_status(200)
@@ -204,6 +206,7 @@ describe API::Runners do
           expect(shared_runner.ref_protected?).to be_truthy
           expect(shared_runner.ensure_runner_queue_value)
             .not_to eq(runner_queue_value)
+          expect(shared_runner.maximum_timeout).to eq(1234)
         end
       end
 
diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb
index 16431ed4188d2b96ca545118a6498205e333d3aa..70402bac2e2c37ae8acff343a8645b6a76f30d29 100644
--- a/spec/serializers/status_entity_spec.rb
+++ b/spec/serializers/status_entity_spec.rb
@@ -25,5 +25,10 @@ describe StatusEntity do
       allow(Rails.env).to receive(:development?) { true }
       expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/dev/favicon_status_success.ico')
     end
+
+    it 'contains a canary namespaced favicon if canary env' do
+      stub_env('CANARY', 'true')
+      expect(entity.as_json[:favicon]).to match_asset_path('/assets/ci_favicons/canary/favicon_status_success.ico')
+    end
   end
 end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index b86a3d72bb454fa2f589ecb571f66e3aa96de0dd..8de0bdf92e2877bacb98a8911c50e82a312faeb1 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -29,7 +29,8 @@ describe Ci::RetryBuildService do
        commit_id deployments erased_by_id last_deployment project_id
        runner_id tag_taggings taggings tags trigger_request_id
        user_id auto_canceled_by_id retried failure_reason
-       artifacts_file_store artifacts_metadata_store].freeze
+       artifacts_file_store artifacts_metadata_store
+       metadata].freeze
 
   shared_examples 'build duplication' do
     let(:another_pipeline) { create(:ci_empty_pipeline, project: project) }
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 47c1ebbeb812c1409664daf8ecb58dc9a4738b6f..7ae49c0689689bb989f9117310e8f2f63e53b839 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -67,6 +67,10 @@ describe Issues::CloseService do
         expect(issue).to be_closed
       end
 
+      it 'records closed user' do
+        expect(issue.closed_by_id).to be(user.id)
+      end
+
       it 'sends email to user2 about assign of new issue' do
         email = ActionMailer::Base.deliveries.last
         expect(email.to.first).to eq(user2.email)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 4413c6ef83ea63a28060a15a800879cc47d42c1c..2cacb97a2935234fe6fa3332084b5893e0ccbb97 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -70,6 +70,16 @@ describe Projects::CreateService, '#execute' do
       opts[:default_branch] = 'master'
       expect(create_project(user, opts)).to eq(nil)
     end
+
+    it 'sets invalid service as inactive' do
+      create(:service, type: 'JiraService', project: nil, template: true, active: true)
+
+      project = create_project(user, opts)
+      service = project.services.first
+
+      expect(project).to be_persisted
+      expect(service.active).to be false
+    end
   end
 
   context 'wiki_enabled creates repository directory' do
@@ -232,14 +242,15 @@ describe Projects::CreateService, '#execute' do
   end
 
   context 'when a bad service template is created' do
-    it 'reports an error in the imported project' do
+    it 'sets service to be inactive' do
       opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-ce'
       create(:service, type: 'DroneCiService', project: nil, template: true, active: true)
 
       project = create_project(user, opts)
+      service = project.services.first
 
-      expect(project.errors.full_messages_for(:base).first).to match(/Unable to save project. Error: Unable to save DroneCiService/)
-      expect(project.services.count).to eq 0
+      expect(project).to be_persisted
+      expect(service.active).to be false
     end
   end
 
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..51491c7d5292d0c73fd519cb0933515a1a784988
--- /dev/null
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe Projects::ImportExport::ExportService do
+  describe '#execute' do
+    let!(:user) { create(:user) }
+    let(:project) { create(:project) }
+    let(:shared) { project.import_export_shared }
+    let(:service) { described_class.new(project, user) }
+    let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
+
+    context 'when all saver services succeed' do
+      before do
+        allow(service).to receive(:save_services).and_return(true)
+      end
+
+      it 'saves the project in the file system' do
+        expect(Gitlab::ImportExport::Saver).to receive(:save).with(project: project, shared: shared)
+
+        service.execute
+      end
+
+      it 'calls the after export strategy' do
+        expect(after_export_strategy).to receive(:execute)
+
+        service.execute(after_export_strategy)
+      end
+
+      context 'when after export strategy fails' do
+        before do
+          allow(after_export_strategy).to receive(:execute).and_return(false)
+        end
+
+        after do
+          service.execute(after_export_strategy)
+        end
+
+        it 'removes the remaining exported data' do
+          allow(shared).to receive(:export_path).and_return('whatever')
+          allow(FileUtils).to receive(:rm_rf)
+
+          expect(FileUtils).to receive(:rm_rf).with(shared.export_path)
+        end
+
+        it 'notifies the user' do
+          expect_any_instance_of(NotificationService).to receive(:project_not_exported)
+        end
+
+        it 'notifies logger' do
+          allow(Rails.logger).to receive(:error)
+
+          expect(Rails.logger).to receive(:error)
+        end
+      end
+    end
+
+    context 'when saver services fail' do
+      before do
+        allow(service).to receive(:save_services).and_return(false)
+      end
+
+      after do
+        expect { service.execute }.to raise_error(Gitlab::ImportExport::Error)
+      end
+
+      it 'removes the remaining exported data' do
+        allow(shared).to receive(:export_path).and_return('whatever')
+        allow(FileUtils).to receive(:rm_rf)
+
+        expect(FileUtils).to receive(:rm_rf).with(shared.export_path)
+      end
+
+      it 'notifies the user' do
+        expect_any_instance_of(NotificationService).to receive(:project_not_exported)
+      end
+
+      it 'notifies logger' do
+        expect(Rails.logger).to receive(:error)
+      end
+
+      it 'the after export strategy is not called' do
+        expect(service).not_to receive(:execute_after_export_action)
+      end
+    end
+  end
+end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index bf7facaec99fda214341314a3fed0096d7c3c516..30c89ebd82159adc22e4a1e41174b57d0de21ebb 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -156,7 +156,7 @@ describe Projects::ImportService do
         result = described_class.new(project, user).execute
 
         expect(result[:status]).to eq :error
-        expect(result[:message]).to end_with 'Blocked import URL.'
+        expect(result[:message]).to include('Requests to localhost are not allowed')
       end
 
       it 'fails with port 25' do
@@ -165,7 +165,7 @@ describe Projects::ImportService do
         result = described_class.new(project, user).execute
 
         expect(result[:status]).to eq :error
-        expect(result[:message]).to end_with 'Blocked import URL.'
+        expect(result[:message]).to include('Only allowed ports are 22, 80, 443')
       end
     end
 
diff --git a/spec/support/cookie_helper.rb b/spec/support/cookie_helper.rb
index d72925e1838a690c959f90dded1bba878068e64b..5ff7b0b68c9268bf0868c22a5b9c5bfe9f30ccfa 100644
--- a/spec/support/cookie_helper.rb
+++ b/spec/support/cookie_helper.rb
@@ -2,12 +2,25 @@
 #
 module CookieHelper
   def set_cookie(name, value, options = {})
+    case page.driver
+    when Capybara::RackTest::Driver
+      rack_set_cookie(name, value)
+    else
+      selenium_set_cookie(name, value, options)
+    end
+  end
+
+  def selenium_set_cookie(name, value, options = {})
     # Selenium driver will not set cookies for a given domain when the browser is at `about:blank`.
     # It also doesn't appear to allow overriding the cookie path. loading `/` is the most inclusive.
     visit options.fetch(:path, '/') unless on_a_page?
     page.driver.browser.manage.add_cookie(name: name, value: value, **options)
   end
 
+  def rack_set_cookie(name, value)
+    page.driver.browser.set_cookie("#{name}=#{value}")
+  end
+
   def get_cookie(name)
     page.driver.browser.manage.cookie_named(name)
   end
diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb
index c7e8a39a6170dbc24c0984ec1d0f5a11e81c7858..9cf541372b5118f7d4c91fe29331d2f30682c27b 100644
--- a/spec/support/gitaly.rb
+++ b/spec/support/gitaly.rb
@@ -1,11 +1,13 @@
 RSpec.configure do |config|
   config.before(:each) do |example|
     if example.metadata[:disable_gitaly]
-      allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(false)
+      # Use 'and_wrap_original' to make sure the arguments are valid
+      allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) && false }
     else
       next if example.metadata[:skip_gitaly_mock]
 
-      allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+      # Use 'and_wrap_original' to make sure the arguments are valid
+      allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_wrap_original { |m, *args| m.call(*args) || true }
     end
   end
 end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index d08183846a02c1b02e9a584f5ae28ce8284e094b..db34090e971b54eb19e95acb3ef93419a0a6339e 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -140,6 +140,10 @@ module LoginHelpers
     end
     allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config)
     stub_omniauth_setting(messages)
+    stub_saml_authorize_path_helpers
+  end
+
+  def stub_saml_authorize_path_helpers
     allow_any_instance_of(Object).to receive(:user_saml_omniauth_authorize_path).and_return('/users/auth/saml')
     allow_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml')
   end
diff --git a/spec/support/migrations_helpers.rb b/spec/support/migrations_helpers.rb
index 6bf976a2cf9a857d0c7f623bb90b02593428ca87..5d6f662e8fe93e164063abbf4d3674cb9c2f6dcb 100644
--- a/spec/support/migrations_helpers.rb
+++ b/spec/support/migrations_helpers.rb
@@ -1,6 +1,9 @@
 module MigrationsHelpers
   def table(name)
-    Class.new(ActiveRecord::Base) { self.table_name = name }
+    Class.new(ActiveRecord::Base) do
+      self.table_name = name
+      self.inheritance_column = :_type_disabled
+    end
   end
 
   def migrations_paths
diff --git a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
index cd9974cd6e2b3a518900e3c291c0d98a73dbe982..6352f1527cdc9fb2283fc1e87c677fe68efede92 100644
--- a/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
+++ b/spec/support/shared_examples/uploaders/object_storage_shared_examples.rb
@@ -61,12 +61,18 @@ shared_examples "migrates" do |to_store:, from_store: nil|
     expect { migrate(to) }.not_to change { file.exists? }
   end
 
-  context 'when migrate! is not oqqupied by another process' do
+  context 'when migrate! is not occupied by another process' do
     it 'executes migrate!' do
       expect(subject).to receive(:object_store=).at_least(1)
 
       migrate(to)
     end
+
+    it 'executes use_file' do
+      expect(subject).to receive(:unsafe_use_file).once
+
+      subject.use_file
+    end
   end
 
   context 'when migrate! is occupied by another process' do
@@ -79,7 +85,13 @@ shared_examples "migrates" do |to_store:, from_store: nil|
     it 'does not execute migrate!' do
       expect(subject).not_to receive(:unsafe_migrate!)
 
-      expect { migrate(to) }.to raise_error('Already running')
+      expect { migrate(to) }.to raise_error('exclusive lease already taken')
+    end
+
+    it 'does not execute use_file' do
+      expect(subject).not_to receive(:unsafe_use_file)
+
+      expect { subject.use_file }.to raise_error('exclusive lease already taken')
     end
 
     after do
diff --git a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
index b778d26060da3d159da015330d1127980510b4b2..6fcfae358ec018ec6118755cf324b4caf338c2dc 100644
--- a/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
+++ b/spec/tasks/gitlab/uploads/migrate_rake_spec.rb
@@ -1,10 +1,9 @@
 require 'rake_helper'
 
 describe 'gitlab:uploads:migrate rake tasks' do
-  let!(:projects) { create_list(:project, 10, :with_avatar) }
-  let(:model_class) { Project }
-  let(:uploader_class) { AvatarUploader }
-  let(:mounted_as) { :avatar }
+  let(:model_class) { nil }
+  let(:uploader_class) { nil }
+  let(:mounted_as) { nil }
   let(:batch_size) { 3 }
 
   before do
@@ -20,9 +19,125 @@ describe 'gitlab:uploads:migrate rake tasks' do
     run_rake_task("gitlab:uploads:migrate", *args)
   end
 
-  it 'enqueue jobs in batch' do
-    expect(ObjectStorage::MigrateUploadsWorker).to receive(:enqueue!).exactly(4).times
+  shared_examples 'enqueue jobs in batch' do |batch:|
+    it do
+      expect(ObjectStorage::MigrateUploadsWorker)
+        .to receive(:perform_async).exactly(batch).times
+              .and_return("A fake job.")
 
-    run
+      run
+    end
+  end
+
+  context "for AvatarUploader" do
+    let(:uploader_class) { AvatarUploader }
+    let(:mounted_as) { :avatar }
+
+    context "for Project" do
+      let(:model_class) { Project }
+      let!(:projects) { create_list(:project, 10, :with_avatar) }
+
+      it_behaves_like 'enqueue jobs in batch', batch: 4
+
+      context 'Upload has store = nil' do
+        before do
+          Upload.where(model: projects).update_all(store: nil)
+        end
+
+        it_behaves_like 'enqueue jobs in batch', batch: 4
+      end
+    end
+
+    context "for Group" do
+      let(:model_class) { Group }
+
+      before do
+        create_list(:group, 10, :with_avatar)
+      end
+
+      it_behaves_like 'enqueue jobs in batch', batch: 4
+    end
+
+    context "for User" do
+      let(:model_class) { User }
+
+      before do
+        create_list(:user, 10, :with_avatar)
+      end
+
+      it_behaves_like 'enqueue jobs in batch', batch: 4
+    end
+  end
+
+  context "for AttachmentUploader" do
+    let(:uploader_class) { AttachmentUploader }
+
+    context "for Note" do
+      let(:model_class) { Note }
+      let(:mounted_as) { :attachment }
+
+      before do
+        create_list(:note, 10, :with_attachment)
+      end
+
+      it_behaves_like 'enqueue jobs in batch', batch: 4
+    end
+
+    context "for Appearance" do
+      let(:model_class) { Appearance }
+      let(:mounted_as) { :logo }
+
+      before do
+        create(:appearance, :with_logos)
+      end
+
+      %i(logo header_logo).each do |mount|
+        it_behaves_like 'enqueue jobs in batch', batch: 1 do
+          let(:mounted_as) { mount }
+        end
+      end
+    end
+  end
+
+  context "for FileUploader" do
+    let(:uploader_class) { FileUploader }
+    let(:model_class) { Project }
+
+    before do
+      create_list(:project, 10) do |model|
+        uploader_class.new(model)
+          .store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
+      end
+    end
+
+    it_behaves_like 'enqueue jobs in batch', batch: 4
+  end
+
+  context "for PersonalFileUploader" do
+    let(:uploader_class) { PersonalFileUploader }
+    let(:model_class) { PersonalSnippet }
+
+    before do
+      create_list(:personal_snippet, 10) do |model|
+        uploader_class.new(model)
+          .store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
+      end
+    end
+
+    it_behaves_like 'enqueue jobs in batch', batch: 4
+  end
+
+  context "for NamespaceFileUploader" do
+    let(:uploader_class) { NamespaceFileUploader }
+    let(:model_class) { Snippet }
+
+    before do
+      create_list(:snippet, 10) do |model|
+        uploader_class.new(model)
+          .store!(fixture_file_upload('spec/fixtures/doc_sample.txt'))
+      end
+    end
+
+    it_behaves_like 'enqueue jobs in batch', batch: 4
   end
 end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index 1d406c7195574b26c0e36ed703e9f3ff8e608e79..59e02fecbcee73eb6bf020cd58b6c5b59f135263 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -308,6 +308,30 @@ describe ObjectStorage do
     it { is_expected.to eq(remote_directory) }
   end
 
+  context 'when file is in use' do
+    def when_file_is_in_use
+      uploader.use_file do
+        yield
+      end
+    end
+
+    it 'cannot migrate' do
+      when_file_is_in_use do
+        expect(uploader).not_to receive(:unsafe_migrate!)
+
+        expect { uploader.migrate!(described_class::Store::REMOTE) }.to raise_error('exclusive lease already taken')
+      end
+    end
+
+    it 'cannot use_file' do
+      when_file_is_in_use do
+        expect(uploader).not_to receive(:unsafe_use_file)
+
+        expect { uploader.use_file }.to raise_error('exclusive lease already taken')
+      end
+    end
+  end
+
   describe '#fog_credentials' do
     let(:connection) { Settingslogic.new("provider" => "AWS") }
 
diff --git a/spec/uploaders/workers/object_storage/background_move_worker_spec.rb b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b34f427fd8a3fb793949d96ba96eb0f79bac782d
--- /dev/null
+++ b/spec/uploaders/workers/object_storage/background_move_worker_spec.rb
@@ -0,0 +1,146 @@
+require 'spec_helper'
+
+describe ObjectStorage::BackgroundMoveWorker do
+  let(:local) { ObjectStorage::Store::LOCAL }
+  let(:remote) { ObjectStorage::Store::REMOTE }
+
+  def perform
+    described_class.perform_async(uploader_class.name, subject_class, file_field, subject_id)
+  end
+
+  context 'for LFS' do
+    let!(:lfs_object) { create(:lfs_object, :with_file, file_store: local) }
+    let(:uploader_class) { LfsObjectUploader }
+    let(:subject_class) { LfsObject }
+    let(:file_field) { :file }
+    let(:subject_id) { lfs_object.id }
+
+    context 'when object storage is enabled' do
+      before do
+        stub_lfs_object_storage(background_upload: true)
+      end
+
+      it 'uploads object to storage' do
+        expect { perform }.to change { lfs_object.reload.file_store }.from(local).to(remote)
+      end
+
+      context 'when background upload is disabled' do
+        before do
+          allow(Gitlab.config.lfs.object_store).to receive(:background_upload) { false }
+        end
+
+        it 'is skipped' do
+          expect { perform }.not_to change { lfs_object.reload.file_store }
+        end
+      end
+    end
+
+    context 'when object storage is disabled' do
+      before do
+        stub_lfs_object_storage(enabled: false)
+      end
+
+      it "doesn't migrate files" do
+        perform
+
+        expect(lfs_object.reload.file_store).to eq(local)
+      end
+    end
+  end
+
+  context 'for legacy artifacts' do
+    let(:build) { create(:ci_build, :legacy_artifacts) }
+    let(:uploader_class) { LegacyArtifactUploader }
+    let(:subject_class) { Ci::Build }
+    let(:file_field) { :artifacts_file }
+    let(:subject_id) { build.id }
+
+    context 'when local storage is used' do
+      let(:store) { local }
+
+      context 'and remote storage is defined' do
+        before do
+          stub_artifacts_object_storage(background_upload: true)
+        end
+
+        it "migrates file to remote storage" do
+          perform
+
+          expect(build.reload.artifacts_file_store).to eq(remote)
+        end
+
+        context 'for artifacts_metadata' do
+          let(:file_field) { :artifacts_metadata }
+
+          it 'migrates metadata to remote storage' do
+            perform
+
+            expect(build.reload.artifacts_metadata_store).to eq(remote)
+          end
+        end
+      end
+    end
+  end
+
+  context 'for job artifacts' do
+    let(:artifact) { create(:ci_job_artifact, :archive) }
+    let(:uploader_class) { JobArtifactUploader }
+    let(:subject_class) { Ci::JobArtifact }
+    let(:file_field) { :file }
+    let(:subject_id) { artifact.id }
+
+    context 'when local storage is used' do
+      let(:store) { local }
+
+      context 'and remote storage is defined' do
+        before do
+          stub_artifacts_object_storage(background_upload: true)
+        end
+
+        it "migrates file to remote storage" do
+          perform
+
+          expect(artifact.reload.file_store).to eq(remote)
+        end
+      end
+    end
+  end
+
+  context 'for uploads' do
+    let!(:project) { create(:project, :with_avatar) }
+    let(:uploader_class) { AvatarUploader }
+    let(:file_field) { :avatar }
+
+    context 'when local storage is used' do
+      let(:store) { local }
+
+      context 'and remote storage is defined' do
+        before do
+          stub_uploads_object_storage(uploader_class, background_upload: true)
+        end
+
+        describe 'supports using the model' do
+          let(:subject_class) { project.class }
+          let(:subject_id) { project.id }
+
+          it "migrates file to remote storage" do
+            perform
+
+            expect(project.reload.avatar.file_storage?).to be_falsey
+          end
+        end
+
+        describe 'supports using the Upload' do
+          let(:subject_class) { Upload }
+          let(:subject_id) { project.avatar.upload.id }
+
+          it "migrates file to remote storage" do
+            perform
+
+            expect(project.reload.avatar.file_storage?).to be_falsey
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7a7dcb716804b1304456e2ea25905fce171e73c7
--- /dev/null
+++ b/spec/uploaders/workers/object_storage/migrate_uploads_worker_spec.rb
@@ -0,0 +1,119 @@
+require 'spec_helper'
+
+describe ObjectStorage::MigrateUploadsWorker, :sidekiq do
+  shared_context 'sanity_check! fails' do
+    before do
+      expect(described_class).to receive(:sanity_check!).and_raise(described_class::SanityCheckError)
+    end
+  end
+
+  let!(:projects) { create_list(:project, 10, :with_avatar) }
+  let(:uploads) { Upload.all }
+  let(:model_class) { Project }
+  let(:mounted_as) { :avatar }
+  let(:to_store) { ObjectStorage::Store::REMOTE }
+
+  before do
+    stub_uploads_object_storage(AvatarUploader)
+  end
+
+  describe '.enqueue!' do
+    def enqueue!
+      described_class.enqueue!(uploads, Project, mounted_as, to_store)
+    end
+
+    it 'is guarded by .sanity_check!' do
+      expect(described_class).to receive(:perform_async)
+      expect(described_class).to receive(:sanity_check!)
+
+      enqueue!
+    end
+
+    context 'sanity_check! fails' do
+      include_context 'sanity_check! fails'
+
+      it 'does not enqueue a job' do
+        expect(described_class).not_to receive(:perform_async)
+
+        expect { enqueue! }.to raise_error(described_class::SanityCheckError)
+      end
+    end
+  end
+
+  describe '.sanity_check!' do
+    shared_examples 'raises a SanityCheckError' do
+      let(:mount_point) { nil }
+
+      it do
+        expect { described_class.sanity_check!(uploads, model_class, mount_point) }
+          .to raise_error(described_class::SanityCheckError)
+      end
+    end
+
+    context 'uploader types mismatch' do
+      let!(:outlier) { create(:upload, uploader: 'FileUploader') }
+
+      include_examples 'raises a SanityCheckError'
+    end
+
+    context 'model types mismatch' do
+      let!(:outlier) { create(:upload, model_type: 'Potato') }
+
+      include_examples 'raises a SanityCheckError'
+    end
+
+    context 'mount point not found' do
+      include_examples 'raises a SanityCheckError' do
+        let(:mount_point) { :potato }
+      end
+    end
+  end
+
+  describe '#perform' do
+    def perform
+      described_class.new.perform(uploads.ids, model_class.to_s, mounted_as, to_store)
+    rescue ObjectStorage::MigrateUploadsWorker::Report::MigrationFailures
+      # swallow
+    end
+
+    shared_examples 'outputs correctly' do |success: 0, failures: 0|
+      total = success + failures
+
+      if success > 0
+        it 'outputs the reports' do
+          expect(Rails.logger).to receive(:info).with(%r{Migrated #{success}/#{total} files})
+
+          perform
+        end
+      end
+
+      if failures > 0
+        it 'outputs upload failures' do
+          expect(Rails.logger).to receive(:warn).with(/Error .* I am a teapot/)
+
+          perform
+        end
+      end
+    end
+
+    it_behaves_like 'outputs correctly', success: 10
+
+    it 'migrates files' do
+      perform
+
+      aggregate_failures do
+        projects.each do |project|
+          expect(project.reload.avatar.upload.local?).to be_falsey
+        end
+      end
+    end
+
+    context 'migration is unsuccessful' do
+      before do
+        allow_any_instance_of(ObjectStorage::Concern).to receive(:migrate!).and_raise(CarrierWave::UploadError, "I am a teapot.")
+      end
+
+      it_behaves_like 'outputs correctly', failures: 10
+    end
+  end
+end
diff --git a/spec/workers/project_export_worker_spec.rb b/spec/workers/project_export_worker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8899969c178b2d610ad57ff7b9c53f972e650945
--- /dev/null
+++ b/spec/workers/project_export_worker_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe ProjectExportWorker do
+  let!(:user) { create(:user) }
+  let!(:project) { create(:project) }
+
+  subject { described_class.new }
+
+  describe '#perform' do
+    context 'when it succeeds' do
+      it 'calls the ExportService' do
+        expect_any_instance_of(::Projects::ImportExport::ExportService).to receive(:execute)
+
+        subject.perform(user.id, project.id, { 'klass' => 'Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy' })
+      end
+    end
+
+    context 'when it fails' do
+      it 'raises an exception when params are invalid' do
+        expect_any_instance_of(::Projects::ImportExport::ExportService).not_to receive(:execute)
+
+        expect { subject.perform(1234, project.id, {}) }.to raise_exception(ActiveRecord::RecordNotFound)
+        expect { subject.perform(user.id, 1234, {}) }.to raise_exception(ActiveRecord::RecordNotFound)
+        expect { subject.perform(user.id, project.id, { 'klass' => 'Whatever' }) }.to raise_exception(Gitlab::ImportExport::AfterExportStrategyBuilder::StrategyNotFoundError)
+      end
+    end
+  end
+end
diff --git a/yarn.lock b/yarn.lock
index 584951b5da06f22106fde0dd2c5204d95aaea33e..af7bda5d56286bb9fc77388b54bb78cc183d791b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3605,7 +3605,7 @@ fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
 
-fsevents@^1.0.0, fsevents@^1.1.3:
+fsevents@^1.0.0:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.1.3.tgz#11f82318f5fe7bb2cd22965a108e9306208216d8"
   dependencies: