Browse Source

Add latest changes from gitlab-org/gitlab@master

merge-requests/33007/head
GitLab Bot 1 month ago
parent
commit
1691cbe307
67 changed files with 766 additions and 797 deletions
  1. +5
    -1
      Dangerfile
  2. +6
    -0
      app/assets/javascripts/blob/components/blob_header.vue
  3. +7
    -0
      app/assets/javascripts/blob/components/blob_header_default_actions.vue
  4. +9
    -1
      app/assets/javascripts/snippets/components/snippet_blob_view.vue
  5. +1
    -1
      app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
  6. +0
    -1
      app/assets/stylesheets/framework/common.scss
  7. +0
    -18
      app/assets/stylesheets/pages/alert_management/list.scss
  8. +0
    -7
      app/controllers/projects/import/jira_controller.rb
  9. +0
    -2
      app/graphql/resolvers/projects/jira_imports_resolver.rb
  10. +1
    -1
      app/graphql/resolvers/projects/jira_projects_resolver.rb
  11. +7
    -0
      app/helpers/notes_helper.rb
  12. +1
    -6
      app/models/project.rb
  13. +1
    -1
      app/models/user.rb
  14. +4
    -2
      app/views/events/event/_note.html.haml
  15. +9
    -17
      app/views/projects/issues/import_csv/_button.html.haml
  16. +4
    -5
      app/views/projects/issues/index.html.haml
  17. +0
    -1
      app/workers/concerns/gitlab/jira_import/import_worker.rb
  18. +0
    -1
      app/workers/gitlab/jira_import/stage/start_import_worker.rb
  19. +5
    -0
      changelogs/unreleased/218510-hide-copy-btn-for-rendering-error.yml
  20. +5
    -0
      changelogs/unreleased/219002-remove-ghost-column.yml
  21. +5
    -0
      changelogs/unreleased/22834-ff-merge-msg.yml
  22. +5
    -0
      changelogs/unreleased/fix-design-notes-filename-duplication.yml
  23. +5
    -0
      changelogs/unreleased/iterations_add_daterange_constraint.yml
  24. +5
    -0
      changelogs/unreleased/perf-use-build-stubbed.yml
  25. +0
    -1
      config/pseudonymizer.yml
  26. +13
    -0
      db/migrate/20200515152649_enable_btree_gist_extension.rb
  27. +39
    -0
      db/migrate/20200515153633_iteration_date_range_constraint.rb
  28. +19
    -0
      db/migrate/20200525114553_rename_user_type_index.rb
  29. +29
    -0
      db/post_migrate/20200525121014_drop_users_ghost_column.rb
  30. +13
    -4
      db/structure.sql
  31. +1
    -1
      doc/administration/gitaly/index.md
  32. +0
    -1
      doc/ci/yaml/README.md
  33. +5
    -5
      doc/development/telemetry/usage_ping.md
  34. +13
    -8
      doc/development/understanding_explain_plans.md
  35. +3
    -1
      doc/install/aws/index.md
  36. +8
    -14
      doc/integration/akismet.md
  37. BIN
      doc/user/application_security/img/security_configuration_page_v13_1.png
  38. +5
    -14
      locale/gitlab.pot
  39. +1
    -1
      package.json
  40. +104
    -136
      spec/controllers/projects/import/jira_controller_spec.rb
  41. +1
    -4
      spec/features/markdown/mermaid_spec.rb
  42. +8
    -0
      spec/frontend/blob/components/blob_header_default_actions_spec.js
  43. +11
    -0
      spec/frontend/blob/components/blob_header_spec.js
  44. +39
    -73
      spec/frontend/releases/components/app_index_spec.js
  45. +88
    -0
      spec/frontend/releases/mock_data.js
  46. +4
    -4
      spec/frontend/releases/stores/modules/list/actions_spec.js
  47. +0
    -0
      spec/frontend/releases/stores/modules/list/helpers.js
  48. +0
    -0
      spec/frontend/releases/stores/modules/list/mutations_spec.js
  49. +19
    -1
      spec/frontend/snippets/components/snippet_blob_view_spec.js
  50. +1
    -3
      spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js
  51. +0
    -10
      spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb
  52. +30
    -0
      spec/helpers/notes_helper_spec.rb
  53. +1
    -1
      spec/helpers/todos_helper_spec.rb
  54. +0
    -148
      spec/javascripts/releases/mock_data.js
  55. +0
    -14
      spec/lib/gitlab/jira_import/base_importer_spec.rb
  56. +0
    -1
      spec/lib/gitlab/jira_import/issues_importer_spec.rb
  57. +0
    -1
      spec/lib/gitlab/jira_import/labels_importer_spec.rb
  58. +76
    -9
      spec/models/iteration_spec.rb
  59. +27
    -47
      spec/models/project_spec.rb
  60. +22
    -40
      spec/requests/api/graphql/mutations/jira_import/start_spec.rb
  61. +17
    -31
      spec/workers/gitlab/jira_import/stage/finish_import_worker_spec.rb
  62. +5
    -20
      spec/workers/gitlab/jira_import/stage/import_attachments_worker_spec.rb
  63. +27
    -39
      spec/workers/gitlab/jira_import/stage/import_issues_worker_spec.rb
  64. +14
    -29
      spec/workers/gitlab/jira_import/stage/import_labels_worker_spec.rb
  65. +5
    -20
      spec/workers/gitlab/jira_import/stage/import_notes_worker_spec.rb
  66. +29
    -47
      spec/workers/gitlab/jira_import/stage/start_import_worker_spec.rb
  67. +4
    -4
      yarn.lock

+ 5
- 1
Dangerfile View File

@@ -15,4 +15,8 @@ gitlab_danger.rule_names.each do |file|
danger.import_dangerfile(path: File.join('danger', file))
end

markdown("**If needed, you can retry the [`danger-review` job](#{ENV['CI_JOB_URL']}) that generated this comment.**") if gitlab_danger.ci?
anything_to_post = status_report.values.any? { |data| data.any? }

if gitlab_danger.ci? && anything_to_post
markdown("**If needed, you can retry the [`danger-review` job](#{ENV['CI_JOB_URL']}) that generated this comment.**")
end

+ 6
- 0
app/assets/javascripts/blob/components/blob_header.vue View File

@@ -30,6 +30,11 @@ export default {
required: false,
default: SIMPLE_BLOB_VIEWER,
},
hasRenderError: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
@@ -75,6 +80,7 @@ export default {
v-if="showDefaultActions"
:raw-path="blob.rawPath"
:active-viewer="viewer"
:has-render-error="hasRenderError"
@copy="proxyCopyRequest"
/>
</div>


+ 7
- 0
app/assets/javascripts/blob/components/blob_header_default_actions.vue View File

@@ -27,6 +27,11 @@ export default {
default: SIMPLE_BLOB_VIEWER,
required: false,
},
hasRenderError: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
downloadUrl() {
@@ -44,11 +49,13 @@ export default {
<template>
<gl-button-group>
<gl-deprecated-button
v-if="!hasRenderError"
v-gl-tooltip.hover
:aria-label="$options.BTN_COPY_CONTENTS_TITLE"
:title="$options.BTN_COPY_CONTENTS_TITLE"
:disabled="copyDisabled"
data-clipboard-target="#blob-code-content"
data-testid="copyContentsButton"
>
<gl-icon name="copy-to-clipboard" :size="14" />
</gl-deprecated-button>


+ 9
- 1
app/assets/javascripts/snippets/components/snippet_blob_view.vue View File

@@ -74,6 +74,9 @@ export default {
canBeCloned() {
return this.snippet.sshUrlToRepo || this.snippet.httpUrlToRepo;
},
hasRenderError() {
return Boolean(this.viewer.renderError);
},
},
methods: {
switchViewer(newViewer) {
@@ -92,7 +95,12 @@ export default {
<div>
<blob-embeddable v-if="embeddable" class="mb-3" :url="snippet.webUrl" />
<article class="file-holder snippet-file-content">
<blob-header :blob="blob" :active-viewer-type="viewer.type" @viewer-changed="switchViewer">
<blob-header
:blob="blob"
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
@viewer-changed="switchViewer"
>
<template #actions>
<clone-dropdown-button
v-if="canBeCloned"


+ 1
- 1
app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue View File

@@ -127,7 +127,7 @@ export default {
</button>
<span v-if="!rebasingError" class="bold">{{
__(
'Fast-forward merge is not possible. Rebase the source branch onto the target branch or merge target branch into source branch to allow this merge request to be merged.',
'Fast-forward merge is not possible. Rebase the source branch onto the target branch.',
)
}}</span>
<span v-else class="bold danger">{{ rebasingError }}</span>


+ 0
- 1
app/assets/stylesheets/framework/common.scss View File

@@ -425,7 +425,6 @@ img.emoji {
.append-right-32 { margin-right: 32px; }
.append-right-48 { margin-right: 48px; }
.prepend-right-32 { margin-right: 32px; }
.append-bottom-2 { margin-bottom: 2px; }
.append-bottom-4 { margin-bottom: $gl-padding-4; }
.append-bottom-5 { margin-bottom: 5px; }
.append-bottom-8 { margin-bottom: $grid-size; }


+ 0
- 18
app/assets/stylesheets/pages/alert_management/list.scss View File

@@ -1,22 +1,4 @@
.alert-management-list {
// consider adding these stateful variants to @gitlab-ui
// https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/1178
.hover-bg-blue-50:hover {
background-color: $blue-50;
}

.hover-gl-cursor-pointer:hover {
cursor: pointer;
}

.hover-gl-border-b-solid:hover {
@include gl-border-b-solid;
}

.hover-gl-border-blue-200:hover {
border-color: $blue-200;
}

// these styles need to be deleted once GlTable component looks in GitLab same as in @gitlab/ui
table {
color: $gray-700;


+ 0
- 7
app/controllers/projects/import/jira_controller.rb View File

@@ -6,7 +6,6 @@ module Projects
before_action :authenticate_user!
before_action :check_issues_available!
before_action :authorize_read_project!
before_action :jira_import_enabled?
before_action :jira_integration_configured?
before_action :authorize_admin_project!, only: [:import]

@@ -44,12 +43,6 @@ module Projects

private

def jira_import_enabled?
return if @project.jira_issues_import_feature_flag_enabled?

redirect_to project_issues_path(@project)
end

def jira_integration_configured?
return if Feature.enabled?(:jira_issue_import_vue, @project, default_enabled: true)
return if @project.jira_service


+ 0
- 2
app/graphql/resolvers/projects/jira_imports_resolver.rb View File

@@ -14,8 +14,6 @@ module Resolvers
end

def authorized_resource?(project)
return false unless project.jira_issues_import_feature_flag_enabled?

context[:current_user].present? && Ability.allowed?(context[:current_user], :read_project, project)
end
end


+ 1
- 1
app/graphql/resolvers/projects/jira_projects_resolver.rb View File

@@ -20,7 +20,7 @@ module Resolvers
end

def authorized_resource?(project)
Feature.enabled?(:jira_issue_import, project) && Ability.allowed?(context[:current_user], :admin_project, project)
Ability.allowed?(context[:current_user], :admin_project, project)
end

private


+ 7
- 0
app/helpers/notes_helper.rb View File

@@ -3,6 +3,13 @@
module NotesHelper
MAX_PRERENDERED_NOTES = 10

def note_target_title(note)
# The design title is already present in `Event#note_target_reference`.
return if note.nil? || note.for_design?

note.title
end

def note_target_fields(note)
if note.noteable
hidden_field_tag(:target_type, note.noteable.class.name.underscore) +


+ 1
- 6
app/models/project.rb View File

@@ -808,10 +808,6 @@ class Project < ApplicationRecord
Feature.enabled?(:context_commits, default_enabled: true)
end

def jira_issues_import_feature_flag_enabled?
Feature.enabled?(:jira_issue_import, self, default_enabled: true)
end

# LFS and hashed repository storage are required for using Design Management.
def design_management_enabled?
lfs_enabled? && hashed_storage?(:repository)
@@ -900,7 +896,6 @@ class Project < ApplicationRecord
end

def validate_jira_import_settings!(user: nil)
raise Projects::ImportService::Error, _('Jira import feature is disabled.') unless jira_issues_import_feature_flag_enabled?
raise Projects::ImportService::Error, _('Jira integration not configured.') unless jira_service&.active?

if user
@@ -1015,7 +1010,7 @@ class Project < ApplicationRecord
end

def jira_import?
import_type == 'jira' && latest_jira_import.present? && jira_issues_import_feature_flag_enabled?
import_type == 'jira' && latest_jira_import.present?
end

def gitlab_project_import?


+ 1
- 1
app/models/user.rb View File

@@ -1818,7 +1818,7 @@ class User < ApplicationRecord
def update_highest_role?
return false unless persisted?

(previous_changes.keys & %w(state user_type ghost)).any?
(previous_changes.keys & %w(state user_type)).any?
end

def update_highest_role_attribute


+ 4
- 2
app/views/events/event/_note.html.haml View File

@@ -7,8 +7,10 @@
%span.event-type.d-inline-block.append-right-4{ class: event.action_name }
= event.action_name
= event_note_title_html(event)
%span.event-target-title.append-right-4{ dir: "auto" }
= "&quot;".html_safe + event.target.title + "&quot".html_safe
- title = note_target_title(event.target)
- if title.present?
%span.event-target-title.append-right-4{ dir: "auto" }
= "&quot;".html_safe + title + "&quot".html_safe

= render "events/event_scope", event: event



+ 9
- 17
app/views/projects/issues/import_csv/_button.html.haml View File

@@ -1,22 +1,14 @@
- type = local_assigns.fetch(:type, :icon)

- if @project.jira_issues_import_feature_flag_enabled?
.dropdown.btn-group
%button.btn.rounded-right.text-center{ class: ('has-tooltip' if type == :icon), title: (_('Import issues') if type == :icon),
data: { toggle: 'dropdown' }, 'aria-label' => _('Import issues'), 'aria-haspopup' => 'true', 'aria-expanded' => 'false' }
- if type == :icon
= sprite_icon('import')
- else
= _('Import issues')
%ul.dropdown-menu
%li
%button.btn{ data: { toggle: 'modal', target: '.issues-import-modal' } }
= _('Import CSV')
%li= link_to _('Import from Jira'), project_import_jira_path(@project)
- else
%button.csv-import-button.btn{ title: _('Import CSV'), class: ('has-tooltip' if type == :icon),
data: { toggle: 'modal', target: '.issues-import-modal' } }
.dropdown.btn-group
%button.btn.rounded-right.text-center{ class: ('has-tooltip' if type == :icon), title: (_('Import issues') if type == :icon),
data: { toggle: 'dropdown' }, 'aria-label' => _('Import issues'), 'aria-haspopup' => 'true', 'aria-expanded' => 'false' }
- if type == :icon
= sprite_icon('import')
- else
= _('Import CSV')
= _('Import issues')
%ul.dropdown-menu
%li
%button.btn{ data: { toggle: 'modal', target: '.issues-import-modal' } }
= _('Import CSV')
%li= link_to _('Import from Jira'), project_import_jira_path(@project)

+ 4
- 5
app/views/projects/issues/index.html.haml View File

@@ -6,11 +6,10 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} issues")

- if @project.jira_issues_import_feature_flag_enabled?
.js-projects-issues-root{ data: { can_edit: can?(current_user, :admin_project, @project).to_s,
is_jira_configured: @project.jira_service.present?.to_s,
issues_path: project_issues_path(@project),
project_path: @project.full_path } }
.js-projects-issues-root{ data: { can_edit: can?(current_user, :admin_project, @project).to_s,
is_jira_configured: @project.jira_service.present?.to_s,
issues_path: project_issues_path(@project),
project_path: @project.full_path } }

- if project_issues(@project).exists?
.top-area


+ 0
- 1
app/workers/concerns/gitlab/jira_import/import_worker.rb View File

@@ -27,7 +27,6 @@ module Gitlab

def can_import?(project)
return false unless project
return false unless project.jira_issues_import_feature_flag_enabled?

project.latest_jira_import&.started?
end


+ 0
- 1
app/workers/gitlab/jira_import/stage/start_import_worker.rb View File

@@ -25,7 +25,6 @@ module Gitlab

def start_import
return false unless project
return false unless project.jira_issues_import_feature_flag_enabled?
return true if start(project.latest_jira_import)

Gitlab::Import::Logger.info(


+ 5
- 0
changelogs/unreleased/218510-hide-copy-btn-for-rendering-error.yml View File

@@ -0,0 +1,5 @@
---
title: Hid copy contents button when blob has rendering error
merge_request: 32632
author:
type: fixed

+ 5
- 0
changelogs/unreleased/219002-remove-ghost-column.yml View File

@@ -0,0 +1,5 @@
---
title: Remove obsolete users.ghost column
merge_request: 32957
author:
type: other

+ 5
- 0
changelogs/unreleased/22834-ff-merge-msg.yml View File

@@ -0,0 +1,5 @@
---
title: Improve fast-forward merge is not possible message
merge_request: 22834
author: Ben Bodenmiller
type: other

+ 5
- 0
changelogs/unreleased/fix-design-notes-filename-duplication.yml View File

@@ -0,0 +1,5 @@
---
title: Fix filename duplication in design notes in activity feeds
merge_request: 32823
author: Arun Kumar Mohan
type: fixed

+ 5
- 0
changelogs/unreleased/iterations_add_daterange_constraint.yml View File

@@ -0,0 +1,5 @@
---
title: Add btree_gist PGSQL extension and add DB constraints for Iteration date ranges
merge_request: 32335
author:
type: added

+ 5
- 0
changelogs/unreleased/perf-use-build-stubbed.yml View File

@@ -0,0 +1,5 @@
---
title: Use build_stubbed to avoid interacting with the DB in todos helper specs
merge_request: 32906
author: Arun Kumar Mohan
type: performance

+ 0
- 1
config/pseudonymizer.yml View File

@@ -465,7 +465,6 @@ tables:
- auditor
- require_two_factor_authentication_from_group
- two_factor_grace_period
- ghost
- last_activity_on
- notified_of_own_activity
- user_type


+ 13
- 0
db/migrate/20200515152649_enable_btree_gist_extension.rb View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true

class EnableBtreeGistExtension < ActiveRecord::Migration[6.0]
DOWNTIME = false

def up
execute 'CREATE EXTENSION IF NOT EXISTS btree_gist'
end

def down
execute 'DROP EXTENSION IF EXISTS btree_gist'
end
end

+ 39
- 0
db/migrate/20200515153633_iteration_date_range_constraint.rb View File

@@ -0,0 +1,39 @@
# frozen_string_literal: true

class IterationDateRangeConstraint < ActiveRecord::Migration[6.0]
DOWNTIME = false

def up
execute <<~SQL
ALTER TABLE sprints
ADD CONSTRAINT iteration_start_and_due_daterange_project_id_constraint
EXCLUDE USING gist
( project_id WITH =,
daterange(start_date, due_date, '[]') WITH &&
)
WHERE (project_id IS NOT NULL)
SQL

execute <<~SQL
ALTER TABLE sprints
ADD CONSTRAINT iteration_start_and_due_daterange_group_id_constraint
EXCLUDE USING gist
( group_id WITH =,
daterange(start_date, due_date, '[]') WITH &&
)
WHERE (group_id IS NOT NULL)
SQL
end

def down
execute <<~SQL
ALTER TABLE sprints
DROP CONSTRAINT IF EXISTS iteration_start_and_due_daterange_project_id_constraint
SQL

execute <<~SQL
ALTER TABLE sprints
DROP CONSTRAINT IF EXISTS iteration_start_and_due_daterange_group_id_constraint
SQL
end
end

+ 19
- 0
db/migrate/20200525114553_rename_user_type_index.rb View File

@@ -0,0 +1,19 @@
# frozen_string_literal: true

class RenameUserTypeIndex < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers

DOWNTIME = false

disable_ddl_transaction!

def up
add_concurrent_index :users, [:state, :user_type], name: 'index_users_on_state_and_user_type'
remove_concurrent_index_by_name :users, 'index_users_on_state_and_user_type_internal'
end

def down
add_concurrent_index :users, [:state, :user_type], where: 'ghost IS NOT TRUE', name: 'index_users_on_state_and_user_type_internal'
remove_concurrent_index_by_name :users, 'index_users_on_state_and_user_type'
end
end

+ 29
- 0
db/post_migrate/20200525121014_drop_users_ghost_column.rb View File

@@ -0,0 +1,29 @@
# frozen_string_literal: true

class DropUsersGhostColumn < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers

DOWNTIME = false

disable_ddl_transaction!

def up
remove_concurrent_index_by_name :users, 'index_users_on_ghost'

with_lock_retries do
remove_column :users, :ghost
end
end

def down
unless column_exists?(:users, :ghost)
with_lock_retries do
add_column :users, :ghost, :boolean # rubocop:disable Migration/AddColumnsToWideTables
end
end

execute 'UPDATE users set ghost = TRUE WHERE user_type = 5'

add_concurrent_index :users, :ghost
end
end

+ 13
- 4
db/structure.sql View File

@@ -2,6 +2,8 @@ SET search_path=public;

CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;

CREATE EXTENSION IF NOT EXISTS btree_gist WITH SCHEMA public;

CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public;

CREATE TABLE public.abuse_reports (
@@ -6806,7 +6808,6 @@ CREATE TABLE public.users (
auditor boolean DEFAULT false NOT NULL,
require_two_factor_authentication_from_group boolean DEFAULT false NOT NULL,
two_factor_grace_period integer DEFAULT 48 NOT NULL,
ghost boolean,
last_activity_on date,
notified_of_own_activity boolean,
preferred_language character varying,
@@ -8410,6 +8411,12 @@ ALTER TABLE ONLY public.issue_user_mentions
ALTER TABLE ONLY public.issues
ADD CONSTRAINT issues_pkey PRIMARY KEY (id);

ALTER TABLE ONLY public.sprints
ADD CONSTRAINT iteration_start_and_due_daterange_group_id_constraint EXCLUDE USING gist (group_id WITH =, daterange(start_date, due_date, '[]'::text) WITH &&) WHERE ((group_id IS NOT NULL));

ALTER TABLE ONLY public.sprints
ADD CONSTRAINT iteration_start_and_due_daterange_project_id_constraint EXCLUDE USING gist (project_id WITH =, daterange(start_date, due_date, '[]'::text) WITH &&) WHERE ((project_id IS NOT NULL));

ALTER TABLE ONLY public.jira_connect_installations
ADD CONSTRAINT jira_connect_installations_pkey PRIMARY KEY (id);

@@ -10826,8 +10833,6 @@ CREATE INDEX index_users_on_email_trigram ON public.users USING gin (email publi

CREATE INDEX index_users_on_feed_token ON public.users USING btree (feed_token);

CREATE INDEX index_users_on_ghost ON public.users USING btree (ghost);

CREATE INDEX index_users_on_group_view ON public.users USING btree (group_view);

CREATE INDEX index_users_on_incoming_email_token ON public.users USING btree (incoming_email_token);
@@ -10844,7 +10849,7 @@ CREATE UNIQUE INDEX index_users_on_reset_password_token ON public.users USING bt

CREATE INDEX index_users_on_state ON public.users USING btree (state);

CREATE INDEX index_users_on_state_and_user_type_internal ON public.users USING btree (state, user_type) WHERE (ghost IS NOT TRUE);
CREATE INDEX index_users_on_state_and_user_type ON public.users USING btree (state, user_type);

CREATE UNIQUE INDEX index_users_on_static_object_token ON public.users USING btree (static_object_token);

@@ -13922,8 +13927,12 @@ COPY "schema_migrations" (version) FROM STDIN;
20200514000009
20200514000132
20200514000340
20200515152649
20200515153633
20200515155620
20200519115908
20200519171058
20200525114553
20200525121014
\.


+ 1
- 1
doc/administration/gitaly/index.md View File

@@ -199,6 +199,7 @@ authentication](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/configurati
sidekiq['enable'] = false
gitlab_workhorse['enable'] = false
grafana['enable'] = false
gitlab_exporter['enable'] = false

# If you run a separate monitoring node you can disable these services
alertmanager['enable'] = false
@@ -211,7 +212,6 @@ authentication](https://gitlab.com/gitlab-org/gitaly/blob/master/doc/configurati
# prometheus['monitor_kubernetes'] = false

# If you don't want to run monitoring services uncomment the following (not recommended)
# gitlab_exporter['enable'] = false
# node_exporter['enable'] = false

# Prevent database connections during 'gitlab-ctl reconfigure'


+ 0
- 1
doc/ci/yaml/README.md View File

@@ -310,7 +310,6 @@ include:
- template: 'Workflows/Branch-Pipelines.gitlab-ci.yml'
```

The [`MergeRequest-Pipelines` include](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Workflows/MergeRequest-Pipelines.gitlab-ci.yml) sets your pipelines to run for the default branch (usually `master`), tags, and
The [`MergeRequest-Pipelines` template](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates/Workflows/MergeRequest-Pipelines.gitlab-ci.yml)
makes your pipelines run for the default branch (usually `master`), tags, and
all types of merge request pipelines. Use this template if you use any of the


+ 5
- 5
doc/development/telemetry/usage_ping.md View File

@@ -257,11 +257,11 @@ Your Rails console will return the generated SQL queries.
Example:

```ruby
pry(main)> Gitlab::UsageData.count(User.active)
(0.4ms) SELECT "features"."key" FROM "features"
(0.7ms) SELECT MIN("users"."id") FROM "users" WHERE ("users"."state" IN ('active')) AND (ghost IS NOT TRUE) AND ("users"."user_type" IS NULL OR "users"."user_type" NOT IN (2, 1, 3))
(0.6ms) SELECT MAX("users"."id") FROM "users" WHERE ("users"."state" IN ('active')) AND (ghost IS NOT TRUE) AND ("users"."user_type" IS NULL OR "users"."user_type" NOT IN (2, 1, 3))
(0.5ms) SELECT COUNT("users"."id") FROM "users" WHERE ("users"."state" IN ('active')) AND (ghost IS NOT TRUE) AND ("users"."user_type" IS NULL OR "users"."user_type" NOT IN (2, 1, 3)) AND "users"."id" BETWEEN 0 AND 99999
pry(main)> Gitlab::UsageData.count(User.active)
(2.6ms) SELECT "features"."key" FROM "features"
(15.3ms) SELECT MIN("users"."id") FROM "users" WHERE ("users"."state" IN ('active')) AND ("users"."user_type" IS NULL OR "users"."user_type" IN (6, 4))
(2.4ms) SELECT MAX("users"."id") FROM "users" WHERE ("users"."state" IN ('active')) AND ("users"."user_type" IS NULL OR "users"."user_type" IN (6, 4))
(1.9ms) SELECT COUNT("users"."id") FROM "users" WHERE ("users"."state" IN ('active')) AND ("users"."user_type" IS NULL OR "users"."user_type" IN (6, 4)) AND "users"."id" BETWEEN 1 AND 100000
```

### 3. Optimize queries with #database-lab


+ 13
- 8
doc/development/understanding_explain_plans.md View File

@@ -315,25 +315,30 @@ the `Indexes:` section:
```sql
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
"users_confirmation_token_key" UNIQUE CONSTRAINT, btree (confirmation_token)
"users_email_key" UNIQUE CONSTRAINT, btree (email)
"users_reset_password_token_key" UNIQUE CONSTRAINT, btree (reset_password_token)
"index_on_users_lower_email" btree (lower(email::text))
"index_on_users_lower_username" btree (lower(username::text))
"index_users_on_confirmation_token" UNIQUE, btree (confirmation_token)
"index_users_on_email" UNIQUE, btree (email)
"index_users_on_reset_password_token" UNIQUE, btree (reset_password_token)
"index_users_on_static_object_token" UNIQUE, btree (static_object_token)
"index_users_on_unlock_token" UNIQUE, btree (unlock_token)
"index_on_users_name_lower" btree (lower(name::text))
"index_users_on_accepted_term_id" btree (accepted_term_id)
"index_users_on_admin" btree (admin)
"index_users_on_created_at" btree (created_at)
"index_users_on_email_trigram" gin (email gin_trgm_ops)
"index_users_on_feed_token" btree (feed_token)
"index_users_on_ghost" btree (ghost)
"index_users_on_group_view" btree (group_view)
"index_users_on_incoming_email_token" btree (incoming_email_token)
"index_users_on_managing_group_id" btree (managing_group_id)
"index_users_on_name" btree (name)
"index_users_on_name_trigram" gin (name gin_trgm_ops)
"index_users_on_public_email" btree (public_email) WHERE public_email::text <> ''::text
"index_users_on_state" btree (state)
"index_users_on_state_and_internal_attrs" btree (state) WHERE ghost <> true AND support_bot <> true
"index_users_on_support_bot" btree (support_bot)
"index_users_on_state_and_user_type" btree (state, user_type)
"index_users_on_unconfirmed_email" btree (unconfirmed_email) WHERE unconfirmed_email IS NOT NULL
"index_users_on_user_type" btree (user_type)
"index_users_on_username" btree (username)
"index_users_on_username_trigram" gin (username gin_trgm_ops)
"tmp_idx_on_user_id_where_bio_is_filled" btree (id) WHERE COALESCE(bio, ''::character varying)::text IS DISTINCT FROM ''::text
```

Here we can see there is no index on the `twitter` column, which means


+ 3
- 1
doc/install/aws/index.md View File

@@ -237,7 +237,9 @@ On the EC2 dashboard, look for Load Balancer in the left navigation bar:
1. Click **Assign Security Groups** and select **Create a new security group**, give it a name
(we'll use `gitlab-loadbalancer-sec-group`) and description, and allow both HTTP and HTTPS traffic
from anywhere (`0.0.0.0/0, ::/0`). Also allow SSH traffic from a single IP address or an IP address range in CIDR notation.
1. Click **Configure Security Settings** and select an SSL/TLS certificate from ACM or upload a certificate to IAM.
1. Click **Configure Security Settings** and set the following:
1. Select an SSL/TLS certificate from ACM or upload a certificate to IAM.
1. Under **Select a Cipher**, pick a predefined security policy from the dropdown. You can see a breakdown of [Predefined SSL Security Policies for Classic Load Balancers](https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/elb-security-policy-table.html) in the AWS docs. Check the GitLab codebase for a list of [supported SSL ciphers and protocols](https://gitlab.com/gitlab-org/gitlab/-/blob/9ee7ad433269b37251e0dd5b5e00a0f00d8126b4/lib/support/nginx/gitlab-ssl#L97-99).
1. Click **Configure Health Check** and set up a health check for your EC2 instances.
1. For **Ping Protocol**, select HTTP.
1. For **Ping Port**, enter 80.


+ 8
- 14
doc/integration/akismet.md View File

@@ -1,42 +1,36 @@
# Akismet

> *Note:* Before 8.11 only issues submitted via the API and for non-project
members were submitted to Akismet.

GitLab leverages [Akismet](https://akismet.com/) to protect against spam. Currently
GitLab uses Akismet to prevent the creation of spam issues on public projects. Issues
created via the WebUI or the API can be submitted to Akismet for review.
created via the web UI or the API can be submitted to Akismet for review.

Detected spam will be rejected, and an entry in the "Spam Log" section in the
Admin page will be created.

Privacy note: GitLab submits the user's IP and user agent to Akismet. Note that
adding a user to a project will disable the Akismet check and prevent this
from happening.
Privacy note: GitLab submits the user's IP and user agent to Akismet.

NOTE: **Note:**
In GitLab 8.11 and later, all issues are submitted to Akismet.
In earlier GitLab versions, this only applied to API and non-project members.

## Configuration

To use Akismet:

1. Go to the URL: <https://akismet.com/account/>

1. Sign-in or create a new account.

1. Click on **Show** to reveal the API key.

1. Go to **Admin Area > Settings > Reporting** (`/admin/application_settings/reporting`).

1. Check the **Enable Akismet** checkbox.

1. Fill in the API key from step 3.

1. Save the configuration.

![Screenshot of Akismet settings](img/akismet_settings.png)

## Training

> *Note:* Training the Akismet filter is only available in 8.11 and above.
NOTE: **Note:**
Training the Akismet filter is only available in GitLab 8.11 and later.

As a way to better recognize between spam and ham, you can train the Akismet
filter whenever there is a false positive or false negative.


BIN
doc/user/application_security/img/security_configuration_page_v13_1.png View File

Before After
Width: 1200  |  Height: 805  |  Size: 62 KiB Width: 1559  |  Height: 872  |  Size: 195 KiB

+ 5
- 14
locale/gitlab.pot View File

@@ -5699,9 +5699,6 @@ msgstr ""
msgid "Configure Prometheus"
msgstr ""

msgid "Configure Security %{wordBreakOpportunity}and Compliance"
msgstr ""

msgid "Configure Tracing"
msgstr ""

@@ -9291,7 +9288,7 @@ msgstr ""
msgid "Fast-forward merge is not possible. Rebase the source branch onto %{targetBranch} to allow this merge request to be merged."
msgstr ""

msgid "Fast-forward merge is not possible. Rebase the source branch onto the target branch or merge target branch into source branch to allow this merge request to be merged."
msgid "Fast-forward merge is not possible. Rebase the source branch onto the target branch."
msgstr ""

msgid "Fast-forward merge without a merge commit"
@@ -12204,9 +12201,6 @@ msgstr ""
msgid "Jira Issue Import"
msgstr ""

msgid "Jira import feature is disabled."
msgstr ""

msgid "Jira import is already running."
msgstr ""

@@ -18994,9 +18988,6 @@ msgstr ""
msgid "Security Dashboard"
msgstr ""

msgid "Security configuration help link"
msgstr ""

msgid "Security dashboard"
msgstr ""

@@ -19009,21 +19000,21 @@ msgstr ""
msgid "SecurityConfiguration|Enabled"
msgstr ""

msgid "SecurityConfiguration|Feature"
msgstr ""

msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""

msgid "SecurityConfiguration|Not yet enabled"
msgstr ""

msgid "SecurityConfiguration|Secure features"
msgid "SecurityConfiguration|Security Control"
msgstr ""

msgid "SecurityConfiguration|Status"
msgstr ""

msgid "SecurityConfiguration|Testing & Compliance"
msgstr ""

msgid "SecurityReports|%{firstProject} and %{secondProject}"
msgstr ""



+ 1
- 1
package.json View File

@@ -41,7 +41,7 @@
"@babel/preset-env": "^7.8.4",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.130.0",
"@gitlab/ui": "15.5.0",
"@gitlab/ui": "15.6.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3",
"@sentry/browser": "^5.10.2",


+ 104
- 136
spec/controllers/projects/import/jira_controller_spec.rb View File

@@ -10,10 +10,6 @@ describe Projects::Import::JiraController do
let_it_be(:jira_project_key) { 'Test' }

context 'with anonymous user' do
before do
stub_feature_flags(jira_issue_import: true)
end

context 'get show' do
it 'redirects to issues page' do
get :show, params: { namespace_id: project.namespace, project_id: project }
@@ -35,195 +31,167 @@ describe Projects::Import::JiraController do
before do
sign_in(user)
project.add_maintainer(user)
stub_feature_flags(jira_issue_import_vue: false)
stub_jira_service_test
end

context 'when feature flag not enabled' do
before do
stub_feature_flags(jira_issue_import: false)
end
context 'when Jira service is enabled for the project' do
let_it_be(:jira_service) { create(:jira_service, project: project) }

context 'get show' do
it 'redirects to issues page' do
get :show, params: { namespace_id: project.namespace, project_id: project }
context 'when user is developer' do
let_it_be(:dev) { create(:user) }

expect(response).to redirect_to(project_issues_path(project))
before do
sign_in(dev)
project.add_developer(dev)
end
end

context 'post import' do
it 'redirects to issues page' do
post :import, params: { namespace_id: project.namespace, project_id: project, jira_project_key: jira_project_key }

expect(response).to redirect_to(project_issues_path(project))
end
end
end

context 'when feature flag enabled' do
before do
stub_feature_flags(jira_issue_import: true)
stub_feature_flags(jira_issue_import_vue: false)
stub_jira_service_test
end

context 'when Jira service is enabled for the project' do
let_it_be(:jira_service) { create(:jira_service, project: project) }

context 'when user is developer' do
let_it_be(:dev) { create(:user) }

context 'get show' do
before do
sign_in(dev)
project.add_developer(dev)
get :show, params: { namespace_id: project.namespace.to_param, project_id: project }
end

context 'get show' do
before do
get :show, params: { namespace_id: project.namespace.to_param, project_id: project }
end

it 'does not query Jira service' do
expect(project).not_to receive(:jira_service)
end
it 'does not query Jira service' do
expect(project).not_to receive(:jira_service)
end

it 'renders show template' do
expect(response).to render_template(:show)
expect(assigns(:jira_projects)).not_to be_present
end
it 'renders show template' do
expect(response).to render_template(:show)
expect(assigns(:jira_projects)).not_to be_present
end
end

context 'post import' do
it 'returns 404' do
post :import, params: { namespace_id: project.namespace, project_id: project, jira_project_key: jira_project_key }
context 'post import' do
it 'returns 404' do
post :import, params: { namespace_id: project.namespace, project_id: project, jira_project_key: jira_project_key }

expect(response).to have_gitlab_http_status(:not_found)
end
expect(response).to have_gitlab_http_status(:not_found)
end
end
end

context 'when issues disabled' do
let_it_be(:disabled_issues_project) { create(:project, :public, :issues_disabled) }
context 'when issues disabled' do
let_it_be(:disabled_issues_project) { create(:project, :public, :issues_disabled) }

context 'get show' do
it 'returs 404' do
get :show, params: { namespace_id: project.namespace.to_param, project_id: disabled_issues_project }
context 'get show' do
it 'returs 404' do
get :show, params: { namespace_id: project.namespace.to_param, project_id: disabled_issues_project }

expect(response).to have_gitlab_http_status(:not_found)
end
expect(response).to have_gitlab_http_status(:not_found)
end
end

context 'post import' do
it 'returs 404' do
post :import, params: { namespace_id: disabled_issues_project.namespace, project_id: disabled_issues_project, jira_project_key: jira_project_key }
context 'post import' do
it 'returs 404' do
post :import, params: { namespace_id: disabled_issues_project.namespace, project_id: disabled_issues_project, jira_project_key: jira_project_key }

expect(response).to have_gitlab_http_status(:not_found)
end
expect(response).to have_gitlab_http_status(:not_found)
end
end
end

context 'when running Jira import first time' do
context 'get show' do
before do
allow(JIRA::Resource::Project).to receive(:all).and_return(jira_projects)
context 'when running Jira import first time' do
context 'get show' do
before do
allow(JIRA::Resource::Project).to receive(:all).and_return(jira_projects)

expect(project.jira_imports).to be_empty
expect(project.jira_imports).to be_empty

get :show, params: { namespace_id: project.namespace.to_param, project_id: project }
end
get :show, params: { namespace_id: project.namespace.to_param, project_id: project }
end

context 'when no projects have been retrieved from Jira' do
let(:jira_projects) { [] }
context 'when no projects have been retrieved from Jira' do
let(:jira_projects) { [] }

it 'render an error message' do
expect(flash[:alert]).to eq('No projects have been returned from Jira. Please check your Jira configuration.')
expect(response).to render_template(:show)
end
it 'render an error message' do
expect(flash[:alert]).to eq('No projects have been returned from Jira. Please check your Jira configuration.')
expect(response).to render_template(:show)
end
end

context 'when projects retrieved from Jira' do
let(:jira_projects) { [double(name: 'FOO project', key: 'FOO')] }
context 'when projects retrieved from Jira' do
let(:jira_projects) { [double(name: 'FOO project', key: 'FOO')] }

it 'renders show template' do
expect(response).to render_template(:show)
end
it 'renders show template' do
expect(response).to render_template(:show)
end
end
end

context 'post import' do
context 'when Jira project key is empty' do
it 'redirects back to show with an error' do
post :import, params: { namespace_id: project.namespace, project_id: project, jira_project_key: '' }
context 'post import' do
context 'when Jira project key is empty' do
it 'redirects back to show with an error' do
post :import, params: { namespace_id: project.namespace, project_id: project, jira_project_key: '' }

expect(response).to redirect_to(project_import_jira_path(project))
expect(flash[:alert]).to eq('No Jira project key has been provided.')
end
expect(response).to redirect_to(project_import_jira_path(project))
expect(flash[:alert]).to eq('No Jira project key has been provided.')
end
end

context 'when everything is ok' do
it 'creates import state' do
expect(project.latest_jira_import).to be_nil
context 'when everything is ok' do
it 'creates import state' do
expect(project.latest_jira_import).to be_nil

post :import, params: { namespace_id: project.namespace, project_id: project, jira_project_key: jira_project_key }
post :import, params: { namespace_id: project.namespace, project_id: project, jira_project_key: jira_project_key }

project.reload
project.reload

jira_import = project.latest_jira_import
expect(project.import_type).to eq 'jira'
expect(jira_import.status).to eq 'scheduled'
expect(jira_import.jira_project_key).to eq jira_project_key
expect(response).to redirect_to(project_import_jira_path(project))
end
jira_import = project.latest_jira_import
expect(project.import_type).to eq 'jira'
expect(jira_import.status).to eq 'scheduled'
expect(jira_import.jira_project_key).to eq jira_project_key
expect(response).to redirect_to(project_import_jira_path(project))
end
end
end
end

context 'when import state is scheduled' do
let_it_be(:jira_import_state) { create(:jira_import_state, :scheduled, project: project) }
context 'when import state is scheduled' do
let_it_be(:jira_import_state) { create(:jira_import_state, :scheduled, project: project) }

context 'get show' do
it 'renders import status' do
get :show, params: { namespace_id: project.namespace.to_param, project_id: project }
context 'get show' do
it 'renders import status' do
get :show, params: { namespace_id: project.namespace.to_param, project_id: project }

jira_import = project.latest_jira_import
expect(jira_import.status).to eq 'scheduled'
expect(flash.now[:notice]).to eq 'Import scheduled'
end
jira_import = project.latest_jira_import
expect(jira_import.status).to eq 'scheduled'
expect(flash.now[:notice]).to eq 'Import scheduled'
end
end

context 'post import' do
it 'uses the existing import data' do
post :import, params: { namespace_id: project.namespace, project_id: project, jira_project_key: 'New Project' }
context 'post import' do
it 'uses the existing import data' do
post :import, params: { namespace_id: project.namespace, project_id: project, jira_project_key: 'New Project' }

expect(flash[:notice]).to eq('Jira import is already running.')
expect(response).to redirect_to(project_import_jira_path(project))
end
expect(flash[:notice]).to eq('Jira import is already running.')
expect(response).to redirect_to(project_import_jira_path(project))
end
end
end

context 'when Jira import ran before' do
let_it_be(:jira_import_state) { create(:jira_import_state, :finished, project: project, jira_project_key: jira_project_key) }
context 'when Jira import ran before' do
let_it_be(:jira_import_state) { create(:jira_import_state, :finished, project: project, jira_project_key: jira_project_key) }

context 'get show' do
it 'renders import status' do
allow(JIRA::Resource::Project).to receive(:all).and_return([])
get :show, params: { namespace_id: project.namespace.to_param, project_id: project }
context 'get show' do
it 'renders import status' do
allow(JIRA::Resource::Project).to receive(:all).and_return([])
get :show, params: { namespace_id: project.namespace.to_param, project_id: project }

expect(project.latest_jira_import.status).to eq 'finished'
expect(flash.now[:notice]).to eq 'Import finished'
end
expect(project.latest_jira_import.status).to eq 'finished'
expect(flash.now[:notice]).to eq 'Import finished'
end
end

context 'post import' do
it 'uses the existing import data' do
post :import, params: { namespace_id: project.namespace, project_id: project, jira_project_key: 'New Project' }
context 'post import' do
it 'uses the existing import data' do
post :import, params: { namespace_id: project.namespace, project_id: project, jira_project_key: 'New Project' }

project.reload
expect(project.latest_jira_import.status).to eq 'scheduled'
expect(project.jira_imports.size).to eq 2
expect(project.jira_imports.first.jira_project_key).to eq jira_project_key
expect(project.jira_imports.last.jira_project_key).to eq 'New Project'
expect(response).to redirect_to(project_import_jira_path(project))
end
project.reload
expect(project.latest_jira_import.status).to eq 'scheduled'
expect(project.jira_imports.size).to eq 2
expect(project.jira_imports.first.jira_project_key).to eq jira_project_key
expect(project.jira_imports.last.jira_project_key).to eq 'New Project'
expect(response).to redirect_to(project_import_jira_path(project))
end
end
end


+ 1
- 4
spec/features/markdown/mermaid_spec.rb View File

@@ -118,10 +118,7 @@ describe 'Mermaid rendering', :js do

visit project_issue_path(project, issue)

svg = page.find('svg.mermaid')
expect(svg[:style]).to match(/max-width/)
expect(svg[:width].to_i).to eq(100)
expect(svg[:height].to_i).to eq(0)
expect(page).to have_css('svg.mermaid[style*="max-width"][width="100%"]')
end

it 'display button when diagram exceeds length', :js do


+ 8
- 0
spec/frontend/blob/components/blob_header_default_actions_spec.js View File

@@ -66,5 +66,13 @@ describe('Blob Header Default Actions', () => {

expect(buttons.at(0).attributes('disabled')).toBeTruthy();
});

it('does not render the copy button if a rendering error is set', () => {
createComponent({
hasRenderError: true,
});

expect(wrapper.find('[data-testid="copyContentsButton"]').exists()).toBe(false);
});
});
});

+ 11
- 0
spec/frontend/blob/components/blob_header_spec.js View File

@@ -87,6 +87,17 @@ describe('Blob Header Default Actions', () => {
expect(wrapper.text()).toContain(slotContent);
});
});

it('passes information about render error down to default actions', () => {
createComponent(
{},
{},
{
hasRenderError: true,
},
);
expect(wrapper.find(DefaultActions).props('hasRenderError')).toBe(true);
});
});

describe('functionality', () => {


spec/javascripts/releases/components/app_index_spec.js → spec/frontend/releases/components/app_index_spec.js View File

@@ -1,6 +1,6 @@
import { range as rge } from 'lodash';
import Vue from 'vue';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import app from '~/releases/components/app_index.vue';
import createStore from '~/releases/stores';
import listModule from '~/releases/stores/modules/list';
@@ -9,11 +9,11 @@ import { resetStore } from '../stores/modules/list/helpers';
import {
pageInfoHeadersWithoutPagination,
pageInfoHeadersWithPagination,
release,
release2 as release,
releases,
} from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import waitForPromises from 'spec/helpers/wait_for_promises';
import waitForPromises from 'helpers/wait_for_promises';

describe('Releases App ', () => {
const Component = Vue.extend(app);
@@ -42,83 +42,67 @@ describe('Releases App ', () => {

describe('while loading', () => {
beforeEach(() => {
spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [], headers: {} }));
jest
.spyOn(api, 'releases')
// Need to defer the return value here to the next stack,
// otherwise the loading state disappears before our test even starts.
.mockImplementation(() => waitForPromises().then(() => ({ data: [], headers: {} })));
vm = mountComponentWithStore(Component, { props, store });
});

it('renders loading icon', done => {
it('renders loading icon', () => {
expect(vm.$el.querySelector('.js-loading')).not.toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull();

waitForPromises()
.then(done)
.catch(done.fail);
return waitForPromises();
});
});

describe('with successful request', () => {
beforeEach(() => {
spyOn(api, 'releases').and.returnValue(
Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination }),
);
jest
.spyOn(api, 'releases')
.mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination });
vm = mountComponentWithStore(Component, { props, store });
});

it('renders success state', done => {
waitForPromises()
.then(() => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull();

done();
})
.catch(done.fail);
it('renders success state', () => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
});
});

describe('with successful request and pagination', () => {
beforeEach(() => {
spyOn(api, 'releases').and.returnValue(
Promise.resolve({ data: releasesPagination, headers: pageInfoHeadersWithPagination }),
);
jest
.spyOn(api, 'releases')
.mockResolvedValue({ data: releasesPagination, headers: pageInfoHeadersWithPagination });
vm = mountComponentWithStore(Component, { props, store });
});

it('renders success state', done => {
waitForPromises()
.then(() => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).not.toBeNull();

done();
})
.catch(done.fail);
it('renders success state', () => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).toBeNull();
expect(vm.$el.querySelector('.js-success-state')).not.toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).not.toBeNull();
});
});

describe('with empty request', () => {
beforeEach(() => {
spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [], headers: {} }));
jest.spyOn(api, 'releases').mockResolvedValue({ data: [], headers: {} });
vm = mountComponentWithStore(Component, { props, store });
});

it('renders empty state', done => {
waitForPromises()
.then(() => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).not.toBeNull();
expect(vm.$el.querySelector('.js-success-state')).toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull();

done();
})
.catch(done.fail);
it('renders empty state', () => {
expect(vm.$el.querySelector('.js-loading')).toBeNull();
expect(vm.$el.querySelector('.js-empty-state')).not.toBeNull();
expect(vm.$el.querySelector('.js-success-state')).toBeNull();
expect(vm.$el.querySelector('.gl-pagination')).toBeNull();
});
});

@@ -126,7 +110,7 @@ describe('Releases App ', () => {
const findNewReleaseButton = () => vm.$el.querySelector('.js-new-release-btn');

beforeEach(() => {
spyOn(api, 'releases').and.returnValue(Promise.resolve({ data: [], headers: {} }));
jest.spyOn(api, 'releases').mockResolvedValue({ data: [], headers: {} });
});

const factory = additionalProps => {
@@ -146,38 +130,20 @@ describe('Releases App ', () => {
factory({ newReleasePath });
});

it('renders the "New release" button', done => {
waitForPromises()
.then(() => {
expect(findNewReleaseButton()).not.toBeNull();

done();
})
.catch(done.fail);
it('renders the "New release" button', () => {
expect(findNewReleaseButton()).not.toBeNull();
});

it('renders the "New release" button with the correct href', done => {
waitForPromises()
.then(() => {
expect(findNewReleaseButton().getAttribute('href')).toBe(newReleasePath);

done();
})
.catch(done.fail);
it('renders the "New release" button with the correct href', () => {
expect(findNewReleaseButton().getAttribute('href')).toBe(newReleasePath);
});
});

describe('when the user is not allowed to create a new Release', () => {
beforeEach(() => factory());

it('does not render the "New release" button', done => {
waitForPromises()
.then(() => {
expect(findNewReleaseButton()).toBeNull();

done();
})
.catch(done.fail);
it('does not render the "New release" button', () => {
expect(findNewReleaseButton()).toBeNull();
});
});
});

+ 88
- 0
spec/frontend/releases/mock_data.js View File

@@ -131,3 +131,91 @@ export const release = {
edit_url: 'http://0.0.0.0:3001/root/release-test/-/releases/v0.3/edit',
},
};

export const pageInfoHeadersWithoutPagination = {
'X-NEXT-PAGE': '',
'X-PAGE': '1',
'X-PER-PAGE': '20',
'X-PREV-PAGE': '',
'X-TOTAL': '19',
'X-TOTAL-PAGES': '1',
};

export const pageInfoHeadersWithPagination = {
'X-NEXT-PAGE': '2',
'X-PAGE': '1',
'X-PER-PAGE': '20',
'X-PREV-PAGE': '',
'X-TOTAL': '21',
'X-TOTAL-PAGES': '2',
};

export const release2 = {
name: 'Bionic Beaver',
tag_name: '18.04',
description: '## changelog\n\n* line 1\n* line2',
description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
author_name: 'Release bot',
author_email: 'release-bot@example.com',
created_at: '2012-05-28T05:00:00-07:00',
commit: {
id: '2695effb5807a22ff3d138d593fd856244e155e7',
short_id: '2695effb',
title: 'Initial commit',
created_at: '2017-07-26T11:08:53.000+02:00',
parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'],
message: 'Initial commit',
author: {
avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
id: 482476,
name: 'John Doe',
path: '/johndoe',
state: 'active',
status_tooltip_html: null,
username: 'johndoe',
web_url: 'https://gitlab.com/johndoe',
},
authored_date: '2012-05-28T04:42:42-07:00',
committer_name: 'Jack Smith',
committer_email: 'jack@example.com',
committed_date: '2012-05-28T04:42:42-07:00',
},
assets: {
count: 6,
sources: [
{
format: 'zip',
url: 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.zip',
},
{
format: 'tar.gz',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz',
},
{
format: 'tar.bz2',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2',
},
{
format: 'tar',
url: 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar',
},
],
links: [
{
name: 'release-18.04.dmg',
url: 'https://my-external-hosting.example.com/scrambled-url/',
external: true,
},
{
name: 'binary-linux-amd64',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
external: false,
},
],
},
};

export const releases = [release, release2];

spec/javascripts/releases/stores/modules/list/actions_spec.js → spec/frontend/releases/stores/modules/list/actions_spec.js View File

@@ -1,4 +1,4 @@
import testAction from 'spec/helpers/vuex_action_helper';
import testAction from 'helpers/vuex_action_helper';
import {
requestReleases,
fetchReleases,
@@ -31,7 +31,7 @@ describe('Releases State actions', () => {
describe('fetchReleases', () => {
describe('success', () => {
it('dispatches requestReleases and receiveReleasesSuccess', done => {
spyOn(api, 'releases').and.callFake((id, options) => {
jest.spyOn(api, 'releases').mockImplementation((id, options) => {
expect(id).toEqual(1);
expect(options.page).toEqual('1');
return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination });
@@ -56,7 +56,7 @@ describe('Releases State actions', () => {
});

it('dispatches requestReleases and receiveReleasesSuccess on page two', done => {
spyOn(api, 'releases').and.callFake((_, options) => {
jest.spyOn(api, 'releases').mockImplementation((_, options) => {
expect(options.page).toEqual('2');
return Promise.resolve({ data: releases, headers: pageInfoHeadersWithoutPagination });
});
@@ -82,7 +82,7 @@ describe('Releases State actions', () => {

describe('error', () => {
it('dispatches requestReleases and receiveReleasesError', done => {
spyOn(api, 'releases').and.returnValue(Promise.reject());
jest.spyOn(api, 'releases').mockReturnValue(Promise.reject());

testAction(
fetchReleases,

spec/javascripts/releases/stores/modules/list/helpers.js → spec/frontend/releases/stores/modules/list/helpers.js View File


spec/javascripts/releases/stores/modules/list/mutations_spec.js → spec/frontend/releases/stores/modules/list/mutations_spec.js View File


+ 19
- 1
spec/frontend/snippets/components/snippet_blob_view_spec.js View File

@@ -3,7 +3,11 @@ import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import BlobContent from '~/blob/components/blob_content.vue';
import { BLOB_RENDER_EVENT_LOAD, BLOB_RENDER_EVENT_SHOW_SOURCE } from '~/blob/components/constants';
import {
BLOB_RENDER_EVENT_LOAD,
BLOB_RENDER_EVENT_SHOW_SOURCE,
BLOB_RENDER_ERRORS,
} from '~/blob/components/constants';
import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
import {
SNIPPET_VISIBILITY_PRIVATE,
@@ -109,6 +113,20 @@ describe('Blob Embeddable', () => {
});
});

it('passes information about render error down to blob header', () => {
createComponent({
blob: {
...BlobMock,
simpleViewer: {
...SimpleViewerMock,
renderError: BLOB_RENDER_ERRORS.REASONS.COLLAPSED.id,
},
},
});

expect(wrapper.find(BlobHeader).props('hasRenderError')).toBe(true);
});

describe('URLS with hash', () => {
beforeEach(() => {
window.location.hash = '#LC2';


+ 1
- 3
spec/frontend/vue_mr_widget/components/mr_widget_rebase_spec.js View File

@@ -45,10 +45,8 @@ describe('Merge request widget rebase component', () => {

expect(text).toContain('Fast-forward merge is not possible.');
expect(text.replace(/\s\s+/g, ' ')).toContain(
'Rebase the source branch onto the target branch or merge target',
'Rebase the source branch onto the target branch.',
);

expect(text).toContain('branch into source branch to allow this merge request to be merged.');
});

it('it should render error message when it fails', done => {


+ 0
- 10
spec/graphql/resolvers/projects/jira_imports_resolver_spec.rb View File

@@ -40,16 +40,6 @@ describe Resolvers::Projects::JiraImportsResolver do
let_it_be(:jira_import1) { create(:jira_import_state, :finished, project: project, jira_project_key: 'AA', created_at: 2.days.ago) }
let_it_be(:jira_import2) { create(:jira_import_state, :finished, project: project, jira_project_key: 'BB', created_at: 5.days.ago) }

context 'when feature flag disabled' do
let(:current_user) { user }

before do
stub_feature_flags(jira_issue_import: false)
end

it_behaves_like 'no Jira import access'
end

context 'when user cannot read Jira imports' do
context 'when anonymous user' do
let(:current_user) { nil }


+ 30
- 0
spec/helpers/notes_helper_spec.rb View File

@@ -24,6 +24,36 @@ describe NotesHelper do
project.add_guest(guest)
end

describe '#note_target_title' do
context 'note does not exist' do
it 'returns nil' do
expect(helper.note_target_title(nil)).to be_blank
end
end

context 'target does not exist' do
it 'returns nil' do
note = Note.new
expect(helper.note_target_title(note)).to be_blank
end
end

context 'when given a design target' do
it 'returns nil' do
note = build_stubbed(:note_on_design)
expect(helper.note_target_title(note)).to be_blank
end
end

context 'when given a non-design target' do
it 'returns the issue title' do
issue = build_stubbed(:issue, title: 'Issue 1')
note = build_stubbed(:note, noteable: issue)
expect(helper.note_target_title(note)).to eq('Issue 1')
end
end
end

describe "#notes_max_access_for_users" do
it 'returns access levels' do
expect(helper.note_max_access_for_user(owner_note)).to eq(Gitlab::Access::OWNER)


+ 1
- 1
spec/helpers/todos_helper_spec.rb View File

@@ -89,7 +89,7 @@ describe TodosHelper do

context 'when given a non-design todo' do
let(:todo) do
create(:todo, :assigned,
build_stubbed(:todo, :assigned,
user: user,
project: issue.project,
target: issue,


+ 0
- 148
spec/javascripts/releases/mock_data.js View File

@@ -1,148 +0,0 @@
export const pageInfoHeadersWithoutPagination = {
'X-NEXT-PAGE': '',
'X-PAGE': '1',
'X-PER-PAGE': '20',
'X-PREV-PAGE': '',
'X-TOTAL': '19',
'X-TOTAL-PAGES': '1',
};

export const pageInfoHeadersWithPagination = {
'X-NEXT-PAGE': '2',
'X-PAGE': '1',
'X-PER-PAGE': '20',
'X-PREV-PAGE': '',
'X-TOTAL': '21',
'X-TOTAL-PAGES': '2',
};

export const release = {
name: 'Bionic Beaver',
tag_name: '18.04',
description: '## changelog\n\n* line 1\n* line2',
description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
author_name: 'Release bot',
author_email: 'release-bot@example.com',
created_at: '2012-05-28T05:00:00-07:00',
commit: {
id: '2695effb5807a22ff3d138d593fd856244e155e7',
short_id: '2695effb',
title: 'Initial commit',
created_at: '2017-07-26T11:08:53.000+02:00',
parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'],
message: 'Initial commit',
author: {
avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
id: 482476,
name: 'John Doe',
path: '/johndoe',
state: 'active',
status_tooltip_html: null,
username: 'johndoe',
web_url: 'https://gitlab.com/johndoe',
},
authored_date: '2012-05-28T04:42:42-07:00',
committer_name: 'Jack Smith',
committer_email: 'jack@example.com',
committed_date: '2012-05-28T04:42:42-07:00',
},
assets: {
count: 6,
sources: [
{
format: 'zip',
url: 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.zip',
},
{
format: 'tar.gz',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz',
},
{
format: 'tar.bz2',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2',
},
{
format: 'tar',
url: 'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar',
},
],
links: [
{
name: 'release-18.04.dmg',
url: 'https://my-external-hosting.example.com/scrambled-url/',
external: true,
},
{
name: 'binary-linux-amd64',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
external: false,
},
],
},
};

export const releases = [
release,
{
name: 'JoJos Bizarre Adventure',
tag_name: '19.00',
description: '## changelog\n\n* line 1\n* line2',
description_html: '<div><h2>changelog</h2><ul><li>line1</li<li>line 2</li></ul></div>',
author_name: 'Release bot',
author_email: 'release-bot@example.com',
created_at: '2012-05-28T05:00:00-07:00',
commit: {
id: '2695effb5807a22ff3d138d593fd856244e155e7',
short_id: '2695effb',
title: 'Initial commit',
created_at: '2017-07-26T11:08:53.000+02:00',
parent_ids: ['2a4b78934375d7f53875269ffd4f45fd83a84ebe'],
message: 'Initial commit',
author: {
avatar_url: 'uploads/-/system/user/avatar/johndoe/avatar.png',
id: 482476,
name: 'John Doe',
path: '/johndoe',
state: 'active',
status_tooltip_html: null,
username: 'johndoe',
web_url: 'https://gitlab.com/johndoe',
},
authored_date: '2012-05-28T04:42:42-07:00',
committer_name: 'Jack Smith',
committer_email: 'jack@example.com',
committed_date: '2012-05-28T04:42:42-07:00',
},
assets: {
count: 4,
sources: [
{
format: 'tar.gz',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.gz',
},
{
format: 'tar.bz2',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar.bz2',
},
{
format: 'tar',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/archive/v11.3.12/gitlab-ce-v11.3.12.tar',
},
],
links: [
{
name: 'binary-linux-amd64',
url:
'https://gitlab.com/gitlab-org/gitlab-foss/-/jobs/artifacts/v11.6.0-rc4/download?job=rspec-mysql+41%2F50',
external: false,
},
],
},
},
];

+ 0
- 14
spec/lib/gitlab/jira_import/base_importer_spec.rb View File

@@ -8,24 +8,10 @@ describe Gitlab::JiraImport::BaseImporter do
let(:project) { create(:project) }

describe 'with any inheriting class' do
context 'when an error is returned from the project validation' do
before do
stub_feature_flags(jira_issue_import: false)

allow(project).to receive(:validate_jira_import_settings!)
.and_raise(Projects::ImportService::Error, 'Jira import feature is disabled.')
end

it 'raises exception' do
expect { described_class.new(project) }.to raise_error(Projects::ImportService::Error, 'Jira import feature is disabled.')
end
end

context 'when project validation is ok' do
let!(:jira_service) { create(:jira_service, project: project) }

before do
stub_feature_flags(jira_issue_import: true)
stub_jira_service_test

allow(project).to receive(:validate_jira_import_settings!)


+ 0
- 1
spec/lib/gitlab/jira_import/issues_importer_spec.rb View File

@@ -14,7 +14,6 @@ describe Gitlab::JiraImport::IssuesImporter do
subject { described_class.new(project) }

before do
stub_feature_flags(jira_issue_import: true)
stub_jira_service_test
end



+ 0
- 1
spec/lib/gitlab/jira_import/labels_importer_spec.rb View File

@@ -15,7 +15,6 @@ describe Gitlab::JiraImport::LabelsImporter do
subject { importer.execute }

before do
stub_feature_flags(jira_issue_import: true)
stub_const('Gitlab::JiraImport::LabelsImporter::MAX_LABELS', 2)
end



+ 76
- 9
spec/models/iteration_spec.rb View File

@@ -50,7 +50,10 @@ describe Iteration do
end

context 'when dates overlap' do
context 'same group' do
let(:start_date) { 5.days.from_now }
let(:due_date) { 6.days.from_now }

shared_examples_for 'overlapping dates' do
context 'when start_date is in range' do
let(:start_date) { 5.days.from_now }
let(:due_date) { 3.weeks.from_now }
@@ -59,6 +62,11 @@ describe Iteration do
expect(subject).not_to be_valid
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
end

it 'is not valid even if forced' do
subject.validate # to generate iid/etc
expect { subject.save(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/)
end
end

context 'when end_date is in range' do
@@ -69,25 +77,84 @@ describe Iteration do
expect(subject).not_to be_valid
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
end

it 'is not valid even if forced' do
subject.validate # to generate iid/etc
expect { subject.save(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/)
end
end

context 'when both overlap' do
let(:start_date) { 5.days.from_now }
let(:due_date) { 6.days.from_now }

it 'is not valid' do
expect(subject).not_to be_valid
expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations')
end

it 'is not valid even if forced' do
subject.validate # to generate iid/etc
expect { subject.save(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/)
end
end
end

context 'different group' do
let(:start_date) { 5.days.from_now }
let(:due_date) { 6.days.from_now }
let(:group) { create(:group) }
context 'group' do
it_behaves_like 'overlapping dates' do
let(:constraint_name) { 'iteration_start_and_due_daterange_group_id_constraint' }
end

context 'different group' do
let(:group) { create(:group) }

it { is_expected.to be_valid }

it 'does not trigger exclusion constraints' do
expect { subject.save }.not_to raise_exception
end
end

context 'in a project' do
let(:project) { create(:project) }