Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-02-22 18:10:55 +00:00
parent 232e7582b0
commit 57f6fa3cd7
113 changed files with 1376 additions and 1100 deletions

View File

@ -1 +1 @@
c45afa70f5bd9723f0836ff228a11bc896c45511
771df64aaf511cc3c64d7b55aee2d961941bfdab

View File

@ -84,6 +84,11 @@ export default {
cancelActionProps() {
return {
text: this.$options.translations.cancelActionLabel,
attributes: [
{
category: 'secondary',
},
],
};
},
canRegenerateInstanceId() {
@ -120,11 +125,11 @@ export default {
<template>
<gl-modal
:modal-id="modalId"
:action-cancel="cancelActionProps"
:action-primary="regenerateInstanceIdActionProps"
@canceled="clearState"
:action-primary="cancelActionProps"
:action-secondary="regenerateInstanceIdActionProps"
@secondary.prevent="rotateToken"
@hide="clearState"
@primary.prevent="rotateToken"
@primary="clearState"
>
<template #modal-title>
{{ $options.translations.modalTitle }}

View File

@ -266,6 +266,7 @@ class GfmAutoComplete {
},
// eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${username}',
limit: 10,
searchKey: 'search',
alwaysHighlightFirst: true,
skipSpecialCharacterTest: true,
@ -311,6 +312,38 @@ class GfmAutoComplete {
return data;
},
sorter(query, items) {
if (!query) {
return items;
}
// Disable auto-selecting the loading icon
this.setting.highlightFirst = this.setting.alwaysHighlightFirst;
if (GfmAutoComplete.isLoading(items)) {
this.setting.highlightFirst = false;
return items;
}
const lowercaseQuery = query.toLowerCase();
const members = items.slice();
const { nameOrUsernameStartsWith, nameOrUsernameIncludes } = GfmAutoComplete.Members;
return members.sort((a, b) => {
if (nameOrUsernameStartsWith(a, lowercaseQuery)) {
return -1;
}
if (nameOrUsernameStartsWith(b, lowercaseQuery)) {
return 1;
}
if (nameOrUsernameIncludes(a, lowercaseQuery)) {
return -1;
}
if (nameOrUsernameIncludes(b, lowercaseQuery)) {
return 1;
}
return 0;
});
},
},
});
}
@ -772,6 +805,14 @@ GfmAutoComplete.Members = {
title,
)}${availabilityStatus}</small> ${icon}</li>`;
},
nameOrUsernameStartsWith(member, query) {
// `member.search` is a name:username string like `MargeSimpson msimpson`
return member.search.split(' ').some((name) => name.toLowerCase().startsWith(query));
},
nameOrUsernameIncludes(member, query) {
// `member.search` is a name:username string like `MargeSimpson msimpson`
return member.search.toLowerCase().includes(query);
},
};
GfmAutoComplete.Labels = {
templateFunction(color, title) {

View File

@ -122,10 +122,8 @@ export function createResolvers({ endpoints, sourceUrl, GroupsManager = SourceGr
});
groupManager.startImport({ group, importId: response.data.id });
} catch (e) {
createFlash({
message: s__('BulkImport|Importing the group failed'),
});
const message = e?.response?.data?.error ?? s__('BulkImport|Importing the group failed');
createFlash({ message });
groupManager.setImportStatus(group, STATUSES.NONE);
throw e;
}

View File

@ -1,18 +1,3 @@
import Vue from 'vue';
import Vuex from 'vuex';
import UserList from '~/user_lists/components/user_list.vue';
import createStore from '~/user_lists/store/show';
import featureFlagsUserListInit from '~/projects/feature_flags_user_lists/show/index';
Vue.use(Vuex);
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('js-edit-user-list');
return new Vue({
el,
store: createStore(el.dataset),
render(h) {
const { emptyStatePath } = el.dataset;
return h(UserList, { props: { emptyStatePath } });
},
});
});
featureFlagsUserListInit();

View File

@ -12,11 +12,9 @@
* 4. Commit widget
*/
import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import $ from 'jquery';
import { deprecatedCreateFlash as Flash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { PIPELINES_TABLE } from '../../constants';
import eventHub from '../../event_hub';
import JobItem from '../graph/job_item.vue';
@ -31,19 +29,16 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
stage: {
type: Object,
required: true,
},
updateDropdown: {
type: Boolean,
required: false,
default: false,
},
type: {
type: String,
required: false,
@ -57,11 +52,6 @@ export default {
};
},
computed: {
isCiMiniPipelineGlDropdown() {
// Feature flag ci_mini_pipeline_gl_dropdown
// See more at https://gitlab.com/gitlab-org/gitlab/-/issues/300400
return this.glFeatures?.ciMiniPipelineGlDropdown;
},
triggerButtonClass() {
return `ci-status-icon-${this.stage.status.group}`;
},
@ -76,24 +66,12 @@ export default {
}
},
},
updated() {
if (!this.isCiMiniPipelineGlDropdown && this.dropdownContent.length) {
this.stopDropdownClickPropagation();
}
},
methods: {
onShowDropdown() {
eventHub.$emit('clickedDropdown');
this.isLoading = true;
this.fetchJobs();
},
onClickStage() {
if (!this.isDropdownOpen()) {
eventHub.$emit('clickedDropdown');
this.isLoading = true;
this.fetchJobs();
}
},
fetchJobs() {
axios
.get(this.stage.dropdown_path)
@ -102,133 +80,60 @@ export default {
this.isLoading = false;
})
.catch(() => {
if (this.isCiMiniPipelineGlDropdown) {
this.$refs.stageGlDropdown.hide();
} else {
this.closeDropdown();
}
this.$refs.stageGlDropdown.hide();
this.isLoading = false;
Flash(__('Something went wrong on our end.'));
});
},
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*
* Note: This should be removed once ci_mini_pipeline_gl_dropdown FF is removed as true.
*/
stopDropdownClickPropagation() {
$(
'.js-builds-dropdown-list button, .js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item',
this.$el,
).on('click', (e) => {
e.stopPropagation();
});
},
closeDropdown() {
if (this.isDropdownOpen()) {
$(this.$refs.dropdown).dropdown('toggle');
}
},
isDropdownOpen() {
return this.$el.classList.contains('show');
},
pipelineActionRequestComplete() {
if (this.type === PIPELINES_TABLE) {
// warn the table to update
// warn the pipelines table to update
eventHub.$emit('refreshPipelinesTable');
return;
}
// close the dropdown in mr widget
if (this.isCiMiniPipelineGlDropdown) {
this.$refs.stageGlDropdown.hide();
} else {
$(this.$refs.dropdown).dropdown('toggle');
}
// close the dropdown in MR widget
this.$refs.stageGlDropdown.hide();
},
},
};
</script>
<template>
<div class="dropdown">
<gl-dropdown
v-if="isCiMiniPipelineGlDropdown"
ref="stageGlDropdown"
v-gl-tooltip.hover
data-testid="mini-pipeline-graph-dropdown"
:title="stage.title"
variant="link"
:lazy="true"
:popper-opts="{ placement: 'bottom' }"
:toggle-class="['mini-pipeline-graph-gl-dropdown-toggle', triggerButtonClass]"
menu-class="mini-pipeline-graph-dropdown-menu"
@show="onShowDropdown"
>
<template #button-content>
<span class="gl-pointer-events-none">
<gl-icon :name="borderlessIcon" />
</span>
</template>
<gl-loading-icon v-if="isLoading" />
<ul
v-else
class="js-builds-dropdown-list scrollable-menu"
data-testid="mini-pipeline-graph-dropdown-menu-list"
>
<li v-for="job in dropdownContent" :key="job.id">
<job-item
:dropdown-length="dropdownContent.length"
:job="job"
css-class-job-name="mini-pipeline-graph-dropdown-item"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
</ul>
</gl-dropdown>
<template v-else>
<button
id="stageDropdown"
ref="dropdown"
v-gl-tooltip.hover
:class="triggerButtonClass"
:title="stage.title"
class="mini-pipeline-graph-dropdown-toggle"
data-testid="mini-pipeline-graph-dropdown-toggle"
data-toggle="dropdown"
data-display="static"
type="button"
aria-haspopup="true"
aria-expanded="false"
@click="onClickStage"
>
<span :aria-label="stage.title" aria-hidden="true" class="gl-pointer-events-none">
<gl-icon :name="borderlessIcon" />
</span>
</button>
<div
class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container"
aria-labelledby="stageDropdown"
>
<gl-loading-icon v-if="isLoading" />
<ul v-else class="js-builds-dropdown-list scrollable-menu">
<li v-for="job in dropdownContent" :key="job.id">
<job-item
:dropdown-length="dropdownContent.length"
:job="job"
css-class-job-name="mini-pipeline-graph-dropdown-item"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
</ul>
</div>
<gl-dropdown
ref="stageGlDropdown"
v-gl-tooltip.hover
data-testid="mini-pipeline-graph-dropdown"
:title="stage.title"
variant="link"
:lazy="true"
:popper-opts="{ placement: 'bottom' }"
:toggle-class="['mini-pipeline-graph-dropdown-toggle', triggerButtonClass]"
menu-class="mini-pipeline-graph-dropdown-menu"
@show="onShowDropdown"
>
<template #button-content>
<span class="gl-pointer-events-none">
<gl-icon :name="borderlessIcon" />
</span>
</template>
</div>
<gl-loading-icon v-if="isLoading" />
<ul
v-else
class="js-builds-dropdown-list scrollable-menu"
data-testid="mini-pipeline-graph-dropdown-menu-list"
>
<li v-for="job in dropdownContent" :key="job.id">
<job-item
:dropdown-length="dropdownContent.length"
:job="job"
css-class-job-name="mini-pipeline-graph-dropdown-item"
@pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
</ul>
</gl-dropdown>
</template>

View File

@ -0,0 +1,23 @@
import Vue from 'vue';
import Vuex from 'vuex';
import UserList from '~/user_lists/components/user_list.vue';
import createStore from '~/user_lists/store/show';
Vue.use(Vuex);
export default function featureFlagsUserListInit() {
const el = document.getElementById('js-edit-user-list');
if (!el) {
return null;
}
return new Vue({
el,
store: createStore(el.dataset),
render(h) {
const { emptyStatePath } = el.dataset;
return h(UserList, { props: { emptyStatePath } });
},
});
}

View File

@ -37,7 +37,6 @@ export default {
v-for="(listItem, index) in images"
:key="index"
:item="listItem"
:first="index === 0"
:metadata-loading="metadataLoading"
@delete="$emit('delete', $event)"
/>

View File

@ -9,7 +9,6 @@ import {
LIST_INTRO_TEXT,
EXPIRATION_POLICY_WILL_RUN_IN,
EXPIRATION_POLICY_DISABLED_TEXT,
EXPIRATION_POLICY_DISABLED_MESSAGE,
} from '../../constants/index';
export default {
@ -34,11 +33,6 @@ export default {
default: '',
required: false,
},
expirationPolicyHelpPagePath: {
type: String,
default: '',
required: false,
},
hideExpirationPolicyData: {
type: Boolean,
required: false,
@ -79,19 +73,8 @@ export default {
? sprintf(EXPIRATION_POLICY_WILL_RUN_IN, { time: this.timeTillRun })
: EXPIRATION_POLICY_DISABLED_TEXT;
},
showExpirationPolicyTip() {
return (
!this.expirationPolicyEnabled && this.imagesCount > 0 && !this.hideExpirationPolicyData
);
},
infoMessages() {
const base = [{ text: LIST_INTRO_TEXT, link: this.helpPagePath }];
return this.showExpirationPolicyTip
? [
...base,
{ text: EXPIRATION_POLICY_DISABLED_MESSAGE, link: this.expirationPolicyHelpPagePath },
]
: base;
return [{ text: LIST_INTRO_TEXT, link: this.helpPagePath }];
},
},
};

View File

@ -6,9 +6,6 @@ export const EXPIRATION_POLICY_WILL_RUN_IN = s__(
export const EXPIRATION_POLICY_DISABLED_TEXT = s__(
'ContainerRegistry|Expiration policy is disabled',
);
export const EXPIRATION_POLICY_DISABLED_MESSAGE = s__(
'ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}',
);
export const DELETE_ALERT_TITLE = s__('ContainerRegistry|Some tags were not deleted');
export const DELETE_ALERT_LINK_TEXT = s__(
'ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}',

View File

@ -288,7 +288,6 @@ export default {
:images-count="containerRepositoriesCount"
:expiration-policy="config.expirationPolicy"
:help-page-path="config.helpPagePath"
:expiration-policy-help-page-path="config.expirationPolicyHelpPagePath"
:hide-expiration-policy-data="config.isGroupPage"
>
<template #commands>

View File

@ -54,7 +54,7 @@ export default {
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1"
:class="optionalClasses"
>
<div class="gl-display-flex gl-align-items-center gl-py-3">
<div class="gl-display-flex gl-align-items-center gl-py-3 gl-px-5">
<div
v-if="$slots['left-action']"
class="gl-w-7 gl-display-none gl-sm-display-flex gl-justify-content-start gl-pl-2"

View File

@ -11,7 +11,12 @@
.dropdown-menu.show {
// Make the dropdown a little wider and longer than usual
// since it contains quite a bit of content.
overflow: hidden;
width: 20rem;
max-height: $dropdown-max-height-lg;
&,
.gl-new-dropdown-inner {
max-height: $dropdown-max-height-lg;
}
}
}

View File

@ -67,8 +67,7 @@
// Mini Pipelines
.stage-cell {
.mini-pipeline-graph-dropdown-toggle,
.mini-pipeline-graph-gl-dropdown-toggle {
.mini-pipeline-graph-dropdown-toggle {
svg {
height: $ci-action-icon-size;
width: $ci-action-icon-size;
@ -138,14 +137,16 @@
}
}
// Dropdown button in mini pipeline graph
// Commit mini pipeline (HAML)
button.mini-pipeline-graph-dropdown-toggle,
// As the `mini-pipeline-item` mixin specificity is lower
// than the toggle of dropdown with 'variant="link"' we add
// classes ".gl-button.btn-link" to make it more specific.
// Once FF ci_mini_pipeline_gl_dropdown is removed, the `mini-pipeline-item`
// itself could increase its specificity to simplify this selector
button.gl-button.btn-link.mini-pipeline-graph-gl-dropdown-toggle {
// GlDropdown mini pipeline (Vue)
// As the `mini-pipeline-item` mixin specificity is lower
// than the toggle of dropdown with 'variant="link"' we add
// classes ".gl-button.btn-link" to make it more specific
// and avoid having the size overriden
//
// See https://gitlab.com/gitlab-org/gitlab/-/issues/320737
button.gl-button.btn-link.mini-pipeline-graph-dropdown-toggle {
@include mini-pipeline-item();
}

View File

@ -226,10 +226,6 @@ $tabs-holder-z-index: 250;
}
}
.mini-pipeline-graph-dropdown-toggle {
vertical-align: top;
}
.normal {
flex: 1;
flex-basis: auto;
@ -982,15 +978,15 @@ $tabs-holder-z-index: 250;
line-height: initial;
}
.mini-pipeline-graph-dropdown-toggle,
.stage-cell .mini-pipeline-graph-dropdown-toggle svg,
// As the `mini-pipeline-item` mixin specificity is lower
// than the toggle of dropdown with 'variant="link"' we add
// classes ".gl-button.btn-link" to make it more specific.
// Once FF ci_mini_pipeline_gl_dropdown is removed, the `mini-pipeline-item`
// itself could increase its specificity to simplify this selector
button.gl-button.btn-link.mini-pipeline-graph-gl-dropdown-toggle,
.stage-cell button.gl-button.btn-link.mini-pipeline-graph-gl-dropdown-toggle svg {
// GlDropdown mini pipeline (Vue)
// As the `mini-pipeline-item` mixin specificity is lower
// than the toggle of dropdown with 'variant="link"' we add
// classes ".gl-button.btn-link" to make it more specific
// and avoid having the size overriden
//
// See https://gitlab.com/gitlab-org/gitlab/-/issues/320737
button.gl-button.btn-link.mini-pipeline-graph-dropdown-toggle,
.stage-cell button.gl-button.btn-link.mini-pipeline-graph-dropdown-toggle svg {
height: $ci-action-icon-size-lg;
width: $ci-action-icon-size-lg;
}

View File

@ -16,7 +16,18 @@ class Groups::DependencyProxyForContainersController < Groups::ApplicationContro
result = DependencyProxy::FindOrCreateManifestService.new(group, image, tag, token).execute
if result[:status] == :success
send_upload(result[:manifest].file)
response.headers['Docker-Content-Digest'] = result[:manifest].digest
response.headers['Content-Length'] = result[:manifest].size
response.headers['Docker-Distribution-Api-Version'] = DependencyProxy::DISTRIBUTION_API_VERSION
response.headers['Etag'] = "\"#{result[:manifest].digest}\""
content_type = result[:manifest].content_type
send_upload(
result[:manifest].file,
proxy: true,
redirect_params: { query: { 'response-content-type' => content_type } },
send_params: { type: content_type }
)
else
render status: result[:http_status], json: result[:message]
end

View File

@ -37,8 +37,13 @@ class Import::BulkImportsController < ApplicationController
end
def create
result = BulkImportService.new(current_user, create_params, credentials).execute
render json: result.to_json(only: [:id])
response = BulkImportService.new(current_user, create_params, credentials).execute
if response.success?
render json: response.payload.to_json(only: [:id])
else
render json: { error: response.message }, status: response.http_status
end
end
def realtime_changes

View File

@ -18,9 +18,6 @@ class Projects::CommitController < Projects::ApplicationController
before_action :define_commit_vars, only: [:show, :diff_for_path, :diff_files, :pipelines, :merge_requests]
before_action :define_note_vars, only: [:show, :diff_for_path, :diff_files]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
before_action only: [:pipelines] do
push_frontend_feature_flag(:ci_mini_pipeline_gl_dropdown, @project, type: :development, default_enabled: :yaml)
end
BRANCH_SEARCH_LIMIT = 1000
COMMIT_DIFFS_PER_PAGE = 75

View File

@ -44,7 +44,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:suggestions_custom_commit, @project, default_enabled: true)
push_frontend_feature_flag(:local_file_reviews, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:ci_mini_pipeline_gl_dropdown, @project, type: :development, default_enabled: :yaml)
record_experiment_user(:invite_members_version_a)
record_experiment_user(:invite_members_version_b)

View File

@ -17,7 +17,6 @@ class Projects::PipelinesController < Projects::ApplicationController
push_frontend_feature_flag(:new_pipeline_form, project, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_pipeline_details, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:graphql_pipeline_details_users, current_user, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:ci_mini_pipeline_gl_dropdown, project, type: :development, default_enabled: :yaml)
push_frontend_feature_flag(:jira_for_vulnerabilities, project, type: :development, default_enabled: :yaml)
end
before_action :ensure_pipeline, only: [:show]

View File

@ -1,6 +1,7 @@
# frozen_string_literal: true
module DependencyProxy
URL_SUFFIX = '/dependency_proxy/containers'
DISTRIBUTION_API_VERSION = 'registry/2.0'
def self.table_name_prefix
'dependency_proxy_'

View File

@ -12,5 +12,10 @@ class DependencyProxy::Manifest < ApplicationRecord
mount_file_store_uploader DependencyProxy::FileUploader
scope :find_or_initialize_by_file_name, ->(file_name) { find_or_initialize_by(file_name: file_name) }
def self.find_or_initialize_by_file_name_or_digest(file_name:, digest:)
result = find_by(file_name: file_name) || find_by(digest: digest)
return result if result
new(file_name: file_name, digest: digest)
end
end

View File

@ -388,7 +388,7 @@ class Group < Namespace
end
def user_ids_for_project_authorizations
members_with_parents.pluck(:user_id)
members_with_parents.pluck(Arel.sql('DISTINCT members.user_id'))
end
def self_and_ancestors_ids

View File

@ -345,7 +345,7 @@ class Project < ApplicationRecord
has_many :daily_build_group_report_results, class_name: 'Ci::DailyBuildGroupReportResult'
has_many :repository_storage_moves, class_name: 'ProjectRepositoryStorageMove', inverse_of: :container
has_many :repository_storage_moves, class_name: 'Projects::RepositoryStorageMove', inverse_of: :container
has_many :webide_pipelines, -> { webide_source }, class_name: 'Ci::Pipeline', inverse_of: :project
has_many :reviews, inverse_of: :project

View File

@ -1,34 +1,13 @@
# frozen_string_literal: true
# ProjectRepositoryStorageMove are details of repository storage moves for a
# project. For example, moving a project to another gitaly node to help
# balance storage capacity.
class ProjectRepositoryStorageMove < ApplicationRecord
extend ::Gitlab::Utils::Override
include RepositoryStorageMovable
belongs_to :container, class_name: 'Project', inverse_of: :repository_storage_moves, foreign_key: :project_id
alias_attribute :project, :container
scope :with_projects, -> { includes(container: :route) }
override :update_repository_storage
def update_repository_storage(new_storage)
container.update_column(:repository_storage, new_storage)
end
override :schedule_repository_storage_update_worker
def schedule_repository_storage_update_worker
ProjectUpdateRepositoryStorageWorker.perform_async(
project_id,
destination_storage_name,
id
)
end
private
override :error_key
def error_key
:project
end
# This is a compatibility class to avoid calling a non-existent
# class from sidekiq during deployment.
#
# This class was moved to a namespace in https://gitlab.com/gitlab-org/gitlab/-/issues/299853.
# we cannot remove this class entirely because there can be jobs
# referencing it.
#
# We can get rid of this class in 14.0
# https://gitlab.com/gitlab-org/gitlab/-/issues/322393
class ProjectRepositoryStorageMove < Projects::RepositoryStorageMove
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
# Projects::RepositoryStorageMove are details of repository storage moves for a
# project. For example, moving a project to another gitaly node to help
# balance storage capacity.
module Projects
class RepositoryStorageMove < ApplicationRecord
extend ::Gitlab::Utils::Override
include RepositoryStorageMovable
self.table_name = 'project_repository_storage_moves'
belongs_to :container, class_name: 'Project', inverse_of: :repository_storage_moves, foreign_key: :project_id
alias_attribute :project, :container
scope :with_projects, -> { includes(container: :route) }
override :update_repository_storage
def update_repository_storage(new_storage)
container.update_column(:repository_storage, new_storage)
end
override :schedule_repository_storage_update_worker
def schedule_repository_storage_update_worker
Projects::UpdateRepositoryStorageWorker.perform_async(
project_id,
destination_storage_name,
id
)
end
private
override :error_key
def error_key
:project
end
end
end

View File

@ -39,7 +39,12 @@ class BulkImportService
BulkImportWorker.perform_async(bulk_import.id)
bulk_import
ServiceResponse.success(payload: bulk_import)
rescue ActiveRecord::RecordInvalid => e
ServiceResponse.error(
message: e.message,
http_status: :unprocessable_entity
)
end
private

View File

@ -13,7 +13,7 @@ module DependencyProxy
def execute
@manifest = @group.dependency_proxy_manifests
.find_or_initialize_by_file_name(@file_name)
.find_or_initialize_by_file_name_or_digest(file_name: @file_name, digest: @tag)
head_result = DependencyProxy::HeadManifestService.new(@image, @tag, @token).execute
@ -30,6 +30,7 @@ module DependencyProxy
def pull_new_manifest
DependencyProxy::PullManifestService.new(@image, @tag, @token).execute_with_manifest do |new_manifest|
@manifest.update!(
content_type: new_manifest[:content_type],
digest: new_manifest[:digest],
file: new_manifest[:file],
size: new_manifest[:file].size
@ -38,7 +39,9 @@ module DependencyProxy
end
def cached_manifest_matches?(head_result)
@manifest && @manifest.digest == head_result[:digest]
return false if head_result[:status] == :error
@manifest && @manifest.digest == head_result[:digest] && @manifest.content_type == head_result[:content_type]
end
def respond

View File

@ -2,6 +2,8 @@
module DependencyProxy
class HeadManifestService < DependencyProxy::BaseService
ACCEPT_HEADERS = ::ContainerRegistry::Client::ACCEPTED_TYPES.join(',')
def initialize(image, tag, token)
@image = image
@tag = tag
@ -9,10 +11,10 @@ module DependencyProxy
end
def execute
response = Gitlab::HTTP.head(manifest_url, headers: auth_headers)
response = Gitlab::HTTP.head(manifest_url, headers: auth_headers.merge(Accept: ACCEPT_HEADERS))
if response.success?
success(digest: response.headers['docker-content-digest'])
success(digest: response.headers['docker-content-digest'], content_type: response.headers['content-type'])
else
error(response.body, response.code)
end

View File

@ -11,7 +11,7 @@ module DependencyProxy
def execute_with_manifest
raise ArgumentError, 'Block must be provided' unless block_given?
response = Gitlab::HTTP.get(manifest_url, headers: auth_headers)
response = Gitlab::HTTP.get(manifest_url, headers: auth_headers.merge(Accept: ::ContainerRegistry::Client::ACCEPTED_TYPES.join(',')))
if response.success?
file = Tempfile.new
@ -20,7 +20,7 @@ module DependencyProxy
file.write(response)
file.flush
yield(success(file: file, digest: response.headers['docker-content-digest']))
yield(success(file: file, digest: response.headers['docker-content-digest'], content_type: response.headers['content-type']))
ensure
file.close
file.unlink

View File

@ -25,7 +25,7 @@ module Projects
override :schedule_bulk_worker_klass
def self.schedule_bulk_worker_klass
::ProjectScheduleBulkRepositoryShardMovesWorker
::Projects::ScheduleBulkRepositoryShardMovesWorker
end
end
end

View File

@ -3,6 +3,7 @@
class DependencyProxy::FileUploader < GitlabUploader
include ObjectStorage::Concern
before :cache, :set_content_type
storage_options Gitlab.config.dependency_proxy
alias_method :upload, :model
@ -17,6 +18,17 @@ class DependencyProxy::FileUploader < GitlabUploader
private
# Docker manifests return a custom content type
# GCP will only use the content-type that is stored with the file
# and will not allow it to be overwritten when downloaded
# so we must store the custom content type in object storage.
# This does not apply to DependencyProxy::Blob uploads.
def set_content_type(file)
return unless model.class == DependencyProxy::Manifest
file.content_type = model.content_type
end
def dynamic_segment
Gitlab::HashedPath.new('dependency_proxy', model.group_id, 'files', model.id, root_hash: model.group_id)
end

View File

@ -12,7 +12,6 @@
"registry_host_url_with_port" => escape_once(registry_config.host_port),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
"is_admin": current_user&.admin.to_s,
is_group_page: "true",
"group_path": @group.full_path,

View File

@ -15,7 +15,6 @@
"expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
"project_path": @project.full_path,
"gid_prefix": container_repository_gid_prefix,
"is_admin": current_user&.admin.to_s,

View File

@ -2020,6 +2020,22 @@
:weight: 1
:idempotent:
:tags: []
- :name: projects_schedule_bulk_repository_shard_moves
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :throttled
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: projects_update_repository_storage
:feature_category: :gitaly
:has_external_dependencies:
:urgency: :throttled
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: prometheus_create_default_alerts
:feature_category: :incident_management
:has_external_dependencies:

View File

@ -1,13 +1,15 @@
# frozen_string_literal: true
class ProjectScheduleBulkRepositoryShardMovesWorker
include ApplicationWorker
# This is a compatibility class to avoid calling a non-existent
# class from sidekiq during deployment.
#
# This class was moved to a namespace in https://gitlab.com/gitlab-org/gitlab/-/issues/299853.
# we cannot remove this class entirely because there can be jobs
# referencing it.
#
# We can get rid of this class in 14.0
# https://gitlab.com/gitlab-org/gitlab/-/issues/322393
class ProjectScheduleBulkRepositoryShardMovesWorker < Projects::ScheduleBulkRepositoryShardMovesWorker
idempotent!
feature_category :gitaly
urgency :throttled
def perform(source_storage_name, destination_storage_name = nil)
Projects::ScheduleBulkRepositoryShardMovesService.new.execute(source_storage_name, destination_storage_name)
end
end

View File

@ -1,23 +1,15 @@
# frozen_string_literal: true
class ProjectUpdateRepositoryStorageWorker # rubocop:disable Scalability/IdempotentWorker
extend ::Gitlab::Utils::Override
include UpdateRepositoryStorageWorker
private
override :find_repository_storage_move
def find_repository_storage_move(repository_storage_move_id)
ProjectRepositoryStorageMove.find(repository_storage_move_id)
end
override :find_container
def find_container(container_id)
Project.find(container_id)
end
override :update_repository_storage
def update_repository_storage(repository_storage_move)
::Projects::UpdateRepositoryStorageService.new(repository_storage_move).execute
end
# This is a compatibility class to avoid calling a non-existent
# class from sidekiq during deployment.
#
# This class was moved to a namespace in https://gitlab.com/gitlab-org/gitlab/-/issues/299853.
# we cannot remove this class entirely because there can be jobs
# referencing it.
#
# We can get rid of this class in 14.0
# https://gitlab.com/gitlab-org/gitlab/-/issues/322393
class ProjectUpdateRepositoryStorageWorker < Projects::UpdateRepositoryStorageWorker
idempotent!
urgency :throttled
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Projects
class ScheduleBulkRepositoryShardMovesWorker
include ApplicationWorker
idempotent!
feature_category :gitaly
urgency :throttled
def perform(source_storage_name, destination_storage_name = nil)
Projects::ScheduleBulkRepositoryShardMovesService.new.execute(source_storage_name, destination_storage_name)
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Projects
class UpdateRepositoryStorageWorker # rubocop:disable Scalability/IdempotentWorker
extend ::Gitlab::Utils::Override
include ::UpdateRepositoryStorageWorker
private
override :find_repository_storage_move
def find_repository_storage_move(repository_storage_move_id)
::Projects::RepositoryStorageMove.find(repository_storage_move_id)
end
override :find_container
def find_container(container_id)
Project.find(container_id)
end
override :update_repository_storage
def update_repository_storage(repository_storage_move)
::Projects::UpdateRepositoryStorageService.new(repository_storage_move).execute
end
end
end

View File

@ -0,0 +1,5 @@
---
title: Improve at.js members autocomplete matching
merge_request: 54681
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Remove Expiration Policy text from container registry header
merge_request: 54665
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Refine Registry Lists and Search Bar UI
merge_request: 54549
author:
type: changed

View File

@ -0,0 +1,6 @@
---
title: Consider only distinct user ids for project authorizations refresh jobs for
group members
merge_request: 54697
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Fix double scrollbar in ref selector dropdown
merge_request: 54719
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Change the order of action buttons in the configure feature flags modal
merge_request: 54731
author:
type: changed

View File

@ -1,8 +0,0 @@
---
name: ci_mini_pipeline_gl_dropdown
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52821
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/300400
milestone: '13.9'
type: development
group: group::continuous integration
default_enabled: true

View File

@ -1,9 +1,5 @@
# frozen_string_literal: true
def max_puma_workers
Puma.cli_config.options[:workers].to_i
end
if Gitlab::Runtime.puma? && !Gitlab::Runtime.puma_in_clustered_mode?
raise 'Puma is only supported in Clustered mode (workers > 0)' if Gitlab.com?

View File

@ -282,6 +282,10 @@
- 1
- - projects_git_garbage_collect
- 1
- - projects_schedule_bulk_repository_shard_moves
- 1
- - projects_update_repository_storage
- 1
- - prometheus_create_default_alerts
- 1
- - propagate_integration

View File

@ -10,16 +10,16 @@ class BackfillUpdatedAtAfterRepositoryStorageMove < ActiveRecord::Migration[6.0]
disable_ddl_transaction!
class ProjectRepositoryStorageMove < ActiveRecord::Base
class RepositoryStorageMove < ActiveRecord::Base
include EachBatch
self.table_name = 'project_repository_storage_moves'
end
def up
ProjectRepositoryStorageMove.reset_column_information
RepositoryStorageMove.reset_column_information
ProjectRepositoryStorageMove.select(:project_id).distinct.each_batch(of: BATCH_SIZE, column: :project_id) do |batch, index|
RepositoryStorageMove.select(:project_id).distinct.each_batch(of: BATCH_SIZE, column: :project_id) do |batch, index|
migrate_in(
INTERVAL * index,
MIGRATION_CLASS,

View File

@ -406,6 +406,8 @@ all the App nodes and Sidekiq nodes.
#### Using Pages with reduced authentication scope
> [Introduced](https://gitlab.com/gitlab-org/gitlab-pages/-/merge_requests/423) in GitLab 13.10.
By default, the Pages daemon uses the `api` scope to authenticate. You can configure this. For
example, this reduces the scope to `read_api` in `/etc/gitlab/gitlab.rb`:

View File

@ -125,7 +125,9 @@ they have the following privileges:
## Deployment-only access to protected environments
Users granted access to a protected environment, but not push or merge access
to the branch deployed to it, are only granted access to deploy the environment.
to the branch deployed to it, are only granted access to deploy the environment. An individual in a
group with the Reporter permission, or in groups added to the project with Reporter permissions,
appears in the dropdown menu for deployment-only access.
Note that deployment-only access is the only possible access level for users with
[Reporter permissions](../../user/permissions.md).

View File

@ -59,7 +59,7 @@ Before you push your changes, Lefthook automatically runs the following checks:
- ES lint: Run `yarn run internal:eslint` checks (with the [`.eslintrc.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.eslintrc.yml) configuration) on the modified `*.{js,vue}` files. Tags: `frontend`, `style`.
- HAML lint: Run `bundle exec haml-lint` checks (with the [`.haml-lint.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.haml-lint.yml) configuration) on the modified `*.html.haml` files. Tags: `view`, `haml`, `style`.
- Markdown lint: Run `yarn markdownlint` checks on the modified `*.md` files. Tags: `documentation`, `style`.
- SCSS lint: Run `yarn stylelint` checks (with the [`.stylelintrc`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.stylelintrc) configuration) on the modified `*.scss{,.css}` files. Tags: `stylesheet`, `css`, `style`.
- SCSS lint: Run `yarn lint:stylelint` checks (with the [`.stylelintrc`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.stylelintrc) configuration) on the modified `*.scss{,.css}` files. Tags: `stylesheet`, `css`, `style`.
- RuboCop: Run `bundle exec rubocop` checks (with the [`.rubocop.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.rubocop.yml) configuration) on the modified `*.rb` files. Tags: `backend`, `style`.
- Vale: Run `vale` checks (with the [`.vale.ini`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.vale.ini) configuration) on the modified `*.md` files. Tags: `documentation`, `style`.

View File

@ -601,6 +601,7 @@ Follow these guidelines for punctuation:
| Rule | Example |
|------------------------------------------------------------------|--------------------------------------------------------|
| Avoid semicolons. Use two sentences instead. | _That's the way that the world goes 'round. You're up one day and the next you're down._
| Always end full sentences with a period. | _For a complete overview, read through this document._ |
| Always add a space after a period when beginning a new sentence. | _For a complete overview, check this doc. For other references, check out this guide._ |
| Do not use double spaces. (Tested in [`SentenceSpacing.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/SentenceSpacing.yml).) | --- |

View File

@ -135,7 +135,7 @@ Before adding a new variable for a color or a size, guarantee:
We use [stylelint](https://stylelint.io) to check for style guide conformity. It uses the
ruleset in `.stylelintrc` and rules from [our SCSS configuration](https://gitlab.com/gitlab-org/frontend/gitlab-stylelint-config). `.stylelintrc` is located in the home directory of the project.
To check if any warnings are produced by your changes, run `yarn stylelint` in the GitLab directory. Stylelint also runs in GitLab CI/CD to
To check if any warnings are produced by your changes, run `yarn lint:stylelint` in the GitLab directory. Stylelint also runs in GitLab CI/CD to
catch any warnings.
If the Rake task is throwing warnings you don't understand, SCSS Lint's

View File

@ -6,20 +6,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Packages
This document guides you through adding another [package management system](../administration/packages/index.md) support to GitLab.
This document guides you through adding support to GitLab for a new a [package management system](../administration/packages/index.md).
See already supported package types in [Packages documentation](../administration/packages/index.md)
See the already supported formats in the [Packages & Registries documentation](../user/packages/index.md)
Since GitLab packages' UI is pretty generic, it is possible to add basic new
package system support with solely backend changes. This guide is superficial and does
not cover the way the code should be written. However, you can find a good example
by looking at the following merge requests:
It is possible to add a new format with only backend changes.
This guide is superficial and does not cover the way the code should be written.
However, you can find a good example by looking at the following merge requests:
- [npm registry support](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8673).
- [Maven repository](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6607).
- [Composer repository for PHP dependencies](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22415).
- [Terraform modules registry](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/18834).
- [Instance-level endpoint for Maven repository](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8757).
- [npm registry support](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8673)
- [Maven repository](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6607)
- [Instance-level API for Maven repository](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/8757)
- [NuGet group-level API](https://gitlab.com/gitlab-org/gitlab/-/issues/36423)
## General information
@ -60,26 +58,13 @@ project are visible. Alternatively, a group-level endpoint may be used to allow
within a given group. Lastly, an instance-level endpoint can be used to allow visibility to all packages within an
entire GitLab instance.
Using group and project level endpoints allows for more flexibility in package naming, however, more remotes
have to be managed. Using instance level endpoints requires [stricter naming conventions](#naming-conventions).
As an MVC, we recommend beginning with a project-level endpoint. A typical iteration plan for remote hierarchies is to go from:
The current state of existing package registries availability is:
- Publish and install in a project
- Install from a group
- Publish and install in an Instance (this is for Self-Managed customers)
| Repository Type | Project Level | Group Level | Instance Level |
|------------------|---------------|-------------|----------------|
| Maven | Yes | Yes | Yes |
| Conan | Yes | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/11679) | Yes |
| npm | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/36853) | Yes | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/36853) |
| NuGet | Yes | Yes | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/36425) |
| PyPI | Yes | No | No |
| Go | Yes | No - [open issue](https://gitlab.com/gitlab-org/gitlab/-/issues/213900) | No - [open-issue](https://gitlab.com/gitlab-org/gitlab/-/issues/213902) |
| Composer | Yes | Yes | No |
| Generic | Yes | No | No |
NOTE:
npm is currently a hybrid of the instance level and group level.
It is using the top-level group or namespace as the defining portion of the name
(for example, `@my-group-name/my-package-name`).
Using instance-level endpoints requires [stricter naming conventions](#naming-conventions).
NOTE:
Composer package naming scope is Instance Level.
@ -116,8 +101,8 @@ Packages can be configured to use object storage, therefore your code must suppo
The way new package systems are integrated in GitLab is using an [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc). Therefore, the first iteration should support the bare minimum user actions:
- Authentication
- Uploading a package
- Authentication with a GitLab job, personal access, project access, or deploy token
- Uploading a package and displaying basic metadata in the user interface
- Pulling a package
- Required actions
@ -242,6 +227,17 @@ create the package record. Workhorse provides a variety of file metadata such as
For testing purposes, you may want to [enable object storage](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/howto/object_storage.md)
in your local development environment.
#### File size limits
Files uploaded to the GitLab Package Registry are [limited by format](../administration/instance_limits.md#package-registry-limits).
On GitLab.com, these are typically set to 5GB to help prevent timeout issues and abuse.
When a new package type is added to the `Packages::Package` model, a size limit must be added
similar to [this example](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/52639/diffs#382f879fb09b0212e3cedd99e6c46e2083867216),
or the [related test](https://gitlab.com/gitlab-org/gitlab/-/blob/fe4ba43766781371cebfacd78364a1de762917cd/spec/models/packages/package_spec.rb#L761)
must be updated if file size limits do not apply. The only reason a size limit does not apply is if
the package format does not upload and store package files.
#### Rate Limits on GitLab.com
Package manager clients can make rapid requests that exceed the

View File

@ -96,20 +96,20 @@ Depending on your target platform, some features might not be available to you.
Comprised of a set of [stages](stages.md), Auto DevOps brings these best practices to your
project in a simple and automatic way:
1. [Auto Build](stages.md#auto-build)
1. [Auto Test](stages.md#auto-test)
1. [Auto Code Quality](stages.md#auto-code-quality)
1. [Auto SAST (Static Application Security Testing)](stages.md#auto-sast)
1. [Auto Secret Detection](stages.md#auto-secret-detection)
1. [Auto Dependency Scanning](stages.md#auto-dependency-scanning) **(ULTIMATE)**
1. [Auto License Compliance](stages.md#auto-license-compliance) **(ULTIMATE)**
1. [Auto Container Scanning](stages.md#auto-container-scanning) **(ULTIMATE)**
1. [Auto Review Apps](stages.md#auto-review-apps)
1. [Auto DAST (Dynamic Application Security Testing)](stages.md#auto-dast) **(ULTIMATE)**
1. [Auto Deploy](stages.md#auto-deploy)
1. [Auto Browser Performance Testing](stages.md#auto-browser-performance-testing) **(PREMIUM)**
1. [Auto Monitoring](stages.md#auto-monitoring)
1. [Auto Code Intelligence](stages.md#auto-code-intelligence)
- [Auto Browser Performance Testing](stages.md#auto-browser-performance-testing)
- [Auto Build](stages.md#auto-build)
- [Auto Code Intelligence](stages.md#auto-code-intelligence)
- [Auto Code Quality](stages.md#auto-code-quality)
- [Auto Container Scanning](stages.md#auto-container-scanning)
- [Auto DAST (Dynamic Application Security Testing)](stages.md#auto-dast)
- [Auto Dependency Scanning](stages.md#auto-dependency-scanning)
- [Auto Deploy](stages.md#auto-deploy)
- [Auto License Compliance](stages.md#auto-license-compliance)
- [Auto Monitoring](stages.md#auto-monitoring)
- [Auto Review Apps](stages.md#auto-review-apps)
- [Auto SAST (Static Application Security Testing)](stages.md#auto-sast)
- [Auto Secret Detection](stages.md#auto-secret-detection)
- [Auto Test](stages.md#auto-test)
As Auto DevOps relies on many different components, you should have a basic
knowledge of the following:
@ -334,3 +334,6 @@ spec:
- name: https_proxy
value: "PUT_YOUR_HTTPS_PROXY_HERE"
```
<!-- DO NOT ADD TROUBLESHOOTING INFO HERE -->
<!-- Troubleshooting information has moved to troubleshooting.md -->

View File

@ -529,7 +529,8 @@ workers:
### Network Policy
> [Introduced](https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/merge_requests/30) in GitLab 12.7.
- [Introduced](https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/merge_requests/30) in GitLab 12.7.
- [Deprecated](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/merge_requests/184) in GitLab 13.9.
By default, all Kubernetes pods are
[non-isolated](https://kubernetes.io/docs/concepts/services-networking/network-policies/#isolated-and-non-isolated-pods),
@ -580,6 +581,76 @@ networkPolicy:
For more information on installing Network Policies, see
[Install Cilium using GitLab CI/CD](../../user/clusters/applications.md#install-cilium-using-gitlab-cicd).
### Cilium Network Policy
> [Introduced](https://gitlab.com/gitlab-org/cluster-integration/auto-deploy-image/-/merge_requests/184) in GitLab 13.9.
By default, all Kubernetes pods are
[non-isolated](https://kubernetes.io/docs/concepts/services-networking/network-policies/#isolated-and-non-isolated-pods),
and accept traffic to and from any source. You can use
[CiliumNetworkPolicy](https://docs.cilium.io/en/v1.8/concepts/kubernetes/policy/#ciliumnetworkpolicy)
to restrict connections to and from selected pods, namespaces, and the internet.
#### Requirements
As the default network plugin for Kubernetes (`kubenet`)
[does not implement](https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/network-plugins/#kubenet)
support for it, you must have [Cilium](https://docs.cilium.io/en/v1.8/intro/) as your Kubernetes network plugin.
The [Cilium](https://cilium.io/) network plugin can be
installed as a [cluster application](../../user/clusters/applications.md#install-cilium-using-gitlab-cicd)
to enable support for network policies.
#### Configuration
You can enable deployment of a network policy by setting the following
in the `.gitlab/auto-deploy-values.yaml` file:
```yaml
ciliumNetworkPolicy:
enabled: true
```
The default policy deployed by the Auto Deploy pipeline allows
traffic within a local namespace, and from the `gitlab-managed-apps`
namespace. All other inbound connections are blocked. Outbound
traffic (for example, to the internet) is not affected by the default policy.
You can also provide a custom [policy specification](https://docs.cilium.io/en/v1.8/policy/language/#simple-ingress-allow)
in the `.gitlab/auto-deploy-values.yaml` file, for example:
```yaml
ciliumNetworkPolicy:
enabled: true
spec:
endpointSelector:
matchLabels:
app.gitlab.com/env: staging
ingress:
- fromEndpoints:
- matchLabels:
app.gitlab.com/managed_by: gitlab
```
#### Enabling Alerts
You can also enable alerts. Network policies with alerts are considered only if
[GitLab Kubernetes Agent](https://docs.gitlab.com/13.6/ee/user/clusters/agent/)
has been integrated.
You can enable alerts as follows:
```yaml
ciliumNetworkPolicy:
enabled: true
alerts:
enabled: true
```
For more information on installing Network Policies, see
[Install Cilium using GitLab CI/CD](../../user/clusters/applications.md#install-cilium-using-gitlab-cicd).
### Web Application Firewall (ModSecurity) customization
> [Introduced](https://gitlab.com/gitlab-org/charts/auto-deploy-app/-/merge_requests/44) in GitLab 12.8.

View File

@ -152,20 +152,20 @@ To set a limit on how long these sessions are valid:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3649) in GitLab Ultimate 12.6.
Users can optionally specify an expiration date for
Users can optionally specify a lifetime for
[personal access tokens](../../profile/personal_access_tokens.md).
This expiration date is not a requirement, and can be set to any arbitrary date.
This lifetime is not a requirement, and can be set to any arbitrary number of days.
Personal access tokens are the only tokens needed for programmatic access to GitLab.
However, organizations with security requirements may want to enforce more protection by
requiring the regular rotation of these tokens.
### Setting a limit
### Setting a lifetime
Only a GitLab administrator can set a limit. Leaving it empty means
Only a GitLab administrator can set a lifetime. Leaving it empty means
there are no restrictions.
To set a limit on how long personal access tokens are valid:
To set a lifetime on how long personal access tokens are valid:
1. Navigate to **Admin Area > Settings > General**.
1. Expand the **Account and limit** section.

View File

@ -48,6 +48,7 @@ The following resources are migrated to the target instance:
- author ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/298745))
- parent epic ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/297459))
- emoji award ([Introduced in 13.9](https://gitlab.com/gitlab-org/gitlab/-/issues/297466))
- events ([Introduced in 13.10](https://gitlab.com/gitlab-org/gitlab/-/issues/297465))
Any other items are **not** migrated.

View File

@ -4,12 +4,13 @@ group: Package
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
# Dependency Proxy
# Dependency Proxy **(FREE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/7934) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.11.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to [GitLab Core](https://about.gitlab.com/pricing/) in GitLab 13.6.
> - [Support for private groups](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) in [GitLab Core](https://about.gitlab.com/pricing/) 13.7.
> - Anonymous access to images in public groups is no longer available starting in [GitLab Core](https://about.gitlab.com/pricing/) 13.7.
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/273655) to GitLab Free in GitLab 13.6.
> - [Support for private groups](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) in GitLab Free 13.7.
> - Anonymous access to images in public groups is no longer available starting in GitLab Free 13.7.
> - [Support for pull-by-digest and Docker version 20.x](https://gitlab.com/gitlab-org/gitlab/-/issues/290944) in GitLab Free 13.10.
The GitLab Dependency Proxy is a local proxy you can use for your frequently-accessed
upstream images.
@ -17,11 +18,6 @@ upstream images.
In the case of CI/CD, the Dependency Proxy receives a request and returns the
upstream image from a registry, acting as a pull-through cache.
NOTE:
The Dependency Proxy is not compatible with Docker version 20.x and later.
If you are using the Dependency Proxy, Docker version 19.x.x is recommended until
[issue #290944](https://gitlab.com/gitlab-org/gitlab/-/issues/290944) is resolved.
## Prerequisites
The Dependency Proxy must be [enabled by an administrator](../../../administration/packages/dependency_proxy.md).
@ -60,7 +56,7 @@ Prerequisites:
### Authenticate with the Dependency Proxy
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) in [GitLab Core](https://about.gitlab.com/pricing/) 13.7.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11582) in GitLab Free 13.7.
> - It's [deployed behind a feature flag](../../feature_flags.md), enabled by default.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
@ -162,7 +158,7 @@ the [Dependency Proxy API](../../../api/dependency_proxy.md).
## Docker Hub rate limits and the Dependency Proxy
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241639) in [GitLab Core](https://about.gitlab.com/pricing/) 13.7.
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/241639) in GitLab Free 13.7.
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
Watch how to [use the Dependency Proxy to help avoid Docker Hub rate limits](https://youtu.be/Nc4nUo7Pq08).

View File

@ -15,25 +15,24 @@ Markdown fields. When you start typing a word in a Markdown field with one of
the following characters, GitLab progressively autocompletes against a set of
matching values. The string matching is not case sensitive.
| Character | Autocompletes |
| :-------- | :------------ |
| `~` | Labels |
| `%` | Milestones |
| `@` | Users and groups |
| `#` | Issues |
| `!` | Merge requests |
| `&` | Epics |
| `$` | Snippets |
| `:` | Emoji |
| `/` | Quick Actions |
| Character | Autocompletes | Relevant matches shown |
| :-------- | :------------ | :---- |
| `~` | Labels | 20 |
| `%` | Milestones | 5 |
| `@` | Users and groups | 10 |
| `#` | Issues | 5 |
| `!` | Merge requests | 5 |
| `&` | Epics | 5 |
| `$` | Snippets | 5 |
| `:` | Emoji | 5 |
| `/` | Quick Actions | 100 |
Up to 5 of the most relevant matches are displayed in a popup list. When you
select an item from the list, the value is entered in the field. The more
characters you enter, the more precise the matches are.
When you select an item from the list, the value is entered in the field.
The more characters you enter, the more precise the matches are.
Autocomplete characters are useful when combined with [Quick Actions](quick_actions.md).
## Example
## User autocomplete
Assume your GitLab instance includes the following users:
@ -49,17 +48,9 @@ Assume your GitLab instance includes the following users:
<!-- vale gitlab.Spelling = YES -->
In an Issue comment, entering `@l` results in the following popup list
appearing. Note that user `shelba` is not included, because the list includes
only the 5 users most relevant to the Issue.
![Popup list which includes users whose username or name contains the letter `l`](img/autocomplete_characters_example1_v12_0.png)
If you continue to type, `@le`, the popup list changes to the following. The
popup now only includes users where `le` appears in their username, or a word in
their name.
![Popup list which includes users whose username or name contains the string](img/autocomplete_characters_example2_v12_0.png)
User autocompletion sorts by the users whose username or name start with your query first.
For example, typing `@lea` shows `leanna` first and typing `@ros` shows `Rosemarie Rogahn` and `Rosy Grant` first.
Any usernames or names that include your query are shown afterwards in the autocomplete menu.
You can also search across the full name to find a user.
To find `Rosy Grant`, even if their username is for example `hunter2`, you can type their full name without spaces like `@rosygrant`.
To find `Rosy Grant`, even if their username is for example `alessandra`, you can type their full name without spaces like `@rosygrant`.

View File

@ -475,7 +475,7 @@ terminal.
Read the [Release CLI documentation](https://gitlab.com/gitlab-org/release-cli/-/blob/master/docs/index.md)
for details.
## Release Metrics **(PREMIUM)**
## Release Metrics **(ULTIMATE)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259703) in GitLab Premium 13.9.

View File

@ -1,9 +0,0 @@
# frozen_string_literal: true
module API
module Entities
class ProjectRepositoryStorageMove < BasicRepositoryStorageMove
expose :project, using: Entities::ProjectIdentity
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
module Projects
class RepositoryStorageMove < BasicRepositoryStorageMove
expose :project, using: Entities::ProjectIdentity
end
end
end
end

View File

@ -11,28 +11,28 @@ module API
resource :project_repository_storage_moves do
desc 'Get a list of all project repository storage moves' do
detail 'This feature was introduced in GitLab 13.0.'
success Entities::ProjectRepositoryStorageMove
success Entities::Projects::RepositoryStorageMove
end
params do
use :pagination
end
get do
storage_moves = ProjectRepositoryStorageMove.with_projects.order_created_at_desc
storage_moves = ::Projects::RepositoryStorageMove.with_projects.order_created_at_desc
present paginate(storage_moves), with: Entities::ProjectRepositoryStorageMove, current_user: current_user
present paginate(storage_moves), with: Entities::Projects::RepositoryStorageMove, current_user: current_user
end
desc 'Get a project repository storage move' do
detail 'This feature was introduced in GitLab 13.0.'
success Entities::ProjectRepositoryStorageMove
success Entities::Projects::RepositoryStorageMove
end
params do
requires :repository_storage_move_id, type: Integer, desc: 'The ID of a project repository storage move'
end
get ':repository_storage_move_id' do
storage_move = ProjectRepositoryStorageMove.find(params[:repository_storage_move_id])
storage_move = ::Projects::RepositoryStorageMove.find(params[:repository_storage_move_id])
present storage_move, with: Entities::ProjectRepositoryStorageMove, current_user: current_user
present storage_move, with: Entities::Projects::RepositoryStorageMove, current_user: current_user
end
desc 'Schedule bulk project repository storage moves' do
@ -58,7 +58,7 @@ module API
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a list of all project repository storage moves' do
detail 'This feature was introduced in GitLab 13.1.'
success Entities::ProjectRepositoryStorageMove
success Entities::Projects::RepositoryStorageMove
end
params do
use :pagination
@ -66,12 +66,12 @@ module API
get ':id/repository_storage_moves' do
storage_moves = user_project.repository_storage_moves.with_projects.order_created_at_desc
present paginate(storage_moves), with: Entities::ProjectRepositoryStorageMove, current_user: current_user
present paginate(storage_moves), with: Entities::Projects::RepositoryStorageMove, current_user: current_user
end
desc 'Get a project repository storage move' do
detail 'This feature was introduced in GitLab 13.1.'
success Entities::ProjectRepositoryStorageMove
success Entities::Projects::RepositoryStorageMove
end
params do
requires :repository_storage_move_id, type: Integer, desc: 'The ID of a project repository storage move'
@ -79,12 +79,12 @@ module API
get ':id/repository_storage_moves/:repository_storage_move_id' do
storage_move = user_project.repository_storage_moves.find(params[:repository_storage_move_id])
present storage_move, with: Entities::ProjectRepositoryStorageMove, current_user: current_user
present storage_move, with: Entities::Projects::RepositoryStorageMove, current_user: current_user
end
desc 'Schedule a project repository storage move' do
detail 'This feature was introduced in GitLab 13.1.'
success Entities::ProjectRepositoryStorageMove
success Entities::Projects::RepositoryStorageMove
end
params do
optional :destination_storage_name, type: String, desc: 'The destination storage shard'
@ -95,7 +95,7 @@ module API
)
if storage_move.schedule
present storage_move, with: Entities::ProjectRepositoryStorageMove, current_user: current_user
present storage_move, with: Entities::Projects::RepositoryStorageMove, current_user: current_user
else
render_validation_error!(storage_move)
end

View File

@ -1,25 +0,0 @@
# frozen_string_literal: true
module BulkImports
module Common
module Transformers
class AwardEmojiTransformer
def transform(context, data)
user = find_user(context, data&.dig('user', 'public_email')) || context.current_user
data
.except('user')
.merge('user_id' => user.id)
end
private
def find_user(context, email)
return if email.blank?
context.group.users.find_by_any_email(email, confirmed: true) # rubocop: disable CodeReuse/ActiveRecord
end
end
end
end
end

View File

@ -15,6 +15,8 @@ module BulkImports
).freeze
def transform(context, data)
return unless data
data.each_with_object({}) do |(key, value), result|
prohibited = prohibited_key?(key)

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
# UserReferenceTransformer replaces specified user
# reference key with a user id being either:
# - A user id found by `public_email` in the group
# - Current user id
# under a new key `"#{@reference}_id"`.
module BulkImports
module Common
module Transformers
class UserReferenceTransformer
DEFAULT_REFERENCE = 'user'
def initialize(options = {})
@reference = options[:reference] || DEFAULT_REFERENCE
@suffixed_reference = "#{@reference}_id"
end
def transform(context, data)
return unless data
user = find_user(context, data&.dig(@reference, 'public_email')) || context.current_user
data
.except(@reference)
.merge(@suffixed_reference => user.id)
end
private
def find_user(context, email)
return if email.blank?
context.group.users.find_by_any_email(email, confirmed: true) # rubocop: disable CodeReuse/ActiveRecord
end
end
end
end
end

View File

@ -3,6 +3,7 @@
module BulkImports
module Pipeline
extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
include Gitlab::ClassAttributes
include Runner
@ -60,12 +61,17 @@ module BulkImports
# end
# end
#
# In the example above `MyTransformerOne` is the first to run and
# the instance `#transform` method is the last.
# In the example above `#transform` is the first to run and
# `MyTransformerTwo` method is the last.
def transformers
@transformers ||= self.class.transformers.map(&method(:instantiate))
@transformers << self if respond_to?(:transform) && @transformers.exclude?(self)
@transformers
strong_memoize(:transformers) do
defined_transformers = self.class.transformers.map(&method(:instantiate))
transformers = []
transformers << self if respond_to?(:transform)
transformers.concat(defined_transformers)
transformers
end
end
# Fetch pipeline loader.
@ -126,7 +132,7 @@ module BulkImports
end
def transformers
class_attributes[:transformers]
class_attributes[:transformers] || []
end
def get_loader

View File

@ -5,7 +5,7 @@ module Gitlab
# Update existent project update_at column after their repository storage was moved
class BackfillProjectUpdatedAtAfterRepositoryStorageMove
def perform(*project_ids)
updated_repository_storages = ProjectRepositoryStorageMove.select("project_id, MAX(updated_at) as updated_at").where(project_id: project_ids).group(:project_id)
updated_repository_storages = Projects::RepositoryStorageMove.select("project_id, MAX(updated_at) as updated_at").where(project_id: project_ids).group(:project_id)
Project.connection.execute <<-SQL
WITH repository_storage_cte as (

View File

@ -82,7 +82,10 @@ module Gitlab
end
def puma_in_clustered_mode?
puma? && Puma.cli_config.options[:workers].to_i > 0
return unless puma?
return unless Puma.respond_to?(:cli_config)
Puma.cli_config.options[:workers].to_i > 0
end
def max_threads

View File

@ -7960,9 +7960,6 @@ msgstr ""
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|Expiration policy is disabled"
msgstr ""
@ -17559,9 +17556,6 @@ msgstr ""
msgid "Learn more about signing commits"
msgstr ""
msgid "Learn more about the dependency list"
msgstr ""
msgid "Learn more in the"
msgstr ""
@ -17784,9 +17778,6 @@ msgstr ""
msgid "Licenses|Error fetching the license list. Please check your network connection and try again."
msgstr ""
msgid "Licenses|Learn more about license compliance"
msgstr ""
msgid "Licenses|License Compliance"
msgstr ""

View File

@ -5,10 +5,10 @@
"block-dependencies": "node scripts/frontend/block_dependencies.js",
"clean": "rm -rf public/assets tmp/cache/*-loader",
"dev-server": "NODE_OPTIONS=\"--max-old-space-size=3584\" node scripts/frontend/webpack_dev_server.js",
"eslint-fix": "echo 'Please use lint:eslint:fix instead' && exit 1",
"eslint-staged": "echo 'Please use lint:eslint:staged instead' && exit 1",
"eslint-staged-fix": "echo 'Please use lint:eslint:staged:fix instead' && exit 1",
"eslint-report": "echo 'Please use lint:eslint:report instead' && exit 1",
"eslint-fix": "echo 'Please use `yarn lint:eslint:fix` instead' && exit 1",
"eslint-staged": "echo 'Please use `yarn lint:eslint:staged` instead' && exit 1",
"eslint-staged-fix": "echo 'Please use `yarn lint:eslint:staged:fix` instead' && exit 1",
"eslint-report": "echo 'Please use `yarn lint:eslint:report` instead' && exit 1",
"file-coverage": "scripts/frontend/file_test_coverage.js",
"lint-docs": "scripts/lint-doc.sh",
"internal:eslint": "eslint --cache --max-warnings 0 --report-unused-disable-directives --ext .js,.vue",
@ -30,14 +30,16 @@
"lint:prettier:fix": "yarn run prettier --write '**/*.{graphql,js,vue}'",
"lint:prettier:staged": "scripts/frontend/execute-on-staged-files.sh prettier '(graphql|js|vue)' --check",
"lint:prettier:staged:fix": "scripts/frontend/execute-on-staged-files.sh prettier '(graphql|js|vue)' --write",
"lint:stylelint": "stylelint --cache -q '{ee/,}app/assets/stylesheets/**/*.{css,scss}'",
"lint:stylelint:fix": "yarn run lint:stylelint --fix",
"lint:stylelint:staged": "scripts/frontend/execute-on-staged-files.sh stylelint '(css|scss)' -q",
"lint:stylelint:staged:fix": "yarn run lint:stylelint:staged --fix",
"markdownlint": "markdownlint --config .markdownlint.json",
"postinstall": "node ./scripts/frontend/postinstall.js",
"prettier-all": "echo 'Please use lint:prettier instead' && exit 1",
"prettier-all-save": "echo 'Please use lint:prettier:fix instead' && exit 1",
"prettier-staged": "echo 'Please use lint:prettier:staged instead' && exit 1",
"prettier-staged-save": "echo 'Please use lint:prettier:staged:fixed instead' && exit 1",
"stylelint": "yarn stylelint-file 'app/assets/stylesheets/**/*.*' 'ee/app/assets/stylesheets/**/*.*' '!app/assets/stylesheets/startup/startup-*.scss' '!**/vendors/**'",
"stylelint-file": "BROWSERSLIST_IGNORE_OLD_DATA=true node node_modules/stylelint/bin/stylelint.js",
"prettier-all": "echo 'Please use `yarn lint:prettier` instead' && exit 1",
"prettier-all-save": "echo 'Please use `yarn lint:prettier:fix` instead' && exit 1",
"prettier-staged": "echo 'Please use `yarn lint:prettier:staged` instead' && exit 1",
"prettier-staged-save": "echo 'Please use `yarn lint:prettier:staged:fix` instead' && exit 1",
"stylelint-create-utility-map": "node scripts/frontend/stylelint/stylelint-utility-map.js",
"webpack": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.config.js",
"webpack-vendor": "NODE_OPTIONS=\"--max-old-space-size=3584\" webpack --config config/webpack.vendor.config.js",

View File

@ -1,7 +1,8 @@
# frozen_string_literal: true
module QA
RSpec.describe 'Plan', :smoke, :reliable do
# TODO: Remove :requires_admin meta when the `Runtime::Feature.enable` method call is removed
RSpec.describe 'Plan', :smoke, :reliable, :requires_admin do
describe 'mention' do
let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
let(:project) do

View File

@ -33,7 +33,7 @@ class StaticAnalysis
%w[bin/rake gitlab:sidekiq:all_queues_yml:check] => 13,
(Gitlab.ee? ? %w[bin/rake gitlab:sidekiq:sidekiq_queues_yml:check] : nil) => 13,
%w[bin/rake config_lint] => 11,
%w[yarn run stylelint] => 9,
%w[yarn run lint:stylelint] => 9,
%w[scripts/lint-conflicts.sh] => 0.59,
%w[yarn run block-dependencies] => 0.35,
%w[scripts/lint-rugged] => 0.23,

View File

@ -130,7 +130,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
}
end
it 'proxies status from the remote token request' do
it 'proxies status from the remote token request', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:service_unavailable)
@ -147,7 +147,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
}
end
it 'proxies status from the remote manifest request' do
it 'proxies status from the remote manifest request', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:bad_request)
@ -156,7 +156,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
end
it 'sends a file' do
expect(controller).to receive(:send_file).with(manifest.file.path, {})
expect(controller).to receive(:send_file).with(manifest.file.path, type: manifest.content_type)
subject
end
@ -165,6 +165,10 @@ RSpec.describe Groups::DependencyProxyForContainersController do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['Docker-Content-Digest']).to eq(manifest.digest)
expect(response.headers['Content-Length']).to eq(manifest.size)
expect(response.headers['Docker-Distribution-Api-Version']).to eq(DependencyProxy::DISTRIBUTION_API_VERSION)
expect(response.headers['Etag']).to eq("\"#{manifest.digest}\"")
expect(response.headers['Content-Disposition']).to match(/^attachment/)
end
end
@ -207,7 +211,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
}
end
it 'proxies status from the remote blob request' do
it 'proxies status from the remote blob request', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:bad_request)
@ -221,7 +225,7 @@ RSpec.describe Groups::DependencyProxyForContainersController do
subject
end
it 'returns Content-Disposition: attachment' do
it 'returns Content-Disposition: attachment', :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:ok)

View File

@ -184,9 +184,15 @@ RSpec.describe Import::BulkImportsController do
end
describe 'POST create' do
let(:instance_url) { "http://fake-intance" }
let(:instance_url) { "http://fake-instance" }
let(:bulk_import) { create(:bulk_import) }
let(:pat) { "fake-pat" }
let(:bulk_import_params) do
[{ "source_type" => "group_entity",
"source_full_path" => "full_path",
"destination_name" => "destination_name",
"destination_namespace" => "root" }]
end
before do
session[:bulk_import_gitlab_access_token] = pat
@ -194,15 +200,9 @@ RSpec.describe Import::BulkImportsController do
end
it 'executes BulkImportService' do
bulk_import_params = [{ "source_type" => "group_entity",
"source_full_path" => "full_path",
"destination_name" =>
"destination_name",
"destination_namespace" => "root" }]
expect_next_instance_of(
BulkImportService, user, bulk_import_params, { url: instance_url, access_token: pat }) do |service|
allow(service).to receive(:execute).and_return(bulk_import)
allow(service).to receive(:execute).and_return(ServiceResponse.success(payload: bulk_import))
end
post :create, params: { bulk_import: bulk_import_params }
@ -210,6 +210,19 @@ RSpec.describe Import::BulkImportsController do
expect(response).to have_gitlab_http_status(:ok)
expect(response.body).to eq({ id: bulk_import.id }.to_json)
end
it 'returns error when validation fails' do
error_response = ServiceResponse.error(message: 'Record invalid', http_status: :unprocessable_entity)
expect_next_instance_of(
BulkImportService, user, bulk_import_params, { url: instance_url, access_token: pat }) do |service|
allow(service).to receive(:execute).and_return(error_response)
end
post :create, params: { bulk_import: bulk_import_params }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(response.body).to eq({ error: 'Record invalid' }.to_json)
end
end
end

View File

@ -10,7 +10,8 @@ FactoryBot.define do
factory :dependency_proxy_manifest, class: 'DependencyProxy::Manifest' do
group
file { fixture_file_upload('spec/fixtures/dependency_proxy/manifest') }
digest { 'sha256:5ab5a6872b264fe4fd35d63991b9b7d8425f4bc79e7cf4d563c10956581170c9' }
digest { 'sha256:d0710affa17fad5f466a70159cc458227bd25d4afb39514ef662ead3e6c99515' }
file_name { 'alpine:latest.json' }
content_type { 'application/vnd.docker.distribution.manifest.v2+json' }
end
end

View File

@ -1,29 +1,29 @@
# frozen_string_literal: true
FactoryBot.define do
factory :project_repository_storage_move, class: 'ProjectRepositoryStorageMove' do
factory :project_repository_storage_move, class: 'Projects::RepositoryStorageMove' do
container { association(:project) }
source_storage_name { 'default' }
trait :scheduled do
state { ProjectRepositoryStorageMove.state_machines[:state].states[:scheduled].value }
state { Projects::RepositoryStorageMove.state_machines[:state].states[:scheduled].value }
end
trait :started do
state { ProjectRepositoryStorageMove.state_machines[:state].states[:started].value }
state { Projects::RepositoryStorageMove.state_machines[:state].states[:started].value }
end
trait :replicated do
state { ProjectRepositoryStorageMove.state_machines[:state].states[:replicated].value }
state { Projects::RepositoryStorageMove.state_machines[:state].states[:replicated].value }
end
trait :finished do
state { ProjectRepositoryStorageMove.state_machines[:state].states[:finished].value }
state { Projects::RepositoryStorageMove.state_machines[:state].states[:finished].value }
end
trait :failed do
state { ProjectRepositoryStorageMove.state_machines[:state].states[:failed].value }
state { Projects::RepositoryStorageMove.state_machines[:state].states[:failed].value }
end
end
end

View File

@ -6,6 +6,7 @@ RSpec.describe 'GFM autocomplete', :js do
let_it_be(:user_xss_title) { 'eve <img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;' }
let_it_be(:user_xss) { create(:user, name: user_xss_title, username: 'xss.user') }
let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') }
let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') }
let_it_be(:group) { create(:group, name: 'Ancestor') }
let_it_be(:child_group) { create(:group, parent: group, name: 'My group') }
let_it_be(:project) { create(:project, group: child_group) }
@ -16,6 +17,7 @@ RSpec.describe 'GFM autocomplete', :js do
before_all do
project.add_maintainer(user)
project.add_maintainer(user_xss)
project.add_maintainer(user2)
end
describe 'when tribute_autocomplete feature flag is off' do
@ -86,11 +88,7 @@ RSpec.describe 'GFM autocomplete', :js do
wait_for_requests
expect(page).to have_selector('.atwho-container')
page.within '.atwho-container #at-view-users' do
expect(find('li').text).to have_content(user_xss.username)
end
expect(find_highlighted_autocomplete_item).to have_content(user_xss.username)
end
it 'opens autocomplete menu for Milestone when field starts with text with item escaping HTML characters' do
@ -190,7 +188,30 @@ RSpec.describe 'GFM autocomplete', :js do
wait_for_requests
expect(find('.atwho-view li', visible: true)).to have_content(user.name)
expect(find_highlighted_autocomplete_item).to have_content(user.name)
end
it 'shows names that start with the query as the top result' do
type(find('#note-body'), '@mar')
wait_for_requests
expect(find_highlighted_autocomplete_item).to have_content(user2.name)
end
it 'shows usernames that start with the query as the top result' do
type(find('#note-body'), '@msi')
wait_for_requests
expect(find_highlighted_autocomplete_item).to have_content(user2.name)
end
# Regression test for https://gitlab.com/gitlab-org/gitlab/-/issues/321925
it 'shows username when pasting then pressing Enter' do
fill_in 'Description', with: "@#{user.username}\n"
expect(find_field('Description').value).to have_content "@#{user.username}"
end
it 'selects the first item for non-assignee dropdowns if a query is entered' do
@ -1004,4 +1025,8 @@ RSpec.describe 'GFM autocomplete', :js do
wait_for_requests
end
def find_highlighted_autocomplete_item
find('.atwho-view li.cur', visible: true)
end
end

View File

@ -9,156 +9,139 @@ RSpec.describe 'Merge request < User sees mini pipeline graph', :js do
let(:pipeline) { create(:ci_empty_pipeline, project: project, ref: 'master', status: 'running', sha: project.commit.id) }
let(:build) { create(:ci_build, pipeline: pipeline, stage: 'test') }
shared_examples 'mini pipeline renders' do |ci_mini_pipeline_gl_dropdown_enabled|
dropdown_selector = '[data-testid="mini-pipeline-graph-dropdown"]'
before do
build.run
build.trace.set('hello')
sign_in(user)
visit_merge_request
end
def visit_merge_request(format: :html, serializer: nil)
visit project_merge_request_path(project, merge_request, format: format, serializer: serializer)
end
it 'displays a mini pipeline graph' do
expect(page).to have_selector('.mr-widget-pipeline-graph')
end
context 'as json' do
let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') }
let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') }
before do
build.run
build.trace.set('hello')
sign_in(user)
stub_feature_flags(ci_mini_pipeline_gl_dropdown: ci_mini_pipeline_gl_dropdown_enabled)
visit_merge_request
job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
create(:ci_job_artifact, :archive, file: artifacts_file1, job: job)
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
end
let_it_be(:dropdown_toggle_selector) do
if ci_mini_pipeline_gl_dropdown_enabled
'[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle'
else
'[data-testid="mini-pipeline-graph-dropdown-toggle"]'
end
# TODO: https://gitlab.com/gitlab-org/gitlab-foss/issues/48034
xit 'avoids repeated database queries' do
before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
create(:ci_job_artifact, :archive, file: artifacts_file2, job: job)
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
expect(before.count).to eq(after.count)
expect(before.cached_count).to eq(after.cached_count)
end
end
describe 'build list toggle' do
let(:toggle) do
find(dropdown_selector)
first(dropdown_selector)
end
def visit_merge_request(format: :html, serializer: nil)
visit project_merge_request_path(project, merge_request, format: format, serializer: serializer)
# Status icon button styles should update as described in
# https://gitlab.com/gitlab-org/gitlab-foss/issues/42769
it 'has unique styles for default, :hover, :active, and :focus states' do
default_background_color, default_foreground_color, default_box_shadow = get_toggle_colors(dropdown_selector)
toggle.hover
hover_background_color, hover_foreground_color, hover_box_shadow = get_toggle_colors(dropdown_selector)
page.driver.browser.action.click_and_hold(toggle.native).perform
active_background_color, active_foreground_color, active_box_shadow = get_toggle_colors(dropdown_selector)
page.driver.browser.action.release(toggle.native).perform
page.driver.browser.action.click(toggle.native).move_by(100, 100).perform
focus_background_color, focus_foreground_color, focus_box_shadow = get_toggle_colors(dropdown_selector)
expect(default_background_color).not_to eq(hover_background_color)
expect(hover_background_color).not_to eq(active_background_color)
expect(default_background_color).not_to eq(active_background_color)
expect(default_foreground_color).not_to eq(hover_foreground_color)
expect(hover_foreground_color).not_to eq(active_foreground_color)
expect(default_foreground_color).not_to eq(active_foreground_color)
expect(focus_background_color).to eq(hover_background_color)
expect(focus_foreground_color).to eq(hover_foreground_color)
expect(default_box_shadow).to eq('none')
expect(hover_box_shadow).to eq('none')
expect(active_box_shadow).not_to eq('none')
expect(focus_box_shadow).not_to eq('none')
end
it 'displays a mini pipeline graph' do
expect(page).to have_selector('.mr-widget-pipeline-graph')
it 'shows tooltip when hovered' do
toggle.hover
expect(page).to have_selector('.tooltip')
end
end
describe 'builds list menu' do
let(:toggle) do
find(dropdown_selector)
first(dropdown_selector)
end
context 'as json' do
let(:artifacts_file1) { fixture_file_upload(File.join('spec/fixtures/banana_sample.gif'), 'image/gif') }
let(:artifacts_file2) { fixture_file_upload(File.join('spec/fixtures/dk.png'), 'image/png') }
before do
toggle.click
wait_for_requests
end
before do
job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
create(:ci_job_artifact, :archive, file: artifacts_file1, job: job)
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
it 'pens when toggle is clicked' do
expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu')
end
it 'closes when toggle is clicked again' do
toggle.click
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
end
it 'closes when clicking somewhere else' do
find('body').click
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
end
describe 'build list build item' do
let(:build_item) do
find('.mini-pipeline-graph-dropdown-item')
first('.mini-pipeline-graph-dropdown-item')
end
# TODO: https://gitlab.com/gitlab-org/gitlab-foss/issues/48034
xit 'avoids repeated database queries' do
before = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
it 'visits the build page when clicked' do
build_item.click
find('.build-page')
job = create(:ci_build, :success, :trace_artifact, pipeline: pipeline)
create(:ci_job_artifact, :archive, file: artifacts_file2, job: job)
create(:ci_build, :manual, pipeline: pipeline, when: 'manual')
after = ActiveRecord::QueryRecorder.new { visit_merge_request(format: :json, serializer: 'widget') }
expect(before.count).to eq(after.count)
expect(before.cached_count).to eq(after.cached_count)
end
end
describe 'build list toggle' do
let(:toggle) do
find(dropdown_toggle_selector)
first(dropdown_toggle_selector)
end
# Status icon button styles should update as described in
# https://gitlab.com/gitlab-org/gitlab-foss/issues/42769
it 'has unique styles for default, :hover, :active, and :focus states' do
default_background_color, default_foreground_color, default_box_shadow = get_toggle_colors(dropdown_toggle_selector)
toggle.hover
hover_background_color, hover_foreground_color, hover_box_shadow = get_toggle_colors(dropdown_toggle_selector)
page.driver.browser.action.click_and_hold(toggle.native).perform
active_background_color, active_foreground_color, active_box_shadow = get_toggle_colors(dropdown_toggle_selector)
page.driver.browser.action.release(toggle.native).perform
page.driver.browser.action.click(toggle.native).move_by(100, 100).perform
focus_background_color, focus_foreground_color, focus_box_shadow = get_toggle_colors(dropdown_toggle_selector)
expect(default_background_color).not_to eq(hover_background_color)
expect(hover_background_color).not_to eq(active_background_color)
expect(default_background_color).not_to eq(active_background_color)
expect(default_foreground_color).not_to eq(hover_foreground_color)
expect(hover_foreground_color).not_to eq(active_foreground_color)
expect(default_foreground_color).not_to eq(active_foreground_color)
expect(focus_background_color).to eq(hover_background_color)
expect(focus_foreground_color).to eq(hover_foreground_color)
expect(default_box_shadow).to eq('none')
expect(hover_box_shadow).to eq('none')
expect(active_box_shadow).not_to eq('none')
expect(focus_box_shadow).not_to eq('none')
expect(current_path).to eql(project_job_path(project, build))
end
it 'shows tooltip when hovered' do
toggle.hover
build_item.hover
expect(page).to have_selector('.tooltip')
end
end
describe 'builds list menu' do
let(:toggle) do
find(dropdown_toggle_selector)
first(dropdown_toggle_selector)
end
before do
toggle.click
wait_for_requests
end
it 'pens when toggle is clicked' do
expect(toggle.find(:xpath, '..')).to have_selector('.mini-pipeline-graph-dropdown-menu')
end
it 'closes when toggle is clicked again' do
toggle.click
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
end
it 'closes when clicking somewhere else' do
find('body').click
expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
end
describe 'build list build item' do
let(:build_item) do
find('.mini-pipeline-graph-dropdown-item')
first('.mini-pipeline-graph-dropdown-item')
end
it 'visits the build page when clicked' do
build_item.click
find('.build-page')
expect(current_path).to eql(project_job_path(project, build))
end
it 'shows tooltip when hovered' do
build_item.hover
expect(page).to have_selector('.tooltip')
end
end
end
end
context 'with ci_mini_pipeline_gl_dropdown disabled' do
it_behaves_like "mini pipeline renders", false
end
context 'with ci_mini_pipeline_gl_dropdown enabled' do
it_behaves_like "mini pipeline renders", true
end
private
@ -166,9 +149,9 @@ RSpec.describe 'Merge request < User sees mini pipeline graph', :js do
def get_toggle_colors(selector)
find(selector)
[
evaluate_script("$('#{selector}:visible').css('background-color');"),
evaluate_script("$('#{selector}:visible svg').css('fill');"),
evaluate_script("$('#{selector}:visible').css('box-shadow');")
evaluate_script("$('#{selector} button:visible').css('background-color');"),
evaluate_script("$('#{selector} button:visible svg').css('fill');"),
evaluate_script("$('#{selector} button:visible').css('box-shadow');")
]
end
end

View File

@ -519,75 +519,58 @@ RSpec.describe 'Pipelines', :js do
end
end
shared_examples 'mini pipeline renders' do |ci_mini_pipeline_gl_dropdown_enabled|
context 'mini pipeline graph' do
context 'mini pipeline graph' do
let!(:build) do
create(:ci_build, :pending, pipeline: pipeline,
stage: 'build',
name: 'build')
end
dropdown_selector = '[data-testid="mini-pipeline-graph-dropdown"]'
before do
visit_project_pipelines
end
it 'renders a mini pipeline graph' do
expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]')
expect(page).to have_selector(dropdown_selector)
end
context 'when clicking a stage badge' do
it 'opens a dropdown' do
find(dropdown_selector).click
expect(page).to have_link build.name
end
it 'is possible to cancel pending build' do
find(dropdown_selector).click
find('.js-ci-action').click
wait_for_requests
expect(build.reload).to be_canceled
end
end
context 'for a failed pipeline' do
let!(:build) do
create(:ci_build, :pending, pipeline: pipeline,
stage: 'build',
name: 'build')
create(:ci_build, :failed, pipeline: pipeline,
stage: 'build',
name: 'build')
end
before do
stub_feature_flags(ci_mini_pipeline_gl_dropdown: ci_mini_pipeline_gl_dropdown_enabled)
visit_project_pipelines
end
it 'displays the failure reason' do
find(dropdown_selector).click
let_it_be(:dropdown_toggle_selector) do
if ci_mini_pipeline_gl_dropdown_enabled
'[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle'
else
'[data-testid="mini-pipeline-graph-dropdown-toggle"]'
end
end
it 'renders a mini pipeline graph' do
expect(page).to have_selector('[data-testid="widget-mini-pipeline-graph"]')
expect(page).to have_selector(dropdown_toggle_selector)
end
context 'when clicking a stage badge' do
it 'opens a dropdown' do
find(dropdown_toggle_selector).click
expect(page).to have_link build.name
end
it 'is possible to cancel pending build' do
find(dropdown_toggle_selector).click
find('.js-ci-action').click
wait_for_requests
expect(build.reload).to be_canceled
end
end
context 'for a failed pipeline' do
let!(:build) do
create(:ci_build, :failed, pipeline: pipeline,
stage: 'build',
name: 'build')
end
it 'displays the failure reason' do
find(dropdown_toggle_selector).click
within('.js-builds-dropdown-list') do
build_element = page.find('.mini-pipeline-graph-dropdown-item')
expect(build_element['title']).to eq('build - failed - (unknown failure)')
end
within('.js-builds-dropdown-list') do
build_element = page.find('.mini-pipeline-graph-dropdown-item')
expect(build_element['title']).to eq('build - failed - (unknown failure)')
end
end
end
end
context 'with ci_mini_pipeline_gl_dropdown disabled' do
it_behaves_like "mini pipeline renders", false
end
context 'with ci_mini_pipeline_gl_dropdown enabled' do
it_behaves_like "mini pipeline renders", true
end
context 'with pagination' do
before do
allow(Ci::Pipeline).to receive(:default_per_page).and_return(1)

View File

@ -1,38 +1,16 @@
{
"schemaVersion": 1,
"name": "library/alpine",
"tag": "latest",
"architecture": "amd64",
"fsLayers": [
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1472,
"digest": "sha256:7731472c3f2a25edbb9c085c78f42ec71259f2b83485aa60648276d408865839"
},
"layers": [
{
"blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"
},
{
"blobSum": "sha256:188c0c94c7c576fff0792aca7ec73d67a2f7f4cb3a6e53a84559337260b36964"
}
],
"history": [
{
"v1Compatibility": "{\"architecture\":\"amd64\",\"config\":{\"Hostname\":\"\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\"],\"ArgsEscaped\":true,\"Image\":\"sha256:3543079adc6fb5170279692361be8b24e89ef1809a374c1b4429e1d560d1459c\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":null},\"container\":\"8c59eb170e19b8c3768b8d06c91053b0debf4a6fa6a452df394145fe9b885ea5\",\"container_config\":{\"Hostname\":\"8c59eb170e19\",\"Domainname\":\"\",\"User\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) \",\"CMD [\\\"/bin/sh\\\"]\"],\"ArgsEscaped\":true,\"Image\":\"sha256:3543079adc6fb5170279692361be8b24e89ef1809a374c1b4429e1d560d1459c\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"OnBuild\":null,\"Labels\":{}},\"created\":\"2020-10-22T02:19:24.499382102Z\",\"docker_version\":\"18.09.7\",\"id\":\"c5f1aab5bb88eaf1aa62bea08ea6654547d43fd4d15b1a476c77e705dd5385ba\",\"os\":\"linux\",\"parent\":\"dc0b50cc52bc340d7848a62cfe8a756f4420592f4984f7a680ef8f9d258176ed\",\"throwaway\":true}"
},
{
"v1Compatibility": "{\"id\":\"dc0b50cc52bc340d7848a62cfe8a756f4420592f4984f7a680ef8f9d258176ed\",\"created\":\"2020-10-22T02:19:24.33416307Z\",\"container_config\":{\"Cmd\":[\"/bin/sh -c #(nop) ADD file:f17f65714f703db9012f00e5ec98d0b2541ff6147c2633f7ab9ba659d0c507f4 in / \"]}}"
}
],
"signatures": [
{
"header": {
"jwk": {
"crv": "P-256",
"kid": "XOTE:DZ4C:YBPJ:3O3L:YI4B:NYXU:T4VR:USH6:CXXN:SELU:CSCC:FVPE",
"kty": "EC",
"x": "cR1zye_3354mdbD7Dn-mtXNXvtPtmLlUVDa5vH6Lp74",
"y": "rldUXSllLit6_2BW6AV8aqkwWJXHoYPG9OwkIBouwxQ"
},
"alg": "ES256"
},
"signature": "DYB2iB-XKIisqp5Q0OXFOBIOlBOuRV7pnZuKy0cxVB2Qj1VFRhWX4Tq336y0VMWbF6ma1he5A1E_Vk4jazrJ9g",
"protected": "eyJmb3JtYXRMZW5ndGgiOjIxMzcsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAyMC0xMS0yNFQyMjowMTo1MVoifQ"
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2810825,
"digest": "sha256:596ba82af5aaa3e2fd9d6f955b8b94f0744a2b60710e3c243ba3e4a467f051d1"
}
]
}

View File

@ -32,8 +32,9 @@ describe('Configure Feature Flags Modal', () => {
});
};
const findGlModal = () => wrapper.find(GlModal);
const findGlModal = () => wrapper.findComponent(GlModal);
const findPrimaryAction = () => findGlModal().props('actionPrimary');
const findSecondaryAction = () => findGlModal().props('actionSecondary');
const findProjectNameInput = () => wrapper.find('#project_name_verification');
const findDangerGlAlert = () =>
wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'danger');
@ -42,18 +43,18 @@ describe('Configure Feature Flags Modal', () => {
afterEach(() => wrapper.destroy());
beforeEach(factory);
it('should have Primary and Cancel actions', () => {
expect(findGlModal().props('actionCancel').text).toBe('Close');
expect(findPrimaryAction().text).toBe('Regenerate instance ID');
it('should have Primary and Secondary actions', () => {
expect(findPrimaryAction().text).toBe('Close');
expect(findSecondaryAction().text).toBe('Regenerate instance ID');
});
it('should default disable the primary action', async () => {
const [{ disabled }] = findPrimaryAction().attributes;
it('should default disable the primary action', () => {
const [{ disabled }] = findSecondaryAction().attributes;
expect(disabled).toBe(true);
});
it('should emit a `token` event when clicking on the Primary action', async () => {
findGlModal().vm.$emit('primary', mockEvent);
findGlModal().vm.$emit('secondary', mockEvent);
await wrapper.vm.$nextTick();
expect(wrapper.emitted('token')).toEqual([[]]);
expect(mockEvent.preventDefault).toHaveBeenCalled();
@ -112,10 +113,10 @@ describe('Configure Feature Flags Modal', () => {
afterEach(() => wrapper.destroy());
beforeEach(factory);
it('should enable the primary action', async () => {
it('should enable the secondary action', async () => {
findProjectNameInput().vm.$emit('input', provide.projectName);
await wrapper.vm.$nextTick();
const [{ disabled }] = findPrimaryAction().attributes;
const [{ disabled }] = findSecondaryAction().attributes;
expect(disabled).toBe(false);
});
});
@ -124,8 +125,8 @@ describe('Configure Feature Flags Modal', () => {
afterEach(() => wrapper.destroy());
beforeEach(factory.bind(null, { canUserRotateToken: false }));
it('should not display the primary action', async () => {
expect(findPrimaryAction()).toBe(null);
it('should not display the primary action', () => {
expect(findSecondaryAction()).toBe(null);
});
it('should not display regenerating instance ID', async () => {

View File

@ -576,55 +576,95 @@ describe('GfmAutoComplete', () => {
});
});
describe('Members.templateFunction', () => {
it('should return html with avatarTag and username', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: '',
icon: '',
availabilityStatus: '',
}),
).toBe('<li>IMG my-group <small></small> </li>');
});
describe('GfmAutoComplete.Members', () => {
const member = {
name: 'Marge Simpson',
username: 'msimpson',
search: 'MargeSimpson msimpson',
};
it('should add icon if icon is set', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: '',
icon: '<i class="icon"/>',
availabilityStatus: '',
}),
).toBe('<li>IMG my-group <small></small> <i class="icon"/></li>');
});
describe('templateFunction', () => {
it('should return html with avatarTag and username', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: '',
icon: '',
availabilityStatus: '',
}),
).toBe('<li>IMG my-group <small></small> </li>');
});
it('should add escaped title if title is set', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: 'MyGroup+',
icon: '<i class="icon"/>',
availabilityStatus: '',
}),
).toBe('<li>IMG my-group <small>MyGroup+</small> <i class="icon"/></li>');
});
it('should add icon if icon is set', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: '',
icon: '<i class="icon"/>',
availabilityStatus: '',
}),
).toBe('<li>IMG my-group <small></small> <i class="icon"/></li>');
});
it('should add user availability status if availabilityStatus is set', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: '',
icon: '<i class="icon"/>',
availabilityStatus: '<span class="gl-text-gray-500"> (Busy)</span>',
}),
).toBe(
'<li>IMG my-group <small><span class="gl-text-gray-500"> (Busy)</span></small> <i class="icon"/></li>',
);
it('should add escaped title if title is set', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: 'MyGroup+',
icon: '<i class="icon"/>',
availabilityStatus: '',
}),
).toBe('<li>IMG my-group <small>MyGroup+</small> <i class="icon"/></li>');
});
it('should add user availability status if availabilityStatus is set', () => {
expect(
GfmAutoComplete.Members.templateFunction({
avatarTag: 'IMG',
username: 'my-group',
title: '',
icon: '<i class="icon"/>',
availabilityStatus: '<span class="gl-text-gray-500"> (Busy)</span>',
}),
).toBe(
'<li>IMG my-group <small><span class="gl-text-gray-500"> (Busy)</span></small> <i class="icon"/></li>',
);
});
describe('nameOrUsernameStartsWith', () => {
it.each`
query | result
${'mar'} | ${true}
${'msi'} | ${true}
${'margesimpson'} | ${true}
${'msimpson'} | ${true}
${'arge'} | ${false}
${'rgesimp'} | ${false}
${'maria'} | ${false}
${'homer'} | ${false}
`('returns $result for $query', ({ query, result }) => {
expect(GfmAutoComplete.Members.nameOrUsernameStartsWith(member, query)).toBe(result);
});
});
describe('nameOrUsernameIncludes', () => {
it.each`
query | result
${'mar'} | ${true}
${'msi'} | ${true}
${'margesimpson'} | ${true}
${'msimpson'} | ${true}
${'arge'} | ${true}
${'rgesimp'} | ${true}
${'maria'} | ${false}
${'homer'} | ${false}
`('returns $result for $query', ({ query, result }) => {
expect(GfmAutoComplete.Members.nameOrUsernameIncludes(member, query)).toBe(result);
});
});
});
});

View File

@ -2,6 +2,7 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
import MockAdapter from 'axios-mock-adapter';
import { createMockClient } from 'mock-apollo-client';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { STATUSES } from '~/import_entities/constants';
import {
clientTypenames,
@ -18,6 +19,7 @@ import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import { statusEndpointFixture, availableNamespacesFixture } from './fixtures';
jest.mock('~/flash');
jest.mock('~/import_entities/import_groups/graphql/services/status_poller', () => ({
StatusPoller: jest.fn().mockImplementation(function mock() {
this.startPolling = jest.fn();
@ -287,6 +289,40 @@ describe('Bulk import resolvers', () => {
expect(results[0].status).toBe(STATUSES.NONE);
});
it('shows default error message when server error is not provided', async () => {
axiosMockAdapter
.onPost(FAKE_ENDPOINTS.createBulkImport)
.reply(httpStatus.INTERNAL_SERVER_ERROR);
client
.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID },
})
.catch(() => {});
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: 'Importing the group failed' });
});
it('shows provided error message when error is included in backend response', async () => {
const CUSTOM_MESSAGE = 'custom message';
axiosMockAdapter
.onPost(FAKE_ENDPOINTS.createBulkImport)
.reply(httpStatus.INTERNAL_SERVER_ERROR, { error: CUSTOM_MESSAGE });
client
.mutate({
mutation: importGroupMutation,
variables: { sourceGroupId: GROUP_ID },
})
.catch(() => {});
await waitForPromises();
expect(createFlash).toHaveBeenCalledWith({ message: CUSTOM_MESSAGE });
});
});
});
});

View File

@ -6,7 +6,7 @@ exports[`packages_list_row renders 1`] = `
data-qa-selector="package_row"
>
<div
class="gl-display-flex gl-align-items-center gl-py-3"
class="gl-display-flex gl-align-items-center gl-py-3 gl-px-5"
>
<!---->

View File

@ -69,7 +69,8 @@ describe('Pipelines', () => {
const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button');
const findCiLintButton = () => wrapper.findByTestId('ci-lint-button');
const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button');
const findStagesDropdown = () => wrapper.findByTestId('mini-pipeline-graph-dropdown-toggle');
const findStagesDropdownToggle = () =>
wrapper.find('[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle');
const findPipelineUrlLinks = () => wrapper.findAll('[data-testid="pipeline-url-link"]');
const createComponent = (props = defaultProps) => {
@ -642,7 +643,7 @@ describe('Pipelines', () => {
// Mock init a polling cycle
wrapper.vm.poll.options.notificationCallback(true);
findStagesDropdown().trigger('click');
findStagesDropdownToggle().trigger('click');
await waitForPromises();
@ -652,7 +653,9 @@ describe('Pipelines', () => {
});
it('stops polling & restarts polling', async () => {
findStagesDropdown().trigger('click');
findStagesDropdownToggle().trigger('click');
await waitForPromises();
expect(cancelMock).not.toHaveBeenCalled();
expect(stopMock).toHaveBeenCalled();

View File

@ -153,11 +153,10 @@ describe('Pipelines Table Row', () => {
});
it('should render an icon for each stage', () => {
expect(
wrapper.findAll(
'.table-section:nth-child(5) [data-testid="mini-pipeline-graph-dropdown-toggle"]',
).length,
).toEqual(pipeline.details.stages.length);
const stages = wrapper.findAll(
'.table-section:nth-child(5) [data-testid="mini-pipeline-graph-dropdown"]',
);
expect(stages).toHaveLength(pipeline.details.stages.length);
});
});

View File

@ -1,47 +1,38 @@
import 'bootstrap/js/dist/dropdown';
import { GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import StageComponent from '~/pipelines/components/pipelines_list/stage.vue';
import eventHub from '~/pipelines/event_hub';
import { stageReply } from './mock_data';
const dropdownPath = 'path.json';
describe('Pipelines stage component', () => {
let wrapper;
let mock;
let glFeatures;
const defaultProps = {
stage: {
status: {
group: 'success',
icon: 'status_success',
title: 'success',
},
dropdown_path: 'path.json',
},
updateDropdown: false,
};
const createComponent = (props = {}) => {
wrapper = mount(StageComponent, {
attachTo: document.body,
propsData: {
...defaultProps,
stage: {
status: {
group: 'success',
icon: 'status_success',
title: 'success',
},
dropdown_path: dropdownPath,
},
updateDropdown: false,
...props,
},
provide: {
glFeatures,
},
});
};
beforeEach(() => {
mock = new MockAdapter(axios);
jest.spyOn(eventHub, '$emit');
glFeatures = {};
});
afterEach(() => {
@ -52,245 +43,142 @@ describe('Pipelines stage component', () => {
mock.restore();
});
describe('when ci_mini_pipeline_gl_dropdown feature flag is disabled', () => {
const isDropdownOpen = () => wrapper.classes('show');
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
const findDropdownMenu = () =>
wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]');
const findCiActionBtn = () => wrapper.find('.js-ci-action');
describe('default', () => {
beforeEach(() => {
createComponent();
});
it('should render a dropdown with the status icon', () => {
expect(wrapper.attributes('class')).toEqual('dropdown');
expect(wrapper.find('svg').exists()).toBe(true);
expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown');
});
const openStageDropdown = () => {
findDropdownToggle().trigger('click');
return new Promise((resolve) => {
wrapper.vm.$root.$on('bv::dropdown::show', resolve);
});
};
describe('with successful request', () => {
beforeEach(() => {
mock.onGet('path.json').reply(200, stageReply);
createComponent();
});
it('should render the received data and emit `clickedDropdown` event', async () => {
wrapper.find('button').trigger('click');
await axios.waitForAll();
expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
stageReply.latest_statuses[0].name,
);
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
});
});
it('when request fails should close the dropdown', async () => {
mock.onGet('path.json').reply(500);
describe('default appearance', () => {
beforeEach(() => {
createComponent();
wrapper.find({ ref: 'dropdown' }).trigger('click');
expect(isDropdownOpen()).toBe(true);
wrapper.find('button').trigger('click');
await axios.waitForAll();
expect(isDropdownOpen()).toBe(false);
});
describe('update endpoint correctly', () => {
beforeEach(() => {
const copyStage = { ...stageReply };
copyStage.latest_statuses[0].name = 'this is the updated content';
mock.onGet('bar.json').reply(200, copyStage);
createComponent({
stage: {
status: {
group: 'running',
icon: 'status_running',
title: 'running',
},
dropdown_path: 'bar.json',
},
});
return axios.waitForAll();
});
it('should update the stage to request the new endpoint provided', async () => {
wrapper.find('button').trigger('click');
await axios.waitForAll();
expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
'this is the updated content',
);
});
});
describe('pipelineActionRequestComplete', () => {
beforeEach(() => {
mock.onGet('path.json').reply(200, stageReply);
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
});
const clickCiAction = async () => {
wrapper.find('button').trigger('click');
await axios.waitForAll();
wrapper.find('.js-ci-action').trigger('click');
await axios.waitForAll();
};
describe('within pipeline table', () => {
it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
createComponent({ type: 'PIPELINES_TABLE' });
await clickCiAction();
expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
});
});
describe('in MR widget', () => {
beforeEach(() => {
jest.spyOn($.fn, 'dropdown');
});
it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
createComponent();
await clickCiAction();
expect($.fn.dropdown).toHaveBeenCalledWith('toggle');
});
});
it('should render a dropdown with the status icon', () => {
expect(findDropdown().exists()).toBe(true);
expect(findDropdownToggle().exists()).toBe(true);
expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
});
});
describe('when ci_mini_pipeline_gl_dropdown feature flag is enabled', () => {
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownToggle = () => wrapper.find('button.gl-dropdown-toggle');
const findDropdownMenu = () =>
wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]');
const findCiActionBtn = () => wrapper.find('.js-ci-action');
const openGlDropdown = () => {
findDropdownToggle().trigger('click');
return new Promise((resolve) => {
wrapper.vm.$root.$on('bv::dropdown::show', resolve);
});
};
describe('when update dropdown is changed', () => {
beforeEach(() => {
glFeatures = { ciMiniPipelineGlDropdown: true };
createComponent();
});
});
describe('default', () => {
beforeEach(() => {
createComponent();
});
it('should render a dropdown with the status icon', () => {
expect(findDropdown().exists()).toBe(true);
expect(findDropdownToggle().classes('gl-dropdown-toggle')).toEqual(true);
expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
});
});
describe('with successful request', () => {
beforeEach(() => {
mock.onGet('path.json').reply(200, stageReply);
createComponent();
});
it('should render the received data and emit `clickedDropdown` event', async () => {
await openGlDropdown();
await axios.waitForAll();
expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
});
});
it('when request fails should close the dropdown', async () => {
mock.onGet('path.json').reply(500);
describe('when user opens dropdown and stage request is successful', () => {
beforeEach(async () => {
mock.onGet(dropdownPath).reply(200, stageReply);
createComponent();
await openGlDropdown();
await openStageDropdown();
await axios.waitForAll();
});
it('should render the received data and emit `clickedDropdown` event', async () => {
expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
});
it('should refresh when updateDropdown is set to true', async () => {
expect(mock.history.get).toHaveLength(1);
wrapper.setProps({ updateDropdown: true });
await axios.waitForAll();
expect(mock.history.get).toHaveLength(2);
});
});
describe('when user opens dropdown and stage request fails', () => {
beforeEach(async () => {
mock.onGet(dropdownPath).reply(500);
createComponent();
await openStageDropdown();
await axios.waitForAll();
});
it('should close the dropdown', () => {
expect(findDropdown().classes('show')).toBe(false);
});
});
describe('update endpoint correctly', () => {
beforeEach(async () => {
const copyStage = { ...stageReply };
copyStage.latest_statuses[0].name = 'this is the updated content';
mock.onGet('bar.json').reply(200, copyStage);
createComponent({
stage: {
status: {
group: 'running',
icon: 'status_running',
title: 'running',
},
dropdown_path: 'bar.json',
describe('update endpoint correctly', () => {
beforeEach(async () => {
const copyStage = { ...stageReply };
copyStage.latest_statuses[0].name = 'this is the updated content';
mock.onGet('bar.json').reply(200, copyStage);
createComponent({
stage: {
status: {
group: 'running',
icon: 'status_running',
title: 'running',
},
});
await axios.waitForAll();
dropdown_path: 'bar.json',
},
});
await axios.waitForAll();
});
it('should update the stage to request the new endpoint provided', async () => {
await openStageDropdown();
await axios.waitForAll();
expect(findDropdownMenu().text()).toContain('this is the updated content');
});
});
describe('pipelineActionRequestComplete', () => {
beforeEach(() => {
mock.onGet(dropdownPath).reply(200, stageReply);
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
});
const clickCiAction = async () => {
await openStageDropdown();
await axios.waitForAll();
findCiActionBtn().trigger('click');
await axios.waitForAll();
};
describe('within pipeline table', () => {
beforeEach(() => {
createComponent({ type: 'PIPELINES_TABLE' });
});
it('should update the stage to request the new endpoint provided', async () => {
await openGlDropdown();
await axios.waitForAll();
it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
await clickCiAction();
expect(findDropdownMenu().text()).toContain('this is the updated content');
expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
});
});
describe('pipelineActionRequestComplete', () => {
describe('in MR widget', () => {
beforeEach(() => {
mock.onGet('path.json').reply(200, stageReply);
mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
createComponent();
});
const clickCiAction = async () => {
await openGlDropdown();
await axios.waitForAll();
it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
const hidden = jest.fn();
findCiActionBtn().trigger('click');
await axios.waitForAll();
};
wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
describe('within pipeline table', () => {
beforeEach(() => {
createComponent({ type: 'PIPELINES_TABLE' });
});
expect(hidden).toHaveBeenCalledTimes(0);
it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
await clickCiAction();
await clickCiAction();
expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
});
});
describe('in MR widget', () => {
beforeEach(() => {
jest.spyOn($.fn, 'dropdown');
createComponent();
});
it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
const hidden = jest.fn();
wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
expect(hidden).toHaveBeenCalledTimes(0);
await clickCiAction();
expect(hidden).toHaveBeenCalledTimes(1);
});
expect(hidden).toHaveBeenCalledTimes(1);
});
});
});

View File

@ -4,7 +4,6 @@ import Component from '~/registry/explorer/components/list_page/registry_header.
import {
CONTAINER_REGISTRY_TITLE,
LIST_INTRO_TEXT,
EXPIRATION_POLICY_DISABLED_MESSAGE,
EXPIRATION_POLICY_DISABLED_TEXT,
} from '~/registry/explorer/constants';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
@ -132,41 +131,5 @@ describe('registry_header', () => {
]);
});
});
describe('expiration policy info message', () => {
describe('when there are images', () => {
describe('when expiration policy is disabled', () => {
beforeEach(() => {
return mountComponent({
expirationPolicy: { enabled: false },
expirationPolicyHelpPagePath: 'foo',
imagesCount: 1,
});
});
it('the prop is correctly bound', () => {
expect(findTitleArea().props('infoMessages')).toEqual([
{ text: LIST_INTRO_TEXT, link: '' },
{ text: EXPIRATION_POLICY_DISABLED_MESSAGE, link: 'foo' },
]);
});
});
describe.each`
desc | props
${'when there are no images'} | ${{ expirationPolicy: { enabled: false }, imagesCount: 0 }}
${'when expiration policy is enabled'} | ${{ expirationPolicy: { enabled: true }, imagesCount: 1 }}
${'when the expiration policy is completely disabled'} | ${{ expirationPolicy: { enabled: false }, imagesCount: 1, hideExpirationPolicyData: true }}
`('$desc', ({ props }) => {
it('message does not exist', () => {
mountComponent(props);
expect(findTitleArea().props('infoMessages')).toEqual([
{ text: LIST_INTRO_TEXT, link: '' },
]);
});
});
});
});
});
});

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe API::Entities::ProjectRepositoryStorageMove do
RSpec.describe API::Entities::Projects::RepositoryStorageMove do
describe '#as_json' do
subject { entity.as_json }

View File

@ -68,5 +68,11 @@ RSpec.describe BulkImports::Common::Transformers::ProhibitedAttributesTransforme
expect(transformed_hash).to eq(expected_hash)
end
context 'when there is no data to transform' do
it 'returns' do
expect(subject.transform(nil, nil)).to be_nil
end
end
end
end

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe BulkImports::Common::Transformers::AwardEmojiTransformer do
RSpec.describe BulkImports::Common::Transformers::UserReferenceTransformer do
describe '#transform' do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
@ -12,7 +12,6 @@ RSpec.describe BulkImports::Common::Transformers::AwardEmojiTransformer do
let(:hash) do
{
'name' => 'thumbs up',
'user' => {
'public_email' => email
}
@ -44,5 +43,27 @@ RSpec.describe BulkImports::Common::Transformers::AwardEmojiTransformer do
include_examples 'sets user_id and removes user key'
end
context 'when there is no data to transform' do
it 'returns' do
expect(subject.transform(nil, nil)).to be_nil
end
end
context 'when custom reference is provided' do
it 'updates provided reference' do
hash = {
'author' => {
'public_email' => user.email
}
}
transformer = described_class.new(reference: 'author')
result = transformer.transform(context, hash)
expect(result['author']).to be_nil
expect(result['author_id']).to eq(user.id)
end
end
end
end

View File

@ -117,4 +117,27 @@ RSpec.describe BulkImports::Pipeline do
end
end
end
describe '#transformers' do
before do
klass = Class.new do
include BulkImports::Pipeline
transformer BulkImports::Transformer
def transform; end
end
stub_const('BulkImports::TransformersPipeline', klass)
end
it 'has instance transform method first to run' do
transformer = double
allow(BulkImports::Transformer).to receive(:new).and_return(transformer)
pipeline = BulkImports::TransformersPipeline.new(nil)
expect(pipeline.send(:transformers)).to eq([pipeline, transformer])
end
end
end

View File

@ -29,24 +29,32 @@ RSpec.describe DependencyProxy::Manifest, type: :model do
end
end
describe '.find_or_initialize_by_file_name' do
subject { DependencyProxy::Manifest.find_or_initialize_by_file_name(file_name) }
describe '.find_or_initialize_by_file_name_or_digest' do
let_it_be(:file_name) { 'foo' }
let_it_be(:digest) { 'bar' }
subject { DependencyProxy::Manifest.find_or_initialize_by_file_name_or_digest(file_name: file_name, digest: digest) }
context 'no manifest exists' do
let_it_be(:file_name) { 'foo' }
it 'initializes a manifest' do
expect(DependencyProxy::Manifest).to receive(:new).with(file_name: file_name)
expect(DependencyProxy::Manifest).to receive(:new).with(file_name: file_name, digest: digest)
subject
end
end
context 'manifest exists' do
context 'manifest exists and matches file_name' do
let_it_be(:dependency_proxy_manifest) { create(:dependency_proxy_manifest) }
let_it_be(:file_name) { dependency_proxy_manifest.file_name }
it { is_expected.to eq(dependency_proxy_manifest) }
end
context 'manifest exists and matches digest' do
let_it_be(:dependency_proxy_manifest) { create(:dependency_proxy_manifest) }
let_it_be(:digest) { dependency_proxy_manifest.digest }
it { is_expected.to eq(dependency_proxy_manifest) }
end
end
end

View File

@ -1045,6 +1045,29 @@ RSpec.describe Group do
include(group_user.id))
end
end
context 'distinct user ids' do
let_it_be(:subgroup) { create(:group, :nested) }
let_it_be(:user) { create(:user) }
let_it_be(:shared_with_group) { create(:group) }
let_it_be(:other_subgroup_user) { create(:user) }
before do
create(:group_group_link, shared_group: subgroup, shared_with_group: shared_with_group)
subgroup.add_maintainer(other_subgroup_user)
# `user` is added as a direct member of the parent group, the subgroup
# and another group shared with the subgroup.
subgroup.parent.add_maintainer(user)
subgroup.add_developer(user)
shared_with_group.add_guest(user)
end
it 'returns only distinct user ids of users for which to refresh authorizations' do
expect(subgroup.user_ids_for_project_authorizations).to(
contain_exactly(user.id, other_subgroup_user.id))
end
end
end
describe '#update_two_factor_requirement' do

View File

@ -9,7 +9,7 @@ RSpec.describe ProjectRepositoryStorageMove, type: :model do
let(:container) { project }
let(:repository_storage_factory_key) { :project_repository_storage_move }
let(:error_key) { :project }
let(:repository_storage_worker) { ProjectUpdateRepositoryStorageWorker }
let(:repository_storage_worker) { Projects::UpdateRepositoryStorageWorker }
end
describe 'state transitions' do

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::RepositoryStorageMove, type: :model do
let_it_be_with_refind(:project) { create(:project) }
it_behaves_like 'handles repository moves' do
let(:container) { project }
let(:repository_storage_factory_key) { :project_repository_storage_move }
let(:error_key) { :project }
let(:repository_storage_worker) { Projects::UpdateRepositoryStorageWorker }
end
describe 'state transitions' do
let(:storage) { 'test_second_storage' }
before do
stub_storage_settings(storage => { 'path' => 'tmp/tests/extra_storage' })
end
context 'when started' do
subject(:storage_move) { create(:project_repository_storage_move, :started, container: project, destination_storage_name: storage) }
context 'and transits to replicated' do
it 'sets the repository storage and marks the container as writable' do
storage_move.finish_replication!
expect(project.repository_storage).to eq(storage)
expect(project).not_to be_repository_read_only
end
end
end
end
end

View File

@ -7,6 +7,6 @@ RSpec.describe API::ProjectRepositoryStorageMoves do
let_it_be(:container) { create(:project, :repository).tap { |project| project.track_project_repository } }
let_it_be(:storage_move) { create(:project_repository_storage_move, :scheduled, container: container) }
let(:repository_storage_move_factory) { :project_repository_storage_move }
let(:bulk_worker_klass) { ProjectScheduleBulkRepositoryShardMovesWorker }
let(:bulk_worker_klass) { Projects::ScheduleBulkRepositoryShardMovesWorker }
end
end

Some files were not shown because too many files have changed in this diff Show More