diff options
84 files changed, 837 insertions, 457 deletions
diff --git a/.gitlab/ci/cache-repo.gitlab-ci.yml b/.gitlab/ci/cache-repo.gitlab-ci.yml index ff7655cee71..18e1ca1644d 100644 --- a/.gitlab/ci/cache-repo.gitlab-ci.yml +++ b/.gitlab/ci/cache-repo.gitlab-ci.yml @@ -49,6 +49,7 @@ cache-repo: cd .. && rm -rf $CI_PROJECT_NAME; echo "Cloning $CI_REPOSITORY_URL into $CI_PROJECT_NAME."; time git clone --progress $CI_REPOSITORY_URL $CI_PROJECT_NAME; + cd $CI_PROJECT_NAME; echo "Archiving $CI_PROJECT_NAME into /tmp/$FULL_CLONE_TAR_FILENAME."; time tar cf /tmp/$FULL_CLONE_TAR_FILENAME .; echo "GZipping /tmp/$FULL_CLONE_TAR_FILENAME."; diff --git a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue index 9420480e35a..2e66a1196a4 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_integrations_list.vue @@ -13,6 +13,7 @@ import { import { s__, __ } from '~/locale'; import Tracking from '~/tracking'; import { trackAlertIntegrationsViewsOptions, integrationToDeleteDefault } from '../constants'; +import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql'; export const i18n = { title: s__('AlertsIntegrations|Current integrations'), @@ -58,11 +59,6 @@ export default { required: false, default: false, }, - currentIntegration: { - type: Object, - required: false, - default: null, - }, }, fields: [ { @@ -82,9 +78,15 @@ export default { label: __('Actions'), }, ], + apollo: { + currentIntegration: { + query: getCurrentIntegrationQuery, + }, + }, data() { return { integrationToDelete: integrationToDeleteDefault, + currentIntegration: null, }; }, mounted() { @@ -101,11 +103,11 @@ export default { const { category, action } = trackAlertIntegrationsViewsOptions; Tracking.event(category, action); }, - intergrationToDelete({ name, id }) { + setIntegrationToDelete({ name, id }) { this.integrationToDelete.id = id; this.integrationToDelete.name = name; }, - deleteIntergration() { + deleteIntegration() { this.$emit('delete-integration', { id: this.integrationToDelete.id }); this.integrationToDelete = { ...integrationToDeleteDefault }; }, @@ -153,7 +155,7 @@ export default { <gl-button v-gl-modal.deleteIntegration icon="remove" - @click="intergrationToDelete(item)" + @click="setIntegrationToDelete(item)" /> </gl-button-group> </template> @@ -175,7 +177,7 @@ export default { :title="__('Are you sure?')" :ok-title="s__('AlertSettings|Delete integration')" ok-variant="danger" - @ok="deleteIntergration" + @ok="deleteIntegration" > <gl-sprintf :message=" diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue index cbb247a79e6..2aabe0b8e07 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form_new.vue @@ -12,11 +12,12 @@ import { GlModalDirective, GlToggle, } from '@gitlab/ui'; -import MappingBuilder from './alert_mapping_builder.vue'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { s__ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import MappingBuilder from './alert_mapping_builder.vue'; import AlertSettingsFormHelpBlock from './alert_settings_form_help_block.vue'; +import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql'; import service from '../services'; import { integrationTypesNew, @@ -117,16 +118,16 @@ export default { type: Boolean, required: true, }, - currentIntegration: { - type: Object, - required: false, - default: null, - }, canAddIntegration: { type: Boolean, required: true, }, }, + apollo: { + currentIntegration: { + query: getCurrentIntegrationQuery, + }, + }, data() { return { selectedIntegration: integrationTypesNew[0].value, @@ -140,6 +141,7 @@ export default { resetSamplePayloadConfirmed: false, customMapping: null, parsingPayload: false, + currentIntegration: null, }; }, computed: { diff --git a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue index 3904a5b0fe2..4f341ac07e9 100644 --- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue +++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue @@ -4,6 +4,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { fetchPolicies } from '~/lib/graphql'; import createFlash, { FLASH_TYPES } from '~/flash'; import getIntegrationsQuery from '../graphql/queries/get_integrations.query.graphql'; +import getCurrentIntegrationQuery from '../graphql/queries/get_current_integration.query.graphql'; import createHttpIntegrationMutation from '../graphql/mutations/create_http_integration.mutation.graphql'; import createPrometheusIntegrationMutation from '../graphql/mutations/create_prometheus_integration.mutation.graphql'; import updateHttpIntegrationMutation from '../graphql/mutations/update_http_integration.mutation.graphql'; @@ -11,6 +12,7 @@ import updatePrometheusIntegrationMutation from '../graphql/mutations/update_pro import destroyHttpIntegrationMutation from '../graphql/mutations/destroy_http_integration.mutation.graphql'; import resetHttpTokenMutation from '../graphql/mutations/reset_http_token.mutation.graphql'; import resetPrometheusTokenMutation from '../graphql/mutations/reset_prometheus_token.mutation.graphql'; +import updateCurrentIntergrationMutation from '../graphql/mutations/update_current_intergration.mutation.graphql'; import IntegrationsList from './alerts_integrations_list.vue'; import SettingsFormOld from './alerts_settings_form_old.vue'; import SettingsFormNew from './alerts_settings_form_new.vue'; @@ -75,6 +77,9 @@ export default { createFlash({ message: err }); }, }, + currentIntegration: { + query: getCurrentIntegrationQuery, + }, }, data() { return { @@ -87,7 +92,7 @@ export default { loading() { return this.$apollo.queries.integrations.loading; }, - intergrationsOptionsOld() { + integrationsOptionsOld() { return [ { name: s__('AlertSettings|HTTP endpoint'), @@ -208,7 +213,19 @@ export default { }); }, editIntegration({ id }) { - this.currentIntegration = this.integrations.list.find(integration => integration.id === id); + const currentIntegration = this.integrations.list.find(integration => integration.id === id); + this.$apollo.mutate({ + mutation: updateCurrentIntergrationMutation, + variables: { + id: currentIntegration.id, + name: currentIntegration.name, + active: currentIntegration.active, + token: currentIntegration.token, + type: currentIntegration.type, + url: currentIntegration.url, + apiUrl: currentIntegration.apiUrl, + }, + }); }, deleteIntegration({ id }) { const { projectPath } = this; @@ -229,7 +246,7 @@ export default { if (error) { return createFlash({ message: error }); } - this.currentIntegration = null; + this.clearCurrentIntegration(); return createFlash({ message: this.$options.i18n.integrationRemoved, type: FLASH_TYPES.SUCCESS, @@ -243,7 +260,10 @@ export default { }); }, clearCurrentIntegration() { - this.currentIntegration = null; + this.$apollo.mutate({ + mutation: updateCurrentIntergrationMutation, + variables: {}, + }); }, testPayloadFailure() { createFlash({ message: INTEGRATION_PAYLOAD_TEST_ERROR }); @@ -255,16 +275,14 @@ export default { <template> <div> <integrations-list - :integrations="glFeatures.httpIntegrationsList ? integrations.list : intergrationsOptionsOld" + :integrations="glFeatures.httpIntegrationsList ? integrations.list : integrationsOptionsOld" :loading="loading" - :current-integration="currentIntegration" @edit-integration="editIntegration" @delete-integration="deleteIntegration" /> <settings-form-new v-if="glFeatures.httpIntegrationsList" :loading="isUpdating" - :current-integration="currentIntegration" :can-add-integration="canAddIntegration" @create-new-integration="createNewIntegration" @update-integration="updateIntegration" diff --git a/app/assets/javascripts/alerts_settings/graphql.js b/app/assets/javascripts/alerts_settings/graphql.js new file mode 100644 index 00000000000..02c2def87fa --- /dev/null +++ b/app/assets/javascripts/alerts_settings/graphql.js @@ -0,0 +1,44 @@ +import Vue from 'vue'; +import produce from 'immer'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import getCurrentIntegrationQuery from './graphql/queries/get_current_integration.query.graphql'; + +Vue.use(VueApollo); + +const resolvers = { + Mutation: { + updateCurrentIntegration: ( + _, + { id = null, name, active, token, type, url, apiUrl }, + { cache }, + ) => { + const sourceData = cache.readQuery({ query: getCurrentIntegrationQuery }); + const data = produce(sourceData, draftData => { + if (id === null) { + // eslint-disable-next-line no-param-reassign + draftData.currentIntegration = null; + } else { + // eslint-disable-next-line no-param-reassign + draftData.currentIntegration = { + id, + name, + active, + token, + type, + url, + apiUrl, + }; + } + }); + cache.writeQuery({ query: getCurrentIntegrationQuery, data }); + }, + }, +}; + +export default new VueApollo({ + defaultClient: createDefaultClient(resolvers, { + cacheConfig: {}, + assumeImmutableResults: true, + }), +}); diff --git a/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_intergration.mutation.graphql b/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_intergration.mutation.graphql new file mode 100644 index 00000000000..3505241309e --- /dev/null +++ b/app/assets/javascripts/alerts_settings/graphql/mutations/update_current_intergration.mutation.graphql @@ -0,0 +1,19 @@ +mutation updateCurrentIntegration( + $id: String + $name: String + $active: Boolean + $token: String + $type: String + $url: String + $apiUrl: String +) { + updateCurrentIntegration( + id: $id + name: $name + active: $active + token: $token + type: $type + url: $url + apiUrl: $apiUrl + ) @client +} diff --git a/app/assets/javascripts/alerts_settings/graphql/queries/get_current_integration.query.graphql b/app/assets/javascripts/alerts_settings/graphql/queries/get_current_integration.query.graphql new file mode 100644 index 00000000000..4f22849a618 --- /dev/null +++ b/app/assets/javascripts/alerts_settings/graphql/queries/get_current_integration.query.graphql @@ -0,0 +1,3 @@ +query currentIntegration { + currentIntegration @client +} diff --git a/app/assets/javascripts/alerts_settings/index.js b/app/assets/javascripts/alerts_settings/index.js index a9d109b3f3e..5867991a738 100644 --- a/app/assets/javascripts/alerts_settings/index.js +++ b/app/assets/javascripts/alerts_settings/index.js @@ -1,10 +1,13 @@ import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createDefaultClient from '~/lib/graphql'; import { parseBoolean } from '~/lib/utils/common_utils'; import AlertSettingsWrapper from './components/alerts_settings_wrapper.vue'; +import apolloProvider from './graphql'; -Vue.use(VueApollo); +apolloProvider.clients.defaultClient.cache.writeData({ + data: { + currentIntegration: null, + }, +}); export default el => { if (!el) { @@ -32,15 +35,6 @@ export default el => { multiIntegrations, } = el.dataset; - const resolvers = {}; - - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(resolvers, { - cacheConfig: {}, - assumeImmutableResults: true, - }), - }); - return new Vue({ el, provide: { diff --git a/app/assets/javascripts/groups/new_group_child.js b/app/assets/javascripts/groups/new_group_child.js deleted file mode 100644 index bb2aea3ea76..00000000000 --- a/app/assets/javascripts/groups/new_group_child.js +++ /dev/null @@ -1,65 +0,0 @@ -import { visitUrl } from '../lib/utils/url_utility'; -import DropLab from '../droplab/drop_lab'; -import ISetter from '../droplab/plugins/input_setter'; - -const InputSetter = { ...ISetter }; - -const NEW_PROJECT = 'new-project'; -const NEW_SUBGROUP = 'new-subgroup'; - -export default class NewGroupChild { - constructor(buttonWrapper) { - this.buttonWrapper = buttonWrapper; - this.newGroupChildButton = this.buttonWrapper.querySelector('.js-new-group-child'); - this.dropdownToggle = this.buttonWrapper.querySelector('.js-dropdown-toggle'); - this.dropdownList = this.buttonWrapper.querySelector('.dropdown-menu'); - - this.newGroupPath = this.buttonWrapper.dataset.projectPath; - this.subgroupPath = this.buttonWrapper.dataset.subgroupPath; - - this.init(); - } - - init() { - this.initDroplab(); - this.bindEvents(); - } - - initDroplab() { - this.droplab = new DropLab(); - this.droplab.init( - this.dropdownToggle, - this.dropdownList, - [InputSetter], - this.getDroplabConfig(), - ); - } - - getDroplabConfig() { - return { - InputSetter: [ - { - input: this.newGroupChildButton, - valueAttribute: 'data-value', - inputAttribute: 'data-action', - }, - { - input: this.newGroupChildButton, - valueAttribute: 'data-text', - }, - ], - }; - } - - bindEvents() { - this.newGroupChildButton.addEventListener('click', this.onClickNewGroupChildButton.bind(this)); - } - - onClickNewGroupChildButton(e) { - if (e.target.dataset.action === NEW_PROJECT) { - visitUrl(this.newGroupPath); - } else if (e.target.dataset.action === NEW_SUBGROUP) { - visitUrl(this.subgroupPath); - } - } -} diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js index 8546b1f759f..8d956c694c0 100644 --- a/app/assets/javascripts/pages/groups/shared/group_details.js +++ b/app/assets/javascripts/pages/groups/shared/group_details.js @@ -2,7 +2,6 @@ import { getPagePath, getDashPath } from '~/lib/utils/common_utils'; import { ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED } from '~/groups/constants'; -import NewGroupChild from '~/groups/new_group_child'; import notificationsDropdown from '~/notifications_dropdown'; import NotificationsForm from '~/notifications_form'; import ProjectsList from '~/projects_list'; @@ -11,7 +10,6 @@ import GroupTabs from './group_tabs'; import initInviteMembersBanner from '~/groups/init_invite_members_banner'; export default function initGroupDetails(actionName = 'show') { - const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); const loadableActions = [ACTIVE_TAB_SHARED, ACTIVE_TAB_ARCHIVED]; const dashPath = getDashPath(); let action = loadableActions.includes(dashPath) ? dashPath : getPagePath(1); @@ -25,8 +23,5 @@ export default function initGroupDetails(actionName = 'show') { notificationsDropdown(); new ProjectsList(); - if (newGroupChildWrapper) { - new NewGroupChild(newGroupChildWrapper); - } initInviteMembersBanner(); } diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue index 8308ac7e327..b0a7c4824bd 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue @@ -72,7 +72,7 @@ export default { <template #left-primary> <router-link class="gl-text-body gl-font-weight-bold" - data-testid="detailsLink" + data-testid="details-link" :to="{ name: 'details', params: { id: item.id } }" > {{ item.path }} diff --git a/app/assets/javascripts/registry/explorer/stores/actions.js b/app/assets/javascripts/registry/explorer/stores/actions.js index abc0bc8ce8c..c1883095097 100644 --- a/app/assets/javascripts/registry/explorer/stores/actions.js +++ b/app/assets/javascripts/registry/explorer/stores/actions.js @@ -56,7 +56,7 @@ export const requestTagsList = ({ commit, dispatch, state: { imageDetails } }, p dispatch('receiveTagsListSuccess', { data, headers }); }) .catch(() => { - createFlash(FETCH_TAGS_LIST_ERROR_MESSAGE); + createFlash({ message: FETCH_TAGS_LIST_ERROR_MESSAGE }); }) .finally(() => { commit(types.SET_MAIN_LOADING, false); @@ -71,7 +71,7 @@ export const requestImageDetailsAndTagsList = ({ dispatch, commit }, id) => { dispatch('requestTagsList'); }) .catch(() => { - createFlash(FETCH_IMAGE_DETAILS_ERROR_MESSAGE); + createFlash({ message: FETCH_IMAGE_DETAILS_ERROR_MESSAGE }); commit(types.SET_MAIN_LOADING, false); }); }; diff --git a/app/assets/javascripts/registry/explorer/utils.js b/app/assets/javascripts/registry/explorer/utils.js index ff86a6b807d..2c89d508c31 100644 --- a/app/assets/javascripts/registry/explorer/utils.js +++ b/app/assets/javascripts/registry/explorer/utils.js @@ -1,8 +1,16 @@ export const pathGenerator = (imageDetails, ending = '?format=json') => { // this method is a temporary workaround, to be removed with graphql implementation // https://gitlab.com/gitlab-org/gitlab/-/issues/276432 - const basePath = imageDetails.name - ? imageDetails.path.replace(`/${imageDetails.name}`, '') - : imageDetails.path; + + const splitPath = imageDetails.path.split('/').reverse(); + const splitName = imageDetails.name ? imageDetails.name.split('/').reverse() : []; + const basePath = splitPath + .reduce((acc, curr, index) => { + if (splitPath[index] !== splitName[index]) { + acc.unshift(curr); + } + return acc; + }, []) + .join('/'); return `/${basePath}/registry/repository/${imageDetails.id}/tags${ending}`; }; diff --git a/app/assets/javascripts/vue_shared/components/gl_modal.vue b/app/assets/javascripts/vue_shared/components/gl_modal.vue deleted file mode 100644 index 4b91d4c00e3..00000000000 --- a/app/assets/javascripts/vue_shared/components/gl_modal.vue +++ /dev/null @@ -1,6 +0,0 @@ -<script> -// This file was only introduced to not break master and shall be delete soon. -import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; - -export default DeprecatedModal2; -</script> diff --git a/app/controllers/projects/releases_controller.rb b/app/controllers/projects/releases_controller.rb index 4e8260d9e53..a6e795a2b91 100644 --- a/app/controllers/projects/releases_controller.rb +++ b/app/controllers/projects/releases_controller.rb @@ -54,7 +54,7 @@ class Projects::ReleasesController < Projects::ApplicationController end def sanitized_filepath - CGI.unescape(params[:filepath]) + "/#{CGI.unescape(params[:filepath])}" end def sanitized_tag_name diff --git a/app/graphql/mutations/commits/create.rb b/app/graphql/mutations/commits/create.rb index 9ed1bb819c8..2b9107350fd 100644 --- a/app/graphql/mutations/commits/create.rb +++ b/app/graphql/mutations/commits/create.rb @@ -13,7 +13,11 @@ module Mutations argument :branch, GraphQL::STRING_TYPE, required: true, - description: 'Name of the branch' + description: 'Name of the branch to commit into, it can be a new branch' + + argument :start_branch, GraphQL::STRING_TYPE, + required: false, + description: 'If on a new branch, name of the original branch' argument :message, GraphQL::STRING_TYPE, @@ -32,13 +36,13 @@ module Mutations authorize :push_code - def resolve(project_path:, branch:, message:, actions:) + def resolve(project_path:, branch:, message:, actions:, **args) project = authorized_find!(full_path: project_path) attributes = { commit_message: message, branch_name: branch, - start_branch: branch, + start_branch: args[:start_branch] || branch, actions: actions.map { |action| action.to_h } } diff --git a/app/graphql/types/release_asset_link_type.rb b/app/graphql/types/release_asset_link_type.rb index 0e519ece791..8fb051f5627 100644 --- a/app/graphql/types/release_asset_link_type.rb +++ b/app/graphql/types/release_asset_link_type.rb @@ -24,10 +24,8 @@ module Types def direct_asset_url return object.url unless object.filepath - release = object.release - project = release.project - - Gitlab::Routing.url_helpers.project_release_url(project, release) << object.filepath + release = object.release.present + release.download_url(object.filepath) end end end diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb index 80963ff3304..34030e079c7 100644 --- a/app/models/bulk_imports/entity.rb +++ b/app/models/bulk_imports/entity.rb @@ -26,6 +26,10 @@ class BulkImports::Entity < ApplicationRecord belongs_to :project, optional: true belongs_to :group, foreign_key: :namespace_id, optional: true + has_many :trackers, + class_name: 'BulkImports::Tracker', + foreign_key: :bulk_import_entity_id + validates :project, absence: true, if: :group validates :group, absence: true, if: :project validates :source_type, :source_full_path, :destination_name, diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb new file mode 100644 index 00000000000..02e0904e1af --- /dev/null +++ b/app/models/bulk_imports/tracker.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# This model is responsible for keeping track of the requests/pagination +# happening during a Group Migration (BulkImport). +class BulkImports::Tracker < ApplicationRecord + self.table_name = 'bulk_import_trackers' + + belongs_to :entity, + class_name: 'BulkImports::Entity', + foreign_key: :bulk_import_entity_id, + optional: false + + validates :relation, + presence: true, + uniqueness: { scope: :bulk_import_entity_id } + + validates :next_page, presence: { if: :has_next_page? } +end diff --git a/app/models/design_management/design.rb b/app/models/design_management/design.rb index 9f2eada0890..f5e52c04944 100644 --- a/app/models/design_management/design.rb +++ b/app/models/design_management/design.rb @@ -146,12 +146,6 @@ module DesignManagement strong_memoize(:most_recent_action) { actions.ordered.last } end - def participants(current_user = nil) - return [] unless Feature.enabled?(:design_management_design_notification_participants, project) - - super - end - # A reference for a design is the issue reference, indexed by the filename # with an optional infix when full. # diff --git a/app/presenters/release_presenter.rb b/app/presenters/release_presenter.rb index 9d7e183aaf3..b11585d0d1c 100644 --- a/app/presenters/release_presenter.rb +++ b/app/presenters/release_presenter.rb @@ -71,6 +71,12 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated can_download_code? ? release.name : "Release-#{release.id}" end + def download_url(filepath) + filepath = filepath.sub(%r{^/}, '') if filepath.start_with?('/') + + downloads_project_release_url(project, release, filepath) + end + private def can_download_code? diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml index a50327ecbec..ee08829d990 100644 --- a/app/views/groups/_home_panel.html.haml +++ b/app/views/groups/_home_panel.html.haml @@ -20,37 +20,16 @@ %span.access-request-links.gl-ml-3 = render 'shared/members/access_request_links', source: @group - .home-panel-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end + .home-panel-buttons.col-md-12.col-lg-6 - if current_user - .group-buttons - = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn', emails_disabled: emails_disabled - - new_project_label = _("New project") - - new_subgroup_label = _("New subgroup") - - if can_create_projects and can_create_subgroups - .btn-group.new-project-subgroup.droplab-dropdown.home-panel-action-button.gl-mt-3.js-new-project-subgroup.qa-new-project-or-subgroup-dropdown{ data: { project_path: new_project_path(namespace_id: @group.id), subgroup_path: new_group_path(parent_id: @group.id) } } - %input.btn.btn-success.dropdown-primary.js-new-group-child.qa-new-in-group-button{ type: "button", value: new_project_label, data: { action: "new-project" } } - %button.btn.btn-success.dropdown-toggle.js-dropdown-toggle.qa-new-project-or-subgroup-dropdown-toggle{ type: "button", data: { "dropdown-trigger" => "#new-project-or-subgroup-dropdown", 'display' => 'static' } } - = sprite_icon("chevron-down", css_class: "icon dropdown-btn-icon") - %ul#new-project-or-subgroup-dropdown.dropdown-menu.dropdown-menu-right{ data: { dropdown: true } } - %li.droplab-item-selected.qa-new-project-option{ role: "button", data: { value: "new-project", text: new_project_label } } - .menu-item - .icon-container - = sprite_icon("check", css_class: "list-item-checkmark") - .description - %strong= new_project_label - %span= s_("GroupsTree|Create a project in this group.") - %li.divider.droplap-item-ignore - %li.qa-new-subgroup-option{ role: "button", data: { value: "new-subgroup", text: new_subgroup_label } } - .menu-item - .icon-container - = sprite_icon("check", css_class: "list-item-checkmark") - .description - %strong= new_subgroup_label - %span= s_("GroupsTree|Create a subgroup in this group.") - - elsif can_create_projects - = link_to new_project_label, new_project_path(namespace_id: @group.id), class: "btn btn-success gl-mt-3" - - elsif can_create_subgroups - = link_to new_subgroup_label, new_group_path(parent_id: @group.id), class: "btn btn-success gl-mt-3" + .gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2{ data: { testid: 'group-buttons' } } + = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn gl-button gl-sm-w-auto gl-w-full', dropdown_container_class: 'gl-mr-0 gl-px-2 gl-sm-w-auto gl-w-full', emails_disabled: emails_disabled + - if can_create_subgroups + .gl-px-2.gl-sm-w-auto.gl-w-full + = link_to _("New subgroup"), new_group_path(parent_id: @group.id), class: "btn btn-success btn-md gl-button btn-success-secondary gl-mt-3 gl-sm-w-auto gl-w-full", data: { qa_selector: 'new_subgroup_button' } + - if can_create_projects + .gl-px-2.gl-sm-w-auto.gl-w-full + = link_to _("New project"), new_project_path(namespace_id: @group.id), class: "btn btn-success btn-md gl-button gl-mt-3 gl-sm-w-auto gl-w-full", data: { qa_selector: 'new_project_button' } - if @group.description.present? .group-home-desc.mt-1 diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index eb847bea95b..0c33cefc65a 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -44,7 +44,7 @@ .project-repo-buttons.col-md-12.col-lg-6.d-inline-flex.flex-wrap.justify-content-lg-end - if current_user .d-inline-flex - = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs', emails_disabled: emails_disabled + = render 'shared/notifications/new_button', notification_setting: @notification_setting, btn_class: 'btn-xs', dropdown_container_class: 'gl-mr-3', emails_disabled: emails_disabled .count-buttons.d-inline-flex = render 'projects/buttons/star' diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml index 80ce4b28931..690f0fe10f7 100644 --- a/app/views/projects/buttons/_star.html.haml +++ b/app/views/projects/buttons/_star.html.haml @@ -13,7 +13,7 @@ - else .count-badge.d-inline-flex.align-item-stretch.gl-mr-3 - = link_to new_user_session_path, class: 'gl-button btn btn-default btn-xs has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do + = link_to new_user_session_path, class: 'btn btn-default btn-xs has-tooltip count-badge-button d-flex align-items-center star-btn', title: s_('ProjectOverview|You must sign in to star a project') do = sprite_icon('star-o', css_class: 'icon') %span= s_('ProjectOverview|Star') %span.star-count.count-badge-count.d-flex.align-items-center diff --git a/app/views/shared/notifications/_new_button.html.haml b/app/views/shared/notifications/_new_button.html.haml index fbcfec5fd96..14f4b04ef78 100644 --- a/app/views/shared/notifications/_new_button.html.haml +++ b/app/views/shared/notifications/_new_button.html.haml @@ -1,4 +1,5 @@ - btn_class = local_assigns.fetch(:btn_class, '') +- dropdown_container_class = local_assigns.fetch(:dropdown_container_class, '') - emails_disabled = local_assigns.fetch(:emails_disabled, false) - if notification_setting @@ -8,8 +9,8 @@ - else - button_title = _("Notification setting - %{notification_title}") % { notification_title: notification_title(notification_setting.level) } - .js-notification-dropdown.notification-dropdown.home-panel-action-button.gl-mt-3.gl-mr-3.dropdown.inline - = form_for notification_setting, remote: true, html: { class: "inline notification-form no-label" } do |f| + .js-notification-dropdown.notification-dropdown.home-panel-action-button.gl-mt-3.dropdown.inline{ class: dropdown_container_class } + = form_for notification_setting, remote: true, html: { class: "notification-form no-label" } do |f| = hidden_setting_source_input(notification_setting) = hidden_field_tag "hide_label", true = f.hidden_field :level, class: "notification_setting_level" diff --git a/changelogs/unreleased/217553-permanent-links-to-release-assets-not-found-404.yml b/changelogs/unreleased/217553-permanent-links-to-release-assets-not-found-404.yml new file mode 100644 index 00000000000..95fc65e5779 --- /dev/null +++ b/changelogs/unreleased/217553-permanent-links-to-release-assets-not-found-404.yml @@ -0,0 +1,5 @@ +--- +title: Fix release assets link redirection +merge_request: 35381 +author: +type: fixed diff --git a/changelogs/unreleased/245263-mr-refs-cleanup-old-mrs.yml b/changelogs/unreleased/245263-mr-refs-cleanup-old-mrs.yml new file mode 100644 index 00000000000..6e105559cfb --- /dev/null +++ b/changelogs/unreleased/245263-mr-refs-cleanup-old-mrs.yml @@ -0,0 +1,5 @@ +--- +title: Backfill cleanup schedules for old closed/merged MRs +merge_request: 46782 +author: +type: other diff --git a/changelogs/unreleased/263144-commit-to-new-branch.yml b/changelogs/unreleased/263144-commit-to-new-branch.yml new file mode 100644 index 00000000000..3248b635630 --- /dev/null +++ b/changelogs/unreleased/263144-commit-to-new-branch.yml @@ -0,0 +1,5 @@ +--- +title: Extend GraphQL API to commit to a new branch in a single operation +merge_request: 47203 +author: +type: changed diff --git a/changelogs/unreleased/267113-separate-new-group-project-creation-button.yml b/changelogs/unreleased/267113-separate-new-group-project-creation-button.yml new file mode 100644 index 00000000000..c4d46ad74c4 --- /dev/null +++ b/changelogs/unreleased/267113-separate-new-group-project-creation-button.yml @@ -0,0 +1,5 @@ +--- +title: Move "New subgroup" and "New project" out of the dropdown into individual buttons +merge_request: 46907 +author: +type: changed diff --git a/changelogs/unreleased/ab-add-missing-expression-indexes.yml b/changelogs/unreleased/ab-add-missing-expression-indexes.yml new file mode 100644 index 00000000000..6e6d6b8f5c4 --- /dev/null +++ b/changelogs/unreleased/ab-add-missing-expression-indexes.yml @@ -0,0 +1,5 @@ +--- +title: Add missing expression indexes +merge_request: 47424 +author: +type: performance diff --git a/changelogs/unreleased/add-nulls-last-to-db-index.yml b/changelogs/unreleased/add-nulls-last-to-db-index.yml new file mode 100644 index 00000000000..f1fc7b04785 --- /dev/null +++ b/changelogs/unreleased/add-nulls-last-to-db-index.yml @@ -0,0 +1,5 @@ +--- +title: Add NULLS LAST to index on merge request metrics +merge_request: 47300 +author: +type: performance diff --git a/changelogs/unreleased/kassio-bulkimport-entity-trackers.yml b/changelogs/unreleased/kassio-bulkimport-entity-trackers.yml new file mode 100644 index 00000000000..152f9c5c5b9 --- /dev/null +++ b/changelogs/unreleased/kassio-bulkimport-entity-trackers.yml @@ -0,0 +1,6 @@ +--- +title: Add BulkImport::Tracker to store the pagination information of the Group Migration + (BulkImport) requests +merge_request: 47009 +author: +type: changed diff --git a/config/feature_flags/development/ci_lint_creates_pipeline_with_dry_run.yml b/config/feature_flags/development/ci_lint_creates_pipeline_with_dry_run.yml deleted file mode 100644 index 935382989ed..00000000000 --- a/config/feature_flags/development/ci_lint_creates_pipeline_with_dry_run.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: ci_lint_creates_pipeline_with_dry_run -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37828 -rollout_issue_url: -milestone: '13.3' -type: development -group: group::continuous integration -default_enabled: true diff --git a/config/feature_flags/development/design_management_design_notification_participants.yml b/config/feature_flags/development/design_management_design_notification_participants.yml deleted file mode 100644 index a91d61ca44d..00000000000 --- a/config/feature_flags/development/design_management_design_notification_participants.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: design_management_design_notification_participants -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46642 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/277319 -milestone: '13.6' -type: development -group: group::knowledge -default_enabled: false diff --git a/db/migrate/20201104204739_create_bulk_import_trackers.rb b/db/migrate/20201104204739_create_bulk_import_trackers.rb new file mode 100644 index 00000000000..906bd06248d --- /dev/null +++ b/db/migrate/20201104204739_create_bulk_import_trackers.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class CreateBulkImportTrackers < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + with_lock_retries do + unless table_exists?(:bulk_import_trackers) + create_table :bulk_import_trackers do |t| + t.references :bulk_import_entity, + null: false, + index: false, + foreign_key: { on_delete: :cascade } + + t.text :relation, null: false + t.text :next_page + t.boolean :has_next_page, default: false, null: false + + t.index %w(bulk_import_entity_id relation), + unique: true, + name: :bulk_import_trackers_uniq_relation_by_entity + end + end + end + + add_check_constraint :bulk_import_trackers, + '(has_next_page IS FALSE or next_page IS NOT NULL)', + 'check_next_page_requirement' + add_text_limit :bulk_import_trackers, :relation, 255 + add_text_limit :bulk_import_trackers, :next_page, 255 + end + + def down + with_lock_retries do + drop_table :bulk_import_trackers + end + end +end diff --git a/db/migrate/20201110133629_change_index_mr_metrics_target_project_id.rb b/db/migrate/20201110133629_change_index_mr_metrics_target_project_id.rb new file mode 100644 index 00000000000..5bd810207d8 --- /dev/null +++ b/db/migrate/20201110133629_change_index_mr_metrics_target_project_id.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class ChangeIndexMrMetricsTargetProjectId < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + disable_ddl_transaction! + + DOWNTIME = false + INDEX_NAME = 'index_merge_request_metrics_on_target_project_id_merged_at' + NULLS_LAST_INDEX_NAME = 'index_mr_metrics_on_target_project_id_merged_at_nulls_last' + + def up + add_concurrent_index :merge_request_metrics, [:target_project_id, :merged_at, :id], order: { merged_at: 'DESC NULLS LAST', id: 'DESC' }, name: NULLS_LAST_INDEX_NAME + remove_concurrent_index_by_name(:merge_request_metrics, INDEX_NAME) + end + + def down + add_concurrent_index :merge_request_metrics, [:target_project_id, :merged_at], name: INDEX_NAME + remove_concurrent_index_by_name(:merge_request_metrics, NULLS_LAST_INDEX_NAME) + end +end diff --git a/db/migrate/20201111152859_add_missing_expression_indexes.rb b/db/migrate/20201111152859_add_missing_expression_indexes.rb new file mode 100644 index 00000000000..e2742f7f3bc --- /dev/null +++ b/db/migrate/20201111152859_add_missing_expression_indexes.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +class AddMissingExpressionIndexes < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + INDEXES = [ + [:namespaces, :index_on_namespaces_lower_name, 'LOWER(name)'], + [:namespaces, :index_on_namespaces_lower_path, 'LOWER(path)'], + [:projects, :index_on_projects_lower_path, 'LOWER(path)'], + [:routes, :index_on_routes_lower_path, 'LOWER(path)'], + [:users, :index_on_users_lower_username, 'LOWER(username)'], + [:users, :index_on_users_lower_email, 'LOWER(email)'] + ] + + def up + # Those indexes had been introduced before, but they haven't been + # captured in structure.sql. For installations that already have it, + # this is a no-op - others will get it retroactively with + # this migration. + + tables = Set.new + + INDEXES.each do |(table, name, expression)| + unless index_name_exists?(table, name) + add_concurrent_index table, expression, name: name + tables.add(table) + end + end + + # Rebuild statistics on affected tables only + tables.each do |table| + execute("ANALYZE #{table}") + end + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20201103110018_schedule_merge_request_cleanup_schedules_backfill.rb b/db/post_migrate/20201103110018_schedule_merge_request_cleanup_schedules_backfill.rb new file mode 100644 index 00000000000..77057205b09 --- /dev/null +++ b/db/post_migrate/20201103110018_schedule_merge_request_cleanup_schedules_backfill.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +class ScheduleMergeRequestCleanupSchedulesBackfill < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + MIGRATION = 'BackfillMergeRequestCleanupSchedules' + DELAY_INTERVAL = 2.minutes + BATCH_SIZE = 10_000 + TEMP_INDEX_NAME = 'merge_requests_state_id_temp_index' + + disable_ddl_transaction! + + def up + add_concurrent_index :merge_requests, :id, name: TEMP_INDEX_NAME, where: "state_id IN (2, 3)" + + eligible_mrs = Gitlab::BackgroundMigration::BackfillMergeRequestCleanupSchedules::MergeRequest.eligible + + queue_background_migration_jobs_by_range_at_intervals( + eligible_mrs, + MIGRATION, + DELAY_INTERVAL, + batch_size: BATCH_SIZE + ) + end + + def down + remove_concurrent_index_by_name :merge_requests, TEMP_INDEX_NAME + end +end diff --git a/db/schema_migrations/20201103110018 b/db/schema_migrations/20201103110018 new file mode 100644 index 00000000000..82ab2fda543 --- /dev/null +++ b/db/schema_migrations/20201103110018 @@ -0,0 +1 @@ +8a45a6186d7e18f1dea43593dc0226860fc2e8d3ae386f50a022958b758c7c75
\ No newline at end of file diff --git a/db/schema_migrations/20201104204739 b/db/schema_migrations/20201104204739 new file mode 100644 index 00000000000..83794fd569f --- /dev/null +++ b/db/schema_migrations/20201104204739 @@ -0,0 +1 @@ +9431c771b14d61851e8e69b3a789f222463bbe460078a35c8ad3cbcf8df8b077
\ No newline at end of file diff --git a/db/schema_migrations/20201110133629 b/db/schema_migrations/20201110133629 new file mode 100644 index 00000000000..487b5eab7c1 --- /dev/null +++ b/db/schema_migrations/20201110133629 @@ -0,0 +1 @@ +83773b825db9b2671fd4ffb2c0d6733036737385ce7a933040011026b34ba1e1
\ No newline at end of file diff --git a/db/schema_migrations/20201111152859 b/db/schema_migrations/20201111152859 new file mode 100644 index 00000000000..2d5b337b857 --- /dev/null +++ b/db/schema_migrations/20201111152859 @@ -0,0 +1 @@ +4c5baa6a09a339fac544f830d5ef822b1e7e4eae8431bd91df5113125accbc77
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 0d913c08f08..a7334cd10f2 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9918,6 +9918,26 @@ CREATE SEQUENCE bulk_import_entities_id_seq ALTER SEQUENCE bulk_import_entities_id_seq OWNED BY bulk_import_entities.id; +CREATE TABLE bulk_import_trackers ( + id bigint NOT NULL, + bulk_import_entity_id bigint NOT NULL, + relation text NOT NULL, + next_page text, + has_next_page boolean DEFAULT false NOT NULL, + CONSTRAINT check_2d45cae629 CHECK ((char_length(relation) <= 255)), + CONSTRAINT check_40aeaa600b CHECK ((char_length(next_page) <= 255)), + CONSTRAINT check_next_page_requirement CHECK (((has_next_page IS FALSE) OR (next_page IS NOT NULL))) +); + +CREATE SEQUENCE bulk_import_trackers_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE bulk_import_trackers_id_seq OWNED BY bulk_import_trackers.id; + CREATE TABLE bulk_imports ( id bigint NOT NULL, user_id integer NOT NULL, @@ -17621,6 +17641,8 @@ ALTER TABLE ONLY bulk_import_configurations ALTER COLUMN id SET DEFAULT nextval( ALTER TABLE ONLY bulk_import_entities ALTER COLUMN id SET DEFAULT nextval('bulk_import_entities_id_seq'::regclass); +ALTER TABLE ONLY bulk_import_trackers ALTER COLUMN id SET DEFAULT nextval('bulk_import_trackers_id_seq'::regclass); + ALTER TABLE ONLY bulk_imports ALTER COLUMN id SET DEFAULT nextval('bulk_imports_id_seq'::regclass); ALTER TABLE ONLY chat_names ALTER COLUMN id SET DEFAULT nextval('chat_names_id_seq'::regclass); @@ -18631,6 +18653,9 @@ ALTER TABLE ONLY bulk_import_configurations ALTER TABLE ONLY bulk_import_entities ADD CONSTRAINT bulk_import_entities_pkey PRIMARY KEY (id); +ALTER TABLE ONLY bulk_import_trackers + ADD CONSTRAINT bulk_import_trackers_pkey PRIMARY KEY (id); + ALTER TABLE ONLY bulk_imports ADD CONSTRAINT bulk_imports_pkey PRIMARY KEY (id); @@ -19914,6 +19939,8 @@ CREATE INDEX backup_labels_title_idx ON backup_labels USING btree (title); CREATE INDEX backup_labels_type_project_id_idx ON backup_labels USING btree (type, project_id); +CREATE UNIQUE INDEX bulk_import_trackers_uniq_relation_by_entity ON bulk_import_trackers USING btree (bulk_import_entity_id, relation); + CREATE INDEX ci_builds_gitlab_monitor_metrics ON ci_builds USING btree (status, created_at, project_id) WHERE ((type)::text = 'Ci::Build'::text); CREATE INDEX code_owner_approval_required ON protected_branches USING btree (project_id, code_owner_approval_required) WHERE (code_owner_approval_required = true); @@ -21136,8 +21163,6 @@ CREATE INDEX index_merge_request_metrics_on_pipeline_id ON merge_request_metrics CREATE INDEX index_merge_request_metrics_on_target_project_id ON merge_request_metrics USING btree (target_project_id); -CREATE INDEX index_merge_request_metrics_on_target_project_id_merged_at ON merge_request_metrics USING btree (target_project_id, merged_at); - CREATE UNIQUE INDEX index_merge_request_reviewers_on_merge_request_id_and_user_id ON merge_request_reviewers USING btree (merge_request_id, user_id); CREATE INDEX index_merge_request_reviewers_on_user_id ON merge_request_reviewers USING btree (user_id); @@ -21228,6 +21253,8 @@ CREATE INDEX index_mr_cleanup_schedules_timestamps ON merge_request_cleanup_sche CREATE UNIQUE INDEX index_mr_context_commits_on_merge_request_id_and_sha ON merge_request_context_commits USING btree (merge_request_id, sha); +CREATE INDEX index_mr_metrics_on_target_project_id_merged_at_nulls_last ON merge_request_metrics USING btree (target_project_id, merged_at DESC NULLS LAST, id DESC); + CREATE UNIQUE INDEX index_namespace_aggregation_schedules_on_namespace_id ON namespace_aggregation_schedules USING btree (namespace_id); CREATE UNIQUE INDEX index_namespace_root_storage_statistics_on_namespace_id ON namespace_root_storage_statistics USING btree (namespace_id); @@ -21326,12 +21353,24 @@ CREATE UNIQUE INDEX index_on_instance_statistics_recorded_at_and_identifier ON a CREATE INDEX index_on_label_links_all_columns ON label_links USING btree (target_id, label_id, target_type); +CREATE INDEX index_on_namespaces_lower_name ON namespaces USING btree (lower((name)::text)); + +CREATE INDEX index_on_namespaces_lower_path ON namespaces USING btree (lower((path)::text)); + +CREATE INDEX index_on_projects_lower_path ON projects USING btree (lower((path)::text)); + +CREATE INDEX index_on_routes_lower_path ON routes USING btree (lower((path)::text)); + CREATE UNIQUE INDEX index_on_segment_selections_group_id_segment_id ON analytics_devops_adoption_segment_selections USING btree (group_id, segment_id); CREATE UNIQUE INDEX index_on_segment_selections_project_id_segment_id ON analytics_devops_adoption_segment_selections USING btree (project_id, segment_id); CREATE INDEX index_on_segment_selections_segment_id ON analytics_devops_adoption_segment_selections USING btree (segment_id); +CREATE INDEX index_on_users_lower_email ON users USING btree (lower((email)::text)); + +CREATE INDEX index_on_users_lower_username ON users USING btree (lower((username)::text)); + CREATE INDEX index_on_users_name_lower ON users USING btree (lower((name)::text)); CREATE INDEX index_open_project_tracker_data_on_service_id ON open_project_tracker_data USING btree (service_id); @@ -22248,6 +22287,8 @@ CREATE UNIQUE INDEX merge_request_user_mentions_on_mr_id_and_note_id_index ON me CREATE UNIQUE INDEX merge_request_user_mentions_on_mr_id_index ON merge_request_user_mentions USING btree (merge_request_id) WHERE (note_id IS NULL); +CREATE INDEX merge_requests_state_id_temp_index ON merge_requests USING btree (id) WHERE (state_id = ANY (ARRAY[2, 3])); + CREATE INDEX note_mentions_temp_index ON notes USING btree (id, noteable_type) WHERE (note ~~ '%@%'::text); CREATE UNIQUE INDEX one_canonical_wiki_page_slug_per_metadata ON wiki_page_slugs USING btree (wiki_page_meta_id) WHERE (canonical = true); @@ -24122,6 +24163,9 @@ ALTER TABLE ONLY analytics_cycle_analytics_group_stages ALTER TABLE ONLY metrics_dashboard_annotations ADD CONSTRAINT fk_rails_aeb11a7643 FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE CASCADE; +ALTER TABLE ONLY bulk_import_trackers + ADD CONSTRAINT fk_rails_aed566d3f3 FOREIGN KEY (bulk_import_entity_id) REFERENCES bulk_import_entities(id) ON DELETE CASCADE; + ALTER TABLE ONLY pool_repositories ADD CONSTRAINT fk_rails_af3f8c5d62 FOREIGN KEY (shard_id) REFERENCES shards(id) ON DELETE RESTRICT; diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index ad6ce5501d6..e13412fabb8 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2996,7 +2996,7 @@ input CommitCreateInput { actions: [CommitAction!]! """ - Name of the branch + Name of the branch to commit into, it can be a new branch """ branch: String! @@ -3014,6 +3014,11 @@ input CommitCreateInput { Project full path the branch is associated with """ projectPath: ID! + + """ + If on a new branch, name of the original branch + """ + startBranch: String } """ diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index fca75870115..841224447d6 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -8114,7 +8114,7 @@ }, { "name": "branch", - "description": "Name of the branch", + "description": "Name of the branch to commit into, it can be a new branch", "type": { "kind": "NON_NULL", "name": null, @@ -8127,6 +8127,16 @@ "defaultValue": null }, { + "name": "startBranch", + "description": "If on a new branch, name of the original branch", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { "name": "message", "description": "Raw commit message", "type": { diff --git a/doc/user/group/img/create_new_project_from_group_v13_6.png b/doc/user/group/img/create_new_project_from_group_v13_6.png Binary files differnew file mode 100644 index 00000000000..72d19817686 --- /dev/null +++ b/doc/user/group/img/create_new_project_from_group_v13_6.png diff --git a/doc/user/group/index.md b/doc/user/group/index.md index d1149a95f4e..f05bf7b487e 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -235,7 +235,7 @@ There are two different ways to add a new project to a group: - Select a group, and then click **New project**. You can then continue [creating your project](../../gitlab-basics/create-project.md). - ![New project](img/create_new_project_from_group.png) + ![New project](img/create_new_project_from_group_v13_6.png) - While you are creating a project, select a group namespace you've already created from the dropdown menu. diff --git a/doc/user/group/subgroups/img/create_subgroup_button.png b/doc/user/group/subgroups/img/create_subgroup_button.png Binary files differdeleted file mode 100644 index d1355d4b2c3..00000000000 --- a/doc/user/group/subgroups/img/create_subgroup_button.png +++ /dev/null diff --git a/doc/user/group/subgroups/img/create_subgroup_button_v13_6.png b/doc/user/group/subgroups/img/create_subgroup_button_v13_6.png Binary files differnew file mode 100644 index 00000000000..013aee6b0b4 --- /dev/null +++ b/doc/user/group/subgroups/img/create_subgroup_button_v13_6.png diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md index 031815d4d1a..268014a3cd2 100644 --- a/doc/user/group/subgroups/index.md +++ b/doc/user/group/subgroups/index.md @@ -99,10 +99,9 @@ creation is disabled by an administrator in their settings. To create a subgroup: -1. In the group's dashboard expand the **New project** split button, select - **New subgroup** and click the **New subgroup** button. +1. In the group's dashboard click the **New subgroup** button. - ![Subgroups page](img/create_subgroup_button.png) + ![Subgroups page](img/create_subgroup_button_v13_6.png) 1. Create a new group like you would normally do. Notice that the immediate parent group namespace is fixed under **Group path**. The visibility level can differ from diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md index 64bc6e48b81..f3d27147557 100644 --- a/doc/user/profile/notifications.md +++ b/doc/user/profile/notifications.md @@ -213,14 +213,7 @@ then that user will also be notified. ## Design email notifications -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217095) in GitLab 13.6. -> - It's [deployed behind a feature flag](../../user/feature_flags.md), disabled by default. -> - It's disabled on GitLab.com. -> - It's not recommended for production use. -> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-design-email-notifications). **(CORE ONLY)** - -CAUTION: **Warning:** -This feature might not be available to you. Check the **version history** note above for details. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217095) in GitLab 13.6. Email notifications are sent to the participants when comments are made on a design. @@ -230,25 +223,6 @@ The participants are: - Authors of comments on the design. - Anyone that is `@mentioned` in a comment on the design. -### Enable or disable design email notifications **(CORE ONLY)** - -The design email notifications feature is under development and not ready for production use. It is -deployed behind a feature flag that is **disabled by default**. -[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) -can enable it. - -To enable it: - -```ruby -Feature.enable(:design_management_design_notification_participants) -``` - -To disable it: - -```ruby -Feature.disable(:design_management_design_notification_participants) -``` - ## Filtering email Notification email messages include GitLab-specific headers. You can filter the notification emails based on the content of these headers to better manage your notifications. For example, you could filter all emails for a specific project where you are being assigned either a merge request or issue. diff --git a/doc/user/project/static_site_editor/index.md b/doc/user/project/static_site_editor/index.md index 8a2f62ec7a2..47b18863e4d 100644 --- a/doc/user/project/static_site_editor/index.md +++ b/doc/user/project/static_site_editor/index.md @@ -80,6 +80,7 @@ codebase. ## Edit content > - Support for modifying the default merge request title and description [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216861) in GitLab 13.5. +> - Support for selecting a merge request template [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/263252) in GitLab 13.6. After setting up your project, you can start editing content directly from the Static Site Editor. @@ -91,7 +92,9 @@ To edit a file: wish to edit the raw Markdown instead, you can toggle the **Markdown** mode in the bottom-right corner. 1. When you're done, click **Submit changes...**. -1. (Optional) Adjust the default title and description of the merge request that will be submitted with your changes. +1. (Optional) Adjust the default title and description of the merge request that will be submitted + with your changes. Alternatively, select a [merge request template](../../../user/project/description_templates.md#creating-merge-request-templates) + from the dropdown menu and edit it accordingly. 1. Click **Submit changes**. 1. A new merge request is automatically created and you can assign a colleague for review. diff --git a/lib/api/entities/releases/link.rb b/lib/api/entities/releases/link.rb index 654df2e2caf..c1d83a8924f 100644 --- a/lib/api/entities/releases/link.rb +++ b/lib/api/entities/releases/link.rb @@ -14,10 +14,8 @@ module API def direct_asset_url return object.url unless object.filepath - release = object.release - project = release.project - - Gitlab::Routing.url_helpers.project_release_url(project, release) << object.filepath + release = object.release.present + release.download_url(object.filepath) end end end diff --git a/lib/gitlab/background_migration/backfill_merge_request_cleanup_schedules.rb b/lib/gitlab/background_migration/backfill_merge_request_cleanup_schedules.rb new file mode 100644 index 00000000000..8a58cf9b302 --- /dev/null +++ b/lib/gitlab/background_migration/backfill_merge_request_cleanup_schedules.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # Backfill merge request cleanup schedules of closed/merged merge requests + # without any corresponding records. + class BackfillMergeRequestCleanupSchedules + # Model used for migration added in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/46782. + class MergeRequest < ActiveRecord::Base + include EachBatch + + self.table_name = 'merge_requests' + + def self.eligible + where('merge_requests.state_id IN (2, 3)') + end + end + + def perform(start_id, end_id) + eligible_mrs = MergeRequest.eligible.where(id: start_id..end_id) + scheduled_at_column = "COALESCE(metrics.merged_at, COALESCE(metrics.latest_closed_at, merge_requests.updated_at)) + interval '14 days'" + query = + eligible_mrs + .select("merge_requests.id, #{scheduled_at_column}, NOW(), NOW()") + .joins('LEFT JOIN merge_request_metrics metrics ON metrics.merge_request_id = merge_requests.id') + + result = ActiveRecord::Base.connection.execute <<~SQL + INSERT INTO merge_request_cleanup_schedules (merge_request_id, scheduled_at, created_at, updated_at) + #{query.to_sql} + ON CONFLICT (merge_request_id) DO NOTHING; + SQL + + ::Gitlab::BackgroundMigration::Logger.info( + message: 'Backfilled merge_request_cleanup_schedules records', + count: result.cmd_tuples + ) + end + end + end +end diff --git a/lib/gitlab/ci/features.rb b/lib/gitlab/ci/features.rb index 9b109df9fe0..661189eea50 100644 --- a/lib/gitlab/ci/features.rb +++ b/lib/gitlab/ci/features.rb @@ -38,10 +38,6 @@ module Gitlab ::Feature.enabled?(:ci_disallow_to_create_merge_request_pipelines_in_target_project, target_project) end - def self.lint_creates_pipeline_with_dry_run?(project) - ::Feature.enabled?(:ci_lint_creates_pipeline_with_dry_run, project, default_enabled: true) - end - def self.project_transactionless_destroy?(project) Feature.enabled?(:project_transactionless_destroy, project, default_enabled: false) end diff --git a/lib/gitlab/ci/lint.rb b/lib/gitlab/ci/lint.rb index 44f2ac23ce3..fb795152abe 100644 --- a/lib/gitlab/ci/lint.rb +++ b/lib/gitlab/ci/lint.rb @@ -24,7 +24,7 @@ module Gitlab end def validate(content, dry_run: false) - if dry_run && Gitlab::Ci::Features.lint_creates_pipeline_with_dry_run?(@project) + if dry_run simulate_pipeline_creation(content) else static_validation(content) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a65fec265d3..82fdd0fe1cc 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -13565,12 +13565,6 @@ msgstr "" msgid "GroupsTree|Are you sure you want to leave the \"%{fullName}\" group?" msgstr "" -msgid "GroupsTree|Create a project in this group." -msgstr "" - -msgid "GroupsTree|Create a subgroup in this group." -msgstr "" - msgid "GroupsTree|Edit group" msgstr "" diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb index 7639def98b7..38d919be4db 100644 --- a/qa/qa/page/group/show.rb +++ b/qa/qa/page/group/show.rb @@ -7,11 +7,8 @@ module QA include Page::Component::GroupsFilter view 'app/views/groups/_home_panel.html.haml' do - element :new_project_or_subgroup_dropdown - element :new_project_or_subgroup_dropdown_toggle - element :new_project_option - element :new_subgroup_option - element :new_in_group_button + element :new_project_button + element :new_subgroup_button end view 'app/assets/javascripts/groups/constants.js' do @@ -26,8 +23,9 @@ module QA click_link name end - def has_new_project_or_subgroup_dropdown? - has_element?(:new_project_or_subgroup_dropdown) + def has_new_project_and_new_subgroup_buttons? + has_element?(:new_project_button) + has_element?(:new_subgroup_button) end def has_subgroup?(name) @@ -35,15 +33,11 @@ module QA end def go_to_new_subgroup - select_kind :new_subgroup_option - - click_element :new_in_group_button + click_element :new_subgroup_button end def go_to_new_project - select_kind :new_project_option - - click_element :new_in_group_button + click_element :new_project_button end def leave_group @@ -51,23 +45,6 @@ module QA click_element :leave_group_link end end - - private - - def select_kind(kind) - QA::Support::Retrier.retry_on_exception(sleep_interval: 1.0) do - within_element(:new_project_or_subgroup_dropdown) do - # May need to click again because it is possible to click the button quicker than the JS is bound - wait_until(reload: false) do - click_element :new_project_or_subgroup_dropdown_toggle - - has_element?(kind) - end - - click_element kind - end - end - end end end end diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb index 1cb33a7c71c..2e29ec9a6a7 100644 --- a/qa/qa/resource/group.rb +++ b/qa/qa/resource/group.rb @@ -44,7 +44,7 @@ module QA # Ensure that the group was actually created group_show.wait_until(sleep_interval: 1) do group_show.has_text?(path) && - group_show.has_new_project_or_subgroup_dropdown? + group_show.has_new_project_and_new_subgroup_buttons? end end end diff --git a/spec/controllers/projects/ci/lints_controller_spec.rb b/spec/controllers/projects/ci/lints_controller_spec.rb index 24048aea838..c4e040b0287 100644 --- a/spec/controllers/projects/ci/lints_controller_spec.rb +++ b/spec/controllers/projects/ci/lints_controller_spec.rb @@ -123,20 +123,6 @@ RSpec.describe Projects::Ci::LintsController do subject end - - context 'when dry_run feature flag is disabled' do - before do - stub_feature_flags(ci_lint_creates_pipeline_with_dry_run: false) - end - - it_behaves_like 'returns a successful validation' - - it 'runs validations through YamlProcessor' do - expect(Gitlab::Ci::YamlProcessor).to receive(:new).and_call_original - - subject - end - end end end diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb index 420d818daeb..07fb03b39c6 100644 --- a/spec/controllers/projects/releases_controller_spec.rb +++ b/spec/controllers/projects/releases_controller_spec.rb @@ -201,102 +201,7 @@ RSpec.describe Projects::ReleasesController do end end - context 'GET #downloads' do - subject do - get :downloads, params: { namespace_id: project.namespace, project_id: project, tag: tag, filepath: filepath } - end - - before do - sign_in(user) - end - - let(:release) { create(:release, project: project, tag: tag ) } - let!(:link) { create(:release_link, release: release, name: 'linux-amd64 binaries', filepath: '/binaries/linux-amd64', url: 'https://downloads.example.com/bin/gitlab-linux-amd64') } - let(:tag) { 'v11.9.0-rc2' } - - context 'valid filepath' do - let(:filepath) { CGI.escape('/binaries/linux-amd64') } - - it 'redirects to the asset direct link' do - subject - - expect(response).to redirect_to('https://downloads.example.com/bin/gitlab-linux-amd64') - end - - it 'redirects with a status of 302' do - subject - - expect(response).to have_gitlab_http_status(:redirect) - end - end - - context 'invalid filepath' do - let(:filepath) { CGI.escape('/binaries/win32') } - - it 'is not found' do - subject - - expect(response).to have_gitlab_http_status(:not_found) - end - end - end - - context 'GET #downloads' do - subject do - get :downloads, params: { - namespace_id: project.namespace, - project_id: project, - tag: tag, - filepath: filepath - } - end - - before do - sign_in(user) - end - - let(:release) { create(:release, project: project, tag: tag ) } - let(:tag) { 'v11.9.0-rc2' } - let(:db_filepath) { '/binaries/linux-amd64' } - let!(:link) do - create :release_link, - release: release, - name: 'linux-amd64 binaries', - filepath: db_filepath, - url: 'https://downloads.example.com/bin/gitlab-linux-amd64' - end - - context 'valid filepath' do - let(:filepath) { CGI.escape('/binaries/linux-amd64') } - - it 'redirects to the asset direct link' do - subject - - expect(response).to redirect_to(link.url) - end - end - - context 'invalid filepath' do - let(:filepath) { CGI.escape('/binaries/win32') } - - it 'is not found' do - subject - - expect(response).to have_gitlab_http_status(:not_found) - end - end - - context 'ignores filepath extension' do - let(:db_filepath) { '/binaries/linux-amd64.json' } - let(:filepath) { CGI.escape(db_filepath) } - - it 'redirects to the asset direct link' do - subject - - expect(response).to redirect_to(link.url) - end - end - end + # `GET #downloads` is addressed in spec/requests/projects/releases_controller_spec.rb private diff --git a/spec/factories/bulk_import/trackers.rb b/spec/factories/bulk_import/trackers.rb new file mode 100644 index 00000000000..7a1fa0849fc --- /dev/null +++ b/spec/factories/bulk_import/trackers.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :bulk_import_tracker, class: 'BulkImports::Tracker' do + association :entity, factory: :bulk_import_entity + + relation { :relation } + has_next_page { false } + end +end diff --git a/spec/features/groups/container_registry_spec.rb b/spec/features/groups/container_registry_spec.rb index acac8724edf..1b23b8b4bf9 100644 --- a/spec/features/groups/container_registry_spec.rb +++ b/spec/features/groups/container_registry_spec.rb @@ -89,6 +89,20 @@ RSpec.describe 'Container Registry', :js do end end + context 'when an image has the same name as the subgroup' do + before do + stub_container_registry_tags(tags: %w[latest], with_manifest: true) + project.container_repositories << create(:container_repository, name: group.name) + visit_container_registry + end + + it 'details page loads properly' do + find('a[data-testid="details-link"]').click + + expect(page).to have_content 'latest' + end + end + def visit_container_registry visit group_container_registries_path(group) end diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb index 304573ecd6e..97732374eb9 100644 --- a/spec/features/groups/show_spec.rb +++ b/spec/features/groups/show_spec.rb @@ -81,8 +81,7 @@ RSpec.describe 'Group show page' do it 'allows creating subgroups' do visit path - expect(page) - .to have_css("li[data-text='New subgroup']", visible: false) + expect(page).to have_link('New subgroup') end end end @@ -102,8 +101,7 @@ RSpec.describe 'Group show page' do path = group_path(relaxed_group) visit path - expect(page) - .to have_css("li[data-text='New subgroup']", visible: false) + expect(page).to have_link('New subgroup') end end @@ -116,9 +114,7 @@ RSpec.describe 'Group show page' do path = group_path(restricted_group) visit path - expect(page) - .not_to have_selector("li[data-text='New subgroup']", - visible: false) + expect(page).not_to have_link('New subgroup') end end end diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 8264ec2eddd..b9fd3a1a5cc 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -294,35 +294,43 @@ RSpec.describe 'Group' do describe 'new subgroup / project button' do let(:group) { create(:group, project_creation_level: Gitlab::Access::NO_ONE_PROJECT_ACCESS, subgroup_creation_level: Gitlab::Access::OWNER_SUBGROUP_ACCESS) } - it 'new subgroup button is displayed without project creation permission' do - visit group_path(group) + context 'when user has subgroup creation permissions but not project creation permissions' do + it 'only displays "New subgroup" button' do + visit group_path(group) - page.within '.group-buttons' do - expect(page).to have_link('New subgroup') + page.within '[data-testid="group-buttons"]' do + expect(page).to have_link('New subgroup') + expect(page).not_to have_link('New project') + end end end - it 'new subgroup button is displayed together with new project button when having project creation permission' do - group.update!(project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS) - visit group_path(group) + context 'when user has project creation permissions but not subgroup creation permissions' do + it 'only displays "New project" button' do + group.update!(project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS) + user = create(:user) - page.within '.group-buttons' do - expect(page).to have_css("li[data-text='New subgroup']", visible: false) - expect(page).to have_css("li[data-text='New project']", visible: false) + group.add_maintainer(user) + sign_out(:user) + sign_in(user) + + visit group_path(group) + page.within '[data-testid="group-buttons"]' do + expect(page).to have_link('New project') + expect(page).not_to have_link('New subgroup') + end end end - it 'new project button is displayed without subgroup creation permission' do - group.update!(project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS) - user = create(:user) - - group.add_maintainer(user) - sign_out(:user) - sign_in(user) + context 'when user has project and subgroup creation permissions' do + it 'displays "New subgroup" and "New project" buttons' do + group.update!(project_creation_level: Gitlab::Access::MAINTAINER_PROJECT_ACCESS) + visit group_path(group) - visit group_path(group) - page.within '.group-buttons' do - expect(page).to have_link('New project') + page.within '[data-testid="group-buttons"]' do + expect(page).to have_link('New subgroup') + expect(page).to have_link('New project') + end end end end diff --git a/spec/features/projects/container_registry_spec.rb b/spec/features/projects/container_registry_spec.rb index 7514a26f020..45bf35a6aab 100644 --- a/spec/features/projects/container_registry_spec.rb +++ b/spec/features/projects/container_registry_spec.rb @@ -10,6 +10,10 @@ RSpec.describe 'Container Registry', :js do create(:container_repository, name: 'my/image') end + let(:nameless_container_repository) do + create(:container_repository, name: '') + end + before do sign_in(user) project.add_developer(user) @@ -96,6 +100,20 @@ RSpec.describe 'Container Registry', :js do end end + describe 'image repo details when image has no name' do + before do + stub_container_registry_tags(tags: %w[latest], with_manifest: true) + project.container_repositories << nameless_container_repository + visit_container_registry + end + + it 'renders correctly' do + find('a[data-testid="details-link"]').click + + expect(page).to have_content 'latest' + end + end + context 'when there are more than 10 images' do before do create_list(:container_repository, 12, project: project) diff --git a/spec/features/projects/releases/user_views_releases_spec.rb b/spec/features/projects/releases/user_views_releases_spec.rb index 6ca5073c5a4..aabbc8cea7b 100644 --- a/spec/features/projects/releases/user_views_releases_spec.rb +++ b/spec/features/projects/releases/user_views_releases_spec.rb @@ -38,7 +38,7 @@ RSpec.describe 'User views releases', :js do context 'when there is a link as an asset' do let!(:release_link) { create(:release_link, release: release_v1, url: url ) } let(:url) { "#{project.web_url}/-/jobs/1/artifacts/download" } - let(:direct_asset_link) { Gitlab::Routing.url_helpers.project_release_url(project, release_v1) << release_link.filepath } + let(:direct_asset_link) { Gitlab::Routing.url_helpers.project_release_url(project, release_v1) << "/downloads#{release_link.filepath}" } it 'sees the link' do page.within("##{release_v1.tag} .js-assets-list") do diff --git a/spec/frontend/alerts_settings/alerts_integrations_list_spec.js b/spec/frontend/alerts_settings/alerts_integrations_list_spec.js index c7a9db82bea..04ecefa7140 100644 --- a/spec/frontend/alerts_settings/alerts_integrations_list_spec.js +++ b/spec/frontend/alerts_settings/alerts_integrations_list_spec.js @@ -24,11 +24,14 @@ const mockIntegrations = [ describe('AlertIntegrationsList', () => { let wrapper; - function mountComponent(propsData = {}) { + function mountComponent({ data = {}, props = {} } = {}) { wrapper = mount(AlertIntegrationsList, { + data() { + return { ...data }; + }, propsData: { integrations: mockIntegrations, - ...propsData, + ...props, }, stubs: { GlIcon: true, @@ -57,7 +60,7 @@ describe('AlertIntegrationsList', () => { }); it('renders an empty state when no integrations provided', () => { - mountComponent({ integrations: [] }); + mountComponent({ props: { integrations: [] } }); expect(findTableComponent().text()).toContain(i18n.emptyState); }); @@ -66,7 +69,7 @@ describe('AlertIntegrationsList', () => { }); it('renders an highlighted row when a current integration is selected to edit', () => { - mountComponent({ currentIntegration: { id: '1' } }); + mountComponent({ data: { currentIntegration: { id: '1' } } }); expect( findTableComponentRows() .at(0) diff --git a/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js b/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js index d5a1239f701..97a53c20b6b 100644 --- a/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js +++ b/spec/frontend/alerts_settings/alerts_settings_form_new_spec.js @@ -152,9 +152,9 @@ describe('AlertsSettingsFormNew', () => { createComponent({ data: { selectedIntegration: typeSet.http, + currentIntegration: { id: '1', name: 'Test integration pre' }, }, props: { - currentIntegration: { id: '1', name: 'Test integration pre' }, loading: false, }, }); @@ -183,9 +183,9 @@ describe('AlertsSettingsFormNew', () => { createComponent({ data: { selectedIntegration: typeSet.prometheus, + currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' }, }, props: { - currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' }, loading: false, }, }); @@ -219,10 +219,10 @@ describe('AlertsSettingsFormNew', () => { createComponent({ data: { selectedIntegration: typeSet.http, + currentIntegration: { id: '1', name: 'Test' }, active: true, }, props: { - currentIntegration: { id: '1', name: 'Test' }, loading: false, }, }); diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js index e85ee39f165..9f7a2758ae1 100644 --- a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js +++ b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js @@ -19,7 +19,7 @@ describe('Image List Row', () => { let wrapper; const item = imagesListResponse.data[0]; - const findDetailsLink = () => wrapper.find('[data-testid="detailsLink"]'); + const findDetailsLink = () => wrapper.find('[data-testid="details-link"]'); const findTagsCount = () => wrapper.find('[data-testid="tagsCount"]'); const findDeleteBtn = () => wrapper.find(DeleteButton); const findClipboardButton = () => wrapper.find(ClipboardButton); diff --git a/spec/frontend/registry/explorer/stores/actions_spec.js b/spec/frontend/registry/explorer/stores/actions_spec.js index 17ce6a7054d..dcd4d8015a4 100644 --- a/spec/frontend/registry/explorer/stores/actions_spec.js +++ b/spec/frontend/registry/explorer/stores/actions_spec.js @@ -8,6 +8,11 @@ import * as actions from '~/registry/explorer/stores/actions'; import * as types from '~/registry/explorer/stores/mutation_types'; import { reposServerResponse, registryServerResponse } from '../mock_data'; import * as utils from '~/registry/explorer/utils'; +import { + FETCH_IMAGES_LIST_ERROR_MESSAGE, + FETCH_TAGS_LIST_ERROR_MESSAGE, + FETCH_IMAGE_DETAILS_ERROR_MESSAGE, +} from '~/registry/explorer/constants/index'; jest.mock('~/flash.js'); jest.mock('~/registry/explorer/utils'); @@ -129,7 +134,7 @@ describe('Actions RegistryExplorer Store', () => { ], [], () => { - expect(createFlash).toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalledWith({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE }); done(); }, ); @@ -169,7 +174,7 @@ describe('Actions RegistryExplorer Store', () => { ], [], () => { - expect(createFlash).toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalledWith({ message: FETCH_TAGS_LIST_ERROR_MESSAGE }); done(); }, ); @@ -261,7 +266,7 @@ describe('Actions RegistryExplorer Store', () => { ], [], () => { - expect(createFlash).toHaveBeenCalled(); + expect(createFlash).toHaveBeenCalledWith({ message: FETCH_IMAGE_DETAILS_ERROR_MESSAGE }); done(); }, ); diff --git a/spec/frontend/registry/explorer/utils_spec.js b/spec/frontend/registry/explorer/utils_spec.js index 365bfc9ea19..0cd4a1cec29 100644 --- a/spec/frontend/registry/explorer/utils_spec.js +++ b/spec/frontend/registry/explorer/utils_spec.js @@ -16,6 +16,20 @@ describe('Utils', () => { expect(pathGenerator(imageDetails, '/foo')).toBe('/foo/bar/registry/repository/1/tags/foo'); }); + it.each` + path | name | result + ${'foo/foo'} | ${''} | ${'/foo/foo/registry/repository/1/tags?format=json'} + ${'foo/foo/foo'} | ${'foo'} | ${'/foo/foo/registry/repository/1/tags?format=json'} + ${'baz/foo/foo/foo'} | ${'foo'} | ${'/baz/foo/foo/registry/repository/1/tags?format=json'} + ${'baz/foo/foo/foo'} | ${'foo'} | ${'/baz/foo/foo/registry/repository/1/tags?format=json'} + ${'foo/foo/baz/foo/foo'} | ${'foo/foo'} | ${'/foo/foo/baz/registry/repository/1/tags?format=json'} + ${'foo/foo/baz/foo/bar'} | ${'foo/bar'} | ${'/foo/foo/baz/registry/repository/1/tags?format=json'} + ${'baz/foo/foo'} | ${'foo'} | ${'/baz/foo/registry/repository/1/tags?format=json'} + ${'baz/foo/bar'} | ${'foo'} | ${'/baz/foo/bar/registry/repository/1/tags?format=json'} + `('returns the correct path when path is $path and name is $name', ({ name, path, result }) => { + expect(pathGenerator({ id: 1, name, path })).toBe(result); + }); + it('returns the url unchanged when imageDetails have no name', () => { const imageDetailsWithoutName = { path: 'foo/bar/baz', diff --git a/spec/frontend/releases/__snapshots__/util_spec.js.snap b/spec/frontend/releases/__snapshots__/util_spec.js.snap index aefd1edb87e..f49d3d7b716 100644 --- a/spec/frontend/releases/__snapshots__/util_spec.js.snap +++ b/spec/frontend/releases/__snapshots__/util_spec.js.snap @@ -18,7 +18,7 @@ Object { "count": 8, "links": Array [ Object { - "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/binaries/awesome-app-3", + "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-3", "external": true, "id": "gid://gitlab/Releases::Link/13", "linkType": "image", @@ -26,7 +26,7 @@ Object { "url": "https://example.com/image", }, Object { - "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/binaries/awesome-app-2", + "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-2", "external": true, "id": "gid://gitlab/Releases::Link/12", "linkType": "package", @@ -34,7 +34,7 @@ Object { "url": "https://example.com/package", }, Object { - "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/binaries/awesome-app-1", + "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-1", "external": false, "id": "gid://gitlab/Releases::Link/11", "linkType": "runbook", @@ -42,7 +42,7 @@ Object { "url": "http://localhost/releases-namespace/releases-project/runbook", }, Object { - "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/binaries/linux-amd64", + "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/linux-amd64", "external": true, "id": "gid://gitlab/Releases::Link/10", "linkType": "other", @@ -146,7 +146,7 @@ Object { "count": 8, "links": Array [ Object { - "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/binaries/awesome-app-3", + "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-3", "external": true, "id": "gid://gitlab/Releases::Link/13", "linkType": "image", @@ -154,7 +154,7 @@ Object { "url": "https://example.com/image", }, Object { - "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/binaries/awesome-app-2", + "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-2", "external": true, "id": "gid://gitlab/Releases::Link/12", "linkType": "package", @@ -162,7 +162,7 @@ Object { "url": "https://example.com/package", }, Object { - "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/binaries/awesome-app-1", + "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/awesome-app-1", "external": false, "id": "gid://gitlab/Releases::Link/11", "linkType": "runbook", @@ -170,7 +170,7 @@ Object { "url": "http://localhost/releases-namespace/releases-project/runbook", }, Object { - "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/binaries/linux-amd64", + "directAssetUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1/downloads/binaries/linux-amd64", "external": true, "id": "gid://gitlab/Releases::Link/10", "linkType": "other", diff --git a/spec/graphql/mutations/commits/create_spec.rb b/spec/graphql/mutations/commits/create_spec.rb index fb1baafe7bd..82a5e3a62f5 100644 --- a/spec/graphql/mutations/commits/create_spec.rb +++ b/spec/graphql/mutations/commits/create_spec.rb @@ -5,8 +5,9 @@ require 'spec_helper' RSpec.describe Mutations::Commits::Create do subject(:mutation) { described_class.new(object: nil, context: context, field: nil) } - let_it_be(:project) { create(:project, :public, :repository) } let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + let(:context) do GraphQL::Query::Context.new( query: OpenStruct.new(schema: nil), @@ -18,9 +19,10 @@ RSpec.describe Mutations::Commits::Create do specify { expect(described_class).to require_graphql_authorizations(:push_code) } describe '#resolve' do - subject { mutation.resolve(project_path: project.full_path, branch: branch, message: message, actions: actions) } + subject { mutation.resolve(project_path: project.full_path, branch: branch, start_branch: start_branch, message: message, actions: actions) } let(:branch) { 'master' } + let(:start_branch) { nil } let(:message) { 'Commit message' } let(:actions) do [ @@ -142,6 +144,29 @@ RSpec.describe Mutations::Commits::Create do end end + context 'when branch does not exist and a start branch is provided' do + let(:branch) { 'my-branch' } + let(:start_branch) { 'master' } + let(:actions) do + [ + { + action: 'create', + file_path: 'ANOTHER_FILE.md', + content: 'Bye' + } + ] + end + + it 'returns a new commit' do + expect(mutated_commit).to have_attributes(message: message, project: project) + expect(subject[:errors]).to be_empty + + expect_to_contain_deltas([ + a_hash_including(a_mode: '0', b_mode: '100644', new_file: true, new_path: 'ANOTHER_FILE.md') + ]) + end + end + context 'when message is not set' do let(:message) { nil } diff --git a/spec/lib/gitlab/background_migration/backfill_merge_request_cleanup_schedules_spec.rb b/spec/lib/gitlab/background_migration/backfill_merge_request_cleanup_schedules_spec.rb new file mode 100644 index 00000000000..c2daa35703d --- /dev/null +++ b/spec/lib/gitlab/background_migration/backfill_merge_request_cleanup_schedules_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::BackgroundMigration::BackfillMergeRequestCleanupSchedules, schema: 20201103110018 do + let(:merge_requests) { table(:merge_requests) } + let(:cleanup_schedules) { table(:merge_request_cleanup_schedules) } + let(:metrics) { table(:merge_request_metrics) } + + let(:namespace) { table(:namespaces).create!(name: 'name', path: 'path') } + let(:project) { table(:projects).create!(namespace_id: namespace.id) } + + subject { described_class.new } + + describe '#perform' do + let!(:open_mr) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master') } + + let!(:closed_mr_1) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master', state_id: 2) } + let!(:closed_mr_2) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master', state_id: 2) } + let!(:closed_mr_1_metrics) { metrics.create!(merge_request_id: closed_mr_1.id, target_project_id: project.id, latest_closed_at: Time.current, created_at: Time.current, updated_at: Time.current) } + let!(:closed_mr_2_metrics) { metrics.create!(merge_request_id: closed_mr_2.id, target_project_id: project.id, latest_closed_at: Time.current, created_at: Time.current, updated_at: Time.current) } + let!(:closed_mr_2_cleanup_schedule) { cleanup_schedules.create!(merge_request_id: closed_mr_2.id, scheduled_at: Time.current) } + + let!(:merged_mr_1) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master', state_id: 3) } + let!(:merged_mr_2) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master', state_id: 3, updated_at: Time.current) } + let!(:merged_mr_1_metrics) { metrics.create!(merge_request_id: merged_mr_1.id, target_project_id: project.id, merged_at: Time.current, created_at: Time.current, updated_at: Time.current) } + + let!(:closed_mr_3) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master', state_id: 2) } + let!(:closed_mr_3_metrics) { metrics.create!(merge_request_id: closed_mr_3.id, target_project_id: project.id, latest_closed_at: Time.current, created_at: Time.current, updated_at: Time.current) } + + it 'creates records for all closed and merged merge requests in range' do + expect(Gitlab::BackgroundMigration::Logger).to receive(:info).with( + message: 'Backfilled merge_request_cleanup_schedules records', + count: 3 + ) + + subject.perform(open_mr.id, merged_mr_2.id) + + aggregate_failures do + expect(cleanup_schedules.all.pluck(:merge_request_id)) + .to contain_exactly(closed_mr_1.id, closed_mr_2.id, merged_mr_1.id, merged_mr_2.id) + expect(cleanup_schedules.find_by(merge_request_id: closed_mr_1.id).scheduled_at.to_s) + .to eq((closed_mr_1_metrics.latest_closed_at + 14.days).to_s) + expect(cleanup_schedules.find_by(merge_request_id: closed_mr_2.id).scheduled_at.to_s) + .to eq(closed_mr_2_cleanup_schedule.scheduled_at.to_s) + expect(cleanup_schedules.find_by(merge_request_id: merged_mr_1.id).scheduled_at.to_s) + .to eq((merged_mr_1_metrics.merged_at + 14.days).to_s) + expect(cleanup_schedules.find_by(merge_request_id: merged_mr_2.id).scheduled_at.to_s) + .to eq((merged_mr_2.updated_at + 14.days).to_s) + end + end + end +end diff --git a/spec/migrations/schedule_merge_request_cleanup_schedules_backfill_spec.rb b/spec/migrations/schedule_merge_request_cleanup_schedules_backfill_spec.rb new file mode 100644 index 00000000000..fa8dd38619a --- /dev/null +++ b/spec/migrations/schedule_merge_request_cleanup_schedules_backfill_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +require_migration! + +RSpec.describe ScheduleMergeRequestCleanupSchedulesBackfill, :sidekiq, schema: 20201023114628 do + let(:merge_requests) { table(:merge_requests) } + let(:cleanup_schedules) { table(:merge_request_cleanup_schedules) } + + let(:namespace) { table(:namespaces).create!(name: 'name', path: 'path') } + let(:project) { table(:projects).create!(namespace_id: namespace.id) } + + describe '#up' do + let!(:open_mr) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master') } + + let!(:closed_mr_1) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master', state_id: 2) } + let!(:closed_mr_2) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master', state_id: 2) } + + let!(:merged_mr_1) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master', state_id: 3) } + let!(:merged_mr_2) { merge_requests.create!(target_project_id: project.id, source_branch: 'master', target_branch: 'master', state_id: 3) } + + before do + stub_const("#{described_class}::BATCH_SIZE", 2) + end + + it 'schdules BackfillMergeRequestCleanupSchedules background jobs' do + Sidekiq::Testing.fake! do + migrate! + + aggregate_failures do + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(2.minutes, closed_mr_1.id, closed_mr_2.id) + expect(described_class::MIGRATION) + .to be_scheduled_delayed_migration(4.minutes, merged_mr_1.id, merged_mr_2.id) + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end + end +end diff --git a/spec/models/bulk_imports/tracker_spec.rb b/spec/models/bulk_imports/tracker_spec.rb new file mode 100644 index 00000000000..8eb5a6c27dd --- /dev/null +++ b/spec/models/bulk_imports/tracker_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe BulkImports::Tracker, type: :model do + describe 'associations' do + it { is_expected.to belong_to(:entity).required } + end + + describe 'validations' do + before do + create(:bulk_import_tracker) + end + + it { is_expected.to validate_presence_of(:relation) } + it { is_expected.to validate_uniqueness_of(:relation).scoped_to(:bulk_import_entity_id) } + + context 'when has_next_page is true' do + it "validates presence of `next_page`" do + tracker = build(:bulk_import_tracker, has_next_page: true) + + expect(tracker).not_to be_valid + expect(tracker.errors).to include(:next_page) + end + end + end +end diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb index 946541a0602..d3ce2f2d48f 100644 --- a/spec/models/design_management/design_spec.rb +++ b/spec/models/design_management/design_spec.rb @@ -375,14 +375,6 @@ RSpec.describe DesignManagement::Design do end it { is_expected.to contain_exactly(version_author, note_author, mentioned_user) } - - context 'when the feature flag is disabled' do - before do - stub_feature_flags(design_management_design_notification_participants: false) - end - - it { is_expected.to be_empty } - end end end diff --git a/spec/requests/api/graphql/mutations/commits/create_spec.rb b/spec/requests/api/graphql/mutations/commits/create_spec.rb index ac4fa7cfe83..375d4f10b40 100644 --- a/spec/requests/api/graphql/mutations/commits/create_spec.rb +++ b/spec/requests/api/graphql/mutations/commits/create_spec.rb @@ -23,6 +23,18 @@ RSpec.describe 'Creation of a new commit' do let(:mutation) { graphql_mutation(:commit_create, input) } let(:mutation_response) { graphql_mutation_response(:commit_create) } + shared_examples 'a commit is successful' do + it 'creates a new commit' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(response).to have_gitlab_http_status(:success) + + expect(mutation_response['commit']).to include( + 'title' => message + ) + end + end + context 'the user is not allowed to create a commit' do it_behaves_like 'a mutation that returns a top-level access error' end @@ -32,14 +44,7 @@ RSpec.describe 'Creation of a new commit' do project.add_developer(current_user) end - it 'creates a new commit' do - post_graphql_mutation(mutation, current_user: current_user) - - expect(response).to have_gitlab_http_status(:success) - expect(mutation_response['commit']).to include( - 'title' => message - ) - end + it_behaves_like 'a commit is successful' context 'when branch is not correct' do let(:branch) { 'unknown' } @@ -47,5 +52,22 @@ RSpec.describe 'Creation of a new commit' do it_behaves_like 'a mutation that returns errors in the response', errors: ['You can only create or edit files when you are on a branch'] end + + context 'when branch is new, and a start_branch is defined' do + let(:input) { { project_path: project.full_path, branch: branch, start_branch: start_branch, message: message, actions: actions } } + let(:branch) { 'new-branch' } + let(:start_branch) { 'master' } + let(:actions) do + [ + { + action: 'CREATE', + filePath: 'ANOTHER_FILE.md', + content: 'Bye' + } + ] + end + + it_behaves_like 'a commit is successful' + end end end diff --git a/spec/requests/api/graphql/mutations/releases/create_spec.rb b/spec/requests/api/graphql/mutations/releases/create_spec.rb index 2402cf62a49..d745eb3083d 100644 --- a/spec/requests/api/graphql/mutations/releases/create_spec.rb +++ b/spec/requests/api/graphql/mutations/releases/create_spec.rb @@ -121,7 +121,7 @@ RSpec.describe 'Creation of a new release' do create_release release = mutation_response[:release] - expected_direct_asset_url = Gitlab::Routing.url_helpers.project_release_url(project, Release.find_by(tag: tag_name)) << asset_link[:directAssetPath] + expected_direct_asset_url = Gitlab::Routing.url_helpers.project_release_url(project, Release.find_by(tag: tag_name)) << "/downloads#{asset_link[:directAssetPath]}" expected_attributes = { tagName: tag_name, diff --git a/spec/requests/api/graphql/project/release_spec.rb b/spec/requests/api/graphql/project/release_spec.rb index 57b620dbdf7..57dbe258ce4 100644 --- a/spec/requests/api/graphql/project/release_spec.rb +++ b/spec/requests/api/graphql/project/release_spec.rb @@ -147,7 +147,7 @@ RSpec.describe 'Query.project(fullPath).release(tagName)' do 'name' => link.name, 'url' => link.url, 'external' => link.external?, - 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << link.filepath : link.url + 'directAssetUrl' => link.filepath ? Gitlab::Routing.url_helpers.project_release_url(project, release) << "/downloads#{link.filepath}" : link.url } end diff --git a/spec/requests/api/release/links_spec.rb b/spec/requests/api/release/links_spec.rb index 82d0d64eba4..c03dd0331cf 100644 --- a/spec/requests/api/release/links_spec.rb +++ b/spec/requests/api/release/links_spec.rb @@ -146,7 +146,7 @@ RSpec.describe API::Release::Links do specify do get api("/projects/#{project.id}/releases/v0.1/assets/links/#{link.id}", maintainer) - expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.namespace.path}/#{project.name}/-/releases/#{release.tag}/bin/bigfile.exe") + expect(json_response['direct_asset_url']).to eq("http://localhost/#{project.namespace.path}/#{project.name}/-/releases/#{release.tag}/downloads/bin/bigfile.exe") end end diff --git a/spec/requests/projects/releases_controller_spec.rb b/spec/requests/projects/releases_controller_spec.rb new file mode 100644 index 00000000000..8e492125ace --- /dev/null +++ b/spec/requests/projects/releases_controller_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Projects::ReleasesController' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + + before do + project.add_developer(user) + login_as(user) + end + + # Added as a request spec because of https://gitlab.com/gitlab-org/gitlab/-/issues/232386 + describe 'GET #downloads' do + context 'filepath redirection' do + let_it_be(:release) { create(:release, project: project, tag: 'v11.9.0-rc2' ) } + let!(:link) { create(:release_link, release: release, name: 'linux-amd64 binaries', filepath: filepath, url: 'https://aws.example.com/s3/project/bin/hello-darwin-amd64') } + let_it_be(:url) { "#{project_releases_path(project)}/#{release.tag}/downloads/bin/darwin-amd64" } + + let(:subject) { get url } + + context 'valid filepath' do + let(:filepath) { '/bin/darwin-amd64' } + + it 'redirects to the asset direct link' do + subject + + expect(response).to redirect_to('https://aws.example.com/s3/project/bin/hello-darwin-amd64') + end + + it 'redirects with a status of 302' do + subject + + expect(response).to have_gitlab_http_status(:redirect) + end + end + + context 'invalid filepath' do + let(:filepath) { '/binaries/win32' } + + it 'is not found' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + end + + context 'invalid filepath' do + let(:invalid_filepath) { 'bin/darwin-amd64' } + + let(:subject) { create(:release_link, name: 'linux-amd64 binaries', filepath: invalid_filepath, url: 'https://aws.example.com/s3/project/bin/hello-darwin-amd64') } + + it 'cannot create an invalid filepath' do + expect { subject }.to raise_error(ActiveRecord::RecordInvalid) + end + end + end +end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index e4ec3e56c20..6f7225fadbf 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -867,23 +867,6 @@ RSpec.describe NotificationService, :mailer do should_not_email(non_member_and_mentioned) should_not_email(note.author) end - - context 'when the feature flag is disabled' do - before do - stub_feature_flags(design_management_design_notification_participants: false) - end - - it 'sends a new note notification only to the mentioned member', :aggregate_failures do - notification.new_note(note) - - should_email(member_and_mentioned) - should_not_email(design.authors.first) - should_not_email(member_and_author_of_second_note) - should_not_email(member_and_not_mentioned) - should_not_email(non_member_and_mentioned) - should_not_email(note.author) - end - end end context 'design management is disabled' do |