提交 2b3bfe8f 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 d203316c
......@@ -386,6 +386,7 @@ class ProjectsController < Projects::ApplicationController
:template_project_id,
:merge_method,
:initialize_with_readme,
:autoclose_referenced_issues,
project_feature_attributes: %i[
builds_access_level
......
......@@ -5,10 +5,9 @@ module Mutations
class Toggle < Base
graphql_name 'ToggleAwardEmoji'
field :toggledOn,
GraphQL::BOOLEAN_TYPE,
null: false,
description: 'True when the emoji was awarded, false when it was removed'
field :toggledOn, GraphQL::BOOLEAN_TYPE, null: false,
description: 'Indicates the status of the emoji. ' \
'True if the toggle awarded the emoji, and false if the toggle removed the emoji.'
def resolve(args)
awardable = authorized_find!(id: args[:awardable_id])
......
......@@ -69,7 +69,7 @@ module Types
field :participants, Types::UserType.connection_type, null: true, complexity: 5,
description: 'List of participants in the issue'
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,
description: 'Time estimate of the issue'
field :total_time_spent, GraphQL::INT_TYPE, null: false,
......
......@@ -25,7 +25,7 @@ module Types
kword_args = kword_args.reverse_merge(
name: name,
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)
field(**kword_args) # rubocop:disable Graphql/Descriptions
......
......@@ -46,7 +46,7 @@ module Types
description: 'Timestamp of the project last activity'
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,
description: 'Visibility of the project'
......@@ -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'
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'
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,
description: 'Namespace of the project'
......
......@@ -89,7 +89,7 @@ module DiffViewer
{
viewer: switcher_title,
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
......
......@@ -142,13 +142,9 @@ class Key < ApplicationRecord
end
def forbidden_key_type_message
allowed_types =
Gitlab::CurrentSettings
.allowed_key_types
.map(&:upcase)
.to_sentence(last_word_connector: ', or ', two_words_connector: ' or ')
allowed_types = Gitlab::CurrentSettings.allowed_key_types.map(&:upcase)
"type is forbidden. Must be #{allowed_types}"
"type is forbidden. Must be #{Gitlab::Utils.to_exclusive_sentence(allowed_types)}"
end
end
......
......@@ -75,6 +75,7 @@ class Project < ApplicationRecord
default_value_for :snippets_enabled, gitlab_config_features.snippets
default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
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 }
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
end
end
def autoclose_referenced_issues
return true if super.nil?
super
end
def preload_protected_branches
preloader = ActiveRecord::Associations::Preloader.new
preloader.preload(self, protected_branches: [:push_access_levels, :merge_access_levels])
......
......@@ -21,7 +21,8 @@ class KeyRestrictionValidator < ActiveModel::EachValidator
def supported_sizes_message
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
def valid_restriction?(value)
......
......@@ -3,5 +3,5 @@
The #{viewer.switcher_title} could not be displayed because #{blob_render_error_reason(viewer)}.
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.
......@@ -4,6 +4,6 @@ After you've reviewed these contribution guidelines, you'll be all set to
- options = contribution_options(viewer.project)
- if options.any?
= succeed '.' do
= options.to_sentence(two_words_connector: ' or ', last_word_connector: ', or ').html_safe
= Gitlab::Utils.to_exclusive_sentence(options).html_safe
- else
contribute to this project.
......@@ -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.')
.settings-content
- if @project.empty_repo?
.text-secondary
= _('A default branch cannot be chosen for an empty project.')
- else
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, anchor: 'default-branch-settings' }, authenticity_token: true do |f|
%fieldset
= 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?
.text-secondary
= _('A default branch cannot be chosen for an empty project.')
- else
.form-group
= f.label :default_branch, "Default Branch", class: 'label-bold'
= f.select(:default_branch, @project.repository.branch_names, {}, {class: 'select2 select-wide'})
= f.submit 'Save changes', class: "btn btn-success"
.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"
- project = local_assigns.fetch(:project)
- model = local_assigns.fetch(:model)
- 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…')
- supports_quick_actions = model.new_record?
......@@ -14,6 +16,8 @@
= form.label :description, 'Description', class: 'col-form-label col-sm-2'
.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 'projects/zen', f: form, attr: :description,
classes: 'note-textarea qa-issuable-form-description rspec-issuable-form-description',
......
......@@ -17,7 +17,6 @@
.form-group.row
= 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?)
#js-suggestions{ data: { project_path: @project.full_path } }
......
......@@ -2,7 +2,7 @@
- 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 } }
= template_dropdown_tag(issuable) do
%ul.dropdown-footer-list
......
......@@ -3,6 +3,9 @@
class ChatNotificationWorker
include ApplicationWorker
TimeoutExceeded = Class.new(StandardError)
sidekiq_options retry: false
feature_category :chatops
latency_sensitive_worker!
# TODO: break this into multiple jobs
......@@ -11,18 +14,21 @@ class ChatNotificationWorker
# worker_has_external_dependencies!
RESCHEDULE_INTERVAL = 2.seconds
RESCHEDULE_TIMEOUT = 5.minutes
# 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|
send_response(build)
end
rescue Gitlab::Chat::Output::MissingBuildSectionError
raise TimeoutExceeded if timeout_exceeded?(reschedule_count)
# 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
# sections are present. To better handle such cases we'll just reschedule
# 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
# rubocop: enable CodeReuse/ActiveRecord
......@@ -37,4 +43,10 @@ class ChatNotificationWorker
end
end
end
private
def timeout_exceeded?(reschedule_count)
(reschedule_count * RESCHEDULE_INTERVAL) >= RESCHEDULE_TIMEOUT
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
t.boolean "remove_source_branch_after_merge"
t.date "marked_for_deletion_at"
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 ["created_at", "id"], name: "index_projects_on_created_at_and_id"
t.index ["creator_id"], name: "index_projects_on_creator_id"
......
......@@ -318,6 +318,40 @@ Slots where `active` is `f` are not active.
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
GitLab places a timeout on all repository clones, including project imports
......
......@@ -1668,7 +1668,7 @@ type Epic implements Noteable {
state: EpicState!
"""
Boolean flag for whether the currently logged in user is subscribed to this epic
Indicates the currently logged in user is subscribed to the epic
"""
subscribed: Boolean!
......@@ -1984,7 +1984,7 @@ type EpicIssue implements Noteable {
state: IssueState!
"""
Boolean flag for whether the currently logged in user is subscribed to this issue
Indicates the currently logged in user is subscribed to the issue
"""
subscribed: Boolean!
......@@ -2089,42 +2089,42 @@ Check permissions for the current user on an epic
"""
type EpicPermissions {
"""
Whether or not a user can perform `admin_epic` on this resource
Indicates the user can perform `admin_epic` on this resource
"""
adminEpic: Boolean!
"""
Whether or not a user can perform `award_emoji` on this resource
Indicates the user can perform `award_emoji` on this resource
"""
awardEmoji: Boolean!
"""
Whether or not a user can perform `create_epic` on this resource
Indicates the user can perform `create_epic` on this resource
"""
createEpic: Boolean!
"""
Whether or not a user can perform `create_note` on this resource
Indicates the user can perform `create_note` on this resource
"""
createNote: Boolean!
"""
Whether or not a user can perform `destroy_epic` on this resource
Indicates the user can perform `destroy_epic` on this resource
"""
destroyEpic: Boolean!
"""
Whether or not a user can perform `read_epic` on this resource
Indicates the user can perform `read_epic` on this resource
"""
readEpic: Boolean!
"""
Whether or not a user can perform `read_epic_iid` on this resource
Indicates the user can perform `read_epic_iid` on this resource
"""
readEpicIid: Boolean!
"""
Whether or not a user can perform `update_epic` on this resource
Indicates the user can perform `update_epic` on this resource
"""
updateEpic: Boolean!
}
......@@ -2585,7 +2585,7 @@ type Group {
type GroupPermissions {
"""
Whether or not a user can perform `read_group` on this resource
Indicates the user can perform `read_group` on this resource
"""
readGroup: Boolean!
}
......@@ -2816,7 +2816,7 @@ type Issue implements Noteable {
state: IssueState!
"""
Boolean flag for whether the currently logged in user is subscribed to this issue
Indicates the currently logged in user is subscribed to the issue
"""
subscribed: Boolean!
......@@ -2921,42 +2921,42 @@ Check permissions for the current user on a issue
"""
type IssuePermissions {
"""
Whether or not a user can perform `admin_issue` on this resource
Indicates the user can perform `admin_issue` on this resource
"""
adminIssue: Boolean!
"""
Whether or not a user can perform `create_design` on this resource
Indicates the user can perform `create_design` on this resource
"""
createDesign: Boolean!
"""
Whether or not a user can perform `create_note` on this resource
Indicates the user can perform `create_note` on this resource
"""
createNote: Boolean!
"""
Whether or not a user can perform `destroy_design` on this resource
Indicates the user can perform `destroy_design` on this resource
"""
destroyDesign: Boolean!
"""
Whether or not a user can perform `read_design` on this resource
Indicates the user can perform `read_design` on this resource
"""
readDesign: Boolean!
"""
Whether or not a user can perform `read_issue` on this resource
Indicates the user can perform `read_issue` on this resource
"""
readIssue: Boolean!
"""
Whether or not a user can perform `reopen_issue` on this resource
Indicates the user can perform `reopen_issue` on this resource
"""
reopenIssue: Boolean!
"""
Whether or not a user can perform `update_issue` on this resource
Indicates the user can perform `update_issue` on this resource
"""
updateIssue: Boolean!
}
......@@ -3714,42 +3714,42 @@ Check permissions for the current user on a merge request
"""
type MergeRequestPermissions {
"""
Whether or not a user can perform `admin_merge_request` on this resource
Indicates the user can perform `admin_merge_request` on this resource
"""
adminMergeRequest: Boolean!
"""
Whether or not a user can perform `cherry_pick_on_current_merge_request` on this resource
Indicates the user can perform `cherry_pick_on_current_merge_request` on this resource
"""
cherryPickOnCurrentMergeRequest: Boolean!
"""
Whether or not a user can perform `create_note` on this resource
Indicates the user can perform `create_note` on this resource
"""
createNote: Boolean!
"""
Whether or not a user can perform `push_to_source_branch` on this resource
Indicates the user can perform `push_to_source_branch` on this resource
"""
pushToSourceBranch: Boolean!
"""
Whether or not a user can perform `read_merge_request` on this resource
Indicates the user can perform `read_merge_request` on this resource
"""
readMergeRequest: Boolean!
"""
Whether or not a user can perform `remove_source_branch` on this resource
Indicates the user can perform `remove_source_branch` on this resource
"""
removeSourceBranch: Boolean!
"""
Whether or not a user can perform `revert_on_current_merge_request` on this resource
Indicates the user can perform `revert_on_current_merge_request` on this resource
"""
revertOnCurrentMergeRequest: Boolean!
"""
Whether or not a user can perform `update_merge_request` on this resource
Indicates the user can perform `update_merge_request` on this resource
"""
updateMergeRequest: Boolean!
}
......@@ -4362,27 +4362,27 @@ type NoteEdge {
type NotePermissions {
"""
Whether or not a user can perform `admin_note` on this resource
Indicates the user can perform `admin_note` on this resource
"""
adminNote: Boolean!
"""
Whether or not a user can perform `award_emoji` on this resource
Indicates the user can perform `award_emoji` on this resource
"""
awardEmoji: Boolean!
"""
Whether or not a user can perform `create_note` on this resource
Indicates the user can perform `create_note` on this resource
"""
createNote: Boolean!
"""
Whether or not a user can perform `read_note` on this resource
Indicates the user can perform `read_note` on this resource
"""
readNote: Boolean!
"""
Whether or not a user can perform `resolve_note` on this resource
Indicates the user can perform `resolve_note` on this resource
"""
resolveNote: Boolean!
}
......@@ -4574,17 +4574,17 @@ type PipelineEdge {
type PipelinePermissions {
"""
Whether or not a user can perform `admin_pipeline` on this resource
Indicates the user can perform `admin_pipeline` on this resource
"""
adminPipeline: Boolean!
"""
Whether or not a user can perform `destroy_pipeline` on this resource
Indicates the user can perform `destroy_pipeline` on this resource
"""
destroyPipeline: Boolean!
"""
Whether or not a user can perform `update_pipeline` on this resource
Indicates the user can perform `update_pipeline` on this resource
"""
updatePipeline: Boolean!
}
......@@ -4605,10 +4605,15 @@ enum PipelineStatusEnum {
type Project {
"""
Archived status of the project
Indicates the archived status of the project
"""
archived: Boolean
"""
Indicates if issues referenced by merge requests and commits within the default branch are closed automatically
"""
autocloseReferencedIssues: Boolean
"""
URL to avatar image file of the project
"""
......@@ -5145,207 +5150,207 @@ type ProjectEdge {
type ProjectPermissions {
"""
Whether or not a user can perform `admin_operations` on this resource
Indicates the user can perform `admin_operations` on this resource
"""
adminOperations: Boolean!
"""
Whether or not a user can perform `admin_project` on this resource
Indicates the user can perform `admin_project` on this resource
"""
adminProject: Boolean!
"""
Whether or not a user can perform `admin_remote_mirror` on this resource
Indicates the user can perform `admin_remote_mirror` on this resource
"""
adminRemoteMirror: Boolean!
"""
Whether or not a user can perform `admin_wiki` on this resource
Indicates the user can perform `admin_wiki` on this resource
"""
adminWiki: Boolean!
"""
Whether or not a user can perform `archive_project` on this resource
Indicates the user can perform `archive_project` on this resource
"""
archiveProject: Boolean!
"""
Whether or not a user can perform `change_namespace` on this resource
Indicates the user can perform `change_namespace` on this resource
"""
changeNamespace: Boolean!
"""
Whether or not a user can perform `change_visibility_level` on this resource
Indicates the user can perform `change_visibility_level` on this resource
"""
changeVisibilityLevel: Boolean!
"""
Whether or not a user can perform `create_deployment` on this resource
Indicates the user can perform `create_deployment` on this resource
"""
createDeployment: Boolean!
"""
Whether or not a user can perform `create_design` on this resource
Indicates the user can perform `create_design` on this resource
"""
createDesign: Boolean!
"""
Whether or not a user can perform `create_issue` on this resource
Indicates the user can perform `create_issue` on this resource
"""
createIssue: Boolean!
"""
Whether or not a user can perform `create_label` on this resource
Indicates the user can perform `create_label` on this resource
"""
createLabel: Boolean!
"""
Whether or not a user can perform `create_merge_request_from` on this resource
Indicates the user can perform `create_merge_request_from` on this resource
"""
createMergeRequestFrom: Boolean!
"""
Whether or not a user can perform `create_merge_request_in` on this resource
Indicates the user can perform `create_merge_request_in` on this resource
"""
createMergeRequestIn: Boolean!
"""
Whether or not a user can perform `create_pages` on this resource
Indicates the user can perform `create_pages` on this resource
"""
createPages: Boolean!
"""
Whether or not a user can perform `create_pipeline` on this resource
Indicates the user can perform `create_pipeline` on this resource
"""
createPipeline: Boolean!
"""
Whether or not a user can perform `create_pipeline_schedule` on this resource
Indicates the user can perform `create_pipeline_schedule` on this resource
"""
createPipelineSchedule: Boolean!
"""
Whether or not a user can perform `create_snippet` on this resource
Indicates the user can perform `create_snippet` on this resource
"""
createSnippet: Boolean!
"""
Whether or not a user can perform `create_wiki` on this resource
Indicates the user can perform `create_wiki` on this resource
"""
createWiki: Boolean!
"""
Whether or not a user can perform `destroy_design` on this resource
Indicates the user can perform `destroy_design` on this resource
"""
destroyDesign: Boolean!
"""
Whether or not a user can perform `destroy_pages` on this resource
Indicates the user can perform `destroy_pages` on this resource
"""
destroyPages: Boolean!
"""
Whether or not a user can perform `destroy_wiki` on this resource
Indicates the user can perform `destroy_wiki` on this resource
"""
destroyWiki: Boolean!
"""
Whether or not a user can perform `download_code` on this resource
Indicates the user can perform `download_code` on this resource
"""
downloadCode: Boolean!
"""
Whether or not a user can perform `download_wiki_code` on this resource
Indicates the user can perform `download_wiki_code` on this resource
"""
downloadWikiCode: Boolean!
"""
Whether or not a user can perform `fork_project` on this resource
Indicates the user can perform `fork_project` on this resource
"""
forkProject: Boolean!
"""
Whether or not a user can perform `push_code` on this resource
Indicates the user can perform `push_code` on this resource
"""
pushCode: Boolean!
"""
Whether or not a user can perform `push_to_delete_protected_branch` on this resource
Indicates the user can perform `push_to_delete_protected_branch` on this resource
"""
pushToDeleteProtectedBranch: Boolean!
"""
Whether or not a user can perform `read_commit_status` on this resource
Indicates the user can perform `read_commit_status` on this resource
"""
readCommitStatus: Boolean!
"""
Whether or not a user can perform `read_cycle_analytics` on this resource
Indicates the user can perform `read_cycle_analytics` on this resource
"""
readCycleAnalytics: Boolean!
"""
Whether or not a user can perform `read_design` on this resource
Indicates the user can perform `read_design` on this resource
"""
readDesign: Boolean!
"""
Whether or not a user can perform `read_pages_content` on this resource
Indicates the user can perform `read_pages_content` on this resource
"""
readPagesContent: Boolean!
"""
Whether or not a user can perform `read_project` on this resource
Indicates the user can perform `read_project` on this resource
"""
readProject: Boolean!
"""
Whether or not a user can perform `read_project_member` on this resource
Indicates the user can perform `read_project_member` on this resource
"""
readProjectMember: Boolean!
"""
Whether or not a user can perform `read_wiki` on this resource
Indicates the user can perform `read_wiki` on this resource
"""
readWiki: Boolean!
"""
Whether or not a user can perform `remove_fork_project` on this resource
Indicates the user can perform `remove_fork_project` on this resource
"""
removeForkProject: Boolean!
"""
Whether or not a user can perform `remove_pages` on this resource
Indicates the user can perform `remove_pages` on this resource
"""
removePages: Boolean!
"""
Whether or not a user can perform `remove_project` on this resource
Indicates the user can perform `remove_project` on this resource
"""
removeProject: Boolean!
"""
Whether or not a user can perform `rename_project` on this resource
Indicates the user can perform `rename_project` on this resource
"""
renameProject: Boolean!
"""
Whether or not a user can perform `request_access` on this resource
Indicates the user can perform `request_access` on this resource
"""
requestAccess: Boolean!
"""
Whether or not a user can perform `update_pages` on this resource
Indicates the user can perform `update_pages` on this resource
"""
updatePages: Boolean!
"""
Whether or not a user can perform `update_wiki` on this resource
Indicates the user can perform `update_wiki` on this resource
"""
updateWiki: Boolean!
"""
Whether or not a user can perform `upload_file` on this resource
Indicates the user can perform `upload_file` on this resource
"""
uploadFile: Boolean!
}
......@@ -5909,32 +5914,32 @@ type SnippetEdge {
type SnippetPermissions {
"""
Whether or not a user can perform `admin_snippet` on this resource
Indicates the user can perform `admin_snippet` on this resource
"""
adminSnippet: Boolean!
"""
Whether or not a user can perform `award_emoji` on this resource
Indicates the user can perform `award_emoji` on this resource
"""
awardEmoji: Boolean!
"""
Whether or not a user can perform `create_note` on this resource
Indicates the user can perform `create_note` on this resource
"""
createNote: Boolean!
"""
Whether or not a user can perform `read_snippet` on this resource
Indicates the user can perform `read_snippet` on this resource
"""
readSnippet: Boolean!
"""
Whether or not a user can perform `report_snippet` on this resource
Indicates the user can perform `report_snippet` on this resource
"""
reportSnippet: Boolean!
"""
Whether or not a user can perform `update_snippet` on this resource
Indicates the user can perform `update_snippet` on this resource
"""
updateSnippet: Boolean!
}
......@@ -6360,7 +6365,7 @@ type ToggleAwardEmojiPayload {
errors: [String!]!
"""
True when the emoji was awarded, false when it was removed
Indicates the status of the emoji. True if the toggle awarded the emoji, and false if the toggle removed the emoji.
"""
toggledOn: Boolean!
}
......@@ -6873,7 +6878,7 @@ type UserEdge {
type UserPermissions {
"""
Whether or not a user can perform `create_snippet` on this resource
Indicates the user can perform `create_snippet` on this resource
"""
createSnippet: Boolean!
}
......
此差异已折叠。
......@@ -156,6 +156,7 @@ When the user is authenticated and `simple` is not set this returns something li
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
"statistics": {
"commit_count": 37,
"storage_size": 1038090,
......@@ -254,6 +255,7 @@ When the user is authenticated and `simple` is not set this returns something li
"packages_enabled": true,
"service_desk_enabled": false,
"service_desk_address": null,
"autoclose_referenced_issues": true,
"statistics": {
"commit_count": 12,
"storage_size": 2066080,
......@@ -385,6 +387,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
"statistics": {
"commit_count": 37,
"storage_size": 1038090,
......@@ -483,6 +486,7 @@ This endpoint supports [keyset pagination](README.md#keyset-based-pagination) fo
"packages_enabled": true,
"service_desk_enabled": false,
"service_desk_address": null,
"autoclose_referenced_issues": true,
"statistics": {
"commit_count": 12,
"storage_size": 2066080,
......@@ -593,6 +597,7 @@ Example response:
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
"statistics": {
"commit_count": 37,
"storage_size": 1038090,
......@@ -688,6 +693,7 @@ Example response:
"packages_enabled": true,
"service_desk_enabled": false,
"service_desk_address": null,
"autoclose_referenced_issues": true,
"statistics": {
"commit_count": 12,
"storage_size": 2066080,
......@@ -829,6 +835,7 @@ GET /projects/:id
"packages_enabled": true,
"service_desk_enabled": false,
"service_desk_address": null,
"autoclose_referenced_issues": true,
"statistics": {
"commit_count": 37,
"storage_size": 1038090,
......@@ -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_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 |
| `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 |
......@@ -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_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 |
| `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 |
......@@ -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_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 |
| `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 |
......@@ -1244,6 +1254,7 @@ Example responses:
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -1332,6 +1343,7 @@ Example response:
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -1419,6 +1431,7 @@ Example response:
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -1593,6 +1606,7 @@ Example response:
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......@@ -1699,6 +1713,7 @@ Example response:
"remove_source_branch_after_merge": false,
"request_access_enabled": false,
"merge_method": "merge",
"autoclose_referenced_issues": true,
"_links": {
"self": "http://example.com/api/v4/projects",
"issues": "http://example.com/api/v4/projects/1/issues",
......
......@@ -210,7 +210,7 @@ class MergeRequestPermissionsType < BasePermissionType
abilities :admin_merge_request, :update_merge_request, :create_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?
end
```
......
......@@ -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 `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.
- 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
......
......@@ -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
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)**
In order to change the default issue closing pattern, GitLab administrators must edit the
......
......@@ -331,6 +331,7 @@ module API
expose :auto_devops_deploy_strategy do |project, options|
project.auto_devops.nil? ? 'continuous' : project.auto_devops.deploy_strategy
end
expose :autoclose_referenced_issues
# rubocop: disable CodeReuse/ActiveRecord
def self.preload_relation(projects_relation, options = {})
......
......@@ -47,6 +47,7 @@ module API
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_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
params :optional_project_params_ee do
......@@ -85,6 +86,7 @@ module API
:container_registry_enabled,
:default_branch,
:description,
:autoclose_referenced_issues,
:issues_access_level,
:lfs_enabled,
:merge_requests_access_level,
......
......@@ -11,11 +11,13 @@ module Gitlab
end
def initialize(project, current_user = nil)
@project = project
@extractor = Gitlab::ReferenceExtractor.new(project, current_user)
end
def closed_by_message(message)
return [] if message.nil?
return [] unless @project.autoclose_referenced_issues
closing_statements = []
message.scan(ISSUE_CLOSING_REGEX) do
......
......@@ -50,6 +50,12 @@ module Gitlab
.gsub(/(\A-+|-+\z)/, '')
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
def nlbr(str)
ActionView::Base.full_sanitizer.sanitize(+str, tags: []).gsub(/\r?\n/, '<br>').html_safe
......
......@@ -2381,6 +2381,9 @@ msgstr ""
msgid "Auto-cancel redundant, pending pipelines"
msgstr ""
msgid "Auto-close referenced issues on default branch"
msgstr ""
msgid "AutoDevOps|Auto DevOps"
msgstr ""
......@@ -10077,6 +10080,9 @@ msgstr ""
msgid "Issues closed"
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"
msgstr ""
......@@ -21231,6 +21237,9 @@ msgstr ""
msgid "already being used for another group or project milestone."
msgstr ""
msgid "already has a \"created\" issue link"
msgstr ""
msgid "already shared with this group"
msgstr ""
......@@ -21688,6 +21697,9 @@ msgstr ""
msgid "group"
msgstr ""
msgid "has already been linked to another vulnerability"
msgstr ""
msgid "has already been taken"
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
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
grafanaIntegration autocloseReferencedIssues
]
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
.to match_array([issue])
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
def urls
......
......@@ -463,6 +463,7 @@ project:
- import_failures
- container_expiration_policy
- resource_groups
- autoclose_referenced_issues
award_emoji:
- awardable
- user
......
......@@ -534,6 +534,7 @@ Project:
- pages_https_only
- merge_requests_disable_committers_approval
- require_password_to_approve
- autoclose_referenced_issues
ProjectTracingSetting:
- external_url
Author:
......
......@@ -3,8 +3,9 @@
require 'spec_helper'
describe Gitlab::Utils do
delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which, :ensure_array_from_string,
:bytes_to_megabytes, :append_path, :check_path_traversal!, to: :described_class
delegate :to_boolean, :boolean_to_yes_no, :slugify, :random_string, :which,
:ensure_array_from_string, :to_exclusive_sentence, :bytes_to_megabytes,
:append_path, :check_path_traversal!, to: :described_class
describe '.check_path_traversal!' do
it 'detects path traversal at the start of the string' do
......@@ -46,6 +47,36 @@ describe Gitlab::Utils do
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
it 'replaces new lines with <br>' do
expect(described_class.nlbr("<b>hello</b>\n<i>world</i>".freeze)).to eq("hello<br>world")
......
......@@ -390,6 +390,17 @@ eos
expect(commit.closes_issues).to include(issue)
expect(commit.closes_issues).to include(other_issue)
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
it_behaves_like 'a mentionable' do
......
......@@ -963,6 +963,15 @@ describe MergeRequest do
expect(subject.closes_issues).to be_empty
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
describe '#issues_mentioned_but_not_closing' do
......
......@@ -474,6 +474,32 @@ describe Project do
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
it 'sets an random token if none provided' do
project = FactoryBot.create(:project, runners_token: '')
......
......@@ -635,6 +635,7 @@ describe API::Projects do
wiki_enabled: false,
resolve_outdated_diff_discussions: false,
remove_source_branch_after_merge: true,
autoclose_referenced_issues: true,
only_allow_merge_if_pipeline_succeeds: false,
request_access_enabled: true,
only_allow_merge_if_all_discussions_are_resolved: false,
......@@ -807,6 +808,22 @@ describe API::Projects do
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
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
project = attributes_for(:project, merge_method: 'rebase_merge')
......
......@@ -8,6 +8,10 @@ describe ChatNotificationWorker do
create(:ci_build, pipeline: create(:ci_pipeline, source: :chat))
end
it 'instructs sidekiq not to retry on failure' do
expect(described_class.get_sidekiq_options['retry']).to eq(false)
end
describe '#perform' do
it 'does nothing when the build no longer exists' do
expect(worker).not_to receive(:send_response)
......@@ -23,16 +27,31 @@ describe ChatNotificationWorker do
worker.perform(chat_build.id)
end
it 'reschedules the job if the trace sections could not be found' do
expect(worker)
.to receive(:send_response)
.and_raise(Gitlab::Chat::Output::MissingBuildSectionError)
context 'when the trace sections could not be found' do
it 'reschedules the job' do
expect(worker)
.to receive(:send_response)
.and_raise(Gitlab::Chat::Output::MissingBuildSectionError)
expect(described_class)
.to receive(:perform_in)
.with(described_class::RESCHEDULE_INTERVAL, chat_build.id)
expect(described_class)
.to receive(:perform_in)
.with(described_class::RESCHEDULE_INTERVAL, chat_build.id, 1)
worker.perform(chat_build.id)
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)
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
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册