diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql index e0bbf12f3ebc346e38003db5394b7ea1194ef486..eb4b00d925192ebbcf4aab87c94b25e9326f1d6f 100644 --- a/app/assets/javascripts/repository/queries/blob_info.query.graphql +++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql @@ -2,28 +2,25 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) { project(fullPath: $projectPath) { id repository { - blobs(path: $filePath) { - name - size - rawBlob - type - fileType - tooLarge - path - editBlobPath - ideEditPath - storedExternally - rawPath - externalStorageUrl - replacePath - deletePath - canLock - isLocked - lockLink - canModifyBlob - forkPath - simpleViewer - richViewer + blobs(paths: [$filePath]) { + nodes { + webPath + name + rawSize + rawTextBlob + fileType + path + editBlobPath + ideEditPath + storedExternally + rawPath + externalStorageUrl + replacePath + canModifyBlob + forkPath + simpleViewer + richViewer + } } } } diff --git a/app/graphql/types/blob_viewer_type.rb b/app/graphql/types/blob_viewer_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..8d863c32bc7a2f58ea4ba6c33bb39fb2f91bfafa --- /dev/null +++ b/app/graphql/types/blob_viewer_type.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module Types + class BlobViewerType < BaseObject # rubocop:disable Graphql/AuthorizeTypes + graphql_name 'BlobViewer' + description 'Represents how the blob content should be displayed' + + field :type, Types::BlobViewers::TypeEnum, + description: 'Type of blob viewer.', + null: false + + field :load_async, GraphQL::BOOLEAN_TYPE, + description: 'Shows whether the blob content is loaded asynchronously.', + null: false + + field :collapsed, GraphQL::BOOLEAN_TYPE, + description: 'Shows whether the blob should be displayed collapsed.', + method: :collapsed?, + null: false + + field :too_large, GraphQL::BOOLEAN_TYPE, + description: 'Shows whether the blob is too large to be displayed.', + method: :too_large?, + null: false + + field :render_error, GraphQL::STRING_TYPE, + description: 'Error rendering the blob content.', + null: true + + field :file_type, GraphQL::STRING_TYPE, + description: 'Content file type.', + method: :partial_name, + null: false + + field :loading_partial_name, GraphQL::STRING_TYPE, + description: 'Loading partial name.', + null: false + + def collapsed + !!object&.collapsed? + end + + def too_large + !!object&.too_large? + end + end +end diff --git a/app/graphql/types/repository/blob_type.rb b/app/graphql/types/repository/blob_type.rb index 912fc5f643affbee51106c3ade0b86bdbf6c443f..508389715f2644bccb0d9311cbc7887e12098b58 100644 --- a/app/graphql/types/repository/blob_type.rb +++ b/app/graphql/types/repository/blob_type.rb @@ -32,6 +32,45 @@ class BlobType < BaseObject field :web_path, GraphQL::STRING_TYPE, null: true, description: 'Web path of the blob.' + field :size, GraphQL::INT_TYPE, null: true, + description: 'Size (in bytes) of the blob.' + + field :raw_size, GraphQL::INT_TYPE, null: true, + description: 'Size (in bytes) of the blob, or the blob target if stored externally.' + + field :raw_blob, GraphQL::STRING_TYPE, null: true, method: :data, + description: 'The raw content of the blob.' + + field :raw_text_blob, GraphQL::STRING_TYPE, null: true, method: :text_only_data, + description: 'The raw content of the blob, if the blob is text data.' + + field :stored_externally, GraphQL::BOOLEAN_TYPE, null: true, method: :stored_externally?, + description: "Whether the blob's content is stored externally (for instance, in LFS)." + + field :edit_blob_path, GraphQL::STRING_TYPE, null: true, + description: 'Web path to edit the blob in the old-style editor.' + + field :raw_path, GraphQL::STRING_TYPE, null: true, + description: 'Web path to download the raw blob.' + + field :replace_path, GraphQL::STRING_TYPE, null: true, + description: 'Web path to replace the blob content.' + + field :file_type, GraphQL::STRING_TYPE, null: true, + description: 'The expected format of the blob based on the extension.' + + field :simple_viewer, type: Types::BlobViewerType, + description: 'Blob content simple viewer.', + null: false + + field :rich_viewer, type: Types::BlobViewerType, + description: 'Blob content rich viewer.', + null: true + + def raw_text_blob + object.data unless object.binary? + end + def lfs_oid Gitlab::Graphql::Loaders::BatchLfsOidLoader.new(object.repository, object.id).find end diff --git a/app/graphql/types/snippets/blob_viewer_type.rb b/app/graphql/types/snippets/blob_viewer_type.rb index 9e77457c8434840296c13721ecaee94e19cebd3b..8b73234bbd907612ef9ff68524da40db1367a0d1 100644 --- a/app/graphql/types/snippets/blob_viewer_type.rb +++ b/app/graphql/types/snippets/blob_viewer_type.rb @@ -2,48 +2,10 @@ module Types module Snippets - class BlobViewerType < BaseObject # rubocop:disable Graphql/AuthorizeTypes + # Kept to avoid changing the type of existing fields. New fields should use + # ::Types::BlobViewerType directly + class BlobViewerType < ::Types::BlobViewerType # rubocop:disable Graphql/AuthorizeTypes graphql_name 'SnippetBlobViewer' - description 'Represents how the blob content should be displayed' - - field :type, Types::BlobViewers::TypeEnum, - description: 'Type of blob viewer.', - null: false - - field :load_async, GraphQL::BOOLEAN_TYPE, - description: 'Shows whether the blob content is loaded asynchronously.', - null: false - - field :collapsed, GraphQL::BOOLEAN_TYPE, - description: 'Shows whether the blob should be displayed collapsed.', - method: :collapsed?, - null: false - - field :too_large, GraphQL::BOOLEAN_TYPE, - description: 'Shows whether the blob too large to be displayed.', - method: :too_large?, - null: false - - field :render_error, GraphQL::STRING_TYPE, - description: 'Error rendering the blob content.', - null: true - - field :file_type, GraphQL::STRING_TYPE, - description: 'Content file type.', - method: :partial_name, - null: false - - field :loading_partial_name, GraphQL::STRING_TYPE, - description: 'Loading partial name.', - null: false - - def collapsed - !!object&.collapsed? - end - - def too_large - !!object&.too_large? - end end end end diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb index cff935d51b5f0cf608a34fd5e4987d8e9b3c11de..000fc8214aa9c168019bf5c082cc25271466ea83 100644 --- a/app/presenters/blob_presenter.rb +++ b/app/presenters/blob_presenter.rb @@ -15,15 +15,39 @@ def highlight(to: nil, plain: nil) end def web_url - Gitlab::Routing.url_helpers.project_blob_url(blob.repository.project, File.join(blob.commit_id, blob.path)) + url_helpers.project_blob_url(project, ref_qualified_path) end def web_path - Gitlab::Routing.url_helpers.project_blob_path(blob.repository.project, File.join(blob.commit_id, blob.path)) + url_helpers.project_blob_path(project, ref_qualified_path) + end + + def edit_blob_path + url_helpers.project_edit_blob_path(project, ref_qualified_path) + end + + def raw_path + url_helpers.project_raw_path(project, ref_qualified_path) + end + + def replace_path + url_helpers.project_create_blob_path(project, ref_qualified_path) end private + def url_helpers + Gitlab::Routing.url_helpers + end + + def project + blob.repository.project + end + + def ref_qualified_path + File.join(blob.commit_id, blob.path) + end + def load_all_blob_data blob.load_all_data! if blob.respond_to?(:load_all_data!) end diff --git a/changelogs/unreleased/323195-more-blob-attributes.yml b/changelogs/unreleased/323195-more-blob-attributes.yml new file mode 100644 index 0000000000000000000000000000000000000000..0cd4ee0aa38baeae86f6adae244aa9c0a04acee1 --- /dev/null +++ b/changelogs/unreleased/323195-more-blob-attributes.yml @@ -0,0 +1,5 @@ +--- +title: Add more fields to the GraphQL blob type +merge_request: 58906 +author: +type: added diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 9773ff441c5fa4e67248fd548208983025d00ee0..765478c73cd3d944cc0cc58cb9882811a5207e1b 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -6742,6 +6742,22 @@ An emoji awarded by a user. | `webPath` | [`String`](#string) | Web path of the blob. | | `webUrl` | [`String`](#string) | Web URL of the blob. | +### `BlobViewer` + +Represents how the blob content should be displayed. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `collapsed` | [`Boolean!`](#boolean) | Shows whether the blob should be displayed collapsed. | +| `fileType` | [`String!`](#string) | Content file type. | +| `loadAsync` | [`Boolean!`](#boolean) | Shows whether the blob content is loaded asynchronously. | +| `loadingPartialName` | [`String!`](#string) | Loading partial name. | +| `renderError` | [`String`](#string) | Error rendering the blob content. | +| `tooLarge` | [`Boolean!`](#boolean) | Shows whether the blob is too large to be displayed. | +| `type` | [`BlobViewersType!`](#blobviewerstype) | Type of blob viewer. | + ### `Board` Represents a project or group issue board. @@ -11257,12 +11273,23 @@ Returns [`Tree`](#tree). | Name | Type | Description | | ---- | ---- | ----------- | +| `editBlobPath` | [`String`](#string) | Web path to edit the blob in the old-style editor. | +| `fileType` | [`String`](#string) | The expected format of the blob based on the extension. | | `id` | [`ID!`](#id) | ID of the blob. | | `lfsOid` | [`String`](#string) | LFS OID of the blob. | | `mode` | [`String`](#string) | Blob mode. | | `name` | [`String`](#string) | Blob name. | | `oid` | [`String!`](#string) | OID of the blob. | | `path` | [`String!`](#string) | Path of the blob. | +| `rawBlob` | [`String`](#string) | The raw content of the blob. | +| `rawPath` | [`String`](#string) | Web path to download the raw blob. | +| `rawSize` | [`Int`](#int) | Size (in bytes) of the blob, or the blob target if stored externally. | +| `rawTextBlob` | [`String`](#string) | The raw content of the blob, if the blob is text data. | +| `replacePath` | [`String`](#string) | Web path to replace the blob content. | +| `richViewer` | [`BlobViewer`](#blobviewer) | Blob content rich viewer. | +| `simpleViewer` | [`BlobViewer!`](#blobviewer) | Blob content simple viewer. | +| `size` | [`Int`](#int) | Size (in bytes) of the blob. | +| `storedExternally` | [`Boolean`](#boolean) | Whether the blob's content is stored externally (for instance, in LFS). | | `webPath` | [`String`](#string) | Web path of the blob. | ### `Requirement` @@ -11757,7 +11784,7 @@ Represents how the blob content should be displayed. | `loadAsync` | [`Boolean!`](#boolean) | Shows whether the blob content is loaded asynchronously. | | `loadingPartialName` | [`String!`](#string) | Loading partial name. | | `renderError` | [`String`](#string) | Error rendering the blob content. | -| `tooLarge` | [`Boolean!`](#boolean) | Shows whether the blob too large to be displayed. | +| `tooLarge` | [`Boolean!`](#boolean) | Shows whether the blob is too large to be displayed. | | `type` | [`BlobViewersType!`](#blobviewerstype) | Type of blob viewer. | ### `SnippetPermissions` diff --git a/spec/graphql/types/blob_viewer_type_spec.rb b/spec/graphql/types/blob_viewer_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..1c020c635359f36e2fdd3eac04c5949cd8f279ef --- /dev/null +++ b/spec/graphql/types/blob_viewer_type_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['BlobViewer'] do + it 'has the correct fields' do + expected_fields = [:type, :load_async, :too_large, :collapsed, + :render_error, :file_type, :loading_partial_name] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/graphql/types/repository/blob_type_spec.rb b/spec/graphql/types/repository/blob_type_spec.rb index f8647e4e9643384a8b84475bc243b27d6734b565..8accee90fa33e2feb3e92a10e280816cf60708fa 100644 --- a/spec/graphql/types/repository/blob_type_spec.rb +++ b/spec/graphql/types/repository/blob_type_spec.rb @@ -5,5 +5,26 @@ RSpec.describe Types::Repository::BlobType do specify { expect(described_class.graphql_name).to eq('RepositoryBlob') } - specify { expect(described_class).to have_graphql_fields(:id, :oid, :name, :path, :web_path, :lfs_oid, :mode) } + specify do + expect(described_class).to have_graphql_fields( + :id, + :oid, + :name, + :path, + :web_path, + :lfs_oid, + :mode, + :size, + :raw_size, + :raw_blob, + :raw_text_blob, + :file_type, + :edit_blob_path, + :stored_externally, + :raw_path, + :replace_path, + :simple_viewer, + :rich_viewer + ) + end end diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb index 47402fea2b55a962daec86d5e7540de513b38420..d6acc20396f3ad0aace132f043e6a16a6b7c1ac9 100644 --- a/spec/presenters/blob_presenter_spec.rb +++ b/spec/presenters/blob_presenter_spec.rb @@ -2,52 +2,59 @@ require 'spec_helper' -RSpec.describe BlobPresenter, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '', 'group/project') } - - let(:git_blob) do - Gitlab::Git::Blob.find( - repository, - 'fa1b1e6c004a68b7d8763b86455da9e6b23e36d6', - 'files/ruby/regex.rb' - ) +RSpec.describe BlobPresenter do + let_it_be(:project) { create(:project, :repository) } + + let(:repository) { project.repository } + let(:blob) { repository.blob_at('HEAD', 'files/ruby/regex.rb') } + + subject(:presenter) { described_class.new(blob) } + + describe '#web_url' do + it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } end - let(:blob) { Blob.new(git_blob) } + describe '#web_path' do + it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + end - describe '.web_url' do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository } - let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(repository.tree.blobs.first, repository) } + describe '#edit_blob_path' do + it { expect(presenter.edit_blob_path).to eq("/#{project.full_path}/-/edit/#{blob.commit_id}/#{blob.path}") } + end - subject { described_class.new(blob) } + describe '#raw_path' do + it { expect(presenter.raw_path).to eq("/#{project.full_path}/-/raw/#{blob.commit_id}/#{blob.path}") } + end - it { expect(subject.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + describe '#replace_path' do + it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/create/#{blob.commit_id}/#{blob.path}") } end - describe '#web_path' do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository } - let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(repository.tree.blobs.first, repository) } + context 'given a Gitlab::Graphql::Representation::TreeEntry' do + let(:blob) { Gitlab::Graphql::Representation::TreeEntry.new(super(), repository) } - subject { described_class.new(blob) } + describe '#web_url' do + it { expect(presenter.web_url).to eq("http://localhost/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + end - it { expect(subject.web_path).to eq("/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + describe '#web_path' do + it { expect(presenter.web_path).to eq("/#{project.full_path}/-/blob/#{blob.commit_id}/#{blob.path}") } + end end describe '#highlight' do - subject { described_class.new(blob) } + let(:git_blob) { blob.__getobj__ } it 'returns highlighted content' do expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: nil) - subject.highlight + presenter.highlight end it 'returns plain content when :plain is true' do expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: true, language: nil) - subject.highlight(plain: true) + presenter.highlight(plain: true) end context '"to" param is present' do @@ -60,7 +67,7 @@ it 'returns limited highlighted content' do expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', "line one\n", plain: nil, language: nil) - subject.highlight(to: 1) + presenter.highlight(to: 1) end end @@ -72,7 +79,7 @@ it 'passes language to inner call' do expect(Gitlab::Highlight).to receive(:highlight).with('files/ruby/regex.rb', git_blob.data, plain: nil, language: 'ruby') - subject.highlight + presenter.highlight end end end