diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-22 15:08:15 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-05-22 15:08:15 +0300 |
commit | 808c799a67a1cf2489a343a6976f55c74aec398b (patch) | |
tree | 4902ff7dbcdd7f3a9dde5ab3bd20dba94835a8a7 | |
parent | 4a3ba3e5f261eb09e6b2b4fd44373e7a1c454a72 (diff) |
Add latest changes from gitlab-org/gitlab@master
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 |