Commit 386e5740 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent b1ffdbb7
# frozen_string_literal: true
module Mutations
module MergeRequests
class SetAssignees < Base
graphql_name 'MergeRequestSetAssignees'
argument :assignee_usernames,
[GraphQL::STRING_TYPE],
required: true,
description: <<~DESC
The usernames to assign to the merge request. Replaces existing assignees by default.
DESC
argument :operation_mode,
Types::MutationOperationModeEnum,
required: false,
description: <<~DESC
The operation to perform. Defaults to REPLACE.
DESC
def resolve(project_path:, iid:, assignee_usernames:, operation_mode: Types::MutationOperationModeEnum.enum[:replace])
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab/issues/36098')
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
assignee_ids = []
assignee_ids += merge_request.assignees.map(&:id) if Types::MutationOperationModeEnum.enum.values_at(:remove, :append).include?(operation_mode)
user_ids = UsersFinder.new(current_user, username: assignee_usernames).execute.map(&:id)
if operation_mode == Types::MutationOperationModeEnum.enum[:remove]
assignee_ids -= user_ids
else
assignee_ids |= user_ids
end
::MergeRequests::UpdateService.new(project, current_user, assignee_ids: assignee_ids)
.execute(merge_request)
{
merge_request: merge_request,
errors: merge_request.errors.full_messages
}
end
end
end
end
# frozen_string_literal: true
module Types
class MutationOperationModeEnum < BaseEnum
graphql_name 'MutationOperationMode'
description 'Different toggles for changing mutator behavior.'
# Suggested param name for the enum: `operation_mode`
value 'REPLACE', 'Performs a replace operation'
value 'APPEND', 'Performs an append operation'
value 'REMOVE', 'Performs a removal operation'
end
end
...@@ -11,6 +11,7 @@ module Types ...@@ -11,6 +11,7 @@ module Types
mount_mutation Mutations::AwardEmojis::Toggle mount_mutation Mutations::AwardEmojis::Toggle
mount_mutation Mutations::MergeRequests::SetMilestone mount_mutation Mutations::MergeRequests::SetMilestone
mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true mount_mutation Mutations::MergeRequests::SetWip, calls_gitaly: true
mount_mutation Mutations::MergeRequests::SetAssignees
mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true mount_mutation Mutations::Notes::Create::Note, calls_gitaly: true
mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true mount_mutation Mutations::Notes::Create::DiffNote, calls_gitaly: true
mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true
......
...@@ -60,6 +60,7 @@ class Milestone < ApplicationRecord ...@@ -60,6 +60,7 @@ class Milestone < ApplicationRecord
validates :group, presence: true, unless: :project validates :group, presence: true, unless: :project
validates :project, presence: true, unless: :group validates :project, presence: true, unless: :group
validates :title, presence: true
validate :uniqueness_of_title, if: :title_changed? validate :uniqueness_of_title, if: :title_changed?
validate :milestone_type_check validate :milestone_type_check
......
---
title: Ensure milestone titles are never empty
merge_request: 19985
author:
type: fixed
---
title: Add MergeRequestSetAssignees GraphQL mutation
merge_request: 19272
author:
type: added
# frozen_string_literal: true
class EnsureNoEmptyMilestoneTitles < ActiveRecord::Migration[5.2]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
loop do
rows_updated = exec_update <<~SQL
UPDATE milestones SET title = '%BLANK' WHERE id IN (SELECT id FROM milestones WHERE title = '' LIMIT 500)
SQL
break if rows_updated < 500
end
end
def down; end
end
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_11_12_221821) do ActiveRecord::Schema.define(version: 2019_11_12_232338) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm" enable_extension "pg_trgm"
......
...@@ -3357,6 +3357,56 @@ type MergeRequestPermissions { ...@@ -3357,6 +3357,56 @@ type MergeRequestPermissions {
updateMergeRequest: Boolean! updateMergeRequest: Boolean!
} }
"""
Autogenerated input type of MergeRequestSetAssignees
"""
input MergeRequestSetAssigneesInput {
"""
The usernames to assign to the merge request. Replaces existing assignees by default.
"""
assigneeUsernames: [String!]!
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The iid of the merge request to mutate
"""
iid: String!
"""
The operation to perform. Defaults to REPLACE.
"""
operationMode: MutationOperationMode
"""
The project the merge request to mutate is in
"""
projectPath: ID!
}
"""
Autogenerated return type of MergeRequestSetAssignees
"""
type MergeRequestSetAssigneesPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The merge request after mutation
"""
mergeRequest: MergeRequest
}
""" """
Autogenerated input type of MergeRequestSetMilestone Autogenerated input type of MergeRequestSetMilestone
""" """
...@@ -3537,6 +3587,7 @@ type Mutation { ...@@ -3537,6 +3587,7 @@ type Mutation {
destroyNote(input: DestroyNoteInput!): DestroyNotePayload destroyNote(input: DestroyNoteInput!): DestroyNotePayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
mergeRequestSetAssignees(input: MergeRequestSetAssigneesInput!): MergeRequestSetAssigneesPayload
mergeRequestSetMilestone(input: MergeRequestSetMilestoneInput!): MergeRequestSetMilestonePayload mergeRequestSetMilestone(input: MergeRequestSetMilestoneInput!): MergeRequestSetMilestonePayload
mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload
...@@ -3546,6 +3597,26 @@ type Mutation { ...@@ -3546,6 +3597,26 @@ type Mutation {
updateNote(input: UpdateNoteInput!): UpdateNotePayload updateNote(input: UpdateNoteInput!): UpdateNotePayload
} }
"""
Different toggles for changing mutator behavior.
"""
enum MutationOperationMode {
"""
Performs an append operation
"""
APPEND
"""
Performs a removal operation
"""
REMOVE
"""
Performs a replace operation
"""
REPLACE
}
type Namespace { type Namespace {
""" """
Description of the namespace Description of the namespace
......
...@@ -14736,6 +14736,33 @@ ...@@ -14736,6 +14736,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "mergeRequestSetAssignees",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "MergeRequestSetAssigneesInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "MergeRequestSetAssigneesPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "mergeRequestSetMilestone", "name": "mergeRequestSetMilestone",
"description": null, "description": null,
...@@ -15676,6 +15703,183 @@ ...@@ -15676,6 +15703,183 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "MergeRequestSetAssigneesPayload",
"description": "Autogenerated return type of MergeRequestSetAssignees",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Reasons why the mutation failed.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "mergeRequest",
"description": "The merge request after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "MergeRequest",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "MergeRequestSetAssigneesInput",
"description": "Autogenerated input type of MergeRequestSetAssignees",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project the merge request to mutate is in",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "iid",
"description": "The iid of the merge request to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "assigneeUsernames",
"description": "The usernames to assign to the merge request. Replaces existing assignees by default.\n",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"defaultValue": null
},
{
"name": "operationMode",
"description": "The operation to perform. Defaults to REPLACE.\n",
"type": {
"kind": "ENUM",
"name": "MutationOperationMode",
"ofType": null
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "MutationOperationMode",
"description": "Different toggles for changing mutator behavior.",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "REPLACE",
"description": "Performs a replace operation",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "APPEND",
"description": "Performs an append operation",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "REMOVE",
"description": "Performs a removal operation",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "CreateNotePayload", "name": "CreateNotePayload",
......
...@@ -483,6 +483,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -483,6 +483,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `cherryPickOnCurrentMergeRequest` | Boolean! | Whether or not a user can perform `cherry_pick_on_current_merge_request` on this resource | | `cherryPickOnCurrentMergeRequest` | Boolean! | Whether or not a user can perform `cherry_pick_on_current_merge_request` on this resource |
| `revertOnCurrentMergeRequest` | Boolean! | Whether or not a user can perform `revert_on_current_merge_request` on this resource | | `revertOnCurrentMergeRequest` | Boolean! | Whether or not a user can perform `revert_on_current_merge_request` on this resource |
### MergeRequestSetAssigneesPayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `mergeRequest` | MergeRequest | The merge request after mutation |
### MergeRequestSetMilestonePayload ### MergeRequestSetMilestonePayload
| Name | Type | Description | | Name | Type | Description |
......
...@@ -178,3 +178,27 @@ Currently, the following tools might not work because their XML formats are unsu ...@@ -178,3 +178,27 @@ Currently, the following tools might not work because their XML formats are unsu
|Case|Tool|Issue| |Case|Tool|Issue|
|---|---|---| |---|---|---|
|`<testcase>` does not have `classname` attribute|ESlint, sass-lint|<https://gitlab.com/gitlab-org/gitlab-foss/issues/50964>| |`<testcase>` does not have `classname` attribute|ESlint, sass-lint|<https://gitlab.com/gitlab-org/gitlab-foss/issues/50964>|
## Viewing JUnit test reports on GitLab
> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/24792) in GitLab 12.5.
If JUnit XML files are generated and uploaded as part of a pipeline, these reports
can be viewed inside the pipelines details page. The **Tests** tab on this page will
display a list of test suites and cases reported from the XML file.
![Test Reports Widget](img/junit_test_report_ui.png)
You can view all the known test suites and click on each of these to see further
details, including the cases that makeup the suite. Cases are ordered by status,
with failed showing at the top, skipped next and successful cases last.
### Enabling the feature
This feature comes with the `:junit_pipeline_view` feature flag disabled by default.
To enable this feature, ask a GitLab administrator with Rails console access to run the
following command:
```ruby
Feature.enable(:junit_pipeline_view)
```
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::MergeRequests::SetAssignees do
let(:merge_request) { create(:merge_request) }
let(:user) { create(:user) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }) }
describe '#resolve' do
let(:assignee) { create(:user) }
let(:assignee2) { create(:user) }
let(:assignee_usernames) { [assignee.username] }
let(:mutated_merge_request) { subject[:merge_request] }
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, assignee_usernames: assignee_usernames) }
before do
merge_request.project.add_developer(assignee)
merge_request.project.add_developer(assignee2)
end
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when the user can update the merge request' do
before do
merge_request.project.add_developer(user)
end
it 'replaces the assignee' do
merge_request.assignees = [assignee2]
merge_request.save!
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.assignees).to contain_exactly(assignee)
expect(subject[:errors]).to be_empty
end
it 'returns errors merge request could not be updated' do
# Make the merge request invalid
merge_request.allow_broken = true
merge_request.update!(source_project: nil)
expect(subject[:errors]).not_to be_empty
end
context 'when passing an empty assignee list' do
let(:assignee_usernames) { [] }
before do
merge_request.assignees = [assignee]
merge_request.save!
end
it 'removes all assignees' do
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.assignees).to eq([])
expect(subject[:errors]).to be_empty
end
end
context 'when passing "append" as true' do
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, assignee_usernames: assignee_usernames, operation_mode: Types::MutationOperationModeEnum.enum[:append]) }
before do
merge_request.assignees = [assignee2]
merge_request.save!
# In CE, APPEND is a NOOP as you can't have multiple assignees
# We test multiple assignment in EE specs
stub_licensed_features(multiple_merge_request_assignees: false)
end
it 'is a NO-OP in FOSS' do
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.assignees).to contain_exactly(assignee2)
expect(subject[:errors]).to be_empty
end
end
context 'when passing "remove" as true' do
before do
merge_request.assignees = [assignee]
merge_request.save!
end
it 'removes named assignee' do
mutated_merge_request = mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, assignee_usernames: assignee_usernames, operation_mode: Types::MutationOperationModeEnum.enum[:remove])[:merge_request]
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.assignees).to eq([])
expect(subject[:errors]).to be_empty
end
it 'does not remove unnamed assignee' do
mutated_merge_request = mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, assignee_usernames: [assignee2.username], operation_mode: Types::MutationOperationModeEnum.enum[:remove])[:merge_request]
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.assignees).to contain_exactly(assignee)
expect(subject[:errors]).to be_empty
end
end
end
end
end
...@@ -55,6 +55,17 @@ describe Milestone do ...@@ -55,6 +55,17 @@ describe Milestone do
end end
end end
describe 'title' do
it { is_expected.to validate_presence_of(:title) }
it 'is invalid if title would be empty after sanitation' do
milestone = build(:milestone, project: project, title: '<img src=x onerror=prompt(1)>')
expect(milestone).not_to be_valid
expect(milestone.errors[:title]).to include("can't be blank")
end
end
describe 'milestone_releases' do describe 'milestone_releases' do
let(:milestone) { build(:milestone, project: project) } let(:milestone) { build(:milestone, project: project) }
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Setting assignees of a merge request' do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:assignee) { create(:user) }
let(:assignee2) { create(:user) }
let(:input) { { assignee_usernames: [assignee.username] } }
let(:expected_result) do
[{ 'username' => assignee.username }]
end
let(:mutation) do
variables = {
project_path: project.full_path,
iid: merge_request.iid.to_s
}
graphql_mutation(:merge_request_set_assignees, variables.merge(input),
<<-QL.strip_heredoc
clientMutationId
errors
mergeRequest {
id
assignees {
nodes {
username
}
}
}
QL
)
end
def mutation_response
graphql_mutation_response(:merge_request_set_assignees)
end
def mutation_assignee_nodes
mutation_response['mergeRequest']['assignees']['nodes']
end
before do
project.add_developer(current_user)
project.add_developer(assignee)
project.add_developer(assignee2)
end
it 'returns an error if the user is not allowed to update the merge request' do
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).not_to be_empty
end
it 'does not allow members without the right permission to add assignees' do
user = create(:user)
project.add_guest(user)
post_graphql_mutation(mutation, current_user: user)
expect(graphql_errors).not_to be_empty
end
context 'with assignees already assigned' do
before do
merge_request.assignees = [assignee2]
merge_request.save!
end
it 'replaces the assignee' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_assignee_nodes).to match_array(expected_result)
end
end
context 'when passing an empty list of assignees' do
let(:input) { { assignee_usernames: [] } }
before do
merge_request.assignees = [assignee2]
merge_request.save!
end
it 'removes assignee' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_assignee_nodes).to eq([])
end
end
context 'when passing append as true' do
let(:input) { { assignee_usernames: [assignee2.username], operation_mode: Types::MutationOperationModeEnum.enum[:append] } }
before do
# In CE, APPEND is a NOOP as you can't have multiple assignees
# We test multiple assignment in EE specs
stub_licensed_features(multiple_merge_request_assignees: false)
merge_request.assignees = [assignee]
merge_request.save!
end
it 'does not replace the assignee in CE' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_assignee_nodes).to match_array(expected_result)
end
end
context 'when passing remove as true' do
let(:input) { { assignee_usernames: [assignee.username], operation_mode: Types::MutationOperationModeEnum.enum[:remove] } }
let(:expected_result) { [] }
before do
merge_request.assignees = [assignee]
merge_request.save!
end
it 'removes the users in the list, while adding none' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_assignee_nodes).to match_array(expected_result)
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