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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-08-22 15:09:21 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-08-22 15:09:21 +0300
commit4fc46d75644b28789e83c95ec4d1309498bb4ba3 (patch)
treec9609e71d701f19835ccad3f5f1c4b24490d4049 /app
parent48641ca0e8b4517fbc73704b132c0157943deec6 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/issuable/index.js19
-rw-r--r--app/assets/javascripts/issues/index.js3
-rw-r--r--app/assets/javascripts/merge_requests/components/header_metadata.vue (renamed from app/assets/javascripts/issuable/components/issuable_header_warnings.vue)28
-rw-r--r--app/assets/javascripts/merge_requests/index.js19
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/utils.js4
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/page.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/list_actions/constants.js16
-rw-r--r--app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js44
-rw-r--r--app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue52
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue49
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue2
-rw-r--r--app/assets/javascripts/work_items/constants.js7
-rw-r--r--app/assets/javascripts/work_items/pages/create_work_item.vue11
-rw-r--r--app/controllers/projects/pages_controller.rb10
-rw-r--r--app/graphql/mutations/issues/bulk_update.rb2
-rw-r--r--app/graphql/resolvers/blame_resolver.rb62
-rw-r--r--app/graphql/types/blame/blame_type.rb20
-rw-r--r--app/graphql/types/blame/commit_data_type.rb20
-rw-r--r--app/graphql/types/blame/groups_type.rb20
-rw-r--r--app/graphql/types/repository/blob_type.rb3
-rw-r--r--app/presenters/gitlab/blame_presenter.rb16
-rw-r--r--app/services/google_cloud/generate_pipeline_service.rb8
-rw-r--r--app/services/packages/nuget/check_duplicates_service.rb88
-rw-r--r--app/services/packages/nuget/extract_metadata_file_service.rb15
-rw-r--r--app/services/packages/nuget/extract_remote_metadata_file_service.rb82
-rw-r--r--app/services/packages/nuget/metadata_extraction_service.rb18
-rw-r--r--app/services/packages/nuget/update_package_from_metadata_service.rb2
-rw-r--r--app/services/projects/update_service.rb1
-rw-r--r--app/views/projects/merge_requests/_mr_title.html.haml2
-rw-r--r--app/views/projects/pages/_pages_settings.html.haml17
31 files changed, 524 insertions, 122 deletions
diff --git a/app/assets/javascripts/issuable/index.js b/app/assets/javascripts/issuable/index.js
index acc0161bf6a..40f92763b29 100644
--- a/app/assets/javascripts/issuable/index.js
+++ b/app/assets/javascripts/issuable/index.js
@@ -5,7 +5,6 @@ import Sidebar from '~/right_sidebar';
import { getSidebarOptions } from '~/sidebar/mount_sidebar';
import CsvImportExportButtons from './components/csv_import_export_buttons.vue';
import IssuableByEmail from './components/issuable_by_email.vue';
-import IssuableHeaderWarnings from './components/issuable_header_warnings.vue';
import issuableBulkUpdateActions from './issuable_bulk_update_actions';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableContext from './issuable_context';
@@ -97,24 +96,6 @@ export function initIssuableByEmail() {
});
}
-export function initIssuableHeaderWarnings(store) {
- const el = document.getElementById('js-issuable-header-warnings');
-
- if (!el) {
- return null;
- }
-
- const { hidden } = el.dataset;
-
- return new Vue({
- el,
- name: 'IssuableHeaderWarningsRoot',
- store,
- provide: { hidden: parseBoolean(hidden) },
- render: (createElement) => createElement(IssuableHeaderWarnings),
- });
-}
-
export function initIssuableSidebar() {
const el = document.querySelector('.js-sidebar-options');
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
index eec7c6bf842..c90473f96b7 100644
--- a/app/assets/javascripts/issues/index.js
+++ b/app/assets/javascripts/issues/index.js
@@ -3,7 +3,7 @@ import IssuableForm from 'ee_else_ce/issuable/issuable_form';
import IssuableLabelSelector from '~/issuable/issuable_label_selector';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import { initIssuableHeaderWarnings, initIssuableSidebar } from '~/issuable';
+import { initIssuableSidebar } from '~/issuable';
import { TYPE_INCIDENT } from '~/issues/constants';
import Issue from '~/issues/issue';
import { initTitleSuggestions, initTypePopover, initTypeSelect } from '~/issues/new';
@@ -62,7 +62,6 @@ export function initShow({ notesParams } = {}) {
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
- initIssuableHeaderWarnings(store);
initIssuableSidebar();
initNotesApp(notesParams);
initRelatedMergeRequests();
diff --git a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue b/app/assets/javascripts/merge_requests/components/header_metadata.vue
index ab7b35982f5..fce7ba385b4 100644
--- a/app/assets/javascripts/issuable/components/issuable_header_warnings.vue
+++ b/app/assets/javascripts/merge_requests/components/header_metadata.vue
@@ -2,16 +2,10 @@
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters } from 'vuex';
-import { sprintf, __ } from '~/locale';
-import { TYPE_ISSUE, TYPE_MERGE_REQUEST, WORKSPACE_PROJECT } from '~/issues/constants';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { __ } from '~/locale';
+import { TYPE_ISSUE, WORKSPACE_PROJECT } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-const noteableTypeText = {
- issue: __('issue'),
- merge_request: __('merge request'),
-};
-
export default {
TYPE_ISSUE,
WORKSPACE_PROJECT,
@@ -22,7 +16,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagMixin()],
inject: ['hidden'],
computed: {
...mapGetters(['getNoteableData']),
@@ -32,26 +25,19 @@ export default {
isConfidential() {
return this.getNoteableData.confidential;
},
- isMergeRequest() {
- return this.getNoteableData.targetType === TYPE_MERGE_REQUEST;
- },
warningIconsMeta() {
return [
{
iconName: 'lock',
visible: this.isLocked,
dataTestId: 'locked',
- tooltip: sprintf(__('This %{issuable} is locked. Only project members can comment.'), {
- issuable: noteableTypeText[this.getNoteableData.targetType],
- }),
+ tooltip: __('This merge request is locked. Only project members can comment.'),
},
{
iconName: 'spam',
visible: this.hidden,
dataTestId: 'hidden',
- tooltip: sprintf(__('This %{issuable} is hidden because its author has been banned'), {
- issuable: noteableTypeText[this.getNoteableData.targetType],
- }),
+ tooltip: __('This merge request is hidden because its author has been banned'),
},
];
},
@@ -74,11 +60,7 @@ export default {
v-gl-tooltip.bottom
:data-testid="meta.dataTestId"
:title="meta.tooltip || null"
- :class="{
- 'gl-mr-3 gl-mt-2 gl-display-flex gl-justify-content-center gl-align-items-center': isMergeRequest,
- 'gl-display-inline-block': !isMergeRequest,
- }"
- class="issuable-warning-icon"
+ class="issuable-warning-icon gl-mr-3 gl-mt-2 gl-display-flex gl-justify-content-center gl-align-items-center"
>
<gl-icon :name="meta.iconName" class="icon" />
</div>
diff --git a/app/assets/javascripts/merge_requests/index.js b/app/assets/javascripts/merge_requests/index.js
new file mode 100644
index 00000000000..29218eb53e0
--- /dev/null
+++ b/app/assets/javascripts/merge_requests/index.js
@@ -0,0 +1,19 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import HeaderMetadata from './components/header_metadata.vue';
+
+export function mountHeaderMetadata(store) {
+ const el = document.querySelector('.js-header-metadata-root');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'HeaderMetadataRoot',
+ store,
+ provide: { hidden: parseBoolean(el.dataset.hidden) },
+ render: (createElement) => createElement(HeaderMetadata),
+ });
+}
diff --git a/app/assets/javascripts/organizations/groups_and_projects/utils.js b/app/assets/javascripts/organizations/groups_and_projects/utils.js
index d2a4e05e806..1a77242d6dc 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/utils.js
+++ b/app/assets/javascripts/organizations/groups_and_projects/utils.js
@@ -1,5 +1,5 @@
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/projects_list/constants';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
export const formatProjects = (projects) =>
projects.map(({ id, nameWithNamespace, accessLevel, webUrl, ...project }) => ({
@@ -13,7 +13,7 @@ export const formatProjects = (projects) =>
},
webUrl,
editPath: `${webUrl}/edit`,
- actions: [ACTION_EDIT, ACTION_DELETE],
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
}));
export const formatGroups = (groups) =>
diff --git a/app/assets/javascripts/pages/projects/merge_requests/page.js b/app/assets/javascripts/pages/projects/merge_requests/page.js
index 75e308e706f..f7b522f7c85 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/page.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/page.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import initMrNotes from 'ee_else_ce/mr_notes';
+import { mountHeaderMetadata } from '~/merge_requests';
import StickyHeader from '~/merge_requests/components/sticky_header.vue';
-import { initIssuableHeaderWarnings } from '~/issuable';
import { start as startCodeReviewMessaging } from '~/code_review/signals';
import diffsEventHub from '~/diffs/event_hub';
import store from '~/mr_notes/stores';
@@ -24,7 +24,7 @@ export function initMrPage() {
requestIdleCallback(() => {
initSidebarBundle(store);
- initIssuableHeaderWarnings(store);
+ mountHeaderMetadata(store);
const el = document.getElementById('js-merge-sticky-header');
diff --git a/app/assets/javascripts/vue_shared/components/list_actions/constants.js b/app/assets/javascripts/vue_shared/components/list_actions/constants.js
new file mode 100644
index 00000000000..b1506ae1e93
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_actions/constants.js
@@ -0,0 +1,16 @@
+import { __ } from '~/locale';
+
+export const ACTION_EDIT = 'edit';
+export const ACTION_DELETE = 'delete';
+
+export const BASE_ACTIONS = {
+ [ACTION_EDIT]: {
+ text: __('Edit'),
+ },
+ [ACTION_DELETE]: {
+ text: __('Delete'),
+ extraAttrs: {
+ class: 'gl-text-red-500!',
+ },
+ },
+};
diff --git a/app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js
new file mode 100644
index 00000000000..d34729c2373
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.stories.js
@@ -0,0 +1,44 @@
+import { makeContainer } from 'storybook_addons/make_container';
+import ListActions from './list_actions.vue';
+import { ACTION_DELETE, ACTION_EDIT } from './constants';
+
+export default {
+ component: ListActions,
+ title: 'vue_shared/list_actions',
+ decorators: [makeContainer({ height: '115px' })],
+ parameters: {
+ docs: {
+ description: {
+ component: `
+This component renders actions used by lists of resources such as groups and projects.
+Currently it is used by \`ProjectsListItem\`. There are base actions defined in \`~/vue_shared/components/list_actions\`
+that help reduce the amount of boilerplate needed for common actions such as edit and delete. This component accepts an
+\`actions\` prop that can extend the base actions and/or add custom actions. These actions should follow the format of
+a [disclosure dropdown item](https://gitlab-org.gitlab.io/gitlab-ui/?path=/docs/base-new-dropdowns-disclosure--docs#setting-disclosure-dropdown-items).
+The \`availableActions\` prop defines what actions to render and in what order. This prop will generally be set by checking
+permissions of the current user.
+`,
+ },
+ },
+ },
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { ListActions },
+ props: Object.keys(argTypes),
+ template: '<list-actions v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ actions: {
+ [ACTION_EDIT]: {
+ href: '/?path=/story/vue-shared-list-actions--default',
+ },
+ [ACTION_DELETE]: {
+ // eslint-disable-next-line no-console
+ action: () => console.log('Deleted'),
+ },
+ },
+ availableActions: [ACTION_EDIT, ACTION_DELETE],
+};
diff --git a/app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue
new file mode 100644
index 00000000000..7b78cc1da8f
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_actions/list_actions.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { BASE_ACTIONS } from './constants';
+
+export default {
+ name: 'ListActions',
+ i18n: {
+ actions: __('Actions'),
+ },
+ components: {
+ GlDisclosureDropdown,
+ },
+ props: {
+ // Can extend `BASE_ACTIONS` and/or add new actions.
+ // Expected format: https://gitlab-org.gitlab.io/gitlab-ui/?path=/docs/base-new-dropdowns-disclosure--docs#setting-disclosure-dropdown-items
+ actions: {
+ type: Object,
+ required: true,
+ },
+ availableActions: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ items() {
+ return this.availableActions.reduce((accumulator, action) => {
+ return [
+ ...accumulator,
+ {
+ ...BASE_ACTIONS[action],
+ ...this.actions[action],
+ },
+ ];
+ }, []);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown
+ :items="items"
+ icon="ellipsis_v"
+ no-caret
+ :toggle-text="$options.i18n.actions"
+ text-sr-only
+ placement="right"
+ category="tertiary"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/constants.js b/app/assets/javascripts/vue_shared/components/projects_list/constants.js
deleted file mode 100644
index aa0b1418a06..00000000000
--- a/app/assets/javascripts/vue_shared/components/projects_list/constants.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export const ACTION_EDIT = 'edit';
-export const ACTION_DELETE = 'delete';
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
index 9fc4571b0dc..ce75e305473 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
@@ -7,7 +7,6 @@ import {
GlTooltipDirective,
GlPopover,
GlSprintf,
- GlDisclosureDropdown,
} from '@gitlab/ui';
import uniqueId from 'lodash/uniqueId';
@@ -20,8 +19,9 @@ import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import { truncate } from '~/lib/utils/text_utility';
import SafeHtml from '~/vue_shared/directives/safe_html';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import ListActions from '~/vue_shared/components/list_actions/list_actions.vue';
+import { ACTION_EDIT, ACTION_DELETE } from '~/vue_shared/components/list_actions/constants';
import DeleteModal from '~/projects/components/shared/delete_modal.vue';
-import { ACTION_EDIT, ACTION_DELETE } from './constants';
const MAX_TOPICS_TO_SHOW = 3;
const MAX_TOPIC_TITLE_LENGTH = 15;
@@ -51,8 +51,8 @@ export default {
GlPopover,
GlSprintf,
TimeAgoTooltip,
- GlDisclosureDropdown,
DeleteModal,
+ ListActions,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -163,30 +163,21 @@ export default {
return numberToMetricPrefix(this.project.openIssuesCount);
},
- actionsDropdownItems() {
- return [
- {
- id: ACTION_EDIT,
- text: __('Edit'),
+ actions() {
+ return {
+ [ACTION_EDIT]: {
href: this.project.editPath,
},
- {
- id: ACTION_DELETE,
- text: __('Delete'),
- extraAttrs: {
- class: 'gl-text-red-500!',
- },
- action: () => {
- this.isDeleteModalVisible = true;
- },
+ [ACTION_DELETE]: {
+ action: this.onActionDelete,
},
- ].filter(({ id }) => this.project.actions?.includes(id));
+ };
},
hasActions() {
- return this.actionsDropdownItems.length;
+ return this.project.availableActions?.length;
},
- hasDeleteAction() {
- return this.actionsDropdownItems.find((action) => action.id === ACTION_DELETE);
+ hasActionDelete() {
+ return this.project.availableActions?.includes(ACTION_DELETE);
},
},
methods: {
@@ -204,6 +195,9 @@ export default {
return null;
},
+ onActionDelete() {
+ this.isDeleteModalVisible = true;
+ },
},
};
</script>
@@ -336,20 +330,15 @@ export default {
</div>
</div>
</div>
- <gl-disclosure-dropdown
+ <list-actions
v-if="hasActions"
class="gl-ml-3 gl-md-align-self-center"
- :items="actionsDropdownItems"
- icon="ellipsis_v"
- no-caret
- :toggle-text="$options.i18n.actions"
- text-sr-only
- placement="right"
- category="tertiary"
+ :actions="actions"
+ :available-actions="project.availableActions"
/>
<delete-modal
- v-if="hasDeleteAction"
+ v-if="hasActionDelete"
v-model="isDeleteModalVisible"
:confirm-phrase="project.name"
:is-fork="project.isForked"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
index 7858e1d4e0e..fadd56fc697 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
@@ -163,7 +163,7 @@ export default {
<template>
<div class="detail-page-header gl-flex-direction-column gl-sm-flex-direction-row">
<div class="detail-page-header-body gl-flex-wrap gl-gap-2">
- <gl-badge :variant="badgeVariant">
+ <gl-badge :variant="badgeVariant" data-testid="issue-state-badge">
<gl-icon v-if="statusIcon" :name="statusIcon" :class="statusIconClass" />
<span class="gl-display-none gl-sm-display-block" :class="{ 'gl-ml-2': statusIcon }">
<slot name="status-badge">{{ badgeText }}</slot>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index d82ad31822c..715c2ef843f 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -57,6 +57,9 @@ export const i18n = {
export const I18N_WORK_ITEM_ERROR_FETCHING_LABELS = s__(
'WorkItem|Something went wrong when fetching labels. Please try again.',
);
+export const I18N_WORK_ITEM_ERROR_FETCHING_TYPES = s__(
+ 'WorkItem|Something went wrong when fetching work item types. Please try again',
+);
export const I18N_WORK_ITEM_ERROR_CREATING = s__(
'WorkItem|Something went wrong when creating %{workItemType}. Please try again.',
);
@@ -239,10 +242,6 @@ export const TODO_DONE_ICON = 'todo-done';
export const TODO_DONE_STATE = 'done';
export const TODO_PENDING_STATE = 'pending';
-export const CURRENT_USER_TODOS_TYPENAME = 'WorkItemWidgetCurrentUserTodos';
-
-export const EMOJI_ACTION_ADD = 'ADD';
-export const EMOJI_ACTION_REMOVE = 'REMOVE';
export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
diff --git a/app/assets/javascripts/work_items/pages/create_work_item.vue b/app/assets/javascripts/work_items/pages/create_work_item.vue
index 49ec12db4e1..b5705b21b5a 100644
--- a/app/assets/javascripts/work_items/pages/create_work_item.vue
+++ b/app/assets/javascripts/work_items/pages/create_work_item.vue
@@ -3,7 +3,11 @@ import { GlButton, GlAlert, GlLoadingIcon, GlFormSelect } from '@gitlab/ui';
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import { getPreferredLocales, s__ } from '~/locale';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { sprintfWorkItem, I18N_WORK_ITEM_ERROR_CREATING } from '../constants';
+import {
+ I18N_WORK_ITEM_ERROR_CREATING,
+ I18N_WORK_ITEM_ERROR_FETCHING_TYPES,
+ sprintfWorkItem,
+} from '../constants';
import createWorkItemMutation from '../graphql/create_work_item.mutation.graphql';
import projectWorkItemTypesQuery from '../graphql/project_work_item_types.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
@@ -11,9 +15,6 @@ import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import ItemTitle from '../components/item_title.vue';
export default {
- fetchTypesErrorText: s__(
- 'WorkItem|Something went wrong when fetching work item types. Please try again',
- ),
components: {
GlButton,
GlAlert,
@@ -53,7 +54,7 @@ export default {
}));
},
error() {
- this.error = this.$options.fetchTypesErrorText;
+ this.error = I18N_WORK_ITEM_ERROR_FETCHING_TYPES;
},
},
},
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
index 02579cd4283..5b32eb8e58e 100644
--- a/app/controllers/projects/pages_controller.rb
+++ b/app/controllers/projects/pages_controller.rb
@@ -65,7 +65,15 @@ class Projects::PagesController < Projects::ApplicationController
end
def project_params_attributes
- [:pages_https_only, { project_setting_attributes: [:pages_unique_domain_enabled] }]
+ [
+ :pages_https_only,
+ { project_setting_attributes: project_setting_attributes }
+ ]
+ end
+
+ # overridden in EE
+ def project_setting_attributes
+ [:pages_unique_domain_enabled]
end
end
diff --git a/app/graphql/mutations/issues/bulk_update.rb b/app/graphql/mutations/issues/bulk_update.rb
index 9c9dd3cf2fc..05e83fc82bb 100644
--- a/app/graphql/mutations/issues/bulk_update.rb
+++ b/app/graphql/mutations/issues/bulk_update.rb
@@ -15,7 +15,7 @@ module Mutations
argument :parent_id, ::Types::GlobalIDType[::IssueParent],
required: true,
description: 'Global ID of the parent to which the bulk update will be scoped. ' \
- 'The parent can be a project **(FREE)** or a group **(PREMIUM)**. ' \
+ 'The parent can be a project **(FREE ALL)** or a group **(PREMIUM ALL)**. ' \
'Example `IssueParentID` are `"gid://gitlab/Project/1"` and `"gid://gitlab/Group/1"`.'
argument :ids, [::Types::GlobalIDType[::Issue]],
diff --git a/app/graphql/resolvers/blame_resolver.rb b/app/graphql/resolvers/blame_resolver.rb
new file mode 100644
index 00000000000..f8b985e6582
--- /dev/null
+++ b/app/graphql/resolvers/blame_resolver.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class BlameResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::Blame::BlameType, null: true
+ calls_gitaly!
+
+ argument :from_line, GraphQL::Types::Int,
+ required: false,
+ default_value: 1,
+ description: 'Range starting from the line. Cannot be less than 1 or greater than `to_line`.'
+ argument :to_line, GraphQL::Types::Int,
+ required: false,
+ default_value: 1,
+ description: 'Range ending on the line. Cannot be less than 1 or less than `to_line`.'
+
+ alias_method :blob, :object
+
+ def ready?(**args)
+ validate_line_params!(args) if feature_enabled?
+
+ super
+ end
+
+ def resolve(from_line:, to_line:)
+ return unless feature_enabled?
+
+ authorize!
+
+ Gitlab::Blame.new(blob, blob.repository.commit(blob.commit_id),
+ range: (from_line..to_line))
+ end
+
+ private
+
+ def authorize!
+ read_code? || raise_resource_not_available_error!
+ end
+
+ def read_code?
+ Ability.allowed?(current_user, :read_code, blob.repository.project)
+ end
+
+ def feature_enabled?
+ Feature.enabled?(:graphql_git_blame, blob.repository.project)
+ end
+
+ def validate_line_params!(args)
+ if args[:from_line] <= 0 || args[:to_line] <= 0
+ raise Gitlab::Graphql::Errors::ArgumentError,
+ '`from_line` and `to_line` must be greater than or equal to 1'
+ end
+
+ return unless args[:from_line] > args[:to_line]
+
+ raise Gitlab::Graphql::Errors::ArgumentError,
+ '`to_line` must be greater than or equal to `from_line`'
+ end
+ end
+end
diff --git a/app/graphql/types/blame/blame_type.rb b/app/graphql/types/blame/blame_type.rb
new file mode 100644
index 00000000000..7e7ba14988d
--- /dev/null
+++ b/app/graphql/types/blame/blame_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ module Blame
+ # rubocop: disable Graphql/AuthorizeTypes
+ class BlameType < BaseObject
+ # This is presented through `Repository` that has its own authorization
+ graphql_name 'Blame'
+
+ present_using Gitlab::BlamePresenter
+
+ field :first_line, GraphQL::Types::String, null: true,
+ description: 'First line of Git Blame for given range.', calls_gitaly: true
+ field :groups, [Types::Blame::GroupsType], null: true,
+ description: 'Git Blame grouped by contiguous lines for commit.', calls_gitaly: true,
+ method: :groups_commit_data
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/blame/commit_data_type.rb b/app/graphql/types/blame/commit_data_type.rb
new file mode 100644
index 00000000000..faac34b06f4
--- /dev/null
+++ b/app/graphql/types/blame/commit_data_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ module Blame
+ # rubocop: disable Graphql/AuthorizeTypes
+ class CommitDataType < BaseObject
+ # This is presented through `Repository` that has its own authorization
+ graphql_name 'CommitData'
+
+ field :age_map_class, GraphQL::Types::String, null: false, description: 'CSS class for age of commit.'
+ field :author_avatar, GraphQL::Types::String, null: false, description: 'Link to author avatar.'
+ field :commit_author_link, GraphQL::Types::String, null: false, description: 'Link to the commit author.'
+ field :commit_link, GraphQL::Types::String, null: false, description: 'Link to the commit.'
+ field :project_blame_link, GraphQL::Types::String,
+ null: true, description: 'Link to blame prior to the change.'
+ field :time_ago_tooltip, GraphQL::Types::String, null: false, description: 'Time of commit.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/blame/groups_type.rb b/app/graphql/types/blame/groups_type.rb
new file mode 100644
index 00000000000..754f3f95364
--- /dev/null
+++ b/app/graphql/types/blame/groups_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ module Blame
+ # rubocop: disable Graphql/AuthorizeTypes
+ class GroupsType < BaseObject
+ # This is presented through `Repository` that has its own authorization
+ graphql_name 'Groups'
+
+ field :commit, Types::CommitType, null: false, description: 'Commit responsible for specified group.'
+ field :commit_data, Types::Blame::CommitDataType, null: true,
+ description: 'HTML data derived from commit needed to present blame.', calls_gitaly: true
+ field :lineno, GraphQL::Types::Int, null: false, description: 'Starting line number for the commit group.'
+ field :lines, [GraphQL::Types::String], null: false, description: 'Array of lines added for the commit group.'
+ field :span, GraphQL::Types::Int, null: false,
+ description: 'Number of contiguous lines which the blame spans for the commit group.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/repository/blob_type.rb b/app/graphql/types/repository/blob_type.rb
index c5d6e26e94b..3959118631f 100644
--- a/app/graphql/types/repository/blob_type.rb
+++ b/app/graphql/types/repository/blob_type.rb
@@ -86,6 +86,9 @@ module Types
field :blame_path, GraphQL::Types::String, null: true,
description: 'Web path to blob blame page.'
+ field :blame, Types::Blame::BlameType, null: true,
+ description: 'Blob blame. Available only when feature flag `graphql_git_blame` is enabled.', alpha: { milestone: '16.3' }, resolver: Resolvers::BlameResolver
+
field :history_path, GraphQL::Types::String, null: true,
description: 'Web path to blob history page.'
diff --git a/app/presenters/gitlab/blame_presenter.rb b/app/presenters/gitlab/blame_presenter.rb
index 6230e61d2be..0a2738bea5f 100644
--- a/app/presenters/gitlab/blame_presenter.rb
+++ b/app/presenters/gitlab/blame_presenter.rb
@@ -40,6 +40,10 @@ module Gitlab
@commits[commit.id] ||= get_commit_data(commit, previous_path)
end
+ def groups_commit_data
+ groups.each { |group| group[:commit_data] = commit_data(group[:commit]) }
+ end
+
private
# Huge source files with a high churn rate (e.g. 'locale/gitlab.pot') could have
@@ -86,5 +90,17 @@ module Gitlab
def mail_to(*args, &block)
ActionController::Base.helpers.mail_to(*args, &block)
end
+
+ def project
+ return super.project if defined?(super.project)
+
+ blame.commit.repository.project
+ end
+
+ def page
+ return super.page if defined?(super.page)
+
+ nil
+ end
end
end
diff --git a/app/services/google_cloud/generate_pipeline_service.rb b/app/services/google_cloud/generate_pipeline_service.rb
index 95de1fa21b7..30c358687aa 100644
--- a/app/services/google_cloud/generate_pipeline_service.rb
+++ b/app/services/google_cloud/generate_pipeline_service.rb
@@ -71,8 +71,12 @@ module GoogleCloud
end
def pipeline_content(include_path)
- gitlab_ci_yml = ::Gitlab::Ci::Config::Yaml.load!(default_branch_gitlab_ci_yml || '{}')
- append_remote_include(gitlab_ci_yml, "https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/#{include_path}")
+ gitlab_ci_yml = ::Gitlab::Ci::Config::Yaml::Loader.new(default_branch_gitlab_ci_yml || '{}').load
+
+ append_remote_include(
+ gitlab_ci_yml.content,
+ "https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library/-/raw/main/#{include_path}"
+ )
end
def append_remote_include(gitlab_ci_yml, include_url)
diff --git a/app/services/packages/nuget/check_duplicates_service.rb b/app/services/packages/nuget/check_duplicates_service.rb
new file mode 100644
index 00000000000..7ad9038d7c1
--- /dev/null
+++ b/app/services/packages/nuget/check_duplicates_service.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ class CheckDuplicatesService < BaseService
+ include Gitlab::Utils::StrongMemoize
+
+ ExtractionError = Class.new(StandardError)
+
+ def execute
+ return ServiceResponse.success if package_settings_allow_duplicates? || !target_package_is_duplicate?
+
+ ServiceResponse.error(
+ message: 'A package with the same name and version already exists',
+ reason: :conflict
+ )
+ rescue ExtractionError => e
+ ServiceResponse.error(message: e.message, reason: :bad_request)
+ end
+
+ private
+
+ def package_settings_allow_duplicates?
+ package_settings.nuget_duplicates_allowed? || package_settings.class.duplicates_allowed?(existing_package)
+ end
+
+ def target_package_is_duplicate?
+ existing_package.name.casecmp(metadata[:package_name]) == 0 &&
+ (existing_package.version.casecmp(metadata[:package_version]) == 0 ||
+ existing_package.normalized_nuget_version&.casecmp(metadata[:package_version]) == 0)
+ end
+
+ def package_settings
+ project.namespace.package_settings
+ end
+ strong_memoize_attr :package_settings
+
+ def existing_package
+ ::Packages::Nuget::PackageFinder
+ .new(
+ current_user,
+ project,
+ package_name: metadata[:package_name],
+ package_version: metadata[:package_version]
+ )
+ .execute
+ .first
+ end
+ strong_memoize_attr :existing_package
+
+ def metadata
+ if remote_package_file?
+ ExtractMetadataContentService
+ .new(nuspec_file_content)
+ .execute
+ .payload
+ else # to cover the case when package file is on disk not in object storage
+ MetadataExtractionService
+ .new(mock_package_file)
+ .execute
+ .payload
+ end
+ end
+ strong_memoize_attr :metadata
+
+ def remote_package_file?
+ params[:remote_url].present?
+ end
+
+ def nuspec_file_content
+ ExtractRemoteMetadataFileService
+ .new(params[:remote_url])
+ .execute
+ .payload
+ rescue ExtractRemoteMetadataFileService::ExtractionError => e
+ raise ExtractionError, e.message
+ end
+
+ def mock_package_file
+ ::Packages::PackageFile.new(
+ params
+ .slice(:file, :file_name)
+ .merge(package: ::Packages::Package.nuget.build)
+ )
+ end
+ end
+ end
+end
diff --git a/app/services/packages/nuget/extract_metadata_file_service.rb b/app/services/packages/nuget/extract_metadata_file_service.rb
index 61e4892fee7..cc040a45016 100644
--- a/app/services/packages/nuget/extract_metadata_file_service.rb
+++ b/app/services/packages/nuget/extract_metadata_file_service.rb
@@ -3,14 +3,12 @@
module Packages
module Nuget
class ExtractMetadataFileService
- include Gitlab::Utils::StrongMemoize
-
ExtractionError = Class.new(StandardError)
MAX_FILE_SIZE = 4.megabytes.freeze
- def initialize(package_file_id)
- @package_file_id = package_file_id
+ def initialize(package_file)
+ @package_file = package_file
end
def execute
@@ -21,12 +19,7 @@ module Packages
private
- attr_reader :package_file_id
-
- def package_file
- ::Packages::PackageFile.find_by_id(package_file_id)
- end
- strong_memoize_attr :package_file
+ attr_reader :package_file
def valid_package_file?
package_file &&
@@ -41,7 +34,7 @@ module Packages
raise ExtractionError, 'nuspec file not found' unless entry
raise ExtractionError, 'nuspec file too big' if MAX_FILE_SIZE < entry.size
- Tempfile.open("nuget_extraction_package_file_#{package_file_id}") do |file|
+ Tempfile.open("nuget_extraction_package_file_#{package_file.id}") do |file|
entry.extract(file.path) { true } # allow #extract to overwrite the file
file.unlink
file.read
diff --git a/app/services/packages/nuget/extract_remote_metadata_file_service.rb b/app/services/packages/nuget/extract_remote_metadata_file_service.rb
new file mode 100644
index 00000000000..37624002ce7
--- /dev/null
+++ b/app/services/packages/nuget/extract_remote_metadata_file_service.rb
@@ -0,0 +1,82 @@
+# frozen_string_literal: true
+
+module Packages
+ module Nuget
+ class ExtractRemoteMetadataFileService
+ include Gitlab::Utils::StrongMemoize
+
+ ExtractionError = Class.new(StandardError)
+
+ MAX_FILE_SIZE = 4.megabytes.freeze
+ METADATA_FILE_EXTENSION = '.nuspec'
+ MAX_FRAGMENTS = 5 # nuspec file is usually in the first 2 fragments but we buffer 5 max
+
+ def initialize(remote_url)
+ @remote_url = remote_url
+ end
+
+ def execute
+ raise ExtractionError, 'invalid file url' if remote_url.blank?
+
+ if nuspec_file_content.blank? || !nuspec_file_content.instance_of?(String)
+ raise ExtractionError, 'nuspec file not found'
+ end
+
+ ServiceResponse.success(payload: nuspec_file_content)
+ end
+
+ private
+
+ attr_reader :remote_url
+
+ def nuspec_file_content
+ fragments = []
+
+ Gitlab::HTTP.get(remote_url, stream_body: true, allow_object_storage: true) do |fragment|
+ break if fragments.size >= MAX_FRAGMENTS
+
+ fragments << fragment
+ joined_fragments = fragments.join
+
+ next if joined_fragments.exclude?(METADATA_FILE_EXTENSION)
+
+ nuspec_content = extract_nuspec_file(joined_fragments)
+
+ break nuspec_content if nuspec_content.present?
+ end
+ end
+ strong_memoize_attr :nuspec_file_content
+
+ def extract_nuspec_file(fragments)
+ StringIO.open(fragments) do |io|
+ Zip::InputStream.open(io) do |zip|
+ process_zip_entries(zip)
+ end
+ rescue Zip::Error => e
+ raise ExtractionError, "Error opening zip stream: #{e.message}"
+ end
+ end
+
+ def process_zip_entries(zip)
+ while (entry = zip.get_next_entry) # rubocop:disable Lint/AssignmentInCondition
+ next unless entry.name.end_with?(METADATA_FILE_EXTENSION)
+
+ raise ExtractionError, 'nuspec file too big' if entry.size > MAX_FILE_SIZE
+
+ return extract_file_content(entry)
+ end
+ end
+
+ def extract_file_content(entry)
+ Tempfile.create('extract_remote_metadata_file_service') do |file|
+ entry.extract(file.path) { true } # allow #extract to overwrite the file
+ file.read
+ end
+ rescue Zip::DecompressionError
+ '' # Ignore decompression errors and continue reading the next fragment
+ rescue Zip::EntrySizeError => e
+ raise ExtractionError, "nuspec file has the wrong entry size: #{e.message}"
+ end
+ end
+ end
+end
diff --git a/app/services/packages/nuget/metadata_extraction_service.rb b/app/services/packages/nuget/metadata_extraction_service.rb
index e1ee29ef2c6..2c758a5ec20 100644
--- a/app/services/packages/nuget/metadata_extraction_service.rb
+++ b/app/services/packages/nuget/metadata_extraction_service.rb
@@ -3,8 +3,8 @@
module Packages
module Nuget
class MetadataExtractionService
- def initialize(package_file_id)
- @package_file_id = package_file_id
+ def initialize(package_file)
+ @package_file = package_file
end
def execute
@@ -13,18 +13,18 @@ module Packages
private
- attr_reader :package_file_id
+ attr_reader :package_file
- def nuspec_file_content
- ExtractMetadataFileService
- .new(package_file_id)
+ def metadata
+ ExtractMetadataContentService
+ .new(nuspec_file_content)
.execute
.payload
end
- def metadata
- ExtractMetadataContentService
- .new(nuspec_file_content)
+ def nuspec_file_content
+ ExtractMetadataFileService
+ .new(package_file)
.execute
.payload
end
diff --git a/app/services/packages/nuget/update_package_from_metadata_service.rb b/app/services/packages/nuget/update_package_from_metadata_service.rb
index 73a52ea569f..258f8c8f6aa 100644
--- a/app/services/packages/nuget/update_package_from_metadata_service.rb
+++ b/app/services/packages/nuget/update_package_from_metadata_service.rb
@@ -148,7 +148,7 @@ module Packages
end
def metadata
- ::Packages::Nuget::MetadataExtractionService.new(@package_file.id).execute.payload
+ ::Packages::Nuget::MetadataExtractionService.new(@package_file).execute.payload
end
strong_memoize_attr :metadata
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 8639e2f833f..40d3e2392c6 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -112,6 +112,7 @@ module Projects
# overridden by EE module
end
+ # overridden by EE module
def remove_unallowed_params
params.delete(:emails_enabled) unless can?(current_user, :set_emails_disabled, project)
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index dfa582f4c60..f0e7df8a379 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -15,7 +15,7 @@
.detail-page-header.border-bottom-0.gl-display-block.gl-pt-5{ class: "gl-md-display-flex! #{'is-merge-request' if moved_mr_sidebar_enabled? && !fluid_layout}" }
.detail-page-header-body
.issuable-meta.gl-display-flex
- #js-issuable-header-warnings{ data: { hidden: @merge_request.hidden?.to_s } }
+ .js-header-metadata-root{ data: { hidden: @merge_request.hidden?.to_s } }
%h1.title.page-title.gl-font-size-h-display.gl-my-0.gl-display-inline-block{ data: { qa_selector: 'title_content' } }
= markdown_field(@merge_request, :title)
diff --git a/app/views/projects/pages/_pages_settings.html.haml b/app/views/projects/pages/_pages_settings.html.haml
index b1ec7a362b7..1aa8148dfed 100644
--- a/app/views/projects/pages/_pages_settings.html.haml
+++ b/app/views/projects/pages/_pages_settings.html.haml
@@ -1,11 +1,7 @@
-- can_edit_max_page_size = can?(current_user, :update_max_pages_size)
-- can_enforce_https_only = Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
-
= gitlab_ui_form_for @project, url: project_pages_path(@project), html: { class: 'inline', title: pages_https_only_title } do |f|
- - if can_edit_max_page_size
- = render_if_exists 'shared/pages/max_pages_size_input', form: f
+ = render_if_exists 'shared/pages/max_pages_size_input', form: f
- - if can_enforce_https_only
+ - if Gitlab.config.pages.external_http || Gitlab.config.pages.external_https
.form-group
= f.gitlab_ui_checkbox_component :pages_https_only,
s_('GitLabPages|Force HTTPS (requires valid certificates)'),
@@ -24,5 +20,14 @@
%p.gl-pl-6
= s_("GitLabPages|When enabled, a unique domain is generated to access pages.").html_safe
+ - if can?(current_user, :pages_multiple_versions, @project)
+ .form-group
+ = f.fields_for :project_setting do |settings|
+ = settings.gitlab_ui_checkbox_component :pages_multiple_versions_enabled,
+ s_('GitLabPages|Use multiple versions'),
+ label_options: { class: 'label-bold' }
+ %p.gl-pl-6
+ = s_("GitLabPages|When enabled, you can create multiple versions of your pages site.").html_safe
+
.gl-mt-3
= f.submit s_('GitLabPages|Save changes'), pajamas_button: true