diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index cbcefb2c18f49e27bf86fb3c5bb529df4fb615b3..8ad3d18b30223b5cfbf01df6391e9bec54173fbe 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -10,6 +10,9 @@ const Api = {
   projectsPath: '/api/:version/projects.json',
   projectPath: '/api/:version/projects/:id',
   projectLabelsPath: '/:namespace_path/:project_path/labels',
+  mergeRequestPath: '/api/:version/projects/:id/merge_requests/:mrid',
+  mergeRequestChangesPath: '/api/:version/projects/:id/merge_requests/:mrid/changes',
+  mergeRequestVersionsPath: '/api/:version/projects/:id/merge_requests/:mrid/versions',
   groupLabelsPath: '/groups/:namespace_path/-/labels',
   licensePath: '/api/:version/templates/licenses/:key',
   gitignorePath: '/api/:version/templates/gitignores/:key',
@@ -22,25 +25,27 @@ const Api = {
   createBranchPath: '/api/:version/projects/:id/repository/branches',
 
   group(groupId, callback) {
-    const url = Api.buildUrl(Api.groupPath)
-      .replace(':id', groupId);
-    return axios.get(url)
-      .then(({ data }) => {
-        callback(data);
+    const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
+    return axios.get(url).then(({ data }) => {
+      callback(data);
 
-        return data;
-      });
+      return data;
+    });
   },
 
   // Return groups list. Filtered by query
   groups(query, options, callback = $.noop) {
     const url = Api.buildUrl(Api.groupsPath);
-    return axios.get(url, {
-      params: Object.assign({
-        search: query,
-        per_page: 20,
-      }, options),
-    })
+    return axios
+      .get(url, {
+        params: Object.assign(
+          {
+            search: query,
+            per_page: 20,
+          },
+          options,
+        ),
+      })
       .then(({ data }) => {
         callback(data);
 
@@ -51,12 +56,13 @@ const Api = {
   // Return namespaces list. Filtered by query
   namespaces(query, callback) {
     const url = Api.buildUrl(Api.namespacesPath);
-    return axios.get(url, {
-      params: {
-        search: query,
-        per_page: 20,
-      },
-    })
+    return axios
+      .get(url, {
+        params: {
+          search: query,
+          per_page: 20,
+        },
+      })
       .then(({ data }) => callback(data));
   },
 
@@ -73,9 +79,10 @@ const Api = {
       defaults.membership = true;
     }
 
-    return axios.get(url, {
-      params: Object.assign(defaults, options),
-    })
+    return axios
+      .get(url, {
+        params: Object.assign(defaults, options),
+      })
       .then(({ data }) => {
         callback(data);
 
@@ -85,8 +92,32 @@ const Api = {
 
   // Return single project
   project(projectPath) {
-    const url = Api.buildUrl(Api.projectPath)
-            .replace(':id', encodeURIComponent(projectPath));
+    const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
+
+    return axios.get(url);
+  },
+
+  // Return Merge Request for project
+  mergeRequest(projectPath, mergeRequestId) {
+    const url = Api.buildUrl(Api.mergeRequestPath)
+      .replace(':id', encodeURIComponent(projectPath))
+      .replace(':mrid', mergeRequestId);
+
+    return axios.get(url);
+  },
+
+  mergeRequestChanges(projectPath, mergeRequestId) {
+    const url = Api.buildUrl(Api.mergeRequestChangesPath)
+      .replace(':id', encodeURIComponent(projectPath))
+      .replace(':mrid', mergeRequestId);
+
+    return axios.get(url);
+  },
+
+  mergeRequestVersions(projectPath, mergeRequestId) {
+    const url = Api.buildUrl(Api.mergeRequestVersionsPath)
+      .replace(':id', encodeURIComponent(projectPath))
+      .replace(':mrid', mergeRequestId);
 
     return axios.get(url);
   },
@@ -102,30 +133,30 @@ const Api = {
       url = Api.buildUrl(Api.groupLabelsPath).replace(':namespace_path', namespacePath);
     }
 
-    return axios.post(url, {
-      label: data,
-    })
+    return axios
+      .post(url, {
+        label: data,
+      })
       .then(res => callback(res.data))
       .catch(e => callback(e.response.data));
   },
 
   // Return group projects list. Filtered by query
   groupProjects(groupId, query, callback) {
-    const url = Api.buildUrl(Api.groupProjectsPath)
-      .replace(':id', groupId);
-    return axios.get(url, {
-      params: {
-        search: query,
-        per_page: 20,
-      },
-    })
+    const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
+    return axios
+      .get(url, {
+        params: {
+          search: query,
+          per_page: 20,
+        },
+      })
       .then(({ data }) => callback(data));
   },
 
   commitMultiple(id, data) {
     // see https://docs.gitlab.com/ce/api/commits.html#create-a-commit-with-multiple-files-and-actions
-    const url = Api.buildUrl(Api.commitPath)
-      .replace(':id', encodeURIComponent(id));
+    const url = Api.buildUrl(Api.commitPath).replace(':id', encodeURIComponent(id));
     return axios.post(url, JSON.stringify(data), {
       headers: {
         'Content-Type': 'application/json; charset=utf-8',
@@ -136,39 +167,34 @@ const Api = {
   branchSingle(id, branch) {
     const url = Api.buildUrl(Api.branchSinglePath)
       .replace(':id', encodeURIComponent(id))
-      .replace(':branch', branch);
+      .replace(':branch', encodeURIComponent(branch));
 
     return axios.get(url);
   },
 
   // Return text for a specific license
   licenseText(key, data, callback) {
-    const url = Api.buildUrl(Api.licensePath)
-      .replace(':key', key);
-    return axios.get(url, {
-      params: data,
-    })
+    const url = Api.buildUrl(Api.licensePath).replace(':key', key);
+    return axios
+      .get(url, {
+        params: data,
+      })
       .then(res => callback(res.data));
   },
 
   gitignoreText(key, callback) {
-    const url = Api.buildUrl(Api.gitignorePath)
-      .replace(':key', key);
-    return axios.get(url)
-      .then(({ data }) => callback(data));
+    const url = Api.buildUrl(Api.gitignorePath).replace(':key', key);
+    return axios.get(url).then(({ data }) => callback(data));
   },
 
   gitlabCiYml(key, callback) {
-    const url = Api.buildUrl(Api.gitlabCiYmlPath)
-      .replace(':key', key);
-    return axios.get(url)
-      .then(({ data }) => callback(data));
+    const url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key);
+    return axios.get(url).then(({ data }) => callback(data));
   },
 
   dockerfileYml(key, callback) {
     const url = Api.buildUrl(Api.dockerfilePath).replace(':key', key);
-    return axios.get(url)
-      .then(({ data }) => callback(data));
+    return axios.get(url).then(({ data }) => callback(data));
   },
 
   issueTemplate(namespacePath, projectPath, key, type, callback) {
@@ -177,7 +203,8 @@ const Api = {
       .replace(':type', type)
       .replace(':project_path', projectPath)
       .replace(':namespace_path', namespacePath);
-    return axios.get(url)
+    return axios
+      .get(url)
       .then(({ data }) => callback(null, data))
       .catch(callback);
   },
@@ -185,10 +212,13 @@ const Api = {
   users(query, options) {
     const url = Api.buildUrl(this.usersPath);
     return axios.get(url, {
-      params: Object.assign({
-        search: query,
-        per_page: 20,
-      }, options),
+      params: Object.assign(
+        {
+          search: query,
+          per_page: 20,
+        },
+        options,
+      ),
     });
   },
 
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 0c54c992e51e076d55ab8fc1edbd3029ed81cce0..037e3efb4ce687f3dd0343b89e7fb92932adbbbc 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -1,25 +1,25 @@
 <script>
-  import icon from '~/vue_shared/components/icon.vue';
+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: {
+    changedIcon() {
+      return this.file.tempFile ? 'file-addition' : 'file-modified';
     },
-    computed: {
-      changedIcon() {
-        return this.file.tempFile ? 'file-addition' : 'file-modified';
-      },
-      changedIconClass() {
-        return `multi-${this.changedIcon}`;
-      },
+    changedIconClass() {
+      return `multi-${this.changedIcon}`;
     },
-  };
+  },
+};
 </script>
 
 <template>
diff --git a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
index 170347881e0f20539a44454f1d44bcbf85416a55..0c44a755f56f3dfafa39e630d86107a81b7dc1a0 100644
--- a/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
+++ b/app/assets/javascripts/ide/components/editor_mode_dropdown.vue
@@ -1,31 +1,44 @@
 <script>
-  import Icon from '~/vue_shared/components/icon.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import { __, sprintf } from '~/locale';
 
-  export default {
-    components: {
-      Icon,
+export default {
+  components: {
+    Icon,
+  },
+  props: {
+    hasChanges: {
+      type: Boolean,
+      required: false,
+      default: false,
     },
-    props: {
-      hasChanges: {
-        type: Boolean,
-        required: false,
-        default: false,
-      },
-      viewer: {
-        type: String,
-        required: true,
-      },
-      showShadow: {
-        type: Boolean,
-        required: true,
-      },
+    mergeRequestId: {
+      type: String,
+      required: false,
+      default: '',
     },
-    methods: {
-      changeMode(mode) {
-        this.$emit('click', mode);
-      },
+    viewer: {
+      type: String,
+      required: true,
     },
-  };
+    showShadow: {
+      type: Boolean,
+      required: true,
+    },
+  },
+  computed: {
+    mergeReviewLine() {
+      return sprintf(__('Reviewing (merge request !%{mergeRequestId})'), {
+        mergeRequestId: this.mergeRequestId,
+      });
+    },
+  },
+  methods: {
+    changeMode(mode) {
+      this.$emit('click', mode);
+    },
+  },
+};
 </script>
 
 <template>
@@ -43,7 +56,10 @@
       }"
       data-toggle="dropdown"
     >
-      <template v-if="viewer === 'editor'">
+      <template v-if="viewer === 'mrdiff' && mergeRequestId">
+        {{ mergeReviewLine }}
+      </template>
+      <template v-else-if="viewer === 'editor'">
         {{ __('Editing') }}
       </template>
       <template v-else>
@@ -57,6 +73,29 @@
     </button>
     <div class="dropdown-menu dropdown-menu-selectable dropdown-open-left">
       <ul>
+        <template v-if="mergeRequestId">
+          <li>
+            <a
+              href="#"
+              @click.prevent="changeMode('mrdiff')"
+              :class="{
+                'is-active': viewer === 'mrdiff',
+              }"
+            >
+              <strong class="dropdown-menu-inner-title">
+                {{ mergeReviewLine }}
+              </strong>
+              <span class="dropdown-menu-inner-content">
+                {{ __('Compare changes with the merge request target branch') }}
+              </span>
+            </a>
+          </li>
+          <li
+            role="separator"
+            class="divider"
+          >
+          </li>
+        </template>
         <li>
           <a
             href="#"
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 015e750525a0d280a9657eb017baf25e9ba655ae..5e44af01241a636c4c9e262121fa40fe33359da6 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -1,51 +1,51 @@
 <script>
-  import { mapState, mapGetters } from 'vuex';
-  import ideSidebar from './ide_side_bar.vue';
-  import ideContextbar from './ide_context_bar.vue';
-  import repoTabs from './repo_tabs.vue';
-  import repoFileButtons from './repo_file_buttons.vue';
-  import ideStatusBar from './ide_status_bar.vue';
-  import repoEditor from './repo_editor.vue';
+import { mapState, mapGetters } from 'vuex';
+import ideSidebar from './ide_side_bar.vue';
+import ideContextbar from './ide_context_bar.vue';
+import repoTabs from './repo_tabs.vue';
+import repoFileButtons from './repo_file_buttons.vue';
+import ideStatusBar from './ide_status_bar.vue';
+import repoEditor from './repo_editor.vue';
 
-  export default {
-    components: {
-      ideSidebar,
-      ideContextbar,
-      repoTabs,
-      repoFileButtons,
-      ideStatusBar,
-      repoEditor,
+export default {
+  components: {
+    ideSidebar,
+    ideContextbar,
+    repoTabs,
+    repoFileButtons,
+    ideStatusBar,
+    repoEditor,
+  },
+  props: {
+    emptyStateSvgPath: {
+      type: String,
+      required: true,
     },
-    props: {
-      emptyStateSvgPath: {
-        type: String,
-        required: true,
-      },
-      noChangesStateSvgPath: {
-        type: String,
-        required: true,
-      },
-      committedStateSvgPath: {
-        type: String,
-        required: true,
-      },
+    noChangesStateSvgPath: {
+      type: String,
+      required: true,
     },
-    computed: {
-      ...mapState(['changedFiles', 'openFiles', 'viewer']),
-      ...mapGetters(['activeFile', 'hasChanges']),
+    committedStateSvgPath: {
+      type: String,
+      required: true,
     },
-    mounted() {
-      const returnValue = 'Are you sure you want to lose unsaved changes?';
-      window.onbeforeunload = e => {
-        if (!this.changedFiles.length) return undefined;
+  },
+  computed: {
+    ...mapState(['changedFiles', 'openFiles', 'viewer', 'currentMergeRequestId']),
+    ...mapGetters(['activeFile', 'hasChanges']),
+  },
+  mounted() {
+    const returnValue = 'Are you sure you want to lose unsaved changes?';
+    window.onbeforeunload = e => {
+      if (!this.changedFiles.length) return undefined;
 
-        Object.assign(e, {
-          returnValue,
-        });
-        return returnValue;
-      };
-    },
-  };
+      Object.assign(e, {
+        returnValue,
+      });
+      return returnValue;
+    };
+  },
+};
 </script>
 
 <template>
@@ -63,6 +63,7 @@
           :files="openFiles"
           :viewer="viewer"
           :has-changes="hasChanges"
+          :merge-request-id="currentMergeRequestId"
         />
         <repo-editor
           class="multi-file-edit-pane-content"
diff --git a/app/assets/javascripts/ide/components/mr_file_icon.vue b/app/assets/javascripts/ide/components/mr_file_icon.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8a440902dfc7af539798db3383e42ca7e067fef6
--- /dev/null
+++ b/app/assets/javascripts/ide/components/mr_file_icon.vue
@@ -0,0 +1,23 @@
+<script>
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+  components: {
+    icon,
+  },
+  directives: {
+    tooltip,
+  },
+};
+</script>
+
+<template>
+  <icon
+    name="git-merge"
+    v-tooltip
+    title="__('Part of merge request changes')"
+    css-classes="ide-file-changed-icon"
+    :size="12"
+  />
+</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index e73d1ce839fdce7f458e69f624274579139ad8f3..b6f8f8a1c991a4b5b0e25c703f048081bf2a2a9f 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,6 +1,6 @@
 <script>
 /* global monaco */
-import { mapState, mapActions } from 'vuex';
+import { mapState, mapGetters, mapActions } from 'vuex';
 import flash from '~/flash';
 import monacoLoader from '../monaco_loader';
 import Editor from '../lib/editor';
@@ -13,12 +13,8 @@ export default {
     },
   },
   computed: {
-    ...mapState([
-      'leftPanelCollapsed',
-      'rightPanelCollapsed',
-      'viewer',
-      'delayViewerUpdated',
-    ]),
+    ...mapState(['leftPanelCollapsed', 'rightPanelCollapsed', 'viewer', 'delayViewerUpdated']),
+    ...mapGetters(['currentMergeRequest']),
     shouldHideEditor() {
       return this.file && this.file.binary && !this.file.raw;
     },
@@ -68,9 +64,14 @@ export default {
 
       this.editor.clearEditor();
 
-      this.getRawFileData(this.file)
+      this.getRawFileData({
+        path: this.file.path,
+        baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
+      })
         .then(() => {
-          const viewerPromise = this.delayViewerUpdated ? this.updateViewer('editor') : Promise.resolve();
+          const viewerPromise = this.delayViewerUpdated
+            ? this.updateViewer('editor')
+            : Promise.resolve();
 
           return viewerPromise;
         })
@@ -78,7 +79,7 @@ export default {
           this.updateDelayViewerUpdated(false);
           this.createEditorInstance();
         })
-        .catch((err) => {
+        .catch(err => {
           flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
           throw err;
         });
@@ -101,9 +102,13 @@ export default {
 
       this.model = this.editor.createModel(this.file);
 
-      this.editor.attachModel(this.model);
+      if (this.viewer === 'mrdiff') {
+        this.editor.attachMergeRequestModel(this.model);
+      } else {
+        this.editor.attachModel(this.model);
+      }
 
-      this.model.onChange((model) => {
+      this.model.onChange(model => {
         const { file } = model;
 
         if (file.active) {
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 297b9c2628f9e3833dfd970baa821365017717d8..1935ee1a4bbc8dd662310a7f6b550ff0e87dadb7 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -6,6 +6,7 @@ import router from '../ide_router';
 import newDropdown from './new_dropdown/index.vue';
 import fileStatusIcon from './repo_file_status_icon.vue';
 import changedFileIcon from './changed_file_icon.vue';
+import mrFileIcon from './mr_file_icon.vue';
 
 export default {
   name: 'RepoFile',
@@ -15,6 +16,7 @@ export default {
     fileStatusIcon,
     fileIcon,
     changedFileIcon,
+    mrFileIcon,
   },
   props: {
     file: {
@@ -56,10 +58,7 @@ export default {
     ...mapActions(['toggleTreeOpen', 'updateDelayViewerUpdated']),
     clickFile() {
       // Manual Action if a tree is selected/opened
-      if (
-        this.isTree &&
-        this.$router.currentRoute.path === `/project${this.file.url}`
-      ) {
+      if (this.isTree && this.$router.currentRoute.path === `/project${this.file.url}`) {
         this.toggleTreeOpen(this.file.path);
       }
 
@@ -102,11 +101,15 @@ export default {
             :file="file"
           />
         </span>
-        <changed-file-icon
-          :file="file"
-          v-if="file.changed || file.tempFile"
-          class="prepend-top-5 pull-right"
-        />
+        <span class="pull-right">
+          <mr-file-icon
+            v-if="file.mrChange"
+          />
+          <changed-file-icon
+            :file="file"
+            v-if="file.changed || file.tempFile"
+          />
+        </span>
         <new-dropdown
           v-if="isTree"
           :project-id="file.projectId"
diff --git a/app/assets/javascripts/ide/components/repo_file_status_icon.vue b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
index 25d311142d54200e15843ab1e954c84d42308376..97589e116c58be138d8524ca5a660450ac6d152f 100644
--- a/app/assets/javascripts/ide/components/repo_file_status_icon.vue
+++ b/app/assets/javascripts/ide/components/repo_file_status_icon.vue
@@ -1,27 +1,27 @@
 <script>
-  import icon from '~/vue_shared/components/icon.vue';
-  import tooltip from '~/vue_shared/directives/tooltip';
-  import '~/lib/utils/datetime_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import '~/lib/utils/datetime_utility';
 
-  export default {
-    components: {
-      icon,
+export default {
+  components: {
+    icon,
+  },
+  directives: {
+    tooltip,
+  },
+  props: {
+    file: {
+      type: Object,
+      required: true,
     },
-    directives: {
-      tooltip,
+  },
+  computed: {
+    lockTooltip() {
+      return `Locked by ${this.file.file_lock.user.name}`;
     },
-    props: {
-      file: {
-        type: Object,
-        required: true,
-      },
-    },
-    computed: {
-      lockTooltip() {
-        return `Locked by ${this.file.file_lock.user.name}`;
-      },
-    },
-  };
+  },
+};
 </script>
 
 <template>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index 8ea64ddf84a1a8b5638c37558ef38ad5f572d001..a44e418b2eb88797d8d5773431eddf45ea1aa5a3 100644
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
@@ -1,42 +1,46 @@
 <script>
-  import { mapActions } from 'vuex';
-  import RepoTab from './repo_tab.vue';
-  import EditorMode from './editor_mode_dropdown.vue';
+import { mapActions } from 'vuex';
+import RepoTab from './repo_tab.vue';
+import EditorMode from './editor_mode_dropdown.vue';
 
-  export default {
-    components: {
-      RepoTab,
-      EditorMode,
+export default {
+  components: {
+    RepoTab,
+    EditorMode,
+  },
+  props: {
+    files: {
+      type: Array,
+      required: true,
     },
-    props: {
-      files: {
-        type: Array,
-        required: true,
-      },
-      viewer: {
-        type: String,
-        required: true,
-      },
-      hasChanges: {
-        type: Boolean,
-        required: true,
-      },
+    viewer: {
+      type: String,
+      required: true,
     },
-    data() {
-      return {
-        showShadow: false,
-      };
+    hasChanges: {
+      type: Boolean,
+      required: true,
     },
-    updated() {
-      if (!this.$refs.tabsScroller) return;
-
-      this.showShadow =
-        this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
-    },
-    methods: {
-      ...mapActions(['updateViewer']),
+    mergeRequestId: {
+      type: String,
+      required: false,
+      default: '',
     },
-  };
+  },
+  data() {
+    return {
+      showShadow: false,
+    };
+  },
+  updated() {
+    if (!this.$refs.tabsScroller) return;
+
+    this.showShadow = this.$refs.tabsScroller.scrollWidth > this.$refs.tabsScroller.offsetWidth;
+  },
+  methods: {
+    ...mapActions(['updateViewer']),
+  },
+};
 </script>
 
 <template>
@@ -55,6 +59,7 @@
       :viewer="viewer"
       :show-shadow="showShadow"
       :has-changes="hasChanges"
+      :merge-request-id="mergeRequestId"
       @click="updateViewer"
     />
   </div>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index db89c1d44db8a3bb109cd61eef8923a4d7680769..be2c12c0487aa0206622068754467bc2d9eba891 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -44,7 +44,7 @@ const router = new VueRouter({
           component: EmptyRouterComponent,
         },
         {
-          path: 'mr/:mrid',
+          path: 'merge_requests/:mrid',
           component: EmptyRouterComponent,
         },
       ],
@@ -76,9 +76,7 @@ router.beforeEach((to, from, next) => {
             .then(() => {
               if (to.params[0]) {
                 const path =
-                  to.params[0].slice(-1) === '/'
-                    ? to.params[0].slice(0, -1)
-                    : to.params[0];
+                  to.params[0].slice(-1) === '/' ? to.params[0].slice(0, -1) : to.params[0];
                 const treeEntry = store.state.entries[path];
                 if (treeEntry) {
                   store.dispatch('handleTreeEntryAction', treeEntry);
@@ -96,6 +94,60 @@ router.beforeEach((to, from, next) => {
               );
               throw e;
             });
+        } else if (to.params.mrid) {
+          store.dispatch('updateViewer', 'mrdiff');
+
+          store
+            .dispatch('getMergeRequestData', {
+              projectId: fullProjectId,
+              mergeRequestId: to.params.mrid,
+            })
+            .then(mr => {
+              store.dispatch('getBranchData', {
+                projectId: fullProjectId,
+                branchId: mr.source_branch,
+              });
+
+              return store.dispatch('getFiles', {
+                projectId: fullProjectId,
+                branchId: mr.source_branch,
+              });
+            })
+            .then(() =>
+              store.dispatch('getMergeRequestVersions', {
+                projectId: fullProjectId,
+                mergeRequestId: to.params.mrid,
+              }),
+            )
+            .then(() =>
+              store.dispatch('getMergeRequestChanges', {
+                projectId: fullProjectId,
+                mergeRequestId: to.params.mrid,
+              }),
+            )
+            .then(mrChanges => {
+              mrChanges.changes.forEach((change, ind) => {
+                const changeTreeEntry = store.state.entries[change.new_path];
+
+                if (changeTreeEntry) {
+                  store.dispatch('setFileMrChange', {
+                    file: changeTreeEntry,
+                    mrChange: change,
+                  });
+
+                  if (ind < 10) {
+                    store.dispatch('getFileData', {
+                      path: change.new_path,
+                      makeFileActive: ind === 0,
+                    });
+                  }
+                }
+              });
+            })
+            .catch(e => {
+              flash('Error while loading the merge request. Please try again.');
+              throw e;
+            });
         }
       })
       .catch(e => {
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index 73cd684351ce0b56ca2e1a4af11a1963c1d02bff..d372c2aaad859cef335d28373a51b237c0469a9f 100644
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -21,6 +21,15 @@ export default class Model {
         new this.monaco.Uri(null, null, this.file.path),
       )),
     );
+    if (this.file.mrChange) {
+      this.disposable.add(
+        (this.baseModel = this.monaco.editor.createModel(
+          this.file.baseRaw,
+          undefined,
+          new this.monaco.Uri(null, null, `target/${this.file.path}`),
+        )),
+      );
+    }
 
     this.events = new Map();
 
@@ -28,10 +37,7 @@ export default class Model {
     this.dispose = this.dispose.bind(this);
 
     eventHub.$on(`editor.update.model.dispose.${this.file.path}`, this.dispose);
-    eventHub.$on(
-      `editor.update.model.content.${this.file.path}`,
-      this.updateContent,
-    );
+    eventHub.$on(`editor.update.model.content.${this.file.path}`, this.updateContent);
   }
 
   get url() {
@@ -58,6 +64,10 @@ export default class Model {
     return this.originalModel;
   }
 
+  getBaseModel() {
+    return this.baseModel;
+  }
+
   setValue(value) {
     this.getModel().setValue(value);
   }
@@ -78,13 +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.content.${this.file.path}`,
-      this.updateContent,
-    );
+    eventHub.$off(`editor.update.model.dispose.${this.file.path}`, this.dispose);
+    eventHub.$off(`editor.update.model.content.${this.file.path}`, this.updateContent);
   }
 }
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index 887dd7e39b1a65617f324876c64894503ca6f070..6b4ba30e08679b871ceeaf9502085c79d563cb84 100644
--- a/app/assets/javascripts/ide/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -109,11 +109,19 @@ export default class Editor {
     if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
   }
 
+  attachMergeRequestModel(model) {
+    this.instance.setModel({
+      original: model.getBaseModel(),
+      modified: model.getModel(),
+    });
+
+    this.monaco.editor.createDiffNavigator(this.instance, {
+      alwaysRevealFirst: true,
+    });
+  }
+
   setupMonacoTheme() {
-    this.monaco.editor.defineTheme(
-      gitlabTheme.themeName,
-      gitlabTheme.monacoTheme,
-    );
+    this.monaco.editor.defineTheme(gitlabTheme.themeName, gitlabTheme.monacoTheme);
 
     this.monaco.editor.setTheme('gitlab');
   }
@@ -161,8 +169,6 @@ export default class Editor {
   onPositionChange(cb) {
     if (!this.instance.onDidChangeCursorPosition) return;
 
-    this.disposable.add(
-      this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
-    );
+    this.disposable.add(this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)));
   }
 }
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 5f1fb6cf843d66732e85cd2863d436d6a3fbaefd..a12e637616a70d05472ac352b3637fadb23ceaf8 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -20,12 +20,35 @@ export default {
       return Promise.resolve(file.raw);
     }
 
-    return Vue.http.get(file.rawPath, { params: { format: 'json' } })
+    return Vue.http.get(file.rawPath, { params: { format: 'json' } }).then(res => res.text());
+  },
+  getBaseRawFileData(file, sha) {
+    if (file.tempFile) {
+      return Promise.resolve(file.baseRaw);
+    }
+
+    if (file.baseRaw) {
+      return Promise.resolve(file.baseRaw);
+    }
+
+    return Vue.http
+      .get(file.rawPath.replace(`/raw/${file.branchId}/${file.path}`, `/raw/${sha}/${file.path}`), {
+        params: { format: 'json' },
+      })
       .then(res => res.text());
   },
   getProjectData(namespace, project) {
     return Api.project(`${namespace}/${project}`);
   },
+  getProjectMergeRequestData(projectId, mergeRequestId) {
+    return Api.mergeRequest(projectId, mergeRequestId);
+  },
+  getProjectMergeRequestChanges(projectId, mergeRequestId) {
+    return Api.mergeRequestChanges(projectId, mergeRequestId);
+  },
+  getProjectMergeRequestVersions(projectId, mergeRequestId) {
+    return Api.mergeRequestVersions(projectId, mergeRequestId);
+  },
   getBranchData(projectId, currentBranchId) {
     return Api.branchSingle(projectId, currentBranchId);
   },
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 7e920aa9f30bfabcdeaadb0c76967b041d4b0e44..0a74f4f8925503eaa011565a8fb3c44f0a672e4a 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -6,8 +6,7 @@ import FilesDecoratorWorker from './workers/files_decorator_worker';
 
 export const redirectToUrl = (_, url) => visitUrl(url);
 
-export const setInitialData = ({ commit }, data) =>
-  commit(types.SET_INITIAL_DATA, data);
+export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
 
 export const discardAllChanges = ({ state, commit, dispatch }) => {
   state.changedFiles.forEach(file => {
@@ -43,14 +42,11 @@ export const createTempEntry = (
 ) =>
   new Promise(resolve => {
     const worker = new FilesDecoratorWorker();
-    const fullName =
-      name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
+    const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
 
     if (state.entries[name]) {
       flash(
-        `The name "${name
-          .split('/')
-          .pop()}" is already taken in this directory.`,
+        `The name "${name.split('/').pop()}" is already taken in this directory.`,
         'alert',
         document,
         null,
@@ -119,3 +115,4 @@ export const updateDelayViewerUpdated = ({ commit }, delay) => {
 export * from './actions/tree';
 export * from './actions/file';
 export * from './actions/project';
+export * from './actions/merge_request';
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index ddc4b757bf957044f010eb94e124950e9a0a88c5..c21c1a3f5d4eaa0edfdbb25ef4d14d8d00f8de98 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -46,53 +46,63 @@ export const setFileActive = ({ commit, state, getters, dispatch }, path) => {
   commit(types.SET_CURRENT_BRANCH, file.branchId);
 };
 
-export const getFileData = ({ state, commit, dispatch }, file) => {
+export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive = true }) => {
+  const file = state.entries[path];
   commit(types.TOGGLE_LOADING, { entry: file });
-
   return service
     .getFileData(file.url)
     .then(res => {
       const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
-
       setPageTitle(pageTitle);
 
       return res.json();
     })
     .then(data => {
       commit(types.SET_FILE_DATA, { data, file });
-      commit(types.TOGGLE_FILE_OPEN, file.path);
-      dispatch('setFileActive', file.path);
+      commit(types.TOGGLE_FILE_OPEN, path);
+      if (makeFileActive) dispatch('setFileActive', path);
       commit(types.TOGGLE_LOADING, { entry: file });
     })
     .catch(() => {
       commit(types.TOGGLE_LOADING, { entry: file });
-      flash(
-        'Error loading file data. Please try again.',
-        'alert',
-        document,
-        null,
-        false,
-        true,
-      );
+      flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
     });
 };
 
-export const getRawFileData = ({ commit, dispatch }, file) =>
-  service
-    .getRawFileData(file)
-    .then(raw => {
-      commit(types.SET_FILE_RAW_DATA, { file, raw });
-    })
-    .catch(() =>
-      flash(
-        'Error loading file content. Please try again.',
-        'alert',
-        document,
-        null,
-        false,
-        true,
-      ),
-    );
+export const setFileMrChange = ({ state, commit }, { file, mrChange }) => {
+  commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
+};
+
+export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
+  const file = state.entries[path];
+  return new Promise((resolve, reject) => {
+    service
+      .getRawFileData(file)
+      .then(raw => {
+        commit(types.SET_FILE_RAW_DATA, { file, raw });
+        if (file.mrChange && file.mrChange.new_file === false) {
+          service
+            .getBaseRawFileData(file, baseSha)
+            .then(baseRaw => {
+              commit(types.SET_FILE_BASE_RAW_DATA, {
+                file,
+                baseRaw,
+              });
+              resolve(raw);
+            })
+            .catch(e => {
+              reject(e);
+            });
+        } else {
+          resolve(raw);
+        }
+      })
+      .catch(() => {
+        flash('Error loading file content. Please try again.');
+        reject();
+      });
+  });
+};
 
 export const changeFileContent = ({ state, commit }, { path, content }) => {
   const file = state.entries[path];
@@ -119,10 +129,7 @@ export const setFileEOL = ({ getters, commit }, { eol }) => {
   }
 };
 
-export const setEditorPosition = (
-  { getters, commit },
-  { editorRow, editorColumn },
-) => {
+export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn }) => {
   if (getters.activeFile) {
     commit(types.SET_FILE_POSITION, {
       file: getters.activeFile,
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
new file mode 100644
index 0000000000000000000000000000000000000000..da73034fd7d3a70e91d0a25247e05cb1d788fc1e
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -0,0 +1,84 @@
+import flash from '~/flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+
+export const getMergeRequestData = (
+  { commit, state, dispatch },
+  { projectId, mergeRequestId, force = false } = {},
+) =>
+  new Promise((resolve, reject) => {
+    if (!state.projects[projectId].mergeRequests[mergeRequestId] || force) {
+      service
+        .getProjectMergeRequestData(projectId, mergeRequestId)
+        .then(res => res.data)
+        .then(data => {
+          commit(types.SET_MERGE_REQUEST, {
+            projectPath: projectId,
+            mergeRequestId,
+            mergeRequest: data,
+          });
+          if (!state.currentMergeRequestId) {
+            commit(types.SET_CURRENT_MERGE_REQUEST, mergeRequestId);
+          }
+          resolve(data);
+        })
+        .catch(() => {
+          flash('Error loading merge request data. Please try again.');
+          reject(new Error(`Merge Request not loaded ${projectId}`));
+        });
+    } else {
+      resolve(state.projects[projectId].mergeRequests[mergeRequestId]);
+    }
+  });
+
+export const getMergeRequestChanges = (
+  { commit, state, dispatch },
+  { projectId, mergeRequestId, force = false } = {},
+) =>
+  new Promise((resolve, reject) => {
+    if (!state.projects[projectId].mergeRequests[mergeRequestId].changes.length || force) {
+      service
+        .getProjectMergeRequestChanges(projectId, mergeRequestId)
+        .then(res => res.data)
+        .then(data => {
+          commit(types.SET_MERGE_REQUEST_CHANGES, {
+            projectPath: projectId,
+            mergeRequestId,
+            changes: data,
+          });
+          resolve(data);
+        })
+        .catch(() => {
+          flash('Error loading merge request changes. Please try again.');
+          reject(new Error(`Merge Request Changes not loaded ${projectId}`));
+        });
+    } else {
+      resolve(state.projects[projectId].mergeRequests[mergeRequestId].changes);
+    }
+  });
+
+export const getMergeRequestVersions = (
+  { commit, state, dispatch },
+  { projectId, mergeRequestId, force = false } = {},
+) =>
+  new Promise((resolve, reject) => {
+    if (!state.projects[projectId].mergeRequests[mergeRequestId].versions.length || force) {
+      service
+        .getProjectMergeRequestVersions(projectId, mergeRequestId)
+        .then(res => res.data)
+        .then(data => {
+          commit(types.SET_MERGE_REQUEST_VERSIONS, {
+            projectPath: projectId,
+            mergeRequestId,
+            versions: data,
+          });
+          resolve(data);
+        })
+        .catch(() => {
+          flash('Error loading merge request versions. Please try again.');
+          reject(new Error(`Merge Request Versions not loaded ${projectId}`));
+        });
+    } else {
+      resolve(state.projects[projectId].mergeRequests[mergeRequestId].versions);
+    }
+  });
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
index 70a969a03253744c26a1d366bca499c26595590d..6536be04f0a1d1a47a5b8a50f238759acd3c5b94 100644
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -2,9 +2,7 @@ import { normalizeHeaders } from '~/lib/utils/common_utils';
 import flash from '~/flash';
 import service from '../../services';
 import * as types from '../mutation_types';
-import {
-  findEntry,
-} from '../utils';
+import { findEntry } from '../utils';
 import FilesDecoratorWorker from '../workers/files_decorator_worker';
 
 export const toggleTreeOpen = ({ commit, dispatch }, path) => {
@@ -21,23 +19,24 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
 
     dispatch('setFileActive', row.path);
   } else {
-    dispatch('getFileData', row);
+    dispatch('getFileData', { path: row.path });
   }
 };
 
 export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
   if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
 
-  service.getTreeLastCommit(tree.lastCommitPath)
-    .then((res) => {
+  service
+    .getTreeLastCommit(tree.lastCommitPath)
+    .then(res => {
       const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
 
       commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
 
       return res.json();
     })
-    .then((data) => {
-      data.forEach((lastCommit) => {
+    .then(data => {
+      data.forEach(lastCommit => {
         const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
 
         if (entry) {
@@ -50,44 +49,47 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
     .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
 };
 
-export const getFiles = (
-  { state, commit, dispatch },
-  { projectId, branchId } = {},
-) => new Promise((resolve, reject) => {
-  if (!state.trees[`${projectId}/${branchId}`]) {
-    const selectedProject = state.projects[projectId];
-    commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
-
-    service
-      .getFiles(selectedProject.web_url, branchId)
-      .then(res => res.json())
-      .then((data) => {
-        const worker = new FilesDecoratorWorker();
-        worker.addEventListener('message', (e) => {
-          const { entries, treeList } = e.data;
-          const selectedTree = state.trees[`${projectId}/${branchId}`];
-
-          commit(types.SET_ENTRIES, entries);
-          commit(types.SET_DIRECTORY_DATA, { treePath: `${projectId}/${branchId}`, data: treeList });
-          commit(types.TOGGLE_LOADING, { entry: selectedTree, forceValue: false });
-
-          worker.terminate();
-
-          resolve();
-        });
-
-        worker.postMessage({
-          data,
-          projectId,
-          branchId,
+export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) =>
+  new Promise((resolve, reject) => {
+    if (!state.trees[`${projectId}/${branchId}`]) {
+      const selectedProject = state.projects[projectId];
+      commit(types.CREATE_TREE, { treePath: `${projectId}/${branchId}` });
+
+      service
+        .getFiles(selectedProject.web_url, branchId)
+        .then(res => res.json())
+        .then(data => {
+          const worker = new FilesDecoratorWorker();
+          worker.addEventListener('message', e => {
+            const { entries, treeList } = e.data;
+            const selectedTree = state.trees[`${projectId}/${branchId}`];
+
+            commit(types.SET_ENTRIES, entries);
+            commit(types.SET_DIRECTORY_DATA, {
+              treePath: `${projectId}/${branchId}`,
+              data: treeList,
+            });
+            commit(types.TOGGLE_LOADING, {
+              entry: selectedTree,
+              forceValue: false,
+            });
+
+            worker.terminate();
+
+            resolve();
+          });
+
+          worker.postMessage({
+            data,
+            projectId,
+            branchId,
+          });
+        })
+        .catch(e => {
+          flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
+          reject(e);
         });
-      })
-      .catch((e) => {
-        flash('Error loading tree data. Please try again.', 'alert', document, null, false, true);
-        reject(e);
-      });
-  } else {
-    resolve();
-  }
-});
-
+    } else {
+      resolve();
+    }
+  });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index eba325a31df27773e48b82a66841df4c148ecd45..a77cdbc13c8fd036d67ff995742945177ce1aa7f 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -1,10 +1,8 @@
-export const activeFile = state =>
-  state.openFiles.find(file => file.active) || null;
+export const activeFile = state => state.openFiles.find(file => file.active) || null;
 
 export const addedFiles = state => state.changedFiles.filter(f => f.tempFile);
 
-export const modifiedFiles = state =>
-  state.changedFiles.filter(f => !f.tempFile);
+export const modifiedFiles = state => state.changedFiles.filter(f => !f.tempFile);
 
 export const projectsWithTrees = state =>
   Object.keys(state.projects).map(projectId => {
@@ -23,8 +21,17 @@ export const projectsWithTrees = state =>
     };
   });
 
+export const currentMergeRequest = state => {
+  if (state.projects[state.currentProjectId]) {
+    return state.projects[state.currentProjectId].mergeRequests[state.currentMergeRequestId];
+  }
+  return null;
+};
+
 // eslint-disable-next-line no-confusing-arrow
 export const currentIcon = state =>
   state.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
 
 export const hasChanges = state => !!state.changedFiles.length;
+
+export const hasMergeRequest = state => !!state.currentMergeRequestId;
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index e28f190897c1b4c98047533f112407d28a4398d7..e06be0a3fe9c4620df484c2275cf13aaf0fc0346 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -11,6 +11,12 @@ export const SET_PROJECT = 'SET_PROJECT';
 export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
 export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
 
+// Merge Request Mutation Types
+export const SET_MERGE_REQUEST = 'SET_MERGE_REQUEST';
+export const SET_CURRENT_MERGE_REQUEST = 'SET_CURRENT_MERGE_REQUEST';
+export const SET_MERGE_REQUEST_CHANGES = 'SET_MERGE_REQUEST_CHANGES';
+export const SET_MERGE_REQUEST_VERSIONS = 'SET_MERGE_REQUEST_VERSIONS';
+
 // Branch Mutation Types
 export const SET_BRANCH = 'SET_BRANCH';
 export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
@@ -28,6 +34,7 @@ export const SET_FILE_DATA = 'SET_FILE_DATA';
 export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
 export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
 export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
+export const SET_FILE_BASE_RAW_DATA = 'SET_FILE_BASE_RAW_DATA';
 export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
 export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
 export const SET_FILE_POSITION = 'SET_FILE_POSITION';
@@ -39,5 +46,6 @@ export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
 export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
 export const SET_ENTRIES = 'SET_ENTRIES';
 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';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index da41fc9285c7c5951e026d6965fef5c6b394aac0..5e5eb83166209d7af29d79a1aad0519ea50052da 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,5 +1,6 @@
 import * as types from './mutation_types';
 import projectMutations from './mutations/project';
+import mergeRequestMutation from './mutations/merge_request';
 import fileMutations from './mutations/file';
 import treeMutations from './mutations/tree';
 import branchMutations from './mutations/branch';
@@ -11,10 +12,7 @@ export default {
   [types.TOGGLE_LOADING](state, { entry, forceValue = undefined }) {
     if (entry.path) {
       Object.assign(state.entries[entry.path], {
-        loading:
-          forceValue !== undefined
-            ? forceValue
-            : !state.entries[entry.path].loading,
+        loading: forceValue !== undefined ? forceValue : !state.entries[entry.path].loading,
       });
     } else {
       Object.assign(entry, {
@@ -83,9 +81,7 @@ export default {
 
     if (!foundEntry) {
       Object.assign(state.trees[`${projectId}/${branchId}`], {
-        tree: state.trees[`${projectId}/${branchId}`].tree.concat(
-          data.treeList,
-        ),
+        tree: state.trees[`${projectId}/${branchId}`].tree.concat(data.treeList),
       });
     }
   },
@@ -100,6 +96,7 @@ export default {
     });
   },
   ...projectMutations,
+  ...mergeRequestMutation,
   ...fileMutations,
   ...treeMutations,
   ...branchMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index 2500f13db7c6cb54d86dc49b9ec452af647f2231..692fe39b38e63efa4575aea06aa71a7046e0f5bf 100644
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -28,6 +28,8 @@ export default {
       rawPath: data.raw_path,
       binary: data.binary,
       renderError: data.render_error,
+      raw: null,
+      baseRaw: null,
     });
   },
   [types.SET_FILE_RAW_DATA](state, { file, raw }) {
@@ -35,6 +37,11 @@ export default {
       raw,
     });
   },
+  [types.SET_FILE_BASE_RAW_DATA](state, { file, baseRaw }) {
+    Object.assign(state.entries[file.path], {
+      baseRaw,
+    });
+  },
   [types.UPDATE_FILE_CONTENT](state, { path, content }) {
     const changed = content !== state.entries[path].raw;
 
@@ -59,6 +66,11 @@ export default {
       editorColumn,
     });
   },
+  [types.SET_FILE_MERGE_REQUEST_CHANGE](state, { file, mrChange }) {
+    Object.assign(state.entries[file.path], {
+      mrChange,
+    });
+  },
   [types.DISCARD_FILE_CHANGES](state, path) {
     Object.assign(state.entries[path], {
       content: state.entries[path].raw,
diff --git a/app/assets/javascripts/ide/stores/mutations/merge_request.js b/app/assets/javascripts/ide/stores/mutations/merge_request.js
new file mode 100644
index 0000000000000000000000000000000000000000..334819fe702e4e1c5aeb4c25b21e672ad37ad901
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/merge_request.js
@@ -0,0 +1,33 @@
+import * as types from '../mutation_types';
+
+export default {
+  [types.SET_CURRENT_MERGE_REQUEST](state, currentMergeRequestId) {
+    Object.assign(state, {
+      currentMergeRequestId,
+    });
+  },
+  [types.SET_MERGE_REQUEST](state, { projectPath, mergeRequestId, mergeRequest }) {
+    Object.assign(state.projects[projectPath], {
+      mergeRequests: {
+        [mergeRequestId]: {
+          ...mergeRequest,
+          active: true,
+          changes: [],
+          versions: [],
+          baseCommitSha: null,
+        },
+      },
+    });
+  },
+  [types.SET_MERGE_REQUEST_CHANGES](state, { projectPath, mergeRequestId, changes }) {
+    Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
+      changes,
+    });
+  },
+  [types.SET_MERGE_REQUEST_VERSIONS](state, { projectPath, mergeRequestId, versions }) {
+    Object.assign(state.projects[projectPath].mergeRequests[mergeRequestId], {
+      versions,
+      baseCommitSha: versions.length ? versions[0].base_commit_sha : null,
+    });
+  },
+};
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
index 2816562a91968985a64559bce570abf75dd882a0..284b39a2c72a81278937f1a95d782af9dcebd580 100644
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -11,6 +11,7 @@ export default {
     Object.assign(project, {
       tree: [],
       branches: {},
+      mergeRequests: {},
       active: true,
     });
 
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 6110f54951c8926de95c756d6bc31555408d22a0..e5cc8814000238fa361f674c3a14f5c68a3e9472 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,6 +1,7 @@
 export default () => ({
   currentProjectId: '',
   currentBranchId: '',
+  currentMergeRequestId: '',
   changedFiles: [],
   endpoints: {},
   lastCommitMsg: '',
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 487ea1ead8e6aee72040a6260f9e25043d02bdde..3389eeeaa2ea453258cc0bc912c98a4b7ecca58f 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -38,7 +38,7 @@ export const dataStructure = () => ({
   eol: '',
 });
 
-export const decorateData = (entity) => {
+export const decorateData = entity => {
   const {
     id,
     projectId,
@@ -57,7 +57,6 @@ export const decorateData = (entity) => {
     base64 = false,
 
     file_lock,
-
   } = entity;
 
   return {
@@ -80,17 +79,15 @@ export const decorateData = (entity) => {
     base64,
 
     file_lock,
-
   };
 };
 
-export const findEntry = (tree, type, name, prop = 'name') => tree.find(
-  f => f.type === type && f[prop] === name,
-);
+export const findEntry = (tree, type, name, prop = 'name') =>
+  tree.find(f => f.type === type && f[prop] === name);
 
 export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
 
-export const setPageTitle = (title) => {
+export const setPageTitle = title => {
   document.title = title;
 };
 
@@ -120,6 +117,11 @@ const sortTreesByTypeAndName = (a, b) => {
   return 0;
 };
 
-export const sortTree = sortedTree => sortedTree.map(entity => Object.assign(entity, {
-  tree: entity.tree.length ? sortTree(entity.tree) : [],
-})).sort(sortTreesByTypeAndName);
+export const sortTree = sortedTree =>
+  sortedTree
+    .map(entity =>
+      Object.assign(entity, {
+        tree: entity.tree.length ? sortTree(entity.tree) : [],
+      }),
+    )
+    .sort(sortTreesByTypeAndName);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index a266bb6771fa6c5ab3faeda6ffe12ea1404876fc..dd17544b656be8f7b046e15fde63d54d91580641 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -51,7 +51,7 @@ export function removeParams(params) {
   const url = document.createElement('a');
   url.href = window.location.href;
 
-  params.forEach((param) => {
+  params.forEach(param => {
     url.search = removeParamQueryString(url.search, param);
   });
 
@@ -83,3 +83,11 @@ export function refreshCurrentPage() {
 export function redirectTo(url) {
   return window.location.assign(url);
 }
+
+export function webIDEUrl(route = undefined) {
+  let returnUrl = `${gon.relative_url_root}/-/ide/`;
+  if (route) {
+    returnUrl += `project${route}`;
+  }
+  return returnUrl;
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index 3d886e7d628a6f1dd450e110163453a3c66175b3..18ee4c62bf11aa7ab8d4451bd0daec3cb03931ab 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,53 +1,57 @@
 <script>
-  import tooltip from '~/vue_shared/directives/tooltip';
-  import { n__ } from '~/locale';
-  import icon from '~/vue_shared/components/icon.vue';
-  import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import tooltip from '~/vue_shared/directives/tooltip';
+import { n__ } from '~/locale';
+import { webIDEUrl } from '~/lib/utils/url_utility';
+import icon from '~/vue_shared/components/icon.vue';
+import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
 
-  export default {
-    name: 'MRWidgetHeader',
-    directives: {
-      tooltip,
+export default {
+  name: 'MRWidgetHeader',
+  directives: {
+    tooltip,
+  },
+  components: {
+    icon,
+    clipboardButton,
+  },
+  props: {
+    mr: {
+      type: Object,
+      required: true,
     },
-    components: {
-      icon,
-      clipboardButton,
+  },
+  computed: {
+    shouldShowCommitsBehindText() {
+      return this.mr.divergedCommitsCount > 0;
     },
-    props: {
-      mr: {
-        type: Object,
-        required: true,
-      },
+    commitsText() {
+      return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
     },
-    computed: {
-      shouldShowCommitsBehindText() {
-        return this.mr.divergedCommitsCount > 0;
-      },
-      commitsText() {
-        return n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount);
-      },
-      branchNameClipboardData() {
-        // This supports code in app/assets/javascripts/copy_to_clipboard.js that
-        // works around ClipboardJS limitations to allow the context-specific
-        // copy/pasting of plain text or GFM.
-        return JSON.stringify({
-          text: this.mr.sourceBranch,
-          gfm: `\`${this.mr.sourceBranch}\``,
-        });
-      },
-      isSourceBranchLong() {
-        return this.isBranchTitleLong(this.mr.sourceBranch);
-      },
-      isTargetBranchLong() {
-        return this.isBranchTitleLong(this.mr.targetBranch);
-      },
+    branchNameClipboardData() {
+      // This supports code in app/assets/javascripts/copy_to_clipboard.js that
+      // works around ClipboardJS limitations to allow the context-specific
+      // copy/pasting of plain text or GFM.
+      return JSON.stringify({
+        text: this.mr.sourceBranch,
+        gfm: `\`${this.mr.sourceBranch}\``,
+      });
     },
-    methods: {
-      isBranchTitleLong(branchTitle) {
-        return branchTitle.length > 32;
-      },
+    isSourceBranchLong() {
+      return this.isBranchTitleLong(this.mr.sourceBranch);
     },
-  };
+    isTargetBranchLong() {
+      return this.isBranchTitleLong(this.mr.targetBranch);
+    },
+    webIdePath() {
+      return webIDEUrl(this.mr.statusPath.replace('.json', ''));
+    },
+  },
+  methods: {
+    isBranchTitleLong(branchTitle) {
+      return branchTitle.length > 32;
+    },
+  },
+};
 </script>
 <template>
   <div class="mr-source-target">
@@ -96,6 +100,13 @@
     </div>
 
     <div v-if="mr.isOpen">
+      <a
+        v-if="!mr.sourceBranchRemoved"
+        :href="webIdePath"
+        class="btn btn-sm btn-default inline js-web-ide"
+      >
+        {{ s__("mrWidget|Web IDE") }}
+      </a>
       <button
         data-target="#modal_merge_info"
         data-toggle="modal"
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 343408531657b18fe68aeffc304934a39a40ea37..96e781146a70007f1adcfac4a26cd5a0584f1618 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -53,6 +53,7 @@
       flex: 1;
       white-space: nowrap;
       text-overflow: ellipsis;
+      max-width: inherit;
 
       svg {
         vertical-align: middle;
diff --git a/spec/javascripts/api_spec.js b/spec/javascripts/api_spec.js
index 5477581c1b961ff7a3e3a49ecffd3d0318158448..3d7ccf432be91adfa50d71a50e886cbdc099dd70 100644
--- a/spec/javascripts/api_spec.js
+++ b/spec/javascripts/api_spec.js
@@ -35,14 +35,14 @@ describe('Api', () => {
   });
 
   describe('group', () => {
-    it('fetches a group', (done) => {
+    it('fetches a group', done => {
       const groupId = '123456';
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`;
       mock.onGet(expectedUrl).reply(200, {
         name: 'test',
       });
 
-      Api.group(groupId, (response) => {
+      Api.group(groupId, response => {
         expect(response.name).toBe('test');
         done();
       });
@@ -50,15 +50,17 @@ describe('Api', () => {
   });
 
   describe('groups', () => {
-    it('fetches groups', (done) => {
+    it('fetches groups', done => {
       const query = 'dummy query';
       const options = { unused: 'option' };
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`;
-      mock.onGet(expectedUrl).reply(200, [{
-        name: 'test',
-      }]);
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          name: 'test',
+        },
+      ]);
 
-      Api.groups(query, options, (response) => {
+      Api.groups(query, options, response => {
         expect(response.length).toBe(1);
         expect(response[0].name).toBe('test');
         done();
@@ -67,14 +69,16 @@ describe('Api', () => {
   });
 
   describe('namespaces', () => {
-    it('fetches namespaces', (done) => {
+    it('fetches namespaces', done => {
       const query = 'dummy query';
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`;
-      mock.onGet(expectedUrl).reply(200, [{
-        name: 'test',
-      }]);
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          name: 'test',
+        },
+      ]);
 
-      Api.namespaces(query, (response) => {
+      Api.namespaces(query, response => {
         expect(response.length).toBe(1);
         expect(response[0].name).toBe('test');
         done();
@@ -83,31 +87,35 @@ describe('Api', () => {
   });
 
   describe('projects', () => {
-    it('fetches projects with membership when logged in', (done) => {
+    it('fetches projects with membership when logged in', done => {
       const query = 'dummy query';
       const options = { unused: 'option' };
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
       window.gon.current_user_id = 1;
-      mock.onGet(expectedUrl).reply(200, [{
-        name: 'test',
-      }]);
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          name: 'test',
+        },
+      ]);
 
-      Api.projects(query, options, (response) => {
+      Api.projects(query, options, response => {
         expect(response.length).toBe(1);
         expect(response[0].name).toBe('test');
         done();
       });
     });
 
-    it('fetches projects without membership when not logged in', (done) => {
+    it('fetches projects without membership when not logged in', done => {
       const query = 'dummy query';
       const options = { unused: 'option' };
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
-      mock.onGet(expectedUrl).reply(200, [{
-        name: 'test',
-      }]);
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          name: 'test',
+        },
+      ]);
 
-      Api.projects(query, options, (response) => {
+      Api.projects(query, options, response => {
         expect(response.length).toBe(1);
         expect(response[0].name).toBe('test');
         done();
@@ -115,8 +123,65 @@ describe('Api', () => {
     });
   });
 
+  describe('mergerequest', () => {
+    it('fetches a merge request', done => {
+      const projectPath = 'abc';
+      const mergeRequestId = '123456';
+      const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}`;
+      mock.onGet(expectedUrl).reply(200, {
+        title: 'test',
+      });
+
+      Api.mergeRequest(projectPath, mergeRequestId)
+        .then(({ data }) => {
+          expect(data.title).toBe('test');
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+  });
+
+  describe('mergerequest changes', () => {
+    it('fetches the changes of a merge request', done => {
+      const projectPath = 'abc';
+      const mergeRequestId = '123456';
+      const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/changes`;
+      mock.onGet(expectedUrl).reply(200, {
+        title: 'test',
+      });
+
+      Api.mergeRequestChanges(projectPath, mergeRequestId)
+        .then(({ data }) => {
+          expect(data.title).toBe('test');
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+  });
+
+  describe('mergerequest versions', () => {
+    it('fetches the versions of a merge request', done => {
+      const projectPath = 'abc';
+      const mergeRequestId = '123456';
+      const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/versions`;
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          id: 123,
+        },
+      ]);
+
+      Api.mergeRequestVersions(projectPath, mergeRequestId)
+        .then(({ data }) => {
+          expect(data.length).toBe(1);
+          expect(data[0].id).toBe(123);
+        })
+        .then(done)
+        .catch(done.fail);
+    });
+  });
+
   describe('newLabel', () => {
-    it('creates a new label', (done) => {
+    it('creates a new label', done => {
       const namespace = 'some namespace';
       const project = 'some project';
       const labelData = { some: 'data' };
@@ -124,36 +189,42 @@ describe('Api', () => {
       const expectedData = {
         label: labelData,
       };
-      mock.onPost(expectedUrl).reply((config) => {
+      mock.onPost(expectedUrl).reply(config => {
         expect(config.data).toBe(JSON.stringify(expectedData));
 
-        return [200, {
-          name: 'test',
-        }];
+        return [
+          200,
+          {
+            name: 'test',
+          },
+        ];
       });
 
-      Api.newLabel(namespace, project, labelData, (response) => {
+      Api.newLabel(namespace, project, labelData, response => {
         expect(response.name).toBe('test');
         done();
       });
     });
 
-    it('creates a group label', (done) => {
+    it('creates a group label', done => {
       const namespace = 'group/subgroup';
       const labelData = { some: 'data' };
       const expectedUrl = `${dummyUrlRoot}/groups/${namespace}/-/labels`;
       const expectedData = {
         label: labelData,
       };
-      mock.onPost(expectedUrl).reply((config) => {
+      mock.onPost(expectedUrl).reply(config => {
         expect(config.data).toBe(JSON.stringify(expectedData));
 
-        return [200, {
-          name: 'test',
-        }];
+        return [
+          200,
+          {
+            name: 'test',
+          },
+        ];
       });
 
-      Api.newLabel(namespace, undefined, labelData, (response) => {
+      Api.newLabel(namespace, undefined, labelData, response => {
         expect(response.name).toBe('test');
         done();
       });
@@ -161,15 +232,17 @@ describe('Api', () => {
   });
 
   describe('groupProjects', () => {
-    it('fetches group projects', (done) => {
+    it('fetches group projects', done => {
       const groupId = '123456';
       const query = 'dummy query';
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
-      mock.onGet(expectedUrl).reply(200, [{
-        name: 'test',
-      }]);
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          name: 'test',
+        },
+      ]);
 
-      Api.groupProjects(groupId, query, (response) => {
+      Api.groupProjects(groupId, query, response => {
         expect(response.length).toBe(1);
         expect(response[0].name).toBe('test');
         done();
@@ -178,13 +251,13 @@ describe('Api', () => {
   });
 
   describe('licenseText', () => {
-    it('fetches a license text', (done) => {
+    it('fetches a license text', done => {
       const licenseKey = "driver's license";
       const data = { unused: 'option' };
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/licenses/${licenseKey}`;
       mock.onGet(expectedUrl).reply(200, 'test');
 
-      Api.licenseText(licenseKey, data, (response) => {
+      Api.licenseText(licenseKey, data, response => {
         expect(response).toBe('test');
         done();
       });
@@ -192,12 +265,12 @@ describe('Api', () => {
   });
 
   describe('gitignoreText', () => {
-    it('fetches a gitignore text', (done) => {
+    it('fetches a gitignore text', done => {
       const gitignoreKey = 'ignore git';
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitignores/${gitignoreKey}`;
       mock.onGet(expectedUrl).reply(200, 'test');
 
-      Api.gitignoreText(gitignoreKey, (response) => {
+      Api.gitignoreText(gitignoreKey, response => {
         expect(response).toBe('test');
         done();
       });
@@ -205,12 +278,12 @@ describe('Api', () => {
   });
 
   describe('gitlabCiYml', () => {
-    it('fetches a .gitlab-ci.yml', (done) => {
+    it('fetches a .gitlab-ci.yml', done => {
       const gitlabCiYmlKey = 'Y CI ML';
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/gitlab_ci_ymls/${gitlabCiYmlKey}`;
       mock.onGet(expectedUrl).reply(200, 'test');
 
-      Api.gitlabCiYml(gitlabCiYmlKey, (response) => {
+      Api.gitlabCiYml(gitlabCiYmlKey, response => {
         expect(response).toBe('test');
         done();
       });
@@ -218,12 +291,12 @@ describe('Api', () => {
   });
 
   describe('dockerfileYml', () => {
-    it('fetches a Dockerfile', (done) => {
+    it('fetches a Dockerfile', done => {
       const dockerfileYmlKey = 'a giant whale';
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/templates/dockerfiles/${dockerfileYmlKey}`;
       mock.onGet(expectedUrl).reply(200, 'test');
 
-      Api.dockerfileYml(dockerfileYmlKey, (response) => {
+      Api.dockerfileYml(dockerfileYmlKey, response => {
         expect(response).toBe('test');
         done();
       });
@@ -231,12 +304,14 @@ describe('Api', () => {
   });
 
   describe('issueTemplate', () => {
-    it('fetches an issue template', (done) => {
+    it('fetches an issue template', done => {
       const namespace = 'some namespace';
       const project = 'some project';
       const templateKey = ' template #%?.key ';
       const templateType = 'template type';
-      const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(templateKey)}`;
+      const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(
+        templateKey,
+      )}`;
       mock.onGet(expectedUrl).reply(200, 'test');
 
       Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => {
@@ -247,13 +322,15 @@ describe('Api', () => {
   });
 
   describe('users', () => {
-    it('fetches users', (done) => {
+    it('fetches users', done => {
       const query = 'dummy query';
       const options = { unused: 'option' };
       const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`;
-      mock.onGet(expectedUrl).reply(200, [{
-        name: 'test',
-      }]);
+      mock.onGet(expectedUrl).reply(200, [
+        {
+          name: 'test',
+        },
+      ]);
 
       Api.users(query, options)
         .then(({ data }) => {
diff --git a/spec/javascripts/ide/components/changed_file_icon_spec.js b/spec/javascripts/ide/components/changed_file_icon_spec.js
index 987aea7befcea2401a63174aa6ae7259a5b0201b..541864e912ecc08d4fe6bcac859468d970512482 100644
--- a/spec/javascripts/ide/components/changed_file_icon_spec.js
+++ b/spec/javascripts/ide/components/changed_file_icon_spec.js
@@ -11,6 +11,7 @@ describe('IDE changed file icon', () => {
     vm = createComponent(component, {
       file: {
         tempFile: false,
+        changed: true,
       },
     });
   });
@@ -20,7 +21,7 @@ describe('IDE changed file icon', () => {
   });
 
   describe('changedIcon', () => {
-    it('equals file-modified when not a temp file', () => {
+    it('equals file-modified when not a temp file and has changes', () => {
       expect(vm.changedIcon).toBe('file-modified');
     });
 
diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js
index ae657e8c881a8b796bf9b0e11de0667e11f9b4ee..9d3fa1280f4b17019e5c165becef4a92d391400b 100644
--- a/spec/javascripts/ide/components/repo_editor_spec.js
+++ b/spec/javascripts/ide/components/repo_editor_spec.js
@@ -89,6 +89,20 @@ describe('RepoEditor', () => {
         done();
       });
     });
+
+    it('calls createDiffInstance when viewer is a merge request diff', done => {
+      vm.$store.state.viewer = 'mrdiff';
+
+      spyOn(vm.editor, 'createDiffInstance');
+
+      vm.createEditorInstance();
+
+      vm.$nextTick(() => {
+        expect(vm.editor.createDiffInstance).toHaveBeenCalled();
+
+        done();
+      });
+    });
   });
 
   describe('setupEditor', () => {
@@ -134,4 +148,48 @@ describe('RepoEditor', () => {
       });
     });
   });
+
+  describe('setup editor for merge request viewing', () => {
+    beforeEach(done => {
+      // Resetting as the main test setup has already done it
+      vm.$destroy();
+      resetStore(vm.$store);
+      Editor.editorInstance.modelManager.dispose();
+
+      const f = {
+        ...file(),
+        active: true,
+        tempFile: true,
+        html: 'testing',
+        mrChange: { diff: 'ABC' },
+        baseRaw: 'testing',
+        content: 'test',
+      };
+      const RepoEditor = Vue.extend(repoEditor);
+      vm = createComponentWithStore(RepoEditor, store, {
+        file: f,
+      });
+
+      vm.$store.state.openFiles.push(f);
+      vm.$store.state.entries[f.path] = f;
+
+      vm.$store.state.viewer = 'mrdiff';
+
+      vm.monaco = true;
+
+      vm.$mount();
+
+      monacoLoader(['vs/editor/editor.main'], () => {
+        setTimeout(done, 0);
+      });
+    });
+
+    it('attaches merge request model to editor when merge request diff', () => {
+      spyOn(vm.editor, 'attachMergeRequestModel').and.callThrough();
+
+      vm.setupEditor();
+
+      expect(vm.editor.attachMergeRequestModel).toHaveBeenCalledWith(vm.model);
+    });
+  });
 });
diff --git a/spec/javascripts/ide/components/repo_tabs_spec.js b/spec/javascripts/ide/components/repo_tabs_spec.js
index ceb0416aff8c7baf92ff0a5593c0a70b78d63039..73ea796048544ee1246643dea43f38d7fb71a0ef 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,
+      hasMergeRequest: false,
     });
     openedFiles[0].active = true;
 
@@ -56,6 +57,7 @@ describe('RepoTabs', () => {
           files: [],
           viewer: 'editor',
           hasChanges: false,
+          hasMergeRequest: false,
         },
         '#test-app',
       );
diff --git a/spec/javascripts/ide/lib/common/model_spec.js b/spec/javascripts/ide/lib/common/model_spec.js
index adc6a93c06b51f77c23c796c75b0e40e844cd86f..7cd990adb53308526ec0b453f5b312b92447baf6 100644
--- a/spec/javascripts/ide/lib/common/model_spec.js
+++ b/spec/javascripts/ide/lib/common/model_spec.js
@@ -11,7 +11,10 @@ describe('Multi-file editor library model', () => {
     spyOn(eventHub, '$on').and.callThrough();
 
     monacoLoader(['vs/editor/editor.main'], () => {
-      model = new Model(monaco, file('path'));
+      const f = file('path');
+      f.mrChange = { diff: 'ABC' };
+      f.baseRaw = 'test';
+      model = new Model(monaco, f);
 
       done();
     });
@@ -21,9 +24,10 @@ describe('Multi-file editor library model', () => {
     model.dispose();
   });
 
-  it('creates original model & new model', () => {
+  it('creates original model & base model & new model', () => {
     expect(model.originalModel).not.toBeNull();
     expect(model.model).not.toBeNull();
+    expect(model.baseModel).not.toBeNull();
   });
 
   it('adds eventHub listener', () => {
@@ -51,6 +55,12 @@ describe('Multi-file editor library model', () => {
     });
   });
 
+  describe('getBaseModel', () => {
+    it('returns base model', () => {
+      expect(model.getBaseModel()).toBe(model.baseModel);
+    });
+  });
+
   describe('setValue', () => {
     it('updates models value', () => {
       model.setValue('testing 123');
diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js
index 2ccd87de1a76f433c25f1f4a01030839ab6c42ce..ec56ebc03412a7a43247fb98e2664ae0847ebffa 100644
--- a/spec/javascripts/ide/lib/editor_spec.js
+++ b/spec/javascripts/ide/lib/editor_spec.js
@@ -143,6 +143,31 @@ describe('Multi-file editor library', () => {
     });
   });
 
+  describe('attachMergeRequestModel', () => {
+    let model;
+
+    beforeEach(() => {
+      instance.createDiffInstance(document.createElement('div'));
+
+      const f = file();
+      f.mrChanges = { diff: 'ABC' };
+      f.baseRaw = 'testing';
+
+      model = instance.createModel(f);
+    });
+
+    it('sets original & modified', () => {
+      spyOn(instance.instance, 'setModel');
+
+      instance.attachMergeRequestModel(model);
+
+      expect(instance.instance.setModel).toHaveBeenCalledWith({
+        original: model.getBaseModel(),
+        modified: model.getModel(),
+      });
+    });
+  });
+
   describe('clearEditor', () => {
     it('resets the editor model', () => {
       instance.createInstance(document.createElement('div'));
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 5b7c83656414f352d4e60bac5434a11d866f62a5..2f4516377cf487ff94a97539b8abb5a3a50de547 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -5,7 +5,7 @@ import router from '~/ide/ide_router';
 import eventHub from '~/ide/eventhub';
 import { file, resetStore } from '../../helpers';
 
-describe('Multi-file store file actions', () => {
+describe('IDE store file actions', () => {
   beforeEach(() => {
     spyOn(router, 'push');
   });
@@ -189,7 +189,7 @@ describe('Multi-file store file actions', () => {
 
     it('calls the service', done => {
       store
-        .dispatch('getFileData', localFile)
+        .dispatch('getFileData', { path: localFile.path })
         .then(() => {
           expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
 
@@ -200,7 +200,7 @@ describe('Multi-file store file actions', () => {
 
     it('sets the file data', done => {
       store
-        .dispatch('getFileData', localFile)
+        .dispatch('getFileData', { path: localFile.path })
         .then(() => {
           expect(localFile.blamePath).toBe('blame_path');
 
@@ -211,7 +211,7 @@ describe('Multi-file store file actions', () => {
 
     it('sets document title', done => {
       store
-        .dispatch('getFileData', localFile)
+        .dispatch('getFileData', { path: localFile.path })
         .then(() => {
           expect(document.title).toBe('testing getFileData');
 
@@ -222,7 +222,7 @@ describe('Multi-file store file actions', () => {
 
     it('sets the file as active', done => {
       store
-        .dispatch('getFileData', localFile)
+        .dispatch('getFileData', { path: localFile.path })
         .then(() => {
           expect(localFile.active).toBeTruthy();
 
@@ -231,9 +231,20 @@ describe('Multi-file store file actions', () => {
         .catch(done.fail);
     });
 
+    it('sets the file not as active if we pass makeFileActive false', done => {
+      store
+        .dispatch('getFileData', { path: localFile.path, makeFileActive: false })
+        .then(() => {
+          expect(localFile.active).toBeFalsy();
+
+          done();
+        })
+        .catch(done.fail);
+    });
+
     it('adds the file to open files', done => {
       store
-        .dispatch('getFileData', localFile)
+        .dispatch('getFileData', { path: localFile.path })
         .then(() => {
           expect(store.state.openFiles.length).toBe(1);
           expect(store.state.openFiles[0].name).toBe(localFile.name);
@@ -256,7 +267,7 @@ describe('Multi-file store file actions', () => {
 
     it('calls getRawFileData service method', done => {
       store
-        .dispatch('getRawFileData', tmpFile)
+        .dispatch('getRawFileData', { path: tmpFile.path })
         .then(() => {
           expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
 
@@ -267,7 +278,7 @@ describe('Multi-file store file actions', () => {
 
     it('updates file raw data', done => {
       store
-        .dispatch('getRawFileData', tmpFile)
+        .dispatch('getRawFileData', { path: tmpFile.path })
         .then(() => {
           expect(tmpFile.raw).toBe('raw');
 
@@ -275,6 +286,22 @@ describe('Multi-file store file actions', () => {
         })
         .catch(done.fail);
     });
+
+    it('calls also getBaseRawFileData service method', done => {
+      spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
+
+      tmpFile.mrChange = { new_file: false };
+
+      store
+        .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' })
+        .then(() => {
+          expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
+          expect(tmpFile.baseRaw).toBe('baseraw');
+
+          done();
+        })
+        .catch(done.fail);
+    });
   });
 
   describe('changeFileContent', () => {
diff --git a/spec/javascripts/ide/stores/actions/merge_request_spec.js b/spec/javascripts/ide/stores/actions/merge_request_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..b4ec4a0b1737fe36bb161fe91b176807a34fe0d8
--- /dev/null
+++ b/spec/javascripts/ide/stores/actions/merge_request_spec.js
@@ -0,0 +1,110 @@
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import { resetStore } from '../../helpers';
+
+describe('IDE store merge request actions', () => {
+  beforeEach(() => {
+    store.state.projects.abcproject = {
+      mergeRequests: {},
+    };
+  });
+
+  afterEach(() => {
+    resetStore(store);
+  });
+
+  describe('getMergeRequestData', () => {
+    beforeEach(() => {
+      spyOn(service, 'getProjectMergeRequestData').and.returnValue(
+        Promise.resolve({ data: { title: 'mergerequest' } }),
+      );
+    });
+
+    it('calls getProjectMergeRequestData service method', done => {
+      store
+        .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+        .then(() => {
+          expect(service.getProjectMergeRequestData).toHaveBeenCalledWith('abcproject', 1);
+
+          done();
+        })
+        .catch(done.fail);
+    });
+
+    it('sets the Merge Request Object', done => {
+      store
+        .dispatch('getMergeRequestData', { projectId: 'abcproject', mergeRequestId: 1 })
+        .then(() => {
+          expect(store.state.projects.abcproject.mergeRequests['1'].title).toBe('mergerequest');
+          expect(store.state.currentMergeRequestId).toBe(1);
+
+          done();
+        })
+        .catch(done.fail);
+    });
+  });
+
+  describe('getMergeRequestChanges', () => {
+    beforeEach(() => {
+      spyOn(service, 'getProjectMergeRequestChanges').and.returnValue(
+        Promise.resolve({ data: { title: 'mergerequest' } }),
+      );
+
+      store.state.projects.abcproject.mergeRequests['1'] = { changes: [] };
+    });
+
+    it('calls getProjectMergeRequestChanges service method', done => {
+      store
+        .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+        .then(() => {
+          expect(service.getProjectMergeRequestChanges).toHaveBeenCalledWith('abcproject', 1);
+
+          done();
+        })
+        .catch(done.fail);
+    });
+
+    it('sets the Merge Request Changes Object', done => {
+      store
+        .dispatch('getMergeRequestChanges', { projectId: 'abcproject', mergeRequestId: 1 })
+        .then(() => {
+          expect(store.state.projects.abcproject.mergeRequests['1'].changes.title).toBe(
+            'mergerequest',
+          );
+          done();
+        })
+        .catch(done.fail);
+    });
+  });
+
+  describe('getMergeRequestVersions', () => {
+    beforeEach(() => {
+      spyOn(service, 'getProjectMergeRequestVersions').and.returnValue(
+        Promise.resolve({ data: [{ id: 789 }] }),
+      );
+
+      store.state.projects.abcproject.mergeRequests['1'] = { versions: [] };
+    });
+
+    it('calls getProjectMergeRequestVersions service method', done => {
+      store
+        .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+        .then(() => {
+          expect(service.getProjectMergeRequestVersions).toHaveBeenCalledWith('abcproject', 1);
+
+          done();
+        })
+        .catch(done.fail);
+    });
+
+    it('sets the Merge Request Versions Object', done => {
+      store
+        .dispatch('getMergeRequestVersions', { projectId: 'abcproject', mergeRequestId: 1 })
+        .then(() => {
+          expect(store.state.projects.abcproject.mergeRequests['1'].versions.length).toBe(1);
+          done();
+        })
+        .catch(done.fail);
+    });
+  });
+});
diff --git a/spec/javascripts/ide/stores/actions/tree_spec.js b/spec/javascripts/ide/stores/actions/tree_spec.js
index 381f038067b1c4aa96012aafb1a6b49bf37dd3c7..e0ef57a39666d59824d922caf1beb4707b2f0cf4 100644
--- a/spec/javascripts/ide/stores/actions/tree_spec.js
+++ b/spec/javascripts/ide/stores/actions/tree_spec.js
@@ -68,9 +68,7 @@ describe('Multi-file store tree actions', () => {
           expect(projectTree.tree[0].tree[1].name).toBe('fileinfolder.js');
           expect(projectTree.tree[1].type).toBe('blob');
           expect(projectTree.tree[0].tree[0].tree[0].type).toBe('blob');
-          expect(projectTree.tree[0].tree[0].tree[0].name).toBe(
-            'fileinsubfolder.js',
-          );
+          expect(projectTree.tree[0].tree[0].tree[0].name).toBe('fileinsubfolder.js');
 
           done();
         })
@@ -132,9 +130,7 @@ describe('Multi-file store tree actions', () => {
       store
         .dispatch('getLastCommitData', projectTree)
         .then(() => {
-          expect(service.getTreeLastCommit).toHaveBeenCalledWith(
-            'lastcommitpath',
-          );
+          expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
 
           done();
         })
@@ -160,9 +156,7 @@ describe('Multi-file store tree actions', () => {
         .dispatch('getLastCommitData', projectTree)
         .then(Vue.nextTick)
         .then(() => {
-          expect(projectTree.tree[0].lastCommit.message).not.toBe(
-            'commit message',
-          );
+          expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message');
 
           done();
         })
diff --git a/spec/javascripts/ide/stores/getters_spec.js b/spec/javascripts/ide/stores/getters_spec.js
index a613f3a21cc8f387f058b5ee6f38d74c7edd2f58..33733b97dff1b72f7949bcc6662edb8a5cc9d097 100644
--- a/spec/javascripts/ide/stores/getters_spec.js
+++ b/spec/javascripts/ide/stores/getters_spec.js
@@ -2,7 +2,7 @@ import * as getters from '~/ide/stores/getters';
 import state from '~/ide/stores/state';
 import { file } from '../helpers';
 
-describe('Multi-file store getters', () => {
+describe('IDE store getters', () => {
   let localState;
 
   beforeEach(() => {
@@ -52,4 +52,24 @@ describe('Multi-file store getters', () => {
       expect(modifiedFiles[0].name).toBe('added');
     });
   });
+
+  describe('currentMergeRequest', () => {
+    it('returns Current Merge Request', () => {
+      localState.currentProjectId = 'abcproject';
+      localState.currentMergeRequestId = 1;
+      localState.projects.abcproject = {
+        mergeRequests: {
+          1: { mergeId: 1 },
+        },
+      };
+
+      expect(getters.currentMergeRequest(localState).mergeId).toBe(1);
+    });
+
+    it('returns null if no active Merge Request was found', () => {
+      localState.currentProjectId = 'otherproject';
+
+      expect(getters.currentMergeRequest(localState)).toBeNull();
+    });
+  });
 });
diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js
index 131380248e8538ca73a1328ded2cd191d38ac5cd..8fec94e882a9e6f864687b948d18530af4df8180 100644
--- a/spec/javascripts/ide/stores/mutations/file_spec.js
+++ b/spec/javascripts/ide/stores/mutations/file_spec.js
@@ -2,7 +2,7 @@ import mutations from '~/ide/stores/mutations/file';
 import state from '~/ide/stores/state';
 import { file } from '../../helpers';
 
-describe('Multi-file store file mutations', () => {
+describe('IDE store file mutations', () => {
   let localState;
   let localFile;
 
@@ -62,6 +62,8 @@ describe('Multi-file store file mutations', () => {
       expect(localFile.rawPath).toBe('raw');
       expect(localFile.binary).toBeTruthy();
       expect(localFile.renderError).toBe('render_error');
+      expect(localFile.raw).toBeNull();
+      expect(localFile.baseRaw).toBeNull();
     });
   });
 
@@ -76,6 +78,17 @@ describe('Multi-file store file mutations', () => {
     });
   });
 
+  describe('SET_FILE_BASE_RAW_DATA', () => {
+    it('sets raw data from base branch', () => {
+      mutations.SET_FILE_BASE_RAW_DATA(localState, {
+        file: localFile,
+        baseRaw: 'testing',
+      });
+
+      expect(localFile.baseRaw).toBe('testing');
+    });
+  });
+
   describe('UPDATE_FILE_CONTENT', () => {
     beforeEach(() => {
       localFile.raw = 'test';
@@ -112,6 +125,17 @@ describe('Multi-file store file mutations', () => {
     });
   });
 
+  describe('SET_FILE_MERGE_REQUEST_CHANGE', () => {
+    it('sets file mr change', () => {
+      mutations.SET_FILE_MERGE_REQUEST_CHANGE(localState, {
+        file: localFile,
+        mrChange: { diff: 'ABC' },
+      });
+
+      expect(localFile.mrChange.diff).toBe('ABC');
+    });
+  });
+
   describe('DISCARD_FILE_CHANGES', () => {
     beforeEach(() => {
       localFile.content = 'test';
diff --git a/spec/javascripts/ide/stores/mutations/merge_request_spec.js b/spec/javascripts/ide/stores/mutations/merge_request_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..f724bf464f583b042eb7d161083d17479c1e8e85
--- /dev/null
+++ b/spec/javascripts/ide/stores/mutations/merge_request_spec.js
@@ -0,0 +1,65 @@
+import mutations from '~/ide/stores/mutations/merge_request';
+import state from '~/ide/stores/state';
+
+describe('IDE store merge request mutations', () => {
+  let localState;
+
+  beforeEach(() => {
+    localState = state();
+    localState.projects = { abcproject: { mergeRequests: {} } };
+
+    mutations.SET_MERGE_REQUEST(localState, {
+      projectPath: 'abcproject',
+      mergeRequestId: 1,
+      mergeRequest: {
+        title: 'mr',
+      },
+    });
+  });
+
+  describe('SET_CURRENT_MERGE_REQUEST', () => {
+    it('sets current merge request', () => {
+      mutations.SET_CURRENT_MERGE_REQUEST(localState, 2);
+
+      expect(localState.currentMergeRequestId).toBe(2);
+    });
+  });
+
+  describe('SET_MERGE_REQUEST', () => {
+    it('setsmerge request data', () => {
+      const newMr = localState.projects.abcproject.mergeRequests[1];
+
+      expect(newMr.title).toBe('mr');
+      expect(newMr.active).toBeTruthy();
+    });
+  });
+
+  describe('SET_MERGE_REQUEST_CHANGES', () => {
+    it('sets merge request changes', () => {
+      mutations.SET_MERGE_REQUEST_CHANGES(localState, {
+        projectPath: 'abcproject',
+        mergeRequestId: 1,
+        changes: {
+          diff: 'abc',
+        },
+      });
+
+      const newMr = localState.projects.abcproject.mergeRequests[1];
+      expect(newMr.changes.diff).toBe('abc');
+    });
+  });
+
+  describe('SET_MERGE_REQUEST_VERSIONS', () => {
+    it('sets merge request versions', () => {
+      mutations.SET_MERGE_REQUEST_VERSIONS(localState, {
+        projectPath: 'abcproject',
+        mergeRequestId: 1,
+        versions: [{ id: 123 }],
+      });
+
+      const newMr = localState.projects.abcproject.mergeRequests[1];
+      expect(newMr.versions.length).toBe(1);
+      expect(newMr.versions[0].id).toBe(123);
+    });
+  });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index 235c33fac0d5ec452c4ab033b68b41512328f7c6..9b9c9656979c6ebe22fa53490a82033fea774eec 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -17,46 +17,58 @@ describe('MRWidgetHeader', () => {
   describe('computed', () => {
     describe('shouldShowCommitsBehindText', () => {
       it('return true when there are divergedCommitsCount', () => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 12,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
-          targetBranch: 'master',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 12,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+            targetBranch: 'master',
+            statusPath: 'abc',
+          },
+        });
 
         expect(vm.shouldShowCommitsBehindText).toEqual(true);
       });
 
       it('returns false where there are no divergedComits count', () => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 0,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
-          targetBranch: 'master',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 0,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+            targetBranch: 'master',
+            statusPath: 'abc',
+          },
+        });
         expect(vm.shouldShowCommitsBehindText).toEqual(false);
       });
     });
 
     describe('commitsText', () => {
       it('returns singular when there is one commit', () => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 1,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
-          targetBranch: 'master',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 1,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+            targetBranch: 'master',
+            statusPath: 'abc',
+          },
+        });
 
         expect(vm.commitsText).toEqual('1 commit behind');
       });
 
       it('returns plural when there is more than one commit', () => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 2,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
-          targetBranch: 'master',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 2,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">Link</a>',
+            targetBranch: 'master',
+            statusPath: 'abc',
+          },
+        });
 
         expect(vm.commitsText).toEqual('2 commits behind');
       });
@@ -66,24 +78,27 @@ describe('MRWidgetHeader', () => {
   describe('template', () => {
     describe('common elements', () => {
       beforeEach(() => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 12,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
-          sourceBranchRemoved: false,
-          targetBranchPath: 'foo/bar/commits-path',
-          targetBranchTreePath: 'foo/bar/tree/path',
-          targetBranch: 'master',
-          isOpen: true,
-          emailPatchesPath: '/mr/email-patches',
-          plainDiffPath: '/mr/plainDiffPath',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 12,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+            sourceBranchRemoved: false,
+            targetBranchPath: 'foo/bar/commits-path',
+            targetBranchTreePath: 'foo/bar/tree/path',
+            targetBranch: 'master',
+            isOpen: true,
+            emailPatchesPath: '/mr/email-patches',
+            plainDiffPath: '/mr/plainDiffPath',
+            statusPath: 'abc',
+          },
+        });
       });
 
       it('renders source branch link', () => {
-        expect(
-          vm.$el.querySelector('.js-source-branch').innerHTML,
-        ).toEqual('<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>');
+        expect(vm.$el.querySelector('.js-source-branch').innerHTML).toEqual(
+          '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+        );
       });
 
       it('renders clipboard button', () => {
@@ -101,18 +116,21 @@ describe('MRWidgetHeader', () => {
       });
 
       beforeEach(() => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 12,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
-          sourceBranchRemoved: false,
-          targetBranchPath: 'foo/bar/commits-path',
-          targetBranchTreePath: 'foo/bar/tree/path',
-          targetBranch: 'master',
-          isOpen: true,
-          emailPatchesPath: '/mr/email-patches',
-          plainDiffPath: '/mr/plainDiffPath',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 12,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+            sourceBranchRemoved: false,
+            targetBranchPath: 'foo/bar/commits-path',
+            targetBranchTreePath: 'foo/bar/tree/path',
+            targetBranch: 'master',
+            isOpen: true,
+            emailPatchesPath: '/mr/email-patches',
+            plainDiffPath: '/mr/plainDiffPath',
+            statusPath: 'abc',
+          },
+        });
       });
 
       it('renders checkout branch button with modal trigger', () => {
@@ -123,39 +141,49 @@ describe('MRWidgetHeader', () => {
         expect(button.getAttribute('data-toggle')).toEqual('modal');
       });
 
+      it('renders web ide button', () => {
+        const button = vm.$el.querySelector('.js-web-ide');
+
+        expect(button.textContent.trim()).toEqual('Web IDE');
+        expect(button.getAttribute('href')).toEqual('undefined/-/ide/projectabc');
+      });
+
       it('renders download dropdown with links', () => {
-        expect(
-          vm.$el.querySelector('.js-download-email-patches').textContent.trim(),
-        ).toEqual('Email patches');
+        expect(vm.$el.querySelector('.js-download-email-patches').textContent.trim()).toEqual(
+          'Email patches',
+        );
 
-        expect(
-          vm.$el.querySelector('.js-download-email-patches').getAttribute('href'),
-        ).toEqual('/mr/email-patches');
+        expect(vm.$el.querySelector('.js-download-email-patches').getAttribute('href')).toEqual(
+          '/mr/email-patches',
+        );
 
-        expect(
-          vm.$el.querySelector('.js-download-plain-diff').textContent.trim(),
-        ).toEqual('Plain diff');
+        expect(vm.$el.querySelector('.js-download-plain-diff').textContent.trim()).toEqual(
+          'Plain diff',
+        );
 
-        expect(
-          vm.$el.querySelector('.js-download-plain-diff').getAttribute('href'),
-        ).toEqual('/mr/plainDiffPath');
+        expect(vm.$el.querySelector('.js-download-plain-diff').getAttribute('href')).toEqual(
+          '/mr/plainDiffPath',
+        );
       });
     });
 
     describe('with a closed merge request', () => {
       beforeEach(() => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 12,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
-          sourceBranchRemoved: false,
-          targetBranchPath: 'foo/bar/commits-path',
-          targetBranchTreePath: 'foo/bar/tree/path',
-          targetBranch: 'master',
-          isOpen: false,
-          emailPatchesPath: '/mr/email-patches',
-          plainDiffPath: '/mr/plainDiffPath',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 12,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+            sourceBranchRemoved: false,
+            targetBranchPath: 'foo/bar/commits-path',
+            targetBranchTreePath: 'foo/bar/tree/path',
+            targetBranch: 'master',
+            isOpen: false,
+            emailPatchesPath: '/mr/email-patches',
+            plainDiffPath: '/mr/plainDiffPath',
+            statusPath: 'abc',
+          },
+        });
       });
 
       it('does not render checkout branch button with modal trigger', () => {
@@ -165,30 +193,29 @@ describe('MRWidgetHeader', () => {
       });
 
       it('does not render download dropdown with links', () => {
-        expect(
-          vm.$el.querySelector('.js-download-email-patches'),
-        ).toEqual(null);
+        expect(vm.$el.querySelector('.js-download-email-patches')).toEqual(null);
 
-        expect(
-          vm.$el.querySelector('.js-download-plain-diff'),
-        ).toEqual(null);
+        expect(vm.$el.querySelector('.js-download-plain-diff')).toEqual(null);
       });
     });
 
     describe('without diverged commits', () => {
       beforeEach(() => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 0,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
-          sourceBranchRemoved: false,
-          targetBranchPath: 'foo/bar/commits-path',
-          targetBranchTreePath: 'foo/bar/tree/path',
-          targetBranch: 'master',
-          isOpen: true,
-          emailPatchesPath: '/mr/email-patches',
-          plainDiffPath: '/mr/plainDiffPath',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 0,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+            sourceBranchRemoved: false,
+            targetBranchPath: 'foo/bar/commits-path',
+            targetBranchTreePath: 'foo/bar/tree/path',
+            targetBranch: 'master',
+            isOpen: true,
+            emailPatchesPath: '/mr/email-patches',
+            plainDiffPath: '/mr/plainDiffPath',
+            statusPath: 'abc',
+          },
+        });
       });
 
       it('does not render diverged commits info', () => {
@@ -198,22 +225,27 @@ describe('MRWidgetHeader', () => {
 
     describe('with diverged commits', () => {
       beforeEach(() => {
-        vm = mountComponent(Component, { mr: {
-          divergedCommitsCount: 12,
-          sourceBranch: 'mr-widget-refactor',
-          sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
-          sourceBranchRemoved: false,
-          targetBranchPath: 'foo/bar/commits-path',
-          targetBranchTreePath: 'foo/bar/tree/path',
-          targetBranch: 'master',
-          isOpen: true,
-          emailPatchesPath: '/mr/email-patches',
-          plainDiffPath: '/mr/plainDiffPath',
-        } });
+        vm = mountComponent(Component, {
+          mr: {
+            divergedCommitsCount: 12,
+            sourceBranch: 'mr-widget-refactor',
+            sourceBranchLink: '<a href="/foo/bar/mr-widget-refactor">mr-widget-refactor</a>',
+            sourceBranchRemoved: false,
+            targetBranchPath: 'foo/bar/commits-path',
+            targetBranchTreePath: 'foo/bar/tree/path',
+            targetBranch: 'master',
+            isOpen: true,
+            emailPatchesPath: '/mr/email-patches',
+            plainDiffPath: '/mr/plainDiffPath',
+            statusPath: 'abc',
+          },
+        });
       });
 
       it('renders diverged commits info', () => {
-        expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual('(12 commits behind)');
+        expect(vm.$el.querySelector('.diverged-commits-count').textContent.trim()).toEqual(
+          '(12 commits behind)',
+        );
       });
     });
   });