Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
a350f877c4
commit
dcf94a7641
53 changed files with 544 additions and 417 deletions
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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' });
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
|
@ -1,8 +0,0 @@
|
|||
mutation issueSetDueDate($input: UpdateIssueInput!) {
|
||||
updateIssue(input: $input) {
|
||||
issue {
|
||||
dueDate
|
||||
}
|
||||
errors
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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'));
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', {
|
||||
fetchLogsTree(
|
||||
defaultClient,
|
||||
path,
|
||||
'0',
|
||||
{
|
||||
resolve,
|
||||
entry: {
|
||||
name: fileName,
|
||||
type,
|
||||
},
|
||||
});
|
||||
},
|
||||
maxOffset,
|
||||
);
|
||||
});
|
||||
},
|
||||
readme(_, { url }) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -51,8 +51,12 @@ class ProjectPolicy < BasePolicy
|
|||
|
||||
desc "Container registry is disabled"
|
||||
condition(:container_registry_disabled, scope: :subject) do
|
||||
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"
|
||||
condition(:has_external_wiki, scope: :subject, score: 0) { project.has_external_wiki? }
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Github
|
||||
end
|
|
@ -8,6 +8,6 @@ module SecurityScansQueue
|
|||
|
||||
included do
|
||||
queue_namespace :security_scans
|
||||
feature_category :static_application_security_testing
|
||||
feature_category :vulnerability_management
|
||||
end
|
||||
end
|
||||
|
|
|
@ -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
|
|
@ -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:
|
||||
|
|
|
@ -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 * * *'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
1
db/schema_migrations/20210606143426
Normal file
1
db/schema_migrations/20210606143426
Normal file
|
@ -0,0 +1 @@
|
|||
1f99d446428ddac2a0fa7d64bdce9fc300bf02e88c35cdb3d726c501641e721d
|
|
@ -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);
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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`
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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}" \
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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: {
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
@ -55,9 +60,9 @@ 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}
|
||||
${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();
|
||||
});
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ function factory(propsData = {}) {
|
|||
name: propsData.path,
|
||||
projectPath: 'gitlab-org/gitlab-ce',
|
||||
url: `https://test.com`,
|
||||
totalEntries: 10,
|
||||
},
|
||||
directives: {
|
||||
GlHoverLoad: createMockDirective(),
|
||||
|
|
|
@ -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/', {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue