diff --git a/CHANGELOG b/CHANGELOG
index e410d73d1f69174cb2a1af00ea23b96119fdfd8b..9458413669d8ddbc6ebee000aa30f18bf05bd320 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -37,6 +37,7 @@ v 8.12.0 (unreleased)
   - Request only the LDAP attributes we need !6187
   - Center build stage columns in pipeline overview (ClemMakesApps)
   - Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps)
+  - Fix bug stopping issue description being scrollable after selecting issue template
   - Remove suggested colors hover underline (ClemMakesApps)
   - Shorten task status phrase (ClemMakesApps)
   - Fix project visibility level fields on settings
@@ -92,6 +93,7 @@ v 8.12.0 (unreleased)
   - Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell)
   - Fix repo title alignment (ClemMakesApps)
   - Change update interval of contacted_at
+  - Add LFS support to SSH !6043
   - Fix branch title trailing space on hover (ClemMakesApps)
   - Don't include 'Created By' tag line when importing from GitHub if there is a linked GitLab account (EspadaV8)
   - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index b18b6962382fa2d78e18a38ebd87fa0692deeee6..95352164d76a8e63bf384ca28a282561e724c185 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -13,6 +13,9 @@
       this.buildDropdown();
       this.bindEvents();
       this.onFilenameUpdate();
+
+      this.autosizeUpdateEvent = document.createEvent('Event');
+      this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
     }
 
     TemplateSelector.prototype.buildDropdown = function() {
@@ -72,6 +75,10 @@
     TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
       this.editor.setValue(file.content, 1);
       if (!skipFocus) this.editor.focus();
+
+      if (this.editor instanceof jQuery) {
+        this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
+      }
     };
 
     TemplateSelector.prototype.startLoadingSpinner = function() {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 2d66ab25da6a7a91fd854568ab82fad5fc64f54f..cc71b8eb045715a083783872a6196b5b9360e47e 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -318,9 +318,17 @@
 
     .build-content {
       width: 130px;
-      white-space: nowrap;
-      overflow: hidden;
-      text-overflow: ellipsis;
+
+      .ci-status-text {
+        width: 110px;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        vertical-align: middle;
+        display: inline-block;
+        position: relative;
+        top: -1px;
+      }
 
       a {
         color: $layout-link-gray;
@@ -331,13 +339,74 @@
             text-decoration: underline;
           }
         }
+      }
+
+      .dropdown-menu-toggle {
+        border: none;
+        width: auto;
+        padding: 0;
+        color: $layout-link-gray;
+
+        .ci-status-text {
+          width: 80px;
+        }
+      }
+
+      .grouped-pipeline-dropdown {
+        padding: 8px 0;
+        width: 200px;
+        left: auto;
+        right: -214px;
+        top: -9px;
+
+        a:hover {
+          .ci-status-text {
+            text-decoration: none;
+          }
+        }
+
+        .ci-status-text {
+          width: 145px;
+        }
+
+        .arrow {
+          &:before,
+          &:after {
+            content: '';
+            display: inline-block;
+            position: absolute;
+            width: 0;
+            height: 0;
+            border-color: transparent;
+            border-style: solid;
+            top: 18px;
+          }
+
+          &:before {
+            left: -5px;
+            margin-top: -6px;
+            border-width: 7px 5px 7px 0;
+            border-right-color: $border-color;
+          }
 
+          &:after {
+            left: -4px;
+            margin-top: -9px;
+            border-width: 10px 7px 10px 0;
+            border-right-color: $white-light;
+          }
+        }
+      }
+
+      .badge {
+        background-color: $gray-dark;
+        color: $layout-link-gray;
+        font-weight: normal;
       }
     }
 
     svg {
-      position: relative;
-      top: 2px;
+      vertical-align: middle;
       margin-right: 5px;
     }
 
@@ -442,7 +511,7 @@
       width: 21px;
       height: 25px;
       position: absolute;
-      top: -28.5px;
+      top: -29px;
       border-top: 2px solid $border-color;
     }
 
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 3e6e50375f6a5f9682c30ec9fb1cdfcff1ea7db1..db46d8072cee3769fb223fb2dbf042be58b3d76e 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -334,6 +334,10 @@ a.deploy-project-label {
   a {
     color: $gl-dark-link-color;
   }
+
+  .dropdown-menu {
+    width: 240px;
+  }
 }
 
 .last-push-widget {
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index c2a298fe37f6e3fd468552b4f04af9369d6d66af..14e83ddda04236eace3bdf196589508d5c5c4f36 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -4,7 +4,7 @@ class Projects::GitHttpClientController < Projects::ApplicationController
   include ActionController::HttpAuthentication::Basic
   include KerberosSpnegoHelper
 
-  attr_reader :user, :capabilities
+  attr_reader :actor, :capabilities
 
   # Git clients will not know what authenticity token to send along
   skip_before_action :verify_authenticity_token
@@ -21,31 +21,14 @@ class Projects::GitHttpClientController < Projects::ApplicationController
 
     if allow_basic_auth? && basic_auth_provided?
       login, password = user_name_and_password(request)
-      auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
-
-      if auth_result.type == :ci && !download_request?
-        # Not allowed
-        auth_result = Gitlab::Auth::Result.new
-      elsif auth_result.type == :oauth && !download_request?
-        # Not allowed
-        auth_result = Gitlab::Auth::Result.new
-      elsif auth_result.type == :missing_personal_token
-        render_missing_personal_token
-        return # Render above denied access, nothing left to do
-      else
-        @user = auth_result.user
-      end
-
-      @capabilities = auth_result.capabilities || []
-      @ci = auth_result.type == :ci
 
-      if auth_result.succeeded?
+      if handle_basic_authentication(login, password)
         return # Allow access
       end
     elsif allow_kerberos_spnego_auth? && spnego_provided?
-      @user = find_kerberos_user
+      @actor = find_kerberos_user
 
-      if user
+      if actor
         send_final_spnego_response
         return # Allow access
       end
@@ -53,6 +36,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
 
     send_challenges
     render plain: "HTTP Basic: Access denied\n", status: 401
+  rescue Gitlab::Auth::MissingPersonalTokenError
+    render_missing_personal_token
   end
 
   def basic_auth_provided?
@@ -120,7 +105,49 @@ class Projects::GitHttpClientController < Projects::ApplicationController
   end
 
   def ci?
-    @ci.present?
+    @ci
+  end
+
+  def user
+    @actor
+  end
+
+  def handle_basic_authentication(login, password)
+    auth_result = Gitlab::Auth.find_for_git_client(login, password, project: project, ip: request.ip)
+
+    case auth_result.type
+    when :ci
+      if download_request?
+        @ci = true
+      else
+        return false
+      end
+    when :oauth
+      if download_request?
+        @actor = auth_result.actor
+        @capabilities = auth_result.capabilities
+      else
+        return false
+      end
+    when :lfs_deploy_token
+      if download_request?
+        @lfs_deploy_key = true
+        @actor = auth_result.actor
+        @capabilities = auth_result.capabilities
+      end
+    when :lfs_token, :personal_token, :gitlab_or_ldap, :build
+      @actor = auth_result.actor
+      @capabilities = auth_result.capabilities
+    else
+      # Not allowed
+      return false
+    end
+
+    true
+  end
+
+  def lfs_deploy_key?
+    @lfs_deploy_key && actor && actor.projects.include?(project)
   end
 
   def has_capability?(capability)
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
index d27b87ed5e425547ae4aca9aa877ceaf3e923325..0d6b72ff74677193c231d1c4480e728ae7195790 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/helpers/lfs_helper.rb
@@ -25,7 +25,7 @@ module LfsHelper
   def lfs_download_access?
     return false unless project.lfs_enabled?
 
-    project.public? || ci? || user_can_download_code? || build_can_download_code?
+    project.public? || ci? || lfs_deploy_key? || user_can_download_code? || build_can_download_code?
   end
 
   def user_can_download_code?
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 4a6289244997c61c220fb36513cd2cc722722d46..c85561291c8700544923071052a378d484c7e50a 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -69,17 +69,15 @@ class CommitStatus < ActiveRecord::Base
       commit_status.update_attributes finished_at: Time.now
     end
 
-    # We use around_transition to process pipeline on next stages as soon as possible, before the `after_*` is executed
-    around_transition any => [:success, :failed, :canceled] do |commit_status, block|
-      block.call
-
-      commit_status.pipeline.try(:process!)
-    end
-
     after_transition do |commit_status, transition|
       commit_status.pipeline.try(:build_updated) unless transition.loopback?
     end
 
+    after_transition any => [:success, :failed, :canceled] do |commit_status|
+      commit_status.pipeline.try(:process!)
+      true
+    end
+
     after_transition [:created, :pending, :running] => :success do |commit_status|
       MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
     end
@@ -95,6 +93,10 @@ class CommitStatus < ActiveRecord::Base
     pipeline.before_sha || Gitlab::Git::BLANK_SHA
   end
 
+  def group_name
+    name.gsub(/\d+[\s:\/\\]+\d+\s*/, '').strip
+  end
+
   def self.stages
     # We group by stage name, but order stages by theirs' index
     unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').pluck('sg.stage')
@@ -113,6 +115,10 @@ class CommitStatus < ActiveRecord::Base
     allow_failure? && (failed? || canceled?)
   end
 
+  def playable?
+    false
+  end
+
   def duration
     calculate_duration
   end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index f7b8352405c6ea665697a6467b1d92f1e7792ad2..d658552f695e544d21b62e0f2dddb86b75b79435 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -8,8 +8,9 @@ module HasStatus
 
   class_methods do
     def status_sql
-      scope = all.relevant
+      scope = all
       builds = scope.select('count(*)').to_sql
+      created = scope.created.select('count(*)').to_sql
       success = scope.success.select('count(*)').to_sql
       ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored)
       ignored ||= '0'
@@ -19,12 +20,12 @@ module HasStatus
       skipped = scope.skipped.select('count(*)').to_sql
 
       deduce_status = "(CASE
-        WHEN (#{builds})=0 THEN NULL
+        WHEN (#{builds})=(#{created}) THEN NULL
         WHEN (#{builds})=(#{skipped}) THEN 'skipped'
         WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
-        WHEN (#{builds})=(#{pending})+(#{skipped}) THEN 'pending'
+        WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending'
         WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
-        WHEN (#{running})+(#{pending})>0 THEN 'running'
+        WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
         ELSE 'failed'
       END)"
 
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
index 36fb0300aebc7a554d938d0170795957ea72726b..547bc0c9c1971a4dd5d31827c56092e9c352386a 100644
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -1,15 +1,12 @@
 - is_playable = subject.playable? && can?(current_user, :update_build, @project)
-%li.build{class: ("playable" if is_playable)}
-  .curve
-  .build-content
-    - if is_playable
-      = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
-        = render_status_with_link('build', 'play')
-        %span.ci-status-text= subject.name
-    - elsif can?(current_user, :read_build, @project)
-      = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
-        = render_status_with_link('build', subject.status)
-        %span.ci-status-text= subject.name
-    - else
-      = render_status_with_link('build', subject.status)
-      = ci_icon_for_status(subject.status)
+- if is_playable
+  = link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
+    = render_status_with_link('build', 'play')
+    .ci-status-text= subject.name
+- elsif can?(current_user, :read_build, @project)
+  = link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
+    = render_status_with_link('build', subject.status)
+    .ci-status-text= subject.name
+- else
+  = render_status_with_link('build', subject.status)
+  = ci_icon_for_status(subject.status)
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 20a85148ab5cdf06b20bf641867a6f3fc5c68fef..9258f4b3c25cf9bc401d93d68b555a6f8c666b7a 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -39,8 +39,7 @@
               = stage.titleize
           .builds-container
             %ul
-              - statuses.each do |status|
-                = render "projects/#{status.to_partial_path}_pipeline", subject: status
+              = render "projects/commit/pipeline_stage", statuses: statuses
 
 
 - if pipeline.yaml_errors.present?
diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..23c5c51fbc270b9f1b834d6457be0afcc6e2c4a8
--- /dev/null
+++ b/app/views/projects/commit/_pipeline_stage.html.haml
@@ -0,0 +1,14 @@
+- status_groups = statuses.group_by(&:group_name)
+- status_groups.each do |group_name, grouped_statuses|
+  - if grouped_statuses.one?
+    - status = grouped_statuses.first
+    - is_playable = status.playable? && can?(current_user, :update_build, @project)
+    %li.build{ class: ("playable" if is_playable) }
+      .curve
+      .build-content
+        = render "projects/#{status.to_partial_path}_pipeline", subject: status
+  - else
+    %li.build
+      .curve
+      .build-content
+        = render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..4e7a6f1af081b83b8da5c831e93c9b85ffad9c5d
--- /dev/null
+++ b/app/views/projects/commit/_pipeline_status_group.html.haml
@@ -0,0 +1,11 @@
+- group_status = CommitStatus.where(id: subject).status
+= render_status_with_link('build', group_status)
+.dropdown.inline
+  %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+    %span.ci-status-text
+      = name
+    %span.badge= subject.size
+  %ul.dropdown-menu.grouped-pipeline-dropdown
+    .arrow
+    - subject.each do |status|
+      = render "projects/#{status.to_partial_path}_pipeline", subject: status
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
index 576d0bec51bb334a744edb024d56468bcea8b2ff..409f4701e4bc317d4020edd9c11499e67e0670c0 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -1,10 +1,7 @@
-%li.build
-  .curve
-  .build-content
-    - if subject.target_url
-      - link_to subject.target_url do
-        = render_status_with_link('commit status', subject.status)
-        %span.ci-status-text= subject.name
-    - else
-      = render_status_with_link('commit status', subject.status)
-      %span.ci-status-text= subject.name
+- if subject.target_url
+  = link_to subject.target_url do
+    = render_status_with_link('commit status', subject.status)
+    %span.ci-status-text= subject.name
+- else
+  = render_status_with_link('commit status', subject.status)
+  %span.ci-status-text= subject.name
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 49e6e2361b1d7a8e3a4ccddf9a4c4f5935c35a9e..650b410595cd3b60313e1bda5dbd920db929c92c 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -3,9 +3,13 @@ class Gitlab::Seeder::Pipelines
   BUILDS = [
     { name: 'build:linux', stage: 'build', status: :success },
     { name: 'build:osx', stage: 'build', status: :success },
-    { name: 'rspec:linux', stage: 'test', status: :success },
-    { name: 'rspec:windows', stage: 'test', status: :success },
-    { name: 'rspec:windows', stage: 'test', status: :success },
+    { name: 'rspec:linux 0 3', stage: 'test', status: :success },
+    { name: 'rspec:linux 1 3', stage: 'test', status: :success },
+    { name: 'rspec:linux 2 3', stage: 'test', status: :success },
+    { name: 'rspec:windows 0 3', stage: 'test', status: :success },
+    { name: 'rspec:windows 1 3', stage: 'test', status: :success },
+    { name: 'rspec:windows 2 3', stage: 'test', status: :success },
+    { name: 'rspec:windows 2 3', stage: 'test', status: :success },
     { name: 'rspec:osx', stage: 'test', status_event: :success },
     { name: 'spinach:linux', stage: 'test', status: :success },
     { name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true},
diff --git a/db/migrate/20140502125220_migrate_repo_size.rb b/db/migrate/20140502125220_migrate_repo_size.rb
index 84463727b3b8a7cdf925594384c87361a488153c..e8de7ccf3db39dce3575d533a7fc1cf600510ae4 100644
--- a/db/migrate/20140502125220_migrate_repo_size.rb
+++ b/db/migrate/20140502125220_migrate_repo_size.rb
@@ -1,12 +1,15 @@
 # rubocop:disable all
 class MigrateRepoSize < ActiveRecord::Migration
+  DOWNTIME = false
+
   def up
     project_data = execute('SELECT projects.id, namespaces.path AS namespace_path, projects.path AS project_path FROM projects LEFT JOIN namespaces ON projects.namespace_id = namespaces.id')
 
     project_data.each do |project|
       id = project['id']
       namespace_path = project['namespace_path'] || ''
-      path = File.join(Gitlab.config.gitlab_shell.repos_path, namespace_path, project['project_path'] + '.git')
+      repos_path = Gitlab.config.gitlab_shell['repos_path'] || Gitlab.config.repositories.storages.default
+      path = File.join(repos_path, namespace_path, project['project_path'] + '.git')
 
       begin
         repo = Gitlab::Git::Repository.new(path)
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index c835ebc2d449a2eca852061f935d3976814c1ff4..c40cdd55ea5e9127475c2557a99c0e7bf8762732 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -105,7 +105,8 @@ What is important is that each job is run independently from each other.
 
 If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
 Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
-the link under **Settings > CI settings** in your project.
+a "CI Lint" button to go to this page under **Pipelines > Pipelines** and
+**Pipelines > Builds** in your project.
 
 For more information and a complete `.gitlab-ci.yml` syntax, please read
 [the documentation on .gitlab-ci.yml](../yaml/README.md).
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 1498cb361c8718d28768129e705729c74bc2c21a..f1b75298180723e3ca526e87f806ebcafa2841d2 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -63,7 +63,7 @@ The following table depicts the various user permission levels in a project.
 | Force push to protected branches [^2] |         |            |             |          |        |
 | Remove protected branches [^2]        |         |            |             |          |        |
 
-[^1]: If **Allow guest to access builds** is enabled in CI settings
+[^1]: If **Public pipelines** is enabled in **Project Settings > CI/CD Pipelines**
 [^2]: Not allowed for Guest, Reporter, Developer, Master, or Owner
 
 ## Group
diff --git a/doc/workflow/lfs/lfs_administration.md b/doc/workflow/lfs/lfs_administration.md
index 9dc1e9b47e36348e3940d2a6deb4f045e1b9db85..b3c73e947f03510e47c8a82e29b388d3d5edfb1e 100644
--- a/doc/workflow/lfs/lfs_administration.md
+++ b/doc/workflow/lfs/lfs_administration.md
@@ -45,5 +45,5 @@ In `config/gitlab.yml`:
 * Currently, storing GitLab Git LFS objects on a non-local storage (like S3 buckets)
   is not supported
 * Currently, removing LFS objects from GitLab Git LFS storage is not supported
-* LFS authentications via SSH is not supported for the time being
-* Only compatible with the GitLFS client versions 1.1.0 or 1.0.2.
+* LFS authentications via SSH was added with GitLab 8.12
+* Only compatible with the GitLFS client versions 1.1.0 and up, or 1.0.2.
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index 9fe065fa68003895c1395b87a0b57f2502d0e18d..1a4f213a792d4eec1d5ada4897a4feb272cefcbe 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -35,6 +35,10 @@ Documentation for GitLab instance administrators is under [LFS administration do
   credentials store is recommended
 * Git LFS always assumes HTTPS so if you have GitLab server on HTTP you will have
   to add the URL to Git config manually (see #troubleshooting)
+  
+>**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+ still goes over HTTP, but now the SSH client passes the correct credentials
+ to the Git LFS client, so no action is required by the user.
 
 ## Using Git LFS
 
@@ -132,6 +136,10 @@ git config --add lfs.url "http://gitlab.example.com/group/project.git/info/lfs"
 
 ### Credentials are always required when pushing an object
 
+>**Note**: With 8.12 GitLab added LFS support to SSH. The Git LFS communication
+ still goes over HTTP, but now the SSH client passes the correct credentials
+ to the Git LFS client, so no action is required by the user.
+
 Given that Git LFS uses HTTP Basic Authentication to authenticate the user pushing
 the LFS object on every push for every object, user HTTPS credentials are required.
 
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 2610fd329d6a207f494b2a89e215ceb5e2c9ab28..865379c51c476e0eafe7c97633ad5c9a384464ad 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -82,6 +82,19 @@ module API
         response
       end
 
+      post "/lfs_authenticate" do
+        status 200
+
+        key = Key.find(params[:key_id])
+        token_handler = Gitlab::LfsToken.new(key)
+
+        {
+          username: token_handler.actor_name,
+          lfs_token: token_handler.generate,
+          repository_http_path: project.http_url_to_repo
+        }
+      end
+
       get "/merge_request_urls" do
         ::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
       end
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index b1427f412b01f0cb813e0b5e7530f6dd09b627fd..b14c4e565d596ca88962904378bed7a2e4fb3e63 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -1,23 +1,28 @@
 module Gitlab
   module Auth
-    Result = Struct.new(:user, :project, :type, :capabilities) do
-      def succeeded?
-        user.present? || [:ci].include?(type)
+    Result = Struct.new(:actor, :project, :type, :capabilities) do
+      def success?
+        actor.present? || type == :ci
       end
     end
 
+    class MissingPersonalTokenError < StandardError; end
+
     class << self
       def find_for_git_client(login, password, project:, ip:)
         raise "Must provide an IP for rate limiting" if ip.nil?
 
-        result = service_access_token_check(login, password, project) ||
+        result =
+          service_request_check(login, password, project) ||
           build_access_token_check(login, password) ||
           user_with_password_for_git(login, password) ||
           oauth_access_token_check(login, password) ||
+          lfs_token_check(login, password) ||
           personal_access_token_check(login, password) ||
           Result.new
 
-        rate_limit!(ip, success: result.succeeded?, login: login)
+        rate_limit!(ip, success: result.success?, login: login)
+
         result
       end
 
@@ -59,7 +64,7 @@ module Gitlab
 
       private
 
-      def service_access_token_check(login, password, project)
+      def service_request_check(login, password, project)
         matched_login = /(?<service>^[a-zA-Z]*-ci)-token$/.match(login)
 
         return unless project && matched_login.present?
@@ -81,14 +86,9 @@ module Gitlab
         user = find_with_user_password(login, password)
         return unless user
 
-        type =
-          if user.two_factor_enabled?
-            :missing_personal_token
-          else
-            :gitlab_or_ldap
-          end
+        raise Gitlab::Auth::MissingPersonalTokenError if user.two_factor_enabled?
 
-        Result.new(user, nil, type, full_capabilities)
+        Result.new(user, nil, :gitlab_or_ldap, full_capabilities)
       end
 
       def oauth_access_token_check(login, password)
@@ -105,9 +105,24 @@ module Gitlab
         if login && password
           user = User.find_by_personal_access_token(password)
           validation = User.by_login(login)
-          if user && user == validation
-            Result.new(user, nil, :personal_token, full_capabilities)
+          Result.new(user, nil, :personal_token, full_capabilities) if user.present? && user == validation
+        end
+      end
+
+      def lfs_token_check(login, password)
+        deploy_key_matches = login.match(/\Alfs\+deploy-key-(\d+)\z/)
+
+        actor =
+          if deploy_key_matches
+            DeployKey.find(deploy_key_matches[1])
+          else
+            User.by_login(login)
           end
+
+        if actor
+          token_handler = Gitlab::LfsToken.new(actor)
+
+          Result.new(actor, nil, token_handler.type, read_capabilities) if Devise.secure_compare(token_handler.value, password)
         end
       end
 
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index 6d9379acf25869458600edb6d4daf552a22fbd93..d1e33ea86784f0960796d6a67774000619ae5d0f 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -22,10 +22,6 @@ module Gitlab
 
       private
 
-      def repos_path
-        Gitlab.config.gitlab_shell.repos_path
-      end
-
       def path_to_repo
         @project.repository.path_to_repo
       end
diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f492754b1c8a999e5dc17d6be4e3a015e3abade1
--- /dev/null
+++ b/lib/gitlab/lfs_token.rb
@@ -0,0 +1,50 @@
+module Gitlab
+  class LfsToken
+    attr_accessor :actor
+
+    TOKEN_LENGTH = 50
+    EXPIRY_TIME = 1800
+
+    def initialize(actor)
+      @actor =
+        case actor
+        when DeployKey, User
+          actor
+        when Key
+          actor.user
+        else
+          raise 'Bad Actor'
+        end
+    end
+
+    def generate
+      token = Devise.friendly_token(TOKEN_LENGTH)
+
+      Gitlab::Redis.with do |redis|
+        redis.set(redis_key, token, ex: EXPIRY_TIME)
+      end
+
+      token
+    end
+
+    def value
+      Gitlab::Redis.with do |redis|
+        redis.get(redis_key)
+      end
+    end
+
+    def type
+      actor.is_a?(User) ? :lfs_token : :lfs_deploy_token
+    end
+
+    def actor_name
+      actor.is_a?(User) ? actor.username : "lfs+deploy-key-#{actor.id}"
+    end
+
+    private
+
+    def redis_key
+      "gitlab:lfs_token:#{actor.class.name.underscore}_#{actor.id}" if actor
+    end
+  end
+end
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 4a83740621a698129f07e2e1f8e7c947c0bebe4c..d0f4e5469edc02d713c5c0827fb4fea4272fa840 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -13,10 +13,12 @@ feature 'issuable templates', feature: true, js: true do
 
   context 'user creates an issue using templates' do
     let(:template_content) { 'this is a test "bug" template' }
+    let(:longtemplate_content) { %Q(this\n\n\n\n\nis\n\n\n\n\na\n\n\n\n\nbug\n\n\n\n\ntemplate) }
     let(:issue) { create(:issue, author: user, assignee: user, project: project) }
 
     background do
       project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
+      project.repository.commit_file(user, '.gitlab/issue_templates/test.md', longtemplate_content, 'added issue template', 'master', false)
       visit edit_namespace_project_issue_path project.namespace, project, issue
       fill_in :'issue[title]', with: 'test issue title'
     end
@@ -27,6 +29,17 @@ feature 'issuable templates', feature: true, js: true do
       preview_template
       save_changes
     end
+
+    it 'updates height of markdown textarea' do
+      start_height = page.evaluate_script('$(".markdown-area").outerHeight()')
+
+      select_template 'test'
+      wait_for_ajax
+
+      end_height = page.evaluate_script('$(".markdown-area").outerHeight()')
+      
+      expect(end_height).not_to eq(start_height)
+    end
   end
 
   context 'user creates a merge request using templates' do
diff --git a/spec/features/todos/todos_filtering_spec.rb b/spec/features/todos/todos_filtering_spec.rb
index 83cf306437d781fe5343ffda636db3aed3fab213..b9e66243d84def29c102d1612973bcd8b2e612b7 100644
--- a/spec/features/todos/todos_filtering_spec.rb
+++ b/spec/features/todos/todos_filtering_spec.rb
@@ -29,8 +29,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
       fill_in 'Search projects', with: project_1.name_with_namespace
       click_link project_1.name_with_namespace
     end
+
     wait_for_ajax
-    expect('.prepend-top-default').not_to have_content project_2.name_with_namespace
+
+    expect(page).to     have_content project_1.name_with_namespace
+    expect(page).not_to have_content project_2.name_with_namespace
   end
 
   it 'filters by author' do
@@ -39,8 +42,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
       fill_in 'Search authors', with: user_1.name
       click_link user_1.name
     end
+
     wait_for_ajax
-    expect('.prepend-top-default').not_to have_content user_2.name
+
+    expect(find('.todos-list')).to     have_content user_1.name
+    expect(find('.todos-list')).not_to have_content user_2.name
   end
 
   it 'filters by type' do
@@ -48,8 +54,11 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
     within '.dropdown-menu-type' do
       click_link 'Issue'
     end
+
     wait_for_ajax
-    expect('.prepend-top-default').not_to have_content ' merge request !'
+
+    expect(find('.todos-list')).to     have_content issue.to_reference
+    expect(find('.todos-list')).not_to have_content merge_request.to_reference
   end
 
   it 'filters by action' do
@@ -57,7 +66,10 @@ describe 'Dashboard > User filters todos', feature: true, js: true do
     within '.dropdown-menu-action' do
       click_link 'Assigned'
     end
+
     wait_for_ajax
-    expect('.prepend-top-default').not_to have_content ' mentioned '
+
+    expect(find('.todos-list')).to     have_content ' assigned you '
+    expect(find('.todos-list')).not_to have_content ' mentioned '
   end
 end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index b665517bbb0aebbee2ce67ff7816a048e8cd5f8a..c09ab1dbd5743d4e89896126af31566548e2b5df 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -59,6 +59,24 @@ describe Gitlab::Auth, lib: true do
       expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_capabilities))
     end
 
+    it 'recognizes user lfs tokens' do
+      user = create(:user)
+      ip = 'ip'
+      token = Gitlab::LfsToken.new(user).generate
+
+      expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
+      expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, :lfs_token))
+    end
+
+    it 'recognizes deploy key lfs tokens' do
+      key = create(:deploy_key)
+      ip = 'ip'
+      token = Gitlab::LfsToken.new(key).generate
+
+      expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}")
+      expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, :lfs_deploy_token))
+    end
+
     it 'recognizes OAuth tokens' do
       user = create(:user)
       application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
@@ -73,7 +91,7 @@ describe Gitlab::Auth, lib: true do
       login = 'foo'
       ip = 'ip'
 
-      expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login)
+      expect(gl_auth).to receive(:rate_limit!).with(ip, success: nil, login: login)
       expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new)
     end
   end
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9f04f67e0a8ddc5e6289fdb555df4f3e4a0d9bc2
--- /dev/null
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe Gitlab::LfsToken, lib: true do
+  describe '#generate and #value' do
+    shared_examples 'an LFS token generator' do
+      it 'returns a randomly generated token' do
+        token = handler.generate
+
+        expect(token).not_to be_nil
+        expect(token).to be_a String
+        expect(token.length).to eq 50
+      end
+
+      it 'returns the correct token based on the key' do
+        token = handler.generate
+
+        expect(handler.value).to eq(token)
+      end
+    end
+
+    context 'when the actor is a user' do
+      let(:actor) { create(:user) }
+      let(:handler) { described_class.new(actor) }
+
+      it_behaves_like 'an LFS token generator'
+
+      it 'returns the correct username' do
+        expect(handler.actor_name).to eq(actor.username)
+      end
+
+      it 'returns the correct token type' do
+        expect(handler.type).to eq(:lfs_token)
+      end
+    end
+
+    context 'when the actor is a deploy key' do
+      let(:actor) { create(:deploy_key) }
+      let(:handler) { described_class.new(actor) }
+
+      it_behaves_like 'an LFS token generator'
+
+      it 'returns the correct username' do
+        expect(handler.actor_name).to eq("lfs+deploy-key-#{actor.id}")
+      end
+
+      it 'returns the correct token type' do
+        expect(handler.type).to eq(:lfs_deploy_token)
+      end
+    end
+  end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index fbf945c757ce44c1edab8874788df2c5b88419fd..f1857f846dcf6a3aa3608016812a6da905d67520 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -373,8 +373,8 @@ describe Ci::Pipeline, models: true do
   end
 
   describe '#execute_hooks' do
-    let!(:build_a) { create_build('a') }
-    let!(:build_b) { create_build('b') }
+    let!(:build_a) { create_build('a', 0) }
+    let!(:build_b) { create_build('b', 1) }
 
     let!(:hook) do
       create(:project_hook, project: project, pipeline_events: enabled)
@@ -398,7 +398,7 @@ describe Ci::Pipeline, models: true do
             build_b.enqueue
           end
 
-          it 'receive a pending event once' do
+          it 'receives a pending event once' do
             expect(WebMock).to have_requested_pipeline_hook('pending').once
           end
         end
@@ -411,7 +411,7 @@ describe Ci::Pipeline, models: true do
             build_b.run
           end
 
-          it 'receive a running event once' do
+          it 'receives a running event once' do
             expect(WebMock).to have_requested_pipeline_hook('running').once
           end
         end
@@ -422,11 +422,21 @@ describe Ci::Pipeline, models: true do
             build_b.success
           end
 
-          it 'receive a success event once' do
+          it 'receives a success event once' do
             expect(WebMock).to have_requested_pipeline_hook('success').once
           end
         end
 
+        context 'when stage one failed' do
+          before do
+            build_a.drop
+          end
+
+          it 'receives a failed event once' do
+            expect(WebMock).to have_requested_pipeline_hook('failed').once
+          end
+        end
+
         def have_requested_pipeline_hook(status)
           have_requested(:post, hook.url).with do |req|
             json_body = JSON.parse(req.body)
@@ -450,8 +460,12 @@ describe Ci::Pipeline, models: true do
       end
     end
 
-    def create_build(name)
-      create(:ci_build, :created, pipeline: pipeline, name: name)
+    def create_build(name, stage_idx)
+      create(:ci_build,
+             :created,
+             pipeline: pipeline,
+             name: name,
+             stage_idx: stage_idx)
     end
   end
 end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index fcfa3138ce50b16cc2b0f5e7ae34acf15da63e5d..2f1baff5d66a9c83a5a29c25db8bc2dd8094e378 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -40,7 +40,7 @@ describe CommitStatus, models: true do
       it { is_expected.to be_falsey }
     end
 
-    %w(running success failed).each do |status|
+    %w[running success failed].each do |status|
       context "if commit status is #{status}" do
         before { commit_status.status = status }
 
@@ -48,7 +48,7 @@ describe CommitStatus, models: true do
       end
     end
 
-    %w(pending canceled).each do |status|
+    %w[pending canceled].each do |status|
       context "if commit status is #{status}" do
         before { commit_status.status = status }
 
@@ -60,7 +60,7 @@ describe CommitStatus, models: true do
   describe '#active?' do
     subject { commit_status.active? }
 
-    %w(pending running).each do |state|
+    %w[pending running].each do |state|
       context "if commit_status.status is #{state}" do
         before { commit_status.status = state }
 
@@ -68,7 +68,7 @@ describe CommitStatus, models: true do
       end
     end
 
-    %w(success failed canceled).each do |state|
+    %w[success failed canceled].each do |state|
       context "if commit_status.status is #{state}" do
         before { commit_status.status = state }
 
@@ -80,7 +80,7 @@ describe CommitStatus, models: true do
   describe '#complete?' do
     subject { commit_status.complete? }
 
-    %w(success failed canceled).each do |state|
+    %w[success failed canceled].each do |state|
       context "if commit_status.status is #{state}" do
         before { commit_status.status = state }
 
@@ -88,7 +88,7 @@ describe CommitStatus, models: true do
       end
     end
 
-    %w(pending running).each do |state|
+    %w[pending running].each do |state|
       context "if commit_status.status is #{state}" do
         before { commit_status.status = state }
 
@@ -187,7 +187,7 @@ describe CommitStatus, models: true do
       subject { CommitStatus.where(pipeline: pipeline).stages }
 
       it 'returns ordered list of stages' do
-        is_expected.to eq(%w(build test deploy))
+        is_expected.to eq(%w[build test deploy])
       end
     end
 
@@ -223,4 +223,33 @@ describe CommitStatus, models: true do
       expect(commit_status.commit).to eq project.commit
     end
   end
+
+  describe '#group_name' do
+    subject { commit_status.group_name }
+
+    tests = {
+      'rspec:windows' => 'rspec:windows',
+      'rspec:windows 0' => 'rspec:windows 0',
+      'rspec:windows 0 test' => 'rspec:windows 0 test',
+      'rspec:windows 0 1' => 'rspec:windows',
+      'rspec:windows 0 1 name' => 'rspec:windows name',
+      'rspec:windows 0/1' => 'rspec:windows',
+      'rspec:windows 0/1 name' => 'rspec:windows name',
+      'rspec:windows 0:1' => 'rspec:windows',
+      'rspec:windows 0:1 name' => 'rspec:windows name',
+      'rspec:windows 10000 20000' => 'rspec:windows',
+      'rspec:windows 0 : / 1' => 'rspec:windows',
+      'rspec:windows 0 : / 1 name' => 'rspec:windows name',
+      '0 1 name ruby' => 'name ruby',
+      '0 :/ 1 name ruby' => 'name ruby'
+    }
+
+    tests.each do |name, group_name|
+      it "'#{name}' puts in '#{group_name}'" do
+        commit_status.name = name
+
+        is_expected.to eq(group_name)
+      end
+    end
+  end
 end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 46d1b868782d0a9e3ae7f3e2535f35f49985d158..46e8e6f11697e5eed5a3e98aa1da50cddcc358b5 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -100,6 +100,43 @@ describe API::API, api: true  do
     end
   end
 
+  describe "POST /internal/lfs_authenticate" do
+    before do
+      project.team << [user, :developer]
+    end
+
+    context 'user key' do
+      it 'returns the correct information about the key' do
+        lfs_auth(key.id, project)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['username']).to eq(user.username)
+        expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value)
+
+        expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
+      end
+
+      it 'returns a 404 when the wrong key is provided' do
+        lfs_auth(nil, project)
+
+        expect(response).to have_http_status(404)
+      end
+    end
+
+    context 'deploy key' do
+      let(:key) { create(:deploy_key) }
+
+      it 'returns the correct information about the key' do
+        lfs_auth(key.id, project)
+
+        expect(response).to have_http_status(200)
+        expect(json_response['username']).to eq("lfs+deploy-key-#{key.id}")
+        expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value)
+        expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
+      end
+    end
+  end
+
   describe "GET /internal/discover" do
     it do
       get(api("/internal/discover"), key_id: key.id, secret_token: secret_token)
@@ -389,4 +426,13 @@ describe API::API, api: true  do
       protocol: 'ssh'
     )
   end
+
+  def lfs_auth(key_id, project)
+    post(
+      api("/internal/lfs_authenticate"),
+      key_id: key_id,
+      secret_token: secret_token,
+      project: project.path_with_namespace
+    )
+  end
 end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 7bf43a03f233f0d28a5edf9f3d178ec09dac6265..8ead97efb012026fe2b61cc36ba2ff2f11170a47 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -246,6 +246,18 @@ describe 'Git LFS API and storage' do
           end
         end
 
+        context 'when deploy key is authorized' do
+          let(:key) { create(:deploy_key) }
+          let(:authorization) { authorize_deploy_key }
+
+          let(:update_permissions) do
+            project.deploy_keys << key
+            project.lfs_objects << lfs_object
+          end
+
+          it_behaves_like 'responds with a file'
+        end
+
         context 'when build is authorized' do
           let(:authorization) { authorize_ci_project }
 
@@ -906,6 +918,10 @@ describe 'Git LFS API and storage' do
     ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
   end
 
+  def authorize_deploy_key
+    ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).generate)
+  end
+
   def fork_project(project, user, object = nil)
     allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
     Projects::ForkService.new(project, user, {}).execute
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..c5b16c1c304290d3fe13cb5260d1afd66c4b4117
--- /dev/null
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe 'projects/pipelines/show' do
+  include Devise::TestHelpers
+
+  let(:project) { create(:project) }
+  let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
+
+  before do
+    controller.prepend_view_path('app/views/projects')
+
+    create_build('build', 0, 'build')
+    create_build('test', 1, 'rspec 0:2')
+    create_build('test', 1, 'rspec 1:2')
+    create_build('test', 1, 'audit')
+    create_build('deploy', 2, 'production')
+
+    create(:generic_commit_status, pipeline: pipeline, stage: 'external', name: 'jenkins', stage_idx: 3)
+
+    assign(:project, project)
+    assign(:pipeline, pipeline)
+
+    allow(view).to receive(:can?).and_return(true)
+  end
+
+  it 'shows a graph with grouped stages' do
+    render
+
+    expect(rendered).to have_css('.pipeline-graph')
+    expect(rendered).to have_css('.grouped-pipeline-dropdown')
+
+    # stages
+    expect(rendered).to have_text('Build')
+    expect(rendered).to have_text('Test')
+    expect(rendered).to have_text('Deploy')
+    expect(rendered).to have_text('External')
+
+    # builds
+    expect(rendered).to have_text('rspec')
+    expect(rendered).to have_text('rspec 0:2')
+    expect(rendered).to have_text('production')
+    expect(rendered).to have_text('jenkins')
+  end
+
+  private
+
+  def create_build(stage, stage_idx, name)
+    create(:ci_build, pipeline: pipeline, stage: stage, stage_idx: stage_idx, name: name)
+  end
+end