summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-01-13 12:08:04 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-01-13 12:08:04 +0000
commiteb30dd6e28f6fc9eb8021d205f6ed84511f001e2 (patch)
tree9557e4782c762f4d08f57c9e04991bf988695085
parentc3ad57034cc1cbd6d0ad02de7ac57f6004440c83 (diff)
downloadgitlab-ce-eb30dd6e28f6fc9eb8021d205f6ed84511f001e2.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue53
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue44
-rw-r--r--app/assets/javascripts/reports/components/report_section.vue20
-rw-r--r--app/controllers/projects_controller.rb1
-rw-r--r--app/finders/pipelines_finder.rb2
-rw-r--r--app/graphql/types/project_type.rb2
-rw-r--r--app/models/ci/bridge.rb4
-rw-r--r--app/models/ci/pipeline.rb25
-rw-r--r--app/models/ci/pipeline_enums.rb8
-rw-r--r--app/models/ci/sources/pipeline.rb2
-rw-r--r--app/serializers/pipeline_details_entity.rb2
-rw-r--r--app/serializers/pipeline_serializer.rb1
-rw-r--r--app/services/ci/create_pipeline_service.rb7
-rw-r--r--app/services/suggestions/apply_service.rb37
-rw-r--r--app/views/devise/sessions/two_factor.html.haml2
-rw-r--r--app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml6
-rw-r--r--app/views/projects/_merge_request_merge_suggestions_settings.html.haml17
-rw-r--r--app/views/projects/_merge_request_settings.html.haml2
-rw-r--r--app/views/projects/_merge_request_settings_description_text.html.haml2
-rw-r--r--changelogs/unreleased/4913-frontend-outdated-security-report.yml5
-rw-r--r--changelogs/unreleased/create-downstream-pipeline-in-same-project.yml5
-rw-r--r--changelogs/unreleased/feat-customizable-suggestion-commit-messages.yml5
-rw-r--r--db/migrate/20191208110214_add_suggestion_commit_message_to_projects.rb9
-rw-r--r--db/schema.rb1
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json14
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/api/projects.md14
-rw-r--r--doc/user/discussions/img/suggestion-commit-message-configuration.pngbin0 -> 62700 bytes
-rw-r--r--doc/user/discussions/index.md23
-rw-r--r--doc/user/project/service_desk.md27
-rw-r--r--doc/user/project/settings/index.md1
-rw-r--r--lib/api/entities.rb1
-rw-r--r--lib/api/helpers/projects_helpers.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb2
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content.rb4
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/bridge.rb25
-rw-r--r--lib/gitlab/ci/pipeline/chain/config/content/runtime.rb30
-rw-r--r--locale/gitlab.pot57
-rw-r--r--spec/finders/pipelines_finder_spec.rb13
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js143
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js113
-rw-r--r--spec/graphql/types/project_type_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/policy/refs_spec.rb28
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb53
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml2
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml1
-rw-r--r--spec/models/ci/pipeline_spec.rb110
-rw-r--r--spec/services/ci/create_pipeline_service/custom_config_content_spec.rb29
-rw-r--r--spec/services/suggestions/apply_service_spec.rb31
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb27
51 files changed, 952 insertions, 68 deletions
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue
new file mode 100644
index 00000000000..12b6070a79a
--- /dev/null
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_network_dropdown.vue
@@ -0,0 +1,53 @@
+<script>
+import { createNamespacedHelpers, mapState, mapGetters, mapActions } from 'vuex';
+
+import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
+
+const { mapState: mapDropdownState } = createNamespacedHelpers('networks');
+const { mapActions: mapSubnetworkActions } = createNamespacedHelpers('subnetworks');
+
+export default {
+ components: {
+ ClusterFormDropdown,
+ },
+ props: {
+ fieldName: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['selectedNetwork']),
+ ...mapDropdownState(['items', 'isLoadingItems', 'loadingItemsError']),
+ ...mapGetters(['hasZone', 'projectId', 'region']),
+ },
+ methods: {
+ ...mapActions(['setNetwork', 'setSubnetwork']),
+ ...mapSubnetworkActions({ fetchSubnetworks: 'fetchItems' }),
+ setNetworkAndFetchSubnetworks(network) {
+ const { projectId: project, region } = this;
+
+ this.setSubnetwork('');
+ this.setNetwork(network);
+ this.fetchSubnetworks({ project, region, network: network.selfLink });
+ },
+ },
+};
+</script>
+<template>
+ <cluster-form-dropdown
+ :field-name="fieldName"
+ :value="selectedNetwork"
+ :items="items"
+ :disabled="!hasZone"
+ :loading="isLoadingItems"
+ :has-errors="Boolean(loadingItemsError)"
+ :loading-text="s__('ClusterIntegration|Loading networks')"
+ :placeholder="s__('ClusterIntergation|Select a network')"
+ :search-field-placeholder="s__('ClusterIntegration|Search networks')"
+ :empty-text="s__('ClusterIntegration|No networks found')"
+ :error-message="s__('ClusterIntegration|Could not load networks')"
+ :disabled-text="s__('ClusterIntegration|Select a zone to choose a network')"
+ @input="setNetworkAndFetchSubnetworks"
+ />
+</template>
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue
new file mode 100644
index 00000000000..ec7889e2907
--- /dev/null
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue
@@ -0,0 +1,44 @@
+<script>
+import { createNamespacedHelpers, mapState, mapGetters, mapActions } from 'vuex';
+
+import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
+
+const { mapState: mapDropdownState } = createNamespacedHelpers('subnetworks');
+
+export default {
+ components: {
+ ClusterFormDropdown,
+ },
+ props: {
+ fieldName: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState(['selectedSubnetwork']),
+ ...mapDropdownState(['items', 'isLoadingItems', 'loadingItemsError']),
+ ...mapGetters(['hasNetwork']),
+ },
+ methods: {
+ ...mapActions(['setSubnetwork']),
+ },
+};
+</script>
+<template>
+ <cluster-form-dropdown
+ :field-name="fieldName"
+ :value="selectedSubnetwork"
+ :items="items"
+ :disabled="!hasNetwork"
+ :loading="isLoadingItems"
+ :has-errors="Boolean(loadingItemsError)"
+ :loading-text="s__('ClusterIntegration|Loading subnetworks')"
+ :placeholder="s__('ClusterIntergation|Select a subnetwork')"
+ :search-field-placeholder="s__('ClusterIntegration|Search subnetworks')"
+ :empty-text="s__('ClusterIntegration|No subnetworks found')"
+ :error-message="s__('ClusterIntegration|Could not load subnetworks')"
+ :disabled-text="s__('ClusterIntegration|Select a network to choose a subnetwork')"
+ @input="setSubnetwork"
+ />
+</template>
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/reports/components/report_section.vue
index 45c890769a0..20b0c52dbda 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/reports/components/report_section.vue
@@ -165,21 +165,23 @@ export default {
<template>
<section class="media-section">
<div class="media">
- <status-icon :status="statusIconName" :size="24" />
- <div class="media-body d-flex flex-align-self-center">
- <span class="js-code-text code-text">
- {{ headerText }}
- <slot :name="slotName"></slot>
-
- <popover v-if="hasPopover" :options="popoverOptions" class="prepend-left-5" />
- </span>
+ <status-icon :status="statusIconName" :size="24" class="align-self-center" />
+ <div class="media-body d-flex flex-align-self-center align-items-center">
+ <div class="js-code-text code-text">
+ <div>
+ {{ headerText }}
+ <slot :name="slotName"></slot>
+ <popover v-if="hasPopover" :options="popoverOptions" class="prepend-left-5" />
+ </div>
+ <slot name="subHeading"></slot>
+ </div>
<slot name="actionButtons"></slot>
<button
v-if="isCollapsible"
type="button"
- class="js-collapse-btn btn float-right btn-sm align-self-start qa-expand-report-button"
+ class="js-collapse-btn btn float-right btn-sm align-self-center qa-expand-report-button"
@click="toggleCollapsed"
>
{{ collapseText }}
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index dd392bd39a8..1d9f81aaa23 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -387,6 +387,7 @@ class ProjectsController < Projects::ApplicationController
:merge_method,
:initialize_with_readme,
:autoclose_referenced_issues,
+ :suggestion_commit_message,
project_feature_attributes: %i[
builds_access_level
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index 5a0d53d9683..48da44123f6 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -17,7 +17,7 @@ class PipelinesFinder
return Ci::Pipeline.none
end
- items = pipelines
+ items = pipelines.no_child
items = by_scope(items)
items = by_status(items)
items = by_ref(items)
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 0bd2b0c81d9..31cde7b6d48 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -104,6 +104,8 @@ module Types
description: 'Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project'
field :autoclose_referenced_issues, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if issues referenced by merge requests and commits within the default branch are closed automatically'
+ field :suggestion_commit_message, GraphQL::STRING_TYPE, null: true,
+ description: 'The commit message used to apply merge request suggestions'
field :namespace, Types::NamespaceType, null: true,
description: 'Namespace of the project'
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 804e01bfab0..e6d41dd2779 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -53,6 +53,10 @@ module Ci
def to_partial_path
'projects/generic_commit_statuses/generic_commit_status'
end
+
+ def yaml_for_downstream
+ nil
+ end
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 7a48fa8595b..5ad3a9b4431 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -60,7 +60,9 @@ module Ci
has_one :chat_data, class_name: 'Ci::PipelineChatData'
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline
+ has_many :child_pipelines, -> { merge(Ci::Sources::Pipeline.same_project) }, through: :sourced_pipelines, source: :pipeline
has_one :triggered_by_pipeline, through: :source_pipeline, source: :source_pipeline
+ has_one :parent_pipeline, -> { merge(Ci::Sources::Pipeline.same_project) }, through: :source_pipeline, source: :source_pipeline
has_one :source_job, through: :source_pipeline, source: :source_job
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
@@ -212,6 +214,7 @@ module Ci
end
scope :internal, -> { where(source: internal_sources) }
+ scope :no_child, -> { where.not(source: :parent_pipeline) }
scope :ci_sources, -> { where(config_source: ::Ci::PipelineEnums.ci_config_sources_values) }
scope :for_user, -> (user) { where(user: user) }
scope :for_sha, -> (sha) { where(sha: sha) }
@@ -507,10 +510,6 @@ module Ci
builds.skipped.after_stage(stage_idx).find_each(&:process)
end
- def child?
- false
- end
-
def latest?
return false unless git_ref && commit.present?
@@ -693,6 +692,24 @@ module Ci
all_merge_requests.order(id: :desc)
end
+ # If pipeline is a child of another pipeline, include the parent
+ # and the siblings, otherwise return only itself.
+ def same_family_pipeline_ids
+ if (parent = parent_pipeline)
+ [parent.id] + parent.child_pipelines.pluck(:id)
+ else
+ [self.id]
+ end
+ end
+
+ def child?
+ parent_pipeline.present?
+ end
+
+ def parent?
+ child_pipelines.exists?
+ end
+
def detailed_status(current_user)
Gitlab::Ci::Status::Pipeline::Factory
.new(self, current_user)
diff --git a/app/models/ci/pipeline_enums.rb b/app/models/ci/pipeline_enums.rb
index 3cd88807969..fde169d2f03 100644
--- a/app/models/ci/pipeline_enums.rb
+++ b/app/models/ci/pipeline_enums.rb
@@ -23,10 +23,13 @@ module Ci
schedule: 4,
api: 5,
external: 6,
+ # TODO: Rename `pipeline` to `cross_project_pipeline` in 13.0
+ # https://gitlab.com/gitlab-org/gitlab/issues/195991
pipeline: 7,
chat: 8,
merge_request_event: 10,
- external_pull_request_event: 11
+ external_pull_request_event: 11,
+ parent_pipeline: 12
}
end
@@ -38,7 +41,8 @@ module Ci
repository_source: 1,
auto_devops_source: 2,
remote_source: 4,
- external_project_source: 5
+ external_project_source: 5,
+ bridge_source: 6
}
end
diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
index feaec27281c..d71e3b55b9a 100644
--- a/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -18,6 +18,8 @@ module Ci
validates :source_project, presence: true
validates :source_job, presence: true
validates :source_pipeline, presence: true
+
+ scope :same_project, -> { where(arel_table[:source_project_id].eq(arel_table[:project_id])) }
end
end
end
diff --git a/app/serializers/pipeline_details_entity.rb b/app/serializers/pipeline_details_entity.rb
index 71589ac8315..a4ab1d399bc 100644
--- a/app/serializers/pipeline_details_entity.rb
+++ b/app/serializers/pipeline_details_entity.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class PipelineDetailsEntity < PipelineEntity
+ expose :project, using: ProjectEntity
+
expose :flags do
expose :latest?, as: :latest
end
diff --git a/app/serializers/pipeline_serializer.rb b/app/serializers/pipeline_serializer.rb
index b25a1ea9209..be535a5d414 100644
--- a/app/serializers/pipeline_serializer.rb
+++ b/app/serializers/pipeline_serializer.rb
@@ -41,6 +41,7 @@ class PipelineSerializer < BaseSerializer
def preloaded_relations
[
:latest_statuses_ordered_by_stage,
+ :project,
:stages,
{
failed_builds: %i(project metadata)
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index ce3a9eb0772..2daf3a51235 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -23,7 +23,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
# rubocop: disable Metrics/ParameterLists
- def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, **options, &block)
+ def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, merge_request: nil, external_pull_request: nil, bridge: nil, **options, &block)
@pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new(
@@ -46,6 +46,7 @@ module Ci
current_user: current_user,
push_options: params[:push_options] || {},
chat_data: params[:chat_data],
+ bridge: bridge,
**extra_options(options))
sequence = Gitlab::Ci::Pipeline::Chain::Sequence
@@ -104,14 +105,14 @@ module Ci
if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true)
project.ci_pipelines
.where(ref: pipeline.ref)
- .where.not(id: pipeline.id)
+ .where.not(id: pipeline.same_family_pipeline_ids)
.where.not(sha: project.commit(pipeline.ref).try(:id))
.alive_or_scheduled
.with_only_interruptible_builds
else
project.ci_pipelines
.where(ref: pipeline.ref)
- .where.not(id: pipeline.id)
+ .where.not(id: pipeline.same_family_pipeline_ids)
.where.not(sha: project.commit(pipeline.ref).try(:id))
.created_or_pending
end
diff --git a/app/services/suggestions/apply_service.rb b/app/services/suggestions/apply_service.rb
index 8ba50e22b09..a6485e42bdb 100644
--- a/app/services/suggestions/apply_service.rb
+++ b/app/services/suggestions/apply_service.rb
@@ -2,6 +2,24 @@
module Suggestions
class ApplyService < ::BaseService
+ DEFAULT_SUGGESTION_COMMIT_MESSAGE = 'Apply suggestion to %{file_path}'
+
+ PLACEHOLDERS = {
+ 'project_path' => ->(suggestion, user) { suggestion.project.path },
+ 'project_name' => ->(suggestion, user) { suggestion.project.name },
+ 'file_path' => ->(suggestion, user) { suggestion.file_path },
+ 'branch_name' => ->(suggestion, user) { suggestion.branch },
+ 'username' => ->(suggestion, user) { user.username },
+ 'user_full_name' => ->(suggestion, user) { user.name }
+ }.freeze
+
+ # This regex is built dynamically using the keys from the PLACEHOLDER struct.
+ # So, we can easily add new placeholder just by modifying the PLACEHOLDER hash.
+ # This regex will build the new PLACEHOLDER_REGEX with the new information
+ PLACEHOLDERS_REGEX = Regexp.union(PLACEHOLDERS.keys.map { |key| Regexp.new(Regexp.escape(key)) }).freeze
+
+ attr_reader :current_user
+
def initialize(current_user)
@current_user = current_user
end
@@ -22,7 +40,7 @@ module Suggestions
end
params = file_update_params(suggestion, diff_file)
- result = ::Files::UpdateService.new(suggestion.project, @current_user, params).execute
+ result = ::Files::UpdateService.new(suggestion.project, current_user, params).execute
if result[:status] == :success
suggestion.update(commit_id: result[:result], applied: true)
@@ -46,13 +64,14 @@ module Suggestions
def file_update_params(suggestion, diff_file)
blob = diff_file.new_blob
+ project = suggestion.project
file_path = suggestion.file_path
branch_name = suggestion.branch
file_content = new_file_content(suggestion, blob)
- commit_message = "Apply suggestion to #{file_path}"
+ commit_message = processed_suggestion_commit_message(suggestion)
file_last_commit =
- Gitlab::Git::Commit.last_for_path(suggestion.project.repository,
+ Gitlab::Git::Commit.last_for_path(project.repository,
blob.commit_id,
blob.path)
@@ -75,5 +94,17 @@ module Suggestions
content.join
end
+
+ def suggestion_commit_message(project)
+ project.suggestion_commit_message || DEFAULT_SUGGESTION_COMMIT_MESSAGE
+ end
+
+ def processed_suggestion_commit_message(suggestion)
+ message = suggestion_commit_message(suggestion.project)
+
+ Gitlab::StringPlaceholderReplacer.replace_string_placeholders(message, PLACEHOLDERS_REGEX) do |key|
+ PLACEHOLDERS[key].call(suggestion, current_user)
+ end
+ end
end
end
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index f49cdfbf8da..08675c16124 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -8,7 +8,7 @@
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%div
= f.label 'Two-Factor Authentication code', name: :otp_attempt
- = f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.'
+ = f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.', inputmode: 'numeric', pattern: '[0-9]*'
%p.form-text.text-muted.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
.prepend-top-20
= f.submit "Verify code", class: "btn btn-success"
diff --git a/app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml b/app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml
index 5d163d03c73..e3142ff96a1 100644
--- a/app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml
+++ b/app/views/devise/shared/_experimental_separate_sign_up_flow_box.html.haml
@@ -5,7 +5,8 @@
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
.devise-errors.mt-0
= render "devise/shared/error_messages", resource: resource
- = invisible_captcha
+ - if Feature.enabled?(:invisible_captcha)
+ = invisible_captcha
.username.form-group
= f.label :username, class: 'label-bold'
= f.text_field :username, class: "form-control middle js-block-emoji js-validate-length js-validate-username", :data => { :max_length => max_username_length, :max_length_message => s_("SignUp|Username is too long (maximum is %{max_length} characters).") % { max_length: max_username_length }, :qa_selector => 'new_user_username_field' }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.")
@@ -27,5 +28,8 @@
- accept_terms_label = _("I accept the %{terms_link}") % { terms_link: terms_link }
= accept_terms_label.html_safe
= render_if_exists 'devise/shared/email_opted_in', f: f
+ %div
+ - if show_recaptcha_sign_up?
+ = recaptcha_tags
.submit-container.mt-3
= f.submit _("Register"), class: "btn-register btn btn-block btn-success mb-0 p-2", data: { qa_selector: 'new_user_register_button' }
diff --git a/app/views/projects/_merge_request_merge_suggestions_settings.html.haml b/app/views/projects/_merge_request_merge_suggestions_settings.html.haml
new file mode 100644
index 00000000000..06bb9056e61
--- /dev/null
+++ b/app/views/projects/_merge_request_merge_suggestions_settings.html.haml
@@ -0,0 +1,17 @@
+- form = local_assigns.fetch(:form)
+
+.form-group
+ %b= s_('ProjectSettings|Merge suggestions')
+ %p.text-secondary
+ = s_('ProjectSettings|The commit message used to apply merge request suggestions')
+ = link_to icon('question-circle'),
+ help_page_path('user/discussions/index.md',
+ anchor: 'configure-the-commit-message-for-applied-suggestions'),
+ target: '_blank'
+ .mb-2
+ = form.text_field :suggestion_commit_message, class: 'form-control mb-2', placeholder: Suggestions::ApplyService::DEFAULT_SUGGESTION_COMMIT_MESSAGE
+ %p.form-text.text-muted
+ = s_('ProjectSettings|The variables GitLab supports:')
+ - Suggestions::ApplyService::PLACEHOLDERS.keys.each do |placeholder|
+ %code
+ = "%{#{placeholder}}".html_safe
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index f2ba38387a3..dc3a3fcc647 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -5,3 +5,5 @@
= render 'projects/merge_request_merge_options_settings', project: @project, form: form
= render 'projects/merge_request_merge_checks_settings', project: @project, form: form
+
+= render 'projects/merge_request_merge_suggestions_settings', project: @project, form: form
diff --git a/app/views/projects/_merge_request_settings_description_text.html.haml b/app/views/projects/_merge_request_settings_description_text.html.haml
index 42964c900b3..dc9dc92675d 100644
--- a/app/views/projects/_merge_request_settings_description_text.html.haml
+++ b/app/views/projects/_merge_request_settings_description_text.html.haml
@@ -1 +1 @@
-%p= s_('ProjectSettings|Choose your merge method, merge options, and merge checks.')
+%p= s_('ProjectSettings|Choose your merge method, merge options, merge checks, and merge suggestions.')
diff --git a/changelogs/unreleased/4913-frontend-outdated-security-report.yml b/changelogs/unreleased/4913-frontend-outdated-security-report.yml
new file mode 100644
index 00000000000..b1144f621f3
--- /dev/null
+++ b/changelogs/unreleased/4913-frontend-outdated-security-report.yml
@@ -0,0 +1,5 @@
+---
+title: Display in MR if security report is outdated
+merge_request: 20954
+author:
+type: other
diff --git a/changelogs/unreleased/create-downstream-pipeline-in-same-project.yml b/changelogs/unreleased/create-downstream-pipeline-in-same-project.yml
new file mode 100644
index 00000000000..ccba831abfe
--- /dev/null
+++ b/changelogs/unreleased/create-downstream-pipeline-in-same-project.yml
@@ -0,0 +1,5 @@
+---
+title: Allow an upstream pipeline to create a downstream pipeline in the same project
+merge_request: 22663
+author:
+type: added
diff --git a/changelogs/unreleased/feat-customizable-suggestion-commit-messages.yml b/changelogs/unreleased/feat-customizable-suggestion-commit-messages.yml
new file mode 100644
index 00000000000..9fbff4118c8
--- /dev/null
+++ b/changelogs/unreleased/feat-customizable-suggestion-commit-messages.yml
@@ -0,0 +1,5 @@
+---
+title: Implement customizable commit messages for applied suggested changes
+merge_request: 21411
+author: Fabio Huser
+type: added
diff --git a/db/migrate/20191208110214_add_suggestion_commit_message_to_projects.rb b/db/migrate/20191208110214_add_suggestion_commit_message_to_projects.rb
new file mode 100644
index 00000000000..c4344cf212c
--- /dev/null
+++ b/db/migrate/20191208110214_add_suggestion_commit_message_to_projects.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddSuggestionCommitMessageToProjects < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ add_column :projects, :suggestion_commit_message, :string, limit: 255
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index d981592fe3c..ea2df01e1e2 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -3348,6 +3348,7 @@ ActiveRecord::Schema.define(version: 2020_01_08_233040) do
t.date "marked_for_deletion_at"
t.integer "marked_for_deletion_by_user_id"
t.boolean "autoclose_referenced_issues"
+ t.string "suggestion_commit_message", limit: 255
t.index "lower((name)::text)", name: "index_projects_on_lower_name"
t.index ["created_at", "id"], name: "index_projects_on_created_at_and_id"
t.index ["creator_id"], name: "index_projects_on_creator_id"
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 345172658d2..4c037a00857 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -5155,6 +5155,11 @@ type Project {
statistics: ProjectStatistics
"""
+ The commit message used to apply merge request suggestions
+ """
+ suggestionCommitMessage: String
+
+ """
List of project tags
"""
tagList: String
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 2668ea98fc0..eb6e3f8ef16 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -1526,6 +1526,20 @@
"deprecationReason": null
},
{
+ "name": "suggestionCommitMessage",
+ "description": "The commit message used to apply merge request suggestions",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "tagList",
"description": "List of project tags",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9e053c9cdcb..2b8719f725d 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -769,6 +769,7 @@ Information about pagination in a connection.
| `printingMergeRequestLinkEnabled` | Boolean | Indicates if a link to create or view a merge request should display after a push to Git repositories of the project from the command line |
| `removeSourceBranchAfterMerge` | Boolean | Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project |
| `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically |
+| `suggestionCommitMessage` | String | The commit message used to apply merge request suggestions |
| `namespace` | Namespace | Namespace of the project |
| `group` | Group | Group of the project |
| `statistics` | ProjectStatistics | Statistics of the project |
diff --git a/doc/api/projects.md b/doc/api/projects.md
index a856c01eb00..8cfba68acee 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -157,6 +157,7 @@ When the user is authenticated and `simple` is not set this returns something li
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
+ "suggestion_commit_message": null,
"statistics": {
"commit_count": 37,
"storage_size": 1038090,
@@ -256,6 +257,7 @@ When the user is authenticated and `simple` is not set this returns something li
"service_desk_enabled": false,
"service_desk_address": null,
"autoclose_referenced_issues": true,
+ "suggestion_commit_message": null,
"statistics": {
"commit_count": 12,
"storage_size": 2066080,
@@ -388,6 +390,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
+ "suggestion_commit_message": null,
"statistics": {
"commit_count": 37,
"storage_size": 1038090,
@@ -487,6 +490,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo
"service_desk_enabled": false,
"service_desk_address": null,
"autoclose_referenced_issues": true,
+ "suggestion_commit_message": null,
"statistics": {
"commit_count": 12,
"storage_size": 2066080,
@@ -598,6 +602,7 @@ Example response:
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
+ "suggestion_commit_message": null,
"statistics": {
"commit_count": 37,
"storage_size": 1038090,
@@ -694,6 +699,7 @@ Example response:
"service_desk_enabled": false,
"service_desk_address": null,
"autoclose_referenced_issues": true,
+ "suggestion_commit_message": null,
"statistics": {
"commit_count": 12,
"storage_size": 2066080,
@@ -844,6 +850,7 @@ GET /projects/:id
"service_desk_enabled": false,
"service_desk_address": null,
"autoclose_referenced_issues": true,
+ "suggestion_commit_message": null,
"statistics": {
"commit_count": 37,
"storage_size": 1038090,
@@ -1068,6 +1075,7 @@ POST /projects/user/:user_id
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `merge_method` | string | no | Set the [merge method](#project-merge-method) used |
| `autoclose_referenced_issues` | boolean | no | Set whether auto-closing referenced issues on default branch |
+| `suggestion_commit_message` | string | no | The commit message used to apply merge request suggestions |
| `remove_source_branch_after_merge` | boolean | no | Enable `Delete source branch` option by default for all new merge requests |
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
@@ -1133,6 +1141,7 @@ PUT /projects/:id
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `merge_method` | string | no | Set the [merge method](#project-merge-method) used |
| `autoclose_referenced_issues` | boolean | no | Set whether auto-closing referenced issues on default branch |
+| `suggestion_commit_message` | string | no | The commit message used to apply merge request suggestions |
| `remove_source_branch_after_merge` | boolean | no | Enable `Delete source branch` option by default for all new merge requests |
| `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access |
@@ -1265,6 +1274,7 @@ Example responses:
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
+ "suggestion_commit_message": null,
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@@ -1354,6 +1364,7 @@ Example response:
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
+ "suggestion_commit_message": null,
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@@ -1442,6 +1453,7 @@ Example response:
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
+ "suggestion_commit_message": null,
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@@ -1617,6 +1629,7 @@ Example response:
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
+ "suggestion_commit_message": null,
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
@@ -1724,6 +1737,7 @@ Example response:
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
+ "suggestion_commit_message": null,
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
diff --git a/doc/user/discussions/img/suggestion-commit-message-configuration.png b/doc/user/discussions/img/suggestion-commit-message-configuration.png
new file mode 100644
index 00000000000..962bc9b0aed
--- /dev/null
+++ b/doc/user/discussions/img/suggestion-commit-message-configuration.png
Binary files differ
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 56b4e940665..0a2a9c6d2fa 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -414,9 +414,8 @@ the Merge Request authored by the user that applied them.
Once the author applies a suggestion, it will be marked with the **Applied** label,
the thread will be automatically resolved, and GitLab will create a new commit
-with the message `Apply suggestion to <file-name>` and push the suggested change
-directly into the codebase in the merge request's branch.
-[Developer permission](../permissions.md) is required to do so.
+and push the suggested change directly into the codebase in the merge request's
+branch. [Developer permission](../permissions.md) is required to do so.
> **Note:**
Custom commit messages will be introduced by
@@ -444,6 +443,24 @@ Suggestions covering multiple lines are limited to 100 lines _above_ and 100
lines _below_ the commented diff line, allowing up to 200 changed lines per
suggestion.
+### Configure the commit message for applied suggestions
+
+GitLab will use `Apply suggestion to %{file_path}` by default as commit messages
+when applying change suggestions. This commit message can be customized to
+follow any guidelines you might have. To do so, open the **Merge requests** tab
+within your project settings and change the **Merge suggestions** text.
+
+![Suggestion Commit Message Configuration](img/suggestion-commit-message-configuration.png)
+
+You can also use following variables besides static text:
+
+- `%{project_path}`: The full URL safe project path. E.g: *my-group/my-project*
+- `%{project_name}`: The human readable name of the project. E.g: *My Project*
+- `%{file_path}`: The full path of the file the suggestion is applied to. E.g: *docs/index.md*
+- `%{branch_name}`: The name of the branch the suggestion is applied on. E.g: *my-feature-branch*
+- `%{username}`: The username of the user applying the suggestion. E.g: *user_1*
+- `%{user_full_name}`: The full name of the user applying the suggestion. E.g: *User 1*
+
## Start a thread by replying to a standard comment
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/30299) in GitLab 11.9
diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md
index 59c67cdda2f..e9fce474040 100644
--- a/doc/user/project/service_desk.md
+++ b/doc/user/project/service_desk.md
@@ -81,6 +81,33 @@ navigation's **Issues** menu.
![Service Desk Navigation Item](img/service_desk_nav_item.png)
+### Using customized email templates
+
+ > [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/2460) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.7.
+
+When a user submits a new issue using Service Desk, or when a new note is created on a Service Desk issue, an email is sent to the author.
+
+The body of these email messages can customized by using templates. To create a new customized template,
+create a new Markdown (`.md`) file inside the `.gitlab/service_desk_templates/`
+directory in your repository. Commit and push to your default branch.
+
+#### Thank you email
+
+The **Thank you email** is the email sent to a user after they submit an issue.
+The file name of the template has to be `thank_you.md`.
+You can use `%{ISSUE_ID}` placeholder which will be replaced by an issue iid in the email and
+`%{ISSUE_PATH}` placeholder which will be replaced by project path and the issue iid.
+As the service desk issues are created as confidential (only project members can see them)
+the response email doesn't provide the issue link.
+
+#### New note email
+
+The **New note email** is the email sent to a user when the issue they submitted has a new comment.
+The file name of the template has to be `new_note.md`.
+You can use `%{ISSUE_ID}` placeholder which will be replaced by an issue iid
+in the email, `%{ISSUE_PATH}` placeholder which will be replaced by
+ project path and the issue iid and `%{NOTE_TEXT}` placeholder which will be replaced by the note text.
+
## Using Service Desk
### As an end user (issue creator)
diff --git a/doc/user/project/settings/index.md b/doc/user/project/settings/index.md
index f68ca90e235..8fa005cc39c 100644
--- a/doc/user/project/settings/index.md
+++ b/doc/user/project/settings/index.md
@@ -91,6 +91,7 @@ Set up your project's merge request settings:
- Enable [merge only if pipeline succeeds](../merge_requests/merge_when_pipeline_succeeds.md).
- Enable [merge only when all threads are resolved](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-threads-are-resolved).
- Enable [`delete source branch after merge` option by default](../merge_requests/getting_started.md#deleting-the-source-branch)
+- Configure [suggested changes commit messages](../../discussions/index.md#configure-the-commit-message-for-applied-suggestions)
![project's merge request settings](img/merge_requests_settings.png)
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 9433edb20b0..3cb438baa80 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -335,6 +335,7 @@ module API
expose :remove_source_branch_after_merge
expose :printing_merge_request_link_enabled
expose :merge_method
+ expose :suggestion_commit_message
expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) {
options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project)
}
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index e4f943bd925..6333e00daf5 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -46,6 +46,7 @@ module API
optional :avatar, type: File, desc: 'Avatar image for project' # rubocop:disable Scalability/FileUploads
optional :printing_merge_request_link_enabled, type: Boolean, desc: 'Show link to create/view merge request when pushing from the command line'
optional :merge_method, type: String, values: %w(ff rebase_merge merge), desc: 'The merge method used when merging merge requests'
+ optional :suggestion_commit_message, type: String, desc: 'The commit message used to apply merge request suggestions'
optional :initialize_with_readme, type: Boolean, desc: "Initialize a project with a README.md"
optional :ci_default_git_depth, type: Integer, desc: 'Default number of revisions for shallow cloning'
optional :auto_devops_enabled, type: Boolean, desc: 'Flag indication if Auto DevOps is enabled'
@@ -119,6 +120,7 @@ module API
:visibility,
:wiki_access_level,
:avatar,
+ :suggestion_commit_message,
# TODO: remove in API v5, replaced by *_access_level
:issues_enabled,
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index c2df419cca0..0f355906948 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -10,7 +10,7 @@ module Gitlab
:trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options,
- :chat_data, :allow_mirror_update,
+ :chat_data, :allow_mirror_update, :bridge,
# These attributes are set by Chains during processing:
:config_content, :config_processor, :stage_seeds
) do
diff --git a/lib/gitlab/ci/pipeline/chain/config/content.rb b/lib/gitlab/ci/pipeline/chain/config/content.rb
index f9fffbcb517..66bead3a416 100644
--- a/lib/gitlab/ci/pipeline/chain/config/content.rb
+++ b/lib/gitlab/ci/pipeline/chain/config/content.rb
@@ -9,7 +9,7 @@ module Gitlab
include Chain::Helpers
SOURCES = [
- Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
+ Gitlab::Ci::Pipeline::Chain::Config::Content::Bridge,
Gitlab::Ci::Pipeline::Chain::Config::Content::Repository,
Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject,
Gitlab::Ci::Pipeline::Chain::Config::Content::Remote,
@@ -17,7 +17,7 @@ module Gitlab
].freeze
LEGACY_SOURCES = [
- Gitlab::Ci::Pipeline::Chain::Config::Content::Runtime,
+ Gitlab::Ci::Pipeline::Chain::Config::Content::Bridge,
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyRepository,
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops
].freeze
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/bridge.rb b/lib/gitlab/ci/pipeline/chain/config/content/bridge.rb
new file mode 100644
index 00000000000..39ffa2d4e25
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/chain/config/content/bridge.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Pipeline
+ module Chain
+ module Config
+ class Content
+ class Bridge < Source
+ def content
+ return unless @command.bridge
+
+ @command.bridge.yaml_for_downstream
+ end
+
+ def source
+ :bridge_source
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/chain/config/content/runtime.rb b/lib/gitlab/ci/pipeline/chain/config/content/runtime.rb
deleted file mode 100644
index 4811d3d913d..00000000000
--- a/lib/gitlab/ci/pipeline/chain/config/content/runtime.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Ci
- module Pipeline
- module Chain
- module Config
- class Content
- class Runtime < Source
- def content
- @command.config_content
- end
-
- def source
- # The only case when this source is used is when the config content
- # is passed in as parameter to Ci::CreatePipelineService.
- # This would only occur with parent/child pipelines which is being
- # implemented.
- # TODO: change source to return :runtime_source
- # https://gitlab.com/gitlab-org/gitlab/merge_requests/21041
-
- nil
- end
- end
- end
- end
- end
- end
- end
-end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 638ecd7e38e..b8fa6e845cb 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -3854,6 +3854,9 @@ msgstr ""
msgid "ClusterIntegration|Could not load instance types"
msgstr ""
+msgid "ClusterIntegration|Could not load networks"
+msgstr ""
+
msgid "ClusterIntegration|Could not load regions from your AWS account"
msgstr ""
@@ -3863,6 +3866,9 @@ msgstr ""
msgid "ClusterIntegration|Could not load subnets for the selected VPC"
msgstr ""
+msgid "ClusterIntegration|Could not load subnetworks"
+msgstr ""
+
msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr ""
@@ -4127,12 +4133,18 @@ msgstr ""
msgid "ClusterIntegration|Loading instance types"
msgstr ""
+msgid "ClusterIntegration|Loading networks"
+msgstr ""
+
msgid "ClusterIntegration|Loading security groups"
msgstr ""
msgid "ClusterIntegration|Loading subnets"
msgstr ""
+msgid "ClusterIntegration|Loading subnetworks"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr ""
@@ -4157,6 +4169,9 @@ msgstr ""
msgid "ClusterIntegration|No machine types matched your search"
msgstr ""
+msgid "ClusterIntegration|No networks found"
+msgstr ""
+
msgid "ClusterIntegration|No projects found"
msgstr ""
@@ -4172,6 +4187,9 @@ msgstr ""
msgid "ClusterIntegration|No subnet found"
msgstr ""
+msgid "ClusterIntegration|No subnetworks found"
+msgstr ""
+
msgid "ClusterIntegration|No zones matched your search"
msgstr ""
@@ -4268,6 +4286,9 @@ msgstr ""
msgid "ClusterIntegration|Search machine types"
msgstr ""
+msgid "ClusterIntegration|Search networks"
+msgstr ""
+
msgid "ClusterIntegration|Search projects"
msgstr ""
@@ -4280,6 +4301,9 @@ msgstr ""
msgid "ClusterIntegration|Search subnets"
msgstr ""
+msgid "ClusterIntegration|Search subnetworks"
+msgstr ""
+
msgid "ClusterIntegration|Search zones"
msgstr ""
@@ -4298,6 +4322,9 @@ msgstr ""
msgid "ClusterIntegration|Select a VPC to use for your EKS Cluster resources. To use a new VPC, first create one on %{startLink}Amazon Web Services %{externalLinkIcon} %{endLink}."
msgstr ""
+msgid "ClusterIntegration|Select a network to choose a subnetwork"
+msgstr ""
+
msgid "ClusterIntegration|Select a region to choose a Key Pair"
msgstr ""
@@ -4307,6 +4334,9 @@ msgstr ""
msgid "ClusterIntegration|Select a stack to install Crossplane."
msgstr ""
+msgid "ClusterIntegration|Select a zone to choose a network"
+msgstr ""
+
msgid "ClusterIntegration|Select machine type"
msgstr ""
@@ -4484,6 +4514,9 @@ msgstr ""
msgid "ClusterIntergation|Select a VPC"
msgstr ""
+msgid "ClusterIntergation|Select a network"
+msgstr ""
+
msgid "ClusterIntergation|Select a region"
msgstr ""
@@ -4493,6 +4526,9 @@ msgstr ""
msgid "ClusterIntergation|Select a subnet"
msgstr ""
+msgid "ClusterIntergation|Select a subnetwork"
+msgstr ""
+
msgid "ClusterIntergation|Select an instance type"
msgstr ""
@@ -8330,7 +8366,7 @@ msgstr ""
msgid "GeoNodes|Pausing replication stops the sync process. Are you sure?"
msgstr ""
-msgid "GeoNodes|Removing a Geo primary node stops the synchronization to that node. Are you sure?"
+msgid "GeoNodes|Removing a Geo primary node stops the synchronization to all nodes. Are you sure?"
msgstr ""
msgid "GeoNodes|Removing a Geo secondary node stops the synchronization to that node. Are you sure?"
@@ -14130,10 +14166,10 @@ msgstr ""
msgid "ProjectSettings|Build, test, and deploy your changes"
msgstr ""
-msgid "ProjectSettings|Choose your merge method, merge options, and merge checks."
+msgid "ProjectSettings|Choose your merge method, merge options, merge checks, and merge suggestions."
msgstr ""
-msgid "ProjectSettings|Choose your merge method, merge options, merge checks, and set up a default description template for merge requests."
+msgid "ProjectSettings|Choose your merge method, merge options, merge checks, merge suggestions, and set up a default description template for merge requests."
msgstr ""
msgid "ProjectSettings|Contact an admin to change this setting."
@@ -14217,6 +14253,9 @@ msgstr ""
msgid "ProjectSettings|Merge requests"
msgstr ""
+msgid "ProjectSettings|Merge suggestions"
+msgstr ""
+
msgid "ProjectSettings|No merge commits are created"
msgstr ""
@@ -14268,6 +14307,12 @@ msgstr ""
msgid "ProjectSettings|Submit changes to be merged upstream"
msgstr ""
+msgid "ProjectSettings|The commit message used to apply merge request suggestions"
+msgstr ""
+
+msgid "ProjectSettings|The variables GitLab supports:"
+msgstr ""
+
msgid "ProjectSettings|These checks must pass before merge requests can be merged"
msgstr ""
@@ -16100,6 +16145,12 @@ msgstr ""
msgid "Security dashboard"
msgstr ""
+msgid "Security report is out of date. Please incorporate latest changes from %{targetBranchName}"
+msgstr ""
+
+msgid "Security report is out of date. Retry the pipeline for the target branch."
+msgstr ""
+
msgid "SecurityConfiguration|Configured"
msgstr ""
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
index c8a4ea799c3..1dbf9491118 100644
--- a/spec/finders/pipelines_finder_spec.rb
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -64,6 +64,19 @@ describe PipelinesFinder do
end
end
+ context 'when project has child pipelines' do
+ let!(:parent_pipeline) { create(:ci_pipeline, project: project) }
+ let!(:child_pipeline) { create(:ci_pipeline, project: project, source: :parent_pipeline) }
+
+ let!(:pipeline_source) do
+ create(:ci_sources_pipeline, pipeline: child_pipeline, source_pipeline: parent_pipeline)
+ end
+
+ it 'filters out child pipelines and show only the parents' do
+ is_expected.to eq([parent_pipeline])
+ end
+ end
+
HasStatus::AVAILABLE_STATUSES.each do |target|
context "when status is #{target}" do
let(:params) { { status: target } }
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js
new file mode 100644
index 00000000000..1df583af711
--- /dev/null
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_network_dropdown_spec.js
@@ -0,0 +1,143 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import GkeNetworkDropdown from '~/create_cluster/gke_cluster/components/gke_network_dropdown.vue';
+import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
+import createClusterDropdownState from '~/create_cluster/store/cluster_dropdown/state';
+
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
+describe('GkeNetworkDropdown', () => {
+ let wrapper;
+ let store;
+ const defaultProps = { fieldName: 'field-name' };
+ const selectedNetwork = { selfLink: '123456' };
+ const projectId = '6789';
+ const region = 'east-1';
+ const setNetwork = jest.fn();
+ const setSubnetwork = jest.fn();
+ const fetchSubnetworks = jest.fn();
+
+ const buildStore = ({ clusterDropdownState } = {}) =>
+ new Vuex.Store({
+ state: {
+ selectedNetwork,
+ },
+ actions: {
+ setNetwork,
+ setSubnetwork,
+ },
+ getters: {
+ hasZone: () => false,
+ region: () => region,
+ projectId: () => projectId,
+ },
+ modules: {
+ networks: {
+ namespaced: true,
+ state: {
+ ...createClusterDropdownState(),
+ ...(clusterDropdownState || {}),
+ },
+ },
+ subnetworks: {
+ namespaced: true,
+ actions: {
+ fetchItems: fetchSubnetworks,
+ },
+ },
+ },
+ });
+
+ const buildWrapper = (propsData = defaultProps) =>
+ shallowMount(GkeNetworkDropdown, {
+ propsData,
+ store,
+ localVue,
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('sets correct field-name', () => {
+ const fieldName = 'field-name';
+
+ store = buildStore();
+ wrapper = buildWrapper({ fieldName });
+
+ expect(wrapper.find(ClusterFormDropdown).props('fieldName')).toBe(fieldName);
+ });
+
+ it('sets selected network as the dropdown value', () => {
+ store = buildStore();
+ wrapper = buildWrapper();
+
+ expect(wrapper.find(ClusterFormDropdown).props('value')).toBe(selectedNetwork);
+ });
+
+ it('maps networks store items to the dropdown items property', () => {
+ const items = [{ name: 'network' }];
+
+ store = buildStore({ clusterDropdownState: { items } });
+ wrapper = buildWrapper();
+
+ expect(wrapper.find(ClusterFormDropdown).props('items')).toBe(items);
+ });
+
+ describe('when network dropdown store is loading items', () => {
+ it('sets network dropdown as loading', () => {
+ store = buildStore({ clusterDropdownState: { isLoadingItems: true } });
+ wrapper = buildWrapper();
+
+ expect(wrapper.find(ClusterFormDropdown).props('loading')).toBe(true);
+ });
+ });
+
+ describe('when there is no selected zone', () => {
+ it('disables the network dropdown', () => {
+ store = buildStore();
+ wrapper = buildWrapper();
+
+ expect(wrapper.find(ClusterFormDropdown).props('disabled')).toBe(true);
+ });
+ });
+
+ describe('when an error occurs while loading networks', () => {
+ it('sets the network dropdown as having errors', () => {
+ store = buildStore({ clusterDropdownState: { loadingItemsError: new Error() } });
+ wrapper = buildWrapper();
+
+ expect(wrapper.find(ClusterFormDropdown).props('hasErrors')).toBe(true);
+ });
+ });
+
+ describe('when dropdown emits input event', () => {
+ beforeEach(() => {
+ store = buildStore();
+ wrapper = buildWrapper();
+ wrapper.find(ClusterFormDropdown).vm.$emit('input', selectedNetwork);
+ });
+
+ it('cleans selected subnetwork', () => {
+ expect(setSubnetwork).toHaveBeenCalledWith(expect.anything(), '', undefined);
+ });
+
+ it('dispatches the setNetwork action', () => {
+ expect(setNetwork).toHaveBeenCalledWith(expect.anything(), selectedNetwork, undefined);
+ });
+
+ it('fetches subnetworks for the selected project, region, and network', () => {
+ expect(fetchSubnetworks).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ project: projectId,
+ region,
+ network: selectedNetwork.selfLink,
+ },
+ undefined,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js
new file mode 100644
index 00000000000..a1dc3960fe9
--- /dev/null
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_subnetwork_dropdown_spec.js
@@ -0,0 +1,113 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import GkeSubnetworkDropdown from '~/create_cluster/gke_cluster/components/gke_subnetwork_dropdown.vue';
+import ClusterFormDropdown from '~/create_cluster/components/cluster_form_dropdown.vue';
+import createClusterDropdownState from '~/create_cluster/store/cluster_dropdown/state';
+
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
+describe('GkeSubnetworkDropdown', () => {
+ let wrapper;
+ let store;
+ const defaultProps = { fieldName: 'field-name' };
+ const selectedSubnetwork = '123456';
+ const setSubnetwork = jest.fn();
+
+ const buildStore = ({ clusterDropdownState } = {}) =>
+ new Vuex.Store({
+ state: {
+ selectedSubnetwork,
+ },
+ actions: {
+ setSubnetwork,
+ },
+ getters: {
+ hasNetwork: () => false,
+ },
+ modules: {
+ subnetworks: {
+ namespaced: true,
+ state: {
+ ...createClusterDropdownState(),
+ ...(clusterDropdownState || {}),
+ },
+ },
+ },
+ });
+
+ const buildWrapper = (propsData = defaultProps) =>
+ shallowMount(GkeSubnetworkDropdown, {
+ propsData,
+ store,
+ localVue,
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('sets correct field-name', () => {
+ const fieldName = 'field-name';
+
+ store = buildStore();
+ wrapper = buildWrapper({ fieldName });
+
+ expect(wrapper.find(ClusterFormDropdown).props('fieldName')).toBe(fieldName);
+ });
+
+ it('sets selected subnetwork as the dropdown value', () => {
+ store = buildStore();
+ wrapper = buildWrapper();
+
+ expect(wrapper.find(ClusterFormDropdown).props('value')).toBe(selectedSubnetwork);
+ });
+
+ it('maps subnetworks store items to the dropdown items property', () => {
+ const items = [{ name: 'subnetwork' }];
+
+ store = buildStore({ clusterDropdownState: { items } });
+ wrapper = buildWrapper();
+
+ expect(wrapper.find(ClusterFormDropdown).props('items')).toBe(items);
+ });
+
+ describe('when subnetwork dropdown store is loading items', () => {
+ it('sets subnetwork dropdown as loading', () => {
+ store = buildStore({ clusterDropdownState: { isLoadingItems: true } });
+ wrapper = buildWrapper();
+
+ expect(wrapper.find(ClusterFormDropdown).props('loading')).toBe(true);
+ });
+ });
+
+ describe('when there is no selected network', () => {
+ it('disables the subnetwork dropdown', () => {
+ store = buildStore();
+ wrapper = buildWrapper();
+
+ expect(wrapper.find(ClusterFormDropdown).props('disabled')).toBe(true);
+ });
+ });
+
+ describe('when an error occurs while loading subnetworks', () => {
+ it('sets the subnetwork dropdown as having errors', () => {
+ store = buildStore({ clusterDropdownState: { loadingItemsError: new Error() } });
+ wrapper = buildWrapper();
+
+ expect(wrapper.find(ClusterFormDropdown).props('hasErrors')).toBe(true);
+ });
+ });
+
+ describe('when dropdown emits input event', () => {
+ it('dispatches the setSubnetwork action', () => {
+ store = buildStore();
+ wrapper = buildWrapper();
+
+ wrapper.find(ClusterFormDropdown).vm.$emit('input', selectedSubnetwork);
+
+ expect(setSubnetwork).toHaveBeenCalledWith(expect.anything(), selectedSubnetwork, undefined);
+ });
+ });
+});
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 27fa98bbde4..7bed1f72e0b 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -23,7 +23,7 @@ describe GitlabSchema.types['Project'] do
only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled
namespace group statistics repository merge_requests merge_request issues
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
- grafanaIntegration autocloseReferencedIssues
+ grafanaIntegration autocloseReferencedIssues suggestion_commit_message
]
is_expected.to include_graphql_fields(*expected_fields)
diff --git a/spec/lib/gitlab/ci/build/policy/refs_spec.rb b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
index 8fc1e0a4e88..c32fdc5c72e 100644
--- a/spec/lib/gitlab/ci/build/policy/refs_spec.rb
+++ b/spec/lib/gitlab/ci/build/policy/refs_spec.rb
@@ -98,6 +98,34 @@ describe Gitlab::Ci::Build::Policy::Refs do
.not_to be_satisfied_by(pipeline)
end
end
+
+ context 'when source is pipeline' do
+ let(:pipeline) { build_stubbed(:ci_pipeline, source: :pipeline) }
+
+ it 'is satisfied with only: pipelines' do
+ expect(described_class.new(%w[pipelines]))
+ .to be_satisfied_by(pipeline)
+ end
+
+ it 'is satisfied with only: pipeline' do
+ expect(described_class.new(%w[pipeline]))
+ .to be_satisfied_by(pipeline)
+ end
+ end
+
+ context 'when source is parent_pipeline' do
+ let(:pipeline) { build_stubbed(:ci_pipeline, source: :parent_pipeline) }
+
+ it 'is satisfied with only: parent_pipelines' do
+ expect(described_class.new(%w[parent_pipelines]))
+ .to be_satisfied_by(pipeline)
+ end
+
+ it 'is satisfied with only: parent_pipeline' do
+ expect(described_class.new(%w[parent_pipeline]))
+ .to be_satisfied_by(pipeline)
+ end
+ end
end
context 'when matching a ref by a regular expression' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
index aaea044595f..4c4359ad5d2 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb
@@ -15,6 +15,42 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
stub_feature_flags(ci_root_config_content: false)
end
+ context 'when bridge job is passed in as parameter' do
+ let(:ci_config_path) { nil }
+ let(:bridge) { create(:ci_bridge) }
+
+ before do
+ command.bridge = bridge
+ end
+
+ context 'when bridge job has downstream yaml' do
+ before do
+ allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
+ end
+
+ it 'returns the content already available in command' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'bridge_source'
+ expect(command.config_content).to eq 'the-yaml'
+ end
+ end
+
+ context 'when bridge job does not have downstream yaml' do
+ before do
+ allow(bridge).to receive(:yaml_for_downstream).and_return(nil)
+ end
+
+ it 'returns the next available source' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'auto_devops_source'
+ template = Gitlab::Template::GitlabCiYmlTemplate.find('Beta/Auto-DevOps')
+ expect(command.config_content).to eq(template.content)
+ end
+ end
+ end
+
context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' }
@@ -135,6 +171,23 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
end
end
+ context 'when bridge job is passed in as parameter' do
+ let(:ci_config_path) { nil }
+ let(:bridge) { create(:ci_bridge) }
+
+ before do
+ command.bridge = bridge
+ allow(bridge).to receive(:yaml_for_downstream).and_return('the-yaml')
+ end
+
+ it 'returns the content already available in command' do
+ subject.perform!
+
+ expect(pipeline.config_source).to eq 'bridge_source'
+ expect(command.config_content).to eq 'the-yaml'
+ end
+ end
+
context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' }
let(:config_content_result) do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index a45ad514da2..07439880beb 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -201,6 +201,8 @@ ci_pipelines:
- sourced_pipelines
- triggered_by_pipeline
- triggered_pipelines
+- child_pipelines
+- parent_pipeline
- downstream_bridges
- job_artifacts
- vulnerabilities_occurrence_pipelines
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index adb49c8c7e7..e5014eeca09 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -535,6 +535,7 @@ Project:
- merge_requests_disable_committers_approval
- require_password_to_approve
- autoclose_referenced_issues
+- suggestion_commit_message
ProjectTracingSetting:
- external_url
Author:
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index b30e88532e1..ce01765bb8c 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -2716,4 +2716,114 @@ describe Ci::Pipeline, :mailer do
end
end
end
+
+ describe '#parent_pipeline' do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when pipeline is triggered by a pipeline from the same project' do
+ let(:upstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
+
+ before do
+ create(:ci_sources_pipeline,
+ source_pipeline: upstream_pipeline,
+ source_project: project,
+ pipeline: pipeline,
+ project: project)
+ end
+
+ it 'returns the parent pipeline' do
+ expect(pipeline.parent_pipeline).to eq(upstream_pipeline)
+ end
+
+ it 'is child' do
+ expect(pipeline).to be_child
+ end
+ end
+
+ context 'when pipeline is triggered by a pipeline from another project' do
+ let(:upstream_pipeline) { create(:ci_pipeline) }
+
+ before do
+ create(:ci_sources_pipeline,
+ source_pipeline: upstream_pipeline,
+ source_project: upstream_pipeline.project,
+ pipeline: pipeline,
+ project: project)
+ end
+
+ it 'returns nil' do
+ expect(pipeline.parent_pipeline).to be_nil
+ end
+
+ it 'is not child' do
+ expect(pipeline).not_to be_child
+ end
+ end
+
+ context 'when pipeline is not triggered by a pipeline' do
+ it 'returns nil' do
+ expect(pipeline.parent_pipeline).to be_nil
+ end
+
+ it 'is not child' do
+ expect(pipeline).not_to be_child
+ end
+ end
+ end
+
+ describe '#child_pipelines' do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ context 'when pipeline triggered other pipelines on same project' do
+ let(:downstream_pipeline) { create(:ci_pipeline, project: pipeline.project) }
+
+ before do
+ create(:ci_sources_pipeline,
+ source_pipeline: pipeline,
+ source_project: pipeline.project,
+ pipeline: downstream_pipeline,
+ project: pipeline.project)
+ end
+
+ it 'returns the child pipelines' do
+ expect(pipeline.child_pipelines).to eq [downstream_pipeline]
+ end
+
+ it 'is parent' do
+ expect(pipeline).to be_parent
+ end
+ end
+
+ context 'when pipeline triggered other pipelines on another project' do
+ let(:downstream_pipeline) { create(:ci_pipeline) }
+
+ before do
+ create(:ci_sources_pipeline,
+ source_pipeline: pipeline,
+ source_project: pipeline.project,
+ pipeline: downstream_pipeline,
+ project: downstream_pipeline.project)
+ end
+
+ it 'returns empty array' do
+ expect(pipeline.child_pipelines).to be_empty
+ end
+
+ it 'is not parent' do
+ expect(pipeline).not_to be_parent
+ end
+ end
+
+ context 'when pipeline did not trigger any pipelines' do
+ it 'returns empty array' do
+ expect(pipeline.child_pipelines).to be_empty
+ end
+
+ it 'is not parent' do
+ expect(pipeline).not_to be_parent
+ end
+ end
+ end
end
diff --git a/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb
new file mode 100644
index 00000000000..33cd6e164b0
--- /dev/null
+++ b/spec/services/ci/create_pipeline_service/custom_config_content_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe Ci::CreatePipelineService do
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:admin) }
+ let(:ref) { 'refs/heads/master' }
+ let(:service) { described_class.new(project, user, { ref: ref }) }
+
+ context 'custom config content' do
+ let(:bridge) do
+ double(:bridge, yaml_for_downstream: <<~YML
+ rspec:
+ script: rspec
+ custom:
+ script: custom
+ YML
+ )
+ end
+
+ subject { service.execute(:push, bridge: bridge) }
+
+ it 'creates a pipeline using the content passed in as param' do
+ expect(subject).to be_persisted
+ expect(subject.builds.map(&:name)).to eq %w[rspec custom]
+ expect(subject.config_source).to eq 'bridge_source'
+ end
+ end
+end
diff --git a/spec/services/suggestions/apply_service_spec.rb b/spec/services/suggestions/apply_service_spec.rb
index bdbcb0fdb07..84529af7187 100644
--- a/spec/services/suggestions/apply_service_spec.rb
+++ b/spec/services/suggestions/apply_service_spec.rb
@@ -48,10 +48,34 @@ describe Suggestions::ApplyService do
expect(commit.committer_email).to eq(user.commit_email)
expect(commit.author_name).to eq(user.name)
end
+
+ context 'when a custom suggestion commit message' do
+ before do
+ project.update!(suggestion_commit_message: message)
+
+ apply(suggestion)
+ end
+
+ context 'is not specified' do
+ let(:message) { nil }
+
+ it 'sets default commit message' do
+ expect(project.repository.commit.message).to eq("Apply suggestion to files/ruby/popen.rb")
+ end
+ end
+
+ context 'is specified' do
+ let(:message) { 'refactor: %{project_path} %{project_name} %{file_path} %{branch_name} %{username} %{user_full_name}' }
+
+ it 'sets custom commit message' do
+ expect(project.repository.commit.message).to eq("refactor: project-1 Project_1 files/ruby/popen.rb master test.user Test User")
+ end
+ end
+ end
end
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user, :commit_email) }
+ let(:project) { create(:project, :repository, path: 'project-1', name: 'Project_1') }
+ let(:user) { create(:user, :commit_email, name: 'Test User', username: 'test.user') }
let(:position) { build_position }
@@ -113,7 +137,8 @@ describe Suggestions::ApplyService do
context 'non-fork project' do
let(:merge_request) do
create(:merge_request, source_project: project,
- target_project: project)
+ target_project: project,
+ source_branch: 'master')
end
before do
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
index 8005b549838..e95dec56a2d 100644
--- a/spec/views/projects/edit.html.haml_spec.rb
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -28,6 +28,33 @@ describe 'projects/edit' do
end
end
+ context 'merge suggestions settings' do
+ it 'displays all possible variables' do
+ render
+
+ expect(rendered).to have_content('%{project_path}')
+ expect(rendered).to have_content('%{project_name}')
+ expect(rendered).to have_content('%{file_path}')
+ expect(rendered).to have_content('%{branch_name}')
+ expect(rendered).to have_content('%{username}')
+ expect(rendered).to have_content('%{user_full_name}')
+ end
+
+ it 'displays a placeholder if none is set' do
+ render
+
+ expect(rendered).to have_field('project[suggestion_commit_message]', placeholder: 'Apply suggestion to %{file_path}')
+ end
+
+ it 'displays the user entered value' do
+ project.update!(suggestion_commit_message: 'refactor: changed %{file_path}')
+
+ render
+
+ expect(rendered).to have_field('project[suggestion_commit_message]', with: 'refactor: changed %{file_path}')
+ end
+ end
+
context 'forking' do
before do
assign(:project, project)