Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-24 00:09:26 +00:00
parent 78f935d566
commit 27f3465d8a
60 changed files with 1051 additions and 567 deletions

View File

@ -47,7 +47,7 @@ export default class FileTemplateSelector {
}
isHidden() {
return this.$wrapper.hasClass('hidden');
return !this.$wrapper || this.$wrapper.hasClass('hidden');
}
getToggleText() {

View File

@ -82,7 +82,7 @@ export default class EditBlob {
this.$editModePanes.hide();
currentPane.fadeIn(200);
currentPane.show();
if (paneId === '#preview') {
this.$toggleButton.hide();

View File

@ -16,21 +16,16 @@ import { initRails } from '~/lib/utils/rails_ujs';
import * as popovers from '~/popovers';
import * as tooltips from '~/tooltips';
import initAlertHandler from './alert_handler';
import { deprecatedCreateFlash as Flash, removeFlashClickListener } from './flash';
import { removeFlashClickListener } from './flash';
import initTodoToggle from './header';
import initLayoutNav from './layout_nav';
import {
handleLocationHash,
addSelectOnFocusBehaviour,
getCspNonceValue,
} from './lib/utils/common_utils';
import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash, visitUrl } from './lib/utils/url_utility';
// everything else
import initFeatureHighlight from './feature_highlight';
import LazyLoader from './lazy_loader';
import { __ } from './locale';
import initLogoAnimation from './logo';
import initFrequentItemDropdowns from './frequent_items';
import initBreadcrumbs from './breadcrumb';
@ -49,29 +44,8 @@ applyGitLabUIConfig();
window.jQuery = jQuery;
window.$ = jQuery;
// Add nonce to jQuery script handler
jQuery.ajaxSetup({
converters: {
// eslint-disable-next-line @gitlab/require-i18n-strings, func-names
'text script': function (text) {
jQuery.globalEval(text, { nonce: getCspNonceValue() });
return text;
},
},
});
function disableJQueryAnimations() {
$.fx.off = true;
}
// Disable jQuery animations
if (gon?.disable_animations) {
disableJQueryAnimations();
}
// inject test utilities if necessary
if (process.env.NODE_ENV !== 'production' && gon?.test_env) {
disableJQueryAnimations();
import(/* webpackMode: "eager" */ './test_utils/');
}
@ -239,17 +213,6 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// eslint-disable-next-line no-jquery/no-ajax-events
$(document).ajaxError((e, xhrObj) => {
const ref = xhrObj.status;
if (ref === 401) {
Flash(__('You need to be logged in.'));
} else if (ref === 404 || ref === 500) {
Flash(__('Something went wrong on our end.'));
}
});
$('.navbar-toggler').on('click', () => {
$('.header-content').toggleClass('menu-expanded');
});

View File

@ -127,10 +127,20 @@ export default {
pipelineCoverageJobNumberText() {
return n__('from %d job', 'from %d jobs', this.buildsWithCoverage.length);
},
pipelineCoverageTooltipDeltaDescription() {
const delta = parseFloat(this.pipelineCoverageDelta) || 0;
if (delta > 0) {
return s__('Pipeline|This change will increase the overall test coverage if merged.');
}
if (delta < 0) {
return s__('Pipeline|This change will decrease the overall test coverage if merged.');
}
return s__('Pipeline|This change will not change the overall test coverage if merged.');
},
pipelineCoverageTooltipDescription() {
return n__(
'Coverage value for this pipeline was calculated by the coverage value of %d job.',
'Coverage value for this pipeline was calculated by averaging the resulting coverage values of %d jobs.',
'Test coverage value for this pipeline was calculated by the coverage value of %d job.',
'Test coverage value for this pipeline was calculated by averaging the resulting coverage values of %d jobs.',
this.buildsWithCoverage.length,
);
},
@ -218,13 +228,15 @@ export default {
</template>
</div>
<div v-if="pipeline.coverage" class="coverage" data-testid="pipeline-coverage">
{{ s__('Pipeline|Coverage') }} {{ pipeline.coverage }}%
{{ s__('Pipeline|Test coverage') }} {{ pipeline.coverage }}%
<span
v-if="pipelineCoverageDelta"
ref="pipelineCoverageDelta"
:class="coverageDeltaClass"
data-testid="pipeline-coverage-delta"
>({{ pipelineCoverageDelta }}%)</span
>
({{ pipelineCoverageDelta }}%)
</span>
{{ pipelineCoverageJobNumberText }}
<span ref="pipelineCoverageQuestion">
<gl-icon name="question" :size="12" />
@ -242,6 +254,12 @@ export default {
{{ build.name }} ({{ build.coverage }}%)
</div>
</gl-tooltip>
<gl-tooltip
:target="() => $refs.pipelineCoverageDelta"
data-testid="pipeline-coverage-delta-tooltip"
>
{{ pipelineCoverageTooltipDeltaDescription }}
</gl-tooltip>
</div>
</div>
</div>

View File

@ -47,6 +47,15 @@ class IssuesFinder < IssuableFinder
# rubocop: disable CodeReuse/ActiveRecord
def with_confidentiality_access_check
return Issue.all if params.user_can_see_all_confidential_issues?
if Feature.enabled?(:optimize_issue_filter_assigned_to_self, default_enabled: :yaml)
# If already filtering by assignee we can skip confidentiality since a user
# can always see confidential issues assigned to them. This is just an
# optimization since a very common usecase of this Finder is to load the
# count of issues assigned to the user for the header bar.
return Issue.all if current_user && params.assignees.include?(current_user)
end
return Issue.where('issues.confidential IS NOT TRUE') if params.user_cannot_see_confidential_issues?
Issue.where('

View File

@ -73,11 +73,7 @@ module Ci
end
def variables
if ::Feature.enabled?(:ci_trigger_payload_into_pipeline, project, default_enabled: :yaml)
param_variables + [payload_variable]
else
param_variables
end
param_variables + [payload_variable]
end
def param_variables

View File

@ -110,7 +110,8 @@ module Namespaces
end
def range
(interval + 1).days.ago.beginning_of_day..(interval + 1).days.ago.end_of_day
date = (interval + 1).days.ago
date.beginning_of_day..date.end_of_day
end
def incomplete_action

View File

@ -0,0 +1,6 @@
---
title: Fix Conan project-level API to return correct download-urls and fix Conan project-level
functionality
merge_request: 56899
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Clarify what coverage means on the merge request pipeline section
merge_request: 57275
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Remove the FF ci_trigger_payload_into_pipeline
merge_request: 57087
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Optimize database performance of loading assigned issue count on header bar
merge_request: 57073
author:
type: performance

View File

@ -1,8 +1,8 @@
---
name: ci_trigger_payload_into_pipeline
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53837
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321027
milestone: '13.9'
name: optimize_issue_filter_assigned_to_self
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57073
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/325470
milestone: '13.11'
type: development
group: group::pipeline authoring
default_enabled: true
group: group::global search
default_enabled: false

View File

@ -26,7 +26,7 @@
},
"status": {
"type": ["string"],
"enum": ["data_available", "planned", "in_progress", "implemented", "not_used", "deprecated"]
"enum": ["data_available", "implemented", "not_used", "deprecated"]
},
"milestone": {
"type": ["string", "null"],

View File

@ -121,6 +121,7 @@ const alias = {
images: path.join(ROOT_PATH, 'app/assets/images'),
vendor: path.join(ROOT_PATH, 'vendor/assets/javascripts'),
vue$: 'vue/dist/vue.esm.js',
jquery$: 'jquery/dist/jquery.slim.js',
spec: path.join(ROOT_PATH, 'spec/javascripts'),
jest: path.join(ROOT_PATH, 'spec/frontend'),
shared_queries: path.join(ROOT_PATH, 'app/graphql/queries'),

View File

@ -13,6 +13,9 @@ module.exports = {
mode: 'development',
resolve: {
extensions: ['.js'],
alias: {
jquery$: 'jquery/dist/jquery.slim.js',
},
},
// ensure output is not generated when errors are encountered
@ -22,7 +25,7 @@ module.exports = {
entry: {
vendor: [
'jquery',
'jquery/dist/jquery.slim.js',
'pdfjs-dist/build/pdf',
'pdfjs-dist/build/pdf.worker.min',
'sql.js',

View File

@ -21,6 +21,7 @@ def check_changelog_yaml(path)
fail "`type` should be set, in #{helper.html_link(path)}! #{SEE_DOC}" if yaml["type"].nil?
return if helper.security_mr?
return if helper.mr_iid.empty?
cherry_pick_against_stable_branch = helper.cherry_pick_mr? && helper.stable_branch?
@ -28,12 +29,12 @@ def check_changelog_yaml(path)
mr_line = raw_file.lines.find_index("merge_request:\n")
if mr_line
markdown(format(SUGGEST_MR_COMMENT, mr_iid: gitlab.mr_json["iid"]), file: path, line: mr_line.succ)
markdown(format(SUGGEST_MR_COMMENT, mr_iid: helper.mr_iid), file: path, line: mr_line.succ)
else
message "Consider setting `merge_request` to #{gitlab.mr_json["iid"]} in #{helper.html_link(path)}. #{SEE_DOC}"
message "Consider setting `merge_request` to #{helper.mr_iid} in #{helper.html_link(path)}. #{SEE_DOC}"
end
elsif yaml["merge_request"] != gitlab.mr_json["iid"] && !cherry_pick_against_stable_branch
fail "Merge request ID was not set to #{gitlab.mr_json["iid"]}! #{SEE_DOC}"
elsif yaml["merge_request"] != helper.mr_iid && !cherry_pick_against_stable_branch
fail "Merge request ID was not set to #{helper.mr_iid}! #{SEE_DOC}"
end
rescue Psych::Exception
# YAML could not be parsed, fail the build.

View File

@ -188,38 +188,13 @@ source repository. Be sure to URL-encode `ref` if it contains slashes.
### Using webhook payload in the triggered pipeline
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/31197) in GitLab 13.9.
> - It's [deployed behind a feature flag](../../user/feature_flags.md), enabled by default.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-the-trigger_payload-variable). **(FREE SELF)**
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/321027) in GitLab 13.11.
If you trigger a pipeline by using a webhook, you can access the webhook payload with
the `TRIGGER_PAYLOAD` [predefined CI/CD variable](../variables/predefined_variables.md).
The payload is exposed as a [file-type variable](../variables/README.md#custom-cicd-variables-of-type-file),
so you can access the data with `cat $TRIGGER_PAYLOAD` or a similar command.
#### Enable or disable the `TRIGGER_PAYLOAD` variable
The `TRIGGER_PAYLOAD` CI/CD variable is under development but ready for production use.
It is deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md)
can opt to disable it.
To disable it:
```ruby
Feature.disable(:ci_trigger_payload_into_pipeline)
```
To enable it:
```ruby
Feature.enable(:ci_trigger_payload_into_pipeline)
```
## Making use of trigger variables
You can pass any number of arbitrary variables in the trigger API call and they

View File

@ -33,7 +33,7 @@ Each metric is defined in a separate YAML file consisting of a number of fields:
| `product_group` | yes | The [group](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/stages.yml) that owns the metric. |
| `product_category` | no | The [product category](https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/categories.yml) for the metric. |
| `value_type` | yes | `string`; one of [`string`, `number`, `boolean`, `object`](https://json-schema.org/understanding-json-schema/reference/type.html). |
| `status` | yes | `string`; status of the metric, may be set to `data_available`, `planned`, `in_progress`, `implemented`, `not_used`, `deprecated` |
| `status` | yes | `string`; [status](#metric-statuses) of the metric, may be set to `data_available`, `implemented`, `not_used`, `deprecated`. |
| `time_frame` | yes | `string`; may be set to a value like `7d`, `28d`, `all`, `none`. |
| `data_source` | yes | `string`; may be set to a value like `database`, `redis`, `redis_hll`, `prometheus`, `ruby`. |
| `distribution` | yes | `array`; may be set to one of `ce, ee` or `ee`. The [distribution](https://about.gitlab.com/handbook/marketing/strategic-marketing/tiers/#definitions) where the tracked feature is available. |
@ -43,6 +43,16 @@ Each metric is defined in a separate YAML file consisting of a number of fields:
| `introduced_by_url` | no | The URL to the Merge Request that introduced the metric. |
| `skip_validation` | no | This should **not** be set. [Used for imported metrics until we review, update and make them valid](https://gitlab.com/groups/gitlab-org/-/epics/5425). |
### Metric statuses
Metric definitions can have one of the following statuses:
- `data_available`: Metric data is available and used in a Sisense dashboard.
- `implemented`: Metric is implemented but data is not yet available. This is a temporary
status for newly added metrics awaiting inclusion in a new release.
- `not_used`: Metric is not used in any dashboard.
- `deprecated`: Metric is deprecated and possibly planned to be removed.
### Example YAML metric definition
The linked [`uuid`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/license/uuid.yml)

View File

@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# On-call Schedule Management **(PREMIUM)**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4544) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.10.
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4544) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.11.
Use on-call schedule management to create schedules for responders to rotate on-call
responsibilities. Maintain the availability of your software services by putting your teams on-call.

View File

@ -25,6 +25,10 @@ You can restrict the password authentication for web interface and Git over HTTP
## Admin Mode
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2158) in GitLab 13.10.
> - It's [deployed behind the feature flag](../../../user/feature_flags.md) `:user_mode_in_session`, disabled by default.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to enable it.
When this feature is enabled, instance administrators are limited as regular users. During that period,
they do not have access to all projects, groups, or the **Admin Area** menu.
@ -47,7 +51,7 @@ OmniAuth providers and LDAP auth. The Admin Mode status is stored in the active
session and remains active until it is explicitly disabled (it will be disabled
automatically after a timeout otherwise).
### Limitations
### Limitations of Admin Mode
The following access methods are **not** protected by Admin Mode:
@ -61,7 +65,7 @@ authentication steps.
We may address these limitations in the future. For more information see the following epic:
[Admin mode for GitLab Administrators](https://gitlab.com/groups/gitlab-org/-/epics/2158).
### Troubleshooting
### Troubleshooting Admin Mode
If necessary, you can disable **Admin Mode** as an administrator by using one of these two methods:
@ -76,6 +80,25 @@ If necessary, you can disable **Admin Mode** as an administrator by using one of
```ruby
::Gitlab::CurrentSettings.update_attributes!(admin_mode: false)
```
## Enable or disable Admin Mode
Admin Mode is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it.
To enable it:
```ruby
Feature.enable(:user_mode_in_session)
```
To disable it:
```ruby
Feature.disable(:user_mode_in_session)
```
## Two-factor authentication

View File

@ -536,7 +536,8 @@ variables:
### URL scan
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214120) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.4.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214120) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.4.
> - [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/273141) in GitLab 13.11.
A URL scan allows you to specify which parts of a website are scanned by DAST.
@ -560,26 +561,19 @@ category/shoes/page1.html
```
To scan the URLs in that file, set the CI/CD variable `DAST_PATHS_FILE` to the path of that file.
The file can be checked into the project repository or generated as an artifact by a job that
runs before DAST.
By default, DAST scans do not clone the project repository. Instruct the DAST job to clone
the project by setting `GIT_STRATEGY` to fetch. Give a file path relative to `CI_PROJECT_DIR` to `DAST_PATHS_FILE`.
```yaml
include:
- template: DAST.gitlab-ci.yml
variables:
DAST_PATHS_FILE: url_file.txt
```
By default, DAST scans do not clone the project repository. If the file is checked in to the project, instruct the DAST job to clone the project by setting GIT_STRATEGY to fetch. The file is expected to be in the `/zap/wrk` directory.
```yaml
dast:
script:
- mkdir -p /zap/wrk
- cp url_file.txt /zap/wrk/url_file.txt
- /analyze -t $DAST_WEBSITE
variables:
GIT_STRATEGY: fetch
DAST_PATHS_FILE: url_file.txt
GIT_STRATEGY: fetch
DAST_PATHS_FILE: url_file.txt # url_file.txt lives in the root directory of the project
```
##### Use `DAST_PATHS` CI/CD variable
@ -653,7 +647,7 @@ DAST can be [configured](#customizing-the-dast-settings) using CI/CD variables.
| `DAST_MASK_HTTP_HEADERS` | string | Comma-separated list of request and response headers to be masked (GitLab 13.1). Must contain **all** headers to be masked. Refer to [list of headers that are masked by default](#hide-sensitive-information). |
| `DAST_EXCLUDE_URLS` | URLs | The URLs to skip during the authenticated scan; comma-separated. Regular expression syntax can be used to match multiple URLs. For example, `.*` matches an arbitrary character sequence. Not supported for API scans. |
| `DAST_FULL_SCAN_ENABLED` | boolean | Set to `true` to run a [ZAP Full Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Full-Scan) instead of a [ZAP Baseline Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Baseline-Scan). Default: `false` |
| `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` | boolean | Set to `true` to require [domain validation](#domain-validation) when running DAST full scans. Not supported for API scans. Default: `false` |
| `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` | boolean | [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/293595) in GitLab 13.8, to be removed in 14.0. Set to `true` to require [domain validation](#domain-validation) when running DAST full scans. Not supported for API scans. Default: `false` |
| `DAST_AUTO_UPDATE_ADDONS` | boolean | ZAP add-ons are pinned to specific versions in the DAST Docker image. Set to `true` to download the latest versions when the scan starts. Default: `false` |
| `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. |
@ -666,7 +660,7 @@ DAST can be [configured](#customizing-the-dast-settings) using CI/CD variables.
| `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_PATHS_FILE` | string | The file path containing the paths within `DAST_WEBSITE` to scan. The file must be plain text with one path per line and be in `/zap/wrk`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/258825) in GitLab 13.6. |
| `DAST_PATHS_FILE` | string | The file path containing the paths within `DAST_WEBSITE` to scan. The file must be plain text with one path per line. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/258825) in GitLab 13.6. |
| `DAST_SUBMIT_FIELD` | string | The `id` or `name` of the element that when clicked submits the login form or the password form of a multi-page login process. [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9894) in GitLab 12.4. |
| `DAST_FIRST_SUBMIT_FIELD` | string | The `id` or `name` of the element that when clicked submits the username form of a multi-page login process. [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/9894) in GitLab 12.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. |

View File

@ -402,16 +402,3 @@ The GitLab Conan repository supports the following Conan CLI commands:
packages you have permission to view.
- `conan info`: View the information on a given package from the Package Registry.
- `conan remove`: Delete the package from the Package Registry.
## Troubleshooting Conan packages
### `ERROR: <package> was not found in remote <remote>`
When you attempt to install a Conan package, you might receive a `404` error
like `ERROR: <package> was not found in remote <remote>`.
This issue occurs when you request a download from the project-level Conan API.
The resulting URL is missing is project's `/<id>` and Conan commands, like
`conan install`, fail.
For more information, see [issue 270129](https://gitlab.com/gitlab-org/gitlab/-/issues/270129).

View File

@ -50,6 +50,7 @@ module.exports = (path, options = {}) => {
'emojis(/.*).json': '<rootDir>/fixtures/emojis$1.json',
'^spec/test_constants$': '<rootDir>/spec/frontend/__helpers__/test_constants',
'^jest/(.*)$': '<rootDir>/spec/frontend/$1',
'^jquery$': '<rootDir>/node_modules/jquery/dist/jquery.slim.js',
...extModuleNameMapper,
};

View File

@ -14,7 +14,8 @@ module API
package,
current_user,
project,
conan_package_reference: params[:conan_package_reference]
conan_package_reference: params[:conan_package_reference],
id: params[:id]
)
render_api_error!("No recipe manifest found", 404) if yield(presenter).empty?

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module UserMentions
module Lib
module Banzai
module ReferenceParser
# isolated Banzai::ReferenceParser::MentionedGroupParser
class IsolatedMentionedProjectParser < ::Banzai::ReferenceParser::MentionedProjectParser
extend ::Gitlab::Utils::Override
self.reference_type = :user
override :references_relation
def references_relation
::Gitlab::BackgroundMigration::UserMentions::Models::Project
end
end
end
end
end
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module UserMentions
module Lib
module Banzai
module ReferenceParser
# isolated Banzai::ReferenceParser::MentionedGroupParser
class IsolatedMentionedUserParser < ::Banzai::ReferenceParser::MentionedUserParser
extend ::Gitlab::Utils::Override
self.reference_type = :user
override :references_relation
def references_relation
::Gitlab::BackgroundMigration::UserMentions::Models::User
end
end
end
end
end
end
end
end

View File

@ -7,7 +7,7 @@ module Gitlab
module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class IsolatedReferenceExtractor < ::Gitlab::ReferenceExtractor
REFERABLES = %i(isolated_mentioned_group).freeze
REFERABLES = %i(isolated_mentioned_group isolated_mentioned_user isolated_mentioned_project).freeze
REFERABLES.each do |type|
define_method("#{type}s") do

View File

@ -0,0 +1,60 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module UserMentions
module Lib
module Gitlab
# Gitlab::IsolatedVisibilityLevel module
#
# Define allowed public modes that can be used for
# GitLab projects to determine project public mode
#
module IsolatedVisibilityLevel
extend ::ActiveSupport::Concern
included do
scope :public_to_user, -> (user = nil) do
where(visibility_level: IsolatedVisibilityLevel.levels_for_user(user))
end
end
PRIVATE = 0 unless const_defined?(:PRIVATE)
INTERNAL = 10 unless const_defined?(:INTERNAL)
PUBLIC = 20 unless const_defined?(:PUBLIC)
class << self
def levels_for_user(user = nil)
return [PUBLIC] unless user
if user.can_read_all_resources?
[PRIVATE, INTERNAL, PUBLIC]
elsif user.external?
[PUBLIC]
else
[INTERNAL, PUBLIC]
end
end
end
def private?
visibility_level_value == PRIVATE
end
def internal?
visibility_level_value == INTERNAL
end
def public?
visibility_level_value == PUBLIC
end
def visibility_level_value
self[visibility_level_field]
end
end
end
end
end
end
end

View File

@ -7,6 +7,7 @@ module Gitlab
module Models
class CommitUserMention < ActiveRecord::Base
self.table_name = 'commit_user_mentions'
self.inheritance_column = :_type_disabled
def self.resource_foreign_key
:commit_id

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module UserMentions
module Models
module Concerns
# isolated FeatureGate module
module IsolatedFeatureGate
def flipper_id
return if new_record?
"#{self.class.name}:#{id}"
end
end
end
end
end
end
end

View File

@ -70,8 +70,8 @@ module Gitlab
def build_mention_values(resource_foreign_key)
refs = all_references(author)
mentioned_users_ids = array_to_sql(refs.mentioned_users.pluck(:id))
mentioned_projects_ids = array_to_sql(refs.mentioned_projects.pluck(:id))
mentioned_users_ids = array_to_sql(refs.isolated_mentioned_users.pluck(:id))
mentioned_projects_ids = array_to_sql(refs.isolated_mentioned_projects.pluck(:id))
mentioned_groups_ids = array_to_sql(refs.isolated_mentioned_groups.pluck(:id))
return if mentioned_users_ids.blank? && mentioned_projects_ids.blank? && mentioned_groups_ids.blank?

View File

@ -6,7 +6,7 @@ module Gitlab
module Models
module Concerns
module Namespace
# extracted methods for recursive traversing of namespace hierarchy
# isolate recursive traversal code for namespace hierarchy
module RecursiveTraversal
extend ActiveSupport::Concern

View File

@ -10,6 +10,9 @@ module Gitlab
include EachBatch
include Concerns::MentionableMigrationMethods
self.table_name = 'design_management_designs'
self.inheritance_column = :_type_disabled
def self.user_mention_model
Gitlab::BackgroundMigration::UserMentions::Models::DesignUserMention
end

View File

@ -7,6 +7,7 @@ module Gitlab
module Models
class DesignUserMention < ActiveRecord::Base
self.table_name = 'design_user_mentions'
self.inheritance_column = :_type_disabled
def self.resource_foreign_key
:design_id

View File

@ -17,10 +17,10 @@ module Gitlab
cache_markdown_field :description, issuable_state_filter_enabled: true
self.table_name = 'epics'
self.inheritance_column = :_type_disabled
belongs_to :author, class_name: "User"
belongs_to :project
belongs_to :group
belongs_to :author, class_name: "::Gitlab::BackgroundMigration::UserMentions::Models::User"
belongs_to :group, class_name: "::Gitlab::BackgroundMigration::UserMentions::Models::Group"
def self.user_mention_model
Gitlab::BackgroundMigration::UserMentions::Models::EpicUserMention

View File

@ -7,6 +7,7 @@ module Gitlab
module Models
class EpicUserMention < ActiveRecord::Base
self.table_name = 'epic_user_mentions'
self.inheritance_column = :_type_disabled
def self.resource_foreign_key
:epic_id

View File

@ -7,6 +7,8 @@ module Gitlab
# isolated Group model
class Group < ::Gitlab::BackgroundMigration::UserMentions::Models::Namespace
self.store_full_sti_class = false
self.inheritance_column = :_type_disabled
has_one :saml_provider
def self.declarative_policy_class

View File

@ -17,10 +17,11 @@ module Gitlab
cache_markdown_field :description, issuable_state_filter_enabled: true
self.table_name = 'merge_requests'
self.inheritance_column = :_type_disabled
belongs_to :author, class_name: "User"
belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project"
belongs_to :author, class_name: "::Gitlab::BackgroundMigration::UserMentions::Models::User"
belongs_to :target_project, class_name: "::Gitlab::BackgroundMigration::UserMentions::Models::Project"
belongs_to :source_project, class_name: "::Gitlab::BackgroundMigration::UserMentions::Models::Project"
alias_attribute :project, :target_project

View File

@ -7,6 +7,7 @@ module Gitlab
module Models
class MergeRequestUserMention < ActiveRecord::Base
self.table_name = 'merge_request_user_mentions'
self.inheritance_column = :_type_disabled
def self.resource_foreign_key
:merge_request_id

View File

@ -5,9 +5,11 @@ module Gitlab
module UserMentions
module Models
# isolated Namespace model
class Namespace < ApplicationRecord
include FeatureGate
include ::Gitlab::VisibilityLevel
class Namespace < ActiveRecord::Base
self.inheritance_column = :_type_disabled
include Concerns::IsolatedFeatureGate
include Gitlab::BackgroundMigration::UserMentions::Lib::Gitlab::IsolatedVisibilityLevel
include ::Gitlab::Utils::StrongMemoize
include Gitlab::BackgroundMigration::UserMentions::Models::Concerns::Namespace::RecursiveTraversal

View File

@ -16,9 +16,9 @@ module Gitlab
attr_mentionable :note, pipeline: :note
cache_markdown_field :note, pipeline: :note, issuable_state_filter_enabled: true
belongs_to :author, class_name: "User"
belongs_to :author, class_name: "::Gitlab::BackgroundMigration::UserMentions::Models::User"
belongs_to :noteable, polymorphic: true
belongs_to :project
belongs_to :project, class_name: "::Gitlab::BackgroundMigration::UserMentions::Models::Project"
def for_personal_snippet?
noteable && noteable.class.name == 'PersonalSnippet'

View File

@ -0,0 +1,48 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module UserMentions
module Models
# isolated Namespace model
class Project < ActiveRecord::Base
include Concerns::IsolatedFeatureGate
include Gitlab::BackgroundMigration::UserMentions::Lib::Gitlab::IsolatedVisibilityLevel
self.table_name = 'projects'
self.inheritance_column = :_type_disabled
belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id', class_name: "::Gitlab::BackgroundMigration::UserMentions::Models::Group"
belongs_to :namespace, class_name: "::Gitlab::BackgroundMigration::UserMentions::Models::Namespace"
alias_method :parent, :namespace
# Returns a collection of projects that is either public or visible to the
# logged in user.
def self.public_or_visible_to_user(user = nil, min_access_level = nil)
min_access_level = nil if user&.can_read_all_resources?
return public_to_user unless user
if user.is_a?(::Gitlab::BackgroundMigration::UserMentions::Models::User)
where('EXISTS (?) OR projects.visibility_level IN (?)',
user.authorizations_for_projects(min_access_level: min_access_level),
levels_for_user(user))
end
end
def grafana_integration
nil
end
def default_issues_tracker?
true # we do not care of the issue tracker type(internal or external) when parsing mentions
end
def visibility_level_field
:visibility_level
end
end
end
end
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
module UserMentions
module Models
# isolated Namespace model
class User < ActiveRecord::Base
include Concerns::IsolatedFeatureGate
self.table_name = 'users'
self.inheritance_column = :_type_disabled
has_many :project_authorizations, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
def authorizations_for_projects(min_access_level: nil, related_project_column: 'projects.id')
authorizations = project_authorizations
.select(1)
.where("project_authorizations.project_id = #{related_project_column}")
return authorizations unless min_access_level.present?
authorizations.where('project_authorizations.access_level >= ?', min_access_level)
end
def can_read_all_resources?
can?(:read_all_resources)
end
def can?(action, subject = :global)
Ability.allowed?(self, action, subject)
end
end
end
end
end
end

View File

@ -3,12 +3,12 @@
module Gitlab
module Database
module BackgroundMigration
class Scheduler
def perform(migration_wrapper: BatchedMigrationWrapper.new)
active_migration = BatchedMigration.active.queue_order.first
return unless active_migration&.interval_elapsed?
class BatchedMigrationRunner
def initialize(migration_wrapper = BatchedMigrationWrapper.new)
@migration_wrapper = migration_wrapper
end
def run_migration_job(active_migration)
if next_batched_job = create_next_batched_job!(active_migration)
migration_wrapper.perform(next_batched_job)
else
@ -16,8 +16,18 @@ module Gitlab
end
end
def run_entire_migration(migration)
while migration.active?
run_migration_job(migration)
migration.reload_last_job
end
end
private
attr_reader :migration_wrapper
def create_next_batched_job!(active_migration)
next_batch_range = find_next_batch_range(active_migration)

View File

@ -4,6 +4,7 @@ module Gitlab
module Database
module MigrationHelpers
include Migrations::BackgroundMigrationHelpers
include DynamicModelHelpers
# https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS
MAX_IDENTIFIER_NAME_LENGTH = 63
@ -927,52 +928,19 @@ module Gitlab
# This is crucial for Primary Key conversions, because setting a column
# as the PK converts even check constraints to NOT NULL constraints
# and forces an inline re-verification of the whole table.
# - It backfills the new column with the values of the existing primary key
# by scheduling background jobs.
# - It tracks the scheduled background jobs through the use of
# Gitlab::Database::BackgroundMigrationJob
# which allows a more thorough check that all jobs succeeded in the
# cleanup migration and is way faster for very large tables.
# - It sets up a trigger to keep the two columns in sync
# - It does not schedule a cleanup job: we have to do that with followup
# post deployment migrations in the next release.
# - It sets up a trigger to keep the two columns in sync.
#
# This needs to be done manually by using the
# `cleanup_initialize_conversion_of_integer_to_bigint`
# (not yet implemented - check #288005)
# Note: this helper is intended to be used in a regular (pre-deployment) migration.
#
# This helper is part 1 of a multi-step migration process:
# 1. initialize_conversion_of_integer_to_bigint to create the new column and database triggers
# 2. backfill_conversion_of_integer_to_bigint to copy historic data using background migrations
# 3. remaining steps TBD, see #288005
#
# table - The name of the database table containing the column
# column - The name of the column that we want to convert to bigint.
# primary_key - The name of the primary key column (most often :id)
# batch_size - The number of rows to schedule in a single background migration
# sub_batch_size - The smaller batches that will be used by each scheduled job
# to update the table. Useful to keep each update at ~100ms while executing
# more updates per interval (2.minutes)
# Note that each execution of a sub-batch adds a constant 100ms sleep
# time in between the updates, which must be taken into account
# while calculating the batch, sub_batch and interval values.
# interval - The time interval between every background migration
#
# example:
# Assume that we have figured out that updating 200 records of the events
# table takes ~100ms on average.
# We can set the sub_batch_size to 200, leave the interval to the default
# and set the batch_size to 50_000 which will require
# ~50s = (50000 / 200) * (0.1 + 0.1) to complete and leaves breathing space
# between the scheduled jobs
def initialize_conversion_of_integer_to_bigint(
table,
column,
primary_key: :id,
batch_size: 20_000,
sub_batch_size: 1000,
interval: 2.minutes
)
if transaction_open?
raise 'initialize_conversion_of_integer_to_bigint can not be run inside a transaction'
end
def initialize_conversion_of_integer_to_bigint(table, column, primary_key: :id)
unless table_exists?(table)
raise "Table #{table} does not exist"
end
@ -1003,28 +971,85 @@ module Gitlab
install_rename_triggers(table, column, tmp_column)
end
end
source_model = Class.new(ActiveRecord::Base) do
include EachBatch
# Backfills the new column used in the conversion of an integer column to bigint using background migrations.
#
# - This helper should be called from a post-deployment migration.
# - In order for this helper to work properly, the new column must be first initialized with
# the `initialize_conversion_of_integer_to_bigint` helper.
# - It tracks the scheduled background jobs through Gitlab::Database::BackgroundMigration::BatchedMigration,
# which allows a more thorough check that all jobs succeeded in the
# cleanup migration and is way faster for very large tables.
#
# Note: this helper is intended to be used in a post-deployment migration, to ensure any new code is
# deployed (including background job changes) before we begin processing the background migration.
#
# This helper is part 2 of a multi-step migration process:
# 1. initialize_conversion_of_integer_to_bigint to create the new column and database triggers
# 2. backfill_conversion_of_integer_to_bigint to copy historic data using background migrations
# 3. remaining steps TBD, see #288005
#
# table - The name of the database table containing the column
# column - The name of the column that we want to convert to bigint.
# primary_key - The name of the primary key column (most often :id)
# batch_size - The number of rows to schedule in a single background migration
# sub_batch_size - The smaller batches that will be used by each scheduled job
# to update the table. Useful to keep each update at ~100ms while executing
# more updates per interval (2.minutes)
# Note that each execution of a sub-batch adds a constant 100ms sleep
# time in between the updates, which must be taken into account
# while calculating the batch, sub_batch and interval values.
# interval - The time interval between every background migration
#
# example:
# Assume that we have figured out that updating 200 records of the events
# table takes ~100ms on average.
# We can set the sub_batch_size to 200, leave the interval to the default
# and set the batch_size to 50_000 which will require
# ~50s = (50000 / 200) * (0.1 + 0.1) to complete and leaves breathing space
# between the scheduled jobs
def backfill_conversion_of_integer_to_bigint(
table,
column,
primary_key: :id,
batch_size: 20_000,
sub_batch_size: 1000,
interval: 2.minutes
)
self.table_name = table
self.inheritance_column = :_type_disabled
unless table_exists?(table)
raise "Table #{table} does not exist"
end
queue_background_migration_jobs_by_range_at_intervals(
source_model,
unless column_exists?(table, primary_key)
raise "Column #{primary_key} does not exist on #{table}"
end
unless column_exists?(table, column)
raise "Column #{column} does not exist on #{table}"
end
tmp_column = "#{column}_convert_to_bigint"
unless column_exists?(table, tmp_column)
raise 'The temporary column does not exist, initialize it with `initialize_conversion_of_integer_to_bigint`'
end
batched_migration = queue_batched_background_migration(
'CopyColumnUsingBackgroundMigrationJob',
interval,
table,
primary_key,
column,
tmp_column,
job_interval: interval,
batch_size: batch_size,
other_job_arguments: [table, primary_key, sub_batch_size, column, tmp_column],
track_jobs: true,
primary_column_name: primary_key
)
sub_batch_size: sub_batch_size)
if perform_background_migration_inline?
# To ensure the schema is up to date immediately we perform the
# migration inline in dev / test environments.
Gitlab::BackgroundMigration.steal('CopyColumnUsingBackgroundMigrationJob')
Gitlab::Database::BackgroundMigration::BatchedMigrationRunner.new.run_entire_migration(batched_migration)
end
end

View File

@ -8722,11 +8722,6 @@ msgstr ""
msgid "Coverage Fuzzing"
msgstr ""
msgid "Coverage value for this pipeline was calculated by the coverage value of %d job."
msgid_plural "Coverage value for this pipeline was calculated by averaging the resulting coverage values of %d jobs."
msgstr[0] ""
msgstr[1] ""
msgid "Create"
msgstr ""
@ -22668,9 +22663,6 @@ msgstr ""
msgid "Pipeline|Could not retrieve the pipeline status. For troubleshooting steps, read the %{linkStart}documentation%{linkEnd}."
msgstr ""
msgid "Pipeline|Coverage"
msgstr ""
msgid "Pipeline|Created"
msgstr ""
@ -22767,6 +22759,18 @@ msgstr ""
msgid "Pipeline|Tag name"
msgstr ""
msgid "Pipeline|Test coverage"
msgstr ""
msgid "Pipeline|This change will decrease the overall test coverage if merged."
msgstr ""
msgid "Pipeline|This change will increase the overall test coverage if merged."
msgstr ""
msgid "Pipeline|This change will not change the overall test coverage if merged."
msgstr ""
msgid "Pipeline|Trigger author"
msgstr ""
@ -29834,6 +29838,11 @@ msgstr ""
msgid "Test coverage parsing"
msgstr ""
msgid "Test coverage value for this pipeline was calculated by the coverage value of %d job."
msgid_plural "Test coverage value for this pipeline was calculated by averaging the resulting coverage values of %d jobs."
msgstr[0] ""
msgstr[1] ""
msgid "Test coverage: %d hit"
msgid_plural "Test coverage: %d hits"
msgstr[0] ""
@ -34910,9 +34919,6 @@ msgstr ""
msgid "You need permission."
msgstr ""
msgid "You need to be logged in."
msgstr ""
msgid "You need to register a two-factor authentication app before you can set up a device."
msgstr ""

View File

@ -1,6 +1,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount, mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import PipelineMiniGraph from '~/pipelines/components/pipelines_list/pipeline_mini_graph.vue';
import PipelineStage from '~/pipelines/components/pipelines_list/pipeline_stage.vue';
import PipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
@ -22,27 +23,30 @@ describe('MRWidgetPipeline', () => {
'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.';
const monitoringMessage = 'Checking pipeline status.';
const findCIErrorMessage = () => wrapper.find('[data-testid="ci-error-message"]');
const findPipelineID = () => wrapper.find('[data-testid="pipeline-id"]');
const findPipelineInfoContainer = () => wrapper.find('[data-testid="pipeline-info-container"]');
const findCommitLink = () => wrapper.find('[data-testid="commit-link"]');
const findPipelineMiniGraph = () => wrapper.find(PipelineMiniGraph);
const findAllPipelineStages = () => wrapper.findAll(PipelineStage);
const findPipelineCoverage = () => wrapper.find('[data-testid="pipeline-coverage"]');
const findPipelineCoverageDelta = () => wrapper.find('[data-testid="pipeline-coverage-delta"]');
const findCIErrorMessage = () => wrapper.findByTestId('ci-error-message');
const findPipelineID = () => wrapper.findByTestId('pipeline-id');
const findPipelineInfoContainer = () => wrapper.findByTestId('pipeline-info-container');
const findCommitLink = () => wrapper.findByTestId('commit-link');
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const findAllPipelineStages = () => wrapper.findAllComponents(PipelineStage);
const findPipelineCoverage = () => wrapper.findByTestId('pipeline-coverage');
const findPipelineCoverageDelta = () => wrapper.findByTestId('pipeline-coverage-delta');
const findPipelineCoverageTooltipText = () =>
wrapper.find('[data-testid="pipeline-coverage-tooltip"]').text();
const findMonitoringPipelineMessage = () =>
wrapper.find('[data-testid="monitoring-pipeline-message"]');
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
wrapper.findByTestId('pipeline-coverage-tooltip').text();
const findPipelineCoverageDeltaTooltipText = () =>
wrapper.findByTestId('pipeline-coverage-delta-tooltip').text();
const findMonitoringPipelineMessage = () => wrapper.findByTestId('monitoring-pipeline-message');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const createWrapper = (props = {}, mountFn = shallowMount) => {
wrapper = mountFn(PipelineComponent, {
propsData: {
...defaultProps,
...props,
},
});
wrapper = extendedWrapper(
mountFn(PipelineComponent, {
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
@ -94,7 +98,9 @@ describe('MRWidgetPipeline', () => {
describe('should render pipeline coverage information', () => {
it('should render coverage percentage', () => {
expect(findPipelineCoverage().text()).toMatch(`Coverage ${mockData.pipeline.coverage}%`);
expect(findPipelineCoverage().text()).toMatch(
`Test coverage ${mockData.pipeline.coverage}%`,
);
});
it('should render coverage delta', () => {
@ -102,24 +108,9 @@ describe('MRWidgetPipeline', () => {
expect(findPipelineCoverageDelta().text()).toBe(`(${mockData.pipelineCoverageDelta}%)`);
});
it('coverage delta should have no special style if there is no coverage change', () => {
createWrapper({ pipelineCoverageDelta: '0' });
expect(findPipelineCoverageDelta().classes()).toEqual([]);
});
it('coverage delta should have text-success style if coverage increased', () => {
createWrapper({ pipelineCoverageDelta: '10' });
expect(findPipelineCoverageDelta().classes()).toEqual(['text-success']);
});
it('coverage delta should have text-danger style if coverage increased', () => {
createWrapper({ pipelineCoverageDelta: '-10' });
expect(findPipelineCoverageDelta().classes()).toEqual(['text-danger']);
});
it('should render tooltip for jobs contributing to code coverage', () => {
const tooltipText = findPipelineCoverageTooltipText();
const expectedDescription = `Coverage value for this pipeline was calculated by averaging the resulting coverage values of ${mockData.buildsWithCoverage.length} jobs.`;
const expectedDescription = `Test coverage value for this pipeline was calculated by averaging the resulting coverage values of ${mockData.buildsWithCoverage.length} jobs.`;
expect(tooltipText).toContain(expectedDescription);
});
@ -132,6 +123,26 @@ describe('MRWidgetPipeline', () => {
expect(tooltipText).toContain(`${build.name} (${build.coverage}%)`);
},
);
describe.each`
style | coverageState | coverageChangeText | styleClass | pipelineCoverageDelta
${'no special'} | ${'the same'} | ${'not change'} | ${''} | ${'0'}
${'success'} | ${'increased'} | ${'increase'} | ${'text-success'} | ${'10'}
${'danger'} | ${'decreased'} | ${'decrease'} | ${'text-danger'} | ${'-10'}
`(
'if test coverage is $coverageState',
({ style, styleClass, coverageChangeText, pipelineCoverageDelta }) => {
it(`coverage delta should have ${style}`, () => {
createWrapper({ pipelineCoverageDelta });
expect(findPipelineCoverageDelta().classes()).toEqual(styleClass ? [styleClass] : []);
});
it(`coverage delta tooltip should say that the coverage will ${coverageChangeText}`, () => {
createWrapper({ pipelineCoverageDelta });
expect(findPipelineCoverageDeltaTooltipText()).toContain(coverageChangeText);
});
},
);
});
});
@ -163,7 +174,7 @@ describe('MRWidgetPipeline', () => {
});
it('should render coverage information', () => {
expect(findPipelineCoverage().text()).toMatch(`Coverage ${mockData.pipeline.coverage}%`);
expect(findPipelineCoverage().text()).toMatch(`Test coverage ${mockData.pipeline.coverage}%`);
});
});

View File

@ -0,0 +1,185 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigrationRunner do
let(:migration_wrapper) { double('test wrapper') }
let(:runner) { described_class.new(migration_wrapper) }
describe '#run_migration_job' do
shared_examples_for 'it has completed the migration' do
it 'does not create and run a migration job' do
expect(migration_wrapper).not_to receive(:perform)
expect do
runner.run_migration_job(migration)
end.not_to change { Gitlab::Database::BackgroundMigration::BatchedJob.count }
end
it 'marks the migration as finished' do
relation = Gitlab::Database::BackgroundMigration::BatchedMigration.finished.where(id: migration.id)
expect { runner.run_migration_job(migration) }.to change { relation.count }.by(1)
end
end
context 'when the migration has no previous jobs' do
let(:migration) { create(:batched_background_migration, :active, batch_size: 2) }
let(:job_relation) do
Gitlab::Database::BackgroundMigration::BatchedJob.where(batched_background_migration_id: migration.id)
end
context 'when the migration has batches to process' do
let!(:event1) { create(:event) }
let!(:event2) { create(:event) }
let!(:event3) { create(:event) }
it 'runs the job for the first batch' do
migration.update!(min_value: event1.id, max_value: event2.id)
expect(migration_wrapper).to receive(:perform) do |job_record|
expect(job_record).to eq(job_relation.first)
end
expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(1)
expect(job_relation.first).to have_attributes(
min_value: event1.id,
max_value: event2.id,
batch_size: migration.batch_size,
sub_batch_size: migration.sub_batch_size)
end
end
context 'when the batch maximum exceeds the migration maximum' do
let!(:events) { create_list(:event, 3) }
let(:event1) { events[0] }
let(:event2) { events[1] }
it 'clamps the batch maximum to the migration maximum' do
migration.update!(min_value: event1.id, max_value: event2.id, batch_size: 5)
expect(migration_wrapper).to receive(:perform)
expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(1)
expect(job_relation.first).to have_attributes(
min_value: event1.id,
max_value: event2.id,
batch_size: migration.batch_size,
sub_batch_size: migration.sub_batch_size)
end
end
context 'when the migration has no batches to process' do
it_behaves_like 'it has completed the migration'
end
end
context 'when the migration has previous jobs' do
let!(:event1) { create(:event) }
let!(:event2) { create(:event) }
let!(:event3) { create(:event) }
let!(:migration) do
create(:batched_background_migration, :active, batch_size: 2, min_value: event1.id, max_value: event3.id)
end
let!(:previous_job) do
create(:batched_background_migration_job,
batched_migration: migration,
min_value: event1.id,
max_value: event2.id,
batch_size: 2,
sub_batch_size: 1)
end
let(:job_relation) do
Gitlab::Database::BackgroundMigration::BatchedJob.where(batched_background_migration_id: migration.id)
end
context 'when the migration has batches to process' do
it 'runs the migration job for the next batch' do
expect(migration_wrapper).to receive(:perform) do |job_record|
expect(job_record).to eq(job_relation.last)
end
expect { runner.run_migration_job(migration) }.to change { job_relation.count }.by(1)
expect(job_relation.last).to have_attributes(
min_value: event3.id,
max_value: event3.id,
batch_size: migration.batch_size,
sub_batch_size: migration.sub_batch_size)
end
context 'when the batch minimum exceeds the migration maximum' do
before do
migration.update!(batch_size: 5, max_value: event2.id)
end
it_behaves_like 'it has completed the migration'
end
end
context 'when the migration has no batches remaining' do
before do
create(:batched_background_migration_job,
batched_migration: migration,
min_value: event3.id,
max_value: event3.id,
batch_size: 2,
sub_batch_size: 1)
end
it_behaves_like 'it has completed the migration'
end
end
end
describe '#run_entire_migration' do
context 'when the given migration is not active' do
it 'does not create and run migration jobs' do
migration = build(:batched_background_migration, :finished)
expect(migration_wrapper).not_to receive(:perform)
expect do
runner.run_entire_migration(migration)
end.not_to change { Gitlab::Database::BackgroundMigration::BatchedJob.count }
end
end
context 'when the given migration is active' do
let!(:event1) { create(:event) }
let!(:event2) { create(:event) }
let!(:event3) { create(:event) }
let!(:migration) do
create(:batched_background_migration, :active, batch_size: 2, min_value: event1.id, max_value: event3.id)
end
let(:job_relation) do
Gitlab::Database::BackgroundMigration::BatchedJob.where(batched_background_migration_id: migration.id)
end
it 'runs all jobs inline until finishing the migration' do
expect(migration_wrapper).to receive(:perform) do |job_record|
expect(job_record).to eq(job_relation.first)
end
expect(migration_wrapper).to receive(:perform) do |job_record|
expect(job_record).to eq(job_relation.last)
end
expect { runner.run_entire_migration(migration) }.to change { job_relation.count }.by(2)
expect(job_relation.first).to have_attributes(min_value: event1.id, max_value: event2.id)
expect(job_relation.last).to have_attributes(min_value: event3.id, max_value: event3.id)
expect(migration.reload).to be_finished
end
end
end
end

View File

@ -1,182 +0,0 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Database::BackgroundMigration::Scheduler, '#perform' do
let(:scheduler) { described_class.new }
shared_examples_for 'it has no jobs to run' do
it 'does not create and run a migration job' do
test_wrapper = double('test wrapper')
expect(test_wrapper).not_to receive(:perform)
expect do
scheduler.perform(migration_wrapper: test_wrapper)
end.not_to change { Gitlab::Database::BackgroundMigration::BatchedJob.count }
end
end
context 'when there are no active migrations' do
let!(:migration) { create(:batched_background_migration, :finished) }
it_behaves_like 'it has no jobs to run'
end
shared_examples_for 'it has completed the migration' do
it 'marks the migration as finished' do
relation = Gitlab::Database::BackgroundMigration::BatchedMigration.finished.where(id: first_migration.id)
expect { scheduler.perform }.to change { relation.count }.by(1)
end
end
context 'when there are active migrations' do
let!(:first_migration) { create(:batched_background_migration, :active, batch_size: 2) }
let!(:last_migration) { create(:batched_background_migration, :active) }
let(:job_relation) do
Gitlab::Database::BackgroundMigration::BatchedJob.where(batched_background_migration_id: first_migration.id)
end
context 'when the migration interval has not elapsed' do
before do
expect_next_found_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigration) do |migration|
expect(migration).to receive(:interval_elapsed?).and_return(false)
end
end
it_behaves_like 'it has no jobs to run'
end
context 'when the interval has elapsed' do
before do
expect_next_found_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigration) do |migration|
expect(migration).to receive(:interval_elapsed?).and_return(true)
end
end
context 'when the first migration has no previous jobs' do
context 'when the migration has batches to process' do
let!(:event1) { create(:event) }
let!(:event2) { create(:event) }
let!(:event3) { create(:event) }
it 'runs the job for the first batch' do
first_migration.update!(min_value: event1.id, max_value: event3.id)
expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper) do |wrapper|
expect(wrapper).to receive(:perform).and_wrap_original do |_, job_record|
expect(job_record).to eq(job_relation.first)
end
end
expect { scheduler.perform }.to change { job_relation.count }.by(1)
expect(job_relation.first).to have_attributes(
min_value: event1.id,
max_value: event2.id,
batch_size: first_migration.batch_size,
sub_batch_size: first_migration.sub_batch_size)
end
end
context 'when the migration has no batches to process' do
it_behaves_like 'it has no jobs to run'
it_behaves_like 'it has completed the migration'
end
end
context 'when the first migration has previous jobs' do
let!(:event1) { create(:event) }
let!(:event2) { create(:event) }
let!(:event3) { create(:event) }
let!(:previous_job) do
create(:batched_background_migration_job,
batched_migration: first_migration,
min_value: event1.id,
max_value: event2.id,
batch_size: 2,
sub_batch_size: 1)
end
context 'when the migration is ready to process another job' do
it 'runs the migration job for the next batch' do
first_migration.update!(min_value: event1.id, max_value: event3.id)
expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper) do |wrapper|
expect(wrapper).to receive(:perform).and_wrap_original do |_, job_record|
expect(job_record).to eq(job_relation.last)
end
end
expect { scheduler.perform }.to change { job_relation.count }.by(1)
expect(job_relation.last).to have_attributes(
min_value: event3.id,
max_value: event3.id,
batch_size: first_migration.batch_size,
sub_batch_size: first_migration.sub_batch_size)
end
end
context 'when the migration has no batches remaining' do
let!(:final_job) do
create(:batched_background_migration_job,
batched_migration: first_migration,
min_value: event3.id,
max_value: event3.id,
batch_size: 2,
sub_batch_size: 1)
end
it_behaves_like 'it has no jobs to run'
it_behaves_like 'it has completed the migration'
end
end
context 'when the bounds of the next batch exceed the migration maximum value' do
let!(:events) { create_list(:event, 3) }
let(:event1) { events[0] }
let(:event2) { events[1] }
context 'when the batch maximum exceeds the migration maximum' do
it 'clamps the batch maximum to the migration maximum' do
first_migration.update!(batch_size: 5, min_value: event1.id, max_value: event2.id)
expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationWrapper) do |wrapper|
expect(wrapper).to receive(:perform)
end
expect { scheduler.perform }.to change { job_relation.count }.by(1)
expect(job_relation.first).to have_attributes(
min_value: event1.id,
max_value: event2.id,
batch_size: first_migration.batch_size,
sub_batch_size: first_migration.sub_batch_size)
end
end
context 'when the batch minimum exceeds the migration maximum' do
let!(:previous_job) do
create(:batched_background_migration_job,
batched_migration: first_migration,
min_value: event1.id,
max_value: event2.id,
batch_size: 5,
sub_batch_size: 1)
end
before do
first_migration.update!(batch_size: 5, min_value: 1, max_value: event2.id)
end
it_behaves_like 'it has no jobs to run'
it_behaves_like 'it has completed the migration'
end
end
end
end
end

View File

@ -1702,65 +1702,171 @@ RSpec.describe Gitlab::Database::MigrationHelpers do
end
describe '#initialize_conversion_of_integer_to_bigint' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project) }
let!(:event) do
create(:event, :created, project: project, target: issue, author: user)
end
let(:table) { :test_table }
let(:column) { :id }
let(:tmp_column) { "#{column}_convert_to_bigint" }
context 'in a transaction' do
it 'raises RuntimeError' do
allow(model).to receive(:transaction_open?).and_return(true)
expect { model.initialize_conversion_of_integer_to_bigint(:events, :id) }
.to raise_error(RuntimeError)
before do
model.create_table table, id: false do |t|
t.integer :id, primary_key: true
t.integer :non_nullable_column, null: false
t.integer :nullable_column
t.timestamps
end
end
context 'outside a transaction' do
context 'when the target table does not exist' do
it 'raises an error' do
expect { model.initialize_conversion_of_integer_to_bigint(:this_table_is_not_real, column) }
.to raise_error('Table this_table_is_not_real does not exist')
end
end
context 'when the primary key does not exist' do
it 'raises an error' do
expect { model.initialize_conversion_of_integer_to_bigint(table, column, primary_key: :foobar) }
.to raise_error("Column foobar does not exist on #{table}")
end
end
context 'when the column to convert does not exist' do
let(:column) { :foobar }
it 'raises an error' do
expect { model.initialize_conversion_of_integer_to_bigint(table, column) }
.to raise_error("Column #{column} does not exist on #{table}")
end
end
context 'when the column to convert is the primary key' do
it 'creates a not-null bigint column and installs triggers' do
expect(model).to receive(:add_column).with(table, tmp_column, :bigint, default: 0, null: false)
expect(model).to receive(:install_rename_triggers).with(table, column, tmp_column)
model.initialize_conversion_of_integer_to_bigint(table, column)
end
end
context 'when the column to convert is not the primary key, but non-nullable' do
let(:column) { :non_nullable_column }
it 'creates a not-null bigint column and installs triggers' do
expect(model).to receive(:add_column).with(table, tmp_column, :bigint, default: 0, null: false)
expect(model).to receive(:install_rename_triggers).with(table, column, tmp_column)
model.initialize_conversion_of_integer_to_bigint(table, column)
end
end
context 'when the column to convert is not the primary key, but nullable' do
let(:column) { :nullable_column }
it 'creates a nullable bigint column and installs triggers' do
expect(model).to receive(:add_column).with(table, tmp_column, :bigint, default: nil)
expect(model).to receive(:install_rename_triggers).with(table, column, tmp_column)
model.initialize_conversion_of_integer_to_bigint(table, column)
end
end
end
describe '#backfill_conversion_of_integer_to_bigint' do
let(:table) { :_test_backfill_table }
let(:column) { :id }
let(:tmp_column) { "#{column}_convert_to_bigint" }
before do
model.create_table table, id: false do |t|
t.integer :id, primary_key: true
t.text :message, null: false
t.timestamps
end
allow(model).to receive(:perform_background_migration_inline?).and_return(false)
end
context 'when the target table does not exist' do
it 'raises an error' do
expect { model.backfill_conversion_of_integer_to_bigint(:this_table_is_not_real, column) }
.to raise_error('Table this_table_is_not_real does not exist')
end
end
context 'when the primary key does not exist' do
it 'raises an error' do
expect { model.backfill_conversion_of_integer_to_bigint(table, column, primary_key: :foobar) }
.to raise_error("Column foobar does not exist on #{table}")
end
end
context 'when the column to convert does not exist' do
let(:column) { :foobar }
it 'raises an error' do
expect { model.backfill_conversion_of_integer_to_bigint(table, column) }
.to raise_error("Column #{column} does not exist on #{table}")
end
end
context 'when the temporary column does not exist' do
it 'raises an error' do
expect { model.backfill_conversion_of_integer_to_bigint(table, column) }
.to raise_error('The temporary column does not exist, initialize it with `initialize_conversion_of_integer_to_bigint`')
end
end
context 'when the conversion is properly initialized' do
let(:model_class) do
Class.new(ActiveRecord::Base) do
self.table_name = :_test_backfill_table
end
end
let(:migration_relation) { Gitlab::Database::BackgroundMigration::BatchedMigration.active }
before do
allow(model).to receive(:transaction_open?).and_return(false)
model.initialize_conversion_of_integer_to_bigint(table, column)
model_class.create!(message: 'hello')
model_class.create!(message: 'so long')
end
it 'creates a bigint column and starts backfilling it' do
expect(model)
.to receive(:add_column)
.with(
:events,
'id_convert_to_bigint',
:bigint,
default: 0,
null: false
)
it 'creates the batched migration tracking record' do
last_record = model_class.create!(message: 'goodbye')
expect(model)
.to receive(:install_rename_triggers)
.with(:events, :id, 'id_convert_to_bigint')
expect do
model.backfill_conversion_of_integer_to_bigint(table, column, batch_size: 2, sub_batch_size: 1)
end.to change { migration_relation.count }.by(1)
expect(model).to receive(:queue_background_migration_jobs_by_range_at_intervals).and_call_original
expect(BackgroundMigrationWorker)
.to receive(:perform_in)
.ordered
.with(
2.minutes,
'CopyColumnUsingBackgroundMigrationJob',
[event.id, event.id, :events, :id, 100, :id, 'id_convert_to_bigint']
)
expect(Gitlab::BackgroundMigration)
.to receive(:steal)
.ordered
.with('CopyColumnUsingBackgroundMigrationJob')
model.initialize_conversion_of_integer_to_bigint(
:events,
:id,
batch_size: 300,
sub_batch_size: 100
expect(migration_relation.last).to have_attributes(
job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
table_name: table.to_s,
column_name: column.to_s,
min_value: 1,
max_value: last_record.id,
interval: 120,
batch_size: 2,
sub_batch_size: 1,
job_arguments: [column.to_s, "#{column}_convert_to_bigint"]
)
end
context 'when the migration should be performed inline' do
it 'calls the runner to run the entire migration' do
expect(model).to receive(:perform_background_migration_inline?).and_return(true)
expect_next_instance_of(Gitlab::Database::BackgroundMigration::BatchedMigrationRunner) do |scheduler|
expect(scheduler).to receive(:run_entire_migration) do |batched_migration|
expect(batched_migration).to eq(migration_relation.last)
end
end
model.backfill_conversion_of_integer_to_bigint(table, column, batch_size: 2, sub_batch_size: 1)
end
end
end
end

View File

@ -55,17 +55,6 @@ RSpec.describe Ci::PipelineTriggerService do
expect(var.variable_type).to eq('file')
end
context 'when FF ci_trigger_payload_into_pipeline is disabled' do
before do
stub_feature_flags(ci_trigger_payload_into_pipeline: false)
end
it 'does not store the payload as a variable' do
expect { result }.not_to change { Ci::PipelineVariable.count }
expect(result[:pipeline].variables).to be_empty
end
end
context 'when commit message has [ci skip]' do
before do
allow_next(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' }

View File

@ -3,14 +3,12 @@
require 'spec_helper'
RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do
subject(:execute_service) do
travel_to(frozen_time) { described_class.new(track, interval).execute }
end
subject(:execute_service) { described_class.new(track, interval).execute }
let(:track) { :create }
let(:interval) { 1 }
let(:frozen_time) { Time.current }
let(:frozen_time) { Time.zone.parse('23 Mar 2021 10:14:40 UTC') }
let(:previous_action_completed_at) { frozen_time - 2.days }
let(:current_action_completed_at) { nil }
let(:experiment_enabled) { true }
@ -21,6 +19,7 @@ RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do
let_it_be(:user) { create(:user, email_opted_in: true) }
before do
travel_to(frozen_time)
create(:onboarding_progress, namespace: group, **actions_completed)
group.add_developer(user)
stub_experiment_for_subject(in_product_marketing_emails: experiment_enabled)

View File

@ -41,13 +41,6 @@ RSpec.shared_context 'conan recipe endpoints' do
let(:jwt) { build_jwt(personal_access_token) }
let(:headers) { build_token_auth_header(jwt.encoded) }
let(:conan_package_reference) { '123456789' }
let(:presenter) { double('::Packages::Conan::PackagePresenter') }
before do
allow(::Packages::Conan::PackagePresenter).to receive(:new)
.with(package, user, package.project, any_args)
.and_return(presenter)
end
end
RSpec.shared_context 'conan file download endpoints' do

View File

@ -1,6 +1,16 @@
# frozen_string_literal: true
RSpec.shared_examples 'assignee ID filter' do
context 'when optimize_issue_filter_assigned_to_self is disabled' do
before do
stub_feature_flags(optimize_issue_filter_assigned_to_self: false)
end
it 'returns issuables assigned to that user' do
expect(issuables).to contain_exactly(*expected_issuables)
end
end
it 'returns issuables assigned to that user' do
expect(issuables).to contain_exactly(*expected_issuables)
end
@ -13,6 +23,16 @@ RSpec.shared_examples 'assignee NOT ID filter' do
end
RSpec.shared_examples 'assignee username filter' do
context 'when optimize_issue_filter_assigned_to_self is disabled' do
before do
stub_feature_flags(optimize_issue_filter_assigned_to_self: false)
end
it 'returns issuables assigned to those users' do
expect(issuables).to contain_exactly(*expected_issuables)
end
end
it 'returns issuables assigned to those users' do
expect(issuables).to contain_exactly(*expected_issuables)
end

View File

@ -205,6 +205,14 @@ RSpec.shared_examples 'empty recipe for not found package' do
'aa/bb/%{project}/ccc' % { project: ::Packages::Conan::Metadatum.package_username_from(full_path: project.full_path) }
end
let(:presenter) { double('::Packages::Conan::PackagePresenter') }
before do
allow(::Packages::Conan::PackagePresenter).to receive(:new)
.with(package, user, package.project, any_args)
.and_return(presenter)
end
it 'returns not found' do
allow(::Packages::Conan::PackagePresenter).to receive(:new)
.with(
@ -248,8 +256,6 @@ RSpec.shared_examples 'recipe download_urls' do
'conanmanifest.txt' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/export/conanmanifest.txt"
}
allow(presenter).to receive(:recipe_urls) { expected_response }
subject
expect(json_response).to eq(expected_response)
@ -268,8 +274,6 @@ RSpec.shared_examples 'package download_urls' do
'conan_package.tgz' => "#{url_prefix}/packages/conan/v1/files/#{package.conan_recipe_path}/0/package/123456789/0/conan_package.tgz"
}
allow(presenter).to receive(:package_urls) { expected_response }
subject
expect(json_response).to eq(expected_response)
@ -309,12 +313,13 @@ RSpec.shared_examples 'recipe snapshot endpoint' do
context 'with existing package' do
it 'returns a hash of files with their md5 hashes' do
expected_response = {
'conanfile.py' => 'md5hash1',
'conanmanifest.txt' => 'md5hash2'
}
conan_file_file = package.package_files.find_by(file_name: 'conanfile.py')
conan_manifest_file = package.package_files.find_by(file_name: 'conanmanifest.txt')
allow(presenter).to receive(:recipe_snapshot) { expected_response }
expected_response = {
'conanfile.py' => conan_file_file.file_md5,
'conanmanifest.txt' => conan_manifest_file.file_md5
}
subject
@ -333,13 +338,11 @@ RSpec.shared_examples 'package snapshot endpoint' do
context 'with existing package' do
it 'returns a hash of md5 values for the files' do
expected_response = {
'conaninfo.txt' => "md5hash1",
'conanmanifest.txt' => "md5hash2",
'conan_package.tgz' => "md5hash3"
'conaninfo.txt' => "12345abcde",
'conanmanifest.txt' => "12345abcde",
'conan_package.tgz' => "12345abcde"
}
allow(presenter).to receive(:package_snapshot) { expected_response }
subject
expect(json_response).to eq(expected_response)

View File

@ -161,23 +161,42 @@ RSpec.describe Tooling::Danger::Changelog do
describe '#modified_text' do
subject { changelog.modified_text }
context "when title is not changed from sanitization", :aggregate_failures do
let(:mr_title) { 'Fake Title' }
context 'when in CI context' do
shared_examples 'changelog modified text' do |key|
specify do
expect(subject).to include('CHANGELOG.md was edited')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
end
end
specify do
expect(subject).to include('CHANGELOG.md was edited')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
before do
allow(fake_helper).to receive(:ci?).and_return(true)
end
context "when title is not changed from sanitization", :aggregate_failures do
let(:mr_title) { 'Fake Title' }
it_behaves_like 'changelog modified text'
end
context "when title needs sanitization", :aggregate_failures do
let(:mr_title) { 'DRAFT: Fake Title' }
it_behaves_like 'changelog modified text'
end
end
context "when title needs sanitization", :aggregate_failures do
let(:mr_title) { 'DRAFT: Fake Title' }
context 'when in local context' do
let(:mr_title) { 'Fake Title' }
before do
allow(fake_helper).to receive(:ci?).and_return(false)
end
specify do
expect(subject).to include('CHANGELOG.md was edited')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
expect(subject).not_to include('bin/changelog')
end
end
end
@ -187,56 +206,116 @@ RSpec.describe Tooling::Danger::Changelog do
subject { changelog.required_texts }
shared_examples 'changelog required text' do |key|
specify do
expect(subject).to have_key(key)
expect(subject[key]).to include('CHANGELOG missing')
expect(subject[key]).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject[key]).not_to include('--ee')
context 'when in CI context' do
before do
allow(fake_helper).to receive(:ci?).and_return(true)
end
shared_examples 'changelog required text' do |key|
specify do
expect(subject).to have_key(key)
expect(subject[key]).to include('CHANGELOG missing')
expect(subject[key]).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject[key]).not_to include('--ee')
end
end
context 'with a new migration file' do
let(:changes) { changes_class.new([change_class.new('foo', :added, :migration)]) }
context "when title is not changed from sanitization", :aggregate_failures do
it_behaves_like 'changelog required text', :db_changes
end
context "when title needs sanitization", :aggregate_failures do
let(:mr_title) { 'DRAFT: Fake Title' }
it_behaves_like 'changelog required text', :db_changes
end
end
context 'with a removed feature flag file' do
let(:changes) { changes_class.new([change_class.new('foo', :deleted, :feature_flag)]) }
it_behaves_like 'changelog required text', :feature_flag_removed
end
end
context 'with a new migration file' do
let(:changes) { changes_class.new([change_class.new('foo', :added, :migration)]) }
context "when title is not changed from sanitization", :aggregate_failures do
it_behaves_like 'changelog required text', :db_changes
context 'when in local context' do
before do
allow(fake_helper).to receive(:ci?).and_return(false)
end
context "when title needs sanitization", :aggregate_failures do
let(:mr_title) { 'DRAFT: Fake Title' }
it_behaves_like 'changelog required text', :db_changes
shared_examples 'changelog required text' do |key|
specify do
expect(subject).to have_key(key)
expect(subject[key]).to include('CHANGELOG missing')
expect(subject[key]).not_to include('bin/changelog')
expect(subject[key]).not_to include('--ee')
end
end
end
context 'with a removed feature flag file' do
let(:changes) { changes_class.new([change_class.new('foo', :deleted, :feature_flag)]) }
context 'with a new migration file' do
let(:changes) { changes_class.new([change_class.new('foo', :added, :migration)]) }
it_behaves_like 'changelog required text', :feature_flag_removed
context "when title is not changed from sanitization", :aggregate_failures do
it_behaves_like 'changelog required text', :db_changes
end
context "when title needs sanitization", :aggregate_failures do
let(:mr_title) { 'DRAFT: Fake Title' }
it_behaves_like 'changelog required text', :db_changes
end
end
context 'with a removed feature flag file' do
let(:changes) { changes_class.new([change_class.new('foo', :deleted, :feature_flag)]) }
it_behaves_like 'changelog required text', :feature_flag_removed
end
end
end
describe '#optional_text' do
subject { changelog.optional_text }
context "when title is not changed from sanitization", :aggregate_failures do
let(:mr_title) { 'Fake Title' }
context 'when in CI context' do
shared_examples 'changelog optional text' do |key|
specify do
expect(subject).to include('CHANGELOG missing')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
end
end
specify do
expect(subject).to include('CHANGELOG missing')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
before do
allow(fake_helper).to receive(:ci?).and_return(true)
end
context "when title is not changed from sanitization", :aggregate_failures do
let(:mr_title) { 'Fake Title' }
it_behaves_like 'changelog optional text'
end
context "when title needs sanitization", :aggregate_failures do
let(:mr_title) { 'DRAFT: Fake Title' }
it_behaves_like 'changelog optional text'
end
end
context "when title needs sanitization", :aggregate_failures do
let(:mr_title) { 'DRAFT: Fake Title' }
context 'when in local context' do
let(:mr_title) { 'Fake Title' }
before do
allow(fake_helper).to receive(:ci?).and_return(false)
end
specify do
expect(subject).to include('CHANGELOG missing')
expect(subject).to include('bin/changelog -m 1234 "Fake Title"')
expect(subject).to include('bin/changelog --ee -m 1234 "Fake Title"')
expect(subject).not_to include('bin/changelog')
end
end
end

View File

@ -203,7 +203,7 @@ RSpec.describe Tooling::Danger::ProjectHelper do
describe '.local_warning_message' do
it 'returns an informational message with rules that can run' do
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changes_size, commit_messages, database, documentation, duplicate_yarn_dependencies, eslint, karma, pajamas, pipeline, prettier, product_intelligence, utility_css')
expect(described_class.local_warning_message).to eq('==> Only the following Danger rules can be run locally: changelog, changes_size, commit_messages, database, documentation, duplicate_yarn_dependencies, eslint, karma, pajamas, pipeline, prettier, product_intelligence, utility_css')
end
end

View File

@ -18,29 +18,35 @@ module Tooling
CHANGELOG_MODIFIED_URL_TEXT = "**CHANGELOG.md was edited.** Please remove the additions and create a CHANGELOG entry.\n\n"
CHANGELOG_MISSING_URL_TEXT = "**[CHANGELOG missing](https://docs.gitlab.com/ee/development/changelog.html)**:\n\n"
OPTIONAL_CHANGELOG_MESSAGE = <<~MSG
If you want to create a changelog entry for GitLab FOSS, run the following:
OPTIONAL_CHANGELOG_MESSAGE = {
local: "If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.",
ci: <<~MSG
If you want to create a changelog entry for GitLab FOSS, run the following:
#{CREATE_CHANGELOG_COMMAND}
#{CREATE_CHANGELOG_COMMAND}
If you want to create a changelog entry for GitLab EE, run the following instead:
If you want to create a changelog entry for GitLab EE, run the following instead:
#{CREATE_EE_CHANGELOG_COMMAND}
#{CREATE_EE_CHANGELOG_COMMAND}
If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.
MSG
If this merge request [doesn't need a CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry), feel free to ignore this message.
MSG
}.freeze
REQUIRED_CHANGELOG_REASONS = {
db_changes: 'introduces a database migration',
feature_flag_removed: 'removes a feature flag'
}.freeze
REQUIRED_CHANGELOG_MESSAGE = <<~MSG
To create a changelog entry, run the following:
REQUIRED_CHANGELOG_MESSAGE = {
local: "This merge request requires a changelog entry because it [%<reason>s](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry).",
ci: <<~MSG
To create a changelog entry, run the following:
#{CREATE_CHANGELOG_COMMAND}
#{CREATE_CHANGELOG_COMMAND}
This merge request requires a changelog entry because it [%<reason>s](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry).
MSG
This merge request requires a changelog entry because it [%<reason>s](https://docs.gitlab.com/ee/development/changelog.html#what-warrants-a-changelog-entry).
MSG
}.freeze
def required_reasons
[].tap do |reasons|
@ -67,20 +73,20 @@ module Tooling
def modified_text
CHANGELOG_MODIFIED_URL_TEXT +
format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: helper.mr_iid, mr_title: sanitized_mr_title)
(helper.ci? ? format(OPTIONAL_CHANGELOG_MESSAGE[:ci], mr_iid: helper.mr_iid, mr_title: sanitized_mr_title) : OPTIONAL_CHANGELOG_MESSAGE[:local])
end
def required_texts
required_reasons.each_with_object({}) do |required_reason, memo|
memo[required_reason] =
CHANGELOG_MISSING_URL_TEXT +
format(REQUIRED_CHANGELOG_MESSAGE, reason: REQUIRED_CHANGELOG_REASONS.fetch(required_reason), mr_iid: helper.mr_iid, mr_title: sanitized_mr_title)
(helper.ci? ? format(REQUIRED_CHANGELOG_MESSAGE[:ci], reason: REQUIRED_CHANGELOG_REASONS.fetch(required_reason), mr_iid: helper.mr_iid, mr_title: sanitized_mr_title) : REQUIRED_CHANGELOG_MESSAGE[:local])
end
end
def optional_text
CHANGELOG_MISSING_URL_TEXT +
format(OPTIONAL_CHANGELOG_MESSAGE, mr_iid: helper.mr_iid, mr_title: sanitized_mr_title)
(helper.ci? ? format(OPTIONAL_CHANGELOG_MESSAGE[:ci], mr_iid: helper.mr_iid, mr_title: sanitized_mr_title) : OPTIONAL_CHANGELOG_MESSAGE[:local])
end
private

View File

@ -4,6 +4,7 @@ module Tooling
module Danger
module ProjectHelper
LOCAL_RULES ||= %w[
changelog
changes_size
commit_messages
database
@ -20,7 +21,6 @@ module Tooling
CI_ONLY_RULES ||= %w[
ce_ee_vue_templates
changelog
ci_templates
metadata
feature_flag

View File

@ -33,7 +33,10 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
(function (root, factory) {
if (typeof module !== 'undefined' && module.exports) {
factory(root, root.jasmine, require('jquery'));
// The line below is patched from jquery => jquery/dist/jquery
// in order to load a jQuery with ajax, so that this testing library
// doesn't break
factory(root, root.jasmine, require('jquery/dist/jquery'));
} else {
factory(root, root.jasmine, root.jQuery);
}