Commit 670b4fc1 authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch '323195-path-locks-graphql' into 'master'

Expose Path Locks in the GraphQL API

See merge request gitlab-org/gitlab!61380
parents ae477f27 24fd7add
...@@ -27,3 +27,5 @@ module Types ...@@ -27,3 +27,5 @@ module Types
end end
end end
end end
::Types::PermissionTypes::Project.prepend_mod_with('Types::PermissionTypes::Project')
...@@ -5806,6 +5806,29 @@ The edge type for [`PackageTag`](#packagetag). ...@@ -5806,6 +5806,29 @@ The edge type for [`PackageTag`](#packagetag).
| <a id="packagetagedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | <a id="packagetagedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="packagetagedgenode"></a>`node` | [`PackageTag`](#packagetag) | The item at the end of the edge. | | <a id="packagetagedgenode"></a>`node` | [`PackageTag`](#packagetag) | The item at the end of the edge. |
#### `PathLockConnection`
The connection type for [`PathLock`](#pathlock).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="pathlockconnectionedges"></a>`edges` | [`[PathLockEdge]`](#pathlockedge) | A list of edges. |
| <a id="pathlockconnectionnodes"></a>`nodes` | [`[PathLock]`](#pathlock) | A list of nodes. |
| <a id="pathlockconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
#### `PathLockEdge`
The edge type for [`PathLock`](#pathlock).
##### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="pathlockedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="pathlockedgenode"></a>`node` | [`PathLock`](#pathlock) | The item at the end of the edge. |
#### `PipelineArtifactRegistryConnection` #### `PipelineArtifactRegistryConnection`
The connection type for [`PipelineArtifactRegistry`](#pipelineartifactregistry). The connection type for [`PipelineArtifactRegistry`](#pipelineartifactregistry).
...@@ -10632,6 +10655,18 @@ Information about pagination in a connection. ...@@ -10632,6 +10655,18 @@ Information about pagination in a connection.
| <a id="pageinfohaspreviouspage"></a>`hasPreviousPage` | [`Boolean!`](#boolean) | When paginating backwards, are there more items?. | | <a id="pageinfohaspreviouspage"></a>`hasPreviousPage` | [`Boolean!`](#boolean) | When paginating backwards, are there more items?. |
| <a id="pageinfostartcursor"></a>`startCursor` | [`String`](#string) | When paginating backwards, the cursor to continue. | | <a id="pageinfostartcursor"></a>`startCursor` | [`String`](#string) | When paginating backwards, the cursor to continue. |
### `PathLock`
Represents a file or directory in the project repository that has been locked.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="pathlockid"></a>`id` | [`PathLockID!`](#pathlockid) | ID of the path lock. |
| <a id="pathlockpath"></a>`path` | [`String`](#string) | The locked path. |
| <a id="pathlockuser"></a>`user` | [`UserCore`](#usercore) | The user that has locked this path. |
### `Pipeline` ### `Pipeline`
#### Fields #### Fields
...@@ -10847,6 +10882,7 @@ Represents vulnerability finding of a security report on the pipeline. ...@@ -10847,6 +10882,7 @@ Represents vulnerability finding of a security report on the pipeline.
| <a id="projectonlyallowmergeifpipelinesucceeds"></a>`onlyAllowMergeIfPipelineSucceeds` | [`Boolean`](#boolean) | Indicates if merge requests of the project can only be merged with successful jobs. | | <a id="projectonlyallowmergeifpipelinesucceeds"></a>`onlyAllowMergeIfPipelineSucceeds` | [`Boolean`](#boolean) | Indicates if merge requests of the project can only be merged with successful jobs. |
| <a id="projectopenissuescount"></a>`openIssuesCount` | [`Int`](#int) | Number of open issues for the project. | | <a id="projectopenissuescount"></a>`openIssuesCount` | [`Int`](#int) | Number of open issues for the project. |
| <a id="projectpath"></a>`path` | [`String!`](#string) | Path of the project. | | <a id="projectpath"></a>`path` | [`String!`](#string) | Path of the project. |
| <a id="projectpathlocks"></a>`pathLocks` | [`PathLockConnection`](#pathlockconnection) | The project's path locks. (see [Connections](#connections)) |
| <a id="projectpipelineanalytics"></a>`pipelineAnalytics` | [`PipelineAnalytics`](#pipelineanalytics) | Pipeline analytics. | | <a id="projectpipelineanalytics"></a>`pipelineAnalytics` | [`PipelineAnalytics`](#pipelineanalytics) | Pipeline analytics. |
| <a id="projectprintingmergerequestlinkenabled"></a>`printingMergeRequestLinkEnabled` | [`Boolean`](#boolean) | Indicates if a link to create or view a merge request should display after a push to Git repositories of the project from the command line. | | <a id="projectprintingmergerequestlinkenabled"></a>`printingMergeRequestLinkEnabled` | [`Boolean`](#boolean) | Indicates if a link to create or view a merge request should display after a push to Git repositories of the project from the command line. |
| <a id="projectpublicjobs"></a>`publicJobs` | [`Boolean`](#boolean) | Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts. | | <a id="projectpublicjobs"></a>`publicJobs` | [`Boolean`](#boolean) | Indicates if there is public access to pipelines and job details of the project, including output logs and artifacts. |
...@@ -11661,6 +11697,7 @@ Represents a Project Membership. ...@@ -11661,6 +11697,7 @@ Represents a Project Membership.
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="projectpermissionsadminoperations"></a>`adminOperations` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_operations` on this resource. | | <a id="projectpermissionsadminoperations"></a>`adminOperations` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_operations` on this resource. |
| <a id="projectpermissionsadminpathlocks"></a>`adminPathLocks` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_path_locks` on this resource. |
| <a id="projectpermissionsadminproject"></a>`adminProject` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_project` on this resource. | | <a id="projectpermissionsadminproject"></a>`adminProject` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_project` on this resource. |
| <a id="projectpermissionsadminremotemirror"></a>`adminRemoteMirror` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_remote_mirror` on this resource. | | <a id="projectpermissionsadminremotemirror"></a>`adminRemoteMirror` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_remote_mirror` on this resource. |
| <a id="projectpermissionsadminwiki"></a>`adminWiki` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_wiki` on this resource. | | <a id="projectpermissionsadminwiki"></a>`adminWiki` | [`Boolean!`](#boolean) | Indicates the user can perform `admin_wiki` on this resource. |
...@@ -15062,6 +15099,12 @@ A `PackagesPackageID` is a global ID. It is encoded as a string. ...@@ -15062,6 +15099,12 @@ A `PackagesPackageID` is a global ID. It is encoded as a string.
An example `PackagesPackageID` is: `"gid://gitlab/Packages::Package/1"`. An example `PackagesPackageID` is: `"gid://gitlab/Packages::Package/1"`.
### `PathLockID`
A `PathLockID` is a global ID. It is encoded as a string.
An example `PathLockID` is: `"gid://gitlab/PathLock/1"`.
### `PayloadAlertFieldPathSegment` ### `PayloadAlertFieldPathSegment`
String or integer. String or integer.
......
# frozen_string_literal: true
module EE
module Types
module PermissionTypes
module Project
extend ActiveSupport::Concern
prepended do
ability_field :admin_path_locks
end
end
end
end
end
...@@ -142,6 +142,13 @@ module EE ...@@ -142,6 +142,13 @@ module EE
null: true, null: true,
description: "The project's push rules settings.", description: "The project's push rules settings.",
method: :push_rule method: :push_rule
field :path_locks,
::Types::PathLockType.connection_type,
null: true,
description: "The project's path locks.",
extras: [:lookahead],
resolver: ::Resolvers::PathLocksResolver
end end
def api_fuzzing_ci_configuration def api_fuzzing_ci_configuration
......
# frozen_string_literal: true
module Resolvers
class PathLocksResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
include LooksAhead
authorize :download_code
type Types::PathLockType, null: true
alias_method :project, :object
def resolve_with_lookahead(**args)
authorize!(project)
return [] unless path_lock_feature_enabled?
find_path_locks(args)
end
private
def preloads
{ user: [:user] }
end
def find_path_locks(args)
apply_lookahead(project.path_locks)
end
def path_lock_feature_enabled?
project.licensed_feature_available?(:file_locks)
end
end
end
# frozen_string_literal: true
module Types
class PathLockType < BaseObject # rubocop:disable Graphql/AuthorizeTypes
graphql_name 'PathLock'
description 'Represents a file or directory in the project repository that has been locked.'
field :id, ::Types::GlobalIDType[PathLock], null: false,
description: 'ID of the path lock.'
field :path, GraphQL::STRING_TYPE, null: true,
description: 'The locked path.'
field :user, ::Types::UserType, null: true,
description: 'The user that has locked this path.'
end
end
---
title: Expose Path Locks in the GraphQL API
merge_request: 61380
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::PathLocksResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:path_lock) { create(:path_lock, path: 'README.md', project: project) }
let(:user) { project.owner }
describe '#resolve' do
subject(:resolve_path_locks) { resolve(described_class, obj: project, lookahead: positive_lookahead, ctx: { current_user: user }) }
context 'feature is not licensed' do
before do
stub_licensed_features(file_locks: false)
end
it { is_expected.to be_empty }
end
context 'feature is licensed' do
before do
stub_licensed_features(file_locks: true)
end
it { is_expected.to contain_exactly(path_lock) }
it 'preloads users' do
path_lock = resolve_path_locks.first
expect(path_lock.association_cached?(:user)).to be_truthy
end
context 'user is unauthorized' do
let(:user) { create(:user) }
it { expect { resolve_path_locks }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['PathLock'] do
it { expect(described_class.graphql_name).to eq('PathLock') }
it 'has the expected fields' do
expect(described_class).to have_graphql_fields(:id, :path, :user)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::PermissionTypes::Project do
specify do
expected_permissions = [:admin_path_locks]
expected_permissions.each do |permission|
expect(described_class).to have_graphql_field(permission)
end
end
end
...@@ -20,7 +20,7 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -20,7 +20,7 @@ RSpec.describe GitlabSchema.types['Project'] do
vulnerabilities vulnerability_scanners requirement_states_count vulnerabilities vulnerability_scanners requirement_states_count
vulnerability_severities_count packages compliance_frameworks vulnerabilities_count_by_day vulnerability_severities_count packages compliance_frameworks vulnerabilities_count_by_day
security_dashboard_path iterations iteration_cadences cluster_agents repository_size_excess actual_repository_size_limit security_dashboard_path iterations iteration_cadences cluster_agents repository_size_excess actual_repository_size_limit
code_coverage_summary api_fuzzing_ci_configuration code_coverage_summary api_fuzzing_ci_configuration path_locks
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.project(fullPath).pathLocks' do
using RSpec::Parameterized::TableSyntax
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, namespace: user.namespace) }
let_it_be(:path_lock) { create(:path_lock, project: project, path: 'README.md') }
subject(:path_locks_response) do
post_graphql(
graphql_query_for(
:project, { full_path: project.full_path }, "pathLocks { nodes { #{all_graphql_fields_for('PathLock')} } }"
),
current_user: user
)
graphql_data_at(:project, :pathLocks, :nodes)
end
context 'unlicensed feature' do
before do
stub_licensed_features(file_locks: false)
end
it { is_expected.to be_empty }
end
context 'licensed feature' do
before do
stub_licensed_features(file_locks: true)
end
it 'returns path locks' do
is_expected.to match_array(
a_hash_including('id' => path_lock.to_global_id.to_s, 'path' => 'README.md')
)
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment