From a078f040d9a31bdea013d1bbce960c03a1684fec Mon Sep 17 00:00:00 2001 From: Emma Park Date: Thu, 3 Apr 2025 22:47:27 +1100 Subject: [PATCH] Add `permalinkPath` field to TreeType via TreePresenter Adds a `permalinkPath` field to the GraphQL TreeType, enabling frontend to generate permalinks for directory entries at a specific commit SHA. Introduces a TreePresenter to handle decoration logic for trees, blobs, and submodules, and to centralize permalink generation. Changelog: added --- app/graphql/types/tree/tree_type.rb | 7 +++ app/presenters/projects/tree_presenter.rb | 20 +++++++ doc/api/graphql/reference/_index.md | 1 + spec/graphql/types/tree/tree_type_spec.rb | 2 +- .../projects/tree_presenter_spec.rb | 57 +++++++++++++++++++ 5 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 app/presenters/projects/tree_presenter.rb create mode 100644 spec/presenters/projects/tree_presenter_spec.rb diff --git a/app/graphql/types/tree/tree_type.rb b/app/graphql/types/tree/tree_type.rb index 9fadb44220d7ca..e943f987499f7f 100644 --- a/app/graphql/types/tree/tree_type.rb +++ b/app/graphql/types/tree/tree_type.rb @@ -6,6 +6,8 @@ module Tree class TreeType < BaseObject graphql_name 'Tree' + present_using ::Projects::TreePresenter + # Complexity 10 as it triggers a Gitaly call on each render field :last_commit, Types::Repositories::CommitType, null: true, complexity: 10, calls_gitaly: true, resolver: Resolvers::LastCommitResolver, @@ -22,6 +24,11 @@ class TreeType < BaseObject description: 'Blobs of the tree.', calls_gitaly: true + field :permalink_path, GraphQL::Types::String, null: true, + description: 'Web path to tree permalink.', + calls_gitaly: true, + experiment: { milestone: '17.11' } + def trees Gitlab::Graphql::Representation::TreeEntry.decorate(object.trees, object.repository) end diff --git a/app/presenters/projects/tree_presenter.rb b/app/presenters/projects/tree_presenter.rb new file mode 100644 index 00000000000000..51ce5b5d4e1432 --- /dev/null +++ b/app/presenters/projects/tree_presenter.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Projects + class TreePresenter < Gitlab::View::Presenter::Delegated + presents Tree, as: :tree + + def permalink_path + return unless tree.sha.present? + + project = tree.repository.project + commit = tree.repository.commit(tree.sha) + return unless commit + + path = tree.path.presence + full_path = path.present? ? File.join(commit.sha, path) : commit.sha + + Gitlab::Routing.url_helpers.project_tree_path(project, full_path) + end + end +end diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 37bcae456589b0..495bc7343e3b05 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -38829,6 +38829,7 @@ Representing a to-do entry. | Name | Type | Description | | ---- | ---- | ----------- | | `blobs` | [`BlobConnection!`](#blobconnection) | Blobs of the tree. (see [Connections](#connections)) | +| `permalinkPath` {{< icon name="warning-solid" >}} | [`String`](#string) | **Introduced** in GitLab 17.11. **Status**: Experiment. Web path to tree permalink. | | `submodules` | [`SubmoduleConnection!`](#submoduleconnection) | Sub-modules of the tree. (see [Connections](#connections)) | | `trees` | [`TreeEntryConnection!`](#treeentryconnection) | Trees of the tree. (see [Connections](#connections)) | diff --git a/spec/graphql/types/tree/tree_type_spec.rb b/spec/graphql/types/tree/tree_type_spec.rb index 362ecdfca91ffc..573b69047421ed 100644 --- a/spec/graphql/types/tree/tree_type_spec.rb +++ b/spec/graphql/types/tree/tree_type_spec.rb @@ -5,5 +5,5 @@ RSpec.describe Types::Tree::TreeType do specify { expect(described_class.graphql_name).to eq('Tree') } - specify { expect(described_class).to have_graphql_fields(:trees, :submodules, :blobs, :last_commit) } + specify { expect(described_class).to have_graphql_fields(:trees, :submodules, :blobs, :last_commit, :permalink_path) } end diff --git a/spec/presenters/projects/tree_presenter_spec.rb b/spec/presenters/projects/tree_presenter_spec.rb new file mode 100644 index 00000000000000..72bd89015e6cdf --- /dev/null +++ b/spec/presenters/projects/tree_presenter_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::TreePresenter, feature_category: :source_code_management do + let_it_be(:project) { create(:project, :repository) } # rubocop:disable RSpec/FactoryBot/AvoidCreate -- Need persisted objects + let(:repository) { project.repository } + let(:user) { project.first_owner } + + let(:ref) { 'HEAD' } + let(:path) { 'lib' } + + let(:commit) { repository.commit(ref) } + let(:tree) { repository.tree(ref, path) } + + subject(:presenter) { described_class.new(tree, current_user: user) } + + describe '#permalink_path' do + it 'returns the permalink path with commit SHA and directory path' do + expect(presenter.permalink_path).to eq("/#{project.full_path}/-/tree/#{commit.sha}/#{path}") + end + + context 'when tree path is empty (root tree)' do + let(:path) { '' } + + it 'returns the permalink path pointing to the commit SHA only' do + expect(presenter.permalink_path).to eq("/#{project.full_path}/-/tree/#{commit.sha}/") + end + end + + context 'when tree has no sha' do + before do + tree.sha = nil + end + + it 'returns nil' do + expect(presenter.permalink_path).to be_nil + end + end + + context 'when commit is not found' do + before do + allow(repository).to receive(:commit).and_return(nil) + end + + let(:tree) do + repository.tree(ref, path).tap do |t| + t.sha = 'nonexistentsha123' + end + end + + it 'returns nil' do + expect(presenter.permalink_path).to be_nil + end + end + end +end -- GitLab