Commit eb30dd6e authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent c3ad5703
<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>
<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>
...@@ -165,21 +165,23 @@ export default { ...@@ -165,21 +165,23 @@ export default {
<template> <template>
<section class="media-section"> <section class="media-section">
<div class="media"> <div class="media">
<status-icon :status="statusIconName" :size="24" /> <status-icon :status="statusIconName" :size="24" class="align-self-center" />
<div class="media-body d-flex flex-align-self-center"> <div class="media-body d-flex flex-align-self-center align-items-center">
<span class="js-code-text code-text"> <div class="js-code-text code-text">
{{ headerText }} <div>
<slot :name="slotName"></slot> {{ headerText }}
<slot :name="slotName"></slot>
<popover v-if="hasPopover" :options="popoverOptions" class="prepend-left-5" /> <popover v-if="hasPopover" :options="popoverOptions" class="prepend-left-5" />
</span> </div>
<slot name="subHeading"></slot>
</div>
<slot name="actionButtons"></slot> <slot name="actionButtons"></slot>
<button <button
v-if="isCollapsible" v-if="isCollapsible"
type="button" 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" @click="toggleCollapsed"
> >
{{ collapseText }} {{ collapseText }}
......
...@@ -387,6 +387,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -387,6 +387,7 @@ class ProjectsController < Projects::ApplicationController
:merge_method, :merge_method,
:initialize_with_readme, :initialize_with_readme,
:autoclose_referenced_issues, :autoclose_referenced_issues,
:suggestion_commit_message,
project_feature_attributes: %i[ project_feature_attributes: %i[
builds_access_level builds_access_level
......
...@@ -17,7 +17,7 @@ class PipelinesFinder ...@@ -17,7 +17,7 @@ class PipelinesFinder
return Ci::Pipeline.none return Ci::Pipeline.none
end end
items = pipelines items = pipelines.no_child
items = by_scope(items) items = by_scope(items)
items = by_status(items) items = by_status(items)
items = by_ref(items) items = by_ref(items)
......
...@@ -104,6 +104,8 @@ module Types ...@@ -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' 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, 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' 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, field :namespace, Types::NamespaceType, null: true,
description: 'Namespace of the project' description: 'Namespace of the project'
......
...@@ -53,6 +53,10 @@ module Ci ...@@ -53,6 +53,10 @@ module Ci
def to_partial_path def to_partial_path
'projects/generic_commit_statuses/generic_commit_status' 'projects/generic_commit_statuses/generic_commit_status'
end end
def yaml_for_downstream
nil
end
end end
end end
......
...@@ -60,7 +60,9 @@ module Ci ...@@ -60,7 +60,9 @@ module Ci
has_one :chat_data, class_name: 'Ci::PipelineChatData' has_one :chat_data, class_name: 'Ci::PipelineChatData'
has_many :triggered_pipelines, through: :sourced_pipelines, source: :pipeline 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 :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 :source_job, through: :source_pipeline, source: :source_job
has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline has_one :pipeline_config, class_name: 'Ci::PipelineConfig', inverse_of: :pipeline
...@@ -212,6 +214,7 @@ module Ci ...@@ -212,6 +214,7 @@ module Ci
end end
scope :internal, -> { where(source: internal_sources) } 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 :ci_sources, -> { where(config_source: ::Ci::PipelineEnums.ci_config_sources_values) }
scope :for_user, -> (user) { where(user: user) } scope :for_user, -> (user) { where(user: user) }
scope :for_sha, -> (sha) { where(sha: sha) } scope :for_sha, -> (sha) { where(sha: sha) }
...@@ -507,10 +510,6 @@ module Ci ...@@ -507,10 +510,6 @@ module Ci
builds.skipped.after_stage(stage_idx).find_each(&:process) builds.skipped.after_stage(stage_idx).find_each(&:process)
end end
def child?
false
end
def latest? def latest?
return false unless git_ref && commit.present? return false unless git_ref && commit.present?
...@@ -693,6 +692,24 @@ module Ci ...@@ -693,6 +692,24 @@ module Ci
all_merge_requests.order(id: :desc) all_merge_requests.order(id: :desc)
end 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) def detailed_status(current_user)
Gitlab::Ci::Status::Pipeline::Factory Gitlab::Ci::Status::Pipeline::Factory
.new(self, current_user) .new(self, current_user)
......
...@@ -23,10 +23,13 @@ module Ci ...@@ -23,10 +23,13 @@ module Ci
schedule: 4, schedule: 4,
api: 5, api: 5,
external: 6, external: 6,
# TODO: Rename `pipeline` to `cross_project_pipeline` in 13.0
# https://gitlab.com/gitlab-org/gitlab/issues/195991
pipeline: 7, pipeline: 7,
chat: 8, chat: 8,
merge_request_event: 10, merge_request_event: 10,
external_pull_request_event: 11 external_pull_request_event: 11,
parent_pipeline: 12
} }
end end
...@@ -38,7 +41,8 @@ module Ci ...@@ -38,7 +41,8 @@ module Ci
repository_source: 1, repository_source: 1,
auto_devops_source: 2, auto_devops_source: 2,
remote_source: 4, remote_source: 4,
external_project_source: 5 external_project_source: 5,
bridge_source: 6
} }
end end
......
...@@ -18,6 +18,8 @@ module Ci ...@@ -18,6 +18,8 @@ module Ci
validates :source_project, presence: true validates :source_project, presence: true
validates :source_job, presence: true validates :source_job, presence: true
validates :source_pipeline, presence: true validates :source_pipeline, presence: true
scope :same_project, -> { where(arel_table[:source_project_id].eq(arel_table[:project_id])) }
end end
end end
end end
......
# frozen_string_literal: true # frozen_string_literal: true
class PipelineDetailsEntity < PipelineEntity class PipelineDetailsEntity < PipelineEntity
expose :project, using: ProjectEntity
expose :flags do expose :flags do
expose :latest?, as: :latest expose :latest?, as: :latest
end end
......
...@@ -41,6 +41,7 @@ class PipelineSerializer < BaseSerializer ...@@ -41,6 +41,7 @@ class PipelineSerializer < BaseSerializer
def preloaded_relations def preloaded_relations
[ [
:latest_statuses_ordered_by_stage, :latest_statuses_ordered_by_stage,
:project,
:stages, :stages,
{ {
failed_builds: %i(project metadata) failed_builds: %i(project metadata)
......
...@@ -23,7 +23,7 @@ module Ci ...@@ -23,7 +23,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze Gitlab::Ci::Pipeline::Chain::Limit::JobActivity].freeze
# rubocop: disable Metrics/ParameterLists # 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 @pipeline = Ci::Pipeline.new
command = Gitlab::Ci::Pipeline::Chain::Command.new( command = Gitlab::Ci::Pipeline::Chain::Command.new(
...@@ -46,6 +46,7 @@ module Ci ...@@ -46,6 +46,7 @@ module Ci
current_user: current_user, current_user: current_user,
push_options: params[:push_options] || {}, push_options: params[:push_options] || {},
chat_data: params[:chat_data], chat_data: params[:chat_data],
bridge: bridge,
**extra_options(options)) **extra_options(options))
sequence = Gitlab::Ci::Pipeline::Chain::Sequence sequence = Gitlab::Ci::Pipeline::Chain::Sequence
...@@ -104,14 +105,14 @@ module Ci ...@@ -104,14 +105,14 @@ module Ci
if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true) if Feature.enabled?(:ci_support_interruptible_pipelines, project, default_enabled: true)
project.ci_pipelines project.ci_pipelines
.where(ref: pipeline.ref) .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)) .where.not(sha: project.commit(pipeline.ref).try(:id))
.alive_or_scheduled .alive_or_scheduled
.with_only_interruptible_builds .with_only_interruptible_builds
else else
project.ci_pipelines project.ci_pipelines
.where(ref: pipeline.ref) .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)) .where.not(sha: project.commit(pipeline.ref).try(:id))
.created_or_pending .created_or_pending
end end
......
...@@ -2,6 +2,24 @@ ...@@ -2,6 +2,24 @@
module Suggestions module Suggestions
class ApplyService < ::BaseService 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) def initialize(current_user)
@current_user = current_user @current_user = current_user
end end
...@@ -22,7 +40,7 @@ module Suggestions ...@@ -22,7 +40,7 @@ module Suggestions
end end
params = file_update_params(suggestion, diff_file) 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 if result[:status] == :success
suggestion.update(commit_id: result[:result], applied: true) suggestion.update(commit_id: result[:result], applied: true)
...@@ -46,13 +64,14 @@ module Suggestions ...@@ -46,13 +64,14 @@ module Suggestions
def file_update_params(suggestion, diff_file) def file_update_params(suggestion, diff_file)
blob = diff_file.new_blob blob = diff_file.new_blob
project = suggestion.project
file_path = suggestion.file_path file_path = suggestion.file_path
branch_name = suggestion.branch branch_name = suggestion.branch
file_content = new_file_content(suggestion, blob) file_content = new_file_content(suggestion, blob)
commit_message = "Apply suggestion to #{file_path}" commit_message = processed_suggestion_commit_message(suggestion)
file_last_commit = file_last_commit =
Gitlab::Git::Commit.last_for_path(suggestion.project.repository, Gitlab::Git::Commit.last_for_path(project.repository,
blob.commit_id, blob.commit_id,
blob.path) blob.path)
...@@ -75,5 +94,17 @@ module Suggestions ...@@ -75,5 +94,17 @@ module Suggestions
content.join content.join
end 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
end end
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
%div %div
= f.label 'Two-Factor Authentication code', name: :otp_attempt = 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. %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 .prepend-top-20
= f.submit "Verify code", class: "btn btn-success" = f.submit "Verify code", class: "btn btn-success"
......
...@@ -5,7 +5,8 @@ ...@@ -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| = 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 .devise-errors.mt-0
= render "devise/shared/error_messages", resource: resource = render "devise/shared/error_messages", resource: resource
= invisible_captcha - if Feature.enabled?(:invisible_captcha)
= invisible_captcha
.username.form-group .username.form-group
= f.label :username, class: 'label-bold' = 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.") = 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 @@ ...@@ -27,5 +28,8 @@
- accept_terms_label = _("I accept the %{terms_link}") % { terms_link: terms_link } - accept_terms_label = _("I accept the %{terms_link}") % { terms_link: terms_link }
= accept_terms_label.html_safe = accept_terms_label.html_safe
= render_if_exists 'devise/shared/email_opted_in', f: f = render_if_exists 'devise/shared/email_opted_in', f: f
%div
- if show_recaptcha_sign_up?
= recaptcha_tags
.submit-container.mt-3 .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' } = f.submit _("Register"), class: "btn-register btn btn-block btn-success mb-0 p-2", data: { qa_selector: 'new_user_register_button' }
- 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
...@@ -5,3 +5,5 @@ ...@@ -5,3 +5,5 @@
= render 'projects/merge_request_merge_options_settings', project: @project, form: form = 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_checks_settings', project: @project, form: form
= render 'projects/merge_request_merge_suggestions_settings', project: @project, form: form
%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.')
---
title: Display in MR if security report is outdated
merge_request: 20954
author:
type: other
---
title: Allow an upstream pipeline to create a downstream pipeline in the same project
merge_request: 22663
author:
type: added
---
title: Implement customizable commit messages for applied suggested changes
merge_request: 21411
author: Fabio Huser
type: added
# 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
...@@ -3348,6 +3348,7 @@ ActiveRecord::Schema.define(version: 2020_01_08_233040) do ...@@ -3348,6 +3348,7 @@ ActiveRecord::Schema.define(version: 2020_01_08_233040) do
t.date "marked_for_deletion_at" t.date "marked_for_deletion_at"
t.integer "marked_for_deletion_by_user_id" t.integer "marked_for_deletion_by_user_id"
t.boolean "autoclose_referenced_issues" 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 "lower((name)::text)", name: "index_projects_on_lower_name"
t.index ["created_at", "id"], name: "index_projects_on_created_at_and_id" t.index ["created_at", "id"], name: "index_projects_on_created_at_and_id"
t.index ["creator_id"], name: "index_projects_on_creator_id" t.index ["creator_id"], name: "index_projects_on_creator_id"
......
...@@ -5154,6 +5154,11 @@ type Project { ...@@ -5154,6 +5154,11 @@ type Project {
""" """
statistics: ProjectStatistics statistics: ProjectStatistics
"""
The commit message used to apply merge request suggestions
"""
suggestionCommitMessage: String
""" """
List of project tags List of project tags
""" """
......
...@@ -1525,6 +1525,20 @@ ...@@ -1525,6 +1525,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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", "name": "tagList",
"description": "List of project tags", "description": "List of project tags",
......
...@@ -769,6 +769,7 @@ Information about pagination in a connection. ...@@ -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 | | `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 | | `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 | | `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 | | `namespace` | Namespace | Namespace of the project |
| `group` | Group | Group of the project | | `group` | Group | Group of the project |
| `statistics` | ProjectStatistics | Statistics of the project | | `statistics` | ProjectStatistics | Statistics of the project |
......
...@@ -157,6 +157,7 @@ When the user is authenticated and `simple` is not set this returns something li ...@@ -157,6 +157,7 @@ When the user is authenticated and `simple` is not set this returns something li
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true, "autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"statistics": { "statistics": {
"commit_count": 37, "commit_count": 37,
"storage_size": 1038090, "storage_size": 1038090,
...@@ -256,6 +257,7 @@ When the user is authenticated and `simple` is not set this returns something li ...@@ -256,6 +257,7 @@ When the user is authenticated and `simple` is not set this returns something li
"service_desk_enabled": false, "service_desk_enabled": false,
"service_desk_address": null, "service_desk_address": null,
"autoclose_referenced_issues": true, "autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"statistics": { "statistics": {
"commit_count": 12, "commit_count": 12,
"storage_size": 2066080, "storage_size": 2066080,
...@@ -388,6 +390,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo ...@@ -388,6 +390,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true, "autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"statistics": { "statistics": {
"commit_count": 37, "commit_count": 37,
"storage_size": 1038090, "storage_size": 1038090,
...@@ -487,6 +490,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo ...@@ -487,6 +490,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo
"service_desk_enabled": false, "service_desk_enabled": false,
"service_desk_address": null, "service_desk_address": null,
"autoclose_referenced_issues": true, "autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"statistics": { "statistics": {
"commit_count": 12, "commit_count": 12,
"storage_size": 2066080, "storage_size": 2066080,
...@@ -598,6 +602,7 @@ Example response: ...@@ -598,6 +602,7 @@ Example response:
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true, "autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"statistics": { "statistics": {
"commit_count": 37, "commit_count": 37,
"storage_size": 1038090, "storage_size": 1038090,
...@@ -694,6 +699,7 @@ Example response: ...@@ -694,6 +699,7 @@ Example response:
"service_desk_enabled": false, "service_desk_enabled": false,
"service_desk_address": null, "service_desk_address": null,
"autoclose_referenced_issues": true, "autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"statistics": { "statistics": {
"commit_count": 12, "commit_count": 12,
"storage_size": 2066080, "storage_size": 2066080,
...@@ -844,6 +850,7 @@ GET /projects/:id ...@@ -844,6 +850,7 @@ GET /projects/:id
"service_desk_enabled": false, "service_desk_enabled": false,
"service_desk_address": null, "service_desk_address": null,
"autoclose_referenced_issues": true, "autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"statistics": { "statistics": {
"commit_count": 37, "commit_count": 37,
"storage_size": 1038090, "storage_size": 1038090,
...@@ -1068,6 +1075,7 @@ POST /projects/user/:user_id ...@@ -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 | | `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 | | `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 | | `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 | | `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 | | `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access | | `request_access_enabled` | boolean | no | Allow users to request member access |
...@@ -1133,6 +1141,7 @@ PUT /projects/:id ...@@ -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 | | `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 | | `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 | | `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 | | `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 | | `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access | | `request_access_enabled` | boolean | no | Allow users to request member access |
...@@ -1265,6 +1274,7 @@ Example responses: ...@@ -1265,6 +1274,7 @@ Example responses:
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true, "autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"_links": { "_links": {
"self": "http://example.com/api/v4/projects", "self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues", "issues": "http://example.com/api/v4/projects/1/issues",
...@@ -1354,6 +1364,7 @@ Example response: ...@@ -1354,6 +1364,7 @@ Example response:
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true, "autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"_links": { "_links": {
"self": "http://example.com/api/v4/projects", "self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues", "issues": "http://example.com/api/v4/projects/1/issues",
...@@ -1442,6 +1453,7 @@ Example response: ...@@ -1442,6 +1453,7 @@ Example response:
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true, "autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"_links": { "_links": {
"self": "http://example.com/api/v4/projects", "self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues", "issues": "http://example.com/api/v4/projects/1/issues",
...@@ -1617,6 +1629,7 @@ Example response: ...@@ -1617,6 +1629,7 @@ Example response:
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true, "autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"_links": { "_links": {
"self": "http://example.com/api/v4/projects", "self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues", "issues": "http://example.com/api/v4/projects/1/issues",
...@@ -1724,6 +1737,7 @@ Example response: ...@@ -1724,6 +1737,7 @@ Example response:
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true, "autoclose_referenced_issues": true,
"suggestion_commit_message": null,
"_links": { "_links": {
"self": "http://example.com/api/v4/projects", "self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues", "issues": "http://example.com/api/v4/projects/1/issues",
......
...@@ -414,9 +414,8 @@ the Merge Request authored by the user that applied them. ...@@ -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, 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 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 and push the suggested change directly into the codebase in the merge request's
directly into the codebase in the merge request's branch. branch. [Developer permission](../permissions.md) is required to do so.
[Developer permission](../permissions.md) is required to do so.
> **Note:** > **Note:**
Custom commit messages will be introduced by Custom commit messages will be introduced by
...@@ -444,6 +443,24 @@ Suggestions covering multiple lines are limited to 100 lines _above_ and 100 ...@@ -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 lines _below_ the commented diff line, allowing up to 200 changed lines per
suggestion. 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 ## Start a thread by replying to a standard comment
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/30299) in GitLab 11.9 > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/issues/30299) in GitLab 11.9
......
...@@ -81,6 +81,33 @@ navigation's **Issues** menu. ...@@ -81,6 +81,33 @@ navigation's **Issues** menu.
![Service Desk Navigation Item](img/service_desk_nav_item.png) ![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 ## Using Service Desk
### As an end user (issue creator) ### As an end user (issue creator)
......
...@@ -91,6 +91,7 @@ Set up your project's merge request settings: ...@@ -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 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 [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) - 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) ![project's merge request settings](img/merge_requests_settings.png)
......
...@@ -335,6 +335,7 @@ module API ...@@ -335,6 +335,7 @@ module API
expose :remove_source_branch_after_merge expose :remove_source_branch_after_merge
expose :printing_merge_request_link_enabled expose :printing_merge_request_link_enabled
expose :merge_method expose :merge_method
expose :suggestion_commit_message
expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) { expose :statistics, using: 'API::Entities::ProjectStatistics', if: -> (project, options) {
options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project) options[:statistics] && Ability.allowed?(options[:current_user], :read_statistics, project)
} }
......
...@@ -46,6 +46,7 @@ module API ...@@ -46,6 +46,7 @@ module API
optional :avatar, type: File, desc: 'Avatar image for project' # rubocop:disable Scalability/FileUploads 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 :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 :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 :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 :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' optional :auto_devops_enabled, type: Boolean, desc: 'Flag indication if Auto DevOps is enabled'
...@@ -119,6 +120,7 @@ module API ...@@ -119,6 +120,7 @@ module API
:visibility, :visibility,
:wiki_access_level, :wiki_access_level,
:avatar, :avatar,
:suggestion_commit_message,
# TODO: remove in API v5, replaced by *_access_level # TODO: remove in API v5, replaced by *_access_level
:issues_enabled, :issues_enabled,
......
...@@ -10,7 +10,7 @@ module Gitlab ...@@ -10,7 +10,7 @@ module Gitlab
:trigger_request, :schedule, :merge_request, :external_pull_request, :trigger_request, :schedule, :merge_request, :external_pull_request,
:ignore_skip_ci, :save_incompleted, :ignore_skip_ci, :save_incompleted,
:seeds_block, :variables_attributes, :push_options, :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: # These attributes are set by Chains during processing:
:config_content, :config_processor, :stage_seeds :config_content, :config_processor, :stage_seeds
) do ) do
......
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
include Chain::Helpers include Chain::Helpers
SOURCES = [ 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::Repository,
Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject, Gitlab::Ci::Pipeline::Chain::Config::Content::ExternalProject,
Gitlab::Ci::Pipeline::Chain::Config::Content::Remote, Gitlab::Ci::Pipeline::Chain::Config::Content::Remote,
...@@ -17,7 +17,7 @@ module Gitlab ...@@ -17,7 +17,7 @@ module Gitlab
].freeze ].freeze
LEGACY_SOURCES = [ 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::LegacyRepository,
Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops Gitlab::Ci::Pipeline::Chain::Config::Content::LegacyAutoDevops
].freeze ].freeze
......
...@@ -6,20 +6,15 @@ module Gitlab ...@@ -6,20 +6,15 @@ module Gitlab
module Chain module Chain
module Config module Config
class Content class Content
class Runtime < Source class Bridge < Source
def content def content
@command.config_content return unless @command.bridge
@command.bridge.yaml_for_downstream
end end
def source def source
# The only case when this source is used is when the config content :bridge_source
# 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
......
...@@ -3854,6 +3854,9 @@ msgstr "" ...@@ -3854,6 +3854,9 @@ msgstr ""
msgid "ClusterIntegration|Could not load instance types" msgid "ClusterIntegration|Could not load instance types"
msgstr "" msgstr ""
msgid "ClusterIntegration|Could not load networks"
msgstr ""
msgid "ClusterIntegration|Could not load regions from your AWS account" msgid "ClusterIntegration|Could not load regions from your AWS account"
msgstr "" msgstr ""
...@@ -3863,6 +3866,9 @@ msgstr "" ...@@ -3863,6 +3866,9 @@ msgstr ""
msgid "ClusterIntegration|Could not load subnets for the selected VPC" msgid "ClusterIntegration|Could not load subnets for the selected VPC"
msgstr "" msgstr ""
msgid "ClusterIntegration|Could not load subnetworks"
msgstr ""
msgid "ClusterIntegration|Create Kubernetes cluster" msgid "ClusterIntegration|Create Kubernetes cluster"
msgstr "" msgstr ""
...@@ -4127,12 +4133,18 @@ msgstr "" ...@@ -4127,12 +4133,18 @@ msgstr ""
msgid "ClusterIntegration|Loading instance types" msgid "ClusterIntegration|Loading instance types"
msgstr "" msgstr ""
msgid "ClusterIntegration|Loading networks"
msgstr ""
msgid "ClusterIntegration|Loading security groups" msgid "ClusterIntegration|Loading security groups"
msgstr "" msgstr ""
msgid "ClusterIntegration|Loading subnets" msgid "ClusterIntegration|Loading subnets"
msgstr "" msgstr ""
msgid "ClusterIntegration|Loading subnetworks"
msgstr ""
msgid "ClusterIntegration|Machine type" msgid "ClusterIntegration|Machine type"
msgstr "" msgstr ""
...@@ -4157,6 +4169,9 @@ msgstr "" ...@@ -4157,6 +4169,9 @@ msgstr ""
msgid "ClusterIntegration|No machine types matched your search" msgid "ClusterIntegration|No machine types matched your search"
msgstr "" msgstr ""
msgid "ClusterIntegration|No networks found"
msgstr ""
msgid "ClusterIntegration|No projects found" msgid "ClusterIntegration|No projects found"
msgstr "" msgstr ""
...@@ -4172,6 +4187,9 @@ msgstr "" ...@@ -4172,6 +4187,9 @@ msgstr ""
msgid "ClusterIntegration|No subnet found" msgid "ClusterIntegration|No subnet found"
msgstr "" msgstr ""
msgid "ClusterIntegration|No subnetworks found"
msgstr ""
msgid "ClusterIntegration|No zones matched your search" msgid "ClusterIntegration|No zones matched your search"
msgstr "" msgstr ""
...@@ -4268,6 +4286,9 @@ msgstr "" ...@@ -4268,6 +4286,9 @@ msgstr ""
msgid "ClusterIntegration|Search machine types" msgid "ClusterIntegration|Search machine types"
msgstr "" msgstr ""
msgid "ClusterIntegration|Search networks"
msgstr ""
msgid "ClusterIntegration|Search projects" msgid "ClusterIntegration|Search projects"
msgstr "" msgstr ""
...@@ -4280,6 +4301,9 @@ msgstr "" ...@@ -4280,6 +4301,9 @@ msgstr ""
msgid "ClusterIntegration|Search subnets" msgid "ClusterIntegration|Search subnets"
msgstr "" msgstr ""
msgid "ClusterIntegration|Search subnetworks"
msgstr ""
msgid "ClusterIntegration|Search zones" msgid "ClusterIntegration|Search zones"
msgstr "" msgstr ""
...@@ -4298,6 +4322,9 @@ 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}." 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 "" msgstr ""
msgid "ClusterIntegration|Select a network to choose a subnetwork"
msgstr ""
msgid "ClusterIntegration|Select a region to choose a Key Pair" msgid "ClusterIntegration|Select a region to choose a Key Pair"
msgstr "" msgstr ""
...@@ -4307,6 +4334,9 @@ msgstr "" ...@@ -4307,6 +4334,9 @@ msgstr ""
msgid "ClusterIntegration|Select a stack to install Crossplane." msgid "ClusterIntegration|Select a stack to install Crossplane."
msgstr "" msgstr ""
msgid "ClusterIntegration|Select a zone to choose a network"
msgstr ""
msgid "ClusterIntegration|Select machine type" msgid "ClusterIntegration|Select machine type"
msgstr "" msgstr ""
...@@ -4484,6 +4514,9 @@ msgstr "" ...@@ -4484,6 +4514,9 @@ msgstr ""
msgid "ClusterIntergation|Select a VPC" msgid "ClusterIntergation|Select a VPC"
msgstr "" msgstr ""
msgid "ClusterIntergation|Select a network"
msgstr ""
msgid "ClusterIntergation|Select a region" msgid "ClusterIntergation|Select a region"
msgstr "" msgstr ""
...@@ -4493,6 +4526,9 @@ msgstr "" ...@@ -4493,6 +4526,9 @@ msgstr ""
msgid "ClusterIntergation|Select a subnet" msgid "ClusterIntergation|Select a subnet"
msgstr "" msgstr ""
msgid "ClusterIntergation|Select a subnetwork"
msgstr ""
msgid "ClusterIntergation|Select an instance type" msgid "ClusterIntergation|Select an instance type"
msgstr "" msgstr ""
...@@ -8330,7 +8366,7 @@ msgstr "" ...@@ -8330,7 +8366,7 @@ msgstr ""
msgid "GeoNodes|Pausing replication stops the sync process. Are you sure?" msgid "GeoNodes|Pausing replication stops the sync process. Are you sure?"
msgstr "" 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 "" msgstr ""
msgid "GeoNodes|Removing a Geo secondary node stops the synchronization to that node. Are you sure?" msgid "GeoNodes|Removing a Geo secondary node stops the synchronization to that node. Are you sure?"
...@@ -14130,10 +14166,10 @@ msgstr "" ...@@ -14130,10 +14166,10 @@ msgstr ""
msgid "ProjectSettings|Build, test, and deploy your changes" msgid "ProjectSettings|Build, test, and deploy your changes"
msgstr "" 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 "" 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 "" msgstr ""
msgid "ProjectSettings|Contact an admin to change this setting." msgid "ProjectSettings|Contact an admin to change this setting."
...@@ -14217,6 +14253,9 @@ msgstr "" ...@@ -14217,6 +14253,9 @@ msgstr ""
msgid "ProjectSettings|Merge requests" msgid "ProjectSettings|Merge requests"
msgstr "" msgstr ""
msgid "ProjectSettings|Merge suggestions"
msgstr ""
msgid "ProjectSettings|No merge commits are created" msgid "ProjectSettings|No merge commits are created"
msgstr "" msgstr ""
...@@ -14268,6 +14307,12 @@ msgstr "" ...@@ -14268,6 +14307,12 @@ msgstr ""
msgid "ProjectSettings|Submit changes to be merged upstream" msgid "ProjectSettings|Submit changes to be merged upstream"
msgstr "" 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" msgid "ProjectSettings|These checks must pass before merge requests can be merged"
msgstr "" msgstr ""
...@@ -16100,6 +16145,12 @@ msgstr "" ...@@ -16100,6 +16145,12 @@ msgstr ""
msgid "Security dashboard" msgid "Security dashboard"
msgstr "" 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" msgid "SecurityConfiguration|Configured"
msgstr "" msgstr ""
......
...@@ -64,6 +64,19 @@ describe PipelinesFinder do ...@@ -64,6 +64,19 @@ describe PipelinesFinder do
end end
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| HasStatus::AVAILABLE_STATUSES.each do |target|
context "when status is #{target}" do context "when status is #{target}" do
let(:params) { { status: target } } let(:params) { { status: target } }
......
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,
);
});
});
});
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);
});
});
});
...@@ -23,7 +23,7 @@ describe GitlabSchema.types['Project'] do ...@@ -23,7 +23,7 @@ describe GitlabSchema.types['Project'] do
only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled
namespace group statistics repository merge_requests merge_request issues namespace group statistics repository merge_requests merge_request issues
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
grafanaIntegration autocloseReferencedIssues grafanaIntegration autocloseReferencedIssues suggestion_commit_message
] ]
is_expected.to include_graphql_fields(*expected_fields) is_expected.to include_graphql_fields(*expected_fields)
......
...@@ -98,6 +98,34 @@ describe Gitlab::Ci::Build::Policy::Refs do ...@@ -98,6 +98,34 @@ describe Gitlab::Ci::Build::Policy::Refs do
.not_to be_satisfied_by(pipeline) .not_to be_satisfied_by(pipeline)
end end
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 end
context 'when matching a ref by a regular expression' do context 'when matching a ref by a regular expression' do
......
...@@ -15,6 +15,42 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do ...@@ -15,6 +15,42 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
stub_feature_flags(ci_root_config_content: false) stub_feature_flags(ci_root_config_content: false)
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
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 context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' } let(:ci_config_path) { 'path/to/config.yml' }
...@@ -135,6 +171,23 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do ...@@ -135,6 +171,23 @@ describe Gitlab::Ci::Pipeline::Chain::Config::Content do
end end
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 context 'when config is defined in a custom path in the repository' do
let(:ci_config_path) { 'path/to/config.yml' } let(:ci_config_path) { 'path/to/config.yml' }
let(:config_content_result) do let(:config_content_result) do
......
...@@ -201,6 +201,8 @@ ci_pipelines: ...@@ -201,6 +201,8 @@ ci_pipelines:
- sourced_pipelines - sourced_pipelines
- triggered_by_pipeline - triggered_by_pipeline
- triggered_pipelines - triggered_pipelines
- child_pipelines
- parent_pipeline
- downstream_bridges - downstream_bridges
- job_artifacts - job_artifacts
- vulnerabilities_occurrence_pipelines - vulnerabilities_occurrence_pipelines
......
...@@ -535,6 +535,7 @@ Project: ...@@ -535,6 +535,7 @@ Project:
- merge_requests_disable_committers_approval - merge_requests_disable_committers_approval
- require_password_to_approve - require_password_to_approve
- autoclose_referenced_issues - autoclose_referenced_issues
- suggestion_commit_message
ProjectTracingSetting: ProjectTracingSetting:
- external_url - external_url
Author: Author:
......
...@@ -2716,4 +2716,114 @@ describe Ci::Pipeline, :mailer do ...@@ -2716,4 +2716,114 @@ describe Ci::Pipeline, :mailer do
end end
end 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 end
# 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
...@@ -48,10 +48,34 @@ describe Suggestions::ApplyService do ...@@ -48,10 +48,34 @@ describe Suggestions::ApplyService do
expect(commit.committer_email).to eq(user.commit_email) expect(commit.committer_email).to eq(user.commit_email)
expect(commit.author_name).to eq(user.name) expect(commit.author_name).to eq(user.name)
end 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 end
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository, path: 'project-1', name: 'Project_1') }
let(:user) { create(:user, :commit_email) } let(:user) { create(:user, :commit_email, name: 'Test User', username: 'test.user') }
let(:position) { build_position } let(:position) { build_position }
...@@ -113,7 +137,8 @@ describe Suggestions::ApplyService do ...@@ -113,7 +137,8 @@ describe Suggestions::ApplyService do
context 'non-fork project' do context 'non-fork project' do
let(:merge_request) do let(:merge_request) do
create(:merge_request, source_project: project, create(:merge_request, source_project: project,
target_project: project) target_project: project,
source_branch: 'master')
end end
before do before do
......
...@@ -28,6 +28,33 @@ describe 'projects/edit' do ...@@ -28,6 +28,33 @@ describe 'projects/edit' do
end end
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 context 'forking' do
before do before do
assign(:project, project) assign(:project, project)
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment