diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index df3458b47dd06d13836322aa61f2d39be209cd4c..4e6fcab3808490e2e7a7f6460e0249930126c70a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-git-2.7-phantomjs-2.1-node-7.1"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.3-golang-1.8-git-2.7-phantomjs-2.1-node-7.1"
 
 cache:
   key: "ruby-233"
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index 34c2e097ba8608acc851360fc032af01e92c5e3e..6bb21e6a3af3e60da65c6964e2a150df605a0738 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -25,14 +25,20 @@ logs, and code as it's very hard to read otherwise.)
 
 #### Results of GitLab environment info
 
+<details>
+
 (For installations with omnibus-gitlab package run and paste the output of:
 `sudo gitlab-rake gitlab:env:info`)
 
 (For installations from source run and paste the output of:
 `sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
 
+</details>
+
 #### Results of GitLab application Check
 
+<details>
+
 (For installations with omnibus-gitlab package run and paste the output of:
 `sudo gitlab-rake gitlab:check SANITIZE=true`)
 
@@ -41,6 +47,8 @@ logs, and code as it's very hard to read otherwise.)
 
 (we will only investigate if the tests are passing)
 
+</details>
+
 ### Possible fixes
 
 (If you can, link to the line of code that might be responsible for the problem)
diff --git a/app/assets/javascripts/boards/components/board.js b/app/assets/javascripts/boards/components/board.js
index 93b8960da2edc10826d42a75aeeb2946d5fe1809..239eeacf2d74842e94c3de7a5958ef26fccbf6db 100644
--- a/app/assets/javascripts/boards/components/board.js
+++ b/app/assets/javascripts/boards/components/board.js
@@ -7,100 +7,98 @@ import boardBlankState from './board_blank_state';
 require('./board_delete');
 require('./board_list');
 
-(() => {
-  const Store = gl.issueBoards.BoardsStore;
+const Store = gl.issueBoards.BoardsStore;
 
-  window.gl = window.gl || {};
-  window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
 
-  gl.issueBoards.Board = Vue.extend({
-    template: '#js-board-template',
-    components: {
-      boardList,
-      'board-delete': gl.issueBoards.BoardDelete,
-      boardBlankState,
-    },
-    props: {
-      list: Object,
-      disabled: Boolean,
-      issueLinkBase: String,
-      rootPath: String,
-    },
-    data () {
-      return {
-        detailIssue: Store.detail,
-        filter: Store.filter,
-      };
-    },
-    watch: {
-      filter: {
-        handler() {
-          this.list.page = 1;
-          this.list.getIssues(true);
-        },
-        deep: true,
+gl.issueBoards.Board = Vue.extend({
+  template: '#js-board-template',
+  components: {
+    boardList,
+    'board-delete': gl.issueBoards.BoardDelete,
+    boardBlankState,
+  },
+  props: {
+    list: Object,
+    disabled: Boolean,
+    issueLinkBase: String,
+    rootPath: String,
+  },
+  data () {
+    return {
+      detailIssue: Store.detail,
+      filter: Store.filter,
+    };
+  },
+  watch: {
+    filter: {
+      handler() {
+        this.list.page = 1;
+        this.list.getIssues(true);
       },
-      detailIssue: {
-        handler () {
-          if (!Object.keys(this.detailIssue.issue).length) return;
+      deep: true,
+    },
+    detailIssue: {
+      handler () {
+        if (!Object.keys(this.detailIssue.issue).length) return;
 
-          const issue = this.list.findIssue(this.detailIssue.issue.id);
+        const issue = this.list.findIssue(this.detailIssue.issue.id);
 
-          if (issue) {
-            const offsetLeft = this.$el.offsetLeft;
-            const boardsList = document.querySelectorAll('.boards-list')[0];
-            const left = boardsList.scrollLeft - offsetLeft;
-            let right = (offsetLeft + this.$el.offsetWidth);
+        if (issue) {
+          const offsetLeft = this.$el.offsetLeft;
+          const boardsList = document.querySelectorAll('.boards-list')[0];
+          const left = boardsList.scrollLeft - offsetLeft;
+          let right = (offsetLeft + this.$el.offsetWidth);
 
-            if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
-              // -290 here because width of boardsList is animating so therefore
-              // getting the width here is incorrect
-              // 290 is the width of the sidebar
-              right -= (boardsList.offsetWidth - 290);
-            } else {
-              right -= boardsList.offsetWidth;
-            }
+          if (window.innerWidth > 768 && boardsList.classList.contains('is-compact')) {
+            // -290 here because width of boardsList is animating so therefore
+            // getting the width here is incorrect
+            // 290 is the width of the sidebar
+            right -= (boardsList.offsetWidth - 290);
+          } else {
+            right -= boardsList.offsetWidth;
+          }
 
-            if (right - boardsList.scrollLeft > 0) {
-              $(boardsList).animate({
-                scrollLeft: right
-              }, this.sortableOptions.animation);
-            } else if (left > 0) {
-              $(boardsList).animate({
-                scrollLeft: offsetLeft
-              }, this.sortableOptions.animation);
-            }
+          if (right - boardsList.scrollLeft > 0) {
+            $(boardsList).animate({
+              scrollLeft: right
+            }, this.sortableOptions.animation);
+          } else if (left > 0) {
+            $(boardsList).animate({
+              scrollLeft: offsetLeft
+            }, this.sortableOptions.animation);
           }
-        },
-        deep: true
-      }
-    },
-    methods: {
-      showNewIssueForm() {
-        this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
-      }
-    },
-    mounted () {
-      this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
-        disabled: this.disabled,
-        group: 'boards',
-        draggable: '.is-draggable',
-        handle: '.js-board-handle',
-        onEnd: (e) => {
-          gl.issueBoards.onEnd();
+        }
+      },
+      deep: true
+    }
+  },
+  methods: {
+    showNewIssueForm() {
+      this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
+    }
+  },
+  mounted () {
+    this.sortableOptions = gl.issueBoards.getBoardSortableDefaultOptions({
+      disabled: this.disabled,
+      group: 'boards',
+      draggable: '.is-draggable',
+      handle: '.js-board-handle',
+      onEnd: (e) => {
+        gl.issueBoards.onEnd();
 
-          if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
-            const order = this.sortable.toArray();
-            const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
+        if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
+          const order = this.sortable.toArray();
+          const list = Store.findList('id', parseInt(e.item.dataset.id, 10));
 
-            this.$nextTick(() => {
-              Store.moveList(list, order);
-            });
-          }
+          this.$nextTick(() => {
+            Store.moveList(list, order);
+          });
         }
-      });
+      }
+    });
 
-      this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
-    },
-  });
-})();
+    this.sortable = Sortable.create(this.$el.parentNode, this.sortableOptions);
+  },
+});
diff --git a/app/assets/javascripts/boards/components/board_delete.js b/app/assets/javascripts/boards/components/board_delete.js
index af621cfd57fda2270f0b69365c3e32fc2d0be1ac..8a1b177bba8c31110e40b004e6b6a452ce4b410e 100644
--- a/app/assets/javascripts/boards/components/board_delete.js
+++ b/app/assets/javascripts/boards/components/board_delete.js
@@ -2,22 +2,20 @@
 
 import Vue from 'vue';
 
-(() => {
-  window.gl = window.gl || {};
-  window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
 
-  gl.issueBoards.BoardDelete = Vue.extend({
-    props: {
-      list: Object
-    },
-    methods: {
-      deleteBoard () {
-        $(this.$el).tooltip('hide');
+gl.issueBoards.BoardDelete = Vue.extend({
+  props: {
+    list: Object
+  },
+  methods: {
+    deleteBoard () {
+      $(this.$el).tooltip('hide');
 
-        if (confirm('Are you sure you want to delete this list?')) {
-          this.list.destroy();
-        }
+      if (confirm('Are you sure you want to delete this list?')) {
+        this.list.destroy();
       }
     }
-  });
-})();
+  }
+});
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 3c080008244f360e046bb7c393e7a3c29e0e5f93..004bac09f598bcacf345a74d6eb948def8acc227 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -8,66 +8,64 @@ import Vue from 'vue';
 
 require('./sidebar/remove_issue');
 
-(() => {
-  const Store = gl.issueBoards.BoardsStore;
+const Store = gl.issueBoards.BoardsStore;
 
-  window.gl = window.gl || {};
-  window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
 
-  gl.issueBoards.BoardSidebar = Vue.extend({
-    props: {
-      currentUser: Object
-    },
-    data() {
-      return {
-        detail: Store.detail,
-        issue: {},
-        list: {},
-      };
-    },
-    computed: {
-      showSidebar () {
-        return Object.keys(this.issue).length;
-      }
-    },
-    watch: {
-      detail: {
-        handler () {
-          if (this.issue.id !== this.detail.issue.id) {
-            $('.js-issue-board-sidebar', this.$el).each((i, el) => {
-              $(el).data('glDropdown').clearMenu();
-            });
-          }
-
-          this.issue = this.detail.issue;
-          this.list = this.detail.list;
-        },
-        deep: true
-      },
-      issue () {
-        if (this.showSidebar) {
-          this.$nextTick(() => {
-            $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
-            $('.right-sidebar').getNiceScroll().resize();
+gl.issueBoards.BoardSidebar = Vue.extend({
+  props: {
+    currentUser: Object
+  },
+  data() {
+    return {
+      detail: Store.detail,
+      issue: {},
+      list: {},
+    };
+  },
+  computed: {
+    showSidebar () {
+      return Object.keys(this.issue).length;
+    }
+  },
+  watch: {
+    detail: {
+      handler () {
+        if (this.issue.id !== this.detail.issue.id) {
+          $('.js-issue-board-sidebar', this.$el).each((i, el) => {
+            $(el).data('glDropdown').clearMenu();
           });
         }
-      }
+
+        this.issue = this.detail.issue;
+        this.list = this.detail.list;
+      },
+      deep: true
     },
-    methods: {
-      closeSidebar () {
-        this.detail.issue = {};
+    issue () {
+      if (this.showSidebar) {
+        this.$nextTick(() => {
+          $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
+          $('.right-sidebar').getNiceScroll().resize();
+        });
       }
-    },
-    mounted () {
-      new IssuableContext(this.currentUser);
-      new MilestoneSelect();
-      new gl.DueDateSelectors();
-      new LabelsSelect();
-      new Sidebar();
-      gl.Subscription.bindAll('.subscription');
-    },
-    components: {
-      removeBtn: gl.issueBoards.RemoveIssueBtn,
-    },
-  });
-})();
+    }
+  },
+  methods: {
+    closeSidebar () {
+      this.detail.issue = {};
+    }
+  },
+  mounted () {
+    new IssuableContext(this.currentUser);
+    new MilestoneSelect();
+    new gl.DueDateSelectors();
+    new LabelsSelect();
+    new Sidebar();
+    gl.Subscription.bindAll('.subscription');
+  },
+  components: {
+    removeBtn: gl.issueBoards.RemoveIssueBtn,
+  },
+});
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.js b/app/assets/javascripts/boards/components/issue_card_inner.js
index e48d3344a2b5d99960176441a8128051cc090181..fc154ee7b8b031f6d79d1454c0c0ad133d8b9001 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.js
+++ b/app/assets/javascripts/boards/components/issue_card_inner.js
@@ -1,141 +1,139 @@
 import Vue from 'vue';
 import eventHub from '../eventhub';
 
-(() => {
-  const Store = gl.issueBoards.BoardsStore;
+const Store = gl.issueBoards.BoardsStore;
 
-  window.gl = window.gl || {};
-  window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
 
-  gl.issueBoards.IssueCardInner = Vue.extend({
-    props: {
-      issue: {
-        type: Object,
-        required: true,
-      },
-      issueLinkBase: {
-        type: String,
-        required: true,
-      },
-      list: {
-        type: Object,
-        required: false,
-        default: () => ({}),
-      },
-      rootPath: {
-        type: String,
-        required: true,
-      },
-      updateFilters: {
-        type: Boolean,
-        required: false,
-        default: false,
-      },
+gl.issueBoards.IssueCardInner = Vue.extend({
+  props: {
+    issue: {
+      type: Object,
+      required: true,
     },
-    computed: {
-      cardUrl() {
-        return `${this.issueLinkBase}/${this.issue.id}`;
-      },
-      assigneeUrl() {
-        return `${this.rootPath}${this.issue.assignee.username}`;
-      },
-      assigneeUrlTitle() {
-        return `Assigned to ${this.issue.assignee.name}`;
-      },
-      avatarUrlTitle() {
-        return `Avatar for ${this.issue.assignee.name}`;
-      },
-      issueId() {
-        return `#${this.issue.id}`;
-      },
-      showLabelFooter() {
-        return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
-      },
+    issueLinkBase: {
+      type: String,
+      required: true,
     },
-    methods: {
-      showLabel(label) {
-        if (!this.list) return true;
+    list: {
+      type: Object,
+      required: false,
+      default: () => ({}),
+    },
+    rootPath: {
+      type: String,
+      required: true,
+    },
+    updateFilters: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+  },
+  computed: {
+    cardUrl() {
+      return `${this.issueLinkBase}/${this.issue.id}`;
+    },
+    assigneeUrl() {
+      return `${this.rootPath}${this.issue.assignee.username}`;
+    },
+    assigneeUrlTitle() {
+      return `Assigned to ${this.issue.assignee.name}`;
+    },
+    avatarUrlTitle() {
+      return `Avatar for ${this.issue.assignee.name}`;
+    },
+    issueId() {
+      return `#${this.issue.id}`;
+    },
+    showLabelFooter() {
+      return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
+    },
+  },
+  methods: {
+    showLabel(label) {
+      if (!this.list) return true;
 
-        return !this.list.label || label.id !== this.list.label.id;
-      },
-      filterByLabel(label, e) {
-        if (!this.updateFilters) return;
+      return !this.list.label || label.id !== this.list.label.id;
+    },
+    filterByLabel(label, e) {
+      if (!this.updateFilters) return;
 
-        const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
-        const labelTitle = encodeURIComponent(label.title);
-        const param = `label_name[]=${labelTitle}`;
-        const labelIndex = filterPath.indexOf(param);
-        $(e.currentTarget).tooltip('hide');
+      const filterPath = gl.issueBoards.BoardsStore.filter.path.split('&');
+      const labelTitle = encodeURIComponent(label.title);
+      const param = `label_name[]=${labelTitle}`;
+      const labelIndex = filterPath.indexOf(param);
+      $(e.currentTarget).tooltip('hide');
 
-        if (labelIndex === -1) {
-          filterPath.push(param);
-        } else {
-          filterPath.splice(labelIndex, 1);
-        }
+      if (labelIndex === -1) {
+        filterPath.push(param);
+      } else {
+        filterPath.splice(labelIndex, 1);
+      }
 
-        gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
+      gl.issueBoards.BoardsStore.filter.path = filterPath.join('&');
 
-        Store.updateFiltersUrl();
+      Store.updateFiltersUrl();
 
-        eventHub.$emit('updateTokens');
-      },
-      labelStyle(label) {
-        return {
-          backgroundColor: label.color,
-          color: label.textColor,
-        };
-      },
+      eventHub.$emit('updateTokens');
+    },
+    labelStyle(label) {
+      return {
+        backgroundColor: label.color,
+        color: label.textColor,
+      };
     },
-    template: `
-      <div>
-        <div class="card-header">
-          <h4 class="card-title">
-            <i
-              class="fa fa-eye-slash confidential-icon"
-              v-if="issue.confidential"
-              aria-hidden="true"
-            />
-            <a
-              class="js-no-trigger"
-              :href="cardUrl"
-              :title="issue.title">{{ issue.title }}</a>
-            <span
-              class="card-number"
-              v-if="issue.id"
-            >
-              {{ issueId }}
-            </span>
-          </h4>
+  },
+  template: `
+    <div>
+      <div class="card-header">
+        <h4 class="card-title">
+          <i
+            class="fa fa-eye-slash confidential-icon"
+            v-if="issue.confidential"
+            aria-hidden="true"
+          />
           <a
-            class="card-assignee has-tooltip js-no-trigger"
-            :href="assigneeUrl"
-            :title="assigneeUrlTitle"
-            v-if="issue.assignee"
-            data-container="body"
+            class="js-no-trigger"
+            :href="cardUrl"
+            :title="issue.title">{{ issue.title }}</a>
+          <span
+            class="card-number"
+            v-if="issue.id"
           >
-            <img
-              class="avatar avatar-inline s20 js-no-trigger"
-              :src="issue.assignee.avatar"
-              width="20"
-              height="20"
-              :alt="avatarUrlTitle"
-            />
-          </a>
-        </div>
-        <div class="card-footer" v-if="showLabelFooter">
-          <button
-            class="label color-label has-tooltip js-no-trigger"
-            v-for="label in issue.labels"
-            type="button"
-            v-if="showLabel(label)"
-            @click="filterByLabel(label, $event)"
-            :style="labelStyle(label)"
-            :title="label.description"
-            data-container="body">
-            {{ label.title }}
-          </button>
-        </div>
+            {{ issueId }}
+          </span>
+        </h4>
+        <a
+          class="card-assignee has-tooltip js-no-trigger"
+          :href="assigneeUrl"
+          :title="assigneeUrlTitle"
+          v-if="issue.assignee"
+          data-container="body"
+        >
+          <img
+            class="avatar avatar-inline s20 js-no-trigger"
+            :src="issue.assignee.avatar"
+            width="20"
+            height="20"
+            :alt="avatarUrlTitle"
+          />
+        </a>
+      </div>
+      <div class="card-footer" v-if="showLabelFooter">
+        <button
+          class="label color-label has-tooltip js-no-trigger"
+          v-for="label in issue.labels"
+          type="button"
+          v-if="showLabel(label)"
+          @click="filterByLabel(label, $event)"
+          :style="labelStyle(label)"
+          :title="label.description"
+          data-container="body">
+          {{ label.title }}
+        </button>
       </div>
-    `,
-  });
-})();
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/empty_state.js b/app/assets/javascripts/boards/components/modal/empty_state.js
index 823319df6e7aa87ee995a69905d0aee3f02839a3..13569df0c2059068cae5728b064e96e09a36ecca 100644
--- a/app/assets/javascripts/boards/components/modal/empty_state.js
+++ b/app/assets/javascripts/boards/components/modal/empty_state.js
@@ -1,71 +1,69 @@
 import Vue from 'vue';
 
-(() => {
-  const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
 
-  gl.issueBoards.ModalEmptyState = Vue.extend({
-    mixins: [gl.issueBoards.ModalMixins],
-    data() {
-      return ModalStore.store;
+gl.issueBoards.ModalEmptyState = Vue.extend({
+  mixins: [gl.issueBoards.ModalMixins],
+  data() {
+    return ModalStore.store;
+  },
+  props: {
+    image: {
+      type: String,
+      required: true,
     },
-    props: {
-      image: {
-        type: String,
-        required: true,
-      },
-      newIssuePath: {
-        type: String,
-        required: true,
-      },
+    newIssuePath: {
+      type: String,
+      required: true,
     },
-    computed: {
-      contents() {
-        const obj = {
-          title: 'You haven\'t added any issues to your project yet',
-          content: `
-            An issue can be a bug, a todo or a feature request that needs to be
-            discussed in a project. Besides, issues are searchable and filterable.
-          `,
-        };
+  },
+  computed: {
+    contents() {
+      const obj = {
+        title: 'You haven\'t added any issues to your project yet',
+        content: `
+          An issue can be a bug, a todo or a feature request that needs to be
+          discussed in a project. Besides, issues are searchable and filterable.
+        `,
+      };
 
-        if (this.activeTab === 'selected') {
-          obj.title = 'You haven\'t selected any issues yet';
-          obj.content = `
-            Go back to <strong>Open issues</strong> and select some issues
-            to add to your board.
-          `;
-        }
+      if (this.activeTab === 'selected') {
+        obj.title = 'You haven\'t selected any issues yet';
+        obj.content = `
+          Go back to <strong>Open issues</strong> and select some issues
+          to add to your board.
+        `;
+      }
 
-        return obj;
-      },
+      return obj;
     },
-    template: `
-      <section class="empty-state">
-        <div class="row">
-          <div class="col-xs-12 col-sm-6 col-sm-push-6">
-            <aside class="svg-content" v-html="image"></aside>
-          </div>
-          <div class="col-xs-12 col-sm-6 col-sm-pull-6">
-            <div class="text-content">
-              <h4>{{ contents.title }}</h4>
-              <p v-html="contents.content"></p>
-              <a
-                :href="newIssuePath"
-                class="btn btn-success btn-inverted"
-                v-if="activeTab === 'all'">
-                New issue
-              </a>
-              <button
-                type="button"
-                class="btn btn-default"
-                @click="changeTab('all')"
-                v-if="activeTab === 'selected'">
-                Open issues
-              </button>
-            </div>
+  },
+  template: `
+    <section class="empty-state">
+      <div class="row">
+        <div class="col-xs-12 col-sm-6 col-sm-push-6">
+          <aside class="svg-content" v-html="image"></aside>
+        </div>
+        <div class="col-xs-12 col-sm-6 col-sm-pull-6">
+          <div class="text-content">
+            <h4>{{ contents.title }}</h4>
+            <p v-html="contents.content"></p>
+            <a
+              :href="newIssuePath"
+              class="btn btn-success btn-inverted"
+              v-if="activeTab === 'all'">
+              New issue
+            </a>
+            <button
+              type="button"
+              class="btn btn-default"
+              @click="changeTab('all')"
+              v-if="activeTab === 'selected'">
+              Open issues
+            </button>
           </div>
         </div>
-      </section>
-    `,
-  });
-})();
+      </div>
+    </section>
+  `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/footer.js b/app/assets/javascripts/boards/components/modal/footer.js
index 887ce37309615dc01bd357c1e55a7eb9810cf834..ccd270b27dab42e6962567ae03423c0c0116a4d4 100644
--- a/app/assets/javascripts/boards/components/modal/footer.js
+++ b/app/assets/javascripts/boards/components/modal/footer.js
@@ -5,80 +5,78 @@ import Vue from 'vue';
 
 require('./lists_dropdown');
 
-(() => {
-  const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
 
-  gl.issueBoards.ModalFooter = Vue.extend({
-    mixins: [gl.issueBoards.ModalMixins],
-    data() {
-      return {
-        modal: ModalStore.store,
-        state: gl.issueBoards.BoardsStore.state,
-      };
+gl.issueBoards.ModalFooter = Vue.extend({
+  mixins: [gl.issueBoards.ModalMixins],
+  data() {
+    return {
+      modal: ModalStore.store,
+      state: gl.issueBoards.BoardsStore.state,
+    };
+  },
+  computed: {
+    submitDisabled() {
+      return !ModalStore.selectedCount();
     },
-    computed: {
-      submitDisabled() {
-        return !ModalStore.selectedCount();
-      },
-      submitText() {
-        const count = ModalStore.selectedCount();
+    submitText() {
+      const count = ModalStore.selectedCount();
 
-        return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
-      },
+      return `Add ${count > 0 ? count : ''} ${gl.text.pluralize('issue', count)}`;
     },
-    methods: {
-      addIssues() {
-        const list = this.modal.selectedList || this.state.lists[0];
-        const selectedIssues = ModalStore.getSelectedIssues();
-        const issueIds = selectedIssues.map(issue => issue.globalId);
+  },
+  methods: {
+    addIssues() {
+      const list = this.modal.selectedList || this.state.lists[0];
+      const selectedIssues = ModalStore.getSelectedIssues();
+      const issueIds = selectedIssues.map(issue => issue.globalId);
 
-        // Post the data to the backend
-        gl.boardService.bulkUpdate(issueIds, {
-          add_label_ids: [list.label.id],
-        }).catch(() => {
-          new Flash('Failed to update issues, please try again.', 'alert');
+      // Post the data to the backend
+      gl.boardService.bulkUpdate(issueIds, {
+        add_label_ids: [list.label.id],
+      }).catch(() => {
+        new Flash('Failed to update issues, please try again.', 'alert');
 
-          selectedIssues.forEach((issue) => {
-            list.removeIssue(issue);
-            list.issuesSize -= 1;
-          });
-        });
-
-        // Add the issues on the frontend
         selectedIssues.forEach((issue) => {
-          list.addIssue(issue);
-          list.issuesSize += 1;
+          list.removeIssue(issue);
+          list.issuesSize -= 1;
         });
+      });
 
-        this.toggleModal(false);
-      },
-    },
-    components: {
-      'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
+      // Add the issues on the frontend
+      selectedIssues.forEach((issue) => {
+        list.addIssue(issue);
+        list.issuesSize += 1;
+      });
+
+      this.toggleModal(false);
     },
-    template: `
-      <footer
-        class="form-actions add-issues-footer">
-        <div class="pull-left">
-          <button
-            class="btn btn-success"
-            type="button"
-            :disabled="submitDisabled"
-            @click="addIssues">
-            {{ submitText }}
-          </button>
-          <span class="inline add-issues-footer-to-list">
-            to list
-          </span>
-          <lists-dropdown></lists-dropdown>
-        </div>
+  },
+  components: {
+    'lists-dropdown': gl.issueBoards.ModalFooterListsDropdown,
+  },
+  template: `
+    <footer
+      class="form-actions add-issues-footer">
+      <div class="pull-left">
         <button
-          class="btn btn-default pull-right"
+          class="btn btn-success"
           type="button"
-          @click="toggleModal(false)">
-          Cancel
+          :disabled="submitDisabled"
+          @click="addIssues">
+          {{ submitText }}
         </button>
-      </footer>
-    `,
-  });
-})();
+        <span class="inline add-issues-footer-to-list">
+          to list
+        </span>
+        <lists-dropdown></lists-dropdown>
+      </div>
+      <button
+        class="btn btn-default pull-right"
+        type="button"
+        @click="toggleModal(false)">
+        Cancel
+      </button>
+    </footer>
+  `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/header.js b/app/assets/javascripts/boards/components/modal/header.js
index 116e29cd1778882203e133da9b990197f9316a35..e2b3f9ae7e2234fa50e267fc6cc1689b3a9ac5a4 100644
--- a/app/assets/javascripts/boards/components/modal/header.js
+++ b/app/assets/javascripts/boards/components/modal/header.js
@@ -3,80 +3,78 @@ import modalFilters from './filters';
 
 require('./tabs');
 
-(() => {
-  const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
 
-  gl.issueBoards.ModalHeader = Vue.extend({
-    mixins: [gl.issueBoards.ModalMixins],
-    props: {
-      projectId: {
-        type: Number,
-        required: true,
-      },
-      milestonePath: {
-        type: String,
-        required: true,
-      },
-      labelPath: {
-        type: String,
-        required: true,
-      },
+gl.issueBoards.ModalHeader = Vue.extend({
+  mixins: [gl.issueBoards.ModalMixins],
+  props: {
+    projectId: {
+      type: Number,
+      required: true,
     },
-    data() {
-      return ModalStore.store;
+    milestonePath: {
+      type: String,
+      required: true,
     },
-    computed: {
-      selectAllText() {
-        if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
-          return 'Select all';
-        }
-
-        return 'Deselect all';
-      },
-      showSearch() {
-        return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
-      },
+    labelPath: {
+      type: String,
+      required: true,
     },
-    methods: {
-      toggleAll() {
-        this.$refs.selectAllBtn.blur();
+  },
+  data() {
+    return ModalStore.store;
+  },
+  computed: {
+    selectAllText() {
+      if (ModalStore.selectedCount() !== this.issues.length || this.issues.length === 0) {
+        return 'Select all';
+      }
 
-        ModalStore.toggleAll();
-      },
+      return 'Deselect all';
+    },
+    showSearch() {
+      return this.activeTab === 'all' && !this.loading && this.issuesCount > 0;
     },
-    components: {
-      'modal-tabs': gl.issueBoards.ModalTabs,
-      modalFilters,
+  },
+  methods: {
+    toggleAll() {
+      this.$refs.selectAllBtn.blur();
+
+      ModalStore.toggleAll();
     },
-    template: `
-      <div>
-        <header class="add-issues-header form-actions">
-          <h2>
-            Add issues
-            <button
-              type="button"
-              class="close"
-              data-dismiss="modal"
-              aria-label="Close"
-              @click="toggleModal(false)">
-              <span aria-hidden="true">×</span>
-            </button>
-          </h2>
-        </header>
-        <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
-        <div
-          class="add-issues-search append-bottom-10"
-          v-if="showSearch">
-          <modal-filters :store="filter" />
+  },
+  components: {
+    'modal-tabs': gl.issueBoards.ModalTabs,
+    modalFilters,
+  },
+  template: `
+    <div>
+      <header class="add-issues-header form-actions">
+        <h2>
+          Add issues
           <button
             type="button"
-            class="btn btn-success btn-inverted prepend-left-10"
-            ref="selectAllBtn"
-            @click="toggleAll">
-            {{ selectAllText }}
+            class="close"
+            data-dismiss="modal"
+            aria-label="Close"
+            @click="toggleModal(false)">
+            <span aria-hidden="true">×</span>
           </button>
-        </div>
+        </h2>
+      </header>
+      <modal-tabs v-if="!loading && issuesCount > 0"></modal-tabs>
+      <div
+        class="add-issues-search append-bottom-10"
+        v-if="showSearch">
+        <modal-filters :store="filter" />
+        <button
+          type="button"
+          class="btn btn-success btn-inverted prepend-left-10"
+          ref="selectAllBtn"
+          @click="toggleAll">
+          {{ selectAllText }}
+        </button>
       </div>
-    `,
-  });
-})();
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/index.js b/app/assets/javascripts/boards/components/modal/index.js
index 91c08cde13a92909b53c2d11350b508775724b81..fb0aac3c0e4944519c27acd80ffec20936ad3405 100644
--- a/app/assets/javascripts/boards/components/modal/index.js
+++ b/app/assets/javascripts/boards/components/modal/index.js
@@ -8,160 +8,158 @@ require('./list');
 require('./footer');
 require('./empty_state');
 
-(() => {
-  const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
 
-  gl.issueBoards.IssuesModal = Vue.extend({
-    props: {
-      blankStateImage: {
-        type: String,
-        required: true,
-      },
-      newIssuePath: {
-        type: String,
-        required: true,
-      },
-      issueLinkBase: {
-        type: String,
-        required: true,
-      },
-      rootPath: {
-        type: String,
-        required: true,
-      },
-      projectId: {
-        type: Number,
-        required: true,
-      },
-      milestonePath: {
-        type: String,
-        required: true,
-      },
-      labelPath: {
-        type: String,
-        required: true,
-      },
+gl.issueBoards.IssuesModal = Vue.extend({
+  props: {
+    blankStateImage: {
+      type: String,
+      required: true,
     },
-    data() {
-      return ModalStore.store;
+    newIssuePath: {
+      type: String,
+      required: true,
     },
-    watch: {
-      page() {
-        this.loadIssues();
-      },
-      showAddIssuesModal() {
-        if (this.showAddIssuesModal && !this.issues.length) {
-          this.loading = true;
+    issueLinkBase: {
+      type: String,
+      required: true,
+    },
+    rootPath: {
+      type: String,
+      required: true,
+    },
+    projectId: {
+      type: Number,
+      required: true,
+    },
+    milestonePath: {
+      type: String,
+      required: true,
+    },
+    labelPath: {
+      type: String,
+      required: true,
+    },
+  },
+  data() {
+    return ModalStore.store;
+  },
+  watch: {
+    page() {
+      this.loadIssues();
+    },
+    showAddIssuesModal() {
+      if (this.showAddIssuesModal && !this.issues.length) {
+        this.loading = true;
+
+        this.loadIssues()
+          .then(() => {
+            this.loading = false;
+          });
+      } else if (!this.showAddIssuesModal) {
+        this.issues = [];
+        this.selectedIssues = [];
+        this.issuesCount = false;
+      }
+    },
+    filter: {
+      handler() {
+        if (this.$el.tagName) {
+          this.page = 1;
+          this.filterLoading = true;
 
-          this.loadIssues()
+          this.loadIssues(true)
             .then(() => {
-              this.loading = false;
+              this.filterLoading = false;
             });
-        } else if (!this.showAddIssuesModal) {
-          this.issues = [];
-          this.selectedIssues = [];
-          this.issuesCount = false;
         }
       },
-      filter: {
-        handler() {
-          if (this.$el.tagName) {
-            this.page = 1;
-            this.filterLoading = true;
-
-            this.loadIssues(true)
-              .then(() => {
-                this.filterLoading = false;
-              });
-          }
-        },
-        deep: true,
-      },
+      deep: true,
     },
-    methods: {
-      loadIssues(clearIssues = false) {
-        if (!this.showAddIssuesModal) return false;
-
-        return gl.boardService.getBacklog(queryData(this.filter.path, {
-          page: this.page,
-          per: this.perPage,
-        })).then((res) => {
-          const data = res.json();
-
-          if (clearIssues) {
-            this.issues = [];
-          }
+  },
+  methods: {
+    loadIssues(clearIssues = false) {
+      if (!this.showAddIssuesModal) return false;
 
-          data.issues.forEach((issueObj) => {
-            const issue = new ListIssue(issueObj);
-            const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
-            issue.selected = !!foundSelectedIssue;
+      return gl.boardService.getBacklog(queryData(this.filter.path, {
+        page: this.page,
+        per: this.perPage,
+      })).then((res) => {
+        const data = res.json();
 
-            this.issues.push(issue);
-          });
+        if (clearIssues) {
+          this.issues = [];
+        }
 
-          this.loadingNewPage = false;
+        data.issues.forEach((issueObj) => {
+          const issue = new ListIssue(issueObj);
+          const foundSelectedIssue = ModalStore.findSelectedIssue(issue);
+          issue.selected = !!foundSelectedIssue;
 
-          if (!this.issuesCount) {
-            this.issuesCount = data.size;
-          }
+          this.issues.push(issue);
         });
-      },
-    },
-    computed: {
-      showList() {
-        if (this.activeTab === 'selected') {
-          return this.selectedIssues.length > 0;
-        }
 
-        return this.issuesCount > 0;
-      },
-      showEmptyState() {
-        if (!this.loading && this.issuesCount === 0) {
-          return true;
-        }
+        this.loadingNewPage = false;
 
-        return this.activeTab === 'selected' && this.selectedIssues.length === 0;
-      },
+        if (!this.issuesCount) {
+          this.issuesCount = data.size;
+        }
+      });
     },
-    created() {
-      this.page = 1;
+  },
+  computed: {
+    showList() {
+      if (this.activeTab === 'selected') {
+        return this.selectedIssues.length > 0;
+      }
+
+      return this.issuesCount > 0;
     },
-    components: {
-      'modal-header': gl.issueBoards.ModalHeader,
-      'modal-list': gl.issueBoards.ModalList,
-      'modal-footer': gl.issueBoards.ModalFooter,
-      'empty-state': gl.issueBoards.ModalEmptyState,
+    showEmptyState() {
+      if (!this.loading && this.issuesCount === 0) {
+        return true;
+      }
+
+      return this.activeTab === 'selected' && this.selectedIssues.length === 0;
     },
-    template: `
-      <div
-        class="add-issues-modal"
-        v-if="showAddIssuesModal">
-        <div class="add-issues-container">
-          <modal-header
-            :project-id="projectId"
-            :milestone-path="milestonePath"
-            :label-path="labelPath">
-          </modal-header>
-          <modal-list
-            :image="blankStateImage"
-            :issue-link-base="issueLinkBase"
-            :root-path="rootPath"
-            v-if="!loading && showList && !filterLoading"></modal-list>
-          <empty-state
-            v-if="showEmptyState"
-            :image="blankStateImage"
-            :new-issue-path="newIssuePath"></empty-state>
-          <section
-            class="add-issues-list text-center"
-            v-if="loading || filterLoading">
-            <div class="add-issues-list-loading">
-              <i class="fa fa-spinner fa-spin"></i>
-            </div>
-          </section>
-          <modal-footer></modal-footer>
-        </div>
+  },
+  created() {
+    this.page = 1;
+  },
+  components: {
+    'modal-header': gl.issueBoards.ModalHeader,
+    'modal-list': gl.issueBoards.ModalList,
+    'modal-footer': gl.issueBoards.ModalFooter,
+    'empty-state': gl.issueBoards.ModalEmptyState,
+  },
+  template: `
+    <div
+      class="add-issues-modal"
+      v-if="showAddIssuesModal">
+      <div class="add-issues-container">
+        <modal-header
+          :project-id="projectId"
+          :milestone-path="milestonePath"
+          :label-path="labelPath">
+        </modal-header>
+        <modal-list
+          :image="blankStateImage"
+          :issue-link-base="issueLinkBase"
+          :root-path="rootPath"
+          v-if="!loading && showList && !filterLoading"></modal-list>
+        <empty-state
+          v-if="showEmptyState"
+          :image="blankStateImage"
+          :new-issue-path="newIssuePath"></empty-state>
+        <section
+          class="add-issues-list text-center"
+          v-if="loading || filterLoading">
+          <div class="add-issues-list-loading">
+            <i class="fa fa-spinner fa-spin"></i>
+          </div>
+        </section>
+        <modal-footer></modal-footer>
       </div>
-    `,
-  });
-})();
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/list.js b/app/assets/javascripts/boards/components/modal/list.js
index aba56d4aa3127136621da960969810497ed77201..363269c0d5d1dcb617d4a9415c9154409d4aaf92 100644
--- a/app/assets/javascripts/boards/components/modal/list.js
+++ b/app/assets/javascripts/boards/components/modal/list.js
@@ -3,159 +3,157 @@
 
 import Vue from 'vue';
 
-(() => {
-  const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
 
-  gl.issueBoards.ModalList = Vue.extend({
-    props: {
-      issueLinkBase: {
-        type: String,
-        required: true,
-      },
-      rootPath: {
-        type: String,
-        required: true,
-      },
-      image: {
-        type: String,
-        required: true,
-      },
+gl.issueBoards.ModalList = Vue.extend({
+  props: {
+    issueLinkBase: {
+      type: String,
+      required: true,
     },
-    data() {
-      return ModalStore.store;
+    rootPath: {
+      type: String,
+      required: true,
     },
-    watch: {
-      activeTab() {
-        if (this.activeTab === 'all') {
-          ModalStore.purgeUnselectedIssues();
-        }
-      },
+    image: {
+      type: String,
+      required: true,
     },
-    computed: {
-      loopIssues() {
-        if (this.activeTab === 'all') {
-          return this.issues;
-        }
-
-        return this.selectedIssues;
-      },
-      groupedIssues() {
-        const groups = [];
-        this.loopIssues.forEach((issue, i) => {
-          const index = i % this.columns;
-
-          if (!groups[index]) {
-            groups.push([]);
-          }
-
-          groups[index].push(issue);
-        });
+  },
+  data() {
+    return ModalStore.store;
+  },
+  watch: {
+    activeTab() {
+      if (this.activeTab === 'all') {
+        ModalStore.purgeUnselectedIssues();
+      }
+    },
+  },
+  computed: {
+    loopIssues() {
+      if (this.activeTab === 'all') {
+        return this.issues;
+      }
 
-        return groups;
-      },
+      return this.selectedIssues;
     },
-    methods: {
-      scrollHandler() {
-        const currentPage = Math.floor(this.issues.length / this.perPage);
+    groupedIssues() {
+      const groups = [];
+      this.loopIssues.forEach((issue, i) => {
+        const index = i % this.columns;
 
-        if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
-          && currentPage === this.page) {
-          this.loadingNewPage = true;
-          this.page += 1;
+        if (!groups[index]) {
+          groups.push([]);
         }
-      },
-      toggleIssue(e, issue) {
-        if (e.target.tagName !== 'A') {
-          ModalStore.toggleIssue(issue);
-        }
-      },
-      listHeight() {
-        return this.$refs.list.getBoundingClientRect().height;
-      },
-      scrollHeight() {
-        return this.$refs.list.scrollHeight;
-      },
-      scrollTop() {
-        return this.$refs.list.scrollTop + this.listHeight();
-      },
-      showIssue(issue) {
-        if (this.activeTab === 'all') return true;
-
-        const index = ModalStore.selectedIssueIndex(issue);
 
-        return index !== -1;
-      },
-      setColumnCount() {
-        const breakpoint = bp.getBreakpointSize();
+        groups[index].push(issue);
+      });
 
-        if (breakpoint === 'lg' || breakpoint === 'md') {
-          this.columns = 3;
-        } else if (breakpoint === 'sm') {
-          this.columns = 2;
-        } else {
-          this.columns = 1;
-        }
-      },
+      return groups;
     },
-    mounted() {
-      this.scrollHandlerWrapper = this.scrollHandler.bind(this);
-      this.setColumnCountWrapper = this.setColumnCount.bind(this);
-      this.setColumnCount();
+  },
+  methods: {
+    scrollHandler() {
+      const currentPage = Math.floor(this.issues.length / this.perPage);
 
-      this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
-      window.addEventListener('resize', this.setColumnCountWrapper);
+      if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
+        && currentPage === this.page) {
+        this.loadingNewPage = true;
+        this.page += 1;
+      }
+    },
+    toggleIssue(e, issue) {
+      if (e.target.tagName !== 'A') {
+        ModalStore.toggleIssue(issue);
+      }
+    },
+    listHeight() {
+      return this.$refs.list.getBoundingClientRect().height;
     },
-    beforeDestroy() {
-      this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
-      window.removeEventListener('resize', this.setColumnCountWrapper);
+    scrollHeight() {
+      return this.$refs.list.scrollHeight;
     },
-    components: {
-      'issue-card-inner': gl.issueBoards.IssueCardInner,
+    scrollTop() {
+      return this.$refs.list.scrollTop + this.listHeight();
     },
-    template: `
-      <section
-        class="add-issues-list add-issues-list-columns"
-        ref="list">
+    showIssue(issue) {
+      if (this.activeTab === 'all') return true;
+
+      const index = ModalStore.selectedIssueIndex(issue);
+
+      return index !== -1;
+    },
+    setColumnCount() {
+      const breakpoint = bp.getBreakpointSize();
+
+      if (breakpoint === 'lg' || breakpoint === 'md') {
+        this.columns = 3;
+      } else if (breakpoint === 'sm') {
+        this.columns = 2;
+      } else {
+        this.columns = 1;
+      }
+    },
+  },
+  mounted() {
+    this.scrollHandlerWrapper = this.scrollHandler.bind(this);
+    this.setColumnCountWrapper = this.setColumnCount.bind(this);
+    this.setColumnCount();
+
+    this.$refs.list.addEventListener('scroll', this.scrollHandlerWrapper);
+    window.addEventListener('resize', this.setColumnCountWrapper);
+  },
+  beforeDestroy() {
+    this.$refs.list.removeEventListener('scroll', this.scrollHandlerWrapper);
+    window.removeEventListener('resize', this.setColumnCountWrapper);
+  },
+  components: {
+    'issue-card-inner': gl.issueBoards.IssueCardInner,
+  },
+  template: `
+    <section
+      class="add-issues-list add-issues-list-columns"
+      ref="list">
+      <div
+        class="empty-state add-issues-empty-state-filter text-center"
+        v-if="issuesCount > 0 && issues.length === 0">
         <div
-          class="empty-state add-issues-empty-state-filter text-center"
-          v-if="issuesCount > 0 && issues.length === 0">
-          <div
-            class="svg-content"
-            v-html="image">
-          </div>
-          <div class="text-content">
-            <h4>
-              There are no issues to show.
-            </h4>
-          </div>
+          class="svg-content"
+          v-html="image">
+        </div>
+        <div class="text-content">
+          <h4>
+            There are no issues to show.
+          </h4>
         </div>
+      </div>
+      <div
+        v-for="group in groupedIssues"
+        class="add-issues-list-column">
         <div
-          v-for="group in groupedIssues"
-          class="add-issues-list-column">
+          v-for="issue in group"
+          v-if="showIssue(issue)"
+          class="card-parent">
           <div
-            v-for="issue in group"
-            v-if="showIssue(issue)"
-            class="card-parent">
-            <div
-              class="card"
-              :class="{ 'is-active': issue.selected }"
-              @click="toggleIssue($event, issue)">
-              <issue-card-inner
-                :issue="issue"
-                :issue-link-base="issueLinkBase"
-                :root-path="rootPath">
-              </issue-card-inner>
-              <span
-                :aria-label="'Issue #' + issue.id + ' selected'"
-                aria-checked="true"
-                v-if="issue.selected"
-                class="issue-card-selected text-center">
-                <i class="fa fa-check"></i>
-              </span>
-            </div>
+            class="card"
+            :class="{ 'is-active': issue.selected }"
+            @click="toggleIssue($event, issue)">
+            <issue-card-inner
+              :issue="issue"
+              :issue-link-base="issueLinkBase"
+              :root-path="rootPath">
+            </issue-card-inner>
+            <span
+              :aria-label="'Issue #' + issue.id + ' selected'"
+              aria-checked="true"
+              v-if="issue.selected"
+              class="issue-card-selected text-center">
+              <i class="fa fa-check"></i>
+            </span>
           </div>
         </div>
-      </section>
-    `,
-  });
-})();
+      </div>
+    </section>
+  `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/lists_dropdown.js b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
index 9e9ed46ab8de6fec1598f1a517733a807f90f80f..8cd15df90fa09b64af1a1bb9953cf80161dac5d6 100644
--- a/app/assets/javascripts/boards/components/modal/lists_dropdown.js
+++ b/app/assets/javascripts/boards/components/modal/lists_dropdown.js
@@ -1,57 +1,55 @@
 import Vue from 'vue';
 
-(() => {
-  const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
 
-  gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
-    data() {
-      return {
-        modal: ModalStore.store,
-        state: gl.issueBoards.BoardsStore.state,
-      };
+gl.issueBoards.ModalFooterListsDropdown = Vue.extend({
+  data() {
+    return {
+      modal: ModalStore.store,
+      state: gl.issueBoards.BoardsStore.state,
+    };
+  },
+  computed: {
+    selected() {
+      return this.modal.selectedList || this.state.lists[0];
     },
-    computed: {
-      selected() {
-        return this.modal.selectedList || this.state.lists[0];
-      },
-    },
-    destroyed() {
-      this.modal.selectedList = null;
-    },
-    template: `
-      <div class="dropdown inline">
-        <button
-          class="dropdown-menu-toggle"
-          type="button"
-          data-toggle="dropdown"
-          aria-expanded="false">
-          <span
-            class="dropdown-label-box"
-            :style="{ backgroundColor: selected.label.color }">
-          </span>
-          {{ selected.title }}
-          <i class="fa fa-chevron-down"></i>
-        </button>
-        <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
-          <ul>
-            <li
-              v-for="list in state.lists"
-              v-if="list.type == 'label'">
-              <a
-                href="#"
-                role="button"
-                :class="{ 'is-active': list.id == selected.id }"
-                @click.prevent="modal.selectedList = list">
-                <span
-                  class="dropdown-label-box"
-                  :style="{ backgroundColor: list.label.color }">
-                </span>
-                {{ list.title }}
-              </a>
-            </li>
-          </ul>
-        </div>
+  },
+  destroyed() {
+    this.modal.selectedList = null;
+  },
+  template: `
+    <div class="dropdown inline">
+      <button
+        class="dropdown-menu-toggle"
+        type="button"
+        data-toggle="dropdown"
+        aria-expanded="false">
+        <span
+          class="dropdown-label-box"
+          :style="{ backgroundColor: selected.label.color }">
+        </span>
+        {{ selected.title }}
+        <i class="fa fa-chevron-down"></i>
+      </button>
+      <div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
+        <ul>
+          <li
+            v-for="list in state.lists"
+            v-if="list.type == 'label'">
+            <a
+              href="#"
+              role="button"
+              :class="{ 'is-active': list.id == selected.id }"
+              @click.prevent="modal.selectedList = list">
+              <span
+                class="dropdown-label-box"
+                :style="{ backgroundColor: list.label.color }">
+              </span>
+              {{ list.title }}
+            </a>
+          </li>
+        </ul>
       </div>
-    `,
-  });
-})();
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/boards/components/modal/tabs.js b/app/assets/javascripts/boards/components/modal/tabs.js
index 23cb1b13d111489b24b5eb54ae2d19ceb0888d88..3e5d08e3d755a6a3201dfc987135ead43367217c 100644
--- a/app/assets/javascripts/boards/components/modal/tabs.js
+++ b/app/assets/javascripts/boards/components/modal/tabs.js
@@ -1,48 +1,46 @@
 import Vue from 'vue';
 
-(() => {
-  const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
 
-  gl.issueBoards.ModalTabs = Vue.extend({
-    mixins: [gl.issueBoards.ModalMixins],
-    data() {
-      return ModalStore.store;
+gl.issueBoards.ModalTabs = Vue.extend({
+  mixins: [gl.issueBoards.ModalMixins],
+  data() {
+    return ModalStore.store;
+  },
+  computed: {
+    selectedCount() {
+      return ModalStore.selectedCount();
     },
-    computed: {
-      selectedCount() {
-        return ModalStore.selectedCount();
-      },
-    },
-    destroyed() {
-      this.activeTab = 'all';
-    },
-    template: `
-      <div class="top-area prepend-top-10 append-bottom-10">
-        <ul class="nav-links issues-state-filters">
-          <li :class="{ 'active': activeTab == 'all' }">
-            <a
-              href="#"
-              role="button"
-              @click.prevent="changeTab('all')">
-              Open issues
-              <span class="badge">
-                {{ issuesCount }}
-              </span>
-            </a>
-          </li>
-          <li :class="{ 'active': activeTab == 'selected' }">
-            <a
-              href="#"
-              role="button"
-              @click.prevent="changeTab('selected')">
-              Selected issues
-              <span class="badge">
-                {{ selectedCount }}
-              </span>
-            </a>
-          </li>
-        </ul>
-      </div>
-    `,
-  });
-})();
+  },
+  destroyed() {
+    this.activeTab = 'all';
+  },
+  template: `
+    <div class="top-area prepend-top-10 append-bottom-10">
+      <ul class="nav-links issues-state-filters">
+        <li :class="{ 'active': activeTab == 'all' }">
+          <a
+            href="#"
+            role="button"
+            @click.prevent="changeTab('all')">
+            Open issues
+            <span class="badge">
+              {{ issuesCount }}
+            </span>
+          </a>
+        </li>
+        <li :class="{ 'active': activeTab == 'selected' }">
+          <a
+            href="#"
+            role="button"
+            @click.prevent="changeTab('selected')">
+            Selected issues
+            <span class="badge">
+              {{ selectedCount }}
+            </span>
+          </a>
+        </li>
+      </ul>
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 556826a91487b2f40f1005f60570ee99fb266df5..22f20305624f02ecbc9ed97cd0b830cfc530be22 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -1,76 +1,74 @@
 /* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var */
 
-(() => {
-  window.gl = window.gl || {};
-  window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
 
-  const Store = gl.issueBoards.BoardsStore;
+const Store = gl.issueBoards.BoardsStore;
 
-  $(document).off('created.label').on('created.label', (e, label) => {
-    Store.new({
+$(document).off('created.label').on('created.label', (e, label) => {
+  Store.new({
+    title: label.title,
+    position: Store.state.lists.length - 2,
+    list_type: 'label',
+    label: {
+      id: label.id,
       title: label.title,
-      position: Store.state.lists.length - 2,
-      list_type: 'label',
-      label: {
-        id: label.id,
-        title: label.title,
-        color: label.color
-      }
-    });
+      color: label.color
+    }
   });
+});
 
-  gl.issueBoards.newListDropdownInit = () => {
-    $('.js-new-board-list').each(function () {
-      const $this = $(this);
-      new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
+gl.issueBoards.newListDropdownInit = () => {
+  $('.js-new-board-list').each(function () {
+    const $this = $(this);
+    new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
 
-      $this.glDropdown({
-        data(term, callback) {
-          $.get($this.attr('data-labels'))
-            .then((resp) => {
-              callback(resp);
-            });
-        },
-        renderRow (label) {
-          const active = Store.findList('title', label.title);
-          const $li = $('<li />');
-          const $a = $('<a />', {
-            class: (active ? `is-active js-board-list-${active.id}` : ''),
-            text: label.title,
-            href: '#'
-          });
-          const $labelColor = $('<span />', {
-            class: 'dropdown-label-box',
-            style: `background-color: ${label.color}`
+    $this.glDropdown({
+      data(term, callback) {
+        $.get($this.attr('data-labels'))
+          .then((resp) => {
+            callback(resp);
           });
+      },
+      renderRow (label) {
+        const active = Store.findList('title', label.title);
+        const $li = $('<li />');
+        const $a = $('<a />', {
+          class: (active ? `is-active js-board-list-${active.id}` : ''),
+          text: label.title,
+          href: '#'
+        });
+        const $labelColor = $('<span />', {
+          class: 'dropdown-label-box',
+          style: `background-color: ${label.color}`
+        });
 
-          return $li.append($a.prepend($labelColor));
-        },
-        search: {
-          fields: ['title']
-        },
-        filterable: true,
-        selectable: true,
-        multiSelect: true,
-        clicked (label, $el, e) {
-          e.preventDefault();
+        return $li.append($a.prepend($labelColor));
+      },
+      search: {
+        fields: ['title']
+      },
+      filterable: true,
+      selectable: true,
+      multiSelect: true,
+      clicked (label, $el, e) {
+        e.preventDefault();
 
-          if (!Store.findList('title', label.title)) {
-            Store.new({
+        if (!Store.findList('title', label.title)) {
+          Store.new({
+            title: label.title,
+            position: Store.state.lists.length - 2,
+            list_type: 'label',
+            label: {
+              id: label.id,
               title: label.title,
-              position: Store.state.lists.length - 2,
-              list_type: 'label',
-              label: {
-                id: label.id,
-                title: label.title,
-                color: label.color
-              }
-            });
+              color: label.color
+            }
+          });
 
-            Store.state.lists = _.sortBy(Store.state.lists, 'position');
-          }
+          Store.state.lists = _.sortBy(Store.state.lists, 'position');
         }
-      });
+      }
     });
-  };
-})();
+  });
+};
diff --git a/app/assets/javascripts/boards/components/sidebar/remove_issue.js b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
index 772ea4c55651b19c86ebaaa85ed7448dc940e7a3..5597f128b80db4aa078042afd11cf60820d8d4e9 100644
--- a/app/assets/javascripts/boards/components/sidebar/remove_issue.js
+++ b/app/assets/javascripts/boards/components/sidebar/remove_issue.js
@@ -3,59 +3,57 @@
 
 import Vue from 'vue';
 
-(() => {
-  const Store = gl.issueBoards.BoardsStore;
-
-  window.gl = window.gl || {};
-  window.gl.issueBoards = window.gl.issueBoards || {};
-
-  gl.issueBoards.RemoveIssueBtn = Vue.extend({
-    props: {
-      issue: {
-        type: Object,
-        required: true,
-      },
-      list: {
-        type: Object,
-        required: true,
-      },
+const Store = gl.issueBoards.BoardsStore;
+
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
+
+gl.issueBoards.RemoveIssueBtn = Vue.extend({
+  props: {
+    issue: {
+      type: Object,
+      required: true,
     },
-    methods: {
-      removeIssue() {
-        const issue = this.issue;
-        const lists = issue.getLists();
-        const labelIds = lists.map(list => list.label.id);
-
-        // Post the remove data
-        gl.boardService.bulkUpdate([issue.globalId], {
-          remove_label_ids: labelIds,
-        }).catch(() => {
-          new Flash('Failed to remove issue from board, please try again.', 'alert');
-
-          lists.forEach((list) => {
-            list.addIssue(issue);
-          });
-        });
+    list: {
+      type: Object,
+      required: true,
+    },
+  },
+  methods: {
+    removeIssue() {
+      const issue = this.issue;
+      const lists = issue.getLists();
+      const labelIds = lists.map(list => list.label.id);
+
+      // Post the remove data
+      gl.boardService.bulkUpdate([issue.globalId], {
+        remove_label_ids: labelIds,
+      }).catch(() => {
+        new Flash('Failed to remove issue from board, please try again.', 'alert');
 
-        // Remove from the frontend store
         lists.forEach((list) => {
-          list.removeIssue(issue);
+          list.addIssue(issue);
         });
+      });
+
+      // Remove from the frontend store
+      lists.forEach((list) => {
+        list.removeIssue(issue);
+      });
 
-        Store.detail.issue = {};
-      },
+      Store.detail.issue = {};
     },
-    template: `
-      <div
-        class="block list"
-        v-if="list.type !== 'closed'">
-        <button
-          class="btn btn-default btn-block"
-          type="button"
-          @click="removeIssue">
-          Remove from board
-        </button>
-      </div>
-    `,
-  });
-})();
+  },
+  template: `
+    <div
+      class="block list"
+      v-if="list.type !== 'closed'">
+      <button
+        class="btn btn-default btn-block"
+        type="button"
+        @click="removeIssue">
+        Remove from board
+      </button>
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/boards/mixins/modal_mixins.js b/app/assets/javascripts/boards/mixins/modal_mixins.js
index d378b7d4baf0be3265482c8e482587e8a2c36586..2b0a1aaa89ffb31c6a7165e079091b78700ce666 100644
--- a/app/assets/javascripts/boards/mixins/modal_mixins.js
+++ b/app/assets/javascripts/boards/mixins/modal_mixins.js
@@ -1,14 +1,12 @@
-(() => {
-  const ModalStore = gl.issueBoards.ModalStore;
+const ModalStore = gl.issueBoards.ModalStore;
 
-  gl.issueBoards.ModalMixins = {
-    methods: {
-      toggleModal(toggle) {
-        ModalStore.store.showAddIssuesModal = toggle;
-      },
-      changeTab(tab) {
-        ModalStore.store.activeTab = tab;
-      },
+gl.issueBoards.ModalMixins = {
+  methods: {
+    toggleModal(toggle) {
+      ModalStore.store.showAddIssuesModal = toggle;
     },
-  };
-})();
+    changeTab(tab) {
+      ModalStore.store.activeTab = tab;
+    },
+  },
+};
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index b6c6d17274f1d351e77018ee03b04525fa3c1536..38a0eb12f920a40d9cda4ef773ac7dbc2ac80b13 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -1,39 +1,37 @@
 /* eslint-disable no-unused-vars, no-mixed-operators, comma-dangle */
 /* global DocumentTouch */
 
-((w) => {
-  window.gl = window.gl || {};
-  window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
 
-  gl.issueBoards.onStart = () => {
-    $('.has-tooltip').tooltip('hide')
-      .tooltip('disable');
-    document.body.classList.add('is-dragging');
-  };
-
-  gl.issueBoards.onEnd = () => {
-    $('.has-tooltip').tooltip('enable');
-    document.body.classList.remove('is-dragging');
-  };
+gl.issueBoards.onStart = () => {
+  $('.has-tooltip').tooltip('hide')
+    .tooltip('disable');
+  document.body.classList.add('is-dragging');
+};
 
-  gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
+gl.issueBoards.onEnd = () => {
+  $('.has-tooltip').tooltip('enable');
+  document.body.classList.remove('is-dragging');
+};
 
-  gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
-    const defaultSortOptions = {
-      animation: 200,
-      forceFallback: true,
-      fallbackClass: 'is-dragging',
-      fallbackOnBody: true,
-      ghostClass: 'is-ghost',
-      filter: '.board-delete, .btn',
-      delay: gl.issueBoards.touchEnabled ? 100 : 0,
-      scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
-      scrollSpeed: 20,
-      onStart: gl.issueBoards.onStart,
-      onEnd: gl.issueBoards.onEnd
-    };
+gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
 
-    Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
-    return defaultSortOptions;
+gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
+  const defaultSortOptions = {
+    animation: 200,
+    forceFallback: true,
+    fallbackClass: 'is-dragging',
+    fallbackOnBody: true,
+    ghostClass: 'is-ghost',
+    filter: '.board-delete, .btn',
+    delay: gl.issueBoards.touchEnabled ? 100 : 0,
+    scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
+    scrollSpeed: 20,
+    onStart: gl.issueBoards.onStart,
+    onEnd: gl.issueBoards.onEnd
   };
-})(window);
+
+  Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
+  return defaultSortOptions;
+};
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index bcda70d063846b1396800bc1f20f4a51592339a8..66384d9c03861d74878538c32589489f128864e6 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -3,125 +3,123 @@
 
 import Cookies from 'js-cookie';
 
-(() => {
-  window.gl = window.gl || {};
-  window.gl.issueBoards = window.gl.issueBoards || {};
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
 
-  gl.issueBoards.BoardsStore = {
-    disabled: false,
-    filter: {
-      path: '',
-    },
-    state: {},
-    detail: {
-      issue: {}
-    },
-    moving: {
-      issue: {},
-      list: {}
-    },
-    create () {
-      this.state.lists = [];
-      this.filter.path = gl.utils.getUrlParamsArray().join('&');
-    },
-    addList (listObj) {
-      const list = new List(listObj);
-      this.state.lists.push(list);
+gl.issueBoards.BoardsStore = {
+  disabled: false,
+  filter: {
+    path: '',
+  },
+  state: {},
+  detail: {
+    issue: {}
+  },
+  moving: {
+    issue: {},
+    list: {}
+  },
+  create () {
+    this.state.lists = [];
+    this.filter.path = gl.utils.getUrlParamsArray().join('&');
+  },
+  addList (listObj) {
+    const list = new List(listObj);
+    this.state.lists.push(list);
 
-      return list;
-    },
-    new (listObj) {
-      const list = this.addList(listObj);
+    return list;
+  },
+  new (listObj) {
+    const list = this.addList(listObj);
 
-      list
-        .save()
-        .then(() => {
-          this.state.lists = _.sortBy(this.state.lists, 'position');
-        });
-      this.removeBlankState();
-    },
-    updateNewListDropdown (listId) {
-      $(`.js-board-list-${listId}`).removeClass('is-active');
-    },
-    shouldAddBlankState () {
-      // Decide whether to add the blank state
-      return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
-    },
-    addBlankState () {
-      if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
-
-      this.addList({
-        id: 'blank',
-        list_type: 'blank',
-        title: 'Welcome to your Issue Board!',
-        position: 0
+    list
+      .save()
+      .then(() => {
+        this.state.lists = _.sortBy(this.state.lists, 'position');
       });
+    this.removeBlankState();
+  },
+  updateNewListDropdown (listId) {
+    $(`.js-board-list-${listId}`).removeClass('is-active');
+  },
+  shouldAddBlankState () {
+    // Decide whether to add the blank state
+    return !(this.state.lists.filter(list => list.type !== 'closed')[0]);
+  },
+  addBlankState () {
+    if (!this.shouldAddBlankState() || this.welcomeIsHidden() || this.disabled) return;
 
-      this.state.lists = _.sortBy(this.state.lists, 'position');
-    },
-    removeBlankState () {
-      this.removeList('blank');
-
-      Cookies.set('issue_board_welcome_hidden', 'true', {
-        expires: 365 * 10,
-        path: ''
-      });
-    },
-    welcomeIsHidden () {
-      return Cookies.get('issue_board_welcome_hidden') === 'true';
-    },
-    removeList (id, type = 'blank') {
-      const list = this.findList('id', id, type);
+    this.addList({
+      id: 'blank',
+      list_type: 'blank',
+      title: 'Welcome to your Issue Board!',
+      position: 0
+    });
 
-      if (!list) return;
+    this.state.lists = _.sortBy(this.state.lists, 'position');
+  },
+  removeBlankState () {
+    this.removeList('blank');
 
-      this.state.lists = this.state.lists.filter(list => list.id !== id);
-    },
-    moveList (listFrom, orderLists) {
-      orderLists.forEach((id, i) => {
-        const list = this.findList('id', parseInt(id, 10));
+    Cookies.set('issue_board_welcome_hidden', 'true', {
+      expires: 365 * 10,
+      path: ''
+    });
+  },
+  welcomeIsHidden () {
+    return Cookies.get('issue_board_welcome_hidden') === 'true';
+  },
+  removeList (id, type = 'blank') {
+    const list = this.findList('id', id, type);
 
-        list.position = i;
-      });
-      listFrom.update();
-    },
-    moveIssueToList (listFrom, listTo, issue, newIndex) {
-      const issueTo = listTo.findIssue(issue.id);
-      const issueLists = issue.getLists();
-      const listLabels = issueLists.map(listIssue => listIssue.label);
+    if (!list) return;
 
-      if (!issueTo) {
-        // Add to new lists issues if it doesn't already exist
-        listTo.addIssue(issue, listFrom, newIndex);
-      } else {
-        listTo.updateIssueLabel(issue, listFrom);
-        issueTo.removeLabel(listFrom.label);
-      }
+    this.state.lists = this.state.lists.filter(list => list.id !== id);
+  },
+  moveList (listFrom, orderLists) {
+    orderLists.forEach((id, i) => {
+      const list = this.findList('id', parseInt(id, 10));
 
-      if (listTo.type === 'closed') {
-        issueLists.forEach((list) => {
-          list.removeIssue(issue);
-        });
-        issue.removeLabels(listLabels);
-      } else {
-        listFrom.removeIssue(issue);
-      }
-    },
-    moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
-      const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
-      const afterId = parseInt(idArray[newIndex + 1], 10) || null;
+      list.position = i;
+    });
+    listFrom.update();
+  },
+  moveIssueToList (listFrom, listTo, issue, newIndex) {
+    const issueTo = listTo.findIssue(issue.id);
+    const issueLists = issue.getLists();
+    const listLabels = issueLists.map(listIssue => listIssue.label);
 
-      list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
-    },
-    findList (key, val, type = 'label') {
-      return this.state.lists.filter((list) => {
-        const byType = type ? list['type'] === type : true;
+    if (!issueTo) {
+      // Add to new lists issues if it doesn't already exist
+      listTo.addIssue(issue, listFrom, newIndex);
+    } else {
+      listTo.updateIssueLabel(issue, listFrom);
+      issueTo.removeLabel(listFrom.label);
+    }
 
-        return list[key] === val && byType;
-      })[0];
-    },
-    updateFiltersUrl () {
-      history.pushState(null, null, `?${this.filter.path}`);
+    if (listTo.type === 'closed') {
+      issueLists.forEach((list) => {
+        list.removeIssue(issue);
+      });
+      issue.removeLabels(listLabels);
+    } else {
+      listFrom.removeIssue(issue);
     }
-  };
-})();
+  },
+  moveIssueInList (list, issue, oldIndex, newIndex, idArray) {
+    const beforeId = parseInt(idArray[newIndex - 1], 10) || null;
+    const afterId = parseInt(idArray[newIndex + 1], 10) || null;
+
+    list.moveIssue(issue, oldIndex, newIndex, beforeId, afterId);
+  },
+  findList (key, val, type = 'label') {
+    return this.state.lists.filter((list) => {
+      const byType = type ? list['type'] === type : true;
+
+      return list[key] === val && byType;
+    })[0];
+  },
+  updateFiltersUrl () {
+    history.pushState(null, null, `?${this.filter.path}`);
+  }
+};
diff --git a/app/assets/javascripts/boards/stores/modal_store.js b/app/assets/javascripts/boards/stores/modal_store.js
index 9b009483a3c6c255dfac47c957b88a5d2c38b54a..4fdc925c825de136764e9e4e7b78ae972333f971 100644
--- a/app/assets/javascripts/boards/stores/modal_store.js
+++ b/app/assets/javascripts/boards/stores/modal_store.js
@@ -1,100 +1,98 @@
-(() => {
-  window.gl = window.gl || {};
-  window.gl.issueBoards = window.gl.issueBoards || {};
-
-  class ModalStore {
-    constructor() {
-      this.store = {
-        columns: 3,
-        issues: [],
-        issuesCount: false,
-        selectedIssues: [],
-        showAddIssuesModal: false,
-        activeTab: 'all',
-        selectedList: null,
-        searchTerm: '',
-        loading: false,
-        loadingNewPage: false,
-        filterLoading: false,
-        page: 1,
-        perPage: 50,
-        filter: {
-          path: '',
-        },
-      };
-    }
+window.gl = window.gl || {};
+window.gl.issueBoards = window.gl.issueBoards || {};
+
+class ModalStore {
+  constructor() {
+    this.store = {
+      columns: 3,
+      issues: [],
+      issuesCount: false,
+      selectedIssues: [],
+      showAddIssuesModal: false,
+      activeTab: 'all',
+      selectedList: null,
+      searchTerm: '',
+      loading: false,
+      loadingNewPage: false,
+      filterLoading: false,
+      page: 1,
+      perPage: 50,
+      filter: {
+        path: '',
+      },
+    };
+  }
 
-    selectedCount() {
-      return this.getSelectedIssues().length;
-    }
+  selectedCount() {
+    return this.getSelectedIssues().length;
+  }
 
-    toggleIssue(issueObj) {
-      const issue = issueObj;
-      const selected = issue.selected;
+  toggleIssue(issueObj) {
+    const issue = issueObj;
+    const selected = issue.selected;
 
-      issue.selected = !selected;
+    issue.selected = !selected;
 
-      if (!selected) {
-        this.addSelectedIssue(issue);
-      } else {
-        this.removeSelectedIssue(issue);
-      }
+    if (!selected) {
+      this.addSelectedIssue(issue);
+    } else {
+      this.removeSelectedIssue(issue);
     }
+  }
 
-    toggleAll() {
-      const select = this.selectedCount() !== this.store.issues.length;
+  toggleAll() {
+    const select = this.selectedCount() !== this.store.issues.length;
 
-      this.store.issues.forEach((issue) => {
-        const issueUpdate = issue;
+    this.store.issues.forEach((issue) => {
+      const issueUpdate = issue;
 
-        if (issueUpdate.selected !== select) {
-          issueUpdate.selected = select;
+      if (issueUpdate.selected !== select) {
+        issueUpdate.selected = select;
 
-          if (select) {
-            this.addSelectedIssue(issue);
-          } else {
-            this.removeSelectedIssue(issue);
-          }
+        if (select) {
+          this.addSelectedIssue(issue);
+        } else {
+          this.removeSelectedIssue(issue);
         }
-      });
-    }
+      }
+    });
+  }
 
-    getSelectedIssues() {
-      return this.store.selectedIssues.filter(issue => issue.selected);
-    }
+  getSelectedIssues() {
+    return this.store.selectedIssues.filter(issue => issue.selected);
+  }
 
-    addSelectedIssue(issue) {
-      const index = this.selectedIssueIndex(issue);
+  addSelectedIssue(issue) {
+    const index = this.selectedIssueIndex(issue);
 
-      if (index === -1) {
-        this.store.selectedIssues.push(issue);
-      }
+    if (index === -1) {
+      this.store.selectedIssues.push(issue);
     }
+  }
 
-    removeSelectedIssue(issue, forcePurge = false) {
-      if (this.store.activeTab === 'all' || forcePurge) {
-        this.store.selectedIssues = this.store.selectedIssues
-          .filter(fIssue => fIssue.id !== issue.id);
-      }
+  removeSelectedIssue(issue, forcePurge = false) {
+    if (this.store.activeTab === 'all' || forcePurge) {
+      this.store.selectedIssues = this.store.selectedIssues
+        .filter(fIssue => fIssue.id !== issue.id);
     }
+  }
 
-    purgeUnselectedIssues() {
-      this.store.selectedIssues.forEach((issue) => {
-        if (!issue.selected) {
-          this.removeSelectedIssue(issue, true);
-        }
-      });
-    }
+  purgeUnselectedIssues() {
+    this.store.selectedIssues.forEach((issue) => {
+      if (!issue.selected) {
+        this.removeSelectedIssue(issue, true);
+      }
+    });
+  }
 
-    selectedIssueIndex(issue) {
-      return this.store.selectedIssues.indexOf(issue);
-    }
+  selectedIssueIndex(issue) {
+    return this.store.selectedIssues.indexOf(issue);
+  }
 
-    findSelectedIssue(issue) {
-      return this.store.selectedIssues
-        .filter(filteredIssue => filteredIssue.id === issue.id)[0];
-    }
+  findSelectedIssue(issue) {
+    return this.store.selectedIssues
+      .filter(filteredIssue => filteredIssue.id === issue.id)[0];
   }
+}
 
-  gl.issueBoards.ModalStore = new ModalStore();
-})();
+gl.issueBoards.ModalStore = new ModalStore();
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
index 3f419a96ff96a490bb3c8a1bd05cefb874cb7391..80bd2df6f42eaf1fb91c6f72d4ed4129002a8b99 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_code_component.js
@@ -2,46 +2,45 @@
 
 import Vue from 'vue';
 
-((global) => {
-  global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
 
-  global.cycleAnalytics.StageCodeComponent = Vue.extend({
-    props: {
-      items: Array,
-      stage: Object,
-    },
-    template: `
-      <div>
-        <div class="events-description">
-          {{ stage.description }}
-          <limit-warning :count="items.length" />
-        </div>
-        <ul class="stage-event-list">
-          <li v-for="mergeRequest in items" class="stage-event-item">
-            <div class="item-details">
-              <img class="avatar" :src="mergeRequest.author.avatarUrl">
-              <h5 class="item-title merge-merquest-title">
-                <a :href="mergeRequest.url">
-                  {{ mergeRequest.title }}
-                </a>
-              </h5>
-              <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
-              &middot;
-              <span>
-                Opened
-                <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
-              </span>
-              <span>
-                by
-                <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
-              </span>
-            </div>
-            <div class="item-time">
-              <total-time :time="mergeRequest.totalTime"></total-time>
-            </div>
-          </li>
-        </ul>
+global.cycleAnalytics.StageCodeComponent = Vue.extend({
+  props: {
+    items: Array,
+    stage: Object,
+  },
+  template: `
+    <div>
+      <div class="events-description">
+        {{ stage.description }}
+        <limit-warning :count="items.length" />
       </div>
-    `,
-  });
-})(window.gl || (window.gl = {}));
+      <ul class="stage-event-list">
+        <li v-for="mergeRequest in items" class="stage-event-item">
+          <div class="item-details">
+            <img class="avatar" :src="mergeRequest.author.avatarUrl">
+            <h5 class="item-title merge-merquest-title">
+              <a :href="mergeRequest.url">
+                {{ mergeRequest.title }}
+              </a>
+            </h5>
+            <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
+            &middot;
+            <span>
+              Opened
+              <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
+            </span>
+            <span>
+              by
+              <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
+            </span>
+          </div>
+          <div class="item-time">
+            <total-time :time="mergeRequest.totalTime"></total-time>
+          </div>
+        </li>
+      </ul>
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
index 7ffa38edd9ed2abd68012fa252352aef6c5731fd..20a43798fbedc752ca528f5523cba5dd74006867 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_issue_component.js
@@ -2,48 +2,47 @@
 
 import Vue from 'vue';
 
-((global) => {
-  global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
 
-  global.cycleAnalytics.StageIssueComponent = Vue.extend({
-    props: {
-      items: Array,
-      stage: Object,
-    },
-    template: `
-      <div>
-        <div class="events-description">
-          {{ stage.description }}
-          <limit-warning :count="items.length" />
-        </div>
-        <ul class="stage-event-list">
-          <li v-for="issue in items" class="stage-event-item">
-            <div class="item-details">
-              <img class="avatar" :src="issue.author.avatarUrl">
-              <h5 class="item-title issue-title">
-                <a class="issue-title" :href="issue.url">
-                  {{ issue.title }}
-                </a>
-              </h5>
-              <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
-              &middot;
-              <span>
-                Opened
-                <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
-              </span>
-              <span>
-                by
-                <a :href="issue.author.webUrl" class="issue-author-link">
-                  {{ issue.author.name }}
-                </a>
-              </span>
-            </div>
-            <div class="item-time">
-              <total-time :time="issue.totalTime"></total-time>
-            </div>
-          </li>
-        </ul>
+global.cycleAnalytics.StageIssueComponent = Vue.extend({
+  props: {
+    items: Array,
+    stage: Object,
+  },
+  template: `
+    <div>
+      <div class="events-description">
+        {{ stage.description }}
+        <limit-warning :count="items.length" />
       </div>
-    `,
-  });
-})(window.gl || (window.gl = {}));
+      <ul class="stage-event-list">
+        <li v-for="issue in items" class="stage-event-item">
+          <div class="item-details">
+            <img class="avatar" :src="issue.author.avatarUrl">
+            <h5 class="item-title issue-title">
+              <a class="issue-title" :href="issue.url">
+                {{ issue.title }}
+              </a>
+            </h5>
+            <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
+            &middot;
+            <span>
+              Opened
+              <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
+            </span>
+            <span>
+              by
+              <a :href="issue.author.webUrl" class="issue-author-link">
+                {{ issue.author.name }}
+              </a>
+            </span>
+          </div>
+          <div class="item-time">
+            <total-time :time="issue.totalTime"></total-time>
+          </div>
+        </li>
+      </ul>
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
index d736c8b0c2897445ad49832b53d175aafb526f73..f33cac3da8248131689a98764d158f1f47c19c46 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_plan_component.js
@@ -2,50 +2,49 @@
 import Vue from 'vue';
 import iconCommit from '../svg/icon_commit.svg';
 
-((global) => {
-  global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
 
-  global.cycleAnalytics.StagePlanComponent = Vue.extend({
-    props: {
-      items: Array,
-      stage: Object,
-    },
+global.cycleAnalytics.StagePlanComponent = Vue.extend({
+  props: {
+    items: Array,
+    stage: Object,
+  },
 
-    data() {
-      return { iconCommit };
-    },
+  data() {
+    return { iconCommit };
+  },
 
-    template: `
-      <div>
-        <div class="events-description">
-          {{ stage.description }}
-          <limit-warning :count="items.length" />
-        </div>
-        <ul class="stage-event-list">
-          <li v-for="commit in items" class="stage-event-item">
-            <div class="item-details item-conmmit-component">
-              <img class="avatar" :src="commit.author.avatarUrl">
-              <h5 class="item-title commit-title">
-                <a :href="commit.commitUrl">
-                  {{ commit.title }}
-                </a>
-              </h5>
-              <span>
-                First
-                <span class="commit-icon">${iconCommit}</span>
-                <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
-                pushed by
-                <a :href="commit.author.webUrl" class="commit-author-link">
-                  {{ commit.author.name }}
-                </a>
-              </span>
-            </div>
-            <div class="item-time">
-              <total-time :time="commit.totalTime"></total-time>
-            </div>
-          </li>
-        </ul>
+  template: `
+    <div>
+      <div class="events-description">
+        {{ stage.description }}
+        <limit-warning :count="items.length" />
       </div>
-    `,
-  });
-})(window.gl || (window.gl = {}));
+      <ul class="stage-event-list">
+        <li v-for="commit in items" class="stage-event-item">
+          <div class="item-details item-conmmit-component">
+            <img class="avatar" :src="commit.author.avatarUrl">
+            <h5 class="item-title commit-title">
+              <a :href="commit.commitUrl">
+                {{ commit.title }}
+              </a>
+            </h5>
+            <span>
+              First
+              <span class="commit-icon">${iconCommit}</span>
+              <a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
+              pushed by
+              <a :href="commit.author.webUrl" class="commit-author-link">
+                {{ commit.author.name }}
+              </a>
+            </span>
+          </div>
+          <div class="item-time">
+            <total-time :time="commit.totalTime"></total-time>
+          </div>
+        </li>
+      </ul>
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
index 698a79ca68c4bde999537ff16481919619daefcc..657f538537433799d46416188dcbade6875f3e85 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_production_component.js
@@ -2,48 +2,47 @@
 
 import Vue from 'vue';
 
-((global) => {
-  global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
 
-  global.cycleAnalytics.StageProductionComponent = Vue.extend({
-    props: {
-      items: Array,
-      stage: Object,
-    },
-    template: `
-      <div>
-        <div class="events-description">
-          {{ stage.description }}
-          <limit-warning :count="items.length" />
-        </div>
-        <ul class="stage-event-list">
-          <li v-for="issue in items" class="stage-event-item">
-            <div class="item-details">
-              <img class="avatar" :src="issue.author.avatarUrl">
-              <h5 class="item-title issue-title">
-                <a class="issue-title" :href="issue.url">
-                  {{ issue.title }}
-                </a>
-              </h5>
-              <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
-              &middot;
-              <span>
-                Opened
-                <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
-              </span>
-              <span>
-              by
-              <a :href="issue.author.webUrl" class="issue-author-link">
-                {{ issue.author.name }}
-              </a>
-              </span>
-            </div>
-            <div class="item-time">
-              <total-time :time="issue.totalTime"></total-time>
-            </div>
-          </li>
-        </ul>
+global.cycleAnalytics.StageProductionComponent = Vue.extend({
+  props: {
+    items: Array,
+    stage: Object,
+  },
+  template: `
+    <div>
+      <div class="events-description">
+        {{ stage.description }}
+        <limit-warning :count="items.length" />
       </div>
-    `,
-  });
-})(window.gl || (window.gl = {}));
+      <ul class="stage-event-list">
+        <li v-for="issue in items" class="stage-event-item">
+          <div class="item-details">
+            <img class="avatar" :src="issue.author.avatarUrl">
+            <h5 class="item-title issue-title">
+              <a class="issue-title" :href="issue.url">
+                {{ issue.title }}
+              </a>
+            </h5>
+            <a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
+            &middot;
+            <span>
+              Opened
+              <a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
+            </span>
+            <span>
+            by
+            <a :href="issue.author.webUrl" class="issue-author-link">
+              {{ issue.author.name }}
+            </a>
+            </span>
+          </div>
+          <div class="item-time">
+            <total-time :time="issue.totalTime"></total-time>
+          </div>
+        </li>
+      </ul>
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
index e63c41f2a5730410240c870d7c93008c78744194..8a801300647aac189d99c940f971bb771890b44b 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_review_component.js
@@ -2,58 +2,57 @@
 
 import Vue from 'vue';
 
-((global) => {
-  global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
 
-  global.cycleAnalytics.StageReviewComponent = Vue.extend({
-    props: {
-      items: Array,
-      stage: Object,
-    },
-    template: `
-      <div>
-        <div class="events-description">
-          {{ stage.description }}
-          <limit-warning :count="items.length" />
-        </div>
-        <ul class="stage-event-list">
-          <li v-for="mergeRequest in items" class="stage-event-item">
-            <div class="item-details">
-              <img class="avatar" :src="mergeRequest.author.avatarUrl">
-              <h5 class="item-title merge-merquest-title">
-                <a :href="mergeRequest.url">
-                  {{ mergeRequest.title }}
-                </a>
-              </h5>
-              <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
-              &middot;
-              <span>
-                Opened
-                <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
+global.cycleAnalytics.StageReviewComponent = Vue.extend({
+  props: {
+    items: Array,
+    stage: Object,
+  },
+  template: `
+    <div>
+      <div class="events-description">
+        {{ stage.description }}
+        <limit-warning :count="items.length" />
+      </div>
+      <ul class="stage-event-list">
+        <li v-for="mergeRequest in items" class="stage-event-item">
+          <div class="item-details">
+            <img class="avatar" :src="mergeRequest.author.avatarUrl">
+            <h5 class="item-title merge-merquest-title">
+              <a :href="mergeRequest.url">
+                {{ mergeRequest.title }}
+              </a>
+            </h5>
+            <a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
+            &middot;
+            <span>
+              Opened
+              <a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
+            </span>
+            <span>
+              by
+              <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
+            </span>
+            <template v-if="mergeRequest.state === 'closed'">
+              <span class="merge-request-state">
+                <i class="fa fa-ban"></i>
+                {{ mergeRequest.state.toUpperCase() }}
               </span>
-              <span>
-                by
-                <a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
+            </template>
+            <template v-else>
+              <span class="merge-request-branch" v-if="mergeRequest.branch">
+                <i class= "fa fa-code-fork"></i>
+                <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
               </span>
-              <template v-if="mergeRequest.state === 'closed'">
-                <span class="merge-request-state">
-                  <i class="fa fa-ban"></i>
-                  {{ mergeRequest.state.toUpperCase() }}
-                </span>
-              </template>
-              <template v-else>
-                <span class="merge-request-branch" v-if="mergeRequest.branch">
-                  <i class= "fa fa-code-fork"></i>
-                  <a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
-                </span>
-              </template>
-            </div>
-            <div class="item-time">
-              <total-time :time="mergeRequest.totalTime"></total-time>
-            </div>
-          </li>
-        </ul>
-      </div>
-    `,
-  });
-})(window.gl || (window.gl = {}));
+            </template>
+          </div>
+          <div class="item-time">
+            <total-time :time="mergeRequest.totalTime"></total-time>
+          </div>
+        </li>
+      </ul>
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
index d51f7134e251b722a0fdc10daa66ab8a5cc3f242..4a28637958848ce2fff20c3c63139226b90e489a 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_staging_component.js
@@ -2,48 +2,47 @@
 import Vue from 'vue';
 import iconBranch from '../svg/icon_branch.svg';
 
-((global) => {
-  global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
 
-  global.cycleAnalytics.StageStagingComponent = Vue.extend({
-    props: {
-      items: Array,
-      stage: Object,
-    },
-    data() {
-      return { iconBranch };
-    },
-    template: `
-      <div>
-        <div class="events-description">
-          {{ stage.description }}
-          <limit-warning :count="items.length" />
-        </div>
-        <ul class="stage-event-list">
-          <li v-for="build in items" class="stage-event-item item-build-component">
-            <div class="item-details">
-              <img class="avatar" :src="build.author.avatarUrl">
-              <h5 class="item-title">
-                <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
-                <i class="fa fa-code-fork"></i>
-                <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
-                <span class="icon-branch">${iconBranch}</span>
-                <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
-              </h5>
-              <span>
-                <a :href="build.url" class="build-date">{{ build.date }}</a>
-                by
-                <a :href="build.author.webUrl" class="issue-author-link">
-                  {{ build.author.name }}
-                </a>
-              </span>
-            </div>
-            <div class="item-time">
-              <total-time :time="build.totalTime"></total-time>
-            </div>
-          </li>
-        </ul>
+global.cycleAnalytics.StageStagingComponent = Vue.extend({
+  props: {
+    items: Array,
+    stage: Object,
+  },
+  data() {
+    return { iconBranch };
+  },
+  template: `
+    <div>
+      <div class="events-description">
+        {{ stage.description }}
+        <limit-warning :count="items.length" />
       </div>
-    `,
-  });
-})(window.gl || (window.gl = {}));
+      <ul class="stage-event-list">
+        <li v-for="build in items" class="stage-event-item item-build-component">
+          <div class="item-details">
+            <img class="avatar" :src="build.author.avatarUrl">
+            <h5 class="item-title">
+              <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
+              <i class="fa fa-code-fork"></i>
+              <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
+              <span class="icon-branch">${iconBranch}</span>
+              <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
+            </h5>
+            <span>
+              <a :href="build.url" class="build-date">{{ build.date }}</a>
+              by
+              <a :href="build.author.webUrl" class="issue-author-link">
+                {{ build.author.name }}
+              </a>
+            </span>
+          </div>
+          <div class="item-time">
+            <total-time :time="build.totalTime"></total-time>
+          </div>
+        </li>
+      </ul>
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
index 17ae3a9ddc1516172e7999d79b9b09f5fa204bed..e306026429e004cffcb52e5409b38b42c9794f50 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/stage_test_component.js
@@ -3,48 +3,47 @@ import Vue from 'vue';
 import iconBuildStatus from '../svg/icon_build_status.svg';
 import iconBranch from '../svg/icon_branch.svg';
 
-((global) => {
-  global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
 
-  global.cycleAnalytics.StageTestComponent = Vue.extend({
-    props: {
-      items: Array,
-      stage: Object,
-    },
-    data() {
-      return { iconBuildStatus, iconBranch };
-    },
-    template: `
-      <div>
-        <div class="events-description">
-          {{ stage.description }}
-          <limit-warning :count="items.length" />
-        </div>
-        <ul class="stage-event-list">
-          <li v-for="build in items" class="stage-event-item item-build-component">
-            <div class="item-details">
-              <h5 class="item-title">
-                <span class="icon-build-status">${iconBuildStatus}</span>
-                <a :href="build.url" class="item-build-name">{{ build.name }}</a>
-                &middot;
-                <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
-                <i class="fa fa-code-fork"></i>
-                <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
-                <span class="icon-branch">${iconBranch}</span>
-                <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
-              </h5>
-              <span>
-                <a :href="build.url" class="issue-date">
-                  {{ build.date }}
-                </a>
-              </span>
-            </div>
-            <div class="item-time">
-              <total-time :time="build.totalTime"></total-time>
-            </div>
-          </li>
-        </ul>
+global.cycleAnalytics.StageTestComponent = Vue.extend({
+  props: {
+    items: Array,
+    stage: Object,
+  },
+  data() {
+    return { iconBuildStatus, iconBranch };
+  },
+  template: `
+    <div>
+      <div class="events-description">
+        {{ stage.description }}
+        <limit-warning :count="items.length" />
       </div>
-    `,
-  });
-})(window.gl || (window.gl = {}));
+      <ul class="stage-event-list">
+        <li v-for="build in items" class="stage-event-item item-build-component">
+          <div class="item-details">
+            <h5 class="item-title">
+              <span class="icon-build-status">${iconBuildStatus}</span>
+              <a :href="build.url" class="item-build-name">{{ build.name }}</a>
+              &middot;
+              <a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
+              <i class="fa fa-code-fork"></i>
+              <a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
+              <span class="icon-branch">${iconBranch}</span>
+              <a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
+            </h5>
+            <span>
+              <a :href="build.url" class="issue-date">
+                {{ build.date }}
+              </a>
+            </span>
+          </div>
+          <div class="item-time">
+            <total-time :time="build.totalTime"></total-time>
+          </div>
+        </li>
+      </ul>
+    </div>
+  `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time_component.js b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
index b4442ea5566d5d33d46b8738b73f47ffdc84b0ea..77edcb7627323eaa7f22b02b6fe969b095a10490 100644
--- a/app/assets/javascripts/cycle_analytics/components/total_time_component.js
+++ b/app/assets/javascripts/cycle_analytics/components/total_time_component.js
@@ -2,25 +2,24 @@
 
 import Vue from 'vue';
 
-((global) => {
-  global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
 
-  global.cycleAnalytics.TotalTimeComponent = Vue.extend({
-    props: {
-      time: Object,
-    },
-    template: `
-      <span class="total-time">
-        <template v-if="Object.keys(time).length">
-          <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
-          <template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
-          <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
-          <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
-        </template>
-        <template v-else>
-          --
-        </template>
-      </span>
-    `,
-  });
-})(window.gl || (window.gl = {}));
+global.cycleAnalytics.TotalTimeComponent = Vue.extend({
+  props: {
+    time: Object,
+  },
+  template: `
+    <span class="total-time">
+      <template v-if="Object.keys(time).length">
+        <template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
+        <template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
+        <template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
+        <template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
+      </template>
+      <template v-else>
+        --
+      </template>
+    </span>
+  `,
+});
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
index 9f74b14c4b970f896bdbe1a8a871874fc290605a..681d6eef565770152c1c17ad065bdf5b60cc9cef 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_service.js
@@ -1,41 +1,41 @@
 /* eslint-disable no-param-reassign */
-((global) => {
-  global.cycleAnalytics = global.cycleAnalytics || {};
 
-  class CycleAnalyticsService {
-    constructor(options) {
-      this.requestPath = options.requestPath;
-    }
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
 
-    fetchCycleAnalyticsData(options) {
-      options = options || { startDate: 30 };
-
-      return $.ajax({
-        url: this.requestPath,
-        method: 'GET',
-        dataType: 'json',
-        contentType: 'application/json',
-        data: {
-          cycle_analytics: {
-            start_date: options.startDate,
-          },
-        },
-      });
-    }
+class CycleAnalyticsService {
+  constructor(options) {
+    this.requestPath = options.requestPath;
+  }
 
-    fetchStageData(options) {
-      const {
-        stage,
-        startDate,
-      } = options;
+  fetchCycleAnalyticsData(options) {
+    options = options || { startDate: 30 };
 
-      return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, {
+    return $.ajax({
+      url: this.requestPath,
+      method: 'GET',
+      dataType: 'json',
+      contentType: 'application/json',
+      data: {
         cycle_analytics: {
-          start_date: startDate,
+          start_date: options.startDate,
         },
-      });
-    }
+      },
+    });
+  }
+
+  fetchStageData(options) {
+    const {
+      stage,
+      startDate,
+    } = options;
+
+    return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, {
+      cycle_analytics: {
+        start_date: startDate,
+      },
+    });
   }
+}
 
-  global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
-})(window.gl || (window.gl = {}));
+global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
index 7ae9de7297c680b201ebc25269a81674d93c9a07..6536a8fd7fa286a657afeebd765e8e05fde1f835 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_store.js
@@ -3,102 +3,101 @@
 require('../lib/utils/text_utility');
 const DEFAULT_EVENT_OBJECTS = require('./default_event_objects');
 
-((global) => {
-  global.cycleAnalytics = global.cycleAnalytics || {};
+const global = window.gl || (window.gl = {});
+global.cycleAnalytics = global.cycleAnalytics || {};
 
-  const EMPTY_STAGE_TEXTS = {
-    issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
-    plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
-    code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
-    test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
-    review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
-    staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
-    production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
-  };
+const EMPTY_STAGE_TEXTS = {
+  issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
+  plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
+  code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
+  test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
+  review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
+  staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
+  production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
+};
 
-  global.cycleAnalytics.CycleAnalyticsStore = {
-    state: {
-      summary: '',
-      stats: '',
-      analytics: '',
-      events: [],
-      stages: [],
-    },
-    setCycleAnalyticsData(data) {
-      this.state = Object.assign(this.state, this.decorateData(data));
-    },
-    decorateData(data) {
-      const newData = {};
+global.cycleAnalytics.CycleAnalyticsStore = {
+  state: {
+    summary: '',
+    stats: '',
+    analytics: '',
+    events: [],
+    stages: [],
+  },
+  setCycleAnalyticsData(data) {
+    this.state = Object.assign(this.state, this.decorateData(data));
+  },
+  decorateData(data) {
+    const newData = {};
 
-      newData.stages = data.stats || [];
-      newData.summary = data.summary || [];
+    newData.stages = data.stats || [];
+    newData.summary = data.summary || [];
 
-      newData.summary.forEach((item) => {
-        item.value = item.value || '-';
-      });
+    newData.summary.forEach((item) => {
+      item.value = item.value || '-';
+    });
 
-      newData.stages.forEach((item) => {
-        const stageSlug = gl.text.dasherize(item.title.toLowerCase());
-        item.active = false;
-        item.isUserAllowed = data.permissions[stageSlug];
-        item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
-        item.component = `stage-${stageSlug}-component`;
-        item.slug = stageSlug;
-      });
-      newData.analytics = data;
-      return newData;
-    },
-    setLoadingState(state) {
-      this.state.isLoading = state;
-    },
-    setErrorState(state) {
-      this.state.hasError = state;
-    },
-    deactivateAllStages() {
-      this.state.stages.forEach((stage) => {
-        stage.active = false;
-      });
-    },
-    setActiveStage(stage) {
-      this.deactivateAllStages();
-      stage.active = true;
-    },
-    setStageEvents(events, stage) {
-      this.state.events = this.decorateEvents(events, stage);
-    },
-    decorateEvents(events, stage) {
-      const newEvents = [];
+    newData.stages.forEach((item) => {
+      const stageSlug = gl.text.dasherize(item.title.toLowerCase());
+      item.active = false;
+      item.isUserAllowed = data.permissions[stageSlug];
+      item.emptyStageText = EMPTY_STAGE_TEXTS[stageSlug];
+      item.component = `stage-${stageSlug}-component`;
+      item.slug = stageSlug;
+    });
+    newData.analytics = data;
+    return newData;
+  },
+  setLoadingState(state) {
+    this.state.isLoading = state;
+  },
+  setErrorState(state) {
+    this.state.hasError = state;
+  },
+  deactivateAllStages() {
+    this.state.stages.forEach((stage) => {
+      stage.active = false;
+    });
+  },
+  setActiveStage(stage) {
+    this.deactivateAllStages();
+    stage.active = true;
+  },
+  setStageEvents(events, stage) {
+    this.state.events = this.decorateEvents(events, stage);
+  },
+  decorateEvents(events, stage) {
+    const newEvents = [];
 
-      events.forEach((item) => {
-        if (!item) return;
+    events.forEach((item) => {
+      if (!item) return;
 
-        const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item);
+      const eventItem = Object.assign({}, DEFAULT_EVENT_OBJECTS[stage.slug], item);
 
-        eventItem.totalTime = eventItem.total_time;
+      eventItem.totalTime = eventItem.total_time;
 
-        if (eventItem.author) {
-          eventItem.author.webUrl = eventItem.author.web_url;
-          eventItem.author.avatarUrl = eventItem.author.avatar_url;
-        }
+      if (eventItem.author) {
+        eventItem.author.webUrl = eventItem.author.web_url;
+        eventItem.author.avatarUrl = eventItem.author.avatar_url;
+      }
 
-        if (eventItem.created_at) eventItem.createdAt = eventItem.created_at;
-        if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha;
-        if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url;
+      if (eventItem.created_at) eventItem.createdAt = eventItem.created_at;
+      if (eventItem.short_sha) eventItem.shortSha = eventItem.short_sha;
+      if (eventItem.commit_url) eventItem.commitUrl = eventItem.commit_url;
 
-        delete eventItem.author.web_url;
-        delete eventItem.author.avatar_url;
-        delete eventItem.total_time;
-        delete eventItem.created_at;
-        delete eventItem.short_sha;
-        delete eventItem.commit_url;
+      delete eventItem.author.web_url;
+      delete eventItem.author.avatar_url;
+      delete eventItem.total_time;
+      delete eventItem.created_at;
+      delete eventItem.short_sha;
+      delete eventItem.commit_url;
 
-        newEvents.push(eventItem);
-      });
+      newEvents.push(eventItem);
+    });
 
-      return newEvents;
-    },
-    currentActiveStage() {
-      return this.state.stages.find(stage => stage.active);
-    },
-  };
-})(window.gl || (window.gl = {}));
+    return newEvents;
+  },
+  currentActiveStage() {
+    return this.state.stages.find(stage => stage.active);
+  },
+};
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 9ac4c49d697dbc5b05e4934e3b5573fb117b0d0b..b62b2cec4d8edf2d16e26578827a778182d00e7c 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -50,7 +50,7 @@ window.gl.GfmAutoComplete = {
     template: '<li>${title}</li>'
   },
   Loading: {
-    template: '<li style="pointer-events: none;"><i class="fa fa-refresh fa-spin"></i> Loading...</li>'
+    template: '<li style="pointer-events: none;"><i class="fa fa-spinner fa-spin"></i> Loading...</li>'
   },
   DefaultOptions: {
     sorter: function(query, items, searchKey) {
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 47e675f537e94af46e9fea18f774dd8efc271d45..011043e992fc29bc32b3fe304f1fdeced94319ef 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -20,57 +20,60 @@ class Issue {
       });
       Issue.initIssueBtnEventListeners();
     }
+
+    Issue.$btnNewBranch = $('#new-branch');
+
     Issue.initMergeRequests();
     Issue.initRelatedBranches();
     Issue.initCanCreateBranch();
   }
 
   static initIssueBtnEventListeners() {
-    var issueFailMessage;
-    issueFailMessage = 'Unable to update this issue at this time.';
-    return $('a.btn-close, a.btn-reopen').on('click', function(e) {
-      var $this, isClose, shouldSubmit, url;
+    const issueFailMessage = 'Unable to update this issue at this time.';
+
+    const closeButtons = $('a.btn-close');
+    const isClosedBadge = $('div.status-box-closed');
+    const isOpenBadge = $('div.status-box-open');
+    const projectIssuesCounter = $('.issue_counter');
+    const reopenButtons = $('a.btn-reopen');
+
+    return closeButtons.add(reopenButtons).on('click', function(e) {
+      var $this, shouldSubmit, url;
       e.preventDefault();
       e.stopImmediatePropagation();
       $this = $(this);
-      isClose = $this.hasClass('btn-close');
       shouldSubmit = $this.hasClass('btn-comment');
       if (shouldSubmit) {
         Issue.submitNoteForm($this.closest('form'));
       }
       $this.prop('disabled', true);
+      Issue.setNewBranchButtonState(true, null);
       url = $this.attr('href');
       return $.ajax({
         type: 'PUT',
-        url: url,
-        error: function(jqXHR, textStatus, errorThrown) {
-          var issueStatus;
-          issueStatus = isClose ? 'close' : 'open';
-          return new Flash(issueFailMessage, 'alert');
-        },
-        success: function(data, textStatus, jqXHR) {
-          if ('id' in data) {
-            $(document).trigger('issuable:change');
-            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');
-              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');
-              total += 1;
-            }
-            $('.issue_counter').text(gl.text.addDelimiter(total));
-          } else {
-            new Flash(issueFailMessage, 'alert');
-          }
-          return $this.prop('disabled', false);
+        url: url
+      }).fail(function(jqXHR, textStatus, errorThrown) {
+        new Flash(issueFailMessage);
+        Issue.initCanCreateBranch();
+      }).done(function(data, textStatus, jqXHR) {
+        if ('id' in data) {
+          $(document).trigger('issuable:change');
+
+          const isClosed = $this.hasClass('btn-close');
+          closeButtons.toggleClass('hidden', isClosed);
+          reopenButtons.toggleClass('hidden', !isClosed);
+          isClosedBadge.toggleClass('hidden', !isClosed);
+          isOpenBadge.toggleClass('hidden', isClosed);
+
+          let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, ''));
+          numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
+          projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
+        } else {
+          new Flash(issueFailMessage);
         }
+
+        $this.prop('disabled', false);
+        Issue.initCanCreateBranch();
       });
     });
   }
@@ -86,9 +89,9 @@ class Issue {
   static initMergeRequests() {
     var $container;
     $container = $('#merge-requests');
-    return $.getJSON($container.data('url')).error(function() {
-      return new Flash('Failed to load referenced merge requests', 'alert');
-    }).success(function(data) {
+    return $.getJSON($container.data('url')).fail(function() {
+      return new Flash('Failed to load referenced merge requests');
+    }).done(function(data) {
       if ('html' in data) {
         return $container.html(data.html);
       }
@@ -98,9 +101,9 @@ class Issue {
   static initRelatedBranches() {
     var $container;
     $container = $('#related-branches');
-    return $.getJSON($container.data('url')).error(function() {
-      return new Flash('Failed to load related branches', 'alert');
-    }).success(function(data) {
+    return $.getJSON($container.data('url')).fail(function() {
+      return new Flash('Failed to load related branches');
+    }).done(function(data) {
       if ('html' in data) {
         return $container.html(data.html);
       }
@@ -108,24 +111,27 @@ class Issue {
   }
 
   static initCanCreateBranch() {
-    var $container;
-    $container = $('#new-branch');
     // If the user doesn't have the required permissions the container isn't
     // rendered at all.
-    if ($container.length === 0) {
+    if (Issue.$btnNewBranch.length === 0) {
       return;
     }
-    return $.getJSON($container.data('path')).error(function() {
-      $container.find('.unavailable').show();
-      return new Flash('Failed to check if a new branch can be created.', 'alert');
-    }).success(function(data) {
-      if (data.can_create_branch) {
-        $container.find('.available').show();
-      } else {
-        return $container.find('.unavailable').show();
-      }
+    return $.getJSON(Issue.$btnNewBranch.data('path')).fail(function() {
+      Issue.setNewBranchButtonState(false, false);
+      new Flash('Failed to check if a new branch can be created.');
+    }).done(function(data) {
+      Issue.setNewBranchButtonState(false, data.can_create_branch);
     });
   }
+
+  static setNewBranchButtonState(isPending, canCreate) {
+    if (Issue.$btnNewBranch.length === 0) {
+      return;
+    }
+
+    Issue.$btnNewBranch.find('.available').toggle(!isPending && canCreate);
+    Issue.$btnNewBranch.find('.unavailable').toggle(!isPending && !canCreate);
+  }
 }
 
 export default Issue;
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 2e5f8a09fc1ba8642957d75feead5ee499379c09..fecd531328de5c8809cd9fa25e9204e2d40fd3e5 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,192 +1,188 @@
-/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len */
-
+/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, quotes, one-var, one-var-declaration-per-line, operator-assignment, no-else-return, prefer-template, prefer-arrow-callback, no-empty, max-len, consistent-return, no-unused-vars, no-return-assign, max-len, vars-on-top */
 require('vendor/latinise');
 
-(function() {
-  (function(w) {
-    var base;
-    if (w.gl == null) {
-      w.gl = {};
-    }
-    if ((base = w.gl).text == null) {
-      base.text = {};
+var base;
+var w = window;
+if (w.gl == null) {
+  w.gl = {};
+}
+if ((base = w.gl).text == null) {
+  base.text = {};
+}
+gl.text.addDelimiter = function(text) {
+  return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
+};
+gl.text.highCountTrim = function(count) {
+  return count > 99 ? '99+' : count;
+};
+gl.text.randomString = function() {
+  return Math.random().toString(36).substring(7);
+};
+gl.text.replaceRange = function(s, start, end, substitute) {
+  return s.substring(0, start) + substitute + s.substring(end);
+};
+gl.text.getTextWidth = function(text, font) {
+  /**
+  * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
+  *
+  * @param {String} text The text to be rendered.
+  * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
+  *
+  * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
+  */
+  // re-use canvas object for better performance
+  var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
+  var context = canvas.getContext('2d');
+  context.font = font;
+  return context.measureText(text).width;
+};
+gl.text.selectedText = function(text, textarea) {
+  return text.substring(textarea.selectionStart, textarea.selectionEnd);
+};
+gl.text.lineBefore = function(text, textarea) {
+  var split;
+  split = text.substring(0, textarea.selectionStart).trim().split('\n');
+  return split[split.length - 1];
+};
+gl.text.lineAfter = function(text, textarea) {
+  return text.substring(textarea.selectionEnd).trim().split('\n')[0];
+};
+gl.text.blockTagText = function(text, textArea, blockTag, selected) {
+  var lineAfter, lineBefore;
+  lineBefore = this.lineBefore(text, textArea);
+  lineAfter = this.lineAfter(text, textArea);
+  if (lineBefore === blockTag && lineAfter === blockTag) {
+    // To remove the block tag we have to select the line before & after
+    if (blockTag != null) {
+      textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
+      textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
     }
-    gl.text.addDelimiter = function(text) {
-      return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
-    };
-    gl.text.highCountTrim = function(count) {
-      return count > 99 ? '99+' : count;
-    };
-    gl.text.randomString = function() {
-      return Math.random().toString(36).substring(7);
-    };
-    gl.text.replaceRange = function(s, start, end, substitute) {
-      return s.substring(0, start) + substitute + s.substring(end);
-    };
-    gl.text.getTextWidth = function(text, font) {
-      /**
-      * Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
-      *
-      * @param {String} text The text to be rendered.
-      * @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
-      *
-      * @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
-      */
-      // re-use canvas object for better performance
-      var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
-      var context = canvas.getContext('2d');
-      context.font = font;
-      return context.measureText(text).width;
-    };
-    gl.text.selectedText = function(text, textarea) {
-      return text.substring(textarea.selectionStart, textarea.selectionEnd);
-    };
-    gl.text.lineBefore = function(text, textarea) {
-      var split;
-      split = text.substring(0, textarea.selectionStart).trim().split('\n');
-      return split[split.length - 1];
-    };
-    gl.text.lineAfter = function(text, textarea) {
-      return text.substring(textarea.selectionEnd).trim().split('\n')[0];
-    };
-    gl.text.blockTagText = function(text, textArea, blockTag, selected) {
-      var lineAfter, lineBefore;
-      lineBefore = this.lineBefore(text, textArea);
-      lineAfter = this.lineAfter(text, textArea);
-      if (lineBefore === blockTag && lineAfter === blockTag) {
-        // To remove the block tag we have to select the line before & after
-        if (blockTag != null) {
-          textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
-          textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
-        }
-        return selected;
-      } else {
-        return blockTag + "\n" + selected + "\n" + blockTag;
-      }
-    };
-    gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
-      var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
-      removedLastNewLine = false;
-      removedFirstNewLine = false;
-      currentLineEmpty = false;
+    return selected;
+  } else {
+    return blockTag + "\n" + selected + "\n" + blockTag;
+  }
+};
+gl.text.insertText = function(textArea, text, tag, blockTag, selected, wrap) {
+  var insertText, inserted, selectedSplit, startChar, removedLastNewLine, removedFirstNewLine, currentLineEmpty, lastNewLine;
+  removedLastNewLine = false;
+  removedFirstNewLine = false;
+  currentLineEmpty = false;
 
-      // Remove the first newline
-      if (selected.indexOf('\n') === 0) {
-        removedFirstNewLine = true;
-        selected = selected.replace(/\n+/, '');
-      }
+  // Remove the first newline
+  if (selected.indexOf('\n') === 0) {
+    removedFirstNewLine = true;
+    selected = selected.replace(/\n+/, '');
+  }
 
-      // Remove the last newline
-      if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
-        removedLastNewLine = true;
-        selected = selected.replace(/\n$/, '');
-      }
+  // Remove the last newline
+  if (textArea.selectionEnd - textArea.selectionStart > selected.replace(/\n$/, '').length) {
+    removedLastNewLine = true;
+    selected = selected.replace(/\n$/, '');
+  }
 
-      selectedSplit = selected.split('\n');
+  selectedSplit = selected.split('\n');
 
-      if (!wrap) {
-        lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
+  if (!wrap) {
+    lastNewLine = textArea.value.substr(0, textArea.selectionStart).lastIndexOf('\n');
 
-        // Check whether the current line is empty or consists only of spaces(=handle as empty)
-        if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
-          currentLineEmpty = true;
-        }
-      }
+    // Check whether the current line is empty or consists only of spaces(=handle as empty)
+    if (/^\s*$/.test(textArea.value.substring(lastNewLine, textArea.selectionStart))) {
+      currentLineEmpty = true;
+    }
+  }
 
-      startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
+  startChar = !wrap && !currentLineEmpty && textArea.selectionStart > 0 ? '\n' : '';
 
-      if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
-        if (blockTag != null) {
-          insertText = this.blockTagText(text, textArea, blockTag, selected);
+  if (selectedSplit.length > 1 && (!wrap || (blockTag != null))) {
+    if (blockTag != null) {
+      insertText = this.blockTagText(text, textArea, blockTag, selected);
+    } else {
+      insertText = selectedSplit.map(function(val) {
+        if (val.indexOf(tag) === 0) {
+          return "" + (val.replace(tag, ''));
         } else {
-          insertText = selectedSplit.map(function(val) {
-            if (val.indexOf(tag) === 0) {
-              return "" + (val.replace(tag, ''));
-            } else {
-              return "" + tag + val;
-            }
-          }).join('\n');
+          return "" + tag + val;
         }
-      } else {
-        insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
-      }
+      }).join('\n');
+    }
+  } else {
+    insertText = "" + startChar + tag + selected + (wrap ? tag : ' ');
+  }
 
-      if (removedFirstNewLine) {
-        insertText = '\n' + insertText;
-      }
+  if (removedFirstNewLine) {
+    insertText = '\n' + insertText;
+  }
 
-      if (removedLastNewLine) {
-        insertText += '\n';
-      }
+  if (removedLastNewLine) {
+    insertText += '\n';
+  }
 
-      if (document.queryCommandSupported('insertText')) {
-        inserted = document.execCommand('insertText', false, insertText);
-      }
-      if (!inserted) {
-        try {
-          document.execCommand("ms-beginUndoUnit");
-        } catch (error) {}
-        textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
-        try {
-          document.execCommand("ms-endUndoUnit");
-        } catch (error) {}
-      }
-      return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
-    };
-    gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
-      var pos;
-      if (!textArea.setSelectionRange) {
-        return;
-      }
-      if (textArea.selectionStart === textArea.selectionEnd) {
-        if (wrapped) {
-          pos = textArea.selectionStart - tag.length;
-        } else {
-          pos = textArea.selectionStart;
-        }
+  if (document.queryCommandSupported('insertText')) {
+    inserted = document.execCommand('insertText', false, insertText);
+  }
+  if (!inserted) {
+    try {
+      document.execCommand("ms-beginUndoUnit");
+    } catch (error) {}
+    textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
+    try {
+      document.execCommand("ms-endUndoUnit");
+    } catch (error) {}
+  }
+  return this.moveCursor(textArea, tag, wrap, removedLastNewLine);
+};
+gl.text.moveCursor = function(textArea, tag, wrapped, removedLastNewLine) {
+  var pos;
+  if (!textArea.setSelectionRange) {
+    return;
+  }
+  if (textArea.selectionStart === textArea.selectionEnd) {
+    if (wrapped) {
+      pos = textArea.selectionStart - tag.length;
+    } else {
+      pos = textArea.selectionStart;
+    }
 
-        if (removedLastNewLine) {
-          pos -= 1;
-        }
+    if (removedLastNewLine) {
+      pos -= 1;
+    }
 
-        return textArea.setSelectionRange(pos, pos);
-      }
-    };
-    gl.text.updateText = function(textArea, tag, blockTag, wrap) {
-      var $textArea, selected, text;
-      $textArea = $(textArea);
-      textArea = $textArea.get(0);
-      text = $textArea.val();
-      selected = this.selectedText(text, textArea);
-      $textArea.focus();
-      return this.insertText(textArea, text, tag, blockTag, selected, wrap);
-    };
-    gl.text.init = function(form) {
-      var self;
-      self = this;
-      return $('.js-md', form).off('click').on('click', function() {
-        var $this;
-        $this = $(this);
-        return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
-      });
-    };
-    gl.text.removeListeners = function(form) {
-      return $('.js-md', form).off();
-    };
-    gl.text.humanize = function(string) {
-      return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
-    };
-    gl.text.pluralize = function(str, count) {
-      return str + (count > 1 || count === 0 ? 's' : '');
-    };
-    gl.text.truncate = function(string, maxLength) {
-      return string.substr(0, (maxLength - 3)) + '...';
-    };
-    gl.text.dasherize = function(str) {
-      return str.replace(/[_\s]+/g, '-');
-    };
-    gl.text.slugify = function(str) {
-      return str.trim().toLowerCase().latinise();
-    };
-  })(window);
-}).call(window);
+    return textArea.setSelectionRange(pos, pos);
+  }
+};
+gl.text.updateText = function(textArea, tag, blockTag, wrap) {
+  var $textArea, selected, text;
+  $textArea = $(textArea);
+  textArea = $textArea.get(0);
+  text = $textArea.val();
+  selected = this.selectedText(text, textArea);
+  $textArea.focus();
+  return this.insertText(textArea, text, tag, blockTag, selected, wrap);
+};
+gl.text.init = function(form) {
+  var self;
+  self = this;
+  return $('.js-md', form).off('click').on('click', function() {
+    var $this;
+    $this = $(this);
+    return self.updateText($this.closest('.md-area').find('textarea'), $this.data('md-tag'), $this.data('md-block'), !$this.data('md-prepend'));
+  });
+};
+gl.text.removeListeners = function(form) {
+  return $('.js-md', form).off();
+};
+gl.text.humanize = function(string) {
+  return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
+};
+gl.text.pluralize = function(str, count) {
+  return str + (count > 1 || count === 0 ? 's' : '');
+};
+gl.text.truncate = function(string, maxLength) {
+  return string.substr(0, (maxLength - 3)) + '...';
+};
+gl.text.dasherize = function(str) {
+  return str.replace(/[_\s]+/g, '-');
+};
+gl.text.slugify = function(str) {
+  return str.trim().toLowerCase().latinise();
+};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 09c4261b3187d25d5486220db2a7035844749b7f..b9d2fc25c39dca4fc0e5e446c1bfc274e6defdd5 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,93 +1,90 @@
 /* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, one-var, one-var-declaration-per-line, no-void, guard-for-in, no-restricted-syntax, prefer-template, quotes, max-len */
-(function() {
-  (function(w) {
-    var base;
-    if (w.gl == null) {
-      w.gl = {};
+var base;
+var w = window;
+if (w.gl == null) {
+  w.gl = {};
+}
+if ((base = w.gl).utils == null) {
+  base.utils = {};
+}
+// Returns an array containing the value(s) of the
+// of the key passed as an argument
+w.gl.utils.getParameterValues = function(sParam) {
+  var i, sPageURL, sParameterName, sURLVariables, values;
+  sPageURL = decodeURIComponent(window.location.search.substring(1));
+  sURLVariables = sPageURL.split('&');
+  sParameterName = void 0;
+  values = [];
+  i = 0;
+  while (i < sURLVariables.length) {
+    sParameterName = sURLVariables[i].split('=');
+    if (sParameterName[0] === sParam) {
+      values.push(sParameterName[1].replace(/\+/g, ' '));
     }
-    if ((base = w.gl).utils == null) {
-      base.utils = {};
+    i += 1;
+  }
+  return values;
+};
+// @param {Object} params - url keys and value to merge
+// @param {String} url
+w.gl.utils.mergeUrlParams = function(params, url) {
+  var lastChar, newUrl, paramName, paramValue, pattern;
+  newUrl = decodeURIComponent(url);
+  for (paramName in params) {
+    paramValue = params[paramName];
+    pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)");
+    if (paramValue == null) {
+      newUrl = newUrl.replace(pattern, '');
+    } else if (url.search(pattern) !== -1) {
+      newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2");
+    } else {
+      newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
     }
-    // Returns an array containing the value(s) of the
-    // of the key passed as an argument
-    w.gl.utils.getParameterValues = function(sParam) {
-      var i, sPageURL, sParameterName, sURLVariables, values;
-      sPageURL = decodeURIComponent(window.location.search.substring(1));
-      sURLVariables = sPageURL.split('&');
-      sParameterName = void 0;
-      values = [];
-      i = 0;
-      while (i < sURLVariables.length) {
-        sParameterName = sURLVariables[i].split('=');
-        if (sParameterName[0] === sParam) {
-          values.push(sParameterName[1].replace(/\+/g, ' '));
-        }
-        i += 1;
+  }
+  // Remove a trailing ampersand
+  lastChar = newUrl[newUrl.length - 1];
+  if (lastChar === '&') {
+    newUrl = newUrl.slice(0, -1);
+  }
+  return newUrl;
+};
+// removes parameter query string from url. returns the modified url
+w.gl.utils.removeParamQueryString = function(url, param) {
+  var urlVariables, variables;
+  url = decodeURIComponent(url);
+  urlVariables = url.split('&');
+  return ((function() {
+    var j, len, results;
+    results = [];
+    for (j = 0, len = urlVariables.length; j < len; j += 1) {
+      variables = urlVariables[j];
+      if (variables.indexOf(param) === -1) {
+        results.push(variables);
       }
-      return values;
-    };
-    // @param {Object} params - url keys and value to merge
-    // @param {String} url
-    w.gl.utils.mergeUrlParams = function(params, url) {
-      var lastChar, newUrl, paramName, paramValue, pattern;
-      newUrl = decodeURIComponent(url);
-      for (paramName in params) {
-        paramValue = params[paramName];
-        pattern = new RegExp("\\b(" + paramName + "=).*?(&|$)");
-        if (paramValue == null) {
-          newUrl = newUrl.replace(pattern, '');
-        } else if (url.search(pattern) !== -1) {
-          newUrl = newUrl.replace(pattern, "$1" + paramValue + "$2");
-        } else {
-          newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
-        }
-      }
-      // Remove a trailing ampersand
-      lastChar = newUrl[newUrl.length - 1];
-      if (lastChar === '&') {
-        newUrl = newUrl.slice(0, -1);
-      }
-      return newUrl;
-    };
-    // removes parameter query string from url. returns the modified url
-    w.gl.utils.removeParamQueryString = function(url, param) {
-      var urlVariables, variables;
-      url = decodeURIComponent(url);
-      urlVariables = url.split('&');
-      return ((function() {
-        var j, len, results;
-        results = [];
-        for (j = 0, len = urlVariables.length; j < len; j += 1) {
-          variables = urlVariables[j];
-          if (variables.indexOf(param) === -1) {
-            results.push(variables);
-          }
-        }
-        return results;
-      })()).join('&');
-    };
-    w.gl.utils.removeParams = (params) => {
-      const url = new URL(window.location.href);
-      params.forEach((param) => {
-        url.search = w.gl.utils.removeParamQueryString(url.search, param);
-      });
-      return url.href;
-    };
-    w.gl.utils.getLocationHash = function(url) {
-      var hashIndex;
-      if (typeof url === 'undefined') {
-        // Note: We can't use window.location.hash here because it's
-        // not consistent across browsers - Firefox will pre-decode it
-        url = window.location.href;
-      }
-      hashIndex = url.indexOf('#');
-      return hashIndex === -1 ? null : url.substring(hashIndex + 1);
-    };
+    }
+    return results;
+  })()).join('&');
+};
+w.gl.utils.removeParams = (params) => {
+  const url = new URL(window.location.href);
+  params.forEach((param) => {
+    url.search = w.gl.utils.removeParamQueryString(url.search, param);
+  });
+  return url.href;
+};
+w.gl.utils.getLocationHash = function(url) {
+  var hashIndex;
+  if (typeof url === 'undefined') {
+    // Note: We can't use window.location.hash here because it's
+    // not consistent across browsers - Firefox will pre-decode it
+    url = window.location.href;
+  }
+  hashIndex = url.indexOf('#');
+  return hashIndex === -1 ? null : url.substring(hashIndex + 1);
+};
 
-    w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
+w.gl.utils.refreshCurrentPage = () => gl.utils.visitUrl(document.location.href);
 
-    w.gl.utils.visitUrl = (url) => {
-      document.location.href = url;
-    };
-  })(window);
-}).call(window);
+w.gl.utils.visitUrl = (url) => {
+  document.location.href = url;
+};
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
index 9548a98f499ec87f0777a3e1961e168102540d9b..7b0997c6520d0ec6c0f1b0a84230c13a7c3b4c3b 100644
--- a/app/assets/javascripts/merged_buttons.js
+++ b/app/assets/javascripts/merged_buttons.js
@@ -1,11 +1,13 @@
 /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, max-len */
 
-(function() {
-  var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
+import '~/lib/utils/url_utility';
 
+(function() {
   this.MergedButtons = (function() {
     function MergedButtons() {
-      this.removeSourceBranch = bind(this.removeSourceBranch, this);
+      this.removeSourceBranch = this.removeSourceBranch.bind(this);
+      this.removeBranchSuccess = this.removeBranchSuccess.bind(this);
+      this.removeBranchError = this.removeBranchError.bind(this);
       this.$removeBranchWidget = $('.remove_source_branch_widget');
       this.$removeBranchProgress = $('.remove_source_branch_in_progress');
       this.$removeBranchFailed = $('.remove_source_branch_widget.failed');
@@ -22,7 +24,7 @@
     MergedButtons.prototype.initEventListeners = function() {
       $(document).on('click', '.remove_source_branch', this.removeSourceBranch);
       $(document).on('ajax:success', '.remove_source_branch', this.removeBranchSuccess);
-      return $(document).on('ajax:error', '.remove_source_branch', this.removeBranchError);
+      $(document).on('ajax:error', '.remove_source_branch', this.removeBranchError);
     };
 
     MergedButtons.prototype.removeSourceBranch = function() {
@@ -31,7 +33,7 @@
     };
 
     MergedButtons.prototype.removeBranchSuccess = function() {
-      return location.reload();
+      gl.utils.refreshCurrentPage();
     };
 
     MergedButtons.prototype.removeBranchError = function() {
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 9a0f7a14e570627a75d15c470243cb07fe078904..759401a78066e24da1604e904b35d05670c46f54 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -5,7 +5,7 @@
   direction: rtl;
 
   @media (min-width: $screen-sm-min) and (max-width: $screen-md-max) {
-    overflow-x: scroll;
+    overflow-x: auto;
   }
 }
 
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 12465d4a70b18256af9247b28056e230ad47625d..11d44df48671469c89c7e29db55c4604e4396e32 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -82,7 +82,7 @@
   .input-token:last-child {
     flex: 1;
     -webkit-flex: 1;
-    max-width: initial;
+    max-width: inherit;
   }
 }
 
@@ -246,17 +246,17 @@
   }
 }
 
-.filtered-search-history-dropdown-toggle-button {
+.filtered-search-history-dropdown-wrapper {
+  position: static;
   display: flex;
-  align-items: center;
+  flex-direction: column;
+}
+
+.filtered-search-history-dropdown-toggle-button {
+  flex: 1;
   width: auto;
-  height: 100%;
-  padding-top: 0;
-  padding-left: 0.75em;
-  padding-bottom: 0;
-  padding-right: 0.5em;
+  padding-right: 10px;
 
-  background-color: transparent;
   border-radius: 0;
   border-top: 0;
   border-left: 0;
@@ -264,6 +264,7 @@
   border-right: 1px solid $border-color;
 
   color: $gl-text-color-secondary;
+  line-height: 1;
 
   transition: color 0.1s linear;
 
@@ -275,24 +276,21 @@
   }
 
   .dropdown-toggle-text {
+    display: inline-block;
     color: inherit;
 
     .fa {
+      vertical-align: middle;
       color: inherit;
     }
   }
 
   .fa {
-    position: initial;
+    position: static;
   }
 
 }
 
-.filtered-search-history-dropdown-wrapper {
-  position: initial;
-  flex-shrink: 0;
-}
-
 .filtered-search-history-dropdown {
   width: 40%;
 
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index c241816788b4f9ee361fde5c5a5f9a4601dfbcbc..664539e93e1be8c2255eb2d8dfe24e53b2c2384a 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -158,6 +158,7 @@
     li.task-list-item {
       list-style-type: none;
       position: relative;
+      min-height: 22px;
       padding-left: 28px;
       margin-left: 0 !important;
 
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 712eb7caf33f898b0b3ae1478d44603952dfaba8..20ef9a774e4ff37548d752568a4357e99751a407 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -26,6 +26,7 @@ $gray-dark: darken($gray-light, $darken-dark-factor);
 $gray-darker: #eee;
 $gray-darkest: #c4c4c4;
 
+$green-25: #f6fcf8;
 $green-50: #e4f5eb;
 $green-100: #bae6cc;
 $green-200: #8dd5aa;
@@ -37,6 +38,7 @@ $green-700: #12753a;
 $green-800: #0e5a2d;
 $green-900: #0a4020;
 
+$blue-25: #f6fafd;
 $blue-50: #e4eff9;
 $blue-100: #bcd7f1;
 $blue-200: #8fbce8;
@@ -48,6 +50,7 @@ $blue-700: #17599c;
 $blue-800: #134a81;
 $blue-900: #0f3b66;
 
+$orange-25: #fffcf8;
 $orange-50: #fff2e1;
 $orange-100: #fedfb3;
 $orange-200: #feca81;
@@ -59,6 +62,7 @@ $orange-700: #c26700;
 $orange-800: #a35100;
 $orange-900: #853b00;
 
+$red-25: #fef7f6;
 $red-50: #fbe7e4;
 $red-100: #f4c4bc;
 $red-200: #ed9d90;
@@ -147,7 +151,7 @@ $gl-sidebar-padding: 22px;
 /*
  * Misc
  */
-$row-hover: lighten($blue-50, 2%);
+$row-hover: $blue-25;
 $row-hover-border: $blue-100;
 $progress-color: #c0392b;
 $header-height: 50px;
@@ -223,18 +227,18 @@ $gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background;
 /*
  * Commit Diff Colors
  */
-$added: $green-300;
-$deleted: $red-300;
-$line-added: $green-50;
-$line-added-dark: $green-100;
-$line-removed: $red-50;
-$line-removed-dark: $red-100;
-$line-number-old: lighten($red-100, 5%);
-$line-number-new: lighten($green-100, 5%);
-$line-number-select: lighten($orange-100, 5%);
-$line-target-blue: $blue-50;
-$line-select-yellow: $orange-50;
-$line-select-yellow-dark: $orange-100;
+$added: #63c363;
+$deleted: #f77;
+$line-added: #ecfdf0;
+$line-added-dark: #c7f0d2;
+$line-removed: #fbe9eb;
+$line-removed-dark: #fac5cd;
+$line-number-old: #f9d7dc;
+$line-number-new: #ddfbe6;
+$line-number-select: #fbf2da;
+$line-target-blue: #f6faff;
+$line-select-yellow: #fcf8e7;
+$line-select-yellow-dark: #f0e2bd;
 $dark-diff-match-bg: rgba(255, 255, 255, 0.3);
 $dark-diff-match-color: rgba(255, 255, 255, 0.1);
 $file-mode-changed: #777;
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 03fddaeb163f61ff2c95f6db6669647a4b3bd27f..144adbcdaef3235de782fcb6b49a319e72fed3f8 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -39,7 +39,7 @@
     overflow-y: hidden;
     font-size: 12px;
 
-    .fa-refresh {
+    .fa-spinner {
       font-size: 24px;
       margin-left: 20px;
     }
@@ -219,7 +219,7 @@
   font-size: 12px;
   position: relative;
 
-  .fa-refresh {
+  .fa-spinner {
     font-size: 24px;
   }
 
@@ -366,7 +366,7 @@
         background-color: $row-hover;
       }
 
-      .fa-refresh {
+      .fa-spinner {
         font-size: 13px;
         margin-left: 3px;
       }
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 79da490675abc9ebc25d7934a86940391cbbbf84..5b723f7c7228c96aa5d1d19141a96533c192b0bc 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -10,10 +10,14 @@
   position: relative;
 
   &.event-inline {
-    .profile-icon {
+    .system-note-image {
       top: 20px;
     }
 
+    .user-avatar {
+      top: 14px;
+    }
+
     .event-title,
     .event-item-timestamp {
       line-height: 40px;
@@ -24,7 +28,7 @@
     color: $gl-text-color;
   }
 
-  .profile-icon {
+  .system-note-image {
     position: absolute;
     left: 0;
     top: 14px;
@@ -35,15 +39,18 @@
       fill: $gl-text-color-secondary;
     }
 
-    &.open-icon svg {
-      fill: $green-300;
+    &.opened-icon,
+    &.created-icon {
+      svg {
+        fill: $green-300;
+      }
     }
 
     &.closed-icon svg {
       fill: $red-300;
     }
 
-    &.fork-icon svg {
+    &.accepted-icon svg {
       fill: $blue-300;
     }
   }
@@ -128,8 +135,7 @@
     li {
       &.commit {
         background: transparent;
-        padding: 3px;
-        padding-left: 0;
+        padding: 0;
         border: none;
 
         .commit-row-title {
@@ -183,7 +189,7 @@
       max-width: 100%;
     }
 
-    .profile-icon {
+    .system-note-image {
       display: none;
     }
 
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 94ea4c5c8c6ef098e945edf139a691b828a74121..c78fb8ede79d595860e721f1893612e012d009f4 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -18,12 +18,12 @@ ul.notes {
     float: left;
 
     svg {
-      width: 18px;
-      height: 18px;
+      width: 16px;
+      height: 16px;
       fill: $gray-darkest;
       position: absolute;
-      left: 30px;
-      top: 15px;
+      left: 0;
+      top: 16px;
     }
   }
 
@@ -144,6 +144,10 @@ ul.notes {
     padding: 0;
     clear: both;
 
+    @media (min-width: $screen-sm-min) {
+      margin-left: 65px;
+    }
+
     &.timeline-entry::after {
       clear: none;
     }
@@ -172,6 +176,10 @@ ul.notes {
 
     .timeline-content {
       padding: 14px 10px;
+
+      @media (min-width: $screen-sm-min) {
+        margin-left: 20px;
+      }
     }
 
     .note-header {
@@ -619,7 +627,6 @@ ul.notes {
   }
 
   &:not(.is-disabled):hover,
-  &:not(.is-disabled):focus,
   &.is-active {
     color: $gl-text-green;
 
@@ -633,6 +640,11 @@ ul.notes {
     height: 15px;
     width: 15px;
   }
+
+  .loading {
+    margin: 0;
+    height: auto;
+  }
 }
 
 .discussion-next-btn {
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index b668a9331e710ce9a6abf3b616dcbc1f0e61ab90..1e41f980f3173267d6aa66c2ac4c00f5a940987a 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -10,7 +10,7 @@ class Projects::HooksController < Projects::ApplicationController
     @hook = @project.hooks.new(hook_params)
     @hook.save
 
-    unless @hook.valid?      
+    unless @hook.valid?
       @hooks = @project.hooks.select(&:persisted?)
       flash[:alert] = @hook.errors.full_messages.join.html_safe
     end
@@ -49,7 +49,7 @@ class Projects::HooksController < Projects::ApplicationController
 
   def hook_params
     params.require(:hook).permit(
-      :build_events,
+      :job_events,
       :pipeline_events,
       :enable_ssl_verification,
       :issues_events,
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 224b44db39778e0951514235715227a49f159109..09dc8b38229bc3311faebb8d8898d80fcb7dbd04 100755
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -38,7 +38,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     @collection_type    = "MergeRequest"
     @merge_requests     = merge_requests_collection
     @merge_requests     = @merge_requests.page(params[:page])
-    @merge_requests     = @merge_requests.includes(merge_request_diff: :merge_request)
+    @merge_requests     = @merge_requests.preload(merge_request_diff: :merge_request)
     @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
 
     if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 8109427a45f34cfeb6b1d0f78f98b22062e18c25..3ca14dee33c8e7b5acd57fd62930a499c3bb240a 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -60,7 +60,7 @@ class RegistrationsController < Devise::RegistrationsController
   end
 
   def resource
-    @resource ||= Users::CreateService.new(current_user, sign_up_params).build
+    @resource ||= Users::BuildService.new(current_user, sign_up_params).execute
   end
 
   def devise_mapping
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index fb872a13f740a40c325389b2a8552b2808b25337..5f5c76d3722e8ea6a9299585278e97f0e9f6800c 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -1,4 +1,15 @@
 module EventsHelper
+  ICON_NAMES_BY_EVENT_TYPE = {
+    'pushed to' => 'icon_commit',
+    'pushed new' => 'icon_commit',
+    'created' => 'icon_status_open',
+    'opened' => 'icon_status_open',
+    'closed' => 'icon_status_closed',
+    'accepted' => 'icon_code_fork',
+    'commented on' => 'icon_comment_o',
+    'deleted' => 'icon_trash_o'
+  }.freeze
+
   def link_to_author(event)
     author = event.author
 
@@ -183,4 +194,21 @@ module EventsHelper
       "event-inline"
     end
   end
+
+  def icon_for_event(note)
+    icon_name = ICON_NAMES_BY_EVENT_TYPE[note]
+    custom_icon(icon_name) if icon_name
+  end
+
+  def icon_for_profile_event(event)
+    if current_path?('users#show')
+      content_tag :div, class: "system-note-image #{event.action_name.parameterize}-icon" do
+        icon_for_event(event.action_name)
+      end
+    else
+      content_tag :div, class: 'system-note-image user-avatar' do
+        author_avatar(event, size: 32)
+      end
+    end
+  end
 end
diff --git a/app/helpers/javascript_helper.rb b/app/helpers/javascript_helper.rb
index 68c09c922a67533eb826777cc85decdacfce2b7a..d5e77c7e271e315c0d7cea4ce4b6994c5d2a32dc 100644
--- a/app/helpers/javascript_helper.rb
+++ b/app/helpers/javascript_helper.rb
@@ -3,7 +3,8 @@ module JavascriptHelper
     javascript_include_tag asset_path(js)
   end
 
-  def page_specific_javascript_bundle_tag(js)
-    javascript_include_tag(*webpack_asset_paths(js))
+  # deprecated; use webpack_bundle_tag directly instead
+  def page_specific_javascript_bundle_tag(bundle)
+    webpack_bundle_tag(bundle)
   end
 end
diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6bacda9fe75153913e25ea770a344247629eb3b1
--- /dev/null
+++ b/app/helpers/webpack_helper.rb
@@ -0,0 +1,30 @@
+require 'webpack/rails/manifest'
+
+module WebpackHelper
+  def webpack_bundle_tag(bundle)
+    javascript_include_tag(*gitlab_webpack_asset_paths(bundle))
+  end
+
+  # override webpack-rails gem helper until changes can make it upstream
+  def gitlab_webpack_asset_paths(source, extension: nil)
+    return "" unless source.present?
+
+    paths = Webpack::Rails::Manifest.asset_paths(source)
+    if extension
+      paths = paths.select { |p| p.ends_with? ".#{extension}" }
+    end
+
+    # include full webpack-dev-server url for rspec tests running locally
+    if Rails.env.test? && Rails.configuration.webpack.dev_server.enabled
+      host = Rails.configuration.webpack.dev_server.host
+      port = Rails.configuration.webpack.dev_server.port
+      protocol = Rails.configuration.webpack.dev_server.https ? 'https' : 'http'
+
+      paths.map! do |p|
+        "#{protocol}://#{host}:#{port}#{p}"
+      end
+    end
+
+    paths
+  end
+end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 5c452f7854647b1badfb66f5e8abda99a4ce7c94..8b8b3f002020db0f2b061582fdd973541d592c49 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -326,14 +326,13 @@ class Commit
   end
 
   def 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
+    # NOTE: This feature is intentionally disabled until
+    # https://gitlab.com/gitlab-org/gitaly/issues/178 is resolved
+    # if Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
+    #   Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
+    # else
+    raw.diffs(*args)
+    # end
   end
 
   def diffs(diff_options = nil)
diff --git a/app/models/concerns/discussion_on_diff.rb b/app/models/concerns/discussion_on_diff.rb
index 67b1cace3eb44ba7ceff27df2d9904bdb7b15cb3..8ee42875670c43717d10ae8a6f316c835c0405af 100644
--- a/app/models/concerns/discussion_on_diff.rb
+++ b/app/models/concerns/discussion_on_diff.rb
@@ -2,9 +2,9 @@
 module DiscussionOnDiff
   extend ActiveSupport::Concern
 
-  included do
-    NUMBER_OF_TRUNCATED_DIFF_LINES = 16
+  NUMBER_OF_TRUNCATED_DIFF_LINES = 16
 
+  included do
     delegate  :line_code,
               :original_line_code,
               :diff_file,
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 82f4182d59a91c7a943b672cfeb6e372a7db1dbb..d0c94d3b6944c82124b628a8cd37063bba1b7cb6 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -20,7 +20,8 @@ class ContainerRepository < ActiveRecord::Base
   end
 
   def path
-    @path ||= [project.full_path, name].select(&:present?).join('/')
+    @path ||= [project.full_path, name]
+      .select(&:present?).join('/').downcase
   end
 
   def location
diff --git a/app/models/label.rb b/app/models/label.rb
index 568fa6d44f520fd80c9c4e464a577681b873212c..d8b0e250732731ac00c74cb44b112dc648cf43fd 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -21,6 +21,8 @@ class Label < ActiveRecord::Base
   has_many :issues, through: :label_links, source: :target, source_type: 'Issue'
   has_many :merge_requests, through: :label_links, source: :target, source_type: 'MergeRequest'
 
+  before_validation :strip_whitespace_from_title_and_color
+
   validates :color, color: true, allow_blank: false
 
   # Don't allow ',' for label titles
@@ -193,4 +195,8 @@ class Label < ActiveRecord::Base
   def sanitize_title(value)
     CGI.unescapeHTML(Sanitize.clean(value.to_s))
   end
+
+  def strip_whitespace_from_title_and_color
+    %w(color title).each { |attr| self[attr] = self[attr]&.strip }
+  end
 end
diff --git a/app/models/note.rb b/app/models/note.rb
index c85692c5aec17d5b9e5a402db663250f34efd3a2..630d0adbece66b61fa505004626931265a5b5e24 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -96,6 +96,7 @@ class Note < ActiveRecord::Base
   before_validation :set_discussion_id, on: :create
   after_save :keep_around_commit, unless: :for_personal_snippet?
   after_save :expire_etag_cache
+  after_destroy :expire_etag_cache
 
   class << self
     def model_name
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 526ab1e77a7e9f444e987a100fef6ddb892b745b..2b11ed6128ecff0b0f9dfcd68355d02a80fd71f3 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -963,13 +963,15 @@ class Repository
   end
 
   def is_ancestor?(ancestor_id, descendant_id)
-    Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
-      if is_enabled
-        raw_repository.is_ancestor?(ancestor_id, descendant_id)
-      else
-        merge_base_commit(ancestor_id, descendant_id) == ancestor_id
-      end
-    end
+    # NOTE: This feature is intentionally disabled until
+    # https://gitlab.com/gitlab-org/gitlab-ce/issues/30586 is resolved
+    # Gitlab::GitalyClient.migrate(:is_ancestor) do |is_enabled|
+    #   if is_enabled
+    #     raw_repository.is_ancestor?(ancestor_id, descendant_id)
+    #   else
+    merge_base_commit(ancestor_id, descendant_id) == ancestor_id
+    #   end
+    # end
   end
 
   def empty_repo?
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index cb58c115d542b1db467243e94b994a792224200f..87398303c687100b56188127cabb994848234311 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -28,6 +28,7 @@ class GroupPolicy < BasePolicy
       can! :admin_namespace
       can! :admin_group_member
       can! :change_visibility_level
+      can! :create_subgroup if @user.can_create_group
     end
 
     if globally_viewable && @subject.request_access_enabled && !member
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index fbdaa45565104fbcf1d81f141428e6d79703ceec..7828c5806b06c9090daa8261137a7a055bf65100 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -58,6 +58,9 @@ module Projects
         fail(error: @project.errors.full_messages.join(', '))
       end
       @project
+    rescue ActiveRecord::RecordInvalid => e
+      message = "Unable to save #{e.record.type}: #{e.record.errors.full_messages.join(", ")} "
+      fail(error: message)
     rescue => e
       fail(error: e.message)
     end
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9a0a5a12f91817b8442a357461844ff482a582be
--- /dev/null
+++ b/app/services/users/build_service.rb
@@ -0,0 +1,100 @@
+module Users
+  # Service for building a new user.
+  class BuildService < BaseService
+    def initialize(current_user, params = {})
+      @current_user = current_user
+      @params = params.dup
+    end
+
+    def execute
+      raise Gitlab::Access::AccessDeniedError unless can_create_user?
+
+      user = User.new(build_user_params)
+
+      if current_user&.admin?
+        if params[:reset_password]
+          user.generate_reset_token
+          params[:force_random_password] = true
+        end
+
+        if params[:force_random_password]
+          random_password = Devise.friendly_token.first(Devise.password_length.min)
+          user.password = user.password_confirmation = random_password
+        end
+      end
+
+      identity_attrs = params.slice(:extern_uid, :provider)
+
+      if identity_attrs.any?
+        user.identities.build(identity_attrs)
+      end
+
+      user
+    end
+
+    private
+
+    def can_create_user?
+      (current_user.nil? && current_application_settings.signup_enabled?) || current_user&.admin?
+    end
+
+    # Allowed params for creating a user (admins only)
+    def admin_create_params
+      [
+        :access_level,
+        :admin,
+        :avatar,
+        :bio,
+        :can_create_group,
+        :color_scheme_id,
+        :email,
+        :external,
+        :force_random_password,
+        :hide_no_password,
+        :hide_no_ssh_key,
+        :key_id,
+        :linkedin,
+        :name,
+        :password,
+        :password_automatically_set,
+        :password_expires_at,
+        :projects_limit,
+        :remember_me,
+        :skip_confirmation,
+        :skype,
+        :theme_id,
+        :twitter,
+        :username,
+        :website_url
+      ]
+    end
+
+    # Allowed params for user signup
+    def signup_params
+      [
+        :email,
+        :email_confirmation,
+        :password_automatically_set,
+        :name,
+        :password,
+        :username
+      ]
+    end
+
+    def build_user_params
+      if current_user&.admin?
+        user_params = params.slice(*admin_create_params)
+        user_params[:created_by_id] = current_user&.id
+
+        if params[:reset_password]
+          user_params.merge!(force_random_password: true, password_expires_at: nil)
+        end
+      else
+        user_params = params.slice(*signup_params)
+        user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email
+      end
+
+      user_params
+    end
+  end
+end
diff --git a/app/services/users/create_service.rb b/app/services/users/create_service.rb
index 93ca7b1141a18ddccfa238f21621f164aa57c8b8..a2105d31f71f3f2281f1f5028d5e95cc47e8f48d 100644
--- a/app/services/users/create_service.rb
+++ b/app/services/users/create_service.rb
@@ -6,34 +6,10 @@ module Users
       @params = params.dup
     end
 
-    def build
-      raise Gitlab::Access::AccessDeniedError unless can_create_user?
-
-      user = User.new(build_user_params)
-
-      if current_user&.admin?
-        if params[:reset_password]
-          @reset_token = user.generate_reset_token
-          params[:force_random_password] = true
-        end
-
-        if params[:force_random_password]
-          random_password = Devise.friendly_token.first(Devise.password_length.min)
-          user.password = user.password_confirmation = random_password
-        end
-      end
-
-      identity_attrs = params.slice(:extern_uid, :provider)
-
-      if identity_attrs.any?
-        user.identities.build(identity_attrs)
-      end
-
-      user
-    end
-
     def execute
-      user = build
+      user = Users::BuildService.new(current_user, params).execute
+
+      @reset_token = user.generate_reset_token if user.recently_sent_password_reset?
 
       if user.save
         log_info("User \"#{user.name}\" (#{user.email}) was created")
@@ -43,70 +19,5 @@ module Users
 
       user
     end
-
-    private
-
-    def can_create_user?
-      (current_user.nil? && current_application_settings.signup_enabled?) || current_user&.admin?
-    end
-
-    # Allowed params for creating a user (admins only)
-    def admin_create_params
-      [
-        :access_level,
-        :admin,
-        :avatar,
-        :bio,
-        :can_create_group,
-        :color_scheme_id,
-        :email,
-        :external,
-        :force_random_password,
-        :password_automatically_set,
-        :hide_no_password,
-        :hide_no_ssh_key,
-        :key_id,
-        :linkedin,
-        :name,
-        :password,
-        :password_expires_at,
-        :projects_limit,
-        :remember_me,
-        :skip_confirmation,
-        :skype,
-        :theme_id,
-        :twitter,
-        :username,
-        :website_url
-      ]
-    end
-
-    # Allowed params for user signup
-    def signup_params
-      [
-        :email,
-        :email_confirmation,
-        :password_automatically_set,
-        :name,
-        :password,
-        :username
-      ]
-    end
-
-    def build_user_params
-      if current_user&.admin?
-        user_params = params.slice(*admin_create_params)
-        user_params[:created_by_id] = current_user&.id
-
-        if params[:reset_password]
-          user_params.merge!(force_random_password: true, password_expires_at: nil)
-        end
-      else
-        user_params = params.slice(*signup_params)
-        user_params[:skip_confirmation] = !current_application_settings.send_user_confirmation_email
-      end
-
-      user_params
-    end
   end
 end
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index e79303240f0d6b08deb827865fb0d221ae356617..6a208d76a38cfca7630c1b7d1264691989d858de 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -13,7 +13,7 @@
     = button_to reset_health_check_token_admin_application_settings_path,
       method: :put, class: 'btn btn-default',
       data: { confirm: 'Are you sure you want to reset the health check token?' } do
-      = icon('refresh')
+      = icon('spinner')
       Reset health check access token
   %p.light
     Health information can be retrieved as plain text, JSON, or XML using:
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 7d26864d0f353c3f8f1e290ef39a863d7da53023..f118804cace086612fd49d3182d99d9eb7387e0f 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -21,7 +21,7 @@
         = button_to reset_runners_token_admin_application_settings_path,
           method: :put, class: 'btn btn-default',
           data: { confirm: 'Are you sure you want to reset registration token?' } do
-          = icon('refresh')
+          = icon('spinner')
           Reset runners registration token
 
   .bs-callout
diff --git a/app/views/admin/services/index.html.haml b/app/views/admin/services/index.html.haml
index 6a5986f496a9c4b49e5e76e7bf6669dc0cbfe16c..50132572096df2a02890a3a1c3061ce8468fac4b 100644
--- a/app/views/admin/services/index.html.haml
+++ b/app/views/admin/services/index.html.haml
@@ -13,7 +13,7 @@
     - @services.sort_by(&:title).each do |service|
       %tr
         %td
-          = icon("copy", class: 'clgray')
+          = boolean_to_icon service.activated?
         %td
           = link_to edit_admin_application_settings_service_path(service.id) do
             %strong= service.title
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index af97e9588a576c7336b4df956800edbc47c936a4..01e72862114b40f56b6247116b975125a501e14a 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -1,13 +1,4 @@
-- if event.target
-  - if event.action_name == "opened"
-    .profile-icon.open-icon
-      = custom_icon("icon_status_open")
-  - elsif event.action_name == "closed"
-    .profile-icon.closed-icon
-      = custom_icon("icon_status_closed")
-  - else
-    .profile-icon.fork-icon
-      = custom_icon("icon_code_fork")
+= icon_for_profile_event(event)
 
 .event-title
   %span.author_name= link_to_author event
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index fee85c94277bad1b6773d309c86ed14859dbe9b9..d8e59be57bb422193385c0011a1c5ecdff1c25f4 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -1,5 +1,4 @@
-.profile-icon.open-icon
-  = custom_icon("icon_status_open")
+= icon_for_profile_event(event)
 
 .event-title
   %span.author_name= link_to_author event
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index 83709f5e4d09813995221ba164112969708b3a40..df4b956221504955a3b560246f9f6ef8c8ce5224 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -1,5 +1,4 @@
-.profile-icon
-  = custom_icon("icon_comment_o")
+= icon_for_profile_event(event)
 
 .event-title
   %span.author_name= link_to_author event
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index efdc8764acf7d7f4f24530d56be4fd391b4b6c06..c0943100ae3fc89450d91469a93e0080c957fb93 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -1,10 +1,6 @@
 - project = event.project
 
-.profile-icon
-  - if event.action_name == "deleted"
-    = custom_icon("trash_o")
-  - else
-    = custom_icon("icon_commit")
+= icon_for_profile_event(event)
 
 .event-title
   %span.author_name= link_to_author event
diff --git a/app/views/groups/subgroups.html.haml b/app/views/groups/subgroups.html.haml
index be80908313919fd5a5546eb3d38e4e24ad433031..8f0724c067767f0bd99151dd92a04e14a14bebab 100644
--- a/app/views/groups/subgroups.html.haml
+++ b/app/views/groups/subgroups.html.haml
@@ -9,7 +9,7 @@
     .nav-controls
       = form_tag request.path, method: :get do |f|
         = search_field_tag :filter_groups, params[:filter_groups], placeholder: 'Filter by name', class: 'form-control', spellcheck: false
-      - if can? current_user, :admin_group, @group
+      - if can?(current_user, :create_subgroup, @group)
         = link_to new_group_path(parent_id: @group.id), class: 'btn btn-new pull-right' do
           New Subgroup
 
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 207f80bedfedb2b3866cd32d5d47fd4bf1662c50..615dd56afbdbd652481c40c8f1f95cc46b6200b6 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -252,7 +252,7 @@
           = icon('chevron-down')
         .dropdown-menu.dropdown-select.dropdown-menu-selectable
           .dropdown-title
-            %span Dropdown Title
+            %span Dropdown title
             %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
               = icon('times')
           .dropdown-input
@@ -291,7 +291,7 @@
           = icon('chevron-down')
         .dropdown-menu.dropdown-select.dropdown-menu-selectable.is-loading
           .dropdown-title
-            %span Dropdown Title
+            %span Dropdown title
             %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
               = icon('times')
           .dropdown-input
@@ -335,7 +335,7 @@
           = icon('chevron-down')
         .dropdown-menu.dropdown-select.dropdown-menu-selectable.dropdown-menu-user
           .dropdown-title
-            %span Dropdown Title
+            %span Dropdown title
             %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
               = icon('times')
           .dropdown-input
@@ -362,7 +362,7 @@
             .dropdown-title
               %button.dropdown-title-button.dropdown-menu-back{ aria: { label: "Go back" } }
                 = icon('arrow-left')
-              %span Dropdown Title
+              %span Dropdown title
               %button.dropdown-title-button.dropdown-menu-close{ aria: { label: "Close" } }
                 = icon('times')
             .dropdown-input
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index a611481a0a43c3f4532cfd70206bd451f30d511e..19473b6ab276ddad03f0ae36d0d47ca9bf413829 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -28,9 +28,9 @@
   = stylesheet_link_tag "application", media: "all"
   = stylesheet_link_tag "print",       media: "print"
 
-  = javascript_include_tag(*webpack_asset_paths("runtime"))
-  = javascript_include_tag(*webpack_asset_paths("common"))
-  = javascript_include_tag(*webpack_asset_paths("main"))
+  = webpack_bundle_tag "runtime"
+  = webpack_bundle_tag "common"
+  = webpack_bundle_tag "main"
 
   - if content_for?(:page_specific_javascripts)
     = yield :page_specific_javascripts
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 299dace34069072aded8fecbf98ab09d21f8aeee..e34cddeb3e26c00d75ea999850fbc15f036c0416 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -23,7 +23,7 @@
             Registry
 
     - if project_nav_tab? :issues
-      = nav_link(controller: [:issues, :labels, :milestones, :boards]) do
+      = nav_link(controller: @project.default_issues_tracker? ? [:issues, :labels, :milestones, :boards] : :issues) do
         = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do
           %span
             Issues
@@ -31,7 +31,7 @@
               %span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
 
     - if project_nav_tab? :merge_requests
-      = nav_link(controller: :merge_requests) do
+      = nav_link(controller: @project.default_issues_tracker? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
         = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
           %span
             Merge Requests
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index f4a66398c85faabc3d094dc8c25c3d0524a7b380..c4159ce1a3624af7c88978b6eb8ae47a0bc40d2b 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -136,7 +136,7 @@
               - else
                 = build.id
             - if build.retried?
-              %i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
+              %i.fa.fa-spinner.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
 
 :javascript
   new Sidebar();
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 4700b7a9a4516de3bb8576a042edf6938e8cd3c5..2c3fd1fcd4d054b1a57954f109a971d026eb58b1 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -36,7 +36,7 @@
       = icon('warning', class: 'text-warning has-tooltip', title: 'Job is stuck. Check runners.')
 
     - if retried
-      = icon('refresh', class: 'text-warning has-tooltip', title: 'Job was retried')
+      = icon('spinner', class: 'text-warning has-tooltip', title: 'Job was retried')
 
     .label-container
       - if job.tags.any?
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index a80a07b52e61d0dcec5c7af5632ecfa4c64d780e..7f0059cdcda05e51623dc869782835957e5dd03c 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -1,6 +1,6 @@
 - @no_container = true
 - page_title "Edit", @label.name, "Labels"
-= render "projects/issues/head"
+= render "shared/mr_head"
 
 %div{ class: container_class }
   %h3.page-title
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index 29f861c09c614955e4060c9335fc8a026c4ffcef..fc72c4fb635d4d7cfe0242b9ad9a6416d2eb7eeb 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,7 +1,7 @@
 - @no_container = true
 - page_title "Labels"
 - hide_class = ''
-= render "projects/issues/head"
+= render "shared/mr_head"
 
 - if @labels.exists? || @prioritized_labels.exists?
   %div{ class: container_class }
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index f0d9be744d1438952059a0d00198dbe604f335eb..8f6c085a361dfb303388569028f6df663995e241 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -1,6 +1,6 @@
 - @no_container = true
 - page_title "New Label"
-= render "projects/issues/head"
+= render "shared/mr_head"
 
 %div{ class: container_class }
   %h3.page-title
diff --git a/app/views/projects/merge_requests/_head.html.haml b/app/views/projects/merge_requests/_head.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..b7f73fe5339dd6efba89ca9f663c93ea6ef086e6
--- /dev/null
+++ b/app/views/projects/merge_requests/_head.html.haml
@@ -0,0 +1,21 @@
+= content_for :sub_nav do
+  .scrolling-tabs-container.sub-nav-scroll
+    = render 'shared/nav_scroll'
+    .nav-links.sub-nav.scrolling-tabs
+      %ul{ class: (container_class) }
+        = nav_link(controller: :merge_requests) do
+          = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
+            %span
+              List
+
+        - if project_nav_tab? :labels
+          = nav_link(controller: :labels) do
+            = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
+              %span
+                Labels
+
+        - if project_nav_tab? :milestones
+          = nav_link(controller: :milestones) do
+            = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
+              %span
+                Milestones
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 64f17ab34b1d262dbf6c4ab89d38ad63c67899e9..6bf0035e051e65c1b898e14839ae484e0fcb77c5 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -2,6 +2,9 @@
 - @bulk_edit = can?(current_user, :admin_merge_request, @project)
 
 - page_title "Merge Requests"
+- unless @project.default_issues_tracker?
+  = content_for :sub_nav do
+    = render "projects/merge_requests/head"
 = render 'projects/last_push'
 
 - content_for :page_specific_javascripts do
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index 55b0b837c6d438ace95aac72c8f74eb8dc5fe3a1..e57a76dbfd2c0897258204a2c831f6205ed84a88 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1,6 +1,6 @@
 - @no_container = true
 - page_title "Edit", @milestone.title, "Milestones"
-= render "projects/issues/head"
+= render "shared/mr_head"
 
 %div{ class: container_class }
 
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 8e85b2e8a209866957819d990584f288f9da3435..e1096bd1d67f5e6af2b74fa8f6e8dbe5ca454c1c 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,6 +1,6 @@
 - @no_container = true
 - page_title 'Milestones'
-= render 'projects/issues/head'
+= render "shared/mr_head"
 
 %div{ class: container_class }
   .top-area
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index cda093ade819502f923f7bd2b8cd7e1994b95ba3..586eb909afa09f6721a65597d8be7d689df5ddd9 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1,6 +1,6 @@
 - @no_container = true
 - page_title "New Milestone"
-= render "projects/issues/head"
+= render "shared/mr_head"
 
 %div{ class: container_class }
   %h3.page-title
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 8b62b156853e7c64892959bd74436b41321ec099..a173117984d435f17b9834effd1d743a7979a181 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,7 +1,7 @@
 - @no_container = true
 - page_title       @milestone.title, "Milestones"
 - page_description @milestone.description
-= render "projects/issues/head"
+= render "shared/mr_head"
 
 %div{ class: container_class }
   .detail-page-header.milestone-page-header
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index c12c05eeb73dc85079f89edf11fc7b175b137efc..1f021ad77e58c239c9076d25de74f2e4b0a4d914 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -52,11 +52,10 @@
                     ":aria-label" => "buttonText",
                     "@click" => "resolve",
                     ":title" => "buttonText",
-                    "v-show" => "!loading",
                     ":ref" => "'button'" }
-                  = icon("spin spinner", "v-show" => "loading")
 
-                  = render "shared/icons/icon_status_success.svg"
+                  = icon("spin spinner", "v-show" => "loading", class: 'loading')
+                  %div{ 'v-show' => '!loading' }= render "shared/icons/icon_status_success.svg"
 
             - if current_user
               - if note.emoji_awardable?
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index de1229d58aaefcdcf8bc84ec6d20716a9b054e5f..fd7bd21677cbeafba43a091450b8be8fd1e85eb8 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -12,7 +12,7 @@
 = render "projects/last_push"
 = render "home_panel"
 
-- if current_user && can?(current_user, :download_code, @project)
+- if can?(current_user, :download_code, @project)
   %nav.project-stats{ class: container_class }
     %ul.nav
       %li
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 54b5ae2402eca58dff15422a322b524252efd5e9..1c7c73be933a3ab99725dda8717e3e4a75e59edc 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -2,7 +2,7 @@
   = f.label :import_url, class: 'control-label' do
     %span Git repository URL
   .col-sm-10
-    = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', disabled: true
+    = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
 
     .well.prepend-top-20
       %ul
diff --git a/app/views/shared/_mr_head.html.haml b/app/views/shared/_mr_head.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..4211ec6351dfeb9f30825fdd8be5213c4063e233
--- /dev/null
+++ b/app/views/shared/_mr_head.html.haml
@@ -0,0 +1,4 @@
+- if @project.default_issues_tracker?
+  = render "projects/issues/head"
+- else
+  = render "projects/merge_requests/head"
diff --git a/app/views/shared/empty_states/_issues.html.haml b/app/views/shared/empty_states/_issues.html.haml
index 7a7e3d467969b115fbd39a27b8ba37b35b117446..c229d18903f4f793825643f99c4fef3564040c2f 100644
--- a/app/views/shared/empty_states/_issues.html.haml
+++ b/app/views/shared/empty_states/_issues.html.haml
@@ -16,6 +16,8 @@
           Also, issues are searchable and filterable.
         - if project_select_button
           = render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
+        = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
       - else
-        %h4 There are no issues to show.
-      = link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
+        .text-center
+          %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/icons/_icon_merged.svg b/app/views/shared/icons/_icon_merged.svg
index d8f96558beaef5437a304023fb803b0216cd6ae4..43d591daefa77358d797250f9046638adaef42ad 100644
--- a/app/views/shared/icons/_icon_merged.svg
+++ b/app/views/shared/icons/_icon_merged.svg
@@ -1 +1 @@
-<svg width="14" height="14" viewBox="0 0 14 14" xmlns="http://www.w3.org/2000/svg"><path d="M0 7c0-3.866 3.142-7 7-7 3.866 0 7 3.142 7 7 0 3.866-3.142 7-7 7-3.866 0-7-3.142-7-7z"/><path d="M1 7c0 3.309 2.69 6 6 6 3.309 0 6-2.69 6-6 0-3.309-2.69-6-6-6-3.309 0-6 2.69-6 6z" fill="#FFF"/><path d="M9.427 6.523a.932.932 0 0 0-.808.489v-.01c-.49-.01-1.059-.172-1.46-.489-.35-.278-.7-.772-.882-1.17a.964.964 0 0 0 .35-.744.943.943 0 0 0-.934-.959c-.518 0-.933.432-.933.964 0 .35.191.662.467.825v3.147a.97.97 0 0 0-.467.825c0 .532.415.959.933.959a.943.943 0 0 0 .934-.96.965.965 0 0 0-.467-.824V6.844c.313.336.672.61 1.073.81.402.202.948.303 1.386.308v-.01c.168.293.467.49.808.49a.943.943 0 0 0 .933-.96.943.943 0 0 0-.933-.96z"/></svg>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path d="m2 3c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1m.761.85c.154 2.556 1.987 4.692 4.45 5.255.328-.655 1.01-1.105 1.789-1.105 1.105 0 2 .895 2 2 0 1.105-.895 2-2 2-.89 0-1.645-.582-1.904-1.386-1.916-.376-3.548-1.5-4.596-3.044v4.493c.863.222 1.5 1.01 1.5 1.937 0 1.105-.895 2-2 2-1.105 0-2-.895-2-2 0-.74.402-1.387 1-1.732v-8.535c-.598-.346-1-.992-1-1.732 0-1.105.895-2 2-2 1.105 0 2 .895 2 2 0 .835-.512 1.551-1.239 1.85m6.239 7.15c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1m-7 4c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1" transform="translate(3)"/></svg>
diff --git a/app/views/shared/icons/_trash_o.svg b/app/views/shared/icons/_icon_trash_o.svg
similarity index 100%
rename from app/views/shared/icons/_trash_o.svg
rename to app/views/shared/icons/_icon_trash_o.svg
diff --git a/app/workers/build_coverage_worker.rb b/app/workers/build_coverage_worker.rb
index def0ab1dde12a9be9b95be014bbabb27558880c3..f7ae996bb170dcf6968e90029108b50dcf89891b 100644
--- a/app/workers/build_coverage_worker.rb
+++ b/app/workers/build_coverage_worker.rb
@@ -3,7 +3,6 @@ class BuildCoverageWorker
   include BuildQueue
 
   def perform(build_id)
-    Ci::Build.find_by(id: build_id)
-      .try(:update_coverage)
+    Ci::Build.find_by(id: build_id)&.update_coverage
   end
 end
diff --git a/changelogs/unreleased/2120-issues-search-bar-not-picking-up.yml b/changelogs/unreleased/2120-issues-search-bar-not-picking-up.yml
new file mode 100644
index 0000000000000000000000000000000000000000..706609b7baf63fae43130b6e6edcb2a513c9faa2
--- /dev/null
+++ b/changelogs/unreleased/2120-issues-search-bar-not-picking-up.yml
@@ -0,0 +1,4 @@
+---
+title: Fix filtered search input width for IE
+merge_request:
+author:
diff --git a/changelogs/unreleased/22714-update-all-instances-of-fa-refresh.yml b/changelogs/unreleased/22714-update-all-instances-of-fa-refresh.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ad7c011933f6e5d7e25500be181e98aad1b46c35
--- /dev/null
+++ b/changelogs/unreleased/22714-update-all-instances-of-fa-refresh.yml
@@ -0,0 +1,4 @@
+---
+title: Update all instances of the old loading icon
+merge_request: 10490
+author: Andrew Torres
diff --git a/changelogs/unreleased/27729-improve-webpack-dev-environment.yml b/changelogs/unreleased/27729-improve-webpack-dev-environment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d04ea70ab1ce0fb3d7a42d0eaca90ccba3789248
--- /dev/null
+++ b/changelogs/unreleased/27729-improve-webpack-dev-environment.yml
@@ -0,0 +1,4 @@
+---
+title: Add webpack_bundle_tag helper to improve non-localhost GDK configurations
+merge_request: 10604
+author:
diff --git a/changelogs/unreleased/29056-backport-ee-cleanup-database-file.yml b/changelogs/unreleased/29056-backport-ee-cleanup-database-file.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0ebb9d57611390f6ee49a12bea2ea82f6c03a031
--- /dev/null
+++ b/changelogs/unreleased/29056-backport-ee-cleanup-database-file.yml
@@ -0,0 +1,4 @@
+---
+title: Turns true value and false value database methods from instance to class methods
+merge_request: 10583
+author:
diff --git a/changelogs/unreleased/30349-create-users-build-service.yml b/changelogs/unreleased/30349-create-users-build-service.yml
new file mode 100644
index 0000000000000000000000000000000000000000..49b571f5646a5b945af0d8a8f93b68a1b4af0c4f
--- /dev/null
+++ b/changelogs/unreleased/30349-create-users-build-service.yml
@@ -0,0 +1,4 @@
+---
+title: Implement Users::BuildService
+merge_request: 30349
+author: George Andrinopoulos
diff --git a/changelogs/unreleased/30457-expire-note-destroy.yml b/changelogs/unreleased/30457-expire-note-destroy.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f5c89da68a91d24076543b48ff2fb8645a406150
--- /dev/null
+++ b/changelogs/unreleased/30457-expire-note-destroy.yml
@@ -0,0 +1,4 @@
+---
+title: Fix issue's note cache expiration after delete
+merge_request:
+author: mhasbini
diff --git a/changelogs/unreleased/30779-show-mr-subnav-issue-tracker.yml b/changelogs/unreleased/30779-show-mr-subnav-issue-tracker.yml
new file mode 100644
index 0000000000000000000000000000000000000000..59f8942911c5b7a2c70b8d0e29489400d77bba90
--- /dev/null
+++ b/changelogs/unreleased/30779-show-mr-subnav-issue-tracker.yml
@@ -0,0 +1,4 @@
+---
+title: Show sub-nav under Merge Requests when issue tracker is non-default.
+merge_request: 10658
+author:
diff --git a/changelogs/unreleased/bb_save_trace.yml b/changelogs/unreleased/bb_save_trace.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6ff31f4f1111a5e771263215a442449acfff1558
--- /dev/null
+++ b/changelogs/unreleased/bb_save_trace.yml
@@ -0,0 +1,5 @@
+---
+title: "[BB Importer] Save the error trace and the whole raw document to debug problems
+  easier"
+merge_request:
+author:
diff --git a/changelogs/unreleased/empty-task-list-alignment.yml b/changelogs/unreleased/empty-task-list-alignment.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ca04e1cab5a0c99b1d12944dff8d363c3eaab0de
--- /dev/null
+++ b/changelogs/unreleased/empty-task-list-alignment.yml
@@ -0,0 +1,4 @@
+---
+title: Fixed alignment of empty task list items
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-missing-capitalisation-buttons.yml b/changelogs/unreleased/fix-missing-capitalisation-buttons.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b2c404834751034f7441a97e05d12173849a8cd0
--- /dev/null
+++ b/changelogs/unreleased/fix-missing-capitalisation-buttons.yml
@@ -0,0 +1,4 @@
+---
+title: Fix missing capitalisation on views
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-preloading-merge_request_diff.yml b/changelogs/unreleased/fix-preloading-merge_request_diff.yml
new file mode 100644
index 0000000000000000000000000000000000000000..d38b6b0a707b6eaf7335a2853758c9e357d33939
--- /dev/null
+++ b/changelogs/unreleased/fix-preloading-merge_request_diff.yml
@@ -0,0 +1,4 @@
+---
+title: Fix bad query for PostgreSQL showing merge requests list
+merge_request: 10666
+author:
diff --git a/changelogs/unreleased/fix-trace-seeking.yml b/changelogs/unreleased/fix-trace-seeking.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b753df4bb430fa2b597f1702a57302a38a7d6d59
--- /dev/null
+++ b/changelogs/unreleased/fix-trace-seeking.yml
@@ -0,0 +1,4 @@
+---
+title: Fix invalid encoding when showing some traces
+merge_request: 10681
+author:
diff --git a/changelogs/unreleased/fix_spaces_in_label_title.yml b/changelogs/unreleased/fix_spaces_in_label_title.yml
new file mode 100644
index 0000000000000000000000000000000000000000..51f07438edbdeb9e415ab801f43e535b651dcbe7
--- /dev/null
+++ b/changelogs/unreleased/fix_spaces_in_label_title.yml
@@ -0,0 +1,4 @@
+---
+title: Remove heading and trailing spaces from label's color and title
+merge_request: 10603
+author: blackst0ne
diff --git a/changelogs/unreleased/issues-empty-state-not-centered.yml b/changelogs/unreleased/issues-empty-state-not-centered.yml
new file mode 100644
index 0000000000000000000000000000000000000000..883125e28b1e4f916736cf4647565cd06b0efa04
--- /dev/null
+++ b/changelogs/unreleased/issues-empty-state-not-centered.yml
@@ -0,0 +1,4 @@
+---
+title: Centered issues empty state
+merge_request:
+author:
diff --git a/changelogs/unreleased/omnibus-gitlab-1993-check-shell-repositories-path-group-is-root.yml b/changelogs/unreleased/omnibus-gitlab-1993-check-shell-repositories-path-group-is-root.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3b9284258cb04b9378af5abd0228195608a9b7c3
--- /dev/null
+++ b/changelogs/unreleased/omnibus-gitlab-1993-check-shell-repositories-path-group-is-root.yml
@@ -0,0 +1,4 @@
+---
+title: "Make the `gitlab:gitlab_shell:check` task check that the repositories storage path are owned by the `root` group"
+merge_request:
+author:
diff --git a/changelogs/unreleased/pms-lighter-colors.yml b/changelogs/unreleased/pms-lighter-colors.yml
new file mode 100644
index 0000000000000000000000000000000000000000..958d4bc0ac0c94cb21daa27a4f77bf30ba7ad0bf
--- /dev/null
+++ b/changelogs/unreleased/pms-lighter-colors.yml
@@ -0,0 +1,4 @@
+---
+title: Add lighter colors and fix existing light colors
+merge_request: 10690
+author:
diff --git a/changelogs/unreleased/reset-new-branch-button.yml b/changelogs/unreleased/reset-new-branch-button.yml
new file mode 100644
index 0000000000000000000000000000000000000000..318ee46298f6a6de45ac1bddfdcbf344e7ae555d
--- /dev/null
+++ b/changelogs/unreleased/reset-new-branch-button.yml
@@ -0,0 +1,4 @@
+---
+title: Reset New branch button when issue state changes
+merge_request: 5962
+author: winniehell
diff --git a/changelogs/unreleased/siemens-gitlab-ce-fix-subgroup-hide-button.yml b/changelogs/unreleased/siemens-gitlab-ce-fix-subgroup-hide-button.yml
new file mode 100644
index 0000000000000000000000000000000000000000..716311c75822feaad845a43b96b1fa8b0a1685d8
--- /dev/null
+++ b/changelogs/unreleased/siemens-gitlab-ce-fix-subgroup-hide-button.yml
@@ -0,0 +1,4 @@
+---
+title: Hide new subgroup button if user has no permission to create one
+merge_request: 10627
+author:
diff --git a/changelogs/unreleased/user-activity-scroll-bar.yml b/changelogs/unreleased/user-activity-scroll-bar.yml
new file mode 100644
index 0000000000000000000000000000000000000000..97cccee42cb26b9c4fe37ec4318e3726b03cd5d9
--- /dev/null
+++ b/changelogs/unreleased/user-activity-scroll-bar.yml
@@ -0,0 +1,4 @@
+---
+title: Fix preemptive scroll bar on user activity calendar.
+merge_request: !10636
+author:
diff --git a/changelogs/unreleased/zj-api-fix-build-events.yml b/changelogs/unreleased/zj-api-fix-build-events.yml
new file mode 100644
index 0000000000000000000000000000000000000000..7700d8dcd22ac5f797b593aa4f5381786c89ad44
--- /dev/null
+++ b/changelogs/unreleased/zj-api-fix-build-events.yml
@@ -0,0 +1,4 @@
+---
+title: "Bugfix: POST /projects/:id/hooks and PUT /projects/:id/hook/:hook_id no longer ignore the the job_events param in the V4 API"
+merge_request: 10586
+author:
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 3c70f35b9d07ef2a268892c37bd0563ac75a089b..06c9f734c2adee309ad1a113a6291a229b458240 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -579,9 +579,9 @@ test:
     storages:
       default:
         path: tmp/tests/repositories/
-        gitaly_address: unix:<%= Rails.root.join('tmp/sockets/private/gitaly.socket') %>
+        gitaly_address: unix:tmp/tests/gitaly/gitaly.socket
   gitaly:
-    enabled: false
+    enabled: true
   backup:
     path: tmp/tests/backups
   gitlab_shell:
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 0fea3f8222b83cd1634e27d399c36ade718c8c0f..ffb161900933d5d1b1dc616c3020c9e2b5a69a40 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -11,6 +11,7 @@ var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeMod
 var ROOT_PATH = path.resolve(__dirname, '..');
 var IS_PRODUCTION = process.env.NODE_ENV === 'production';
 var IS_DEV_SERVER = process.argv[1].indexOf('webpack-dev-server') !== -1;
+var DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
 var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
 var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
 var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
@@ -182,12 +183,13 @@ if (IS_PRODUCTION) {
 if (IS_DEV_SERVER) {
   config.devtool = 'cheap-module-eval-source-map';
   config.devServer = {
+    host: DEV_SERVER_HOST,
     port: DEV_SERVER_PORT,
     headers: { 'Access-Control-Allow-Origin': '*' },
     stats: 'errors-only',
     inline: DEV_SERVER_LIVERELOAD
   };
-  config.output.publicPath = '//localhost:' + DEV_SERVER_PORT + config.output.publicPath;
+  config.output.publicPath = '//' + DEV_SERVER_HOST + ':' + DEV_SERVER_PORT + config.output.publicPath;
   config.plugins.push(
     // watch node_modules for changes if we encounter a missing module compile error
     new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules'))
diff --git a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
index 94c0a6845d586cfebe3d880bec92829c5d7ebb48..67a0d3b53eb005ee3b89858877852dd1c69fba6f 100644
--- a/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
+++ b/db/migrate/20130218141258_convert_closed_to_state_in_issue.rb
@@ -1,6 +1,6 @@
 # rubocop:disable all
 class ConvertClosedToStateInIssue < ActiveRecord::Migration
-  include Gitlab::Database
+  include Gitlab::Database::MigrationHelpers
 
   def up
     execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}"
diff --git a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
index 64a9c761352dd1a9cd736a81cf84f124eae2b44e..307fc6a023d4395222de7b11978d8d3313b9eada 100644
--- a/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
+++ b/db/migrate/20130218141327_convert_closed_to_state_in_merge_request.rb
@@ -1,6 +1,6 @@
 # rubocop:disable all
 class ConvertClosedToStateInMergeRequest < ActiveRecord::Migration
-  include Gitlab::Database
+  include Gitlab::Database::MigrationHelpers
 
   def up
     execute "UPDATE #{table_name} SET state = 'merged' WHERE closed = #{true_value} AND merged = #{true_value}"
diff --git a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
index 41508c2dc954a8ca3796f1a0c0ca16a96d97a5e2..d12703cf3b29de2e7729c0b7762be9b73ebdfeb2 100644
--- a/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
+++ b/db/migrate/20130218141344_convert_closed_to_state_in_milestone.rb
@@ -1,6 +1,6 @@
 # rubocop:disable all
 class ConvertClosedToStateInMilestone < ActiveRecord::Migration
-  include Gitlab::Database
+  include Gitlab::Database::MigrationHelpers
 
   def up
     execute "UPDATE #{table_name} SET state = 'closed' WHERE closed = #{true_value}"
diff --git a/db/migrate/20130315124931_user_color_scheme.rb b/db/migrate/20130315124931_user_color_scheme.rb
index 06e28a49d9d7c0cac115dbf211e45301c617bb56..09af928fde7b5adb1b590b5fbc58757b51925ea4 100644
--- a/db/migrate/20130315124931_user_color_scheme.rb
+++ b/db/migrate/20130315124931_user_color_scheme.rb
@@ -1,6 +1,6 @@
 # rubocop:disable all
 class UserColorScheme < ActiveRecord::Migration
-  include Gitlab::Database
+  include Gitlab::Database::MigrationHelpers
 
   def up
     add_column :users, :color_scheme_id, :integer, null: false, default: 1
diff --git a/db/migrate/20131112220935_add_visibility_level_to_projects.rb b/db/migrate/20131112220935_add_visibility_level_to_projects.rb
index 5efc17b228e91457cbe576c5004d8567826a100b..86d73753adcc66f69fe8465a0e3a0747ff0fb0e4 100644
--- a/db/migrate/20131112220935_add_visibility_level_to_projects.rb
+++ b/db/migrate/20131112220935_add_visibility_level_to_projects.rb
@@ -1,6 +1,6 @@
 # rubocop:disable all
 class AddVisibilityLevelToProjects < ActiveRecord::Migration
-  include Gitlab::Database
+  include Gitlab::Database::MigrationHelpers
 
   def self.up
     add_column :projects, :visibility_level, :integer, :default => 0, :null => false
diff --git a/db/migrate/20140313092127_migrate_already_imported_projects.rb b/db/migrate/20140313092127_migrate_already_imported_projects.rb
index f2e91fe1b409b457425a727fadde1fa80a73f964..0afc26b876441706053588179b6168ad2028f20c 100644
--- a/db/migrate/20140313092127_migrate_already_imported_projects.rb
+++ b/db/migrate/20140313092127_migrate_already_imported_projects.rb
@@ -1,6 +1,6 @@
 # rubocop:disable all
 class MigrateAlreadyImportedProjects < ActiveRecord::Migration
-  include Gitlab::Database
+  include Gitlab::Database::MigrationHelpers
 
   def up
     execute("UPDATE projects SET import_status = 'finished' WHERE imported = #{true_value}")
diff --git a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb
index 688d8578478999893313460e94b102e9e67ba345..0c14f75c154872270e43d87fbedf2e999449b7ff 100644
--- a/db/migrate/20141007100818_add_visibility_level_to_snippet.rb
+++ b/db/migrate/20141007100818_add_visibility_level_to_snippet.rb
@@ -1,6 +1,6 @@
 # rubocop:disable all
 class AddVisibilityLevelToSnippet < ActiveRecord::Migration
-  include Gitlab::Database
+  include Gitlab::Database::MigrationHelpers
 
   def up
     add_column :snippets, :visibility_level, :integer, :default => 0, :null => false
diff --git a/db/migrate/20151209144329_migrate_ci_web_hooks.rb b/db/migrate/20151209144329_migrate_ci_web_hooks.rb
index cb1e556623a62c9bfe1b19b95c3a3d2f35ea207c..62a6d334f047f090698eb338260a82f9d329df9a 100644
--- a/db/migrate/20151209144329_migrate_ci_web_hooks.rb
+++ b/db/migrate/20151209144329_migrate_ci_web_hooks.rb
@@ -1,6 +1,6 @@
 # rubocop:disable all
 class MigrateCiWebHooks < ActiveRecord::Migration
-  include Gitlab::Database
+  include Gitlab::Database::MigrationHelpers
 
   def up
     execute(
diff --git a/db/migrate/20151209145909_migrate_ci_emails.rb b/db/migrate/20151209145909_migrate_ci_emails.rb
index 6b7a106814d2c7f2cd01872973fac885f8d65421..5de7b205fb16766a6dc9b1b5a33f132203392840 100644
--- a/db/migrate/20151209145909_migrate_ci_emails.rb
+++ b/db/migrate/20151209145909_migrate_ci_emails.rb
@@ -1,6 +1,6 @@
 # rubocop:disable all
 class MigrateCiEmails < ActiveRecord::Migration
-  include Gitlab::Database
+  include Gitlab::Database::MigrationHelpers
 
   def up
     # This inserts a new service: BuildsEmailService
diff --git a/db/migrate/20151210125232_migrate_ci_slack_service.rb b/db/migrate/20151210125232_migrate_ci_slack_service.rb
index 633d5148d979d2883f7cc0c8cbe96af151771a3d..fff130b7b10486528d16366b8756a4c291f3faa5 100644
--- a/db/migrate/20151210125232_migrate_ci_slack_service.rb
+++ b/db/migrate/20151210125232_migrate_ci_slack_service.rb
@@ -1,6 +1,6 @@
 # rubocop:disable all
 class MigrateCiSlackService < ActiveRecord::Migration
-  include Gitlab::Database
+  include Gitlab::Database::MigrationHelpers
 
   def up
     properties_query = 'SELECT properties FROM ci_services ' \
diff --git a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb
index dae084ce180905bec1767cd2285a6b3bcb9b6b55..824f6f841951c3829f3f1ec9fc6186d353914ece 100644
--- a/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb
+++ b/db/migrate/20151210125927_migrate_ci_hip_chat_service.rb
@@ -1,6 +1,6 @@
 # rubocop:disable all
 class MigrateCiHipChatService < ActiveRecord::Migration
-  include Gitlab::Database
+  include Gitlab::Database::MigrationHelpers
 
   def up
     # From properties strip `hipchat_` key
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
index 2dd14ee5a787eebe2f393dc0b047c84772f45e64..04bf89c96870069b2d15ce23b69fd088f267aa11 100644
--- a/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
+++ b/db/post_migrate/20170301205640_migrate_build_events_to_pipeline_events.rb
@@ -1,6 +1,5 @@
 class MigrateBuildEventsToPipelineEvents < ActiveRecord::Migration
   include Gitlab::Database::MigrationHelpers
-  include Gitlab::Database
 
   DOWNTIME = false
 
diff --git a/doc/ci/img/pipelines.png b/doc/ci/img/pipelines.png
index 5937e9d99c896fcc2e9960aac33b2fe021fbf394..a604fcb258780fde65a94176f66714e3eaf287bd 100644
Binary files a/doc/ci/img/pipelines.png and b/doc/ci/img/pipelines.png differ
diff --git a/doc/development/fe_guide/performance.md b/doc/development/fe_guide/performance.md
index e74eb72951599138c8e5115e1997c7078fc762f2..2ddcbe13afa7faf0c60c4668c1727c8e676034fe 100644
--- a/doc/development/fe_guide/performance.md
+++ b/doc/development/fe_guide/performance.md
@@ -48,8 +48,8 @@ Steps to split page-specific JavaScript from the main `main.js`:
 
 ```haml
 - content_for :page_specific_javascripts do
-  = page_specific_javascript_bundle_tag('lib_chart')
-  = page_specific_javascript_bundle_tag('graphs')
+  = webpack_bundle_tag 'lib_chart'
+  = webpack_bundle_tag 'graphs'
 ```
 
 The above loads `chart.js` and `graphs_bundle.js` for this page only. `chart.js`
diff --git a/doc/development/fe_guide/testing.md b/doc/development/fe_guide/testing.md
index 8d3513d3566831fbf9abc6e95c1595a920e27769..a4631fd007308372e8bca48c45bf6fd12cb14bb5 100644
--- a/doc/development/fe_guide/testing.md
+++ b/doc/development/fe_guide/testing.md
@@ -13,10 +13,19 @@ for more information on general testing practices at GitLab.
 ## Karma test suite
 
 GitLab uses the [Karma][karma] test runner with [Jasmine][jasmine] as its test
-framework for our JavaScript unit tests.  For tests that rely on DOM
+framework for our JavaScript unit tests. For tests that rely on DOM
 manipulation we use fixtures which are pre-compiled from HAML source files and
 served during testing by the [jasmine-jquery][jasmine-jquery] plugin.
 
+JavaScript tests live in `spec/javascripts/`, matching the folder structure
+of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js`
+has a corresponding `spec/javascripts/behaviors/autosize_spec.js` file.
+
+Keep in mind that in a CI environment, these tests are run in a headless
+browser and you will not have access to certain APIs, such as
+[`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
+which will have to be stubbed.
+
 ### Running frontend tests
 
 `rake karma` runs the frontend-only (JavaScript) tests.
@@ -80,24 +89,23 @@ If an integration test depends on JavaScript to run correctly, you need to make
 sure the spec is configured to enable JavaScript when the tests are run. If you
 don't do this you'll see vague error messages from the spec runner.
 
-To enable a JavaScript driver in an `rspec` test, add `js: true` to the
+To enable a JavaScript driver in an `rspec` test, add `:js` to the
 individual spec or the context block containing multiple specs that need
 JavaScript enabled:
 
 ```ruby
-
 # For one spec
-it 'presents information about abuse report', js: true do
-    # assertions...
+it 'presents information about abuse report', :js do
+  # assertions...
 end
 
-describe "Admin::AbuseReports", js: true do
-    it 'presents information about abuse report' do
-        # assertions...
-    end
-    it 'shows buttons for adding to abuse report' do
-        # assertions...
-    end
+describe "Admin::AbuseReports", :js do
+  it 'presents information about abuse report' do
+    # assertions...
+  end
+  it 'shows buttons for adding to abuse report' do
+    # assertions...
+  end
 end
 ```
 
@@ -113,13 +121,12 @@ file for the failing spec, add the `@javascript` flag above the Scenario:
 ```
 @javascript
 Scenario: Developer can approve merge request
-    Given I am a "Shop" developer
-    And I visit project "Shop" merge requests page
-    And merge request 'Bug NS-04' must be approved
-    And I click link "Bug NS-04"
-    When I click link "Approve"
-    Then I should see approved merge request "Bug NS-04"
-
+  Given I am a "Shop" developer
+  And I visit project "Shop" merge requests page
+  And merge request 'Bug NS-04' must be approved
+  And I click link "Bug NS-04"
+  When I click link "Approve"
+  Then I should see approved merge request "Bug NS-04"
 ```
 
 [capybara]: http://teamcapybara.github.io/capybara/
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 587922d0136fe7d13e6be37cdbf256dd4baa4d52..3e8b709c18f7be75320a51c376b193f170ee59b1 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -4,28 +4,53 @@ When writing migrations for GitLab, you have to take into account that
 these will be ran by hundreds of thousands of organizations of all sizes, some with
 many years of data in their database.
 
-In addition, having to take a server offline for a an upgrade small or big is
-a big burden for most organizations. For this reason it is important that your
-migrations are written carefully, can be applied online and adhere to the style guide below.
+In addition, having to take a server offline for a a upgrade small or big is a
+big burden for most organizations. For this reason it is important that your
+migrations are written carefully, can be applied online and adhere to the style
+guide below.
 
-Migrations should not require GitLab installations to be taken offline unless
-_absolutely_ necessary - see the ["What Requires Downtime?"](what_requires_downtime.md)
-page. If a migration requires downtime, this should be clearly mentioned during
-the review process, as well as being documented in the monthly release post. For
-more information, see the "Downtime Tagging" section below.
+Migrations are **not** allowed to require GitLab installations to be taken
+offline unless _absolutely necessary_. Downtime assumptions should be based on
+the behaviour of a migration when performed using PostgreSQL, as various
+operations in MySQL may require downtime without there being alternatives.
+
+When downtime is necessary the migration has to be approved by:
+
+1. The VP of Engineering
+1. A Backend Lead
+1. A Database Specialist
+
+An up-to-date list of people holding these titles can be found at
+<https://about.gitlab.com/team/>.
+
+The document ["What Requires Downtime?"](what_requires_downtime.md) specifies
+various database operations, whether they require downtime and how to
+work around that whenever possible.
 
 When writing your migrations, also consider that databases might have stale data
-or inconsistencies and guard for that. Try to make as little assumptions as possible
-about the state of the database.
+or inconsistencies and guard for that. Try to make as few assumptions as
+possible about the state of the database.
+
+Please don't depend on GitLab-specific code since it can change in future
+versions. If needed copy-paste GitLab code into the migration to make it forward
+compatible.
+
+## Commit Guidelines
 
-Please don't depend on GitLab specific code since it can change in future versions.
-If needed copy-paste GitLab code into the migration to make it forward compatible.
+Each migration **must** be added in its own commit with a descriptive commit
+message. If a commit adds a migration it _should only_ include the migration and
+any corresponding changes to `db/schema.rb`. This makes it easy to revert a
+database migration without accidentally reverting other changes.
 
 ## Downtime Tagging
 
 Every migration must specify if it requires downtime or not, and if it should
-require downtime it must also specify a reason for this. To do so, add the
-following two constants to the migration class' body:
+require downtime it must also specify a reason for this. This is required even
+if 99% of the migrations won't require downtime as this makes it easier to find
+the migrations that _do_ require downtime.
+
+To tag a migration, add the following two constants to the migration class'
+body:
 
 * `DOWNTIME`: a boolean that when set to `true` indicates the migration requires
   downtime.
@@ -50,12 +75,53 @@ from a migration class.
 
 ## Reversibility
 
-Your migration should be reversible. This is very important, as it should
+Your migration **must be** reversible. This is very important, as it should
 be possible to downgrade in case of a vulnerability or bugs.
 
 In your migration, add a comment describing how the reversibility of the
 migration was tested.
 
+## Multi Threading
+
+Sometimes a migration might need to use multiple Ruby threads to speed up a
+migration. For this to work your migration needs to include the module
+`Gitlab::Database::MultiThreadedMigration`:
+
+```ruby
+class MyMigration < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  include Gitlab::Database::MultiThreadedMigration
+end
+```
+
+You can then use the method `with_multiple_threads` to perform work in separate
+threads. For example:
+
+```ruby
+class MyMigration < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+  include Gitlab::Database::MultiThreadedMigration
+
+  def up
+    with_multiple_threads(4) do
+      disable_statement_timeout
+
+      # ...
+    end
+  end
+end
+```
+
+Here the call to `disable_statement_timeout` will use the connection local to
+the `with_multiple_threads` block, instead of re-using the global connection
+pool.  This ensures each thread has its own connection object, and won't time
+out when trying to obtain one.
+
+**NOTE:** PostgreSQL has a maximum amount of connections that it allows. This
+limit can vary from installation to installation. As a result it's recommended
+you do not use more than 32 threads in a single migration. Usually 4-8 threads
+should be more than enough.
+
 ## Removing indices
 
 When removing an index make sure to use the method `remove_concurrent_index` instead
@@ -78,7 +144,10 @@ end
 
 ## Adding indices
 
-If you need to add an unique index please keep in mind there is possibility of existing duplicates. If it is possible write a separate migration for handling this situation. It can be just removing or removing with overwriting all references to these duplicates depend on situation.
+If you need to add a unique index please keep in mind there is the possibility
+of existing duplicates being present in the database. This means that should
+always _first_ add a migration that removes any duplicates, before adding the
+unique index.
 
 When adding an index make sure to use the method `add_concurrent_index` instead
 of the regular `add_index` method. The `add_concurrent_index` method
@@ -90,17 +159,22 @@ so:
 ```ruby
 class MyMigration < ActiveRecord::Migration
   include Gitlab::Database::MigrationHelpers
+
   disable_ddl_transaction!
 
-  def change
+  def up
+    add_concurrent_index :table, :column
+  end
 
+  def down
+    remove_index :table, :column if index_exists?(:table, :column)
   end
 end
 ```
 
 ## Adding Columns With Default Values
 
-When adding columns with default values you should use the method
+When adding columns with default values you must use the method
 `add_column_with_default`. This method ensures the table is updated without
 requiring downtime. This method is not reversible so you must manually define
 the `up` and `down` methods in your migration class.
@@ -123,6 +197,9 @@ class MyMigration < ActiveRecord::Migration
 end
 ```
 
+Keep in mind that this operation can easily take 10-15 minutes to complete on
+larger installations (e.g. GitLab.com). As a result you should only add default
+values if absolutely necessary.
 
 ## Integer column type
 
@@ -147,13 +224,15 @@ add_column(:projects, :foo, :integer, default: 10, limit: 8)
 
 ## Testing
 
-Make sure that your migration works with MySQL and PostgreSQL with data. An empty database does not guarantee that your migration is correct.
+Make sure that your migration works with MySQL and PostgreSQL with data. An
+empty database does not guarantee that your migration is correct.
 
 Make sure your migration can be reversed.
 
 ## Data migration
 
-Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of using plain SQL you need to quote all input manually with `quote_string` helper.
+Please prefer Arel and plain SQL over usual ActiveRecord syntax. In case of
+using plain SQL you need to quote all input manually with `quote_string` helper.
 
 Example with Arel:
 
@@ -177,3 +256,17 @@ select_all("SELECT name, COUNT(id) as cnt FROM tags GROUP BY name HAVING COUNT(i
   execute("DELETE FROM tags WHERE id IN(#{duplicate_ids.join(",")})")
 end
 ```
+
+If you need more complex logic you can define and use models local to a
+migration. For example:
+
+```ruby
+class MyMigration < ActiveRecord::Migration
+  class Project < ActiveRecord::Base
+    self.table_name = 'projects'
+  end
+end
+```
+
+When doing so be sure to explicitly set the model's table name so it's not
+derived from the class name or namespace.
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 5bc958f5a9682c21cc6673eebf3fe850dc09103d..ad540ec13db897a734e8092ddd303205fe75bb94 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -9,52 +9,179 @@ this guide defines a rule that contradicts the thoughtbot guide, this guide
 takes precedence. Some guidelines may be repeated verbatim to stress their
 importance.
 
-## Factories
+## Definitions
+
+### Unit tests
+
+Formal definition: https://en.wikipedia.org/wiki/Unit_testing
+
+These kind of tests ensure that a single unit of code (a method) works as
+expected (given an input, it has a predictable output). These tests should be
+isolated as much as possible. For example, model methods that don't do anything
+with the database shouldn't need a DB record. Classes that don't need database
+records should use stubs/doubles as much as possible.
+
+| Code path | Tests path | Testing engine | Notes |
+| --------- | ---------- | -------------- | ----- |
+| `app/finders/` | `spec/finders/` | RSpec | |
+| `app/helpers/` | `spec/helpers/` | RSpec | |
+| `app/db/{post_,}migrate/` | `spec/migrations/` | RSpec | |
+| `app/policies/` | `spec/policies/` | RSpec | |
+| `app/presenters/` | `spec/presenters/` | RSpec | |
+| `app/routing/` | `spec/routing/` | RSpec | |
+| `app/serializers/` | `spec/serializers/` | RSpec | |
+| `app/services/` | `spec/services/` | RSpec | |
+| `app/tasks/` | `spec/tasks/` | RSpec | |
+| `app/uploaders/` | `spec/uploaders/` | RSpec | |
+| `app/views/` | `spec/views/` | RSpec | |
+| `app/workers/` | `spec/workers/` | RSpec | |
+| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. |
+
+### Integration tests
+
+Formal definition: https://en.wikipedia.org/wiki/Integration_testing
+
+These kind of tests ensure that individual parts of the application work well together, without the overhead of the actual app environment (i.e. the browser). These tests should assert at the request/response level: status code, headers, body. They're useful to test permissions, redirections, what view is rendered etc.
+
+| Code path | Tests path | Testing engine | Notes |
+| --------- | ---------- | -------------- | ----- |
+| `app/controllers/` | `spec/controllers/` | RSpec | |
+| `app/mailers/` | `spec/mailers/` | RSpec | |
+| `lib/api/` | `spec/requests/api/` | RSpec | |
+| `lib/ci/api/` | `spec/requests/ci/api/` | RSpec | |
+| `app/assets/javascripts/` | `spec/javascripts/` | Karma | More details in the [JavaScript](#javascript) section. |
+
+#### About controller tests
+
+In an ideal world, controllers should be thin. However, when this is not the
+case, it's acceptable to write a system/feature test without JavaScript instead
+of a controller test. The reason is that testing a fat controller usually
+involves a lot of stubbing, things like:
 
-GitLab uses [factory_girl] as a test fixture replacement.
-
-- Factory definitions live in `spec/factories/`, named using the pluralization
-  of their corresponding model (`User` factories are defined in `users.rb`).
-- There should be only one top-level factory definition per file.
-- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and
-  should) call `create(...)` instead of `FactoryGirl.create(...)`.
-- Make use of [traits] to clean up definitions and usages.
-- When defining a factory, don't define attributes that are not required for the
-  resulting record to pass validation.
-- When instantiating from a factory, don't supply attributes that aren't
-  required by the test.
-- Factories don't have to be limited to `ActiveRecord` objects.
-  [See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d).
-
-[factory_girl]: https://github.com/thoughtbot/factory_girl
-[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits
-
-## JavaScript
-
-GitLab uses [Karma] to run its [Jasmine] JavaScript specs. They can be run on
-the command line via `bundle exec karma`.
-
-- JavaScript tests live in `spec/javascripts/`, matching the folder structure
-  of `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js`
-  has a corresponding `spec/javascripts/behaviors/autosize_spec.js` file.
-- Haml fixtures required for JavaScript tests live in
-  `spec/javascripts/fixtures`. They should contain the bare minimum amount of
-  markup necessary for the test.
-
-    > **Warning:** Keep in mind that a Rails view may change and
-    invalidate your test, but everything will still pass because your fixture
-    doesn't reflect the latest view. Because of this we encourage you to
-    generate fixtures from actual rails views whenever possible.
-
-- Keep in mind that in a CI environment, these tests are run in a headless
-  browser and you will not have access to certain APIs, such as
-  [`Notification`](https://developer.mozilla.org/en-US/docs/Web/API/notification),
-  which will have to be stubbed.
-
-[Karma]: https://github.com/karma-runner/karma
-[Jasmine]: https://github.com/jasmine/jasmine
+```ruby
+controller.instance_variable_set(:@user, user)
+```
 
-For more information, see the [frontend testing guide](fe_guide/testing.md).
+and use methods which are deprecated in Rails 5 ([#23768]).
+
+[#23768]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23768
+
+#### About Karma
+
+As you may have noticed, Karma is both in the Unit tests and the Integration
+tests category. That's because Karma is a tool that provides an environment to
+run JavaScript tests, so you can either run unit tests (e.g. test a single
+JavaScript method), or integration tests (e.g. test a component that is composed
+of multiple components).
+
+### System tests or Feature tests
+
+Formal definition: https://en.wikipedia.org/wiki/System_testing.
+
+These kind of tests ensure the application works as expected from a user point
+of view (aka black-box testing). These tests should test a happy path for a
+given page or set of pages, and a test case should be added for any regression
+that couldn't have been caught at lower levels with better tests (i.e. if a
+regression is found, regression tests should be added at the lowest-level
+possible).
+
+| Tests path | Testing engine | Notes |
+| ---------- | -------------- | ----- |
+| `spec/features/` | [Capybara] + [RSpec] | If your spec has the `:js` metadata, the browser driver will be [Poltergeist], otherwise it's using [RackTest]. |
+| `features/` | Spinach | Spinach tests are deprecated, [you shouldn't add new Spinach tests](#spinach-feature-tests). |
+
+[Capybara]: https://github.com/teamcapybara/capybara
+[RSpec]: https://github.com/rspec/rspec-rails#feature-specs
+[Poltergeist]: https://github.com/teamcapybara/capybara#poltergeist
+[RackTest]: https://github.com/teamcapybara/capybara#racktest
+
+#### Best practices
+
+- Create only the necessary records in the database
+- Test a happy path and a less happy path but that's it
+- Every other possible path should be tested with Unit or Integration tests
+- Test what's displayed on the page, not the internals of ActiveRecord models.
+  For instance, if you want to verify that a record was created, add
+  expectations that its attributes are displayed on the page, not that
+  `Model.count` increased by one.
+- It's ok to look for DOM elements but don't abuse it since it makes the tests
+  more brittle
+
+If we're confident that the low-level components work well (and we should be if
+we have enough Unit & Integration tests), we shouldn't need to duplicate their
+thorough testing at the System test level.
+
+It's very easy to add tests, but a lot harder to remove or improve tests, so one
+should take care of not introducing too many (slow and duplicated) specs.
+
+The reasons why we should follow these best practices are as follows:
+
+- System tests are slow to run since they spin up the entire application stack
+  in a headless browser, and even slower when they integrate a JS driver
+- When system tests run with a JavaScript driver, the tests are run in a
+  different thread than the application. This means it does not share a
+  database connection and your test will have to commit the transactions in
+  order for the running application to see the data (and vice-versa). In that
+  case we need to truncate the database after each spec instead of simply
+  rolling back a transaction (the faster strategy that's in use for other kind
+  of tests). This is slower than transactions, however, so we want to use
+  truncation only when necessary.
+
+### Black-box tests or End-to-end tests
+
+GitLab consists of [multiple pieces] such as [GitLab Shell], [GitLab Workhorse],
+[Gitaly], [GitLab Pages], [GitLab Runner], and GitLab Rails. All theses pieces
+are configured and packaged by [GitLab Omnibus].
+
+[GitLab QA] is a tool that allows to test that all these pieces integrate well
+together by building a Docker image for a given version of GitLab Rails and
+running feature tests (i.e. using Capybara) against it.
+
+The actual test scenarios and steps are [part of GitLab Rails] so that they're
+always in-sync with the codebase.
+
+[multiple pieces]: ./architecture.md#components
+[GitLab Shell]: https://gitlab.com/gitlab-org/gitlab-shell
+[GitLab Workhorse]: https://gitlab.com/gitlab-org/gitlab-workhorse
+[Gitaly]: https://gitlab.com/gitlab-org/gitaly
+[GitLab Pages]: https://gitlab.com/gitlab-org/gitlab-pages
+[GitLab Runner]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner
+[GitLab Omnibus]: https://gitlab.com/gitlab-org/omnibus-gitlab
+[GitLab QA]: https://gitlab.com/gitlab-org/gitlab-qa
+[part of GitLab Rails]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa
+
+## How to test at the correct level?
+
+As many things in life, deciding what to test at each level of testing is a
+trade-off:
+
+- Unit tests are usually cheap, and you should consider them like the basement
+  of your house: you need them to be confident that your code is behaving
+  correctly. However if you run only unit tests without integration / system tests, you might [miss] the [big] [picture]!
+- Integration tests are a bit more expensive, but don't abuse them. A feature test
+  is often better than an integration test that is stubbing a lot of internals.
+- System tests are expensive (compared to unit tests), even more if they require
+  a JavaScript driver. Make sure to follow the guidelines in the [Speed](#test-speed)
+  section.
+
+Another way to see it is to think about the "cost of tests", this is well
+explained [in this article][tests-cost] and the basic idea is that the cost of a
+test includes:
+
+- The time it takes to write the test
+- The time it takes to run the test every time the suite runs
+- The time it takes to understand the test
+- The time it takes to fix the test if it breaks and the underlying code is OK
+- Maybe, the time it takes to change the code to make the code testable.
+
+[miss]: https://twitter.com/ThePracticalDev/status/850748070698651649
+[big]: https://twitter.com/timbray/status/822470746773409794
+[picture]: https://twitter.com/withzombies/status/829716565834752000
+[tests-cost]: https://medium.com/table-xi/high-cost-tests-and-high-value-tests-a86e27a54df#.2ulyh3a4e
+
+## Frontend testing
+
+Please consult the [dedicated "Frontend testing" guide](./fe_guide/testing.md).
 
 ## RSpec
 
@@ -117,53 +244,124 @@ it 'is overdue' do
 end
 ```
 
-### Test speed
+### System / Feature tests
 
-GitLab has a massive test suite that, without parallelization, can take more
-than an hour to run. It's important that we make an effort to write tests that
-are accurate and effective _as well as_ fast.
+- Feature specs should be named `ROLE_ACTION_spec.rb`, such as
+  `user_changes_password_spec.rb`.
+- Use only one `feature` block per feature spec file.
+- Use scenario titles that describe the success and failure paths.
+- Avoid scenario titles that add no information, such as "successfully".
+- Avoid scenario titles that repeat the feature title.
 
-Here are some things to keep in mind regarding test performance:
+### Matchers
 
-- `double` and `spy` are faster than `FactoryGirl.build(...)`
-- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`.
-- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
-  `spy`, or `double` will do. Database persistence is slow!
-- Use `create(:empty_project)` instead of `create(:project)` when you don't need
-  the underlying Git repository. Filesystem operations are slow!
-- Don't mark a feature as requiring JavaScript (through `@javascript` in
-  Spinach or `js: true` in RSpec) unless it's _actually_ required for the test
-  to be valid. Headless browser testing is slow!
+Custom matchers should be created to clarify the intent and/or hide the
+complexity of RSpec expectations.They should be placed under
+`spec/support/matchers/`. Matchers can be placed in subfolder if they apply to
+a certain type of specs only (e.g. features, requests etc.) but shouldn't be if
+they apply to multiple type of specs.
 
-### Features / Integration
+### Shared contexts
 
-GitLab uses [rspec-rails feature specs] to test features in a browser
-environment. These are [capybara] specs running on the headless [poltergeist]
-driver.
+All shared contexts should be be placed under `spec/support/shared_contexts/`.
+Shared contexts can be placed in subfolder if they apply to a certain type of
+specs only (e.g. features, requests etc.) but shouldn't be if they apply to
+multiple type of specs.
 
-- Feature specs live in `spec/features/` and should be named
-  `ROLE_ACTION_spec.rb`, such as `user_changes_password_spec.rb`.
-- Use only one `feature` block per feature spec file.
-- Use scenario titles that describe the success and failure paths.
-- Avoid scenario titles that add no information, such as "successfully."
-- Avoid scenario titles that repeat the feature title.
+Each file should include only one context and have a descriptive name, e.g.
+`spec/support/shared_contexts/controllers/githubish_import_controller_shared_context.rb`.
 
-[rspec-rails feature specs]: https://github.com/rspec/rspec-rails#feature-specs
-[capybara]: https://github.com/teamcapybara/capybara
-[poltergeist]: https://github.com/teampoltergeist/poltergeist
+### Shared examples
 
-## Spinach (feature) tests
+All shared examples should be be placed under `spec/support/shared_examples/`.
+Shared examples can be placed in subfolder if they apply to a certain type of
+specs only (e.g. features, requests etc.) but shouldn't be if they apply to
+multiple type of specs.
 
-GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
-for its feature/integration tests in September 2012.
+Each file should include only one context and have a descriptive name, e.g.
+`spec/support/shared_examples/controllers/githubish_import_controller_shared_example.rb`.
 
-As of March 2016, we are [trying to avoid adding new Spinach
-tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward,
-opting for [RSpec feature](#features-integration) specs.
+### Helpers
 
-Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
-no more than one new `step` definition. If more than that is required, the
-test should be re-implemented using RSpec instead.
+Helpers are usually modules that provide some methods to hide the complexity of
+specific RSpec examples. You can define helpers in RSpec files if they're not
+intended to be shared with other specs. Otherwise, they should be be placed
+under `spec/support/helpers/`. Helpers can be placed in subfolder if they apply
+to a certain type of specs only (e.g. features, requests etc.) but shouldn't be
+if they apply to multiple type of specs.
+
+Helpers should follow the Rails naming / namespacing convention. For instance
+`spec/support/helpers/cycle_analytics_helpers.rb` should define:
+
+```ruby
+module Spec
+  module Support
+    module Helpers
+      module CycleAnalyticsHelpers
+        def create_commit_referencing_issue(issue, branch_name: random_git_name)
+          project.repository.add_branch(user, branch_name, 'master')
+          create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
+        end
+      end
+    end
+  end
+end
+```
+
+Helpers should not change the RSpec config. For instance, the helpers module
+described above should not include:
+
+```ruby
+RSpec.configure do |config|
+  config.include Spec::Support::Helpers::CycleAnalyticsHelpers
+end
+```
+
+### Factories
+
+GitLab uses [factory_girl] as a test fixture replacement.
+
+- Factory definitions live in `spec/factories/`, named using the pluralization
+  of their corresponding model (`User` factories are defined in `users.rb`).
+- There should be only one top-level factory definition per file.
+- FactoryGirl methods are mixed in to all RSpec groups. This means you can (and
+  should) call `create(...)` instead of `FactoryGirl.create(...)`.
+- Make use of [traits] to clean up definitions and usages.
+- When defining a factory, don't define attributes that are not required for the
+  resulting record to pass validation.
+- When instantiating from a factory, don't supply attributes that aren't
+  required by the test.
+- Factories don't have to be limited to `ActiveRecord` objects.
+  [See example](https://gitlab.com/gitlab-org/gitlab-ce/commit/0b8cefd3b2385a21cfed779bd659978c0402766d).
+
+[factory_girl]: https://github.com/thoughtbot/factory_girl
+[traits]: http://www.rubydoc.info/gems/factory_girl/file/GETTING_STARTED.md#Traits
+
+### Fixtures
+
+All fixtures should be be placed under `spec/fixtures/`.
+
+### Config
+
+RSpec config files are files that change the RSpec config (i.e.
+`RSpec.configure do |config|` blocks). They should be placed under
+`spec/support/config/`.
+
+Each file should be related to a specific domain, e.g.
+`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc.
+
+Helpers can be included in the `spec/support/config/rspec.rb` file. If a
+helpers module applies only to a certain kind of specs, it should add modifiers
+to the `config.include` call. For instance if
+`spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and
+`type: :model` specs only, you would write the following:
+
+```ruby
+RSpec.configure do |config|
+  config.include Spec::Support::Helpers::CycleAnalyticsHelpers, :lib
+  config.include Spec::Support::Helpers::CycleAnalyticsHelpers, type: :model
+end
+```
 
 ## Testing Rake Tasks
 
@@ -201,6 +399,77 @@ describe 'gitlab:shell rake tasks' do
 end
 ```
 
+## Test speed
+
+GitLab has a massive test suite that, without [parallelization], can take hours
+to run. It's important that we make an effort to write tests that are accurate
+and effective _as well as_ fast.
+
+Here are some things to keep in mind regarding test performance:
+
+- `double` and `spy` are faster than `FactoryGirl.build(...)`
+- `FactoryGirl.build(...)` and `.build_stubbed` are faster than `.create`.
+- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
+  `spy`, or `double` will do. Database persistence is slow!
+- Use `create(:empty_project)` instead of `create(:project)` when you don't need
+  the underlying Git repository. Filesystem operations are slow!
+- Don't mark a feature as requiring JavaScript (through `@javascript` in
+  Spinach or `:js` in RSpec) unless it's _actually_ required for the test
+  to be valid. Headless browser testing is slow!
+
+[parallelization]: #test-suite-parallelization-on-the-ci
+
+### Test suite parallelization on the CI
+
+Our current CI parallelization setup is as follows:
+
+1. The `knapsack` job in the prepare stage that is supposed to ensure we have a
+  `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file:
+  - The `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file is fetched
+    from S3, if it's not here we initialize the file with `{}`.
+1. Each `rspec x y` job are run with `knapsack rspec` and should have an evenly
+  distributed share of tests:
+  - It works because the jobs have access to the
+    `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` since the "artifacts
+    from all previous stages are passed by default". [^1]
+  - the jobs set their own report path to
+    `KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`.
+  - if knapsack is doing its job, test files that are run should be listed under
+    `Report specs`, not under `Leftover specs`.
+1. The `update-knapsack` job takes all the
+  `knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json`
+  files from the `rspec x y` jobs and merge them all together into a single
+  `knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file that is then
+  uploaded to S3.
+
+After that, the next pipeline will use the up-to-date
+`knapsack/${CI_PROJECT_NAME}/rspec_report-master.json` file. The same strategy
+is used for Spinach tests as well.
+
+### Monitoring
+
+The GitLab test suite is [monitored] and a [public dashboard] is available for
+everyone to see. Feel free to look at the slowest test files and try to improve
+them.
+
+[monitored]: ./performance.md#rspec-profiling
+[public dashboard]: https://redash.gitlab.com/public/dashboards/l1WhHXaxrCWM5Ai9D7YDqHKehq6OU3bx5gssaiWe?org_slug=default
+
+## Spinach (feature) tests
+
+GitLab [moved from Cucumber to Spinach](https://github.com/gitlabhq/gitlabhq/pull/1426)
+for its feature/integration tests in September 2012.
+
+As of March 2016, we are [trying to avoid adding new Spinach
+tests](https://gitlab.com/gitlab-org/gitlab-ce/issues/14121) going forward,
+opting for [RSpec feature](#features-integration) specs.
+
+Adding new Spinach scenarios is acceptable _only if_ the new scenario requires
+no more than one new `step` definition. If more than that is required, the
+test should be re-implemented using RSpec instead.
+
 ---
 
 [Return to Development documentation](README.md)
+
+[^1]: /ci/yaml/README.html#dependencies
diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md
index bbcd26477f34ff46cdeacd66c73c3ec1569d20f5..8da6ad684f5f5fcc70c1a5d5425e6091dded2f2e 100644
--- a/doc/development/what_requires_downtime.md
+++ b/doc/development/what_requires_downtime.md
@@ -2,7 +2,8 @@
 
 When working with a database certain operations can be performed without taking
 GitLab offline, others do require a downtime period. This guide describes
-various operations and their impact.
+various operations, their impact, and how to perform them without requiring
+downtime.
 
 ## Adding Columns
 
@@ -41,50 +42,156 @@ information on how to use this method.
 
 ## Dropping Columns
 
-On PostgreSQL you can safely remove an existing column without the need for
-downtime. When you drop a column in PostgreSQL it's not immediately removed,
-instead it is simply disabled. The data is removed on the next vacuum run.
+Removing columns is tricky because running GitLab processes may still be using
+the columns. To work around this you will need two separate merge requests and
+releases: one to ignore and then remove the column, and one to remove the ignore
+rule.
 
-On MySQL this operation requires downtime.
+### Step 1: Ignoring The Column
 
-While database wise dropping a column may be fine on PostgreSQL this operation
-still requires downtime because the application code may still be using the
-column that was removed. For example, consider the following migration:
+The first step is to ignore the column in the application code. This is
+necessary because Rails caches the columns and re-uses this cache in various
+places. This can be done by including the `IgnorableColumn` module into the
+model, followed by defining the columns to ignore. For example, to ignore
+`updated_at` in the User model you'd use the following:
 
 ```ruby
-class MyMigration < ActiveRecord::Migration
-  def change
-    remove_column :projects, :dummy
-  end
+class User < ActiveRecord::Base
+  include IgnorableColumn
+
+  ignore_column :updated_at
 end
 ```
 
-Now imagine that the GitLab instance is running and actively uses the `dummy`
-column. If we were to run the migration this would result in the GitLab instance
-producing errors whenever it tries to use the `dummy` column.
+Once added you should create a _post-deployment_ migration that removes the
+column. Both these changes should be submitted in the same merge request.
 
-As a result of the above downtime _is_ required when removing a column, even
-when using PostgreSQL.
+### Step 2: Removing The Ignore Rule
+
+Once the changes from step 1 have been released & deployed you can set up a
+separate merge request that removes the ignore rule. This merge request can
+simply remove the `ignore_column` line, and the `include IgnorableColumn` line
+if no other `ignore_column` calls remain.
 
 ## Renaming Columns
 
-Renaming columns requires downtime as running GitLab instances will continue
-using the old column name until a new version is deployed. This can result
-in the instance producing errors, which in turn can impact the user experience.
+Renaming columns the normal way requires downtime as an application may continue
+using the old column name during/after a database migration. To rename a column
+without requiring downtime we need two migrations: a regular migration, and a
+post-deployment migration. Both these migration can go in the same release.
 
-## Changing Column Constraints
+### Step 1: Add The Regular Migration
+
+First we need to create the regular migration. This migration should use
+`Gitlab::Database::MigrationHelpers#rename_column_concurrently` to perform the
+renaming. For example
+
+```ruby
+# A regular migration in db/migrate
+class RenameUsersUpdatedAtToUpdatedAtTimestamp < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  def up
+    rename_column_concurrently :users, :updated_at, :updated_at_timestamp
+  end
+
+  def down
+    cleanup_concurrent_column_rename :users, :updated_at_timestamp, :updated_at
+  end
+end
+```
+
+This will take care of renaming the column, ensuring data stays in sync, copying
+over indexes and foreign keys, etc.
+
+**NOTE:** if a column contains 1 or more indexes that do not contain the name of
+the original column, the above procedure will fail. In this case you will first
+need to rename these indexes.
 
-Generally changing column constraints requires checking all rows in the table to
-see if they meet the new constraint, unless a constraint is _removed_. For
-example, changing a column that previously allowed NULL values to not allow NULL
-values requires the database to verify all existing rows.
+### Step 2: Add A Post-Deployment Migration
 
-The specific behaviour varies a bit between databases but in general the safest
-approach is to assume changing constraints requires downtime.
+The renaming procedure requires some cleaning up in a post-deployment migration.
+We can perform this cleanup using
+`Gitlab::Database::MigrationHelpers#cleanup_concurrent_column_rename`:
+
+```ruby
+# A post-deployment migration in db/post_migrate
+class CleanupUsersUpdatedAtRename < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  def up
+    cleanup_concurrent_column_rename :users, :updated_at, :updated_at_timestamp
+  end
+
+  def down
+    rename_column_concurrently :users, :updated_at_timestamp, :updated_at
+  end
+end
+```
+
+## Changing Column Constraints
+
+Adding or removing a NOT NULL clause (or another constraint) can typically be
+done without requiring downtime. However, this does require that any application
+changes are deployed _first_. Thus, changing the constraints of a column should
+happen in a post-deployment migration.
 
 ## Changing Column Types
 
-This operation requires downtime.
+Changing the type of a column can be done using
+`Gitlab::Database::MigrationHelpers#change_column_type_concurrently`. This
+method works similarly to `rename_column_concurrently`. For example, let's say
+we want to change the type of `users.username` from `string` to `text`.
+
+### Step 1: Create A Regular Migration
+
+A regular migration is used to create a new column with a temporary name along
+with setting up some triggers to keep data in sync. Such a migration would look
+as follows:
+
+```ruby
+# A regular migration in db/migrate
+class ChangeUsersUsernameStringToText < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  def up
+    change_column_type_concurrently :users, :username, :text
+  end
+
+  def down
+    cleanup_concurrent_column_type_change :users, :username
+  end
+end
+```
+
+### Step 2: Create A Post Deployment Migration
+
+Next we need to clean up our changes using a post-deployment migration:
+
+```ruby
+# A post-deployment migration in db/post_migrate
+class ChangeUsersUsernameStringToTextCleanup < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  disable_ddl_transaction!
+
+  def up
+    cleanup_concurrent_column_type_change :users
+  end
+
+  def down
+    change_column_type_concurrently :users, :username, :string
+  end
+end
+```
+
+And that's it, we're done!
 
 ## Adding Indexes
 
@@ -101,12 +208,19 @@ Migrations can take advantage of this by using the method
 
 ```ruby
 class MyMigration < ActiveRecord::Migration
-  def change
+  def up
     add_concurrent_index :projects, :column_name
   end
+
+  def down
+    remove_index(:projects, :column_name) if index_exists?(:projects, :column_name)
+  end
 end
 ```
 
+Note that `add_concurrent_index` can not be reversed automatically, thus you
+need to manually define `up` and `down`.
+
 When running this on PostgreSQL the `CONCURRENTLY` option mentioned above is
 used. On MySQL this method produces a regular `CREATE INDEX` query.
 
@@ -125,43 +239,54 @@ This operation is safe as there's no code using the table just yet.
 
 ## Dropping Tables
 
-This operation requires downtime as application code may still be using the
-table.
+Dropping tables can be done safely using a post-deployment migration, but only
+if the application no longer uses the table.
 
 ## Adding Foreign Keys
 
-Adding foreign keys acquires an exclusive lock on both the source and target
-tables in PostgreSQL. This requires downtime as otherwise the entire application
-grinds to a halt for the duration of the operation.
+Adding foreign keys usually works in 3 steps:
+
+1. Start a transaction
+1. Run `ALTER TABLE` to add the constraint(s)
+1. Check all existing data
 
-On MySQL this operation also requires downtime _unless_ foreign key checks are
-disabled. Because this means checks aren't enforced this is not ideal, as such
-one should assume MySQL also requires downtime.
+Because `ALTER TABLE` typically acquires an exclusive lock until the end of a
+transaction this means this approach would require downtime.
+
+GitLab allows you to work around this by using
+`Gitlab::Database::MigrationHelpers#add_concurrent_foreign_key`. This method
+ensures that when PostgreSQL is used no downtime is needed.
 
 ## Removing Foreign Keys
 
-This operation should not require downtime on both PostgreSQL and MySQL.
+This operation does not require downtime.
 
-## Updating Data
+## Data Migrations
 
-Updating data should generally be safe. The exception to this is data that's
-being migrated from one version to another while the application still produces
-data in the old version.
+Data migrations can be tricky. The usual approach to migrate data is to take a 3
+step approach:
 
-For example, imagine the application writes the string `'dog'` to a column but
-it really is meant to write `'cat'` instead. One might think that the following
-migration is all that is needed to solve this problem:
+1. Migrate the initial batch of data
+1. Deploy the application code
+1. Migrate any remaining data
 
-```ruby
-class MyMigration < ActiveRecord::Migration
-  def up
-    execute("UPDATE some_table SET column = 'cat' WHERE column = 'dog';")
-  end
-end
-```
+Usually this works, but not always. For example, if a field's format is to be
+changed from JSON to something else we have a bit of a problem. If we were to
+change existing data before deploying application code we'll most likely run
+into errors. On the other hand, if we were to migrate after deploying the
+application code we could run into the same problems.
+
+If you merely need to correct some invalid data, then a post-deployment
+migration is usually enough. If you need to change the format of data (e.g. from
+JSON to something else) it's typically best to add a new column for the new data
+format, and have the application use that. In such a case the procedure would
+be:
 
-Unfortunately this is not enough. Because the application is still running and
-using the old value this may result in the table still containing rows where
-`column` is set to `dog`, even after the migration finished.
+1. Add a new column in the new format
+1. Copy over existing data to this new column
+1. Deploy the application code
+1. In a post-deployment migration, copy over any remaining data
 
-In these cases downtime _is_ required, even for rarely updated tables.
+In general there is no one-size-fits-all solution, therefore it's best to
+discuss these kind of migrations in a merge request to make sure they are
+implemented in the best way possible.
diff --git a/doc/topics/authentication/index.md b/doc/topics/authentication/index.md
index 47d3f05999df132404ca98e79bc3c7137176d757..eafd2fd9d04f18c4bca0942a30442e7b0a7bd7ac 100644
--- a/doc/topics/authentication/index.md
+++ b/doc/topics/authentication/index.md
@@ -15,11 +15,11 @@ This page gathers all the resources for the topic **Authentication** within GitL
 ## GitLab administrators
 
 - [LDAP (Community Edition)](../../administration/auth/ldap.md)
-- [LDAP (Enterprise Edition)](https://docs.gitlab.com/ee/administration/auth/ldap-ee.md)
+- [LDAP (Enterprise Edition)](https://docs.gitlab.com/ee/administration/auth/ldap-ee.html)
 - [Enforce Two-factor Authentication (2FA)](../../security/two_factor_authentication.md#enforce-two-factor-authentication-2fa)
 - **Articles:**
   - [Feature Highlight: LDAP Integration](https://about.gitlab.com/2014/07/10/feature-highlight-ldap-sync/)
-  - [Debugging LDAP](https://about.gitlab.com/handbook/support/workflows/ldap/debugging_ldap.md)
+  - [Debugging LDAP](https://about.gitlab.com/handbook/support/workflows/ldap/debugging_ldap.html)
 - **Integrations:**
   - [OmniAuth](../../integration/omniauth.md)
   - [Authentiq OmniAuth Provider](../../administration/auth/authentiq.md#authentiq-omniauth-provider)
@@ -27,7 +27,7 @@ This page gathers all the resources for the topic **Authentication** within GitL
   - [CAS OmniAuth Provider](../../integration/cas.md)
   - [SAML OmniAuth Provider](../../integration/saml.md)
   - [Okta SSO provider](../../administration/auth/okta.md)
-  - [Kerberos integration (GitLab EE)](https://docs.gitlab.com/ee/integration/kerberos.md)
+  - [Kerberos integration (GitLab EE)](https://docs.gitlab.com/ee/integration/kerberos.html)
 
 ## API
 
diff --git a/doc/topics/index.md b/doc/topics/index.md
index 4a16f6f629c8c318a67556b9f07d6d9f42a6dbe0..ad388dff8221ffbda8512d2b2c8beeb68e8537ce 100644
--- a/doc/topics/index.md
+++ b/doc/topics/index.md
@@ -7,9 +7,10 @@ you through better understanding GitLab's concepts
 through our regular docs, and, when available, through articles (guides,
 tutorials, technical overviews, blog posts) and videos.
 
+- [Authentication](authentication/index.md)
 - [Continuous Integration (GitLab CI)](../ci/README.md)
 - [Git](git/index.md)
 - [GitLab Installation](../install/README.md)
 - [GitLab Pages](../user/project/pages/index.md)
 
->**Note:** More topics will be available soon.
\ No newline at end of file
+>**Note:** More topics will be available soon.
diff --git a/doc/update/README.md b/doc/update/README.md
index 837b31abb979e4161643d7965f5f49e6fc814be8..d024a809f24b26101159a08652f3799556e028d2 100644
--- a/doc/update/README.md
+++ b/doc/update/README.md
@@ -48,6 +48,20 @@ GitLab provides official Docker images for both Community and Enterprise
 editions. They are based on the Omnibus package and instructions on how to
 update them are in [a separate document][omnidocker].
 
+## Upgrading without downtime
+
+Starting with GitLab 9.1.0 it's possible to upgrade to a newer major, minor, or patch version of GitLab
+without having to take your GitLab instance offline. However, for this to work
+there are the following requirements:
+
+1. You can only upgrade 1 minor release at a time. So from 9.1 to 9.2, not to 9.3.
+2. You have to be on the most recent patch release. For example, if 9.1.15 is the last
+   release of 9.1 then you can safely upgrade from that version to any 9.2.x version.
+   However, if you are running 9.1.14 you first need to upgrade to 9.1.15.
+2. You have to use [post-deployment
+   migrations](../development/post_deployment_migrations.md).
+3. You are using PostgreSQL. If you are using MySQL please look at the release post to see if downtime is required.
+
 ## Upgrading between editions
 
 GitLab comes in two flavors: [Community Edition][ce] which is MIT licensed,
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
index 62afd8cf2478e8ffefe0d11c53c6d48fbce4828c..8f6b530c033e874a14c1f24439ca1a112f2bb1c3 100644
--- a/doc/user/project/cycle_analytics.md
+++ b/doc/user/project/cycle_analytics.md
@@ -5,10 +5,10 @@
 
 Cycle Analytics measures the time it takes to go from an [idea to production] for
 each project you have. This is achieved by not only indicating the total time it
-takes to reach at that point, but the total time is broken down into the
+takes to reach that point, but the total time is broken down into the
 multiple stages an idea has to pass through to be shipped.
 
-Cycle Analytics is that it is tightly coupled with the [GitLab flow] and
+Cycle Analytics is tightly coupled with the [GitLab flow] and
 calculates a separate median for each stage.
 
 ## Overview
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
index 2a890acde4dcb90cf428ff47ae63c8b61932659e..f7d5e3a8ab243c4470ff15a7ce10961b868319a3 100644
--- a/doc/user/project/integrations/kubernetes.md
+++ b/doc/user/project/integrations/kubernetes.md
@@ -60,7 +60,7 @@ to use terminals. Support is currently limited to the first container in the
 first pod of your environment.
 
 When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals)
-support to your environments. This is based on the `exec` functionality found in
+support to your [environments](../../../ci/environments.md). This is based on the `exec` functionality found in
 Docker and Kubernetes, so you get a new shell session within your existing
 containers. To use this integration, you should deploy to Kubernetes using
 the deployment variables above, ensuring any pods you create are labelled with
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 53791166c33db4658469b6e444f0e47e9536f8bd..87dfd1573a4122006ee87ee4c57fe24c110da439 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -13,7 +13,7 @@ module API
         optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
         optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
         optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
-        optional :build_events, type: Boolean, desc: "Trigger hook on build events"
+        optional :job_events, type: Boolean, desc: "Trigger hook on job events"
         optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
         optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
         optional :enable_ssl_verification, type: Boolean, desc: "Do SSL verification when triggering the hook"
@@ -53,7 +53,10 @@ module API
         use :project_hook_properties
       end
       post ":id/hooks" do
-        hook = user_project.hooks.new(declared_params(include_missing: false))
+        hook_params = declared_params(include_missing: false)
+        hook_params[:build_events] = hook_params.delete(:job_events) { false }
+
+        hook = user_project.hooks.new(hook_params)
 
         if hook.save
           present hook, with: Entities::ProjectHook
@@ -74,7 +77,10 @@ module API
       put ":id/hooks/:hook_id" do
         hook = user_project.hooks.find(params.delete(:hook_id))
 
-        if hook.update_attributes(declared_params(include_missing: false))
+        update_params = declared_params(include_missing: false)
+        update_params[:build_events] = update_params.delete(:job_events) if update_params[:job_events]
+
+        if hook.update_attributes(update_params)
           present hook, with: Entities::ProjectHook
         else
           error!("Invalid url given", 422) if hook.errors[:url].present?
diff --git a/lib/banzai/filter/issuable_state_filter.rb b/lib/banzai/filter/issuable_state_filter.rb
index 6b78aa795b444da2525a8535c07503d945707f3e..0b2b8bd7f4db909f46e2baa091d5a58f0d0a7fc1 100644
--- a/lib/banzai/filter/issuable_state_filter.rb
+++ b/lib/banzai/filter/issuable_state_filter.rb
@@ -13,8 +13,8 @@ module Banzai
         issuables = extractor.extract([doc])
 
         issuables.each do |node, issuable|
-          if VISIBLE_STATES.include?(issuable.state)
-            node.children.last.content += " [#{issuable.state}]"
+          if VISIBLE_STATES.include?(issuable.state) && node.children.present?
+            node.add_child(Nokogiri::XML::Text.new(" [#{issuable.state}]", doc))
           end
         end
 
diff --git a/lib/bitbucket/representation/base.rb b/lib/bitbucket/representation/base.rb
index 94adaacc9b5598a85c2415ae8053e28380265892..800d5a075c6841c962adfa7f2e132f357970c069 100644
--- a/lib/bitbucket/representation/base.rb
+++ b/lib/bitbucket/representation/base.rb
@@ -1,6 +1,8 @@
 module Bitbucket
   module Representation
     class Base
+      attr_reader :raw
+
       def initialize(raw)
         @raw = raw
       end
@@ -8,10 +10,6 @@ module Bitbucket
       def self.decorate(entries)
         entries.map { |entry| new(entry)}
       end
-
-      private
-
-      attr_reader :raw
     end
   end
 end
diff --git a/lib/container_registry/path.rb b/lib/container_registry/path.rb
index a4b5f2aba6cf14372e49f623af76172d016914f7..4a585996aa59757a1cd6d099cdce7443d1a9eddf 100644
--- a/lib/container_registry/path.rb
+++ b/lib/container_registry/path.rb
@@ -15,7 +15,7 @@ module ContainerRegistry
     LEVELS_SUPPORTED = 3
 
     def initialize(path)
-      @path = path
+      @path = path.to_s.downcase
     end
 
     def valid?
@@ -25,7 +25,7 @@ module ContainerRegistry
     end
 
     def components
-      @components ||= @path.to_s.split('/')
+      @components ||= @path.split('/')
     end
 
     def nodes
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index f4efa20374a92913f688a31e9e4cb5778d97b89e..5a6d9ae99a0d28c901fa2f347b60ba511e275bec 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -149,7 +149,7 @@ module Gitlab
             description += @formatter.author_line(pull_request.author) unless find_user_id(pull_request.author)
             description += pull_request.description
 
-            merge_request = project.merge_requests.create(
+            merge_request = project.merge_requests.create!(
               iid: pull_request.iid,
               title: pull_request.title,
               description: description,
@@ -168,7 +168,7 @@ module Gitlab
 
             import_pull_request_comments(pull_request, merge_request) if merge_request.persisted?
           rescue StandardError => e
-            errors << { type: :pull_request, iid: pull_request.iid, errors: e.message }
+            errors << { type: :pull_request, iid: pull_request.iid, errors: e.message, trace: e.backtrace.join("\n"), raw_response: pull_request.raw }
           end
         end
       end
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index 2af94e2c60eff5b826542a1a899510275245c9dc..3b335cdfd01feb5947a4aa475a6b30f565dd8cdf 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -25,11 +25,10 @@ module Gitlab
         end
 
         def limit(last_bytes = LIMIT_SIZE)
-          stream_size = size
-          if stream_size < last_bytes
-            last_bytes = stream_size
+          if last_bytes < size
+            stream.seek(-last_bytes, IO::SEEK_END)
+            stream.readline
           end
-          stream.seek(-last_bytes, IO::SEEK_END)
         end
 
         def append(data, offset)
@@ -76,11 +75,14 @@ module Gitlab
           stream.each_line do |line|
             matches = line.scan(regex)
             next unless matches.is_a?(Array)
+            next if matches.empty?
 
             match = matches.flatten.last
             coverage = match.gsub(/\d+(\.\d+)?/).first
-            return coverage.to_f if coverage.present?
+            return coverage if coverage.present?
           end
+
+          nil
         rescue
           # if bad regex or something goes wrong we dont want to interrupt transition
           # so we just silentrly ignore error for now
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index 63b8d0d3b9d1fdb267799c0051af876899893d5c..d0bd129967143df816ae62e98e5f1033690688f8 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -57,16 +57,16 @@ module Gitlab
       postgresql? ? "RANDOM()" : "RAND()"
     end
 
-    def true_value
-      if Gitlab::Database.postgresql?
+    def self.true_value
+      if postgresql?
         "'t'"
       else
         1
       end
     end
 
-    def false_value
-      if Gitlab::Database.postgresql?
+    def self.false_value
+      if postgresql?
         "'f'"
       else
         0
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 525aa9203285d8955829ce534781e24212d88358..6dabbe0264c2b23f9e44a8e3a2b4fad423126fa3 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -89,7 +89,8 @@ module Gitlab
         ADD CONSTRAINT #{key_name}
         FOREIGN KEY (#{column})
         REFERENCES #{target} (id)
-        ON DELETE #{on_delete} NOT VALID;
+        #{on_delete ? "ON DELETE #{on_delete}" : ''}
+        NOT VALID;
         EOF
 
         # Validate the existing constraint. This can potentially take a very
@@ -114,6 +115,14 @@ module Gitlab
         execute('SET statement_timeout TO 0') if Database.postgresql?
       end
 
+      def true_value
+        Database.true_value
+      end
+
+      def false_value
+        Database.false_value
+      end
+
       # Updates the value of a column in batches.
       #
       # This method updates the table in batches of 5% of the total row count.
@@ -250,6 +259,245 @@ module Gitlab
           raise error
         end
       end
+
+      # Renames a column without requiring downtime.
+      #
+      # Concurrent renames work by using database triggers to ensure both the
+      # old and new column are in sync. However, this method will _not_ remove
+      # the triggers or the old column automatically; this needs to be done
+      # manually in a post-deployment migration. This can be done using the
+      # method `cleanup_concurrent_column_rename`.
+      #
+      # table - The name of the database table containing the column.
+      # old - The old column name.
+      # new - The new column name.
+      # type - The type of the new column. If no type is given the old column's
+      #        type is used.
+      def rename_column_concurrently(table, old, new, type: nil)
+        if transaction_open?
+          raise 'rename_column_concurrently can not be run inside a transaction'
+        end
+
+        trigger_name = rename_trigger_name(table, old, new)
+        quoted_table = quote_table_name(table)
+        quoted_old = quote_column_name(old)
+        quoted_new = quote_column_name(new)
+
+        if Database.postgresql?
+          install_rename_triggers_for_postgresql(trigger_name, quoted_table,
+                                                 quoted_old, quoted_new)
+        else
+          install_rename_triggers_for_mysql(trigger_name, quoted_table,
+                                            quoted_old, quoted_new)
+        end
+
+        old_col = column_for(table, old)
+        new_type = type || old_col.type
+
+        add_column(table, new, new_type,
+                   limit: old_col.limit,
+                   default: old_col.default,
+                   null: old_col.null,
+                   precision: old_col.precision,
+                   scale: old_col.scale)
+
+        update_column_in_batches(table, new, Arel::Table.new(table)[old])
+
+        copy_indexes(table, old, new)
+        copy_foreign_keys(table, old, new)
+      end
+
+      # Changes the type of a column concurrently.
+      #
+      # table - The table containing the column.
+      # column - The name of the column to change.
+      # new_type - The new column type.
+      def change_column_type_concurrently(table, column, new_type)
+        temp_column = "#{column}_for_type_change"
+
+        rename_column_concurrently(table, column, temp_column, type: new_type)
+      end
+
+      # Performs cleanup of a concurrent type change.
+      #
+      # table - The table containing the column.
+      # column - The name of the column to change.
+      # new_type - The new column type.
+      def cleanup_concurrent_column_type_change(table, column)
+        temp_column = "#{column}_for_type_change"
+
+        transaction do
+          # This has to be performed in a transaction as otherwise we might have
+          # inconsistent data.
+          cleanup_concurrent_column_rename(table, column, temp_column)
+          rename_column(table, temp_column, column)
+        end
+      end
+
+      # Cleans up a concurrent column name.
+      #
+      # This method takes care of removing previously installed triggers as well
+      # as removing the old column.
+      #
+      # table - The name of the database table.
+      # old - The name of the old column.
+      # new - The name of the new column.
+      def cleanup_concurrent_column_rename(table, old, new)
+        trigger_name = rename_trigger_name(table, old, new)
+
+        if Database.postgresql?
+          remove_rename_triggers_for_postgresql(table, trigger_name)
+        else
+          remove_rename_triggers_for_mysql(trigger_name)
+        end
+
+        remove_column(table, old)
+      end
+
+      # Performs a concurrent column rename when using PostgreSQL.
+      def install_rename_triggers_for_postgresql(trigger, table, old, new)
+        execute <<-EOF.strip_heredoc
+        CREATE OR REPLACE FUNCTION #{trigger}()
+        RETURNS trigger AS
+        $BODY$
+        BEGIN
+          NEW.#{new} := NEW.#{old};
+          RETURN NEW;
+        END;
+        $BODY$
+        LANGUAGE 'plpgsql'
+        VOLATILE
+        EOF
+
+        execute <<-EOF.strip_heredoc
+        CREATE TRIGGER #{trigger}
+        BEFORE INSERT OR UPDATE
+        ON #{table}
+        FOR EACH ROW
+        EXECUTE PROCEDURE #{trigger}()
+        EOF
+      end
+
+      # Installs the triggers necessary to perform a concurrent column rename on
+      # MySQL.
+      def install_rename_triggers_for_mysql(trigger, table, old, new)
+        execute <<-EOF.strip_heredoc
+        CREATE TRIGGER #{trigger}_insert
+        BEFORE INSERT
+        ON #{table}
+        FOR EACH ROW
+        SET NEW.#{new} = NEW.#{old}
+        EOF
+
+        execute <<-EOF.strip_heredoc
+        CREATE TRIGGER #{trigger}_update
+        BEFORE UPDATE
+        ON #{table}
+        FOR EACH ROW
+        SET NEW.#{new} = NEW.#{old}
+        EOF
+      end
+
+      # Removes the triggers used for renaming a PostgreSQL column concurrently.
+      def remove_rename_triggers_for_postgresql(table, trigger)
+        execute("DROP TRIGGER #{trigger} ON #{table}")
+        execute("DROP FUNCTION #{trigger}()")
+      end
+
+      # Removes the triggers used for renaming a MySQL column concurrently.
+      def remove_rename_triggers_for_mysql(trigger)
+        execute("DROP TRIGGER #{trigger}_insert")
+        execute("DROP TRIGGER #{trigger}_update")
+      end
+
+      # Returns the (base) name to use for triggers when renaming columns.
+      def rename_trigger_name(table, old, new)
+        'trigger_' + Digest::SHA256.hexdigest("#{table}_#{old}_#{new}").first(12)
+      end
+
+      # Returns an Array containing the indexes for the given column
+      def indexes_for(table, column)
+        column = column.to_s
+
+        indexes(table).select { |index| index.columns.include?(column) }
+      end
+
+      # Returns an Array containing the foreign keys for the given column.
+      def foreign_keys_for(table, column)
+        column = column.to_s
+
+        foreign_keys(table).select { |fk| fk.column == column }
+      end
+
+      # Copies all indexes for the old column to a new column.
+      #
+      # table - The table containing the columns and indexes.
+      # old - The old column.
+      # new - The new column.
+      def copy_indexes(table, old, new)
+        old = old.to_s
+        new = new.to_s
+
+        indexes_for(table, old).each do |index|
+          new_columns = index.columns.map do |column|
+            column == old ? new : column
+          end
+
+          # This is necessary as we can't properly rename indexes such as
+          # "ci_taggings_idx".
+          unless index.name.include?(old)
+            raise "The index #{index.name} can not be copied as it does not "\
+              "mention the old column. You have to rename this index manually first."
+          end
+
+          name = index.name.gsub(old, new)
+
+          options = {
+            unique: index.unique,
+            name: name,
+            length: index.lengths,
+            order: index.orders
+          }
+
+          # These options are not supported by MySQL, so we only add them if
+          # they were previously set.
+          options[:using] = index.using if index.using
+          options[:where] = index.where if index.where
+
+          unless index.opclasses.blank?
+            opclasses = index.opclasses.dup
+
+            # Copy the operator classes for the old column (if any) to the new
+            # column.
+            opclasses[new] = opclasses.delete(old) if opclasses[old]
+
+            options[:opclasses] = opclasses
+          end
+
+          add_concurrent_index(table, new_columns, options)
+        end
+      end
+
+      # Copies all foreign keys for the old column to the new column.
+      #
+      # table - The table containing the columns and indexes.
+      # old - The old column.
+      # new - The new column.
+      def copy_foreign_keys(table, old, new)
+        foreign_keys_for(table, old).each do |fk|
+          add_concurrent_foreign_key(fk.from_table,
+                                     fk.to_table,
+                                     column: new,
+                                     on_delete: fk.on_delete)
+        end
+      end
+
+      # Returns the column for the given table and column name.
+      def column_for(table, name)
+        name = name.to_s
+
+        columns(table).find { |column| column.name == name }
+      end
     end
   end
 end
diff --git a/lib/gitlab/database/multi_threaded_migration.rb b/lib/gitlab/database/multi_threaded_migration.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7ae5a4c17c8b3d7c73f8f71e5cc7cc194cd96a31
--- /dev/null
+++ b/lib/gitlab/database/multi_threaded_migration.rb
@@ -0,0 +1,52 @@
+module Gitlab
+  module Database
+    module MultiThreadedMigration
+      MULTI_THREAD_AR_CONNECTION = :thread_local_ar_connection
+
+      # This overwrites the default connection method so that every thread can
+      # use a thread-local connection, while still supporting all of Rails'
+      # migration methods.
+      def connection
+        Thread.current[MULTI_THREAD_AR_CONNECTION] ||
+          ActiveRecord::Base.connection
+      end
+
+      # Starts a thread-pool for N threads, along with N threads each using a
+      # single connection. The provided block is yielded from inside each
+      # thread.
+      #
+      # Example:
+      #
+      #     with_multiple_threads(4) do
+      #       execute('SELECT ...')
+      #     end
+      #
+      # thread_count - The number of threads to start.
+      #
+      # join - When set to true this method will join the threads, blocking the
+      #        caller until all threads have finished running.
+      #
+      # Returns an Array containing the started threads.
+      def with_multiple_threads(thread_count, join: true)
+        pool = Gitlab::Database.create_connection_pool(thread_count)
+
+        threads = Array.new(thread_count) do
+          Thread.new do
+            pool.with_connection do |connection|
+              begin
+                Thread.current[MULTI_THREAD_AR_CONNECTION] = connection
+                yield
+              ensure
+                Thread.current[MULTI_THREAD_AR_CONNECTION] = nil
+              end
+            end
+          end
+        end
+
+        threads.each(&:join) if join
+
+        threads
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 41ab73abb567ee97c8ea0ef01381925c9518a839..d7dac9f6149862bd39bbb50a2e4ae3f97fe24d9a 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -45,13 +45,15 @@ module Gitlab
 
       # Default branch in the repository
       def root_ref
-        @root_ref ||= Gitlab::GitalyClient.migrate(:root_ref) do |is_enabled|
-          if is_enabled
-            gitaly_ref_client.default_branch_name
-          else
-            discover_default_branch
-          end
-        end
+        # NOTE: This feature is intentionally disabled until
+        # https://gitlab.com/gitlab-org/gitaly/issues/179 is resolved
+        # @root_ref ||= Gitlab::GitalyClient.migrate(:root_ref) do |is_enabled|
+        #   if is_enabled
+        #     gitaly_ref_client.default_branch_name
+        #   else
+        @root_ref ||= discover_default_branch
+        #   end
+        # end
       rescue GRPC::BadStatus => e
         raise CommandError.new(e)
       end
@@ -70,13 +72,15 @@ module Gitlab
       # Returns an Array of branch names
       # sorted by name ASC
       def branch_names
-        Gitlab::GitalyClient.migrate(:branch_names) do |is_enabled|
-          if is_enabled
-            gitaly_ref_client.branch_names
-          else
-            branches.map(&:name)
-          end
-        end
+        # Gitlab::GitalyClient.migrate(:branch_names) do |is_enabled|
+        #   NOTE: This feature is intentionally disabled until
+        #   https://gitlab.com/gitlab-org/gitaly/issues/179 is resolved
+        #   if is_enabled
+        #     gitaly_ref_client.branch_names
+        #   else
+        branches.map(&:name)
+        #   end
+        # end
       rescue GRPC::BadStatus => e
         raise CommandError.new(e)
       end
@@ -131,13 +135,15 @@ module Gitlab
 
       # Returns an Array of tag names
       def tag_names
-        Gitlab::GitalyClient.migrate(:tag_names) do |is_enabled|
-          if is_enabled
-            gitaly_ref_client.tag_names
-          else
-            rugged.tags.map { |t| t.name }
-          end
-        end
+        # Gitlab::GitalyClient.migrate(:tag_names) do |is_enabled|
+        #   NOTE: This feature is intentionally disabled until
+        #   https://gitlab.com/gitlab-org/gitaly/issues/179 is resolved
+        #   if is_enabled
+        #     gitaly_ref_client.tag_names
+        #   else
+        rugged.tags.map { |t| t.name }
+        #   end
+        # end
       rescue GRPC::BadStatus => e
         raise CommandError.new(e)
       end
@@ -458,17 +464,19 @@ module Gitlab
 
       # Returns a RefName for a given SHA
       def ref_name_for_sha(ref_path, sha)
-        Gitlab::GitalyClient.migrate(:find_ref_name) do |is_enabled|
-          if is_enabled
-            gitaly_ref_client.find_ref_name(sha, ref_path)
-          else
-            args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
-
-            # Not found -> ["", 0]
-            # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
-            Gitlab::Popen.popen(args, @path).first.split.last
-          end
-        end
+        # NOTE: This feature is intentionally disabled until
+        # https://gitlab.com/gitlab-org/gitaly/issues/180 is resolved
+        # Gitlab::GitalyClient.migrate(:find_ref_name) do |is_enabled|
+        #   if is_enabled
+        #     gitaly_ref_client.find_ref_name(sha, ref_path)
+        #   else
+        args = %W(#{Gitlab.config.git.bin_path} for-each-ref --count=1 #{ref_path} --contains #{sha})
+
+        # Not found -> ["", 0]
+        # Found -> ["b8d95eb4969eefacb0a58f6a28f6803f8070e7b9 commit\trefs/environments/production/77\n", 0]
+        Gitlab::Popen.popen(args, @path).first.split.last
+        #   end
+        # end
       end
 
       # Returns commits collection
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
index f98481c6d3a9d0f20a7919e5162e1dceea63c7bd..6e42d8941fbb73a320ff2454d327e1ad87b0a964 100644
--- a/lib/gitlab/o_auth/user.rb
+++ b/lib/gitlab/o_auth/user.rb
@@ -148,7 +148,7 @@ module Gitlab
 
       def build_new_user
         user_params = user_attributes.merge(extern_uid: auth_hash.uid, provider: auth_hash.provider, skip_confirmation: true)
-        Users::CreateService.new(nil, user_params).build
+        Users::BuildService.new(nil, user_params).execute
       end
 
       def user_attributes
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index a9a48f7188f054963da275b190430befdf6681f0..f41c73154f506ce3695ef981aee04ec0a5264790 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -431,8 +431,7 @@ namespace :gitlab do
 
     def check_repo_base_user_and_group
       gitlab_shell_ssh_user = Gitlab.config.gitlab_shell.ssh_user
-      gitlab_shell_owner_group = Gitlab.config.gitlab_shell.owner_group
-      puts "Repo paths owned by #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group}?"
+      puts "Repo paths owned by #{gitlab_shell_ssh_user}:root, or #{gitlab_shell_ssh_user}:#{Gitlab.config.gitlab_shell.owner_group}?"
 
       Gitlab.config.repositories.storages.each do |name, repository_storage|
         repo_base_path = repository_storage['path']
@@ -443,15 +442,16 @@ namespace :gitlab do
           break
         end
 
-        uid = uid_for(gitlab_shell_ssh_user)
-        gid = gid_for(gitlab_shell_owner_group)
-        if File.stat(repo_base_path).uid == uid && File.stat(repo_base_path).gid == gid
+        user_id = uid_for(gitlab_shell_ssh_user)
+        root_group_id = gid_for('root')
+        group_ids = [root_group_id, gid_for(Gitlab.config.gitlab_shell.owner_group)]
+        if File.stat(repo_base_path).uid == user_id && group_ids.include?(File.stat(repo_base_path).gid)
           puts "yes".color(:green)
         else
           puts "no".color(:red)
-          puts "  User id for #{gitlab_shell_ssh_user}: #{uid}. Groupd id for #{gitlab_shell_owner_group}: #{gid}".color(:blue)
+          puts "  User id for #{gitlab_shell_ssh_user}: #{user_id}. Groupd id for root: #{root_group_id}".color(:blue)
           try_fixing_it(
-            "sudo chown -R #{gitlab_shell_ssh_user}:#{gitlab_shell_owner_group} #{repo_base_path}"
+            "sudo chown -R #{gitlab_shell_ssh_user}:root #{repo_base_path}"
           )
           for_more_information(
             see_installation_guide_section "GitLab Shell"
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index 9f6cfe3957c96572aedddbcac59c305e76a77775..8079c6e416cdd89dc2ed86151dca8f447a5bb02a 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -7,10 +7,10 @@ namespace :gitlab do
         abort %(Please specify the directory where you want to install gitaly:\n  rake "gitlab:gitaly:install[/home/git/gitaly]")
       end
 
-      tag = "v#{Gitlab::GitalyClient.expected_server_version}"
+      version = Gitlab::GitalyClient.expected_server_version
       repo = 'https://gitlab.com/gitlab-org/gitaly.git'
 
-      checkout_or_clone_tag(tag: tag, repo: repo, target_dir: args.dir)
+      checkout_or_clone_version(version: version, repo: repo, target_dir: args.dir)
 
       _, status = Gitlab::Popen.popen(%w[which gmake])
       command = status.zero? ? 'gmake' : 'make'
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index dd2fda54e622922c61d96b0a05b8d9e3954278fa..956870668190c0a685d4e78ee32fe39fbc2cd78b 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -1,19 +1,18 @@
 namespace :gitlab do
   namespace :shell do
     desc "GitLab | Install or upgrade gitlab-shell"
-    task :install, [:tag, :repo] => :environment do |t, args|
+    task :install, [:repo] => :environment do |t, args|
       warn_user_is_not_gitlab
 
       default_version = Gitlab::Shell.version_required
-      default_version_tag = "v#{default_version}"
-      args.with_defaults(tag: default_version_tag, repo: 'https://gitlab.com/gitlab-org/gitlab-shell.git')
+      args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitlab-shell.git')
 
       gitlab_url = Gitlab.config.gitlab.url
       # gitlab-shell requires a / at the end of the url
       gitlab_url += '/' unless gitlab_url.end_with?('/')
       target_dir = Gitlab.config.gitlab_shell.path
 
-      checkout_or_clone_tag(tag: default_version_tag, repo: args.repo, target_dir: target_dir)
+      checkout_or_clone_version(version: default_version, repo: args.repo, target_dir: target_dir)
 
       # Make sure we're on the right tag
       Dir.chdir(target_dir) do
diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb
index cdba2262bc27aa642410101dc6be89424f1e1cd1..e3c9d3b491c8e0460491bb7bad89eaf32d350d1a 100644
--- a/lib/tasks/gitlab/task_helpers.rb
+++ b/lib/tasks/gitlab/task_helpers.rb
@@ -147,41 +147,30 @@ module Gitlab
       Rails.env.test? ? Rails.root.join('tmp/tests') : Gitlab.config.gitlab.user_home
     end
 
-    def checkout_or_clone_tag(tag:, repo:, target_dir:)
-      if Dir.exist?(target_dir)
-        checkout_tag(tag, target_dir)
-      else
-        clone_repo(repo, target_dir)
-      end
+    def checkout_or_clone_version(version:, repo:, target_dir:)
+      version =
+        if version.starts_with?("=")
+          version.sub(/\A=/, '') # tag or branch
+        else
+          "v#{version}" # tag
+        end
 
-      reset_to_tag(tag, target_dir)
+      clone_repo(repo, target_dir) unless Dir.exist?(target_dir)
+      checkout_version(version, target_dir)
+      reset_to_version(version, target_dir)
     end
 
     def clone_repo(repo, target_dir)
       run_command!(%W[#{Gitlab.config.git.bin_path} clone -- #{repo} #{target_dir}])
     end
 
-    def checkout_tag(tag, target_dir)
-      run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch --tags --quiet])
-      run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} checkout --quiet #{tag}])
+    def checkout_version(version, target_dir)
+      run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch --quiet])
+      run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} checkout --quiet #{version}])
     end
 
-    def reset_to_tag(tag_wanted, target_dir)
-      tag =
-        begin
-          # First try to checkout without fetching
-          # to avoid stalling tests if the Internet is down.
-          run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- #{tag_wanted}])
-        rescue Gitlab::TaskFailedError
-          run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} fetch origin])
-          run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} describe -- origin/#{tag_wanted}])
-        end
-
-      if tag
-        run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{tag.strip}])
-      else
-        raise Gitlab::TaskFailedError
-      end
+    def reset_to_version(version, target_dir)
+      run_command!(%W[#{Gitlab.config.git.bin_path} -C #{target_dir} reset --hard #{version}])
     end
   end
 end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
index dbdfb335a5cf7493daacea166283f0adc6e6a347..cb2adc81c9dea13a0b3734a2ee81dfbd4fd81582 100644
--- a/lib/tasks/gitlab/update_templates.rake
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -44,7 +44,7 @@ namespace :gitlab do
     ),
     Template.new(
       "https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
-      /(\.{1,2}|LICENSE|Pages|autodeploy|\.gitlab-ci.yml)\z/
+      /(\.{1,2}|LICENSE|CONTRIBUTING.md|Pages|autodeploy|\.gitlab-ci.yml)\z/
     )
   ].freeze
 
diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake
index baea94bf8ca0456fb9e26b31cf9eed11ffb41858..a00b02188cfa26e2bd12c2b43b46b4f5d6f9d585 100644
--- a/lib/tasks/gitlab/workhorse.rake
+++ b/lib/tasks/gitlab/workhorse.rake
@@ -7,10 +7,10 @@ namespace :gitlab do
         abort %(Please specify the directory where you want to install gitlab-workhorse:\n  rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]")
       end
 
-      tag = "v#{Gitlab::Workhorse.version}"
+      version = Gitlab::Workhorse.version
       repo = 'https://gitlab.com/gitlab-org/gitlab-workhorse.git'
 
-      checkout_or_clone_tag(tag: tag, repo: repo, target_dir: args.dir)
+      checkout_or_clone_version(version: version, repo: repo, target_dir: args.dir)
 
       _, status = Gitlab::Popen.popen(%w[which gmake])
       command = status.zero? ? 'gmake' : 'make'
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index 45db7a92fa42cafe3a57b2bb60429f3843e93d4a..7ce4e9009f568c82910337b9df00f7cad7cfcab3 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -11,7 +11,7 @@ module QA
         end
 
         def go_to_admin_area
-          within_user_menu { click_link 'Admin Area' }
+          within_user_menu { click_link 'Admin area' }
         end
 
         def sign_out
diff --git a/spec/features/discussion_comments/commit_spec.rb b/spec/features/discussion_comments/commit_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..96e0b78f6b9446420083a5cdf2348c526cdbf34d
--- /dev/null
+++ b/spec/features/discussion_comments/commit_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe 'Discussion Comments Merge Request', :feature, :js do
+  include RepoHelpers
+
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+  let(:merge_request) { create(:merge_request, source_project: project) }
+
+  before do
+    project.add_master(user)
+    login_as(user)
+
+    visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
+  end
+
+  it_behaves_like 'discussion comments', 'commit'
+end
diff --git a/spec/features/discussion_comments/issue_spec.rb b/spec/features/discussion_comments/issue_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ccc9efccd1855fa12f6f80ea1a742c52a4341a0b
--- /dev/null
+++ b/spec/features/discussion_comments/issue_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe 'Discussion Comments Issue', :feature, :js do
+  let(:user) { create(:user) }
+  let(:project) { create(:empty_project) }
+  let(:issue) { create(:issue, project: project) }
+
+  before do
+    project.add_master(user)
+    login_as(user)
+
+    visit namespace_project_issue_path(project.namespace, project, issue)
+  end
+
+  it_behaves_like 'discussion comments', 'issue'
+end
diff --git a/spec/features/discussion_comments/merge_request_spec.rb b/spec/features/discussion_comments/merge_request_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f99ebeb9cd927ad0bca7f2050a960028483f1f1b
--- /dev/null
+++ b/spec/features/discussion_comments/merge_request_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe 'Discussion Comments Merge Request', :feature, :js do
+  let(:user) { create(:user) }
+  let(:project) { create(:project) }
+  let(:merge_request) { create(:merge_request, source_project: project) }
+
+  before do
+    project.add_master(user)
+    login_as(user)
+
+    visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+  end
+
+  it_behaves_like 'discussion comments', 'merge request'
+end
diff --git a/spec/features/discussion_comments/snippets_spec.rb b/spec/features/discussion_comments/snippets_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..19a306511b2efe901afc642f2c9ae916a523cd26
--- /dev/null
+++ b/spec/features/discussion_comments/snippets_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe 'Discussion Comments Issue', :feature, :js do
+  let(:user) { create(:user) }
+  let(:project) { create(:empty_project) }
+  let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
+
+  before do
+    project.add_master(user)
+    login_as(user)
+
+    visit namespace_project_snippet_path(project.namespace, project, snippet)
+  end
+
+  it_behaves_like 'discussion comments', 'snippet'
+end
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index 88d28b649a4ba0e61be205b490d34321ea8a8ddd..0e23c3a8849d938824e2c350fa99413621027212 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -198,6 +198,8 @@ feature 'Diff notes resolve', feature: true, js: true do
       it 'does not mark discussion as resolved when resolving single note' do
         page.first '.diff-content .note' do
           first('.line-resolve-btn').click
+
+          expect(page).to have_selector('.note-action-button .loading')
           expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
         end
 
diff --git a/spec/fixtures/trace/ansi-sequence-and-unicode b/spec/fixtures/trace/ansi-sequence-and-unicode
new file mode 100644
index 0000000000000000000000000000000000000000..5d2466f0d0f4ed17b6cc1834728458e89b66f4c5
--- /dev/null
+++ b/spec/fixtures/trace/ansi-sequence-and-unicode
@@ -0,0 +1,5 @@
+.
+..
+😺
+ヾ(´༎ຶД༎ຶ`)ノ
+許功蓋
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index fddeaaf504d6dd074f66cad4b0e041352d61024a..47d904b865bf43550bc83a87df524fa93f3b3e34 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -7,6 +7,7 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
   let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
   let(:project) { create(:project, namespace: namespace, path: 'merge-requests-project') }
   let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') }
+  let(:merged_merge_request) { create(:merge_request, :merged, source_project: project, target_project: project) }
   let(:pipeline) do
     create(
       :ci_pipeline,
@@ -32,6 +33,12 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
     render_merge_request(example.description, merge_request)
   end
 
+  it 'merge_requests/merged_merge_request.html.raw' do |example|
+    allow_any_instance_of(MergeRequest).to receive(:source_branch_exists?).and_return(true)
+    allow_any_instance_of(MergeRequest).to receive(:can_remove_source_branch?).and_return(true)
+    render_merge_request(example.description, merged_merge_request)
+  end
+
   private
 
   def render_merge_request(fixture_file_name, merge_request)
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index aabc8bea12f48f7001f7dec9143edb443b342e80..9a2570ef7e93cc5d4248961b8639ec473f48c67a 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,18 +1,17 @@
-/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
+/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
 import Issue from '~/issue';
 
 require('~/lib/utils/text_utility');
 
 describe('Issue', function() {
-  var INVALID_URL = 'http://goesnowhere.nothing/whereami';
-  var $boxClosed, $boxOpen, $btnClose, $btnReopen;
+  let $boxClosed, $boxOpen, $btnClose, $btnReopen;
 
   preloadFixtures('issues/closed-issue.html.raw');
   preloadFixtures('issues/issue-with-task-list.html.raw');
   preloadFixtures('issues/open-issue.html.raw');
 
   function expectErrorMessage() {
-    var $flashMessage = $('div.flash-alert');
+    const $flashMessage = $('div.flash-alert');
     expect($flashMessage).toExist();
     expect($flashMessage).toBeVisible();
     expect($flashMessage).toHaveText('Unable to update this issue at this time.');
@@ -26,10 +25,28 @@ describe('Issue', function() {
     expectVisibility($btnReopen, !isIssueOpen);
   }
 
-  function expectPendingRequest(req, $triggeredButton) {
-    expect(req.type).toBe('PUT');
-    expect(req.url).toBe($triggeredButton.attr('href'));
-    expect($triggeredButton).toHaveProp('disabled', true);
+  function expectNewBranchButtonState(isPending, canCreate) {
+    if (Issue.$btnNewBranch.length === 0) {
+      return;
+    }
+
+    const $available = Issue.$btnNewBranch.find('.available');
+    expect($available).toHaveText('New branch');
+
+    if (!isPending && canCreate) {
+      expect($available).toBeVisible();
+    } else {
+      expect($available).toBeHidden();
+    }
+
+    const $unavailable = Issue.$btnNewBranch.find('.unavailable');
+    expect($unavailable).toHaveText('New branch unavailable');
+
+    if (!isPending && !canCreate) {
+      expect($unavailable).toBeVisible();
+    } else {
+      expect($unavailable).toBeHidden();
+    }
   }
 
   function expectVisibility($element, shouldBeVisible) {
@@ -81,100 +98,107 @@ describe('Issue', function() {
     });
   });
 
-  describe('close issue', function() {
-    beforeEach(function() {
-      loadFixtures('issues/open-issue.html.raw');
-      findElements();
-      this.issue = new Issue();
-
-      expectIssueState(true);
-    });
+  [true, false].forEach((isIssueInitiallyOpen) => {
+    describe(`with ${isIssueInitiallyOpen ? 'open' : 'closed'} issue`, function() {
+      const action = isIssueInitiallyOpen ? 'close' : 'reopen';
+
+      function ajaxSpy(req) {
+        if (req.url === this.$triggeredButton.attr('href')) {
+          expect(req.type).toBe('PUT');
+          expect(this.$triggeredButton).toHaveProp('disabled', true);
+          expectNewBranchButtonState(true, false);
+          return this.issueStateDeferred;
+        } else if (req.url === Issue.$btnNewBranch.data('path')) {
+          expect(req.type).toBe('get');
+          expectNewBranchButtonState(true, false);
+          return this.canCreateBranchDeferred;
+        }
+
+        expect(req.url).toBe('unexpected');
+        return null;
+      }
+
+      beforeEach(function() {
+        if (isIssueInitiallyOpen) {
+          loadFixtures('issues/open-issue.html.raw');
+        } else {
+          loadFixtures('issues/closed-issue.html.raw');
+        }
+
+        findElements();
+        this.issue = new Issue();
+        expectIssueState(isIssueInitiallyOpen);
+        this.$triggeredButton = isIssueInitiallyOpen ? $btnClose : $btnReopen;
+
+        this.$projectIssuesCounter = $('.issue_counter');
+        this.$projectIssuesCounter.text('1,001');
+
+        this.issueStateDeferred = new jQuery.Deferred();
+        this.canCreateBranchDeferred = new jQuery.Deferred();
+
+        spyOn(jQuery, 'ajax').and.callFake(ajaxSpy.bind(this));
+      });
 
-    it('closes an issue', function() {
-      spyOn(jQuery, 'ajax').and.callFake(function(req) {
-        expectPendingRequest(req, $btnClose);
-        req.success({
+      it(`${action}s the issue`, function() {
+        this.$triggeredButton.trigger('click');
+        this.issueStateDeferred.resolve({
           id: 34
         });
-      });
-
-      $btnClose.trigger('click');
+        this.canCreateBranchDeferred.resolve({
+          can_create_branch: !isIssueInitiallyOpen
+        });
 
-      expectIssueState(false);
-      expect($btnClose).toHaveProp('disabled', false);
-      expect($('.issue_counter')).toHaveText(0);
-    });
+        expectIssueState(!isIssueInitiallyOpen);
+        expect(this.$triggeredButton).toHaveProp('disabled', false);
+        expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
+        expectNewBranchButtonState(false, !isIssueInitiallyOpen);
+      });
 
-    it('fails to close an issue with success:false', function() {
-      spyOn(jQuery, 'ajax').and.callFake(function(req) {
-        expectPendingRequest(req, $btnClose);
-        req.success({
+      it(`fails to ${action} the issue if saved:false`, function() {
+        this.$triggeredButton.trigger('click');
+        this.issueStateDeferred.resolve({
           saved: false
         });
-      });
-
-      $btnClose.attr('href', INVALID_URL);
-      $btnClose.trigger('click');
-
-      expectIssueState(true);
-      expect($btnClose).toHaveProp('disabled', false);
-      expectErrorMessage();
-      expect($('.issue_counter')).toHaveText(1);
-    });
+        this.canCreateBranchDeferred.resolve({
+          can_create_branch: isIssueInitiallyOpen
+        });
 
-    it('fails to closes an issue with HTTP error', function() {
-      spyOn(jQuery, 'ajax').and.callFake(function(req) {
-        expectPendingRequest(req, $btnClose);
-        req.error();
+        expectIssueState(isIssueInitiallyOpen);
+        expect(this.$triggeredButton).toHaveProp('disabled', false);
+        expectErrorMessage();
+        expect(this.$projectIssuesCounter.text()).toBe('1,001');
+        expectNewBranchButtonState(false, isIssueInitiallyOpen);
       });
 
-      $btnClose.attr('href', INVALID_URL);
-      $btnClose.trigger('click');
-
-      expectIssueState(true);
-      expect($btnClose).toHaveProp('disabled', true);
-      expectErrorMessage();
-      expect($('.issue_counter')).toHaveText(1);
-    });
-
-    it('updates counter', () => {
-      spyOn(jQuery, 'ajax').and.callFake(function(req) {
-        expectPendingRequest(req, $btnClose);
-        req.success({
-          id: 34
+      it(`fails to ${action} the issue if HTTP error occurs`, function() {
+        this.$triggeredButton.trigger('click');
+        this.issueStateDeferred.reject();
+        this.canCreateBranchDeferred.resolve({
+          can_create_branch: isIssueInitiallyOpen
         });
-      });
 
-      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');
-    });
-  });
+        expectIssueState(isIssueInitiallyOpen);
+        expect(this.$triggeredButton).toHaveProp('disabled', true);
+        expectErrorMessage();
+        expect(this.$projectIssuesCounter.text()).toBe('1,001');
+        expectNewBranchButtonState(false, isIssueInitiallyOpen);
+      });
 
-  describe('reopen issue', function() {
-    beforeEach(function() {
-      loadFixtures('issues/closed-issue.html.raw');
-      findElements();
-      this.issue = new Issue();
+      it('disables the new branch button if Ajax call fails', function() {
+        this.$triggeredButton.trigger('click');
+        this.issueStateDeferred.reject();
+        this.canCreateBranchDeferred.reject();
 
-      expectIssueState(false);
-    });
-
-    it('reopens an issue', function() {
-      spyOn(jQuery, 'ajax').and.callFake(function(req) {
-        expectPendingRequest(req, $btnReopen);
-        req.success({
-          id: 34
-        });
+        expectNewBranchButtonState(false, false);
       });
 
-      $btnReopen.trigger('click');
+      it('does not trigger Ajax call if new branch button is missing', function() {
+        Issue.$btnNewBranch = $();
+        this.canCreateBranchDeferred = null;
 
-      expectIssueState(true);
-      expect($btnReopen).toHaveProp('disabled', false);
-      expect($('.issue_counter')).toHaveText(1);
+        this.$triggeredButton.trigger('click');
+        this.issueStateDeferred.reject();
+      });
     });
   });
 });
diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js
index e3429c2a1cba1aaa2622c6ab75e55b099b706dc5..918b6d32c4315460970bd58891f8dc6284b56746 100644
--- a/spec/javascripts/lib/utils/poll_spec.js
+++ b/spec/javascripts/lib/utils/poll_spec.js
@@ -4,6 +4,20 @@ import Poll from '~/lib/utils/poll';
 
 Vue.use(VueResource);
 
+const waitForAllCallsToFinish = (service, waitForCount, successCallback) => {
+  const timer = () => {
+    setTimeout(() => {
+      if (service.fetch.calls.count() === waitForCount) {
+        successCallback();
+      } else {
+        timer();
+      }
+    }, 5);
+  };
+
+  timer();
+};
+
 class ServiceMock {
   constructor(endpoint) {
     this.service = Vue.resource(endpoint);
@@ -16,6 +30,7 @@ class ServiceMock {
 
 describe('Poll', () => {
   let callbacks;
+  let service;
 
   beforeEach(() => {
     callbacks = {
@@ -23,8 +38,11 @@ describe('Poll', () => {
       error: () => {},
     };
 
+    service = new ServiceMock('endpoint');
+
     spyOn(callbacks, 'success');
     spyOn(callbacks, 'error');
+    spyOn(service, 'fetch').and.callThrough();
   });
 
   it('calls the success callback when no header for interval is provided', (done) => {
@@ -35,19 +53,20 @@ describe('Poll', () => {
     Vue.http.interceptors.push(successInterceptor);
 
     new Poll({
-      resource: new ServiceMock('endpoint'),
+      resource: service,
       method: 'fetch',
       successCallback: callbacks.success,
       errorCallback: callbacks.error,
     }).makeRequest();
 
-    setTimeout(() => {
+    waitForAllCallsToFinish(service, 1, () => {
       expect(callbacks.success).toHaveBeenCalled();
       expect(callbacks.error).not.toHaveBeenCalled();
+
+      Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor);
+
       done();
     }, 0);
-
-    Vue.http.interceptors = _.without(Vue.http.interceptors, successInterceptor);
   });
 
   it('calls the error callback whe the http request returns an error', (done) => {
@@ -58,19 +77,19 @@ describe('Poll', () => {
     Vue.http.interceptors.push(errorInterceptor);
 
     new Poll({
-      resource: new ServiceMock('endpoint'),
+      resource: service,
       method: 'fetch',
       successCallback: callbacks.success,
       errorCallback: callbacks.error,
     }).makeRequest();
 
-    setTimeout(() => {
+    waitForAllCallsToFinish(service, 1, () => {
       expect(callbacks.success).not.toHaveBeenCalled();
       expect(callbacks.error).toHaveBeenCalled();
-      done();
-    }, 0);
+      Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor);
 
-    Vue.http.interceptors = _.without(Vue.http.interceptors, errorInterceptor);
+      done();
+    });
   });
 
   it('should call the success callback when the interval header is -1', (done) => {
@@ -81,7 +100,7 @@ describe('Poll', () => {
     Vue.http.interceptors.push(intervalInterceptor);
 
     new Poll({
-      resource: new ServiceMock('endpoint'),
+      resource: service,
       method: 'fetch',
       successCallback: callbacks.success,
       errorCallback: callbacks.error,
@@ -90,10 +109,11 @@ describe('Poll', () => {
     setTimeout(() => {
       expect(callbacks.success).toHaveBeenCalled();
       expect(callbacks.error).not.toHaveBeenCalled();
+
+      Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor);
+
       done();
     }, 0);
-
-    Vue.http.interceptors = _.without(Vue.http.interceptors, intervalInterceptor);
   });
 
   it('starts polling when http status is 200 and interval header is provided', (done) => {
@@ -103,26 +123,28 @@ describe('Poll', () => {
 
     Vue.http.interceptors.push(pollInterceptor);
 
-    const service = new ServiceMock('endpoint');
-    spyOn(service, 'fetch').and.callThrough();
-
-    new Poll({
+    const Polling = new Poll({
       resource: service,
       method: 'fetch',
       data: { page: 1 },
       successCallback: callbacks.success,
       errorCallback: callbacks.error,
-    }).makeRequest();
+    });
+
+    Polling.makeRequest();
+
+    waitForAllCallsToFinish(service, 2, () => {
+      Polling.stop();
 
-    setTimeout(() => {
       expect(service.fetch.calls.count()).toEqual(2);
       expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
       expect(callbacks.success).toHaveBeenCalled();
       expect(callbacks.error).not.toHaveBeenCalled();
-      done();
-    }, 5);
 
-    Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+      Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+
+      done();
+    });
   });
 
   describe('stop', () => {
@@ -133,9 +155,6 @@ describe('Poll', () => {
 
       Vue.http.interceptors.push(pollInterceptor);
 
-      const service = new ServiceMock('endpoint');
-      spyOn(service, 'fetch').and.callThrough();
-
       const Polling = new Poll({
         resource: service,
         method: 'fetch',
@@ -150,14 +169,15 @@ describe('Poll', () => {
 
       Polling.makeRequest();
 
-      setTimeout(() => {
+      waitForAllCallsToFinish(service, 1, () => {
         expect(service.fetch.calls.count()).toEqual(1);
         expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
         expect(Polling.stop).toHaveBeenCalled();
-        done();
-      }, 100);
 
-      Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+        Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+
+        done();
+      });
     });
   });
 
@@ -169,10 +189,6 @@ describe('Poll', () => {
 
       Vue.http.interceptors.push(pollInterceptor);
 
-      const service = new ServiceMock('endpoint');
-
-      spyOn(service, 'fetch').and.callThrough();
-
       const Polling = new Poll({
         resource: service,
         method: 'fetch',
@@ -187,17 +203,22 @@ describe('Poll', () => {
       });
 
       spyOn(Polling, 'stop').and.callThrough();
+      spyOn(Polling, 'restart').and.callThrough();
 
       Polling.makeRequest();
 
-      setTimeout(() => {
+      waitForAllCallsToFinish(service, 2, () => {
+        Polling.stop();
+
         expect(service.fetch.calls.count()).toEqual(2);
         expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
         expect(Polling.stop).toHaveBeenCalled();
-        done();
-      }, 10);
+        expect(Polling.restart).toHaveBeenCalled();
 
-      Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+        Vue.http.interceptors = _.without(Vue.http.interceptors, pollInterceptor);
+
+        done();
+      });
     });
   });
 });
diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js
index 4200e9431219cef3dcc030e73a41852b5e092fcb..daef9b93fa57084a8a04bc9ab3a9f3ba20fe5872 100644
--- a/spec/javascripts/lib/utils/text_utility_spec.js
+++ b/spec/javascripts/lib/utils/text_utility_spec.js
@@ -1,110 +1,108 @@
 require('~/lib/utils/text_utility');
 
-(() => {
-  describe('text_utility', () => {
-    describe('gl.text.getTextWidth', () => {
-      it('returns zero width when no text is passed', () => {
-        expect(gl.text.getTextWidth('')).toBe(0);
-      });
+describe('text_utility', () => {
+  describe('gl.text.getTextWidth', () => {
+    it('returns zero width when no text is passed', () => {
+      expect(gl.text.getTextWidth('')).toBe(0);
+    });
 
-      it('returns zero width when no text is passed and font is passed', () => {
-        expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
-      });
+    it('returns zero width when no text is passed and font is passed', () => {
+      expect(gl.text.getTextWidth('', '100px sans-serif')).toBe(0);
+    });
 
-      it('returns width when text is passed', () => {
-        expect(gl.text.getTextWidth('foo') > 0).toBe(true);
-      });
+    it('returns width when text is passed', () => {
+      expect(gl.text.getTextWidth('foo') > 0).toBe(true);
+    });
 
-      it('returns bigger width when font is larger', () => {
-        const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
-        const regular = gl.text.getTextWidth('foo', '10px sans-serif');
-        expect(largeFont > regular).toBe(true);
-      });
+    it('returns bigger width when font is larger', () => {
+      const largeFont = gl.text.getTextWidth('foo', '100px sans-serif');
+      const regular = gl.text.getTextWidth('foo', '10px sans-serif');
+      expect(largeFont > regular).toBe(true);
     });
+  });
 
-    describe('gl.text.pluralize', () => {
-      it('returns pluralized', () => {
-        expect(gl.text.pluralize('test', 2)).toBe('tests');
-      });
+  describe('gl.text.pluralize', () => {
+    it('returns pluralized', () => {
+      expect(gl.text.pluralize('test', 2)).toBe('tests');
+    });
 
-      it('returns pluralized when count is 0', () => {
-        expect(gl.text.pluralize('test', 0)).toBe('tests');
-      });
+    it('returns pluralized when count is 0', () => {
+      expect(gl.text.pluralize('test', 0)).toBe('tests');
+    });
 
-      it('does not return pluralized', () => {
-        expect(gl.text.pluralize('test', 1)).toBe('test');
-      });
+    it('does not return pluralized', () => {
+      expect(gl.text.pluralize('test', 1)).toBe('test');
     });
+  });
 
-    describe('gl.text.highCountTrim', () => {
-      it('returns 99+ for count >= 100', () => {
-        expect(gl.text.highCountTrim(105)).toBe('99+');
-        expect(gl.text.highCountTrim(100)).toBe('99+');
-      });
+  describe('gl.text.highCountTrim', () => {
+    it('returns 99+ for count >= 100', () => {
+      expect(gl.text.highCountTrim(105)).toBe('99+');
+      expect(gl.text.highCountTrim(100)).toBe('99+');
+    });
 
-      it('returns exact number for count < 100', () => {
-        expect(gl.text.highCountTrim(45)).toBe(45);
-      });
+    it('returns exact number for count < 100', () => {
+      expect(gl.text.highCountTrim(45)).toBe(45);
     });
+  });
 
-    describe('gl.text.insertText', () => {
-      let textArea;
+  describe('gl.text.insertText', () => {
+    let textArea;
 
-      beforeAll(() => {
-        textArea = document.createElement('textarea');
-        document.querySelector('body').appendChild(textArea);
-      });
+    beforeAll(() => {
+      textArea = document.createElement('textarea');
+      document.querySelector('body').appendChild(textArea);
+    });
 
-      afterAll(() => {
-        textArea.parentNode.removeChild(textArea);
-      });
+    afterAll(() => {
+      textArea.parentNode.removeChild(textArea);
+    });
 
-      describe('without selection', () => {
-        it('inserts the tag on an empty line', () => {
-          const initialValue = '';
+    describe('without selection', () => {
+      it('inserts the tag on an empty line', () => {
+        const initialValue = '';
 
-          textArea.value = initialValue;
-          textArea.selectionStart = 0;
-          textArea.selectionEnd = 0;
+        textArea.value = initialValue;
+        textArea.selectionStart = 0;
+        textArea.selectionEnd = 0;
 
-          gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+        gl.text.insertText(textArea, textArea.value, '*', null, '', false);
 
-          expect(textArea.value).toEqual(`${initialValue}* `);
-        });
+        expect(textArea.value).toEqual(`${initialValue}* `);
+      });
 
-        it('inserts the tag on a new line if the current one is not empty', () => {
-          const initialValue = 'some text';
+      it('inserts the tag on a new line if the current one is not empty', () => {
+        const initialValue = 'some text';
 
-          textArea.value = initialValue;
-          textArea.setSelectionRange(initialValue.length, initialValue.length);
+        textArea.value = initialValue;
+        textArea.setSelectionRange(initialValue.length, initialValue.length);
 
-          gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+        gl.text.insertText(textArea, textArea.value, '*', null, '', false);
 
-          expect(textArea.value).toEqual(`${initialValue}\n* `);
-        });
+        expect(textArea.value).toEqual(`${initialValue}\n* `);
+      });
 
-        it('inserts the tag on the same line if the current line only contains spaces', () => {
-          const initialValue = '  ';
+      it('inserts the tag on the same line if the current line only contains spaces', () => {
+        const initialValue = '  ';
 
-          textArea.value = initialValue;
-          textArea.setSelectionRange(initialValue.length, initialValue.length);
+        textArea.value = initialValue;
+        textArea.setSelectionRange(initialValue.length, initialValue.length);
 
-          gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+        gl.text.insertText(textArea, textArea.value, '*', null, '', false);
 
-          expect(textArea.value).toEqual(`${initialValue}* `);
-        });
+        expect(textArea.value).toEqual(`${initialValue}* `);
+      });
 
-        it('inserts the tag on the same line if the current line only contains tabs', () => {
-          const initialValue = '\t\t\t';
+      it('inserts the tag on the same line if the current line only contains tabs', () => {
+        const initialValue = '\t\t\t';
 
-          textArea.value = initialValue;
-          textArea.setSelectionRange(initialValue.length, initialValue.length);
+        textArea.value = initialValue;
+        textArea.setSelectionRange(initialValue.length, initialValue.length);
 
-          gl.text.insertText(textArea, textArea.value, '*', null, '', false);
+        gl.text.insertText(textArea, textArea.value, '*', null, '', false);
 
-          expect(textArea.value).toEqual(`${initialValue}* `);
-        });
+        expect(textArea.value).toEqual(`${initialValue}* `);
       });
     });
   });
-})();
+});
diff --git a/spec/javascripts/merged_buttons_spec.js b/spec/javascripts/merged_buttons_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b5c5e60dd971a358a9e531f0d9eb6364055d28a0
--- /dev/null
+++ b/spec/javascripts/merged_buttons_spec.js
@@ -0,0 +1,44 @@
+/* global MergedButtons */
+
+import '~/merged_buttons';
+
+describe('MergedButtons', () => {
+  const fixturesPath = 'merge_requests/merged_merge_request.html.raw';
+  preloadFixtures(fixturesPath);
+
+  beforeEach(() => {
+    loadFixtures(fixturesPath);
+    this.mergedButtons = new MergedButtons();
+    this.$removeBranchWidget = $('.remove_source_branch_widget:not(.failed)');
+    this.$removeBranchProgress = $('.remove_source_branch_in_progress');
+    this.$removeBranchFailed = $('.remove_source_branch_widget.failed');
+    this.$removeBranchButton = $('.remove_source_branch');
+  });
+
+  describe('removeSourceBranch', () => {
+    it('shows loader', () => {
+      $('.remove_source_branch').trigger('click');
+      expect(this.$removeBranchProgress).toBeVisible();
+      expect(this.$removeBranchWidget).not.toBeVisible();
+    });
+  });
+
+  describe('removeBranchSuccess', () => {
+    it('refreshes page when branch removed', () => {
+      spyOn(gl.utils, 'refreshCurrentPage').and.stub();
+      const response = { status: 200 };
+      this.$removeBranchButton.trigger('ajax:success', response, 'xhr');
+      expect(gl.utils.refreshCurrentPage).toHaveBeenCalled();
+    });
+  });
+
+  describe('removeBranchError', () => {
+    it('shows error message', () => {
+      const response = { status: 500 };
+      this.$removeBranchButton.trigger('ajax:error', response, 'xhr');
+      expect(this.$removeBranchFailed).toBeVisible();
+      expect(this.$removeBranchProgress).not.toBeVisible();
+      expect(this.$removeBranchWidget).not.toBeVisible();
+    });
+  });
+});
diff --git a/spec/lib/banzai/filter/issuable_state_filter_spec.rb b/spec/lib/banzai/filter/issuable_state_filter_spec.rb
index 603b79a323c5591dbc301b723c79ecf47a165a4f..5cb98163746ddcd842e5b72d282c10f0dc1c4597 100644
--- a/spec/lib/banzai/filter/issuable_state_filter_spec.rb
+++ b/spec/lib/banzai/filter/issuable_state_filter_spec.rb
@@ -6,8 +6,8 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
 
   let(:user) { create(:user) }
 
-  def create_link(data)
-    link_to('text', '', class: 'gfm has-tooltip', data: data)
+  def create_link(text, data)
+    link_to(text, '', class: 'gfm has-tooltip', data: data)
   end
 
   it 'ignores non-GFM links' do
@@ -19,16 +19,37 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
 
   it 'ignores non-issuable links' do
     project = create(:empty_project, :public)
-    link = create_link(project: project, reference_type: 'issue')
+    link = create_link('text', project: project, reference_type: 'issue')
     doc = filter(link, current_user: user)
 
     expect(doc.css('a').last.text).to eq('text')
   end
 
+  it 'ignores issuable links with empty content' do
+    issue = create(:issue, :closed)
+    link = create_link('', issue: issue.id, reference_type: 'issue')
+    doc = filter(link, current_user: user)
+
+    expect(doc.css('a').last.text).to eq('')
+  end
+
+  it 'adds text with standard formatting' do
+    issue = create(:issue, :closed)
+    link = create_link(
+      'something <strong>else</strong>'.html_safe,
+      issue: issue.id,
+      reference_type: 'issue'
+    )
+    doc = filter(link, current_user: user)
+
+    expect(doc.css('a').last.inner_html).
+      to eq('something <strong>else</strong> [closed]')
+  end
+
   context 'for issue references' do
     it 'ignores open issue references' do
       issue = create(:issue)
-      link = create_link(issue: issue.id, reference_type: 'issue')
+      link = create_link('text', issue: issue.id, reference_type: 'issue')
       doc = filter(link, current_user: user)
 
       expect(doc.css('a').last.text).to eq('text')
@@ -36,7 +57,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
 
     it 'ignores reopened issue references' do
       reopened_issue = create(:issue, :reopened)
-      link = create_link(issue: reopened_issue.id, reference_type: 'issue')
+      link = create_link('text', issue: reopened_issue.id, reference_type: 'issue')
       doc = filter(link, current_user: user)
 
       expect(doc.css('a').last.text).to eq('text')
@@ -44,7 +65,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
 
     it 'appends [closed] to closed issue references' do
       closed_issue = create(:issue, :closed)
-      link = create_link(issue: closed_issue.id, reference_type: 'issue')
+      link = create_link('text', issue: closed_issue.id, reference_type: 'issue')
       doc = filter(link, current_user: user)
 
       expect(doc.css('a').last.text).to eq('text [closed]')
@@ -54,7 +75,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
   context 'for merge request references' do
     it 'ignores open merge request references' do
       mr = create(:merge_request)
-      link = create_link(merge_request: mr.id, reference_type: 'merge_request')
+      link = create_link('text', merge_request: mr.id, reference_type: 'merge_request')
       doc = filter(link, current_user: user)
 
       expect(doc.css('a').last.text).to eq('text')
@@ -62,7 +83,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
 
     it 'ignores reopened merge request references' do
       mr = create(:merge_request, :reopened)
-      link = create_link(merge_request: mr.id, reference_type: 'merge_request')
+      link = create_link('text', merge_request: mr.id, reference_type: 'merge_request')
       doc = filter(link, current_user: user)
 
       expect(doc.css('a').last.text).to eq('text')
@@ -70,7 +91,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
 
     it 'ignores locked merge request references' do
       mr = create(:merge_request, :locked)
-      link = create_link(merge_request: mr.id, reference_type: 'merge_request')
+      link = create_link('text', merge_request: mr.id, reference_type: 'merge_request')
       doc = filter(link, current_user: user)
 
       expect(doc.css('a').last.text).to eq('text')
@@ -78,7 +99,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
 
     it 'appends [closed] to closed merge request references' do
       mr = create(:merge_request, :closed)
-      link = create_link(merge_request: mr.id, reference_type: 'merge_request')
+      link = create_link('text', merge_request: mr.id, reference_type: 'merge_request')
       doc = filter(link, current_user: user)
 
       expect(doc.css('a').last.text).to eq('text [closed]')
@@ -86,7 +107,7 @@ describe Banzai::Filter::IssuableStateFilter, lib: true do
 
     it 'appends [merged] to merged merge request references' do
       mr = create(:merge_request, :merged)
-      link = create_link(merge_request: mr.id, reference_type: 'merge_request')
+      link = create_link('text', merge_request: mr.id, reference_type: 'merge_request')
       doc = filter(link, current_user: user)
 
       expect(doc.css('a').last.text).to eq('text [merged]')
diff --git a/spec/lib/container_registry/path_spec.rb b/spec/lib/container_registry/path_spec.rb
index b9c4572c26926f0f63c815a21de5d7bc86494182..f3b3a9a715f1816ae8dd538237ee4347f43360ee 100644
--- a/spec/lib/container_registry/path_spec.rb
+++ b/spec/lib/container_registry/path_spec.rb
@@ -33,10 +33,20 @@ describe ContainerRegistry::Path do
   end
 
   describe '#to_s' do
-    let(:path) { 'some/image' }
+    context 'when path does not have uppercase characters' do
+      let(:path) { 'some/image' }
 
-    it 'return a string with a repository path' do
-      expect(subject.to_s).to eq path
+      it 'return a string with a repository path' do
+        expect(subject.to_s).to eq 'some/image'
+      end
+    end
+
+    context 'when path has uppercase characters' do
+      let(:path) { 'SoMe/ImAgE' }
+
+      it 'return a string with a repository path' do
+        expect(subject.to_s).to eq 'some/image'
+      end
     end
   end
 
@@ -70,6 +80,12 @@ describe ContainerRegistry::Path do
 
       it { is_expected.to be_valid }
     end
+
+    context 'when path contains uppercase letters' do
+      let(:path) { 'Some/Registry' }
+
+      it { is_expected.to be_valid }
+    end
   end
 
   describe '#has_repository?' do
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index f1a1a71c52815b92470384ec86443f12133d0dee..9e3bd6d662fdd9fcca3e702e1f156eadc6d77337 100644
--- a/spec/lib/gitlab/ci/trace/stream_spec.rb
+++ b/spec/lib/gitlab/ci/trace/stream_spec.rb
@@ -17,12 +17,12 @@ describe Gitlab::Ci::Trace::Stream do
   describe '#limit' do
     let(:stream) do
       described_class.new do
-        StringIO.new("12345678")
+        StringIO.new((1..8).to_a.join("\n"))
       end
     end
 
-    it 'if size is larger we start from beggining' do
-      stream.limit(10)
+    it 'if size is larger we start from beginning' do
+      stream.limit(20)
 
       expect(stream.tell).to eq(0)
     end
@@ -30,7 +30,27 @@ describe Gitlab::Ci::Trace::Stream do
     it 'if size is smaller we start from the end' do
       stream.limit(2)
 
-      expect(stream.tell).to eq(6)
+      expect(stream.raw).to eq("8")
+    end
+
+    context 'when the trace contains ANSI sequence and Unicode' do
+      let(:stream) do
+        described_class.new do
+          File.open(expand_fixture_path('trace/ansi-sequence-and-unicode'))
+        end
+      end
+
+      it 'forwards to the next linefeed, case 1' do
+        stream.limit(7)
+
+        expect(stream.raw).to eq('')
+      end
+
+      it 'forwards to the next linefeed, case 2' do
+        stream.limit(29)
+
+        expect(stream.raw).to eq("\e[01;32m許功蓋\e[0m\n")
+      end
     end
   end
 
@@ -167,7 +187,7 @@ describe Gitlab::Ci::Trace::Stream do
       let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered' }
       let(:regex) { '\(\d+.\d+\%\) covered' }
 
-      it { is_expected.to eq(98.29) }
+      it { is_expected.to eq("98.29") }
     end
 
     context 'valid content & bad regex' do
@@ -188,14 +208,14 @@ describe Gitlab::Ci::Trace::Stream do
       let(:data) { ' (98.39%) covered. (98.29%) covered' }
       let(:regex) { '\(\d+.\d+\%\) covered' }
 
-      it { is_expected.to eq(98.29) }
+      it { is_expected.to eq("98.29") }
     end
 
     context 'using a regex capture' do
       let(:data) { 'TOTAL      9926   3489    65%' }
       let(:regex) { 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)' }
 
-      it { is_expected.to eq(65) }
+      it { is_expected.to eq("65") }
     end
   end
 end
diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb
index 69e8dc9220d51abc69d0c9b12c22cfbcf81f3c2f..9cb0b62590a0a04fda0c2f5a6d88356e35b54ad0 100644
--- a/spec/lib/gitlab/ci/trace_spec.rb
+++ b/spec/lib/gitlab/ci/trace_spec.rb
@@ -40,12 +40,24 @@ describe Gitlab::Ci::Trace do
   describe '#extract_coverage' do
     let(:regex) { '\(\d+.\d+\%\) covered' }
 
-    before do
-      trace.set('Coverage 1033 / 1051 LOC (98.29%) covered')
+    context 'matching coverage' do
+      before do
+        trace.set('Coverage 1033 / 1051 LOC (98.29%) covered')
+      end
+
+      it "returns valid coverage" do
+        expect(trace.extract_coverage(regex)).to eq("98.29")
+      end
     end
 
-    it "returns valid coverage" do
-      expect(trace.extract_coverage(regex)).to eq(98.29)
+    context 'no coverage' do
+      before do
+        trace.set('No coverage')
+      end
+
+      it 'returs nil' do
+        expect(trace.extract_coverage(regex)).to be_nil
+      end
     end
   end
 
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index 4ac79454647e7f4c5e7b01cf726e60e37266b9bb..a044b871730cdb4052e4566f5ad2c0e5fd50fff8 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -175,6 +175,50 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
     end
   end
 
+  describe '#true_value' do
+    context 'using PostgreSQL' do
+      before do
+        expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
+      end
+
+      it 'returns the appropriate value' do
+        expect(model.true_value).to eq("'t'")
+      end
+    end
+
+    context 'using MySQL' do
+      before do
+        expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+      end
+
+      it 'returns the appropriate value' do
+        expect(model.true_value).to eq(1)
+      end
+    end
+  end
+
+  describe '#false_value' do
+    context 'using PostgreSQL' do
+      before do
+        expect(Gitlab::Database).to receive(:postgresql?).and_return(true)
+      end
+
+      it 'returns the appropriate value' do
+        expect(model.false_value).to eq("'f'")
+      end
+    end
+
+    context 'using MySQL' do
+      before do
+        expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
+      end
+
+      it 'returns the appropriate value' do
+        expect(model.false_value).to eq(0)
+      end
+    end
+  end
+
   describe '#update_column_in_batches' do
     before do
       create_list(:empty_project, 5)
@@ -294,4 +338,392 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
       end
     end
   end
+
+  describe '#rename_column_concurrently' do
+    context 'in a transaction' do
+      it 'raises RuntimeError' do
+        allow(model).to receive(:transaction_open?).and_return(true)
+
+        expect { model.rename_column_concurrently(:users, :old, :new) }.
+          to raise_error(RuntimeError)
+      end
+    end
+
+    context 'outside a transaction' do
+      let(:old_column) do
+        double(:column,
+               type: :integer,
+               limit: 8,
+               default: 0,
+               null: false,
+               precision: 5,
+               scale: 1)
+      end
+
+      let(:trigger_name) { model.rename_trigger_name(:users, :old, :new) }
+
+      before do
+        allow(model).to receive(:transaction_open?).and_return(false)
+        allow(model).to receive(:column_for).and_return(old_column)
+
+        # Since MySQL and PostgreSQL use different quoting styles we'll just
+        # stub the methods used for this to make testing easier.
+        allow(model).to receive(:quote_column_name) { |name| name.to_s }
+        allow(model).to receive(:quote_table_name) { |name| name.to_s }
+      end
+
+      context 'using MySQL' do
+        it 'renames a column concurrently' do
+          allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+          expect(model).to receive(:install_rename_triggers_for_mysql).
+            with(trigger_name, 'users', 'old', 'new')
+
+          expect(model).to receive(:add_column).
+            with(:users, :new, :integer,
+                 limit: old_column.limit,
+                 default: old_column.default,
+                 null: old_column.null,
+                 precision: old_column.precision,
+                 scale: old_column.scale)
+
+          expect(model).to receive(:update_column_in_batches)
+
+          expect(model).to receive(:copy_indexes).with(:users, :old, :new)
+          expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
+
+          model.rename_column_concurrently(:users, :old, :new)
+        end
+      end
+
+      context 'using PostgreSQL' do
+        it 'renames a column concurrently' do
+          allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+          expect(model).to receive(:install_rename_triggers_for_postgresql).
+            with(trigger_name, 'users', 'old', 'new')
+
+          expect(model).to receive(:add_column).
+            with(:users, :new, :integer,
+                 limit: old_column.limit,
+                 default: old_column.default,
+                 null: old_column.null,
+                 precision: old_column.precision,
+                 scale: old_column.scale)
+
+          expect(model).to receive(:update_column_in_batches)
+
+          expect(model).to receive(:copy_indexes).with(:users, :old, :new)
+          expect(model).to receive(:copy_foreign_keys).with(:users, :old, :new)
+
+          model.rename_column_concurrently(:users, :old, :new)
+        end
+      end
+    end
+  end
+
+  describe '#cleanup_concurrent_column_rename' do
+    it 'cleans up the renaming procedure for PostgreSQL' do
+      allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
+      expect(model).to receive(:remove_rename_triggers_for_postgresql).
+        with(:users, /trigger_.{12}/)
+
+      expect(model).to receive(:remove_column).with(:users, :old)
+
+      model.cleanup_concurrent_column_rename(:users, :old, :new)
+    end
+
+    it 'cleans up the renaming procedure for MySQL' do
+      allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
+      expect(model).to receive(:remove_rename_triggers_for_mysql).
+        with(/trigger_.{12}/)
+
+      expect(model).to receive(:remove_column).with(:users, :old)
+
+      model.cleanup_concurrent_column_rename(:users, :old, :new)
+    end
+  end
+
+  describe '#change_column_type_concurrently' do
+    it 'changes the column type' do
+      expect(model).to receive(:rename_column_concurrently).
+        with('users', 'username', 'username_for_type_change', type: :text)
+
+      model.change_column_type_concurrently('users', 'username', :text)
+    end
+  end
+
+  describe '#cleanup_concurrent_column_type_change' do
+    it 'cleans up the type changing procedure' do
+      expect(model).to receive(:cleanup_concurrent_column_rename).
+        with('users', 'username', 'username_for_type_change')
+
+      expect(model).to receive(:rename_column).
+        with('users', 'username_for_type_change', 'username')
+
+      model.cleanup_concurrent_column_type_change('users', 'username')
+    end
+  end
+
+  describe '#install_rename_triggers_for_postgresql' do
+    it 'installs the triggers for PostgreSQL' do
+      expect(model).to receive(:execute).
+        with(/CREATE OR REPLACE FUNCTION foo()/m)
+
+      expect(model).to receive(:execute).
+        with(/CREATE TRIGGER foo/m)
+
+      model.install_rename_triggers_for_postgresql('foo', :users, :old, :new)
+    end
+  end
+
+  describe '#install_rename_triggers_for_mysql' do
+    it 'installs the triggers for MySQL' do
+      expect(model).to receive(:execute).
+        with(/CREATE TRIGGER foo_insert.+ON users/m)
+
+      expect(model).to receive(:execute).
+        with(/CREATE TRIGGER foo_update.+ON users/m)
+
+      model.install_rename_triggers_for_mysql('foo', :users, :old, :new)
+    end
+  end
+
+  describe '#remove_rename_triggers_for_postgresql' do
+    it 'removes the function and trigger' do
+      expect(model).to receive(:execute).with('DROP TRIGGER foo ON bar')
+      expect(model).to receive(:execute).with('DROP FUNCTION foo()')
+
+      model.remove_rename_triggers_for_postgresql('bar', 'foo')
+    end
+  end
+
+  describe '#remove_rename_triggers_for_mysql' do
+    it 'removes the triggers' do
+      expect(model).to receive(:execute).with('DROP TRIGGER foo_insert')
+      expect(model).to receive(:execute).with('DROP TRIGGER foo_update')
+
+      model.remove_rename_triggers_for_mysql('foo')
+    end
+  end
+
+  describe '#rename_trigger_name' do
+    it 'returns a String' do
+      expect(model.rename_trigger_name(:users, :foo, :bar)).
+        to match(/trigger_.{12}/)
+    end
+  end
+
+  describe '#indexes_for' do
+    it 'returns the indexes for a column' do
+      idx1 = double(:idx, columns: %w(project_id))
+      idx2 = double(:idx, columns: %w(user_id))
+
+      allow(model).to receive(:indexes).with('table').and_return([idx1, idx2])
+
+      expect(model.indexes_for('table', :user_id)).to eq([idx2])
+    end
+  end
+
+  describe '#foreign_keys_for' do
+    it 'returns the foreign keys for a column' do
+      fk1 = double(:fk, column: 'project_id')
+      fk2 = double(:fk, column: 'user_id')
+
+      allow(model).to receive(:foreign_keys).with('table').and_return([fk1, fk2])
+
+      expect(model.foreign_keys_for('table', :user_id)).to eq([fk2])
+    end
+  end
+
+  describe '#copy_indexes' do
+    context 'using a regular index using a single column' do
+      it 'copies the index' do
+        index = double(:index,
+                       columns: %w(project_id),
+                       name: 'index_on_issues_project_id',
+                       using: nil,
+                       where: nil,
+                       opclasses: {},
+                       unique: false,
+                       lengths: [],
+                       orders: [])
+
+        allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+          and_return([index])
+
+        expect(model).to receive(:add_concurrent_index).
+          with(:issues,
+               %w(gl_project_id),
+               unique: false,
+               name: 'index_on_issues_gl_project_id',
+               length: [],
+               order: [])
+
+        model.copy_indexes(:issues, :project_id, :gl_project_id)
+      end
+    end
+
+    context 'using a regular index with multiple columns' do
+      it 'copies the index' do
+        index = double(:index,
+                       columns: %w(project_id foobar),
+                       name: 'index_on_issues_project_id_foobar',
+                       using: nil,
+                       where: nil,
+                       opclasses: {},
+                       unique: false,
+                       lengths: [],
+                       orders: [])
+
+        allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+          and_return([index])
+
+        expect(model).to receive(:add_concurrent_index).
+          with(:issues,
+               %w(gl_project_id foobar),
+               unique: false,
+               name: 'index_on_issues_gl_project_id_foobar',
+               length: [],
+               order: [])
+
+        model.copy_indexes(:issues, :project_id, :gl_project_id)
+      end
+    end
+
+    context 'using an index with a WHERE clause' do
+      it 'copies the index' do
+        index = double(:index,
+                       columns: %w(project_id),
+                       name: 'index_on_issues_project_id',
+                       using: nil,
+                       where: 'foo',
+                       opclasses: {},
+                       unique: false,
+                       lengths: [],
+                       orders: [])
+
+        allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+          and_return([index])
+
+        expect(model).to receive(:add_concurrent_index).
+          with(:issues,
+               %w(gl_project_id),
+               unique: false,
+               name: 'index_on_issues_gl_project_id',
+               length: [],
+               order: [],
+               where: 'foo')
+
+        model.copy_indexes(:issues, :project_id, :gl_project_id)
+      end
+    end
+
+    context 'using an index with a USING clause' do
+      it 'copies the index' do
+        index = double(:index,
+                       columns: %w(project_id),
+                       name: 'index_on_issues_project_id',
+                       where: nil,
+                       using: 'foo',
+                       opclasses: {},
+                       unique: false,
+                       lengths: [],
+                       orders: [])
+
+        allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+          and_return([index])
+
+        expect(model).to receive(:add_concurrent_index).
+          with(:issues,
+               %w(gl_project_id),
+               unique: false,
+               name: 'index_on_issues_gl_project_id',
+               length: [],
+               order: [],
+               using: 'foo')
+
+        model.copy_indexes(:issues, :project_id, :gl_project_id)
+      end
+    end
+
+    context 'using an index with custom operator classes' do
+      it 'copies the index' do
+        index = double(:index,
+                       columns: %w(project_id),
+                       name: 'index_on_issues_project_id',
+                       using: nil,
+                       where: nil,
+                       opclasses: { 'project_id' => 'bar' },
+                       unique: false,
+                       lengths: [],
+                       orders: [])
+
+        allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+          and_return([index])
+
+        expect(model).to receive(:add_concurrent_index).
+          with(:issues,
+               %w(gl_project_id),
+               unique: false,
+               name: 'index_on_issues_gl_project_id',
+               length: [],
+               order: [],
+               opclasses: { 'gl_project_id' => 'bar' })
+
+        model.copy_indexes(:issues, :project_id, :gl_project_id)
+      end
+    end
+
+    describe 'using an index of which the name does not contain the source column' do
+      it 'raises RuntimeError' do
+        index = double(:index,
+                       columns: %w(project_id),
+                       name: 'index_foobar_index',
+                       using: nil,
+                       where: nil,
+                       opclasses: {},
+                       unique: false,
+                       lengths: [],
+                       orders: [])
+
+        allow(model).to receive(:indexes_for).with(:issues, 'project_id').
+          and_return([index])
+
+        expect { model.copy_indexes(:issues, :project_id, :gl_project_id) }.
+          to raise_error(RuntimeError)
+      end
+    end
+  end
+
+  describe '#copy_foreign_keys' do
+    it 'copies foreign keys from one column to another' do
+      fk = double(:fk,
+                  from_table: 'issues',
+                  to_table: 'projects',
+                  on_delete: :cascade)
+
+      allow(model).to receive(:foreign_keys_for).with(:issues, :project_id).
+        and_return([fk])
+
+      expect(model).to receive(:add_concurrent_foreign_key).
+        with('issues', 'projects', column: :gl_project_id, on_delete: :cascade)
+
+      model.copy_foreign_keys(:issues, :project_id, :gl_project_id)
+    end
+  end
+
+  describe '#column_for' do
+    it 'returns a column object for an existing column' do
+      column = model.column_for(:users, :id)
+
+      expect(column.name).to eq('id')
+    end
+
+    it 'returns nil when a column does not exist' do
+      expect(model.column_for(:users, :kittens)).to be_nil
+    end
+  end
 end
diff --git a/spec/lib/gitlab/database/multi_threaded_migration_spec.rb b/spec/lib/gitlab/database/multi_threaded_migration_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..6c45f13bb5aac0292ec2e16f9831c39591b2f654
--- /dev/null
+++ b/spec/lib/gitlab/database/multi_threaded_migration_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+describe Gitlab::Database::MultiThreadedMigration do
+  let(:migration) do
+    Class.new { include Gitlab::Database::MultiThreadedMigration }.new
+  end
+
+  describe '#connection' do
+    after do
+      Thread.current[described_class::MULTI_THREAD_AR_CONNECTION] = nil
+    end
+
+    it 'returns the thread-local connection if present' do
+      Thread.current[described_class::MULTI_THREAD_AR_CONNECTION] = 10
+
+      expect(migration.connection).to eq(10)
+    end
+
+    it 'returns the global connection if no thread-local connection was set' do
+      expect(migration.connection).to eq(ActiveRecord::Base.connection)
+    end
+  end
+
+  describe '#with_multiple_threads' do
+    it 'starts multiple threads and yields the supplied block in every thread' do
+      output = Queue.new
+
+      migration.with_multiple_threads(2) do
+        output << migration.connection.execute('SELECT 1')
+      end
+
+      expect(output.size).to eq(2)
+    end
+
+    it 'joins the threads when the join parameter is set' do
+      expect_any_instance_of(Thread).to receive(:join).and_call_original
+
+      migration.with_multiple_threads(1) { }
+    end
+  end
+end
diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb
index 4ce4e6e10346a976c5b5323a961ab4afdc76d323..9b1d66a1b1c1b20d932341cb049826e424fee8cb 100644
--- a/spec/lib/gitlab/database_spec.rb
+++ b/spec/lib/gitlab/database_spec.rb
@@ -150,13 +150,13 @@ describe Gitlab::Database, lib: true do
     it 'returns correct value for PostgreSQL' do
       expect(described_class).to receive(:postgresql?).and_return(true)
 
-      expect(MigrationTest.new.true_value).to eq "'t'"
+      expect(described_class.true_value).to eq "'t'"
     end
 
     it 'returns correct value for MySQL' do
       expect(described_class).to receive(:postgresql?).and_return(false)
 
-      expect(MigrationTest.new.true_value).to eq 1
+      expect(described_class.true_value).to eq 1
     end
   end
 
@@ -164,13 +164,13 @@ describe Gitlab::Database, lib: true do
     it 'returns correct value for PostgreSQL' do
       expect(described_class).to receive(:postgresql?).and_return(true)
 
-      expect(MigrationTest.new.false_value).to eq "'f'"
+      expect(described_class.false_value).to eq "'f'"
     end
 
     it 'returns correct value for MySQL' do
       expect(described_class).to receive(:postgresql?).and_return(false)
 
-      expect(MigrationTest.new.false_value).to eq 0
+      expect(described_class.false_value).to eq 0
     end
   end
 end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 690f604db5e4cbf90d71ca88b58bf65e914ec535..3d6d7292b420710d2a17930350f37ce665e03010 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -24,20 +24,21 @@ describe Gitlab::Git::Repository, seed_helper: true do
       end
     end
 
-    context 'with gitaly enabled' do
-      before { stub_gitaly }
-
-      it 'gets the branch name from GitalyClient' do
-        expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
-        repository.root_ref
-      end
-
-      it 'wraps GRPC exceptions' do
-        expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name).
-          and_raise(GRPC::Unknown)
-        expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError)
-      end
-    end
+    # TODO: Uncomment when feature is reenabled
+    # context 'with gitaly enabled' do
+    #   before { stub_gitaly }
+    #
+    #   it 'gets the branch name from GitalyClient' do
+    #     expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name)
+    #     repository.root_ref
+    #   end
+    #
+    #   it 'wraps GRPC exceptions' do
+    #     expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:default_branch_name).
+    #       and_raise(GRPC::Unknown)
+    #     expect { repository.root_ref }.to raise_error(Gitlab::Git::CommandError)
+    #   end
+    # end
   end
 
   describe "#rugged" do
@@ -112,20 +113,21 @@ describe Gitlab::Git::Repository, seed_helper: true do
     it { is_expected.to include("master") }
     it { is_expected.not_to include("branch-from-space") }
 
-    context 'with gitaly enabled' do
-      before { stub_gitaly }
-
-      it 'gets the branch names from GitalyClient' do
-        expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
-        subject
-      end
-
-      it 'wraps GRPC exceptions' do
-        expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names).
-          and_raise(GRPC::Unknown)
-        expect { subject }.to raise_error(Gitlab::Git::CommandError)
-      end
-    end
+    # TODO: Uncomment when feature is reenabled
+    # context 'with gitaly enabled' do
+    #   before { stub_gitaly }
+    #
+    #   it 'gets the branch names from GitalyClient' do
+    #     expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names)
+    #     subject
+    #   end
+    #
+    #   it 'wraps GRPC exceptions' do
+    #     expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:branch_names).
+    #       and_raise(GRPC::Unknown)
+    #     expect { subject }.to raise_error(Gitlab::Git::CommandError)
+    #   end
+    # end
   end
 
   describe '#tag_names' do
@@ -143,20 +145,21 @@ describe Gitlab::Git::Repository, seed_helper: true do
     it { is_expected.to include("v1.0.0") }
     it { is_expected.not_to include("v5.0.0") }
 
-    context 'with gitaly enabled' do
-      before { stub_gitaly }
-
-      it 'gets the tag names from GitalyClient' do
-        expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
-        subject
-      end
-
-      it 'wraps GRPC exceptions' do
-        expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names).
-          and_raise(GRPC::Unknown)
-        expect { subject }.to raise_error(Gitlab::Git::CommandError)
-      end
-    end
+    # TODO: Uncomment when feature is reenabled
+    # context 'with gitaly enabled' do
+    #   before { stub_gitaly }
+    #
+    #   it 'gets the tag names from GitalyClient' do
+    #     expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names)
+    #     subject
+    #   end
+    #
+    #   it 'wraps GRPC exceptions' do
+    #     expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:tag_names).
+    #       and_raise(GRPC::Unknown)
+    #     expect { subject }.to raise_error(Gitlab::Git::CommandError)
+    #   end
+    # end
   end
 
   shared_examples 'archive check' do |extenstion|
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index 980a1b70ef5a636e5b8d6b6299b00c8ba6cc497f..ce31c8ed94cacc4e705b569fa3b56c47e3c92c62 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -389,31 +389,32 @@ eos
     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
+  # describe '#raw_diffs' do
+  # TODO: Uncomment when feature is reenabled
+  #   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/concerns/ignorable_column_spec.rb b/spec/models/concerns/ignorable_column_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..dba9fe43327e73760e3d37e07d78c8830723e03b
--- /dev/null
+++ b/spec/models/concerns/ignorable_column_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe IgnorableColumn do
+  let :base_class do
+    Class.new do
+      def self.columns
+        # This method does not have access to "double"
+        [Struct.new(:name).new('id'), Struct.new(:name).new('title')]
+      end
+    end
+  end
+
+  let :model do
+    Class.new(base_class) do
+      include IgnorableColumn
+    end
+  end
+
+  describe '.columns' do
+    it 'returns the columns, excluding the ignored ones' do
+      model.ignore_column(:title)
+
+      expect(model.columns.map(&:name)).to eq(%w(id))
+    end
+  end
+
+  describe '.ignored_columns' do
+    it 'returns a Set' do
+      expect(model.ignored_columns).to be_an_instance_of(Set)
+    end
+
+    it 'returns the names of the ignored columns' do
+      model.ignore_column(:title)
+
+      expect(model.ignored_columns).to eq(Set.new(%w(title)))
+    end
+  end
+end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 6d6c9f2adfcdd1ae6654e568c361eb5fb81c5ba6..eff41d85972d4d7775abb3507e0efa3469a8b92c 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -34,8 +34,18 @@ describe ContainerRepository do
   end
 
   describe '#path' do
-    it 'returns a full path to the repository' do
-      expect(repository.path).to eq('group/test/my_image')
+    context 'when project path does not contain uppercase letters' do
+      it 'returns a full path to the repository' do
+        expect(repository.path).to eq('group/test/my_image')
+      end
+    end
+
+    context 'when path contains uppercase letters' do
+      let(:project) { create(:project, path: 'MY_PROJECT', group: group) }
+
+      it 'returns a full path without capital letters' do
+        expect(repository.path).to eq('group/my_project/my_image')
+      end
     end
   end
 
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index af7753caba6d7cbe31b5f71f58eb40c19f7725c7..070716e859a7b713856a950cae949d38753d207d 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -110,17 +110,18 @@ describe Environment, models: true do
       end
     end
 
-    context 'Gitaly find_ref_name feature enabled' do
-      before do
-        allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:find_ref_name).and_return(true)
-      end
-
-      it 'calls GitalyClient' do
-        expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:find_ref_name)
-
-        environment.first_deployment_for(commit)
-      end
-    end
+    # TODO: Uncomment when feature is reenabled
+    # context 'Gitaly find_ref_name feature enabled' do
+    #   before do
+    #     allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:find_ref_name).and_return(true)
+    #   end
+    #
+    #   it 'calls GitalyClient' do
+    #     expect_any_instance_of(Gitlab::GitalyClient::Ref).to receive(:find_ref_name)
+    #
+    #     environment.first_deployment_for(commit)
+    #   end
+    # end
   end
 
   describe '#environment_type' do
diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb
index a9139f7d4ab22b4901bb270a9d7d185cae2d9fad..80ca19acddae6a8255261680883e067dca6fb26e 100644
--- a/spec/models/label_spec.rb
+++ b/spec/models/label_spec.rb
@@ -42,11 +42,27 @@ describe Label, models: true do
     end
   end
 
+  describe '#color' do
+    it 'strips color' do
+      label = described_class.new(color: '   #abcdef   ')
+      label.valid?
+
+      expect(label.color).to eq('#abcdef')
+    end
+  end
+
   describe '#title' do
     it 'sanitizes title' do
       label = described_class.new(title: '<b>foo & bar?</b>')
       expect(label.title).to eq('foo & bar?')
     end
+
+    it 'strips title' do
+      label = described_class.new(title: '   label   ')
+      label.valid?
+
+      expect(label.title).to eq('label')
+    end
   end
 
   describe 'priorization' do
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 3c4bf3f4ddb1bfa601ce174a5e45451b987c62f4..557ea97b008891a76ebf0832a8e9e3a6d37aba3f 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -622,12 +622,22 @@ describe Note, models: true do
   describe 'expiring ETag cache' do
     let(:note) { build(:note_on_issue) }
 
-    it "expires cache for note's issue when note is saved" do
+    def expect_expiration(note)
       expect_any_instance_of(Gitlab::EtagCaching::Store)
         .to receive(:touch)
         .with("/#{note.project.namespace.to_param}/#{note.project.to_param}/noteable/issue/#{note.noteable.id}/notes")
+    end
+
+    it "expires cache for note's issue when note is saved" do
+      expect_expiration(note)
 
       note.save!
     end
+
+    it "expires cache for note's issue when note is destroyed" do
+      expect_expiration(note)
+
+      note.destroy!
+    end
   end
 end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 8bd436558cb0928baeb6d82d5fba6c4c1ec0635d..5e5c2b016b6536a44795ad66357d0a331d8f8b3e 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1829,16 +1829,17 @@ describe Repository, models: true do
     end
   end
 
-  describe '#is_ancestor?' do
-    context 'Gitaly is_ancestor feature enabled' do
-      it 'asks Gitaly server if it\'s an ancestor' do
-        commit = repository.commit
-        allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true)
-        expect(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).
-          with(repository.raw_repository, commit.id, commit.id).and_return(true)
-
-        expect(repository.is_ancestor?(commit.id, commit.id)).to be true
-      end
-    end
-  end
+  # TODO: Uncomment when feature is reenabled
+  # describe '#is_ancestor?' do
+  #   context 'Gitaly is_ancestor feature enabled' do
+  #     it 'asks Gitaly server if it\'s an ancestor' do
+  #       commit = repository.commit
+  #       allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(:is_ancestor).and_return(true)
+  #       expect(Gitlab::GitalyClient::Commit).to receive(:is_ancestor).
+  #         with(repository.raw_repository, commit.id, commit.id).and_return(true)
+  #
+  #       expect(repository.is_ancestor?(commit.id, commit.id)).to be true
+  #     end
+  #   end
+  # end
 end
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 5c34ff041529f6c254fa8018f05b1ab200bb4b81..2077c14ff7a3c15c1a680f1b242f95f12e3a375d 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -22,7 +22,8 @@ describe GroupPolicy, models: true do
       :admin_group,
       :admin_namespace,
       :admin_group_member,
-      :change_visibility_level
+      :change_visibility_level,
+      :create_subgroup
     ]
   end
 
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index b1f8c249092484491e0319d7a993f17718f90987..b1603233f9e56907d989d6dd22084033ee3b5237 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -22,8 +22,8 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
     context "authorized user" do
       it "returns project hooks" do
         get api("/projects/#{project.id}/hooks", user)
-        expect(response).to have_http_status(200)
 
+        expect(response).to have_http_status(200)
         expect(json_response).to be_an Array
         expect(response).to include_pagination_headers
         expect(json_response.count).to eq(1)
@@ -43,6 +43,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
     context "unauthorized user" do
       it "does not access project hooks" do
         get api("/projects/#{project.id}/hooks", user3)
+
         expect(response).to have_http_status(403)
       end
     end
@@ -52,6 +53,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
     context "authorized user" do
       it "returns a project hook" do
         get api("/projects/#{project.id}/hooks/#{hook.id}", user)
+
         expect(response).to have_http_status(200)
         expect(json_response['url']).to eq(hook.url)
         expect(json_response['issues_events']).to eq(hook.issues_events)
@@ -67,6 +69,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
 
       it "returns a 404 error if hook id is not available" do
         get api("/projects/#{project.id}/hooks/1234", user)
+
         expect(response).to have_http_status(404)
       end
     end
@@ -88,7 +91,8 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
     it "adds hook to project" do
       expect do
         post api("/projects/#{project.id}/hooks", user),
-          url: "http://example.com", issues_events: true, wiki_page_events: true
+          url: "http://example.com", issues_events: true, wiki_page_events: true,
+          job_events: true
       end.to change {project.hooks.count}.by(1)
 
       expect(response).to have_http_status(201)
@@ -98,7 +102,7 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
       expect(json_response['merge_requests_events']).to eq(false)
       expect(json_response['tag_push_events']).to eq(false)
       expect(json_response['note_events']).to eq(false)
-      expect(json_response['job_events']).to eq(false)
+      expect(json_response['job_events']).to eq(true)
       expect(json_response['pipeline_events']).to eq(false)
       expect(json_response['wiki_page_events']).to eq(true)
       expect(json_response['enable_ssl_verification']).to eq(true)
@@ -136,7 +140,8 @@ describe API::ProjectHooks, 'ProjectHooks', api: true do
   describe "PUT /projects/:id/hooks/:hook_id" do
     it "updates an existing project hook" do
       put api("/projects/#{project.id}/hooks/#{hook.id}", user),
-        url: 'http://example.org', push_events: false
+        url: 'http://example.org', push_events: false, job_events: true
+
       expect(response).to have_http_status(200)
       expect(json_response['url']).to eq('http://example.org')
       expect(json_response['issues_events']).to eq(hook.issues_events)
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 62f21049b0bf7936ad982e9585e90112f6855341..7a07ea618c01944d6ea35b4c1e03b0914ae38f92 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -144,6 +144,20 @@ describe Projects::CreateService, '#execute', services: true do
     end
   end
 
+  context 'when a bad service template is created' do
+    before do
+      create(:service, type: 'DroneCiService', project: nil, template: true, active: true)
+    end
+
+    it 'reports an error in the imported project' do
+      opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-ce'
+      project = create_project(user, opts)
+
+      expect(project.errors.full_messages_for(:base).first).to match /Unable to save project. Error: Unable to save DroneCiService/
+      expect(project.services.count).to eq 0
+    end
+  end
+
   def create_project(user, opts)
     Projects::CreateService.new(user, opts).execute
   end
diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..2a6bfc1b3a085a93d684d8273f51a1dcfe274143
--- /dev/null
+++ b/spec/services/users/build_service_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe Users::BuildService, services: true do
+  describe '#execute' do
+    let(:params) do
+      { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' }
+    end
+
+    context 'with an admin user' do
+      let(:admin_user) { create(:admin) }
+      let(:service) { described_class.new(admin_user, params) }
+
+      it 'returns a valid user' do
+        expect(service.execute).to be_valid
+      end
+    end
+
+    context 'with non admin user' do
+      let(:user) { create(:user) }
+      let(:service) { described_class.new(user, params) }
+
+      it 'raises AccessDeniedError exception' do
+        expect { service.execute }.to raise_error Gitlab::Access::AccessDeniedError
+      end
+    end
+
+    context 'with nil user' do
+      let(:service) { described_class.new(nil, params) }
+
+      it 'returns a valid user' do
+        expect(service.execute).to be_valid
+      end
+
+      context 'when "send_user_confirmation_email" application setting is true' do
+        before do
+          stub_application_setting(send_user_confirmation_email: true, signup_enabled?: true)
+        end
+
+        it 'does not confirm the user' do
+          expect(service.execute).not_to be_confirmed
+        end
+      end
+
+      context 'when "send_user_confirmation_email" application setting is false' do
+        before do
+          stub_application_setting(send_user_confirmation_email: false, signup_enabled?: true)
+        end
+
+        it 'confirms the user' do
+          expect(service.execute).to be_confirmed
+        end
+      end
+    end
+  end
+end
diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb
index a111aec2f899ecea894d6403ede9612c1d9fbc01..7574627857311ce1fbc3218a3bf0f03566e034f8 100644
--- a/spec/services/users/create_service_spec.rb
+++ b/spec/services/users/create_service_spec.rb
@@ -1,38 +1,6 @@
 require 'spec_helper'
 
 describe Users::CreateService, services: true do
-  describe '#build' do
-    let(:params) do
-      { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' }
-    end
-
-    context 'with an admin user' do
-      let(:admin_user) { create(:admin) }
-      let(:service) { described_class.new(admin_user, params) }
-
-      it 'returns a valid user' do
-        expect(service.build).to be_valid
-      end
-    end
-
-    context 'with non admin user' do
-      let(:user) { create(:user) }
-      let(:service) { described_class.new(user, params) }
-
-      it 'raises AccessDeniedError exception' do
-        expect { service.build }.to raise_error Gitlab::Access::AccessDeniedError
-      end
-    end
-
-    context 'with nil user' do
-      let(:service) { described_class.new(nil, params) }
-
-      it 'returns a valid user' do
-        expect(service.build).to be_valid
-      end
-    end
-  end
-
   describe '#execute' do
     let(:admin_user) { create(:admin) }
 
@@ -185,40 +153,18 @@ describe Users::CreateService, services: true do
       end
       let(:service) { described_class.new(nil, params) }
 
-      context 'when "send_user_confirmation_email" application setting is true' do
-        before do
-          current_application_settings = double(:current_application_settings, send_user_confirmation_email: true, signup_enabled?: true)
-          allow(service).to receive(:current_application_settings).and_return(current_application_settings)
-        end
-
-        it 'does not confirm the user' do
-          expect(service.execute).not_to be_confirmed
-        end
-      end
-
-      context 'when "send_user_confirmation_email" application setting is false' do
-        before do
-          current_application_settings = double(:current_application_settings, send_user_confirmation_email: false, signup_enabled?: true)
-          allow(service).to receive(:current_application_settings).and_return(current_application_settings)
-        end
-
-        it 'confirms the user' do
-          expect(service.execute).to be_confirmed
-        end
-
-        it 'persists the given attributes' do
-          user = service.execute
-          user.reload
-
-          expect(user).to have_attributes(
-            name: params[:name],
-            username: params[:username],
-            email: params[:email],
-            password: params[:password],
-            created_by_id: nil,
-            admin: false
-          )
-        end
+      it 'persists the given attributes' do
+        user = service.execute
+        user.reload
+
+        expect(user).to have_attributes(
+          name: params[:name],
+          username: params[:username],
+          email: params[:email],
+          password: params[:password],
+          created_by_id: nil,
+          admin: false
+        )
       end
     end
   end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 4eb5b150af58bae933bdceffa85ef2c98661fcd3..e67ad8f345593e9bd433cd475db6b9324d58884a 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -9,8 +9,14 @@ require 'rspec/rails'
 require 'shoulda/matchers'
 require 'rspec/retry'
 
-if (ENV['RSPEC_PROFILING_POSTGRES_URL'] || ENV['RSPEC_PROFILING']) &&
-    (!ENV.has_key?('CI') || ENV['CI_COMMIT_REF_NAME'] == 'master')
+rspec_profiling_is_configured =
+  ENV['RSPEC_PROFILING_POSTGRES_URL'] ||
+  ENV['RSPEC_PROFILING']
+branch_can_be_profiled =
+  ENV['CI_COMMIT_REF_NAME'] == 'master' ||
+  ENV['CI_COMMIT_REF_NAME'] =~ /rspec-profile/
+
+if rspec_profiling_is_configured && (!ENV.key?('CI') || branch_can_be_profiled)
   require 'rspec_profiling/rspec'
 end
 
@@ -59,6 +65,10 @@ RSpec.configure do |config|
     TestEnv.init
   end
 
+  config.after(:suite) do
+    TestEnv.cleanup
+  end
+
   if ENV['CI']
     # Retry only on feature specs that use JS
     config.around :each, :js do |ex|
diff --git a/spec/features/discussion_comments_spec.rb b/spec/support/features/discussion_comments_shared_example.rb
similarity index 75%
rename from spec/features/discussion_comments_spec.rb
rename to spec/support/features/discussion_comments_shared_example.rb
index ae778118c5cc6f4bb53f1365bb8ab78999e0ace4..1a061ef069e66dff11b4b186fb1832ba09d619fb 100644
--- a/spec/features/discussion_comments_spec.rb
+++ b/spec/support/features/discussion_comments_shared_example.rb
@@ -1,5 +1,3 @@
-require 'spec_helper'
-
 shared_examples 'discussion comments' do |resource_name|
   let(:form_selector) { '.js-main-target-form' }
   let(:dropdown_selector) { "#{form_selector} .comment-type-dropdown" }
@@ -9,11 +7,9 @@ shared_examples 'discussion comments' do |resource_name|
   let(:close_selector) { "#{form_selector} .btn-comment-and-close" }
   let(:comments_selector) { '.timeline > .note.timeline-entry' }
 
-  it 'should show a comment type toggle' do
+  it 'clicking "Comment" will post a comment' do
     expect(page).to have_selector toggle_selector
-  end
 
-  it 'clicking "Comment" will post a comment' do
     find("#{form_selector} .note-textarea").send_keys('a')
 
     find(submit_selector).click
@@ -49,44 +45,29 @@ shared_examples 'discussion comments' do |resource_name|
       find(toggle_selector).click
     end
 
-    it 'opens a comment type dropdown with "Comment" and "Start discussion"' do
+    it 'has a "Comment" item (selected by default) and "Start discussion" item' do
       expect(page).to have_selector menu_selector
-    end
-
-    it 'has a "Comment" item' do
-      menu = find(menu_selector)
-
-      expect(menu).to have_content 'Comment'
-      expect(menu).to have_content "Add a general comment to this #{resource_name}."
-    end
 
-    it 'has a "Start discussion" item' do
-      menu = find(menu_selector)
-
-      expect(menu).to have_content 'Start discussion'
-      expect(menu).to have_content "Discuss a specific suggestion or question#{' that needs to be resolved' if resource_name == 'merge request'}."
-    end
-
-    it 'has the "Comment" item selected by default' do
       find("#{menu_selector} li", match: :first)
       items = all("#{menu_selector} li")
 
       expect(items.first).to have_content 'Comment'
+      expect(items.first).to have_content "Add a general comment to this #{resource_name}."
       expect(items.first).to have_selector '.fa-check'
       expect(items.first['class']).to match 'droplab-item-selected'
 
       expect(items.last).to have_content 'Start discussion'
+      expect(items.last).to have_content "Discuss a specific suggestion or question#{' that needs to be resolved' if resource_name == 'merge request'}."
       expect(items.last).not_to have_selector '.fa-check'
       expect(items.last['class']).not_to match 'droplab-item-selected'
     end
 
-    it 'closes the menu when clicking the toggle' do
+    it 'closes the menu when clicking the toggle or body' do
       find(toggle_selector).click
 
       expect(page).not_to have_selector menu_selector
-    end
 
-    it 'closes the menu when clicking the body' do
+      find(toggle_selector).click
       find('body').click
 
       expect(page).not_to have_selector menu_selector
@@ -104,12 +85,10 @@ shared_examples 'discussion comments' do |resource_name|
         all("#{menu_selector} li").last.click
       end
 
-      it 'updates the note_type input to "DiscussionNote"' do
-        expect(find("#{form_selector} #note_type", visible: false).value).to eq('DiscussionNote')
-      end
-
-      it 'updates the submit button text' do
+      it 'updates the submit button text, note_type input and closes the dropdown' do
         expect(find(dropdown_selector)).to have_content 'Start discussion'
+        expect(find("#{form_selector} #note_type", visible: false).value).to eq('DiscussionNote')
+        expect(page).not_to have_selector menu_selector
       end
 
       if resource_name =~ /(issue|merge request)/
@@ -124,10 +103,6 @@ shared_examples 'discussion comments' do |resource_name|
         end
       end
 
-      it 'closes the dropdown' do
-        expect(page).not_to have_selector menu_selector
-      end
-
       it 'clicking "Start discussion" will post a discussion' do
         find(submit_selector).click
 
@@ -176,12 +151,10 @@ shared_examples 'discussion comments' do |resource_name|
             find("#{menu_selector} li", match: :first).click
           end
 
-          it 'clears the note_type input"' do
-            expect(find("#{form_selector} #note_type", visible: false).value).to eq('')
-          end
-
-          it 'updates the submit button text' do
+          it 'updates the submit button text, clears the note_type input and closes the dropdown' do
             expect(find(dropdown_selector)).to have_content 'Comment'
+            expect(find("#{form_selector} #note_type", visible: false).value).to eq('')
+            expect(page).not_to have_selector menu_selector
           end
 
           if resource_name =~ /(issue|merge request)/
@@ -196,10 +169,6 @@ shared_examples 'discussion comments' do |resource_name|
             end
           end
 
-          it 'closes the dropdown' do
-            expect(page).not_to have_selector menu_selector
-          end
-
           it 'should have "Comment" selected when opening the menu' do
             find(toggle_selector).click
 
@@ -242,54 +211,3 @@ shared_examples 'discussion comments' do |resource_name|
     end
   end
 end
-
-describe 'Discussion Comments', :feature, :js do
-  include RepoHelpers
-
-  let(:user) { create(:user) }
-  let(:project) { create(:project) }
-
-  before do
-    project.team << [user, :developer]
-
-    login_as(user)
-  end
-
-  describe 'on a merge request' do
-    let(:merge_request) { create(:merge_request, source_project: project) }
-
-    before do
-      visit namespace_project_merge_request_path(project.namespace, project, merge_request)
-    end
-
-    it_behaves_like 'discussion comments', 'merge request'
-  end
-
-  describe 'on an issue' do
-    let(:issue) { create(:issue, project: project) }
-
-    before do
-      visit namespace_project_issue_path(project.namespace, project, issue)
-    end
-
-    it_behaves_like 'discussion comments', 'issue'
-  end
-
-  describe 'on an snippet' do
-    let(:snippet) { create(:project_snippet, :private, project: project, author: user) }
-
-    before do
-      visit namespace_project_snippet_path(project.namespace, project, snippet)
-    end
-
-    it_behaves_like 'discussion comments', 'snippet'
-  end
-
-  describe 'on a commit' do
-    before do
-      visit namespace_project_commit_path(project.namespace, project, sample_commit.id)
-    end
-
-    it_behaves_like 'discussion comments', 'commit'
-  end
-end
diff --git a/spec/support/fixture_helpers.rb b/spec/support/fixture_helpers.rb
index a05c9d18002e4a19e0bf9efa709daa04d44ee57a..5515c355cea9b2b5bc4f2efa4b8b65eb8a3f60a3 100644
--- a/spec/support/fixture_helpers.rb
+++ b/spec/support/fixture_helpers.rb
@@ -1,8 +1,11 @@
 module FixtureHelpers
   def fixture_file(filename)
     return '' if filename.blank?
-    file_path = File.expand_path(Rails.root.join('spec/fixtures/', filename))
-    File.read(file_path)
+    File.read(expand_fixture_path(filename))
+  end
+
+  def expand_fixture_path(filename)
+    File.expand_path(Rails.root.join('spec/fixtures/', filename))
   end
 end
 
diff --git a/spec/support/gitaly.rb b/spec/support/gitaly.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7aca902fc613bb5a7e8524e1d57c7513d58179e3
--- /dev/null
+++ b/spec/support/gitaly.rb
@@ -0,0 +1,7 @@
+if Gitlab::GitalyClient.enabled?
+  RSpec.configure do |config|
+    config.before(:each) do
+      allow(Gitlab::GitalyClient).to receive(:feature_enabled?).and_return(true)
+    end
+  end
+end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 1b5cb71a6b0528f8dde496fce13d1726eae08908..60c2096a12672860fe5672512abf6373d4597293 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -64,6 +64,8 @@ module TestEnv
     # Setup GitLab shell for test instance
     setup_gitlab_shell
 
+    setup_gitaly if Gitlab::GitalyClient.enabled?
+
     # Create repository for FactoryGirl.create(:project)
     setup_factory_repo
 
@@ -71,6 +73,10 @@ module TestEnv
     setup_forked_repo
   end
 
+  def cleanup
+    stop_gitaly
+  end
+
   def disable_mailer
     allow_any_instance_of(NotificationService).to receive(:mailer).
       and_return(double.as_null_object)
@@ -92,7 +98,7 @@ module TestEnv
     tmp_test_path = Rails.root.join('tmp', 'tests', '**')
 
     Dir[tmp_test_path].each do |entry|
-      unless File.basename(entry) =~ /\Agitlab-(shell|test|test_bare|test-fork|test-fork_bare)\z/
+      unless File.basename(entry) =~ /\A(gitaly|gitlab-(shell|test|test_bare|test-fork|test-fork_bare))\z/
         FileUtils.rm_rf(entry)
       end
     end
@@ -110,6 +116,28 @@ module TestEnv
     end
   end
 
+  def setup_gitaly
+    socket_path = Gitlab::GitalyClient.get_address('default').sub(/\Aunix:/, '')
+    gitaly_dir = File.dirname(socket_path)
+
+    unless File.directory?(gitaly_dir) || system('rake', "gitlab:gitaly:install[#{gitaly_dir}]")
+      raise "Can't clone gitaly"
+    end
+
+    start_gitaly(gitaly_dir, socket_path)
+  end
+
+  def start_gitaly(gitaly_dir, socket_path)
+    gitaly_exec = File.join(gitaly_dir, 'gitaly')
+    @gitaly_pid = spawn({ "GITALY_SOCKET_PATH" => socket_path }, gitaly_exec, [:out, :err] => '/dev/null')
+  end
+
+  def stop_gitaly
+    return unless @gitaly_pid
+
+    Process.kill('KILL', @gitaly_pid)
+  end
+
   def setup_factory_repo
     setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name,
                BRANCH_SHA)
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index b369dcbb305cfb45beea6d7b7029897c0ef63a11..aaf998a546f6f4ca7e0940f7ccd2be81b19ecfea 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -8,7 +8,7 @@ describe 'gitlab:gitaly namespace rake task' do
   describe 'install' do
     let(:repo) { 'https://gitlab.com/gitlab-org/gitaly.git' }
     let(:clone_path) { Rails.root.join('tmp/tests/gitaly').to_s }
-    let(:tag) { "v#{File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp}" }
+    let(:version) { File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp }
 
     context 'no dir given' do
       it 'aborts and display a help message' do
@@ -21,7 +21,7 @@ describe 'gitlab:gitaly namespace rake task' do
     context 'when an underlying Git command fail' do
       it 'aborts and display a help message' do
         expect_any_instance_of(Object).
-          to receive(:checkout_or_clone_tag).and_raise 'Git error'
+          to receive(:checkout_or_clone_version).and_raise 'Git error'
 
         expect { run_rake_task('gitlab:gitaly:install', clone_path) }.to raise_error 'Git error'
       end
@@ -32,9 +32,9 @@ describe 'gitlab:gitaly namespace rake task' do
         expect(Dir).to receive(:chdir).with(clone_path)
       end
 
-      it 'calls checkout_or_clone_tag with the right arguments' do
+      it 'calls checkout_or_clone_version with the right arguments' do
         expect_any_instance_of(Object).
-          to receive(:checkout_or_clone_tag).with(tag: tag, repo: repo, target_dir: clone_path)
+          to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path)
 
         run_rake_task('gitlab:gitaly:install', clone_path)
       end
@@ -48,7 +48,7 @@ describe 'gitlab:gitaly namespace rake task' do
 
       context 'gmake is available' do
         before do
-          expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
+          expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
           allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true)
         end
 
@@ -62,7 +62,7 @@ describe 'gitlab:gitaly namespace rake task' do
 
       context 'gmake is not available' do
         before do
-          expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
+          expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
           allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true)
         end
 
diff --git a/spec/tasks/gitlab/task_helpers_spec.rb b/spec/tasks/gitlab/task_helpers_spec.rb
index 86e42d845cea48de85c8c290d8be9e0b6335c420..3d9ba7cdc6f4a74e32d3741c4d86c04f54485c83 100644
--- a/spec/tasks/gitlab/task_helpers_spec.rb
+++ b/spec/tasks/gitlab/task_helpers_spec.rb
@@ -10,19 +10,38 @@ describe Gitlab::TaskHelpers do
 
   let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-test.git' }
   let(:clone_path) { Rails.root.join('tmp/tests/task_helpers_tests').to_s }
+  let(:version) { '1.1.0' }
   let(:tag) { 'v1.1.0' }
 
-  describe '#checkout_or_clone_tag' do
+  describe '#checkout_or_clone_version' do
     before do
       allow(subject).to receive(:run_command!)
-      expect(subject).to receive(:reset_to_tag).with(tag, clone_path)
     end
 
-    context 'target_dir does not exist' do
-      it 'clones the repo, retrieve the tag from origin, and checkout the tag' do
+    it 'checkout the version and reset to it' do
+      expect(subject).to receive(:checkout_version).with(tag, clone_path)
+      expect(subject).to receive(:reset_to_version).with(tag, clone_path)
+
+      subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
+    end
+
+    context 'with a branch version' do
+      let(:version) { '=branch_name' }
+      let(:branch) { 'branch_name' }
+
+      it 'checkout the version and reset to it with a branch name' do
+        expect(subject).to receive(:checkout_version).with(branch, clone_path)
+        expect(subject).to receive(:reset_to_version).with(branch, clone_path)
+
+        subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
+      end
+    end
+
+    context "target_dir doesn't exist" do
+      it 'clones the repo' do
         expect(subject).to receive(:clone_repo).with(repo, clone_path)
 
-        subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path)
+        subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
       end
     end
 
@@ -31,10 +50,10 @@ describe Gitlab::TaskHelpers do
         expect(Dir).to receive(:exist?).and_return(true)
       end
 
-      it 'fetch and checkout the tag' do
-        expect(subject).to receive(:checkout_tag).with(tag, clone_path)
+      it "doesn't clone the repository" do
+        expect(subject).not_to receive(:clone_repo)
 
-        subject.checkout_or_clone_tag(tag: tag, repo: repo, target_dir: clone_path)
+        subject.checkout_or_clone_version(version: version, repo: repo, target_dir: clone_path)
       end
     end
   end
@@ -48,49 +67,23 @@ describe Gitlab::TaskHelpers do
     end
   end
 
-  describe '#checkout_tag' do
+  describe '#checkout_version' do
     it 'clones the repo in the target dir' do
       expect(subject).
-        to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --tags --quiet])
+        to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch --quiet])
       expect(subject).
         to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} checkout --quiet #{tag}])
 
-      subject.checkout_tag(tag, clone_path)
+      subject.checkout_version(tag, clone_path)
     end
   end
 
-  describe '#reset_to_tag' do
-    let(:tag) { 'v1.1.0' }
-    before do
+  describe '#reset_to_version' do
+    it 'resets --hard to the given version' do
       expect(subject).
         to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} reset --hard #{tag}])
-    end
 
-    context 'when the tag is not checked out locally' do
-      before do
-        expect(subject).
-          to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_raise(Gitlab::TaskFailedError)
-      end
-
-      it 'fetch origin, ensure the tag exists, and resets --hard to the given tag' do
-        expect(subject).
-          to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} fetch origin])
-        expect(subject).
-          to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- origin/#{tag}]).and_return(tag)
-
-        subject.reset_to_tag(tag, clone_path)
-      end
-    end
-
-    context 'when the tag is checked out locally' do
-      before do
-        expect(subject).
-          to receive(:run_command!).with(%W[#{Gitlab.config.git.bin_path} -C #{clone_path} describe -- #{tag}]).and_return(tag)
-      end
-
-      it 'resets --hard to the given tag' do
-        subject.reset_to_tag(tag, clone_path)
-      end
+      subject.reset_to_version(tag, clone_path)
     end
   end
 end
diff --git a/spec/tasks/gitlab/workhorse_rake_spec.rb b/spec/tasks/gitlab/workhorse_rake_spec.rb
index 8a66a4aa047f67a6874a7de544bd68535ac92d2a..63d1cf2bbe59732bb985a71dfb1386b2eccd0229 100644
--- a/spec/tasks/gitlab/workhorse_rake_spec.rb
+++ b/spec/tasks/gitlab/workhorse_rake_spec.rb
@@ -8,7 +8,7 @@ describe 'gitlab:workhorse namespace rake task' do
   describe 'install' do
     let(:repo) { 'https://gitlab.com/gitlab-org/gitlab-workhorse.git' }
     let(:clone_path) { Rails.root.join('tmp/tests/gitlab-workhorse').to_s }
-    let(:tag) { "v#{File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp}" }
+    let(:version) { File.read(Rails.root.join(Gitlab::Workhorse::VERSION_FILE)).chomp }
 
     context 'no dir given' do
       it 'aborts and display a help message' do
@@ -21,7 +21,7 @@ describe 'gitlab:workhorse namespace rake task' do
     context 'when an underlying Git command fail' do
       it 'aborts and display a help message' do
         expect_any_instance_of(Object).
-          to receive(:checkout_or_clone_tag).and_raise 'Git error'
+          to receive(:checkout_or_clone_version).and_raise 'Git error'
 
         expect { run_rake_task('gitlab:workhorse:install', clone_path) }.to raise_error 'Git error'
       end
@@ -32,9 +32,9 @@ describe 'gitlab:workhorse namespace rake task' do
         expect(Dir).to receive(:chdir).with(clone_path)
       end
 
-      it 'calls checkout_or_clone_tag with the right arguments' do
+      it 'calls checkout_or_clone_version with the right arguments' do
         expect_any_instance_of(Object).
-          to receive(:checkout_or_clone_tag).with(tag: tag, repo: repo, target_dir: clone_path)
+          to receive(:checkout_or_clone_version).with(version: version, repo: repo, target_dir: clone_path)
 
         run_rake_task('gitlab:workhorse:install', clone_path)
       end
@@ -48,7 +48,7 @@ describe 'gitlab:workhorse namespace rake task' do
 
       context 'gmake is available' do
         before do
-          expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
+          expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
           allow_any_instance_of(Object).to receive(:run_command!).with(['gmake']).and_return(true)
         end
 
@@ -62,7 +62,7 @@ describe 'gitlab:workhorse namespace rake task' do
 
       context 'gmake is not available' do
         before do
-          expect_any_instance_of(Object).to receive(:checkout_or_clone_tag)
+          expect_any_instance_of(Object).to receive(:checkout_or_clone_version)
           allow_any_instance_of(Object).to receive(:run_command!).with(['make']).and_return(true)
         end
 
diff --git a/vendor/assets/javascripts/notebooklab.js b/vendor/assets/javascripts/notebooklab.js
index 601a645b655cd26c77830740e38b75af40b854fe..b8cfdc53b4835d0c8d4a535a13a86affea4c2a13 100644
--- a/vendor/assets/javascripts/notebooklab.js
+++ b/vendor/assets/javascripts/notebooklab.js
@@ -699,6 +699,48 @@ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { de
 //
 //
 
+var renderer = new _marked2.default.Renderer();
+
+/*
+  Regex to match KaTex blocks.
+
+  Supports the following:
+
+  \begin{equation}<math>\end{equation}
+  $$<math>$$
+  inline $<math>$
+
+  The matched text then goes through the KaTex renderer & then outputs the HTML
+*/
+var katexRegexString = '(\n  ^\\\\begin{[a-zA-Z]+}\\s\n  |\n  ^\\$\\$\n  |\n  \\s\\$(?!\\$)\n)\n  (.+?)\n(\n  \\s\\\\end{[a-zA-Z]+}$\n  |\n  \\$\\$$\n  |\n  \\$\n)\n'.replace(/\s/g, '').trim();
+
+renderer.paragraph = function (t) {
+  var text = t;
+  var inline = false;
+
+  if (typeof katex !== 'undefined') {
+    var katexString = text.replace(/\\/g, '\\');
+    var matches = new RegExp(katexRegexString, 'gi').exec(katexString);
+
+    if (matches && matches.length > 0) {
+      if (matches[1].trim() === '$' && matches[3].trim() === '$') {
+        inline = true;
+
+        text = katexString.replace(matches[0], '') + ' ' + katex.renderToString(matches[2]);
+      } else {
+        text = katex.renderToString(matches[2]);
+      }
+    }
+  }
+
+  return '<p class="' + (inline ? 'inline-katex' : '') + '">' + text + '</p>';
+};
+
+_marked2.default.setOptions({
+  sanitize: true,
+  renderer: renderer
+});
+
 exports.default = {
   components: {
     prompt: _prompt2.default
@@ -711,20 +753,7 @@ exports.default = {
   },
   computed: {
     markdown: function markdown() {
-      var regex = new RegExp('^\\$\\$(.*)\\$\\$$', 'g');
-
-      var source = this.cell.source.map(function (line) {
-        var matches = regex.exec(line.trim());
-
-        // Only render use the Katex library if it is actually loaded
-        if (matches && matches.length > 0 && typeof katex !== 'undefined') {
-          return katex.renderToString(matches[1]);
-        }
-
-        return line;
-      });
-
-      return (0, _marked2.default)(source.join(''));
+      return (0, _marked2.default)(this.cell.source.join(''));
     }
   }
 };
@@ -3047,7 +3076,7 @@ exports = module.exports = __webpack_require__(1)(undefined);
 
 
 // module
-exports.push([module.i, ".markdown .katex{display:block;text-align:center}", ""]);
+exports.push([module.i, ".markdown .katex{display:block;text-align:center}.markdown .inline-katex .katex{display:inline;text-align:initial}", ""]);
 
 // exports
 
diff --git a/vendor/assets/javascripts/pdf.worker.js b/vendor/assets/javascripts/pdf.worker.js
index f8a94e207f84b744b8ac72cdacb9dab12b772f94..970caaaba86379509cc46d04c1c2fcef873de997 100644
--- a/vendor/assets/javascripts/pdf.worker.js
+++ b/vendor/assets/javascripts/pdf.worker.js
@@ -73,7 +73,7 @@ return /******/ (function(modules) { // webpackBootstrap
 /******/ 	__webpack_require__.p = "";
 /******/
 /******/ 	// Load entry module and return exports
-/******/ 	return __webpack_require__(__webpack_require__.s = 16);
+/******/ 	return __webpack_require__(__webpack_require__.s = 24);
 /******/ })
 /************************************************************************/
 /******/ ({
@@ -20214,6 +20214,7 @@ var stringToUTF8String = sharedUtil.stringToUTF8String;
 var warn = sharedUtil.warn;
 var createValidAbsoluteUrl = sharedUtil.createValidAbsoluteUrl;
 var Util = sharedUtil.Util;
+var Dict = corePrimitives.Dict;
 var Ref = corePrimitives.Ref;
 var RefSet = corePrimitives.RefSet;
 var RefSetCache = corePrimitives.RefSetCache;
@@ -20233,9 +20234,10 @@ var Catalog = function CatalogClosure() {
     this.pdfManager = pdfManager;
     this.xref = xref;
     this.catDict = xref.getCatalogObj();
+    assert(isDict(this.catDict), 'catalog object is not a dictionary');
     this.fontCache = new RefSetCache();
     this.builtInCMapCache = Object.create(null);
-    assert(isDict(this.catDict), 'catalog object is not a dictionary');
+    this.pageKidsCountCache = new RefSetCache();
     this.pageFactory = pageFactory;
     this.pagePromises = [];
   }
@@ -20551,6 +20553,7 @@ var Catalog = function CatalogClosure() {
       return shadow(this, 'javaScript', javaScript);
     },
     cleanup: function Catalog_cleanup() {
+      this.pageKidsCountCache.clear();
       var promises = [];
       this.fontCache.forEach(function (promise) {
         promises.push(promise);
@@ -20577,15 +20580,25 @@ var Catalog = function CatalogClosure() {
     getPageDict: function Catalog_getPageDict(pageIndex) {
       var capability = createPromiseCapability();
       var nodesToVisit = [this.catDict.getRaw('Pages')];
-      var currentPageIndex = 0;
-      var xref = this.xref;
+      var count,
+          currentPageIndex = 0;
+      var xref = this.xref,
+          pageKidsCountCache = this.pageKidsCountCache;
       function next() {
         while (nodesToVisit.length) {
           var currentNode = nodesToVisit.pop();
           if (isRef(currentNode)) {
+            count = pageKidsCountCache.get(currentNode);
+            if (count > 0 && currentPageIndex + count < pageIndex) {
+              currentPageIndex += count;
+              continue;
+            }
             xref.fetchAsync(currentNode).then(function (obj) {
               if (isDict(obj, 'Page') || isDict(obj) && !obj.has('Kids')) {
                 if (pageIndex === currentPageIndex) {
+                  if (currentNode && !pageKidsCountCache.has(currentNode)) {
+                    pageKidsCountCache.put(currentNode, 1);
+                  }
                   capability.resolve([obj, currentNode]);
                 } else {
                   currentPageIndex++;
@@ -20599,7 +20612,11 @@ var Catalog = function CatalogClosure() {
             return;
           }
           assert(isDict(currentNode), 'page dictionary kid reference points to wrong type of object');
-          var count = currentNode.get('Count');
+          count = currentNode.get('Count');
+          var objId = currentNode.objId;
+          if (objId && !pageKidsCountCache.has(objId)) {
+            pageKidsCountCache.put(objId, count);
+          }
           if (currentPageIndex + count <= pageIndex) {
             currentPageIndex += count;
             continue;
@@ -21191,7 +21208,7 @@ var XRef = function XRefClosure() {
       var num = ref.num;
       if (num in this.cache) {
         var cacheEntry = this.cache[num];
-        if (isDict(cacheEntry) && !cacheEntry.objId) {
+        if (cacheEntry instanceof Dict && !cacheEntry.objId) {
           cacheEntry.objId = ref.toString();
         }
         return cacheEntry;
@@ -26178,7 +26195,7 @@ var CMapFactory = function CMapFactoryClosure() {
       return Promise.resolve(new IdentityCMap(true, 2));
     }
     if (BUILT_IN_CMAPS.indexOf(name) === -1) {
-      return Promise.reject(new Error('Unknown cMap name: ' + name));
+      return Promise.reject(new Error('Unknown CMap name: ' + name));
     }
     assert(fetchBuiltInCMap, 'Built-in CMap parameters are not provided.');
     return fetchBuiltInCMap(name).then(function (data) {
@@ -28458,9 +28475,6 @@ var Font = function FontClosure() {
               }
               glyphId = offsetIndex < 0 ? j : offsets[offsetIndex + j - start];
               glyphId = glyphId + delta & 0xFFFF;
-              if (glyphId === 0) {
-                continue;
-              }
               mappings.push({
                 charCode: j,
                 glyphId: glyphId
@@ -37160,8 +37174,8 @@ exports.Type1Parser = Type1Parser;
 "use strict";
 
 
-var pdfjsVersion = '1.7.395';
-var pdfjsBuild = '07f7c97b';
+var pdfjsVersion = '1.8.172';
+var pdfjsBuild = '8ff1fbe7';
 var pdfjsCoreWorker = __w_pdfjs_require__(8);
 {
   __w_pdfjs_require__(19);
@@ -37646,20 +37660,28 @@ if (typeof PDFJS === 'undefined' || !PDFJS.compatibilityChecked) {
     }
   })();
   (function checkRequestAnimationFrame() {
-    function fakeRequestAnimationFrame(callback) {
-      window.setTimeout(callback, 20);
+    function installFakeAnimationFrameFunctions() {
+      window.requestAnimationFrame = function (callback) {
+        return window.setTimeout(callback, 20);
+      };
+      window.cancelAnimationFrame = function (timeoutID) {
+        window.clearTimeout(timeoutID);
+      };
     }
     if (!hasDOM) {
       return;
     }
     if (isIOS) {
-      window.requestAnimationFrame = fakeRequestAnimationFrame;
+      installFakeAnimationFrameFunctions();
       return;
     }
     if ('requestAnimationFrame' in window) {
       return;
     }
-    window.requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || fakeRequestAnimationFrame;
+    window.requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame;
+    if (!('requestAnimationFrame' in window)) {
+      installFakeAnimationFrameFunctions();
+    }
   })();
   (function checkCanvasSizeLimitation() {
     if (isIOS || isAndroid) {
@@ -38588,7 +38610,7 @@ if (typeof PDFJS === 'undefined' || !PDFJS.compatibilityChecked) {
 
 /***/ }),
 
-/***/ 16:
+/***/ 24:
 /***/ (function(module, exports, __webpack_require__) {
 
 /* Copyright 2016 Mozilla Foundation
diff --git a/vendor/assets/javascripts/pdflab.js b/vendor/assets/javascripts/pdflab.js
index 94e7c40e75ec12960765716583de753a3e8f5723..5d9c348ce35595af435f30fe3cd72607b1b309cd 100644
--- a/vendor/assets/javascripts/pdflab.js
+++ b/vendor/assets/javascripts/pdflab.js
@@ -71,17 +71,10 @@ return /******/ (function(modules) { // webpackBootstrap
 /******/ 		if(installedChunks[chunkId] === 0)
 /******/ 			return Promise.resolve();
 /******/
-/******/ 		// a Promise means "currently loading".
+/******/ 		// an Promise means "currently loading".
 /******/ 		if(installedChunks[chunkId]) {
 /******/ 			return installedChunks[chunkId][2];
 /******/ 		}
-/******/
-/******/ 		// setup Promise in chunk cache
-/******/ 		var promise = new Promise(function(resolve, reject) {
-/******/ 			installedChunks[chunkId] = [resolve, reject];
-/******/ 		});
-/******/ 		installedChunks[chunkId][2] = promise;
-/******/
 /******/ 		// start chunk loading
 /******/ 		var head = document.getElementsByTagName('head')[0];
 /******/ 		var script = document.createElement('script');
@@ -106,8 +99,13 @@ return /******/ (function(modules) { // webpackBootstrap
 /******/ 				installedChunks[chunkId] = undefined;
 /******/ 			}
 /******/ 		};
-/******/ 		head.appendChild(script);
 /******/
+/******/ 		var promise = new Promise(function(resolve, reject) {
+/******/ 			installedChunks[chunkId] = [resolve, reject];
+/******/ 		});
+/******/ 		installedChunks[chunkId][2] = promise;
+/******/
+/******/ 		head.appendChild(script);
 /******/ 		return promise;
 /******/ 	};
 /******/
@@ -150,7 +148,7 @@ return /******/ (function(modules) { // webpackBootstrap
 /******/ 	__webpack_require__.oe = function(err) { console.error(err); throw err; };
 /******/
 /******/ 	// Load entry module and return exports
-/******/ 	return __webpack_require__(__webpack_require__.s = 7);
+/******/ 	return __webpack_require__(__webpack_require__.s = 23);
 /******/ })
 /************************************************************************/
 /******/ ([
@@ -1615,7 +1613,10 @@ var DOMCMapReaderFactory = function DOMCMapReaderFactoryClosure() {
           request.responseType = 'arraybuffer';
         }
         request.onreadystatechange = function () {
-          if (request.readyState === XMLHttpRequest.DONE && (request.status === 200 || request.status === 0)) {
+          if (request.readyState !== XMLHttpRequest.DONE) {
+            return;
+          }
+          if (request.status === 200 || request.status === 0) {
             var data;
             if (this.isCompressed && request.response) {
               data = new Uint8Array(request.response);
@@ -1629,8 +1630,8 @@ var DOMCMapReaderFactory = function DOMCMapReaderFactoryClosure() {
               });
               return;
             }
-            reject(new Error('Unable to load ' + (this.isCompressed ? 'binary ' : '') + 'CMap at: ' + url));
           }
+          reject(new Error('Unable to load ' + (this.isCompressed ? 'binary ' : '') + 'CMap at: ' + url));
         }.bind(this);
         request.send(null);
       }.bind(this));
@@ -1670,6 +1671,16 @@ var CustomStyle = function CustomStyleClosure() {
   };
   return CustomStyle;
 }();
+var RenderingCancelledException = function RenderingCancelledException() {
+  function RenderingCancelledException(msg, type) {
+    this.message = msg;
+    this.type = type;
+  }
+  RenderingCancelledException.prototype = new Error();
+  RenderingCancelledException.prototype.name = 'RenderingCancelledException';
+  RenderingCancelledException.constructor = RenderingCancelledException;
+  return RenderingCancelledException;
+}();
 var hasCanvasTypedArrays;
 hasCanvasTypedArrays = function hasCanvasTypedArrays() {
   var canvas = document.createElement('canvas');
@@ -1762,6 +1773,8 @@ function getDefaultSetting(id) {
       return globalSettings ? globalSettings.externalLinkRel : DEFAULT_LINK_REL;
     case 'enableStats':
       return !!(globalSettings && globalSettings.enableStats);
+    case 'pdfjsNext':
+      return !!(globalSettings && globalSettings.pdfjsNext);
     default:
       throw new Error('Unknown default setting: ' + id);
   }
@@ -1789,6 +1802,7 @@ exports.isExternalLinkTargetSet = isExternalLinkTargetSet;
 exports.isValidUrl = isValidUrl;
 exports.getFilenameFromUrl = getFilenameFromUrl;
 exports.LinkTarget = LinkTarget;
+exports.RenderingCancelledException = RenderingCancelledException;
 exports.hasCanvasTypedArrays = hasCanvasTypedArrays;
 exports.getDefaultSetting = getDefaultSetting;
 exports.DEFAULT_LINK_REL = DEFAULT_LINK_REL;
@@ -2450,6 +2464,7 @@ var FontFaceObject = displayFontLoader.FontFaceObject;
 var FontLoader = displayFontLoader.FontLoader;
 var CanvasGraphics = displayCanvas.CanvasGraphics;
 var Metadata = displayMetadata.Metadata;
+var RenderingCancelledException = displayDOMUtils.RenderingCancelledException;
 var getDefaultSetting = displayDOMUtils.getDefaultSetting;
 var DOMCanvasFactory = displayDOMUtils.DOMCanvasFactory;
 var DOMCMapReaderFactory = displayDOMUtils.DOMCMapReaderFactory;
@@ -3711,7 +3726,11 @@ var InternalRenderTask = function InternalRenderTaskClosure() {
     cancel: function InternalRenderTask_cancel() {
       this.running = false;
       this.cancelled = true;
-      this.callback('cancelled');
+      if (getDefaultSetting('pdfjsNext')) {
+        this.callback(new RenderingCancelledException('Rendering cancelled, page ' + this.pageNumber, 'canvas'));
+      } else {
+        this.callback('cancelled');
+      }
     },
     operatorListChanged: function InternalRenderTask_operatorListChanged() {
       if (!this.graphicsReady) {
@@ -3776,8 +3795,8 @@ var _UnsupportedManager = function UnsupportedManagerClosure() {
     }
   };
 }();
-exports.version = '1.7.395';
-exports.build = '07f7c97b';
+exports.version = '1.8.172';
+exports.build = '8ff1fbe7';
 exports.getDocument = getDocument;
 exports.PDFDataRangeTransport = PDFDataRangeTransport;
 exports.PDFWorker = PDFWorker;
@@ -5716,8 +5735,8 @@ if (!globalScope.PDFJS) {
   globalScope.PDFJS = {};
 }
 var PDFJS = globalScope.PDFJS;
-PDFJS.version = '1.7.395';
-PDFJS.build = '07f7c97b';
+PDFJS.version = '1.8.172';
+PDFJS.build = '8ff1fbe7';
 PDFJS.pdfBug = false;
 if (PDFJS.verbosity !== undefined) {
   sharedUtil.setVerbosityLevel(PDFJS.verbosity);
@@ -5777,6 +5796,7 @@ PDFJS.disableWebGL = PDFJS.disableWebGL === undefined ? true : PDFJS.disableWebG
 PDFJS.externalLinkTarget = PDFJS.externalLinkTarget === undefined ? LinkTarget.NONE : PDFJS.externalLinkTarget;
 PDFJS.externalLinkRel = PDFJS.externalLinkRel === undefined ? DEFAULT_LINK_REL : PDFJS.externalLinkRel;
 PDFJS.isEvalSupported = PDFJS.isEvalSupported === undefined ? true : PDFJS.isEvalSupported;
+PDFJS.pdfjsNext = PDFJS.pdfjsNext === undefined ? false : PDFJS.pdfjsNext;
 var savedOpenExternalLinksInNewWindow = PDFJS.openExternalLinksInNewWindow;
 delete PDFJS.openExternalLinksInNewWindow;
 Object.defineProperty(PDFJS, 'openExternalLinksInNewWindow', {
@@ -8227,8 +8247,8 @@ exports.TilingPattern = TilingPattern;
 "use strict";
 
 
-var pdfjsVersion = '1.7.395';
-var pdfjsBuild = '07f7c97b';
+var pdfjsVersion = '1.8.172';
+var pdfjsBuild = '8ff1fbe7';
 var pdfjsSharedUtil = __w_pdfjs_require__(0);
 var pdfjsDisplayGlobal = __w_pdfjs_require__(9);
 var pdfjsDisplayAPI = __w_pdfjs_require__(3);
@@ -8259,6 +8279,7 @@ exports.createObjectURL = pdfjsSharedUtil.createObjectURL;
 exports.removeNullCharacters = pdfjsSharedUtil.removeNullCharacters;
 exports.shadow = pdfjsSharedUtil.shadow;
 exports.createBlob = pdfjsSharedUtil.createBlob;
+exports.RenderingCancelledException = pdfjsDisplayDOMUtils.RenderingCancelledException;
 exports.getFilenameFromUrl = pdfjsDisplayDOMUtils.getFilenameFromUrl;
 exports.addLinkAttributes = pdfjsDisplayDOMUtils.addLinkAttributes;
 
@@ -8740,20 +8761,28 @@ if (typeof PDFJS === 'undefined' || !PDFJS.compatibilityChecked) {
     }
   })();
   (function checkRequestAnimationFrame() {
-    function fakeRequestAnimationFrame(callback) {
-      window.setTimeout(callback, 20);
+    function installFakeAnimationFrameFunctions() {
+      window.requestAnimationFrame = function (callback) {
+        return window.setTimeout(callback, 20);
+      };
+      window.cancelAnimationFrame = function (timeoutID) {
+        window.clearTimeout(timeoutID);
+      };
     }
     if (!hasDOM) {
       return;
     }
     if (isIOS) {
-      window.requestAnimationFrame = fakeRequestAnimationFrame;
+      installFakeAnimationFrameFunctions();
       return;
     }
     if ('requestAnimationFrame' in window) {
       return;
     }
-    window.requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || fakeRequestAnimationFrame;
+    window.requestAnimationFrame = window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame;
+    if (!('requestAnimationFrame' in window)) {
+      installFakeAnimationFrameFunctions();
+    }
   })();
   (function checkCanvasSizeLimitation() {
     if (isIOS || isAndroid) {
@@ -9760,7 +9789,7 @@ function toComment(sourceMap) {
   return '/*# ' + data + ' */';
 }
 
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(11).Buffer))
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(10).Buffer))
 
 /***/ }),
 /* 4 */
@@ -9839,7 +9868,7 @@ if (typeof DEBUG !== 'undefined' && DEBUG) {
   ) }
 }
 
-var listToStyles = __webpack_require__(23)
+var listToStyles = __webpack_require__(21)
 
 /*
 type StyleObject = {
@@ -10046,34 +10075,18 @@ function applyToTag (styleElement, obj) {
 
 
 /* styles */
-__webpack_require__(21)
+__webpack_require__(19)
 
 var Component = __webpack_require__(4)(
   /* script */
-  __webpack_require__(8),
+  __webpack_require__(7),
   /* template */
-  __webpack_require__(19),
+  __webpack_require__(17),
   /* scopeId */
   null,
   /* cssModules */
   null
 )
-Component.options.__file = "/Users/samrose/Projects/pdflab/src/index.vue"
-if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
-if (Component.options.functional) {console.error("[vue-loader] index.vue: functional components are not supported with templates, they should use render functions.")}
-
-/* hot reload */
-if (false) {(function () {
-  var hotAPI = require("vue-hot-reload-api")
-  hotAPI.install(require("vue"), false)
-  if (!hotAPI.compatible) return
-  module.hot.accept()
-  if (!module.hot.data) {
-    hotAPI.createRecord("data-v-7c7bed7e", Component.options)
-  } else {
-    hotAPI.reload("data-v-7c7bed7e", Component.options)
-  }
-})()}
 
 module.exports = Component.exports
 
@@ -10085,25 +10098,6 @@ module.exports = Component.exports
 "use strict";
 
 
-var PDF = __webpack_require__(6);
-var pdfjsLib = __webpack_require__(2);
-
-module.exports = {
-  install: function install(_vue, _ref) {
-    var workerSrc = _ref.workerSrc;
-
-    pdfjsLib.PDFJS.workerSrc = workerSrc;
-    _vue.component('pdf-lab', PDF);
-  }
-};
-
-/***/ }),
-/* 8 */
-/***/ (function(module, exports, __webpack_require__) {
-
-"use strict";
-
-
 Object.defineProperty(exports, "__esModule", {
   value: true
 });
@@ -10112,7 +10106,7 @@ var _pdfjsDist = __webpack_require__(2);
 
 var _pdfjsDist2 = _interopRequireDefault(_pdfjsDist);
 
-var _index = __webpack_require__(18);
+var _index = __webpack_require__(16);
 
 var _index2 = _interopRequireDefault(_index);
 
@@ -10138,7 +10132,7 @@ exports.default = {
   },
   data: function data() {
     return {
-      isLoading: false,
+      loading: false,
       pages: []
     };
   },
@@ -10163,17 +10157,17 @@ exports.default = {
       }).catch(function (error) {
         return _this.$emit('pdflaberror', error);
       }).then(function () {
-        return _this.isLoading = false;
+        _this.loading = false;
       });
     },
     renderPages: function renderPages(pdf) {
       var _this2 = this;
 
       var pagePromises = [];
-      this.isLoading = true;
-      for (var num = 1; num <= pdf.numPages; num++) {
-        pagePromises.push(pdf.getPage(num).then(function (page) {
-          return _this2.pages.push(page);
+      this.loading = true;
+      for (var num = 1; num <= pdf.numPages; num += 1) {
+        pagePromises.push(pdf.getPage(num).then(function (p) {
+          return _this2.pages.push(p);
         }));
       }
       return Promise.all(pagePromises);
@@ -10185,7 +10179,7 @@ exports.default = {
 };
 
 /***/ }),
-/* 9 */
+/* 8 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
@@ -10213,10 +10207,16 @@ exports.default = {
       required: true
     }
   },
+  data: function data() {
+    return {
+      scale: 4,
+      rendering: false
+    };
+  },
+
   computed: {
     viewport: function viewport() {
-      var scale = 4;
-      return this.page.getViewport(scale);
+      return this.page.getViewport(this.scale);
     },
     context: function context() {
       return this.$refs.canvas.getContext('2d');
@@ -10229,14 +10229,19 @@ exports.default = {
     }
   },
   mounted: function mounted() {
+    var _this = this;
+
     this.$refs.canvas.height = this.viewport.height;
     this.$refs.canvas.width = this.viewport.width;
-    this.page.render(this.renderContext);
+    this.rendering = true;
+    this.page.render(this.renderContext).then(function () {
+      _this.rendering = false;
+    });
   }
 };
 
 /***/ }),
-/* 10 */
+/* 9 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
@@ -10357,7 +10362,7 @@ function fromByteArray (uint8) {
 
 
 /***/ }),
-/* 11 */
+/* 10 */
 /***/ (function(module, exports, __webpack_require__) {
 
 "use strict";
@@ -10371,9 +10376,9 @@ function fromByteArray (uint8) {
 
 
 
-var base64 = __webpack_require__(10)
-var ieee754 = __webpack_require__(14)
-var isArray = __webpack_require__(15)
+var base64 = __webpack_require__(9)
+var ieee754 = __webpack_require__(13)
+var isArray = __webpack_require__(14)
 
 exports.Buffer = Buffer
 exports.SlowBuffer = SlowBuffer
@@ -12151,10 +12156,10 @@ function isnan (val) {
   return val !== val // eslint-disable-line no-self-compare
 }
 
-/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(24)))
+/* WEBPACK VAR INJECTION */}.call(exports, __webpack_require__(22)))
 
 /***/ }),
-/* 12 */
+/* 11 */
 /***/ (function(module, exports, __webpack_require__) {
 
 exports = module.exports = __webpack_require__(3)(undefined);
@@ -12162,13 +12167,13 @@ exports = module.exports = __webpack_require__(3)(undefined);
 
 
 // module
-exports.push([module.i, "\n.pdf-viewer {\n  background: url(" + __webpack_require__(17) + ");\n  display: flex;\n  flex-flow: column nowrap;\n}\n", ""]);
+exports.push([module.i, ".pdf-viewer{background:url(" + __webpack_require__(15) + ");display:flex;flex-flow:column nowrap}", ""]);
 
 // exports
 
 
 /***/ }),
-/* 13 */
+/* 12 */
 /***/ (function(module, exports, __webpack_require__) {
 
 exports = module.exports = __webpack_require__(3)(undefined);
@@ -12176,13 +12181,13 @@ exports = module.exports = __webpack_require__(3)(undefined);
 
 
 // module
-exports.push([module.i, "\n.pdf-page {\n  margin: 8px auto 0 auto;\n  border-top: 1px #ddd solid;\n  border-bottom: 1px #ddd solid;\n  width: 100%;\n}\n.pdf-page:first-child {\n  margin-top: 0px;\n  border-top: 0px;\n}\n.pdf-page:last-child {\n  margin-bottom: 0px;\n  border-bottom: 0px;\n}\n", ""]);
+exports.push([module.i, ".pdf-page{margin:8px auto 0;border-top:1px solid #ddd;border-bottom:1px solid #ddd;width:100%}.pdf-page:first-child{margin-top:0;border-top:0}.pdf-page:last-child{margin-bottom:0;border-bottom:0}", ""]);
 
 // exports
 
 
 /***/ }),
-/* 14 */
+/* 13 */
 /***/ (function(module, exports) {
 
 exports.read = function (buffer, offset, isLE, mLen, nBytes) {
@@ -12272,7 +12277,7 @@ exports.write = function (buffer, value, offset, isLE, mLen, nBytes) {
 
 
 /***/ }),
-/* 15 */
+/* 14 */
 /***/ (function(module, exports) {
 
 var toString = {}.toString;
@@ -12283,53 +12288,36 @@ module.exports = Array.isArray || function (arr) {
 
 
 /***/ }),
-/* 16 */,
-/* 17 */
+/* 15 */
 /***/ (function(module, exports) {
 
 module.exports = "data:image/gif;base64,R0lGODlhCgAKAIAAAOXl5f///yH5BAAAAAAALAAAAAAKAAoAAAIRhB2ZhxoM3GMSykqd1VltzxQAOw=="
 
 /***/ }),
-/* 18 */
+/* 16 */
 /***/ (function(module, exports, __webpack_require__) {
 
 
 /* styles */
-__webpack_require__(22)
+__webpack_require__(20)
 
 var Component = __webpack_require__(4)(
   /* script */
-  __webpack_require__(9),
+  __webpack_require__(8),
   /* template */
-  __webpack_require__(20),
+  __webpack_require__(18),
   /* scopeId */
   null,
   /* cssModules */
   null
 )
-Component.options.__file = "/Users/samrose/Projects/pdflab/src/page/index.vue"
-if (Component.esModule && Object.keys(Component.esModule).some(function (key) {return key !== "default" && key !== "__esModule"})) {console.error("named exports are not supported in *.vue files.")}
-if (Component.options.functional) {console.error("[vue-loader] index.vue: functional components are not supported with templates, they should use render functions.")}
-
-/* hot reload */
-if (false) {(function () {
-  var hotAPI = require("vue-hot-reload-api")
-  hotAPI.install(require("vue"), false)
-  if (!hotAPI.compatible) return
-  module.hot.accept()
-  if (!module.hot.data) {
-    hotAPI.createRecord("data-v-7e912b1a", Component.options)
-  } else {
-    hotAPI.reload("data-v-7e912b1a", Component.options)
-  }
-})()}
 
 module.exports = Component.exports
 
 
 /***/ }),
-/* 19 */
-/***/ (function(module, exports, __webpack_require__) {
+/* 17 */
+/***/ (function(module, exports) {
 
 module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
   return (_vm.hasPDF) ? _c('div', {
@@ -12338,24 +12326,17 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
     return _c('page', {
       key: index,
       attrs: {
-        "v-if": !_vm.isLoading,
+        "v-if": !_vm.loading,
         "page": page,
         "number": index + 1
       }
     })
   })) : _vm._e()
 },staticRenderFns: []}
-module.exports.render._withStripped = true
-if (false) {
-  module.hot.accept()
-  if (module.hot.data) {
-     require("vue-hot-reload-api").rerender("data-v-7c7bed7e", module.exports)
-  }
-}
 
 /***/ }),
-/* 20 */
-/***/ (function(module, exports, __webpack_require__) {
+/* 18 */
+/***/ (function(module, exports) {
 
 module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;
   return _c('canvas', {
@@ -12366,32 +12347,25 @@ module.exports={render:function (){var _vm=this;var _h=_vm.$createElement;var _c
     }
   })
 },staticRenderFns: []}
-module.exports.render._withStripped = true
-if (false) {
-  module.hot.accept()
-  if (module.hot.data) {
-     require("vue-hot-reload-api").rerender("data-v-7e912b1a", module.exports)
-  }
-}
 
 /***/ }),
-/* 21 */
+/* 19 */
 /***/ (function(module, exports, __webpack_require__) {
 
 // style-loader: Adds some css to the DOM by adding a <style> tag
 
 // load the styles
-var content = __webpack_require__(12);
+var content = __webpack_require__(11);
 if(typeof content === 'string') content = [[module.i, content, '']];
 if(content.locals) module.exports = content.locals;
 // add the styles to the DOM
-var update = __webpack_require__(5)("8018213c", content, false);
+var update = __webpack_require__(5)("59cf066f", content, true);
 // Hot Module Replacement
 if(false) {
  // When the styles change, update the <style> tags
  if(!content.locals) {
-   module.hot.accept("!!../node_modules/css-loader/index.js!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7c7bed7e\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue", function() {
-     var newContent = require("!!../node_modules/css-loader/index.js!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7c7bed7e\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue");
+   module.hot.accept("!!../node_modules/css-loader/index.js?minimize!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7c7bed7e\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue", function() {
+     var newContent = require("!!../node_modules/css-loader/index.js?minimize!../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7c7bed7e\",\"scoped\":false,\"hasInlineConfig\":false}!../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue");
      if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
      update(newContent);
    });
@@ -12401,23 +12375,23 @@ if(false) {
 }
 
 /***/ }),
-/* 22 */
+/* 20 */
 /***/ (function(module, exports, __webpack_require__) {
 
 // style-loader: Adds some css to the DOM by adding a <style> tag
 
 // load the styles
-var content = __webpack_require__(13);
+var content = __webpack_require__(12);
 if(typeof content === 'string') content = [[module.i, content, '']];
 if(content.locals) module.exports = content.locals;
 // add the styles to the DOM
-var update = __webpack_require__(5)("6d9dea59", content, false);
+var update = __webpack_require__(5)("09f1e2d8", content, true);
 // Hot Module Replacement
 if(false) {
  // When the styles change, update the <style> tags
  if(!content.locals) {
-   module.hot.accept("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7e912b1a\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue", function() {
-     var newContent = require("!!../../node_modules/css-loader/index.js!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7e912b1a\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue");
+   module.hot.accept("!!../../node_modules/css-loader/index.js?minimize!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7e912b1a\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue", function() {
+     var newContent = require("!!../../node_modules/css-loader/index.js?minimize!../../node_modules/vue-loader/lib/style-compiler/index.js?{\"id\":\"data-v-7e912b1a\",\"scoped\":false,\"hasInlineConfig\":false}!../../node_modules/vue-loader/lib/selector.js?type=styles&index=0!./index.vue");
      if(typeof newContent === 'string') newContent = [[module.id, newContent, '']];
      update(newContent);
    });
@@ -12427,7 +12401,7 @@ if(false) {
 }
 
 /***/ }),
-/* 23 */
+/* 21 */
 /***/ (function(module, exports) {
 
 /**
@@ -12460,7 +12434,7 @@ module.exports = function listToStyles (parentId, list) {
 
 
 /***/ }),
-/* 24 */
+/* 22 */
 /***/ (function(module, exports) {
 
 var g;
@@ -12486,6 +12460,25 @@ try {
 module.exports = g;
 
 
+/***/ }),
+/* 23 */
+/***/ (function(module, exports, __webpack_require__) {
+
+"use strict";
+
+
+var PDF = __webpack_require__(6);
+var pdfjsLib = __webpack_require__(2);
+
+module.exports = {
+  install: function install(_vue) {
+    var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
+
+    pdfjsLib.PDFJS.workerSrc = options.workerSrc || '';
+    _vue.component('pdf-lab', PDF);
+  }
+};
+
 /***/ })
 /******/ ]);
 });
\ No newline at end of file
diff --git a/vendor/gitlab-ci-yml/CONTRIBUTING.md b/vendor/gitlab-ci-yml/CONTRIBUTING.md
new file mode 100644
index 0000000000000000000000000000000000000000..6e5160a24877987a15f837876dd615fee577e66f
--- /dev/null
+++ b/vendor/gitlab-ci-yml/CONTRIBUTING.md
@@ -0,0 +1,5 @@
+The canonical repository for `.gitlab-ci.yml` templates is
+https://gitlab.com/gitlab-org/gitlab-ci-yml.
+
+GitLab only mirrors the templates. Please submit your merge requests to 
+https://gitlab.com/gitlab-org/gitlab-ci-yml.