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 {
<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 }}
......
......@@ -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
......
......@@ -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)
......
......@@ -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'
......
......@@ -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
......
......@@ -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)
......
......@@ -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
......
......@@ -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
......
# frozen_string_literal: true
class PipelineDetailsEntity < PipelineEntity
expose :project, using: ProjectEntity
expose :flags do
expose :latest?, as: :latest
end
......
......@@ -41,6 +41,7 @@ class PipelineSerializer < BaseSerializer
def preloaded_relations
[
:latest_statuses_ordered_by_stage,
:project,
:stages,
{
failed_builds: %i(project metadata)
......
......@@ -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
......
......@@ -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
......@@ -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"
......
......@@ -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' }
- 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 @@
= 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
%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
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"
......
......@@ -5154,6 +5154,11 @@ type Project {
"""
statistics: ProjectStatistics
"""
The commit message used to apply merge request suggestions
"""
suggestionCommitMessage: String
"""
List of project tags
"""
......
......@@ -1525,6 +1525,20 @@
"isDeprecated": false,
"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",
......
......@@ -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 |
......
......@@ -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",
......
......@@ -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
......
......@@ -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)
......
......@@ -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)
......
......@@ -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)
}
......
......@@ -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,
......
......@@ -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
......
......@@ -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
......
......@@ -6,20 +6,15 @@ module Gitlab
module Chain
module Config
class Content
class Runtime < Source
class Bridge < Source
def content
@command.config_content
return unless @command.bridge
@command.bridge.yaml_for_downstream
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
:bridge_source
end
end
end
......
......@@ -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 ""
......
......@@ -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 } }
......
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
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)
......
......@@ -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
......
......@@ -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
......
......@@ -201,6 +201,8 @@ ci_pipelines:
- sourced_pipelines
- triggered_by_pipeline
- triggered_pipelines
- child_pipelines
- parent_pipeline
- downstream_bridges
- job_artifacts
- vulnerabilities_occurrence_pipelines
......
......@@ -535,6 +535,7 @@ Project:
- merge_requests_disable_committers_approval
- require_password_to_approve
- autoclose_referenced_issues
- suggestion_commit_message
ProjectTracingSetting:
- external_url
Author:
......
......@@ -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
# 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
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
......
......@@ -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)
......
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