diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000000000000000000000000000000000000..ee4c391da300adaaa94814b165e94234915bc2a1
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,21 @@
+{
+  "presets": [
+    ["latest", { "es2015": { "modules": false } }],
+    "stage-2"
+  ],
+  "env": {
+    "coverage": {
+      "plugins": [
+        ["istanbul", {
+          "exclude": [
+            "app/assets/javascripts/droplab/**/*",
+            "spec/javascripts/**/*"
+          ]
+        }],
+        ["transform-define", {
+          "process.env.BABEL_ENV": "coverage"
+        }]
+      ]
+    }
+  }
+}
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 080d8cd6c7f82ccd4744f82c79e74fc23451bf30..34c10b3b77fb95992623558422c332fbf5683325 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -277,6 +277,8 @@ rake karma:
   stage: test
   <<: *use-db
   <<: *dedicated-runner
+  variables:
+    BABEL_ENV: "coverage"
   script:
     - bundle exec rake karma
   artifacts:
@@ -389,9 +391,11 @@ trigger_docs:
   cache: {}
   artifacts: {}
   script:
-    - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/1794617/trigger/builds"
+    - "HTTP_STATUS=$(curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=${CI_PROJECT_NAME} --silent --output curl.log --write-out '%{http_code}' https://gitlab.com/api/v3/projects/1794617/trigger/builds)"
+    - if [ "${HTTP_STATUS}" -ne "201" ]; then echo "Error ${HTTP_STATUS}"; cat curl.log; echo; exit 1; fi
   only:
     - master@gitlab-org/gitlab-ce
+    - master@gitlab-org/gitlab-ee
 
 # Notify slack in the end
 notify:slack:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 42e094bdfc6cc8189daa882b733555eeb52779ce..da1898e37707609c001cc6b38d0f30e2b8fb0072 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,11 @@
 documentation](doc/development/changelog.md) for instructions on adding your own
 entry.
 
+## 8.17.4 (2017-03-19)
+
+- Only show public emails in atom feeds.
+- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+
 ## 8.17.3 (2017-03-07)
 
 - Fix the redirect to custom home page URL. !9518
@@ -210,6 +215,11 @@ entry.
 - Remove deprecated GitlabCiService.
 - Requeue pending deletion projects.
 
+## 8.16.8 (2017-03-19)
+
+- Only show public emails in atom feeds.
+- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+
 ## 8.16.7 (2017-02-27)
 
 - No changes.
@@ -411,6 +421,11 @@ entry.
 - Add margin to markdown math blocks.
 - Add hover state to MR comment reply button.
 
+## 8.15.8 (2017-03-19)
+
+- Only show public emails in atom feeds.
+- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+
 ## 8.15.7 (2017-02-15)
 
 - No changes.
diff --git a/Gemfile b/Gemfile
index 2f813324a35f81eb9b28ff47ea0989beba5fab8e..6af27ce0f3efd306bedbcb5bc8a10ef0c44b6bde 100644
--- a/Gemfile
+++ b/Gemfile
@@ -352,4 +352,4 @@ gem 'vmstat', '~> 2.3.0'
 gem 'sys-filesystem', '~> 1.1.6'
 
 # Gitaly GRPC client
-gem 'gitaly', '~> 0.2.1'
+gem 'gitaly', '~> 0.3.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index c60c045a4c27328f13fe510c9c4fc262babe5952..043ca4f8800cf116ddd25fc31fc85949f7ed457e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -250,7 +250,7 @@ GEM
       json
     get_process_mem (0.2.0)
     gherkin-ruby (0.3.2)
-    gitaly (0.2.1)
+    gitaly (0.3.0)
       google-protobuf (~> 3.1)
       grpc (~> 1.0)
     github-linguist (4.7.6)
@@ -304,7 +304,7 @@ GEM
       multi_json (~> 1.10)
       retriable (~> 1.4)
       signet (~> 0.6)
-    google-protobuf (3.2.0)
+    google-protobuf (3.2.0.2)
     googleauth (0.5.1)
       faraday (~> 0.9)
       jwt (~> 1.4)
@@ -896,7 +896,7 @@ DEPENDENCIES
   fuubar (~> 2.0.0)
   gemnasium-gitlab-service (~> 0.2)
   gemojione (~> 3.0)
-  gitaly (~> 0.2.1)
+  gitaly (~> 0.3.0)
   github-linguist (~> 4.7.0)
   gitlab-flowdock-git-hook (~> 1.0.1)
   gitlab-markup (~> 1.5.1)
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
index 9538f5b69e9300eb92653a0819bd26c5e67bcd32..e6973c3fd599e1659756d7ac119061a7093a223c 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -30,7 +30,7 @@
         if (this.activeTab === 'selected') {
           obj.title = 'You haven\'t selected any issues yet';
           obj.content = `
-            Go back to <strong>All issues</strong> and select some issues
+            Go back to <strong>Open issues</strong> and select some issues
             to add to your board.
           `;
         }
@@ -59,7 +59,7 @@
                 class="btn btn-default"
                 @click="changeTab('all')"
                 v-if="activeTab === 'selected'">
-                All issues
+                Open issues
               </button>
             </div>
           </div>
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index 1b66c8b922dbc8a212c15965a4c29b48b392a357..4240c97617d14e8a7711b337beaf175c19fea5d8 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -64,6 +64,7 @@ require('./empty_state');
       },
       filter: {
         handler() {
+          this.page = 1;
           this.loadIssues(true);
         },
         deep: true,
@@ -115,6 +116,9 @@ require('./empty_state');
         return this.activeTab === 'selected' && this.selectedIssues.length === 0;
       },
     },
+    created() {
+      this.page = 1;
+    },
     components: {
       'modal-header': gl.issueBoards.ModalHeader,
       'modal-list': gl.issueBoards.ModalList,
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
index e8cb43f350338eba116834fcee8a643788626f71..1cd6ca0ee88952ae9c66d4bc1dd746ec13c7b4fb 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.js
+++ b/app/assets/javascripts/boards/components/modal/tabs.js
@@ -23,7 +23,7 @@
               href="#"
               role="button"
               @click.prevent="changeTab('all')">
-              All issues
+              Open issues
               <span class="badge">
                 {{ issuesCount }}
               </span>
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index b5a988df8975eeceda684c48c992573fc2b3fe3d..a9f2d462c31092c612df50193c44c1a62f64becc 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -1,8 +1,9 @@
-/* eslint-disable no-new, no-param-reassign */
-/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
+/* eslint-disable no-param-reassign */
+import CommitPipelinesTable from './pipelines_table';
 
 window.Vue = require('vue');
-require('./pipelines_table');
+window.Vue.use(require('vue-resource'));
+
 /**
  * Commits View > Pipelines Tab > Pipelines Table.
  * Merge Request View > Pipelines Tab > Pipelines Table.
@@ -21,7 +22,7 @@ $(() => {
   }
 
   const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
-  gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView();
+  gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable();
 
   if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
     gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_service.js b/app/assets/javascripts/commit/pipelines/pipelines_service.js
deleted file mode 100644
index 8ae98f9bf978fd95d197109f4c80e054227c2561..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/commit/pipelines/pipelines_service.js
+++ /dev/null
@@ -1,44 +0,0 @@
-/* globals Vue */
-/* eslint-disable no-unused-vars, no-param-reassign */
-
-/**
- * Pipelines service.
- *
- * Used to fetch the data used to render the pipelines table.
- * Uses Vue.Resource
- */
-class PipelinesService {
-
-  /**
-   * FIXME: The url provided to request the pipelines in the new merge request
-   * page already has `.json`.
-   * This should be fixed when the endpoint is improved.
-   *
-   * @param  {String} root
-   */
-  constructor(root) {
-    let endpoint;
-
-    if (root.indexOf('.json') === -1) {
-      endpoint = `${root}.json`;
-    } else {
-      endpoint = root;
-    }
-    this.pipelines = Vue.resource(endpoint);
-  }
-
-  /**
-   * Given the root param provided when the class is initialized, will
-   * make a GET request.
-   *
-   * @return {Promise}
-   */
-  all() {
-    return this.pipelines.get();
-  }
-}
-
-window.gl = window.gl || {};
-gl.commits = gl.commits || {};
-gl.commits.pipelines = gl.commits.pipelines || {};
-gl.commits.pipelines.PipelinesService = PipelinesService;
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index 631ed34851c9318fc99264f5b3d4c4ee1a4e7c1b..832c4b1bd2ad1e6b16201766743d557ed228bc4d 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -1,13 +1,12 @@
-/* eslint-disable no-new, no-param-reassign */
-/* global Vue, CommitsPipelineStore, PipelinesService, Flash */
-
-window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-require('../../vue_shared/components/pipelines_table');
-require('./pipelines_service');
-const PipelineStore = require('./pipelines_store');
+/* eslint-disable no-new*/
+/* global Flash */
+import Vue from 'vue';
+import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
+import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
+import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
+import eventHub from '../../vue_pipelines_index/event_hub';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
 
 /**
  *
@@ -20,48 +19,59 @@ const PipelineStore = require('./pipelines_store');
  * as soon as we have Webpack and can load them directly into JS files.
  */
 
-(() => {
-  window.gl = window.gl || {};
-  gl.commits = gl.commits || {};
-  gl.commits.pipelines = gl.commits.pipelines || {};
+export default Vue.component('pipelines-table', {
+  components: {
+    'pipelines-table-component': PipelinesTableComponent,
+  },
 
-  gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', {
+  /**
+   * Accesses the DOM to provide the needed data.
+   * Returns the necessary props to render `pipelines-table-component` component.
+   *
+   * @return {Object}
+   */
+  data() {
+    const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
+    const store = new PipelineStore();
 
-    components: {
-      'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
-    },
+    return {
+      endpoint: pipelinesTableData.endpoint,
+      store,
+      state: store.state,
+      isLoading: false,
+    };
+  },
 
-    /**
-     * Accesses the DOM to provide the needed data.
-     * Returns the necessary props to render `pipelines-table-component` component.
-     *
-     * @return {Object}
-     */
-    data() {
-      const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
-      const store = new PipelineStore();
+  /**
+   * When the component is about to be mounted, tell the service to fetch the data
+   *
+   * A request to fetch the pipelines will be made.
+   * In case of a successfull response we will store the data in the provided
+   * store, in case of a failed response we need to warn the user.
+   *
+   */
+  beforeMount() {
+    this.service = new PipelinesService(this.endpoint);
 
-      return {
-        endpoint: pipelinesTableData.endpoint,
-        store,
-        state: store.state,
-        isLoading: false,
-      };
-    },
+    this.fetchPipelines();
+
+    eventHub.$on('refreshPipelines', this.fetchPipelines);
+  },
+
+  beforeUpdate() {
+    if (this.state.pipelines.length && this.$children) {
+      this.store.startTimeAgoLoops.call(this, Vue);
+    }
+  },
 
-    /**
-     * When the component is about to be mounted, tell the service to fetch the data
-     *
-     * A request to fetch the pipelines will be made.
-     * In case of a successfull response we will store the data in the provided
-     * store, in case of a failed response we need to warn the user.
-     *
-     */
-    beforeMount() {
-      const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
+  beforeDestroyed() {
+    eventHub.$off('refreshPipelines');
+  },
 
+  methods: {
+    fetchPipelines() {
       this.isLoading = true;
-      return pipelinesService.all()
+      return this.service.getPipelines()
         .then(response => response.json())
         .then((json) => {
           // depending of the endpoint the response can either bring a `pipelines` key or not.
@@ -71,34 +81,30 @@ const PipelineStore = require('./pipelines_store');
         })
         .catch(() => {
           this.isLoading = false;
-          new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert');
+          new Flash('An error occurred while fetching the pipelines, please reload the page again.');
         });
     },
+  },
 
-    beforeUpdate() {
-      if (this.state.pipelines.length && this.$children) {
-        PipelineStore.startTimeAgoLoops.call(this, Vue);
-      }
-    },
-
-    template: `
-      <div class="pipelines">
-        <div class="realtime-loading" v-if="isLoading">
-          <i class="fa fa-spinner fa-spin"></i>
-        </div>
+  template: `
+    <div class="pipelines">
+      <div class="realtime-loading" v-if="isLoading">
+        <i class="fa fa-spinner fa-spin"></i>
+      </div>
 
-        <div class="blank-state blank-state-no-icon"
-          v-if="!isLoading && state.pipelines.length === 0">
-          <h2 class="blank-state-title js-blank-state-title">
-            No pipelines to show
-          </h2>
-        </div>
+      <div class="blank-state blank-state-no-icon"
+        v-if="!isLoading && state.pipelines.length === 0">
+        <h2 class="blank-state-title js-blank-state-title">
+          No pipelines to show
+        </h2>
+      </div>
 
-        <div class="table-holder pipelines"
-          v-if="!isLoading && state.pipelines.length > 0">
-          <pipelines-table-component :pipelines="state.pipelines"/>
-        </div>
+      <div class="table-holder pipelines"
+        v-if="!isLoading && state.pipelines.length > 0">
+        <pipelines-table-component
+          :pipelines="state.pipelines"
+          :service="service" />
       </div>
-    `,
-  });
-})();
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
index 788daa96b3d3911bb5162a73f05957544ec6c675..dd7081aefb7bb79bf1f26ff87ea7ee6f04add0a0 100644
--- a/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
+++ b/app/assets/javascripts/diff_notes/components/diff_note_avatars.js
@@ -25,6 +25,7 @@ import collapseIcon from '../icons/collapse_icon.svg';
             role="button"
             data-container="body"
             data-placement="top"
+            data-html="true"
             :data-line-type="lineType"
             :title="note.authorName + ': ' + note.noteTruncated"
             :src="note.authorAvatar"
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index db1a2848d8d86ba246bb500a62fad538b4ec4944..3557f6f617ed64dcbcf7c21a09e7e4a1aa0774ee 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -1,4 +1,3 @@
-import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make this a bundle
 /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
 /* global UsernameValidator */
 /* global ActiveTabMemoizer */
@@ -329,8 +328,6 @@ const UserCallout = require('./user_callout');
         case 'ci:lints:show':
           new gl.CILintEditor();
           break;
-        case 'projects:environments:metrics':
-          new PrometheusGraph();
         case 'users:show':
           new UserCallout();
           break;
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index fdbb4644971188c0980ae075c3286c76354dcf2c..db10b3839138eae4b4f566cd296602a3a572ca0b 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -132,7 +132,7 @@ class DueDateSelect {
         const selectedDateValue = this.datePayload[this.abilityName].due_date;
         const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
 
-        this.$loading.fadeIn();
+        this.$loading.removeClass('hidden').fadeIn();
 
         if (isDropdown) {
           this.$dropdown.trigger('loading.gl.dropdown');
diff --git a/app/assets/javascripts/environments/components/environment.js b/app/assets/javascripts/environments/components/environment.js
index 0923ce6b550fac7d618ebee93590b66650386505..51aab8460f6e16dd6a470bb3c1620e6088446564 100644
--- a/app/assets/javascripts/environments/components/environment.js
+++ b/app/assets/javascripts/environments/components/environment.js
@@ -1,21 +1,18 @@
-/* eslint-disable no-param-reassign, no-new */
+/* eslint-disable no-new */
 /* global Flash */
+import Vue from 'vue';
 import EnvironmentsService from '../services/environments_service';
 import EnvironmentTable from './environments_table';
 import EnvironmentsStore from '../stores/environments_store';
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
 import eventHub from '../event_hub';
 
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
-
 export default Vue.component('environment-component', {
 
   components: {
     'environment-table': EnvironmentTable,
-    'table-pagination': gl.VueGlPagination,
+    'table-pagination': TablePaginationComponent,
   },
 
   data() {
@@ -59,7 +56,6 @@ export default Vue.component('environment-component', {
     canCreateEnvironmentParsed() {
       return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
     },
-
   },
 
   /**
diff --git a/app/assets/javascripts/environments/components/environment_external_url.js b/app/assets/javascripts/environments/components/environment_external_url.js
index a554998f52c76400602b4bccad02bc43a3d6fe60..b4f9eb357fde7fc82bb96ae057cdc9ccaa88ec97 100644
--- a/app/assets/javascripts/environments/components/environment_external_url.js
+++ b/app/assets/javascripts/environments/components/environment_external_url.js
@@ -14,6 +14,7 @@ export default {
       class="btn external_url"
       :href="externalUrl"
       target="_blank"
+      rel="noopener noreferrer"
       title="Environment external URL">
       <i class="fa fa-external-link" aria-hidden="true"></i>
     </a>
diff --git a/app/assets/javascripts/environments/components/environment_item.js b/app/assets/javascripts/environments/components/environment_item.js
index 93919d41c6009cc00792e5f5249b6a2635356182..66ed10e19d12f9c80b50a17b70323f5e92bc47cf 100644
--- a/app/assets/javascripts/environments/components/environment_item.js
+++ b/app/assets/javascripts/environments/components/environment_item.js
@@ -1,24 +1,22 @@
 import Timeago from 'timeago.js';
+import '../../lib/utils/text_utility';
 import ActionsComponent from './environment_actions';
 import ExternalUrlComponent from './environment_external_url';
 import StopComponent from './environment_stop';
 import RollbackComponent from './environment_rollback';
 import TerminalButtonComponent from './environment_terminal_button';
-import '../../lib/utils/text_utility';
-import '../../vue_shared/components/commit';
+import CommitComponent from '../../vue_shared/components/commit';
 
 /**
  * Envrionment Item Component
  *
  * Renders a table row for each environment.
  */
-
 const timeagoInstance = new Timeago();
 
 export default {
-
   components: {
-    'commit-component': gl.CommitComponent,
+    'commit-component': CommitComponent,
     'actions-component': ActionsComponent,
     'external-url-component': ExternalUrlComponent,
     'stop-component': StopComponent,
diff --git a/app/assets/javascripts/environments/components/environments_table.js b/app/assets/javascripts/environments/components/environments_table.js
index 5f07b612b915f87c590c8c5863a1d5c6008526d0..338dff40bc986982e5604d53435ac37bbf4e409d 100644
--- a/app/assets/javascripts/environments/components/environments_table.js
+++ b/app/assets/javascripts/environments/components/environments_table.js
@@ -1,11 +1,11 @@
 /**
  * Render environments table.
  */
-import EnvironmentItem from './environment_item';
+import EnvironmentTableRowComponent from './environment_item';
 
 export default {
   components: {
-    'environment-item': EnvironmentItem,
+    'environment-item': EnvironmentTableRowComponent,
   },
 
   props: {
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js b/app/assets/javascripts/environments/folder/environments_folder_view.js
index 7abcf6dbbea79fe62e3297cac3a3deafd6837fd3..8abbcf0c227f20ecf4b1b2929bda18ddcee705ea 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.js
@@ -1,20 +1,17 @@
-/* eslint-disable no-param-reassign, no-new */
+/* eslint-disable no-new */
 /* global Flash */
+import Vue from 'vue';
 import EnvironmentsService from '../services/environments_service';
 import EnvironmentTable from '../components/environments_table';
 import EnvironmentsStore from '../stores/environments_store';
-
-const Vue = window.Vue = require('vue');
-window.Vue.use(require('vue-resource'));
-require('../../vue_shared/components/table_pagination');
-require('../../lib/utils/common_utils');
-require('../../vue_shared/vue_resource_interceptor');
+import TablePaginationComponent from '../../vue_shared/components/table_pagination';
+import '../../lib/utils/common_utils';
+import '../../vue_shared/vue_resource_interceptor';
 
 export default Vue.component('environment-folder-view', {
-
   components: {
     'environment-table': EnvironmentTable,
-    'table-pagination': gl.VueGlPagination,
+    'table-pagination': TablePaginationComponent,
   },
 
   data() {
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 76296c83d113001a420de34def120f461b512465..07040bf0d7374db826fa6e4fe15b969853745250 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -1,5 +1,8 @@
 /* eslint-disable class-methods-use-this */
 import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
 
 export default class EnvironmentsService {
   constructor(endpoint) {
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index d3fe3872c56348521c5d579ed6518223d033590c..3c3084f3b78961d85056b23b7cece66fd3cc9193 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -1,5 +1,4 @@
 import '~/lib/utils/common_utils';
-
 /**
  * Environments Store.
  *
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
index 134bdc6ad80aef288e97c92762d9045da77eb6e9..e7bf530d3430cd63f0937e9bd4fd7faca24f74f2 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown.js
@@ -38,6 +38,7 @@
           gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
         }
 
+        this.resetFilters();
         this.dismissDropdown();
         this.dispatchInputEvent();
       }
@@ -107,7 +108,7 @@
       const hook = this.getCurrentHook();
 
       if (hook) {
-        const data = hook.list.data;
+        const data = hook.list.data || [];
         const results = data.map((o) => {
           const updated = o;
           updated.droplab_hidden = false;
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 7ace51748aa4d7ed242a75bf27b4534f96a50008..c6bb7fda8f21ab04c8c5ea70c778a6649cd0bd12 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -40,6 +40,8 @@ import FilteredSearchContainer from './container';
       this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
       this.editTokenWrapper = this.editToken.bind(this);
       this.tokenChange = this.tokenChange.bind(this);
+      this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
+      this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
 
       this.filteredSearchInputForm = this.filteredSearchInput.form;
       this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
@@ -51,11 +53,13 @@ import FilteredSearchContainer from './container';
       this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
       this.filteredSearchInput.addEventListener('click', this.tokenChange);
       this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
+      this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
       this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
       this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
       this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
       document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
       document.addEventListener('click', this.unselectEditTokensWrapper);
+      document.addEventListener('click', this.removeInputContainerFocusWrapper);
       document.addEventListener('keydown', this.removeSelectedTokenWrapper);
     }
 
@@ -69,11 +73,13 @@ import FilteredSearchContainer from './container';
       this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
       this.filteredSearchInput.removeEventListener('click', this.tokenChange);
       this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
+      this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
       this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
       this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
       this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
       document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
       document.removeEventListener('click', this.unselectEditTokensWrapper);
+      document.removeEventListener('click', this.removeInputContainerFocusWrapper);
       document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
     }
 
@@ -124,6 +130,26 @@ import FilteredSearchContainer from './container';
       }
     }
 
+    addInputContainerFocus() {
+      const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
+
+      if (inputContainer) {
+        inputContainer.classList.add('focus');
+      }
+    }
+
+    removeInputContainerFocus(e) {
+      const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
+      const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
+      const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
+      const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
+
+      if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
+        !isElementInStaticFilterDropdown && inputContainer) {
+        inputContainer.classList.remove('focus');
+      }
+    }
+
     static selectToken(e) {
       const button = e.target.closest('.selectable');
 
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index ef4029a862314b298dd456682a33fd474668965a..47e675f537e94af46e9fea18f774dd8efc271d45 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -2,6 +2,7 @@
 /* global Flash */
 
 require('./flash');
+require('~/lib/utils/text_utility');
 require('vendor/jquery.waitforimages');
 require('./task_list');
 
@@ -50,20 +51,21 @@ class Issue {
         success: function(data, textStatus, jqXHR) {
           if ('id' in data) {
             $(document).trigger('issuable:change');
-            const currentTotal = Number($('.issue_counter').text());
+            let total = Number($('.issue_counter').text().replace(/[^\d]/, ''));
             if (isClose) {
               $('a.btn-close').addClass('hidden');
               $('a.btn-reopen').removeClass('hidden');
               $('div.status-box-closed').removeClass('hidden');
               $('div.status-box-open').addClass('hidden');
-              $('.issue_counter').text(currentTotal - 1);
+              total -= 1;
             } else {
               $('a.btn-reopen').addClass('hidden');
               $('a.btn-close').removeClass('hidden');
               $('div.status-box-closed').addClass('hidden');
               $('div.status-box-open').removeClass('hidden');
-              $('.issue_counter').text(currentTotal + 1);
+              total += 1;
             }
+            $('.issue_counter').text(gl.text.addDelimiter(total));
           } else {
             new Flash(issueFailMessage, 'alert');
           }
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index c648a0f076c45ee73bfa656a8d3a396839b55d56..443fb3e0ca96c183148c2f8f3076b31888495aec 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -76,7 +76,7 @@
           if (!selected.length) {
             data[abilityName].label_ids = [''];
           }
-          $loading.fadeIn();
+          $loading.removeClass('hidden').fadeIn();
           $dropdown.trigger('loading.gl.dropdown');
           return $.ajax({
             type: 'PUT',
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index 94a4f24f1d74347d19fc09c925d740b5a7f8cb3f..0e2af3df071d53c6094e885398876df425af9e49 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -14,13 +14,13 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
          <%= ci_success_icon %>
          <span>
            Deployed to
-           <a href="<%- url %>" target="_blank" class="environment">
+           <a href="<%- url %>" target="_blank" rel="noopener noreferrer" class="environment">
              <%- name %>
            </a>
            <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>">
              <%- deployed_at %>
            </span>
-           <a class="js-environment-link" href="<%- external_url %>" target="_blank">
+           <a class="js-environment-link" href="<%- external_url %>" target="_blank" rel="noopener noreferrer">
              <i class="fa fa-external-link"></i>
              View on <%- external_url_formatted %>
            </a>
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 02ff6f5682c99208c59df15479c0e42870bcceb9..40e977df693e8b9dc098b1f79cc0a32d8f7d9f15 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -159,7 +159,7 @@
               }
 
               $dropdown.trigger('loading.gl.dropdown');
-              $loading.fadeIn();
+              $loading.removeClass('hidden').fadeIn();
 
               gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
                 .then(function () {
@@ -171,7 +171,7 @@
               data = {};
               data[abilityName] = {};
               data[abilityName].milestone_id = selected != null ? selected : null;
-              $loading.fadeIn();
+              $loading.removeClass('hidden').fadeIn();
               $dropdown.trigger('loading.gl.dropdown');
               return $.ajax({
                 type: 'PUT',
diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js
new file mode 100644
index 0000000000000000000000000000000000000000..b3ce931041767f1dcacb761e36a13bc0afbe6196
--- /dev/null
+++ b/app/assets/javascripts/monitoring/monitoring_bundle.js
@@ -0,0 +1,6 @@
+import PrometheusGraph from './prometheus_graph';
+
+document.addEventListener('DOMContentLoaded', function onLoad() {
+  document.removeEventListener('DOMContentLoaded', onLoad, false);
+  return new PrometheusGraph();
+}, false);
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
index 71eb746edac26b9c9dbef0cc2eefd8b2a9e1760d..fcffc11a2df0e8b411f54d370724f52c023d0133 100644
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -2,10 +2,9 @@
 /* global Flash */
 
 import d3 from 'd3';
-import _ from 'underscore';
 import statusCodes from '~/lib/utils/http_status';
-import '~/lib/utils/common_utils';
-import '~/flash';
+import '../lib/utils/common_utils';
+import '../flash';
 
 const prometheusGraphsContainer = '.prometheus-graph';
 const metricsEndpoint = 'metrics.json';
@@ -31,22 +30,21 @@ class PrometheusGraph {
   }
 
   createGraph() {
-    const self = this;
-    _.each(this.data, (value, key) => {
-      if (value.length > 0 && (key === 'cpu_values' || key === 'memory_values')) {
-        self.plotValues(value, key);
+    Object.keys(this.data).forEach((key) => {
+      const value = this.data[key];
+      if (value.length > 0) {
+        this.plotValues(value, key);
       }
     });
   }
 
   init() {
-    const self = this;
     this.getData().then((metricsResponse) => {
-      if (metricsResponse === {}) {
+      if (Object.keys(metricsResponse).length === 0) {
         new Flash('Empty metrics', 'alert');
       } else {
-        self.transformData(metricsResponse);
-        self.createGraph();
+        this.transformData(metricsResponse);
+        this.createGraph();
       }
     });
   }
@@ -321,12 +319,14 @@ class PrometheusGraph {
 
   transformData(metricsResponse) {
     const metricTypes = {};
-    _.each(metricsResponse.metrics, (value, key) => {
-      const metricValues = value[0].values;
-      metricTypes[key] = _.map(metricValues, metric => ({
-        time: new Date(metric[0] * 1000),
-        value: metric[1],
-      }));
+    Object.keys(metricsResponse.metrics).forEach((key) => {
+      if (key === 'cpu_values' || key === 'memory_values') {
+        const metricValues = (metricsResponse.metrics[key])[0];
+        metricTypes[key] = metricValues.values.map(metric => ({
+          time: new Date(metric[0] * 1000),
+          value: metric[1],
+        }));
+      }
     });
     this.data = metricTypes;
   }
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index c7a57b47834af55d2e065fec7d5472bef3015be6..eb897e9dfe9acfdca59e2dc0529789704ccb3f44 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -53,7 +53,7 @@
           $loading = $block.find('.block-loading').fadeOut();
 
           var updateIssueBoardsIssue = function () {
-            $loading.fadeIn();
+            $loading.removeClass('hidden').fadeIn();
             gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
               .then(function () {
                 $loading.fadeOut();
@@ -90,7 +90,7 @@
             data = {};
             data[abilityName] = {};
             data[abilityName].assignee_id = selected != null ? selected : null;
-            $loading.fadeIn();
+            $loading.removeClass('hidden').fadeIn();
             $dropdown.trigger('loading.gl.dropdown');
             return $.ajax({
               type: 'PUT',
diff --git a/app/assets/javascripts/vue_pipelines_index/components/async_button.js b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
new file mode 100644
index 0000000000000000000000000000000000000000..aaebf29d8ae3cbfe463a346f88ddf5e3e51dec80
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/async_button.js
@@ -0,0 +1,92 @@
+/* eslint-disable no-new, no-alert */
+/* global Flash */
+import '~/flash';
+import eventHub from '../event_hub';
+
+export default {
+  props: {
+    endpoint: {
+      type: String,
+      required: true,
+    },
+
+    service: {
+      type: Object,
+      required: true,
+    },
+
+    title: {
+      type: String,
+      required: true,
+    },
+
+    icon: {
+      type: String,
+      required: true,
+    },
+
+    cssClass: {
+      type: String,
+      required: true,
+    },
+
+    confirmActionMessage: {
+      type: String,
+      required: false,
+    },
+  },
+
+  data() {
+    return {
+      isLoading: false,
+    };
+  },
+
+  computed: {
+    iconClass() {
+      return `fa fa-${this.icon}`;
+    },
+
+    buttonClass() {
+      return `btn has-tooltip ${this.cssClass}`;
+    },
+  },
+
+  methods: {
+    onClick() {
+      if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
+        this.makeRequest();
+      } else if (!this.confirmActionMessage) {
+        this.makeRequest();
+      }
+    },
+
+    makeRequest() {
+      this.isLoading = true;
+
+      this.service.postAction(this.endpoint)
+      .then(() => {
+        this.isLoading = false;
+        eventHub.$emit('refreshPipelines');
+      })
+      .catch(() => {
+        this.isLoading = false;
+        new Flash('An error occured while making the request.');
+      });
+    },
+  },
+
+  template: `
+    <button
+      type="button"
+      @click="onClick"
+      :class="buttonClass"
+      :title="title"
+      :aria-label="title"
+      data-placement="top"
+      :disabled="isLoading">
+      <i :class="iconClass" aria-hidden="true"/>
+      <i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" />
+    </button>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
new file mode 100644
index 0000000000000000000000000000000000000000..4e183d5c8eca1400f87745c7a43ecb6296c90694
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipeline_url.js
@@ -0,0 +1,56 @@
+export default {
+  props: [
+    'pipeline',
+  ],
+  computed: {
+    user() {
+      return !!this.pipeline.user;
+    },
+  },
+  template: `
+    <td>
+      <a
+        :href="pipeline.path"
+        class="js-pipeline-url-link">
+        <span class="pipeline-id">#{{pipeline.id}}</span>
+      </a>
+      <span>by</span>
+      <a
+        class="js-pipeline-url-user"
+        v-if="user"
+        :href="pipeline.user.web_url">
+        <img
+          v-if="user"
+          class="avatar has-tooltip s20 "
+          :title="pipeline.user.name"
+          data-container="body"
+          :src="pipeline.user.avatar_url"
+        >
+      </a>
+      <span
+        v-if="!user"
+        class="js-pipeline-url-api api monospace">
+        API
+      </span>
+      <span
+        v-if="pipeline.flags.latest"
+        class="js-pipeline-url-lastest label label-success has-tooltip"
+        title="Latest pipeline for this branch"
+        data-original-title="Latest pipeline for this branch">
+        latest
+      </span>
+      <span
+        v-if="pipeline.flags.yaml_errors"
+        class="js-pipeline-url-yaml label label-danger has-tooltip"
+        :title="pipeline.yaml_errors"
+        :data-original-title="pipeline.yaml_errors">
+        yaml invalid
+      </span>
+      <span
+        v-if="pipeline.flags.stuck"
+        class="js-pipeline-url-stuck label label-warning">
+        stuck
+      </span>
+    </td>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
new file mode 100644
index 0000000000000000000000000000000000000000..4bb2b04888460d24e8d3b74a95580c9e5c9cdf8a
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_actions.js
@@ -0,0 +1,71 @@
+/* eslint-disable no-new */
+/* global Flash */
+import '~/flash';
+import playIconSvg from 'icons/_icon_play.svg';
+import eventHub from '../event_hub';
+
+export default {
+  props: {
+    actions: {
+      type: Array,
+      required: true,
+    },
+
+    service: {
+      type: Object,
+      required: true,
+    },
+  },
+
+  data() {
+    return {
+      playIconSvg,
+      isLoading: false,
+    };
+  },
+
+  methods: {
+    onClickAction(endpoint) {
+      this.isLoading = true;
+
+      this.service.postAction(endpoint)
+      .then(() => {
+        this.isLoading = false;
+        eventHub.$emit('refreshPipelines');
+      })
+      .catch(() => {
+        this.isLoading = false;
+        new Flash('An error occured while making the request.');
+      });
+    },
+  },
+
+  template: `
+    <div class="btn-group" v-if="actions">
+      <button
+        type="button"
+        class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
+        title="Manual job"
+        data-toggle="dropdown"
+        data-placement="top"
+        aria-label="Manual job"
+        :disabled="isLoading">
+        ${playIconSvg}
+        <i class="fa fa-caret-down" aria-hidden="true"></i>
+        <i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+      </button>
+
+      <ul class="dropdown-menu dropdown-menu-align-right">
+        <li v-for="action in actions">
+          <button
+            type="button"
+            class="js-pipeline-action-link no-btn"
+            @click="onClickAction(action.path)">
+            ${playIconSvg}
+            <span>{{action.name}}</span>
+          </button>
+        </li>
+      </ul>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
new file mode 100644
index 0000000000000000000000000000000000000000..3555040d60f754b561a49508e6b77a722529e64f
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/pipelines_artifacts.js
@@ -0,0 +1,32 @@
+export default {
+  props: {
+    artifacts: {
+      type: Array,
+      required: true,
+    },
+  },
+
+  template: `
+    <div class="btn-group" role="group">
+      <button
+        class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
+        title="Artifacts"
+        data-placement="top"
+        data-toggle="dropdown"
+        aria-label="Artifacts">
+        <i class="fa fa-download" aria-hidden="true"></i>
+        <i class="fa fa-caret-down" aria-hidden="true"></i>
+      </button>
+      <ul class="dropdown-menu dropdown-menu-align-right">
+        <li v-for="artifact in artifacts">
+          <a
+            rel="nofollow"
+            :href="artifact.path">
+            <i class="fa fa-download" aria-hidden="true"></i>
+            <span>Download {{artifact.name}} artifacts</span>
+          </a>
+        </li>
+      </ul>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/stage.js b/app/assets/javascripts/vue_pipelines_index/components/stage.js
new file mode 100644
index 0000000000000000000000000000000000000000..a2c29002707005a2fd35c5c5f1767e02645c9767
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/stage.js
@@ -0,0 +1,116 @@
+/* global Flash */
+import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
+import createdSvg from 'icons/_icon_status_created_borderless.svg';
+import failedSvg from 'icons/_icon_status_failed_borderless.svg';
+import manualSvg from 'icons/_icon_status_manual_borderless.svg';
+import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
+import runningSvg from 'icons/_icon_status_running_borderless.svg';
+import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
+import successSvg from 'icons/_icon_status_success_borderless.svg';
+import warningSvg from 'icons/_icon_status_warning_borderless.svg';
+
+export default {
+  data() {
+    const svgsDictionary = {
+      icon_status_canceled: canceledSvg,
+      icon_status_created: createdSvg,
+      icon_status_failed: failedSvg,
+      icon_status_manual: manualSvg,
+      icon_status_pending: pendingSvg,
+      icon_status_running: runningSvg,
+      icon_status_skipped: skippedSvg,
+      icon_status_success: successSvg,
+      icon_status_warning: warningSvg,
+    };
+
+    return {
+      builds: '',
+      spinner: '<span class="fa fa-spinner fa-spin"></span>',
+      svg: svgsDictionary[this.stage.status.icon],
+    };
+  },
+
+  props: {
+    stage: {
+      type: Object,
+      required: true,
+    },
+  },
+
+  updated() {
+    if (this.builds) {
+      this.stopDropdownClickPropagation();
+    }
+  },
+
+  methods: {
+    fetchBuilds(e) {
+      const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
+
+      if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
+
+      return this.$http.get(this.stage.dropdown_path)
+        .then((response) => {
+          this.builds = JSON.parse(response.body).html;
+        }, () => {
+          const flash = new Flash('Something went wrong on our end.');
+          return flash;
+        });
+    },
+
+    /**
+     * When the user right clicks or cmd/ctrl + click in the job name
+     * the dropdown should not be closed and the link should open in another tab,
+     * so we stop propagation of the click event inside the dropdown.
+     *
+     * Since this component is rendered multiple times per page we need to guarantee we only
+     * target the click event of this component.
+     */
+    stopDropdownClickPropagation() {
+      $(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
+        e.stopPropagation();
+      });
+    },
+  },
+  computed: {
+    buildsOrSpinner() {
+      return this.builds ? this.builds : this.spinner;
+    },
+    dropdownClass() {
+      if (this.builds) return 'js-builds-dropdown-container';
+      return 'js-builds-dropdown-loading builds-dropdown-loading';
+    },
+    buildStatus() {
+      return `Build: ${this.stage.status.label}`;
+    },
+    tooltip() {
+      return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
+    },
+    triggerButtonClass() {
+      return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
+    },
+  },
+  template: `
+    <div>
+      <button
+        @click="fetchBuilds($event)"
+        :class="triggerButtonClass"
+        :title="stage.title"
+        data-placement="top"
+        data-toggle="dropdown"
+        type="button"
+        :aria-label="stage.title">
+        <span v-html="svg" aria-hidden="true"></span>
+        <i class="fa fa-caret-down" aria-hidden="true"></i>
+      </button>
+      <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
+        <div class="arrow-up" aria-hidden="true"></div>
+        <div
+          :class="dropdownClass"
+          class="js-builds-dropdown-list scrollable-menu"
+          v-html="buildsOrSpinner">
+        </div>
+      </ul>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/status.js b/app/assets/javascripts/vue_pipelines_index/components/status.js
new file mode 100644
index 0000000000000000000000000000000000000000..21a281af438ddaeafd95cdbebd2bfc5156a45d48
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/status.js
@@ -0,0 +1,60 @@
+import canceledSvg from 'icons/_icon_status_canceled.svg';
+import createdSvg from 'icons/_icon_status_created.svg';
+import failedSvg from 'icons/_icon_status_failed.svg';
+import manualSvg from 'icons/_icon_status_manual.svg';
+import pendingSvg from 'icons/_icon_status_pending.svg';
+import runningSvg from 'icons/_icon_status_running.svg';
+import skippedSvg from 'icons/_icon_status_skipped.svg';
+import successSvg from 'icons/_icon_status_success.svg';
+import warningSvg from 'icons/_icon_status_warning.svg';
+
+export default {
+  props: {
+    pipeline: {
+      type: Object,
+      required: true,
+    },
+  },
+
+  data() {
+    const svgsDictionary = {
+      icon_status_canceled: canceledSvg,
+      icon_status_created: createdSvg,
+      icon_status_failed: failedSvg,
+      icon_status_manual: manualSvg,
+      icon_status_pending: pendingSvg,
+      icon_status_running: runningSvg,
+      icon_status_skipped: skippedSvg,
+      icon_status_success: successSvg,
+      icon_status_warning: warningSvg,
+    };
+
+    return {
+      svg: svgsDictionary[this.pipeline.details.status.icon],
+    };
+  },
+
+  computed: {
+    cssClasses() {
+      return `ci-status ci-${this.pipeline.details.status.group}`;
+    },
+
+    detailsPath() {
+      const { status } = this.pipeline.details;
+      return status.has_details ? status.details_path : false;
+    },
+
+    content() {
+      return `${this.svg} ${this.pipeline.details.status.text}`;
+    },
+  },
+  template: `
+    <td class="commit-link">
+      <a
+        :class="cssClasses"
+        :href="detailsPath"
+        v-html="content">
+      </a>
+    </td>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/components/time_ago.js b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
new file mode 100644
index 0000000000000000000000000000000000000000..498d0715f54657320ce8e6c7705c218c679b6324
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/components/time_ago.js
@@ -0,0 +1,71 @@
+import iconTimerSvg from 'icons/_icon_timer.svg';
+import '../../lib/utils/datetime_utility';
+
+export default {
+  data() {
+    return {
+      currentTime: new Date(),
+      iconTimerSvg,
+    };
+  },
+  props: ['pipeline'],
+  computed: {
+    timeAgo() {
+      return gl.utils.getTimeago();
+    },
+    localTimeFinished() {
+      return gl.utils.formatDate(this.pipeline.details.finished_at);
+    },
+    timeStopped() {
+      const changeTime = this.currentTime;
+      const options = {
+        weekday: 'long',
+        year: 'numeric',
+        month: 'short',
+        day: 'numeric',
+      };
+      options.timeZoneName = 'short';
+      const finished = this.pipeline.details.finished_at;
+      if (!finished && changeTime) return false;
+      return ({ words: this.timeAgo.format(finished) });
+    },
+    duration() {
+      const { duration } = this.pipeline.details;
+      const date = new Date(duration * 1000);
+
+      let hh = date.getUTCHours();
+      let mm = date.getUTCMinutes();
+      let ss = date.getSeconds();
+
+      if (hh < 10) hh = `0${hh}`;
+      if (mm < 10) mm = `0${mm}`;
+      if (ss < 10) ss = `0${ss}`;
+
+      if (duration !== null) return `${hh}:${mm}:${ss}`;
+      return false;
+    },
+  },
+  methods: {
+    changeTime() {
+      this.currentTime = new Date();
+    },
+  },
+  template: `
+    <td class="pipelines-time-ago">
+      <p class="duration" v-if='duration'>
+        <span v-html="iconTimerSvg"></span>
+        {{duration}}
+      </p>
+      <p class="finished-at" v-if='timeStopped'>
+        <i class="fa fa-calendar"></i>
+        <time
+          data-toggle="tooltip"
+          data-placement="top"
+          data-container="body"
+          :data-original-title='localTimeFinished'>
+          {{timeStopped.words}}
+        </time>
+      </p>
+    </td>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/event_hub.js b/app/assets/javascripts/vue_pipelines_index/event_hub.js
new file mode 100644
index 0000000000000000000000000000000000000000..0948c2e53524a736a55c060600868ce89ee7687a
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/event_hub.js
@@ -0,0 +1,3 @@
+import Vue from 'vue';
+
+export default new Vue();
diff --git a/app/assets/javascripts/vue_pipelines_index/index.js b/app/assets/javascripts/vue_pipelines_index/index.js
index a90bd1518e9697acb70698f715290f1893d78018..b4e2d3a1143dc35d567dc8a0e3e721a7c5663ccc 100644
--- a/app/assets/javascripts/vue_pipelines_index/index.js
+++ b/app/assets/javascripts/vue_pipelines_index/index.js
@@ -1,29 +1,28 @@
-/* eslint-disable no-param-reassign */
-/* global Vue, VueResource, gl */
-window.Vue = require('vue');
+import PipelinesStore from './stores/pipelines_store';
+import PipelinesComponent from './pipelines';
+import '../vue_shared/vue_resource_interceptor';
+
+const Vue = window.Vue = require('vue');
 window.Vue.use(require('vue-resource'));
-require('../lib/utils/common_utils');
-require('../vue_shared/vue_resource_interceptor');
-require('./pipelines');
 
 $(() => new Vue({
   el: document.querySelector('.vue-pipelines-index'),
 
   data() {
     const project = document.querySelector('.pipelines');
+    const store = new PipelinesStore();
 
     return {
-      scope: project.dataset.url,
-      store: new gl.PipelineStore(),
+      store,
+      endpoint: project.dataset.url,
     };
   },
   components: {
-    'vue-pipelines': gl.VuePipelines,
+    'vue-pipelines': PipelinesComponent,
   },
   template: `
     <vue-pipelines
-      :scope="scope"
-      :store="store">
-    </vue-pipelines>
+      :endpoint="endpoint"
+      :store="store" />
   `,
 }));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js
deleted file mode 100644
index 583d6915a8530c2151b61b82313666d594d3def2..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js
+++ /dev/null
@@ -1,123 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign,  no-alert */
-const playIconSvg = require('icons/_icon_play.svg');
-
-((gl) => {
-  gl.VuePipelineActions = Vue.extend({
-    props: ['pipeline'],
-    computed: {
-      actions() {
-        return this.pipeline.details.manual_actions.length > 0;
-      },
-      artifacts() {
-        return this.pipeline.details.artifacts.length > 0;
-      },
-    },
-    methods: {
-      download(name) {
-        return `Download ${name} artifacts`;
-      },
-
-      /**
-       * Shows a dialog when the user clicks in the cancel button.
-       * We need to prevent the default behavior and stop propagation because the
-       * link relies on UJS.
-       *
-       * @param  {Event} event
-       */
-      confirmAction(event) {
-        if (!confirm('Are you sure you want to cancel this pipeline?')) {
-          event.preventDefault();
-          event.stopPropagation();
-        }
-      },
-    },
-
-    data() {
-      return { playIconSvg };
-    },
-
-    template: `
-      <td class="pipeline-actions">
-        <div class="pull-right">
-          <div class="btn-group">
-            <div class="btn-group" v-if="actions">
-              <button
-                class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
-                data-toggle="dropdown"
-                title="Manual job"
-                data-placement="top"
-                data-container="body"
-                aria-label="Manual job">
-                <span v-html="playIconSvg" aria-hidden="true"></span>
-                <i class="fa fa-caret-down" aria-hidden="true"></i>
-              </button>
-              <ul class="dropdown-menu dropdown-menu-align-right">
-                <li v-for='action in pipeline.details.manual_actions'>
-                  <a
-                    rel="nofollow"
-                    data-method="post"
-                    :href="action.path" >
-                    <span v-html="playIconSvg" aria-hidden="true"></span>
-                    <span>{{action.name}}</span>
-                  </a>
-                </li>
-              </ul>
-            </div>
-
-            <div class="btn-group" v-if="artifacts">
-              <button
-                class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
-                title="Artifacts"
-                data-placement="top"
-                data-container="body"
-                data-toggle="dropdown"
-                aria-label="Artifacts">
-                <i class="fa fa-download" aria-hidden="true"></i>
-                <i class="fa fa-caret-down" aria-hidden="true"></i>
-              </button>
-              <ul class="dropdown-menu dropdown-menu-align-right">
-                <li v-for='artifact in pipeline.details.artifacts'>
-                  <a
-                    rel="nofollow"
-                    :href="artifact.path">
-                    <i class="fa fa-download" aria-hidden="true"></i>
-                    <span>{{download(artifact.name)}}</span>
-                  </a>
-                </li>
-              </ul>
-            </div>
-            <div class="btn-group" v-if="pipeline.flags.retryable">
-              <a
-                class="btn btn-default btn-retry has-tooltip"
-                title="Retry"
-                rel="nofollow"
-                data-method="post"
-                data-placement="top"
-                data-container="body"
-                data-toggle="dropdown"
-                :href='pipeline.retry_path'
-                aria-label="Retry">
-                <i class="fa fa-repeat" aria-hidden="true"></i>
-              </a>
-            </div>
-            <div class="btn-group" v-if="pipeline.flags.cancelable">
-              <a
-                class="btn btn-remove has-tooltip"
-                title="Cancel"
-                rel="nofollow"
-                data-method="post"
-                data-placement="top"
-                data-container="body"
-                data-toggle="dropdown"
-                :href='pipeline.cancel_path'
-                aria-label="Cancel">
-                <i class="fa fa-remove" aria-hidden="true"></i>
-              </a>
-            </div>
-          </div>
-        </div>
-      </td>
-    `,
-  });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js b/app/assets/javascripts/vue_pipelines_index/pipeline_url.js
deleted file mode 100644
index ae5649f0519d67e477c2e08633093bf200ddfb8b..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_url.js
+++ /dev/null
@@ -1,63 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
-  gl.VuePipelineUrl = Vue.extend({
-    props: [
-      'pipeline',
-    ],
-    computed: {
-      user() {
-        return !!this.pipeline.user;
-      },
-    },
-    template: `
-      <td>
-        <a :href='pipeline.path'>
-          <span class="pipeline-id">#{{pipeline.id}}</span>
-        </a>
-        <span>by</span>
-        <a
-          v-if='user'
-          :href='pipeline.user.web_url'
-        >
-          <img
-            v-if='user'
-            class="avatar has-tooltip s20 "
-            :title='pipeline.user.name'
-            data-container="body"
-            :src='pipeline.user.avatar_url'
-          >
-        </a>
-        <span
-          v-if='!user'
-          class="api monospace"
-        >
-          API
-        </span>
-        <span
-          v-if='pipeline.flags.latest'
-          class="label label-success has-tooltip"
-          title="Latest pipeline for this branch"
-          data-original-title="Latest pipeline for this branch"
-        >
-          latest
-        </span>
-        <span
-          v-if='pipeline.flags.yaml_errors'
-          class="label label-danger has-tooltip"
-          :title='pipeline.yaml_errors'
-          :data-original-title='pipeline.yaml_errors'
-        >
-          yaml invalid
-        </span>
-        <span
-          v-if='pipeline.flags.stuck'
-          class="label label-warning"
-        >
-          stuck
-        </span>
-      </td>
-    `,
-  });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/pipelines.js b/app/assets/javascripts/vue_pipelines_index/pipelines.js
index 601ef41e9171d34955954352f9e4119f19fad760..f389e5e495041a69f704768576e4fe14f52b2588 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipelines.js
+++ b/app/assets/javascripts/vue_pipelines_index/pipelines.js
@@ -1,87 +1,121 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
+/* global Flash */
+/* eslint-disable no-new */
+import '~/flash';
+import Vue from 'vue';
+import PipelinesService from './services/pipelines_service';
+import eventHub from './event_hub';
+import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
+import TablePaginationComponent from '../vue_shared/components/table_pagination';
 
-window.Vue = require('vue');
-require('../vue_shared/components/table_pagination');
-require('./store');
-require('../vue_shared/components/pipelines_table');
-const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
-
-((gl) => {
-  gl.VuePipelines = Vue.extend({
-
-    components: {
-      'gl-pagination': gl.VueGlPagination,
-      'pipelines-table-component': gl.pipelines.PipelinesTableComponent,
+export default {
+  props: {
+    endpoint: {
+      type: String,
+      required: true,
     },
 
-    data() {
-      return {
-        pipelines: [],
-        timeLoopInterval: '',
-        intervalId: '',
-        apiScope: 'all',
-        pageInfo: {},
-        pagenum: 1,
-        count: {},
-        pageRequest: false,
-      };
-    },
-    props: ['scope', 'store'],
-    created() {
-      const pagenum = gl.utils.getParameterByName('page');
-      const scope = gl.utils.getParameterByName('scope');
-      if (pagenum) this.pagenum = pagenum;
-      if (scope) this.apiScope = scope;
-
-      this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
+    store: {
+      type: Object,
+      required: true,
     },
+  },
+
+  components: {
+    'gl-pagination': TablePaginationComponent,
+    'pipelines-table-component': PipelinesTableComponent,
+  },
+
+  data() {
+    return {
+      state: this.store.state,
+      apiScope: 'all',
+      pagenum: 1,
+      pageRequest: false,
+    };
+  },
+
+  created() {
+    this.service = new PipelinesService(this.endpoint);
+
+    this.fetchPipelines();
+
+    eventHub.$on('refreshPipelines', this.fetchPipelines);
+  },
+
+  beforeUpdate() {
+    if (this.state.pipelines.length && this.$children) {
+      this.store.startTimeAgoLoops.call(this, Vue);
+    }
+  },
 
-    beforeUpdate() {
-      if (this.pipelines.length && this.$children) {
-        CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
-      }
+  beforeDestroyed() {
+    eventHub.$off('refreshPipelines');
+  },
+
+  methods: {
+    /**
+     * Will change the page number and update the URL.
+     *
+     * @param  {Number} pageNumber desired page to go to.
+     */
+    change(pageNumber) {
+      const param = gl.utils.setParamInURL('page', pageNumber);
+
+      gl.utils.visitUrl(param);
+      return param;
     },
 
-    methods: {
-      /**
-       * Will change the page number and update the URL.
-       *
-       * @param  {Number} pageNumber desired page to go to.
-       */
-      change(pageNumber) {
-        const param = gl.utils.setParamInURL('page', pageNumber);
-
-        gl.utils.visitUrl(param);
-        return param;
-      },
+    fetchPipelines() {
+      const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
+      const scope = gl.utils.getParameterByName('scope') || this.apiScope;
+
+      this.pageRequest = true;
+      return this.service.getPipelines(scope, pageNumber)
+        .then(resp => ({
+          headers: resp.headers,
+          body: resp.json(),
+        }))
+        .then((response) => {
+          this.store.storeCount(response.body.count);
+          this.store.storePipelines(response.body.pipelines);
+          this.store.storePagination(response.headers);
+        })
+        .then(() => {
+          this.pageRequest = false;
+        })
+        .catch(() => {
+          this.pageRequest = false;
+          new Flash('An error occurred while fetching the pipelines, please reload the page again.');
+        });
     },
-    template: `
-      <div>
-        <div class="pipelines realtime-loading" v-if='pageRequest'>
-          <i class="fa fa-spinner fa-spin"></i>
-        </div>
-
-        <div class="blank-state blank-state-no-icon"
-          v-if="!pageRequest && pipelines.length === 0">
-          <h2 class="blank-state-title js-blank-state-title">
-            No pipelines to show
-          </h2>
-        </div>
-
-        <div class="table-holder" v-if='!pageRequest && pipelines.length'>
-          <pipelines-table-component :pipelines='pipelines'/>
-        </div>
-
-        <gl-pagination
-          v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage'
-          :pagenum='pagenum'
-          :change='change'
-          :count='count.all'
-          :pageInfo='pageInfo'
-        >
-        </gl-pagination>
+  },
+  template: `
+    <div>
+      <div class="pipelines realtime-loading" v-if="pageRequest">
+        <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+      </div>
+
+      <div class="blank-state blank-state-no-icon"
+        v-if="!pageRequest && state.pipelines.length === 0">
+        <h2 class="blank-state-title js-blank-state-title">
+          No pipelines to show
+        </h2>
+      </div>
+
+      <div class="table-holder" v-if="!pageRequest && state.pipelines.length">
+        <pipelines-table-component
+          :pipelines="state.pipelines"
+          :service="service"/>
       </div>
-    `,
-  });
-})(window.gl || (window.gl = {}));
+
+      <gl-pagination
+        v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage"
+        :pagenum="pagenum"
+        :change="change"
+        :count="state.count.all"
+        :pageInfo="state.pageInfo"
+      >
+      </gl-pagination>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
new file mode 100644
index 0000000000000000000000000000000000000000..708f5068dd30ad2b10660d070589c5eaca84b644
--- /dev/null
+++ b/app/assets/javascripts/vue_pipelines_index/services/pipelines_service.js
@@ -0,0 +1,44 @@
+/* eslint-disable class-methods-use-this */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class PipelinesService {
+
+  /**
+  * Commits and merge request endpoints need to be requested with `.json`.
+  *
+  * The url provided to request the pipelines in the new merge request
+  * page already has `.json`.
+  *
+  * @param  {String} root
+  */
+  constructor(root) {
+    let endpoint;
+
+    if (root.indexOf('.json') === -1) {
+      endpoint = `${root}.json`;
+    } else {
+      endpoint = root;
+    }
+
+    this.pipelines = Vue.resource(endpoint);
+  }
+
+  getPipelines(scope, page) {
+    return this.pipelines.get({ scope, page });
+  }
+
+  /**
+   * Post request for all pipelines actions.
+   * Endpoint content type needs to be:
+   * `Content-Type:application/x-www-form-urlencoded`
+   *
+   * @param  {String} endpoint
+   * @return {Promise}
+   */
+  postAction(endpoint) {
+    return Vue.http.post(endpoint, {}, { emulateJSON: true });
+  }
+}
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js b/app/assets/javascripts/vue_pipelines_index/stage.js
deleted file mode 100644
index ae4f0b4a53b4e89105c8a414d0cde4391abb28f8..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/stage.js
+++ /dev/null
@@ -1,119 +0,0 @@
-/* global Vue, Flash, gl */
-/* eslint-disable no-param-reassign */
-import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
-import createdSvg from 'icons/_icon_status_created_borderless.svg';
-import failedSvg from 'icons/_icon_status_failed_borderless.svg';
-import manualSvg from 'icons/_icon_status_manual_borderless.svg';
-import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
-import runningSvg from 'icons/_icon_status_running_borderless.svg';
-import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
-import successSvg from 'icons/_icon_status_success_borderless.svg';
-import warningSvg from 'icons/_icon_status_warning_borderless.svg';
-
-((gl) => {
-  gl.VueStage = Vue.extend({
-    data() {
-      const svgsDictionary = {
-        icon_status_canceled: canceledSvg,
-        icon_status_created: createdSvg,
-        icon_status_failed: failedSvg,
-        icon_status_manual: manualSvg,
-        icon_status_pending: pendingSvg,
-        icon_status_running: runningSvg,
-        icon_status_skipped: skippedSvg,
-        icon_status_success: successSvg,
-        icon_status_warning: warningSvg,
-      };
-
-      return {
-        builds: '',
-        spinner: '<span class="fa fa-spinner fa-spin"></span>',
-        svg: svgsDictionary[this.stage.status.icon],
-      };
-    },
-
-    props: {
-      stage: {
-        type: Object,
-        required: true,
-      },
-    },
-
-    updated() {
-      if (this.builds) {
-        this.stopDropdownClickPropagation();
-      }
-    },
-
-    methods: {
-      fetchBuilds(e) {
-        const areaExpanded = e.currentTarget.attributes['aria-expanded'];
-
-        if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
-
-        return this.$http.get(this.stage.dropdown_path)
-          .then((response) => {
-            this.builds = JSON.parse(response.body).html;
-          }, () => {
-            const flash = new Flash('Something went wrong on our end.');
-            return flash;
-          });
-      },
-
-      /**
-       * When the user right clicks or cmd/ctrl + click in the job name
-       * the dropdown should not be closed and the link should open in another tab,
-       * so we stop propagation of the click event inside the dropdown.
-       *
-       * Since this component is rendered multiple times per page we need to guarantee we only
-       * target the click event of this component.
-       */
-      stopDropdownClickPropagation() {
-        $(this.$el).on('click', '.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', (e) => {
-          e.stopPropagation();
-        });
-      },
-    },
-    computed: {
-      buildsOrSpinner() {
-        return this.builds ? this.builds : this.spinner;
-      },
-      dropdownClass() {
-        if (this.builds) return 'js-builds-dropdown-container';
-        return 'js-builds-dropdown-loading builds-dropdown-loading';
-      },
-      buildStatus() {
-        return `Build: ${this.stage.status.label}`;
-      },
-      tooltip() {
-        return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
-      },
-      triggerButtonClass() {
-        return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
-      },
-    },
-    template: `
-      <div>
-        <button
-          @click="fetchBuilds($event)"
-          :class="triggerButtonClass"
-          :title="stage.title"
-          data-placement="top"
-          data-toggle="dropdown"
-          type="button"
-          :aria-label="stage.title">
-          <span v-html="svg" aria-hidden="true"></span>
-          <i class="fa fa-caret-down" aria-hidden="true"></i>
-        </button>
-        <ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
-          <div class="arrow-up" aria-hidden="true"></div>
-          <div
-            :class="dropdownClass"
-            class="js-builds-dropdown-list scrollable-menu"
-            v-html="buildsOrSpinner">
-          </div>
-        </ul>
-      </div>
-    `,
-  });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/status.js b/app/assets/javascripts/vue_pipelines_index/status.js
deleted file mode 100644
index 8d9f83ac1130fb7b1b7928447da5bfc5440c87cb..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/status.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-import canceledSvg from 'icons/_icon_status_canceled.svg';
-import createdSvg from 'icons/_icon_status_created.svg';
-import failedSvg from 'icons/_icon_status_failed.svg';
-import manualSvg from 'icons/_icon_status_manual.svg';
-import pendingSvg from 'icons/_icon_status_pending.svg';
-import runningSvg from 'icons/_icon_status_running.svg';
-import skippedSvg from 'icons/_icon_status_skipped.svg';
-import successSvg from 'icons/_icon_status_success.svg';
-import warningSvg from 'icons/_icon_status_warning.svg';
-
-((gl) => {
-  gl.VueStatusScope = Vue.extend({
-    props: [
-      'pipeline',
-    ],
-
-    data() {
-      const svgsDictionary = {
-        icon_status_canceled: canceledSvg,
-        icon_status_created: createdSvg,
-        icon_status_failed: failedSvg,
-        icon_status_manual: manualSvg,
-        icon_status_pending: pendingSvg,
-        icon_status_running: runningSvg,
-        icon_status_skipped: skippedSvg,
-        icon_status_success: successSvg,
-        icon_status_warning: warningSvg,
-      };
-
-      return {
-        svg: svgsDictionary[this.pipeline.details.status.icon],
-      };
-    },
-
-    computed: {
-      cssClasses() {
-        const cssObject = { 'ci-status': true };
-        cssObject[`ci-${this.pipeline.details.status.group}`] = true;
-        return cssObject;
-      },
-
-      detailsPath() {
-        const { status } = this.pipeline.details;
-        return status.has_details ? status.details_path : false;
-      },
-
-      content() {
-        return `${this.svg} ${this.pipeline.details.status.text}`;
-      },
-    },
-    template: `
-      <td class="commit-link">
-        <a
-          :class="cssClasses"
-          :href="detailsPath"
-          v-html="content">
-        </a>
-      </td>
-    `,
-  });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js b/app/assets/javascripts/vue_pipelines_index/store.js
deleted file mode 100644
index 909007267b952ee3fbf7cf18273bee8df7d43c3f..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/store.js
+++ /dev/null
@@ -1,31 +0,0 @@
-/* global gl, Flash */
-/* eslint-disable no-param-reassign */
-
-((gl) => {
-  const pageValues = (headers) => {
-    const normalized = gl.utils.normalizeHeaders(headers);
-    const paginationInfo = gl.utils.parseIntPagination(normalized);
-    return paginationInfo;
-  };
-
-  gl.PipelineStore = class {
-    fetchDataLoop(Vue, pageNum, url, apiScope) {
-      this.pageRequest = true;
-
-      return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
-      .then((response) => {
-        const pageInfo = pageValues(response.headers);
-        this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
-
-        const res = JSON.parse(response.body);
-        this.count = Object.assign({}, this.count, res.count);
-        this.pipelines = Object.assign([], this.pipelines, res.pipelines);
-
-        this.pageRequest = false;
-      }, () => {
-        this.pageRequest = false;
-        return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
-      });
-    }
-  };
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_store.js b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
similarity index 62%
rename from app/assets/javascripts/commit/pipelines/pipelines_store.js
rename to app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
index f1b80e45444568616f02b8c805dd3d1b3e7b11e0..7ac10086a55cac7b35ff13fda90993c2d649c1c5 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_store.js
+++ b/app/assets/javascripts/vue_pipelines_index/stores/pipelines_store.js
@@ -1,31 +1,46 @@
 /* eslint-disable no-underscore-dangle*/
-/**
- * Pipelines' Store for commits view.
- *
- * Used to store the Pipelines rendered in the commit view in the pipelines table.
- */
-require('../../vue_realtime_listener');
-
-class PipelinesStore {
+import '../../vue_realtime_listener';
+
+export default class PipelinesStore {
   constructor() {
     this.state = {};
+
     this.state.pipelines = [];
+    this.state.count = {};
+    this.state.pageInfo = {};
   }
 
   storePipelines(pipelines = []) {
     this.state.pipelines = pipelines;
+  }
 
-    return pipelines;
+  storeCount(count = {}) {
+    this.state.count = count;
+  }
+
+  storePagination(pagination = {}) {
+    let paginationInfo;
+
+    if (Object.keys(pagination).length) {
+      const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
+      paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
+    } else {
+      paginationInfo = pagination;
+    }
+
+    this.state.pageInfo = paginationInfo;
   }
 
   /**
+   * FIXME: Move this inside the component.
+   *
    * Once the data is received we will start the time ago loops.
    *
    * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
    * update the time to show how long as passed.
    *
    */
-  static startTimeAgoLoops() {
+  startTimeAgoLoops() {
     const startTimeLoops = () => {
       this.timeLoopInterval = setInterval(() => {
         this.$children[0].$children.reduce((acc, component) => {
@@ -44,5 +59,3 @@ class PipelinesStore {
     gl.VueRealtimeListener(removeIntervals, startIntervals);
   }
 }
-
-module.exports = PipelinesStore;
diff --git a/app/assets/javascripts/vue_pipelines_index/time_ago.js b/app/assets/javascripts/vue_pipelines_index/time_ago.js
deleted file mode 100644
index a383570857d756608dbf454b8bc9720944d25438..0000000000000000000000000000000000000000
--- a/app/assets/javascripts/vue_pipelines_index/time_ago.js
+++ /dev/null
@@ -1,78 +0,0 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign */
-
-window.Vue = require('vue');
-require('../lib/utils/datetime_utility');
-
-const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg');
-
-((gl) => {
-  gl.VueTimeAgo = Vue.extend({
-    data() {
-      return {
-        currentTime: new Date(),
-        iconTimerSvg,
-      };
-    },
-    props: ['pipeline'],
-    computed: {
-      timeAgo() {
-        return gl.utils.getTimeago();
-      },
-      localTimeFinished() {
-        return gl.utils.formatDate(this.pipeline.details.finished_at);
-      },
-      timeStopped() {
-        const changeTime = this.currentTime;
-        const options = {
-          weekday: 'long',
-          year: 'numeric',
-          month: 'short',
-          day: 'numeric',
-        };
-        options.timeZoneName = 'short';
-        const finished = this.pipeline.details.finished_at;
-        if (!finished && changeTime) return false;
-        return ({ words: this.timeAgo.format(finished) });
-      },
-      duration() {
-        const { duration } = this.pipeline.details;
-        const date = new Date(duration * 1000);
-
-        let hh = date.getUTCHours();
-        let mm = date.getUTCMinutes();
-        let ss = date.getSeconds();
-
-        if (hh < 10) hh = `0${hh}`;
-        if (mm < 10) mm = `0${mm}`;
-        if (ss < 10) ss = `0${ss}`;
-
-        if (duration !== null) return `${hh}:${mm}:${ss}`;
-        return false;
-      },
-    },
-    methods: {
-      changeTime() {
-        this.currentTime = new Date();
-      },
-    },
-    template: `
-      <td class="pipelines-time-ago">
-        <p class="duration" v-if='duration'>
-          <span v-html="iconTimerSvg"></span>
-          {{duration}}
-        </p>
-        <p class="finished-at" v-if='timeStopped'>
-          <i class="fa fa-calendar"></i>
-          <time
-            data-toggle="tooltip"
-            data-placement="top"
-            data-container="body"
-            :data-original-title='localTimeFinished'>
-            {{timeStopped.words}}
-          </time>
-        </p>
-      </td>
-    `,
-  });
-})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js
index 4381487b79eb08c7a0b9f27302c6ff6e38603635..fb68abd95a21825e32e8242de4c0ee419e6b26ca 100644
--- a/app/assets/javascripts/vue_shared/components/commit.js
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -1,164 +1,157 @@
-/* global Vue */
-window.Vue = require('vue');
-const commitIconSvg = require('icons/_icon_commit.svg');
-
-(() => {
-  window.gl = window.gl || {};
-
-  window.gl.CommitComponent = Vue.component('commit-component', {
-
-    props: {
-      /**
-       * Indicates the existance of a tag.
-       * Used to render the correct icon, if true will render `fa-tag` icon,
-       * if false will render `fa-code-fork` icon.
-       */
-      tag: {
-        type: Boolean,
-        required: false,
-        default: false,
-      },
-
-      /**
-       * If provided is used to render the branch name and url.
-       * Should contain the following properties:
-       * name
-       * ref_url
-       */
-      commitRef: {
-        type: Object,
-        required: false,
-        default: () => ({}),
-      },
-
-      /**
-       * Used to link to the commit sha.
-       */
-      commitUrl: {
-        type: String,
-        required: false,
-        default: '',
-      },
-
-      /**
-       * Used to show the commit short sha that links to the commit url.
-       */
-      shortSha: {
-        type: String,
-        required: false,
-        default: '',
-      },
-
-      /**
-       * If provided shows the commit tile.
-       */
-      title: {
-        type: String,
-        required: false,
-        default: '',
-      },
-
-      /**
-       * If provided renders information about the author of the commit.
-       * When provided should include:
-       * `avatar_url` to render the avatar icon
-       * `web_url` to link to user profile
-       * `username` to render alt and title tags
-       */
-      author: {
-        type: Object,
-        required: false,
-        default: () => ({}),
-      },
+import commitIconSvg from 'icons/_icon_commit.svg';
+
+export default {
+  props: {
+    /**
+     * Indicates the existance of a tag.
+     * Used to render the correct icon, if true will render `fa-tag` icon,
+     * if false will render `fa-code-fork` icon.
+     */
+    tag: {
+      type: Boolean,
+      required: false,
+      default: false,
     },
 
-    computed: {
-      /**
-       * Used to verify if all the properties needed to render the commit
-       * ref section were provided.
-       *
-       * TODO: Improve this! Use lodash _.has when we have it.
-       *
-       * @returns {Boolean}
-       */
-      hasCommitRef() {
-        return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
-      },
-
-      /**
-       * Used to verify if all the properties needed to render the commit
-       * author section were provided.
-       *
-       * TODO: Improve this! Use lodash _.has when we have it.
-       *
-       * @returns {Boolean}
-       */
-      hasAuthor() {
-        return this.author &&
-          this.author.avatar_url &&
-          this.author.web_url &&
-          this.author.username;
-      },
-
-      /**
-       * If information about the author is provided will return a string
-       * to be rendered as the alt attribute of the img tag.
-       *
-       * @returns {String}
-       */
-      userImageAltDescription() {
-        return this.author &&
-          this.author.username ? `${this.author.username}'s avatar` : null;
-      },
+    /**
+     * If provided is used to render the branch name and url.
+     * Should contain the following properties:
+     * name
+     * ref_url
+     */
+    commitRef: {
+      type: Object,
+      required: false,
+      default: () => ({}),
     },
 
-    data() {
-      return { commitIconSvg };
+    /**
+     * Used to link to the commit sha.
+     */
+    commitUrl: {
+      type: String,
+      required: false,
+      default: '',
     },
 
-    template: `
-      <div class="branch-commit">
-
-        <div v-if="hasCommitRef" class="icon-container">
-          <i v-if="tag" class="fa fa-tag"></i>
-          <i v-if="!tag" class="fa fa-code-fork"></i>
-        </div>
-
-        <a v-if="hasCommitRef"
-          class="monospace branch-name"
-          :href="commitRef.ref_url">
-          {{commitRef.name}}
-        </a>
-
-        <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
-
-        <a class="commit-id monospace"
-          :href="commitUrl">
-          {{shortSha}}
-        </a>
-
-        <p class="commit-title">
-          <span v-if="title">
-            <a v-if="hasAuthor"
-              class="avatar-image-container"
-              :href="author.web_url">
-              <img
-                class="avatar has-tooltip s20"
-                :src="author.avatar_url"
-                :alt="userImageAltDescription"
-                :title="author.username" />
-            </a>
-
-            <a class="commit-row-message"
-              :href="commitUrl">
-              {{title}}
-            </a>
-          </span>
-          <span v-else>
-            Cant find HEAD commit for this branch
-          </span>
-        </p>
+    /**
+     * Used to show the commit short sha that links to the commit url.
+     */
+    shortSha: {
+      type: String,
+      required: false,
+      default: '',
+    },
+
+    /**
+     * If provided shows the commit tile.
+     */
+    title: {
+      type: String,
+      required: false,
+      default: '',
+    },
+
+    /**
+     * If provided renders information about the author of the commit.
+     * When provided should include:
+     * `avatar_url` to render the avatar icon
+     * `web_url` to link to user profile
+     * `username` to render alt and title tags
+     */
+    author: {
+      type: Object,
+      required: false,
+      default: () => ({}),
+    },
+  },
+
+  computed: {
+    /**
+     * Used to verify if all the properties needed to render the commit
+     * ref section were provided.
+     *
+     * TODO: Improve this! Use lodash _.has when we have it.
+     *
+     * @returns {Boolean}
+     */
+    hasCommitRef() {
+      return this.commitRef && this.commitRef.name && this.commitRef.ref_url;
+    },
+
+    /**
+     * Used to verify if all the properties needed to render the commit
+     * author section were provided.
+     *
+     * TODO: Improve this! Use lodash _.has when we have it.
+     *
+     * @returns {Boolean}
+     */
+    hasAuthor() {
+      return this.author &&
+        this.author.avatar_url &&
+        this.author.web_url &&
+        this.author.username;
+    },
+
+    /**
+     * If information about the author is provided will return a string
+     * to be rendered as the alt attribute of the img tag.
+     *
+     * @returns {String}
+     */
+    userImageAltDescription() {
+      return this.author &&
+        this.author.username ? `${this.author.username}'s avatar` : null;
+    },
+  },
+
+  data() {
+    return { commitIconSvg };
+  },
+
+  template: `
+    <div class="branch-commit">
+
+      <div v-if="hasCommitRef" class="icon-container">
+        <i v-if="tag" class="fa fa-tag"></i>
+        <i v-if="!tag" class="fa fa-code-fork"></i>
       </div>
-    `,
-  });
-})();
+
+      <a v-if="hasCommitRef"
+        class="monospace branch-name"
+        :href="commitRef.ref_url">
+        {{commitRef.name}}
+      </a>
+
+      <div v-html="commitIconSvg" class="commit-icon js-commit-icon"></div>
+
+      <a class="commit-id monospace"
+        :href="commitUrl">
+        {{shortSha}}
+      </a>
+
+      <p class="commit-title">
+        <span v-if="title">
+          <a v-if="hasAuthor"
+            class="avatar-image-container"
+            :href="author.web_url">
+            <img
+              class="avatar has-tooltip s20"
+              :src="author.avatar_url"
+              :alt="userImageAltDescription"
+              :title="author.username" />
+          </a>
+
+          <a class="commit-row-message"
+            :href="commitUrl">
+            {{title}}
+          </a>
+        </span>
+        <span v-else>
+          Cant find HEAD commit for this branch
+        </span>
+      </p>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table.js b/app/assets/javascripts/vue_shared/components/pipelines_table.js
index 0d8f85db965dac4dbe2ac80402073754ebbbd985..afd8d7acf6bddc59723b696bc4b0eb46a44cd9fb 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table.js
@@ -1,52 +1,48 @@
-/* eslint-disable no-param-reassign */
-/* global Vue */
+import PipelinesTableRowComponent from './pipelines_table_row';
 
-require('./pipelines_table_row');
 /**
  * Pipelines Table Component.
  *
  * Given an array of objects, renders a table.
  */
-
-(() => {
-  window.gl = window.gl || {};
-  gl.pipelines = gl.pipelines || {};
-
-  gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', {
-
-    props: {
-      pipelines: {
-        type: Array,
-        required: true,
-        default: () => ([]),
-      },
-
+export default {
+  props: {
+    pipelines: {
+      type: Array,
+      required: true,
+      default: () => ([]),
     },
 
-    components: {
-      'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent,
+    service: {
+      type: Object,
+      required: true,
     },
+  },
+
+  components: {
+    'pipelines-table-row-component': PipelinesTableRowComponent,
+  },
 
-    template: `
-      <table class="table ci-table">
-        <thead>
-          <tr>
-            <th class="js-pipeline-status pipeline-status">Status</th>
-            <th class="js-pipeline-info pipeline-info">Pipeline</th>
-            <th class="js-pipeline-commit pipeline-commit">Commit</th>
-            <th class="js-pipeline-stages pipeline-stages">Stages</th>
-            <th class="js-pipeline-date pipeline-date"></th>
-            <th class="js-pipeline-actions pipeline-actions"></th>
-          </tr>
-        </thead>
-        <tbody>
-          <template v-for="model in pipelines"
-            v-bind:model="model">
-            <tr is="pipelines-table-row-component"
-              :pipeline="model"></tr>
-          </template>
-        </tbody>
-      </table>
-    `,
-  });
-})();
+  template: `
+    <table class="table ci-table">
+      <thead>
+        <tr>
+          <th class="js-pipeline-status pipeline-status">Status</th>
+          <th class="js-pipeline-info pipeline-info">Pipeline</th>
+          <th class="js-pipeline-commit pipeline-commit">Commit</th>
+          <th class="js-pipeline-stages pipeline-stages">Stages</th>
+          <th class="js-pipeline-date pipeline-date"></th>
+          <th class="js-pipeline-actions pipeline-actions"></th>
+        </tr>
+      </thead>
+      <tbody>
+        <template v-for="model in pipelines"
+          v-bind:model="model">
+          <tr is="pipelines-table-row-component"
+            :pipeline="model"
+            :service="service"></tr>
+        </template>
+      </tbody>
+    </table>
+  `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
index e5e88186a850b7cbc0b119bfa4f9d70358ca7c0f..f5b3cb9214e8b0554b2182625496bcff9b6a9e48 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -1,199 +1,228 @@
 /* eslint-disable no-param-reassign */
-/* global Vue */
-
-require('../../vue_pipelines_index/status');
-require('../../vue_pipelines_index/pipeline_url');
-require('../../vue_pipelines_index/stage');
-require('../../vue_pipelines_index/pipeline_actions');
-require('../../vue_pipelines_index/time_ago');
-require('./commit');
+
+import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button';
+import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
+import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
+import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
+import PipelinesStageComponent from '../../vue_pipelines_index/components/stage';
+import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url';
+import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago';
+import CommitComponent from './commit';
+
 /**
  * Pipeline table row.
  *
  * Given the received object renders a table row in the pipelines' table.
  */
-(() => {
-  window.gl = window.gl || {};
-  gl.pipelines = gl.pipelines || {};
-
-  gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', {
-
-    props: {
-      pipeline: {
-        type: Object,
-        required: true,
-        default: () => ({}),
-      },
+export default {
+  props: {
+    pipeline: {
+      type: Object,
+      required: true,
+    },
 
+    service: {
+      type: Object,
+      required: true,
+    },
+  },
+
+  components: {
+    'async-button-component': AsyncButtonComponent,
+    'pipelines-actions-component': PipelinesActionsComponent,
+    'pipelines-artifacts-component': PipelinesArtifactsComponent,
+    'commit-component': CommitComponent,
+    'dropdown-stage': PipelinesStageComponent,
+    'pipeline-url': PipelinesUrlComponent,
+    'status-scope': PipelinesStatusComponent,
+    'time-ago': PipelinesTimeagoComponent,
+  },
+
+  computed: {
+    /**
+     * If provided, returns the commit tag.
+     * Needed to render the commit component column.
+     *
+     * This field needs a lot of verification, because of different possible cases:
+     *
+     * 1. person who is an author of a commit might be a GitLab user
+     * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
+     * 3. If GitLab user does not have avatar he/she might have a Gravatar
+     * 4. If committer is not a GitLab User he/she can have a Gravatar
+     * 5. We do not have consistent API object in this case
+     * 6. We should improve API and the code
+     *
+     * @returns {Object|Undefined}
+     */
+    commitAuthor() {
+      let commitAuthorInformation;
+
+      // 1. person who is an author of a commit might be a GitLab user
+      if (this.pipeline &&
+        this.pipeline.commit &&
+        this.pipeline.commit.author) {
+        // 2. if person who is an author of a commit is a GitLab user
+        // he/she can have a GitLab avatar
+        if (this.pipeline.commit.author.avatar_url) {
+          commitAuthorInformation = this.pipeline.commit.author;
+
+          // 3. If GitLab user does not have avatar he/she might have a Gravatar
+        } else if (this.pipeline.commit.author_gravatar_url) {
+          commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
+            avatar_url: this.pipeline.commit.author_gravatar_url,
+          });
+        }
+      }
+
+      // 4. If committer is not a GitLab User he/she can have a Gravatar
+      if (this.pipeline &&
+        this.pipeline.commit) {
+        commitAuthorInformation = {
+          avatar_url: this.pipeline.commit.author_gravatar_url,
+          web_url: `mailto:${this.pipeline.commit.author_email}`,
+          username: this.pipeline.commit.author_name,
+        };
+      }
+
+      return commitAuthorInformation;
     },
 
-    components: {
-      'commit-component': gl.CommitComponent,
-      'pipeline-actions': gl.VuePipelineActions,
-      'dropdown-stage': gl.VueStage,
-      'pipeline-url': gl.VuePipelineUrl,
-      'status-scope': gl.VueStatusScope,
-      'time-ago': gl.VueTimeAgo,
+    /**
+     * If provided, returns the commit tag.
+     * Needed to render the commit component column.
+     *
+     * @returns {String|Undefined}
+     */
+    commitTag() {
+      if (this.pipeline.ref &&
+        this.pipeline.ref.tag) {
+        return this.pipeline.ref.tag;
+      }
+      return undefined;
     },
 
-    computed: {
-      /**
-       * If provided, returns the commit tag.
-       * Needed to render the commit component column.
-       *
-       * This field needs a lot of verification, because of different possible cases:
-       *
-       * 1. person who is an author of a commit might be a GitLab user
-       * 2. if person who is an author of a commit is a GitLab user he/she can have a GitLab avatar
-       * 3. If GitLab user does not have avatar he/she might have a Gravatar
-       * 4. If committer is not a GitLab User he/she can have a Gravatar
-       * 5. We do not have consistent API object in this case
-       * 6. We should improve API and the code
-       *
-       * @returns {Object|Undefined}
-       */
-      commitAuthor() {
-        let commitAuthorInformation;
-
-        // 1. person who is an author of a commit might be a GitLab user
-        if (this.pipeline &&
-          this.pipeline.commit &&
-          this.pipeline.commit.author) {
-          // 2. if person who is an author of a commit is a GitLab user
-          // he/she can have a GitLab avatar
-          if (this.pipeline.commit.author.avatar_url) {
-            commitAuthorInformation = this.pipeline.commit.author;
-
-            // 3. If GitLab user does not have avatar he/she might have a Gravatar
-          } else if (this.pipeline.commit.author_gravatar_url) {
-            commitAuthorInformation = Object.assign({}, this.pipeline.commit.author, {
-              avatar_url: this.pipeline.commit.author_gravatar_url,
-            });
+    /**
+     * If provided, returns the commit ref.
+     * Needed to render the commit component column.
+     *
+     * Matches `path` prop sent in the API to `ref_url` prop needed
+     * in the commit component.
+     *
+     * @returns {Object|Undefined}
+     */
+    commitRef() {
+      if (this.pipeline.ref) {
+        return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
+          if (prop === 'path') {
+            accumulator.ref_url = this.pipeline.ref[prop];
+          } else {
+            accumulator[prop] = this.pipeline.ref[prop];
           }
-        }
+          return accumulator;
+        }, {});
+      }
 
-        // 4. If committer is not a GitLab User he/she can have a Gravatar
-        if (this.pipeline &&
-          this.pipeline.commit) {
-          commitAuthorInformation = {
-            avatar_url: this.pipeline.commit.author_gravatar_url,
-            web_url: `mailto:${this.pipeline.commit.author_email}`,
-            username: this.pipeline.commit.author_name,
-          };
-        }
+      return undefined;
+    },
 
-        return commitAuthorInformation;
-      },
-
-      /**
-       * If provided, returns the commit tag.
-       * Needed to render the commit component column.
-       *
-       * @returns {String|Undefined}
-       */
-      commitTag() {
-        if (this.pipeline.ref &&
-          this.pipeline.ref.tag) {
-          return this.pipeline.ref.tag;
-        }
-        return undefined;
-      },
-
-      /**
-       * If provided, returns the commit ref.
-       * Needed to render the commit component column.
-       *
-       * Matches `path` prop sent in the API to `ref_url` prop needed
-       * in the commit component.
-       *
-       * @returns {Object|Undefined}
-       */
-      commitRef() {
-        if (this.pipeline.ref) {
-          return Object.keys(this.pipeline.ref).reduce((accumulator, prop) => {
-            if (prop === 'path') {
-              accumulator.ref_url = this.pipeline.ref[prop];
-            } else {
-              accumulator[prop] = this.pipeline.ref[prop];
-            }
-            return accumulator;
-          }, {});
-        }
+    /**
+     * If provided, returns the commit url.
+     * Needed to render the commit component column.
+     *
+     * @returns {String|Undefined}
+     */
+    commitUrl() {
+      if (this.pipeline.commit &&
+        this.pipeline.commit.commit_path) {
+        return this.pipeline.commit.commit_path;
+      }
+      return undefined;
+    },
 
-        return undefined;
-      },
-
-      /**
-       * If provided, returns the commit url.
-       * Needed to render the commit component column.
-       *
-       * @returns {String|Undefined}
-       */
-      commitUrl() {
-        if (this.pipeline.commit &&
-          this.pipeline.commit.commit_path) {
-          return this.pipeline.commit.commit_path;
-        }
-        return undefined;
-      },
-
-      /**
-       * If provided, returns the commit short sha.
-       * Needed to render the commit component column.
-       *
-       * @returns {String|Undefined}
-       */
-      commitShortSha() {
-        if (this.pipeline.commit &&
-          this.pipeline.commit.short_id) {
-          return this.pipeline.commit.short_id;
-        }
-        return undefined;
-      },
-
-      /**
-       * If provided, returns the commit title.
-       * Needed to render the commit component column.
-       *
-       * @returns {String|Undefined}
-       */
-      commitTitle() {
-        if (this.pipeline.commit &&
-          this.pipeline.commit.title) {
-          return this.pipeline.commit.title;
-        }
-        return undefined;
-      },
+    /**
+     * If provided, returns the commit short sha.
+     * Needed to render the commit component column.
+     *
+     * @returns {String|Undefined}
+     */
+    commitShortSha() {
+      if (this.pipeline.commit &&
+        this.pipeline.commit.short_id) {
+        return this.pipeline.commit.short_id;
+      }
+      return undefined;
     },
 
-    template: `
-      <tr class="commit">
-        <status-scope :pipeline="pipeline"/>
-
-        <pipeline-url :pipeline="pipeline"></pipeline-url>
-
-        <td>
-          <commit-component
-            :tag="commitTag"
-            :commit-ref="commitRef"
-            :commit-url="commitUrl"
-            :short-sha="commitShortSha"
-            :title="commitTitle"
-            :author="commitAuthor"/>
-        </td>
-
-        <td class="stage-cell">
-          <div class="stage-container dropdown js-mini-pipeline-graph"
-            v-if="pipeline.details.stages.length > 0"
-            v-for="stage in pipeline.details.stages">
-            <dropdown-stage :stage="stage"/>
-          </div>
-        </td>
-
-        <time-ago :pipeline="pipeline"/>
-
-        <pipeline-actions :pipeline="pipeline" />
-      </tr>
-    `,
-  });
-})();
+    /**
+     * If provided, returns the commit title.
+     * Needed to render the commit component column.
+     *
+     * @returns {String|Undefined}
+     */
+    commitTitle() {
+      if (this.pipeline.commit &&
+        this.pipeline.commit.title) {
+        return this.pipeline.commit.title;
+      }
+      return undefined;
+    },
+  },
+
+  template: `
+    <tr class="commit">
+      <status-scope :pipeline="pipeline"/>
+
+      <pipeline-url :pipeline="pipeline"></pipeline-url>
+
+      <td>
+        <commit-component
+          :tag="commitTag"
+          :commit-ref="commitRef"
+          :commit-url="commitUrl"
+          :short-sha="commitShortSha"
+          :title="commitTitle"
+          :author="commitAuthor"/>
+      </td>
+
+      <td class="stage-cell">
+        <div class="stage-container dropdown js-mini-pipeline-graph"
+          v-if="pipeline.details.stages.length > 0"
+          v-for="stage in pipeline.details.stages">
+          <dropdown-stage :stage="stage"/>
+        </div>
+      </td>
+
+      <time-ago :pipeline="pipeline"/>
+
+      <td class="pipeline-actions">
+        <div class="pull-right btn-group">
+          <pipelines-actions-component
+            v-if="pipeline.details.manual_actions.length"
+            :actions="pipeline.details.manual_actions"
+            :service="service" />
+
+          <pipelines-artifacts-component
+            v-if="pipeline.details.artifacts.length"
+            :artifacts="pipeline.details.artifacts" />
+
+          <async-button-component
+            v-if="pipeline.flags.retryable"
+            :service="service"
+            :endpoint="pipeline.retry_path"
+            css-class="js-pipelines-retry-button btn-default btn-retry"
+            title="Retry"
+            icon="repeat" />
+
+          <async-button-component
+            v-if="pipeline.flags.cancelable"
+            :service="service"
+            :endpoint="pipeline.cancel_path"
+            css-class="js-pipelines-cancel-button btn-remove"
+            title="Cancel"
+            icon="remove"
+            confirm-action-message="Are you sure you want to cancel this pipeline?" />
+        </div>
+      </td>
+    </tr>
+  `,
+};
diff --git a/app/assets/javascripts/vue_shared/components/table_pagination.js b/app/assets/javascripts/vue_shared/components/table_pagination.js
index 8943b850a72b4b6cf895a4bd05c4143dfb8559d3..b9cd28f62493584bbe3c4df1b3f6f5802b561b66 100644
--- a/app/assets/javascripts/vue_shared/components/table_pagination.js
+++ b/app/assets/javascripts/vue_shared/components/table_pagination.js
@@ -1,147 +1,135 @@
-/* global Vue, gl */
-/* eslint-disable no-param-reassign, no-plusplus */
-
-window.Vue = require('vue');
-
-((gl) => {
-  const PAGINATION_UI_BUTTON_LIMIT = 4;
-  const UI_LIMIT = 6;
-  const SPREAD = '...';
-  const PREV = 'Prev';
-  const NEXT = 'Next';
-  const FIRST = '<< First';
-  const LAST = 'Last >>';
-
-  gl.VueGlPagination = Vue.extend({
-    props: {
-
-      // TODO: Consider refactoring in light of turbolinks removal.
-
-      /**
-        This function will take the information given by the pagination component
-
-        Here is an example `change` method:
-
-        change(pagenum) {
-          gl.utils.visitUrl(`?page=${pagenum}`);
-        },
-      */
-
-      change: {
-        type: Function,
-        required: true,
+const PAGINATION_UI_BUTTON_LIMIT = 4;
+const UI_LIMIT = 6;
+const SPREAD = '...';
+const PREV = 'Prev';
+const NEXT = 'Next';
+const FIRST = '<< First';
+const LAST = 'Last >>';
+
+export default {
+  props: {
+    /**
+      This function will take the information given by the pagination component
+
+      Here is an example `change` method:
+
+      change(pagenum) {
+        gl.utils.visitUrl(`?page=${pagenum}`);
       },
+    */
+    change: {
+      type: Function,
+      required: true,
+    },
 
-      /**
-        pageInfo will come from the headers of the API call
-        in the `.then` clause of the VueResource API call
-        there should be a function that contructs the pageInfo for this component
-
-        This is an example:
-
-        const pageInfo = headers => ({
-          perPage: +headers['X-Per-Page'],
-          page: +headers['X-Page'],
-          total: +headers['X-Total'],
-          totalPages: +headers['X-Total-Pages'],
-          nextPage: +headers['X-Next-Page'],
-          previousPage: +headers['X-Prev-Page'],
-        });
-      */
-
-      pageInfo: {
-        type: Object,
-        required: true,
-      },
+    /**
+      pageInfo will come from the headers of the API call
+      in the `.then` clause of the VueResource API call
+      there should be a function that contructs the pageInfo for this component
+
+      This is an example:
+
+      const pageInfo = headers => ({
+        perPage: +headers['X-Per-Page'],
+        page: +headers['X-Page'],
+        total: +headers['X-Total'],
+        totalPages: +headers['X-Total-Pages'],
+        nextPage: +headers['X-Next-Page'],
+        previousPage: +headers['X-Prev-Page'],
+      });
+    */
+    pageInfo: {
+      type: Object,
+      required: true,
     },
-    methods: {
-      changePage(e) {
-        const text = e.target.innerText;
-        const { totalPages, nextPage, previousPage } = this.pageInfo;
-
-        switch (text) {
-          case SPREAD:
-            break;
-          case LAST:
-            this.change(totalPages);
-            break;
-          case NEXT:
-            this.change(nextPage);
-            break;
-          case PREV:
-            this.change(previousPage);
-            break;
-          case FIRST:
-            this.change(1);
-            break;
-          default:
-            this.change(+text);
-            break;
-        }
-      },
+  },
+  methods: {
+    changePage(e) {
+      const text = e.target.innerText;
+      const { totalPages, nextPage, previousPage } = this.pageInfo;
+
+      switch (text) {
+        case SPREAD:
+          break;
+        case LAST:
+          this.change(totalPages);
+          break;
+        case NEXT:
+          this.change(nextPage);
+          break;
+        case PREV:
+          this.change(previousPage);
+          break;
+        case FIRST:
+          this.change(1);
+          break;
+        default:
+          this.change(+text);
+          break;
+      }
     },
-    computed: {
-      prev() {
-        return this.pageInfo.previousPage;
-      },
-      next() {
-        return this.pageInfo.nextPage;
-      },
-      getItems() {
-        const total = this.pageInfo.totalPages;
-        const page = this.pageInfo.page;
-        const items = [];
+  },
+  computed: {
+    prev() {
+      return this.pageInfo.previousPage;
+    },
+    next() {
+      return this.pageInfo.nextPage;
+    },
+    getItems() {
+      const total = this.pageInfo.totalPages;
+      const page = this.pageInfo.page;
+      const items = [];
 
-        if (page > 1) items.push({ title: FIRST });
+      if (page > 1) items.push({ title: FIRST });
 
-        if (page > 1) {
-          items.push({ title: PREV, prev: true });
-        } else {
-          items.push({ title: PREV, disabled: true, prev: true });
-        }
+      if (page > 1) {
+        items.push({ title: PREV, prev: true });
+      } else {
+        items.push({ title: PREV, disabled: true, prev: true });
+      }
 
-        if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
+      if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
 
-        const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
-        const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
+      const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
+      const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
 
-        for (let i = start; i <= end; i++) {
-          const isActive = i === page;
-          items.push({ title: i, active: isActive, page: true });
-        }
+      for (let i = start; i <= end; i += 1) {
+        const isActive = i === page;
+        items.push({ title: i, active: isActive, page: true });
+      }
 
-        if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
-          items.push({ title: SPREAD, separator: true, page: true });
-        }
+      if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
+        items.push({ title: SPREAD, separator: true, page: true });
+      }
 
-        if (page === total) {
-          items.push({ title: NEXT, disabled: true, next: true });
-        } else if (total - page >= 1) {
-          items.push({ title: NEXT, next: true });
-        }
+      if (page === total) {
+        items.push({ title: NEXT, disabled: true, next: true });
+      } else if (total - page >= 1) {
+        items.push({ title: NEXT, next: true });
+      }
 
-        if (total - page >= 1) items.push({ title: LAST, last: true });
+      if (total - page >= 1) items.push({ title: LAST, last: true });
 
-        return items;
-      },
+      return items;
     },
-    template: `
-      <div class="gl-pagination">
-        <ul class="pagination clearfix">
-          <li v-for='item in getItems'
-            :class='{
-              page: item.page,
-              prev: item.prev,
-              next: item.next,
-              separator: item.separator,
-              active: item.active,
-              disabled: item.disabled
-            }'
-          >
-            <a @click="changePage($event)">{{item.title}}</a>
-          </li>
-        </ul>
-      </div>
-    `,
-  });
-})(window.gl || (window.gl = {}));
+  },
+  template: `
+    <div class="gl-pagination">
+      <ul class="pagination clearfix">
+        <li v-for='item in getItems'
+          :class='{
+            page: item.page,
+            prev: item.prev,
+            next: item.next,
+            separator: item.separator,
+            active: item.active,
+            disabled: item.disabled
+          }'
+        >
+          <a @click="changePage($event)">{{item.title}}</a>
+        </li>
+      </ul>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
index 4157fefddc9d99d3d60da969bd9d40174dc6b3c9..f1c1e553b1669108d151ae716bda0e313f6bdc5d 100644
--- a/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
+++ b/app/assets/javascripts/vue_shared/vue_resource_interceptor.js
@@ -1,11 +1,13 @@
-/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars,
-no-param-reassign, no-plusplus */
-/* global Vue */
+/* eslint-disable no-param-reassign, no-plusplus */
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
 
 Vue.http.interceptors.push((request, next) => {
   Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
 
-  next((response) => {
+  next(() => {
     Vue.activeResources--;
   });
 });
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index a4b38723bbde89f07a9e7e35e75cf067353b41f7..2c33b235980b727b6553e5e44689587accf2cc51 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -429,3 +429,9 @@ table {
     @include str-truncated(100%);
   }
 }
+
+.tooltip {
+  .tooltip-inner {
+    word-wrap: break-word;
+  }
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 2ebeaf9a40dbc930bc4dd35e162807007d596dd6..51805c5d7340be3b85c023e5ff5439d1e16e896a 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -76,12 +76,14 @@
   }
 
   .input-token {
-    flex: 1;
-    -webkit-flex: 1;
+    max-width: 200px;
   }
 
-  .filtered-search-token + .input-token:not(:last-child) {
-    max-width: 200px;
+  .input-token:only-child,
+  .input-token:last-child {
+    flex: 1;
+    -webkit-flex: 1;
+    max-width: initial;
   }
 }
 
@@ -158,8 +160,8 @@
   background-color: $white-light;
 
   @media (max-width: $screen-xs-min) {
-    -webkit-flex: 1 1 100%;
-    flex: 1 1 100%;
+    -webkit-flex: 1 1 auto;
+    flex: 1 1 auto;
     margin-bottom: 10px;
 
     .dropdown-menu {
@@ -171,17 +173,26 @@
     }
   }
 
+  &:hover {
+    @extend .form-control:hover;
+  }
+
+  &.focus,
+  &.focus:hover {
+    border-color: $dropdown-input-focus-border;
+    box-shadow: 0 0 4px $search-input-focus-shadow-color;
+  }
+
+  &.focus .fa-filter {
+    color: $common-gray-dark;
+  }
+
   .form-control {
     position: relative;
     min-width: 200px;
-    padding-left: 0;
-    padding-right: 25px;
+    padding: 5px 25px 6px 0;
     border-color: transparent;
 
-    &:focus ~ .fa-filter {
-      color: $common-gray-dark;
-    }
-
     &:focus,
     &:hover {
       outline: none;
@@ -221,6 +232,10 @@
 .filter-dropdown-container {
   display: -webkit-flex;
   display: flex;
+
+  .dropdown-toggle {
+    line-height: 22px;
+  }
 }
 
 .dropdown-menu .filter-dropdown-item {
@@ -246,7 +261,9 @@
     background-color: $white-light;
     border-top: 0;
   }
+}
 
+@media (max-width: $screen-xs) {
   .filter-dropdown-container {
     .dropdown-toggle,
     .dropdown {
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index ea45aaa0253f2876a1411fac88042c246f595716..205d23b13291dd6e9561d389edbe762dcdc0f1a6 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -138,7 +138,6 @@
 
   .nav-links {
     display: inline-block;
-    width: 50%;
     margin-bottom: 0;
     border-bottom: none;
 
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index db5e2c51fe7704fff0c8209a99063413bc299c24..c241816788b4f9ee361fde5c5a5f9a4601dfbcbc 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -306,6 +306,11 @@ a > code {
  * Textareas intended for GFM
  *
  */
+textarea.js-gfm-input {
+  font-family: $monospace_font;
+  font-size: 13px;
+}
+
 .strikethrough {
   text-decoration: line-through;
 }
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c2156a5ac6906db2e112e503ef0a7596648aed88..927bf9805cec1456ba87f2cfe470f938cb2ea223 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -148,6 +148,18 @@
 .error-alert > .alert {
   margin-top: 5px;
   margin-bottom: 5px;
+
+  &.alert-dismissable {
+    .close {
+      color: $white-light;
+      opacity: 0.85;
+      font-weight: normal;
+
+      &:hover {
+        opacity: 1;
+      }
+    }
+  }
 }
 
 .discussion-body,
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 20eabc83142e64882a1488952d24abed39cd3c80..33b38ca692339d23d32aedffbf698ec5c6144212 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -72,11 +72,6 @@
           color: $gl-text-color-secondary;
           font-size: 14px;
         }
-
-        svg,
-        .fa {
-          margin-right: 0;
-        }
       }
 
       .btn-group {
@@ -921,3 +916,22 @@
     }
   }
 }
+
+/**
+ * Play button with icon in dropdowns
+ */
+.ci-table .no-btn {
+  border: none;
+  background: none;
+  outline: none;
+  width: 100%;
+  text-align: left;
+
+  .icon-play {
+    position: relative;
+    top: 2px;
+    margin-right: 5px;
+    height: 13px;
+    width: 12px;
+  }
+}
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index 2992568ae661942de6c32b9647b530f4b620ff18..a8c0937569c3f0fb152e9b3cadc648fa162a2b6a 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -37,7 +37,6 @@ module ServiceParams
     :namespace,
     :new_issue_url,
     :notify,
-    :notify_only_broken_builds,
     :notify_only_broken_pipelines,
     :password,
     :priority,
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index 498690e8f1164cff4d5a6b5d08e877bc6333d727..096de8032ae75f3eb864723a468c0b97d61f2f70 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -51,7 +51,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
   private
 
   def find_todos
-    @todos ||= TodosFinder.new(current_user, params).execute
+    @todos ||= TodosFinder.new(current_user, params.merge(include_associations: true)).execute
   end
 
   def todos_counts
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index f2fee62ebd646e67b7ef1f9253f06ecf7ed6aac2..cdb5b4173d3a88b5aadec321c7f1053058b95d88 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -6,6 +6,8 @@ class Projects::IssuesController < Projects::ApplicationController
   include IssuableCollections
   include SpammableActions
 
+  prepend_before_action :authenticate_user!, only: [:new]
+
   before_action :redirect_to_external_issue_tracker, only: [:index, :new]
   before_action :module_enabled
   before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
@@ -146,7 +148,7 @@ class Projects::IssuesController < Projects::ApplicationController
       end
 
       format.json do
-        render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+        render json: @issue.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
       end
     end
 
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 82f9b6e06db91c42157427d723d3656f25cdee93..677a8a1a73a3f687d411d380ad8f91a87469d44a 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -308,7 +308,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
       end
 
       format.json do
-        render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
+        render json: @merge_request.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
       end
     end
   rescue ActiveRecord::StaleObjectError
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 8b6c83d4fed9fa5f44840142d3952cc036423613..f210f7e61d2699ae7f04531c262a7fd55e4fe8b5 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -45,8 +45,9 @@ class Projects::WikisController < Projects::ApplicationController
     return render('empty') unless can?(current_user, :create_wiki, @project)
 
     @page = @project_wiki.find_page(params[:id])
+    @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
 
-    if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
+    if @page.valid?
       redirect_to(
         namespace_project_wiki_path(@project.namespace, @project, @page),
         notice: 'Wiki was successfully updated.'
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index b7f091f334d2843cc96818d946007fcb0ecb63f1..13d33a1c31b19b3b3209b24aa66ef8dac85b398e 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -24,6 +24,7 @@ class TodosFinder
 
   def execute
     items = current_user.todos
+    items = include_associations(items)
     items = by_action_id(items)
     items = by_action(items)
     items = by_author(items)
@@ -38,6 +39,17 @@ class TodosFinder
 
   private
 
+  def include_associations(items)
+    return items unless params[:include_associations]
+
+    items.includes(
+      [
+        target: { project: [:route, namespace: :route] },
+        author: { namespace: :route },
+      ]
+    )
+  end
+
   def action_id?
     action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i)
   end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 0b0c6a07efdb94d1f461949f2e75576b80b64369..8631bc54509b3347ccd6d777577797684c58edf6 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -215,6 +215,6 @@ module BlobHelper
   end
 
   def open_raw_file_button(path)
-    link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', title: 'Open raw', data: { container: 'body' }
+    link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' }
   end
 end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 8aad39e148bd2a7d8764b71fff3f14ea6d866f39..cef624430da0fd7709fec47947a1d8278daa1752 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -211,7 +211,7 @@ module CommitsHelper
     external_url = environment.external_url_for(diff_new_path, commit_sha)
     return unless external_url
 
-    link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
+    link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
       icon('external-link')
     end
   end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index a0642a1894b6413b8c4ef7d52f416f32729390bb..a57b5a8fea5d4ce7a09774582360843e9cd0919f 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -7,7 +7,7 @@ module ImportHelper
   def provider_project_link(provider, path_with_namespace)
     url = __send__("#{provider}_project_url", path_with_namespace)
 
-    link_to path_with_namespace, url, target: '_blank'
+    link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer'
   end
 
   private
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index c1523b4dabf87202f0e461c1f5cac57306123417..a8f167cbff268cc7b7b2734e33a270d450f2e052 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -16,6 +16,7 @@ module NavHelper
       "page-gutter build-sidebar right-sidebar-expanded"
     elsif current_path?('wikis#show') ||
         current_path?('wikis#edit') ||
+        current_path?('wikis#update') ||
         current_path?('wikis#history') ||
         current_path?('wikis#git_access')
       "page-gutter wiki-sidebar right-sidebar-expanded"
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 4f5adf623f2f6ae11a40e602ce423cb4bde3736b..847a8fdfca676b02f3ea6a1c0f52325d0a2c1b98 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -39,9 +39,13 @@ module TodosHelper
       namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
                                     todo.target, anchor: anchor)
     else
-      path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target]
-
-      path.unshift(:pipelines) if todo.build_failed?
+      if todo.build_failed?
+        # associated namespace and route would be loaded from the db again if todo.project was used
+        project = todo.target.project
+        path = [:pipelines, project.namespace.becomes(Namespace), project, todo.target]
+      else
+        path = [todo.target]
+      end
 
       polymorphic_path(path, anchor: anchor)
     end
diff --git a/app/mailers/emails/builds.rb b/app/mailers/emails/builds.rb
deleted file mode 100644
index 3853af6201ac9509fc963581f2e586ba91efa727..0000000000000000000000000000000000000000
--- a/app/mailers/emails/builds.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module Emails
-  module Builds
-    def build_fail_email(build_id, to)
-      @build = Ci::Build.find(build_id)
-      @project = @build.project
-
-      add_project_headers
-      add_build_headers('failed')
-
-      mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
-    end
-
-    def build_success_email(build_id, to)
-      @build = Ci::Build.find(build_id)
-      @project = @build.project
-
-      add_project_headers
-      add_build_headers('success')
-      mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
-    end
-
-    private
-
-    def add_build_headers(status)
-      headers['X-GitLab-Build-Id'] = @build.id
-      headers['X-GitLab-Build-Ref'] = @build.ref
-      headers['X-GitLab-Build-Status'] = status.to_s
-    end
-  end
-end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 5b9226a6b81a51a69c6fbeb4b39e36d08284996c..14df6f8f0a3cd7f2fc06ec811dcf895a8d216995 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -6,7 +6,6 @@ class Notify < BaseMailer
   include Emails::Notes
   include Emails::Projects
   include Emails::Profile
-  include Emails::Builds
   include Emails::Pipelines
   include Emails::Members
 
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index be6329308951d4af9b7db5031a0532410a5ffdd4..671a0fe98cc312a8abc359131055ffe684b2cc01 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -163,6 +163,8 @@ class ApplicationSetting < ActiveRecord::Base
   end
 
   def self.current
+    ensure_cache_setup
+
     Rails.cache.fetch(CACHE_KEY) do
       ApplicationSetting.last
     end
@@ -176,9 +178,16 @@ class ApplicationSetting < ActiveRecord::Base
   end
 
   def self.cached
+    ensure_cache_setup
     Rails.cache.fetch(CACHE_KEY)
   end
 
+  def self.ensure_cache_setup
+    # This is a workaround for a Rails bug that causes attribute methods not
+    # to be loaded when read from cache: https://github.com/rails/rails/issues/27348
+    ApplicationSetting.define_attribute_methods
+  end
+
   def self.defaults_ce
     {
       after_sign_up_text: nil,
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3722047251daa285fc6e9c506386d5b6782e462b..ad0be70c32a4c6d939e7fa1c71992b0ae9c48707 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -15,7 +15,7 @@ module Ci
     def persisted_environment
       @persisted_environment ||= Environment.find_by(
         name: expanded_environment_name,
-        project_id: gl_project_id
+        project: project
       )
     end
 
@@ -223,7 +223,8 @@ module Ci
 
     def merge_request
       merge_requests = MergeRequest.includes(:merge_request_diff)
-                                   .where(source_branch: ref, source_project_id: pipeline.gl_project_id)
+                                   .where(source_branch: ref,
+                                          source_project: pipeline.project)
                                    .reorder(iid: :asc)
 
       merge_requests.find do |merge_request|
@@ -231,10 +232,6 @@ module Ci
       end
     end
 
-    def project_id
-      gl_project_id
-    end
-
     def repo_url
       auth = "gitlab-ci-token:#{ensure_token!}@"
       project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
@@ -542,6 +539,16 @@ module Ci
       Gitlab::Ci::Build::Credentials::Factory.new(self).create!
     end
 
+    def dependencies
+      depended_jobs = depends_on_builds
+
+      return depended_jobs unless options[:dependencies].present?
+
+      depended_jobs.select do |job|
+        options[:dependencies].include?(job.name)
+      end
+    end
+
     private
 
     def update_artifacts_size
@@ -561,7 +568,7 @@ module Ci
     end
 
     def unscoped_project
-      @unscoped_project ||= Project.unscoped.find_by(id: gl_project_id)
+      @unscoped_project ||= Project.unscoped.find_by(id: project_id)
     end
 
     CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index d1009f88549047201dd9ce027966ff0ac180bffa..f12be98c80c0bf82f61fd1b09e2f2d254116725b 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -5,9 +5,7 @@ module Ci
     include Importable
     include AfterCommitQueue
 
-    self.table_name = 'ci_commits'
-
-    belongs_to :project, foreign_key: :gl_project_id
+    belongs_to :project
     belongs_to :user
 
     has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index edd21f984c880bccccca5da74a776b1bde2f86ba..487ba61bc9c7019618cf8a25f5c60961c8b3761f 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -9,7 +9,7 @@ module Ci
 
     has_many :builds
     has_many :runner_projects, dependent: :destroy
-    has_many :projects, through: :runner_projects, foreign_key: :gl_project_id
+    has_many :projects, through: :runner_projects
 
     has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
 
@@ -24,7 +24,7 @@ module Ci
 
     scope :owned_or_shared, ->(project_id) do
       joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
-        .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
+        .where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
     end
 
     scope :assignable_for, ->(project) do
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index 234376a7e4cbb62fd1d1cd4d550ebeb0a05eaaad..5f01a0daae917a85faed929d3e5c7c18db789e4d 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -1,10 +1,10 @@
 module Ci
   class RunnerProject < ActiveRecord::Base
     extend Ci::Model
-    
+
     belongs_to :runner
-    belongs_to :project, foreign_key: :gl_project_id
+    belongs_to :project
 
-    validates :runner_id, uniqueness: { scope: :gl_project_id }
+    validates :runner_id, uniqueness: { scope: :project_id }
   end
 end
diff --git a/app/models/ci/trigger.rb b/app/models/ci/trigger.rb
index 90473d41c042b8d17d2392436e3367eb2acf2e4d..cba1d81a8616d2c0bcab9308ae91b6534eb1a000 100644
--- a/app/models/ci/trigger.rb
+++ b/app/models/ci/trigger.rb
@@ -4,7 +4,7 @@ module Ci
 
     acts_as_paranoid
 
-    belongs_to :project, foreign_key: :gl_project_id
+    belongs_to :project
     belongs_to :owner, class_name: "User"
 
     has_many :trigger_requests, dependent: :destroy
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 2c8698d8b5de8d4f2605641a1dc10185cb751402..6c6586110c5e98ae54637f43a335419f86931bf6 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -2,11 +2,11 @@ module Ci
   class Variable < ActiveRecord::Base
     extend Ci::Model
 
-    belongs_to :project, foreign_key: :gl_project_id
+    belongs_to :project
 
     validates :key,
       presence: true,
-      uniqueness: { scope: :gl_project_id },
+      uniqueness: { scope: :project_id },
       length: { maximum: 255 },
       format: { with: /\A[a-zA-Z0-9_]+\z/,
                 message: "can contain only letters, digits and '_'." }
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 6ea5b1ae51fcf3c6c986718eb5f241c8c20229bf..ce92cc369ad2576bfe01470dee660af955468789 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -321,7 +321,14 @@ class Commit
   end
 
   def raw_diffs(*args)
-    raw.diffs(*args)
+    use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
+    deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
+
+    if use_gitaly && !deltas_only
+      Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
+    else
+      raw.diffs(*args)
+    end
   end
 
   def diffs(diff_options = nil)
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 7e23e14794f7403af428054d1eb5ce8b228dcc2e..8c71267da65d04d7a71024bd921691582526bc86 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base
 
   self.table_name = 'ci_builds'
 
-  belongs_to :project, foreign_key: :gl_project_id
+  belongs_to :project
   belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
   belongs_to :user
 
@@ -133,6 +133,12 @@ class CommitStatus < ActiveRecord::Base
     false
   end
 
+  # Added in 9.0 to keep backward compatibility for projects exported in 8.17
+  # and prior.
+  def gl_project_id
+    'dummy'
+  end
+
   def detailed_status(current_user)
     Gitlab::Ci::Status::Factory
       .new(self, current_user)
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 91f4eb13eccf77abd3567f2d4e93d73da78d2858..e7bd20b322ab633d3cda1f2169f6875505b71d31 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -48,11 +48,13 @@ module Issuable
 
     delegate :name,
              :email,
+             :public_email,
              to: :author,
              prefix: true
 
     delegate :name,
              :email,
+             :public_email,
              to: :assignee,
              allow_nil: true,
              prefix: true
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 9f6d215ceb3a3c4b3d7d7f37b9c19d9ed5cfd494..529fb5ce988be46d07cda9ff5e6d7fbb4d9e9da3 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -51,11 +51,13 @@ module Routable
 
       paths.each do |path|
         path = connection.quote(path)
-        where = "(routes.path = #{path})"
 
-        if cast_lower
-          where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))"
-        end
+        where =
+          if cast_lower
+            "(LOWER(routes.path) = LOWER(#{path}))"
+          else
+            "(routes.path = #{path})"
+          end
 
         wheres << where
       end
diff --git a/app/models/event.rb b/app/models/event.rb
index d7ca8e3c5997a5c3dd15c0558ec600c2b6d597da..5c34844b5d30a86573bf11ab50ff30775aeff2b1 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base
 
   RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
 
-  delegate :name, :email, to: :author, prefix: true, allow_nil: true
+  delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true
   delegate :title, to: :issue, prefix: true, allow_nil: true
   delegate :title, to: :merge_request, prefix: true, allow_nil: true
   delegate :title, to: :note, prefix: true, allow_nil: true
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 1427fdc31a4d68c8817459161d5ebec5e0fadb55..602eed86d9ed78035fc10933dc6a93bdb3af764b 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -55,6 +55,14 @@ class Issue < ActiveRecord::Base
     state :opened
     state :reopened
     state :closed
+
+    before_transition any => :closed do |issue|
+      issue.closed_at = Time.zone.now
+    end
+
+    before_transition closed: any do |issue|
+      issue.closed_at = nil
+    end
   end
 
   def hook_attrs
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 4759829a15c3bec5e0fe85d80e92a372104b132f..cef8ad76b07df9a021ab4e40f28a1559ec3b8cf1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -7,6 +7,7 @@ class MergeRequest < ActiveRecord::Base
 
   belongs_to :target_project, class_name: "Project"
   belongs_to :source_project, class_name: "Project"
+  belongs_to :project, foreign_key: :target_project_id
   belongs_to :merge_user, class_name: "User"
 
   has_many :merge_request_diffs, dependent: :destroy
@@ -540,10 +541,6 @@ class MergeRequest < ActiveRecord::Base
     target_project != source_project
   end
 
-  def project
-    target_project
-  end
-
   # If the merge request closes any issues, save this information in the
   # `MergeRequestsClosingIssues` model. This is a performance optimization.
   # Calculating this information for a number of merge requests requires
diff --git a/app/models/project.rb b/app/models/project.rb
index 2ffaaac93f3716edd6587ed438421ecb76cd7d58..da4704554b3f6fd08d87ded562316a3329ddec0b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -89,7 +89,6 @@ class Project < ActiveRecord::Base
   has_one :campfire_service, dependent: :destroy
   has_one :drone_ci_service, dependent: :destroy
   has_one :emails_on_push_service, dependent: :destroy
-  has_one :builds_email_service, dependent: :destroy
   has_one :pipelines_email_service, dependent: :destroy
   has_one :irker_service, dependent: :destroy
   has_one :pivotaltracker_service, dependent: :destroy
@@ -159,13 +158,13 @@ class Project < ActiveRecord::Base
   has_one :project_feature, dependent: :destroy
   has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
 
-  has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id
-  has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id
-  has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses
-  has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id
+  has_many :commit_statuses, dependent: :destroy
+  has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline'
+  has_many :builds, class_name: 'Ci::Build' # the builds are created from the commit_statuses
+  has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
   has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
-  has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id
-  has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id
+  has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
+  has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
   has_many :environments, dependent: :destroy
   has_many :deployments, dependent: :destroy
 
@@ -197,6 +196,7 @@ class Project < ActiveRecord::Base
   validates :name, uniqueness: { scope: :namespace_id }
   validates :path, uniqueness: { scope: :namespace_id }
   validates :import_url, addressable_url: true, if: :external_import?
+  validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
   validates :star_count, numericality: { greater_than_or_equal_to: 0 }
   validate :check_limit, on: :create
   validate :avatar_type,
@@ -881,13 +881,9 @@ class Project < ActiveRecord::Base
   end
 
   def http_url_to_repo(user = nil)
-    url = web_url
+    credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
 
-    if user
-      url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" }
-    end
-
-    "#{url}.git"
+    Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url
   end
 
   # Check if current branch name is marked as protected in the system
diff --git a/app/models/project_services/builds_email_service.rb b/app/models/project_services/builds_email_service.rb
index ebd21e3718910bcb25076fbff710d7b97e93c4d3..0c526b53d724c1012f6cba22062743904a0e6f72 100644
--- a/app/models/project_services/builds_email_service.rb
+++ b/app/models/project_services/builds_email_service.rb
@@ -1,107 +1,11 @@
+# This class is to be removed with 9.1
+# We should also by then remove BuildsEmailService from database
 class BuildsEmailService < Service
-  prop_accessor :recipients
-  boolean_accessor :add_pusher
-  boolean_accessor :notify_only_broken_builds
-  validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? }
-
-  def initialize_properties
-    if properties.nil?
-      self.properties = {}
-      self.notify_only_broken_builds = true
-    end
-  end
-
-  def title
-    'Builds emails'
-  end
-
-  def description
-    'Email the builds status to a list of recipients.'
-  end
-
   def self.to_param
     'builds_email'
   end
 
   def self.supported_events
-    %w(build)
-  end
-
-  def execute(push_data)
-    return unless supported_events.include?(push_data[:object_kind])
-    return unless should_build_be_notified?(push_data)
-
-    recipients = all_recipients(push_data)
-
-    if recipients.any?
-      BuildEmailWorker.perform_async(
-        push_data[:build_id],
-        recipients,
-        push_data
-      )
-    end
-  end
-
-  def can_test?
-    project.builds.any?
-  end
-
-  def disabled_title
-    "Please setup a build on your repository."
-  end
-
-  def test_data(project = nil, user = nil)
-    Gitlab::DataBuilder::Build.build(project.builds.last)
-  end
-
-  def fields
-    [
-      { type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' },
-      { type: 'checkbox', name: 'add_pusher', label: 'Add pusher to recipients list' },
-      { type: 'checkbox', name: 'notify_only_broken_builds' },
-    ]
-  end
-
-  def test(data)
-    begin
-      # bypass build status verification when testing
-      data[:build_status] = "failed"
-      data[:build_allow_failure] = false
-
-      result = execute(data)
-    rescue StandardError => error
-      return { success: false, result: error }
-    end
-
-    { success: true, result: result }
-  end
-
-  def should_build_be_notified?(data)
-    case data[:build_status]
-    when 'success'
-      !notify_only_broken_builds?
-    when 'failed'
-      !allow_failure?(data)
-    else
-      false
-    end
-  end
-
-  def allow_failure?(data)
-    data[:build_allow_failure] == true
-  end
-
-  def all_recipients(data)
-    all_recipients = []
-
-    unless recipients.blank?
-      all_recipients += recipients.split(',').compact.reject(&:blank?)
-    end
-
-    if add_pusher? && data[:user][:email]
-      all_recipients << data[:user][:email]
-    end
-
-    all_recipients
+    %w[]
   end
 end
diff --git a/app/models/project_services/chat_message/build_message.rb b/app/models/project_services/chat_message/build_message.rb
deleted file mode 100644
index c776e0a20c411fb964786fddf00390d6e08cc4d9..0000000000000000000000000000000000000000
--- a/app/models/project_services/chat_message/build_message.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-module ChatMessage
-  class BuildMessage < BaseMessage
-    attr_reader :sha
-    attr_reader :ref_type
-    attr_reader :ref
-    attr_reader :status
-    attr_reader :project_name
-    attr_reader :project_url
-    attr_reader :user_name
-    attr_reader :user_url
-    attr_reader :duration
-    attr_reader :stage
-    attr_reader :build_id
-    attr_reader :build_name
-
-    def initialize(params)
-      @sha = params[:sha]
-      @ref_type = params[:tag] ? 'tag' : 'branch'
-      @ref = params[:ref]
-      @project_name = params[:project_name]
-      @project_url = params[:project_url]
-      @status = params[:commit][:status]
-      @user_name = params[:commit][:author_name]
-      @user_url = params[:commit][:author_url]
-      @duration = params[:commit][:duration]
-      @stage = params[:build_stage]
-      @build_name = params[:build_name]
-      @build_id = params[:build_id]
-    end
-
-    def pretext
-      ''
-    end
-
-    def fallback
-      format(message)
-    end
-
-    def attachments
-      [{ text: format(message), color: attachment_color }]
-    end
-
-    private
-
-    def message
-      "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_link} #{humanized_status} on build #{build_link} of stage #{stage} in #{duration} #{'second'.pluralize(duration)}"
-    end
-
-    def build_url
-      "#{project_url}/builds/#{build_id}"
-    end
-
-    def build_link
-      link(build_name, build_url)
-    end
-
-    def user_link
-      link(user_name, user_url)
-    end
-
-    def format(string)
-      Slack::Notifier::LinkFormatter.format(string)
-    end
-
-    def humanized_status
-      case status
-      when 'success'
-        'passed'
-      else
-        status
-      end
-    end
-
-    def attachment_color
-      if status == 'success'
-        'good'
-      else
-        'danger'
-      end
-    end
-
-    def branch_url
-      "#{project_url}/commits/#{ref}"
-    end
-
-    def branch_link
-      link(ref, branch_url)
-    end
-
-    def project_link
-      link(project_name, project_url)
-    end
-
-    def commit_url
-      "#{project_url}/commit/#{sha}/builds"
-    end
-
-    def commit_link
-      link(Commit.truncate_sha(sha), commit_url)
-    end
-  end
-end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 8468934425fc113a0ee40147528820d9165c13a0..200be99f36b8e42da5c28cc1147b1fbbcece5218 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -6,7 +6,7 @@ class ChatNotificationService < Service
   default_value_for :category, 'chat'
 
   prop_accessor :webhook, :username, :channel
-  boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
+  boolean_accessor :notify_only_broken_pipelines
 
   validates :webhook, presence: true, url: true, if: :activated?
 
@@ -16,7 +16,6 @@ class ChatNotificationService < Service
 
     if properties.nil?
       self.properties = {}
-      self.notify_only_broken_builds = true
       self.notify_only_broken_pipelines = true
     end
   end
@@ -27,7 +26,7 @@ class ChatNotificationService < Service
 
   def self.supported_events
     %w[push issue confidential_issue merge_request note tag_push
-       build pipeline wiki_page]
+       pipeline wiki_page]
   end
 
   def execute(data)
@@ -89,8 +88,6 @@ class ChatNotificationService < Service
       ChatMessage::MergeMessage.new(data) unless is_update?(data)
     when "note"
       ChatMessage::NoteMessage.new(data)
-    when "build"
-      ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data)
     when "pipeline"
       ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
     when "wiki_page"
@@ -125,17 +122,6 @@ class ChatNotificationService < Service
     data[:object_attributes][:action] == 'update'
   end
 
-  def should_build_be_notified?(data)
-    case data[:commit][:status]
-    when 'success'
-      !notify_only_broken_builds?
-    when 'failed'
-      true
-    else
-      false
-    end
-  end
-
   def should_pipeline_be_notified?(data)
     case data[:object_attributes][:status]
     when 'success'
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index c4142c38b2f4fe809087169b94d909afad607811..8b181221bb044cf9689085dcd0888ccf9954a5b8 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -9,13 +9,13 @@ class HipchatService < Service
   ].freeze
 
   prop_accessor :token, :room, :server, :color, :api_version
-  boolean_accessor :notify_only_broken_builds, :notify
+  boolean_accessor :notify_only_broken_pipelines, :notify
   validates :token, presence: true, if: :activated?
 
   def initialize_properties
     if properties.nil?
       self.properties = {}
-      self.notify_only_broken_builds = true
+      self.notify_only_broken_pipelines = true
     end
   end
 
@@ -41,12 +41,12 @@ class HipchatService < Service
         placeholder: 'Leave blank for default (v2)' },
       { type: 'text', name: 'server',
         placeholder: 'Leave blank for default. https://hipchat.example.com' },
-      { type: 'checkbox', name: 'notify_only_broken_builds' },
+      { type: 'checkbox', name: 'notify_only_broken_pipelines' },
     ]
   end
 
   def self.supported_events
-    %w(push issue confidential_issue merge_request note tag_push build)
+    %w(push issue confidential_issue merge_request note tag_push pipeline)
   end
 
   def execute(data)
@@ -90,8 +90,8 @@ class HipchatService < Service
       create_merge_request_message(data) unless is_update?(data)
     when "note"
       create_note_message(data)
-    when "build"
-      create_build_message(data) if should_build_be_notified?(data)
+    when "pipeline"
+      create_pipeline_message(data) if should_pipeline_be_notified?(data)
     end
   end
 
@@ -240,28 +240,29 @@ class HipchatService < Service
     message
   end
 
-  def create_build_message(data)
-    ref_type = data[:tag] ? 'tag' : 'branch'
-    ref = data[:ref]
-    sha = data[:sha]
-    user_name = data[:commit][:author_name]
-    status = data[:commit][:status]
-    duration = data[:commit][:duration]
+  def create_pipeline_message(data)
+    pipeline_attributes = data[:object_attributes]
+    pipeline_id = pipeline_attributes[:id]
+    ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+    ref = pipeline_attributes[:ref]
+    user_name = (data[:user] && data[:user][:name]) || 'API'
+    status = pipeline_attributes[:status]
+    duration = pipeline_attributes[:duration]
 
     branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"
-    commit_link = "<a href=\"#{project_url}/commit/#{CGI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>"
+    pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>"
 
-    "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
+    "#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
   end
 
   def message_color(data)
-    build_status_color(data) || color || 'yellow'
+    pipeline_status_color(data) || color || 'yellow'
   end
 
-  def build_status_color(data)
-    return unless data && data[:object_kind] == 'build'
+  def pipeline_status_color(data)
+    return unless data && data[:object_kind] == 'pipeline'
 
-    case data[:commit][:status]
+    case data[:object_attributes][:status]
     when 'success'
       'green'
     else
@@ -294,10 +295,10 @@ class HipchatService < Service
     end
   end
 
-  def should_build_be_notified?(data)
-    case data[:commit][:status]
+  def should_pipeline_be_notified?(data)
+    case data[:object_attributes][:status]
     when 'success'
-      !notify_only_broken_builds?
+      !notify_only_broken_pipelines?
     when 'failed'
       true
     else
diff --git a/app/models/project_services/mattermost_service.rb b/app/models/project_services/mattermost_service.rb
index c13538e9fea2685a6649bb84176c654fa9899505..1156d05062203cfe2cff7fea2e8bb121594e455b 100644
--- a/app/models/project_services/mattermost_service.rb
+++ b/app/models/project_services/mattermost_service.rb
@@ -30,7 +30,6 @@ class MattermostService < ChatNotificationService
     [
       { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
       { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
-      { type: 'checkbox', name: 'notify_only_broken_builds' },
       { type: 'checkbox', name: 'notify_only_broken_pipelines' },
     ]
   end
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index da7496573ef160211166c7aee4f876218ba50a58..b657db6f9ee126d3b7866e5ea27963174127152e 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -29,7 +29,6 @@ class SlackService < ChatNotificationService
     [
       { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
       { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
-      { type: 'checkbox', name: 'notify_only_broken_builds' },
       { type: 'checkbox', name: 'notify_only_broken_pipelines' },
     ]
   end
diff --git a/app/models/project_wiki.rb b/app/models/project_wiki.rb
index 539b31780b32fc166273beb2d12b0f093f895746..70eef359cdd0920c1586211919ef16c7fc38ecf7 100644
--- a/app/models/project_wiki.rb
+++ b/app/models/project_wiki.rb
@@ -42,8 +42,11 @@ class ProjectWiki
     url_to_repo
   end
 
-  def http_url_to_repo
-    [Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('')
+  def http_url_to_repo(user = nil)
+    url = "#{Gitlab.config.gitlab.url}/#{path_with_namespace}.git"
+    credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
+
+    Gitlab::UrlSanitizer.new(url, credentials: credentials).full_url
   end
 
   def wiki_base_path
diff --git a/app/models/route.rb b/app/models/route.rb
index 73574a6206b6e1d21918f6ab37e0437a66de42c0..41e6eb7cb732c2c195c617abf39052b0dd6a26d2 100644
--- a/app/models/route.rb
+++ b/app/models/route.rb
@@ -21,7 +21,7 @@ class Route < ActiveRecord::Base
           attributes[:path] = route.path.sub(path_was, path)
         end
 
-        if name_changed? && route.name.present?
+        if name_changed? && name_was.present? && route.name.present?
           attributes[:name] = route.name.sub(name_was, name)
         end
 
diff --git a/app/models/service.rb b/app/models/service.rb
index 2f75a2e4e7fa9c94314284c031aa11b1c14fe171..e73f7e5d1a3530b8279c6d052a772bd6a7b225a9 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -215,7 +215,6 @@ class Service < ActiveRecord::Base
       assembla
       bamboo
       buildkite
-      builds_email
       bugzilla
       campfire
       custom_issue_tracker
diff --git a/app/models/user.rb b/app/models/user.rb
index 39c1281179b234c3ba0fb2b7090562e4de61fb42..8c7ad5d51741f3e823c7033ba5e71cc3a86c9563 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -877,7 +877,7 @@ class User < ActiveRecord::Base
   def ci_authorized_runners
     @ci_authorized_runners ||= begin
       runner_ids = Ci::RunnerProject.
-        where("ci_runner_projects.gl_project_id IN (#{ci_projects_union.to_sql})").
+        where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})").
         select(:runner_id)
       Ci::Runner.specific.where(id: runner_ids)
     end
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 465c4d903ac3bc9c4bde6cfa4f0f86bcf553dcb1..c771c22f46a1620b37986d738eedac588be66cfc 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -155,7 +155,7 @@ class WikiPage
   end
 
   # Returns boolean True or False if this instance
-  # has been fully saved to disk or not.
+  # has been fully created on disk or not.
   def persisted?
     @persisted == true
   end
@@ -226,6 +226,8 @@ class WikiPage
   end
 
   def save(method, *args)
+    saved = false
+
     project_wiki = wiki
     if valid? && project_wiki.send(method, *args)
 
@@ -243,10 +245,10 @@ class WikiPage
       set_attributes
 
       @persisted = true
+      saved = true
     else
       errors.add(:base, project_wiki.error_message) if project_wiki.error_message
-      @persisted = false
     end
-    @persisted
+    saved
   end
 end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index 0ab9042bf2454e7e8d31d7447f5e5b0aaa778255..d6a4280ce4c514cd487843c8d4f173e2b2e3a456 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -55,13 +55,13 @@ module Ci
       new_builds.
         # don't run projects which have not enabled shared runners and builds
         joins(:project).where(projects: { shared_runners_enabled: true }).
-        joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id').
+        joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id').
         where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
 
         # Implement fair scheduling
         # this returns builds that are ordered by number of running builds
         # we prefer projects that don't use shared runners at all
-        joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id").
+        joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id").
         order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
     end
 
@@ -71,7 +71,7 @@ module Ci
 
     def running_builds_for_shared_runners
       Ci::Build.running.where(runner: Ci::Runner.shared).
-        group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds')
+        group(:project_id).select(:project_id, 'count(*) AS running_builds')
     end
 
     def new_builds
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 9d4739e37bba66901e4d6d09cc0bb555c80670d1..fdce542bd9e04080379ef2c5e338c66a56ff5efe 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -6,7 +6,7 @@ module MergeRequests
       merge_request.source_project  = find_source_project
       merge_request.target_project  = find_target_project
       merge_request.target_branch   = find_target_branch
-      merge_request.can_be_created  = branches_valid? && source_branch_specified? && target_branch_specified?
+      merge_request.can_be_created  = branches_valid?
 
       compare_branches if branches_present?
       assign_title_and_description if merge_request.can_be_created
diff --git a/app/services/notification_recipient_service.rb b/app/services/notification_recipient_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..44ae23fad18109807352daa045278710a875f5db
--- /dev/null
+++ b/app/services/notification_recipient_service.rb
@@ -0,0 +1,293 @@
+#
+# Used by NotificationService to determine who should receive notification
+#
+class NotificationRecipientService
+  attr_reader :project
+  
+  def initialize(project)
+    @project = project
+  end
+
+  def build_recipients(target, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
+    custom_action = build_custom_key(action, target)
+
+    recipients = target.participants(current_user)
+
+    unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
+      recipients = add_project_watchers(recipients)
+    end
+
+    recipients = add_custom_notifications(recipients, custom_action)
+    recipients = reject_mention_users(recipients)
+
+    # Re-assign is considered as a mention of the new assignee so we add the
+    # new assignee to the list of recipients after we rejected users with
+    # the "on mention" notification level
+    if [:reassign_merge_request, :reassign_issue].include?(custom_action)
+      recipients << previous_assignee if previous_assignee
+      recipients << target.assignee
+    end
+
+    recipients = reject_muted_users(recipients)
+    recipients = add_subscribed_users(recipients, target)
+
+    if [:new_issue, :new_merge_request].include?(custom_action)
+      recipients = add_labels_subscribers(recipients, target)
+    end
+
+    recipients = reject_unsubscribed_users(recipients, target)
+    recipients = reject_users_without_access(recipients, target)
+
+    recipients.delete(current_user) if skip_current_user
+
+    recipients.uniq
+  end
+
+  def build_relabeled_recipients(target, current_user, labels:)
+    recipients = add_labels_subscribers([], target, labels: labels)
+    recipients = reject_unsubscribed_users(recipients, target)
+    recipients = reject_users_without_access(recipients, target)
+    recipients.delete(current_user)
+    recipients.uniq
+  end
+
+  def build_new_note_recipients(note)
+    target = note.noteable
+
+    ability, subject = if note.for_personal_snippet?
+                         [:read_personal_snippet, note.noteable]
+                       else
+                         [:read_project, note.project]
+                       end
+
+    mentioned_users = note.mentioned_users.select { |user| user.can?(ability, subject) }
+
+    # Add all users participating in the thread (author, assignee, comment authors)
+    recipients =
+      if target.respond_to?(:participants)
+        target.participants(note.author)
+      else
+        mentioned_users
+      end
+
+    unless note.for_personal_snippet?
+      # Merge project watchers
+      recipients = add_project_watchers(recipients)
+
+      # Merge project with custom notification
+      recipients = add_custom_notifications(recipients, :new_note)
+    end
+
+    # Reject users with Mention notification level, except those mentioned in _this_ note.
+    recipients = reject_mention_users(recipients - mentioned_users)
+    recipients = recipients + mentioned_users
+
+    recipients = reject_muted_users(recipients)
+
+    recipients = add_subscribed_users(recipients, note.noteable)
+    recipients = reject_unsubscribed_users(recipients, note.noteable)
+    recipients = reject_users_without_access(recipients, note.noteable)
+
+    recipients.delete(note.author)
+    recipients.uniq
+  end
+
+  # Remove users with disabled notifications from array
+  # Also remove duplications and nil recipients
+  def reject_muted_users(users)
+    reject_users(users, :disabled)
+  end
+
+  protected
+
+  # Get project/group users with CUSTOM notification level
+  def add_custom_notifications(recipients, action)
+    user_ids = []
+
+    # Users with a notification setting on group or project
+    user_ids += user_ids_notifiable_on(project, :custom, action)
+    user_ids += user_ids_notifiable_on(project.group, :custom, action)
+
+    # Users with global level custom
+    user_ids_with_project_level_global = user_ids_notifiable_on(project, :global)
+    user_ids_with_group_level_global   = user_ids_notifiable_on(project.group, :global)
+
+    global_users_ids = user_ids_with_project_level_global.concat(user_ids_with_group_level_global)
+    user_ids += user_ids_with_global_level_custom(global_users_ids, action)
+
+    recipients.concat(User.find(user_ids))
+  end
+
+  def add_project_watchers(recipients)
+    recipients.concat(project_watchers).compact
+  end
+
+  # Get project users with WATCH notification level
+  def project_watchers
+    project_members_ids = user_ids_notifiable_on(project)
+
+    user_ids_with_project_global = user_ids_notifiable_on(project, :global)
+    user_ids_with_group_global   = user_ids_notifiable_on(project.group, :global)
+
+    user_ids = user_ids_with_global_level_watch((user_ids_with_project_global + user_ids_with_group_global).uniq)
+
+    user_ids_with_project_setting = select_project_members_ids(project, user_ids_with_project_global, user_ids)
+    user_ids_with_group_setting = select_group_members_ids(project.group, project_members_ids, user_ids_with_group_global, user_ids)
+
+    User.where(id: user_ids_with_project_setting.concat(user_ids_with_group_setting).uniq).to_a
+  end
+
+  # Remove users with notification level 'Mentioned'
+  def reject_mention_users(users)
+    reject_users(users, :mention)
+  end
+
+  def add_subscribed_users(recipients, target)
+    return recipients unless target.respond_to? :subscribers
+
+    recipients + target.subscribers(project)
+  end
+
+  def user_ids_notifiable_on(resource, notification_level = nil, action = nil)
+    return [] unless resource
+
+    if notification_level
+      settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
+      settings = settings.select { |setting| setting.events[action] } if action.present?
+      settings.map(&:user_id)
+    else
+      resource.notification_settings.pluck(:user_id)
+    end
+  end
+
+  # Build a list of user_ids based on project notification settings
+  def select_project_members_ids(project, global_setting, user_ids_global_level_watch)
+    user_ids = user_ids_notifiable_on(project, :watch)
+
+    # If project setting is global, add to watch list if global setting is watch
+    global_setting.each do |user_id|
+      if user_ids_global_level_watch.include?(user_id)
+        user_ids << user_id
+      end
+    end
+
+    user_ids
+  end
+
+  # Build a list of user_ids based on group notification settings
+  def select_group_members_ids(group, project_members, global_setting, user_ids_global_level_watch)
+    uids = user_ids_notifiable_on(group, :watch)
+
+    # Group setting is watch, add to user_ids list if user is not project member
+    user_ids = []
+    uids.each do |user_id|
+      if project_members.exclude?(user_id)
+        user_ids << user_id
+      end
+    end
+
+    # Group setting is global, add to user_ids list if global setting is watch
+    global_setting.each do |user_id|
+      if project_members.exclude?(user_id) && user_ids_global_level_watch.include?(user_id)
+        user_ids << user_id
+      end
+    end
+
+    user_ids
+  end
+
+  def user_ids_with_global_level_watch(ids)
+    settings_with_global_level_of(:watch, ids).pluck(:user_id)
+  end
+
+  def user_ids_with_global_level_custom(ids, action)
+    settings = settings_with_global_level_of(:custom, ids)
+    settings = settings.select { |setting| setting.events[action] }
+    settings.map(&:user_id)
+  end
+
+  def settings_with_global_level_of(level, ids)
+    NotificationSetting.where(
+      user_id: ids,
+      source_type: nil,
+      level: NotificationSetting.levels[level]
+    )
+  end
+
+  # Reject users which has certain notification level
+  #
+  # Example:
+  #   reject_users(users, :watch, project)
+  #
+  def reject_users(users, level)
+    level = level.to_s
+
+    unless NotificationSetting.levels.keys.include?(level)
+      raise 'Invalid notification level'
+    end
+
+    users = users.to_a.compact.uniq
+    users = users.select { |u| u.can?(:receive_notifications) }
+
+    users.reject do |user|
+      global_notification_setting = user.global_notification_setting
+
+      next global_notification_setting.level == level unless project
+
+      setting = user.notification_settings_for(project)
+
+      if project.group && (setting.nil? || setting.global?)
+        setting = user.notification_settings_for(project.group)
+      end
+
+      # reject users who globally set mention notification and has no setting per project/group
+      next global_notification_setting.level == level unless setting
+
+      # reject users who set mention notification in project
+      next true if setting.level == level
+
+      # reject users who have mention level in project and disabled in global settings
+      setting.global? && global_notification_setting.level == level
+    end
+  end
+
+  def reject_unsubscribed_users(recipients, target)
+    return recipients unless target.respond_to? :subscriptions
+
+    recipients.reject do |user|
+      subscription = target.subscriptions.find_by_user_id(user.id)
+      subscription && !subscription.subscribed
+    end
+  end
+
+  def reject_users_without_access(recipients, target)
+    ability = case target
+              when Issuable
+                :"read_#{target.to_ability_name}"
+              when Ci::Pipeline
+                :read_build # We have build trace in pipeline emails
+              end
+
+    return recipients unless ability
+
+    recipients.select do |user|
+      user.can?(ability, target)
+    end
+  end
+
+  def add_labels_subscribers(recipients, target, labels: nil)
+    return recipients unless target.respond_to? :labels
+
+    (labels || target.labels).each do |label|
+      recipients += label.subscribers(project)
+    end
+
+    recipients
+  end
+
+  # Build event key to search on custom notification level
+  # Check NotificationSetting::EMAIL_EVENTS
+  def build_custom_key(action, object)
+    "#{action}_#{object.class.model_name.name.underscore}".to_sym
+  end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index fdaba9b95fbe493ea67d95b3c7760980ff4e6c56..f9aa234675982a651e4c3d78bf4f1c04a7eebde6 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -150,7 +150,10 @@ class NotificationService
   end
 
   def resolve_all_discussions(merge_request, current_user)
-    recipients = build_recipients(merge_request, merge_request.target_project, current_user, action: "resolve_all_discussions")
+    recipients = NotificationRecipientService.new(merge_request.target_project).build_recipients(
+      merge_request,
+      current_user,
+      action: "resolve_all_discussions")
 
     recipients.each do |recipient|
       mailer.resolved_all_discussions_email(recipient.id, merge_request.id, current_user.id).deliver_later
@@ -164,64 +167,15 @@ class NotificationService
   end
 
   # Notify users on new note in system
-  #
-  # TODO: split on methods and refactor
-  #
   def new_note(note)
     return true unless note.noteable_type.present?
 
     # ignore gitlab service messages
     return true if note.cross_reference? && note.system?
 
-    target = note.noteable
-
-    recipients = []
-
-    mentioned_users = note.mentioned_users
-
-    ability, subject = if note.for_personal_snippet?
-                         [:read_personal_snippet, note.noteable]
-                       else
-                         [:read_project, note.project]
-                       end
-
-    mentioned_users.select! do |user|
-      user.can?(ability, subject)
-    end
-
-    # Add all users participating in the thread (author, assignee, comment authors)
-    participants =
-      if target.respond_to?(:participants)
-        target.participants(note.author)
-      else
-        mentioned_users
-      end
-
-    recipients = recipients.concat(participants)
-
-    unless note.for_personal_snippet?
-      # Merge project watchers
-      recipients = add_project_watchers(recipients, note.project)
-
-      # Merge project with custom notification
-      recipients = add_custom_notifications(recipients, note.project, :new_note)
-    end
-
-    # Reject users with Mention notification level, except those mentioned in _this_ note.
-    recipients = reject_mention_users(recipients - mentioned_users, note.project)
-    recipients = recipients + mentioned_users
-
-    recipients = reject_muted_users(recipients, note.project)
-
-    recipients = add_subscribed_users(recipients, note.project, note.noteable)
-    recipients = reject_unsubscribed_users(recipients, note.noteable)
-    recipients = reject_users_without_access(recipients, note.noteable)
-
-    recipients.delete(note.author)
-    recipients = recipients.uniq
-
     notify_method = "note_#{note.to_ability_name}_email".to_sym
 
+    recipients = NotificationRecipientService.new(note.project).build_new_note_recipients(note)
     recipients.each do |recipient|
       mailer.send(notify_method, recipient.id, note.id).deliver_later
     end
@@ -290,7 +244,7 @@ class NotificationService
 
   def project_was_moved(project, old_path_with_namespace)
     recipients = project.team.members
-    recipients = reject_muted_users(recipients, project)
+    recipients = NotificationRecipientService.new(project).reject_muted_users(recipients)
 
     recipients.each do |recipient|
       mailer.project_was_moved_email(
@@ -302,7 +256,7 @@ class NotificationService
   end
 
   def issue_moved(issue, new_issue, current_user)
-    recipients = build_recipients(issue, issue.project, current_user)
+    recipients = NotificationRecipientService.new(issue.project).build_recipients(issue, current_user)
 
     recipients.map do |recipient|
       email = mailer.issue_moved_email(recipient, issue, new_issue, current_user)
@@ -324,9 +278,8 @@ class NotificationService
 
     return unless mailer.respond_to?(email_template)
 
-    recipients ||= build_recipients(
+    recipients ||= NotificationRecipientService.new(pipeline.project).build_recipients(
       pipeline,
-      pipeline.project,
       nil, # The acting user, who won't be added to recipients
       action: pipeline.status).map(&:notification_email)
 
@@ -337,199 +290,8 @@ class NotificationService
 
   protected
 
-  # Get project/group users with CUSTOM notification level
-  def add_custom_notifications(recipients, project, action)
-    user_ids = []
-
-    # Users with a notification setting on group or project
-    user_ids += notification_settings_for(project, :custom, action)
-    user_ids += notification_settings_for(project.group, :custom, action)
-
-    # Users with global level custom
-    users_with_project_level_global = notification_settings_for(project, :global)
-    users_with_group_level_global   = notification_settings_for(project.group, :global)
-
-    global_users_ids = users_with_project_level_global.concat(users_with_group_level_global)
-    user_ids += users_with_global_level_custom(global_users_ids, action)
-
-    recipients.concat(User.find(user_ids))
-  end
-
-  # Get project users with WATCH notification level
-  def project_watchers(project)
-    project_members = notification_settings_for(project)
-
-    users_with_project_level_global = notification_settings_for(project, :global)
-    users_with_group_level_global   = notification_settings_for(project.group, :global)
-
-    users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
-
-    users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
-    users_with_group_setting = select_group_member_setting(project.group, project_members, users_with_group_level_global, users)
-
-    User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
-  end
-
-  def notification_settings_for(resource, notification_level = nil, action = nil)
-    return [] unless resource
-
-    if notification_level
-      settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
-      settings = settings.select { |setting| setting.events[action] } if action.present?
-      settings.map(&:user_id)
-    else
-      resource.notification_settings.pluck(:user_id)
-    end
-  end
-
-  def users_with_global_level_watch(ids)
-    settings_with_global_level_of(:watch, ids).pluck(:user_id)
-  end
-
-  def users_with_global_level_custom(ids, action)
-    settings = settings_with_global_level_of(:custom, ids)
-    settings = settings.select { |setting| setting.events[action] }
-    settings.map(&:user_id)
-  end
-
-  def settings_with_global_level_of(level, ids)
-    NotificationSetting.where(
-      user_id: ids,
-      source_type: nil,
-      level: NotificationSetting.levels[level]
-    )
-  end
-
-  # Build a list of users based on project notification settings
-  def select_project_member_setting(project, global_setting, users_global_level_watch)
-    users = notification_settings_for(project, :watch)
-
-    # If project setting is global, add to watch list if global setting is watch
-    global_setting.each do |user_id|
-      if users_global_level_watch.include?(user_id)
-        users << user_id
-      end
-    end
-
-    users
-  end
-
-  # Build a list of users based on group notification settings
-  def select_group_member_setting(group, project_members, global_setting, users_global_level_watch)
-    uids = notification_settings_for(group, :watch)
-
-    # Group setting is watch, add to users list if user is not project member
-    users = []
-    uids.each do |user_id|
-      if project_members.exclude?(user_id)
-        users << user_id
-      end
-    end
-
-    # Group setting is global, add to users list if global setting is watch
-    global_setting.each do |user_id|
-      if project_members.exclude?(user_id) && users_global_level_watch.include?(user_id)
-        users << user_id
-      end
-    end
-
-    users
-  end
-
-  def add_project_watchers(recipients, project)
-    recipients.concat(project_watchers(project)).compact
-  end
-
-  # Remove users with disabled notifications from array
-  # Also remove duplications and nil recipients
-  def reject_muted_users(users, project = nil)
-    reject_users(users, :disabled, project)
-  end
-
-  # Remove users with notification level 'Mentioned'
-  def reject_mention_users(users, project = nil)
-    reject_users(users, :mention, project)
-  end
-
-  # Reject users which has certain notification level
-  #
-  # Example:
-  #   reject_users(users, :watch, project)
-  #
-  def reject_users(users, level, project = nil)
-    level = level.to_s
-
-    unless NotificationSetting.levels.keys.include?(level)
-      raise 'Invalid notification level'
-    end
-
-    users = users.to_a.compact.uniq
-    users = users.select { |u| u.can?(:receive_notifications) }
-
-    users.reject do |user|
-      global_notification_setting = user.global_notification_setting
-
-      next global_notification_setting.level == level unless project
-
-      setting = user.notification_settings_for(project)
-
-      if project.group && (setting.nil? || setting.global?)
-        setting = user.notification_settings_for(project.group)
-      end
-
-      # reject users who globally set mention notification and has no setting per project/group
-      next global_notification_setting.level == level unless setting
-
-      # reject users who set mention notification in project
-      next true if setting.level == level
-
-      # reject users who have mention level in project and disabled in global settings
-      setting.global? && global_notification_setting.level == level
-    end
-  end
-
-  def reject_unsubscribed_users(recipients, target)
-    return recipients unless target.respond_to? :subscriptions
-
-    recipients.reject do |user|
-      subscription = target.subscriptions.find_by_user_id(user.id)
-      subscription && !subscription.subscribed
-    end
-  end
-
-  def reject_users_without_access(recipients, target)
-    ability = case target
-              when Issuable
-                :"read_#{target.to_ability_name}"
-              when Ci::Pipeline
-                :read_build # We have build trace in pipeline emails
-              end
-
-    return recipients unless ability
-
-    recipients.select do |user|
-      user.can?(ability, target)
-    end
-  end
-
-  def add_subscribed_users(recipients, project, target)
-    return recipients unless target.respond_to? :subscribers
-
-    recipients + target.subscribers(project)
-  end
-
-  def add_labels_subscribers(recipients, project, target, labels: nil)
-    return recipients unless target.respond_to? :labels
-
-    (labels || target.labels).each do |label|
-      recipients += label.subscribers(project)
-    end
-
-    recipients
-  end
-
   def new_resource_email(target, project, method)
-    recipients = build_recipients(target, project, target.author, action: "new")
+    recipients = NotificationRecipientService.new(project).build_recipients(target, target.author, action: "new")
 
     recipients.each do |recipient|
       mailer.send(method, recipient.id, target.id).deliver_later
@@ -537,7 +299,7 @@ class NotificationService
   end
 
   def new_mentions_in_resource_email(target, project, new_mentioned_users, current_user, method)
-    recipients = build_recipients(target, project, current_user, action: "new")
+    recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "new")
     recipients = recipients & new_mentioned_users
 
     recipients.each do |recipient|
@@ -548,9 +310,8 @@ class NotificationService
   def close_resource_email(target, project, current_user, method, skip_current_user: true)
     action = method == :merged_merge_request_email ? "merge" : "close"
 
-    recipients = build_recipients(
+    recipients = NotificationRecipientService.new(project).build_recipients(
       target,
-      project,
       current_user,
       action: action,
       skip_current_user: skip_current_user
@@ -565,7 +326,12 @@ class NotificationService
     previous_assignee_id = previous_record(target, 'assignee_id')
     previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
 
-    recipients = build_recipients(target, project, current_user, action: "reassign", previous_assignee: previous_assignee)
+    recipients = NotificationRecipientService.new(project).build_recipients(
+      target,
+      current_user,
+      action: "reassign",
+      previous_assignee: previous_assignee
+    )
 
     recipients.each do |recipient|
       mailer.send(
@@ -579,7 +345,7 @@ class NotificationService
   end
 
   def relabeled_resource_email(target, project, labels, current_user, method)
-    recipients = build_relabeled_recipients(target, project, current_user, labels: labels)
+    recipients = NotificationRecipientService.new(project).build_relabeled_recipients(target, current_user, labels: labels)
     label_names = labels.map(&:name)
 
     recipients.each do |recipient|
@@ -588,58 +354,13 @@ class NotificationService
   end
 
   def reopen_resource_email(target, project, current_user, method, status)
-    recipients = build_recipients(target, project, current_user, action: "reopen")
+    recipients = NotificationRecipientService.new(project).build_recipients(target, current_user, action: "reopen")
 
     recipients.each do |recipient|
       mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later
     end
   end
 
-  def build_recipients(target, project, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
-    custom_action = build_custom_key(action, target)
-
-    recipients = target.participants(current_user)
-
-    unless NotificationSetting::EXCLUDED_WATCHER_EVENTS.include?(custom_action)
-      recipients = add_project_watchers(recipients, project)
-    end
-
-    recipients = add_custom_notifications(recipients, project, custom_action)
-    recipients = reject_mention_users(recipients, project)
-
-    recipients = recipients.uniq
-
-    # Re-assign is considered as a mention of the new assignee so we add the
-    # new assignee to the list of recipients after we rejected users with
-    # the "on mention" notification level
-    if [:reassign_merge_request, :reassign_issue].include?(custom_action)
-      recipients << previous_assignee if previous_assignee
-      recipients << target.assignee
-    end
-
-    recipients = reject_muted_users(recipients, project)
-    recipients = add_subscribed_users(recipients, project, target)
-
-    if [:new_issue, :new_merge_request].include?(custom_action)
-      recipients = add_labels_subscribers(recipients, project, target)
-    end
-
-    recipients = reject_unsubscribed_users(recipients, target)
-    recipients = reject_users_without_access(recipients, target)
-
-    recipients.delete(current_user) if skip_current_user
-
-    recipients.uniq
-  end
-
-  def build_relabeled_recipients(target, project, current_user, labels:)
-    recipients = add_labels_subscribers([], project, target, labels: labels)
-    recipients = reject_unsubscribed_users(recipients, target)
-    recipients = reject_users_without_access(recipients, target)
-    recipients.delete(current_user)
-    recipients.uniq
-  end
-
   def mailer
     Notify
   end
@@ -651,10 +372,4 @@ class NotificationService
       end
     end
   end
-
-  # Build event key to search on custom notification level
-  # Check NotificationSetting::EMAIL_EVENTS
-  def build_custom_key(action, object)
-    "#{action}_#{object.class.model_name.name.underscore}".to_sym
-  end
 end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 1c5a549feb9cec77ca766d4cf3a92afab83e3f82..d484a96f785d3b6d888b0e9655e350ec7b80535c 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -33,6 +33,7 @@ module Projects
 
     def import_repository
       begin
+        raise Error, "Blocked import URL." if Gitlab::UrlBlocker.blocked_url?(project.import_url)
         gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
       rescue => e
         # Expire cache to prevent scenarios such as:
@@ -40,7 +41,7 @@ module Projects
         # 2. Retried import, repo is broken or not imported but +exists?+ still returns true
         project.repository.before_import if project.repository_exists?
 
-        raise Error,  "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
+        raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
       end
     end
 
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 868fa7b3f2198a13c0d4500c81889107bccbd2f4..af0ddbe59349a7aaa3428c961bf74fbf2851ba7e 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -24,10 +24,9 @@ class SystemHooksService
         key: model.key,
         id: model.id
       )
+ 
       if model.user
-        data.merge!(
-          username: model.user.username
-        )
+        data[:username] = model.user.username
       end
     when Project
       data.merge!(project_data(model))
@@ -35,8 +34,6 @@ class SystemHooksService
       if event == :rename || event == :transfer
         data[:old_path_with_namespace] = model.old_path_with_namespace
       end
-
-      data
     when User
       data.merge!({
         name: model.name,
@@ -59,6 +56,8 @@ class SystemHooksService
     when GroupMember
       data.merge!(group_member_data(model))
     end
+    
+    data
   end
 
   def build_event_name(model, event)
diff --git a/app/validators/importable_url_validator.rb b/app/validators/importable_url_validator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..37a314adee6c4207ebec153816683c06fe31715b
--- /dev/null
+++ b/app/validators/importable_url_validator.rb
@@ -0,0 +1,11 @@
+# ImportableUrlValidator
+#
+# This validator blocks projects from using dangerous import_urls to help
+# protect against Server-side Request Forgery (SSRF).
+class ImportableUrlValidator < ActiveModel::EachValidator
+  def validate_each(record, attribute, value)
+    if Gitlab::UrlBlocker.blocked_url?(value)
+      record.errors.add(attribute, "imports are not allowed from that URL")
+    end
+  end
+end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 9175b3d3f964dd93d0cb8491ff02372c4b239709..e403a9da616d69d0cefc6fad9b3df3303de52db4 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -48,7 +48,7 @@
   .form-actions
     = f.submit 'Save', class: 'btn btn-save append-right-10'
     - if @appearance.persisted?
-      = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank'
+      = link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank', rel: 'noopener noreferrer'
 
     - if @appearance.updated_at
       %span.pull-right
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 00366b0a8c96ad0142e7ae00978eba5f024d4185..3eab065bb9f9aa9ab282971e5388ba5cbccaac05 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -404,7 +404,7 @@
             Enable Sentry
           .help-block
             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' } https://getsentry.com
+            %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'
diff --git a/app/views/events/_event.atom.builder b/app/views/events/_event.atom.builder
index 43a52cf3002e5ffdf4e061d642b60187a4508023..158061579f6e2df64a67897c32d98a52ae141b58 100644
--- a/app/views/events/_event.atom.builder
+++ b/app/views/events/_event.atom.builder
@@ -9,7 +9,7 @@ xml.entry do
 
   xml.author do
     xml.name event.author_name
-    xml.email event.author_email
+    xml.email event.author_public_email
   end
 
   xml.summary(type: "xhtml") do |summary|
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index f08c96df309c9c68fa33091ea7578e34f38547da..64b5a733b77987b54c1dd0e78626f0727a584aa9 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -15,6 +15,6 @@
         = link_to note.attachment.url, target: '_blank' do
           = image_tag note.attachment.url, class: 'note-image-attach'
       - else
-        = link_to note.attachment.url, target: "_blank", class: 'note-file-attach' do
+        = link_to note.attachment.url, target: '_blank',  class: 'note-file-attach' do
           %i.fa.fa-paperclip
           = note.attachment_identifier
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 31631887317849fec46dd73c641251388508dd68..f93b6b63426eca6a2335e03f9c870e162fc257b9 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -17,7 +17,7 @@
     %br
     Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
     %br
-    Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}.
+    Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}.
     - if current_application_settings.help_page_text.present?
       %hr
       = markdown_field(current_application_settings, :help_page_text)
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index e18bd47798b379355b707b2c553223dd53a7a942..e6058617ac99c0772b058d2f1e2694060180731e 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -33,7 +33,7 @@
       - @already_added_projects.each do |project|
         %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
           %td
-            = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank'
+            = link_to project.import_source, "https://bitbucket.org/#{project.import_source}", target: '_blank', rel: 'noopener noreferrer'
           %td
             = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
           %td.job-status
@@ -50,7 +50,7 @@
       - @repos.each do |repo|
         %tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
           %td
-            = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: "_blank"
+            = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer'
           %td.import-target
             %fieldset.row
             .input-group
@@ -70,7 +70,7 @@
       - @incompatible_repos.each do |repo|
         %tr{ id: "repo_#{repo.owner}___#{repo.slug}" }
           %td
-            = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank'
+            = link_to repo.full_name, "https://bitbucket.org/#{repo.full_name}", target: '_blank', rel: 'noopener noreferrer'
           %td.import-target
           %td.import-actions-job-status
             = label_tag 'Incompatible Project', nil, class: 'label label-danger'
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index d5b88709a349c163a616bf77a343f1f688a7491b..7456799ca0e62b1d41eba94ac89463ae737731b0 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -43,7 +43,7 @@
       - @repos.each do |repo|
         %tr{ id: "repo_#{repo["id"]}" }
           %td
-            = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank"
+            = link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank", rel: 'noopener noreferrer'
           %td.import-target
             = import_project_target(repo['namespace']['path'], repo['name'])
           %td.import-actions.job-status
diff --git a/app/views/import/google_code/new.html.haml b/app/views/import/google_code/new.html.haml
index 336becd229e8d3d6a5efcd0ea7840f6cd7d53c4c..c5800a1cca0db475ae0010a732ad3879389536c7 100644
--- a/app/views/import/google_code/new.html.haml
+++ b/app/views/import/google_code/new.html.haml
@@ -13,7 +13,7 @@
     %li
       %p
         Go to
-        #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: "_blank"}.
+        #{link_to "Google Takeout", "https://www.google.com/settings/takeout", target: '_blank', rel: 'noopener noreferrer'}.
     %li
       %p
         Make sure you're logged into the account that owns the projects you'd like to import.
diff --git a/app/views/import/google_code/status.html.haml b/app/views/import/google_code/status.html.haml
index 5e01af008bec56fc06a60036fd9ee4de2f66a4fe..60de6bfe8163beb5cf71b11a2117038597b6481b 100644
--- a/app/views/import/google_code/status.html.haml
+++ b/app/views/import/google_code/status.html.haml
@@ -36,7 +36,7 @@
       - @already_added_projects.each do |project|
         %tr{ id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}" }
           %td
-            = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank"
+            = link_to project.import_source, "https://code.google.com/p/#{project.import_source}", target: "_blank", rel: 'noopener noreferrer'
           %td
             = link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
           %td.job-status
@@ -53,7 +53,7 @@
       - @repos.each do |repo|
         %tr{ id: "repo_#{repo.id}" }
           %td
-            = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
+            = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer'
           %td.import-target
             #{current_user.username}/#{repo.name}
           %td.import-actions.job-status
@@ -63,7 +63,7 @@
       - @incompatible_repos.each do |repo|
         %tr{ id: "repo_#{repo.id}" }
           %td
-            = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank"
+            = link_to repo.name, "https://code.google.com/p/#{repo.name}", target: "_blank", rel: 'noopener noreferrer'
           %td.import-target
           %td.import-actions-job-status
             = label_tag "Incompatible Project", nil, class: "label label-danger"
diff --git a/app/views/issues/_issue.atom.builder b/app/views/issues/_issue.atom.builder
index fcd30c8c765756b0107000a7202604079325372a..23a884480552ace1c011d29fedf795d3eeb7f985 100644
--- a/app/views/issues/_issue.atom.builder
+++ b/app/views/issues/_issue.atom.builder
@@ -7,7 +7,7 @@ xml.entry do
 
   xml.author do
     xml.name issue.author_name
-    xml.email issue.author_email
+    xml.email issue.author_public_email
   end
 
   xml.summary issue.title
@@ -26,7 +26,7 @@ xml.entry do
   if issue.assignee
     xml.assignee do
       xml.name issue.assignee.name
-      xml.email issue.assignee.email
+      xml.email issue.assignee_public_email
     end
   end
 end
diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml
index 65887aacbafb8ee20c1c6dcc3c60c993a895e623..04e2d4b63e6b2c7fb429b1f3ea3b9cafc611b3ac 100644
--- a/app/views/koding/index.html.haml
+++ b/app/views/koding/index.html.haml
@@ -2,5 +2,5 @@
   %p
     = icon('circle', class: 'cgreen')
     Integration is active for
-    = link_to koding_project_url, target: '_blank' do
+    = link_to koding_project_url, target: '_blank', rel: 'noopener noreferrer' do
       #{current_application_settings.koding_url}
diff --git a/app/views/notify/build_fail_email.html.haml b/app/views/notify/build_fail_email.html.haml
deleted file mode 100644
index 060b50ffc698aad7db2714254bf75a65bf61a906..0000000000000000000000000000000000000000
--- a/app/views/notify/build_fail_email.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- content_for :header do
-  %h1{ style: "background: #c40834; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
-    GitLab (job failed)
-
-%h3
-  Project:
-  = link_to namespace_project_url(@project.namespace, @project) do
-    = @project.name
-
-%p
-  Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
-%p
-  Author: #{@build.pipeline.git_author_name}
-%p
-  Branch: #{@build.ref}
-%p
-  Stage: #{@build.stage}
-%p
-  Job: #{@build.name}
-%p
-  Message: #{@build.pipeline.git_commit_message}
-
-%p
-  Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_fail_email.text.erb b/app/views/notify/build_fail_email.text.erb
deleted file mode 100644
index 2a94688a6b060e4714f890e33f1df91beda250ba..0000000000000000000000000000000000000000
--- a/app/views/notify/build_fail_email.text.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-Job failed for <%= @project.name %>
-
-Status:   <%= @build.status %>
-Commit:   <%= @build.pipeline.short_sha %>
-Author:   <%= @build.pipeline.git_author_name %>
-Branch:   <%= @build.ref %>
-Stage:    <%= @build.stage %>
-Job:      <%= @build.name %>
-Message:  <%= @build.pipeline.git_commit_message %>
-
-Url:      <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/notify/build_success_email.html.haml b/app/views/notify/build_success_email.html.haml
deleted file mode 100644
index ca0eaa96a9db3166a915ab257adae6665b252371..0000000000000000000000000000000000000000
--- a/app/views/notify/build_success_email.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-- content_for :header do
-  %h1{ style: "background: #38CF5B; color: #FFF; font: normal 20px Helvetica, Arial, sans-serif; margin: 0; padding: 5px 10px; line-height: 32px; font-size: 16px;" }
-    GitLab (job successful)
-
-%h3
-  Project:
-  = link_to namespace_project_url(@project.namespace, @project) do
-    = @project.name
-
-%p
-  Commit: #{link_to @build.short_sha, namespace_project_commit_url(@build.project.namespace, @build.project, @build.sha)}
-%p
-  Author: #{@build.pipeline.git_author_name}
-%p
-  Branch: #{@build.ref}
-%p
-  Stage: #{@build.stage}
-%p
-  Job: #{@build.name}
-%p
-  Message: #{@build.pipeline.git_commit_message}
-
-%p
-  Job details: #{link_to "Job #{@build.id}", namespace_project_build_url(@build.project.namespace, @build.project, @build)}
diff --git a/app/views/notify/build_success_email.text.erb b/app/views/notify/build_success_email.text.erb
deleted file mode 100644
index 445cd46e64fe66daa390891a1f0839b7920f5113..0000000000000000000000000000000000000000
--- a/app/views/notify/build_success_email.text.erb
+++ /dev/null
@@ -1,11 +0,0 @@
-Job successful for <%= @project.name %>
-
-Status:   <%= @build.status %>
-Commit:   <%= @build.pipeline.short_sha %>
-Author:   <%= @build.pipeline.git_author_name %>
-Branch:   <%= @build.ref %>
-Stage:    <%= @build.stage %>
-Job:      <%= @build.name %>
-Message:  <%= @build.pipeline.git_commit_message %>
-
-Url:      <%= namespace_project_build_url(@build.project.namespace, @build.project, @build) %>
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index d551754a2e53826e9ddcbf5e8f628afe19cb7085..c74b3249a13057c85d8977d5c64922a58493ca7f 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -18,7 +18,7 @@
             or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
     .col-lg-9
       .clearfix.avatar-image.append-bottom-default
-        = link_to avatar_icon(@user, 400), target: '_blank' do
+        = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
           = image_tag avatar_icon(@user, 160), alt: '', class: 'avatar s160'
       %h5.prepend-top-0
         Upload new avatar
diff --git a/app/views/projects/blob/_image.html.haml b/app/views/projects/blob/_image.html.haml
index f864702d862c30c78f1fbb3f83d2001327fa1677..ea3cecb86a94c9456a58c3b1c648be4eb1360862 100644
--- a/app/views/projects/blob/_image.html.haml
+++ b/app/views/projects/blob/_image.html.haml
@@ -9,7 +9,7 @@
     - else
       .nothing-here-block
         The SVG could not be displayed as it is too large, you can
-        #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank')}
+        #{link_to('view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer')}
         instead.
   - else
     %img{ src: namespace_project_raw_path(@project.namespace, @project, tree_join(@commit.id, blob.path)), alt: "#{blob.name}" }
diff --git a/app/views/projects/blob/_text.html.haml b/app/views/projects/blob/_text.html.haml
index b1e1be49de944e235f15ae39430d88e75760af2d..7b16d266982a6d394c884f4d602d81e041c73886 100644
--- a/app/views/projects/blob/_text.html.haml
+++ b/app/views/projects/blob/_text.html.haml
@@ -3,7 +3,7 @@
     .nothing-here-block
       File too large, you can
       = succeed '.' do
-        = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank'
+        = link_to 'view the raw file', namespace_project_raw_path(@project.namespace, @project, @id), target: '_blank', rel: 'noopener noreferrer'
 
 - else
   - blob.load_all_data!(@repository)
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 8853801016b303b1f75f113f3aadbe96cdd118ff..3bcddcb37f1aa58e4a0d4dd4232e265d3a21422f 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -9,7 +9,7 @@
   - if @conflict
     .alert.alert-danger
       Someone edited the file the same time you did. Please check out
-      = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank"
+      = link_to "the file", namespace_project_blob_path(@project.namespace, @project, tree_join(@target_branch, @file_path)), target: "_blank", rel: 'noopener noreferrer'
       and make sure your changes will not unintentionally remove theirs.
 
   .file-editor
diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml
index 5d9a776da8931dd9379b0ac136de22523e115605..a5a9e4d0621f4beb678f3b8e4794d12dec66c862 100644
--- a/app/views/projects/buttons/_koding.html.haml
+++ b/app/views/projects/buttons/_koding.html.haml
@@ -1,3 +1,3 @@
 - if koding_enabled? && current_user && @repository.koding_yml && can_push_branch?(@project, @project.default_branch)
-  = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank' do
+  = link_to koding_project_url(@project), class: 'btn project-action-button inline', target: '_blank', rel: 'noopener noreferrer' do
     Run in IDE (Koding)
diff --git a/app/views/projects/cycle_analytics/_overview.html.haml b/app/views/projects/cycle_analytics/_overview.html.haml
index c8f0b547f8049326555d1aff5460016797479a0c..9007f2c24ba54be1407dc8b6227140678b96dddf 100644
--- a/app/views/projects/cycle_analytics/_overview.html.haml
+++ b/app/views/projects/cycle_analytics/_overview.html.haml
@@ -9,7 +9,7 @@
               Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
               To set up CA, you must first define a production environment by setting up your CI and then deploy to production.
             %p
-              %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: "_blank" } Read more
+              %a.btn{ href: help_page_path('user/project/cycle_analytics'), target: '_blank' } Read more
           .col-md-6.overview-image
             %span.overview-icon
               = custom_icon ('icon_cycle_analytics_overview')
diff --git a/app/views/projects/environments/_external_url.html.haml b/app/views/projects/environments/_external_url.html.haml
index 4c8fe1c271b6d6cf7dec025673647d10cda0b46c..bf0f181907339e129ece9321181a9420b209e71f 100644
--- a/app/views/projects/environments/_external_url.html.haml
+++ b/app/views/projects/environments/_external_url.html.haml
@@ -1,3 +1,3 @@
 - if environment.external_url && can?(current_user, :read_environment, environment)
-  = link_to environment.external_url, target: '_blank', class: 'btn external-url' do
+  = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do
     = icon('external-link')
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index f8e94ca98aeb90a77436ce605b433d26ba953bcd..b8c1782f050ec8b952ab05192d3aca77509ceb34 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -1,5 +1,8 @@
 - @no_container = true
 - page_title "Metrics for environment", @environment.name
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('common_d3')
+  = page_specific_javascript_bundle_tag('monitoring')
 = render "projects/pipelines/head"
 
 %div{ class: container_class }
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 7b7d7b1e00ee5b983add8c67be0cf9db53047ebc..f3a429d12d94ae0ddb54ba4ac2f62bc6bbfcd158 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -19,15 +19,14 @@
       .nav-controls
         = link_to params.merge(rss_url_options), class: 'btn append-right-10 has-tooltip', title: 'Subscribe' do
           = icon('rss')
-        - if can? current_user, :create_issue, @project
-          = link_to new_namespace_project_issue_path(@project.namespace,
-                                                     @project,
-                                                     issue: { assignee_id: issues_finder.assignee.try(:id),
-                                                              milestone_id: issues_finder.milestones.first.try(:id) }),
-                                                     class: "btn btn-new",
-                                                     title: "New Issue",
-                                                     id: "new_issue_link" do
-            New Issue
+        = link_to new_namespace_project_issue_path(@project.namespace,
+                                                   @project,
+                                                   issue: { assignee_id: issues_finder.assignee.try(:id),
+                                                            milestone_id: issues_finder.milestones.first.try(:id) }),
+                                                   class: "btn btn-new",
+                                                   title: "New Issue",
+                                                   id: "new_issue_link" do
+          New Issue
     = render 'shared/issuable/search_bar', type: :issues
 
     .issues-holder
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index d39f36e94c7c7b33b22f4aa23bcfd6921668f4c5..6ac05bf3afee6e62486400c948a0cfb8ccd9af04 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -20,37 +20,34 @@
       = confidential_icon(@issue)
       = issuable_meta(@issue, @project, "Issue")
 
-  - if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue)
-    .issuable-actions
-      .clearfix.issue-btn-group.dropdown
-        %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
-          Options
-          = icon('caret-down')
-        .dropdown-menu.dropdown-menu-align-right.hidden-lg
-          %ul
-            - if can?(current_user, :create_issue, @project)
-              %li
-                = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
-            - if can?(current_user, :update_issue, @issue)
-              %li
-                = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
-              %li
-                = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
-              %li
-                = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
-            - if @issue.submittable_as_spam_by?(current_user)
-              %li
-                = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
-
-        - if can?(current_user, :create_issue, @project)
-          = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
-            New issue
-        - if can?(current_user, :update_issue, @issue)
-          = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
-          = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+  .issuable-actions
+    .clearfix.issue-btn-group.dropdown
+      %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
+        Options
+        = icon('caret-down')
+      .dropdown-menu.dropdown-menu-align-right.hidden-lg
+        %ul
+          %li
+            = link_to 'New issue', new_namespace_project_issue_path(@project.namespace, @project), title: 'New issue', id: 'new_issue_link'
+          - if can?(current_user, :update_issue, @issue)
+            %li
+              = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+            %li
+              = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+            %li
+              = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue)
           - if @issue.submittable_as_spam_by?(current_user)
-            = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
-          = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
+            %li
+              = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
+
+    = link_to new_namespace_project_issue_path(@project.namespace, @project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
+      New issue
+    - if can?(current_user, :update_issue, @issue)
+      = link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+      = link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), data: {no_turbolink: true}, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+      - if @issue.submittable_as_spam_by?(current_user)
+        = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
+      = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
 
 
 .issue-details.issuable-details
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index c8f097c69da499f4e544bd990f7d7d68f165dd85..6682a85ffa63ff15b8cdac14005a0214f4844ce2 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -16,7 +16,7 @@
         .pull-right
           - if @merge_request.source_branch_exists?
             - if koding_enabled? && @repository.koding_yml
-              = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do
+              = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank', rel: 'noopener noreferrer' do
                 Run in IDE (Koding)
             = link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
               Check out branch
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index 93ed4b68e0e83a8b73e705a8db65cd588e5fc07e..cde0ce08e1440fe3a14c2f1009021e1e6b95b838 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -49,7 +49,7 @@
           %strong Tip:
           = succeed '.' do
             You can also checkout merge requests locally by
-            = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank'
+            = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer'
 
 :javascript
   $(function(){
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 3a323d94cc2e70daa3ae90b168d577cdd9f4c145..2fb88297fb322874995ba4b2d1e1cbbb519bb34b 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -4,13 +4,13 @@
 %ul.list-unstyled.indent-list
   %li
     1.
-    = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noreferrer noopener nofollow' do
+    = link_to 'https://docs.mattermost.com/developer/slash-commands.html#enabling-custom-commands', target: '_blank', rel: 'noopener noreferrer nofollow' do
       Enable custom slash commands
       = icon('external-link')
     on your Mattermost installation
   %li
     2.
-    = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noreferrer noopener nofollow' do
+    = link_to 'https://docs.mattermost.com/developer/slash-commands.html#set-up-a-custom-command', target: '_blank', rel: 'noopener noreferrer nofollow' do
       Add a slash command
       = icon('external-link')
     in your Mattermost team with these options:
diff --git a/app/views/projects/services/mattermost_slash_commands/_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
index a04fd5035a6fb3660ef423f2b16a0e6b46dff554..2a1b9d4c465def74bdf99a0c2898beab45c09ceb 100644
--- a/app/views/projects/services/mattermost_slash_commands/_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_help.html.haml
@@ -4,7 +4,7 @@
   %p
     This service allows users to perform common operations on this
     project by entering slash commands in Mattermost.
-    = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+    = link_to help_page_path('user/project/integrations/mattermost_slash_commands.md'), target: '_blank' do
       View documentation
       = icon('external-link')
   %p.inline
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index 0d973a20d4c9ccc18feebf5a3bb72db56e49a6ce..078b7be68650925878111a9e6f414b9ba870f05e 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -5,7 +5,7 @@
   %p
     This service allows users to perform common operations on this
     project by entering slash commands in Slack.
-    = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank', ref: 'noreferrer nofollow noopener' do
+    = link_to help_page_path('user/project/integrations/slack_slash_commands.md'), target: '_blank' do
       View documentation
       = icon('external-link')
   %p.inline
@@ -57,7 +57,7 @@
         = label_tag nil, 'Customize icon', class: 'col-sm-2 col-xs-12 control-label'
         .col-sm-10.col-xs-12.text-block
           = image_tag(asset_url('slash-command-logo.png'), width: 36, height: 36)
-          = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank')
+          = link_to('Download image', asset_url('gitlab_logo.png'), class: 'btn btn-sm', target: '_blank', rel: 'noopener noreferrer')
 
       .form-group
         = label_tag nil, 'Autocomplete', class: 'col-sm-2 col-xs-12 control-label'
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 367aa550a788aacb7a6f716a925c7f721a8c897d..a212c714826d4bc2160e85e2ea0337a9bc2bbeeb 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -1,6 +1,5 @@
 .dropdown.inline.prepend-left-10
   %button.dropdown-toggle{ type: 'button', data: {toggle: 'dropdown' } }
-    %span.light
     - if @sort.present?
       = sort_options_hash[@sort]
     - else
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index e20336540182f0915d67e8d1fd5e8f5200219c8f..7a7e3d467969b115fbd39a27b8ba37b35b117446 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -16,7 +16,6 @@
           Also, issues are searchable and filterable.
         - if project_select_button
           = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
-        - else
-          = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
       - else
-        %h4.text-center There are no issues to show.
+        %h4 There are no issues to show.
+      = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 0b0f2c9cd1a78945794a121b3a6dcfab2504b5df..17107f55a2d618f9746574b99ce0fed3214ea64e 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -8,7 +8,7 @@
   .alert.alert-danger
     Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.
     Please check out
-    = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank"
+    = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank", rel: 'noopener noreferrer'
     and make sure your changes will not unintentionally remove theirs
 
 .form-group
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 048fc4882072606acf5db245c21d147ad7e1b7e5..25a4aec0a384586a7704b7650c4b265df3bea357 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -30,7 +30,7 @@
             = icon('user', 'aria-hidden': 'true')
         .title.hide-collapsed
           Assignee
-          = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+          = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
           - if can_edit_issuable
             = link_to 'Edit', '#', class: 'edit-link pull-right'
         .value.hide-collapsed
@@ -64,7 +64,7 @@
               None
         .title.hide-collapsed
           Milestone
-          = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+          = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
           - if can_edit_issuable
             = link_to 'Edit', '#', class: 'edit-link pull-right'
         .value.hide-collapsed
@@ -91,7 +91,7 @@
               = issuable.due_date.try(:to_s, :medium) || 'None'
           .title.hide-collapsed
             Due date
-            = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+            = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
             - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
               = link_to 'Edit', '#', class: 'edit-link pull-right'
           .value.hide-collapsed
@@ -121,12 +121,12 @@
         - selected_labels = issuable.labels
         .block.labels
           .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
-            = icon('tags', 'aria-hidden': 'true')
+            = icon('tags', class: 'hidden', 'aria-hidden': 'true')
             %span
               = selected_labels.size
           .title.hide-collapsed
             Labels
-            = icon('spinner spin', class: 'block-loading', 'aria-hidden': 'true')
+            = icon('spinner spin', class: 'hidden block-loading', 'aria-hidden': 'true')
             - if can_edit_issuable
               = link_to 'Edit', '#', class: 'edit-link pull-right'
           .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 76cd330e80ae3267caf032e52b23127c89157ca9..dc9a3b0d0df69741178e1680faaa2ee220e09b58 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -33,7 +33,7 @@
 
     .profile-header
       .avatar-holder
-        = link_to avatar_icon(@user, 400), target: '_blank' do
+        = link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
           = image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
 
       .user-info
diff --git a/app/workers/build_email_worker.rb b/app/workers/build_email_worker.rb
deleted file mode 100644
index 5fdb1f2baa05fce65de38baf8707fb11099d87e9..0000000000000000000000000000000000000000
--- a/app/workers/build_email_worker.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-class BuildEmailWorker
-  include Sidekiq::Worker
-  include BuildQueue
-
-  def perform(build_id, recipients, push_data)
-    recipients.each do |recipient|
-      begin
-        case push_data['build_status']
-        when 'success'
-          Notify.build_success_email(build_id, recipient).deliver_now
-        when 'failed'
-          Notify.build_fail_email(build_id, recipient).deliver_now
-        end
-      # These are input errors and won't be corrected even if Sidekiq retries
-      rescue Net::SMTPFatalError, Net::SMTPSyntaxError => e
-        logger.info("Failed to send e-mail for project '#{push_data['project_name']}' to #{recipient}: #{e}")
-      end
-    end
-  end
-end
diff --git a/changelogs/unreleased/23993-drop-ci_projects.yml b/changelogs/unreleased/23993-drop-ci_projects.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ee9cf774e37fdcdc69371ff639b4778cf1ba000e
--- /dev/null
+++ b/changelogs/unreleased/23993-drop-ci_projects.yml
@@ -0,0 +1,6 @@
+---
+title: Drop unused ci_projects table and some unused project_id columns,
+  then rename gl_project_id to project_id. Stop exporting job trace when
+  exporting projects.
+merge_request: 9378
+author: David Wagner
diff --git a/changelogs/unreleased/24683-sidebar-spinners.yml b/changelogs/unreleased/24683-sidebar-spinners.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3fec273152fcf94afadf624787d1227bc3203e1c
--- /dev/null
+++ b/changelogs/unreleased/24683-sidebar-spinners.yml
@@ -0,0 +1,4 @@
+---
+title: hide loading spinners for server-rendered sidebar fields
+merge_request:
+author:
diff --git a/changelogs/unreleased/26236-monospace-gfm.yml b/changelogs/unreleased/26236-monospace-gfm.yml
new file mode 100644
index 0000000000000000000000000000000000000000..c44f3d4d3dc44f96776641109ec1a281417476dc
--- /dev/null
+++ b/changelogs/unreleased/26236-monospace-gfm.yml
@@ -0,0 +1,4 @@
+---
+title: Change gfm textarea to use monospace font
+merge_request:
+author:
diff --git a/changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml b/changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e0e826a67f8e781204f867e58690645220b4a9f7
--- /dev/null
+++ b/changelogs/unreleased/28058-hide-emails-in-atom-feeds.yml
@@ -0,0 +1,4 @@
+---
+title: Only show public emails in atom feeds
+merge_request:
+author:
diff --git a/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml b/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml
new file mode 100644
index 0000000000000000000000000000000000000000..660a881e094e3bd74628c4f88b17764d8eabe7ba
--- /dev/null
+++ b/changelogs/unreleased/28499-fix-large-text-tooltip-in-diff-file-name.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes large file name tooltip cutoff in diff header
+merge_request: 9529
+author:
diff --git a/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml b/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8b592766bf3c768f2275672d4f9328918b566bac
--- /dev/null
+++ b/changelogs/unreleased/28660-fix-dismissable-error-close-not-visible-enough.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes dismissable error close is not visible enough
+merge_request: 9516
+author:
diff --git a/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml b/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml
new file mode 100644
index 0000000000000000000000000000000000000000..114a14ec2df651b21b6d4f37be8a0a0d2b1bfa3a
--- /dev/null
+++ b/changelogs/unreleased/28890-allow-creating-mr-without-target-branch-in-url.yml
@@ -0,0 +1,5 @@
+---
+title: Allow creating merge request even if target branch is not specified in query
+  params
+merge_request: 9968
+author:
diff --git a/changelogs/unreleased/29405-fix-project-wiki-update.yml b/changelogs/unreleased/29405-fix-project-wiki-update.yml
new file mode 100644
index 0000000000000000000000000000000000000000..85be36f7902a8b17ae42b27b65d77e2b93225641
--- /dev/null
+++ b/changelogs/unreleased/29405-fix-project-wiki-update.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Project Wiki update
+merge_request: 9990
+author: Dongqing Hu
diff --git a/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml b/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml
new file mode 100644
index 0000000000000000000000000000000000000000..61ffb64fa8f9e29bca37585d667ac24e0043518c
--- /dev/null
+++ b/changelogs/unreleased/29438-fix-trigger-webhook-for-ref-with-dot.yml
@@ -0,0 +1,4 @@
+---
+title: Fix trigger webhook for ref with a dot
+merge_request: 10001
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml b/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml
new file mode 100644
index 0000000000000000000000000000000000000000..15d7b9dcafb5517b07dfb1c13ec78baf23b3946e
--- /dev/null
+++ b/changelogs/unreleased/29662-allow-unauthenticated-branches-api.yml
@@ -0,0 +1,4 @@
+---
+title: Allow unauthenticated access to some Branch API GET endpoints
+merge_request:
+author:
diff --git a/changelogs/unreleased/bugfix-systemhook.yml b/changelogs/unreleased/bugfix-systemhook.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4c4d0dcc7a29746974ec32a62fc50ec28231897b
--- /dev/null
+++ b/changelogs/unreleased/bugfix-systemhook.yml
@@ -0,0 +1,4 @@
+---
+title: Fix bug when system hook for deploy key
+merge_request: 9796
+author: billy.lb
diff --git a/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml b/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4b668d994a1a62d1729db223d60280f1c7d08bf6
--- /dev/null
+++ b/changelogs/unreleased/feature-use-gitaly-for-commit-show.yml
@@ -0,0 +1,4 @@
+---
+title: Use Gitaly for CommitController#show
+merge_request: 9629
+author:
diff --git a/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml b/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a42b0db3cfc2352efbf83371feda124e88e15980
--- /dev/null
+++ b/changelogs/unreleased/fix-prometheus-including-d3-main-bundle.yml
@@ -0,0 +1,4 @@
+---
+title: Removed d3 from the main application.js bundle
+merge_request: 10062
+author:
diff --git a/changelogs/unreleased/fl-remove-ujs-pipelines.yml b/changelogs/unreleased/fl-remove-ujs-pipelines.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f353400753a5d949f1933b97241b179d64f5c32d
--- /dev/null
+++ b/changelogs/unreleased/fl-remove-ujs-pipelines.yml
@@ -0,0 +1,4 @@
+---
+title: 'Removes UJS from pipelines tables'
+merge_request: 9929
+author:
diff --git a/changelogs/unreleased/issue_27212.yml b/changelogs/unreleased/issue_27212.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7a7e04f7ca7265919f7aef1dd1ace68fd9883fb7
--- /dev/null
+++ b/changelogs/unreleased/issue_27212.yml
@@ -0,0 +1,4 @@
+---
+title: Add closed_at field to issues
+merge_request:
+author:
diff --git a/changelogs/unreleased/make-karma-fast-again.yml b/changelogs/unreleased/make-karma-fast-again.yml
new file mode 100644
index 0000000000000000000000000000000000000000..9b95e06954a69558978aff6593c8393e355b12ee
--- /dev/null
+++ b/changelogs/unreleased/make-karma-fast-again.yml
@@ -0,0 +1,4 @@
+---
+title: Only add code coverage instrumentation when generating coverage report
+merge_request: 9987
+author:
diff --git a/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml b/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ce4d5092c17efb3efec3dca769d60c214e5416c3
--- /dev/null
+++ b/changelogs/unreleased/migrate-pipeline-events-and-email-service.yml
@@ -0,0 +1,4 @@
+---
+title: Migrate SlackService and MattermostService from build_events to pipeline_events, and migrate BuildsEmailService to PipelinesEmailService. Update Hipchat to use pipeline events rather than build events.
+merge_request: 8196
+author:
diff --git a/changelogs/unreleased/remove-unused-ci-tables.yml b/changelogs/unreleased/remove-unused-ci-tables.yml
new file mode 100644
index 0000000000000000000000000000000000000000..fccfb882bd93455659eb6efdc5ae09c9f97fe13e
--- /dev/null
+++ b/changelogs/unreleased/remove-unused-ci-tables.yml
@@ -0,0 +1,4 @@
+---
+title: Remove various unused CI tables and columns
+merge_request: 9639
+author:
diff --git a/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml b/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4067b3de00c72d2a081e92cf852d17c7d5ecab19
--- /dev/null
+++ b/changelogs/unreleased/rename-ci_commits-to-ci_pipeline.yml
@@ -0,0 +1,4 @@
+---
+title: Rename table ci_commits to ci_pipelines
+merge_request: 9638
+author:
diff --git a/changelogs/unreleased/rename_all_issues.yml b/changelogs/unreleased/rename_all_issues.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d3109bdb17e38190b2919cdda94e00fef28e4066
--- /dev/null
+++ b/changelogs/unreleased/rename_all_issues.yml
@@ -0,0 +1,4 @@
+---
+title: Rename 'All issues' to 'Open issues' in Add issues modal
+merge_request: 10042
+author: blackst0ne
diff --git a/changelogs/unreleased/routes-lower-case.yml b/changelogs/unreleased/routes-lower-case.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2110956680c9c2f9e9ff4dff4a84d05e7d69bcda
--- /dev/null
+++ b/changelogs/unreleased/routes-lower-case.yml
@@ -0,0 +1,4 @@
+---
+title: Remove repeated routes.path check for postgresql database
+merge_request:
+author: mhasbini
diff --git a/changelogs/unreleased/simplify-docs-trigger.yml b/changelogs/unreleased/simplify-docs-trigger.yml
new file mode 100644
index 0000000000000000000000000000000000000000..062626359ef2c65a87127a6a695ce63213744eb7
--- /dev/null
+++ b/changelogs/unreleased/simplify-docs-trigger.yml
@@ -0,0 +1,4 @@
+---
+title: Simplify trigger_docs build job for CE and EE
+merge_request: 9820
+author: winniehell
diff --git a/changelogs/unreleased/ssrf-protections.yml b/changelogs/unreleased/ssrf-protections.yml
new file mode 100644
index 0000000000000000000000000000000000000000..8d8037380092630ca675d0bc53df5e7cb000bd27
--- /dev/null
+++ b/changelogs/unreleased/ssrf-protections.yml
@@ -0,0 +1,4 @@
+---
+title: To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
+merge_request:
+author:
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 2b018c6870380964b3d19b07fe16360513fc71c7..ecd7395648837341a6ce33c523f8fb9619fdaac4 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -42,7 +42,7 @@ Sidekiq.configure_server do |config|
 
   Gitlab::SidekiqThrottler.execute!
 
-  config = ActiveRecord::Base.configurations[Rails.env] ||
+  config = Gitlab::Database.config ||
     Rails.application.config.database_configuration[Rails.env]
   config['pool'] = Sidekiq.options[:concurrency]
   ActiveRecord::Base.establish_connection(config)
diff --git a/config/karma.config.js b/config/karma.config.js
index c1d3751d88ffcb39daab466d552cc3ea2758b941..eb082dd28bfdff962770e4a7101ce5a986b41c24 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -3,17 +3,6 @@ var webpack = require('webpack');
 var webpackConfig = require('./webpack.config.js');
 var ROOT_PATH = path.resolve(__dirname, '..');
 
-// add coverage instrumentation to babel config
-if (webpackConfig.module && webpackConfig.module.rules) {
-  var babelConfig = webpackConfig.module.rules.find(function (rule) {
-    return rule.loader === 'babel-loader';
-  });
-
-  babelConfig.options = babelConfig.options || {};
-  babelConfig.options.plugins = babelConfig.options.plugins || [];
-  babelConfig.options.plugins.push('istanbul');
-}
-
 // remove problematic plugins
 if (webpackConfig.plugins) {
   webpackConfig.plugins = webpackConfig.plugins.filter(function (plugin) {
@@ -27,7 +16,8 @@ if (webpackConfig.plugins) {
 // Karma configuration
 module.exports = function(config) {
   var progressReporter = process.env.CI ? 'mocha' : 'progress';
-  config.set({
+
+  var karmaConfig = {
     basePath: ROOT_PATH,
     browsers: ['PhantomJS'],
     frameworks: ['jasmine'],
@@ -38,14 +28,20 @@ module.exports = function(config) {
     preprocessors: {
       'spec/javascripts/**/*.js': ['webpack', 'sourcemap'],
     },
-    reporters: [progressReporter, 'coverage-istanbul'],
-    coverageIstanbulReporter: {
+    reporters: [progressReporter],
+    webpack: webpackConfig,
+    webpackMiddleware: { stats: 'errors-only' },
+  };
+
+  if (process.env.BABEL_ENV === 'coverage' || process.env.NODE_ENV === 'coverage') {
+    karmaConfig.reporters.push('coverage-istanbul');
+    karmaConfig.coverageIstanbulReporter = {
       reports: ['html', 'text-summary'],
       dir: 'coverage-javascript/',
       subdir: '.',
       fixWebpackSourcePaths: true
-    },
-    webpack: webpackConfig,
-    webpackMiddleware: { stats: 'errors-only' },
-  });
+    };
+  }
+
+  config.set(karmaConfig);
 };
diff --git a/config/webpack.config.js b/config/webpack.config.js
index cbcc9ac5aeaf42e3e44b40c7d1d4305b64a07049..c6794d6b944f58589c80ea249fa8753309183c36 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -35,6 +35,7 @@ var config = {
     issuable:             './issuable/issuable_bundle.js',
     merge_conflicts:      './merge_conflicts/merge_conflicts_bundle.js',
     merge_request_widget: './merge_request_widget/ci_bundle.js',
+    monitoring:           './monitoring/monitoring_bundle.js',
     network:              './network/network_bundle.js',
     profile:              './profile/profile_bundle.js',
     protected_branches:   './protected_branches/protected_branches_bundle.js',
@@ -58,13 +59,7 @@ var config = {
       {
         test: /\.js$/,
         exclude: /(node_modules|vendor\/assets)/,
-        loader: 'babel-loader',
-        options: {
-          presets: [
-            ["es2015", {"modules": false}],
-            'stage-2'
-          ]
-        }
+        loader: 'babel-loader'
       },
       {
         test: /\.svg$/,
@@ -120,7 +115,7 @@ var config = {
     // create cacheable common library bundle for all d3 chunks
     new webpack.optimize.CommonsChunkPlugin({
       name: 'common_d3',
-      chunks: ['graphs', 'users'],
+      chunks: ['graphs', 'users', 'monitoring'],
     }),
 
     // create cacheable common library bundles
diff --git a/db/fixtures/development/17_cycle_analytics.rb b/db/fixtures/development/17_cycle_analytics.rb
index aea0a72b6332bd733344e8e7a369fd2adb25c8a3..4bc735916c18c9ed9ae83ba6cd731fd380da47b5 100644
--- a/db/fixtures/development/17_cycle_analytics.rb
+++ b/db/fixtures/development/17_cycle_analytics.rb
@@ -155,7 +155,7 @@ class Gitlab::Seeder::CycleAnalytics
 
       issue.project.repository.add_branch(@user, branch_name, 'master')
 
-      commit_sha = issue.project.repository.create_file(@user, filename, "content", options, message: "Commit for ##{issue.iid}", branch_name: branch_name)
+      commit_sha = issue.project.repository.create_file(@user, filename, "content", message: "Commit for ##{issue.iid}", branch_name: branch_name)
       issue.project.repository.commit(commit_sha)
 
       GitPushService.new(issue.project,
diff --git a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
index 65adc90c2c11a8baaa5dcc1f7adf9dcea89bd8bd..8a96a784c9727ed0e4d96e0854be03d7d11f205c 100644
--- a/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
+++ b/db/migrate/20170216135621_add_index_for_latest_successful_pipeline.rb
@@ -5,7 +5,7 @@ class AddIndexForLatestSuccessfulPipeline < ActiveRecord::Migration
   disable_ddl_transaction!
 
   def up
-    add_concurrent_index :ci_commits, [:gl_project_id, :ref, :status]
+    add_concurrent_index(:ci_commits, [:gl_project_id, :ref, :status])
   end
 
   def down
diff --git a/db/migrate/20170222143317_drop_ci_projects.rb b/db/migrate/20170222143317_drop_ci_projects.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4db8658f36fbe1c3edcc128ebc492174eb14ab22
--- /dev/null
+++ b/db/migrate/20170222143317_drop_ci_projects.rb
@@ -0,0 +1,34 @@
+class DropCiProjects < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  def up
+    drop_table :ci_projects
+  end
+
+  def down
+    create_table "ci_projects", force: :cascade do |t|
+      t.string "name"
+      t.integer "timeout", default: 3600, null: false
+      t.datetime "created_at"
+      t.datetime "updated_at"
+      t.string "token"
+      t.string "default_ref"
+      t.string "path"
+      t.boolean "always_build", default: false, null: false
+      t.integer "polling_interval"
+      t.boolean "public", default: false, null: false
+      t.string "ssh_url_to_repo"
+      t.integer "gitlab_id"
+      t.boolean "allow_git_fetch", default: true, null: false
+      t.string "email_recipients", default: "", null: false
+      t.boolean "email_add_pusher", default: true, null: false
+      t.boolean "email_only_broken_builds", default: true, null: false
+      t.string "skip_refs"
+      t.string "coverage_regex"
+      t.boolean "shared_runners_enabled", default: false
+      t.text "generated_yaml_config"
+    end
+  end
+end
diff --git a/db/migrate/20170222143500_remove_old_project_id_columns.rb b/db/migrate/20170222143500_remove_old_project_id_columns.rb
new file mode 100644
index 0000000000000000000000000000000000000000..eac93e8e407c1c9eb5155db895c466d3e0ac1cd4
--- /dev/null
+++ b/db/migrate/20170222143500_remove_old_project_id_columns.rb
@@ -0,0 +1,28 @@
+class RemoveOldProjectIdColumns < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  disable_ddl_transaction!
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Unused columns are being removed.'
+
+  def up
+    remove_index :ci_builds, :project_id if
+      index_exists?(:ci_builds, :project_id)
+
+    remove_column :ci_builds, :project_id
+    remove_column :ci_commits, :project_id
+    remove_column :ci_runner_projects, :project_id
+    remove_column :ci_triggers, :project_id
+    remove_column :ci_variables, :project_id
+  end
+
+  def down
+    add_column :ci_builds, :project_id, :integer
+    add_column :ci_commits, :project_id, :integer
+    add_column :ci_runner_projects, :project_id, :integer
+    add_column :ci_triggers, :project_id, :integer
+    add_column :ci_variables, :project_id, :integer
+
+    add_concurrent_index :ci_builds, :project_id
+  end
+end
diff --git a/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb b/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7c19d471557811cccd05bcb9c388710cad7a518f
--- /dev/null
+++ b/db/migrate/20170222143603_rename_gl_project_id_to_project_id.rb
@@ -0,0 +1,14 @@
+class RenameGlProjectIdToProjectId < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Renaming an actively used column.'
+
+  def change
+    rename_column :ci_builds, :gl_project_id, :project_id
+    rename_column :ci_commits, :gl_project_id, :project_id
+    rename_column :ci_runner_projects, :gl_project_id, :project_id
+    rename_column :ci_triggers, :gl_project_id, :project_id
+    rename_column :ci_variables, :gl_project_id, :project_id
+  end
+end
diff --git a/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb b/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f061d9639214562447a7190311302110ab286f4
--- /dev/null
+++ b/db/migrate/20170301195939_rename_ci_commits_to_ci_pipelines.rb
@@ -0,0 +1,10 @@
+class RenameCiCommitsToCiPipelines < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON = 'Rename table ci_commits to ci_pipelines'
+
+  def change
+    rename_table 'ci_commits', 'ci_pipelines'
+  end
+end
diff --git a/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1e2abea5254c7e2aae899db165445fea265ad052
--- /dev/null
+++ b/db/migrate/20170301205639_remove_unused_ci_tables_and_columns.rb
@@ -0,0 +1,83 @@
+class RemoveUnusedCiTablesAndColumns < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = true
+  DOWNTIME_REASON =
+    'Remove unused columns in used tables.' \
+    ' Downtime required in case Rails caches them'
+
+  def up
+    %w[ci_application_settings
+       ci_events
+       ci_jobs
+       ci_sessions
+       ci_taggings
+       ci_tags].each do |table|
+      drop_table(table)
+    end
+
+    remove_column :ci_pipelines, :push_data, :text
+    remove_column :ci_builds, :job_id, :integer
+    remove_column :ci_builds, :deploy, :boolean
+  end
+
+  def down
+    add_column :ci_builds, :deploy, :boolean
+    add_column :ci_builds, :job_id, :integer
+    add_column :ci_pipelines, :push_data, :text
+
+    create_table "ci_tags", force: :cascade do |t|
+      t.string "name"
+      t.integer "taggings_count", default: 0
+    end
+
+    create_table "ci_taggings", force: :cascade do |t|
+      t.integer "tag_id"
+      t.integer "taggable_id"
+      t.string "taggable_type"
+      t.integer "tagger_id"
+      t.string "tagger_type"
+      t.string "context", limit: 128
+      t.datetime "created_at"
+    end
+
+    add_index "ci_taggings", %w[taggable_id taggable_type context]
+
+    create_table "ci_sessions", force: :cascade do |t|
+      t.string "session_id", null: false
+      t.text "data"
+      t.datetime "created_at"
+      t.datetime "updated_at"
+    end
+
+    create_table "ci_jobs", force: :cascade do |t|
+      t.integer "project_id", null: false
+      t.text "commands"
+      t.boolean "active", default: true, null: false
+      t.datetime "created_at"
+      t.datetime "updated_at"
+      t.string "name"
+      t.boolean "build_branches", default: true, null: false
+      t.boolean "build_tags", default: false, null: false
+      t.string "job_type", default: "parallel"
+      t.string "refs"
+      t.datetime "deleted_at"
+    end
+
+    create_table "ci_events", force: :cascade do |t|
+      t.integer "project_id"
+      t.integer "user_id"
+      t.integer "is_admin"
+      t.text "description"
+      t.datetime "created_at"
+      t.datetime "updated_at"
+    end
+
+    create_table "ci_application_settings", force: :cascade do |t|
+      t.boolean "all_broken_builds"
+      t.boolean "add_pusher"
+      t.datetime "created_at"
+      t.datetime "updated_at"
+    end
+  end
+end
diff --git a/db/migrate/20170313213916_add_index_to_user_ghost.rb b/db/migrate/20170313213916_add_index_to_user_ghost.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c429039c275a299188741cba4b55479a9f65b3f8
--- /dev/null
+++ b/db/migrate/20170313213916_add_index_to_user_ghost.rb
@@ -0,0 +1,24 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToUserGhost < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  # Set this constant to true if this migration requires downtime.
+  DOWNTIME = false
+
+  # When a migration requires downtime you **must** uncomment the following
+  # constant and define a short and easy to understand explanation as to why the
+  # migration requires downtime.
+  # DOWNTIME_REASON = ''
+
+  disable_ddl_transaction!
+
+  def up
+    add_concurrent_index :users, :ghost
+  end
+
+  def down
+    remove_index :users, :ghost
+  end
+end
diff --git a/db/migrate/20170315194013_add_closed_at_to_issues.rb b/db/migrate/20170315194013_add_closed_at_to_issues.rb
new file mode 100644
index 0000000000000000000000000000000000000000..1326118cc8dacec0e9a47b641e86b48978a81036
--- /dev/null
+++ b/db/migrate/20170315194013_add_closed_at_to_issues.rb
@@ -0,0 +1,7 @@
+class AddClosedAtToIssues < ActiveRecord::Migration
+  DOWNTIME = false
+
+  def change
+    add_column :issues, :closed_at, :datetime
+  end
+end
diff --git a/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb b/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2dd14ee5a787eebe2f393dc0b047c84772f45e64
--- /dev/null
+++ b/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
@@ -0,0 +1,87 @@
+class MigrateBuildEventsToPipelineEvents < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  include Gitlab::Database
+
+  DOWNTIME = false
+
+  def up
+    Gitlab::Database.with_connection_pool(2) do |pool|
+      threads = []
+
+      threads << Thread.new do
+        pool.with_connection do |connection|
+          Thread.current[:foreign_key_connection] = connection
+
+          execute(<<-SQL.strip_heredoc)
+            UPDATE services
+              SET properties = replace(properties,
+                                       'notify_only_broken_builds',
+                                       'notify_only_broken_pipelines')
+                , pipeline_events = #{true_value}
+                , build_events = #{false_value}
+            WHERE type IN
+              ('SlackService', 'MattermostService', 'HipchatService')
+              AND build_events = #{true_value};
+          SQL
+        end
+      end
+
+      threads << Thread.new do
+        pool.with_connection do |connection|
+          Thread.current[:foreign_key_connection] = connection
+
+          execute(update_pipeline_services_sql)
+        end
+      end
+
+      threads.each(&:join)
+    end
+  end
+
+  def down
+    # Don't bother to migrate the data back
+  end
+
+  def connection
+    # Rails memoizes connection objects, but this causes them to be shared
+    # amongst threads; we don't want that.
+    Thread.current[:foreign_key_connection] || ActiveRecord::Base.connection
+  end
+
+  private
+
+  def update_pipeline_services_sql
+    if Gitlab::Database.postgresql?
+      <<-SQL
+        UPDATE services
+          SET type = 'PipelinesEmailService'
+            , properties = replace(properties,
+                                   'notify_only_broken_builds',
+                                   'notify_only_broken_pipelines')
+            , pipeline_events = #{true_value}
+            , build_events = #{false_value}
+        WHERE type = 'BuildsEmailService'
+        AND
+          (SELECT 1 FROM services pipeline_services
+             WHERE pipeline_services.project_id = services.project_id
+               AND pipeline_services.type = 'PipelinesEmailService' LIMIT 1)
+          IS NULL;
+      SQL
+    else
+      <<-SQL
+        UPDATE services build_services
+         LEFT OUTER JOIN services pipeline_services
+           ON build_services.project_id = pipeline_services.project_id
+          AND pipeline_services.type = 'PipelinesEmailService'
+          SET build_services.type = 'PipelinesEmailService'
+            , build_services.properties = replace(build_services.properties,
+                                         'notify_only_broken_builds',
+                                         'notify_only_broken_pipelines')
+            , build_services.pipeline_events = #{true_value}
+            , build_services.build_events = #{false_value}
+        WHERE build_services.type = 'BuildsEmailService'
+          AND pipeline_services.id IS NULL;
+      SQL
+    end.strip_heredoc
+  end
+end
diff --git a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
index 9dfe77bedb7687a698d40b5a5c1dcb5f5330cb71..44c688fa134ca7e9a497d8379c846f6b97c972aa 100644
--- a/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
+++ b/db/post_migrate/20170313133418_rename_more_reserved_project_names.rb
@@ -6,41 +6,12 @@ class RenameMoreReservedProjectNames < ActiveRecord::Migration
 
   DOWNTIME = false
 
-  THREAD_COUNT = 8
-
   KNOWN_PATHS = %w(artifacts graphs refs badges).freeze
 
   def up
-    queues = Array.new(THREAD_COUNT) { Queue.new }
-    start = false
-
-    threads = Array.new(THREAD_COUNT) do |index|
-      Thread.new do
-        queue = queues[index]
-
-        # Wait until we have input to process.
-        until start; end
-
-        rename_projects(queue.pop) until queue.empty?
-      end
-    end
-
-    enum = queues.each
-
     reserved_projects.each_slice(100) do |slice|
-      begin
-        queue = enum.next
-      rescue StopIteration
-        enum.rewind
-        retry
-      end
-
-      queue << slice
+      rename_projects(slice)
     end
-
-    start = true
-
-    threads.each(&:join)
   end
 
   def down
diff --git a/db/schema.rb b/db/schema.rb
index 6eb3c95de93bc32bcdb454e0dadc32e445e727d7..f96a7d21890894f754054fe9959d1f3b40c30dc2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 20170315174634) do
+ActiveRecord::Schema.define(version: 20170315194013) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "plpgsql"
@@ -61,7 +61,6 @@ ActiveRecord::Schema.define(version: 20170315174634) do
     t.boolean "shared_runners_enabled", default: true, null: false
     t.integer "max_artifacts_size", default: 100, null: false
     t.string "runners_registration_token"
-    t.integer "max_pages_size", default: 100, null: false
     t.boolean "require_two_factor_authentication", default: false
     t.integer "two_factor_grace_period", default: 48
     t.boolean "metrics_enabled", default: false
@@ -111,6 +110,7 @@ ActiveRecord::Schema.define(version: 20170315174634) do
     t.string "plantuml_url"
     t.boolean "plantuml_enabled"
     t.integer "terminal_max_session_time", default: 0, null: false
+    t.integer "max_pages_size", default: 100, null: false
     t.string "default_artifacts_expire_in", default: "0", null: false
     t.integer "unique_ips_limit_per_user"
     t.integer "unique_ips_limit_time_window"
@@ -185,15 +185,7 @@ ActiveRecord::Schema.define(version: 20170315174634) do
 
   add_index "chat_teams", ["namespace_id"], name: "index_chat_teams_on_namespace_id", unique: true, using: :btree
 
-  create_table "ci_application_settings", force: :cascade do |t|
-    t.boolean "all_broken_builds"
-    t.boolean "add_pusher"
-    t.datetime "created_at"
-    t.datetime "updated_at"
-  end
-
   create_table "ci_builds", force: :cascade do |t|
-    t.integer "project_id"
     t.string "status"
     t.datetime "finished_at"
     t.text "trace"
@@ -204,9 +196,7 @@ ActiveRecord::Schema.define(version: 20170315174634) do
     t.float "coverage"
     t.integer "commit_id"
     t.text "commands"
-    t.integer "job_id"
     t.string "name"
-    t.boolean "deploy", default: false
     t.text "options"
     t.boolean "allow_failure", default: false, null: false
     t.string "stage"
@@ -219,7 +209,7 @@ ActiveRecord::Schema.define(version: 20170315174634) do
     t.string "target_url"
     t.string "description"
     t.text "artifacts_file"
-    t.integer "gl_project_id"
+    t.integer "project_id"
     t.text "artifacts_metadata"
     t.integer "erased_by_id"
     t.datetime "erased_at"
@@ -238,25 +228,22 @@ ActiveRecord::Schema.define(version: 20170315174634) do
   add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
   add_index "ci_builds", ["commit_id", "type", "name", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_name_and_ref", using: :btree
   add_index "ci_builds", ["commit_id", "type", "ref"], name: "index_ci_builds_on_commit_id_and_type_and_ref", using: :btree
-  add_index "ci_builds", ["gl_project_id"], name: "index_ci_builds_on_gl_project_id", using: :btree
   add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
   add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
   add_index "ci_builds", ["status", "type", "runner_id"], name: "index_ci_builds_on_status_and_type_and_runner_id", using: :btree
   add_index "ci_builds", ["status"], name: "index_ci_builds_on_status", using: :btree
   add_index "ci_builds", ["token"], name: "index_ci_builds_on_token", unique: true, using: :btree
 
-  create_table "ci_commits", force: :cascade do |t|
-    t.integer "project_id"
+  create_table "ci_pipelines", force: :cascade do |t|
     t.string "ref"
     t.string "sha"
     t.string "before_sha"
-    t.text "push_data"
     t.datetime "created_at"
     t.datetime "updated_at"
     t.boolean "tag", default: false
     t.text "yaml_errors"
     t.datetime "committed_at"
-    t.integer "gl_project_id"
+    t.integer "project_id"
     t.string "status"
     t.datetime "started_at"
     t.datetime "finished_at"
@@ -265,67 +252,20 @@ ActiveRecord::Schema.define(version: 20170315174634) do
     t.integer "lock_version"
   end
 
-  add_index "ci_commits", ["gl_project_id", "ref", "status"], name: "index_ci_commits_on_gl_project_id_and_ref_and_status", using: :btree
-  add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree
-  add_index "ci_commits", ["gl_project_id"], name: "index_ci_commits_on_gl_project_id", using: :btree
-  add_index "ci_commits", ["status"], name: "index_ci_commits_on_status", using: :btree
-  add_index "ci_commits", ["user_id"], name: "index_ci_commits_on_user_id", using: :btree
-
-  create_table "ci_events", force: :cascade do |t|
-    t.integer "project_id"
-    t.integer "user_id"
-    t.integer "is_admin"
-    t.text "description"
-    t.datetime "created_at"
-    t.datetime "updated_at"
-  end
-
-  create_table "ci_jobs", force: :cascade do |t|
-    t.integer "project_id", null: false
-    t.text "commands"
-    t.boolean "active", default: true, null: false
-    t.datetime "created_at"
-    t.datetime "updated_at"
-    t.string "name"
-    t.boolean "build_branches", default: true, null: false
-    t.boolean "build_tags", default: false, null: false
-    t.string "job_type", default: "parallel"
-    t.string "refs"
-    t.datetime "deleted_at"
-  end
-
-  create_table "ci_projects", force: :cascade do |t|
-    t.string "name"
-    t.integer "timeout", default: 3600, null: false
-    t.datetime "created_at"
-    t.datetime "updated_at"
-    t.string "token"
-    t.string "default_ref"
-    t.string "path"
-    t.boolean "always_build", default: false, null: false
-    t.integer "polling_interval"
-    t.boolean "public", default: false, null: false
-    t.string "ssh_url_to_repo"
-    t.integer "gitlab_id"
-    t.boolean "allow_git_fetch", default: true, null: false
-    t.string "email_recipients", default: "", null: false
-    t.boolean "email_add_pusher", default: true, null: false
-    t.boolean "email_only_broken_builds", default: true, null: false
-    t.string "skip_refs"
-    t.string "coverage_regex"
-    t.boolean "shared_runners_enabled", default: false
-    t.text "generated_yaml_config"
-  end
+  add_index "ci_pipelines", ["project_id", "ref", "status"], name: "index_ci_pipelines_on_project_id_and_ref_and_status", using: :btree
+  add_index "ci_pipelines", ["project_id", "sha"], name: "index_ci_pipelines_on_project_id_and_sha", using: :btree
+  add_index "ci_pipelines", ["project_id"], name: "index_ci_pipelines_on_project_id", using: :btree
+  add_index "ci_pipelines", ["status"], name: "index_ci_pipelines_on_status", using: :btree
+  add_index "ci_pipelines", ["user_id"], name: "index_ci_pipelines_on_user_id", using: :btree
 
   create_table "ci_runner_projects", force: :cascade do |t|
     t.integer "runner_id", null: false
-    t.integer "project_id"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer "gl_project_id"
+    t.integer "project_id"
   end
 
-  add_index "ci_runner_projects", ["gl_project_id"], name: "index_ci_runner_projects_on_gl_project_id", using: :btree
+  add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree
   add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
 
   create_table "ci_runners", force: :cascade do |t|
@@ -349,30 +289,6 @@ ActiveRecord::Schema.define(version: 20170315174634) do
   add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
   add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
 
-  create_table "ci_sessions", force: :cascade do |t|
-    t.string "session_id", null: false
-    t.text "data"
-    t.datetime "created_at"
-    t.datetime "updated_at"
-  end
-
-  create_table "ci_taggings", force: :cascade do |t|
-    t.integer "tag_id"
-    t.integer "taggable_id"
-    t.string "taggable_type"
-    t.integer "tagger_id"
-    t.string "tagger_type"
-    t.string "context", limit: 128
-    t.datetime "created_at"
-  end
-
-  add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
-
-  create_table "ci_tags", force: :cascade do |t|
-    t.string "name"
-    t.integer "taggings_count", default: 0
-  end
-
   create_table "ci_trigger_requests", force: :cascade do |t|
     t.integer "trigger_id", null: false
     t.text "variables"
@@ -385,28 +301,26 @@ ActiveRecord::Schema.define(version: 20170315174634) do
 
   create_table "ci_triggers", force: :cascade do |t|
     t.string "token"
-    t.integer "project_id"
     t.datetime "deleted_at"
     t.datetime "created_at"
     t.datetime "updated_at"
-    t.integer "gl_project_id"
+    t.integer "project_id"
     t.integer "owner_id"
     t.string "description"
   end
 
-  add_index "ci_triggers", ["gl_project_id"], name: "index_ci_triggers_on_gl_project_id", using: :btree
+  add_index "ci_triggers", ["project_id"], name: "index_ci_triggers_on_project_id", using: :btree
 
   create_table "ci_variables", force: :cascade do |t|
-    t.integer "project_id"
     t.string "key"
     t.text "value"
     t.text "encrypted_value"
     t.string "encrypted_value_salt"
     t.string "encrypted_value_iv"
-    t.integer "gl_project_id"
+    t.integer "project_id"
   end
 
-  add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree
+  add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
 
   create_table "deploy_keys_projects", force: :cascade do |t|
     t.integer "deploy_key_id", null: false
@@ -531,6 +445,7 @@ ActiveRecord::Schema.define(version: 20170315174634) do
     t.text "description_html"
     t.integer "time_estimate"
     t.integer "relative_position"
+    t.datetime "closed_at"
   end
 
   add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -773,8 +688,8 @@ ActiveRecord::Schema.define(version: 20170315174634) do
     t.integer "visibility_level", default: 20, null: false
     t.boolean "request_access_enabled", default: false, null: false
     t.datetime "deleted_at"
-    t.text "description_html"
     t.boolean "lfs_enabled"
+    t.text "description_html"
     t.integer "parent_id"
   end
 
@@ -1316,8 +1231,8 @@ ActiveRecord::Schema.define(version: 20170315174634) do
     t.datetime "otp_grace_period_started_at"
     t.boolean "ldap_email", default: false, null: false
     t.boolean "external", default: false
-    t.string "incoming_email_token"
     t.string "organization"
+    t.string "incoming_email_token"
     t.boolean "authorized_projects_populated"
     t.boolean "ghost"
   end
@@ -1329,6 +1244,7 @@ ActiveRecord::Schema.define(version: 20170315174634) do
   add_index "users", ["current_sign_in_at"], name: "index_users_on_current_sign_in_at", using: :btree
   add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
   add_index "users", ["email"], name: "index_users_on_email_trigram", using: :gin, opclasses: {"email"=>"gin_trgm_ops"}
+  add_index "users", ["ghost"], name: "index_users_on_ghost", using: :btree
   add_index "users", ["incoming_email_token"], name: "index_users_on_incoming_email_token", using: :btree
   add_index "users", ["name"], name: "index_users_on_name", using: :btree
   add_index "users", ["name"], name: "index_users_on_name_trigram", using: :gin, opclasses: {"name"=>"gin_trgm_ops"}
@@ -1378,7 +1294,7 @@ ActiveRecord::Schema.define(version: 20170315174634) do
   add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
   add_foreign_key "lists", "boards"
   add_foreign_key "lists", "labels"
-  add_foreign_key "merge_request_metrics", "ci_commits", column: "pipeline_id", on_delete: :cascade
+  add_foreign_key "merge_request_metrics", "ci_pipelines", column: "pipeline_id", on_delete: :cascade
   add_foreign_key "merge_request_metrics", "merge_requests", on_delete: :cascade
   add_foreign_key "merge_requests_closing_issues", "issues", on_delete: :cascade
   add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index 69b16b7c483f8f5cab941f68f7734c67a8222402..b2445d1c0e511178742eda4ffdfcac95b43bd222 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -96,21 +96,15 @@ Sample Prometheus queries:
 
 > Introduced in GitLab 9.0.
 
-If your GitLab server is running within Kubernetes, an option is now available
-to monitor the health of each node in the cluster. This is particularly helpful
-if your CI/CD environments run in the same cluster, and you would like enable
-[Prometheus integration][] to monitor them.
+If your GitLab server is running within Kubernetes, Prometheus will collect metrics from the Nodes in the cluster including performance data on each container. This is particularly helpful if your CI/CD environments run in the same cluster, as you can use the [Prometheus project integration][] to monitor them.
 
-When enabled, the bundled Prometheus server monitors Kubernetes and automatically
-[collects metrics][prometheus-cadvisor-metrics] from each Node in the cluster.
-
-To enable the Kubernetes monitoring:
+To disable the monitoring of Kubernetes:
 
 1. Edit `/etc/gitlab/gitlab.rb`
-1. Add or find and uncomment the following line:
+1. Add or find and uncomment the following line and set it to `false`:
 
     ```ruby
-    prometheus['monitor_kubernetes'] = true
+    prometheus['monitor_kubernetes'] = false
     ```
 
 1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
@@ -165,4 +159,4 @@ The GitLab monitor exporter allows you to measure various GitLab metrics.
 [reconfigure]: ../../restart_gitlab.md#omnibus-gitlab-reconfigure
 [1261]: https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/1261
 [prometheus integration]: ../../../user/project/integrations/prometheus.md
-[rometheus-cadvisor-metrics]: https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md
+[prometheus-cadvisor-metrics]: https://github.com/google/cadvisor/blob/master/docs/storage/prometheus.md
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 83705106160c81e49d2c87fe76dc9e7419c84e3d..815aabda8e3fdabead4a76363d68a3b52babc9ae 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -3,6 +3,8 @@
 ## List repository branches
 
 Get a list of repository branches from a project, sorted by name alphabetically.
+This endpoint can be accessed without authentication if the repository is
+publicly accessible.
 
 ```
 GET /projects/:id/repository/branches
@@ -48,7 +50,8 @@ Example response:
 
 ## Get single repository branch
 
-Get a single project repository branch.
+Get a single project repository branch. This endpoint can be accessed without
+authentication if the repository is publicly accessible.
 
 ```
 GET /projects/:id/repository/branches/:branch
diff --git a/doc/api/services.md b/doc/api/services.md
index 8e7afe41b0c24d68e9b79b069b2aa8de9eb1e39e..7d4779f11376315623a771d2c672c5384d25d2b0 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -139,43 +139,6 @@ Get Buildkite service settings for a project.
 GET /projects/:id/services/buildkite
 ```
 
-## Build-Emails
-
-Get emails for GitLab CI builds.
-
-### Create/Edit Build-Emails service
-
-Set Build-Emails service for a project.
-
-```
-PUT /projects/:id/services/jobs-email
-```
-
-Parameters:
-
-| Attribute | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `recipients` | string | yes | Comma-separated list of recipient email addresses |
-| `add_pusher` | boolean | no | Add pusher to recipients list |
-| `notify_only_broken_jobs` | boolean | no | Notify only broken jobs |
-
-
-### Delete Job-Emails service
-
-Delete Build-Emails service for a project.
-
-```
-DELETE /projects/:id/services/jobs-email
-```
-
-### Get Job-Emails service settings
-
-Get Build-Emails service settings for a project.
-
-```
-GET /projects/:id/services/jobs-email
-```
-
 ## Campfire
 
 Simple web-based real-time group chat
@@ -580,8 +543,7 @@ Parameters:
 | --------- | ---- | -------- | ----------- |
 | `recipients` | string | yes | Comma-separated list of recipient email addresses |
 | `add_pusher` | boolean | no | Add pusher to recipients list |
-| `notify_only_broken_jobs` | boolean | no | Notify only broken pipelines |
-
+| `notify_only_broken_pipelines` | boolean | no | Notify only broken pipelines |
 
 ### Delete Pipeline-Emails service
 
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 0794156bc39d97f549b09ca01ed96e988c169586..7f4426ee85d0ef35bfa12b31c531671fd59a3e08 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -8,16 +8,16 @@ Below are the changes made between V3 and V4.
 
 ### 8.17
 
-- Removed `/projects/:search` (use: `/projects?search=x`) [!8877](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8877)
-- `iid` filter has been removed from `projects/:id/issues` [!8967](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8967)
-- `projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
-- Endpoints under `projects/merge_request/:id` have been removed (use: `projects/merge_requests/:id`) [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
+- Removed `GET /projects/:search` (use: `GET /projects?search=x`) [!8877](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8877)
+- `iid` filter has been removed from `GET /projects/:id/issues` [!8967](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8967)
+- `GET /projects/:id/merge_requests?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
+- Endpoints under `GET /projects/merge_request/:id` have been removed (use: `GET /projects/merge_requests/:id`) [!8793](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8793)
 - Project snippets do not return deprecated field `expires_at` [!8723](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8723)
-- Endpoints under `projects/:id/keys` have been removed (use `projects/:id/deploy_keys`) [!8716](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8716)
+- Endpoints under `GET /projects/:id/keys` have been removed (use `GET /projects/:id/deploy_keys`) [!8716](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8716)
 
 ### 9.0
 
-- Status 409 returned for POST `project/:id/members` when a member already exists [!9093](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9093)
+- Status 409 returned for `POST /projects/:id/members` when a member already exists [!9093](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9093)
 - Moved `DELETE /projects/:id/star` to `POST /projects/:id/unstar` [!9328](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9328)
 - Removed the following deprecated Templates endpoints (these are still accessible with `/templates` prefix) [!8853](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8853)
   - `/licences`
@@ -28,31 +28,31 @@ Below are the changes made between V3 and V4.
   - `/gitignores/:key`
   - `/gitlab_ci_ymls/:key`
   - `/dockerfiles/:key`
-- Moved `/projects/fork/:id` to `/projects/:id/fork` [!8940](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8940)
+- Moved `POST /projects/fork/:id` to `POST /projects/:id/fork` [!8940](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8940)
 - Moved `DELETE /todos` to `POST /todos/mark_as_done` and `DELETE /todos/:todo_id` to `POST /todos/:todo_id/mark_as_done` [!9410](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9410)
 - Project filters are no longer available as `GET /projects/foo`, but as `GET /projects?foo=true` instead [!8962](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8962)
   - `GET /projects/visible` & `GET /projects/all` are consolidated into `GET /projects` and can be used with or without authorization
   - `GET /projects/owned` moved to `GET /projects?owned=true`
   - `GET /projects/starred` moved to `GET /projects?starred=true`
 - `GET /projects` returns all projects visible to current user, even if the user is not a member [!9674](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9674)
-  - To get projects the user is a member of, use `/projects?membership=true`
+  - To get projects the user is a member of, use `GET /projects?membership=true`
 - Return pagination headers for all endpoints that return an array [!8606](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8606)
 - Added `POST /environments/:environment_id/stop` to stop an environment [!8808](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8808)
-- Removed `DELETE projects/:id/deploy_keys/:key_id/disable`. Use `DELETE projects/:id/deploy_keys/:key_id` instead [!9366](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9366)
+- Removed `DELETE /projects/:id/deploy_keys/:key_id/disable`. Use `DELETE /projects/:id/deploy_keys/:key_id` instead [!9366](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9366)
 - Moved `PUT /users/:id/(block|unblock)` to `POST /users/:id/(block|unblock)` [!9371](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9371)
-- Make subscription API more RESTful. Use `post ":project_id/:subscribable_type/:subscribable_id/subscribe"` to subscribe and `post ":project_id/:subscribable_type/:subscribable_id/unsubscribe"` to unsubscribe from a resource. [!9325](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9325)
-- Labels filter on `projects/:id/issues` and `/issues` now matches only issues containing all labels (i.e.: Logical AND, not OR) [!8849](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8849)
+- Make subscription API more RESTful. Use `POST /projects/:id/:subscribable_type/:subscribable_id/subscribe` to subscribe and `POST /projects/:id/:subscribable_type/:subscribable_id/unsubscribe` to unsubscribe from a resource. [!9325](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9325)
+- Labels filter on `GET /projects/:id/issues` and `GET /issues` now matches only issues containing all labels (i.e.: Logical AND, not OR) [!8849](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8849)
 - Renamed param `branch_name` to `branch` on the following endpoints [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
-  - POST `:id/repository/branches`
-  - POST `:id/repository/commits`
-  - POST/PUT/DELETE `:id/repository/files`
-- Renamed `merge when build succeeds` to merge `when pipeline succeeds parameters` on the following endpoints: [!9335](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/)
-  - PUT `projects/:id/merge_requests/:merge_request_id/merge`
-  - POST `projects/:id/merge_requests/:merge_request_id/cancel_merge_when_pipeline_succeeds`
-  - POST `projects`
-  - POST `projects/user/:user_id`
-  - PUT `projects/:id`
-- Renamed `branch_name` to `branch` on DELETE `id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
+  - `POST /projects/:id/repository/branches`
+  - `POST /projects/:id/repository/commits`
+  - `POST/PUT/DELETE :id/repository/files`
+- Renamed the `merge_when_build_succeeds` parameter to `merge_when_pipeline_succeeds` on the following endpoints: [!9335](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/)
+  - `PUT /projects/:id/merge_requests/:merge_request_id/merge`
+  - `POST /projects/:id/merge_requests/:merge_request_id/cancel_merge_when_pipeline_succeeds`
+  - `POST /projects`
+  - `POST /projects/user/:user_id`
+  - `PUT /projects/:id`
+- Renamed `branch_name` to `branch` on `DELETE /projects/:id/repository/branches/:branch` response [!8936](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8936)
 - Remove `public` param from create and edit actions of projects [!8736](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8736)
 - Remove `subscribed` field from responses returning list of issues or merge
   requests. Fetch individual issues or merge requests to obtain the value
@@ -62,21 +62,21 @@ Below are the changes made between V3 and V4.
 - Notes do not return deprecated field `upvote` and `downvote` [!9384](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9384)
 - Return HTTP status code `400` for all validation errors when creating or updating a member instead of sometimes `422` error. [!9523](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9523)
 - Remove `GET /groups/owned`. Use `GET /groups?owned=true` instead [!9505](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9505)
-- Return 202 with JSON body on async removals on V4 API (DELETE `/projects/:id/repository/merged_branches` and DELETE `/projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449)
-- `projects/:id/milestones?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!9096](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9096)
+- Return 202 with JSON body on async removals on V4 API (`DELETE /projects/:id/repository/merged_branches` and `DELETE /projects/:id`) [!9449](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9449)
+- `GET /projects/:id/milestones?iid[]=x&iid[]=y` array filter has been renamed to `iids` [!9096](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9096)
 - Return basic info about pipeline in `GET /projects/:id/pipelines` [!8875](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8875)
 - Renamed all `build` references to `job` [!9463](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9463)
-- Drop GET '/projects/:id/repository/commits/:sha/jobs' [!9463](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9463)
+- Drop `GET /projects/:id/repository/commits/:sha/jobs` [!9463](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9463)
 - Rename Build Triggers to be Pipeline Triggers API [!9713](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9713)
   - `POST /projects/:id/trigger/builds` to `POST /projects/:id/trigger/pipeline`
   - Require description when creating a new trigger `POST /projects/:id/triggers`
 - Simplify project payload exposed on Environment endpoints [!9675](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9675)
 - API uses merge request `IID`s (internal ID, as in the web UI) rather than `ID`s. This affects the merge requests, award emoji, todos, and time tracking APIs. [!9530](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9530)
 - API uses issue `IID`s (internal ID, as in the web UI) rather than `ID`s. This affects the issues, award emoji, todos, and time tracking APIs. [!9530](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9530)
-- Change initial page from `0` to `1` on `GET projects/:id/repository/commits` (like on the rest of the API) [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
-- Return correct `Link` header data for `GET projects/:id/repository/commits` [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
+- Change initial page from `0` to `1` on `GET /projects/:id/repository/commits` (like on the rest of the API) [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
+- Return correct `Link` header data for `GET /projects/:id/repository/commits` [!9679] (https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9679)
 - Update endpoints for repository files [!9637](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/9637)
-  - Moved `/projects/:id/repository/files?file_path=:file_path` to `/projects/:id/repository/files/:file_path` (`:file_path` should be URL-encoded)
-  - `/projects/:id/repository/blobs/:sha` now returns JSON attributes for the blob identified by `:sha`, instead of finding the commit identified by `:sha` and returning the raw content of the blob in that commit identified by the required `?filepath=:filepath`
-  - Moved `/projects/:id/repository/commits/:sha/blob?file_path=:file_path`  and `/projects/:id/repository/blobs/:sha?file_path=:file_path`  to `/projects/:id/repository/files/:file_path/raw?ref=:sha`
-  - `/projects/:id/repository/tree` parameter `ref_name` has been renamed to `ref` for consistency
+  - Moved `GET /projects/:id/repository/files?file_path=:file_path` to `GET /projects/:id/repository/files/:file_path` (`:file_path` should be URL-encoded)
+  - `GET /projects/:id/repository/blobs/:sha` now returns JSON attributes for the blob identified by `:sha`, instead of finding the commit identified by `:sha` and returning the raw content of the blob in that commit identified by the required `?filepath=:filepath`
+  - Moved `GET /projects/:id/repository/commits/:sha/blob?file_path=:file_path`  and `GET /projects/:id/repository/blobs/:sha?file_path=:file_path` to `GET /projects/:id/repository/files/:file_path/raw?ref=:sha`
+  - `GET /projects/:id/repository/tree` parameter `ref_name` has been renamed to `ref` for consistency
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 76e86f3e3c3b65b466c001a54c9403352e77089d..30f209f80eb614163a3116334089c60e1a2cd3cd 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -207,15 +207,6 @@ you expected.
 You are also able to view the status of any commit in the various pages in
 GitLab, such as **Commits** and **Merge requests**.
 
-## Enabling build emails
-
-If you want to receive e-mail notifications about the result status of the
-jobs, you should explicitly enable the **Builds Emails** service under your
-project's settings.
-
-For more information read the
-[Builds emails service documentation](../../user/project/integrations/builds_emails.md).
-
 ## Examples
 
 Visit the [examples README][examples] to see a list of examples using GitLab
diff --git a/doc/project_services/builds_emails.md b/doc/project_services/builds_emails.md
deleted file mode 100644
index ee54d8652256619150abeda05eea1233610af1c3..0000000000000000000000000000000000000000
--- a/doc/project_services/builds_emails.md
+++ /dev/null
@@ -1 +0,0 @@
-This document was moved to [user/project/integrations/builds_emails.md](../user/project/integrations/builds_emails.md).
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 678f5199b02617ec6f4435581d638a117ce54ec0..cf28f1a2ecabbf41039ddcc5c373208a40c76e29 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -170,12 +170,12 @@ Integration (CI) server. By using deploy keys, you don't have to setup a
 dummy user account.
 
 If you are a project master or owner, you can add a deploy key in the
-project settings under the section 'Deploy Keys'. Press the 'New Deploy
-Key' button and upload a public SSH key. After this, the machine that uses
+project settings under the section 'Repository'. Specify a title for the new
+deploy key and paste a public SSH key. After this, the machine that uses
 the corresponding private SSH key has read-only or read-write (if enabled) 
 access to the project.
 
-You can't add the same deploy key twice with the 'New Deploy Key' option.
+You can't add the same deploy key twice using the form.
 If you want to add the same key to another project, please enable it in the
 list that says 'Deploy keys from projects available to you'. All the deploy
 keys of all the projects you have access to are available. This project
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index 75956aeb360af2687504f9827fefe59aa47b8df3..ed0e668d854f8f8c5489a83958cc9bf96d3a4832 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-13-stable-ee
 ```bash
 cd /home/git/gitlab-shell
 sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.6.6
+sudo -u git -H git checkout v3.6.7
 ```
 
 ### 6. Update gitlab-workhorse
diff --git a/doc/update/8.17-to-9.0.md b/doc/update/8.17-to-9.0.md
index 626507c0482f6ba882ea0f385a6b468db61ff235..b7ba970031c4758deedcddc1d3f016e83dad9d1e 100644
--- a/doc/update/8.17-to-9.0.md
+++ b/doc/update/8.17-to-9.0.md
@@ -115,11 +115,11 @@ sudo -u git -H bundle clean
 # Run database migrations
 sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
 
-# Install/update frontend asset dependencies
-sudo -u git -H npm install --production
+# Update node dependencies and recompile assets
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
 
-# Clean up assets and cache
-sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production
+# Clean up cache
+sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
 ```
 
 **MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
diff --git a/doc/user/project/integrations/builds_emails.md b/doc/user/project/integrations/builds_emails.md
deleted file mode 100644
index f769dece242e5260425eb805e1285ae9e44b3059..0000000000000000000000000000000000000000
--- a/doc/user/project/integrations/builds_emails.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Enabling build emails
-
-By enabling this service, you will be able to receive e-mail notifications about
-the result status of your builds.
-
-Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
-and select the **Builds emails** service to configure it.
-
-In the _Recipients_ area, provide a list of e-mails separated by comma.
-
-Check the _Add pusher_ checkbox if you want the committer to also receive
-e-mail notifications about each build's status.
-
-If you enable the _Notify only broken builds_ option, e-mail notifications will
-be sent only for failed builds.
diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md
index cfb0931273d918f1684d3518d059bb189f9d9cf6..3e77823a6aa6c9cfe5808c8a0f3a82ba891f1a95 100644
--- a/doc/user/project/integrations/mattermost.md
+++ b/doc/user/project/integrations/mattermost.md
@@ -28,7 +28,6 @@ There, you will see a checkbox with the following events that can be triggered:
 - Merge request
 - Note
 - Tag push
-- Build
 - Pipeline
 - Wiki page
 
@@ -41,7 +40,6 @@ At the end, fill in your Mattermost details:
 | ----- | ----------- |
 | **Webhook**  | The incoming webhook URL which you have to setup on Mattermost, it will be something like: http://mattermost.example/hooks/5xo… |
 | **Username** | Optional username which can be on messages sent to Mattermost. Fill this in if you want to change the username of the bot. |
-| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
 | **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
 
 ![Mattermost configuration](img/mattermost_configuration.png)
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 3cf1cc704a2c906c5b8f6d0a41840c958f6b95e0..25400633de5894a5ccd2b0d850b06ce55f1a1e89 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -32,7 +32,6 @@ Click on the service links to see further configuration instructions and details
 | Assembla 	| Project Management Software (Source Commits Endpoint) |
 | [Atlassian Bamboo CI](bamboo.md) | A continuous integration and build server |
 | Buildkite | Continuous integration and deployments |
-| [Builds emails](builds_emails.md) |	Email the builds status to a list of recipients |
 | [Bugzilla](bugzilla.md) | Bugzilla issue tracker |
 | Campfire | Simple web-based real-time group chat |
 | Custom Issue Tracker | Custom issue tracker |
@@ -48,6 +47,7 @@ Click on the service links to see further configuration instructions and details
 | [Kubernetes](kubernetes.md) | A containerized deployment service |
 | [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
 | [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
+| Pipelines emails | Email the pipeline status to a list of recipients |
 | [Slack Notifications](slack.md) | Receive event notifications in Slack |
 | [Slack slash commands](slack_slash_commands.md) | Slack chat and ChatOps slash commands |
 | PivotalTracker | Project Management Software (Source Commits Endpoint) |
diff --git a/doc/user/project/integrations/slack.md b/doc/user/project/integrations/slack.md
index f27f9a726fc0dbd8e4c23f5e8ce3bda1e5f51011..e8b238351ca25352a848ad253d33e04e2c5adaf0 100644
--- a/doc/user/project/integrations/slack.md
+++ b/doc/user/project/integrations/slack.md
@@ -25,7 +25,6 @@ There, you will see a checkbox with the following events that can be triggered:
 - Merge request
 - Note
 - Tag push
-- Build
 - Pipeline
 - Wiki page
 
@@ -38,7 +37,6 @@ At the end, fill in your Slack details:
 | ----- | ----------- |
 | **Webhook**  | The [incoming webhook URL][slackhook] which you have to setup on Slack. |
 | **Username** | Optional username which can be on messages sent to Slack. Fill this in if you want to change the username of the bot. |
-| **Notify only broken builds** | If you choose to enable the **Build** event and you want to be only notified about failed builds. |
 | **Notify only broken pipelines** | If you choose to enable the **Pipeline** event and you want to be only notified about failed pipelines. |
 
 After you are all done, click **Save changes** for the changes to take effect.
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index be042ddf6236802d9903d5ee22be67acd486d65f..7a4f9f408f190d3479a8feb8876ba514c6825405 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -34,7 +34,7 @@ with all their related data and be moved into a new GitLab instance.
 | 8.10.0   | 0.1.2    |
 | 8.9.5    | 0.1.1    |
 | 8.9.0    | 0.1.0    |
- 
+
  > The table reflects what GitLab version we updated the Import/Export version at.
  > For instance, 8.10.3 and 8.11 will have the same Import/Export version (0.1.3)
  > and the exports between them will be compatible.
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 2cc64fc671240b420cf99293bc93debe4c43a255..f35084a582ac286f5f69f8f2b1c42c996d975bf5 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -4,7 +4,6 @@ module API
   class Branches < Grape::API
     include PaginationParams
 
-    before { authenticate! }
     before { authorize! :download_code, user_project }
 
     params do
@@ -102,6 +101,7 @@ module API
       end
       post ":id/repository/branches" do
         authorize_push_project
+
         result = CreateBranchService.new(user_project, current_user).
                  execute(params[:branch], params[:ref])
 
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 0a12ee72d49f03a0132b2986fe364e5f34e6c6c1..5954aea80411d40c6b49d6b8dc5b2e1cfc3ee697 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -768,7 +768,7 @@ module API
       end
 
       class Dependency < Grape::Entity
-        expose :id, :name
+        expose :id, :name, :token
         expose :artifacts_file, using: ArtifactFile, if: ->(job, _) { job.artifacts? }
       end
 
@@ -796,7 +796,7 @@ module API
         expose :artifacts, using: Artifacts
         expose :cache, using: Cache
         expose :credentials, using: Credentials
-        expose :depends_on_builds, as: :dependencies, using: Dependency
+        expose :dependencies, using: Dependency
       end
     end
   end
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index ec2bcaed9297048d75b08aa397b2aa24b34aad76..74848a6e1442297d71399c07663317ee3ef42fc0 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -41,14 +41,6 @@ module API
           (Time.now - current_runner.contacted_at) >= contacted_at_max_age
       end
 
-      def job_not_found!
-        if headers['User-Agent'].to_s =~ /gitlab(-ci-multi)?-runner \d+\.\d+\.\d+(~beta\.\d+\.g[0-9a-f]+)? /
-          no_content!
-        else
-          not_found!
-        end
-      end
-
       def validate_job!(job)
         not_found! unless job
 
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index c700d2ef4a13a30c8c2da5c92a927a6ab9735ca8..4c9db2c87161591bbd0f0f2bbf984d34663ff024 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -47,11 +47,25 @@ module API
         authenticate_runner!
         Ci::Runner.find_by_token(params[:token]).destroy
       end
+
+      desc 'Validates authentication credentials' do
+        http_codes [[200, 'Credentials are valid'], [403, 'Forbidden']]
+      end
+      params do
+        requires :token, type: String, desc: %q(Runner's authentication token)
+      end
+      post '/verify' do
+        authenticate_runner!
+        status 200
+      end
     end
 
     resource :jobs do
       desc 'Request a job' do
         success Entities::JobRequest::Response
+        http_codes [[201, 'Job was scheduled'],
+                    [204, 'No job for Runner'],
+                    [403, 'Forbidden']]
       end
       params do
         requires :token, type: String, desc: %q(Runner's authentication token)
@@ -60,13 +74,13 @@ module API
       end
       post '/request' do
         authenticate_runner!
-        not_found! unless current_runner.active?
+        no_content! unless current_runner.active?
         update_runner_info
 
         if current_runner.is_runner_queue_value_latest?(params[:last_update])
           header 'X-GitLab-Last-Update', params[:last_update]
           Gitlab::Metrics.add_event(:build_not_found_cached)
-          return job_not_found!
+          return no_content!
         end
 
         new_update = current_runner.ensure_runner_queue_value
@@ -80,7 +94,7 @@ module API
           else
             Gitlab::Metrics.add_event(:build_not_found)
             header 'X-GitLab-Last-Update', new_update
-            job_not_found!
+            no_content!
           end
         else
           # We received build that is invalid due to concurrency conflict
diff --git a/lib/api/services.rb b/lib/api/services.rb
index be614bb8dc07b2ffce8269ca0e6d1efdcea134c1..4e0c9cb1f63c20a783e06475be9d3f53d435082a 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -107,26 +107,6 @@ module API
           desc: 'Enable SSL verification for communication'
         }
       ],
-      'builds-email' => [
-        {
-          required: true,
-          name: :recipients,
-          type: String,
-          desc: 'Comma-separated list of recipient email addresses'
-        },
-        {
-          required: false,
-          name: :add_pusher,
-          type: Boolean,
-          desc: 'Add pusher to recipients list'
-        },
-        {
-          required: false,
-          name: :notify_only_broken_jobs,
-          type: Boolean,
-          desc: 'Notify only broken jobs'
-        }
-      ],
       'campfire' => [
         {
           required: true,
@@ -403,9 +383,9 @@ module API
         },
         {
           required: false,
-          name: :notify_only_broken_jobs,
+          name: :notify_only_broken_pipelines,
           type: Boolean,
-          desc: 'Notify only broken jobs'
+          desc: 'Notify only broken pipelines'
         }
       ],
       'pivotaltracker' => [
@@ -550,7 +530,6 @@ module API
       BambooService,
       BugzillaService,
       BuildkiteService,
-      BuildsEmailService,
       CampfireService,
       CustomIssueTrackerService,
       DroneCiService,
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index aa3c9a06ed56bf38c737d0bd205432dad84e2125..a9f2ca2608ea526c3b395fa96daaaee5b9e53c01 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -14,7 +14,7 @@ module API
         requires :token, type: String, desc: 'The unique token of trigger'
         optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
       end
-      post ":id/(ref/:ref/)trigger/pipeline" do
+      post ":id/(ref/:ref/)trigger/pipeline", requirements: { ref: /.+/ } do
         project = find_project(params[:id])
         trigger = Ci::Trigger.find_by_token(params[:token].to_s)
         not_found! unless project && trigger
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
index b46639a2205ec97d409a781901860df374fcd616..a23d6b6b48cb1546c7a6dd696c7cc53a00a03e46 100644
--- a/lib/api/v3/triggers.rb
+++ b/lib/api/v3/triggers.rb
@@ -15,7 +15,7 @@ module API
           requires :token, type: String, desc: 'The unique token of trigger'
           optional :variables, type: Hash, desc: 'The list of variables to be injected into build'
         end
-        post ":id/(ref/:ref/)trigger/builds" do
+        post ":id/(ref/:ref/)trigger/builds", requirements: { ref: /.+/ } do
           project = find_project(params[:id])
           trigger = Ci::Trigger.find_by_token(params[:token].to_s)
           not_found! unless project && trigger
diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb
index 651b55523c0ae8b94c06d6455025663a2ce7a4ab..123c92fd2500b3906dc9197489d76cb4ca61bc83 100644
--- a/lib/banzai/filter/image_link_filter.rb
+++ b/lib/banzai/filter/image_link_filter.rb
@@ -2,7 +2,6 @@ module Banzai
   module Filter
     # HTML filter that wraps links around inline images.
     class ImageLinkFilter < HTML::Pipeline::Filter
-      
       # Find every image that isn't already wrapped in an `a` tag, create
       # a new node (a link to the image source), copy the image as a child
       # of the anchor, and then replace the img with the link-wrapped version.
@@ -12,7 +11,8 @@ module Banzai
             'a',
             class: 'no-attachment-icon',
             href: img['src'],
-            target: '_blank'
+            target: '_blank',
+            rel: 'noopener noreferrer'
           )
 
           link.children = img.clone
diff --git a/lib/banzai/filter/video_link_filter.rb b/lib/banzai/filter/video_link_filter.rb
index b64a1287d4ded2337b3aa436d81e19f935aab4ec..35cb10eae5dd068bd00bd3141c2314bdaafe8905 100644
--- a/lib/banzai/filter/video_link_filter.rb
+++ b/lib/banzai/filter/video_link_filter.rb
@@ -43,6 +43,7 @@ module Banzai
           element['title'] || element['alt'],
           href: element['src'],
           target: '_blank',
+          rel: 'noopener noreferrer',
           title: "Download '#{element['title'] || element['alt']}'")
         download_paragraph = doc.document.create_element('p')
         download_paragraph.children = link
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index f3f417c1a638cd6a5b597da15574f4f8d46718ad..63b8d0d3b9d1fdb267799c0051af876899893d5c 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -5,8 +5,12 @@ module Gitlab
     # http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
     MAX_INT_VALUE = 2147483647
 
+    def self.config
+      ActiveRecord::Base.configurations[Rails.env]
+    end
+
     def self.adapter_name
-      ActiveRecord::Base.configurations[Rails.env]['adapter']
+      config['adapter']
     end
 
     def self.mysql?
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index 2a017c93f57c68405e0660c4cc30080fb870f974..019be15135309bdb434bc9085de707ca559ae2f9 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -176,9 +176,13 @@ module Gitlab
       def initialize(raw_diff, collapse: false)
         case raw_diff
         when Hash
-          init_from_hash(raw_diff, collapse: collapse)
+          init_from_hash(raw_diff)
+          prune_diff_if_eligible(collapse)
         when Rugged::Patch, Rugged::Diff::Delta
           init_from_rugged(raw_diff, collapse: collapse)
+        when Gitaly::CommitDiffResponse
+          init_from_gitaly(raw_diff)
+          prune_diff_if_eligible(collapse)
         when nil
           raise "Nil as raw diff passed"
         else
@@ -266,13 +270,26 @@ module Gitlab
         @diff = encode!(strip_diff_headers(patch.to_s))
       end
 
-      def init_from_hash(hash, collapse: false)
+      def init_from_hash(hash)
         raw_diff = hash.symbolize_keys
 
         serialize_keys.each do |key|
           send(:"#{key}=", raw_diff[key.to_sym])
         end
+      end
+
+      def init_from_gitaly(diff_msg)
+        @diff = diff_msg.raw_chunks.join
+        @new_path = encode!(diff_msg.to_path.dup)
+        @old_path = encode!(diff_msg.from_path.dup)
+        @a_mode = diff_msg.old_mode.to_s(8)
+        @b_mode = diff_msg.new_mode.to_s(8)
+        @new_file = diff_msg.from_id == BLANK_SHA
+        @renamed_file = diff_msg.from_path != diff_msg.to_path
+        @deleted_file = diff_msg.to_id == BLANK_SHA
+      end
 
+      def prune_diff_if_eligible(collapse = false)
         prune_large_diff! if too_large?
         prune_collapsed_diff! if collapse && collapsible?
       end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 65e06f5065dbe83ab9b14be856b0d4b64183c071..4e45ec7c17407ebd99b0733154b4720276972db1 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -30,7 +30,9 @@ module Gitlab
         elsif @deltas_only
           each_delta(&block)
         else
-          each_patch(&block)
+          Gitlab::GitalyClient.migrate(:commit_raw_diffs) do
+            each_patch(&block)
+          end
         end
       end
 
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index b981a629fb0a00f83f9a628595667bca32ee427c..5534d4af4397b58ac0c23cf7b1640620c47fe43b 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -25,5 +25,19 @@ module Gitlab
     def self.enabled?
       gitaly_address.present?
     end
+
+    def self.feature_enabled?(feature)
+      enabled? && ENV["GITALY_#{feature.upcase}"] == '1'
+    end
+
+    def self.migrate(feature)
+      is_enabled  = feature_enabled?(feature)
+      metric_name = feature.to_s
+      metric_name += "_gitaly" if is_enabled
+
+      Gitlab::Metrics.measure(metric_name) do
+        yield is_enabled
+      end
+    end
   end
 end
diff --git a/lib/gitlab/gitaly_client/commit.rb b/lib/gitlab/gitaly_client/commit.rb
new file mode 100644
index 0000000000000000000000000000000000000000..525b8d680e90fc0f2b00db1191b850a0fb207bba
--- /dev/null
+++ b/lib/gitlab/gitaly_client/commit.rb
@@ -0,0 +1,25 @@
+module Gitlab
+  module GitalyClient
+    class Commit
+      # The ID of empty tree.
+      # See http://stackoverflow.com/a/40884093/1856239 and https://github.com/git/git/blob/3ad8b5bf26362ac67c9020bf8c30eee54a84f56d/cache.h#L1011-L1012
+      EMPTY_TREE_ID = '4b825dc642cb6eb9a060e54bf8d69288fbee4904'.freeze
+
+      class << self
+        def diff_from_parent(commit, options = {})
+          stub      = Gitaly::Diff::Stub.new(nil, nil, channel_override: GitalyClient.channel)
+          repo      = Gitaly::Repository.new(path: commit.project.repository.path_to_repo)
+          parent    = commit.parents[0]
+          parent_id = parent ? parent.id : EMPTY_TREE_ID
+          request   = Gitaly::CommitDiffRequest.new(
+            repository: repo,
+            left_commit_id: parent_id,
+            right_commit_id: commit.id
+          )
+
+          Gitlab::Git::DiffCollection.new(stub.commit_diff(request), options)
+        end
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 416194e57d7fd7a4dc26127c25edb1acc6ee98ac..ab74c8782f6f08911aa8a07d2429c8039b97551a 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -73,6 +73,9 @@ excluded_attributes:
     - :milestone_id
   award_emoji:
     - :awardable_id
+  statuses:
+    - :trace
+    - :token
 
 methods:
   labels:
@@ -81,6 +84,7 @@ methods:
     - :type
   statuses:
     - :type
+    - :gl_project_id
   services:
     - :type
   merge_request_diff:
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index fae792237d90370d0a9a5cb1e62f8002bd57b400..d44563333a5032f29122c9e76e3aedf1105c8355 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -15,7 +15,7 @@ module Gitlab
 
       USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id merge_user_id resolved_by_id].freeze
 
-      PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
+      PROJECT_REFERENCES = %w[project_id source_project_id target_project_id].freeze
 
       BUILD_MODELS = %w[Ci::Build commit_status].freeze
 
@@ -98,12 +98,11 @@ module Gitlab
       end
 
       def generate_imported_object
-        if BUILD_MODELS.include?(@relation_name) # call #trace= method after assigning the other attributes
-          trace = @relation_hash.delete('trace')
+        if BUILD_MODELS.include?(@relation_name)
+          @relation_hash.delete('trace') # old export files have trace
           @relation_hash.delete('token')
 
           imported_object do |object|
-            object.trace = trace
             object.commit_id = nil
           end
         else
@@ -121,7 +120,6 @@ module Gitlab
 
         # project_id may not be part of the export, but we always need to populate it if required.
         @relation_hash['project_id'] = project_id
-        @relation_hash['gl_project_id'] = project_id if @relation_hash['gl_project_id']
         @relation_hash['target_project_id'] = project_id if @relation_hash['target_project_id']
       end
 
diff --git a/lib/gitlab/url_blocker.rb b/lib/gitlab/url_blocker.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7e14a5666960c34f85602c7118cc17250e0d8fa4
--- /dev/null
+++ b/lib/gitlab/url_blocker.rb
@@ -0,0 +1,59 @@
+require 'resolv'
+
+module Gitlab
+  class UrlBlocker
+    class << self
+      # Used to specify what hosts and port numbers should be prohibited for project
+      # imports.
+      VALID_PORTS = [22, 80, 443].freeze
+
+      def blocked_url?(url)
+        return false if url.nil?
+
+        blocked_ips = ["127.0.0.1", "::1", "0.0.0.0"]
+        blocked_ips.concat(Socket.ip_address_list.map(&:ip_address))
+
+        begin
+          uri = Addressable::URI.parse(url)
+          # Allow imports from the GitLab instance itself but only from the configured ports
+          return false if internal?(uri)
+
+          return true if blocked_port?(uri.port)
+
+          server_ips = Resolv.getaddresses(uri.hostname)
+          return true if (blocked_ips & server_ips).any?
+        rescue Addressable::URI::InvalidURIError
+          return true
+        end
+
+        false
+      end
+
+      private
+
+      def blocked_port?(port)
+        return false if port.blank?
+
+        port < 1024 && !VALID_PORTS.include?(port)
+      end
+
+      def internal?(uri)
+        internal_web?(uri) || internal_shell?(uri)
+      end
+
+      def internal_web?(uri)
+        uri.hostname == config.gitlab.host &&
+          (uri.port.blank? || uri.port == config.gitlab.port)
+      end
+
+      def internal_shell?(uri)
+        uri.hostname == config.gitlab_shell.ssh_host &&
+          (uri.port.blank? || uri.port == config.gitlab_shell.ssh_port)
+      end
+
+      def config
+        Gitlab.config
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/url_sanitizer.rb b/lib/gitlab/url_sanitizer.rb
index c81dc7e30d0d9ef38c5bbd57a277fe609c9bd542..9ce13feb79a73f7aab32219de5fa60ebb8445d3e 100644
--- a/lib/gitlab/url_sanitizer.rb
+++ b/lib/gitlab/url_sanitizer.rb
@@ -18,6 +18,12 @@ module Gitlab
       false
     end
 
+    def self.http_credentials_for_user(user)
+      return {} unless user.respond_to?(:username)
+
+      { user: user.username }
+    end
+
     def initialize(url, credentials: nil)
       @url = Addressable::URI.parse(url.strip)
       @credentials = credentials
diff --git a/lib/gitlab/visibility_level.rb b/lib/gitlab/visibility_level.rb
index 2248763c10670b129723a022fba913260e890861..8f1d1fdc02eddd9e52d3e83a260aa0f8077b0142 100644
--- a/lib/gitlab/visibility_level.rb
+++ b/lib/gitlab/visibility_level.rb
@@ -96,8 +96,8 @@ module Gitlab
       end
 
       def level_value(level)
-        return string_options[level] if level.is_a? String
-        level
+        return level.to_i if level.to_i.to_s == level.to_s && string_options.key(level.to_i)
+        string_options[level] || PRIVATE
       end
 
       def string_level(level)
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 2f7c34a3f31f1e04b2b4be1a5c4a907b901b375a..f25e66d54c89a888fed6d9bef87246ec255f7498 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -38,6 +38,13 @@ server {
 
   ## See app/controllers/application_controller.rb for headers set
 
+  ## Real IP Module Config
+  ## http://nginx.org/en/docs/http/ngx_http_realip_module.html
+  real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol
+  real_ip_recursive off;    ## If you enable 'on'
+  ## If you have a trusted IP address, uncomment it and set it
+  # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
+
   ## Individual nginx logs for this GitLab vhost
   access_log  /var/log/nginx/gitlab_access.log;
   error_log   /var/log/nginx/gitlab_error.log;
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 330031aaddcaf7cdbab875c4b804fc70223b53ab..2b40da18babcbd3858f81895cc29e8888cdda970 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -85,6 +85,13 @@ server {
   ## [Optional] Enable HTTP Strict Transport Security
   # add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
 
+  ## Real IP Module Config
+  ## http://nginx.org/en/docs/http/ngx_http_realip_module.html
+  real_ip_header X-Real-IP; ## X-Real-IP or X-Forwarded-For or proxy_protocol
+  real_ip_recursive off;    ## If you enable 'on'
+  ## If you have a trusted IP address, uncomment it and set it
+  # set_real_ip_from YOUR_TRUSTED_ADDRESS; ## Replace this with something like 192.168.1.0/24
+
   ## Individual nginx logs for this GitLab vhost
   access_log  /var/log/nginx/gitlab_access.log;
   error_log   /var/log/nginx/gitlab_error.log;
diff --git a/package.json b/package.json
index 1048e29d0acf45eeb836fe36ae02d81ec6272de2..b3d038bd3d1a1b203bba2d361c579b11305c2838 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
     "eslint-fix": "eslint --max-warnings 0 --ext .js --fix .",
     "eslint-report": "eslint --max-warnings 0 --ext .js --format html --output-file ./eslint-report.html .",
     "karma": "karma start config/karma.config.js --single-run",
+    "karma-coverage": "BABEL_ENV=coverage karma start config/karma.config.js --single-run",
     "karma-start": "karma start config/karma.config.js",
     "webpack": "webpack --config config/webpack.config.js",
     "webpack-prod": "NODE_ENV=production webpack --config config/webpack.config.js"
@@ -13,7 +14,8 @@
   "dependencies": {
     "babel-core": "^6.22.1",
     "babel-loader": "^6.2.10",
-    "babel-preset-es2015": "^6.22.0",
+    "babel-plugin-transform-define": "^1.2.0",
+    "babel-preset-latest": "^6.24.0",
     "babel-preset-stage-2": "^6.22.0",
     "bootstrap-sass": "^3.3.6",
     "compression-webpack-plugin": "^0.3.2",
@@ -57,12 +59,5 @@
     "karma-sourcemap-loader": "^0.3.7",
     "karma-webpack": "^2.0.2",
     "webpack-dev-server": "^2.3.0"
-  },
-  "nyc": {
-    "exclude": [
-      "spec/javascripts/test_bundle.js",
-      "spec/javascripts/**/*_spec.js",
-      "app/assets/javascripts/droplab/**/*"
-    ]
   }
 }
diff --git a/qa/qa/page/main/entry.rb b/qa/qa/page/main/entry.rb
index fe80deb642987319d1dc5a8b01a6fd8c60063c82..a9810beeb296f19b60bc9bab8bf6ac563c2f190d 100644
--- a/qa/qa/page/main/entry.rb
+++ b/qa/qa/page/main/entry.rb
@@ -5,8 +5,14 @@ module QA
         def initialize
           visit('/')
 
-          # This resolves cold boot problems with login page
-          find('.application', wait: 120)
+          # This resolves cold boot / background tasks problems
+          #
+          start = Time.now
+
+          while Time.now - start < 240
+            break if page.has_css?('.application', wait: 10)
+            refresh
+          end
         end
 
         def sign_in_using_credentials
diff --git a/spec/controllers/admin/application_settings_controller_spec.rb b/spec/controllers/admin/application_settings_controller_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..84a1ce773a1612f740fe12639a4224d2e9e331e3
--- /dev/null
+++ b/spec/controllers/admin/application_settings_controller_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe Admin::ApplicationSettingsController do
+  include StubENV
+
+  let(:admin) { create(:admin) }
+
+  before do
+    sign_in(admin)
+    stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
+  end
+
+  describe 'PATCH #update' do
+    it 'updates the default_project_visibility for string value' do
+      patch :update, application_setting: { default_project_visibility: "20" }
+
+      expect(response).to redirect_to(admin_application_settings_path)
+      expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PUBLIC
+    end
+
+    it 'falls back to default with default_project_visibility setting is omitted' do
+      patch :update, application_setting: {}
+
+      expect(response).to redirect_to(admin_application_settings_path)
+      expect(ApplicationSetting.current.default_project_visibility).to eq Gitlab::VisibilityLevel::PRIVATE
+    end
+  end
+end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 6ceaf96f78f24cecd3685fbd9d92a47e8986639d..57a921e3676800394489cd6b5c819d3ec862eddc 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -87,6 +87,12 @@ describe Projects::IssuesController do
   end
 
   describe 'GET #new' do
+    it 'redirects to signin if not logged in' do
+      get :new, namespace_id: project.namespace, project_id: project
+
+      expect(response).to redirect_to(new_user_session_path)
+    end
+
     context 'internal issue tracker' do
       before do
         sign_in(user)
@@ -121,6 +127,11 @@ describe Projects::IssuesController do
     end
 
     context 'external issue tracker' do
+      before do
+        sign_in(user)
+        project.team << [user, :developer]
+      end
+
       it 'redirects to the external issue tracker' do
         external = double(new_issue_path: 'https://example.com/issues/new')
         allow(project).to receive(:external_issue_tracker).and_return(external)
@@ -141,6 +152,24 @@ describe Projects::IssuesController do
 
     it_behaves_like 'update invalid issuable', Issue
 
+    context 'changing the assignee' do
+      it 'limits the attributes exposed on the assignee' do
+        assignee = create(:user)
+        project.add_developer(assignee)
+
+        put :update,
+          namespace_id: project.namespace.to_param,
+          project_id: project,
+          id: issue.iid,
+          issue: { assignee_id: assignee.id },
+          format: :json
+        body = JSON.parse(response.body)
+
+        expect(body['assignee'].keys)
+          .to match_array(%w(name username avatar_url))
+      end
+    end
+
     context 'when moving issue to another private project' do
       let(:another_project) { create(:empty_project, :private) }
 
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 250d64f70554dd884066e76be081433df91b90f7..c310d830e817ebefa3fc2fdf1bff3f460308731d 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -203,6 +203,24 @@ describe Projects::MergeRequestsController do
   end
 
   describe 'PUT update' do
+    context 'changing the assignee' do
+      it 'limits the attributes exposed on the assignee' do
+        assignee = create(:user)
+        project.add_developer(assignee)
+
+        put :update,
+          namespace_id: project.namespace.to_param,
+          project_id: project,
+          id: merge_request.iid,
+          merge_request: { assignee_id: assignee.id },
+          format: :json
+        body = JSON.parse(response.body)
+
+        expect(body['assignee'].keys)
+          .to match_array(%w(name username avatar_url))
+      end
+    end
+
     context 'there is no source project' do
       let(:project)       { create(:project) }
       let(:fork_project)  { create(:forked_project_with_submodules) }
diff --git a/spec/controllers/projects/variables_controller_spec.rb b/spec/controllers/projects/variables_controller_spec.rb
index e3f3b4fe8eb73d132ad75cfec1fa8725340f8c40..1ecfe48475cbaaa17fc989dea634d818b165dc57 100644
--- a/spec/controllers/projects/variables_controller_spec.rb
+++ b/spec/controllers/projects/variables_controller_spec.rb
@@ -35,7 +35,7 @@ describe Projects::VariablesController do
 
     context 'updating a variable with valid characters' do
       before do
-        variable.gl_project_id = project.id
+        variable.project_id = project.id
         project.variables << variable
       end
 
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
index 3372e5ab685708553720cc96252d9b1b4d232b05..6712dd5d82ef805261e9966526ee9f0b3d882643 100644
--- a/spec/factories/ci/runner_projects.rb
+++ b/spec/factories/ci/runner_projects.rb
@@ -1,6 +1,6 @@
 FactoryGirl.define do
   factory :ci_runner_project, class: Ci::RunnerProject do
     runner_id 1
-    gl_project_id 1
+    project_id 1
   end
 end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index ae0bbbd6aeb8a21893a8a384db43898f858b651b..21487541507fcc17f789763e9e222281019d3d0b 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -4,6 +4,7 @@ FactoryGirl.define do
     author
     association :source_project, :repository, factory: :project
     target_project { source_project }
+    project { target_project }
 
     # $ git log --pretty=oneline feature..master
     # 5937ac0a7beb003549fc5fd26fc247adbce4a52e Add submodule from gitlab.com
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index de42ab81face88065cb77639461bfd66e8847987..03daab12c8f2f1e659b851963012c7efce011f94 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -9,6 +9,13 @@ feature 'Admin updates settings', feature: true do
     visit admin_application_settings_path
   end
 
+  scenario 'Change visibility settings' do
+    choose "application_setting_default_project_visibility_20"
+    click_button 'Save'
+
+    expect(page).to have_content "Application settings saved successfully"
+  end
+
   scenario 'Change application settings' do
     uncheck 'Gravatar enabled'
     fill_in 'Home page URL', with: 'https://about.gitlab.com/'
@@ -26,7 +33,7 @@ feature 'Admin updates settings', feature: true do
     fill_in 'Webhook', with: 'http://localhost'
     fill_in 'Username', with: 'test_user'
     fill_in 'service_push_channel', with: '#test_channel'
-    page.check('Notify only broken builds')
+    page.check('Notify only broken pipelines')
 
     check_all_events
     click_on 'Save'
@@ -50,7 +57,6 @@ feature 'Admin updates settings', feature: true do
     page.check('Note')
     page.check('Issue')
     page.check('Merge request')
-    page.check('Build')
     page.check('Pipeline')
   end
 end
diff --git a/spec/features/atom/dashboard_issues_spec.rb b/spec/features/atom/dashboard_issues_spec.rb
index a7c22615b892a9d85e8aa60c7a0067b520c4c0bd..58b14e09740fab59db25c3ab518625f997e1cbe9 100644
--- a/spec/features/atom/dashboard_issues_spec.rb
+++ b/spec/features/atom/dashboard_issues_spec.rb
@@ -2,7 +2,8 @@ require 'spec_helper'
 
 describe "Dashboard Issues Feed", feature: true  do
   describe "GET /issues" do
-    let!(:user)     { create(:user) }
+    let!(:user)     { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+    let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
     let!(:project1) { create(:project) }
     let!(:project2) { create(:project) }
 
@@ -31,7 +32,7 @@ describe "Dashboard Issues Feed", feature: true  do
       end
 
       context "issue with basic fields" do
-        let!(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'test desc') }
+        let!(:issue2) { create(:issue, author: user, assignee: assignee, project: project2, description: 'test desc') }
 
         it "renders issue fields" do
           visit issues_dashboard_path(:atom, private_token: user.private_token)
@@ -39,8 +40,8 @@ describe "Dashboard Issues Feed", feature: true  do
           entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue2.title}')]")
 
           expect(entry).to be_present
-          expect(entry).to have_selector('author email', text: issue2.author_email)
-          expect(entry).to have_selector('assignee email', text: issue2.author_email)
+          expect(entry).to have_selector('author email', text: issue2.author_public_email)
+          expect(entry).to have_selector('assignee email', text: issue2.assignee_public_email)
           expect(entry).not_to have_selector('labels')
           expect(entry).not_to have_selector('milestone')
           expect(entry).to have_selector('description', text: issue2.description)
@@ -50,7 +51,7 @@ describe "Dashboard Issues Feed", feature: true  do
       context "issue with label and milestone" do
         let!(:milestone1) { create(:milestone, project: project1, title: 'v1') }
         let!(:label1)     { create(:label, project: project1, title: 'label1') }
-        let!(:issue1)     { create(:issue, author: user, assignee: user, project: project1, milestone: milestone1) }
+        let!(:issue1)     { create(:issue, author: user, assignee: assignee, project: project1, milestone: milestone1) }
 
         before do
           issue1.labels << label1
@@ -62,8 +63,8 @@ describe "Dashboard Issues Feed", feature: true  do
           entry = find(:xpath, "//feed/entry[contains(summary/text(),'#{issue1.title}')]")
 
           expect(entry).to be_present
-          expect(entry).to have_selector('author email', text: issue1.author_email)
-          expect(entry).to have_selector('assignee email', text: issue1.author_email)
+          expect(entry).to have_selector('author email', text: issue1.author_public_email)
+          expect(entry).to have_selector('assignee email', text: issue1.assignee_public_email)
           expect(entry).to have_selector('labels label', text: label1.title)
           expect(entry).to have_selector('milestone', text: milestone1.title)
           expect(entry).not_to have_selector('description')
diff --git a/spec/features/atom/issues_spec.rb b/spec/features/atom/issues_spec.rb
index a01a050a0130f372fd99ed9e282c20d17a7c87d1..b3903ec2faf92705f45f12126c13ea1c0779f992 100644
--- a/spec/features/atom/issues_spec.rb
+++ b/spec/features/atom/issues_spec.rb
@@ -2,10 +2,11 @@ require 'spec_helper'
 
 describe 'Issues Feed', feature: true  do
   describe 'GET /issues' do
-    let!(:user)     { create(:user) }
+    let!(:user)     { create(:user, email: 'private1@example.com', public_email: 'public1@example.com') }
+    let!(:assignee) { create(:user, email: 'private2@example.com', public_email: 'public2@example.com') }
     let!(:group)    { create(:group) }
     let!(:project)  { create(:project) }
-    let!(:issue)    { create(:issue, author: user, project: project) }
+    let!(:issue)    { create(:issue, author: user, assignee: assignee, project: project) }
 
     before do
       project.team << [user, :developer]
@@ -20,7 +21,8 @@ describe 'Issues Feed', feature: true  do
         expect(response_headers['Content-Type']).
           to have_content('application/atom+xml')
         expect(body).to have_selector('title', text: "#{project.name} issues")
-        expect(body).to have_selector('author email', text: issue.author_email)
+        expect(body).to have_selector('author email', text: issue.author_public_email)
+        expect(body).to have_selector('assignee email', text: issue.author_public_email)
         expect(body).to have_selector('entry summary', text: issue.title)
       end
     end
@@ -33,7 +35,8 @@ describe 'Issues Feed', feature: true  do
         expect(response_headers['Content-Type']).
           to have_content('application/atom+xml')
         expect(body).to have_selector('title', text: "#{project.name} issues")
-        expect(body).to have_selector('author email', text: issue.author_email)
+        expect(body).to have_selector('author email', text: issue.author_public_email)
+        expect(body).to have_selector('assignee email', text: issue.author_public_email)
         expect(body).to have_selector('entry summary', text: issue.title)
       end
     end
diff --git a/spec/features/groups/group_name_toggle.rb b/spec/features/groups/group_name_toggle_spec.rb
similarity index 95%
rename from spec/features/groups/group_name_toggle.rb
rename to spec/features/groups/group_name_toggle_spec.rb
index ada4ac66e04e51f92cf9bf9d523f55b474fc358b..8528718a2f751cf1aa3e33f6f72595babe0ac18f 100644
--- a/spec/features/groups/group_name_toggle.rb
+++ b/spec/features/groups/group_name_toggle_spec.rb
@@ -1,6 +1,6 @@
 require 'spec_helper'
 
-feature 'Group name toggle', js: true do
+feature 'Group name toggle', feature: true, js: true do
   let(:group) { create(:group) }
   let(:nested_group_1) { create(:group, parent: group) }
   let(:nested_group_2) { create(:group, parent: nested_group_1) }
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 1c8267b15935d1000a35b3fc13f438343e97a38e..a58aedc924e40805f1794975b3a1f6b9cccb7e53 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -6,7 +6,7 @@ describe 'Issues', feature: true do
   include SortingHelper
   include WaitForAjax
 
-  let(:project) { create(:project) }
+  let(:project) { create(:project, :public) }
 
   before do
     login_as :user
@@ -565,6 +565,24 @@ describe 'Issues', feature: true do
   end
 
   describe 'new issue' do
+    context 'by unauthenticated user' do
+      before do
+        logout
+      end
+
+      it 'redirects to signin then back to new issue after signin' do
+        visit namespace_project_issues_path(project.namespace, project)
+
+        click_link 'New issue'
+
+        expect(current_path).to eq new_user_session_path
+
+        login_as :user
+
+        expect(current_path).to eq new_namespace_project_issue_path(project.namespace, project)
+      end
+    end
+
     context 'dropzone upload file', js: true do
       before do
         visit new_namespace_project_issue_path(project.namespace, project)
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index 73c5ef31edc5513d65d40522459efcc258259918..18833ba72668d30a155f27a50ea4f908d338ab76 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -60,9 +60,6 @@ feature 'Merge request created from fork' do
         expect(page).to have_content pipeline.status
         expect(page).to have_content pipeline.id
       end
-
-      expect(page.find('a.btn-remove')[:href])
-        .to include fork_project.path_with_namespace
     end
   end
 
diff --git a/spec/features/merge_requests/reset_filters_spec.rb b/spec/features/merge_requests/reset_filters_spec.rb
index 6fed1568fcf9a2185f54bef780636c396c950f65..14511707af4c6223d94530917666244587dc6dca 100644
--- a/spec/features/merge_requests/reset_filters_spec.rb
+++ b/spec/features/merge_requests/reset_filters_spec.rb
@@ -49,6 +49,26 @@ feature 'Merge requests filter clear button', feature: true, js: true do
     end
   end
 
+  context 'when multiple label filters have been applied' do
+    let!(:label) { create(:label, project: project, name: 'Frontend') }
+    let(:filter_dropdown) { find("#js-dropdown-label .filter-dropdown") }
+
+    before do
+      visit_merge_requests(project)
+      init_label_search
+    end
+
+    it 'filters bug label' do
+      filtered_search.set('~bug')
+
+      filter_dropdown.find('.filter-dropdown-item', text: bug.title).click
+      init_label_search
+
+      expect(filter_dropdown.find('.filter-dropdown-item', text: bug.title)).to be_visible
+      expect(filter_dropdown.find('.filter-dropdown-item', text: label.title)).to be_visible
+    end
+  end
+
   context 'when a text search has been conducted' do
     it 'resets the text search filter' do
       visit_merge_requests(project, search: 'Bug')
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index 2f3c3e45ae63082403ff987d251d23f40c3f89bc..a1f4eb2688b014f2bf17b8d3842f535d6bc8bc80 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -133,7 +133,6 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
 
       it 'changes target_branch in new merge_request' do
         visit new_namespace_project_merge_request_path(another_project.namespace, another_project, new_url_opts)
-        click_button "Compare branches and continue"
 
         fill_in "merge_request_title", with: 'My brand new feature'
         fill_in "merge_request_description", with: "le feature \n/target_branch fix\nFeature description:"
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 20cdfbae24fefb2424cd4b4045d233d1601e0db1..399c1d478c5bf7e6e71905710c0c80e25f430a9f 100644
Binary files a/spec/features/projects/import_export/test_project_export.tar.gz and b/spec/features/projects/import_export/test_project_export.tar.gz differ
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 22bf1bfbdf09e2584a60b3350dbd97076bdff759..162056671e08aa43f47971343e257d440062280b 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -99,15 +99,18 @@ describe 'Pipelines', :feature, :js do
         end
 
         it 'indicates that pipeline can be canceled' do
-          expect(page).to have_link('Cancel')
+          expect(page).to have_selector('.js-pipelines-cancel-button')
           expect(page).to have_selector('.ci-running')
         end
 
         context 'when canceling' do
-          before { click_link('Cancel') }
+          before do
+            find('.js-pipelines-cancel-button').click
+            wait_for_vue_resource
+          end
 
           it 'indicated that pipelines was canceled' do
-            expect(page).not_to have_link('Cancel')
+            expect(page).not_to have_selector('.js-pipelines-cancel-button')
             expect(page).to have_selector('.ci-canceled')
           end
         end
@@ -126,15 +129,18 @@ describe 'Pipelines', :feature, :js do
         end
 
         it 'indicates that pipeline can be retried' do
-          expect(page).to have_link('Retry')
+          expect(page).to have_selector('.js-pipelines-retry-button')
           expect(page).to have_selector('.ci-failed')
         end
 
         context 'when retrying' do
-          before { click_link('Retry') }
+          before do
+            find('.js-pipelines-retry-button').click
+            wait_for_vue_resource
+          end
 
           it 'shows running pipeline that is not retryable' do
-            expect(page).not_to have_link('Retry')
+            expect(page).not_to have_selector('.js-pipelines-retry-button')
             expect(page).to have_selector('.ci-running')
           end
         end
@@ -176,17 +182,17 @@ describe 'Pipelines', :feature, :js do
         it 'has link to the manual action' do
           find('.js-pipeline-dropdown-manual-actions').click
 
-          expect(page).to have_link('manual build')
+          expect(page).to have_button('manual build')
         end
 
         context 'when manual action was played' do
           before do
             find('.js-pipeline-dropdown-manual-actions').click
-            click_link('manual build')
+            click_button('manual build')
           end
 
           it 'enqueues manual action job' do
-            expect(manual.reload).to be_pending
+            expect(page).to have_selector('.js-pipeline-dropdown-manual-actions:disabled')
           end
         end
       end
@@ -203,7 +209,7 @@ describe 'Pipelines', :feature, :js do
           before { visit_project_pipelines }
 
           it 'is cancelable' do
-            expect(page).to have_link('Cancel')
+            expect(page).to have_selector('.js-pipelines-cancel-button')
           end
 
           it 'has pipeline running' do
@@ -211,10 +217,10 @@ describe 'Pipelines', :feature, :js do
           end
 
           context 'when canceling' do
-            before { click_link('Cancel') }
+            before { find('.js-pipelines-cancel-button').trigger('click') }
 
             it 'indicates that pipeline was canceled' do
-              expect(page).not_to have_link('Cancel')
+              expect(page).not_to have_selector('.js-pipelines-cancel-button')
               expect(page).to have_selector('.ci-canceled')
             end
           end
@@ -233,7 +239,7 @@ describe 'Pipelines', :feature, :js do
           end
 
           it 'is not retryable' do
-            expect(page).not_to have_link('Retry')
+            expect(page).not_to have_selector('.js-pipelines-retry-button')
           end
 
           it 'has failed pipeline' do
diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb
index 16541f51d98cb9fc6e54b9ba6189db1929325efb..c0a4a1e4bf5196e14d18a012c211c7007c701722 100644
--- a/spec/features/projects/services/slack_service_spec.rb
+++ b/spec/features/projects/services/slack_service_spec.rb
@@ -7,7 +7,7 @@ feature 'Projects > Slack service > Setup events', feature: true do
 
   background do
     service.fields
-    service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, build_channel: 6, wiki_page_channel: 7)
+    service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, pipeline_channel: 6, wiki_page_channel: 7)
     project.team << [user, :master]
     login_as(user)
   end
@@ -20,7 +20,7 @@ feature 'Projects > Slack service > Setup events', feature: true do
     expect(page.find_field("service_merge_request_channel").value).to have_content '3'
     expect(page.find_field("service_note_channel").value).to have_content '4'
     expect(page.find_field("service_tag_push_channel").value).to have_content '5'
-    expect(page.find_field("service_build_channel").value).to have_content '6'
+    expect(page.find_field("service_pipeline_channel").value).to have_content '6'
     expect(page.find_field("service_wiki_page_channel").value).to have_content '7'
   end
 end
diff --git a/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6825b95c8aa3ddfb153636cb6d00c0701ae620fc
--- /dev/null
+++ b/spec/features/projects/wiki/user_git_access_wiki_page_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe 'Projects > Wiki > User views Git access wiki page', :feature do
+  let(:user) { create(:user) }
+  let(:project) { create(:project, :public) }
+  let(:wiki_page) do
+    WikiPages::CreateService.new(
+      project,
+      user,
+      title: 'home',
+      content: '[some link](other-page)'
+    ).execute
+  end
+
+  before do
+    login_as(user)
+  end
+
+  scenario 'Visit Wiki Page Current Commit' do
+    visit namespace_project_wiki_path(project.namespace, project, wiki_page)
+
+    click_link 'Clone repository'
+    expect(page).to have_text("Clone repository #{project.wiki.path_with_namespace}")
+    expect(page).to have_text(project.wiki.http_url_to_repo(user))
+  end
+end
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index f842d14fa962e66d145f09e3dd73fa4273df3394..aedc0333cb9a2fb29f3e9196363dc959ba8eee46 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -15,15 +15,30 @@ feature 'Projects > Wiki > User updates wiki page', feature: true do
   context 'in the user namespace' do
     let(:project) { create(:project, namespace: user.namespace) }
 
-    scenario 'the home page' do
-      click_link 'Edit'
-
-      fill_in :wiki_content, with: 'My awesome wiki!'
-      click_button 'Save changes'
-
-      expect(page).to have_content('Home')
-      expect(page).to have_content("Last edited by #{user.name}")
-      expect(page).to have_content('My awesome wiki!')
+    context 'the home page' do
+      scenario 'success when the wiki content is not empty' do
+        click_link 'Edit'
+
+        fill_in :wiki_content, with: 'My awesome wiki!'
+        click_button 'Save changes'
+
+        expect(page).to have_content('Home')
+        expect(page).to have_content("Last edited by #{user.name}")
+        expect(page).to have_content('My awesome wiki!')
+      end
+
+      scenario 'failure when the wiki content is empty' do
+        click_link 'Edit'
+
+        fill_in :wiki_content, with: ''
+        click_button 'Save changes'
+
+        expect(page).to have_selector('.wiki-form')
+        expect(page).to have_content('Edit Page')
+        expect(page).to have_content('The form contains the following error:')
+        expect(page).to have_content('Content can\'t be blank')
+        expect(find('textarea#wiki_content').value).to eq ''
+      end
     end
   end
 
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
index 50060a0925d72965dfde16a17631916835370136..21e0e74e0083ec2af186173093eb41a604723b42 100644
--- a/spec/helpers/todos_helper_spec.rb
+++ b/spec/helpers/todos_helper_spec.rb
@@ -1,6 +1,40 @@
 require "spec_helper"
 
 describe TodosHelper do
+  include GitlabRoutingHelper
+
+  describe '#todo_target_path' do
+    let(:project)       { create(:project) }
+    let(:merge_request) { create(:merge_request, target_project: project, source_project: project) }
+    let(:issue)         { create(:issue, project: project) }
+    let(:note)          { create(:note_on_issue, noteable: issue, project: project) }
+
+    let(:mr_todo)           { build(:todo, project: project, target: merge_request) }
+    let(:issue_todo)        { build(:todo, project: project, target: issue) }
+    let(:note_todo)         { build(:todo, project: project, target: issue, note: note) }
+    let(:build_failed_todo) { build(:todo, :build_failed, project: project, target: merge_request) }
+
+    it 'returns correct path to the todo MR' do
+      expect(todo_target_path(mr_todo)).
+        to eq("/#{project.full_path}/merge_requests/#{merge_request.iid}")
+    end
+
+    it 'returns correct path to the todo issue' do
+      expect(todo_target_path(issue_todo)).
+        to eq("/#{project.full_path}/issues/#{issue.iid}")
+    end
+
+    it 'returns correct path to the todo note' do
+      expect(todo_target_path(note_todo)).
+        to eq("/#{project.full_path}/issues/#{issue.iid}#note_#{note.id}")
+    end
+
+    it 'returns correct path to build_todo MR when pipeline failed' do
+      expect(todo_target_path(build_failed_todo)).
+        to eq("/#{project.full_path}/merge_requests/#{merge_request.iid}/pipelines")
+    end
+  end
+
   describe '#todo_projects_options' do
     let(:projects) { create_list(:empty_project, 3) }
     let(:user)     { create(:user) }
diff --git a/spec/javascripts/commit/pipelines/mock_data.js b/spec/javascripts/commit/pipelines/mock_data.js
index 188908d66bdc5212a372275b79c4352e800f426a..82b00b4c1ec614c9a4fe8abeefb82abd3275bba5 100644
--- a/spec/javascripts/commit/pipelines/mock_data.js
+++ b/spec/javascripts/commit/pipelines/mock_data.js
@@ -1,5 +1,4 @@
-/* eslint-disable no-unused-vars */
-const pipeline = {
+export default {
   id: 73,
   user: {
     name: 'Administrator',
@@ -88,5 +87,3 @@ const pipeline = {
   created_at: '2017-01-16T17:13:59.800Z',
   updated_at: '2017-01-25T00:00:17.132Z',
 };
-
-module.exports = pipeline;
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index f09c57978a10d6329496d6709024fc6251147837..75efcc065853a5ae3b909cd3aafb89d535d11411 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -1,11 +1,6 @@
-/* global pipeline, Vue */
-
-require('~/flash');
-require('~/commit/pipelines/pipelines_store');
-require('~/commit/pipelines/pipelines_service');
-require('~/commit/pipelines/pipelines_table');
-require('~/vue_shared/vue_resource_interceptor');
-const pipeline = require('./mock_data');
+import Vue from 'vue';
+import PipelinesTable from '~/commit/pipelines/pipelines_table';
+import pipeline from './mock_data';
 
 describe('Pipelines table in Commits and Merge requests', () => {
   preloadFixtures('static/pipelines_table.html.raw');
@@ -33,7 +28,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
       });
 
       it('should render the empty state', (done) => {
-        const component = new gl.commits.pipelines.PipelinesTableView({
+        const component = new PipelinesTable({
           el: document.querySelector('#commit-pipeline-table-view'),
         });
 
@@ -62,7 +57,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
       });
 
       it('should render a table with the received pipelines', (done) => {
-        const component = new gl.commits.pipelines.PipelinesTableView({
+        const component = new PipelinesTable({
           el: document.querySelector('#commit-pipeline-table-view'),
         });
 
@@ -92,7 +87,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
     });
 
     it('should render empty state', (done) => {
-      const component = new gl.commits.pipelines.PipelinesTableView({
+      const component = new PipelinesTable({
         el: document.querySelector('#commit-pipeline-table-view'),
       });
 
diff --git a/spec/javascripts/commit/pipelines/pipelines_store_spec.js b/spec/javascripts/commit/pipelines/pipelines_store_spec.js
deleted file mode 100644
index 949734199790bd4c331cce8288de99f29d7640db..0000000000000000000000000000000000000000
--- a/spec/javascripts/commit/pipelines/pipelines_store_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-const PipelinesStore = require('~/commit/pipelines/pipelines_store');
-
-describe('Store', () => {
-  let store;
-
-  beforeEach(() => {
-    store = new PipelinesStore();
-  });
-
-  // unregister intervals and event handlers
-  afterEach(() => gl.VueRealtimeListener.reset());
-
-  it('should start with a blank state', () => {
-    expect(store.state.pipelines.length).toBe(0);
-  });
-
-  it('should store an array of pipelines', () => {
-    const pipelines = [
-      {
-        id: '1',
-        name: 'pipeline',
-      },
-      {
-        id: '2',
-        name: 'pipeline_2',
-      },
-    ];
-
-    store.storePipelines(pipelines);
-
-    expect(store.state.pipelines.length).toBe(pipelines.length);
-  });
-});
diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
index ae9c263d1d75c54bc64c0c4ab0e99f905d249eb2..113161c21c60d5995888c8d7c63552af6c68ac26 100644
--- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js
@@ -246,5 +246,17 @@ const FilteredSearchSpecHelper = require('../helpers/filtered_search_spec_helper
         expect(gl.FilteredSearchVisualTokens.unselectTokens).toHaveBeenCalled();
       });
     });
+
+    describe('toggleInputContainerFocus', () => {
+      it('toggles on focus', () => {
+        input.focus();
+        expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(true);
+      });
+
+      it('toggles on blur', () => {
+        input.blur();
+        expect(document.querySelector('.filtered-search-input-container').classList.contains('focus')).toEqual(false);
+      });
+    });
   });
 })();
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 8d25500b9fd4dc5fb704827e30754b747cf535ba..aabc8bea12f48f7001f7dec9143edb443b342e80 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -136,6 +136,21 @@ describe('Issue', function() {
       expectErrorMessage();
       expect($('.issue_counter')).toHaveText(1);
     });
+
+    it('updates counter', () => {
+      spyOn(jQuery, 'ajax').and.callFake(function(req) {
+        expectPendingRequest(req, $btnClose);
+        req.success({
+          id: 34
+        });
+      });
+
+      expect($('.issue_counter')).toHaveText(1);
+      $('.issue_counter').text('1,001');
+      expect($('.issue_counter').text()).toEqual('1,001');
+      $btnClose.trigger('click');
+      expect($('.issue_counter').text()).toEqual('1,000');
+    });
   });
 
   describe('reopen issue', function() {
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index c12b44cea89168f1e8f74fadc18f199f4463337e..5cdb6473edae150c3224bb3467339d698dddee6c 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -32,10 +32,11 @@ testsContext.keys().forEach(function (path) {
   }
 });
 
-// workaround: include all source files to find files with 0% coverage
-// see also https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
-describe('Uncovered files', function () {
-  // the following files throw errors because of undefined variables
+// if we're generating coverage reports, make sure to include all files so
+// that we can catch files with 0% coverage
+// see: https://github.com/deepsweet/istanbul-instrumenter-loader/issues/15
+if (process.env.BABEL_ENV === 'coverage') {
+  // exempt these files from the coverage report
   const troubleMakers = [
     './blob_edit/blob_edit_bundle.js',
     './cycle_analytics/components/stage_plan_component.js',
@@ -48,21 +49,23 @@ describe('Uncovered files', function () {
     './network/branch_graph.js',
   ];
 
-  const sourceFiles = require.context('~', true, /^\.\/(?!application\.js).*\.js$/);
-  sourceFiles.keys().forEach(function (path) {
-    // ignore if there is a matching spec file
-    if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) {
-      return;
-    }
+  describe('Uncovered files', function () {
+    const sourceFiles = require.context('~', true, /\.js$/);
+    sourceFiles.keys().forEach(function (path) {
+      // ignore if there is a matching spec file
+      if (testsContext.keys().indexOf(`${path.replace(/\.js$/, '')}_spec`) > -1) {
+        return;
+      }
 
-    it(`includes '${path}'`, function () {
-      try {
-        sourceFiles(path);
-      } catch (err) {
-        if (troubleMakers.indexOf(path) === -1) {
-          expect(err).toBeNull();
+      it(`includes '${path}'`, function () {
+        try {
+          sourceFiles(path);
+        } catch (err) {
+          if (troubleMakers.indexOf(path) === -1) {
+            expect(err).toBeNull();
+          }
         }
-      }
+      });
     });
   });
-});
+}
diff --git a/spec/javascripts/vue_pipelines_index/async_button_spec.js b/spec/javascripts/vue_pipelines_index/async_button_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..bc8e504c4137399f5868708613d21629e3257bd3
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/async_button_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import asyncButtonComp from '~/vue_pipelines_index/components/async_button';
+
+describe('Pipelines Async Button', () => {
+  let component;
+  let spy;
+  let AsyncButtonComponent;
+
+  beforeEach(() => {
+    AsyncButtonComponent = Vue.extend(asyncButtonComp);
+
+    spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+    component = new AsyncButtonComponent({
+      propsData: {
+        endpoint: '/foo',
+        title: 'Foo',
+        icon: 'fa fa-foo',
+        cssClass: 'bar',
+        service: {
+          postAction: spy,
+        },
+      },
+    }).$mount();
+  });
+
+  it('should render a button', () => {
+    expect(component.$el.tagName).toEqual('BUTTON');
+  });
+
+  it('should render the provided icon', () => {
+    expect(component.$el.querySelector('i').getAttribute('class')).toContain('fa fa-foo');
+  });
+
+  it('should render the provided title', () => {
+    expect(component.$el.getAttribute('title')).toContain('Foo');
+    expect(component.$el.getAttribute('aria-label')).toContain('Foo');
+  });
+
+  it('should render the provided cssClass', () => {
+    expect(component.$el.getAttribute('class')).toContain('bar');
+  });
+
+  it('should call the service when it is clicked with the provided endpoint', () => {
+    component.$el.click();
+    expect(spy).toHaveBeenCalledWith('/foo');
+  });
+
+  it('should hide loading if request fails', () => {
+    spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
+
+    component = new AsyncButtonComponent({
+      propsData: {
+        endpoint: '/foo',
+        title: 'Foo',
+        icon: 'fa fa-foo',
+        cssClass: 'bar',
+        dataAttributes: {
+          'data-foo': 'foo',
+        },
+        service: {
+          postAction: spy,
+        },
+      },
+    }).$mount();
+
+    component.$el.click();
+    expect(component.$el.querySelector('.fa-spinner')).toBe(null);
+  });
+
+  describe('With confirm dialog', () => {
+    it('should call the service when confimation is positive', () => {
+      spyOn(window, 'confirm').and.returnValue(true);
+      spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+      component = new AsyncButtonComponent({
+        propsData: {
+          endpoint: '/foo',
+          title: 'Foo',
+          icon: 'fa fa-foo',
+          cssClass: 'bar',
+          service: {
+            postAction: spy,
+          },
+          confirmActionMessage: 'bar',
+        },
+      }).$mount();
+
+      component.$el.click();
+      expect(spy).toHaveBeenCalledWith('/foo');
+    });
+  });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..96a2a37b5f78aec05e4f34749022159397d61a0b
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipeline_url_spec.js
@@ -0,0 +1,100 @@
+import Vue from 'vue';
+import pipelineUrlComp from '~/vue_pipelines_index/components/pipeline_url';
+
+describe('Pipeline Url Component', () => {
+  let PipelineUrlComponent;
+
+  beforeEach(() => {
+    PipelineUrlComponent = Vue.extend(pipelineUrlComp);
+  });
+
+  it('should render a table cell', () => {
+    const component = new PipelineUrlComponent({
+      propsData: {
+        pipeline: {
+          id: 1,
+          path: 'foo',
+          flags: {},
+        },
+      },
+    }).$mount();
+
+    expect(component.$el.tagName).toEqual('TD');
+  });
+
+  it('should render a link the provided path and id', () => {
+    const component = new PipelineUrlComponent({
+      propsData: {
+        pipeline: {
+          id: 1,
+          path: 'foo',
+          flags: {},
+        },
+      },
+    }).$mount();
+
+    expect(component.$el.querySelector('.js-pipeline-url-link').getAttribute('href')).toEqual('foo');
+    expect(component.$el.querySelector('.js-pipeline-url-link span').textContent).toEqual('#1');
+  });
+
+  it('should render user information when a user is provided', () => {
+    const mockData = {
+      pipeline: {
+        id: 1,
+        path: 'foo',
+        flags: {},
+        user: {
+          web_url: '/',
+          name: 'foo',
+          avatar_url: '/',
+        },
+      },
+    };
+
+    const component = new PipelineUrlComponent({
+      propsData: mockData,
+    }).$mount();
+
+    const image = component.$el.querySelector('.js-pipeline-url-user img');
+
+    expect(
+      component.$el.querySelector('.js-pipeline-url-user').getAttribute('href'),
+    ).toEqual(mockData.pipeline.user.web_url);
+    expect(image.getAttribute('title')).toEqual(mockData.pipeline.user.name);
+    expect(image.getAttribute('src')).toEqual(mockData.pipeline.user.avatar_url);
+  });
+
+  it('should render "API" when no user is provided', () => {
+    const component = new PipelineUrlComponent({
+      propsData: {
+        pipeline: {
+          id: 1,
+          path: 'foo',
+          flags: {},
+        },
+      },
+    }).$mount();
+
+    expect(component.$el.querySelector('.js-pipeline-url-api').textContent).toContain('API');
+  });
+
+  it('should render latest, yaml invalid and stuck flags when provided', () => {
+    const component = new PipelineUrlComponent({
+      propsData: {
+        pipeline: {
+          id: 1,
+          path: 'foo',
+          flags: {
+            latest: true,
+            yaml_errors: true,
+            stuck: true,
+          },
+        },
+      },
+    }).$mount();
+
+    expect(component.$el.querySelector('.js-pipeline-url-lastest').textContent).toContain('latest');
+    expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid');
+    expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck');
+  });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..dba998c7688d356357aa57d627abc9e9ebc7f62e
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_actions_spec.js
@@ -0,0 +1,62 @@
+import Vue from 'vue';
+import pipelinesActionsComp from '~/vue_pipelines_index/components/pipelines_actions';
+
+describe('Pipelines Actions dropdown', () => {
+  let component;
+  let spy;
+  let actions;
+  let ActionsComponent;
+
+  beforeEach(() => {
+    ActionsComponent = Vue.extend(pipelinesActionsComp);
+
+    actions = [
+      {
+        name: 'stop_review',
+        path: '/root/review-app/builds/1893/play',
+      },
+    ];
+
+    spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
+
+    component = new ActionsComponent({
+      propsData: {
+        actions,
+        service: {
+          postAction: spy,
+        },
+      },
+    }).$mount();
+  });
+
+  it('should render a dropdown with the provided actions', () => {
+    expect(
+      component.$el.querySelectorAll('.dropdown-menu li').length,
+    ).toEqual(actions.length);
+  });
+
+  it('should call the service when an action is clicked', () => {
+    component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
+    component.$el.querySelector('.js-pipeline-action-link').click();
+
+    expect(spy).toHaveBeenCalledWith(actions[0].path);
+  });
+
+  it('should hide loading if request fails', () => {
+    spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
+
+    component = new ActionsComponent({
+      propsData: {
+        actions,
+        service: {
+          postAction: spy,
+        },
+      },
+    }).$mount();
+
+    component.$el.querySelector('.js-pipeline-dropdown-manual-actions').click();
+    component.$el.querySelector('.js-pipeline-action-link').click();
+
+    expect(component.$el.querySelector('.fa-spinner')).toEqual(null);
+  });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f7f49649c1c6b52525093416f6c94a713e50a687
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_artifacts_spec.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import artifactsComp from '~/vue_pipelines_index/components/pipelines_artifacts';
+
+describe('Pipelines Artifacts dropdown', () => {
+  let component;
+  let artifacts;
+
+  beforeEach(() => {
+    const ArtifactsComponent = Vue.extend(artifactsComp);
+
+    artifacts = [
+      {
+        name: 'artifact',
+        path: '/download/path',
+      },
+    ];
+
+    component = new ArtifactsComponent({
+      propsData: {
+        artifacts,
+      },
+    }).$mount();
+  });
+
+  it('should render a dropdown with the provided artifacts', () => {
+    expect(
+      component.$el.querySelectorAll('.dropdown-menu li').length,
+    ).toEqual(artifacts.length);
+  });
+
+  it('should render a link with the provided path', () => {
+    expect(
+      component.$el.querySelector('.dropdown-menu li a').getAttribute('href'),
+    ).toEqual(artifacts[0].path);
+
+    expect(
+      component.$el.querySelector('.dropdown-menu li a span').textContent,
+    ).toContain(artifacts[0].name);
+  });
+});
diff --git a/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..5c0934404bb5deaec9f91bfcc81c08c81d2b42d9
--- /dev/null
+++ b/spec/javascripts/vue_pipelines_index/pipelines_store_spec.js
@@ -0,0 +1,72 @@
+import PipelineStore from '~/vue_pipelines_index/stores/pipelines_store';
+
+describe('Pipelines Store', () => {
+  let store;
+
+  beforeEach(() => {
+    store = new PipelineStore();
+  });
+
+  it('should be initialized with an empty state', () => {
+    expect(store.state.pipelines).toEqual([]);
+    expect(store.state.count).toEqual({});
+    expect(store.state.pageInfo).toEqual({});
+  });
+
+  describe('storePipelines', () => {
+    it('should use the default parameter if none is provided', () => {
+      store.storePipelines();
+      expect(store.state.pipelines).toEqual([]);
+    });
+
+    it('should store the provided array', () => {
+      const array = [{ id: 1, status: 'running' }, { id: 2, status: 'success' }];
+      store.storePipelines(array);
+      expect(store.state.pipelines).toEqual(array);
+    });
+  });
+
+  describe('storeCount', () => {
+    it('should use the default parameter if none is provided', () => {
+      store.storeCount();
+      expect(store.state.count).toEqual({});
+    });
+
+    it('should store the provided count', () => {
+      const count = { all: 20, finished: 10 };
+      store.storeCount(count);
+
+      expect(store.state.count).toEqual(count);
+    });
+  });
+
+  describe('storePagination', () => {
+    it('should use the default parameter if none is provided', () => {
+      store.storePagination();
+      expect(store.state.pageInfo).toEqual({});
+    });
+
+    it('should store pagination information normalized and parsed', () => {
+      const pagination = {
+        'X-nExt-pAge': '2',
+        'X-page': '1',
+        'X-Per-Page': '1',
+        'X-Prev-Page': '2',
+        'X-TOTAL': '37',
+        'X-Total-Pages': '2',
+      };
+
+      const expectedResult = {
+        perPage: 1,
+        page: 1,
+        total: 37,
+        totalPages: 2,
+        nextPage: 2,
+        previousPage: 2,
+      };
+
+      store.storePagination(pagination);
+      expect(store.state.pageInfo).toEqual(expectedResult);
+    });
+  });
+});
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index 15ab10b9b6952bdb2874443f495cd7e52da1e468..df547299d75cbd8bc71a55bf5de55dcedff5b928 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -1,13 +1,17 @@
-require('~/vue_shared/components/commit');
+import Vue from 'vue';
+import commitComp from '~/vue_shared/components/commit';
 
 describe('Commit component', () => {
   let props;
   let component;
+  let CommitComponent;
+
+  beforeEach(() => {
+    CommitComponent = Vue.extend(commitComp);
+  });
 
   it('should render a code-fork icon if it does not represent a tag', () => {
-    setFixtures('<div class="test-commit-container"></div>');
-    component = new window.gl.CommitComponent({
-      el: document.querySelector('.test-commit-container'),
+    component = new CommitComponent({
       propsData: {
         tag: false,
         commitRef: {
@@ -23,15 +27,13 @@ describe('Commit component', () => {
           username: 'jschatz1',
         },
       },
-    });
+    }).$mount();
 
     expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork');
   });
 
   describe('Given all the props', () => {
     beforeEach(() => {
-      setFixtures('<div class="test-commit-container"></div>');
-
       props = {
         tag: true,
         commitRef: {
@@ -49,10 +51,9 @@ describe('Commit component', () => {
         commitIconSvg: '<svg></svg>',
       };
 
-      component = new window.gl.CommitComponent({
-        el: document.querySelector('.test-commit-container'),
+      component = new CommitComponent({
         propsData: props,
-      });
+      }).$mount();
     });
 
     it('should render a tag icon if it represents a tag', () => {
@@ -105,7 +106,6 @@ describe('Commit component', () => {
 
   describe('When commit title is not provided', () => {
     it('should render default message', () => {
-      setFixtures('<div class="test-commit-container"></div>');
       props = {
         tag: false,
         commitRef: {
@@ -118,10 +118,9 @@ describe('Commit component', () => {
         author: {},
       };
 
-      component = new window.gl.CommitComponent({
-        el: document.querySelector('.test-commit-container'),
+      component = new CommitComponent({
         propsData: props,
-      });
+      }).$mount();
 
       expect(
         component.$el.querySelector('.commit-title span').textContent,
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
index 412abfd5e4174622a98d55794df32dcd4a669dd7..699625cdbb7e1ac3bdc1a14efff7988d136890db 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
+++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
@@ -1,20 +1,20 @@
-require('~/vue_shared/components/pipelines_table_row');
-const pipeline = require('../../commit/pipelines/mock_data');
+import Vue from 'vue';
+import tableRowComp from '~/vue_shared/components/pipelines_table_row';
+import pipeline from '../../commit/pipelines/mock_data';
 
 describe('Pipelines Table Row', () => {
   let component;
-  preloadFixtures('static/environments/element.html.raw');
 
   beforeEach(() => {
-    loadFixtures('static/environments/element.html.raw');
+    const PipelinesTableRowComponent = Vue.extend(tableRowComp);
 
-    component = new gl.pipelines.PipelinesTableRowComponent({
+    component = new PipelinesTableRowComponent({
       el: document.querySelector('.test-dom-element'),
       propsData: {
         pipeline,
-        svgs: {},
+        service: {},
       },
-    });
+    }).$mount();
   });
 
   it('should render a table row', () => {
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
index 54d81e2ea7d63e9427c19a3b486f28f3d44778f5..b0b1df5a753d90116df9bec9d0a6440153de9bce 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_spec.js
+++ b/spec/javascripts/vue_shared/components/pipelines_table_spec.js
@@ -1,24 +1,24 @@
-require('~/vue_shared/components/pipelines_table');
-require('~/lib/utils/datetime_utility');
-const pipeline = require('../../commit/pipelines/mock_data');
+import Vue from 'vue';
+import pipelinesTableComp from '~/vue_shared/components/pipelines_table';
+import '~/lib/utils/datetime_utility';
+import pipeline from '../../commit/pipelines/mock_data';
 
 describe('Pipelines Table', () => {
-  preloadFixtures('static/environments/element.html.raw');
+  let PipelinesTableComponent;
 
   beforeEach(() => {
-    loadFixtures('static/environments/element.html.raw');
+    PipelinesTableComponent = Vue.extend(pipelinesTableComp);
   });
 
   describe('table', () => {
     let component;
     beforeEach(() => {
-      component = new gl.pipelines.PipelinesTableComponent({
-        el: document.querySelector('.test-dom-element'),
+      component = new PipelinesTableComponent({
         propsData: {
           pipelines: [],
-          svgs: {},
+          service: {},
         },
-      });
+      }).$mount();
     });
 
     it('should render a table', () => {
@@ -37,26 +37,25 @@ describe('Pipelines Table', () => {
 
   describe('without data', () => {
     it('should render an empty table', () => {
-      const component = new gl.pipelines.PipelinesTableComponent({
-        el: document.querySelector('.test-dom-element'),
+      const component = new PipelinesTableComponent({
         propsData: {
           pipelines: [],
-          svgs: {},
+          service: {},
         },
-      });
+      }).$mount();
       expect(component.$el.querySelectorAll('tbody tr').length).toEqual(0);
     });
   });
 
   describe('with data', () => {
     it('should render rows', () => {
-      const component = new gl.pipelines.PipelinesTableComponent({
+      const component = new PipelinesTableComponent({
         el: document.querySelector('.test-dom-element'),
         propsData: {
           pipelines: [pipeline],
-          svgs: {},
+          service: {},
         },
-      });
+      }).$mount();
 
       expect(component.$el.querySelectorAll('tbody tr').length).toEqual(1);
     });
diff --git a/spec/javascripts/vue_shared/components/table_pagination_spec.js b/spec/javascripts/vue_shared/components/table_pagination_spec.js
index 9cb067921a714b37c39093803afe9ad087c46a1f..a5c3870b3acaa10cb2081c5b47067e109d4db864 100644
--- a/spec/javascripts/vue_shared/components/table_pagination_spec.js
+++ b/spec/javascripts/vue_shared/components/table_pagination_spec.js
@@ -1,8 +1,10 @@
-require('~/lib/utils/common_utils');
-require('~/vue_shared/components/table_pagination');
+import Vue from 'vue';
+import paginationComp from '~/vue_shared/components/table_pagination';
+import '~/lib/utils/common_utils';
 
 describe('Pagination component', () => {
   let component;
+  let PaginationComponent;
 
   const changeChanges = {
     one: '',
@@ -12,11 +14,12 @@ describe('Pagination component', () => {
     changeChanges.one = one;
   };
 
-  it('should render and start at page 1', () => {
-    setFixtures('<div class="test-pagination-container"></div>');
+  beforeEach(() => {
+    PaginationComponent = Vue.extend(paginationComp);
+  });
 
-    component = new window.gl.VueGlPagination({
-      el: document.querySelector('.test-pagination-container'),
+  it('should render and start at page 1', () => {
+    component = new PaginationComponent({
       propsData: {
         pageInfo: {
           totalPages: 10,
@@ -25,7 +28,7 @@ describe('Pagination component', () => {
         },
         change,
       },
-    });
+    }).$mount();
 
     expect(component.$el.classList).toContain('gl-pagination');
 
@@ -35,10 +38,7 @@ describe('Pagination component', () => {
   });
 
   it('should go to the previous page', () => {
-    setFixtures('<div class="test-pagination-container"></div>');
-
-    component = new window.gl.VueGlPagination({
-      el: document.querySelector('.test-pagination-container'),
+    component = new PaginationComponent({
       propsData: {
         pageInfo: {
           totalPages: 10,
@@ -47,7 +47,7 @@ describe('Pagination component', () => {
         },
         change,
       },
-    });
+    }).$mount();
 
     component.changePage({ target: { innerText: 'Prev' } });
 
@@ -55,10 +55,7 @@ describe('Pagination component', () => {
   });
 
   it('should go to the next page', () => {
-    setFixtures('<div class="test-pagination-container"></div>');
-
-    component = new window.gl.VueGlPagination({
-      el: document.querySelector('.test-pagination-container'),
+    component = new PaginationComponent({
       propsData: {
         pageInfo: {
           totalPages: 10,
@@ -67,7 +64,7 @@ describe('Pagination component', () => {
         },
         change,
       },
-    });
+    }).$mount();
 
     component.changePage({ target: { innerText: 'Next' } });
 
@@ -75,10 +72,7 @@ describe('Pagination component', () => {
   });
 
   it('should go to the last page', () => {
-    setFixtures('<div class="test-pagination-container"></div>');
-
-    component = new window.gl.VueGlPagination({
-      el: document.querySelector('.test-pagination-container'),
+    component = new PaginationComponent({
       propsData: {
         pageInfo: {
           totalPages: 10,
@@ -87,7 +81,7 @@ describe('Pagination component', () => {
         },
         change,
       },
-    });
+    }).$mount();
 
     component.changePage({ target: { innerText: 'Last >>' } });
 
@@ -95,10 +89,7 @@ describe('Pagination component', () => {
   });
 
   it('should go to the first page', () => {
-    setFixtures('<div class="test-pagination-container"></div>');
-
-    component = new window.gl.VueGlPagination({
-      el: document.querySelector('.test-pagination-container'),
+    component = new PaginationComponent({
       propsData: {
         pageInfo: {
           totalPages: 10,
@@ -107,7 +98,7 @@ describe('Pagination component', () => {
         },
         change,
       },
-    });
+    }).$mount();
 
     component.changePage({ target: { innerText: '<< First' } });
 
@@ -115,10 +106,7 @@ describe('Pagination component', () => {
   });
 
   it('should do nothing', () => {
-    setFixtures('<div class="test-pagination-container"></div>');
-
-    component = new window.gl.VueGlPagination({
-      el: document.querySelector('.test-pagination-container'),
+    component = new PaginationComponent({
       propsData: {
         pageInfo: {
           totalPages: 10,
@@ -127,7 +115,7 @@ describe('Pagination component', () => {
         },
         change,
       },
-    });
+    }).$mount();
 
     component.changePage({ target: { innerText: '...' } });
 
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index edd01d032c86276e0a633069959a98a2d66c1f78..4ce4e6e10346a976c5b5323a961ab4afdc76d323 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -1,10 +1,16 @@
 require 'spec_helper'
 
-class MigrationTest
-  include Gitlab::Database
-end
-
 describe Gitlab::Database, lib: true do
+  before do
+    stub_const('MigrationTest', Class.new { include Gitlab::Database })
+  end
+
+  describe '.config' do
+    it 'returns a Hash' do
+      expect(described_class.config).to be_an_instance_of(Hash)
+    end
+  end
+
   describe '.adapter_name' do
     it 'returns the name of the adapter' do
       expect(described_class.adapter_name).to be_an_instance_of(String)
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 4c55532d165f7461c2a5648a6d02af2b08b69687..992126ef153b9ecc6fe24f4f12bb530d044da244 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -109,6 +109,43 @@ EOT
         end
       end
     end
+
+    context 'using a Gitaly::CommitDiffResponse' do
+      let(:diff) do
+        described_class.new(
+          Gitaly::CommitDiffResponse.new(
+            to_path: ".gitmodules",
+            from_path: ".gitmodules",
+            old_mode: 0100644,
+            new_mode: 0100644,
+            from_id: '357406f3075a57708d0163752905cc1576fceacc',
+            to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
+            raw_chunks: raw_chunks,
+          )
+        )
+      end
+
+      context 'with a small diff' do
+        let(:raw_chunks) { [@raw_diff_hash[:diff]] }
+
+        it 'initializes the diff' do
+          expect(diff.to_hash).to eq(@raw_diff_hash)
+        end
+
+        it 'does not prune the diff' do
+          expect(diff).not_to be_too_large
+        end
+      end
+
+      context 'using a diff that is too large' do
+        let(:raw_chunks) { ['a' * 204800] }
+
+        it 'prunes the diff' do
+          expect(diff.diff).to be_empty
+          expect(diff).to be_too_large
+        end
+      end
+    end
   end
 
   describe 'straight diffs' do
diff --git a/spec/lib/gitlab/gitaly_client/commit_spec.rb b/spec/lib/gitlab/gitaly_client/commit_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4684b1d1ac048816fb97ccf7a6f5ab653a29d90f
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/commit_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::Commit do
+  describe '.diff_from_parent' do
+    let(:diff_stub) { double('Gitaly::Diff::Stub') }
+    let(:project) { create(:project, :repository) }
+    let(:repository_message) { Gitaly::Repository.new(path: project.repository.path) }
+    let(:commit) { project.commit('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
+
+    before do
+      allow(Gitaly::Diff::Stub).to receive(:new).and_return(diff_stub)
+      allow(diff_stub).to receive(:commit_diff).and_return([])
+    end
+
+    context 'when a commit has a parent' do
+      it 'sends an RPC request with the parent ID as left commit' do
+        request = Gitaly::CommitDiffRequest.new(
+          repository: repository_message,
+          left_commit_id: 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660',
+          right_commit_id: commit.id,
+        )
+
+        expect(diff_stub).to receive(:commit_diff).with(request)
+
+        described_class.diff_from_parent(commit)
+      end
+    end
+
+    context 'when a commit does not have a parent' do
+      it 'sends an RPC request with empty tree ref as left commit' do
+        initial_commit = project.commit('1a0b36b3cdad1d2ee32457c102a8c0b7056fa863')
+        request        = Gitaly::CommitDiffRequest.new(
+          repository: repository_message,
+          left_commit_id: '4b825dc642cb6eb9a060e54bf8d69288fbee4904',
+          right_commit_id: initial_commit.id,
+        )
+
+        expect(diff_stub).to receive(:commit_diff).with(request)
+
+        described_class.diff_from_parent(initial_commit)
+      end
+    end
+
+    it 'returns a Gitlab::Git::DiffCollection' do
+      ret = described_class.diff_from_parent(commit)
+
+      expect(ret).to be_kind_of(Gitlab::Git::DiffCollection)
+    end
+
+    it 'passes options to Gitlab::Git::DiffCollection' do
+      options = { max_files: 31, max_lines: 13 }
+
+      expect(Gitlab::Git::DiffCollection).to receive(:new).with([], options)
+
+      described_class.diff_from_parent(commit, options)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index e47956a365f67695b7dc908de2985ac4d684d344..ddeb71730e7fa665d5f8f921f8e6d4aa48acab77 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -130,7 +130,6 @@ project:
 - campfire_service
 - drone_ci_service
 - emails_on_push_service
-- builds_email_service
 - pipelines_email_service
 - mattermost_slash_commands_service
 - slack_slash_commands_service
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index c3d5c451a3c2a06e28c3cbdc1de29cf05fdf9611..d9b674268180ed1052e316cc87e38c2d8053fa22 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -6507,7 +6507,6 @@
       "tag": null,
       "yaml_errors": null,
       "committed_at": null,
-      "gl_project_id": 5,
       "status": "failed",
       "started_at": null,
       "finished_at": null,
@@ -6565,7 +6564,6 @@
           "artifacts_file": {
             "url": null
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": null
           },
@@ -6603,7 +6601,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz"
           },
@@ -6624,7 +6621,6 @@
       "tag": null,
       "yaml_errors": null,
       "committed_at": null,
-      "gl_project_id": 5,
       "status": "failed",
       "started_at": null,
       "finished_at": null,
@@ -6659,7 +6655,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz"
           },
@@ -6695,7 +6690,6 @@
           "artifacts_file": {
             "url": null
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": null
           },
@@ -6716,7 +6710,6 @@
       "tag": null,
       "yaml_errors": null,
       "committed_at": null,
-      "gl_project_id": 5,
       "status": "failed",
       "started_at": null,
       "finished_at": null,
@@ -6751,7 +6744,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz"
           },
@@ -6787,7 +6779,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz"
           },
@@ -6808,7 +6799,6 @@
       "tag": null,
       "yaml_errors": null,
       "committed_at": null,
-      "gl_project_id": 5,
       "status": "failed",
       "started_at": null,
       "finished_at": null,
@@ -6843,7 +6833,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz"
           },
@@ -6879,7 +6868,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz"
           },
@@ -6900,7 +6888,6 @@
       "tag": null,
       "yaml_errors": null,
       "committed_at": null,
-      "gl_project_id": 5,
       "status": "failed",
       "started_at": null,
       "finished_at": null,
@@ -6935,7 +6922,6 @@
           "artifacts_file": {
             "url": null
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": null
           },
@@ -6971,7 +6957,6 @@
           "artifacts_file": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip"
           },
-          "gl_project_id": 5,
           "artifacts_metadata": {
             "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz"
           },
@@ -6985,11 +6970,10 @@
     {
       "id": 123,
       "token": "cdbfasdf44a5958c83654733449e585",
-      "project_id": null,
+      "project_id": 5,
       "deleted_at": null,
       "created_at": "2017-01-16T15:25:28.637Z",
-      "updated_at": "2017-01-16T15:25:28.637Z",
-      "gl_project_id": 123
+      "updated_at": "2017-01-16T15:25:28.637Z"
     }
   ],
   "deploy_keys": [
@@ -7047,7 +7031,7 @@
       "updated_at": "2016-06-14T15:01:51.303Z",
       "active": false,
       "properties": {
-        "notify_only_broken_builds": true
+        "notify_only_broken_pipelines": true
       },
       "template": false,
       "push_events": true,
@@ -7055,7 +7039,7 @@
       "merge_requests_events": true,
       "tag_push_events": true,
       "note_events": true,
-      "build_events": true,
+      "pipeline_events": true,
       "category": "common",
       "default": false,
       "wiki_page_events": true
@@ -7174,7 +7158,7 @@
       "updated_at": "2016-06-14T15:01:51.219Z",
       "active": false,
       "properties": {
-        "notify_only_broken_builds": true
+        "notify_only_broken_pipelines": true
       },
       "template": false,
       "push_events": true,
@@ -7182,7 +7166,7 @@
       "merge_requests_events": true,
       "tag_push_events": true,
       "note_events": true,
-      "build_events": true,
+      "pipeline_events": true,
       "category": "common",
       "default": false,
       "wiki_page_events": true
@@ -7334,27 +7318,6 @@
       "default": false,
       "wiki_page_events": true
     },
-    {
-      "id": 85,
-      "title": "Builds emails",
-      "project_id": 5,
-      "created_at": "2016-06-14T15:01:51.090Z",
-      "updated_at": "2016-06-14T15:01:51.090Z",
-      "active": false,
-      "properties": {
-        "notify_only_broken_builds": true
-      },
-      "template": false,
-      "push_events": true,
-      "issues_events": true,
-      "merge_requests_events": true,
-      "tag_push_events": true,
-      "note_events": true,
-      "build_events": true,
-      "category": "common",
-      "default": false,
-      "wiki_page_events": true
-    },
     {
       "id": 84,
       "title": "Buildkite",
@@ -7503,4 +7466,4 @@
     "updated_at": "2016-09-23T11:58:28.000Z",
     "wiki_access_level": 20
   }
-}
\ No newline at end of file
+}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index f4a21c24fa1c1ecb245e5629baaf0311004e0ebd..c36f12dbd82237d94aa5687a9f236f62b32011cf 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -129,6 +129,25 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
           expect(Ci::Build.where(token: 'abcd')).to be_empty
         end
       end
+
+      context 'has restored the correct number of records' do
+        it 'has the correct number of merge requests' do
+          expect(@project.merge_requests.size).to eq(9)
+        end
+
+        it 'has the correct number of triggers' do
+          expect(@project.triggers.size).to eq(1)
+        end
+
+        it 'has the correct number of pipelines and statuses' do
+          expect(@project.pipelines.size).to eq(5)
+
+          @project.pipelines.zip([2, 2, 2, 2, 2])
+            .each do |(pipeline, expected_status_size)|
+              expect(pipeline.statuses.size).to eq(expected_status_size)
+            end
+        end
+      end
     end
   end
 
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index c718e7924612e0d7d2e28bbaf9dcc644806110bb..1ad16a9b57dd2fb5f68b869835b3eb25a3e703a4 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -15,6 +15,7 @@ Issue:
 - updated_by_id
 - confidential
 - deleted_at
+- closed_at
 - due_date
 - moved_to_id
 - lock_version
@@ -176,7 +177,6 @@ Ci::Pipeline:
 - tag
 - yaml_errors
 - committed_at
-- gl_project_id
 - status
 - started_at
 - finished_at
@@ -211,7 +211,6 @@ CommitStatus:
 - target_url
 - description
 - artifacts_file
-- gl_project_id
 - artifacts_metadata
 - erased_by_id
 - erased_at
@@ -232,7 +231,6 @@ Ci::Variable:
 - encrypted_value
 - encrypted_value_salt
 - encrypted_value_iv
-- gl_project_id
 Ci::Trigger:
 - id
 - token
@@ -240,7 +238,6 @@ Ci::Trigger:
 - deleted_at
 - created_at
 - updated_at
-- gl_project_id
 - owner_id
 - description
 DeployKey:
diff --git a/spec/lib/gitlab/url_blocker_spec.rb b/spec/lib/gitlab/url_blocker_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a504d299307d74d4173b488d99f64fb6da0057d9
--- /dev/null
+++ b/spec/lib/gitlab/url_blocker_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Gitlab::UrlBlocker, lib: true do
+  describe '#blocked_url?' do
+    it 'allows imports from configured web host and port' do
+      import_url = "http://#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/t.git"
+      expect(described_class.blocked_url?(import_url)).to be false
+    end
+
+    it 'allows imports from configured SSH host and port' do
+      import_url = "http://#{Gitlab.config.gitlab_shell.ssh_host}:#{Gitlab.config.gitlab_shell.ssh_port}/t.git"
+      expect(described_class.blocked_url?(import_url)).to be false
+    end
+
+    it 'returns true for bad localhost hostname' do
+      expect(described_class.blocked_url?('https://localhost:65535/foo/foo.git')).to be true
+    end
+
+    it 'returns true for bad port' do
+      expect(described_class.blocked_url?('https://gitlab.com:25/foo/foo.git')).to be true
+    end
+
+    it 'returns true for invalid URL' do
+      expect(described_class.blocked_url?('http://:8080')).to be true
+    end
+
+    it 'returns false for legitimate URL' do
+      expect(described_class.blocked_url?('https://gitlab.com/foo/foo.git')).to be false
+    end
+  end
+end
diff --git a/spec/lib/gitlab/url_sanitizer_spec.rb b/spec/lib/gitlab/url_sanitizer_spec.rb
index 3fd361de458407a52d4145fdf6abfe6278c6c915..fc144a2556a4566a20ecb0d65a30693a2b30a2b2 100644
--- a/spec/lib/gitlab/url_sanitizer_spec.rb
+++ b/spec/lib/gitlab/url_sanitizer_spec.rb
@@ -5,6 +5,7 @@ describe Gitlab::UrlSanitizer, lib: true do
   let(:url_sanitizer) do
     described_class.new("https://github.com/me/project.git", credentials: credentials)
   end
+  let(:user) { double(:user, username: 'john.doe') }
 
   describe '.sanitize' do
     def sanitize_url(url)
@@ -53,12 +54,33 @@ describe Gitlab::UrlSanitizer, lib: true do
     end
   end
 
+  describe '.valid?' do
+    it 'validates url strings' do
+      expect(described_class.valid?(nil)).to be(false)
+      expect(described_class.valid?('valid@project:url.git')).to be(true)
+      expect(described_class.valid?('123://invalid:url')).to be(false)
+    end
+  end
+
+  describe '.http_credentials_for_user' do
+    it { expect(described_class.http_credentials_for_user(user)).to eq({ user: 'john.doe' }) }
+    it { expect(described_class.http_credentials_for_user('foo')).to eq({}) }
+  end
+
   describe '#sanitized_url' do
     it { expect(url_sanitizer.sanitized_url).to eq("https://github.com/me/project.git") }
   end
 
   describe '#credentials' do
     it { expect(url_sanitizer.credentials).to eq(credentials) }
+
+    context 'when user is given to #initialize' do
+      let(:url_sanitizer) do
+        described_class.new("https://github.com/me/project.git", credentials: described_class.http_credentials_for_user(user))
+      end
+
+      it { expect(url_sanitizer.credentials).to eq({ user: 'john.doe' }) }
+    end
   end
 
   describe '#full_url' do
@@ -69,13 +91,13 @@ describe Gitlab::UrlSanitizer, lib: true do
 
       expect(sanitizer.full_url).to eq('user@server:project.git')
     end
-  end
 
-  describe '.valid?' do
-    it 'validates url strings' do
-      expect(described_class.valid?(nil)).to be(false)
-      expect(described_class.valid?('valid@project:url.git')).to be(true)
-      expect(described_class.valid?('123://invalid:url')).to be(false)
+    context 'when user is given to #initialize' do
+      let(:url_sanitizer) do
+        described_class.new("https://github.com/me/project.git", credentials: described_class.http_credentials_for_user(user))
+      end
+
+      it { expect(url_sanitizer.full_url).to eq("https://john.doe@github.com/me/project.git") }
     end
   end
 end
diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..3255c6f1ef73dc762d723a1a5e13c297db4d55f7
--- /dev/null
+++ b/spec/lib/gitlab/visibility_level_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Gitlab::VisibilityLevel, lib: true do
+  describe '.level_value' do
+    it 'converts "public" to integer value' do
+      expect(described_class.level_value('public')).to eq(Gitlab::VisibilityLevel::PUBLIC)
+    end
+
+    it 'converts string integer to integer value' do
+      expect(described_class.level_value('20')).to eq(20)
+    end
+
+    it 'defaults to PRIVATE when string value is not valid' do
+      expect(described_class.level_value('invalid')).to eq(Gitlab::VisibilityLevel::PRIVATE)
+    end
+
+    it 'defaults to PRIVATE when integer value is not valid' do
+      expect(described_class.level_value(100)).to eq(Gitlab::VisibilityLevel::PRIVATE)
+    end
+  end
+end
diff --git a/spec/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb
deleted file mode 100644
index d968096783c4a08c0c1a81991b80e712af0e5cf1..0000000000000000000000000000000000000000
--- a/spec/mailers/emails/builds_spec.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-require 'spec_helper'
-require 'email_spec'
-
-describe Notify do
-  include EmailSpec::Matchers
-
-  include_context 'gitlab email notification'
-
-  describe 'build notification email' do
-    let(:build) { create(:ci_build) }
-    let(:project) { build.project }
-
-    shared_examples 'build email' do
-      it 'contains name of project' do
-        is_expected.to have_body_text build.project_name
-      end
-
-      it 'contains link to project' do
-        is_expected.to have_body_text namespace_project_path(project.namespace, project)
-      end
-    end
-
-    shared_examples 'an email with X-GitLab headers containing build details' do
-      it 'has X-GitLab-Build* headers' do
-        is_expected.to have_header 'X-GitLab-Build-Id', /#{build.id}/
-        is_expected.to have_header 'X-GitLab-Build-Ref', /#{build.ref}/
-      end
-    end
-
-    describe 'build success' do
-      subject { Notify.build_success_email(build.id, 'wow@example.com') }
-      before { build.success }
-
-      it_behaves_like 'build email'
-      it_behaves_like 'an email with X-GitLab headers containing build details'
-      it_behaves_like 'an email with X-GitLab headers containing project details'
-
-      it 'has header indicating build status' do
-        is_expected.to have_header 'X-GitLab-Build-Status', 'success'
-      end
-
-      it 'has the correct subject' do
-        is_expected.to have_subject /Build success for/
-      end
-    end
-
-    describe 'build fail' do
-      subject { Notify.build_fail_email(build.id, 'wow@example.com') }
-      before { build.drop }
-
-      it_behaves_like 'build email'
-      it_behaves_like 'an email with X-GitLab headers containing build details'
-      it_behaves_like 'an email with X-GitLab headers containing project details'
-
-      it 'has header indicating build status' do
-        is_expected.to have_header 'X-GitLab-Build-Status', 'failed'
-      end
-
-      it 'has the correct subject' do
-        is_expected.to have_subject /Build failed for/
-      end
-    end
-  end
-end
diff --git a/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..57eb03e3c809341e2918c8796347266ffc15adf9
--- /dev/null
+++ b/spec/migrations/migrate_build_events_to_pipeline_events_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170301205640_migrate_build_events_to_pipeline_events.rb')
+
+# This migration uses multiple threads, and thus different transactions. This
+# means data created in this spec may not be visible to some threads. To work
+# around this we use the TRUNCATE cleaning strategy.
+describe MigrateBuildEventsToPipelineEvents, truncate: true do
+  let(:migration) { described_class.new }
+  let(:project_with_pipeline_service) { create(:empty_project) }
+  let(:project_with_build_service) { create(:empty_project) }
+
+  before do
+    ActiveRecord::Base.connection.execute <<-SQL
+      INSERT INTO services (properties, build_events, pipeline_events, type)
+      VALUES
+        ('{"notify_only_broken_builds":true}', true, false, 'SlackService')
+      , ('{"notify_only_broken_builds":true}', true, false, 'MattermostService')
+      , ('{"notify_only_broken_builds":true}', true, false, 'HipchatService')
+      ;
+    SQL
+
+    ActiveRecord::Base.connection.execute <<-SQL
+      INSERT INTO services
+        (properties, build_events, pipeline_events, type, project_id)
+      VALUES
+        ('{"notify_only_broken_builds":true}', true, false,
+         'BuildsEmailService', #{project_with_pipeline_service.id})
+      , ('{"notify_only_broken_pipelines":true}', false, true,
+         'PipelinesEmailService', #{project_with_pipeline_service.id})
+      , ('{"notify_only_broken_builds":true}', true, false,
+         'BuildsEmailService', #{project_with_build_service.id})
+      ;
+    SQL
+  end
+
+  describe '#up' do
+    before do
+      silence_migration = Module.new do
+        # rubocop:disable Rails/Delegate
+        def execute(query)
+          connection.execute(query)
+        end
+      end
+
+      migration.extend(silence_migration)
+      migration.up
+    end
+
+    it 'migrates chat service properly' do
+      [SlackService, MattermostService, HipchatService].each do |service|
+        expect(service.count).to eq(1)
+
+        verify_service_record(service.first)
+      end
+    end
+
+    it 'migrates pipelines email service only if it has none before' do
+      Project.find_each do |project|
+        pipeline_service_count =
+          project.services.where(type: 'PipelinesEmailService').count
+
+        expect(pipeline_service_count).to eq(1)
+
+        verify_service_record(project.pipelines_email_service)
+      end
+    end
+
+    def verify_service_record(service)
+      expect(service.notify_only_broken_pipelines).to be(true)
+      expect(service.build_events).to be(false)
+      expect(service.pipeline_events).to be(true)
+    end
+  end
+end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index fd6ea2d6722a04388baa1d12271e4f70e824a1d5..8dbcf50ee0c5cf157f02879068230d85cee04f1e 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -795,8 +795,8 @@ describe Ci::Build, :models do
 
   describe '#merge_request' do
     def create_mr(build, pipeline, factory: :merge_request, created_at: Time.now)
-      create(factory, source_project_id: pipeline.gl_project_id,
-                      target_project_id: pipeline.gl_project_id,
+      create(factory, source_project: pipeline.project,
+                      target_project: pipeline.project,
                       source_branch: build.ref,
                       created_at: created_at)
     end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index bee9f7148497e91c2e59ff3f3b84f99cb208fc20..048d25869bc92e2eb202457d55415279345f2796 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -6,7 +6,7 @@ describe Ci::Variable, models: true do
   let(:secret_value) { 'secret' }
 
   it { is_expected.to validate_presence_of(:key) }
-  it { is_expected.to validate_uniqueness_of(:key).scoped_to(:gl_project_id) }
+  it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id) }
   it { is_expected.to validate_length_of(:key).is_at_most(255) }
   it { is_expected.to allow_value('foo').for(:key) }
   it { is_expected.not_to allow_value('foo bar').for(:key) }
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 4b449546a30e08e73418e7e478ea21ce2c611e91..980a1b70ef5a636e5b8d6b6299b00c8ba6cc497f 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -388,4 +388,32 @@ eos
       expect(described_class.valid_hash?('a' * 41)).to be false
     end
   end
+
+  describe '#raw_diffs' do
+    context 'Gitaly commit_raw_diffs feature enabled' do
+      before do
+        allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:commit_raw_diffs).and_return(true)
+      end
+
+      context 'when a truthy deltas_only is not passed to args' do
+        it 'fetches diffs from Gitaly server' do
+          expect(Gitlab::GitalyClient::Commit).to receive(:diff_from_parent).
+            with(commit)
+
+          commit.raw_diffs
+        end
+      end
+
+      context 'when a truthy deltas_only is passed to args' do
+        it 'fetches diffs using Rugged' do
+          opts = { deltas_only: true }
+
+          expect(Gitlab::GitalyClient::Commit).not_to receive(:diff_from_parent)
+          expect(commit.raw).to receive(:diffs).with(opts)
+
+          commit.raw_diffs(opts)
+        end
+      end
+    end
+  end
 end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 9ffcb88bafd3284a1be02fe8083e9ff4a996d75f..73977d031f9d803fb5dc12b5c4574a475e7c3a6b 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -37,6 +37,30 @@ describe Issue, models: true do
     end
   end
 
+  describe '#closed_at' do
+    after do
+      Timecop.return
+    end
+
+    let!(:now) { Timecop.freeze(Time.now) }
+
+    it 'sets closed_at to Time.now when issue is closed' do
+      issue = create(:issue, state: 'opened')
+
+      issue.close
+
+      expect(issue.closed_at).to eq(now)
+    end
+
+    it 'sets closed_at to nil when issue is reopened' do
+      issue = create(:issue, state: 'closed')
+
+      issue.reopen
+
+      expect(issue.closed_at).to be_nil
+    end
+  end
+
   describe '#to_reference' do
     let(:namespace) { build(:namespace, path: 'sample-namespace') }
     let(:project)   { build(:empty_project, name: 'sample-project', namespace: namespace) }
diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb
deleted file mode 100644
index 0194f9e256306bf63ae140b48029eea97259eccd..0000000000000000000000000000000000000000
--- a/spec/models/project_services/builds_email_service_spec.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-require 'spec_helper'
-
-describe BuildsEmailService do
-  let(:data) do
-    Gitlab::DataBuilder::Build.build(create(:ci_build))
-  end
-
-  describe 'Validations' do
-    context 'when service is active' do
-      before { subject.active = true }
-
-      it { is_expected.to validate_presence_of(:recipients) }
-
-      context 'when pusher is added' do
-        before { subject.add_pusher = true }
-
-        it { is_expected.not_to validate_presence_of(:recipients) }
-      end
-    end
-
-    context 'when service is inactive' do
-      before { subject.active = false }
-
-      it { is_expected.not_to validate_presence_of(:recipients) }
-    end
-  end
-
-  describe '#test_data' do
-    let(:build)   { create(:ci_build) }
-    let(:project) { build.project }
-    let(:user)    { create(:user) }
-
-    before { project.team << [user, :developer] }
-
-    it 'builds test data' do
-      data = subject.test_data(project)
-
-      expect(data[:object_kind]).to eq("build")
-    end
-  end
-
-  describe '#test' do
-    it 'sends email' do
-      data = Gitlab::DataBuilder::Build.build(create(:ci_build))
-      subject.recipients = 'test@gitlab.com'
-
-      expect(BuildEmailWorker).to receive(:perform_async)
-
-      subject.test(data)
-    end
-
-    context 'notify only failed builds is true' do
-      it 'sends email' do
-        data = Gitlab::DataBuilder::Build.build(create(:ci_build))
-        data[:build_status] = "success"
-        subject.recipients = 'test@gitlab.com'
-
-        expect(subject).not_to receive(:notify_only_broken_builds)
-        expect(BuildEmailWorker).to receive(:perform_async)
-
-        subject.test(data)
-      end
-    end
-  end
-
-  describe '#execute' do
-    it 'sends email' do
-      subject.recipients = 'test@gitlab.com'
-      data[:build_status] = 'failed'
-
-      expect(BuildEmailWorker).to receive(:perform_async)
-
-      subject.execute(data)
-    end
-
-    it 'does not send email with succeeded build and notify_only_broken_builds on' do
-      expect(subject).to receive(:notify_only_broken_builds).and_return(true)
-      data[:build_status] = 'success'
-
-      expect(BuildEmailWorker).not_to receive(:perform_async)
-
-      subject.execute(data)
-    end
-
-    it 'does not send email with failed build and build_allow_failure is true' do
-      data[:build_status] = 'failed'
-      data[:build_allow_failure] = true
-
-      expect(BuildEmailWorker).not_to receive(:perform_async)
-
-      subject.execute(data)
-    end
-
-    it 'does not send email with unknown build status' do
-      data[:build_status] = 'foo'
-
-      expect(BuildEmailWorker).not_to receive(:perform_async)
-
-      subject.execute(data)
-    end
-
-    it 'does not send email when recipients list is empty' do
-      subject.recipients = ' ,, '
-      data[:build_status] = 'failed'
-
-      expect(BuildEmailWorker).not_to receive(:perform_async)
-
-      subject.execute(data)
-    end
-  end
-end
diff --git a/spec/models/project_services/chat_message/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb
deleted file mode 100644
index 3bd7ec18ae0b8fa70995618e8d9544eda585c261..0000000000000000000000000000000000000000
--- a/spec/models/project_services/chat_message/build_message_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'spec_helper'
-
-describe ChatMessage::BuildMessage do
-  subject { described_class.new(args) }
-
-  let(:args) do
-    {
-      sha: '97de212e80737a608d939f648d959671fb0a0142',
-      ref: 'develop',
-      tag: false,
-
-      project_name: 'project_name',
-      project_url: 'http://example.gitlab.com',
-      build_id: 1,
-      build_name: build_name,
-      build_stage: stage,
-
-      commit: {
-        status: status,
-        author_name: 'hacker',
-        author_url: 'http://example.gitlab.com/hacker',
-        duration: duration,
-      },
-    }
-  end
-
-  let(:message) { build_message }
-  let(:stage) { 'test' }
-  let(:status) { 'success' }
-  let(:build_name) { 'rspec' }
-  let(:duration) { 10 }
-
-  context 'build succeeded' do
-    let(:status) { 'success' }
-    let(:color) { 'good' }
-    let(:message) { build_message('passed') }
-
-    it 'returns a message with information about succeeded build' do
-      expect(subject.pretext).to be_empty
-      expect(subject.fallback).to eq(message)
-      expect(subject.attachments).to eq([text: message, color: color])
-    end
-  end
-
-  context 'build failed' do
-    let(:status) { 'failed' }
-    let(:color) { 'danger' }
-
-    it 'returns a message with information about failed build' do
-      expect(subject.pretext).to be_empty
-      expect(subject.fallback).to eq(message)
-      expect(subject.attachments).to eq([text: message, color: color])
-    end
-  end
-
-  it 'returns a message with information on build' do
-    expect(subject.fallback).to include("on build <http://example.gitlab.com/builds/1|#{build_name}>")
-  end
-
-  it 'returns a message with stage name' do
-    expect(subject.fallback).to include("of stage #{stage}")
-  end
-
-  it 'returns a message with link to author' do
-    expect(subject.fallback).to include("by <http://example.gitlab.com/hacker|hacker>")
-  end
-
-  def build_message(status_text = status, stage_text = stage, build_text = build_name)
-    "<http://example.gitlab.com|project_name>:" \
-    " Commit <http://example.gitlab.com/commit/" \
-    "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
-    " of <http://example.gitlab.com/commits/develop|develop> branch" \
-    " by <http://example.gitlab.com/hacker|hacker> #{status_text}" \
-    " on build <http://example.gitlab.com/builds/1|#{build_text}>" \
-    " of stage #{stage_text} in #{duration} #{'second'.pluralize(duration)}"
-  end
-end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index bf422ac7ce1a82c076b38a813be337cba3ed30e3..1200ae7eb224a94303f665899a47f2dbf9487ae3 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -280,13 +280,14 @@ describe HipchatService, models: true do
       end
     end
 
-    context 'build events' do
-      let(:pipeline) { create(:ci_empty_pipeline) }
-      let(:build) { create(:ci_build, pipeline: pipeline) }
-      let(:data) { Gitlab::DataBuilder::Build.build(build.reload) }
+    context 'pipeline events' do
+      let(:pipeline) { create(:ci_empty_pipeline, user: create(:user)) }
+      let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) }
 
       context 'for failed' do
-        before { build.drop }
+        before do
+          pipeline.drop
+        end
 
         it "calls Hipchat API" do
           hipchat.execute(data)
@@ -295,35 +296,36 @@ describe HipchatService, models: true do
         end
 
         it "creates a build message" do
-          message = hipchat.send(:create_build_message, data)
+          message = hipchat.__send__(:create_pipeline_message, data)
 
           project_url = project.web_url
           project_name = project.name_with_namespace.gsub(/\s/, '')
-          sha = data[:sha]
-          ref = data[:ref]
-          ref_type = data[:tag] ? 'tag' : 'branch'
-          duration = data[:commit][:duration]
+          pipeline_attributes = data[:object_attributes]
+          ref = pipeline_attributes[:ref]
+          ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+          duration = pipeline_attributes[:duration]
+          user_name = data[:user][:name]
 
           expect(message).to eq("<a href=\"#{project_url}\">#{project_name}</a>: " \
-            "Commit <a href=\"#{project_url}/commit/#{sha}/builds\">#{Commit.truncate_sha(sha)}</a> " \
+            "Pipeline <a href=\"#{project_url}/pipelines/#{pipeline.id}\">##{pipeline.id}</a> " \
             "of <a href=\"#{project_url}/commits/#{ref}\">#{ref}</a> #{ref_type} " \
-            "by #{data[:commit][:author_name]} failed in #{duration} second(s)")
+            "by #{user_name} failed in #{duration} second(s)")
         end
       end
 
       context 'for succeeded' do
         before do
-          build.success
+          pipeline.succeed
         end
 
         it "calls Hipchat API" do
-          hipchat.notify_only_broken_builds = false
+          hipchat.notify_only_broken_pipelines = false
           hipchat.execute(data)
           expect(WebMock).to have_requested(:post, api_url).once
         end
 
         it "notifies only broken" do
-          hipchat.notify_only_broken_builds = true
+          hipchat.notify_only_broken_pipelines = true
           hipchat.execute(data)
           expect(WebMock).not_to have_requested(:post, api_url).once
         end
@@ -349,17 +351,19 @@ describe HipchatService, models: true do
 
       context 'with a successful build' do
         it 'uses the green color' do
-          build_data = { object_kind: 'build', commit: { status: 'success' } }
+          data = { object_kind: 'pipeline',
+                   object_attributes: { status: 'success' } }
 
-          expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'green' })
+          expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'green' })
         end
       end
 
       context 'with a failed build' do
         it 'uses the red color' do
-          build_data = { object_kind: 'build', commit: { status: 'failed' } }
+          data = { object_kind: 'pipeline',
+                   object_attributes: { status: 'failed' } }
 
-          expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' })
+          expect(hipchat.__send__(:message_options, data)).to eq({ notify: false, color: 'red' })
         end
       end
     end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index ff1defcd32d224199d8a213a9346ee6c4255716d..5e5f690acd4666e249ba3baba6a9c47237514752 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -29,8 +29,7 @@ describe Project, models: true do
     it { is_expected.to have_one(:campfire_service).dependent(:destroy) }
     it { is_expected.to have_one(:drone_ci_service).dependent(:destroy) }
     it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
-    it { is_expected.to have_one(:builds_email_service).dependent(:destroy) }
-    it { is_expected.to have_one(:emails_on_push_service).dependent(:destroy) }
+    it { is_expected.to have_one(:pipelines_email_service).dependent(:destroy) }
     it { is_expected.to have_one(:irker_service).dependent(:destroy) }
     it { is_expected.to have_one(:pivotaltracker_service).dependent(:destroy) }
     it { is_expected.to have_one(:hipchat_service).dependent(:destroy) }
@@ -219,6 +218,20 @@ describe Project, models: true do
       expect(project2.import_data).to be_nil
     end
 
+    it "does not allow blocked import_url localhost" do
+      project2 = build(:empty_project, import_url: 'http://localhost:9000/t.git')
+
+      expect(project2).to be_invalid
+      expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+    end
+
+    it "does not allow blocked import_url port" do
+      project2 = build(:empty_project, import_url: 'http://github.com:25/t.git')
+
+      expect(project2).to be_invalid
+      expect(project2.errors[:import_url]).to include('imports are not allowed from that URL')
+    end
+
     describe 'project pending deletion' do
       let!(:project_pending_deletion) do
         create(:empty_project,
@@ -1901,10 +1914,8 @@ describe Project, models: true do
 
     context 'when no user is given' do
       it 'returns the url to the repo without a username' do
-        url = project.http_url_to_repo
-
-        expect(url).to eq(project.http_url_to_repo)
-        expect(url).not_to include('@')
+        expect(project.http_url_to_repo).to eq("#{project.web_url}.git")
+        expect(project.http_url_to_repo).not_to include('@')
       end
     end
 
@@ -1912,7 +1923,7 @@ describe Project, models: true do
       it 'returns the url to the repo with the username' do
         user = build_stubbed(:user)
 
-        expect(project.http_url_to_repo(user)).to match(%r{https?:\/\/#{user.username}@})
+        expect(project.http_url_to_repo(user)).to start_with("http://#{user.username}@")
       end
     end
   end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 58b57bd4fef4dc94d1eecd60f2d6f4ae33f6c5c3..b5b9cd024b0568e251543ef20d26204c9ade61ca 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -35,10 +35,23 @@ describe ProjectWiki, models: true do
   end
 
   describe "#http_url_to_repo" do
-    it "provides the full http url to the repo" do
-      gitlab_url = Gitlab.config.gitlab.url
-      repo_http_url = "#{gitlab_url}/#{subject.path_with_namespace}.git"
-      expect(subject.http_url_to_repo).to eq(repo_http_url)
+    let(:project) { create :empty_project }
+
+    context 'when no user is given' do
+      it 'returns the url to the repo without a username' do
+        expected_url = "#{Gitlab.config.gitlab.url}/#{subject.path_with_namespace}.git"
+
+        expect(project_wiki.http_url_to_repo).to eq(expected_url)
+        expect(project_wiki.http_url_to_repo).not_to include('@')
+      end
+    end
+
+    context 'when user is given' do
+      it 'returns the url to the repo with the username' do
+        user = build_stubbed(:user)
+
+        expect(project_wiki.http_url_to_repo(user)).to start_with("http://#{user.username}@")
+      end
     end
   end
 
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 0b222022e62360aeaf0fcf3b170ee7e7918f55fe..bc8ae4ae5a8531d12e6f7ad36e6d3cbde8e59256 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -43,14 +43,22 @@ describe Route, models: true do
     end
 
     context 'name update' do
-      before { route.update_attributes(name: 'bar') }
-
       it "updates children routes with new path" do
+        route.update_attributes(name: 'bar') 
+
         expect(described_class.exists?(name: 'bar')).to be_truthy
         expect(described_class.exists?(name: 'bar / test')).to be_truthy
         expect(described_class.exists?(name: 'bar / test / foo')).to be_truthy
         expect(described_class.exists?(name: 'gitlab-org')).to be_truthy
       end
+
+      it 'handles a rename from nil' do
+        # Note: using `update_columns` to skip all validation and callbacks
+        route.update_columns(name: nil)
+
+        expect { route.update_attributes(name: 'bar') }
+          .to change { route.name }.from(nil).to('bar')
+      end
     end
   end
 end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index ab5a7e4d3deddbb9df2c483bf2857b64846e0a05..a70f7beaae0d387e0ba37fe2344b8a9ff2779fb3 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -5,77 +5,146 @@ describe API::Branches, api: true  do
   include ApiHelpers
 
   let(:user) { create(:user) }
-  let(:user2) { create(:user) }
   let!(:project) { create(:project, :repository, creator: user) }
   let!(:master) { create(:project_member, :master, user: user, project: project) }
-  let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
+  let(:guest) { create(:user).tap { |u| create(:project_member, :guest, user: u, project: project) } }
   let!(:branch_name) { 'feature' }
   let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
-  let!(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master") }
+  let(:branch_with_dot) { CreateBranchService.new(project, user).execute("with.1.2.3", "master")[:branch] }
 
   describe "GET /projects/:id/repository/branches" do
-    it "returns an array of project branches" do
-      project.repository.expire_all_method_caches
+    let(:route) { "/projects/#{project.id}/repository/branches" }
 
-      get api("/projects/#{project.id}/repository/branches", user), per_page: 100
+    shared_examples_for 'repository branches' do
+      it 'returns the repository branches' do
+        get api(route, current_user), per_page: 100
 
-      expect(response).to have_http_status(200)
-      expect(response).to include_pagination_headers
-      expect(json_response).to be_an Array
-      branch_names = json_response.map { |x| x['name'] }
-      expect(branch_names).to match_array(project.repository.branch_names)
+        expect(response).to have_http_status(200)
+        expect(response).to include_pagination_headers
+        expect(json_response).to be_an Array
+        branch_names = json_response.map { |x| x['name'] }
+        expect(branch_names).to match_array(project.repository.branch_names)
+      end
+
+      context 'when repository is disabled' do
+        include_context 'disabled repository'
+
+        it_behaves_like '403 response' do
+          let(:request) { get api(route, current_user) }
+        end
+      end
     end
-  end
 
-  describe "GET /projects/:id/repository/branches/:branch" do
-    it "returns the branch information for a single branch" do
-      get api("/projects/#{project.id}/repository/branches/#{branch_name}", user)
-      expect(response).to have_http_status(200)
+    context 'when unauthenticated', 'and project is public' do
+      it_behaves_like 'repository branches' do
+        let(:project) { create(:project, :public, :repository) }
+        let(:current_user) { nil }
+      end
+    end
 
-      expect(json_response['name']).to eq(branch_name)
-      json_commit = json_response['commit']
-      expect(json_commit['id']).to eq(branch_sha)
-      expect(json_commit).to have_key('short_id')
-      expect(json_commit).to have_key('title')
-      expect(json_commit).to have_key('message')
-      expect(json_commit).to have_key('author_name')
-      expect(json_commit).to have_key('author_email')
-      expect(json_commit).to have_key('authored_date')
-      expect(json_commit).to have_key('committer_name')
-      expect(json_commit).to have_key('committer_email')
-      expect(json_commit).to have_key('committed_date')
-      expect(json_commit).to have_key('parent_ids')
-      expect(json_response['merged']).to eq(false)
-      expect(json_response['protected']).to eq(false)
-      expect(json_response['developers_can_push']).to eq(false)
-      expect(json_response['developers_can_merge']).to eq(false)
+    context 'when unauthenticated', 'and project is private' do
+      it_behaves_like '404 response' do
+        let(:request) { get api(route) }
+        let(:message) { '404 Project Not Found' }
+      end
     end
 
-    it "returns the branch information for a single branch with dots in the name" do
-      get api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+    context 'when authenticated', 'as a developer' do
+      it_behaves_like 'repository branches' do
+        let(:current_user) { user }
+      end
+    end
 
-      expect(response).to have_http_status(200)
-      expect(json_response['name']).to eq("with.1.2.3")
+    context 'when authenticated', 'as a guest' do
+      it_behaves_like '403 response' do
+        let(:request) { get api(route, guest) }
+      end
     end
+  end
+
+  describe "GET /projects/:id/repository/branches/:branch" do
+    let(:route) { "/projects/#{project.id}/repository/branches/#{branch_name}" }
 
-    context 'on a merged branch' do
-      it "returns the branch information for a single branch" do
-        get api("/projects/#{project.id}/repository/branches/merge-test", user)
+    shared_examples_for 'repository branch' do |merged: false|
+      it 'returns the repository branch' do
+        get api(route, current_user)
 
         expect(response).to have_http_status(200)
-        expect(json_response['name']).to eq('merge-test')
-        expect(json_response['merged']).to eq(true)
+        expect(json_response['name']).to eq(branch_name)
+        expect(json_response['merged']).to eq(merged)
+        expect(json_response['protected']).to eq(false)
+        expect(json_response['developers_can_push']).to eq(false)
+        expect(json_response['developers_can_merge']).to eq(false)
+
+        json_commit = json_response['commit']
+        expect(json_commit['id']).to eq(branch_sha)
+        expect(json_commit).to have_key('short_id')
+        expect(json_commit).to have_key('title')
+        expect(json_commit).to have_key('message')
+        expect(json_commit).to have_key('author_name')
+        expect(json_commit).to have_key('author_email')
+        expect(json_commit).to have_key('authored_date')
+        expect(json_commit).to have_key('committer_name')
+        expect(json_commit).to have_key('committer_email')
+        expect(json_commit).to have_key('committed_date')
+        expect(json_commit).to have_key('parent_ids')
+      end
+
+      context 'when branch does not exist' do
+        let(:branch_name) { 'unknown' }
+
+        it_behaves_like '404 response' do
+          let(:request) { get api(route, current_user) }
+          let(:message) { '404 Branch Not Found' }
+        end
+      end
+
+      context 'when repository is disabled' do
+        include_context 'disabled repository'
+
+        it_behaves_like '403 response' do
+          let(:request) { get api(route, current_user) }
+        end
       end
     end
 
-    it "returns a 403 error if guest" do
-      get api("/projects/#{project.id}/repository/branches", user2)
-      expect(response).to have_http_status(403)
+    context 'when unauthenticated', 'and project is public' do
+      it_behaves_like 'repository branch' do
+        let(:project) { create(:project, :public, :repository) }
+        let(:current_user) { nil }
+      end
     end
 
-    it "returns a 404 error if branch is not available" do
-      get api("/projects/#{project.id}/repository/branches/unknown", user)
-      expect(response).to have_http_status(404)
+    context 'when unauthenticated', 'and project is private' do
+      it_behaves_like '404 response' do
+        let(:request) { get api(route) }
+        let(:message) { '404 Project Not Found' }
+      end
+    end
+
+    context 'when authenticated', 'as a developer' do
+      let(:current_user) { user }
+      it_behaves_like 'repository branch'
+
+      context 'when branch contains a dot' do
+        let(:branch_name) { branch_with_dot.name }
+        let(:branch_sha) { project.commit('master').sha }
+
+        it_behaves_like 'repository branch'
+      end
+
+      context 'when branch is merged' do
+        let(:branch_name) { 'merge-test' }
+        let(:branch_sha) { project.commit('merge-test').sha }
+
+        it_behaves_like 'repository branch', merged: true
+      end
+    end
+
+    context 'when authenticated', 'as a guest' do
+      it_behaves_like '403 response' do
+        let(:request) { get api(route, guest) }
+      end
     end
   end
 
@@ -93,10 +162,10 @@ describe API::Branches, api: true  do
       end
 
       it "protects a single branch with dots in the name" do
-        put api("/projects/#{project.id}/repository/branches/with.1.2.3/protect", user)
+        put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/protect", user)
 
         expect(response).to have_http_status(200)
-        expect(json_response['name']).to eq("with.1.2.3")
+        expect(json_response['name']).to eq(branch_with_dot.name)
         expect(json_response['protected']).to eq(true)
       end
 
@@ -234,7 +303,7 @@ describe API::Branches, api: true  do
     end
 
     it "returns a 403 error if guest" do
-      put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user2)
+      put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", guest)
       expect(response).to have_http_status(403)
     end
   end
@@ -250,10 +319,10 @@ describe API::Branches, api: true  do
     end
 
     it "update branches with dots in branch name" do
-      put api("/projects/#{project.id}/repository/branches/with.1.2.3/unprotect", user)
+      put api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}/unprotect", user)
 
       expect(response).to have_http_status(200)
-      expect(json_response['name']).to eq("with.1.2.3")
+      expect(json_response['name']).to eq(branch_with_dot.name)
       expect(json_response['protected']).to eq(false)
     end
 
@@ -282,7 +351,7 @@ describe API::Branches, api: true  do
     end
 
     it "denies for user without push access" do
-      post api("/projects/#{project.id}/repository/branches", user2),
+      post api("/projects/#{project.id}/repository/branches", guest),
            branch: branch_name,
            ref: branch_sha
       expect(response).to have_http_status(403)
@@ -330,7 +399,7 @@ describe API::Branches, api: true  do
     end
 
     it "removes a branch with dots in the branch name" do
-      delete api("/projects/#{project.id}/repository/branches/with.1.2.3", user)
+      delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}", user)
 
       expect(response).to have_http_status(204)
     end
@@ -367,7 +436,7 @@ describe API::Branches, api: true  do
     end
 
     it 'returns a 403 error if guest' do
-      delete api("/projects/#{project.id}/repository/merged_branches", user2)
+      delete api("/projects/#{project.id}/repository/merged_branches", guest)
       expect(response).to have_http_status(403)
     end
   end
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 442b2df19522cec4ed38c8f7cb9ed62c79492659..044b989e5ba31b85b1faf8be4e3e5f2e7d872f6d 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -152,6 +152,34 @@ describe API::Runner do
         end
       end
     end
+
+    describe 'POST /api/v4/runners/verify' do
+      let(:runner) { create(:ci_runner) }
+
+      context 'when no token is provided' do
+        it 'returns 400 error' do
+          post api('/runners/verify')
+
+          expect(response).to have_http_status :bad_request
+        end
+      end
+
+      context 'when invalid token is provided' do
+        it 'returns 403 error' do
+          post api('/runners/verify'), token: 'invalid-token'
+
+          expect(response).to have_http_status 403
+        end
+      end
+
+      context 'when valid token is provided' do
+        it 'verifies Runner credentials' do
+          post api('/runners/verify'), token: runner.token
+
+          expect(response).to have_http_status 200
+        end
+      end
+    end
   end
 
   describe '/api/v4/jobs' do
@@ -220,18 +248,6 @@ describe API::Runner do
             it { expect(response).to have_http_status(204) }
           end
         end
-
-        context "when runner doesn't send version in User-Agent" do
-          let(:user_agent) { 'Go-http-client/1.1' }
-
-          it { expect(response).to have_http_status(404) }
-        end
-
-        context "when runner doesn't have a User-Agent" do
-          let(:user_agent) { nil }
-
-          it { expect(response).to have_http_status(404) }
-        end
       end
 
       context 'when no token is provided' do
@@ -254,10 +270,10 @@ describe API::Runner do
         context 'when Runner is not active' do
           let(:runner) { create(:ci_runner, :inactive) }
 
-          it 'returns 404 error' do
+          it 'returns 204 error' do
             request_job
 
-            expect(response).to have_http_status 404
+            expect(response).to have_http_status 204
           end
         end
 
@@ -401,9 +417,39 @@ describe API::Runner do
           end
 
           context 'when project and pipeline have multiple jobs' do
+            let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+            let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
             let!(:test_job) { create(:ci_build, pipeline: pipeline, name: 'deploy', stage: 'deploy', stage_idx: 1) }
 
-            before { job.success }
+            before do
+              job.success
+              job2.success
+            end
+
+            it 'returns dependent jobs' do
+              request_job
+
+              expect(response).to have_http_status(201)
+              expect(json_response['id']).to eq(test_job.id)
+              expect(json_response['dependencies'].count).to eq(2)
+              expect(json_response['dependencies']).to include({ 'id' => job.id, 'name' => job.name, 'token' => job.token },
+                                                               { 'id' => job2.id, 'name' => job2.name, 'token' => job2.token })
+            end
+          end
+
+          context 'when explicit dependencies are defined' do
+            let!(:job) { create(:ci_build_tag, pipeline: pipeline, name: 'spinach', stage: 'test', stage_idx: 0) }
+            let!(:job2) { create(:ci_build_tag, pipeline: pipeline, name: 'rubocop', stage: 'test', stage_idx: 0) }
+            let!(:test_job) do
+              create(:ci_build, pipeline: pipeline, token: 'test-job-token', name: 'deploy',
+                                stage: 'deploy', stage_idx: 1,
+                                options: { dependencies: [job2.name] })
+            end
+
+            before do
+              job.success
+              job2.success
+            end
 
             it 'returns dependent jobs' do
               request_job
@@ -411,7 +457,7 @@ describe API::Runner do
               expect(response).to have_http_status(201)
               expect(json_response['id']).to eq(test_job.id)
               expect(json_response['dependencies'].count).to eq(1)
-              expect(json_response['dependencies'][0]).to include('id' => job.id, 'name' => 'spinach')
+              expect(json_response['dependencies'][0]).to include('id' => job2.id, 'name' => job2.name, 'token' => job2.token)
             end
           end
 
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 424c02932ab1ab5eacc74db80bfea062da4d656b..d93a734f5b69d590cb0e3377fa7cec05cfbaf7f6 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -59,14 +59,6 @@ describe API::Triggers do
         expect(pipeline.builds.size).to eq(5)
       end
 
-      it 'creates builds on webhook from other gitlab repository and branch' do
-        expect do
-          post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
-        end.to change(project.builds, :count).by(5)
-
-        expect(response).to have_http_status(201)
-      end
-
       it 'returns bad request with no pipeline created if there\'s no commit for that ref' do
         post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch')
 
@@ -101,6 +93,28 @@ describe API::Triggers do
         end
       end
     end
+
+    context 'when triggering a pipeline from a trigger token' do
+      it 'creates builds from the ref given in the URL, not in the body' do
+        expect do
+          post api("/projects/#{project.id}/ref/master/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+        end.to change(project.builds, :count).by(5)
+
+        expect(response).to have_http_status(201)
+      end
+
+      context 'when ref contains a dot' do
+        it 'creates builds from the ref given in the URL, not in the body' do
+          project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch')
+
+          expect do
+            post api("/projects/#{project.id}/ref/v.1-branch/trigger/pipeline?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+          end.to change(project.builds, :count).by(4)
+
+          expect(response).to have_http_status(201)
+        end
+      end
+    end
   end
 
   describe 'GET /projects/:id/triggers' do
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
index 4819269d69f9c8cfe69ac992e7b70685efff413a..9233e9621bf51c05fa73343dfb0cc68f3b58b16a 100644
--- a/spec/requests/api/v3/triggers_spec.rb
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -51,13 +51,6 @@ describe API::V3::Triggers do
         expect(pipeline.builds.size).to eq(5)
       end
 
-      it 'creates builds on webhook from other gitlab repository and branch' do
-        expect do
-          post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
-        end.to change(project.builds, :count).by(5)
-        expect(response).to have_http_status(201)
-      end
-
       it 'returns bad request with no builds created if there\'s no commit for that ref' do
         post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
         expect(response).to have_http_status(400)
@@ -89,6 +82,27 @@ describe API::V3::Triggers do
         end
       end
     end
+
+    context 'when triggering a pipeline from a trigger token' do
+      it 'creates builds from the ref given in the URL, not in the body' do
+        expect do
+          post v3_api("/projects/#{project.id}/ref/master/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+        end.to change(project.builds, :count).by(5)
+        expect(response).to have_http_status(201)
+      end
+
+      context 'when ref contains a dot' do
+        it 'creates builds from the ref given in the URL, not in the body' do
+          project.repository.create_file(user, '.gitlab/gitlabhq/new_feature.md', 'something valid', message: 'new_feature', branch_name: 'v.1-branch')
+
+          expect do
+            post v3_api("/projects/#{project.id}/ref/v.1-branch/trigger/builds?token=#{trigger_token}"), { ref: 'refs/heads/other-branch' }
+          end.to change(project.builds, :count).by(4)
+
+          expect(response).to have_http_status(201)
+        end
+      end
+    end
   end
 
   describe 'GET /projects/:id/triggers' do
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index 65af4e13118e41c4e8f3377344b0971576a1e5ec..8567817147b72f397ddb76af955c2a878cd8d743 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -19,7 +19,7 @@ describe Ci::RetryBuildService, :services do
        erased_at].freeze
 
   IGNORE_ACCESSORS =
-    %i[type lock_version target_url gl_project_id deploy job_id base_tags
+    %i[type lock_version target_url base_tags
        commit_id deployments erased_by_id last_deployment project_id
        runner_id tag_taggings taggings tags trigger_request_id
        user_id].freeze
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 0768f644036d43c2755e0c1fe1971dac5ed05c2e..adfa75a524fb3966502a08e63cf74969c30bc812 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -49,10 +49,13 @@ describe MergeRequests::BuildService, services: true do
       let(:commits) { Commit.decorate([commit_1], project) }
 
       it 'creates compare object with target branch as default branch' do
-        expect(merge_request.can_be_created).to eq(false)
         expect(merge_request.compare).to be_present
         expect(merge_request.target_branch).to eq(project.default_branch)
       end
+
+      it 'allows the merge request to be created' do
+        expect(merge_request.can_be_created).to eq(true)
+      end
     end
 
     context 'same source and target branch' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 82a4ec3f5819baf604efb8fd34242a07f445239c..f7240969588ddf196768fd9fbd0c7baa664166fc 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -758,7 +758,7 @@ describe NotificationService, services: true do
         update_custom_notification(:reopen_issue, @u_custom_global)
       end
 
-      it 'sends email to issue assignee and issue author' do
+      it 'sends email to issue notification recipients' do
         notification.reopen_issue(issue, @u_disabled)
 
         should_email(issue.assignee)
@@ -772,6 +772,7 @@ describe NotificationService, services: true do
         should_email(@watcher_and_subscriber)
         should_not_email(@unsubscriber)
         should_not_email(@u_participating)
+        should_not_email(@u_disabled)
         should_not_email(@u_lazy_participant)
       end
 
@@ -781,6 +782,32 @@ describe NotificationService, services: true do
         let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) }
       end
     end
+
+    describe '#issue_moved' do
+      let(:new_issue) { create(:issue) }
+
+      it 'sends email to issue notification recipients' do
+        notification.issue_moved(issue, new_issue, @u_disabled)
+
+        should_email(issue.assignee)
+        should_email(issue.author)
+        should_email(@u_watcher)
+        should_email(@u_guest_watcher)
+        should_email(@u_participant_mentioned)
+        should_email(@subscriber)
+        should_email(@watcher_and_subscriber)
+        should_not_email(@unsubscriber)
+        should_not_email(@u_participating)
+        should_not_email(@u_disabled)
+        should_not_email(@u_lazy_participant)
+      end
+
+      it_behaves_like 'participating notifications' do
+        let(:participant) { create(:user, username: 'user-participant') }
+        let(:issuable) { issue }
+        let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) }
+      end
+    end
   end
 
   describe 'Merge Requests' do
@@ -1192,6 +1219,48 @@ describe NotificationService, services: true do
     end
   end
 
+  describe 'Pipelines' do
+    describe '#pipeline_finished' do
+      let(:project) { create(:project, :public) }
+      let(:current_user) { create(:user) }
+      let(:u_member) { create(:user) }
+      let(:u_other) { create(:user) }
+
+      let(:commit) { project.commit }
+      let(:pipeline) do
+        create(:ci_pipeline, :success,
+               project: project,
+               user: current_user,
+               ref: 'refs/heads/master',
+               sha: commit.id,
+               before_sha: '00000000')
+      end
+
+      before do
+        project.add_master(current_user)
+        project.add_master(u_member)
+        reset_delivered_emails!
+      end
+
+      context 'without custom recipients' do
+        it 'notifies the pipeline user' do
+          notification.pipeline_finished(pipeline)
+
+          should_only_email(current_user, kind: :bcc)
+        end
+      end
+
+      context 'with custom recipients' do
+        it 'notifies the custom recipients' do
+          users = [u_member, u_other]
+          notification.pipeline_finished(pipeline, users.map(&:notification_email))
+
+          should_only_email(*users, kind: :bcc)
+        end
+      end
+    end
+  end
+
   def build_team(project)
     @u_watcher               = create_global_setting_for(create(:user), :watch)
     @u_participating         = create_global_setting_for(create(:user), :participating)
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index ab6e8f537bac91e1f6d89c55adf0c848eacae554..e5917bb0b7a165c34b1baf69fde6d96afda8cde9 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -120,6 +120,26 @@ describe Projects::ImportService, services: true do
       end
     end
 
+    context 'with blocked import_URL' do
+      it 'fails with localhost' do
+        project.import_url = 'https://localhost:9000/vim/vim.git'
+
+        result = described_class.new(project, user).execute
+
+        expect(result[:status]).to eq :error
+        expect(result[:message]).to end_with 'Blocked import URL.'
+      end
+
+      it 'fails with port 25' do
+        project.import_url = "https://github.com:25/vim/vim.git"
+
+        result = described_class.new(project, user).execute
+
+        expect(result[:status]).to eq :error
+        expect(result[:message]).to end_with 'Blocked import URL.'
+      end
+    end
+
     def stub_github_omniauth_provider
       provider = OpenStruct.new(
         'name' => 'github',
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index db9f1231682a7c328aa5c0fc1fe7058c7f28cc47..11037a4917b8aa1880e5b19e8a20ffbf9725cc04 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -5,6 +5,7 @@ describe SystemHooksService, services: true do
   let(:project)       { create :project }
   let(:project_member) { create :project_member }
   let(:key)           { create(:key, user: user) }
+  let(:deploy_key)    { create(:key) }
   let(:group)         { create(:group) }
   let(:group_member)  { create(:group_member) }
 
@@ -18,6 +19,8 @@ describe SystemHooksService, services: true do
     it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) }
     it { expect(event_data(key, :create)).to include(:username, :key, :id) }
     it { expect(event_data(key, :destroy)).to include(:username, :key, :id) }
+    it { expect(event_data(deploy_key, :create)).to include(:key, :id) }
+    it { expect(event_data(deploy_key, :destroy)).to include(:key, :id) }
 
     it do
       project.old_path_with_namespace = 'renamed_from_path'
diff --git a/spec/workers/build_email_worker_spec.rb b/spec/workers/build_email_worker_spec.rb
deleted file mode 100644
index 542e674c15015e9b9c6c51863022a6aac5955544..0000000000000000000000000000000000000000
--- a/spec/workers/build_email_worker_spec.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-require 'spec_helper'
-
-describe BuildEmailWorker do
-  include EmailHelpers
-  include RepoHelpers
-
-  let(:build) { create(:ci_build) }
-  let(:user) { create(:user) }
-  let(:data) { Gitlab::DataBuilder::Build.build(build) }
-
-  subject { BuildEmailWorker.new }
-
-  before do
-    allow(build).to receive(:execute_hooks).and_return(false)
-    build.success
-  end
-
-  describe "#perform" do
-    it "sends mail" do
-      subject.perform(build.id, [user.email], data.stringify_keys)
-
-      email = ActionMailer::Base.deliveries.last
-      expect(email.subject).to include('Build success for')
-      expect(email.to).to eq([user.email])
-    end
-
-    it "gracefully handles an input SMTP error" do
-      reset_delivered_emails!
-      allow(Notify).to receive(:build_success_email).and_raise(Net::SMTPFatalError)
-
-      subject.perform(build.id, [user.email], data.stringify_keys)
-
-      expect(ActionMailer::Base.deliveries.count).to eq(0)
-    end
-  end
-end
diff --git a/yarn.lock b/yarn.lock
index 391b1c7eccfb211d953c27e24de607cd3b35ed70..2500ddc6f6b8ca40ee1bdd99fd0d1f9e76a6f0bc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -473,6 +473,13 @@ babel-plugin-transform-decorators@^6.22.0:
     babel-template "^6.22.0"
     babel-types "^6.22.0"
 
+babel-plugin-transform-define@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-define/-/babel-plugin-transform-define-1.2.0.tgz#f036bda05162f29a542e434f585da1ccf1e7ec6a"
+  dependencies:
+    lodash.get "4.4.2"
+    traverse "0.6.6"
+
 babel-plugin-transform-es2015-arrow-functions@^6.22.0:
   version "6.22.0"
   resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221"
@@ -549,17 +556,17 @@ babel-plugin-transform-es2015-literals@^6.22.0:
   dependencies:
     babel-runtime "^6.22.0"
 
-babel-plugin-transform-es2015-modules-amd@^6.22.0:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.22.0.tgz#bf69cd34889a41c33d90dfb740e0091ccff52f21"
+babel-plugin-transform-es2015-modules-amd@^6.24.0:
+  version "6.24.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.0.tgz#a1911fb9b7ec7e05a43a63c5995007557bcf6a2e"
   dependencies:
-    babel-plugin-transform-es2015-modules-commonjs "^6.22.0"
+    babel-plugin-transform-es2015-modules-commonjs "^6.24.0"
     babel-runtime "^6.22.0"
     babel-template "^6.22.0"
 
-babel-plugin-transform-es2015-modules-commonjs@^6.22.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.23.0.tgz#cba7aa6379fb7ec99250e6d46de2973aaffa7b92"
+babel-plugin-transform-es2015-modules-commonjs@^6.24.0:
+  version "6.24.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.24.0.tgz#e921aefb72c2cc26cb03d107626156413222134f"
   dependencies:
     babel-plugin-transform-strict-mode "^6.22.0"
     babel-runtime "^6.22.0"
@@ -574,11 +581,11 @@ babel-plugin-transform-es2015-modules-systemjs@^6.22.0:
     babel-runtime "^6.22.0"
     babel-template "^6.23.0"
 
-babel-plugin-transform-es2015-modules-umd@^6.22.0:
-  version "6.23.0"
-  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.23.0.tgz#8d284ae2e19ed8fe21d2b1b26d6e7e0fcd94f0f1"
+babel-plugin-transform-es2015-modules-umd@^6.24.0:
+  version "6.24.0"
+  resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.0.tgz#fd5fa63521cae8d273927c3958afd7c067733450"
   dependencies:
-    babel-plugin-transform-es2015-modules-amd "^6.22.0"
+    babel-plugin-transform-es2015-modules-amd "^6.24.0"
     babel-runtime "^6.22.0"
     babel-template "^6.23.0"
 
@@ -669,9 +676,9 @@ babel-plugin-transform-strict-mode@^6.22.0:
     babel-runtime "^6.22.0"
     babel-types "^6.22.0"
 
-babel-preset-es2015@^6.22.0:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.22.0.tgz#af5a98ecb35eb8af764ad8a5a05eb36dc4386835"
+babel-preset-es2015@^6.24.0:
+  version "6.24.0"
+  resolved "https://registry.yarnpkg.com/babel-preset-es2015/-/babel-preset-es2015-6.24.0.tgz#c162d68b1932696e036cd3110dc1ccd303d2673a"
   dependencies:
     babel-plugin-check-es2015-constants "^6.22.0"
     babel-plugin-transform-es2015-arrow-functions "^6.22.0"
@@ -684,10 +691,10 @@ babel-preset-es2015@^6.22.0:
     babel-plugin-transform-es2015-for-of "^6.22.0"
     babel-plugin-transform-es2015-function-name "^6.22.0"
     babel-plugin-transform-es2015-literals "^6.22.0"
-    babel-plugin-transform-es2015-modules-amd "^6.22.0"
-    babel-plugin-transform-es2015-modules-commonjs "^6.22.0"
+    babel-plugin-transform-es2015-modules-amd "^6.24.0"
+    babel-plugin-transform-es2015-modules-commonjs "^6.24.0"
     babel-plugin-transform-es2015-modules-systemjs "^6.22.0"
-    babel-plugin-transform-es2015-modules-umd "^6.22.0"
+    babel-plugin-transform-es2015-modules-umd "^6.24.0"
     babel-plugin-transform-es2015-object-super "^6.22.0"
     babel-plugin-transform-es2015-parameters "^6.22.0"
     babel-plugin-transform-es2015-shorthand-properties "^6.22.0"
@@ -698,6 +705,27 @@ babel-preset-es2015@^6.22.0:
     babel-plugin-transform-es2015-unicode-regex "^6.22.0"
     babel-plugin-transform-regenerator "^6.22.0"
 
+babel-preset-es2016@^6.22.0:
+  version "6.22.0"
+  resolved "https://registry.yarnpkg.com/babel-preset-es2016/-/babel-preset-es2016-6.22.0.tgz#b061aaa3983d40c9fbacfa3743b5df37f336156c"
+  dependencies:
+    babel-plugin-transform-exponentiation-operator "^6.22.0"
+
+babel-preset-es2017@^6.22.0:
+  version "6.22.0"
+  resolved "https://registry.yarnpkg.com/babel-preset-es2017/-/babel-preset-es2017-6.22.0.tgz#de2f9da5a30c50d293fb54a0ba15d6ddc573f0f2"
+  dependencies:
+    babel-plugin-syntax-trailing-function-commas "^6.22.0"
+    babel-plugin-transform-async-to-generator "^6.22.0"
+
+babel-preset-latest@^6.24.0:
+  version "6.24.0"
+  resolved "https://registry.yarnpkg.com/babel-preset-latest/-/babel-preset-latest-6.24.0.tgz#a68d20f509edcc5d7433a48dfaebf7e4f2cd4cb7"
+  dependencies:
+    babel-preset-es2015 "^6.24.0"
+    babel-preset-es2016 "^6.22.0"
+    babel-preset-es2017 "^6.22.0"
+
 babel-preset-stage-2@^6.22.0:
   version "6.22.0"
   resolved "https://registry.yarnpkg.com/babel-preset-stage-2/-/babel-preset-stage-2-6.22.0.tgz#ccd565f19c245cade394b21216df704a73b27c07"
@@ -2900,6 +2928,10 @@ lodash.deburr@^4.0.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-4.1.0.tgz#ddb1bbb3ef07458c0177ba07de14422cb033ff9b"
 
+lodash.get@4.4.2:
+  version "4.4.2"
+  resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
+
 lodash.get@^3.7.0:
   version "3.7.0"
   resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-3.7.0.tgz#3ce68ae2c91683b281cc5394128303cbf75e691f"
@@ -4271,6 +4303,10 @@ tough-cookie@~2.3.0:
   dependencies:
     punycode "^1.4.1"
 
+traverse@0.6.6:
+  version "0.6.6"
+  resolved "https://registry.yarnpkg.com/traverse/-/traverse-0.6.6.tgz#cbdf560fd7b9af632502fed40f918c157ea97137"
+
 trim-right@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003"