Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue13
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js41
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js6
-rw-r--r--app/assets/javascripts/pages/projects/feature_flags_user_lists/show/index.js19
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/stage.vue167
-rw-r--r--app/assets/javascripts/projects/feature_flags_user_lists/show/index.js23
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list.vue1
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue19
-rw-r--r--app/assets/javascripts/registry/explorer/constants/expiration_policies.js3
-rw-r--r--app/assets/javascripts/registry/explorer/pages/list.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue2
-rw-r--r--app/assets/stylesheets/components/ref_selector.scss7
-rw-r--r--app/assets/stylesheets/page_bundles/pipelines.scss19
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss22
-rw-r--r--app/controllers/groups/dependency_proxy_for_containers_controller.rb13
-rw-r--r--app/controllers/import/bulk_imports_controller.rb9
-rw-r--r--app/controllers/projects/commit_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/controllers/projects/pipelines_controller.rb1
-rw-r--r--app/models/dependency_proxy.rb1
-rw-r--r--app/models/dependency_proxy/manifest.rb7
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/project_repository_storage_move.rb41
-rw-r--r--app/models/projects/repository_storage_move.rb38
-rw-r--r--app/services/bulk_import_service.rb7
-rw-r--r--app/services/dependency_proxy/find_or_create_manifest_service.rb7
-rw-r--r--app/services/dependency_proxy/head_manifest_service.rb6
-rw-r--r--app/services/dependency_proxy/pull_manifest_service.rb4
-rw-r--r--app/services/projects/schedule_bulk_repository_shard_moves_service.rb2
-rw-r--r--app/uploaders/dependency_proxy/file_uploader.rb12
-rw-r--r--app/views/groups/registry/repositories/index.html.haml1
-rw-r--r--app/views/projects/registry/repositories/index.html.haml1
-rw-r--r--app/workers/all_queues.yml16
-rw-r--r--app/workers/project_schedule_bulk_repository_shard_moves_worker.rb18
-rw-r--r--app/workers/project_update_repository_storage_worker.rb32
-rw-r--r--app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb15
-rw-r--r--app/workers/projects/update_repository_storage_worker.rb25
-rw-r--r--changelogs/unreleased/118597-improve-autofill-suggestions-for-usernames.yml5
-rw-r--r--changelogs/unreleased/296945-update-the-container-registry-ui-header-copy-to-remove-expiration-.yml5
-rw-r--r--changelogs/unreleased/321057-refine-the-user-interface-related-to-package-lists-and-the-search-.yml5
-rw-r--r--changelogs/unreleased/322098-limit-projectauthorizations-refresh-jobs-to-distinct-users.yml6
-rw-r--r--changelogs/unreleased/nfriend-fix-ref-selector-double-dropdown.yml5
-rw-r--r--changelogs/unreleased/switch-order-of-buttons-configure-feature-flag-modal.yml5
-rw-r--r--config/feature_flags/development/ci_mini_pipeline_gl_dropdown.yml8
-rw-r--r--config/initializers/validate_puma.rb4
-rw-r--r--config/sidekiq_queues.yml4
-rw-r--r--db/post_migrate/20210210093901_backfill_updated_at_after_repository_storage_move.rb6
-rw-r--r--doc/administration/pages/index.md2
-rw-r--r--doc/ci/environments/protected_environments.md4
-rw-r--r--doc/development/contributing/style_guides.md2
-rw-r--r--doc/development/documentation/styleguide/index.md1
-rw-r--r--doc/development/fe_guide/style/scss.md2
-rw-r--r--doc/development/packages.md58
-rw-r--r--doc/topics/autodevops/index.md31
-rw-r--r--doc/topics/autodevops/stages.md73
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md10
-rw-r--r--doc/user/group/import/index.md1
-rw-r--r--doc/user/packages/dependency_proxy/index.md18
-rw-r--r--doc/user/project/autocomplete_characters.md47
-rw-r--r--doc/user/project/releases/index.md2
-rw-r--r--lib/api/entities/project_repository_storage_move.rb9
-rw-r--r--lib/api/entities/projects/repository_storage_move.rb11
-rw-r--r--lib/api/project_repository_storage_moves.rb24
-rw-r--r--lib/bulk_imports/common/transformers/award_emoji_transformer.rb25
-rw-r--r--lib/bulk_imports/common/transformers/prohibited_attributes_transformer.rb2
-rw-r--r--lib/bulk_imports/common/transformers/user_reference_transformer.rb39
-rw-r--r--lib/bulk_imports/pipeline.rb18
-rw-r--r--lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move.rb2
-rw-r--r--lib/gitlab/runtime.rb5
-rw-r--r--locale/gitlab.pot9
-rw-r--r--package.json22
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb3
-rwxr-xr-xscripts/static-analysis2
-rw-r--r--spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb14
-rw-r--r--spec/controllers/import/bulk_imports_controller_spec.rb29
-rw-r--r--spec/factories/dependency_proxy.rb3
-rw-r--r--spec/factories/project_repository_storage_moves.rb12
-rw-r--r--spec/features/issues/gfm_autocomplete_spec.rb37
-rw-r--r--spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb217
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb89
-rw-r--r--spec/fixtures/dependency_proxy/manifest44
-rw-r--r--spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js23
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js132
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js36
-rw-r--r--spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap2
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js9
-rw-r--r--spec/frontend/pipelines/pipelines_table_row_spec.js9
-rw-r--r--spec/frontend/pipelines/stage_spec.js314
-rw-r--r--spec/frontend/registry/explorer/components/list_page/registry_header_spec.js37
-rw-r--r--spec/lib/api/entities/projects/repository_storage_move_spec.rb (renamed from spec/lib/api/entities/project_repository_storage_move_spec.rb)2
-rw-r--r--spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb6
-rw-r--r--spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb (renamed from spec/lib/bulk_imports/common/transformers/award_emoji_transformer_spec.rb)25
-rw-r--r--spec/lib/bulk_imports/pipeline_spec.rb23
-rw-r--r--spec/models/dependency_proxy/manifest_spec.rb20
-rw-r--r--spec/models/group_spec.rb23
-rw-r--r--spec/models/project_repository_storage_move_spec.rb2
-rw-r--r--spec/models/projects/repository_storage_move_spec.rb35
-rw-r--r--spec/requests/api/project_repository_storage_moves_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb4
-rw-r--r--spec/services/bulk_import_service_spec.rb17
-rw-r--r--spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb46
-rw-r--r--spec/services/dependency_proxy/head_manifest_service_spec.rb9
-rw-r--r--spec/services/dependency_proxy/pull_manifest_service_spec.rb6
-rw-r--r--spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb4
-rw-r--r--spec/services/projects/update_service_spec.rb2
-rw-r--r--spec/support/helpers/dependency_proxy_helpers.rb4
-rw-r--r--spec/uploaders/dependency_proxy/file_uploader_spec.rb46
-rw-r--r--spec/workers/project_schedule_bulk_repository_shard_moves_worker_spec.rb4
-rw-r--r--spec/workers/project_update_repository_storage_worker_spec.rb2
-rw-r--r--spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb12
-rw-r--r--spec/workers/projects/update_repository_storage_worker_spec.rb15
113 files changed, 1338 insertions, 1062 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 42d44849316..81e42e07b08 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-c45afa70f5bd9723f0836ff228a11bc896c45511
+771df64aaf511cc3c64d7b55aee2d961941bfdab
diff --git a/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue b/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue
index 5fcca11e695..77e40039b43 100644
--- a/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue
+++ b/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue
@@ -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 }}
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index d209a971c39..32692d81125 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -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) {
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
index 8aeab59cb65..d444cc77aa7 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
@@ -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;
}
diff --git a/app/assets/javascripts/pages/projects/feature_flags_user_lists/show/index.js b/app/assets/javascripts/pages/projects/feature_flags_user_lists/show/index.js
index bccd9dce2ec..2dca0ea7f29 100644
--- a/app/assets/javascripts/pages/projects/feature_flags_user_lists/show/index.js
+++ b/app/assets/javascripts/pages/projects/feature_flags_user_lists/show/index.js
@@ -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();
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
index f5dfb9e72d5..b597b389d6e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/stage.vue
@@ -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>
diff --git a/app/assets/javascripts/projects/feature_flags_user_lists/show/index.js b/app/assets/javascripts/projects/feature_flags_user_lists/show/index.js
new file mode 100644
index 00000000000..2bd3e57322d
--- /dev/null
+++ b/app/assets/javascripts/projects/feature_flags_user_lists/show/index.js
@@ -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 } });
+ },
+ });
+}
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
index 10ad99d5956..5bd13322ebb 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue
@@ -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)"
/>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
index 8d7505dfbae..6d2ff9ea7b6 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue
@@ -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 }];
},
},
};
diff --git a/app/assets/javascripts/registry/explorer/constants/expiration_policies.js b/app/assets/javascripts/registry/explorer/constants/expiration_policies.js
index 48a6a015461..40f9b09a982 100644
--- a/app/assets/javascripts/registry/explorer/constants/expiration_policies.js
+++ b/app/assets/javascripts/registry/explorer/constants/expiration_policies.js
@@ -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}',
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue
index 8cad9b4ecfc..625d491db6a 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/registry/explorer/pages/list.vue
@@ -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>
diff --git a/app/assets/javascripts/vue_shared/components/registry/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
index 9db5d6953d7..4ade75e705e 100644
--- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -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"
diff --git a/app/assets/stylesheets/components/ref_selector.scss b/app/assets/stylesheets/components/ref_selector.scss
index 970a7b967ee..b23fb4363af 100644
--- a/app/assets/stylesheets/components/ref_selector.scss
+++ b/app/assets/stylesheets/components/ref_selector.scss
@@ -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;
+ }
}
}
diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss
index ae36f7e3ac1..6ef7f912ea9 100644
--- a/app/assets/stylesheets/page_bundles/pipelines.scss
+++ b/app/assets/stylesheets/page_bundles/pipelines.scss
@@ -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();
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index c20d795b810..23e368a2e73 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -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;
}
diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
index 0f640397320..e2c104f88a4 100644
--- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb
+++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
@@ -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
diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb
index ef32ba4d119..9b44828607c 100644
--- a/app/controllers/import/bulk_imports_controller.rb
+++ b/app/controllers/import/bulk_imports_controller.rb
@@ -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
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index ffdd9fca95b..133490164df 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -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
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index c9e9a34ad88..6cf03cd42a6 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -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)
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index aa1c6ab7550..28f4ac4b052 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -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]
diff --git a/app/models/dependency_proxy.rb b/app/models/dependency_proxy.rb
index 9cbaf7e9884..0ed17921aaa 100644
--- a/app/models/dependency_proxy.rb
+++ b/app/models/dependency_proxy.rb
@@ -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_'
diff --git a/app/models/dependency_proxy/manifest.rb b/app/models/dependency_proxy/manifest.rb
index f3c7f34e0d7..d613d5708f0 100644
--- a/app/models/dependency_proxy/manifest.rb
+++ b/app/models/dependency_proxy/manifest.rb
@@ -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
diff --git a/app/models/group.rb b/app/models/group.rb
index afcf3fe69e6..82142a7fc3f 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -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
diff --git a/app/models/project.rb b/app/models/project.rb
index 08e3203a81e..7e0b43e69ac 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -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
diff --git a/app/models/project_repository_storage_move.rb b/app/models/project_repository_storage_move.rb
index 1e3782a1fb5..e54489ddb88 100644
--- a/app/models/project_repository_storage_move.rb
+++ b/app/models/project_repository_storage_move.rb
@@ -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
diff --git a/app/models/projects/repository_storage_move.rb b/app/models/projects/repository_storage_move.rb
new file mode 100644
index 00000000000..f4411e0b4fd
--- /dev/null
+++ b/app/models/projects/repository_storage_move.rb
@@ -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
diff --git a/app/services/bulk_import_service.rb b/app/services/bulk_import_service.rb
index 29439a79afe..4e13e967dbd 100644
--- a/app/services/bulk_import_service.rb
+++ b/app/services/bulk_import_service.rb
@@ -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
diff --git a/app/services/dependency_proxy/find_or_create_manifest_service.rb b/app/services/dependency_proxy/find_or_create_manifest_service.rb
index 6b46f5e4c59..ee608d715aa 100644
--- a/app/services/dependency_proxy/find_or_create_manifest_service.rb
+++ b/app/services/dependency_proxy/find_or_create_manifest_service.rb
@@ -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
diff --git a/app/services/dependency_proxy/head_manifest_service.rb b/app/services/dependency_proxy/head_manifest_service.rb
index 87d9c417c98..ecc3eb77399 100644
--- a/app/services/dependency_proxy/head_manifest_service.rb
+++ b/app/services/dependency_proxy/head_manifest_service.rb
@@ -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
diff --git a/app/services/dependency_proxy/pull_manifest_service.rb b/app/services/dependency_proxy/pull_manifest_service.rb
index 5c804489fd1..737414c396e 100644
--- a/app/services/dependency_proxy/pull_manifest_service.rb
+++ b/app/services/dependency_proxy/pull_manifest_service.rb
@@ -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
diff --git a/app/services/projects/schedule_bulk_repository_shard_moves_service.rb b/app/services/projects/schedule_bulk_repository_shard_moves_service.rb
index 53de9abdb59..98fc2e22967 100644
--- a/app/services/projects/schedule_bulk_repository_shard_moves_service.rb
+++ b/app/services/projects/schedule_bulk_repository_shard_moves_service.rb
@@ -25,7 +25,7 @@ module Projects
override :schedule_bulk_worker_klass
def self.schedule_bulk_worker_klass
- ::ProjectScheduleBulkRepositoryShardMovesWorker
+ ::Projects::ScheduleBulkRepositoryShardMovesWorker
end
end
end
diff --git a/app/uploaders/dependency_proxy/file_uploader.rb b/app/uploaders/dependency_proxy/file_uploader.rb
index b67a22bae4d..c46539bafaa 100644
--- a/app/uploaders/dependency_proxy/file_uploader.rb
+++ b/app/uploaders/dependency_proxy/file_uploader.rb
@@ -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
diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml
index 899e58050af..fa6bd021e45 100644
--- a/app/views/groups/registry/repositories/index.html.haml
+++ b/app/views/groups/registry/repositories/index.html.haml
@@ -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,
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index a2009b96c0d..bbef5150a62 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -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,
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index b8481382577..70f884ed7ee 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -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:
diff --git a/app/workers/project_schedule_bulk_repository_shard_moves_worker.rb b/app/workers/project_schedule_bulk_repository_shard_moves_worker.rb
index 4d2a6b47e3c..23d1594e4d9 100644
--- a/app/workers/project_schedule_bulk_repository_shard_moves_worker.rb
+++ b/app/workers/project_schedule_bulk_repository_shard_moves_worker.rb
@@ -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
diff --git a/app/workers/project_update_repository_storage_worker.rb b/app/workers/project_update_repository_storage_worker.rb
index 5636eec8233..0d68c0e16f8 100644
--- a/app/workers/project_update_repository_storage_worker.rb
+++ b/app/workers/project_update_repository_storage_worker.rb
@@ -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
diff --git a/app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb b/app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb
new file mode 100644
index 00000000000..3841ae9b922
--- /dev/null
+++ b/app/workers/projects/schedule_bulk_repository_shard_moves_worker.rb
@@ -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
diff --git a/app/workers/projects/update_repository_storage_worker.rb b/app/workers/projects/update_repository_storage_worker.rb
new file mode 100644
index 00000000000..f4c44458446
--- /dev/null
+++ b/app/workers/projects/update_repository_storage_worker.rb
@@ -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
diff --git a/changelogs/unreleased/118597-improve-autofill-suggestions-for-usernames.yml b/changelogs/unreleased/118597-improve-autofill-suggestions-for-usernames.yml
new file mode 100644
index 00000000000..9feb3af9984
--- /dev/null
+++ b/changelogs/unreleased/118597-improve-autofill-suggestions-for-usernames.yml
@@ -0,0 +1,5 @@
+---
+title: Improve at.js members autocomplete matching
+merge_request: 54681
+author:
+type: changed
diff --git a/changelogs/unreleased/296945-update-the-container-registry-ui-header-copy-to-remove-expiration-.yml b/changelogs/unreleased/296945-update-the-container-registry-ui-header-copy-to-remove-expiration-.yml
new file mode 100644
index 00000000000..cc9dec724ef
--- /dev/null
+++ b/changelogs/unreleased/296945-update-the-container-registry-ui-header-copy-to-remove-expiration-.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Expiration Policy text from container registry header
+merge_request: 54665
+author:
+type: changed
diff --git a/changelogs/unreleased/321057-refine-the-user-interface-related-to-package-lists-and-the-search-.yml b/changelogs/unreleased/321057-refine-the-user-interface-related-to-package-lists-and-the-search-.yml
new file mode 100644
index 00000000000..fea15e883f3
--- /dev/null
+++ b/changelogs/unreleased/321057-refine-the-user-interface-related-to-package-lists-and-the-search-.yml
@@ -0,0 +1,5 @@
+---
+title: Refine Registry Lists and Search Bar UI
+merge_request: 54549
+author:
+type: changed
diff --git a/changelogs/unreleased/322098-limit-projectauthorizations-refresh-jobs-to-distinct-users.yml b/changelogs/unreleased/322098-limit-projectauthorizations-refresh-jobs-to-distinct-users.yml
new file mode 100644
index 00000000000..6908a48cee0
--- /dev/null
+++ b/changelogs/unreleased/322098-limit-projectauthorizations-refresh-jobs-to-distinct-users.yml
@@ -0,0 +1,6 @@
+---
+title: Consider only distinct user ids for project authorizations refresh jobs for
+ group members
+merge_request: 54697
+author:
+type: performance
diff --git a/changelogs/unreleased/nfriend-fix-ref-selector-double-dropdown.yml b/changelogs/unreleased/nfriend-fix-ref-selector-double-dropdown.yml
new file mode 100644
index 00000000000..61d549bd15b
--- /dev/null
+++ b/changelogs/unreleased/nfriend-fix-ref-selector-double-dropdown.yml
@@ -0,0 +1,5 @@
+---
+title: Fix double scrollbar in ref selector dropdown
+merge_request: 54719
+author:
+type: fixed
diff --git a/changelogs/unreleased/switch-order-of-buttons-configure-feature-flag-modal.yml b/changelogs/unreleased/switch-order-of-buttons-configure-feature-flag-modal.yml
new file mode 100644
index 00000000000..7257d63bf1d
--- /dev/null
+++ b/changelogs/unreleased/switch-order-of-buttons-configure-feature-flag-modal.yml
@@ -0,0 +1,5 @@
+---
+title: Change the order of action buttons in the configure feature flags modal
+merge_request: 54731
+author:
+type: changed
diff --git a/config/feature_flags/development/ci_mini_pipeline_gl_dropdown.yml b/config/feature_flags/development/ci_mini_pipeline_gl_dropdown.yml
deleted file mode 100644
index 50e1e6715df..00000000000
--- a/config/feature_flags/development/ci_mini_pipeline_gl_dropdown.yml
+++ /dev/null
@@ -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
diff --git a/config/initializers/validate_puma.rb b/config/initializers/validate_puma.rb
index 9723765d00f..b8a237f3c15 100644
--- a/config/initializers/validate_puma.rb
+++ b/config/initializers/validate_puma.rb
@@ -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?
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 8fb44e63a77..e74f27a9012 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -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
diff --git a/db/post_migrate/20210210093901_backfill_updated_at_after_repository_storage_move.rb b/db/post_migrate/20210210093901_backfill_updated_at_after_repository_storage_move.rb
index 0631cc8095e..efd0eeb9d47 100644
--- a/db/post_migrate/20210210093901_backfill_updated_at_after_repository_storage_move.rb
+++ b/db/post_migrate/20210210093901_backfill_updated_at_after_repository_storage_move.rb
@@ -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,
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 4febd8648d1..7ff7cc62e31 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -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`:
diff --git a/doc/ci/environments/protected_environments.md b/doc/ci/environments/protected_environments.md
index b52c2701daf..ee307607a6b 100644
--- a/doc/ci/environments/protected_environments.md
+++ b/doc/ci/environments/protected_environments.md
@@ -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).
diff --git a/doc/development/contributing/style_guides.md b/doc/development/contributing/style_guides.md
index db1d01383ad..c2ef6c4e200 100644
--- a/doc/development/contributing/style_guides.md
+++ b/doc/development/contributing/style_guides.md
@@ -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`.
diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md
index 7737aa58506..29f2da8858f 100644
--- a/doc/development/documentation/styleguide/index.md
+++ b/doc/development/documentation/styleguide/index.md
@@ -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).) | --- |
diff --git a/doc/development/fe_guide/style/scss.md b/doc/development/fe_guide/style/scss.md
index 13c44a098bc..a39cc1305b7 100644
--- a/doc/development/fe_guide/style/scss.md
+++ b/doc/development/fe_guide/style/scss.md
@@ -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
diff --git a/doc/development/packages.md b/doc/development/packages.md
index 2476c876b77..5a3ca79f992 100644
--- a/doc/development/packages.md
+++ b/doc/development/packages.md
@@ -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
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index 44072147fde..4bd0bddfd7d 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -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 -->
diff --git a/doc/topics/autodevops/stages.md b/doc/topics/autodevops/stages.md
index 9051f6926bf..83b4f3d4f7b 100644
--- a/doc/topics/autodevops/stages.md
+++ b/doc/topics/autodevops/stages.md
@@ -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.
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
index 25ab4ec173c..2b230f9fb6e 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -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.
diff --git a/doc/user/group/import/index.md b/doc/user/group/import/index.md
index 7f02c7b169f..42439d27c83 100644
--- a/doc/user/group/import/index.md
+++ b/doc/user/group/import/index.md
@@ -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.
diff --git a/doc/user/packages/dependency_proxy/index.md b/doc/user/packages/dependency_proxy/index.md
index fdf0caba090..5b096f846e6 100644
--- a/doc/user/packages/dependency_proxy/index.md
+++ b/doc/user/packages/dependency_proxy/index.md
@@ -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).
diff --git a/doc/user/project/autocomplete_characters.md b/doc/user/project/autocomplete_characters.md
index 8d8ff942d05..aa8c21fc781 100644
--- a/doc/user/project/autocomplete_characters.md
+++ b/doc/user/project/autocomplete_characters.md
@@ -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 |
-
-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.
+| 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 |
+
+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`.
diff --git a/doc/user/project/releases/index.md b/doc/user/project/releases/index.md
index b5751797870..922973b8ae7 100644
--- a/doc/user/project/releases/index.md
+++ b/doc/user/project/releases/index.md
@@ -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.
diff --git a/lib/api/entities/project_repository_storage_move.rb b/lib/api/entities/project_repository_storage_move.rb
deleted file mode 100644
index 191bbaf19d7..00000000000
--- a/lib/api/entities/project_repository_storage_move.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module Entities
- class ProjectRepositoryStorageMove < BasicRepositoryStorageMove
- expose :project, using: Entities::ProjectIdentity
- end
- end
-end
diff --git a/lib/api/entities/projects/repository_storage_move.rb b/lib/api/entities/projects/repository_storage_move.rb
new file mode 100644
index 00000000000..7844cd36e02
--- /dev/null
+++ b/lib/api/entities/projects/repository_storage_move.rb
@@ -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
diff --git a/lib/api/project_repository_storage_moves.rb b/lib/api/project_repository_storage_moves.rb
index 196b7d88500..ab5d8b3a888 100644
--- a/lib/api/project_repository_storage_moves.rb
+++ b/lib/api/project_repository_storage_moves.rb
@@ -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
diff --git a/lib/bulk_imports/common/transformers/award_emoji_transformer.rb b/lib/bulk_imports/common/transformers/award_emoji_transformer.rb
deleted file mode 100644
index 676c71f1316..00000000000
--- a/lib/bulk_imports/common/transformers/award_emoji_transformer.rb
+++ /dev/null
@@ -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
diff --git a/lib/bulk_imports/common/transformers/prohibited_attributes_transformer.rb b/lib/bulk_imports/common/transformers/prohibited_attributes_transformer.rb
index 37b64923d56..38e2fc0b1b9 100644
--- a/lib/bulk_imports/common/transformers/prohibited_attributes_transformer.rb
+++ b/lib/bulk_imports/common/transformers/prohibited_attributes_transformer.rb
@@ -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)
diff --git a/lib/bulk_imports/common/transformers/user_reference_transformer.rb b/lib/bulk_imports/common/transformers/user_reference_transformer.rb
new file mode 100644
index 00000000000..ca077b4ef43
--- /dev/null
+++ b/lib/bulk_imports/common/transformers/user_reference_transformer.rb
@@ -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
diff --git a/lib/bulk_imports/pipeline.rb b/lib/bulk_imports/pipeline.rb
index 254ae0e792e..f8e5edc27d8 100644
--- a/lib/bulk_imports/pipeline.rb
+++ b/lib/bulk_imports/pipeline.rb
@@ -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
diff --git a/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move.rb b/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move.rb
index 61eb3b332de..7484027a0fa 100644
--- a/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move.rb
+++ b/lib/gitlab/background_migration/backfill_project_updated_at_after_repository_storage_move.rb
@@ -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 (
diff --git a/lib/gitlab/runtime.rb b/lib/gitlab/runtime.rb
index 647ac169f05..968ef06b085 100644
--- a/lib/gitlab/runtime.rb
+++ b/lib/gitlab/runtime.rb
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 736f7d426c1..72fe8763a82 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/package.json b/package.json
index e5f3e284a09..15dd7546b37 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
index 2ce8c749e43..45222d2ec7e 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb
@@ -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
diff --git a/scripts/static-analysis b/scripts/static-analysis
index 11b460e7c1b..f0134b3df18 100755
--- a/scripts/static-analysis
+++ b/scripts/static-analysis
@@ -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,
diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
index 39cbdfb9123..83775dcdbdf 100644
--- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
+++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb
@@ -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)
diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb
index 08a54f112bb..70ed7c5812b 100644
--- a/spec/controllers/import/bulk_imports_controller_spec.rb
+++ b/spec/controllers/import/bulk_imports_controller_spec.rb
@@ -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
diff --git a/spec/factories/dependency_proxy.rb b/spec/factories/dependency_proxy.rb
index de95df19876..94a7986a8fa 100644
--- a/spec/factories/dependency_proxy.rb
+++ b/spec/factories/dependency_proxy.rb
@@ -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
diff --git a/spec/factories/project_repository_storage_moves.rb b/spec/factories/project_repository_storage_moves.rb
index 5df2b7c32d6..018b6cde32b 100644
--- a/spec/factories/project_repository_storage_moves.rb
+++ b/spec/factories/project_repository_storage_moves.rb
@@ -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
diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb
index eb3d0f223db..42c5f0ec4a8 100644
--- a/spec/features/issues/gfm_autocomplete_spec.rb
+++ b/spec/features/issues/gfm_autocomplete_spec.rb
@@ -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
diff --git a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
index 1ef6d2a1068..c0dc2ec3baf 100644
--- a/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
+++ b/spec/features/merge_request/user_sees_mini_pipeline_graph_spec.rb
@@ -9,166 +9,149 @@ 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|
- 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
- end
+ dropdown_selector = '[data-testid="mini-pipeline-graph-dropdown"]'
- 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
+ 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
+ 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
+ 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') }
+ 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
- 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
+ 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')
+ 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') }
+ # 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')
+ 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') }
+ 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
+ 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
+ describe 'build list toggle' do
+ let(:toggle) do
+ find(dropdown_selector)
+ first(dropdown_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)
+ # 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_toggle_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_toggle_selector)
- page.driver.browser.action.release(toggle.native).perform
+ 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_toggle_selector)
+ 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_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(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(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
+ 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 'shows tooltip when hovered' do
- toggle.hover
+ it 'shows tooltip when hovered' do
+ toggle.hover
- expect(page).to have_selector('.tooltip')
- end
+ 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
+ describe 'builds list menu' do
+ let(:toggle) do
+ find(dropdown_selector)
+ first(dropdown_selector)
+ end
- before do
- toggle.click
- wait_for_requests
- 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 '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
+ it 'closes when toggle is clicked again' do
+ toggle.click
- expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
- end
+ expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
+ end
- it 'closes when clicking somewhere else' do
- find('body').click
+ it 'closes when clicking somewhere else' do
+ find('body').click
- expect(toggle.find(:xpath, '..')).not_to have_selector('.mini-pipeline-graph-dropdown-menu')
- end
+ 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
+ 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')
+ 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
+ expect(current_path).to eql(project_job_path(project, build))
+ end
- it 'shows tooltip when hovered' do
- build_item.hover
+ it 'shows tooltip when hovered' do
+ build_item.hover
- expect(page).to have_selector('.tooltip')
- end
+ expect(page).to have_selector('.tooltip')
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
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
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 6421d3db2cd..2b407868f35 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -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
- let!(:build) do
- create(:ci_build, :pending, pipeline: pipeline,
- stage: 'build',
- name: 'build')
- end
+ context 'mini pipeline graph' do
+ let!(:build) do
+ create(:ci_build, :pending, 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
+ dropdown_selector = '[data-testid="mini-pipeline-graph-dropdown"]'
- 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
+ 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_toggle_selector)
- 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_toggle_selector).click
+ context 'when clicking a stage badge' do
+ it 'opens a dropdown' do
+ find(dropdown_selector).click
- expect(page).to have_link build.name
- end
+ 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
+ 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
+ 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
+ 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
+ it 'displays the failure reason' do
+ find(dropdown_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)
diff --git a/spec/fixtures/dependency_proxy/manifest b/spec/fixtures/dependency_proxy/manifest
index a899d05d697..ed543883d60 100644
--- a/spec/fixtures/dependency_proxy/manifest
+++ b/spec/fixtures/dependency_proxy/manifest
@@ -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"
}
]
} \ No newline at end of file
diff --git a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
index 84e71ffd204..27ec6a7280f 100644
--- a/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
+++ b/spec/frontend/feature_flags/components/configure_feature_flags_modal_spec.js
@@ -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 () => {
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 08368e1f2ca..13dbda9cf55 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -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);
+ });
+ });
});
});
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index 835b8af5359..1feff861c1e 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -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 });
+ });
});
});
});
diff --git a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
index 4a75deebcf9..73a64875026 100644
--- a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
@@ -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"
>
<!---->
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index ccf3f0b6667..aeca210b8ce 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -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();
diff --git a/spec/frontend/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js
index 8edf891b443..ff997912384 100644
--- a/spec/frontend/pipelines/pipelines_table_row_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_row_spec.js
@@ -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);
});
});
diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js
index 87b43558252..bc4c5600d99 100644
--- a/spec/frontend/pipelines/stage_spec.js
+++ b/spec/frontend/pipelines/stage_spec.js
@@ -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,
- );
+ describe('default appearance', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
- });
+ 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);
});
+ });
- it('when request fails should close the dropdown', async () => {
- mock.onGet('path.json').reply(500);
+ describe('when update dropdown is changed', () => {
+ beforeEach(() => {
createComponent();
- wrapper.find({ ref: 'dropdown' }).trigger('click');
+ });
+ });
- expect(isDropdownOpen()).toBe(true);
+ describe('when user opens dropdown and stage request is successful', () => {
+ beforeEach(async () => {
+ mock.onGet(dropdownPath).reply(200, stageReply);
+ createComponent();
- wrapper.find('button').trigger('click');
+ await openStageDropdown();
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',
- );
- });
+ 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');
});
- 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('should refresh when updateDropdown is set to true', async () => {
+ expect(mock.history.get).toHaveLength(1);
- it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
- createComponent();
-
- await clickCiAction();
+ wrapper.setProps({ updateDropdown: true });
+ await axios.waitForAll();
- expect($.fn.dropdown).toHaveBeenCalledWith('toggle');
- });
- });
+ expect(mock.history.get).toHaveLength(2);
});
});
- 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 user opens dropdown and stage request fails', () => {
+ beforeEach(async () => {
+ mock.onGet(dropdownPath).reply(500);
+ createComponent();
- beforeEach(() => {
- glFeatures = { ciMiniPipelineGlDropdown: true };
+ await openStageDropdown();
+ await axios.waitForAll();
});
- 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);
- });
+ it('should close the dropdown', () => {
+ expect(findDropdown().classes('show')).toBe(false);
});
+ });
- describe('with successful request', () => {
- beforeEach(() => {
- mock.onGet('path.json').reply(200, stageReply);
- createComponent();
+ 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',
+ },
});
+ await axios.waitForAll();
+ });
- it('should render the received data and emit `clickedDropdown` event', async () => {
- await openGlDropdown();
- await axios.waitForAll();
+ it('should update the stage to request the new endpoint provided', async () => {
+ await openStageDropdown();
+ await axios.waitForAll();
- expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
- expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
- });
+ expect(findDropdownMenu().text()).toContain('this is the updated content');
});
+ });
- it('when request fails should close the dropdown', async () => {
- mock.onGet('path.json').reply(500);
-
- createComponent();
+ describe('pipelineActionRequestComplete', () => {
+ beforeEach(() => {
+ mock.onGet(dropdownPath).reply(200, stageReply);
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ });
- await openGlDropdown();
+ const clickCiAction = async () => {
+ await openStageDropdown();
await axios.waitForAll();
- expect(findDropdown().classes('show')).toBe(false);
- });
+ findCiActionBtn().trigger('click');
+ await axios.waitForAll();
+ };
- 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',
- },
- });
- 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);
- });
-
- const clickCiAction = async () => {
- await openGlDropdown();
- await axios.waitForAll();
-
- findCiActionBtn().trigger('click');
- await axios.waitForAll();
- };
-
- describe('within pipeline table', () => {
- beforeEach(() => {
- createComponent({ type: 'PIPELINES_TABLE' });
- });
-
- it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
- await clickCiAction();
-
- expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
- });
+ createComponent();
});
- describe('in MR widget', () => {
- beforeEach(() => {
- jest.spyOn($.fn, 'dropdown');
- createComponent();
- });
-
- it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
- const hidden = jest.fn();
+ it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
+ const hidden = jest.fn();
- wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
+ wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
- expect(hidden).toHaveBeenCalledTimes(0);
+ expect(hidden).toHaveBeenCalledTimes(0);
- await clickCiAction();
+ await clickCiAction();
- expect(hidden).toHaveBeenCalledTimes(1);
- });
+ expect(hidden).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js
index 07256d2bbf5..11a3acd9eb9 100644
--- a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js
@@ -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: '' },
- ]);
- });
- });
- });
- });
});
});
diff --git a/spec/lib/api/entities/project_repository_storage_move_spec.rb b/spec/lib/api/entities/projects/repository_storage_move_spec.rb
index b0102dc376a..81f5d98b713 100644
--- a/spec/lib/api/entities/project_repository_storage_move_spec.rb
+++ b/spec/lib/api/entities/projects/repository_storage_move_spec.rb
@@ -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 }
diff --git a/spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb
index 03d138b227c..08a82bc84ed 100644
--- a/spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb
+++ b/spec/lib/bulk_imports/common/transformers/prohibited_attributes_transformer_spec.rb
@@ -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
diff --git a/spec/lib/bulk_imports/common/transformers/award_emoji_transformer_spec.rb b/spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb
index 5b560a30bf5..ff11a10bfe9 100644
--- a/spec/lib/bulk_imports/common/transformers/award_emoji_transformer_spec.rb
+++ b/spec/lib/bulk_imports/common/transformers/user_reference_transformer_spec.rb
@@ -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
diff --git a/spec/lib/bulk_imports/pipeline_spec.rb b/spec/lib/bulk_imports/pipeline_spec.rb
index 2bbb55f9419..c882e3d26ea 100644
--- a/spec/lib/bulk_imports/pipeline_spec.rb
+++ b/spec/lib/bulk_imports/pipeline_spec.rb
@@ -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
diff --git a/spec/models/dependency_proxy/manifest_spec.rb b/spec/models/dependency_proxy/manifest_spec.rb
index aa2e73356dd..4203644c003 100644
--- a/spec/models/dependency_proxy/manifest_spec.rb
+++ b/spec/models/dependency_proxy/manifest_spec.rb
@@ -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' }
- context 'no manifest exists' do
- let_it_be(:file_name) { 'foo' }
+ subject { DependencyProxy::Manifest.find_or_initialize_by_file_name_or_digest(file_name: file_name, digest: digest) }
+ context 'no manifest exists' do
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
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 6afd422e554..7d18e0503d8 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -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
diff --git a/spec/models/project_repository_storage_move_spec.rb b/spec/models/project_repository_storage_move_spec.rb
index 88535f6dd6e..eb193a44680 100644
--- a/spec/models/project_repository_storage_move_spec.rb
+++ b/spec/models/project_repository_storage_move_spec.rb
@@ -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
diff --git a/spec/models/projects/repository_storage_move_spec.rb b/spec/models/projects/repository_storage_move_spec.rb
new file mode 100644
index 00000000000..ab0ad81f77a
--- /dev/null
+++ b/spec/models/projects/repository_storage_move_spec.rb
@@ -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
diff --git a/spec/requests/api/project_repository_storage_moves_spec.rb b/spec/requests/api/project_repository_storage_moves_spec.rb
index 5e200312d1f..b40645ba2de 100644
--- a/spec/requests/api/project_repository_storage_moves_spec.rb
+++ b/spec/requests/api/project_repository_storage_moves_spec.rb
@@ -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
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index ad36777184a..8a68db6ae4b 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -2818,7 +2818,7 @@ RSpec.describe API::Projects do
Sidekiq::Testing.fake! do
put(api("/projects/#{new_project.id}", user), params: { repository_storage: unknown_storage, issues_enabled: false })
end
- end.not_to change(ProjectUpdateRepositoryStorageWorker.jobs, :size)
+ end.not_to change(Projects::UpdateRepositoryStorageWorker.jobs, :size)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['issues_enabled']).to eq(false)
@@ -2845,7 +2845,7 @@ RSpec.describe API::Projects do
Sidekiq::Testing.fake! do
put(api("/projects/#{new_project.id}", admin), params: { repository_storage: 'test_second_storage' })
end
- end.to change(ProjectUpdateRepositoryStorageWorker.jobs, :size).by(1)
+ end.to change(Projects::UpdateRepositoryStorageWorker.jobs, :size).by(1)
expect(response).to have_gitlab_http_status(:ok)
end
diff --git a/spec/services/bulk_import_service_spec.rb b/spec/services/bulk_import_service_spec.rb
index e4a50b9d523..1b60a5cb0f8 100644
--- a/spec/services/bulk_import_service_spec.rb
+++ b/spec/services/bulk_import_service_spec.rb
@@ -48,5 +48,22 @@ RSpec.describe BulkImportService do
subject.execute
end
+
+ it 'returns success ServiceResponse' do
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_success
+ end
+
+ it 'returns ServiceResponse with error if validation fails' do
+ params[0][:source_full_path] = nil
+
+ result = subject.execute
+
+ expect(result).to be_a(ServiceResponse)
+ expect(result).to be_error
+ expect(result.message).to eq("Validation failed: Source full path can't be blank")
+ end
end
end
diff --git a/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb b/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb
index c375e5a2fa3..40a2f954786 100644
--- a/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb
+++ b/spec/services/dependency_proxy/find_or_create_manifest_service_spec.rb
@@ -10,7 +10,12 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
let(:manifest) { dependency_proxy_manifest.file.read }
let(:group) { dependency_proxy_manifest.group }
let(:token) { Digest::SHA256.hexdigest('123') }
- let(:headers) { { 'docker-content-digest' => dependency_proxy_manifest.digest } }
+ let(:headers) do
+ {
+ 'docker-content-digest' => dependency_proxy_manifest.digest,
+ 'content-type' => dependency_proxy_manifest.content_type
+ }
+ end
describe '#execute' do
subject { described_class.new(group, image, tag, token).execute }
@@ -18,22 +23,37 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
context 'when no manifest exists' do
let_it_be(:image) { 'new-image' }
- before do
- stub_manifest_head(image, tag, digest: dependency_proxy_manifest.digest)
- stub_manifest_download(image, tag, headers: headers)
+ shared_examples 'downloading the manifest' do
+ it 'downloads manifest from remote registry if there is no cached one', :aggregate_failures do
+ expect { subject }.to change { group.dependency_proxy_manifests.count }.by(1)
+ expect(subject[:status]).to eq(:success)
+ expect(subject[:manifest]).to be_a(DependencyProxy::Manifest)
+ expect(subject[:manifest]).to be_persisted
+ end
end
- it 'downloads manifest from remote registry if there is no cached one', :aggregate_failures do
- expect { subject }.to change { group.dependency_proxy_manifests.count }.by(1)
- expect(subject[:status]).to eq(:success)
- expect(subject[:manifest]).to be_a(DependencyProxy::Manifest)
- expect(subject[:manifest]).to be_persisted
+ context 'successful head request' do
+ before do
+ stub_manifest_head(image, tag, headers: headers)
+ stub_manifest_download(image, tag, headers: headers)
+ end
+
+ it_behaves_like 'downloading the manifest'
+ end
+
+ context 'failed head request' do
+ before do
+ stub_manifest_head(image, tag, status: :error)
+ stub_manifest_download(image, tag, headers: headers)
+ end
+
+ it_behaves_like 'downloading the manifest'
end
end
context 'when manifest exists' do
before do
- stub_manifest_head(image, tag, digest: dependency_proxy_manifest.digest)
+ stub_manifest_head(image, tag, headers: headers)
end
shared_examples 'using the cached manifest' do
@@ -48,15 +68,17 @@ RSpec.describe DependencyProxy::FindOrCreateManifestService do
context 'when digest is stale' do
let(:digest) { 'new-digest' }
+ let(:content_type) { 'new-content-type' }
before do
- stub_manifest_head(image, tag, digest: digest)
- stub_manifest_download(image, tag, headers: { 'docker-content-digest' => digest })
+ stub_manifest_head(image, tag, headers: { 'docker-content-digest' => digest, 'content-type' => content_type })
+ stub_manifest_download(image, tag, headers: { 'docker-content-digest' => digest, 'content-type' => content_type })
end
it 'downloads the new manifest and updates the existing record', :aggregate_failures do
expect(subject[:status]).to eq(:success)
expect(subject[:manifest]).to eq(dependency_proxy_manifest)
+ expect(subject[:manifest].content_type).to eq(content_type)
expect(subject[:manifest].digest).to eq(digest)
end
end
diff --git a/spec/services/dependency_proxy/head_manifest_service_spec.rb b/spec/services/dependency_proxy/head_manifest_service_spec.rb
index 7c7ebe4d181..9c1e4d650f8 100644
--- a/spec/services/dependency_proxy/head_manifest_service_spec.rb
+++ b/spec/services/dependency_proxy/head_manifest_service_spec.rb
@@ -8,12 +8,19 @@ RSpec.describe DependencyProxy::HeadManifestService do
let(:tag) { 'latest' }
let(:token) { Digest::SHA256.hexdigest('123') }
let(:digest) { '12345' }
+ let(:content_type) { 'foo' }
+ let(:headers) do
+ {
+ 'docker-content-digest' => digest,
+ 'content-type' => content_type
+ }
+ end
subject { described_class.new(image, tag, token).execute }
context 'remote request is successful' do
before do
- stub_manifest_head(image, tag, digest: digest)
+ stub_manifest_head(image, tag, headers: headers)
end
it { expect(subject[:status]).to eq(:success) }
diff --git a/spec/services/dependency_proxy/pull_manifest_service_spec.rb b/spec/services/dependency_proxy/pull_manifest_service_spec.rb
index b760839d1fb..b3053174cc0 100644
--- a/spec/services/dependency_proxy/pull_manifest_service_spec.rb
+++ b/spec/services/dependency_proxy/pull_manifest_service_spec.rb
@@ -9,7 +9,10 @@ RSpec.describe DependencyProxy::PullManifestService do
let(:token) { Digest::SHA256.hexdigest('123') }
let(:manifest) { { foo: 'bar' }.to_json }
let(:digest) { '12345' }
- let(:headers) { { 'docker-content-digest' => digest } }
+ let(:content_type) { 'foo' }
+ let(:headers) do
+ { 'docker-content-digest' => digest, 'content-type' => content_type }
+ end
subject { described_class.new(image, tag, token).execute_with_manifest(&method(:check_response)) }
@@ -25,6 +28,7 @@ RSpec.describe DependencyProxy::PullManifestService do
expect(response[:status]).to eq(:success)
expect(response[:file].read).to eq(manifest)
expect(response[:digest]).to eq(digest)
+ expect(response[:content_type]).to eq(content_type)
end
subject
diff --git a/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb b/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb
index 15c9d1e5925..2dc4a56368b 100644
--- a/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb
+++ b/spec/services/projects/schedule_bulk_repository_shard_moves_service_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Projects::ScheduleBulkRepositoryShardMovesService do
it_behaves_like 'moves repository shard in bulk' do
let_it_be_with_reload(:container) { create(:project, :repository).tap { |project| project.track_project_repository } }
- let(:move_service_klass) { ProjectRepositoryStorageMove }
- let(:bulk_worker_klass) { ::ProjectScheduleBulkRepositoryShardMovesWorker }
+ let(:move_service_klass) { Projects::RepositoryStorageMove }
+ let(:bulk_worker_klass) { ::Projects::ScheduleBulkRepositoryShardMovesWorker }
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index a59b6adf346..b9e909e8615 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -551,7 +551,7 @@ RSpec.describe Projects::UpdateService do
expect(project).to be_repository_read_only
expect(project.repository_storage_moves.last).to have_attributes(
- state: ::ProjectRepositoryStorageMove.state_machines[:state].states[:scheduled].value,
+ state: ::Projects::RepositoryStorageMove.state_machines[:state].states[:scheduled].value,
source_storage_name: 'default',
destination_storage_name: 'test_second_storage'
)
diff --git a/spec/support/helpers/dependency_proxy_helpers.rb b/spec/support/helpers/dependency_proxy_helpers.rb
index ebb849628bf..0d8f56906e3 100644
--- a/spec/support/helpers/dependency_proxy_helpers.rb
+++ b/spec/support/helpers/dependency_proxy_helpers.rb
@@ -18,11 +18,11 @@ module DependencyProxyHelpers
.to_return(status: status, body: body || manifest, headers: headers)
end
- def stub_manifest_head(image, tag, status: 200, body: nil, digest: '123456')
+ def stub_manifest_head(image, tag, status: 200, body: nil, headers: {})
manifest_url = registry.manifest_url(image, tag)
stub_full_request(manifest_url, method: :head)
- .to_return(status: status, body: body, headers: { 'docker-content-digest' => digest } )
+ .to_return(status: status, body: body, headers: headers )
end
def stub_blob_download(image, blob_sha, status = 200, body = '123456')
diff --git a/spec/uploaders/dependency_proxy/file_uploader_spec.rb b/spec/uploaders/dependency_proxy/file_uploader_spec.rb
index 724a9c42f47..6e94a661d6d 100644
--- a/spec/uploaders/dependency_proxy/file_uploader_spec.rb
+++ b/spec/uploaders/dependency_proxy/file_uploader_spec.rb
@@ -2,25 +2,43 @@
require 'spec_helper'
RSpec.describe DependencyProxy::FileUploader do
- let(:blob) { create(:dependency_proxy_blob) }
- let(:uploader) { described_class.new(blob, :file) }
- let(:path) { Gitlab.config.dependency_proxy.storage_path }
+ describe 'DependencyProxy::Blob uploader' do
+ let_it_be(:blob) { create(:dependency_proxy_blob) }
+ let_it_be(:path) { Gitlab.config.dependency_proxy.storage_path }
+ let(:uploader) { described_class.new(blob, :file) }
- subject { uploader }
+ subject { uploader }
- it_behaves_like "builds correct paths",
- store_dir: %r[\h{2}/\h{2}],
- cache_dir: %r[/dependency_proxy/tmp/cache],
- work_dir: %r[/dependency_proxy/tmp/work]
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\h{2}/\h{2}],
+ cache_dir: %r[/dependency_proxy/tmp/cache],
+ work_dir: %r[/dependency_proxy/tmp/work]
+
+ context 'object store is remote' do
+ before do
+ stub_dependency_proxy_object_storage
+ end
- context 'object store is remote' do
- before do
- stub_dependency_proxy_object_storage
+ include_context 'with storage', described_class::Store::REMOTE
+
+ it_behaves_like "builds correct paths",
+ store_dir: %r[\h{2}/\h{2}]
end
+ end
- include_context 'with storage', described_class::Store::REMOTE
+ describe 'DependencyProxy::Manifest uploader' do
+ let_it_be(:manifest) { create(:dependency_proxy_manifest) }
+ let_it_be(:initial_content_type) { 'application/json' }
+ let_it_be(:fixture_file) { fixture_file_upload('spec/fixtures/dependency_proxy/manifest', initial_content_type) }
+ let(:uploader) { described_class.new(manifest, :file) }
- it_behaves_like "builds correct paths",
- store_dir: %r[\h{2}/\h{2}]
+ subject { uploader }
+
+ it 'will change upload file content type to match the model content type', :aggregate_failures do
+ uploader.cache!(fixture_file)
+
+ expect(uploader.file.content_type).to eq(manifest.content_type)
+ expect(uploader.file.content_type).not_to eq(initial_content_type)
+ end
end
end
diff --git a/spec/workers/project_schedule_bulk_repository_shard_moves_worker_spec.rb b/spec/workers/project_schedule_bulk_repository_shard_moves_worker_spec.rb
index fb762593d75..f284e1ab8c6 100644
--- a/spec/workers/project_schedule_bulk_repository_shard_moves_worker_spec.rb
+++ b/spec/workers/project_schedule_bulk_repository_shard_moves_worker_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe ProjectScheduleBulkRepositoryShardMovesWorker do
it_behaves_like 'schedules bulk repository shard moves' do
let_it_be_with_reload(:container) { create(:project, :repository).tap { |project| project.track_project_repository } }
- let(:move_service_klass) { ProjectRepositoryStorageMove }
- let(:worker_klass) { ProjectUpdateRepositoryStorageWorker }
+ let(:move_service_klass) { Projects::RepositoryStorageMove }
+ let(:worker_klass) { Projects::UpdateRepositoryStorageWorker }
end
end
diff --git a/spec/workers/project_update_repository_storage_worker_spec.rb b/spec/workers/project_update_repository_storage_worker_spec.rb
index 490f1f5a2ad..6924e8a93a3 100644
--- a/spec/workers/project_update_repository_storage_worker_spec.rb
+++ b/spec/workers/project_update_repository_storage_worker_spec.rb
@@ -10,6 +10,6 @@ RSpec.describe ProjectUpdateRepositoryStorageWorker do
let_it_be(:repository_storage_move) { create(:project_repository_storage_move) }
let(:service_klass) { Projects::UpdateRepositoryStorageService }
- let(:repository_storage_move_klass) { ProjectRepositoryStorageMove }
+ let(:repository_storage_move_klass) { Projects::RepositoryStorageMove }
end
end
diff --git a/spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb b/spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb
new file mode 100644
index 00000000000..24957a35b72
--- /dev/null
+++ b/spec/workers/projects/schedule_bulk_repository_shard_moves_worker_spec.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::ScheduleBulkRepositoryShardMovesWorker do
+ it_behaves_like 'schedules bulk repository shard moves' do
+ let_it_be_with_reload(:container) { create(:project, :repository).tap { |project| project.track_project_repository } }
+
+ let(:move_service_klass) { Projects::RepositoryStorageMove }
+ let(:worker_klass) { Projects::UpdateRepositoryStorageWorker }
+ end
+end
diff --git a/spec/workers/projects/update_repository_storage_worker_spec.rb b/spec/workers/projects/update_repository_storage_worker_spec.rb
new file mode 100644
index 00000000000..7570d706325
--- /dev/null
+++ b/spec/workers/projects/update_repository_storage_worker_spec.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::UpdateRepositoryStorageWorker do
+ subject { described_class.new }
+
+ it_behaves_like 'an update storage move worker' do
+ let_it_be_with_refind(:container) { create(:project, :repository) }
+ let_it_be(:repository_storage_move) { create(:project_repository_storage_move) }
+
+ let(:service_klass) { Projects::UpdateRepositoryStorageService }
+ let(:repository_storage_move_klass) { Projects::RepositoryStorageMove }
+ end
+end