diff --git a/.rubocop.yml b/.rubocop.yml
index 5a4c13bed36..c50a536e2f6 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1274,6 +1274,7 @@ Gitlab/TokenWithoutPrefix:
Rake/TopLevelMethodDefinition:
Enabled: true
Include:
+ - '**/Rakefile'
- 'lib/tasks/**/*.rake'
- 'ee/lib/tasks/**/*.rake'
diff --git a/app/graphql/types/tree/tree_type.rb b/app/graphql/types/tree/tree_type.rb
index 9fadb44220d..e943f987499 100644
--- a/app/graphql/types/tree/tree_type.rb
+++ b/app/graphql/types/tree/tree_type.rb
@@ -6,6 +6,8 @@ module Types
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 @@ module Types
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 00000000000..51ce5b5d4e1
--- /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 63e33548167..eb4616f5894 100644
--- a/doc/api/graphql/reference/_index.md
+++ b/doc/api/graphql/reference/_index.md
@@ -39085,6 +39085,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/doc/api/openapi/openapi_v2.yaml b/doc/api/openapi/openapi_v2.yaml
index 6ef336bdddb..52be670e6c6 100644
--- a/doc/api/openapi/openapi_v2.yaml
+++ b/doc/api/openapi/openapi_v2.yaml
@@ -43524,6 +43524,8 @@ definitions:
type: string
repository_storage:
type: string
+ duo_nano_features_enabled:
+ type: string
duo_features_enabled:
type: string
lock_duo_features_enabled:
@@ -44050,6 +44052,10 @@ definitions:
service_access_tokens_expiration_enforced:
type: boolean
description: To enforce token expiration for Service accounts users for group
+ duo_nano_features_enabled:
+ type: boolean
+ description: Indicates whether GitLab Duo Nano features are enabled for the
+ group
duo_features_enabled:
type: boolean
description: Indicates whether GitLab Duo features are enabled for the group
@@ -44163,6 +44169,8 @@ definitions:
type: string
repository_storage:
type: string
+ duo_nano_features_enabled:
+ type: string
duo_features_enabled:
type: string
lock_duo_features_enabled:
diff --git a/gems/config/rubocop.yml b/gems/config/rubocop.yml
index 9feeebb40b5..91884794b4b 100644
--- a/gems/config/rubocop.yml
+++ b/gems/config/rubocop.yml
@@ -147,3 +147,9 @@ Rails/StrongParams:
# This cop doesn't make sense in the context of gems
Gitlab/NoFindInWorkers:
Enabled: false
+
+Rake/TopLevelMethodDefinition:
+ Enabled: true
+ Include:
+ - '**/Rakefile'
+ - '**/*.rake'
diff --git a/rubocop/cop/rake/top_level_method_definition.rb b/rubocop/cop/rake/top_level_method_definition.rb
index 408d0b170cf..bb227825354 100644
--- a/rubocop/cop/rake/top_level_method_definition.rb
+++ b/rubocop/cop/rake/top_level_method_definition.rb
@@ -79,34 +79,20 @@ module RuboCop
'See https://github.com/rubocop/rubocop-rake/issues/42'
def on_def(node)
- return unless in_rake_file?
-
add_offense(node)
end
def on_defs(node)
- return unless in_rake_file?
-
add_offense(node)
end
def on_class(node)
- return unless in_rake_file?
-
add_offense(node, message: CLASS_MSG)
end
def on_module(node)
- return unless in_rake_file?
-
add_offense(node, message: MODULE_MSG)
end
-
- private
-
- def in_rake_file?
- processed_source.file_path.end_with?('.rake')
- end
end
end
end
diff --git a/spec/graphql/types/tree/tree_type_spec.rb b/spec/graphql/types/tree/tree_type_spec.rb
index 362ecdfca91..573b6904742 100644
--- a/spec/graphql/types/tree/tree_type_spec.rb
+++ b/spec/graphql/types/tree/tree_type_spec.rb
@@ -5,5 +5,5 @@ require 'spec_helper'
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 00000000000..72bd89015e6
--- /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
diff --git a/spec/rubocop/cop/rake/top_level_method_definition_spec.rb b/spec/rubocop/cop/rake/top_level_method_definition_spec.rb
index 385965ade66..62673f4545e 100644
--- a/spec/rubocop/cop/rake/top_level_method_definition_spec.rb
+++ b/spec/rubocop/cop/rake/top_level_method_definition_spec.rb
@@ -180,32 +180,77 @@ RSpec.describe RuboCop::Cop::Rake::TopLevelMethodDefinition, :aggregate_failures
end
end
- context 'in a non-rake file' do
- let(:source_file) { 'elastic.rb' }
+ context 'in a Rakefile' do
+ let(:source_file) { 'Rakefile' }
- it 'does not register an offense for method definitions outside modules' do
- expect_no_offenses(<<~RUBY, source_file)
+ it 'registers an offense for method definitions' do
+ expect_offense(<<~RUBY, source_file)
def task_executor_service
+ ^^^^^^^^^^^^^^^^^^^^^^^^^ Methods defined in rake tasks share the same namespace and can cause collisions. Please define it in a bounded contexts module in a separate Ruby file. For example, Search::RakeTask::. See https://github.com/rubocop/rubocop-rake/issues/42
Search::RakeTaskExecutorService.new(logger: stdout_logger)
end
RUBY
end
- it 'does not register an offense for method definitions inside blocks' do
- expect_no_offenses(<<~RUBY, source_file)
- something do
- def task_executor_service
+ it 'registers an offense for class definitions' do
+ expect_offense(<<~RUBY, source_file)
+ class TaskHelper
+ ^^^^^^^^^^^^^^^^ Classes should not be defined in rake files. Please define it in a bounded contexts module in a separate Ruby file. For example, Search::RakeTask::. See https://github.com/rubocop/rubocop-rake/issues/42
+ def self.task_executor_service
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Methods defined in rake tasks share the same namespace and can cause collisions. Please define it in a bounded contexts module in a separate Ruby file. For example, Search::RakeTask::. See https://github.com/rubocop/rubocop-rake/issues/42
Search::RakeTaskExecutorService.new(logger: stdout_logger)
end
end
RUBY
end
- it 'does not register an offense for class and module definitions' do
- expect_no_offenses(<<~RUBY, source_file)
+ it 'registers an offense for module definitions' do
+ expect_offense(<<~RUBY, source_file)
module SomeNamespace
+ ^^^^^^^^^^^^^^^^^^^^ Modules should not be defined in rake files. Please define it in a separate Ruby file. For example, Search::RakeTask::. See https://github.com/rubocop/rubocop-rake/issues/42
class TaskHelper
+ ^^^^^^^^^^^^^^^^ Classes should not be defined in rake files. Please define it in a bounded contexts module in a separate Ruby file. For example, Search::RakeTask::. See https://github.com/rubocop/rubocop-rake/issues/42
def self.task_executor_service
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Methods defined in rake tasks share the same namespace and can cause collisions. Please define it in a bounded contexts module in a separate Ruby file. For example, Search::RakeTask::. See https://github.com/rubocop/rubocop-rake/issues/42
+ Search::RakeTaskExecutorService.new(logger: stdout_logger)
+ end
+ end
+ end
+ RUBY
+ end
+ end
+
+ context 'in non-rake files (should still be checked based on .rubocop.yml Include directive)' do
+ let(:source_file) { 'elastic.rb' }
+
+ it 'registers an offense for method definitions outside modules' do
+ expect_offense(<<~RUBY, source_file)
+ def task_executor_service
+ ^^^^^^^^^^^^^^^^^^^^^^^^^ Methods defined in rake tasks share the same namespace and can cause collisions. Please define it in a bounded contexts module in a separate Ruby file. For example, Search::RakeTask::. See https://github.com/rubocop/rubocop-rake/issues/42
+ Search::RakeTaskExecutorService.new(logger: stdout_logger)
+ end
+ RUBY
+ end
+
+ it 'registers an offense for method definitions inside blocks' do
+ expect_offense(<<~RUBY, source_file)
+ something do
+ def task_executor_service
+ ^^^^^^^^^^^^^^^^^^^^^^^^^ Methods defined in rake tasks share the same namespace and can cause collisions. Please define it in a bounded contexts module in a separate Ruby file. For example, Search::RakeTask::. See https://github.com/rubocop/rubocop-rake/issues/42
+ Search::RakeTaskExecutorService.new(logger: stdout_logger)
+ end
+ end
+ RUBY
+ end
+
+ it 'registers an offense for class and module definitions' do
+ expect_offense(<<~RUBY, source_file)
+ module SomeNamespace
+ ^^^^^^^^^^^^^^^^^^^^ Modules should not be defined in rake files. Please define it in a separate Ruby file. For example, Search::RakeTask::. See https://github.com/rubocop/rubocop-rake/issues/42
+ class TaskHelper
+ ^^^^^^^^^^^^^^^^ Classes should not be defined in rake files. Please define it in a bounded contexts module in a separate Ruby file. For example, Search::RakeTask::. See https://github.com/rubocop/rubocop-rake/issues/42
+ def self.task_executor_service
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Methods defined in rake tasks share the same namespace and can cause collisions. Please define it in a bounded contexts module in a separate Ruby file. For example, Search::RakeTask::. See https://github.com/rubocop/rubocop-rake/issues/42
Search::RakeTaskExecutorService.new(logger: stdout_logger)
end
end