Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
3cda3d43ae
commit
8f9307985e
|
|
@ -1 +1 @@
|
|||
ac201ee33b2c7b2974945fb15ba9b1aec4794017
|
||||
57e11eb9431c93a6349ffc0222cf447705c0fada
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -512,7 +512,7 @@ gem 'ssh_data', '~> 1.3'
|
|||
gem 'spamcheck', '~> 1.0.0'
|
||||
|
||||
# Gitaly GRPC protocol definitions
|
||||
gem 'gitaly', '~> 15.8.0-rc1'
|
||||
gem 'gitaly', '~> 15.9.0-rc1'
|
||||
|
||||
# KAS GRPC protocol definitions
|
||||
gem 'kas-grpc', '~> 0.0.2'
|
||||
|
|
|
|||
|
|
@ -197,7 +197,7 @@
|
|||
{"name":"gettext_i18n_rails","version":"1.8.0","platform":"ruby","checksum":"95e5cf8440b1e08705b27f2bccb56143272c5a7a0dabcf54ea1bd701140a496f"},
|
||||
{"name":"gettext_i18n_rails_js","version":"1.3.0","platform":"ruby","checksum":"5d10afe4be3639bff78c50a56768c20f39aecdabc580c08aa45573911c2bd687"},
|
||||
{"name":"git","version":"1.11.0","platform":"ruby","checksum":"7e95ba4da8298a0373ef1a6862aa22007d761f3c8274b675aa787966fecea0f1"},
|
||||
{"name":"gitaly","version":"15.8.0.pre.rc1","platform":"ruby","checksum":"9244245b602c6c903eb0e3b3629b51e888af179cbbe339269095a1ab9113dbb5"},
|
||||
{"name":"gitaly","version":"15.9.0.pre.rc1","platform":"ruby","checksum":"c5ebbe6b1f2770020b0857a6a03bf1f52cd0be9ae05dbbb296316b3e7d75b42b"},
|
||||
{"name":"gitlab","version":"4.19.0","platform":"ruby","checksum":"3f645e3e195dbc24f0834fbf83e8ccfb2056d8e9712b01a640aad418a6949679"},
|
||||
{"name":"gitlab-chronic","version":"0.10.5","platform":"ruby","checksum":"f80f18dc699b708870a80685243331290bc10cfeedb6b99c92219722f729c875"},
|
||||
{"name":"gitlab-dangerfiles","version":"3.6.6","platform":"ruby","checksum":"cabfe23490120188a653c827a32121bdd4abf4e9e91d1754bf170dd7e93781f1"},
|
||||
|
|
|
|||
|
|
@ -563,7 +563,7 @@ GEM
|
|||
rails (>= 3.2.0)
|
||||
git (1.11.0)
|
||||
rchardet (~> 1.8)
|
||||
gitaly (15.8.0.pre.rc1)
|
||||
gitaly (15.9.0.pre.rc1)
|
||||
grpc (~> 1.0)
|
||||
gitlab (4.19.0)
|
||||
httparty (~> 0.20)
|
||||
|
|
@ -1677,7 +1677,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.3)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.3)
|
||||
gitaly (~> 15.8.0.pre.rc1)
|
||||
gitaly (~> 15.9.0.pre.rc1)
|
||||
gitlab-chronic (~> 0.10.5)
|
||||
gitlab-dangerfiles (~> 3.6.6)
|
||||
gitlab-experiment (~> 0.7.1)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import { buildApiUrl } from './api_utils';
|
|||
|
||||
const GROUP_PATH = '/api/:version/groups/:id';
|
||||
const GROUPS_PATH = '/api/:version/groups.json';
|
||||
const GROUP_MEMBERS_PATH = '/api/:version/groups/:id/members';
|
||||
const GROUP_ALL_MEMBERS_PATH = '/api/:version/groups/:id/members/all';
|
||||
const DESCENDANT_GROUPS_PATH = '/api/:version/groups/:id/descendant_groups';
|
||||
const GROUP_TRANSFER_LOCATIONS_PATH = 'api/:version/groups/:id/transfer_locations';
|
||||
|
||||
|
|
@ -45,3 +47,10 @@ export const getGroupTransferLocations = (groupId, params = {}) => {
|
|||
|
||||
return axios.get(url, { params: { ...defaultParams, ...params } });
|
||||
};
|
||||
|
||||
export const getGroupMembers = (groupId, inherited = false) => {
|
||||
const path = inherited ? GROUP_ALL_MEMBERS_PATH : GROUP_MEMBERS_PATH;
|
||||
const url = buildApiUrl(path).replace(':id', groupId);
|
||||
|
||||
return axios.get(url);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import axios from '../lib/utils/axios_utils';
|
|||
import { buildApiUrl } from './api_utils';
|
||||
|
||||
const PROJECTS_PATH = '/api/:version/projects.json';
|
||||
const PROJECT_MEMBERS_PATH = '/api/:version/projects/:id/members';
|
||||
const PROJECT_ALL_MEMBERS_PATH = '/api/:version/projects/:id/members/all';
|
||||
const PROJECT_IMPORT_MEMBERS_PATH = '/api/:version/projects/:id/import_project_members/:project_id';
|
||||
const PROJECT_REPOSITORY_SIZE_PATH = '/api/:version/projects/:id/repository_size';
|
||||
const PROJECT_TRANSFER_LOCATIONS_PATH = 'api/:version/projects/:id/transfer_locations';
|
||||
|
|
@ -54,3 +56,10 @@ export const getTransferLocations = (projectId, params = {}) => {
|
|||
|
||||
return axios.get(url, { params: { ...defaultParams, ...params } });
|
||||
};
|
||||
|
||||
export const getProjectMembers = (projectId, inherited = false) => {
|
||||
const path = inherited ? PROJECT_ALL_MEMBERS_PATH : PROJECT_MEMBERS_PATH;
|
||||
const url = buildApiUrl(path).replace(':id', projectId);
|
||||
|
||||
return axios.get(url);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import query from 'ee_else_ce/issuable/popover/queries/issue.query.graphql';
|
|||
import IssueDueDate from '~/boards/components/issue_due_date.vue';
|
||||
import IssueMilestone from '~/issuable/components/issue_milestone.vue';
|
||||
import StatusBox from '~/issuable/components/status_box.vue';
|
||||
import { IssuableStatus } from '~/issues/constants';
|
||||
import { STATUS_CLOSED } from '~/issues/constants';
|
||||
import timeagoMixin from '~/vue_shared/mixins/timeago';
|
||||
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ export default {
|
|||
return Object.keys(this.issue).length > 0;
|
||||
},
|
||||
isIssueClosed() {
|
||||
return this.issue?.state === IssuableStatus.Closed;
|
||||
return this.issue?.state === STATUS_CLOSED;
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
export const IssuableStatus = {
|
||||
Closed: 'closed',
|
||||
Open: 'opened',
|
||||
Reopened: 'reopened',
|
||||
};
|
||||
export const STATUS_CLOSED = 'closed';
|
||||
export const STATUS_OPEN = 'opened';
|
||||
export const STATUS_REOPENED = 'reopened';
|
||||
|
||||
export const IssuableStatusText = {
|
||||
[IssuableStatus.Closed]: __('Closed'),
|
||||
[IssuableStatus.Open]: __('Open'),
|
||||
[IssuableStatus.Reopened]: __('Open'),
|
||||
[STATUS_CLOSED]: __('Closed'),
|
||||
[STATUS_OPEN]: __('Open'),
|
||||
[STATUS_REOPENED]: __('Open'),
|
||||
};
|
||||
|
||||
export const IssuableType = {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import * as Sentry from '@sentry/browser';
|
|||
import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query.graphql';
|
||||
import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_statistics.vue';
|
||||
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
|
||||
import { IssuableStatus } from '~/issues/constants';
|
||||
import { STATUS_CLOSED } from '~/issues/constants';
|
||||
import {
|
||||
CREATED_DESC,
|
||||
defaultTypeTokenOptions,
|
||||
|
|
@ -363,10 +363,10 @@ export default {
|
|||
return axios.get('/-/autocomplete/users.json', { params: { active: true, search } });
|
||||
},
|
||||
getStatus(issue) {
|
||||
if (issue.state === IssuableStatus.Closed && issue.moved) {
|
||||
if (issue.state === STATUS_CLOSED && issue.moved) {
|
||||
return this.$options.i18n.closedMoved;
|
||||
}
|
||||
if (issue.state === IssuableStatus.Closed) {
|
||||
if (issue.state === STATUS_CLOSED) {
|
||||
return this.$options.i18n.closed;
|
||||
}
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<script>
|
||||
import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { IssuableStatus } from '~/issues/constants';
|
||||
import { STATUS_CLOSED } from '~/issues/constants';
|
||||
import {
|
||||
dateInWords,
|
||||
getTimeRemainingInWords,
|
||||
|
|
@ -43,8 +43,7 @@ export default {
|
|||
},
|
||||
showDueDateInRed() {
|
||||
return (
|
||||
isInPast(newDateAsLocaleTime(this.issue.dueDate)) &&
|
||||
this.issue.state !== IssuableStatus.Closed
|
||||
isInPast(newDateAsLocaleTime(this.issue.dueDate)) && this.issue.state !== STATUS_CLOSED
|
||||
);
|
||||
},
|
||||
timeEstimate() {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
|
|||
import { ITEM_TYPE } from '~/groups/constants';
|
||||
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
|
||||
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
|
||||
import { IssuableStatus } from '~/issues/constants';
|
||||
import { STATUS_CLOSED } from '~/issues/constants';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { fetchPolicies } from '~/lib/graphql';
|
||||
import { isPositiveInteger } from '~/lib/utils/number_utils';
|
||||
|
|
@ -572,10 +572,10 @@ export default {
|
|||
return `${this.exportCsvPath}${window.location.search}`;
|
||||
},
|
||||
getStatus(issue) {
|
||||
if (issue.state === IssuableStatus.Closed && issue.moved) {
|
||||
if (issue.state === STATUS_CLOSED && issue.moved) {
|
||||
return this.$options.i18n.closedMoved;
|
||||
}
|
||||
if (issue.state === IssuableStatus.Closed) {
|
||||
if (issue.state === STATUS_CLOSED) {
|
||||
return this.$options.i18n.closed;
|
||||
}
|
||||
return undefined;
|
||||
|
|
|
|||
|
|
@ -2,12 +2,7 @@
|
|||
import { GlIcon, GlBadge, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
|
||||
import Visibility from 'visibilityjs';
|
||||
import { createAlert } from '~/flash';
|
||||
import {
|
||||
IssuableStatus,
|
||||
IssuableStatusText,
|
||||
WorkspaceType,
|
||||
IssuableType,
|
||||
} from '~/issues/constants';
|
||||
import { IssuableStatusText, WorkspaceType, IssuableType, STATUS_CLOSED } from '~/issues/constants';
|
||||
import Poll from '~/lib/utils/poll';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
import { __, sprintf } from '~/locale';
|
||||
|
|
@ -251,7 +246,7 @@ export default {
|
|||
return sprintf(__('Error updating %{issuableType}'), { issuableType: this.issuableType });
|
||||
},
|
||||
isClosed() {
|
||||
return this.issuableStatus === IssuableStatus.Closed;
|
||||
return this.issuableStatus === STATUS_CLOSED;
|
||||
},
|
||||
pinnedLinkClasses() {
|
||||
return this.showTitleBorder
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import {
|
|||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import { createAlert, VARIANT_SUCCESS } from '~/flash';
|
||||
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
|
||||
import { IssuableStatus, IssueType } from '~/issues/constants';
|
||||
import { IssueType, STATUS_CLOSED } from '~/issues/constants';
|
||||
import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
|
||||
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
|
||||
import { visitUrl } from '~/lib/utils/url_utility';
|
||||
|
|
@ -114,7 +114,7 @@ export default {
|
|||
...mapState(['isToggleStateButtonLoading']),
|
||||
...mapGetters(['openState', 'getBlockedByIssues']),
|
||||
isClosed() {
|
||||
return this.openState === IssuableStatus.Closed;
|
||||
return this.openState === STATUS_CLOSED;
|
||||
},
|
||||
issueTypeText() {
|
||||
const issueTypeTexts = {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ export const timelineItemI18n = Object.freeze({
|
|||
|
||||
export const timelineEventTagsI18n = Object.freeze({
|
||||
startTime: __('Start time'),
|
||||
impactDetected: __('Impact detected'),
|
||||
responseInitiated: __('Response initiated'),
|
||||
impactMitigated: __('Impact mitigated'),
|
||||
causeIdentified: __('Cause identified'),
|
||||
endTime: __('End time'),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ export const generateColumnsFromLayersListMemoized = memoize(generateColumnsFrom
|
|||
// See https://gitlab.com/gitlab-org/gitlab/-/issues/367547
|
||||
export const keepLatestDownstreamPipelines = (downstreamPipelines = []) => {
|
||||
// handles GraphQL
|
||||
return downstreamPipelines.filter((job) => {
|
||||
return !job.sourceJob.retried;
|
||||
return downstreamPipelines.filter((pipeline) => {
|
||||
return !pipeline?.sourceJob?.retried || false;
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
getQueryHeaders,
|
||||
toggleQueryPollingByVisibility,
|
||||
} from '~/pipelines/components/graph/utils';
|
||||
import { keepLatestDownstreamPipelines } from '~/pipelines/components/parsing_utils';
|
||||
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
|
||||
import { formatStages } from '../utils';
|
||||
import getLinkedPipelinesQuery from '../graphql/queries/get_linked_pipelines.query.graphql';
|
||||
|
|
@ -91,7 +92,8 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
downstreamPipelines() {
|
||||
return this.pipeline?.downstream?.nodes;
|
||||
const downstream = this.pipeline?.downstream?.nodes;
|
||||
return keepLatestDownstreamPipelines(downstream);
|
||||
},
|
||||
pipelinePath() {
|
||||
return this.pipeline?.path ?? '';
|
||||
|
|
|
|||
|
|
@ -1,20 +1,27 @@
|
|||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import ScopeNavigation from '~/search/sidebar/components/scope_navigation.vue';
|
||||
import { SCOPE_ISSUES, SCOPE_MERGE_REQUESTS } from '../constants';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { SCOPE_ISSUES, SCOPE_MERGE_REQUESTS, SCOPE_BLOB } from '../constants';
|
||||
import ResultsFilters from './results_filters.vue';
|
||||
import LanguageFilter from './language_filter.vue';
|
||||
|
||||
export default {
|
||||
name: 'GlobalSearchSidebar',
|
||||
components: {
|
||||
ResultsFilters,
|
||||
ScopeNavigation,
|
||||
LanguageFilter,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
computed: {
|
||||
...mapState(['urlQuery']),
|
||||
showFilters() {
|
||||
showIssueAndMergeFilters() {
|
||||
return this.urlQuery.scope === SCOPE_ISSUES || this.urlQuery.scope === SCOPE_MERGE_REQUESTS;
|
||||
},
|
||||
showBlobFilter() {
|
||||
return this.urlQuery.scope === SCOPE_BLOB && this.glFeatures.searchBlobsLanguageAggregation;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -22,6 +29,7 @@ export default {
|
|||
<template>
|
||||
<section class="search-sidebar gl-display-flex gl-flex-direction-column gl-mr-4 gl-mb-6 gl-mt-5">
|
||||
<scope-navigation />
|
||||
<results-filters v-if="showFilters" />
|
||||
<results-filters v-if="showIssueAndMergeFilters" />
|
||||
<language-filter v-if="showBlobFilter" />
|
||||
</section>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
<script>
|
||||
import { GlFormCheckboxGroup, GlFormCheckbox } from '@gitlab/ui';
|
||||
import { mapState, mapActions } from 'vuex';
|
||||
import { intersection } from 'lodash';
|
||||
import { NAV_LINK_COUNT_DEFAULT_CLASSES, LABEL_DEFAULT_CLASSES } from '../constants';
|
||||
import { formatSearchResultCount } from '../../store/utils';
|
||||
|
||||
export default {
|
||||
name: 'CheckboxFilter',
|
||||
components: {
|
||||
GlFormCheckboxGroup,
|
||||
GlFormCheckbox,
|
||||
},
|
||||
props: {
|
||||
filterData: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapState(['query']),
|
||||
scope() {
|
||||
return this.query.scope;
|
||||
},
|
||||
queryFilters() {
|
||||
return this.query[this.filterData?.filterParam] || [];
|
||||
},
|
||||
dataFilters() {
|
||||
return Object.values(this.filterData?.filters || []);
|
||||
},
|
||||
flatDataFilterValues() {
|
||||
return this.dataFilters.map(({ value }) => value);
|
||||
},
|
||||
selectedFilter: {
|
||||
get() {
|
||||
return intersection(this.flatDataFilterValues, this.queryFilters);
|
||||
},
|
||||
set(value) {
|
||||
this.setQuery({ key: this.filterData?.filterParam, value });
|
||||
},
|
||||
},
|
||||
labelCountClasses() {
|
||||
return [...NAV_LINK_COUNT_DEFAULT_CLASSES, 'gl-text-gray-500'];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setQuery']),
|
||||
getFormatedCount(count) {
|
||||
return formatSearchResultCount(count);
|
||||
},
|
||||
},
|
||||
NAV_LINK_COUNT_DEFAULT_CLASSES,
|
||||
LABEL_DEFAULT_CLASSES,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="gl-mx-5">
|
||||
<h5 class="gl-mt-0">{{ filterData.header }}</h5>
|
||||
<gl-form-checkbox-group v-model="selectedFilter">
|
||||
<gl-form-checkbox
|
||||
v-for="f in dataFilters"
|
||||
:key="f.label"
|
||||
:value="f.label"
|
||||
class="gl-flex-grow-1 gl-display-inline-flex gl-justify-content-space-between gl-w-full"
|
||||
:class="$options.LABEL_DEFAULT_CLASSES"
|
||||
>
|
||||
<span
|
||||
class="gl-flex-grow-1 gl-display-inline-flex gl-justify-content-space-between gl-w-full"
|
||||
>
|
||||
<span data-testid="label">
|
||||
{{ f.label }}
|
||||
</span>
|
||||
<span v-if="f.count" :class="labelCountClasses" data-testid="labelCount">
|
||||
{{ getFormatedCount(f.count) }}
|
||||
</span>
|
||||
</span>
|
||||
</gl-form-checkbox>
|
||||
</gl-form-checkbox-group>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
<script>
|
||||
import { GlButton, GlAlert, GlForm } from '@gitlab/ui';
|
||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { DEFAULT_ITEM_LENGTH, MAX_ITEM_LENGTH } from '../constants/language_filter_data';
|
||||
import { HR_DEFAULT_CLASSES, ONLY_SHOW_MD } from '../constants';
|
||||
import { convertFiltersData } from '../utils';
|
||||
import CheckboxFilter from './checkbox_filter.vue';
|
||||
|
||||
export default {
|
||||
name: 'LanguageFilter',
|
||||
components: {
|
||||
CheckboxFilter,
|
||||
GlButton,
|
||||
GlAlert,
|
||||
GlForm,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
data() {
|
||||
return {
|
||||
showAll: false,
|
||||
};
|
||||
},
|
||||
i18n: {
|
||||
showMore: s__('GlobalSearch|Show more'),
|
||||
apply: __('Apply'),
|
||||
showingMax: sprintf(s__('GlobalSearch|Showing top %{maxItems}'), { maxItems: MAX_ITEM_LENGTH }),
|
||||
loadError: s__('GlobalSearch|Aggregations load error.'),
|
||||
},
|
||||
computed: {
|
||||
...mapState(['aggregations', 'sidebarDirty']),
|
||||
...mapGetters(['langugageAggregationBuckets']),
|
||||
ffBasedXPadding() {
|
||||
return this.glFeatures.searchPageVerticalNav ? 'gl-px-5' : 'gl-px-0';
|
||||
},
|
||||
hasBuckets() {
|
||||
return this.langugageAggregationBuckets.length > 0;
|
||||
},
|
||||
filtersData() {
|
||||
return convertFiltersData(this.shortenedLanguageFilters);
|
||||
},
|
||||
shortenedLanguageFilters() {
|
||||
if (!this.hasShowMore) {
|
||||
return this.langugageAggregationBuckets;
|
||||
}
|
||||
if (this.showAll) {
|
||||
return this.trimBuckets(MAX_ITEM_LENGTH);
|
||||
}
|
||||
return this.trimBuckets(DEFAULT_ITEM_LENGTH);
|
||||
},
|
||||
hasShowMore() {
|
||||
return this.langugageAggregationBuckets.length > DEFAULT_ITEM_LENGTH;
|
||||
},
|
||||
hasOverMax() {
|
||||
return this.langugageAggregationBuckets.length > MAX_ITEM_LENGTH;
|
||||
},
|
||||
dividerClasses() {
|
||||
return [...HR_DEFAULT_CLASSES, ...ONLY_SHOW_MD];
|
||||
},
|
||||
},
|
||||
async created() {
|
||||
await this.fetchLanguageAggregation();
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['applyQuery', 'fetchLanguageAggregation']),
|
||||
onShowMore() {
|
||||
this.showAll = true;
|
||||
},
|
||||
trimBuckets(length) {
|
||||
return this.langugageAggregationBuckets.slice(0, length);
|
||||
},
|
||||
},
|
||||
HR_DEFAULT_CLASSES,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<gl-form
|
||||
v-if="hasBuckets"
|
||||
class="gl-pt-5 gl-md-pt-0 language-filter-checkbox"
|
||||
@submit.prevent="applyQuery"
|
||||
>
|
||||
<hr :class="dividerClasses" />
|
||||
<div
|
||||
v-if="!aggregations.error"
|
||||
class="gl-overflow-x-hidden gl-overflow-y-auto"
|
||||
:class="{ 'language-filter-max-height': showAll }"
|
||||
>
|
||||
<checkbox-filter :class="ffBasedXPadding" :filter-data="filtersData" />
|
||||
<span v-if="showAll && hasOverMax" data-testid="has-over-max-text">{{
|
||||
$options.i18n.showingMax
|
||||
}}</span>
|
||||
</div>
|
||||
<gl-alert v-else class="gl-mx-5" variant="danger" :dismissible="false">{{
|
||||
$options.i18n.loadError
|
||||
}}</gl-alert>
|
||||
<div v-if="hasShowMore && !showAll" class="gl-px-5 language-filter-show-all">
|
||||
<gl-button
|
||||
data-testid="show-more-button"
|
||||
category="tertiary"
|
||||
variant="link"
|
||||
size="small"
|
||||
button-text-classes="gl-font-sm"
|
||||
@click="onShowMore"
|
||||
>
|
||||
{{ $options.i18n.showMore }}
|
||||
</gl-button>
|
||||
</div>
|
||||
<div v-if="!aggregations.error">
|
||||
<hr :class="$options.HR_DEFAULT_CLASSES" />
|
||||
<div class="gl-display-flex gl-align-items-center gl-mt-4 gl-mx-5" :class="ffBasedXPadding">
|
||||
<gl-button
|
||||
category="primary"
|
||||
variant="confirm"
|
||||
type="submit"
|
||||
:disabled="!sidebarDirty"
|
||||
data-testid="apply-button"
|
||||
>
|
||||
{{ $options.i18n.apply }}
|
||||
</gl-button>
|
||||
</div>
|
||||
</div>
|
||||
</gl-form>
|
||||
</template>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import { languageFilterData } from '~/search/sidebar/constants/language_filter_data';
|
||||
|
||||
export const convertFiltersData = (rawBuckets) => {
|
||||
return rawBuckets.reduce(
|
||||
(acc, bucket) => {
|
||||
return {
|
||||
...acc,
|
||||
filters: {
|
||||
...acc.filters,
|
||||
[bucket.key.toUpperCase()]: {
|
||||
label: bucket.key,
|
||||
value: bucket.key,
|
||||
count: bucket.count,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
{ ...languageFilterData, filters: {} },
|
||||
);
|
||||
};
|
||||
|
|
@ -634,9 +634,10 @@ $status-icon-size: 22px;
|
|||
/*
|
||||
* Social Icons
|
||||
*/
|
||||
$twitter: #1d9bf0;
|
||||
$skype: #0078d7;
|
||||
$discord: #5865f2;
|
||||
$linkedin: #2867b2;
|
||||
$skype: #0078d7;
|
||||
$twitter: #1d9bf0;
|
||||
|
||||
/*
|
||||
* Award emoji
|
||||
|
|
|
|||
|
|
@ -240,6 +240,10 @@
|
|||
color: $twitter;
|
||||
}
|
||||
|
||||
.discord-icon {
|
||||
color: $discord;
|
||||
}
|
||||
|
||||
.key-created-at {
|
||||
line-height: 42px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ $search-avatar-size: 16px;
|
|||
$search-sidebar-min-width: 240px;
|
||||
$search-sidebar-max-width: 300px;
|
||||
$search-keyboard-shortcut: '/';
|
||||
$language-filter-max-height: 20rem;
|
||||
|
||||
$border-radius-medium: 3px;
|
||||
|
||||
|
|
@ -25,6 +26,16 @@ $border-radius-medium: 3px;
|
|||
min-width: $search-sidebar-min-width;
|
||||
max-width: $search-sidebar-max-width;
|
||||
}
|
||||
|
||||
.language-filter-checkbox {
|
||||
.custom-control-label {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.language-filter-max-height {
|
||||
max-height: $language-filter-max-height;
|
||||
}
|
||||
}
|
||||
|
||||
.search-max-w-inherit {
|
||||
|
|
|
|||
|
|
@ -359,6 +359,7 @@ class Admin::UsersController < Admin::ApplicationController
|
|||
:skype,
|
||||
:theme_id,
|
||||
:twitter,
|
||||
:discord,
|
||||
:username,
|
||||
:website_url,
|
||||
:note,
|
||||
|
|
|
|||
|
|
@ -127,6 +127,7 @@ class ProfilesController < Profiles::ApplicationController
|
|||
:commit_email,
|
||||
:skype,
|
||||
:twitter,
|
||||
:discord,
|
||||
:username,
|
||||
:website_url,
|
||||
:organization,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,9 @@ class SearchController < ApplicationController
|
|||
end
|
||||
before_action :check_search_rate_limit!, only: search_rate_limited_endpoints
|
||||
|
||||
before_action only: :show do
|
||||
push_frontend_feature_flag(:search_blobs_language_aggregation, current_user)
|
||||
end
|
||||
before_action only: :show do
|
||||
update_scope_for_code_search
|
||||
end
|
||||
|
|
|
|||
|
|
@ -368,6 +368,12 @@ module ApplicationHelper
|
|||
end
|
||||
end
|
||||
|
||||
def discord_url(user)
|
||||
return '' if user.discord.blank?
|
||||
|
||||
"https://discord.com/users/#{user.discord}"
|
||||
end
|
||||
|
||||
def collapsed_sidebar?
|
||||
cookies["sidebar_collapsed"] == "true"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -380,6 +380,7 @@ class User < ApplicationRecord
|
|||
delegate :website_url, :website_url=, to: :user_detail, allow_nil: true
|
||||
delegate :location, :location=, to: :user_detail, allow_nil: true
|
||||
delegate :organization, :organization=, to: :user_detail, allow_nil: true
|
||||
delegate :discord, :discord=, to: :user_detail, allow_nil: true
|
||||
|
||||
accepts_nested_attributes_for :user_preference, update_only: true
|
||||
accepts_nested_attributes_for :user_detail, update_only: true
|
||||
|
|
|
|||
|
|
@ -14,11 +14,13 @@ class UserDetail < ApplicationRecord
|
|||
|
||||
DEFAULT_FIELD_LENGTH = 500
|
||||
|
||||
validates :discord, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
|
||||
validate :discord_format
|
||||
validates :linkedin, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
|
||||
validates :twitter, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
|
||||
validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
|
||||
validates :location, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
|
||||
validates :organization, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
|
||||
validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
|
||||
validates :twitter, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
|
||||
validates :website_url, length: { maximum: DEFAULT_FIELD_LENGTH }, url: true, allow_blank: true, if: :website_url_changed?
|
||||
|
||||
before_validation :sanitize_attrs
|
||||
|
|
@ -27,7 +29,7 @@ class UserDetail < ApplicationRecord
|
|||
enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true
|
||||
|
||||
def sanitize_attrs
|
||||
%i[linkedin skype twitter website_url].each do |attr|
|
||||
%i[discord linkedin skype twitter website_url].each do |attr|
|
||||
value = self[attr]
|
||||
self[attr] = Sanitize.clean(value) if value.present?
|
||||
end
|
||||
|
|
@ -41,13 +43,20 @@ class UserDetail < ApplicationRecord
|
|||
|
||||
def prevent_nil_fields
|
||||
self.bio = '' if bio.nil?
|
||||
self.discord = '' if discord.nil?
|
||||
self.linkedin = '' if linkedin.nil?
|
||||
self.twitter = '' if twitter.nil?
|
||||
self.skype = '' if skype.nil?
|
||||
self.location = '' if location.nil?
|
||||
self.organization = '' if organization.nil?
|
||||
self.skype = '' if skype.nil?
|
||||
self.twitter = '' if twitter.nil?
|
||||
self.website_url = '' if website_url.nil?
|
||||
end
|
||||
end
|
||||
|
||||
def discord_format
|
||||
return if discord.blank? || discord =~ %r{\A\d{17,20}\z}
|
||||
|
||||
errors.add(:discord, _('must contain only a discord user ID.'))
|
||||
end
|
||||
|
||||
UserDetail.prepend_mod_with('UserDetail')
|
||||
|
|
|
|||
|
|
@ -49,6 +49,6 @@ class WikiDirectory
|
|||
# Relative path to the partial to be used when rendering collections
|
||||
# of this object.
|
||||
def to_partial_path
|
||||
'../shared/wikis/wiki_directory'
|
||||
'shared/wikis/wiki_directory'
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -259,7 +259,7 @@ class WikiPage
|
|||
# Relative path to the partial to be used when rendering collections
|
||||
# of this object.
|
||||
def to_partial_path
|
||||
'../shared/wikis/wiki_page'
|
||||
'shared/wikis/wiki_page'
|
||||
end
|
||||
|
||||
def sha
|
||||
|
|
|
|||
|
|
@ -301,6 +301,8 @@ class ProjectPolicy < BasePolicy
|
|||
|
||||
rule { can?(:reporter_access) & can?(:create_issue) }.enable :create_incident
|
||||
|
||||
rule { can?(:reporter_access) & can?(:read_environment) }.enable :read_freeze_period
|
||||
|
||||
rule { can?(:create_issue) }.enable :create_work_item
|
||||
|
||||
rule { can?(:create_issue) }.enable :create_task
|
||||
|
|
|
|||
|
|
@ -68,10 +68,6 @@ module Projects
|
|||
}
|
||||
end
|
||||
|
||||
def to_partial_path
|
||||
'../../shared/deploy_keys/index'
|
||||
end
|
||||
|
||||
def form_partial_path
|
||||
'shared/deploy_keys/project_group_form'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -260,7 +260,7 @@ module Ci
|
|||
end
|
||||
|
||||
def acquire_temporary_lock(build_id)
|
||||
return true unless Feature.enabled?(:ci_register_job_temporary_lock, runner)
|
||||
return true if Feature.disabled?(:ci_register_job_temporary_lock, runner, type: :ops)
|
||||
|
||||
key = "build/register/#{build_id}"
|
||||
|
||||
|
|
|
|||
|
|
@ -103,6 +103,15 @@
|
|||
.form-group.gl-form-group
|
||||
= f.label :twitter
|
||||
= f.text_field :twitter, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|@username")
|
||||
.form-group.gl-form-group
|
||||
- external_accounts_help_url = help_page_path('user/profile/index', anchor: 'add-external-accounts-to-your-user-profile-page')
|
||||
- external_accounts_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: external_accounts_help_url }
|
||||
- external_accounts_docs_link = s_('Profiles|Your Discord user ID. Should be between %{min} and %{max} digits long. %{external_accounts_link_start}Learn more.%{external_accounts_link_end}').html_safe % { min: '17', max: '20', external_accounts_link_start: external_accounts_link_start, external_accounts_link_end: '</a>'.html_safe }
|
||||
= f.label :discord
|
||||
= f.text_field :discord, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|User ID")
|
||||
%small.form-text.text-gl-muted
|
||||
= external_accounts_docs_link
|
||||
|
||||
.form-group.gl-form-group
|
||||
= f.label :website_url, s_('Profiles|Website url')
|
||||
= f.text_field :website_url, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|https://website.com")
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
-# reused in EE.
|
||||
= render "projects/settings/repository/protected_branches", protected_branch_entity: @project
|
||||
= render "shared/deploy_tokens/index", group_or_project: @project, description: deploy_token_description
|
||||
= render @deploy_keys
|
||||
= render 'shared/deploy_keys/index'
|
||||
= render "projects/cleanup/show"
|
||||
|
||||
= render_if_exists 'shared/promotions/promote_repository_features'
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@
|
|||
= render_wiki_content(@sidebar_page)
|
||||
- elsif @sidebar_wiki_entries
|
||||
%ul.wiki-pages
|
||||
= render @sidebar_wiki_entries, context: 'sidebar'
|
||||
- @sidebar_wiki_entries.each do |entry|
|
||||
= render partial: entry.to_partial_path, object: entry, locals: { context: 'sidebar' }
|
||||
.block.w-100
|
||||
- if @sidebar_limited
|
||||
= link_to wiki_path(@wiki, action: :pages), class: 'btn gl-button btn-block', data: { qa_selector: 'view_all_pages_button' } do
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@
|
|||
= link_to wiki_page_path(@wiki, wiki_directory), data: { qa_selector: 'wiki_dir_page_link', qa_page_name: wiki_directory.title } do
|
||||
= wiki_directory.title
|
||||
%ul
|
||||
= render wiki_directory.entries, context: context
|
||||
- wiki_directory.entries.each do |entry|
|
||||
= render partial: entry.to_partial_path, object: entry, locals: { context: context }
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
= wiki_sort_controls(@wiki, params[:direction])
|
||||
|
||||
%ul.wiki-pages-list.content-list
|
||||
= render @wiki_entries, context: 'pages'
|
||||
- @wiki_entries.each do |entry|
|
||||
= render partial: entry.to_partial_path, object: entry, locals: { context: 'pages' }
|
||||
|
||||
= paginate @wiki_pages, theme: 'gitlab'
|
||||
|
|
|
|||
|
|
@ -101,6 +101,10 @@
|
|||
= render 'middle_dot_divider', breakpoint: 'sm' do
|
||||
= link_to twitter_url(@user), class: 'gl-hover-text-decoration-none', title: "Twitter", target: '_blank', rel: 'noopener noreferrer nofollow' do
|
||||
= sprite_icon('twitter', css_class: 'twitter-icon')
|
||||
- unless @user.discord.blank?
|
||||
= render 'middle_dot_divider', breakpoint: 'sm' do
|
||||
= link_to discord_url(@user), class: 'gl-hover-text-decoration-none', title: "Discord", target: '_blank', rel: 'noopener noreferrer nofollow' do
|
||||
= sprite_icon('discord', css_class: 'discord-icon')
|
||||
- unless @user.website_url.blank?
|
||||
= render 'middle_dot_divider', stacking: true do
|
||||
- if Feature.enabled?(:security_auto_fix) && @user.bot?
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@ name: ci_register_job_temporary_lock
|
|||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55202
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323180
|
||||
milestone: '13.10'
|
||||
type: development
|
||||
type: ops
|
||||
group: group::pipeline execution
|
||||
default_enabled: false
|
||||
default_enabled: true
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDiscordToUserDetails < Gitlab::Database::Migration[2.1]
|
||||
enable_lock_retries!
|
||||
|
||||
# rubocop:disable Migration/AddLimitToTextColumns
|
||||
# limits are added in 20221128165833_add_discord_field_limit_to_user_details.rb
|
||||
def change
|
||||
add_column :user_details, :discord, :text, default: '', null: false
|
||||
end
|
||||
# rubocop:enable Migration/AddLimitToTextColumns
|
||||
end
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDiscordFieldLimitToUserDetails < Gitlab::Database::Migration[2.1]
|
||||
disable_ddl_transaction!
|
||||
|
||||
USER_DETAILS_FIELD_LIMIT = 500
|
||||
|
||||
def up
|
||||
add_text_limit :user_details, :discord, USER_DETAILS_FIELD_LIMIT
|
||||
end
|
||||
|
||||
def down
|
||||
remove_text_limit :user_details, :discord
|
||||
end
|
||||
end
|
||||
|
|
@ -3,29 +3,13 @@
|
|||
class SanitizeConfidentialNoteTodos < Gitlab::Database::Migration[2.0]
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
MIGRATION = 'SanitizeConfidentialTodos'
|
||||
DELAY_INTERVAL = 2.minutes.to_i
|
||||
BATCH_SIZE = 200
|
||||
MAX_BATCH_SIZE = 1000
|
||||
SUB_BATCH_SIZE = 20
|
||||
|
||||
disable_ddl_transaction!
|
||||
restrict_gitlab_migration gitlab_schema: :gitlab_main
|
||||
|
||||
def up
|
||||
queue_batched_background_migration(
|
||||
MIGRATION,
|
||||
:notes,
|
||||
:id,
|
||||
job_interval: DELAY_INTERVAL,
|
||||
batch_size: BATCH_SIZE,
|
||||
max_batch_size: MAX_BATCH_SIZE,
|
||||
sub_batch_size: SUB_BATCH_SIZE,
|
||||
gitlab_schema: :gitlab_main
|
||||
)
|
||||
# no-op: this empty migration is left here only for compatibility reasons.
|
||||
# It was a temporary migration which used not-isolated code.
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/382557
|
||||
end
|
||||
|
||||
def down
|
||||
delete_batched_background_migration(MIGRATION, :notes, :id, [])
|
||||
# no-op
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
39ca72ad461ff7b56ce6feed351ef46ee9f3584a8c3c9383ca75f44b61baa1a1
|
||||
|
|
@ -0,0 +1 @@
|
|||
4f4846fe8e5f84ee566dfc8f9b8249e1ff1d77f8f6c2f0006d89a73a2e734b9d
|
||||
|
|
@ -22714,9 +22714,11 @@ CREATE TABLE user_details (
|
|||
organization text DEFAULT ''::text NOT NULL,
|
||||
password_last_changed_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
onboarding_step_url text,
|
||||
discord text DEFAULT ''::text NOT NULL,
|
||||
CONSTRAINT check_245664af82 CHECK ((char_length(webauthn_xid) <= 100)),
|
||||
CONSTRAINT check_444573ee52 CHECK ((char_length(skype) <= 500)),
|
||||
CONSTRAINT check_466a25be35 CHECK ((char_length(twitter) <= 500)),
|
||||
CONSTRAINT check_4ef1de1a15 CHECK ((char_length(discord) <= 500)),
|
||||
CONSTRAINT check_4f51129940 CHECK ((char_length(onboarding_step_url) <= 2000)),
|
||||
CONSTRAINT check_7b246dad73 CHECK ((char_length(organization) <= 500)),
|
||||
CONSTRAINT check_7d6489f8f3 CHECK ((char_length(linkedin) <= 500)),
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ POST /features/:name
|
|||
| `name` | string | yes | Name of the feature to create or update |
|
||||
| `value` | integer/string | yes | `true` or `false` to enable/disable, or an integer for percentage of time |
|
||||
| `key` | string | no | `percentage_of_actors` or `percentage_of_time` (default) |
|
||||
| `feature_group` | string | no | A Feature group name |
|
||||
| `feature_group` | string | no | A [Feature group](../development/feature_flags/index.md#feature-groups) name |
|
||||
| `user` | string | no | A GitLab username or comma-separated multiple usernames |
|
||||
| `group` | string | no | A GitLab group's path, for example `gitlab-org`, or comma-separated multiple group paths |
|
||||
| `namespace` | string | no | A GitLab group or user namespace's path, for example `john-doe`, or comma-separated multiple namespace paths. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/353117) in GitLab 15.0. |
|
||||
|
|
|
|||
|
|
@ -13,8 +13,9 @@ You can use the Freeze Periods API to manipulate GitLab [Freeze Period](../user/
|
|||
|
||||
## Permissions and security
|
||||
|
||||
Only users with Maintainer [permissions](../user/permissions.md) can
|
||||
interact with the Freeze Period API endpoints.
|
||||
Users with Reporter [permissions](../user/permissions.md) or greater can read
|
||||
Freeze Period API endpoints. Only users with the Maintainer role can modify
|
||||
Freeze Periods.
|
||||
|
||||
## List freeze periods
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,10 @@ The Debian group API is behind a feature flag that is disabled by default.
|
|||
can opt to enable it. To enable it, follow the instructions in
|
||||
[Enable the Debian group API](../../user/packages/debian_repository/index.md#enable-the-debian-group-api).
|
||||
|
||||
### Authenticate to the Debian Package Repositories
|
||||
|
||||
See [Authenticate to the Debian Package Repositories](../../user/packages/debian_repository/index.md#authenticate-to-the-debian-package-repositories).
|
||||
|
||||
## Upload a package file
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62028) in GitLab 14.0.
|
||||
|
|
|
|||
|
|
@ -26,12 +26,16 @@ Debian group repository support is still a work in progress. It's gated behind a
|
|||
can opt to enable it. To enable it, follow the instructions in
|
||||
[Enable the Debian group API](../../user/packages/debian_repository/index.md#enable-the-debian-group-api).
|
||||
|
||||
## Authenticate to the Debian distributions APIs
|
||||
|
||||
See [Authenticate to the Debian distributions APIs](../../user/packages/debian_repository/index.md#authenticate-to-the-debian-distributions-apis).
|
||||
|
||||
## List all Debian distributions in a group
|
||||
|
||||
Lists Debian distributions in the given group.
|
||||
|
||||
```plaintext
|
||||
GET /groups/:id/debian_distributions
|
||||
GET /groups/:id/-/debian_distributions
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|
|
@ -73,7 +77,7 @@ Example response:
|
|||
Gets a single Debian group distribution.
|
||||
|
||||
```plaintext
|
||||
GET /groups/:id/debian_distributions/:codename
|
||||
GET /groups/:id/-/debian_distributions/:codename
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|
|
@ -112,7 +116,7 @@ Example response:
|
|||
Gets a single Debian group distribution key.
|
||||
|
||||
```plaintext
|
||||
GET /groups/:id/debian_distributions/:codename/key.asc
|
||||
GET /groups/:id/-/debian_distributions/:codename/key.asc
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|
|
@ -149,7 +153,7 @@ DAAKCRDyMVUMT0fjjlnQAQDFHUs6TIcxrNTtEZFjUFm1M0PJ1Dng/cDW4xN80fsn
|
|||
Creates a Debian group distribution.
|
||||
|
||||
```plaintext
|
||||
POST /groups/:id/debian_distributions
|
||||
POST /groups/:id/-/debian_distributions
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|
|
@ -196,7 +200,7 @@ Example response:
|
|||
Updates a Debian group distribution.
|
||||
|
||||
```plaintext
|
||||
PUT /groups/:id/debian_distributions/:codename
|
||||
PUT /groups/:id/-/debian_distributions/:codename
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|
|
@ -243,7 +247,7 @@ Example response:
|
|||
Deletes a Debian group distribution.
|
||||
|
||||
```plaintext
|
||||
DELETE /groups/:id/debian_distributions/:codename
|
||||
DELETE /groups/:id/-/debian_distributions/:codename
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ The Debian API is behind a feature flag that is disabled by default.
|
|||
can opt to enable it. To enable it, follow the instructions in
|
||||
[Enable the Debian API](../../user/packages/debian_repository/index.md#enable-the-debian-api).
|
||||
|
||||
## Authenticate to the Debian distributions APIs
|
||||
|
||||
See [Authenticate to the Debian distributions APIs](../../user/packages/debian_repository/index.md#authenticate-to-the-debian-distributions-apis).
|
||||
|
||||
## List all Debian distributions in a project
|
||||
|
||||
Lists Debian distributions in the given project.
|
||||
|
|
|
|||
|
|
@ -2867,7 +2867,7 @@ Read more in the [Project vulnerabilities](project_vulnerabilities.md) documenta
|
|||
|
||||
## Get a project's pull mirror details **(PREMIUM)**
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354506) in GitLab 15.5.
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/354506) in GitLab 15.6.
|
||||
|
||||
Returns the details of the project's pull mirror.
|
||||
|
||||
|
|
|
|||
|
|
@ -336,9 +336,12 @@ scope.
|
|||
| GitLab Rails app | `%15.8` | Create database migration to add `config` column to `ci_runner_machines` table. |
|
||||
| GitLab Runner | `%15.9` | Start sending `system_id` value in `POST /jobs/request` request and other follow-up requests that require identifying the unique system. |
|
||||
| GitLab Rails app | `%15.9` | Create service similar to `StaleGroupRunnersPruneCronWorker` service to clean up `ci_runner_machines` records instead of `ci_runners` records.<br/>Existing service continues to exist but focuses only on legacy runners. |
|
||||
| GitLab Rails app | `%15.9` | [Feature flag] Rollout of `create_runner_machine`. |
|
||||
| GitLab Rails app | `%15.9` | Create `ci_runner_machines` record in `POST /runners/verify` request if the runner token is prefixed with `glrt-`. |
|
||||
| GitLab Rails app | `%15.9` | Use runner token + `system_id` JSON parameters in `POST /jobs/request` request in the [heartbeat request](https://gitlab.com/gitlab-org/gitlab/blob/c73c96a8ffd515295842d72a3635a8ae873d688c/lib/api/ci/helpers/runner.rb#L14-20) to update the `ci_runner_machines` cache/table. |
|
||||
| GitLab Rails app | `%15.9` | [Feature flag] Enable runner creation workflow (`create_runner_workflow`). |
|
||||
| GitLab Rails app | `%15.9` | Implement `create_{instance|group|project}_runner` permissions. |
|
||||
| GitLab Rails app | `%15.10` | Rename `ci_runner_machines.machine_xid` column to `system_xid` to be consistent with `system_id` passed in APIs. |
|
||||
|
||||
### Stage 4 - New UI
|
||||
|
||||
|
|
@ -355,13 +358,22 @@ scope.
|
|||
|
||||
| Component | Milestone | Changes |
|
||||
|------------------|----------:|---------|
|
||||
| GitLab Rails app | | Add UI to allow disabling use of registration tokens at project or group level. |
|
||||
| GitLab Rails app | `16.0` | Introduce `:disable_runner_registration_tokens` feature flag (enabled by default) to control whether use of registration tokens is allowed. |
|
||||
| GitLab Rails app | | Make [`POST /api/v4/runners` endpoint](../../../api/runners.md#register-a-new-runner) permanently return `HTTP 410 Gone` if either `allow_runner_registration_token` setting or `:disable_runner_registration_tokens` feature flag disables registration tokens.<br/>A future v5 version of the API should return `HTTP 404 Not Found`. |
|
||||
| GitLab Rails app | | Start refusing job requests that don't include a unique ID, if either `allow_runner_registration_token` setting or `:disable_runner_registration_tokens` feature flag disables registration tokens. |
|
||||
| GitLab Rails app | | Hide legacy UI showing registration with a registration token, if `:disable_runner_registration_tokens` feature flag disables registration tokens. |
|
||||
| GitLab Rails app | `%15.11` | Adapt `register_{group|project}_runner` permissions to take [application setting](https://gitlab.com/gitlab-org/gitlab/-/issues/386712) in consideration. |
|
||||
| GitLab Rails app | `%15.11` | Add UI to allow disabling use of registration tokens at project or group level. |
|
||||
| GitLab Rails app | `%15.11` | Introduce `:enforce_create_runner_workflow` feature flag (disabled by default) to control whether use of registration tokens is allowed. |
|
||||
| GitLab Rails app | `%15.11` | Make [`POST /api/v4/runners` endpoint](../../../api/runners.md#register-a-new-runner) permanently return `HTTP 410 Gone` if either `allow_runner_registration_token` setting or `:enforce_create_runner_workflow` feature flag disables registration tokens.<br/>A future v5 version of the API should return `HTTP 404 Not Found`. |
|
||||
| GitLab Rails app | `%15.11` | Start refusing job requests that don't include a unique ID, if either `allow_runner_registration_token` setting or `:enforce_create_runner_workflow` feature flag disables registration tokens. |
|
||||
| GitLab Rails app | `%15.11` | Hide legacy UI showing registration with a registration token, if `:enforce_create_runner_workflow` feature flag disables registration tokens. |
|
||||
|
||||
### Stage 6 - Removals
|
||||
### Stage 6 - Enforcement
|
||||
|
||||
| Component | Milestone | Changes |
|
||||
|------------------|----------:|---------|
|
||||
| GitLab Runner | `%16.0` | Do not allow runner to start if `.runner_system_id` file cannot be written. |
|
||||
| GitLab Rails app | `%16.0` | Enable `:enforce_create_runner_workflow` feature flag by default. |
|
||||
| GitLab Rails app | `%16.0` | Start reject job requests that don't include `system_id` value. |
|
||||
|
||||
### Stage 7 - Removals
|
||||
|
||||
| Component | Milestone | Changes |
|
||||
|------------------|----------:|---------|
|
||||
|
|
@ -369,7 +381,7 @@ scope.
|
|||
| GitLab Runner | `17.0` | Remove runner model arguments from `register` command (for example `--run-untagged`, `--tag-list`, etc.) |
|
||||
| GitLab Rails app | `17.0` | Create database migrations to drop `allow_runner_registration_token` setting columns from `application_settings` and `namespace_settings` tables. |
|
||||
| GitLab Rails app | `17.0` | Create database migrations to drop:<br/>- `runners_registration_token`/`runners_registration_token_encrypted` columns from `application_settings`;<br/>- `runners_token`/`runners_token_encrypted` from `namespaces` table;<br/>- `runners_token`/`runners_token_encrypted` from `projects` table. |
|
||||
| GitLab Rails app | `17.0` | Remove `:disable_runner_registration_tokens` feature flag. |
|
||||
| GitLab Rails app | `17.0` | Remove `:enforce_create_runner_workflow` feature flag. |
|
||||
|
||||
## Status
|
||||
|
||||
|
|
|
|||
|
|
@ -196,7 +196,22 @@ enabled for only the `gitlab` project. The project is passed by supplying a
|
|||
/chatops run feature set --project=gitlab-org/gitlab some_feature true
|
||||
```
|
||||
|
||||
For groups the `--group` flag is available:
|
||||
You can use the `--user` option to enable a feature flag for a specific user:
|
||||
|
||||
```shell
|
||||
/chatops run feature set --user=myusername some_feature true
|
||||
```
|
||||
|
||||
If you would like to gather feedback internally first,
|
||||
feature flags scoped to a user can also be enabled
|
||||
for GitLab team members with the `gitlab_team_members`
|
||||
[feature group](index.md#feature-groups):
|
||||
|
||||
```shell
|
||||
/chatops run feature set --feature-group=gitlab_team_members some_feature true
|
||||
```
|
||||
|
||||
You can use the `--group` flag to enable a feature flag for a specific group:
|
||||
|
||||
```shell
|
||||
/chatops run feature set --group=gitlab-org some_feature true
|
||||
|
|
|
|||
|
|
@ -459,6 +459,18 @@ dynamic (querying the DB, for example).
|
|||
Once defined in `lib/feature.rb`, you can to activate a
|
||||
feature for a given feature group via the [`feature_group` parameter of the features API](../../api/features.md#set-or-create-a-feature)
|
||||
|
||||
The available feature groups are:
|
||||
|
||||
| Group name | Scoped to | Description |
|
||||
| --------------------- | --------- | ----------- |
|
||||
| `gitlab_team_members` | Users | Enables the feature for users who are members of [`gitlab-com`](https://gitlab.com/gitlab-com) |
|
||||
|
||||
Feature groups can be enabled via the group name:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:feature_flag_name, :gitlab_team_members)
|
||||
```
|
||||
|
||||
### Enabling a feature flag locally (in development)
|
||||
|
||||
In the rails console (`rails c`), enter the following command to enable a feature flag:
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Error Tracking **(FREE)**
|
||||
|
||||
Error Tracking allows developers to easily discover and view the errors that their application may be generating. By surfacing error information where the code is being developed, efficiency and awareness can be increased.
|
||||
> [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/389991) in GitLab 15.9.
|
||||
|
||||
WARNING:
|
||||
This feature is in its end-of-life process. It is [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/389991)
|
||||
for use in GitLab 15.9, and is planned for removal in GitLab 16.0. We are replacing this feature with functionality in the [GitLab Observability UI](https://gitlab.com/gitlab-org/opstrace/opstrace-ui). Please also reference our direction for [Observability](https://about.gitlab.com/direction/monitor/observability/) and [data visualization](https://about.gitlab.com/direction/monitor/observability/data-visualization/).
|
||||
|
||||
Error Tracking allows developers to discover and view errors generated by their application. Because error information is surfaced where the code is being developed, efficiency and awareness are increased.
|
||||
|
||||
## How error tracking works
|
||||
|
||||
|
|
|
|||
|
|
@ -264,6 +264,10 @@ NOTE:
|
|||
Specific information that follow related to Ruby and Git versions do not apply to [Omnibus installations](https://docs.gitlab.com/omnibus/)
|
||||
and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with appropriate Ruby and Git versions and are not using system binaries for Ruby and Git. There is no need to install Ruby or Git when utilizing these two approaches.
|
||||
|
||||
### 15.9.0
|
||||
|
||||
- This version removes `SanitizeConfidentialTodos` background migration which was [added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87908/diffs) in 15.6 and removed any user inaccessible Todos. Make sure that this migration is finished before upgrading to 15.9.
|
||||
|
||||
### 15.8.0
|
||||
|
||||
- Git 2.38.0 and later is required by Gitaly. For installations from source, you should use the [Git version provided by Gitaly](../install/installation.md#git).
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@ To modify the maximum file size for imports in GitLab:
|
|||
1. On the left sidebar, select **Settings > General**, then expand **Account and limit**.
|
||||
1. Increase or decrease by changing the value in **Maximum import size (MB)**.
|
||||
|
||||
This setting applies only to repositories
|
||||
[imported from a GitLab export file](../../project/settings/import_export.md#import-a-project-and-its-data).
|
||||
|
||||
If you choose a size larger than the configured value for the web server,
|
||||
you may receive errors. See the [troubleshooting section](#troubleshooting) for more
|
||||
details.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ Prerequisite:
|
|||
To view the list of agents:
|
||||
|
||||
1. On the top bar, select **Main menu > Projects** and find the project that contains your agent configuration file.
|
||||
You cannot view registered agents from a project that does not contain the agent configuration file.
|
||||
1. On the left sidebar, select **Infrastructure > Kubernetes clusters**.
|
||||
1. Select **Agent** tab to view clusters connected to GitLab through the agent.
|
||||
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ OKRs are in [**Alpha**](../policy/alpha-beta-support.md#alpha-features).
|
|||
For the OKR feature roadmap, see [epic 7864](https://gitlab.com/groups/gitlab-org/-/epics/7864).
|
||||
|
||||
FLAG:
|
||||
On self-managed GitLab, by default this feature is not available. To make it available,
|
||||
ask an administrator to [enable the featured flag](../administration/feature_flags.md) named `okrs_mvc`.
|
||||
On self-managed GitLab, by default this feature is not available. To make it available per project, ask an administrator to [enable the featured flag](../administration/feature_flags.md) named `okrs_mvc`.
|
||||
On GitLab.com, this feature is not available.
|
||||
The feature is not ready for production use.
|
||||
|
||||
Use objectives and key results to align your workforce towards common goals and track the progress.
|
||||
|
|
|
|||
|
|
@ -62,24 +62,44 @@ Feature.disable(:debian_group_packages)
|
|||
|
||||
Creating a Debian package is documented [on the Debian Wiki](https://wiki.debian.org/Packaging).
|
||||
|
||||
## Authenticate to the Package Registry
|
||||
## Authenticate to the Debian endpoints
|
||||
|
||||
To create a distribution, publish a package, or install a private package, you need one of the
|
||||
following:
|
||||
Authentication methods differs between [distributions APIs](#authenticate-to-the-debian-distributions-apis)
|
||||
and [package repositories](#authenticate-to-the-debian-package-repositories).
|
||||
|
||||
- [Personal access token](../../../api/rest/index.md#personalprojectgroup-access-tokens)
|
||||
- [CI/CD job token](../../../ci/jobs/ci_job_token.md)
|
||||
### Authenticate to the Debian distributions APIs
|
||||
|
||||
To create, read, update, or delete a distribution, you need one of the following:
|
||||
|
||||
- [Personal access token](../../../api/rest/index.md#personalprojectgroup-access-tokens),
|
||||
using `--header "PRIVATE-TOKEN: <personal_access_token>"`
|
||||
- [Deploy token](../../project/deploy_tokens/index.md)
|
||||
using `--header "Deploy-Token: <deploy_token>"`
|
||||
- [CI/CD job token](../../../ci/jobs/ci_job_token.md)
|
||||
using `--header "Job-Token: <job_token>"`
|
||||
|
||||
### Authenticate to the Debian Package Repositories
|
||||
|
||||
To publish a package, or install a private package, you need to use basic authentication,
|
||||
with one of the following:
|
||||
|
||||
- [Personal access token](../../../api/rest/index.md#personalprojectgroup-access-tokens),
|
||||
using `<username>:<personal_access_token>`
|
||||
- [Deploy token](../../project/deploy_tokens/index.md)
|
||||
using `<deploy_token_name>:<deploy_token>`
|
||||
- [CI/CD job token](../../../ci/jobs/ci_job_token.md)
|
||||
using `gitlab-ci-token:<job_token>`
|
||||
|
||||
## Create a Distribution
|
||||
|
||||
On the project-level, Debian packages are published using *Debian Distributions*. To publish
|
||||
packages on the group level, create a distribution with the same `codename`.
|
||||
|
||||
To create a project-level distribution:
|
||||
To create a project-level distribution using a personal access token:
|
||||
|
||||
```shell
|
||||
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/<project_id>/debian_distributions?codename=<codename>"
|
||||
curl --request POST --header "PRIVATE-TOKEN: <personal_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/projects/<project_id>/debian_distributions?codename=<codename>"
|
||||
```
|
||||
|
||||
Example response with `codename=sid`:
|
||||
|
|
@ -120,33 +140,50 @@ Once built, several files are created:
|
|||
- `.buildinfo` file: Used for Reproducible builds (optional)
|
||||
- `.changes` file: Upload metadata, and list of uploaded files (all the above)
|
||||
|
||||
To upload these files, you can use `dput-ng >= 1.32` (Debian bullseye):
|
||||
To upload these files, you can use `dput-ng >= 1.32` (Debian bullseye).
|
||||
`<username>` and `<password>` are defined
|
||||
[as above](#authenticate-to-the-debian-package-repositories):
|
||||
|
||||
```shell
|
||||
cat <<EOF > dput.cf
|
||||
[gitlab]
|
||||
method = https
|
||||
fqdn = <username>:<your_access_token>@gitlab.example.com
|
||||
fqdn = <username>:<password>@gitlab.example.com
|
||||
incoming = /api/v4/projects/<project_id>/packages/debian
|
||||
EOF
|
||||
|
||||
dput --config=dput.cf --unchecked --no-upload-log gitlab <your_package>.changes
|
||||
```
|
||||
|
||||
## Directly upload a package
|
||||
|
||||
> Direct upload [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101838) in GitLab 15.9.
|
||||
|
||||
When you don't have access to `.changes` file, you can directly upload a `.deb` by passing
|
||||
distribution `codename` and target `component` as parameters with
|
||||
your [credentials](#authenticate-to-the-debian-package-repositories).
|
||||
For example, to upload to component `main` of distribution `sid` using a personal access token:
|
||||
|
||||
```shell
|
||||
curl --request PUT --user "<username>:<personal_access_token>" \
|
||||
"https://gitlab.example.com/api/v4/projects/<project_id>/packages/debian/?distribution=sid&component=main" \
|
||||
--upload-file /path/to/your.deb
|
||||
```
|
||||
|
||||
## Install a package
|
||||
|
||||
To install a package:
|
||||
|
||||
1. Configure the repository:
|
||||
|
||||
If you are using a private project, add your [credentials](#authenticate-to-the-package-registry) to your apt configuration:
|
||||
If you are using a private project, add your [credentials](#authenticate-to-the-debian-package-repositories) to your apt configuration:
|
||||
|
||||
```shell
|
||||
echo 'machine gitlab.example.com login <username> password <your_access_token>' \
|
||||
echo 'machine gitlab.example.com login <username> password <password>' \
|
||||
| sudo tee /etc/apt/auth.conf.d/gitlab_project.conf
|
||||
```
|
||||
|
||||
Download your distribution key:
|
||||
Download your distribution key using your [credentials](#authenticate-to-the-debian-distributions-apis):
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /usr/local/share/keyrings
|
||||
|
|
@ -179,14 +216,14 @@ To download a source package:
|
|||
|
||||
1. Configure the repository:
|
||||
|
||||
If you are using a private project, add your [credentials](#authenticate-to-the-package-registry) to your apt configuration:
|
||||
If you are using a private project, add your [credentials](#authenticate-to-the-debian-package-repositories) to your apt configuration:
|
||||
|
||||
```shell
|
||||
echo 'machine gitlab.example.com login <username> password <your_access_token>' \
|
||||
echo 'machine gitlab.example.com login <username> password <password>' \
|
||||
| sudo tee /etc/apt/auth.conf.d/gitlab_project.conf
|
||||
```
|
||||
|
||||
Download your distribution key:
|
||||
Download your distribution key using your [credentials](#authenticate-to-the-debian-distributions-apis):
|
||||
|
||||
```shell
|
||||
sudo mkdir -p /usr/local/share/keyrings
|
||||
|
|
|
|||
|
|
@ -132,8 +132,9 @@ To add links to other accounts:
|
|||
1. On the top bar, in the upper-right corner, select your avatar.
|
||||
1. Select **Edit profile**.
|
||||
1. In the **Main settings** section, add your information from:
|
||||
- Skype
|
||||
- Discord ([User ID](https://support.discord.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-))
|
||||
- LinkedIn
|
||||
- Skype
|
||||
- Twitter
|
||||
1. Select **Update profile settings**.
|
||||
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ threads. Some quick actions might not be available to all subscription tiers.
|
|||
| `/create_merge_request <branch name>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Create a new merge request starting from the current issue. |
|
||||
| `/done` | **{check-circle}** Yes | **{check-circle}** Yes | **{check-circle}** Yes | Mark to do as done. |
|
||||
| `/draft` | **{dotted-circle}** No | **{check-circle}** Yes | **{dotted-circle}** No | Set the [draft status](merge_requests/drafts.md). Use for toggling the draft status ([deprecated](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/92654) in GitLab 15.4.) |
|
||||
| `/due <date>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Set due date. Examples of valid `<date>` include `in 2 days`, `this Friday` and `December 31st`. |
|
||||
| `/due <date>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Set due date. Examples of valid `<date>` include `in 2 days`, `this Friday` and `December 31st`. See [Chronic](https://gitlab.com/gitlab-org/ruby/gems/gitlab-chronic#examples) for more examples. |
|
||||
| `/duplicate <#issue>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Close this issue. Mark as a duplicate of, and related to, issue `<#issue>`. |
|
||||
| `/epic <epic>` | **{check-circle}** Yes | **{dotted-circle}** No | **{dotted-circle}** No | Add to epic `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. |
|
||||
| `/estimate <time>` or `/estimate_time <time>` | **{check-circle}** Yes | **{check-circle}** Yes | **{dotted-circle}** No | Set time estimate. For example, `/estimate 1mo 2w 3d 4h 5m`. Learn more about [time tracking](time_tracking.md). Alias `/estimate_time` [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16501) in GitLab 15.6. |
|
||||
|
|
|
|||
|
|
@ -137,6 +137,12 @@ Storage types that add to the total namespace storage are:
|
|||
|
||||
If your total namespace storage exceeds the available namespace storage quota, all projects under the namespace become read-only. Your ability to write new data is restricted until the read-only state is removed. For more information, see [Restricted actions](../user/read_only_namespaces.md#restricted-actions).
|
||||
|
||||
To notify you that you have nearly exceeded your namespace storage quota:
|
||||
|
||||
- In the command line interface, a notification displays after each `git push` action when you've reached 95% and 100% of your namespace storage quota.
|
||||
- In the GitLab UI, a notification displays when you've reached 75%, 95%, and 100% of your namespace storage quota.
|
||||
- GitLab sends an email to members with the Owner role to notify them when namespace storage usage is at 70%, 85%, 95%, and 100%.
|
||||
|
||||
To prevent exceeding the namespace storage quota, you can:
|
||||
|
||||
- Reduce storage consumption by following the suggestions in the [Manage Your Storage Usage](#manage-your-storage-usage) section of this page.
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@ module HamlLint
|
|||
class DocumentationLinks < Linter
|
||||
include ::HamlLint::LinterRegistry
|
||||
include ::Gitlab::Utils::Markdown
|
||||
extend ::RuboCop::AST::NodePattern::Macros
|
||||
|
||||
DOCS_DIRECTORY = File.join(File.expand_path('../..', __dir__), 'doc')
|
||||
|
||||
HELP_PATH_LINK_PATTERN = <<~PATTERN
|
||||
(send nil? {:help_page_url :help_page_path} $...)
|
||||
def_node_matcher :help_link, <<~PATTERN
|
||||
(send _ {:help_page_url :help_page_path} $...)
|
||||
PATTERN
|
||||
|
||||
MARKDOWN_HEADER = %r{\A\#{1,6}\s+(?<header>.+)\Z}.freeze
|
||||
|
|
@ -59,7 +60,7 @@ module HamlLint
|
|||
end
|
||||
|
||||
def extract_link_and_anchor(ast_tree)
|
||||
link_match, attributes_match = ::RuboCop::NodePattern.new(HELP_PATH_LINK_PATTERN).match(ast_tree)
|
||||
link_match, attributes_match = help_link(ast_tree)
|
||||
|
||||
{ link: fetch_link(link_match), anchor: fetch_anchor(attributes_match) }.compact
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,43 +10,14 @@ module Gitlab
|
|||
# to extract all related logic.
|
||||
# Details in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87908#note_952459215
|
||||
class SanitizeConfidentialTodos < BatchedMigrationJob
|
||||
scope_to ->(relation) { relation.where(confidential: true) }
|
||||
|
||||
operation_name :delete_invalid_todos
|
||||
feature_category :database
|
||||
|
||||
def perform
|
||||
each_sub_batch do |sub_batch|
|
||||
delete_ids = invalid_todo_ids(sub_batch)
|
||||
|
||||
Todo.where(id: delete_ids).delete_all if delete_ids.present?
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def invalid_todo_ids(notes_batch)
|
||||
todos = Todo.where(note_id: notes_batch.select(:id)).includes(:note, :user)
|
||||
|
||||
todos.each_with_object([]) do |todo, ids|
|
||||
ids << todo.id if invalid_todo?(todo)
|
||||
end
|
||||
end
|
||||
|
||||
def invalid_todo?(todo)
|
||||
return false unless todo.note
|
||||
return false if Ability.allowed?(todo.user, :read_todo, todo)
|
||||
|
||||
logger.info(
|
||||
message: "#{self.class.name} deleting invalid todo",
|
||||
attributes: todo.attributes
|
||||
)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def logger
|
||||
@logger ||= Gitlab::BackgroundMigration::Logger.build
|
||||
# no-op: this BG migration is left here only for compatibility reasons,
|
||||
# but it's not scheduled from any migration anymore.
|
||||
# It was a temporary migration which used not-isolated code.
|
||||
# https://gitlab.com/gitlab-org/gitlab/-/issues/382557
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -847,9 +847,10 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def list_refs(patterns = [Gitlab::Git::BRANCH_REF_PREFIX])
|
||||
# peel_tags slows down the request by a factor of 3-4
|
||||
def list_refs(patterns = [Gitlab::Git::BRANCH_REF_PREFIX], pointing_at_oids: [], peel_tags: false)
|
||||
wrapped_gitaly_errors do
|
||||
gitaly_ref_client.list_refs(patterns)
|
||||
gitaly_ref_client.list_refs(patterns, pointing_at_oids: pointing_at_oids, peel_tags: peel_tags)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -205,10 +205,13 @@ module Gitlab
|
|||
raise ArgumentError, ex
|
||||
end
|
||||
|
||||
def list_refs(patterns = [Gitlab::Git::BRANCH_REF_PREFIX])
|
||||
# peel_tags slows down the request by a factor of 3-4
|
||||
def list_refs(patterns = [Gitlab::Git::BRANCH_REF_PREFIX], pointing_at_oids: [], peel_tags: false)
|
||||
request = Gitaly::ListRefsRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
patterns: patterns
|
||||
patterns: patterns,
|
||||
pointing_at_oids: pointing_at_oids,
|
||||
peel_tags: peel_tags
|
||||
)
|
||||
|
||||
response = gitaly_client_call(@storage, :ref_service, :list_refs, request, timeout: GitalyClient.fast_timeout)
|
||||
|
|
|
|||
|
|
@ -72,6 +72,9 @@ msgstr ""
|
|||
msgid "\"%{repository_name}\" size (%{repository_size}) is larger than the limit of %{limit}."
|
||||
msgstr ""
|
||||
|
||||
msgid "##### ERROR ##### You have used %{usage_percentage} of the storage quota for %{namespace_name} (%{current_size} of %{size_limit}). %{namespace_name} is now read-only. Projects under this namespace are locked and actions will be restricted. To manage storage, or purchase additional storage, see %{manage_storage_url}. To learn more about restricted actions, see %{restricted_actions_url}"
|
||||
msgstr ""
|
||||
|
||||
msgid "#%{issueIid} (closed)"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -8056,6 +8059,9 @@ msgstr ""
|
|||
msgid "CascadingSettings|cannot be nil when locking the attribute"
|
||||
msgstr ""
|
||||
|
||||
msgid "Cause identified"
|
||||
msgstr ""
|
||||
|
||||
msgid "Certain user content will be moved to a system-wide \"Ghost User\" in order to maintain content for posterity. For further information, please refer to the %{link_start}user account deletion documentation.%{link_end}"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -19050,6 +19056,9 @@ msgstr ""
|
|||
msgid "GlobalSearch|%{count} default results provided. Use the up and down arrow keys to navigate search results list."
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Aggregations load error."
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Close"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -19119,6 +19128,12 @@ msgstr ""
|
|||
msgid "GlobalSearch|Settings"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Show more"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Showing top %{maxItems}"
|
||||
msgstr ""
|
||||
|
||||
msgid "GlobalSearch|Syntax options"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -21023,6 +21038,9 @@ msgstr ""
|
|||
msgid "Identity|Provider ID"
|
||||
msgstr ""
|
||||
|
||||
msgid "If %{namespace_name} exceeds the storage quota, all projects in the namespace will be locked and actions will be restricted. To manage storage, or purchase additional storage, see %{manage_storage_url}. To learn more about restricted actions, see %{restricted_actions_url}"
|
||||
msgstr ""
|
||||
|
||||
msgid "If any indexed field exceeds this limit, it is truncated to this number of characters. The rest of the content is neither indexed nor searchable. This does not apply to repository and wiki indexing. For unlimited characters, set this to 0."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -21155,6 +21173,12 @@ msgstr ""
|
|||
msgid "Images with incorrect dimensions are not resized automatically, and may result in unexpected behavior."
|
||||
msgstr ""
|
||||
|
||||
msgid "Impact detected"
|
||||
msgstr ""
|
||||
|
||||
msgid "Impact mitigated"
|
||||
msgstr ""
|
||||
|
||||
msgid "Impersonate"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -27491,7 +27515,7 @@ msgstr ""
|
|||
msgid "NamespaceStorageSize|You have consumed all of your additional storage, please purchase more to unlock your projects over the free %{free_size_limit} limit. You can't %{repository_limits_description}"
|
||||
msgstr ""
|
||||
|
||||
msgid "NamespaceStorageSize|You have reached the free storage limit of %{free_size_limit} on one or more projects."
|
||||
msgid "NamespaceStorageSize|You have reached the free storage limit of %{free_size_limit} on one or more projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "NamespaceStorageSize|You have used %{usage_in_percent} of the storage quota for %{namespace_name} (%{used_storage} of %{storage_limit})"
|
||||
|
|
@ -29050,6 +29074,9 @@ msgstr ""
|
|||
msgid "OnDemandScans|%{profileType} profile library"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|%{textStart}Tags specify which runners process this scan. Runners must have every tag selected.%{textEnd} %{linkStart}What are runner tags?%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Add a schedule to run this scan at a specified date and time or on a recurring basis. Scheduled scans are automatically saved to scan library."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -29089,7 +29116,7 @@ msgstr ""
|
|||
msgid "OnDemandScans|Delete profile"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Description (optional)"
|
||||
msgid "OnDemandScans|Description"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Discard changes"
|
||||
|
|
@ -29158,13 +29185,16 @@ msgstr ""
|
|||
msgid "OnDemandScans|On-demand scans run outside the DevOps cycle and find vulnerabilities in your projects. %{learnMoreLinkStart}Learn more%{learnMoreLinkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Only project owners and maintainers can select runner tags."
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Repeats"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Run scan"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Runner tags (optional)"
|
||||
msgid "OnDemandScans|Runner tags"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Save and run scan"
|
||||
|
|
@ -29227,9 +29257,6 @@ msgstr ""
|
|||
msgid "OnDemandScans|Unable to fetch runner tags. Try reloading the page."
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Use runner tags to select specific runners for this security scan. %{linkStart}What are runner tags?%{linkEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "OnDemandScans|Verify configuration"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -32684,6 +32711,9 @@ msgstr ""
|
|||
msgid "Profiles|You must transfer ownership or delete these groups before you can delete your account."
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Your Discord user ID. Should be between %{min} and %{max} digits long. %{external_accounts_link_start}Learn more.%{external_accounts_link_end}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Profiles|Your LinkedIn profile name from linkedin.com/in/profilename"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -34355,6 +34385,9 @@ msgstr ""
|
|||
msgid "ProtectedEnvironments|Set which groups, access levels or users that are allowed to deploy to this environment"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedEnvironments|Users"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedEnvironment|%{environment_name} will be writable for developers. Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -36085,6 +36118,9 @@ msgstr ""
|
|||
msgid "Response didn't include `service_desk_address`"
|
||||
msgstr ""
|
||||
|
||||
msgid "Response initiated"
|
||||
msgstr ""
|
||||
|
||||
msgid "Response metrics (AWS ELB)"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -50893,6 +50929,9 @@ msgstr ""
|
|||
msgid "must belong to same project of the work item."
|
||||
msgstr ""
|
||||
|
||||
msgid "must contain only a discord user ID."
|
||||
msgstr ""
|
||||
|
||||
msgid "must have a repository"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -126,6 +126,16 @@ RSpec.describe ProfilesController, :request_store do
|
|||
expect(user.reload.pronunciation).to eq(pronunciation)
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
end
|
||||
|
||||
it 'allows updating user specified Discord User ID', :aggregate_failures do
|
||||
discord_user_id = '1234567890123456789'
|
||||
sign_in(user)
|
||||
|
||||
put :update, params: { user: { discord: discord_user_id } }
|
||||
|
||||
expect(user.reload.discord).to eq(discord_user_id)
|
||||
expect(response).to have_gitlab_http_status(:found)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET audit_log' do
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ RSpec.describe Ci::FreezePeriodsFinder, feature_category: :release_orchestration
|
|||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it_behaves_like 'returns nothing'
|
||||
it_behaves_like 'returns freeze_periods ordered by created_at asc'
|
||||
end
|
||||
|
||||
context 'when user is not a project member' do
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import getGroupTransferLocationsResponse from 'test_fixtures/api/groups/transfer
|
|||
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { DEFAULT_PER_PAGE } from '~/api';
|
||||
import { updateGroup, getGroupTransferLocations } from '~/api/groups_api';
|
||||
import { updateGroup, getGroupTransferLocations, getGroupMembers } from '~/api/groups_api';
|
||||
|
||||
const mockApiVersion = 'v4';
|
||||
const mockUrlRoot = '/gitlab';
|
||||
|
|
@ -68,4 +68,30 @@ describe('GroupsApi', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getGroupMembers', () => {
|
||||
it('requests members of a group', async () => {
|
||||
const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}/members`;
|
||||
|
||||
const response = [{ id: 0, username: 'root' }];
|
||||
|
||||
mock.onGet(expectedUrl).replyOnce(200, response);
|
||||
|
||||
await expect(getGroupMembers(mockGroupId)).resolves.toMatchObject({
|
||||
data: response,
|
||||
});
|
||||
});
|
||||
|
||||
it('requests inherited members of a group when requested', async () => {
|
||||
const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}/members/all`;
|
||||
|
||||
const response = [{ id: 0, username: 'root' }];
|
||||
|
||||
mock.onGet(expectedUrl).replyOnce(200, response);
|
||||
|
||||
await expect(getGroupMembers(mockGroupId, true)).resolves.toMatchObject({
|
||||
data: response,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -125,4 +125,30 @@ describe('~/api/projects_api.js', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProjectMembers', () => {
|
||||
it('requests members of a project', async () => {
|
||||
const expectedUrl = `/api/v7/projects/1/members`;
|
||||
|
||||
const response = [{ id: 0, username: 'root' }];
|
||||
|
||||
mock.onGet(expectedUrl).replyOnce(200, response);
|
||||
|
||||
await expect(projectsApi.getProjectMembers(projectId)).resolves.toMatchObject({
|
||||
data: response,
|
||||
});
|
||||
});
|
||||
|
||||
it('requests inherited members of a project when requested', async () => {
|
||||
const expectedUrl = `/api/v7/projects/1/members/all`;
|
||||
|
||||
const response = [{ id: 0, username: 'root' }];
|
||||
|
||||
mock.onGet(expectedUrl).replyOnce(200, response);
|
||||
|
||||
await expect(projectsApi.getProjectMembers(projectId, true)).resolves.toMatchObject({
|
||||
data: response,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -141,6 +141,16 @@ describe('Commit box pipeline mini graph', () => {
|
|||
expect(upstreamPipeline).toEqual(null);
|
||||
});
|
||||
|
||||
it('should render the latest downstream pipeline only', async () => {
|
||||
createComponent(downstreamHandler);
|
||||
|
||||
await waitForPromises();
|
||||
|
||||
const downstreamPipelines = findPipelineMiniGraph().props('downstreamPipelines');
|
||||
|
||||
expect(downstreamPipelines).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should pass the pipeline path prop for the counter badge', async () => {
|
||||
createComponent(downstreamHandler);
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,48 @@ const downstream = {
|
|||
},
|
||||
__typename: 'Pipeline',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Pipeline/611',
|
||||
path: '/root/job-log-sections/-/pipelines/611',
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/21',
|
||||
name: 'job-log-sections',
|
||||
__typename: 'Project',
|
||||
},
|
||||
detailedStatus: {
|
||||
id: 'success-611-611',
|
||||
group: 'success',
|
||||
icon: 'status_success',
|
||||
label: 'passed',
|
||||
__typename: 'DetailedStatus',
|
||||
},
|
||||
sourceJob: {
|
||||
id: 'gid://gitlab/Ci::Bridge/531',
|
||||
retried: true,
|
||||
},
|
||||
__typename: 'Pipeline',
|
||||
},
|
||||
{
|
||||
id: 'gid://gitlab/Ci::Pipeline/609',
|
||||
path: '/root/job-log-sections/-/pipelines/609',
|
||||
project: {
|
||||
id: 'gid://gitlab/Project/21',
|
||||
name: 'job-log-sections',
|
||||
__typename: 'Project',
|
||||
},
|
||||
detailedStatus: {
|
||||
id: 'success-609-609',
|
||||
group: 'success',
|
||||
icon: 'status_success',
|
||||
label: 'passed',
|
||||
__typename: 'DetailedStatus',
|
||||
},
|
||||
sourceJob: {
|
||||
id: 'gid://gitlab/Ci::Bridge/530',
|
||||
retried: true,
|
||||
},
|
||||
__typename: 'Pipeline',
|
||||
},
|
||||
],
|
||||
__typename: 'PipelineConnection',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { GlIcon, GlLink } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { useFakeDate } from 'helpers/fake_date';
|
||||
import { IssuableStatus } from '~/issues/constants';
|
||||
import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
|
||||
import IssueCardTimeInfo from '~/issues/list/components/issue_card_time_info.vue';
|
||||
|
||||
describe('CE IssueCardTimeInfo component', () => {
|
||||
|
|
@ -25,7 +25,7 @@ describe('CE IssueCardTimeInfo component', () => {
|
|||
const findDueDate = () => wrapper.find('[data-testid="issuable-due-date"]');
|
||||
|
||||
const mountComponent = ({
|
||||
state = IssuableStatus.Open,
|
||||
state = STATUS_OPEN,
|
||||
dueDate = issue.dueDate,
|
||||
milestoneDueDate = issue.milestone.dueDate,
|
||||
milestoneStartDate = issue.milestone.startDate,
|
||||
|
|
@ -102,7 +102,7 @@ describe('CE IssueCardTimeInfo component', () => {
|
|||
it('does not render in red', () => {
|
||||
wrapper = mountComponent({
|
||||
dueDate: '2020-10-10',
|
||||
state: IssuableStatus.Closed,
|
||||
state: STATUS_CLOSED,
|
||||
});
|
||||
|
||||
expect(findDueDate().classes()).not.toContain('gl-text-red-500');
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
|
|||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { createAlert } from '~/flash';
|
||||
import { IssuableStatus, IssuableStatusText, IssuableType } from '~/issues/constants';
|
||||
import {
|
||||
IssuableStatusText,
|
||||
IssuableType,
|
||||
STATUS_CLOSED,
|
||||
STATUS_OPEN,
|
||||
STATUS_REOPENED,
|
||||
} from '~/issues/constants';
|
||||
import IssuableApp from '~/issues/show/components/app.vue';
|
||||
import DescriptionComponent from '~/issues/show/components/description.vue';
|
||||
import EditedComponent from '~/issues/show/components/edited.vue';
|
||||
|
|
@ -473,11 +479,11 @@ describe('Issuable output', () => {
|
|||
});
|
||||
|
||||
it.each`
|
||||
issuableType | issuableStatus | statusIcon
|
||||
${IssuableType.Issue} | ${IssuableStatus.Open} | ${'issues'}
|
||||
${IssuableType.Issue} | ${IssuableStatus.Closed} | ${'issue-closed'}
|
||||
${IssuableType.Epic} | ${IssuableStatus.Open} | ${'epic'}
|
||||
${IssuableType.Epic} | ${IssuableStatus.Closed} | ${'epic-closed'}
|
||||
issuableType | issuableStatus | statusIcon
|
||||
${IssuableType.Issue} | ${STATUS_OPEN} | ${'issues'}
|
||||
${IssuableType.Issue} | ${STATUS_CLOSED} | ${'issue-closed'}
|
||||
${IssuableType.Epic} | ${STATUS_OPEN} | ${'epic'}
|
||||
${IssuableType.Epic} | ${STATUS_CLOSED} | ${'epic-closed'}
|
||||
`(
|
||||
'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus',
|
||||
async ({ issuableType, issuableStatus, statusIcon }) => {
|
||||
|
|
@ -491,9 +497,9 @@ describe('Issuable output', () => {
|
|||
|
||||
it.each`
|
||||
title | state
|
||||
${'shows with Open when status is opened'} | ${IssuableStatus.Open}
|
||||
${'shows with Closed when status is closed'} | ${IssuableStatus.Closed}
|
||||
${'shows with Open when status is reopened'} | ${IssuableStatus.Reopened}
|
||||
${'shows with Open when status is opened'} | ${STATUS_OPEN}
|
||||
${'shows with Closed when status is closed'} | ${STATUS_CLOSED}
|
||||
${'shows with Open when status is reopened'} | ${STATUS_REOPENED}
|
||||
`('$title', async ({ state }) => {
|
||||
wrapper.setProps({ issuableStatus: state });
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { shallowMount } from '@vue/test-utils';
|
|||
import Vuex from 'vuex';
|
||||
import { mockTracking } from 'helpers/tracking_helper';
|
||||
import { createAlert, VARIANT_SUCCESS } from '~/flash';
|
||||
import { IssuableStatus, IssueType } from '~/issues/constants';
|
||||
import { IssueType, STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
|
||||
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
|
||||
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
|
||||
import HeaderActions from '~/issues/show/components/header_actions.vue';
|
||||
|
|
@ -81,7 +81,7 @@ describe('HeaderActions component', () => {
|
|||
|
||||
const mountComponent = ({
|
||||
props = {},
|
||||
issueState = IssuableStatus.Open,
|
||||
issueState = STATUS_OPEN,
|
||||
blockedByIssues = [],
|
||||
mutateResponse = {},
|
||||
} = {}) => {
|
||||
|
|
@ -123,9 +123,9 @@ describe('HeaderActions component', () => {
|
|||
`('when issue type is $issueType', ({ issueType }) => {
|
||||
describe('close/reopen button', () => {
|
||||
describe.each`
|
||||
description | issueState | buttonText | newIssueState
|
||||
${`when the ${issueType} is open`} | ${IssuableStatus.Open} | ${`Close ${issueType}`} | ${ISSUE_STATE_EVENT_CLOSE}
|
||||
${`when the ${issueType} is closed`} | ${IssuableStatus.Closed} | ${`Reopen ${issueType}`} | ${ISSUE_STATE_EVENT_REOPEN}
|
||||
description | issueState | buttonText | newIssueState
|
||||
${`when the ${issueType} is open`} | ${STATUS_OPEN} | ${`Close ${issueType}`} | ${ISSUE_STATE_EVENT_CLOSE}
|
||||
${`when the ${issueType} is closed`} | ${STATUS_CLOSED} | ${`Reopen ${issueType}`} | ${ISSUE_STATE_EVENT_REOPEN}
|
||||
`('$description', ({ issueState, buttonText, newIssueState }) => {
|
||||
beforeEach(() => {
|
||||
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
|
||||
|
|
|
|||
|
|
@ -149,7 +149,7 @@ describe('Timeline events form', () => {
|
|||
it('should show the number of selected tags, when more than one is selected', async () => {
|
||||
await selectTags(mockTags);
|
||||
|
||||
expect(findTagsListbox().props('toggleText')).toBe('2 tags');
|
||||
expect(findTagsListbox().props('toggleText')).toBe(`${mockTags.length} tags`);
|
||||
});
|
||||
|
||||
it('should be cleared when clear is triggered', async () => {
|
||||
|
|
|
|||
|
|
@ -622,3 +622,34 @@ export const MOCK_RECEIVE_AGGREGATIONS_ERROR_MUTATION = [
|
|||
type: types.RECEIVE_AGGREGATIONS_ERROR,
|
||||
},
|
||||
];
|
||||
|
||||
export const TEST_RAW_BUCKETS = [
|
||||
{ key: 'Go', count: 350 },
|
||||
{ key: 'C', count: 298 },
|
||||
{ key: 'JavaScript', count: 128 },
|
||||
{ key: 'YAML', count: 58 },
|
||||
{ key: 'Text', count: 46 },
|
||||
{ key: 'Markdown', count: 37 },
|
||||
{ key: 'HTML', count: 34 },
|
||||
{ key: 'Shell', count: 34 },
|
||||
{ key: 'Makefile', count: 21 },
|
||||
{ key: 'JSON', count: 15 },
|
||||
];
|
||||
|
||||
export const TEST_FILTER_DATA = {
|
||||
header: 'Language',
|
||||
scopes: { BLOBS: 'blobs' },
|
||||
filterParam: 'language',
|
||||
filters: {
|
||||
GO: { label: 'Go', value: 'Go', count: 350 },
|
||||
C: { label: 'C', value: 'C', count: 298 },
|
||||
JAVASCRIPT: { label: 'JavaScript', value: 'JavaScript', count: 128 },
|
||||
YAML: { label: 'YAML', value: 'YAML', count: 58 },
|
||||
TEXT: { label: 'Text', value: 'Text', count: 46 },
|
||||
MARKDOWN: { label: 'Markdown', value: 'Markdown', count: 37 },
|
||||
HTML: { label: 'HTML', value: 'HTML', count: 34 },
|
||||
SHELL: { label: 'Shell', value: 'Shell', count: 34 },
|
||||
MAKEFILE: { label: 'Makefile', value: 'Makefile', count: 21 },
|
||||
JSON: { label: 'JSON', value: 'JSON', count: 15 },
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { MOCK_QUERY } from 'jest/search/mock_data';
|
|||
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
|
||||
import ResultsFilters from '~/search/sidebar/components/results_filters.vue';
|
||||
import ScopeNavigation from '~/search/sidebar/components/scope_navigation.vue';
|
||||
import LanguageFilter from '~/search/sidebar/components/language_filter.vue';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
|
|
@ -35,53 +36,41 @@ describe('GlobalSearchSidebar', () => {
|
|||
});
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
wrapper.destroy();
|
||||
});
|
||||
|
||||
const findSidebarSection = () => wrapper.find('section');
|
||||
const findFilters = () => wrapper.findComponent(ResultsFilters);
|
||||
const findSidebarNavigation = () => wrapper.findComponent(ScopeNavigation);
|
||||
const findLanguageAggregation = () => wrapper.findComponent(LanguageFilter);
|
||||
|
||||
describe('renders properly', () => {
|
||||
describe('scope=projects', () => {
|
||||
describe('always', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ urlQuery: { ...MOCK_QUERY, scope: 'projects' } });
|
||||
createComponent({});
|
||||
});
|
||||
|
||||
it('shows section', () => {
|
||||
it(`shows section`, () => {
|
||||
expect(findSidebarSection().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("doesn't shows filters", () => {
|
||||
expect(findFilters().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scope=merge_requests', () => {
|
||||
describe.each`
|
||||
scope | showFilters | ShowsLanguage
|
||||
${'issues'} | ${true} | ${false}
|
||||
${'merge_requests'} | ${true} | ${false}
|
||||
${'projects'} | ${false} | ${false}
|
||||
${'blobs'} | ${false} | ${true}
|
||||
`('sidebar scope: $scope', ({ scope, showFilters, ShowsLanguage }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ urlQuery: { ...MOCK_QUERY, scope: 'merge_requests' } });
|
||||
createComponent(
|
||||
{ urlQuery: { scope } },
|
||||
{ searchBlobsLanguageAggregation: true, searchPageVerticalNav: true },
|
||||
);
|
||||
});
|
||||
|
||||
it('shows section', () => {
|
||||
expect(findSidebarSection().exists()).toBe(true);
|
||||
it(`${!showFilters ? "doesn't" : ''} shows filters`, () => {
|
||||
expect(findFilters().exists()).toBe(showFilters);
|
||||
});
|
||||
|
||||
it('shows filters', () => {
|
||||
expect(findFilters().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('scope=issues', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ urlQuery: MOCK_QUERY });
|
||||
});
|
||||
it('shows section', () => {
|
||||
expect(findSidebarSection().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows filters', () => {
|
||||
expect(findFilters().exists()).toBe(true);
|
||||
it(`${!ShowsLanguage ? "doesn't" : ''} shows language filters`, () => {
|
||||
expect(findLanguageAggregation().exists()).toBe(ShowsLanguage);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -94,4 +83,22 @@ describe('GlobalSearchSidebar', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when search_blobs_language_aggregation is enabled', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ urlQuery: { scope: 'blobs' } }, { searchBlobsLanguageAggregation: true });
|
||||
});
|
||||
it('shows the language filter', () => {
|
||||
expect(findLanguageAggregation().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when search_blobs_language_aggregation is disabled', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ urlQuery: { scope: 'blobs' } }, { searchBlobsLanguageAggregation: false });
|
||||
});
|
||||
it('hides the language filter', () => {
|
||||
expect(findLanguageAggregation().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import { GlFormCheckboxGroup, GlFormCheckbox } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { MOCK_QUERY, MOCK_LANGUAGE_AGGREGATIONS_BUCKETS } from 'jest/search/mock_data';
|
||||
import CheckboxFilter from '~/search/sidebar/components/checkbox_filter.vue';
|
||||
|
||||
import { languageFilterData } from '~/search/sidebar/constants/language_filter_data';
|
||||
import { convertFiltersData } from '~/search/sidebar/utils';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
describe('CheckboxFilter', () => {
|
||||
let wrapper;
|
||||
|
||||
const actionSpies = {
|
||||
setQuery: jest.fn(),
|
||||
};
|
||||
|
||||
const defaultProps = {
|
||||
filterData: convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
|
||||
};
|
||||
|
||||
const createComponent = () => {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
query: MOCK_QUERY,
|
||||
},
|
||||
actions: actionSpies,
|
||||
});
|
||||
|
||||
wrapper = shallowMountExtended(CheckboxFilter, {
|
||||
store,
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
const findFormCheckboxGroup = () => wrapper.findComponent(GlFormCheckboxGroup);
|
||||
const findAllCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
|
||||
const fintAllCheckboxLabels = () => wrapper.findAllByTestId('label');
|
||||
const fintAllCheckboxLabelCounts = () => wrapper.findAllByTestId('labelCount');
|
||||
|
||||
describe('Renders correctly', () => {
|
||||
it('renders form', () => {
|
||||
expect(findFormCheckboxGroup().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders checkbox-filter', () => {
|
||||
expect(findAllCheckboxes().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders all checkbox-filter checkboxes', () => {
|
||||
expect(findAllCheckboxes()).toHaveLength(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS.length);
|
||||
});
|
||||
|
||||
it('renders correctly label for the element', () => {
|
||||
expect(fintAllCheckboxLabels().at(0).text()).toBe(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS[0].key);
|
||||
});
|
||||
|
||||
it('renders correctly count for the element', () => {
|
||||
expect(fintAllCheckboxLabelCounts().at(0).text()).toBe(
|
||||
MOCK_LANGUAGE_AGGREGATIONS_BUCKETS[0].count.toString(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
it('triggers setQuery', () => {
|
||||
const filter =
|
||||
defaultProps.filterData.filters[Object.keys(defaultProps.filterData.filters)[0]].value;
|
||||
findFormCheckboxGroup().vm.$emit('input', filter);
|
||||
|
||||
expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
|
||||
key: languageFilterData.filterParam,
|
||||
value: filter,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
import { GlAlert, GlFormCheckbox, GlForm } from '@gitlab/ui';
|
||||
import Vue, { nextTick } from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import {
|
||||
MOCK_QUERY,
|
||||
MOCK_AGGREGATIONS,
|
||||
MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
|
||||
} from 'jest/search/mock_data';
|
||||
import LanguageFilter from '~/search/sidebar/components/language_filter.vue';
|
||||
import CheckboxFilter from '~/search/sidebar/components/checkbox_filter.vue';
|
||||
import { MAX_ITEM_LENGTH } from '~/search/sidebar/constants/language_filter_data';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
describe('GlobalSearchSidebarLanguageFilter', () => {
|
||||
let wrapper;
|
||||
|
||||
const actionSpies = {
|
||||
fetchLanguageAggregation: jest.fn(),
|
||||
applyQuery: jest.fn(),
|
||||
};
|
||||
|
||||
const getterSpies = {
|
||||
langugageAggregationBuckets: jest.fn(() => MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
|
||||
};
|
||||
|
||||
const createComponent = (initialState) => {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
query: MOCK_QUERY,
|
||||
urlQuery: MOCK_QUERY,
|
||||
aggregations: MOCK_AGGREGATIONS,
|
||||
...initialState,
|
||||
},
|
||||
actions: actionSpies,
|
||||
getters: getterSpies,
|
||||
});
|
||||
|
||||
wrapper = shallowMountExtended(LanguageFilter, {
|
||||
store,
|
||||
stubs: {
|
||||
CheckboxFilter,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const findForm = () => wrapper.findComponent(GlForm);
|
||||
const findCheckboxFilter = () => wrapper.findComponent(CheckboxFilter);
|
||||
const findApplyButton = () => wrapper.findByTestId('apply-button');
|
||||
const findShowMoreButton = () => wrapper.findByTestId('show-more-button');
|
||||
const findAlert = () => wrapper.findComponent(GlAlert);
|
||||
const findAllCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
|
||||
const findHasOverMax = () => wrapper.findByTestId('has-over-max-text');
|
||||
|
||||
describe('Renders correctly', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders form', () => {
|
||||
expect(findForm().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders checkbox-filter', () => {
|
||||
expect(findCheckboxFilter().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders all checkbox-filter checkboxes', () => {
|
||||
// 11th checkbox is hidden
|
||||
expect(findAllCheckboxes()).toHaveLength(10);
|
||||
});
|
||||
|
||||
it('renders ApplyButton', () => {
|
||||
expect(findApplyButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('renders Show More button', () => {
|
||||
expect(findShowMoreButton().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it("doesn't render Alert", () => {
|
||||
expect(findAlert().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ApplyButton', () => {
|
||||
describe('when sidebarDirty is false', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ sidebarDirty: false });
|
||||
});
|
||||
|
||||
it('disables the button', () => {
|
||||
expect(findApplyButton().attributes('disabled')).toBe('true');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when sidebarDirty is true', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ sidebarDirty: true });
|
||||
});
|
||||
|
||||
it('enables the button', () => {
|
||||
expect(findApplyButton().attributes('disabled')).toBe(undefined);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Show All button works', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it(`renders ${MAX_ITEM_LENGTH} amount of items`, async () => {
|
||||
findShowMoreButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
expect(findAllCheckboxes()).toHaveLength(MAX_ITEM_LENGTH);
|
||||
});
|
||||
|
||||
it(`renders more then ${MAX_ITEM_LENGTH} text`, async () => {
|
||||
findShowMoreButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
expect(findHasOverMax().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it(`doesn't render show more button after click`, async () => {
|
||||
findShowMoreButton().vm.$emit('click');
|
||||
await nextTick();
|
||||
expect(findShowMoreButton().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({});
|
||||
});
|
||||
|
||||
it('uses getter langugageAggregationBuckets', () => {
|
||||
expect(getterSpies.langugageAggregationBuckets).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses action fetchLanguageAggregation', () => {
|
||||
expect(actionSpies.fetchLanguageAggregation).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('clicking ApplyButton calls applyQuery', () => {
|
||||
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
||||
|
||||
expect(actionSpies.applyQuery).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import { convertFiltersData } from '~/search/sidebar/utils';
|
||||
import { TEST_RAW_BUCKETS, TEST_FILTER_DATA } from '../mock_data';
|
||||
|
||||
describe('Global Search sidebar utils', () => {
|
||||
describe('convertFiltersData', () => {
|
||||
it('converts raw buckets to array', () => {
|
||||
expect(convertFiltersData(TEST_RAW_BUCKETS)).toStrictEqual(TEST_FILTER_DATA);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -6,7 +6,7 @@ require 'haml_lint/spec'
|
|||
|
||||
require_relative '../../../haml_lint/linter/documentation_links'
|
||||
|
||||
RSpec.describe HamlLint::Linter::DocumentationLinks do
|
||||
RSpec.describe HamlLint::Linter::DocumentationLinks, feature_category: :tooling do
|
||||
include_context 'linter'
|
||||
|
||||
shared_examples 'link validation rules' do |link_pattern|
|
||||
|
|
@ -95,11 +95,8 @@ RSpec.describe HamlLint::Linter::DocumentationLinks do
|
|||
end
|
||||
end
|
||||
|
||||
context 'help_page_path' do
|
||||
it_behaves_like 'link validation rules', 'help_page_path'
|
||||
end
|
||||
|
||||
context 'help_page_url' do
|
||||
it_behaves_like 'link validation rules', 'help_page_url'
|
||||
end
|
||||
it_behaves_like 'link validation rules', 'help_page_path'
|
||||
it_behaves_like 'link validation rules', 'help_page_url'
|
||||
it_behaves_like 'link validation rules', 'Rails.application.routes.url_helpers.help_page_url'
|
||||
it_behaves_like 'link validation rules', 'Gitlab::Routing.url_helpers.help_page_url'
|
||||
end
|
||||
|
|
|
|||
|
|
@ -540,6 +540,23 @@ RSpec.describe ApplicationHelper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#profile_social_links' do
|
||||
context 'when discord is set' do
|
||||
let_it_be(:user) { build(:user) }
|
||||
let(:discord) { discord_url(user) }
|
||||
|
||||
it 'returns an empty string if discord is not set' do
|
||||
expect(discord).to eq('')
|
||||
end
|
||||
|
||||
it 'returns discord url when discord id is set' do
|
||||
user.discord = '1234567890123456789'
|
||||
|
||||
expect(discord).to eq('https://discord.com/users/1234567890123456789')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#gitlab_ui_form_for' do
|
||||
let_it_be(:user) { build(:user) }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,102 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::BackgroundMigration::SanitizeConfidentialTodos, :migration, feature_category: :team_planning do
|
||||
let!(:issue_type_id) { table(:work_item_types).find_by(base_type: 0).id }
|
||||
|
||||
let(:todos) { table(:todos) }
|
||||
let(:notes) { table(:notes) }
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:projects) { table(:projects) }
|
||||
let(:project_features) { table(:project_features) }
|
||||
let(:users) { table(:users) }
|
||||
let(:issues) { table(:issues) }
|
||||
let(:members) { table(:members) }
|
||||
let(:project_authorizations) { table(:project_authorizations) }
|
||||
|
||||
let(:user) { users.create!(first_name: 'Test', last_name: 'User', email: 'test@user.com', projects_limit: 1) }
|
||||
let(:project_namespace1) { namespaces.create!(path: 'pns1', name: 'pns1') }
|
||||
let(:project_namespace2) { namespaces.create!(path: 'pns2', name: 'pns2') }
|
||||
|
||||
let(:project1) do
|
||||
projects.create!(namespace_id: project_namespace1.id,
|
||||
project_namespace_id: project_namespace1.id, visibility_level: 20)
|
||||
end
|
||||
|
||||
let(:project2) do
|
||||
projects.create!(namespace_id: project_namespace2.id,
|
||||
project_namespace_id: project_namespace2.id)
|
||||
end
|
||||
|
||||
let(:issue1) do
|
||||
issues.create!(
|
||||
project_id: project1.id, namespace_id: project_namespace1.id, issue_type: 1, title: 'issue1', author_id: user.id,
|
||||
work_item_type_id: issue_type_id
|
||||
)
|
||||
end
|
||||
|
||||
let(:issue2) do
|
||||
issues.create!(
|
||||
project_id: project2.id, namespace_id: project_namespace2.id, issue_type: 1, title: 'issue2',
|
||||
work_item_type_id: issue_type_id
|
||||
)
|
||||
end
|
||||
|
||||
let(:public_note) { notes.create!(note: 'text', project_id: project1.id) }
|
||||
|
||||
let(:confidential_note) do
|
||||
notes.create!(note: 'text', project_id: project1.id, confidential: true,
|
||||
noteable_id: issue1.id, noteable_type: 'Issue')
|
||||
end
|
||||
|
||||
let(:other_confidential_note) do
|
||||
notes.create!(note: 'text', project_id: project2.id, confidential: true,
|
||||
noteable_id: issue2.id, noteable_type: 'Issue')
|
||||
end
|
||||
|
||||
let(:common_params) { { user_id: user.id, author_id: user.id, action: 1, state: 'pending', target_type: 'Note' } }
|
||||
let!(:ignored_todo1) { todos.create!(**common_params) }
|
||||
let!(:ignored_todo2) { todos.create!(**common_params, target_id: public_note.id, note_id: public_note.id) }
|
||||
let!(:valid_todo) { todos.create!(**common_params, target_id: confidential_note.id, note_id: confidential_note.id) }
|
||||
let!(:invalid_todo) do
|
||||
todos.create!(**common_params, target_id: other_confidential_note.id, note_id: other_confidential_note.id)
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
before do
|
||||
project_features.create!(project_id: project1.id, issues_access_level: 20, pages_access_level: 20)
|
||||
members.create!(state: 0, source_id: project1.id, source_type: 'Project',
|
||||
type: 'ProjectMember', user_id: user.id, access_level: 50, notification_level: 0,
|
||||
member_namespace_id: project_namespace1.id)
|
||||
project_authorizations.create!(project_id: project1.id, user_id: user.id, access_level: 50)
|
||||
end
|
||||
|
||||
subject(:perform) do
|
||||
described_class.new(
|
||||
start_id: notes.minimum(:id),
|
||||
end_id: notes.maximum(:id),
|
||||
batch_table: :notes,
|
||||
batch_column: :id,
|
||||
sub_batch_size: 1,
|
||||
pause_ms: 0,
|
||||
connection: ApplicationRecord.connection
|
||||
).perform
|
||||
end
|
||||
|
||||
it 'deletes todos where user can not read its note and logs deletion', :aggregate_failures do
|
||||
expect_next_instance_of(Gitlab::BackgroundMigration::Logger) do |logger|
|
||||
expect(logger).to receive(:info).with(
|
||||
hash_including(
|
||||
message: "#{described_class.name} deleting invalid todo",
|
||||
attributes: hash_including(invalid_todo.attributes.slice(:id, :user_id, :target_id, :target_type))
|
||||
)
|
||||
).once
|
||||
end
|
||||
|
||||
expect { perform }.to change(todos, :count).by(-1)
|
||||
|
||||
expect(todos.all).to match_array([ignored_todo1, ignored_todo2, valid_todo])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1947,6 +1947,53 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen
|
|||
expect(reference.name).to be_a(String)
|
||||
expect(reference.target).to be_a(String)
|
||||
end
|
||||
|
||||
it 'filters by pattern' do
|
||||
refs = repository.list_refs([Gitlab::Git::TAG_REF_PREFIX])
|
||||
|
||||
refs.each do |reference|
|
||||
expect(reference.name).to include(Gitlab::Git::TAG_REF_PREFIX)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with pointing_at_oids and peel_tags options' do
|
||||
let(:commit_id) { mutable_repository.commit.id }
|
||||
let!(:annotated_tag) { mutable_repository.add_tag('annotated-tag', user: user, target: commit_id, message: 'Tag message') }
|
||||
let!(:lw_tag) { mutable_repository.add_tag('lw-tag', user: user, target: commit_id) }
|
||||
|
||||
it 'filters by target OIDs' do
|
||||
refs = mutable_repository.list_refs([Gitlab::Git::TAG_REF_PREFIX], pointing_at_oids: [commit_id])
|
||||
|
||||
expect(refs.length).to eq(2)
|
||||
expect(refs).to contain_exactly(
|
||||
Gitaly::ListRefsResponse::Reference.new(
|
||||
name: "#{Gitlab::Git::TAG_REF_PREFIX}#{lw_tag.name}",
|
||||
target: commit_id
|
||||
),
|
||||
Gitaly::ListRefsResponse::Reference.new(
|
||||
name: "#{Gitlab::Git::TAG_REF_PREFIX}#{annotated_tag.name}",
|
||||
target: annotated_tag.id
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns peeled_target for annotated tags' do
|
||||
refs = mutable_repository.list_refs([Gitlab::Git::TAG_REF_PREFIX], pointing_at_oids: [commit_id], peel_tags: true)
|
||||
|
||||
expect(refs.length).to eq(2)
|
||||
expect(refs).to contain_exactly(
|
||||
Gitaly::ListRefsResponse::Reference.new(
|
||||
name: "#{Gitlab::Git::TAG_REF_PREFIX}#{lw_tag.name}",
|
||||
target: commit_id
|
||||
),
|
||||
Gitaly::ListRefsResponse::Reference.new(
|
||||
name: "#{Gitlab::Git::TAG_REF_PREFIX}#{annotated_tag.name}",
|
||||
target: annotated_tag.id,
|
||||
peeled_target: commit_id
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#refs_by_oid' do
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::GitalyClient::RefService do
|
||||
RSpec.describe Gitlab::GitalyClient::RefService, feature_category: :gitaly do
|
||||
let_it_be(:project) { create(:project, :repository, create_tag: 'test') }
|
||||
|
||||
let(:storage_name) { project.repository_storage }
|
||||
|
|
@ -390,10 +390,15 @@ RSpec.describe Gitlab::GitalyClient::RefService do
|
|||
end
|
||||
|
||||
describe '#list_refs' do
|
||||
let(:oid) { project.repository.commit.id }
|
||||
|
||||
it 'sends a list_refs message' do
|
||||
expect_any_instance_of(Gitaly::RefService::Stub)
|
||||
.to receive(:list_refs)
|
||||
.with(gitaly_request_with_params(patterns: ['refs/heads/']), kind_of(Hash))
|
||||
.with(
|
||||
gitaly_request_with_params(patterns: ['refs/heads/'], pointing_at_oids: [], peel_tags: false),
|
||||
kind_of(Hash)
|
||||
)
|
||||
.and_call_original
|
||||
|
||||
client.list_refs
|
||||
|
|
@ -407,6 +412,24 @@ RSpec.describe Gitlab::GitalyClient::RefService do
|
|||
|
||||
client.list_refs([Gitlab::Git::TAG_REF_PREFIX])
|
||||
end
|
||||
|
||||
it 'accepts a pointing_at_oids argument' do
|
||||
expect_any_instance_of(Gitaly::RefService::Stub)
|
||||
.to receive(:list_refs)
|
||||
.with(gitaly_request_with_params(pointing_at_oids: [oid]), kind_of(Hash))
|
||||
.and_call_original
|
||||
|
||||
client.list_refs(pointing_at_oids: [oid])
|
||||
end
|
||||
|
||||
it 'accepts a peel_tags argument' do
|
||||
expect_any_instance_of(Gitaly::RefService::Stub)
|
||||
.to receive(:list_refs)
|
||||
.with(gitaly_request_with_params(peel_tags: true), kind_of(Hash))
|
||||
.and_call_original
|
||||
|
||||
client.list_refs(peel_tags: true)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_refs_by_oid' do
|
||||
|
|
|
|||
|
|
@ -1,33 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
require_migration!
|
||||
|
||||
RSpec.describe SanitizeConfidentialNoteTodos, feature_category: :team_planning do
|
||||
let(:migration) { described_class::MIGRATION }
|
||||
|
||||
describe '#up' do
|
||||
it 'schedules a batched background migration' do
|
||||
migrate!
|
||||
|
||||
expect(migration).to have_scheduled_batched_migration(
|
||||
table_name: :notes,
|
||||
column_name: :id,
|
||||
interval: described_class::DELAY_INTERVAL,
|
||||
batch_size: described_class::BATCH_SIZE,
|
||||
max_batch_size: described_class::MAX_BATCH_SIZE,
|
||||
sub_batch_size: described_class::SUB_BATCH_SIZE
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#down' do
|
||||
it 'deletes all batched migration records' do
|
||||
migrate!
|
||||
schema_migrate_down!
|
||||
|
||||
expect(migration).not_to have_scheduled_batched_migration
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -38,6 +38,27 @@ RSpec.describe UserDetail do
|
|||
it { is_expected.to validate_length_of(:skype).is_at_most(500) }
|
||||
end
|
||||
|
||||
describe '#discord' do
|
||||
it { is_expected.to validate_length_of(:discord).is_at_most(500) }
|
||||
|
||||
context 'when discord is set' do
|
||||
let_it_be(:user_detail) { create(:user_detail) }
|
||||
|
||||
it 'accepts a valid discord user id' do
|
||||
user_detail.discord = '1234567890123456789'
|
||||
|
||||
expect(user_detail).to be_valid
|
||||
end
|
||||
|
||||
it 'throws an error when other url format is wrong' do
|
||||
user_detail.discord = '123456789'
|
||||
|
||||
expect(user_detail).not_to be_valid
|
||||
expect(user_detail.errors.full_messages).to match_array([_('Discord must contain only a discord user ID.')])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#location' do
|
||||
it { is_expected.to validate_length_of(:location).is_at_most(500) }
|
||||
end
|
||||
|
|
@ -72,11 +93,12 @@ RSpec.describe UserDetail do
|
|||
let(:user_detail) do
|
||||
create(:user_detail,
|
||||
bio: 'bio',
|
||||
discord: '1234567890123456789',
|
||||
linkedin: 'linkedin',
|
||||
twitter: 'twitter',
|
||||
skype: 'skype',
|
||||
location: 'location',
|
||||
organization: 'organization',
|
||||
skype: 'skype',
|
||||
twitter: 'twitter',
|
||||
website_url: 'https://example.com')
|
||||
end
|
||||
|
||||
|
|
@ -90,11 +112,12 @@ RSpec.describe UserDetail do
|
|||
end
|
||||
|
||||
it_behaves_like 'prevents `nil` value', :bio
|
||||
it_behaves_like 'prevents `nil` value', :discord
|
||||
it_behaves_like 'prevents `nil` value', :linkedin
|
||||
it_behaves_like 'prevents `nil` value', :twitter
|
||||
it_behaves_like 'prevents `nil` value', :skype
|
||||
it_behaves_like 'prevents `nil` value', :location
|
||||
it_behaves_like 'prevents `nil` value', :organization
|
||||
it_behaves_like 'prevents `nil` value', :skype
|
||||
it_behaves_like 'prevents `nil` value', :twitter
|
||||
it_behaves_like 'prevents `nil` value', :website_url
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ RSpec.describe User, feature_category: :user_profile do
|
|||
it { is_expected.to delegate_method(:requires_credit_card_verification).to(:user_detail).allow_nil }
|
||||
it { is_expected.to delegate_method(:requires_credit_card_verification=).to(:user_detail).with_arguments(:args).allow_nil }
|
||||
|
||||
it { is_expected.to delegate_method(:discord).to(:user_detail).allow_nil }
|
||||
it { is_expected.to delegate_method(:discord=).to(:user_detail).with_arguments(:args).allow_nil }
|
||||
|
||||
it { is_expected.to delegate_method(:linkedin).to(:user_detail).allow_nil }
|
||||
it { is_expected.to delegate_method(:linkedin=).to(:user_detail).with_arguments(:args).allow_nil }
|
||||
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ RSpec.describe WikiDirectory do
|
|||
|
||||
describe '#to_partial_path' do
|
||||
it 'returns the relative path to the partial to be used' do
|
||||
expect(directory.to_partial_path).to eq('../shared/wikis/wiki_directory')
|
||||
expect(directory.to_partial_path).to eq('shared/wikis/wiki_directory')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -912,7 +912,7 @@ RSpec.describe WikiPage do
|
|||
|
||||
describe '#to_partial_path' do
|
||||
it 'returns the relative path to the partial to be used' do
|
||||
expect(build_wiki_page(container).to_partial_path).to eq('../shared/wikis/wiki_page')
|
||||
expect(build_wiki_page(container).to_partial_path).to eq('shared/wikis/wiki_page')
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
require 'rubocop_spec_helper'
|
||||
require_relative '../../../../rubocop/cop/migration/prevent_single_statement_with_disable_ddl_transaction'
|
||||
|
||||
RSpec.describe RuboCop::Cop::Migration::PreventSingleStatementWithDisableDdlTransaction do
|
||||
RSpec.describe RuboCop::Cop::Migration::PreventSingleStatementWithDisableDdlTransaction, feature_category: :database do
|
||||
context 'when in migration' do
|
||||
before do
|
||||
allow(cop).to receive(:in_migration?).and_return(true)
|
||||
|
|
|
|||
|
|
@ -52,8 +52,8 @@ RSpec.describe 'shared/wikis/_sidebar.html.haml' do
|
|||
before do
|
||||
assign(:sidebar_wiki_entries, create_list(:wiki_page, 3, wiki: wiki))
|
||||
assign(:sidebar_limited, true)
|
||||
stub_template "../shared/wikis/_wiki_pages.html.erb" => "Entries: <%= @sidebar_wiki_entries.size %>"
|
||||
stub_template "../shared/wikis/_wiki_page.html.erb" => 'A WIKI PAGE'
|
||||
stub_template "shared/wikis/_wiki_pages.html.erb" => "Entries: <%= @sidebar_wiki_entries.size %>"
|
||||
stub_template "shared/wikis/_wiki_page.html.erb" => 'A WIKI PAGE'
|
||||
end
|
||||
|
||||
it 'does not show an alert' do
|
||||
|
|
@ -66,7 +66,7 @@ RSpec.describe 'shared/wikis/_sidebar.html.haml' do
|
|||
it 'renders the wiki content' do
|
||||
render
|
||||
|
||||
expect(rendered).to include('A WIKI PAGE' * 3)
|
||||
expect(rendered).to include("A WIKI PAGE\n" * 3)
|
||||
expect(rendered).to have_link('View All Pages')
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue