mirror of
https://github.com/gitlabhq/gitlabhq.git
synced 2025-08-15 23:30:46 +00:00
Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
5
.rubocop_todo/api/ensure_string_detail.yml
Normal file
5
.rubocop_todo/api/ensure_string_detail.yml
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
API/EnsureStringDetail:
|
||||
Details: grace period
|
||||
Exclude:
|
||||
- 'ee/lib/api/analytics/group_activity_analytics.rb'
|
@ -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'
|
||||
|
@ -63,6 +63,9 @@ export default {
|
||||
filters: this.filterParams,
|
||||
};
|
||||
},
|
||||
context: {
|
||||
isSingleRequest: true,
|
||||
},
|
||||
skip() {
|
||||
return this.isEpicBoard;
|
||||
},
|
||||
|
@ -1,4 +1,4 @@
|
||||
query BoardList($id: ListID!, $filters: BoardIssueInput) {
|
||||
query BoardListCount($id: ListID!, $filters: BoardIssueInput) {
|
||||
boardList(id: $id, issueFilters: $filters) {
|
||||
id
|
||||
issuesCount
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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 };
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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') }
|
||||
|
@ -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) } }
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
@ -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. |
|
||||
|
@ -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
|
||||
|
||||

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

|
||||
|
||||
### 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
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
25
lib/api/entities/user_counts.rb
Normal file
25
lib/api/entities/user_counts.rb
Normal file
@ -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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
51
rubocop/cop/api/ensure_string_detail.rb
Normal file
51
rubocop/cop/api/ensure_string_detail.rb
Normal file
@ -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
|
24
spec/lib/api/entities/user_counts_spec.rb
Normal file
24
spec/lib/api/entities/user_counts_spec.rb
Normal file
@ -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
|
@ -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
|
||||
|
136
spec/rubocop/cop/api/ensure_string_detail_spec.rb
Normal file
136
spec/rubocop/cop/api/ensure_string_detail_spec.rb
Normal file
@ -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
|
@ -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: [])
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
Reference in New Issue
Block a user