diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index f34340fc21c24d12a364d362093e3f5dd7254a27..99e0d1ed987332a0f10efa6e9d7cafa625ffd15d 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.119.0
+0.120.0
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c3ca147e850e098c7c72f997894deb516cef0917
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/editor_header.vue
@@ -0,0 +1,78 @@
+<script>
+import $ from 'jquery';
+import { mapActions } from 'vuex';
+import { __ } from '~/locale';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import ChangedFileIcon from '../changed_file_icon.vue';
+
+export default {
+  components: {
+    FileIcon,
+    ChangedFileIcon,
+  },
+  props: {
+    activeFile: {
+      type: Object,
+      required: true,
+    },
+  },
+  computed: {
+    activeButtonText() {
+      return this.activeFile.staged ? __('Unstage') : __('Stage');
+    },
+    isStaged() {
+      return !this.activeFile.changed && this.activeFile.staged;
+    },
+  },
+  methods: {
+    ...mapActions(['stageChange', 'unstageChange']),
+    actionButtonClicked() {
+      if (this.activeFile.staged) {
+        this.unstageChange(this.activeFile.path);
+      } else {
+        this.stageChange(this.activeFile.path);
+      }
+    },
+    showDiscardModal() {
+      $(document.getElementById(`discard-file-${this.activeFile.path}`)).modal('show');
+    },
+  },
+};
+</script>
+
+<template>
+  <div class="d-flex ide-commit-editor-header align-items-center">
+    <file-icon
+      :file-name="activeFile.name"
+      :size="16"
+      class="mr-2"
+    />
+    <strong class="mr-2">
+      {{ activeFile.path }}
+    </strong>
+    <changed-file-icon
+      :file="activeFile"
+    />
+    <div class="ml-auto">
+      <button
+        v-if="!isStaged"
+        type="button"
+        class="btn btn-remove btn-inverted append-right-8"
+        @click="showDiscardModal"
+      >
+        {{ __('Discard') }}
+      </button>
+      <button
+        :class="{
+          'btn-success': !isStaged,
+          'btn-warning': isStaged
+        }"
+        type="button"
+        class="btn btn-inverted"
+        @click="actionButtonClicked"
+      >
+        {{ activeButtonText }}
+      </button>
+    </div>
+  </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index d0fb0e3d99e249e8cb2df5cfba026f84ab744f82..3fdd35ad228d7c109755401b792b6714644a8b3a 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -1,7 +1,9 @@
 <script>
+import $ from 'jquery';
 import { mapActions } from 'vuex';
 import { __, sprintf } from '~/locale';
 import Icon from '~/vue_shared/components/icon.vue';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
 import tooltip from '~/vue_shared/directives/tooltip';
 import ListItem from './list_item.vue';
 
@@ -9,6 +11,7 @@ export default {
   components: {
     Icon,
     ListItem,
+    GlModal,
   },
   directives: {
     tooltip,
@@ -56,6 +59,11 @@ export default {
       type: String,
       required: true,
     },
+    emptyStateText: {
+      type: String,
+      required: false,
+      default: __('No changes'),
+    },
   },
   computed: {
     titleText() {
@@ -68,11 +76,19 @@ export default {
     },
   },
   methods: {
-    ...mapActions(['stageAllChanges', 'unstageAllChanges']),
+    ...mapActions(['stageAllChanges', 'unstageAllChanges', 'discardAllChanges']),
     actionBtnClicked() {
       this[this.action]();
+
+      $(this.$refs.actionBtn).tooltip('hide');
+    },
+    openDiscardModal() {
+      $('#discard-all-changes').modal('show');
     },
   },
+  discardModalText: __(
+    "You will loose all the unstaged changes you've made in this project. This action cannot be undone.",
+  ),
 };
 </script>
 
@@ -81,27 +97,32 @@ export default {
     class="ide-commit-list-container"
   >
     <header
-      class="multi-file-commit-panel-header"
+      class="multi-file-commit-panel-header d-flex mb-0"
     >
       <div
-        class="multi-file-commit-panel-header-title"
+        class="d-flex align-items-center flex-fill"
       >
         <icon
           v-once
           :name="iconName"
           :size="18"
+          class="append-right-8"
         />
-        {{ titleText }}
+        <strong>
+          {{ titleText }}
+        </strong>
         <div class="d-flex ml-auto">
           <button
             v-tooltip
-            v-show="filesLength"
+            ref="actionBtn"
+            :title="actionBtnText"
+            :aria-label="actionBtnText"
+            :disabled="!filesLength"
             :class="{
-              'd-flex': filesLength
+              'disabled-content': !filesLength
             }"
-            :title="actionBtnText"
             type="button"
-            class="btn btn-default ide-staged-action-btn p-0 order-1 align-items-center"
+            class="d-flex ide-staged-action-btn p-0 border-0 align-items-center"
             data-placement="bottom"
             data-container="body"
             data-boundary="viewport"
@@ -109,18 +130,32 @@ export default {
           >
             <icon
               :name="actionBtnIcon"
-              :size="12"
+              :size="16"
               class="ml-auto mr-auto"
             />
           </button>
-          <span
+          <button
+            v-tooltip
+            v-if="!stagedList"
+            :title="__('Discard all changes')"
+            :aria-label="__('Discard all changes')"
+            :disabled="!filesLength"
             :class="{
-              'rounded-right': !filesLength
+              'disabled-content': !filesLength
             }"
-            class="ide-commit-file-count order-0 rounded-left text-center"
+            type="button"
+            class="d-flex ide-staged-action-btn p-0 border-0 align-items-center"
+            data-placement="bottom"
+            data-container="body"
+            data-boundary="viewport"
+            @click="openDiscardModal"
           >
-            {{ filesLength }}
-          </span>
+            <icon
+              :size="16"
+              name="remove-all"
+              class="ml-auto mr-auto"
+            />
+          </button>
         </div>
       </div>
     </header>
@@ -143,9 +178,19 @@ export default {
     </ul>
     <p
       v-else
-      class="multi-file-commit-list form-text text-muted"
+      class="multi-file-commit-list form-text text-muted text-center"
     >
-      {{ __('No changes') }}
+      {{ emptyStateText }}
     </p>
+    <gl-modal
+      v-if="!stagedList"
+      id="discard-all-changes"
+      :footer-primary-button-text="__('Discard all changes')"
+      :header-title-text="__('Discard all unstaged changes?')"
+      footer-primary-button-variant="danger"
+      @submit="discardAllChanges"
+    >
+      {{ $options.discardModalText }}
+    </gl-modal>
   </div>
 </template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 391004dcd3c6b4cc6a2b5f71f485b23c1de72629..10c78a8030247045b8c651d72856958cdc756542 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -2,6 +2,7 @@
 import { mapActions } from 'vuex';
 import tooltip from '~/vue_shared/directives/tooltip';
 import Icon from '~/vue_shared/components/icon.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
 import StageButton from './stage_button.vue';
 import UnstageButton from './unstage_button.vue';
 import { viewerTypes } from '../../constants';
@@ -12,6 +13,7 @@ export default {
     Icon,
     StageButton,
     UnstageButton,
+    FileIcon,
   },
   directives: {
     tooltip,
@@ -48,7 +50,7 @@ export default {
       return `${getCommitIconMap(this.file).icon}${suffix}`;
     },
     iconClass() {
-      return `${getCommitIconMap(this.file).class} append-right-8`;
+      return `${getCommitIconMap(this.file).class} ml-auto mr-auto`;
     },
     fullKey() {
       return `${this.keyPrefix}-${this.file.key}`;
@@ -105,17 +107,24 @@ export default {
       @click="openFileInEditor"
     >
       <span class="multi-file-commit-list-file-path d-flex align-items-center">
-        <icon
-          :name="iconName"
-          :size="16"
-          :css-classes="iconClass"
+        <file-icon
+          :file-name="file.name"
+          class="append-right-8"
         />{{ file.name }}
       </span>
+      <div class="ml-auto d-flex align-items-center">
+        <div class="d-flex align-items-center ide-commit-list-changed-icon">
+          <icon
+            :name="iconName"
+            :size="16"
+            :css-classes="iconClass"
+          />
+        </div>
+        <component
+          :is="actionComponent"
+          :path="file.path"
+        />
+      </div>
     </div>
-    <component
-      :is="actionComponent"
-      :path="file.path"
-      class="d-flex position-absolute"
-    />
   </div>
 </template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
index e6044401c9fd5757647bad428794f58119bc8866..8a1836a5c92d66d2757e12c1ca57e1a6e952a4c8 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/stage_button.vue
@@ -1,11 +1,15 @@
 <script>
+import $ from 'jquery';
 import { mapActions } from 'vuex';
+import { sprintf, __ } from '~/locale';
 import Icon from '~/vue_shared/components/icon.vue';
 import tooltip from '~/vue_shared/directives/tooltip';
+import GlModal from '~/vue_shared/components/gl_modal.vue';
 
 export default {
   components: {
     Icon,
+    GlModal,
   },
   directives: {
     tooltip,
@@ -16,8 +20,22 @@ export default {
       required: true,
     },
   },
+  computed: {
+    modalId() {
+      return `discard-file-${this.path}`;
+    },
+    modalTitle() {
+      return sprintf(
+        __('Discard changes to %{path}?'),
+        { path: this.path },
+      );
+    },
+  },
   methods: {
     ...mapActions(['stageChange', 'discardFileChanges']),
+    showDiscardModal() {
+      $(document.getElementById(this.modalId)).modal('show');
+    },
   },
 };
 </script>
@@ -25,51 +43,50 @@ export default {
 <template>
   <div
     v-once
-    class="multi-file-discard-btn dropdown"
+    class="multi-file-discard-btn d-flex"
   >
     <button
       v-tooltip
       :aria-label="__('Stage changes')"
       :title="__('Stage changes')"
       type="button"
-      class="btn btn-blank append-right-5 d-flex align-items-center"
+      class="btn btn-blank align-items-center"
       data-container="body"
       data-boundary="viewport"
       data-placement="bottom"
-      @click.stop="stageChange(path)"
+      @click.stop.prevent="stageChange(path)"
     >
       <icon
-        :size="12"
+        :size="16"
         name="mobile-issue-close"
+        class="ml-auto mr-auto"
       />
     </button>
     <button
       v-tooltip
-      :title="__('More actions')"
+      :aria-label="__('Discard changes')"
+      :title="__('Discard changes')"
       type="button"
-      class="btn btn-blank d-flex align-items-center"
+      class="btn btn-blank align-items-center"
       data-container="body"
       data-boundary="viewport"
       data-placement="bottom"
-      data-toggle="dropdown"
-      data-display="static"
+      @click.stop.prevent="showDiscardModal"
     >
       <icon
-        :size="12"
-        name="ellipsis_h"
+        :size="16"
+        name="remove"
+        class="ml-auto mr-auto"
       />
     </button>
-    <div class="dropdown-menu dropdown-menu-right">
-      <ul>
-        <li>
-          <button
-            type="button"
-            @click.stop="discardFileChanges(path)"
-          >
-            {{ __('Discard changes') }}
-          </button>
-        </li>
-      </ul>
-    </div>
+    <gl-modal
+      :id="modalId"
+      :header-title-text="modalTitle"
+      :footer-primary-button-text="__('Discard changes')"
+      footer-primary-button-variant="danger"
+      @submit="discardFileChanges(path)"
+    >
+      {{ __("You will loose all changes you've made to this file. This action cannot be undone.") }}
+    </gl-modal>
   </div>
 </template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
index 9cec73ec00e8ab0ea6b40d9c32c23af9cd797531..86c4060207464ff8a3b612d277e72fec56ab8cd1 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/unstage_button.vue
@@ -25,22 +25,23 @@ export default {
 <template>
   <div
     v-once
-    class="multi-file-discard-btn"
+    class="multi-file-discard-btn d-flex"
   >
     <button
       v-tooltip
       :aria-label="__('Unstage changes')"
       :title="__('Unstage changes')"
       type="button"
-      class="btn btn-blank d-flex align-items-center"
+      class="btn btn-blank align-items-center"
       data-container="body"
       data-boundary="viewport"
       data-placement="bottom"
-      @click="unstageChange(path)"
+      @click.stop.prevent="unstageChange(path)"
     >
       <icon
-        :size="12"
-        name="history"
+        :size="16"
+        name="redo"
+        class="ml-auto mr-auto"
       />
     </button>
   </div>
diff --git a/app/assets/javascripts/ide/components/file_templates/bar.vue b/app/assets/javascripts/ide/components/file_templates/bar.vue
new file mode 100644
index 0000000000000000000000000000000000000000..23be5f45f1656c138da0a0c5421f5f3b6cc72f5b
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_templates/bar.vue
@@ -0,0 +1,80 @@
+<script>
+import { mapActions, mapGetters, mapState } from 'vuex';
+import Dropdown from './dropdown.vue';
+
+export default {
+  components: {
+    Dropdown,
+  },
+  computed: {
+    ...mapGetters(['activeFile']),
+    ...mapGetters('fileTemplates', ['templateTypes']),
+    ...mapState('fileTemplates', ['selectedTemplateType', 'updateSuccess']),
+    showTemplatesDropdown() {
+      return Object.keys(this.selectedTemplateType).length > 0;
+    },
+  },
+  watch: {
+    activeFile: 'setInitialType',
+  },
+  mounted() {
+    this.setInitialType();
+  },
+  methods: {
+    ...mapActions('fileTemplates', [
+      'setSelectedTemplateType',
+      'fetchTemplate',
+      'undoFileTemplate',
+    ]),
+    setInitialType() {
+      const initialTemplateType = this.templateTypes.find(t => t.name === this.activeFile.name);
+
+      if (initialTemplateType) {
+        this.setSelectedTemplateType(initialTemplateType);
+      }
+    },
+    selectTemplateType(templateType) {
+      this.setSelectedTemplateType(templateType);
+    },
+    selectTemplate(template) {
+      this.fetchTemplate(template);
+    },
+    undo() {
+      this.undoFileTemplate();
+    },
+  },
+};
+</script>
+
+<template>
+  <div class="d-flex align-items-center ide-file-templates">
+    <strong class="append-right-default">
+      {{ __('File templates') }}
+    </strong>
+    <dropdown
+      :data="templateTypes"
+      :label="selectedTemplateType.name || __('Choose a type...')"
+      class="mr-2"
+      @click="selectTemplateType"
+    />
+    <dropdown
+      v-if="showTemplatesDropdown"
+      :label="__('Choose a template...')"
+      :is-async-data="true"
+      :searchable="true"
+      :title="__('File templates')"
+      class="mr-2"
+      @click="selectTemplate"
+    />
+    <transition name="fade">
+      <button
+        v-show="updateSuccess"
+        type="button"
+        class="btn btn-default"
+        @click="undo"
+      >
+        {{ __('Undo') }}
+      </button>
+    </transition>
+  </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
new file mode 100644
index 0000000000000000000000000000000000000000..13059937f852219c6a0187b64a9f3d8a8665cba2
--- /dev/null
+++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
@@ -0,0 +1,125 @@
+<script>
+import $ from 'jquery';
+import { mapActions, mapState } from 'vuex';
+import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
+import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
+
+export default {
+  components: {
+    DropdownButton,
+    LoadingIcon,
+  },
+  props: {
+    data: {
+      type: Array,
+      required: false,
+      default: () => [],
+    },
+    label: {
+      type: String,
+      required: true,
+    },
+    title: {
+      type: String,
+      required: false,
+      default: null,
+    },
+    isAsyncData: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+    searchable: {
+      type: Boolean,
+      required: false,
+      default: false,
+    },
+  },
+  data() {
+    return {
+      search: '',
+    };
+  },
+  computed: {
+    ...mapState('fileTemplates', ['templates', 'isLoading']),
+    outputData() {
+      return (this.isAsyncData ? this.templates : this.data).filter(t => {
+        if (!this.searchable) return true;
+
+        return t.name.toLowerCase().indexOf(this.search.toLowerCase()) >= 0;
+      });
+    },
+    showLoading() {
+      return this.isAsyncData ? this.isLoading : false;
+    },
+  },
+  mounted() {
+    $(this.$el).on('show.bs.dropdown', this.fetchTemplatesIfAsync);
+  },
+  beforeDestroy() {
+    $(this.$el).off('show.bs.dropdown', this.fetchTemplatesIfAsync);
+  },
+  methods: {
+    ...mapActions('fileTemplates', ['fetchTemplateTypes']),
+    fetchTemplatesIfAsync() {
+      if (this.isAsyncData) {
+        this.fetchTemplateTypes();
+      }
+    },
+    clickItem(item) {
+      this.$emit('click', item);
+    },
+  },
+};
+</script>
+
+<template>
+  <div class="dropdown">
+    <dropdown-button
+      :toggle-text="label"
+      data-display="static"
+    />
+    <div class="dropdown-menu pb-0">
+      <div
+        v-if="title"
+        class="dropdown-title ml-0 mr-0"
+      >
+        {{ title }}
+      </div>
+      <div
+        v-if="!showLoading && searchable"
+        class="dropdown-input"
+      >
+        <input
+          v-model="search"
+          :placeholder="__('Filter...')"
+          type="search"
+          class="dropdown-input-field"
+        />
+        <i
+          aria-hidden="true"
+          class="fa fa-search dropdown-input-search"
+        ></i>
+      </div>
+      <div class="dropdown-content">
+        <loading-icon
+          v-if="showLoading"
+          size="2"
+        />
+        <ul v-else>
+          <li
+            v-for="(item, index) in outputData"
+            :key="index"
+          >
+            <button
+              type="button"
+              @click="clickItem(item)"
+            >
+              {{ item.name }}
+            </button>
+          </li>
+        </ul>
+      </div>
+    </div>
+  </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 6a5ab35a16a062a6427c0b35018a4d2bfb9f5025..a3add3b778ff0435d6c3e149d1116eebbd76154b 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -10,6 +10,7 @@ import RepoEditor from './repo_editor.vue';
 import FindFile from './file_finder/index.vue';
 import RightPane from './panes/right.vue';
 import ErrorMessage from './error_message.vue';
+import CommitEditorHeader from './commit_sidebar/editor_header.vue';
 
 const originalStopCallback = Mousetrap.stopCallback;
 
@@ -23,6 +24,7 @@ export default {
     FindFile,
     RightPane,
     ErrorMessage,
+    CommitEditorHeader,
   },
   computed: {
     ...mapState([
@@ -34,7 +36,7 @@ export default {
       'currentProjectId',
       'errorMessage',
     ]),
-    ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges']),
+    ...mapGetters(['activeFile', 'hasChanges', 'someUncommitedChanges', 'isCommitModeActive']),
   },
   mounted() {
     window.onbeforeunload = e => this.onBeforeUnload(e);
@@ -96,7 +98,12 @@ export default {
         <template
           v-if="activeFile"
         >
+          <commit-editor-header
+            v-if="isCommitModeActive"
+            :active-file="activeFile"
+          />
           <repo-tabs
+            v-else
             :active-file="activeFile"
             :files="openFiles"
             :viewer="viewer"
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index e500ef0e1b5c1ac39c7dad8dfe1d962517936b33..bcd53ac1ba2ed42cc1dd3081ffbe913eafb2767a 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,6 +1,7 @@
 <script>
+import $ from 'jquery';
 import { __ } from '~/locale';
-import { mapActions, mapState } from 'vuex';
+import { mapActions, mapState, mapGetters } from 'vuex';
 import GlModal from '~/vue_shared/components/gl_modal.vue';
 import { modalTypes } from '../../constants';
 
@@ -15,6 +16,7 @@ export default {
   },
   computed: {
     ...mapState(['entryModal']),
+    ...mapGetters('fileTemplates', ['templateTypes']),
     entryName: {
       get() {
         if (this.entryModal.type === modalTypes.rename) {
@@ -31,7 +33,9 @@ export default {
       if (this.entryModal.type === modalTypes.tree) {
         return __('Create new directory');
       } else if (this.entryModal.type === modalTypes.rename) {
-        return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
+        return this.entryModal.entry.type === modalTypes.tree
+          ? __('Rename folder')
+          : __('Rename file');
       }
 
       return __('Create new file');
@@ -40,11 +44,16 @@ export default {
       if (this.entryModal.type === modalTypes.tree) {
         return __('Create directory');
       } else if (this.entryModal.type === modalTypes.rename) {
-        return this.entryModal.entry.type === modalTypes.tree ? __('Rename folder') : __('Rename file');
+        return this.entryModal.entry.type === modalTypes.tree
+          ? __('Rename folder')
+          : __('Rename file');
       }
 
       return __('Create file');
     },
+    isCreatingNew() {
+      return this.entryModal.type !== modalTypes.rename;
+    },
   },
   methods: {
     ...mapActions(['createTempEntry', 'renameEntry']),
@@ -61,6 +70,14 @@ export default {
         });
       }
     },
+    createFromTemplate(template) {
+      this.createTempEntry({
+        name: template.name,
+        type: this.entryModal.type,
+      });
+
+      $('#ide-new-entry').modal('toggle');
+    },
     focusInput() {
       this.$refs.fieldName.focus();
     },
@@ -77,6 +94,7 @@ export default {
     :header-title-text="modalTitle"
     :footer-primary-button-text="buttonLabel"
     footer-primary-button-variant="success"
+    modal-size="lg"
     @submit="submitForm"
     @open="focusInput"
     @closed="closedModal"
@@ -84,16 +102,35 @@ export default {
     <div
       class="form-group row"
     >
-      <label class="label-bold col-form-label col-sm-3">
+      <label class="label-bold col-form-label col-sm-2">
         {{ __('Name') }}
       </label>
-      <div class="col-sm-9">
+      <div class="col-sm-10">
         <input
           ref="fieldName"
           v-model="entryName"
           type="text"
           class="form-control"
+          placeholder="/dir/file_name"
         />
+        <ul
+          v-if="isCreatingNew"
+          class="prepend-top-default list-inline"
+        >
+          <li
+            v-for="(template, index) in templateTypes"
+            :key="index"
+            class="list-inline-item"
+          >
+            <button
+              type="button"
+              class="btn btn-missing p-1 pr-2 pl-2"
+              @click="createFromTemplate(template)"
+            >
+              {{ template.name }}
+            </button>
+          </li>
+        </ul>
       </div>
     </div>
   </gl-modal>
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 6f1a941fbc4940d612228193c1fc1634b70ae5fe..d3b24c5b7934fff09c4bd13b27a906b9c4ac3511 100644
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -95,8 +95,9 @@ export default {
         :file-list="changedFiles"
         :action-btn-text="__('Stage all changes')"
         :active-file-key="activeFileKey"
+        :empty-state-text="__('There are no unstaged changes')"
         action="stageAllChanges"
-        action-btn-icon="mobile-issue-close"
+        action-btn-icon="stage-all"
         item-action-component="stage-button"
         class="is-first"
         icon-name="unstaged"
@@ -108,8 +109,9 @@ export default {
         :action-btn-text="__('Unstage all changes')"
         :staged-list="true"
         :active-file-key="activeFileKey"
+        :empty-state-text="__('There are no staged changes')"
         action="unstageAllChanges"
-        action-btn-icon="history"
+        action-btn-icon="unstage-all"
         item-action-component="unstage-button"
         icon-name="staged"
       />
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f55aa843444b7e41def9a0d89433016a52000890..d3a73e84cc7d6216ab932055cc121a566a902cd4 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -6,12 +6,14 @@ import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
 import { activityBarViews, viewerTypes } from '../constants';
 import Editor from '../lib/editor';
 import ExternalLink from './external_link.vue';
+import FileTemplatesBar from './file_templates/bar.vue';
 
 export default {
   components: {
     ContentViewer,
     DiffViewer,
     ExternalLink,
+    FileTemplatesBar,
   },
   props: {
     file: {
@@ -34,6 +36,7 @@ export default {
       'isCommitModeActive',
       'isReviewModeActive',
     ]),
+    ...mapGetters('fileTemplates', ['showFileTemplatesBar']),
     shouldHideEditor() {
       return this.file && this.file.binary && !this.file.content;
     },
@@ -216,7 +219,7 @@ export default {
     id="ide"
     class="blob-viewer-container blob-editor-container"
   >
-    <div class="ide-mode-tabs clearfix" >
+    <div class="ide-mode-tabs clearfix">
       <ul
         v-if="!shouldHideEditor && isEditModeActive"
         class="nav-links float-left"
@@ -249,6 +252,9 @@ export default {
         :file="file"
       />
     </div>
+    <file-templates-bar
+      v-if="showFileTemplatesBar(file.name)"
+    />
     <div
       v-show="!shouldHideEditor && file.viewMode ==='editor'"
       ref="editor"
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index aa02dfbddc4ac5be2fa99aad2163a23f82693add..b8b64aead302907a58cb219744547b879b3371dd 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -4,6 +4,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
 import flash from '~/flash';
 import * as types from './mutation_types';
 import FilesDecoratorWorker from './workers/files_decorator_worker';
+import { stageKeys } from '../constants';
 
 export const redirectToUrl = (_, url) => visitUrl(url);
 
@@ -122,14 +123,28 @@ export const scrollToTab = () => {
   });
 };
 
-export const stageAllChanges = ({ state, commit }) => {
+export const stageAllChanges = ({ state, commit, dispatch }) => {
+  const openFile = state.openFiles[0];
+
   commit(types.SET_LAST_COMMIT_MSG, '');
 
   state.changedFiles.forEach(file => commit(types.STAGE_CHANGE, file.path));
+
+  dispatch('openPendingTab', {
+    file: state.stagedFiles.find(f => f.path === openFile.path),
+    keyPrefix: stageKeys.staged,
+  });
 };
 
-export const unstageAllChanges = ({ state, commit }) => {
+export const unstageAllChanges = ({ state, commit, dispatch }) => {
+  const openFile = state.openFiles[0];
+
   state.stagedFiles.forEach(file => commit(types.UNSTAGE_CHANGE, file.path));
+
+  dispatch('openPendingTab', {
+    file: state.changedFiles.find(f => f.path === openFile.path),
+    keyPrefix: stageKeys.unstaged,
+  });
 };
 
 export const updateViewer = ({ commit }, viewer) => {
@@ -206,6 +221,7 @@ export const resetOpenFiles = ({ commit }) => commit(types.RESET_OPEN_FILES);
 
 export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath = null }) => {
   const entry = state.entries[entryPath || path];
+
   commit(types.RENAME_ENTRY, { path, name, entryPath });
 
   if (entry.type === 'tree') {
@@ -214,7 +230,7 @@ export const renameEntry = ({ dispatch, commit, state }, { path, name, entryPath
     );
   }
 
-  if (!entryPath) {
+  if (!entryPath && !entry.tempFile) {
     dispatch('deleteEntry', path);
   }
 };
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 28b9d0df20142225f14834a40e3a00af9a2214d2..30dcf7ef4dfd5a0124e38533601a4c6778eba0d9 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -5,7 +5,7 @@ import service from '../../services';
 import * as types from '../mutation_types';
 import router from '../../ide_router';
 import { setPageTitle } from '../utils';
-import { viewerTypes } from '../../constants';
+import { viewerTypes, stageKeys } from '../../constants';
 
 export const closeFile = ({ commit, state, dispatch }, file) => {
   const { path } = file;
@@ -208,8 +208,9 @@ export const discardFileChanges = ({ dispatch, state, commit, getters }, path) =
   eventHub.$emit(`editor.update.model.dispose.unstaged-${file.key}`, file.content);
 };
 
-export const stageChange = ({ commit, state }, path) => {
+export const stageChange = ({ commit, state, dispatch }, path) => {
   const stagedFile = state.stagedFiles.find(f => f.path === path);
+  const openFile = state.openFiles.find(f => f.path === path);
 
   commit(types.STAGE_CHANGE, path);
   commit(types.SET_LAST_COMMIT_MSG, '');
@@ -217,21 +218,39 @@ export const stageChange = ({ commit, state }, path) => {
   if (stagedFile) {
     eventHub.$emit(`editor.update.model.new.content.staged-${stagedFile.key}`, stagedFile.content);
   }
+
+  if (openFile && openFile.active) {
+    const file = state.stagedFiles.find(f => f.path === path);
+
+    dispatch('openPendingTab', {
+      file,
+      keyPrefix: stageKeys.staged,
+    });
+  }
 };
 
-export const unstageChange = ({ commit }, path) => {
+export const unstageChange = ({ commit, dispatch, state }, path) => {
+  const openFile = state.openFiles.find(f => f.path === path);
+
   commit(types.UNSTAGE_CHANGE, path);
+
+  if (openFile && openFile.active) {
+    const file = state.changedFiles.find(f => f.path === path);
+
+    dispatch('openPendingTab', {
+      file,
+      keyPrefix: stageKeys.unstaged,
+    });
+  }
 };
 
-export const openPendingTab = ({ commit, getters, dispatch, state }, { file, keyPrefix }) => {
+export const openPendingTab = ({ commit, getters, state }, { file, keyPrefix }) => {
   if (getters.activeFile && getters.activeFile.key === `${keyPrefix}-${file.key}`) return false;
 
   state.openFiles.forEach(f => eventHub.$emit(`editor.update.model.dispose.${f.key}`));
 
   commit(types.ADD_PENDING_TAB, { file, keyPrefix });
 
-  dispatch('scrollToTab');
-
   router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`);
 
   return true;
diff --git a/app/assets/javascripts/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index a601dc8f5a0b726b8fe2bd65af23dfe3494b261a..877d88bb060f25f8ffaf90c0412d56e8820ae60b 100644
--- a/app/assets/javascripts/ide/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
@@ -8,6 +8,7 @@ import commitModule from './modules/commit';
 import pipelines from './modules/pipelines';
 import mergeRequests from './modules/merge_requests';
 import branches from './modules/branches';
+import fileTemplates from './modules/file_templates';
 
 Vue.use(Vuex);
 
@@ -22,6 +23,7 @@ export const createStore = () =>
       pipelines,
       mergeRequests,
       branches,
+      fileTemplates: fileTemplates(),
     },
   });
 
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
index 43237a294663bce690cf7f2c219c1f4ca62d6ee2..dd53213ed18a4ff734635780faa6fef60e77782c 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/actions.js
@@ -1,6 +1,7 @@
 import Api from '~/api';
 import { __ } from '~/locale';
 import * as types from './mutation_types';
+import eventHub from '../../../eventhub';
 
 export const requestTemplateTypes = ({ commit }) => commit(types.REQUEST_TEMPLATE_TYPES);
 export const receiveTemplateTypesError = ({ commit, dispatch }) => {
@@ -31,9 +32,23 @@ export const fetchTemplateTypes = ({ dispatch, state }) => {
     .catch(() => dispatch('receiveTemplateTypesError'));
 };
 
-export const setSelectedTemplateType = ({ commit }, type) =>
+export const setSelectedTemplateType = ({ commit, dispatch, rootGetters }, type) => {
   commit(types.SET_SELECTED_TEMPLATE_TYPE, type);
 
+  if (rootGetters.activeFile.prevPath === type.name) {
+    dispatch('discardFileChanges', rootGetters.activeFile.path, { root: true });
+  } else if (rootGetters.activeFile.name !== type.name) {
+    dispatch(
+      'renameEntry',
+      {
+        path: rootGetters.activeFile.path,
+        name: type.name,
+      },
+      { root: true },
+    );
+  }
+};
+
 export const receiveTemplateError = ({ dispatch }, template) => {
   dispatch(
     'setErrorMessage',
@@ -69,6 +84,7 @@ export const setFileTemplate = ({ dispatch, commit, rootGetters }, template) =>
     { root: true },
   );
   commit(types.SET_UPDATE_SUCCESS, true);
+  eventHub.$emit(`editor.update.model.new.content.${rootGetters.activeFile.key}`, template.content);
 };
 
 export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => {
@@ -76,6 +92,12 @@ export const undoFileTemplate = ({ dispatch, commit, rootGetters }) => {
 
   dispatch('changeFileContent', { path: file.path, content: file.raw }, { root: true });
   commit(types.SET_UPDATE_SUCCESS, false);
+
+  eventHub.$emit(`editor.update.model.new.content.${file.key}`, file.raw);
+
+  if (file.prevPath) {
+    dispatch('discardFileChanges', file.path, { root: true });
+  }
 };
 
 // prevent babel-plugin-rewire from generating an invalid default during karma tests
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
index 38318fd49bf6517bb5b3a528585e1e50bafe450a..628babe6a01976c6a96dc26501931cdbddeda35d 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/getters.js
@@ -1,3 +1,5 @@
+import { activityBarViews } from '../../../constants';
+
 export const templateTypes = () => [
   {
     name: '.gitlab-ci.yml',
@@ -17,7 +19,8 @@ export const templateTypes = () => [
   },
 ];
 
-export const showFileTemplatesBar = (_, getters) => name =>
-  getters.templateTypes.find(t => t.name === name);
+export const showFileTemplatesBar = (_, getters, rootState) => name =>
+  getters.templateTypes.find(t => t.name === name) &&
+  rootState.currentActivityView === activityBarViews.edit;
 
 export default () => {};
diff --git a/app/assets/javascripts/ide/stores/modules/file_templates/index.js b/app/assets/javascripts/ide/stores/modules/file_templates/index.js
index dfa5ef54413240ed89c4df1c7545fb994c4fa0c6..383ff5db392e3abde781a9cf1cbc16bbb0fd9424 100644
--- a/app/assets/javascripts/ide/stores/modules/file_templates/index.js
+++ b/app/assets/javascripts/ide/stores/modules/file_templates/index.js
@@ -3,10 +3,10 @@ import * as actions from './actions';
 import * as getters from './getters';
 import mutations from './mutations';
 
-export default {
+export default () => ({
   namespaced: true,
   actions,
   state: createState(),
   getters,
   mutations,
-};
+});
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index f2bb87ac6741ef364c67d517772062b4d0c64bd9..2c8535bda591cb6446c376476adda99160e14470 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,3 +1,4 @@
+import Vue from 'vue';
 import * as types from './mutation_types';
 import projectMutations from './mutations/project';
 import mergeRequestMutation from './mutations/merge_request';
@@ -226,7 +227,7 @@ export default {
       path: newPath,
       name: entryPath ? oldEntry.name : name,
       tempFile: true,
-      prevPath: oldEntry.path,
+      prevPath: oldEntry.tempFile ? null : oldEntry.path,
       url: oldEntry.url.replace(new RegExp(`${oldEntry.path}/?$`), newPath),
       tree: [],
       parentPath,
@@ -245,6 +246,20 @@ export default {
     if (newEntry.type === 'blob') {
       state.changedFiles = state.changedFiles.concat(newEntry);
     }
+
+    if (state.entries[newPath].opened) {
+      state.openFiles.push(state.entries[newPath]);
+    }
+
+    if (oldEntry.tempFile) {
+      const filterMethod = f => f.path !== oldEntry.path;
+
+      state.openFiles = state.openFiles.filter(filterMethod);
+      state.changedFiles = state.changedFiles.filter(filterMethod);
+      parent.tree = parent.tree.filter(filterMethod);
+
+      Vue.delete(state.entries, oldEntry.path);
+    }
   },
   ...projectMutations,
   ...mergeRequestMutation,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 66f298248986910be8dbe98ce5346ad2a32ed60b..6ca246c1d63a1b1237ba855eb2c2e4603002228c 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -55,7 +55,7 @@ export default {
       f => f.path === file.path && f.pending && !(f.tempFile && !f.prevPath),
     );
 
-    if (file.tempFile) {
+    if (file.tempFile && file.content === '') {
       Object.assign(state.entries[file.path], {
         content: raw,
       });
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index e5680a0499f35aac3984857fbf41ca2dbdc2d531..a13f30e6079a0684d9fbf10ad13c25b2fb692184 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -82,11 +82,12 @@ export default {
         value: 0,
       },
       currentXCoordinate: 0,
-      currentCoordinates: [],
+      currentCoordinates: {},
       showFlag: false,
       showFlagContent: false,
       timeSeries: [],
       realPixelRatio: 1,
+      seriesUnderMouse: [],
     };
   },
   computed: {
@@ -126,6 +127,9 @@ export default {
     this.draw();
   },
   methods: {
+    showDot(path) {
+      return this.showFlagContent && this.seriesUnderMouse.includes(path);
+    },
     draw() {
       const breakpointSize = bp.getBreakpointSize();
       const query = this.graphData.queries[0];
@@ -155,7 +159,24 @@ export default {
       point.y = e.clientY;
       point = point.matrixTransform(this.$refs.graphData.getScreenCTM().inverse());
       point.x += 7;
-      const firstTimeSeries = this.timeSeries[0];
+
+      this.seriesUnderMouse = this.timeSeries.filter((series) => {
+        const mouseX = series.timeSeriesScaleX.invert(point.x);
+        let minDistance = Infinity;
+
+        const closestTickMark = Object.keys(this.allXAxisValues).reduce((closest, x) => {
+          const distance = Math.abs(Number(new Date(x)) - Number(mouseX));
+          if (distance < minDistance) {
+            minDistance = distance;
+            return x;
+          }
+          return closest;
+        });
+
+        return series.values.find(v => v.time.toString() === closestTickMark);
+      });
+
+      const firstTimeSeries = this.seriesUnderMouse[0];
       const timeValueOverlay = firstTimeSeries.timeSeriesScaleX.invert(point.x);
       const overlayIndex = bisectDate(firstTimeSeries.values, timeValueOverlay, 1);
       const d0 = firstTimeSeries.values[overlayIndex - 1];
@@ -190,6 +211,17 @@ export default {
       axisXScale.domain(d3.extent(allValues, d => d.time));
       axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
 
+      this.allXAxisValues = this.timeSeries.reduce((obj, series) => {
+        const seriesKeys = {};
+        series.values.forEach(v => {
+          seriesKeys[v.time] = true;
+        });
+        return {
+          ...obj,
+          ...seriesKeys,
+        };
+      }, {});
+
       const xAxis = d3
         .axisBottom()
         .scale(axisXScale)
@@ -277,9 +309,8 @@ export default {
             :line-style="path.lineStyle"
             :line-color="path.lineColor"
             :area-color="path.areaColor"
-            :current-coordinates="currentCoordinates[index]"
-            :current-time-series-index="index"
-            :show-dot="showFlagContent"
+            :current-coordinates="currentCoordinates[path.metricTag]"
+            :show-dot="showDot(path)"
           />
           <graph-deployment
             :deployment-data="reducedDeploymentData"
@@ -303,7 +334,7 @@ export default {
         :graph-height="graphHeight"
         :graph-height-offset="graphHeightOffset"
         :show-flag-content="showFlagContent"
-        :time-series="timeSeries"
+        :time-series="seriesUnderMouse"
         :unit-of-display="unitOfDisplay"
         :legend-title="legendTitle"
         :deployment-flag-data="deploymentFlagData"
diff --git a/app/assets/javascripts/monitoring/components/graph/flag.vue b/app/assets/javascripts/monitoring/components/graph/flag.vue
index e00882526bcc9b65ad4812d279c719fe611c636c..f603ba6498dafa10502e7552465ba1e84e148cd0 100644
--- a/app/assets/javascripts/monitoring/components/graph/flag.vue
+++ b/app/assets/javascripts/monitoring/components/graph/flag.vue
@@ -52,7 +52,7 @@ export default {
       required: true,
     },
     currentCoordinates: {
-      type: Array,
+      type: Object,
       required: true,
     },
   },
@@ -91,8 +91,8 @@ export default {
   },
   methods: {
     seriesMetricValue(seriesIndex, series) {
-      const indexFromCoordinates = this.currentCoordinates[seriesIndex]
-      ? this.currentCoordinates[seriesIndex].currentDataIndex : 0;
+      const indexFromCoordinates = this.currentCoordinates[series.metricTag]
+      ? this.currentCoordinates[series.metricTag].currentDataIndex : 0;
       const index = this.deploymentFlagData
         ? this.deploymentFlagData.seriesIndex
         : indexFromCoordinates;
diff --git a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
index 4f23814ff3e07a760f5d5aae7e2966a11500d4b3..007451d5c7a07ced345eb8989faed0327b1936e4 100644
--- a/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
+++ b/app/assets/javascripts/monitoring/mixins/monitoring_mixins.js
@@ -50,19 +50,24 @@ const mixins = {
     },
 
     positionFlag() {
-      const timeSeries = this.timeSeries[0];
-      const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate, 1);
+      const timeSeries = this.seriesUnderMouse[0];
+      if (!timeSeries) {
+        return;
+      }
+      const hoveredDataIndex = bisectDate(timeSeries.values, this.hoverData.hoveredDate);
 
       this.currentData = timeSeries.values[hoveredDataIndex];
       this.currentXCoordinate = Math.floor(timeSeries.timeSeriesScaleX(this.currentData.time));
 
-      this.currentCoordinates = this.timeSeries.map((series) => {
-        const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate, 1);
+      this.currentCoordinates = {};
+
+      this.seriesUnderMouse.forEach((series) => {
+        const currentDataIndex = bisectDate(series.values, this.hoverData.hoveredDate);
         const currentData = series.values[currentDataIndex];
         const currentX = Math.floor(series.timeSeriesScaleX(currentData.time));
         const currentY = Math.floor(series.timeSeriesScaleY(currentData.value));
 
-        return {
+        this.currentCoordinates[series.metricTag] = {
           currentX,
           currentY,
           currentDataIndex,
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index cee39fd0559e316cff92d91efec5c1c0a9e15b03..eff0d7325cd11b591b6dbb8970849f041acd9625 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -2,7 +2,7 @@ import _ from 'underscore';
 import { scaleLinear, scaleTime } from 'd3-scale';
 import { line, area, curveLinear } from 'd3-shape';
 import { extent, max, sum } from 'd3-array';
-import { timeMinute } from 'd3-time';
+import { timeMinute, timeSecond } from 'd3-time';
 import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
 
 const d3 = {
@@ -14,6 +14,7 @@ const d3 = {
   extent,
   max,
   timeMinute,
+  timeSecond,
   sum,
 };
 
@@ -51,6 +52,24 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
     return defaultColorPalette[pick];
   }
 
+  function findByDate(series, time) {
+    const val = series.find(v => Math.abs(d3.timeSecond.count(time, v.time)) < 60);
+    if (val) {
+      return val.value;
+    }
+    return NaN;
+  }
+
+  // The timeseries data may have gaps in it
+  // but we need a regularly-spaced set of time/value pairs
+  // this gives us a complete range of one minute intervals
+  // offset the same amount as the original data
+  const [minX, maxX] = xDom;
+  const offset = d3.timeMinute(minX) - Number(minX);
+  const datesWithoutGaps = d3.timeSecond.every(60)
+    .range(d3.timeMinute.offset(minX, -1), maxX)
+    .map(d => d - offset);
+
   query.result.forEach((timeSeries, timeSeriesNumber) => {
     let metricTag = '';
     let lineColor = '';
@@ -119,9 +138,14 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
       });
     }
 
+    const values = datesWithoutGaps.map(time => ({
+      time,
+      value: findByDate(timeSeries.values, time),
+    }));
+
     timeSeriesParsed.push({
-      linePath: lineFunction(timeSeries.values),
-      areaPath: areaFunction(timeSeries.values),
+      linePath: lineFunction(values),
+      areaPath: areaFunction(values),
       timeSeriesScaleX,
       timeSeriesScaleY,
       values: timeSeries.values,
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 563a731ece443f846cfb08938c33d6187322d549..e07214c54cfa882925465a10aac3270ea2a06c23 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -166,6 +166,10 @@
       @include btn-outline($white-light, $red-500, $red-500, $red-500, $white-light, $red-600, $red-600, $red-700);
     }
 
+    &.btn-warning {
+      @include btn-outline($white-light, $orange-500, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700);
+    }
+
     &.btn-primary,
     &.btn-info {
       @include btn-outline($white-light, $blue-500, $blue-500, $blue-500, $white-light, $blue-600, $blue-600, $blue-700);
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 5ff4e487d04619d6aa4a60628dc2162b553be935..45df8391f9a3cdc4e1fb0dd99eeac9825b1c2273 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -7,6 +7,8 @@ $ide-context-header-padding: 10px;
 $ide-project-avatar-end: $ide-context-header-padding + 48px;
 $ide-tree-padding: $gl-padding;
 $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
+$ide-commit-row-height: 32px;
+$ide-commit-header-height: 48px;
 
 .project-refs-form,
 .project-refs-target-form {
@@ -567,24 +569,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
 }
 
 .multi-file-commit-panel-header {
-  display: flex;
-  align-items: center;
-  margin-bottom: 0;
+  height: $ide-commit-header-height;
   border-bottom: 1px solid $white-dark;
   padding: 12px 0;
 }
 
-.multi-file-commit-panel-header-title {
-  display: flex;
-  flex: 1;
-  align-items: center;
-
-  svg {
-    margin-right: $gl-btn-padding;
-    color: $theme-gray-700;
-  }
-}
-
 .multi-file-commit-panel-collapse-btn {
   border-left: 1px solid $white-dark;
   margin-left: auto;
@@ -594,8 +583,6 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
   flex: 1;
   overflow: auto;
   padding: $grid-size 0;
-  margin-left: -$grid-size;
-  margin-right: -$grid-size;
   min-height: 60px;
 
   &.form-text.text-muted {
@@ -660,6 +647,8 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
 
 .multi-file-commit-list-path {
   cursor: pointer;
+  height: $ide-commit-row-height;
+  padding-right: 0;
 
   &.is-active {
     background-color: $white-normal;
@@ -668,6 +657,12 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
   &:hover,
   &:focus {
     outline: 0;
+
+    .multi-file-discard-btn {
+      > .btn {
+        display: flex;
+      }
+    }
   }
 
   svg {
@@ -679,6 +674,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
 
 .multi-file-commit-list-file-path {
   @include str-truncated(calc(100% - 30px));
+  user-select: none;
 
   &:active {
     text-decoration: none;
@@ -686,9 +682,11 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
 }
 
 .multi-file-discard-btn {
-  top: 4px;
-  right: 8px;
-  bottom: 4px;
+  > .btn {
+    display: none;
+    width: $ide-commit-row-height;
+    height: $ide-commit-row-height;
+  }
 
   svg {
     top: 0;
@@ -807,10 +805,9 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
 }
 
 .ide-staged-action-btn {
-  width: 22px;
-  margin-left: -1px;
-  border-top-left-radius: 0;
-  border-bottom-left-radius: 0;
+  width: $ide-commit-row-height;
+  height: $ide-commit-row-height;
+  color: inherit;
 
   > svg {
     top: 0;
@@ -1442,3 +1439,29 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
   top: 50%;
   transform: translateY(-50%);
 }
+
+.ide-file-templates {
+  padding: $grid-size $gl-padding;
+  background-color: $gray-light;
+  border-bottom: 1px solid $white-dark;
+
+  .dropdown {
+    min-width: 180px;
+  }
+
+  .dropdown-content {
+    max-height: 222px;
+  }
+}
+
+.ide-commit-editor-header {
+  height: 65px;
+  padding: 8px 16px;
+  background-color: $theme-gray-50;
+  box-shadow: inset 0 -1px $white-dark;
+}
+
+.ide-commit-list-changed-icon {
+  width: $ide-commit-row-height;
+  height: $ide-commit-row-height;
+}
diff --git a/app/controllers/admin/logs_controller.rb b/app/controllers/admin/logs_controller.rb
index 6a7af9edeec388384afee2daee8d923496703e52..5861f7b9d3dd56af7f1180a06e789ba01eb55a84 100644
--- a/app/controllers/admin/logs_controller.rb
+++ b/app/controllers/admin/logs_controller.rb
@@ -14,7 +14,8 @@ def loggers
       Gitlab::GitLogger,
       Gitlab::EnvironmentLogger,
       Gitlab::SidekiqLogger,
-      Gitlab::RepositoryCheckLogger
+      Gitlab::RepositoryCheckLogger,
+      Gitlab::ProjectServiceLogger
     ]
   end
 end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 6f50cbb4a36bd25b45f17a5f67723b5a46cd0337..5671663f81eb153b4cfdd562ef10a7e4372a18b7 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -101,6 +101,7 @@ def user_params
       :organization,
       :preferred_language,
       :private_profile,
+      :include_private_contributions,
       status: [:emoji, :message]
     )
   end
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 48e02581d54ebbf59f13bbea489bc71396b9fa62..34de554212f25dd2119ed6e79483ffee1487aed9 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -21,6 +21,8 @@ def diff_for_path
   def render_diffs
     @environment = @merge_request.environments_for(current_user).last
 
+    @diffs.write_cache
+
     render json: DiffsSerializer.new(current_user: current_user).represent(@diffs, additional_attributes)
   end
 
diff --git a/app/finders/user_recent_events_finder.rb b/app/finders/user_recent_events_finder.rb
index 876f086a3ef0d02dbbd74b7bb2a5982e8a91da6d..b874f6959c9e8dac78667ea1c3c2653742862e05 100644
--- a/app/finders/user_recent_events_finder.rb
+++ b/app/finders/user_recent_events_finder.rb
@@ -48,20 +48,6 @@ def target_events
   end
 
   def projects
-    # Compile a list of projects `current_user` interacted with
-    # and `target_user` is allowed to see.
-
-    authorized = target_user
-      .project_interactions
-      .joins(:project_authorizations)
-      .where(project_authorizations: { user: current_user })
-      .select(:id)
-
-    visible = target_user
-      .project_interactions
-      .where(visibility_level: Gitlab::VisibilityLevel.levels_for_user(current_user))
-      .select(:id)
-
-    Gitlab::SQL::Union.new([authorized, visible]).to_sql
+    target_user.project_interactions.to_sql
   end
 end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 269acf5b2e2ccfaf12bd46c5daed4eb2b529d9f4..34d54e2d68164d1420dd24b80b6a36001325f033 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -19,7 +19,7 @@ def link_to_author(event, self_added: false)
       name = self_added ? 'You' : author.name
       link_to name, user_path(author.username), title: name
     else
-      event.author_name
+      escape_once(event.author_name)
     end
   end
 
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 3b3d4faf07fa1cd6e23d667b068e5dd2d12494e4..3e44b4eac9e871acb90f66ba14ad4722e5f48f92 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -111,6 +111,10 @@ def locking_enabled?
     def allows_multiple_assignees?
       false
     end
+
+    def has_multiple_assignees?
+      assignees.count > 1
+    end
   end
 
   class_methods do
diff --git a/app/models/concerns/project_services_loggable.rb b/app/models/concerns/project_services_loggable.rb
new file mode 100644
index 0000000000000000000000000000000000000000..248a21f357878e0244845079e0d51c0858cff6be
--- /dev/null
+++ b/app/models/concerns/project_services_loggable.rb
@@ -0,0 +1,26 @@
+module ProjectServicesLoggable
+  def log_info(message, params = {})
+    message = build_message(message, params)
+
+    logger.info(message)
+  end
+
+  def log_error(message, params = {})
+    message = build_message(message, params)
+
+    logger.error(message)
+  end
+
+  def build_message(message, params = {})
+    {
+      service_class: self.class.name,
+      project_id: project.id,
+      project_path: project.full_path,
+      message: message
+    }.merge(params)
+  end
+
+  def logger
+    Gitlab::ProjectServiceLogger
+  end
+end
diff --git a/app/models/event.rb b/app/models/event.rb
index e37dd5322ea93ea5366408c78c5bd138ff85c7e1..351576f038efd0e527af953e4d906f249b3ad484 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -157,15 +157,17 @@ def visible_to_user?(user = nil)
     if push? || commit_note?
       Ability.allowed?(user, :download_code, project)
     elsif membership_changed?
-      true
+      Ability.allowed?(user, :read_project, project)
     elsif created_project?
-      true
+      Ability.allowed?(user, :read_project, project)
     elsif issue? || issue_note?
       Ability.allowed?(user, :read_issue, note? ? note_target : target)
     elsif merge_request? || merge_request_note?
       Ability.allowed?(user, :read_merge_request, note? ? note_target : target)
+    elsif milestone?
+      Ability.allowed?(user, :read_project, project)
     else
-      milestone?
+      false # No other event types are visible
     end
   end
 
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 35c19049c04003556006d72a4b757060f70317b2..568f870c2db4c0c79986bb17e3c1f909334e8fc2 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -101,7 +101,7 @@ def check_commit(message, push_msg)
           task.update(completed: true)
         end
       rescue => e
-        Rails.logger.error(e.message)
+        log_error(e.message)
         next
       end
     end
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index a783a314071f736e6dd8bf49da630dc22e4baa57..a15780c14f96e609ac8fbe93b1e247949d6be07e 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -104,7 +104,7 @@ def format_channel(recipient)
         new_recipient = URI.join(default_irc_uri, '/', recipient).to_s
         uri = consider_uri(URI.parse(new_recipient))
       rescue
-        Rails.logger.error("Unable to create a valid URL from #{default_irc_uri} and #{recipient}")
+        log_error("Unable to create a valid URL", default_irc_uri: default_irc_uri, recipient: recipient)
       end
     end
 
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 2b867256cb36a7590ac59205895c7bc23d312f31..1c2b2a8fc5d584cfbf855df7e7a9606481c1786e 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -92,7 +92,7 @@ def execute(data)
     rescue Gitlab::HTTP::Error, Timeout::Error, SocketError, Errno::ECONNRESET, Errno::ECONNREFUSED, OpenSSL::SSL::SSLError => error
       message = "#{self.type} had an error when trying to connect to #{self.project_url}: #{error.message}"
     end
-    Rails.logger.info(message)
+    log_info(message)
     result
   end
 
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index cc98b3f5a411b94cd4bbfb033c6ac3b0c7417ffa..ba7fcb0cf930ab69f8940b08a2b02771fd7c5594 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -205,7 +205,7 @@ def transition_issue(issue)
       begin
         issue.transitions.build.save!(transition: { id: transition_id })
       rescue => error
-        Rails.logger.info "#{self.class.name} Issue Transition failed message ERROR: #{client_url} - #{error.message}"
+        log_error("Issue transition failed", error: error.message, client_url: client_url)
         return false
       end
     end
@@ -257,9 +257,8 @@ def send_message(issue, message, remote_link_props)
         new_remote_link.save!(remote_link_props)
       end
 
-      result_message = "#{self.class.name} SUCCESS: Successfully posted to #{client_url}."
-      Rails.logger.info(result_message)
-      result_message
+      log_info("Successfully posted", client_url: client_url)
+      "SUCCESS: Successfully posted to http://jira.example.net."
     end
   end
 
@@ -317,7 +316,7 @@ def jira_request
 
   rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET, Errno::ECONNREFUSED, URI::InvalidURIError, JIRA::HTTPError, OpenSSL::SSL::SSLError => e
     @error = e.message
-    Rails.logger.info "#{self.class.name} Send message ERROR: #{client_url} - #{@error}"
+    log_error("Error sending message", client_url: client_url, error: @error)
     nil
   end
 
diff --git a/app/models/service.rb b/app/models/service.rb
index 17d3f26377358741bcd9bc1c784844ab0f21099b..55c31e3c3540c9acd51c4953b9483d25d898acdb 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -6,6 +6,7 @@ class Service < ActiveRecord::Base
   prepend EE::Service
   include Sortable
   include Importable
+  include ProjectServicesLoggable
 
   serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
 
diff --git a/app/services/merge_requests/reload_diffs_service.rb b/app/services/merge_requests/reload_diffs_service.rb
index 8d85dc9eb5f146cd8fac1eaaef75660e824af74a..1390ae0e199c6cc76bfb8cc45608b24a6d2bcf91 100644
--- a/app/services/merge_requests/reload_diffs_service.rb
+++ b/app/services/merge_requests/reload_diffs_service.rb
@@ -30,7 +30,7 @@ def update_diff_discussion_positions(old_diff_refs)
     def clear_cache(new_diff)
       # Executing the iteration we cache highlighted diffs for each diff file of
       # MergeRequestDiff.
-      new_diff.diffs_collection.diff_files.to_a
+      new_diff.diffs_collection.write_cache
 
       # Remove cache for all diffs on this MR. Do not use the association on the
       # model, as that will interfere with other actions happening when
@@ -38,7 +38,7 @@ def clear_cache(new_diff)
       MergeRequestDiff.where(merge_request: merge_request).each do |merge_request_diff|
         next if merge_request_diff == new_diff
 
-        merge_request_diff.diffs_collection.clear_cache!
+        merge_request_diff.diffs_collection.clear_cache
       end
     end
   end
diff --git a/app/services/wikis/create_attachment_service.rb b/app/services/wikis/create_attachment_service.rb
index 30fe0e371a6c1f48fef5bc7ec6e77b8d9185170f..df31ad7c8eaed8fae4a3e40dbbde7f3b90ac69d5 100644
--- a/app/services/wikis/create_attachment_service.rb
+++ b/app/services/wikis/create_attachment_service.rb
@@ -11,7 +11,7 @@ class CreateAttachmentService < Files::CreateService
     def initialize(*args)
       super
 
-      @file_name = truncate_file_name(params[:file_name])
+      @file_name = clean_file_name(params[:file_name])
       @file_path = File.join(ATTACHMENT_PATH, SecureRandom.hex, @file_name) if @file_name
       @commit_message ||= "Upload attachment #{@file_name}"
       @branch_name ||= wiki.default_branch
@@ -23,8 +23,16 @@ def create_commit!
 
     private
 
-    def truncate_file_name(file_name)
+    def clean_file_name(file_name)
       return unless file_name.present?
+
+      file_name = truncate_file_name(file_name)
+      # CommonMark does not allow Urls with whitespaces, so we have to replace them
+      # Using the same regex Carrierwave use to replace invalid characters
+      file_name.gsub(CarrierWave::SanitizedFile.sanitize_regexp, '_')
+    end
+
+    def truncate_file_name(file_name)
       return file_name if file_name.length <= MAX_FILENAME_LENGTH
 
       extension = File.extname(file_name)
diff --git a/app/uploaders/namespace_file_uploader.rb b/app/uploaders/namespace_file_uploader.rb
index 52969762b7da1078cc8f49a4066bf19d56e4250b..b0154f85a5ce12c2bc58caddc97b60c410d96c63 100644
--- a/app/uploaders/namespace_file_uploader.rb
+++ b/app/uploaders/namespace_file_uploader.rb
@@ -6,8 +6,15 @@ def self.root
     options.storage_path
   end
 
-  def self.base_dir(model, _store = nil)
-    File.join(options.base_dir, 'namespace', model_path_segment(model))
+  def self.base_dir(model, store = nil)
+    base_dirs(model)[store || Store::LOCAL]
+  end
+
+  def self.base_dirs(model)
+    {
+      Store::LOCAL => File.join(options.base_dir, 'namespace', model_path_segment(model)),
+      Store::REMOTE => File.join('namespace', model_path_segment(model))
+    }
   end
 
   def self.model_path_segment(model)
@@ -18,11 +25,4 @@ def self.model_path_segment(model)
   def store_dir
     store_dirs[object_store]
   end
-
-  def store_dirs
-    {
-      Store::LOCAL => File.join(base_dir, dynamic_segment),
-      Store::REMOTE => File.join('namespace', self.class.model_path_segment(model), dynamic_segment)
-    }
-  end
 end
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 53a33adc14dcf20c62450e17e146618016c22465..5623f0f590a50b51089e17f09fc84b8282b2cac7 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -11,3 +11,5 @@
       = render "events/event/note", event: event
     - else
       = render "events/event/common", event: event
+- elsif @user.include_private_contributions?
+  = render "events/event/private", event: event
diff --git a/app/views/events/_event_scope.html.haml b/app/views/events/_event_scope.html.haml
index 8f7da7d8c4ffa203984affadd7127cfbb450c48a..989417224349386597d14ba4e89aee05eda6b5c5 100644
--- a/app/views/events/_event_scope.html.haml
+++ b/app/views/events/_event_scope.html.haml
@@ -1,7 +1,7 @@
 %span.event-scope
   = event_preposition(event)
   - if event.project
-    = link_to_project event.project
+    = link_to_project(event.project)
   - else
     = event.project_name
 
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index 01e72862114b40f56b6247116b975125a501e14a..829a3da15580c2132eb7280d157f396fc79d7826 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -1,7 +1,7 @@
 = icon_for_profile_event(event)
 
 .event-title
-  %span.author_name= link_to_author event
+  %span.author_name= link_to_author(event)
   %span{ class: event.action_name }
   - if event.target
     = event.action_name
diff --git a/app/views/events/event/_created_project.html.haml b/app/views/events/event/_created_project.html.haml
index d8e59be57bb422193385c0011a1c5ecdff1c25f4..6ad7e15713181ea79db69879bb052791889b9ada 100644
--- a/app/views/events/event/_created_project.html.haml
+++ b/app/views/events/event/_created_project.html.haml
@@ -1,11 +1,11 @@
 = icon_for_profile_event(event)
 
 .event-title
-  %span.author_name= link_to_author event
+  %span.author_name= link_to_author(event)
   %span{ class: event.action_name }
     = event_action_name(event)
 
   - if event.project
-    = link_to_project event.project
+    = link_to_project(event.project)
   - else
     = event.project_name
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index de6383e40979e722a962a9ffbe0889b776ab8d23..cdacd998a6987434c11780bbf5b92c5321406a95 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -1,7 +1,7 @@
 = icon_for_profile_event(event)
 
 .event-title
-  %span.author_name= link_to_author event
+  %span.author_name= link_to_author(event)
   = event.action_name
   = event_note_title_html(event)
 
diff --git a/app/views/events/event/_private.html.haml b/app/views/events/event/_private.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..ccd2aacb4eafa929930cc19d7d3cf93213b3a354
--- /dev/null
+++ b/app/views/events/event/_private.html.haml
@@ -0,0 +1,10 @@
+.event-inline.event-item
+  .event-item-timestamp
+    = time_ago_with_tooltip(event.created_at)
+
+  .system-note-image= sprite_icon('eye-slash', size: 16, css_class: 'icon')
+
+  .event-title
+    - author_name = capture do
+      %span.author_name= link_to_author(event)
+    = s_('Profiles|%{author_name} made a private contribution').html_safe % { author_name: author_name }
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 85f2d00bde31d642432528310f06253fabbd0017..5f0ee79cd9b1f0b77e08dce0d64e74cff8a0d170 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -3,7 +3,7 @@
 = icon_for_profile_event(event)
 
 .event-title
-  %span.author_name= link_to_author event
+  %span.author_name= link_to_author(event)
   %span.pushed #{event.action_name} #{event.ref_type}
   %strong
     - commits_link = project_commits_path(project, event.ref_name)
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 9f79feb4ddd4ed94ab2c21dddab109244ee8d08d..0a1ee648d9787b7bafb5cf3ba3a8104d21912f4f 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,5 +1,6 @@
-- breadcrumb_title "Edit Profile"
+- breadcrumb_title s_("Profiles|Edit Profile")
 - @content_class = "limit-container-width" unless fluid_layout
+- gravatar_link = link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host
 
 = bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f|
   = form_errors(@user)
@@ -7,34 +8,36 @@
   .row
     .col-lg-4.profile-settings-sidebar
       %h4.prepend-top-0
-        Public Avatar
+        = s_("Profiles|Public Avatar")
       %p
         - if @user.avatar?
-          You can change your avatar here
           - if gravatar_enabled?
-            or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host}
+            = s_("Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
+          - else
+            = s_("Profiles|You can change your avatar here")
         - else
-          You can upload an avatar here
           - if gravatar_enabled?
-            or change it at #{link_to Gitlab.config.gravatar.host, 'https://' + Gitlab.config.gravatar.host}
+            = s_("Profiles|You can upload your avatar here or change it at %{gravatar_link}").html_safe % { gravatar_link: gravatar_link }
+          - else
+            = s_("Profiles|You can upload your avatar here")
     .col-lg-8
       .clearfix.avatar-image.append-bottom-default
         = link_to avatar_icon_for_user(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
           = image_tag avatar_icon_for_user(@user, 160), alt: '', class: 'avatar s160'
-      %h5.prepend-top-0= _("Upload new avatar")
+      %h5.prepend-top-0= s_("Profiles|Upload new avatar")
       .prepend-top-5.append-bottom-10
-        %button.btn.js-choose-user-avatar-button{ type: 'button' }= _("Choose file...")
-        %span.avatar-file-name.prepend-left-default.js-avatar-filename= _("No file chosen")
+        %button.btn.js-choose-user-avatar-button{ type: 'button' }= s_("Profiles|Choose file...")
+        %span.avatar-file-name.prepend-left-default.js-avatar-filename= s_("Profiles|No file chosen")
         = f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
-      .form-text.text-muted= _("The maximum file size allowed is 200KB.")
+      .form-text.text-muted= s_("Profiles|The maximum file size allowed is 200KB.")
       - if @user.avatar?
         %hr
-        = link_to _('Remove avatar'), profile_avatar_path, data: { confirm: _('Avatar will be removed. Are you sure?') }, method: :delete, class: 'btn btn-danger btn-inverted'
+        = link_to s_("Profiles|Remove avatar"), profile_avatar_path, data: { confirm: s_("Profiles|Avatar will be removed. Are you sure?") }, method: :delete, class: 'btn btn-danger btn-inverted'
 
   %hr
   .row
     .col-lg-4.profile-settings-sidebar
-      %h4.prepend-top-0= s_("User|Current status")
+      %h4.prepend-top-0= s_("Profiles|Current status")
       %p= s_("Profiles|This emoji and message will appear on your profile and throughout the interface.")
     .col-lg-8
       = f.fields_for :status, @user.status do |status_form|
@@ -66,62 +69,66 @@
   .row
     .col-lg-4.profile-settings-sidebar
       %h4.prepend-top-0
-        Main settings
+        = s_("Profiles|Main settings")
       %p
-        This information will appear on your profile.
+        = s_("Profiles|This information will appear on your profile.")
         - if current_user.ldap_user?
-          Some options are unavailable for LDAP accounts
+          = s_("Profiles|Some options are unavailable for LDAP accounts")
     .col-lg-8
       .row
         - if @user.read_only_attribute?(:name)
           = f.text_field :name, required: true, readonly: true, wrapper: { class: 'col-md-9' },
-          help: "Your name was automatically set based on your #{ attribute_provider_label(:name) } account, so people you know can recognize you."
+          help: s_("Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you.") % { provider_label: attribute_provider_label(:name) }
         - else
           = f.text_field :name, label: 'Full name', required: true, wrapper: { class: 'col-md-9' }, help: "Enter your name, so people you know can recognize you."
         = f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' }
 
       - if @user.read_only_attribute?(:email)
-        = f.text_field :email, required: true, readonly: true, help: "Your email address was automatically set based on your #{ attribute_provider_label(:email) } account."
+        = f.text_field :email, required: true, readonly: true, help: s_("Profiles|Your email address was automatically set based on your %{provider_label} account.") % { provider_label: attribute_provider_label(:email) }
       - else
         = f.text_field :email, required: true, value: (@user.email unless @user.temp_oauth_email?),
           help: user_email_help_text(@user)
       = f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email),
-        { help: 'This email will be displayed on your public profile.', include_blank: 'Do not show on profile' },
+        { help: s_("Profiles|This email will be displayed on your public profile."), include_blank: s_("Profiles|Do not show on profile") },
         control_class: 'select2'
       = f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] },
-        { help: 'This feature is experimental and translations are not complete yet.' },
+        { help: s_("Profiles|This feature is experimental and translations are not complete yet.") },
         control_class: 'select2'
       = f.text_field :skype
       = f.text_field :linkedin
       = f.text_field :twitter
-      = f.text_field :website_url, label: 'Website'
+      = f.text_field :website_url, label: s_("Profiles|Website")
       - if @user.read_only_attribute?(:location)
-        = f.text_field :location, readonly: true, help: "Your location was automatically set based on your #{ attribute_provider_label(:location) } account."
+        = f.text_field :location, readonly: true, help: s_("Profiles|Your location was automatically set based on your %{provider_label} account.") % { provider_label: attribute_provider_label(:location) }
       - else
         = f.text_field :location
       = f.text_field :organization
-      = f.text_area :bio, rows: 4, maxlength: 250, help: 'Tell us about yourself in fewer than 250 characters.'
+      = f.text_area :bio, rows: 4, maxlength: 250, help: s_("Profiles|Tell us about yourself in fewer than 250 characters.")
       %hr
-      %h5 Private profile
+      %h5= ("Private profile")
       - private_profile_label = capture do
-        Don't display activity-related personal information on your profile
+        = s_("Profiles|Don't display activity-related personal information on your profiles")
         = link_to icon('question-circle'), help_page_path('user/profile/index.md', anchor: 'private-profile')
       = f.check_box :private_profile, label: private_profile_label
+      %h5= s_("Profiles|Private contributions")
+      = f.check_box :include_private_contributions, label: 'Include private contributions on my profile'
+      .help-block
+        = s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information.")
       .prepend-top-default.append-bottom-default
-        = f.submit 'Update profile settings', class: 'btn btn-success'
-        = link_to 'Cancel', user_path(current_user), class: 'btn btn-cancel'
+        = f.submit s_("Profiles|Update profile settings"), class: 'btn btn-success'
+        = link_to _("Cancel"), user_path(current_user), class: 'btn btn-cancel'
 
 .modal.modal-profile-crop
   .modal-dialog
     .modal-content
       .modal-header
         %h4.modal-title
-          Position and size your new avatar
-        %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
+          = s_("Profiles|Position and size your new avatar")
+        %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _("Close") }
           %span{ "aria-hidden": true } &times;
       .modal-body
         .profile-crop-image-container
-          %img.modal-profile-crop-image{ alt: 'Avatar cropper' }
+          %img.modal-profile-crop-image{ alt: s_("Profiles|Avatar cropper") }
         .crop-controls
           .btn-group
             %button.btn.btn-primary{ data: { method: 'zoom', option: '0.1' } }
@@ -130,4 +137,4 @@
               %span.fa.fa-search-minus
       .modal-footer
         %button.btn.btn-primary.js-upload-user-avatar{ type: 'button' }
-          Set new profile picture
+          = s_("Profiles|Set new profile picture")
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index d49273e7b2a3109aa340ba4e8fefbfefd4ee325d..553b2bf04e5fde15e1a9a745e3840114faa2fb78 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -20,17 +20,17 @@
         - if !membership_locked? && @project.allowed_to_share_with_group?
           %ul.nav-links.nav.nav-tabs.gitlab-tabs{ role: 'tablist' }
             %li.nav-tab{ role: 'presentation' }
-              %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' } Add member
+              %a.nav-link.active{ href: '#invite-member-pane', id: 'invite-member-tab', data: { toggle: 'tab' }, role: 'tab' } Invite member
             %li.nav-tab{ role: 'presentation', class: ('active' if membership_locked?) }
               %a.nav-link{ href: '#invite-group-pane', id: 'invite-group-tab', data: { toggle: 'tab' }, role: 'tab' } Invite group
 
           .tab-content.gitlab-tab-content
             .tab-pane.active{ id: 'invite-member-pane', role: 'tabpanel' }
-              = render 'projects/project_members/new_project_member', tab_title: 'Add member'
+              = render 'projects/project_members/new_project_member', tab_title: 'Invite member'
             .tab-pane{ id: 'invite-group-pane', role: 'tabpanel', class: ('active' if membership_locked?) }
               = render 'projects/project_members/new_project_group', tab_title: 'Invite group'
         - elsif !membership_locked?
-          .invite-member= render 'projects/project_members/new_project_member', tab_title: 'Add member'
+          .invite-member= render 'projects/project_members/new_project_member', tab_title: 'Invite member'
         - elsif @project.allowed_to_share_with_group?
           .invite-group= render 'projects/project_members/new_project_group', tab_title: 'Invite group'
 
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 2d4656e8608434608c007e33d935be31deccfba1..938cb579e9fbc5988f0be64d58b2f5383529bb46 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -1,6 +1,5 @@
 %h4.prepend-top-20
-  Contributions for
-  %strong= @calendar_date.to_s(:medium)
+  = _("Contributions for <strong>%{calendar_date}</strong>").html_safe % { calendar_date: @calendar_date.to_s(:medium) }
 
 - if @events.any?
   %ul.bordered-list
@@ -9,25 +8,28 @@
         %span.light
           %i.fa.fa-clock-o
           = event.created_at.strftime('%-I:%M%P')
-        - if event.push?
-          #{event.action_name} #{event.ref_type}
+        - if event.visible_to_user?(current_user)
+          - if event.push?
+            #{event.action_name} #{event.ref_type}
+            %strong
+              - commits_path = project_commits_path(event.project, event.ref_name)
+              = link_to_if event.project.repository.branch_exists?(event.ref_name), event.ref_name, commits_path
+          - else
+            = event_action_name(event)
+            %strong
+              - if event.note?
+                = link_to event.note_target.to_reference, event_note_target_url(event), class: 'has-tooltip', title: event.target_title
+              - elsif event.target
+                = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title
+
+          at
           %strong
-            - commits_path = project_commits_path(event.project, event.ref_name)
-            = link_to_if event.project.repository.branch_exists?(event.ref_name), event.ref_name, commits_path
+            - if event.project
+              = link_to_project(event.project)
+            - else
+              = event.project_name
         - else
-          = event_action_name(event)
-          %strong
-            - if event.note?
-              = link_to event.note_target.to_reference, event_note_target_url(event), class: 'has-tooltip', title: event.target_title
-            - elsif event.target
-              = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title
-
-        at
-        %strong
-          - if event.project
-            = link_to_project event.project
-          - else
-            = event.project_name
+          made a private contribution
 - else
   %p
-    No contributions found for #{@calendar_date.to_s(:medium)}
+    = _('No contributions were found')
diff --git a/app/workers/new_merge_request_worker.rb b/app/workers/new_merge_request_worker.rb
index 5d8b8904502bc80780570ceeaa7c1e933c5c3693..62f9d9b6f57e5513287a140ecd0a336089ec3819 100644
--- a/app/workers/new_merge_request_worker.rb
+++ b/app/workers/new_merge_request_worker.rb
@@ -9,6 +9,8 @@ def perform(merge_request_id, user_id)
 
     EventCreateService.new.open_mr(issuable, user)
     NotificationService.new.new_merge_request(issuable, user)
+
+    issuable.diffs.write_cache
     issuable.create_cross_references!(user)
   end
 
diff --git a/changelogs/unreleased/ee-6381-multiseries.yml b/changelogs/unreleased/ee-6381-multiseries.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a749e31d27ce50d9bdd0a4f878397a143895638b
--- /dev/null
+++ b/changelogs/unreleased/ee-6381-multiseries.yml
@@ -0,0 +1,5 @@
+---
+title: Allow gaps in multiseries metrics charts
+merge_request: 21427
+author:
+type: fixed
diff --git a/changelogs/unreleased/feat-update-contribution-calendar.yml b/changelogs/unreleased/feat-update-contribution-calendar.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4ada8b1fcd5482875aeea8a70c766564ee80bc14
--- /dev/null
+++ b/changelogs/unreleased/feat-update-contribution-calendar.yml
@@ -0,0 +1,5 @@
+---
+title: Include private contributions to contributions calendar
+merge_request: 17296
+author: George Tsiolis
+type: added
diff --git a/changelogs/unreleased/fix-namespace-uploader.yml b/changelogs/unreleased/fix-namespace-uploader.yml
new file mode 100644
index 0000000000000000000000000000000000000000..081adc9a6f165a30b11d17d0931aa74d6382d7e5
--- /dev/null
+++ b/changelogs/unreleased/fix-namespace-uploader.yml
@@ -0,0 +1,5 @@
+---
+title: Fix NamespaceUploader.base_dir for remote uploads
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-51194-fix-wiki-attachments-with-whitespaces.yml b/changelogs/unreleased/fj-51194-fix-wiki-attachments-with-whitespaces.yml
new file mode 100644
index 0000000000000000000000000000000000000000..a6464807e01388535c68be2027a366ac7413b6b8
--- /dev/null
+++ b/changelogs/unreleased/fj-51194-fix-wiki-attachments-with-whitespaces.yml
@@ -0,0 +1,5 @@
+---
+title: Replace white spaces in wiki attachments file names
+merge_request: 21569
+author:
+type: fixed
diff --git a/changelogs/unreleased/ide-commit-panel-improved.yml b/changelogs/unreleased/ide-commit-panel-improved.yml
new file mode 100644
index 0000000000000000000000000000000000000000..245214185e5ecebfeac456ba7ab7c69969e4ab82
--- /dev/null
+++ b/changelogs/unreleased/ide-commit-panel-improved.yml
@@ -0,0 +1,5 @@
+---
+title: Improved commit panel in Web IDE
+merge_request: 21471
+author:
+type: changed
diff --git a/changelogs/unreleased/ide-file-templates.yml b/changelogs/unreleased/ide-file-templates.yml
new file mode 100644
index 0000000000000000000000000000000000000000..68983670b25ba77ddd1d69387f9d94d6c7176eeb
--- /dev/null
+++ b/changelogs/unreleased/ide-file-templates.yml
@@ -0,0 +1,5 @@
+---
+title: Added file templates to the Web IDE
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/issue_50488.yml b/changelogs/unreleased/issue_50488.yml
new file mode 100644
index 0000000000000000000000000000000000000000..dad7ae55a0d8f80569b445bc543f124552c45e29
--- /dev/null
+++ b/changelogs/unreleased/issue_50488.yml
@@ -0,0 +1,5 @@
+---
+title: Move project services log to a separate file
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/osw-send-max-patch-bytes-to-gitaly.yml b/changelogs/unreleased/osw-send-max-patch-bytes-to-gitaly.yml
new file mode 100644
index 0000000000000000000000000000000000000000..3c50448e3ffb43b2d89c7a28fcedf583a677b77f
--- /dev/null
+++ b/changelogs/unreleased/osw-send-max-patch-bytes-to-gitaly.yml
@@ -0,0 +1,5 @@
+---
+title: Send max_patch_bytes to Gitaly via Gitaly::CommitDiffRequest
+merge_request: 21575
+author:
+type: other
diff --git a/changelogs/unreleased/osw-write-cache-upon-mr-creation-and-cache-refactoring.yml b/changelogs/unreleased/osw-write-cache-upon-mr-creation-and-cache-refactoring.yml
new file mode 100644
index 0000000000000000000000000000000000000000..4fba33decfa55016ce856f18b8274bfb989a0536
--- /dev/null
+++ b/changelogs/unreleased/osw-write-cache-upon-mr-creation-and-cache-refactoring.yml
@@ -0,0 +1,5 @@
+---
+title: Write diff highlighting cache upon MR creation (refactors caching)
+merge_request: 21489
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-remove-orphaned-label-links.yml b/changelogs/unreleased/sh-remove-orphaned-label-links.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b035b57ff1b009c4856044c8bec9741ef2c9f4bf
--- /dev/null
+++ b/changelogs/unreleased/sh-remove-orphaned-label-links.yml
@@ -0,0 +1,5 @@
+---
+title: Remove orphaned label links
+merge_request: 21552
+author:
+type: fixed
diff --git a/changelogs/unreleased/zj-cleanup-port-gitaly.yml b/changelogs/unreleased/zj-cleanup-port-gitaly.yml
new file mode 100644
index 0000000000000000000000000000000000000000..25e13b0fd503238d63c8235717962acc7e00d533
--- /dev/null
+++ b/changelogs/unreleased/zj-cleanup-port-gitaly.yml
@@ -0,0 +1,5 @@
+---
+title: Administrative cleanup rake tasks now leverage Gitaly
+merge_request: 21588
+author:
+type: changed
diff --git a/config/prometheus/common_metrics.yml b/config/prometheus/common_metrics.yml
index 8ef9af0a47358b7d787c7c49688058c61c0978f2..4ebd77ca7a1a40dfdafcd912e3c7560a051a7025 100644
--- a/config/prometheus/common_metrics.yml
+++ b/config/prometheus/common_metrics.yml
@@ -213,4 +213,3 @@
       label: Pod average
       unit: "cores"
       track: canary
-
diff --git a/db/migrate/20180228172924_add_include_private_contributions_to_users.rb b/db/migrate/20180228172924_add_include_private_contributions_to_users.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ea3ebdd83d1b91524f79629e85633e206666e63f
--- /dev/null
+++ b/db/migrate/20180228172924_add_include_private_contributions_to_users.rb
@@ -0,0 +1,7 @@
+class AddIncludePrivateContributionsToUsers < ActiveRecord::Migration
+  DOWNTIME = false
+
+  def change
+    add_column :users, :include_private_contributions, :boolean
+  end
+end
diff --git a/db/post_migrate/20180906051323_remove_orphaned_label_links.rb b/db/post_migrate/20180906051323_remove_orphaned_label_links.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b56b74f483ebf6db50cd8e4bbb1061c5a9733850
--- /dev/null
+++ b/db/post_migrate/20180906051323_remove_orphaned_label_links.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+class RemoveOrphanedLabelLinks < ActiveRecord::Migration
+  include Gitlab::Database::MigrationHelpers
+
+  DOWNTIME = false
+
+  disable_ddl_transaction!
+
+  class LabelLinks < ActiveRecord::Base
+    self.table_name = 'label_links'
+    include EachBatch
+
+    def self.orphaned
+      where('NOT EXISTS ( SELECT 1 FROM labels WHERE labels.id = label_links.label_id )')
+    end
+  end
+
+  def up
+    # Some of these queries can take up to 10 seconds to run on GitLab.com,
+    # which is pretty close to our 15 second statement timeout. To ensure a
+    # smooth deployment procedure we disable the statement timeouts for this
+    # migration, just in case.
+    disable_statement_timeout do
+      # On GitLab.com there are over 2,000,000 orphaned label links. On
+      # staging, removing 100,000 rows generated a max replication lag of 6.7
+      # MB. In total, removing all these rows will only generate about 136 MB
+      # of data, so it should be safe to do this.
+      LabelLinks.orphaned.each_batch(of: 100_000) do |batch|
+        batch.delete_all
+      end
+    end
+
+    add_concurrent_foreign_key(:label_links, :labels, column: :label_id, on_delete: :cascade)
+  end
+
+  def down
+    # There is no way to restore orphaned label links.
+    if foreign_key_exists?(:label_links, column: :label_id)
+      remove_foreign_key(:label_links, column: :label_id)
+    end
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 5392a1845785045e193475628ef137fa08c6a760..d91c7d9fcd3cf8f185449a5ddbd0de71a8cb49da 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2892,6 +2892,7 @@
     t.string "feed_token"
     t.boolean "private_profile"
     t.integer "roadmap_layout", limit: 2
+    t.boolean "include_private_contributions"
   end
 
   add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -3108,6 +3109,7 @@
   add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify
   add_foreign_key "issues", "users", column: "closed_by_id", name: "fk_c63cbf6c25", on_delete: :nullify
   add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify
+  add_foreign_key "label_links", "labels", name: "fk_d97dd08678", on_delete: :cascade
   add_foreign_key "label_priorities", "labels", on_delete: :cascade
   add_foreign_key "label_priorities", "projects", on_delete: :cascade
   add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 0fbb4481fb8ad14d530d9a3c34875948a0d22367..98134075b94b41e3c35c4ed3215b12838a403e49 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -113,6 +113,19 @@ October 07, 2014 11:25: User "Claudie Hodkiewicz" (nasir_stehr@olson.co.uk)  was
 October 07, 2014 11:25: Project "project133" was removed
 ```
 
+## `integrations_json.log`
+
+This file lives in `/var/log/gitlab/gitlab-rails/integrations_json.log` for
+Omnibus GitLab packages or in `/home/git/gitlab/log/integrations_json.log` for
+installations from source.
+
+It contains information about [integrations](../user/project/integrations/project_services.md) activities such as JIRA, Asana and Irker services. It uses JSON format like the example below:  
+
+``` json
+{"severity":"ERROR","time":"2018-09-06T14:56:20.439Z","service_class":"JiraService","project_id":8,"project_path":"h5bp/html5-boilerplate","message":"Error sending message","client_url":"http://jira.gitlap.com:8080","error":"execution expired"}
+{"severity":"INFO","time":"2018-09-06T17:15:16.365Z","service_class":"JiraService","project_id":3,"project_path":"namespace2/project2","message":"Successfully posted","client_url":"http://jira.example.net"}
+```
+
 ## `githost.log`
 
 This file lives in `/var/log/gitlab/gitlab-rails/githost.log` for
diff --git a/doc/development/prometheus_metrics.md b/doc/development/prometheus_metrics.md
index 6977815a979a9430814367ce3130b8ecd0361216..b6b6d9665ea1fccbb4a811f015d3e71da6e04511 100644
--- a/doc/development/prometheus_metrics.md
+++ b/doc/development/prometheus_metrics.md
@@ -6,8 +6,7 @@ We strive to support the 2-4 most important metrics for each common system servi
 
 ### Query identifier
 
-The requirement for adding a new metrics is to make each query to have an unique identifier.
-Identifier is used to update the metric later when changed.
+The requirement for adding a new metric is to make each query to have an unique identifier which is used to update the metric later when changed:
 
 ```yaml
 - group: Response metrics (NGINX Ingress)
@@ -25,9 +24,10 @@ Identifier is used to update the metric later when changed.
 
 After you add or change existing _common_ metric you have to create a new database migration that will query and update all existing metrics.
 
-**Note: If a query metric (which is identified by `id:`) is removed it will not be removed from database by default.**
-**You might want to add additional database migration that makes a decision what to do with removed one.**
-**For example: you might be interested in migrating all dependent data to a different metric.**
+NOTE: **Note:**
+If a query metric (which is identified by `id:`) is removed it will not be removed from database by default.
+You might want to add additional database migration that makes a decision what to do with removed one.
+For example: you might be interested in migrating all dependent data to a different metric.
 
 ```ruby
 class ImportCommonMetrics < ActiveRecord::Migration
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index b1b822f25bddc3743539e0fc0c40df98435ef3f4..6b225147232d324134cc9b1379594d5baaefdeb5 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -91,6 +91,18 @@ To enable private profile:
 NOTE: **Note:**
 You and GitLab admins can see your the abovementioned information on your profile even if it is private.
 
+## Private contributions
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/14078) in GitLab 11.3.
+
+Enabling private contributions will include contributions to private projects, in the user contribution calendar graph and user recent activity. 
+
+To enable private contributions:
+
+1. Navigate to your personal [profile settings](#profile-settings).
+2. Check the "Private contributions" option.
+3. Hit **Update profile settings**.
+
 ## Current status
 
 > Introduced in GitLab 11.2.
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index 4d6956144275506fa94fd4bb459c3b73e476231b..98305de041797975bfb40d1128deda3e34c628bc 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -78,13 +78,14 @@ switching to a different branch.
 
 The Web IDE can be used to preview JavaScript projects right in the browser.
 This feature uses CodeSandbox to compile and bundle the JavaScript used to
-preview the web application. On public projects, an `Open in CodeSandbox`
-button is visible which will transfer the contents of the project into a
-CodeSandbox project to share with others.
-**Note** this button is not visible on private or internal projects.
+preview the web application.
 
 ![Web IDE Client Side Evaluation](img/clientside_evaluation.png)
 
+Additionally, for public projects an `Open in CodeSandbox` button is available
+to transfer the contents of the project into a public CodeSandbox project to
+quickly share your project with others.
+
 ### Enabling Client Side Evaluation
 
 The Client Side Evaluation feature needs to be enabled in the GitLab instances
diff --git a/spec/features/projects/members/invite_group_and_members_spec.rb b/ee/spec/features/projects/members/invite_group_and_members_spec.rb
similarity index 69%
rename from spec/features/projects/members/invite_group_and_members_spec.rb
rename to ee/spec/features/projects/members/invite_group_and_members_spec.rb
index 36d3c14b720d999ac58c06c3d4dcf4c242bd5f53..6d175c148947bf01509b5713553b1fc109481adb 100644
--- a/spec/features/projects/members/invite_group_and_members_spec.rb
+++ b/ee/spec/features/projects/members/invite_group_and_members_spec.rb
@@ -1,12 +1,12 @@
 require 'spec_helper'
 
-describe 'Project > Members > Invite Group', :js do
+describe 'Project > Members > Invite group and members', :js do
   include Select2Helper
   include ActionView::Helpers::DateHelper
 
   let(:maintainer) { create(:user) }
 
-  describe 'Invite group lock' do
+  describe 'Share group lock' do
     shared_examples 'the project cannot be shared with groups' do
       it 'user is only able to share with members' do
         visit project_settings_members_path(project)
@@ -189,95 +189,4 @@
       end
     end
   end
-
-  describe 'setting an expiration date for a group link' do
-    let(:project) { create(:project) }
-    let!(:group) { create(:group) }
-
-    around do |example|
-      Timecop.freeze { example.run }
-    end
-
-    before do
-      project.add_maintainer(maintainer)
-      sign_in(maintainer)
-
-      visit project_settings_members_path(project)
-
-      click_on 'invite-group-tab'
-
-      select2 group.id, from: '#link_group_id'
-
-      fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d')
-      click_on 'invite-group-tab'
-      find('.btn-create').click
-    end
-
-    it 'the group link shows the expiration time with a warning class' do
-      page.within('.project-members-groups') do
-        # Using distance_of_time_in_words_to_now because it is not the same as
-        # subtraction, and this way avoids time zone issues as well
-        expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at)
-        expect(page).to have_content(expires_in_text)
-        expect(page).to have_selector('.text-warning')
-      end
-    end
-  end
-
-  describe 'the groups dropdown' do
-    context 'with multiple groups to choose from' do
-      let(:project) { create(:project) }
-
-      before do
-        project.add_maintainer(maintainer)
-        sign_in(maintainer)
-
-        create(:group).add_owner(maintainer)
-        create(:group).add_owner(maintainer)
-
-        visit project_settings_members_path(project)
-
-        click_link 'Invite group'
-
-        find('.ajax-groups-select.select2-container')
-
-        execute_script 'GROUP_SELECT_PER_PAGE = 1;'
-        open_select2 '#link_group_id'
-      end
-
-      it 'should infinitely scroll' do
-        expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
-
-        scroll_select2_to_bottom('.select2-drop .select2-results:visible')
-
-        expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
-      end
-    end
-
-    context 'for a project in a nested group' do
-      let(:group) { create(:group) }
-      let!(:nested_group) { create(:group, parent: group) }
-      let!(:group_to_share_with) { create(:group) }
-      let!(:project) { create(:project, namespace: nested_group) }
-
-      before do
-        project.add_maintainer(maintainer)
-        sign_in(maintainer)
-        group.add_maintainer(maintainer)
-        group_to_share_with.add_maintainer(maintainer)
-      end
-
-      it 'the groups dropdown does not show ancestors', :nested_groups do
-        visit project_settings_members_path(project)
-
-        click_on 'invite-group-tab'
-        click_link 'Search for a group'
-
-        page.within '.select2-drop' do
-          expect(page).to have_content(group_to_share_with.name)
-          expect(page).not_to have_content(group.name)
-        end
-      end
-    end
-  end
 end
diff --git a/lib/banzai/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb
index 574a8a6c7a5171b010b16d1a94f57af06e39fa21..a4dd6abfe03caa440241ee2e8219dc86e58f4e4f 100644
--- a/lib/banzai/filter/spaced_link_filter.rb
+++ b/lib/banzai/filter/spaced_link_filter.rb
@@ -8,8 +8,9 @@ module Filter
     #
     # Based on Banzai::Filter::AutolinkFilter
     #
-    # CommonMark does not allow spaces in the url portion of a link.
-    # For example, `[example](page slug)` is not valid.  However,
+    # CommonMark does not allow spaces in the url portion of a link/url.
+    # For example, `[example](page slug)` is not valid.
+    # Neither is `![example](test image.jpg)`. However,
     # in our wikis, we support (via RedCarpet) this type of link, allowing
     # wiki pages to be easily linked by their title.  This filter adds that functionality.
     # The intent is for this to only be used in Wikis - in general, we want
@@ -20,10 +21,17 @@ class SpacedLinkFilter < HTML::Pipeline::Filter
 
       # Pattern to match a standard markdown link
       #
-      # Rubular: http://rubular.com/r/z9EAHxYmKI
-      LINK_PATTERN = /\[([^\]]+)\]\(([^)"]+)(?: \"([^\"]+)\")?\)/
-
-      # Text matching LINK_PATTERN inside these elements will not be linked
+      # Rubular: http://rubular.com/r/2EXEQ49rg5
+      LINK_OR_IMAGE_PATTERN = %r{
+        (?<preview_operator>!)?
+        \[(?<text>.+?)\]
+        \(
+          (?<new_link>.+?)
+          (?<title>\ ".+?")?
+        \)
+      }x
+
+      # Text matching LINK_OR_IMAGE_PATTERN inside these elements will not be linked
       IGNORE_PARENTS = %w(a code kbd pre script style).to_set
 
       # The XPath query to use for finding text nodes to parse.
@@ -38,7 +46,7 @@ def call
         doc.xpath(TEXT_QUERY).each do |node|
           content = node.to_html
 
-          next unless content.match(LINK_PATTERN)
+          next unless content.match(LINK_OR_IMAGE_PATTERN)
 
           html = spaced_link_filter(content)
 
@@ -53,25 +61,37 @@ def call
       private
 
       def spaced_link_match(link)
-        match = LINK_PATTERN.match(link)
-        return link unless match && match[1] && match[2]
+        match = LINK_OR_IMAGE_PATTERN.match(link)
+        return link unless match
 
         # escape the spaces in the url so that it's a valid markdown link,
         # then run it through the markdown processor again, let it do its magic
-        text     = match[1]
-        new_link = match[2].gsub(' ', '%20')
-        title    = match[3] ? " \"#{match[3]}\"" : ''
-        html     = Banzai::Filter::MarkdownFilter.call("[#{text}](#{new_link}#{title})", context)
+        html = Banzai::Filter::MarkdownFilter.call(transform_markdown(match), context)
 
         # link is wrapped in a <p>, so strip that off
         html.sub('<p>', '').chomp('</p>')
       end
 
       def spaced_link_filter(text)
-        Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_PATTERN) do |link, left:, right:|
+        Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_OR_IMAGE_PATTERN) do |link, left:, right:|
           spaced_link_match(link)
         end
       end
+
+      def transform_markdown(match)
+        preview_operator, text, new_link, title = process_match(match)
+
+        "#{preview_operator}[#{text}](#{new_link}#{title})"
+      end
+
+      def process_match(match)
+        [
+          match[:preview_operator],
+          match[:text],
+          match[:new_link].gsub(' ', '%20'),
+          match[:title]
+        ]
+      end
     end
   end
 end
diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb
index 737ff0cc8184576f0f26eb7793be585073c03d37..d2fe5a6492f575cfce3b48b0234b4507e56c556d 100644
--- a/lib/banzai/pipeline/wiki_pipeline.rb
+++ b/lib/banzai/pipeline/wiki_pipeline.rb
@@ -5,7 +5,7 @@ def self.filters
         @filters ||= begin
           super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter)
                .insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
-               .insert_before(Filter::WikiLinkFilter, Filter::SpacedLinkFilter)
+               .insert_before(Filter::VideoLinkFilter, Filter::SpacedLinkFilter)
         end
       end
     end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 4c28489f45a3f56530fd19ee9818cae0717dbe6d..58ca077e636ce7889bdb08915eb8812150b83b7d 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -7,7 +7,11 @@ class ContributionsCalendar
     def initialize(contributor, current_user = nil)
       @contributor = contributor
       @current_user = current_user
-      @projects = ContributedProjectsFinder.new(contributor).execute(current_user)
+      @projects = if @contributor.include_private_contributions?
+                    ContributedProjectsFinder.new(@contributor).execute(@contributor)
+                  else
+                    ContributedProjectsFinder.new(contributor).execute(current_user)
+                  end
     end
 
     def activity_dates
@@ -36,13 +40,9 @@ def activity_dates
     def events_by_date(date)
       return Event.none unless can_read_cross_project?
 
-      events = Event.contributions.where(author_id: contributor.id)
+      Event.contributions.where(author_id: contributor.id)
         .where(created_at: date.beginning_of_day..date.end_of_day)
         .where(project_id: projects)
-
-      # Use visible_to_user? instead of the complicated logic in activity_dates
-      # because we're only viewing the events for a single day.
-      events.select { |event| event.visible_to_user?(current_user) }
     end
 
     def starting_year
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index c79d8d3cb215f0e51c49e9dfcbaf0442ba9e7dbf..2acb0e43b69239853da2ece0fad93078dc7ac625 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -2,7 +2,7 @@ module Gitlab
   module Diff
     module FileCollection
       class Base
-        attr_reader :project, :diff_options, :diff_refs, :fallback_diff_refs
+        attr_reader :project, :diff_options, :diff_refs, :fallback_diff_refs, :diffable
 
         delegate :count, :size, :real_size, to: :diff_files
 
@@ -33,6 +33,14 @@ def diff_file_with_new_path(new_path)
           diff_files.find { |diff_file| diff_file.new_path == new_path }
         end
 
+        def clear_cache
+          # No-op
+        end
+
+        def write_cache
+          # No-op
+        end
+
         private
 
         def decorate_diff!(diff)
diff --git a/lib/gitlab/diff/file_collection/merge_request_diff.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index be25e1bab21be6ed0db578eca32fc1ec5d04dd64..0dd073a3a8e3923e0452ca45fff90566e7a9ddaf 100644
--- a/lib/gitlab/diff/file_collection/merge_request_diff.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -2,6 +2,8 @@ module Gitlab
   module Diff
     module FileCollection
       class MergeRequestDiff < Base
+        extend ::Gitlab::Utils::Override
+
         def initialize(merge_request_diff, diff_options:)
           @merge_request_diff = merge_request_diff
 
@@ -13,70 +15,35 @@ def initialize(merge_request_diff, diff_options:)
         end
 
         def diff_files
-          # Make sure to _not_ send any method call to Gitlab::Diff::File
-          # _before_ all of them were collected (`super`). Premature method calls will
-          # trigger N+1 RPCs to Gitaly through BatchLoader records (Blob.lazy).
-          #
           diff_files = super
 
-          diff_files.each { |diff_file| cache_highlight!(diff_file) if cacheable?(diff_file) }
-          store_highlight_cache
+          diff_files.each { |diff_file| cache.decorate(diff_file) }
 
           diff_files
         end
 
-        def real_size
-          @merge_request_diff.real_size
+        override :write_cache
+        def write_cache
+          cache.write_if_empty
         end
 
-        def clear_cache!
-          Rails.cache.delete(cache_key)
+        override :clear_cache
+        def clear_cache
+          cache.clear
         end
 
         def cache_key
-          [@merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::Line::SERIALIZE_KEYS, diff_options]
-        end
-
-        private
-
-        def highlight_diff_file_from_cache!(diff_file, cache_diff_lines)
-          diff_file.highlighted_diff_lines = cache_diff_lines.map do |line|
-            Gitlab::Diff::Line.init_from_hash(line)
-          end
+          cache.key
         end
 
-        #
-        # If we find the highlighted diff files lines on the cache we replace existing diff_files lines (no highlighted)
-        # for the highlighted ones, so we just skip their execution.
-        # If the highlighted diff files lines are not cached we calculate and cache them.
-        #
-        # The content of the cache is a Hash where the key identifies the file and the values are Arrays of
-        # hashes that represent serialized diff lines.
-        #
-        def cache_highlight!(diff_file)
-          item_key = diff_file.file_identifier
-
-          if highlight_cache[item_key]
-            highlight_diff_file_from_cache!(diff_file, highlight_cache[item_key])
-          else
-            highlight_cache[item_key] = diff_file.highlighted_diff_lines.map(&:to_hash)
-          end
-        end
-
-        def highlight_cache
-          return @highlight_cache if defined?(@highlight_cache)
-
-          @highlight_cache = Rails.cache.read(cache_key) || {}
-          @highlight_cache_was_empty = @highlight_cache.empty?
-          @highlight_cache
+        def real_size
+          @merge_request_diff.real_size
         end
 
-        def store_highlight_cache
-          Rails.cache.write(cache_key, highlight_cache, expires_in: 1.week) if @highlight_cache_was_empty
-        end
+        private
 
-        def cacheable?(diff_file)
-          @merge_request_diff.present? && diff_file.text? && diff_file.diffable?
+        def cache
+          @cache ||= Gitlab::Diff::HighlightCache.new(self)
         end
       end
     end
diff --git a/lib/gitlab/diff/highlight_cache.rb b/lib/gitlab/diff/highlight_cache.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e4390771db2c54cc12e320e4472db57f4a487f81
--- /dev/null
+++ b/lib/gitlab/diff/highlight_cache.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+#
+module Gitlab
+  module Diff
+    class HighlightCache
+      delegate :diffable, to: :@diff_collection
+      delegate :diff_options, to: :@diff_collection
+
+      def initialize(diff_collection, backend: Rails.cache)
+        @backend = backend
+        @diff_collection = diff_collection
+      end
+
+      # - Reads from cache
+      # - Assigns DiffFile#highlighted_diff_lines for cached files
+      def decorate(diff_file)
+        if content = read_file(diff_file)
+          diff_file.highlighted_diff_lines = content.map do |line|
+            Gitlab::Diff::Line.init_from_hash(line)
+          end
+        end
+      end
+
+      # It populates a Hash in order to submit a single write to the memory
+      # cache. This avoids excessive IO generated by N+1's (1 writing for
+      # each highlighted line or file).
+      def write_if_empty
+        return if cached_content.present?
+
+        @diff_collection.diff_files.each do |diff_file|
+          next unless cacheable?(diff_file)
+
+          diff_file_id = diff_file.file_identifier
+
+          cached_content[diff_file_id] = diff_file.highlighted_diff_lines.map(&:to_hash)
+        end
+
+        cache.write(key, cached_content, expires_in: 1.week)
+      end
+
+      def clear
+        cache.delete(key)
+      end
+
+      def key
+        [diffable, 'highlighted-diff-files', Gitlab::Diff::Line::SERIALIZE_KEYS, diff_options]
+      end
+
+      private
+
+      def read_file(diff_file)
+        cached_content[diff_file.file_identifier]
+      end
+
+      def cache
+        @backend
+      end
+
+      def cached_content
+        @cached_content ||= cache.read(key) || {}
+      end
+
+      def cacheable?(diff_file)
+        diffable.present? && diff_file.text? && diff_file.diffable?
+      end
+    end
+  end
+end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 219c69893ad3514d710faf64661d1b1812e268be..20dce8d0e06df9f18cddc43ccaf012a69be7b9c6 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -11,7 +11,7 @@ class DiffCollection
 
       delegate :max_files, :max_lines, :max_bytes, :safe_max_files, :safe_max_lines, :safe_max_bytes, to: :limits
 
-      def self.collection_limits(options = {})
+      def self.limits(options = {})
         limits = {}
         limits[:max_files] = options.fetch(:max_files, DEFAULT_LIMITS[:max_files])
         limits[:max_lines] = options.fetch(:max_lines, DEFAULT_LIMITS[:max_lines])
@@ -19,13 +19,14 @@ def self.collection_limits(options = {})
         limits[:safe_max_files] = [limits[:max_files], DEFAULT_LIMITS[:max_files]].min
         limits[:safe_max_lines] = [limits[:max_lines], DEFAULT_LIMITS[:max_lines]].min
         limits[:safe_max_bytes] = limits[:safe_max_files] * 5.kilobytes # Average 5 KB per file
+        limits[:max_patch_bytes] = Gitlab::Git::Diff::SIZE_LIMIT
 
         OpenStruct.new(limits)
       end
 
       def initialize(iterator, options = {})
         @iterator = iterator
-        @limits = self.class.collection_limits(options)
+        @limits = self.class.limits(options)
         @enforce_limits = !!options.fetch(:limits, true)
         @expanded = !!options.fetch(:expanded, true)
 
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 6a97cd8ed1709e939f83058afe70728345b4b673..aa5b4f940908d5a66314489f736ac4a972c7103b 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -369,7 +369,7 @@ def call_commit_diff(request_params, options = {})
         request_params[:ignore_whitespace_change] = options.fetch(:ignore_whitespace_change, false)
         request_params[:enforce_limits] = options.fetch(:limits, true)
         request_params[:collapse_diffs] = !options.fetch(:expanded, true)
-        request_params.merge!(Gitlab::Git::DiffCollection.collection_limits(options).to_h)
+        request_params.merge!(Gitlab::Git::DiffCollection.limits(options).to_h)
 
         request = Gitaly::CommitDiffRequest.new(request_params)
         response = GitalyClient.call(@repository.storage, :diff_service, :commit_diff, request, timeout: GitalyClient.medium_timeout)
diff --git a/lib/gitlab/gitaly_client/remote_service.rb b/lib/gitlab/gitaly_client/remote_service.rb
index 6415c64b4e2d81ba03ce1a517597a38b2a63707a..4661448621b8efe8cee4a13837e51324697381e1 100644
--- a/lib/gitlab/gitaly_client/remote_service.rb
+++ b/lib/gitlab/gitaly_client/remote_service.rb
@@ -1,6 +1,8 @@
 module Gitlab
   module GitalyClient
     class RemoteService
+      include Gitlab::EncodingHelper
+
       MAX_MSG_SIZE = 128.kilobytes.freeze
 
       def self.exists?(remote_url)
@@ -61,7 +63,7 @@ def find_remote_root_ref(remote_name)
         response = GitalyClient.call(@storage, :remote_service,
                                      :find_remote_root_ref, request)
 
-        response.ref.presence
+        encode_utf8(response.ref)
       end
 
       def update_remote_mirror(ref_name, only_branches_matching)
diff --git a/lib/gitlab/gitaly_client/storage_service.rb b/lib/gitlab/gitaly_client/storage_service.rb
index eb0e910665bdf324bdb67a6fc3091c347eb717f0..3a26dd58ff404e678697c682c280748b24b40dc0 100644
--- a/lib/gitlab/gitaly_client/storage_service.rb
+++ b/lib/gitlab/gitaly_client/storage_service.rb
@@ -5,6 +5,14 @@ def initialize(storage)
         @storage = storage
       end
 
+      # Returns all directories in the git storage directory, lexically ordered
+      def list_directories(depth: 1)
+        request = Gitaly::ListDirectoriesRequest.new(storage_name: @storage, depth: depth)
+
+        GitalyClient.call(@storage, :storage_service, :list_directories, request)
+          .flat_map(&:paths)
+      end
+
       # Delete all repositories in the storage. This is a slow and VERY DESTRUCTIVE operation.
       def delete_all_repositories
         request = Gitaly::DeleteAllRepositoriesRequest.new(storage_name: @storage)
diff --git a/lib/gitlab/project_service_logger.rb b/lib/gitlab/project_service_logger.rb
new file mode 100644
index 0000000000000000000000000000000000000000..e84dca97962b21e7acb8bc7166ab5f06b43c7e9f
--- /dev/null
+++ b/lib/gitlab/project_service_logger.rb
@@ -0,0 +1,7 @@
+module Gitlab
+  class ProjectServiceLogger < Gitlab::JsonLogger
+    def self.file_name_noext
+      'integrations_json'
+    end
+  end
+end
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index ba0224bdaa3428dd7f621637eaf81d9f731296a4..959361b9aa04db82abebb2e30d3c99da1826d92d 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -1,40 +1,29 @@
-# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/954
-#
+# frozen_string_literal: true
+require 'set'
+
 namespace :gitlab do
   namespace :cleanup do
-    HASHED_REPOSITORY_NAME = '@hashed'.freeze
-
     desc "GitLab | Cleanup | Clean namespaces"
     task dirs: :gitlab_environment do
-      warn_user_is_not_gitlab
+      namespaces = Set.new(Namespace.pluck(:path))
+      namespaces << Storage::HashedProject::ROOT_PATH_PREFIX
 
-      namespaces = Namespace.pluck(:path)
-      namespaces << HASHED_REPOSITORY_NAME  # add so that it will be ignored
-      Gitlab.config.repositories.storages.each do |name, repository_storage|
-        git_base_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository_storage.legacy_disk_path }
-        all_dirs = Dir.glob(git_base_path + '/*')
+      Gitaly::Server.all.each do |server|
+        all_dirs = Gitlab::GitalyClient::StorageService
+          .new(server.storage)
+          .list_directories(depth: 0)
+          .reject { |dir| dir.ends_with?('.git') || namespaces.include?(File.basename(dir)) }
 
-        puts git_base_path.color(:yellow)
         puts "Looking for directories to remove... "
-
-        all_dirs.reject! do |dir|
-          # skip if git repo
-          dir =~ /.git$/
-        end
-
-        all_dirs.reject! do |dir|
-          dir_name = File.basename dir
-
-          # skip if namespace present
-          namespaces.include?(dir_name)
-        end
-
         all_dirs.each do |dir_path|
           if remove?
-            if FileUtils.rm_rf dir_path
-              puts "Removed...#{dir_path}".color(:red)
-            else
-              puts "Cannot remove #{dir_path}".color(:red)
+            begin
+              Gitlab::GitalyClient::NamespaceService.new(server.storage)
+                .remove(dir_path)
+
+              puts "Removed...#{dir_path}"
+            rescue StandardError => e
+              puts "Cannot remove #{dir_path}: #{e.message}".color(:red)
             end
           else
             puts "Can be removed: #{dir_path}".color(:red)
@@ -79,29 +68,29 @@ namespace :gitlab do
 
     desc "GitLab | Cleanup | Clean repositories"
     task repos: :gitlab_environment do
-      warn_user_is_not_gitlab
-
       move_suffix = "+orphaned+#{Time.now.to_i}"
-      Gitlab.config.repositories.storages.each do |name, repository_storage|
-        repo_root = Gitlab::GitalyClient::StorageSettings.allow_disk_access { repository_storage.legacy_disk_path }
 
-        # Look for global repos (legacy, depth 1) and normal repos (depth 2)
-        IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find|
-          find.each_line do |path|
-            path.chomp!
-            repo_with_namespace = path
-              .sub(repo_root, '')
-              .sub(%r{^/*}, '')
-              .chomp('.git')
-              .chomp('.wiki')
-
-            # TODO ignoring hashed repositories for now.  But revisit to fully support
-            # possible orphaned hashed repos
-            next if repo_with_namespace.start_with?("#{HASHED_REPOSITORY_NAME}/") || Project.find_by_full_path(repo_with_namespace)
-
-            new_path = path + move_suffix
-            puts path.inspect + ' -> ' + new_path.inspect
-            File.rename(path, new_path)
+      Gitaly::Server.all.each do |server|
+        Gitlab::GitalyClient::StorageService
+          .new(server.storage)
+          .list_directories
+          .each do |path|
+          repo_with_namespace = path.chomp('.git').chomp('.wiki')
+
+          # TODO ignoring hashed repositories for now.  But revisit to fully support
+          # possible orphaned hashed repos
+          next if repo_with_namespace.start_with?(Storage::HashedProject::ROOT_PATH_PREFIX)
+          next if Project.find_by_full_path(repo_with_namespace)
+
+          new_path = path + move_suffix
+          puts path.inspect + ' -> ' + new_path.inspect
+
+          begin
+            Gitlab::GitalyClient::NamespaceService
+              .new(server.storage)
+              .rename(path, new_path)
+          rescue StandardError => e
+            puts "Error occured while moving the repository: #{e.message}".color(:red)
           end
         end
       end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 41b567873384548c39a0ffa54bafe33e12d1ebfa..774eccad495975bc70d8ce882543fdd610048f79 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1421,6 +1421,12 @@ msgstr ""
 msgid "Choose a branch/tag (e.g. %{master}) or enter a commit (e.g. %{sha}) to see what's changed or to create a merge request."
 msgstr ""
 
+msgid "Choose a template..."
+msgstr ""
+
+msgid "Choose a type..."
+msgstr ""
+
 msgid "Choose any color."
 msgstr ""
 
@@ -2202,6 +2208,9 @@ msgstr ""
 msgid "Contribution guide"
 msgstr ""
 
+msgid "Contributions for <strong>%{calendar_date}</strong>"
+msgstr ""
+
 msgid "Contributions per group member"
 msgstr ""
 
@@ -2672,9 +2681,21 @@ msgstr ""
 msgid "Disable group Runners"
 msgstr ""
 
+msgid "Discard"
+msgstr ""
+
+msgid "Discard all changes"
+msgstr ""
+
+msgid "Discard all unstaged changes?"
+msgstr ""
+
 msgid "Discard changes"
 msgstr ""
 
+msgid "Discard changes to %{path}?"
+msgstr ""
+
 msgid "Discard draft"
 msgstr ""
 
@@ -3173,6 +3194,9 @@ msgstr ""
 msgid "Fields on this page are now uneditable, you can configure"
 msgstr ""
 
+msgid "File templates"
+msgstr ""
+
 msgid "Files"
 msgstr ""
 
@@ -3188,6 +3212,9 @@ msgstr ""
 msgid "Filter by commit message"
 msgstr ""
 
+msgid "Filter..."
+msgstr ""
+
 msgid "Find by path"
 msgstr ""
 
@@ -4825,9 +4852,6 @@ msgstr ""
 msgid "More"
 msgstr ""
 
-msgid "More actions"
-msgstr ""
-
 msgid "More info"
 msgstr ""
 
@@ -4980,6 +5004,9 @@ msgstr ""
 msgid "No container images stored for this project. Add one by following the instructions above."
 msgstr ""
 
+msgid "No contributions were found"
+msgstr ""
+
 msgid "No due date"
 msgstr ""
 
@@ -5588,6 +5615,9 @@ msgstr ""
 msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible."
 msgstr ""
 
+msgid "Profiles|%{author_name} made a private contribution"
+msgstr ""
+
 msgid "Profiles|Account scheduled for removal."
 msgstr ""
 
@@ -5597,15 +5627,30 @@ msgstr ""
 msgid "Profiles|Add status emoji"
 msgstr ""
 
+msgid "Profiles|Avatar cropper"
+msgstr ""
+
+msgid "Profiles|Avatar will be removed. Are you sure?"
+msgstr ""
+
 msgid "Profiles|Change username"
 msgstr ""
 
+msgid "Profiles|Choose file..."
+msgstr ""
+
+msgid "Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information."
+msgstr ""
+
 msgid "Profiles|Clear status"
 msgstr ""
 
 msgid "Profiles|Current path: %{path}"
 msgstr ""
 
+msgid "Profiles|Current status"
+msgstr ""
+
 msgid "Profiles|Delete Account"
 msgstr ""
 
@@ -5618,39 +5663,108 @@ msgstr ""
 msgid "Profiles|Deleting an account has the following effects:"
 msgstr ""
 
+msgid "Profiles|Do not show on profile"
+msgstr ""
+
+msgid "Profiles|Don't display activity-related personal information on your profiles"
+msgstr ""
+
+msgid "Profiles|Edit Profile"
+msgstr ""
+
 msgid "Profiles|Invalid password"
 msgstr ""
 
 msgid "Profiles|Invalid username"
 msgstr ""
 
+msgid "Profiles|Main settings"
+msgstr ""
+
+msgid "Profiles|No file chosen"
+msgstr ""
+
 msgid "Profiles|Path"
 msgstr ""
 
+msgid "Profiles|Position and size your new avatar"
+msgstr ""
+
+msgid "Profiles|Private contributions"
+msgstr ""
+
+msgid "Profiles|Public Avatar"
+msgstr ""
+
+msgid "Profiles|Remove avatar"
+msgstr ""
+
+msgid "Profiles|Set new profile picture"
+msgstr ""
+
+msgid "Profiles|Some options are unavailable for LDAP accounts"
+msgstr ""
+
+msgid "Profiles|Tell us about yourself in fewer than 250 characters."
+msgstr ""
+
+msgid "Profiles|The maximum file size allowed is 200KB."
+msgstr ""
+
 msgid "Profiles|This doesn't look like a public SSH key, are you sure you want to add it?"
 msgstr ""
 
+msgid "Profiles|This email will be displayed on your public profile."
+msgstr ""
+
 msgid "Profiles|This emoji and message will appear on your profile and throughout the interface."
 msgstr ""
 
+msgid "Profiles|This feature is experimental and translations are not complete yet."
+msgstr ""
+
+msgid "Profiles|This information will appear on your profile."
+msgstr ""
+
 msgid "Profiles|Type your %{confirmationValue} to confirm:"
 msgstr ""
 
 msgid "Profiles|Typically starts with \"ssh-rsa …\""
 msgstr ""
 
+msgid "Profiles|Update profile settings"
+msgstr ""
+
 msgid "Profiles|Update username"
 msgstr ""
 
+msgid "Profiles|Upload new avatar"
+msgstr ""
+
 msgid "Profiles|Username change failed - %{message}"
 msgstr ""
 
 msgid "Profiles|Username successfully changed"
 msgstr ""
 
+msgid "Profiles|Website"
+msgstr ""
+
 msgid "Profiles|What's your status?"
 msgstr ""
 
+msgid "Profiles|You can change your avatar here"
+msgstr ""
+
+msgid "Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}"
+msgstr ""
+
+msgid "Profiles|You can upload your avatar here"
+msgstr ""
+
+msgid "Profiles|You can upload your avatar here or change it at %{gravatar_link}"
+msgstr ""
+
 msgid "Profiles|You don't have access to delete this user."
 msgstr ""
 
@@ -5660,6 +5774,15 @@ msgstr ""
 msgid "Profiles|Your account is currently an owner in these groups:"
 msgstr ""
 
+msgid "Profiles|Your email address was automatically set based on your %{provider_label} account."
+msgstr ""
+
+msgid "Profiles|Your location was automatically set based on your %{provider_label} account."
+msgstr ""
+
+msgid "Profiles|Your name was automatically set based on your %{provider_label} account, so people you know can recognize you."
+msgstr ""
+
 msgid "Profiles|Your status"
 msgstr ""
 
@@ -7266,6 +7389,12 @@ msgstr ""
 msgid "There are no projects shared with this group yet"
 msgstr ""
 
+msgid "There are no staged changes"
+msgstr ""
+
+msgid "There are no unstaged changes"
+msgstr ""
+
 msgid "There are problems accessing Git storage: "
 msgstr ""
 
@@ -7786,6 +7915,9 @@ msgstr ""
 msgid "Unable to sign you in to the group with SAML due to \"%{reason}\""
 msgstr ""
 
+msgid "Undo"
+msgstr ""
+
 msgid "Unknown"
 msgstr ""
 
@@ -7801,6 +7933,9 @@ msgstr ""
 msgid "Unresolve discussion"
 msgstr ""
 
+msgid "Unstage"
+msgstr ""
+
 msgid "Unstage all changes"
 msgstr ""
 
@@ -7870,9 +8005,6 @@ msgstr ""
 msgid "Upload file"
 msgstr ""
 
-msgid "Upload new avatar"
-msgstr ""
-
 msgid "UploadLink|click to upload"
 msgstr ""
 
@@ -7924,9 +8056,6 @@ msgstr ""
 msgid "Users"
 msgstr ""
 
-msgid "User|Current status"
-msgstr ""
-
 msgid "Variables"
 msgstr ""
 
@@ -8284,6 +8413,12 @@ msgstr ""
 msgid "You need permission."
 msgstr ""
 
+msgid "You will loose all changes you've made to this file. This action cannot be undone."
+msgstr ""
+
+msgid "You will loose all the unstaged changes you've made in this project. This action cannot be undone."
+msgstr ""
+
 msgid "You will not get any notifications via email"
 msgstr ""
 
diff --git a/public/robots.txt b/public/robots.txt
index 1f9d42f4adc9862306a232e69fb4e8b27c30a5ab..ea931e1a223b93e87b1774d5b55eecacff9fe6b1 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -21,6 +21,8 @@ Disallow: /groups/new
 Disallow: /groups/*/edit
 Disallow: /users
 Disallow: /help
+# Only specifically allow the Sign In page to avoid very ugly search results
+Allow: /users/sign_in
 
 # Global snippets
 User-Agent: *
diff --git a/qa/qa.rb b/qa/qa.rb
index c21cb3c19294c509f8f51fcb7359e53ab337dd1c..cb3202c8e1ce7cc3520e51db94861fb18a1ef2f1 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -100,7 +100,7 @@ module Integration
       end
 
       module Sanity
-        autoload :Failing, 'qa/scenario/test/sanity/failing'
+        autoload :Framework, 'qa/scenario/test/sanity/framework'
         autoload :Selectors, 'qa/scenario/test/sanity/selectors'
       end
     end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index afc8b66d8784dd2b7e960d72efa700f4d9193a6f..1ea98b94aeb1878d2da28df7a074ad30f437bc51 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -63,6 +63,14 @@ def self.path
           '/users/sign_in'
         end
 
+        def sign_in_tab?
+          page.has_button?('Sign in')
+        end
+
+        def ldap_tab?
+          page.has_button?('LDAP')
+        end
+
         def switch_to_sign_in_tab
           click_on 'Sign in'
         end
@@ -90,8 +98,8 @@ def sign_in_using_ldap_credentials
         end
 
         def sign_in_using_gitlab_credentials(user)
-          switch_to_sign_in_tab unless page.has_button?('Sign in')
-          switch_to_standard_tab if page.has_content?('LDAP')
+          switch_to_sign_in_tab unless sign_in_tab?
+          switch_to_standard_tab if ldap_tab?
 
           fill_in :user_login, with: user.username
           fill_in :user_password, with: user.password
diff --git a/qa/qa/scenario/test/sanity/failing.rb b/qa/qa/scenario/test/sanity/framework.rb
similarity index 51%
rename from qa/qa/scenario/test/sanity/failing.rb
rename to qa/qa/scenario/test/sanity/framework.rb
index 03452f6693d7daaa0624bde0a2c3372bc365cfbc..7835d2564f012bb2133ad951f7d8a3be61dfb139 100644
--- a/qa/qa/scenario/test/sanity/failing.rb
+++ b/qa/qa/scenario/test/sanity/framework.rb
@@ -5,12 +5,13 @@ module Scenario
     module Test
       module Sanity
         ##
-        # This scenario exits with a 1 exit code.
+        # This scenario runs 1 passing example, and 1 failing example, and exits
+        # with a 1 exit code.
         #
-        class Failing < Template
+        class Framework < Template
           include Bootable
 
-          tags :failing
+          tags :framework
         end
       end
     end
diff --git a/qa/qa/specs/features/sanity/failing_spec.rb b/qa/qa/specs/features/sanity/failing_spec.rb
deleted file mode 100644
index 7e0480e90673d995705bff38264f7357282a7831..0000000000000000000000000000000000000000
--- a/qa/qa/specs/features/sanity/failing_spec.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-module QA
-  context 'Sanity checks', :orchestrated, :failing do
-    describe 'Failing orchestrated example' do
-      it 'always fails' do
-        Runtime::Browser.visit(:gitlab, Page::Main::Login)
-
-        expect(page).to have_text("These Aren't the Texts You're Looking For", wait: 1)
-      end
-    end
-  end
-end
diff --git a/qa/qa/specs/features/sanity/framework_spec.rb b/qa/qa/specs/features/sanity/framework_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..ee9d068eb3a4e14cd02057633c05a8e244940600
--- /dev/null
+++ b/qa/qa/specs/features/sanity/framework_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module QA
+  context 'Framework sanity checks', :orchestrated, :framework do
+    describe 'Passing orchestrated example' do
+      it 'succeeds' do
+        Runtime::Browser.visit(:gitlab, Page::Main::Login)
+
+        Page::Main::Login.perform do |main_login|
+          expect(main_login.sign_in_tab?).to be(true)
+        end
+      end
+    end
+
+    describe 'Failing orchestrated example' do
+      it 'fails' do
+        Runtime::Browser.visit(:gitlab, Page::Main::Login)
+
+        expect(page).to have_text("These Aren't the Texts You're Looking For", wait: 1)
+      end
+    end
+  end
+end
diff --git a/qa/spec/scenario/test/sanity/framework_spec.rb b/qa/spec/scenario/test/sanity/framework_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..44ac780556edc70189a96051c39b0fcdb5e6e1e3
--- /dev/null
+++ b/qa/spec/scenario/test/sanity/framework_spec.rb
@@ -0,0 +1,5 @@
+describe QA::Scenario::Test::Sanity::Framework do
+  it_behaves_like 'a QA scenario class' do
+    let(:tags) { [:framework] }
+  end
+end
diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb
index 9dc4edf97d1f7d34c3feb12b96754a923b13f424..c59add88a82671252da38b848804ec818f136394 100644
--- a/spec/bin/changelog_spec.rb
+++ b/spec/bin/changelog_spec.rb
@@ -95,6 +95,7 @@
 
         it 'shows error message and exits the program' do
           allow($stdin).to receive(:getc).and_return(type)
+
           expect do
             expect { described_class.read_type }.to raise_error(
               ChangelogHelpers::Abort,
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index ca7d30fec831841b0ab9da468d7c3f0eb472a1e2..751919f95017f487eaabe53afc239314c0109b52 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -166,8 +166,8 @@ def get_index(**extra_params)
             expect(response).to match_response_schema('job/job_details')
             expect(json_response['artifact']['download_path']).to match(%r{artifacts/download})
             expect(json_response['artifact']['browse_path']).to match(%r{artifacts/browse})
-            expect(json_response['artifact']).not_to have_key(:expired)
-            expect(json_response['artifact']).not_to have_key(:expired_at)
+            expect(json_response['artifact']).not_to have_key('expired')
+            expect(json_response['artifact']).not_to have_key('expired_at')
           end
         end
 
@@ -177,8 +177,8 @@ def get_index(**extra_params)
           it 'exposes needed information' do
             expect(response).to have_gitlab_http_status(:ok)
             expect(response).to match_response_schema('job/job_details')
-            expect(json_response['artifact']).not_to have_key(:download_path)
-            expect(json_response['artifact']).not_to have_key(:browse_path)
+            expect(json_response['artifact']).not_to have_key('download_path')
+            expect(json_response['artifact']).not_to have_key('browse_path')
             expect(json_response['artifact']['expired']).to eq(true)
             expect(json_response['artifact']['expire_at']).not_to be_empty
           end
diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..db4d41568895e66a64a0edc6151bad23565d19e8
--- /dev/null
+++ b/spec/features/projects/members/invite_group_spec.rb
@@ -0,0 +1,195 @@
+require 'spec_helper'
+
+describe 'Project > Members > Invite group', :js do
+  include Select2Helper
+  include ActionView::Helpers::DateHelper
+
+  let(:maintainer) { create(:user) }
+
+  describe 'Share with group lock' do
+    shared_examples 'the project can be shared with groups' do
+      it 'the "Invite group" tab exists' do
+        visit project_settings_members_path(project)
+        expect(page).to have_selector('#invite-group-tab')
+      end
+    end
+
+    shared_examples 'the project cannot be shared with groups' do
+      it 'the "Invite group" tab does not exist' do
+        visit project_settings_members_path(project)
+        expect(page).not_to have_selector('#invite-group-tab')
+      end
+    end
+
+    context 'for a project in a root group' do
+      let!(:group_to_share_with) { create(:group) }
+      let(:project) { create(:project, namespace: create(:group)) }
+
+      before do
+        project.add_maintainer(maintainer)
+        sign_in(maintainer)
+      end
+
+      context 'when the group has "Share group lock" disabled' do
+        it_behaves_like 'the project can be shared with groups'
+
+        it 'the project can be shared with another group' do
+          visit project_settings_members_path(project)
+
+          click_on 'invite-group-tab'
+
+          select2 group_to_share_with.id, from: '#link_group_id'
+          page.find('body').click
+          find('.btn-create').click
+
+          page.within('.project-members-groups') do
+            expect(page).to have_content(group_to_share_with.name)
+          end
+        end
+      end
+
+      context 'when the group has "Share group lock" enabled' do
+        before do
+          project.namespace.update_column(:share_with_group_lock, true)
+        end
+
+        it_behaves_like 'the project cannot be shared with groups'
+      end
+    end
+
+    context 'for a project in a subgroup', :nested_groups do
+      let!(:group_to_share_with) { create(:group) }
+      let(:root_group) { create(:group) }
+      let(:subgroup) { create(:group, parent: root_group) }
+      let(:project) { create(:project, namespace: subgroup) }
+
+      before do
+        project.add_maintainer(maintainer)
+        sign_in(maintainer)
+      end
+
+      context 'when the root_group has "Share group lock" disabled' do
+        context 'when the subgroup has "Share group lock" disabled' do
+          it_behaves_like 'the project can be shared with groups'
+        end
+
+        context 'when the subgroup has "Share group lock" enabled' do
+          before do
+            subgroup.update_column(:share_with_group_lock, true)
+          end
+
+          it_behaves_like 'the project cannot be shared with groups'
+        end
+      end
+
+      context 'when the root_group has "Share group lock" enabled' do
+        before do
+          root_group.update_column(:share_with_group_lock, true)
+        end
+
+        context 'when the subgroup has "Share group lock" disabled (parent overridden)' do
+          it_behaves_like 'the project can be shared with groups'
+        end
+
+        context 'when the subgroup has "Share group lock" enabled' do
+          before do
+            subgroup.update_column(:share_with_group_lock, true)
+          end
+
+          it_behaves_like 'the project cannot be shared with groups'
+        end
+      end
+    end
+  end
+
+  describe 'setting an expiration date for a group link' do
+    let(:project) { create(:project) }
+    let!(:group) { create(:group) }
+
+    around do |example|
+      Timecop.freeze { example.run }
+    end
+
+    before do
+      project.add_maintainer(maintainer)
+      sign_in(maintainer)
+
+      visit project_settings_members_path(project)
+
+      click_on 'invite-group-tab'
+
+      select2 group.id, from: '#link_group_id'
+
+      fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d')
+      click_on 'invite-group-tab'
+      find('.btn-create').click
+    end
+
+    it 'the group link shows the expiration time with a warning class' do
+      page.within('.project-members-groups') do
+        # Using distance_of_time_in_words_to_now because it is not the same as
+        # subtraction, and this way avoids time zone issues as well
+        expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at)
+        expect(page).to have_content(expires_in_text)
+        expect(page).to have_selector('.text-warning')
+      end
+    end
+  end
+
+  describe 'the groups dropdown' do
+    context 'with multiple groups to choose from' do
+      let(:project) { create(:project) }
+
+      before do
+        project.add_maintainer(maintainer)
+        sign_in(maintainer)
+
+        create(:group).add_owner(maintainer)
+        create(:group).add_owner(maintainer)
+
+        visit project_settings_members_path(project)
+
+        click_link 'Invite group'
+
+        find('.ajax-groups-select.select2-container')
+
+        execute_script 'GROUP_SELECT_PER_PAGE = 1;'
+        open_select2 '#link_group_id'
+      end
+
+      it 'should infinitely scroll' do
+        expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
+
+        scroll_select2_to_bottom('.select2-drop .select2-results:visible')
+
+        expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
+      end
+    end
+
+    context 'for a project in a nested group' do
+      let(:group) { create(:group) }
+      let!(:nested_group) { create(:group, parent: group) }
+      let!(:group_to_share_with) { create(:group) }
+      let!(:project) { create(:project, namespace: nested_group) }
+
+      before do
+        project.add_maintainer(maintainer)
+        sign_in(maintainer)
+        group.add_maintainer(maintainer)
+        group_to_share_with.add_maintainer(maintainer)
+      end
+
+      it 'the groups dropdown does not show ancestors', :nested_groups do
+        visit project_settings_members_path(project)
+
+        click_on 'invite-group-tab'
+        click_link 'Search for a group'
+
+        page.within '.select2-drop' do
+          expect(page).to have_content(group_to_share_with.name)
+          expect(page).not_to have_content(group.name)
+        end
+      end
+    end
+  end
+end
diff --git a/spec/finders/contributed_projects_finder_spec.rb b/spec/finders/contributed_projects_finder_spec.rb
index 9155a8d6fe9870b03eb2b542c5217945d44b4f89..81fb4e3561c8e9e07cdf81259fc7b6c013eb33e7 100644
--- a/spec/finders/contributed_projects_finder_spec.rb
+++ b/spec/finders/contributed_projects_finder_spec.rb
@@ -8,6 +8,7 @@
 
   let!(:public_project) { create(:project, :public) }
   let!(:private_project) { create(:project, :private) }
+  let!(:internal_project) { create(:project, :internal) }
 
   before do
     private_project.add_maintainer(source_user)
@@ -16,17 +17,18 @@
 
     create(:push_event, project: public_project, author: source_user)
     create(:push_event, project: private_project, author: source_user)
+    create(:push_event, project: internal_project, author: source_user)
   end
 
-  describe 'without a current user' do
+  describe 'activity without a current user' do
     subject { finder.execute }
 
-    it { is_expected.to eq([public_project]) }
+    it { is_expected.to match_array([public_project]) }
   end
 
-  describe 'with a current user' do
+  describe 'activity with a current user' do
     subject { finder.execute(current_user) }
 
-    it { is_expected.to eq([private_project, public_project]) }
+    it { is_expected.to match_array([private_project, internal_project, public_project]) }
   end
 end
diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb
index 58470f4c84d73802921e4bbc072d5b325421c24b..c5fcd68eb4ced2957652fb447ae1f47c0e78c1f6 100644
--- a/spec/finders/user_recent_events_finder_spec.rb
+++ b/spec/finders/user_recent_events_finder_spec.rb
@@ -13,49 +13,25 @@
   subject(:finder) { described_class.new(current_user, project_owner) }
 
   describe '#execute' do
-    context 'current user does not have access to projects' do
-      it 'returns public and internal events' do
-        records = finder.execute
-
-        expect(records).to include(public_event, internal_event)
-        expect(records).not_to include(private_event)
+    context 'when profile is public' do
+      it 'returns all the events' do
+        expect(finder.execute).to include(private_event, internal_event, public_event)
       end
     end
 
-    context 'when current user has access to the projects' do
-      before do
-        private_project.add_developer(current_user)
-        internal_project.add_developer(current_user)
-        public_project.add_developer(current_user)
-      end
-
-      context 'when profile is public' do
-        it 'returns all the events' do
-          expect(finder.execute).to include(private_event, internal_event, public_event)
-        end
-      end
-
-      context 'when profile is private' do
-        it 'returns no event' do
-          allow(Ability).to receive(:allowed?).and_call_original
-          allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, project_owner).and_return(false)
-          expect(finder.execute).to be_empty
-        end
-      end
+    context 'when profile is private' do
+      it 'returns no event' do
+        allow(Ability).to receive(:allowed?).and_call_original
+        allow(Ability).to receive(:allowed?).with(current_user, :read_user_profile, project_owner).and_return(false)
 
-      it 'does not include the events if the user cannot read cross project' do
-        expect(Ability).to receive(:allowed?).and_call_original
-        expect(Ability).to receive(:allowed?).with(current_user, :read_cross_project) { false }
         expect(finder.execute).to be_empty
       end
     end
 
-    context 'when current user is anonymous' do
-      let(:current_user) { nil }
-
-      it 'returns public events only' do
-        expect(finder.execute).to eq([public_event])
-      end
+    it 'does not include the events if the user cannot read cross project' do
+      expect(Ability).to receive(:allowed?).and_call_original
+      expect(Ability).to receive(:allowed?).with(current_user, :read_cross_project) { false }
+      expect(finder.execute).to be_empty
     end
   end
 end
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
index 41d8bfff7e7ef002f3d79f8c5c90c56769545bb1..b45ae5bbb0f9a84bd1a429ff6c101a4826ba9216 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -30,7 +30,7 @@ describe('Multi-file editor commit sidebar list item', () => {
   });
 
   it('renders file path', () => {
-    expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path);
+    expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent).toContain(f.path);
   });
 
   it('renders actionn button', () => {
diff --git a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
index a5b906da8a1e672a732f63578a3c7515a14c9ab8..e09ccbe2a63d0782529368d053e5ac05aa083f20 100644
--- a/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/stage_button_spec.js
@@ -29,7 +29,7 @@ describe('IDE stage file button', () => {
   });
 
   it('renders button to discard & stage', () => {
-    expect(vm.$el.querySelectorAll('.btn').length).toBe(2);
+    expect(vm.$el.querySelectorAll('.btn-blank').length).toBe(2);
   });
 
   it('calls store with stage button', () => {
@@ -39,7 +39,7 @@ describe('IDE stage file button', () => {
   });
 
   it('calls store with discard button', () => {
-    vm.$el.querySelector('.dropdown-menu button').click();
+    vm.$el.querySelector('.btn-danger').click();
 
     expect(vm.discardFileChanges).toHaveBeenCalledWith(f.path);
   });
diff --git a/spec/javascripts/ide/components/file_templates/bar_spec.js b/spec/javascripts/ide/components/file_templates/bar_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..a688f7f69a61eac220e9ea4795c502ead7b94ae5
--- /dev/null
+++ b/spec/javascripts/ide/components/file_templates/bar_spec.js
@@ -0,0 +1,117 @@
+import Vue from 'vue';
+import { createStore } from '~/ide/stores';
+import Bar from '~/ide/components/file_templates/bar.vue';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore, file } from '../../helpers';
+
+describe('IDE file templates bar component', () => {
+  let Component;
+  let vm;
+
+  beforeAll(() => {
+    Component = Vue.extend(Bar);
+  });
+
+  beforeEach(() => {
+    const store = createStore();
+
+    store.state.openFiles.push({
+      ...file('file'),
+      opened: true,
+      active: true,
+    });
+
+    vm = mountComponentWithStore(Component, { store });
+  });
+
+  afterEach(() => {
+    vm.$destroy();
+    resetStore(vm.$store);
+  });
+
+  describe('template type dropdown', () => {
+    it('renders dropdown component', () => {
+      expect(vm.$el.querySelector('.dropdown').textContent).toContain('Choose a type');
+    });
+
+    it('calls setSelectedTemplateType when clicking item', () => {
+      spyOn(vm, 'setSelectedTemplateType').and.stub();
+
+      vm.$el.querySelector('.dropdown-content button').click();
+
+      expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
+        name: '.gitlab-ci.yml',
+        key: 'gitlab_ci_ymls',
+      });
+    });
+  });
+
+  describe('template dropdown', () => {
+    beforeEach(done => {
+      vm.$store.state.fileTemplates.templates = [
+        {
+          name: 'test',
+        },
+      ];
+      vm.$store.state.fileTemplates.selectedTemplateType = {
+        name: '.gitlab-ci.yml',
+        key: 'gitlab_ci_ymls',
+      };
+
+      vm.$nextTick(done);
+    });
+
+    it('renders dropdown component', () => {
+      expect(vm.$el.querySelectorAll('.dropdown')[1].textContent).toContain('Choose a template');
+    });
+
+    it('calls fetchTemplate on click', () => {
+      spyOn(vm, 'fetchTemplate').and.stub();
+
+      vm.$el
+        .querySelectorAll('.dropdown-content')[1]
+        .querySelector('button')
+        .click();
+
+      expect(vm.fetchTemplate).toHaveBeenCalledWith({
+        name: 'test',
+      });
+    });
+  });
+
+  it('shows undo button if updateSuccess is true', done => {
+    vm.$store.state.fileTemplates.updateSuccess = true;
+
+    vm.$nextTick(() => {
+      expect(vm.$el.querySelector('.btn-default').style.display).not.toBe('none');
+
+      done();
+    });
+  });
+
+  it('calls undoFileTemplate when clicking undo button', () => {
+    spyOn(vm, 'undoFileTemplate').and.stub();
+
+    vm.$el.querySelector('.btn-default').click();
+
+    expect(vm.undoFileTemplate).toHaveBeenCalled();
+  });
+
+  it('calls setSelectedTemplateType if activeFile name matches a template', done => {
+    const fileName = '.gitlab-ci.yml';
+
+    spyOn(vm, 'setSelectedTemplateType');
+    vm.$store.state.openFiles[0].name = fileName;
+
+    vm.setInitialType();
+
+    vm.$nextTick(() => {
+      expect(vm.setSelectedTemplateType).toHaveBeenCalledWith({
+        name: fileName,
+        key: 'gitlab_ci_ymls',
+      });
+
+      done();
+    });
+  });
+});
diff --git a/spec/javascripts/ide/components/file_templates/dropdown_spec.js b/spec/javascripts/ide/components/file_templates/dropdown_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..898796f4fa0f3633cdd313de2f424b54def72668
--- /dev/null
+++ b/spec/javascripts/ide/components/file_templates/dropdown_spec.js
@@ -0,0 +1,201 @@
+import $ from 'jquery';
+import Vue from 'vue';
+import { createStore } from '~/ide/stores';
+import Dropdown from '~/ide/components/file_templates/dropdown.vue';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { resetStore } from '../../helpers';
+
+describe('IDE file templates dropdown component', () => {
+  let Component;
+  let vm;
+
+  beforeAll(() => {
+    Component = Vue.extend(Dropdown);
+  });
+
+  beforeEach(() => {
+    const store = createStore();
+
+    vm = createComponentWithStore(Component, store, {
+      label: 'Test',
+    }).$mount();
+  });
+
+  afterEach(() => {
+    vm.$destroy();
+    resetStore(vm.$store);
+  });
+
+  describe('async', () => {
+    beforeEach(() => {
+      vm.isAsyncData = true;
+    });
+
+    it('calls async store method on Bootstrap dropdown event', () => {
+      spyOn(vm, 'fetchTemplateTypes').and.stub();
+
+      $(vm.$el).trigger('show.bs.dropdown');
+
+      expect(vm.fetchTemplateTypes).toHaveBeenCalled();
+    });
+
+    it('renders templates when async', done => {
+      vm.$store.state.fileTemplates.templates = [
+        {
+          name: 'test',
+        },
+      ];
+
+      vm.$nextTick(() => {
+        expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test');
+
+        done();
+      });
+    });
+
+    it('renders loading icon when isLoading is true', done => {
+      vm.$store.state.fileTemplates.isLoading = true;
+
+      vm.$nextTick(() => {
+        expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
+
+        done();
+      });
+    });
+
+    it('searches template data', () => {
+      vm.$store.state.fileTemplates.templates = [
+        {
+          name: 'test',
+        },
+      ];
+      vm.searchable = true;
+      vm.search = 'hello';
+
+      expect(vm.outputData).toEqual([]);
+    });
+
+    it('does not filter data is searchable is false', () => {
+      vm.$store.state.fileTemplates.templates = [
+        {
+          name: 'test',
+        },
+      ];
+      vm.search = 'hello';
+
+      expect(vm.outputData).toEqual([
+        {
+          name: 'test',
+        },
+      ]);
+    });
+
+    it('calls clickItem on click', done => {
+      spyOn(vm, 'clickItem').and.stub();
+
+      vm.$store.state.fileTemplates.templates = [
+        {
+          name: 'test',
+        },
+      ];
+
+      vm.$nextTick(() => {
+        vm.$el.querySelector('.dropdown-content button').click();
+
+        expect(vm.clickItem).toHaveBeenCalledWith({
+          name: 'test',
+        });
+
+        done();
+      });
+    });
+
+    it('renders input when searchable is true', done => {
+      vm.searchable = true;
+
+      vm.$nextTick(() => {
+        expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null);
+
+        done();
+      });
+    });
+
+    it('does not render input when searchable is true & showLoading is true', done => {
+      vm.searchable = true;
+      vm.$store.state.fileTemplates.isLoading = true;
+
+      vm.$nextTick(() => {
+        expect(vm.$el.querySelector('.dropdown-input')).toBe(null);
+
+        done();
+      });
+    });
+  });
+
+  describe('sync', () => {
+    beforeEach(done => {
+      vm.data = [
+        {
+          name: 'test sync',
+        },
+      ];
+
+      vm.$nextTick(done);
+    });
+
+    it('renders props data', () => {
+      expect(vm.$el.querySelector('.dropdown-content').textContent).toContain('test sync');
+    });
+
+    it('renders input when searchable is true', done => {
+      vm.searchable = true;
+
+      vm.$nextTick(() => {
+        expect(vm.$el.querySelector('.dropdown-input')).not.toBe(null);
+
+        done();
+      });
+    });
+
+    it('calls clickItem on click', done => {
+      spyOn(vm, 'clickItem').and.stub();
+
+      vm.$nextTick(() => {
+        vm.$el.querySelector('.dropdown-content button').click();
+
+        expect(vm.clickItem).toHaveBeenCalledWith({
+          name: 'test sync',
+        });
+
+        done();
+      });
+    });
+
+    it('searches template data', () => {
+      vm.searchable = true;
+      vm.search = 'hello';
+
+      expect(vm.outputData).toEqual([]);
+    });
+
+    it('does not filter data is searchable is false', () => {
+      vm.search = 'hello';
+
+      expect(vm.outputData).toEqual([
+        {
+          name: 'test sync',
+        },
+      ]);
+    });
+
+    it('renders dropdown title', done => {
+      vm.title = 'Test title';
+
+      vm.$nextTick(() => {
+        expect(vm.$el.querySelector('.dropdown-title').textContent).toContain('Test title');
+
+        done();
+      });
+    });
+  });
+});
diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js
index 30cd92b2ca485264db4938747c01420b9f1252d8..d09ccd7ac34fded59199d5e6e0a9a9a8f4b3539c 100644
--- a/spec/javascripts/ide/components/repo_commit_section_spec.js
+++ b/spec/javascripts/ide/components/repo_commit_section_spec.js
@@ -111,7 +111,7 @@ describe('RepoCommitSection', () => {
       .then(vm.$nextTick)
       .then(() => {
         expect(vm.$el.querySelector('.ide-commit-list-container').textContent).toContain(
-          'No changes',
+          'There are no unstaged changes',
         );
       })
       .then(done)
@@ -133,7 +133,7 @@ describe('RepoCommitSection', () => {
   });
 
   it('discards a single file', done => {
-    vm.$el.querySelector('.multi-file-discard-btn .dropdown-menu button').click();
+    vm.$el.querySelector('.multi-file-commit-list li:first-child .js-modal-primary-action').click();
 
     Vue.nextTick(() => {
       expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1');
diff --git a/spec/javascripts/ide/helpers.js b/spec/javascripts/ide/helpers.js
index c11c482fef8efb25d6fe20bba1864191ac51af79..3ce9c9fcda112ba30df4df70ef75a2559d5bc6e0 100644
--- a/spec/javascripts/ide/helpers.js
+++ b/spec/javascripts/ide/helpers.js
@@ -5,6 +5,7 @@ import commitState from '~/ide/stores/modules/commit/state';
 import mergeRequestsState from '~/ide/stores/modules/merge_requests/state';
 import pipelinesState from '~/ide/stores/modules/pipelines/state';
 import branchesState from '~/ide/stores/modules/branches/state';
+import fileTemplatesState from '~/ide/stores/modules/file_templates/state';
 
 export const resetStore = store => {
   const newState = {
@@ -13,6 +14,7 @@ export const resetStore = store => {
     mergeRequests: mergeRequestsState(),
     pipelines: pipelinesState(),
     branches: branchesState(),
+    fileTemplates: fileTemplatesState(),
   };
   store.replaceState(newState);
 };
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index bca2033ff97612271ef52ee31ab32b8d559487cc..1ca811e996b43a3032884e78b6d41426cd500439 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -692,21 +692,6 @@ describe('IDE store file actions', () => {
         .then(done)
         .catch(done.fail);
     });
-
-    it('calls scrollToTab', done => {
-      const scrollToTabSpy = jasmine.createSpy('scrollToTab');
-      const oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
-      store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
-
-      store
-        .dispatch('openPendingTab', { file: f, keyPrefix: 'pending' })
-        .then(() => {
-          expect(scrollToTabSpy).toHaveBeenCalled();
-          store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
-        })
-        .then(done)
-        .catch(done.fail);
-    });
   });
 
   describe('removePendingTab', () => {
diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js
index d84f1717a6188f7cc65d5f2f4abeb20fe008d614..c9a1158a14ed7da593e3bb823ffe00e12f4bfd48 100644
--- a/spec/javascripts/ide/stores/actions_spec.js
+++ b/spec/javascripts/ide/stores/actions_spec.js
@@ -305,7 +305,11 @@ describe('Multi-file store actions', () => {
 
   describe('stageAllChanges', () => {
     it('adds all files from changedFiles to stagedFiles', done => {
-      store.state.changedFiles.push(file(), file('new'));
+      const openFile = { ...file(), path: 'test' };
+
+      store.state.openFiles.push(openFile);
+      store.state.stagedFiles.push(openFile);
+      store.state.changedFiles.push(openFile, file('new'));
 
       testAction(
         stageAllChanges,
@@ -316,7 +320,12 @@ describe('Multi-file store actions', () => {
           { type: types.STAGE_CHANGE, payload: store.state.changedFiles[0].path },
           { type: types.STAGE_CHANGE, payload: store.state.changedFiles[1].path },
         ],
-        [],
+        [
+          {
+            type: 'openPendingTab',
+            payload: { file: openFile, keyPrefix: 'staged' },
+          },
+        ],
         done,
       );
     });
@@ -324,7 +333,11 @@ describe('Multi-file store actions', () => {
 
   describe('unstageAllChanges', () => {
     it('removes all files from stagedFiles after unstaging', done => {
-      store.state.stagedFiles.push(file(), file('new'));
+      const openFile = { ...file(), path: 'test' };
+
+      store.state.openFiles.push(openFile);
+      store.state.changedFiles.push(openFile);
+      store.state.stagedFiles.push(openFile, file('new'));
 
       testAction(
         unstageAllChanges,
@@ -334,7 +347,12 @@ describe('Multi-file store actions', () => {
           { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[0].path },
           { type: types.UNSTAGE_CHANGE, payload: store.state.stagedFiles[1].path },
         ],
-        [],
+        [
+          {
+            type: 'openPendingTab',
+            payload: { file: openFile, keyPrefix: 'unstaged' },
+          },
+        ],
         done,
       );
     });
diff --git a/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js
index f831a9f0a5d17845c1faa07eb128de5d574725a9..c29dd9f0d06958e025cfaf721259d0e45b100298 100644
--- a/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js
+++ b/spec/javascripts/ide/stores/modules/file_templates/actions_spec.js
@@ -148,14 +148,66 @@ describe('IDE file templates actions', () => {
   });
 
   describe('setSelectedTemplateType', () => {
-    it('commits SET_SELECTED_TEMPLATE_TYPE', done => {
-      testAction(
-        actions.setSelectedTemplateType,
-        'test',
-        state,
-        [{ type: types.SET_SELECTED_TEMPLATE_TYPE, payload: 'test' }],
-        [],
-        done,
+    it('commits SET_SELECTED_TEMPLATE_TYPE', () => {
+      const commit = jasmine.createSpy('commit');
+      const options = {
+        commit,
+        dispatch() {},
+        rootGetters: {
+          activeFile: {
+            name: 'test',
+            prevPath: '',
+          },
+        },
+      };
+
+      actions.setSelectedTemplateType(options, { name: 'test' });
+
+      expect(commit).toHaveBeenCalledWith(types.SET_SELECTED_TEMPLATE_TYPE, { name: 'test' });
+    });
+
+    it('dispatches discardFileChanges if prevPath matches templates name', () => {
+      const dispatch = jasmine.createSpy('dispatch');
+      const options = {
+        commit() {},
+        dispatch,
+        rootGetters: {
+          activeFile: {
+            name: 'test',
+            path: 'test',
+            prevPath: 'test',
+          },
+        },
+      };
+
+      actions.setSelectedTemplateType(options, { name: 'test' });
+
+      expect(dispatch).toHaveBeenCalledWith('discardFileChanges', 'test', { root: true });
+    });
+
+    it('dispatches renameEntry if file name doesnt match', () => {
+      const dispatch = jasmine.createSpy('dispatch');
+      const options = {
+        commit() {},
+        dispatch,
+        rootGetters: {
+          activeFile: {
+            name: 'oldtest',
+            path: 'oldtest',
+            prevPath: '',
+          },
+        },
+      };
+
+      actions.setSelectedTemplateType(options, { name: 'test' });
+
+      expect(dispatch).toHaveBeenCalledWith(
+        'renameEntry',
+        {
+          path: 'oldtest',
+          name: 'test',
+        },
+        { root: true },
       );
     });
   });
@@ -332,5 +384,20 @@ describe('IDE file templates actions', () => {
 
       expect(commit).toHaveBeenCalledWith('SET_UPDATE_SUCCESS', false);
     });
+
+    it('dispatches discardFileChanges if file has prevPath', () => {
+      const dispatch = jasmine.createSpy('dispatch');
+      const rootGetters = {
+        activeFile: { path: 'test', prevPath: 'newtest', raw: 'raw content' },
+      };
+
+      actions.undoFileTemplate({ dispatch, commit() {}, rootGetters });
+
+      expect(dispatch.calls.mostRecent().args).toEqual([
+        'discardFileChanges',
+        'test',
+        { root: true },
+      ]);
+    });
   });
 });
diff --git a/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js b/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js
index e337c3f331b42a42bb01b0b1bd50b88d40ac3652..17cb457881f1e9d06c2f8dd4cc5df306b44743c7 100644
--- a/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js
+++ b/spec/javascripts/ide/stores/modules/file_templates/getters_spec.js
@@ -1,3 +1,5 @@
+import createState from '~/ide/stores/state';
+import { activityBarViews } from '~/ide/constants';
 import * as getters from '~/ide/stores/modules/file_templates/getters';
 
 describe('IDE file templates getters', () => {
@@ -8,22 +10,49 @@ describe('IDE file templates getters', () => {
   });
 
   describe('showFileTemplatesBar', () => {
-    it('finds template type by name', () => {
+    let rootState;
+
+    beforeEach(() => {
+      rootState = createState();
+    });
+
+    it('returns true if template is found and currentActivityView is edit', () => {
+      rootState.currentActivityView = activityBarViews.edit;
+
+      expect(
+        getters.showFileTemplatesBar(
+          null,
+          {
+            templateTypes: getters.templateTypes(),
+          },
+          rootState,
+        )('LICENSE'),
+      ).toBe(true);
+    });
+
+    it('returns false if template is found and currentActivityView is not edit', () => {
+      rootState.currentActivityView = activityBarViews.commit;
+
       expect(
-        getters.showFileTemplatesBar(null, {
-          templateTypes: getters.templateTypes(),
-        })('LICENSE'),
-      ).toEqual({
-        name: 'LICENSE',
-        key: 'licenses',
-      });
+        getters.showFileTemplatesBar(
+          null,
+          {
+            templateTypes: getters.templateTypes(),
+          },
+          rootState,
+        )('LICENSE'),
+      ).toBe(false);
     });
 
     it('returns undefined if not found', () => {
       expect(
-        getters.showFileTemplatesBar(null, {
-          templateTypes: getters.templateTypes(),
-        })('test'),
+        getters.showFileTemplatesBar(
+          null,
+          {
+            templateTypes: getters.templateTypes(),
+          },
+          rootState,
+        )('test'),
       ).toBe(undefined);
     });
   });
diff --git a/spec/javascripts/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 6ce76aaa03b425b059256f48686dec3248da67c9..41dd3d3c67f8f319c493fa8f36746bbe27c171d7 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -339,5 +339,13 @@ describe('Multi-file store mutations', () => {
 
       expect(localState.entries.parentPath.tree.length).toBe(1);
     });
+
+    it('adds to openFiles if previously opened', () => {
+      localState.entries.oldPath.opened = true;
+
+      mutations.RENAME_ENTRY(localState, { path: 'oldPath', name: 'newPath' });
+
+      expect(localState.openFiles).toEqual([localState.entries.newPath]);
+    });
   });
 });
diff --git a/spec/javascripts/monitoring/graph/flag_spec.js b/spec/javascripts/monitoring/graph/flag_spec.js
index 91453a367b9262123a3dee03fd11e9b4fbd17018..ab12ed447b2823661b4401391b68f9bedb392c93 100644
--- a/spec/javascripts/monitoring/graph/flag_spec.js
+++ b/spec/javascripts/monitoring/graph/flag_spec.js
@@ -35,7 +35,7 @@ const defaultValuesComponent = {
   unitOfDisplay: 'ms',
   currentDataIndex: 0,
   legendTitle: 'Average',
-  currentCoordinates: [],
+  currentCoordinates: {},
 };
 
 const deploymentFlagData = {
diff --git a/spec/javascripts/monitoring/graph_spec.js b/spec/javascripts/monitoring/graph_spec.js
index a46a387a5345306232bbd5569bebd33bfe6f62f6..990619b4109699f1d5c792d5788d3c34eea84d0a 100644
--- a/spec/javascripts/monitoring/graph_spec.js
+++ b/spec/javascripts/monitoring/graph_spec.js
@@ -113,6 +113,9 @@ describe('Graph', () => {
       projectPath,
     });
 
+    // simulate moving mouse over data series
+    component.seriesUnderMouse = component.timeSeries;
+
     component.positionFlag();
     expect(component.currentData).toBe(component.timeSeries[0].values[10]);
   });
diff --git a/spec/lib/api/helpers/pagination_spec.rb b/spec/lib/api/helpers/pagination_spec.rb
index c73c6023b60c468dd9ffc3fd1fb432aa7a8d6db1..0a7682d906bb5a5e1bed44fb99a51cea4511edf4 100644
--- a/spec/lib/api/helpers/pagination_spec.rb
+++ b/spec/lib/api/helpers/pagination_spec.rb
@@ -189,9 +189,9 @@
           it 'it returns the right link to the next page' do
             allow(subject).to receive(:params)
               .and_return({ pagination: 'keyset', ks_prev_id: projects[3].id, ks_prev_name: projects[3].name, per_page: 2 })
+
             expect_header('X-Per-Page', '2')
             expect_header('X-Next-Page', "#{value}?ks_prev_id=#{projects[6].id}&ks_prev_name=#{projects[6].name}&pagination=keyset&per_page=2")
-
             expect_header('Link', anything) do |_key, val|
               expect(val).to include('rel="next"')
             end
diff --git a/spec/lib/banzai/filter/spaced_link_filter_spec.rb b/spec/lib/banzai/filter/spaced_link_filter_spec.rb
index 4463c0115228f9b4fbe8cf1a7742ac20b9278a79..1ad7f3ff5675443550c441c6623bc9269a634b80 100644
--- a/spec/lib/banzai/filter/spaced_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/spaced_link_filter_spec.rb
@@ -3,49 +3,73 @@
 describe Banzai::Filter::SpacedLinkFilter do
   include FilterSpecHelper
 
-  let(:link) { '[example](page slug)' }
+  let(:link)  { '[example](page slug)' }
+  let(:image) { '![example](img test.jpg)' }
 
-  it 'converts slug with spaces to a link' do
-    doc = filter("See #{link}")
+  context 'when a link is detected' do
+    it 'converts slug with spaces to a link' do
+      doc = filter("See #{link}")
 
-    expect(doc.at_css('a').text).to eq 'example'
-    expect(doc.at_css('a')['href']).to eq 'page%20slug'
-    expect(doc.at_css('p')).to eq nil
-  end
+      expect(doc.at_css('a').text).to eq 'example'
+      expect(doc.at_css('a')['href']).to eq 'page%20slug'
+      expect(doc.at_css('a')['title']).to be_nil
+      expect(doc.at_css('p')).to be_nil
+    end
 
-  it 'converts slug with spaces and a title to a link' do
-    link = '[example](page slug "title")'
-    doc  = filter("See #{link}")
+    it 'converts slug with spaces and a title to a link' do
+      link = '[example](page slug "title")'
+      doc  = filter("See #{link}")
 
-    expect(doc.at_css('a').text).to eq 'example'
-    expect(doc.at_css('a')['href']).to eq 'page%20slug'
-    expect(doc.at_css('a')['title']).to eq 'title'
-    expect(doc.at_css('p')).to eq nil
-  end
+      expect(doc.at_css('a').text).to eq 'example'
+      expect(doc.at_css('a')['href']).to eq 'page%20slug'
+      expect(doc.at_css('a')['title']).to eq 'title'
+      expect(doc.at_css('p')).to be_nil
+    end
 
-  it 'does nothing when markdown_engine is redcarpet' do
-    exp = act = link
-    expect(filter(act, markdown_engine: :redcarpet).to_html).to eq exp
-  end
+    it 'does nothing when markdown_engine is redcarpet' do
+      exp = act = link
+      expect(filter(act, markdown_engine: :redcarpet).to_html).to eq exp
+    end
+
+    it 'does nothing with empty text' do
+      link = '[](page slug)'
+      doc  = filter("See #{link}")
+
+      expect(doc.at_css('a')).to be_nil
+    end
 
-  it 'does nothing with empty text' do
-    link = '[](page slug)'
-    doc  = filter("See #{link}")
+    it 'does nothing with an empty slug' do
+      link = '[example]()'
+      doc  = filter("See #{link}")
 
-    expect(doc.at_css('a')).to eq nil
+      expect(doc.at_css('a')).to be_nil
+    end
   end
 
-  it 'does nothing with an empty slug' do
-    link = '[example]()'
-    doc  = filter("See #{link}")
+  context 'when an image is detected' do
+    it 'converts slug with spaces to an iamge' do
+      doc = filter("See #{image}")
+
+      expect(doc.at_css('img')['src']).to eq 'img%20test.jpg'
+      expect(doc.at_css('img')['alt']).to eq 'example'
+      expect(doc.at_css('p')).to be_nil
+    end
+
+    it 'converts slug with spaces and a title to an image' do
+      image = '![example](img test.jpg "title")'
+      doc   = filter("See #{image}")
 
-    expect(doc.at_css('a')).to eq nil
+      expect(doc.at_css('img')['src']).to eq 'img%20test.jpg'
+      expect(doc.at_css('img')['alt']).to eq 'example'
+      expect(doc.at_css('img')['title']).to eq 'title'
+      expect(doc.at_css('p')).to be_nil
+    end
   end
 
   it 'converts multiple URLs' do
     link1 = '[first](slug one)'
     link2 = '[second](http://example.com/slug two)'
-    doc   = filter("See #{link1} and #{link2}")
+    doc   = filter("See #{link1} and #{image} and #{link2}")
 
     found_links = doc.css('a')
 
@@ -54,6 +78,12 @@
     expect(found_links[0]['href']).to eq 'slug%20one'
     expect(found_links[1].text).to eq 'second'
     expect(found_links[1]['href']).to eq 'http://example.com/slug%20two'
+
+    found_images = doc.css('img')
+
+    expect(found_images.size).to eq(1)
+    expect(found_images[0]['src']).to eq 'img%20test.jpg'
+    expect(found_images[0]['alt']).to eq 'example'
   end
 
   described_class::IGNORE_PARENTS.each do |elem|
diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
index 52b8c9be6470d253802e5cdcf9e9478160bd8db6..64ca3ec345d1d8e8a64512ad62c0284884240c68 100644
--- a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb
@@ -178,4 +178,25 @@
       end
     end
   end
+
+  describe 'videos' do
+    let(:namespace) { create(:namespace, name: "wiki_link_ns") }
+    let(:project)   { create(:project, :public, name: "wiki_link_project", namespace: namespace) }
+    let(:project_wiki) { ProjectWiki.new(project, double(:user)) }
+    let(:page) { build(:wiki_page, wiki: project_wiki, page: OpenStruct.new(url_path: 'nested/twice/start-page')) }
+
+    it 'generates video html structure' do
+      markdown = "![video_file](video_file_name.mp4)"
+      output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+      expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video_file_name.mp4"')
+    end
+
+    it 'rewrites and replaces video links names with white spaces to %20' do
+      markdown = "![video file](video file name.mp4)"
+      output = described_class.to_html(markdown, project: project, project_wiki: project_wiki, page_slug: page.slug)
+
+      expect(output).to include('<video src="/wiki_link_ns/wiki_link_project/wikis/nested/twice/video%20file%20name.mp4"')
+    end
+  end
 end
diff --git a/spec/lib/forever_spec.rb b/spec/lib/forever_spec.rb
index cf40c467c722ae6dea74ae1d76d42be15345a6dc..494c0561975c6e6f1aa2f4b18982e9db611040f4 100644
--- a/spec/lib/forever_spec.rb
+++ b/spec/lib/forever_spec.rb
@@ -7,6 +7,7 @@
     context 'when using PostgreSQL' do
       it 'should return Postgresql future date' do
         allow(Gitlab::Database).to receive(:postgresql?).and_return(true)
+
         expect(subject).to eq(described_class::POSTGRESQL_DATE)
       end
     end
@@ -14,6 +15,7 @@
     context 'when using MySQL' do
       it 'should return MySQL future date' do
         allow(Gitlab::Database).to receive(:postgresql?).and_return(false)
+
         expect(subject).to eq(described_class::MYSQL_DATE)
       end
     end
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
index 2c63f3b0455f1f13a643fbacdc5debef8bf9489d..6d29044ffd52e68dc49b8b8af21186af05a254b6 100644
--- a/spec/lib/gitlab/contributions_calendar_spec.rb
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -62,13 +62,16 @@ def create_event(project, day, hour = 0, action = Event::CREATED, target_symbol
       expect(calendar.activity_dates).to eq(last_week => 2, today => 1)
     end
 
-    it "only shows private events to authorized users" do
-      create_event(private_project, today)
-      create_event(feature_project, today)
+    context "when the user has opted-in for private contributions" do
+      it "shows private and public events to all users" do
+        user.update_column(:include_private_contributions, true)
+        create_event(private_project, today)
+        create_event(public_project, today)
 
-      expect(calendar.activity_dates[today]).to eq(0)
-      expect(calendar(user).activity_dates[today]).to eq(0)
-      expect(calendar(contributor).activity_dates[today]).to eq(2)
+        expect(calendar.activity_dates[today]).to eq(1)
+        expect(calendar(user).activity_dates[today]).to eq(1)
+        expect(calendar(contributor).activity_dates[today]).to eq(2)
+      end
     end
 
     it "counts the diff notes on merge request" do
@@ -128,7 +131,7 @@ def create_event(project, day, hour = 0, action = Event::CREATED, target_symbol
       e3 = create_event(feature_project, today)
       create_event(public_project, last_week)
 
-      expect(calendar.events_by_date(today)).to contain_exactly(e1)
+      expect(calendar.events_by_date(today)).to contain_exactly(e1, e3)
       expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3)
     end
 
diff --git a/spec/lib/gitlab/diff/highlight_cache_spec.rb b/spec/lib/gitlab/diff/highlight_cache_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..bfcfed4231f7a95d139031f12f888cc230ef6e6c
--- /dev/null
+++ b/spec/lib/gitlab/diff/highlight_cache_spec.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Diff::HighlightCache do
+  let(:merge_request) { create(:merge_request_with_diffs) }
+
+  subject(:cache) { described_class.new(merge_request.diffs, backend: backend) }
+
+  describe '#decorate' do
+    let(:backend) { double('backend').as_null_object }
+
+    # Manually creates a Diff::File object to avoid triggering the cache on
+    # the FileCollection::MergeRequestDiff
+    let(:diff_file) do
+      diffs = merge_request.diffs
+      raw_diff = diffs.diffable.raw_diffs(diffs.diff_options.merge(paths: ['CHANGELOG'])).first
+      Gitlab::Diff::File.new(raw_diff,
+                             repository: diffs.project.repository,
+                             diff_refs: diffs.diff_refs,
+                             fallback_diff_refs: diffs.fallback_diff_refs)
+    end
+
+    it 'does not calculate highlighting when reading from cache' do
+      cache.write_if_empty
+      cache.decorate(diff_file)
+
+      expect_any_instance_of(Gitlab::Diff::Highlight).not_to receive(:highlight)
+
+      diff_file.highlighted_diff_lines
+    end
+
+    it 'assigns highlighted diff lines to the DiffFile' do
+      cache.write_if_empty
+      cache.decorate(diff_file)
+
+      expect(diff_file.highlighted_diff_lines.size).to be > 5
+    end
+
+    it 'submits a single reading from the cache' do
+      cache.decorate(diff_file)
+      cache.decorate(diff_file)
+
+      expect(backend).to have_received(:read).with(cache.key).once
+    end
+  end
+
+  describe '#write_if_empty' do
+    let(:backend) { double('backend', read: {}).as_null_object }
+
+    it 'submits a single writing to the cache' do
+      cache.write_if_empty
+      cache.write_if_empty
+
+      expect(backend).to have_received(:write).with(cache.key,
+                                                    hash_including('CHANGELOG-false-false-false'),
+                                                    expires_in: 1.week).once
+    end
+  end
+
+  describe '#clear' do
+    let(:backend) { double('backend').as_null_object }
+
+    it 'clears cache' do
+      cache.clear
+
+      expect(backend).to have_received(:delete).with(cache.key)
+    end
+  end
+end
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 1098a2661405a792cd0adc89be26fb710ed0e255..28c34e234f7f96efed5e2a52aa7d88ca61c7ff7b 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -591,6 +591,10 @@ def new_repository_path
       expect(repository.find_remote_root_ref('origin')).to eq 'master'
     end
 
+    it 'returns UTF-8' do
+      expect(repository.find_remote_root_ref('origin')).to be_utf8
+    end
+
     it 'returns nil when remote name is nil' do
       expect_any_instance_of(Gitlab::GitalyClient::RemoteService)
         .not_to receive(:find_remote_root_ref)
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index 54f2ea33f90cfdf268ec5363b124d86a4ac9b67d..bcdf12a00a0e50fe1731861db1eb2f835e9596fc 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -19,7 +19,14 @@
           right_commit_id: commit.id,
           collapse_diffs: false,
           enforce_limits: true,
-          **Gitlab::Git::DiffCollection.collection_limits.to_h
+          # Tests limitation parameters explicitly
+          max_files: 100,
+          max_lines: 5000,
+          max_bytes: 512000,
+          safe_max_files: 100,
+          safe_max_lines: 5000,
+          safe_max_bytes: 512000,
+          max_patch_bytes: 102400
         )
 
         expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash))
@@ -37,7 +44,14 @@
           right_commit_id: initial_commit.id,
           collapse_diffs: false,
           enforce_limits: true,
-          **Gitlab::Git::DiffCollection.collection_limits.to_h
+          # Tests limitation parameters explicitly
+          max_files: 100,
+          max_lines: 5000,
+          max_bytes: 512000,
+          safe_max_files: 100,
+          safe_max_lines: 5000,
+          safe_max_bytes: 512000,
+          max_patch_bytes: 102400
         )
 
         expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_diff).with(request, kind_of(Hash))
diff --git a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
index b8831c54aba3865bdf44c00e0c255983a7718467..9030a49983de04e16652dcd3340c5bc02ea469eb 100644
--- a/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/remote_service_spec.rb
@@ -54,6 +54,15 @@
 
       expect(client.find_remote_root_ref('origin')).to eq 'master'
     end
+
+    it 'ensure ref is a valid UTF-8 string' do
+      expect_any_instance_of(Gitaly::RemoteService::Stub)
+        .to receive(:find_remote_root_ref)
+        .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+        .and_return(double(ref: "an_invalid_ref_\xE5"))
+
+      expect(client.find_remote_root_ref('origin')).to eq "an_invalid_ref_Ã¥"
+    end
   end
 
   describe '#update_remote_mirror' do
diff --git a/spec/migrations/remove_orphaned_label_links_spec.rb b/spec/migrations/remove_orphaned_label_links_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..13b8919343e786bc784dc8c0f6e539674b086482
--- /dev/null
+++ b/spec/migrations/remove_orphaned_label_links_spec.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180906051323_remove_orphaned_label_links.rb')
+
+describe RemoveOrphanedLabelLinks, :migration do
+  let(:label_links) { table(:label_links) }
+  let(:labels) { table(:labels) }
+
+  let(:project) { create(:project) } # rubocop:disable RSpec/FactoriesInMigrationSpecs
+  let(:label) { create_label }
+
+  context 'add foreign key on label_id' do
+    let!(:label_link_with_label) { create_label_link(label_id: label.id) }
+    let!(:label_link_without_label) { create_label_link(label_id: nil) }
+
+    it 'removes orphaned labels without corresponding label' do
+      expect { migrate! }.to change { LabelLink.count }.from(2).to(1)
+    end
+
+    it 'does not remove entries with valid label_id' do
+      expect { migrate! }.not_to change { label_link_with_label.reload }
+    end
+  end
+
+  def create_label(**opts)
+    labels.create!(
+      project_id: project.id,
+      **opts
+    )
+  end
+
+  def create_label_link(**opts)
+    label_links.create!(
+      target_id: 1,
+      target_type: 'Issue',
+      **opts
+    )
+  end
+end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 54f1a0e38a598db655f559dafad3dec84bc1f0f7..788b3179b018f10b99a94754541980f531df958f 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -231,12 +231,12 @@
       end
 
       it 'logs exception when transition id is not valid' do
-        allow(Rails.logger).to receive(:info)
-        WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise('Bad Request')
+        allow(@jira_service).to receive(:log_error)
+        WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise("Bad Request")
 
         @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
 
-        expect(Rails.logger).to have_received(:info).with('JiraService Issue Transition failed message ERROR: http://jira.example.com - Bad Request')
+        expect(@jira_service).to have_received(:log_error).with("Issue transition failed", error: "Bad Request", client_url: "http://jira.example.com")
       end
 
       it 'calls the api with jira_issue_transition_id' do
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 029ad7f3e9f2afbe372bf1e33ac3c0345714ca03..25eecb3f9090f28890b9d23ff1b5062ab465bb0b 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -345,4 +345,31 @@ def fields
       expect(service.api_field_names).to eq(['safe_field'])
     end
   end
+
+  context 'logging' do
+    let(:project) { create(:project) }
+    let(:service) { create(:service, project: project) }
+    let(:test_message) { "test message" }
+    let(:arguments) do
+      {
+        service_class: service.class.name,
+        project_path: project.full_path,
+        project_id: project.id,
+        message: test_message,
+        additional_argument: 'some argument'
+      }
+    end
+
+    it 'logs info messages using json logger' do
+      expect(Gitlab::JsonLogger).to receive(:info).with(arguments)
+
+      service.log_info(test_message, additional_argument: 'some argument')
+    end
+
+    it 'logs error messages using json logger' do
+      expect(Gitlab::JsonLogger).to receive(:error).with(arguments)
+
+      service.log_error(test_message, additional_argument: 'some argument')
+    end
+  end
 end
diff --git a/spec/services/merge_requests/reload_diffs_service_spec.rb b/spec/services/merge_requests/reload_diffs_service_spec.rb
index a0a27d247fcedf8be6599f4a6b10de17625b2054..21f369a3818ec9f915150282261b5129dfe8dd71 100644
--- a/spec/services/merge_requests/reload_diffs_service_spec.rb
+++ b/spec/services/merge_requests/reload_diffs_service_spec.rb
@@ -57,6 +57,7 @@
         expect(Rails.cache).to receive(:delete).with(old_cache_key).and_call_original
         expect(Rails.cache).to receive(:read).with(new_cache_key).and_call_original
         expect(Rails.cache).to receive(:write).with(new_cache_key, anything, anything).and_call_original
+
         subject.execute
       end
     end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 0dfd69155270223c10a6e1450c551dc400d85464..da60916428ecc7d924f36152108f302ed209e58a 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -688,7 +688,7 @@ def build_note(old_assignees, new_assignees)
     let(:jira_tracker)    { project.jira_service }
     let(:commit)          { project.commit }
     let(:comment_url)     { jira_api_comment_url(jira_issue.id) }
-    let(:success_message) { "JiraService SUCCESS: Successfully posted to http://jira.example.net." }
+    let(:success_message) { "SUCCESS: Successfully posted to http://jira.example.net." }
 
     before do
       stub_jira_urls(jira_issue.id)
diff --git a/spec/services/wikis/create_attachment_service_spec.rb b/spec/services/wikis/create_attachment_service_spec.rb
index 3f4da873ce44f8fed28353fd08d6c218be4d0c40..f5899f292c898e7be7466f17e6a7844de81491d4 100644
--- a/spec/services/wikis/create_attachment_service_spec.rb
+++ b/spec/services/wikis/create_attachment_service_spec.rb
@@ -88,8 +88,30 @@
     end
   end
 
-  describe 'validations' do
+  describe '#parse_file_name' do
     context 'when file_name' do
+      context 'has white spaces' do
+        let(:file_name) { 'file with spaces' }
+
+        it "replaces all of them with '_'" do
+          result = service.execute
+
+          expect(result[:status]).to eq :success
+          expect(result[:result][:file_name]).to eq 'file_with_spaces'
+        end
+      end
+
+      context 'has other invalid characters' do
+        let(:file_name) { "file\twith\tinvalid chars" }
+
+        it "replaces all of them with '_'" do
+          result = service.execute
+
+          expect(result[:status]).to eq :success
+          expect(result[:result][:file_name]).to eq 'file_with_invalid_chars'
+        end
+      end
+
       context 'is not present' do
         let(:file_name) { nil }
 
diff --git a/spec/tasks/gitlab/cleanup_rake_spec.rb b/spec/tasks/gitlab/cleanup_rake_spec.rb
index cc2cca10f585dcad6b34327216e5fb8c5363587d..19794227d9f7fd7f1839d2936f8b8d034225af2f 100644
--- a/spec/tasks/gitlab/cleanup_rake_spec.rb
+++ b/spec/tasks/gitlab/cleanup_rake_spec.rb
@@ -6,6 +6,8 @@
   end
 
   describe 'cleanup namespaces and repos' do
+    let(:gitlab_shell) { Gitlab::Shell.new }
+    let(:storage) { storages.keys.first }
     let(:storages) do
       {
         'default' => Gitlab::GitalyClient::StorageSettings.new(@default_storage_hash.merge('path' => 'tmp/tests/default_storage'))
@@ -17,53 +19,56 @@
     end
 
     before do
-      FileUtils.mkdir(Settings.absolute('tmp/tests/default_storage'))
       allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
     end
 
     after do
-      FileUtils.rm_rf(Settings.absolute('tmp/tests/default_storage'))
+      Gitlab::GitalyClient::StorageService.new(storage).delete_all_repositories
     end
 
     describe 'cleanup:repos' do
       before do
-        FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/broken/project.git'))
-        FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))
+        gitlab_shell.add_namespace(storage, 'broken/project.git')
+        gitlab_shell.add_namespace(storage, '@hashed/12/34/5678.git')
       end
 
       it 'moves it to an orphaned path' do
-        run_rake_task('gitlab:cleanup:repos')
-        repo_list = Dir['tmp/tests/default_storage/broken/*']
+        now = Time.now
+
+        Timecop.freeze(now) do
+          run_rake_task('gitlab:cleanup:repos')
+          repo_list = Gitlab::GitalyClient::StorageService.new(storage).list_directories(depth: 0)
 
-        expect(repo_list.first).to include('+orphaned+')
+          expect(repo_list.last).to include("broken+orphaned+#{now.to_i}")
+        end
       end
 
       it 'ignores @hashed repos' do
         run_rake_task('gitlab:cleanup:repos')
 
-        expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))).to be_truthy
+        expect(gitlab_shell.exists?(storage, '@hashed/12/34/5678.git')).to be(true)
       end
     end
 
     describe 'cleanup:dirs' do
       it 'removes missing namespaces' do
-        FileUtils.mkdir_p(Settings.absolute("tmp/tests/default_storage/namespace_1/project.git"))
-        FileUtils.mkdir_p(Settings.absolute("tmp/tests/default_storage/namespace_2/project.git"))
-        allow(Namespace).to receive(:pluck).and_return('namespace_1')
+        gitlab_shell.add_namespace(storage, "namespace_1/project.git")
+        gitlab_shell.add_namespace(storage, "namespace_2/project.git")
+        allow(Namespace).to receive(:pluck).and_return(['namespace_1'])
 
         stub_env('REMOVE', 'true')
         run_rake_task('gitlab:cleanup:dirs')
 
-        expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/namespace_1'))).to be_truthy
-        expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/namespace_2'))).to be_falsey
+        expect(gitlab_shell.exists?(storage, 'namespace_1')).to be(true)
+        expect(gitlab_shell.exists?(storage, 'namespace_2')).to be(false)
       end
 
       it 'ignores @hashed directory' do
-        FileUtils.mkdir_p(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))
+        gitlab_shell.add_namespace(storage, '@hashed/12/34/5678.git')
 
         run_rake_task('gitlab:cleanup:dirs')
 
-        expect(Dir.exist?(Settings.absolute('tmp/tests/default_storage/@hashed/12/34/5678.git'))).to be_truthy
+        expect(gitlab_shell.exists?(storage, '@hashed/12/34/5678.git')).to be(true)
       end
     end
   end
diff --git a/spec/uploaders/namespace_file_uploader_spec.rb b/spec/uploaders/namespace_file_uploader_spec.rb
index 71fe2c353c07fc0e4f6b1bdca5ab103f38ac5c79..eafbea07e10cac0a992d28202a39009d692979f1 100644
--- a/spec/uploaders/namespace_file_uploader_spec.rb
+++ b/spec/uploaders/namespace_file_uploader_spec.rb
@@ -26,6 +26,20 @@
                     upload_path: IDENTIFIER
   end
 
+  context '.base_dir' do
+    it 'returns local storage base_dir without store param' do
+      expect(described_class.base_dir(group)).to eq("uploads/-/system/namespace/#{group.id}")
+    end
+
+    it 'returns local storage base_dir when store param is Store::LOCAL' do
+      expect(described_class.base_dir(group, ObjectStorage::Store::LOCAL)).to eq("uploads/-/system/namespace/#{group.id}")
+    end
+
+    it 'returns remote base_dir when store param is Store::REMOTE' do
+      expect(described_class.base_dir(group, ObjectStorage::Store::REMOTE)).to eq("namespace/#{group.id}")
+    end
+  end
+
   describe "#migrate!" do
     before do
       uploader.store!(fixture_file_upload(File.join('spec/fixtures/doc_sample.txt')))
diff --git a/yarn.lock b/yarn.lock
index b020332d3695a4293983cc380a6d8f84f2f20550..0a1b964ffef05120ad202105acb726df0f51cbc7 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -78,11 +78,7 @@
     lodash "^4.2.0"
     to-fast-properties "^2.0.0"
 
-"@gitlab-org/gitlab-svgs@^1.23.0":
-  version "1.27.0"
-  resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.27.0.tgz#638e70399ebd59e503732177316bb9a18bf7a13f"
-
-"@gitlab-org/gitlab-svgs@^1.29.0":
+"@gitlab-org/gitlab-svgs@^1.23.0", "@gitlab-org/gitlab-svgs@^1.29.0":
   version "1.29.0"
   resolved "https://registry.yarnpkg.com/@gitlab-org/gitlab-svgs/-/gitlab-svgs-1.29.0.tgz#03b65b513f9099bbda6ecf94d673a2952f8c6c70"
 
@@ -307,11 +303,7 @@ acorn@^3.0.4:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
 
-acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0:
-  version "5.6.2"
-  resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.6.2.tgz#b1da1d7be2ac1b4a327fb9eab851702c5045b4e7"
-
-acorn@^5.6.2:
+acorn@^5.0.0, acorn@^5.3.0, acorn@^5.5.0, acorn@^5.6.2:
   version "5.7.1"
   resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.1.tgz#f095829297706a7c9776958c0afc8930a9b9d9d8"
 
@@ -553,13 +545,7 @@ async@1.x, async@^1.4.0, async@^1.5.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
 
-async@^2.0.0, async@^2.1.4:
-  version "2.6.0"
-  resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
-  dependencies:
-    lodash "^4.14.0"
-
-async@~2.6.0:
+async@^2.0.0, async@^2.1.4, async@~2.6.0:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
   dependencies:
@@ -1380,13 +1366,9 @@ bootstrap-vue@^2.0.0-rc.11:
     popper.js "^1.12.9"
     vue-functional-data-merge "^2.0.5"
 
-bootstrap@^4.1.1:
-  version "4.1.3"
-  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.3.tgz#0eb371af2c8448e8c210411d0cb824a6409a12be"
-
-bootstrap@~4.1.1:
-  version "4.1.1"
-  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.1.tgz#3aec85000fa619085da8d2e4983dfd67cf2114cb"
+bootstrap@^4.1.1, bootstrap@~4.1.1:
+  version "4.1.2"
+  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.2.tgz#aee2a93472e61c471fc79fb475531dcbc87de326"
 
 boxen@^1.2.1:
   version "1.3.0"
@@ -1681,25 +1663,7 @@ check-types@^7.3.0:
   version "7.3.0"
   resolved "https://registry.yarnpkg.com/check-types/-/check-types-7.3.0.tgz#468f571a4435c24248f5fd0cb0e8d87c3c341e7d"
 
-chokidar@^2.0.0, chokidar@^2.0.2:
-  version "2.0.2"
-  resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.2.tgz#4dc65139eeb2714977735b6a35d06e97b494dfd7"
-  dependencies:
-    anymatch "^2.0.0"
-    async-each "^1.0.0"
-    braces "^2.3.0"
-    glob-parent "^3.1.0"
-    inherits "^2.0.1"
-    is-binary-path "^1.0.0"
-    is-glob "^4.0.0"
-    normalize-path "^2.1.1"
-    path-is-absolute "^1.0.0"
-    readdirp "^2.0.0"
-    upath "^1.0.0"
-  optionalDependencies:
-    fsevents "^1.0.0"
-
-chokidar@^2.0.3:
+chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.3:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26"
   dependencies:
@@ -2784,15 +2748,7 @@ engine.io@~3.1.0:
   optionalDependencies:
     uws "~9.14.0"
 
-enhanced-resolve@^4.0.0:
-  version "4.0.0"
-  resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.0.0.tgz#e34a6eaa790f62fccd71d93959f56b2b432db10a"
-  dependencies:
-    graceful-fs "^4.1.2"
-    memory-fs "^0.4.0"
-    tapable "^1.0.0"
-
-enhanced-resolve@^4.1.0:
+enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0:
   version "4.1.0"
   resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz#41c7e0bfdfe74ac1ffe1e57ad6a5c6c9f3742a7f"
   dependencies:
@@ -2816,13 +2772,7 @@ entities@^1.1.1, entities@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
 
-errno@^0.1.3:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d"
-  dependencies:
-    prr "~0.0.0"
-
-errno@^0.1.4:
+errno@^0.1.3, errno@^0.1.4:
   version "0.1.7"
   resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618"
   dependencies:
@@ -3470,7 +3420,7 @@ fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
 
-fsevents@^1.0.0, fsevents@^1.2.2:
+fsevents@^1.2.2:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426"
   dependencies:
@@ -4490,11 +4440,7 @@ istanbul-api@^1.1.14:
     mkdirp "^0.5.1"
     once "^1.4.0"
 
-istanbul-lib-coverage@^1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da"
-
-istanbul-lib-coverage@^1.2.0:
+istanbul-lib-coverage@^1.1.1, istanbul-lib-coverage@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.2.0.tgz#f7d8f2e42b97e37fe796114cb0f9d68b5e3a4341"
 
@@ -4945,7 +4891,7 @@ lodash@4.17.4:
   version "4.17.4"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
 
-lodash@^4.0.0, lodash@^4.11.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
+lodash@^4.0.0, lodash@^4.11.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
   version "4.17.10"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
 
@@ -6003,15 +5949,7 @@ postcss-value-parser@^3.3.0:
   version "3.3.0"
   resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.0.tgz#87f38f9f18f774a4ab4c8a232f5c5ce8872a9d15"
 
-postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.20:
-  version "6.0.22"
-  resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3"
-  dependencies:
-    chalk "^2.4.1"
-    source-map "^0.6.1"
-    supports-color "^5.4.0"
-
-postcss@^6.0.23:
+postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.20, postcss@^6.0.23:
   version "6.0.23"
   resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.23.tgz#61c82cc328ac60e677645f979054eb98bc0e3324"
   dependencies:
@@ -6095,10 +6033,6 @@ proxy-from-env@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
 
-prr@~0.0.0:
-  version "0.0.0"
-  resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a"
-
 prr@~1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@@ -6276,16 +6210,16 @@ read-pkg@^2.0.0:
     normalize-package-data "^2.3.2"
     path-type "^2.0.0"
 
-"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3:
-  version "2.3.4"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071"
+"readable-stream@1 || 2", readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.2.9, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.6:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
   dependencies:
     core-util-is "~1.0.0"
     inherits "~2.0.3"
     isarray "~1.0.0"
     process-nextick-args "~2.0.0"
     safe-buffer "~5.1.1"
-    string_decoder "~1.0.3"
+    string_decoder "~1.1.1"
     util-deprecate "~1.0.1"
 
 readable-stream@1.1.x, "readable-stream@1.x >=1.1.9":
@@ -6297,18 +6231,6 @@ readable-stream@1.1.x, "readable-stream@1.x >=1.1.9":
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@^2.3.6:
-  version "2.3.6"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.3"
-    isarray "~1.0.0"
-    process-nextick-args "~2.0.0"
-    safe-buffer "~5.1.1"
-    string_decoder "~1.1.1"
-    util-deprecate "~1.0.1"
-
 readable-stream@~2.0.5, readable-stream@~2.0.6:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.0.6.tgz#8f90341e68a53ccc928788dacfcd11b36eb9b78e"
@@ -7021,14 +6943,10 @@ source-map@^0.4.4:
   dependencies:
     amdefine ">=0.0.4"
 
-source-map@^0.5.0, source-map@^0.5.7, source-map@~0.5.6:
+source-map@^0.5.0, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.6:
   version "0.5.7"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
 
-source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1:
-  version "0.5.6"
-  resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
-
 source-map@^0.6.1, source-map@~0.6.1:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
@@ -7130,11 +7048,7 @@ static-extend@^0.1.1:
     define-property "^0.2.5"
     object-copy "^0.1.0"
 
-"statuses@>= 1.3.1 < 2":
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087"
-
-"statuses@>= 1.4.0 < 2":
+"statuses@>= 1.3.1 < 2", "statuses@>= 1.4.0 < 2":
   version "1.5.0"
   resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
 
@@ -7218,12 +7132,6 @@ string_decoder@~0.10.x:
   version "0.10.31"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
 
-string_decoder@~1.0.3:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab"
-  dependencies:
-    safe-buffer "~5.1.0"
-
 stringstream@~0.0.4, stringstream@~0.0.5:
   version "0.0.5"
   resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
@@ -7621,10 +7529,6 @@ unzip-response@^2.0.1:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97"
 
-upath@^1.0.0:
-  version "1.0.5"
-  resolved "https://registry.yarnpkg.com/upath/-/upath-1.0.5.tgz#02cab9ecebe95bbec6d5fc2566325725ab6d1a73"
-
 upath@^1.0.5:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd"
@@ -7744,11 +7648,7 @@ validate-npm-package-license@^3.0.1:
     spdx-correct "~1.0.0"
     spdx-expression-parse "~1.0.0"
 
-vary@~1.1.1:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37"
-
-vary@~1.1.2:
+vary@~1.1.1, vary@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"