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:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-06-11 15:09:49 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-06-11 15:09:49 +0300
commitdcf94a76413ddb50148bdac7b189afb7bffa7580 (patch)
treeb5ecff1d1aea4d3ad95d728531f95f80c00a47ca
parenta350f877c4246fee981690388239d1e19e17202a (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml2
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column_trigger.vue10
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue11
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue3
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue9
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue4
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue3
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue109
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql8
-rw-r--r--app/assets/javascripts/boards/stores/actions.js25
-rw-r--r--app/assets/javascripts/nav/utils/has_menu_expanded.js7
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue4
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue5
-rw-r--r--app/assets/javascripts/repository/graphql.js20
-rw-r--r--app/assets/javascripts/repository/log_tree.js12
-rw-r--r--app/assets/javascripts/repository/queries/commit.query.graphql4
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue4
-rw-r--r--app/models/container_repository.rb11
-rw-r--r--app/models/project.rb9
-rw-r--r--app/models/project_feature.rb8
-rw-r--r--app/policies/project_policy.rb6
-rw-r--r--app/services/github.rb4
-rw-r--r--app/workers/concerns/security_scans_queue.rb2
-rw-r--r--config/feature_flags/development/read_container_registry_access_level.yml8
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--config/initializers/1_settings.rb2
-rw-r--r--config/metrics/settings/20210204124908_mattermost_enabled.yml12
-rw-r--r--db/post_migrate/20210606143426_add_index_for_container_registry_access_level.rb36
-rw-r--r--db/schema_migrations/202106061434261
-rw-r--r--db/structure.sql4
-rw-r--r--doc/api/graphql/reference/index.md44
-rw-r--r--doc/ci/yaml/README.md3
-rw-r--r--doc/development/background_migrations.md21
-rw-r--r--doc/development/usage_ping/dictionary.md4
-rw-r--r--lib/api/entities/project.rb8
-rw-r--r--locale/gitlab.pot5
-rw-r--r--scripts/review_apps/base-config.yaml4
-rwxr-xr-xscripts/review_apps/review-apps.sh13
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js6
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js135
-rw-r--r--spec/frontend/boards/stores/actions_spec.js51
-rw-r--r--spec/frontend/nav/components/responsive_app_spec.js17
-rw-r--r--spec/frontend/repository/components/table/row_spec.js1
-rw-r--r--spec/frontend/repository/log_tree_spec.js5
-rw-r--r--spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js7
-rw-r--r--spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb3
-rw-r--r--spec/lib/sidebars/projects/menus/settings_menu_spec.rb3
-rw-r--r--spec/models/container_repository_spec.rb34
-rw-r--r--spec/models/project_spec.rb54
-rw-r--r--spec/policies/project_policy_spec.rb161
-rw-r--r--spec/requests/api/projects_spec.rb26
-rw-r--r--workhorse/internal/httprs/httprs_test.go8
53 files changed, 544 insertions, 417 deletions
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index dc1923a22c3..affbeedc25a 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -321,6 +321,8 @@
changes: *ci-build-images-patterns
- <<: *if-dot-com-gitlab-org-and-security-merge-request
changes: *code-qa-patterns
+ - <<: *if-dot-com-gitlab-org-default-branch
+ changes: *code-qa-patterns
- <<: *if-dot-com-gitlab-org-schedule
.build-images:rules:build-assets-image:
diff --git a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
index 85f001d9d61..2aee84b805f 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column_trigger.vue
@@ -1,21 +1,25 @@
<script>
import { GlButton } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import Tracking from '~/tracking';
export default {
components: {
GlButton,
},
+ mixins: [Tracking.mixin()],
methods: {
...mapActions(['setAddColumnFormVisibility']),
+ handleClick() {
+ this.setAddColumnFormVisibility(true);
+ this.track('click_button', { label: 'create_list' });
+ },
},
};
</script>
<template>
<div class="gl-ml-3 gl-display-flex gl-align-items-center" data-testid="boards-create-list">
- <gl-button variant="confirm" @click="setAddColumnFormVisibility(true)"
- >{{ __('Create list') }}
- </gl-button>
+ <gl-button variant="confirm" @click="handleClick">{{ __('Create list') }} </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 53c893e0734..1e780f9ef84 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,5 +1,6 @@
<script>
import { mapActions, mapState } from 'vuex';
+import Tracking from '~/tracking';
import BoardCardInner from './board_card_inner.vue';
export default {
@@ -7,6 +8,7 @@ export default {
components: {
BoardCardInner,
},
+ mixins: [Tracking.mixin()],
props: {
list: {
type: Object,
@@ -58,6 +60,7 @@ export default {
this.toggleBoardItemMultiSelection(this.item);
} else {
this.toggleBoardItem({ boardItem: this.item });
+ this.track('click_card', { label: 'right_sidebar' });
}
},
},
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 8fd72cfceb5..f83927ea4e7 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -2,7 +2,6 @@
import { GlDrawer } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
-import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
@@ -10,6 +9,7 @@ import { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
+import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
export default {
@@ -18,10 +18,10 @@ export default {
GlDrawer,
BoardSidebarTitle,
SidebarAssigneesWidget,
+ SidebarDateWidget,
SidebarConfidentialityWidget,
BoardSidebarTimeTracker,
BoardSidebarLabelsSelect,
- BoardSidebarDueDate,
SidebarSubscriptionsWidget,
SidebarDropdownWidget,
BoardSidebarWeightInput: () =>
@@ -116,7 +116,12 @@ export default {
/>
</div>
<board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
- <board-sidebar-due-date />
+ <sidebar-date-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ data-testid="sidebar-due-date"
+ />
<board-sidebar-labels-select class="labels" />
<board-sidebar-weight-input v-if="weightFeatureAvailable" class="weight" />
<sidebar-confidentiality-widget
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 4f396bddb90..81740b5cd17 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -5,6 +5,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_options';
import { sprintf, __ } from '~/locale';
import defaultSortableConfig from '~/sortable/sortable_config';
+import Tracking from '~/tracking';
import eventHub from '../eventhub';
import BoardCard from './board_card.vue';
import BoardNewIssue from './board_new_issue.vue';
@@ -23,6 +24,7 @@ export default {
GlLoadingIcon,
GlIntersectionObserver,
},
+ mixins: [Tracking.mixin()],
inject: {
canAdminList: {
default: false,
@@ -155,6 +157,7 @@ export default {
},
handleDragOnStart() {
sortableStart();
+ this.track('drag_card', { label: 'board' });
},
handleDragOnEnd(params) {
sortableEnd();
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index a73786dd613..bf8396f52a6 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -14,6 +14,7 @@ import { isScopedLabel, parseBoolean } from '~/lib/utils/common_utils';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { n__, s__, __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
+import Tracking from '~/tracking';
import AccessorUtilities from '../../lib/utils/accessor';
import { inactiveId, LIST, ListType } from '../constants';
import eventHub from '../eventhub';
@@ -38,6 +39,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [Tracking.mixin()],
inject: {
boardId: {
default: '',
@@ -155,6 +157,8 @@ export default {
}
this.setActiveId({ id: this.list.id, sidebarType: LIST });
+
+ this.track('click_button', { label: 'list_settings' });
},
showScopedLabels(label) {
return this.scopedLabelsAvailable && isScopedLabel(label);
@@ -176,6 +180,11 @@ export default {
// When expanding/collapsing, the tooltip on the caret button sometimes stays open.
// Close all tooltips manually to prevent dangling tooltips.
this.$root.$emit(BV_HIDE_TOOLTIP);
+
+ this.track('click_toggle_button', {
+ label: 'toggle_list',
+ property: collapsed ? 'closed' : 'open',
+ });
},
addToLocalStorage() {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 3d7f1f38a34..75975c77df5 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -6,6 +6,7 @@ import boardsStore from '~/boards/stores/boards_store';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
+import Tracking from '~/tracking';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
@@ -21,7 +22,7 @@ export default {
BoardSettingsListTypes: () =>
import('ee_component/boards/components/board_settings_list_types.vue'),
},
- mixins: [glFeatureFlagMixin()],
+ mixins: [glFeatureFlagMixin(), Tracking.mixin()],
inject: ['canAdminList'],
data() {
return {
@@ -72,6 +73,7 @@ export default {
// eslint-disable-next-line no-alert
if (window.confirm(__('Are you sure you want to remove this list?'))) {
if (this.shouldUseGraphQL || this.isEpicBoard) {
+ this.track('click_button', { label: 'remove_list' });
this.removeList(this.activeId);
} else {
this.activeList.destroy();
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
index fdb60d0ae6a..30e304b8a65 100644
--- a/app/assets/javascripts/boards/components/config_toggle.vue
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -3,6 +3,7 @@ import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import { formType } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import { s__, __ } from '~/locale';
+import Tracking from '~/tracking';
export default {
components: {
@@ -12,6 +13,7 @@ export default {
GlTooltip: GlTooltipDirective,
GlModalDirective,
},
+ mixins: [Tracking.mixin()],
props: {
boardsStore: {
type: Object,
@@ -37,6 +39,7 @@ export default {
},
methods: {
showPage() {
+ this.track('click_button', { label: 'edit_board' });
eventHub.$emit('showBoardModal', formType.edit);
if (this.boardsStore) {
this.boardsStore.showPage(formType.edit);
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
deleted file mode 100644
index 87ae17025ea..00000000000
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_due_date.vue
+++ /dev/null
@@ -1,109 +0,0 @@
-<script>
-import { GlButton, GlDatepicker } from '@gitlab/ui';
-import { mapGetters, mapActions } from 'vuex';
-import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
-import { __ } from '~/locale';
-
-export default {
- components: {
- BoardEditableItem,
- GlButton,
- GlDatepicker,
- },
- data() {
- return {
- loading: false,
- };
- },
- computed: {
- ...mapGetters(['activeBoardItem', 'projectPathForActiveIssue']),
- hasDueDate() {
- return this.activeBoardItem.dueDate != null;
- },
- parsedDueDate() {
- if (!this.hasDueDate) {
- return null;
- }
-
- return parsePikadayDate(this.activeBoardItem.dueDate);
- },
- formattedDueDate() {
- if (!this.hasDueDate) {
- return '';
- }
-
- return dateInWords(this.parsedDueDate, true);
- },
- },
- methods: {
- ...mapActions(['setActiveIssueDueDate', 'setError']),
- async openDatePicker() {
- await this.$nextTick();
- this.$refs.datePicker.calendar.show();
- },
- async setDueDate(date) {
- this.loading = true;
- this.$refs.sidebarItem.collapse();
-
- try {
- const dueDate = date ? formatDate(date, 'yyyy-mm-dd') : null;
- await this.setActiveIssueDueDate({ dueDate, projectPath: this.projectPathForActiveIssue });
- } catch (e) {
- this.setError({ message: this.$options.i18n.updateDueDateError });
- } finally {
- this.loading = false;
- }
- },
- },
- i18n: {
- dueDate: __('Due date'),
- removeDueDate: __('remove due date'),
- updateDueDateError: __('An error occurred when updating the issue due date'),
- },
-};
-</script>
-
-<template>
- <board-editable-item
- ref="sidebarItem"
- class="board-sidebar-due-date"
- data-testid="sidebar-due-date"
- :title="$options.i18n.dueDate"
- :loading="loading"
- @open="openDatePicker"
- >
- <template v-if="hasDueDate" #collapsed>
- <div class="gl-display-flex gl-align-items-center">
- <strong class="gl-text-gray-900">{{ formattedDueDate }}</strong>
- <span class="gl-mx-2">-</span>
- <gl-button
- variant="link"
- class="gl-text-gray-500!"
- data-testid="reset-button"
- :disabled="loading"
- @click="setDueDate(null)"
- >
- {{ $options.i18n.removeDueDate }}
- </gl-button>
- </div>
- </template>
- <gl-datepicker
- ref="datePicker"
- :value="parsedDueDate"
- show-clear-button
- @input="setDueDate"
- @clear="setDueDate(null)"
- />
- </board-editable-item>
-</template>
-<style>
-/*
- * This can be removed after closing:
- * https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1048
- */
-.board-sidebar-due-date .gl-datepicker,
-.board-sidebar-due-date .gl-datepicker-input {
- width: 100%;
-}
-</style>
diff --git a/app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql
deleted file mode 100644
index bbea248cf85..00000000000
--- a/app/assets/javascripts/boards/graphql/issue_set_due_date.mutation.graphql
+++ /dev/null
@@ -1,8 +0,0 @@
-mutation issueSetDueDate($input: UpdateIssueInput!) {
- updateIssue(input: $input) {
- issue {
- dueDate
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 694483e92ea..d4893f9eca7 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -35,7 +35,6 @@ import {
import boardLabelsQuery from '../graphql/board_labels.query.graphql';
import groupProjectsQuery from '../graphql/group_projects.query.graphql';
import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
-import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.graphql';
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import * as types from './mutation_types';
@@ -559,30 +558,6 @@ export default {
});
},
- setActiveIssueDueDate: async ({ commit, getters }, input) => {
- const { activeBoardItem } = getters;
- const { data } = await gqlClient.mutate({
- mutation: issueSetDueDateMutation,
- variables: {
- input: {
- iid: String(activeBoardItem.iid),
- projectPath: input.projectPath,
- dueDate: input.dueDate,
- },
- },
- });
-
- if (data.updateIssue?.errors?.length > 0) {
- throw new Error(data.updateIssue.errors);
- }
-
- commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: activeBoardItem.id,
- prop: 'dueDate',
- value: data.updateIssue.issue.dueDate,
- });
- },
-
setActiveItemSubscribed: async ({ commit, getters, state }, input) => {
const { activeBoardItem, isEpicBoard } = getters;
const { fullPath, issuableType } = state;
diff --git a/app/assets/javascripts/nav/utils/has_menu_expanded.js b/app/assets/javascripts/nav/utils/has_menu_expanded.js
index 4e4d6c7c71e..5f126bbdf76 100644
--- a/app/assets/javascripts/nav/utils/has_menu_expanded.js
+++ b/app/assets/javascripts/nav/utils/has_menu_expanded.js
@@ -1,5 +1,2 @@
-export const hasMenuExpanded = () => {
- const header = document.querySelector('.header-content');
-
- return Boolean(header?.classList.contains('menu-expanded'));
-};
+export const hasMenuExpanded = () =>
+ Boolean(document.querySelector('.header-content.menu-expanded'));
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 22dffb7d2db..ca5711de49c 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -51,6 +51,9 @@ export default {
};
},
computed: {
+ totalEntries() {
+ return Object.values(this.entries).flat().length;
+ },
tableCaption() {
if (this.isLoading) {
return sprintf(
@@ -111,6 +114,7 @@ export default {
:submodule-tree-url="entry.treeUrl"
:lfs-oid="entry.lfsOid"
:loading-path="loadingPath"
+ :total-entries="totalEntries"
/>
</template>
<template v-if="isLoading">
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 1b7fb6a63df..62f863db871 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -43,12 +43,17 @@ export default {
type: this.type,
path: this.currentPath,
projectPath: this.projectPath,
+ maxOffset: this.totalEntries,
};
},
},
},
mixins: [getRefMixin, glFeatureFlagMixin()],
props: {
+ totalEntries: {
+ type: Number,
+ required: true,
+ },
id: {
type: String,
required: true,
diff --git a/app/assets/javascripts/repository/graphql.js b/app/assets/javascripts/repository/graphql.js
index 4a4b9d115b7..4892e54ebef 100644
--- a/app/assets/javascripts/repository/graphql.js
+++ b/app/assets/javascripts/repository/graphql.js
@@ -17,15 +17,21 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
const defaultClient = createDefaultClient(
{
Query: {
- commit(_, { path, fileName, type }) {
+ commit(_, { path, fileName, type, maxOffset }) {
return new Promise((resolve) => {
- fetchLogsTree(defaultClient, path, '0', {
- resolve,
- entry: {
- name: fileName,
- type,
+ fetchLogsTree(
+ defaultClient,
+ path,
+ '0',
+ {
+ resolve,
+ entry: {
+ name: fileName,
+ type,
+ },
},
- });
+ maxOffset,
+ );
});
},
readme(_, { url }) {
diff --git a/app/assets/javascripts/repository/log_tree.js b/app/assets/javascripts/repository/log_tree.js
index 9001bcd8fc3..7d9d962b6f4 100644
--- a/app/assets/javascripts/repository/log_tree.js
+++ b/app/assets/javascripts/repository/log_tree.js
@@ -7,6 +7,7 @@ import refQuery from './queries/ref.query.graphql';
const fetchpromises = {};
const resolvers = {};
+let maxOffset;
export function resolveCommit(commits, path, { resolve, entry }) {
const commit = commits.find(
@@ -18,7 +19,15 @@ export function resolveCommit(commits, path, { resolve, entry }) {
}
}
-export function fetchLogsTree(client, path, offset, resolver = null) {
+export function fetchLogsTree(client, path, offset, resolver = null, _maxOffset = null) {
+ if (_maxOffset) {
+ maxOffset = _maxOffset;
+ }
+
+ if (Number(offset) > maxOffset) {
+ return Promise.resolve();
+ }
+
if (resolver) {
if (!resolvers[path]) {
resolvers[path] = [resolver];
@@ -60,6 +69,7 @@ export function fetchLogsTree(client, path, offset, resolver = null) {
fetchLogsTree(client, path, headerLogsOffset);
} else {
delete resolvers[path];
+ maxOffset = null;
}
});
diff --git a/app/assets/javascripts/repository/queries/commit.query.graphql b/app/assets/javascripts/repository/queries/commit.query.graphql
index e4aeaaff8fe..7ae4a3b984a 100644
--- a/app/assets/javascripts/repository/queries/commit.query.graphql
+++ b/app/assets/javascripts/repository/queries/commit.query.graphql
@@ -1,7 +1,7 @@
#import "ee_else_ce/repository/queries/commit.fragment.graphql"
-query getCommit($fileName: String!, $type: String!, $path: String!) {
- commit(path: $path, fileName: $fileName, type: $type) @client {
+query getCommit($fileName: String!, $type: String!, $path: String!, $maxOffset: Number!) {
+ commit(path: $path, fileName: $fileName, type: $type, maxOffset: $maxOffset) @client {
...TreeEntryCommit
}
}
diff --git a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
index 6a68e914b84..c3dfa5f8b14 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
@@ -112,6 +112,9 @@ export default {
dateValue() {
return this.issuable?.[this.dateType] || null;
},
+ firstDay() {
+ return gon.first_day_of_week;
+ },
isLoading() {
return this.$apollo.queries.issuable.loading || this.loading;
},
@@ -286,6 +289,7 @@ export default {
ref="datePicker"
class="gl-relative"
:default-date="parsedDate"
+ :first-day="firstDay"
show-clear-button
autocomplete="off"
@input="setDate"
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 1be0d626b19..2d28a81f462 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -24,8 +24,15 @@ class ContainerRepository < ApplicationRecord
scope :for_group_and_its_subgroups, ->(group) do
project_scope = Project
.for_group_and_its_subgroups(group)
- .with_container_registry
- .select(:id)
+
+ project_scope =
+ if Feature.enabled?(:read_container_registry_access_level, group, default_enabled: :yaml)
+ project_scope.with_feature_enabled(:container_registry)
+ else
+ project_scope.with_container_registry
+ end
+
+ project_scope = project_scope.select(:id)
joins("INNER JOIN (#{project_scope.to_sql}) projects on projects.id=container_repositories.project_id")
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 3af1e671f1c..6895bba7cf7 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -2612,6 +2612,15 @@ class Project < ApplicationRecord
!!read_attribute(:merge_requests_author_approval)
end
+ def container_registry_enabled
+ if Feature.enabled?(:read_container_registry_access_level, self.namespace, default_enabled: :yaml)
+ project_feature.container_registry_enabled?
+ else
+ read_attribute(:container_registry_enabled)
+ end
+ end
+ alias_method :container_registry_enabled?, :container_registry_enabled
+
private
def set_container_registry_access_level
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index eb4ad327438..f6e889396c6 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -24,7 +24,11 @@ class ProjectFeature < ApplicationRecord
set_available_features(FEATURES)
- PRIVATE_FEATURES_MIN_ACCESS_LEVEL = { merge_requests: Gitlab::Access::REPORTER, metrics_dashboard: Gitlab::Access::REPORTER }.freeze
+ PRIVATE_FEATURES_MIN_ACCESS_LEVEL = {
+ merge_requests: Gitlab::Access::REPORTER,
+ metrics_dashboard: Gitlab::Access::REPORTER,
+ container_registry: Gitlab::Access::REPORTER
+ }.freeze
PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT = { repository: Gitlab::Access::REPORTER }.freeze
class << self
@@ -92,7 +96,7 @@ class ProjectFeature < ApplicationRecord
def set_container_registry_access_level
self.container_registry_access_level =
- if project&.container_registry_enabled
+ if project&.read_attribute(:container_registry_enabled)
ENABLED
else
DISABLED
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 164d73fe332..4dfdbd87a34 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -51,7 +51,11 @@ class ProjectPolicy < BasePolicy
desc "Container registry is disabled"
condition(:container_registry_disabled, scope: :subject) do
- !project.container_registry_enabled
+ if ::Feature.enabled?(:read_container_registry_access_level, @subject&.namespace, default_enabled: :yaml)
+ !access_allowed_to?(:container_registry)
+ else
+ !project.container_registry_enabled
+ end
end
desc "Project has an external wiki"
diff --git a/app/services/github.rb b/app/services/github.rb
deleted file mode 100644
index e76e7351ab9..00000000000
--- a/app/services/github.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-module Github
-end
diff --git a/app/workers/concerns/security_scans_queue.rb b/app/workers/concerns/security_scans_queue.rb
index f731317bb37..27e97169926 100644
--- a/app/workers/concerns/security_scans_queue.rb
+++ b/app/workers/concerns/security_scans_queue.rb
@@ -8,6 +8,6 @@ module SecurityScansQueue
included do
queue_namespace :security_scans
- feature_category :static_application_security_testing
+ feature_category :vulnerability_management
end
end
diff --git a/config/feature_flags/development/read_container_registry_access_level.yml b/config/feature_flags/development/read_container_registry_access_level.yml
new file mode 100644
index 00000000000..9f4a223a169
--- /dev/null
+++ b/config/feature_flags/development/read_container_registry_access_level.yml
@@ -0,0 +1,8 @@
+---
+name: read_container_registry_access_level
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55071
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332751
+milestone: '14.0'
+type: development
+group: group::package
+default_enabled: false
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 557fbd0a107..c9b056ce956 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -505,7 +505,7 @@ production: &base
ee_cron_jobs:
# Schedule snapshots for all devops adoption segments
analytics_devops_adoption_create_all_snapshots_worker:
- cron: 0 4 * * 0
+ cron: 0 0 1 * *
# Snapshot active users statistics
historical_data_worker:
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index d589bc6069a..8f4c6492cad 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -586,7 +586,7 @@ end
Gitlab.ee do
Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= Settingslogic.new({})
- Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker']['cron'] ||= '0 4 * * 0'
+ Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker']['cron'] ||= '0 0 1 * *'
Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker']['job_class'] = 'Analytics::DevopsAdoption::CreateAllSnapshotsWorker'
Settings.cron_jobs['active_user_count_threshold_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['active_user_count_threshold_worker']['cron'] ||= '0 12 * * *'
diff --git a/config/metrics/settings/20210204124908_mattermost_enabled.yml b/config/metrics/settings/20210204124908_mattermost_enabled.yml
index a3c88cf976e..14303726d1f 100644
--- a/config/metrics/settings/20210204124908_mattermost_enabled.yml
+++ b/config/metrics/settings/20210204124908_mattermost_enabled.yml
@@ -1,16 +1,18 @@
---
key_path: mattermost_enabled
description: Whether Mattermost is enabled
-product_section: growth
-product_stage: growth
-product_group: group::product intelligence
-product_category: collection
+product_section: dev
+product_stage: create
+product_group: group::ecosystem
+product_category: integrations
value_type: boolean
status: data_available
time_frame: none
data_source: system
distribution:
- ce
+- ee
tier:
- free
-skip_validation: true
+- premium
+- ultimate
diff --git a/db/post_migrate/20210606143426_add_index_for_container_registry_access_level.rb b/db/post_migrate/20210606143426_add_index_for_container_registry_access_level.rb
new file mode 100644
index 00000000000..64d37054eb8
--- /dev/null
+++ b/db/post_migrate/20210606143426_add_index_for_container_registry_access_level.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class AddIndexForContainerRegistryAccessLevel < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::SchemaHelpers
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ INDEX = 'index_project_features_on_project_id_include_container_registry'
+
+ def up
+ if index_exists_by_name?('project_features', INDEX)
+ Gitlab::AppLogger.warn "Index not created because it already exists (this may be due to an aborted migration or similar): table_name: project_features, index_name: #{INDEX}"
+ return
+ end
+
+ begin
+ disable_statement_timeout do
+ execute "CREATE UNIQUE INDEX CONCURRENTLY #{INDEX} ON project_features " \
+ 'USING btree (project_id) INCLUDE (container_registry_access_level)'
+ end
+ rescue ActiveRecord::StatementInvalid => ex
+ raise "The index #{INDEX} couldn't be added: #{ex.message}"
+ end
+
+ create_comment(
+ 'INDEX',
+ INDEX,
+ 'Included column (container_registry_access_level) improves performance of the ContainerRepository.for_group_and_its_subgroups scope query'
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name('project_features', INDEX)
+ end
+end
diff --git a/db/schema_migrations/20210606143426 b/db/schema_migrations/20210606143426
new file mode 100644
index 00000000000..a8a2d7d784c
--- /dev/null
+++ b/db/schema_migrations/20210606143426
@@ -0,0 +1 @@
+1f99d446428ddac2a0fa7d64bdce9fc300bf02e88c35cdb3d726c501641e721d \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 49563bdb895..7220869c5ea 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -24144,6 +24144,10 @@ CREATE UNIQUE INDEX index_project_features_on_project_id ON project_features USI
CREATE INDEX index_project_features_on_project_id_bal_20 ON project_features USING btree (project_id) WHERE (builds_access_level = 20);
+CREATE UNIQUE INDEX index_project_features_on_project_id_include_container_registry ON project_features USING btree (project_id) INCLUDE (container_registry_access_level);
+
+COMMENT ON INDEX index_project_features_on_project_id_include_container_registry IS 'Included column (container_registry_access_level) improves performance of the ContainerRepository.for_group_and_its_subgroups scope query';
+
CREATE INDEX index_project_features_on_project_id_ral_20 ON project_features USING btree (project_id) WHERE (repository_access_level = 20);
CREATE INDEX index_project_group_links_on_group_id ON project_group_links USING btree (group_id);
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 4bf40e1c69a..be544331e8b 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -5033,6 +5033,29 @@ The edge type for [`DevopsAdoptionEnabledNamespace`](#devopsadoptionenablednames
| <a id="devopsadoptionenablednamespaceedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
| <a id="devopsadoptionenablednamespaceedgenode"></a>`node` | [`DevopsAdoptionEnabledNamespace`](#devopsadoptionenablednamespace) | The item at the end of the edge. |
+#### `DevopsAdoptionSnapshotConnection`
+
+The connection type for [`DevopsAdoptionSnapshot`](#devopsadoptionsnapshot).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="devopsadoptionsnapshotconnectionedges"></a>`edges` | [`[DevopsAdoptionSnapshotEdge]`](#devopsadoptionsnapshotedge) | A list of edges. |
+| <a id="devopsadoptionsnapshotconnectionnodes"></a>`nodes` | [`[DevopsAdoptionSnapshot]`](#devopsadoptionsnapshot) | A list of nodes. |
+| <a id="devopsadoptionsnapshotconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
+
+#### `DevopsAdoptionSnapshotEdge`
+
+The edge type for [`DevopsAdoptionSnapshot`](#devopsadoptionsnapshot).
+
+##### Fields
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="devopsadoptionsnapshotedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. |
+| <a id="devopsadoptionsnapshotedgenode"></a>`node` | [`DevopsAdoptionSnapshot`](#devopsadoptionsnapshot) | The item at the end of the edge. |
+
#### `DiscussionConnection`
The connection type for [`Discussion`](#discussion).
@@ -8191,9 +8214,28 @@ Enabled namespace for DevopsAdoption.
| ---- | ---- | ----------- |
| <a id="devopsadoptionenablednamespacedisplaynamespace"></a>`displayNamespace` | [`Namespace`](#namespace) | Namespace where data should be displayed. |
| <a id="devopsadoptionenablednamespaceid"></a>`id` | [`ID!`](#id) | ID of the enabled namespace. |
-| <a id="devopsadoptionenablednamespacelatestsnapshot"></a>`latestSnapshot` | [`DevopsAdoptionSnapshot`](#devopsadoptionsnapshot) | The latest adoption metrics for the enabled namespace. |
+| <a id="devopsadoptionenablednamespacelatestsnapshot"></a>`latestSnapshot` | [`DevopsAdoptionSnapshot`](#devopsadoptionsnapshot) | Metrics snapshot for previous month for the enabled namespace. |
| <a id="devopsadoptionenablednamespacenamespace"></a>`namespace` | [`Namespace`](#namespace) | Namespace which should be calculated. |
+#### Fields with arguments
+
+##### `DevopsAdoptionEnabledNamespace.snapshots`
+
+Data snapshots of the namespace.
+
+Returns [`DevopsAdoptionSnapshotConnection`](#devopsadoptionsnapshotconnection).
+
+This field returns a [connection](#connections). It accepts the
+four standard [pagination arguments](#connection-pagination-arguments):
+`before: String`, `after: String`, `first: Int`, `last: Int`.
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="devopsadoptionenablednamespacesnapshotsendtimeafter"></a>`endTimeAfter` | [`Time`](#time) | Filter to snapshots with month end after the provided date. |
+| <a id="devopsadoptionenablednamespacesnapshotsendtimebefore"></a>`endTimeBefore` | [`Time`](#time) | Filter to snapshots with month end before the provided date. |
+
### `DevopsAdoptionSnapshot`
Snapshot.
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index f4275914b0b..ae59c35318c 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1518,7 +1518,8 @@ job:
Glob patterns are interpreted with Ruby [`File.fnmatch`](https://docs.ruby-lang.org/en/2.7.0/File.html#method-c-fnmatch)
with the flags `File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB`.
-For performance reasons, GitLab matches a maximum of 10,000 `exists` patterns. After the 10,000th check, rules with patterned globs always match.
+For performance reasons, GitLab matches a maximum of 10,000 `exists` patterns or file paths. After the 10,000th check, rules with patterned globs always match.
+In other words, the `exists` rule always assumes a match in projects with more than 10,000 files.
#### `rules:allow_failure`
diff --git a/doc/development/background_migrations.md b/doc/development/background_migrations.md
index fb1e61642d7..31621a4bb0c 100644
--- a/doc/development/background_migrations.md
+++ b/doc/development/background_migrations.md
@@ -7,28 +7,25 @@ info: "See the Technical Writers assigned to Development Guidelines: https://abo
# Background migrations
-Background migrations can be used to perform data migrations that would
-otherwise take a very long time (hours, days, years, etc) to complete. For
-example, you can use background migrations to migrate data so that instead of
-storing data in a single JSON column the data is stored in a separate table.
+Background migrations should be used to perform data migrations whenever a
+migration exceeds [the time limits in our guidelines](database_review.md#timing-guidelines-for-migrations). For example, you can use background
+migrations to migrate data that's stored in a single JSON column
+to a separate table instead.
If the database cluster is considered to be in an unhealthy state, background
migrations automatically reschedule themselves for a later point in time.
## When To Use Background Migrations
-In the vast majority of cases you will want to use a regular Rails migration
-instead. Background migrations should be used when migrating _data_ in
-tables that have so many rows this process would take hours when performed in a
-regular Rails migration.
+You should use a background migration when you migrate _data_ in tables that have
+so many rows that the process would exceed [the time limits in our guidelines](database_review.md#timing-guidelines-for-migrations) if performed using a regular Rails migration.
-Background migrations _may_ also be used when executing numerous single-row queries
+- Background migrations should be used when migrating data in [high-traffic tables](migration_style_guide.md#high-traffic-tables).
+- Background migrations may also be used when executing numerous single-row queries
for every item on a large dataset. Typically, for single-record patterns, runtime is
largely dependent on the size of the dataset, hence it should be split accordingly
and put into background migrations.
-
-Background migrations _may not_ be used to perform schema migrations, they
-should only be used for data migrations.
+- Background migrations should not be used to perform schema migrations.
Some examples where background migrations can be useful:
diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md
index ea9d43ace8c..aae62801b62 100644
--- a/doc/development/usage_ping/dictionary.md
+++ b/doc/development/usage_ping/dictionary.md
@@ -7348,11 +7348,11 @@ Whether Mattermost is enabled
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/settings/20210204124908_mattermost_enabled.yml)
-Group: `group::product intelligence`
+Group: `group::ecosystem`
Status: `data_available`
-Tiers: `free`
+Tiers: `free`, `premium`, `ultimate`
### `object_store.artifacts.enabled`
diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb
index fabad33f52a..5bfbac4270f 100644
--- a/lib/api/entities/project.rb
+++ b/lib/api/entities/project.rb
@@ -43,7 +43,6 @@ module API
expose :visibility
expose :owner, using: Entities::UserBasic, unless: ->(project, options) { project.group }
expose :resolve_outdated_diff_discussions
- expose :container_registry_enabled
expose :container_expiration_policy, using: Entities::ContainerExpirationPolicy,
if: -> (project, _) { project.container_expiration_policy }
@@ -54,6 +53,13 @@ module API
expose(:wiki_enabled) { |project, options| project.feature_available?(:wiki, options[:current_user]) }
expose(:jobs_enabled) { |project, options| project.feature_available?(:builds, options[:current_user]) }
expose(:snippets_enabled) { |project, options| project.feature_available?(:snippets, options[:current_user]) }
+ expose(:container_registry_enabled) do |project, options|
+ if ::Feature.enabled?(:read_container_registry_access_level, project.namespace, default_enabled: :yaml)
+ project.feature_available?(:container_registry, options[:current_user])
+ else
+ project.read_attribute(:container_registry_enabled)
+ end
+ end
expose :service_desk_enabled
expose :service_desk_address
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5d197a876bd..24abd92df62 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3450,9 +3450,6 @@ msgstr ""
msgid "An error occurred when toggling the notification subscription"
msgstr ""
-msgid "An error occurred when updating the issue due date"
-msgstr ""
-
msgid "An error occurred when updating the issue weight"
msgstr ""
@@ -11294,7 +11291,7 @@ msgstr ""
msgid "DevopsAdoption|DevOps adoption tracks the use of key features across your favorite groups. Add a group to the table to begin."
msgstr ""
-msgid "DevopsAdoption|Feature adoption is based on usage in the current calendar month. Last updated: %{timestamp}."
+msgid "DevopsAdoption|Feature adoption is based on usage in the previous calendar month. Last updated: %{timestamp}."
msgstr ""
msgid "DevopsAdoption|Filter by name"
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
index 29c8e5dc2ba..bb4d5392b3b 100644
--- a/scripts/review_apps/base-config.yaml
+++ b/scripts/review_apps/base-config.yaml
@@ -5,9 +5,9 @@ global:
ingress:
annotations:
external-dns.alpha.kubernetes.io/ttl: 10
- cert-manager.io/cluster-issuer: review-apps-route53-dns01-wildcard-cluster-issuer
- kubernetes.io/tls-acme: true
configureCertmanager: false
+ tls:
+ secretName: review-apps-tls
initialRootPassword:
secret: shared-gitlab-initial-root-password
certmanager:
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 78a62cf0a29..6fb83e79f7f 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -161,6 +161,15 @@ function ensure_namespace() {
kubectl describe namespace "${namespace}" || kubectl create namespace "${namespace}"
}
+function label_namespace() {
+ local namespace="${1}"
+ local label="${2}"
+
+ echoinfo "Labeling the ${namespace} namespace with ${label}" true
+
+ kubectl label namespace "${namespace}" "${label}"
+}
+
function install_external_dns() {
local namespace="${KUBE_NAMESPACE}"
local release="dns-gitlab-review-app-helm3"
@@ -302,6 +311,7 @@ function deploy() {
gitlab_workhorse_image_repository="${IMAGE_REPOSITORY}/gitlab-workhorse-ee"
ensure_namespace "${namespace}"
+ label_namespace "${namespace}" "tls=review-apps-tls" # label namespace for kubed to sync tls
create_application_secret
@@ -319,9 +329,6 @@ HELM_CMD=$(cat << EOF
--set releaseOverride="${release}" \
--set global.hosts.hostSuffix="${HOST_SUFFIX}" \
--set global.hosts.domain="${REVIEW_APPS_DOMAIN}" \
- --set gitlab.webservice.ingress.tls.secretName="${release}-gitlab-tls" \
- --set registry.ingress.tls.secretName="${release}-registry-tls" \
- --set minio.ingress.tls.secretName="${release}-minio-tls" \
--set gitlab.migrations.image.repository="${gitlab_migrations_image_repository}" \
--set gitlab.migrations.image.tag="${CI_COMMIT_REF_SLUG}" \
--set gitlab.gitaly.image.repository="${gitlab_gitaly_image_repository}" \
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index e97bdba5fea..10d739c65f5 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -4,10 +4,10 @@ import Vuex from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import { stubComponent } from 'helpers/stub_component';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
-import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
+import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
@@ -109,8 +109,8 @@ describe('BoardContentSidebar', () => {
expect(wrapper.findComponent(BoardSidebarTitle).exists()).toBe(true);
});
- it('renders BoardSidebarDueDate', () => {
- expect(wrapper.findComponent(BoardSidebarDueDate).exists()).toBe(true);
+ it('renders SidebarDateWidget', () => {
+ expect(wrapper.findComponent(SidebarDateWidget).exists()).toBe(true);
});
it('renders BoardSidebarSubscription', () => {
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js
deleted file mode 100644
index 2f91beda275..00000000000
--- a/spec/frontend/boards/components/sidebar/board_sidebar_due_date_spec.js
+++ /dev/null
@@ -1,135 +0,0 @@
-import { GlDatepicker } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
-import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
-import { createStore } from '~/boards/stores';
-
-const TEST_DUE_DATE = '2020-02-20';
-const TEST_FORMATTED_DUE_DATE = 'Feb 20, 2020';
-const TEST_PARSED_DATE = new Date(2020, 1, 20);
-const TEST_ISSUE = { id: 'gid://gitlab/Issue/1', iid: 9, dueDate: null, referencePath: 'h/b#2' };
-
-describe('~/boards/components/sidebar/board_sidebar_due_date.vue', () => {
- let wrapper;
- let store;
-
- afterEach(() => {
- wrapper.destroy();
- store = null;
- wrapper = null;
- });
-
- const createWrapper = ({ dueDate = null } = {}) => {
- store = createStore();
- store.state.boardItems = { [TEST_ISSUE.id]: { ...TEST_ISSUE, dueDate } };
- store.state.activeId = TEST_ISSUE.id;
-
- wrapper = shallowMount(BoardSidebarDueDate, {
- store,
- provide: {
- canUpdate: true,
- },
- stubs: {
- 'board-editable-item': BoardEditableItem,
- },
- });
- };
-
- const findDatePicker = () => wrapper.find(GlDatepicker);
- const findResetButton = () => wrapper.find('[data-testid="reset-button"]');
- const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
-
- it('renders "None" when no due date is set', () => {
- createWrapper();
-
- expect(findCollapsed().text()).toBe('None');
- expect(findResetButton().exists()).toBe(false);
- });
-
- it('renders formatted due date with reset button when set', () => {
- createWrapper({ dueDate: TEST_DUE_DATE });
-
- expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
- expect(findResetButton().exists()).toBe(true);
- });
-
- describe('when due date is submitted', () => {
- beforeEach(async () => {
- createWrapper();
-
- jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
- store.state.boardItems[TEST_ISSUE.id].dueDate = TEST_DUE_DATE;
- });
- findDatePicker().vm.$emit('input', TEST_PARSED_DATE);
- await wrapper.vm.$nextTick();
- });
-
- it('collapses sidebar and renders formatted due date with reset button', () => {
- expect(findCollapsed().isVisible()).toBe(true);
- expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
- expect(findResetButton().exists()).toBe(true);
- });
-
- it('commits change to the server', () => {
- expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalledWith({
- dueDate: TEST_DUE_DATE,
- projectPath: 'h/b',
- });
- });
- });
-
- describe('when due date is cleared', () => {
- beforeEach(async () => {
- createWrapper();
-
- jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
- store.state.boardItems[TEST_ISSUE.id].dueDate = null;
- });
- findDatePicker().vm.$emit('clear');
- await wrapper.vm.$nextTick();
- });
-
- it('collapses sidebar and renders "None"', () => {
- expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled();
- expect(findCollapsed().isVisible()).toBe(true);
- expect(findCollapsed().text()).toBe('None');
- });
- });
-
- describe('when due date is resetted', () => {
- beforeEach(async () => {
- createWrapper({ dueDate: TEST_DUE_DATE });
-
- jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
- store.state.boardItems[TEST_ISSUE.id].dueDate = null;
- });
- findResetButton().vm.$emit('click');
- await wrapper.vm.$nextTick();
- });
-
- it('collapses sidebar and renders "None"', () => {
- expect(wrapper.vm.setActiveIssueDueDate).toHaveBeenCalled();
- expect(findCollapsed().isVisible()).toBe(true);
- expect(findCollapsed().text()).toBe('None');
- });
- });
-
- describe('when the mutation fails', () => {
- beforeEach(async () => {
- createWrapper({ dueDate: TEST_DUE_DATE });
-
- jest.spyOn(wrapper.vm, 'setActiveIssueDueDate').mockImplementation(() => {
- throw new Error(['failed mutation']);
- });
- jest.spyOn(wrapper.vm, 'setError').mockImplementation(() => {});
- findDatePicker().vm.$emit('input', 'Invalid date');
- await wrapper.vm.$nextTick();
- });
-
- it('collapses sidebar and renders former issue due date', () => {
- expect(findCollapsed().isVisible()).toBe(true);
- expect(findCollapsed().text()).toContain(TEST_FORMATTED_DUE_DATE);
- expect(wrapper.vm.setError).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 63569333408..b28412f2127 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1386,57 +1386,6 @@ describe('setActiveIssueLabels', () => {
});
});
-describe('setActiveIssueDueDate', () => {
- const state = { boardItems: { [mockIssue.id]: mockIssue } };
- const getters = { activeBoardItem: mockIssue };
- const testDueDate = '2020-02-20';
- const input = {
- dueDate: testDueDate,
- projectPath: 'h/b',
- };
-
- it('should commit due date after setting the issue', (done) => {
- jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
- data: {
- updateIssue: {
- issue: {
- dueDate: testDueDate,
- },
- errors: [],
- },
- },
- });
-
- const payload = {
- itemId: getters.activeBoardItem.id,
- prop: 'dueDate',
- value: testDueDate,
- };
-
- testAction(
- actions.setActiveIssueDueDate,
- input,
- { ...state, ...getters },
- [
- {
- type: types.UPDATE_BOARD_ITEM_BY_ID,
- payload,
- },
- ],
- [],
- done,
- );
- });
-
- it('throws error if fails', async () => {
- jest
- .spyOn(gqlClient, 'mutate')
- .mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
-
- await expect(actions.setActiveIssueDueDate({ getters }, input)).rejects.toThrow(Error);
- });
-});
-
describe('setActiveItemSubscribed', () => {
const state = {
boardItems: {
diff --git a/spec/frontend/nav/components/responsive_app_spec.js b/spec/frontend/nav/components/responsive_app_spec.js
index 4d7f053e43b..7221ea2c5cd 100644
--- a/spec/frontend/nav/components/responsive_app_spec.js
+++ b/spec/frontend/nav/components/responsive_app_spec.js
@@ -8,6 +8,11 @@ import { resetMenuItemsActive } from '~/nav/utils/reset_menu_items_active';
import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
import { TEST_NAV_DATA } from '../mock_data';
+const HTML_HEADER_CONTENT = '<div class="header-content"></div>';
+const HTML_MENU_EXPANDED = '<div class="menu-expanded"></div>';
+const HTML_HEADER_WITH_MENU_EXPANDED =
+ '<div></div><div class="header-content menu-expanded"></div>';
+
describe('~/nav/components/responsive_app.vue', () => {
let wrapper;
@@ -53,11 +58,11 @@ describe('~/nav/components/responsive_app.vue', () => {
});
it.each`
- bodyHtml | expectation
- ${''} | ${false}
- ${'<div class="header-content"></div>'} | ${false}
- ${'<div class="menu-expanded"></div>'} | ${false}
- ${'<div></div><div class="header-content menu-expanded"></div>}'} | ${true}
+ bodyHtml | expectation
+ ${''} | ${false}
+ ${HTML_HEADER_CONTENT} | ${false}
+ ${HTML_MENU_EXPANDED} | ${false}
+ ${HTML_HEADER_WITH_MENU_EXPANDED} | ${true}
`(
'with responsive toggle event and html set to $bodyHtml, responsive open = $expectation',
({ bodyHtml, expectation }) => {
@@ -93,7 +98,7 @@ describe('~/nav/components/responsive_app.vue', () => {
describe('with menu expanded in body', () => {
beforeEach(() => {
- document.body.innerHTML = '<div></div><div class="header-content menu-expanded"></div>';
+ document.body.innerHTML = HTML_HEADER_WITH_MENU_EXPANDED;
createComponent();
});
diff --git a/spec/frontend/repository/components/table/row_spec.js b/spec/frontend/repository/components/table/row_spec.js
index 254e2fc07b4..da28c9873d9 100644
--- a/spec/frontend/repository/components/table/row_spec.js
+++ b/spec/frontend/repository/components/table/row_spec.js
@@ -19,6 +19,7 @@ function factory(propsData = {}) {
name: propsData.path,
projectPath: 'gitlab-org/gitlab-ce',
url: `https://test.com`,
+ totalEntries: 10,
},
directives: {
GlHoverLoad: createMockDirective(),
diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js
index a842053caad..d338af88ce3 100644
--- a/spec/frontend/repository/log_tree_spec.js
+++ b/spec/frontend/repository/log_tree_spec.js
@@ -69,6 +69,11 @@ describe('fetchLogsTree', () => {
mock.restore();
});
+ it('does not call axios get if offset is larger than the maximum offset', () =>
+ fetchLogsTree(client, '', '1000', resolver, 900).then(() => {
+ expect(axios.get).not.toHaveBeenCalled();
+ }));
+
it('calls axios get', () =>
fetchLogsTree(client, '', '0', resolver).then(() => {
expect(axios.get).toHaveBeenCalledWith('/gitlab-org/gitlab-foss/-/refs/main/logs_tree/', {
diff --git a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
index 91cbcc6cc27..619e89beb23 100644
--- a/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
+++ b/spec/frontend/sidebar/components/date/sidebar_date_widget_spec.js
@@ -22,6 +22,10 @@ describe('Sidebar date Widget', () => {
let fakeApollo;
const date = '2021-04-15';
+ window.gon = {
+ first_day_of_week: 1,
+ };
+
const findEditableItem = () => wrapper.findComponent(SidebarEditableItem);
const findPopoverIcon = () => wrapper.find('[data-testid="inherit-date-popover"]');
const findDatePicker = () => wrapper.find(GlDatepicker);
@@ -119,11 +123,12 @@ describe('Sidebar date Widget', () => {
expect(wrapper.emitted('dueDateUpdated')).toEqual([[date]]);
});
- it('uses a correct prop to set the initial date for GlDatePicker', () => {
+ it('uses a correct prop to set the initial date and first day of the week for GlDatePicker', () => {
expect(findDatePicker().props()).toMatchObject({
value: null,
autocomplete: 'off',
defaultDate: expect.any(Object),
+ firstDay: window.gon.first_day_of_week,
});
});
diff --git a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
index 731dd5eca23..cc4760e69e5 100644
--- a/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/packages_registries_menu_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::PackagesRegistriesMenu do
- let(:project) { build(:project) }
+ let_it_be(:project) { create(:project) }
+
let(:user) { project.owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
diff --git a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
index 88f2df6cd84..6817f0e6ed6 100644
--- a/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
+++ b/spec/lib/sidebars/projects/menus/settings_menu_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
- let(:project) { build(:project) }
+ let_it_be(:project) { create(:project) }
+
let(:user) { project.owner }
let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) }
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index fa19d512b8d..3232a559d0b 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -331,6 +331,40 @@ RSpec.describe ContainerRepository do
it { is_expected.to eq([]) }
end
+
+ context 'with read_container_registry_access_level disabled' do
+ before do
+ stub_feature_flags(read_container_registry_access_level: false)
+ end
+
+ context 'in a group' do
+ let(:test_group) { group }
+
+ it { is_expected.to contain_exactly(repository) }
+ end
+
+ context 'with a subgroup' do
+ let(:test_group) { create(:group) }
+ let(:another_project) { create(:project, path: 'test', group: test_group) }
+
+ let(:another_repository) do
+ create(:container_repository, name: 'my_image', project: another_project)
+ end
+
+ before do
+ group.parent = test_group
+ group.save!
+ end
+
+ it { is_expected.to contain_exactly(repository, another_repository) }
+ end
+
+ context 'group without container_repositories' do
+ let(:test_group) { create(:group) }
+
+ it { is_expected.to eq([]) }
+ end
+ end
end
describe '.search_by_name' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 48353323fb2..0e6f662da50 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -653,6 +653,16 @@ RSpec.describe Project, factory_default: :keep do
it { is_expected.to delegate_method(:root_ancestor).to(:namespace).with_arguments(allow_nil: true) }
it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) }
it { is_expected.to delegate_method(:allow_editing_commit_messages?).to(:project_setting) }
+ it { is_expected.to delegate_method(:container_registry_enabled?).to(:project_feature) }
+ it { is_expected.to delegate_method(:container_registry_access_level).to(:project_feature) }
+
+ context 'when read_container_registry_access_level is disabled' do
+ before do
+ stub_feature_flags(read_container_registry_access_level: false)
+ end
+
+ it { is_expected.not_to delegate_method(:container_registry_enabled?).to(:project_feature) }
+ end
end
describe 'reference methods' do
@@ -2285,35 +2295,55 @@ RSpec.describe Project, factory_default: :keep do
it 'updates project_feature', :aggregate_failures do
# Simulate an existing project that has container_registry enabled
project.update_column(:container_registry_enabled, true)
- project.project_feature.update_column(:container_registry_access_level, ProjectFeature::DISABLED)
-
- expect(project.container_registry_enabled).to eq(true)
- expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::DISABLED)
+ project.project_feature.update_column(:container_registry_access_level, ProjectFeature::ENABLED)
project.update!(container_registry_enabled: false)
- expect(project.container_registry_enabled).to eq(false)
+ expect(project.read_attribute(:container_registry_enabled)).to eq(false)
expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::DISABLED)
project.update!(container_registry_enabled: true)
- expect(project.container_registry_enabled).to eq(true)
+ expect(project.read_attribute(:container_registry_enabled)).to eq(true)
expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::ENABLED)
end
it 'rollsback both projects and project_features row in case of error', :aggregate_failures do
project.update_column(:container_registry_enabled, true)
- project.project_feature.update_column(:container_registry_access_level, ProjectFeature::DISABLED)
-
- expect(project.container_registry_enabled).to eq(true)
- expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::DISABLED)
+ project.project_feature.update_column(:container_registry_access_level, ProjectFeature::ENABLED)
allow(project).to receive(:valid?).and_return(false)
expect { project.update!(container_registry_enabled: false) }.to raise_error(ActiveRecord::RecordInvalid)
- expect(project.reload.container_registry_enabled).to eq(true)
- expect(project.project_feature.reload.container_registry_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.reload.read_attribute(:container_registry_enabled)).to eq(true)
+ expect(project.project_feature.reload.container_registry_access_level).to eq(ProjectFeature::ENABLED)
+ end
+ end
+
+ describe '#container_registry_enabled' do
+ let_it_be_with_reload(:project) { create(:project) }
+
+ it 'delegates to project_feature', :aggregate_failures do
+ project.update_column(:container_registry_enabled, true)
+ project.project_feature.update_column(:container_registry_access_level, ProjectFeature::DISABLED)
+
+ expect(project.container_registry_enabled).to eq(false)
+ expect(project.container_registry_enabled?).to eq(false)
+ end
+
+ context 'with read_container_registry_access_level disabled' do
+ before do
+ stub_feature_flags(read_container_registry_access_level: false)
+ end
+
+ it 'reads project.container_registry_enabled' do
+ project.update_column(:container_registry_enabled, true)
+ project.project_feature.update_column(:container_registry_access_level, ProjectFeature::DISABLED)
+
+ expect(project.container_registry_enabled).to eq(true)
+ expect(project.container_registry_enabled?).to eq(true)
+ end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index 53d9bdb01a3..2b4501a71a5 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -1434,4 +1434,165 @@ RSpec.describe ProjectPolicy do
end
end
end
+
+ describe 'container_image policies' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:guest_operations_permissions) { [:read_container_image] }
+
+ let(:developer_operations_permissions) do
+ guest_operations_permissions + [
+ :create_container_image, :update_container_image, :destroy_container_image
+ ]
+ end
+
+ let(:maintainer_operations_permissions) do
+ developer_operations_permissions + [
+ :admin_container_image
+ ]
+ end
+
+ where(:project_visibility, :access_level, :role, :allowed) do
+ :public | ProjectFeature::ENABLED | :maintainer | true
+ :public | ProjectFeature::ENABLED | :developer | true
+ :public | ProjectFeature::ENABLED | :reporter | true
+ :public | ProjectFeature::ENABLED | :guest | true
+ :public | ProjectFeature::ENABLED | :anonymous | true
+ :public | ProjectFeature::PRIVATE | :maintainer | true
+ :public | ProjectFeature::PRIVATE | :developer | true
+ :public | ProjectFeature::PRIVATE | :reporter | true
+ :public | ProjectFeature::PRIVATE | :guest | false
+ :public | ProjectFeature::PRIVATE | :anonymous | false
+ :public | ProjectFeature::DISABLED | :maintainer | false
+ :public | ProjectFeature::DISABLED | :developer | false
+ :public | ProjectFeature::DISABLED | :reporter | false
+ :public | ProjectFeature::DISABLED | :guest | false
+ :public | ProjectFeature::DISABLED | :anonymous | false
+ :internal | ProjectFeature::ENABLED | :maintainer | true
+ :internal | ProjectFeature::ENABLED | :developer | true
+ :internal | ProjectFeature::ENABLED | :reporter | true
+ :internal | ProjectFeature::ENABLED | :guest | true
+ :internal | ProjectFeature::ENABLED | :anonymous | false
+ :internal | ProjectFeature::PRIVATE | :maintainer | true
+ :internal | ProjectFeature::PRIVATE | :developer | true
+ :internal | ProjectFeature::PRIVATE | :reporter | true
+ :internal | ProjectFeature::PRIVATE | :guest | false
+ :internal | ProjectFeature::PRIVATE | :anonymous | false
+ :internal | ProjectFeature::DISABLED | :maintainer | false
+ :internal | ProjectFeature::DISABLED | :developer | false
+ :internal | ProjectFeature::DISABLED | :reporter | false
+ :internal | ProjectFeature::DISABLED | :guest | false
+ :internal | ProjectFeature::DISABLED | :anonymous | false
+ :private | ProjectFeature::ENABLED | :maintainer | true
+ :private | ProjectFeature::ENABLED | :developer | true
+ :private | ProjectFeature::ENABLED | :reporter | true
+ :private | ProjectFeature::ENABLED | :guest | false
+ :private | ProjectFeature::ENABLED | :anonymous | false
+ :private | ProjectFeature::PRIVATE | :maintainer | true
+ :private | ProjectFeature::PRIVATE | :developer | true
+ :private | ProjectFeature::PRIVATE | :reporter | true
+ :private | ProjectFeature::PRIVATE | :guest | false
+ :private | ProjectFeature::PRIVATE | :anonymous | false
+ :private | ProjectFeature::DISABLED | :maintainer | false
+ :private | ProjectFeature::DISABLED | :developer | false
+ :private | ProjectFeature::DISABLED | :reporter | false
+ :private | ProjectFeature::DISABLED | :guest | false
+ :private | ProjectFeature::DISABLED | :anonymous | false
+ end
+
+ with_them do
+ let(:current_user) { send(role) }
+ let(:project) { send("#{project_visibility}_project") }
+
+ it 'allows/disallows the abilities based on the container_registry feature access level' do
+ project.project_feature.update!(container_registry_access_level: access_level)
+
+ if allowed
+ expect_allowed(*permissions_abilities(role))
+ else
+ expect_disallowed(*permissions_abilities(role))
+ end
+ end
+
+ def permissions_abilities(role)
+ case role
+ when :maintainer
+ maintainer_operations_permissions
+ when :developer
+ developer_operations_permissions
+ when :reporter, :guest, :anonymous
+ guest_operations_permissions
+ else
+ raise "Unknown role #{role}"
+ end
+ end
+ end
+
+ context 'with read_container_registry_access_level disabled' do
+ before do
+ stub_feature_flags(read_container_registry_access_level: false)
+ end
+
+ where(:project_visibility, :container_registry_enabled, :role, :allowed) do
+ :public | true | :maintainer | true
+ :public | true | :developer | true
+ :public | true | :reporter | true
+ :public | true | :guest | true
+ :public | true | :anonymous | true
+ :public | false | :maintainer | false
+ :public | false | :developer | false
+ :public | false | :reporter | false
+ :public | false | :guest | false
+ :public | false | :anonymous | false
+ :internal | true | :maintainer | true
+ :internal | true | :developer | true
+ :internal | true | :reporter | true
+ :internal | true | :guest | true
+ :internal | true | :anonymous | false
+ :internal | false | :maintainer | false
+ :internal | false | :developer | false
+ :internal | false | :reporter | false
+ :internal | false | :guest | false
+ :internal | false | :anonymous | false
+ :private | true | :maintainer | true
+ :private | true | :developer | true
+ :private | true | :reporter | true
+ :private | true | :guest | false
+ :private | true | :anonymous | false
+ :private | false | :maintainer | false
+ :private | false | :developer | false
+ :private | false | :reporter | false
+ :private | false | :guest | false
+ :private | false | :anonymous | false
+ end
+
+ with_them do
+ let(:current_user) { send(role) }
+ let(:project) { send("#{project_visibility}_project") }
+
+ it 'allows/disallows the abilities based on container_registry_enabled' do
+ project.update_column(:container_registry_enabled, container_registry_enabled)
+
+ if allowed
+ expect_allowed(*permissions_abilities(role))
+ else
+ expect_disallowed(*permissions_abilities(role))
+ end
+ end
+
+ def permissions_abilities(role)
+ case role
+ when :maintainer
+ maintainer_operations_permissions
+ when :developer
+ developer_operations_permissions
+ when :reporter, :guest, :anonymous
+ guest_operations_permissions
+ else
+ raise "Unknown role #{role}"
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index a1532ea464e..6ec48f88e93 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -184,6 +184,32 @@ RSpec.describe API::Projects do
end
end
+ it 'includes correct value of container_registry_enabled', :aggregate_failures do
+ project.update_column(:container_registry_enabled, true)
+ project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
+
+ get api('/projects', user)
+ project_response = json_response.find { |p| p['id'] == project.id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(project_response['container_registry_enabled']).to eq(false)
+ end
+
+ it 'reads projects.container_registry_enabled when read_container_registry_access_level is disabled' do
+ stub_feature_flags(read_container_registry_access_level: false)
+
+ project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
+ project.update_column(:container_registry_enabled, true)
+
+ get api('/projects', user)
+ project_response = json_response.find { |p| p['id'] == project.id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(project_response['container_registry_enabled']).to eq(true)
+ end
+
it 'includes project topics' do
get api('/projects', user)
diff --git a/workhorse/internal/httprs/httprs_test.go b/workhorse/internal/httprs/httprs_test.go
index 62279d895c9..e26d2d21215 100644
--- a/workhorse/internal/httprs/httprs_test.go
+++ b/workhorse/internal/httprs/httprs_test.go
@@ -53,6 +53,10 @@ func (f *fakeRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) {
if err != nil {
return nil, err
}
+ if err := os.Remove(fw.tmp.Name()); err != nil {
+ return nil, err
+ }
+
if f.downgradeZeroToNoRange {
// There are implementations that downgrades bytes=0- to a normal un-ranged GET
if r.Header.Get("Range") == "bytes=0-" {
@@ -79,6 +83,10 @@ func newRSFactory(flags int) RSFactory {
if err != nil {
return nil
}
+ if err := os.Remove(tmp.Name()); err != nil {
+ return nil
+ }
+
for i := 0; i < SZ; i++ {
tmp.WriteString(fmt.Sprintf("%04d", i))
}