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