Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot
2022-11-14 21:10:12 +00:00
parent 3244feeb4f
commit 7f35b02e86
40 changed files with 675 additions and 333 deletions

View File

@ -0,0 +1,5 @@
---
API/EnsureStringDetail:
Details: grace period
Exclude:
- 'ee/lib/api/analytics/group_activity_analytics.rb'

View File

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

View File

@ -63,6 +63,9 @@ export default {
filters: this.filterParams,
};
},
context: {
isSingleRequest: true,
},
skip() {
return this.isEpicBoard;
},

View File

@ -1,4 +1,4 @@
query BoardList($id: ListID!, $filters: BoardIssueInput) {
query BoardListCount($id: ListID!, $filters: BoardIssueInput) {
boardList(id: $id, issueFilters: $filters) {
id
issuesCount

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View 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

View File

@ -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: [])

View File

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

View File

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

View File

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