Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-22 15:08:15 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-22 15:08:15 +0300
commit808c799a67a1cf2489a343a6976f55c74aec398b (patch)
tree4902ff7dbcdd7f3a9dde5ab3bd20dba94835a8a7
parent4a3ba3e5f261eb09e6b2b4fd44373e7a1c454a72 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_key_field.vue2
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue2
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss2
-rw-r--r--app/assets/stylesheets/components/dashboard_skeleton.scss6
-rw-r--r--app/assets/stylesheets/framework/animations.scss8
-rw-r--r--app/assets/stylesheets/framework/common.scss3
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/gitlab_theme.scss4
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss2
-rw-r--r--app/assets/stylesheets/framework/tables.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss10
-rw-r--r--app/assets/stylesheets/pages/labels.scss4
-rw-r--r--app/assets/stylesheets/pages/note_form.scss4
-rw-r--r--app/assets/stylesheets/pages/notes.scss6
-rw-r--r--app/assets/stylesheets/pages/projects.scss2
-rw-r--r--app/graphql/resolvers/projects/jira_projects_resolver.rb69
-rw-r--r--app/graphql/types/projects/services/jira_project_type.rb21
-rw-r--r--app/graphql/types/projects/services/jira_service_type.rb11
-rw-r--r--app/helpers/active_sessions_helper.rb2
-rw-r--r--app/models/project_services/jira_service.rb20
-rw-r--r--app/serializers/diff_file_base_entity.rb6
-rw-r--r--app/serializers/diff_file_metadata_entity.rb5
-rw-r--r--app/services/ci/authorize_job_artifact_service.rb53
-rw-r--r--app/services/event_create_service.rb26
-rw-r--r--app/services/resource_events/change_state_service.rb36
-rw-r--r--app/services/resource_events/merge_into_notes_service.rb5
-rw-r--r--app/services/resource_events/synthetic_state_notes_builder_service.rb20
-rw-r--r--app/services/system_notes/issuables_service.rb12
-rw-r--r--app/views/admin/users/_user_detail.html.haml2
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml2
-rw-r--r--app/views/shared/milestones/_form_dates.html.haml12
-rw-r--r--app/workers/concerns/gitlab/jira_import/import_worker.rb1
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_form_d.yml5
-rw-r--r--changelogs/unreleased/jira-projects-api-wrapper.yml5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql82
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json238
-rw-r--r--doc/api/graphql/reference/index.md9
-rw-r--r--doc/development/feature_flags/controls.md59
-rw-r--r--doc/development/integrations/secure.md4
-rw-r--r--lib/api/runner.rb12
-rw-r--r--lib/gitlab/diff/file.rb8
-rw-r--r--locale/gitlab.pot18
-rw-r--r--spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb68
-rw-r--r--spec/graphql/types/projects/jira_project_type_spec.rb11
-rw-r--r--spec/graphql/types/projects/jira_service_type_spec.rb2
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb12
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb28
-rw-r--r--spec/models/merge_request_spec.rb36
-rw-r--r--spec/models/project_services/jira_service_spec.rb81
-rw-r--r--spec/requests/api/graphql/project/jira_projects_spec.rb114
-rw-r--r--spec/requests/api/runner_spec.rb25
-rw-r--r--spec/services/event_create_service_spec.rb7
-rw-r--r--spec/services/issues/close_service_spec.rb23
-rw-r--r--spec/services/merge_requests/close_service_spec.rb65
-rw-r--r--spec/services/merge_requests/ff_merge_service_spec.rb99
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb25
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb164
-rw-r--r--spec/services/merge_requests/reopen_service_spec.rb22
-rw-r--r--spec/services/resource_events/change_state_service_spec.rb39
-rw-r--r--spec/services/system_notes/issuables_service_spec.rb15
-rw-r--r--spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb104
-rw-r--r--spec/support/shared_examples/features/discussion_comments_shared_example.rb2
-rw-r--r--spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb2
66 files changed, 1519 insertions, 235 deletions
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 2f70731b8aa..9a717a55ed1 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-8.31.0
+8.32.0
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_key_field.vue b/app/assets/javascripts/ci_variable_list/components/ci_key_field.vue
index f5c2cc57f3f..c15d638d92b 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_key_field.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_key_field.vue
@@ -154,7 +154,7 @@ export default {
v-for="(result, i) in results"
:key="i"
role="option"
- :class="{ 'gl-bg-gray-100': i === arrowCounter }"
+ :class="{ 'gl-bg-gray-50': i === arrowCounter }"
:aria-selected="i === arrowCounter"
>
<gl-button tabindex="-1" class="btn-transparent pl-2" @click="selectToken(result)">{{
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 1b8c75202fb..f9132c3307e 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -150,7 +150,7 @@ export default {
class="metadata align-items-md-center d-flex flex-grow-1 flex-shrink-0 flex-wrap justify-content-md-between"
>
<item-actions v-if="isGroup" :group="group" :parent-group="parentGroup" />
- <item-stats :item="group" class="group-stats prepend-top-2 d-none d-md-flex" />
+ <item-stats :item="group" class="group-stats gl-mt-2 d-none d-md-flex" />
</div>
</div>
</div>
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 1c15400542a..f819c2dbce1 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -111,7 +111,7 @@ kbd {
code {
padding: 2px 4px;
color: $code-color;
- background-color: $gray-100;
+ background-color: $gray-50;
border-radius: $border-radius-default;
.code > & {
diff --git a/app/assets/stylesheets/components/dashboard_skeleton.scss b/app/assets/stylesheets/components/dashboard_skeleton.scss
index ce33aa94df3..64091201221 100644
--- a/app/assets/stylesheets/components/dashboard_skeleton.scss
+++ b/app/assets/stylesheets/components/dashboard_skeleton.scss
@@ -67,10 +67,10 @@
background-repeat: no-repeat;
background-size: cover;
background-image: linear-gradient(to right,
- $gray-100 0%,
+ $gray-50 0%,
$gray-10 20%,
- $gray-100 40%,
- $gray-100 100%);
+ $gray-50 40%,
+ $gray-50 100%);
border-radius: $gl-padding;
height: $gl-padding;
margin-top: -$gl-padding-8;
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 13174687e5d..566ab5bc5c4 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -177,7 +177,7 @@ a {
[class^='skeleton-line-'] {
position: relative;
- background-color: $gray-100;
+ background-color: $gray-50;
height: 10px;
overflow: hidden;
@@ -192,10 +192,10 @@ a {
background-repeat: no-repeat;
background-size: cover;
background-image: linear-gradient(to right,
- $gray-100 0%,
+ $gray-50 0%,
$gray-10 20%,
- $gray-100 40%,
- $gray-100 100%);
+ $gray-50 40%,
+ $gray-50 100%);
height: 10px;
}
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 90a6f471374..158d2133f13 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -396,7 +396,6 @@ img.emoji {
🚨 Do not use these classes — they are deprecated and being removed. 🚨
See https://gitlab.com/gitlab-org/gitlab/-/issues/217418 for more details.
**/
-.prepend-top-2 { margin-top: 2px; }
.prepend-top-4 { margin-top: $gl-padding-4; }
.prepend-top-5 { margin-top: 5px; }
.prepend-top-8 { margin-top: $grid-size; }
@@ -576,7 +575,7 @@ img.emoji {
bottom: 40px;
right: 40px;
font-size: $gl-font-size-small;
- background: $gray-100;
+ background: $gray-50;
width: 200px;
border-radius: 24px;
box-shadow: 0 2px 4px $issue-boards-card-shadow;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 1df9818a877..8c75bff2741 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -940,7 +940,7 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
position: absolute;
top: 13px;
right: 25px;
- color: $gray-100;
+ color: $gray-50;
}
}
@@ -979,7 +979,7 @@ header.header-content .dropdown-menu.frequent-items-dropdown-menu {
&:hover {
.frequent-items-item-avatar-container .avatar {
- border-color: $gray-100;
+ border-color: $gray-50;
}
}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 7ee3e68ceea..eef6d9031f8 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -480,7 +480,7 @@ span.idiff {
padding-bottom: $gl-padding;
.discussion-reply-holder {
- border-bottom: 1px solid $gray-100;
+ border-bottom: 1px solid $gray-50;
border-radius: 0;
}
}
diff --git a/app/assets/stylesheets/framework/gitlab_theme.scss b/app/assets/stylesheets/framework/gitlab_theme.scss
index 6a2f36d2509..4b50bd2d88b 100644
--- a/app/assets/stylesheets/framework/gitlab_theme.scss
+++ b/app/assets/stylesheets/framework/gitlab_theme.scss
@@ -314,12 +314,12 @@ body {
$gray-800,
$gray-700,
$gray-700,
- $gray-100,
+ $gray-50,
$gray-700
);
.navbar-gitlab {
- background-color: $gray-100;
+ background-color: $gray-50;
box-shadow: 0 1px 0 0 $border-color;
.logo-text svg {
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 97698fefbee..2a97009e605 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -123,7 +123,7 @@
.markdown-area {
border-radius: 0;
background: $white;
- border: 1px solid $gray-100;
+ border: 1px solid $gray-50;
min-height: 140px;
max-height: 500px;
padding: 5px;
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 5739f048e86..5bc2874ea05 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -47,7 +47,7 @@ table {
}
th {
- @include gl-bg-gray-100;
+ @include gl-bg-gray-50;
border-bottom: 0;
&.wide {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index ac4d431ea57..1253ff698e0 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -165,7 +165,7 @@ $red-950: #4b140b;
$gray-10: #fafafa;
$gray-50: #f0f0f0;
-$gray-100: #f2f2f2;
+$gray-100: #dbdbdb;
$gray-200: #dfdfdf;
$gray-300: #ccc;
$gray-400: #bababa;
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 295af0311ae..ddfdf8d0553 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -387,7 +387,7 @@ $ide-commit-header-height: 48px;
&:hover,
&:focus {
- background: var(--ide-background, $gray-100);
+ background: var(--ide-background, $gray-50);
outline: 0;
}
@@ -559,7 +559,7 @@ $ide-commit-header-height: 48px;
&:hover {
color: var(--ide-text-color, $gl-text-color);
- background-color: var(--ide-background-hover, $gray-100);
+ background-color: var(--ide-background-hover, $gray-50);
}
&:focus {
@@ -1041,7 +1041,7 @@ $ide-commit-header-height: 48px;
.ide-entry-dropdown-toggle {
padding: $gl-padding-4;
color: var(--ide-text-color, $gl-text-color);
- background-color: var(--ide-background, $gray-100);
+ background-color: var(--ide-background, $gray-50);
&:hover {
background-color: var(--ide-file-row-btn-hover-background, $gray-200);
@@ -1142,12 +1142,12 @@ $ide-commit-header-height: 48px;
}
.file-row.is-active {
- background: var(--ide-background, $gray-100);
+ background: var(--ide-background, $gray-50);
}
.file-row:hover,
.file-row:focus {
- background: var(--ide-background, $gray-100);
+ background: var(--ide-background, $gray-50);
.ide-new-btn {
display: block;
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 22c1cb127cd..c3bac053a0a 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -86,7 +86,7 @@
justify-content: space-between;
padding: $gl-padding;
border-radius: $border-radius-default;
- border: 1px solid $gray-100;
+ border: 1px solid $gray-50;
&:last-child {
margin-bottom: 0;
@@ -276,7 +276,7 @@
}
.label-badge-gray {
- background-color: $gray-100;
+ background-color: $gray-50;
}
.label-links {
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 57afe45a74b..c3f3dbc223b 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -253,11 +253,11 @@ table {
background-color: $gray-light;
border-radius: 0 0 3px 3px;
padding: $gl-padding;
- border-top: 1px solid $gray-100;
+ border-top: 1px solid $gray-50;
+ .new-note {
background-color: $gray-light;
- border-top: 1px solid $gray-100;
+ border-top: 1px solid $gray-50;
}
&.is-replying {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index bed147aa3a7..e8cdfd717c0 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -5,7 +5,7 @@ $note-form-margin-left: 72px;
@mixin vertical-line($left) {
&::before {
content: '';
- border-left: 2px solid $gray-100;
+ border-left: 2px solid $gray-50;
position: absolute;
top: 0;
bottom: 0;
@@ -83,8 +83,8 @@ $note-form-margin-left: 72px;
.replies-toggle {
background-color: $gray-light;
padding: $gl-padding-8 $gl-padding;
- border-top: 1px solid $gray-100;
- border-bottom: 1px solid $gray-100;
+ border-top: 1px solid $gray-50;
+ border-bottom: 1px solid $gray-50;
.collapse-replies-btn:hover {
color: $blue-600;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index c0a1cf10fe4..438f6c2546e 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -396,7 +396,7 @@
margin-right: $gl-padding-4;
margin-bottom: $gl-padding-4;
color: $gl-text-color-secondary;
- background-color: $gray-100;
+ background-color: $gray-50;
line-height: $gl-btn-line-height;
&:hover {
diff --git a/app/graphql/resolvers/projects/jira_projects_resolver.rb b/app/graphql/resolvers/projects/jira_projects_resolver.rb
new file mode 100644
index 00000000000..7004976adab
--- /dev/null
+++ b/app/graphql/resolvers/projects/jira_projects_resolver.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Projects
+ class JiraProjectsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ argument :name,
+ GraphQL::STRING_TYPE,
+ required: false,
+ description: 'Project name or key'
+
+ def resolve(name: nil, **args)
+ authorize!(project)
+
+ response, start_cursor, end_cursor = jira_projects(name: name, **compute_pagination_params(args))
+ end_cursor = nil if !!response.payload[:is_last]
+
+ response.success? ? Gitlab::Graphql::ExternallyPaginatedArray.new(start_cursor, end_cursor, *response.payload[:projects]) : nil
+ end
+
+ def authorized_resource?(project)
+ Feature.enabled?(:jira_issue_import, project) && Ability.allowed?(context[:current_user], :admin_project, project)
+ end
+
+ private
+
+ alias_method :jira_service, :object
+
+ def project
+ jira_service&.project
+ end
+
+ def compute_pagination_params(params)
+ after_cursor = Base64.decode64(params[:after].to_s)
+ before_cursor = Base64.decode64(params[:before].to_s)
+
+ # differentiate between 0 cursor and nil or invalid cursor that decodes into zero.
+ after_index = after_cursor.to_i == 0 && after_cursor != "0" ? nil : after_cursor.to_i
+ before_index = before_cursor.to_i == 0 && before_cursor != "0" ? nil : before_cursor.to_i
+
+ if after_index.present? && before_index.present?
+ if after_index >= before_index
+ { start_at: 0, limit: 0 }
+ else
+ { start_at: after_index + 1, limit: before_index - after_index - 1 }
+ end
+ elsif after_index.present?
+ { start_at: after_index + 1, limit: nil }
+ elsif before_index.present?
+ { start_at: 0, limit: before_index - 1 }
+ else
+ { start_at: 0, limit: nil }
+ end
+ end
+
+ def jira_projects(name:, start_at:, limit:)
+ args = { query: name, start_at: start_at, limit: limit }.compact
+
+ response = jira_service&.jira_projects(args)
+ projects = response.payload[:projects]
+ start_cursor = start_at == 0 ? nil : Base64.encode64((start_at - 1).to_s)
+ end_cursor = Base64.encode64((start_at + projects.size - 1).to_s)
+
+ [response, start_cursor, end_cursor]
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/projects/services/jira_project_type.rb b/app/graphql/types/projects/services/jira_project_type.rb
new file mode 100644
index 00000000000..ccf9107f398
--- /dev/null
+++ b/app/graphql/types/projects/services/jira_project_type.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Types
+ module Projects
+ module Services
+ # rubocop:disable Graphql/AuthorizeTypes
+ class JiraProjectType < BaseObject
+ graphql_name 'JiraProject'
+
+ field :key, GraphQL::STRING_TYPE, null: false,
+ description: 'Key of the Jira project'
+ field :project_id, GraphQL::INT_TYPE, null: false,
+ description: 'ID of the Jira project',
+ method: :id
+ field :name, GraphQL::STRING_TYPE, null: true,
+ description: 'Name of the Jira project'
+ end
+ # rubocop:enable Graphql/AuthorizeTypes
+ end
+ end
+end
diff --git a/app/graphql/types/projects/services/jira_service_type.rb b/app/graphql/types/projects/services/jira_service_type.rb
index 4fd9e61f5a4..e81963f752d 100644
--- a/app/graphql/types/projects/services/jira_service_type.rb
+++ b/app/graphql/types/projects/services/jira_service_type.rb
@@ -9,9 +9,14 @@ module Types
implements(Types::Projects::ServiceType)
authorize :admin_project
- # This is a placeholder for now for the actuall implementation of the JiraServiceType
- # Here we will want to expose a field with jira_projects fetched through Jira Rest API
- # MR implementing it https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28190
+
+ field :projects,
+ Types::Projects::Services::JiraProjectType.connection_type,
+ null: true,
+ connection: false,
+ extensions: [Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension],
+ description: 'List of Jira projects fetched through Jira REST API',
+ resolver: Resolvers::Projects::JiraProjectsResolver
end
end
end
diff --git a/app/helpers/active_sessions_helper.rb b/app/helpers/active_sessions_helper.rb
index 84aa1160f12..8fb23f99cb3 100644
--- a/app/helpers/active_sessions_helper.rb
+++ b/app/helpers/active_sessions_helper.rb
@@ -20,6 +20,6 @@ module ActiveSessionsHelper
'monitor-o'
end
- sprite_icon(icon_name, size: 16, css_class: 'prepend-top-2')
+ sprite_icon(icon_name, size: 16, css_class: 'gl-mt-2')
end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 53da874ede8..c71c939f452 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -6,6 +6,8 @@ class JiraService < IssueTrackerService
include ApplicationHelper
include ActionView::Helpers::AssetUrlHelper
+ PROJECTS_PER_PAGE = 50
+
validates :url, public_url: true, presence: true, if: :activated?
validates :api_url, public_url: true, allow_blank: true
validates :username, presence: true, if: :activated?
@@ -224,8 +226,26 @@ class JiraService < IssueTrackerService
true
end
+ def jira_projects(query: '', limit: PROJECTS_PER_PAGE, start_at: 0)
+ return ServiceResponse.success(payload: { projects: [], is_last: true }) if limit.to_i <= 0
+
+ response = jira_request { client.get(projects_url(query: query, limit: limit.to_i, start_at: start_at.to_i)) }
+
+ return ServiceResponse.error(message: @error.message) if @error.present?
+ return ServiceResponse.success(payload: { projects: [] }) unless response['values'].present?
+
+ projects = response['values'].map { |v| JIRA::Resource::Project.build(client, v) }
+
+ ServiceResponse.success(payload: { projects: projects, is_last: response['isLast'] })
+ end
+
private
+ def projects_url(query:, limit:, start_at:)
+ '/rest/api/2/project/search?query=%{query}&maxResults=%{limit}&startAt=%{start_at}' %
+ { query: CGI.escape(query.to_s), limit: limit, start_at: start_at }
+ end
+
def test_settings
return unless client_url.present?
diff --git a/app/serializers/diff_file_base_entity.rb b/app/serializers/diff_file_base_entity.rb
index 8c2b3a65d57..33eb33d314b 100644
--- a/app/serializers/diff_file_base_entity.rb
+++ b/app/serializers/diff_file_base_entity.rb
@@ -67,10 +67,8 @@ class DiffFileBaseEntity < Grape::Entity
end
end
- expose :file_hash do |diff_file|
- Digest::SHA1.hexdigest(diff_file.file_path)
- end
-
+ expose :file_identifier_hash
+ expose :file_hash
expose :file_path
expose :old_path
expose :new_path
diff --git a/app/serializers/diff_file_metadata_entity.rb b/app/serializers/diff_file_metadata_entity.rb
index 05280518f39..460f4967e99 100644
--- a/app/serializers/diff_file_metadata_entity.rb
+++ b/app/serializers/diff_file_metadata_entity.rb
@@ -7,7 +7,6 @@ class DiffFileMetadataEntity < Grape::Entity
expose :old_path
expose :new_file?, as: :new_file
expose :deleted_file?, as: :deleted_file
- expose :file_hash do |diff_file|
- Digest::SHA1.hexdigest(diff_file.file_path)
- end
+ expose :file_identifier_hash
+ expose :file_hash
end
diff --git a/app/services/ci/authorize_job_artifact_service.rb b/app/services/ci/authorize_job_artifact_service.rb
new file mode 100644
index 00000000000..cfcbdcae450
--- /dev/null
+++ b/app/services/ci/authorize_job_artifact_service.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Ci
+ class AuthorizeJobArtifactService
+ include Gitlab::Utils::StrongMemoize
+
+ # Max size of the zipped LSIF artifact
+ LSIF_ARTIFACT_MAX_SIZE = 20.megabytes
+ LSIF_ARTIFACT_TYPE = 'lsif'
+
+ def initialize(job, params, max_size:)
+ @job = job
+ @max_size = max_size
+ @size = params[:filesize]
+ @type = params[:artifact_type].to_s
+ end
+
+ def forbidden?
+ lsif? && !code_navigation_enabled?
+ end
+
+ def too_large?
+ size && max_size <= size.to_i
+ end
+
+ def headers
+ default_headers = JobArtifactUploader.workhorse_authorize(has_length: false, maximum_size: max_size)
+ default_headers.tap do |h|
+ h[:ProcessLsif] = true if lsif? && code_navigation_enabled?
+ end
+ end
+
+ private
+
+ attr_reader :job, :size, :type
+
+ def code_navigation_enabled?
+ strong_memoize(:code_navigation_enabled) do
+ Feature.enabled?(:code_navigation)
+ end
+ end
+
+ def lsif?
+ strong_memoize(:lsif) do
+ type == LSIF_ARTIFACT_TYPE
+ end
+ end
+
+ def max_size
+ lsif? ? LSIF_ARTIFACT_MAX_SIZE : @max_size.to_i
+ end
+ end
+end
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 522f36cda46..c879c7432a9 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -11,30 +11,44 @@ class EventCreateService
IllegalActionError = Class.new(StandardError)
def open_issue(issue, current_user)
+ create_resource_event(issue, current_user, :opened)
+
create_record_event(issue, current_user, Event::CREATED)
end
def close_issue(issue, current_user)
+ create_resource_event(issue, current_user, :closed)
+
create_record_event(issue, current_user, Event::CLOSED)
end
def reopen_issue(issue, current_user)
+ create_resource_event(issue, current_user, :reopened)
+
create_record_event(issue, current_user, Event::REOPENED)
end
def open_mr(merge_request, current_user)
+ create_resource_event(merge_request, current_user, :opened)
+
create_record_event(merge_request, current_user, Event::CREATED)
end
def close_mr(merge_request, current_user)
+ create_resource_event(merge_request, current_user, :closed)
+
create_record_event(merge_request, current_user, Event::CLOSED)
end
def reopen_mr(merge_request, current_user)
+ create_resource_event(merge_request, current_user, :reopened)
+
create_record_event(merge_request, current_user, Event::REOPENED)
end
def merge_mr(merge_request, current_user)
+ create_resource_event(merge_request, current_user, :merged)
+
create_record_event(merge_request, current_user, Event::MERGED)
end
@@ -157,6 +171,18 @@ class EventCreateService
Event.create!(attributes)
end
+
+ def create_resource_event(issuable, current_user, status)
+ return unless state_change_tracking_enabled?(issuable)
+
+ ResourceEvents::ChangeStateService.new(resource: issuable, user: current_user)
+ .execute(status)
+ end
+
+ def state_change_tracking_enabled?(issuable)
+ issuable&.respond_to?(:resource_state_events) &&
+ ::Feature.enabled?(:track_resource_state_change_events, issuable&.project)
+ end
end
EventCreateService.prepend_if_ee('EE::EventCreateService')
diff --git a/app/services/resource_events/change_state_service.rb b/app/services/resource_events/change_state_service.rb
new file mode 100644
index 00000000000..8beb76d8aee
--- /dev/null
+++ b/app/services/resource_events/change_state_service.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module ResourceEvents
+ class ChangeStateService
+ attr_reader :resource, :user
+
+ def initialize(user:, resource:)
+ @user, @resource = user, resource
+ end
+
+ def execute(state)
+ ResourceStateEvent.create(
+ user: user,
+ issue: issue,
+ merge_request: merge_request,
+ state: ResourceStateEvent.states[state],
+ created_at: Time.zone.now)
+
+ resource.expire_note_etag_cache
+ end
+
+ private
+
+ def issue
+ return unless resource.is_a?(Issue)
+
+ resource
+ end
+
+ def merge_request
+ return unless resource.is_a?(MergeRequest)
+
+ resource
+ end
+ end
+end
diff --git a/app/services/resource_events/merge_into_notes_service.rb b/app/services/resource_events/merge_into_notes_service.rb
index 4aa9bb80229..122bcb8550f 100644
--- a/app/services/resource_events/merge_into_notes_service.rb
+++ b/app/services/resource_events/merge_into_notes_service.rb
@@ -11,7 +11,8 @@ module ResourceEvents
SYNTHETIC_NOTE_BUILDER_SERVICES = [
SyntheticLabelNotesBuilderService,
- SyntheticMilestoneNotesBuilderService
+ SyntheticMilestoneNotesBuilderService,
+ SyntheticStateNotesBuilderService
].freeze
attr_reader :resource, :current_user, :params
@@ -23,7 +24,7 @@ module ResourceEvents
end
def execute(notes = [])
- (notes + synthetic_notes).sort_by { |n| n.created_at }
+ (notes + synthetic_notes).sort_by(&:created_at)
end
private
diff --git a/app/services/resource_events/synthetic_state_notes_builder_service.rb b/app/services/resource_events/synthetic_state_notes_builder_service.rb
new file mode 100644
index 00000000000..763134d98d8
--- /dev/null
+++ b/app/services/resource_events/synthetic_state_notes_builder_service.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module ResourceEvents
+ class SyntheticStateNotesBuilderService < BaseSyntheticNotesBuilderService
+ private
+
+ def synthetic_notes
+ state_change_events.map do |event|
+ StateNote.from_event(event, resource: resource, resource_parent: resource_parent)
+ end
+ end
+
+ def state_change_events
+ return [] unless resource.respond_to?(:resource_state_events)
+
+ events = resource.resource_state_events.includes(user: :status) # rubocop: disable CodeReuse/ActiveRecord
+ since_fetch_at(events)
+ end
+ end
+end
diff --git a/app/services/system_notes/issuables_service.rb b/app/services/system_notes/issuables_service.rb
index 275c64bea89..fcb7fc908f1 100644
--- a/app/services/system_notes/issuables_service.rb
+++ b/app/services/system_notes/issuables_service.rb
@@ -225,7 +225,12 @@ module SystemNotes
action = status == 'reopened' ? 'opened' : status
- create_note(NoteSummary.new(noteable, project, author, body, action: action))
+ # A state event which results in a synthetic note will be
+ # created by EventCreateService if change event tracking
+ # is enabled.
+ unless state_change_tracking_enabled?
+ create_note(NoteSummary.new(noteable, project, author, body, action: action))
+ end
end
# Check if a cross reference to a noteable from a mentioner already exists
@@ -318,6 +323,11 @@ module SystemNotes
def self.cross_reference?(note_text)
note_text =~ /\A#{cross_reference_note_prefix}/i
end
+
+ def state_change_tracking_enabled?
+ noteable.respond_to?(:resource_state_events) &&
+ ::Feature.enabled?(:track_resource_state_change_events, noteable.project)
+ end
end
end
diff --git a/app/views/admin/users/_user_detail.html.haml b/app/views/admin/users/_user_detail.html.haml
index 3cc3fc6fa92..a29f369b9de 100644
--- a/app/views/admin/users/_user_detail.html.haml
+++ b/app/views/admin/users/_user_detail.html.haml
@@ -3,7 +3,7 @@
= image_tag avatar_icon_for_user(user), class: 'avatar s32 d-none d-md-flex', alt: _('Avatar for %{name}') % { name: sanitize_name(user.name) }
.row-main-content
.row-title.str-truncated-100
- = image_tag avatar_icon_for_user(user), class: 'avatar s16 d-xs-flex d-md-none mr-1 prepend-top-2', alt: _('Avatar for %{name}') % { name: sanitize_name(user.name) }
+ = image_tag avatar_icon_for_user(user), class: 'avatar s16 d-xs-flex d-md-none mr-1 gl-mt-2', alt: _('Avatar for %{name}') % { name: sanitize_name(user.name) }
= link_to user.name, admin_user_path(user), class: 'text-plain js-user-link', data: { user_id: user.id, qa_selector: 'username_link' }
= render_if_exists 'admin/users/user_listing_note', user: user
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 7d9924719a2..7618b53e3ae 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -6,7 +6,7 @@
= current_user.name
= current_user.to_reference
- if current_user.status
- .user-status.d-flex.align-items-center.prepend-top-2.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } }
+ .user-status.d-flex.align-items-center.gl-mt-2.has-tooltip{ title: current_user.status.message_html, data: { html: 'true', placement: 'bottom' } }
%span.user-status-emoji.d-flex.align-items-center
= emoji_icon current_user.status.emoji
%span.user-status-message.str-truncated
diff --git a/app/views/shared/milestones/_form_dates.html.haml b/app/views/shared/milestones/_form_dates.html.haml
index 4de89d7c7a0..6dbc460d9bf 100644
--- a/app/views/shared/milestones/_form_dates.html.haml
+++ b/app/views/shared/milestones/_form_dates.html.haml
@@ -1,13 +1,13 @@
.col-md-6
.form-group.row
.col-form-label.col-sm-2
- = f.label :start_date, "Start Date"
+ = f.label :start_date, _('Start Date')
.col-sm-10
- = f.text_field :start_date, class: "datepicker form-control", placeholder: "Select start date", autocomplete: 'off'
- %a.inline.float-right.prepend-top-5.js-clear-start-date{ href: "#" } Clear start date
+ = f.text_field :start_date, class: "datepicker form-control", placeholder: _('Select start date'), autocomplete: 'off'
+ %a.inline.float-right.prepend-top-5.js-clear-start-date{ href: "#" }= _('Clear start date')
.form-group.row
.col-form-label.col-sm-2
- = f.label :due_date, "Due Date"
+ = f.label :due_date, _('Due Date')
.col-sm-10
- = f.text_field :due_date, class: "datepicker form-control", placeholder: "Select due date", autocomplete: 'off'
- %a.inline.float-right.prepend-top-5.js-clear-due-date{ href: "#" } Clear due date
+ = f.text_field :due_date, class: "datepicker form-control", placeholder: _('Select due date'), autocomplete: 'off'
+ %a.inline.float-right.prepend-top-5.js-clear-due-date{ href: "#" }= _('Clear due date')
diff --git a/app/workers/concerns/gitlab/jira_import/import_worker.rb b/app/workers/concerns/gitlab/jira_import/import_worker.rb
index 537300e6eba..7606cf76c0f 100644
--- a/app/workers/concerns/gitlab/jira_import/import_worker.rb
+++ b/app/workers/concerns/gitlab/jira_import/import_worker.rb
@@ -7,6 +7,7 @@ module Gitlab
included do
include ApplicationWorker
+ include ProjectImportOptions
include Gitlab::JiraImport::QueueOptions
end
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_form_d.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_form_d.yml
new file mode 100644
index 00000000000..f6f54617e1a
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_form_d.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/milestones/_form_dates.html.haml
+merge_request: 32162
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/jira-projects-api-wrapper.yml b/changelogs/unreleased/jira-projects-api-wrapper.yml
new file mode 100644
index 00000000000..4fe86aa3f4c
--- /dev/null
+++ b/changelogs/unreleased/jira-projects-api-wrapper.yml
@@ -0,0 +1,5 @@
+---
+title: Add a GraphQL endpoint to fetch Jira projects through its REST API
+merge_request: 28190
+author:
+type: changed
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 48ef6f5ae36..9e351124126 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -5531,6 +5531,58 @@ type JiraImportStartPayload {
jiraImport: JiraImport
}
+type JiraProject {
+ """
+ Key of the Jira project
+ """
+ key: String!
+
+ """
+ Name of the Jira project
+ """
+ name: String
+
+ """
+ ID of the Jira project
+ """
+ projectId: Int!
+}
+
+"""
+The connection type for JiraProject.
+"""
+type JiraProjectConnection {
+ """
+ A list of edges.
+ """
+ edges: [JiraProjectEdge]
+
+ """
+ A list of nodes.
+ """
+ nodes: [JiraProject]
+
+ """
+ Information to aid in pagination.
+ """
+ pageInfo: PageInfo!
+}
+
+"""
+An edge in a connection.
+"""
+type JiraProjectEdge {
+ """
+ A cursor for use in pagination.
+ """
+ cursor: String!
+
+ """
+ The item at the end of the edge.
+ """
+ node: JiraProject
+}
+
type JiraService implements Service {
"""
Indicates if the service is active
@@ -5538,6 +5590,36 @@ type JiraService implements Service {
active: Boolean
"""
+ List of Jira projects fetched through Jira REST API
+ """
+ projects(
+ """
+ Returns the elements in the list that come after the specified cursor.
+ """
+ after: String
+
+ """
+ Returns the elements in the list that come before the specified cursor.
+ """
+ before: String
+
+ """
+ Returns the first _n_ elements from the list.
+ """
+ first: Int
+
+ """
+ Returns the last _n_ elements from the list.
+ """
+ last: Int
+
+ """
+ Project name or key
+ """
+ name: String
+ ): JiraProjectConnection
+
+ """
Class name of the service
"""
type: String
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 923830512ff..c1addf68b79 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -15376,6 +15376,181 @@
},
{
"kind": "OBJECT",
+ "name": "JiraProject",
+ "description": null,
+ "fields": [
+ {
+ "name": "key",
+ "description": "Key of the Jira project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "name",
+ "description": "Name of the Jira project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "projectId",
+ "description": "ID of the Jira project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "JiraProjectConnection",
+ "description": "The connection type for JiraProject.",
+ "fields": [
+ {
+ "name": "edges",
+ "description": "A list of edges.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "JiraProjectEdge",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "nodes",
+ "description": "A list of nodes.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "JiraProject",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "pageInfo",
+ "description": "Information to aid in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "OBJECT",
+ "name": "PageInfo",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "JiraProjectEdge",
+ "description": "An edge in a connection.",
+ "fields": [
+ {
+ "name": "cursor",
+ "description": "A cursor for use in pagination.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "node",
+ "description": "The item at the end of the edge.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "JiraProject",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "JiraService",
"description": null,
"fields": [
@@ -15394,6 +15569,69 @@
"deprecationReason": null
},
{
+ "name": "projects",
+ "description": "List of Jira projects fetched through Jira REST API",
+ "args": [
+ {
+ "name": "name",
+ "description": "Project name or key",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "after",
+ "description": "Returns the elements in the list that come after the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "before",
+ "description": "Returns the elements in the list that come before the specified cursor.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "first",
+ "description": "Returns the first _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "last",
+ "description": "Returns the last _n_ elements from the list.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "JiraProjectConnection",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "type",
"description": "Class name of the service",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 1fe468c54c3..dfeb4e0f46d 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -818,11 +818,20 @@ Autogenerated return type of JiraImportStart
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `jiraImport` | JiraImport | The Jira import data after mutation |
+## JiraProject
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `key` | String! | Key of the Jira project |
+| `name` | String | Name of the Jira project |
+| `projectId` | Int! | ID of the Jira project |
+
## JiraService
| Name | Type | Description |
| --- | ---- | ---------- |
| `active` | Boolean | Indicates if the service is active |
+| `projects` | JiraProjectConnection | List of Jira projects fetched through Jira REST API |
| `type` | String | Class name of the service |
## Label
diff --git a/doc/development/feature_flags/controls.md b/doc/development/feature_flags/controls.md
index 309aecd7978..6ce03a0e1f7 100644
--- a/doc/development/feature_flags/controls.md
+++ b/doc/development/feature_flags/controls.md
@@ -113,17 +113,58 @@ When you begin to enable the feature, please link to the relevant
Feature Flag Rollout Issue within a Slack thread of the first `/chatops`
command you make so people can understand the change if they need to.
-To enable a feature for 25% of all users, run the following in Slack:
+To enable a feature for 25% of the time, run the following in Slack:
```shell
/chatops run feature set new_navigation_bar 25
```
+This sets a feature flag to `true` based on the following formula:
+
+```ruby
+feature_flag_state = rand < (25 / 100.0)
+```
+
This will enable the feature for GitLab.com, with `new_navigation_bar` being the
name of the feature.
This command does *not* enable the feature for 25% of the total users.
Instead, when the feature is checked with `enabled?`, it will return `true` 25% of the time.
+To enable a feature for 25% of actors such as users, projects, or groups,
+run the following in Slack:
+
+```shell
+/chatops run feature set some_feature 25 --actors
+```
+
+This sets a feature flag to `true` based on the following formula:
+
+```ruby
+feature_flag_state = Zlib.crc32("some_feature<Actor>:#{actor.id}") % (100 * 1_000) < 25 * 1_000]
+# where <Actor>: is a `User`, `Group`, `Project` and actor is an instance
+```
+
+During development, based on the nature of the feature, an actor choice
+should be made.
+
+For user focused features:
+
+```ruby
+Feature.enabled?(:feature_cool_avatars, current_user)
+```
+
+For group or namespace level features:
+
+```ruby
+Feature.enabled?(:feature_cooler_groups, group)
+```
+
+For project level features:
+
+```ruby
+Feature.enabled?(:feature_ice_cold_projects, project)
+```
+
If you are not certain what percentages to use, simply use the following steps:
1. 25%
@@ -158,15 +199,21 @@ you run these 2 commands:
```shell
/chatops run feature set --project=gitlab-org/gitlab some_feature true
-/chatops run feature set some_feature 25
+/chatops run feature set some_feature 25 --actors
+```
+
+Then `some_feature` will be enabled for both 25% of actors and always when interacting with
+`gitlab-org/gitlab`. This is a good idea if the feature flag development makes use of group
+actors.
+
+```ruby
+Feature.enabled?(:some_feature, group)
```
-Then `some_feature` will be enabled for both 25% of users and all users interacting with
-`gitlab-org/gitlab`.
+NOTE:
-NOTE: **Note:**
**Percentage of time** rollout is not a good idea if what you want is to make sure a feature
-is always on or off to the users.
+is always on or off to the users. In that case, **Percentage of actors** rollout is a better method.
### Feature flag change logging
diff --git a/doc/development/integrations/secure.md b/doc/development/integrations/secure.md
index 9e197ea8f5b..d7739f232ae 100644
--- a/doc/development/integrations/secure.md
+++ b/doc/development/integrations/secure.md
@@ -470,6 +470,10 @@ The confidence ranges from `Low` to `Confirmed`, but it can also be `Unknown`,
`Experimental` or even `Ignore` if the vulnerability is to be ignored.
Valid values are: `Ignore`, `Unknown`, `Experimental`, `Low`, `Medium`, `High`, or `Confirmed`
+`Unknown` values means that data is unavailable to determine it's actual value. Therefore, it may be `high`, `medium`, or `low`,
+and needs to be investigated. We have [provided a chart](../../user/application_security/sast/analyzers.md#analyzers-data)
+of the available SAST Analyzers and what data is currently available.
+
### Remediations
The `remediations` field of the report is an array of remediation objects.
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 9095aba7340..e070e57a376 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -220,6 +220,8 @@ module API
requires :id, type: Integer, desc: %q(Job's ID)
optional :token, type: String, desc: %q(Job's authentication token)
optional :filesize, type: Integer, desc: %q(Artifacts filesize)
+ optional :artifact_type, type: String, desc: %q(The type of artifact),
+ default: 'archive', values: Ci::JobArtifact.file_types.keys
end
post '/:id/artifacts/authorize' do
not_allowed! unless Gitlab.config.artifacts.enabled
@@ -229,16 +231,14 @@ module API
job = authenticate_job!
forbidden!('Job is not running') unless job.running?
- max_size = max_artifacts_size(job)
+ service = Ci::AuthorizeJobArtifactService.new(job, params, max_size: max_artifacts_size(job))
- if params[:filesize]
- file_size = params[:filesize].to_i
- file_too_large! unless file_size < max_size
- end
+ forbidden! if service.forbidden?
+ file_too_large! if service.too_large?
status 200
content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
- JobArtifactUploader.workhorse_authorize(has_length: false, maximum_size: max_size)
+ service.headers
end
desc 'Upload artifacts for job' do
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index d1398ddb642..72dcc4fde71 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -225,6 +225,10 @@ module Gitlab
new_path.presence || old_path
end
+ def file_hash
+ Digest::SHA1.hexdigest(file_path)
+ end
+
def added_lines
@stats&.additions || diff_lines.count(&:added?)
end
@@ -237,6 +241,10 @@ module Gitlab
"#{file_path}-#{new_file?}-#{deleted_file?}-#{renamed_file?}"
end
+ def file_identifier_hash
+ Digest::SHA1.hexdigest(file_identifier)
+ end
+
def diffable?
repository.attributes(file_path).fetch('diff') { true }
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index a68418e539d..aafdd18c64a 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4338,6 +4338,9 @@ msgstr ""
msgid "Clear chart filters"
msgstr ""
+msgid "Clear due date"
+msgstr ""
+
msgid "Clear input"
msgstr ""
@@ -4350,6 +4353,9 @@ msgstr ""
msgid "Clear search input"
msgstr ""
+msgid "Clear start date"
+msgstr ""
+
msgid "Clear templates search input"
msgstr ""
@@ -7797,6 +7803,9 @@ msgstr ""
msgid "Drop your designs to start your upload."
msgstr ""
+msgid "Due Date"
+msgstr ""
+
msgid "Due date"
msgstr ""
@@ -19228,6 +19237,9 @@ msgstr ""
msgid "Select branch/tag"
msgstr ""
+msgid "Select due date"
+msgstr ""
+
msgid "Select group or project"
msgstr ""
@@ -19273,6 +19285,9 @@ msgstr ""
msgid "Select source branch"
msgstr ""
+msgid "Select start date"
+msgstr ""
+
msgid "Select status"
msgstr ""
@@ -20466,6 +20481,9 @@ msgstr ""
msgid "Stars"
msgstr ""
+msgid "Start Date"
+msgstr ""
+
msgid "Start Web Terminal"
msgstr ""
diff --git a/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb
new file mode 100644
index 00000000000..6251ead3954
--- /dev/null
+++ b/spec/graphql/resolvers/projects/jira_projects_resolver_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::Projects::JiraProjectsResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ shared_examples 'no project service access' do
+ it 'raises error' do
+ expect do
+ resolve_jira_projects
+ end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ end
+ end
+
+ context 'when project has no jira service' do
+ let_it_be(:jira_service) { nil }
+
+ context 'when user is a maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'no project service access'
+ end
+ end
+
+ context 'when project has jira service' do
+ let(:jira_service) { create(:jira_service, project: project) }
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'no project service access'
+ end
+
+ context 'when user is a maintainer' do
+ include_context 'jira projects request context'
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'returns jira projects' do
+ jira_projects = resolve_jira_projects
+ project_keys = jira_projects.map(&:key)
+ project_names = jira_projects.map(&:name)
+ project_ids = jira_projects.map(&:id)
+
+ expect(jira_projects.size).to eq 2
+ expect(project_keys).to eq(%w(EX ABC))
+ expect(project_names).to eq(%w(Example Alphabetical))
+ expect(project_ids).to eq(%w(10000 10001))
+ end
+ end
+ end
+ end
+
+ def resolve_jira_projects(args = {}, context = { current_user: user })
+ resolve(described_class, obj: jira_service, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/types/projects/jira_project_type_spec.rb b/spec/graphql/types/projects/jira_project_type_spec.rb
new file mode 100644
index 00000000000..cbb01117717
--- /dev/null
+++ b/spec/graphql/types/projects/jira_project_type_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['JiraProject'] do
+ it { expect(described_class.graphql_name).to eq('JiraProject') }
+
+ it 'has basic expected fields' do
+ expect(described_class).to have_graphql_fields(:key, :project_id, :name)
+ end
+end
diff --git a/spec/graphql/types/projects/jira_service_type_spec.rb b/spec/graphql/types/projects/jira_service_type_spec.rb
index 91d7e4586cb..fad0c91caab 100644
--- a/spec/graphql/types/projects/jira_service_type_spec.rb
+++ b/spec/graphql/types/projects/jira_service_type_spec.rb
@@ -6,7 +6,7 @@ describe GitlabSchema.types['JiraService'] do
specify { expect(described_class.graphql_name).to eq('JiraService') }
it 'has basic expected fields' do
- expect(described_class).to have_graphql_fields(:type, :active)
+ expect(described_class).to have_graphql_fields(:type, :active, :projects)
end
specify { expect(described_class).to require_graphql_authorizations(:admin_project) }
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index d1592e60d3d..8dbedcf26b9 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -282,6 +282,18 @@ describe Gitlab::Diff::File do
end
end
+ describe '#file_hash' do
+ it 'returns a hash of file_path' do
+ expect(diff_file.file_hash).to eq(Digest::SHA1.hexdigest(diff_file.file_path))
+ end
+ end
+
+ describe '#file_identifier_hash' do
+ it 'returns a hash of file_identifier' do
+ expect(diff_file.file_identifier_hash).to eq(Digest::SHA1.hexdigest(diff_file.file_identifier))
+ end
+ end
+
context 'diff file stats' do
let(:diff_file) do
described_class.new(diff,
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index 909a7618df4..af963e1b695 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -65,16 +65,24 @@ describe Gitlab::Email::Handler::CreateNoteHandler do
end
end
- context 'and current user can update noteable' do
- before do
- project.add_developer(user)
- end
-
- it 'does not raise an error' do
- # One system note is created for the 'close' event
- expect { receiver.execute }.to change { noteable.notes.count }.by(1)
-
- expect(noteable.reload).to be_closed
+ [true, false].each do |state_tracking_enabled|
+ context "and current user can update noteable #{state_tracking_enabled ? 'enabled' : 'disabled'}" do
+ before do
+ stub_feature_flags(track_resource_state_change_events: state_tracking_enabled)
+
+ project.add_developer(user)
+ end
+
+ it 'does not raise an error' do
+ if state_tracking_enabled
+ expect { receiver.execute }.to change { noteable.resource_state_events.count }.by(1)
+ else
+ # One system note is created for the 'close' event
+ expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+ end
+
+ expect(noteable.reload).to be_closed
+ end
end
end
end
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index cc4a12467da..5a9e76e7717 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -2157,7 +2157,34 @@ describe MergeRequest do
end
end
- context 'when merging note is persisted, but no metrics or merge event exists' do
+ context 'when state event tracking is disabled' do
+ before do
+ stub_feature_flags(track_resource_state_change_events: false)
+ end
+
+ context 'when merging note is persisted, but no metrics or merge event exists' do
+ let(:user) { create(:user) }
+ let(:merge_request) { create(:merge_request, :merged) }
+
+ before do
+ merge_request.metrics.destroy!
+
+ SystemNoteService.change_status(merge_request,
+ merge_request.target_project,
+ user,
+ merge_request.state, nil)
+ end
+
+ it 'returns merging note creation date' do
+ expect(merge_request.reload.metrics).to be_nil
+ expect(merge_request.merge_event).to be_nil
+ expect(merge_request.notes.count).to eq(1)
+ expect(merge_request.merged_at).to eq(merge_request.notes.first.created_at)
+ end
+ end
+ end
+
+ context 'when state event tracking is enabled' do
let(:user) { create(:user) }
let(:merge_request) { create(:merge_request, :merged) }
@@ -2170,11 +2197,8 @@ describe MergeRequest do
merge_request.state, nil)
end
- it 'returns merging note creation date' do
- expect(merge_request.reload.metrics).to be_nil
- expect(merge_request.merge_event).to be_nil
- expect(merge_request.notes.count).to eq(1)
- expect(merge_request.merged_at).to eq(merge_request.notes.first.created_at)
+ it 'does not create a system note' do
+ expect(merge_request.notes).to be_empty
end
end
end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index a0d36f0a238..4399a722f89 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -816,4 +816,85 @@ describe JiraService do
end
end
end
+
+ describe '#jira_projects' do
+ let(:project) { create(:project) }
+ let(:jira_service) do
+ described_class.new(
+ project: project,
+ url: url,
+ username: username,
+ password: password
+ )
+ end
+
+ context 'when request to the jira server fails' do
+ it 'returns error' do
+ test_url = "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=0"
+ WebMock.stub_request(:get, test_url).with(basic_auth: [username, password])
+ .to_raise(JIRA::HTTPError.new(double(message: 'random error')))
+
+ response = jira_service.jira_projects
+
+ expect(response.error?).to be true
+ expect(response.message).to eq('random error')
+ end
+ end
+
+ context 'with invalid params' do
+ it 'escapes params' do
+ escaped_url = "#{url}/rest/api/2/project/search?query=Test%26maxResults%3D3&maxResults=10&startAt=0"
+ WebMock.stub_request(:get, escaped_url).with(basic_auth: [username, password])
+ .to_return(body: {}.to_json, headers: { "Content-Type": "application/json" })
+
+ response = jira_service.jira_projects(query: 'Test&maxResults=3', limit: 10, start_at: 'zero')
+
+ expect(response.error?).to be false
+ end
+ end
+
+ context 'when no jira_projects are returned' do
+ let(:jira_projects_json) do
+ '{
+ "self": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=0&maxResults=2",
+ "nextPage": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=2&maxResults=2",
+ "maxResults": 2,
+ "startAt": 0,
+ "total": 7,
+ "isLast": false,
+ "values": []
+ }'
+ end
+
+ it 'returns empty array of jira projects' do
+ test_url = "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=0"
+ WebMock.stub_request(:get, test_url).with(basic_auth: [username, password])
+ .to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" })
+
+ response = jira_service.jira_projects
+
+ expect(response.success?).to be true
+ expect(response.payload).not_to be nil
+ end
+ end
+
+ context 'when jira_projects are returned' do
+ include_context 'jira projects request context'
+
+ it 'returns array of jira projects' do
+ response = jira_service.jira_projects
+
+ projects = response.payload[:projects]
+ project_keys = projects.map(&:key)
+ project_names = projects.map(&:name)
+ project_ids = projects.map(&:id)
+
+ expect(response.success?).to be true
+ expect(projects.size).to eq(2)
+ expect(project_keys).to eq(%w(EX ABC))
+ expect(project_names).to eq(%w(Example Alphabetical))
+ expect(project_ids).to eq(%w(10000 10001))
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/project/jira_projects_spec.rb b/spec/requests/api/graphql/project/jira_projects_spec.rb
new file mode 100644
index 00000000000..d67c89f18c9
--- /dev/null
+++ b/spec/requests/api/graphql/project/jira_projects_spec.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe 'query Jira projects' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+
+ include_context 'jira projects request context'
+
+ let(:services) { graphql_data_at(:project, :services, :edges) }
+ let(:jira_projects) { services.first.dig('node', 'projects', 'nodes') }
+ let(:projects_query) { 'projects' }
+ let(:query) do
+ %(
+ query {
+ project(fullPath: "#{project.full_path}") {
+ services(active: true, type: JIRA_SERVICE) {
+ edges {
+ node {
+ ... on JiraService {
+ %{projects_query} {
+ nodes {
+ key
+ name
+ projectId
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ) % { projects_query: projects_query }
+ end
+
+ context 'when user does not have access' do
+ it_behaves_like 'unauthorized users cannot read services'
+ end
+
+ context 'when user can access project services' do
+ before do
+ project.add_maintainer(current_user)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+
+ it 'retuns list of jira projects' do
+ project_keys = jira_projects.map { |jp| jp['key'] }
+ project_names = jira_projects.map { |jp| jp['name'] }
+ project_ids = jira_projects.map { |jp| jp['projectId'] }
+
+ expect(jira_projects.size).to eq(2)
+ expect(project_keys).to eq(%w(EX ABC))
+ expect(project_names).to eq(%w(Example Alphabetical))
+ expect(project_ids).to eq([10000, 10001])
+ end
+
+ context 'with pagination' do
+ context 'when fetching limited number of projects' do
+ shared_examples_for 'fetches first project' do
+ it 'retuns first project from list of fetched projects' do
+ project_keys = jira_projects.map { |jp| jp['key'] }
+ project_names = jira_projects.map { |jp| jp['name'] }
+ project_ids = jira_projects.map { |jp| jp['projectId'] }
+
+ expect(jira_projects.size).to eq(1)
+ expect(project_keys).to eq(%w(EX))
+ expect(project_names).to eq(%w(Example))
+ expect(project_ids).to eq([10000])
+ end
+ end
+
+ context 'without cursor' do
+ let(:projects_query) { 'projects(first: 1)' }
+
+ it_behaves_like 'fetches first project'
+ end
+
+ context 'with before cursor' do
+ let(:projects_query) { 'projects(before: "Mg==", first: 1)' }
+
+ it_behaves_like 'fetches first project'
+ end
+
+ context 'with after cursor' do
+ let(:projects_query) { 'projects(after: "MA==", first: 1)' }
+
+ it_behaves_like 'fetches first project'
+ end
+ end
+
+ context 'with valid but inexistent after cursor' do
+ let(:projects_query) { 'projects(after: "MTk==")' }
+
+ it 'retuns empty list of jira projects' do
+ expect(jira_projects.size).to eq(0)
+ end
+ end
+
+ context 'with invalid after cursor' do
+ let(:projects_query) { 'projects(after: "invalid==")' }
+
+ it 'treats the invalid cursor as no cursor and returns list of jira projects' do
+ expect(jira_projects.size).to eq(2)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index 7284f33f3af..a4331c3a0ec 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -1634,6 +1634,31 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
+ context 'authorize uploading of an lsif artifact' do
+ it 'adds ProcessLsif header' do
+ authorize_artifacts_with_token_in_headers(artifact_type: :lsif)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['ProcessLsif']).to be_truthy
+ end
+
+ it 'fails to authorize too large artifact' do
+ authorize_artifacts_with_token_in_headers(artifact_type: :lsif, filesize: 30.megabytes)
+
+ expect(response).to have_gitlab_http_status(:payload_too_large)
+ end
+
+ context 'code_navigation feature flag is disabled' do
+ it 'does not add ProcessLsif header' do
+ stub_feature_flags(code_navigation: false)
+
+ authorize_artifacts_with_token_in_headers(artifact_type: :lsif)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+
def authorize_artifacts(params = {}, request_headers = headers)
post api("/jobs/#{job.id}/artifacts/authorize"), params: params, headers: request_headers
end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 987b4ad68f7..282f2485f3e 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -13,6 +13,7 @@ describe EventCreateService do
it "creates new event" do
expect { service.open_issue(issue, issue.author) }.to change { Event.count }
+ expect { service.open_issue(issue, issue.author) }.to change { ResourceStateEvent.count }
end
end
@@ -23,6 +24,7 @@ describe EventCreateService do
it "creates new event" do
expect { service.close_issue(issue, issue.author) }.to change { Event.count }
+ expect { service.close_issue(issue, issue.author) }.to change { ResourceStateEvent.count }
end
end
@@ -33,6 +35,7 @@ describe EventCreateService do
it "creates new event" do
expect { service.reopen_issue(issue, issue.author) }.to change { Event.count }
+ expect { service.reopen_issue(issue, issue.author) }.to change { ResourceStateEvent.count }
end
end
end
@@ -45,6 +48,7 @@ describe EventCreateService do
it "creates new event" do
expect { service.open_mr(merge_request, merge_request.author) }.to change { Event.count }
+ expect { service.open_mr(merge_request, merge_request.author) }.to change { ResourceStateEvent.count }
end
end
@@ -55,6 +59,7 @@ describe EventCreateService do
it "creates new event" do
expect { service.close_mr(merge_request, merge_request.author) }.to change { Event.count }
+ expect { service.close_mr(merge_request, merge_request.author) }.to change { ResourceStateEvent.count }
end
end
@@ -65,6 +70,7 @@ describe EventCreateService do
it "creates new event" do
expect { service.merge_mr(merge_request, merge_request.author) }.to change { Event.count }
+ expect { service.merge_mr(merge_request, merge_request.author) }.to change { ResourceStateEvent.count }
end
end
@@ -75,6 +81,7 @@ describe EventCreateService do
it "creates new event" do
expect { service.reopen_mr(merge_request, merge_request.author) }.to change { Event.count }
+ expect { service.reopen_mr(merge_request, merge_request.author) }.to change { ResourceStateEvent.count }
end
end
end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index 6fc1928d47b..78eba565de4 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -224,11 +224,26 @@ describe Issues::CloseService do
expect(email.subject).to include(issue.title)
end
- it 'creates system note about issue reassign' do
- close_issue
+ context 'when resource state events are disabled' do
+ before do
+ stub_feature_flags(track_resource_state_change_events: false)
+ end
+
+ it 'creates system note about the issue being closed' do
+ close_issue
+
+ note = issue.notes.last
+ expect(note.note).to include "closed"
+ end
+ end
- note = issue.notes.last
- expect(note.note).to include "closed"
+ context 'when resource state events are enabled' do
+ it 'creates resource state event about the issue being closed' do
+ close_issue
+
+ event = issue.resource_state_events.last
+ expect(event.state).to eq('closed')
+ end
end
it 'marks todos as done' do
diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb
index b037b73752e..0e51de48fb1 100644
--- a/spec/services/merge_requests/close_service_spec.rb
+++ b/spec/services/merge_requests/close_service_spec.rb
@@ -19,45 +19,54 @@ describe MergeRequests::CloseService do
describe '#execute' do
it_behaves_like 'cache counters invalidator'
- context 'valid params' do
- let(:service) { described_class.new(project, user, {}) }
+ [true, false].each do |state_tracking_enabled|
+ context "valid params with state_tracking #{state_tracking_enabled ? 'enabled' : 'disabled'}" do
+ let(:service) { described_class.new(project, user, {}) }
- before do
- allow(service).to receive(:execute_hooks)
+ before do
+ stub_feature_flags(track_resource_state_change_events: state_tracking_enabled)
- perform_enqueued_jobs do
- @merge_request = service.execute(merge_request)
+ allow(service).to receive(:execute_hooks)
+
+ perform_enqueued_jobs do
+ @merge_request = service.execute(merge_request)
+ end
end
- end
- it { expect(@merge_request).to be_valid }
- it { expect(@merge_request).to be_closed }
+ it { expect(@merge_request).to be_valid }
+ it { expect(@merge_request).to be_closed }
- it 'executes hooks with close action' do
- expect(service).to have_received(:execute_hooks)
+ it 'executes hooks with close action' do
+ expect(service).to have_received(:execute_hooks)
.with(@merge_request, 'close')
- end
+ end
- it 'sends email to user2 about assign of new merge_request', :sidekiq_might_not_need_inline do
- email = ActionMailer::Base.deliveries.last
- expect(email.to.first).to eq(user2.email)
- expect(email.subject).to include(merge_request.title)
- end
+ it 'sends email to user2 about assign of new merge_request', :sidekiq_might_not_need_inline do
+ email = ActionMailer::Base.deliveries.last
+ expect(email.to.first).to eq(user2.email)
+ expect(email.subject).to include(merge_request.title)
+ end
- it 'creates system note about merge_request reassign' do
- note = @merge_request.notes.last
- expect(note.note).to include 'closed'
- end
+ it 'creates system note about merge_request reassign' do
+ if state_tracking_enabled
+ event = @merge_request.resource_state_events.last
+ expect(event.state).to eq('closed')
+ else
+ note = @merge_request.notes.last
+ expect(note.note).to include 'closed'
+ end
+ end
- it 'marks todos as done' do
- expect(todo.reload).to be_done
- end
+ it 'marks todos as done' do
+ expect(todo.reload).to be_done
+ end
- context 'when auto merge is enabled' do
- let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
+ context 'when auto merge is enabled' do
+ let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
- it 'cancels the auto merge' do
- expect(@merge_request).not_to be_auto_merge_enabled
+ it 'cancels the auto merge' do
+ expect(@merge_request).not_to be_auto_merge_enabled
+ end
end
end
end
diff --git a/spec/services/merge_requests/ff_merge_service_spec.rb b/spec/services/merge_requests/ff_merge_service_spec.rb
index efe1860d4e5..415b351e13a 100644
--- a/spec/services/merge_requests/ff_merge_service_spec.rb
+++ b/spec/services/merge_requests/ff_merge_service_spec.rb
@@ -21,65 +21,74 @@ describe MergeRequests::FfMergeService do
end
describe '#execute' do
- context 'valid params' do
- let(:service) { described_class.new(project, user, valid_merge_params) }
-
- def execute_ff_merge
- perform_enqueued_jobs do
- service.execute(merge_request)
+ [true, false].each do |state_tracking_enabled|
+ context "valid params with state_tracking #{state_tracking_enabled ? 'enabled' : 'disabled'}" do
+ let(:service) { described_class.new(project, user, valid_merge_params) }
+
+ def execute_ff_merge
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ end
end
- end
- before do
- allow(service).to receive(:execute_hooks)
- end
+ before do
+ stub_feature_flags(track_resource_state_change_events: state_tracking_enabled)
- it "does not create merge commit" do
- execute_ff_merge
+ allow(service).to receive(:execute_hooks)
+ end
- source_branch_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha
- target_branch_sha = merge_request.target_project.repository.commit(merge_request.target_branch).sha
+ it "does not create merge commit" do
+ execute_ff_merge
- expect(source_branch_sha).to eq(target_branch_sha)
- end
+ source_branch_sha = merge_request.source_project.repository.commit(merge_request.source_branch).sha
+ target_branch_sha = merge_request.target_project.repository.commit(merge_request.target_branch).sha
- it 'keeps the merge request valid' do
- expect { execute_ff_merge }
- .not_to change { merge_request.valid? }
- end
+ expect(source_branch_sha).to eq(target_branch_sha)
+ end
- it 'updates the merge request to merged' do
- expect { execute_ff_merge }
- .to change { merge_request.merged? }
- .from(false)
- .to(true)
- end
+ it 'keeps the merge request valid' do
+ expect { execute_ff_merge }
+ .not_to change { merge_request.valid? }
+ end
- it 'sends email to user2 about merge of new merge_request' do
- execute_ff_merge
+ it 'updates the merge request to merged' do
+ expect { execute_ff_merge }
+ .to change { merge_request.merged? }
+ .from(false)
+ .to(true)
+ end
- email = ActionMailer::Base.deliveries.last
- expect(email.to.first).to eq(user2.email)
- expect(email.subject).to include(merge_request.title)
- end
+ it 'sends email to user2 about merge of new merge_request' do
+ execute_ff_merge
- it 'creates system note about merge_request merge' do
- execute_ff_merge
+ email = ActionMailer::Base.deliveries.last
+ expect(email.to.first).to eq(user2.email)
+ expect(email.subject).to include(merge_request.title)
+ end
- note = merge_request.notes.last
- expect(note.note).to include 'merged'
- end
+ it 'creates system note about merge_request merge' do
+ execute_ff_merge
- it 'does not update squash_commit_sha if it is not a squash' do
- expect { execute_ff_merge }.not_to change { merge_request.squash_commit_sha }
- end
+ if state_tracking_enabled
+ event = merge_request.resource_state_events.last
+ expect(event.state).to eq('merged')
+ else
+ note = merge_request.notes.last
+ expect(note.note).to include 'merged'
+ end
+ end
- it 'updates squash_commit_sha if it is a squash' do
- merge_request.update!(squash: true)
+ it 'does not update squash_commit_sha if it is not a squash' do
+ expect { execute_ff_merge }.not_to change { merge_request.squash_commit_sha }
+ end
- expect { execute_ff_merge }
- .to change { merge_request.squash_commit_sha }
- .from(nil)
+ it 'updates squash_commit_sha if it is a squash' do
+ merge_request.update!(squash: true)
+
+ expect { execute_ff_merge }
+ .to change { merge_request.squash_commit_sha }
+ .from(nil)
+ end
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index b98fc9785ff..2274d917527 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -20,7 +20,11 @@ describe MergeRequests::MergeService do
end
context 'valid params' do
+ let(:state_tracking) { true }
+
before do
+ stub_feature_flags(track_resource_state_change_events: state_tracking)
+
allow(service).to receive(:execute_hooks)
perform_enqueued_jobs do
@@ -42,9 +46,22 @@ describe MergeRequests::MergeService do
expect(email.subject).to include(merge_request.title)
end
- it 'creates system note about merge_request merge' do
- note = merge_request.notes.last
- expect(note.note).to include 'merged'
+ context 'note creation' do
+ context 'when resource state event tracking is disabled' do
+ let(:state_tracking) { false }
+
+ it 'creates system note about merge_request merge' do
+ note = merge_request.notes.last
+ expect(note.note).to include 'merged'
+ end
+ end
+
+ context 'when resource state event tracking is enabled' do
+ it 'creates resource state event about merge_request merge' do
+ event = merge_request.resource_state_events.last
+ expect(event.state).to eq('merged')
+ end
+ end
end
context 'when squashing' do
@@ -55,7 +72,7 @@ describe MergeRequests::MergeService do
end
let(:merge_request) do
- # A merge reqeust with 5 commits
+ # A merge request with 5 commits
create(:merge_request, :simple,
author: user2,
assignees: [user2],
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index 94e65d895ac..e60ff6eb98a 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -362,76 +362,101 @@ describe MergeRequests::RefreshService do
end
end
- context 'push to origin repo target branch', :sidekiq_might_not_need_inline do
- context 'when all MRs to the target branch had diffs' do
+ [true, false].each do |state_tracking_enabled|
+ context "push to origin repo target branch with state tracking #{state_tracking_enabled ? 'enabled' : 'disabled'}", :sidekiq_might_not_need_inline do
before do
- service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
- reload_mrs
+ stub_feature_flags(track_resource_state_change_events: state_tracking_enabled)
end
- it 'updates the merge state' do
- expect(@merge_request.notes.last.note).to include('merged')
- expect(@merge_request).to be_merged
- expect(@fork_merge_request).to be_merged
- expect(@fork_merge_request.notes.last.note).to include('merged')
- expect(@build_failed_todo).to be_done
- expect(@fork_build_failed_todo).to be_done
+ context 'when all MRs to the target branch had diffs' do
+ before do
+ service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
+ reload_mrs
+ end
+
+ it 'updates the merge state' do
+ expect(@merge_request).to be_merged
+ expect(@fork_merge_request).to be_merged
+ expect(@build_failed_todo).to be_done
+ expect(@fork_build_failed_todo).to be_done
+
+ if state_tracking_enabled
+ expect(@merge_request.resource_state_events.last.state).to eq('merged')
+ expect(@fork_merge_request.resource_state_events.last.state).to eq('merged')
+ else
+ expect(@merge_request.notes.last.note).to include('merged')
+ expect(@fork_merge_request.notes.last.note).to include('merged')
+ end
+ end
end
- end
- context 'when an MR to be closed was empty already' do
- let!(:empty_fork_merge_request) do
- create(:merge_request,
- source_project: @fork_project,
- source_branch: 'master',
- target_branch: 'master',
- target_project: @project)
+ context 'when an MR to be closed was empty already' do
+ let!(:empty_fork_merge_request) do
+ create(:merge_request,
+ source_project: @fork_project,
+ source_branch: 'master',
+ target_branch: 'master',
+ target_project: @project)
+ end
+
+ before do
+ # This spec already has a fake push, so pretend that we were targeting
+ # feature all along.
+ empty_fork_merge_request.update_columns(target_branch: 'feature')
+
+ service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
+ reload_mrs
+ empty_fork_merge_request.reload
+ end
+
+ it 'only updates the non-empty MRs' do
+ expect(@merge_request).to be_merged
+ expect(@fork_merge_request).to be_merged
+
+ expect(empty_fork_merge_request).to be_open
+ expect(empty_fork_merge_request.merge_request_diff.state).to eq('empty')
+ expect(empty_fork_merge_request.notes).to be_empty
+
+ if state_tracking_enabled
+ expect(@merge_request.resource_state_events.last.state).to eq('merged')
+ expect(@fork_merge_request.resource_state_events.last.state).to eq('merged')
+ else
+ expect(@merge_request.notes.last.note).to include('merged')
+ expect(@fork_merge_request.notes.last.note).to include('merged')
+ end
+ end
end
+ end
+ context "manual merge of source branch #{state_tracking_enabled ? 'enabled' : 'disabled'}", :sidekiq_might_not_need_inline do
before do
- # This spec already has a fake push, so pretend that we were targeting
- # feature all along.
- empty_fork_merge_request.update_columns(target_branch: 'feature')
+ stub_feature_flags(track_resource_state_change_events: state_tracking_enabled)
- service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
+ # Merge master -> feature branch
+ @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, 'Test message')
+ commit = @project.repository.commit('feature')
+ service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature')
reload_mrs
- empty_fork_merge_request.reload
end
- it 'only updates the non-empty MRs' do
- expect(@merge_request).to be_merged
- expect(@merge_request.notes.last.note).to include('merged')
+ it 'updates the merge state' do
+ if state_tracking_enabled
+ expect(@merge_request.resource_state_events.last.state).to eq('merged')
+ expect(@fork_merge_request.resource_state_events.last.state).to eq('merged')
+ else
+ expect(@merge_request.notes.last.note).to include('merged')
+ expect(@fork_merge_request.notes.last.note).to include('merged')
+ end
+ expect(@merge_request).to be_merged
+ expect(@merge_request.diffs.size).to be > 0
expect(@fork_merge_request).to be_merged
- expect(@fork_merge_request.notes.last.note).to include('merged')
-
- expect(empty_fork_merge_request).to be_open
- expect(empty_fork_merge_request.merge_request_diff.state).to eq('empty')
- expect(empty_fork_merge_request.notes).to be_empty
+ expect(@build_failed_todo).to be_done
+ expect(@fork_build_failed_todo).to be_done
end
end
end
- context 'manual merge of source branch', :sidekiq_might_not_need_inline do
- before do
- # Merge master -> feature branch
- @project.repository.merge(@user, @merge_request.diff_head_sha, @merge_request, 'Test message')
- commit = @project.repository.commit('feature')
- service.new(@project, @user).execute(@oldrev, commit.id, 'refs/heads/feature')
- reload_mrs
- end
-
- it 'updates the merge state' do
- expect(@merge_request.notes.last.note).to include('merged')
- expect(@merge_request).to be_merged
- expect(@merge_request.diffs.size).to be > 0
- expect(@fork_merge_request).to be_merged
- expect(@fork_merge_request.notes.last.note).to include('merged')
- expect(@build_failed_todo).to be_done
- expect(@fork_build_failed_todo).to be_done
- end
- end
-
context 'push to fork repo source branch', :sidekiq_might_not_need_inline do
let(:refresh_service) { service.new(@fork_project, @user) }
@@ -583,20 +608,29 @@ describe MergeRequests::RefreshService do
end
end
- context 'push to origin repo target branch after fork project was removed' do
- before do
- @fork_project.destroy
- service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
- reload_mrs
- end
+ [true, false].each do |state_tracking_enabled|
+ context "push to origin repo target branch after fork project was removed #{state_tracking_enabled ? 'enabled' : 'disabled'}" do
+ before do
+ stub_feature_flags(track_resource_state_change_events: state_tracking_enabled)
- it 'updates the merge request state' do
- expect(@merge_request.notes.last.note).to include('merged')
- expect(@merge_request).to be_merged
- expect(@fork_merge_request).to be_open
- expect(@fork_merge_request.notes).to be_empty
- expect(@build_failed_todo).to be_done
- expect(@fork_build_failed_todo).to be_done
+ @fork_project.destroy
+ service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature')
+ reload_mrs
+ end
+
+ it 'updates the merge request state' do
+ if state_tracking_enabled
+ expect(@merge_request.resource_state_events.last.state).to eq('merged')
+ else
+ expect(@merge_request.notes.last.note).to include('merged')
+ end
+
+ expect(@merge_request).to be_merged
+ expect(@fork_merge_request).to be_open
+ expect(@fork_merge_request.notes).to be_empty
+ expect(@build_failed_todo).to be_done
+ expect(@fork_build_failed_todo).to be_done
+ end
end
end
diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb
index 25ab79d70c3..3807c44b01f 100644
--- a/spec/services/merge_requests/reopen_service_spec.rb
+++ b/spec/services/merge_requests/reopen_service_spec.rb
@@ -20,8 +20,11 @@ describe MergeRequests::ReopenService do
context 'valid params' do
let(:service) { described_class.new(project, user, {}) }
+ let(:state_tracking) { true }
before do
+ stub_feature_flags(track_resource_state_change_events: state_tracking)
+
allow(service).to receive(:execute_hooks)
perform_enqueued_jobs do
@@ -43,9 +46,22 @@ describe MergeRequests::ReopenService do
expect(email.subject).to include(merge_request.title)
end
- it 'creates system note about merge_request reopen' do
- note = merge_request.notes.last
- expect(note.note).to include 'reopened'
+ context 'note creation' do
+ context 'when state event tracking is disabled' do
+ let(:state_tracking) { false }
+
+ it 'creates system note about merge_request reopen' do
+ note = merge_request.notes.last
+ expect(note.note).to include 'reopened'
+ end
+ end
+
+ context 'when state event tracking is enabled' do
+ it 'creates resource state event about merge_request reopen' do
+ event = merge_request.resource_state_events.last
+ expect(event.state).to eq('reopened')
+ end
+ end
end
end
diff --git a/spec/services/resource_events/change_state_service_spec.rb b/spec/services/resource_events/change_state_service_spec.rb
new file mode 100644
index 00000000000..e5d2a4ab11e
--- /dev/null
+++ b/spec/services/resource_events/change_state_service_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ResourceEvents::ChangeStateService do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ let(:issue) { create(:issue, project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ describe '#execute' do
+ context 'when resource is an issue' do
+ %w[opened reopened closed locked].each do |state|
+ it "creates the expected event if issue has #{state} state" do
+ described_class.new(user: user, resource: issue).execute(state)
+
+ event = issue.resource_state_events.last
+ expect(event.issue).to eq(issue)
+ expect(event.merge_request).to be_nil
+ expect(event.state).to eq(state)
+ end
+ end
+ end
+
+ context 'when resource is a merge request' do
+ %w[opened reopened closed locked merged].each do |state|
+ it "creates the expected event if merge request has #{state} state" do
+ described_class.new(user: user, resource: merge_request).execute(state)
+
+ event = merge_request.resource_state_events.last
+ expect(event.issue).to be_nil
+ expect(event.merge_request).to eq(merge_request)
+ expect(event.state).to eq(state)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/system_notes/issuables_service_spec.rb b/spec/services/system_notes/issuables_service_spec.rb
index 477f9eae39e..c3b3c877583 100644
--- a/spec/services/system_notes/issuables_service_spec.rb
+++ b/spec/services/system_notes/issuables_service_spec.rb
@@ -157,7 +157,18 @@ describe ::SystemNotes::IssuablesService do
describe '#change_status' do
subject { service.change_status(status, source) }
+ context 'when resource state event tracking is enabled' do
+ let(:status) { 'reopened' }
+ let(:source) { nil }
+
+ it { is_expected.to be_nil }
+ end
+
context 'with status reopened' do
+ before do
+ stub_feature_flags(track_resource_state_change_events: false)
+ end
+
let(:status) { 'reopened' }
let(:source) { nil }
@@ -169,6 +180,10 @@ describe ::SystemNotes::IssuablesService do
end
context 'with a source' do
+ before do
+ stub_feature_flags(track_resource_state_change_events: false)
+ end
+
let(:status) { 'opened' }
let(:source) { double('commit', gfm_reference: 'commit 123456') }
diff --git a/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb
new file mode 100644
index 00000000000..f0722beb3ed
--- /dev/null
+++ b/spec/support/shared_contexts/requests/api/graphql/jira_import/jira_projects_context.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+shared_context 'jira projects request context' do
+ let(:url) { 'https://jira.example.com' }
+ let(:username) { 'jira-username' }
+ let(:password) { 'jira-password' }
+ let!(:jira_service) do
+ create(:jira_service,
+ project: project,
+ url: url,
+ username: username,
+ password: password
+ )
+ end
+
+ let_it_be(:jira_projects_json) do
+ '{
+ "self": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=0&maxResults=2",
+ "nextPage": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=2&maxResults=2",
+ "maxResults": 2,
+ "startAt": 0,
+ "total": 7,
+ "isLast": false,
+ "values": [
+ {
+ "self": "https://your-domain.atlassian.net/rest/api/2/project/EX",
+ "id": "10000",
+ "key": "EX",
+ "name": "Example",
+ "avatarUrls": {
+ "48x48": "https://your-domain.atlassian.net/secure/projectavatar?size=large&pid=10000",
+ "24x24": "https://your-domain.atlassian.net/secure/projectavatar?size=small&pid=10000",
+ "16x16": "https://your-domain.atlassian.net/secure/projectavatar?size=xsmall&pid=10000",
+ "32x32": "https://your-domain.atlassian.net/secure/projectavatar?size=medium&pid=10000"
+ },
+ "projectCategory": {
+ "self": "https://your-domain.atlassian.net/rest/api/2/projectCategory/10000",
+ "id": "10000",
+ "name": "FIRST",
+ "description": "First Project Category"
+ },
+ "simplified": false,
+ "style": "classic",
+ "insight": {
+ "totalIssueCount": 100,
+ "lastIssueUpdateTime": "2020-03-31T05:45:24.792+0000"
+ }
+ },
+ {
+ "self": "https://your-domain.atlassian.net/rest/api/2/project/ABC",
+ "id": "10001",
+ "key": "ABC",
+ "name": "Alphabetical",
+ "avatarUrls": {
+ "48x48": "https://your-domain.atlassian.net/secure/projectavatar?size=large&pid=10001",
+ "24x24": "https://your-domain.atlassian.net/secure/projectavatar?size=small&pid=10001",
+ "16x16": "https://your-domain.atlassian.net/secure/projectavatar?size=xsmall&pid=10001",
+ "32x32": "https://your-domain.atlassian.net/secure/projectavatar?size=medium&pid=10001"
+ },
+ "projectCategory": {
+ "self": "https://your-domain.atlassian.net/rest/api/2/projectCategory/10000",
+ "id": "10000",
+ "name": "FIRST",
+ "description": "First Project Category"
+ },
+ "simplified": false,
+ "style": "classic",
+ "insight": {
+ "totalIssueCount": 100,
+ "lastIssueUpdateTime": "2020-03-31T05:45:24.792+0000"
+ }
+ }
+ ]
+ }'
+ end
+
+ let_it_be(:empty_jira_projects_json) do
+ '{
+ "self": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=0&maxResults=2",
+ "nextPage": "https://your-domain.atlassian.net/rest/api/2/project/search?startAt=2&maxResults=2",
+ "maxResults": 2,
+ "startAt": 0,
+ "total": 7,
+ "isLast": false,
+ "values": []
+ }'
+ end
+
+ let(:test_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=0" }
+ let(:start_at_20_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=20" }
+ let(:start_at_1_url) { "#{url}/rest/api/2/project/search?maxResults=50&query=&startAt=1" }
+ let(:max_results_1_url) { "#{url}/rest/api/2/project/search?maxResults=1&query=&startAt=0" }
+
+ before do
+ WebMock.stub_request(:get, test_url).with(basic_auth: [username, password])
+ .to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" })
+ WebMock.stub_request(:get, start_at_20_url).with(basic_auth: [username, password])
+ .to_return(body: empty_jira_projects_json, headers: { "Content-Type": "application/json" })
+ WebMock.stub_request(:get, start_at_1_url).with(basic_auth: [username, password])
+ .to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" })
+ WebMock.stub_request(:get, max_results_1_url).with(basic_auth: [username, password])
+ .to_return(body: jira_projects_json, headers: { "Content-Type": "application/json" })
+ end
+end
diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
index 81433d124c9..6e584cb44e2 100644
--- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb
+++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb
@@ -30,6 +30,8 @@ RSpec.shared_examples 'thread comments' do |resource_name|
click_button 'Comment & close issue'
+ wait_for_all_requests
+
expect(page).to have_content(comment)
expect(page).to have_content "@#{user.username} closed"
diff --git a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
index db5c4b45b70..1ef08de31a9 100644
--- a/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
+++ b/spec/support/shared_examples/serializers/diff_file_entity_shared_examples.rb
@@ -8,7 +8,7 @@ RSpec.shared_examples 'diff file base entity' do
:file_hash, :file_path, :old_path, :new_path,
:viewer, :diff_refs, :stored_externally,
:external_storage, :renamed_file, :deleted_file,
- :a_mode, :b_mode, :new_file)
+ :a_mode, :b_mode, :new_file, :file_identifier_hash)
end
# Converted diff files from GitHub import does not contain blob file