Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-09-06 12:10:02 +00:00
parent 6431ee6152
commit 65b6ccd12e
51 changed files with 1084 additions and 182 deletions

View file

@ -174,6 +174,7 @@ export default {
this.$emit('open-form', this.discussion.id);
this.isFormRendered = true;
},
toggleResolvedStatus() {
this.isResolving = true;
@ -234,6 +235,7 @@ export default {
:note="firstNote"
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
:noteable-id="noteableId"
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
@error="$emit('update-note-error', $event)"
>
@ -276,6 +278,7 @@ export default {
:note="note"
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
:noteable-id="noteableId"
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
@error="$emit('update-note-error', $event)"
/>
@ -307,6 +310,8 @@ export default {
v-model="discussionComment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
:noteable-id="noteableId"
:discussion-id="discussion.id"
@submit-form="mutate"
@cancel-form="hideForm"
>

View file

@ -45,6 +45,10 @@ export default {
required: false,
default: '',
},
noteableId: {
type: String,
required: true,
},
},
data() {
return {
@ -160,6 +164,7 @@ export default {
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
:is-new-comment="false"
:noteable-id="noteableId"
class="gl-mt-5"
@submit-form="mutate"
@cancel-form="hideForm"

View file

@ -1,7 +1,11 @@
<script>
import { GlButton, GlModal } from '@gitlab/ui';
import $ from 'jquery';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import Autosave from '~/autosave';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
export default {
@ -30,10 +34,20 @@ export default {
required: false,
default: true,
},
noteableId: {
type: String,
required: true,
},
discussionId: {
type: String,
required: false,
default: 'new',
},
},
data() {
return {
formText: this.value,
isLoggedIn: isLoggedIn(),
};
},
computed: {
@ -64,13 +78,19 @@ export default {
markdownDocsPath() {
return helpPagePath('user/markdown');
},
shortDiscussionId() {
return isGid(this.discussionId) ? getIdFromGraphQLId(this.discussionId) : this.discussionId;
},
},
mounted() {
this.focusInput();
},
methods: {
submitForm() {
if (this.hasValue) this.$emit('submit-form');
if (this.hasValue) {
this.$emit('submit-form');
this.autosaveDiscussion.reset();
}
},
cancelComment() {
if (this.hasValue && this.formText !== this.value) {
@ -79,8 +99,22 @@ export default {
this.$emit('cancel-form');
}
},
confirmCancelCommentModal() {
this.$emit('cancel-form');
this.autosaveDiscussion.reset();
},
focusInput() {
this.$refs.textarea.focus();
this.initAutosaveComment();
},
initAutosaveComment() {
if (this.isLoggedIn) {
this.autosaveDiscussion = new Autosave($(this.$refs.textarea), [
s__('DesignManagement|Discussion'),
getIdFromGraphQLId(this.noteableId),
this.shortDiscussionId,
]);
}
},
},
};
@ -124,7 +158,7 @@ export default {
type="submit"
data-track-action="click_button"
data-qa-selector="save_comment_button"
@click="$emit('submit-form')"
@click="submitForm"
>
{{ buttonText }}
</gl-button>
@ -144,7 +178,7 @@ export default {
:ok-title="modalSettings.okTitle"
:cancel-title="modalSettings.cancelTitle"
modal-id="cancel-comment-modal"
@ok="$emit('cancel-form')"
@ok="confirmCancelCommentModal"
>{{ modalSettings.content }}
</gl-modal>
</form>

View file

@ -418,6 +418,7 @@ export default {
v-model="comment"
:is-saving="loading"
:markdown-preview-path="markdownPreviewPath"
:noteable-id="design.id"
@submit-form="mutate"
@cancel-form="closeCommentForm"
/> </apollo-mutation

View file

@ -12,7 +12,6 @@ query getJobs($fullPath: ID!, $after: String, $first: Int = 30, $statuses: [CiJo
nodes {
artifacts {
nodes {
id
downloadPath
fileType
}

View file

@ -0,0 +1,10 @@
import groupsSelect from '~/groups_select';
import UserCallout from '~/user_callout';
import UsersSelect from '~/users_select';
// eslint-disable-next-line no-new
new UsersSelect();
groupsSelect();
// eslint-disable-next-line no-new
new UserCallout({ className: 'js-mr-approval-callout' });

View file

@ -18,6 +18,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-project-usage-limitations-callout',
'.js-namespace-storage-alert',
'.js-web-hook-disabled-callout',
'.js-merge-request-settings-callout',
];
const initCallouts = () => {

View file

@ -12,7 +12,6 @@ query getPipelineJobs($fullPath: ID!, $iid: ID!, $after: String) {
nodes {
artifacts {
nodes {
id
downloadPath
fileType
}

View file

@ -6,7 +6,6 @@ fragment JobArtifacts on Pipeline {
name
artifacts {
nodes {
id
downloadPath
fileType
}

View file

@ -15,7 +15,6 @@ query securityReportDownloadPaths(
name
artifacts {
nodes {
id
downloadPath
fileType
}

View file

@ -31,8 +31,7 @@
width: 100%;
padding-left: 10px;
padding-right: 10px;
white-space: break-spaces;
word-break: break-word;
white-space: pre;
&:empty::before {
content: '\200b';

View file

@ -383,6 +383,10 @@ input[type='checkbox']:hover {
.line_holder {
pre {
padding: 0; // This overrides the existing style that will add space between each line.
.line {
@include gl-word-break-word;
white-space: break-spaces;
}
}
svg {

View file

@ -0,0 +1,67 @@
# frozen_string_literal: true
module Projects
module Settings
class MergeRequestsController < Projects::ApplicationController
layout 'project_settings'
before_action :merge_requests_enabled?
before_action :present_project, only: [:edit]
before_action :authorize_admin_project!
feature_category :code_review
def update
result = ::Projects::UpdateService.new(@project, current_user, project_params).execute
if result[:status] == :success
flash[:notice] = format(_("Project '%{project_name}' was successfully updated."), project_name: @project.name)
redirect_to project_settings_merge_requests_path(@project)
else
# Refresh the repo in case anything changed
@repository = @project.repository.reset
flash[:alert] = result[:message]
@project.reset
render 'show'
end
end
private
def merge_requests_enabled?
render_404 unless @project.merge_requests_enabled?
end
def project_params
params.require(:project)
.permit(project_params_attributes)
end
def project_setting_attributes
%i[
squash_option
allow_editing_commit_messages
mr_default_target_self
]
end
def project_params_attributes
[
:allow_merge_on_skipped_pipeline,
:resolve_outdated_diff_discussions,
:only_allow_merge_if_all_discussions_are_resolved,
:only_allow_merge_if_pipeline_succeeds,
:printing_merge_request_link_enabled,
:remove_source_branch_after_merge,
:merge_method,
:merge_commit_template_or_default,
:squash_commit_template_or_default,
:suggestion_commit_message
] + [project_setting_attributes: project_setting_attributes]
end
end
end
end
Projects::Settings::MergeRequestsController.prepend_mod_with('Projects::Settings::MergeRequestsController')

View file

@ -6,9 +6,6 @@ module Types
class JobArtifactType < BaseObject
graphql_name 'CiJobArtifact'
field :id, Types::GlobalIDType[::Ci::JobArtifact], null: false,
description: 'ID of the artifact.'
field :download_path, GraphQL::Types::String, null: true,
description: "URL for downloading the artifact's file."
@ -19,12 +16,6 @@ module Types
description: 'File name of the artifact.',
method: :filename
field :size, GraphQL::Types::Int, null: false,
description: 'Size of the artifact in bytes.'
field :expire_at, Types::TimeType, null: true,
description: 'Expiry date of the artifact.'
def download_path
::Gitlab::Routing.url_helpers.download_project_job_artifacts_path(
object.project,

View file

@ -10,6 +10,7 @@ module Users
REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout'
UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout'
MERGE_REQUEST_SETTINGS_MOVED_CALLOUT = 'merge_request_settings_moved_callout'
REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze
WEB_HOOK_DISABLED = 'web_hook_disabled'
@ -74,6 +75,10 @@ module Users
user_dismissed?(WEB_HOOK_DISABLED, last_failure, project: project)
end
def show_merge_request_settings_callout?
!user_dismissed?(MERGE_REQUEST_SETTINGS_MOVED_CALLOUT)
end
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil, project: nil)

View file

@ -60,7 +60,8 @@ module Users
namespace_storage_limit_banner_warning_threshold: 56, # EE-only
namespace_storage_limit_banner_alert_threshold: 57, # EE-only
namespace_storage_limit_banner_error_threshold: 58, # EE-only
project_quality_summary_feedback: 59 # EE-only
project_quality_summary_feedback: 59, # EE-only
merge_request_settings_moved_callout: 60
}
validates :feature_name,

View file

@ -0,0 +1,107 @@
# frozen_string_literal: true
# When a user is destroyed, some of their associated records are
# moved to a "Ghost User", to prevent these associated records from
# being destroyed.
#
# For example, all the issues/MRs a user has created are _not_ destroyed
# when the user is destroyed.
module Users
class MigrateRecordsToGhostUserService
extend ActiveSupport::Concern
DestroyError = Class.new(StandardError)
attr_reader :ghost_user, :user, :initiator_user, :hard_delete
def initialize(user, initiator_user)
@user = user
@initiator_user = initiator_user
@ghost_user = User.ghost
end
def execute(hard_delete: false)
@hard_delete = hard_delete
migrate_records
post_migrate_records
end
private
def migrate_records
return if hard_delete
migrate_issues
migrate_merge_requests
migrate_notes
migrate_abuse_reports
migrate_award_emoji
migrate_snippets
migrate_reviews
end
def post_migrate_records
delete_snippets
# Rails attempts to load all related records into memory before
# destroying: https://github.com/rails/rails/issues/22510
# This ensures we delete records in batches.
user.destroy_dependent_associations_in_batches(exclude: [:snippets])
user.nullify_dependent_associations_in_batches
# Destroy the namespace after destroying the user since certain methods may depend on the namespace existing
user_data = user.destroy
user.namespace.destroy
user_data
end
def delete_snippets
response = Snippets::BulkDestroyService.new(initiator_user, user.snippets).execute(skip_authorization: true)
raise DestroyError, response.message if response.error?
end
def migrate_issues
batched_migrate(Issue, :author_id)
batched_migrate(Issue, :last_edited_by_id)
end
def migrate_merge_requests
batched_migrate(MergeRequest, :author_id)
batched_migrate(MergeRequest, :merge_user_id)
end
def migrate_notes
batched_migrate(Note, :author_id)
end
def migrate_abuse_reports
user.reported_abuse_reports.update_all(reporter_id: ghost_user.id)
end
def migrate_award_emoji
user.award_emoji.update_all(user_id: ghost_user.id)
end
def migrate_snippets
snippets = user.snippets.only_project_snippets
snippets.update_all(author_id: ghost_user.id)
end
def migrate_reviews
batched_migrate(Review, :author_id)
end
# rubocop:disable CodeReuse/ActiveRecord
def batched_migrate(base_scope, column, batch_size: 50)
loop do
update_count = base_scope.where(column => user.id).limit(batch_size).update_all(column => ghost_user.id)
break if update_count == 0
end
end
# rubocop:enable CodeReuse/ActiveRecord
end
end
Users::MigrateRecordsToGhostUserService.prepend_mod_with('Users::MigrateRecordsToGhostUserService')

View file

@ -26,23 +26,13 @@
%template.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project).to_json.html_safe
.js-project-permissions-form{ data: visibility_confirm_modal_data(@project, reduce_visibility_form_id) }
%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded ? _('Collapse') : _('Expand')
= render_if_exists 'projects/merge_request_settings_description_text'
.settings-content
= render_if_exists 'shared/promotions/promote_mr_features'
= gitlab_ui_form_for @project, html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' }
= render 'projects/merge_request_settings', form: f
= f.submit _('Save changes'), class: "btn gl-button btn-confirm rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }
= render_if_exists 'projects/merge_request_approvals_settings', expanded: expanded
- if show_merge_request_settings_callout?
%section.settings.expanded
= render Pajamas::AlertComponent.new(variant: :info,
title: _('Merge requests and approvals settings have moved.'),
alert_options: { class: 'js-merge-request-settings-callout gl-my-5', data: { feature_id: Users::CalloutsHelper::MERGE_REQUEST_SETTINGS_MOVED_CALLOUT, dismiss_endpoint: callouts_path, defer_links: 'true' } }) do |c|
= c.body do
= _('On the left sidebar, select %{merge_requests_link} to view them.').html_safe % { merge_requests_link: link_to('Settings > Merge requests', project_settings_merge_requests_path(@project)).html_safe }
%section.settings.no-animate{ class: ('expanded' if expanded), data: { qa_selector: 'badges_settings_content' } }
.settings-header

View file

@ -0,0 +1,18 @@
- breadcrumb_title _('Merge requests')
- page_title _('Merge requests')
- @content_class = 'limit-container-width' unless fluid_layout
%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings.expanded{ class: [('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests')
= render_if_exists 'projects/merge_request_settings_description_text'
.settings-content
= render_if_exists 'shared/promotions/promote_mr_features'
= gitlab_ui_form_for @project, url: project_settings_merge_requests_path(@project), html: { multipart: true, class: "merge-request-settings-form js-mr-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-merge-request-settings' }
= render 'projects/merge_request_settings', form: f
= f.submit _('Save changes'), class: "btn gl-button btn-confirm rspec-save-merge-request-changes", data: { qa_selector: 'save_merge_request_changes_button' }
= render_if_exists 'projects/settings/merge_requests/merge_request_approvals_settings', expanded: true

View file

@ -159,6 +159,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resource :packages_and_registries, only: [:show] do
get '/cleanup_image_tags', to: 'packages_and_registries#cleanup_tags'
end
resource :merge_requests, only: [:show, :update]
end
resources :usage_quotas, only: [:index]

View file

@ -10267,11 +10267,8 @@ CI/CD variables for a GitLab instance.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cijobartifactdownloadpath"></a>`downloadPath` | [`String`](#string) | URL for downloading the artifact's file. |
| <a id="cijobartifactexpireat"></a>`expireAt` | [`Time`](#time) | Expiry date of the artifact. |
| <a id="cijobartifactfiletype"></a>`fileType` | [`JobArtifactFileType`](#jobartifactfiletype) | File type of the artifact. |
| <a id="cijobartifactid"></a>`id` | [`CiJobArtifactID!`](#cijobartifactid) | ID of the artifact. |
| <a id="cijobartifactname"></a>`name` | [`String`](#string) | File name of the artifact. |
| <a id="cijobartifactsize"></a>`size` | [`Int!`](#int) | Size of the artifact in bytes. |
### `CiJobTokenScopeType`
@ -20962,6 +20959,7 @@ Name of the feature that the callout is for.
| <a id="usercalloutfeaturenameenumgeo_migrate_hashed_storage"></a>`GEO_MIGRATE_HASHED_STORAGE` | Callout feature name for geo_migrate_hashed_storage. |
| <a id="usercalloutfeaturenameenumgke_cluster_integration"></a>`GKE_CLUSTER_INTEGRATION` | Callout feature name for gke_cluster_integration. |
| <a id="usercalloutfeaturenameenumgold_trial_billings"></a>`GOLD_TRIAL_BILLINGS` | Callout feature name for gold_trial_billings. |
| <a id="usercalloutfeaturenameenummerge_request_settings_moved_callout"></a>`MERGE_REQUEST_SETTINGS_MOVED_CALLOUT` | Callout feature name for merge_request_settings_moved_callout. |
| <a id="usercalloutfeaturenameenummr_experience_survey"></a>`MR_EXPERIENCE_SURVEY` | Callout feature name for mr_experience_survey. |
| <a id="usercalloutfeaturenameenumnamespace_storage_limit_banner_alert_threshold"></a>`NAMESPACE_STORAGE_LIMIT_BANNER_ALERT_THRESHOLD` | Callout feature name for namespace_storage_limit_banner_alert_threshold. |
| <a id="usercalloutfeaturenameenumnamespace_storage_limit_banner_error_threshold"></a>`NAMESPACE_STORAGE_LIMIT_BANNER_ERROR_THRESHOLD` | Callout feature name for namespace_storage_limit_banner_error_threshold. |
@ -21288,12 +21286,6 @@ A `CiBuildID` is a global ID. It is encoded as a string.
An example `CiBuildID` is: `"gid://gitlab/Ci::Build/1"`.
### `CiJobArtifactID`
A `CiJobArtifactID` is a global ID. It is encoded as a string.
An example `CiJobArtifactID` is: `"gid://gitlab/Ci::JobArtifact/1"`.
### `CiPipelineID`
A `CiPipelineID` is a global ID. It is encoded as a string.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View file

@ -309,15 +309,15 @@ To resolve a thread:
At the top of the page, the number of unresolved threads is updated:
![Count of unresolved threads](img/unresolved_threads_v15.png)
![Count of unresolved threads](img/unresolved_threads_v15_4.png)
### Move all unresolved threads in a merge request to an issue
If you have multiple unresolved threads in a merge request, you can
create an issue to resolve them separately. In the merge request, at the top of the page,
select **Create issue to resolve all threads** (**{issue-new}**):
click the ellipsis icon button (**{ellipsis_v}**) in the threads control and then select **Create issue to resolve all threads**:
![Open new issue for all unresolved threads](img/create-new-issue_v15.png)
![Open new issue for all unresolved threads](img/create_new_issue_v15_4.png)
All threads are marked as resolved, and a link is added from the merge request to
the newly created issue.

View file

@ -59,9 +59,9 @@ module Sidebars
override :active_routes
def active_routes
if context.project.issues_enabled?
{ controller: :merge_requests }
{ controller: 'projects/merge_requests' }
else
{ controller: [:merge_requests, :milestones] }
{ controller: ['projects/merge_requests', :milestones] }
end
end
end

View file

@ -13,6 +13,7 @@ module Sidebars
add_item(webhooks_menu_item)
add_item(access_tokens_menu_item)
add_item(repository_menu_item)
add_item(merge_requests_menu_item)
add_item(ci_cd_menu_item)
add_item(packages_and_registries_menu_item)
add_item(pages_menu_item)
@ -150,6 +151,17 @@ module Sidebars
item_id: :usage_quotas
)
end
def merge_requests_menu_item
return unless context.project.merge_requests_enabled?
::Sidebars::MenuItem.new(
title: _('Merge requests'),
link: project_settings_merge_requests_path(context.project),
active_routes: { path: 'projects/settings/merge_requests#show' },
item_id: :merge_requests
)
end
end
end
end

View file

@ -13345,6 +13345,9 @@ msgstr ""
msgid "DesignManagement|Discard comment"
msgstr ""
msgid "DesignManagement|Discussion"
msgstr ""
msgid "DesignManagement|Download design"
msgstr ""
@ -24759,6 +24762,9 @@ msgstr ""
msgid "Merge requests"
msgstr ""
msgid "Merge requests and approvals settings have moved."
msgstr ""
msgid "Merge requests are a place to propose changes you've made to a project and discuss those changes with others"
msgstr ""
@ -27029,6 +27035,9 @@ msgid_plural "On %{end_date}, your trial will end and %{namespace_name} will be
msgstr[0] ""
msgstr[1] ""
msgid "On the left sidebar, select %{merge_requests_link} to view them."
msgstr ""
msgid "On track"
msgstr ""

View file

@ -6,8 +6,7 @@ module QA
extend self
def enable_merge_trains
Page::Project::Menu.perform(&:go_to_general_settings)
Page::Project::Settings::Main.perform(&:expand_merge_requests_settings)
Page::Project::Menu.perform(&:go_to_merge_request_settings)
Page::Project::Settings::MergeRequest.perform(&:enable_merge_train)
end

View file

@ -13,11 +13,14 @@ module QA
view 'app/views/projects/edit.html.haml' do
element :advanced_settings_content
element :merge_request_settings_content
element :visibility_features_permissions_content
element :badges_settings_content
end
view 'app/views/projects/settings/merge_requests/show.html.haml' do
element :merge_request_settings_content
end
view 'app/views/projects/settings/_general.html.haml' do
element :project_name_field
element :save_naming_topics_avatar_button
@ -42,12 +45,6 @@ module QA
end
end
def expand_merge_requests_settings(&block)
expand_content(:merge_request_settings_content) do
MergeRequest.perform(&block)
end
end
def expand_visibility_project_features_permissions(&block)
expand_content(:visibility_features_permissions_content) do
VisibilityFeaturesPermissions.perform(&block)

View file

@ -7,7 +7,7 @@ module QA
class MergeRequest < QA::Page::Base
include QA::Page::Settings::Common
view 'app/views/projects/edit.html.haml' do
view 'app/views/projects/settings/merge_requests/show.html.haml' do
element :save_merge_request_changes_button
end

View file

@ -77,6 +77,14 @@ module QA
end
end
def go_to_merge_request_settings
hover_settings do
within_submenu do
click_element(:sidebar_menu_item_link, menu_item: 'Merge requests')
end
end
end
private
def hover_settings

View file

@ -12,12 +12,10 @@ module QA
it 'user rebases source branch of merge request', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347735' do
merge_request.project.visit!
Page::Project::Menu.perform(&:go_to_general_settings)
Page::Project::Settings::Main.perform do |main|
main.expand_merge_requests_settings do |settings|
Page::Project::Menu.perform(&:go_to_merge_request_settings)
Page::Project::Settings::MergeRequest.perform do |settings|
settings.enable_ff_only
end
end
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = merge_request.project

View file

@ -8,12 +8,10 @@ module QA
# Require one approval from any eligible user on any branch
# This will confirm that this type of unrestricted approval is
# also satisfied when a code owner grants approval
Page::Project::Menu.perform(&:go_to_general_settings)
Page::Project::Settings::Main.perform do |main|
main.expand_merge_request_approvals_settings do |settings|
Page::Project::Menu.perform(&:go_to_merge_request_settings)
Page::Project::Settings::MergeRequest.perform do |settings|
settings.set_default_number_of_approvals_required(1)
end
end
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project

View file

@ -0,0 +1,52 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Projects::Settings::MergeRequestsController do
let(:project) { create(:project_empty_repo, :public) }
let(:user) { create(:user) }
before do
project.add_maintainer(user)
sign_in(user)
end
describe 'GET show' do
it 'renders show with 200 status code' do
get :show, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:show)
end
end
describe '#update', :enable_admin_mode do
render_views
let(:admin) { create(:admin) }
before do
sign_in(admin)
end
it 'updates Fast Forward Merge attributes' do
controller.instance_variable_set(:@project, project)
params = {
merge_method: :ff
}
put :update,
params: {
namespace_id: project.namespace,
project_id: project.id,
project: params
}
expect(response).to redirect_to project_settings_merge_requests_path(project)
params.each do |param, value|
expect(project.public_send(param)).to eq(value)
end
end
end
end

View file

@ -0,0 +1,261 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Projects > Settings > Merge requests' do
include ProjectForksHelper
let(:user) { create(:user) }
let(:project) { create(:project, :public, namespace: user.namespace, path: 'gitlab', name: 'sample') }
before do
sign_in(user)
visit(project_settings_merge_requests_path(project))
end
it 'shows "Merge commit" strategy' do
page.within '.merge-request-settings-form' do
expect(page).to have_content 'Merge commit'
end
end
it 'shows "Merge commit with semi-linear history " strategy' do
page.within '.merge-request-settings-form' do
expect(page).to have_content 'Merge commit with semi-linear history'
end
end
it 'shows "Fast-forward merge" strategy' do
page.within '.merge-request-settings-form' do
expect(page).to have_content 'Fast-forward merge'
end
end
it 'shows Squash commit options', :aggregate_failures do
page.within '.merge-request-settings-form' do
expect(page).to have_content 'Do not allow'
expect(page).to have_content 'Squashing is never performed and the checkbox is hidden.'
expect(page).to have_content 'Allow'
expect(page).to have_content 'Checkbox is visible and unselected by default.'
expect(page).to have_content 'Encourage'
expect(page).to have_content 'Checkbox is visible and selected by default.'
expect(page).to have_content 'Require'
end
end
context 'when Merge Request and Pipelines are initially enabled', :js do
context 'when Pipelines are initially enabled' do
it 'shows the Merge Requests settings' do
expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved'
visit edit_project_path(project)
within('.sharing-permissions-form') do
within('[data-for="project[project_feature_attributes][merge_requests_access_level]"]') do
find('.gl-toggle').click
end
end
find('[data-testid="project-features-save-button"]').send_keys(:return)
visit project_settings_merge_requests_path(project)
expect(page).to have_content('Not Found')
end
end
context 'when Pipelines are initially disabled', :js do
before do
project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED)
visit project_settings_merge_requests_path(project)
end
it 'shows the Merge Requests settings that do not depend on Builds feature' do
expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved'
visit edit_project_path(project)
within('.sharing-permissions-form') do
within('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"]') do
find('.gl-toggle').click
end
end
find('[data-testid="project-features-save-button"]').send_keys(:return)
visit project_settings_merge_requests_path(project)
expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved'
end
end
end
context 'when Merge Request are initially disabled', :js do
before do
project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::DISABLED)
visit(project_settings_merge_requests_path(project))
end
it 'does not show the Merge Requests settings' do
expect(page).to have_content('Not Found')
visit edit_project_path(project)
within('.sharing-permissions-form') do
within('[data-for="project[project_feature_attributes][merge_requests_access_level]"]') do
find('.gl-toggle').click
end
end
find('[data-testid="project-features-save-button"]').send_keys(:return)
visit project_settings_merge_requests_path(project)
expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved'
end
end
describe 'Checkbox to enable merge request link', :js do
it 'is initially checked' do
checkbox = find_field('project_printing_merge_request_link_enabled')
expect(checkbox).to be_checked
end
it 'when unchecked sets :printing_merge_request_link_enabled to false' do
uncheck('project_printing_merge_request_link_enabled')
within('.merge-request-settings-form') do
find('.rspec-save-merge-request-changes')
click_on('Save changes')
end
find('.flash-notice')
checkbox = find_field('project_printing_merge_request_link_enabled')
expect(checkbox).not_to be_checked
project.reload
expect(project.printing_merge_request_link_enabled).to be(false)
end
end
describe 'Checkbox to remove source branch after merge', :js do
it 'is initially checked' do
checkbox = find_field('project_remove_source_branch_after_merge')
expect(checkbox).to be_checked
end
it 'when unchecked sets :remove_source_branch_after_merge to false' do
uncheck('project_remove_source_branch_after_merge')
within('.merge-request-settings-form') do
find('.rspec-save-merge-request-changes')
click_on('Save changes')
end
find('.flash-notice')
checkbox = find_field('project_remove_source_branch_after_merge')
expect(checkbox).not_to be_checked
project.reload
expect(project.remove_source_branch_after_merge).to be(false)
end
end
describe 'Squash commits when merging', :js do
it 'initially has :squash_option set to :default_off' do
radio = find_field('project_project_setting_attributes_squash_option_default_off')
expect(radio).to be_checked
end
it 'allows :squash_option to be set to :default_on' do
choose('project_project_setting_attributes_squash_option_default_on')
within('.merge-request-settings-form') do
find('.rspec-save-merge-request-changes')
click_on('Save changes')
end
wait_for_requests
radio = find_field('project_project_setting_attributes_squash_option_default_on')
expect(radio).to be_checked
expect(project.reload.project_setting.squash_option).to eq('default_on')
end
it 'allows :squash_option to be set to :always' do
choose('project_project_setting_attributes_squash_option_always')
within('.merge-request-settings-form') do
find('.rspec-save-merge-request-changes')
click_on('Save changes')
end
wait_for_requests
radio = find_field('project_project_setting_attributes_squash_option_always')
expect(radio).to be_checked
expect(project.reload.project_setting.squash_option).to eq('always')
end
it 'allows :squash_option to be set to :never' do
choose('project_project_setting_attributes_squash_option_never')
within('.merge-request-settings-form') do
find('.rspec-save-merge-request-changes')
click_on('Save changes')
end
wait_for_requests
radio = find_field('project_project_setting_attributes_squash_option_never')
expect(radio).to be_checked
expect(project.reload.project_setting.squash_option).to eq('never')
end
end
describe 'target project settings' do
context 'when project is a fork' do
let_it_be(:upstream) { create(:project, :public) }
let(:project) { fork_project(upstream, user) }
it 'allows to change merge request target project behavior' do
expect(page).to have_content 'The default target project for merge requests'
radio = find_field('project_project_setting_attributes_mr_default_target_self_false')
expect(radio).to be_checked
choose('project_project_setting_attributes_mr_default_target_self_true')
within('.merge-request-settings-form') do
find('.rspec-save-merge-request-changes')
click_on('Save changes')
end
wait_for_requests
radio = find_field('project_project_setting_attributes_mr_default_target_self_true')
expect(radio).to be_checked
expect(project.reload.project_setting.mr_default_target_self).to be_truthy
end
end
it 'does not show target project section' do
expect(page).not_to have_content 'The default target project for merge requests'
end
end
end

View file

@ -9,29 +9,29 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
before do
sign_in(user)
visit edit_project_path(project)
visit project_settings_merge_requests_path(project)
end
it 'shows "Merge commit" strategy' do
page.within '#js-merge-request-settings' do
page.within '.merge-request-settings-form' do
expect(page).to have_content 'Merge commit'
end
end
it 'shows "Merge commit with semi-linear history " strategy' do
page.within '#js-merge-request-settings' do
page.within '.merge-request-settings-form' do
expect(page).to have_content 'Merge commit with semi-linear history'
end
end
it 'shows "Fast-forward merge" strategy' do
page.within '#js-merge-request-settings' do
page.within '.merge-request-settings-form' do
expect(page).to have_content 'Fast-forward merge'
end
end
it 'shows Squash commit options', :aggregate_failures do
page.within '#js-merge-request-settings' do
page.within '.merge-request-settings-form' do
expect(page).to have_content 'Do not allow'
expect(page).to have_content 'Squashing is never performed and the checkbox is hidden.'
@ -52,30 +52,33 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved'
within('.sharing-permissions-form') do
visit edit_project_path(project)
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
find('[data-testid="project-features-save-button"]').send_keys(:return)
end
expect(page).not_to have_content 'Pipelines must succeed'
expect(page).not_to have_content 'All threads must be resolved'
visit project_settings_merge_requests_path(project)
expect(page).to have_content "Page Not Found"
end
end
context 'when Pipelines are initially disabled', :js do
before do
project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED)
visit edit_project_path(project)
visit project_settings_merge_requests_path(project)
end
it 'shows the Merge Requests settings that do not depend on Builds feature' do
expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved'
within('.sharing-permissions-form') do
visit edit_project_path(project)
find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click
find('[data-testid="project-features-save-button"]').send_keys(:return)
end
visit project_settings_merge_requests_path(project)
expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved'
@ -86,18 +89,22 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do
context 'when Merge Request are initially disabled', :js do
before do
project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::DISABLED)
visit edit_project_path(project)
visit project_settings_merge_requests_path(project)
end
it 'does not show the Merge Requests settings' do
expect(page).not_to have_content 'Pipelines must succeed'
expect(page).not_to have_content 'All threads must be resolved'
visit edit_project_path(project)
within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
find('[data-testid="project-features-save-button"]').send_keys(:return)
end
visit project_settings_merge_requests_path(project)
expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved'
end

View file

@ -28,26 +28,12 @@ RSpec.describe 'Projects > Settings > Visibility settings', :js do
expect(visibility_select_container).to have_content 'Only accessible by project members. Membership must be explicitly granted to each user.'
end
context 'merge requests select' do
it 'hides merge requests section' do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click
expect(page).to have_selector('.merge-requests-feature', visible: false)
end
context 'given project with merge_requests_disabled access level' do
let(:project) { create(:project, :merge_requests_disabled, namespace: user.namespace) }
it 'hides merge requests section' do
expect(page).to have_selector('.merge-requests-feature', visible: false)
end
end
end
context 'builds select' do
it 'hides builds select section' do
find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click
visit project_settings_merge_requests_path(project)
expect(page).to have_selector('.builds-feature', visible: false)
end
@ -55,6 +41,8 @@ RSpec.describe 'Projects > Settings > Visibility settings', :js do
let(:project) { create(:project, :builds_disabled, namespace: user.namespace) }
it 'hides builds select section' do
visit project_settings_merge_requests_path(project)
expect(page).to have_selector('.builds-feature', visible: false)
end
end

View file

@ -418,8 +418,7 @@ RSpec.describe 'Project' do
visit path
end
it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="project[name]"]' },
{ form: '.rspec-merge-request-settings', input: '#project_printing_merge_request_link_enabled' }]
it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="project[name]"]' }]
end
describe 'view for a user without an access to a repo' do

View file

@ -43,6 +43,7 @@ describe('Design note component', () => {
wrapper = shallowMountExtended(DesignNote, {
propsData: {
note: {},
noteableId: 'gid://gitlab/DesignManagement::Design/6',
...props,
},
data() {

View file

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Autosave from '~/autosave';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
const showModal = jest.fn();
@ -13,6 +14,7 @@ const GlModal = {
describe('Design reply form component', () => {
let wrapper;
let originalGon;
const findTextarea = () => wrapper.find('textarea');
const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' });
@ -24,6 +26,7 @@ describe('Design reply form component', () => {
propsData: {
value: '',
isSaving: false,
noteableId: 'gid://gitlab/DesignManagement::Design/6',
...props,
},
stubs: { GlModal },
@ -31,8 +34,14 @@ describe('Design reply form component', () => {
});
}
beforeEach(() => {
originalGon = window.gon;
window.gon.current_user_id = 1;
});
afterEach(() => {
wrapper.destroy();
window.gon = originalGon;
});
it('textarea has focus after component mount', () => {
@ -66,6 +75,25 @@ describe('Design reply form component', () => {
expect(findSubmitButton().html()).toMatchSnapshot();
});
it.each`
discussionId | shortDiscussionId
${undefined} | ${'new'}
${'gid://gitlab/DiffDiscussion/123'} | ${123}
`(
'initializes autosave support on discussion with proper key',
async ({ discussionId, shortDiscussionId }) => {
createComponent({ discussionId });
await nextTick();
// We discourage testing `wrapper.vm` properties but
// since `autosave` library instantiates on component
// there's no other way to test whether instantiation
// happened correctly or not.
expect(wrapper.vm.autosaveDiscussion).toBeInstanceOf(Autosave);
expect(wrapper.vm.autosaveDiscussion.key).toBe(`autosave/Discussion/6/${shortDiscussionId}`);
},
);
describe('when form has no text', () => {
beforeEach(() => {
createComponent({
@ -120,28 +148,37 @@ describe('Design reply form component', () => {
});
it('emits submitForm event on Comment button click', async () => {
const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
findSubmitButton().vm.$emit('click');
await nextTick();
expect(wrapper.emitted('submit-form')).toBeTruthy();
expect(autosaveResetSpy).toHaveBeenCalled();
});
it('emits submitForm event on textarea ctrl+enter keydown', async () => {
const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
findTextarea().trigger('keydown.enter', {
ctrlKey: true,
});
await nextTick();
expect(wrapper.emitted('submit-form')).toBeTruthy();
expect(autosaveResetSpy).toHaveBeenCalled();
});
it('emits submitForm event on textarea meta+enter keydown', async () => {
const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
findTextarea().trigger('keydown.enter', {
metaKey: true,
});
await nextTick();
expect(wrapper.emitted('submit-form')).toBeTruthy();
expect(autosaveResetSpy).toHaveBeenCalled();
});
it('emits input event on changing textarea content', async () => {
@ -180,10 +217,13 @@ describe('Design reply form component', () => {
});
it('emits cancelForm event on modal Ok button click', () => {
const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
findTextarea().trigger('keyup.esc');
findModal().vm.$emit('ok');
expect(wrapper.emitted('cancel-form')).toBeTruthy();
expect(autosaveResetSpy).toHaveBeenCalled();
});
});
});

View file

@ -528,7 +528,6 @@ export const mockPipelineJobsQueryResponse = {
artifacts: {
nodes: [
{
id: 'gid://gitlab/Ci::JobArtifact/101',
downloadPath: '/root/ci-project/-/jobs/620/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
@ -581,7 +580,6 @@ export const mockPipelineJobsQueryResponse = {
artifacts: {
nodes: [
{
id: 'gid://gitlab/Ci::JobArtifact/102',
downloadPath: '/root/ci-project/-/jobs/619/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',

View file

@ -356,14 +356,12 @@ export const securityReportMergeRequestDownloadPathsQueryResponse = {
artifacts: {
nodes: [
{
id: 'gid://gitlab/Ci::JobArtifact/101',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
id: 'gid://gitlab/Ci::JobArtifact/102',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=secret_detection',
fileType: 'SECRET_DETECTION',
@ -380,14 +378,12 @@ export const securityReportMergeRequestDownloadPathsQueryResponse = {
artifacts: {
nodes: [
{
id: 'gid://gitlab/Ci::JobArtifact/103',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
id: 'gid://gitlab/Ci::JobArtifact/104',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=sast',
fileType: 'SAST',
@ -404,14 +400,12 @@ export const securityReportMergeRequestDownloadPathsQueryResponse = {
artifacts: {
nodes: [
{
id: 'gid://gitlab/Ci::JobArtifact/105',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
id: 'gid://gitlab/Ci::JobArtifact/106',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=sast',
fileType: 'SAST',
@ -428,21 +422,18 @@ export const securityReportMergeRequestDownloadPathsQueryResponse = {
artifacts: {
nodes: [
{
id: 'gid://gitlab/Ci::JobArtifact/107',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=archive',
fileType: 'ARCHIVE',
__typename: 'CiJobArtifact',
},
{
id: 'gid://gitlab/Ci::JobArtifact/108',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
id: 'gid://gitlab/Ci::JobArtifact/109',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=metadata',
fileType: 'METADATA',
@ -477,14 +468,12 @@ export const securityReportPipelineDownloadPathsQueryResponse = {
artifacts: {
nodes: [
{
id: 'gid://gitlab/Ci::JobArtifact/110',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
id: 'gid://gitlab/Ci::JobArtifact/111',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=secret_detection',
fileType: 'SECRET_DETECTION',
@ -501,14 +490,12 @@ export const securityReportPipelineDownloadPathsQueryResponse = {
artifacts: {
nodes: [
{
id: 'gid://gitlab/Ci::JobArtifact/112',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
id: 'gid://gitlab/Ci::JobArtifact/113',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=sast',
fileType: 'SAST',
@ -525,14 +512,12 @@ export const securityReportPipelineDownloadPathsQueryResponse = {
artifacts: {
nodes: [
{
id: 'gid://gitlab/Ci::JobArtifact/114',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
id: 'gid://gitlab/Ci::JobArtifact/115',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=sast',
fileType: 'SAST',
@ -549,21 +534,18 @@ export const securityReportPipelineDownloadPathsQueryResponse = {
artifacts: {
nodes: [
{
id: 'gid://gitlab/Ci::JobArtifact/116',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=archive',
fileType: 'ARCHIVE',
__typename: 'CiJobArtifact',
},
{
id: 'gid://gitlab/Ci::JobArtifact/117',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
id: 'gid://gitlab/Ci::JobArtifact/118',
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=metadata',
fileType: 'METADATA',

View file

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['CiJobArtifact'] do
it 'has the correct fields' do
expected_fields = [:id, :download_path, :file_type, :name, :size, :expire_at]
expected_fields = [:download_path, :file_type, :name]
expect(described_class).to have_graphql_fields(*expected_fields)
end

View file

@ -133,6 +133,12 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
end
end
describe 'Merge requests' do
let(:item_id) { :merge_requests }
it_behaves_like 'access rights checks'
end
describe 'Packages and registries' do
let(:item_id) { :packages_and_registries }
let(:packages_enabled) { false }

View file

@ -0,0 +1,258 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Users::MigrateRecordsToGhostUserService do
let!(:user) { create(:user) }
let(:service) { described_class.new(user, admin) }
let_it_be(:admin) { create(:admin) }
let_it_be(:project) { create(:project, :repository) }
context "when migrating a user's associated records to the ghost user" do
context 'for issues' do
context 'when deleted user is present as both author and edited_user' do
include_examples 'migrating records to the ghost user', Issue, [:author, :last_edited_by] do
let(:created_record) do
create(:issue, project: project, author: user, last_edited_by: user)
end
end
end
context 'when deleted user is present only as edited_user' do
include_examples 'migrating records to the ghost user', Issue, [:last_edited_by] do
let(:created_record) { create(:issue, project: project, author: create(:user), last_edited_by: user) }
end
end
context "when deleted user is the assignee" do
let!(:issue) { create(:issue, project: project, assignees: [user]) }
it 'migrates the issue so that it is "Unassigned"' do
service.execute
migrated_issue = Issue.find_by_id(issue.id)
expect(migrated_issue).to be_present
expect(migrated_issue.assignees).to be_empty
end
end
end
context 'for merge requests' do
context 'when deleted user is present as both author and merge_user' do
include_examples 'migrating records to the ghost user', MergeRequest, [:author, :merge_user] do
let(:created_record) do
create(:merge_request, source_project: project,
author: user,
merge_user: user,
target_branch: "first")
end
end
end
context 'when deleted user is present only as both merge_user' do
include_examples 'migrating records to the ghost user', MergeRequest, [:merge_user] do
let(:created_record) do
create(:merge_request, source_project: project,
merge_user: user,
target_branch: "first")
end
end
end
context "when deleted user is the assignee" do
let!(:merge_request) { create(:merge_request, source_project: project, assignees: [user]) }
it 'migrates the merge request so that it is "Unassigned"' do
service.execute
migrated_merge_request = MergeRequest.find_by_id(merge_request.id)
expect(migrated_merge_request).to be_present
expect(migrated_merge_request.assignees).to be_empty
end
end
end
context 'for notes' do
include_examples 'migrating records to the ghost user', Note do
let(:created_record) { create(:note, project: project, author: user) }
end
end
context 'for abuse reports' do
include_examples 'migrating records to the ghost user', AbuseReport do
let(:created_record) { create(:abuse_report, reporter: user, user: create(:user)) }
end
end
context 'for award emoji' do
include_examples 'migrating records to the ghost user', AwardEmoji, [:user] do
let(:created_record) { create(:award_emoji, user: user) }
context "when the awardable already has an award emoji of the same name assigned to the ghost user" do
let(:awardable) { create(:issue) }
let!(:existing_award_emoji) { create(:award_emoji, user: User.ghost, name: "thumbsup", awardable: awardable) }
let!(:award_emoji) { create(:award_emoji, user: user, name: "thumbsup", awardable: awardable) }
it "migrates the award emoji regardless" do
service.execute
migrated_record = AwardEmoji.find_by_id(award_emoji.id)
expect(migrated_record.user).to eq(User.ghost)
end
it "does not leave the migrated award emoji in an invalid state" do
service.execute
migrated_record = AwardEmoji.find_by_id(award_emoji.id)
expect(migrated_record).to be_valid
end
end
end
end
context 'for snippets' do
include_examples 'migrating records to the ghost user', Snippet do
let(:created_record) { create(:snippet, project: project, author: user) }
end
end
context 'for reviews' do
include_examples 'migrating records to the ghost user', Review, [:author] do
let(:created_record) { create(:review, author: user) }
end
end
end
context 'on post-migrate cleanups' do
it 'destroys the user and personal namespace' do
namespace = user.namespace
allow(user).to receive(:destroy).and_call_original
service.execute
expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound)
expect { Namespace.find(namespace.id) }.to raise_error(ActiveRecord::RecordNotFound)
end
it 'deletes user associations in batches' do
expect(user).to receive(:destroy_dependent_associations_in_batches)
service.execute
end
context 'for batched nullify' do
it 'nullifies related associations in batches' do
expect(user).to receive(:nullify_dependent_associations_in_batches).and_call_original
service.execute
end
it 'nullifies last_updated_issues, closed_issues, resource_label_events' do
issue = create(:issue, closed_by: user, updated_by: user)
resource_label_event = create(:resource_label_event, user: user)
service.execute
issue.reload
resource_label_event.reload
expect(issue.closed_by).to be_nil
expect(issue.updated_by).to be_nil
expect(resource_label_event.user).to be_nil
end
end
context 'for snippets' do
let(:gitlab_shell) { Gitlab::Shell.new }
it 'does not include snippets when deleting in batches' do
expect(user).to receive(:destroy_dependent_associations_in_batches).with({ exclude: [:snippets] })
service.execute
end
it 'calls the bulk snippet destroy service for the user personal snippets' do
repo1 = create(:personal_snippet, :repository, author: user).snippet_repository
repo2 = create(:project_snippet, :repository, project: project, author: user).snippet_repository
aggregate_failures do
expect(gitlab_shell.repository_exists?(repo1.shard_name, "#{repo1.disk_path}.git")).to be(true)
expect(gitlab_shell.repository_exists?(repo2.shard_name, "#{repo2.disk_path}.git")).to be(true)
end
# Call made when destroying user personal projects
expect(Snippets::BulkDestroyService).not_to(
receive(:new).with(admin, project.snippets).and_call_original)
# Call to remove user personal snippets and for
# project snippets where projects are not user personal
# ones
expect(Snippets::BulkDestroyService).to(
receive(:new).with(admin, user.snippets.only_personal_snippets).and_call_original)
service.execute
aggregate_failures do
expect(gitlab_shell.repository_exists?(repo1.shard_name, "#{repo1.disk_path}.git")).to be(false)
expect(gitlab_shell.repository_exists?(repo2.shard_name, "#{repo2.disk_path}.git")).to be(true)
end
end
it 'calls the bulk snippet destroy service with hard delete option if it is present' do
# this avoids getting into Projects::DestroyService as it would
# call Snippets::BulkDestroyService first!
allow(user).to receive(:personal_projects).and_return([])
expect_next_instance_of(Snippets::BulkDestroyService) do |bulk_destroy_service|
expect(bulk_destroy_service).to receive(:execute).with({ skip_authorization: true }).and_call_original
end
service.execute(hard_delete: true)
end
it 'does not delete project snippets that the user is the author of' do
repo = create(:project_snippet, :repository, author: user).snippet_repository
service.execute
expect(gitlab_shell.repository_exists?(repo.shard_name, "#{repo.disk_path}.git")).to be(true)
expect(User.ghost.snippets).to include(repo.snippet)
end
context 'when an error is raised deleting snippets' do
it 'does not delete user' do
snippet = create(:personal_snippet, :repository, author: user)
bulk_service = double
allow(Snippets::BulkDestroyService).to receive(:new).and_call_original
allow(Snippets::BulkDestroyService).to receive(:new).with(admin, user.snippets).and_return(bulk_service)
allow(bulk_service).to receive(:execute).and_return(ServiceResponse.error(message: 'foo'))
aggregate_failures do
expect { service.execute }.to(
raise_error(Users::MigrateRecordsToGhostUserService::DestroyError, 'foo' ))
expect(snippet.reload).not_to be_nil
expect(
gitlab_shell.repository_exists?(snippet.repository_storage,
"#{snippet.disk_path}.git")
).to be(true)
end
end
end
end
context 'when hard_delete option is given' do
it 'will not ghost certain records' do
issue = create(:issue, author: user)
service.execute(hard_delete: true)
expect(Issue).not_to exist(issue.id)
end
end
end
end

View file

@ -109,6 +109,7 @@ RSpec.shared_context 'project navbar structure' do
_('Webhooks'),
_('Access Tokens'),
_('Repository'),
_('Merge requests'),
_('CI/CD'),
_('Packages and registries'),
_('Monitor'),

View file

@ -0,0 +1,39 @@
# frozen_string_literal: true
RSpec.shared_examples 'migrating records to the ghost user' do |record_class, fields|
record_class_name = record_class.to_s.titleize.downcase
let(:project) do
case record_class
when MergeRequest
create(:project, :repository)
else
create(:project)
end
end
before do
project.add_developer(user)
end
context "for a #{record_class_name} the user has created" do
let!(:record) { created_record }
let(:migrated_fields) { fields || [:author] }
it "does not delete the #{record_class_name}" do
service.execute
expect(record_class.find_by_id(record.id)).to be_present
end
it 'migrates all associated fields to the "Ghost user"' do
service.execute
migrated_record = record_class.find_by_id(record.id)
migrated_fields.each do |field|
expect(migrated_record.public_send(field)).to eq(User.ghost)
end
end
end
end

View file

@ -28,62 +28,6 @@ RSpec.describe 'projects/edit' do
end
end
context 'merge suggestions settings' do
it 'displays a placeholder if none is set' do
render
expect(rendered).to have_field('project[suggestion_commit_message]', placeholder: "Apply %{suggestions_count} suggestion(s) to %{files_count} file(s)")
end
it 'displays the user entered value' do
project.update!(suggestion_commit_message: 'refactor: changed %{file_paths}')
render
expect(rendered).to have_field('project[suggestion_commit_message]', with: 'refactor: changed %{file_paths}')
end
end
context 'merge commit template' do
it 'displays default template if none is set' do
render
expect(rendered).to have_field('project[merge_commit_template_or_default]', with: <<~MSG.rstrip)
Merge branch '%{source_branch}' into '%{target_branch}'
%{title}
%{issues}
See merge request %{reference}
MSG
end
it 'displays the user entered value' do
project.update!(merge_commit_template: '%{title}')
render
expect(rendered).to have_field('project[merge_commit_template_or_default]', with: '%{title}')
end
end
context 'squash template' do
it 'displays default template if none is set' do
render
expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{title}')
end
it 'displays the user entered value' do
project.update!(squash_commit_template: '%{first_multiline_commit}')
render
expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{first_multiline_commit}')
end
end
context 'forking' do
before do
assign(:project, project)

View file

@ -0,0 +1,78 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'projects/settings/merge_requests/show' do
include Devise::Test::ControllerHelpers
include ProjectForksHelper
let(:project) { create(:project) }
let(:user) { create(:admin) }
before do
assign(:project, project)
allow(controller).to receive(:current_user).and_return(user)
allow(view).to receive_messages(current_user: user,
can?: true,
current_application_settings: Gitlab::CurrentSettings.current_application_settings)
end
describe 'merge suggestions settings' do
it 'displays a placeholder if none is set' do
render
placeholder = "Apply %{suggestions_count} suggestion(s) to %{files_count} file(s)"
expect(rendered).to have_field('project[suggestion_commit_message]', placeholder: placeholder)
end
it 'displays the user entered value' do
project.update!(suggestion_commit_message: 'refactor: changed %{file_paths}')
render
expect(rendered).to have_field('project[suggestion_commit_message]', with: 'refactor: changed %{file_paths}')
end
end
describe 'merge commit template' do
it 'displays default template if none is set' do
render
expect(rendered).to have_field('project[merge_commit_template_or_default]', with: <<~MSG.rstrip)
Merge branch '%{source_branch}' into '%{target_branch}'
%{title}
%{issues}
See merge request %{reference}
MSG
end
it 'displays the user entered value' do
project.update!(merge_commit_template: '%{title}')
render
expect(rendered).to have_field('project[merge_commit_template_or_default]', with: '%{title}')
end
end
describe 'squash template' do
it 'displays default template if none is set' do
render
expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{title}')
end
it 'displays the user entered value' do
project.update!(squash_commit_template: '%{first_multiline_commit}')
render
expect(rendered).to have_field('project[squash_commit_template_or_default]', with: '%{first_multiline_commit}')
end
end
end