Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-06-11 12:09:49 +00:00
parent a350f877c4
commit dcf94a7641
53 changed files with 544 additions and 417 deletions

View file

@ -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:

View file

@ -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>

View file

@ -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' });
}
},
},

View file

@ -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

View file

@ -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();

View file

@ -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()) {

View file

@ -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();

View file

@ -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);

View file

@ -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>

View file

@ -1,8 +0,0 @@
mutation issueSetDueDate($input: UpdateIssueInput!) {
updateIssue(input: $input) {
issue {
dueDate
}
errors
}
}

View file

@ -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;

View file

@ -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'));

View file

@ -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">

View file

@ -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,

View file

@ -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 }) {

View file

@ -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;
}
});

View file

@ -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
}
}

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -1,4 +0,0 @@
# frozen_string_literal: true
module Github
end

View file

@ -8,6 +8,6 @@ module SecurityScansQueue
included do
queue_namespace :security_scans
feature_category :static_application_security_testing
feature_category :vulnerability_management
end
end

View file

@ -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

View file

@ -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:

View file

@ -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 * * *'

View file

@ -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

View file

@ -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

View file

@ -0,0 +1 @@
1f99d446428ddac2a0fa7d64bdce9fc300bf02e88c35cdb3d726c501641e721d

View file

@ -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);

View file

@ -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.

View file

@ -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`

View file

@ -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:

View file

@ -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`

View file

@ -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

View file

@ -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"

View file

@ -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:

View file

@ -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}" \

View file

@ -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', () => {

View file

@ -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();
});
});
});

View file

@ -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: {

View file

@ -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();
});

View file

@ -19,6 +19,7 @@ function factory(propsData = {}) {
name: propsData.path,
projectPath: 'gitlab-org/gitlab-ce',
url: `https://test.com`,
totalEntries: 10,
},
directives: {
GlHoverLoad: createMockDirective(),

View file

@ -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/', {

View file

@ -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,
});
});

View file

@ -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) }

View file

@ -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) }

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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))
}