diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-22 15:09:21 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-08-22 15:09:21 +0300 |
commit | 4fc46d75644b28789e83c95ec4d1309498bb4ba3 (patch) | |
tree | c9609e71d701f19835ccad3f5f1c4b24490d4049 /app | |
parent | 48641ca0e8b4517fbc73704b132c0157943deec6 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
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 |