Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-03-29 21:09:08 +00:00
parent 5b2abea8db
commit 12c766c774
41 changed files with 904 additions and 221 deletions

View file

@ -1,7 +1,16 @@
<script>
import { GlFormGroup, GlFormCheckbox, GlFormRadio } from '@gitlab/ui';
import {
GlFormGroup,
GlFormCheckbox,
GlFormRadio,
GlFormInput,
GlLink,
GlSprintf,
} from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
const commentDetailOptions = [
{
@ -18,12 +27,41 @@ const commentDetailOptions = [
},
];
const ISSUE_TRANSITION_AUTO = true;
const ISSUE_TRANSITION_CUSTOM = false;
const issueTransitionOptions = [
{
value: ISSUE_TRANSITION_AUTO,
label: s__('JiraService|Move to Done'),
help: s__(
'JiraService|Automatically transitions Jira issues to the "Done" category. %{linkStart}Learn more%{linkEnd}',
),
link: helpPagePath('user/project/integrations/jira.html', {
anchor: 'automatic-issue-transitions',
}),
},
{
value: ISSUE_TRANSITION_CUSTOM,
label: s__('JiraService|Use custom transitions'),
help: s__(
'JiraService|Set a custom final state by using transition IDs. %{linkStart}Learn about transition IDs%{linkEnd}',
),
link: helpPagePath('user/project/integrations/jira.html', {
anchor: 'custom-issue-transitions',
}),
},
];
export default {
name: 'JiraTriggerFields',
components: {
GlFormGroup,
GlFormCheckbox,
GlFormRadio,
GlFormInput,
GlLink,
GlSprintf,
},
props: {
initialTriggerCommit: {
@ -43,21 +81,58 @@ export default {
required: false,
default: 'standard',
},
initialJiraIssueTransitionAutomatic: {
type: Boolean,
required: false,
default: false,
},
initialJiraIssueTransitionId: {
type: String,
required: false,
default: '',
},
},
data() {
return {
validated: false,
triggerCommit: this.initialTriggerCommit,
triggerMergeRequest: this.initialTriggerMergeRequest,
enableComments: this.initialEnableComments,
commentDetail: this.initialCommentDetail,
jiraIssueTransitionAutomatic:
this.initialJiraIssueTransitionAutomatic || !this.initialJiraIssueTransitionId,
jiraIssueTransitionId: this.initialJiraIssueTransitionId,
issueTransitionEnabled:
this.initialJiraIssueTransitionAutomatic || Boolean(this.initialJiraIssueTransitionId),
commentDetailOptions,
issueTransitionOptions,
};
},
computed: {
...mapGetters(['isInheriting']),
showEnableComments() {
showTriggerSettings() {
return this.triggerCommit || this.triggerMergeRequest;
},
validIssueTransitionId() {
return !this.validated || Boolean(this.jiraIssueTransitionId);
},
},
created() {
eventHub.$on('validateForm', this.validateForm);
},
beforeDestroy() {
eventHub.$off('validateForm', this.validateForm);
},
methods: {
validateForm() {
this.validated = true;
},
showCustomIssueTransitions(currentOption) {
return (
this.jiraIssueTransitionAutomatic === ISSUE_TRANSITION_CUSTOM &&
currentOption === ISSUE_TRANSITION_CUSTOM
);
},
},
};
</script>
@ -89,7 +164,7 @@ export default {
</gl-form-group>
<gl-form-group
v-show="showEnableComments"
v-show="showTriggerSettings"
:label="s__('Integrations|Comment settings:')"
label-for="service[comment_on_event_enabled]"
class="gl-pl-6"
@ -106,7 +181,7 @@ export default {
</gl-form-group>
<gl-form-group
v-show="showEnableComments && enableComments"
v-show="showTriggerSettings && enableComments"
:label="s__('Integrations|Comment detail:')"
label-for="service[comment_detail]"
class="gl-pl-9"
@ -126,5 +201,67 @@ export default {
</template>
</gl-form-radio>
</gl-form-group>
<gl-form-group
v-if="showTriggerSettings"
:label="s__('JiraService|Transition Jira issues to their final state:')"
class="gl-pl-6"
data-testid="issue-transition-enabled"
>
<input type="hidden" name="service[jira_issue_transition_automatic]" value="false" />
<input type="hidden" name="service[jira_issue_transition_id]" value="" />
<gl-form-checkbox
v-model="issueTransitionEnabled"
:disabled="isInheriting"
data-qa-selector="service_jira_issue_transition_enabled"
>
{{ s__('JiraService|Enable Jira transitions') }}
</gl-form-checkbox>
</gl-form-group>
<gl-form-group
v-if="showTriggerSettings && issueTransitionEnabled"
class="gl-pl-9"
data-testid="issue-transition-mode"
>
<gl-form-radio
v-for="issueTransitionOption in issueTransitionOptions"
:key="issueTransitionOption.value"
v-model="jiraIssueTransitionAutomatic"
name="service[jira_issue_transition_automatic]"
:value="issueTransitionOption.value"
:disabled="isInheriting"
:data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}`"
>
{{ issueTransitionOption.label }}
<template v-if="showCustomIssueTransitions(issueTransitionOption.value)">
<gl-form-input
v-model="jiraIssueTransitionId"
name="service[jira_issue_transition_id]"
type="text"
class="gl-my-3"
data-qa-selector="service_jira_issue_transition_id_field"
:placeholder="s__('JiraService|For example, 12, 24')"
:disabled="isInheriting"
:required="true"
:state="validIssueTransitionId"
/>
<span class="invalid-feedback">
{{ __('This field is required.') }}
</span>
</template>
<template #help>
<gl-sprintf :message="issueTransitionOption.help">
<template #link="{ content }">
<gl-link :href="issueTransitionOption.link" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</template>
</gl-form-radio>
</gl-form-group>
</div>
</template>

View file

@ -29,6 +29,8 @@ function parseDatasetToProps(data) {
testPath,
resetPath,
vulnerabilitiesIssuetype,
jiraIssueTransitionAutomatic,
jiraIssueTransitionId,
...booleanAttributes
} = data;
const {
@ -60,6 +62,8 @@ function parseDatasetToProps(data) {
initialTriggerMergeRequest: mergeRequestEvents,
initialEnableComments: enableComments,
initialCommentDetail: commentDetail,
initialJiraIssueTransitionAutomatic: jiraIssueTransitionAutomatic,
initialJiraIssueTransitionId: jiraIssueTransitionId,
},
jiraIssuesProps: {
showJiraIssuesIntegration,

View file

@ -44,6 +44,7 @@ module ServiceParams
# make those event names plural as special case.
:issues_events,
:issues_url,
:jira_issue_transition_automatic,
:jira_issue_transition_id,
:manual_configuration,
:merge_requests_events,

View file

@ -67,7 +67,7 @@ module GroupsHelper
def group_open_issues_count(group)
if Feature.enabled?(:cached_sidebar_open_issues_count, group, default_enabled: :yaml)
cached_open_group_issues_count(group)
cached_issuables_count(group, type: :issues)
else
number_with_delimiter(group_issues_count(state: 'opened'))
end
@ -80,18 +80,11 @@ module GroupsHelper
.count
end
def cached_open_group_issues_count(group)
count_service = Groups::OpenIssuesCountService
issues_count = count_service.new(group, current_user).count
if issues_count > count_service::CACHED_COUNT_THRESHOLD
ActiveSupport::NumberHelper
.number_to_human(
issues_count,
units: { thousand: 'k', million: 'm' }, precision: 1, significant: false, format: '%n%u'
)
def group_open_merge_requests_count(group)
if Feature.enabled?(:cached_sidebar_merge_requests_count, group, default_enabled: :yaml)
cached_issuables_count(@group, type: :merge_requests)
else
number_with_delimiter(issues_count)
number_with_delimiter(group_merge_requests_count(state: 'opened'))
end
end
@ -102,6 +95,14 @@ module GroupsHelper
.count
end
def cached_issuables_count(group, type: nil)
count_service = issuables_count_service_class(type)
return unless count_service.present?
issuables_count = count_service.new(group, current_user).count
format_issuables_count(count_service, issuables_count)
end
def group_dependency_proxy_url(group)
# The namespace path can include uppercase letters, which
# Docker doesn't allow. The proxy expects it to be downcased.
@ -120,7 +121,9 @@ module GroupsHelper
@has_group_title = true
full_title = []
group.ancestors.reverse.each_with_index do |parent, index|
ancestors = group.ancestors.with_route
ancestors.reverse_each.with_index do |parent, index|
if index > 0
add_to_breadcrumb_dropdown(group_title_link(parent, hidable: false, show_avatar: true, for_dropdown: true), location: :before)
else
@ -306,6 +309,26 @@ module GroupsHelper
def ancestor_locked_and_has_been_overridden(group)
s_("GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup.").html_safe % { ancestor_group: ancestor_group(group) }
end
def issuables_count_service_class(type)
if type == :issues
Groups::OpenIssuesCountService
elsif type == :merge_requests
Groups::MergeRequestsCountService
end
end
def format_issuables_count(count_service, count)
if count > count_service::CACHED_COUNT_THRESHOLD
ActiveSupport::NumberHelper
.number_to_human(
count,
units: { thousand: 'k', million: 'm' }, precision: 1, significant: false, format: '%n%u'
)
else
number_with_delimiter(count)
end
end
end
GroupsHelper.prepend_if_ee('EE::GroupsHelper')

View file

@ -86,7 +86,7 @@ module ServicesHelper
end
def integration_form_data(integration, group: nil)
{
form_data = {
id: integration.id,
show_active: integration.show_active_box?.to_s,
activated: (integration.active || integration.new_record?).to_s,
@ -106,6 +106,13 @@ module ServicesHelper
test_path: scoped_test_integration_path(integration),
reset_path: scoped_reset_integration_path(integration, group: group)
}
if integration.is_a?(JiraService)
form_data[:jira_issue_transition_automatic] = integration.jira_issue_transition_automatic
form_data[:jira_issue_transition_id] = integration.jira_issue_transition_id
end
form_data
end
def trigger_events_for_service(integration)

View file

@ -31,7 +31,7 @@ class JiraService < IssueTrackerService
# TODO: we can probably just delegate as part of
# https://gitlab.com/gitlab-org/gitlab/issues/29404
data_field :username, :password, :url, :api_url, :jira_issue_transition_id, :project_key, :issues_enabled,
data_field :username, :password, :url, :api_url, :jira_issue_transition_automatic, :jira_issue_transition_id, :project_key, :issues_enabled,
:vulnerabilities_enabled, :vulnerabilities_issuetype
before_update :reset_password
@ -124,9 +124,6 @@ class JiraService < IssueTrackerService
end
def fields
transition_id_help_path = help_page_path('user/project/integrations/jira', anchor: 'obtaining-a-transition-id')
transition_id_help_link_start = '<a href="%{transition_id_help_path}" target="_blank" rel="noopener noreferrer">'.html_safe % { transition_id_help_path: transition_id_help_path }
[
{
type: 'text',
@ -156,13 +153,6 @@ class JiraService < IssueTrackerService
non_empty_password_help: s_('JiraService|Leave blank to use your current password or API token'),
placeholder: s_('JiraService|Use a password for server version and an API token for cloud version'),
required: true
},
{
type: 'text',
name: 'jira_issue_transition_id',
title: s_('JiraService|Jira workflow transition IDs'),
placeholder: s_('JiraService|For example, 12, 24'),
help: s_('JiraService|Set transition IDs for Jira workflow transitions. %{link_start}Learn more%{link_end}'.html_safe % { link_start: transition_id_help_link_start, link_end: '</a>'.html_safe })
}
]
end
@ -190,17 +180,19 @@ class JiraService < IssueTrackerService
# support any events.
end
def find_issue(issue_key, rendered_fields: false)
options = {}
options = options.merge(expand: 'renderedFields') if rendered_fields
def find_issue(issue_key, rendered_fields: false, transitions: false)
expands = []
expands << 'renderedFields' if rendered_fields
expands << 'transitions' if transitions
options = { expand: expands.join(',') } if expands.any?
jira_request { client.Issue.find(issue_key, options) }
jira_request { client.Issue.find(issue_key, options || {}) }
end
def close_issue(entity, external_issue, current_user)
issue = find_issue(external_issue.iid)
issue = find_issue(external_issue.iid, transitions: jira_issue_transition_automatic)
return if issue.nil? || has_resolution?(issue) || !jira_issue_transition_id.present?
return if issue.nil? || has_resolution?(issue) || !issue_transition_enabled?
commit_id = case entity
when Commit then entity.id
@ -275,6 +267,10 @@ class JiraService < IssueTrackerService
true
end
def issue_transition_enabled?
jira_issue_transition_automatic || jira_issue_transition_id.present?
end
private
def server_info
@ -295,22 +291,46 @@ class JiraService < IssueTrackerService
# the issue is transitioned at the order given by the user
# if any transition fails it will log the error message and stop the transition sequence
def transition_issue(issue)
jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).each do |transition_id|
issue.transitions.build.save!(transition: { id: transition_id })
rescue => error
log_error(
"Issue transition failed",
error: {
exception_class: error.class.name,
exception_message: error.message,
exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
},
client_url: client_url
)
return false
return transition_issue_to_done(issue) if jira_issue_transition_automatic
jira_issue_transition_id.scan(Gitlab::Regex.jira_transition_id_regex).all? do |transition_id|
transition_issue_to_id(issue, transition_id)
end
end
def transition_issue_to_id(issue, transition_id)
issue.transitions.build.save!(
transition: { id: transition_id }
)
true
rescue => error
log_error(
"Issue transition failed",
error: {
exception_class: error.class.name,
exception_message: error.message,
exception_backtrace: Gitlab::BacktraceCleaner.clean_backtrace(error.backtrace)
},
client_url: client_url
)
false
end
def transition_issue_to_done(issue)
transitions = issue.transitions rescue []
transition = transitions.find do |transition|
status = transition&.to&.statusCategory
status && status['key'] == 'done'
end
return false unless transition
transition_issue_to_id(issue, transition.id)
end
def log_usage(action, user)
key = "i_ecosystem_jira_service_#{action}"

View file

@ -0,0 +1,49 @@
# frozen_string_literal: true
module Groups
class CountService < BaseCountService
include Gitlab::Utils::StrongMemoize
VERSION = 1
CACHED_COUNT_THRESHOLD = 1000
EXPIRATION_TIME = 24.hours
attr_reader :group, :user
def initialize(group, user = nil)
@group = group
@user = user
end
def count
cached_count = Rails.cache.read(cache_key)
return cached_count unless cached_count.blank?
refreshed_count = uncached_count
update_cache_for_key(cache_key) { refreshed_count } if refreshed_count > CACHED_COUNT_THRESHOLD
refreshed_count
end
def cache_key
['groups', "#{issuable_key}_count_service", VERSION, group.id, cache_key_name]
end
private
def relation_for_count
raise NotImplementedError
end
def cache_options
super.merge({ expires_in: EXPIRATION_TIME })
end
def cache_key_name
raise NotImplementedError, 'cache_key_name must be implemented and return a String'
end
def issuable_key
raise NotImplementedError, 'issuable_key must be implemented and return a String'
end
end
end

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Groups
# Service class for counting and caching the number of open merge requests of a group.
class MergeRequestsCountService < Groups::CountService
private
def cache_key_name
'open_merge_requests_count'
end
def relation_for_count
MergeRequestsFinder
.new(user, group_id: group.id, state: 'opened', non_archived: true, include_subgroups: true)
.execute
end
def issuable_key
'open_merge_requests'
end
end
end

View file

@ -2,47 +2,12 @@
module Groups
# Service class for counting and caching the number of open issues of a group.
class OpenIssuesCountService < BaseCountService
include Gitlab::Utils::StrongMemoize
VERSION = 1
class OpenIssuesCountService < Groups::CountService
PUBLIC_COUNT_KEY = 'group_public_open_issues_count'
TOTAL_COUNT_KEY = 'group_total_open_issues_count'
CACHED_COUNT_THRESHOLD = 1000
EXPIRATION_TIME = 24.hours
attr_reader :group, :user
def initialize(group, user = nil)
@group = group
@user = user
end
# Reads count value from cache and return it if present.
# If empty or expired, #uncached_count will calculate the issues count for the group and
# compare it with the threshold. If it is greater, it will be written to the cache and returned.
# If below, it will be returned without being cached.
# This results in only caching large counts and calculating the rest with every call to maintain
# accuracy.
def count
cached_count = Rails.cache.read(cache_key)
return cached_count unless cached_count.blank?
refreshed_count = uncached_count
update_cache_for_key(cache_key) { refreshed_count } if refreshed_count > CACHED_COUNT_THRESHOLD
refreshed_count
end
def cache_key(key = nil)
['groups', 'open_issues_count_service', VERSION, group.id, cache_key_name]
end
private
def cache_options
super.merge({ expires_in: EXPIRATION_TIME })
end
def cache_key_name
public_only? ? PUBLIC_COUNT_KEY : TOTAL_COUNT_KEY
end
@ -60,5 +25,9 @@ module Groups
def relation_for_count
IssuesFinder.new(user, group_id: group.id, state: 'opened', non_archived: true, include_subgroups: true, public_only: public_only?).execute
end
def issuable_key
'open_issues'
end
end
end

View file

@ -2,7 +2,7 @@
- page_title _("Merge Requests")
- if group_merge_requests_count(state: 'all') == 0
- if @merge_requests&.size == 0
= render 'shared/empty_states/merge_requests', project_select_button: true
- else
.top-area

View file

@ -1,5 +1,5 @@
- issues_count = group_open_issues_count(@group)
- merge_requests_count = group_merge_requests_count(state: 'opened')
- merge_requests_count = group_open_merge_requests_count(@group)
- aside_title = @group.subgroup? ? _('Subgroup navigation') : _('Group navigation')
- overview_title = @group.subgroup? ? _('Subgroup overview') : _('Group overview')
@ -92,13 +92,13 @@
= sprite_icon('git-merge')
%span.nav-item-name
= _('Merge Requests')
%span.badge.badge-pill.count= number_with_delimiter(merge_requests_count)
%span.badge.badge-pill.count= merge_requests_count
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(path: 'groups#merge_requests', html_options: { class: "fly-out-top-item" } ) do
= link_to merge_requests_group_path(@group) do
%strong.fly-out-top-item-name
= _('Merge Requests')
%span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(merge_requests_count)
%span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= merge_requests_count
= render_if_exists "layouts/nav/ee/security_link" # EE-specific

View file

@ -0,0 +1,5 @@
---
title: Support automatic transitions of Jira issues
merge_request: 55773
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Cache open merge requests count in group sidebar
merge_request: 55971
author:
type: performance

View file

@ -0,0 +1,5 @@
---
title: Avoid N+1 queries in breadcrumbs
merge_request: 57725
author:
type: performance

View file

@ -0,0 +1,8 @@
---
name: cached_sidebar_merge_requests_count
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55971
rollout_issue_url:
milestone: '13.11'
type: development
group: group::product planning
default_enabled: true

View file

@ -0,0 +1,9 @@
# frozen_string_literal: true
class AddJiraIssueTransitionAutomaticToJiraTrackerData < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
add_column :jira_tracker_data, :jira_issue_transition_automatic, :boolean, null: false, default: false
end
end

View file

@ -0,0 +1 @@
328e095123eb0b8822342b0d4a338d42265ca8eafbcadcc7e15628e9d02c863d

View file

@ -13977,6 +13977,7 @@ CREATE TABLE jira_tracker_data (
encrypted_proxy_username_iv text,
encrypted_proxy_password text,
encrypted_proxy_password_iv text,
jira_issue_transition_automatic boolean DEFAULT false NOT NULL,
CONSTRAINT check_0bf84b76e9 CHECK ((char_length(vulnerabilities_issuetype) <= 255)),
CONSTRAINT check_214cf6a48b CHECK ((char_length(project_key) <= 255))
);

View file

@ -814,7 +814,8 @@ Parameters:
| `username` | string | yes | The username of the user created to be used with GitLab/Jira. |
| `password` | string | yes | The password of the user created to be used with GitLab/Jira. |
| `active` | boolean | no | Activates or deactivates the service. Defaults to false (deactivated). |
| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the Jira workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the transitions ID column. By default, this ID is set to `2`. |
| `jira_issue_transition_automatic` | boolean | no | Enable [automatic issue transitions](../user/project/integrations/jira.md#automatic-issue-transitions). Takes precedence over `jira_issue_transition_id` if enabled. Defaults to `false` |
| `jira_issue_transition_id` | string | no | The ID of one or more transitions for [custom issue transitions](../user/project/integrations/jira.md#custom-issue-transitions). Ignored if `jira_issue_transition_automatic` is enabled. Defaults to a blank string, which disables custom transitions. |
| `commit_events` | boolean | false | Enable notifications for commit events |
| `merge_requests_events` | boolean | false | Enable notifications for merge request events |
| `comment_on_event_enabled` | boolean | false | Enable comments inside Jira issues on each GitLab event (commit / merge request) |

View file

@ -251,9 +251,6 @@ Prerequisites:
- [Authenticate](#authenticate-to-the-package-registry) to the Package Registry.
- Set a [project-level npm endpoint](#use-the-gitlab-endpoint-for-npm-packages).
- Your npm package name must be in the format of [@scope/package-name](#package-naming-convention).
It must match exactly, including the case. This is different than the
npm naming convention, but it is required to work with the GitLab Package Registry.
To upload an npm package to your project, run this command:

View file

@ -106,7 +106,8 @@ To enable the Jira integration in a project:
1. To include a comment on the Jira issue when the above reference is made in GitLab, select
**Enable comments**.
1. Select the **Comment detail**: **Standard** or **All details**.
1. To transition Jira issues when a [closing reference](../issues/managing_issues.md#closing-issues-automatically) is made in GitLab,
select **Enable Jira transitions**.
1. Enter the further details on the page as described in the following table.
@ -116,7 +117,6 @@ To enable the Jira integration in a project:
| `Jira API URL` | The base URL to the Jira instance API. Web URL value is used if not set. For example, `https://jira-api.example.com`. Leave this field blank (or use the same value of `Web URL`) if using **Jira on Atlassian cloud**. |
| `Username or Email` | Created in [configure Jira](#configure-jira) step. Use `username` for **Jira Server** or `email` for **Jira on Atlassian cloud**. |
| `Password/API token` | Created in [configure Jira](#configure-jira) step. Use `password` for **Jira Server** or `API token` for **Jira on Atlassian cloud**. |
| `Jira workflow transition IDs` | Required for closing Jira issues via commits or merge requests. These are the IDs of transitions in Jira that move issues to a particular state. (See [Obtaining a transition ID](#obtaining-a-transition-id).) If you insert multiple transition IDs separated by `,` or `;`, the issue is moved to each state, one after another, using the given order. In GitLab 13.6 and earlier, field was called `Transition ID`. |
1. To enable users to view Jira issues inside the GitLab project, select **Enable Jira issues** and
enter a Jira project key. **(PREMIUM)**
@ -138,10 +138,26 @@ To enable the Jira integration in a project:
Your GitLab project can now interact with all Jira projects in your instance and the project now
displays a Jira link that opens the Jira project.
#### Obtaining a transition ID
#### Automatic issue transitions
In the most recent Jira user interface, you can no longer see transition IDs in the workflow
administration UI. You can get the ID you need in either of the following ways:
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/...) in GitLab 13.10.
In this mode the referenced Jira issue is transitioned to the next available status with a category of "Done".
See the [Configure GitLab](#configure-gitlab) section, check the **Enable Jira transitions** setting and select the **Move to Done** option.
#### Custom issue transitions
For advanced workflows you can specify custom Jira transition IDs.
See the [Configure GitLab](#configure-gitlab) section, check the **Enable Jira transitions** setting, select the **Custom transitions** option, and enter your transition IDs in the text field.
If you insert multiple transition IDs separated by `,` or `;`, the issue is moved to each state, one after another, using the given order. If a transition fails the sequence is aborted.
To see the transition IDs on Jira Cloud, edit a workflow in the **Text** view.
The transition IDs display in the **Transitions** column.
On Jira Server you can get the transition IDs in either of the following ways:
1. By using the API, with a request like `https://yourcompany.atlassian.net/rest/api/2/issue/ISSUE-123/transitions`
using an issue that is in the appropriate "open" state

View file

@ -28,6 +28,39 @@ You can [search and filter the results](../../search/index.md#filtering-issue-an
![Group Issues list view](img/group_merge_requests_list_view.png)
## Cached merge request count
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/299542) in GitLab 13.11.
> - It's [deployed behind a feature flag](../../feature_flags.md), enabled by default.
> - It's enabled on GitLab.com.
> - It's recommended for production use.
> - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-cached-merge-request-count).
WARNING:
This feature might not be available to you. Check the **version history** note above for details.
In a group, the sidebar displays the total count of open merge requests and this value is cached if higher
than 1000. The cached value is rounded to thousands (or millions) and updated every 24 hours.
### Enable or disable cached merge request count **(FREE SELF)**
Cached merge request count in the left sidebar is under development but ready for production use. It is
deployed behind a feature flag that is **enabled by default**.
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can disable it.
To disable it:
```ruby
Feature.disable(:cached_sidebar_merge_requests_count)
```
To enable it:
```ruby
Feature.enable(:cached_sidebar_merge_requests_count)
```
## Semi-linear history merge requests
A merge commit is created for every merge, but the branch is only merged if

View file

@ -541,11 +541,17 @@ module API
type: String,
desc: 'The password of the user created to be used with GitLab/Jira'
},
{
required: false,
name: :jira_issue_transition_automatic,
type: Boolean,
desc: 'Enable automatic issue transitions'
},
{
required: false,
name: :jira_issue_transition_id,
type: String,
desc: 'The ID of a transition that moves issues to a closed state. You can find this number under the Jira workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`'
desc: 'The ID of one or more transitions for custom issue transitions'
},
{
required: false,

View file

@ -128,6 +128,11 @@ module API
yield
end
end
# Overridden in EE
def geo_proxy
{}
end
end
namespace 'internal' do
@ -322,6 +327,12 @@ module API
two_factor_otp_check
end
# Workhorse calls this to determine if it is a Geo secondary site
# that should proxy requests. FOSS can quickly return empty data.
get '/geo_proxy', feature_category: :geo_replication do
geo_proxy
end
end
end
end

View file

@ -17396,6 +17396,9 @@ msgstr ""
msgid "JiraService|An error occurred while fetching issue list"
msgstr ""
msgid "JiraService|Automatically transitions Jira issues to the \"Done\" category. %{linkStart}Learn more%{linkEnd}"
msgstr ""
msgid "JiraService|Define the type of Jira issue to create from a vulnerability."
msgstr ""
@ -17408,6 +17411,9 @@ msgstr ""
msgid "JiraService|Enable Jira issues creation from vulnerabilities"
msgstr ""
msgid "JiraService|Enable Jira transitions"
msgstr ""
msgid "JiraService|Enter new password or API token"
msgstr ""
@ -17456,10 +17462,10 @@ msgstr ""
msgid "JiraService|Jira project key"
msgstr ""
msgid "JiraService|Jira workflow transition IDs"
msgid "JiraService|Leave blank to use your current password or API token"
msgstr ""
msgid "JiraService|Leave blank to use your current password or API token"
msgid "JiraService|Move to Done"
msgstr ""
msgid "JiraService|Not all data may be displayed here. To view more details or make changes to this issue, go to %{linkStart}Jira%{linkEnd}."
@ -17480,7 +17486,7 @@ msgstr ""
msgid "JiraService|Select issue type"
msgstr ""
msgid "JiraService|Set transition IDs for Jira workflow transitions. %{link_start}Learn more%{link_end}"
msgid "JiraService|Set a custom final state by using transition IDs. %{linkStart}Learn about transition IDs%{linkEnd}"
msgstr ""
msgid "JiraService|Sign in to GitLab.com to get started."
@ -17492,12 +17498,18 @@ msgstr ""
msgid "JiraService|This issue is synchronized with Jira"
msgstr ""
msgid "JiraService|Transition Jira issues to their final state:"
msgstr ""
msgid "JiraService|Use a password for server version and an API token for cloud version"
msgstr ""
msgid "JiraService|Use a username for server version and an email for cloud version"
msgstr ""
msgid "JiraService|Use custom transitions"
msgstr ""
msgid "JiraService|Username or Email"
msgstr ""

View file

@ -171,8 +171,8 @@ module QA
# This is a helpful workaround when there is a transparent element overlapping
# the target element and so, normal `click_element` on target would raise
# Selenium::WebDriver::Error::ElementClickInterceptedError
def click_element_coordinates(name)
page.driver.browser.action.move_to(find_element(name).native).click.perform
def click_element_coordinates(name, **kwargs)
page.driver.browser.action.move_to(find_element(name, **kwargs).native).click.perform
end
# replace with (..., page = self.class)

View file

@ -10,7 +10,13 @@ module QA
element :service_url_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern
element :service_username_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern
element :service_password_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern
element :service_jira_issue_transition_id_field, ':data-qa-selector="`${fieldId}_field`"' # rubocop:disable QA/ElementWithPattern
end
view 'app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue' do
element :service_jira_issue_transition_enabled
element :service_jira_issue_transition_automatic_true, ':data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}`"' # rubocop:disable QA/ElementWithPattern
element :service_jira_issue_transition_automatic_false, ':data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}`"' # rubocop:disable QA/ElementWithPattern
element :service_jira_issue_transition_id_field
end
view 'app/assets/javascripts/integrations/edit/components/integration_form.vue' do
@ -23,7 +29,10 @@ module QA
set_jira_server_url(url)
set_username(Runtime::Env.jira_admin_username)
set_password(Runtime::Env.jira_admin_password)
set_transaction_ids('11,21,31,41')
enable_transitions
use_custom_transitions
set_transition_ids('11,21,31,41')
click_save_changes_button
wait_until(reload: false) do
@ -45,12 +54,24 @@ module QA
fill_element(:service_password_field, password)
end
def set_transaction_ids(transaction_ids)
fill_element(:service_jira_issue_transition_id_field, transaction_ids)
def enable_transitions
click_element_coordinates(:service_jira_issue_transition_enabled, visible: false)
end
def use_automatic_transitions
click_element_coordinates(:service_jira_issue_transition_automatic_true, visible: false)
end
def use_custom_transitions
click_element_coordinates(:service_jira_issue_transition_automatic_false, visible: false)
end
def set_transition_ids(transition_ids)
fill_element(:service_jira_issue_transition_id_field, transition_ids)
end
def click_save_changes_button
click_element :save_changes_button
click_element(:save_changes_button)
end
end
end

View file

@ -64,7 +64,7 @@ module QA
super
end
def click_element_coordinates(name)
def click_element_coordinates(name, **kwargs)
log(%Q(clicking the coordinates of :#{name}))
super

View file

@ -56,6 +56,7 @@ FactoryBot.define do
api_url { nil }
username { 'jira_username' }
password { 'jira_password' }
jira_issue_transition_automatic { false }
jira_issue_transition_id { '56-1' }
issues_enabled { false }
project_key { nil }
@ -66,7 +67,9 @@ FactoryBot.define do
before(:create) do |service, evaluator|
if evaluator.create_data
create(:jira_tracker_data, service: service,
url: evaluator.url, api_url: evaluator.api_url, jira_issue_transition_id: evaluator.jira_issue_transition_id,
url: evaluator.url, api_url: evaluator.api_url,
jira_issue_transition_automatic: evaluator.jira_issue_transition_automatic,
jira_issue_transition_id: evaluator.jira_issue_transition_id,
username: evaluator.username, password: evaluator.password, issues_enabled: evaluator.issues_enabled,
project_key: evaluator.project_key, vulnerabilities_enabled: evaluator.vulnerabilities_enabled,
vulnerabilities_issuetype: evaluator.vulnerabilities_issuetype

View file

@ -6,12 +6,13 @@ RSpec.describe 'User activates Jira', :js do
include_context 'project service activation'
include_context 'project service Jira context'
before do
stub_request(:get, test_url).to_return(body: { key: 'value' }.to_json)
end
describe 'user tests Jira Service' do
context 'when Jira connection test succeeds' do
before do
server_info = { key: 'value' }.to_json
stub_request(:get, test_url).with(basic_auth: %w(username password)).to_return(body: server_info)
visit_project_integration('Jira')
fill_form
click_test_then_save_integration(expect_test_to_fail: false)
@ -81,4 +82,68 @@ RSpec.describe 'User activates Jira', :js do
end
end
end
describe 'issue transition settings' do
it 'using custom transitions' do
visit_project_integration('Jira')
expect(page).to have_field('Enable Jira transitions', checked: false)
check 'Enable Jira transitions'
expect(page).to have_field('Move to Done', checked: true)
fill_form
choose 'Use custom transitions'
click_save_integration
within '[data-testid="issue-transition-mode"]' do
expect(page).to have_content('This field is required.')
end
fill_in 'service[jira_issue_transition_id]', with: '1, 2, 3'
click_save_integration
expect(page).to have_content('Jira settings saved and active.')
expect(project.reload.jira_service.data_fields).to have_attributes(
jira_issue_transition_automatic: false,
jira_issue_transition_id: '1, 2, 3'
)
end
it 'using automatic transitions' do
create(:jira_service, project: project, jira_issue_transition_automatic: false, jira_issue_transition_id: '1, 2, 3')
visit_project_integration('Jira')
expect(page).to have_field('Enable Jira transitions', checked: true)
expect(page).to have_field('Use custom transitions', checked: true)
expect(page).to have_field('service[jira_issue_transition_id]', with: '1, 2, 3')
choose 'Move to Done'
click_save_integration
expect(page).to have_content('Jira settings saved and active.')
expect(project.reload.jira_service.data_fields).to have_attributes(
jira_issue_transition_automatic: true,
jira_issue_transition_id: ''
)
end
it 'disabling issue transitions' do
create(:jira_service, project: project, jira_issue_transition_automatic: true, jira_issue_transition_id: '1, 2, 3')
visit_project_integration('Jira')
expect(page).to have_field('Enable Jira transitions', checked: true)
expect(page).to have_field('Move to Done', checked: true)
uncheck 'Enable Jira transitions'
click_save_integration
expect(page).to have_content('Jira settings saved and active.')
expect(project.reload.jira_service.data_fields).to have_attributes(
jira_issue_transition_automatic: false,
jira_issue_transition_id: ''
)
end
end
end

View file

@ -30,14 +30,23 @@ describe('JiraTriggerFields', () => {
const findCommentSettings = () => wrapper.find('[data-testid="comment-settings"]');
const findCommentDetail = () => wrapper.find('[data-testid="comment-detail"]');
const findCommentSettingsCheckbox = () => findCommentSettings().find(GlFormCheckbox);
const findIssueTransitionEnabled = () =>
wrapper.find('[data-testid="issue-transition-enabled"] input[type="checkbox"]');
const findIssueTransitionMode = () => wrapper.find('[data-testid="issue-transition-mode"]');
const findIssueTransitionModeRadios = () =>
findIssueTransitionMode().findAll('input[type="radio"]');
const findIssueTransitionIdsField = () =>
wrapper.find('input[type="text"][name="service[jira_issue_transition_id]"]');
describe('template', () => {
describe('initialTriggerCommit and initialTriggerMergeRequest are false', () => {
it('does not show comment settings', () => {
it('does not show trigger settings', () => {
createComponent();
expect(findCommentSettings().isVisible()).toBe(false);
expect(findCommentDetail().isVisible()).toBe(false);
expect(findIssueTransitionEnabled().exists()).toBe(false);
expect(findIssueTransitionMode().exists()).toBe(false);
});
});
@ -48,9 +57,11 @@ describe('JiraTriggerFields', () => {
});
});
it('shows comment settings', () => {
it('shows trigger settings', () => {
expect(findCommentSettings().isVisible()).toBe(true);
expect(findCommentDetail().isVisible()).toBe(false);
expect(findIssueTransitionEnabled().isVisible()).toBe(true);
expect(findIssueTransitionMode().exists()).toBe(false);
});
// As per https://vuejs.org/v2/guide/forms.html#Checkbox-1,
@ -73,13 +84,15 @@ describe('JiraTriggerFields', () => {
});
describe('initialTriggerMergeRequest is true', () => {
it('shows comment settings', () => {
it('shows trigger settings', () => {
createComponent({
initialTriggerMergeRequest: true,
});
expect(findCommentSettings().isVisible()).toBe(true);
expect(findCommentDetail().isVisible()).toBe(false);
expect(findIssueTransitionEnabled().isVisible()).toBe(true);
expect(findIssueTransitionMode().exists()).toBe(false);
});
});
@ -95,21 +108,94 @@ describe('JiraTriggerFields', () => {
});
});
it('disables checkboxes and radios if inheriting', () => {
describe('initialJiraIssueTransitionAutomatic is false, initialJiraIssueTransitionId is not set', () => {
it('selects automatic transitions when enabling transitions', () => {
createComponent({
initialTriggerCommit: true,
initialEnableComments: true,
});
const checkbox = findIssueTransitionEnabled();
expect(checkbox.element.checked).toBe(false);
checkbox.trigger('click');
return wrapper.vm.$nextTick().then(() => {
const [radio1, radio2] = findIssueTransitionModeRadios().wrappers;
expect(radio1.element.checked).toBe(true);
expect(radio2.element.checked).toBe(false);
});
});
});
describe('initialJiraIssueTransitionAutomatic is true', () => {
it('uses automatic transitions', () => {
createComponent({
initialTriggerCommit: true,
initialJiraIssueTransitionAutomatic: true,
});
expect(findIssueTransitionEnabled().element.checked).toBe(true);
const [radio1, radio2] = findIssueTransitionModeRadios().wrappers;
expect(radio1.element.checked).toBe(true);
expect(radio2.element.checked).toBe(false);
expect(findIssueTransitionIdsField().exists()).toBe(false);
});
});
describe('initialJiraIssueTransitionId is set', () => {
it('uses custom transitions', () => {
createComponent({
initialTriggerCommit: true,
initialJiraIssueTransitionId: '1, 2, 3',
});
expect(findIssueTransitionEnabled().element.checked).toBe(true);
const [radio1, radio2] = findIssueTransitionModeRadios().wrappers;
expect(radio1.element.checked).toBe(false);
expect(radio2.element.checked).toBe(true);
const field = findIssueTransitionIdsField();
expect(field.isVisible()).toBe(true);
expect(field.element).toMatchObject({
type: 'text',
value: '1, 2, 3',
});
});
});
describe('initialJiraIssueTransitionAutomatic is true, initialJiraIssueTransitionId is set', () => {
it('uses automatic transitions', () => {
createComponent({
initialTriggerCommit: true,
initialJiraIssueTransitionAutomatic: true,
initialJiraIssueTransitionId: '1, 2, 3',
});
expect(findIssueTransitionEnabled().element.checked).toBe(true);
const [radio1, radio2] = findIssueTransitionModeRadios().wrappers;
expect(radio1.element.checked).toBe(true);
expect(radio2.element.checked).toBe(false);
expect(findIssueTransitionIdsField().exists()).toBe(false);
});
});
it('disables input fields if inheriting', () => {
createComponent(
{
initialTriggerCommit: true,
initialEnableComments: true,
initialJiraIssueTransitionId: '1, 2, 3',
},
true,
);
wrapper.findAll('[type=checkbox]').wrappers.forEach((checkbox) => {
expect(checkbox.attributes('disabled')).toBe('disabled');
});
wrapper.findAll('[type=radio]').wrappers.forEach((radio) => {
expect(radio.attributes('disabled')).toBe('disabled');
wrapper.findAll('[type=text], [type=checkbox], [type=radio]').wrappers.forEach((input) => {
expect(input.attributes('disabled')).toBe('disabled');
});
});
});

View file

@ -109,6 +109,16 @@ RSpec.describe GroupsHelper do
subject
end
it 'avoids N+1 queries' do
control_count = ActiveRecord::QueryRecorder.new do
helper.group_title(nested_group)
end
expect do
helper.group_title(very_deep_nested_group)
end.not_to exceed_query_limit(control_count)
end
end
describe '#share_with_group_lock_help_text' do
@ -481,45 +491,68 @@ RSpec.describe GroupsHelper do
end
end
describe '#cached_open_group_issues_count' do
describe '#cached_issuables_count' do
let_it_be(:current_user) { create(:user) }
let_it_be(:group) { create(:group, name: 'group') }
let_it_be(:count_service) { Groups::OpenIssuesCountService }
subject { helper.cached_issuables_count(group, type: type) }
before do
allow(helper).to receive(:current_user) { current_user }
allow(count_service).to receive(:new).and_call_original
end
it 'returns all digits for count value under 1000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(999)
shared_examples 'caching issuables count' do
it 'calls the correct service class' do
subject
expect(count_service).to have_received(:new).with(group, current_user)
end
expect(helper.cached_open_group_issues_count(group)).to eq('999')
it 'returns all digits for count value under 1000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(999)
end
expect(subject).to eq('999')
end
it 'returns truncated digits for count value over 1000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(2300)
end
expect(subject).to eq('2.3k')
end
it 'returns truncated digits for count value over 10000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(12560)
end
expect(subject).to eq('12.6k')
end
it 'returns truncated digits for count value over 100000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(112560)
end
expect(subject).to eq('112.6k')
end
end
it 'returns truncated digits for count value over 1000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(2300)
end
context 'with issue type' do
let(:type) { :issues }
let(:count_service) { Groups::OpenIssuesCountService }
expect(helper.cached_open_group_issues_count(group)).to eq('2.3k')
it_behaves_like 'caching issuables count'
end
it 'returns truncated digits for count value over 10000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(12560)
end
context 'with merge request type' do
let(:type) { :merge_requests }
let(:count_service) { Groups::MergeRequestsCountService }
expect(helper.cached_open_group_issues_count(group)).to eq('12.6k')
end
it 'returns truncated digits for count value over 100000' do
allow_next_instance_of(count_service) do |service|
allow(service).to receive(:count).and_return(112560)
end
expect(helper.cached_open_group_issues_count(group)).to eq('112.6k')
it_behaves_like 'caching issuables count'
end
end
end

View file

@ -27,17 +27,31 @@ RSpec.describe ServicesHelper do
]
end
let(:jira_fields) do
[
:jira_issue_transition_automatic,
:jira_issue_transition_id
]
end
subject { helper.integration_form_data(integration) }
context 'Slack service' do
let(:integration) { build(:slack_service) }
it { is_expected.to include(*fields) }
it { is_expected.not_to include(*jira_fields) }
specify do
expect(subject[:reset_path]).to eq(helper.scoped_reset_integration_path(integration))
end
end
context 'Jira service' do
let(:integration) { build(:jira_service) }
it { is_expected.to include(*fields, *jira_fields) }
end
end
describe '#scoped_reset_integration_path' do

View file

@ -82,11 +82,8 @@ RSpec.describe JiraService do
subject(:fields) { service.fields }
it 'includes transition help link' do
transition_id_field = fields.find { |field| field[:name] == 'jira_issue_transition_id' }
expect(transition_id_field[:title]).to eq('Jira workflow transition IDs')
expect(transition_id_field[:help]).to include('/help/user/project/integrations/jira')
it 'returns custom fields' do
expect(fields.pluck(:name)).to eq(%w[url api_url username password])
end
end
@ -460,10 +457,10 @@ RSpec.describe JiraService do
end
context 'with options' do
let(:issue_url) { "#{url}/rest/api/2/issue/#{issue_key}?expand=renderedFields" }
let(:issue_url) { "#{url}/rest/api/2/issue/#{issue_key}?expand=renderedFields,transitions" }
it 'calls the Jira API with the options to get the issue' do
jira_service.find_issue(issue_key, rendered_fields: true)
jira_service.find_issue(issue_key, rendered_fields: true, transitions: true)
expect(WebMock).to have_requested(:get, issue_url)
end
@ -494,7 +491,7 @@ RSpec.describe JiraService do
end
before do
allow(jira_service).to receive_messages(jira_issue_transition_id: '999')
jira_service.jira_issue_transition_id = '999'
# These stubs are needed to test JiraService#close_issue.
# We close the issue then do another request to API to check if it got closed.
@ -505,7 +502,7 @@ RSpec.describe JiraService do
allow(closed_issue).to receive(:resolution).and_return(true)
allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue)
allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return('JIRA-123')
allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return(issue_key)
allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
WebMock.stub_request(:get, issue_url).with(basic_auth: %w(jira-username jira-password))
@ -664,6 +661,61 @@ RSpec.describe JiraService do
).once
end
context 'when custom transition IDs are blank' do
before do
jira_service.jira_issue_transition_id = ''
end
it 'does not transition the issue' do
close_issue
expect(WebMock).not_to have_requested(:post, transitions_url)
end
end
context 'when using automatic issue transitions' do
let(:transitions) do
[
{ id: '1' },
{ id: '2', to: { statusCategory: { key: 'new' } } },
{ id: '3', to: { statusCategory: { key: 'done' } } },
{ id: '4', to: { statusCategory: { key: 'done' } } }
]
end
before do
jira_service.jira_issue_transition_automatic = true
close_issue
end
it 'uses the next transition with a status category of done' do
expect(WebMock).to have_requested(:post, transitions_url).with(
body: /"id":"3"/
).once
end
context 'when no done transition is available' do
let(:transitions) do
[
{ id: '1', to: { statusCategory: { key: 'new' } } }
]
end
it 'does not attempt to transition' do
expect(WebMock).not_to have_requested(:post, transitions_url)
end
end
context 'when no valid transitions are returned' do
let(:transitions) { 'foo' }
it 'does not attempt to transition' do
expect(WebMock).not_to have_requested(:post, transitions_url)
end
end
end
context 'when using multiple transition ids' do
before do
allow(jira_service).to receive_messages(jira_issue_transition_id: '1,2,3')
@ -902,4 +954,22 @@ RSpec.describe JiraService do
end
end
end
describe '#issue_transition_enabled?' do
it 'returns true if automatic transitions are enabled' do
jira_service.jira_issue_transition_automatic = true
expect(jira_service.issue_transition_enabled?).to be(true)
end
it 'returns true if custom transitions are set' do
jira_service.jira_issue_transition_id = '1, 2, 3'
expect(jira_service.issue_transition_enabled?).to be(true)
end
it 'returns false if automatic and custom transitions are disabled' do
expect(jira_service.issue_transition_enabled?).to be(false)
end
end
end

View file

@ -1413,6 +1413,29 @@ RSpec.describe API::Internal::Base do
end
end
describe 'GET /internal/geo_proxy' do
subject { get api('/internal/geo_proxy'), params: { secret_token: secret_token } }
context 'with valid auth' do
it 'returns empty data' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_empty
end
end
context 'with invalid auth' do
let(:secret_token) { 'invalid_token' }
it 'returns unauthorized' do
subject
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
end
def lfs_auth_project(project)
post(
api("/internal/lfs_authenticate"),

View file

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::MergeRequestsCountService, :use_clean_rails_memory_store_caching do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group, :public)}
let_it_be(:project) { create(:project, :repository, namespace: group) }
let_it_be(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
subject { described_class.new(group, user) }
describe '#relation_for_count' do
before do
group.add_reporter(user)
allow(MergeRequestsFinder).to receive(:new).and_call_original
end
it 'uses the MergeRequestsFinder to scope merge requests' do
expect(MergeRequestsFinder)
.to receive(:new)
.with(user, group_id: group.id, state: 'opened', non_archived: true, include_subgroups: true)
subject.count
end
end
it_behaves_like 'a counter caching service with threshold'
end

View file

@ -54,53 +54,7 @@ RSpec.describe Groups::OpenIssuesCountService, :use_clean_rails_memory_store_cac
end
end
context 'with different cache values' do
let(:public_count_key) { subject.cache_key(described_class::PUBLIC_COUNT_KEY) }
let(:under_threshold) { described_class::CACHED_COUNT_THRESHOLD - 1 }
let(:over_threshold) { described_class::CACHED_COUNT_THRESHOLD + 1 }
context 'when cache is empty' do
before do
Rails.cache.delete(public_count_key)
end
it 'refreshes cache if value over threshold' do
allow(subject).to receive(:uncached_count).and_return(over_threshold)
expect(subject.count).to eq(over_threshold)
expect(Rails.cache.read(public_count_key)).to eq(over_threshold)
end
it 'does not refresh cache if value under threshold' do
allow(subject).to receive(:uncached_count).and_return(under_threshold)
expect(subject.count).to eq(under_threshold)
expect(Rails.cache.read(public_count_key)).to be_nil
end
end
context 'when cached count is under the threshold value' do
before do
Rails.cache.write(public_count_key, under_threshold)
end
it 'does not refresh cache' do
expect(Rails.cache).not_to receive(:write)
expect(subject.count).to eq(under_threshold)
end
end
context 'when cached count is over the threshold value' do
before do
Rails.cache.write(public_count_key, over_threshold)
end
it 'does not refresh cache' do
expect(Rails.cache).not_to receive(:write)
expect(subject.count).to eq(over_threshold)
end
end
end
it_behaves_like 'a counter caching service with threshold'
end
end
end

View file

@ -166,20 +166,6 @@ RSpec.describe MergeRequests::MergeService do
service.execute(merge_request)
end
context 'when jira_issue_transition_id is not present' do
before do
allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(nil)
end
it 'does not close issue' do
jira_tracker.update!(jira_issue_transition_id: nil)
expect_any_instance_of(JiraService).not_to receive(:transition_issue)
service.execute(merge_request)
end
end
context 'wrong issue markdown' do
it 'does not close issues on Jira issue tracker' do
jira_issue = ExternalIssue.new('#JIRA-123', project)

View file

@ -1,8 +1,8 @@
# frozen_string_literal: true
RSpec.shared_context 'project service Jira context' do
let(:url) { 'http://jira.example.com' }
let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' }
let(:url) { 'https://jira.example.com' }
let(:test_url) { 'https://jira.example.com/rest/api/2/serverInfo' }
def fill_form(disable: false)
click_active_checkbox if disable
@ -10,6 +10,5 @@ RSpec.shared_context 'project service Jira context' do
fill_in 'service_url', with: url
fill_in 'service_username', with: 'username'
fill_in 'service_password', with: 'password'
fill_in 'service_jira_issue_transition_id', with: '25'
end
end

View file

@ -15,7 +15,10 @@ RSpec.shared_context 'project service activation' do
def visit_project_integration(name)
visit_project_integrations
click_link(name)
within('#content-body') do
click_link(name)
end
end
def click_active_checkbox

View file

@ -0,0 +1,55 @@
# frozen_string_literal: true
# The calling spec should use `:use_clean_rails_memory_store_caching`
# when including this shared example. E.g.:
#
# describe MyCountService, :use_clean_rails_memory_store_caching do
# it_behaves_like 'a counter caching service with threshold'
# end
RSpec.shared_examples 'a counter caching service with threshold' do
let(:cache_key) { subject.cache_key }
let(:under_threshold) { described_class::CACHED_COUNT_THRESHOLD - 1 }
let(:over_threshold) { described_class::CACHED_COUNT_THRESHOLD + 1 }
context 'when cache is empty' do
before do
Rails.cache.delete(cache_key)
end
it 'refreshes cache if value over threshold' do
allow(subject).to receive(:uncached_count).and_return(over_threshold)
expect(subject.count).to eq(over_threshold)
expect(Rails.cache.read(cache_key)).to eq(over_threshold)
end
it 'does not refresh cache if value under threshold' do
allow(subject).to receive(:uncached_count).and_return(under_threshold)
expect(subject.count).to eq(under_threshold)
expect(Rails.cache.read(cache_key)).to be_nil
end
end
context 'when cached count is under the threshold value' do
before do
Rails.cache.write(cache_key, under_threshold)
end
it 'does not refresh cache' do
expect(Rails.cache).not_to receive(:write)
expect(subject.count).to eq(under_threshold)
end
end
context 'when cached count is over the threshold value' do
before do
Rails.cache.write(cache_key, over_threshold)
end
it 'does not refresh cache' do
expect(Rails.cache).not_to receive(:write)
expect(subject.count).to eq(over_threshold)
end
end
end