Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-11-15 00:10:12 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-11-15 00:10:12 +0300
commit7f35b02e86cd3d2e8b4a81c5c3a8483ff6973c5a (patch)
tree958f0603e4d8d0bcd5a5fa2e0a006b6f1b280068
parent3244feeb4f1980251fd9ff6cc263e34072fbf7c7 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo/api/ensure_string_detail.yml5
-rw-r--r--.rubocop_todo/layout/line_length.yml2
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue3
-rw-r--r--app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql2
-rw-r--r--app/assets/javascripts/sidebar/mount_milestone_sidebar.js2
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js294
-rw-r--r--app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml4
-rw-r--r--app/views/projects/merge_requests/show.html.haml2
-rw-r--r--app/views/shared/issuable/_bulk_update_sidebar.html.haml2
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml28
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar_reviewers.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
-rw-r--r--config/open_api.yml4
-rw-r--r--doc/api/openapi/openapi_v2.yaml40
-rw-r--r--doc/development/packages/dependency_proxy.md2
-rw-r--r--doc/user/application_security/index.md6
-rw-r--r--doc/user/application_security/sast/index.md88
-rw-r--r--doc/user/project/settings/index.md5
-rw-r--r--lib/api/api.rb8
-rw-r--r--lib/api/branches.rb41
-rw-r--r--lib/api/entities/branch.rb44
-rw-r--r--lib/api/entities/merge_request_approvals.rb6
-rw-r--r--lib/api/entities/package.rb20
-rw-r--r--lib/api/entities/user_counts.rb25
-rw-r--r--lib/api/group_avatar.rb4
-rw-r--r--lib/api/group_packages.rb19
-rw-r--r--lib/api/helpers/merge_requests_helpers.rb5
-rw-r--r--lib/api/merge_request_approvals.rb28
-rw-r--r--lib/api/user_counts.rb9
-rw-r--r--locale/gitlab.pot13
-rw-r--r--rubocop/cop/api/ensure_string_detail.rb51
-rw-r--r--spec/lib/api/entities/user_counts_spec.rb24
-rw-r--r--spec/requests/api/user_counts_spec.rb26
-rw-r--r--spec/rubocop/cop/api/ensure_string_detail_spec.rb136
-rw-r--r--spec/support/helpers/test_env.rb38
-rw-r--r--spec/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml_spec.rb4
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb4
-rw-r--r--spec/views/shared/issuable/_sidebar.html.haml_spec.rb6
40 files changed, 675 insertions, 333 deletions
diff --git a/.rubocop_todo/api/ensure_string_detail.yml b/.rubocop_todo/api/ensure_string_detail.yml
new file mode 100644
index 00000000000..feac99aa569
--- /dev/null
+++ b/.rubocop_todo/api/ensure_string_detail.yml
@@ -0,0 +1,5 @@
+---
+API/EnsureStringDetail:
+ Details: grace period
+ Exclude:
+ - 'ee/lib/api/analytics/group_activity_analytics.rb'
diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml
index 9bc4734ed44..a53c450fee3 100644
--- a/.rubocop_todo/layout/line_length.yml
+++ b/.rubocop_todo/layout/line_length.yml
@@ -2943,7 +2943,6 @@ Layout/LineLength:
- 'lib/api/entities/issue_basic.rb'
- 'lib/api/entities/merge_request.rb'
- 'lib/api/entities/namespace.rb'
- - 'lib/api/entities/package.rb'
- 'lib/api/entities/project.rb'
- 'lib/api/entities/user.rb'
- 'lib/api/environments.rb'
@@ -2955,7 +2954,6 @@ Layout/LineLength:
- 'lib/api/group_clusters.rb'
- 'lib/api/group_import.rb'
- 'lib/api/group_labels.rb'
- - 'lib/api/group_packages.rb'
- 'lib/api/group_variables.rb'
- 'lib/api/groups.rb'
- 'lib/api/helm_packages.rb'
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 6a5e2801de5..816b22e4dc6 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -63,6 +63,9 @@ export default {
filters: this.filterParams,
};
},
+ context: {
+ isSingleRequest: true,
+ },
skip() {
return this.isEpicBoard;
},
diff --git a/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql b/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql
index f48383624c9..d5498f03e4b 100644
--- a/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql
+++ b/app/assets/javascripts/boards/graphql/board_lists_deferred.query.graphql
@@ -1,4 +1,4 @@
-query BoardList($id: ListID!, $filters: BoardIssueInput) {
+query BoardListCount($id: ListID!, $filters: BoardIssueInput) {
boardList(id: $id, issueFilters: $filters) {
id
issuesCount
diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
index cc5de5e4083..afce59d304f 100644
--- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
@@ -5,7 +5,7 @@ import TimeTracker from './components/time_tracking/time_tracker.vue';
export default class SidebarMilestone {
constructor() {
- const el = document.getElementById('issuable-time-tracker');
+ const el = document.querySelector('.js-sidebar-time-tracking-root');
if (!el) return;
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index d078511151b..b37486283ca 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -49,27 +49,24 @@ function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-op
return JSON.parse(sidebarOptEl.innerHTML);
}
-function mountSidebarToDoWidget() {
- const el = document.querySelector('.js-issuable-todo');
+function mountSidebarTodoWidget() {
+ const el = document.querySelector('.js-sidebar-todo-widget-root');
if (!el) {
- return false;
+ return null;
}
const { projectPath, iid, id } = el.dataset;
return new Vue({
el,
- name: 'SidebarTodoRoot',
+ name: 'SidebarTodoWidgetRoot',
apolloProvider,
- components: {
- SidebarTodoWidget,
- },
provide: {
isClassicSidebar: true,
},
render: (createElement) =>
- createElement('sidebar-todo-widget', {
+ createElement(SidebarTodoWidget, {
props: {
fullPath: projectPath,
issuableId:
@@ -99,23 +96,22 @@ function getSidebarAssigneeAvailabilityData() {
);
}
-function mountAssigneesComponentDeprecated(mediator) {
- const el = document.getElementById('js-vue-sidebar-assignees');
+function mountSidebarAssigneesDeprecated(mediator) {
+ const el = document.querySelector('.js-sidebar-assignees-root');
- if (!el) return;
+ if (!el) {
+ return null;
+ }
const { id, iid, fullPath } = getSidebarOptions();
const assigneeAvailabilityStatus = getSidebarAssigneeAvailabilityData();
- // eslint-disable-next-line no-new
- new Vue({
+
+ return new Vue({
el,
name: 'SidebarAssigneesRoot',
apolloProvider,
- components: {
- SidebarAssignees,
- },
render: (createElement) =>
- createElement('sidebar-assignees', {
+ createElement(SidebarAssignees, {
props: {
mediator,
issuableIid: String(iid),
@@ -133,10 +129,12 @@ function mountAssigneesComponentDeprecated(mediator) {
});
}
-function mountAssigneesComponent() {
- const el = document.getElementById('js-vue-sidebar-assignees');
+function mountSidebarAssigneesWidget() {
+ const el = document.querySelector('.js-sidebar-assignees-root');
- if (!el) return;
+ if (!el) {
+ return;
+ }
const { id, iid, fullPath, editable } = getSidebarOptions();
const isIssuablePage = isInIssuePage() || isInIncidentPage() || isInDesignPage();
@@ -146,9 +144,6 @@ function mountAssigneesComponent() {
el,
name: 'SidebarAssigneesRoot',
apolloProvider,
- components: {
- SidebarAssigneesWidget,
- },
provide: {
canUpdate: editable,
directlyInviteMembers: Object.prototype.hasOwnProperty.call(
@@ -157,7 +152,7 @@ function mountAssigneesComponent() {
),
},
render: (createElement) =>
- createElement('sidebar-assignees-widget', {
+ createElement(SidebarAssigneesWidget, {
props: {
iid: String(iid),
fullPath,
@@ -185,10 +180,12 @@ function mountAssigneesComponent() {
}
}
-function mountReviewersComponent(mediator) {
- const el = document.getElementById('js-vue-sidebar-reviewers');
+function mountSidebarReviewers(mediator) {
+ const el = document.querySelector('.js-sidebar-reviewers-root');
- if (!el) return;
+ if (!el) {
+ return;
+ }
const { iid, fullPath } = getSidebarOptions();
// eslint-disable-next-line no-new
@@ -196,11 +193,8 @@ function mountReviewersComponent(mediator) {
el,
name: 'SidebarReviewersRoot',
apolloProvider,
- components: {
- SidebarReviewers,
- },
render: (createElement) =>
- createElement('sidebar-reviewers', {
+ createElement(SidebarReviewers, {
props: {
mediator,
issuableIid: String(iid),
@@ -231,22 +225,21 @@ function mountReviewersComponent(mediator) {
}
}
-function mountCrmContactsComponent() {
- const el = document.getElementById('js-issue-crm-contacts');
+function mountSidebarCrmContacts() {
+ const el = document.querySelector('.js-sidebar-crm-contacts-root');
- if (!el) return;
+ if (!el) {
+ return null;
+ }
const { issueId, groupIssuesPath } = el.dataset;
- // eslint-disable-next-line no-new
- new Vue({
+
+ return new Vue({
el,
name: 'SidebarCrmContactsRoot',
apolloProvider,
- components: {
- CrmContacts,
- },
render: (createElement) =>
- createElement('crm-contacts', {
+ createElement(CrmContacts, {
props: {
issueId,
groupIssuesPath,
@@ -255,28 +248,25 @@ function mountCrmContactsComponent() {
});
}
-function mountMilestoneSelect() {
- const el = document.querySelector('.js-milestone-select');
+function mountSidebarMilestoneWidget() {
+ const el = document.querySelector('.js-sidebar-milestone-widget-root');
if (!el) {
- return false;
+ return null;
}
const { canEdit, projectPath, issueIid } = el.dataset;
return new Vue({
el,
- name: 'SidebarMilestoneRoot',
+ name: 'SidebarMilestoneWidgetRoot',
apolloProvider,
- components: {
- SidebarDropdownWidget,
- },
provide: {
canUpdate: parseBoolean(canEdit),
isClassicSidebar: true,
},
render: (createElement) =>
- createElement('sidebar-dropdown-widget', {
+ createElement(SidebarDropdownWidget, {
props: {
attrWorkspacePath: projectPath,
workspacePath: projectPath,
@@ -291,7 +281,7 @@ function mountMilestoneSelect() {
}
export function mountMilestoneDropdown() {
- const el = document.querySelector('.js-milestone-dropdown');
+ const el = document.querySelector('.js-milestone-dropdown-root');
if (!el) {
return null;
@@ -330,21 +320,17 @@ export function mountMilestoneDropdown() {
});
}
-export function mountSidebarLabels() {
- const el = document.querySelector('.js-sidebar-labels');
+export function mountSidebarLabelsWidget() {
+ const el = document.querySelector('.js-sidebar-labels-widget-root');
if (!el) {
- return false;
+ return null;
}
return new Vue({
el,
- name: 'SidebarLabelsRoot',
+ name: 'SidebarLabelsWidgetRoot',
apolloProvider,
-
- components: {
- LabelsSelectWidget,
- },
provide: {
...el.dataset,
canUpdate: parseBoolean(el.dataset.canEdit),
@@ -354,7 +340,7 @@ export function mountSidebarLabels() {
isClassicSidebar: true,
},
render: (createElement) =>
- createElement('labels-select-widget', {
+ createElement(LabelsSelectWidget, {
props: {
iid: String(el.dataset.iid),
fullPath: el.dataset.projectPath,
@@ -381,31 +367,27 @@ export function mountSidebarLabels() {
});
}
-function mountConfidentialComponent() {
- const el = document.getElementById('js-confidential-entry-point');
+function mountSidebarConfidentialityWidget() {
+ const el = document.querySelector('.js-sidebar-confidential-widget-root');
+
if (!el) {
- return;
+ return null;
}
const { fullPath, iid } = getSidebarOptions();
const dataNode = document.getElementById('js-confidential-issue-data');
const initialData = JSON.parse(dataNode.innerHTML);
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
- name: 'SidebarConfidentialRoot',
+ name: 'SidebarConfidentialityWidgetRoot',
apolloProvider,
- components: {
- SidebarConfidentialityWidget,
- },
provide: {
canUpdate: initialData.is_editable,
isClassicSidebar: true,
},
-
render: (createElement) =>
- createElement('sidebar-confidentiality-widget', {
+ createElement(SidebarConfidentialityWidget, {
props: {
iid: String(iid),
fullPath,
@@ -418,28 +400,24 @@ function mountConfidentialComponent() {
});
}
-function mountDueDateComponent() {
- const el = document.getElementById('js-due-date-entry-point');
+function mountSidebarDueDateWidget() {
+ const el = document.querySelector('.js-sidebar-due-date-widget-root');
+
if (!el) {
- return;
+ return null;
}
const { fullPath, iid, editable } = getSidebarOptions();
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
- name: 'SidebarDueDateRoot',
+ name: 'SidebarDueDateWidgetRoot',
apolloProvider,
- components: {
- SidebarDueDateWidget,
- },
provide: {
canUpdate: editable,
},
-
render: (createElement) =>
- createElement('sidebar-due-date-widget', {
+ createElement(SidebarDueDateWidget, {
props: {
iid: String(iid),
fullPath,
@@ -449,29 +427,25 @@ function mountDueDateComponent() {
});
}
-function mountReferenceComponent() {
- const el = document.getElementById('js-reference-entry-point');
+function mountSidebarReferenceWidget() {
+ const el = document.querySelector('.js-sidebar-reference-widget-root');
+
if (!el) {
- return;
+ return null;
}
const { fullPath, iid } = getSidebarOptions();
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
- name: 'SidebarReferenceRoot',
+ name: 'SidebarReferenceWidgetRoot',
apolloProvider,
- components: {
- SidebarReferenceWidget,
- },
provide: {
iid: String(iid),
fullPath,
},
-
render: (createElement) =>
- createElement('sidebar-reference-widget', {
+ createElement(SidebarReferenceWidget, {
props: {
issuableType:
isInIssuePage() || isInIncidentPage() || isInDesignPage()
@@ -482,17 +456,16 @@ function mountReferenceComponent() {
});
}
-function mountLockComponent(store) {
- const el = document.getElementById('js-lock-entry-point');
+function mountIssuableLockForm(store) {
+ const el = document.querySelector('.js-sidebar-lock-root');
if (!el || !store) {
- return;
+ return null;
}
const { fullPath, editable } = getSidebarOptions();
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
name: 'SidebarLockRoot',
store,
@@ -508,23 +481,21 @@ function mountLockComponent(store) {
});
}
-function mountParticipantsComponent() {
- const el = document.querySelector('.js-sidebar-participants-entry-point');
+function mountSidebarParticipantsWidget() {
+ const el = document.querySelector('.js-sidebar-participants-widget-root');
- if (!el) return;
+ if (!el) {
+ return null;
+ }
const { fullPath, iid } = getSidebarOptions();
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
- name: 'SidebarParticipantsRoot',
+ name: 'SidebarParticipantsWidgetRoot',
apolloProvider,
- components: {
- SidebarParticipantsWidget,
- },
render: (createElement) =>
- createElement('sidebar-participants-widget', {
+ createElement(SidebarParticipantsWidget, {
props: {
iid: String(iid),
fullPath,
@@ -537,26 +508,24 @@ function mountParticipantsComponent() {
});
}
-function mountSubscriptionsComponent() {
- const el = document.querySelector('.js-sidebar-subscriptions-entry-point');
+function mountSidebarSubscriptionsWidget() {
+ const el = document.querySelector('.js-sidebar-subscriptions-widget-root');
- if (!el) return;
+ if (!el) {
+ return null;
+ }
const { fullPath, iid, editable } = getSidebarOptions();
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
- name: 'SidebarSubscriptionsRoot',
+ name: 'SidebarSubscriptionsWidgetRoot',
apolloProvider,
- components: {
- SidebarSubscriptionsWidget,
- },
provide: {
canUpdate: editable,
},
render: (createElement) =>
- createElement('sidebar-subscriptions-widget', {
+ createElement(SidebarSubscriptionsWidget, {
props: {
iid: String(iid),
fullPath,
@@ -569,14 +538,15 @@ function mountSubscriptionsComponent() {
});
}
-function mountTimeTrackingComponent() {
- const el = document.getElementById('issuable-time-tracker');
+function mountSidebarTimeTracking() {
+ const el = document.querySelector('.js-sidebar-time-tracking-root');
const { id, iid, fullPath, issuableType, timeTrackingLimitToHours } = getSidebarOptions();
- if (!el) return;
+ if (!el) {
+ return null;
+ }
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
name: 'SidebarTimeTrackingRoot',
apolloProvider,
@@ -593,27 +563,24 @@ function mountTimeTrackingComponent() {
});
}
-function mountSeverityComponent() {
- const severityContainerEl = document.querySelector('#js-severity');
+function mountSidebarSeverity() {
+ const el = document.querySelector('.js-sidebar-severity-root');
- if (!severityContainerEl) {
- return false;
+ if (!el) {
+ return null;
}
const { fullPath, iid, severity, editable } = getSidebarOptions();
return new Vue({
- el: severityContainerEl,
+ el,
name: 'SidebarSeverityRoot',
apolloProvider,
- components: {
- SidebarSeverity,
- },
provide: {
canUpdate: editable,
},
render: (createElement) =>
- createElement('sidebar-severity', {
+ createElement(SidebarSeverity, {
props: {
projectPath: fullPath,
iid: String(iid),
@@ -623,27 +590,25 @@ function mountSeverityComponent() {
});
}
-function mountEscalationStatusComponent() {
- const statusContainerEl = document.querySelector('#js-escalation-status');
+function mountSidebarEscalationStatus() {
+ const el = document.querySelector('.js-sidebar-escalation-status-root');
- if (!statusContainerEl) {
- return false;
+ if (!el) {
+ return null;
}
const { issuableType } = getSidebarOptions();
- const { canUpdate, issueIid, projectPath } = statusContainerEl.dataset;
+ const { canUpdate, issueIid, projectPath } = el.dataset;
return new Vue({
- el: statusContainerEl,
+ el,
+ name: 'SidebarEscalationStatusRoot',
apolloProvider,
- components: {
- SidebarEscalationStatus,
- },
provide: {
canUpdate: parseBoolean(canUpdate),
},
render: (createElement) =>
- createElement('sidebar-escalation-status', {
+ createElement(SidebarEscalationStatus, {
props: {
iid: issueIid,
issuableType,
@@ -653,15 +618,16 @@ function mountEscalationStatusComponent() {
});
}
-function mountCopyEmailComponent() {
- const el = document.getElementById('issuable-copy-email');
+function mountCopyEmailToClipboard() {
+ const el = document.querySelector('.js-sidebar-copy-email-root');
- if (!el) return;
+ if (!el) {
+ return null;
+ }
const { createNoteEmail } = getSidebarOptions();
- // eslint-disable-next-line no-new
- new Vue({
+ return new Vue({
el,
name: 'SidebarCopyEmailRoot',
render: (createElement) =>
@@ -675,36 +641,32 @@ const isAssigneesWidgetShown =
export function mountSidebar(mediator, store) {
initInviteMembersModal();
initInviteMembersTrigger();
-
- mountSidebarToDoWidget();
+ mountSidebarTodoWidget();
if (isAssigneesWidgetShown) {
- mountAssigneesComponent();
+ mountSidebarAssigneesWidget();
} else {
- mountAssigneesComponentDeprecated(mediator);
+ mountSidebarAssigneesDeprecated(mediator);
}
- mountReviewersComponent(mediator);
- mountCrmContactsComponent();
- mountSidebarLabels();
- mountMilestoneSelect();
- mountConfidentialComponent(mediator);
- mountDueDateComponent(mediator);
- mountReferenceComponent(mediator);
- mountLockComponent(store);
- mountParticipantsComponent();
- mountSubscriptionsComponent();
- mountCopyEmailComponent();
+ mountSidebarReviewers(mediator);
+ mountSidebarCrmContacts();
+ mountSidebarLabelsWidget();
+ mountSidebarMilestoneWidget();
+ mountSidebarConfidentialityWidget();
+ mountSidebarDueDateWidget();
+ mountSidebarReferenceWidget();
+ mountIssuableLockForm(store);
+ mountSidebarParticipantsWidget();
+ mountSidebarSubscriptionsWidget();
+ mountCopyEmailToClipboard();
+ mountSidebarTimeTracking();
+ mountSidebarSeverity();
+ mountSidebarEscalationStatus();
new SidebarMoveIssue(
mediator,
$('.js-move-issue'),
$('.js-move-issue-confirmation-button'),
).init();
-
- mountTimeTrackingComponent();
-
- mountSeverityComponent();
-
- mountEscalationStatusComponent();
}
export { getSidebarOptions };
diff --git a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
index a2b6e6213e0..78fce3f7087 100644
--- a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
+++ b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
@@ -11,7 +11,7 @@
.gl-new-dropdown-contents
%ul
- if current_user && moved_mr_sidebar_enabled?
- %li.gl-new-dropdown-item.js-sidebar-subscriptions-entry-point
+ %li.gl-new-dropdown-item.js-sidebar-subscriptions-widget-root
%li.gl-new-dropdown-divider
%hr.dropdown-divider
- if can?(current_user, :update_merge_request, @merge_request)
@@ -36,7 +36,7 @@
= _('Reopen')
= display_issuable_type
- if moved_mr_sidebar_enabled?
- %li.gl-new-dropdown-item#js-lock-entry-point
+ %li.gl-new-dropdown-item.js-sidebar-lock-root
%li.gl-new-dropdown-item
%button.dropdown-item.js-copy-reference{ type: "button", data: { 'clipboard-text': @merge_request.to_reference(full: true) } }
.gl-new-dropdown-item-text-wrapper
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 615aac340bc..64a7f068eff 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -48,7 +48,7 @@
#js-vue-discussion-counter{ data: { blocks_merge: @project.only_allow_merge_if_all_discussions_are_resolved?.to_s } }
- if moved_mr_sidebar_enabled?
- if !!@issuable_sidebar.dig(:current_user, :id)
- .js-issuable-todo{ data: { project_path: @issuable_sidebar[:project_full_path], iid: @issuable_sidebar[:iid], id: @issuable_sidebar[:id] } }
+ .js-sidebar-todo-widget-root{ data: { project_path: @issuable_sidebar[:project_full_path], iid: @issuable_sidebar[:iid], id: @issuable_sidebar[:id] } }
.gl-ml-auto.gl-align-items-center.gl-display-none.gl-md-display-flex.gl-ml-3.js-expand-sidebar{ class: "gl-lg-display-none!" }
= render Pajamas::ButtonComponent.new(icon: 'chevron-double-lg-left',
button_options: { class: 'js-sidebar-toggle' }) do
diff --git a/app/views/shared/issuable/_bulk_update_sidebar.html.haml b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
index 843d2462851..da8477f4b2e 100644
--- a/app/views/shared/issuable/_bulk_update_sidebar.html.haml
+++ b/app/views/shared/issuable/_bulk_update_sidebar.html.haml
@@ -34,7 +34,7 @@
.title
= _('Milestone')
.filter-item
- .js-milestone-dropdown{ data: { full_path: @project.full_path, workspace_type: Namespaces::ProjectNamespace.sti_name.downcase } }
+ .js-milestone-dropdown-root{ data: { full_path: @project.full_path, workspace_type: Namespaces::ProjectNamespace.sti_name.downcase } }
- if is_issue
= render_if_exists 'shared/issuable/iterations_dropdown', parent: @project.group
- if is_issue
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 6984709e735..a02d2851c4c 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -2,7 +2,7 @@
- project = @target_project || @project
- selected = local_assigns.fetch(:selected, nil)
-.js-milestone-dropdown{ data: { can_admin_milestone: can?(current_user, :admin_milestone, project),
+.js-milestone-dropdown-root{ data: { can_admin_milestone: can?(current_user, :admin_milestone, project),
full_path: project.full_path,
input_name: name,
milestone_id: selected.try(:id),
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index daac65b2722..0fd128df997 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -17,14 +17,14 @@
%a.gutter-toggle.float-right.js-sidebar-toggle.has-tooltip{ role: "button", class: "#{'gl-display-block' if moved_sidebar_enabled}", href: "#", "aria-label" => _('Toggle sidebar'), title: sidebar_gutter_tooltip_text, data: { container: 'body', placement: 'left', boundary: 'viewport' } }
= sidebar_gutter_toggle_icon
- if signed_in && !moved_sidebar_enabled
- .js-issuable-todo{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
+ .js-sidebar-todo-widget-root{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
= form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
.block.assignee{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}", data: { qa_selector: 'assignee_block_container' } }
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
- if issuable_sidebar[:supports_severity]
- #js-severity
+ .js-sidebar-severity-root
- if reviewers
.block.reviewer{ data: { qa_selector: 'reviewers_block_container' } }
@@ -32,17 +32,17 @@
- if issuable_sidebar[:supports_escalation]
.block.escalation-status{ data: { testid: 'escalation_status_container' } }
- #js-escalation-status{ data: { can_update: issuable_sidebar.dig(:current_user, :can_update_escalation_status).to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
+ .js-sidebar-escalation-status-root{ data: { can_update: issuable_sidebar.dig(:current_user, :can_update_escalation_status).to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
= render_if_exists 'shared/issuable/sidebar_escalation_policy', issuable_sidebar: issuable_sidebar
- if @project.group.present?
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
- .js-sidebar-labels{ data: sidebar_labels_data(issuable_sidebar, @project) }
+ .js-sidebar-labels-widget-root{ data: sidebar_labels_data(issuable_sidebar, @project) }
- if issuable_sidebar[:supports_milestone]
.block.milestone{ :class => ("gl-border-b-0!" if in_group_context_with_iterations), data: { qa_selector: 'milestone_block', testid: 'sidebar-milestones' } }
- .js-milestone-select{ data: { can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
+ .js-sidebar-milestone-widget-root{ data: { can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
- if in_group_context_with_iterations
.block.gl-collapse-empty{ data: { qa_selector: 'iteration_container', testid: 'iteration_container' } }<
@@ -50,15 +50,15 @@
- if issuable_sidebar[:show_crm_contacts]
.block.contact
- #js-issue-crm-contacts{ data: { issue_id: issuable_sidebar[:id], group_issues_path: issues_group_path(@project.group) } }
+ .js-sidebar-crm-contacts-root{ data: { issue_id: issuable_sidebar[:id], group_issues_path: issues_group_path(@project.group) } }
= render_if_exists 'shared/issuable/sidebar_weight', issuable_sidebar: issuable_sidebar, can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid]
- if issuable_sidebar.has_key?(:due_date)
- #js-due-date-entry-point
+ .js-sidebar-due-date-widget-root
- if issuable_sidebar[:supports_time_tracking]
- #issuable-time-tracker.block
+ .js-sidebar-time-tracking-root.block
// Fallback while content is loading
.title.hide-collapsed
= _('Time tracking')
@@ -70,20 +70,20 @@
- if issuable_sidebar.has_key?(:confidential)
-# haml-lint:disable InlineJavaScript
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe
- #js-confidential-entry-point
+ .js-sidebar-confidential-widget-root
= render_if_exists 'shared/issuable/sidebar_cve_id_request', issuable_sidebar: issuable_sidebar
- if !moved_sidebar_enabled
- #js-lock-entry-point
+ .js-sidebar-lock-root
- if signed_in
- .js-sidebar-subscriptions-entry-point
+ .js-sidebar-subscriptions-widget-root
- .js-sidebar-participants-entry-point
+ .js-sidebar-participants-widget-root
.block.with-sub-blocks
- if !moved_sidebar_enabled
- #js-reference-entry-point
+ .js-sidebar-reference-widget-root
- if issuable_type == 'merge_request' && !moved_sidebar_enabled
.sub-block.js-sidebar-source-branch
.sidebar-collapsed-icon.js-dont-change-state
@@ -95,7 +95,7 @@
- if show_forwarding_email
.block
- #issuable-copy-email
+ .js-sidebar-copy-email-root
- if issuable_sidebar.dig(:current_user, :can_move)
.block.js-sidebar-move-issue-block
.sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') }
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index 62221fb8218..b8a9a82d9b2 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -1,7 +1,7 @@
- issuable_type = issuable_sidebar[:type]
- dropdown_options = assignees_dropdown_options(issuable_type)
-#js-vue-sidebar-assignees{ data: { field: issuable_type,
+.js-sidebar-assignees-root{ data: { field: issuable_type,
signed_in: signed_in,
max_assignees: dropdown_options[:data][:"max-select"],
directly_invite_members: can_admin_project_member?(@project) } }
diff --git a/app/views/shared/issuable/_sidebar_reviewers.html.haml b/app/views/shared/issuable/_sidebar_reviewers.html.haml
index 6bbd522a43b..4df393eeb67 100644
--- a/app/views/shared/issuable/_sidebar_reviewers.html.haml
+++ b/app/views/shared/issuable/_sidebar_reviewers.html.haml
@@ -1,6 +1,6 @@
- issuable_type = issuable_sidebar[:type]
-#js-vue-sidebar-reviewers{ data: { field: issuable_type, signed_in: signed_in } }
+.js-sidebar-reviewers-root{ data: { field: issuable_type, signed_in: signed_in } }
.title.hide-collapsed
= _('Reviewers')
= gl_loading_icon(inline: true)
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 6a65909b1c2..cc1965945ac 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -94,7 +94,7 @@
= milestone.issues_visible_to_user(current_user).closed.count
.block
- #issuable-time-tracker{ data: { time_estimate: @milestone.total_time_estimate,
+ .js-sidebar-time-tracking-root{ data: { time_estimate: @milestone.total_time_estimate,
time_spent: @milestone.total_time_spent,
human_time_estimate: @milestone.human_total_time_estimate,
human_time_spent: @milestone.human_total_time_spent,
diff --git a/config/open_api.yml b/config/open_api.yml
index dc01b2730c3..41036a26291 100644
--- a/config/open_api.yml
+++ b/config/open_api.yml
@@ -47,10 +47,14 @@ metadata:
description: Operations related to deploy freeze periods
- name: ci_lint
description: Operations related to linting a CI config file
+ - name: geo_nodes
+ description: Operations related Geo Nodes
- name: group_export
description: Operations related to exporting groups
- name: group_import
description: Operations related to importing groups
+ - name: group_packages
+ description: Operations related to group packages
- name: merge_requests
description: Operations related to merge requests
- name: metadata
diff --git a/doc/api/openapi/openapi_v2.yaml b/doc/api/openapi/openapi_v2.yaml
index a3c1934bf2a..5a99f6c8793 100644
--- a/doc/api/openapi/openapi_v2.yaml
+++ b/doc/api/openapi/openapi_v2.yaml
@@ -16,6 +16,8 @@ securityDefinitions:
in: query
host: gitlab.com
tags:
+- name: user_counts
+ description: Operations about user_counts
- name: metadata
description: Operations related to metadata of the GitLab instance
- name: access_requests
@@ -289,6 +291,20 @@ paths:
tags:
- access_requests
operationId: getApiV4ProjectsIdAccessRequests
+ "/api/v4/user_counts":
+ get:
+ summary: Return the user specific counts
+ description: Assigned open issues, assigned MRs and pending todos count
+ produces:
+ - application/json
+ responses:
+ '200':
+ description: Return the user specific counts
+ schema:
+ "$ref": "#/definitions/API_Entities_UserCounts"
+ tags:
+ - user_counts
+ operationId: getApiV4UserCounts
"/api/v4/metadata":
get:
summary: Retrieve metadata information for this GitLab instance.
@@ -374,6 +390,30 @@ definitions:
type: string
value:
type: string
+ API_Entities_UserCounts:
+ type: object
+ properties:
+ merge_requests:
+ type: integer
+ format: int32
+ example: 10
+ assigned_issues:
+ type: integer
+ format: int32
+ example: 10
+ assigned_merge_requests:
+ type: integer
+ format: int32
+ example: 10
+ review_requested_merge_requests:
+ type: integer
+ format: int32
+ example: 10
+ todos:
+ type: integer
+ format: int32
+ example: 10
+ description: API_Entities_UserCounts model
API_Entities_Metadata:
type: object
properties:
diff --git a/doc/development/packages/dependency_proxy.md b/doc/development/packages/dependency_proxy.md
index 0996c8be3e6..d5cc219cba0 100644
--- a/doc/development/packages/dependency_proxy.md
+++ b/doc/development/packages/dependency_proxy.md
@@ -130,7 +130,7 @@ graph TD
A[Receive manifest request] --> | We have the manifest cached.| B{Docker manifest HEAD request}
A --> | We do not have manifest cached.| C{Docker manifest GET request}
B --> | Digest matches the one in the DB | D[Fetch manifest from cache]
- B --> | Network failure, cannot reach DockerHub | D[Fetch manifest from cache]
+ B --> | HEAD request error, network failure, cannot reach DockerHub | D[Fetch manifest from cache]
B --> | Digest does not match the one in DB | C
C --> E[Save manifest to cache, save digest to database]
D --> F
diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md
index 30db267d891..f58c146f773 100644
--- a/doc/user/application_security/index.md
+++ b/doc/user/application_security/index.md
@@ -499,8 +499,8 @@ GitLab provides two methods of accomplishing this, each with advantages and disa
are recommended when:
- Scan execution enforcement is required for DAST which uses a DAST site or scan profile.
- - Scan execution enforcement is required for SAST, Secret Detection, or Container Scanning with project-specific variable
- customizations. To accomplish this, users must create a separate security policy per project.
+ - Scan execution enforcement is required for SAST, Secret Detection, Dependency Scanning, or Container Scanning with project-specific
+variable customizations. To accomplish this, users must create a separate security policy per project.
- Scans are required to run on a regular, scheduled cadence.
- Either solution can be used equally well when:
@@ -514,7 +514,7 @@ Additional details about the differences between the two solutions are outlined
| | Compliance Framework Pipelines | Scan Execution Policies |
| ------ | ------ | ------ |
-| **Flexibility** | Supports anything that can be done in a CI file. | Limited to only the items for which GitLab has explicitly added support. DAST, SAST, Secret Detection, and Container Scanning scans are supported. |
+| **Flexibility** | Supports anything that can be done in a CI file. | Limited to only the items for which GitLab has explicitly added support. DAST, SAST, Secret Detection, Dependency Scanning, and Container Scanning scans are supported. |
| **Usability** | Requires knowledge of CI YAML. | Follows a `rules` and `actions`-based YAML structure. |
| **Inclusion in CI pipeline** | The compliance pipeline is executed instead of the project's `.gitlab-ci.yml` file. To include the project's `.gitlab-ci.yml` file, use an `include` statement. Defined variables aren't allowed to be overwritten by the included project's YAML file. | Forced inclusion of a new job into the CI pipeline. DAST jobs that must be customized on a per-project basis can have project-level Site Profiles and Scan Profiles defined. To ensure separation of duties, these profiles are immutable when referenced in a scan execution policy. All jobs can be customized as part of the security policy itself with the same variables that are normally available to the CI job. |
| **Schedulable** | Can be scheduled through a scheduled pipeline on the group. | Can be scheduled natively through the policy configuration itself. |
diff --git a/doc/user/application_security/sast/index.md b/doc/user/application_security/sast/index.md
index ffb9dea4aaa..b1bc9794ced 100644
--- a/doc/user/application_security/sast/index.md
+++ b/doc/user/application_security/sast/index.md
@@ -117,7 +117,7 @@ Check the [SAST direction page](https://about.gitlab.com/direction/secure/static
and the [Maven wrapper](https://github.com/takari/maven-wrapper). However, SpotBugs has [limitations](https://gitlab.com/gitlab-org/gitlab/-/issues/350801) when used against [Ant](https://ant.apache.org/)-based projects. We recommend using the Semgrep-based analyzer for Ant-based Java projects.
1. These analyzers reached [End of Support](https://about.gitlab.com/handbook/product/gitlab-the-product/#end-of-support) status [in GitLab 15.4](https://gitlab.com/gitlab-org/gitlab/-/issues/352554).
-### Multi-project support
+## Multi-project support
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4895) in GitLab 13.7.
@@ -137,16 +137,52 @@ The following analyzers have multi-project support:
- SpotBugs
- Sobelow
-#### Enable multi-project support for Security Code Scan
+### Enable multi-project support for Security Code Scan
Multi-project support in the Security Code Scan requires a Solution (`.sln`) file in the root of
the repository. For details on the Solution format, see the Microsoft reference [Solution (`.sln`) file](https://learn.microsoft.com/en-us/visualstudio/extensibility/internals/solution-dot-sln-file?view=vs-2019).
-### Supported distributions
+## False positive detection **(ULTIMATE)**
+
+> Introduced in GitLab 14.2.
+
+Vulnerabilities that have been detected and are false positives will be flagged as false positives in the security dashboard.
+
+False positive detection is available in a subset of the [supported languages](#supported-languages-and-frameworks) and [analyzers](analyzers.md):
+
+- Ruby, in the Brakeman-based analyzer
+
+![SAST false-positives show in Vulnerability Pages](img/sast_vulnerability_page_fp_detection_v15_2.png)
+
+## Advanced vulnerability tracking **(ULTIMATE)**
+
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5144) in GitLab 14.2.
+
+Source code is volatile; as developers make changes, source code may move within files or between files.
+Security analyzers may have already reported vulnerabilities that are being tracked in the [Vulnerability Report](../vulnerability_report/index.md).
+These vulnerabilities are linked to specific problematic code fragments so that they can be found and fixed.
+If the code fragments are not tracked reliably as they move, vulnerability management is harder because the same vulnerability could be reported again.
+
+GitLab SAST uses an advanced vulnerability tracking algorithm to more accurately identify when the same vulnerability has moved within a file due to refactoring or unrelated changes.
+
+Advanced vulnerability tracking is available in a subset of the [supported languages](#supported-languages-and-frameworks) and [analyzers](analyzers.md):
+
+- C, in the Semgrep-based analyzer only
+- Go, in the Gosec- and Semgrep-based analyzers
+- Java, in the Semgrep-based analyzer only
+- JavaScript, in the Semgrep-based analyzer only
+- Python, in the Semgrep-based analyzer only
+- Ruby, in the Brakeman-based analyzer
+
+Support for more languages and analyzers is tracked in [this epic](https://gitlab.com/groups/gitlab-org/-/epics/5144).
+
+For more information, see the confidential project `https://gitlab.com/gitlab-org/security-products/post-analyzers/tracking-calculator`. The content of this project is available only to GitLab team members.
+
+## Supported distributions
The default scanner images are build off a base Alpine image for size and maintainability.
-#### FIPS-enabled images
+### FIPS-enabled images
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/6479) in GitLab 14.10.
@@ -170,11 +206,7 @@ A FIPS-compliant image is only available for the Semgrep-based analyzer.
To use SAST in a FIPS-compliant manner, you must [exclude other analyzers from running](analyzers.md#customize-analyzers).
-### Making SAST analyzers available to all GitLab tiers
-
-All open source (OSS) analyzers have been moved to the GitLab Free tier as of GitLab 13.3.
-
-#### Summary of features per tier
+## Summary of features per tier
Different features are available in different [GitLab tiers](https://about.gitlab.com/pricing/),
as shown in the following table:
@@ -302,7 +334,7 @@ spotbugs-sast:
FAIL_NEVER: 1
```
-#### Pinning to minor image version
+### Pinning to minor image version
The GitLab-managed CI/CD template specifies a major version and automatically pulls the latest analyzer release within that major version.
@@ -336,42 +368,6 @@ brakeman-sast:
SAST_ANALYZER_IMAGE_TAG: "3.1.1"
```
-### False Positive Detection **(ULTIMATE)**
-
-> Introduced in GitLab 14.2.
-
-Vulnerabilities that have been detected and are false positives will be flagged as false positives in the security dashboard.
-
-False positive detection is available in a subset of the [supported languages](#supported-languages-and-frameworks) and [analyzers](analyzers.md):
-
-- Ruby, in the Brakeman-based analyzer
-
-![SAST false-positives show in Vulnerability Pages](img/sast_vulnerability_page_fp_detection_v15_2.png)
-
-### Advanced vulnerability tracking **(ULTIMATE)**
-
-> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/5144) in GitLab 14.2.
-
-Source code is volatile; as developers make changes, source code may move within files or between files.
-Security analyzers may have already reported vulnerabilities that are being tracked in the [Vulnerability Report](../vulnerability_report/index.md).
-These vulnerabilities are linked to specific problematic code fragments so that they can be found and fixed.
-If the code fragments are not tracked reliably as they move, vulnerability management is harder because the same vulnerability could be reported again.
-
-GitLab SAST uses an advanced vulnerability tracking algorithm to more accurately identify when the same vulnerability has moved within a file due to refactoring or unrelated changes.
-
-Advanced vulnerability tracking is available in a subset of the [supported languages](#supported-languages-and-frameworks) and [analyzers](analyzers.md):
-
-- C, in the Semgrep-based analyzer only
-- Go, in the Gosec- and Semgrep-based analyzers
-- Java, in the Semgrep-based analyzer only
-- JavaScript, in the Semgrep-based analyzer only
-- Python, in the Semgrep-based analyzer only
-- Ruby, in the Brakeman-based analyzer
-
-Support for more languages and analyzers is tracked in [this epic](https://gitlab.com/groups/gitlab-org/-/epics/5144).
-
-For more information, see the confidential project `https://gitlab.com/gitlab-org/security-products/post-analyzers/tracking-calculator`. The content of this project is available only to GitLab team members.
-
### Using CI/CD variables to pass credentials for private repositories
Some analyzers require downloading the project's dependencies to
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index 7eb2c649f84..a872a339433 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -91,9 +91,12 @@ Use the toggles to enable or disable features in the project.
| **Wiki** | ✓ | Enables a separate system for [documentation](../wiki/index.md). |
| **Snippets** | ✓ | Enables [sharing of code and text](../../snippets.md). |
| **Pages** | ✓ | Allows you to [publish static websites](../pages/index.md). |
-| **Operations** | ✓ | Control access to Operations-related features, including [Operations Dashboard](../../../operations/index.md), [Environments and Deployments](../../../ci/environments/index.md), [Feature Flags](../../../operations/feature_flags.md). |
| **Metrics Dashboard** | ✓ | Control access to [metrics dashboard](../integrations/prometheus.md). |
| **Releases** | ✓ | Control access to [Releases](../releases/index.md). |
+| **Environments** | ✓ | Control access to [Environments and Deployments](../../../ci/environments/index.md). |
+| **Feature flags** | ✓ | Control access to [Feature Flags](../../../operations/feature_flags.md). |
+| **Monitor** | ✓ | Control access to [Monitor](../../../operations/index.md) features. |
+| **Infrastructure** | ✓ | Control access to [Infrastructure](../../infrastructure/index.md) features. |
When you disable a feature, the following additional features are also disabled:
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 9d2e5f545d2..3857979b67b 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -176,6 +176,7 @@ module API
mount ::API::Appearance
mount ::API::Applications
mount ::API::Badges
+ mount ::API::Branches
mount ::API::BroadcastMessages
mount ::API::BulkImports
mount ::API::Ci::Jobs
@@ -199,15 +200,18 @@ module API
mount ::API::Features
mount ::API::Files
mount ::API::FreezePeriods
+ mount ::API::GroupAvatar
mount ::API::GroupClusters
mount ::API::GroupExport
mount ::API::GroupImport
+ mount ::API::GroupPackages
mount ::API::GroupVariables
mount ::API::ImportBitbucketServer
mount ::API::ImportGithub
mount ::API::Invitations
mount ::API::Keys
mount ::API::Lint
+ mount ::API::MergeRequestApprovals
mount ::API::MergeRequestDiffs
mount ::API::Metadata
mount ::API::Metrics::UserStarredDashboards
@@ -256,7 +260,6 @@ module API
mount ::API::Avatar
mount ::API::AwardEmoji
mount ::API::Boards
- mount ::API::Branches
mount ::API::Ci::JobArtifacts
mount ::API::Ci::Pipelines
mount ::API::Ci::PipelineSchedules
@@ -278,13 +281,11 @@ module API
mount ::API::GenericPackages
mount ::API::Geo
mount ::API::GoProxy
- mount ::API::GroupAvatar
mount ::API::GroupBoards
mount ::API::GroupContainerRepositories
mount ::API::GroupDebianDistributions
mount ::API::GroupLabels
mount ::API::GroupMilestones
- mount ::API::GroupPackages
mount ::API::Groups
mount ::API::HelmPackages
mount ::API::Integrations
@@ -295,7 +296,6 @@ module API
mount ::API::Markdown
mount ::API::MavenPackages
mount ::API::Members
- mount ::API::MergeRequestApprovals
mount ::API::MergeRequests
mount ::API::Metrics::Dashboard::Annotations
mount ::API::Namespaces
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 51c9a4e53df..a7418deb88a 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -34,12 +34,16 @@ module API
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Get a project repository branches' do
success Entities::Branch
+ success code: 200, model: Entities::Branch
+ failure [{ code: 404, message: '404 Project Not Found' }]
+ tags %w[branches]
+ is_array true
end
params do
use :pagination
use :filter_params
- optional :page_token, type: String, desc: 'Name of branch to start the paginaition from'
+ optional :page_token, type: String, desc: 'Name of branch to start the pagination from'
end
get ':id/repository/branches', urgency: :low do
cache_action([user_project, :branches, current_user, declared_params], expires_in: 30.seconds) do
@@ -65,15 +69,23 @@ module API
end
resource ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
- desc 'Get a single branch' do
- success Entities::Branch
- end
params do
requires :branch, type: String, desc: 'The name of the branch'
end
+ desc 'Check if a branch exists' do
+ success [{ code: 204, message: 'No Content' }]
+ failure [{ code: 404, message: 'Not Found' }]
+ tags %w[branches]
+ end
head do
user_project.repository.branch_exists?(params[:branch]) ? no_content! : not_found!
end
+ desc 'Get a single repository branch' do
+ success Entities::Branch
+ success code: 200, model: Entities::Branch
+ failure [{ code: 404, message: 'Branch Not Found' }, { code: 404, message: 'Project Not Found' }]
+ tags %w[branches]
+ end
get '/', urgency: :low do
branch = find_branch!(params[:branch])
@@ -87,6 +99,9 @@ module API
# but it works with the changed data model to infer `developers_can_merge` and `developers_can_push`.
desc 'Protect a single branch' do
success Entities::Branch
+ success code: 200, model: Entities::Branch
+ failure [{ code: 404, message: '404 Branch Not Found' }]
+ tags %w[branches]
end
params do
requires :branch, type: String, desc: 'The name of the branch', allow_blank: false
@@ -126,6 +141,9 @@ module API
# Note: This API will be deprecated in favor of the protected branches API.
desc 'Unprotect a single branch' do
success Entities::Branch
+ success code: 200, model: Entities::Branch
+ failure [{ code: 404, message: '404 Project Not Found' }, { code: 404, message: '404 Branch Not Found' }]
+ tags %w[branches]
end
params do
requires :branch, type: String, desc: 'The name of the branch', allow_blank: false
@@ -145,6 +163,9 @@ module API
desc 'Create branch' do
success Entities::Branch
+ success code: 201, model: Entities::Branch
+ failure [{ code: 400, message: 'Failed to create branch' }, { code: 400, message: 'Branch already exists' }]
+ tags %w[branches]
end
params do
requires :branch, type: String, desc: 'The name of the branch', allow_blank: false
@@ -166,7 +187,11 @@ module API
end
end
- desc 'Delete a branch'
+ desc 'Delete a branch' do
+ success code: 204
+ failure [{ code: 404, message: 'Branch Not Found' }]
+ tags %w[branches]
+ end
params do
requires :branch, type: String, desc: 'The name of the branch', allow_blank: false
end
@@ -187,7 +212,11 @@ module API
end
end
- desc 'Delete all merged branches'
+ desc 'Delete all merged branches' do
+ success code: 202, message: '202 Accepted'
+ failure [{ code: 404, message: '404 Project Not Found' }]
+ tags %w[branches]
+ end
delete ':id/repository/merged_branches' do
::Branches::DeleteMergedService.new(user_project, current_user).async_execute
diff --git a/lib/api/entities/branch.rb b/lib/api/entities/branch.rb
index 6a75dcddeda..01eaf5c8d31 100644
--- a/lib/api/entities/branch.rb
+++ b/lib/api/entities/branch.rb
@@ -5,13 +5,17 @@ module API
class Branch < Grape::Entity
include Gitlab::Routing
- expose :name
+ expose :name, documentation: { type: 'string', example: 'master' }
expose :commit, using: Entities::Commit do |repo_branch, options|
options[:project].repository.commit(repo_branch.dereferenced_target)
end
- expose :merged do |repo_branch, options|
+ expose :merged,
+ documentation: {
+ type: 'boolean',
+ example: true
+ } do |repo_branch, options|
if options[:merged_branch_names]
options[:merged_branch_names].include?(repo_branch.name)
else
@@ -19,27 +23,51 @@ module API
end
end
- expose :protected do |repo_branch, options|
+ expose :protected,
+ documentation: {
+ type: 'boolean',
+ example: true
+ } do |repo_branch, options|
::ProtectedBranch.protected?(options[:project], repo_branch.name)
end
- expose :developers_can_push do |repo_branch, options|
+ expose :developers_can_push,
+ documentation: {
+ type: 'boolean',
+ example: true
+ } do |repo_branch, options|
::ProtectedBranch.developers_can?(:push, repo_branch.name, protected_refs: options[:project].protected_branches)
end
- expose :developers_can_merge do |repo_branch, options|
+ expose :developers_can_merge,
+ documentation: {
+ type: 'boolean',
+ example: true
+ } do |repo_branch, options|
::ProtectedBranch.developers_can?(:merge, repo_branch.name, protected_refs: options[:project].protected_branches)
end
- expose :can_push do |repo_branch, options|
+ expose :can_push,
+ documentation: {
+ type: 'boolean',
+ example: true
+ } do |repo_branch, options|
Gitlab::UserAccess.new(options[:current_user], container: options[:project]).can_push_to_branch?(repo_branch.name)
end
- expose :default do |repo_branch, options|
+ expose :default,
+ documentation: {
+ type: 'boolean',
+ example: true
+ } do |repo_branch, options|
options[:project].default_branch == repo_branch.name
end
- expose :web_url do |repo_branch|
+ expose :web_url,
+ documentation: {
+ type: 'string',
+ example: 'https://gitlab.example.com/Commit921/the-dude/-/tree/master'
+ } do |repo_branch|
project_tree_url(options[:project], repo_branch.name)
end
end
diff --git a/lib/api/entities/merge_request_approvals.rb b/lib/api/entities/merge_request_approvals.rb
index 6810952b2fc..54f196e0d74 100644
--- a/lib/api/entities/merge_request_approvals.rb
+++ b/lib/api/entities/merge_request_approvals.rb
@@ -3,15 +3,15 @@
module API
module Entities
class MergeRequestApprovals < Grape::Entity
- expose :user_has_approved do |merge_request, options|
+ expose :user_has_approved, documentation: { type: 'boolean' } do |merge_request, options|
merge_request.approved_by?(options[:current_user])
end
- expose :user_can_approve do |merge_request, options|
+ expose :user_can_approve, documentation: { type: 'boolean' } do |merge_request, options|
merge_request.eligible_for_approval_by?(options[:current_user])
end
- expose :approved do |merge_request|
+ expose :approved, documentation: { type: 'boolean' } do |merge_request|
merge_request.approvals.present?
end
diff --git a/lib/api/entities/package.rb b/lib/api/entities/package.rb
index 9b73a142fcd..c92a4677220 100644
--- a/lib/api/entities/package.rb
+++ b/lib/api/entities/package.rb
@@ -7,9 +7,9 @@ module API
include ::Routing::PackagesHelper
extend ::API::Entities::EntityHelpers
- expose :id
+ expose :id, documentation: { type: 'integer', example: 1 }
- expose :name do |package|
+ expose :name, documentation: { type: 'string', example: '@foo/bar' } do |package|
if package.conan?
package.conan_recipe
else
@@ -21,9 +21,9 @@ module API
package.name
end
- expose :version
- expose :package_type
- expose :status
+ expose :version, documentation: { type: 'string', example: '1.0.3' }
+ expose :package_type, documentation: { type: 'string', example: 'npm' }
+ expose :status, documentation: { type: 'string', example: 'default' }
expose :_links do
expose :web_path do |package|
@@ -35,10 +35,12 @@ module API
end
end
- expose :created_at
- expose :last_downloaded_at
- expose :project_id, if: ->(_, opts) { opts[:group] }
- expose :project_path, if: ->(obj, opts) { opts[:group] && Ability.allowed?(opts[:user], :read_project, obj.project) }
+ expose :created_at, documentation: { type: 'dateTime', example: '2022-09-16T12:47:31.949Z' }
+ expose :last_downloaded_at, documentation: { type: 'dateTime', example: '2022-09-19T11:32:35.169Z' }
+ expose :project_id, documentation: { type: 'integer', example: 2 }, if: ->(_, opts) { opts[:group] }
+ expose :project_path, documentation: { type: 'string', example: 'gitlab/foo/bar' }, if: ->(obj, opts) do
+ opts[:group] && Ability.allowed?(opts[:user], :read_project, obj.project)
+ end
expose :tags
expose :pipeline, if: ->(package) { package.original_build_info }, using: Package::Pipeline
diff --git a/lib/api/entities/user_counts.rb b/lib/api/entities/user_counts.rb
new file mode 100644
index 00000000000..e86454c249b
--- /dev/null
+++ b/lib/api/entities/user_counts.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class UserCounts < Grape::Entity
+ expose(
+ :assigned_open_merge_requests_count, # @deprecated
+ as: :merge_requests,
+ documentation: { type: 'integer', example: 10 }
+ )
+ expose :assigned_open_issues_count, as: :assigned_issues, documentation: { type: 'integer', example: 10 }
+ expose(
+ :assigned_open_merge_requests_count,
+ as: :assigned_merge_requests,
+ documentation: { type: 'integer', example: 10 }
+ )
+ expose(
+ :review_requested_open_merge_requests_count,
+ as: :review_requested_merge_requests,
+ documentation: { type: 'integer', example: 10 }
+ )
+ expose :todos_pending_count, as: :todos, documentation: { type: 'integer', example: 10 }
+ end
+ end
+end
diff --git a/lib/api/group_avatar.rb b/lib/api/group_avatar.rb
index 9063040c763..0820011fd89 100644
--- a/lib/api/group_avatar.rb
+++ b/lib/api/group_avatar.rb
@@ -7,11 +7,13 @@ module API
feature_category :subgroups
params do
- requires :id, type: String, desc: 'The ID of a group'
+ requires :id, type: String, desc: 'The ID of the group'
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Download the group avatar' do
detail 'This feature was introduced in GitLab 14.0'
+ tags %w[group_avatar]
+ success code: 200
end
get ':id/avatar' do
avatar = user_group.avatar
diff --git a/lib/api/group_packages.rb b/lib/api/group_packages.rb
index 72d67b41c31..c2b4cbf732f 100644
--- a/lib/api/group_packages.rb
+++ b/lib/api/group_packages.rb
@@ -14,13 +14,19 @@ module API
helpers ::API::Helpers::PackagesHelpers
params do
- requires :id, type: String, desc: "Group's ID or path"
+ requires :id, types: [String, Integer], desc: 'ID or URL-encoded path of the group'
optional :exclude_subgroups, type: Boolean, default: false, desc: 'Determines if subgroups should be excluded'
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
- desc 'Get all project packages within a group' do
- detail 'This feature was introduced in GitLab 12.5'
+ desc 'List packages within a group' do
+ detail 'Get a list of project packages at the group level. This feature was introduced in GitLab 12.5'
success ::API::Entities::Package
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Group Not Found' }
+ ]
+ is_array true
+ tags %w[group_packages]
end
params do
use :pagination
@@ -53,10 +59,13 @@ module API
packages = Packages::GroupPackagesFinder.new(
current_user,
user_group,
- declared(params).slice(:exclude_subgroups, :order_by, :sort, :package_type, :package_name, :include_versionless, :status)
+ declared(params).slice(
+ :exclude_subgroups, :order_by, :sort, :package_type, :package_name, :include_versionless, :status
+ )
).execute
- present paginate(packages), with: ::API::Entities::Package, user: current_user, group: true, namespace: user_group
+ present paginate(packages), with: ::API::Entities::Package, user: current_user, group: true,
+ namespace: user_group
end
end
end
diff --git a/lib/api/helpers/merge_requests_helpers.rb b/lib/api/helpers/merge_requests_helpers.rb
index 85648cd166d..eed9fa30d3c 100644
--- a/lib/api/helpers/merge_requests_helpers.rb
+++ b/lib/api/helpers/merge_requests_helpers.rb
@@ -8,6 +8,9 @@ module API
UNPROCESSABLE_ERROR_KEYS = [:project_access, :branch_conflict, :validate_fork, :base].freeze
+ params :ee_approval_params do
+ end
+
params :merge_requests_negatable_params do
optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID'
optional :author_username, type: String, desc: 'Return merge requests which are authored by the user with the given username'
@@ -136,3 +139,5 @@ module API
end
end
end
+
+API::Helpers::MergeRequestsHelpers.prepend_mod_with('API::Helpers::MergeRequestsHelpers')
diff --git a/lib/api/merge_request_approvals.rb b/lib/api/merge_request_approvals.rb
index 71ca8331ed6..7622ec717cc 100644
--- a/lib/api/merge_request_approvals.rb
+++ b/lib/api/merge_request_approvals.rb
@@ -6,10 +6,9 @@ module API
feature_category :source_code_management
- helpers do
- params :ee_approval_params do
- end
+ helpers ::API::Helpers::MergeRequestsHelpers
+ helpers do
def present_approval(merge_request)
present merge_request, with: ::API::Entities::MergeRequestApprovals, current_user: current_user
end
@@ -24,7 +23,12 @@ module API
# merge_request_iid (required) - IID of MR
# Examples:
# GET /projects/:id/merge_requests/:merge_request_iid/approvals
- desc 'List approvals for merge request'
+ desc 'List approvals for merge request' do
+ success ::API::Entities::MergeRequestApprovals
+ failure [
+ { code: 404, message: 'Not found' }
+ ]
+ end
get 'approvals', urgency: :low do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
@@ -39,7 +43,13 @@ module API
# Examples:
# POST /projects/:id/merge_requests/:merge_request_iid/approve
#
- desc 'Approve a merge request'
+ desc 'Approve a merge request' do
+ success code: 201, model: ::API::Entities::MergeRequestApprovals
+ failure [
+ { code: 404, message: 'Not found' },
+ { code: 401, message: 'Unauthorized' }
+ ]
+ end
params do
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
@@ -60,7 +70,13 @@ module API
present_approval(merge_request)
end
- desc 'Remove an approval from a merge request'
+ desc 'Remove an approval from a merge request' do
+ success code: 201, model: ::API::Entities::MergeRequestApprovals
+ failure [
+ { code: 404, message: 'Not found' },
+ { code: 401, message: 'Unauthorized' }
+ ]
+ end
post 'unapprove', urgency: :low do
merge_request = find_merge_request_with_access(params[:merge_request_iid], :approve_merge_request)
diff --git a/lib/api/user_counts.rb b/lib/api/user_counts.rb
index 388aa5e375c..e9420f1e2b7 100644
--- a/lib/api/user_counts.rb
+++ b/lib/api/user_counts.rb
@@ -8,17 +8,12 @@ module API
resource :user_counts do
desc 'Return the user specific counts' do
detail 'Assigned open issues, assigned MRs and pending todos count'
+ success Entities::UserCounts
end
get do
unauthorized! unless current_user
- {
- merge_requests: current_user.assigned_open_merge_requests_count, # @deprecated
- assigned_issues: current_user.assigned_open_issues_count,
- assigned_merge_requests: current_user.assigned_open_merge_requests_count,
- review_requested_merge_requests: current_user.review_requested_open_merge_requests_count,
- todos: current_user.todos_pending_count
- }
+ present current_user, with: Entities::UserCounts
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 86179ee79dd..7abfb806d2a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2651,6 +2651,9 @@ msgstr ""
msgid "AdminArea|Total users"
msgstr ""
+msgid "AdminArea|Updated %{last_update_time}"
+msgstr ""
+
msgid "AdminArea|Users"
msgstr ""
@@ -4010,9 +4013,6 @@ msgstr ""
msgid "All users with matching cards"
msgstr ""
-msgid "Allow %{strongOpen}%{group_name}%{strongClose} to sign you in?"
-msgstr ""
-
msgid "Allow access to members of the following group"
msgstr ""
@@ -35517,16 +35517,19 @@ msgstr ""
msgid "SAML single sign-on for %{group_name}"
msgstr ""
+msgid "SAML|Allow %{groupName} to sign you in?"
+msgstr ""
+
msgid "SAML|Sign in to GitLab to connect your organization's account"
msgstr ""
-msgid "SAML|The %{strongOpen}%{group_path}%{strongClose} group allows you to sign in using single sign-on."
+msgid "SAML|The %{groupName} group allows you to sign in using single sign-on."
msgstr ""
msgid "SAML|To access %{strongOpen}%{group_name}%{strongClose}, you must sign in using single sign-on through an external sign-in page."
msgstr ""
-msgid "SAML|To allow %{strongOpen}%{group_name}%{strongClose} to manage your GitLab account %{strongOpen}%{username}%{strongClose} (%{email}) after you sign in successfully using single sign-on, select %{strongOpen}Authorize%{strongClose}."
+msgid "SAML|To allow %{groupName} to manage your GitLab account %{username} after you sign in successfully using single sign-on, select %{strongStart}Authorize%{strongEnd}."
msgstr ""
msgid "SAML|Your organization's SSO has been connected to your GitLab account"
diff --git a/rubocop/cop/api/ensure_string_detail.rb b/rubocop/cop/api/ensure_string_detail.rb
new file mode 100644
index 00000000000..bc999525055
--- /dev/null
+++ b/rubocop/cop/api/ensure_string_detail.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require_relative '../../code_reuse_helpers'
+
+module RuboCop
+ module Cop
+ module API
+ class EnsureStringDetail < RuboCop::Cop::Base
+ include CodeReuseHelpers
+
+ # This cop checks that API detail entries use Strings
+ #
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/379037
+ #
+ # @example
+ #
+ # # bad
+ # detail ['Foo bar baz bat', 'http://example.com']
+ #
+ # # good
+ # detail 'Foo bar baz bat. http://example.com'
+ #
+ # end
+ #
+ MSG = 'Only String objects are permitted in API detail field.'
+
+ def_node_matcher :detail_in_desc, <<~PATTERN
+ (block
+ (send nil? :desc ...)
+ _args
+ `(send nil? :detail $_ ...)
+ )
+ PATTERN
+
+ RESTRICT_ON_SEND = %i[detail].freeze
+
+ def on_send(node)
+ return unless in_api?(node)
+
+ parent = node.each_ancestor(:block).first
+ detail_arg = detail_in_desc(parent)
+
+ return unless detail_arg
+ return if [:str, :dstr].include?(detail_arg.type)
+
+ add_offense(node)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/api/entities/user_counts_spec.rb b/spec/lib/api/entities/user_counts_spec.rb
new file mode 100644
index 00000000000..0ed989ad7e9
--- /dev/null
+++ b/spec/lib/api/entities/user_counts_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Entities::UserCounts do
+ let(:user) { build(:user) }
+
+ subject(:entity) { described_class.new(user).as_json }
+
+ it 'represents user counts', :aggregate_failures do
+ expect(user).to receive(:assigned_open_merge_requests_count).and_return(1).twice
+ expect(user).to receive(:assigned_open_issues_count).and_return(2).once
+ expect(user).to receive(:review_requested_open_merge_requests_count).and_return(3).once
+ expect(user).to receive(:todos_pending_count).and_return(4).once
+
+ expect(entity).to include(
+ merge_requests: 1,
+ assigned_issues: 2,
+ assigned_merge_requests: 1,
+ review_requested_merge_requests: 3,
+ todos: 4
+ )
+ end
+end
diff --git a/spec/requests/api/user_counts_spec.rb b/spec/requests/api/user_counts_spec.rb
index ab2aa87d1b7..369ae49de08 100644
--- a/spec/requests/api/user_counts_spec.rb
+++ b/spec/requests/api/user_counts_spec.rb
@@ -26,24 +26,22 @@ RSpec.describe API::UserCounts do
expect(json_response['assigned_issues']).to eq(1)
end
- context 'merge requests' do
- it 'returns assigned MR counts for current user' do
- get api('/user_counts', user)
+ it 'returns assigned MR counts for current user' do
+ get api('/user_counts', user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_a Hash
- expect(json_response['merge_requests']).to eq(1)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_a Hash
+ expect(json_response['merge_requests']).to eq(1)
+ end
- it 'updates the mr count when a new mr is assigned' do
- create(:merge_request, source_project: project, author: user, assignees: [user])
+ it 'updates the mr count when a new mr is assigned' do
+ create(:merge_request, source_project: project, author: user, assignees: [user])
- get api('/user_counts', user)
+ get api('/user_counts', user)
- expect(response).to have_gitlab_http_status(:ok)
- expect(json_response).to be_a Hash
- expect(json_response['merge_requests']).to eq(2)
- end
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_a Hash
+ expect(json_response['merge_requests']).to eq(2)
end
it 'returns pending todo counts for current_user' do
diff --git a/spec/rubocop/cop/api/ensure_string_detail_spec.rb b/spec/rubocop/cop/api/ensure_string_detail_spec.rb
new file mode 100644
index 00000000000..d4f68711e78
--- /dev/null
+++ b/spec/rubocop/cop/api/ensure_string_detail_spec.rb
@@ -0,0 +1,136 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+require_relative '../../../../rubocop/cop/api/ensure_string_detail'
+
+RSpec.describe RuboCop::Cop::API::EnsureStringDetail do
+ context "when in_api? == true" do
+ before do
+ allow(cop).to receive(:in_api?).and_return(true)
+ end
+
+ context "when detail field uses a string" do
+ it "does not add an offense" do
+ expect_no_offenses(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ desc 'Some API thing related to a project' do
+ detail "foo bar"
+ end
+ end
+ end
+ CODE
+ end
+ end
+
+ context "when detail field uses interpolation in a string" do
+ it "does not add an offense" do
+ baz = "bat"
+
+ expect_no_offenses(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ desc 'Some API thing related to a project' do
+ detail "foo bar #{baz}"
+ end
+ end
+ end
+ CODE
+ end
+ end
+
+ context "when detail field uses a multiline string" do
+ it "does not add an offense" do
+ expect_no_offenses(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ desc 'Some API thing related to a project' do
+ detail "foo bar"\
+ "baz bat"
+ end
+ end
+ end
+ CODE
+ end
+ end
+
+ context "when detail field uses a constant" do
+ it "does not add an offense" do
+ pending
+
+ expect_no_offenses(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ DESCRIPTION = 'A string'
+
+ desc 'Some API thing related to a project' do
+ detail DESCRIPTION
+ end
+ end
+ end
+ CODE
+ end
+ end
+
+ context "when detail field uses a HEREDOC string" do
+ it "does not add an offense" do
+ expect_no_offenses(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ desc 'Some API thing related to a project' do
+ detail <<~END
+ foo bar
+ baz bat
+ END
+ end
+ end
+ end
+ CODE
+ end
+ end
+
+ context "when detail field uses an array" do
+ it "adds an offense" do
+ expect_offense(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ desc 'Some API thing related to a project' do
+ something 'else'
+ detail ["foo", "bar"]
+ ^^^^^^^^^^^^^^^^^^^^^ Only String objects are permitted in API detail field.
+ end
+ end
+ end
+ CODE
+ end
+ end
+
+ context "when detail field is outside of desc block" do
+ it "does not add an offense" do
+ expect_no_offenses(<<~CODE)
+ class Foo
+ detail ["foo", "bar"]
+ end
+ CODE
+ end
+ end
+ end
+
+ context "when in_api? == false" do
+ before do
+ allow(cop).to receive(:in_api?).and_return(false)
+ end
+
+ it "does not add an offense" do
+ expect_no_offenses(<<~CODE)
+ class SomeAPI
+ resource :projects do
+ desc 'Some API thing related to a project' do
+ detail ["foo", "bar"]
+ end
+ end
+ end
+ CODE
+ end
+ end
+end
diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb
index 31d24052b3e..e1b461cf37e 100644
--- a/spec/support/helpers/test_env.rb
+++ b/spec/support/helpers/test_env.rb
@@ -284,15 +284,30 @@ module TestEnv
unless File.directory?(repo_path)
start = Time.now
system(*%W(#{Gitlab.config.git.bin_path} clone --quiet -- #{clone_url} #{repo_path}))
- system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} remote remove origin))
puts "==> #{repo_path} set up in #{Time.now - start} seconds...\n"
end
- set_repo_refs(repo_path, refs)
+ create_bundle = !File.file?(repo_bundle_path)
- unless File.file?(repo_bundle_path)
+ unless set_repo_refs(repo_path, refs)
+ # Prefer not to fetch over the network. Only fetch when we have failed to
+ # set all the required local branches. This would happen when a new
+ # branch is added to BRANCH_SHA, in which case we want to update
+ # everything.
+ unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin))
+ raise 'Could not fetch test seed repository.'
+ end
+
+ unless set_repo_refs(repo_path, refs)
+ raise "Could not update test seed repository, please delete #{repo_path} and try again"
+ end
+
+ create_bundle = true
+ end
+
+ if create_bundle
start = Time.now
- system(git_env, *%W(#{Gitlab.config.git.bin_path} -C #{repo_path} bundle create #{repo_bundle_path} --all))
+ system(git_env, *%W(#{Gitlab.config.git.bin_path} -C #{repo_path} bundle create #{repo_bundle_path} --exclude refs/remotes/* --all))
puts "==> #{repo_bundle_path} generated in #{Time.now - start} seconds...\n"
end
end
@@ -394,20 +409,13 @@ module TestEnv
end
def set_repo_refs(repo_path, branch_sha)
- instructions = branch_sha.map { |branch, sha| "update refs/heads/#{branch}\x00#{sha}\x00" }.join("\x00") << "\x00"
- update_refs = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
- reset = proc do
- Dir.chdir(repo_path) do
- IO.popen(update_refs, "w") { |io| io.write(instructions) }
- $?.success?
+ IO.popen(%W[#{Gitlab.config.git.bin_path} -C #{repo_path} update-ref --stdin -z], "w") do |io|
+ branch_sha.each do |branch, sha|
+ io.write("update refs/heads/#{branch}\x00#{sha}\x00\x00")
end
end
- # Try to reset without fetching to avoid using the network.
- unless reset.call
- raise 'Could not fetch test seed repository.' unless system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} fetch origin))
- raise "Could not update test seed repository, please delete #{repo_path} and try again" unless reset.call
- end
+ $?.success?
end
def component_timed_setup(component, install_dir:, version:, task:, fresh_install: true, task_args: [])
diff --git a/spec/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml_spec.rb b/spec/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml_spec.rb
index 416f4253e1b..99339e956cc 100644
--- a/spec/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml_spec.rb
@@ -19,7 +19,7 @@ RSpec.describe 'projects/merge_requests/_close_reopen_draft_report_toggle.html.h
render
- expect(rendered).to have_css('li', class: 'js-sidebar-subscriptions-entry-point')
+ expect(rendered).to have_css('li', class: 'js-sidebar-subscriptions-widget-root')
end
end
@@ -27,7 +27,7 @@ RSpec.describe 'projects/merge_requests/_close_reopen_draft_report_toggle.html.h
it 'is not present' do
render
- expect(rendered).not_to have_css('li', class: 'js-sidebar-subscriptions-entry-point')
+ expect(rendered).not_to have_css('li', class: 'js-sidebar-subscriptions-widget-root')
end
end
end
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
index 05ac037c9cb..75956160c0a 100644
--- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe 'projects/merge_requests/edit.html.haml' do
expect(rendered).to have_field('merge_request[title]')
expect(rendered).to have_field('merge_request[description]')
expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false)
- expect(rendered).to have_selector('.js-milestone-dropdown')
+ expect(rendered).to have_selector('.js-milestone-dropdown-root')
expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false)
end
end
@@ -57,7 +57,7 @@ RSpec.describe 'projects/merge_requests/edit.html.haml' do
expect(rendered).to have_field('merge_request[title]')
expect(rendered).to have_field('merge_request[description]')
expect(rendered).to have_selector('input[name="merge_request[label_ids][]"]', visible: false)
- expect(rendered).to have_selector('.js-milestone-dropdown')
+ expect(rendered).to have_selector('.js-milestone-dropdown-root')
expect(rendered).to have_selector('#merge_request_target_branch', visible: false)
end
end
diff --git a/spec/views/shared/issuable/_sidebar.html.haml_spec.rb b/spec/views/shared/issuable/_sidebar.html.haml_spec.rb
index 43a723dbb2c..31f79c25073 100644
--- a/spec/views/shared/issuable/_sidebar.html.haml_spec.rb
+++ b/spec/views/shared/issuable/_sidebar.html.haml_spec.rb
@@ -43,7 +43,7 @@ RSpec.describe 'shared/issuable/_sidebar.html.haml' do
it 'is expected not to be shown' do
create(:contact, group: group)
- expect(rendered).not_to have_css('#js-issue-crm-contacts')
+ expect(rendered).not_to have_css('.js-sidebar-crm-contacts-root')
end
end
@@ -51,7 +51,7 @@ RSpec.describe 'shared/issuable/_sidebar.html.haml' do
it 'is expected not to be shown' do
group.add_developer(user)
- expect(rendered).not_to have_css('#js-issue-crm-contacts')
+ expect(rendered).not_to have_css('.js-sidebar-crm-contacts-root')
end
end
@@ -60,7 +60,7 @@ RSpec.describe 'shared/issuable/_sidebar.html.haml' do
create(:contact, group: group)
group.add_developer(user)
- expect(rendered).to have_css('#js-issue-crm-contacts')
+ expect(rendered).to have_css('.js-sidebar-crm-contacts-root')
end
end
end