Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-02-03 18:08:55 +00:00
parent 3cda3d43ae
commit 8f9307985e
97 changed files with 1190 additions and 369 deletions

View File

@ -1 +1 @@
ac201ee33b2c7b2974945fb15ba9b1aec4794017
57e11eb9431c93a6349ffc0222cf447705c0fada

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -634,9 +634,10 @@ $status-icon-size: 22px;
/*
* Social Icons
*/
$twitter: #1d9bf0;
$skype: #0078d7;
$discord: #5865f2;
$linkedin: #2867b2;
$skype: #0078d7;
$twitter: #1d9bf0;
/*
* Award emoji

View File

@ -240,6 +240,10 @@
color: $twitter;
}
.discord-icon {
color: $discord;
}
.key-created-at {
line-height: 42px;
}

View File

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

View File

@ -359,6 +359,7 @@ class Admin::UsersController < Admin::ApplicationController
:skype,
:theme_id,
:twitter,
:discord,
:username,
:website_url,
:note,

View File

@ -127,6 +127,7 @@ class ProfilesController < Profiles::ApplicationController
:commit_email,
:skype,
:twitter,
:discord,
:username,
:website_url,
:organization,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1 @@
39ca72ad461ff7b56ce6feed351ef46ee9f3584a8c3c9383ca75f44b61baa1a1

View File

@ -0,0 +1 @@
4f4846fe8e5f84ee566dfc8f9b8249e1ff1d77f8f6c2f0006d89a73a2e734b9d

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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