Commit 3ff382cd authored by Heinrich Lee Yu's avatar Heinrich Lee Yu

Merge branch 'ajk-graphql-group-mrs' into 'master'

Add Group.mergeRequests field

See merge request gitlab-org/gitlab!43863
parents 004cb345 bde9e7c4
......@@ -2,6 +2,8 @@
module Resolvers
class AssignedMergeRequestsResolver < UserMergeRequestsResolver
accept_author
def user_role
:assignee
end
......
......@@ -2,6 +2,8 @@
module Resolvers
class AuthoredMergeRequestsResolver < UserMergeRequestsResolver
accept_assignee
def user_role
:author
end
......
# frozen_string_literal: true
module GroupIssuableResolver
extend ActiveSupport::Concern
class_methods do
def include_subgroups(name_of_things)
argument :include_subgroups, GraphQL::BOOLEAN_TYPE,
required: false,
default_value: false,
description: "Include #{name_of_things} belonging to subgroups"
end
end
end
......@@ -12,7 +12,7 @@ module ResolvesMergeRequests
def resolve_with_lookahead(**args)
mr_finder = MergeRequestsFinder.new(current_user, args.compact)
finder = Gitlab::Graphql::Loaders::IssuableLoader.new(project, mr_finder)
finder = Gitlab::Graphql::Loaders::IssuableLoader.new(mr_parent, mr_finder)
select_result(finder.batching_find_all { |query| apply_lookahead(query) })
end
......@@ -29,6 +29,10 @@ module ResolvesMergeRequests
private
def mr_parent
project
end
def unconditional_includes
[:target_project]
end
......
......@@ -2,9 +2,8 @@
module Resolvers
class GroupIssuesResolver < IssuesResolver
argument :include_subgroups, GraphQL::BOOLEAN_TYPE,
required: false,
default_value: false,
description: 'Include issues belonging to subgroups.'
include GroupIssuableResolver
include_subgroups 'issues'
end
end
# frozen_string_literal: true
module Resolvers
class GroupMergeRequestsResolver < MergeRequestsResolver
include GroupIssuableResolver
alias_method :group, :synchronized_object
include_subgroups 'merge requests'
accept_assignee
accept_author
def project
nil
end
def mr_parent
group
end
def no_results_possible?(args)
group.nil? || some_argument_is_empty?(args)
end
end
end
......@@ -6,6 +6,18 @@ module Resolvers
alias_method :project, :synchronized_object
def self.accept_assignee
argument :assignee_username, GraphQL::STRING_TYPE,
required: false,
description: 'Username of the assignee'
end
def self.accept_author
argument :author_username, GraphQL::STRING_TYPE,
required: false,
description: 'Username of the author'
end
argument :iids, [GraphQL::STRING_TYPE],
required: false,
description: 'Array of IIDs of merge requests, for example `[1, 2]`'
......
......@@ -2,11 +2,7 @@
module Resolvers
class ProjectMergeRequestsResolver < MergeRequestsResolver
argument :assignee_username, GraphQL::STRING_TYPE,
required: false,
description: 'Username of the assignee'
argument :author_username, GraphQL::STRING_TYPE,
required: false,
description: 'Username of the author'
accept_assignee
accept_author
end
end
......@@ -46,9 +46,15 @@ module Types
field :issues,
Types::IssueType.connection_type,
null: true,
description: 'Issues of the group',
description: 'Issues for projects in this group',
resolver: Resolvers::GroupIssuesResolver
field :merge_requests,
Types::MergeRequestType.connection_type,
null: true,
description: 'Merge requests for projects in this group',
resolver: Resolvers::GroupMergeRequestsResolver
field :milestones, Types::MilestoneType.connection_type, null: true,
description: 'Milestones of the group',
resolver: Resolvers::GroupMilestonesResolver
......
---
title: Enable querying for merge requests within a group
merge_request: 43863
author:
type: added
......@@ -7349,7 +7349,7 @@ type Group {
isTemporaryStorageIncreaseEnabled: Boolean!
"""
Issues of the group
Issues for projects in this group
"""
issues(
"""
......@@ -7418,7 +7418,7 @@ type Group {
iids: [String!]
"""
Include issues belonging to subgroups.
Include issues belonging to subgroups
"""
includeSubgroups: Boolean = false
......@@ -7585,6 +7585,91 @@ type Group {
"""
mentionsDisabled: Boolean
"""
Merge requests for projects in this group
"""
mergeRequests(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Username of the assignee
"""
assigneeUsername: String
"""
Username of the author
"""
authorUsername: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Array of IIDs of merge requests, for example `[1, 2]`
"""
iids: [String!]
"""
Include merge requests belonging to subgroups
"""
includeSubgroups: Boolean = false
"""
Array of label names. All resolved merge requests will have all of these labels.
"""
labels: [String!]
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
Merge requests merged after this date
"""
mergedAfter: Time
"""
Merge requests merged before this date
"""
mergedBefore: Time
"""
Title of the milestone
"""
milestoneTitle: String
"""
Sort merge requests by this criteria
"""
sort: MergeRequestSort = created_desc
"""
Array of source branch names. All resolved merge requests will have one of these branches as their source.
"""
sourceBranches: [String!]
"""
A merge request state. If provided, all resolved merge requests will have this state.
"""
state: MergeRequestState
"""
Array of target branch names. All resolved merge requests will have one of these branches as their target.
"""
targetBranches: [String!]
): MergeRequestConnection
"""
Milestones of the group
"""
......@@ -19180,6 +19265,11 @@ type User {
"""
after: String
"""
Username of the author
"""
authorUsername: String
"""
Returns the elements in the list that come before the specified cursor.
"""
......@@ -19260,6 +19350,11 @@ type User {
"""
after: String
"""
Username of the assignee
"""
assigneeUsername: String
"""
Returns the elements in the list that come before the specified cursor.
"""
......
......@@ -20302,7 +20302,7 @@
},
{
"name": "issues",
"description": "Issues of the group",
"description": "Issues for projects in this group",
"args": [
{
"name": "iid",
......@@ -20532,7 +20532,7 @@
},
{
"name": "includeSubgroups",
"description": "Include issues belonging to subgroups.",
"description": "Include issues belonging to subgroups",
"type": {
"kind": "SCALAR",
"name": "Boolean",
......@@ -20830,6 +20830,211 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeRequests",
"description": "Merge requests for projects in this group",
"args": [
{
"name": "iids",
"description": "Array of IIDs of merge requests, for example `[1, 2]`",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "sourceBranches",
"description": "Array of source branch names. All resolved merge requests will have one of these branches as their source.",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "targetBranches",
"description": "Array of target branch names. All resolved merge requests will have one of these branches as their target.",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "state",
"description": "A merge request state. If provided, all resolved merge requests will have this state.",
"type": {
"kind": "ENUM",
"name": "MergeRequestState",
"ofType": null
},
"defaultValue": null
},
{
"name": "labels",
"description": "Array of label names. All resolved merge requests will have all of these labels.",
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
},
"defaultValue": null
},
{
"name": "mergedAfter",
"description": "Merge requests merged after this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "mergedBefore",
"description": "Merge requests merged before this date",
"type": {
"kind": "SCALAR",
"name": "Time",
"ofType": null
},
"defaultValue": null
},
{
"name": "milestoneTitle",
"description": "Title of the milestone",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "sort",
"description": "Sort merge requests by this criteria",
"type": {
"kind": "ENUM",
"name": "MergeRequestSort",
"ofType": null
},
"defaultValue": "created_desc"
},
{
"name": "includeSubgroups",
"description": "Include merge requests belonging to subgroups",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": "false"
},
{
"name": "assigneeUsername",
"description": "Username of the assignee",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "authorUsername",
"description": "Username of the author",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "MergeRequestConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "milestones",
"description": "Milestones of the group",
......@@ -56028,6 +56233,16 @@
},
"defaultValue": null
},
{
"name": "authorUsername",
"description": "Username of the author",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
......@@ -56223,6 +56438,16 @@
},
"defaultValue": null
},
{
"name": "assigneeUsername",
"description": "Username of the assignee",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
......@@ -164,6 +164,10 @@ FactoryBot.define do
target_branch { generate(:branch) }
end
trait :unique_author do
author { association(:user) }
end
trait :with_coverage_reports do
after(:build) do |merge_request|
merge_request.head_pipeline = build(
......
......@@ -17,6 +17,7 @@ RSpec.describe GitlabSchema.types['Group'] do
subgroup_creation_level require_two_factor_authentication
two_factor_grace_period auto_devops_enabled emails_disabled
mentions_disabled parent boards milestones group_members
merge_requests
]
expect(described_class).to include_graphql_fields(*expected_fields)
......
# frozen_string_literal: true
require 'spec_helper'
# Based on ee/spec/requests/api/epics_spec.rb
# Should follow closely in order to ensure all situations are covered
RSpec.describe 'Query.group.mergeRequests' do
include GraphqlHelpers
let_it_be(:group) { create(:group) }
let_it_be(:sub_group) { create(:group, parent: group) }
let_it_be(:project_a) { create(:project, :repository, group: group) }
let_it_be(:project_b) { create(:project, :repository, group: group) }
let_it_be(:project_c) { create(:project, :repository, group: sub_group) }
let_it_be(:project_x) { create(:project, :repository) }
let_it_be(:user) { create(:user, developer_projects: [project_x]) }
let_it_be(:mr_attrs) do
{ target_branch: 'master' }
end
let_it_be(:mr_traits) do
[:unique_branches, :unique_author]
end
let_it_be(:mrs_a, reload: true) { create_list(:merge_request, 2, *mr_traits, **mr_attrs, source_project: project_a) }
let_it_be(:mrs_b, reload: true) { create_list(:merge_request, 2, *mr_traits, **mr_attrs, source_project: project_b) }
let_it_be(:mrs_c, reload: true) { create_list(:merge_request, 2, *mr_traits, **mr_attrs, source_project: project_c) }
let_it_be(:other_mr) { create(:merge_request, source_project: project_x) }
let(:mrs_data) { graphql_data_at(:group, :merge_requests, :nodes) }
before do
group.add_developer(user)
end
def expected_mrs(mrs)
mrs.map { |mr| a_hash_including('id' => global_id_of(mr)) }
end
describe 'not passing any arguments' do
let(:query) do
<<~GQL
query($path: ID!) {
group(fullPath: $path) {
mergeRequests { nodes { id } }
}
}
GQL
end
it 'can find all merge requests in the group, excluding sub-groups' do
post_graphql(query, current_user: user, variables: { path: group.full_path })
expect(mrs_data).to match_array(expected_mrs(mrs_a + mrs_b))
end
end
describe 'restricting by author' do
let(:query) do
<<~GQL
query($path: ID!, $user: String) {
group(fullPath: $path) {
mergeRequests(authorUsername: $user) { nodes { id author { username } } }
}
}
GQL
end
let(:author) { mrs_b.first.author }
it 'can find all merge requests with user as author' do
post_graphql(query, current_user: user, variables: { user: author.username, path: group.full_path })
expect(mrs_data).to match_array(expected_mrs([mrs_b.first]))
end
end
describe 'restricting by assignee' do
let(:query) do
<<~GQL
query($path: ID!, $user: String) {
group(fullPath: $path) {
mergeRequests(assigneeUsername: $user) { nodes { id } }
}
}
GQL
end
let_it_be(:assignee) { create(:user) }
before_all do
mrs_b.second.assignees << assignee
mrs_a.first.assignees << assignee
end
it 'can find all merge requests assigned to user' do
post_graphql(query, current_user: user, variables: { user: assignee.username, path: group.full_path })
expect(mrs_data).to match_array(expected_mrs([mrs_a.first, mrs_b.second]))
end
end
describe 'passing include_subgroups: true' do
let(:query) do
<<~GQL
query($path: ID!) {
group(fullPath: $path) {
mergeRequests(includeSubgroups: true) { nodes { id } }
}
}
GQL
end
it 'can find all merge requests in the group, including sub-groups' do
post_graphql(query, current_user: user, variables: { path: group.full_path })
expect(mrs_data).to match_array(expected_mrs(mrs_a + mrs_b + mrs_c))
end
end
end
......@@ -29,15 +29,15 @@ RSpec.describe 'getting user information' do
let_it_be(:unauthorized_user) { create(:user) }
let_it_be(:assigned_mr) do
create(:merge_request, :unique_branches,
create(:merge_request, :unique_branches, :unique_author,
source_project: project_a, assignees: [user])
end
let_it_be(:assigned_mr_b) do
create(:merge_request, :unique_branches,
create(:merge_request, :unique_branches, :unique_author,
source_project: project_b, assignees: [user])
end
let_it_be(:assigned_mr_c) do
create(:merge_request, :unique_branches,
create(:merge_request, :unique_branches, :unique_author,
source_project: project_b, assignees: [user])
end
let_it_be(:authored_mr) do
......@@ -133,6 +133,17 @@ RSpec.describe 'getting user information' do
)
end
end
context 'filtering by author' do
let(:author) { assigned_mr_b.author }
let(:mr_args) { { author_username: author.username } }
it 'finds the authored mrs' do
expect(assigned_mrs).to contain_exactly(
a_hash_including('id' => global_id_of(assigned_mr_b))
)
end
end
end
context 'the current user does not have access' do
......@@ -172,6 +183,23 @@ RSpec.describe 'getting user information' do
end
end
context 'filtering by assignee' do
let(:assignee) { create(:user) }
let(:mr_args) { { assignee_username: assignee.username } }
it 'finds the assigned mrs' do
authored_mr.assignees << assignee
authored_mr_c.assignees << assignee
post_graphql(query, current_user: current_user)
expect(authored_mrs).to contain_exactly(
a_hash_including('id' => global_id_of(authored_mr)),
a_hash_including('id' => global_id_of(authored_mr_c))
)
end
end
context 'filtering by project path and IID' do
let(:mr_args) do
{ project_path: project_b.full_path, iids: [authored_mr_b.iid.to_s] }
......@@ -253,8 +281,10 @@ RSpec.describe 'getting user information' do
let(:current_user) { user }
it 'can be found' do
expect(assigned_mrs).to include(
a_hash_including('id' => global_id_of(assigned_mr))
expect(assigned_mrs).to contain_exactly(
a_hash_including('id' => global_id_of(assigned_mr)),
a_hash_including('id' => global_id_of(assigned_mr_b)),
a_hash_including('id' => global_id_of(assigned_mr_c))
)
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