summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-03 18:08:55 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-03 18:08:55 +0000
commit8f9307985ea047abb5b8a7c6c56bb644e0b7c363 (patch)
tree474c91c280b903c345bd94f4842abf481c535656
parent3cda3d43aef1e92e2eedf7383122c6db9c61149f (diff)
downloadgitlab-ce-8f9307985ea047abb5b8a7c6c56bb644e0b7c363.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/api/groups_api.js9
-rw-r--r--app/assets/javascripts/api/projects_api.js9
-rw-r--r--app/assets/javascripts/issuable/popover/components/issue_popover.vue4
-rw-r--r--app/assets/javascripts/issues/constants.js14
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue6
-rw-r--r--app/assets/javascripts/issues/list/components/issue_card_time_info.vue5
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue6
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue9
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue4
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/constants.js4
-rw-r--r--app/assets/javascripts/pipelines/components/parsing_utils.js4
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue4
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue14
-rw-r--r--app/assets/javascripts/search/sidebar/components/checkbox_filter.vue81
-rw-r--r--app/assets/javascripts/search/sidebar/components/language_filter.vue125
-rw-r--r--app/assets/javascripts/search/sidebar/utils.js20
-rw-r--r--app/assets/stylesheets/framework/variables.scss5
-rw-r--r--app/assets/stylesheets/page_bundles/profile.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/search.scss11
-rw-r--r--app/controllers/admin/users_controller.rb1
-rw-r--r--app/controllers/profiles_controller.rb1
-rw-r--r--app/controllers/search_controller.rb3
-rw-r--r--app/helpers/application_helper.rb6
-rw-r--r--app/models/user.rb1
-rw-r--r--app/models/user_detail.rb19
-rw-r--r--app/models/wiki_directory.rb2
-rw-r--r--app/models/wiki_page.rb2
-rw-r--r--app/policies/project_policy.rb2
-rw-r--r--app/presenters/projects/settings/deploy_keys_presenter.rb4
-rw-r--r--app/services/ci/register_job_service.rb2
-rw-r--r--app/views/profiles/show.html.haml9
-rw-r--r--app/views/projects/settings/repository/show.html.haml2
-rw-r--r--app/views/shared/wikis/_sidebar.html.haml3
-rw-r--r--app/views/shared/wikis/_wiki_directory.html.haml3
-rw-r--r--app/views/shared/wikis/pages.html.haml3
-rw-r--r--app/views/users/show.html.haml4
-rw-r--r--config/feature_flags/ops/ci_register_job_temporary_lock.yml (renamed from config/feature_flags/development/ci_register_job_temporary_lock.yml)4
-rw-r--r--db/migrate/20221128155738_add_discord_to_user_details.rb12
-rw-r--r--db/migrate/20221128165833_add_discord_field_limit_to_user_details.rb15
-rw-r--r--db/post_migrate/20221110045406_sanitize_confidential_note_todos.rb24
-rw-r--r--db/schema_migrations/202211281557381
-rw-r--r--db/schema_migrations/202211281658331
-rw-r--r--db/structure.sql2
-rw-r--r--doc/api/features.md2
-rw-r--r--doc/api/freeze_periods.md5
-rw-r--r--doc/api/packages/debian.md4
-rw-r--r--doc/api/packages/debian_group_distributions.md16
-rw-r--r--doc/api/packages/debian_project_distributions.md4
-rw-r--r--doc/api/projects.md2
-rw-r--r--doc/architecture/blueprints/runner_tokens/index.md26
-rw-r--r--doc/development/feature_flags/controls.md17
-rw-r--r--doc/development/feature_flags/index.md12
-rw-r--r--doc/operations/error_tracking.md8
-rw-r--r--doc/update/index.md4
-rw-r--r--doc/user/admin_area/settings/account_and_limit_settings.md3
-rw-r--r--doc/user/clusters/agent/work_with_agent.md1
-rw-r--r--doc/user/okrs.md4
-rw-r--r--doc/user/packages/debian_repository/index.md65
-rw-r--r--doc/user/profile/index.md3
-rw-r--r--doc/user/project/quick_actions.md2
-rw-r--r--doc/user/usage_quotas.md6
-rw-r--r--haml_lint/linter/documentation_links.rb7
-rw-r--r--lib/gitlab/background_migration/sanitize_confidential_todos.rb37
-rw-r--r--lib/gitlab/git/repository.rb5
-rw-r--r--lib/gitlab/gitaly_client/ref_service.rb7
-rw-r--r--locale/gitlab.pot51
-rw-r--r--spec/controllers/profiles_controller_spec.rb10
-rw-r--r--spec/finders/ci/freeze_periods_finder_spec.rb2
-rw-r--r--spec/frontend/api/groups_api_spec.js28
-rw-r--r--spec/frontend/api/projects_api_spec.js26
-rw-r--r--spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js10
-rw-r--r--spec/frontend/commit/mock_data.js42
-rw-r--r--spec/frontend/issues/list/components/issue_card_time_info_spec.js6
-rw-r--r--spec/frontend/issues/show/components/app_spec.js24
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js10
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js2
-rw-r--r--spec/frontend/search/mock_data.js31
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js69
-rw-r--r--spec/frontend/search/sidebar/components/checkbox_filter_spec.js85
-rw-r--r--spec/frontend/search/sidebar/components/language_filters_spec.js152
-rw-r--r--spec/frontend/search/sidebar/utils_spec.js10
-rw-r--r--spec/haml_lint/linter/documentation_links_spec.rb13
-rw-r--r--spec/helpers/application_helper_spec.rb17
-rw-r--r--spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb102
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb47
-rw-r--r--spec/lib/gitlab/gitaly_client/ref_service_spec.rb27
-rw-r--r--spec/migrations/sanitize_confidential_note_todos_spec.rb33
-rw-r--r--spec/models/user_detail_spec.rb31
-rw-r--r--spec/models/user_spec.rb3
-rw-r--r--spec/models/wiki_directory_spec.rb2
-rw-r--r--spec/models/wiki_page_spec.rb2
-rw-r--r--spec/rubocop/cop/migration/prevent_single_statement_with_disable_ddl_transaction_spec.rb2
-rw-r--r--spec/views/shared/wikis/_sidebar.html.haml_spec.rb6
97 files changed, 1189 insertions, 368 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 3dc57cea50f..65023816ad5 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-ac201ee33b2c7b2974945fb15ba9b1aec4794017
+57e11eb9431c93a6349ffc0222cf447705c0fada
diff --git a/Gemfile b/Gemfile
index 5ef62e60226..03461fbd72e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -512,7 +512,7 @@ gem 'ssh_data', '~> 1.3'
gem 'spamcheck', '~> 1.0.0'
# Gitaly GRPC protocol definitions
-gem 'gitaly', '~> 15.8.0-rc1'
+gem 'gitaly', '~> 15.9.0-rc1'
# KAS GRPC protocol definitions
gem 'kas-grpc', '~> 0.0.2'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index 1c299c12ae1..5f710e42d29 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -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"},
diff --git a/Gemfile.lock b/Gemfile.lock
index b65ff44c9f4..d51c3f226d5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -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)
diff --git a/app/assets/javascripts/api/groups_api.js b/app/assets/javascripts/api/groups_api.js
index e859160c2e7..1b216e6f721 100644
--- a/app/assets/javascripts/api/groups_api.js
+++ b/app/assets/javascripts/api/groups_api.js
@@ -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);
+};
diff --git a/app/assets/javascripts/api/projects_api.js b/app/assets/javascripts/api/projects_api.js
index fe2e2bde940..5c0d101ef5b 100644
--- a/app/assets/javascripts/api/projects_api.js
+++ b/app/assets/javascripts/api/projects_api.js
@@ -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);
+};
diff --git a/app/assets/javascripts/issuable/popover/components/issue_popover.vue b/app/assets/javascripts/issuable/popover/components/issue_popover.vue
index 945a3782642..55fb3958e82 100644
--- a/app/assets/javascripts/issuable/popover/components/issue_popover.vue
+++ b/app/assets/javascripts/issuable/popover/components/issue_popover.vue
@@ -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: {
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
index 4b9a42da178..7213a4edf89 100644
--- a/app/assets/javascripts/issues/constants.js
+++ b/app/assets/javascripts/issues/constants.js
@@ -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 = {
diff --git a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
index ace128e22db..a4a2feba716 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -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;
diff --git a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
index 1139861ae78..d11540ad3dd 100644
--- a/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
+++ b/app/assets/javascripts/issues/list/components/issue_card_time_info.vue
@@ -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() {
diff --git a/app/assets/javascripts/issues/list/components/issues_list_app.vue b/app/assets/javascripts/issues/list/components/issues_list_app.vue
index 87c12a4eee1..6c46013e4f9 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -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;
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index 45c4e0889c0..7a79202e007 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -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
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 66f9a253e24..aa2cbd65c0e 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -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 = {
diff --git a/app/assets/javascripts/issues/show/components/incidents/constants.js b/app/assets/javascripts/issues/show/components/incidents/constants.js
index eeebd6bb32b..c0aadf9c14e 100644
--- a/app/assets/javascripts/issues/show/components/incidents/constants.js
+++ b/app/assets/javascripts/issues/show/components/incidents/constants.js
@@ -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'),
});
diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js
index b64299b79ac..83ba984ce07 100644
--- a/app/assets/javascripts/pipelines/components/parsing_utils.js
+++ b/app/assets/javascripts/pipelines/components/parsing_utils.js
@@ -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;
});
};
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
index 0256eec6d56..dafc4bc5abf 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_mini_graph.vue
@@ -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 ?? '';
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 927ae6f6b81..2efc80fef75 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -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>
diff --git a/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue b/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue
new file mode 100644
index 00000000000..b580d58b21b
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/checkbox_filter.vue
@@ -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>
diff --git a/app/assets/javascripts/search/sidebar/components/language_filter.vue b/app/assets/javascripts/search/sidebar/components/language_filter.vue
new file mode 100644
index 00000000000..7a65f8c7956
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/language_filter.vue
@@ -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>
diff --git a/app/assets/javascripts/search/sidebar/utils.js b/app/assets/javascripts/search/sidebar/utils.js
new file mode 100644
index 00000000000..5c08ad2f959
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/utils.js
@@ -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: {} },
+ );
+};
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 539e92eeca4..f136a8c3a08 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -634,9 +634,10 @@ $status-icon-size: 22px;
/*
* Social Icons
*/
-$twitter: #1d9bf0;
-$skype: #0078d7;
+$discord: #5865f2;
$linkedin: #2867b2;
+$skype: #0078d7;
+$twitter: #1d9bf0;
/*
* Award emoji
diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss
index ac1e9fb024b..fc745433f1b 100644
--- a/app/assets/stylesheets/page_bundles/profile.scss
+++ b/app/assets/stylesheets/page_bundles/profile.scss
@@ -240,6 +240,10 @@
color: $twitter;
}
+.discord-icon {
+ color: $discord;
+}
+
.key-created-at {
line-height: 42px;
}
diff --git a/app/assets/stylesheets/page_bundles/search.scss b/app/assets/stylesheets/page_bundles/search.scss
index 6ffa14dd342..cde570cfb0f 100644
--- a/app/assets/stylesheets/page_bundles/search.scss
+++ b/app/assets/stylesheets/page_bundles/search.scss
@@ -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 {
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 4f379d8a75b..23ec80c1fa9 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -359,6 +359,7 @@ class Admin::UsersController < Admin::ApplicationController
:skype,
:theme_id,
:twitter,
+ :discord,
:username,
:website_url,
:note,
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 93b131369b8..45b274fc920 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -127,6 +127,7 @@ class ProfilesController < Profiles::ApplicationController
:commit_email,
:skype,
:twitter,
+ :discord,
:username,
:website_url,
:organization,
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 84182e57e2e..5bedf9512e7 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -31,6 +31,9 @@ class SearchController < ApplicationController
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
rescue_from ActiveRecord::QueryCanceled, with: :render_timeout
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index ef174584a4b..4afbe98226e 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -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
diff --git a/app/models/user.rb b/app/models/user.rb
index 535f1bd874a..6151ba54555 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -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
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index b6765cb0285..9d3df3d6400 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -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')
diff --git a/app/models/wiki_directory.rb b/app/models/wiki_directory.rb
index f5d00013622..76fe664f23d 100644
--- a/app/models/wiki_directory.rb
+++ b/app/models/wiki_directory.rb
@@ -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
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 24b0b94eeb7..b04aa196883 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -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
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index eb9d13cc9a3..4b9e999231e 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -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
diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb
index b760786aa4c..14aa9f26677 100644
--- a/app/presenters/projects/settings/deploy_keys_presenter.rb
+++ b/app/presenters/projects/settings/deploy_keys_presenter.rb
@@ -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
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index cd879e9bc07..ccc23283ec4 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -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}"
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 1bc1e0614de..5ffffb80d97 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -104,6 +104,15 @@
= 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")
.form-group.gl-form-group
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 953bfcf71ab..de171a25e8d 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -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'
diff --git a/app/views/shared/wikis/_sidebar.html.haml b/app/views/shared/wikis/_sidebar.html.haml
index cb6a67bd8d4..8b8c981da96 100644
--- a/app/views/shared/wikis/_sidebar.html.haml
+++ b/app/views/shared/wikis/_sidebar.html.haml
@@ -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
diff --git a/app/views/shared/wikis/_wiki_directory.html.haml b/app/views/shared/wikis/_wiki_directory.html.haml
index a29e6ba7a85..5c2233a4db2 100644
--- a/app/views/shared/wikis/_wiki_directory.html.haml
+++ b/app/views/shared/wikis/_wiki_directory.html.haml
@@ -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 }
diff --git a/app/views/shared/wikis/pages.html.haml b/app/views/shared/wikis/pages.html.haml
index e1252e91c10..f35649d031c 100644
--- a/app/views/shared/wikis/pages.html.haml
+++ b/app/views/shared/wikis/pages.html.haml
@@ -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'
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index ed76960d3d7..b9290972656 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -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?
diff --git a/config/feature_flags/development/ci_register_job_temporary_lock.yml b/config/feature_flags/ops/ci_register_job_temporary_lock.yml
index cca6a24ffd4..4b91bf97e9e 100644
--- a/config/feature_flags/development/ci_register_job_temporary_lock.yml
+++ b/config/feature_flags/ops/ci_register_job_temporary_lock.yml
@@ -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
diff --git a/db/migrate/20221128155738_add_discord_to_user_details.rb b/db/migrate/20221128155738_add_discord_to_user_details.rb
new file mode 100644
index 00000000000..4d59a53dcd7
--- /dev/null
+++ b/db/migrate/20221128155738_add_discord_to_user_details.rb
@@ -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
diff --git a/db/migrate/20221128165833_add_discord_field_limit_to_user_details.rb b/db/migrate/20221128165833_add_discord_field_limit_to_user_details.rb
new file mode 100644
index 00000000000..a63b2019b20
--- /dev/null
+++ b/db/migrate/20221128165833_add_discord_field_limit_to_user_details.rb
@@ -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
diff --git a/db/post_migrate/20221110045406_sanitize_confidential_note_todos.rb b/db/post_migrate/20221110045406_sanitize_confidential_note_todos.rb
index f98be3f036f..d409a4dfdd1 100644
--- a/db/post_migrate/20221110045406_sanitize_confidential_note_todos.rb
+++ b/db/post_migrate/20221110045406_sanitize_confidential_note_todos.rb
@@ -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
diff --git a/db/schema_migrations/20221128155738 b/db/schema_migrations/20221128155738
new file mode 100644
index 00000000000..5322aa1f75e
--- /dev/null
+++ b/db/schema_migrations/20221128155738
@@ -0,0 +1 @@
+39ca72ad461ff7b56ce6feed351ef46ee9f3584a8c3c9383ca75f44b61baa1a1 \ No newline at end of file
diff --git a/db/schema_migrations/20221128165833 b/db/schema_migrations/20221128165833
new file mode 100644
index 00000000000..e2aeaa26c32
--- /dev/null
+++ b/db/schema_migrations/20221128165833
@@ -0,0 +1 @@
+4f4846fe8e5f84ee566dfc8f9b8249e1ff1d77f8f6c2f0006d89a73a2e734b9d \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 4595145cbc7..aa0a4f486ca 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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)),
diff --git a/doc/api/features.md b/doc/api/features.md
index 819405bea77..c3db1e53f68 100644
--- a/doc/api/features.md
+++ b/doc/api/features.md
@@ -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. |
diff --git a/doc/api/freeze_periods.md b/doc/api/freeze_periods.md
index 9cf5ba5ed7f..ce7377e1e35 100644
--- a/doc/api/freeze_periods.md
+++ b/doc/api/freeze_periods.md
@@ -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
diff --git a/doc/api/packages/debian.md b/doc/api/packages/debian.md
index dd8679c3bdc..87b583de5e6 100644
--- a/doc/api/packages/debian.md
+++ b/doc/api/packages/debian.md
@@ -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.
diff --git a/doc/api/packages/debian_group_distributions.md b/doc/api/packages/debian_group_distributions.md
index a000373089e..23bc85bf0a0 100644
--- a/doc/api/packages/debian_group_distributions.md
+++ b/doc/api/packages/debian_group_distributions.md
@@ -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 |
diff --git a/doc/api/packages/debian_project_distributions.md b/doc/api/packages/debian_project_distributions.md
index b66a70f1e15..0a43546e2e1 100644
--- a/doc/api/packages/debian_project_distributions.md
+++ b/doc/api/packages/debian_project_distributions.md
@@ -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.
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 356492fb3ab..d53c515bfbe 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -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.
diff --git a/doc/architecture/blueprints/runner_tokens/index.md b/doc/architecture/blueprints/runner_tokens/index.md
index d3376e06ef7..f63e453bfae 100644
--- a/doc/architecture/blueprints/runner_tokens/index.md
+++ b/doc/architecture/blueprints/runner_tokens/index.md
@@ -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
diff --git a/doc/development/feature_flags/controls.md b/doc/development/feature_flags/controls.md
index f1eafc2a95a..3adf5248b8d 100644
--- a/doc/development/feature_flags/controls.md
+++ b/doc/development/feature_flags/controls.md
@@ -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
diff --git a/doc/development/feature_flags/index.md b/doc/development/feature_flags/index.md
index 8f5c19efd94..7370697b082 100644
--- a/doc/development/feature_flags/index.md
+++ b/doc/development/feature_flags/index.md
@@ -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:
diff --git a/doc/operations/error_tracking.md b/doc/operations/error_tracking.md
index 47badae7472..0be2f087c62 100644
--- a/doc/operations/error_tracking.md
+++ b/doc/operations/error_tracking.md
@@ -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
diff --git a/doc/update/index.md b/doc/update/index.md
index b09ba691081..022c5472ba4 100644
--- a/doc/update/index.md
+++ b/doc/update/index.md
@@ -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).
diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md
index 7f678344955..35a4c0aeea7 100644
--- a/doc/user/admin_area/settings/account_and_limit_settings.md
+++ b/doc/user/admin_area/settings/account_and_limit_settings.md
@@ -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.
diff --git a/doc/user/clusters/agent/work_with_agent.md b/doc/user/clusters/agent/work_with_agent.md
index 28a0d366ec5..0b0e1365604 100644
--- a/doc/user/clusters/agent/work_with_agent.md
+++ b/doc/user/clusters/agent/work_with_agent.md
@@ -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.
diff --git a/doc/user/okrs.md b/doc/user/okrs.md
index a4ba1f8f646..0d3be8474fe 100644
--- a/doc/user/okrs.md
+++ b/doc/user/okrs.md
@@ -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.
diff --git a/doc/user/packages/debian_repository/index.md b/doc/user/packages/debian_repository/index.md
index 08b069c3f69..7ec20e3d036 100644
--- a/doc/user/packages/debian_repository/index.md
+++ b/doc/user/packages/debian_repository/index.md
@@ -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)
+### 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
diff --git a/doc/user/profile/index.md b/doc/user/profile/index.md
index 65951ce2dda..a84d16e6d0c 100644
--- a/doc/user/profile/index.md
+++ b/doc/user/profile/index.md
@@ -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**.
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index d12a71c9ab3..194b501d1fb 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -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. |
diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md
index 18eac74cd96..89761a4819a 100644
--- a/doc/user/usage_quotas.md
+++ b/doc/user/usage_quotas.md
@@ -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.
diff --git a/haml_lint/linter/documentation_links.rb b/haml_lint/linter/documentation_links.rb
index 0cabae40c4b..9d80344dd20 100644
--- a/haml_lint/linter/documentation_links.rb
+++ b/haml_lint/linter/documentation_links.rb
@@ -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
diff --git a/lib/gitlab/background_migration/sanitize_confidential_todos.rb b/lib/gitlab/background_migration/sanitize_confidential_todos.rb
index 2df0b8a4d93..8215e92cbeb 100644
--- a/lib/gitlab/background_migration/sanitize_confidential_todos.rb
+++ b/lib/gitlab/background_migration/sanitize_confidential_todos.rb
@@ -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
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 2f9cfe3e764..ad49e9e7221 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -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
diff --git a/lib/gitlab/gitaly_client/ref_service.rb b/lib/gitlab/gitaly_client/ref_service.rb
index 74034c4e717..ac6491e8770 100644
--- a/lib/gitlab/gitaly_client/ref_service.rb
+++ b/lib/gitlab/gitaly_client/ref_service.rb
@@ -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)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6936591df9e..50d0286cd54 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 ""
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index aa92ff6be33..daf0f36c28b 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -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
diff --git a/spec/finders/ci/freeze_periods_finder_spec.rb b/spec/finders/ci/freeze_periods_finder_spec.rb
index 6c58028a221..0aa73e698ed 100644
--- a/spec/finders/ci/freeze_periods_finder_spec.rb
+++ b/spec/finders/ci/freeze_periods_finder_spec.rb
@@ -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
diff --git a/spec/frontend/api/groups_api_spec.js b/spec/frontend/api/groups_api_spec.js
index e064ee9fec4..abd627dab10 100644
--- a/spec/frontend/api/groups_api_spec.js
+++ b/spec/frontend/api/groups_api_spec.js
@@ -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,
+ });
+ });
+ });
});
diff --git a/spec/frontend/api/projects_api_spec.js b/spec/frontend/api/projects_api_spec.js
index 0a6392bdc5b..a6ae8b14ea9 100644
--- a/spec/frontend/api/projects_api_spec.js
+++ b/spec/frontend/api/projects_api_spec.js
@@ -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,
+ });
+ });
+ });
});
diff --git a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
index c73f5cf3043..debd10de118 100644
--- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
+++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
@@ -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);
diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js
index 0f168ca6b17..80f62d5bfd8 100644
--- a/spec/frontend/commit/mock_data.js
+++ b/spec/frontend/commit/mock_data.js
@@ -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',
};
diff --git a/spec/frontend/issues/list/components/issue_card_time_info_spec.js b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
index b0d3a63a8cf..ab4d023ee39 100644
--- a/spec/frontend/issues/list/components/issue_card_time_info_spec.js
+++ b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
@@ -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');
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 7c1d643dc0f..acb04e0af56 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -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 });
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 8a0fdcb50d0..43507f78a97 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -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');
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
index b2d7a61f548..e352f9708e4 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
@@ -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 () => {
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index 4ccfbe41836..fb9c0a93907 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -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 },
+ },
+};
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index 6aa40b7a2be..760c83a1dde 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -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);
+ });
+ });
});
diff --git a/spec/frontend/search/sidebar/components/checkbox_filter_spec.js b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
new file mode 100644
index 00000000000..82017754b23
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
@@ -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,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/components/language_filters_spec.js b/spec/frontend/search/sidebar/components/language_filters_spec.js
new file mode 100644
index 00000000000..e297d1c33b0
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/language_filters_spec.js
@@ -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();
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/utils_spec.js b/spec/frontend/search/sidebar/utils_spec.js
new file mode 100644
index 00000000000..652d32c836e
--- /dev/null
+++ b/spec/frontend/search/sidebar/utils_spec.js
@@ -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);
+ });
+ });
+});
diff --git a/spec/haml_lint/linter/documentation_links_spec.rb b/spec/haml_lint/linter/documentation_links_spec.rb
index 380df49cde3..d47127d9661 100644
--- a/spec/haml_lint/linter/documentation_links_spec.rb
+++ b/spec/haml_lint/linter/documentation_links_spec.rb
@@ -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
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index f628651b4da..4e921fe1719 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -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) }
diff --git a/spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb b/spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb
deleted file mode 100644
index a19a3760958..00000000000
--- a/spec/lib/gitlab/background_migration/sanitize_confidential_todos_spec.rb
+++ /dev/null
@@ -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
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 6cff39c1167..d6d0de63a3b 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
index 14d5cef103b..09d8ea3cc0a 100644
--- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb
@@ -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
diff --git a/spec/migrations/sanitize_confidential_note_todos_spec.rb b/spec/migrations/sanitize_confidential_note_todos_spec.rb
deleted file mode 100644
index 142378e07e1..00000000000
--- a/spec/migrations/sanitize_confidential_note_todos_spec.rb
+++ /dev/null
@@ -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
diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb
index 1893b6530a5..7d433896cf8 100644
--- a/spec/models/user_detail_spec.rb
+++ b/spec/models/user_detail_spec.rb
@@ -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
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 5570eb36c81..7b5b8acdb66 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -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 }
diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb
index 90ff42998ae..1b177934ace 100644
--- a/spec/models/wiki_directory_spec.rb
+++ b/spec/models/wiki_directory_spec.rb
@@ -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
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index fcb041aebe5..21da06a222f 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -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
diff --git a/spec/rubocop/cop/migration/prevent_single_statement_with_disable_ddl_transaction_spec.rb b/spec/rubocop/cop/migration/prevent_single_statement_with_disable_ddl_transaction_spec.rb
index 3f5845f8f58..968de7d2160 100644
--- a/spec/rubocop/cop/migration/prevent_single_statement_with_disable_ddl_transaction_spec.rb
+++ b/spec/rubocop/cop/migration/prevent_single_statement_with_disable_ddl_transaction_spec.rb
@@ -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)
diff --git a/spec/views/shared/wikis/_sidebar.html.haml_spec.rb b/spec/views/shared/wikis/_sidebar.html.haml_spec.rb
index 0e7b657a154..821112e12bc 100644
--- a/spec/views/shared/wikis/_sidebar.html.haml_spec.rb
+++ b/spec/views/shared/wikis/_sidebar.html.haml_spec.rb
@@ -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