diff --git a/app/graphql/resolvers/tree_resolver.rb b/app/graphql/resolvers/tree_resolver.rb new file mode 100644 index 00000000000..5aad1c71b40 --- /dev/null +++ b/app/graphql/resolvers/tree_resolver.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module Resolvers + class TreeResolver < BaseResolver + argument :path, GraphQL::STRING_TYPE, + required: false, + default_value: '', + description: 'The path to get the tree for. Default value is the root of the repository' + argument :ref, GraphQL::STRING_TYPE, + required: false, + default_value: :head, + description: 'The commit ref to get the tree for. Default value is HEAD' + argument :recursive, GraphQL::BOOLEAN_TYPE, + required: false, + default_value: false, + description: 'Used to get a recursive tree. Default is false' + + alias_method :repository, :object + + def resolve(**args) + return unless repository.exists? + + repository.tree(args[:ref], args[:path], recursive: args[:recursive]) + end + end +end diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index baea6658e05..06a1aab09f6 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -69,6 +69,8 @@ module Types field :namespace, Types::NamespaceType, null: false field :group, Types::GroupType, null: true + field :repository, Types::RepositoryType, null: false + field :merge_requests, Types::MergeRequestType.connection_type, null: true, diff --git a/app/graphql/types/repository_type.rb b/app/graphql/types/repository_type.rb new file mode 100644 index 00000000000..5987467e1ea --- /dev/null +++ b/app/graphql/types/repository_type.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + class RepositoryType < BaseObject + graphql_name 'Repository' + + authorize :download_code + + field :root_ref, GraphQL::STRING_TYPE, null: true + field :empty, GraphQL::BOOLEAN_TYPE, null: false, method: :empty? + field :exists, GraphQL::BOOLEAN_TYPE, null: false, method: :exists? + field :tree, Types::Tree::TreeType, null: true, resolver: Resolvers::TreeResolver + end +end diff --git a/app/graphql/types/tree/blob_type.rb b/app/graphql/types/tree/blob_type.rb new file mode 100644 index 00000000000..230624201b0 --- /dev/null +++ b/app/graphql/types/tree/blob_type.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +module Types + module Tree + class BlobType < BaseObject + implements Types::Tree::EntryType + + graphql_name 'Blob' + end + end +end diff --git a/app/graphql/types/tree/entry_type.rb b/app/graphql/types/tree/entry_type.rb new file mode 100644 index 00000000000..d8e8642ddb8 --- /dev/null +++ b/app/graphql/types/tree/entry_type.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true +module Types + module Tree + module EntryType + include Types::BaseInterface + + field :id, GraphQL::ID_TYPE, null: false + field :name, GraphQL::STRING_TYPE, null: false + field :type, Tree::TypeEnum, null: false + field :path, GraphQL::STRING_TYPE, null: false + field :flat_path, GraphQL::STRING_TYPE, null: false + end + end +end diff --git a/app/graphql/types/tree/submodule_type.rb b/app/graphql/types/tree/submodule_type.rb new file mode 100644 index 00000000000..cea76dbfd2a --- /dev/null +++ b/app/graphql/types/tree/submodule_type.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true +module Types + module Tree + class SubmoduleType < BaseObject + implements Types::Tree::EntryType + + graphql_name 'Submodule' + end + end +end diff --git a/app/graphql/types/tree/tree_entry_type.rb b/app/graphql/types/tree/tree_entry_type.rb new file mode 100644 index 00000000000..d5cfb898aea --- /dev/null +++ b/app/graphql/types/tree/tree_entry_type.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true +module Types + module Tree + class TreeEntryType < BaseObject + implements Types::Tree::EntryType + + graphql_name 'TreeEntry' + description 'Represents a directory' + end + end +end diff --git a/app/graphql/types/tree/tree_type.rb b/app/graphql/types/tree/tree_type.rb new file mode 100644 index 00000000000..1eb6c43972e --- /dev/null +++ b/app/graphql/types/tree/tree_type.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true +module Types + module Tree + class TreeType < BaseObject + graphql_name 'Tree' + + field :trees, Types::Tree::TreeEntryType.connection_type, null: false + field :submodules, Types::Tree::SubmoduleType.connection_type, null: false + field :blobs, Types::Tree::BlobType.connection_type, null: false + end + end +end diff --git a/app/graphql/types/tree/type_enum.rb b/app/graphql/types/tree/type_enum.rb new file mode 100644 index 00000000000..6560d91e9e5 --- /dev/null +++ b/app/graphql/types/tree/type_enum.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module Types + module Tree + class TypeEnum < BaseEnum + graphql_name 'EntryType' + description 'Type of a tree entry' + + value 'tree', value: :tree + value 'blob', value: :blob + value 'commit', value: :commit + end + end +end diff --git a/spec/graphql/resolvers/tree_resolver_spec.rb b/spec/graphql/resolvers/tree_resolver_spec.rb new file mode 100644 index 00000000000..9f95b740ab1 --- /dev/null +++ b/spec/graphql/resolvers/tree_resolver_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Resolvers::TreeResolver do + include GraphqlHelpers + + let(:repository) { create(:project, :repository).repository } + + describe '#resolve' do + it 'resolves to a tree' do + result = resolve_repository({ ref: "master" }) + + expect(result).to be_an_instance_of(Tree) + end + + it 'resolve to a recursive tree' do + result = resolve_repository({ ref: "master", recursive: true }) + + expect(result.trees[4].path).to eq('files/html') + end + + context 'when repository does not exist' do + it 'returns nil' do + allow(repository).to receive(:exists?).and_return(false) + + result = resolve_repository({ ref: "master" }) + + expect(result).to be(nil) + end + end + end + + def resolve_repository(args) + resolve(described_class, obj: repository, args: args) + end +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index e0ad09bdf22..075fa7c7e43 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -17,4 +17,6 @@ describe GitlabSchema.types['Project'] do end it { is_expected.to have_graphql_field(:pipelines) } + + it { is_expected.to have_graphql_field(:repository) } end diff --git a/spec/graphql/types/repository_type_spec.rb b/spec/graphql/types/repository_type_spec.rb new file mode 100644 index 00000000000..8a8238f2a2a --- /dev/null +++ b/spec/graphql/types/repository_type_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe GitlabSchema.types['Repository'] do + it { expect(described_class.graphql_name).to eq('Repository') } + + it { expect(described_class).to require_graphql_authorizations(:download_code) } + + it { is_expected.to have_graphql_field(:root_ref) } + + it { is_expected.to have_graphql_field(:tree) } +end diff --git a/spec/graphql/types/tree/blob_type_spec.rb b/spec/graphql/types/tree/blob_type_spec.rb new file mode 100644 index 00000000000..fa29bb5fff7 --- /dev/null +++ b/spec/graphql/types/tree/blob_type_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Types::Tree::BlobType do + it { expect(described_class.graphql_name).to eq('Blob') } + + it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path) } +end diff --git a/spec/graphql/types/tree/submodule_type_spec.rb b/spec/graphql/types/tree/submodule_type_spec.rb new file mode 100644 index 00000000000..bdb3149b41c --- /dev/null +++ b/spec/graphql/types/tree/submodule_type_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Types::Tree::SubmoduleType do + it { expect(described_class.graphql_name).to eq('Submodule') } + + it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path) } +end diff --git a/spec/graphql/types/tree/tree_entry_type_spec.rb b/spec/graphql/types/tree/tree_entry_type_spec.rb new file mode 100644 index 00000000000..397cabde8e5 --- /dev/null +++ b/spec/graphql/types/tree/tree_entry_type_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Types::Tree::TreeEntryType do + it { expect(described_class.graphql_name).to eq('TreeEntry') } + + it { expect(described_class).to have_graphql_fields(:id, :name, :type, :path, :flat_path) } +end diff --git a/spec/graphql/types/tree/tree_type_spec.rb b/spec/graphql/types/tree/tree_type_spec.rb new file mode 100644 index 00000000000..b9c5570115e --- /dev/null +++ b/spec/graphql/types/tree/tree_type_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +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) } +end diff --git a/spec/graphql/types/tree/type_enum_spec.rb b/spec/graphql/types/tree/type_enum_spec.rb new file mode 100644 index 00000000000..4caf9e1c457 --- /dev/null +++ b/spec/graphql/types/tree/type_enum_spec.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Types::Tree::TypeEnum do + it { expect(described_class.graphql_name).to eq('EntryType') } + + it 'exposes all tree entry types' do + expect(described_class.values.keys).to include(*%w[tree blob commit]) + end +end diff --git a/spec/requests/api/graphql/project/repository_spec.rb b/spec/requests/api/graphql/project/repository_spec.rb new file mode 100644 index 00000000000..67af612a4a0 --- /dev/null +++ b/spec/requests/api/graphql/project/repository_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe 'getting a repository in a project' do + include GraphqlHelpers + + let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } + let(:fields) do + <<~QUERY + #{all_graphql_fields_for('repository'.classify)} + QUERY + end + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('repository', {}, fields) + ) + end + + it 'returns repository' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']['repository']).to be_present + end + + context 'as a non-authorized user' do + let(:current_user) { create(:user) } + + it 'returns nil' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']).to be(nil) + end + end +end diff --git a/spec/requests/api/graphql/project/tree/tree_spec.rb b/spec/requests/api/graphql/project/tree/tree_spec.rb new file mode 100644 index 00000000000..b07aa1e12d3 --- /dev/null +++ b/spec/requests/api/graphql/project/tree/tree_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe 'getting a tree in a project' do + include GraphqlHelpers + + let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } + let(:path) { "" } + let(:ref) { "master" } + let(:fields) do + <<~QUERY + tree(path:"#{path}", ref:"#{ref}") { + #{all_graphql_fields_for('tree'.classify)} + } + QUERY + end + let(:query) do + graphql_query_for( + 'project', + { 'fullPath' => project.full_path }, + query_graphql_field('repository', {}, fields) + ) + end + + context 'when path does not exist' do + let(:path) { "testing123" } + + it 'returns empty tree' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']['repository']['tree']['trees']['edges']).to eq([]) + expect(graphql_data['project']['repository']['tree']['submodules']['edges']).to eq([]) + expect(graphql_data['project']['repository']['tree']['blobs']['edges']).to eq([]) + end + end + + context 'when ref does not exist' do + let(:ref) { "testing123" } + + it 'returns empty tree' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']['repository']['tree']['trees']['edges']).to eq([]) + expect(graphql_data['project']['repository']['tree']['submodules']['edges']).to eq([]) + expect(graphql_data['project']['repository']['tree']['blobs']['edges']).to eq([]) + end + end + + context 'when ref and path exist' do + it 'returns tree' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']['repository']['tree']).to be_present + end + + it 'returns blobs, subtrees and submodules inside tree' do + post_graphql(query, current_user: current_user) + + expect(graphql_data['project']['repository']['tree']['trees']['edges'].size).to be > 0 + 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 + end + + context 'when current user is nil' do + it 'returns empty project' do + post_graphql(query, current_user: nil) + + expect(graphql_data['project']).to be(nil) + end + end +end