From 6f6119b7389ef7b5e13f2800611d5d7a806e41a5 Mon Sep 17 00:00:00 2001
From: Kamil Trzcinski <ayufan@ayufan.eu>
Date: Thu, 10 Nov 2016 15:32:23 +0100
Subject: [PATCH] Support pipelines API

Pass `updated_at` to get only incremental changes since last update
---
 .../projects/pipelines_controller.rb          | 16 ++++
 app/models/ci/pipeline.rb                     | 29 +++++--
 app/models/commit_status.rb                   | 11 +--
 app/serializers/pipeline_action_entity.rb     | 14 ++++
 app/serializers/pipeline_artifact_entity.rb   | 14 ++++
 app/serializers/pipeline_entity.rb            | 84 +++++++++++++++++++
 app/serializers/pipeline_serializer.rb        |  3 +
 app/serializers/pipeline_stage_entity.rb      | 19 +++++
 app/serializers/request_aware_entity.rb       |  8 ++
 .../projects/ci/pipelines/_pipeline.html.haml |  7 +-
 app/views/projects/commit/_pipeline.html.haml |  2 +-
 .../projects/commit/_pipelines_list.haml      |  2 +-
 app/views/projects/pipelines/index.html.haml  |  3 +-
 13 files changed, 190 insertions(+), 22 deletions(-)
 create mode 100644 app/serializers/pipeline_action_entity.rb
 create mode 100644 app/serializers/pipeline_artifact_entity.rb
 create mode 100644 app/serializers/pipeline_entity.rb
 create mode 100644 app/serializers/pipeline_serializer.rb
 create mode 100644 app/serializers/pipeline_stage_entity.rb

diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 371cc3787fb..0b3503c6848 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -11,6 +11,22 @@ class Projects::PipelinesController < Projects::ApplicationController
 
     @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
     @pipelines_count = PipelinesFinder.new(project).execute.count
+    @last_updated = params[:updated_at]
+
+    respond_to do |format|
+      format.html
+      format.json do
+         render json: {
+           pipelines: PipelineSerializer.new(project: @project).
+            represent(@pipelines, current_user: current_user, last_updated: @last_updated),
+           updated_at: Time.now,
+           count: {
+             all: @pipelines_count,
+             running_or_pending: @running_or_pending_count
+           }
+         }
+      end
+    end
   end
 
   def new
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 3fee6c18770..37c2c86e3a3 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -98,19 +98,38 @@ module Ci
       sha[0...8]
     end
 
-    def self.stages
-      # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
-      CommitStatus.where(pipeline: pluck(:id)).stages
-    end
-
     def self.total_duration
       where.not(duration: nil).sum(:duration)
     end
 
+    def stages
+      statuses.group('stage').select(:stage)
+        .order('max(stage_idx)')
+    end
+
+    def stages_with_statuses
+      status_sql = statuses.latest.where('stage=sg.stage').status_sql
+
+      stages_with_statuses = CommitStatus.from(self.stages, :sg).
+        pluck('sg.stage', status_sql)
+
+      stages_with_statuses.map do |stage|
+        OpenStruct.new(
+          name: stage.first,
+          status: stage.last,
+          pipeline: self
+        )
+      end
+    end
+
     def stages_with_latest_statuses
       statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
     end
 
+    def artifacts
+      builds.latest.with_artifacts_not_expired
+    end
+
     def project_id
       project.id
     end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index d159fc6c5c7..c0b7f44fa9e 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -119,16 +119,7 @@ class CommitStatus < ActiveRecord::Base
 
   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')
-  end
-
-  def self.stages_status
-    # We execute subquery for each stage to calculate a stage status
-    statuses = unscoped.from(all, :sg).group('stage').pluck('sg.stage', all.where('stage=sg.stage').status_sql)
-    statuses.inject({}) do |h, k|
-      h[k.first] = k.last
-      h
-    end
+    unscoped.from(all, :sg).group('stage').order('max(stage_idx)', 'stage').select('sg.stage')
   end
 
   def failed_but_allowed?
diff --git a/app/serializers/pipeline_action_entity.rb b/app/serializers/pipeline_action_entity.rb
new file mode 100644
index 00000000000..d45341f09d2
--- /dev/null
+++ b/app/serializers/pipeline_action_entity.rb
@@ -0,0 +1,14 @@
+class PipelineActionEntity < Grape::Entity
+  include RequestAwareEntity
+
+  expose :name do |build|
+    build.name.humanize
+  end
+
+  expose :url do |build|
+    play_namespace_project_build_path(
+      pipeline.project.namespace,
+      pipeline.project,
+      build)
+  end
+end
diff --git a/app/serializers/pipeline_artifact_entity.rb b/app/serializers/pipeline_artifact_entity.rb
new file mode 100644
index 00000000000..01393dbea2d
--- /dev/null
+++ b/app/serializers/pipeline_artifact_entity.rb
@@ -0,0 +1,14 @@
+class PipelineArtifactEntity < Grape::Entity
+  include RequestAwareEntity
+
+  expose :name do |build|
+    build.name
+  end
+
+  expose :url do |build|
+    download_namespace_project_build_artifacts_path(
+      pipeline.project.namespace,
+      pipeline.project,
+      build)
+  end
+end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
new file mode 100644
index 00000000000..7afce8fd15b
--- /dev/null
+++ b/app/serializers/pipeline_entity.rb
@@ -0,0 +1,84 @@
+class PipelineEntity < Grape::Entity
+  include RequestAwareEntity
+
+  expose :id
+  expose :user, if: -> (pipeline, opts) { created?(pipeline, opts) }, using: UserEntity
+
+  expose :status
+  expose :duration
+  expose :finished_at
+  expose :stages_with_statuses, as: :stages, if: -> (pipeline, opts) { updated?(pipeline, opts) }, using: PipelineStageEntity
+  expose :artifacts, if: -> (pipeline, opts) { updated?(pipeline, opts) }, using: PipelineArtifactEntity
+  expose :manual_actions, if: -> (pipeline, opts) { updated?(pipeline, opts) }, using: PipelineActionEntity
+
+  expose :flags, if: -> (pipeline, opts) { created?(pipeline, opts) } do
+    expose :latest?, as: :latest
+    expose :triggered?, as: :triggered
+    expose :yaml_errors?, as: :yaml_errors do |pipeline|
+      pipeline.yaml_errors.present?
+    end
+    expose :stuck?, as: :stuck do |pipeline|
+      pipeline.builds.any?(&:stuck?)
+    end
+  end
+
+  expose :ref, if: -> (pipeline, opts) { created?(pipeline, opts) } do
+    expose :name do |pipeline|
+      pipeline.ref
+    end
+
+    expose :ref_url do |pipeline|
+      namespace_project_tree_url(
+        pipeline.project.namespace,
+        pipeline.project,
+        id: pipeline.ref)
+    end
+
+    expose :tag?
+  end
+
+  expose :commit, if: -> (pipeline, opts) { created?(pipeline, opts) } do
+    expose :short_sha
+
+    expose :sha_url do |pipeline|
+      namespace_project_commit_path(
+        pipeline.project.namespace,
+        pipeline.project,
+        pipeline.sha)
+    end
+
+    expose :title do |pipeline|
+      pipeline.commit.try(:title)
+    end
+
+    expose :author, using: UserEntity do |pipeline|
+      pipeline.commit.try(:author)
+    end
+  end
+
+  expose :retry_url, if: -> (pipeline, opts) { updated?(pipeline, opts) } do |pipeline|
+    can?(current_user, :update_pipeline, pipeline.project) &&
+      pipeline.retryable? &&
+      retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id)
+  end
+
+  expose :cancel_url, if: -> (pipeline, opts) { updated?(pipeline, opts) } do |pipeline|
+    can?(current_user, :update_pipeline, pipeline.project) &&
+      pipeline.cancelable? &&
+      cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id)
+  end
+
+  private
+
+  def last_updated(opts)
+    opts.fetch(:last_updated)
+  end
+
+  def created?(pipeline, opts)
+    !last_updated(opts) || pipeline.created_at > last_updated(opts)
+  end
+
+  def updated?(pipeline, opts)
+    !last_updated(opts) || pipeline.updated_at > last_updated(opts)
+  end
+end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
new file mode 100644
index 00000000000..f7abbec7d45
--- /dev/null
+++ b/app/serializers/pipeline_serializer.rb
@@ -0,0 +1,3 @@
+class PipelineSerializer < BaseSerializer
+  entity PipelineEntity
+end
diff --git a/app/serializers/pipeline_stage_entity.rb b/app/serializers/pipeline_stage_entity.rb
new file mode 100644
index 00000000000..230ef8a22da
--- /dev/null
+++ b/app/serializers/pipeline_stage_entity.rb
@@ -0,0 +1,19 @@
+class PipelineStageEntity < Grape::Entity
+  include RequestAwareEntity
+
+  expose :name do |stage|
+    stage.name
+  end
+
+  expose :status do |stage|
+    stage.status || 'not found'
+  end
+
+  expose :url do |stage|
+    namespace_project_pipeline_path(
+      stage.pipeline.project.namespace,
+      stage.pipeline.project,
+      stage.pipeline.id,
+      anchor: stage.name)
+  end
+end
diff --git a/app/serializers/request_aware_entity.rb b/app/serializers/request_aware_entity.rb
index ff8c1142abc..7a096d9d5a8 100644
--- a/app/serializers/request_aware_entity.rb
+++ b/app/serializers/request_aware_entity.rb
@@ -8,4 +8,12 @@ module RequestAwareEntity
   def request
     @options.fetch(:request)
   end
+
+  def current_user
+    @options.fetch(:current_user)
+  end
+
+  def can?(object, action, subject)
+    Ability.allowed?(object, action, subject)
+  end
 end
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 2a2d24be736..50817f28d78 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -43,9 +43,10 @@
 
   - stages_status = pipeline.statuses.latest.stages_status
   %td.stage-cell
-    - stages.each do |stage|
-      - status = stages_status[stage]
-      - tooltip = "#{stage.titleize}: #{status || 'not found'}"
+    - pipeline.statuses.latest.stages_status.each do |stage|
+      - name = stage.first
+      - status = stage.last
+      - tooltip = "#{name.titleize}: #{status || 'not found'}"
       - if status
         .stage-container
           = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index d6916fb7f1a..516893fc6e5 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -66,5 +66,5 @@
         - if pipeline.project.build_coverage_enabled?
           %th Coverage
         %th
-    - pipeline.statuses.relevant.stages.each do |stage|
+    - pipeline.stages.each do |stage|
       = render 'projects/commit/ci_stage', stage: stage, statuses: pipeline.statuses.relevant.where(stage: stage)
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 2dc91a9b762..7f42fde0fea 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -12,4 +12,4 @@
           %th Stages
           %th
           %th
-        = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, show_commit: false
+        = render pipelines, commit_sha: true, stage: true, allow_retry: true, show_commit: false
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 6f70b239826..4f9fb699abc 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -37,7 +37,6 @@
           %span CI Lint
 
   %div.content-list.pipelines{"data-project-id": "#{@project.id}", "data-count": "#{@pipelines_count}"}
-    - stages = @pipelines.stages
     - if @pipelines.blank?
       %div
         .nothing-here-block No pipelines to show
@@ -52,7 +51,7 @@
             %th
             %th.hidden-xs
 
-          = render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
+          = render @pipelines, commit_sha: true, stage: true, allow_retry: true
       = paginate @pipelines, theme: 'gitlab'
     - else
       .vue-pipelines-index
-- 
GitLab