Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-15 06:10:17 +00:00
parent 03c5d7f2c1
commit d35de87f96
60 changed files with 1043 additions and 285 deletions

View File

@ -354,7 +354,11 @@ export default {
v-if="!diffFile.submodule && addMergeRequestButtons"
class="file-actions d-flex align-items-center gl-ml-auto gl-align-self-start"
>
<diff-stats :added-lines="diffFile.added_lines" :removed-lines="diffFile.removed_lines" />
<diff-stats
:diff-file="diffFile"
:added-lines="diffFile.added_lines"
:removed-lines="diffFile.removed_lines"
/>
<gl-form-checkbox
v-if="isReviewable && showLocalFileReviews"
v-gl-tooltip.hover

View File

@ -2,10 +2,16 @@
import { GlIcon } from '@gitlab/ui';
import { isNumber } from 'lodash';
import { n__ } from '~/locale';
import { isNotDiffable, stats } from '../utils/diff_file';
export default {
components: { GlIcon },
props: {
diffFile: {
type: Object,
required: false,
default: () => null,
},
addedLines: {
type: Number,
required: true,
@ -33,6 +39,12 @@ export default {
hasDiffFiles() {
return isNumber(this.diffFilesLength) && this.diffFilesLength >= 0;
},
notDiffable() {
return isNotDiffable(this.diffFile);
},
fileStats() {
return stats(this.diffFile);
},
},
};
</script>
@ -45,23 +57,28 @@ export default {
'd-none d-sm-inline-flex': !isCompareVersionsHeader,
}"
>
<div v-if="hasDiffFiles" class="diff-stats-group">
<gl-icon name="doc-code" class="diff-stats-icon text-secondary" />
<span class="text-secondary bold">{{ diffFilesCountText }} {{ filesText }}</span>
<div v-if="notDiffable" :class="fileStats.classes">
{{ fileStats.text }}
</div>
<div
class="diff-stats-group cgreen d-flex align-items-center"
:class="{ bold: isCompareVersionsHeader }"
>
<span>+</span>
<span class="js-file-addition-line">{{ addedLines }}</span>
</div>
<div
class="diff-stats-group cred d-flex align-items-center"
:class="{ bold: isCompareVersionsHeader }"
>
<span>-</span>
<span class="js-file-deletion-line">{{ removedLines }}</span>
<div v-else class="diff-stats-contents">
<div v-if="hasDiffFiles" class="diff-stats-group">
<gl-icon name="doc-code" class="diff-stats-icon text-secondary" />
<span class="text-secondary bold">{{ diffFilesCountText }} {{ filesText }}</span>
</div>
<div
class="diff-stats-group cgreen d-flex align-items-center"
:class="{ bold: isCompareVersionsHeader }"
>
<span>+</span>
<span class="js-file-addition-line">{{ addedLines }}</span>
</div>
<div
class="diff-stats-group cred d-flex align-items-center"
:class="{ bold: isCompareVersionsHeader }"
>
<span>-</span>
<span class="js-file-deletion-line">{{ removedLines }}</span>
</div>
</div>
</div>
</template>

View File

@ -1,3 +1,5 @@
import { diffViewerModes as viewerModes } from '~/ide/constants';
import { changeInPercent, numberToHumanSize } from '~/lib/utils/number_utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { uuids } from '~/lib/utils/uuids';
@ -46,6 +48,8 @@ function identifier(file) {
})[0];
}
export const isNotDiffable = (file) => file?.viewer?.name === viewerModes.not_diffable;
export function prepareRawDiffFile({ file, allFiles, meta = false }) {
const additionalProperties = {
brokenSymlink: fileSymlinkInformation(file, allFiles),
@ -84,3 +88,35 @@ export function isCollapsed(file) {
export function getShortShaFromFile(file) {
return file.content_sha ? truncateSha(String(file.content_sha)) : null;
}
export function stats(file) {
let valid = false;
let classes = '';
let sign = '';
let text = '';
let percent = 0;
let diff = 0;
if (file) {
percent = changeInPercent(file.old_size, file.new_size);
diff = file.new_size - file.old_size;
sign = diff >= 0 ? '+' : '';
text = `${sign}${numberToHumanSize(diff)} (${sign}${percent}%)`;
valid = true;
if (diff > 0) {
classes = 'cgreen';
} else if (diff < 0) {
classes = 'cred';
}
}
return {
changed: diff,
text,
percent,
classes,
sign,
valid,
};
}

View File

@ -32,9 +32,9 @@ export default {
:href="helpPath"
:title="__('About this feature')"
target="_blank"
class="d-flex-center pl-1"
class="d-flex-center"
>
<gl-icon name="question" />
<gl-icon name="question-o" class="gl-ml-3" />
</gl-link>
</div>
</template>

View File

@ -53,6 +53,6 @@ export default {
</span>
<gl-link v-else target="_blank" :href="helpPath" :aria-label="$options.i18n.securityReportsHelp">
<gl-icon name="question" />
<gl-icon name="question-o" />
</gl-link>
</template>

View File

@ -191,6 +191,7 @@ export default {
<security-summary :message="groupedSummaryText" />
<help-icon
class="gl-ml-3"
:help-path="securityReportsDocsPath"
:discover-project-security-path="discoverProjectSecurityPath"
/>
@ -219,6 +220,7 @@ export default {
{{ $options.i18n.scansHaveRun }}
<help-icon
class="gl-ml-3"
:help-path="securityReportsDocsPath"
:discover-project-security-path="discoverProjectSecurityPath"
/>

View File

@ -646,6 +646,10 @@ table.code {
align-items: center;
padding: 0 1rem;
.diff-stats-contents {
display: contents;
}
.diff-stats-group {
padding: 0 0.25rem;
}

View File

@ -183,7 +183,12 @@ class GroupsController < Groups::ApplicationController
def download_export
if @group.export_file_exists?
send_upload(@group.export_file, attachment: @group.export_file.filename)
if @group.export_archive_exists?
send_upload(@group.export_file, attachment: @group.export_file.filename)
else
redirect_to edit_group_path(@group),
alert: _('The file containing the export is not available yet; it may still be transferring. Please try again later.')
end
else
redirect_to edit_group_path(@group),
alert: _('Group export link has expired. Please generate a new export from your group settings.')

View File

@ -226,7 +226,14 @@ class ProjectsController < Projects::ApplicationController
def download_export
if @project.export_file_exists?
send_upload(@project.export_file, attachment: @project.export_file.filename)
if @project.export_archive_exists?
send_upload(@project.export_file, attachment: @project.export_file.filename)
else
redirect_to(
edit_project_path(@project, anchor: 'js-export-project'),
alert: _("The file containing the export is not available yet; it may still be transferring. Please try again later.")
)
end
else
redirect_to(
edit_project_path(@project, anchor: 'js-export-project'),

View File

@ -338,6 +338,8 @@ module ApplicationSettingsHelper
:version_check_enabled,
:web_ide_clientside_preview_enabled,
:diff_max_patch_bytes,
:diff_max_files,
:diff_max_lines,
:commit_email_hostname,
:protected_ci_variables,
:local_markdown_version,

View File

@ -273,6 +273,18 @@ class ApplicationSetting < ApplicationRecord
greater_than_or_equal_to: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
less_than_or_equal_to: Gitlab::Git::Diff::MAX_PATCH_BYTES_UPPER_BOUND }
validates :diff_max_files,
presence: true,
numericality: { only_integer: true,
greater_than_or_equal_to: Commit::DEFAULT_MAX_DIFF_FILES_SETTING,
less_than_or_equal_to: Commit::MAX_DIFF_FILES_SETTING_UPPER_BOUND }
validates :diff_max_lines,
presence: true,
numericality: { only_integer: true,
greater_than_or_equal_to: Commit::DEFAULT_MAX_DIFF_LINES_SETTING,
less_than_or_equal_to: Commit::MAX_DIFF_LINES_SETTING_UPPER_BOUND }
validates :user_default_internal_regex, js_regex: true, allow_nil: true
validates :personal_access_token_prefix,

View File

@ -60,6 +60,8 @@ module ApplicationSettingImplementation
default_projects_limit: Settings.gitlab['default_projects_limit'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES,
diff_max_files: Commit::DEFAULT_MAX_DIFF_FILES_SETTING,
diff_max_lines: Commit::DEFAULT_MAX_DIFF_LINES_SETTING,
disable_feed_token: false,
disabled_oauth_sign_in_sources: [],
dns_rebinding_protection_enabled: true,

View File

@ -33,6 +33,12 @@ class Commit
# Used by GFM to match and present link extensions on node texts and hrefs.
LINK_EXTENSION_PATTERN = /(patch)/.freeze
DEFAULT_MAX_DIFF_LINES_SETTING = 50_000
DEFAULT_MAX_DIFF_FILES_SETTING = 1_000
MAX_DIFF_LINES_SETTING_UPPER_BOUND = 100_000
MAX_DIFF_FILES_SETTING_UPPER_BOUND = 3_000
DIFF_SAFE_LIMIT_FACTOR = 10
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :full_title, pipeline: :single_line, limit: 1.kilobyte
cache_markdown_field :description, pipeline: :commit_description, limit: 1.megabyte
@ -78,20 +84,24 @@ class Commit
end
def diff_safe_lines(project: nil)
Gitlab::Git::DiffCollection.default_limits(project: project)[:max_lines]
diff_safe_max_lines(project: project)
end
def diff_hard_limit_files(project: nil)
def diff_max_files(project: nil)
if Feature.enabled?(:increased_diff_limits, project)
3000
elsif Feature.enabled?(:configurable_diff_limits, project)
Gitlab::CurrentSettings.diff_max_files
else
1000
end
end
def diff_hard_limit_lines(project: nil)
def diff_max_lines(project: nil)
if Feature.enabled?(:increased_diff_limits, project)
100000
elsif Feature.enabled?(:configurable_diff_limits, project)
Gitlab::CurrentSettings.diff_max_lines
else
50000
end
@ -99,11 +109,19 @@ class Commit
def max_diff_options(project: nil)
{
max_files: diff_hard_limit_files(project: project),
max_lines: diff_hard_limit_lines(project: project)
max_files: diff_max_files(project: project),
max_lines: diff_max_lines(project: project)
}
end
def diff_safe_max_files(project: nil)
diff_max_files(project: project) / DIFF_SAFE_LIMIT_FACTOR
end
def diff_safe_max_lines(project: nil)
diff_max_lines(project: project) / DIFF_SAFE_LIMIT_FACTOR
end
def from_hash(hash, container)
raw_commit = Gitlab::Git::Commit.new(container.repository.raw, hash)
new(raw_commit, container)

View File

@ -647,13 +647,17 @@ class Group < Namespace
end
def export_file_exists?
export_file&.file
import_export_upload&.export_file_exists?
end
def export_file
import_export_upload&.export_file
end
def export_archive_exists?
import_export_upload&.export_archive_exists?
end
def adjourned_deletion?
false
end

View File

@ -11,10 +11,42 @@ class ImportExportUpload < ApplicationRecord
mount_uploader :import_file, ImportExportUploader
mount_uploader :export_file, ImportExportUploader
# This causes CarrierWave v1 and v3 (but not v2) to upload the file to
# object storage *after* the database entry has been committed to the
# database. This avoids idling in a transaction.
if Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_STORE_EXPORT_FILE_AFTER_COMMIT', true))
skip_callback :save, :after, :store_export_file!
set_callback :commit, :after, :store_export_file!
end
scope :updated_before, ->(date) { where('updated_at < ?', date) }
scope :with_export_file, -> { where.not(export_file: nil) }
def retrieve_upload(_identifier, paths)
Upload.find_by(model: self, path: paths)
end
def export_file_exists?
!!carrierwave_export_file
end
# This checks if the export archive is actually stored on disk. It
# requires a HEAD request if object storage is used.
def export_archive_exists?
!!carrierwave_export_file&.exists?
# Handle any HTTP unexpected error
# https://github.com/excon/excon/blob/bbb5bd791d0bb2251593b80e3bce98dbec6e8f24/lib/excon/error.rb#L129-L169
rescue Excon::Error => e
# The HEAD request will fail with a 403 Forbidden if the file does not
# exist, and the user does not have permission to list the object
# storage bucket.
Gitlab::ErrorTracking.track_exception(e)
false
end
private
def carrierwave_export_file
export_file&.file
end
end

View File

@ -1987,7 +1987,11 @@ class Project < ApplicationRecord
end
def export_file_exists?
export_file&.file
import_export_upload&.export_file_exists?
end
def export_archive_exists?
import_export_upload&.export_archive_exists?
end
def export_file

View File

@ -3,11 +3,30 @@
%fieldset
.form-group
= f.label :diff_max_patch_bytes, _('Maximum diff patch size in bytes'), class: 'label-light'
= f.label :diff_max_patch_bytes, _('Maximum diff patch size (Bytes)'), class: 'label-light'
= f.number_field :diff_max_patch_bytes, class: 'form-control gl-form-input'
%span.form-text.text-muted
= _("Collapse diffs larger than this size, and show a 'too large' message instead.")
= _("Diff files surpassing this limit will be presented as 'too large' and won't be expandable.")
= link_to sprite_icon('question-o'),
help_page_path('user/admin_area/diff_limits')
help_page_path('user/admin_area/diff_limits',
anchor: 'diff-limits-administration')
= f.label :diff_max_files, _('Maximum files in a diff'), class: 'label-light'
= f.number_field :diff_max_files, class: 'form-control gl-form-input'
%span.form-text.text-muted
= _("Diff files surpassing this limit will be presented as 'too large' and won't be expandable.")
= link_to sprite_icon('question-o'),
help_page_path('user/admin_area/diff_limits',
anchor: 'diff-limits-administration')
= f.label :diff_max_lines, _('Maximum lines in a diff'), class: 'label-light'
= f.number_field :diff_max_lines, class: 'form-control gl-form-input'
%span.form-text.text-muted
= _("Diff files surpassing this limit will be presented as 'too large' and won't be expandable.")
= link_to sprite_icon('question-o'),
help_page_path('user/admin_area/diff_limits',
anchor: 'diff-limits-administration')
= f.submit _('Save changes'), class: 'gl-button btn btn-confirm'

View File

@ -2,7 +2,9 @@
- settings_link_start = "<a href=\"#{integrations_admin_application_settings_path}\">".html_safe
.gl-alert.gl-alert-danger.gl-mt-5{ role: 'alert' }
= sprite_icon('error', css_class: 'gl-alert-icon gl-alert-icon-no-title')
%h4.gl-alert-title= s_('AdminSettings|Service templates are deprecated and will be removed in GitLab 14.0.')
.gl-alert-body
= html_escape_once(s_("AdminSettings|You can't add new templates. To migrate or remove a Service template, create a new integration at %{settings_link_start}Settings &gt; Integrations%{link_end}. Learn more about %{doc_link_start}Project integration management%{link_end}.")).html_safe % { settings_link_start: settings_link_start, doc_link_start: doc_link_start, link_end: '</a>'.html_safe }
.gl-alert-container
= sprite_icon('error', css_class: 'gl-alert-icon gl-alert-icon-no-title')
.gl-alert-content
%h4.gl-alert-title= s_('AdminSettings|Service templates are deprecated and will be removed in GitLab 14.0.')
.gl-alert-body
= html_escape_once(s_("AdminSettings|You can't add new templates. To migrate or remove a Service template, create a new integration at %{settings_link_start}Settings &gt; Integrations%{link_end}. Learn more about %{doc_link_start}Project integration management%{link_end}.")).html_safe % { settings_link_start: settings_link_start, doc_link_start: doc_link_start, link_end: '</a>'.html_safe }

View File

@ -18,10 +18,10 @@
- experiment(:new_repo, user: current_user) do |e|
- e.use do
= nav_link(path: 'projects/new#blank_project', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do
= link_to new_project_path(anchor: 'blank_project'), data: { track_label: "projects_dropdown_blank_project", track_event: "click_link", track_experiment: "new_repo" } do
= link_to new_project_path(anchor: 'blank_project'), data: { track_label: "projects_dropdown_blank_project", track_event: "click_link", track_experiment: "new_repo", qa_selector: "create_project_link" } do
= _('Create blank project')
= nav_link(path: 'projects/new#import_project') do
= link_to new_project_path(anchor: 'import_project'), data: { track_label: "projects_dropdown_import_project", track_event: "click_link", track_experiment: "new_repo" } do
= link_to new_project_path(anchor: 'import_project'), data: { track_label: "projects_dropdown_import_project", track_event: "click_link", track_experiment: "new_repo", qa_selector: "import_project_link" } do
= _('Import project')
- e.try do
= nav_link(path: 'projects/new#blank_project', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do

View File

@ -0,0 +1,8 @@
---
name: configurable_diff_limits
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56722
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332194
milestone: '14.0'
type: development
group: group::code review
default_enabled: false

View File

@ -2,15 +2,21 @@
require 'digest/md5'
MESSAGE = <<MARKDOWN
REVIEW_ROULETTE_SECTION = <<MARKDOWN
## Reviewer roulette
Changes that require review have been detected! A merge request is normally
reviewed by both a reviewer and a maintainer in its primary category (e.g.
~frontend or ~backend), and by a maintainer in all other categories.
MARKDOWN
CATEGORY_TABLE_HEADER = <<MARKDOWN
CATEGORY_TABLE = <<MARKDOWN
Changes that require review have been detected!
Please refer to the table below for assigning reviewers and maintainers suggested by Danger in the specified category:
| Category | Reviewer | Maintainer |
| -------- | -------- | ---------- |
MARKDOWN
POST_TABLE_MESSAGE = <<MARKDOWN
To spread load more evenly across eligible reviewers, Danger has picked a candidate for each
review slot, based on their timezone. Feel free to
@ -26,12 +32,15 @@ Please consider assigning a reviewer or maintainer who is a
Once you've decided who will review this merge request, assign them as a reviewer!
Danger does not automatically notify them for you.
MARKDOWN
| Category | Reviewer | Maintainer |
| -------- | -------- | ---------- |
NO_SUGGESTIONS = <<MARKDOWN
There are no reviewer and maintainer suggestions for the changes in this MR.
MARKDOWN
UNKNOWN_FILES_MESSAGE = <<MARKDOWN
### Uncategorised files
These files couldn't be categorised, so Danger was unable to suggest a reviewer.
Please consider creating a merge request to
@ -99,8 +108,14 @@ if changes.any?
markdown_row_for_spins(spin.category, [spin])
end
markdown(MESSAGE)
markdown(CATEGORY_TABLE_HEADER + rows.join("\n")) unless rows.empty?
markdown(REVIEW_ROULETTE_SECTION)
if rows.empty?
markdown(NO_SUGGESTIONS)
else
markdown(CATEGORY_TABLE + rows.join("\n"))
markdown(POST_TABLE_MESSAGE)
end
unknown = changes.fetch(:unknown, [])
markdown(UNKNOWN_FILES_MESSAGE + helper.markdown_list(unknown)) unless unknown.empty?

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class AddDiffMaxLinesToApplicationSettings < ActiveRecord::Migration[6.0]
def change
add_column(:application_settings,
:diff_max_lines,
:integer,
default: 50000,
null: false)
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
class AddDiffMaxFilesToApplicationSettings < ActiveRecord::Migration[6.0]
def change
add_column(:application_settings,
:diff_max_files,
:integer,
default: 1000,
null: false)
end
end

View File

@ -0,0 +1 @@
aaf5936c945451fa98df7c21ab34c9aa7190dcf301f536c259e5b1fe54407f36

View File

@ -0,0 +1 @@
ac4522ee51d4a4cda317b680c16be3d9ef3e1619bba80c26aefe8d5dc70f013c

View File

@ -9517,6 +9517,8 @@ CREATE TABLE application_settings (
elasticsearch_username text,
encrypted_elasticsearch_password bytea,
encrypted_elasticsearch_password_iv bytea,
diff_max_lines integer DEFAULT 50000 NOT NULL,
diff_max_files integer DEFAULT 1000 NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),

View File

@ -251,7 +251,9 @@ listed in the descriptions of the relevant settings.
| `default_projects_limit` | integer | no | Project limit per user. Default is `100000`. |
| `default_snippet_visibility` | string | no | What visibility level new snippets receive. Can take `private`, `internal` and `public` as a parameter. Default is `private`. |
| `deletion_adjourned_period` | integer | no | **(PREMIUM SELF)** The number of days to wait before deleting a project or group that is marked for deletion. Value must be between 0 and 90.
| `diff_max_patch_bytes` | integer | no | Maximum diff patch size (Bytes). |
| `diff_max_patch_bytes` | integer | no | Maximum [diff patch size](../user/admin_area/diff_limits.md), in bytes. |
| `diff_max_files` | integer | no | Maximum [files in a diff](../user/admin_area/diff_limits.md). |
| `diff_max_lines` | integer | no | Maximum [lines in a diff](../user/admin_area/diff_limits.md). |
| `disable_feed_token` | boolean | no | Disable display of RSS/Atom and calendar feed tokens ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/231493) in GitLab 13.7) |
| `disabled_oauth_sign_in_sources` | array of strings | no | Disabled OAuth sign-in sources. |
| `dns_rebinding_protection_enabled` | boolean | no | Enforce DNS rebinding attack protection. |

View File

@ -12,17 +12,25 @@ You can set a maximum size for display of diff files (patches).
For details about diff files, [view changes between files](../project/merge_requests/changes.md).
Read more about the [built-in limits for merge requests and diffs](../../administration/instance_limits.md#merge-requests).
## Maximum diff patch size
## Configure diff limits
Diff files which exceed this value are presented as 'too large' and cannot
be expandable. Instead of an expandable view, a link to the blob view is
shown.
WARNING:
These settings are experimental. An increased maximum increases resource
consumption of your instance. Keep this in mind when adjusting the maximum.
Patches greater than 10% of this size are automatically collapsed, and a
link to expand the diff is presented.
This affects merge requests and branch comparison views.
To speed the loading time of merge request views and branch comparison views
on your instance, you can configure three instance-level maximum values for diffs:
To set the maximum diff patch size:
- **Maximum diff patch size**: The total size, in bytes, of the entire diff.
- **Maximum diff files**: The total number of files changed in a diff.
- **Maximum diff files**: The total number of files changed in a diff. The default value is 1000.
- **Maximum diff lines**: The total number of lines changed in a diff. The default value is 50,000.
When a diff reaches 10% of any of these values, the files are shown in a
collapsed view, with a link to expand the diff. Diffs that exceed any of the
set values are presented as **Too large** are cannot be expanded in the UI.
To configure these values:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. In the left sidebar, select **Settings > General**.
@ -30,10 +38,6 @@ To set the maximum diff patch size:
1. Enter a value for **Maximum diff patch size**, measured in bytes.
1. Select **Save changes**.
WARNING:
This setting is experimental. An increased maximum increases resource
consumption of your instance. Keep this in mind when adjusting the maximum.
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues

View File

@ -170,6 +170,8 @@ the site during a scan could lead to inaccurate results.
#### Include the DAST template
> This template was [updated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62597) to DAST_VERSION: 2 in GitLab 14.0.
If you want to manually add DAST to your application, the DAST job is defined
in a CI/CD template file. Updates to the template are provided with GitLab
upgrades, allowing you to benefit from any improvements and additions.

View File

@ -6,17 +6,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Epic Boards **(PREMIUM)**
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2864) in GitLab 13.10.
> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default.
> - It's disabled on GitLab.com.
> - It's not recommended for production use.
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](../../../administration/feature_flags.md).
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
The GitLab Epic Board is a software project management tool used to plan,
organize, and visualize a workflow for a feature or product release.
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5067) in GitLab 13.10.
> - [Deployed behind a feature flag](../../feature_flags.md), disabled by default.
> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/290039) in GitLab 14.0.
> - Enabled on GitLab.com.
> - Recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](../../../administration/feature_flags.md).
Epic boards build on the existing [epic tracking functionality](index.md) and
[labels](../../project/labels.md). Your epics appear as cards in vertical lists, organized by their assigned
@ -24,45 +19,156 @@ labels.
To view an epic board, in a group, select **Epics > Boards**.
![GitLab epic board - Premium](img/epic_board_v13_10.png)
![GitLab epic board - Premium](img/epic_board_v14_0.png)
## Create an epic board
Prerequisites:
- A minimum of [Reporter](../../permissions.md#group-members-permissions) access to a group in GitLab.
To create a new epic board:
1. Select the dropdown with the current board name in the upper left corner of the Epic Boards page.
1. Go to your group and select **Epics > Boards**.
1. In the upper left corner, select the dropdown with the current board name.
1. Select **Create new board**.
1. Enter the new board's name and select **Create**.
1. Enter the new board's title.
1. Optional. To hide the Open or Closed lists, clear the **Show the Open list** and
**Show the Closed list** checkboxes.
1. Optional. Set board scope:
1. Next to **Scope**, select **Expand**.
1. Next to **Labels**, select **Edit** and select the labels to use as board scope.
1. Select **Create board**.
## Limitations of epic boards
Now you can [add some lists](#create-a-new-list).
To change these options later, [edit the board](#edit-the-scope-of-an-epic-board).
As of GitLab 13.10, these limitations apply:
## Delete an epic board
- Epic Boards need to be enabled by an administrator.
- Epic Boards can be created but not deleted.
- Lists can be added to the board but not deleted.
- There is no sidebar on the board. To edit an epic, go to the epic's page.
- There is no drag and drop support yet. To move an epic between lists, edit epic labels on the epic's page.
- Epics cannot be re-ordered within the list.
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5079) in GitLab 14.0.
To learn more about the future iterations of this feature, visit
[epic 5067](https://gitlab.com/groups/gitlab-org/-/epics/5067).
Prerequisites:
## Enable or disable Epic Boards
- A minimum of [Reporter](../../permissions.md#group-members-permissions) access to a group in GitLab.
- A minimum of two boards present in a group.
Epic Boards are under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default**.
To delete the active epic board:
1. Select the dropdown with the current board name in the upper left corner of the Epic Boards page.
1. Select **Delete board**.
1. Select **Delete**.
## Actions you can take on an epic board
- [Create a new list](#create-a-new-list).
- [Remove an existing list](#remove-a-list).
- [Filter epics](#filter-epics).
- Create workflows, like when using [issue boards](../../project/issue_board.md#create-workflows).
- [Move epics and lists](#move-epics-and-lists).
- Change epic labels (by dragging an epic between lists).
- Close an epic (by dragging it to the **Closed** list).
- [Edit the scope of a board](#edit-the-scope-of-an-epic-board).
### Create a new list
Prerequisites:
- A minimum of [Reporter](../../permissions.md#group-members-permissions) access to a group in GitLab.
To create a new list:
1. Go to your group and select **Epics > Boards**.
1. In the upper-right corner, select **Create list**.
1. In the **New list** column expand the **Select a label** dropdown and select the label to use as
list scope.
1. Select **Add to board**.
### Remove a list
Removing a list doesn't have any effect on epics and labels, as it's just the
list view that's removed. You can always create it again later if you need.
Prerequisites:
- A minimum of [Reporter](../../permissions.md#group-members-permissions) access to a group in GitLab.
To remove a list from an epic board:
1. On the top of the list you want to remove, select the **List settings** icon (**{settings}**).
The list settings sidebar opens on the right.
1. Select **Remove list**. A confirmation dialog appears.
1. Select **OK**.
### Filter epics
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5079) in GitLab 14.0.
Use the filters on top of your epic board to show only
the results you want. It's similar to the filtering used in the epic list,
as the metadata from the epics and labels is re-used in the epic board.
You can filter by the following:
- Author
- Label
### Move epics and lists
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5079) in GitLab 14.0.
You can move epics and lists by dragging them.
Prerequisites:
- A minimum of [Reporter](../../permissions.md#group-members-permissions) access to a group in GitLab.
To move an epic, select the epic card and drag it to another position in its current list or
into another list. Learn about possible effects in [Dragging epics between lists](#dragging-epics-between-lists).
To move a list, select its top bar, and drag it horizontally.
You can't move the **Open** and **Closed** lists, but you can hide them when editing an epic board.
#### Dragging epics between lists
When you drag epics between lists, the result is different depending on the source list
and the target list.
| | To Open | To Closed | To label B list |
| --------------------- | -------------- | ---------- | ------------------------------ |
| **From Open** | - | Close epic | Add label B |
| **From Closed** | Reopen epic | - | Reopen epic and add label B |
| **From label A list** | Remove label A | Close epic | Remove label A and add label B |
### Edit the scope of an epic board
Prerequisites:
- A minimum of [Reporter](../../permissions.md#group-members-permissions) access to a group in GitLab.
To edit the scope of an epic board:
1. In the upper-right corner, select **Edit board**.
1. Optional:
- Edit the board's title.
- Show or hide the Open and Closed columns.
- Select other labels as the board's scope.
1. Select **Save changes**.
## Enable or disable epic boards
Epic boards are 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 enable it.
To enable it:
```ruby
Feature.enable(:epic_boards)
```
can disable it.
To disable it:
```ruby
Feature.disable(:epic_boards)
```
To enable it:
```ruby
Feature.enable(:epic_boards)
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -263,8 +263,9 @@ The following table lists group permissions available for each role:
| Browse group | ✓ | ✓ | ✓ | ✓ | ✓ |
| View group wiki pages **(PREMIUM)** | ✓ (6) | ✓ | ✓ | ✓ | ✓ |
| View Insights charts **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ |
| View group epic **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create/edit group epic **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
| View group epic **(PREMIUM)** | ✓ | ✓ | ✓ | ✓ | ✓ |
| Create/edit group epic **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
| Create/edit/delete epic boards **(PREMIUM)** | | ✓ | ✓ | ✓ | ✓ |
| Manage group labels | | ✓ | ✓ | ✓ | ✓ |
| See a container registry | | ✓ | ✓ | ✓ | ✓ |
| Pull [packages](packages/index.md) | | ✓ | ✓ | ✓ | ✓ |
@ -288,8 +289,8 @@ The following table lists group permissions available for each role:
| Create/Delete group deploy tokens | | | | | ✓ |
| Manage group members | | | | | ✓ |
| Delete group | | | | | ✓ |
| Delete group epic **(PREMIUM)** | | | | | ✓ |
| Edit SAML SSO Billing **(PREMIUM SAAS)** | ✓ | ✓ | ✓ | ✓ | ✓ (4) |
| Delete group epic **(PREMIUM)** | | | | | ✓ |
| Edit SAML SSO Billing **(PREMIUM SAAS)** | ✓ | ✓ | ✓ | ✓ | ✓ (4) |
| View group Audit Events | | | ✓ (7) | ✓ (7) | ✓ |
| Disable notification emails | | | | | ✓ |
| View Contribution analytics | ✓ | ✓ | ✓ | ✓ | ✓ |

View File

@ -34,11 +34,11 @@ boards in the same project.
Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/),
as shown in the following table:
| Tier | Number of project issue boards | Number of [group issue boards](#group-issue-boards) | [Configurable issue boards](#configurable-issue-boards) | [Assignee lists](#assignee-lists) |
|------------------|--------------------------------|------------------------------|---------------------------|----------------|
| Free | Multiple | 1 | No | No |
| Premium | Multiple | Multiple | Yes | Yes |
| Ultimate | Multiple | Multiple | Yes | Yes |
| Tier | Number of project issue boards | Number of [group issue boards](#group-issue-boards) | [Configurable issue boards](#configurable-issue-boards) | [Assignee lists](#assignee-lists) |
| -------- | ------------------------------ | --------------------------------------------------- | ------------------------------------------------------- | --------------------------------- |
| Free | Multiple | 1 | No | No |
| Premium | Multiple | Multiple | Yes | Yes |
| Ultimate | Multiple | Multiple | Yes | Yes |
To learn more, visit [GitLab Enterprise features for issue boards](#gitlab-enterprise-features-for-issue-boards) below.
@ -312,7 +312,7 @@ assignee list:
1. Search and select the user you want to add as an assignee.
Now that the assignee list is added, you can assign or unassign issues to that user
by [dragging issues](#drag-issues-between-lists) to and from an assignee list.
by [moving issues](#move-issues-and-lists) to and from an assignee list.
To remove an assignee list, just as with a label list, click the trash icon.
![Assignee lists](img/issue_board_assignee_lists_v13_6.png)
@ -328,7 +328,7 @@ milestone, giving you more freedom and visibility on the issue board. To add a m
1. Select the **Milestone** tab.
1. Search and click the milestone.
Like the assignee lists, you're able to [drag issues](#drag-issues-between-lists)
Like the assignee lists, you're able to [drag issues](#move-issues-and-lists)
to and from a milestone list to manipulate the milestone of the dragged issues.
As in other list types, click the trash icon to remove a list.
@ -355,7 +355,7 @@ iteration. To add an iteration list:
1. In the dropdown, select an iteration.
1. Select **Add to board**.
Like the milestone lists, you're able to [drag issues](#drag-issues-between-lists)
Like the milestone lists, you're able to [drag issues](#move-issues-and-lists)
to and from a iteration list to manipulate the iteration of the dragged issues.
![Iteration lists](img/issue_board_iteration_lists_v13_10.png)
@ -380,7 +380,20 @@ To group issues by epic in an issue board:
![Epics Swimlanes](img/epics_swimlanes_v13.6.png)
You can also [drag issues](#drag-issues-between-lists) to change their position and epic assignment:
To edit an issue without leaving this view, select the issue card (not its title), and a sidebar
appears on the right. There you can see and edit the issue's:
- Title
- Assignees
- Epic **PREMIUM**
- Milestone
- Time tracking value (view only)
- Due date
- Labels
- Weight
- Notifications setting
You can also [drag issues](#move-issues-and-lists) to change their position and epic assignment:
- To reorder an issue, drag it to the new position within a list.
- To assign an issue to another epic, drag it to the epic's horizontal lane.
@ -435,11 +448,11 @@ This feature is only supported when using the [GraphQL-based boards](#graphql-ba
- [Remove an issue from a list](#remove-an-issue-from-a-list).
- [Filter issues](#filter-issues) that appear across your issue board.
- [Create workflows](#create-workflows).
- [Drag issues between lists](#drag-issues-between-lists).
- [Move issues and lists](#move-issues-and-lists).
- [Multi-select issue cards](#multi-select-issue-cards).
- Drag and reorder the lists.
- Change issue labels (by dragging an issue between lists).
- Close an issue (by dragging it to the **Done** list).
- Close an issue (by dragging it to the **Closed** list).
If you're not able to do some of the things above, make sure you have the right
[permissions](#permissions).
@ -483,12 +496,12 @@ You can now choose it to create a list.
### Remove a list
Removing a list doesn't have any effect on issues and labels, as it's just the
list view that's removed. You can always restore it later if you need.
list view that's removed. You can always create it again later if you need.
To remove a list from an issue board:
1. Select the **List settings** icon (**{settings}**) on the top of the list you want to remove. The
list settings sidebar opens on the right.
1. On the top of the list you want to remove, select the **List settings** icon (**{settings}**).
The list settings sidebar opens on the right.
1. Select **Remove list**. A confirmation dialog appears.
1. Select **OK**.
@ -582,16 +595,33 @@ to another list, the label changes and a system note is recorded.
![issue board system notes](img/issue_board_system_notes_v13_6.png)
### Drag issues between lists
### Move issues and lists
When dragging issues between lists, different behavior occurs depending on the source list and the target list.
You can move issues and lists by dragging them.
| | To Open | To Closed | To label `B` list | To assignee `Bob` list |
| ------------------------------ | ------------------ | ------------ | ---------------------------- | ------------------------------------- |
| **From Open** | - | Issue closed | `B` added | `Bob` assigned |
| **From Closed** | Issue reopened | - | Issue reopened<br/>`B` added | Issue reopened<br/>`Bob` assigned |
| **From label `A` list** | `A` removed | Issue closed | `A` removed<br/>`B` added | `Bob` assigned |
| **From assignee `Alice` list** | `Alice` unassigned | Issue closed | `B` added | `Alice` unassigned<br/>`Bob` assigned |
Prerequisites:
- A minimum of [Reporter](../permissions.md#project-members-permissions) access to a project in GitLab.
To move an issue, select the issue card and drag it to another position in its current list or
into a different list. Learn about possible effects in [Dragging issues between lists](#dragging-issues-between-lists).
To move a list, select its top bar, and drag it horizontally.
You can't move the **Open** and **Closed** lists, but you can hide them when editing an issue board.
#### Dragging issues between lists
To move an issue to another list, select the issue card and drag it onto that list.
When you drag issues between lists, the result is different depending on the source list
and the target list.
| | To Open | To Closed | To label B list | To assignee Bob list |
| ---------------------------- | -------------- | ----------- | ------------------------------ | ----------------------------- |
| **From Open** | - | Close issue | Add label B | Assign Bob |
| **From Closed** | Reopen issue | - | Reopen issue and add label B | Reopen issue and assign Bob |
| **From label A list** | Remove label A | Close issue | Remove label A and add label B | Assign Bob |
| **From assignee Alice list** | Unassign Alice | Close issue | Add label B | Unassign Alice and assign Bob |
### Multi-select issue cards

View File

@ -23,7 +23,11 @@ module API
check_rate_limit! :group_download_export, [current_user, user_group]
if user_group.export_file_exists?
present_carrierwave_file!(user_group.export_file)
if user_group.export_archive_exists?
present_carrierwave_file!(user_group.export_file)
else
render_api_error!('The group export file is not available yet', 404)
end
else
render_api_error!('404 Not found or has expired', 404)
end

View File

@ -30,7 +30,11 @@ module API
check_rate_limit! :project_download_export, [current_user, user_project]
if user_project.export_file_exists?
present_carrierwave_file!(user_project.export_file)
if user_project.export_archive_exists?
present_carrierwave_file!(user_project.export_file)
else
render_api_error!('The project export file is not available yet', 404)
end
else
render_api_error!('404 Not found or has expired', 404)
end

View File

@ -10,7 +10,7 @@ stages:
- dast
variables:
DAST_VERSION: 1
DAST_VERSION: 2
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"

View File

@ -17,7 +17,7 @@
# List of available variables: https://docs.gitlab.com/ee/user/application_security/dast/#available-variables
variables:
DAST_VERSION: 1
DAST_VERSION: 2
# Setting this variable will affect all Security templates
# (SAST, Dependency Scanning, ...)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"

View File

@ -12,11 +12,7 @@ module Gitlab
delegate :max_files, :max_lines, :max_bytes, :safe_max_files, :safe_max_lines, :safe_max_bytes, to: :limits
def self.default_limits(project: nil)
if Feature.enabled?(:increased_diff_limits, project)
{ max_files: 300, max_lines: 10000 }
else
{ max_files: 100, max_lines: 5000 }
end
{ max_files: ::Commit.diff_safe_max_files(project: project), max_lines: ::Commit.diff_safe_max_lines(project: project) }
end
def self.limits(options = {})

View File

@ -7969,9 +7969,6 @@ msgstr ""
msgid "Collapse approvers"
msgstr ""
msgid "Collapse diffs larger than this size, and show a 'too large' message instead."
msgstr ""
msgid "Collapse issues"
msgstr ""
@ -11417,6 +11414,9 @@ msgstr ""
msgid "Didn't receive unlock instructions?"
msgstr ""
msgid "Diff files surpassing this limit will be presented as 'too large' and won't be expandable."
msgstr ""
msgid "Diff limits"
msgstr ""
@ -20252,7 +20252,7 @@ msgstr ""
msgid "Maximum delay (Minutes)"
msgstr ""
msgid "Maximum diff patch size in bytes"
msgid "Maximum diff patch size (Bytes)"
msgstr ""
msgid "Maximum duration of a session."
@ -20276,6 +20276,9 @@ msgstr ""
msgid "Maximum file size is 2MB. Please select a smaller file."
msgstr ""
msgid "Maximum files in a diff"
msgstr ""
msgid "Maximum import size (MB)"
msgstr ""
@ -20288,6 +20291,9 @@ msgstr ""
msgid "Maximum lifetime allowable for Personal Access Tokens is active, your expire date must be set before %{maximum_allowable_date}."
msgstr ""
msgid "Maximum lines in a diff"
msgstr ""
msgid "Maximum npm package file size in bytes"
msgstr ""
@ -32425,6 +32431,9 @@ msgstr ""
msgid "The errors we encountered were:"
msgstr ""
msgid "The file containing the export is not available yet; it may still be transferring. Please try again later."
msgstr ""
msgid "The file has been successfully created."
msgstr ""

View File

@ -55,13 +55,17 @@ module QA
end
def go_to_projects
go_to_menu_dropdown_option(:projects_dropdown)
within_element(:menu_subview_container) do
within_projects_menu do
click_element(:menu_item_link, title: 'Your projects')
end
end
def go_to_create_project
within_projects_menu do
click_element(:menu_item_link, title: 'Create new project')
end
end
def go_to_menu_dropdown_option(option_name)
within_top_menu do
click_element(:navbar_dropdown, title: 'Menu')
@ -84,11 +88,11 @@ module QA
def go_to_admin_area
click_admin_area
if has_text?('Enter Admin Mode', wait: 1.0)
Admin::NewSession.perform do |new_session|
new_session.set_password(Runtime::User.admin_password)
new_session.click_enter_admin_mode
end
return unless has_text?('Enter Admin Mode', wait: 1.0)
Admin::NewSession.perform do |new_session|
new_session.set_password(Runtime::User.admin_password)
new_session.click_enter_admin_mode
end
end
@ -166,19 +170,15 @@ module QA
private
def within_top_menu
within_element(:navbar) do
yield
end
def within_top_menu(&block)
within_element(:navbar, &block)
end
def within_user_menu
def within_user_menu(&block)
within_top_menu do
click_element :user_avatar
within_element(:user_menu) do
yield
end
within_element(:user_menu, &block)
end
end
@ -188,6 +188,12 @@ module QA
within_element(:menu_subview_container, &block)
end
def within_projects_menu(&block)
go_to_menu_dropdown_option(:projects_dropdown)
within_element(:menu_subview_container, &block)
end
def click_admin_area
go_to_menu_dropdown_option(:admin_area_link)
end

View File

@ -32,24 +32,20 @@ module QA
end
def import!(full_path, name)
unless already_imported(full_path)
choose_test_namespace(full_path)
set_path(full_path, name)
import_project(full_path)
wait_for_success
end
return if already_imported(full_path)
go_to_project(name)
choose_test_namespace(full_path)
set_path(full_path, name)
import_project(full_path)
wait_for_success
end
private
def within_repo_path(full_path)
def within_repo_path(full_path, &block)
project_import_row = find_element(:project_import_row, text: full_path)
within(project_import_row) do
yield
end
within(project_import_row, &block)
end
def choose_test_namespace(full_path)
@ -75,8 +71,13 @@ module QA
def wait_for_success
# TODO: set reload:false and remove skip_finished_loading_check_on_refresh when
# https://gitlab.com/gitlab-org/gitlab/-/issues/292861 is fixed
wait_until(max_duration: 60, sleep_interval: 5.0, reload: true, skip_finished_loading_check_on_refresh: true) do
page.has_no_content?('Importing 1 repository', wait: 3.0)
wait_until(
max_duration: 60,
sleep_interval: 5.0,
reload: true,
skip_finished_loading_check_on_refresh: true
) do
page.has_no_content?('Importing 1 repository')
end
end

View File

@ -10,10 +10,10 @@ module QA
include Visibility
attr_accessor :repository_storage # requires admin access
attr_writer :initialize_with_readme
attr_writer :auto_devops_enabled
attr_writer :github_personal_access_token
attr_writer :github_repository_path
attr_writer :initialize_with_readme,
:auto_devops_enabled,
:github_personal_access_token,
:github_repository_path
attribute :id
attribute :name
@ -40,15 +40,11 @@ module QA
end
attribute :repository_ssh_location do
Page::Project::Show.perform do |show|
show.repository_clone_ssh_location
end
Page::Project::Show.perform(&:repository_clone_ssh_location)
end
attribute :repository_http_location do
Page::Project::Show.perform do |show|
show.repository_clone_http_location
end
Page::Project::Show.perform(&:repository_clone_http_location)
end
def initialize
@ -104,7 +100,7 @@ module QA
def has_file?(file_path)
response = repository_tree
raise ResourceNotFoundError, "#{response[:message]}" if response.is_a?(Hash) && response.has_key?(:message)
raise ResourceNotFoundError, (response[:message]).to_s if response.is_a?(Hash) && response.has_key?(:message)
response.any? { |file| file[:path] == file_path }
end
@ -115,14 +111,14 @@ module QA
def has_branches?(branches)
branches.all? do |branch|
response = get(Runtime::API::Request.new(api_client, "#{api_repository_branches_path}/#{branch}").url)
response = get(request_url("#{api_repository_branches_path}/#{branch}"))
response.code == HTTP_STATUS_OK
end
end
def has_tags?(tags)
tags.all? do |tag|
response = get(Runtime::API::Request.new(api_client, "#{api_repository_tags_path}/#{tag}").url)
response = get(request_url("#{api_repository_tags_path}/#{tag}"))
response.code == HTTP_STATUS_OK
end
end
@ -183,6 +179,10 @@ module QA
"#{api_get_path}/pipeline_schedules"
end
def api_issues_path
"#{api_get_path}/issues"
end
def api_put_path
"/projects/#{id}"
end
@ -217,19 +217,28 @@ module QA
def change_repository_storage(new_storage)
put_body = { repository_storage: new_storage }
response = put Runtime::API::Request.new(api_client, api_put_path).url, put_body
response = put(request_url(api_put_path), put_body)
unless response.code == HTTP_STATUS_OK
raise ResourceUpdateFailedError, "Could not change repository storage to #{new_storage}. Request returned (#{response.code}): `#{response}`."
raise(
ResourceUpdateFailedError,
"Could not change repository storage to #{new_storage}. Request returned (#{response.code}): `#{response}`."
)
end
wait_until(sleep_interval: 1) { Runtime::API::RepositoryStorageMoves.has_status?(self, 'finished', new_storage) }
wait_until(sleep_interval: 1) do
Runtime::API::RepositoryStorageMoves.has_status?(self, 'finished', new_storage)
end
rescue Support::Repeater::RepeaterConditionExceededError
raise Runtime::API::RepositoryStorageMoves::RepositoryStorageMovesError, 'Timed out while waiting for the repository storage move to finish'
raise(
Runtime::API::RepositoryStorageMoves::RepositoryStorageMovesError,
'Timed out while waiting for the repository storage move to finish'
)
end
def commits
parse_body(get(Runtime::API::Request.new(api_client, api_commits_path).url))
response = get(request_url(api_commits_path))
parse_body(response)
end
def default_branch
@ -237,7 +246,7 @@ module QA
end
def import_status
response = get Runtime::API::Request.new(api_client, "/projects/#{id}/import").url
response = get(request_url("/projects/#{id}/import"))
unless response.code == HTTP_STATUS_OK
raise ResourceQueryError, "Could not get import status. Request returned (#{response.code}): `#{response}`."
@ -251,7 +260,8 @@ module QA
end
def merge_requests
parse_body(get(Runtime::API::Request.new(api_client, api_merge_requests_path).url))
response = get(request_url(api_merge_requests_path))
parse_body(response)
end
def merge_request_with_title(title)
@ -260,42 +270,52 @@ module QA
def runners(tag_list: nil)
response = if tag_list
get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}", per_page: '100').url
get(request_url("#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}", per_page: '100'))
else
get Runtime::API::Request.new(api_client, "#{api_runners_path}", per_page: '100').url
get(request_url(api_runners_path, per_page: '100'))
end
parse_body(response)
end
def registry_repositories
response = get Runtime::API::Request.new(api_client, "#{api_registry_repositories_path}").url
response = get(request_url(api_registry_repositories_path))
parse_body(response)
end
def packages
response = get Runtime::API::Request.new(api_client, "#{api_packages_path}").url
response = get(request_url(api_packages_path))
parse_body(response)
end
def repository_branches
parse_body(get(Runtime::API::Request.new(api_client, api_repository_branches_path).url))
response = get(request_url(api_repository_branches_path))
parse_body(response)
end
def repository_tags
parse_body(get(Runtime::API::Request.new(api_client, api_repository_tags_path).url))
response = get(request_url(api_repository_tags_path))
parse_body(response)
end
def repository_tree
parse_body(get(Runtime::API::Request.new(api_client, api_repository_tree_path).url))
response = get(request_url(api_repository_tree_path))
parse_body(response)
end
def pipelines
parse_body(get(Runtime::API::Request.new(api_client, api_pipelines_path).url))
response = get(request_url(api_pipelines_path))
parse_body(response)
end
def pipeline_schedules
parse_body(get(Runtime::API::Request.new(api_client, api_pipeline_schedules_path).url))
response = get(request_url(api_pipeline_schedules_path))
parse_body(response)
end
def issues
response = get(request_url(api_issues_path))
parse_body(response)
end
private
@ -307,6 +327,14 @@ module QA
Git::Location.new(api_resource[:http_url_to_repo])
api_resource
end
# Get api request url
#
# @param [String] path
# @return [String]
def request_url(path, **opts)
Runtime::API::Request.new(api_client, path, **opts).url
end
end
end
end

View File

@ -7,23 +7,19 @@ module QA
class ProjectImportedFromGithub < Resource::Project
def fabricate!
self.import = true
super
group.visit!
Page::Main::Menu.perform(&:go_to_create_project)
Page::Group::Show.perform(&:go_to_new_project)
go_to_import_page
Page::Project::New.perform(&:click_github_link)
Page::Project::New.perform do |project_page|
project_page.click_import_project
project_page.click_github_link
end
Page::Project::Import::Github.perform do |import_page|
import_page.add_personal_access_token(@github_personal_access_token)
import_page.import!(@github_repository_path, @name)
end
end
def go_to_import_page
Page::Project::New.perform(&:click_import_project)
end
end
end
end

View File

@ -3,25 +3,26 @@
module QA
RSpec.describe 'Manage', :github, :requires_admin do
describe 'Project import' do
let!(:api_client) { Runtime::API::Client.as_admin }
let!(:group) { Resource::Group.fabricate_via_api! { |resource| resource.api_client = api_client } }
let!(:user) do
Resource::User.fabricate_via_api! do |resource|
resource.api_client = Runtime::API::Client.as_admin
resource.api_client = api_client
resource.hard_delete_on_api_removal = true
end
end
let(:group) { Resource::Group.fabricate_via_api! }
let(:imported_project) do
Resource::ProjectImportedFromGithub.fabricate_via_browser_ui! do |project|
project.name = 'imported-project'
project.group = group
project.github_personal_access_token = Runtime::Env.github_access_token
project.github_repository_path = 'gitlab-qa-github/test-project'
project.api_client = api_client
end
end
before do
Runtime::Feature.enable(:invite_members_group_modal, group: group)
group.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
end
@ -32,90 +33,49 @@ module QA
it 'imports a GitHub repo', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1762' do
Flow::Login.sign_in(as: user)
imported_project # import the project
imported_project.reload! # import the project and reload all fields
Page::Main::Menu.perform(&:go_to_projects)
Page::Dashboard::Projects.perform do |dashboard|
dashboard.go_to_project(imported_project.name)
aggregate_failures do
verify_repository_import
verify_issues_import
verify_merge_requests_import
end
Page::Project::Show.perform(&:wait_for_import)
verify_repository_import
verify_issues_import
verify_merge_requests_import
verify_labels_import
verify_milestones_import
verify_wiki_import
end
def verify_repository_import
Page::Project::Show.perform do |project|
expect(project).to have_content('This test project is used for automated GitHub import by GitLab QA.')
expect(project).to have_content(imported_project.name)
end
expect(imported_project.api_response).to include(
description: 'A new repo for test',
import_status: 'finished',
import_error: nil
)
end
def verify_issues_import
QA::Support::Retrier.retry_on_exception do
Page::Project::Menu.perform(&:click_issues)
issues = imported_project.issues
Page::Project::Issue::Show.perform do |issue_page|
expect(issue_page).to have_content('This is a sample issue')
click_link 'This is a sample issue'
expect(issue_page).to have_content('This is a sample first comment')
# Comments
comment_text = 'This is a comment from @sliaquat'
expect(issue_page).to have_comment(comment_text)
expect(issue_page).to have_label('custom new label')
expect(issue_page).to have_label('help wanted')
expect(issue_page).to have_label('good first issue')
end
end
expect(issues.length).to eq(1)
expect(issues.first).to include(
title: 'This is a sample issue',
description: "*Created by: gitlab-qa-github*\n\nThis is a sample first comment",
labels: ['custom new label', 'good first issue', 'help wanted'],
user_notes_count: 1
)
end
def verify_merge_requests_import
Page::Project::Menu.perform(&:click_merge_requests)
merge_requests = imported_project.merge_requests
Page::MergeRequest::Show.perform do |merge_request|
expect(merge_request).to have_content('Improve readme')
click_link 'Improve readme'
expect(merge_request).to have_content('This improves the README file a bit.')
# Comments
expect(merge_request).to have_content('[PR comment by @sliaquat] Nice work!')
# Diff comments
expect(merge_request).to have_content('[Single diff comment] Good riddance')
expect(merge_request).to have_content('[Single diff comment] Nice addition')
expect(merge_request).to have_label('bug')
expect(merge_request).to have_label('documentation')
end
end
def verify_labels_import
# TODO: Waiting on https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/19228
# to build upon it.
end
def verify_milestones_import
# TODO: Waiting on https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/18727
# to build upon it.
end
def verify_wiki_import
Page::Project::Menu.perform(&:click_wiki)
Page::Project::Wiki::Show.perform do |wiki|
expect(wiki).to have_content('Welcome to the test-project wiki!')
end
expect(merge_requests.length).to eq(1)
expect(merge_requests.first).to include(
title: 'Improve readme',
state: 'opened',
target_branch: 'main',
source_branch: 'improve-readme',
labels: %w[bug documentation],
description: <<~DSC.strip
*Created by: gitlab-qa-github*\n\nThis improves the README file a bit.\r\n\r\nTODO:\r\n\r\n \r\n\r\n- [ ] Do foo\r\n- [ ] Make bar\r\n - [ ] Think about baz
DSC
)
end
end
end

View File

@ -1065,14 +1065,13 @@ RSpec.describe GroupsController, factory_default: :keep do
describe 'GET #download_export' do
let(:admin) { create(:admin) }
let(:export_file) { fixture_file_upload('spec/fixtures/group_export.tar.gz') }
before do
enable_admin_mode!(admin)
end
context 'when there is a file available to download' do
let(:export_file) { fixture_file_upload('spec/fixtures/group_export.tar.gz') }
before do
sign_in(admin)
create(:import_export_upload, group: group, export_file: export_file)
@ -1085,6 +1084,22 @@ RSpec.describe GroupsController, factory_default: :keep do
end
end
context 'when the file is no longer present on disk' do
before do
sign_in(admin)
create(:import_export_upload, group: group, export_file: export_file)
group.export_file.file.delete
end
it 'returns not found' do
get :download_export, params: { id: group.to_param }
expect(flash[:alert]).to include('file containing the export is not available yet')
expect(response).to redirect_to(edit_group_path(group))
end
end
context 'when there is no file available to download' do
before do
sign_in(admin)

View File

@ -7,7 +7,7 @@ RSpec.describe ProjectsController do
include ProjectForksHelper
using RSpec::Parameterized::TableSyntax
let_it_be(:project, reload: true) { create(:project, service_desk_enabled: false) }
let_it_be(:project, reload: true) { create(:project, :with_export, service_desk_enabled: false) }
let_it_be(:public_project) { create(:project, :public) }
let_it_be(:user) { create(:user) }
@ -1349,7 +1349,7 @@ RSpec.describe ProjectsController do
end
end
describe '#download_export' do
describe '#download_export', :clean_gitlab_redis_cache do
let(:action) { :download_export }
context 'object storage enabled' do
@ -1361,6 +1361,17 @@ RSpec.describe ProjectsController do
end
end
context 'when project export file is absent' do
it 'alerts the user and returns 302' do
project.export_file.file.delete
get action, params: { namespace_id: project.namespace, id: project }
expect(flash[:alert]).to include('file containing the export is not available yet')
expect(response).to have_gitlab_http_status(:found)
end
end
context 'when project export is disabled' do
before do
stub_application_setting(project_export_enabled?: false)

View File

@ -58,6 +58,13 @@ FactoryBot.define do
shared_runners_enabled { false }
end
trait :with_export do
after(:create) do |group, _evaluator|
export_file = fixture_file_upload('spec/fixtures/group_export.tar.gz')
create(:import_export_upload, group: group, export_file: export_file)
end
end
trait :allow_descendants_override_disabled_shared_runners do
allow_descendants_override_disabled_shared_runners { true }
end

View File

@ -1,6 +1,7 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DiffStats from '~/diffs/components/diff_stats.vue';
import mockDiffFile from '../mock_data/diff_file';
const TEST_ADDED_LINES = 100;
const TEST_REMOVED_LINES = 200;
@ -38,9 +39,37 @@ describe('diff_stats', () => {
});
});
describe('bytes changes', () => {
let file;
const getBytesContainer = () => wrapper.find('.diff-stats > div:first-child');
beforeEach(() => {
file = {
...mockDiffFile,
viewer: {
...mockDiffFile.viewer,
name: 'not_diffable',
},
};
createComponent({ diffFile: file });
});
it("renders the bytes changes instead of line changes when the file isn't diffable", () => {
const content = getBytesContainer();
expect(content.classes('cgreen')).toBe(true);
expect(content.text()).toBe('+1.00 KiB (+100%)');
});
});
describe('line changes', () => {
const findFileLine = (name) => wrapper.find(name);
beforeEach(() => {
createComponent();
});
it('shows the amount of lines added', () => {
expect(findFileLine('.js-file-addition-line').text()).toBe(TEST_ADDED_LINES.toString());
});

View File

@ -19,6 +19,8 @@ export default {
renamed_file: false,
old_path: 'CHANGELOG',
new_path: 'CHANGELOG',
old_size: 1024,
new_size: 2048,
mode_changed: false,
a_mode: '100644',
b_mode: '100644',

View File

@ -1,4 +1,11 @@
import { prepareRawDiffFile, getShortShaFromFile } from '~/diffs/utils/diff_file';
import {
prepareRawDiffFile,
getShortShaFromFile,
stats,
isNotDiffable,
} from '~/diffs/utils/diff_file';
import { diffViewerModes } from '~/ide/constants';
import mockDiffFile from '../mock_data/diff_file';
function getDiffFiles() {
const loadFull = 'namespace/project/-/merge_requests/12345/diff_for_path?file_identifier=abc';
@ -154,4 +161,73 @@ describe('diff_file utilities', () => {
expect(getShortShaFromFile({ content_sha: cs })).toBe(response);
});
});
describe('stats', () => {
const noFile = [
"returns empty stats when the file isn't provided",
undefined,
{
text: '',
percent: 0,
changed: 0,
classes: '',
sign: '',
valid: false,
},
];
const validFile = [
'computes the correct stats from a file',
mockDiffFile,
{
changed: 1024,
percent: 100,
classes: 'cgreen',
sign: '+',
text: '+1.00 KiB (+100%)',
valid: true,
},
];
const negativeChange = [
'computed the correct states from a file with a negative size change',
{
...mockDiffFile,
new_size: 0,
old_size: 1024,
},
{
changed: -1024,
percent: -100,
classes: 'cred',
sign: '',
text: '-1.00 KiB (-100%)',
valid: true,
},
];
it.each([noFile, validFile, negativeChange])('%s', (_, file, output) => {
expect(stats(file)).toEqual(output);
});
});
describe('isNotDiffable', () => {
it.each`
bool | vw
${true} | ${diffViewerModes.not_diffable}
${false} | ${diffViewerModes.text}
${false} | ${diffViewerModes.image}
`('returns $bool when the viewer is $vw', ({ bool, vw }) => {
expect(isNotDiffable({ viewer: { name: vw } })).toBe(bool);
});
it.each`
file
${undefined}
${null}
${{}}
${{ viewer: undefined }}
${{ viewer: null }}
`('reports `false` when the file is `$file`', ({ file }) => {
expect(isNotDiffable(file)).toBe(false);
});
});
});

View File

@ -990,6 +990,34 @@ RSpec.describe ApplicationSetting do
end
end
end
describe '#diff_max_files' do
context 'validations' do
it { is_expected.to validate_presence_of(:diff_max_files) }
specify do
is_expected
.to validate_numericality_of(:diff_max_files)
.only_integer
.is_greater_than_or_equal_to(Commit::DEFAULT_MAX_DIFF_FILES_SETTING)
.is_less_than_or_equal_to(Commit::MAX_DIFF_FILES_SETTING_UPPER_BOUND)
end
end
end
describe '#diff_max_lines' do
context 'validations' do
it { is_expected.to validate_presence_of(:diff_max_lines) }
specify do
is_expected
.to validate_numericality_of(:diff_max_lines)
.only_integer
.is_greater_than_or_equal_to(Commit::DEFAULT_MAX_DIFF_LINES_SETTING)
.is_less_than_or_equal_to(Commit::MAX_DIFF_LINES_SETTING_UPPER_BOUND)
end
end
end
end
describe '#sourcegraph_url_is_com?' do

View File

@ -672,6 +672,92 @@ eos
it_behaves_like '#uri_type'
end
describe '.diff_max_files' do
subject(:diff_max_files) { described_class.diff_max_files }
let(:increased_diff_limits) { false }
let(:configurable_diff_limits) { false }
before do
stub_feature_flags(increased_diff_limits: increased_diff_limits, configurable_diff_limits: configurable_diff_limits)
end
context 'when increased_diff_limits is enabled' do
let(:increased_diff_limits) { true }
it 'returns 3000' do
expect(diff_max_files).to eq(3000)
end
end
context 'when configurable_diff_limits is enabled' do
let(:configurable_diff_limits) { true }
it 'returns the current settings' do
Gitlab::CurrentSettings.update!(diff_max_files: 1234)
expect(diff_max_files).to eq(1234)
end
end
context 'when neither feature flag is enabled' do
it 'returns 1000' do
expect(diff_max_files).to eq(1000)
end
end
end
describe '.diff_max_lines' do
subject(:diff_max_lines) { described_class.diff_max_lines }
let(:increased_diff_limits) { false }
let(:configurable_diff_limits) { false }
before do
stub_feature_flags(increased_diff_limits: increased_diff_limits, configurable_diff_limits: configurable_diff_limits)
end
context 'when increased_diff_limits is enabled' do
let(:increased_diff_limits) { true }
it 'returns 100000' do
expect(diff_max_lines).to eq(100000)
end
end
context 'when configurable_diff_limits is enabled' do
let(:configurable_diff_limits) { true }
it 'returns the current settings' do
Gitlab::CurrentSettings.update!(diff_max_lines: 65321)
expect(diff_max_lines).to eq(65321)
end
end
context 'when neither feature flag is enabled' do
it 'returns 50000' do
expect(diff_max_lines).to eq(50000)
end
end
end
describe '.diff_safe_max_files' do
subject(:diff_safe_max_files) { described_class.diff_safe_max_files }
it 'returns the commit diff max divided by the limit factor of 10' do
expect(::Commit).to receive(:diff_max_files).and_return(10)
expect(diff_safe_max_files).to eq(1)
end
end
describe '.diff_safe_max_lines' do
subject(:diff_safe_max_lines) { described_class.diff_safe_max_lines }
it 'returns the commit diff max divided by the limit factor of 10' do
expect(::Commit).to receive(:diff_max_lines).and_return(100)
expect(diff_safe_max_lines).to eq(10)
end
end
describe '.from_hash' do
subject { described_class.from_hash(commit.to_hash, container) }

View File

@ -2613,4 +2613,16 @@ RSpec.describe Group do
expect(group.activity_path).to eq(expected_path)
end
end
context 'with export' do
let(:group) { create(:group, :with_export) }
it '#export_file_exists returns true' do
expect(group.export_file_exists?).to be true
end
it '#export_archive_exists? returns true' do
expect(group.export_archive_exists?).to be true
end
end
end

View File

@ -3,7 +3,9 @@
require 'spec_helper'
RSpec.describe ImportExportUpload do
subject { described_class.new(project: create(:project)) }
let(:project) { create(:project) }
subject { described_class.new(project: project) }
shared_examples 'stores the Import/Export file' do |method|
it 'stores the import file' do
@ -43,4 +45,80 @@ RSpec.describe ImportExportUpload do
end
end
end
context 'ActiveRecord callbacks' do
let(:after_save_callbacks) { described_class._save_callbacks.select { |cb| cb.kind == :after } }
let(:after_commit_callbacks) { described_class._commit_callbacks.select { |cb| cb.kind == :after } }
def find_callback(callbacks, key)
callbacks.find { |cb| cb.instance_variable_get(:@key) == key }
end
it 'export file is stored in after_commit callback' do
expect(find_callback(after_commit_callbacks, :store_export_file!)).to be_present
expect(find_callback(after_save_callbacks, :store_export_file!)).to be_nil
end
it 'import file is stored in after_save callback' do
expect(find_callback(after_save_callbacks, :store_import_file!)).to be_present
expect(find_callback(after_commit_callbacks, :store_import_file!)).to be_nil
end
end
describe 'export file' do
it '#export_file_exists? returns false' do
expect(subject.export_file_exists?).to be false
end
it '#export_archive_exists? returns false' do
expect(subject.export_archive_exists?).to be false
end
context 'with export' do
let(:project_with_export) { create(:project, :with_export) }
subject { described_class.with_export_file.find_by(project: project_with_export) }
it '#export_file_exists? returns true' do
expect(subject.export_file_exists?).to be true
end
it '#export_archive_exists? returns false' do
expect(subject.export_archive_exists?).to be true
end
context 'when object file does not exist' do
before do
subject.export_file.file.delete
end
it '#export_file_exists? returns true' do
expect(subject.export_file_exists?).to be true
end
it '#export_archive_exists? returns false' do
expect(subject.export_archive_exists?).to be false
end
end
context 'when checking object existence raises a error' do
let(:exception) { Excon::Error::Forbidden.new('not allowed') }
before do
file = double
allow(file).to receive(:exists?).and_raise(exception)
allow(subject).to receive(:carrierwave_export_file).and_return(file)
end
it '#export_file_exists? returns true' do
expect(subject.export_file_exists?).to be true
end
it '#export_archive_exists? returns false' do
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(exception)
expect(subject.export_archive_exists?).to be false
end
end
end
end
end

View File

@ -4376,6 +4376,18 @@ RSpec.describe Project, factory_default: :keep do
end
end
context 'with export' do
let(:project) { create(:project, :with_export) }
it '#export_file_exists? returns true' do
expect(project.export_file_exists?).to be true
end
it '#export_archive_exists? returns false' do
expect(project.export_archive_exists?).to be true
end
end
describe '#forks_count' do
it 'returns the number of forks' do
project = build(:project)
@ -6638,7 +6650,7 @@ RSpec.describe Project, factory_default: :keep do
context 'when project export is completed' do
before do
finish_job(project_export_job)
allow(project).to receive(:export_file).and_return(double(ImportExportUploader, file: 'exists.zip'))
allow(project).to receive(:export_file_exists?).and_return(true)
end
it { expect(project.export_status).to eq :finished }
@ -6649,7 +6661,7 @@ RSpec.describe Project, factory_default: :keep do
before do
finish_job(project_export_job)
allow(project).to receive(:export_file).and_return(double(ImportExportUploader, file: 'exists.zip'))
allow(project).to receive(:export_file_exists?).and_return(true)
end
it { expect(project.export_status).to eq :regeneration_in_progress }

View File

@ -64,6 +64,23 @@ RSpec.describe API::GroupExport do
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when object is not present' do
let(:other_group) { create(:group, :with_export) }
let(:other_download_path) { "/groups/#{other_group.id}/export/download" }
before do
other_group.add_owner(user)
other_group.export_file.file.delete
end
it 'returns 404' do
get api(other_download_path, user)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('The group export file is not available yet')
end
end
end
context 'when export file does not exist' do

View File

@ -1353,7 +1353,7 @@ RSpec.describe API::MergeRequests do
context 'when a merge request has more than the changes limit' do
it "returns a string indicating that more changes were made" do
allow(Commit).to receive(:diff_hard_limit_files).and_return(5)
allow(Commit).to receive(:diff_max_files).and_return(5)
merge_request_overflow = create(:merge_request, :simple,
author: user,

View File

@ -196,6 +196,19 @@ RSpec.describe API::ProjectExport, :clean_gitlab_redis_cache do
end
end
context 'when export object is not present' do
before do
project_after_export.export_file.file.delete
end
it 'returns 404' do
get api(download_path_export_action, user)
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('The project export file is not available yet')
end
end
context 'when upload complete' do
before do
project_after_export.remove_exports

View File

@ -113,6 +113,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
terms: 'Hello world!',
performance_bar_allowed_group_path: group.full_path,
diff_max_patch_bytes: 300_000,
diff_max_files: 2000,
diff_max_lines: 50000,
default_branch_protection: ::Gitlab::Access::PROTECTION_DEV_CAN_MERGE,
local_markdown_version: 3,
allow_local_requests_from_web_hooks_and_services: true,
@ -159,6 +161,8 @@ RSpec.describe API::Settings, 'Settings', :do_not_mock_admin_mode_setting do
expect(json_response['terms']).to eq('Hello world!')
expect(json_response['performance_bar_allowed_group_id']).to eq(group.id)
expect(json_response['diff_max_patch_bytes']).to eq(300_000)
expect(json_response['diff_max_files']).to eq(2000)
expect(json_response['diff_max_lines']).to eq(50000)
expect(json_response['default_branch_protection']).to eq(Gitlab::Access::PROTECTION_DEV_CAN_MERGE)
expect(json_response['local_markdown_version']).to eq(3)
expect(json_response['allow_local_requests_from_web_hooks_and_services']).to eq(true)