Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-01-26 21:16:58 +00:00
parent 733f1d8bb1
commit c9d1b77888
37 changed files with 764 additions and 119 deletions

View File

@ -403,25 +403,25 @@ export default {
>{{ i18n.cancel }}
</gl-button>
<gl-button
v-if="isKasEnabledInEmptyStateModal"
:href="repositoryPath"
variant="confirm"
category="secondary"
data-testid="agent-secondary-button"
>{{ i18n.secondaryButton }}
</gl-button>
<gl-button
v-if="isEmptyStateModal"
variant="confirm"
category="primary"
category="secondary"
:data-track-action="$options.EVENT_ACTIONS_CLICK"
:data-track-label="$options.EVENT_LABEL_MODAL"
data-track-property="done"
@click="closeModal"
>{{ i18n.done }}
</gl-button>
<gl-button
v-if="isKasEnabledInEmptyStateModal"
:href="repositoryPath"
variant="confirm"
category="primary"
data-testid="agent-primary-button"
>{{ i18n.primaryButton }}
</gl-button>
</template>
</gl-modal>
</template>

View File

@ -112,7 +112,7 @@ export const I18N_AGENT_MODAL = {
"ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it.",
),
altText: s__('ClusterAgents|GitLab Agent for Kubernetes'),
secondaryButton: s__('ClusterAgents|Go to the repository files'),
primaryButton: s__('ClusterAgents|Go to the repository files'),
done: __('Cancel'),
},
};

View File

@ -28,6 +28,7 @@ const groupsSelect = () => {
const skipGroups = $select.data('skipGroups') || [];
const parentGroupID = $select.data('parentId');
const groupsFilter = $select.data('groupsFilter');
const minAccessLevel = $select.data('minAccessLevel');
$select.select2({
placeholder: __('Search for a group'),
@ -45,6 +46,7 @@ const groupsSelect = () => {
page,
per_page: window.GROUP_SELECT_PER_PAGE,
all_available: allAvailable,
min_access_level: minAccessLevel,
};
},
results(data, page) {

View File

@ -24,6 +24,10 @@ export default {
prop: 'selectedGroup',
},
props: {
accessLevels: {
type: Object,
required: true,
},
groupsFilter: {
type: String,
required: false,
@ -50,6 +54,13 @@ export default {
isFetchResultEmpty() {
return this.groups.length === 0;
},
defaultFetchOptions() {
return {
exclude_internal: true,
active: true,
min_access_level: this.accessLevels.Guest,
};
},
},
watch: {
searchTerm() {
@ -84,13 +95,9 @@ export default {
fetchGroups() {
switch (this.groupsFilter) {
case GROUP_FILTERS.DESCENDANT_GROUPS:
return getDescendentGroups(
this.parentGroupId,
this.searchTerm,
this.$options.defaultFetchOptions,
);
return getDescendentGroups(this.parentGroupId, this.searchTerm, this.defaultFetchOptions);
default:
return getGroups(this.searchTerm, this.$options.defaultFetchOptions);
return getGroups(this.searchTerm, this.defaultFetchOptions);
}
},
},
@ -99,10 +106,6 @@ export default {
searchPlaceholder: s__('GroupSelect|Search groups'),
emptySearchResult: s__('GroupSelect|No matching results'),
},
defaultFetchOptions: {
exclude_internal: true,
active: true,
},
};
</script>
<template>

View File

@ -428,6 +428,7 @@ export default {
<group-select
v-if="isInviteGroup"
v-model="groupToBeSharedWith"
:access-levels="accessLevels"
:groups-filter="groupSelectFilter"
:parent-group-id="groupSelectParentId"
@input="handleMembersTokenSelectClear"

View File

@ -0,0 +1,3 @@
import initProjectNew from '~/projects/project_new';
initProjectNew.bindEvents();

View File

@ -300,7 +300,7 @@ export default {
v-gl-tooltip
:title="
__(
'Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results.',
'Merge request pipelines are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for merge request pipelines.',
)
"
variant="info"

View File

@ -1,6 +1,7 @@
import $ from 'jquery';
import { debounce } from 'lodash';
import DEFAULT_PROJECT_TEMPLATES from 'ee_else_ce/projects/default_project_templates';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '../lib/utils/constants';
import axios from '../lib/utils/axios_utils';
import {
convertToTitleCase,
@ -13,20 +14,26 @@ let hasUserDefinedProjectPath = false;
let hasUserDefinedProjectName = false;
const invalidInputClass = 'gl-field-error-outline';
const cancelSource = axios.CancelToken.source();
const endpoint = `${gon.relative_url_root}/import/url/validate`;
let importCredentialsValidationPromise = null;
const validateImportCredentials = (url, user, password) => {
const endpoint = `${gon.relative_url_root}/import/url/validate`;
return axios
.post(endpoint, {
url,
user,
password,
})
cancelSource.cancel();
importCredentialsValidationPromise = axios
.post(endpoint, { url, user, password }, { cancelToken: cancelSource.cancel() })
.then(({ data }) => data)
.catch(() => ({
// intentionally reporting success in case of validation error
// we do not want to block users from trying import in case of validation exception
success: true,
}));
.catch((thrown) =>
axios.isCancel(thrown)
? {
cancelled: true,
}
: {
// intentionally reporting success in case of validation error
// we do not want to block users from trying import in case of validation exception
success: true,
},
);
return importCredentialsValidationPromise;
};
const onProjectNameChange = ($projectNameInput, $projectPathInput) => {
@ -72,7 +79,7 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => {
.parents('.toggle-import-form')
.find('#project_path');
if (hasUserDefinedProjectPath) {
if (hasUserDefinedProjectPath || $currentProjectPath.length === 0) {
return;
}
@ -114,7 +121,7 @@ const bindEvents = () => {
const $projectImportUrlUser = $('#project_import_url_user');
const $projectImportUrlPassword = $('#project_import_url_password');
const $projectImportUrlError = $('.js-import-url-error');
const $projectImportForm = $('.project-import form');
const $projectImportForm = $('form.js-project-import');
const $projectPath = $('.tab-pane.active #project_path');
const $useTemplateBtn = $('.template-button > input');
const $projectFieldsForm = $('.project-fields-form');
@ -124,7 +131,7 @@ const bindEvents = () => {
const $projectTemplateButtons = $('.project-templates-buttons');
const $projectName = $('.tab-pane.active #project_name');
if ($newProjectForm.length !== 1) {
if ($newProjectForm.length !== 1 && $projectImportForm.length !== 1) {
return;
}
@ -168,20 +175,28 @@ const bindEvents = () => {
$projectPath.val($projectPath.val().trim());
});
const updateUrlPathWarningVisibility = debounce(async () => {
const { success: isUrlValid } = await validateImportCredentials(
const updateUrlPathWarningVisibility = async () => {
const { success: isUrlValid, cancelled } = await validateImportCredentials(
$projectImportUrl.val(),
$projectImportUrlUser.val(),
$projectImportUrlPassword.val(),
);
if (cancelled) {
return;
}
$projectImportUrl.toggleClass(invalidInputClass, !isUrlValid);
$projectImportUrlError.toggleClass('hide', isUrlValid);
}, 500);
};
const debouncedUpdateUrlPathWarningVisibility = debounce(
updateUrlPathWarningVisibility,
DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
);
let isProjectImportUrlDirty = false;
$projectImportUrl.on('blur', () => {
isProjectImportUrlDirty = true;
updateUrlPathWarningVisibility();
debouncedUpdateUrlPathWarningVisibility();
});
$projectImportUrl.on('keyup', () => {
deriveProjectPathFromUrl($projectImportUrl);
@ -190,17 +205,33 @@ const bindEvents = () => {
[$projectImportUrl, $projectImportUrlUser, $projectImportUrlPassword].forEach(($f) => {
$f.on('input', () => {
if (isProjectImportUrlDirty) {
updateUrlPathWarningVisibility();
debouncedUpdateUrlPathWarningVisibility();
}
});
});
$projectImportForm.on('submit', (e) => {
$projectImportForm.on('submit', async (e) => {
e.preventDefault();
if (importCredentialsValidationPromise === null) {
// we didn't validate credentials yet
debouncedUpdateUrlPathWarningVisibility.cancel();
updateUrlPathWarningVisibility();
}
const submitBtn = $projectImportForm.find('input[type="submit"]');
submitBtn.disable();
await importCredentialsValidationPromise;
submitBtn.enable();
const $invalidFields = $projectImportForm.find(`.${invalidInputClass}`);
if ($invalidFields.length > 0) {
$invalidFields[0].focus();
e.preventDefault();
e.stopPropagation();
} else {
// calling .submit() on HTMLFormElement does not trigger 'submit' event
// We are using this behavior to bypass this handler and avoid infinite loop
$projectImportForm[0].submit();
}
});

View File

@ -1,11 +1,16 @@
<script>
import { GlBanner } from '@gitlab/ui';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
export const SECURITY_UPGRADE_BANNER = 'security_upgrade_banner';
export const UPGRADE_OR_FREE_TRIAL = 'upgrade_or_free_trial';
export default {
components: {
GlBanner,
},
mixins: [Tracking.mixin({ property: SECURITY_UPGRADE_BANNER })],
inject: ['upgradePath'],
i18n: {
title: s__('SecurityConfiguration|Secure your project'),
@ -22,6 +27,17 @@ export default {
],
buttonText: s__('SecurityConfiguration|Upgrade or start a free trial'),
},
mounted() {
this.track('display_banner', { label: SECURITY_UPGRADE_BANNER });
},
methods: {
bannerClosed() {
this.track('dismiss_banner', { label: SECURITY_UPGRADE_BANNER });
},
bannerButtonClicked() {
this.track('click_button', { label: UPGRADE_OR_FREE_TRIAL });
},
},
};
</script>
@ -31,6 +47,8 @@ export default {
:button-text="$options.i18n.buttonText"
:button-link="upgradePath"
variant="introduction"
@close="bannerClosed"
@primary="bannerButtonClicked"
v-on="$listeners"
>
<p>{{ $options.i18n.bodyStart }}</p>

View File

@ -148,7 +148,11 @@ class UsersController < ApplicationController
end
def exists
render json: { exists: !!Namespace.find_by_path_or_name(params[:username]) }
if Gitlab::CurrentSettings.signup_enabled? || current_user
render json: { exists: !!Namespace.find_by_path_or_name(params[:username]) }
else
render json: { error: _('You must be authenticated to access this path.') }, status: :unauthorized
end
end
def follow

View File

@ -82,7 +82,7 @@
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
= form_for @project, html: { class: 'new_project gl-show-field-errors' } do |f|
= form_for @project, html: { class: 'new_project gl-show-field-errors js-project-import' } do |f|
%hr
= render "shared/import_form", f: f
= render 'projects/new_project_fields', f: f, project_name_id: "import-url-name", hide_init_with_readme: true, track_label: track_label

View File

@ -10,7 +10,7 @@
.text-secondary
- configuring_pipelines_for_merge_requests_help_link_url = help_page_path('ci/pipelines/merge_request_pipelines.md', anchor: 'prerequisites')
- configuring_pipelines_for_merge_requests_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: configuring_pipelines_for_merge_requests_help_link_url }
= s_('ProjectSettings|To enable this feature, configure pipelines. %{link_start}How to configure pipelines for merge requests?%{link_end}').html_safe % { link_start: configuring_pipelines_for_merge_requests_help_link_start, link_end: '</a>'.html_safe }
= s_('ProjectSettings|To enable this feature, configure pipelines. %{link_start}How to configure merge request pipelines?%{link_end}').html_safe % { link_start: configuring_pipelines_for_merge_requests_help_link_start, link_end: '</a>'.html_safe }
.form-check.mb-2
.gl-pl-6
= form.check_box :allow_merge_on_skipped_pipeline, class: 'form-check-input'

View File

@ -12,8 +12,8 @@
:preserve
#{h(@project.import_state.last_error)}
= form_for @project, url: project_import_path(@project), method: :post do |f|
= form_for @project, url: project_import_path(@project), method: :post, html: { class: 'js-project-import' } do |f|
= render "shared/import_form", f: f
.form-actions
= f.submit 'Start import', class: "gl-button btn btn-confirm"
= f.submit 'Start import', class: "gl-button btn btn-confirm", data: { disable_with: false }

View File

@ -38,7 +38,7 @@
- popover_content_text = _('Learn more about Auto DevOps')
= gl_badge_tag s_('Pipelines|Auto DevOps'), { variant: :info, size: :sm }, { class: 'js-pipeline-url-autodevops', href: "#", tabindex: "0", role: "button", data: { container: 'body', toggle: 'popover', placement: 'top', html: 'true', triggers: 'focus', title: "<div class='gl-font-weight-normal gl-line-height-normal'>#{popover_title_text}</div>", content: "<a href='#{popover_content_url}' target='_blank' rel='noopener noreferrer nofollow'>#{popover_content_text}</a>" } }
- if @pipeline.detached_merge_request_pipeline?
= gl_badge_tag s_('Pipelines|detached'), { variant: :info, size: :sm }, { class: 'js-pipeline-url-mergerequest has-tooltip', title: _('Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results.') }
= gl_badge_tag s_('Pipelines|detached'), { variant: :info, size: :sm }, { class: 'js-pipeline-url-mergerequest has-tooltip', title: _('Merge request pipelines are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for merge request pipelines.') }
- if @pipeline.stuck?
= gl_badge_tag s_('Pipelines|stuck'), { variant: :warning, size: :sm }, { class: 'js-pipeline-url-stuck has-tooltip' }

View File

@ -61,7 +61,7 @@
default_access_level: ProjectGroupLink.default_access,
group_link_field: 'link_group_id',
group_access_field: 'link_group_access',
groups_select_tag_data: { skip_groups: @skip_groups }
groups_select_tag_data: { min_access_level: Gitlab::Access::GUEST, skip_groups: @skip_groups }
- elsif !membership_locked?
.invite-member
= render 'shared/members/invite_member',
@ -78,7 +78,7 @@
submit_url: project_group_links_path(@project),
group_link_field: 'link_group_id',
group_access_field: 'link_group_access',
groups_select_tag_data: { skip_groups: @skip_groups }
groups_select_tag_data: { min_access_level: Gitlab::Access::GUEST, skip_groups: @skip_groups }
.js-project-members-list-app{ data: { members_data: project_members_app_data_json(@project,
members: @project_members,
group_links: @group_links,

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
class AddDependencyProxySizeToNamespaceStatistics < Gitlab::Database::Migration[1.0]
def change
add_column :namespace_statistics, :dependency_proxy_size, :bigint, default: 0, null: false
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
class RemoveProjectsCiProjectMonthlyUsagesProjectIdFk < Gitlab::Database::Migration[1.0]
disable_ddl_transaction!
def up
return unless foreign_key_exists?(:ci_project_monthly_usages, :projects, name: "fk_rails_508bcd4aa6")
with_lock_retries do
execute('LOCK projects, ci_project_monthly_usages IN ACCESS EXCLUSIVE MODE') if transaction_open?
remove_foreign_key_if_exists(:ci_project_monthly_usages, :projects, name: "fk_rails_508bcd4aa6")
end
end
def down
add_concurrent_foreign_key(:ci_project_monthly_usages, :projects, name: "fk_rails_508bcd4aa6", column: :project_id, target_column: :id, on_delete: :cascade)
end
end

View File

@ -0,0 +1 @@
18b3ef459c3633ebd4b418b4436d3d50b0dc697fa7c4ef4d2e1f62b01d656bce

View File

@ -0,0 +1 @@
4b1dad4fc34717c9b89a770e4e0682b0cee7d421da68223011bb9fde9460d1f8

View File

@ -16544,7 +16544,8 @@ CREATE TABLE namespace_statistics (
shared_runners_seconds integer DEFAULT 0 NOT NULL,
shared_runners_seconds_last_reset timestamp without time zone,
storage_size bigint DEFAULT 0 NOT NULL,
wiki_size bigint DEFAULT 0 NOT NULL
wiki_size bigint DEFAULT 0 NOT NULL,
dependency_proxy_size bigint DEFAULT 0 NOT NULL
);
CREATE SEQUENCE namespace_statistics_id_seq
@ -30487,9 +30488,6 @@ ALTER TABLE ONLY resource_iteration_events
ALTER TABLE ONLY status_page_settings
ADD CONSTRAINT fk_rails_506e5ba391 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY ci_project_monthly_usages
ADD CONSTRAINT fk_rails_508bcd4aa6 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY project_repository_storage_moves
ADD CONSTRAINT fk_rails_5106dbd44a FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;

View File

@ -8,9 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/332747) in GitLab 14.5 [with a flag](../administration/feature_flags.md) named `ff_external_audit_events_namespace`. Disabled by default.
> - [Enabled on GitLab.com and by default on self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/338939) in GitLab 14.7.
FLAG:
On self-managed GitLab, by default this feature is available. To hide the feature per group, ask an administrator to [disable the feature flag](../administration/feature_flags.md) named `ff_external_audit_events_namespace`. On GitLab.com, this feature is available.
> - [Feature flag `ff_external_audit_events_namespace`](https://gitlab.com/gitlab-org/gitlab/-/issues/349588) removed in GitLab 14.8.
Event streaming allows owners of top-level groups to set an HTTP endpoint to receive **all** audit events about the group, and its
subgroups and projects as structured JSON.

View File

@ -4356,6 +4356,27 @@ Input type: `TimelineEventDestroyInput`
| <a id="mutationtimelineeventdestroyerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationtimelineeventdestroytimelineevent"></a>`timelineEvent` | [`TimelineEventType`](#timelineeventtype) | Timeline event. |
### `Mutation.timelineEventUpdate`
Input type: `TimelineEventUpdateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelineeventupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelineeventupdateid"></a>`id` | [`IncidentManagementTimelineEventID!`](#incidentmanagementtimelineeventid) | ID of the timeline event to update. |
| <a id="mutationtimelineeventupdatenote"></a>`note` | [`String`](#string) | Text note of the timeline event. |
| <a id="mutationtimelineeventupdateoccurredat"></a>`occurredAt` | [`Time`](#time) | Timestamp when the event occurred. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationtimelineeventupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationtimelineeventupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationtimelineeventupdatetimelineevent"></a>`timelineEvent` | [`TimelineEventType`](#timelineeventtype) | Timeline event. |
### `Mutation.todoCreate`
Input type: `TodoCreateInput`

View File

@ -144,6 +144,14 @@ end
After the feature flag is removed, clean up the resource class and delete the variable.
All methods should use the condition procedures of the now-default state.
## Managing flakiness due to caching
All application settings, and all feature flags, are cached inside GitLab for one minute.
All caching is disabled during testing, except on static environments.
When a test changes a feature flag, it can cause flaky behavior if elements are visible only with an
active feature flag. To circumvent this behavior, add a wait for elements behind a feature flag.
## Running a scenario with a feature flag enabled
It's also possible to run an entire scenario with a feature flag enabled, without having to edit

View File

@ -0,0 +1,134 @@
# frozen_string_literal: true
module BulkImports
module Common
module Pipelines
class LfsObjectsPipeline
include Pipeline
def extract(_context)
download_service.execute
decompression_service.execute
extraction_service.execute
file_paths = Dir.glob(File.join(tmpdir, '*'))
BulkImports::Pipeline::ExtractedData.new(data: file_paths)
end
# rubocop: disable CodeReuse/ActiveRecord
def load(_context, file_path)
Gitlab::Utils.check_path_traversal!(file_path)
Gitlab::Utils.check_allowed_absolute_path!(file_path, [Dir.tmpdir])
return if tar_filepath?(file_path)
return if lfs_json_filepath?(file_path)
return if File.directory?(file_path)
return if File.lstat(file_path).symlink?
size = File.size(file_path)
oid = LfsObject.calculate_oid(file_path)
lfs_object = LfsObject.find_or_initialize_by(oid: oid, size: size)
lfs_object.file = File.open(file_path) unless lfs_object.file&.exists?
lfs_object.save! if lfs_object.changed?
repository_types(oid)&.each do |type|
create_lfs_objects_project(lfs_object, type)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def after_run(_)
FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir)
end
private
def download_service
BulkImports::FileDownloadService.new(
configuration: context.configuration,
relative_url: context.entity.relation_download_url_path(relation),
tmpdir: tmpdir,
filename: targz_filename
)
end
def decompression_service
BulkImports::FileDecompressionService.new(tmpdir: tmpdir, filename: targz_filename)
end
def extraction_service
BulkImports::ArchiveExtractionService.new(tmpdir: tmpdir, filename: tar_filename)
end
def lfs_json
@lfs_json ||= Gitlab::Json.parse(File.read(lfs_json_filepath))
rescue StandardError
raise BulkImports::Error, 'LFS Objects JSON read failed'
end
def tmpdir
@tmpdir ||= Dir.mktmpdir('bulk_imports')
end
def relation
BulkImports::FileTransfer::ProjectConfig::LFS_OBJECTS_RELATION
end
def tar_filename
"#{relation}.tar"
end
def targz_filename
"#{tar_filename}.gz"
end
def lfs_json_filepath?(file_path)
file_path == lfs_json_filepath
end
def tar_filepath?(file_path)
File.join(tmpdir, tar_filename) == file_path
end
def lfs_json_filepath
File.join(tmpdir, "#{relation}.json")
end
def create_lfs_objects_project(lfs_object, repository_type)
return unless allowed_repository_types.include?(repository_type)
lfs_objects_project = LfsObjectsProject.create(
project: portable,
lfs_object: lfs_object,
repository_type: repository_type
)
return if lfs_objects_project.persisted?
logger.warn(
project_id: portable.id,
message: 'Failed to save lfs objects project',
errors: lfs_objects_project.errors.full_messages.to_sentence,
**Gitlab::ApplicationContext.current
)
end
def repository_types(oid)
types = lfs_json[oid]
return [] unless types
return [] unless types.is_a?(Array)
# only return allowed repository types
types.uniq & allowed_repository_types
end
def allowed_repository_types
@allowed_repository_types ||= LfsObjectsProject.repository_types.values.push(nil)
end
end
end
end
end

View File

@ -87,6 +87,10 @@ module BulkImports
pipeline: BulkImports::Common::Pipelines::UploadsPipeline,
stage: 5
},
lfs_objects: {
pipeline: BulkImports::Common::Pipelines::LfsObjectsPipeline,
stage: 5
},
auto_devops: {
pipeline: BulkImports::Projects::Pipelines::AutoDevopsPipeline,
stage: 5

View File

@ -129,6 +129,10 @@ packages_package_file_build_infos:
- table: ci_pipelines
column: pipeline_id
on_delete: async_nullify
ci_project_monthly_usages:
- table: projects
column: project_id
on_delete: async_delete
pages_deployments:
- table: ci_builds
column: ci_build_id

View File

@ -9811,12 +9811,24 @@ msgstr ""
msgid "CorpusManagement|Corpus are used in fuzz testing as mutation source to Improve future testing."
msgstr ""
msgid "CorpusManagement|Corpus file"
msgstr ""
msgid "CorpusManagement|Corpus files must be in *.zip format. Maximum 5 GB"
msgstr ""
msgid "CorpusManagement|Corpus name"
msgstr ""
msgid "CorpusManagement|Currently, there are no uploaded or generated corpuses."
msgstr ""
msgid "CorpusManagement|File too large, Maximum 5 GB"
msgstr ""
msgid "CorpusManagement|Filename can contain only lowercase letters (a-z), uppercase letter (A-Z), numbers (0-9), dots (.), hyphens (-), or underscores (_)."
msgstr ""
msgid "CorpusManagement|Fuzz testing corpus management"
msgstr ""
@ -9829,9 +9841,6 @@ msgstr ""
msgid "CorpusManagement|Latest Job:"
msgstr ""
msgid "CorpusManagement|New corpus needs to be a upload in *.zip format. Maximum 5GB"
msgstr ""
msgid "CorpusManagement|New upload"
msgstr ""
@ -22372,6 +22381,9 @@ msgstr ""
msgid "Merge request events"
msgstr ""
msgid "Merge request pipelines are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for merge request pipelines."
msgstr ""
msgid "Merge request reports"
msgstr ""
@ -26201,9 +26213,6 @@ msgstr ""
msgid "Pipelines charts"
msgstr ""
msgid "Pipelines for merge requests are configured. A detached pipeline runs in the context of the merge request, and not against the merged result. Learn more in the documentation for Pipelines for Merged Results."
msgstr ""
msgid "Pipelines settings for '%{project_name}' were successfully updated."
msgstr ""
@ -28232,7 +28241,7 @@ msgstr ""
msgid "ProjectSettings|This setting will be applied to all projects unless overridden by an admin."
msgstr ""
msgid "ProjectSettings|To enable this feature, configure pipelines. %{link_start}How to configure pipelines for merge requests?%{link_end}"
msgid "ProjectSettings|To enable this feature, configure pipelines. %{link_start}How to configure merge request pipelines?%{link_end}"
msgstr ""
msgid "ProjectSettings|Transfer project"
@ -28274,7 +28283,7 @@ msgstr ""
msgid "ProjectSettings|What are badges?"
msgstr ""
msgid "ProjectSettings|When pipelines for merge requests are enabled in the CI/CD configuration file, pipelines validate the combined results of the source and target branches. %{link_start}How to configure pipelines for merge requests?%{link_end}"
msgid "ProjectSettings|When merge request pipelines are enabled in the CI/CD configuration file, pipelines validate the combined results of the source and target branches. %{link_start}How to configure merge request pipelines?%{link_end}"
msgstr ""
msgid "ProjectSettings|When there is a merge conflict, the user is given the option to rebase."
@ -41145,6 +41154,9 @@ msgstr ""
msgid "You may close the milestone now."
msgstr ""
msgid "You must be authenticated to access this path."
msgstr ""
msgid "You must be logged in to search across all of GitLab"
msgstr ""

View File

@ -11,7 +11,7 @@ module RuboCop
If there is no relevant metadata, please disable the cop with a comment
explaining this.
Read more about it https://docs.gitlab.com/ee/development/sidekiq_style_guide.html#worker-context
Read more about it https://docs.gitlab.com/ee/development/sidekiq/logging.html#worker-context
MSG
def_node_matcher :includes_cronjob_queue?, <<~PATTERN

View File

@ -214,31 +214,88 @@ RSpec.describe 'Project > Members > Invite group', :js do
end
context 'for a project in a nested group' do
let(:group) { create(:group) }
let!(:nested_group) { create(:group, parent: group) }
let!(:group_to_share_with) { create(:group) }
let!(:project) { create(:project, namespace: nested_group) }
let!(:parent_group) { create(:group, :public) }
let!(:public_subgroup) { create(:group, :public, parent: parent_group) }
let!(:public_sub_subgroup) { create(:group, :public, parent: public_subgroup) }
let!(:private_subgroup) { create(:group, :private, parent: parent_group) }
let!(:project) { create(:project, :public, namespace: public_subgroup) }
let!(:membership_group) { create(:group, :public) }
before do
project.add_maintainer(maintainer)
membership_group.add_guest(maintainer)
sign_in(maintainer)
group.add_maintainer(maintainer)
group_to_share_with.add_maintainer(maintainer)
end
# This behavior should be changed to exclude the ancestor and project
# group from the options once issue is fixed for the modal:
# https://gitlab.com/gitlab-org/gitlab/-/issues/329835
it 'the groups dropdown does show ancestors and the project group' do
visit project_project_members_path(project)
context 'when invite_members_group_modal feature enabled' do
it 'does not show the groups inherited from projects' do
visit project_project_members_path(project)
click_on 'Invite a group'
click_on 'Select a group'
wait_for_requests
click_on 'Invite a group'
click_on 'Select a group'
wait_for_requests
expect(page).to have_button(group_to_share_with.name)
expect(page).to have_button(group.name)
expect(page).to have_button(nested_group.name)
expect(page).to have_button(membership_group.name)
expect(page).not_to have_button(parent_group.name)
expect(page).not_to have_button(public_subgroup.name)
expect(page).not_to have_button(public_sub_subgroup.name)
expect(page).not_to have_button(private_subgroup.name)
end
# This behavior should be changed to exclude the ancestor and project
# group from the options once issue is fixed for the modal:
# https://gitlab.com/gitlab-org/gitlab/-/issues/329835
it 'does show ancestors and the project group' do
parent_group.add_maintainer(maintainer)
visit project_project_members_path(project)
click_on 'Invite a group'
click_on 'Select a group'
wait_for_requests
expect(page).to have_button(membership_group.name)
expect(page).to have_button(parent_group.name)
expect(page).to have_button(public_subgroup.name)
end
end
context 'when invite_members_group_modal feature disabled' do
let(:group_invite_dropdown) { find('#select2-results-2') }
before do
stub_feature_flags(invite_members_group_modal: false)
end
it 'does not show the groups inherited from projects' do
visit project_project_members_path(project)
click_on 'Invite group'
click_on 'Search for a group'
wait_for_requests
expect(group_invite_dropdown).to have_text(membership_group.name)
expect(group_invite_dropdown).not_to have_text(parent_group.name)
expect(group_invite_dropdown).not_to have_text(public_subgroup.name)
expect(group_invite_dropdown).not_to have_text(public_sub_subgroup.name)
expect(group_invite_dropdown).not_to have_text(private_subgroup.name)
end
it 'does not show ancestors and the project group' do
parent_group.add_maintainer(maintainer)
visit project_project_members_path(project)
click_on 'Invite group'
click_on 'Search for a group'
wait_for_requests
expect(group_invite_dropdown).to have_text(membership_group.name)
expect(group_invite_dropdown).not_to have_text(parent_group.name, exact: true)
expect(group_invite_dropdown).not_to have_text(public_subgroup.name, exact: true)
end
end
end
end

View File

@ -306,10 +306,24 @@ RSpec.describe 'New project', :js do
expect(page).to have_text('There is not a valid Git repository at this URL')
end
it 'reports error if repo URL is not a valid Git repository and submit button is clicked immediately' do
stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return(status: 200, body: "not-a-git-repo")
fill_in 'project_import_url', with: 'http://foo/bar'
click_on 'Create project'
wait_for_requests
expect(page).to have_text('There is not a valid Git repository at this URL')
end
it 'keeps "Import project" tab open after form validation error' do
collision_project = create(:project, name: 'test-name-collision', namespace: user.namespace)
stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return({ status: 200,
body: '001e# service=git-upload-pack',
headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
fill_in 'project_import_url', with: collision_project.http_url_to_repo
fill_in 'project_import_url', with: 'http://foo/bar'
fill_in 'project_name', with: collision_project.name
click_on 'Create project'
@ -319,6 +333,38 @@ RSpec.describe 'New project', :js do
end
end
context 'when import is initiated from project page' do
before do
project_without_repo = create(:project, name: 'project-without-repo', namespace: user.namespace)
visit project_path(project_without_repo)
click_on 'Import repository'
end
it 'reports error when invalid url is provided' do
stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return(status: 200, body: "not-a-git-repo")
fill_in 'project_import_url', with: 'http://foo/bar'
click_on 'Start import'
wait_for_requests
expect(page).to have_text('There is not a valid Git repository at this URL')
end
it 'initiates import when valid repo url is provided' do
stub_request(:get, "http://foo/bar/info/refs?service=git-upload-pack").to_return({ status: 200,
body: '001e# service=git-upload-pack',
headers: { 'Content-Type': 'application/x-git-upload-pack-advertisement' } })
fill_in 'project_import_url', with: 'http://foo/bar'
click_on 'Start import'
wait_for_requests
expect(page).to have_text('Import in progress')
end
end
context 'from GitHub' do
before do
first('.js-import-github').click

View File

@ -65,7 +65,7 @@ describe('InstallAgentModal', () => {
.wrappers.find((button) => button.props('variant') === variant);
const findActionButton = () => findButtonByVariant('confirm');
const findCancelButton = () => findButtonByVariant('default');
const findSecondaryButton = () => wrapper.findByTestId('agent-secondary-button');
const findPrimaryButton = () => wrapper.findByTestId('agent-primary-button');
const findImage = () => wrapper.findByRole('img', { alt: I18N_AGENT_MODAL.empty_state.altText });
const expectDisabledAttribute = (element, disabled) => {
@ -293,9 +293,9 @@ describe('InstallAgentModal', () => {
expect(findImage().attributes('src')).toBe(emptyStateImage);
});
it('renders a secondary button', () => {
expect(findSecondaryButton().isVisible()).toBe(true);
expect(findSecondaryButton().text()).toBe(i18n.secondaryButton);
it('renders a primary button', () => {
expect(findPrimaryButton().isVisible()).toBe(true);
expect(findPrimaryButton().text()).toBe(i18n.primaryButton);
});
it('sends the event with the modalType', () => {
@ -333,7 +333,7 @@ describe('InstallAgentModal', () => {
});
it("doesn't render a secondary button", () => {
expect(findSecondaryButton().exists()).toBe(false);
expect(findPrimaryButton().exists()).toBe(false);
});
});
});

View File

@ -4,8 +4,14 @@ import waitForPromises from 'helpers/wait_for_promises';
import * as groupsApi from '~/api/groups_api';
import GroupSelect from '~/invite_members/components/group_select.vue';
const accessLevels = { Guest: 10, Reporter: 20, Developer: 30, Maintainer: 40, Owner: 50 };
const createComponent = () => {
return mount(GroupSelect, {});
return mount(GroupSelect, {
propsData: {
accessLevels,
},
});
};
const group1 = { id: 1, full_name: 'Group One', avatar_url: 'test' };
@ -61,6 +67,7 @@ describe('GroupSelect', () => {
expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, {
active: true,
exclude_internal: true,
min_access_level: accessLevels.Guest,
});
});

View File

@ -1,15 +1,22 @@
import { GlBanner } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UpgradeBanner from '~/security_configuration/components/upgrade_banner.vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import UpgradeBanner, {
SECURITY_UPGRADE_BANNER,
UPGRADE_OR_FREE_TRIAL,
} from '~/security_configuration/components/upgrade_banner.vue';
const upgradePath = '/upgrade';
describe('UpgradeBanner component', () => {
let wrapper;
let closeSpy;
let primarySpy;
let trackingSpy;
const createComponent = (propsData) => {
closeSpy = jest.fn();
primarySpy = jest.fn();
wrapper = shallowMountExtended(UpgradeBanner, {
provide: {
@ -18,43 +25,83 @@ describe('UpgradeBanner component', () => {
propsData,
listeners: {
close: closeSpy,
primary: primarySpy,
},
});
};
const findGlBanner = () => wrapper.findComponent(GlBanner);
const expectTracking = (action, label) => {
return expect(trackingSpy).toHaveBeenCalledWith(undefined, action, {
label,
property: SECURITY_UPGRADE_BANNER,
});
};
beforeEach(() => {
createComponent();
trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
});
afterEach(() => {
wrapper.destroy();
unmockTracking();
});
it('passes the expected props to GlBanner', () => {
expect(findGlBanner().props()).toMatchObject({
title: UpgradeBanner.i18n.title,
buttonText: UpgradeBanner.i18n.buttonText,
buttonLink: upgradePath,
describe('when the component renders', () => {
it('tracks an event', () => {
expect(trackingSpy).not.toHaveBeenCalled();
createComponent();
expectTracking('display_banner', SECURITY_UPGRADE_BANNER);
});
});
it('renders the list of benefits', () => {
const wrapperText = wrapper.text();
describe('when ready', () => {
beforeEach(() => {
createComponent();
trackingSpy.mockClear();
});
expect(wrapperText).toContain('Immediately begin risk analysis and remediation');
expect(wrapperText).toContain('statistics in the merge request');
expect(wrapperText).toContain('statistics across projects');
expect(wrapperText).toContain('Runtime security metrics');
expect(wrapperText).toContain('More scan types, including Container Scanning,');
});
it('passes the expected props to GlBanner', () => {
expect(findGlBanner().props()).toMatchObject({
title: UpgradeBanner.i18n.title,
buttonText: UpgradeBanner.i18n.buttonText,
buttonLink: upgradePath,
});
});
it(`re-emits GlBanner's close event`, () => {
expect(closeSpy).not.toHaveBeenCalled();
it('renders the list of benefits', () => {
const wrapperText = wrapper.text();
wrapper.findComponent(GlBanner).vm.$emit('close');
expect(wrapperText).toContain('Immediately begin risk analysis and remediation');
expect(wrapperText).toContain('statistics in the merge request');
expect(wrapperText).toContain('statistics across projects');
expect(wrapperText).toContain('Runtime security metrics');
expect(wrapperText).toContain('More scan types, including Container Scanning,');
});
expect(closeSpy).toHaveBeenCalledTimes(1);
describe('when user interacts', () => {
it(`re-emits GlBanner's close event & tracks an event`, () => {
expect(closeSpy).not.toHaveBeenCalled();
expect(trackingSpy).not.toHaveBeenCalled();
wrapper.findComponent(GlBanner).vm.$emit('close');
expect(closeSpy).toHaveBeenCalledTimes(1);
expectTracking('dismiss_banner', SECURITY_UPGRADE_BANNER);
});
it(`re-emits GlBanner's primary event & tracks an event`, () => {
expect(primarySpy).not.toHaveBeenCalled();
expect(trackingSpy).not.toHaveBeenCalled();
wrapper.findComponent(GlBanner).vm.$emit('primary');
expect(primarySpy).toHaveBeenCalledTimes(1);
expectTracking('click_button', UPGRADE_OR_FREE_TRIAL);
});
});
});
});

View File

@ -0,0 +1,210 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe BulkImports::Common::Pipelines::LfsObjectsPipeline do
let_it_be(:portable) { create(:project) }
let_it_be(:oid) { 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' }
let(:tmpdir) { Dir.mktmpdir }
let(:entity) { create(:bulk_import_entity, :project_entity, project: portable, source_full_path: 'test') }
let(:tracker) { create(:bulk_import_tracker, entity: entity) }
let(:context) { BulkImports::Pipeline::Context.new(tracker) }
let(:lfs_dir_path) { tmpdir }
let(:lfs_json_file_path) { File.join(lfs_dir_path, 'lfs_objects.json')}
let(:lfs_file_path) { File.join(lfs_dir_path, oid)}
subject(:pipeline) { described_class.new(context) }
before do
FileUtils.mkdir_p(lfs_dir_path)
FileUtils.touch(lfs_json_file_path)
FileUtils.touch(lfs_file_path)
File.write(lfs_json_file_path, { oid => [0, 1, 2, nil] }.to_json )
allow(Dir).to receive(:mktmpdir).with('bulk_imports').and_return(tmpdir)
end
after do
FileUtils.remove_entry(tmpdir) if Dir.exist?(tmpdir)
end
describe '#run' do
it 'imports lfs objects into destination project and removes tmpdir' do
allow(pipeline)
.to receive(:extract)
.and_return(BulkImports::Pipeline::ExtractedData.new(data: [lfs_json_file_path, lfs_file_path]))
pipeline.run
expect(portable.lfs_objects.count).to eq(1)
expect(portable.lfs_objects_projects.count).to eq(4)
expect(Dir.exist?(tmpdir)).to eq(false)
end
end
describe '#extract' do
it 'downloads & extracts lfs objects filepaths' do
download_service = instance_double("BulkImports::FileDownloadService")
decompression_service = instance_double("BulkImports::FileDecompressionService")
extraction_service = instance_double("BulkImports::ArchiveExtractionService")
expect(BulkImports::FileDownloadService)
.to receive(:new)
.with(
configuration: context.configuration,
relative_url: "/#{entity.pluralized_name}/test/export_relations/download?relation=lfs_objects",
tmpdir: tmpdir,
filename: 'lfs_objects.tar.gz')
.and_return(download_service)
expect(BulkImports::FileDecompressionService).to receive(:new).with(tmpdir: tmpdir, filename: 'lfs_objects.tar.gz').and_return(decompression_service)
expect(BulkImports::ArchiveExtractionService).to receive(:new).with(tmpdir: tmpdir, filename: 'lfs_objects.tar').and_return(extraction_service)
expect(download_service).to receive(:execute)
expect(decompression_service).to receive(:execute)
expect(extraction_service).to receive(:execute)
extracted_data = pipeline.extract(context)
expect(extracted_data.data).to contain_exactly(lfs_json_file_path, lfs_file_path)
end
end
describe '#load' do
before do
allow(pipeline)
.to receive(:extract)
.and_return(BulkImports::Pipeline::ExtractedData.new(data: [lfs_json_file_path, lfs_file_path]))
end
context 'when file path is lfs json' do
it 'returns' do
filepath = File.join(tmpdir, 'lfs_objects.json')
allow(Gitlab::Json).to receive(:parse).with(filepath).and_return({})
expect { pipeline.load(context, filepath) }.not_to change { portable.lfs_objects.count }
end
end
context 'when file path is tar file' do
it 'returns' do
filepath = File.join(tmpdir, 'lfs_objects.tar')
expect { pipeline.load(context, filepath) }.not_to change { portable.lfs_objects.count }
end
end
context 'when lfs json read failed' do
it 'raises an error' do
File.write(lfs_json_file_path, 'invalid json')
expect { pipeline.load(context, lfs_file_path) }.to raise_error(BulkImports::Error, 'LFS Objects JSON read failed')
end
end
context 'when file path is being traversed' do
it 'raises an error' do
expect { pipeline.load(context, File.join(tmpdir, '..')) }.to raise_error(Gitlab::Utils::PathTraversalAttackError, 'Invalid path')
end
end
context 'when file path is not under tmpdir' do
it 'returns' do
expect { pipeline.load(context, '/home/test.txt') }.to raise_error(StandardError, 'path /home/test.txt is not allowed')
end
end
context 'when file path is symlink' do
it 'returns' do
symlink = File.join(tmpdir, 'symlink')
FileUtils.ln_s(File.join(tmpdir, lfs_file_path), symlink)
expect { pipeline.load(context, symlink) }.not_to change { portable.lfs_objects.count }
end
end
context 'when path is a directory' do
it 'returns' do
expect { pipeline.load(context, Dir.tmpdir) }.not_to change { portable.lfs_objects.count }
end
end
context 'lfs objects project' do
context 'when lfs objects json is invalid' do
context 'when oid value is not Array' do
it 'does not create lfs objects project' do
File.write(lfs_json_file_path, { oid => 'test' }.to_json )
expect { pipeline.load(context, lfs_file_path) }.not_to change { portable.lfs_objects_projects.count }
end
end
context 'when oid value is nil' do
it 'does not create lfs objects project' do
File.write(lfs_json_file_path, { oid => nil }.to_json )
expect { pipeline.load(context, lfs_file_path) }.not_to change { portable.lfs_objects_projects.count }
end
end
context 'when oid value is not allowed' do
it 'does not create lfs objects project' do
File.write(lfs_json_file_path, { oid => ['invalid'] }.to_json )
expect { pipeline.load(context, lfs_file_path) }.not_to change { portable.lfs_objects_projects.count }
end
end
context 'when repository type is duplicated' do
it 'creates only one lfs objects project' do
File.write(lfs_json_file_path, { oid => [0, 0, 1, 1, 2, 2] }.to_json )
expect { pipeline.load(context, lfs_file_path) }.to change { portable.lfs_objects_projects.count }.by(3)
end
end
end
context 'when lfs objects project fails to be created' do
it 'logs the failure' do
allow_next_instance_of(LfsObjectsProject) do |object|
allow(object).to receive(:persisted?).and_return(false)
end
expect_next_instance_of(Gitlab::Import::Logger) do |logger|
expect(logger)
.to receive(:warn)
.with(project_id: portable.id,
message: 'Failed to save lfs objects project',
errors: '', **Gitlab::ApplicationContext.current)
.exactly(4).times
end
pipeline.load(context, lfs_file_path)
end
end
end
end
describe '#after_run' do
it 'removes tmpdir' do
allow(FileUtils).to receive(:remove_entry).and_call_original
expect(FileUtils).to receive(:remove_entry).with(tmpdir).and_call_original
pipeline.after_run(nil)
expect(Dir.exist?(tmpdir)).to eq(false)
end
context 'when tmpdir does not exist' do
it 'does not attempt to remove tmpdir' do
FileUtils.remove_entry(tmpdir)
expect(FileUtils).not_to receive(:remove_entry).with(tmpdir)
pipeline.after_run(nil)
end
end
end
end

View File

@ -26,6 +26,7 @@ RSpec.describe BulkImports::Projects::Stage do
[4, BulkImports::Projects::Pipelines::ServiceDeskSettingPipeline],
[5, BulkImports::Common::Pipelines::WikiPipeline],
[5, BulkImports::Common::Pipelines::UploadsPipeline],
[5, BulkImports::Common::Pipelines::LfsObjectsPipeline],
[5, BulkImports::Projects::Pipelines::AutoDevopsPipeline],
[5, BulkImports::Projects::Pipelines::PipelineSchedulesPipeline],
[6, BulkImports::Common::Pipelines::EntityFinisher]

View File

@ -24,7 +24,6 @@ RSpec.describe 'cross-database foreign keys' do
ci_pipeline_schedules.owner_id
ci_pipeline_schedules.project_id
ci_pipelines.project_id
ci_project_monthly_usages.project_id
ci_resource_groups.project_id
ci_runner_namespaces.namespace_id
ci_running_builds.project_id

View File

@ -634,13 +634,13 @@ RSpec.describe UsersController do
end
describe 'GET #exists' do
before do
sign_in(user)
allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
end
context 'when user exists' do
before do
sign_in(user)
allow(::Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
end
it 'returns JSON indicating the user exists' do
get user_exists_url user.username
@ -661,6 +661,15 @@ RSpec.describe UsersController do
end
context 'when the user does not exist' do
it 'will not show a signup page if registration is disabled' do
stub_application_setting(signup_enabled: false)
get user_exists_url 'foo'
expected_json = { error: "You must be authenticated to access this path." }.to_json
expect(response).to have_gitlab_http_status(:unauthorized)
expect(response.body).to eq(expected_json)
end
it 'returns JSON indicating the user does not exist' do
get user_exists_url 'foo'