Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
6431ee6152
commit
65b6ccd12e
51 changed files with 1084 additions and 182 deletions
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -12,7 +12,6 @@ query getJobs($fullPath: ID!, $after: String, $first: Int = 30, $statuses: [CiJo
|
|||
nodes {
|
||||
artifacts {
|
||||
nodes {
|
||||
id
|
||||
downloadPath
|
||||
fileType
|
||||
}
|
||||
|
|
|
@ -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' });
|
|
@ -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 = () => {
|
||||
|
|
|
@ -12,7 +12,6 @@ query getPipelineJobs($fullPath: ID!, $iid: ID!, $after: String) {
|
|||
nodes {
|
||||
artifacts {
|
||||
nodes {
|
||||
id
|
||||
downloadPath
|
||||
fileType
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ fragment JobArtifacts on Pipeline {
|
|||
name
|
||||
artifacts {
|
||||
nodes {
|
||||
id
|
||||
downloadPath
|
||||
fileType
|
||||
}
|
||||
|
|
|
@ -15,7 +15,6 @@ query securityReportDownloadPaths(
|
|||
name
|
||||
artifacts {
|
||||
nodes {
|
||||
id
|
||||
downloadPath
|
||||
fileType
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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')
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
107
app/services/users/migrate_records_to_ghost_user_service.rb
Normal file
107
app/services/users/migrate_records_to_ghost_user_service.rb
Normal 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')
|
|
@ -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
|
||||
|
|
18
app/views/projects/settings/merge_requests/show.html.haml
Normal file
18
app/views/projects/settings/merge_requests/show.html.haml
Normal 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
|
|
@ -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]
|
||||
|
|
|
@ -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 |
BIN
doc/user/discussions/img/create_new_issue_v15_4.png
Normal file
BIN
doc/user/discussions/img/create_new_issue_v15_4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
Before Width: | Height: | Size: 2.7 KiB |
BIN
doc/user/discussions/img/unresolved_threads_v15_4.png
Normal file
BIN
doc/user/discussions/img/unresolved_threads_v15_4.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.6 KiB |
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
261
spec/features/projects/settings/merge_requests_settings_spec.rb
Normal file
261
spec/features/projects/settings/merge_requests_settings_spec.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -43,6 +43,7 @@ describe('Design note component', () => {
|
|||
wrapper = shallowMountExtended(DesignNote, {
|
||||
propsData: {
|
||||
note: {},
|
||||
noteableId: 'gid://gitlab/DesignManagement::Design/6',
|
||||
...props,
|
||||
},
|
||||
data() {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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
|
|
@ -109,6 +109,7 @@ RSpec.shared_context 'project navbar structure' do
|
|||
_('Webhooks'),
|
||||
_('Access Tokens'),
|
||||
_('Repository'),
|
||||
_('Merge requests'),
|
||||
_('CI/CD'),
|
||||
_('Packages and registries'),
|
||||
_('Monitor'),
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue