diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..764c8cc9cca665db46334a8e744d7a9866661d84 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -0,0 +1,102 @@
+class Projects::PipelineController < Projects::ApplicationController
+  before_action :ci_commit, except: [:index, :new, :create]
+  before_action :authorize_read_pipeline!
+  before_action :authorize_create_pipeline!, only: [:new, :create]
+  before_action :authorize_update_pipeline!, only: [:retry, :cancel]
+  layout 'project'
+
+  def index
+    @scope = params[:scope]
+    @all_commits = project.ci_commits
+    @commits = @all_commits.order(id: :desc)
+    @commits =
+      case @scope
+      when 'latest'
+        @commits
+      when 'running'
+        @commits.running_or_pending
+      when 'branches'
+        refs = project.repository.branches.map(&:name)
+        ids = @all_commits.where(ref: refs).group(:ref).select('max(id)')
+        @commits.where(id: ids)
+      when 'tags'
+        refs = project.repository.tags.map(&:name)
+        ids = @all_commits.where(ref: refs).group(:ref).select('max(id)')
+        @commits.where(id: ids)
+      else
+        @commits
+      end
+    @commits = @commits.page(params[:page]).per(30)
+  end
+
+  def new
+  end
+
+  def create
+    ref_names = project.repository.ref_names
+    unless ref_names.include?(params[:ref])
+      @error = 'Reference not found'
+      render action: 'new'
+      return
+    end
+
+    commit = project.commit(params[:ref])
+    unless commit
+      @error = 'Commit not found'
+      render action: 'new'
+      return
+    end
+
+    ci_commit = project.ci_commit(commit.id, params[:ref])
+    if ci_commit
+      @error = 'Pipeline already created'
+      render action: 'new'
+      return
+    end
+
+    # Skip creating ci_commit when no gitlab-ci.yml is found
+    commit = project.ci_commits.new(sha: commit.id, ref: params[:ref], before_sha: Gitlab::Git::BLANK_SHA)
+    unless commit.config_processor
+      @error = commit.yaml_errors || 'Missing .gitlab-ci.yml file'
+      render action: 'new'
+      return
+    end
+
+    Ci::Commit.transaction do
+      commit.save!
+      commit.create_builds(params[:ref], false, current_user)
+    end
+
+    redirect_to builds_namespace_project_commit_path(project.namespace, project, commit.id)
+  end
+
+  def show
+    @commit = @ci_commit.commit
+    @builds = @ci_commit.builds
+    @statuses = @ci_commit.statuses
+
+    respond_to do |format|
+      format.html
+    end
+  end
+
+  def retry
+    ci_commit.builds.latest.failed.select(&:retryable?).each(&:retry)
+
+    redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
+  end
+
+  def cancel
+    ci_commit.builds.running_or_pending.each(&:cancel)
+
+    redirect_back_or_default default: namespace_project_pipelines_path(project.namespace, project)
+  end
+
+  def retry_builds
+  end
+  private
+
+  def ci_commit
+    @ci_commit ||= project.ci_commits.find_by!(id: params[:id])
+  end
+end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index c0bf6def7c538994d1de9469cad800e794d5b60a..ec5ac54c2776474c28f9bebc1c6be68136bf4a2c 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -195,6 +195,7 @@ class Ability
         :admin_label,
         :read_commit_status,
         :read_build,
+        :read_pipeline,
       ]
     end
 
@@ -206,6 +207,8 @@ class Ability
         :update_commit_status,
         :create_build,
         :update_build,
+        :create_pipeline,
+        :update_pipeline,
         :create_merge_request,
         :create_wiki,
         :push_code
@@ -234,7 +237,8 @@ class Ability
         :admin_wiki,
         :admin_project,
         :admin_commit_status,
-        :admin_build
+        :admin_build,
+        :admin_pipeline
       ]
     end
 
@@ -277,6 +281,7 @@ class Ability
 
       unless project.builds_enabled
         rules += named_abilities('build')
+        rules += named_abilities('pipeline')
       end
 
       rules
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 86b46e8c75ec33cf5f4be6463a0c0546f4d98740..fcce1b1dc98114d5bbb7e3e9d9b3b5066aa3cf6a 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -39,6 +39,13 @@
           Commits
 
   - if project_nav_tab? :builds
+    = nav_link(controller: %w(ci_commits)) do
+      = link_to project_ci_commits_path(@project), title: 'Pipelines', class: 'shortcuts-builds' do
+        = icon('ship fw')
+        %span
+          Pipelines
+          %span.count.ci_counter= number_with_delimiter(@project.ci_commits.running_or_pending.count(:all))
+
     = nav_link(controller: %w(builds)) do
       = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
         = icon('cubes fw')
diff --git a/app/views/projects/ci/commits/_commit.html.haml b/app/views/projects/ci/commits/_commit.html.haml
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..29efcc9cfdd4528b73f36afe36900674d89c4212 100644
--- a/app/views/projects/ci/commits/_commit.html.haml
+++ b/app/views/projects/ci/commits/_commit.html.haml
@@ -0,0 +1,73 @@
+- status = commit.status
+%tr.commit
+  %td.commit-link
+    = link_to namespace_project_commit_url(@project.namespace, @project, commit), class: "ci-status ci-#{status}" do
+      = ci_icon_for_status(status)
+      %strong ##{commit.id}
+
+  %td
+    %div
+      - if commit.ref
+        = link_to commit.ref, namespace_project_commits_path(@project.namespace, @project, commit.ref)
+      &nbsp;
+      - if commit.tag?
+        %span.label.label-primary tag
+      - if commit.branch?
+        %span.label.label-primary branch
+      - if commit.trigger_requests.any?
+        %span.label.label-primary triggered
+      - if commit.yaml_errors.present?
+        %span.label.label-danger.has-tooltip(title="#{commit.yaml_errors}") yaml invalid
+      - if commit.builds.any?(&:stuck?)
+        %span.label.label-warning stuck
+
+      - if commit_data = commit.commit_data
+        = render 'projects/branches/commit', commit: commit_data, project: @project
+      - else
+        %p
+          Cant find HEAD commit for this branch
+
+    - stages.each do |stage|
+      %td
+        - status = commit.statuses.latest.where(stage: stage).status
+        %span.has-tooltip(title="#{status || "missing"}"){class: "ci-status-icon-#{status || "skipped"}"}
+          = ci_icon_for_status(status || "missing")
+        -#- if status
+        -#  = ci_status_with_icon(status)
+        -#- else
+        -#  = ci_status_with_icon('missing')
+
+  %td
+    - if commit.started_at && commit.finished_at
+      %p
+        #{duration_in_words(commit.finished_at, commit.started_at)}
+    - if commit.finished_at
+      %p
+        #{time_ago_with_tooltip(commit.finished_at)}
+
+  %td.content
+    .controls.hidden-xs.pull-right
+      - artifacts = commit.builds.latest.select { |status| status.artifacts? }
+      - if artifacts.present?
+        .dropdown.inline
+          %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+            = icon('download')
+            %b.caret
+          %ul.dropdown-menu.dropdown-menu-align-right
+            - artifacts.each do |build|
+              %li
+                = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do
+                  %i.fa.fa-download
+                  %span #{build.name}
+        &nbsp;
+
+      - if can?(current_user, :update_pipeline, @project)
+        - if commit.retryable?
+          = link_to retry_namespace_project_ci_commit_path(@project.namespace, @project, commit.id), class: 'btn has-tooltip', title: "Retry", method: :post do
+            = icon("repeat")
+
+        &nbsp;
+
+        - if commit.active?
+          = link_to cancel_namespace_project_ci_commit_path(@project.namespace, @project, commit.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
+            = icon("remove cred")
diff --git a/app/views/projects/ci_commits/_header_title.html.haml b/app/views/projects/ci_commits/_header_title.html.haml
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..27c125ca40f07f5208e073f09793dc65d7c22643 100644
--- a/app/views/projects/ci_commits/_header_title.html.haml
+++ b/app/views/projects/ci_commits/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Pipelines", project_ci_commits_path(@project))
diff --git a/app/views/projects/ci_commits/index.html.haml b/app/views/projects/ci_commits/index.html.haml
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0347c220382299bedc7b184da18cbcb469aa2b16 100644
--- a/app/views/projects/ci_commits/index.html.haml
+++ b/app/views/projects/ci_commits/index.html.haml
@@ -0,0 +1,65 @@
+- page_title "Pipelines"
+= render "header_title"
+
+.top-area
+  %ul.nav-links
+    %li{class: ('active' if @scope.nil?)}
+      = link_to project_ci_commits_path(@project) do
+        All
+        %span.badge.js-totalbuilds-count
+          = number_with_delimiter(@all_commits.count(:id))
+
+    %li{class: ('active' if @scope == 'branches')}
+      = link_to project_ci_commits_path(@project, scope: :branches) do
+        Branches
+        %span.badge.js-running-count
+          = number_with_delimiter(@all_commits.running_or_pending.count(:id))
+
+    %li{class: ('active' if @scope == 'tags')}
+      = link_to project_ci_commits_path(@project, scope: :tags) do
+        Tags
+        %span.badge.js-running-count
+          = number_with_delimiter(@all_commits.running_or_pending.count(:id))
+
+    %li{class: ('active' if @scope == 'running')}
+      = link_to project_ci_commits_path(@project, scope: :running) do
+        Failed
+        %span.badge.js-running-count
+          = number_with_delimiter(@all_commits.running_or_pending.count(:id))
+
+  .nav-controls
+    - if can? current_user, :create_pipeline, @project
+      = link_to new_namespace_project_ci_commit_path(@project.namespace, @project), class: 'btn btn-create' do
+        = icon('plus')
+        New
+
+    - if can?(current_user, :update_build, @project)
+      - unless @repository.gitlab_ci_yml
+        = link_to 'Get started with Pipelines', help_page_path('ci/quick_start', 'README'), class: 'btn btn-info'
+
+      = link_to ci_lint_path, class: 'btn btn-default' do
+        = icon('wrench')
+        %span CI Lint
+
+.gray-content-block
+  Pipelines for #{(@scope || 'changes')} on this project
+
+%ul.content-list
+  - stages = @commits.stages
+  - if @commits.blank?
+    %li
+      .nothing-here-block No pipelines to show
+  - else
+    .table-holder
+      %table.table.builds
+        %tbody
+          %th Pipeline ID
+          %th Commit
+          - @commits.stages.each do |stage|
+            %th
+              = stage.titleize
+          %th
+          %th
+        = render @commits.includes(:statuses).includes(:builds), commit_sha: true, stage: true, allow_retry: true, stages: stages
+
+    = paginate @commits, theme: 'gitlab'
diff --git a/app/views/projects/ci_commits/new.html.haml b/app/views/projects/ci_commits/new.html.haml
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..e9a22bbb1579abdca42454ebee539878b95997b9 100644
--- a/app/views/projects/ci_commits/new.html.haml
+++ b/app/views/projects/ci_commits/new.html.haml
@@ -0,0 +1,25 @@
+- page_title "New Pipeline"
+= render "header_title"
+
+- if @error
+  .alert.alert-danger
+    %button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
+    = @error
+%h3.page-title
+  New Pipeline
+%hr
+
+= form_tag namespace_project_ci_commits_path, method: :post, id: "new-pipeline-form", class: "form-horizontal js-create-branch-form js-requires-input" do
+  .form-group
+    = label_tag :ref, 'Create for', class: 'control-label'
+    .col-sm-10
+      = text_field_tag :ref, params[:ref] || @project.default_branch, required: true, tabindex: 2, class: 'form-control'
+      .help-block Existing branch name, tag
+  .form-actions
+    = button_tag 'Create pipeline', class: 'btn btn-create', tabindex: 3
+    = link_to 'Cancel', namespace_project_ci_commits_path(@project.namespace, @project), class: 'btn btn-cancel'
+
+:javascript
+  var availableRefs = #{@project.repository.ref_names.to_json};
+
+  new NewBranchForm($('.js-create-branch-form'), availableRefs)
diff --git a/config/routes.rb b/config/routes.rb
index 842fbb99843a92ce465ad7cf9cb14856d0b6c969..841b3f262721cecffaa3aba2a02900757d1ac2c9 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -654,6 +654,13 @@ Rails.application.routes.draw do
         resource :variables, only: [:show, :update]
         resources :triggers, only: [:index, :create, :destroy]
 
+        resources :pipelines, only: [:index, :new, :create] do
+          member do
+            post :cancel
+            post :retry
+          end
+        end
+
         resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
           collection do
             post :cancel_all