From 6e969c53a06b949927b325e57392b6037c2db81d Mon Sep 17 00:00:00 2001
From: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
Date: Wed, 15 Feb 2017 13:34:03 +0200
Subject: [PATCH] Tmp MR Widget base for approvals.

---
 .../diff_notes/components/resolve_btn.js      |   1 +
 .../diff_notes/services/resolve.js            |   1 +
 .../javascripts/merge_request_widget.js       |  23 +-
 app/assets/javascripts/notes.js               |   4 +
 .../components/mr_widget_author.js            |  12 +
 .../components/mr_widget_author_time.js       |  23 +
 .../components/mr_widget_deployment.js        |  76 +++
 .../components/mr_widget_header.js            |  49 ++
 .../components/mr_widget_merge_help.js        |  14 +
 .../components/mr_widget_pipeline.js          |  68 +++
 .../components/mr_widget_related_links.js     |  35 ++
 .../components/states/mr_widget_archived.js   |   9 +
 .../components/states/mr_widget_checking.js   |  12 +
 .../components/states/mr_widget_closed.js     |  28 +
 .../components/states/mr_widget_conflicts.js  |  34 ++
 .../components/states/mr_widget_locked.js     |  19 +
 .../mr_widget_merge_when_pipeline_succeeds.js |  84 +++
 .../components/states/mr_widget_merged.js     |  71 +++
 .../states/mr_widget_missing_branch.js        |  26 +
 .../states/mr_widget_not_allowed.js           |  12 +
 .../states/mr_widget_nothing_to_merge.js      |  12 +
 .../states/mr_widget_pipeline_blocked.js      |   9 +
 .../states/mr_widget_pipeline_failed.js       |   9 +
 .../states/mr_widget_ready_to_merge.js        | 152 ++++++
 .../mr_widget_unresolved_discussions.js       |  20 +
 .../components/states/mr_widget_wip.js        |  34 ++
 .../vue_merge_request_widget/index.js         | 116 ++++
 .../services/mr_widget_service.js             |  41 ++
 .../stores/mr_widget_store.js                 | 149 +++++
 .../stores/state_maps.js                      |  28 +
 .../vue_pipelines_index/components/stage.js   |  24 +-
 .../components/pipeline_status_icon.js        |  23 +
 .../vue_shared/pipeline_svg_icons.js          |  43 ++
 app/assets/stylesheets/framework/icons.scss   |   3 +-
 .../stylesheets/pages/merge_requests.scss     | 130 +++--
 app/controllers/application_controller.rb     |   5 +-
 .../projects/branches_controller.rb           |   2 +
 app/controllers/projects/commit_controller.rb |   2 +-
 .../projects/merge_requests_controller.rb     |  91 +++-
 .../projects/pipelines_controller.rb          |   2 +-
 app/helpers/issuables_helper.rb               |   2 +-
 app/helpers/merge_requests_helper.rb          |   3 +
 app/models/merge_request.rb                   |   5 +-
 app/serializers/event_entity.rb               |   4 +
 app/serializers/merge_request_basic_entity.rb |   3 +
 .../merge_request_basic_serializer.rb         |   4 +
 app/serializers/merge_request_entity.rb       | 214 ++++++++
 app/serializers/pipeline_entity.rb            |   8 +-
 app/serializers/stage_entity.rb               |   2 +-
 ...ancel_merge_when_pipeline_succeeds.js.haml |   2 +-
 .../projects/merge_requests/merge.js.haml     |   6 +-
 .../merge_requests/widget/_show.html.haml     |   8 +
 .../issuable/form/_merge_params.html.haml     |   9 -
 config/webpack.config.js                      |   1 +
 .../projects/branches_controller_spec.rb      |  97 +++-
 .../merge_requests_controller_spec.rb         | 512 +++++++++++++-----
 .../javascripts/commit/pipelines/mock_data.js |   1 +
 .../components/mr_widget_author_spec.js       |  39 ++
 .../components/mr_widget_author_time_spec.js  |  61 +++
 .../components/mr_widget_deployment_spec.js   | 139 +++++
 .../components/mr_widget_header_spec.js       |  86 +++
 .../components/mr_widget_merge_help_spec.js   |  51 ++
 .../components/mr_widget_pipeline_spec.js     | 129 +++++
 .../mr_widget_related_links_spec.js           |  93 ++++
 .../states/mr_widget_archived_spec.js         |  18 +
 .../states/mr_widget_checking_spec.js         |  19 +
 .../states/mr_widget_closed_spec.js           |  51 ++
 .../states/mr_widget_conflicts_spec.js        |  95 ++++
 .../states/mr_widget_locked_spec.js           |  33 ++
 spec/serializers/event_entity_spec.rb         |  13 +
 .../merge_request_basic_serializer_spec.rb    |  12 +
 .../merge_request_serializer_spec.rb          | 269 +++++++++
 spec/serializers/pipeline_entity_spec.rb      |   4 +-
 spec/serializers/stage_entity_spec.rb         |   2 +-
 74 files changed, 3202 insertions(+), 289 deletions(-)
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/index.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
 create mode 100644 app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
 create mode 100644 app/assets/javascripts/vue_shared/components/pipeline_status_icon.js
 create mode 100644 app/assets/javascripts/vue_shared/pipeline_svg_icons.js
 create mode 100644 app/serializers/event_entity.rb
 create mode 100644 app/serializers/merge_request_basic_entity.rb
 create mode 100644 app/serializers/merge_request_basic_serializer.rb
 create mode 100644 spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
 create mode 100644 spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
 create mode 100644 spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
 create mode 100644 spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
 create mode 100644 spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
 create mode 100644 spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
 create mode 100644 spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
 create mode 100644 spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js
 create mode 100644 spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
 create mode 100644 spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
 create mode 100644 spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
 create mode 100644 spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
 create mode 100644 spec/serializers/event_entity_spec.rb
 create mode 100644 spec/serializers/merge_request_basic_serializer_spec.rb
 create mode 100644 spec/serializers/merge_request_serializer_spec.rb

diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index 312f38ce241..e7f7299bccb 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -85,6 +85,7 @@ import Vue from 'vue';
 
             CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
             this.discussion.updateHeadline(data);
+            gl.mrWidget.checkStatus();
           } else {
             new Flash('An error occurred when trying to resolve a comment. Please try again.', 'alert');
           }
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index bfa4fc9037a..a9c41d6ca53 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -51,6 +51,7 @@ Vue.use(VueResource);
           }
 
           discussion.updateHeadline(data);
+          gl.mrWidget.checkStatus();
         } else {
           new Flash('An error occurred when trying to resolve a discussion. Please try again.', 'alert');
         }
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index 0e2af3df071..b0220737612 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -43,7 +43,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
       //   ci_status_url        - String, URL to use to check CI status
       //
       this.opts = opts;
-      this.$widgetBody = $('.mr-widget-body');
+      this.$widgetBody = $('.mr-widget-body:eq(0)');
       $('#modal_merge_info').modal({
         show: false
       });
@@ -111,7 +111,7 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
               urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
               return window.location.href = window.location.pathname + urlSuffix;
             } else if (data.merge_error) {
-              return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
+              return $('.mr-widget-body:eq(0)').html("<h4>" + data.merge_error + "</h4>");
             } else {
               callback = function() {
                 return merge_request_widget.mergeInProgress(deleteSourceBranch);
@@ -130,11 +130,12 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
     };
 
     MergeRequestWidget.prototype.getMergeStatus = function() {
-      return $.get(this.opts.merge_check_url, (data) => {
+      var that = this;
+      return $.get(this.opts.merge_check_url, function(data) {
         var $html = $(data);
-        this.updateMergeButton(this.status, this.hasCi, $html);
-        $('.mr-widget-body').replaceWith($html.find('.mr-widget-body'));
-        $('.mr-widget-footer').replaceWith($html.find('.mr-widget-footer'));
+        that.updateMergeButton(this.status, this.hasCi, $html);
+        $('.mr-widget-body:eq(0)').replaceWith($html.find('.mr-widget-body'));
+        $('.mr-widget-footer:eq(0)').replaceWith($html.find('.mr-widget-footer'));
       });
     };
 
@@ -159,15 +160,15 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
           _this.status = data.status;
           _this.hasCi = data.has_ci;
           _this.updateMergeButton(_this.status, _this.hasCi);
+          if (data.coverage) {
+            _this.showCICoverage(data.coverage);
+          }
           if (data.environments && data.environments.length) _this.renderEnvironments(data.environments);
           if (data.status !== _this.opts.ci_status ||
               data.sha !== _this.opts.ci_sha ||
               data.pipeline !== _this.opts.ci_pipeline) {
             _this.opts.ci_status = data.status;
             _this.showCIStatus(data.status);
-            if (data.coverage) {
-              _this.showCICoverage(data.coverage);
-            }
             if (data.pipeline) {
               _this.opts.ci_pipeline = data.pipeline;
               _this.updatePipelineUrls(data.pipeline);
@@ -233,8 +234,8 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
       if (state == null) {
         return;
       }
-      $('.ci_widget').hide();
-      $('.ci_widget.ci-' + state).show();
+      $('.ci_widget:eq(0)').hide();
+      $('.ci_widget.ci-' + state).eq(0).show();
 
       this.initMiniPipelineGraph();
     };
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 1d563c63f39..3317ba13fb5 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -377,6 +377,7 @@ require('./task_list');
       }
 
       gl.utils.localTimeAgo($('.js-timeago'), false);
+      gl.mrWidget.checkStatus();
       return this.updateNotesCount(1);
     };
 
@@ -683,6 +684,9 @@ require('./task_list');
           return note.remove();
         };
       })(this));
+
+      gl.mrWidget.checkStatus();
+
       // Decrement the "Discussions" counter only once
       return this.updateNotesCount(-1);
     };
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js
new file mode 100644
index 00000000000..caa50e0474f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.js
@@ -0,0 +1,12 @@
+export default {
+  name: 'MRWidgetAuthor',
+  props: {
+    author: { type: Object, required: true },
+  },
+  template: `
+    <a class="author_link" :href="author.webUrl">
+      <img :src="author.avatarUrl" class="avatar avatar-inline s16" />
+      <span class="author">{{author.name}}</span>
+    </a>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js
new file mode 100644
index 00000000000..e9c07220d96
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author_time.js
@@ -0,0 +1,23 @@
+import MRWidgetAuthor from './mr_widget_author';
+
+export default {
+  name: 'MRWidgetAuthorTime',
+  props: {
+    actionText: { type: String, required: true },
+    author: { type: Object, required: true },
+    dateTitle: { type: String, required: true },
+    dateReadable: { type: String, required: true },
+  },
+  components: {
+    'mr-widget-author': MRWidgetAuthor,
+  },
+  template: `
+    <h4>
+      {{actionText}}
+      <mr-widget-author :author="author" />
+      <time :title='dateTitle' data-toggle="tooltip" data-placement="top" data-container="body">
+        {{dateReadable}}
+      </time>
+    </h4>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
new file mode 100644
index 00000000000..0a3e9e73da2
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
@@ -0,0 +1,76 @@
+import '~/lib/utils/datetime_utility';
+import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
+
+export default {
+  name: 'MRWidgetDeployment',
+  props: {
+    mr: { type: Object, required: true },
+  },
+  computed: {
+    svg() {
+      return statusClassToSvgMap.icon_status_success;
+    },
+  },
+  methods: {
+    formatDate(date) {
+      return gl.utils.getTimeago().format(date);
+    },
+    hasExternalUrls(deployment = {}) {
+      return deployment.external_url && deployment.external_url_formatted;
+    },
+    hasDeploymentTime(deployment = {}) {
+      return deployment.deployed_at && deployment.deployed_at_formatted;
+    },
+    hasDeploymentMeta(deployment = {}) {
+      return deployment.url && deployment.name;
+    },
+    stopEnvironment(deployment) {
+      const msg = 'Are you sure you want to stop this environment?';
+      const isConfirmed = confirm(msg); // eslint-disable-line
+
+      if (isConfirmed) {
+        // TODO: Handle deployment cancel when backend is implemented.
+      }
+    },
+  },
+  template: `
+    <div class="mr-widget-heading">
+      <div class="ci_widget" v-for="deployment in mr.deployments">
+        <div class="ci-status-icon ci-status-icon-success">
+          <span class="js-icon-link icon-link">
+            <span v-html="svg" aria-hidden="true"></span>
+          </span>
+        </div>
+        <span>
+          <span v-if="hasDeploymentMeta(deployment)">Deployed to</span>
+          <a
+            v-if="hasDeploymentMeta(deployment)"
+            :href="deployment.url"
+            target="_blank" rel="noopener noreferrer nofollow" class="js-deploy-meta">
+            {{deployment.name}}
+          </a>
+          <span v-if="hasExternalUrls(deployment)">on</span>
+          <a
+            v-if="hasExternalUrls(deployment)"
+            :href="deployment.external_url"
+            target="_blank" rel="noopener noreferrer nofollow" class="js-deploy-url">
+            {{deployment.external_url_formatted}}
+          </a>
+          <span
+            v-if="hasDeploymentTime(deployment)"
+            :data-title="deployment.deployed_at_formatted"
+            class="js-deploy-time" data-toggle="tooltip" data-placement="top">
+            {{formatDate(deployment.deployed_at)}}
+          </span>
+          <button
+            v-if="deployment.stop_url"
+            @click="stopEnvironment(deployment)"
+            class="btn btn-default btn-xs" type="button">
+            Stop environment
+          </button>
+        </span>
+      </div>
+    </div>
+  `,
+};
+
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
new file mode 100644
index 00000000000..de81f5377a0
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.js
@@ -0,0 +1,49 @@
+require('../../lib/utils/text_utility');
+
+export default {
+  name: 'MRWidgetHeader',
+  props: {
+    mr: { type: Object, required: true },
+  },
+  computed: {
+    shouldShowCommitsBehindText() {
+      return this.mr.divergedCommitsCount > 0;
+    },
+    commitsText() {
+      return gl.text.pluralize('commit', this.mr.divergedCommitsCount);
+    },
+  },
+  template: `
+    <div class="mr-source-target">
+      <div class="pull-right" v-if="mr.isOpen">
+        <a href="#modal_merge_info" data-toggle="modal" class="btn inline btn-grouped btn-sm">Check out branch</a>
+        <span class="dropdown inline prepend-left-5">
+          <a class="btn btn-sm dropdown-toggle" data-toggle="dropdown">
+            Download as <i class="fa fa-caret-down" aria-hidden="true"></i>
+          </a>
+          <ul class="dropdown-menu dropdown-menu-align-right">
+            <li>
+              <a :href="mr.emailPatchesPath">Email patches</a>
+            </li>
+            <li>
+              <a :href="mr.plainDiffPath">Plain diff</a>
+            </li>
+          </ul>
+        </span>
+      </div>
+      <div class="normal">
+        <span>Request to merge</span>
+        <span class="label-branch">{{mr.sourceBranch}}</span>
+        <span>into</span>
+        <span class="label-branch">
+          <a href="#">{{mr.targetBranch}}</a>
+        </span>
+        <span
+          v-if="shouldShowCommitsBehindText"
+          class="diverged-commits-count">
+          ({{mr.divergedCommitsCount}} {{commitsText}} behind)
+        </span>
+      </div>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js
new file mode 100644
index 00000000000..4d4bed58e83
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.js
@@ -0,0 +1,14 @@
+export default {
+  name: 'MRWidgetMergeHelp',
+  props: {
+    missingBranch: { type: String, required: false, default: '' },
+  },
+  template: `
+    <section class="mr-widget-help">
+      <template v-if="missingBranch">If the {{missingBranch}} branch exists in your local repository, you</template>
+      <template v-else>You</template>
+      can merge this merge request manually using the
+      <a data-toggle="modal" href="#modal_merge_info">command line</a>
+    </section>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
new file mode 100644
index 00000000000..10fbde56d72
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.js
@@ -0,0 +1,68 @@
+import PipelineStage from '../../vue_pipelines_index/components/stage';
+import pipelineStatusIcon from '../../vue_shared/components/pipeline_status_icon';
+import { statusClassToSvgMap } from '../../vue_shared/pipeline_svg_icons';
+
+export default {
+  name: 'MRWidgetPipeline',
+  props: {
+    mr: { type: Object, required: true },
+  },
+  components: {
+    'pipeline-stage': PipelineStage,
+    'pipeline-status-icon': pipelineStatusIcon,
+  },
+  computed: {
+    hasCIError() {
+      const { hasCI, ciStatus } = this.mr;
+
+      return hasCI && !ciStatus;
+    },
+    svg() {
+      return statusClassToSvgMap.icon_status_failed;
+    },
+  },
+  template: `
+    <div class="mr-widget-heading">
+      <div class="ci_widget">
+        <template v-if="hasCIError">
+          <div class="ci-status-icon ci-status-icon-failed js-ci-error">
+            <span class="js-icon-link icon-link">
+              <span v-html="svg" aria-hidden="true"></span>
+            </span>
+          </div>
+          <span>Could not connect to the CI server. Please check your settings and try again.</span>
+        </template>
+        <template v-else>
+          <pipeline-status-icon :pipelineStatus="mr.pipeline.details.status" />
+          <span>
+            Pipeline
+            <a
+              :href="mr.pipeline.path"
+              class="pipeline-id">#{{mr.pipeline.id}}</a>
+            {{mr.pipeline.details.status.label}}
+          </span>
+          <div class="mr-widget-pipeline-graph">
+            <div class="stage-cell">
+              <div class="stage-container dropdown js-mini-pipeline-graph"
+                v-if="mr.pipeline.details.stages.length > 0"
+                v-for="stage in mr.pipeline.details.stages">
+                <pipeline-stage :stage="stage" />
+              </div>
+            </div>
+          </div>
+          <span>
+            for
+            <a class="monospace js-commit-link"
+              :href="mr.pipeline.commit.commit_path">{{mr.pipeline.commit.short_id}}</a>.
+          </span>
+          <span
+            v-if="mr.pipeline.coverage"
+            class="js-mr-coverage">
+            Coverage {{mr.pipeline.coverage}}%
+          </span>
+        </template>
+      </div>
+    </div>
+  `,
+};
+
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js
new file mode 100644
index 00000000000..f1f42d6ad07
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.js
@@ -0,0 +1,35 @@
+export default {
+  name: 'MRWidgetRelatedLinks',
+  props: {
+    relatedLinks: { type: Object, required: true },
+  },
+  computed: {
+    hasLinks() {
+      return this.relatedLinks.closing || this.relatedLinks.mentioned;
+    },
+  },
+  methods: {
+    hasMultipleIssues(text) {
+      return !text ? false : text.match(/<\/a> and <a/);
+    },
+    issueLabel(field) {
+      return this.hasMultipleIssues(this.relatedLinks[field]) ? 'issues' : 'issue';
+    },
+    verbLabel(field) {
+      return this.hasMultipleIssues(this.relatedLinks[field]) ? 'are' : 'is';
+    },
+  },
+  template: `
+    <section class="mr-info-list mr-links" v-if="hasLinks">
+      <div class="legend"></div>
+      <p v-if="relatedLinks.closing">
+        Closes {{issueLabel('closing')}} <span v-html="relatedLinks.closing"></span>.
+      </p>
+      <p v-if="relatedLinks.mentioned">
+        <span class="capitalize">{{issueLabel('mentioned')}}</span>
+        <span v-html="relatedLinks.mentioned"></span>
+        {{verbLabel('mentioned')}} mentioned but will not be closed.
+      </p>
+    </section>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
new file mode 100644
index 00000000000..9c680bc9845
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.js
@@ -0,0 +1,9 @@
+export default {
+  name: 'MRWidgetArchived',
+  template: `
+    <div class="mr-widget-body">
+      <button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
+      <span class="bold">This project is archived, write access has been disabled.</span>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
new file mode 100644
index 00000000000..69aedee4fc1
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.js
@@ -0,0 +1,12 @@
+export default {
+  name: 'MRWidgetChecking',
+  template: `
+    <div class="mr-widget-body">
+      <button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
+      <span class="bold">
+        Checking ability to merge automatically.
+        <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+      </span>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
new file mode 100644
index 00000000000..58633e1bd76
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.js
@@ -0,0 +1,28 @@
+import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
+
+export default {
+  name: 'MRWidgetClosed',
+  props: {
+    mr: { type: Object, required: true },
+  },
+  components: {
+    'mr-widget-author-and-time': mrWidgetAuthorTime,
+  },
+  template: `
+    <div class="mr-widget-body">
+      <mr-widget-author-and-time
+        actionText="Closed by"
+        :author="mr.closedBy"
+        :dateTitle="mr.updatedAt"
+        :dateReadable="mr.closedAt"
+      />
+      <section>
+        <p>The changes were not merged into
+          <a :href="mr.targetBranchPath" class="label-branch">
+            {{mr.targetBranch}}
+          </a>.
+        </p>
+      </section>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
new file mode 100644
index 00000000000..9f1aad06491
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.js
@@ -0,0 +1,34 @@
+export default {
+  name: 'MRWidgetConflicts',
+  props: {
+    mr: { type: Object, required: true },
+  },
+  computed: {
+    showResolveConflictsButton() {
+      const { canMerge, canResolveConflicts, canResolveConflictsInUI } = this.mr;
+      return canMerge && canResolveConflicts && canResolveConflictsInUI;
+    },
+  },
+  template: `
+    <div class="mr-widget-body">
+      <button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
+      <span class="bold">
+        There are merge conflicts.
+        <span v-if="!mr.canMerge">Resolve these conflicts or ask someone with write access to this repository to merge it locally.</span>
+      </span>
+      <div class="btn-group">
+        <a
+          :href="mr.conflictResolutionPath"
+          v-if="showResolveConflictsButton"
+          class="btn btn-default btn-xs js-resolve-conflicts-button"
+        >Resolve conflicts</a>
+        <a
+          v-if="mr.canMerge"
+          class="btn btn-default btn-xs js-merge-locally-button"
+          data-toggle="modal"
+          href="#modal_merge_info"
+        >Merge locally</a>
+      </div>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js
new file mode 100644
index 00000000000..d7b4284ce4b
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_locked.js
@@ -0,0 +1,19 @@
+export default {
+  name: 'MRWidgetLocked',
+  props: {
+    mr: { type: Object, required: true },
+  },
+  template: `
+    <div class="mr-widget-body">
+      <span class="bold">Locked</span> This merge request is in the process of being merged, during which time it is locked and cannot be closed.
+      <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+      <section>
+        <p>The changes will be merged into
+          <a :href="mr.targetBranchPath" class="label-branch">
+            {{mr.targetBranch}}
+          </a>
+        </p>
+      </section>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
new file mode 100644
index 00000000000..0ad4403e928
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.js
@@ -0,0 +1,84 @@
+import MRWidgetAuthor from '../../components/mr_widget_author';
+
+export default {
+  name: 'MRWidgetMergeWhenPipelineSucceeds',
+  props: {
+    mr: { type: Object, required: true },
+    service: { type: Object, required: true },
+  },
+  components: {
+    'mr-widget-author': MRWidgetAuthor,
+  },
+  data() {
+    return {
+      isCancellingAutoMerge: false,
+      isRemovingSourceBranch: false,
+    };
+  },
+  computed: {
+    canRemoveSourceBranch() {
+      const { shouldRemoveSourceBranch, canRemoveSourceBranch,
+        mergeUserId, currentUserId } = this.mr;
+
+      return !shouldRemoveSourceBranch && canRemoveSourceBranch && mergeUserId === currentUserId;
+    },
+  },
+  methods: {
+    cancelAutomaticMerge() {
+      this.isCancellingAutoMerge = true;
+      this.service.cancelAutomaticMerge()
+        .then(res => res.json())
+        .then((res) => {
+          this.mr.setData(res); // TODO: Should find a better way to update store.
+        });
+        // TODO: Handle catch here.
+    },
+    removeSourceBranch() {
+      const options = {
+        sha: this.mr.sha,
+        merge_when_pipeline_succeeds: true,
+        should_remove_source_branch: true,
+      };
+
+      this.isRemovingSourceBranch = true;
+      this.service.mergeResource.save(options); // TODO: Response and error handling, widget update
+    },
+  },
+  template: `
+    <div class="mr-widget-body">
+      <h4>
+        Set by
+        <mr-widget-author :author="mr.setToMWPSBy" />
+        to be merged automatically when the pipeline succeeds.
+        <button
+          v-if="mr.canCancelAutomaticMerge"
+          @click="cancelAutomaticMerge"
+          :disabled="isCancellingAutoMerge"
+          type="button" class="btn btn-xs btn-default">
+          <i
+            v-if="isCancellingAutoMerge"
+            class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+            Cancel automatic merge</button>
+      </h4>
+      <section>
+        <p>The changes will be merged into
+          <a :href="mr.targetBranchPath" class="label-branch">
+            {{mr.targetBranch}}
+          </a>
+        </p>
+        <p v-if="mr.shouldRemoveSourceBranch">The source branch will be removed.</p>
+        <p v-else>
+          The source branch will not be removed.
+          <button
+            v-if="canRemoveSourceBranch"
+            @click="removeSourceBranch"
+            type="button" class="btn btn-xs btn-default">
+            <i
+            v-if="isRemovingSourceBranch"
+            class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+            Remove source branch</button>
+        </p>
+      </section>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
new file mode 100644
index 00000000000..617e4a0bd0f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.js
@@ -0,0 +1,71 @@
+import mrWidgetAuthorTime from '../../components/mr_widget_author_time';
+
+export default {
+  name: 'MRWidgetMerged',
+  props: {
+    mr: { type: Object, required: true },
+    service: { type: Object, required: true },
+  },
+  components: {
+    'mr-widget-author-and-time': mrWidgetAuthorTime,
+  },
+  data() {
+    return {
+      isRemovingSourceBranch: false,
+    };
+  },
+  methods: {
+    removeSourceBranch() {
+      this.isRemovingSourceBranch = true;
+      this.service.removeSourceBranch()
+        .then(res => res.json()); // TODO: Update widget, handle error
+    },
+  },
+  template: `
+    <div class="mr-widget-body">
+      <mr-widget-author-and-time
+        actionText="Merged by"
+        :author="mr.mergedBy"
+        :dateTitle="mr.updatedAt"
+        :dateReadable="mr.mergedAt"
+      />
+      <section class="mr-info-list">
+        <div class="legend"></div>
+        <p>
+          The changes were merged into
+          <a :href="mr.targetBranchPath" class="label-branch">
+            {{mr.targetBranch}}
+          </a>
+        </p>
+        <p v-if="mr.sourceBranchRemoved">The source branch has been removed.</p>
+        <p v-if="mr.canRemoveSourceBranch">
+          You can remove source branch now.
+          <button
+            @click="removeSourceBranch"
+            :class="{ disabled: isRemovingSourceBranch }"
+            type="button" class="btn btn-xs btn-default">Remove Source Branch</button>
+        </p>
+        <p v-if="isRemovingSourceBranch">
+          The source branch is being removed.
+          <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
+        </p>
+      </section>
+      <div class="merged-buttons clearfix">
+        <a
+          v-if="mr.canRevert"
+          class="btn btn-close btn-sm has-tooltip"
+          href="#modal-revert-commit"
+          data-toggle="modal"
+          data-container="body"
+          data-original-title="Revert this merge request in a new merge request">Revert</a>
+        <a
+          v-if="mr.canBeCherryPicked"
+          class="btn btn-default btn-sm has-tooltip"
+          href="#modal-cherry-pick-commit"
+          data-toggle="modal"
+          data-container="body"
+          data-original-title="Cherry-pick this merge request in a new merge request">Cherry-pick</a>
+      </div>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
new file mode 100644
index 00000000000..c44783583a8
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.js
@@ -0,0 +1,26 @@
+import mrWidgetMergeHelp from '../../components/mr_widget_merge_help';
+
+export default {
+  name: 'MRWidgetMissingBranch',
+  props: {
+    mr: { type: Object, required: true },
+  },
+  components: {
+    'mr-widget-merge-help': mrWidgetMergeHelp,
+  },
+  computed: {
+    missingBranchName() {
+      return this.mr.sourceBranchRemoved ? 'source' : 'target';
+    },
+  },
+  template: `
+    <div class="mr-widget-body">
+      <button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
+      <span class="bold">
+        <span class="capitalize">{{missingBranchName}}</span> branch does not exist.
+        Please restore the {{missingBranchName}} branch or use a different {{missingBranchName}} branch.
+      </span>
+      <mr-widget-merge-help :missing-branch="missingBranchName" />
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
new file mode 100644
index 00000000000..550624cb1a2
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.js
@@ -0,0 +1,12 @@
+export default {
+  name: 'MRWidgetNotAllowed',
+  template: `
+    <div class="mr-widget-body">
+      <button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
+      <span class="bold">
+        Ready to be merged automatically.
+        Ask someone with write access to this repository to merge this request.
+      </span>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
new file mode 100644
index 00000000000..643a9f5d4da
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_nothing_to_merge.js
@@ -0,0 +1,12 @@
+export default {
+  name: 'MRWidgetNothingToMerge',
+  template: `
+    <div class="mr-widget-body">
+      <button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
+      <span class="bold">
+        There is nothing to merge from source branch into target branch.
+        Please push new commits or use a different branch.
+      </span>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
new file mode 100644
index 00000000000..ee26268e2a7
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.js
@@ -0,0 +1,9 @@
+export default {
+  name: 'MRWidgetPipelineBlocked',
+  template: `
+    <div class="mr-widget-body">
+      <button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
+      <span class="bold">Pipeline blocked. The pipeline for this merge request requires a manual action to proceed.</span>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
new file mode 100644
index 00000000000..b5bf9593f58
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_failed.js
@@ -0,0 +1,9 @@
+export default {
+  name: 'MRWidgetPipelineBlocked',
+  template: `
+    <div class="mr-widget-body">
+      <button class="btn btn-success btn-small" disabled="true" type="button">Merge</button>
+      <span class="bold">The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.</span>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
new file mode 100644
index 00000000000..05b8d832728
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -0,0 +1,152 @@
+export default {
+  name: 'MRWidgetReadyToMerge',
+  props: {
+    mr: { type: Object, required: true },
+    service: { type: Object, required: true },
+  },
+  data() {
+    return {
+      removeSourceBranch: true,
+      mergeWhenBuildSucceeds: false,
+      useCommitMessageWithDescription: false,
+      setToMergeWhenPipelineSucceeds: false,
+      showCommitMessageEditor: false,
+      commitMessage: this.mr.commitMessage,
+    };
+  },
+  computed: {
+    commitMessageLinkTitle() {
+      const withDesc = 'Include description in commit message';
+      const withoutDesc = "Don't include description in commit message";
+
+      return this.useCommitMessageWithDescription ? withoutDesc : withDesc;
+    },
+    mergeButtonClass() {
+      const defaultClass = 'btn btn-success';
+      const failedClass = `${defaultClass} btn-danger`;
+      const inActionClass = `${defaultClass} btn-info`;
+      const { pipeline, isPipelineActive, isPipelineFailed, hasCI, ciStatus } = this.mr;
+
+      if (hasCI && !ciStatus) {
+        return failedClass;
+      } else if (!pipeline) {
+        return defaultClass;
+      } else if (isPipelineActive) {
+        return inActionClass;
+      } else if (isPipelineFailed) {
+        return failedClass;
+      }
+
+      return defaultClass;
+    },
+    mergeButtonText() {
+      if (this.mr.isPipelineActive) {
+        return 'Merge when pipeline succeeds';
+      }
+
+      return 'Merge';
+    },
+    shouldShowMergeOptionsDropdown() {
+      return this.mr.isPipelineActive && !this.mr.onlyAllowMergeIfPipelineSucceeds;
+    },
+    isMergeButtonDisabled() {
+      const { commitMessage } = this;
+      return !commitMessage.length || !this.isMergeAllowed();
+    },
+  },
+  methods: {
+    isMergeAllowed() {
+      return !(this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.isPipelineFailed);
+    },
+    updateCommitMessage() {
+      const cmwd = this.mr.commitMessageWithDescription;
+      this.useCommitMessageWithDescription = !this.useCommitMessageWithDescription;
+      this.commitMessage = this.useCommitMessageWithDescription ? cmwd : this.mr.commitMessage;
+    },
+    toggleCommitMessageEditor() {
+      this.showCommitMessageEditor = !this.showCommitMessageEditor;
+    },
+    handleMergeButtonClick(mergeWhenBuildSucceeds) {
+      if (mergeWhenBuildSucceeds === undefined) {
+        mergeWhenBuildSucceeds = this.mr.isPipelineActive; // eslint-disable-line no-param-reassign
+      }
+
+      this.setToMergeWhenPipelineSucceeds = mergeWhenBuildSucceeds === true;
+
+      const options = {
+        sha: this.mr.sha,
+        commit_message: this.commitMessage,
+        merge_when_pipeline_succeeds: this.setToMergeWhenPipelineSucceeds,
+        should_remove_source_branch: this.removeSourceBranch === true,
+      };
+
+      // TODO: Response handling and widget update
+      this.service.merge(options);
+    },
+  },
+  template: `
+    <div class="mr-widget-body">
+      <span class="btn-group">
+        <button
+          @click="handleMergeButtonClick()"
+          :disabled="isMergeButtonDisabled"
+          :class="mergeButtonClass"
+          type="button">{{mergeButtonText}}</button>
+        <button
+          v-if="shouldShowMergeOptionsDropdown"
+          type="button" class="btn btn-info dropdown-toggle" data-toggle="dropdown">
+          <i class="fa fa-caret-down" aria-hidden="true"></i>
+          <span class="sr-only">Select Merge Moment</span>
+        </button>
+        <ul
+          v-if="shouldShowMergeOptionsDropdown"
+          class="dropdown-menu dropdown-menu-right" role="menu">
+          <li>
+            <a
+              @click.prevent="handleMergeButtonClick(true)"
+              class="merge_when_pipeline_succeeds" href="#">
+              <i class="fa fa-check fa-fw" aria-hidden="true"></i> Merge when pipeline succeeds
+            </a>
+          </li>
+          <li>
+            <a
+              @click.prevent="handleMergeButtonClick(false)"
+              class="accept-merge-request" href="#">
+              <i class="fa fa-warning fa-fw" aria-hidden="true"></i> Merge immediately
+            </a>
+          </li>
+        </ul>
+      </span>
+      <template v-if="isMergeAllowed()">
+        <label class="spacing">
+          <input type="checkbox" v-model="removeSourceBranch" /> Remove source branch
+        </label>
+        <a
+          @click.prevent="toggleCommitMessageEditor"
+          class="btn btn-default btn-xs" href="#">Modify commit message</a>
+        <div class="prepend-top-default commit-message-editor" v-if="showCommitMessageEditor">
+          <div class="form-group clearfix">
+            <label class="control-label" for="commit-message">Commit message</label>
+            <div class="col-sm-10">
+              <div class="commit-message-container">
+                <div class="max-width-marker"></div>
+                <textarea
+                  v-model="commitMessage"
+                  class="form-control js-commit-message" required="required" rows="14"></textarea>
+              </div>
+              <p class="hint">Try to keep the first line under 52 characters and the others under 72.</p>
+              <div class="hint">
+                <a @click.prevent="updateCommitMessage" href="#">{{commitMessageLinkTitle}}</a>
+              </div>
+            </div>
+          </div>
+        </div>
+      </template>
+      <template v-else>
+        <span class="bold">
+          The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.
+        </span>
+      </template>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
new file mode 100644
index 00000000000..711f568df73
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions.js
@@ -0,0 +1,20 @@
+export default {
+  name: 'MRWidgetUnresolvedDiscussions',
+  props: {
+    mr: { type: Object, required: true },
+  },
+  template: `
+    <div class="mr-widget-body">
+      <button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
+      <span class="bold">
+        There are unresolved discussions. Please resolve these discussions
+        <span v-if="mr.canCreateIssue">or</span>
+        <span v-else>.</span>
+      </span>
+      <a
+        v-if="mr.canCreateIssue"
+        :href="mr.createIssueToResolveDiscussionsPath"
+        class="btn btn-default btn-xs">Create an issue to resolve them later</a>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
new file mode 100644
index 00000000000..41fa7e6cbe0
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_wip.js
@@ -0,0 +1,34 @@
+/* global Flash */
+
+export default {
+  name: 'MRWidgetWIP',
+  props: {
+    mr: { type: Object, required: true },
+    service: { type: Object, required: true },
+  },
+  methods: {
+    removeWIP() {
+      this.service.removeWIP()
+        .then(res => res.json())
+        .then((res) => {
+          // TODO: Update store better
+          this.mr.setData(res);
+          new Flash('The merge request can now be merged.', 'notice'); // eslint-disable-line
+          $('.merge-request .detail-page-description .title').text(this.mr.title);
+        });
+        // TODO: Catch error state
+    },
+  },
+  template: `
+    <div class="mr-widget-body">
+      <button type="button" class="btn btn-success btn-small" disabled="true">Merge</button>
+      <span class="bold">This merge request is currently Work In Progress and therefore unable to merge</span>
+      <template v-if="mr.canUpdateMergeRequest">
+        <i class="fa fa-question-circle has-tooltip" title="When this merge request is ready, remove the WIP: prefix from the title to allow it to be merged."></i>
+        <button
+          @click="removeWIP"
+          type="button" class="btn btn-default btn-xs">Resolve WIP status</button>
+      </template>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
new file mode 100644
index 00000000000..f4191e2e454
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -0,0 +1,116 @@
+import Vue from 'vue';
+import WidgetHeader from './components/mr_widget_header';
+import WidgetMergeHelp from './components/mr_widget_merge_help';
+import WidgetPipeline from './components/mr_widget_pipeline';
+import WidgetDeployment from './components/mr_widget_deployment';
+import WidgetRelatedLinks from './components/mr_widget_related_links';
+import MergedState from './components/states/mr_widget_merged';
+import ClosedState from './components/states/mr_widget_closed';
+import LockedState from './components/states/mr_widget_locked';
+import WipState from './components/states/mr_widget_wip';
+import ArchivedState from './components/states/mr_widget_archived';
+import ConflictsState from './components/states/mr_widget_conflicts';
+import NothingToMergeState from './components/states/mr_widget_nothing_to_merge';
+import MissingBranchState from './components/states/mr_widget_missing_branch';
+import NotAllowedState from './components/states/mr_widget_not_allowed';
+import ReadyToMergeState from './components/states/mr_widget_ready_to_merge';
+import UnresolvedDiscussionsState from './components/states/mr_widget_unresolved_discussions';
+import PipelineBlockedState from './components/states/mr_widget_pipeline_blocked';
+import PipelineFailedState from './components/states/mr_widget_pipeline_failed';
+import MergeWhenPipelineSucceedsState from './components/states/mr_widget_merge_when_pipeline_succeeds';
+import CheckingState from './components/states/mr_widget_checking';
+import MRWidgetStore from './stores/mr_widget_store';
+import MRWidgetService from './services/mr_widget_service';
+import { stateToComponentMap, statesToShowHelpWidget } from './stores/state_maps';
+
+const mrWidgetOptions = () => ({
+  el: '#js-vue-mr-widget',
+  name: 'MRWidget',
+  data() {
+    const store = new MRWidgetStore(gl.mrWidgetData);
+    const service = new MRWidgetService(store);
+    return {
+      mr: store,
+      service,
+    };
+  },
+  computed: {
+    componentName() {
+      return stateToComponentMap[this.mr.state];
+    },
+    shouldRenderMergeHelp() {
+      return statesToShowHelpWidget.indexOf(this.mr.state) > -1;
+    },
+    shouldRenderPipelines() {
+      return this.mr.pipeline || this.mr.hasCI;
+    },
+    shouldRenderRelatedLinks() {
+      return this.mr.relatedLinks;
+    },
+    shouldRenderDeployments() {
+      return this.mr.deployments.length;
+    },
+  },
+  methods: {
+    checkStatus() {
+      this.service.checkStatus()
+        .then(res => res.json())
+        .then((res) => {
+          this.mr.setData(res);
+        });
+    },
+  },
+  mounted() {
+    this.service.fetchDeployments()
+      .then(res => res.json())
+      .then((res) => {
+        if (res.length) {
+          this.mr.deployments = res;
+        }
+      });
+
+    if (this.mr.state === 'checking') {
+      this.checkStatus();
+    }
+  },
+  components: {
+    'mr-widget-header': WidgetHeader,
+    'mr-widget-merge-help': WidgetMergeHelp,
+    'mr-widget-pipeline': WidgetPipeline,
+    'mr-widget-deployment': WidgetDeployment,
+    'mr-widget-related-links': WidgetRelatedLinks,
+    'mr-widget-merged': MergedState,
+    'mr-widget-closed': ClosedState,
+    'mr-widget-locked': LockedState,
+    'mr-widget-wip': WipState,
+    'mr-widget-archived': ArchivedState,
+    'mr-widget-conflicts': ConflictsState,
+    'mr-widget-nothing-to-merge': NothingToMergeState,
+    'mr-widget-not-allowed': NotAllowedState,
+    'mr-widget-missing-branch': MissingBranchState,
+    'mr-widget-ready-to-merge': ReadyToMergeState,
+    'mr-widget-checking': CheckingState,
+    'mr-widget-unresolved-discussions': UnresolvedDiscussionsState,
+    'mr-widget-pipeline-blocked': PipelineBlockedState,
+    'mr-widget-pipeline-failed': PipelineFailedState,
+    'mr-widget-merge-when-pipeline-succeeds': MergeWhenPipelineSucceedsState,
+  },
+  template: `
+    <div class="mr-state-widget prepend-top-default">
+      <mr-widget-header :mr="mr" />
+      <mr-widget-pipeline v-if="shouldRenderPipelines" :mr="mr" />
+      <mr-widget-deployment v-if="shouldRenderDeployments" :mr="mr" />
+      <component :is="componentName" :mr="mr" :service="service" />
+      <mr-widget-related-links v-if="shouldRenderRelatedLinks" :related-links="mr.relatedLinks" />
+      <mr-widget-merge-help v-if="shouldRenderMergeHelp" />
+    </div>
+  `,
+});
+
+document.addEventListener('DOMContentLoaded', () => {
+  const vm = new Vue(mrWidgetOptions());
+
+  window.gl.mrWidget = {
+    checkStatus: vm.checkStatus,
+  };
+});
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
new file mode 100644
index 00000000000..efc483d3f76
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import VueResource from 'vue-resource';
+
+Vue.use(VueResource);
+
+export default class MRWidgetService {
+  constructor(mr) {
+    this.store = mr;
+
+    this.mergeResource = Vue.resource(mr.mergePath);
+    this.mergeCheckResource = Vue.resource(mr.mergeCheckPath);
+    this.cancelAutoMergeResource = Vue.resource(mr.cancelAutoMergePath);
+    this.removeWIPResource = Vue.resource(mr.removeWIPPath);
+    this.removeSourceBranchResource = Vue.resource(mr.sourceBranchPath);
+    this.deploymentsResource = Vue.resource(mr.ciEnvironmentsStatusPath);
+  }
+
+  merge(data) {
+    return this.mergeResource.save(data);
+  }
+
+  cancelAutomaticMerge() {
+    return this.cancelAutoMergeResource.save();
+  }
+
+  removeWIP() {
+    return this.removeWIPResource.save();
+  }
+
+  removeSourceBranch() {
+    return this.removeSourceBranchResource.delete();
+  }
+
+  fetchDeployments() {
+    return this.deploymentsResource.get();
+  }
+
+  checkStatus() {
+    return this.mergeCheckResource.get();
+  }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
new file mode 100644
index 00000000000..4fd8d198bdb
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -0,0 +1,149 @@
+import Timeago from 'timeago.js';
+
+export default class MergeRequestStore {
+
+  constructor(data) {
+    this.setData(data);
+  }
+
+  setData(data) {
+    // TODO: Remove this
+    this.rawData = data || {};
+
+    const currentUser = data.current_user;
+
+    this.title = data.title;
+    this.targetBranch = data.target_branch;
+    this.sourceBranch = data.source_branch;
+    this.mergeStatus = data.merge_status;
+    this.sha = data.diff_head_sha;
+    this.commitMessage = data.merge_commit_message;
+    this.commitMessageWithDescription = data.merge_commit_message_with_description;
+    this.divergedCommitsCount = data.diverged_commits_count;
+    this.pipeline = data.pipeline;
+    this.deployments = this.deployments || data.deployments || [];
+
+    if (data.issues_links) {
+      const { closing, mentioned_but_not_closing } = data.issues_links;
+      this.relatedLinks = {
+        closing,
+        mentioned: mentioned_but_not_closing,
+      };
+    }
+
+    this.updatedAt = data.updated_at;
+    this.mergedAt = MergeRequestStore.getEventDate(data.merge_event);
+    this.closedAt = MergeRequestStore.getEventDate(data.closed_event);
+    this.mergedBy = MergeRequestStore.getAuthorObject(data.merge_event);
+    this.closedBy = MergeRequestStore.getAuthorObject(data.closed_event);
+    this.setToMWPSBy = MergeRequestStore.getAuthorObject({ author: data.merge_user || {} });
+    this.mergeUserId = data.merge_user_id;
+    this.currentUserId = gon.current_user_id;
+
+    this.sourceBranchPath = data.source_branch_path;
+    this.targetBranchPath = data.target_branch_path;
+    this.conflictResolutionPath = data.conflict_resolution_ui_path;
+    this.cancelAutoMergePath = data.cancel_merge_when_pipeline_succeeds_path;
+    this.removeWIPPath = data.remove_wip_path;
+    this.sourceBranchRemoved = !data.source_branch_exists;
+    this.shouldRemoveSourceBranch = (data.merge_params || {}).should_remove_source_branch || false;
+    this.onlyAllowMergeIfPipelineSucceeds = data.only_allow_merge_if_pipeline_succeeds || false;
+    this.mergeWhenPipelineSucceeds = data.merge_when_pipeline_succeeds || false;
+    this.mergePath = data.merge_path;
+    this.statusPath = data.status_path;
+    this.emailPatchesPath = data.email_patches_path;
+    this.plainDiffPath = data.plain_diff_path;
+    this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
+    this.ciEnvironmentsStatusPath = data.ci_environments_status_url;
+    this.mergeCheckPath = data.merge_check_path;
+
+    this.canRemoveSourceBranch = currentUser.can_remove_source_branch || false;
+    this.canRevert = currentUser.can_revert || false;
+    this.canResolveConflicts = currentUser.can_resolve_conflicts || false;
+    this.canMerge = currentUser.can_merge || false;
+    this.canCreateIssue = currentUser.can_create_issue || false;
+    this.canCancelAutomaticMerge = currentUser.can_cancel_automatic_merge || false;
+    this.canUpdateMergeRequest = currentUser.can_update_merge_request || false;
+    this.canResolveConflictsInUI = data.conflicts_can_be_resolved_in_ui || false;
+    this.canBeCherryPicked = data.can_be_cherry_picked || false;
+    this.canBeMerged = data.can_be_merged || false;
+
+    this.isPipelineActive = data.pipeline ? data.pipeline.active : false;
+    this.isPipelineFailed = data.pipeline ? data.pipeline.details.status.group === 'failed' : false;
+    this.isPipelineBlocked = data.pipeline ? data.pipeline.details.status.group === 'manual' : false;
+    this.isOpen = data.state === 'opened' || data.state === 'reopened' || false;
+    this.hasMergeableDiscussionsState = data.mergeable_discussions_state === false;
+    this.hasCI = data.has_ci;
+    this.ciStatus = data.ci_status;
+
+    this.setState(data);
+  }
+
+  setState(data) {
+    if (this.isOpen) {
+      if (data.project_archived) {
+        this.state = 'archived';
+      } else if (data.branch_missing) {
+        this.state = 'missingBranch';
+      } else if (data.has_no_commits) {
+        this.state = 'nothingToMerge';
+      } else if (this.mergeStatus === 'unchecked') {
+        this.state = 'checking';
+      } else if (data.has_conflicts) {
+        this.state = 'conflicts';
+      } else if (data.work_in_progress) {
+        this.state = 'workInProgress';
+      } else if (this.mergeWhenPipelineSucceeds) {
+        this.state = 'mergeWhenPipelineSucceeds';
+      } else if (!this.canMerge) {
+        this.state = 'notAllowedToMerge';
+      } else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
+        this.state = 'pipelineFailed';
+      } else if (this.hasMergeableDiscussionsState) {
+        this.state = 'unresolvedDiscussions';
+      } else if (this.isPipelineBlocked) {
+        this.state = 'pipelineBlocked';
+      } else if (this.canBeMerged) {
+        this.state = 'readyToMerge';
+      }
+    } else {
+      switch (data.state) {
+        case 'merged':
+          this.state = 'merged';
+          break;
+        case 'closed':
+          this.state = 'closed';
+          break;
+        case 'locked':
+          this.state = 'locked';
+          break;
+        default:
+          this.state = null;
+      }
+    }
+  }
+
+  static getAuthorObject(event) {
+    if (!event) {
+      return {};
+    }
+
+    return {
+      name: event.author.name || '',
+      username: event.author.username || '',
+      webUrl: event.author.web_url || '',
+      avatarUrl: event.author.avatar_url || '',
+    };
+  }
+
+  static getEventDate(event) {
+    const timeagoInstance = new Timeago();
+
+    if (!event) {
+      return '';
+    }
+
+    return timeagoInstance.format(event.updated_at);
+  }
+
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
new file mode 100644
index 00000000000..5f7c6984c4d
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js
@@ -0,0 +1,28 @@
+export const stateToComponentMap = {
+  merged: 'mr-widget-merged',
+  closed: 'mr-widget-closed',
+  locked: 'mr-widget-locked',
+  conflicts: 'mr-widget-conflicts',
+  missingBranch: 'mr-widget-missing-branch',
+  workInProgress: 'mr-widget-wip',
+  readyToMerge: 'mr-widget-ready-to-merge',
+  nothingToMerge: 'mr-widget-nothing-to-merge',
+  notAllowedToMerge: 'mr-widget-not-allowed',
+  archived: 'mr-widget-archived',
+  checking: 'mr-widget-checking',
+  unresolvedDiscussions: 'mr-widget-unresolved-discussions',
+  pipelineBlocked: 'mr-widget-pipeline-blocked',
+  pipelineFailed: 'mr-widget-pipeline-failed',
+  mergeWhenPipelineSucceeds: 'mr-widget-merge-when-pipeline-succeeds',
+};
+
+export const statesToShowHelpWidget = [
+  'locked',
+  'conflicts',
+  'workInProgress',
+  'readyToMerge',
+  'checking',
+  'unresolvedDiscussions',
+  'pipelineFailed',
+  'pipelineBlocked',
+];
diff --git a/app/assets/javascripts/vue_pipelines_index/components/stage.js b/app/assets/javascripts/vue_pipelines_index/components/stage.js
index a2c29002707..df15961a926 100644
--- a/app/assets/javascripts/vue_pipelines_index/components/stage.js
+++ b/app/assets/javascripts/vue_pipelines_index/components/stage.js
@@ -1,32 +1,12 @@
 /* global Flash */
-import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
-import createdSvg from 'icons/_icon_status_created_borderless.svg';
-import failedSvg from 'icons/_icon_status_failed_borderless.svg';
-import manualSvg from 'icons/_icon_status_manual_borderless.svg';
-import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
-import runningSvg from 'icons/_icon_status_running_borderless.svg';
-import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
-import successSvg from 'icons/_icon_status_success_borderless.svg';
-import warningSvg from 'icons/_icon_status_warning_borderless.svg';
+import { statusClassToBorderlessSvgMap } from '../../vue_shared/pipeline_svg_icons';
 
 export default {
   data() {
-    const svgsDictionary = {
-      icon_status_canceled: canceledSvg,
-      icon_status_created: createdSvg,
-      icon_status_failed: failedSvg,
-      icon_status_manual: manualSvg,
-      icon_status_pending: pendingSvg,
-      icon_status_running: runningSvg,
-      icon_status_skipped: skippedSvg,
-      icon_status_success: successSvg,
-      icon_status_warning: warningSvg,
-    };
-
     return {
       builds: '',
       spinner: '<span class="fa fa-spinner fa-spin"></span>',
-      svg: svgsDictionary[this.stage.status.icon],
+      svg: statusClassToBorderlessSvgMap[this.stage.status.icon],
     };
   },
 
diff --git a/app/assets/javascripts/vue_shared/components/pipeline_status_icon.js b/app/assets/javascripts/vue_shared/components/pipeline_status_icon.js
new file mode 100644
index 00000000000..ae246ada01b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/pipeline_status_icon.js
@@ -0,0 +1,23 @@
+import { statusClassToSvgMap } from '../pipeline_svg_icons';
+
+export default {
+  name: 'PipelineStatusIcon',
+  props: {
+    pipelineStatus: { type: Object, required: true, default: () => ({}) },
+  },
+  computed: {
+    svg() {
+      return statusClassToSvgMap[this.pipelineStatus.icon];
+    },
+    statusClass() {
+      return `ci-status-icon ci-status-icon-${this.pipelineStatus.group}`;
+    },
+  },
+  template: `
+    <div :class="statusClass">
+      <a class="icon-link" :href="pipelineStatus.details_path">
+        <span v-html="svg" aria-hidden="true"></span>
+      </a>
+    </div>
+  `,
+};
diff --git a/app/assets/javascripts/vue_shared/pipeline_svg_icons.js b/app/assets/javascripts/vue_shared/pipeline_svg_icons.js
new file mode 100644
index 00000000000..5af30ae74f0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/pipeline_svg_icons.js
@@ -0,0 +1,43 @@
+import canceledSvg from 'icons/_icon_status_canceled.svg';
+import createdSvg from 'icons/_icon_status_created.svg';
+import failedSvg from 'icons/_icon_status_failed.svg';
+import manualSvg from 'icons/_icon_status_manual.svg';
+import pendingSvg from 'icons/_icon_status_pending.svg';
+import runningSvg from 'icons/_icon_status_running.svg';
+import skippedSvg from 'icons/_icon_status_skipped.svg';
+import successSvg from 'icons/_icon_status_success.svg';
+import warningSvg from 'icons/_icon_status_warning.svg';
+
+import canceledBorderlessSvg from 'icons/_icon_status_canceled_borderless.svg';
+import createdBorderlessSvg from 'icons/_icon_status_created_borderless.svg';
+import failedBorderlessSvg from 'icons/_icon_status_failed_borderless.svg';
+import manualBorderlessSvg from 'icons/_icon_status_manual_borderless.svg';
+import pendingBorderlessSvg from 'icons/_icon_status_pending_borderless.svg';
+import runningBorderlessSvg from 'icons/_icon_status_running_borderless.svg';
+import skippedBorderlessSvg from 'icons/_icon_status_skipped_borderless.svg';
+import successBorderlessSvg from 'icons/_icon_status_success_borderless.svg';
+import warningBorderlessSvg from 'icons/_icon_status_warning_borderless.svg';
+
+export const statusClassToSvgMap = {
+  icon_status_canceled: canceledSvg,
+  icon_status_created: createdSvg,
+  icon_status_failed: failedSvg,
+  icon_status_manual: manualSvg,
+  icon_status_pending: pendingSvg,
+  icon_status_running: runningSvg,
+  icon_status_skipped: skippedSvg,
+  icon_status_success: successSvg,
+  icon_status_warning: warningSvg,
+};
+
+export const statusClassToBorderlessSvgMap = {
+  icon_status_canceled: canceledBorderlessSvg,
+  icon_status_created: createdBorderlessSvg,
+  icon_status_failed: failedBorderlessSvg,
+  icon_status_manual: manualBorderlessSvg,
+  icon_status_pending: pendingBorderlessSvg,
+  icon_status_running: runningBorderlessSvg,
+  icon_status_skipped: skippedBorderlessSvg,
+  icon_status_success: successBorderlessSvg,
+  icon_status_warning: warningBorderlessSvg,
+};
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index 87667f39ab8..1b7d4e42258 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -1,4 +1,5 @@
-.ci-status-icon-success {
+.ci-status-icon-success,
+.ci-status-icon-passed {
   color: $green-500;
 
   svg {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 566dcc64802..1266da83746 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -37,12 +37,6 @@
           @include btn-red;
         }
       }
-
-      .dropdown-toggle {
-        .fa {
-          color: inherit;
-        }
-      }
     }
 
     .accept-control {
@@ -89,12 +83,12 @@
   }
 
   .ci_widget {
-    border-bottom: 1px solid $well-inner-border;
     color: $gl-text-color;
     display: -webkit-flex;
     display: flex;
     -webkit-align-items: center;
     align-items: center;
+    padding: $gl-padding-top $gl-padding 0 $gl-padding;
 
     i,
     svg {
@@ -115,16 +109,15 @@
       flex-wrap: wrap;
     }
 
-    .ci-status-icon > .icon-link > svg {
+    .ci-status-icon > .icon-link svg {
       width: 22px;
       height: 22px;
     }
   }
 
   .mr-widget-body,
-  .ci_widget,
   .mr-widget-footer {
-    padding: 16px;
+    margin: 16px;
   }
 
   .mr-widget-pipeline-graph {
@@ -168,10 +161,23 @@
     color: $gl-text-color;
   }
 
+  .capitalize {
+    text-transform: capitalize;
+  }
+
   .js-deployment-link {
     display: inline-block;
   }
 
+  .mr-widget-help {
+    margin: $gl-padding;
+    color: $ci-skipped-color;
+  }
+
+  .mr-links.mr-info-list {
+    margin: 0 0 $gl-padding 26px;
+  }
+
   .mr-widget-body {
     h4 {
       font-weight: 600;
@@ -189,6 +195,29 @@
       margin-right: 7px;
     }
 
+    label {
+      font-weight: normal;
+    }
+
+    .spacing {
+      margin: 0 $gl-padding;
+    }
+
+    .bold {
+      font-weight: bold;
+      color: #5c5c5c;
+    }
+
+    .mr-widget-help {
+      margin: $gl-padding 0;
+    }
+
+    .dropdown-toggle {
+      .fa {
+        color: inherit;
+      }
+    }
+
     @media (max-width: $screen-xs-max) {
       h4 {
         font-size: 14px;
@@ -220,6 +249,12 @@
         margin: 0;
       }
     }
+
+    .commit-message-editor {
+      label {
+        padding: 0;
+      }
+    }
   }
 
   .mr-widget-footer {
@@ -345,61 +380,50 @@
   }
 }
 
-.remove-message-pipes {
-  ul {
-    margin: 10px 0 0 12px;
-    padding: 0;
-    list-style: none;
-    border-left: 2px solid $border-color;
-    display: inline-block;
-  }
+.mr-info-list {
+  position: relative;
+  overflow: hidden;
+  margin: 10px 0 $gl-padding 12px;
 
-  li {
+  p {
+    margin: 6px 0;
     position: relative;
-    margin: 0;
-    padding: 0;
-    display: block;
+    padding-left: 15px;
 
-    span {
-      margin-left: 15px;
-      max-height: 20px;
-    }
-  }
-
-  li::before {
-    content: '';
-    position: absolute;
-    border-top: 2px solid $border-color;
-    height: 1px;
-    top: 8px;
-    width: 8px;
-  }
-
-  li:last-child {
     &::before {
-      top: 18px;
+      content: '';
+      position: absolute;
+      border-top: 2px solid $border-color;
+      height: 1px;
+      top: 8px;
+      width: 8px;
+      left: 0px;
     }
 
-    span {
-      display: block;
-      position: relative;
-      top: 5px;
-      margin-top: 5px;
+    &:last-child {
+      margin-bottom: 0;
+      &::before {
+        top: 14px;
+      }
     }
   }
+
+  .legend {
+    height: 100%;
+    width: 2px;
+    background: $border-color;
+    position: absolute;
+    top: -5px;
+  }
 }
 
 .mr-source-target {
   background-color: $gray-light;
-  line-height: 31px;
-  border-style: solid;
-  border-width: 1px;
-  border-color: $border-color;
-  border-top-right-radius: 3px;
-  border-top-left-radius: 3px;
-  border-bottom: none;
-  padding: 16px;
-  margin-bottom: -1px;
+  border-radius: 3px 3px 0 0;
+  border-bottom: 1px solid $border-color;
+  padding: 0 $gl-padding;
+  margin-bottom: 6px;
+  line-height: 44px;
 }
 
 .panel-new-merge-request {
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 6a6e335d314..65d129ea8e5 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -98,7 +98,10 @@ class ApplicationController < ActionController::Base
   end
 
   def access_denied!
-    render "errors/access_denied", layout: "errors", status: 404
+    respond_to do |format|
+      format.json { head :unauthorized }
+      format.any { render "errors/access_denied", layout: "errors", status: 404 }
+    end
   end
 
   def git_not_found!
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 840405f38cb..d78b71c783f 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -71,7 +71,9 @@ class Projects::BranchesController < Projects::ApplicationController
         redirect_to namespace_project_branches_path(@project.namespace,
                                                     @project), status: 303
       end
+      # TODO: @oswaldo - Handle only JSON and HTML after deleting existing MR widget.
       format.js { render nothing: true, status: status[:return_code] }
+      format.json { render json: { message: status[:message] }, status: status[:return_code] }
     end
   end
 
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index cc67f688d51..a6d442004ea 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -36,7 +36,7 @@ class Projects::CommitController < Projects::ApplicationController
       format.html
       format.json do
         render json: PipelineSerializer
-          .new(project: @project, user: @current_user)
+          .new(project: @project, current_user: @current_user)
           .represent(@pipelines)
       end
     end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 9621b30b251..b6503af42ff 100755
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -74,10 +74,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 
   def show
     respond_to do |format|
-      format.html { define_discussion_vars }
+      format.html do
+        define_discussion_vars
+      end
 
       format.json do
-        render json: MergeRequestSerializer.new.represent(@merge_request)
+        render json: @merge_request_json
       end
 
       format.patch  do
@@ -233,7 +235,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 
       format.json do
         render json: PipelineSerializer
-          .new(project: @project, user: @current_user)
+          .new(project: @project, current_user: @current_user)
           .represent(@pipelines)
       end
     end
@@ -247,7 +249,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 
         render json: {
           pipelines: PipelineSerializer
-          .new(project: @project, user: @current_user)
+          .new(project: @project, current_user: @current_user)
           .represent(@pipelines)
         }
       end
@@ -316,17 +318,37 @@ class Projects::MergeRequestsController < Projects::ApplicationController
   end
 
   def remove_wip
-    MergeRequests::UpdateService.new(project, current_user, wip_event: 'unwip').execute(@merge_request)
+    @merge_request = MergeRequests::UpdateService
+      .new(project, current_user, wip_event: 'unwip')
+      .execute(@merge_request)
 
-    redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
-      notice: "The merge request can now be merged."
+    # TODO: @oswaldo - Handle only JSON after deleting existing MR widget.
+    respond_to do |format|
+      format.json do
+        render json: serializer.represent(@merge_request).to_json
+      end
+
+      format.html do
+        redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
+          notice: "The merge request can now be merged."
+
+      end
+    end
   end
 
   def merge_check
     @merge_request.check_if_can_be_merged
     @pipelines = @merge_request.all_pipelines
 
-    render partial: "projects/merge_requests/widget/show.html.haml", layout: false
+    respond_to do |format|
+      format.js do
+        render partial: "projects/merge_requests/widget/show.html.haml", layout: false
+      end
+
+      format.json do
+        render json: serializer.represent(@merge_request).to_json
+      end
+    end
   end
 
   def cancel_merge_when_pipeline_succeeds
@@ -337,48 +359,62 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     MergeRequests::MergeWhenPipelineSucceedsService
       .new(@project, current_user)
       .cancel(@merge_request)
+
+    # TODO: @oswaldo - Handle only JSON after deleting existing MR widget.
+    respond_to do |format|
+      format.json do
+        render json: serializer.represent(@merge_request.reload).to_json
+      end
+
+      format.js
+    end
   end
 
   def merge
     return access_denied! unless @merge_request.can_be_merged_by?(current_user)
 
+    @status = merge!
+
+    # TODO: @oswaldo - Handle only JSON after deleting existing MR widget.
+    respond_to do |format|
+      format.json { render json: { status: @status } }
+      format.js
+    end
+  end
+
+  def merge!
     # Disable the CI check if merge_when_pipeline_succeeds is enabled since we have
     # to wait until CI completes to know
     unless @merge_request.mergeable?(skip_ci_check: merge_when_pipeline_succeeds_active?)
-      @status = :failed
-      return
+      return :failed
     end
 
-    if params[:sha] != @merge_request.diff_head_sha
-      @status = :sha_mismatch
-      return
-    end
+    return :sha_mismatch if params[:sha] != @merge_request.diff_head_sha
 
     @merge_request.update(merge_error: nil)
 
     if params[:merge_when_pipeline_succeeds].present?
-      unless @merge_request.head_pipeline
-        @status = :failed
-        return
-      end
+      return :failed unless @merge_request.head_pipeline
 
       if @merge_request.head_pipeline.active?
         MergeRequests::MergeWhenPipelineSucceedsService
           .new(@project, current_user, merge_params)
           .execute(@merge_request)
 
-        @status = :merge_when_pipeline_succeeds
+        :merge_when_pipeline_succeeds
       elsif @merge_request.head_pipeline.success?
         # This can be triggered when a user clicks the auto merge button while
         # the tests finish at about the same time
         MergeWorker.perform_async(@merge_request.id, current_user.id, params)
-        @status = :success
+
+        :success
       else
-        @status = :failed
+        :failed
       end
     else
       MergeWorker.perform_async(@merge_request.id, current_user.id, params)
-      @status = :success
+
+      :success
     end
   end
 
@@ -445,6 +481,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
     end
   end
 
+  # TODO: @oswaldo - remove it when deleting old widget parts
   def ci_status
     pipeline = @merge_request.head_pipeline
     @pipelines = @merge_request.all_pipelines
@@ -560,6 +597,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
 
     labels
     define_pipelines_vars
+
+    @merge_request_json = serializer.represent(@merge_request).to_json
   end
 
   # Discussion tab data is rendered on html responses of actions
@@ -708,4 +747,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
       @merge_request.close
     end
   end
+
+  def serializer
+    if params[:basic]
+      MergeRequestBasicSerializer.new
+    else
+      MergeRequestSerializer.new(current_user: current_user, project: merge_request.project)
+    end
+  end
 end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 43a1abaa662..7b853a5d874 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -31,7 +31,7 @@ class Projects::PipelinesController < Projects::ApplicationController
       format.json do
         render json: {
           pipelines: PipelineSerializer
-            .new(project: @project, user: @current_user)
+            .new(project: @project, current_user: @current_user)
             .with_pagination(request, response)
             .represent(@pipelines),
           count: {
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index ec57fec4f99..d5b7dadb5e7 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -37,7 +37,7 @@ module IssuablesHelper
     when Issue
       IssueSerializer.new.represent(issuable).to_json
     when MergeRequest
-      MergeRequestSerializer.new.represent(issuable).to_json
+      @merge_request_json
     end
   end
 
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index 38be073c8dc..dfd0a4a6c96 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -55,6 +55,7 @@ module MergeRequestsHelper
     end
   end
 
+  # TODO: @oswaldo - Delete when removing old widget parts
   def issues_sentence(issues)
     # Sorting based on the `#123` or `group/project#123` reference will sort
     # local issues first.
@@ -63,10 +64,12 @@ module MergeRequestsHelper
     end.sort.to_sentence
   end
 
+  # TODO: @oswaldo - Delete when removing old widget parts
   def mr_closes_issues
     @mr_closes_issues ||= @merge_request.closes_issues(current_user)
   end
 
+  # TODO: @oswaldo - Delete when removing old widget parts
   def mr_issues_mentioned_but_not_closing
     @mr_issues_mentioned_but_not_closing ||= @merge_request.issues_mentioned_but_not_closing(current_user)
   end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 5ff83944d8c..aeeb14fab92 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -443,7 +443,8 @@ class MergeRequest < ActiveRecord::Base
   end
 
   def can_remove_source_branch?(current_user)
-    !source_project.protected_branch?(source_branch) &&
+    source_project &&
+      !source_project.protected_branch?(source_branch) &&
       !source_project.root_ref?(source_branch) &&
       Ability.allowed?(current_user, :push_code, source_project) &&
       diff_head_commit == source_branch_head
@@ -891,6 +892,8 @@ class MergeRequest < ActiveRecord::Base
   end
 
   def conflicts_can_be_resolved_by?(user)
+    return false unless source_project
+
     access = ::Gitlab::UserAccess.new(user, project: source_project)
     access.can_push_to_branch?(source_branch)
   end
diff --git a/app/serializers/event_entity.rb b/app/serializers/event_entity.rb
new file mode 100644
index 00000000000..935d67a4f37
--- /dev/null
+++ b/app/serializers/event_entity.rb
@@ -0,0 +1,4 @@
+class EventEntity < Grape::Entity
+  expose :author, using: UserEntity
+  expose :updated_at
+end
diff --git a/app/serializers/merge_request_basic_entity.rb b/app/serializers/merge_request_basic_entity.rb
new file mode 100644
index 00000000000..89f2af395b9
--- /dev/null
+++ b/app/serializers/merge_request_basic_entity.rb
@@ -0,0 +1,3 @@
+class MergeRequestBasicEntity < Grape::Entity
+  expose :merge_status
+end
diff --git a/app/serializers/merge_request_basic_serializer.rb b/app/serializers/merge_request_basic_serializer.rb
new file mode 100644
index 00000000000..ad880270d50
--- /dev/null
+++ b/app/serializers/merge_request_basic_serializer.rb
@@ -0,0 +1,4 @@
+class MergeRequestBasicSerializer < BaseSerializer
+  entity MergeRequestBasicEntity
+end
+
diff --git a/app/serializers/merge_request_entity.rb b/app/serializers/merge_request_entity.rb
index 5f80ab397a9..a184c215b2d 100644
--- a/app/serializers/merge_request_entity.rb
+++ b/app/serializers/merge_request_entity.rb
@@ -1,4 +1,7 @@
 class MergeRequestEntity < IssuableEntity
+  include RequestAwareEntity
+  include GitlabMarkdownHelper
+
   expose :in_progress_merge_commit_sha
   expose :locked_at
   expose :merge_commit_sha
@@ -11,4 +14,215 @@ class MergeRequestEntity < IssuableEntity
   expose :source_project_id
   expose :target_branch
   expose :target_project_id
+
+  # Events
+  expose :merge_event, using: EventEntity
+  expose :closed_event, using: EventEntity
+
+  # User entities
+  expose :author, using: UserEntity
+  expose :merge_user, using: UserEntity
+
+  # Diff sha's
+  expose :diff_head_sha
+  expose :diff_head_commit_short_id do |merge_request|
+    merge_request.diff_head_commit.try(:short_id)
+  end
+
+  expose :merge_commit_sha
+  expose :merge_commit_message
+  expose :head_pipeline, with: PipelineEntity, as: :pipeline
+
+  # Booleans
+  expose :work_in_progress?, as: :work_in_progress
+  expose :source_branch_exists?, as: :source_branch_exists
+  expose :mergeable_discussions_state?, as: :mergeable_discussions_state
+  expose :conflicts_can_be_resolved_in_ui?, as: :conflicts_can_be_resolved_in_ui
+  expose :branch_missing?, as: :branch_missing
+  expose :has_no_commits?, as: :has_no_commits
+  expose :can_be_cherry_picked?, as: :can_be_cherry_picked
+  expose :cannot_be_merged?, as: :has_conflicts
+  expose :can_be_merged?, as: :can_be_merged
+
+  # CI related
+  expose :has_ci?, as: :has_ci
+  expose :ci_status do |merge_request|
+    pipeline = merge_request.head_pipeline
+
+    if pipeline
+      status = pipeline.status
+      status = "success_with_warnings" if pipeline.success? && pipeline.has_warnings?
+
+      status || "preparing"
+    else
+      ci_service = merge_request.source_project.try(:ci_service)
+      ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service
+    end
+  end
+
+  expose :issues_links do
+    expose :closing do |merge_request|
+      closes_issues = merge_request.closes_issues(current_user)
+
+      markdown issues_sentence(merge_request.project, closes_issues),
+        pipeline: :gfm,
+        author: merge_request.author,
+        project: merge_request.project
+    end
+
+    expose :mentioned_but_not_closing do |merge_request|
+      mentioned_but_not_closing_issues = merge_request
+        .issues_mentioned_but_not_closing(current_user)
+
+      markdown issues_sentence(merge_request.project, mentioned_but_not_closing_issues),
+               pipeline: :gfm,
+               author: merge_request.author,
+               project: merge_request.project
+    end
+  end
+
+  expose :current_user do
+    expose :can_create_issue do |merge_request|
+      merge_request.project.issues_enabled? &&
+        can?(request.current_user, :create_issue, merge_request.project)
+    end
+
+    expose :can_update_merge_request do |merge_request|
+      merge_request.project.merge_requests_enabled? &&
+        can?(request.current_user, :update_merge_request, merge_request.project)
+    end
+
+    expose :can_resolve_conflicts do |merge_request|
+      merge_request.conflicts_can_be_resolved_by?(request.current_user)
+    end
+
+    expose :can_remove_source_branch do |merge_request|
+      merge_request.can_remove_source_branch?(request.current_user)
+    end
+
+    expose :can_merge do |merge_request|
+      merge_request.can_be_merged_by?(request.current_user)
+    end
+
+    expose :can_merge_via_cli do |merge_request|
+      merge_request.can_be_merged_via_command_line_by?(request.current_user)
+    end
+
+    expose :can_revert do |merge_request|
+      merge_request.can_be_reverted?(request.current_user)
+    end
+
+    expose :can_cancel_automatic_merge do |merge_request|
+      merge_request.can_cancel_merge_when_pipeline_succeeds?(request.current_user)
+    end
+  end
+
+  expose :target_branch_path do |merge_request|
+    namespace_project_branch_path(merge_request.target_project.namespace,
+                                  merge_request.target_project,
+                                  merge_request.target_branch)
+  end
+
+  expose :source_branch_path do |merge_request|
+    namespace_project_branch_path(merge_request.source_project.namespace,
+                                  merge_request.source_project,
+                                  merge_request.source_branch)
+  end
+
+  expose :project_archived do |merge_request|
+    merge_request.project.archived?
+  end
+
+  expose :conflict_resolution_ui_path do |merge_request|
+    conflicts_namespace_project_merge_request_path(merge_request.project.namespace,
+                                                   merge_request.project,
+                                                   merge_request)
+  end
+
+  expose :remove_wip_path do |merge_request|
+    remove_wip_namespace_project_merge_request_path(merge_request.project.namespace,
+                                                    merge_request.project,
+                                                    merge_request)
+  end
+
+  expose :merge_path do |merge_request|
+    merge_namespace_project_merge_request_path(merge_request.project.namespace,
+                                               merge_request.project,
+                                               merge_request)
+  end
+
+  expose :cancel_merge_when_pipeline_succeeds_path do |merge_request|
+    cancel_merge_when_pipeline_succeeds_namespace_project_merge_request_path(
+      merge_request.target_project.namespace,
+      merge_request.target_project,
+      merge_request)
+  end
+
+  expose :merge_commit_message_with_description do |merge_request|
+    merge_request.merge_commit_message(include_description: true)
+  end
+
+  expose :diverged_commits_count do |merge_request|
+    merge_request.open? &&
+      merge_request.diverged_from_target_branch? ?
+        merge_request.diverged_commits_count : 0
+  end
+
+  expose :email_patches_path do |merge_request|
+    namespace_project_merge_request_path(merge_request.target_project.namespace,
+                                         merge_request.target_project,
+                                         merge_request,
+                                         format: :patch)
+  end
+
+  expose :plain_diff_path do |merge_request|
+    namespace_project_merge_request_path(merge_request.target_project.namespace,
+                                         merge_request.target_project,
+                                         merge_request,
+                                         format: :diff)
+  end
+
+  # FIXME: @oswaldo, please implement this
+  expose :status_path do |merge_request|
+    path = namespace_project_merge_request_path(merge_request.target_project.namespace,
+                                         merge_request.target_project,
+                                         merge_request,
+                                         format: :diff)
+    path.sub! 'diff', 'json'
+  end
+
+  # TODO: @oswaldo, please verify this
+  expose :merge_check_path do |merge_request|
+    merge_check_namespace_project_merge_request_path(merge_request.target_project.namespace,
+                                         merge_request.target_project,
+                                         merge_request)
+  end
+
+  expose :only_allow_merge_if_pipeline_succeeds do |merge_request|
+    merge_request.project.only_allow_merge_if_pipeline_succeeds?
+  end
+
+  expose :create_issue_to_resolve_discussions_path do |merge_request|
+    new_namespace_project_issue_path(merge_request.project.namespace,
+                                     merge_request.project,
+                                     merge_request_for_resolving_discussions_of: merge_request.iid)
+  end
+
+  expose :ci_environments_status_url do |merge_request|
+    ci_environments_status_namespace_project_merge_request_path(merge_request.project.namespace,
+                                                                merge_request.project,
+                                                                merge_request)
+  end
+
+  private
+
+  delegate :current_user, to: :request
+
+  def issues_sentence(project, issues)
+    # Sorting based on the `#123` or `group/project#123` reference will sort
+    # local issues first.
+    issues.map do |issue|
+      issue.to_reference(project)
+    end.sort.to_sentence
+  end
 end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index 3f16dd66d54..27d37466d82 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -3,6 +3,8 @@ class PipelineEntity < Grape::Entity
 
   expose :id
   expose :user, using: UserEntity
+  expose :active?, as: :active
+  expose :coverage
 
   expose :path do |pipeline|
     namespace_project_pipeline_path(
@@ -70,15 +72,15 @@ class PipelineEntity < Grape::Entity
 
   def can_retry?
     pipeline.retryable? &&
-      can?(request.user, :update_pipeline, pipeline)
+      can?(request.current_user, :update_pipeline, pipeline)
   end
 
   def can_cancel?
     pipeline.cancelable? &&
-      can?(request.user, :update_pipeline, pipeline)
+      can?(request.current_user, :update_pipeline, pipeline)
   end
 
   def detailed_status
-    pipeline.detailed_status(request.user)
+    pipeline.detailed_status(request.current_user)
   end
 end
diff --git a/app/serializers/stage_entity.rb b/app/serializers/stage_entity.rb
index 7a047bdc712..157ab1776a7 100644
--- a/app/serializers/stage_entity.rb
+++ b/app/serializers/stage_entity.rb
@@ -33,6 +33,6 @@ class StageEntity < Grape::Entity
   alias_method :stage, :object
 
   def detailed_status
-    stage.detailed_status(request.user)
+    stage.detailed_status(request.current_user)
   end
 end
diff --git a/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml b/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml
index eab5be488b5..1f803f4001a 100644
--- a/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml
+++ b/app/views/projects/merge_requests/cancel_merge_when_pipeline_succeeds.js.haml
@@ -1,2 +1,2 @@
 :plain
-  $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/accept'))}");
+  $('.mr-widget-body:eq(0)').html("#{escape_javascript(render('projects/merge_requests/widget/open/accept'))}");
diff --git a/app/views/projects/merge_requests/merge.js.haml b/app/views/projects/merge_requests/merge.js.haml
index e632fc681cf..3d63cd26c19 100644
--- a/app/views/projects/merge_requests/merge.js.haml
+++ b/app/views/projects/merge_requests/merge.js.haml
@@ -5,10 +5,10 @@
     merge_request_widget.mergeInProgress(#{remove_source_branch});
 - when :merge_when_pipeline_succeeds
   :plain
-    $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}");
+    $('.mr-widget-body:eq(0)').html("#{escape_javascript(render('projects/merge_requests/widget/open/merge_when_pipeline_succeeds'))}");
 - when :sha_mismatch
   :plain
-    $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/sha_mismatch'))}");
+    $('.mr-widget-body:eq(0)').html("#{escape_javascript(render('projects/merge_requests/widget/open/sha_mismatch'))}");
 - else
   :plain
-    $('.mr-widget-body').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}");
+    $('.mr-widget-body:eq(0)').html("#{escape_javascript(render('projects/merge_requests/widget/open/reload'))}");
diff --git a/app/views/projects/merge_requests/widget/_show.html.haml b/app/views/projects/merge_requests/widget/_show.html.haml
index 0b0fb7854c2..ce6759945b8 100644
--- a/app/views/projects/merge_requests/widget/_show.html.haml
+++ b/app/views/projects/merge_requests/widget/_show.html.haml
@@ -7,6 +7,9 @@
 - elsif @merge_request.locked?
   = render 'projects/merge_requests/widget/locked'
 
+:javascript
+  window.gl.mrWidgetData = #{@merge_request_json}
+
 :javascript
   var opts = {
     merge_check_url: "#{merge_check_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)}",
@@ -37,3 +40,8 @@
   }
 
   merge_request_widget = new window.gl.MergeRequestWidget(opts);
+
+#js-vue-mr-widget.mr-widget
+
+- content_for :page_specific_javascripts do
+  = page_specific_javascript_bundle_tag('vue_merge_request_widget')
diff --git a/app/views/shared/issuable/form/_merge_params.html.haml b/app/views/shared/issuable/form/_merge_params.html.haml
index 03309722326..d23f79be2be 100644
--- a/app/views/shared/issuable/form/_merge_params.html.haml
+++ b/app/views/shared/issuable/form/_merge_params.html.haml
@@ -5,12 +5,3 @@
 
 -# This check is duplicated below, to avoid conflicts with EE.
 - return unless issuable.can_remove_source_branch?(current_user)
-
-.form-group
-  .col-sm-10.col-sm-offset-2
-    - if issuable.can_remove_source_branch?(current_user)
-      .checkbox
-        = label_tag 'merge_request[force_remove_source_branch]' do
-          = hidden_field_tag 'merge_request[force_remove_source_branch]', '0', id: nil
-          = check_box_tag 'merge_request[force_remove_source_branch]', '1', issuable.force_remove_source_branch?
-          Remove source branch when merge request is accepted.
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 70d98b022c1..1f4250014f6 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -45,6 +45,7 @@ var config = {
     u2f:                  ['vendor/u2f'],
     users:                './users/users_bundle.js',
     vue_pipelines:        './vue_pipelines_index/index.js',
+    vue_merge_request_widget: './vue_merge_request_widget/index.js',
   },
 
   output: {
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index d20e7368086..23f55f05d15 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -177,33 +177,98 @@ describe Projects::BranchesController do
       sign_in(user)
 
       post :destroy,
-           format: :js,
-           id: branch,
-           namespace_id: project.namespace,
-           project_id: project
+        format: format,
+        id: branch,
+        namespace_id: project.namespace,
+        project_id: project
     end
 
-    context "valid branch name, valid source" do
+    context 'as JS' do
       let(:branch) { "feature" }
+      let(:format) { :js }
 
-      it { expect(response).to have_http_status(200) }
-    end
+      context "valid branch name, valid source" do
+        let(:branch) { "feature" }
+
+        it { expect(response).to have_http_status(200) }
+        it { expect(response.body).to be_blank }
+      end
+
+      context "valid branch name with unencoded slashes" do
+        let(:branch) { "improve/awesome" }
+
+        it { expect(response).to have_http_status(200) }
+        it { expect(response.body).to be_blank }
+      end
+
+      context "valid branch name with encoded slashes" do
+        let(:branch) { "improve%2Fawesome" }
 
-    context "valid branch name with unencoded slashes" do
-      let(:branch) { "improve/awesome" }
+        it { expect(response).to have_http_status(200) }
+        it { expect(response.body).to be_blank }
+      end
 
-      it { expect(response).to have_http_status(200) }
+      context "invalid branch name, valid ref" do
+        let(:branch) { "no-branch" }
+
+        it { expect(response).to have_http_status(404) }
+        it { expect(response.body).to be_blank }
+      end
     end
 
-    context "valid branch name with encoded slashes" do
-      let(:branch) { "improve%2Fawesome" }
+    context 'as JSON' do
+      let(:branch) { "feature" }
+      let(:format) { :json }
+
+      context 'valid branch name, valid source' do
+        let(:branch) { "feature" }
 
-      it { expect(response).to have_http_status(200) }
+        it 'returns JSON response with message' do
+          expect(json_response).to eql("message" => 'Branch was removed')
+        end
+
+        it { expect(response).to have_http_status(200) }
+      end
+
+      context 'valid branch name with unencoded slashes' do
+        let(:branch) { "improve/awesome" }
+
+        it 'returns JSON response with message' do
+          expect(json_response).to eql('message' => 'Branch was removed')
+        end
+
+        it { expect(response).to have_http_status(200) }
+      end
+
+      context "valid branch name with encoded slashes" do
+        let(:branch) { 'improve%2Fawesome' }
+
+        it 'returns JSON response with message' do
+          expect(json_response).to eql('message' => 'Branch was removed')
+        end
+
+        it { expect(response).to have_http_status(200) }
+      end
+
+      context 'invalid branch name, valid ref' do
+        let(:branch) { 'no-branch' }
+
+        it 'returns JSON response with message' do
+          expect(json_response).to eql('message' => 'No such branch')
+        end
+
+        it { expect(response).to have_http_status(404) }
+      end
     end
-    context "invalid branch name, valid ref" do
-      let(:branch) { "no-branch" }
 
-      it { expect(response).to have_http_status(404) }
+    context 'as HTML' do
+      let(:branch) { "feature" }
+      let(:format) { :html }
+
+      it 'redirects to branches path' do
+        expect(response)
+          .to redirect_to(namespace_project_branches_path(project.namespace, project))
+      end
     end
   end
 
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 72f41f7209a..a9d5705ba2a 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -73,63 +73,51 @@ describe Projects::MergeRequestsController do
   end
 
   describe "GET show" do
-    shared_examples "export merge as" do |format|
-      it "does generally work" do
-        get(:show,
-            namespace_id: project.namespace.to_param,
-            project_id: project,
-            id: merge_request.iid,
-            format: format)
+    def go(extra_params = {})
+      params = {
+        namespace_id: project.namespace.to_param,
+        project_id: project,
+        id: merge_request.iid
+      }
 
-        expect(response).to be_success
-      end
+      get :show, params.merge(extra_params)
+    end
 
-      it_behaves_like "loads labels", :show
+    it_behaves_like "loads labels", :show
 
-      it "generates it" do
-        expect_any_instance_of(MergeRequest).to receive(:"to_#{format}")
+    describe 'as html' do
+      it "renders merge request page" do
+        go(format: :html)
 
-        get(:show,
-            namespace_id: project.namespace.to_param,
-            project_id: project,
-            id: merge_request.iid,
-            format: format)
+        expect(response).to be_success
       end
+    end
 
-      it "renders it" do
-        get(:show,
-            namespace_id: project.namespace.to_param,
-            project_id: project,
-            id: merge_request.iid,
-            format: format)
+    describe 'as json' do
+      context 'with basic param' do
+        it 'renders basic MR entity as json' do
+          go(basic: true, format: :json)
 
-        expect(response.body).to eq(merge_request.send(:"to_#{format}").to_s)
+          expect(json_response)
+            .to eql(MergeRequestBasicSerializer.new.represent(merge_request).as_json)
+        end
       end
 
-      it "does not escape Html" do
-        allow_any_instance_of(MergeRequest).to receive(:"to_#{format}").
-          and_return('HTML entities &<>" ')
-
-        get(:show,
-            namespace_id: project.namespace.to_param,
-            project_id: project,
-            id: merge_request.iid,
-            format: format)
+      context 'without basic param' do
+        it 'renders the merge request in the json format' do
+          go(format: :json)
 
-        expect(response.body).not_to include('&amp;')
-        expect(response.body).not_to include('&gt;')
-        expect(response.body).not_to include('&lt;')
-        expect(response.body).not_to include('&quot;')
+          expect(json_response).to eql(
+            MergeRequestSerializer
+              .new(current_user: user, project: project)
+              .represent(merge_request).as_json)
+        end
       end
     end
 
     describe "as diff" do
       it "triggers workhorse to serve the request" do
-        get(:show,
-            namespace_id: project.namespace.to_param,
-            project_id: project,
-            id: merge_request.iid,
-            format: :diff)
+        go(format: :diff)
 
         expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-diff:")
       end
@@ -137,11 +125,7 @@ describe Projects::MergeRequestsController do
 
     describe "as patch" do
       it 'triggers workhorse to serve the request' do
-        get(:show,
-            namespace_id: project.namespace.to_param,
-            project_id: project,
-            id: merge_request.iid,
-            format: :patch)
+        go(format: :patch)
 
         expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("git-format-patch:")
       end
@@ -285,88 +269,229 @@ describe Projects::MergeRequestsController do
         namespace_id: project.namespace,
         project_id: project,
         id: merge_request.iid,
-        format: 'raw'
+        format: format
       }
     end
 
-    context 'when the user does not have access' do
-      before do
-        project.team.truncate
-        project.team << [user, :reporter]
-        post :merge, base_params
-      end
+    context 'as JSON' do
+      let(:format) { 'json' }
+
+      context 'when the user does not have access' do
+        before do
+          project.team.truncate
+          project.team << [user, :reporter]
+          xhr :post, :merge, base_params
+        end
 
-      it 'returns not found' do
-        expect(response).to be_not_found
+        it 'returns access denied' do
+          expect(response).to have_http_status(401)
+        end
       end
-    end
 
-    context 'when the merge request is not mergeable' do
-      before do
-        merge_request.update_attributes(title: "WIP: #{merge_request.title}")
+      context 'when the merge request is not mergeable' do
+        before do
+          merge_request.update_attributes(title: "WIP: #{merge_request.title}")
+
+          post :merge, base_params
+        end
 
-        post :merge, base_params
+        it 'returns :failed' do
+          expect(json_response).to eq('status' => 'failed')
+        end
       end
 
-      it 'returns :failed' do
-        expect(assigns(:status)).to eq(:failed)
+      context 'when the sha parameter does not match the source SHA' do
+        before { post :merge, base_params.merge(sha: 'foo') }
+
+        it 'returns :sha_mismatch' do
+          expect(json_response).to eq('status' => 'sha_mismatch')
+        end
       end
-    end
 
-    context 'when the sha parameter does not match the source SHA' do
-      before { post :merge, base_params.merge(sha: 'foo') }
+      context 'when the sha parameter matches the source SHA' do
+        def merge_with_sha
+          post :merge, base_params.merge(sha: merge_request.diff_head_sha)
+        end
+
+        it 'returns :success' do
+          merge_with_sha
+
+          expect(json_response).to eq('status' => 'success')
+        end
 
-      it 'returns :sha_mismatch' do
-        expect(assigns(:status)).to eq(:sha_mismatch)
+        it 'starts the merge immediately' do
+          expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything)
+
+          merge_with_sha
+        end
+
+        context 'when the pipeline succeeds is passed' do
+          def merge_when_pipeline_succeeds
+            post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
+          end
+
+          before do
+            create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
+          end
+
+          it 'returns :merge_when_pipeline_succeeds' do
+            merge_when_pipeline_succeeds
+
+            expect(json_response).to eq('status' => 'merge_when_pipeline_succeeds')
+          end
+
+          it 'sets the MR to merge when the pipeline succeeds' do
+            service = double(:merge_when_pipeline_succeeds_service)
+
+            expect(MergeRequests::MergeWhenPipelineSucceedsService)
+              .to receive(:new).with(project, anything, anything)
+              .and_return(service)
+            expect(service).to receive(:execute).with(merge_request)
+
+            merge_when_pipeline_succeeds
+          end
+
+          context 'when project.only_allow_merge_if_pipeline_succeeds? is true' do
+            before do
+              project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
+            end
+
+            it 'returns :merge_when_pipeline_succeeds' do
+              merge_when_pipeline_succeeds
+
+              expect(json_response).to eq('status' => 'merge_when_pipeline_succeeds')
+            end
+          end
+        end
+
+        describe 'only_allow_merge_if_all_discussions_are_resolved? setting' do
+          let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
+
+          context 'when enabled' do
+            before do
+              project.update_column(:only_allow_merge_if_all_discussions_are_resolved, true)
+            end
+
+            context 'with unresolved discussion' do
+              before do
+                expect(merge_request).not_to be_discussions_resolved
+              end
+
+              it 'returns :failed' do
+                merge_with_sha
+
+                expect(json_response).to eq('status' => 'failed')
+              end
+            end
+
+            context 'with all discussions resolved' do
+              before do
+                merge_request.discussions.each { |d| d.resolve!(user) }
+                expect(merge_request).to be_discussions_resolved
+              end
+
+              it 'returns :success' do
+                merge_with_sha
+
+                expect(json_response).to eq('status' => 'success')
+              end
+            end
+          end
+
+          context 'when disabled' do
+            before do
+              project.update_column(:only_allow_merge_if_all_discussions_are_resolved, false)
+            end
+
+            context 'with unresolved discussion' do
+              before do
+                expect(merge_request).not_to be_discussions_resolved
+              end
+
+              it 'returns :success' do
+                merge_with_sha
+
+                expect(json_response).to eq('status' => 'success')
+              end
+            end
+
+            context 'with all discussions resolved' do
+              before do
+                merge_request.discussions.each { |d| d.resolve!(user) }
+                expect(merge_request).to be_discussions_resolved
+              end
+
+              it 'returns :success' do
+                merge_with_sha
+
+                expect(json_response).to eq('status' => 'success')
+              end
+            end
+          end
+        end
       end
     end
 
-    context 'when the sha parameter matches the source SHA' do
-      def merge_with_sha
-        post :merge, base_params.merge(sha: merge_request.diff_head_sha)
-      end
+    # TODO: Delete when removing old widget parts
+    context 'as any other format' do
+      let(:format) { 'js' }
 
-      it 'returns :success' do
-        merge_with_sha
+      context 'when the user does not have access' do
+        before do
+          project.team.truncate
+          project.team << [user, :reporter]
+          post :merge, base_params
+        end
 
-        expect(assigns(:status)).to eq(:success)
+        it 'returns not found' do
+          expect(response).to be_not_found
+        end
       end
 
-      it 'starts the merge immediately' do
-        expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything)
+      context 'when the merge request is not mergeable' do
+        before do
+          merge_request.update_attributes(title: "WIP: #{merge_request.title}")
+
+          post :merge, base_params
+        end
 
-        merge_with_sha
+        it 'returns :failed' do
+          expect(assigns(:status)).to eq(:failed)
+        end
       end
 
-      context 'when the pipeline succeeds is passed' do
-        def merge_when_pipeline_succeeds
-          post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
+      context 'when the sha parameter does not match the source SHA' do
+        before { post :merge, base_params.merge(sha: 'foo') }
+
+        it 'returns :sha_mismatch' do
+          expect(assigns(:status)).to eq(:sha_mismatch)
         end
+      end
 
-        before do
-          create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
+      context 'when the sha parameter matches the source SHA' do
+        def merge_with_sha
+          post :merge, base_params.merge(sha: merge_request.diff_head_sha)
         end
 
-        it 'returns :merge_when_pipeline_succeeds' do
-          merge_when_pipeline_succeeds
+        it 'returns :success' do
+          merge_with_sha
 
-          expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
+          expect(assigns(:status)).to eq(:success)
         end
 
-        it 'sets the MR to merge when the pipeline succeeds' do
-          service = double(:merge_when_pipeline_succeeds_service)
-
-          expect(MergeRequests::MergeWhenPipelineSucceedsService)
-            .to receive(:new).with(project, anything, anything)
-            .and_return(service)
-          expect(service).to receive(:execute).with(merge_request)
+        it 'starts the merge immediately' do
+          expect(MergeWorker).to receive(:perform_async).with(merge_request.id, anything, anything)
 
-          merge_when_pipeline_succeeds
+          merge_with_sha
         end
 
-        context 'when project.only_allow_merge_if_pipeline_succeeds? is true' do
+        context 'when the pipeline succeeds is passed' do
+          def merge_when_pipeline_succeeds
+            post :merge, base_params.merge(sha: merge_request.diff_head_sha, merge_when_pipeline_succeeds: '1')
+          end
+
           before do
-            project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
+            create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch)
           end
 
           it 'returns :merge_when_pipeline_succeeds' do
@@ -374,70 +499,93 @@ describe Projects::MergeRequestsController do
 
             expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
           end
-        end
-      end
 
-      describe 'only_allow_merge_if_all_discussions_are_resolved? setting' do
-        let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
+          it 'sets the MR to merge when the pipeline succeeds' do
+            service = double(:merge_when_pipeline_succeeds_service)
 
-        context 'when enabled' do
-          before do
-            project.update_column(:only_allow_merge_if_all_discussions_are_resolved, true)
+            expect(MergeRequests::MergeWhenPipelineSucceedsService)
+              .to receive(:new).with(project, anything, anything)
+              .and_return(service)
+            expect(service).to receive(:execute).with(merge_request)
+
+            merge_when_pipeline_succeeds
           end
 
-          context 'with unresolved discussion' do
+          context 'when project.only_allow_merge_if_pipeline_succeeds? is true' do
             before do
-              expect(merge_request).not_to be_discussions_resolved
+              project.update_column(:only_allow_merge_if_pipeline_succeeds, true)
             end
 
-            it 'returns :failed' do
-              merge_with_sha
+            it 'returns :merge_when_pipeline_succeeds' do
+              merge_when_pipeline_succeeds
 
-              expect(assigns(:status)).to eq(:failed)
+              expect(assigns(:status)).to eq(:merge_when_pipeline_succeeds)
             end
           end
+        end
+
+        describe 'only_allow_merge_if_all_discussions_are_resolved? setting' do
+          let(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) }
 
-          context 'with all discussions resolved' do
+          context 'when enabled' do
             before do
-              merge_request.discussions.each { |d| d.resolve!(user) }
-              expect(merge_request).to be_discussions_resolved
+              project.update_column(:only_allow_merge_if_all_discussions_are_resolved, true)
             end
 
-            it 'returns :success' do
-              merge_with_sha
+            context 'with unresolved discussion' do
+              before do
+                expect(merge_request).not_to be_discussions_resolved
+              end
 
-              expect(assigns(:status)).to eq(:success)
+              it 'returns :failed' do
+                merge_with_sha
+
+                expect(assigns(:status)).to eq(:failed)
+              end
             end
-          end
-        end
 
-        context 'when disabled' do
-          before do
-            project.update_column(:only_allow_merge_if_all_discussions_are_resolved, false)
+            context 'with all discussions resolved' do
+              before do
+                merge_request.discussions.each { |d| d.resolve!(user) }
+                expect(merge_request).to be_discussions_resolved
+              end
+
+              it 'returns :success' do
+                merge_with_sha
+
+                expect(assigns(:status)).to eq(:success)
+              end
+            end
           end
 
-          context 'with unresolved discussion' do
+          context 'when disabled' do
             before do
-              expect(merge_request).not_to be_discussions_resolved
+              project.update_column(:only_allow_merge_if_all_discussions_are_resolved, false)
             end
 
-            it 'returns :success' do
-              merge_with_sha
+            context 'with unresolved discussion' do
+              before do
+                expect(merge_request).not_to be_discussions_resolved
+              end
 
-              expect(assigns(:status)).to eq(:success)
-            end
-          end
+              it 'returns :success' do
+                merge_with_sha
 
-          context 'with all discussions resolved' do
-            before do
-              merge_request.discussions.each { |d| d.resolve!(user) }
-              expect(merge_request).to be_discussions_resolved
+                expect(assigns(:status)).to eq(:success)
+              end
             end
 
-            it 'returns :success' do
-              merge_with_sha
+            context 'with all discussions resolved' do
+              before do
+                merge_request.discussions.each { |d| d.resolve!(user) }
+                expect(merge_request).to be_discussions_resolved
+              end
+
+              it 'returns :success' do
+                merge_with_sha
 
-              expect(assigns(:status)).to eq(:success)
+                expect(assigns(:status)).to eq(:success)
+              end
             end
           end
         end
@@ -822,16 +970,102 @@ describe Projects::MergeRequestsController do
   end
 
   context 'POST remove_wip' do
-    it 'removes the wip status' do
+    before do
       merge_request.title = merge_request.wip_title
       merge_request.save
+    end
 
-      post :remove_wip,
-           namespace_id: merge_request.project.namespace.to_param,
-           project_id: merge_request.project,
-           id: merge_request.iid
+    context 'as HTML' do
+      before do
+        post :remove_wip,
+          namespace_id: merge_request.project.namespace.to_param,
+          project_id: merge_request.project,
+          id: merge_request.iid
+      end
+
+      it 'removes the wip status' do
+        expect(merge_request.reload.title).to eq(merge_request.wipless_title)
+      end
+
+      it 'redirect to merge request show page' do
+        expect(response).to redirect_to(
+          namespace_project_merge_request_path(merge_request.project.namespace,
+                                               merge_request.project,
+                                               merge_request))
+      end
+    end
+
+    context 'as JSON' do
+      before do
+        xhr :post, :remove_wip,
+          namespace_id: merge_request.project.namespace.to_param,
+          project_id: merge_request.project,
+          id: merge_request.iid,
+          format: :json
+      end
+
+      it 'removes the wip status' do
+        expect(merge_request.reload.title).to eq(merge_request.wipless_title)
+      end
+
+      it 'renders MergeRequest as JSON' do
+        expect(json_response.keys).to include('id', 'iid', 'description')
+      end
+    end
+  end
 
-      expect(merge_request.reload.title).to eq(merge_request.wipless_title)
+  describe 'POST cancel_merge_when_pipeline_succeeds' do
+    context 'as JS' do
+      subject do
+        xhr :post, :cancel_merge_when_pipeline_succeeds,
+          namespace_id: merge_request.project.namespace.to_param,
+          project_id: merge_request.project,
+          id: merge_request.iid
+      end
+
+      it 'calls MergeRequests::MergeWhenPipelineSucceedsService' do
+        mwps_service = double
+
+        allow(MergeRequests::MergeWhenPipelineSucceedsService)
+          .to receive(:new)
+          .and_return(mwps_service)
+
+        expect(mwps_service).to receive(:cancel).with(merge_request)
+
+        subject
+      end
+
+      it { is_expected.to render_template('cancel_merge_when_pipeline_succeeds') }
+    end
+
+    context 'as JSON' do
+      subject do
+        xhr :post, :cancel_merge_when_pipeline_succeeds,
+          namespace_id: merge_request.project.namespace.to_param,
+          project_id: merge_request.project,
+          id: merge_request.iid,
+          format: :json
+      end
+
+      it 'calls MergeRequests::MergeWhenPipelineSucceedsService' do
+        mwps_service = double
+
+        allow(MergeRequests::MergeWhenPipelineSucceedsService)
+          .to receive(:new)
+          .and_return(mwps_service)
+
+        expect(mwps_service).to receive(:cancel).with(merge_request)
+
+        subject
+      end
+
+      it { is_expected.to have_http_status(:success) }
+
+      it 'renders MergeRequest as JSON' do
+        subject
+
+        expect(json_response.keys).to include('id', 'iid', 'description')
+      end
     end
   end
 
diff --git a/spec/javascripts/commit/pipelines/mock_data.js b/spec/javascripts/commit/pipelines/mock_data.js
index 82b00b4c1ec..10a60620f49 100644
--- a/spec/javascripts/commit/pipelines/mock_data.js
+++ b/spec/javascripts/commit/pipelines/mock_data.js
@@ -61,6 +61,7 @@ export default {
     tag: false,
     branch: true,
   },
+  coverage: '42.21',
   commit: {
     id: 'fbd79f04fa98717641deaaeb092a4d417237c2e4',
     short_id: 'fbd79f04',
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
new file mode 100644
index 00000000000..6776c36c766
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author';
+
+const author = {
+  webUrl: 'http://foo.bar',
+  avatarUrl: 'http://gravatar.com/foo',
+  name: 'fatihacet',
+};
+const createComponent = () => {
+  const Component = Vue.extend(authorComponent);
+
+  return new Component({
+    el: document.createElement('div'),
+    propsData: { author },
+  });
+};
+
+describe('MRWidgetAuthor', () => {
+  describe('props', () => {
+    it('should have props', () => {
+      const authorProp = authorComponent.props.author;
+
+      expect(authorProp).toBeDefined();
+      expect(authorProp.type instanceof Object).toBeTruthy();
+      expect(authorProp.required).toBeTruthy();
+    });
+  });
+
+  describe('template', () => {
+    it('should have correct elements', () => {
+      const el = createComponent().$el;
+
+      expect(el.tagName).toEqual('A');
+      expect(el.getAttribute('href')).toEqual(author.webUrl);
+      expect(el.querySelector('img').getAttribute('src')).toEqual(author.avatarUrl);
+      expect(el.querySelector('.author').innerText).toEqual(author.name);
+    });
+  });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
new file mode 100644
index 00000000000..515ddcbb875
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
@@ -0,0 +1,61 @@
+import Vue from 'vue';
+import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time';
+
+const props = {
+  actionText: 'Merged by',
+  author: {
+    webUrl: 'http://foo.bar',
+    avatarUrl: 'http://gravatar.com/foo',
+    name: 'fatihacet',
+  },
+  dateTitle: '2017-03-23T23:02:00.807Z',
+  dateReadable: '12 hours ago',
+};
+const createComponent = () => {
+  const Component = Vue.extend(authorTimeComponent);
+
+  return new Component({
+    el: document.createElement('div'),
+    propsData: props,
+  });
+};
+
+describe('MRWidgetAuthorTime', () => {
+  describe('props', () => {
+    it('should have props', () => {
+      const { actionText, author, dateTitle, dateReadable } = authorTimeComponent.props;
+      const ActionTextClass = actionText.type;
+      const DateTitleClass = dateTitle.type;
+      const DateReadableClass = dateReadable.type;
+
+      expect(new ActionTextClass() instanceof String).toBeTruthy();
+      expect(actionText.required).toBeTruthy();
+
+      expect(author.type instanceof Object).toBeTruthy();
+      expect(author.required).toBeTruthy();
+
+      expect(new DateTitleClass() instanceof String).toBeTruthy();
+      expect(dateTitle.required).toBeTruthy();
+
+      expect(new DateReadableClass() instanceof String).toBeTruthy();
+      expect(dateReadable.required).toBeTruthy();
+    });
+  });
+
+  describe('components', () => {
+    it('should have components', () => {
+      expect(authorTimeComponent.components['mr-widget-author']).toBeDefined();
+    });
+  });
+
+  describe('template', () => {
+    it('should have correct elements', () => {
+      const el = createComponent().$el;
+
+      expect(el.tagName).toEqual('H4');
+      expect(el.querySelector('a').getAttribute('href')).toEqual(props.author.webUrl);
+      expect(el.querySelector('time').innerText).toContain(props.dateReadable);
+      expect(el.querySelector('time').getAttribute('title')).toEqual(props.dateTitle);
+    });
+  });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
new file mode 100644
index 00000000000..745b64c0d09
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
@@ -0,0 +1,139 @@
+import Vue from 'vue';
+import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment';
+import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons';
+
+const deploymentMockData = [
+  {
+    id: 15,
+    name: 'review/diplo',
+    url: '/root/acets-review-apps/environments/15',
+    stop_url: '/root/acets-review-apps/environments/15/stop',
+    external_url: 'http://diplo.',
+    external_url_formatted: 'diplo.',
+    deployed_at: '2017-03-22T22:44:42.258Z',
+    deployed_at_formatted: 'Mar 22, 2017 10:44pm',
+  },
+];
+const createComponent = () => {
+  const Component = Vue.extend(deploymentComponent);
+  const mr = {
+    deployments: deploymentMockData,
+  };
+
+  return new Component({
+    el: document.createElement('div'),
+    propsData: { mr },
+  });
+};
+
+describe('MRWidgetDeployment', () => {
+  describe('props', () => {
+    it('should have props', () => {
+      const { mr } = deploymentComponent.props;
+
+      expect(mr.type instanceof Object).toBeTruthy();
+      expect(mr.required).toBeTruthy();
+    });
+  });
+
+  describe('computed', () => {
+    describe('svg', () => {
+      it('should have the proper SVG icon', () => {
+        const vm = createComponent(deploymentMockData);
+        expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_success);
+      });
+    });
+  });
+
+  describe('methods', () => {
+    const vm = createComponent();
+    const deployment = deploymentMockData[0];
+
+    describe('formatDate', () => {
+      it('should work', () => {
+        const readable = gl.utils.getTimeago().format(deployment.deployed_at);
+        expect(vm.formatDate(deployment.deployed_at)).toEqual(readable);
+      });
+    });
+
+    describe('hasExternalUrls', () => {
+      it('should return true', () => {
+        expect(vm.hasExternalUrls(deployment)).toBeTruthy();
+      });
+
+      it('should return false when there is not enough information', () => {
+        expect(vm.hasExternalUrls()).toBeFalsy();
+        expect(vm.hasExternalUrls({ external_url: 'Diplo' })).toBeFalsy();
+        expect(vm.hasExternalUrls({ external_url_formatted: 'Diplo' })).toBeFalsy();
+      });
+    });
+
+    describe('hasDeploymentTime', () => {
+      it('should return true', () => {
+        expect(vm.hasDeploymentTime(deployment)).toBeTruthy();
+      });
+
+      it('should return false when there is not enough information', () => {
+        expect(vm.hasDeploymentTime()).toBeFalsy();
+        expect(vm.hasDeploymentTime({ deployed_at: 'Diplo' })).toBeFalsy();
+        expect(vm.hasDeploymentTime({ deployed_at_formatted: 'Diplo' })).toBeFalsy();
+      });
+    });
+
+    describe('hasDeploymentMeta', () => {
+      it('should return true', () => {
+        expect(vm.hasDeploymentMeta(deployment)).toBeTruthy();
+      });
+
+      it('should return false when there is not enough information', () => {
+        expect(vm.hasDeploymentMeta()).toBeFalsy();
+        expect(vm.hasDeploymentMeta({ url: 'Diplo' })).toBeFalsy();
+        expect(vm.hasDeploymentMeta({ name: 'Diplo' })).toBeFalsy();
+      });
+    });
+  });
+
+  describe('template', () => {
+    let vm;
+    let el;
+    const [deployment] = deploymentMockData;
+
+    beforeEach(() => {
+      vm = createComponent(deploymentMockData);
+      el = vm.$el;
+    });
+
+    it('should render template elements correctly', () => {
+      expect(el.classList.contains('mr-widget-heading')).toBeTruthy();
+      expect(el.querySelector('.js-icon-link')).toBeDefined();
+      expect(el.querySelector('.js-deploy-meta').getAttribute('href')).toEqual(deployment.url);
+      expect(el.querySelector('.js-deploy-meta').innerText).toContain(deployment.name);
+      expect(el.querySelector('.js-deploy-url').getAttribute('href')).toEqual(deployment.external_url);
+      expect(el.querySelector('.js-deploy-url').innerText).toContain(deployment.external_url_formatted);
+      expect(el.querySelector('.js-deploy-time').innerText).toContain(vm.formatDate(deployment.deployed_at));
+      expect(el.querySelector('button')).toBeDefined();
+    });
+
+    it('should list multiple deployments', (done) => {
+      vm.mr.deployments.push(deployment);
+      vm.mr.deployments.push(deployment);
+
+      Vue.nextTick(() => {
+        expect(el.querySelectorAll('.ci_widget').length).toEqual(3);
+        done();
+      });
+    });
+
+    it('should not have some elements when there is not enough data', (done) => {
+      vm.mr.deployments = [{}];
+
+      Vue.nextTick(() => {
+        expect(el.querySelectorAll('.js-deploy-meta').length).toEqual(0);
+        expect(el.querySelectorAll('.js-deploy-url').length).toEqual(0);
+        expect(el.querySelectorAll('.js-deploy-time').length).toEqual(0);
+        expect(el.querySelectorAll('.button').length).toEqual(0);
+        done();
+      });
+    });
+  });
+});
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
new file mode 100644
index 00000000000..734b14c688e
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -0,0 +1,86 @@
+import Vue from 'vue';
+import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header';
+
+const createComponent = (mr) => {
+  const Component = Vue.extend(headerComponent);
+  return new Component({
+    el: document.createElement('div'),
+    propsData: { mr },
+  });
+};
+
+describe('MRWidgetHeader', () => {
+  describe('props', () => {
+    it('should have props', () => {
+      const { mr } = headerComponent.props;
+
+      expect(mr.type instanceof Object).toBeTruthy();
+      expect(mr.required).toBeTruthy();
+    });
+  });
+
+  describe('computed', () => {
+    it('shouldShowCommitsBehindText', () => {
+      const vm = createComponent({ divergedCommitsCount: 12 });
+      expect(vm.shouldShowCommitsBehindText).toBeTruthy();
+
+      vm.mr.divergedCommitsCount = 0;
+      expect(vm.shouldShowCommitsBehindText).toBeFalsy();
+    });
+
+    it('commitsText', () => {
+      const vm = createComponent({ divergedCommitsCount: 12 });
+      expect(vm.commitsText).toEqual('commits');
+
+      vm.mr.divergedCommitsCount = 1;
+      expect(vm.commitsText).toEqual('commit');
+    });
+  });
+
+  describe('template', () => {
+    let vm;
+    let el;
+    const mr = {
+      divergedCommitsCount: 12,
+      sourceBranch: 'mr-widget-refactor',
+      targetBranch: 'master',
+      isOpen: true,
+      emailPatchesPath: '/mr/email-patches',
+      plainDiffPath: '/mr/plainDiffPath',
+    };
+
+    beforeEach(() => {
+      vm = createComponent(mr);
+      el = vm.$el;
+    });
+
+    it('should render template elements correctly', () => {
+      expect(el.classList.contains('mr-source-target')).toBeTruthy();
+      expect(el.querySelectorAll('.label-branch')[0].textContent).toContain(mr.sourceBranch);
+      expect(el.querySelectorAll('.label-branch')[1].textContent).toContain(mr.targetBranch);
+      expect(el.querySelector('.diverged-commits-count').textContent).toContain('12 commits behind');
+
+      expect(el.textContent).toContain('Check out branch');
+      expect(el.querySelectorAll('.dropdown li a')[0].getAttribute('href')).toEqual(mr.emailPatchesPath);
+      expect(el.querySelectorAll('.dropdown li a')[1].getAttribute('href')).toEqual(mr.plainDiffPath);
+    });
+
+    it('should not have right action links if the MR state is not open', (done) => {
+      vm.mr.isOpen = false;
+      Vue.nextTick(() => {
+        expect(el.textContent).not.toContain('Check out branch');
+        expect(el.querySelectorAll('.dropdown li a').length).toEqual(0);
+        done();
+      });
+    });
+
+    it('should not render diverged commits count if the MR has no diverged commits', (done) => {
+      vm.mr.divergedCommitsCount = null;
+      Vue.nextTick(() => {
+        expect(el.textContent).not.toContain('commits behind');
+        expect(el.querySelectorAll('.diverged-commits-count').length).toEqual(0);
+        done();
+      });
+    });
+  });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
new file mode 100644
index 00000000000..4da4fc82c26
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help';
+
+const props = {
+  missingBranch: 'this-is-not-the-branch-you-are-looking-for',
+};
+const text = `If the ${props.missingBranch} branch exists in your local repository`;
+
+const createComponent = () => {
+  const Component = Vue.extend(mergeHelpComponent);
+  return new Component({
+    el: document.createElement('div'),
+    propsData: props,
+  });
+};
+
+describe('MRWidgetMergeHelp', () => {
+  describe('props', () => {
+    it('should have props', () => {
+      const { missingBranch } = mergeHelpComponent.props;
+      const MissingBranchTypeClass = missingBranch.type;
+
+      expect(new MissingBranchTypeClass() instanceof String).toBeTruthy();
+      expect(missingBranch.required).toBeFalsy();
+      expect(missingBranch.default).toEqual('');
+    });
+  });
+
+  describe('template', () => {
+    let vm;
+    let el;
+
+    beforeEach(() => {
+      vm = createComponent();
+      el = vm.$el;
+    });
+
+    it('should have the correct elements', () => {
+      expect(el.classList.contains('mr-widget-help')).toBeTruthy();
+      expect(el.textContent).toContain(text);
+    });
+
+    it('should not show missing branch name if missingBranch props is not provided', (done) => {
+      vm.missingBranch = null;
+      Vue.nextTick(() => {
+        expect(el.textContent).not.toContain(text);
+        done();
+      });
+    });
+  });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
new file mode 100644
index 00000000000..0824f325068
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -0,0 +1,129 @@
+import Vue from 'vue';
+import { statusClassToSvgMap } from '~/vue_shared/pipeline_svg_icons';
+import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline';
+import pipelineMockData from '../../commit/pipelines/mock_data';
+
+const createComponent = (mr) => {
+  const Component = Vue.extend(pipelineComponent);
+  return new Component({
+    el: document.createElement('div'),
+    propsData: { mr },
+  });
+};
+
+describe('MRWidgetPipeline', () => {
+  describe('props', () => {
+    it('should have props', () => {
+      const { mr } = pipelineComponent.props;
+
+      expect(mr.type instanceof Object).toBeTruthy();
+      expect(mr.required).toBeTruthy();
+    });
+  });
+
+  describe('components', () => {
+    it('should have components added', () => {
+      expect(pipelineComponent.components['pipeline-stage']).toBeDefined();
+      expect(pipelineComponent.components['pipeline-status-icon']).toBeDefined();
+    });
+  });
+
+  describe('computed', () => {
+    describe('svg', () => {
+      it('should have the proper SVG icon', () => {
+        const vm = createComponent({ pipeline: pipelineMockData });
+
+        expect(vm.svg).toEqual(statusClassToSvgMap.icon_status_failed);
+      });
+    });
+
+    describe('hasCIError', () => {
+      it('should return false when there is no CI error', () => {
+        const vm = createComponent({
+          pipeline: pipelineMockData,
+          hasCI: true,
+          ciStatus: 'success',
+        });
+
+        expect(vm.hasCIError).toBeFalsy();
+      });
+
+      it('should return true when there is a CI error', () => {
+        const vm = createComponent({
+          pipeline: pipelineMockData,
+          hasCI: true,
+          ciStatus: null,
+        });
+
+        expect(vm.hasCIError).toBeTruthy();
+      });
+    });
+  });
+
+  describe('template', () => {
+    let vm;
+    let el;
+    const mr = {
+      pipeline: pipelineMockData,
+      hasCI: true,
+      ciStatus: 'failed',
+    };
+
+    beforeEach(() => {
+      vm = createComponent(mr);
+      el = vm.$el;
+    });
+
+    it('should render template elements correctly', () => {
+      expect(el.classList.contains('mr-widget-heading')).toBeTruthy();
+      expect(el.querySelectorAll('.ci-status-icon.ci-status-icon-failed').length).toEqual(1);
+      expect(el.querySelector('.pipeline-id').textContent).toContain(`#${pipelineMockData.id}`);
+      expect(el.innerText).toContain('failed');
+      expect(el.querySelector('.pipeline-id').getAttribute('href')).toEqual(pipelineMockData.path);
+      expect(el.querySelectorAll('.stage-container').length).toEqual(1);
+      expect(el.querySelector('.js-ci-error')).toEqual(null);
+      expect(el.querySelector('.js-commit-link').getAttribute('href')).toEqual(pipelineMockData.commit.commit_path);
+      expect(el.querySelector('.js-commit-link').textContent).toEqual(pipelineMockData.commit.short_id);
+      expect(el.querySelector('.js-mr-coverage').textContent).toContain(`Coverage ${pipelineMockData.coverage}%`);
+    });
+
+    it('should list multiple stages', (done) => {
+      const [stage] = pipelineMockData.details.stages;
+      vm.mr.pipeline.details.stages.push(stage);
+      vm.mr.pipeline.details.stages.push(stage);
+
+      Vue.nextTick(() => {
+        expect(el.querySelectorAll('.stage-container button').length).toEqual(3);
+        done();
+      });
+    });
+
+    it('should not have stages when there is no stage', (done) => {
+      vm.mr.pipeline.details.stages = [];
+
+      Vue.nextTick(() => {
+        expect(el.querySelectorAll('.stage-container button').length).toEqual(0);
+        done();
+      });
+    });
+
+    it('should not have coverage text when pipeline has no coverage info', (done) => {
+      vm.mr.pipeline.coverage = null;
+
+      Vue.nextTick(() => {
+        expect(el.querySelector('.js-mr-coverage')).toEqual(null);
+        done();
+      });
+    });
+
+    it('should show CI error when there is a CI error', (done) => {
+      vm.mr.ciStatus = null;
+
+      Vue.nextTick(() => {
+        expect(el.querySelectorAll('.js-ci-error').length).toEqual(1);
+        expect(el.innerText).toContain('Could not connect to the CI server');
+        done();
+      });
+    });
+  });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
new file mode 100644
index 00000000000..c8366e87871
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links';
+
+const createComponent = (data) => {
+  const Component = Vue.extend(relatedLinksComponent);
+
+  return new Component({
+    el: document.createElement('div'),
+    propsData: data,
+  });
+};
+
+describe('MRWidgetRelatedLinks', () => {
+  describe('props', () => {
+    it('should have props', () => {
+      const { relatedLinks } = relatedLinksComponent.props;
+
+      expect(relatedLinks).toBeDefined();
+      expect(relatedLinks.type instanceof Object).toBeTruthy();
+      expect(relatedLinks.required).toBeTruthy();
+    });
+  });
+
+  describe('methods', () => {
+    const data = {
+      relatedLinks: {
+        closing: '<a href="#">#23</a> and <a>#42</a>',
+        mentioned: '<a href="#">#7</a>',
+      },
+    };
+    const vm = createComponent(data);
+
+    describe('hasMultipleIssues', () => {
+      it('should return true if the given text has multiple issues', () => {
+        expect(vm.hasMultipleIssues(data.relatedLinks.closing)).toBeTruthy();
+      });
+
+      it('should return false if the given text has one issue', () => {
+        expect(vm.hasMultipleIssues(data.relatedLinks.mentioned)).toBeFalsy();
+      });
+    });
+
+    describe('issueLabel', () => {
+      it('should return true if the given text has multiple issues', () => {
+        expect(vm.issueLabel('closing')).toEqual('issues');
+      });
+
+      it('should return false if the given text has one issue', () => {
+        expect(vm.issueLabel('mentioned')).toEqual('issue');
+      });
+    });
+
+    describe('verbLabel', () => {
+      it('should return true if the given text has multiple issues', () => {
+        expect(vm.verbLabel('closing')).toEqual('are');
+      });
+
+      it('should return false if the given text has one issue', () => {
+        expect(vm.verbLabel('mentioned')).toEqual('is');
+      });
+    });
+  });
+
+  describe('template', () => {
+    it('should have only have closing issues text', () => {
+      const vm = createComponent({ relatedLinks: { closing: '<a href="#">#23</a> and <a>#42</a>' } });
+
+      expect(vm.$el.innerText).toContain('Closes issues #23 and #42');
+      expect(vm.$el.innerText).not.toContain('mentioned');
+    });
+
+    it('should have only have mentioned issues text', () => {
+      const vm = createComponent({ relatedLinks: { mentioned: '<a href="#">#7</a>' } });
+
+      expect(vm.$el.innerText).toContain('issue #7');
+      expect(vm.$el.innerText).toContain('is mentioned but will not be closed.');
+      expect(vm.$el.innerText).not.toContain('Closes');
+    });
+
+    it('should have closing and mentioned issues at the same time', () => {
+      const vm = createComponent({
+        relatedLinks: {
+          closing: '<a href="#">#7</a>',
+          mentioned: '<a href="#">#23</a> and <a>#42</a>',
+        },
+      });
+
+      expect(vm.$el.innerText).toContain('Closes issue #7.');
+      expect(vm.$el.innerText).toContain('issues #23 and #42');
+      expect(vm.$el.innerText).toContain('are mentioned but will not be closed.');
+    });
+  });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js
new file mode 100644
index 00000000000..cac2f561a0b
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived';
+
+describe('MRWidgetArchived', () => {
+  describe('template', () => {
+    it('should have correct elements', () => {
+      const Component = Vue.extend(archivedComponent);
+      const el = new Component({
+        el: document.createElement('div'),
+      }).$el;
+
+      expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+      expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy();
+      expect(el.querySelector('button').disabled).toBeTruthy();
+      expect(el.innerText).toContain('This project is archived, write access has been disabled.');
+    });
+  });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
new file mode 100644
index 00000000000..3be11d47227
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking';
+
+describe('MRWidgetChecking', () => {
+  describe('template', () => {
+    it('should have correct elements', () => {
+      const Component = Vue.extend(checkingComponent);
+      const el = new Component({
+        el: document.createElement('div'),
+      }).$el;
+
+      expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+      expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy();
+      expect(el.querySelector('button').disabled).toBeTruthy();
+      expect(el.innerText).toContain('Checking ability to merge automatically.');
+      expect(el.querySelector('i')).toBeDefined();
+    });
+  });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
new file mode 100644
index 00000000000..47303d1e80f
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
@@ -0,0 +1,51 @@
+import Vue from 'vue';
+import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed';
+
+const mr = {
+  targetBranch: 'good-branch',
+  targetBranchPath: '/good-branch',
+  closedBy: {
+    name: 'Fatih Acet',
+    username: 'fatihacet',
+  },
+  updatedAt: '2017-03-23T20:08:08.845Z',
+  closedAt: '1 day ago',
+};
+
+const createComponent = () => {
+  const Component = Vue.extend(closedComponent);
+
+  return new Component({
+    el: document.createElement('div'),
+    propsData: { mr },
+  }).$el;
+};
+
+describe('MRWidgetClosed', () => {
+  describe('props', () => {
+    it('should have props', () => {
+      const mrProp = closedComponent.props.mr;
+
+      expect(mrProp.type instanceof Object).toBeTruthy();
+      expect(mrProp.required).toBeTruthy();
+    });
+  });
+
+  describe('components', () => {
+    it('should have components added', () => {
+      expect(closedComponent.components['mr-widget-author-and-time']).toBeDefined();
+    });
+  });
+
+  describe('template', () => {
+    it('should have correct elements', () => {
+      const el = createComponent();
+
+      expect(el.querySelector('h4').textContent).toContain('Closed by');
+      expect(el.querySelector('h4').textContent).toContain(mr.closedBy.name);
+      expect(el.textContent).toContain('The changes were not merged into');
+      expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath);
+      expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch);
+    });
+  });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
new file mode 100644
index 00000000000..a637c606675
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -0,0 +1,95 @@
+import Vue from 'vue';
+import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts';
+
+const path = '/conflicts';
+const createComponent = () => {
+  const Component = Vue.extend(conflictsComponent);
+
+  return new Component({
+    el: document.createElement('div'),
+    propsData: {
+      mr: {
+        canMerge: true,
+        canResolveConflicts: true,
+        canResolveConflictsInUI: true,
+        conflictResolutionPath: path,
+      },
+    },
+  });
+};
+
+describe('MRWidgetConflicts', () => {
+  describe('props', () => {
+    it('should have props', () => {
+      const { mr } = conflictsComponent.props;
+
+      expect(mr.type instanceof Object).toBeTruthy();
+      expect(mr.required).toBeTruthy();
+    });
+  });
+
+  describe('computed', () => {
+    it('showResolveConflictsButton', () => {
+      const vm = createComponent();
+      expect(vm.showResolveConflictsButton).toBeTruthy();
+
+      vm.mr.canMerge = false;
+      expect(vm.showResolveConflictsButton).toBeFalsy();
+
+      vm.mr.canMerge = true;
+      vm.mr.canResolveConflicts = false;
+      expect(vm.showResolveConflictsButton).toBeFalsy();
+
+      vm.mr.canMerge = true;
+      vm.mr.canResolveConflicts = true;
+      vm.mr.canResolveConflictsInUI = false;
+      expect(vm.showResolveConflictsButton).toBeFalsy();
+
+      vm.mr.canMerge = false;
+      vm.mr.canResolveConflicts = false;
+      vm.mr.canResolveConflictsInUI = false;
+      expect(vm.showResolveConflictsButton).toBeFalsy();
+    });
+  });
+
+  describe('template', () => {
+    it('should have correct elements', () => {
+      const el = createComponent().$el;
+      const resolveButton = el.querySelectorAll('.btn-group .btn')[0];
+      const mergeLocallyButton = el.querySelectorAll('.btn-group .btn')[1];
+
+      expect(el.textContent).toContain('There are merge conflicts.');
+      expect(el.textContent).not.toContain('ask someone with write access');
+      expect(el.querySelector('.btn-success').disabled).toBeTruthy();
+      expect(el.querySelectorAll('.btn-group .btn').length).toBe(2);
+      expect(resolveButton.textContent).toContain('Resolve conflicts');
+      expect(resolveButton.getAttribute('href')).toEqual(path);
+      expect(mergeLocallyButton.textContent).toContain('Merge locally');
+    });
+
+    describe('when user does not have permission to merge', () => {
+      let vm;
+
+      beforeEach(() => {
+        vm = createComponent();
+        vm.mr.canMerge = false;
+      });
+
+      it('should show proper message', (done) => {
+        Vue.nextTick(() => {
+          expect(vm.$el.textContent).toContain('ask someone with write access');
+          done();
+        });
+      });
+
+      it('should not have action buttons', (done) => {
+        Vue.nextTick(() => {
+          expect(vm.$el.querySelectorAll('.btn').length).toBe(1);
+          expect(vm.$el.querySelector('a.js-resolve-conflicts-button')).toEqual(null);
+          expect(vm.$el.querySelector('a.js-merge-locally-button')).toEqual(null);
+          done();
+        });
+      });
+    });
+  });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
new file mode 100644
index 00000000000..a043ee888a2
--- /dev/null
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_locked_spec.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import lockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_locked';
+
+describe('MRWidgetLocked', () => {
+  describe('props', () => {
+    it('should have props', () => {
+      const { mr } = lockedComponent.props;
+
+      expect(mr.type instanceof Object).toBeTruthy();
+      expect(mr.required).toBeTruthy();
+    });
+  });
+
+  describe('template', () => {
+    it('should have correct elements', () => {
+      const Component = Vue.extend(lockedComponent);
+      const mr = {
+        targetBranchPath: '/branch-path',
+        targetBranch: 'branch',
+      };
+      const el = new Component({
+        el: document.createElement('div'),
+        propsData: { mr },
+      }).$el;
+
+      expect(el.classList.contains('mr-widget-body')).toBeTruthy();
+      expect(el.innerText).toContain('it is locked');
+      expect(el.innerText).toContain('changes will be merged into');
+      expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath);
+      expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch);
+    });
+  });
+});
diff --git a/spec/serializers/event_entity_spec.rb b/spec/serializers/event_entity_spec.rb
new file mode 100644
index 00000000000..bb54597c967
--- /dev/null
+++ b/spec/serializers/event_entity_spec.rb
@@ -0,0 +1,13 @@
+require 'spec_helper'
+
+describe EventEntity do
+  subject { described_class.represent(create(:event)).as_json }
+
+  it 'exposes author' do
+    expect(subject).to include(:author)
+  end
+
+  it 'exposes core elements of event' do
+    expect(subject).to include(:updated_at)
+  end
+end
diff --git a/spec/serializers/merge_request_basic_serializer_spec.rb b/spec/serializers/merge_request_basic_serializer_spec.rb
new file mode 100644
index 00000000000..4daf5a59d0c
--- /dev/null
+++ b/spec/serializers/merge_request_basic_serializer_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe MergeRequestBasicSerializer do
+  let(:resource) { create(:merge_request) }
+  let(:user)     { create(:user) }
+
+  subject { described_class.new.represent(resource) }
+
+  it 'has important MergeRequest attributes' do
+    expect(subject).to include(:merge_status)
+  end
+end
diff --git a/spec/serializers/merge_request_serializer_spec.rb b/spec/serializers/merge_request_serializer_spec.rb
new file mode 100644
index 00000000000..9d7baeaabf4
--- /dev/null
+++ b/spec/serializers/merge_request_serializer_spec.rb
@@ -0,0 +1,269 @@
+require 'spec_helper'
+
+describe MergeRequestSerializer do
+  let(:project)  { create :empty_project }
+  let(:resource) { create(:merge_request, source_project: project, target_project: project) }
+  let(:user)     { create(:user) }
+
+  subject { described_class.new(current_user: user).represent(resource) }
+
+  it 'includes author' do
+    req = double('request')
+
+    author_payload = UserEntity
+      .represent(resource.author, request: req)
+      .as_json
+
+    expect(subject[:author]).to eql(author_payload)
+  end
+
+  it 'includes pipeline' do
+    req = double('request', current_user: user)
+    pipeline = build_stubbed(:ci_pipeline)
+    allow(resource).to receive(:head_pipeline).and_return(pipeline)
+
+    pipeline_payload = PipelineEntity
+      .represent(pipeline, request: req)
+      .as_json
+
+    expect(subject[:pipeline]).to eql(pipeline_payload)
+  end
+
+  it 'has important MergeRequest attributes' do
+    expect(subject).to include(:diff_head_sha, :merge_commit_message,
+                              :can_be_merged, :can_be_cherry_picked,
+                              :has_conflicts, :has_ci)
+  end
+
+  it 'has merge_path' do
+    expect(subject[:merge_path])
+      .to eql("/#{resource.project.full_path}/merge_requests/#{resource.iid}/merge")
+  end
+
+  it 'has remove_wip_path' do
+    expect(subject[:remove_wip_path])
+      .to eql("/#{resource.project.full_path}/merge_requests/#{resource.iid}/remove_wip")
+  end
+
+  it 'has conflict_resolution_ui_path' do
+    expect(subject[:conflict_resolution_ui_path])
+      .to eql("/#{resource.project.full_path}/merge_requests/#{resource.iid}/conflicts")
+  end
+
+  it 'has email_patches_path' do
+    expect(subject[:email_patches_path])
+      .to eql("/#{resource.project.full_path}/merge_requests/#{resource.iid}.patch")
+  end
+
+  it 'has plain_diff_path' do
+    expect(subject[:plain_diff_path])
+      .to eql("/#{resource.project.full_path}/merge_requests/#{resource.iid}.diff")
+  end
+
+  it 'has target_branch_path' do
+    expect(subject[:target_branch_path])
+      .to eql("/#{resource.target_project.full_path}/branches/#{resource.target_branch}")
+  end
+
+  it 'has source_branch_path' do
+    expect(subject[:source_branch_path])
+      .to eql("/#{resource.source_project.full_path}/branches/#{resource.source_branch}")
+  end
+
+  it 'has merge_commit_message_with_description' do
+    expect(subject[:merge_commit_message_with_description])
+      .to eql(resource.merge_commit_message(include_description: true))
+  end
+
+  describe 'diff_head_commit_short_id' do
+    context 'when no diff head commit' do
+      let(:project) { create :empty_project }
+
+      it 'returns nil' do
+        expect(subject[:diff_head_commit_short_id]).to be_nil
+      end
+    end
+
+    context 'when diff head commit present' do
+      let(:project) { create :project }
+
+      it 'returns diff head commit short id' do
+        expect(subject[:diff_head_commit_short_id]).to eql(resource.diff_head_commit.short_id)
+      end
+    end
+  end
+
+  describe 'ci_status' do
+    let(:project) { create :project }
+
+    context 'when no head pipeline' do
+      it 'return status using CiService' do
+        ci_service = double(MockCiService)
+        ci_status = double
+
+        allow(resource.source_project)
+          .to receive(:ci_service)
+          .and_return(ci_service)
+
+        allow(resource).to receive(:head_pipeline).and_return(nil)
+
+
+        expect(ci_service).to receive(:commit_status)
+          .with(resource.diff_head_sha, resource.source_branch)
+          .and_return(ci_status)
+
+        expect(subject[:ci_status]).to eql(ci_status)
+      end
+    end
+
+    context 'when head pipeline present' do
+      let(:pipeline) { build_stubbed(:ci_pipeline) }
+
+      before do
+        allow(resource).to receive(:head_pipeline).and_return(pipeline)
+      end
+
+      context 'success with warnings' do
+        before do
+          allow(pipeline).to receive(:success?) { true }
+          allow(pipeline).to receive(:has_warnings?) { true }
+        end
+
+        it 'returns "success_with_warnings"' do
+          expect(subject[:ci_status]).to eql('success_with_warnings')
+        end
+      end
+
+      context 'pipeline HAS status AND its not success with warnings' do
+        before do
+          allow(pipeline).to receive(:success?) { false }
+          allow(pipeline).to receive(:has_warnings?) { false }
+        end
+
+        it 'returns pipeline status' do
+          expect(subject[:ci_status]).to eql('pending')
+        end
+      end
+
+      context 'pipeline has NO status AND its not success with warnings' do
+        before do
+          allow(pipeline).to receive(:status) { nil }
+          allow(pipeline).to receive(:success?) { false }
+          allow(pipeline).to receive(:has_warnings?) { false }
+        end
+
+        it 'returns "preparing"' do
+          expect(subject[:ci_status]).to eql('preparing')
+        end
+      end
+    end
+  end
+
+  it 'includes merge_event' do
+    event = create(:event, :merged, author: user, project: resource.project, target: resource)
+
+    event_payload = EventEntity
+      .represent(event)
+      .as_json
+
+    expect(subject[:merge_event]).to eql(event_payload)
+  end
+
+  it 'includes closed_event' do
+    event = create(:event, :closed, author: user, project: resource.project, target: resource)
+
+    event_payload = EventEntity
+      .represent(event)
+      .as_json
+
+    expect(subject[:closed_event]).to eql(event_payload)
+  end
+
+  describe 'diverged_commits_count' do
+    context 'when MR open and its diverging' do
+      it 'returns diverged commits count' do
+        allow(resource).to receive_messages(open?: true, diverged_from_target_branch?: true,
+                                            diverged_commits_count: 10)
+
+        expect(subject[:diverged_commits_count]).to eql(10)
+      end
+    end
+
+    context 'when MR is not open' do
+      it 'returns 0' do
+        allow(resource).to receive_messages(open?: false)
+
+        expect(subject[:diverged_commits_count]).to be_zero
+      end
+    end
+
+    context 'when MR is not diverging' do
+      it 'returns 0' do
+        allow(resource).to receive_messages(open?: true, diverged_from_target_branch?: false)
+
+        expect(subject[:diverged_commits_count]).to be_zero
+      end
+    end
+  end
+
+  context 'current_user' do
+    describe 'can_update_merge_request' do
+      context 'user can update issue' do
+        it 'returns true' do
+          resource.project.team << [user, :developer]
+
+          expect(subject[:current_user][:can_update_merge_request]).to eql(true)
+        end
+      end
+
+      context 'user cannot update issue' do
+        it 'returns false' do
+          expect(subject[:current_user][:can_update_merge_request]).to eql(false)
+        end
+      end
+    end
+  end
+
+  context 'issues_links' do
+    let(:project) { create(:project, :private, creator: user, namespace: user.namespace) }
+    let(:issue_a) { create(:issue, project: project) }
+    let(:issue_b) { create(:issue, project: project) }
+
+    let(:resource) do
+      create(:merge_request,
+             source_project: project, target_project: project,
+             description: "Fixes #{issue_a.to_reference} Related #{issue_b.to_reference}")
+    end
+
+    before do
+      project.team << [user, :developer]
+
+      allow(resource.project).to receive(:default_branch)
+        .and_return(resource.target_branch)
+    end
+
+    describe 'closing' do
+      let(:sentence) { subject[:issues_links][:closing] }
+
+      it 'presents closing issues links' do
+        expect(sentence).to match("#{project.full_path}/issues/#{issue_a.iid}")
+      end
+
+      it 'does not present related issues links' do
+        expect(sentence).not_to match("#{project.full_path}/issues/#{issue_b.iid}")
+      end
+    end
+
+    describe 'mentioned_but_not_closing' do
+      let(:sentence) { subject[:issues_links][:mentioned_but_not_closing] }
+
+      it 'presents related issues links' do
+        expect(sentence).to match("#{project.full_path}/issues/#{issue_b.iid}")
+      end
+
+      it 'does not present closing issues links' do
+        expect(sentence).not_to match("#{project.full_path}/issues/#{issue_a.iid}")
+      end
+    end
+  end
+end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index 93d5a21419d..d2482ac434b 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -5,7 +5,7 @@ describe PipelineEntity do
   let(:request) { double('request') }
 
   before do
-    allow(request).to receive(:user).and_return(user)
+    allow(request).to receive(:current_user).and_return(user)
   end
 
   let(:entity) do
@@ -19,7 +19,7 @@ describe PipelineEntity do
       let(:pipeline) { create(:ci_empty_pipeline) }
 
       it 'contains required fields' do
-        expect(subject).to include :id, :user, :path
+        expect(subject).to include :id, :user, :path, :coverage
         expect(subject).to include :ref, :commit
         expect(subject).to include :updated_at, :created_at
       end
diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb
index 4ab40d08432..bd7b706bb8d 100644
--- a/spec/serializers/stage_entity_spec.rb
+++ b/spec/serializers/stage_entity_spec.rb
@@ -14,7 +14,7 @@ describe StageEntity do
   end
 
   before do
-    allow(request).to receive(:user).and_return(user)
+    allow(request).to receive(:current_user).and_return(user)
     create(:ci_build, :success, pipeline: pipeline)
   end
 
-- 
GitLab