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

View file

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

View file

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

View file

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

View file

@ -12,7 +12,6 @@ query getJobs($fullPath: ID!, $after: String, $first: Int = 30, $statuses: [CiJo
nodes { nodes {
artifacts { artifacts {
nodes { nodes {
id
downloadPath downloadPath
fileType 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-project-usage-limitations-callout',
'.js-namespace-storage-alert', '.js-namespace-storage-alert',
'.js-web-hook-disabled-callout', '.js-web-hook-disabled-callout',
'.js-merge-request-settings-callout',
]; ];
const initCallouts = () => { const initCallouts = () => {

View file

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

View file

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

View file

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

View file

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

View file

@ -383,6 +383,10 @@ input[type='checkbox']:hover {
.line_holder { .line_holder {
pre { pre {
padding: 0; // This overrides the existing style that will add space between each line. 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 { 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 class JobArtifactType < BaseObject
graphql_name 'CiJobArtifact' graphql_name 'CiJobArtifact'
field :id, Types::GlobalIDType[::Ci::JobArtifact], null: false,
description: 'ID of the artifact.'
field :download_path, GraphQL::Types::String, null: true, field :download_path, GraphQL::Types::String, null: true,
description: "URL for downloading the artifact's file." description: "URL for downloading the artifact's file."
@ -19,12 +16,6 @@ module Types
description: 'File name of the artifact.', description: 'File name of the artifact.',
method: :filename 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 def download_path
::Gitlab::Routing.url_helpers.download_project_job_artifacts_path( ::Gitlab::Routing.url_helpers.download_project_job_artifacts_path(
object.project, object.project,

View file

@ -10,6 +10,7 @@ module Users
REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout' REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout'
UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout' UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_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 REGISTRATION_ENABLED_CALLOUT_ALLOWED_CONTROLLER_PATHS = [/^root/, /^dashboard\S*/, /^admin\S*/].freeze
WEB_HOOK_DISABLED = 'web_hook_disabled' WEB_HOOK_DISABLED = 'web_hook_disabled'
@ -74,6 +75,10 @@ module Users
user_dismissed?(WEB_HOOK_DISABLED, last_failure, project: project) user_dismissed?(WEB_HOOK_DISABLED, last_failure, project: project)
end end
def show_merge_request_settings_callout?
!user_dismissed?(MERGE_REQUEST_SETTINGS_MOVED_CALLOUT)
end
private private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil, project: nil) 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_warning_threshold: 56, # EE-only
namespace_storage_limit_banner_alert_threshold: 57, # EE-only namespace_storage_limit_banner_alert_threshold: 57, # EE-only
namespace_storage_limit_banner_error_threshold: 58, # 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, 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 %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) } .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' } } - if show_merge_request_settings_callout?
.settings-header %section.settings.expanded
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Merge requests') = render Pajamas::AlertComponent.new(variant: :info,
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do title: _('Merge requests and approvals settings have moved.'),
= expanded ? _('Collapse') : _('Expand') 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|
= render_if_exists 'projects/merge_request_settings_description_text' = 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 }
.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
%section.settings.no-animate{ class: ('expanded' if expanded), data: { qa_selector: 'badges_settings_content' } } %section.settings.no-animate{ class: ('expanded' if expanded), data: { qa_selector: 'badges_settings_content' } }
.settings-header .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 resource :packages_and_registries, only: [:show] do
get '/cleanup_image_tags', to: 'packages_and_registries#cleanup_tags' get '/cleanup_image_tags', to: 'packages_and_registries#cleanup_tags'
end end
resource :merge_requests, only: [:show, :update]
end end
resources :usage_quotas, only: [:index] resources :usage_quotas, only: [:index]

View file

@ -10267,11 +10267,8 @@ CI/CD variables for a GitLab instance.
| Name | Type | Description | | Name | Type | Description |
| ---- | ---- | ----------- | | ---- | ---- | ----------- |
| <a id="cijobartifactdownloadpath"></a>`downloadPath` | [`String`](#string) | URL for downloading the artifact's file. | | <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="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="cijobartifactname"></a>`name` | [`String`](#string) | File name of the artifact. |
| <a id="cijobartifactsize"></a>`size` | [`Int!`](#int) | Size of the artifact in bytes. |
### `CiJobTokenScopeType` ### `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="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="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="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="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_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. | | <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"`. 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` ### `CiPipelineID`
A `CiPipelineID` is a global ID. It is encoded as a string. 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: 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 ### Move all unresolved threads in a merge request to an issue
If you have multiple unresolved threads in a merge request, you can 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, 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 All threads are marked as resolved, and a link is added from the merge request to
the newly created issue. the newly created issue.

View file

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

View file

@ -13,6 +13,7 @@ module Sidebars
add_item(webhooks_menu_item) add_item(webhooks_menu_item)
add_item(access_tokens_menu_item) add_item(access_tokens_menu_item)
add_item(repository_menu_item) add_item(repository_menu_item)
add_item(merge_requests_menu_item)
add_item(ci_cd_menu_item) add_item(ci_cd_menu_item)
add_item(packages_and_registries_menu_item) add_item(packages_and_registries_menu_item)
add_item(pages_menu_item) add_item(pages_menu_item)
@ -150,6 +151,17 @@ module Sidebars
item_id: :usage_quotas item_id: :usage_quotas
) )
end 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 end
end end

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ module QA
class MergeRequest < QA::Page::Base class MergeRequest < QA::Page::Base
include QA::Page::Settings::Common 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 element :save_merge_request_changes_button
end end

View file

@ -77,6 +77,14 @@ module QA
end end
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 private
def hover_settings 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 it 'user rebases source branch of merge request', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347735' do
merge_request.project.visit! merge_request.project.visit!
Page::Project::Menu.perform(&:go_to_general_settings) Page::Project::Menu.perform(&:go_to_merge_request_settings)
Page::Project::Settings::Main.perform do |main| Page::Project::Settings::MergeRequest.perform do |settings|
main.expand_merge_requests_settings do |settings|
settings.enable_ff_only settings.enable_ff_only
end end
end
Resource::Repository::ProjectPush.fabricate! do |push| Resource::Repository::ProjectPush.fabricate! do |push|
push.project = merge_request.project push.project = merge_request.project

View file

@ -8,12 +8,10 @@ module QA
# Require one approval from any eligible user on any branch # Require one approval from any eligible user on any branch
# This will confirm that this type of unrestricted approval is # This will confirm that this type of unrestricted approval is
# also satisfied when a code owner grants approval # also satisfied when a code owner grants approval
Page::Project::Menu.perform(&:go_to_general_settings) Page::Project::Menu.perform(&:go_to_merge_request_settings)
Page::Project::Settings::Main.perform do |main| Page::Project::Settings::MergeRequest.perform do |settings|
main.expand_merge_request_approvals_settings do |settings|
settings.set_default_number_of_approvals_required(1) settings.set_default_number_of_approvals_required(1)
end end
end
Resource::Repository::Commit.fabricate_via_api! do |commit| Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project 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 before do
sign_in(user) sign_in(user)
visit edit_project_path(project) visit project_settings_merge_requests_path(project)
end end
it 'shows "Merge commit" strategy' do 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' expect(page).to have_content 'Merge commit'
end end
end end
it 'shows "Merge commit with semi-linear history " strategy' do 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' expect(page).to have_content 'Merge commit with semi-linear history'
end end
end end
it 'shows "Fast-forward merge" strategy' do 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' expect(page).to have_content 'Fast-forward merge'
end end
end end
it 'shows Squash commit options', :aggregate_failures do 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 'Do not allow'
expect(page).to have_content 'Squashing is never performed and the checkbox is hidden.' 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 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved' 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('.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) find('[data-testid="project-features-save-button"]').send_keys(:return)
end
expect(page).not_to have_content 'Pipelines must succeed' visit project_settings_merge_requests_path(project)
expect(page).not_to have_content 'All threads must be resolved'
expect(page).to have_content "Page Not Found"
end end
end end
context 'when Pipelines are initially disabled', :js do context 'when Pipelines are initially disabled', :js do
before do before do
project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED) project.project_feature.update_attribute('builds_access_level', ProjectFeature::DISABLED)
visit edit_project_path(project) visit project_settings_merge_requests_path(project)
end end
it 'shows the Merge Requests settings that do not depend on Builds feature' do 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 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved' 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('.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) 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 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved' 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 context 'when Merge Request are initially disabled', :js do
before do before do
project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::DISABLED) project.project_feature.update_attribute('merge_requests_access_level', ProjectFeature::DISABLED)
visit edit_project_path(project) visit project_settings_merge_requests_path(project)
end end
it 'does not show the Merge Requests settings' do it 'does not show the Merge Requests settings' do
expect(page).not_to have_content 'Pipelines must succeed' expect(page).not_to have_content 'Pipelines must succeed'
expect(page).not_to have_content 'All threads must be resolved' expect(page).not_to have_content 'All threads must be resolved'
visit edit_project_path(project)
within('.sharing-permissions-form') do within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click 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) find('[data-testid="project-features-save-button"]').send_keys(:return)
end end
visit project_settings_merge_requests_path(project)
expect(page).to have_content 'Pipelines must succeed' expect(page).to have_content 'Pipelines must succeed'
expect(page).to have_content 'All threads must be resolved' expect(page).to have_content 'All threads must be resolved'
end 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.' expect(visibility_select_container).to have_content 'Only accessible by project members. Membership must be explicitly granted to each user.'
end 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 context 'builds select' do
it 'hides builds select section' do it 'hides builds select section' do
find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click 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) expect(page).to have_selector('.builds-feature', visible: false)
end end
@ -55,6 +41,8 @@ RSpec.describe 'Projects > Settings > Visibility settings', :js do
let(:project) { create(:project, :builds_disabled, namespace: user.namespace) } let(:project) { create(:project, :builds_disabled, namespace: user.namespace) }
it 'hides builds select section' do it 'hides builds select section' do
visit project_settings_merge_requests_path(project)
expect(page).to have_selector('.builds-feature', visible: false) expect(page).to have_selector('.builds-feature', visible: false)
end end
end end

View file

@ -418,8 +418,7 @@ RSpec.describe 'Project' do
visit path visit path
end end
it_behaves_like 'dirty submit form', [{ form: '.js-general-settings-form', input: 'input[name="project[name]"]' }, 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' }]
end end
describe 'view for a user without an access to a repo' do 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, { wrapper = shallowMountExtended(DesignNote, {
propsData: { propsData: {
note: {}, note: {},
noteableId: 'gid://gitlab/DesignManagement::Design/6',
...props, ...props,
}, },
data() { data() {

View file

@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import Autosave from '~/autosave';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue'; import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
const showModal = jest.fn(); const showModal = jest.fn();
@ -13,6 +14,7 @@ const GlModal = {
describe('Design reply form component', () => { describe('Design reply form component', () => {
let wrapper; let wrapper;
let originalGon;
const findTextarea = () => wrapper.find('textarea'); const findTextarea = () => wrapper.find('textarea');
const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' }); const findSubmitButton = () => wrapper.findComponent({ ref: 'submitButton' });
@ -24,6 +26,7 @@ describe('Design reply form component', () => {
propsData: { propsData: {
value: '', value: '',
isSaving: false, isSaving: false,
noteableId: 'gid://gitlab/DesignManagement::Design/6',
...props, ...props,
}, },
stubs: { GlModal }, stubs: { GlModal },
@ -31,8 +34,14 @@ describe('Design reply form component', () => {
}); });
} }
beforeEach(() => {
originalGon = window.gon;
window.gon.current_user_id = 1;
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
window.gon = originalGon;
}); });
it('textarea has focus after component mount', () => { it('textarea has focus after component mount', () => {
@ -66,6 +75,25 @@ describe('Design reply form component', () => {
expect(findSubmitButton().html()).toMatchSnapshot(); 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', () => { describe('when form has no text', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
@ -120,28 +148,37 @@ describe('Design reply form component', () => {
}); });
it('emits submitForm event on Comment button click', async () => { it('emits submitForm event on Comment button click', async () => {
const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
findSubmitButton().vm.$emit('click'); findSubmitButton().vm.$emit('click');
await nextTick(); await nextTick();
expect(wrapper.emitted('submit-form')).toBeTruthy(); expect(wrapper.emitted('submit-form')).toBeTruthy();
expect(autosaveResetSpy).toHaveBeenCalled();
}); });
it('emits submitForm event on textarea ctrl+enter keydown', async () => { it('emits submitForm event on textarea ctrl+enter keydown', async () => {
const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
findTextarea().trigger('keydown.enter', { findTextarea().trigger('keydown.enter', {
ctrlKey: true, ctrlKey: true,
}); });
await nextTick(); await nextTick();
expect(wrapper.emitted('submit-form')).toBeTruthy(); expect(wrapper.emitted('submit-form')).toBeTruthy();
expect(autosaveResetSpy).toHaveBeenCalled();
}); });
it('emits submitForm event on textarea meta+enter keydown', async () => { it('emits submitForm event on textarea meta+enter keydown', async () => {
const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
findTextarea().trigger('keydown.enter', { findTextarea().trigger('keydown.enter', {
metaKey: true, metaKey: true,
}); });
await nextTick(); await nextTick();
expect(wrapper.emitted('submit-form')).toBeTruthy(); expect(wrapper.emitted('submit-form')).toBeTruthy();
expect(autosaveResetSpy).toHaveBeenCalled();
}); });
it('emits input event on changing textarea content', async () => { 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', () => { it('emits cancelForm event on modal Ok button click', () => {
const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
findTextarea().trigger('keyup.esc'); findTextarea().trigger('keyup.esc');
findModal().vm.$emit('ok'); findModal().vm.$emit('ok');
expect(wrapper.emitted('cancel-form')).toBeTruthy(); expect(wrapper.emitted('cancel-form')).toBeTruthy();
expect(autosaveResetSpy).toHaveBeenCalled();
}); });
}); });
}); });

View file

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

View file

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

View file

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe GitlabSchema.types['CiJobArtifact'] do RSpec.describe GitlabSchema.types['CiJobArtifact'] do
it 'has the correct fields' 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) expect(described_class).to have_graphql_fields(*expected_fields)
end end

View file

@ -133,6 +133,12 @@ RSpec.describe Sidebars::Projects::Menus::SettingsMenu do
end end
end end
describe 'Merge requests' do
let(:item_id) { :merge_requests }
it_behaves_like 'access rights checks'
end
describe 'Packages and registries' do describe 'Packages and registries' do
let(:item_id) { :packages_and_registries } let(:item_id) { :packages_and_registries }
let(:packages_enabled) { false } 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'), _('Webhooks'),
_('Access Tokens'), _('Access Tokens'),
_('Repository'), _('Repository'),
_('Merge requests'),
_('CI/CD'), _('CI/CD'),
_('Packages and registries'), _('Packages and registries'),
_('Monitor'), _('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
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 context 'forking' do
before do before do
assign(:project, project) 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