diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4890738aa3db223f46256f5183e028602db96b77..90aabf5034ac52967d438b90b709f3aef602cabb 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -78,6 +78,19 @@ stages:
     - mysql:latest
     - redis:alpine
 
+.rails5-variables: &rails5-variables
+  script:
+    - export RAILS5=${RAILS5}
+    - export BUNDLE_GEMFILE=${BUNDLE_GEMFILE}
+
+.rails5: &rails5
+  allow_failure: true
+  only:
+    - /rails5/
+  variables:
+    BUNDLE_GEMFILE: "Gemfile.rails5"
+    RAILS5: "true"
+
 # Skip all jobs except the ones that begin with 'docs/'.
 # Used for commits including ONLY documentation changes.
 # https://docs.gitlab.com/ce/development/writing_documentation.html#testing
@@ -118,6 +131,7 @@ stages:
   <<: *dedicated-runner
   <<: *except-docs-and-qa
   <<: *pull-cache
+  <<: *rails5-variables
   stage: test
   script:
     - JOB_NAME=( $CI_JOB_NAME )
@@ -148,14 +162,23 @@ stages:
   <<: *rspec-metadata
   <<: *use-pg
 
+.rspec-metadata-pg-rails5: &rspec-metadata-pg-rails5
+  <<: *rspec-metadata-pg
+  <<: *rails5
+
 .rspec-metadata-mysql: &rspec-metadata-mysql
   <<: *rspec-metadata
   <<: *use-mysql
 
+.rspec-metadata-mysql-rails5: &rspec-metadata-mysql-rails5
+  <<: *rspec-metadata-mysql
+  <<: *rails5
+
 .spinach-metadata: &spinach-metadata
   <<: *dedicated-runner
   <<: *except-docs-and-qa
   <<: *pull-cache
+  <<: *rails5-variables
   stage: test
   script:
     - JOB_NAME=( $CI_JOB_NAME )
@@ -179,10 +202,18 @@ stages:
   <<: *spinach-metadata
   <<: *use-pg
 
+.spinach-metadata-pg-rails5: &spinach-metadata-pg-rails5
+  <<: *spinach-metadata-pg
+  <<: *rails5
+
 .spinach-metadata-mysql: &spinach-metadata-mysql
   <<: *spinach-metadata
   <<: *use-mysql
 
+.spinach-metadata-mysql-rails5: &spinach-metadata-mysql-rails5
+  <<: *spinach-metadata-mysql
+  <<: *rails5
+
 .only-canonical-masters: &only-canonical-masters
   only:
     - master@gitlab-org/gitlab-ce
@@ -266,12 +297,13 @@ package-and-qa:
   when: manual
   variables:
     GIT_STRATEGY: none
+  retry: 0
   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
+    - wget https://gitlab.com/$CI_PROJECT_PATH/raw/$CI_COMMIT_SHA/scripts/trigger-build-omnibus
     - chmod 755 trigger-build-omnibus
   script:
     - ./trigger-build-omnibus
@@ -467,6 +499,70 @@ spinach-pg 1 2: *spinach-metadata-pg
 spinach-mysql 0 2: *spinach-metadata-mysql
 spinach-mysql 1 2: *spinach-metadata-mysql
 
+rspec-pg-rails5 0 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 1 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 2 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 3 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 4 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 5 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 6 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 7 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 8 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 9 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 10 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 11 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 12 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 13 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 14 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 15 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 16 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 17 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 18 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 19 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 20 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 21 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 22 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 23 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 24 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 25 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 26 28: *rspec-metadata-pg-rails5
+rspec-pg-rails5 27 28: *rspec-metadata-pg-rails5
+
+rspec-mysql-rails5 0 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 1 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 2 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 3 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 4 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 5 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 6 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 7 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 8 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 9 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 10 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 11 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 12 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 13 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 14 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 15 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 16 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 17 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 18 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 19 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 20 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 21 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 22 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 23 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 24 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 25 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 26 28: *rspec-metadata-mysql-rails5
+rspec-mysql-rails5 27 28: *rspec-metadata-mysql-rails5
+
+spinach-pg-rails5 0 2: *spinach-metadata-pg-rails5
+spinach-pg-rails5 1 2: *spinach-metadata-pg-rails5
+
+spinach-mysql-rails5 0 2: *spinach-metadata-mysql-rails5
+spinach-mysql-rails5 1 2: *spinach-metadata-mysql-rails5
+
 static-analysis:
   <<: *dedicated-no-docs-no-db-pull-cache-job
   dependencies:
@@ -617,21 +713,26 @@ karma:
 
 codequality:
   <<: *dedicated-no-docs-no-db-pull-cache-job
-  image: docker:latest
+  image: docker:stable
+  allow_failure: true
+  # gitlab-org runners set `privileged: false` but we need to have it set to true
+  # since we're using Docker in Docker
+  tags: []
   before_script: []
   services:
     - docker:dind
   variables:
     SETUP_DB: "false"
     DOCKER_DRIVER: overlay2
-    CODECLIMATE_FORMAT: json
   cache: {}
   dependencies: []
   script:
-    - apk update && apk add jq
-    - ./scripts/codequality analyze -f json > raw_codeclimate.json || true
-    # The following line keeps only the fields used in the MR widget, reducing the JSON artifact size
-    - jq -c 'map({check_name,description,fingerprint,location})' raw_codeclimate.json > codeclimate.json
+    # Get the custom rubocop codeclimate image (https://gitlab.com/gitlab-org/codeclimate-rubocop/wikis/home)
+    - docker pull dev.gitlab.org:5005/gitlab/gitlab-build-images:gitlab-codeclimate-rubocop-0-52-1
+    - docker tag dev.gitlab.org:5005/gitlab/gitlab-build-images:gitlab-codeclimate-rubocop-0-52-1 codeclimate/codeclimate-rubocop:gitlab-codeclimate-rubocop-0-52-1
+    # Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products
+    - 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
   artifacts:
     paths: [codeclimate.json]
     expire_in: 1 week
diff --git a/.gitlab/merge_request_templates/Database Changes.md b/.gitlab/merge_request_templates/Database Changes.md
index 8302b3b30c709a3f592800f469f11810eac80c38..68bc0fd1c7ffb07b49af9732bdf95086dd849ad9 100644
--- a/.gitlab/merge_request_templates/Database Changes.md	
+++ b/.gitlab/merge_request_templates/Database Changes.md	
@@ -33,13 +33,16 @@ When removing columns, tables, indexes or other structures:
 
 ## General Checklist
 
-- [ ] [Changelog entry](https://docs.gitlab.com/ce/development/changelog.html) added, if necessary
-- [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md)
+- [ ] [Changelog entry](https://docs.gitlab.com/ee/development/changelog.html) added, if necessary
+- [ ] [Documentation created/updated](https://docs.gitlab.com/ee/development/doc_styleguide.html)
 - [ ] API support added
 - [ ] Tests added for this feature/bug
 - Review
   - [ ] Has been reviewed by Backend
   - [ ] Has been reviewed by Database
-- [ ] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html)
-- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides)
+- [ ] Conform by the [merge request performance guides](https://docs.gitlab.com/ee/development/merge_request_performance_guidelines.html)
+- [ ] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/CONTRIBUTING.md#style-guides)
 - [ ] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits)
+- [ ] Internationalization required/considered
+- [ ] If paid feature, have we considered GitLab.com plan and how it works for groups and is there a design for promoting it to users who aren't on the correct plan
+- [ ] End-to-end tests pass (`package-qa` manual pipeline job)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8a90a7fcdc2cca4d9beeb2f368b87bb58663a4f9..6491905a1ac628062e051859762f7f6c882bcaea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -191,7 +191,6 @@ entry.
 - Enable privileged mode for GitLab Runner. !17528
 - Expose GITLAB_FEATURES as CI/CD variable (fixes #40994).
 - Upgrade GitLab Workhorse to 4.0.0.
-- Allow CI/CD Jobs being grouped on version strings.
 - Add discussions API for Issues and Snippets.
 - Add one group board to Libre.
 - Add support for filtering by source and target branch to merge requests API.
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 36545ad338e5f19fa893efe276125089bc8dd932..9188543ea641325267b8682081a95f2430234a4c 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.92.0
+0.93.0
diff --git a/Gemfile b/Gemfile
index fd174a60c666a03d24579dd244c0c2baf5d06df2..5eac6d73269f210524cbcea9dbddca6cdb4e5861 100644
--- a/Gemfile
+++ b/Gemfile
@@ -384,7 +384,7 @@ group :test do
   gem 'email_spec', '~> 1.6.0'
   gem 'json-schema', '~> 2.8.0'
   gem 'webmock', '~> 2.3.2'
-  gem 'test_after_commit', '~> 1.1'
+  gem 'test_after_commit', '~> 1.1' unless rails5? # Remove this gem when migrated to rails 5.0. It's been integrated to rails 5.0.
   gem 'sham_rack', '~> 1.3.6'
   gem 'concurrent-ruby', '~> 1.0.5'
   gem 'test-prof', '~> 0.2.5'
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index 86a22dbe5501c357dd6954fa932d11681d18ff21..08ae3fb514c56b31f6002efdaf070fba037ed906 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -60,7 +60,7 @@ GEM
       faraday_middleware-multi_json (~> 0.0)
       oauth2 (~> 1.0)
     asciidoctor (1.5.6.1)
-    asciidoctor-plantuml (0.0.7)
+    asciidoctor-plantuml (0.0.8)
       asciidoctor (~> 1.5)
     asset_sync (2.2.0)
       activemodel (>= 4.1.0)
@@ -97,7 +97,7 @@ GEM
       autoprefixer-rails (>= 5.2.1)
       sass (>= 3.3.4)
     bootstrap_form (2.7.0)
-    brakeman (3.6.2)
+    brakeman (4.2.1)
     browser (2.5.3)
     builder (3.2.3)
     bullet (5.5.1)
@@ -144,6 +144,7 @@ GEM
     connection_pool (2.2.1)
     crack (0.4.3)
       safe_yaml (~> 1.0.0)
+    crass (1.0.3)
     creole (0.5.0)
     css_parser (1.6.0)
       addressable
@@ -244,10 +245,11 @@ GEM
       builder
       excon (~> 0.58)
       formatador (~> 0.2)
-    fog-google (0.6.0)
+    fog-google (1.3.3)
       fog-core
       fog-json
       fog-xml
+      google-api-client (~> 0.19.1)
     fog-json (1.0.2)
       fog-core (~> 1.0)
       multi_json (~> 1.10)
@@ -289,7 +291,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)
@@ -398,7 +400,7 @@ GEM
     hipchat (1.5.4)
       httparty
       mimemagic
-    html-pipeline (2.6.0)
+    html-pipeline (2.7.1)
       activesupport (>= 2)
       nokogiri (>= 1.4)
     html2text (0.2.1)
@@ -484,7 +486,8 @@ GEM
       activesupport (>= 4)
       railties (>= 4)
       request_store (~> 1.0)
-    loofah (2.0.3)
+    loofah (2.2.2)
+      crass (~> 1.0.2)
       nokogiri (>= 1.5.9)
     mail (2.7.0)
       mini_mime (>= 0.1.1)
@@ -527,8 +530,8 @@ GEM
     omniauth (1.8.1)
       hashie (>= 3.4.6, < 3.6.0)
       rack (>= 1.6.2, < 3)
-    omniauth-auth0 (1.4.2)
-      omniauth-oauth2 (~> 1.1)
+    omniauth-auth0 (2.0.0)
+      omniauth-oauth2 (~> 1.4)
     omniauth-authentiq (0.3.1)
       omniauth-oauth2 (~> 1.3, >= 1.3.1)
     omniauth-azure-oauth2 (0.0.9)
@@ -551,6 +554,9 @@ GEM
       jwt (>= 1.5)
       omniauth (>= 1.1.1)
       omniauth-oauth2 (>= 1.5)
+    omniauth-jwt (0.0.2)
+      jwt
+      omniauth (~> 1.1)
     omniauth-kerberos (0.3.0)
       omniauth-multipassword
       timfel-krb5-auth (~> 0.8)
@@ -569,9 +575,9 @@ GEM
       ruby-saml (~> 1.7)
     omniauth-shibboleth (1.2.1)
       omniauth (>= 1.0.0)
-    omniauth-twitter (1.2.1)
-      json (~> 1.3)
+    omniauth-twitter (1.4.0)
       omniauth-oauth (~> 1.1)
+      rack
     omniauth_crowd (2.2.3)
       activesupport
       nokogiri (>= 1.4.4)
@@ -808,7 +814,7 @@ GEM
     rubyzip (1.2.1)
     rufus-scheduler (3.4.2)
       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)
@@ -907,8 +913,6 @@ GEM
     sysexits (1.2.0)
     temple (0.7.7)
     test-prof (0.2.5)
-    test_after_commit (1.1.0)
-      activerecord (>= 3.2)
     text (1.3.1)
     thin (1.7.2)
       daemons (~> 1.0, >= 1.0.9)
@@ -995,8 +999,8 @@ DEPENDENCIES
   akismet (~> 2.0)
   allocations (~> 1.0)
   asana (~> 0.6.0)
-  asciidoctor (~> 1.5.2)
-  asciidoctor-plantuml (= 0.0.7)
+  asciidoctor (~> 1.5.6)
+  asciidoctor-plantuml (= 0.0.8)
   asset_sync (~> 2.2.0)
   attr_encrypted (~> 3.0.0)
   awesome_print (~> 1.2.0)
@@ -1009,7 +1013,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)
@@ -1044,9 +1048,9 @@ DEPENDENCIES
   flipper-active_record (~> 0.13.0)
   flipper-active_support_cache_store (~> 0.13.0)
   fog-aliyun (~> 0.2.0)
-  fog-aws (~> 2.0)
+  fog-aws (~> 2.0.1)
   fog-core (~> 1.44)
-  fog-google (~> 0.5)
+  fog-google (~> 1.3.3)
   fog-local (~> 0.3)
   fog-openstack (~> 0.1)
   fog-rackspace (~> 0.1.1)
@@ -1058,7 +1062,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)
@@ -1080,7 +1084,7 @@ DEPENDENCIES
   hashie-forbidden_attributes
   health_check (~> 2.6.0)
   hipchat (~> 1.5.0)
-  html-pipeline (~> 2.6.0)
+  html-pipeline (~> 2.7.1)
   html2text
   httparty (~> 0.13.3)
   influxdb (~> 0.2)
@@ -1095,7 +1099,7 @@ DEPENDENCIES
   license_finder (~> 3.1)
   licensee (~> 8.9)
   lograge (~> 0.5)
-  loofah (~> 2.0.3)
+  loofah (~> 2.2)
   mail_room (~> 0.9.1)
   method_source (~> 0.8)
   minitest (~> 5.7.0)
@@ -1107,19 +1111,20 @@ DEPENDENCIES
   oauth2 (~> 1.4)
   octokit (~> 4.8)
   omniauth (~> 1.8)
-  omniauth-auth0 (~> 1.4.1)
+  omniauth-auth0 (~> 2.0.0)
   omniauth-authentiq (~> 0.3.1)
   omniauth-azure-oauth2 (~> 0.0.9)
   omniauth-cas3 (~> 1.1.4)
   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)
   omniauth-saml (~> 1.10)
   omniauth-shibboleth (~> 1.2.0)
-  omniauth-twitter (~> 1.2.0)
+  omniauth-twitter (~> 1.4)
   omniauth_crowd (~> 2.2.0)
   org-ruby (~> 0.9.12)
   peek (~> 1.0.1)
@@ -1169,7 +1174,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)
@@ -1197,7 +1202,6 @@ DEPENDENCIES
   state_machines-activerecord (~> 0.5.1)
   sys-filesystem (~> 1.1.6)
   test-prof (~> 0.2.5)
-  test_after_commit (~> 1.1)
   thin (~> 1.7.0)
   timecop (~> 0.8.0)
   toml-rb (~> 1.0.0)
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 6da33a26e58db175e686050c94f78536b33d4eb5..0e1ca7fe883365f15ca2053a35d9b4db98a3eb64 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -4,7 +4,7 @@ import $ from 'jquery';
 import _ from 'underscore';
 import Cookies from 'js-cookie';
 import { __ } from './locale';
-import { isInIssuePage, isInMRPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
+import { isInIssuePage, isInMRPage, isInEpicPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
 import flash from './flash';
 import axios from './lib/utils/axios_utils';
 
@@ -300,7 +300,7 @@ class AwardsHandler {
   }
 
   isInVueNoteablePage() {
-    return isInIssuePage() || this.isVueMRDiscussions();
+    return isInIssuePage() || isInEpicPage() || this.isVueMRDiscussions();
   }
 
   getVotesBlock() {
diff --git a/app/assets/javascripts/boards/services/board_service.js b/app/assets/javascripts/boards/services/board_service.js
index d78d470197464aa71e05a413e1ba41c6f27d13ff..7c90597f77c2ff0a62a14119e6a96e9150fa6a8c 100644
--- a/app/assets/javascripts/boards/services/board_service.js
+++ b/app/assets/javascripts/boards/services/board_service.js
@@ -19,7 +19,7 @@ export default class BoardService {
   }
 
   static generateIssuePath(boardId, id) {
-    return `${gon.relative_url_root}/-/boards/${boardId ? `/${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
+    return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
   }
 
   all() {
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index e6390f0855beb8dcdce3c039d3af69d6c50aee26..d7e1de18d09312ad6a656474b0dafc131f7a8f27 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -26,8 +26,8 @@ export default class FilteredSearchDropdownManager {
     this.filteredSearchInput = this.container.querySelector('.filtered-search');
     this.page = page;
     this.groupsOnly = isGroup;
-    this.groupAncestor = isGroupAncestor;
-    this.isGroupDecendent = isGroupDecendent;
+    this.includeAncestorGroups = isGroupAncestor;
+    this.includeDescendantGroups = isGroupDecendent;
 
     this.setupMapping();
 
@@ -108,7 +108,19 @@ export default class FilteredSearchDropdownManager {
   }
 
   getLabelsEndpoint() {
-    const endpoint = `${this.baseEndpoint}/labels.json`;
+    let endpoint = `${this.baseEndpoint}/labels.json?`;
+
+    if (this.groupsOnly) {
+      endpoint = `${endpoint}only_group_labels=true&`;
+    }
+
+    if (this.includeAncestorGroups) {
+      endpoint = `${endpoint}include_ancestor_groups=true&`;
+    }
+
+    if (this.includeDescendantGroups) {
+      endpoint = `${endpoint}include_descendant_groups=true`;
+    }
 
     return endpoint;
   }
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 71b7e80335bd7cb8fe29e003f9373e054e4be805..cf5ba1e1771f66285ab7f5b9e9d814c7bd1bd465 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -21,7 +21,7 @@ export default class FilteredSearchManager {
   constructor({
     page,
     isGroup = false,
-    isGroupAncestor = false,
+    isGroupAncestor = true,
     isGroupDecendent = false,
     filteredSearchTokenKeys = FilteredSearchTokenKeys,
     stateFiltersSelector = '.issues-state-filters',
@@ -86,6 +86,7 @@ export default class FilteredSearchManager {
         page: this.page,
         isGroup: this.isGroup,
         isGroupAncestor: this.isGroupAncestor,
+        isGroupDecendent: this.isGroupDecendent,
         filteredSearchTokenKeys: this.filteredSearchTokenKeys,
       });
 
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 8f09d0c971af4b404000c8183266df44cce21f4a..ab88055fc36eae98c24b3e211a4862cc31ff0594 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -3,7 +3,6 @@ import { mapActions } from 'vuex';
 import Icon from '~/vue_shared/components/icon.vue';
 import StageButton from './stage_button.vue';
 import UnstageButton from './unstage_button.vue';
-import router from '../../ide_router';
 
 export default {
   components: {
@@ -26,17 +25,17 @@ export default {
       return this.file.tempFile ? 'file-addition' : 'file-modified';
     },
     iconClass() {
-      return `multi-file-${
-        this.file.tempFile ? 'addition' : 'modified'
-      } append-right-8`;
+      return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
     },
   },
   methods: {
-    ...mapActions(['updateViewer', 'stageChange', 'unstageChange']),
+    ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
     openFileInEditor() {
-      this.updateViewer('diff');
-
-      router.push(`/project${this.file.url}`);
+      return this.openPendingTab(this.file).then(changeViewer => {
+        if (changeViewer) {
+          this.updateViewer('diff');
+        }
+      });
     },
     fileAction() {
       if (this.file.staged) {
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 5e44af01241a636c4c9e262121fa40fe33359da6..d22869466c9ce4fb7c1cba93b48bb6060473b372 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -60,6 +60,7 @@ export default {
         v-if="activeFile"
       >
         <repo-tabs
+          :active-file="activeFile"
           :files="openFiles"
           :viewer="viewer"
           :has-changes="hasChanges"
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index b6f8f8a1c991a4b5b0e25c703f048081bf2a2a9f..b1a16350c19ffb1201b805db90be66924d205a97 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -21,7 +21,8 @@ export default {
   },
   watch: {
     file(oldVal, newVal) {
-      if (newVal.path !== this.file.path) {
+      // Compare key to allow for files opened in review mode to be cached differently
+      if (newVal.key !== this.file.key) {
         this.initMonaco();
       }
     },
@@ -70,7 +71,7 @@ export default {
       })
         .then(() => {
           const viewerPromise = this.delayViewerUpdated
-            ? this.updateViewer('editor')
+            ? this.updateViewer(this.file.pending ? 'diff' : 'editor')
             : Promise.resolve();
 
           return viewerPromise;
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index fab53592e4ead3d03690358e5dc525f888caa2b8..b7b8a8e989de3f5b2903b79984d839a934f8e12b 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -62,11 +62,7 @@ export default {
         this.toggleTreeOpen(this.file.path);
       }
 
-      const delayPromise = this.file.changed
-        ? Promise.resolve()
-        : this.updateDelayViewerUpdated(true);
-
-      return delayPromise.then(() => {
+      return this.updateDelayViewerUpdated(true).then(() => {
         router.push(`/project${this.file.url}`);
       });
     },
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index 7bb02bb5e26fb43ba3064fcf83b5c249e7930c15..729be292cc0aef337d8aba8aed7b6072536959fa 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -1,17 +1,17 @@
 <script>
 import { mapActions } from 'vuex';
 
-import fileIcon from '~/vue_shared/components/file_icon.vue';
-import icon from '~/vue_shared/components/icon.vue';
-import fileStatusIcon from './repo_file_status_icon.vue';
-import changedFileIcon from './changed_file_icon.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import FileStatusIcon from './repo_file_status_icon.vue';
+import ChangedFileIcon from './changed_file_icon.vue';
 
 export default {
   components: {
-    fileStatusIcon,
-    fileIcon,
-    icon,
-    changedFileIcon,
+    FileStatusIcon,
+    FileIcon,
+    Icon,
+    ChangedFileIcon,
   },
   props: {
     tab: {
@@ -32,7 +32,7 @@ export default {
       return `Close ${this.tab.name}`;
     },
     showChangedIcon() {
-      return this.fileHasChanged ? !this.tabMouseOver : false;
+      return this.tab.changed ? !this.tabMouseOver : false;
     },
     fileHasChanged() {
       return this.tab.changed || this.tab.tempFile || this.tab.staged;
@@ -40,9 +40,15 @@ export default {
   },
 
   methods: {
-    ...mapActions(['closeFile']),
+    ...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
     clickFile(tab) {
-      this.$router.push(`/project${tab.url}`);
+      this.updateDelayViewerUpdated(true);
+
+      if (tab.pending) {
+        this.openPendingTab(tab);
+      } else {
+        this.$router.push(`/project${tab.url}`);
+      }
     },
     mouseOverTab() {
       if (this.fileHasChanged) {
@@ -67,7 +73,7 @@ export default {
     <button
       type="button"
       class="multi-file-tab-close"
-      @click.stop.prevent="closeFile(tab.path)"
+      @click.stop.prevent="closeFile(tab)"
       :aria-label="closeLabel"
     >
       <icon
@@ -83,7 +89,9 @@ export default {
 
     <div
       class="multi-file-tab"
-      :class="{active : tab.active }"
+      :class="{
+        active: tab.active
+      }"
       :title="tab.url"
     >
       <file-icon
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index a44e418b2eb88797d8d5773431eddf45ea1aa5a3..7bd646ba9b0597ba6a4c9eacfd7279058118ab9e 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -2,6 +2,7 @@
 import { mapActions } from 'vuex';
 import RepoTab from './repo_tab.vue';
 import EditorMode from './editor_mode_dropdown.vue';
+import router from '../ide_router';
 
 export default {
   components: {
@@ -9,6 +10,10 @@ export default {
     EditorMode,
   },
   props: {
+    activeFile: {
+      type: Object,
+      required: true,
+    },
     files: {
       type: Array,
       required: true,
@@ -38,7 +43,18 @@ export default {
     this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
   },
   methods: {
-    ...mapActions(['updateViewer']),
+    ...mapActions(['updateViewer', 'removePendingTab']),
+    openFileViewer(viewer) {
+      this.updateViewer(viewer);
+
+      if (this.activeFile.pending) {
+        return this.removePendingTab(this.activeFile).then(() => {
+          router.push(`/project${this.activeFile.url}`);
+        });
+      }
+
+      return null;
+    },
   },
 };
 </script>
@@ -60,7 +76,7 @@ export default {
       :show-shadow="showShadow"
       :has-changes="hasChanges"
       :merge-request-id="mergeRequestId"
-      @click="updateViewer"
+      @click="openFileViewer"
     />
   </div>
 </template>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index be2c12c0487aa0206622068754467bc2d9eba891..20983666b4a4fedbf8c55fcb50a553311c09cbfc 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -77,7 +77,11 @@ router.beforeEach((to, from, next) => {
               if (to.params[0]) {
                 const path =
                   to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
-                const treeEntry = store.state.entries[path];
+                const treeEntryKey = Object.keys(store.state.entries).find(
+                  key => key === path && !store.state.entries[key].pending,
+                );
+                const treeEntry = store.state.entries[treeEntryKey];
+
                 if (treeEntry) {
                   store.dispatch('handleTreeEntryAction', treeEntry);
                 }
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index f3c052d690330165f3e3a4a3b908eb97bd961060..da467cbd4761a8d7b2bde7072ee8e7e1de60070b 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -13,12 +13,12 @@ export default class Model {
       (this.originalModel = this.monaco.editor.createModel(
         this.file.raw,
         undefined,
-        new this.monaco.Uri(null, null, `original/${this.file.path}`),
+        new this.monaco.Uri(null, null, `original/${this.file.key}`),
       )),
       (this.model = this.monaco.editor.createModel(
         this.content,
         undefined,
-        new this.monaco.Uri(null, null, this.file.path),
+        new this.monaco.Uri(null, null, this.file.key),
       )),
     );
     if (this.file.mrChange) {
@@ -36,7 +36,7 @@ export default class Model {
     this.updateContent = this.updateContent.bind(this);
     this.dispose = this.dispose.bind(this);
 
-    eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
+    eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
     eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
   }
 
@@ -53,7 +53,7 @@ export default class Model {
   }
 
   get path() {
-    return this.file.path;
+    return this.file.key;
   }
 
   getModel() {
@@ -91,7 +91,7 @@ export default class Model {
     this.disposable.dispose();
     this.events.clear();
 
-    eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
+    eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
     eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
   }
 }
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index 57d5e59a88b63d415c89de707a8702826a1e1f96..0e7b563b5d6b9e214f1e28fffb8a45ac8eb20ecb 100644
--- a/app/assets/javascripts/ide/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
@@ -9,17 +9,17 @@ export default class ModelManager {
     this.models = new Map();
   }
 
-  hasCachedModel(path) {
-    return this.models.has(path);
+  hasCachedModel(key) {
+    return this.models.has(key);
   }
 
-  getModel(path) {
-    return this.models.get(path);
+  getModel(key) {
+    return this.models.get(key);
   }
 
   addModel(file) {
-    if (this.hasCachedModel(file.path)) {
-      return this.getModel(file.path);
+    if (this.hasCachedModel(file.key)) {
+      return this.getModel(file.key);
     }
 
     const model = new Model(this.monaco, file);
@@ -27,7 +27,7 @@ export default class ModelManager {
     this.disposable.add(model);
 
     eventHub.$on(
-      `editor.update.model.dispose.${file.path}`,
+      `editor.update.model.dispose.${file.key}`,
       this.removeCachedModel.bind(this, file),
     );
 
@@ -35,12 +35,9 @@ export default class ModelManager {
   }
 
   removeCachedModel(file) {
-    this.models.delete(file.path);
+    this.models.delete(file.key);
 
-    eventHub.$off(
-      `editor.update.model.dispose.${file.path}`,
-      this.removeCachedModel,
-    );
+    eventHub.$off(`editor.update.model.dispose.${file.key}`, this.removeCachedModel);
   }
 
   dispose() {
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 3e512e79bbaf4d7f35c843f35faf4fcf099022bf..cecb4d215bab2c4c816dd794d1ab355aee2087d8 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -22,7 +22,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => {
 };
 
 export const closeAllFiles = ({ state, dispatch }) => {
-  state.openFiles.forEach(file => dispatch('closeFile', file.path));
+  state.openFiles.forEach(file => dispatch('closeFile', file));
 };
 
 export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 470d168e3e3a2b04b24c61588ae4b4c1ff87ece6..ef018935c7a890681a4c66edf7dd23977999993c 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -6,24 +6,34 @@ import * as types from '../mutation_types';
 import router from '../../ide_router';
 import { setPageTitle } from '../utils';
 
-export const closeFile = ({ commit, state, getters, dispatch }, path) => {
-  const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path);
-  const file = state.entries[path];
+export const closeFile = ({ commit, state, dispatch }, file) => {
+  const path = file.path;
+  const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key);
   const fileWasActive = file.active;
 
-  commit(types.TOGGLE_FILE_OPEN, path);
-  commit(types.SET_FILE_ACTIVE, { path, active: false });
+  if (file.pending) {
+    commit(types.REMOVE_PENDING_TAB, file);
+  } else {
+    commit(types.TOGGLE_FILE_OPEN, path);
+    commit(types.SET_FILE_ACTIVE, { path, active: false });
+  }
 
   if (state.openFiles.length > 0 && fileWasActive) {
     const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
-    const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path];
-
-    router.push(`/project${nextFileToOpen.url}`);
+    const nextFileToOpen = state.openFiles[nextIndexToOpen];
+
+    if (nextFileToOpen.pending) {
+      dispatch('updateViewer', 'diff');
+      dispatch('openPendingTab', nextFileToOpen);
+    } else {
+      dispatch('updateDelayViewerUpdated', true);
+      router.push(`/project${nextFileToOpen.url}`);
+    }
   } else if (!state.openFiles.length) {
     router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
   }
 
-  eventHub.$emit(`editor.update.model.dispose.${file.path}`);
+  eventHub.$emit(`editor.update.model.dispose.${file.key}`);
 };
 
 export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
@@ -162,3 +172,23 @@ export const stageChange = ({ commit }, path) => {
 export const unstageChange = ({ commit }, path) => {
   commit(types.UNSTAGE_CHANGE, path);
 };
+
+export const openPendingTab = ({ commit, getters, dispatch, state }, file) => {
+  if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') {
+    return false;
+  }
+
+  commit(types.ADD_PENDING_TAB, { file });
+
+  dispatch('scrollToTab');
+
+  router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`);
+
+  return true;
+};
+
+export const removePendingTab = ({ commit }, file) => {
+  commit(types.REMOVE_PENDING_TAB, file);
+
+  eventHub.$emit(`editor.update.model.dispose.${file.key}`);
+};
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 118b4df33f87abdbbdcd077bac444c258741aefb..e834ff5b59852cef97f272804979690a5bf511d6 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -55,3 +55,5 @@ export const STAGE_CHANGE = 'STAGE_CHANGE';
 export const UNSTAGE_CHANGE = 'UNSTAGE_CHANGE';
 
 export const UPDATE_FILE_AFTER_COMMIT = 'UPDATE_FILE_AFTER_COMMIT';
+export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
+export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index f4a9354b024aba58fc947ee08e36bb93ba5c631b..b52d1f96e283875837ad3618a38f1cbe2b008c7e 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -5,6 +5,14 @@ export default {
     Object.assign(state.entries[path], {
       active,
     });
+
+    if (active && !state.entries[path].pending) {
+      Object.assign(state, {
+        openFiles: state.openFiles.map(f =>
+          Object.assign(f, { active: f.pending ? false : f.active }),
+        ),
+      });
+    }
   },
   [types.TOGGLE_FILE_OPEN](state, path) {
     Object.assign(state.entries[path], {
@@ -12,10 +20,14 @@ export default {
     });
 
     if (state.entries[path].opened) {
-      state.openFiles.push(state.entries[path]);
+      Object.assign(state, {
+        openFiles: state.openFiles.filter(f => f.path !== path).concat(state.entries[path]),
+      });
     } else {
+      const file = state.entries[path];
+
       Object.assign(state, {
-        openFiles: state.openFiles.filter(f => f.path !== path),
+        openFiles: state.openFiles.filter(f => f.key !== file.key),
       });
     }
   },
@@ -141,4 +153,37 @@ export default {
       changed,
     });
   },
+  [types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
+    const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending);
+    let openFiles = state.openFiles.map(f =>
+      Object.assign(f, { active: f.path === file.path, opened: false }),
+    );
+
+    if (!pendingTab) {
+      const openFile = openFiles.find(f => f.path === file.path);
+
+      openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => {
+        if (!f) return acc;
+
+        if (f.path === file.path) {
+          return acc.concat({
+            ...f,
+            active: true,
+            pending: true,
+            opened: true,
+            key: `${keyPrefix}-${f.key}`,
+          });
+        }
+
+        return acc.concat(f);
+      }, []);
+    }
+
+    Object.assign(state, { openFiles });
+  },
+  [types.REMOVE_PENDING_TAB](state, file) {
+    Object.assign(state, {
+      openFiles: state.openFiles.filter(f => f.key !== file.key),
+    });
+  },
 };
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index da0069b63a8c28940a776b29e159797b7974813b..597fa3a1c10b6cce39319319c2ee9e0da0770a16 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -1,5 +1,7 @@
 export const dataStructure = () => ({
   id: '',
+  // Key will contain a mixture of ID and path
+  // it can also contain a prefix `pending-` for files opened in review mode
   key: '',
   type: '',
   projectId: '',
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index 172de6b36791aeafdc5c54ec0cd01c93a005eb05..af47056d98f49f4e25aaa997878df4bb631c8470 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -45,7 +45,7 @@
         return `#${this.job.runner.id}`;
       },
       hasTimeout() {
-        return this.job.metadata != null && this.job.metadata.timeout_human_readable !== '';
+        return this.job.metadata != null && this.job.metadata.timeout_human_readable !== null;
       },
       timeout() {
         if (this.job.metadata == null) {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 0830ebe9e4e087a5b719feeb99ca09b5d6ebb899..9ff2042475bc0f4485c8326fe0d8aca53dd3858f 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -33,6 +33,7 @@ export const checkPageAndAction = (page, action) => {
 
 export const isInIssuePage = () => checkPageAndAction('issues', 'show');
 export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
+export const isInEpicPage = () => checkPageAndAction('epics', 'show');
 export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
 export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
 
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 90dcafd75b7602dbe1909abe0f651bb034e42661..648fa6ff804c822a99d79bbedf78cb5735b74fac 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -99,6 +99,10 @@ export default {
         'js-note-target-reopen': !this.isOpen,
       };
     },
+    supportQuickActions() {
+      // Disable quick actions support for Epics
+      return this.noteableType !== constants.EPIC_NOTEABLE_TYPE;
+    },
     markdownDocsPath() {
       return this.getNotesData.markdownDocsPath;
     },
@@ -355,7 +359,7 @@ Please check your network connection and try again.`;
                   name="note[note]"
                   class="note-textarea js-vue-comment-form
 js-gfm-input js-autosize markdown-area js-vue-textarea"
-                  data-supports-quick-actions="true"
+                  :data-supports-quick-actions="supportQuickActions"
                   aria-label="Description"
                   v-model="note"
                   ref="textarea"
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index a90c6d6381dfd3e36fc532f5632f1501871881c3..5bd81c7cad67ad0eb14ee44cec5f0ab8dbd8464f 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -50,7 +50,11 @@ export default {
     ...mapGetters(['notes', 'getNotesDataByProp', 'discussionCount']),
     noteableType() {
       // FIXME -- @fatihacet Get this from JSON data.
-      const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
+      const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE, EPIC_NOTEABLE_TYPE } = constants;
+
+      if (this.noteableData.noteableType === EPIC_NOTEABLE_TYPE) {
+        return EPIC_NOTEABLE_TYPE;
+      }
 
       return this.noteableData.merge_params
         ? MERGE_REQUEST_NOTEABLE_TYPE
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index f4f407ffd8adbbdcb92cd46dd3a08f701836a76a..68f8cb1cf1e9859d1783a8e3ee95b4fc70fe4e33 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -10,6 +10,7 @@ export const CLOSED = 'closed';
 export const EMOJI_THUMBSUP = 'thumbsup';
 export const EMOJI_THUMBSDOWN = 'thumbsdown';
 export const ISSUE_NOTEABLE_TYPE = 'issue';
+export const EPIC_NOTEABLE_TYPE = 'epic';
 export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
 export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
 export const RESOLVE_NOTE_METHOD_NAME = 'post';
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index f90775d01573bccb9d3a8d97563352dd6551fda9..e4121f151dbb1e5007a995d745c09a4d04b75c36 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -12,8 +12,11 @@ document.addEventListener(
       data() {
         const notesDataset = document.getElementById('js-vue-notes').dataset;
         const parsedUserData = JSON.parse(notesDataset.currentUserData);
+        const noteableData = JSON.parse(notesDataset.noteableData);
         let currentUserData = {};
 
+        noteableData.noteableType = notesDataset.noteableType;
+
         if (parsedUserData) {
           currentUserData = {
             id: parsedUserData.id,
@@ -25,7 +28,7 @@ document.addEventListener(
         }
 
         return {
-          noteableData: JSON.parse(notesDataset.noteableData),
+          noteableData,
           currentUserData,
           notesData: JSON.parse(notesDataset.notesData),
         };
diff --git a/app/assets/javascripts/notes/mixins/noteable.js b/app/assets/javascripts/notes/mixins/noteable.js
index 0da4ff49f0862136687a9209957d45c7f01e64ad..5bf8216a1f331aaf01f2199c8796732655006bed 100644
--- a/app/assets/javascripts/notes/mixins/noteable.js
+++ b/app/assets/javascripts/notes/mixins/noteable.js
@@ -14,6 +14,8 @@ export default {
           return constants.MERGE_REQUEST_NOTEABLE_TYPE;
         case 'Issue':
           return constants.ISSUE_NOTEABLE_TYPE;
+        case 'Epic':
+          return constants.EPIC_NOTEABLE_TYPE;
         default:
           return '';
       }
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index d149b307e7fca450a596cccd99dfa124f84b2cba..914f804fdd3b68a5f0681759e7395f695b4e2b07 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
 document.addEventListener('DOMContentLoaded', () => {
   initFilteredSearch({
     page: FILTERED_SEARCH.ISSUES,
+    isGroupDecendent: true,
   });
   projectSelect();
 });
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index a5cc1f34b63278c9225321fa584839a05dbf390b..1600faa3611e46ceadd36523ccae05b9ed36da1c 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -5,6 +5,7 @@ import { FILTERED_SEARCH } from '~/pages/constants';
 document.addEventListener('DOMContentLoaded', () => {
   initFilteredSearch({
     page: FILTERED_SEARCH.MERGE_REQUESTS,
+    isGroupDecendent: true,
   });
   projectSelect();
 });
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 42772f13155d84c3985aebc5e70b60d42eb80270..ce2f1482456c50b355297f11a3c4accbade4501c 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -706,8 +706,8 @@ button.mini-pipeline-graph-dropdown-toggle {
 // dropdown content for big and mini pipeline
 .big-pipeline-graph-dropdown-menu,
 .mini-pipeline-graph-dropdown-menu {
-  width: 195px;
-  max-width: 195px;
+  width: 240px;
+  max-width: 240px;
 
   .scrollable-menu {
     padding: 0;
@@ -750,7 +750,7 @@ button.mini-pipeline-graph-dropdown-toggle {
         height: #{$ci-action-icon-size - 6};
         left: -3px;
         position: relative;
-        top: -2px;
+        top: -1px;
 
         &.icon-action-stop,
         &.icon-action-cancel {
@@ -931,13 +931,11 @@ button.mini-pipeline-graph-dropdown-toggle {
    */
   &.dropdown-menu {
     transform: translate(-80%, 0);
-    min-width: 150px;
 
     @media(min-width: $screen-md-min) {
       transform: translate(-50%, 0);
       right: auto;
       left: 50%;
-      min-width: 240px;
     }
   }
 }
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index cc38608eda58e6ce202d2476e7fef76a1193950c..001f652009369bf2bcf81f4a931a9ebf00bc8505 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -5,7 +5,7 @@ class Admin::GroupsController < Admin::ApplicationController
 
   def index
     @groups = Group.with_statistics.with_route
-    @groups = @groups.sort(@sort = params[:sort])
+    @groups = @groups.sort_by_attribute(@sort = params[:sort])
     @groups = @groups.search(params[:name]) if params[:name].present?
     @groups = @groups.page(params[:page])
   end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 156a8e2c5157d397ca3d4929c6245bccff4e4797..bfeb5a2d097c2c13aa753cd0090b8f05f306522b 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -4,7 +4,7 @@ class Admin::UsersController < Admin::ApplicationController
   def index
     @users = User.order_name_asc.filter(params[:filter])
     @users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
-    @users = @users.sort(@sort = params[:sort])
+    @users = @users.sort_by_attribute(@sort = params[:sort])
     @users = @users.page(params[:page])
   end
 
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 7f83bd10e93e61855a3cde1b774ea46c87f2f634..24651dd392cf0cbe6897d2e1d658286f89d02370 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -229,10 +229,6 @@ class ApplicationController < ActionController::Base
     @event_filter ||= EventFilter.new(filters)
   end
 
-  def gitlab_ldap_access(&block)
-    Gitlab::Auth::LDAP::Access.open { |access| yield(access) }
-  end
-
   # JSON for infinite scroll via Pager object
   def pager_json(partial, count, locals = {})
     html = render_to_string(
diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb
index fafb10090ca411686d4670f4f167e5ba1e440b8e..56770a1740602cf9f764bf65bd524d5a9d5eea4a 100644
--- a/app/controllers/concerns/group_tree.rb
+++ b/app/controllers/concerns/group_tree.rb
@@ -14,7 +14,7 @@ module GroupTree
               end
 
     @groups = @groups.with_selects_for_list(archived: params[:archived])
-                .sort(@sort = params[:sort])
+                .sort_by_attribute(@sort = params[:sort])
                 .page(params[:page])
 
     respond_to do |format|
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index a21e658fda12cbc05d622318ec276079dfc95036..0379f76fc3d9f6524633fcfe18812ba10ca9db72 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -88,11 +88,15 @@ module IssuableActions
 
     discussions = Discussion.build_collection(notes, issuable)
 
-    render json: DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user).represent(discussions, context: self)
+    render json: discussion_serializer.represent(discussions, context: self)
   end
 
   private
 
+  def discussion_serializer
+    DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity)
+  end
+
   def recaptcha_check_if_spammable(should_redirect = true, &block)
     return yield unless issuable.is_a? Spammable
 
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 03ed5b5310bf58200765ee9c7ea0b8b9630e080a..839cac3687ceed9178a72cb35b429b045cee3ff8 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -212,7 +212,7 @@ module NotesActions
   end
 
   def note_serializer
-    NoteSerializer.new(project: project, noteable: noteable, current_user: current_user)
+    ProjectNoteSerializer.new(project: project, noteable: noteable, current_user: current_user)
   end
 
   def note_project
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index f210434b2d72e72d4dc301496ea45e32aafecd66..134b0dfc0db74af671be9780d6326340f2fe83c1 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -17,7 +17,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
     @members = GroupMembersFinder.new(@group).execute
     @members = @members.non_invite unless can?(current_user, :admin_group, @group)
     @members = @members.search(params[:search]) if params[:search].present?
-    @members = @members.sort(@sort)
+    @members = @members.sort_by_attribute(@sort)
     @members = @members.page(params[:page]).per(50)
     @members = present_members(@members.includes(:user))
 
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index dbf61a1772425851be6f7a4defd03b1e87a38fe7..3d27ae18b17ec933f6f8dd6c9a8eef1f32ff1237 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -51,7 +51,7 @@ class ProfilesController < Profiles::ApplicationController
   end
 
   def update_username
-    result = Users::UpdateService.new(current_user, user: @user, username: user_params[:username]).execute
+    result = Users::UpdateService.new(current_user, user: @user, username: username_param).execute
 
     options = if result[:status] == :success
                 { notice: "Username successfully changed" }
@@ -72,6 +72,10 @@ class ProfilesController < Profiles::ApplicationController
     return render_404 unless @user.can_change_username?
   end
 
+  def username_param
+    @username_param ||= user_params.require(:username)
+  end
+
   def user_params
     @user_params ||= params.require(:user).permit(
       :avatar,
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 176679f08494a785bef0d7bf4753488d978cff75..b7b36f770f57dd7e5c6932a697a42c1133f7b5e2 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -22,9 +22,13 @@ class Projects::BranchesController < Projects::ApplicationController
 
         @refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
         @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
+
+        # n+1: https://gitlab.com/gitlab-org/gitaly/issues/992
+        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
         end
 
         render
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index cba9a53dc4b8b3bc8d0383c395ddec5078d75d27..7bc162140100e09d04e36885db0f88ccd1866b18 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -43,7 +43,7 @@ class Projects::DiscussionsController < Projects::ApplicationController
 
   def render_json_with_discussions_serializer
     render json:
-      DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user)
+      DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user, note_entity:  ProjectNoteEntity)
       .represent(discussion, context: self)
   end
 
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 516198b1b8a175d008b69d413173661fa45c9fe9..91016f6494e1ed0bdcd2dcc178ce5766ad3cc76a 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -150,7 +150,8 @@ class Projects::LabelsController < Projects::ApplicationController
   end
 
   def find_labels
-    @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
+    @available_labels ||=
+      LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: params[:include_ancestor_groups]).execute
   end
 
   def authorize_admin_labels!
diff --git a/app/controllers/projects/lfs_api_controller.rb b/app/controllers/projects/lfs_api_controller.rb
index c77f10ef1dd73c7890dd7a77f3e2710b66f30ba3..ee4ed6741109b48a18c29053073ee2cc3d6b2bb6 100644
--- a/app/controllers/projects/lfs_api_controller.rb
+++ b/app/controllers/projects/lfs_api_controller.rb
@@ -41,7 +41,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
 
   def existing_oids
     @existing_oids ||= begin
-      storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
+      project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
     end
   end
 
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index e898136d203ff7ac7f76c246d68b6abbcc2688de..c5a044541f15efc3b0547659bb3d77fb67692cdc 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -14,7 +14,7 @@ class Projects::MilestonesController < Projects::ApplicationController
 
   def index
     @sort = params[:sort] || 'due_date_asc'
-    @milestones = milestones.sort(@sort)
+    @milestones = milestones.sort_by_attribute(@sort)
 
     respond_to do |format|
       format.html do
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index e9b4679f94c6b5eb050f5e1ed5eb8114e0edaec3..cfa5e72af645e50d19472c6de843d8aec53369d1 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -21,7 +21,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
       @group_links = @group_links.where(group_id: @project.invited_groups.search(params[:search]).select(:id))
     end
 
-    @project_members = present_members(@project_members.sort(@sort).page(params[:page]))
+    @project_members = present_members(@project_members.sort_by_attribute(@sort).page(params[:page]))
     @requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user))
     @project_member = @project.project_members.new
   end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index d06d18c498b997e94b5f0781b5e574ce0803a8fe..dd9e4a2af3edddc14de036d8864873d3e9acdbbc 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -16,6 +16,10 @@ module Projects
         @protected_tags = @project.protected_tags.order(:name).page(params[:page])
         @protected_branch = @project.protected_branches.new
         @protected_tag = @project.protected_tags.new
+
+        @protected_branches_count = @protected_branches.reduce(0) { |sum, branch| sum + branch.matching(@project.repository.branches).size }
+        @protected_tags_count = @protected_tags.reduce(0) { |sum, tag| sum + tag.matching(@project.repository.tags).size }
+
         load_gon_index
       end
 
diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb
index 2c8f21c24003a99b6358fa2d177614810136b897..53b77f5fed93103246c66e3485aa66a5b9ea9d19 100644
--- a/app/finders/admin/projects_finder.rb
+++ b/app/finders/admin/projects_finder.rb
@@ -62,6 +62,6 @@ class Admin::ProjectsFinder
 
   def sort(items)
     sort = params.fetch(:sort) { 'latest_activity_desc' }
-    items.sort(sort)
+    items.sort_by_attribute(sort)
   end
 end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index b2d4f9938ff20f885cd8d25a84e7221783d7e002..61c72aa22a8eb49a445ec48a4eda37e229b4f5f2 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -337,7 +337,7 @@ class IssuableFinder
   def sort(items)
     # Ensure we always have an explicit sort order (instead of inheriting
     # multiple orders when combining ActiveRecord::Relation objects).
-    params[:sort] ? items.sort(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
+    params[:sort] ? items.sort_by_attribute(params[:sort], excluded_labels: label_names) : items.reorder(id: :desc)
   end
 
   def by_assignee(items)
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 780c0fdb03eb806962c2e1dd1e5fb2e76bdb1169..afd1f824b3251dfdb5831d0f4d613d99ac05182e 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -28,9 +28,10 @@ class LabelsFinder < UnionFinder
       if project
         if project.group.present?
           labels_table = Label.arel_table
+          group_ids = group_ids_for(project.group)
 
           label_ids << Label.where(
-            labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].eq(project.group.id)).or(
+            labels_table[:type].eq('GroupLabel').and(labels_table[:group_id].in(group_ids)).or(
               labels_table[:type].eq('ProjectLabel').and(labels_table[:project_id].eq(project.id))
             )
           )
@@ -38,11 +39,14 @@ class LabelsFinder < UnionFinder
           label_ids << project.labels
         end
       end
-    elsif only_group_labels?
-      label_ids << Label.where(group_id: group_ids)
     else
+      if group?
+        group = Group.find(params[:group_id])
+        label_ids << Label.where(group_id: group_ids_for(group))
+      end
+
       label_ids << Label.where(group_id: projects.group_ids)
-      label_ids << Label.where(project_id: projects.select(:id))
+      label_ids << Label.where(project_id: projects.select(:id)) unless only_group_labels?
     end
 
     label_ids
@@ -59,22 +63,33 @@ class LabelsFinder < UnionFinder
     items.where(title: title)
   end
 
-  def group_ids
+  # Gets redacted array of group ids
+  # which can include the ancestors and descendants of the requested group.
+  def group_ids_for(group)
     strong_memoize(:group_ids) do
-      groups_user_can_read_labels(groups_to_include).map(&:id)
+      groups = groups_to_include(group)
+
+      groups_user_can_read_labels(groups).map(&:id)
     end
   end
 
-  def groups_to_include
-    group = Group.find(params[:group_id])
+  def groups_to_include(group)
     groups = [group]
 
-    groups += group.ancestors if params[:include_ancestor_groups].present?
-    groups += group.descendants if params[:include_descendant_groups].present?
+    groups += group.ancestors if include_ancestor_groups?
+    groups += group.descendants if include_descendant_groups?
 
     groups
   end
 
+  def include_ancestor_groups?
+    params[:include_ancestor_groups]
+  end
+
+  def include_descendant_groups?
+    params[:include_descendant_groups]
+  end
+
   def group?
     params[:group_id].present?
   end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 005612ededc8d0120235c3896b767221d6562b39..c7d6bc6cfdcf6ed9e52d4218fb05b9be5eccedb8 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -124,7 +124,7 @@ class ProjectsFinder < UnionFinder
   end
 
   def sort(items)
-    params[:sort].present? ? items.sort(params[:sort]) : items.order_id_desc
+    params[:sort].present? ? items.sort_by_attribute(params[:sort]) : items.order_id_desc
   end
 
   def by_archived(projects)
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 150f4c7688b6d321cd1fdfea6037577fdeeb4e5f..09e2c586f2a10053d833a0633e38c5554e05cf41 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -119,7 +119,7 @@ class TodosFinder
   end
 
   def sort(items)
-    params[:sort] ? items.sort(params[:sort]) : items.order_id_desc
+    params[:sort] ? items.sort_by_attribute(params[:sort]) : items.order_id_desc
   end
 
   def by_action(items)
diff --git a/app/helpers/boards_helper.rb b/app/helpers/boards_helper.rb
index 275e892b2e65a8fb73ae24ea29674c26973e99d6..af878bcf9a0450a77ca9e6067c6c436dce2605ca 100644
--- a/app/helpers/boards_helper.rb
+++ b/app/helpers/boards_helper.rb
@@ -53,10 +53,12 @@ module BoardsHelper
   end
 
   def board_list_data
+    include_descendant_groups = @group&.present?
+
     {
       toggle: "dropdown",
-      list_labels_path: labels_filter_path(true),
-      labels: labels_filter_path(true),
+      list_labels_path: labels_filter_path(true, include_ancestor_groups: true),
+      labels: labels_filter_path(true, include_descendant_groups: include_descendant_groups),
       labels_endpoint: @labels_endpoint,
       namespace_path: @namespace_path,
       project_path: @project&.path,
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 87ff607dc3fc5585d0f663f56ae159d13c977a4a..c4a6a1e4bb3977d4227127f5bccd1e1480f9891f 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -129,13 +129,17 @@ module LabelsHelper
     end
   end
 
-  def labels_filter_path(only_group_labels = false)
+  def labels_filter_path(only_group_labels = false, include_ancestor_groups: true, include_descendant_groups: false)
     project = @target_project || @project
 
+    options = {}
+    options[:include_ancestor_groups] = include_ancestor_groups if include_ancestor_groups
+    options[:include_descendant_groups] = include_descendant_groups if include_descendant_groups
+
     if project
-      project_labels_path(project, :json)
+      project_labels_path(project, :json, options)
     elsif @group
-      options = { only_group_labels: only_group_labels } if only_group_labels
+      options[:only_group_labels] = only_group_labels if only_group_labels
       group_labels_path(@group, :json, options)
     else
       dashboard_labels_path(:json)
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 20aed60cb7a06b88741f203612458fea106785d4..27ed48fdbc7e38132a42c0e389aa98c816bfc329 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -151,16 +151,17 @@ module NotesHelper
     }
   end
 
-  def notes_data(issuable)
-    discussions_path =
-      if issuable.is_a?(Issue)
-        discussions_project_issue_path(@project, issuable, format: :json)
-      else
-        discussions_project_merge_request_path(@project, issuable, format: :json)
-      end
+  def discussions_path(issuable)
+    if issuable.is_a?(Issue)
+      discussions_project_issue_path(@project, issuable, format: :json)
+    else
+      discussions_project_merge_request_path(@project, issuable, format: :json)
+    end
+  end
 
+  def notes_data(issuable)
     {
-      discussionsPath: discussions_path,
+      discussionsPath: discussions_path(issuable),
       registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
       newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'),
       markdownDocsPath: help_page_path('user/markdown'),
@@ -170,7 +171,6 @@ module NotesHelper
       notesPath: notes_url,
       totalNotes: issuable.discussions.length,
       lastFetchedAt: Time.now.to_i
-
     }.to_json
   end
 
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index b64be89c18158a7d47865cfa85e057a0ca37ebe4..5e7c20ef51eb30e0135702e9e925cd355adc0723 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -123,7 +123,7 @@ module TreeHelper
 
   # returns the relative path of the first subdir that doesn't have only one directory descendant
   def flatten_tree(root_path, tree)
-    return tree.flat_path.sub(%r{\A#{root_path}/}, '') if tree.flat_path.present?
+    return tree.flat_path.sub(%r{\A#{Regexp.escape(root_path)}/}, '') if tree.flat_path.present?
 
     subtree = Gitlab::Git::Tree.where(@repository, @commit.id, tree.path)
     if subtree.count == 1 && subtree.first.dir?
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index be99f3780ccd116ac35e73b914b542369a0f55cf..b3f2aeb08ca229cd2f5935862bbf6a66822a6440 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -15,6 +15,7 @@ module Emails
       setup_merge_request_mail(merge_request_id, recipient_id)
       @new_commits = new_commits
       @existing_commits = existing_commits
+      @updated_by_user = User.find(updated_by_user_id)
 
       mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
     end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 18e963891996b4b5b6c4d8dd45409c142b5214eb..4aa65bf42738dbd7edfbd87c036d2e3daa524d11 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -90,6 +90,7 @@ module Ci
     before_save :ensure_token
     before_destroy { unscoped_project }
 
+    before_create :ensure_metadata
     after_create unless: :importing? do |build|
       run_after_commit { BuildHooksWorker.perform_async(build.id) }
     end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index b64462fb7682dd70f81e087f75326e144780ca3a..3f7f36e83c04f5238e9ac2a75c6051d92f48997e 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -32,7 +32,8 @@ class Commit
   COMMIT_SHA_PATTERN = /\h{#{MIN_SHA_LENGTH},40}/.freeze
 
   def banzai_render_context(field)
-    context = { pipeline: :single_line, project: self.project }
+    pipeline = field == :description ? :commit_description : :single_line
+    context = { pipeline: pipeline, project: self.project }
     context[:author] = self.author if self.author
 
     context
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 9fb5b7efec6128201bb222fcd408a024c0d11a61..3469d5d795c98317a4ddac687dfe13d5224b308f 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base
   end
 
   def group_name
-    name.to_s.gsub(%r{\d+[\.\s:/\\]+\d+\s*}, '').strip
+    name.to_s.gsub(%r{\d+[\s:/\\]+\d+\s*}, '').strip
   end
 
   def failed_but_allowed?
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 5a566f3ac0211b0d09391dc2da39a90d61779bb8..b45395343cce032f223c0f6ad0f44f12bae73b46 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -137,7 +137,7 @@ module Issuable
       fuzzy_search(query, [:title, :description])
     end
 
-    def sort(method, excluded_labels: [])
+    def sort_by_attribute(method, excluded_labels: [])
       sorted =
         case method.to_s
         when 'downvotes_desc'     then order_downvotes_desc
diff --git a/app/models/concerns/milestoneish.rb b/app/models/concerns/milestoneish.rb
index caf8afa97f93c48fc9bcc03d21792443a73093b5..5130ecec47201c2b14e0bd5da1a89988e3606483 100644
--- a/app/models/concerns/milestoneish.rb
+++ b/app/models/concerns/milestoneish.rb
@@ -45,11 +45,11 @@ module Milestoneish
   end
 
   def sorted_issues(user)
-    issues_visible_to_user(user).preload_associations.sort('label_priority')
+    issues_visible_to_user(user).preload_associations.sort_by_attribute('label_priority')
   end
 
   def sorted_merge_requests
-    merge_requests.sort('label_priority')
+    merge_requests.sort_by_attribute('label_priority')
   end
 
   def upcoming?
diff --git a/app/models/group.rb b/app/models/group.rb
index d99af79b5fe30a668102fbb39a6c85f461a937da..3cfe21ac93bd76a124ed2b4f0b6955ffa774d9ae 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -53,7 +53,7 @@ class Group < Namespace
       Gitlab::Database.postgresql?
     end
 
-    def sort(method)
+    def sort_by_attribute(method)
       if method == 'storage_size_desc'
         # storage_size is a virtual column so we need to
         # pass a string to avoid AR adding the table name
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 6a94d60c828fd13c959343b57e62d2ede25b890b..13abc6c1a0db99685235ce0bfa0afe687e99fcbe 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -116,7 +116,7 @@ class Issue < ActiveRecord::Base
     'project_id'
   end
 
-  def self.sort(method, excluded_labels: [])
+  def self.sort_by_attribute(method, excluded_labels: [])
     case method.to_s
     when 'due_date'      then order_due_date_asc
     when 'due_date_asc'  then order_due_date_asc
diff --git a/app/models/member.rb b/app/models/member.rb
index e1a32148538d56a182b910a2b36534ea2ec7e3cf..eac4a22a03f6af4fed0943430fa2f9aca5b7a918 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -96,7 +96,7 @@ class Member < ActiveRecord::Base
       joins(:user).merge(User.search(query))
     end
 
-    def sort(method)
+    def sort_by_attribute(method)
       case method.to_s
       when 'access_level_asc' then reorder(access_level: :asc)
       when 'access_level_desc' then reorder(access_level: :desc)
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index e7d397f40f5572f051fe974f2515fb6ed56d0160..dafae58d12138c3ce6940f547f011ca042475587 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -138,7 +138,7 @@ class Milestone < ActiveRecord::Base
     User.joins(assigned_issues: :milestone).where("milestones.id = ?", id).uniq
   end
 
-  def self.sort(method)
+  def self.sort_by_attribute(method)
     case method.to_s
     when 'due_date_asc'
       reorder(Gitlab::Database.nulls_last_order('due_date', 'ASC'))
diff --git a/app/models/note.rb b/app/models/note.rb
index 787a80f0196485dcc177cda2d40360f0f1010f02..0f5fb529a87c6d4d5045f48f0981467f26fa371a 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -379,12 +379,15 @@ class Note < ActiveRecord::Base
   def expire_etag_cache
     return unless noteable&.discussions_rendered_on_frontend?
 
-    key = Gitlab::Routing.url_helpers.project_noteable_notes_path(
+    Gitlab::EtagCaching::Store.new.touch(etag_key)
+  end
+
+  def etag_key
+    Gitlab::Routing.url_helpers.project_noteable_notes_path(
       project,
       target_type: noteable_type.underscore,
       target_id: noteable_id
     )
-    Gitlab::EtagCaching::Store.new.touch(key)
   end
 
   def touch(*args)
diff --git a/app/models/project.rb b/app/models/project.rb
index b343786d2c9b3c5a4ac80a5c375e0908c3501298..32289106f2855a2e4709a4e297928939c583b5e9 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -436,7 +436,7 @@ class Project < ActiveRecord::Base
       Gitlab::VisibilityLevel.options
     end
 
-    def sort(method)
+    def sort_by_attribute(method)
       case method.to_s
       when 'storage_size_desc'
         # storage_size is a joined column so we need to
@@ -566,9 +566,7 @@ class Project < ActiveRecord::Base
   def add_import_job
     job_id =
       if forked?
-        RepositoryForkWorker.perform_async(id,
-                                           forked_from_project.repository_storage_path,
-                                           forked_from_project.disk_path)
+        RepositoryForkWorker.perform_async(id)
       elsif gitlab_project_import?
         # Do not retry on Import/Export until https://gitlab.com/gitlab-org/gitlab-ce/issues/26189 is solved.
         RepositoryImportWorker.set(retry: false).perform_async(self.id)
@@ -1068,6 +1066,16 @@ class Project < ActiveRecord::Base
     end
   end
 
+  # This will return all `lfs_objects` that are accessible to the project.
+  # So this might be `self.lfs_objects` if the project is not part of a fork
+  # network, or it is the base of the fork network.
+  #
+  # TODO: refactor this to get the correct lfs objects when implementing
+  #       https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
+  def all_lfs_objects
+    lfs_storage_project.lfs_objects
+  end
+
   def personal?
     !group
   end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 8afacd188e0e629c3a892d6baefcb266c06eeb27..a2ab405fdbec7d42675b92bf525e11b8c6632d14 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -50,7 +50,7 @@ class Todo < ActiveRecord::Base
     # Priority sorting isn't displayed in the dropdown, because we don't show
     # milestones, but still show something if the user has a URL with that
     # selected.
-    def sort(method)
+    def sort_by_attribute(method)
       sorted =
         case method.to_s
         when 'priority', 'label_priority' then order_by_labels_priority
diff --git a/app/models/user.rb b/app/models/user.rb
index f934b6542257cc36a321dae0a312fd9bd370f4c7..ba51595e6a3abd1df902b182ebd0b7cd6b99136f 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -256,7 +256,7 @@ class User < ActiveRecord::Base
       end
     end
 
-    def sort(method)
+    def sort_by_attribute(method)
       order_method = method || 'id_desc'
 
       case order_method.to_s
diff --git a/app/serializers/build_metadata_entity.rb b/app/serializers/build_metadata_entity.rb
index 39f429aa6c37b8ffd3302613217c5ead3e7ca1cc..f16f3badffa0a8cfaec0c3417c6f10e15fcfd4cb 100644
--- a/app/serializers/build_metadata_entity.rb
+++ b/app/serializers/build_metadata_entity.rb
@@ -1,8 +1,5 @@
 class BuildMetadataEntity < Grape::Entity
-  expose :timeout_human_readable do |metadata|
-    metadata.timeout_human_readable unless metadata.timeout.nil?
-  end
-
+  expose :timeout_human_readable
   expose :timeout_source do |metadata|
     metadata.present.timeout_source
   end
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
index bbbcf6a97c189e8ca70a92f3126d7989d303c46b..718fb35e62d0cfa21c549fade0495b9aa1e55227 100644
--- a/app/serializers/discussion_entity.rb
+++ b/app/serializers/discussion_entity.rb
@@ -4,7 +4,9 @@ class DiscussionEntity < Grape::Entity
   expose :id, :reply_id
   expose :expanded?, as: :expanded
 
-  expose :notes, using: NoteEntity
+  expose :notes do |discussion, opts|
+    request.note_entity.represent(discussion.notes, opts)
+  end
 
   expose :individual_note?, as: :individual_note
   expose :resolvable?, as: :resolvable
@@ -12,7 +14,7 @@ class DiscussionEntity < Grape::Entity
   expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion|
     resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id)
   end
-  expose :resolve_with_issue_path do |discussion|
+  expose :resolve_with_issue_path, if: -> (d, _) { d.resolvable? } do |discussion|
     new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id)
   end
 
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index 4ccf0bca476084a938161bd4a053b851634025aa..c964aa9c99be6ea51dad576b1aeb271a793d3ef9 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -5,10 +5,6 @@ class NoteEntity < API::Entities::Note
 
   expose :author, using: NoteUserEntity
 
-  expose :human_access do |note|
-    note.project.team.human_max_access(note.author_id)
-  end
-
   unexpose :note, as: :body
   expose :note
 
@@ -37,36 +33,10 @@ class NoteEntity < API::Entities::Note
 
   expose :emoji_awardable?, as: :emoji_awardable
   expose :award_emoji, if: -> (note, _) { note.emoji_awardable? }, using: AwardEmojiEntity
-  expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note|
-    if note.for_personal_snippet?
-      toggle_award_emoji_snippet_note_path(note.noteable, note)
-    else
-      toggle_award_emoji_project_note_path(note.project, note.id)
-    end
-  end
 
   expose :report_abuse_path do |note|
     new_abuse_report_path(user_id: note.author.id, ref_url: Gitlab::UrlBuilder.build(note))
   end
 
-  expose :path do |note|
-    if note.for_personal_snippet?
-      snippet_note_path(note.noteable, note)
-    else
-      project_note_path(note.project, note)
-    end
-  end
-
-  expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
-    resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id)
-  end
-
-  expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
-    new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id)
-  end
-
   expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? }
-  expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note|
-    delete_attachment_project_note_path(note.project, note)
-  end
 end
diff --git a/app/serializers/note_serializer.rb b/app/serializers/note_serializer.rb
deleted file mode 100644
index 2afe40d7a34bf67e3de870a6f16f30ff64b2d5d4..0000000000000000000000000000000000000000
--- a/app/serializers/note_serializer.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-class NoteSerializer < BaseSerializer
-  entity NoteEntity
-end
diff --git a/app/serializers/project_note_entity.rb b/app/serializers/project_note_entity.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e541bfbee8d11bc6fb4c5af678ad79c06d3702d6
--- /dev/null
+++ b/app/serializers/project_note_entity.rb
@@ -0,0 +1,25 @@
+class ProjectNoteEntity < NoteEntity
+  expose :human_access do |note|
+    note.project.team.human_max_access(note.author_id)
+  end
+
+  expose :toggle_award_path, if: -> (note, _) { note.emoji_awardable? } do |note|
+    toggle_award_emoji_project_note_path(note.project, note.id)
+  end
+
+  expose :path do |note|
+    project_note_path(note.project, note)
+  end
+
+  expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+    resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id)
+  end
+
+  expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+    new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id)
+  end
+
+  expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note|
+    delete_attachment_project_note_path(note.project, note)
+  end
+end
diff --git a/app/serializers/project_note_serializer.rb b/app/serializers/project_note_serializer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..763ad0bdb3fab85f167044f907e2b9b267d8895e
--- /dev/null
+++ b/app/serializers/project_note_serializer.rb
@@ -0,0 +1,3 @@
+class ProjectNoteSerializer < BaseSerializer
+  entity ProjectNoteEntity
+end
diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb
index 15fed7d17c13c888fbafcd5d639c23141fc53050..3ceab209f3feb1b0b4366c15d6f4e63aa536324d 100644
--- a/app/services/boards/issues/move_service.rb
+++ b/app/services/boards/issues/move_service.rb
@@ -42,7 +42,10 @@ module Boards
           )
         end
 
-        attrs[:move_between_ids] = move_between_ids if move_between_ids
+        if move_between_ids
+          attrs[:move_between_ids] = move_between_ids
+          attrs[:board_group_id] =  board.group&.id
+        end
 
         attrs
       end
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index bebc90c7a8d44995809c46ba832bfdcddf7334ac..02f1c709374b947526b213d664c8ad5b66d8704b 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -12,11 +12,15 @@ module Boards
       private
 
       def available_labels_for(board)
+        options = { include_ancestor_groups: true }
+
         if board.group_board?
-          parent.labels
+          options.merge!(group_id: parent.id, only_group_labels: true)
         else
-          LabelsFinder.new(current_user, project_id: parent.id).execute
+          options[:project_id] = parent.id
         end
+
+        LabelsFinder.new(current_user, options).execute
       end
 
       def next_position(board)
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 02fb48108fbd91a04e217d3b3fb68ec7d6fb8283..91ec702fbc6d9024c483562acd464a535226631d 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -106,7 +106,7 @@ class IssuableBaseService < BaseService
   end
 
   def available_labels
-    @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
+    @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id, include_ancestor_groups: true).execute
   end
 
   def handle_quick_actions_on_create(issuable)
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index d7aa7e2347ea1dad62541463714dd1fbe462d578..4161932ad2ad77c98580656662bed42b3268a862 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -55,9 +55,10 @@ module Issues
       return unless params[:move_between_ids]
 
       after_id, before_id = params.delete(:move_between_ids)
+      board_group_id = params.delete(:board_group_id)
 
-      issue_before = get_issue_if_allowed(issue.project, before_id) if before_id
-      issue_after = get_issue_if_allowed(issue.project, after_id) if after_id
+      issue_before = get_issue_if_allowed(before_id, board_group_id)
+      issue_after = get_issue_if_allowed(after_id, board_group_id)
 
       issue.move_between(issue_before, issue_after)
     end
@@ -84,8 +85,16 @@ module Issues
 
     private
 
-    def get_issue_if_allowed(project, id)
-      issue = project.issues.find(id)
+    def get_issue_if_allowed(id, board_group_id = nil)
+      return unless id
+
+      issue =
+        if board_group_id
+          IssuesFinder.new(current_user, group_id: board_group_id).find_by(id: id)
+        else
+          project.issues.find(id)
+        end
+
       issue if can?(current_user, :update_issue, issue)
     end
 
diff --git a/app/services/projects/autocomplete_service.rb b/app/services/projects/autocomplete_service.rb
index e61ecb696d0a9ba26a5938213c8f2dc053e22eed..346971138b16e2967242f1a43f73c50cfddd0f5f 100644
--- a/app/services/projects/autocomplete_service.rb
+++ b/app/services/projects/autocomplete_service.rb
@@ -21,7 +21,8 @@ module Projects
     end
 
     def labels(target = nil)
-      labels = LabelsFinder.new(current_user, project_id: project.id).execute.select([:color, :title])
+      labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true)
+        .execute.select([:color, :title])
 
       return labels unless target&.respond_to?(:labels)
 
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index 402cddd3ec1b69f4848d38b7b2d68eb758d9d392..7bf0b90b49178c89c50239fccf59adadab170c7d 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -28,7 +28,7 @@ module Projects
       end
 
       def save_services
-        [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
+        [version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver].all?(&:save)
       end
 
       def version_saver
@@ -55,6 +55,10 @@ module Projects
         Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
       end
 
+      def lfs_saver
+        Gitlab::ImportExport::LfsSaver.new(project: project, shared: @shared)
+      end
+
       def cleanup_and_notify_error
         Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
 
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index a3828acc50b408995ccb6986ff07e01f6801f61c..bdd9598f85ac65686f6a38a2b4cb00a6e9848ee0 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -61,7 +61,7 @@ module Projects
           project.ensure_repository
           project.repository.fetch_as_mirror(project.import_url, refmap: refmap)
         else
-          gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, project.import_url)
+          gitlab_shell.import_repository(project.repository_storage, project.disk_path, project.import_url)
         end
       rescue Gitlab::Shell::Error, Gitlab::Git::RepositoryMirroring::RemoteError => e
         # Expire cache to prevent scenarios such as:
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 9c8877be14e4f342068effd01391c5cc762dcb1a..7e228d1833dc1916c70964da69466dcf54e60d13 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -31,15 +31,17 @@ module Projects
 
         # Check if we did extract public directory
         archive_public_path = File.join(archive_path, 'public')
-        raise FailedToExtractError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
+        raise InvaildStateError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
         raise InvaildStateError, 'pages are outdated' unless latest?
 
         deploy_page!(archive_public_path)
         success
       end
-    rescue InvaildStateError, FailedToExtractError => e
-      register_failure
+    rescue InvaildStateError => e
       error(e.message)
+    rescue => e
+      error(e.message, false)
+      raise e
     end
 
     private
@@ -50,12 +52,13 @@ module Projects
       super
     end
 
-    def error(message, http_status = nil)
+    def error(message, allow_delete_artifact = true)
+      register_failure
       log_error("Projects::UpdatePagesService: #{message}")
       @status.allow_failure = !latest?
       @status.description = message
       @status.drop(:script_failure)
-      delete_artifact!
+      delete_artifact! if allow_delete_artifact
       super
     end
 
@@ -76,7 +79,7 @@ module Projects
       elsif artifacts.ends_with?('.zip')
         extract_zip_archive!(temp_path)
       else
-        raise FailedToExtractError, 'unsupported artifacts format'
+        raise InvaildStateError, 'unsupported artifacts format'
       end
     end
 
@@ -91,13 +94,13 @@ module Projects
     end
 
     def extract_zip_archive!(temp_path)
-      raise FailedToExtractError, 'missing artifacts metadata' unless build.artifacts_metadata?
+      raise InvaildStateError, 'missing artifacts metadata' unless build.artifacts_metadata?
 
       # Calculate page size after extract
       public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
 
       if public_entry.total_size > max_size
-        raise FailedToExtractError, "artifacts for pages are too large: #{public_entry.total_size}"
+        raise InvaildStateError, "artifacts for pages are too large: #{public_entry.total_size}"
       end
 
       # Requires UnZip at least 6.00 Info-ZIP.
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index cba49faac31e9d04b8a43a62d552d95faf0a4713..6cc51b6ee1bb1926c6dc4c398ca3831e90b9f1e7 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -200,7 +200,7 @@ module QuickActions
     end
     params '~label1 ~"label 2"'
     condition do
-      available_labels = LabelsFinder.new(current_user, project_id: project.id).execute
+      available_labels = LabelsFinder.new(current_user, project_id: project.id, include_ancestor_groups: true).execute
 
       current_user.can?(:"admin_#{issuable.to_ability_name}", project) &&
         available_labels.any?
@@ -562,7 +562,7 @@ module QuickActions
 
     def find_labels(labels_param)
       extract_references(labels_param, :label) |
-        LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split).execute
+        LabelsFinder.new(current_user, project_id: project.id, name: labels_param.split, include_ancestor_groups: true).execute
     end
 
     def find_label_references(labels_param)
@@ -593,6 +593,7 @@ module QuickActions
 
     def extract_references(arg, type)
       ext = Gitlab::ReferenceExtractor.new(project, current_user)
+
       ext.analyze(arg, author: current_user)
 
       ext.references(type)
diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..bb3fa26a33e3ac3d9cebaaca776d20676b1c766e
--- /dev/null
+++ b/app/views/admin/application_settings/_abuse.html.haml
@@ -0,0 +1,12 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+  = form_errors(@application_setting)
+
+  %fieldset
+    .form-group
+      = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.text_field :admin_notification_email, class: 'form-control'
+        .help-block
+          Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_email.html.haml b/app/views/admin/application_settings/_email.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..6c89f1c4e98312bba1bfff94e754f7a0061660a7
--- /dev/null
+++ b/app/views/admin/application_settings/_email.html.haml
@@ -0,0 +1,26 @@
+= 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 :email_author_in_body do
+            = f.check_box :email_author_in_body
+            Include author name in notification email body
+          .help-block
+            Some email servers do not support overriding the email sender name.
+            Enable this option to include the name of the author of the issue,
+            merge request or comment in the email body instead.
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :html_emails_enabled do
+            = f.check_box :html_emails_enabled
+            Enable HTML emails
+          .help-block
+            By default GitLab sends emails in HTML and plain text formats so mail
+            clients can choose what format to use. Disable this option if you only
+            want to send emails in plain text format.
+
+  = 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
deleted file mode 100644
index 309c7ed5dfa1d9187baa5f7d90754845b276e667..0000000000000000000000000000000000000000
--- a/app/views/admin/application_settings/_form.html.haml
+++ /dev/null
@@ -1,386 +0,0 @@
-= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
-  = form_errors(@application_setting)
-
-  - if Gitlab.config.registry.enabled
-    %fieldset
-      %legend Container Registry
-      .form-group
-        = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2'
-        .col-sm-10
-          = f.number_field :container_registry_token_expire_delay, class: 'form-control'
-
-  %fieldset
-    %legend Abuse reports
-    .form-group
-      = f.label :admin_notification_email, 'Abuse reports notification email', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.text_field :admin_notification_email, class: 'form-control'
-        .help-block
-          Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.
-
-  %fieldset
-    %legend Error Reporting and Logging
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :sentry_enabled do
-            = f.check_box :sentry_enabled
-            Enable Sentry
-          .help-block
-            %p This setting requires a restart to take effect.
-            Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
-            %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
-
-    .form-group
-      = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.text_field :sentry_dsn, class: 'form-control'
-
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :clientside_sentry_enabled do
-            = f.check_box :clientside_sentry_enabled
-            Enable Clientside Sentry
-          .help-block
-            Sentry can also be used for reporting and logging clientside exceptions.
-            %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/
-
-    .form-group
-      = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.text_field :clientside_sentry_dsn, class: 'form-control'
-
-  %fieldset
-    %legend Repository Storage
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :hashed_storage_enabled do
-            = f.check_box :hashed_storage_enabled
-            Create new projects using hashed storage paths
-          .help-block
-            Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents
-            repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance.
-            %em (EXPERIMENTAL)
-    .form-group
-      = f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
-          {include_hidden: false}, multiple: true, class: 'form-control'
-        .help-block
-          Manage repository storage paths. Learn more in the
-          = succeed "." do
-            = link_to "repository storages documentation", help_page_path("administration/repository_storages")
-
-  %fieldset
-    %legend Git Storage Circuitbreaker settings
-    .form-group
-      = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :circuitbreaker_check_interval, class: 'form-control'
-        .help-block
-          = circuitbreaker_check_interval_help_text
-    .form-group
-      = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :circuitbreaker_access_retries, class: 'form-control'
-        .help-block
-          = circuitbreaker_access_retries_help_text
-    .form-group
-      = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
-        .help-block
-          = circuitbreaker_storage_timeout_help_text
-    .form-group
-      = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
-        .help-block
-          = circuitbreaker_failure_count_help_text
-    .form-group
-      = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
-        .help-block
-          = circuitbreaker_failure_reset_time_help_text
-
-  %fieldset
-    %legend Repository Checks
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :repository_checks_enabled do
-            = f.check_box :repository_checks_enabled
-            Enable Repository Checks
-          .help-block
-            GitLab will periodically run
-            %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck'
-            in all project and wiki repositories to look for silent disk corruption issues.
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
-        .help-block
-          If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
-
-  - if koding_enabled?
-    %fieldset
-      %legend Koding
-      .form-group
-        .col-sm-offset-2.col-sm-10
-          .checkbox
-            = f.label :koding_enabled do
-              = f.check_box :koding_enabled
-              Enable Koding
-          .help-block
-            Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
-      .form-group
-        = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
-        .col-sm-10
-          = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
-          .help-block
-            Koding has integration enabled out of the box for the
-            %strong gitlab
-            team, and you need to provide that team's URL here. Learn more in the
-            = succeed "." do
-              = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
-
-  %fieldset
-    %legend PlantUML
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :plantuml_enabled do
-            = f.check_box :plantuml_enabled
-            Enable PlantUML
-    .form-group
-      = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
-        .help-block
-          Allow rendering of
-          = link_to "PlantUML", "http://plantuml.com"
-          diagrams in Asciidoc documents using an external PlantUML service.
-
-  %fieldset
-    %legend#usage-statistics Usage statistics
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :version_check_enabled do
-            = f.check_box :version_check_enabled
-            Enable version check
-          .help-block
-            GitLab will inform you if a new version is available.
-            = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
-            about what information is shared with GitLab Inc.
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        - can_be_configured = @application_setting.usage_ping_can_be_configured?
-        .checkbox
-          = f.label :usage_ping_enabled do
-            = f.check_box :usage_ping_enabled, disabled: !can_be_configured
-            Enable usage ping
-          .help-block
-            - if can_be_configured
-              To help improve GitLab and its user experience, GitLab will
-              periodically collect usage information.
-              = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
-              about what information is shared with GitLab Inc. Visit
-              = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
-              to see the JSON payload sent.
-            - else
-              The usage ping is disabled, and cannot be configured through this
-              form. For more information, see the documentation on
-              = succeed '.' do
-                = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
-
-  %fieldset
-    %legend Email
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :email_author_in_body do
-            = f.check_box :email_author_in_body
-            Include author name in notification email body
-          .help-block
-            Some email servers do not support overriding the email sender name.
-            Enable this option to include the name of the author of the issue,
-            merge request or comment in the email body instead.
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :html_emails_enabled do
-            = f.check_box :html_emails_enabled
-            Enable HTML emails
-          .help-block
-            By default GitLab sends emails in HTML and plain text formats so mail
-            clients can choose what format to use. Disable this option if you only
-            want to send emails in plain text format.
-  %fieldset
-    %legend Automatic Git repository housekeeping
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :housekeeping_enabled do
-            = f.check_box :housekeeping_enabled
-            Enable automatic repository housekeeping (git repack, git gc)
-          .help-block
-            If you keep automatic housekeeping disabled for a long time Git
-            repository access on your GitLab server will become slower and your
-            repositories will use more disk space. We recommend to always leave
-            this enabled.
-        .checkbox
-          = f.label :housekeeping_bitmaps_enabled do
-            = f.check_box :housekeeping_bitmaps_enabled
-            Enable Git pack file bitmap creation
-          .help-block
-            Creating pack file bitmaps makes housekeeping take a little longer but
-            bitmaps should accelerate 'git clone' performance.
-    .form-group
-      = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :housekeeping_incremental_repack_period, class: 'form-control'
-        .help-block
-          Number of Git pushes after which an incremental 'git repack' is run.
-    .form-group
-      = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :housekeeping_full_repack_period, class: 'form-control'
-        .help-block
-          Number of Git pushes after which a full 'git repack' is run.
-    .form-group
-      = f.label :housekeeping_gc_period, 'Git GC period', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :housekeeping_gc_period, class: 'form-control'
-        .help-block
-          Number of Git pushes after which 'git gc' is run.
-
-  %fieldset
-    %legend Gitaly Timeouts
-    .form-group
-      = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :gitaly_timeout_default, class: 'form-control'
-        .help-block
-          Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
-          for git fetch/push operations or Sidekiq jobs.
-    .form-group
-      = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :gitaly_timeout_fast, class: 'form-control'
-        .help-block
-          Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
-          If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
-          can help maintain the stability of the GitLab instance.
-    .form-group
-      = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :gitaly_timeout_medium, class: 'form-control'
-        .help-block
-          Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
-
-  %fieldset
-    %legend Web terminal
-    .form-group
-      = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :terminal_max_session_time, class: 'form-control'
-        .help-block
-          Maximum time for web terminal websocket connection (in seconds).
-          0 for unlimited.
-
-  %fieldset
-    %legend Real-time features
-    .form-group
-      = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.text_field :polling_interval_multiplier, class: 'form-control'
-        .help-block
-          Change this value to influence how frequently the GitLab UI polls for updates.
-          If you set the value to 2 all polling intervals are multiplied
-          by 2, which means that polling happens half as frequently.
-          The multiplier can also have a decimal value.
-          The default value (1) is a reasonable choice for the majority of GitLab
-          installations. Set to 0 to completely disable polling.
-          = link_to icon('question-circle'), help_page_path('administration/polling')
-
-  %fieldset
-    %legend Performance optimization
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :authorized_keys_enabled do
-            = f.check_box :authorized_keys_enabled
-            Write to "authorized_keys" file
-          .help-block
-            By default, we write to the "authorized_keys" file to support Git
-            over SSH without additional configuration. GitLab can be optimized
-            to authenticate SSH keys via the database file. Only uncheck this
-            if you have configured your OpenSSH server to use the
-            AuthorizedKeysCommand. Click on the help icon for more details.
-            = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
-
-  %fieldset
-    %legend User and IP Rate Limits
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :throttle_unauthenticated_enabled do
-            = f.check_box :throttle_unauthenticated_enabled
-            Enable unauthenticated request rate limit
-          %span.help-block
-            Helps reduce request volume (e.g. from crawlers or abusive bots)
-    .form-group
-      = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
-    .form-group
-      = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :throttle_authenticated_api_enabled do
-            = f.check_box :throttle_authenticated_api_enabled
-            Enable authenticated API request rate limit
-          %span.help-block
-            Helps reduce request volume (e.g. from crawlers or abusive bots)
-    .form-group
-      = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
-    .form-group
-      = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :throttle_authenticated_web_enabled do
-            = f.check_box :throttle_authenticated_web_enabled
-            Enable authenticated web request rate limit
-          %span.help-block
-            Helps reduce request volume (e.g. from crawlers or abusive bots)
-    .form-group
-      = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
-    .form-group
-      = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
-      .col-sm-10
-        = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
-
-  %fieldset
-    %legend Outbound requests
-    .form-group
-      .col-sm-offset-2.col-sm-10
-        .checkbox
-          = f.label :allow_local_requests_from_hooks_and_services do
-            = f.check_box :allow_local_requests_from_hooks_and_services
-            Allow requests to the local network from hooks and services
-
-  .form-actions
-    = f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..4acc5b3a0c55d75f920bdbce2993d505224abd6c
--- /dev/null
+++ b/app/views/admin/application_settings/_gitaly.html.haml
@@ -0,0 +1,27 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+  = form_errors(@application_setting)
+
+  %fieldset
+    .form-group
+      = f.label :gitaly_timeout_default, 'Default Timeout Period', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :gitaly_timeout_default, class: 'form-control'
+        .help-block
+          Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced
+          for git fetch/push operations or Sidekiq jobs.
+    .form-group
+      = f.label :gitaly_timeout_fast, 'Fast Timeout Period', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :gitaly_timeout_fast, class: 'form-control'
+        .help-block
+          Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast.
+          If they exceed this threshold, there may be a problem with a storage shard and 'failing fast'
+          can help maintain the stability of the GitLab instance.
+    .form-group
+      = f.label :gitaly_timeout_medium, 'Medium Timeout Period', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :gitaly_timeout_medium, class: 'form-control'
+        .help-block
+          Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout.
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_ip_limits.html.haml b/app/views/admin/application_settings/_ip_limits.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..b83ffc375d9cb6be3d66557ea953b6f80ae70cf6
--- /dev/null
+++ b/app/views/admin/application_settings/_ip_limits.html.haml
@@ -0,0 +1,54 @@
+= 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 :throttle_unauthenticated_enabled do
+            = f.check_box :throttle_unauthenticated_enabled
+            Enable unauthenticated request rate limit
+          %span.help-block
+            Helps reduce request volume (e.g. from crawlers or abusive bots)
+    .form-group
+      = f.label :throttle_unauthenticated_requests_per_period, 'Max requests per period per IP', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :throttle_unauthenticated_requests_per_period, class: 'form-control'
+    .form-group
+      = f.label :throttle_unauthenticated_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :throttle_unauthenticated_period_in_seconds, class: 'form-control'
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :throttle_authenticated_api_enabled do
+            = f.check_box :throttle_authenticated_api_enabled
+            Enable authenticated API request rate limit
+          %span.help-block
+            Helps reduce request volume (e.g. from crawlers or abusive bots)
+    .form-group
+      = f.label :throttle_authenticated_api_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :throttle_authenticated_api_requests_per_period, class: 'form-control'
+    .form-group
+      = f.label :throttle_authenticated_api_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :throttle_authenticated_api_period_in_seconds, class: 'form-control'
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :throttle_authenticated_web_enabled do
+            = f.check_box :throttle_authenticated_web_enabled
+            Enable authenticated web request rate limit
+          %span.help-block
+            Helps reduce request volume (e.g. from crawlers or abusive bots)
+    .form-group
+      = f.label :throttle_authenticated_web_requests_per_period, 'Max requests per period per user', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :throttle_authenticated_web_requests_per_period, class: 'form-control'
+    .form-group
+      = f.label :throttle_authenticated_web_period_in_seconds, 'Rate limit period in seconds', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :throttle_authenticated_web_period_in_seconds, class: 'form-control'
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_koding.html.haml b/app/views/admin/application_settings/_koding.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..17358cf775be58b5b6efd28f403f5ee734e1cced
--- /dev/null
+++ b/app/views/admin/application_settings/_koding.html.haml
@@ -0,0 +1,24 @@
+= 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 :koding_enabled do
+            = f.check_box :koding_enabled
+            Enable Koding
+        .help-block
+          Koding integration has been deprecated since GitLab 10.0. If you disable your Koding integration, you will not be able to enable it again.
+    .form-group
+      = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
+        .help-block
+          Koding has integration enabled out of the box for the
+          %strong gitlab
+          team, and you need to provide that team's URL here. Learn more in the
+          = succeed "." do
+            = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_logging.html.haml b/app/views/admin/application_settings/_logging.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..44a11ddc1200d88275a8a20ef7c59524abe8ff45
--- /dev/null
+++ b/app/views/admin/application_settings/_logging.html.haml
@@ -0,0 +1,36 @@
+= 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 :sentry_enabled do
+            = f.check_box :sentry_enabled
+            Enable Sentry
+          .help-block
+            %p This setting requires a restart to take effect.
+            Sentry is an error reporting and logging tool which is currently not shipped with GitLab, get it here:
+            %a{ href: 'https://getsentry.com', target: '_blank', rel: 'noopener noreferrer' } https://getsentry.com
+
+    .form-group
+      = f.label :sentry_dsn, 'Sentry DSN', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.text_field :sentry_dsn, class: 'form-control'
+
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        .checkbox
+          = f.label :clientside_sentry_enabled do
+            = f.check_box :clientside_sentry_enabled
+            Enable Clientside Sentry
+          .help-block
+            Sentry can also be used for reporting and logging clientside exceptions.
+            %a{ href: 'https://sentry.io/for/javascript/', target: '_blank', rel: 'noopener noreferrer' } https://sentry.io/for/javascript/
+
+    .form-group
+      = f.label :clientside_sentry_dsn, 'Clientside Sentry DSN', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.text_field :clientside_sentry_dsn, class: 'form-control'
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..d10f609006d0576239b3df133af6b5a1a98a4ba1
--- /dev/null
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -0,0 +1,12 @@
+= 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 :allow_local_requests_from_hooks_and_services do
+            = f.check_box :allow_local_requests_from_hooks_and_services
+            Allow requests to the local network from hooks and services
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..01d5a31aa9f01710ed5e8efe16acee073a862939
--- /dev/null
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -0,0 +1,19 @@
+= 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 :authorized_keys_enabled do
+            = f.check_box :authorized_keys_enabled
+            Write to "authorized_keys" file
+          .help-block
+            By default, we write to the "authorized_keys" file to support Git
+            over SSH without additional configuration. GitLab can be optimized
+            to authenticate SSH keys via the database file. Only uncheck this
+            if you have configured your OpenSSH server to use the
+            AuthorizedKeysCommand. Click on the help icon for more details.
+            = link_to icon('question-circle'), help_page_path('administration/operations/fast_ssh_key_lookup')
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..56764b3fb81bdbbc245c643c686461b08e915041
--- /dev/null
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -0,0 +1,20 @@
+= 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 :plantuml_enabled do
+            = f.check_box :plantuml_enabled
+            Enable PlantUML
+    .form-group
+      = f.label :plantuml_url, 'PlantUML URL', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.text_field :plantuml_url, class: 'form-control', placeholder: 'http://gitlab.your-plantuml-instance.com:8080'
+        .help-block
+          Allow rendering of
+          = link_to "PlantUML", "http://plantuml.com"
+          diagrams in Asciidoc documents using an external PlantUML service.
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_realtime.html.haml b/app/views/admin/application_settings/_realtime.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..0a53a75119e8b601a12da77e0163f754d9961857
--- /dev/null
+++ b/app/views/admin/application_settings/_realtime.html.haml
@@ -0,0 +1,19 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+  = form_errors(@application_setting)
+
+  %fieldset
+    .form-group
+      = f.label :polling_interval_multiplier, 'Polling interval multiplier', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.text_field :polling_interval_multiplier, class: 'form-control'
+        .help-block
+          Change this value to influence how frequently the GitLab UI polls for updates.
+          If you set the value to 2 all polling intervals are multiplied
+          by 2, which means that polling happens half as frequently.
+          The multiplier can also have a decimal value.
+          The default value (1) is a reasonable choice for the majority of GitLab
+          installations. Set to 0 to completely disable polling.
+          = link_to icon('question-circle'), help_page_path('administration/polling')
+
+  = f.submit 'Save changes', class: "btn btn-success"
+
diff --git a/app/views/admin/application_settings/_registry.html.haml b/app/views/admin/application_settings/_registry.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..3451ef62458f68c52c62bb4a83919ff00d567051
--- /dev/null
+++ b/app/views/admin/application_settings/_registry.html.haml
@@ -0,0 +1,10 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+  = form_errors(@application_setting)
+
+  %fieldset
+    .form-group
+      = f.label :container_registry_token_expire_delay, 'Authorization token duration (minutes)', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :container_registry_token_expire_delay, class: 'form-control'
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..f33769b23c29131bfb040ae66b13ad3fff35a688
--- /dev/null
+++ b/app/views/admin/application_settings/_repository_check.html.haml
@@ -0,0 +1,62 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+  = form_errors(@application_setting)
+
+  %fieldset
+    .sub-section
+      %h4 Repository checks
+      .form-group
+        .col-sm-offset-2.col-sm-10
+          .checkbox
+            = f.label :repository_checks_enabled do
+              = f.check_box :repository_checks_enabled
+              Enable Repository Checks
+            .help-block
+              GitLab will periodically run
+              %a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck'
+              in all project and wiki repositories to look for silent disk corruption issues.
+      .form-group
+        .col-sm-offset-2.col-sm-10
+          = link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
+          .help-block
+            If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
+
+    .sub-section
+      %h4 Housekeeping
+      .form-group
+        .col-sm-offset-2.col-sm-10
+          .checkbox
+            = f.label :housekeeping_enabled do
+              = f.check_box :housekeeping_enabled
+              Enable automatic repository housekeeping (git repack, git gc)
+            .help-block
+              If you keep automatic housekeeping disabled for a long time Git
+              repository access on your GitLab server will become slower and your
+              repositories will use more disk space. We recommend to always leave
+              this enabled.
+          .checkbox
+            = f.label :housekeeping_bitmaps_enabled do
+              = f.check_box :housekeeping_bitmaps_enabled
+              Enable Git pack file bitmap creation
+            .help-block
+              Creating pack file bitmaps makes housekeeping take a little longer but
+              bitmaps should accelerate 'git clone' performance.
+      .form-group
+        = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'control-label col-sm-2'
+        .col-sm-10
+          = f.number_field :housekeeping_incremental_repack_period, class: 'form-control'
+          .help-block
+            Number of Git pushes after which an incremental 'git repack' is run.
+      .form-group
+        = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'control-label col-sm-2'
+        .col-sm-10
+          = f.number_field :housekeeping_full_repack_period, class: 'form-control'
+          .help-block
+            Number of Git pushes after which a full 'git repack' is run.
+      .form-group
+        = f.label :housekeeping_gc_period, 'Git GC period', class: 'control-label col-sm-2'
+        .col-sm-10
+          = f.number_field :housekeeping_gc_period, class: 'form-control'
+          .help-block
+            Number of Git pushes after which 'git gc' is run.
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..ac31977e1a98fb174f54cef7cea31abb24d40d44
--- /dev/null
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -0,0 +1,58 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+  = form_errors(@application_setting)
+
+  %fieldset
+    .sub-section
+      .form-group
+        .col-sm-offset-2.col-sm-10
+          .checkbox
+            = f.label :hashed_storage_enabled do
+              = f.check_box :hashed_storage_enabled
+              Create new projects using hashed storage paths
+            .help-block
+              Enable immutable, hash-based paths and repository names to store repositories on disk. This prevents
+              repositories from having to be moved or renamed when the Project URL changes and may improve disk I/O performance.
+              %em (EXPERIMENTAL)
+      .form-group
+        = f.label :repository_storages, 'Storage paths for new projects', class: 'control-label col-sm-2'
+        .col-sm-10
+          = f.select :repository_storages, repository_storages_options_for_select(@application_setting.repository_storages),
+            {include_hidden: false}, multiple: true, class: 'form-control'
+          .help-block
+            Manage repository storage paths. Learn more in the
+            = succeed "." do
+              = link_to "repository storages documentation", help_page_path("administration/repository_storages")
+    .sub-section
+      %h4 Circuit breaker
+      .form-group
+        = f.label :circuitbreaker_check_interval, _('Check interval'), class: 'control-label col-sm-2'
+        .col-sm-10
+          = f.number_field :circuitbreaker_check_interval, class: 'form-control'
+          .help-block
+            = circuitbreaker_check_interval_help_text
+      .form-group
+        = f.label :circuitbreaker_access_retries, _('Number of access attempts'), class: 'control-label col-sm-2'
+        .col-sm-10
+          = f.number_field :circuitbreaker_access_retries, class: 'form-control'
+          .help-block
+            = circuitbreaker_access_retries_help_text
+      .form-group
+        = f.label :circuitbreaker_storage_timeout, _('Seconds to wait for a storage access attempt'), class: 'control-label col-sm-2'
+        .col-sm-10
+          = f.number_field :circuitbreaker_storage_timeout, class: 'form-control'
+          .help-block
+            = circuitbreaker_storage_timeout_help_text
+      .form-group
+        = f.label :circuitbreaker_failure_count_threshold, _('Maximum git storage failures'), class: 'control-label col-sm-2'
+        .col-sm-10
+          = f.number_field :circuitbreaker_failure_count_threshold, class: 'form-control'
+          .help-block
+            = circuitbreaker_failure_count_help_text
+      .form-group
+        = f.label :circuitbreaker_failure_reset_time, _('Seconds before reseting failure information'), class: 'control-label col-sm-2'
+        .col-sm-10
+          = f.number_field :circuitbreaker_failure_reset_time, class: 'form-control'
+          .help-block
+            = circuitbreaker_failure_reset_time_help_text
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..36d8838803f5ef1f57867758ddd7b36f4e89e11c
--- /dev/null
+++ b/app/views/admin/application_settings/_terminal.html.haml
@@ -0,0 +1,13 @@
+= form_for @application_setting, url: admin_application_settings_path, html: { class: 'form-horizontal fieldset-form' } do |f|
+  = form_errors(@application_setting)
+
+  %fieldset
+    .form-group
+      = f.label :terminal_max_session_time, 'Max session time', class: 'control-label col-sm-2'
+      .col-sm-10
+        = f.number_field :terminal_max_session_time, class: 'form-control'
+        .help-block
+          Maximum time for web terminal websocket connection (in seconds).
+          0 for unlimited.
+
+  = f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..7684e2cfdd156cb3937e269840858971018e0380
--- /dev/null
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -0,0 +1,37 @@
+= 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 :version_check_enabled do
+            = f.check_box :version_check_enabled
+            Enable version check
+          .help-block
+            GitLab will inform you if a new version is available.
+            = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "version-check")
+            about what information is shared with GitLab Inc.
+    .form-group
+      .col-sm-offset-2.col-sm-10
+        - can_be_configured = @application_setting.usage_ping_can_be_configured?
+        .checkbox
+          = f.label :usage_ping_enabled do
+            = f.check_box :usage_ping_enabled, disabled: !can_be_configured
+            Enable usage ping
+          .help-block
+            - if can_be_configured
+              To help improve GitLab and its user experience, GitLab will
+              periodically collect usage information.
+              = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
+              about what information is shared with GitLab Inc. Visit
+              = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
+              to see the JSON payload sent.
+            - else
+              The usage ping is disabled, and cannot be configured through this
+              form. For more information, see the documentation on
+              = succeed '.' do
+                = link_to 'deactivating the usage ping', help_page_path('user/admin_area/settings/usage_statistics', anchor: 'deactivate-the-usage-ping')
+
+  = 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 d0e612e62e524a236f6f9b3ad159bb22fcfc74af..caaa93aa1e271435467ce92f8980c13d7ddd541c 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -76,7 +76,7 @@
     %button.btn.js-settings-toggle{ type: 'button' }
       = expanded ? 'Collapse' : 'Expand'
     %p
-      = _('Auto DevOps, runners amd job artifacts')
+      = _('Auto DevOps, runners and job artifacts')
   .settings-content
     = render 'ci_cd'
 
@@ -102,7 +102,7 @@
   .settings-content
     = render 'prometheus'
 
-%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) }
+%section.settings.as-performance-bar.no-animate#js-performance-bar-settings{ class: ('expanded' if expanded) }
   .settings-header
     %h4
       = _('Profiling - Performance bar')
@@ -136,5 +136,169 @@
   .settings-content
     = render 'spam'
 
-.prepend-top-20
-  = render 'form'
+%section.settings.as-abuse.no-animate#js-abuse-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Abuse reports')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Set notification email for abuse reports.')
+  .settings-content
+    = render 'abuse'
+
+%section.settings.as-logging.no-animate#js-logging-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Error Reporting and Logging')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Enable Sentry for error reporting and logging.')
+  .settings-content
+    = render 'logging'
+
+%section.settings.as-repository-storage.no-animate#js-repository-storage-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Repository storage')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Configure storage path and circuit breaker settings.')
+  .settings-content
+    = render 'repository_storage'
+
+%section.settings.as-repository-check.no-animate#js-repository-check-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Repository maintenance')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Configure automatic git checks and housekeeping on repositories.')
+  .settings-content
+    = render 'repository_check'
+
+- if Gitlab.config.registry.enabled
+  %section.settings.as-registry.no-animate#js-registry-settings{ class: ('expanded' if expanded) }
+    .settings-header
+      %h4
+        = _('Container Registry')
+      %button.btn.js-settings-toggle{ type: 'button' }
+        = expanded ? 'Collapse' : 'Expand'
+      %p
+        = _('Various container registry settings.')
+    .settings-content
+      = render 'registry'
+
+- if koding_enabled?
+  %section.settings.as-koding.no-animate#js-koding-settings{ class: ('expanded' if expanded) }
+    .settings-header
+      %h4
+        = _('Koding')
+      %button.btn.js-settings-toggle{ type: 'button' }
+        = expanded ? 'Collapse' : 'Expand'
+      %p
+        = _('Online IDE integration settings.')
+    .settings-content
+      = render 'koding'
+
+%section.settings.as-plantuml.no-animate#js-plantuml-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('PlantUML')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Allow rendering of PlantUML diagrams in Asciidoc documents.')
+  .settings-content
+    = render 'plantuml'
+
+%section.settings.as-usage.no-animate#js-usage-settings{ class: ('expanded' if expanded) }
+  .settings-header#usage-statistics
+    %h4
+      = _('Usage statistics')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Enable or disable version check and usage ping.')
+  .settings-content
+    = render 'usage'
+
+%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Email')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Various email settings.')
+  .settings-content
+    = render 'email'
+
+%section.settings.as-gitaly.no-animate#js-gitaly-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Gitaly')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Configure Gitaly timeouts.')
+  .settings-content
+    = render 'gitaly'
+
+%section.settings.as-terminal.no-animate#js-terminal-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Web terminal')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Set max session time for web terminal.')
+  .settings-content
+    = render 'terminal'
+
+%section.settings.as-realtime.no-animate#js-realtime-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Real-time features')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Change this value to influence how frequently the GitLab UI polls for updates.')
+  .settings-content
+    = render 'realtime'
+
+%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Performance optimization')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Various settings that affect GitLab performance.')
+  .settings-content
+    = render 'performance'
+
+%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('User and IP Rate Limits')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Configure limits for web and API requests.')
+  .settings-content
+    = render 'ip_limits'
+
+%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded) }
+  .settings-header
+    %h4
+      = _('Outbound requests')
+    %button.btn.js-settings-toggle{ type: 'button' }
+      = expanded ? 'Collapse' : 'Expand'
+    %p
+      = _('Allow requests to the local network from hooks and services.')
+  .settings-content
+    = render 'outbound'
diff --git a/app/views/notify/push_to_merge_request_email.html.haml b/app/views/notify/push_to_merge_request_email.html.haml
index 5cc6f21c0f36865a13fe3713d635f8f3f4f1ca64..4c507c08ed765128673423743b0379db26a338ea 100644
--- a/app/views/notify/push_to_merge_request_email.html.haml
+++ b/app/views/notify/push_to_merge_request_email.html.haml
@@ -1,7 +1,7 @@
 %h3
-  New commits were pushed to the merge request
+  = @updated_by_user.name
+  pushed new commits to merge request
   = link_to(@merge_request.to_reference, project_merge_request_url(@merge_request.target_project, @merge_request))
-  by #{@current_user.name}
 
 - if @existing_commits.any?
   - count = @existing_commits.size
diff --git a/app/views/notify/push_to_merge_request_email.text.haml b/app/views/notify/push_to_merge_request_email.text.haml
index d7722e5f41f0cbc0e796049a4991967f42029904..553f771f1a6b9ea7321bf36ed84741ff7e16ef9b 100644
--- a/app/views/notify/push_to_merge_request_email.text.haml
+++ b/app/views/notify/push_to_merge_request_email.text.haml
@@ -1,4 +1,4 @@
-New commits were pushed to the merge request #{@merge_request.to_reference} by #{@current_user.name}
+#{@updated_by_user.name} pushed new commits to merge request #{@merge_request.to_reference}
 \
 #{url_for(project_merge_request_url(@merge_request.target_project, @merge_request))}
 \
diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml
index 825bfd0707ff4e0bb9758d1788f90a490cfe3430..1e7d9444986ce4a07573bc58a9353c2058da3268 100644
--- a/app/views/projects/_export.html.haml
+++ b/app/views/projects/_export.html.haml
@@ -21,11 +21,11 @@
           %li Project uploads
           %li Project configuration including web hooks and services
           %li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
+          %li LFS objects
         %p
           The following items will NOT be exported:
         %ul
           %li Job traces and artifacts
-          %li LFS objects
           %li Container registry images
           %li CI variables
           %li Any encrypted tokens
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 461129a3e0e9de4e28c39ff2c9eccabbc2b5ff9a..74c5317428c234a5bb3ac267f75710c9a1235291 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -49,10 +49,10 @@
 
 .commit-box{ data: { project_path: project_path(@project) } }
   %h3.commit-title
-    = markdown(@commit.title, pipeline: :single_line, author: @commit.author)
+    = markdown_field(@commit, :title)
   - if @commit.description.present?
     %pre.commit-description
-      = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author))
+      = preserve(markdown_field(@commit, :description))
 
 .info-well
   .well-segment.branch-info
diff --git a/app/views/projects/commits/_commit.atom.builder b/app/views/projects/commits/_commit.atom.builder
index 50f7e7a3a336384a0f3317f140605460da77ac31..640b5ecf99e3dd86247268020ec12b2a89039d77 100644
--- a/app/views/projects/commits/_commit.atom.builder
+++ b/app/views/projects/commits/_commit.atom.builder
@@ -10,5 +10,5 @@ xml.entry do
     xml.email commit.author_email
   end
 
-  xml.summary markdown(commit.description, pipeline: :single_line), type: 'html'
+  xml.summary markdown_field(commit, :description), type: 'html'
 end
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml
index 2a0704bc7affc5010f382fd445386e4748db3c2c..a09c13176c3ba5fe77f6d0cc0ab181c8672a5c69 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml
@@ -2,7 +2,7 @@
   - if @protected_branches.empty?
     .panel-heading
       %h3.panel-title
-        Protected branch (#{@protected_branches.size})
+        Protected branch (#{@protected_branches_count})
     %p.settings-message.text-center
       There are currently no protected branches, protect a branch with the form above.
   - else
@@ -16,7 +16,7 @@
           %col
       %thead
         %tr
-          %th Protected branch (#{@protected_branches.size})
+          %th Protected branch (#{@protected_branches_count})
           %th Last commit
           %th Allowed to merge
           %th Allowed to push
diff --git a/app/views/projects/protected_tags/shared/_tags_list.html.haml b/app/views/projects/protected_tags/shared/_tags_list.html.haml
index 3f42ae58438957d7fbf780ed8f7aa8e67e33ef87..02908e16dc54b15e5cfa70716108e9815118aeb2 100644
--- a/app/views/projects/protected_tags/shared/_tags_list.html.haml
+++ b/app/views/projects/protected_tags/shared/_tags_list.html.haml
@@ -2,7 +2,7 @@
   - if @protected_tags.empty?
     .panel-heading
       %h3.panel-title
-        Protected tag (#{@protected_tags.size})
+        Protected tag (#{@protected_tags_count})
     %p.settings-message.text-center
       There are currently no protected tags, protect a tag with the form above.
   - else
@@ -17,7 +17,7 @@
           %col
       %thead
         %tr
-          %th Protected tag (#{@protected_tags.size})
+          %th Protected tag (#{@protected_tags_count})
           %th Last commit
           %th Allowed to create
           - if can_admin_project
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 6afcd447f28efae4526b98c0de5c5589647a84ce..975b9cb4729eb6e780734161a0b5ddac5d5ef90e 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -107,7 +107,7 @@
             - selected_labels.each do |label|
               = hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
             .dropdown
-              %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (project_labels_path(@project, :json) if @project) } }
+              %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{ type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:full_path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (labels_filter_path(false) if @project) } }
                 %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?) }
                   = multi_label_name(selected_labels, "Labels")
                 = icon('chevron-down', 'aria-hidden': 'true')
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index 712a63af532f3d15f368564c32c161b67ccfcf77..51fad4faf3618fc2f9b0e148b760447b56c6e7e8 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -1,28 +1,50 @@
-# Gitaly issue: https://gitlab.com/gitlab-org/gitaly/issues/1110
 class RepositoryForkWorker
   include ApplicationWorker
   include Gitlab::ShellAdapter
   include ProjectStartImport
   include ProjectImportOptions
 
-  def perform(project_id, forked_from_repository_storage_path, source_disk_path)
-    project = Project.find(project_id)
+  def perform(*args)
+    target_project_id = args.shift
+    target_project = Project.find(target_project_id)
 
-    return unless start_fork(project)
+    # By v10.8, we should've drained the queue of all jobs using the old arguments.
+    # We can remove the else clause if we're no longer logging the message in that clause.
+    # See https://gitlab.com/gitlab-org/gitaly/issues/1110
+    if args.empty?
+      source_project = target_project.forked_from_project
+      return target_project.mark_import_as_failed('Source project cannot be found.') unless source_project
 
-    Gitlab::Metrics.add_event(:fork_repository,
-                              source_path: source_disk_path,
-                              target_path: project.disk_path)
+      fork_repository(target_project, source_project.repository_storage, source_project.disk_path)
+    else
+      Rails.logger.info("Project #{target_project.id} is being forked using old-style arguments.")
+
+      source_repository_storage_path, source_disk_path = *args
 
-    result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_disk_path,
-                                          project.repository_storage_path, project.disk_path)
-    raise "Unable to fork project #{project_id} for repository #{source_disk_path} -> #{project.disk_path}" unless result
+      source_repository_storage_name = Gitlab.config.repositories.storages.find do |_, info|
+        info.legacy_disk_path == source_repository_storage_path
+      end&.first || raise("no shard found for path '#{source_repository_storage_path}'")
 
-    project.after_import
+      fork_repository(target_project, source_repository_storage_name, source_disk_path)
+    end
   end
 
   private
 
+  def fork_repository(target_project, source_repository_storage_name, source_disk_path)
+    return unless start_fork(target_project)
+
+    Gitlab::Metrics.add_event(:fork_repository,
+                              source_path: source_disk_path,
+                              target_path: target_project.disk_path)
+
+    result = gitlab_shell.fork_repository(source_repository_storage_name, source_disk_path,
+                                          target_project.repository_storage, target_project.disk_path)
+    raise "Unable to fork project #{target_project.id} for repository #{source_disk_path} -> #{target_project.disk_path}" unless result
+
+    target_project.after_import
+  end
+
   def start_fork(project)
     return true if start(project)
 
diff --git a/bin/rspec b/bin/rspec
index 6e6709219af4b84b143079e7d7b6493080c743f9..26583242051c4b0628072a88dcf65e7f6c170af1 100755
--- a/bin/rspec
+++ b/bin/rspec
@@ -1,4 +1,10 @@
 #!/usr/bin/env ruby
+
+# Remove these two lines below when upgraded to rails 5.0.
+# Allow run `rspec` command as `RAILS5=1 rspec ...` instead of `BUNDLE_GEMFILE=Gemfile.rails5 rspec ...`
+gemfile = %w[1 true].include?(ENV["RAILS5"]) ? "Gemfile.rails5" : "Gemfile"
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../#{gemfile}", __dir__)
+
 begin
   load File.expand_path('../spring', __FILE__)
 rescue LoadError => e
diff --git a/changelogs/unreleased/20394-protected-branches-wildcard.yml b/changelogs/unreleased/20394-protected-branches-wildcard.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3fa8ee4f69feeb7d9217c1e8ae34e08af1465c0c
--- /dev/null
+++ b/changelogs/unreleased/20394-protected-branches-wildcard.yml
@@ -0,0 +1,5 @@
+---
+title: Include matching branches and tags in protected branches / tags count
+merge_request:
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/39880-merge-method-api.yml b/changelogs/unreleased/39880-merge-method-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dd44a752c4fbc4b27ec7734e832c7c8116d32953
--- /dev/null
+++ b/changelogs/unreleased/39880-merge-method-api.yml
@@ -0,0 +1,5 @@
+---
+title: 'API: Add parameter merge_method to projects'
+merge_request: 18031
+author: Jan Beckmann
+type: added
diff --git a/changelogs/unreleased/41224-pipeline-icons.yml b/changelogs/unreleased/41224-pipeline-icons.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3fe05448d1cdbcf81f04ea8a280a8002490f12bc
--- /dev/null
+++ b/changelogs/unreleased/41224-pipeline-icons.yml
@@ -0,0 +1,5 @@
+---
+title: Increase dropdown width in pipeline graph & center action icon
+merge_request: 18089
+author:
+type: fixed
diff --git a/changelogs/unreleased/43745-store-metadata-checksum-for-artifacts.yml b/changelogs/unreleased/43745-store-metadata-checksum-for-artifacts.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6283e7979308c1970c52ef149c548d1de7cb2d4e
--- /dev/null
+++ b/changelogs/unreleased/43745-store-metadata-checksum-for-artifacts.yml
@@ -0,0 +1,5 @@
+---
+title: Store sha256 checksum of artifact metadata
+merge_request: 18149
+author:
+type: added
diff --git a/changelogs/unreleased/44425-use-gitlab_environment.yml b/changelogs/unreleased/44425-use-gitlab_environment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a774143d5f596f114dc4bcd0970a555416f64e0d
--- /dev/null
+++ b/changelogs/unreleased/44425-use-gitlab_environment.yml
@@ -0,0 +1,5 @@
+---
+title: Fix `gitlab-rake gitlab:two_factor:disable_for_all_users`
+merge_request: 18154
+author:
+type: fixed
diff --git a/changelogs/unreleased/44902-remove-rake-test-ci.yml b/changelogs/unreleased/44902-remove-rake-test-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..459de1c2ca372f4963c8d2543b92610b6efcac3f
--- /dev/null
+++ b/changelogs/unreleased/44902-remove-rake-test-ci.yml
@@ -0,0 +1,5 @@
+---
+title: Remove test_ci rake task
+merge_request: 18139
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-issues-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-issues-feature.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7defdc0a28ffd8b556d706062e938b50aeb70d4e
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-issues-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the spinach test with an rspec analog
+merge_request: 17950
+author: blackst0ne
+type: other
diff --git a/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-labels-feature.yml b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-labels-feature.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4e1bb15f150d75090af4feff010da8a6af86f5d8
--- /dev/null
+++ b/changelogs/unreleased/blackst0ne-replace-spinach-project-issues-labels-feature.yml
@@ -0,0 +1,5 @@
+---
+title: Replace the `project/issues/labels.feature` spinach test with an rspec analog
+merge_request: 18126
+author: blackst0ne
+type: other
diff --git a/changelogs/unreleased/bvl-export-import-lfs.yml b/changelogs/unreleased/bvl-export-import-lfs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dd1f499c3a304f5a3e7cd480d5c26854ac755bdd
--- /dev/null
+++ b/changelogs/unreleased/bvl-export-import-lfs.yml
@@ -0,0 +1,5 @@
+---
+title: Support LFS objects when importing/exporting GitLab project archives
+merge_request: 18115
+author:
+type: added
diff --git a/changelogs/unreleased/dm-flatten-tree-plus-chars.yml b/changelogs/unreleased/dm-flatten-tree-plus-chars.yml
new file mode 100644
index 0000000000000000000000000000000000000000..23f1b30d8fa1cddd4270e7b93a5a109625dc71ca
--- /dev/null
+++ b/changelogs/unreleased/dm-flatten-tree-plus-chars.yml
@@ -0,0 +1,5 @@
+---
+title: Fix links to subdirectories of a directory with a plus character in its path
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/feature_detect_co_authored_commits.yml b/changelogs/unreleased/feature_detect_co_authored_commits.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7b1269ed98229a8b6399ca92b44c8697a12d2282
--- /dev/null
+++ b/changelogs/unreleased/feature_detect_co_authored_commits.yml
@@ -0,0 +1,6 @@
+---
+title: Detect commit message trailers and link users properly to their accounts
+  on Gitlab
+merge_request: 17919
+author: cousine
+type: added
diff --git a/changelogs/unreleased/fj-174-better-ldap-connection-handling.yml b/changelogs/unreleased/fj-174-better-ldap-connection-handling.yml
new file mode 100644
index 0000000000000000000000000000000000000000..be0b83505fbc175cd842a7008ac42342c2277d83
--- /dev/null
+++ b/changelogs/unreleased/fj-174-better-ldap-connection-handling.yml
@@ -0,0 +1,5 @@
+---
+title: Add better LDAP connection handling
+merge_request: 18039
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue_40915.yml b/changelogs/unreleased/issue_40915.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2b6d98e69a6db05f7642fbdccb81b22caa3a4bfe
--- /dev/null
+++ b/changelogs/unreleased/issue_40915.yml
@@ -0,0 +1,5 @@
+---
+title: Allow assigning and filtering issuables by ancestor group labels
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/issue_44551.yml b/changelogs/unreleased/issue_44551.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d5265667b007cfb4cb053d57ac7f6cb7002824fe
--- /dev/null
+++ b/changelogs/unreleased/issue_44551.yml
@@ -0,0 +1,5 @@
+---
+title: Fix 404 in group boards when moving issue between lists
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/osw-41401-render-mr-commit-sha-instead-diffs.yml b/changelogs/unreleased/osw-41401-render-mr-commit-sha-instead-diffs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4497364132521d31cb84c46f7e4425509af7f75b
--- /dev/null
+++ b/changelogs/unreleased/osw-41401-render-mr-commit-sha-instead-diffs.yml
@@ -0,0 +1,5 @@
+---
+title: Render MR commit SHA instead "diffs" when viable
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/sh-gitlab-sidekiq-logger.yml b/changelogs/unreleased/sh-gitlab-sidekiq-logger.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f68d45d2f38e3bee8ba757c2022226a7eadac06d
--- /dev/null
+++ b/changelogs/unreleased/sh-gitlab-sidekiq-logger.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for Sidekiq JSON logging
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/sh-move-sidekiq-exporter-logs.yml b/changelogs/unreleased/sh-move-sidekiq-exporter-logs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..1990f4f61243ed5a2ce537765c2997d456ae135e
--- /dev/null
+++ b/changelogs/unreleased/sh-move-sidekiq-exporter-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Move Sidekiq exporter logs to log/sidekiq_exporter.log
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/zj-bump-gitaly.yml b/changelogs/unreleased/zj-bump-gitaly.yml
new file mode 100644
index 0000000000000000000000000000000000000000..eb28bed70e4a0dd03907b20783ca9282b2f4ba69
--- /dev/null
+++ b/changelogs/unreleased/zj-bump-gitaly.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade Gitaly to upgrade its charlock_holmes
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/zj-feature-gate-remove-http-api.yml b/changelogs/unreleased/zj-feature-gate-remove-http-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2095f60146c2a39b4ab9048031b2967d95631efb
--- /dev/null
+++ b/changelogs/unreleased/zj-feature-gate-remove-http-api.yml
@@ -0,0 +1,5 @@
+---
+title: Allow feature gates to be removed through the API
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/zj-opt-out-delete-refs.yml b/changelogs/unreleased/zj-opt-out-delete-refs.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b02a45eee17a88f4fb979a7b8cda61ac9e5796a2
--- /dev/null
+++ b/changelogs/unreleased/zj-opt-out-delete-refs.yml
@@ -0,0 +1,5 @@
+---
+title: Bulk deleting refs is handled by Gitaly by default
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/zj-opt-out-list-commits-by-oid.yml b/changelogs/unreleased/zj-opt-out-list-commits-by-oid.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3871293ee04183f1c62ad9182f6aedfa2f20f297
--- /dev/null
+++ b/changelogs/unreleased/zj-opt-out-list-commits-by-oid.yml
@@ -0,0 +1,5 @@
+---
+title: ListCommitsByOid is executed by Gitaly by default
+merge_request:
+author:
+type: performance
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 8db66037d610bef919ac539d90b693e00a43fc7b..126a9b8b8034d575f8895c4d5a55fd8ab6887fba 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -226,6 +226,10 @@ production: &base
     # plain_url: "http://..."     # default: https://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
     # ssl_url:   "https://..."    # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
 
+  ## Sidekiq
+  sidekiq:
+    log_format: default # (json is also supported)
+
   ## Auxiliary jobs
   # Periodically executed jobs, to self-heal GitLab, do external synchronizations, etc.
   # Please read here for more information: https://github.com/ondrejbartas/sidekiq-cron#adding-cron-job
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 69b59b26d8c2b1cdbf4e629f541893e5b37f9612..187e70868ea40395138809052b890b149a3d2d05 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -453,6 +453,12 @@ Settings.cron_jobs['pages_domain_verification_cron_worker'] ||= Settingslogic.ne
 Settings.cron_jobs['pages_domain_verification_cron_worker']['cron'] ||= '*/15 * * * *'
 Settings.cron_jobs['pages_domain_verification_cron_worker']['job_class'] = 'PagesDomainVerificationCronWorker'
 
+#
+# Sidekiq
+#
+Settings['sidekiq'] ||= Settingslogic.new({})
+Settings['sidekiq']['log_format'] ||= 'default'
+
 #
 # GitLab Shell
 #
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 161fb185c9bbc4b88c3c20d2f21eb9ca90c8736e..f6803eb0b5a5cc8f9a896ec1e19e2686037dde0e 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -5,16 +5,23 @@ queues_config_hash[:namespace] = Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE
 # Default is to retry 25 times with exponential backoff. That's too much.
 Sidekiq.default_worker_options = { retry: 3 }
 
+enable_json_logs = Gitlab.config.sidekiq.log_format == 'json'
+
 Sidekiq.configure_server do |config|
   config.redis = queues_config_hash
 
   config.server_middleware do |chain|
-    chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS']
+    chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS'] && !enable_json_logs
     chain.add Gitlab::SidekiqMiddleware::Shutdown
     chain.add Gitlab::SidekiqMiddleware::RequestStoreMiddleware unless ENV['SIDEKIQ_REQUEST_STORE'] == '0'
     chain.add Gitlab::SidekiqStatus::ServerMiddleware
   end
 
+  if enable_json_logs
+    Sidekiq.logger.formatter = Gitlab::SidekiqLogging::JSONFormatter.new
+    config.options[:job_logger] = Gitlab::SidekiqLogging::StructuredLogger
+  end
+
   config.client_middleware do |chain|
     chain.add Gitlab::SidekiqStatus::ClientMiddleware
   end
diff --git a/doc/administration/img/circuitbreaker_config.png b/doc/administration/img/circuitbreaker_config.png
index e811d1736346bcb8e8b6ab03a9a293b94355d07a..693b2ee9c6921537ee002470ef081728c427e35e 100644
Binary files a/doc/administration/img/circuitbreaker_config.png and b/doc/administration/img/circuitbreaker_config.png differ
diff --git a/doc/administration/img/repository_storages_admin_ui.png b/doc/administration/img/repository_storages_admin_ui.png
index 3e76c5b282cf4426b20eea4c298da5a237862de2..036e708cdac66737bb92b584294da3eb7ffc63d4 100644
Binary files a/doc/administration/img/repository_storages_admin_ui.png and b/doc/administration/img/repository_storages_admin_ui.png differ
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 00a2f3d01b84281140ab45e0bb94d0d8a090f0db..c8a3ef80e8f84ce81ea58ac07d8ce1945f3a4763 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -146,6 +146,28 @@ this file. For example:
 2014-06-10T18:18:26Z 14299 TID-55uqo INFO: Booting Sidekiq 3.0.0 with redis options {:url=>"redis://localhost:6379/0", :namespace=>"sidekiq"}
 ```
 
+Instead of the format above, you can opt to generate JSON logs for
+Sidekiq. For example:
+
+```json
+{"severity":"INFO","time":"2018-04-03T22:57:22.071Z","queue":"cronjob:update_all_mirrors","args":[],"class":"UpdateAllMirrorsWorker","retry":false,"queue_namespace":"cronjob","jid":"06aeaa3b0aadacf9981f368e","created_at":"2018-04-03T22:57:21.930Z","enqueued_at":"2018-04-03T22:57:21.931Z","pid":10077,"message":"UpdateAllMirrorsWorker JID-06aeaa3b0aadacf9981f368e: done: 0.139 sec","job_status":"done","duration":0.139,"completed_at":"2018-04-03T22:57:22.071Z"}
+```
+
+For Omnibus GitLab installations, add the configuration option:
+
+```ruby
+sidekiq['log_format'] = 'json'
+```
+
+For source installations, edit the `gitlab.yml` and set the Sidekiq
+`log_format` configuration option:
+
+```yaml
+  ## Sidekiq
+  sidekiq:
+    log_format: json
+```
+
 ## `gitlab-shell.log`
 
 This file lives in `/var/log/gitlab/gitlab-shell/gitlab-shell.log` for
@@ -206,4 +228,12 @@ is populated whenever `gitlab-ctl reconfigure` is run manually or as part of an
 Reconfigure logs files are named according to the UNIX timestamp of when the reconfigure
 was initiated, such as `1509705644.log`
 
+## `sidekiq_exporter.log`
+
+If Prometheus metrics and the Sidekiq Exporter are both enabled, Sidekiq will
+start a Web server and listen to the defined port (default: 3807). Access logs
+will be generated in `/var/log/gitlab/gitlab-rails/sidekiq_exporter.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/sidekiq_exporter.log` for
+installations from source.
+
 [repocheck]: repository_checks.md
diff --git a/doc/api/features.md b/doc/api/features.md
index 6861dbf00a2e0723215a82547bc09eb4d6244645..6ee1c36ef5b8209ffa269e945a48f3f3315a93dc 100644
--- a/doc/api/features.md
+++ b/doc/api/features.md
@@ -86,3 +86,11 @@ Example response:
   ]
 }
 ```
+
+## Delete a feature
+
+Removes a feature gate. Response is equal when the gate exists, or doesn't.
+
+```
+DELETE /features/:name
+```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index f388fae42a9478e42afdeaa54cf9087df1286012..a0cb5aa0820b3e685537e2ead2a3302b54113058 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -16,6 +16,21 @@ Values for the project visibility level are:
 * `public`:
   The project can be cloned without any authentication.
 
+## Project merge method
+
+There are currently three options for `merge_method` to choose from:
+
+* `merge`:
+  A merge commit is created for every merge, and merging is allowed as long as there are no conflicts.
+
+* `rebase_merge`:
+  A merge commit is created for every merge, but merging is only allowed if fast-forward merge is possible.
+  This way you could make sure that if this merge request would build, after merging to target branch it would also build.
+
+* `ff`:
+  No merge commits are created and all merges are fast-forwarded, which means that merging is only allowed if the branch could be fast-forwarded.
+
+
 ## List all projects
 
 Get a list of all visible projects across GitLab for the authenticated user.
@@ -94,6 +109,7 @@ GET /projects
     "only_allow_merge_if_pipeline_succeeds": false,
     "only_allow_merge_if_all_discussions_are_resolved": false,
     "request_access_enabled": false,
+    "merge_method": "merge",
     "statistics": {
       "commit_count": 37,
       "storage_size": 1038090,
@@ -173,6 +189,7 @@ GET /projects
     "only_allow_merge_if_pipeline_succeeds": false,
     "only_allow_merge_if_all_discussions_are_resolved": false,
     "request_access_enabled": false,
+    "merge_method": "merge",
     "statistics": {
       "commit_count": 12,
       "storage_size": 2066080,
@@ -278,6 +295,7 @@ GET /users/:user_id/projects
     "only_allow_merge_if_pipeline_succeeds": false,
     "only_allow_merge_if_all_discussions_are_resolved": false,
     "request_access_enabled": false,
+    "merge_method": "merge",
     "statistics": {
       "commit_count": 37,
       "storage_size": 1038090,
@@ -357,6 +375,7 @@ GET /users/:user_id/projects
     "only_allow_merge_if_pipeline_succeeds": false,
     "only_allow_merge_if_all_discussions_are_resolved": false,
     "request_access_enabled": false,
+    "merge_method": "merge",
     "statistics": {
       "commit_count": 12,
       "storage_size": 2066080,
@@ -467,6 +486,7 @@ GET /projects/:id
   "only_allow_merge_if_all_discussions_are_resolved": false,
   "printing_merge_requests_link_enabled": true,
   "request_access_enabled": false,
+  "merge_method": "merge",
   "statistics": {
     "commit_count": 37,
     "storage_size": 1038090,
@@ -550,6 +570,7 @@ POST /projects
 | `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members |
 | `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
 | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
+| `merge_method` | string | no | Set the merge method used |
 | `lfs_enabled` | boolean | no | Enable LFS |
 | `request_access_enabled` | boolean | no | Allow users to request member access |
 | `tag_list`    | array   | no       | The list of tags for a project; put array of tags, that should be finally assigned to a project |
@@ -586,6 +607,7 @@ POST /projects/user/:user_id
 | `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members |
 | `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
 | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
+| `merge_method` | string | no | Set the merge method used |
 | `lfs_enabled` | boolean | no | Enable LFS |
 | `request_access_enabled` | boolean | no | Allow users to request member access |
 | `tag_list`    | array   | no       | The list of tags for a project; put array of tags, that should be finally assigned to a project |
@@ -621,6 +643,7 @@ PUT /projects/:id
 | `public_jobs` | boolean | no | If `true`, jobs can be viewed by non-project-members |
 | `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
 | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
+| `merge_method` | string | no | Set the merge method used |
 | `lfs_enabled` | boolean | no | Enable LFS |
 | `request_access_enabled` | boolean | no | Allow users to request member access |
 | `tag_list`    | array   | no       | The list of tags for a project; put array of tags, that should be finally assigned to a project |
@@ -724,6 +747,7 @@ Example responses:
     "only_allow_merge_if_pipeline_succeeds": false,
     "only_allow_merge_if_all_discussions_are_resolved": false,
     "request_access_enabled": false,
+    "merge_method": "merge",
     "_links": {
       "self": "http://example.com/api/v4/projects",
       "issues": "http://example.com/api/v4/projects/1/issues",
@@ -801,6 +825,7 @@ Example response:
   "only_allow_merge_if_pipeline_succeeds": false,
   "only_allow_merge_if_all_discussions_are_resolved": false,
   "request_access_enabled": false,
+  "merge_method": "merge",
   "_links": {
     "self": "http://example.com/api/v4/projects",
     "issues": "http://example.com/api/v4/projects/1/issues",
@@ -877,6 +902,7 @@ Example response:
   "only_allow_merge_if_pipeline_succeeds": false,
   "only_allow_merge_if_all_discussions_are_resolved": false,
   "request_access_enabled": false,
+  "merge_method": "merge",
   "_links": {
     "self": "http://example.com/api/v4/projects",
     "issues": "http://example.com/api/v4/projects/1/issues",
@@ -971,6 +997,7 @@ Example response:
   "only_allow_merge_if_pipeline_succeeds": false,
   "only_allow_merge_if_all_discussions_are_resolved": false,
   "request_access_enabled": false,
+  "merge_method": "merge",
   "_links": {
     "self": "http://example.com/api/v4/projects",
     "issues": "http://example.com/api/v4/projects/1/issues",
@@ -1065,6 +1092,7 @@ Example response:
   "only_allow_merge_if_pipeline_succeeds": false,
   "only_allow_merge_if_all_discussions_are_resolved": false,
   "request_access_enabled": false,
+  "merge_method": "merge",
   "_links": {
     "self": "http://example.com/api/v4/projects",
     "issues": "http://example.com/api/v4/projects/1/issues",
diff --git a/doc/ci/examples/code_climate.md b/doc/ci/examples/code_climate.md
index 64a759a9a99741a4784aa7ee03206c14c898295a..92317c7742748094db28cabed4c5dadb2aee5a52 100644
--- a/doc/ci/examples/code_climate.md
+++ b/doc/ci/examples/code_climate.md
@@ -9,11 +9,12 @@ Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `codequali
 
 ```yaml
 codequality:
-  image: docker:latest
+  image: docker:stable
   variables:
-    DOCKER_DRIVER: overlay
+    DOCKER_DRIVER: overlay2
+  allow_failure: true
   services:
-    - docker:dind
+    - docker:stable-dind
   script:
     - 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
diff --git a/doc/ci/examples/container_scanning.md b/doc/ci/examples/container_scanning.md
index 3437b63748ad1862563d6472978b35a62dcadf99..c58efc7392a75f0993485e2e47c70158b4499957 100644
--- a/doc/ci/examples/container_scanning.md
+++ b/doc/ci/examples/container_scanning.md
@@ -11,7 +11,7 @@ called `sast:container`:
 
 ```yaml
 sast:container:
-  image: docker:latest
+  image: docker:stable
   variables:
     DOCKER_DRIVER: overlay2
     ## Define two new variables based on GitLab's CI/CD predefined variables
@@ -20,7 +20,7 @@ sast:container:
     CI_APPLICATION_TAG: $CI_COMMIT_SHA
   allow_failure: true
   services:
-    - docker:dind
+    - docker:stable-dind
   script:
     - docker run -d --name db arminc/clair-db:latest
     - docker run -p 6060:6060 --link db:postgres -d --name clair arminc/clair-local-scan:v2.0.1
diff --git a/doc/ci/examples/dast.md b/doc/ci/examples/dast.md
index 96de0f5ff5c73ded2fe689c5f417e42aa9dd58dd..8df223ee5604005908d6f91604aafcf9213a1880 100644
--- a/doc/ci/examples/dast.md
+++ b/doc/ci/examples/dast.md
@@ -14,9 +14,10 @@ called `dast`:
 
 ```yaml
 dast:
-  image: owasp/zap2docker-stable
+  image: registry.gitlab.com/gitlab-org/security-products/zaproxy
   variables:
     website: "https://example.com"
+  allow_failure: true
   script:
     - mkdir /zap/wrk/
     - /zap/zap-baseline.py -J gl-dast-report.json -t $website || true
@@ -30,6 +31,28 @@ the tests on the URL defined in the `website` variable (change it to use your
 own) and finally write the results in the `gl-dast-report.json` file. You can
 then download and analyze the report artifact in JSON format.
 
+It's also possible to authenticate the user before performing DAST checks:
+
+```yaml
+dast:
+  image: registry.gitlab.com/gitlab-org/security-products/zaproxy
+  variables:
+    website: "https://example.com"
+    login_url: "https://example.com/sign-in"
+  allow_failure: true
+  script:
+    - mkdir /zap/wrk/
+    - /zap/zap-baseline.py -J gl-dast-report.json -t $website \
+        --auth-url $login_url \
+        --auth-username "john.doe@example.com" \
+        --auth-password "john-doe-password" || true
+    - cp /zap/wrk/gl-dast-report.json .
+  artifacts:
+    paths: [gl-dast-report.json]
+```
+See [zaproxy documentation](https://gitlab.com/gitlab-org/security-products/zaproxy)
+to learn more about authentication settings.
+
 TIP: **Tip:**
 Starting with [GitLab Ultimate][ee] 10.4, this information will
 be automatically extracted and shown right in the merge request widget. To do
diff --git a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
index b62874ef0295b544a5746e140151b3b04c193731..1f9b9d53fc10434ea219f9a070ae8d4ba798740d 100644
--- a/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
+++ b/doc/ci/examples/laravel_with_gitlab_and_envoy/index.md
@@ -190,7 +190,7 @@ To start, we create an `Envoy.blade.php` in the root of our app with a simple ta
 ```php
 @servers(['web' => 'remote_username@remote_host'])
 
-@task('list', [on => 'web'])
+@task('list', ['on' => 'web'])
     ls -l
 @endtask
 ```
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 3ba03d2d5912209e9f3ea76763e8763b374f2aad..287143d625558e74cc3e6089fbc4bfdf4b154f55 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -644,6 +644,7 @@ information on managing page-specific javascript within EE.
 To separate EE-specific styles in SCSS files, if a component you're adding styles for
 is limited to only EE, it is better to have a separate SCSS file in appropriate directory
 within `app/assets/stylesheets`.
+See [backporting changes](#backporting-changes) for instructions on how to merge changes safely.
 
 In some cases, this is not entirely possible or creating dedicated SCSS file is an overkill,
 e.g. a text style of some component is different for EE. In such cases,
@@ -683,6 +684,19 @@ to avoid conflicts during CE to EE merge.
 // EE-specific end
 ```
 
+### Backporting changes from EE to CE
+
+When working in EE-specific features, you might have to tweak a few files that are not EE-specific. Here is a workflow to make sure those changes end up backported safely into CE too.
+(This approach does not refer to changes introduced via [csslab](https://gitlab.com/gitlab-org/csslab/).)
+
+1. **Make your changes in the EE branch.** If possible, keep a separated commit (to be squashed) to help backporting and review.
+1. **Open merge request to EE project.**
+1. **Apply the changes you made to CE files in a branch of the CE project.** (Tip: Use `patch` with the diff from your commit in EE branch)
+1. **Open merge request to CE project**, referring it's a backport of EE changes and link to MR open in EE.
+1. Once EE MR is merged, the MR towards CE can be merged. **But not before**.
+
+**Note:** regarding SCSS, make sure the files living outside `/ee/` don't diverge between CE and EE projects.
+
 ## gitlab-svgs
 
 Conflicts in `app/assets/images/icons.json` or `app/assets/images/icons.svg` can
diff --git a/doc/user/project/labels.md b/doc/user/project/labels.md
index dabffaec5fa9ce22c19607527de05c7a4ec84f9f..a89a1206170dbb4ff8ffb809dde366dfb48aee40 100644
--- a/doc/user/project/labels.md
+++ b/doc/user/project/labels.md
@@ -9,7 +9,7 @@ Labels allow you to categorize issues or merge requests using descriptive titles
 In GitLab, you can create project and group labels:
 
 - **Project labels** can be assigned to issues or merge requests in that project only. 
-- **Group labels** can be assigned to any issue or merge request of any project in that group. 
+- **Group labels** can be assigned to any issue or merge request of any project in that group or subgroup.
 - In the [future](https://gitlab.com/gitlab-org/gitlab-ce/issues/40915), you will be able to assign group labels to issues and merge reqeusts of projects in [subgroups](../group/subgroups/index.md).
 
 ## Creating labels
@@ -74,9 +74,9 @@ Every issue and merge request can be assigned any number of labels. The labels a
 
 ### Filtering in list pages
 
-From the project issue list page and the project merge request list page, you can [filter](../search/index.md#issues-and-merge-requests) by both group labels and project labels.
+From the project issue list page and the project merge request list page, you can [filter](../search/index.md#issues-and-merge-requests) by both group (including subgroup ancestors) labels and project labels.
 
-From the group issue list page and the group merge request list page, you can [filter](../search/index.md#issues-and-merge-requests) by both group labels and project labels.
+From the group issue list page and the group merge request list page, you can [filter](../search/index.md#issues-and-merge-requests) by both group labels (including subgroup ancestors and subgroup descendants) and project labels.
 
 ![Labels group issues](img/labels_group_issues.png)
 
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index dedf102fc3777ff0e97ca9b35a51151925a2e4fc..eb0ac221e30c12a563227494cdb763e4c3d7685d 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -57,11 +57,11 @@ The following items will be exported:
 - Project configuration including web hooks and services
 - Issues with comments, merge requests with diffs and comments, labels, milestones, snippets,
   and other project entities
+- LFS objects
 
 The following items will NOT be exported:
 
 - Build traces and artifacts
-- LFS objects
 - Container registry images
 - CI variables
 - Any encrypted tokens
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
deleted file mode 100644
index 819354bb7801e2377f54475bd2d9dcc663151ca8..0000000000000000000000000000000000000000
--- a/features/project/issues/issues.feature
+++ /dev/null
@@ -1,180 +0,0 @@
-@project_issues
-Feature: Project Issues
-  Background:
-    Given I sign in as a user
-    And I own project "Shop"
-    And project "Shop" have "Release 0.4" open issue
-    And project "Shop" have "Tweet control" open issue
-    And project "Shop" have "Release 0.3" closed issue
-    And I visit project "Shop" issues page
-
-  Scenario: I should see open issues
-    Given I should see "Release 0.4" in issues
-    And I should not see "Release 0.3" in issues
-
-  @javascript
-  Scenario: I should see closed issues
-    Given I click link "Closed"
-    Then I should see "Release 0.3" in issues
-    And I should not see "Release 0.4" in issues
-
-  @javascript
-  Scenario: I should see all issues
-    Given I click link "All"
-    Then I should see "Release 0.3" in issues
-    And I should see "Release 0.4" in issues
-
-  Scenario: I visit issue page
-    Given I click link "Release 0.4"
-    Then I should see issue "Release 0.4"
-
-  Scenario: I submit new unassigned issue
-    Given I click link "New Issue"
-    And I submit new issue "500 error on profile"
-    Then I should see issue "500 error on profile"
-
-  @javascript
-  Scenario: I submit new unassigned issue with labels
-    Given project "Shop" has labels: "bug", "feature", "enhancement"
-    And I click link "New Issue"
-    And I submit new issue "500 error on profile" with label 'bug'
-    Then I should see issue "500 error on profile"
-    And I should see label 'bug' with issue
-
-  @javascript
-  Scenario: I comment issue
-    Given I visit issue page "Release 0.4"
-    And I leave a comment like "XML attached"
-    Then I should see comment "XML attached"
-    And I should see an error alert section within the comment form
-
-  @javascript
-  Scenario: Visiting Issues after being sorted the list
-    Given I visit project "Shop" issues page
-    And I sort the list by "Last updated"
-    And I visit my project's home page
-    And I visit project "Shop" issues page
-    Then The list should be sorted by "Last updated"
-
-  @javascript
-  Scenario: Visiting Merge Requests after being sorted the list
-    Given project "Shop" has a "Bugfix MR" merge request open
-    And I visit project "Shop" issues page
-    And I sort the list by "Last updated"
-    And I visit project "Shop" merge requests page
-    Then The list should be sorted by "Last updated"
-
-  @javascript
-  Scenario: Visiting Merge Requests from a differente Project after sorting
-    Given project "Shop" has a "Bugfix MR" merge request open
-    And I visit project "Shop" merge requests page
-    And I sort the list by "Last updated"
-    And I visit dashboard merge requests page
-    Then The list should be sorted by "Last updated"
-
-  @javascript
-  Scenario: Sort issues by upvotes/downvotes
-    Given project "Shop" have "Bugfix" open issue
-    And issue "Release 0.4" have 2 upvotes and 1 downvote
-    And issue "Tweet control" have 1 upvote and 2 downvotes
-    And I sort the list by "Popularity"
-    Then The list should be sorted by "Popularity"
-
-  # Markdown
-
-  @javascript
-  Scenario: Headers inside the description should have ids generated for them.
-    Given I visit issue page "Release 0.4"
-    Then Header "Description header" should have correct id and link
-
-  @javascript
-  Scenario: Headers inside comments should not have ids generated for them.
-    Given I visit issue page "Release 0.4"
-    And I leave a comment with a header containing "Comment with a header"
-    Then The comment with the header should not have an ID
-
-  @javascript
-  Scenario: Blocks inside comments should not build relative links
-    Given I visit issue page "Release 0.4"
-    And I leave a comment with code block
-    Then The code block should be unchanged
-
-  Scenario: Issues on empty project
-    Given empty project "Empty Project"
-    And I have an ssh key
-    When I visit empty project page
-    And I see empty project details with ssh clone info
-    When I visit empty project's issues page
-    Given I click link "New Issue"
-    And I submit new issue "500 error on profile"
-    Then I should see issue "500 error on profile"
-
-  Scenario: Clickable labels
-    Given issue 'Release 0.4' has label 'bug'
-    And I visit project "Shop" issues page
-    When I click label 'bug'
-    And I should see "Release 0.4" in issues
-    And I should not see "Tweet control" in issues
-
-  @javascript
-  Scenario: Issue notes should be editable with +1
-    Given project "Shop" have "Release 0.4" open issue
-    When I visit issue page "Release 0.4"
-    And I leave a comment with a header containing "Comment with a header"
-    Then The comment with the header should not have an ID
-    And I edit the last comment with a +1
-    Then I should see +1 in the description
-
-  # Issue description preview
-
-  @javascript
-  Scenario: I can't preview without text
-    Given I click link "New Issue"
-    And I haven't written any description text
-    Then The Markdown preview tab should say there is nothing to do
-
-  @javascript
-  Scenario: I can preview with text
-    Given I click link "New Issue"
-    And I write a description like ":+1: Nice"
-    Then The Markdown preview tab should display rendered Markdown
-
-  @javascript
-  Scenario: I preview an issue description
-    Given I click link "New Issue"
-    And I preview a description text like "Bug fixed :smile:"
-    Then I should see the Markdown preview
-    And I should not see the Markdown text field
-
-  @javascript
-  Scenario: I can edit after preview
-    Given I click link "New Issue"
-    And I preview a description text like "Bug fixed :smile:"
-    Then I should see the Markdown write tab
-
-  @javascript
-  Scenario: I can preview when editing an existing issue
-    Given I click link "Release 0.4"
-    And I click link "Edit" for the issue
-    And I preview a description text like "Bug fixed :smile:"
-    Then I should see the Markdown write tab
-
-  @javascript
-  Scenario: I can unsubscribe from issue
-    Given project "Shop" have "Release 0.4" open issue
-    When I visit issue page "Release 0.4"
-    Then I should see that I am subscribed
-    When I click the subscription toggle
-    Then I should see that I am unsubscribed
-
-  @javascript
-  Scenario: I submit new unassigned issue as guest
-    Given public project "Community"
-    When I visit project "Community" page
-    And I visit project "Community" issues page
-    And I click link "New Issue"
-    And I should not see assignee field
-    And I should not see milestone field
-    And I should not see labels field
-    And I submit new issue "500 error on profile"
-    Then I should see issue "500 error on profile"
diff --git a/features/project/issues/labels.feature b/features/project/issues/labels.feature
deleted file mode 100644
index 45de57f18e328e8a7880744b0c5c70f9cb62424c..0000000000000000000000000000000000000000
--- a/features/project/issues/labels.feature
+++ /dev/null
@@ -1,48 +0,0 @@
-@project_issues
-Feature: Project Issues Labels
-  Background:
-    Given I sign in as a user
-    And I own project "Shop"
-    And project "Shop" has labels: "bug", "feature", "enhancement"
-    Given I visit project "Shop" labels page
-
-  Scenario: I should see labels list
-    Then I should see label 'bug'
-    And I should see label 'feature'
-
-  Scenario: I create new label
-    Given I visit project "Shop" new label page
-    When I submit new label 'support'
-    Then I should see label 'support'
-
-  Scenario: I edit label
-    Given I visit 'bug' label edit page
-    When I change label 'bug' to 'fix'
-    Then I should not see label 'bug'
-    Then I should see label 'fix'
-
-  Scenario: I remove label
-    When I remove label 'bug'
-    Then I should not see label 'bug'
-
-  @javascript
-  Scenario: I remove all labels
-    When I delete all labels
-    Then I should see labels help message
-
-  Scenario: I create a label with invalid color
-    Given I visit project "Shop" new label page
-    When I submit new label with invalid color
-    Then I should see label color error message
-
-  Scenario: I create a label that already exists
-    Given I visit project "Shop" new label page
-    When I submit new label 'bug'
-    Then I should see label label exist error message
-
-  Scenario: I create the same label on another project
-    Given I own project "Forum"
-    And I visit project "Forum" labels page
-    And I visit project "Forum" new label page
-    When I submit new label 'bug'
-    Then I should see label 'bug'
diff --git a/features/project/issues/milestones.feature b/features/project/issues/milestones.feature
index d121222308d1bbf79d5f63a21dccf7d68293ef02..77c8ed6e5bf8e7588f57933cef95b8eed0acbb23 100644
--- a/features/project/issues/milestones.feature
+++ b/features/project/issues/milestones.feature
@@ -39,4 +39,5 @@ Feature: Project Issues Milestones
 
   Scenario: Headers inside the description should have ids generated for them.
     Given I click link "v2.2"
+    # PLEASE USE the `have_header_with_correct_id_and_link(level, text, id, parent)` matcher on migrating this spec to rspec.
     Then Header "Description header" should have correct id and link
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 3cd26bb429bc4aa55602126c2a73faf31776856c..baa78c2320377d7a42a85b055da7c48f139213f6 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -7,36 +7,14 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
   include SharedMarkdown
   include SharedUser
 
-  step 'I should see "Release 0.4" in issues' do
-    expect(page).to have_content "Release 0.4"
-  end
-
   step 'I should not see "Release 0.3" in issues' do
     expect(page).not_to have_content "Release 0.3"
   end
 
-  step 'I should not see "Tweet control" in issues' do
-    expect(page).not_to have_content "Tweet control"
-  end
-
-  step 'I should see that I am subscribed' do
-    wait_for_requests
-    expect(find('.js-issuable-subscribe-button')).to have_css 'button.is-checked'
-  end
-
-  step 'I should see that I am unsubscribed' do
-    wait_for_requests
-    expect(find('.js-issuable-subscribe-button')).to have_css 'button:not(.is-checked)'
-  end
-
   step 'I click link "Closed"' do
     find('.issues-state-filters [data-state="closed"] span', text: 'Closed').click
   end
 
-  step 'I click the subscription toggle' do
-    find('.js-issuable-subscribe-button button').click
-  end
-
   step 'I should see "Release 0.3" in issues' do
     expect(page).to have_content "Release 0.3"
   end
@@ -51,24 +29,10 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
     expect(find('.issues-state-filters > .active')).to have_content 'All'
   end
 
-  step 'I click link "Release 0.4"' do
-    click_link "Release 0.4"
-  end
-
-  step 'I should see issue "Release 0.4"' do
-    expect(page).to have_content "Release 0.4"
-  end
-
   step 'I should see issue "Tweet control"' do
     expect(page).to have_content "Tweet control"
   end
 
-  step 'I click link "New issue"' do
-    page.within '#content-body' do
-      page.has_link?('New Issue') ? click_link('New Issue') : click_link('New issue')
-    end
-  end
-
   step 'I click "author" dropdown' do
     page.find('.js-author-search').click
     sleep 1
@@ -81,18 +45,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
     expect(users[1].text).to eq "#{current_user.name} #{current_user.to_reference}"
   end
 
-  step 'I submit new issue "500 error on profile"' do
-    fill_in "issue_title", with: "500 error on profile"
-    click_button "Submit issue"
-  end
-
-  step 'I submit new issue "500 error on profile" with label \'bug\'' do
-    fill_in "issue_title", with: "500 error on profile"
-    click_button "Label"
-    click_link "bug"
-    click_button "Submit issue"
-  end
-
   step 'I click link "500 error on profile"' do
     click_link "500 error on profile"
   end
@@ -103,13 +55,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
     end
   end
 
-  step 'I should see issue "500 error on profile"' do
-    issue = Issue.find_by(title: "500 error on profile")
-    expect(page).to have_content issue.title
-    expect(page).to have_content issue.author_name
-    expect(page).to have_content issue.project.name
-  end
-
   step 'I fill in issue search with "Re"' do
     filter_issue "Re"
   end
@@ -163,49 +108,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
     expect(find(issues_assignee_selector)).to have_content(assignee_name)
   end
 
-  step 'project "Shop" have "Release 0.4" open issue' do
-    create(:issue,
-           title: "Release 0.4",
-           project: project,
-           author: project.users.first,
-           description: "# Description header"
-          )
-    wait_for_requests
-  end
-
-  step 'project "Shop" have "Tweet control" open issue' do
-    create(:issue,
-           title: "Tweet control",
-           project: project,
-           author: project.users.first)
-  end
-
-  step 'project "Shop" have "Bugfix" open issue' do
-    create(:issue,
-           title: "Bugfix",
-           project: project,
-           author: project.users.first)
-  end
-
-  step 'project "Shop" have "Release 0.3" closed issue' do
-    create(:closed_issue,
-           title: "Release 0.3",
-           project: project,
-           author: project.users.first)
-  end
-
-  step 'issue "Release 0.4" have 2 upvotes and 1 downvote' do
-    awardable = Issue.find_by(title: 'Release 0.4')
-    create_list(:award_emoji, 2, awardable: awardable)
-    create(:award_emoji, :downvote, awardable: awardable)
-  end
-
-  step 'issue "Tweet control" have 1 upvote and 2 downvotes' do
-    awardable = Issue.find_by(title: 'Tweet control')
-    create(:award_emoji, :upvote, awardable: awardable)
-    create_list(:award_emoji, 2, awardable: awardable, name: 'thumbsdown')
-  end
-
   step 'The list should be sorted by "Least popular"' do
     page.within '.issues-list' do
       page.within 'li.issue:nth-child(1)' do
@@ -225,69 +127,16 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
     end
   end
 
-  step 'The list should be sorted by "Popularity"' do
-    page.within '.issues-list' do
-      page.within 'li.issue:nth-child(1)' do
-        expect(page).to have_content 'Release 0.4'
-        expect(page).to have_content '2 1'
-      end
-
-      page.within 'li.issue:nth-child(2)' do
-        expect(page).to have_content 'Tweet control'
-        expect(page).to have_content '1 2'
-      end
-
-      page.within 'li.issue:nth-child(3)' do
-        expect(page).to have_content 'Bugfix'
-        expect(page).not_to have_content '0 0'
-      end
-    end
-  end
-
-  step 'empty project "Empty Project"' do
-    create :project_empty_repo, name: 'Empty Project', namespace: @user.namespace
-  end
-
   When 'I visit empty project page' do
     project = Project.find_by(name: 'Empty Project')
     visit project_path(project)
   end
 
-  step 'I see empty project details with ssh clone info' do
-    project = Project.find_by(name: 'Empty Project')
-    page.all(:css, '.git-empty .clone').each do |element|
-      expect(element.text).to include(project.url_to_repo)
-    end
-  end
-
   When "I visit project \"Community\" issues page" do
     project = Project.find_by(name: 'Community')
     visit project_issues_path(project)
   end
 
-  When "I visit empty project's issues page" do
-    project = Project.find_by(name: 'Empty Project')
-    visit project_issues_path(project)
-  end
-
-  step 'I leave a comment with code block' do
-    page.within(".js-main-target-form") do
-      fill_in "note[note]", with: "```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```"
-      click_button "Comment"
-      sleep 0.05
-    end
-  end
-
-  step 'I should see an error alert section within the comment form' do
-    page.within(".js-main-target-form") do
-      find(".error-alert")
-    end
-  end
-
-  step 'The code block should be unchanged' do
-    expect(page).to have_content("```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```")
-  end
-
   step 'project \'Shop\' has issue \'Bugfix1\' with description: \'Description for issue1\'' do
     create(:issue, title: 'Bugfix1', description: 'Description for issue1', project: project)
   end
@@ -320,36 +169,6 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
     expect(page).not_to have_content 'Bugfix1'
   end
 
-  step 'issue \'Release 0.4\' has label \'bug\'' do
-    label = project.labels.create!(name: 'bug', color: '#990000')
-    issue = Issue.find_by!(title: 'Release 0.4')
-    issue.labels << label
-  end
-
-  step 'I click label \'bug\'' do
-    page.within ".issues-list" do
-      click_link 'bug'
-    end
-  end
-
-  step 'I should not see labels field' do
-    page.within '.issue-form' do
-      expect(page).not_to have_content("Labels")
-    end
-  end
-
-  step 'I should not see milestone field' do
-    page.within '.issue-form' do
-      expect(page).not_to have_content("Milestone")
-    end
-  end
-
-  step 'I should not see assignee field' do
-    page.within '.issue-form' do
-      expect(page).not_to have_content("Assign to")
-    end
-  end
-
   def filter_issue(text)
     fill_in 'issuable_search', with: text
   end
diff --git a/features/steps/project/issues/labels.rb b/features/steps/project/issues/labels.rb
deleted file mode 100644
index 4df96e081f98aacadb9f46f7f44b75d14e406347..0000000000000000000000000000000000000000
--- a/features/steps/project/issues/labels.rb
+++ /dev/null
@@ -1,101 +0,0 @@
-class Spinach::Features::ProjectIssuesLabels < Spinach::FeatureSteps
-  include SharedAuthentication
-  include SharedProject
-  include SharedPaths
-
-  step 'I visit \'bug\' label edit page' do
-    visit edit_project_label_path(project, bug_label)
-  end
-
-  step 'I remove label \'bug\'' do
-    page.within "#project_label_#{bug_label.id}" do
-      first(:link, 'Delete').click
-    end
-  end
-
-  step 'I delete all labels' do
-    page.within '.labels' do
-      page.all('.label-list-item').each do
-        first('.remove-row').click
-        first(:link, 'Delete label').click
-      end
-    end
-  end
-
-  step 'I should see labels help message' do
-    page.within '.labels' do
-      expect(page).to have_content 'Generate a default set of labels'
-      expect(page).to have_content 'New label'
-    end
-  end
-
-  step 'I submit new label \'support\'' do
-    fill_in 'Title', with: 'support'
-    fill_in 'Background color', with: '#F95610'
-    click_button 'Create label'
-  end
-
-  step 'I submit new label \'bug\'' do
-    fill_in 'Title', with: 'bug'
-    fill_in 'Background color', with: '#F95610'
-    click_button 'Create label'
-  end
-
-  step 'I submit new label with invalid color' do
-    fill_in 'Title', with: 'support'
-    fill_in 'Background color', with: '#12'
-    click_button 'Create label'
-  end
-
-  step 'I should see label label exist error message' do
-    page.within '.label-form' do
-      expect(page).to have_content 'Title has already been taken'
-    end
-  end
-
-  step 'I should see label color error message' do
-    page.within '.label-form' do
-      expect(page).to have_content 'Color must be a valid color code'
-    end
-  end
-
-  step 'I should see label \'feature\'' do
-    page.within '.other-labels .manage-labels-list' do
-      expect(page).to have_content 'feature'
-    end
-  end
-
-  step 'I should see label \'bug\'' do
-    page.within '.other-labels .manage-labels-list' do
-      expect(page).to have_content 'bug'
-    end
-  end
-
-  step 'I should not see label \'bug\'' do
-    page.within '.other-labels .manage-labels-list' do
-      expect(page).not_to have_content 'bug'
-    end
-  end
-
-  step 'I should see label \'support\'' do
-    page.within '.other-labels .manage-labels-list' do
-      expect(page).to have_content 'support'
-    end
-  end
-
-  step 'I change label \'bug\' to \'fix\'' do
-    fill_in 'Title', with: 'fix'
-    fill_in 'Background color', with: '#F15610'
-    click_button 'Save changes'
-  end
-
-  step 'I should see label \'fix\'' do
-    page.within '.other-labels .manage-labels-list' do
-      expect(page).to have_content 'fix'
-    end
-  end
-
-  def bug_label
-    project.labels.find_or_create_by(title: 'bug')
-  end
-end
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index f90247c3fe841d0da208f9dd614f8a2d5368007a..a9174efd334e2fbbc059fa08ee9964bf37e76c6b 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -105,17 +105,6 @@ module SharedIssuable
     edit_issuable
   end
 
-  step 'I click link "Edit" for the issue' do
-    edit_issuable
-  end
-
-  step 'I sort the list by "Last updated"' do
-    find('button.dropdown-toggle').click
-    page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
-      click_link "Last updated"
-    end
-  end
-
   step 'I sort the list by "Least popular"' do
     find('button.dropdown-toggle').click
 
@@ -124,18 +113,6 @@ module SharedIssuable
     end
   end
 
-  step 'I sort the list by "Popularity"' do
-    find('button.dropdown-toggle').click
-
-    page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
-      click_link 'Popularity'
-    end
-  end
-
-  step 'The list should be sorted by "Last updated"' do
-    expect(find('.issues-filters')).to have_content('Last updated')
-  end
-
   step 'I click link "Next" in the sidebar' do
     page.within '.issuable-sidebar' do
       click_link 'Next'
diff --git a/features/steps/shared/markdown.rb b/features/steps/shared/markdown.rb
index c2bec2a6320ca7db2247e1f5486d661e3b6edda3..c66280127e9e051ce82ff02d05233b2df36e5f9c 100644
--- a/features/steps/shared/markdown.rb
+++ b/features/steps/shared/markdown.rb
@@ -18,43 +18,6 @@ module SharedMarkdown
     expect(find('.gfm-form .js-md-preview')).not_to be_visible
   end
 
-  step 'The Markdown preview tab should say there is nothing to do' do
-    page.within('.gfm-form') do
-      find('.js-md-preview-button').click
-      expect(find('.js-md-preview')).to have_content('Nothing to preview.')
-    end
-  end
-
-  step 'I should not see the Markdown text field' do
-    expect(find('.gfm-form textarea')).not_to be_visible
-  end
-
-  step 'I should see the Markdown write tab' do
-    expect(first('.gfm-form')).to have_link('Write', visible: true)
-  end
-
-  step 'I should see the Markdown preview' do
-    expect(find('.gfm-form')).to have_css('.js-md-preview', visible: true)
-  end
-
-  step 'The Markdown preview tab should display rendered Markdown' do
-    page.within('.gfm-form') do
-      find('.js-md-preview-button').click
-      expect(find('.js-md-preview')).to have_css('gl-emoji', visible: true)
-    end
-  end
-
-  step 'I write a description like ":+1: Nice"' do
-    find('.gfm-form').fill_in 'Description', with: ':+1: Nice'
-  end
-
-  step 'I preview a description text like "Bug fixed :smile:"' do
-    page.within(first('.gfm-form')) do
-      fill_in 'Description', with: 'Bug fixed :smile:'
-      click_link 'Preview'
-    end
-  end
-
   step 'I haven\'t written any description text' do
     find('.gfm-form').fill_in 'Description', with: ''
   end
diff --git a/features/steps/shared/note.rb b/features/steps/shared/note.rb
index 95f0cd2156eb73a9c19b0456a509b41a0a997058..cbe1cae096eab2b5e30a563924c6f1f54c9aa9e9 100644
--- a/features/steps/shared/note.rb
+++ b/features/steps/shared/note.rb
@@ -114,34 +114,12 @@ module SharedNote
     end
   end
 
-  step 'I should see comment "XML attached"' do
-    page.within(".note") do
-      expect(page).to have_content("XML attached")
-    end
-  end
-
   step 'I should see no notes at all' do
     expect(page).not_to have_css('.note')
   end
 
   # Markdown
 
-  step 'I leave a comment with a header containing "Comment with a header"' do
-    page.within(".js-main-target-form") do
-      fill_in "note[note]", with: "# Comment with a header"
-      click_button "Comment"
-    end
-
-    wait_for_requests
-  end
-
-  step 'The comment with the header should not have an ID' do
-    page.within(".note-body > .note-text") do
-      expect(page).to have_content("Comment with a header")
-      expect(page).not_to have_css("#comment-with-a-header")
-    end
-  end
-
   step 'I edit the last comment with a +1' do
     page.within(".main-notes-list") do
       note = find('.note')
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index bff0d58aaf4da0bdc52d0b2850f2e6a410baff52..cc893b8391e300f7446c385912dcc3987b6b36c8 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -96,10 +96,6 @@ module SharedPaths
     visit assigned_issues_dashboard_path
   end
 
-  step 'I visit dashboard merge requests page' do
-    visit assigned_mrs_dashboard_path
-  end
-
   step 'I visit dashboard search page' do
     visit search_path
   end
@@ -200,10 +196,6 @@ module SharedPaths
   # Generic Project
   # ----------------------------------------
 
-  step "I visit my project's home page" do
-    visit project_path(@project)
-  end
-
   step "I visit my project's settings page" do
     visit edit_project_path(@project)
   end
@@ -339,20 +331,11 @@ module SharedPaths
     visit project_commit_path(@project, sample_commit.id)
   end
 
-  step 'I visit project "Shop" issues page' do
-    visit project_issues_path(project)
-  end
-
   step 'I visit issue page "Release 0.4"' do
     issue = Issue.find_by(title: "Release 0.4")
     visit project_issue_path(issue.project, issue)
   end
 
-  step 'I visit project "Shop" labels page' do
-    project = Project.find_by(name: 'Shop')
-    visit project_labels_path(project)
-  end
-
   step 'I visit project "Forum" labels page' do
     project = Project.find_by(name: 'Forum')
     visit project_labels_path(project)
@@ -394,10 +377,6 @@ module SharedPaths
     wait_for_requests
   end
 
-  step 'I visit project "Shop" merge requests page' do
-    visit project_merge_requests_path(project)
-  end
-
   step 'I visit forked project "Shop" merge requests page' do
     visit project_merge_requests_path(project)
   end
@@ -418,11 +397,6 @@ module SharedPaths
   # Visibility Projects
   # ----------------------------------------
 
-  step 'I visit project "Community" page' do
-    project = Project.find_by(name: "Community")
-    visit project_path(project)
-  end
-
   step 'I visit project "Community" source page' do
     project = Project.find_by(name: 'Community')
     visit project_tree_path(project, root_ref)
@@ -442,11 +416,6 @@ module SharedPaths
   # Empty Projects
   # ----------------------------------------
 
-  step "I visit empty project page" do
-    project = Project.find_by(name: "Empty Public Project")
-    visit project_path(project)
-  end
-
   step "I should not see command line instructions" do
     expect(page).not_to have_css('.empty_wrapper')
   end
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 07a0e2e072ced934c4ffe455df1fe9d0a8cec23f..be848ebafa08bc576594ac366ce907cfcdad5cc0 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -236,10 +236,6 @@ module SharedProject
     @project.update(public_builds: false)
   end
 
-  step 'project "Shop" has a "Bugfix MR" merge request open' do
-    create(:merge_request, title: "Bugfix MR", target_project: project, source_project: project, author: project.users.first)
-  end
-
   def user_owns_project(user_name:, project_name:, visibility: :private)
     user = user_exists(user_name, username: user_name.gsub(/\s/, '').underscore)
     project = Project.find_by(name: project_name)
diff --git a/features/steps/shared/user.rb b/features/steps/shared/user.rb
index 9856c510aa028c48aab2c2b34c7d89bc8f207e60..9cadc91769d19877499c7a6690962b065c99ef35 100644
--- a/features/steps/shared/user.rb
+++ b/features/steps/shared/user.rb
@@ -19,10 +19,6 @@ module SharedUser
     User.find_by(name: name) || create(:user, { name: name, admin: false }.merge(options))
   end
 
-  step 'I have an ssh key' do
-    create(:personal_key, user: @user)
-  end
-
   step 'I have no ssh keys' do
     @user.keys.delete_all
   end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index b7a390696c78727681d6acc4f599515eb0e1f151..e5ecd37e473d01ba04e73ea241a1b849f209c753 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -206,6 +206,7 @@ module API
       expose :request_access_enabled
       expose :only_allow_merge_if_all_discussions_are_resolved
       expose :printing_merge_request_link_enabled
+      expose :merge_method
 
       expose :statistics, using: 'API::Entities::ProjectStatistics', if: :statistics
 
diff --git a/lib/api/features.rb b/lib/api/features.rb
index 9385c6ca174243cc9a9d8b649df6dc450b7b8fd0..11d848584d9eb443c351acfae617c800217fc930 100644
--- a/lib/api/features.rb
+++ b/lib/api/features.rb
@@ -65,6 +65,13 @@ module API
 
         present feature, with: Entities::Feature, current_user: current_user
       end
+
+      desc 'Remove the gate value for the given feature'
+      delete ':name' do
+        Feature.get(params[:name]).remove
+
+        status 204
+      end
     end
   end
 end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index e59e8a459089ae4a189c1de1aea9e1f7129a065b..61c138a7dec75e7ea33ec6f202efd691e9aead5c 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -83,12 +83,13 @@ module API
     end
 
     def available_labels_for(label_parent)
-      search_params =
-        if label_parent.is_a?(Project)
-          { project_id: label_parent.id }
-        else
-          { group_id: label_parent.id, only_group_labels: true }
-        end
+      search_params = { include_ancestor_groups: true }
+
+      if label_parent.is_a?(Project)
+        search_params[:project_id] = label_parent.id
+      else
+        search_params.merge!(group_id: label_parent.id, only_group_labels: true)
+      end
 
       LabelsFinder.new(current_user, search_params).execute
     end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 467bc78dad8a2d05580ba2174aca4dd299cf361b..3d5b3c5a5354593395a892fa2ed4443573f59f94 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -28,6 +28,7 @@ module API
         optional :tag_list, type: Array[String], desc: 'The list of tags for a project'
         optional :avatar, type: File, desc: 'Avatar image for project'
         optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line'
+        optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests'
       end
 
       params :optional_params do
@@ -274,6 +275,7 @@ module API
             :issues_enabled,
             :lfs_enabled,
             :merge_requests_enabled,
+            :merge_method,
             :name,
             :only_allow_merge_if_all_discussions_are_resolved,
             :only_allow_merge_if_pipeline_succeeds,
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 57c0a7295351b5fd915953b3933e8f0a549e7083..834253d8e94c8fa8a7aee7526695d8f2db8de2d6 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -208,6 +208,7 @@ module API
         optional 'file.sha256', type: String, desc: %q(sha256 checksum of the file)
         optional 'metadata.path', type: String, desc: %q(path to locally stored body (generated by Workhorse))
         optional 'metadata.name', type: String, desc: %q(filename (generated by Workhorse))
+        optional 'metadata.sha256', type: String, desc: %q(sha256 checksum of the file)
       end
       post '/:id/artifacts' do
         not_allowed! unless Gitlab.config.artifacts.enabled
@@ -227,7 +228,7 @@ module API
           Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in
 
         job.build_job_artifacts_archive(project: job.project, file_type: :archive, file: artifacts, file_sha256: params['file.sha256'], expire_in: expire_in)
-        job.build_job_artifacts_metadata(project: job.project, file_type: :metadata, file: metadata, expire_in: expire_in) if metadata
+        job.build_job_artifacts_metadata(project: job.project, file_type: :metadata, file: metadata, file_sha256: params['metadata.sha256'], expire_in: expire_in) if metadata
         job.artifacts_expire_in = expire_in
 
         if job.save
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index c9e3f8ce42bf81b76fd35bcaa7d550e53d367d0e..c3a03f133064f472790905f6b5a722fb8197bdde 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -171,7 +171,7 @@ module Banzai
           end
 
           if object
-            title = object_link_title(object)
+            title = object_link_title(object, matches)
             klass = reference_class(object_sym)
 
             data = data_attributes_for(link_content || match, parent, object,
@@ -216,7 +216,7 @@ module Banzai
         extras
       end
 
-      def object_link_title(object)
+      def object_link_title(object, matches)
         object.title
       end
 
diff --git a/lib/banzai/filter/commit_range_reference_filter.rb b/lib/banzai/filter/commit_range_reference_filter.rb
index 21bcb1c5ca8555d8e241c3c9b7653b452fac4dea..99fa2d9d8fbe0a7146ad575d67acc63e4d471f92 100644
--- a/lib/banzai/filter/commit_range_reference_filter.rb
+++ b/lib/banzai/filter/commit_range_reference_filter.rb
@@ -34,7 +34,7 @@ module Banzai
                                         range.to_param.merge(only_path: context[:only_path]))
       end
 
-      def object_link_title(range)
+      def object_link_title(range, matches)
         nil
       end
     end
diff --git a/lib/banzai/filter/commit_trailers_filter.rb b/lib/banzai/filter/commit_trailers_filter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ef16df1f3ae7f07b33edd5bb4cd2b173a4730f82
--- /dev/null
+++ b/lib/banzai/filter/commit_trailers_filter.rb
@@ -0,0 +1,152 @@
+module Banzai
+  module Filter
+    # HTML filter that replaces users' names and emails in commit trailers
+    # with links to their GitLab accounts or mailto links to their mentioned
+    # emails.
+    #
+    # Commit trailers are special labels in the form of `*-by:` and fall on a
+    # single line, ex:
+    #
+    #   Reported-By: John S. Doe <john.doe@foo.bar>
+    #
+    # More info about this can be found here:
+    # * https://git.wiki.kernel.org/index.php/CommitMessageConventions
+    class CommitTrailersFilter < HTML::Pipeline::Filter
+      include ActionView::Helpers::TagHelper
+      include ApplicationHelper
+      include AvatarsHelper
+
+      TRAILER_REGEXP = /(?<label>[[:alpha:]-]+-by:)/i.freeze
+      AUTHOR_REGEXP = /(?<author_name>.+)/.freeze
+      # Devise.email_regexp wouldn't work here since its designed to match
+      # against strings that only contains email addresses; the \A and \z
+      # around the expression will only match if the string being matched
+      # contains just the email nothing else.
+      MAIL_REGEXP = /&lt;(?<author_email>[^@\s]+@[^@\s]+)&gt;/.freeze
+      FILTER_REGEXP = /(?<trailer>^\s*#{TRAILER_REGEXP}\s*#{AUTHOR_REGEXP}\s+#{MAIL_REGEXP}$)/mi.freeze
+
+      def call
+        doc.xpath('descendant-or-self::text()').each do |node|
+          content = node.to_html
+
+          next unless content.match(FILTER_REGEXP)
+
+          html = trailer_filter(content)
+
+          next if html == content
+
+          node.replace(html)
+        end
+
+        doc
+      end
+
+      private
+
+      # Replace trailer lines with links to GitLab users or mailto links to
+      # non GitLab users.
+      #
+      # text - String text to replace trailers in.
+      #
+      # Returns a String with all trailer lines replaced with links to GitLab
+      # users and mailto links to non GitLab users. All links have `data-trailer`
+      # and `data-user` attributes attached.
+      def trailer_filter(text)
+        text.gsub(FILTER_REGEXP) do |author_match|
+          label = $~[:label]
+          "#{label} #{parse_user($~[:author_name], $~[:author_email], label)}"
+        end
+      end
+
+      # Find a GitLab user using the supplied email and generate
+      # a valid link to them, otherwise, generate a mailto link.
+      #
+      # name - String name used in the commit message for the user
+      # email - String email used in the commit message for the user
+      # trailer - String trailer used in the commit message
+      #
+      # Returns a String with a link to the user.
+      def parse_user(name, email, trailer)
+        link_to_user User.find_by_any_email(email),
+          name: name,
+          email: email,
+          trailer: trailer
+      end
+
+      def urls
+        Gitlab::Routing.url_helpers
+      end
+
+      def link_to_user(user, name:, email:, trailer:)
+        wrapper = link_wrapper(data: {
+          trailer: trailer,
+          user: user.try(:id)
+        })
+
+        avatar = user_avatar_without_link(
+          user: user,
+          user_email: email,
+          css_class: 'avatar-inline',
+          has_tooltip: false
+        )
+
+        link_href = user.nil? ? "mailto:#{email}" : urls.user_url(user)
+
+        avatar_link = link_tag(
+          link_href,
+          content: avatar,
+          title: email
+        )
+
+        name_link = link_tag(
+          link_href,
+          content: name,
+          title: email
+        )
+
+        email_link = link_tag(
+          "mailto:#{email}",
+          content: email,
+          title: email
+        )
+
+        wrapper << "#{avatar_link}#{name_link} <#{email_link}>"
+      end
+
+      def link_wrapper(data: {})
+        data_attributes = data_attributes_from_hash(data)
+
+        doc.document.create_element(
+          'span',
+          data_attributes
+        )
+      end
+
+      def link_tag(url, title: "", content: "", data: {})
+        data_attributes = data_attributes_from_hash(data)
+
+        attributes = data_attributes.merge(
+          href: url,
+          title: title
+        )
+
+        link = doc.document.create_element('a', attributes)
+
+        if content.html_safe?
+          link << content
+        else
+          link.content = content # make sure we escape content using nokogiri's #content=
+        end
+
+        link
+      end
+
+      def data_attributes_from_hash(data = {})
+        data.reject! {|_, value| value.nil?}
+        data.map do |key, value|
+          [%(data-#{key.to_s.dasherize}), value]
+        end.to_h
+      end
+    end
+  end
+end
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index d5360ad8f6890dee3f849191c8e419f0a3a4ceec..faa5b344e6f53a2e4154c883d8a94631312e88bd 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -41,7 +41,7 @@ module Banzai
       end
 
       def find_labels(project)
-        LabelsFinder.new(nil, project_id: project.id).execute(skip_authorization: true)
+        LabelsFinder.new(nil, project_id: project.id, include_ancestor_groups: true).execute(skip_authorization: true)
       end
 
       # Parameters to pass to `Label.find_by` based on the given arguments
@@ -77,7 +77,7 @@ module Banzai
         CGI.unescapeHTML(text.to_s)
       end
 
-      def object_link_title(object)
+      def object_link_title(object, matches)
         # use title of wrapped element instead
         nil
       end
diff --git a/lib/banzai/filter/merge_request_reference_filter.rb b/lib/banzai/filter/merge_request_reference_filter.rb
index b3cfa97d0e048b211fd77adf5c76588997c045a7..5cbdb01c130d3a413cbac4b9500318a3a8ad8531 100644
--- a/lib/banzai/filter/merge_request_reference_filter.rb
+++ b/lib/banzai/filter/merge_request_reference_filter.rb
@@ -17,10 +17,19 @@ module Banzai
                                             only_path: context[:only_path])
       end
 
+      def object_link_title(object, matches)
+        object_link_commit_title(object, matches) || super
+      end
+
       def object_link_text_extras(object, matches)
         extras = super
 
+        if commit_ref = object_link_commit_ref(object, matches)
+          return extras.unshift(commit_ref)
+        end
+
         path = matches[:path] if matches.names.include?("path")
+
         case path
         when '/diffs'
           extras.unshift "diffs"
@@ -38,6 +47,36 @@ module Banzai
           .where(iid: ids.to_a)
           .includes(target_project: :namespace)
       end
+
+      private
+
+      def object_link_commit_title(object, matches)
+        object_link_commit(object, matches)&.title
+      end
+
+      def object_link_commit_ref(object, matches)
+        object_link_commit(object, matches)&.short_id
+      end
+
+      def object_link_commit(object, matches)
+        return unless matches.names.include?('query') && query = matches[:query]
+
+        # Removes leading "?". CGI.parse expects "arg1&arg2&arg3"
+        params = CGI.parse(query.sub(/^\?/, ''))
+
+        return unless commit_sha = params['commit_id']&.first
+
+        if commit = find_commit_by_sha(object, commit_sha)
+          Commit.from_hash(commit.to_hash, object.project)
+        end
+      end
+
+      def find_commit_by_sha(object, commit_sha)
+        @all_commits ||= {}
+        @all_commits[object.id] ||= object.all_commits
+
+        @all_commits[object.id].find { |commit| commit.sha == commit_sha }
+      end
     end
   end
 end
diff --git a/lib/banzai/filter/milestone_reference_filter.rb b/lib/banzai/filter/milestone_reference_filter.rb
index 8ec696ce5fc66dcab5dff7bdbf792c44a4ef1005..1a1d7dbeb3ddc0dbf04e9d170b79d762aeb8e9bf 100644
--- a/lib/banzai/filter/milestone_reference_filter.rb
+++ b/lib/banzai/filter/milestone_reference_filter.rb
@@ -84,7 +84,7 @@ module Banzai
         end
       end
 
-      def object_link_title(object)
+      def object_link_title(object, matches)
         nil
       end
     end
diff --git a/lib/banzai/pipeline/commit_description_pipeline.rb b/lib/banzai/pipeline/commit_description_pipeline.rb
new file mode 100644
index 0000000000000000000000000000000000000000..607c2731ed35385d0876b1a3254f0623e00750a9
--- /dev/null
+++ b/lib/banzai/pipeline/commit_description_pipeline.rb
@@ -0,0 +1,11 @@
+module Banzai
+  module Pipeline
+    class CommitDescriptionPipeline < SingleLinePipeline
+      def self.filters
+        @filters ||= super.concat FilterArray[
+          Filter::CommitTrailersFilter,
+        ]
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/auth/ldap/access.rb b/lib/gitlab/auth/ldap/access.rb
index 77c0ddc2d48ef560e4ce460a063063b820ba7957..34286900e72f53b2e1cc2e5940a0e7ed029365cc 100644
--- a/lib/gitlab/auth/ldap/access.rb
+++ b/lib/gitlab/auth/ldap/access.rb
@@ -52,6 +52,8 @@ module Gitlab
             block_user(user, 'does not exist anymore')
             false
           end
+        rescue LDAPConnectionError
+          false
         end
 
         def adapter
diff --git a/lib/gitlab/auth/ldap/adapter.rb b/lib/gitlab/auth/ldap/adapter.rb
index caf2d18c668b8385dc38bc42a3ff2e2fd74f702b..82ff1e77e5c936f4e054cab86cd344918abe75ac 100644
--- a/lib/gitlab/auth/ldap/adapter.rb
+++ b/lib/gitlab/auth/ldap/adapter.rb
@@ -2,6 +2,9 @@ module Gitlab
   module Auth
     module LDAP
       class Adapter
+        SEARCH_RETRY_FACTOR = [1, 1, 2, 3].freeze
+        MAX_SEARCH_RETRIES = Rails.env.test? ? 1 : SEARCH_RETRY_FACTOR.size.freeze
+
         attr_reader :provider, :ldap
 
         def self.open(provider, &block)
@@ -16,7 +19,7 @@ module Gitlab
 
         def initialize(provider, ldap = nil)
           @provider = provider
-          @ldap = ldap || Net::LDAP.new(config.adapter_options)
+          @ldap = ldap || renew_connection_adapter
         end
 
         def config
@@ -47,8 +50,10 @@ module Gitlab
         end
 
         def ldap_search(*args)
+          retries ||= 0
+
           # Net::LDAP's `time` argument doesn't work. Use Ruby `Timeout` instead.
-          Timeout.timeout(config.timeout) do
+          Timeout.timeout(timeout_time(retries)) do
             results = ldap.search(*args)
 
             if results.nil?
@@ -63,16 +68,26 @@ module Gitlab
               results
             end
           end
-        rescue Net::LDAP::Error => error
-          Rails.logger.warn("LDAP search raised exception #{error.class}: #{error.message}")
-          []
-        rescue Timeout::Error
-          Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds")
-          []
+        rescue Net::LDAP::Error, Timeout::Error => error
+          retries += 1
+          error_message = connection_error_message(error)
+
+          Rails.logger.warn(error_message)
+
+          if retries < MAX_SEARCH_RETRIES
+            renew_connection_adapter
+            retry
+          else
+            raise LDAPConnectionError, error_message
+          end
         end
 
         private
 
+        def timeout_time(retry_number)
+          SEARCH_RETRY_FACTOR[retry_number] * config.timeout
+        end
+
         def user_options(fields, value, limit)
           options = {
             attributes: Gitlab::Auth::LDAP::Person.ldap_attributes(config),
@@ -104,6 +119,18 @@ module Gitlab
             filter
           end
         end
+
+        def connection_error_message(exception)
+          if exception.is_a?(Timeout::Error)
+            "LDAP search timed out after #{config.timeout} seconds"
+          else
+            "LDAP search raised exception #{exception.class}: #{exception.message}"
+          end
+        end
+
+        def renew_connection_adapter
+          @ldap = Net::LDAP.new(config.adapter_options)
+        end
       end
     end
   end
diff --git a/lib/gitlab/auth/ldap/ldap_connection_error.rb b/lib/gitlab/auth/ldap/ldap_connection_error.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ef0a695742bd311eae820e306084d93a8eedb9bf
--- /dev/null
+++ b/lib/gitlab/auth/ldap/ldap_connection_error.rb
@@ -0,0 +1,7 @@
+module Gitlab
+  module Auth
+    module LDAP
+      LDAPConnectionError = Class.new(StandardError)
+    end
+  end
+end
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
index b6a96081278d9948d50374fe51755c6e2bef6069..d0c6b0386ba55edc69e6f461b14fe16c8ab1c694 100644
--- a/lib/gitlab/auth/o_auth/user.rb
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -124,6 +124,9 @@ module Gitlab
           Gitlab::Auth::LDAP::Person.find_by_uid(auth_hash.uid, adapter) ||
             Gitlab::Auth::LDAP::Person.find_by_email(auth_hash.uid, adapter) ||
             Gitlab::Auth::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
+
+        rescue Gitlab::Auth::LDAP::LDAPConnectionError
+          nil
         end
 
         def ldap_config
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index bffbcb861373483555dc919307a8ca8794fc8eb1..f3999e690fa338058747c82790c3b29309b09c03 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -63,7 +63,7 @@ module Gitlab
 
         disk_path = project.wiki.disk_path
         import_url = project.import_url.sub(/\.git\z/, ".git/wiki")
-        gitlab_shell.import_repository(project.repository_storage_path, disk_path, import_url)
+        gitlab_shell.import_repository(project.repository_storage, disk_path, import_url)
       rescue StandardError => e
         errors << { type: :wiki, errors: e.message }
       end
diff --git a/lib/gitlab/checks/lfs_integrity.rb b/lib/gitlab/checks/lfs_integrity.rb
index f7276a380dcc09f585abcda63305559367d74a12..f0e5773ec3c418d2313dfa82e9166922ebfe7e57 100644
--- a/lib/gitlab/checks/lfs_integrity.rb
+++ b/lib/gitlab/checks/lfs_integrity.rb
@@ -15,8 +15,7 @@ module Gitlab
 
         return false unless new_lfs_pointers.present?
 
-        existing_count = @project.lfs_storage_project
-                                 .lfs_objects
+        existing_count = @project.all_lfs_objects
                                  .where(oid: new_lfs_pointers.map(&:lfs_oid))
                                  .count
 
diff --git a/lib/gitlab/git/checksum.rb b/lib/gitlab/git/checksum.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3ef0f0a88542b284a704079b0d4c189909a21b10
--- /dev/null
+++ b/lib/gitlab/git/checksum.rb
@@ -0,0 +1,82 @@
+module Gitlab
+  module Git
+    class Checksum
+      include Gitlab::Git::Popen
+
+      EMPTY_REPOSITORY_CHECKSUM = '0000000000000000000000000000000000000000'.freeze
+
+      Failure = Class.new(StandardError)
+
+      attr_reader :path, :relative_path, :storage, :storage_path
+
+      def initialize(storage, relative_path)
+        @storage       = storage
+        @storage_path  = Gitlab.config.repositories.storages[storage].legacy_disk_path
+        @relative_path = "#{relative_path}.git"
+        @path          = File.join(storage_path, @relative_path)
+      end
+
+      def calculate
+        unless repository_exists?
+          failure!(Gitlab::Git::Repository::NoRepository, 'No repository for such path')
+        end
+
+        calculate_checksum_by_shelling_out
+      end
+
+      private
+
+      def repository_exists?
+        raw_repository.exists?
+      end
+
+      def calculate_checksum_by_shelling_out
+        args = %W(--git-dir=#{path} show-ref --heads --tags)
+        output, status = run_git(args)
+
+        if status&.zero?
+          refs = output.split("\n")
+
+          result = refs.inject(nil) do |checksum, ref|
+            value = Digest::SHA1.hexdigest(ref).hex
+
+            if checksum.nil?
+              value
+            else
+              checksum ^ value
+            end
+          end
+
+          result.to_s(16)
+        else
+          # Empty repositories return with a non-zero status and an empty output.
+          if output&.empty?
+            EMPTY_REPOSITORY_CHECKSUM
+          else
+            failure!(Gitlab::Git::Checksum::Failure, output)
+          end
+        end
+      end
+
+      def failure!(klass, message)
+        Gitlab::GitLogger.error("'git show-ref --heads --tags' in #{path}: #{message}")
+
+        raise klass.new("Could not calculate the checksum for #{path}: #{message}")
+      end
+
+      def circuit_breaker
+        @circuit_breaker ||= Gitlab::Git::Storage::CircuitBreaker.for_storage(storage)
+      end
+
+      def raw_repository
+        Gitlab::Git::Repository.new(storage, relative_path, nil)
+      end
+
+      def run_git(args)
+        circuit_breaker.perform do
+          popen([Gitlab.config.git.bin_path, *args], path)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 93037ed8d903f523477a65bd0dde6f1aace9bcf6..0fb82441bf868555f0da5a242967e2c9e82db577 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -231,7 +231,8 @@ module Gitlab
         # relation to each other. The last 10 commits for a branch for example,
         # should go through .where
         def batch_by_oid(repo, oids)
-          repo.gitaly_migrate(:list_commits_by_oid) do |is_enabled|
+          repo.gitaly_migrate(:list_commits_by_oid,
+                              status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
             if is_enabled
               repo.gitaly_commit_client.list_commits_by_oid(oids)
             else
diff --git a/lib/gitlab/git/gitlab_projects.rb b/lib/gitlab/git/gitlab_projects.rb
index dc0bc8518bcbaf60b1145e3b730e8c1ab4b23b53..099709620b31cc4edcc1ac4dbfb4a4f46cff40b0 100644
--- a/lib/gitlab/git/gitlab_projects.rb
+++ b/lib/gitlab/git/gitlab_projects.rb
@@ -4,20 +4,14 @@ module Gitlab
       include Gitlab::Git::Popen
       include Gitlab::Utils::StrongMemoize
 
-      ShardNameNotFoundError = Class.new(StandardError)
-
-      # Absolute path to directory where repositories are stored.
-      # Example: /home/git/repositories
-      attr_reader :shard_path
+      # Name of shard where repositories are stored.
+      # Example: nfs-file06
+      attr_reader :shard_name
 
       # Relative path is a directory name for repository with .git at the end.
       # Example: gitlab-org/gitlab-test.git
       attr_reader :repository_relative_path
 
-      # Absolute path to the repository.
-      # Example: /home/git/repositorities/gitlab-org/gitlab-test.git
-      attr_reader :repository_absolute_path
-
       # This is the path at which the gitlab-shell hooks directory can be found.
       # It's essential for integration between git and GitLab proper. All new
       # repositories should have their hooks directory symlinked here.
@@ -25,13 +19,12 @@ module Gitlab
 
       attr_reader :logger
 
-      def initialize(shard_path, repository_relative_path, global_hooks_path:, logger:)
-        @shard_path = shard_path
+      def initialize(shard_name, repository_relative_path, global_hooks_path:, logger:)
+        @shard_name = shard_name
         @repository_relative_path = repository_relative_path
 
         @logger = logger
         @global_hooks_path = global_hooks_path
-        @repository_absolute_path = File.join(shard_path, repository_relative_path)
         @output = StringIO.new
       end
 
@@ -41,6 +34,22 @@ module Gitlab
         io.read
       end
 
+      # Absolute path to the repository.
+      # Example: /home/git/repositorities/gitlab-org/gitlab-test.git
+      # Probably will be removed when we fully migrate to Gitaly, part of
+      # https://gitlab.com/gitlab-org/gitaly/issues/1124.
+      def repository_absolute_path
+        strong_memoize(:repository_absolute_path) do
+          File.join(shard_path, repository_relative_path)
+        end
+      end
+
+      def shard_path
+        strong_memoize(:shard_path) do
+          Gitlab.config.repositories.storages.fetch(shard_name).legacy_disk_path
+        end
+      end
+
       # Import project via git clone --bare
       # URL must be publicly cloneable
       def import_project(source, timeout)
@@ -53,12 +62,12 @@ module Gitlab
         end
       end
 
-      def fork_repository(new_shard_path, new_repository_relative_path)
+      def fork_repository(new_shard_name, new_repository_relative_path)
         Gitlab::GitalyClient.migrate(:fork_repository) do |is_enabled|
           if is_enabled
-            gitaly_fork_repository(new_shard_path, new_repository_relative_path)
+            gitaly_fork_repository(new_shard_name, new_repository_relative_path)
           else
-            git_fork_repository(new_shard_path, new_repository_relative_path)
+            git_fork_repository(new_shard_name, new_repository_relative_path)
           end
         end
       end
@@ -205,17 +214,6 @@ module Gitlab
 
       private
 
-      def shard_name
-        strong_memoize(:shard_name) do
-          shard_name_from_shard_path(shard_path)
-        end
-      end
-
-      def shard_name_from_shard_path(shard_path)
-        Gitlab.config.repositories.storages.find { |_, info| info.legacy_disk_path == shard_path }&.first ||
-          raise(ShardNameNotFoundError, "no shard found for path '#{shard_path}'")
-      end
-
       def git_import_repository(source, timeout)
         # Skip import if repo already exists
         return false if File.exist?(repository_absolute_path)
@@ -252,8 +250,9 @@ module Gitlab
         false
       end
 
-      def git_fork_repository(new_shard_path, new_repository_relative_path)
+      def git_fork_repository(new_shard_name, new_repository_relative_path)
         from_path = repository_absolute_path
+        new_shard_path = Gitlab.config.repositories.storages.fetch(new_shard_name).legacy_disk_path
         to_path = File.join(new_shard_path, new_repository_relative_path)
 
         # The repository cannot already exist
@@ -271,8 +270,8 @@ module Gitlab
         run(cmd, nil) && Gitlab::Git::Repository.create_hooks(to_path, global_hooks_path)
       end
 
-      def gitaly_fork_repository(new_shard_path, new_repository_relative_path)
-        target_repository = Gitlab::Git::Repository.new(shard_name_from_shard_path(new_shard_path), new_repository_relative_path, nil)
+      def gitaly_fork_repository(new_shard_name, new_repository_relative_path)
+        target_repository = Gitlab::Git::Repository.new(new_shard_name, new_repository_relative_path, nil)
         raw_repository = Gitlab::Git::Repository.new(shard_name, repository_relative_path, nil)
 
         Gitlab::GitalyClient::RepositoryService.new(target_repository).fork_repository(raw_repository)
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index e692c9ce342dcdfd7fd811872ca5bf4999fa1cff..8d97bfb0e6ac256ef35bfa95a72000f5bd0b6ca3 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -96,7 +96,7 @@ module Gitlab
 
         storage_path = Gitlab.config.repositories.storages[@storage].legacy_disk_path
         @gitlab_projects = Gitlab::Git::GitlabProjects.new(
-          storage_path,
+          storage,
           relative_path,
           global_hooks_path: Gitlab.config.gitlab_shell.hooks_path,
           logger: Rails.logger
@@ -885,7 +885,8 @@ module Gitlab
       end
 
       def delete_refs(*ref_names)
-        gitaly_migrate(:delete_refs) do |is_enabled|
+        gitaly_migrate(:delete_refs,
+                      status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
           if is_enabled
             gitaly_delete_refs(*ref_names)
           else
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
index b1b283e98b5bf9ef96bbcbc52019192ebe7225ac..01168abde6cc5465f29c81c947a8a842a3f45334 100644
--- a/lib/gitlab/github_import/importer/repository_importer.rb
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -56,9 +56,8 @@ module Gitlab
 
         def import_wiki_repository
           wiki_path = "#{project.disk_path}.wiki"
-          storage_path = project.repository_storage_path
 
-          gitlab_shell.import_repository(storage_path, wiki_path, wiki_url)
+          gitlab_shell.import_repository(project.repository_storage, wiki_path, wiki_url)
 
           true
         rescue Gitlab::Shell::Error => e
diff --git a/lib/gitlab/import_export/importer.rb b/lib/gitlab/import_export/importer.rb
index c38df9102ebc72f0e610c7960a28383a88907e44..c490bf059d2e959eb44b2315c02d8953a11c6d08 100644
--- a/lib/gitlab/import_export/importer.rb
+++ b/lib/gitlab/import_export/importer.rb
@@ -13,7 +13,7 @@ module Gitlab
       end
 
       def execute
-        if import_file && check_version! && [repo_restorer, wiki_restorer, project_tree, avatar_restorer, uploads_restorer].all?(&:restore)
+        if import_file && check_version! && restorers.all?(&:restore)
           project_tree.restored_project
         else
           raise Projects::ImportService::Error.new(@shared.errors.join(', '))
@@ -24,6 +24,11 @@ module Gitlab
 
       private
 
+      def restorers
+        [repo_restorer, wiki_restorer, project_tree, avatar_restorer,
+         uploads_restorer, lfs_restorer]
+      end
+
       def import_file
         Gitlab::ImportExport::FileImporter.import(archive_file: @archive_file,
                                                   shared: @shared)
@@ -60,6 +65,10 @@ module Gitlab
         Gitlab::ImportExport::UploadsRestorer.new(project: project_tree.restored_project, shared: @shared)
       end
 
+      def lfs_restorer
+        Gitlab::ImportExport::LfsRestorer.new(project: project_tree.restored_project, shared: @shared)
+      end
+
       def path_with_namespace
         File.join(@project.namespace.full_path, @project.path)
       end
diff --git a/lib/gitlab/import_export/lfs_restorer.rb b/lib/gitlab/import_export/lfs_restorer.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b28c3c161b7716165eb5cdc854327a9bcfd62f4a
--- /dev/null
+++ b/lib/gitlab/import_export/lfs_restorer.rb
@@ -0,0 +1,43 @@
+module Gitlab
+  module ImportExport
+    class LfsRestorer
+      def initialize(project:, shared:)
+        @project = project
+        @shared = shared
+      end
+
+      def restore
+        return true if lfs_file_paths.empty?
+
+        lfs_file_paths.each do |file_path|
+          link_or_create_lfs_object!(file_path)
+        end
+
+        true
+      rescue => e
+        @shared.error(e)
+        false
+      end
+
+      private
+
+      def link_or_create_lfs_object!(path)
+        size = File.size(path)
+        oid = LfsObject.calculate_oid(path)
+
+        lfs_object = LfsObject.find_or_initialize_by(oid: oid, size: size)
+        lfs_object.file = File.open(path) unless lfs_object.file&.exists?
+
+        @project.all_lfs_objects << lfs_object
+      end
+
+      def lfs_file_paths
+        @lfs_file_paths ||= Dir.glob("#{lfs_storage_path}/*")
+      end
+
+      def lfs_storage_path
+        File.join(@shared.export_path, 'lfs-objects')
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/import_export/lfs_saver.rb b/lib/gitlab/import_export/lfs_saver.rb
new file mode 100644
index 0000000000000000000000000000000000000000..29410e2331c41eade626ea49ee4371e40953a077
--- /dev/null
+++ b/lib/gitlab/import_export/lfs_saver.rb
@@ -0,0 +1,55 @@
+module Gitlab
+  module ImportExport
+    class LfsSaver
+      include Gitlab::ImportExport::CommandLineUtil
+
+      def initialize(project:, shared:)
+        @project = project
+        @shared = shared
+      end
+
+      def save
+        @project.all_lfs_objects.each do |lfs_object|
+          save_lfs_object(lfs_object)
+        end
+
+        true
+      rescue => e
+        @shared.error(e)
+
+        false
+      end
+
+      private
+
+      def save_lfs_object(lfs_object)
+        if lfs_object.local_store?
+          copy_file_for_lfs_object(lfs_object)
+        else
+          download_file_for_lfs_object(lfs_object)
+        end
+      end
+
+      def download_file_for_lfs_object(lfs_object)
+        destination = destination_path_for_object(lfs_object)
+        mkdir_p(File.dirname(destination))
+
+        File.open(destination, 'w') do |file|
+          IO.copy_stream(URI.parse(lfs_object.file.url).open, file)
+        end
+      end
+
+      def copy_file_for_lfs_object(lfs_object)
+        copy_files(lfs_object.file.path, destination_path_for_object(lfs_object))
+      end
+
+      def destination_path_for_object(lfs_object)
+        File.join(lfs_export_path, lfs_object.oid)
+      end
+
+      def lfs_export_path
+        File.join(@shared.export_path, 'lfs-objects')
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/legacy_github_import/importer.rb b/lib/gitlab/legacy_github_import/importer.rb
index 0526ef9eb1327de8fd37576d29876a115785ae72..7edd0ad2033c5bc7aa5c189744bd36b68e1bd7af 100644
--- a/lib/gitlab/legacy_github_import/importer.rb
+++ b/lib/gitlab/legacy_github_import/importer.rb
@@ -259,7 +259,7 @@ module Gitlab
       def import_wiki
         unless project.wiki.repository_exists?
           wiki = WikiFormatter.new(project)
-          gitlab_shell.import_repository(project.repository_storage_path, wiki.disk_path, wiki.import_url)
+          gitlab_shell.import_repository(project.repository_storage, wiki.disk_path, wiki.import_url)
         end
       rescue Gitlab::Shell::Error => e
         # GitHub error message when the wiki repo has not been created,
diff --git a/lib/gitlab/metrics/sidekiq_metrics_exporter.rb b/lib/gitlab/metrics/sidekiq_metrics_exporter.rb
index db8bdde74b2916b0d1474d5d2f16b9fb7442aea2..47b4af5d6490da24127fe55dfb4b65135b4d1dbc 100644
--- a/lib/gitlab/metrics/sidekiq_metrics_exporter.rb
+++ b/lib/gitlab/metrics/sidekiq_metrics_exporter.rb
@@ -4,6 +4,8 @@ require 'prometheus/client/rack/exporter'
 module Gitlab
   module Metrics
     class SidekiqMetricsExporter < Daemon
+      LOG_FILENAME = File.join(Rails.root, 'log', 'sidekiq_exporter.log')
+
       def enabled?
         Gitlab::Metrics.metrics_folder_present? && settings.enabled
       end
@@ -17,7 +19,13 @@ module Gitlab
       attr_reader :server
 
       def start_working
-        @server = ::WEBrick::HTTPServer.new(Port: settings.port, BindAddress: settings.address)
+        logger = WEBrick::Log.new(LOG_FILENAME)
+        access_log = [
+          [logger, WEBrick::AccessLog::COMBINED_LOG_FORMAT]
+        ]
+
+        @server = ::WEBrick::HTTPServer.new(Port: settings.port, BindAddress: settings.address,
+                                            Logger: logger, AccessLog: access_log)
         server.mount "/", Rack::Handler::WEBrick, rack_app
         server.start
       end
diff --git a/lib/gitlab/performance_bar.rb b/lib/gitlab/performance_bar.rb
index 6c2b20360748f4605d184c09e88cffb231d60398..92a308a12dc9a7bfc23e004338da1caa0173f3ba 100644
--- a/lib/gitlab/performance_bar.rb
+++ b/lib/gitlab/performance_bar.rb
@@ -5,6 +5,7 @@ module Gitlab
 
     def self.enabled?(user = nil)
       return true if Rails.env.development?
+      return true if user&.admin?
       return false unless user && allowed_group_id
 
       allowed_user_ids.include?(user.id)
diff --git a/lib/gitlab/shell.rb b/lib/gitlab/shell.rb
index c8c15b9684a09cbe29c417d90a1b45542b460c39..67407b651a58a8403b0fb05e95d6e6e05f30e683 100644
--- a/lib/gitlab/shell.rb
+++ b/lib/gitlab/shell.rb
@@ -93,12 +93,12 @@ module Gitlab
 
     # Import repository
     #
-    # storage - project's storage path
+    # storage - project's storage name
     # name - project disk path
     # url - URL to import from
     #
     # Ex.
-    #   import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git")
+    #   import_repository("nfs-file06", "gitlab/gitlab-ci", "https://gitlab.com/gitlab-org/gitlab-test.git")
     #
     # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/874
     def import_repository(storage, name, url)
@@ -131,8 +131,7 @@ module Gitlab
         if is_enabled
           repository.gitaly_repository_client.fetch_remote(remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, timeout: git_timeout, prune: prune)
         else
-          storage_path = Gitlab.config.repositories.storages[repository.storage].legacy_disk_path
-          local_fetch_remote(storage_path, repository.relative_path, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune)
+          local_fetch_remote(repository.storage, repository.relative_path, remote, ssh_auth: ssh_auth, forced: forced, no_tags: no_tags, prune: prune)
         end
       end
     end
@@ -156,13 +155,13 @@ module Gitlab
     end
 
     # Fork repository to new path
-    # forked_from_storage - forked-from project's storage path
-    # forked_from_disk_path - project disk path
-    # forked_to_storage - forked-to project's storage path
-    # forked_to_disk_path - forked project disk path
+    # forked_from_storage - forked-from project's storage name
+    # forked_from_disk_path - project disk relative path
+    # forked_to_storage - forked-to project's storage name
+    # forked_to_disk_path - forked project disk relative path
     #
     # Ex.
-    #  fork_repository("/path/to/forked_from/storage", "gitlab/gitlab-ci", "/path/to/forked_to/storage", "new-namespace/gitlab-ci")
+    #  fork_repository("nfs-file06", "gitlab/gitlab-ci", "nfs-file07", "new-namespace/gitlab-ci")
     #
     # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/817
     def fork_repository(forked_from_storage, forked_from_disk_path, forked_to_storage, forked_to_disk_path)
@@ -420,16 +419,16 @@ module Gitlab
 
     private
 
-    def gitlab_projects(shard_path, disk_path)
+    def gitlab_projects(shard_name, disk_path)
       Gitlab::Git::GitlabProjects.new(
-        shard_path,
+        shard_name,
         disk_path,
         global_hooks_path: Gitlab.config.gitlab_shell.hooks_path,
         logger: Rails.logger
       )
     end
 
-    def local_fetch_remote(storage_path, repository_relative_path, remote, ssh_auth: nil, forced: false, no_tags: false, prune: true)
+    def local_fetch_remote(storage_name, repository_relative_path, remote, ssh_auth: nil, forced: false, no_tags: false, prune: true)
       vars = { force: forced, tags: !no_tags, prune: prune }
 
       if ssh_auth&.ssh_import?
@@ -442,7 +441,7 @@ module Gitlab
         end
       end
 
-      cmd = gitlab_projects(storage_path, repository_relative_path)
+      cmd = gitlab_projects(storage_name, repository_relative_path)
 
       success = cmd.fetch_remote(remote, git_timeout, vars)
 
diff --git a/lib/gitlab/sidekiq_logging/json_formatter.rb b/lib/gitlab/sidekiq_logging/json_formatter.rb
new file mode 100644
index 0000000000000000000000000000000000000000..98f8222fd035c2f2e84dfe3e2f4ca538521d407e
--- /dev/null
+++ b/lib/gitlab/sidekiq_logging/json_formatter.rb
@@ -0,0 +1,21 @@
+module Gitlab
+  module SidekiqLogging
+    class JSONFormatter
+      def call(severity, timestamp, progname, data)
+        output = {
+          severity: severity,
+          time: timestamp.utc.iso8601(3)
+        }
+
+        case data
+        when String
+          output[:message] = data
+        when Hash
+          output.merge!(data)
+        end
+
+        output.to_json + "\n"
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/sidekiq_logging/structured_logger.rb b/lib/gitlab/sidekiq_logging/structured_logger.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9a89ae70b9867befc5d54eb017513f8baf8de010
--- /dev/null
+++ b/lib/gitlab/sidekiq_logging/structured_logger.rb
@@ -0,0 +1,96 @@
+module Gitlab
+  module SidekiqLogging
+    class StructuredLogger
+      START_TIMESTAMP_FIELDS = %w[created_at enqueued_at].freeze
+      DONE_TIMESTAMP_FIELDS = %w[started_at retried_at failed_at completed_at].freeze
+
+      def call(job, queue)
+        started_at = current_time
+        base_payload = parse_job(job)
+
+        Sidekiq.logger.info log_job_start(started_at, base_payload)
+
+        yield
+
+        Sidekiq.logger.info log_job_done(started_at, base_payload)
+      rescue => job_exception
+        Sidekiq.logger.warn log_job_done(started_at, base_payload, job_exception)
+
+        raise
+      end
+
+      private
+
+      def base_message(payload)
+        "#{payload['class']} JID-#{payload['jid']}"
+      end
+
+      def log_job_start(started_at, payload)
+        payload['message'] = "#{base_message(payload)}: start"
+        payload['job_status'] = 'start'
+
+        payload
+      end
+
+      def log_job_done(started_at, payload, job_exception = nil)
+        payload = payload.dup
+        payload['duration'] = elapsed(started_at)
+        payload['completed_at'] = Time.now.utc
+
+        message = base_message(payload)
+
+        if job_exception
+          payload['message'] = "#{message}: fail: #{payload['duration']} sec"
+          payload['job_status'] = 'fail'
+          payload['error_message'] = job_exception.message
+          payload['error'] = job_exception.class
+          payload['error_backtrace'] = backtrace_cleaner.clean(job_exception.backtrace)
+        else
+          payload['message'] = "#{message}: done: #{payload['duration']} sec"
+          payload['job_status'] = 'done'
+        end
+
+        convert_to_iso8601(payload, DONE_TIMESTAMP_FIELDS)
+
+        payload
+      end
+
+      def parse_job(job)
+        job = job.dup
+
+        # Add process id params
+        job['pid'] = ::Process.pid
+
+        job.delete('args') unless ENV['SIDEKIQ_LOG_ARGUMENTS']
+
+        convert_to_iso8601(job, START_TIMESTAMP_FIELDS)
+
+        job
+      end
+
+      def convert_to_iso8601(payload, keys)
+        keys.each do |key|
+          payload[key] = format_time(payload[key]) if payload[key]
+        end
+      end
+
+      def elapsed(start)
+        (current_time - start).round(3)
+      end
+
+      def current_time
+        Gitlab::Metrics::System.monotonic_time
+      end
+
+      def backtrace_cleaner
+        @backtrace_cleaner ||= ActiveSupport::BacktraceCleaner.new
+      end
+
+      def format_time(timestamp)
+        return timestamp if timestamp.is_a?(String)
+
+        Time.at(timestamp).utc.iso8601(3)
+      end
+    end
+  end
+end
diff --git a/lib/tasks/gitlab/two_factor.rake b/lib/tasks/gitlab/two_factor.rake
index 7728c485e8dbc0b3d52ff117bd34232c2cd2b919..6b22499a5c8d4798d14f69bdc0cf390c6c38a477 100644
--- a/lib/tasks/gitlab/two_factor.rake
+++ b/lib/tasks/gitlab/two_factor.rake
@@ -1,7 +1,7 @@
 namespace :gitlab do
   namespace :two_factor do
     desc "GitLab | Disable Two-factor authentication (2FA) for all users"
-    task disable_for_all_users: :environment do
+    task disable_for_all_users: :gitlab_environment do
       scope = User.with_two_factor
       count = scope.count
 
diff --git a/lib/tasks/test.rake b/lib/tasks/test.rake
index 3e01f91d32c5fa6041a294adc6298b403dd506ee..b52af81fc160150f09efbdaa47b7aa2b8dea90a5 100644
--- a/lib/tasks/test.rake
+++ b/lib/tasks/test.rake
@@ -4,8 +4,3 @@ desc "GitLab | Run all tests"
 task :test do
   Rake::Task["gitlab:test"].invoke
 end
-
-unless Rails.env.production?
-  desc "GitLab | Run all tests on CI with simplecov"
-  task test_ci: [:rubocop, :brakeman, :karma, :spinach, :spec]
-end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 68d0c0c8854bac81b30ecf7ece6f7d1bb5634fa2..d7eb123b48b11e5b61aa6ce07273e019e28025b2 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-27 14:40+0300\n"
-"PO-Revision-Date: 2018-03-27 14:40+0300\n"
+"POT-Creation-Date: 2018-04-04 18:02+0200\n"
+"PO-Revision-Date: 2018-04-04 18:02+0200\n"
 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
 "Language-Team: LANGUAGE <LL@li.org>\n"
 "Language: \n"
@@ -133,6 +133,9 @@ msgstr ""
 msgid "Abuse Reports"
 msgstr ""
 
+msgid "Abuse reports"
+msgstr ""
+
 msgid "Access Tokens"
 msgstr ""
 
@@ -142,6 +145,9 @@ msgstr ""
 msgid "Account"
 msgstr ""
 
+msgid "Account and limit settings"
+msgstr ""
+
 msgid "Active"
 msgstr ""
 
@@ -235,6 +241,9 @@ msgstr ""
 msgid "Allow edits from maintainers."
 msgstr ""
 
+msgid "Allow requests to the local network from hooks and services."
+msgstr ""
+
 msgid "Allows you to add and manage Kubernetes clusters."
 msgstr ""
 
@@ -343,6 +352,12 @@ msgstr ""
 msgid "Assign to"
 msgstr ""
 
+msgid "Assigned Issues"
+msgstr ""
+
+msgid "Assigned Merge Requests"
+msgstr ""
+
 msgid "Assigned to :name"
 msgstr ""
 
@@ -370,6 +385,9 @@ msgstr ""
 msgid "Auto DevOps enabled"
 msgstr ""
 
+msgid "Auto DevOps, runners and job artifacts"
+msgstr ""
+
 msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
 msgstr ""
 
@@ -412,6 +430,9 @@ msgstr ""
 msgid "Average per day: %{average}"
 msgstr ""
 
+msgid "Background jobs"
+msgstr ""
+
 msgid "Begin with the selected commit"
 msgstr ""
 
@@ -1102,6 +1123,9 @@ msgstr ""
 msgid "Compare changes with the last commit"
 msgstr ""
 
+msgid "Compare changes with the merge request target branch"
+msgstr ""
+
 msgid "CompareBranches|%{source_branch} and %{target_branch} are the same."
 msgstr ""
 
@@ -1123,6 +1147,21 @@ msgstr ""
 msgid "Confidentiality"
 msgstr ""
 
+msgid "Configure Sidekiq job throttling."
+msgstr ""
+
+msgid "Configure automatic git checks and housekeeping on repositories."
+msgstr ""
+
+msgid "Configure limits for web and API requests."
+msgstr ""
+
+msgid "Configure storage path and circuit breaker settings."
+msgstr ""
+
+msgid "Configure the way a user creates a new account."
+msgstr ""
+
 msgid "Connect"
 msgstr ""
 
@@ -1174,6 +1213,9 @@ msgstr ""
 msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images."
 msgstr ""
 
+msgid "Continuous Integration and Deployment"
+msgstr ""
+
 msgid "Contribution"
 msgstr ""
 
@@ -1419,6 +1461,21 @@ msgstr ""
 msgid "Enable Auto DevOps"
 msgstr ""
 
+msgid "Enable Sentry for error reporting and logging."
+msgstr ""
+
+msgid "Enable and configure InfluxDB metrics."
+msgstr ""
+
+msgid "Enable and configure Prometheus metrics."
+msgstr ""
+
+msgid "Enable reCAPTCHA or Akismet and set IP limits."
+msgstr ""
+
+msgid "Enable the Performance Bar for a given group."
+msgstr ""
+
 msgid "Environments|An error occurred while fetching the environments."
 msgstr ""
 
@@ -1467,6 +1524,9 @@ msgstr ""
 msgid "Environments|You don't have any environments right now."
 msgstr ""
 
+msgid "Error Reporting and Logging"
+msgstr ""
+
 msgid "Error checking branch data. Please try again."
 msgstr ""
 
@@ -1637,6 +1697,9 @@ msgstr ""
 msgid "GitHub import"
 msgstr ""
 
+msgid "GitLab CI Linter has been moved"
+msgstr ""
+
 msgid "GitLab Runner section"
 msgstr ""
 
@@ -1742,6 +1805,12 @@ msgstr ""
 msgid "Help"
 msgstr ""
 
+msgid "Help page"
+msgstr ""
+
+msgid "Help page text and support page url."
+msgstr ""
+
 msgid "Hide value"
 msgid_plural "Hide values"
 msgstr[0] ""
@@ -1879,10 +1948,10 @@ msgstr ""
 msgid "Labels can be applied to issues and merge requests to categorize them."
 msgstr ""
 
-msgid "Labels|Promote Label"
+msgid "Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>"
 msgstr ""
 
-msgid "Labels|Promote label %{labelTitle} to Group Label?"
+msgid "Labels|Promote Label"
 msgstr ""
 
 msgid "Last %d day"
@@ -1950,9 +2019,6 @@ msgstr ""
 msgid "Lock %{issuableDisplayName}"
 msgstr ""
 
-msgid "Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment."
-msgstr ""
-
 msgid "Locked"
 msgstr ""
 
@@ -2010,6 +2076,12 @@ msgstr ""
 msgid "Messages"
 msgstr ""
 
+msgid "Metrics - Influx"
+msgstr ""
+
+msgid "Metrics - Prometheus"
+msgstr ""
+
 msgid "Milestone"
 msgstr ""
 
@@ -2264,12 +2336,18 @@ msgstr ""
 msgid "Otherwise it is recommended you start with one of the options below."
 msgstr ""
 
+msgid "Outbound requests"
+msgstr ""
+
 msgid "Overview"
 msgstr ""
 
 msgid "Owner"
 msgstr ""
 
+msgid "Pages"
+msgstr ""
+
 msgid "Pagination|Last »"
 msgstr ""
 
@@ -2282,6 +2360,9 @@ msgstr ""
 msgid "Pagination|« First"
 msgstr ""
 
+msgid "Part of merge request changes"
+msgstr ""
+
 msgid "Password"
 msgstr ""
 
@@ -2492,6 +2573,9 @@ msgstr ""
 msgid "Profiles|your account"
 msgstr ""
 
+msgid "Profiling - Performance bar"
+msgstr ""
+
 msgid "Programming languages used in this repository"
 msgstr ""
 
@@ -2723,6 +2807,12 @@ msgstr ""
 msgid "Repository"
 msgstr ""
 
+msgid "Repository maintenance"
+msgstr ""
+
+msgid "Repository storage"
+msgstr ""
+
 msgid "Request Access"
 msgstr ""
 
@@ -2752,6 +2842,9 @@ msgstr ""
 msgid "Reviewing"
 msgstr ""
 
+msgid "Reviewing (merge request !%{mergeRequestId})"
+msgstr ""
+
 msgid "Runners"
 msgstr ""
 
@@ -2839,9 +2932,21 @@ msgstr ""
 msgid "Service Templates"
 msgstr ""
 
+msgid "Session expiration, projects limit and attachment size."
+msgstr ""
+
 msgid "Set a password on your account to pull or push via %{protocol}."
 msgstr ""
 
+msgid "Set default and restrict visibility levels. Configure import sources and git access protocol."
+msgstr ""
+
+msgid "Set notification email for abuse reports."
+msgstr ""
+
+msgid "Set requirements for a user to sign-in. Enable mandatory two-factor authentication."
+msgstr ""
+
 msgid "Set up CI/CD"
 msgstr ""
 
@@ -2871,19 +2976,22 @@ msgid_plural "Showing %d events"
 msgstr[0] ""
 msgstr[1] ""
 
-msgid "Snippets"
+msgid "Sign-in restrictions"
 msgstr ""
 
-msgid "Something went wrong on our end"
+msgid "Sign-up restrictions"
 msgstr ""
 
-msgid "Something went wrong on our end."
+msgid "Size and domain settings for static websites"
+msgstr ""
+
+msgid "Snippets"
 msgstr ""
 
-msgid "Something went wrong trying to change the confidentiality of this issue"
+msgid "Something went wrong on our end"
 msgstr ""
 
-msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
+msgid "Something went wrong on our end."
 msgstr ""
 
 msgid "Something went wrong when toggling the button"
@@ -3003,12 +3111,21 @@ msgstr ""
 msgid "Spam Logs"
 msgstr ""
 
+msgid "Spam and Anti-bot Protection"
+msgstr ""
+
 msgid "Specify the following URL during the Runner setup:"
 msgstr ""
 
 msgid "StarProject|Star"
 msgstr ""
 
+msgid "Starred Projects"
+msgstr ""
+
+msgid "Starred Projects' Activity"
+msgstr ""
+
 msgid "Starred projects"
 msgstr ""
 
@@ -3456,6 +3573,9 @@ msgstr ""
 msgid "To import an SVN repository, check out %{svn_link}."
 msgstr ""
 
+msgid "To validate your GitLab CI configurations, go to 'CI/CD → Pipelines' inside your project, and click on the 'CI Lint' button."
+msgstr ""
+
 msgid "Todo"
 msgstr ""
 
@@ -3486,9 +3606,6 @@ msgstr ""
 msgid "Unlock"
 msgstr ""
 
-msgid "Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment."
-msgstr ""
-
 msgid "Unlocked"
 msgstr ""
 
@@ -3522,6 +3639,9 @@ msgstr ""
 msgid "Use your global notification setting"
 msgstr ""
 
+msgid "User and IP Rate Limits"
+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 ""
 
@@ -3546,6 +3666,9 @@ msgstr ""
 msgid "View replaced file @ "
 msgstr ""
 
+msgid "Visibility and access controls"
+msgstr ""
+
 msgid "VisibilityLevel|Internal"
 msgstr ""
 
@@ -3762,9 +3885,21 @@ msgstr ""
 msgid "You're receiving this email because of your account on %{host}. %{manage_notifications_link} &middot; %{help_link}"
 msgstr ""
 
+msgid "Your Groups"
+msgstr ""
+
 msgid "Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure"
 msgstr ""
 
+msgid "Your Projects (default)"
+msgstr ""
+
+msgid "Your Projects' Activity"
+msgstr ""
+
+msgid "Your Todos"
+msgstr ""
+
 msgid "Your changes can be committed to %{branch_name} because a merge request is open."
 msgstr ""
 
@@ -3795,12 +3930,6 @@ msgstr ""
 msgid "command line instructions"
 msgstr ""
 
-msgid "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."
-msgstr ""
-
-msgid "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."
-msgstr ""
-
 msgid "connecting"
 msgstr ""
 
@@ -3823,6 +3952,15 @@ msgstr[1] ""
 msgid "mrWidget| Please restore it or use a different %{missingBranchName} branch"
 msgstr ""
 
+msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
+msgstr ""
+
+msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} increased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB"
+msgstr ""
+
+msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage is %{emphasisStart} unchanged %{emphasisEnd} at %{memoryFrom}MB"
+msgstr ""
+
 msgid "mrWidget|Allows edits from maintainers"
 msgstr ""
 
@@ -3850,18 +3988,27 @@ msgstr ""
 msgid "mrWidget|Closes"
 msgstr ""
 
+msgid "mrWidget|Deployment statistics are not available currently"
+msgstr ""
+
 msgid "mrWidget|Did not close"
 msgstr ""
 
 msgid "mrWidget|Email patches"
 msgstr ""
 
+msgid "mrWidget|Failed to load deployment statistics"
+msgstr ""
+
 msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
 msgstr ""
 
 msgid "mrWidget|If the %{missingBranchName} branch exists in your local repository, you can merge this merge request manually using the command line"
 msgstr ""
 
+msgid "mrWidget|Loading deployment statistics"
+msgstr ""
+
 msgid "mrWidget|Mentions"
 msgstr ""
 
@@ -3943,6 +4090,9 @@ msgstr ""
 msgid "mrWidget|This project is archived, write access has been disabled"
 msgstr ""
 
+msgid "mrWidget|Web IDE"
+msgstr ""
+
 msgid "mrWidget|You can merge this merge request manually using the"
 msgstr ""
 
diff --git a/qa/qa.rb b/qa/qa.rb
index 7220af5088ebec0936a745e61ab22512d8f792ec..56a99c32b263696b41fa9b4856b1988323cb5017 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -90,6 +90,10 @@ module QA
       autoload :OAuth, 'qa/page/main/oauth'
     end
 
+    module Settings
+      autoload :Common, 'qa/page/settings/common'
+    end
+
     module Menu
       autoload :Main, 'qa/page/menu/main'
       autoload :Side, 'qa/page/menu/side'
@@ -150,7 +154,10 @@ module QA
     end
 
     module Admin
-      autoload :Settings, 'qa/page/admin/settings'
+      module Settings
+        autoload :RepositoryStorage, 'qa/page/admin/settings/repository_storage'
+        autoload :Main, 'qa/page/admin/settings/main'
+      end
     end
 
     module Mattermost
diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/factory/settings/hashed_storage.rb
index 13ce2435fe4152d6772022a587813621a085b7c7..c69ebed3c6b1cb1c9534dc6381d3bf0fb97c25fb 100644
--- a/qa/qa/factory/settings/hashed_storage.rb
+++ b/qa/qa/factory/settings/hashed_storage.rb
@@ -9,9 +9,11 @@ module QA
           Page::Menu::Main.act { go_to_admin_area }
           Page::Menu::Admin.act { go_to_settings }
 
-          Page::Admin::Settings.act do
-            enable_hashed_storage
-            save_settings
+          Page::Admin::Settings::Main.perform do |setting|
+            setting.expand_repository_storage do |page|
+              page.enable_hashed_storage
+              page.save_settings
+            end
           end
 
           QA::Page::Menu::Main.act { sign_out }
diff --git a/qa/qa/page/admin/settings.rb b/qa/qa/page/admin/settings.rb
deleted file mode 100644
index 1f646103e7f2e05b5abcfab4809a997710317294..0000000000000000000000000000000000000000
--- a/qa/qa/page/admin/settings.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-module QA
-  module Page
-    module Admin
-      class Settings < Page::Base
-        view 'app/views/admin/application_settings/_form.html.haml' do
-          element :form_actions, '.form-actions'
-          element :submit, "submit 'Save'"
-          element :repository_storage, '%legend Repository Storage'
-          element :hashed_storage,
-            'Create new projects using hashed storage paths'
-        end
-
-        def enable_hashed_storage
-          scroll_to 'legend', text: 'Repository Storage'
-          check 'Create new projects using hashed storage paths'
-        end
-
-        def save_settings
-          scroll_to '.form-actions' do
-            click_button 'Save'
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/qa/qa/page/admin/settings/main.rb b/qa/qa/page/admin/settings/main.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e7c1220c967769c0862a27d77fc2e1df1d1d8041
--- /dev/null
+++ b/qa/qa/page/admin/settings/main.rb
@@ -0,0 +1,21 @@
+module QA
+  module Page
+    module Admin
+      module Settings
+        class Main < Page::Base
+          include QA::Page::Settings::Common
+
+          view 'app/views/admin/application_settings/show.html.haml' do
+            element :advanced_settings_section, 'Repository storage'
+          end
+
+          def expand_repository_storage(&block)
+            expand_section('Repository storage') do
+              RepositoryStorage.perform(&block)
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/admin/settings/repository_storage.rb b/qa/qa/page/admin/settings/repository_storage.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b4a1344216e2128560e056fe25e7cebaffce2c55
--- /dev/null
+++ b/qa/qa/page/admin/settings/repository_storage.rb
@@ -0,0 +1,23 @@
+module QA
+  module Page
+    module Admin
+      module Settings
+        class RepositoryStorage < Page::Base
+          view 'app/views/admin/application_settings/_repository_storage.html.haml' do
+            element :submit, "submit 'Save changes'"
+            element :hashed_storage,
+              'Create new projects using hashed storage paths'
+          end
+
+          def enable_hashed_storage
+            check 'Create new projects using hashed storage paths'
+          end
+
+          def save_settings
+            click_button 'Save changes'
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb
index 319cb1045b6116338678ba01b53f2fb25e84cd8f..874fb381554a5d35099f44c3c672f0a4dc6cfb79 100644
--- a/qa/qa/page/project/settings/common.rb
+++ b/qa/qa/page/project/settings/common.rb
@@ -3,6 +3,8 @@ module QA
     module Project
       module Settings
         module Common
+          include QA::Page::Settings::Common
+
           def self.included(base)
             base.class_eval do
               view 'app/views/projects/edit.html.haml' do
@@ -10,24 +12,6 @@ module QA
               end
             end
           end
-
-          # Click the Expand button present in the specified section
-          #
-          # @param [String] name present in the container in the DOM
-          def expand_section(name)
-            page.within('#content-body') do
-              page.within('section', text: name) do
-                # Because it is possible to click the button before the JS toggle code is bound
-                wait(reload: false) do
-                  click_button 'Expand' unless first('button', text: 'Collapse')
-
-                  page.has_content?('Collapse')
-                end
-
-                yield if block_given?
-              end
-            end
-          end
         end
       end
     end
diff --git a/qa/qa/page/settings/common.rb b/qa/qa/page/settings/common.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a683a6829d56e973ce677b30d151a8e1c41b4dfa
--- /dev/null
+++ b/qa/qa/page/settings/common.rb
@@ -0,0 +1,25 @@
+module QA
+  module Page
+    module Settings
+      module Common
+        # Click the Expand button present in the specified section
+        #
+        # @param [String] name present in the container in the DOM
+        def expand_section(name)
+          page.within('#content-body') do
+            page.within('section', text: name) do
+              # Because it is possible to click the button before the JS toggle code is bound
+              wait(reload: false) do
+                click_button 'Expand' unless first('button', text: 'Collapse')
+
+                page.has_content?('Collapse')
+              end
+
+              yield if block_given?
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/qa/spec/page/validator_spec.rb b/qa/spec/page/validator_spec.rb
index 02822d7d18f3a97cf3a2871d56b79a6614cb0c96..559576499046319353ad0a1a63a11fa0a1cfde66 100644
--- a/qa/spec/page/validator_spec.rb
+++ b/qa/spec/page/validator_spec.rb
@@ -30,7 +30,7 @@ describe QA::Page::Validator do
     let(:view) { spy('view') }
 
     before do
-      allow(QA::Page::Admin::Settings)
+      allow(QA::Page::Admin::Settings::Main)
         .to receive(:views).and_return([view])
     end
 
diff --git a/scripts/codequality b/scripts/codequality
deleted file mode 100755
index 2f3ccef7d2d21399b93a5392b94683d1b3afa6a7..0000000000000000000000000000000000000000
--- a/scripts/codequality
+++ /dev/null
@@ -1,19 +0,0 @@
-#!/bin/sh
-
-set -eo pipefail
-
-code_path=$(pwd)
-
-# docker run --tty will merge stderr and stdout, we don't need this on CI or
-# it will break codequality json file
-[ "$CI" != "" ] || docker_tty="--tty"
-
-# The codebase and instructions for the following image can be found at https://gitlab.com/gitlab-org/codeclimate-rubocop/wikis/home
-docker pull dev.gitlab.org:5005/gitlab/gitlab-build-images:gitlab-codeclimate-rubocop-0-52-1 > /dev/null
-docker tag dev.gitlab.org:5005/gitlab/gitlab-build-images:gitlab-codeclimate-rubocop-0-52-1 codeclimate/codeclimate-rubocop:gitlab-codeclimate-rubocop-0-52-1 > /dev/null
-
-exec docker run --rm $docker_tty --env CODECLIMATE_CODE="$code_path" \
-	--volume "$code_path":/code \
-	--volume /var/run/docker.sock:/var/run/docker.sock \
-	--volume /tmp/cc:/tmp/cc \
-	"codeclimate/codeclimate:${CODECLIMATE_VERSION:-0.71.1}" "$@"
diff --git a/scripts/trigger-build-omnibus b/scripts/trigger-build-omnibus
index 85ea4aa74ac437b9ee74160fed52901a3b3966ba..95f35b44f5af319ea7f261db5c0d2776d4977949 100755
--- a/scripts/trigger-build-omnibus
+++ b/scripts/trigger-build-omnibus
@@ -9,6 +9,7 @@ module Omnibus
 
   class Trigger
     TOKEN = ENV['BUILD_TRIGGER_TOKEN']
+    TRIGGERER = ENV['CI_PROJECT_NAME']
 
     def initialize
       @uri = URI("https://gitlab.com/api/v4/projects/#{CGI.escape(Omnibus::PROJECT_PATH)}/trigger/pipeline")
@@ -32,7 +33,7 @@ module Omnibus
     private
 
     def ee?
-      File.exist?('CHANGELOG-EE.md')
+      TRIGGERER == 'gitlab-ee' || File.exist?('CHANGELOG-EE.md')
     end
 
     def env_params
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 03cbbb21e6240731d2050263cfcec0340f8de6cd..891485406c651bd59f8b729e4e89b3decaec024a 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -84,6 +84,13 @@ describe ProfilesController, :request_store do
       expect(user.username).to eq(new_username)
     end
 
+    it 'raises a correct error when the username is missing' do
+      sign_in(user)
+
+      expect { put :update_username, user: { gandalf: 'you shall not pass' } }
+        .to raise_error(ActionController::ParameterMissing)
+    end
+
     context 'with legacy storage' do
       it 'moves dependent projects to new namespace' do
         project = create(:project_empty_repo, :legacy_storage, namespace: namespace)
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 3b9e06cb5ad5605a655affcbf0a27d8b35f88a78..16fb377b002223a422137d1834882febdd2fd4df 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -398,6 +398,22 @@ describe Projects::BranchesController do
       end
     end
 
+    # We need :request_store because Gitaly only counts the queries whenever
+    # `RequestStore.active?` in GitalyClient.enforce_gitaly_request_limits
+    # And the main goal of this test is making sure TooManyInvocationsError
+    # was not raised whenever the cache is enabled yet cold.
+    context 'when cache is enabled yet cold', :request_store do
+      it 'return with a status 200' do
+        get :index,
+            namespace_id: project.namespace,
+            project_id: project,
+            state: 'all',
+            format: :html
+
+        expect(response).to have_gitlab_http_status(200)
+      end
+    end
+
     context 'when branch contains an invalid UTF-8 sequence' do
       before do
         project.repository.create_branch("wrong-\xE5-utf8-sequence")
@@ -414,7 +430,7 @@ describe Projects::BranchesController do
       end
     end
 
-    context 'when depreated sort/search/page parameters are specified' do
+    context 'when deprecated sort/search/page parameters are specified' do
       it 'returns with a status 301 when sort specified' do
         get :index,
             namespace_id: project.namespace,
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 9918d52e4028a036eef3ee6343e5c41d396e75eb..01b5506b64bba917e47cd1dcb92b2c23feafe86e 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -974,7 +974,7 @@ describe Projects::IssuesController do
       it 'returns discussion json' do
         get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
 
-        expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes diff_discussion individual_note resolvable resolve_with_issue_path resolved])
+        expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes diff_discussion individual_note resolvable resolved])
       end
 
       context 'with cross-reference system note', :request_store do
diff --git a/spec/factories/ci/build_metadata.rb b/spec/factories/ci/build_metadata.rb
deleted file mode 100644
index 66bbd977b88f6de50127af9e050cac531f1e5866..0000000000000000000000000000000000000000
--- a/spec/factories/ci/build_metadata.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-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 3005d74c3cf4f389eeea8b7ab6749514e6825a94..846b8040be6abc18cff7eb13bdb5842ee51b062c 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -123,7 +123,7 @@ feature 'Admin updates settings' do
   scenario 'Change Performance bar settings' do
     group = create(:group)
 
-    page.within('.as-performance') do
+    page.within('.as-performance-bar') do
       check 'Enable the Performance Bar'
       fill_in 'Allowed group', with: group.path
       click_on 'Save changes'
@@ -133,7 +133,7 @@ feature 'Admin updates settings' do
     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
+    page.within('.as-performance-bar') do
       uncheck 'Enable the Performance Bar'
       click_on 'Save changes'
     end
@@ -167,6 +167,26 @@ feature 'Admin updates settings' do
     expect(Gitlab::CurrentSettings.unique_ips_limit_per_user).to eq(15)
   end
 
+  scenario 'Configure web terminal' do
+    page.within('.as-terminal') do
+      fill_in 'Max session time', with: 15
+      click_button 'Save changes'
+    end
+
+    expect(page).to have_content "Application settings saved successfully"
+    expect(Gitlab::CurrentSettings.terminal_max_session_time).to eq(15)
+  end
+
+  scenario 'Enable outbound requests' do
+    page.within('.as-outbound') do
+      check 'Allow requests to the local network from hooks and services'
+      click_button 'Save changes'
+    end
+
+    expect(page).to have_content "Application settings saved successfully"
+    expect(Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services).to be true
+  end
+
   scenario 'Change Slack Notifications Service template settings' do
     first(:link, 'Service Templates').click
     click_link 'Slack notifications'
diff --git a/spec/features/dashboard/issues_filter_spec.rb b/spec/features/dashboard/issues_filter_spec.rb
index 8759950e013e976dbe94df41d594936a8da652fa..029fc45c7919f0e175692929a07c9cc2c0b5d859 100644
--- a/spec/features/dashboard/issues_filter_spec.rb
+++ b/spec/features/dashboard/issues_filter_spec.rb
@@ -1,7 +1,7 @@
 require 'spec_helper'
 
 feature 'Dashboard Issues filtering', :js do
-  include SortingHelper
+  include Spec::Support::Helpers::Features::SortingHelpers
 
   let(:user)      { create(:user) }
   let(:project)   { create(:project) }
@@ -90,14 +90,14 @@ feature 'Dashboard Issues filtering', :js do
 
   context 'sorting' do
     it 'shows sorted issues' do
-      sorting_by('Created date')
+      sort_by('Created date')
       visit_issues
 
       expect(find('.issues-filters')).to have_content('Created date')
     end
 
     it 'keeps sorting issues after visiting Projects Issues page' do
-      sorting_by('Created date')
+      sort_by('Created date')
       visit project_issues_path(project)
 
       expect(find('.issues-filters')).to have_content('Created date')
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index c8f3a8449f5fa5166703d327853056bdd7e6e2dc..4a9344115d211bb3fa85a1164d222f69183fef33 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -1,8 +1,8 @@
 require 'spec_helper'
 
 feature 'Dashboard Merge Requests' do
+  include Spec::Support::Helpers::Features::SortingHelpers
   include FilterItemSelectHelper
-  include SortingHelper
   include ProjectForksHelper
 
   let(:current_user) { create :user }
@@ -115,7 +115,7 @@ feature 'Dashboard Merge Requests' do
     end
 
     it 'shows sorted merge requests' do
-      sorting_by('Created date')
+      sort_by('Created date')
 
       visit merge_requests_dashboard_path(assignee_id: current_user.id)
 
@@ -123,7 +123,7 @@ feature 'Dashboard Merge Requests' do
     end
 
     it 'keeps sorting merge requests after visiting Projects MR page' do
-      sorting_by('Created date')
+      sort_by('Created date')
 
       visit project_merge_requests_path(project)
 
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index b3c509648109020e3ce3192bbb892fabebd4e365..08ba91a2682786840a5653c9d04002e5b29929ea 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -22,15 +22,6 @@ describe 'Filter issues', :js do
     end
   end
 
-  def expect_issues_list_count(open_count, closed_count = 0)
-    all_count = open_count + closed_count
-
-    expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count)
-    page.within '.issues-list' do
-      expect(page).to have_selector('.issue', count: open_count)
-    end
-  end
-
   before do
     project.add_master(user)
 
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index ea7a97d02a05a6cc2a97975e03d244ea583bc224..ff2a0e1571934ec677bbeed55347033edf3f375f 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -1,7 +1,7 @@
 require 'rails_helper'
 
 feature 'Issues > User uses quick actions', :js do
-  include QuickActionsHelpers
+  include Spec::Support::Helpers::Features::NotesHelpers
 
   it_behaves_like 'issuable record that supports quick actions in its description and notes', :issue do
     let(:issuable) { create(:issue, project: project) }
@@ -36,7 +36,7 @@ feature 'Issues > User uses quick actions', :js do
 
       context 'when the current user can update the due date' do
         it 'does not create a note, and sets the due date accordingly' do
-          write_note("/due 2016-08-28")
+          add_note("/due 2016-08-28")
 
           expect(page).not_to have_content '/due 2016-08-28'
           expect(page).to have_content 'Commands applied'
@@ -57,7 +57,7 @@ feature 'Issues > User uses quick actions', :js do
         end
 
         it 'does not create a note, and sets the due date accordingly' do
-          write_note("/due 2016-08-28")
+          add_note("/due 2016-08-28")
 
           expect(page).not_to have_content 'Commands applied'
 
@@ -75,7 +75,7 @@ feature 'Issues > User uses quick actions', :js do
         it 'does not create a note, and removes the due date accordingly' do
           expect(issue.due_date).to eq Date.new(2016, 8, 28)
 
-          write_note("/remove_due_date")
+          add_note("/remove_due_date")
 
           expect(page).not_to have_content '/remove_due_date'
           expect(page).to have_content 'Commands applied'
@@ -96,7 +96,7 @@ feature 'Issues > User uses quick actions', :js do
         end
 
         it 'does not create a note, and sets the due date accordingly' do
-          write_note("/remove_due_date")
+          add_note("/remove_due_date")
 
           expect(page).not_to have_content 'Commands applied'
 
@@ -111,7 +111,7 @@ feature 'Issues > User uses quick actions', :js do
       let(:issue) { create(:issue, project: project) }
 
       it 'does not recognize the command nor create a note' do
-        write_note("/wip")
+        add_note("/wip")
 
         expect(page).not_to have_content '/wip'
       end
@@ -123,7 +123,7 @@ feature 'Issues > User uses quick actions', :js do
 
       context 'when the current user can update issues' do
         it 'does not create a note, and marks the issue as a duplicate' do
-          write_note("/duplicate ##{original_issue.to_reference}")
+          add_note("/duplicate ##{original_issue.to_reference}")
 
           expect(page).not_to have_content "/duplicate #{original_issue.to_reference}"
           expect(page).to have_content 'Commands applied'
@@ -143,7 +143,7 @@ feature 'Issues > User uses quick actions', :js do
         end
 
         it 'does not create a note, and does not mark the issue as a duplicate' do
-          write_note("/duplicate ##{original_issue.to_reference}")
+          add_note("/duplicate ##{original_issue.to_reference}")
 
           expect(page).not_to have_content 'Commands applied'
           expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
@@ -166,7 +166,7 @@ feature 'Issues > User uses quick actions', :js do
         end
 
         it 'moves the issue' do
-          write_note("/move #{target_project.full_path}")
+          add_note("/move #{target_project.full_path}")
 
           expect(page).to have_content 'Commands applied'
           expect(issue.reload).to be_closed
@@ -186,7 +186,7 @@ feature 'Issues > User uses quick actions', :js do
         end
 
         it 'does not move the issue' do
-          write_note("/move #{project_unauthorized.full_path}")
+          add_note("/move #{project_unauthorized.full_path}")
 
           expect(page).not_to have_content 'Commands applied'
           expect(issue.reload).to be_open
@@ -200,7 +200,7 @@ feature 'Issues > User uses quick actions', :js do
         end
 
         it 'does not move the issue' do
-          write_note("/move not/valid")
+          add_note("/move not/valid")
 
           expect(page).not_to have_content 'Commands applied'
           expect(issue.reload).to be_open
@@ -223,7 +223,7 @@ feature 'Issues > User uses quick actions', :js do
         end
 
         it 'applies the commands to both issues and moves the issue' do
-          write_note("/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"\n\n/move #{target_project.full_path}")
+          add_note("/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"\n\n/move #{target_project.full_path}")
 
           expect(page).to have_content 'Commands applied'
           expect(issue.reload).to be_closed
@@ -242,7 +242,7 @@ feature 'Issues > User uses quick actions', :js do
         end
 
         it 'moves the issue and applies the commands to both issues' do
-          write_note("/move #{target_project.full_path}\n\n/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"")
+          add_note("/move #{target_project.full_path}\n\n/label ~#{bug.title} ~#{wontfix.title}\n\n/milestone %\"#{milestone.title}\"")
 
           expect(page).to have_content 'Commands applied'
           expect(issue.reload).to be_closed
diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..99e1fb30d5bb2284352fbccc1f13fdab1d30d033
--- /dev/null
+++ b/spec/features/labels_hierarchy_spec.rb
@@ -0,0 +1,305 @@
+require 'spec_helper'
+
+feature 'Labels Hierarchy', :js, :nested_groups do
+  include FilteredSearchHelpers
+
+  let!(:user) { create(:user) }
+  let!(:grandparent) { create(:group) }
+  let!(:parent) { create(:group, parent: grandparent) }
+  let!(:child) { create(:group, parent: parent) }
+  let!(:project_1) { create(:project, namespace: parent) }
+
+  let!(:grandparent_group_label) { create(:group_label, group: grandparent, title: 'Label_1') }
+  let!(:parent_group_label) { create(:group_label, group: parent, title: 'Label_2') }
+  let!(:child_group_label) { create(:group_label, group: child, title: 'Label_3') }
+  let!(:project_label_1) { create(:label, project: project_1, title: 'Label_4') }
+
+  before do
+    grandparent.add_owner(user)
+
+    sign_in(user)
+  end
+
+  shared_examples 'assigning labels from sidebar' do
+    it 'can assign all ancestors labels' do
+      [grandparent_group_label, parent_group_label, project_label_1].each do |label|
+        page.within('.block.labels') do
+          find('.edit-link').click
+        end
+
+        wait_for_requests
+
+        find('a.label-item', text: label.title).click
+        find('.dropdown-menu-close-icon').click
+
+        wait_for_requests
+
+        expect(page).to have_selector('span.label', text: label.title)
+      end
+    end
+
+    it 'does not find child group labels on dropdown' do
+      page.within('.block.labels') do
+        find('.edit-link').click
+      end
+
+      wait_for_requests
+
+      expect(page).not_to have_selector('span.label', text: child_group_label.title)
+    end
+  end
+
+  shared_examples 'filtering by ancestor labels for projects' do |board = false|
+    it 'filters by ancestor labels' do
+      [grandparent_group_label, parent_group_label, project_label_1].each do |label|
+        select_label_on_dropdown(label.title)
+
+        wait_for_requests
+
+        if board
+          expect(page).to have_selector('.card-title') do |card|
+            expect(card).to have_selector('a', text: labeled_issue.title)
+          end
+        else
+          expect_issues_list_count(1)
+          expect(page).to have_selector('span.issue-title-text', text: labeled_issue.title)
+        end
+      end
+    end
+
+    it 'does not filter by descendant group labels' do
+      filtered_search.set("label:")
+
+      wait_for_requests
+
+      expect(page).not_to have_selector('.btn-link', text: child_group_label.title)
+    end
+  end
+
+  shared_examples 'filtering by ancestor labels for groups' do |board = false|
+    let(:project_2) { create(:project, namespace: parent) }
+    let!(:project_label_2) { create(:label, project: project_2, title: 'Label_4') }
+
+    let(:project_3) { create(:project, namespace: child) }
+    let!(:group_label_3) { create(:group_label, group: child, title: 'Label_5') }
+    let!(:project_label_3) { create(:label, project: project_3, title: 'Label_6') }
+
+    let!(:labeled_issue_2) { create(:labeled_issue, project: project_2, labels: [grandparent_group_label, parent_group_label, project_label_2]) }
+    let!(:labeled_issue_3) { create(:labeled_issue, project: project_3, labels: [grandparent_group_label, parent_group_label, group_label_3]) }
+
+    let!(:issue_2) { create(:issue, project: project_2) }
+
+    it 'filters by ancestors and current group labels' do
+      [grandparent_group_label, parent_group_label].each do |label|
+        select_label_on_dropdown(label.title)
+
+        wait_for_requests
+
+        if board
+          expect(page).to have_selector('.card-title') do |card|
+            expect(card).to have_selector('a', text: labeled_issue.title)
+          end
+
+          expect(page).to have_selector('.card-title') do |card|
+            expect(card).to have_selector('a', text: labeled_issue_2.title)
+          end
+        else
+          expect_issues_list_count(3)
+          expect(page).to have_selector('span.issue-title-text', text: labeled_issue.title)
+          expect(page).to have_selector('span.issue-title-text', text: labeled_issue_2.title)
+          expect(page).to have_selector('span.issue-title-text', text: labeled_issue_3.title)
+        end
+      end
+    end
+
+    it 'filters by descendant group labels' do
+      wait_for_requests
+
+      if board
+        pending("Waiting for https://gitlab.com/gitlab-org/gitlab-ce/issues/44270")
+
+        select_label_on_dropdown(group_label_3.title)
+
+        expect(page).to have_selector('.card-title') do |card|
+          expect(card).to have_selector('a', text: labeled_issue_3.title)
+        end
+      else
+        select_label_on_dropdown(group_label_3.title)
+
+        expect_issues_list_count(1)
+        expect(page).to have_selector('span.issue-title-text', text: labeled_issue_3.title)
+      end
+    end
+
+    it 'does not filter by descendant group project labels' do
+      filtered_search.set("label:")
+
+      wait_for_requests
+
+      expect(page).not_to have_selector('.btn-link', text: project_label_3.title)
+    end
+  end
+
+  context 'when creating new issuable' do
+    before do
+      visit new_project_issue_path(project_1)
+    end
+
+    it 'should be able to assign ancestor group labels' do
+      fill_in 'issue_title', with: 'new created issue'
+      fill_in 'issue_description', with: 'new issue description'
+
+      find(".js-label-select").click
+      wait_for_requests
+
+      find('a.label-item', text: grandparent_group_label.title).click
+      find('a.label-item', text: parent_group_label.title).click
+      find('a.label-item', text: project_label_1.title).click
+
+      find('.btn-create').click
+
+      expect(page.find('.issue-details h2.title')).to have_content('new created issue')
+      expect(page).to have_selector('span.label', text: grandparent_group_label.title)
+      expect(page).to have_selector('span.label', text: parent_group_label.title)
+      expect(page).to have_selector('span.label', text: project_label_1.title)
+    end
+  end
+
+  context 'issuable sidebar' do
+    let!(:issue) { create(:issue, project: project_1) }
+
+    context 'on issue sidebar' do
+      before do
+        visit project_issue_path(project_1, issue)
+      end
+
+      it_behaves_like 'assigning labels from sidebar'
+    end
+
+    context 'on project board issue sidebar' do
+      let(:board)   { create(:board, project: project_1) }
+
+      before do
+        visit project_board_path(project_1, board)
+
+        wait_for_requests
+
+        find('.card').click
+      end
+
+      it_behaves_like 'assigning labels from sidebar'
+    end
+
+    context 'on group board issue sidebar' do
+      let(:board)   { create(:board, group: parent) }
+
+      before do
+        visit group_board_path(parent, board)
+
+        wait_for_requests
+
+        find('.card').click
+      end
+
+      it_behaves_like 'assigning labels from sidebar'
+    end
+  end
+
+  context 'issuable filtering' do
+    let!(:labeled_issue) { create(:labeled_issue, project: project_1, labels: [grandparent_group_label, parent_group_label, project_label_1]) }
+    let!(:issue) { create(:issue, project: project_1) }
+
+    context 'on project issuable list' do
+      before do
+        visit project_issues_path(project_1)
+      end
+
+      it_behaves_like 'filtering by ancestor labels for projects'
+
+      it 'does not filter by descendant group labels' do
+        filtered_search.set("label:")
+
+        wait_for_requests
+
+        expect(page).not_to have_selector('.btn-link', text: child_group_label.title)
+      end
+    end
+
+    context 'on group issuable list' do
+      before do
+        visit issues_group_path(parent)
+      end
+
+      it_behaves_like 'filtering by ancestor labels for groups'
+    end
+
+    context 'on project boards filter' do
+      let(:board) { create(:board, project: project_1) }
+
+      before do
+        visit project_board_path(project_1, board)
+      end
+
+      it_behaves_like 'filtering by ancestor labels for projects', true
+    end
+
+    context 'on group boards filter' do
+      let(:board) { create(:board, group: parent) }
+
+      before do
+        visit group_board_path(parent, board)
+      end
+
+      it_behaves_like 'filtering by ancestor labels for groups', true
+    end
+  end
+
+  context 'creating boards lists' do
+    context 'on project boards' do
+      let(:board) { create(:board, project: project_1) }
+
+      before do
+        visit project_board_path(project_1, board)
+        find('.js-new-board-list').click
+        wait_for_requests
+      end
+
+      it 'creates lists from all ancestor labels' do
+        [grandparent_group_label, parent_group_label, project_label_1].each do |label|
+          find('a', text: label.title).click
+        end
+
+        wait_for_requests
+
+        expect(page).to have_selector('.board-title-text', text: grandparent_group_label.title)
+        expect(page).to have_selector('.board-title-text', text: parent_group_label.title)
+        expect(page).to have_selector('.board-title-text', text: project_label_1.title)
+      end
+    end
+
+    context 'on group boards' do
+      let(:board) { create(:board, group: parent) }
+
+      before do
+        visit group_board_path(parent, board)
+        find('.js-new-board-list').click
+        wait_for_requests
+      end
+
+      it 'creates lists from all ancestor group labels' do
+        [grandparent_group_label, parent_group_label].each do |label|
+          find('a', text: label.title).click
+        end
+
+        wait_for_requests
+
+        expect(page).to have_selector('.board-title-text', text: grandparent_group_label.title)
+        expect(page).to have_selector('.board-title-text', text: parent_group_label.title)
+      end
+
+      it 'does not create lists from descendant groups' do
+        expect(page).not_to have_selector('a', text: child_group_label.title)
+      end
+    end
+  end
+end
diff --git a/spec/features/merge_request/user_uses_slash_commands_spec.rb b/spec/features/merge_request/user_uses_slash_commands_spec.rb
index bd739e69d6c0a6675f4844a4f2d85a2ecd6ffd85..7f261b580f73e87c359bd4e466499ab584054bb1 100644
--- a/spec/features/merge_request/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_request/user_uses_slash_commands_spec.rb
@@ -1,7 +1,7 @@
 require 'rails_helper'
 
 describe 'Merge request > User uses quick actions', :js do
-  include QuickActionsHelpers
+  include Spec::Support::Helpers::Features::NotesHelpers
 
   let(:project) { create(:project, :public, :repository) }
   let(:user) { project.creator }
@@ -33,7 +33,7 @@ describe 'Merge request > User uses quick actions', :js do
     describe 'toggling the WIP prefix in the title from note' do
       context 'when the current user can toggle the WIP prefix' do
         it 'adds the WIP: prefix to the title' do
-          write_note("/wip")
+          add_note("/wip")
 
           expect(page).not_to have_content '/wip'
           expect(page).to have_content 'Commands applied'
@@ -44,7 +44,7 @@ describe 'Merge request > User uses quick actions', :js do
         it 'removes the WIP: prefix from the title' do
           merge_request.title = merge_request.wip_title
           merge_request.save
-          write_note("/wip")
+          add_note("/wip")
 
           expect(page).not_to have_content '/wip'
           expect(page).to have_content 'Commands applied'
@@ -62,7 +62,7 @@ describe 'Merge request > User uses quick actions', :js do
         end
 
         it 'does not change the WIP prefix' do
-          write_note("/wip")
+          add_note("/wip")
 
           expect(page).not_to have_content '/wip'
           expect(page).not_to have_content 'Commands applied'
@@ -75,7 +75,7 @@ describe 'Merge request > User uses quick actions', :js do
     describe 'merging the MR from the note' do
       context 'when the current user can merge the MR' do
         it 'merges the MR' do
-          write_note("/merge")
+          add_note("/merge")
 
           expect(page).to have_content 'Commands applied'
 
@@ -90,7 +90,7 @@ describe 'Merge request > User uses quick actions', :js do
         end
 
         it 'does not merge the MR' do
-          write_note("/merge")
+          add_note("/merge")
 
           expect(page).not_to have_content 'Your commands have been executed!'
 
@@ -107,7 +107,7 @@ describe 'Merge request > User uses quick actions', :js do
         end
 
         it 'does not merge the MR' do
-          write_note("/merge")
+          add_note("/merge")
 
           expect(page).not_to have_content 'Your commands have been executed!'
 
@@ -118,7 +118,7 @@ describe 'Merge request > User uses quick actions', :js do
 
     describe 'adding a due date from note' do
       it 'does not recognize the command nor create a note' do
-        write_note('/due 2016-08-28')
+        add_note('/due 2016-08-28')
 
         expect(page).not_to have_content '/due 2016-08-28'
       end
@@ -162,7 +162,7 @@ describe 'Merge request > User uses quick actions', :js do
     describe '/target_branch command from note' do
       context 'when the current user can change target branch' do
         it 'changes target branch from a note' do
-          write_note("message start \n/target_branch merge-test\n message end.")
+          add_note("message start \n/target_branch merge-test\n message end.")
 
           wait_for_requests
           expect(page).not_to have_content('/target_branch')
@@ -173,7 +173,7 @@ describe 'Merge request > User uses quick actions', :js do
         end
 
         it 'does not fail when target branch does not exists' do
-          write_note('/target_branch totally_not_existing_branch')
+          add_note('/target_branch totally_not_existing_branch')
 
           expect(page).not_to have_content('/target_branch')
 
@@ -190,7 +190,7 @@ describe 'Merge request > User uses quick actions', :js do
         end
 
         it 'does not change target branch' do
-          write_note('/target_branch merge-test')
+          add_note('/target_branch merge-test')
 
           expect(page).not_to have_content '/target_branch merge-test'
 
diff --git a/spec/features/projects/issues/user_comments_on_issue_spec.rb b/spec/features/projects/issues/user_comments_on_issue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c45fdc7642fe9779bce0e79b7e15830f5377557b
--- /dev/null
+++ b/spec/features/projects/issues/user_comments_on_issue_spec.rb
@@ -0,0 +1,73 @@
+require "spec_helper"
+
+describe "User comments on issue", :js do
+  include Spec::Support::Helpers::Features::NotesHelpers
+
+  let(:project) { create(:project_empty_repo, :public) }
+  let(:issue) { create(:issue, project: project) }
+  let(:user) { create(:user) }
+
+  before do
+    project.add_guest(user)
+    sign_in(user)
+
+    visit(project_issue_path(project, issue))
+  end
+
+  context "when adding comments" do
+    it "adds comment" do
+      content = "XML attached"
+      target_form = ".js-main-target-form"
+
+      add_note(content)
+
+      page.within(".note") do
+        expect(page).to have_content(content)
+      end
+
+      page.within(target_form) do
+        find(".error-alert", visible: false)
+      end
+    end
+
+    it "adds comment with code block" do
+      comment = "```\nCommand [1]: /usr/local/bin/git , see [text](doc/text)\n```"
+
+      add_note(comment)
+
+      expect(page).to have_content(comment)
+    end
+  end
+
+  context "when editing comments" do
+    it "edits comment" do
+      add_note("# Comment with a header")
+
+      page.within(".note-body > .note-text") do
+        expect(page).to have_content("Comment with a header").and have_no_css("#comment-with-a-header")
+      end
+
+      page.within(".main-notes-list") do
+        note = find(".note")
+
+        note.hover
+        note.find(".js-note-edit").click
+      end
+
+      expect(page).to have_css(".current-note-edit-form textarea")
+
+      comment = "+1 Awesome!"
+
+      page.within(".current-note-edit-form") do
+        fill_in("note[note]", with: comment)
+        click_button("Save comment")
+      end
+
+      wait_for_requests
+
+      page.within(".note") do
+        expect(page).to have_content(comment)
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/issues/user_creates_issue_spec.rb b/spec/features/projects/issues/user_creates_issue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e76f7c5589d84c7d35df90cb806201c17ffb746a
--- /dev/null
+++ b/spec/features/projects/issues/user_creates_issue_spec.rb
@@ -0,0 +1,87 @@
+require "spec_helper"
+
+describe "User creates issue" do
+  let(:project) { create(:project_empty_repo, :public) }
+  let(:user) { create(:user) }
+
+  context "when signed in as guest" do
+    before do
+      project.add_guest(user)
+      sign_in(user)
+
+      visit(new_project_issue_path(project))
+    end
+
+    it "creates issue" do
+      page.within(".issue-form") do
+        expect(page).to have_no_content("Assign to")
+        .and have_no_content("Labels")
+        .and have_no_content("Milestone")
+      end
+
+      issue_title = "500 error on profile"
+
+      fill_in("Title", with: issue_title)
+      click_button("Submit issue")
+
+      expect(page).to have_content(issue_title)
+        .and have_content(user.name)
+        .and have_content(project.name)
+    end
+  end
+
+  context "when signed in as developer", :js do
+    before do
+      project.add_developer(user)
+      sign_in(user)
+
+      visit(new_project_issue_path(project))
+    end
+
+    context "when previewing" do
+      it "previews content" do
+        form = first(".gfm-form")
+        textarea = first(".gfm-form textarea")
+
+        page.within(form) do
+          click_link("Preview")
+
+          preview = find(".js-md-preview") # this element is findable only when the "Preview" link is clicked.
+
+          expect(preview).to have_content("Nothing to preview.")
+
+          click_link("Write")
+          fill_in("Description", with: "Bug fixed :smile:")
+          click_link("Preview")
+
+          expect(preview).to have_css("gl-emoji")
+          expect(textarea).not_to be_visible
+        end
+      end
+    end
+
+    context "with labels" do
+      LABEL_TITLES = %w(bug feature enhancement).freeze
+
+      before do
+        LABEL_TITLES.each do |title|
+          create(:label, project: project, title: title)
+        end
+      end
+
+      it "creates issue" do
+        issue_title = "500 error on profile"
+
+        fill_in("Title", with: issue_title)
+        click_button("Label")
+        click_link(LABEL_TITLES.first)
+        click_button("Submit issue")
+
+        expect(page).to have_content(issue_title)
+          .and have_content(user.name)
+          .and have_content(project.name)
+          .and have_content(LABEL_TITLES.first)
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/issues/user_edits_issue_spec.rb b/spec/features/projects/issues/user_edits_issue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1d9c3abc20feb92bb5e710cc74083643e0677d0b
--- /dev/null
+++ b/spec/features/projects/issues/user_edits_issue_spec.rb
@@ -0,0 +1,25 @@
+require "spec_helper"
+
+describe "User edits issue", :js do
+  set(:project) { create(:project_empty_repo, :public) }
+  set(:user) { create(:user) }
+  set(:issue) { create(:issue, project: project, author: user) }
+
+  before do
+    project.add_developer(user)
+    sign_in(user)
+
+    visit(edit_project_issue_path(project, issue))
+  end
+
+  it "previews content" do
+    form = first(".gfm-form")
+
+    page.within(form) do
+      fill_in("Description", with: "Bug fixed :smile:")
+      click_link("Preview")
+    end
+
+    expect(form).to have_link("Write")
+  end
+end
diff --git a/spec/features/projects/issues/user_sorts_issues_spec.rb b/spec/features/projects/issues/user_sorts_issues_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c3d63000dac3edc39ed96aba08c4cbf532d2aaa0
--- /dev/null
+++ b/spec/features/projects/issues/user_sorts_issues_spec.rb
@@ -0,0 +1,39 @@
+require "spec_helper"
+
+describe "User sorts issues" do
+  set(:project) { create(:project_empty_repo, :public) }
+  set(:issue1) { create(:issue, project: project) }
+  set(:issue2) { create(:issue, project: project) }
+  set(:issue3) { create(:issue, project: project) }
+
+  before do
+    create_list(:award_emoji, 2, :upvote, awardable: issue1)
+    create_list(:award_emoji, 2, :downvote, awardable: issue2)
+    create(:award_emoji, :downvote, awardable: issue1)
+    create(:award_emoji, :upvote, awardable: issue2)
+
+    visit(project_issues_path(project))
+  end
+
+  it "sorts by popularity" do
+    find("button.dropdown-toggle").click
+
+    page.within(".content ul.dropdown-menu.dropdown-menu-align-right li") do
+      click_link("Popularity")
+    end
+
+    page.within(".issues-list") do
+      page.within("li.issue:nth-child(1)") do
+        expect(page).to have_content(issue1.title)
+      end
+
+      page.within("li.issue:nth-child(2)") do
+        expect(page).to have_content(issue2.title)
+      end
+
+      page.within("li.issue:nth-child(3)") do
+        expect(page).to have_content(issue3.title)
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/issues/user_toggles_subscription_spec.rb b/spec/features/projects/issues/user_toggles_subscription_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..117a614b98063481df6731d9fe870cf837b85f14
--- /dev/null
+++ b/spec/features/projects/issues/user_toggles_subscription_spec.rb
@@ -0,0 +1,28 @@
+require "spec_helper"
+
+describe "User toggles subscription", :js do
+  set(:project) { create(:project_empty_repo, :public) }
+  set(:user) { create(:user) }
+  set(:issue) { create(:issue, project: project, author: user) }
+
+  before do
+    project.add_developer(user)
+    sign_in(user)
+
+    visit(project_issue_path(project, issue))
+  end
+
+  it "unsibscribes from issue" do
+    subscription_button = find(".js-issuable-subscribe-button")
+
+    # Check we're subscribed.
+    expect(subscription_button).to have_css("button.is-checked")
+
+    # Toggle subscription.
+    find(".js-issuable-subscribe-button button").click
+    wait_for_requests
+
+    # Check we're unsubscribed.
+    expect(subscription_button).to have_css("button:not(.is-checked)")
+  end
+end
diff --git a/spec/features/projects/issues/user_views_issue_spec.rb b/spec/features/projects/issues/user_views_issue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f7f2cde3d64795de44ae800c261e4ac099d8a326
--- /dev/null
+++ b/spec/features/projects/issues/user_views_issue_spec.rb
@@ -0,0 +1,16 @@
+require "spec_helper"
+
+describe "User views issue" do
+  set(:project) { create(:project_empty_repo, :public) }
+  set(:user) { create(:user) }
+  set(:issue) { create(:issue, project: project, description: "# Description header", author: user) }
+
+  before do
+    project.add_guest(user)
+    sign_in(user)
+
+    visit(project_issue_path(project, issue))
+  end
+
+  it { expect(page).to have_header_with_correct_id_and_link(1, "Description header", "description-header") }
+end
diff --git a/spec/features/projects/issues/user_views_issues_spec.rb b/spec/features/projects/issues/user_views_issues_spec.rb
index d35009b89749762cc5e6bdce86872dacd712c04b..58afb4efb861f5057b4df655305a27c9a0bf6953 100644
--- a/spec/features/projects/issues/user_views_issues_spec.rb
+++ b/spec/features/projects/issues/user_views_issues_spec.rb
@@ -1,56 +1,116 @@
-require 'spec_helper'
+require "spec_helper"
 
-describe 'User views issues' do
+describe "User views issues" do
+  let!(:closed_issue) { create(:closed_issue, project: project) }
+  let!(:open_issue1) { create(:issue, project: project) }
+  let!(:open_issue2) { create(:issue, project: project) }
   set(:user) { create(:user) }
 
-  shared_examples_for 'shows issues' do
-    it 'shows issues' do
-      expect(page).to have_content(project.name)
-        .and have_content(issue1.title)
-        .and have_content(issue2.title)
-        .and have_no_selector('.js-new-board-list')
+  shared_examples "opens issue from list" do
+    it "opens issue" do
+      click_link(issue.title)
+
+      expect(page).to have_content(issue.title)
     end
   end
 
-  context 'when project is public' do
-    set(:project) { create(:project_empty_repo, :public) }
-    set(:issue1) { create(:issue, project: project) }
-    set(:issue2) { create(:issue, project: project) }
+  shared_examples "open issues" do
+    context "open issues" do
+      let(:label) { create(:label, project: project, title: "bug") }
 
-    context 'when signed in' do
       before do
-        project.add_developer(user)
-        sign_in(user)
+        open_issue1.labels << label
+
+        visit(project_issues_path(project, state: :opened))
+      end
 
-        visit(project_issues_path(project))
+      it "shows open issues" do
+        expect(page).to have_content(project.name)
+          .and have_content(open_issue1.title)
+          .and have_content(open_issue2.title)
+          .and have_no_content(closed_issue.title)
+          .and have_no_selector(".js-new-board-list")
       end
 
-      include_examples 'shows issues'
+      it "opens issues by label" do
+        page.within(".issues-list") do
+          click_link(label.title)
+        end
+
+        expect(page).to have_content(open_issue1.title)
+          .and have_no_content(open_issue2.title)
+          .and have_no_content(closed_issue.title)
+      end
+
+      include_examples "opens issue from list" do
+        let(:issue) { open_issue1 }
+      end
     end
+  end
 
-    context 'when not signed in' do
+  shared_examples "closed issues" do
+    context "closed issues" do
       before do
-        visit(project_issues_path(project))
+        visit(project_issues_path(project, state: :closed))
+      end
+
+      it "shows closed issues" do
+        expect(page).to have_content(project.name)
+          .and have_content(closed_issue.title)
+          .and have_no_content(open_issue1.title)
+          .and have_no_content(open_issue2.title)
+          .and have_no_selector(".js-new-board-list")
       end
 
-      include_examples 'shows issues'
+      include_examples "opens issue from list" do
+        let(:issue) { closed_issue }
+      end
     end
   end
 
-  context 'when project is internal' do
-    set(:project) { create(:project_empty_repo, :internal) }
-    set(:issue1) { create(:issue, project: project) }
-    set(:issue2) { create(:issue, project: project) }
-
-    context 'when signed in' do
+  shared_examples "all issues" do
+    context "all issues" do
       before do
-        project.add_developer(user)
-        sign_in(user)
+        visit(project_issues_path(project, state: :all))
+      end
 
-        visit(project_issues_path(project))
+      it "shows all issues" do
+        expect(page).to have_content(project.name)
+          .and have_content(closed_issue.title)
+          .and have_content(open_issue1.title)
+          .and have_content(open_issue2.title)
+          .and have_no_selector(".js-new-board-list")
       end
 
-      include_examples 'shows issues'
+      include_examples "opens issue from list" do
+        let(:issue) { closed_issue }
+      end
+    end
+  end
+
+  %w[internal public].each do |visibility|
+    shared_examples "#{visibility} project" do
+      context "when project is #{visibility}" do
+        let(:project) { create(:project_empty_repo, :"#{visibility}") }
+
+        include_examples "open issues"
+        include_examples "closed issues"
+        include_examples "all issues"
+      end
     end
   end
+
+  context "when signed in as developer" do
+    before do
+      project.add_developer(user)
+      sign_in(user)
+    end
+
+    include_examples "public project"
+    include_examples "internal project"
+  end
+
+  context "when not signed in" do
+    include_examples "public project"
+  end
 end
diff --git a/spec/features/projects/labels/user_creates_labels_spec.rb b/spec/features/projects/labels/user_creates_labels_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9fd7f3ee77582337954254e56e22eed64efa8cb0
--- /dev/null
+++ b/spec/features/projects/labels/user_creates_labels_spec.rb
@@ -0,0 +1,88 @@
+require "spec_helper"
+
+describe "User creates labels" do
+  set(:project) { create(:project_empty_repo, :public) }
+  set(:user) { create(:user) }
+
+  shared_examples_for "label creation" do
+    it "creates new label" do
+      title = "bug"
+
+      create_label(title)
+
+      page.within(".other-labels .manage-labels-list") do
+        expect(page).to have_content(title)
+      end
+    end
+  end
+
+  context "in project" do
+    before do
+      project.add_master(user)
+      sign_in(user)
+
+      visit(new_project_label_path(project))
+    end
+
+    context "when data is valid" do
+      include_examples "label creation"
+    end
+
+    context "when data is invalid" do
+      context "when title is invalid" do
+        it "shows error message" do
+          create_label("")
+
+          page.within(".label-form") do
+            expect(page).to have_content("Title can't be blank")
+          end
+        end
+      end
+
+      context "when color is invalid" do
+        it "shows error message" do
+          create_label("feature", "#12")
+
+          page.within(".label-form") do
+            expect(page).to have_content("Color must be a valid color code")
+          end
+        end
+      end
+    end
+
+    context "when label already exists" do
+      let!(:label) { create(:label, project: project) }
+
+      it "shows error message" do
+        create_label(label.title)
+
+        page.within(".label-form") do
+          expect(page).to have_content("Title has already been taken")
+        end
+      end
+    end
+  end
+
+  context "in another project" do
+    set(:another_project) { create(:project_empty_repo, :public) }
+
+    before do
+      create(:label, project: project, title: "bug") # Create label for `project` (not `another_project`) project.
+
+      another_project.add_master(user)
+      sign_in(user)
+
+      visit(new_project_label_path(another_project))
+    end
+
+    include_examples "label creation"
+  end
+
+  private
+
+  def create_label(title, color = "#F95610")
+    fill_in("Title", with: title)
+    fill_in("Background color", with: color)
+    click_button("Create label")
+  end
+end
diff --git a/spec/features/projects/labels/user_edits_labels_spec.rb b/spec/features/projects/labels/user_edits_labels_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d1041ff5c1e05fd91d504f38b6e09cda7c7fdcde
--- /dev/null
+++ b/spec/features/projects/labels/user_edits_labels_spec.rb
@@ -0,0 +1,25 @@
+require "spec_helper"
+
+describe "User edits labels" do
+  set(:project) { create(:project_empty_repo, :public) }
+  set(:label) { create(:label, project: project) }
+  set(:user) { create(:user) }
+
+  before do
+    project.add_master(user)
+    sign_in(user)
+
+    visit(edit_project_label_path(project, label))
+  end
+
+  it "updates label's title" do
+    new_title = "fix"
+
+    fill_in("Title", with: new_title)
+    click_button("Save changes")
+
+    page.within(".other-labels .manage-labels-list") do
+      expect(page).to have_content(new_title).and have_no_content(label.title)
+    end
+  end
+end
diff --git a/spec/features/projects/labels/user_removes_labels_spec.rb b/spec/features/projects/labels/user_removes_labels_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f4fda6de465bd5ac598ae72b9f042620a1fc54da
--- /dev/null
+++ b/spec/features/projects/labels/user_removes_labels_spec.rb
@@ -0,0 +1,52 @@
+require "spec_helper"
+
+describe "User removes labels" do
+  let(:project) { create(:project_empty_repo, :public) }
+  let(:user) { create(:user) }
+
+  before do
+    project.add_master(user)
+    sign_in(user)
+  end
+
+  context "when one label" do
+    let!(:label) { create(:label, project: project) }
+
+    before do
+      visit(project_labels_path(project))
+    end
+
+    it "removes label" do
+      page.within(".labels") do
+        page.first(".label-list-item") do
+          first(".remove-row").click
+          first(:link, "Delete label").click
+        end
+      end
+
+      expect(page).to have_content("Label was removed").and have_no_content(label.title)
+    end
+  end
+
+  context "when many labels", :js do
+    before do
+      create_list(:label, 3, project: project)
+
+      visit(project_labels_path(project))
+    end
+
+    it "removes all labels" do
+      page.within(".labels") do
+        loop do
+          li = page.first(".label-list-item")
+          break unless li
+
+          li.click_link("Delete")
+          click_link("Delete label")
+        end
+
+        expect(page).to have_content("Generate a default set of labels").and have_content("New label")
+      end
+    end
+  end
+end
diff --git a/spec/features/projects/labels/user_views_labels_spec.rb b/spec/features/projects/labels/user_views_labels_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..0cbeca4e3929023229e6670ae8bc476ead5deb21
--- /dev/null
+++ b/spec/features/projects/labels/user_views_labels_spec.rb
@@ -0,0 +1,23 @@
+require "spec_helper"
+
+describe "User views labels" do
+  set(:project) { create(:project_empty_repo, :public) }
+  set(:user) { create(:user) }
+
+  LABEL_TITLES = %w[bug enhancement feature].freeze
+
+  before do
+    LABEL_TITLES.each { |title| create(:label, project: project, title: title) }
+
+    project.add_guest(user)
+    sign_in(user)
+
+    visit(project_labels_path(project))
+  end
+
+  it "shows all labels" do
+    page.within('.other-labels .manage-labels-list') do
+      LABEL_TITLES.each { |title| expect(page).to have_content(title) }
+    end
+  end
+end
diff --git a/spec/features/projects/milestones/milestones_sorting_spec.rb b/spec/features/projects/milestones/milestones_sorting_spec.rb
index c531b81e04db38221a6edb3aed693903e27c994d..b64786d4eecc4c8f6b22efacf8fecc0503eb5abc 100644
--- a/spec/features/projects/milestones/milestones_sorting_spec.rb
+++ b/spec/features/projects/milestones/milestones_sorting_spec.rb
@@ -1,7 +1,6 @@
 require 'spec_helper'
 
 feature 'Milestones sorting', :js do
-  include SortingHelper
   let(:user)    { create(:user) }
   let(:project) { create(:project, name: 'test', namespace: user.namespace) }
 
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index a40848182842c8f47906702542f9ac5080640234..43cabd3b9f23064ba33c44a35cd15dc07bd2483a 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -142,7 +142,10 @@ feature 'Protected Branches', :js do
         set_protected_branch_name('*-stable')
         click_on "Protect"
 
-        within(".protected-branches-list") { expect(page).to have_content("2 matching branches") }
+        within(".protected-branches-list") do
+          expect(page).to have_content("Protected branch (2)")
+          expect(page).to have_content("2 matching branches")
+        end
       end
 
       it "displays all the branches matching the wildcard" do
diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb
index 8cc6f17b8d9283de18e75a3c12ed673223dfc086..efccaeaff6c9cd84e97ef113aa11f76797a49ef0 100644
--- a/spec/features/protected_tags_spec.rb
+++ b/spec/features/protected_tags_spec.rb
@@ -65,7 +65,10 @@ feature 'Protected Tags', :js do
       set_protected_tag_name('*-stable')
       click_on "Protect"
 
-      within(".protected-tags-list") { expect(page).to have_content("2 matching tags") }
+      within(".protected-tags-list") do
+        expect(page).to have_content("Protected tag (2)")
+        expect(page).to have_content("2 matching tags")
+      end
     end
 
     it "displays all the tags matching the wildcard" do
diff --git a/spec/features/user_sorts_things_spec.rb b/spec/features/user_sorts_things_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..69ebdddaeecda38e07d6b035875f0ddf64dc88c9
--- /dev/null
+++ b/spec/features/user_sorts_things_spec.rb
@@ -0,0 +1,57 @@
+require "spec_helper"
+
+# The main goal of this spec is not to check whether the sorting UI works, but
+# to check if the sorting option set by user is being kept persisted while going through pages.
+# The `it`s are named here by convention `starting point -> some pages -> final point`.
+# All those specs are moved out to this spec intentionally to keep them all in one place.
+describe "User sorts things" do
+  include Spec::Support::Helpers::Features::SortingHelpers
+  include Helpers::DashboardHelper
+
+  set(:project) { create(:project_empty_repo, :public) }
+  set(:current_user) { create(:user) } # Using `current_user` instead of just `user` because of the hardoced call in `assigned_mrs_dashboard_path` which is used below.
+  set(:issue) { create(:issue, project: project, author: current_user) }
+  set(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: current_user) }
+
+  before do
+    project.add_developer(current_user)
+    sign_in(current_user)
+  end
+
+  it "issues -> project home page -> issues" do
+    sort_option = "Last updated"
+
+    visit(project_issues_path(project))
+
+    sort_by(sort_option)
+
+    visit(project_path(project))
+    visit(project_issues_path(project))
+
+    expect(find(".issues-filters")).to have_content(sort_option)
+  end
+
+  it "issues -> merge requests" do
+    sort_option = "Last updated"
+
+    visit(project_issues_path(project))
+
+    sort_by(sort_option)
+
+    visit(project_merge_requests_path(project))
+
+    expect(find(".issues-filters")).to have_content(sort_option)
+  end
+
+  it "merge requests -> dashboard merge requests" do
+    sort_option = "Last updated"
+
+    visit(project_merge_requests_path(project))
+
+    sort_by(sort_option)
+
+    visit(assigned_mrs_dashboard_path)
+
+    expect(find(".issues-filters")).to have_content(sort_option)
+  end
+end
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index d434c5011107d825d0cc2b388e2e1e965b8898ba..899d0d22819dc98a62cec1f58ffb94670ee372fa 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -71,6 +71,24 @@ describe LabelsFinder do
         end
       end
 
+      context 'when group has no projects' do
+        let(:empty_group) { create(:group) }
+        let!(:empty_group_label_1) { create(:group_label, group: empty_group, title: 'Label 1 (empty group)') }
+        let!(:empty_group_label_2) { create(:group_label, group: empty_group, title: 'Label 2 (empty group)') }
+
+        before do
+          empty_group.add_developer(user)
+        end
+
+        context 'when only group labels is false' do
+          it 'returns group labels' do
+            finder = described_class.new(user, group_id: empty_group.id)
+
+            expect(finder.execute).to eq [empty_group_label_1, empty_group_label_2]
+          end
+        end
+      end
+
       context 'when including labels from group ancestors', :nested_groups do
         it 'returns labels from group and its ancestors' do
           private_group_1.add_developer(user)
@@ -110,7 +128,21 @@ describe LabelsFinder do
       end
     end
 
-    context 'filtering by project_id' do
+    context 'filtering by project_id', :nested_groups do
+      context 'when include_ancestor_groups is true' do
+        let!(:sub_project) { create(:project, namespace: private_subgroup_1 ) }
+        let!(:project_label) { create(:label, project: sub_project, title: 'Label 5') }
+        let(:finder) { described_class.new(user, project_id: sub_project.id, include_ancestor_groups: true) }
+
+        before do
+          private_group_1.add_developer(user)
+        end
+
+        it 'returns all ancestor labels' do
+          expect(finder.execute).to match_array([private_subgroup_label_1, private_group_label_1, project_label])
+        end
+      end
+
       it 'returns labels available for the project' do
         finder = described_class.new(user, project_id: project_1.id)
 
diff --git a/spec/fixtures/exported-project.gz b/spec/fixtures/exported-project.gz
new file mode 100644
index 0000000000000000000000000000000000000000..352384f16c83737e4c4bdfb55b8d413e0aeb3208
Binary files /dev/null and b/spec/fixtures/exported-project.gz differ
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index ccac6e29447275291d4f058ecab2ea343f506163..ffdf6561a53bb54b6750d2c7f721ad32b3af3cf1 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -8,6 +8,7 @@ describe TreeHelper do
   describe '.render_tree' do
     before do
       @id = sha
+      @path = ""
       @project = project
       @lfs_blob_ids = []
     end
@@ -61,6 +62,15 @@ describe TreeHelper do
         end
       end
     end
+
+    context 'when the root path contains a plus character' do
+      let(:root_path) { 'gtk/C++' }
+      let(:tree_item) { double(flat_path: 'gtk/C++/glade') }
+
+      it 'returns the flattened path' do
+        expect(subject).to eq('glade')
+      end
+    end
   end
 
   describe '#commit_in_single_accessible_branch' do
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
index f1e0bf80819f3974d83385b594e44ec1dc590d69..cc7e0a3f26daa1a3784668054182842184ffb3ff 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -9,56 +9,59 @@ describe('Multi-file editor commit sidebar list item', () => {
   let vm;
   let f;
 
-  beforeEach(done => {
+  beforeEach(() => {
     const Component = Vue.extend(listItem);
 
     f = file('test-file');
 
+    store.state.entries[f.path] = f;
+
     vm = createComponentWithStore(Component, store, {
       file: f,
       actionComponent: 'stage-button',
-    });
-
-    vm.$mount();
-
-    Vue.nextTick(done);
+    }).$mount();
   });
 
   afterEach(() => {
     vm.$destroy();
 
-    resetStore(vm.$store);
+    resetStore(store);
   });
 
   it('renders file path', () => {
-    expect(
-      vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim(),
-    ).toBe(f.path);
+    expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path);
   });
 
   it('renders actionn button', () => {
     expect(vm.$el.querySelector('.multi-file-discard-btn')).not.toBeNull();
   });
 
-  it('opens a closed file in the editor when clicking the file path', () => {
-    spyOn(vm, 'openFileInEditor').and.callThrough();
-    spyOn(vm, 'updateViewer');
+  it('opens a closed file in the editor when clicking the file path', done => {
+    spyOn(vm, 'openPendingTab').and.callThrough();
     spyOn(router, 'push');
 
     vm.$el.querySelector('.multi-file-commit-list-path').click();
 
-    expect(vm.openFileInEditor).toHaveBeenCalled();
-    expect(router.push).toHaveBeenCalled();
+    setTimeout(() => {
+      expect(vm.openPendingTab).toHaveBeenCalled();
+      expect(router.push).toHaveBeenCalled();
+
+      done();
+    });
   });
 
-  it('calls updateViewer with diff when clicking file', () => {
+  it('calls updateViewer with diff when clicking file', done => {
     spyOn(vm, 'openFileInEditor').and.callThrough();
-    spyOn(vm, 'updateViewer');
+    spyOn(vm, 'updateViewer').and.callThrough();
     spyOn(router, 'push');
 
     vm.$el.querySelector('.multi-file-commit-list-path').click();
 
-    expect(vm.updateViewer).toHaveBeenCalledWith('diff');
+    setTimeout(() => {
+      expect(vm.updateViewer).toHaveBeenCalledWith('diff');
+
+      done();
+    });
   });
 
   describe('computed', () => {
diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js
index ddb5204e3a702e9735ccd42417fa5e8b4f986f25..8cabc6e89353fe21052ee30c9170105201090e7c 100644
--- a/spec/javascripts/ide/components/repo_tab_spec.js
+++ b/spec/javascripts/ide/components/repo_tab_spec.js
@@ -59,7 +59,7 @@ describe('RepoTab', () => {
 
     vm.$el.querySelector('.multi-file-tab-close').click();
 
-    expect(vm.closeFile).toHaveBeenCalledWith(vm.tab.path);
+    expect(vm.closeFile).toHaveBeenCalledWith(vm.tab);
   });
 
   it('changes icon on hover', done => {
diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js
index 73ea796048544ee1246643dea43f38d7fb71a0ef..cb785ba2cd3c4eeded0965435da48538cc102835 100644
--- a/spec/javascripts/ide/components/repo_tabs_spec.js
+++ b/spec/javascripts/ide/components/repo_tabs_spec.js
@@ -17,6 +17,7 @@ describe('RepoTabs', () => {
       files: openedFiles,
       viewer: 'editor',
       hasChanges: false,
+      activeFile: file('activeFile'),
       hasMergeRequest: false,
     });
     openedFiles[0].active = true;
@@ -57,6 +58,7 @@ describe('RepoTabs', () => {
           files: [],
           viewer: 'editor',
           hasChanges: false,
+          activeFile: file('activeFile'),
           hasMergeRequest: false,
         },
         '#test-app',
diff --git a/spec/javascripts/ide/lib/common/model_manager_spec.js b/spec/javascripts/ide/lib/common/model_manager_spec.js
index 4381f6fcfd0ac0af5d27dcb7b542f260c51bbf29..c00d590c580fd5610a23bb987f4eba8c2fd14383 100644
--- a/spec/javascripts/ide/lib/common/model_manager_spec.js
+++ b/spec/javascripts/ide/lib/common/model_manager_spec.js
@@ -27,9 +27,10 @@ describe('Multi-file editor library model manager', () => {
     });
 
     it('caches model by file path', () => {
-      instance.addModel(file('path-name'));
+      const f = file('path-name');
+      instance.addModel(f);
 
-      expect(instance.models.keys().next().value).toBe('path-name');
+      expect(instance.models.keys().next().value).toBe(f.key);
     });
 
     it('adds model into disposable', () => {
@@ -56,7 +57,7 @@ describe('Multi-file editor library model manager', () => {
       instance.addModel(f);
 
       expect(eventHub.$on).toHaveBeenCalledWith(
-        `editor.update.model.dispose.${f.path}`,
+        `editor.update.model.dispose.${f.key}`,
         jasmine.anything(),
       );
     });
@@ -68,9 +69,11 @@ describe('Multi-file editor library model manager', () => {
     });
 
     it('returns true when model exists', () => {
-      instance.addModel(file('path-name'));
+      const f = file('path-name');
+
+      instance.addModel(f);
 
-      expect(instance.hasCachedModel('path-name')).toBeTruthy();
+      expect(instance.hasCachedModel(f.key)).toBeTruthy();
     });
   });
 
@@ -103,7 +106,7 @@ describe('Multi-file editor library model manager', () => {
       instance.removeCachedModel(f);
 
       expect(eventHub.$off).toHaveBeenCalledWith(
-        `editor.update.model.dispose.${f.path}`,
+        `editor.update.model.dispose.${f.key}`,
         jasmine.anything(),
       );
     });
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
index 7cd990adb53308526ec0b453f5b312b92447baf6..8fc2fccb64c8067a24dad9f02ef6f70f2bd9f75e 100644
--- a/spec/javascripts/ide/lib/common/model_spec.js
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -32,14 +32,14 @@ describe('Multi-file editor library model', () => {
 
   it('adds eventHub listener', () => {
     expect(eventHub.$on).toHaveBeenCalledWith(
-      `editor.update.model.dispose.${model.file.path}`,
+      `editor.update.model.dispose.${model.file.key}`,
       jasmine.anything(),
     );
   });
 
   describe('path', () => {
     it('returns file path', () => {
-      expect(model.path).toBe('path');
+      expect(model.path).toBe(model.file.key);
     });
   });
 
@@ -74,7 +74,7 @@ describe('Multi-file editor library model', () => {
       model.onChange(() => {});
 
       expect(model.events.size).toBe(1);
-      expect(model.events.keys().next().value).toBe('path');
+      expect(model.events.keys().next().value).toBe(model.file.key);
     });
 
     it('calls callback on change', done => {
@@ -115,7 +115,7 @@ describe('Multi-file editor library model', () => {
       model.dispose();
 
       expect(eventHub.$off).toHaveBeenCalledWith(
-        `editor.update.model.dispose.${model.file.path}`,
+        `editor.update.model.dispose.${model.file.key}`,
         jasmine.anything(),
       );
     });
diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js
index 092170d086a15ea2fb989490cdcc308de5c9cc36..aec325e26a9366e492b63695fe91ae6acdf8eecb 100644
--- a/spec/javascripts/ide/lib/decorations/controller_spec.js
+++ b/spec/javascripts/ide/lib/decorations/controller_spec.js
@@ -36,9 +36,7 @@ describe('Multi-file editor library decorations controller', () => {
     });
 
     it('returns decorations by model URL', () => {
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
 
       const decorations = controller.getAllDecorationsForModel(model);
 
@@ -48,39 +46,29 @@ describe('Multi-file editor library decorations controller', () => {
 
   describe('addDecorations', () => {
     it('caches decorations in a new map', () => {
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
 
       expect(controller.decorations.size).toBe(1);
     });
 
     it('does not create new cache model', () => {
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue2' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]);
 
       expect(controller.decorations.size).toBe(1);
     });
 
     it('caches decorations by model URL', () => {
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
 
       expect(controller.decorations.size).toBe(1);
-      expect(controller.decorations.keys().next().value).toBe('path');
+      expect(controller.decorations.keys().next().value).toBe('path--path');
     });
 
     it('calls decorate method', () => {
       spyOn(controller, 'decorate');
 
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
 
       expect(controller.decorate).toHaveBeenCalled();
     });
@@ -92,10 +80,7 @@ describe('Multi-file editor library decorations controller', () => {
 
       controller.decorate(model);
 
-      expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith(
-        [],
-        [],
-      );
+      expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []);
     });
 
     it('caches decorations', () => {
@@ -111,15 +96,13 @@ describe('Multi-file editor library decorations controller', () => {
 
       controller.decorate(model);
 
-      expect(controller.editorDecorations.keys().next().value).toBe('path');
+      expect(controller.editorDecorations.keys().next().value).toBe('path--path');
     });
   });
 
   describe('dispose', () => {
     it('clears cached decorations', () => {
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
 
       controller.dispose();
 
@@ -127,9 +110,7 @@ describe('Multi-file editor library decorations controller', () => {
     });
 
     it('clears cached editorDecorations', () => {
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
 
       controller.dispose();
 
diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js
index c8f3e9f4830c0cec6e04b9c41f7c37a8220f44e7..ff73240734ec5a1de516faafccc328d93c4f7200 100644
--- a/spec/javascripts/ide/lib/diff/controller_spec.js
+++ b/spec/javascripts/ide/lib/diff/controller_spec.js
@@ -131,7 +131,7 @@ describe('Multi-file editor library dirty diff controller', () => {
     it('adds decorations into decorations controller', () => {
       spyOn(controller.decorationsController, 'addDecorations');
 
-      controller.decorate({ data: { changes: [], path: 'path' } });
+      controller.decorate({ data: { changes: [], path: model.path } });
 
       expect(
         controller.decorationsController.addDecorations,
@@ -145,7 +145,7 @@ describe('Multi-file editor library dirty diff controller', () => {
       );
 
       controller.decorate({
-        data: { changes: computeDiff('123', '1234'), path: 'path' },
+        data: { changes: computeDiff('123', '1234'), path: model.path },
       });
 
       expect(spy).toHaveBeenCalledWith(
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 08d39fe1ca48f362968b09ad818f17c81b2b9895..8613c718eed59d30c3d9bb735d74f6232540034e 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -32,7 +32,7 @@ describe('IDE store file actions', () => {
 
     it('closes open files', done => {
       store
-        .dispatch('closeFile', localFile.path)
+        .dispatch('closeFile', localFile)
         .then(() => {
           expect(localFile.opened).toBeFalsy();
           expect(localFile.active).toBeFalsy();
@@ -47,7 +47,7 @@ describe('IDE store file actions', () => {
       store.state.changedFiles.push(localFile);
 
       store
-        .dispatch('closeFile', localFile.path)
+        .dispatch('closeFile', localFile)
         .then(Vue.nextTick)
         .then(() => {
           expect(store.state.openFiles.length).toBe(0);
@@ -68,7 +68,7 @@ describe('IDE store file actions', () => {
       store.state.entries[f.path] = f;
 
       store
-        .dispatch('closeFile', localFile.path)
+        .dispatch('closeFile', localFile)
         .then(Vue.nextTick)
         .then(() => {
           expect(router.push).toHaveBeenCalledWith(`/project${f.url}`);
@@ -77,6 +77,22 @@ describe('IDE store file actions', () => {
         })
         .catch(done.fail);
     });
+
+    it('removes file if it pending', done => {
+      store.state.openFiles.push({
+        ...localFile,
+        pending: true,
+      });
+
+      store
+        .dispatch('closeFile', localFile)
+        .then(() => {
+          expect(store.state.openFiles.length).toBe(0);
+
+          done();
+        })
+        .catch(done.fail);
+    });
   });
 
   describe('setFileActive', () => {
@@ -472,4 +488,113 @@ describe('IDE store file actions', () => {
       );
     });
   });
+
+  describe('openPendingTab', () => {
+    let f;
+
+    beforeEach(() => {
+      f = {
+        ...file(),
+        projectId: '123',
+      };
+
+      store.state.entries[f.path] = f;
+    });
+
+    it('makes file pending in openFiles', done => {
+      store
+        .dispatch('openPendingTab', f)
+        .then(() => {
+          expect(store.state.openFiles[0].pending).toBe(true);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('returns true when opened', done => {
+      store
+        .dispatch('openPendingTab', f)
+        .then(added => {
+          expect(added).toBe(true);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('pushes router URL when added', done => {
+      store.state.currentBranchId = 'master';
+
+      store
+        .dispatch('openPendingTab', f)
+        .then(() => {
+          expect(router.push).toHaveBeenCalledWith('/project/123/tree/master/');
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('calls scrollToTab', done => {
+      const scrollToTabSpy = jasmine.createSpy('scrollToTab');
+      const oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
+      store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
+
+      store
+        .dispatch('openPendingTab', f)
+        .then(() => {
+          expect(scrollToTabSpy).toHaveBeenCalled();
+          store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('returns false when passed in file is active & viewer is diff', done => {
+      f.active = true;
+      store.state.openFiles.push(f);
+      store.state.viewer = 'diff';
+
+      store
+        .dispatch('openPendingTab', f)
+        .then(added => {
+          expect(added).toBe(false);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+  });
+
+  describe('removePendingTab', () => {
+    let f;
+
+    beforeEach(() => {
+      spyOn(eventHub, '$emit');
+
+      f = {
+        ...file('pendingFile'),
+        pending: true,
+      };
+    });
+
+    it('removes pending file from open files', done => {
+      store.state.openFiles.push(f);
+
+      store
+        .dispatch('removePendingTab', f)
+        .then(() => {
+          expect(store.state.openFiles.length).toBe(0);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('emits event to dispose model', done => {
+      store
+        .dispatch('removePendingTab', f)
+        .then(() => {
+          expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.dispose.${f.key}`);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+  });
 });
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index 6e497949862b2577f02145137b162bca9ff245dc..e0e5d915169e83161686cee34047d00057cba584 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -25,6 +25,21 @@ describe('IDE store file mutations', () => {
 
       expect(localFile.active).toBeTruthy();
     });
+
+    it('sets pending tab as not active', () => {
+      localState.openFiles.push({
+        ...localFile,
+        pending: true,
+        active: true,
+      });
+
+      mutations.SET_FILE_ACTIVE(localState, {
+        path: localFile.path,
+        active: true,
+      });
+
+      expect(localState.openFiles[0].active).toBe(false);
+    });
   });
 
   describe('TOGGLE_FILE_OPEN', () => {
@@ -224,4 +239,69 @@ describe('IDE store file mutations', () => {
       expect(localFile.changed).toBeTruthy();
     });
   });
+
+  describe('ADD_PENDING_TAB', () => {
+    beforeEach(() => {
+      const f = {
+        ...file('openFile'),
+        path: 'openFile',
+        active: true,
+        opened: true,
+      };
+
+      localState.entries[f.path] = f;
+      localState.openFiles.push(f);
+    });
+
+    it('adds file into openFiles as pending', () => {
+      mutations.ADD_PENDING_TAB(localState, { file: localFile });
+
+      expect(localState.openFiles.length).toBe(2);
+      expect(localState.openFiles[1].pending).toBe(true);
+      expect(localState.openFiles[1].key).toBe(`pending-${localFile.key}`);
+    });
+
+    it('updates open file to pending', () => {
+      mutations.ADD_PENDING_TAB(localState, { file: localState.openFiles[0] });
+
+      expect(localState.openFiles.length).toBe(1);
+    });
+
+    it('updates pending open file to active', () => {
+      localState.openFiles.push({
+        ...localFile,
+        pending: true,
+      });
+
+      mutations.ADD_PENDING_TAB(localState, { file: localFile });
+
+      expect(localState.openFiles[1].pending).toBe(true);
+      expect(localState.openFiles[1].active).toBe(true);
+    });
+
+    it('sets all openFiles to not active', () => {
+      mutations.ADD_PENDING_TAB(localState, { file: localFile });
+
+      expect(localState.openFiles.length).toBe(2);
+
+      localState.openFiles.forEach(f => {
+        if (f.pending) {
+          expect(f.active).toBe(true);
+        } else {
+          expect(f.active).toBe(false);
+        }
+      });
+    });
+  });
+
+  describe('REMOVE_PENDING_TAB', () => {
+    it('removes pending tab from openFiles', () => {
+      localFile.key = 'testing';
+      localState.openFiles.push(localFile);
+
+      mutations.REMOVE_PENDING_TAB(localState, localFile);
+
+      expect(localState.openFiles.length).toBe(0);
+    });
+  });
 });
diff --git a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1fd145116df0c434d327d7ed6b757e4ed1e1a9dc
--- /dev/null
+++ b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb
@@ -0,0 +1,171 @@
+require 'spec_helper'
+require 'ffaker'
+
+describe Banzai::Filter::CommitTrailersFilter do
+  include FilterSpecHelper
+  include CommitTrailersSpecHelper
+
+  let(:secondary_email)     { create(:email, :confirmed) }
+  let(:user)                { create(:user) }
+
+  let(:trailer)             { "#{FFaker::Lorem.word}-by:"}
+
+  let(:commit_message)      { trailer_line(trailer, user.name, user.email) }
+  let(:commit_message_html) { commit_html(commit_message) }
+
+  context 'detects' do
+    let(:email) { FFaker::Internet.email }
+
+    it 'trailers in the form of *-by and replace users with links' do
+      doc = filter(commit_message_html)
+
+      expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
+    end
+
+    it 'trailers prefixed with whitespaces' do
+      message_html = commit_html("\n\r  #{commit_message}")
+
+      doc = filter(message_html)
+
+      expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
+    end
+
+    it 'GitLab users via a secondary email' do
+      _, message_html = build_commit_message(
+        trailer: trailer,
+        name: secondary_email.user.name,
+        email: secondary_email.email
+      )
+
+      doc = filter(message_html)
+
+      expect_to_have_user_link_with_avatar(
+        doc,
+        user: secondary_email.user,
+        trailer: trailer,
+        email: secondary_email.email
+      )
+    end
+
+    it 'non GitLab users and replaces them with mailto links' do
+      _, message_html = build_commit_message(
+        trailer: trailer,
+        name: FFaker::Name.name,
+        email: email
+      )
+
+      doc = filter(message_html)
+
+      expect_to_have_mailto_link(doc, email: email, trailer: trailer)
+    end
+
+    it 'multiple trailers in the same message' do
+      different_trailer = "#{FFaker::Lorem.word}-by:"
+      message = commit_html %(
+        #{commit_message}
+        #{trailer_line(different_trailer, FFaker::Name.name, email)}
+      )
+
+      doc = filter(message)
+
+      expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
+      expect_to_have_mailto_link(doc, email: email, trailer: different_trailer)
+    end
+
+    context 'special names' do
+      where(:name) do
+        [
+          'John S. Doe',
+          'L33t H@x0r'
+        ]
+      end
+
+      with_them do
+        it do
+          message, message_html = build_commit_message(
+            trailer: trailer,
+            name: name,
+            email: email
+          )
+
+          doc = filter(message_html)
+
+          expect_to_have_mailto_link(doc, email: email, trailer: trailer)
+          expect(doc.text).to match Regexp.escape(message)
+        end
+      end
+    end
+  end
+
+  context "ignores" do
+    it 'commit messages without trailers' do
+      exp = message = commit_html(FFaker::Lorem.sentence)
+      doc = filter(message)
+
+      expect(doc.to_html).to match Regexp.escape(exp)
+    end
+
+    it 'trailers that are inline the commit message body' do
+      message = commit_html %(
+        #{FFaker::Lorem.sentence} #{commit_message} #{FFaker::Lorem.sentence}
+      )
+
+      doc = filter(message)
+
+      expect(doc.css('a').size).to eq 0
+    end
+  end
+
+  context "structure" do
+    it 'preserves the commit trailer structure' do
+      doc = filter(commit_message_html)
+
+      expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
+      expect(doc.text).to match Regexp.escape(commit_message)
+    end
+
+    it 'preserves the original name used in the commit message' do
+      message, message_html = build_commit_message(
+        trailer: trailer,
+        name: FFaker::Name.name,
+        email: user.email
+      )
+
+      doc = filter(message_html)
+
+      expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
+      expect(doc.text).to match Regexp.escape(message)
+    end
+
+    it 'preserves the original email used in the commit message' do
+      message, message_html = build_commit_message(
+        trailer: trailer,
+        name: secondary_email.user.name,
+        email: secondary_email.email
+      )
+
+      doc = filter(message_html)
+
+      expect_to_have_user_link_with_avatar(
+        doc,
+        user: secondary_email.user,
+        trailer: trailer,
+        email: secondary_email.email
+      )
+      expect(doc.text).to match Regexp.escape(message)
+    end
+
+    it 'only replaces trailer lines not the full commit message' do
+      commit_body = FFaker::Lorem.paragraph
+      message = commit_html %(
+        #{commit_body}
+        #{commit_message}
+      )
+
+      doc = filter(message)
+
+      expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer)
+      expect(doc.text).to include(commit_body)
+    end
+  end
+end
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index eeb82822f686796059dd17f4f536563c83b0a488..a1dd72c498f52a554cf8aa61b394d2a31b834c47 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -196,6 +196,41 @@ describe Banzai::Filter::MergeRequestReferenceFilter do
     end
   end
 
+  context 'URL reference for a commit' do
+    let(:mr) { create(:merge_request, :with_diffs) }
+    let(:reference) do
+      urls.project_merge_request_url(mr.project, mr) + "/diffs?commit_id=#{mr.diff_head_sha}"
+    end
+    let(:commit) { mr.commits.find { |commit| commit.sha == mr.diff_head_sha } }
+
+    it 'links to a valid reference' do
+      doc = reference_filter("See #{reference}")
+
+      expect(doc.css('a').first.attr('href'))
+        .to eq reference
+    end
+
+    it 'has valid text' do
+      doc = reference_filter("See #{reference}")
+
+      expect(doc.text).to eq("See #{mr.to_reference(full: true)} (#{commit.short_id})")
+    end
+
+    it 'has valid title attribute' do
+      doc = reference_filter("See #{reference}")
+
+      expect(doc.css('a').first.attr('title')).to eq(commit.title)
+    end
+
+    it 'ignores invalid commit short_ids on link text' do
+      invalidate_commit_reference =
+        urls.project_merge_request_url(mr.project, mr) + "/diffs?commit_id=12345678"
+      doc = reference_filter("See #{invalidate_commit_reference}")
+
+      expect(doc.text).to eq("See #{mr.to_reference(full: true)} (diffs)")
+    end
+  end
+
   context 'cross-project URL reference' do
     let(:namespace) { create(:namespace, name: 'cross-reference') }
     let(:project2)  { create(:project, :public, namespace: namespace) }
diff --git a/spec/lib/gitlab/auth/ldap/access_spec.rb b/spec/lib/gitlab/auth/ldap/access_spec.rb
index 9b3916bf9e350f73e3eb46989eaf1c3d4d6d4152..6b251d824f7fe077ff37d07fd1007ef9ba3d6a21 100644
--- a/spec/lib/gitlab/auth/ldap/access_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/access_spec.rb
@@ -1,6 +1,8 @@
 require 'spec_helper'
 
 describe Gitlab::Auth::LDAP::Access do
+  include LdapHelpers
+
   let(:access) { described_class.new user }
   let(:user) { create(:omniauth_user) }
 
@@ -32,8 +34,10 @@ describe Gitlab::Auth::LDAP::Access do
     end
 
     context 'when the user is found' do
+      let(:ldap_user) { Gitlab::Auth::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') }
+
       before do
-        allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user)
+        allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user)
       end
 
       context 'and the user is disabled via active directory' do
@@ -120,6 +124,22 @@ describe Gitlab::Auth::LDAP::Access do
         end
       end
     end
+
+    context 'when the connection fails' do
+      before do
+        raise_ldap_connection_error
+      end
+
+      it 'does not block the user' do
+        access.allowed?
+
+        expect(user.ldap_blocked?).to be_falsey
+      end
+
+      it 'denies access' do
+        expect(access.allowed?).to be_falsey
+      end
+    end
   end
 
   describe '#block_user' do
diff --git a/spec/lib/gitlab/auth/ldap/adapter_spec.rb b/spec/lib/gitlab/auth/ldap/adapter_spec.rb
index 10c60d792bd84556b25cc2d0aa29a4e06b8ab9e9..3eeaf3862f67c9e8636d70e19e16eed2102a4b20 100644
--- a/spec/lib/gitlab/auth/ldap/adapter_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/adapter_spec.rb
@@ -124,16 +124,36 @@ describe Gitlab::Auth::LDAP::Adapter do
 
     context "when the search raises an LDAP exception" do
       before do
+        allow(adapter).to receive(:renew_connection_adapter).and_return(ldap)
         allow(ldap).to receive(:search) { raise Net::LDAP::Error, "some error" }
         allow(Rails.logger).to receive(:warn)
       end
 
-      it { is_expected.to eq [] }
+      context 'retries the operation' do
+        before do
+          stub_const("#{described_class}::MAX_SEARCH_RETRIES", 3)
+        end
+
+        it 'as many times as MAX_SEARCH_RETRIES' do
+          expect(ldap).to receive(:search).exactly(3).times
+          expect { subject }.to raise_error(Gitlab::Auth::LDAP::LDAPConnectionError)
+        end
+
+        context 'when no more retries' do
+          before do
+            stub_const("#{described_class}::MAX_SEARCH_RETRIES", 1)
+          end
 
-      it 'logs the error' do
-        subject
-        expect(Rails.logger).to have_received(:warn).with(
-          "LDAP search raised exception Net::LDAP::Error: some error")
+          it 'raises the exception' do
+            expect { subject }.to raise_error(Gitlab::Auth::LDAP::LDAPConnectionError)
+          end
+
+          it 'logs the error' do
+            expect { subject }.to raise_error(Gitlab::Auth::LDAP::LDAPConnectionError)
+            expect(Rails.logger).to have_received(:warn).with(
+              "LDAP search raised exception Net::LDAP::Error: some error")
+          end
+        end
       end
     end
   end
diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
index 0c71f1d8ca65e40361b3a1abf9b8a57acba2ed1e..64f3d09a25b12a575006ac02d2485ab5ab8b07ca 100644
--- a/spec/lib/gitlab/auth/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -1,6 +1,8 @@
 require 'spec_helper'
 
 describe Gitlab::Auth::OAuth::User do
+  include LdapHelpers
+
   let(:oauth_user) { described_class.new(auth_hash) }
   let(:gl_user) { oauth_user.gl_user }
   let(:uid) { 'my-uid' }
@@ -38,10 +40,6 @@ describe Gitlab::Auth::OAuth::User do
   end
 
   describe '#save' do
-    def stub_ldap_config(messages)
-      allow(Gitlab::Auth::LDAP::Config).to receive_messages(messages)
-    end
-
     let(:provider) { 'twitter' }
 
     describe 'when account exists on server' do
@@ -269,20 +267,47 @@ describe Gitlab::Auth::OAuth::User do
             end
 
             context 'when an LDAP person is not found by uid' do
-              it 'tries to find an LDAP person by DN and adds the omniauth identity to the user' do
+              it 'tries to find an LDAP person by email and adds the omniauth identity to the user' do
                 allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(nil)
-                allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user)
+                allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_email).and_return(ldap_user)
+
+                oauth_user.save
+
+                identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+                expect(identities_as_hash).to match_array(result_identities(dn, uid))
+              end
+
+              context 'when also not found by email' do
+                it 'tries to find an LDAP person by DN and adds the omniauth identity to the user' do
+                  allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(nil)
+                  allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_email).and_return(nil)
+                  allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user)
+
+                  oauth_user.save
+
+                  identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
+                  expect(identities_as_hash).to match_array(result_identities(dn, uid))
+                end
+              end
+            end
 
+            def result_identities(dn, uid)
+              [
+                { provider: 'ldapmain', extern_uid: dn },
+                { provider: 'twitter', extern_uid: uid }
+              ]
+            end
+
+            context 'when there is an LDAP connection error' do
+              before do
+                raise_ldap_connection_error
+              end
+
+              it 'does not save the identity' do
                 oauth_user.save
 
                 identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } }
-                expect(identities_as_hash)
-                  .to match_array(
-                    [
-                      { provider: 'ldapmain', extern_uid: dn },
-                      { provider: 'twitter', extern_uid: uid }
-                    ]
-                  )
+                expect(identities_as_hash).to match_array([{ provider: 'twitter', extern_uid: uid }])
               end
             end
           end
@@ -739,4 +764,19 @@ describe Gitlab::Auth::OAuth::User do
       expect(oauth_user.find_user).to eql gl_user
     end
   end
+
+  describe '#find_ldap_person' do
+    context 'when LDAP connection fails' do
+      before do
+        raise_ldap_connection_error
+      end
+
+      it 'returns nil' do
+        adapter = Gitlab::Auth::LDAP::Adapter.new('ldapmain')
+        hash = OmniAuth::AuthHash.new(uid: 'whatever', provider: 'ldapmain')
+
+        expect(oauth_user.send(:find_ldap_person, hash, adapter)).to be_nil
+      end
+    end
+  end
 end
diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
index a6a1d9e619fb5856cb3d64d799edc0531e935858..c63120b0b292462b996f14d2fee14b141ecc692a 100644
--- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb
+++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb
@@ -137,7 +137,7 @@ describe Gitlab::BitbucketImport::Importer do
       it 'imports to the project disk_path' do
         expect(project.wiki).to receive(:repository_exists?) { false }
         expect(importer.gitlab_shell).to receive(:import_repository).with(
-          project.repository_storage_path,
+          project.repository_storage,
           project.wiki.disk_path,
           project.import_url + '/wiki'
         )
diff --git a/spec/lib/gitlab/git/checksum_spec.rb b/spec/lib/gitlab/git/checksum_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..8ff310905bf9a84e9970a27c195848dc257385de
--- /dev/null
+++ b/spec/lib/gitlab/git/checksum_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Gitlab::Git::Checksum, seed_helper: true do
+  let(:storage) { 'default' }
+
+  it 'raises Gitlab::Git::Repository::NoRepository when there is no repo' do
+    checksum = described_class.new(storage, 'nonexistent-repo')
+
+    expect { checksum.calculate }.to raise_error Gitlab::Git::Repository::NoRepository
+  end
+
+  it 'pretends that checksum is 000000... when the repo is empty' do
+    FileUtils.rm_rf(File.join(SEED_STORAGE_PATH, 'empty-repo.git'))
+
+    system(git_env, *%W(#{Gitlab.config.git.bin_path} init --bare empty-repo.git),
+           chdir: SEED_STORAGE_PATH,
+           out:   '/dev/null',
+           err:   '/dev/null')
+
+    checksum = described_class.new(storage, 'empty-repo')
+
+    expect(checksum.calculate).to eq '0000000000000000000000000000000000000000'
+  end
+
+  it 'raises Gitlab::Git::Repository::Failure when shelling out to git return non-zero status' do
+    checksum = described_class.new(storage, 'gitlab-git-test')
+
+    allow(checksum).to receive(:popen).and_return(['output', nil])
+
+    expect { checksum.calculate }.to raise_error Gitlab::Git::Checksum::Failure
+  end
+
+  it 'calculates the checksum when there is a repo' do
+    checksum = described_class.new(storage, 'gitlab-git-test')
+
+    expect(checksum.calculate).to eq '54f21be4c32c02f6788d72207fa03ad3bce725e4'
+  end
+end
diff --git a/spec/lib/gitlab/git/gitlab_projects_spec.rb b/spec/lib/gitlab/git/gitlab_projects_spec.rb
index dfccc15a4f3beee73632992b7efd6e3327c4e27b..8b715d717c1e65c3373471fa4931d634494ec18d 100644
--- a/spec/lib/gitlab/git/gitlab_projects_spec.rb
+++ b/spec/lib/gitlab/git/gitlab_projects_spec.rb
@@ -16,7 +16,7 @@ describe Gitlab::Git::GitlabProjects do
   let(:tmp_repos_path) { TestEnv.repos_path }
   let(:repo_name) { project.disk_path + '.git' }
   let(:tmp_repo_path) { File.join(tmp_repos_path, repo_name) }
-  let(:gl_projects) { build_gitlab_projects(tmp_repos_path, repo_name) }
+  let(:gl_projects) { build_gitlab_projects(TestEnv::REPOS_STORAGE, repo_name) }
 
   describe '#initialize' do
     it { expect(gl_projects.shard_path).to eq(tmp_repos_path) }
@@ -223,11 +223,12 @@ describe Gitlab::Git::GitlabProjects do
   end
 
   describe '#fork_repository' do
+    let(:dest_repos) { TestEnv::REPOS_STORAGE }
     let(:dest_repos_path) { tmp_repos_path }
     let(:dest_repo_name) { File.join('@hashed', 'aa', 'bb', 'xyz.git') }
     let(:dest_repo) { File.join(dest_repos_path, dest_repo_name) }
 
-    subject { gl_projects.fork_repository(dest_repos_path, dest_repo_name) }
+    subject { gl_projects.fork_repository(dest_repos, dest_repo_name) }
 
     before do
       FileUtils.mkdir_p(dest_repos_path)
@@ -268,7 +269,12 @@ describe Gitlab::Git::GitlabProjects do
       # that is not very straight-forward so I'm leaving this test here for now till
       # https://gitlab.com/gitlab-org/gitlab-ce/issues/41393 is fixed.
       context 'different storages' do
-        let(:dest_repos_path) { File.join(File.dirname(tmp_repos_path), 'alternative') }
+        let(:dest_repos) { 'alternative' }
+        let(:dest_repos_path) { File.join(File.dirname(tmp_repos_path), dest_repos) }
+
+        before do
+          stub_storage_settings(dest_repos => { 'path' => dest_repos_path })
+        end
 
         it 'forks the repo' do
           is_expected.to be_truthy
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 1f0f1fdd7da18a683c7da6f4c171a5e50793e545..879b1d9fb0f1a10dfb618d8a1b98f4b29e192639 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -9,7 +9,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
       :project,
       import_url: 'foo.git',
       import_source: 'foo/bar',
-      repository_storage_path: 'foo',
+      repository_storage: 'foo',
       disk_path: 'foo',
       repository: repository,
       create_wiki: true
diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..d75416f2a62d2be830a929cd61814d14a18e8b9a
--- /dev/null
+++ b/spec/lib/gitlab/import_export/importer_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::Importer do
+  let(:test_path) { "#{Dir.tmpdir}/importer_spec" }
+  let(:shared) { project.import_export_shared }
+  let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) }
+
+  subject(:importer) { described_class.new(project) }
+
+  before do
+    allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path)
+    FileUtils.mkdir_p(shared.export_path)
+    FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path)
+  end
+
+  after do
+    FileUtils.rm_rf(test_path)
+  end
+
+  describe '#execute' do
+    it 'succeeds' do
+      importer.execute
+
+      expect(shared.errors).to be_empty
+    end
+
+    it 'extracts the archive'  do
+      expect(Gitlab::ImportExport::FileImporter).to receive(:import).and_call_original
+
+      importer.execute
+    end
+
+    it 'checks the version' do
+      expect(Gitlab::ImportExport::VersionChecker).to receive(:check!).and_call_original
+
+      importer.execute
+    end
+
+    context 'all restores are executed' do
+      [
+        Gitlab::ImportExport::AvatarRestorer,
+        Gitlab::ImportExport::RepoRestorer,
+        Gitlab::ImportExport::WikiRestorer,
+        Gitlab::ImportExport::UploadsRestorer,
+        Gitlab::ImportExport::LfsRestorer
+      ].each do |restorer|
+        it "calls the #{restorer}" do
+          fake_restorer = double(restorer.to_s)
+
+          expect(fake_restorer).to receive(:restore).and_return(true).at_least(1)
+          expect(restorer).to receive(:new).and_return(fake_restorer).at_least(1)
+
+          importer.execute
+        end
+      end
+
+      it 'restores the ProjectTree' do
+        expect(Gitlab::ImportExport::ProjectTreeRestorer).to receive(:new).and_call_original
+
+        importer.execute
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/import_export/lfs_restorer_spec.rb b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..70eeb9ee66be130256e8d69d414c72c5f6de74be
--- /dev/null
+++ b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::LfsRestorer do
+  include UploadHelpers
+
+  let(:export_path) { "#{Dir.tmpdir}/lfs_object_restorer_spec" }
+  let(:project) { create(:project) }
+  let(:shared) { project.import_export_shared }
+  subject(:restorer) { described_class.new(project: project, shared: shared) }
+
+  before do
+    allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+    FileUtils.mkdir_p(shared.export_path)
+  end
+
+  after do
+    FileUtils.rm_rf(shared.export_path)
+  end
+
+  describe '#restore' do
+    context 'when the archive contains lfs files' do
+      let(:dummy_lfs_file_path) { File.join(shared.export_path, 'lfs-objects', 'dummy') }
+
+      def create_lfs_object_with_content(content)
+        dummy_lfs_file = Tempfile.new('existing')
+        File.write(dummy_lfs_file.path, content)
+        size = dummy_lfs_file.size
+        oid = LfsObject.calculate_oid(dummy_lfs_file.path)
+        LfsObject.create!(oid: oid, size: size, file: dummy_lfs_file)
+      end
+
+      before do
+        FileUtils.mkdir_p(File.dirname(dummy_lfs_file_path))
+        File.write(dummy_lfs_file_path, 'not very large')
+        allow(restorer).to receive(:lfs_file_paths).and_return([dummy_lfs_file_path])
+      end
+
+      it 'creates an lfs object for the project' do
+        expect { restorer.restore }.to change { project.reload.lfs_objects.size }.by(1)
+      end
+
+      it 'assigns the file correctly' do
+        restorer.restore
+
+        expect(project.lfs_objects.first.file.read).to eq('not very large')
+      end
+
+      it 'links an existing LFS object if it existed' do
+        lfs_object = create_lfs_object_with_content('not very large')
+
+        restorer.restore
+
+        expect(project.lfs_objects).to include(lfs_object)
+      end
+
+      it 'succeeds' do
+        expect(restorer.restore).to be_truthy
+        expect(shared.errors).to be_empty
+      end
+
+      it 'stores the upload' do
+        expect_any_instance_of(LfsObjectUploader).to receive(:store!)
+
+        restorer.restore
+      end
+    end
+
+    context 'without any LFS-objects' do
+      it 'succeeds' do
+        expect(restorer.restore).to be_truthy
+        expect(shared.errors).to be_empty
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/import_export/lfs_saver_spec.rb b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9b0e21deb2e8ba9f49cc5008753ed98c222407a7
--- /dev/null
+++ b/spec/lib/gitlab/import_export/lfs_saver_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::LfsSaver do
+  let(:shared) { project.import_export_shared }
+  let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
+  let(:project) { create(:project) }
+
+  subject(:saver) { described_class.new(project: project, shared: shared) }
+
+  before do
+    allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
+    FileUtils.mkdir_p(shared.export_path)
+  end
+
+  after do
+    FileUtils.rm_rf(shared.export_path)
+  end
+
+  describe '#save' do
+    context 'when the project has LFS objects locally stored' do
+      let(:lfs_object) { create(:lfs_object, :with_file) }
+
+      before do
+        project.lfs_objects << lfs_object
+      end
+
+      it 'does not cause errors' do
+        saver.save
+
+        expect(shared.errors).to be_empty
+      end
+
+      it 'copies the file in the correct location when there is an lfs object' do
+        saver.save
+
+        expect(File).to exist("#{shared.export_path}/lfs-objects/#{lfs_object.oid}")
+      end
+    end
+
+    context 'when the LFS objects are stored in object storage' do
+      let(:lfs_object) { create(:lfs_object, :object_storage) }
+
+      before do
+        allow(LfsObjectUploader).to receive(:object_store_enabled?).and_return(true)
+        allow(lfs_object.file).to receive(:url).and_return('http://my-object-storage.local')
+        project.lfs_objects << lfs_object
+      end
+
+      it 'downloads the file to include in an archive' do
+        fake_uri = double
+        exported_file_path = "#{shared.export_path}/lfs-objects/#{lfs_object.oid}"
+
+        expect(fake_uri).to receive(:open).and_return(StringIO.new('LFS file content'))
+        expect(URI).to receive(:parse).with('http://my-object-storage.local').and_return(fake_uri)
+
+        saver.save
+
+        expect(File.read(exported_file_path)).to eq('LFS file content')
+      end
+    end
+  end
+end
diff --git a/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb b/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb
index 6721e02fb85bbd7debfbef7de9ed3289a7097b9a..61eb059a7314fc5b36af068de8c3833521427d23 100644
--- a/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb
+++ b/spec/lib/gitlab/metrics/sidekiq_metrics_exporter_spec.rb
@@ -38,7 +38,9 @@ describe Gitlab::Metrics::SidekiqMetricsExporter do
 
             expect(::WEBrick::HTTPServer).to have_received(:new).with(
               Port: port,
-              BindAddress: address
+              BindAddress: address,
+              Logger: anything,
+              AccessLog: anything
             )
           end
         end
diff --git a/spec/lib/gitlab/performance_bar_spec.rb b/spec/lib/gitlab/performance_bar_spec.rb
index b8a2267f1a4f2655f5db4889ef86f170d505278e..f480376acb444a7a8d1f0a99a06244e67d23b7c1 100644
--- a/spec/lib/gitlab/performance_bar_spec.rb
+++ b/spec/lib/gitlab/performance_bar_spec.rb
@@ -25,6 +25,12 @@ describe Gitlab::PerformanceBar do
       expect(described_class.enabled?(nil)).to be_falsy
     end
 
+    it 'returns true when given user is an admin' do
+      user = build_stubbed(:user, :admin)
+
+      expect(described_class.enabled?(user)).to be_truthy
+    end
+
     it 'returns false when allowed_group_id is nil' do
       expect(described_class).to receive(:allowed_group_id).and_return(nil)
 
diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb
index ea5ce58e34b35a4a6ad3e6c9f05b18d39a5e89a8..7ff2c0639ec4bce9ee8c03527eaf9c6bb2b978c4 100644
--- a/spec/lib/gitlab/shell_spec.rb
+++ b/spec/lib/gitlab/shell_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Shell do
     allow(Project).to receive(:find).and_return(project)
 
     allow(gitlab_shell).to receive(:gitlab_projects)
-      .with(project.repository_storage_path, project.disk_path + '.git')
+      .with(project.repository_storage, project.disk_path + '.git')
       .and_return(gitlab_projects)
   end
 
@@ -487,21 +487,21 @@ describe Gitlab::Shell do
     describe '#fork_repository' do
       subject do
         gitlab_shell.fork_repository(
-          project.repository_storage_path,
+          project.repository_storage,
           project.disk_path,
-          'new/storage',
+          'nfs-file05',
           'fork/path'
         )
       end
 
       it 'returns true when the command succeeds' do
-        expect(gitlab_projects).to receive(:fork_repository).with('new/storage', 'fork/path.git') { true }
+        expect(gitlab_projects).to receive(:fork_repository).with('nfs-file05', 'fork/path.git') { true }
 
         is_expected.to be_truthy
       end
 
       it 'return false when the command fails' do
-        expect(gitlab_projects).to receive(:fork_repository).with('new/storage', 'fork/path.git') { false }
+        expect(gitlab_projects).to receive(:fork_repository).with('nfs-file05', 'fork/path.git') { false }
 
         is_expected.to be_falsy
       end
@@ -661,7 +661,7 @@ describe Gitlab::Shell do
       it 'returns true when the command succeeds' do
         expect(gitlab_projects).to receive(:import_project).with(import_url, timeout) { true }
 
-        result = gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, import_url)
+        result = gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url)
 
         expect(result).to be_truthy
       end
@@ -671,7 +671,7 @@ describe Gitlab::Shell do
         expect(gitlab_projects).to receive(:import_project) { false }
 
         expect do
-          gitlab_shell.import_repository(project.repository_storage_path, project.disk_path, import_url)
+          gitlab_shell.import_repository(project.repository_storage, project.disk_path, import_url)
         end.to raise_error(Gitlab::Shell::Error, "error")
       end
     end
diff --git a/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb b/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fed9aeba30ced1210a3953702e4889bcd336d3cb
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqLogging::JSONFormatter do
+  let(:hash_input) { { foo: 1, bar: 'test' } }
+  let(:message) { 'This is a test' }
+  let(:timestamp) { Time.now }
+
+  it 'wraps a Hash' do
+    result = subject.call('INFO', timestamp, 'my program', hash_input)
+
+    data = JSON.parse(result)
+    expected_output = hash_input.stringify_keys
+    expected_output['severity'] = 'INFO'
+    expected_output['time'] = timestamp.utc.iso8601(3)
+
+    expect(data).to eq(expected_output)
+  end
+
+  it 'wraps a String' do
+    result = subject.call('DEBUG', timestamp, 'my string', message)
+
+    data = JSON.parse(result)
+    expected_output = {
+      severity: 'DEBUG',
+      time: timestamp.utc.iso8601(3),
+      message: message
+    }
+
+    expect(data).to eq(expected_output.stringify_keys)
+  end
+end
diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2421b1e5a1a58ee60d14a71c36cb9ebc2bab00ce
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb
@@ -0,0 +1,101 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqLogging::StructuredLogger do
+  describe '#call' do
+    let(:timestamp) { Time.new('2018-01-01 12:00:00').utc }
+    let(:job) do
+      {
+        "class" => "TestWorker",
+        "args" => [1234, 'hello'],
+        "retry" => false,
+        "queue" => "cronjob:test_queue",
+        "queue_namespace" => "cronjob",
+        "jid" => "da883554ee4fe414012f5f42",
+        "created_at" => timestamp.to_f,
+        "enqueued_at" => timestamp.to_f
+      }
+    end
+    let(:logger) { double() }
+    let(:start_payload) do
+      job.merge(
+        'message' => 'TestWorker JID-da883554ee4fe414012f5f42: start',
+        'job_status' => 'start',
+        'pid' => Process.pid,
+        'created_at' => timestamp.iso8601(3),
+        'enqueued_at' => timestamp.iso8601(3)
+      )
+    end
+    let(:end_payload) do
+      start_payload.merge(
+        'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec',
+        'job_status' => 'done',
+        'duration' => 0.0,
+        "completed_at" => timestamp.iso8601(3)
+      )
+    end
+    let(:exception_payload) do
+      end_payload.merge(
+        'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec',
+        'job_status' => 'fail',
+        'error' => ArgumentError,
+        'error_message' => 'some exception'
+      )
+    end
+
+    before do
+      allow(Sidekiq).to receive(:logger).and_return(logger)
+
+      allow(subject).to receive(:current_time).and_return(timestamp.to_f)
+    end
+
+    subject { described_class.new }
+
+    context 'with SIDEKIQ_LOG_ARGUMENTS enabled' do
+      before do
+        stub_env('SIDEKIQ_LOG_ARGUMENTS', '1')
+      end
+
+      it 'logs start and end of job' do
+        Timecop.freeze(timestamp) do
+          expect(logger).to receive(:info).with(start_payload).ordered
+          expect(logger).to receive(:info).with(end_payload).ordered
+          expect(subject).to receive(:log_job_start).and_call_original
+          expect(subject).to receive(:log_job_done).and_call_original
+
+          subject.call(job, 'test_queue') { }
+        end
+      end
+
+      it 'logs an exception in job' do
+        Timecop.freeze(timestamp) do
+          expect(logger).to receive(:info).with(start_payload)
+          # This excludes the exception_backtrace
+          expect(logger).to receive(:warn).with(hash_including(exception_payload))
+          expect(subject).to receive(:log_job_start).and_call_original
+          expect(subject).to receive(:log_job_done).and_call_original
+
+          expect do
+            subject.call(job, 'test_queue') do
+              raise ArgumentError, 'some exception'
+            end
+          end.to raise_error(ArgumentError)
+        end
+      end
+    end
+
+    context 'with SIDEKIQ_LOG_ARGUMENTS disabled' do
+      it 'logs start and end of job' do
+        Timecop.freeze(timestamp) do
+          start_payload.delete('args')
+
+          expect(logger).to receive(:info).with(start_payload).ordered
+          expect(logger).to receive(:info).with(end_payload).ordered
+          expect(subject).to receive(:log_job_start).and_call_original
+          expect(subject).to receive(:log_job_done).and_call_original
+
+          subject.call(job, 'test_queue') { }
+        end
+      end
+    end
+  end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 83c33797bbc5fb2cfc4f15ff9a190697bbb89ed2..971a88e9ee9741586c71c398530a9723d4d61314 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -389,6 +389,36 @@ describe Notify do
           end
         end
       end
+
+      describe 'that have new commits' do
+        let(:push_user) { create(:user) }
+
+        subject do
+          described_class.push_to_merge_request_email(recipient.id, merge_request.id, push_user.id, new_commits: merge_request.commits)
+        end
+
+        it_behaves_like 'a multiple recipients email'
+        it_behaves_like 'an answer to an existing thread with reply-by-email enabled' do
+          let(:model) { merge_request }
+        end
+        it_behaves_like 'it should show Gmail Actions View Merge request link'
+        it_behaves_like 'an unsubscribeable thread'
+
+        it 'is sent as the push user' do
+          sender = subject.header[:from].addrs[0]
+
+          expect(sender.display_name).to eq(push_user.name)
+          expect(sender.address).to eq(gitlab_sender)
+        end
+
+        it 'has the correct subject and body' do
+          aggregate_failures do
+            is_expected.to have_referable_subject(merge_request, reply: true)
+            is_expected.to have_body_text("#{push_user.name} pushed new commits")
+            is_expected.to have_body_text(project_merge_request_path(project, merge_request))
+          end
+        end
+      end
     end
 
     context 'for issue notes' do
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index 268561ee9412cbc7c842f8dc2f504334b855032c..7e75d5a5411885022f10263bd3f74aa2a859c725 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -13,7 +13,7 @@ describe Ci::BuildMetadata do
   end
 
   let(:build) { create(:ci_build, pipeline: pipeline) }
-  let(:build_metadata) { create(:ci_build_metadata, build: build) }
+  let(:build_metadata) { build.metadata }
 
   describe '#update_timeout_state' do
     subject { build_metadata }
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index b7ed8be69fc64eac0bd4887f5ebe1cb0a8d17df3..c536dab26818069def5df92862f081cb6d7216e7 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -368,9 +368,7 @@ describe CommitStatus do
       'rspec:windows 0 : / 1' => 'rspec:windows',
       'rspec:windows 0 : / 1 name' => 'rspec:windows name',
       '0 1 name ruby' => 'name ruby',
-      '0 :/ 1 name ruby' => 'name ruby',
-      'golang test 1.8' => 'golang test',
-      '1.9 golang test' => 'golang test'
+      '0 :/ 1 name ruby' => 'name ruby'
     }
 
     tests.each do |name, group_name|
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index f8874d14e3fecab87c63ac141cece0890db2cb34..05693f067e121a5206f92452bf259b6a216f560c 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -176,7 +176,7 @@ describe Issuable do
     end
   end
 
-  describe "#sort" do
+  describe "#sort_by_attribute" do
     let(:project) { create(:project) }
 
     context "by milestone due date" do
@@ -193,12 +193,12 @@ describe Issuable do
       let!(:issue3) { create(:issue, project: project) }
 
       it "sorts desc" do
-        issues = project.issues.sort('milestone_due_desc')
+        issues = project.issues.sort_by_attribute('milestone_due_desc')
         expect(issues).to match_array([issue2, issue1, issue, issue3])
       end
 
       it "sorts asc" do
-        issues = project.issues.sort('milestone_due_asc')
+        issues = project.issues.sort_by_attribute('milestone_due_asc')
         expect(issues).to match_array([issue1, issue2, issue, issue3])
       end
     end
@@ -210,7 +210,7 @@ describe Issuable do
 
       it 'has no duplicates across pages' do
         sorted_issue_ids = 1.upto(10).map do |i|
-          project.issues.sort('milestone_due_desc').page(i).per(1).first.id
+          project.issues.sort_by_attribute('milestone_due_desc').page(i).per(1).first.id
         end
 
         expect(sorted_issue_ids).to eq(sorted_issue_ids.uniq)
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index fef868ac0f2b737259a7d44d0d4161eba7063db0..8bd62dcdccb6c16993d7ee9dc96540dae51b2aa8 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1645,7 +1645,7 @@ describe Project do
 
     before do
       allow_any_instance_of(Gitlab::Shell).to receive(:import_repository)
-        .with(project.repository_storage_path, project.disk_path, project.import_url)
+        .with(project.repository_storage, project.disk_path, project.import_url)
         .and_return(true)
 
       expect_any_instance_of(Repository).to receive(:after_import)
@@ -1798,10 +1798,7 @@ describe Project do
       let(:project) { forked_project_link.forked_to_project }
 
       it 'schedules a RepositoryForkWorker job' do
-        expect(RepositoryForkWorker).to receive(:perform_async).with(
-          project.id,
-          forked_from_project.repository_storage_path,
-          forked_from_project.disk_path).and_return(import_jid)
+        expect(RepositoryForkWorker).to receive(:perform_async).with(project.id).and_return(import_jid)
 
         expect(project.add_import_job).to eq(import_jid)
       end
@@ -2025,6 +2022,22 @@ describe Project do
         expect(forked_project.lfs_storage_project).to eq forked_project
       end
     end
+
+    describe '#all_lfs_objects' do
+      let(:lfs_object) { create(:lfs_object) }
+
+      before do
+        project.lfs_objects << lfs_object
+      end
+
+      it 'returns the lfs object for a project' do
+        expect(project.all_lfs_objects).to contain_exactly(lfs_object)
+      end
+
+      it 'returns the lfs object for a fork' do
+        expect(forked_project.all_lfs_objects).to contain_exactly(lfs_object)
+      end
+    end
   end
 
   describe '#pushes_since_gc' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 100418da804741a78c3e40a452a4757c7686e474..4027c420e47e2789ce483d6b44c6d26dc0c2b3db 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1451,7 +1451,7 @@ describe User do
     end
   end
 
-  describe '#sort' do
+  describe '#sort_by_attribute' do
     before do
       described_class.delete_all
       @user = create :user, created_at: Date.today, current_sign_in_at: Date.today, name: 'Alpha'
@@ -1460,7 +1460,7 @@ describe User do
     end
 
     context 'when sort by recent_sign_in' do
-      let(:users) { described_class.sort('recent_sign_in') }
+      let(:users) { described_class.sort_by_attribute('recent_sign_in') }
 
       it 'sorts users by recent sign-in time' do
         expect(users.first).to eq(@user)
@@ -1473,7 +1473,7 @@ describe User do
     end
 
     context 'when sort by oldest_sign_in' do
-      let(:users) { described_class.sort('oldest_sign_in') }
+      let(:users) { described_class.sort_by_attribute('oldest_sign_in') }
 
       it 'sorts users by the oldest sign-in time' do
         expect(users.first).to eq(@user1)
@@ -1486,15 +1486,15 @@ describe User do
     end
 
     it 'sorts users in descending order by their creation time' do
-      expect(described_class.sort('created_desc').first).to eq(@user)
+      expect(described_class.sort_by_attribute('created_desc').first).to eq(@user)
     end
 
     it 'sorts users in ascending order by their creation time' do
-      expect(described_class.sort('created_asc').first).to eq(@user2)
+      expect(described_class.sort_by_attribute('created_asc').first).to eq(@user2)
     end
 
     it 'sorts users by id in descending order when nil is passed' do
-      expect(described_class.sort(nil).first).to eq(@user2)
+      expect(described_class.sort_by_attribute(nil).first).to eq(@user2)
     end
   end
 
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
index c6c10025f7f2f03750370ed101b7e6bb397aaedf..92b614b087ea6ad7163a10fb623ddd2d7c326f09 100644
--- a/spec/requests/api/boards_spec.rb
+++ b/spec/requests/api/boards_spec.rb
@@ -48,5 +48,36 @@ describe API::Boards do
       expect(json_response['label']['name']).to eq(group_label.title)
       expect(json_response['position']).to eq(3)
     end
+
+    it 'creates a new board list for ancestor group labels' do
+      group = create(:group)
+      sub_group = create(:group, parent: group)
+      group_label = create(:group_label, group: group)
+      board_parent.update(group: sub_group)
+      group.add_developer(user)
+      sub_group.add_developer(user)
+
+      post api(url, user), label_id: group_label.id
+
+      expect(response).to have_gitlab_http_status(201)
+      expect(json_response['label']['name']).to eq(group_label.title)
+    end
+  end
+
+  describe "POST /groups/:id/boards/lists", :nested_groups do
+    set(:group) { create(:group) }
+    set(:board_parent) { create(:group, parent: group ) }
+    let(:url) { "/groups/#{board_parent.id}/boards/#{board.id}/lists" }
+    set(:board) { create(:board, group: board_parent) }
+
+    it 'creates a new board list for ancestor group labels' do
+      group.add_developer(user)
+      group_label = create(:group_label, group: group)
+
+      post api(url, user), label_id: group_label.id
+
+      expect(response).to have_gitlab_http_status(201)
+      expect(json_response['label']['name']).to eq(group_label.title)
+    end
   end
 end
diff --git a/spec/requests/api/features_spec.rb b/spec/requests/api/features_spec.rb
index 267058d98eec214736d2e93a56479bc9ed753217..c5354c2d639d38314030f3e063e5ca1f77391a01 100644
--- a/spec/requests/api/features_spec.rb
+++ b/spec/requests/api/features_spec.rb
@@ -1,8 +1,8 @@
 require 'spec_helper'
 
 describe API::Features do
-  let(:user)  { create(:user) }
-  let(:admin) { create(:admin) }
+  set(:user)  { create(:user) }
+  set(:admin) { create(:admin) }
 
   before do
     Flipper.unregister_groups
@@ -249,4 +249,43 @@ describe API::Features do
       end
     end
   end
+
+  describe 'DELETE /feature/:name' do
+    let(:feature_name) { 'my_feature' }
+
+    context 'when the user has no access' do
+      it 'returns a 401 for anonymous users' do
+        delete api("/features/#{feature_name}")
+
+        expect(response).to have_gitlab_http_status(401)
+      end
+
+      it 'returns a 403 for users' do
+        delete api("/features/#{feature_name}", user)
+
+        expect(response).to have_gitlab_http_status(403)
+      end
+    end
+
+    context 'when the user has access' do
+      it 'returns 204 when the value is not set' do
+        delete api("/features/#{feature_name}", admin)
+
+        expect(response).to have_gitlab_http_status(204)
+      end
+
+      context 'when the gate value was set' do
+        before do
+          Feature.get(feature_name).enable
+        end
+
+        it 'deletes an enabled feature' do
+          delete api("/features/#{feature_name}", admin)
+
+          expect(response).to have_gitlab_http_status(204)
+          expect(Feature.get(feature_name)).not_to be_enabled
+        end
+      end
+    end
+  end
 end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index d73a42f48ada3ff05560b99a0e4c38e3ca47ce23..2ec29a79e9301c7390e826655b100cf60ce8b43a 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -452,7 +452,8 @@ describe API::Projects do
         only_allow_merge_if_pipeline_succeeds: false,
         request_access_enabled: true,
         only_allow_merge_if_all_discussions_are_resolved: false,
-        ci_config_path: 'a/custom/path'
+        ci_config_path: 'a/custom/path',
+        merge_method: 'ff'
       })
 
       post api('/projects', user), project
@@ -569,6 +570,22 @@ describe API::Projects do
       expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
     end
 
+    it 'sets the merge method of a project to rebase merge' do
+      project = attributes_for(:project, merge_method: 'rebase_merge')
+
+      post api('/projects', user), project
+
+      expect(json_response['merge_method']).to eq('rebase_merge')
+    end
+
+    it 'rejects invalid values for merge_method' do
+      project = attributes_for(:project, merge_method: 'totally_not_valid_method')
+
+      post api('/projects', user), project
+
+      expect(response).to have_gitlab_http_status(400)
+    end
+
     it 'ignores import_url when it is nil' do
       project = attributes_for(:project, import_url: nil)
 
@@ -823,6 +840,7 @@ describe API::Projects do
         expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
         expect(json_response['only_allow_merge_if_pipeline_succeeds']).to eq(project.only_allow_merge_if_pipeline_succeeds)
         expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to eq(project.only_allow_merge_if_all_discussions_are_resolved)
+        expect(json_response['merge_method']).to eq(project.merge_method.to_s)
       end
 
       it 'returns a project by path name' do
@@ -1474,6 +1492,26 @@ describe API::Projects do
           expect(json_response[k.to_s]).to eq(v)
         end
       end
+
+      it 'updates merge_method' do
+        project_param = { merge_method: 'ff' }
+
+        put api("/projects/#{project3.id}", user), project_param
+
+        expect(response).to have_gitlab_http_status(200)
+
+        project_param.each_pair do |k, v|
+          expect(json_response[k.to_s]).to eq(v)
+        end
+      end
+
+      it 'rejects to update merge_method when merge_method is invalid' do
+        project_param = { merge_method: 'invalid' }
+
+        put api("/projects/#{project3.id}", user), project_param
+
+        expect(response).to have_gitlab_http_status(400)
+      end
     end
 
     context 'when authenticated as project master' do
@@ -1491,6 +1529,7 @@ describe API::Projects do
                           wiki_enabled: true,
                           snippets_enabled: true,
                           merge_requests_enabled: true,
+                          merge_method: 'ff',
                           description: 'new description' }
 
         put api("/projects/#{project3.id}", user4), project_param
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 5084b36c7616951c8ebbe6e2cc7871bd2eb4f930..4f3420cc0ad2aa7484bc2bd84f632973b51c8efb 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -1159,11 +1159,13 @@ describe API::Runner do
             let!(:artifacts) { file_upload }
             let!(:artifacts_sha256) { Digest::SHA256.file(artifacts.path).hexdigest }
             let!(:metadata) { file_upload2 }
+            let!(:metadata_sha256) { Digest::SHA256.file(metadata.path).hexdigest }
 
             let(:stored_artifacts_file) { job.reload.artifacts_file.file }
             let(:stored_metadata_file) { job.reload.artifacts_metadata.file }
             let(:stored_artifacts_size) { job.reload.artifacts_size }
             let(:stored_artifacts_sha256) { job.reload.job_artifacts_archive.file_sha256 }
+            let(:stored_metadata_sha256) { job.reload.job_artifacts_metadata.file_sha256 }
 
             before do
               post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token)
@@ -1175,7 +1177,8 @@ describe API::Runner do
                   'file.name' => artifacts.original_filename,
                   'file.sha256' => artifacts_sha256,
                   'metadata.path' => metadata.path,
-                  'metadata.name' => metadata.original_filename }
+                  'metadata.name' => metadata.original_filename,
+                  'metadata.sha256' => metadata_sha256 }
               end
 
               it 'stores artifacts and artifacts metadata' do
@@ -1184,6 +1187,7 @@ describe API::Runner do
                 expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
                 expect(stored_artifacts_size).to eq(72821)
                 expect(stored_artifacts_sha256).to eq(artifacts_sha256)
+                expect(stored_metadata_sha256).to eq(metadata_sha256)
               end
             end
 
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index eef860821e50c72c58291ac4d15ea26972b5e0f4..bcc3e3a267868e672254fefb43a4bc5443e8b530 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -23,7 +23,7 @@ describe 'cycle analytics events' do
     it 'lists the issue events' do
       get project_cycle_analytics_issue_path(project, format: :json)
 
-      first_issue_iid = project.issues.sort(:created_desc).pluck(:iid).first.to_s
+      first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
 
       expect(json_response['events']).not_to be_empty
       expect(json_response['events'].first['iid']).to eq(first_issue_iid)
@@ -32,7 +32,7 @@ describe 'cycle analytics events' do
     it 'lists the plan events' do
       get project_cycle_analytics_plan_path(project, format: :json)
 
-      first_mr_short_sha = project.merge_requests.sort(:created_asc).first.commits.first.short_id
+      first_mr_short_sha = project.merge_requests.sort_by_attribute(:created_asc).first.commits.first.short_id
 
       expect(json_response['events']).not_to be_empty
       expect(json_response['events'].first['short_sha']).to eq(first_mr_short_sha)
@@ -43,7 +43,7 @@ describe 'cycle analytics events' do
 
       expect(json_response['events']).not_to be_empty
 
-      first_mr_iid = project.merge_requests.sort(:created_desc).pluck(:iid).first.to_s
+      first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
 
       expect(json_response['events'].first['iid']).to eq(first_mr_iid)
     end
@@ -58,7 +58,7 @@ describe 'cycle analytics events' do
     it 'lists the review events' do
       get project_cycle_analytics_review_path(project, format: :json)
 
-      first_mr_iid = project.merge_requests.sort(:created_desc).pluck(:iid).first.to_s
+      first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
 
       expect(json_response['events']).not_to be_empty
       expect(json_response['events'].first['iid']).to eq(first_mr_iid)
@@ -74,7 +74,7 @@ describe 'cycle analytics events' do
     it 'lists the production events' do
       get project_cycle_analytics_production_path(project, format: :json)
 
-      first_issue_iid = project.issues.sort(:created_desc).pluck(:iid).first.to_s
+      first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
 
       expect(json_response['events']).not_to be_empty
       expect(json_response['events'].first['iid']).to eq(first_issue_iid)
diff --git a/spec/serializers/discussion_entity_spec.rb b/spec/serializers/discussion_entity_spec.rb
index 7ee8e38af1c81a69c1615959df7388e3ed30e94b..7e19e74ca00dcc03a36c940d64e706c62883b1fd 100644
--- a/spec/serializers/discussion_entity_spec.rb
+++ b/spec/serializers/discussion_entity_spec.rb
@@ -6,7 +6,7 @@ describe DiscussionEntity do
   let(:user) { create(:user) }
   let(:note) { create(:discussion_note_on_merge_request) }
   let(:discussion) { note.discussion }
-  let(:request) { double('request') }
+  let(:request) { double('request', note_entity: ProjectNoteEntity) }
   let(:controller) { double('controller') }
   let(:entity) { described_class.new(discussion, request: request, context: controller) }
 
diff --git a/spec/serializers/note_entity_spec.rb b/spec/serializers/note_entity_spec.rb
index 51a8587ace9615883059bf862507a6cd12743274..13cda781cda82ff2cf97af7497f1efc7cb33a57f 100644
--- a/spec/serializers/note_entity_spec.rb
+++ b/spec/serializers/note_entity_spec.rb
@@ -10,53 +10,5 @@ describe NoteEntity do
   let(:user) { create(:user) }
   subject { entity.as_json }
 
-  context 'basic note' do
-    it 'exposes correct elements' do
-      expect(subject).to include(:type, :author, :human_access, :note, :note_html, :current_user,
-        :discussion_id, :emoji_awardable, :award_emoji, :toggle_award_path, :report_abuse_path, :path, :attachment)
-    end
-
-    it 'does not expose elements for specific notes cases' do
-      expect(subject).not_to include(:last_edited_by, :last_edited_at, :system_note_icon_name)
-    end
-
-    it 'exposes author correctly' do
-      expect(subject[:author]).to include(:id, :name, :username, :state, :avatar_url, :path)
-    end
-
-    it 'does not expose web_url for author' do
-      expect(subject[:author]).not_to include(:web_url)
-    end
-  end
-
-  context 'when note was edited' do
-    before do
-      note.update(updated_at: 1.minute.from_now, updated_by: user)
-    end
-
-    it 'exposes last_edited_at and last_edited_by elements' do
-      expect(subject).to include(:last_edited_at, :last_edited_by)
-    end
-  end
-
-  context 'when note is a system note' do
-    before do
-      note.update(system: true)
-    end
-
-    it 'exposes system_note_icon_name element' do
-      expect(subject).to include(:system_note_icon_name)
-    end
-  end
-
-  context 'when note is part of resolvable discussion' do
-    before do
-      allow(note).to receive(:part_of_discussion?).and_return(true)
-      allow(note).to receive(:resolvable?).and_return(true)
-    end
-
-    it 'exposes paths to resolve note' do
-      expect(subject).to include(:resolve_path, :resolve_with_issue_path)
-    end
-  end
+  it_behaves_like 'note entity'
 end
diff --git a/spec/serializers/project_note_entity_spec.rb b/spec/serializers/project_note_entity_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dafd1cf603e417383ef23b80ed0b7bab9e5af624
--- /dev/null
+++ b/spec/serializers/project_note_entity_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe ProjectNoteEntity do
+  include Gitlab::Routing
+
+  let(:request) { double('request', current_user: user, noteable: note.noteable) }
+
+  let(:entity) { described_class.new(note, request: request) }
+  let(:note) { create(:note) }
+  let(:user) { create(:user) }
+  subject { entity.as_json }
+
+  it_behaves_like 'note entity'
+
+  it 'exposes project-specific elements' do
+    expect(subject).to include(:human_access, :toggle_award_path, :path)
+  end
+
+  context 'when note is part of resolvable discussion' do
+    before do
+      allow(note).to receive(:part_of_discussion?).and_return(true)
+      allow(note).to receive(:resolvable?).and_return(true)
+    end
+
+    it 'exposes paths to resolve note' do
+      expect(subject).to include(:resolve_path, :resolve_with_issue_path)
+    end
+  end
+end
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 0a6b6d880d37863c04b5751abb7a625e4899ae44..dd0ad5f11bd5b0ac66fe44fa43d22ca1fd4a7288 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -48,7 +48,7 @@ describe Boards::Issues::MoveService do
         parent.add_developer(user)
       end
 
-      it_behaves_like 'issues move service'
+      it_behaves_like 'issues move service', true
     end
   end
 end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 41237dd71609cf24639d61f5dbc89813a7d256c4..f95474208f3210ee8ade61ad473029e1c362c74b 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -97,6 +97,37 @@ describe Issues::UpdateService, :mailer do
         expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
       end
 
+      context 'when moving issue between issues from different projects' do
+        let(:group) { create(:group) }
+        let(:project_1) { create(:project, namespace: group) }
+        let(:project_2) { create(:project, namespace: group) }
+        let(:project_3) { create(:project, namespace: group) }
+
+        let(:issue_1) { create(:issue, project: project_1) }
+        let(:issue_2) { create(:issue, project: project_2) }
+        let(:issue_3) { create(:issue, project: project_3) }
+
+        before do
+          group.add_developer(user)
+        end
+
+        it 'sorts issues as specified by parameters' do
+          # Moving all issues to end here like the last example won't work since
+          # all projects only have the same issue count
+          # so their relative_position will be the same.
+          issue_1.move_to_end
+          issue_2.move_after(issue_1)
+          issue_3.move_after(issue_2)
+          [issue_1, issue_2, issue_3].map(&:save)
+
+          opts[:move_between_ids] = [issue_1.id, issue_2.id]
+          opts[:board_group_id] = group.id
+
+          described_class.new(issue_3.project, user, opts).execute(issue_3)
+          expect(issue_2.relative_position).to be_between(issue_1.relative_position, issue_2.relative_position)
+        end
+      end
+
       context 'when current user cannot admin issues in the project' do
         let(:guest) { create(:user) }
         before do
diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb
index 51491c7d5292d0c73fd519cb0933515a1a784988..f9e5530bc9d4d418321a267cebdf85b055f706ef 100644
--- a/spec/services/projects/import_export/export_service_spec.rb
+++ b/spec/services/projects/import_export/export_service_spec.rb
@@ -8,6 +8,49 @@ describe Projects::ImportExport::ExportService do
     let(:service) { described_class.new(project, user) }
     let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
 
+    it 'saves the version' do
+      expect(Gitlab::ImportExport::VersionSaver).to receive(:new).and_call_original
+
+      service.execute
+    end
+
+    it 'saves the avatar' do
+      expect(Gitlab::ImportExport::AvatarSaver).to receive(:new).and_call_original
+
+      service.execute
+    end
+
+    it 'saves the models' do
+      expect(Gitlab::ImportExport::ProjectTreeSaver).to receive(:new).and_call_original
+
+      service.execute
+    end
+
+    it 'saves the uploads' do
+      expect(Gitlab::ImportExport::UploadsSaver).to receive(:new).and_call_original
+
+      service.execute
+    end
+
+    it 'saves the repo' do
+      # once for the normal repo, once for the wiki
+      expect(Gitlab::ImportExport::RepoSaver).to receive(:new).twice.and_call_original
+
+      service.execute
+    end
+
+    it 'saves the lfs objects' do
+      expect(Gitlab::ImportExport::LfsSaver).to receive(:new).and_call_original
+
+      service.execute
+    end
+
+    it 'saves the wiki repo' do
+      expect(Gitlab::ImportExport::WikiRepoSaver).to receive(:new).and_call_original
+
+      service.execute
+    end
+
     context 'when all saver services succeed' do
       before do
         allow(service).to receive(:save_services).and_return(true)
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index 934106627a9ee92bf732f95e6d2650d8a5cdde2c..dd31a677dfea380007cebf64ee35ddbce8cf919b 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -87,7 +87,8 @@ describe Projects::UpdatePagesService do
         it 'fails for empty file fails' do
           build.update_attributes(legacy_artifacts_file: empty_file)
 
-          expect(execute).not_to eq(:success)
+          expect { execute }
+            .to raise_error(Projects::UpdatePagesService::FailedToExtractError)
         end
       end
     end
@@ -159,7 +160,8 @@ describe Projects::UpdatePagesService do
       it 'fails for empty file fails' do
         build.job_artifacts_archive.update_attributes(file: empty_file)
 
-        expect(execute).not_to eq(:success)
+        expect { execute }
+          .to raise_error(Projects::UpdatePagesService::FailedToExtractError)
       end
 
       context 'when timeout happens by DNS error' do
@@ -172,7 +174,39 @@ describe Projects::UpdatePagesService do
           expect { execute }.to raise_error(SocketError)
 
           build.reload
-          expect(build.artifacts?).to eq(true)
+          expect(deploy_status).to be_failed
+          expect(build.artifacts?).to be_truthy
+        end
+      end
+
+      context 'when failed to extract zip artifacts' do
+        before do
+          allow_any_instance_of(described_class)
+            .to receive(:extract_zip_archive!)
+            .and_raise(Projects::UpdatePagesService::FailedToExtractError)
+        end
+
+        it 'raises an error' do
+          expect { execute }
+            .to raise_error(Projects::UpdatePagesService::FailedToExtractError)
+
+          build.reload
+          expect(deploy_status).to be_failed
+          expect(build.artifacts?).to be_truthy
+        end
+      end
+
+      context 'when missing artifacts metadata' do
+        before do
+          allow(build).to receive(:artifacts_metadata?).and_return(false)
+        end
+
+        it 'does not raise an error and remove artifacts as failed job' do
+          execute
+
+          build.reload
+          expect(deploy_status).to be_failed
+          expect(build.artifacts?).to be_falsey
         end
       end
     end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index e8cecf361ff3966cbcebb98ea2add7b4e2fb8e4f..beabba99cf59c30a325ea4a23de349cc52f408d0 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -108,7 +108,8 @@ RSpec.configure do |config|
     allow_any_instance_of(Gitlab::Git::GitlabProjects).to receive(:fork_repository).and_wrap_original do |m, *args|
       m.call(*args)
 
-      shard_path, repository_relative_path = args
+      shard_name, repository_relative_path = args
+      shard_path = Gitlab.config.repositories.storages.fetch(shard_name).legacy_disk_path
       # We can't leave the hooks in place after a fork, as those would fail in tests
       # The "internal" API is not available
       FileUtils.rm_rf(File.join(shard_path, repository_relative_path, 'hooks'))
diff --git a/spec/support/commit_trailers_spec_helper.rb b/spec/support/commit_trailers_spec_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..add359946db432a93538e043a9daed058fd32156
--- /dev/null
+++ b/spec/support/commit_trailers_spec_helper.rb
@@ -0,0 +1,41 @@
+module CommitTrailersSpecHelper
+  extend ActiveSupport::Concern
+
+  def expect_to_have_user_link_with_avatar(doc, user:, trailer:, email: nil)
+    wrapper = find_user_wrapper(doc, trailer)
+
+    expect_to_have_links_with_url_and_avatar(wrapper, urls.user_url(user), email || user.email)
+    expect(wrapper.attribute('data-user').value).to eq user.id.to_s
+  end
+
+  def expect_to_have_mailto_link(doc, email:, trailer:)
+    wrapper = find_user_wrapper(doc, trailer)
+
+    expect_to_have_links_with_url_and_avatar(wrapper, "mailto:#{CGI.escape_html(email)}", email)
+  end
+
+  def expect_to_have_links_with_url_and_avatar(doc, url, email)
+    expect(doc).not_to be_nil
+    expect(doc.xpath("a[position()<3 and @href='#{url}']").size).to eq 2
+    expect(doc.xpath("a[position()=3 and @href='mailto:#{CGI.escape_html(email)}']").size).to eq 1
+    expect(doc.css('img').size).to eq 1
+  end
+
+  def find_user_wrapper(doc, trailer)
+    doc.xpath("descendant-or-self::node()[@data-trailer='#{trailer}']").first
+  end
+
+  def build_commit_message(trailer:, name:, email:)
+    message = trailer_line(trailer, name, email)
+
+    [message, commit_html(message)]
+  end
+
+  def trailer_line(trailer, name, email)
+    "#{trailer} #{name} <#{email}>"
+  end
+
+  def commit_html(message)
+    "<pre>#{CGI.escape_html(message)}</pre>"
+  end
+end
diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb
index c8662d417696c2ffefcb88ce8a6616441436e791..80604395adfea7b9285ff5cbb90aa1eb12322664 100644
--- a/spec/support/features/discussion_comments_shared_example.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -81,7 +81,10 @@ shared_examples 'discussion comments' do |resource_name|
 
       # on issues page, the menu closes when clicking anywhere, on other pages it will
       # remain open if clicking divider or menu padding, but should not change button action
-      if resource_name == 'issue'
+      #
+      # if dropdown menu is not toggled (and also not present),
+      # it's "issue-type" dropdown
+      if first(menu_selector).nil?
         expect(find(dropdown_selector)).to have_content 'Comment'
 
         find(toggle_selector).click
@@ -107,8 +110,10 @@ shared_examples 'discussion comments' do |resource_name|
       end
 
       it 'updates the submit button text and closes the dropdown' do
+        button = find(submit_selector)
+
         # on issues page, the submit input is a <button>, on other pages it is <input>
-        if resource_name == 'issue'
+        if button.tag_name == 'button'
           expect(find(submit_selector)).to have_content 'Start discussion'
         else
           expect(find(submit_selector).value).to eq 'Start discussion'
@@ -132,6 +137,8 @@ shared_examples 'discussion comments' do |resource_name|
       describe 'creating a discussion' do
         before do
           find(submit_selector).click
+          wait_for_requests
+
           find(comments_selector, match: :first)
         end
 
@@ -197,11 +204,13 @@ shared_examples 'discussion comments' do |resource_name|
           end
 
           it 'updates the submit button text and closes the dropdown' do
+            button = find(submit_selector)
+
             # on issues page, the submit input is a <button>, on other pages it is <input>
-            if resource_name == 'issue'
-              expect(find(submit_selector)).to have_content 'Comment'
+            if button.tag_name == 'button'
+              expect(button).to have_content 'Comment'
             else
-              expect(find(submit_selector).value).to eq 'Comment'
+              expect(button.value).to eq 'Comment'
             end
 
             expect(page).not_to have_selector menu_selector
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index f61469f673d40a7f5204bcc19eb94023e4a00748..1bd6c25100e111b8c0c6f75053cf9e93962100f3 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -2,7 +2,7 @@
 # It takes a `issuable_type`, and expect an `issuable`.
 
 shared_examples 'issuable record that supports quick actions in its description and notes' do |issuable_type|
-  include QuickActionsHelpers
+  include Spec::Support::Helpers::Features::NotesHelpers
 
   let(:master) { create(:user) }
   let(:project) do
@@ -61,7 +61,7 @@ shared_examples 'issuable record that supports quick actions in its description
     context 'with a note containing commands' do
       it 'creates a note without the commands and interpret the commands accordingly' do
         assignee = create(:user, username: 'bob')
-        write_note("Awesome!\n\n/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
+        add_note("Awesome!\n\n/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
 
         expect(page).to have_content 'Awesome!'
         expect(page).not_to have_content '/assign @bob'
@@ -82,7 +82,7 @@ shared_examples 'issuable record that supports quick actions in its description
     context 'with a note containing only commands' do
       it 'does not create a note but interpret the commands accordingly' do
         assignee = create(:user, username: 'bob')
-        write_note("/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
+        add_note("/assign @bob\n\n/label ~bug\n\n/milestone %\"ASAP\"")
 
         expect(page).not_to have_content '/assign @bob'
         expect(page).not_to have_content '/label ~bug'
@@ -105,7 +105,7 @@ shared_examples 'issuable record that supports quick actions in its description
 
       context "when current user can close #{issuable_type}" do
         it "closes the #{issuable_type}" do
-          write_note("/close")
+          add_note("/close")
 
           expect(page).not_to have_content '/close'
           expect(page).to have_content 'Commands applied'
@@ -125,7 +125,7 @@ shared_examples 'issuable record that supports quick actions in its description
         end
 
         it "does not close the #{issuable_type}" do
-          write_note("/close")
+          add_note("/close")
 
           expect(page).not_to have_content 'Commands applied'
 
@@ -142,7 +142,7 @@ shared_examples 'issuable record that supports quick actions in its description
 
       context "when current user can reopen #{issuable_type}" do
         it "reopens the #{issuable_type}" do
-          write_note("/reopen")
+          add_note("/reopen")
 
           expect(page).not_to have_content '/reopen'
           expect(page).to have_content 'Commands applied'
@@ -162,7 +162,7 @@ shared_examples 'issuable record that supports quick actions in its description
         end
 
         it "does not reopen the #{issuable_type}" do
-          write_note("/reopen")
+          add_note("/reopen")
 
           expect(page).not_to have_content 'Commands applied'
 
@@ -174,7 +174,7 @@ shared_examples 'issuable record that supports quick actions in its description
     context "with a note changing the #{issuable_type}'s title" do
       context "when current user can change title of #{issuable_type}" do
         it "reopens the #{issuable_type}" do
-          write_note("/title Awesome new title")
+          add_note("/title Awesome new title")
 
           expect(page).not_to have_content '/title'
           expect(page).to have_content 'Commands applied'
@@ -194,7 +194,7 @@ shared_examples 'issuable record that supports quick actions in its description
         end
 
         it "does not change the #{issuable_type} title" do
-          write_note("/title Awesome new title")
+          add_note("/title Awesome new title")
 
           expect(page).not_to have_content 'Commands applied'
 
@@ -205,7 +205,7 @@ shared_examples 'issuable record that supports quick actions in its description
 
     context "with a note marking the #{issuable_type} as todo" do
       it "creates a new todo for the #{issuable_type}" do
-        write_note("/todo")
+        add_note("/todo")
 
         expect(page).not_to have_content '/todo'
         expect(page).to have_content 'Commands applied'
@@ -236,7 +236,7 @@ shared_examples 'issuable record that supports quick actions in its description
         expect(todo.author).to eq master
         expect(todo.user).to eq master
 
-        write_note("/done")
+        add_note("/done")
 
         expect(page).not_to have_content '/done'
         expect(page).to have_content 'Commands applied'
@@ -249,7 +249,7 @@ shared_examples 'issuable record that supports quick actions in its description
       it "creates a new todo for the #{issuable_type}" do
         expect(issuable.subscribed?(master, project)).to be_falsy
 
-        write_note("/subscribe")
+        add_note("/subscribe")
 
         expect(page).not_to have_content '/subscribe'
         expect(page).to have_content 'Commands applied'
@@ -266,7 +266,7 @@ shared_examples 'issuable record that supports quick actions in its description
       it "creates a new todo for the #{issuable_type}" do
         expect(issuable.subscribed?(master, project)).to be_truthy
 
-        write_note("/unsubscribe")
+        add_note("/unsubscribe")
 
         expect(page).not_to have_content '/unsubscribe'
         expect(page).to have_content 'Commands applied'
@@ -277,7 +277,7 @@ shared_examples 'issuable record that supports quick actions in its description
 
     context "with a note assigning the #{issuable_type} to the current user" do
       it "assigns the #{issuable_type} to the current user" do
-        write_note("/assign me")
+        add_note("/assign me")
 
         expect(page).not_to have_content '/assign me'
         expect(page).to have_content 'Commands applied'
diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb
index f3f96bd1f0a01eb97869f2eab22a3e0807cd89dc..5f42ff77fb23a7f1fc84e5ad271187435be2df59 100644
--- a/spec/support/filtered_search_helpers.rb
+++ b/spec/support/filtered_search_helpers.rb
@@ -21,6 +21,29 @@ module FilteredSearchHelpers
     end
   end
 
+  # Select a label clicking in the search dropdown instead
+  # of entering label names on the input.
+  def select_label_on_dropdown(label_title)
+    input_filtered_search("label:", submit: false)
+
+    within('#js-dropdown-label') do
+      wait_for_requests
+
+      find('li', text: label_title).click
+    end
+
+    filtered_search.send_keys(:enter)
+  end
+
+  def expect_issues_list_count(open_count, closed_count = 0)
+    all_count = open_count + closed_count
+
+    expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count)
+    page.within '.issues-list' do
+      expect(page).to have_selector('.issue', count: open_count)
+    end
+  end
+
   # Enables input to be added character by character
   def input_filtered_search_keys(search_term)
     # Add an extra space to engage visual tokens
diff --git a/spec/support/helpers/features/notes_helpers.rb b/spec/support/helpers/features/notes_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1a1d5853a7a7929a6d91bf174b7e0a29fd49a5b1
--- /dev/null
+++ b/spec/support/helpers/features/notes_helpers.rb
@@ -0,0 +1,27 @@
+# These helpers allow you to manipulate with notes.
+#
+# Usage:
+#   describe "..." do
+#     include Spec::Support::Helpers::Features::NotesHelpers
+#     ...
+#
+#     add_note("Hello world!")
+#
+module Spec
+  module Support
+    module Helpers
+      module Features
+        module NotesHelpers
+          def add_note(text)
+            Sidekiq::Testing.fake! do
+              page.within(".js-main-target-form") do
+                fill_in("note[note]", with: text)
+                find(".js-comment-submit-button").click
+              end
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/support/helpers/features/sorting_helpers.rb b/spec/support/helpers/features/sorting_helpers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..50457b647455b5d14fcc2c5d9bc0d1cfc12c2e92
--- /dev/null
+++ b/spec/support/helpers/features/sorting_helpers.rb
@@ -0,0 +1,26 @@
+# These helpers allow you to manipulate with sorting features.
+#
+# Usage:
+#   describe "..." do
+#     include Spec::Support::Helpers::Features::SortingHelpers
+#     ...
+#
+#     sort_by("Last updated")
+#
+module Spec
+  module Support
+    module Helpers
+      module Features
+        module SortingHelpers
+          def sort_by(value)
+            find('button.dropdown-toggle').click
+
+            page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
+              click_link(value)
+            end
+          end
+        end
+      end
+    end
+  end
+end
diff --git a/spec/support/ldap_helpers.rb b/spec/support/ldap_helpers.rb
index 081ce0ad7b76cb11bd0f08eca59e435e713acc8b..0e87b3d359dd3efab03431b9fce399ad88400754 100644
--- a/spec/support/ldap_helpers.rb
+++ b/spec/support/ldap_helpers.rb
@@ -41,4 +41,9 @@ module LdapHelpers
 
     entry
   end
+
+  def raise_ldap_connection_error
+    allow_any_instance_of(Gitlab::Auth::LDAP::Adapter)
+      .to receive(:ldap_search).and_raise(Gitlab::Auth::LDAP::LDAPConnectionError)
+  end
 end
diff --git a/spec/support/matchers/issuable_matchers.rb b/spec/support/matchers/issuable_matchers.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f5d9a97051ae0c9a2bbd8a42ca1ae88bc62fffd0
--- /dev/null
+++ b/spec/support/matchers/issuable_matchers.rb
@@ -0,0 +1,11 @@
+RSpec::Matchers.define :have_header_with_correct_id_and_link do |level, text, id, parent = ".wiki"|
+  match do |actual|
+    node = find("#{parent} h#{level} a#user-content-#{id}")
+
+    expect(node[:href]).to end_with("##{id}")
+
+    # Work around a weird Capybara behavior where calling `parent` on a node
+    # returns the whole document, not the node's actual parent element
+    expect(find(:xpath, "#{node.path}/..").text).to eq(text)
+  end
+end
diff --git a/spec/support/quick_actions_helpers.rb b/spec/support/quick_actions_helpers.rb
deleted file mode 100644
index 361190aa3521ed0ef2999b96b4c8d3facfbcc251..0000000000000000000000000000000000000000
--- a/spec/support/quick_actions_helpers.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-module QuickActionsHelpers
-  def write_note(text)
-    Sidekiq::Testing.fake! do
-      page.within('.js-main-target-form') do
-        fill_in 'note[note]', with: text
-        find('.js-comment-submit-button').click
-      end
-    end
-  end
-end
diff --git a/spec/support/shared_examples/serializers/note_entity_examples.rb b/spec/support/shared_examples/serializers/note_entity_examples.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9097c8e5513aca4deb9c86f209589e0e4bb303c8
--- /dev/null
+++ b/spec/support/shared_examples/serializers/note_entity_examples.rb
@@ -0,0 +1,42 @@
+shared_examples 'note entity' do
+  subject { entity.as_json }
+
+  context 'basic note' do
+    it 'exposes correct elements' do
+      expect(subject).to include(:type, :author, :note, :note_html, :current_user,
+        :discussion_id, :emoji_awardable, :award_emoji, :report_abuse_path, :attachment)
+    end
+
+    it 'does not expose elements for specific notes cases' do
+      expect(subject).not_to include(:last_edited_by, :last_edited_at, :system_note_icon_name)
+    end
+
+    it 'exposes author correctly' do
+      expect(subject[:author]).to include(:id, :name, :username, :state, :avatar_url, :path)
+    end
+
+    it 'does not expose web_url for author' do
+      expect(subject[:author]).not_to include(:web_url)
+    end
+  end
+
+  context 'when note was edited' do
+    before do
+      note.update(updated_at: 1.minute.from_now, updated_by: user)
+    end
+
+    it 'exposes last_edited_at and last_edited_by elements' do
+      expect(subject).to include(:last_edited_at, :last_edited_by)
+    end
+  end
+
+  context 'when note is a system note' do
+    before do
+      note.update(system: true)
+    end
+
+    it 'exposes system_note_icon_name element' do
+      expect(subject).to include(:system_note_icon_name)
+    end
+  end
+end
diff --git a/spec/support/shared_examples/services/boards/issues_move_service.rb b/spec/support/shared_examples/services/boards/issues_move_service.rb
index 4a4fbaa3a0e827fe586edfc3eae13fa3c56d41e1..737863ea41126cddd6881b0475449ae492896dbf 100644
--- a/spec/support/shared_examples/services/boards/issues_move_service.rb
+++ b/spec/support/shared_examples/services/boards/issues_move_service.rb
@@ -1,4 +1,4 @@
-shared_examples 'issues move service' do
+shared_examples 'issues move service' do |group|
   context 'when moving an issue between lists' do
     let(:issue)  { create(:labeled_issue, project: project, labels: [bug, development]) }
     let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } }
@@ -83,5 +83,18 @@ shared_examples 'issues move service' do
 
       expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
     end
+
+    if group
+      context 'when on a group board' do
+        it 'sends the board_group_id parameter' do
+          params.merge!(move_after_id: issue1.id, move_before_id: issue2.id)
+
+          match_params = { move_between_ids: [issue1.id, issue2.id], board_group_id: parent.id }
+          expect(Issues::UpdateService).to receive(:new).with(issue.project, user, match_params).and_return(double(execute: build(:issue)))
+
+          described_class.new(parent, user, params).execute(issue)
+        end
+      end
+    end
   end
 end
diff --git a/spec/support/sorting_helper.rb b/spec/support/sorting_helper.rb
deleted file mode 100644
index 577518d726c70dda1bad1101c69677f734694286..0000000000000000000000000000000000000000
--- a/spec/support/sorting_helper.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# Helper allows you to sort items
-#
-# Params
-#   value - value for sorting
-#
-# Usage:
-#   include SortingHelper
-#
-#   sorting_by('Oldest updated')
-#
-module SortingHelper
-  def sorting_by(value)
-    find('button.dropdown-toggle').click
-    page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
-      click_link value
-    end
-  end
-end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index f14e69b104118ed7d9433afface65d6440b1d4ef..d87f265cdf0d1aebadbb1a4ecf69d0644064abc7 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -62,6 +62,7 @@ module TestEnv
   }.freeze
 
   TMP_TEST_PATH = Rails.root.join('tmp', 'tests', '**')
+  REPOS_STORAGE = 'default'.freeze
 
   # Test environment
   #
@@ -225,7 +226,7 @@ module TestEnv
   end
 
   def repos_path
-    Gitlab.config.repositories.storages.default.legacy_disk_path
+    Gitlab.config.repositories.storages[REPOS_STORAGE].legacy_disk_path
   end
 
   def backup_path
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 6c66658d8c323b73396bdfea9301b7554be0992f..4b3c1736ea05fe7277140b46d6da8cc9db713d3b 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -9,70 +9,91 @@ describe RepositoryForkWorker do
 
   describe "#perform" do
     let(:project) { create(:project, :repository) }
-    let(:fork_project) { create(:project, :repository, :import_scheduled, forked_from_project: project) }
     let(:shell) { Gitlab::Shell.new }
+    let(:fork_project) { create(:project, :repository, :import_scheduled, forked_from_project: project) }
 
-    before do
-      allow(subject).to receive(:gitlab_shell).and_return(shell)
-    end
+    shared_examples 'RepositoryForkWorker performing' do
+      before do
+        allow(subject).to receive(:gitlab_shell).and_return(shell)
+      end
 
-    def perform!
-      subject.perform(fork_project.id, '/test/path', project.disk_path)
-    end
+      def expect_fork_repository
+        expect(shell).to receive(:fork_repository).with(
+          'default',
+          project.disk_path,
+          fork_project.repository_storage,
+          fork_project.disk_path
+        )
+      end
 
-    def expect_fork_repository
-      expect(shell).to receive(:fork_repository).with(
-        '/test/path',
-        project.disk_path,
-        fork_project.repository_storage_path,
-        fork_project.disk_path
-      )
-    end
+      describe 'when a worker was reset without cleanup' do
+        let(:jid) { '12345678' }
 
-    describe 'when a worker was reset without cleanup' do
-      let(:jid) { '12345678' }
+        it 'creates a new repository from a fork' do
+          allow(subject).to receive(:jid).and_return(jid)
 
-      it 'creates a new repository from a fork' do
-        allow(subject).to receive(:jid).and_return(jid)
+          expect_fork_repository.and_return(true)
 
+          perform!
+        end
+      end
+
+      it "creates a new repository from a fork" do
         expect_fork_repository.and_return(true)
 
         perform!
       end
-    end
 
-    it "creates a new repository from a fork" do
-      expect_fork_repository.and_return(true)
+      it 'protects the default branch' do
+        expect_fork_repository.and_return(true)
 
-      perform!
-    end
+        perform!
+
+        expect(fork_project.protected_branches.first.name).to eq(fork_project.default_branch)
+      end
+
+      it 'flushes various caches' do
+        expect_fork_repository.and_return(true)
 
-    it 'protects the default branch' do
-      expect_fork_repository.and_return(true)
+        expect_any_instance_of(Repository).to receive(:expire_emptiness_caches)
+          .and_call_original
 
-      perform!
+        expect_any_instance_of(Repository).to receive(:expire_exists_cache)
+          .and_call_original
 
-      expect(fork_project.protected_branches.first.name).to eq(fork_project.default_branch)
-    end
+        perform!
+      end
+
+      it "handles bad fork" do
+        error_message = "Unable to fork project #{fork_project.id} for repository #{project.disk_path} -> #{fork_project.disk_path}"
 
-    it 'flushes various caches' do
-      expect_fork_repository.and_return(true)
+        expect_fork_repository.and_return(false)
 
-      expect_any_instance_of(Repository).to receive(:expire_emptiness_caches)
-        .and_call_original
+        expect { perform! }.to raise_error(StandardError, error_message)
+      end
+    end
 
-      expect_any_instance_of(Repository).to receive(:expire_exists_cache)
-        .and_call_original
+    context 'only project ID passed' do
+      def perform!
+        subject.perform(fork_project.id)
+      end
 
-      perform!
+      it_behaves_like 'RepositoryForkWorker performing'
     end
 
-    it "handles bad fork" do
-      error_message = "Unable to fork project #{fork_project.id} for repository #{project.disk_path} -> #{fork_project.disk_path}"
+    context 'project ID, storage and repo paths passed' do
+      def perform!
+        subject.perform(fork_project.id, TestEnv.repos_path, project.disk_path)
+      end
 
-      expect_fork_repository.and_return(false)
+      it_behaves_like 'RepositoryForkWorker performing'
 
-      expect { perform! }.to raise_error(StandardError, error_message)
+      it 'logs a message about forking with old-style arguments' do
+        allow(Rails.logger).to receive(:info).with(anything) # To compensate for other logs
+        expect(Rails.logger).to receive(:info).with("Project #{fork_project.id} is being forked using old-style arguments.")
+
+        perform!
+      end
     end
   end
 end