Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-21 15:10:28 +00:00
parent 4944e9e5a9
commit 63324f9cfd
57 changed files with 755 additions and 306 deletions

View File

@ -1125,33 +1125,6 @@ Rails/SaveBang:
- 'spec/requests/api/labels_spec.rb'
- 'spec/requests/api/project_import_spec.rb'
- 'spec/requests/projects/cycle_analytics_events_spec.rb'
- 'spec/services/auth/container_registry_authentication_service_spec.rb'
- 'spec/services/auto_merge/base_service_spec.rb'
- 'spec/services/auto_merge_service_spec.rb'
- 'spec/services/clusters/update_service_spec.rb'
- 'spec/services/deployments/after_create_service_spec.rb'
- 'spec/services/design_management/generate_image_versions_service_spec.rb'
- 'spec/services/discussions/resolve_service_spec.rb'
- 'spec/services/draft_notes/destroy_service_spec.rb'
- 'spec/services/emails/confirm_service_spec.rb'
- 'spec/services/groups/destroy_service_spec.rb'
- 'spec/services/groups/import_export/import_service_spec.rb'
- 'spec/services/labels/promote_service_spec.rb'
- 'spec/services/notes/create_service_spec.rb'
- 'spec/services/notification_recipients/build_service_spec.rb'
- 'spec/services/notification_service_spec.rb'
- 'spec/services/packages/conan/create_package_file_service_spec.rb'
- 'spec/services/reset_project_cache_service_spec.rb'
- 'spec/services/resource_events/change_milestone_service_spec.rb'
- 'spec/services/system_hooks_service_spec.rb'
- 'spec/services/system_note_service_spec.rb'
- 'spec/services/system_notes/issuables_service_spec.rb'
- 'spec/services/todo_service_spec.rb'
- 'spec/services/todos/destroy/confidential_issue_service_spec.rb'
- 'spec/services/users/destroy_service_spec.rb'
- 'spec/services/users/repair_ldap_blocked_service_spec.rb'
- 'spec/services/verify_pages_domain_service_spec.rb'
- 'spec/sidekiq/cron/job_gem_dependency_spec.rb'
# Offense count: 187
# Cop supports --auto-correct.
@ -1269,11 +1242,10 @@ RSpec/TimecopTravel:
- 'spec/workers/concerns/reenqueuer_spec.rb'
- 'spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb'
# Offense count: 43
# Offense count: 21
Graphql/IDType:
Exclude:
- 'ee/app/graphql/ee/mutations/issues/update.rb'
- 'ee/app/graphql/ee/types/boards/board_issue_input_base_type.rb'
- 'ee/app/graphql/mutations/iterations/update.rb'
- 'ee/app/graphql/resolvers/iterations_resolver.rb'
- 'app/graphql/mutations/boards/issues/issue_move_list.rb'

View File

@ -12,6 +12,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
tag: __('Yes or No'),
lowercaseValueOnSubmit: true,
capitalizeTokenValue: true,
hideNotEqual: true,
},
conditions: [
{
@ -30,20 +31,6 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
value: __('No'),
operator: '=',
},
{
url: 'not[wip]=yes',
replacementUrl: 'not[draft]=yes',
tokenKey: 'draft',
value: __('Yes'),
operator: '!=',
},
{
url: 'not[wip]=no',
replacementUrl: 'not[draft]=no',
tokenKey: 'draft',
value: __('No'),
operator: '!=',
},
],
};

View File

@ -39,7 +39,7 @@ export default class DropdownOperator extends FilteredSearchDropdown {
this.dispatchInputEvent();
}
renderContent(forceShowList = false) {
renderContent(forceShowList = false, dropdownName = '') {
const dropdownData = [
{
tag: 'equal',
@ -48,8 +48,9 @@ export default class DropdownOperator extends FilteredSearchDropdown {
help: __('is'),
},
];
const dropdownToken = this.tokenKeys.searchByKey(dropdownName.toLowerCase());
if (gon.features?.notIssuableQueries) {
if (gon.features?.notIssuableQueries && !dropdownToken?.hideNotEqual) {
dropdownData.push({
tag: 'not-equal',
type: 'string',

View File

@ -83,16 +83,16 @@ export default class FilteredSearchDropdown {
}
}
render(forceRenderContent = false, forceShowList = false) {
render(forceRenderContent = false, forceShowList = false, hideNotEqual = false) {
this.setAsDropdown();
const currentHook = this.getCurrentHook();
const firstTimeInitialized = currentHook === null;
if (firstTimeInitialized || forceRenderContent) {
this.renderContent(forceShowList);
this.renderContent(forceShowList, hideNotEqual);
} else if (currentHook.list.list.id !== this.dropdown.id) {
this.renderContent(forceShowList);
this.renderContent(forceShowList, hideNotEqual);
}
}

View File

@ -107,7 +107,7 @@ export default class FilteredSearchDropdownManager {
this.mapping[key].reference.setOffset(offset);
}
load(key, firstLoad = false) {
load(key, firstLoad = false, dropdownKey = '') {
const mappingKey = this.mapping[key];
const glClass = mappingKey.gl;
const { element } = mappingKey;
@ -141,12 +141,12 @@ export default class FilteredSearchDropdownManager {
}
this.updateDropdownOffset(key);
mappingKey.reference.render(firstLoad, forceShowList);
mappingKey.reference.render(firstLoad, forceShowList, dropdownKey);
this.currentDropdown = key;
}
loadDropdown(dropdownName = '') {
loadDropdown(dropdownName = '', dropdownKey = '') {
let firstLoad = false;
if (!this.droplab) {
@ -155,7 +155,7 @@ export default class FilteredSearchDropdownManager {
}
if (dropdownName === DROPDOWN_TYPE.operator) {
this.load(dropdownName, firstLoad);
this.load(dropdownName, firstLoad, dropdownKey);
return;
}
@ -167,7 +167,7 @@ export default class FilteredSearchDropdownManager {
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
const key = match && match.key ? match.key : DROPDOWN_TYPE.hint;
this.load(key, firstLoad);
this.load(key, firstLoad, dropdownKey);
}
}
@ -200,11 +200,11 @@ export default class FilteredSearchDropdownManager {
dropdownToOpen = hasOperator && lastOperatorToken ? dropdownName : DROPDOWN_TYPE.operator;
}
this.loadDropdown(dropdownToOpen);
this.loadDropdown(dropdownToOpen, dropdownName);
} else if (lastToken) {
const lastOperator = FilteredSearchVisualTokens.getLastTokenOperator();
// Token has been initialized into an object because it has a value
this.loadDropdown(lastOperator ? lastToken.key : DROPDOWN_TYPE.operator);
this.loadDropdown(lastOperator ? lastToken.key : DROPDOWN_TYPE.operator, lastToken.key);
} else {
this.loadDropdown(DROPDOWN_TYPE.hint);
}

View File

@ -1,17 +1,19 @@
<script>
/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
import { GlButton } from '@gitlab/ui';
import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale';
import { deprecatedCreateFlash as Flash } from '~/flash';
export default {
components: {
GlModal: DeprecatedModal2,
GlModal,
GlButton,
},
directives: {
GlModalDirective,
SafeHtml,
},
props: {
actionUrl: {
type: String,
@ -54,6 +56,21 @@ Please update your Git repository remotes as soon as possible.`),
false,
);
},
primaryProps() {
return {
text: s__('Update username'),
attributes: [
{ variant: 'warning' },
{ category: 'primary' },
{ disabled: this.isRequestPending },
],
};
},
cancelProps() {
return {
text: s__('Cancel'),
};
},
},
methods: {
onConfirm() {
@ -103,22 +120,21 @@ Please update your Git repository remotes as soon as possible.`),
<p class="form-text text-muted">{{ path }}</p>
</div>
<gl-button
:data-target="`#${$options.modalId}`"
v-gl-modal-directive="$options.modalId"
:disabled="isRequestPending || newUsername === username"
category="primary"
variant="warning"
data-toggle="modal"
data-testid="username-change-confirmation-modal"
>{{ $options.buttonText }}</gl-button
>
{{ $options.buttonText }}
</gl-button>
<gl-modal
:id="$options.modalId"
:header-title-text="s__('Profiles|Change username') + '?'"
:footer-primary-button-text="$options.buttonText"
footer-primary-button-variant="warning"
@submit="onConfirm"
:modal-id="$options.modalId"
:title="s__('Profiles|Change username') + '?'"
:action-primary="primaryProps"
:action-cancel="cancelProps"
@primary="onConfirm"
>
<span v-html="modalText"></span>
<span v-safe-html="modalText"></span>
</gl-modal>
</div>
</template>

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
module Resolvers
module Ci
class RunnerSetupResolver < BaseResolver
type Types::Ci::RunnerSetupType, null: true
argument :platform, GraphQL::STRING_TYPE,
required: true,
description: 'Platform to generate the instructions for'
argument :architecture, GraphQL::STRING_TYPE,
required: true,
description: 'Architecture to generate the instructions for'
argument :project_id, ::Types::GlobalIDType[::Project],
required: false,
description: 'Project to register the runner for'
argument :group_id, ::Types::GlobalIDType[::Group],
required: false,
description: 'Group to register the runner for'
def resolve(platform:, architecture:, **args)
instructions = Gitlab::Ci::RunnerInstructions.new(
{ current_user: current_user, os: platform, arch: architecture }.merge(target_param(args))
)
{
install_instructions: instructions.install_script,
register_instructions: instructions.register_command
}
ensure
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'User is not authorized to register a runner for the specified resource!' if instructions.errors.include?('Gitlab::Access::AccessDeniedError')
end
private
def target_param(args)
project_param(args[:project_id]) || group_param(args[:group_id]) || {}
end
def project_param(project_id)
return unless project_id
{ project: find_object(project_id) }
end
def group_param(group_id)
return unless group_id
{ group: find_object(group_id) }
end
def find_object(gid)
GlobalID::Locator.locate(gid)
end
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Types
module Ci
# rubocop: disable Graphql/AuthorizeTypes
class RunnerSetupType < BaseObject
graphql_name 'RunnerSetup'
field :install_instructions, GraphQL::STRING_TYPE, null: false,
description: 'Instructions for installing the runner on the specified architecture'
field :register_instructions, GraphQL::STRING_TYPE, null: false,
description: 'Instructions for registering the runner'
end
end
end

View File

@ -84,6 +84,10 @@ module Types
null: true, description: 'Supported runner platforms',
resolver: Resolvers::Ci::RunnerPlatformsResolver
field :runner_setup, Types::Ci::RunnerSetupType, null: true,
description: 'Get runner setup instructions',
resolver: Resolvers::Ci::RunnerSetupResolver
def design_management
DesignManagementObject.new(nil)
end

View File

@ -0,0 +1,5 @@
---
title: Update change username modal
merge_request: 44325
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix Rails/SaveBang offenses for spec/services/* and spec/sidekiq/*
merge_request: 45391
author: matthewbried
type: other

View File

@ -0,0 +1,5 @@
---
title: Removed not equal filter option for drafts on merge requests
merge_request: 45649
author:
type: fixed

View File

@ -1695,7 +1695,7 @@ input BoardIssueInput {
"""
Filter by epic ID. Incompatible with epicWildcardId
"""
epicId: ID
epicId: EpicID
"""
Filter by epic ID wildcard. Incompatible with epicId
@ -8571,6 +8571,11 @@ type Group {
webUrl: String!
}
"""
Identifier of Group
"""
scalar GroupID
"""
Represents a Group Membership
"""
@ -12568,7 +12573,7 @@ input NegatedBoardIssueInput {
"""
Filter by epic ID. Incompatible with epicWildcardId
"""
epicId: ID
epicId: EpicID
"""
Filter by label name
@ -15629,6 +15634,31 @@ type Query {
last: Int
): RunnerPlatformConnection
"""
Get runner setup instructions
"""
runnerSetup(
"""
Architecture to generate the instructions for
"""
architecture: String!
"""
Group to register the runner for
"""
groupId: GroupID
"""
Platform to generate the instructions for
"""
platform: String!
"""
Project to register the runner for
"""
projectId: ProjectID
): RunnerSetup
"""
Find Snippets visible to the current user
"""
@ -16922,6 +16952,18 @@ type RunnerPlatformEdge {
node: RunnerPlatform
}
type RunnerSetup {
"""
Instructions for installing the runner on the specified architecture
"""
installInstructions: String!
"""
Instructions for registering the runner
"""
registerInstructions: String!
}
"""
Represents a CI configuration of SAST
"""

View File

@ -4518,7 +4518,7 @@
"description": "Filter by epic ID. Incompatible with epicWildcardId",
"type": {
"kind": "SCALAR",
"name": "ID",
"name": "EpicID",
"ofType": null
},
"defaultValue": null
@ -23240,6 +23240,16 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "GroupID",
"description": "Identifier of Group",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "GroupMember",
@ -36941,7 +36951,7 @@
"description": "Filter by epic ID. Incompatible with epicWildcardId",
"type": {
"kind": "SCALAR",
"name": "ID",
"name": "EpicID",
"ofType": null
},
"defaultValue": null
@ -45234,6 +45244,67 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "runnerSetup",
"description": "Get runner setup instructions",
"args": [
{
"name": "platform",
"description": "Platform to generate the instructions for",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "architecture",
"description": "Architecture to generate the instructions for",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "projectId",
"description": "Project to register the runner for",
"type": {
"kind": "SCALAR",
"name": "ProjectID",
"ofType": null
},
"defaultValue": null
},
{
"name": "groupId",
"description": "Group to register the runner for",
"type": {
"kind": "SCALAR",
"name": "GroupID",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "RunnerSetup",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "snippets",
"description": "Find Snippets visible to the current user",
@ -48854,6 +48925,55 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "RunnerSetup",
"description": null,
"fields": [
{
"name": "installInstructions",
"description": "Instructions for installing the runner on the specified architecture",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "registerInstructions",
"description": "Instructions for registering the runner",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "SastCiConfiguration",

View File

@ -2276,6 +2276,13 @@ Autogenerated return type of RunDASTScan.
| `humanReadableName` | String! | Human readable name of the runner platform |
| `name` | String! | Name slug of the runner platform |
### RunnerSetup
| Field | Type | Description |
| ----- | ---- | ----------- |
| `installInstructions` | String! | Instructions for installing the runner on the specified architecture |
| `registerInstructions` | String! | Instructions for registering the runner |
### SastCiConfigurationAnalyzersEntity
Represents an analyzer entity in SAST CI configuration.

View File

@ -0,0 +1,73 @@
---
comments: false
description: 'Image Resizing'
---
# Image resizing for avatars and content images
Currently, we are showing all uploaded images 1:1, which is of course not ideal. To improve performance greatly we will add image resizing to the backend. There are two main areas of image resizing to consider; avatars and content images. The MVC for this implementation will focus on Avatars. Avatars requests consist of approximately 70% of total image requests. There is an identified set of sizes we intend to support which makes the scope of this first MVC very narrow. Content image resizing has many more considerations for size and features. It is entirely possible that we have two separate development efforts with the same goal of increasing performance via image resizing.
## MVC Avatar Resizing
We will implement a dynamic image resizing solution. This means image should be resized and optimized on the fly so that if we define new targeted sizes later we can add them dynamically. This would mean a huge improvement in performance as some of the measurements suggest that we can save up to 95% of our current load size. Our initial investigations indicate that we have uploaded approximately 1.65 million avatars totaling approximately 80GB in size and averaging approximately 48kb each. Early measurements indicate we can reduce the most common avatar dimensions to between 1-3kb in size, netting us a greater than 90% size reduction. For the MVC we will not consider application level caching and rely purely on HTTP based caches as implemented in CDNs and browsers, but might revisit this decision later on. To easily mitigate performance issues with avatar resizing, especially in the case of self managed, an operations feature flag will be implemented to disable dynamic image resizing.
```mermaid
sequenceDiagram
autonumber
Requester->>Workhorse: Incoming request
Workhorse->>RailsApp: Incoming request
alt All is true: 1.Avatar is requested, 2.Requested Width is allowed, 3.Feature is enabled
Note right of RailsApp: Width Allowlist: https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/concerns/avatarable.rb#L10
RailsApp->>Workhorse: `send-scaled-img:` request
Note right of RailsApp: Set `send-scaled-img:` Header
Workhorse->>Workhorse: Image resizing using Go lib
Workhorse->>Requester: Serve the resized image
else All other cases
RailsApp->>Workhorse: Usual request scenario
Workhorse->>Requester: Usual request scenario
end
```
## Content Image Resizing
Content image resizing is a more complex problem to tackle. There are no set size restrictions and there are additional features or requirements to consider.
- Dynamic WebP support - the WebP format typically achieves an average of 30% more compression than JPEG without the loss of image quality. More details [here](https://developers.google.com/speed/webp/docs/c_study)
- Extract first image of GIF's so we can prevent from loading 10MB pixels
- Check Device Pixel Ratio to deliver nice images on High DPI screens
- Progressive image loading, similar to what is described [here](https://www.sitepoint.com/how-to-build-your-own-progressive-image-loader/)
- Resizing recommendations (size, clarity, etc.)
- Storage
The MVC Avatar resizing implementation is integrated into Workhorse. With the extra requirements for content image resizing, this may require further use of GraphicsMagik (GM) or a similar library and breaking it out of Workhorse.
## Iterations
1. [x] POC on different image resizing solutions
1. [x] Review solutions with security team
1. [x] Implement avatar resizing MVC
1. [ ] Deploy, measure, monitor
1. [ ] Clarify features for content image resizing
1. [ ] Weigh options between using current implementation of image resizing vs new solution
1. [ ] Implement content image resizing MVC
1. [ ] Deploy, measure, monitor
## Who
Proposal:
| Role | Who
|------------------------------|-------------------------|
| Author | Craig Gomes |
| Architecture Evolution Coach | Kamil Trzciński |
| Engineering Leader | Tim Zallmann |
| Domain Expert | Matthias Kaeppler |
| Domain Expert | Aleksei Lipniagov |
DRIs:
| Role | Who
|------------------------------|------------------------|
| Product | Josh Lambert |
| Leadership | Craig Gomes |
| Engineering | Matthias Kaeppler |

View File

@ -219,10 +219,17 @@ You can also specify a static part of the URL at `environment:url:`, such as
The assigned URL for the `review/your-branch-name` environment is [visible in the UI](#using-the-environment-url).
> **Notes:**
>
> - `stop_review` doesn't generate a dotenv report artifact, so it won't recognize the `DYNAMIC_ENVIRONMENT_URL` variable. Therefore you should not set `environment:url:` in the `stop_review` job.
> - If the environment URL is not valid (for example, the URL is malformed), the system doesn't update the environment URL.
Note the following:
- `stop_review` doesn't generate a dotenv report artifact, so it won't recognize the
`DYNAMIC_ENVIRONMENT_URL` variable. Therefore you shouldn't set `environment:url:` in the
`stop_review` job.
- If the environment URL isn't valid (for example, the URL is malformed), the system doesn't update
the environment URL.
- If the script that runs in `stop_review` exists only in your repository and therefore can't use
`GIT_STRATEGY: none`, configure [pipelines for merge requests](../../ci/merge_request_pipelines/index.md)
for these jobs. This ensures that runners can fetch the repository even after a feature branch is
deleted. For more information, see [Ref Specs for Runners](../pipelines/index.md#ref-specs-for-runners).
### Configuring manual deployments
@ -675,24 +682,23 @@ deploy_review:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com
on_stop: stop_review
only:
- branches
except:
- master
rules:
- if: $CI_MERGE_REQUEST_ID
stop_review:
stage: deploy
variables:
GIT_STRATEGY: none
script:
- echo "Remove review app"
when: manual
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
rules:
- if: $CI_MERGE_REQUEST_ID
when: manual
```
Setting the [`GIT_STRATEGY`](../yaml/README.md#git-strategy) to `none` is necessary in the
If you can't use [Pipelines for merge requests](../merge_request_pipelines/index.md),
setting the [`GIT_STRATEGY`](../yaml/README.md#git-strategy) to `none` is necessary in the
`stop_review` job so that the [runner](https://docs.gitlab.com/runner/) won't
try to check out the code after the branch is deleted.
@ -748,13 +754,17 @@ review_app:
name: review/$CI_COMMIT_REF_NAME
on_stop: stop_review_app
auto_stop_in: 1 week
rules:
- if: $CI_MERGE_REQUEST_ID
stop_review_app:
script: stop-review-app
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
when: manual
rules:
- if: $CI_MERGE_REQUEST_ID
when: manual
```
As long as a merge request is active and keeps getting new commits,

View File

@ -79,6 +79,27 @@ You can also configure specific aspects of your pipelines through the GitLab UI.
- [Pipeline schedules](schedules.md).
- [Custom CI/CD variables](../variables/README.md#custom-environment-variables).
### Ref Specs for Runners
When a runner picks a pipeline job, GitLab provides that job's metadata. This includes the [Git refspecs](https://git-scm.com/book/en/v2/Git-Internals-The-Refspec),
which indicate which ref (branch, tag, and so on) and commit (SHA1) are checked out from your
project repository.
This table lists the refspecs injected for each pipeline type:
| Pipeline type | Refspecs |
|--------------- |---------------------------------------- |
| Pipeline for Branches | `+refs/pipelines/<id>:refs/pipelines/<id>` and `+refs/heads/<name>:refs/remotes/origin/<name>` |
| pipeline for Tags | `+refs/pipelines/<id>:refs/pipelines/<id>` and `+refs/tags/<name>:refs/tags/<name>` |
| [Pipeline for Merge Requests](../merge_request_pipelines/index.md) | `+refs/pipelines/<id>:refs/pipelines/<id>` |
The refs `refs/heads/<name>` and `refs/tags/<name>` exist in your
project repository. GitLab generates the special ref `refs/pipelines/<id>` during a
running pipeline job. This ref can be created even after the associated branch or tag has been
deleted. It's therefore useful in some features such as [automatically stopping an environment](../environments/index.md#automatically-stopping-an-environment),
and [merge trains](../merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md)
that might run pipelines after branch deletion.
### View pipelines
You can find the current and historical pipeline runs under your project's

View File

@ -489,15 +489,15 @@ DAST can be [configured](#customizing-the-dast-settings) using environment varia
| `DAST_API_HOST_OVERRIDE` | string | Used to override domains defined in API specification files. Only supported when importing the API specification from a URL. Example: `example.com:8080` |
| `DAST_EXCLUDE_RULES` | string | Set to a comma-separated list of Vulnerability Rule IDs to exclude them from running during the scan. Rule IDs are numbers and can be found from the DAST log or on the [ZAP project](https://github.com/zaproxy/zaproxy/blob/develop/docs/scanners.md). For example, `HTTP Parameter Override` has a rule ID of `10026`. **Note:** In earlier versions of GitLab the excluded rules were executed but alerts they generated were suppressed. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118641) in GitLab 12.10. |
| `DAST_REQUEST_HEADERS` | string | Set to a comma-separated list of request header names and values. Headers are added to every request made by DAST. For example, `Cache-control: no-cache,User-Agent: DAST/1.0` |
| `DAST_DEBUG` | boolean | Enable debug message output. Default: `false` |
| `DAST_SPIDER_MINS` | number | The maximum duration of the spider scan in minutes. Set to `0` for unlimited. Default: One minute, or unlimited when the scan is a full scan. |
| `DAST_HTML_REPORT` | string | The filename of the HTML report written at the end of a scan. |
| `DAST_MARKDOWN_REPORT` | string | The filename of the Markdown report written at the end of a scan. |
| `DAST_XML_REPORT` | string | The filename of the XML report written at the end of a scan. |
| `DAST_INCLUDE_ALPHA_VULNERABILITIES` | boolean | Set to `true` to include alpha passive and active scan rules. Default: `false` |
| `DAST_USE_AJAX_SPIDER` | boolean | Set to `true` to use the AJAX spider in addition to the traditional spider, useful for crawling sites that require JavaScript. Default: `false` |
| `DAST_PATHS` | string | Set to a comma-separated list of URLs for DAST to scan. For example, `/page1.html,/category1/page3.html,/page2.html` |
| `DAST_ZAP_CLI_OPTIONS` | string | ZAP server command-line options. For example, `-Xmx3072m` would set the Java maximum memory allocation pool size. |
| `DAST_DEBUG` | boolean | Enable debug message output. Default: `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
| `DAST_SPIDER_MINS` | number | The maximum duration of the spider scan in minutes. Set to `0` for unlimited. Default: One minute, or unlimited when the scan is a full scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
| `DAST_HTML_REPORT` | string | The filename of the HTML report written at the end of a scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
| `DAST_MARKDOWN_REPORT` | string | The filename of the Markdown report written at the end of a scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1.|
| `DAST_XML_REPORT` | string | The filename of the XML report written at the end of a scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
| `DAST_INCLUDE_ALPHA_VULNERABILITIES` | boolean | Set to `true` to include alpha passive and active scan rules. Default: `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
| `DAST_USE_AJAX_SPIDER` | boolean | Set to `true` to use the AJAX spider in addition to the traditional spider, useful for crawling sites that require JavaScript. Default: `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
| `DAST_PATHS` | string | Set to a comma-separated list of URLs for DAST to scan. For example, `/page1.html,/category1/page3.html,/page2.html`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214120) in GitLab 13.4. |
| `DAST_ZAP_CLI_OPTIONS` | string | ZAP server command-line options. For example, `-Xmx3072m` would set the Java maximum memory allocation pool size. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
| `DAST_ZAP_LOG_CONFIGURATION` | string | Set to a semicolon-separated list of additional log4j properties for the ZAP Server. For example, `log4j.logger.org.parosproxy.paros.network.HttpSender=DEBUG;log4j.logger.com.crawljax=DEBUG` |
### DAST command-line options

View File

@ -33,7 +33,7 @@ SAST supports the following official analyzers:
- [`sobelow`](https://gitlab.com/gitlab-org/security-products/analyzers/sobelow) (Sobelow (Elixir Phoenix))
- [`spotbugs`](https://gitlab.com/gitlab-org/security-products/analyzers/spotbugs) (SpotBugs with the Find Sec Bugs plugin (Ant, Gradle and wrapper, Grails, Maven and wrapper, SBT))
The analyzers are published as Docker images that SAST will use to launch
The analyzers are published as Docker images that SAST uses to launch
dedicated containers for each analysis.
SAST is pre-configured with a set of **default images** that are maintained by
@ -77,12 +77,12 @@ variables:
SAST_DEFAULT_ANALYZERS: "bandit,flawfinder"
```
`bandit` runs first. When merging the reports, SAST will
remove the duplicates and will keep the `bandit` entries.
`bandit` runs first. When merging the reports, SAST
removes the duplicates and keeps the `bandit` entries.
### Disabling default analyzers
Setting `SAST_DEFAULT_ANALYZERS` to an empty string will disable all the official
Setting `SAST_DEFAULT_ANALYZERS` to an empty string disables all the official
default analyzers. In `.gitlab-ci.yml` define:
```yaml

View File

@ -189,7 +189,7 @@ requires [GraphQL](../../../api/graphql/index.md) to be enabled.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36427) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
> - Health status of closed issues [can't be edited](https://gitlab.com/gitlab-org/gitlab/-/issues/220867) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.4 and later.
> - Issue health status visible in issue lists [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45141) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.5.
> - Issue health status visible in issue lists [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45141) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.6.
To help you track the status of your issues, you can assign a status to each issue to flag work
that's progressing as planned or needs attention to keep on schedule:

View File

@ -28386,6 +28386,9 @@ msgstr ""
msgid "Update now"
msgstr ""
msgid "Update username"
msgstr ""
msgid "Update variable"
msgstr ""

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Merge Requests > User filters by draft', :js do
include FilteredSearchHelpers
let(:project) { create(:project, :public, :repository) }
let(:user) { project.creator }
before do
create(:merge_request, title: 'Draft: Bugfix', source_project: project, target_project: project, source_branch: 'bugfix2')
sign_in(user)
visit project_merge_requests_path(project)
end
it 'filters results' do
input_filtered_search_keys('draft:=yes')
expect(page).to have_content('Draft: Bugfix')
end
it 'does not allow filtering by is not equal' do
find('#filtered-search-merge_requests').click
click_button 'Draft'
expect(page).not_to have_content('!=')
end
end

View File

@ -101,10 +101,10 @@ RSpec.describe 'Profile account page', :js do
it 'changes my username' do
fill_in 'username-change-input', with: 'new-username'
page.find('[data-target="#username-change-confirmation-modal"]').click
page.find('[data-testid="username-change-confirmation-modal"]').click
page.within('.modal') do
find('.js-modal-primary-action').click
find('.js-modal-action-primary').click
end
expect(page).to have_content('new-username')

View File

@ -128,10 +128,10 @@ def update_username(new_username)
fill_in 'username-change-input', with: new_username
page.find('[data-target="#username-change-confirmation-modal"]').click
page.find('[data-testid="username-change-confirmation-modal"]').click
page.within('.modal') do
find('.js-modal-primary-action').click
find('.js-modal-action-primary').click
end
wait_for_requests

View File

@ -1,173 +1,135 @@
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import mountComponent from 'helpers/vue_mount_component_helper';
import axios from '~/lib/utils/axios_utils';
import updateUsername from '~/profile/account/components/update_username.vue';
import UpdateUsername from '~/profile/account/components/update_username.vue';
describe('UpdateUsername component', () => {
const rootUrl = TEST_HOST;
const actionUrl = `${TEST_HOST}/update/username`;
const username = 'hasnoname';
const newUsername = 'new_username';
let Component;
let vm;
const defaultProps = {
actionUrl,
rootUrl,
initialUsername: 'hasnoname',
};
let wrapper;
let axiosMock;
const createComponent = (props = {}) => {
wrapper = shallowMount(UpdateUsername, {
propsData: {
...defaultProps,
...props,
},
stubs: {
GlModal,
},
});
};
beforeEach(() => {
axiosMock = new MockAdapter(axios);
Component = Vue.extend(updateUsername);
vm = mountComponent(Component, {
actionUrl,
rootUrl,
initialUsername: username,
});
createComponent();
});
afterEach(() => {
vm.$destroy();
wrapper.destroy();
axiosMock.restore();
});
const findElements = () => {
const modalSelector = `#${vm.$options.modalId}`;
const modal = wrapper.find(GlModal);
return {
input: vm.$el.querySelector(`#${vm.$options.inputId}`),
openModalBtn: vm.$el.querySelector(`[data-target="${modalSelector}"]`),
modal: vm.$el.querySelector(modalSelector),
modalBody: vm.$el.querySelector(`${modalSelector} .modal-body`),
modalHeader: vm.$el.querySelector(`${modalSelector} .modal-title`),
confirmModalBtn: vm.$el.querySelector(`${modalSelector} .btn-warning`),
modal,
input: wrapper.find(`#${wrapper.vm.$options.inputId}`),
openModalBtn: wrapper.find('[data-testid="username-change-confirmation-modal"]'),
modalBody: modal.find('.modal-body'),
modalHeader: modal.find('.modal-title'),
confirmModalBtn: wrapper.find('.btn-warning'),
};
};
it('has a disabled button if the username was not changed', done => {
const { input, openModalBtn } = findElements();
input.dispatchEvent(new Event('input'));
it('has a disabled button if the username was not changed', async () => {
const { openModalBtn } = findElements();
Vue.nextTick()
.then(() => {
expect(vm.username).toBe(username);
expect(vm.newUsername).toBe(username);
expect(openModalBtn).toBeDisabled();
})
.then(done)
.catch(done.fail);
await wrapper.vm.$nextTick();
expect(openModalBtn.props('disabled')).toBe(true);
});
it('has an enabled button which if the username was changed', done => {
const { input, openModalBtn } = findElements();
input.value = newUsername;
input.dispatchEvent(new Event('input'));
Vue.nextTick()
.then(() => {
expect(vm.username).toBe(username);
expect(vm.newUsername).toBe(newUsername);
expect(openModalBtn).not.toBeDisabled();
})
.then(done)
.catch(done.fail);
});
it('confirmation modal contains proper header and body', done => {
const { modalBody, modalHeader } = findElements();
vm.newUsername = newUsername;
Vue.nextTick()
.then(() => {
expect(modalHeader.textContent).toContain('Change username?');
expect(modalBody.textContent).toContain(
`You are going to change the username ${username} to ${newUsername}`,
);
})
.then(done)
.catch(done.fail);
});
it('confirmation modal should escape usernames properly', done => {
const { modalBody } = findElements();
vm.username = '<i>Italic</i>';
vm.newUsername = vm.username;
Vue.nextTick()
.then(() => {
expect(modalBody.innerHTML).toContain('&lt;i&gt;Italic&lt;/i&gt;');
expect(modalBody.innerHTML).not.toContain(vm.username);
})
.then(done)
.catch(done.fail);
});
it('executes API call on confirmation button click', done => {
const { confirmModalBtn } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => [200, { message: 'Username changed' }]);
jest.spyOn(axios, 'put');
vm.newUsername = newUsername;
Vue.nextTick()
.then(() => {
confirmModalBtn.click();
expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } });
})
.then(done)
.catch(done.fail);
});
it('sets the username after a successful update', done => {
it('has an enabled button which if the username was changed', async () => {
const { input, openModalBtn } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => {
expect(input).toBeDisabled();
expect(openModalBtn).toBeDisabled();
input.element.value = 'newUsername';
input.trigger('input');
return [200, { message: 'Username changed' }];
await wrapper.vm.$nextTick();
expect(openModalBtn.props('disabled')).toBe(false);
});
describe('changing username', () => {
const newUsername = 'new_username';
beforeEach(async () => {
createComponent();
wrapper.setData({ newUsername });
await wrapper.vm.$nextTick();
});
vm.newUsername = newUsername;
it('confirmation modal contains proper header and body', async () => {
const { modal } = findElements();
vm.onConfirm()
.then(() => {
expect(vm.username).toBe(newUsername);
expect(vm.newUsername).toBe(newUsername);
expect(input).not.toBeDisabled();
expect(input.value).toBe(newUsername);
expect(openModalBtn).toBeDisabled();
})
.then(done)
.catch(done.fail);
});
it('does not set the username after a erroneous update', done => {
const { input, openModalBtn } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => {
expect(input).toBeDisabled();
expect(openModalBtn).toBeDisabled();
return [400, { message: 'Invalid username' }];
expect(modal.attributes('title')).toBe('Change username?');
expect(modal.text()).toContain(
`You are going to change the username ${defaultProps.initialUsername} to ${newUsername}`,
);
});
const invalidUsername = 'anything.git';
vm.newUsername = invalidUsername;
it('executes API call on confirmation button click', async () => {
axiosMock.onPut(actionUrl).replyOnce(() => [200, { message: 'Username changed' }]);
jest.spyOn(axios, 'put');
vm.onConfirm()
.then(() => done.fail('Expected onConfirm to throw!'))
.catch(() => {
expect(vm.username).toBe(username);
expect(vm.newUsername).toBe(invalidUsername);
expect(input).not.toBeDisabled();
expect(input.value).toBe(invalidUsername);
expect(openModalBtn).not.toBeDisabled();
})
.then(done)
.catch(done.fail);
await wrapper.vm.onConfirm();
await wrapper.vm.$nextTick();
expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } });
});
it('sets the username after a successful update', async () => {
const { input, openModalBtn } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => {
expect(input.attributes('disabled')).toBe('disabled');
expect(openModalBtn.props('disabled')).toBe(true);
return [200, { message: 'Username changed' }];
});
await wrapper.vm.onConfirm();
await wrapper.vm.$nextTick();
expect(input.attributes('disabled')).toBe(undefined);
expect(openModalBtn.props('disabled')).toBe(true);
});
it('does not set the username after a erroneous update', async () => {
const { input, openModalBtn } = findElements();
axiosMock.onPut(actionUrl).replyOnce(() => {
expect(input.attributes('disabled')).toBe('disabled');
expect(openModalBtn.props('disabled')).toBe(true);
return [400, { message: 'Invalid username' }];
});
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
expect(input.attributes('disabled')).toBe(undefined);
expect(openModalBtn.props('disabled')).toBe(false);
});
});
});

View File

@ -0,0 +1,86 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Ci::RunnerSetupResolver do
include GraphqlHelpers
describe '#resolve' do
let(:user) { create(:user) }
subject(:resolve_subject) { resolve(described_class, ctx: { current_user: user }, args: { platform: 'linux', architecture: 'amd64' }.merge(target_param)) }
context 'without target parameter' do
let(:target_param) { {} }
context 'when user is not admin' do
it 'returns access error' do
expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when user is admin' do
before do
user.update!(admin: true)
end
it 'returns install and register instructions' do
expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions)
expect(resolve_subject.values).not_to include(nil)
end
end
end
context 'with project target parameter' do
let(:project) { create(:project) }
let(:target_param) { { project_id: project.to_global_id } }
context 'when user has access to admin builds on project' do
before do
project.add_maintainer(user)
end
it 'returns install and register instructions' do
expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions)
expect(resolve_subject.values).not_to include(nil)
end
end
context 'when user does not have access to admin builds on project' do
before do
project.add_developer(user)
end
it 'returns access error' do
expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
context 'with group target parameter' do
let(:group) { create(:group) }
let(:target_param) { { group_id: group.to_global_id } }
context 'when user has access to admin builds on group' do
before do
group.add_owner(user)
end
it 'returns install and register instructions' do
expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions)
expect(resolve_subject.values).not_to include(nil)
end
end
context 'when user does not have access to admin builds on group' do
before do
group.add_developer(user)
end
it 'returns access error' do
expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
end

View File

@ -0,0 +1,16 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::Ci::RunnerSetupType do
specify { expect(described_class.graphql_name).to eq('RunnerSetup') }
it 'exposes the expected fields' do
expected_fields = %i[
install_instructions
register_instructions
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end

View File

@ -80,4 +80,12 @@ RSpec.describe GitlabSchema.types['Query'] do
is_expected.to have_graphql_type(Types::Ci::RunnerPlatformType.connection_type)
end
end
describe 'runner_setup field' do
subject { described_class.fields['runnerSetup'] }
it 'returns runner setup instructions' do
is_expected.to have_graphql_type(Types::Ci::RunnerSetupType)
end
end
end

View File

@ -642,7 +642,7 @@ RSpec.describe Auth::ContainerRegistryAuthenticationService do
let_it_be(:project) { create(:project, :public, container_registry_enabled: false) }
before do
project.update(container_registry_enabled: false)
project.update!(container_registry_enabled: false)
end
context 'disallow when pulling' do

View File

@ -131,7 +131,7 @@ RSpec.describe AutoMerge::BaseService do
end
describe '#update' do
subject { service.update(merge_request) }
subject { service.update(merge_request) } # rubocop:disable Rails/SaveBang
let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }

View File

@ -148,7 +148,7 @@ RSpec.describe AutoMergeService do
end
describe '#update' do
subject { service.update(merge_request) }
subject { service.update(merge_request) } # rubocop:disable Rails/SaveBang
context 'when auto merge is enabled' do
let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }

View File

@ -197,7 +197,7 @@ RSpec.describe Clusters::UpdateService do
context 'manangement_project is outside of the namespace scope' do
before do
management_project.update(group: create(:group))
management_project.update!(group: create(:group))
end
let(:params) do
@ -224,7 +224,7 @@ RSpec.describe Clusters::UpdateService do
context 'manangement_project is outside of the namespace scope' do
before do
management_project.update(group: create(:group))
management_project.update!(group: create(:group))
end
let(:params) do

View File

@ -44,7 +44,7 @@ RSpec.describe DesignManagement::GenerateImageVersionsService do
end
it 'logs if the raw image cannot be found' do
version.designs.first.update(filename: 'foo.png')
version.designs.first.update!(filename: 'foo.png')
expect(Gitlab::AppLogger).to receive(:error).with("No design file found for Action: #{action.id}")

View File

@ -40,11 +40,11 @@ RSpec.describe Discussions::ResolveService do
context 'with a project that requires all discussion to be resolved' do
before do
project.update(only_allow_merge_if_all_discussions_are_resolved: true)
project.update!(only_allow_merge_if_all_discussions_are_resolved: true)
end
after do
project.update(only_allow_merge_if_all_discussions_are_resolved: false)
project.update!(only_allow_merge_if_all_discussions_are_resolved: false)
end
let_it_be(:other_discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }

View File

@ -22,7 +22,7 @@ RSpec.describe DraftNotes::DestroyService do
it 'destroys all draft notes for a user in a merge request' do
create_list(:draft_note, 2, merge_request: merge_request, author: user)
expect { destroy }.to change { DraftNote.count }.by(-2)
expect { destroy }.to change { DraftNote.count }.by(-2) # rubocop:disable Rails/SaveBang
expect(DraftNote.count).to eq(0)
end
@ -45,7 +45,7 @@ RSpec.describe DraftNotes::DestroyService do
allow_any_instance_of(DraftNote).to receive_message_chain(:diff_file, :unfolded?) { true }
expect(merge_request).to receive_message_chain(:diffs, :clear_cache)
destroy
destroy # rubocop:disable Rails/SaveBang
end
end
end

View File

@ -9,7 +9,7 @@ RSpec.describe Emails::ConfirmService do
describe '#execute' do
it 'enqueues a background job to send confirmation email again' do
email = user.emails.create(email: 'new@email.com')
email = user.emails.create!(email: 'new@email.com')
expect { service.execute(email) }.to have_enqueued_job.on_queue('mailers')
end

View File

@ -95,7 +95,7 @@ RSpec.describe Groups::DestroyService do
context 'projects in pending_delete' do
before do
project.pending_delete = true
project.save
project.save!
end
it_behaves_like 'group destruction', false

View File

@ -63,7 +63,7 @@ RSpec.describe Groups::ImportExport::ImportService do
before do
stub_feature_flags(group_import_ndjson: false)
ImportExportUpload.create(group: group, import_file: import_file)
ImportExportUpload.create!(group: group, import_file: import_file)
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
allow(import_logger).to receive(:error)
@ -105,7 +105,7 @@ RSpec.describe Groups::ImportExport::ImportService do
subject { service.execute }
before do
ImportExportUpload.create(group: group, import_file: import_file)
ImportExportUpload.create!(group: group, import_file: import_file)
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
allow(import_logger).to receive(:error)
@ -216,7 +216,7 @@ RSpec.describe Groups::ImportExport::ImportService do
subject { service.execute }
before do
ImportExportUpload.create(group: group, import_file: import_file)
ImportExportUpload.create!(group: group, import_file: import_file)
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
allow(import_logger).to receive(:error)

View File

@ -89,10 +89,10 @@ RSpec.describe Labels::PromoteService do
it 'keeps users\' subscriptions' do
user2 = create(:user)
project_label_1_1.subscriptions.create(user: user, subscribed: true)
project_label_2_1.subscriptions.create(user: user, subscribed: true)
project_label_3_2.subscriptions.create(user: user, subscribed: true)
project_label_2_1.subscriptions.create(user: user2, subscribed: true)
project_label_1_1.subscriptions.create!(user: user, subscribed: true)
project_label_2_1.subscriptions.create!(user: user, subscribed: true)
project_label_3_2.subscriptions.create!(user: user, subscribed: true)
project_label_2_1.subscriptions.create!(user: user2, subscribed: true)
expect { service.execute(project_label_1_1) }.to change { Subscription.count }.from(4).to(3)

View File

@ -286,7 +286,7 @@ RSpec.describe Notes::CreateService do
QuickAction.new(
action_text: "/wip",
before_action: -> {
issuable.reload.update(title: "title")
issuable.reload.update!(title: "title")
},
expectation: ->(issuable, can_use_quick_action) {
expect(issuable.work_in_progress?).to eq(can_use_quick_action)
@ -296,7 +296,7 @@ RSpec.describe Notes::CreateService do
QuickAction.new(
action_text: "/wip",
before_action: -> {
issuable.reload.update(title: "WIP: title")
issuable.reload.update!(title: "WIP: title")
},
expectation: ->(noteable, can_use_quick_action) {
expect(noteable.work_in_progress?).not_to eq(can_use_quick_action)

View File

@ -44,7 +44,7 @@ RSpec.describe NotificationRecipients::BuildService do
context 'when there are multiple subscribers' do
def create_user
subscriber = create(:user)
issue.subscriptions.create(user: subscriber, project: project, subscribed: true)
issue.subscriptions.create!(user: subscriber, project: project, subscribed: true)
end
include_examples 'no N+1 queries'
@ -96,7 +96,7 @@ RSpec.describe NotificationRecipients::BuildService do
context 'when there are multiple subscribers' do
def create_user
subscriber = create(:user)
merge_request.subscriptions.create(user: subscriber, project: project, subscribed: true)
merge_request.subscriptions.create!(user: subscriber, project: project, subscribed: true)
end
include_examples 'no N+1 queries'

View File

@ -361,7 +361,7 @@ RSpec.describe NotificationService, :mailer do
context 'where the project has disabled the feature' do
before do
project.update(service_desk_enabled: false)
project.update!(service_desk_enabled: false)
end
it_should_not_email!
@ -453,7 +453,7 @@ RSpec.describe NotificationService, :mailer do
context 'by note' do
before do
note.author = @u_lazy_participant
note.save
note.save!
end
it { expect { subject }.not_to have_enqueued_email(@u_lazy_participant.id, note.id, mail: "note_issue_email") }
@ -467,7 +467,7 @@ RSpec.describe NotificationService, :mailer do
note.project.namespace_id = group.id
group.add_user(@u_watcher, GroupMember::MAINTAINER)
group.add_user(@u_custom_global, GroupMember::MAINTAINER)
note.project.save
note.project.save!
@u_watcher.notification_settings_for(note.project).participating!
@u_watcher.notification_settings_for(group).global!
@ -1719,7 +1719,7 @@ RSpec.describe NotificationService, :mailer do
before do
merge_request.author = participant
merge_request.save
merge_request.save!
notification.new_merge_request(merge_request, @u_disabled)
end
@ -1962,7 +1962,7 @@ RSpec.describe NotificationService, :mailer do
describe 'when merge_when_pipeline_succeeds is true' do
before do
merge_request.update(
merge_request.update!(
merge_when_pipeline_succeeds: true,
merge_user: create(:user)
)
@ -2725,7 +2725,7 @@ RSpec.describe NotificationService, :mailer do
before do
group = create(:group)
project.update(group: group)
project.update!(group: group)
create(:email, :confirmed, user: u_custom_notification_enabled, email: group_notification_email)
create(:notification_setting, user: u_custom_notification_enabled, source: group, notification_email: group_notification_email)
@ -2761,7 +2761,7 @@ RSpec.describe NotificationService, :mailer do
before do
group = create(:group)
project.update(group: group)
project.update!(group: group)
create(:email, :confirmed, user: u_member, email: group_notification_email)
create(:notification_setting, user: u_member, source: group, notification_email: group_notification_email)
end
@ -2821,7 +2821,7 @@ RSpec.describe NotificationService, :mailer do
context 'when the creator has no read_build access' do
before do
pipeline = create_pipeline(u_member, :failed)
project.update(public_builds: false)
project.update!(public_builds: false)
project.team.truncate
notification.pipeline_finished(pipeline)
end
@ -2855,7 +2855,7 @@ RSpec.describe NotificationService, :mailer do
before do
group = create(:group)
project.update(group: group)
project.update!(group: group)
create(:email, :confirmed, user: u_member, email: group_notification_email)
create(:notification_setting, user: u_member, source: group, notification_email: group_notification_email)
end
@ -3212,7 +3212,7 @@ RSpec.describe NotificationService, :mailer do
# with different notification settings
def build_group(project, visibility: :public)
group = create_nested_group(visibility)
project.update(namespace_id: group.id)
project.update!(namespace_id: group.id)
# Group member: global=disabled, group=watch
@g_watcher ||= create_user_with_notification(:watch, 'group_watcher', project.group)
@ -3277,11 +3277,11 @@ RSpec.describe NotificationService, :mailer do
end
def add_user_subscriptions(issuable)
issuable.subscriptions.create(user: @unsubscribed_mentioned, project: project, subscribed: false)
issuable.subscriptions.create(user: @subscriber, project: project, subscribed: true)
issuable.subscriptions.create(user: @subscribed_participant, project: project, subscribed: true)
issuable.subscriptions.create(user: @unsubscriber, project: project, subscribed: false)
issuable.subscriptions.create!(user: @unsubscribed_mentioned, project: project, subscribed: false)
issuable.subscriptions.create!(user: @subscriber, project: project, subscribed: true)
issuable.subscriptions.create!(user: @subscribed_participant, project: project, subscribed: true)
issuable.subscriptions.create!(user: @unsubscriber, project: project, subscribed: false)
# Make the watcher a subscriber to detect dupes
issuable.subscriptions.create(user: @watcher_and_subscriber, project: project, subscribed: true)
issuable.subscriptions.create!(user: @watcher_and_subscriber, project: project, subscribed: true)
end
end

View File

@ -100,7 +100,7 @@ RSpec.describe Packages::Conan::CreatePackageFileService do
end
let(:tmp_object) do
fog_connection.directories.new(key: 'packages').files.create(
fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang
key: "tmp/uploads/#{file_name}",
body: 'content'
)

View File

@ -20,7 +20,7 @@ RSpec.describe ResetProjectCacheService do
context 'when project cache_index is a numeric value' do
before do
project.update(jobs_cache_index: 1)
project.update!(jobs_cache_index: 1)
end
it 'increments project cache index' do

View File

@ -11,7 +11,7 @@ RSpec.describe ResourceEvents::ChangeMilestoneService do
[:issue, :merge_request].each do |issuable|
it_behaves_like 'timebox(milestone or iteration) resource events creator', ResourceMilestoneEvent do
let_it_be(:resource) { create(issuable) }
let_it_be(:resource) { create(issuable) } # rubocop:disable Rails/SaveBang
end
end
end

View File

@ -85,7 +85,7 @@ RSpec.describe SystemHooksService do
end
it 'handles nil datetime columns' do
user.update(created_at: nil, updated_at: nil)
user.update!(created_at: nil, updated_at: nil)
data = event_data(user, :destroy)
expect(data[:created_at]).to be(nil)

View File

@ -378,13 +378,13 @@ RSpec.describe SystemNoteService do
noteable_types.each do |type|
context "when noteable is a #{type}" do
it "blocks cross reference when #{type.underscore}_events is false" do
jira_tracker.update("#{type}_events" => false)
jira_tracker.update!("#{type}_events" => false)
expect(cross_reference(type)).to eq(s_('JiraService|Events for %{noteable_model_name} are disabled.') % { noteable_model_name: type.pluralize.humanize.downcase })
end
it "creates cross reference when #{type.underscore}_events is true" do
jira_tracker.update("#{type}_events" => true)
jira_tracker.update!("#{type}_events" => true)
expect(cross_reference(type)).to eq(success_message)
end

View File

@ -373,7 +373,7 @@ RSpec.describe ::SystemNotes::IssuablesService do
before do
# Mention issue (noteable) from commit0
system_note = service.cross_reference(commit0)
system_note.update(note: system_note.note.capitalize)
system_note.update!(note: system_note.note.capitalize)
end
it 'is truthy when already mentioned' do
@ -407,7 +407,7 @@ RSpec.describe ::SystemNotes::IssuablesService do
before do
# Mention commit1 from commit0
system_note = service.cross_reference(commit1)
system_note.update(note: system_note.note.capitalize)
system_note.update!(note: system_note.note.capitalize)
end
it 'is truthy when already mentioned' do
@ -436,7 +436,7 @@ RSpec.describe ::SystemNotes::IssuablesService do
context 'legacy capitalized cross reference' do
before do
system_note = service.cross_reference(commit0)
system_note.update(note: system_note.note.capitalize)
system_note.update!(note: system_note.note.capitalize)
end
it 'is true when a fork mentions an external issue' do
@ -582,7 +582,7 @@ RSpec.describe ::SystemNotes::IssuablesService do
it 'creates the note text correctly' do
[:issue, :merge_request].each do |type|
issuable = create(type)
issuable = create(type) # rubocop:disable Rails/SaveBang
service = described_class.new(noteable: issuable, author: author)
expect(service.discussion_lock.note)

View File

@ -145,7 +145,7 @@ RSpec.describe TodoService do
end
it 'creates correct todos for each valid user based on the type of mention' do
issue.update(description: directly_addressed_and_mentioned)
issue.update!(description: directly_addressed_and_mentioned)
service.new_issue(issue, author)
@ -222,7 +222,7 @@ RSpec.describe TodoService do
end
it 'creates a todo for each valid user not included in skip_users based on the type of mention' do
issue.update(description: directly_addressed_and_mentioned)
issue.update!(description: directly_addressed_and_mentioned)
service.update_issue(issue, author, skip_users)
@ -291,7 +291,7 @@ RSpec.describe TodoService do
context 'issues with a task list' do
it 'does not create todo when tasks are marked as completed' do
issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
issue.update!(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
service.update_issue(issue, author)
@ -304,7 +304,7 @@ RSpec.describe TodoService do
end
it 'does not create directly addressed todo when tasks are marked as completed' do
addressed_issue.update(description: "#{directly_addressed}\n- [x] Task 1\n- [x] Task 2\n")
addressed_issue.update!(description: "#{directly_addressed}\n- [x] Task 1\n- [x] Task 2\n")
service.update_issue(addressed_issue, author)
@ -317,7 +317,7 @@ RSpec.describe TodoService do
end
it 'does not raise an error when description not change' do
issue.update(title: 'Sample')
issue.update!(title: 'Sample')
expect { service.update_issue(issue, author) }.not_to raise_error
end
@ -427,7 +427,7 @@ RSpec.describe TodoService do
end
it 'creates a todo for each valid user based on the type of mention' do
note.update(note: directly_addressed_and_mentioned)
note.update!(note: directly_addressed_and_mentioned)
service.new_note(note, john_doe)
@ -694,7 +694,7 @@ RSpec.describe TodoService do
end
it 'creates a todo for each valid user based on the type of mention' do
mr_assigned.update(description: directly_addressed_and_mentioned)
mr_assigned.update!(description: directly_addressed_and_mentioned)
service.new_merge_request(mr_assigned, author)
@ -726,7 +726,7 @@ RSpec.describe TodoService do
end
it 'creates a todo for each valid user not included in skip_users based on the type of mention' do
mr_assigned.update(description: directly_addressed_and_mentioned)
mr_assigned.update!(description: directly_addressed_and_mentioned)
service.update_merge_request(mr_assigned, author, skip_users)
@ -772,7 +772,7 @@ RSpec.describe TodoService do
context 'with a task list' do
it 'does not create todo when tasks are marked as completed' do
mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
mr_assigned.update!(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
service.update_merge_request(mr_assigned, author)
@ -786,7 +786,7 @@ RSpec.describe TodoService do
end
it 'does not create directly addressed todo when tasks are marked as completed' do
addressed_mr_assigned.update(description: "#{directly_addressed}\n- [x] Task 1\n- [X] Task 2")
addressed_mr_assigned.update!(description: "#{directly_addressed}\n- [x] Task 1\n- [X] Task 2")
service.update_merge_request(addressed_mr_assigned, author)
@ -800,7 +800,7 @@ RSpec.describe TodoService do
end
it 'does not raise an error when description not change' do
mr_assigned.update(title: 'Sample')
mr_assigned.update!(title: 'Sample')
expect { service.update_merge_request(mr_assigned, author) }.not_to raise_error
end
@ -883,7 +883,7 @@ RSpec.describe TodoService do
end
it 'creates a pending todo for each merge_participant' do
mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin)
mr_unassigned.update!(merge_when_pipeline_succeeds: true, merge_user: admin)
service.merge_request_became_unmergeable(mr_unassigned)
merge_participants.each do |participant|
@ -991,7 +991,7 @@ RSpec.describe TodoService do
end
it 'creates a todo for each valid user not included in skip_users based on the type of mention' do
note.update(note: directly_addressed_and_mentioned)
note.update!(note: directly_addressed_and_mentioned)
service.update_note(note, author, skip_users)

View File

@ -37,7 +37,7 @@ RSpec.describe Todos::Destroy::ConfidentialIssueService do
context 'when provided issue is not confidential' do
it 'does not remove any todos' do
issue_1.update(confidential: false)
issue_1.update!(confidential: false)
expect { subject }.not_to change { Todo.count }
end

View File

@ -108,7 +108,7 @@ RSpec.describe Users::DestroyService do
context 'projects in pending_delete' do
before do
project.pending_delete = true
project.save
project.save!
end
it 'destroys a project in pending_delete' do
@ -310,7 +310,7 @@ RSpec.describe Users::DestroyService do
it 'of group_members' do
group_member = create(:group_member)
group_member.group.group_members.create(user: user, access_level: 40)
group_member.group.group_members.create!(user: user, access_level: 40)
expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:find).once
expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:initialize).once

View File

@ -10,7 +10,7 @@ RSpec.describe Users::RepairLdapBlockedService do
describe '#execute' do
it 'changes to normal block after destroying last ldap identity' do
identity.destroy
identity.destroy!
service.execute
expect(user.reload).not_to be_ldap_blocked

View File

@ -189,7 +189,7 @@ RSpec.describe VerifyPagesDomainService do
let(:domain) { build(:pages_domain, :expired, :with_missing_chain) }
before do
domain.save(validate: false)
domain.save!(validate: false)
end
it 'can be disabled' do

View File

@ -7,7 +7,7 @@ RSpec.describe Sidekiq::Cron::Job do
context 'when Fugit depends on ZoTime or EoTime' do
before do
described_class
.create(name: 'TestCronWorker',
.create(name: 'TestCronWorker', # rubocop:disable Rails/SaveBang
cron: Settings.cron_jobs[:pipeline_schedule_worker]['cron'],
class: Settings.cron_jobs[:pipeline_schedule_worker]['job_class'])
end

0
vendor/gitignore/C++.gitignore vendored Executable file → Normal file
View File

0
vendor/gitignore/Java.gitignore vendored Executable file → Normal file
View File