diff --git a/app/graphql/types/commit_type.rb b/app/graphql/types/commit_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..d73dd73affd01681e818b47e356a97942eb5bacf --- /dev/null +++ b/app/graphql/types/commit_type.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Types + class CommitType < BaseObject + graphql_name 'Commit' + + authorize :download_code + + present_using CommitPresenter + + field :id, type: GraphQL::ID_TYPE, null: false + field :sha, type: GraphQL::STRING_TYPE, null: false + field :title, type: GraphQL::STRING_TYPE, null: true + field :description, type: GraphQL::STRING_TYPE, null: true + field :message, type: GraphQL::STRING_TYPE, null: true + field :authored_date, type: Types::TimeType, null: true + field :web_url, type: GraphQL::STRING_TYPE, null: false + + # models/commit lazy loads the author by email + field :author, type: Types::UserType, null: true + + field :latest_pipeline, + type: Types::Ci::PipelineType, + null: true, + description: "Latest pipeline for this commit", + resolve: -> (obj, ctx, args) do + Gitlab::Graphql::Loaders::PipelineForShaLoader.new(obj.project, obj.sha).find_last + end + end +end diff --git a/app/graphql/types/tree/tree_type.rb b/app/graphql/types/tree/tree_type.rb index 1ee93ed95420af34273d44e7b06f6185bad1e745..cbc448a0695a16a2a92365cf4fcbef8d8c1c6814 100644 --- a/app/graphql/types/tree/tree_type.rb +++ b/app/graphql/types/tree/tree_type.rb @@ -4,6 +4,11 @@ module Types class TreeType < BaseObject graphql_name 'Tree' + # Complexity 10 as it triggers a Gitaly call on each render + field :last_commit, Types::CommitType, null: true, complexity: 10, resolve: -> (tree, args, ctx) do + tree.repository.last_commit_for_path(tree.sha, tree.path) + end + field :trees, Types::Tree::TreeEntryType.connection_type, null: false, resolve: -> (obj, args, ctx) do Gitlab::Graphql::Representation::TreeEntry.decorate(obj.trees, obj.repository) end diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index 3727a9861aa43318a5ca4e45766d5794c0fdabca..fd5aa21617460e57be52c16ca1fcc624d6ae0704 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -295,6 +295,11 @@ module Ci end end + def self.latest_for_shas(shas) + max_id_per_sha = for_sha(shas).group(:sha).select("max(id)") + where(id: max_id_per_sha) + end + def self.latest_successful_ids_per_project success.group(:project_id).select('max(id) as id') end diff --git a/app/presenters/commit_presenter.rb b/app/presenters/commit_presenter.rb index 05adbe1d4f546caf6a228a2f20d4881faca52534..fc9853733c1465e471684cd723018ebbc4feb236 100644 --- a/app/presenters/commit_presenter.rb +++ b/app/presenters/commit_presenter.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true -class CommitPresenter < Gitlab::View::Presenter::Simple +class CommitPresenter < Gitlab::View::Presenter::Delegated + include GlobalID::Identification + presents :commit def status_for(ref) @@ -10,4 +12,8 @@ class CommitPresenter < Gitlab::View::Presenter::Simple def any_pipelines? can?(current_user, :read_pipeline, commit.project) && commit.pipelines.any? end + + def web_url + Gitlab::UrlBuilder.new(commit).url + end end diff --git a/changelogs/unreleased/graphql-tree-last-commit.yml b/changelogs/unreleased/graphql-tree-last-commit.yml new file mode 100644 index 0000000000000000000000000000000000000000..5104ca6687edb82e6c5099df6b8615c374e7bac6 --- /dev/null +++ b/changelogs/unreleased/graphql-tree-last-commit.yml @@ -0,0 +1,5 @@ +--- +title: Added commit type to tree GraphQL response +merge_request: 29412 +author: +type: added diff --git a/lib/gitlab/graphql/loaders/pipeline_for_sha_loader.rb b/lib/gitlab/graphql/loaders/pipeline_for_sha_loader.rb new file mode 100644 index 0000000000000000000000000000000000000000..81c5cabf45124235244602aecbeaf5301a8d13ff --- /dev/null +++ b/lib/gitlab/graphql/loaders/pipeline_for_sha_loader.rb @@ -0,0 +1,25 @@ +# 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.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 diff --git a/spec/graphql/gitlab_schema_spec.rb b/spec/graphql/gitlab_schema_spec.rb index 4076c1f824b723e43e39994a14892016ee1f03c4..d36e428a8ee867f5dae6810640bec872bfdcef06 100644 --- a/spec/graphql/gitlab_schema_spec.rb +++ b/spec/graphql/gitlab_schema_spec.rb @@ -113,7 +113,7 @@ describe GitlabSchema do end it "raises a meaningful error if a global id couldn't be generated" do - expect { described_class.id_from_object(build(:commit)) } + expect { described_class.id_from_object(build(:wiki_directory)) } .to raise_error(RuntimeError, /include `GlobalID::Identification` into/i) end end diff --git a/spec/graphql/types/commit_type_spec.rb b/spec/graphql/types/commit_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..5d8edcf254c7f5b4a3d849de2a3887cc2d9e005c --- /dev/null +++ b/spec/graphql/types/commit_type_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GitlabSchema.types['Commit'] do + it { expect(described_class.graphql_name).to eq('Commit') } + + it { expect(described_class).to require_graphql_authorizations(:download_code) } + + it { expect(described_class).to have_graphql_fields(:id, :sha, :title, :description, :message, :authored_date, :author, :web_url, :latest_pipeline) } +end diff --git a/spec/graphql/types/tree/tree_type_spec.rb b/spec/graphql/types/tree/tree_type_spec.rb index b9c5570115e6544c83baa7b657de2b17da52df46..23779d75600b90439ccf3692bbcfcc17303f454a 100644 --- a/spec/graphql/types/tree/tree_type_spec.rb +++ b/spec/graphql/types/tree/tree_type_spec.rb @@ -5,5 +5,5 @@ require 'spec_helper' describe Types::Tree::TreeType do it { expect(described_class.graphql_name).to eq('Tree') } - it { expect(described_class).to have_graphql_fields(:trees, :submodules, :blobs) } + it { expect(described_class).to have_graphql_fields(:trees, :submodules, :blobs, :last_commit) } end diff --git a/spec/lib/gitlab/graphql/loaders/pipeline_for_sha_loader_spec.rb b/spec/lib/gitlab/graphql/loaders/pipeline_for_sha_loader_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..927476cc65500e5a5f9ce5b5139df038293f4388 --- /dev/null +++ b/spec/lib/gitlab/graphql/loaders/pipeline_for_sha_loader_spec.rb @@ -0,0 +1,20 @@ +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(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 diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 6ebc6337d50712ee12c63b0ef4d67e67db1a8761..55cea48b64123f558e67ffad36023438c2503076 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1886,6 +1886,17 @@ describe Ci::Pipeline, :mailer do end end + describe '.latest_for_shas' do + let(:sha) { 'abc' } + + it 'returns latest pipeline for sha' do + create(:ci_pipeline, sha: sha) + pipeline2 = create(:ci_pipeline, sha: sha) + + expect(described_class.latest_for_shas(sha)).to contain_exactly(pipeline2) + end + end + describe '.latest_successful_ids_per_project' do let(:projects) { create_list(:project, 2) } let!(:pipeline1) { create(:ci_pipeline, :success, project: projects[0]) } diff --git a/spec/requests/api/graphql/project/tree/tree_spec.rb b/spec/requests/api/graphql/project/tree/tree_spec.rb index b07aa1e12d3f3880a8f8ec661f98739a9f0ffce2..94128cc21eefb8eea18398424e31367e6501f3e7 100644 --- a/spec/requests/api/graphql/project/tree/tree_spec.rb +++ b/spec/requests/api/graphql/project/tree/tree_spec.rb @@ -33,6 +33,12 @@ describe 'getting a tree in a project' do expect(graphql_data['project']['repository']['tree']['submodules']['edges']).to eq([]) expect(graphql_data['project']['repository']['tree']['blobs']['edges']).to eq([]) end + + it 'returns null commit' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']['repository']['last_commit']).to be_nil + end end context 'when ref does not exist' do @@ -45,6 +51,12 @@ describe 'getting a tree in a project' do expect(graphql_data['project']['repository']['tree']['submodules']['edges']).to eq([]) expect(graphql_data['project']['repository']['tree']['blobs']['edges']).to eq([]) end + + it 'returns null commit' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']['repository']['last_commit']).to be_nil + end end context 'when ref and path exist' do @@ -61,6 +73,12 @@ describe 'getting a tree in a project' do expect(graphql_data['project']['repository']['tree']['blobs']['edges'].size).to be > 0 expect(graphql_data['project']['repository']['tree']['submodules']['edges'].size).to be > 0 end + + it 'returns tree latest commit' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']['repository']['tree']['lastCommit']).to be_present + end end context 'when current user is nil' do