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 18934af004a23eb9d6ba0d6a60ce69cc267f7d3f..560cdd941cd89f8d4333a71b9de4fb339a03a0a5 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -1,38 +1,36 @@
 <script>
-  import { mapActions } from 'vuex';
-  import icon from '~/vue_shared/components/icon.vue';
-  import router from '../../ide_router';
+import { mapActions } from 'vuex';
+import Icon from '~/vue_shared/components/icon.vue';
 
-  export default {
-    components: {
-      icon,
+export default {
+  components: {
+    Icon,
+  },
+  props: {
+    file: {
+      type: Object,
+      required: true,
     },
-    props: {
-      file: {
-        type: Object,
-        required: true,
-      },
+  },
+  computed: {
+    iconName() {
+      return this.file.tempFile ? 'file-addition' : 'file-modified';
     },
-    computed: {
-      iconName() {
-        return this.file.tempFile ? 'file-addition' : 'file-modified';
-      },
-      iconClass() {
-        return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
-      },
+    iconClass() {
+      return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
     },
-    methods: {
-      ...mapActions([
-        'discardFileChanges',
-        'updateViewer',
-      ]),
-      openFileInEditor(file) {
-        this.updateViewer('diff');
-
-        router.push(`/project${file.url}`);
-      },
+  },
+  methods: {
+    ...mapActions(['discardFileChanges', 'updateViewer', 'openPendingTab']),
+    openFileInEditor(file) {
+      return this.openPendingTab(file).then(changeViewer => {
+        if (changeViewer) {
+          this.updateViewer('diff');
+        }
+      });
     },
-  };
+  },
+};
 </script>
 
 <template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 5e44af01241a636c4c9e262121fa40fe33359da6..d22869466c9ce4fb7c1cba93b48bb6060473b372 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -60,6 +60,7 @@ export default {
         v-if="activeFile"
       >
         <repo-tabs
+          :active-file="activeFile"
           :files="openFiles"
           :viewer="viewer"
           :has-changes="hasChanges"
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index b6f8f8a1c991a4b5b0e25c703f048081bf2a2a9f..b1a16350c19ffb1201b805db90be66924d205a97 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -21,7 +21,8 @@ export default {
   },
   watch: {
     file(oldVal, newVal) {
-      if (newVal.path !== this.file.path) {
+      // Compare key to allow for files opened in review mode to be cached differently
+      if (newVal.key !== this.file.key) {
         this.initMonaco();
       }
     },
@@ -70,7 +71,7 @@ export default {
       })
         .then(() => {
           const viewerPromise = this.delayViewerUpdated
-            ? this.updateViewer('editor')
+            ? this.updateViewer(this.file.pending ? 'diff' : 'editor')
             : Promise.resolve();
 
           return viewerPromise;
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 1935ee1a4bbc8dd662310a7f6b550ff0e87dadb7..3b5068d4910c99ca93db81120c21372a79907578 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -62,11 +62,7 @@ export default {
         this.toggleTreeOpen(this.file.path);
       }
 
-      const delayPromise = this.file.changed
-        ? Promise.resolve()
-        : this.updateDelayViewerUpdated(true);
-
-      return delayPromise.then(() => {
+      return this.updateDelayViewerUpdated(true).then(() => {
         router.push(`/project${this.file.url}`);
       });
     },
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index c337bc813e6e08db453d6b0f94b74c63c51f23f3..304a73ed1ad91b8e6a03bd7985a73ef28eaf3674 100644
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -1,60 +1,64 @@
 <script>
-  import { mapActions } from 'vuex';
+import { mapActions } from 'vuex';
 
-  import fileIcon from '~/vue_shared/components/file_icon.vue';
-  import icon from '~/vue_shared/components/icon.vue';
-  import fileStatusIcon from './repo_file_status_icon.vue';
-  import changedFileIcon from './changed_file_icon.vue';
+import FileIcon from '~/vue_shared/components/file_icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import FileStatusIcon from './repo_file_status_icon.vue';
+import ChangedFileIcon from './changed_file_icon.vue';
 
-  export default {
-    components: {
-      fileStatusIcon,
-      fileIcon,
-      icon,
-      changedFileIcon,
+export default {
+  components: {
+    FileStatusIcon,
+    FileIcon,
+    Icon,
+    ChangedFileIcon,
+  },
+  props: {
+    tab: {
+      type: Object,
+      required: true,
     },
-    props: {
-      tab: {
-        type: Object,
-        required: true,
-      },
+  },
+  data() {
+    return {
+      tabMouseOver: false,
+    };
+  },
+  computed: {
+    closeLabel() {
+      if (this.tab.changed || this.tab.tempFile) {
+        return `${this.tab.name} changed`;
+      }
+      return `Close ${this.tab.name}`;
     },
-    data() {
-      return {
-        tabMouseOver: false,
-      };
-    },
-    computed: {
-      closeLabel() {
-        if (this.tab.changed || this.tab.tempFile) {
-          return `${this.tab.name} changed`;
-        }
-        return `Close ${this.tab.name}`;
-      },
-      showChangedIcon() {
-        return this.tab.changed ? !this.tabMouseOver : false;
-      },
+    showChangedIcon() {
+      return this.tab.changed ? !this.tabMouseOver : false;
     },
+  },
+
+  methods: {
+    ...mapActions(['closeFile', 'updateDelayViewerUpdated', 'openPendingTab']),
+    clickFile(tab) {
+      this.updateDelayViewerUpdated(true);
 
-    methods: {
-      ...mapActions([
-        'closeFile',
-      ]),
-      clickFile(tab) {
+      if (tab.pending) {
+        this.openPendingTab(tab);
+      } else {
         this.$router.push(`/project${tab.url}`);
-      },
-      mouseOverTab() {
-        if (this.tab.changed) {
-          this.tabMouseOver = true;
-        }
-      },
-      mouseOutTab() {
-        if (this.tab.changed) {
-          this.tabMouseOver = false;
-        }
-      },
+      }
+    },
+    mouseOverTab() {
+      if (this.tab.changed) {
+        this.tabMouseOver = true;
+      }
+    },
+    mouseOutTab() {
+      if (this.tab.changed) {
+        this.tabMouseOver = false;
+      }
     },
-  };
+  },
+};
 </script>
 
 <template>
@@ -66,7 +70,7 @@
     <button
       type="button"
       class="multi-file-tab-close"
-      @click.stop.prevent="closeFile(tab.path)"
+      @click.stop.prevent="closeFile(tab)"
       :aria-label="closeLabel"
     >
       <icon
@@ -82,7 +86,9 @@
 
     <div
       class="multi-file-tab"
-      :class="{active : tab.active }"
+      :class="{
+        active: tab.active
+      }"
       :title="tab.url"
     >
       <file-icon
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index a44e418b2eb88797d8d5773431eddf45ea1aa5a3..7bd646ba9b0597ba6a4c9eacfd7279058118ab9e 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -2,6 +2,7 @@
 import { mapActions } from 'vuex';
 import RepoTab from './repo_tab.vue';
 import EditorMode from './editor_mode_dropdown.vue';
+import router from '../ide_router';
 
 export default {
   components: {
@@ -9,6 +10,10 @@ export default {
     EditorMode,
   },
   props: {
+    activeFile: {
+      type: Object,
+      required: true,
+    },
     files: {
       type: Array,
       required: true,
@@ -38,7 +43,18 @@ export default {
     this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
   },
   methods: {
-    ...mapActions(['updateViewer']),
+    ...mapActions(['updateViewer', 'removePendingTab']),
+    openFileViewer(viewer) {
+      this.updateViewer(viewer);
+
+      if (this.activeFile.pending) {
+        return this.removePendingTab(this.activeFile).then(() => {
+          router.push(`/project${this.activeFile.url}`);
+        });
+      }
+
+      return null;
+    },
   },
 };
 </script>
@@ -60,7 +76,7 @@ export default {
       :show-shadow="showShadow"
       :has-changes="hasChanges"
       :merge-request-id="mergeRequestId"
-      @click="updateViewer"
+      @click="openFileViewer"
     />
   </div>
 </template>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index be2c12c0487aa0206622068754467bc2d9eba891..20983666b4a4fedbf8c55fcb50a553311c09cbfc 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -77,7 +77,11 @@ router.beforeEach((to, from, next) => {
               if (to.params[0]) {
                 const path =
                   to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
-                const treeEntry = store.state.entries[path];
+                const treeEntryKey = Object.keys(store.state.entries).find(
+                  key => key === path && !store.state.entries[key].pending,
+                );
+                const treeEntry = store.state.entries[treeEntryKey];
+
                 if (treeEntry) {
                   store.dispatch('handleTreeEntryAction', treeEntry);
                 }
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index d372c2aaad859cef335d28373a51b237c0469a9f..e47adae99ed1cabd418be099798a23ea8ca4a0be 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -13,12 +13,12 @@ export default class Model {
       (this.originalModel = this.monaco.editor.createModel(
         this.file.raw,
         undefined,
-        new this.monaco.Uri(null, null, `original/${this.file.path}`),
+        new this.monaco.Uri(null, null, `original/${this.file.key}`),
       )),
       (this.model = this.monaco.editor.createModel(
         this.content,
         undefined,
-        new this.monaco.Uri(null, null, this.file.path),
+        new this.monaco.Uri(null, null, this.file.key),
       )),
     );
     if (this.file.mrChange) {
@@ -36,7 +36,7 @@ export default class Model {
     this.updateContent = this.updateContent.bind(this);
     this.dispose = this.dispose.bind(this);
 
-    eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
+    eventHub.$on(`editor.update.model.dispose.${this.file.key}`, this.dispose);
     eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
   }
 
@@ -53,7 +53,7 @@ export default class Model {
   }
 
   get path() {
-    return this.file.path;
+    return this.file.key;
   }
 
   getModel() {
@@ -88,7 +88,7 @@ export default class Model {
     this.disposable.dispose();
     this.events.clear();
 
-    eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
+    eventHub.$off(`editor.update.model.dispose.${this.file.key}`, this.dispose);
     eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
   }
 }
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index 57d5e59a88b63d415c89de707a8702826a1e1f96..0e7b563b5d6b9e214f1e28fffb8a45ac8eb20ecb 100644
--- a/app/assets/javascripts/ide/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
@@ -9,17 +9,17 @@ export default class ModelManager {
     this.models = new Map();
   }
 
-  hasCachedModel(path) {
-    return this.models.has(path);
+  hasCachedModel(key) {
+    return this.models.has(key);
   }
 
-  getModel(path) {
-    return this.models.get(path);
+  getModel(key) {
+    return this.models.get(key);
   }
 
   addModel(file) {
-    if (this.hasCachedModel(file.path)) {
-      return this.getModel(file.path);
+    if (this.hasCachedModel(file.key)) {
+      return this.getModel(file.key);
     }
 
     const model = new Model(this.monaco, file);
@@ -27,7 +27,7 @@ export default class ModelManager {
     this.disposable.add(model);
 
     eventHub.$on(
-      `editor.update.model.dispose.${file.path}`,
+      `editor.update.model.dispose.${file.key}`,
       this.removeCachedModel.bind(this, file),
     );
 
@@ -35,12 +35,9 @@ export default class ModelManager {
   }
 
   removeCachedModel(file) {
-    this.models.delete(file.path);
+    this.models.delete(file.key);
 
-    eventHub.$off(
-      `editor.update.model.dispose.${file.path}`,
-      this.removeCachedModel,
-    );
+    eventHub.$off(`editor.update.model.dispose.${file.key}`, this.removeCachedModel);
   }
 
   dispose() {
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 0a74f4f8925503eaa011565a8fb3c44f0a672e4a..c6ba679d99c44d97e27d7c59c2cd5e744e30d930 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -21,7 +21,7 @@ export const discardAllChanges = ({ state, commit, dispatch }) => {
 };
 
 export const closeAllFiles = ({ state, dispatch }) => {
-  state.openFiles.forEach(file => dispatch('closeFile', file.path));
+  state.openFiles.forEach(file => dispatch('closeFile', file));
 };
 
 export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index c21c1a3f5d4eaa0edfdbb25ef4d14d8d00f8de98..6b034ea1e8227643757b79ac9d9eda05e03e5244 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -6,24 +6,34 @@ import * as types from '../mutation_types';
 import router from '../../ide_router';
 import { setPageTitle } from '../utils';
 
-export const closeFile = ({ commit, state, getters, dispatch }, path) => {
-  const indexOfClosedFile = state.openFiles.findIndex(f => f.path === path);
-  const file = state.entries[path];
+export const closeFile = ({ commit, state, dispatch }, file) => {
+  const path = file.path;
+  const indexOfClosedFile = state.openFiles.findIndex(f => f.key === file.key);
   const fileWasActive = file.active;
 
-  commit(types.TOGGLE_FILE_OPEN, path);
-  commit(types.SET_FILE_ACTIVE, { path, active: false });
+  if (file.pending) {
+    commit(types.REMOVE_PENDING_TAB, file);
+  } else {
+    commit(types.TOGGLE_FILE_OPEN, path);
+    commit(types.SET_FILE_ACTIVE, { path, active: false });
+  }
 
   if (state.openFiles.length > 0 && fileWasActive) {
     const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
-    const nextFileToOpen = state.entries[state.openFiles[nextIndexToOpen].path];
-
-    router.push(`/project${nextFileToOpen.url}`);
+    const nextFileToOpen = state.openFiles[nextIndexToOpen];
+
+    if (nextFileToOpen.pending) {
+      dispatch('updateViewer', 'diff');
+      dispatch('openPendingTab', nextFileToOpen);
+    } else {
+      dispatch('updateDelayViewerUpdated', true);
+      router.push(`/project${nextFileToOpen.url}`);
+    }
   } else if (!state.openFiles.length) {
     router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
   }
 
-  eventHub.$emit(`editor.update.model.dispose.${file.path}`);
+  eventHub.$emit(`editor.update.model.dispose.${file.key}`);
 };
 
 export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
@@ -151,3 +161,23 @@ export const discardFileChanges = ({ state, commit }, path) => {
 
   eventHub.$emit(`editor.update.model.content.${file.path}`, file.raw);
 };
+
+export const openPendingTab = ({ commit, getters, dispatch, state }, file) => {
+  if (getters.activeFile && getters.activeFile.path === file.path && state.viewer === 'diff') {
+    return false;
+  }
+
+  commit(types.ADD_PENDING_TAB, { file });
+
+  dispatch('scrollToTab');
+
+  router.push(`/project/${file.projectId}/tree/${state.currentBranchId}/`);
+
+  return true;
+};
+
+export const removePendingTab = ({ commit }, file) => {
+  commit(types.REMOVE_PENDING_TAB, file);
+
+  eventHub.$emit(`editor.update.model.dispose.${file.key}`);
+};
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index e06be0a3fe9c4620df484c2275cf13aaf0fc0346..ee759bff51650decd285a0e64983c87bdaedcd1b 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -49,3 +49,6 @@ export const CREATE_TMP_ENTRY = 'CREATE_TMP_ENTRY';
 export const SET_FILE_MERGE_REQUEST_CHANGE = 'SET_FILE_MERGE_REQUEST_CHANGE';
 export const UPDATE_VIEWER = 'UPDATE_VIEWER';
 export const UPDATE_DELAY_VIEWER_CHANGE = 'UPDATE_DELAY_VIEWER_CHANGE';
+
+export const ADD_PENDING_TAB = 'ADD_PENDING_TAB';
+export const REMOVE_PENDING_TAB = 'REMOVE_PENDING_TAB';
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 692fe39b38e63efa4575aea06aa71a7046e0f5bf..926b6f66d788a0a9559af8306011bf7274a9c178 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -5,6 +5,14 @@ export default {
     Object.assign(state.entries[path], {
       active,
     });
+
+    if (active && !state.entries[path].pending) {
+      Object.assign(state, {
+        openFiles: state.openFiles.map(f =>
+          Object.assign(f, { active: f.pending ? false : f.active }),
+        ),
+      });
+    }
   },
   [types.TOGGLE_FILE_OPEN](state, path) {
     Object.assign(state.entries[path], {
@@ -12,10 +20,14 @@ export default {
     });
 
     if (state.entries[path].opened) {
-      state.openFiles.push(state.entries[path]);
+      Object.assign(state, {
+        openFiles: state.openFiles.filter(f => f.path !== path).concat(state.entries[path]),
+      });
     } else {
+      const file = state.entries[path];
+
       Object.assign(state, {
-        openFiles: state.openFiles.filter(f => f.path !== path),
+        openFiles: state.openFiles.filter(f => f.key !== file.key),
       });
     }
   },
@@ -92,4 +104,37 @@ export default {
       changed,
     });
   },
+  [types.ADD_PENDING_TAB](state, { file, keyPrefix = 'pending' }) {
+    const pendingTab = state.openFiles.find(f => f.path === file.path && f.pending);
+    let openFiles = state.openFiles.map(f =>
+      Object.assign(f, { active: f.path === file.path, opened: false }),
+    );
+
+    if (!pendingTab) {
+      const openFile = openFiles.find(f => f.path === file.path);
+
+      openFiles = openFiles.concat(openFile ? null : file).reduce((acc, f) => {
+        if (!f) return acc;
+
+        if (f.path === file.path) {
+          return acc.concat({
+            ...f,
+            active: true,
+            pending: true,
+            opened: true,
+            key: `${keyPrefix}-${f.key}`,
+          });
+        }
+
+        return acc.concat(f);
+      }, []);
+    }
+
+    Object.assign(state, { openFiles });
+  },
+  [types.REMOVE_PENDING_TAB](state, file) {
+    Object.assign(state, {
+      openFiles: state.openFiles.filter(f => f.key !== file.key),
+    });
+  },
 };
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 3389eeeaa2ea453258cc0bc912c98a4b7ecca58f..63e4de3b17dbe9e790904ce70a883782ec82ca7a 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -1,5 +1,7 @@
 export const dataStructure = () => ({
   id: '',
+  // Key will contain a mixture of ID and path
+  // it can also contain a prefix `pending-` for files opened in review mode
   key: '',
   type: '',
   projectId: '',
diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
index 15b66952d996e8f069d108786645222b565014e6..509434e4300f5261687be9617ee4a65da60bf140 100644
--- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
+++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js
@@ -1,8 +1,9 @@
 import Vue from 'vue';
 import listItem from '~/ide/components/commit_sidebar/list_item.vue';
 import router from '~/ide/ide_router';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import { file } from '../../helpers';
+import store from '~/ide/stores';
+import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { file, resetStore } from '../../helpers';
 
 describe('Multi-file editor commit sidebar list item', () => {
   let vm;
@@ -13,19 +14,21 @@ describe('Multi-file editor commit sidebar list item', () => {
 
     f = file('test-file');
 
-    vm = mountComponent(Component, {
+    store.state.entries[f.path] = f;
+
+    vm = createComponentWithStore(Component, store, {
       file: f,
-    });
+    }).$mount();
   });
 
   afterEach(() => {
     vm.$destroy();
+
+    resetStore(store);
   });
 
   it('renders file path', () => {
-    expect(
-      vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim(),
-    ).toBe(f.path);
+    expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path);
   });
 
   it('calls discardFileChanges when clicking discard button', () => {
@@ -36,25 +39,32 @@ describe('Multi-file editor commit sidebar list item', () => {
     expect(vm.discardFileChanges).toHaveBeenCalled();
   });
 
-  it('opens a closed file in the editor when clicking the file path', () => {
+  it('opens a closed file in the editor when clicking the file path', done => {
     spyOn(vm, 'openFileInEditor').and.callThrough();
-    spyOn(vm, 'updateViewer');
     spyOn(router, 'push');
 
     vm.$el.querySelector('.multi-file-commit-list-path').click();
 
-    expect(vm.openFileInEditor).toHaveBeenCalled();
-    expect(router.push).toHaveBeenCalled();
+    setTimeout(() => {
+      expect(vm.openFileInEditor).toHaveBeenCalled();
+      expect(router.push).toHaveBeenCalled();
+
+      done();
+    });
   });
 
-  it('calls updateViewer with diff when clicking file', () => {
+  it('calls updateViewer with diff when clicking file', done => {
     spyOn(vm, 'openFileInEditor').and.callThrough();
-    spyOn(vm, 'updateViewer');
+    spyOn(vm, 'updateViewer').and.callThrough();
     spyOn(router, 'push');
 
     vm.$el.querySelector('.multi-file-commit-list-path').click();
 
-    expect(vm.updateViewer).toHaveBeenCalledWith('diff');
+    setTimeout(() => {
+      expect(vm.updateViewer).toHaveBeenCalledWith('diff');
+
+      done();
+    });
   });
 
   describe('computed', () => {
diff --git a/spec/javascripts/ide/components/repo_tab_spec.js b/spec/javascripts/ide/components/repo_tab_spec.js
index ddb5204e3a702e9735ccd42417fa5e8b4f986f25..8cabc6e89353fe21052ee30c9170105201090e7c 100644
--- a/spec/javascripts/ide/components/repo_tab_spec.js
+++ b/spec/javascripts/ide/components/repo_tab_spec.js
@@ -59,7 +59,7 @@ describe('RepoTab', () => {
 
     vm.$el.querySelector('.multi-file-tab-close').click();
 
-    expect(vm.closeFile).toHaveBeenCalledWith(vm.tab.path);
+    expect(vm.closeFile).toHaveBeenCalledWith(vm.tab);
   });
 
   it('changes icon on hover', done => {
diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js
index 73ea796048544ee1246643dea43f38d7fb71a0ef..cb785ba2cd3c4eeded0965435da48538cc102835 100644
--- a/spec/javascripts/ide/components/repo_tabs_spec.js
+++ b/spec/javascripts/ide/components/repo_tabs_spec.js
@@ -17,6 +17,7 @@ describe('RepoTabs', () => {
       files: openedFiles,
       viewer: 'editor',
       hasChanges: false,
+      activeFile: file('activeFile'),
       hasMergeRequest: false,
     });
     openedFiles[0].active = true;
@@ -57,6 +58,7 @@ describe('RepoTabs', () => {
           files: [],
           viewer: 'editor',
           hasChanges: false,
+          activeFile: file('activeFile'),
           hasMergeRequest: false,
         },
         '#test-app',
diff --git a/spec/javascripts/ide/lib/common/model_manager_spec.js b/spec/javascripts/ide/lib/common/model_manager_spec.js
index 4381f6fcfd0ac0af5d27dcb7b542f260c51bbf29..c00d590c580fd5610a23bb987f4eba8c2fd14383 100644
--- a/spec/javascripts/ide/lib/common/model_manager_spec.js
+++ b/spec/javascripts/ide/lib/common/model_manager_spec.js
@@ -27,9 +27,10 @@ describe('Multi-file editor library model manager', () => {
     });
 
     it('caches model by file path', () => {
-      instance.addModel(file('path-name'));
+      const f = file('path-name');
+      instance.addModel(f);
 
-      expect(instance.models.keys().next().value).toBe('path-name');
+      expect(instance.models.keys().next().value).toBe(f.key);
     });
 
     it('adds model into disposable', () => {
@@ -56,7 +57,7 @@ describe('Multi-file editor library model manager', () => {
       instance.addModel(f);
 
       expect(eventHub.$on).toHaveBeenCalledWith(
-        `editor.update.model.dispose.${f.path}`,
+        `editor.update.model.dispose.${f.key}`,
         jasmine.anything(),
       );
     });
@@ -68,9 +69,11 @@ describe('Multi-file editor library model manager', () => {
     });
 
     it('returns true when model exists', () => {
-      instance.addModel(file('path-name'));
+      const f = file('path-name');
+
+      instance.addModel(f);
 
-      expect(instance.hasCachedModel('path-name')).toBeTruthy();
+      expect(instance.hasCachedModel(f.key)).toBeTruthy();
     });
   });
 
@@ -103,7 +106,7 @@ describe('Multi-file editor library model manager', () => {
       instance.removeCachedModel(f);
 
       expect(eventHub.$off).toHaveBeenCalledWith(
-        `editor.update.model.dispose.${f.path}`,
+        `editor.update.model.dispose.${f.key}`,
         jasmine.anything(),
       );
     });
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
index 7cd990adb53308526ec0b453f5b312b92447baf6..8fc2fccb64c8067a24dad9f02ef6f70f2bd9f75e 100644
--- a/spec/javascripts/ide/lib/common/model_spec.js
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -32,14 +32,14 @@ describe('Multi-file editor library model', () => {
 
   it('adds eventHub listener', () => {
     expect(eventHub.$on).toHaveBeenCalledWith(
-      `editor.update.model.dispose.${model.file.path}`,
+      `editor.update.model.dispose.${model.file.key}`,
       jasmine.anything(),
     );
   });
 
   describe('path', () => {
     it('returns file path', () => {
-      expect(model.path).toBe('path');
+      expect(model.path).toBe(model.file.key);
     });
   });
 
@@ -74,7 +74,7 @@ describe('Multi-file editor library model', () => {
       model.onChange(() => {});
 
       expect(model.events.size).toBe(1);
-      expect(model.events.keys().next().value).toBe('path');
+      expect(model.events.keys().next().value).toBe(model.file.key);
     });
 
     it('calls callback on change', done => {
@@ -115,7 +115,7 @@ describe('Multi-file editor library model', () => {
       model.dispose();
 
       expect(eventHub.$off).toHaveBeenCalledWith(
-        `editor.update.model.dispose.${model.file.path}`,
+        `editor.update.model.dispose.${model.file.key}`,
         jasmine.anything(),
       );
     });
diff --git a/spec/javascripts/ide/lib/decorations/controller_spec.js b/spec/javascripts/ide/lib/decorations/controller_spec.js
index 092170d086a15ea2fb989490cdcc308de5c9cc36..aec325e26a9366e492b63695fe91ae6acdf8eecb 100644
--- a/spec/javascripts/ide/lib/decorations/controller_spec.js
+++ b/spec/javascripts/ide/lib/decorations/controller_spec.js
@@ -36,9 +36,7 @@ describe('Multi-file editor library decorations controller', () => {
     });
 
     it('returns decorations by model URL', () => {
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
 
       const decorations = controller.getAllDecorationsForModel(model);
 
@@ -48,39 +46,29 @@ describe('Multi-file editor library decorations controller', () => {
 
   describe('addDecorations', () => {
     it('caches decorations in a new map', () => {
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
 
       expect(controller.decorations.size).toBe(1);
     });
 
     it('does not create new cache model', () => {
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue2' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]);
 
       expect(controller.decorations.size).toBe(1);
     });
 
     it('caches decorations by model URL', () => {
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
 
       expect(controller.decorations.size).toBe(1);
-      expect(controller.decorations.keys().next().value).toBe('path');
+      expect(controller.decorations.keys().next().value).toBe('path--path');
     });
 
     it('calls decorate method', () => {
       spyOn(controller, 'decorate');
 
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
 
       expect(controller.decorate).toHaveBeenCalled();
     });
@@ -92,10 +80,7 @@ describe('Multi-file editor library decorations controller', () => {
 
       controller.decorate(model);
 
-      expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith(
-        [],
-        [],
-      );
+      expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []);
     });
 
     it('caches decorations', () => {
@@ -111,15 +96,13 @@ describe('Multi-file editor library decorations controller', () => {
 
       controller.decorate(model);
 
-      expect(controller.editorDecorations.keys().next().value).toBe('path');
+      expect(controller.editorDecorations.keys().next().value).toBe('path--path');
     });
   });
 
   describe('dispose', () => {
     it('clears cached decorations', () => {
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
 
       controller.dispose();
 
@@ -127,9 +110,7 @@ describe('Multi-file editor library decorations controller', () => {
     });
 
     it('clears cached editorDecorations', () => {
-      controller.addDecorations(model, 'key', [
-        { decoration: 'decorationValue' },
-      ]);
+      controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
 
       controller.dispose();
 
diff --git a/spec/javascripts/ide/lib/diff/controller_spec.js b/spec/javascripts/ide/lib/diff/controller_spec.js
index c8f3e9f4830c0cec6e04b9c41f7c37a8220f44e7..ff73240734ec5a1de516faafccc328d93c4f7200 100644
--- a/spec/javascripts/ide/lib/diff/controller_spec.js
+++ b/spec/javascripts/ide/lib/diff/controller_spec.js
@@ -131,7 +131,7 @@ describe('Multi-file editor library dirty diff controller', () => {
     it('adds decorations into decorations controller', () => {
       spyOn(controller.decorationsController, 'addDecorations');
 
-      controller.decorate({ data: { changes: [], path: 'path' } });
+      controller.decorate({ data: { changes: [], path: model.path } });
 
       expect(
         controller.decorationsController.addDecorations,
@@ -145,7 +145,7 @@ describe('Multi-file editor library dirty diff controller', () => {
       );
 
       controller.decorate({
-        data: { changes: computeDiff('123', '1234'), path: 'path' },
+        data: { changes: computeDiff('123', '1234'), path: model.path },
       });
 
       expect(spy).toHaveBeenCalledWith(
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 2f4516377cf487ff94a97539b8abb5a3a50de547..479ed7ce49ee6e170b80daf185b7204554b30e27 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -29,7 +29,7 @@ describe('IDE store file actions', () => {
 
     it('closes open files', done => {
       store
-        .dispatch('closeFile', localFile.path)
+        .dispatch('closeFile', localFile)
         .then(() => {
           expect(localFile.opened).toBeFalsy();
           expect(localFile.active).toBeFalsy();
@@ -44,7 +44,7 @@ describe('IDE store file actions', () => {
       store.state.changedFiles.push(localFile);
 
       store
-        .dispatch('closeFile', localFile.path)
+        .dispatch('closeFile', localFile)
         .then(Vue.nextTick)
         .then(() => {
           expect(store.state.openFiles.length).toBe(0);
@@ -65,7 +65,7 @@ describe('IDE store file actions', () => {
       store.state.entries[f.path] = f;
 
       store
-        .dispatch('closeFile', localFile.path)
+        .dispatch('closeFile', localFile)
         .then(Vue.nextTick)
         .then(() => {
           expect(router.push).toHaveBeenCalledWith(`/project${f.url}`);
@@ -74,6 +74,22 @@ describe('IDE store file actions', () => {
         })
         .catch(done.fail);
     });
+
+    it('removes file if it pending', done => {
+      store.state.openFiles.push({
+        ...localFile,
+        pending: true,
+      });
+
+      store
+        .dispatch('closeFile', localFile)
+        .then(() => {
+          expect(store.state.openFiles.length).toBe(0);
+
+          done();
+        })
+        .catch(done.fail);
+    });
   });
 
   describe('setFileActive', () => {
@@ -445,4 +461,113 @@ describe('IDE store file actions', () => {
         .catch(done.fail);
     });
   });
+
+  describe('openPendingTab', () => {
+    let f;
+
+    beforeEach(() => {
+      f = {
+        ...file(),
+        projectId: '123',
+      };
+
+      store.state.entries[f.path] = f;
+    });
+
+    it('makes file pending in openFiles', done => {
+      store
+        .dispatch('openPendingTab', f)
+        .then(() => {
+          expect(store.state.openFiles[0].pending).toBe(true);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('returns true when opened', done => {
+      store
+        .dispatch('openPendingTab', f)
+        .then(added => {
+          expect(added).toBe(true);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('pushes router URL when added', done => {
+      store.state.currentBranchId = 'master';
+
+      store
+        .dispatch('openPendingTab', f)
+        .then(() => {
+          expect(router.push).toHaveBeenCalledWith('/project/123/tree/master/');
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('calls scrollToTab', done => {
+      const scrollToTabSpy = jasmine.createSpy('scrollToTab');
+      const oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
+      store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
+
+      store
+        .dispatch('openPendingTab', f)
+        .then(() => {
+          expect(scrollToTabSpy).toHaveBeenCalled();
+          store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('returns false when passed in file is active & viewer is diff', done => {
+      f.active = true;
+      store.state.openFiles.push(f);
+      store.state.viewer = 'diff';
+
+      store
+        .dispatch('openPendingTab', f)
+        .then(added => {
+          expect(added).toBe(false);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+  });
+
+  describe('removePendingTab', () => {
+    let f;
+
+    beforeEach(() => {
+      spyOn(eventHub, '$emit');
+
+      f = {
+        ...file('pendingFile'),
+        pending: true,
+      };
+    });
+
+    it('removes pending file from open files', done => {
+      store.state.openFiles.push(f);
+
+      store
+        .dispatch('removePendingTab', f)
+        .then(() => {
+          expect(store.state.openFiles.length).toBe(0);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+
+    it('emits event to dispose model', done => {
+      store
+        .dispatch('removePendingTab', f)
+        .then(() => {
+          expect(eventHub.$emit).toHaveBeenCalledWith(`editor.update.model.dispose.${f.key}`);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+  });
 });
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index 8fec94e882a9e6f864687b948d18530af4df8180..88285ee409fb16a907fc8e0ad934d5b774b0ee43 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -22,6 +22,21 @@ describe('IDE store file mutations', () => {
 
       expect(localFile.active).toBeTruthy();
     });
+
+    it('sets pending tab as not active', () => {
+      localState.openFiles.push({
+        ...localFile,
+        pending: true,
+        active: true,
+      });
+
+      mutations.SET_FILE_ACTIVE(localState, {
+        path: localFile.path,
+        active: true,
+      });
+
+      expect(localState.openFiles[0].active).toBe(false);
+    });
   });
 
   describe('TOGGLE_FILE_OPEN', () => {
@@ -178,4 +193,69 @@ describe('IDE store file mutations', () => {
       expect(localFile.changed).toBeTruthy();
     });
   });
+
+  describe('ADD_PENDING_TAB', () => {
+    beforeEach(() => {
+      const f = {
+        ...file('openFile'),
+        path: 'openFile',
+        active: true,
+        opened: true,
+      };
+
+      localState.entries[f.path] = f;
+      localState.openFiles.push(f);
+    });
+
+    it('adds file into openFiles as pending', () => {
+      mutations.ADD_PENDING_TAB(localState, { file: localFile });
+
+      expect(localState.openFiles.length).toBe(2);
+      expect(localState.openFiles[1].pending).toBe(true);
+      expect(localState.openFiles[1].key).toBe(`pending-${localFile.key}`);
+    });
+
+    it('updates open file to pending', () => {
+      mutations.ADD_PENDING_TAB(localState, { file: localState.openFiles[0] });
+
+      expect(localState.openFiles.length).toBe(1);
+    });
+
+    it('updates pending open file to active', () => {
+      localState.openFiles.push({
+        ...localFile,
+        pending: true,
+      });
+
+      mutations.ADD_PENDING_TAB(localState, { file: localFile });
+
+      expect(localState.openFiles[1].pending).toBe(true);
+      expect(localState.openFiles[1].active).toBe(true);
+    });
+
+    it('sets all openFiles to not active', () => {
+      mutations.ADD_PENDING_TAB(localState, { file: localFile });
+
+      expect(localState.openFiles.length).toBe(2);
+
+      localState.openFiles.forEach(f => {
+        if (f.pending) {
+          expect(f.active).toBe(true);
+        } else {
+          expect(f.active).toBe(false);
+        }
+      });
+    });
+  });
+
+  describe('REMOVE_PENDING_TAB', () => {
+    it('removes pending tab from openFiles', () => {
+      localFile.key = 'testing';
+      localState.openFiles.push(localFile);
+
+      mutations.REMOVE_PENDING_TAB(localState, localFile);
+
+      expect(localState.openFiles.length).toBe(0);
+    });
+  });
 });