diff --git a/app/graphql/types/tree/tree_type.rb b/app/graphql/types/tree/tree_type.rb
index 9fadb44220d7ca327ced9ce8a4f7fcaf0df36ad7..e943f987499f7f40ca4ae6f3b4605362dc77ad63 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 0000000000000000000000000000000000000000..51ce5b5d4e1432504eb811095d88fb58645e537c
--- /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 37bcae456589b08dd5afe6a171d986a8c032c1f0..495bc7343e3b05447775905fb7e83991a7217b78 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 362ecdfca91ffcb70f5dc5d26321064c42739573..573b69047421ede5c7fb00655f8c8e589929c9f1 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 0000000000000000000000000000000000000000..72bd89015e6cdf5bf0cca9a017e7d3da9cd5a730
--- /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