Commit c93bc628 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'eb-fix-ci-status-indicator-for-warnings' into 'master'

Batch load pipelines instead of statuses

See merge request gitlab-org/gitlab!17034
parents 67353ae2 ecf18fcf
......@@ -72,7 +72,7 @@ class Projects::CommitsController < Projects::ApplicationController
@repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
end
@commits = @commits.with_pipeline_status
@commits = @commits.with_latest_pipeline(@ref)
@commits = set_commits_for_rendering(@commits)
end
......
......@@ -82,7 +82,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
# Get commits from repository
# or from cache if already merged
@commits =
set_commits_for_rendering(@merge_request.commits.with_pipeline_status)
set_commits_for_rendering(@merge_request.commits.with_latest_pipeline)
render json: { html: view_to_html_string('projects/merge_requests/_commits') }
end
......
......@@ -64,7 +64,7 @@ module CiStatusHelper
def ci_icon_for_status(status, size: 16)
if detailed_status?(status)
return sprite_icon(status.icon)
return sprite_icon(status.icon, size: size)
end
icon_name =
......@@ -96,23 +96,29 @@ module CiStatusHelper
sprite_icon(icon_name, size: size)
end
def ci_icon_class_for_status(status)
group = detailed_status?(status) ? status.group : status.dasherize
"ci-status-icon-#{group}"
end
def pipeline_status_cache_key(pipeline_status)
"pipeline-status/#{pipeline_status.sha}-#{pipeline_status.status}"
end
def render_commit_status(commit, ref: nil, tooltip_placement: 'left')
def render_commit_status(commit, status, ref: nil, tooltip_placement: 'left')
project = commit.project
path = pipelines_project_commit_path(project, commit, ref: ref)
render_status_with_link(
commit.status(ref),
status,
path,
tooltip_placement: tooltip_placement,
icon_size: 24)
end
def render_status_with_link(status, path = nil, type: _('pipeline'), tooltip_placement: 'left', cssclass: '', container: 'body', icon_size: 16)
klass = "ci-status-link ci-status-icon-#{status.dasherize} d-inline-flex #{cssclass}"
klass = "ci-status-link #{ci_icon_class_for_status(status)} d-inline-flex #{cssclass}"
title = "#{type.titleize}: #{ci_label_for_status(status)}"
data = { toggle: 'tooltip', placement: tooltip_placement, container: container }
......@@ -127,6 +133,7 @@ module CiStatusHelper
def detailed_status?(status)
status.respond_to?(:text) &&
status.respond_to?(:group) &&
status.respond_to?(:label) &&
status.respond_to?(:icon)
end
......
......@@ -281,16 +281,16 @@ module Ci
end
end
# Returns a Hash containing the latest pipeline status for every given
# Returns a Hash containing the latest pipeline for every given
# commit.
#
# The keys of this Hash are the commit SHAs, the values the statuses.
# The keys of this Hash are the commit SHAs, the values the pipelines.
#
# commits - The list of commit SHAs to get the status for.
# commits - The list of commit SHAs to get the pipelines for.
# ref - The ref to scope the data to (e.g. "master"). If the ref is not
# given we simply get the latest status for the commits, regardless
# of what refs their pipelines belong to.
def self.latest_status_per_commit(commits, ref = nil)
# given we simply get the latest pipelines for the commits, regardless
# of what refs the pipelines belong to.
def self.latest_pipeline_per_commit(commits, ref = nil)
p1 = arel_table
p2 = arel_table.alias
......@@ -304,15 +304,14 @@ module Ci
cond = cond.and(p1[:ref].eq(p2[:ref])) if ref
join = p1.join(p2, Arel::Nodes::OuterJoin).on(cond)
relation = select(:sha, :status)
.where(sha: commits)
relation = where(sha: commits)
.where(p2[:id].eq(nil))
.joins(join.join_sources)
relation = relation.where(ref: ref) if ref
relation.each_with_object({}) do |row, hash|
hash[row[:sha]] = row[:status]
relation.each_with_object({}) do |pipeline, hash|
hash[pipeline.sha] = pipeline
end
end
......
......@@ -119,10 +119,22 @@ class Commit
@raw = raw_commit
@project = project
@statuses = {}
@gpg_commit = Gitlab::Gpg::Commit.new(self) if project
end
delegate \
:pipelines,
:last_pipeline,
:latest_pipeline,
:latest_pipeline_for_project,
:set_latest_pipeline_for_ref,
:status,
to: :with_pipeline
def with_pipeline
@with_pipeline ||= CommitWithPipeline.new(self)
end
def id
raw.id
end
......@@ -301,30 +313,6 @@ class Commit
)
end
def pipelines
project.ci_pipelines.where(sha: sha)
end
def last_pipeline
strong_memoize(:last_pipeline) do
pipelines.last
end
end
def status(ref = nil)
return @statuses[ref] if @statuses.key?(ref)
@statuses[ref] = status_for_project(ref, project)
end
def status_for_project(ref, pipeline_project)
pipeline_project.ci_pipelines.latest_status_per_commit(id, ref)[id]
end
def set_status_for_ref(ref, status)
@statuses[ref] = status
end
def signature
return @signature if defined?(@signature)
......
......@@ -34,6 +34,20 @@ class CommitCollection
end
end
# Returns the collection with the latest pipeline for every commit pre-set.
#
# Setting the pipeline for each commit ahead of time removes the need for running
# a query for every commit we're displaying.
def with_latest_pipeline(ref = nil)
pipelines = project.ci_pipelines.latest_pipeline_per_commit(map(&:id), ref)
each do |commit|
commit.set_latest_pipeline_for_ref(ref, pipelines[commit.id])
end
self
end
def unenriched
commits.reject(&:gitaly_commit?)
end
......@@ -65,20 +79,6 @@ class CommitCollection
self
end
# Sets the pipeline status for every commit.
#
# Setting this status ahead of time removes the need for running a query for
# every commit we're displaying.
def with_pipeline_status
statuses = project.ci_pipelines.latest_status_per_commit(map(&:id), ref)
each do |commit|
commit.set_status_for_ref(ref, statuses[commit.id])
end
self
end
def respond_to_missing?(message, inc_private = false)
commits.respond_to?(message, inc_private)
end
......
# frozen_string_literal: true
class CommitWithPipeline < SimpleDelegator
include Presentable
def initialize(commit)
@latest_pipelines = {}
super(commit)
end
def pipelines
project.ci_pipelines.where(sha: sha)
end
def last_pipeline
strong_memoize(:last_pipeline) do
pipelines.last
end
end
def latest_pipeline(ref = nil)
@latest_pipelines.fetch(ref) do |ref|
@latest_pipelines[ref] = latest_pipeline_for_project(ref, project)
end
end
def latest_pipeline_for_project(ref, pipeline_project)
pipeline_project.ci_pipelines.latest_pipeline_per_commit(id, ref)[id]
end
def set_latest_pipeline_for_ref(ref, pipeline)
@latest_pipelines[ref] = pipeline
end
def status(ref = nil)
latest_pipeline(ref)&.status
end
end
......@@ -6,11 +6,15 @@ class CommitPresenter < Gitlab::View::Presenter::Delegated
presents :commit
def status_for(ref)
can?(current_user, :read_commit_status, commit.project) && commit.status(ref)
return unless can?(current_user, :read_commit_status, commit.project)
commit.latest_pipeline(ref)&.detailed_status(current_user)
end
def any_pipelines?
can?(current_user, :read_pipeline, commit.project) && commit.pipelines.any?
return false unless can?(current_user, :read_pipeline, commit.project)
commit.pipelines.any?
end
def web_url
......
......@@ -35,8 +35,8 @@ class CommitEntity < API::Entities::Commit
pipeline_project = options[:pipeline_project] || commit.project
next unless pipeline_ref && pipeline_project
status = commit.status_for_project(pipeline_ref, pipeline_project)
next unless status
pipeline = commit.latest_pipeline_for_project(pipeline_ref, pipeline_project)
next unless pipeline&.status
pipelines_project_commit_path(pipeline_project, commit.id, ref: pipeline_ref)
end
......
......@@ -6,7 +6,8 @@
- merge_request = local_assigns.fetch(:merge_request, nil)
- project = local_assigns.fetch(:project) { merge_request&.project }
- ref = local_assigns.fetch(:ref) { merge_request&.source_branch }
- commit_status = commit.present(current_user: current_user).status_for(ref)
- commit = commit.present(current_user: current_user)
- commit_status = commit.status_for(ref)
- link = commit_path(project, commit, merge_request: merge_request)
......@@ -48,7 +49,7 @@
= render partial: 'projects/commit/ajax_signature', locals: { commit: commit }
- if commit_status
= render_commit_status(commit, ref: ref)
= render_commit_status(commit, commit_status, ref: ref)
.js-commit-pipeline-status{ data: { endpoint: pipelines_project_commit_path(project, commit.id, ref: ref) } }
......
......@@ -63,7 +63,7 @@
- if pipeline_status && can?(current_user, :read_cross_project) && project.pipeline_status.has_status? && can?(current_user, :read_build, project)
- pipeline_path = pipelines_project_commit_path(project.pipeline_status.project, project.pipeline_status.sha, ref: project.pipeline_status.ref)
%span.icon-wrapper.pipeline-status
= render 'ci/status/icon', status: project.commit.last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
= render 'ci/status/icon', status: project.last_pipeline.detailed_status(current_user), tooltip_placement: 'top', path: pipeline_path
- if project.archived
%span.d-flex.icon-wrapper.badge.badge-warning archived
......
---
title: Show correct CI indicator when build succeeded with warnings.
merge_request: 17034
author:
type: fixed
......@@ -1966,40 +1966,57 @@ describe Ci::Pipeline, :mailer do
end
end
describe '.latest_status_per_commit' do
describe '.latest_pipeline_per_commit' do
let(:project) { create(:project) }
before do
pairs = [
%w[success ref1 123],
%w[manual master 123],
%w[failed ref 456]
]
pairs.each do |(status, ref, sha)|
create(
:ci_empty_pipeline,
status: status,
ref: ref,
sha: sha,
project: project
)
end
let!(:commit_123_ref_master) do
create(
:ci_empty_pipeline,
status: 'success',
ref: 'master',
sha: '123',
project: project
)
end
let!(:commit_123_ref_develop) do
create(
:ci_empty_pipeline,
status: 'success',
ref: 'develop',
sha: '123',
project: project
)
end
let!(:commit_456_ref_test) do
create(
:ci_empty_pipeline,
status: 'success',
ref: 'test',
sha: '456',
project: project
)
end
context 'without a ref' do
it 'returns a Hash containing the latest status per commit for all refs' do
expect(described_class.latest_status_per_commit(%w[123 456]))
.to eq({ '123' => 'manual', '456' => 'failed' })
it 'returns a Hash containing the latest pipeline per commit for all refs' do
result = described_class.latest_pipeline_per_commit(%w[123 456])
expect(result).to match(
'123' => commit_123_ref_develop,
'456' => commit_456_ref_test
)
end
it 'only includes the status of the given commit SHAs' do
expect(described_class.latest_status_per_commit(%w[123]))
.to eq({ '123' => 'manual' })
it 'only includes the latest pipeline of the given commit SHAs' do
result = described_class.latest_pipeline_per_commit(%w[123])
expect(result).to match(
'123' => commit_123_ref_develop
)
end
context 'when there are two pipelines for a ref and SHA' do
it 'returns the status of the latest pipeline' do
let!(:commit_123_ref_master_latest) do
create(
:ci_empty_pipeline,
status: 'failed',
......@@ -2007,17 +2024,25 @@ describe Ci::Pipeline, :mailer do
sha: '123',
project: project
)
end
it 'returns the latest pipeline' do
result = described_class.latest_pipeline_per_commit(%w[123])
expect(described_class.latest_status_per_commit(%w[123]))
.to eq({ '123' => 'failed' })
expect(result).to match(
'123' => commit_123_ref_master_latest
)
end
end
end
context 'with a ref' do
it 'only includes the pipelines for the given ref' do
expect(described_class.latest_status_per_commit(%w[123 456], 'master'))
.to eq({ '123' => 'manual' })
result = described_class.latest_pipeline_per_commit(%w[123 456], 'master')
expect(result).to match(
'123' => commit_123_ref_master
)
end
end
end
......
......@@ -51,6 +51,30 @@ describe CommitCollection do
end
end
describe '#with_latest_pipeline' do
let!(:pipeline) do
create(
:ci_empty_pipeline,
ref: 'master',
sha: commit.id,
status: 'success',
project: project
)
end
let(:collection) { described_class.new(project, [commit]) }
it 'sets the latest pipeline for every commit so no additional queries are necessary' do
commits = collection.with_latest_pipeline('master')
recorder = ActiveRecord::QueryRecorder.new do
expect(commits.map { |c| c.latest_pipeline('master') })
.to eq([pipeline])
end
expect(recorder.count).to be_zero
end
end
describe 'enrichment methods' do
let(:gitaly_commit) { commit }
let(:hash_commit) { Commit.from_hash(gitaly_commit.to_hash, project) }
......@@ -128,27 +152,6 @@ describe CommitCollection do
end
end
describe '#with_pipeline_status' do
it 'sets the pipeline status for every commit so no additional queries are necessary' do
create(
:ci_empty_pipeline,
ref: 'master',
sha: commit.id,
status: 'success',
project: project
)
collection = described_class.new(project, [commit])
collection.with_pipeline_status
recorder = ActiveRecord::QueryRecorder.new do
expect(commit.status).to eq('success')
end
expect(recorder.count).to be_zero
end
end
describe '#respond_to_missing?' do
it 'returns true when the underlying Array responds to the message' do
collection = described_class.new(project, [])
......
......@@ -462,78 +462,6 @@ eos
end
end
describe '#last_pipeline' do
let!(:first_pipeline) do
create(:ci_empty_pipeline,
project: project,
sha: commit.sha,
status: 'success')
end
let!(:second_pipeline) do
create(:ci_empty_pipeline,
project: project,
sha: commit.sha,
status: 'success')
end
it 'returns last pipeline' do
expect(commit.last_pipeline).to eq second_pipeline
end
end
describe '#status' do
context 'without ref argument' do
before do
%w[success failed created pending].each do |status|
create(:ci_empty_pipeline,
project: project,
sha: commit.sha,
status: status)
end
end
it 'gives compound status from latest pipelines' do
expect(commit.status).to eq(Ci::Pipeline.latest_status)
expect(commit.status).to eq('pending')
end
end
context 'when a particular ref is specified' do
let!(:pipeline_from_master) do
create(:ci_empty_pipeline,
project: project,
sha: commit.sha,
ref: 'master',
status: 'failed')
end
let!(:pipeline_from_fix) do
create(:ci_empty_pipeline,
project: project,
sha: commit.sha,
ref: 'fix',
status: 'success')
end
it 'gives pipelines from a particular branch' do
expect(commit.status('master')).to eq(pipeline_from_master.status)
expect(commit.status('fix')).to eq(pipeline_from_fix.status)
end
it 'gives compound status from latest pipelines if ref is nil' do
expect(commit.status(nil)).to eq(pipeline_from_fix.status)
end
end
end
describe '#set_status_for_ref' do
it 'sets the status for a given reference' do
commit.set_status_for_ref('master', 'failed')
expect(commit.status('master')).to eq('failed')
end
end
describe '#participants' do
let(:user1) { build(:user) }
let(:user2) { build(:user) }
......
# frozen_string_literal: true
require 'spec_helper'
describe CommitWithPipeline do
let(:project) { create(:project, :public, :repository) }
let(:commit) { described_class.new(project.commit) }
describe '#last_pipeline' do
let!(:first_pipeline) do
create(:ci_empty_pipeline,
project: project,
sha: commit.sha,
status: 'success')
end
let!(:second_pipeline) do
create(:ci_empty_pipeline,
project: project,
sha: commit.sha,
status: 'success')
end
it 'returns last pipeline' do
expect(commit.last_pipeline).to eq second_pipeline
end
end
describe '#latest_pipeline' do
let(:pipeline) { double }
shared_examples_for 'fetching latest pipeline' do |ref|
it 'returns the latest pipeline for the project' do
expect(commit)
.to receive(:latest_pipeline_for_project)
.with(ref, project)
.and_return(pipeline)
expect(result).to eq(pipeline)
end
it "returns the memoized pipeline for the key of #{ref}" do
commit.set_latest_pipeline_for_ref(ref, pipeline)
expect(commit)
.not_to receive(:latest_pipeline_for_project)
expect(result).to eq(pipeline)
end
end
context 'without ref argument' do
let(:result) { commit.latest_pipeline }
it_behaves_like 'fetching latest pipeline', nil
end
context 'when a particular ref is specified' do
let(:result) { commit.latest_pipeline('master') }
it_behaves_like 'fetching latest pipeline', 'master'
end
end
describe '#latest_pipeline_for_project' do
let(:project_pipelines) { double }
let(:pipeline_project) { double }
let(:pipeline) { double }
let(:ref) { 'master' }
let(:result) { commit.latest_pipeline_for_project(ref, pipeline_project) }
before do
allow(pipeline_project).to receive(:ci_pipelines).and_return(project_pipelines)
end
it 'returns the latest pipeline of the commit for the given ref and project' do
expect(project_pipelines)
.to receive(:latest_pipeline_per_commit)
.with(commit.id, ref)
.and_return(commit.id => pipeline)
expect(result).to eq(pipeline)
end
end
describe '#set_latest_pipeline_for_ref' do
let(:pipeline) { double }
it 'sets the latest pipeline for a given reference' do
commit.set_latest_pipeline_for_ref('master', pipeline)
expect(commit.latest_pipeline('master')).to eq(pipeline)
end
end
describe "#status" do
it 'returns the status of the latest pipeline for the given ref' do
expect(commit)
.to receive(:latest_pipeline)
.with('master')
.and_return(double(status: 'success'))
expect(commit.status('master')).to eq('success')
end
it 'returns nil when latest pipeline is not present for the given ref' do
expect(commit)
.to receive(:latest_pipeline)
.with('master')
.and_return(nil)
expect(commit.status('master')).to eq(nil)
end
it 'returns the status of the latest pipeline when no ref is given' do
expect(commit)
.to receive(:latest_pipeline)
.with(nil)
.and_return(double(status: 'success'))
expect(commit.status).to eq('success')
end
end
end
......@@ -17,15 +17,19 @@ describe CommitPresenter do
end
it 'returns commit status for ref' do
expect(commit).to receive(:status).with('ref').and_return('test')
pipeline = double
status = double
expect(subject).to eq('test')
expect(commit).to receive(:latest_pipeline).with('ref').and_return(pipeline)
expect(pipeline).to receive(:detailed_status).with(user).and_return(status)
expect(subject).to eq(status)
end
end
context 'when user can not read_commit_status' do
it 'is false' do
is_expected.to eq(false)
it 'is nil' do
is_expected.to eq(nil)
end
end
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment