Commit 6d12d7ba authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'ph/commitGraphqlPipelines' into 'master'

GraphQL commit type correctly return pipeline for ref

See merge request gitlab-org/gitlab!19758
parents 467fa2ce f4280ae8
...@@ -38,7 +38,14 @@ export default { ...@@ -38,7 +38,14 @@ export default {
path: this.currentPath.replace(/^\//, ''), path: this.currentPath.replace(/^\//, ''),
}; };
}, },
update: data => data.project.repository.tree.lastCommit, update: data => {
const pipelines = data.project.repository.tree.lastCommit.pipelines.edges;
return {
...data.project.repository.tree.lastCommit,
pipeline: pipelines.length && pipelines[0].node,
};
},
context: { context: {
isSingleRequest: true, isSingleRequest: true,
}, },
...@@ -61,7 +68,7 @@ export default { ...@@ -61,7 +68,7 @@ export default {
computed: { computed: {
statusTitle() { statusTitle() {
return sprintf(s__('Commits|Commit: %{commitText}'), { return sprintf(s__('Commits|Commit: %{commitText}'), {
commitText: this.commit.latestPipeline.detailedStatus.text, commitText: this.commit.pipeline.detailedStatus.text,
}); });
}, },
isLoading() { isLoading() {
...@@ -127,14 +134,14 @@ export default { ...@@ -127,14 +134,14 @@ export default {
<div v-if="commit.signatureHtml" v-html="commit.signatureHtml"></div> <div v-if="commit.signatureHtml" v-html="commit.signatureHtml"></div>
<div class="ci-status-link"> <div class="ci-status-link">
<gl-link <gl-link
v-if="commit.latestPipeline" v-if="commit.pipeline"
v-gl-tooltip.left v-gl-tooltip.left
:href="commit.latestPipeline.detailedStatus.detailsPath" :href="commit.pipeline.detailedStatus.detailsPath"
:title="statusTitle" :title="statusTitle"
class="js-commit-pipeline" class="js-commit-pipeline"
> >
<ci-icon <ci-icon
:status="commit.latestPipeline.detailedStatus" :status="commit.pipeline.detailedStatus"
:size="24" :size="24"
:aria-label="statusTitle" :aria-label="statusTitle"
/> />
......
...@@ -14,7 +14,9 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) { ...@@ -14,7 +14,9 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
webUrl webUrl
} }
signatureHtml signatureHtml
latestPipeline { pipelines(ref: $ref, first: 1) {
edges {
node {
detailedStatus { detailedStatus {
detailsPath detailsPath
icon icon
...@@ -27,4 +29,6 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) { ...@@ -27,4 +29,6 @@ query pathLastCommit($projectPath: ID!, $path: String, $ref: String!) {
} }
} }
} }
}
}
} }
...@@ -10,6 +10,14 @@ module Resolvers ...@@ -10,6 +10,14 @@ module Resolvers
end end
end end
def self.last
@last ||= Class.new(self) do
def resolve(**args)
super.last
end
end
end
def self.resolver_complexity(args, child_complexity:) def self.resolver_complexity(args, child_complexity:)
complexity = 1 complexity = 1
complexity += 1 if args[:sort] complexity += 1 if args[:sort]
......
# frozen_string_literal: true
module Resolvers
class CommitPipelinesResolver < BaseResolver
include ::ResolvesPipelines
alias_method :commit, :object
def resolve(**args)
resolve_pipelines(commit.project, args.merge!({ sha: commit.sha }))
end
end
end
...@@ -29,12 +29,16 @@ module Types ...@@ -29,12 +29,16 @@ module Types
field :author, type: Types::UserType, null: true, field :author, type: Types::UserType, null: true,
description: 'Author of the commit' description: 'Author of the commit'
field :pipelines, Types::Ci::PipelineType.connection_type,
null: true,
description: 'Pipelines of the commit ordered latest first',
resolver: Resolvers::CommitPipelinesResolver
field :latest_pipeline, field :latest_pipeline,
type: Types::Ci::PipelineType, type: Types::Ci::PipelineType,
null: true, null: true,
description: "Latest pipeline of the commit", description: "Latest pipeline of the commit",
resolve: -> (obj, ctx, args) do deprecation_reason: 'use pipelines',
Gitlab::Graphql::Loaders::PipelineForShaLoader.new(obj.project, obj.sha).find_last resolver: Resolvers::CommitPipelinesResolver.last
end
end end
end end
...@@ -139,13 +139,68 @@ type Commit { ...@@ -139,13 +139,68 @@ type Commit {
""" """
Latest pipeline of the commit Latest pipeline of the commit
""" """
latestPipeline: Pipeline latestPipeline(
"""
Filter pipelines by the ref they are run for
"""
ref: String
"""
Filter pipelines by the sha of the commit they are run for
"""
sha: String
"""
Filter pipelines by their status
"""
status: PipelineStatusEnum
): Pipeline @deprecated(reason: "use pipelines")
""" """
Raw commit message Raw commit message
""" """
message: String message: String
"""
Pipelines of the commit ordered latest first
"""
pipelines(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
Filter pipelines by the ref they are run for
"""
ref: String
"""
Filter pipelines by the sha of the commit they are run for
"""
sha: String
"""
Filter pipelines by their status
"""
status: PipelineStatusEnum
): PipelineConnection
""" """
SHA1 ID of the commit SHA1 ID of the commit
""" """
......
...@@ -10210,15 +10210,44 @@ ...@@ -10210,15 +10210,44 @@
"name": "latestPipeline", "name": "latestPipeline",
"description": "Latest pipeline of the commit", "description": "Latest pipeline of the commit",
"args": [ "args": [
{
"name": "status",
"description": "Filter pipelines by their status",
"type": {
"kind": "ENUM",
"name": "PipelineStatusEnum",
"ofType": null
},
"defaultValue": null
},
{
"name": "ref",
"description": "Filter pipelines by the ref they are run for",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "sha",
"description": "Filter pipelines by the sha of the commit they are run for",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
], ],
"type": { "type": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Pipeline", "name": "Pipeline",
"ofType": null "ofType": null
}, },
"isDeprecated": false, "isDeprecated": true,
"deprecationReason": null "deprecationReason": "use pipelines"
}, },
{ {
"name": "message", "name": "message",
...@@ -10234,6 +10263,89 @@ ...@@ -10234,6 +10263,89 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "pipelines",
"description": "Pipelines of the commit ordered latest first",
"args": [
{
"name": "status",
"description": "Filter pipelines by their status",
"type": {
"kind": "ENUM",
"name": "PipelineStatusEnum",
"ofType": null
},
"defaultValue": null
},
{
"name": "ref",
"description": "Filter pipelines by the ref they are run for",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "sha",
"description": "Filter pipelines by the sha of the commit they are run for",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "PipelineConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "sha", "name": "sha",
"description": "SHA1 ID of the commit", "description": "SHA1 ID of the commit",
...@@ -10306,6 +10418,118 @@ ...@@ -10306,6 +10418,118 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "PipelineConnection",
"description": "The connection type for Pipeline.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PipelineEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Pipeline",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "PipelineEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Pipeline",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "Pipeline", "name": "Pipeline",
...@@ -13205,118 +13429,6 @@ ...@@ -13205,118 +13429,6 @@
], ],
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "PipelineConnection",
"description": "The connection type for Pipeline.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PipelineEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Pipeline",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "pageInfo",
"description": "Information to aid in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "PageInfo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "PipelineEdge",
"description": "An edge in a connection.",
"fields": [
{
"name": "cursor",
"description": "A cursor for use in pagination.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "node",
"description": "The item at the end of the edge.",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Pipeline",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "IssueConnection", "name": "IssueConnection",
......
# frozen_string_literal: true
module Gitlab
module Graphql
module Loaders
class PipelineForShaLoader
attr_accessor :project, :sha
def initialize(project, sha)
@project, @sha = project, sha
end
def find_last
BatchLoader::GraphQL.for(sha).batch(key: project) do |shas, loader, args|
pipelines = args[:key].ci_pipelines.latest_for_shas(shas)
pipelines.each do |pipeline|
loader.call(pipeline.sha, pipeline)
end
end
end
end
end
end
end
...@@ -9,8 +9,6 @@ describe 'user reads pipeline status', :js do ...@@ -9,8 +9,6 @@ describe 'user reads pipeline status', :js do
let(:x110_pipeline) { create_pipeline('x1.1.0', 'failed') } let(:x110_pipeline) { create_pipeline('x1.1.0', 'failed') }
before do before do
stub_feature_flags(vue_file_list: false)
project.add_maintainer(user) project.add_maintainer(user)
project.repository.add_tag(user, 'x1.1.0', 'v1.1.0') project.repository.add_tag(user, 'x1.1.0', 'v1.1.0')
...@@ -25,7 +23,7 @@ describe 'user reads pipeline status', :js do ...@@ -25,7 +23,7 @@ describe 'user reads pipeline status', :js do
visit project_tree_path(project, expected_pipeline.ref) visit project_tree_path(project, expected_pipeline.ref)
wait_for_requests wait_for_requests
page.within('.blob-commit-info') do page.within('.commit-detail') do
expect(page).to have_link('', href: project_pipeline_path(project, expected_pipeline)) expect(page).to have_link('', href: project_pipeline_path(project, expected_pipeline))
expect(page).to have_selector(".ci-status-icon-#{expected_pipeline.status}") expect(page).to have_selector(".ci-status-icon-#{expected_pipeline.status}")
end end
......
...@@ -17,7 +17,7 @@ function createCommitData(data = {}) { ...@@ -17,7 +17,7 @@ function createCommitData(data = {}) {
avatarUrl: 'https://test.com', avatarUrl: 'https://test.com',
webUrl: 'https://test.com/test', webUrl: 'https://test.com/test',
}, },
latestPipeline: { pipeline: {
detailedStatus: { detailedStatus: {
detailsPath: 'https://test.com/pipeline', detailsPath: 'https://test.com/pipeline',
icon: 'failed', icon: 'failed',
...@@ -74,7 +74,7 @@ describe('Repository last commit component', () => { ...@@ -74,7 +74,7 @@ describe('Repository last commit component', () => {
}); });
it('hides pipeline components when pipeline does not exist', () => { it('hides pipeline components when pipeline does not exist', () => {
factory(createCommitData({ latestPipeline: null })); factory(createCommitData({ pipeline: null }));
expect(vm.find('.js-commit-pipeline').exists()).toBe(false); expect(vm.find('.js-commit-pipeline').exists()).toBe(false);
}); });
......
...@@ -13,6 +13,14 @@ describe Resolvers::BaseResolver do ...@@ -13,6 +13,14 @@ describe Resolvers::BaseResolver do
end end
end end
let(:last_resolver) do
Class.new(described_class) do
def resolve(**args)
[1, 2]
end
end
end
describe '.single' do describe '.single' do
it 'returns a subclass from the resolver' do it 'returns a subclass from the resolver' do
expect(resolver.single.superclass).to eq(resolver) expect(resolver.single.superclass).to eq(resolver)
...@@ -29,6 +37,22 @@ describe Resolvers::BaseResolver do ...@@ -29,6 +37,22 @@ describe Resolvers::BaseResolver do
end end
end end
describe '.last' do
it 'returns a subclass from the resolver' do
expect(last_resolver.last.superclass).to eq(last_resolver)
end
it 'returns the same subclass every time' do
expect(last_resolver.last.object_id).to eq(last_resolver.last.object_id)
end
it 'returns a resolver that gives the last result from the original resolver' do
result = resolve(last_resolver.last)
expect(result).to eq(2)
end
end
context 'when field is a connection' do context 'when field is a connection' do
it 'increases complexity based on arguments' do it 'increases complexity based on arguments' do
field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 1) field = Types::BaseField.new(name: 'test', type: GraphQL::STRING_TYPE.connection_type, resolver_class: described_class, null: false, max_page_size: 1)
......
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::CommitPipelinesResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let(:commit) { create(:commit, project: project) }
let_it_be(:current_user) { create(:user) }
let!(:pipeline) do
create(
:ci_pipeline,
project: project,
sha: commit.id,
ref: 'master',
status: 'success'
)
end
let!(:pipeline2) do
create(
:ci_pipeline,
project: project,
sha: commit.id,
ref: 'master',
status: 'failed'
)
end
let!(:pipeline3) do
create(
:ci_pipeline,
project: project,
sha: commit.id,
ref: 'my_branch',
status: 'failed'
)
end
before do
commit.project.add_developer(current_user)
end
def resolve_pipelines
resolve(described_class, obj: commit, ctx: { current_user: current_user }, args: { ref: 'master' })
end
it 'resolves pipelines for commit and ref' do
pipelines = resolve_pipelines
expect(pipelines).to eq([pipeline2, pipeline])
end
end
...@@ -10,7 +10,7 @@ describe GitlabSchema.types['Commit'] do ...@@ -10,7 +10,7 @@ describe GitlabSchema.types['Commit'] do
it 'contains attributes related to commit' do it 'contains attributes related to commit' do
expect(described_class).to have_graphql_fields( expect(described_class).to have_graphql_fields(
:id, :sha, :title, :description, :message, :authored_date, :id, :sha, :title, :description, :message, :authored_date,
:author, :web_url, :latest_pipeline, :signature_html :author, :web_url, :latest_pipeline, :pipelines, :signature_html
) )
end end
end end
require 'spec_helper'
describe Gitlab::Graphql::Loaders::PipelineForShaLoader do
include GraphqlHelpers
describe '#find_last' do
it 'batch-resolves latest pipeline' do
project = create(:project, :repository)
pipeline1 = create(:ci_pipeline, project: project, ref: project.default_branch, sha: project.commit.sha)
pipeline2 = create(:ci_pipeline, project: project, ref: project.default_branch, sha: project.commit.sha)
pipeline3 = create(:ci_pipeline, project: project, ref: 'improve/awesome', sha: project.commit('improve/awesome').sha)
result = batch_sync(max_queries: 1) do
[pipeline1.sha, pipeline3.sha].map { |sha| described_class.new(project, sha).find_last }
end
expect(result).to contain_exactly(pipeline2, pipeline3)
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