Commit 2b3bfe8f authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent d203316c
...@@ -386,6 +386,7 @@ class ProjectsController < Projects::ApplicationController ...@@ -386,6 +386,7 @@ class ProjectsController < Projects::ApplicationController
:template_project_id, :template_project_id,
:merge_method, :merge_method,
:initialize_with_readme, :initialize_with_readme,
:autoclose_referenced_issues,
project_feature_attributes: %i[ project_feature_attributes: %i[
builds_access_level builds_access_level
......
...@@ -5,10 +5,9 @@ module Mutations ...@@ -5,10 +5,9 @@ module Mutations
class Toggle < Base class Toggle < Base
graphql_name 'ToggleAwardEmoji' graphql_name 'ToggleAwardEmoji'
field :toggledOn, field :toggledOn, GraphQL::BOOLEAN_TYPE, null: false,
GraphQL::BOOLEAN_TYPE, description: 'Indicates the status of the emoji. ' \
null: false, 'True if the toggle awarded the emoji, and false if the toggle removed the emoji.'
description: 'True when the emoji was awarded, false when it was removed'
def resolve(args) def resolve(args)
awardable = authorized_find!(id: args[:awardable_id]) awardable = authorized_find!(id: args[:awardable_id])
......
...@@ -69,7 +69,7 @@ module Types ...@@ -69,7 +69,7 @@ module Types
field :participants, Types::UserType.connection_type, null: true, complexity: 5, field :participants, Types::UserType.connection_type, null: true, complexity: 5,
description: 'List of participants in the issue' description: 'List of participants in the issue'
field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false, complexity: 5, field :subscribed, GraphQL::BOOLEAN_TYPE, method: :subscribed?, null: false, complexity: 5,
description: 'Boolean flag for whether the currently logged in user is subscribed to this issue' description: 'Indicates the currently logged in user is subscribed to the issue'
field :time_estimate, GraphQL::INT_TYPE, null: false, field :time_estimate, GraphQL::INT_TYPE, null: false,
description: 'Time estimate of the issue' description: 'Time estimate of the issue'
field :total_time_spent, GraphQL::INT_TYPE, null: false, field :total_time_spent, GraphQL::INT_TYPE, null: false,
......
...@@ -25,7 +25,7 @@ module Types ...@@ -25,7 +25,7 @@ module Types
kword_args = kword_args.reverse_merge( kword_args = kword_args.reverse_merge(
name: name, name: name,
type: GraphQL::BOOLEAN_TYPE, type: GraphQL::BOOLEAN_TYPE,
description: "Whether or not a user can perform `#{name}` on this resource", description: "Indicates the user can perform `#{name}` on this resource",
null: false) null: false)
field(**kword_args) # rubocop:disable Graphql/Descriptions field(**kword_args) # rubocop:disable Graphql/Descriptions
......
...@@ -46,7 +46,7 @@ module Types ...@@ -46,7 +46,7 @@ module Types
description: 'Timestamp of the project last activity' description: 'Timestamp of the project last activity'
field :archived, GraphQL::BOOLEAN_TYPE, null: true, field :archived, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Archived status of the project' description: 'Indicates the archived status of the project'
field :visibility, GraphQL::STRING_TYPE, null: true, field :visibility, GraphQL::STRING_TYPE, null: true,
description: 'Visibility of the project' description: 'Visibility of the project'
...@@ -102,6 +102,8 @@ module Types ...@@ -102,6 +102,8 @@ module Types
description: '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' description: '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'
field :remove_source_branch_after_merge, GraphQL::BOOLEAN_TYPE, null: true, field :remove_source_branch_after_merge, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project' description: 'Indicates if `Delete source branch` option should be enabled by default for all new merge requests of the project'
field :autoclose_referenced_issues, GraphQL::BOOLEAN_TYPE, null: true,
description: 'Indicates if issues referenced by merge requests and commits within the default branch are closed automatically'
field :namespace, Types::NamespaceType, null: true, field :namespace, Types::NamespaceType, null: true,
description: 'Namespace of the project' description: 'Namespace of the project'
......
...@@ -89,7 +89,7 @@ module DiffViewer ...@@ -89,7 +89,7 @@ module DiffViewer
{ {
viewer: switcher_title, viewer: switcher_title,
reason: render_error_reason, reason: render_error_reason,
options: render_error_options.to_sentence(two_words_connector: _(' or '), last_word_connector: _(', or ')) options: Gitlab::Utils.to_exclusive_sentence(render_error_options)
} }
end end
......
...@@ -142,13 +142,9 @@ class Key < ApplicationRecord ...@@ -142,13 +142,9 @@ class Key < ApplicationRecord
end end
def forbidden_key_type_message def forbidden_key_type_message
allowed_types = allowed_types = Gitlab::CurrentSettings.allowed_key_types.map(&:upcase)
Gitlab::CurrentSettings
.allowed_key_types
.map(&:upcase)
.to_sentence(last_word_connector: ', or ', two_words_connector: ' or ')
"type is forbidden. Must be #{allowed_types}" "type is forbidden. Must be #{Gitlab::Utils.to_exclusive_sentence(allowed_types)}"
end end
end end
......
...@@ -75,6 +75,7 @@ class Project < ApplicationRecord ...@@ -75,6 +75,7 @@ class Project < ApplicationRecord
default_value_for :snippets_enabled, gitlab_config_features.snippets default_value_for :snippets_enabled, gitlab_config_features.snippets
default_value_for :only_allow_merge_if_all_discussions_are_resolved, false default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
default_value_for :remove_source_branch_after_merge, true default_value_for :remove_source_branch_after_merge, true
default_value_for :autoclose_referenced_issues, true
default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path } default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required } add_authentication_token_field :runners_token, encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption, default_enabled: true) ? :optional : :required }
...@@ -679,6 +680,12 @@ class Project < ApplicationRecord ...@@ -679,6 +680,12 @@ class Project < ApplicationRecord
end end
end end
def autoclose_referenced_issues
return true if super.nil?
super
end
def preload_protected_branches def preload_protected_branches
preloader = ActiveRecord::Associations::Preloader.new preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(self, protected_branches: [:push_access_levels, :merge_access_levels]) preloader.preload(self, protected_branches: [:push_access_levels, :merge_access_levels])
......
...@@ -21,7 +21,8 @@ class KeyRestrictionValidator < ActiveModel::EachValidator ...@@ -21,7 +21,8 @@ class KeyRestrictionValidator < ActiveModel::EachValidator
def supported_sizes_message def supported_sizes_message
sizes = self.class.supported_sizes(options[:type]) sizes = self.class.supported_sizes(options[:type])
sizes.to_sentence(last_word_connector: ', or ', two_words_connector: ' or ')
Gitlab::Utils.to_exclusive_sentence(sizes)
end end
def valid_restriction?(value) def valid_restriction?(value)
......
...@@ -3,5 +3,5 @@ ...@@ -3,5 +3,5 @@
The #{viewer.switcher_title} could not be displayed because #{blob_render_error_reason(viewer)}. The #{viewer.switcher_title} could not be displayed because #{blob_render_error_reason(viewer)}.
You can You can
= blob_render_error_options(viewer).to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe = Gitlab::Utils.to_exclusive_sentence(blob_render_error_options(viewer)).html_safe
instead. instead.
...@@ -4,6 +4,6 @@ After you've reviewed these contribution guidelines, you'll be all set to ...@@ -4,6 +4,6 @@ After you've reviewed these contribution guidelines, you'll be all set to
- options = contribution_options(viewer.project) - options = contribution_options(viewer.project)
- if options.any? - if options.any?
= succeed '.' do = succeed '.' do
= options.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe = Gitlab::Utils.to_exclusive_sentence(options).html_safe
- else - else
contribute to this project. contribute to this project.
...@@ -9,13 +9,23 @@ ...@@ -9,13 +9,23 @@
= _('Select the branch you want to set as the default for this project. All merge requests and commits will automatically be made against this branch unless you specify a different one.') = _('Select the branch you want to set as the default for this project. All merge requests and commits will automatically be made against this branch unless you specify a different one.')
.settings-content .settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, anchor: 'default-branch-settings' }, authenticity_token: true do |f|
%fieldset
- if @project.empty_repo? - if @project.empty_repo?
.text-secondary .text-secondary
= _('A default branch cannot be chosen for an empty project.') = _('A default branch cannot be chosen for an empty project.')
- else - else
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, anchor: 'default-branch-settings' }, authenticity_token: true do |f|
%fieldset
.form-group .form-group
= f.label :default_branch, "Default Branch", class: 'label-bold' = f.label :default_branch, "Default Branch", class: 'label-bold'
= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'}) = f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
.form-group
.form-check
= f.check_box :autoclose_referenced_issues, class: 'form-check-input'
= f.label :autoclose_referenced_issues, class: 'form-check-label' do
%strong= _("Auto-close referenced issues on default branch")
.form-text.text-muted
= _("Issues referenced by merge requests and commits within the default branch will be closed automatically")
= link_to icon('question-circle'), help_page_path('user/project/issues/managing_issues.html', anchor: 'disabling-automatic-issue-closing'), target: '_blank'
= f.submit 'Save changes', class: "btn btn-success" = f.submit 'Save changes', class: "btn btn-success"
- project = local_assigns.fetch(:project) - project = local_assigns.fetch(:project)
- model = local_assigns.fetch(:model) - model = local_assigns.fetch(:model)
- form = local_assigns.fetch(:form) - form = local_assigns.fetch(:form)
- placeholder = model.is_a?(MergeRequest) ? _('Describe the goal of the changes and what reviewers should be aware of.') : _('Write a comment or drag your files here…') - placeholder = model.is_a?(MergeRequest) ? _('Describe the goal of the changes and what reviewers should be aware of.') : _('Write a comment or drag your files here…')
- supports_quick_actions = model.new_record? - supports_quick_actions = model.new_record?
...@@ -14,6 +16,8 @@ ...@@ -14,6 +16,8 @@
= form.label :description, 'Description', class: 'col-form-label col-sm-2' = form.label :description, 'Description', class: 'col-form-label col-sm-2'
.col-sm-10 .col-sm-10
- if model.is_a?(Issuable)
= render 'shared/issuable/form/template_selector', issuable: model
= render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do = render layout: 'projects/md_preview', locals: { url: preview_url, referenced_users: true } do
= render 'projects/zen', f: form, attr: :description, = render 'projects/zen', f: form, attr: :description,
classes: 'note-textarea qa-issuable-form-description rspec-issuable-form-description', classes: 'note-textarea qa-issuable-form-description rspec-issuable-form-description',
......
...@@ -17,7 +17,6 @@ ...@@ -17,7 +17,6 @@
.form-group.row .form-group.row
= form.label :title, class: 'col-form-label col-sm-2' = form.label :title, class: 'col-form-label col-sm-2'
= render 'shared/issuable/form/template_selector', issuable: issuable
= render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?) = render 'shared/issuable/form/title', issuable: issuable, form: form, has_wip_commits: commits && commits.detect(&:work_in_progress?)
#js-suggestions{ data: { project_path: @project.full_path } } #js-suggestions{ data: { project_path: @project.full_path } }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- return unless issuable && issuable_templates(issuable).any? - return unless issuable && issuable_templates(issuable).any?
.col-sm-3.col-lg-2 .issuable-form-select-holder.selectbox.form-group
.js-issuable-selector-wrap{ data: { issuable_type: issuable.to_ability_name } } .js-issuable-selector-wrap{ data: { issuable_type: issuable.to_ability_name } }
= template_dropdown_tag(issuable) do = template_dropdown_tag(issuable) do
%ul.dropdown-footer-list %ul.dropdown-footer-list
......
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
class ChatNotificationWorker class ChatNotificationWorker
include ApplicationWorker include ApplicationWorker
TimeoutExceeded = Class.new(StandardError)
sidekiq_options retry: false
feature_category :chatops feature_category :chatops
latency_sensitive_worker! latency_sensitive_worker!
# TODO: break this into multiple jobs # TODO: break this into multiple jobs
...@@ -11,18 +14,21 @@ class ChatNotificationWorker ...@@ -11,18 +14,21 @@ class ChatNotificationWorker
# worker_has_external_dependencies! # worker_has_external_dependencies!
RESCHEDULE_INTERVAL = 2.seconds RESCHEDULE_INTERVAL = 2.seconds
RESCHEDULE_TIMEOUT = 5.minutes
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def perform(build_id) def perform(build_id, reschedule_count = 0)
Ci::Build.find_by(id: build_id).try do |build| Ci::Build.find_by(id: build_id).try do |build|
send_response(build) send_response(build)
end end
rescue Gitlab::Chat::Output::MissingBuildSectionError rescue Gitlab::Chat::Output::MissingBuildSectionError
raise TimeoutExceeded if timeout_exceeded?(reschedule_count)
# The creation of traces and sections appears to be eventually consistent. # The creation of traces and sections appears to be eventually consistent.
# As a result it's possible for us to run the above code before the trace # As a result it's possible for us to run the above code before the trace
# sections are present. To better handle such cases we'll just reschedule # sections are present. To better handle such cases we'll just reschedule
# the job instead of producing an error. # the job instead of producing an error.
self.class.perform_in(RESCHEDULE_INTERVAL, build_id) self.class.perform_in(RESCHEDULE_INTERVAL, build_id, reschedule_count + 1)
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
...@@ -37,4 +43,10 @@ class ChatNotificationWorker ...@@ -37,4 +43,10 @@ class ChatNotificationWorker
end end
end end
end end
private
def timeout_exceeded?(reschedule_count)
(reschedule_count * RESCHEDULE_INTERVAL) >= RESCHEDULE_TIMEOUT
end
end end
---
title: Limit the amount of time ChatNotificationWorker waits for the build trace
merge_request: 22132
author:
type: fixed
---
title: Changes to template dropdown location
merge_request: 22049
author:
type: changed
---
title: Add capability to disable issue auto-close feature per project
merge_request: 21704
author: Fabio Huser
type: added
# frozen_string_literal: true
class AddAutocloseReferencedIssuesToProjects < ActiveRecord::Migration[5.2]
DOWNTIME = false
def change
add_column :projects, :autoclose_referenced_issues, :boolean
end
end
...@@ -3348,6 +3348,7 @@ ActiveRecord::Schema.define(version: 2020_01_06_085831) do ...@@ -3348,6 +3348,7 @@ ActiveRecord::Schema.define(version: 2020_01_06_085831) do
t.boolean "remove_source_branch_after_merge" t.boolean "remove_source_branch_after_merge"
t.date "marked_for_deletion_at" t.date "marked_for_deletion_at"
t.integer "marked_for_deletion_by_user_id" t.integer "marked_for_deletion_by_user_id"
t.boolean "autoclose_referenced_issues"
t.index "lower((name)::text)", name: "index_projects_on_lower_name" t.index "lower((name)::text)", name: "index_projects_on_lower_name"
t.index ["created_at", "id"], name: "index_projects_on_created_at_and_id" t.index ["created_at", "id"], name: "index_projects_on_created_at_and_id"
t.index ["creator_id"], name: "index_projects_on_creator_id" t.index ["creator_id"], name: "index_projects_on_creator_id"
......
...@@ -318,6 +318,40 @@ Slots where `active` is `f` are not active. ...@@ -318,6 +318,40 @@ Slots where `active` is `f` are not active.
SELECT pg_drop_replication_slot('<name_of_extra_slot>'); SELECT pg_drop_replication_slot('<name_of_extra_slot>');
``` ```
### Message: "ERROR: canceling statement due to conflict with recovery"
This error may rarely occur under normal usage, and the system is resilient
enough to recover.
However, under certain conditions, some database queries on secondaries may run
excessively long, which increases the frequency of this error. At some point,
some of these queries will never be able to complete due to being canceled
every time.
These long-running queries are
[planned to be removed in the future](https://gitlab.com/gitlab-org/gitlab/issues/34269),
but as a workaround, we recommend enabling
[hot_standby_feedback](https://www.postgresql.org/docs/10/hot-standby.html#HOT-STANDBY-CONFLICT).
This increases the likelihood of bloat on the **primary** node as it prevents
`VACUUM` from removing recently-dead rows. However, it has been used
successfully in production on GitLab.com.
To enable `hot_standby_feedback`, add the following to `/etc/gitlab/gitlab.rb`
on the **secondary** node:
```ruby
postgresql['hot_standby_feedback'] = 'on'
```
Then reconfigure GitLab:
```sh
sudo gitlab-ctl reconfigure
```
To help us resolve this problem, consider commenting on
[the issue](https://gitlab.com/gitlab-org/gitlab/issues/4489).
### Very large repositories never successfully synchronize on the **secondary** node ### Very large repositories never successfully synchronize on the **secondary** node
GitLab places a timeout on all repository clones, including project imports GitLab places a timeout on all repository clones, including project imports
......
This diff is collapsed.
...@@ -156,6 +156,7 @@ When the user is authenticated and `simple` is not set this returns something li ...@@ -156,6 +156,7 @@ When the user is authenticated and `simple` is not set this returns something li
"remove_source_branch_after_merge": false, "remove_source_branch_after_merge": false,
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true,
"statistics": { "statistics": {
"commit_count": 37, "commit_count": 37,
"storage_size": 1038090, "storage_size": 1038090,
...@@ -254,6 +255,7 @@ When the user is authenticated and `simple` is not set this returns something li ...@@ -254,6 +255,7 @@ When the user is authenticated and `simple` is not set this returns something li
"packages_enabled": true, "packages_enabled": true,
"service_desk_enabled": false, "service_desk_enabled": false,
"service_desk_address": null, "service_desk_address": null,
"autoclose_referenced_issues": true,
"statistics": { "statistics": {
"commit_count": 12, "commit_count": 12,
"storage_size": 2066080, "storage_size": 2066080,
...@@ -385,6 +387,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo ...@@ -385,6 +387,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo
"remove_source_branch_after_merge": false, "remove_source_branch_after_merge": false,
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true,
"statistics": { "statistics": {
"commit_count": 37, "commit_count": 37,
"storage_size": 1038090, "storage_size": 1038090,
...@@ -483,6 +486,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo ...@@ -483,6 +486,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo
"packages_enabled": true, "packages_enabled": true,
"service_desk_enabled": false, "service_desk_enabled": false,
"service_desk_address": null, "service_desk_address": null,
"autoclose_referenced_issues": true,
"statistics": { "statistics": {
"commit_count": 12, "commit_count": 12,
"storage_size": 2066080, "storage_size": 2066080,
...@@ -593,6 +597,7 @@ Example response: ...@@ -593,6 +597,7 @@ Example response:
"remove_source_branch_after_merge": false, "remove_source_branch_after_merge": false,
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true,
"statistics": { "statistics": {
"commit_count": 37, "commit_count": 37,
"storage_size": 1038090, "storage_size": 1038090,
...@@ -688,6 +693,7 @@ Example response: ...@@ -688,6 +693,7 @@ Example response:
"packages_enabled": true, "packages_enabled": true,
"service_desk_enabled": false, "service_desk_enabled": false,
"service_desk_address": null, "service_desk_address": null,
"autoclose_referenced_issues": true,
"statistics": { "statistics": {
"commit_count": 12, "commit_count": 12,
"storage_size": 2066080, "storage_size": 2066080,
...@@ -829,6 +835,7 @@ GET /projects/:id ...@@ -829,6 +835,7 @@ GET /projects/:id
"packages_enabled": true, "packages_enabled": true,
"service_desk_enabled": false, "service_desk_enabled": false,
"service_desk_address": null, "service_desk_address": null,
"autoclose_referenced_issues": true,
"statistics": { "statistics": {
"commit_count": 37, "commit_count": 37,
"storage_size": 1038090, "storage_size": 1038090,
...@@ -986,6 +993,7 @@ POST /projects ...@@ -986,6 +993,7 @@ POST /projects
| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs | | `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `merge_method` | string | no | Set the [merge method](#project-merge-method) used | | `merge_method` | string | no | Set the [merge method](#project-merge-method) used |
| `autoclose_referenced_issues` | boolean | no | Set whether auto-closing referenced issues on default branch |
| `remove_source_branch_after_merge` | boolean | no | Enable `Delete source branch` option by default for all new merge requests | | `remove_source_branch_after_merge` | boolean | no | Enable `Delete source branch` option by default for all new merge requests |
| `lfs_enabled` | boolean | no | Enable LFS | | `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access | | `request_access_enabled` | boolean | no | Allow users to request member access |
...@@ -1050,6 +1058,7 @@ POST /projects/user/:user_id ...@@ -1050,6 +1058,7 @@ POST /projects/user/:user_id
| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs | | `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `merge_method` | string | no | Set the [merge method](#project-merge-method) used | | `merge_method` | string | no | Set the [merge method](#project-merge-method) used |
| `autoclose_referenced_issues` | boolean | no | Set whether auto-closing referenced issues on default branch |
| `remove_source_branch_after_merge` | boolean | no | Enable `Delete source branch` option by default for all new merge requests | | `remove_source_branch_after_merge` | boolean | no | Enable `Delete source branch` option by default for all new merge requests |
| `lfs_enabled` | boolean | no | Enable LFS | | `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access | | `request_access_enabled` | boolean | no | Allow users to request member access |
...@@ -1113,6 +1122,7 @@ PUT /projects/:id ...@@ -1113,6 +1122,7 @@ PUT /projects/:id
| `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs | | `only_allow_merge_if_pipeline_succeeds` | boolean | no | Set whether merge requests can only be merged with successful jobs |
| `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved | | `only_allow_merge_if_all_discussions_are_resolved` | boolean | no | Set whether merge requests can only be merged when all the discussions are resolved |
| `merge_method` | string | no | Set the [merge method](#project-merge-method) used | | `merge_method` | string | no | Set the [merge method](#project-merge-method) used |
| `autoclose_referenced_issues` | boolean | no | Set whether auto-closing referenced issues on default branch |
| `remove_source_branch_after_merge` | boolean | no | Enable `Delete source branch` option by default for all new merge requests | | `remove_source_branch_after_merge` | boolean | no | Enable `Delete source branch` option by default for all new merge requests |
| `lfs_enabled` | boolean | no | Enable LFS | | `lfs_enabled` | boolean | no | Enable LFS |
| `request_access_enabled` | boolean | no | Allow users to request member access | | `request_access_enabled` | boolean | no | Allow users to request member access |
...@@ -1244,6 +1254,7 @@ Example responses: ...@@ -1244,6 +1254,7 @@ Example responses:
"remove_source_branch_after_merge": false, "remove_source_branch_after_merge": false,
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true,
"_links": { "_links": {
"self": "http://example.com/api/v4/projects", "self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues", "issues": "http://example.com/api/v4/projects/1/issues",
...@@ -1332,6 +1343,7 @@ Example response: ...@@ -1332,6 +1343,7 @@ Example response:
"remove_source_branch_after_merge": false, "remove_source_branch_after_merge": false,
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true,
"_links": { "_links": {
"self": "http://example.com/api/v4/projects", "self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues", "issues": "http://example.com/api/v4/projects/1/issues",
...@@ -1419,6 +1431,7 @@ Example response: ...@@ -1419,6 +1431,7 @@ Example response:
"remove_source_branch_after_merge": false, "remove_source_branch_after_merge": false,
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true,
"_links": { "_links": {
"self": "http://example.com/api/v4/projects", "self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues", "issues": "http://example.com/api/v4/projects/1/issues",
...@@ -1593,6 +1606,7 @@ Example response: ...@@ -1593,6 +1606,7 @@ Example response:
"remove_source_branch_after_merge": false, "remove_source_branch_after_merge": false,
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true,
"_links": { "_links": {
"self": "http://example.com/api/v4/projects", "self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues", "issues": "http://example.com/api/v4/projects/1/issues",
...@@ -1699,6 +1713,7 @@ Example response: ...@@ -1699,6 +1713,7 @@ Example response:
"remove_source_branch_after_merge": false, "remove_source_branch_after_merge": false,
"request_access_enabled": false, "request_access_enabled": false,
"merge_method": "merge", "merge_method": "merge",
"autoclose_referenced_issues": true,
"_links": { "_links": {
"self": "http://example.com/api/v4/projects", "self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues", "issues": "http://example.com/api/v4/projects/1/issues",
......
...@@ -210,7 +210,7 @@ class MergeRequestPermissionsType < BasePermissionType ...@@ -210,7 +210,7 @@ class MergeRequestPermissionsType < BasePermissionType
abilities :admin_merge_request, :update_merge_request, :create_note abilities :admin_merge_request, :update_merge_request, :create_note
ability_field :resolve_note, ability_field :resolve_note,
description: 'Whether or not the user can resolve disussions on the merge request' description: 'Indicates the user can resolve discussions on the merge request'
permission_field :push_to_source_branch, method: :can_push_to_source_branch? permission_field :push_to_source_branch, method: :can_push_to_source_branch?
end end
``` ```
......
...@@ -70,6 +70,7 @@ When using spring and guard together, use `SPRING=1 bundle exec guard` instead t ...@@ -70,6 +70,7 @@ When using spring and guard together, use `SPRING=1 bundle exec guard` instead t
use a Capyabara matcher beforehand (e.g. `find('.js-foo')`) to ensure the element actually exists. use a Capyabara matcher beforehand (e.g. `find('.js-foo')`) to ensure the element actually exists.
- Use `focus: true` to isolate parts of the specs you want to run. - Use `focus: true` to isolate parts of the specs you want to run.
- Use [`:aggregate_failures`](https://relishapp.com/rspec/rspec-core/docs/expectation-framework-integration/aggregating-failures) when there is more than one expectation in a test. - Use [`:aggregate_failures`](https://relishapp.com/rspec/rspec-core/docs/expectation-framework-integration/aggregating-failures) when there is more than one expectation in a test.
- For [empty test description blocks](https://github.com/rubocop-hq/rspec-style-guide#it-and-specify), use `specify` rather than `it do` if the test is self-explanatory.
### System / Feature tests ### System / Feature tests
......
...@@ -211,6 +211,19 @@ as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it do ...@@ -211,6 +211,19 @@ as well as `#22` and `#23` in group/otherproject. `#17` won't be closed as it do
not match the pattern. It works with multi-line commit messages as well as one-liners not match the pattern. It works with multi-line commit messages as well as one-liners
when used from the command line with `git commit -m`. when used from the command line with `git commit -m`.
#### Disabling automatic issue closing
The automatic issue closing feature can be disabled on a per-project basis
within the [project's repository settings](../settings/index.md). Referenced
issues will still be displayed as such but won't be closed automatically.
![disable issue auto close - settings](img/disable_issue_auto_close.png)
This only applies to issues affected by new merge requests or commits. Already
closed issues remain as-is. Disabling automatic issue closing only affects merge
requests *within* the project and won't prevent other projects from closing it
via cross-project issues.
#### Customizing the issue closing pattern **(CORE ONLY)** #### Customizing the issue closing pattern **(CORE ONLY)**
In order to change the default issue closing pattern, GitLab administrators must edit the In order to change the default issue closing pattern, GitLab administrators must edit the
......
...@@ -331,6 +331,7 @@ module API ...@@ -331,6 +331,7 @@ module API
expose :auto_devops_deploy_strategy do |project, options| expose :auto_devops_deploy_strategy do |project, options|
project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy
end end
expose :autoclose_referenced_issues
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def self.preload_relation(projects_relation, options = {}) def self.preload_relation(projects_relation, options = {})
......
...@@ -47,6 +47,7 @@ module API ...@@ -47,6 +47,7 @@ module API
optional :ci_default_git_depth, type: Integer, desc: 'Default number of revisions for shallow cloning' optional :ci_default_git_depth, type: Integer, desc: 'Default number of revisions for shallow cloning'
optional :auto_devops_enabled, type: Boolean, desc: 'Flag indication if Auto DevOps is enabled' optional :auto_devops_enabled, type: Boolean, desc: 'Flag indication if Auto DevOps is enabled'
optional :auto_devops_deploy_strategy, type: String, values: %w(continuous manual timed_incremental), desc: 'Auto Deploy strategy' optional :auto_devops_deploy_strategy, type: String, values: %w(continuous manual timed_incremental), desc: 'Auto Deploy strategy'
optional :autoclose_referenced_issues, type: Boolean, desc: 'Flag indication if referenced issues auto-closing is enabled'
end end
params :optional_project_params_ee do params :optional_project_params_ee do
...@@ -85,6 +86,7 @@ module API ...@@ -85,6 +86,7 @@ module API
:container_registry_enabled, :container_registry_enabled,
:default_branch, :default_branch,
:description, :description,
:autoclose_referenced_issues,
:issues_access_level, :issues_access_level,
:lfs_enabled, :lfs_enabled,
:merge_requests_access_level, :merge_requests_access_level,
......
...@@ -11,11 +11,13 @@ module Gitlab ...@@ -11,11 +11,13 @@ module Gitlab
end end
def initialize(project, current_user = nil) def initialize(project, current_user = nil)
@project = project
@extractor = Gitlab::ReferenceExtractor.new(project, current_user) @extractor = Gitlab::ReferenceExtractor.new(project, current_user)
end end
def closed_by_message(message) def closed_by_message(message)
return [] if message.nil? return [] if message.nil?
return [] unless @project.autoclose_referenced_issues
closing_statements = [] closing_statements = []
message.scan(ISSUE_CLOSING_REGEX) do message.scan(ISSUE_CLOSING_REGEX) do
......
...@@ -50,6 +50,12 @@ module Gitlab ...@@ -50,6 +50,12 @@ module Gitlab
.gsub(/(\A-+|-+\z)/, '') .gsub(/(\A-+|-+\z)/, '')
end end
# Wraps ActiveSupport's Array#to_sentence to convert the given array to a
# comma-separated sentence joined with localized 'or' Strings instead of 'and'.
def to_exclusive_sentence(array)
array.to_sentence(two_words_connector: _(' or '), last_word_connector: _(', or '))
end
# Converts newlines into HTML line break elements # Converts newlines into HTML line break elements
def nlbr(str) def nlbr(str)
ActionView::Base.full_sanitizer.sanitize(+str, tags: []).gsub(/\r?\n/, '<br>').html_safe ActionView::Base.full_sanitizer.sanitize(+str, tags: []).gsub(/\r?\n/, '<br>').html_safe
......
...@@ -2381,6 +2381,9 @@ msgstr "" ...@@ -2381,6 +2381,9 @@ msgstr ""
msgid "Auto-cancel redundant, pending pipelines" msgid "Auto-cancel redundant, pending pipelines"
msgstr "" msgstr ""
msgid "Auto-close referenced issues on default branch"
msgstr ""
msgid "AutoDevOps|Auto DevOps" msgid "AutoDevOps|Auto DevOps"
msgstr "" msgstr ""
...@@ -10077,6 +10080,9 @@ msgstr "" ...@@ -10077,6 +10080,9 @@ msgstr ""
msgid "Issues closed" msgid "Issues closed"
msgstr "" msgstr ""
msgid "Issues referenced by merge requests and commits within the default branch will be closed automatically"
msgstr ""
msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities" msgid "Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities"
msgstr "" msgstr ""
...@@ -21231,6 +21237,9 @@ msgstr "" ...@@ -21231,6 +21237,9 @@ msgstr ""
msgid "already being used for another group or project milestone." msgid "already being used for another group or project milestone."
msgstr "" msgstr ""
msgid "already has a \"created\" issue link"
msgstr ""
msgid "already shared with this group" msgid "already shared with this group"
msgstr "" msgstr ""
...@@ -21688,6 +21697,9 @@ msgstr "" ...@@ -21688,6 +21697,9 @@ msgstr ""
msgid "group" msgid "group"
msgstr "" msgstr ""
msgid "has already been linked to another vulnerability"
msgstr ""
msgid "has already been taken" msgid "has already been taken"
msgstr "" msgstr ""
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { selectedMachineTypeMock, gapiMachineTypesResponseMock } from '../mock_data';
import createState from '~/create_cluster/gke_cluster/store/state';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import GkeMachineTypeDropdown from '~/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue';
const componentConfig = {
fieldId: 'cluster_provider_gcp_attributes_gcp_machine_type',
fieldName: 'cluster[provider_gcp_attributes][gcp_machine_type]',
};
const setMachineType = jest.fn();
const LABELS = {
LOADING: 'Fetching machine types',
DISABLED_NO_PROJECT: 'Select project and zone to choose machine type',
DISABLED_NO_ZONE: 'Select zone to choose machine type',
DEFAULT: 'Select machine type',
};
const localVue = createLocalVue();
localVue.use(Vuex);
const createComponent = (store, propsData = componentConfig) =>
shallowMount(GkeMachineTypeDropdown, {
propsData,
store,
localVue,
sync: false,
});
const createStore = (initialState = {}, getters = {}) =>
new Vuex.Store({
state: {
...createState(),
...initialState,
},
getters: {
hasZone: () => false,
...getters,
},
actions: {
setMachineType,
},
});
describe('GkeMachineTypeDropdown', () => {
let wrapper;
let store;
afterEach(() => {
wrapper.destroy();
});
const dropdownButtonLabel = () => wrapper.find(DropdownButton).props('toggleText');
const dropdownHiddenInputValue = () => wrapper.find(DropdownHiddenInput).props('value');
describe('shows various toggle text depending on state', () => {
it('returns disabled state toggle text when no project and zone are selected', () => {
store = createStore({
projectHasBillingEnabled: false,
});
wrapper = createComponent(store);
expect(dropdownButtonLabel()).toBe(LABELS.DISABLED_NO_PROJECT);
});
it('returns disabled state toggle text when no zone is selected', () => {
store = createStore({
projectHasBillingEnabled: true,
});
wrapper = createComponent(store);
expect(dropdownButtonLabel()).toBe(LABELS.DISABLED_NO_ZONE);
});
it('returns loading toggle text', () => {
store = createStore();
wrapper = createComponent(store);
wrapper.setData({ isLoading: true });
return wrapper.vm.$nextTick().then(() => {
expect(dropdownButtonLabel()).toBe(LABELS.LOADING);
});
});
it('returns default toggle text', () => {
store = createStore(
{
projectHasBillingEnabled: true,
},
{ hasZone: () => true },
);
wrapper = createComponent(store);
expect(dropdownButtonLabel()).toBe(LABELS.DEFAULT);
});
it('returns machine type name if machine type selected', () => {
store = createStore(
{
projectHasBillingEnabled: true,
selectedMachineType: selectedMachineTypeMock,
},
{ hasZone: () => true },
);
wrapper = createComponent(store);
expect(dropdownButtonLabel()).toBe(selectedMachineTypeMock);
});
});
describe('form input', () => {
it('reflects new value when dropdown item is clicked', () => {
store = createStore({
machineTypes: gapiMachineTypesResponseMock.items,
});
wrapper = createComponent(store);
expect(dropdownHiddenInputValue()).toBe('');
wrapper.find('.dropdown-content button').trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(setMachineType).toHaveBeenCalledWith(
expect.anything(),
selectedMachineTypeMock,
undefined,
);
});
});
});
});
export const emptyProjectMock = {
projectId: '',
name: '',
};
export const selectedProjectMock = {
projectId: 'gcp-project-123',
name: 'gcp-project',
};
export const selectedZoneMock = 'us-central1-a';
export const selectedMachineTypeMock = 'n1-standard-2';
export const gapiProjectsResponseMock = {
projects: [
{
projectNumber: '1234',
projectId: 'gcp-project-123',
lifecycleState: 'ACTIVE',
name: 'gcp-project',
createTime: '2017-12-16T01:48:29.129Z',
parent: {
type: 'organization',
id: '12345',
},
},
],
};
export const gapiZonesResponseMock = {
kind: 'compute#zoneList',
id: 'projects/gitlab-internal-153318/zones',
items: [
{
kind: 'compute#zone',
id: '2000',
creationTimestamp: '1969-12-31T16:00:00.000-08:00',
name: 'us-central1-a',
description: 'us-central1-a',
status: 'UP',
region:
'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/regions/us-central1',
selfLink:
'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a',
availableCpuPlatforms: ['Intel Skylake', 'Intel Broadwell', 'Intel Sandy Bridge'],
},
],
selfLink: 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones',
};
export const gapiMachineTypesResponseMock = {
kind: 'compute#machineTypeList',
id: 'projects/gitlab-internal-153318/zones/us-central1-a/machineTypes',
items: [
{
kind: 'compute#machineType',
id: '3002',
creationTimestamp: '1969-12-31T16:00:00.000-08:00',
name: 'n1-standard-2',
description: '2 vCPUs, 7.5 GB RAM',
guestCpus: 2,
memoryMb: 7680,
imageSpaceGb: 10,
maximumPersistentDisks: 64,
maximumPersistentDisksSizeGb: '65536',
zone: 'us-central1-a',
selfLink:
'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a/machineTypes/n1-standard-2',
isSharedCpu: false,
},
],
selfLink:
'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a/machineTypes',
};
...@@ -23,7 +23,7 @@ describe GitlabSchema.types['Project'] do ...@@ -23,7 +23,7 @@ describe GitlabSchema.types['Project'] do
only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled only_allow_merge_if_all_discussions_are_resolved printing_merge_request_link_enabled
namespace group statistics repository merge_requests merge_request issues namespace group statistics repository merge_requests merge_request issues
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
grafanaIntegration grafanaIntegration autocloseReferencedIssues
] ]
is_expected.to include_graphql_fields(*expected_fields) is_expected.to include_graphql_fields(*expected_fields)
......
import Vue from 'vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import GkeMachineTypeDropdown from '~/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue';
import { createStore } from '~/create_cluster/gke_cluster/store';
import {
SET_PROJECT,
SET_PROJECT_BILLING_STATUS,
SET_ZONE,
SET_MACHINE_TYPES,
} from '~/create_cluster/gke_cluster/store/mutation_types';
import {
selectedZoneMock,
selectedProjectMock,
selectedMachineTypeMock,
gapiMachineTypesResponseMock,
} from '../mock_data';
const componentConfig = {
fieldId: 'cluster_provider_gcp_attributes_gcp_machine_type',
fieldName: 'cluster[provider_gcp_attributes][gcp_machine_type]',
};
const LABELS = {
LOADING: 'Fetching machine types',
DISABLED_NO_PROJECT: 'Select project and zone to choose machine type',
DISABLED_NO_ZONE: 'Select zone to choose machine type',
DEFAULT: 'Select machine type',
};
const createComponent = (store, props = componentConfig) => {
const Component = Vue.extend(GkeMachineTypeDropdown);
return mountComponentWithStore(Component, {
el: null,
props,
store,
});
};
describe('GkeMachineTypeDropdown', () => {
let vm;
let store;
beforeEach(() => {
store = createStore();
vm = createComponent(store);
});
afterEach(() => {
vm.$destroy();
});
describe('shows various toggle text depending on state', () => {
it('returns disabled state toggle text when no project and zone are selected', () => {
expect(vm.toggleText).toBe(LABELS.DISABLED_NO_PROJECT);
});
it('returns disabled state toggle text when no zone is selected', () => {
vm.$store.commit(SET_PROJECT, selectedProjectMock);
vm.$store.commit(SET_PROJECT_BILLING_STATUS, true);
expect(vm.toggleText).toBe(LABELS.DISABLED_NO_ZONE);
});
it('returns loading toggle text', () => {
vm.isLoading = true;
expect(vm.toggleText).toBe(LABELS.LOADING);
});
it('returns default toggle text', () => {
expect(vm.toggleText).toBe(LABELS.DISABLED_NO_PROJECT);
vm.$store.commit(SET_PROJECT, selectedProjectMock);
vm.$store.commit(SET_PROJECT_BILLING_STATUS, true);
vm.$store.commit(SET_ZONE, selectedZoneMock);
expect(vm.toggleText).toBe(LABELS.DEFAULT);
});
it('returns machine type name if machine type selected', () => {
vm.setItem(selectedMachineTypeMock);
expect(vm.toggleText).toBe(selectedMachineTypeMock);
});
});
describe('form input', () => {
it('reflects new value when dropdown item is clicked', done => {
expect(vm.$el.querySelector('input').value).toBe('');
vm.$store.commit(SET_MACHINE_TYPES, gapiMachineTypesResponseMock.items);
return vm
.$nextTick()
.then(() => {
vm.$el.querySelector('.dropdown-content button').click();
return vm
.$nextTick()
.then(() => {
expect(vm.$el.querySelector('input').value).toBe(selectedMachineTypeMock);
done();
})
.catch(done.fail);
})
.catch(done.fail);
});
});
});
...@@ -438,6 +438,17 @@ describe Gitlab::ClosingIssueExtractor do ...@@ -438,6 +438,17 @@ describe Gitlab::ClosingIssueExtractor do
.to match_array([issue]) .to match_array([issue])
end end
end end
context "with autoclose referenced issues disabled" do
before do
project.update!(autoclose_referenced_issues: false)
end
it do
message = "Awesome commit (Closes #{reference})"
expect(subject.closed_by_message(message)).to eq([])
end
end
end end
def urls def urls
......
...@@ -463,6 +463,7 @@ project: ...@@ -463,6 +463,7 @@ project:
- import_failures - import_failures
- container_expiration_policy - container_expiration_policy
- resource_groups - resource_groups
- autoclose_referenced_issues
award_emoji: award_emoji:
- awardable - awardable
- user - user
......
...@@ -534,6 +534,7 @@ Project: ...@@ -534,6 +534,7 @@ Project:
- pages_https_only - pages_https_only
- merge_requests_disable_committers_approval - merge_requests_disable_committers_approval
- require_password_to_approve - require_password_to_approve
- autoclose_referenced_issues
ProjectTracingSetting: ProjectTracingSetting:
- external_url - external_url
Author: Author:
......
...@@ -3,8 +3,9 @@ ...@@ -3,8 +3,9 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::Utils do describe Gitlab::Utils do
delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which, :ensure_array_from_string, delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which,
:bytes_to_megabytes, :append_path, :check_path_traversal!, to: :described_class :ensure_array_from_string, :to_exclusive_sentence, :bytes_to_megabytes,
:append_path, :check_path_traversal!, to: :described_class
describe '.check_path_traversal!' do describe '.check_path_traversal!' do
it 'detects path traversal at the start of the string' do it 'detects path traversal at the start of the string' do
...@@ -46,6 +47,36 @@ describe Gitlab::Utils do ...@@ -46,6 +47,36 @@ describe Gitlab::Utils do
end end
end end
describe '.to_exclusive_sentence' do
it 'calls #to_sentence on the array' do
array = double
expect(array).to receive(:to_sentence)
to_exclusive_sentence(array)
end
it 'joins arrays with two elements correctly' do
array = %w(foo bar)
expect(to_exclusive_sentence(array)).to eq('foo or bar')
end
it 'joins arrays with more than two elements correctly' do
array = %w(foo bar baz)
expect(to_exclusive_sentence(array)).to eq('foo, bar, or baz')
end
it 'localizes the connector words' do
array = %w(foo bar baz)
expect(described_class).to receive(:_).with(' or ').and_return(' <1> ')
expect(described_class).to receive(:_).with(', or ').and_return(', <2> ')
expect(to_exclusive_sentence(array)).to eq('foo, bar, <2> baz')
end
end
describe '.nlbr' do describe '.nlbr' do
it 'replaces new lines with <br>' do it 'replaces new lines with <br>' do
expect(described_class.nlbr("<b>hello</b>\n<i>world</i>".freeze)).to eq("hello<br>world") expect(described_class.nlbr("<b>hello</b>\n<i>world</i>".freeze)).to eq("hello<br>world")
......
...@@ -390,6 +390,17 @@ eos ...@@ -390,6 +390,17 @@ eos
expect(commit.closes_issues).to include(issue) expect(commit.closes_issues).to include(issue)
expect(commit.closes_issues).to include(other_issue) expect(commit.closes_issues).to include(other_issue)
end end
it 'ignores referenced issues when auto-close is disabled' do
project.update!(autoclose_referenced_issues: false)
allow(commit).to receive_messages(
safe_message: "Fixes ##{issue.iid}",
committer_email: committer.email
)
expect(commit.closes_issues).to be_empty
end
end end
it_behaves_like 'a mentionable' do it_behaves_like 'a mentionable' do
......
...@@ -963,6 +963,15 @@ describe MergeRequest do ...@@ -963,6 +963,15 @@ describe MergeRequest do
expect(subject.closes_issues).to be_empty expect(subject.closes_issues).to be_empty
end end
it 'ignores referenced issues when auto-close is disabled' do
subject.project.update!(autoclose_referenced_issues: false)
allow(subject.project).to receive(:default_branch)
.and_return(subject.target_branch)
expect(subject.closes_issues).to be_empty
end
end end
describe '#issues_mentioned_but_not_closing' do describe '#issues_mentioned_but_not_closing' do
......
...@@ -474,6 +474,32 @@ describe Project do ...@@ -474,6 +474,32 @@ describe Project do
end end
end end
describe '#autoclose_referenced_issues' do
context 'when DB entry is nil' do
let(:project) { create(:project, autoclose_referenced_issues: nil) }
it 'returns true' do
expect(project.autoclose_referenced_issues).to be_truthy
end
end
context 'when DB entry is true' do
let(:project) { create(:project, autoclose_referenced_issues: true) }
it 'returns true' do
expect(project.autoclose_referenced_issues).to be_truthy
end
end
context 'when DB entry is false' do
let(:project) { create(:project, autoclose_referenced_issues: false) }
it 'returns false' do
expect(project.autoclose_referenced_issues).to be_falsey
end
end
end
describe 'project token' do describe 'project token' do
it 'sets an random token if none provided' do it 'sets an random token if none provided' do
project = FactoryBot.create(:project, runners_token: '') project = FactoryBot.create(:project, runners_token: '')
......
...@@ -635,6 +635,7 @@ describe API::Projects do ...@@ -635,6 +635,7 @@ describe API::Projects do
wiki_enabled: false, wiki_enabled: false,
resolve_outdated_diff_discussions: false, resolve_outdated_diff_discussions: false,
remove_source_branch_after_merge: true, remove_source_branch_after_merge: true,
autoclose_referenced_issues: true,
only_allow_merge_if_pipeline_succeeds: false, only_allow_merge_if_pipeline_succeeds: false,
request_access_enabled: true, request_access_enabled: true,
only_allow_merge_if_all_discussions_are_resolved: false, only_allow_merge_if_all_discussions_are_resolved: false,
...@@ -807,6 +808,22 @@ describe API::Projects do ...@@ -807,6 +808,22 @@ describe API::Projects do
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
end end
it 'sets a project as enabling auto close referenced issues' do
project = attributes_for(:project, autoclose_referenced_issues: true)
post api('/projects', user), params: project
expect(json_response['autoclose_referenced_issues']).to be_truthy
end
it 'sets a project as disabling auto close referenced issues' do
project = attributes_for(:project, autoclose_referenced_issues: false)
post api('/projects', user), params: project
expect(json_response['autoclose_referenced_issues']).to be_falsey
end
it 'sets the merge method of a project to rebase merge' do it 'sets the merge method of a project to rebase merge' do
project = attributes_for(:project, merge_method: 'rebase_merge') project = attributes_for(:project, merge_method: 'rebase_merge')
......
...@@ -8,6 +8,10 @@ describe ChatNotificationWorker do ...@@ -8,6 +8,10 @@ describe ChatNotificationWorker do
create(:ci_build, pipeline: create(:ci_pipeline, source: :chat)) create(:ci_build, pipeline: create(:ci_pipeline, source: :chat))
end end
it 'instructs sidekiq not to retry on failure' do
expect(described_class.get_sidekiq_options['retry']).to eq(false)
end
describe '#perform' do describe '#perform' do
it 'does nothing when the build no longer exists' do it 'does nothing when the build no longer exists' do
expect(worker).not_to receive(:send_response) expect(worker).not_to receive(:send_response)
...@@ -23,16 +27,31 @@ describe ChatNotificationWorker do ...@@ -23,16 +27,31 @@ describe ChatNotificationWorker do
worker.perform(chat_build.id) worker.perform(chat_build.id)
end end
it 'reschedules the job if the trace sections could not be found' do context 'when the trace sections could not be found' do
it 'reschedules the job' do
expect(worker) expect(worker)
.to receive(:send_response) .to receive(:send_response)
.and_raise(Gitlab::Chat::Output::MissingBuildSectionError) .and_raise(Gitlab::Chat::Output::MissingBuildSectionError)
expect(described_class) expect(described_class)
.to receive(:perform_in) .to receive(:perform_in)
.with(described_class::RESCHEDULE_INTERVAL, chat_build.id) .with(described_class::RESCHEDULE_INTERVAL, chat_build.id, 1)
worker.perform(chat_build.id)
end
it "raises an error after #{described_class::RESCHEDULE_TIMEOUT} seconds of retrying" do
allow(described_class).to receive(:new).and_return(worker)
allow(worker).to receive(:send_response).and_raise(Gitlab::Chat::Output::MissingBuildSectionError)
worker.perform(chat_build.id) worker.perform(chat_build.id)
expect { described_class.drain }.to raise_error(described_class::TimeoutExceeded)
max_reschedules = described_class::RESCHEDULE_TIMEOUT / described_class::RESCHEDULE_INTERVAL
expect(worker).to have_received(:send_response).exactly(max_reschedules + 1).times
end
end end
end end
......
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