From 4bb609f350948869f9895a7e3dc7e4a61c5c417c Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Tue, 6 Apr 2021 17:02:14 +0100 Subject: [PATCH] Make blobs directly accessible through the graphql repository Going through the tree is not always easy, especially when you want a single blob in a large subdirectory --- app/graphql/resolvers/blobs_resolver.rb | 37 ++++++++++ app/graphql/types/repository_type.rb | 2 + app/models/blob.rb | 1 + ...ke-blob-info-available-through-graphql.yml | 5 ++ doc/api/graphql/reference/index.md | 1 + spec/graphql/resolvers/blobs_resolver_spec.rb | 74 +++++++++++++++++++ spec/graphql/types/repository_type_spec.rb | 2 + 7 files changed, 122 insertions(+) create mode 100644 app/graphql/resolvers/blobs_resolver.rb create mode 100644 changelogs/unreleased/323195-make-blob-info-available-through-graphql.yml create mode 100644 spec/graphql/resolvers/blobs_resolver_spec.rb diff --git a/app/graphql/resolvers/blobs_resolver.rb b/app/graphql/resolvers/blobs_resolver.rb new file mode 100644 index 00000000000000..521e04827591fe --- /dev/null +++ b/app/graphql/resolvers/blobs_resolver.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Resolvers + class BlobsResolver < BaseResolver + include Gitlab::Graphql::Authorize::AuthorizeResource + + type Types::Tree::BlobType.connection_type, null: true + authorize :download_code + calls_gitaly! + + alias_method :repository, :object + + argument :paths, [GraphQL::STRING_TYPE], + required: true, + description: 'Array of desired blob paths.' + argument :ref, GraphQL::STRING_TYPE, + required: false, + default_value: nil, + description: 'The commit ref to get the blobs from. Default value is HEAD.' + + # We fetch blobs from Gitaly efficiently but it still scales O(N) with the + # number of paths being fetched, so apply a scaling limit to that. + def self.resolver_complexity(args, child_complexity:) + super + args.fetch(:paths, []).size + end + + def resolve(paths:, ref:) + authorize!(repository.container) + + return [] if repository.empty? + + ref ||= repository.root_ref + + repository.blobs_at(paths.map { |path| [ref, path] }) + end + end +end diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb index e319a5f312414a..fc835cdf642ab0 100644 --- a/app/graphql/types/repository_type.rb +++ b/app/graphql/types/repository_type.rb @@ -14,5 +14,7 @@ class RepositoryType < BaseObject description: 'Indicates a corresponding Git repository exists on disk.' field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver, calls_gitaly: true, description: 'Tree of the repository.' + field :blobs, Types::Tree::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true, + description: 'Blobs contained within the repository' end end diff --git a/app/models/blob.rb b/app/models/blob.rb index 8a9db8b45ea220..2185233a1ac55e 100644 --- a/app/models/blob.rb +++ b/app/models/blob.rb @@ -2,6 +2,7 @@ # Blob is a Rails-specific wrapper around Gitlab::Git::Blob, SnippetBlob and Ci::ArtifactBlob class Blob < SimpleDelegator + include GlobalID::Identification include Presentable include BlobLanguageFromGitAttributes include BlobActiveModel diff --git a/changelogs/unreleased/323195-make-blob-info-available-through-graphql.yml b/changelogs/unreleased/323195-make-blob-info-available-through-graphql.yml new file mode 100644 index 00000000000000..e4b6e55bb11946 --- /dev/null +++ b/changelogs/unreleased/323195-make-blob-info-available-through-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Make blobs directly accessible through the graphql repository +merge_request: 58677 +author: +type: added diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 56b5bcce3f1ab0..6da5ec2df2c432 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -5388,6 +5388,7 @@ Autogenerated return type of RepositionImageDiffNote. | Field | Type | Description | | ----- | ---- | ----------- | +| `blobs` | [`BlobConnection`](#blobconnection) | Blobs contained within the repository. | | `empty` | [`Boolean!`](#boolean) | Indicates repository has no visible content. | | `exists` | [`Boolean!`](#boolean) | Indicates a corresponding Git repository exists on disk. | | `rootRef` | [`String`](#string) | Default branch of the repository. | diff --git a/spec/graphql/resolvers/blobs_resolver_spec.rb b/spec/graphql/resolvers/blobs_resolver_spec.rb new file mode 100644 index 00000000000000..bc0344796ee3b3 --- /dev/null +++ b/spec/graphql/resolvers/blobs_resolver_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::BlobsResolver do + include GraphqlHelpers + + describe '.resolver_complexity' do + it 'adds one per path being resolved' do + control = described_class.resolver_complexity({}, child_complexity: 1) + + expect(described_class.resolver_complexity({ paths: %w[a b c] }, child_complexity: 1)) + .to eq(control + 3) + end + end + + describe '#resolve' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository) } + + let(:repository) { project.repository } + let(:args) { { paths: paths, ref: ref } } + let(:paths) { [] } + let(:ref) { nil } + + subject(:resolve_blobs) { resolve(described_class, obj: repository, args: args, ctx: { current_user: user }) } + + context 'when unauthorized' do + it 'raises an exception' do + expect { resolve_blobs }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when authorized' do + before do + project.add_developer(user) + end + + context 'using no filter' do + it 'returns nothing' do + is_expected.to be_empty + end + end + + context 'using paths filter' do + let(:paths) { ['README.md'] } + + it 'returns the specified blobs for HEAD' do + is_expected.to contain_exactly(have_attributes(path: 'README.md')) + end + + context 'specifying a non-existent blob' do + let(:paths) { ['non-existent'] } + + it 'returns nothing' do + is_expected.to be_empty + end + end + + context 'specifying a different ref' do + let(:ref) { 'add-pdf-file' } + let(:paths) { ['files/pdf/test.pdf', 'README.md'] } + + it 'returns the specified blobs for that ref' do + is_expected.to contain_exactly( + have_attributes(path: 'files/pdf/test.pdf'), + have_attributes(path: 'README.md') + ) + end + end + end + end + end +end diff --git a/spec/graphql/types/repository_type_spec.rb b/spec/graphql/types/repository_type_spec.rb index e9199bd286eb2f..a3bb7e502f203a 100644 --- a/spec/graphql/types/repository_type_spec.rb +++ b/spec/graphql/types/repository_type_spec.rb @@ -12,4 +12,6 @@ specify { expect(described_class).to have_graphql_field(:tree) } specify { expect(described_class).to have_graphql_field(:exists, calls_gitaly?: true, complexity: 2) } + + specify { expect(described_class).to have_graphql_field(:blobs) } end -- GitLab