diff options
35 files changed, 628 insertions, 90 deletions
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index f57febbda37..b8c8cc91f53 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -479,24 +479,46 @@ export default class LabelsSelect { // concatenation // see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays + const linkOpenTag = + '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>" class="gl-link gl-label-link has-tooltip" <%= linkAttrs %> title="<%= tooltipTitleTemplate({ label, isScopedLabel, enableScopedLabels, escapeStr }) %>">'; + const spanOpenTag = + '<span class="gl-label-text" style="background-color: <%= escapeStr(label.color) %>; color: <%= escapeStr(label.text_color) %>;">'; const labelTemplate = _.template( [ - '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">', - '<span class="badge label has-tooltip color-label" <%= linkAttrs %> title="<%= tooltipTitleTemplate({ label, isScopedLabel, enableScopedLabels, escapeStr }) %>" style="background-color: <%= escapeStr(label.color) %>; color: <%= escapeStr(label.text_color) %>;">', + '<span class="gl-label">', + linkOpenTag, + spanOpenTag, '<%- label.title %>', '</span>', '</a>', + '</span>', ].join(''), ); const infoIconTemplate = _.template( [ - '<a href="<%= scopedLabelsDocumentationLink %>" class="label scoped-label" target="_blank" rel="noopener">', - '<i class="fa fa-question-circle" style="background-color: <%= escapeStr(label.color) %>; color: <%= escapeStr(label.text_color) %>;"></i>', + '<a href="<%= scopedLabelsDocumentationLink %>" class="gl-link gl-label-icon" target="_blank" rel="noopener">', + '<i class="fa fa-question-circle"></i>', '</a>', ].join(''), ); + const scopedLabelTemplate = _.template( + [ + '<span class="gl-label gl-label-scoped" style="color: <%= escapeStr(label.color) %>;">', + linkOpenTag, + spanOpenTag, + '<%- label.title.slice(0, label.title.lastIndexOf("::")) %>', + '</span>', + '<span class="gl-label-text" style="color: <%= escapeStr(label.color) %>;">', + '<%- label.title.slice(label.title.lastIndexOf("::") + 2) %>', + '</span>', + '</a>', + '<%= infoIconTemplate({ label, scopedLabelsDocumentationLink, escapeStr }) %>', + '</span>', + ].join(''), + ); + const tooltipTitleTemplate = _.template( [ '<% if (isScopedLabel(label) && enableScopedLabels) { %>', @@ -514,8 +536,7 @@ export default class LabelsSelect { '<% _.each(labels, function(label){ %>', '<% if (isScopedLabel(label) && enableScopedLabels) { %>', '<span class="d-inline-block position-relative scoped-label-wrapper">', - '<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, escapeStr, linkAttrs: \'data-html="true"\' }) %>', - '<%= infoIconTemplate({ label, scopedLabelsDocumentationLink, escapeStr }) %>', + '<%= scopedLabelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, infoIconTemplate, scopedLabelsDocumentationLink, tooltipTitleTemplate, escapeStr, linkAttrs: \'data-html="true"\' }) %>', '</span>', '<% } else { %>', '<%= labelTemplate({ label, issueUpdateURL, isScopedLabel, enableScopedLabels, tooltipTitleTemplate, escapeStr, linkAttrs: "" }) %>', @@ -528,6 +549,7 @@ export default class LabelsSelect { ...tplData, labelTemplate, infoIconTemplate, + scopedLabelTemplate, tooltipTitleTemplate, isScopedLabel, escapeStr: _.escape, diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js index 7c2008d9edc..d191479b1b4 100644 --- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js +++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import _ from 'underscore'; +import { debounce } from 'lodash'; import axios from '~/lib/utils/axios_utils'; import flash from '~/flash'; import { __ } from '~/locale'; @@ -34,7 +34,7 @@ export default () => { $broadcastMessage.on( 'input', - _.debounce(function onMessageInput() { + debounce(function onMessageInput() { const message = $(this).val(); if (message === '') { $jsBroadcastMessagePreview.text(__('Your message here')); diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue index 527c16860c0..a99fde54981 100644 --- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue +++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; import { s__, sprintf } from '~/locale'; @@ -34,7 +34,7 @@ export default { return sprintf( s__('AdminProjects|Delete Project %{projectName}?'), { - projectName: `'${_.escape(this.projectName)}'`, + projectName: `'${esc(this.projectName)}'`, }, false, ); @@ -46,7 +46,7 @@ export default { and all related resources including issues, merge requests, etc.. Once you confirm and press %{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`), { - projectName: `<strong>${_.escape(this.projectName)}</strong>`, + projectName: `<strong>${esc(this.projectName)}</strong>`, strong_start: '<strong>', strong_end: '</strong>', }, @@ -57,7 +57,7 @@ export default { return sprintf( s__('AdminUsers|To confirm, type %{projectName}'), { - projectName: `<code>${_.escape(this.projectName)}</code>`, + projectName: `<code>${esc(this.projectName)}</code>`, }, false, ); diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue index b43d6ba17d7..831a3ca1658 100644 --- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue +++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import { GlModal, GlButton, GlFormInput } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; @@ -56,7 +56,7 @@ export default { return sprintf( this.content, { - username: `<strong>${_.escape(this.username)}</strong>`, + username: `<strong>${esc(this.username)}</strong>`, strong_start: '<strong>', strong_end: '</strong>', }, @@ -67,7 +67,7 @@ export default { return sprintf( s__('AdminUsers|To confirm, type %{username}'), { - username: `<code>${_.escape(this.username)}</code>`, + username: `<code>${esc(this.username)}</code>`, }, false, ); diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js index f1e7ff87e5a..eeaa6527431 100644 --- a/app/assets/javascripts/pages/groups/new/group_path_validator.js +++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import { debounce } from 'lodash'; import InputValidator from '~/validators/input_validator'; import fetchGroupPathAvailability from './fetch_group_path_availability'; @@ -20,7 +20,7 @@ export default class GroupPathValidator extends InputValidator { const container = opts.container || ''; const validateElements = document.querySelectorAll(`${container} .js-validate-group-path`); - this.debounceValidateInput = _.debounce(inputDomElement => { + this.debounceValidateInput = debounce(inputDomElement => { GroupPathValidator.validateGroupPathInput(inputDomElement); }, debounceTimeoutDuration); diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue index bb95f33c838..dadb20e511b 100644 --- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue +++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import axios from '~/lib/utils/axios_utils'; import createFlash from '~/flash'; import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue'; @@ -48,7 +48,7 @@ export default { const label = `<span class="label color-label" style="background-color: ${this.labelColor}; color: ${this.labelTextColor};" - >${_.escape(this.labelTitle)}</span>`; + >${esc(this.labelTitle)}</span>`; return sprintf( s__('Labels|<span>Promote label</span> %{labelTitle} <span>to Group Label?</span>'), diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue index bb490919a9a..3a0d9c17228 100644 --- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue +++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue @@ -1,6 +1,4 @@ <script> -import _ from 'underscore'; - export default { props: { initialCronInterval: { @@ -24,7 +22,7 @@ export default { }, computed: { intervalIsPreset() { - return _.contains(this.cronIntervalPresets, this.cronInterval); + return Object.values(this.cronIntervalPresets).includes(this.cronInterval); }, // The text input is editable when there's a custom interval, or when it's // a preset interval and the user clicks the 'custom' radio button diff --git a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue index 2176309ac84..6af346ace67 100644 --- a/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue +++ b/app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue @@ -1,5 +1,5 @@ <script> -import _ from 'underscore'; +import { escape as esc } from 'lodash'; import { GlModal, GlModalDirective } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; @@ -38,7 +38,7 @@ export default { return sprintf( s__('WikiPageConfirmDelete|Delete page %{pageTitle}?'), { - pageTitle: _.escape(this.pageTitle), + pageTitle: esc(this.pageTitle), }, false, ); diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js index 25be71d9ed4..1048e3b4548 100644 --- a/app/assets/javascripts/pages/sessions/new/username_validator.js +++ b/app/assets/javascripts/pages/sessions/new/username_validator.js @@ -1,4 +1,4 @@ -import _ from 'underscore'; +import { debounce } from 'lodash'; import InputValidator from '~/validators/input_validator'; import axios from '~/lib/utils/axios_utils'; @@ -20,7 +20,7 @@ export default class UsernameValidator extends InputValidator { const container = opts.container || ''; const validateLengthElements = document.querySelectorAll(`${container} .js-validate-username`); - this.debounceValidateInput = _.debounce(inputDomElement => { + this.debounceValidateInput = debounce(inputDomElement => { UsernameValidator.validateUsernameInput(inputDomElement); }, debounceTimeoutDuration); diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js index 4f645e511f9..70e9333456d 100644 --- a/app/assets/javascripts/pages/users/activity_calendar.js +++ b/app/assets/javascripts/pages/users/activity_calendar.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import _ from 'underscore'; +import { last } from 'lodash'; import { scaleLinear, scaleThreshold } from 'd3-scale'; import { select } from 'd3-selection'; import dateFormat from 'dateformat'; @@ -164,11 +164,11 @@ export default class ActivityCalendar { .enter() .append('g') .attr('transform', (group, i) => { - _.each(group, (stamp, a) => { + group.forEach((stamp, a) => { if (a === 0 && stamp.day === this.firstDayOfWeek) { const month = stamp.date.getMonth(); const x = this.daySizeWithSpace * i + 1 + this.daySizeWithSpace; - const lastMonth = _.last(this.months); + const lastMonth = last(this.months); if ( lastMonth == null || (month !== lastMonth.month && x - this.daySizeWithSpace !== lastMonth.x) diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 216f6c62e69..c9286e22f82 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -1,22 +1,11 @@ <script> import { GlLoadingIcon } from '@gitlab/ui'; /* eslint-disable vue/require-default-prop */ -/* This is a re-usable vue component for rendering a button - that will probably be sending off ajax requests and need - to show the loading status by setting the `loading` option. - This can also be used for initial page load when you don't - know the action of the button yet by setting - `loading: true, label: undefined`. - - Sample configuration: - - <loading-button - :loading="true" - :label="Hello" - @click="..." - /> - - */ +/* +This component will be deprecated in favor of gl-button. +https://gitlab-org.gitlab.io/gitlab-ui/?path=/story/base-button--loading-button +https://gitlab.com/gitlab-org/gitlab/issues/207412 +*/ export default { components: { diff --git a/changelogs/unreleased/196718-remove-filter-epic-counts.yml b/changelogs/unreleased/196718-remove-filter-epic-counts.yml new file mode 100644 index 00000000000..a3616ab490e --- /dev/null +++ b/changelogs/unreleased/196718-remove-filter-epic-counts.yml @@ -0,0 +1,5 @@ +--- +title: Remove visibility check from epic descendant counts +merge_request: 25975 +author: +type: changed diff --git a/changelogs/unreleased/198323-migrate-snippet-mentions-to-db-table.yml b/changelogs/unreleased/198323-migrate-snippet-mentions-to-db-table.yml new file mode 100644 index 00000000000..7501e713c6c --- /dev/null +++ b/changelogs/unreleased/198323-migrate-snippet-mentions-to-db-table.yml @@ -0,0 +1,6 @@ +--- +title: Migrate mentions for snippet and snippet notes to snippet_user_mentions DB + table +merge_request: 23783 +author: +type: changed diff --git a/changelogs/unreleased/38145-replace-labels-in-non-vue-js-with-gitlab-ui-component.yml b/changelogs/unreleased/38145-replace-labels-in-non-vue-js-with-gitlab-ui-component.yml new file mode 100644 index 00000000000..04302d3653f --- /dev/null +++ b/changelogs/unreleased/38145-replace-labels-in-non-vue-js-with-gitlab-ui-component.yml @@ -0,0 +1,5 @@ +--- +title: Correctly style scoped labels in sidebar after updating +merge_request: 22071 +author: +type: changed diff --git a/changelogs/unreleased/jlouw-improve-audit-log-header-layout.yml b/changelogs/unreleased/jlouw-improve-audit-log-header-layout.yml new file mode 100644 index 00000000000..8a79e699a28 --- /dev/null +++ b/changelogs/unreleased/jlouw-improve-audit-log-header-layout.yml @@ -0,0 +1,5 @@ +--- +title: Improve audit log header layout +merge_request: 25821 +author: +type: changed diff --git a/changelogs/unreleased/migrate-security-scans.yml b/changelogs/unreleased/migrate-security-scans.yml new file mode 100644 index 00000000000..3806aef93a1 --- /dev/null +++ b/changelogs/unreleased/migrate-security-scans.yml @@ -0,0 +1,5 @@ +--- +title: Schedule worker to migrate security job artifacts to security scans +merge_request: 24125 +author: +type: other diff --git a/db/post_migrate/20200127111953_cleanup_empty_snippet_user_mentions.rb b/db/post_migrate/20200127111953_cleanup_empty_snippet_user_mentions.rb new file mode 100644 index 00000000000..aad688fef3f --- /dev/null +++ b/db/post_migrate/20200127111953_cleanup_empty_snippet_user_mentions.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +class CleanupEmptySnippetUserMentions < ActiveRecord::Migration[5.2] + DOWNTIME = false + BATCH_SIZE = 10_000 + + class SnippetUserMention < ActiveRecord::Base + include EachBatch + + self.table_name = 'snippet_user_mentions' + end + + def up + # cleanup snippet user mentions with no actual mentions, + # re https://gitlab.com/gitlab-org/gitlab/-/merge_requests/24586#note_285982468 + SnippetUserMention + .where(mentioned_users_ids: nil) + .where(mentioned_groups_ids: nil) + .where(mentioned_projects_ids: nil) + .each_batch(of: BATCH_SIZE) do |batch| + batch.delete_all + end + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20200127131953_migrate_snippet_mentions_to_db.rb b/db/post_migrate/20200127131953_migrate_snippet_mentions_to_db.rb new file mode 100644 index 00000000000..e25c2c2982a --- /dev/null +++ b/db/post_migrate/20200127131953_migrate_snippet_mentions_to_db.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class MigrateSnippetMentionsToDb < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + DELAY = 2.minutes.to_i + BATCH_SIZE = 10_000 + MIGRATION = 'UserMentions::CreateResourceUserMention' + + JOIN = "LEFT JOIN snippet_user_mentions on snippets.id = snippet_user_mentions.snippet_id" + QUERY_CONDITIONS = "(description like '%@%' OR title like '%@%') AND snippet_user_mentions.snippet_id IS NULL" + + disable_ddl_transaction! + + class Snippet < ActiveRecord::Base + include EachBatch + + self.table_name = 'snippets' + end + + def up + Snippet + .joins(JOIN) + .where(QUERY_CONDITIONS) + .each_batch(of: BATCH_SIZE) do |batch, index| + range = batch.pluck(Arel.sql('MIN(snippets.id)'), Arel.sql('MAX(snippets.id)')).first + migrate_in(index * DELAY, MIGRATION, ['Snippet', JOIN, QUERY_CONDITIONS, false, *range]) + end + end + + def down + # no-op + end +end diff --git a/db/post_migrate/20200127141953_add_temporary_snippet_notes_with_mentions_index.rb b/db/post_migrate/20200127141953_add_temporary_snippet_notes_with_mentions_index.rb new file mode 100644 index 00000000000..ec9b8b76f6f --- /dev/null +++ b/db/post_migrate/20200127141953_add_temporary_snippet_notes_with_mentions_index.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddTemporarySnippetNotesWithMentionsIndex < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'snippet_mentions_temp_index' + INDEX_CONDITION = "note LIKE '%@%'::text AND notes.noteable_type = 'Snippet'" + + disable_ddl_transaction! + + def up + # create temporary index for notes with mentions, may take well over 1h + add_concurrent_index(:notes, :id, where: INDEX_CONDITION, name: INDEX_NAME) + end + + def down + remove_concurrent_index(:notes, :id, where: INDEX_CONDITION, name: INDEX_NAME) + end +end diff --git a/db/post_migrate/20200127151953_migrate_snippet_notes_mentions_to_db.rb b/db/post_migrate/20200127151953_migrate_snippet_notes_mentions_to_db.rb new file mode 100644 index 00000000000..3795a96b426 --- /dev/null +++ b/db/post_migrate/20200127151953_migrate_snippet_notes_mentions_to_db.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +class MigrateSnippetNotesMentionsToDb < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + DELAY = 2.minutes.to_i + BATCH_SIZE = 10_000 + MIGRATION = 'UserMentions::CreateResourceUserMention' + + INDEX_CONDITION = "note LIKE '%@%'::text AND notes.noteable_type = 'Snippet'" + QUERY_CONDITIONS = "#{INDEX_CONDITION} AND snippet_user_mentions.snippet_id IS NULL" + JOIN = 'INNER JOIN snippets ON snippets.id = notes.noteable_id LEFT JOIN snippet_user_mentions ON notes.id = snippet_user_mentions.note_id' + + disable_ddl_transaction! + + class Note < ActiveRecord::Base + include EachBatch + + self.table_name = 'notes' + end + + def up + Note + .joins(JOIN) + .where(QUERY_CONDITIONS) + .each_batch(of: BATCH_SIZE) do |batch, index| + range = batch.pluck(Arel.sql('MIN(notes.id)'), Arel.sql('MAX(notes.id)')).first + migrate_in(index * DELAY, MIGRATION, ['Snippet', JOIN, QUERY_CONDITIONS, true, *range]) + end + end + + def down + # no-op + # temporary index is to be dropped in a different migration in an upcoming release: + # https://gitlab.com/gitlab-org/gitlab/issues/196842 + end +end diff --git a/db/post_migrate/20200217223651_add_index_to_job_artifact_secure_reports.rb b/db/post_migrate/20200217223651_add_index_to_job_artifact_secure_reports.rb new file mode 100644 index 00000000000..ca297272f8e --- /dev/null +++ b/db/post_migrate/20200217223651_add_index_to_job_artifact_secure_reports.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class AddIndexToJobArtifactSecureReports < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'job_artifacts_secure_reports_temp_index' + PARTIAL_FILTER = "file_type BETWEEN 5 AND 8" + + disable_ddl_transaction! + + def up + # This is a temporary index used for the migration of Security Reports to Security Scans + add_concurrent_index(:ci_job_artifacts, + [:id, :file_type, :job_id, :created_at, :updated_at], + name: INDEX_NAME, + where: PARTIAL_FILTER) + end + + def down + remove_concurrent_index(:ci_job_artifacts, + [:id, :file_type, :job_id, :created_at, :updated_at]) + end +end diff --git a/db/post_migrate/20200217225719_schedule_migrate_security_scans.rb b/db/post_migrate/20200217225719_schedule_migrate_security_scans.rb new file mode 100644 index 00000000000..7ef204ed9de --- /dev/null +++ b/db/post_migrate/20200217225719_schedule_migrate_security_scans.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class ScheduleMigrateSecurityScans < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INTERVAL = 2.minutes.to_i + BATCH_SIZE = 10_000 + MIGRATION = 'MigrateSecurityScans'.freeze + + disable_ddl_transaction! + + class JobArtifact < ActiveRecord::Base + include ::EachBatch + + self.table_name = 'ci_job_artifacts' + + scope :security_reports, -> { where('file_type BETWEEN 5 and 8') } + end + + def up + queue_background_migration_jobs_by_range_at_intervals(JobArtifact.security_reports, + MIGRATION, + INTERVAL, + batch_size: BATCH_SIZE) + end + + def down + # intentionally blank + end +end diff --git a/db/schema.rb b/db/schema.rb index 623fdcd0542..c6a5e2dd869 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -766,6 +766,7 @@ ActiveRecord::Schema.define(version: 2020_02_26_162723) do t.integer "file_location", limit: 2 t.index ["expire_at", "job_id"], name: "index_ci_job_artifacts_on_expire_at_and_job_id" t.index ["file_store"], name: "index_ci_job_artifacts_on_file_store" + t.index ["id", "file_type", "job_id", "created_at", "updated_at"], name: "job_artifacts_secure_reports_temp_index", where: "((file_type >= 5) AND (file_type <= 8))" t.index ["job_id", "file_type"], name: "index_ci_job_artifacts_on_job_id_and_file_type", unique: true t.index ["project_id"], name: "index_ci_job_artifacts_on_project_id" t.index ["project_id"], name: "index_ci_job_artifacts_on_project_id_for_security_reports", where: "(file_type = ANY (ARRAY[5, 6, 7, 8]))" @@ -2824,6 +2825,7 @@ ActiveRecord::Schema.define(version: 2020_02_26_162723) do t.index ["discussion_id"], name: "index_notes_on_discussion_id" t.index ["id"], name: "design_mentions_temp_index", where: "((note ~~ '%@%'::text) AND ((noteable_type)::text = 'DesignManagement::Design'::text))" t.index ["id"], name: "epic_mentions_temp_index", where: "((note ~~ '%@%'::text) AND ((noteable_type)::text = 'Epic'::text))" + t.index ["id"], name: "snippet_mentions_temp_index", where: "((note ~~ '%@%'::text) AND ((noteable_type)::text = 'Snippet'::text))" t.index ["line_code"], name: "index_notes_on_line_code" t.index ["note"], name: "index_notes_on_note_trigram", opclass: :gin_trgm_ops, using: :gin t.index ["note"], name: "tmp_idx_on_promoted_notes", where: "(((noteable_type)::text = 'Issue'::text) AND (system IS TRUE) AND (note ~~ 'promoted to epic%'::text))" diff --git a/doc/user/clusters/applications.md b/doc/user/clusters/applications.md index 33639c13b9d..32eca7029da 100644 --- a/doc/user/clusters/applications.md +++ b/doc/user/clusters/applications.md @@ -26,8 +26,9 @@ This namespace: To see a list of available applications to install. For a: - [Project-level cluster](../project/clusters/index.md), navigate to your project's - **Operations > Kubernetes**. -- [Group-level cluster](../group/clusters/index.md), navigate to your group's **Kubernetes** page. + **{cloud-gear}** **Operations > Kubernetes**. +- [Group-level cluster](../group/clusters/index.md), navigate to your group's + **{cloud-gear}** **Kubernetes** page. Install Helm first as it's used to install other applications. @@ -655,7 +656,7 @@ GitLab Runner is installed into the `gitlab-managed-apps` namespace of your clus In order for GitLab Runner to function, you **must** specify the following: -- `gitlabUrl` - the GitLab server full URL (e.g., `https://example.gitlab.com`) to register the Runner against. +- `gitlabUrl` - the GitLab server full URL (for example, `https://example.gitlab.com`) to register the Runner against. - `runnerRegistrationToken` - The registration token for adding new Runners to GitLab. This must be [retrieved from your GitLab instance](../../ci/runners/README.md). @@ -752,7 +753,8 @@ agent: > [Introduced](https://gitlab.com/gitlab-org/cluster-integration/cluster-applications/-/merge_requests/40) in GitLab 12.8. -Enable JupyterHub in the `.gitlab/managed-apps/config.yaml` file to install it: +JupyterHub is installed using GitLab CI by defining configuration in +`.gitlab/managed-apps/config.yaml` as follows: ```yaml jupyterhub: @@ -761,33 +763,40 @@ jupyterhub: gitlabGroupWhitelist: [] ``` -`gitlabProjectIdWhitelist` restricts GitLab authentication to only members of the specified projects. `gitlabGroupWhitelist` restricts GitLab authentication to only members of the specified groups. Specifying an empty array for both will allow any user on the GitLab instance to log in. +In the configuration: -JupyterHub is installed into the `gitlab-managed-apps` namespace of your -cluster. +- `gitlabProjectIdWhitelist` restricts GitLab authentication to only members of the specified projects. +- `gitlabGroupWhitelist` restricts GitLab authentication to only members of the specified groups. +- Specifying an empty array for both will allow any user on the GitLab instance to sign in. -In order for JupyterHub to function, you must setup an [OAuth Application](../../integration/oauth_provider.md). Using the following values: +JupyterHub is installed into the `gitlab-managed-apps` namespace of your cluster. -- "Redirect URI" to `http://<JupyterHub Host>/hub/oauth_callback` -- "Scope" to `api read_repository write_repository` +For JupyterHub to function, you must set up an [OAuth Application](../../integration/oauth_provider.md). +Set: -In addition the following variables must be specified using [CI variables](../../ci/variables/README.md): +- "Redirect URI" to `http://<JupyterHub Host>/hub/oauth_callback`. +- "Scope" to `api read_repository write_repository`. -- `JUPYTERHUB_PROXY_SECRET_TOKEN` will set [`proxy.secretToken`](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference.html#proxy-secrettoken). Generate this using `openssl rand -hex 32`. -- `JUPYTERHUB_COOKIE_SECRET` will set [`hub.cookieSecret`](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference.html#hub-cookiesecret). Generate this using `openssl rand -hex 32`. -- `JUPYTERHUB_HOST` is the hostname used for the installation (e.g., `jupyter.example.gitlab.com`). -- `JUPYTERHUB_GITLAB_HOST` is the hostname of the GitLab instance used for authentication (e.g., `example.gitlab.com`). -- `JUPYTERHUB_AUTH_CRYPTO_KEY` will set [`auth.state.cryptoKey`](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference.html#auth-state-cryptokey). Generate this using `openssl rand -hex 32`. -- `JUPYTERHUB_AUTH_GITLAB_CLIENT_ID` the "Application ID" for the OAuth Application. -- `JUPYTERHUB_AUTH_GITLAB_CLIENT_SECRET` the "Secret" for the OAuth Application. +In addition, the following variables must be specified using [CI variables](../../ci/variables/README.md): -By default JupyterHub will be installed using a +| CI Variable | Description | +|:---------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `JUPYTERHUB_PROXY_SECRET_TOKEN` | Sets [`proxy.secretToken`](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference.html#proxy-secrettoken). Generate using `openssl rand -hex 32`. | +| `JUPYTERHUB_COOKIE_SECRET` | Sets [`hub.cookieSecret`](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference.html#hub-cookiesecret). Generate using `openssl rand -hex 32`. | +| `JUPYTERHUB_HOST` | Hostname used for the installation. For example, `jupyter.gitlab.example.com`. | +| `JUPYTERHUB_GITLAB_HOST` | Hostname of the GitLab instance used for authentication. For example, `gitlab.example.com`. | +| `JUPYTERHUB_AUTH_CRYPTO_KEY` | Sets [`auth.state.cryptoKey`](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference.html#auth-state-cryptokey). Generate using `openssl rand -hex 32`. | +| `JUPYTERHUB_AUTH_GITLAB_CLIENT_ID` | "Application ID" for the OAuth Application. | +| `JUPYTERHUB_AUTH_GITLAB_CLIENT_SECRET` | "Secret" for the OAuth Application. | + +By default, JupyterHub will be installed using a [default values file](https://gitlab.com/gitlab-org/cluster-integration/cluster-applications/-/blob/master/src/default-data/jupyterhub/values.yaml.gotmpl). -You can customize the installation of JupyterHub by defining -`.gitlab/managed-apps/jupyterhub/values.yaml` file in your cluster management -project. Refer to the -[chart reference](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference.html) -for the available configuration options. +You can customize the installation of JupyterHub by defining a +`.gitlab/managed-apps/jupyterhub/values.yaml` file in your cluster management project. + +Refer to the +[chart reference](https://zero-to-jupyterhub.readthedocs.io/en/stable/reference.html) for the +available configuration options. ### Install Elastic Stack using GitLab CI diff --git a/lib/gitlab/background_migration/migrate_security_scans.rb b/lib/gitlab/background_migration/migrate_security_scans.rb new file mode 100644 index 00000000000..189a150cb87 --- /dev/null +++ b/lib/gitlab/background_migration/migrate_security_scans.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Gitlab + module BackgroundMigration + # rubocop: disable Style/Documentation + class MigrateSecurityScans + def perform(start_id, stop_id) + end + end + end +end + +Gitlab::BackgroundMigration::MigrateSecurityScans.prepend_if_ee('EE::Gitlab::BackgroundMigration::MigrateSecurityScans') diff --git a/lib/gitlab/background_migration/user_mentions/models/snippet.rb b/lib/gitlab/background_migration/user_mentions/models/snippet.rb new file mode 100644 index 00000000000..1481cfcc562 --- /dev/null +++ b/lib/gitlab/background_migration/user_mentions/models/snippet.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + module UserMentions + module Models + class Snippet < ActiveRecord::Base + include IsolatedMentionable + include CacheMarkdownField + include MentionableMigrationMethods + + attr_mentionable :title, pipeline: :single_line + attr_mentionable :description + cache_markdown_field :title, pipeline: :single_line + cache_markdown_field :description + + self.table_name = 'snippets' + self.inheritance_column = :_type_disabled + + belongs_to :author, class_name: "User" + belongs_to :project + + def self.user_mention_model + Gitlab::BackgroundMigration::UserMentions::Models::SnippetUserMention + end + + def user_mention_model + self.class.user_mention_model + end + + def user_mention_resource_id + id + end + + def user_mention_note_id + 'NULL' + end + end + end + end + end +end diff --git a/lib/gitlab/background_migration/user_mentions/models/snippet_user_mention.rb b/lib/gitlab/background_migration/user_mentions/models/snippet_user_mention.rb new file mode 100644 index 00000000000..a856a53626e --- /dev/null +++ b/lib/gitlab/background_migration/user_mentions/models/snippet_user_mention.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true +# rubocop:disable Style/Documentation + +module Gitlab + module BackgroundMigration + module UserMentions + module Models + class SnippetUserMention < ActiveRecord::Base + self.table_name = 'snippet_user_mentions' + + def self.resource_foreign_key + :snippet_id + end + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6d20a0e9ba8..7028e44eac2 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -5724,12 +5724,6 @@ msgstr "" msgid "Created a branch and a merge request to resolve this issue." msgstr "" -msgid "Created after" -msgstr "" - -msgid "Created before" -msgstr "" - msgid "Created branch '%{branch_name}' and a merge request to resolve this issue." msgstr "" diff --git a/spec/frontend/labels_select_spec.js b/spec/frontend/labels_select_spec.js index d54e0eab845..59116b64298 100644 --- a/spec/frontend/labels_select_spec.js +++ b/spec/frontend/labels_select_spec.js @@ -41,29 +41,29 @@ describe('LabelsSelect', () => { }); it('generated label item template has correct label URL', () => { - expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label'); + expect($labelEl.find('a').attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label'); }); it('generated label item template has correct label title', () => { - expect($labelEl.find('span.label').text()).toBe(label.title); + expect($labelEl.find('span.gl-label-text').text()).toBe(label.title); }); it('generated label item template has label description as title attribute', () => { - expect($labelEl.find('span.label').attr('title')).toBe(label.description); + expect($labelEl.find('a').attr('title')).toBe(label.description); }); it('generated label item template has correct label styles', () => { - expect($labelEl.find('span.label').attr('style')).toBe( + expect($labelEl.find('span.gl-label-text').attr('style')).toBe( `background-color: ${label.color}; color: ${label.text_color};`, ); }); - it('generated label item has a badge class', () => { - expect($labelEl.find('span').hasClass('badge')).toEqual(true); + it('generated label item has a gl-label-text class', () => { + expect($labelEl.find('span').hasClass('gl-label-text')).toEqual(true); }); - it('generated label item template does not have scoped-label class', () => { - expect($labelEl.find('.scoped-label')).toHaveLength(0); + it('generated label item template does not have gl-label-icon class', () => { + expect($labelEl.find('.gl-label-icon')).toHaveLength(0); }); }); @@ -87,29 +87,31 @@ describe('LabelsSelect', () => { }); it('generated label item template has correct label title', () => { - expect($labelEl.find('span.label').text()).toBe(label.title); + const scopedTitle = label.title.split('::'); + expect($labelEl.find('span.gl-label-text').text()).toContain(scopedTitle[0]); + expect($labelEl.find('span.gl-label-text').text()).toContain(scopedTitle[1]); }); it('generated label item template has html flag as true', () => { - expect($labelEl.find('span.label').attr('data-html')).toBe('true'); + expect($labelEl.find('a').attr('data-html')).toBe('true'); }); it('generated label item template has question icon', () => { expect($labelEl.find('i.fa-question-circle')).toHaveLength(1); }); - it('generated label item template has scoped-label class', () => { - expect($labelEl.find('.scoped-label')).toHaveLength(1); + it('generated label item template has gl-label-icon class', () => { + expect($labelEl.find('.gl-label-icon')).toHaveLength(1); }); it('generated label item template has correct label styles', () => { - expect($labelEl.find('span.label').attr('style')).toBe( + expect($labelEl.find('span.gl-label-text').attr('style')).toBe( `background-color: ${label.color}; color: ${label.text_color};`, ); }); it('generated label item has a badge class', () => { - expect($labelEl.find('span').hasClass('badge')).toEqual(true); + expect($labelEl.find('span').hasClass('gl-label-text')).toEqual(true); }); }); }); diff --git a/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb b/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb new file mode 100644 index 00000000000..9c085b3cef8 --- /dev/null +++ b/spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' +require './db/post_migrate/20200127131953_migrate_snippet_mentions_to_db' +require './db/post_migrate/20200127151953_migrate_snippet_notes_mentions_to_db' + +describe Gitlab::BackgroundMigration::UserMentions::CreateResourceUserMention, schema: 20200127151953 do + include MigrationsHelpers + + context 'when migrating data' do + let(:users) { table(:users) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:notes) { table(:notes) } + + let(:author) { users.create!(email: 'author@example.com', notification_email: 'author@example.com', name: 'author', username: 'author', projects_limit: 10, state: 'active') } + let(:member) { users.create!(email: 'member@example.com', notification_email: 'member@example.com', name: 'member', username: 'member', projects_limit: 10, state: 'active') } + let(:admin) { users.create!(email: 'administrator@example.com', notification_email: 'administrator@example.com', name: 'administrator', username: 'administrator', admin: 1, projects_limit: 10, state: 'active') } + let(:john_doe) { users.create!(email: 'john_doe@example.com', notification_email: 'john_doe@example.com', name: 'john_doe', username: 'john_doe', projects_limit: 10, state: 'active') } + let(:skipped) { users.create!(email: 'skipped@example.com', notification_email: 'skipped@example.com', name: 'skipped', username: 'skipped', projects_limit: 10, state: 'active') } + + let(:mentioned_users) { [author, member, admin, john_doe, skipped] } + let(:mentioned_users_refs) { mentioned_users.map { |u| "@#{u.username}" }.join(' ') } + + let(:group) { namespaces.create!(name: 'test1', path: 'test1', runners_token: 'my-token1', project_creation_level: 1, visibility_level: 20, type: 'Group') } + let(:inaccessible_group) { namespaces.create!(name: 'test2', path: 'test2', runners_token: 'my-token2', project_creation_level: 1, visibility_level: 0, type: 'Group') } + let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) } + + let(:mentioned_groups) { [group, inaccessible_group] } + let(:group_mentions) { [group, inaccessible_group].map { |gr| "@#{gr.path}" }.join(' ') } + let(:description_mentions) { "description with mentions #{mentioned_users_refs} and #{group_mentions}" } + + before do + # build personal namespaces and routes for users + mentioned_users.each { |u| u.becomes(User).save! } + + # build namespaces and routes for groups + mentioned_groups.each do |gr| + gr.name += '-org' + gr.path += '-org' + gr.becomes(Namespace).save! + end + end + + context 'migrate snippet mentions' do + let(:snippets) { table(:snippets) } + let(:snippet_user_mentions) { table(:snippet_user_mentions) } + + let!(:snippet1) { snippets.create!(project_id: project.id, author_id: author.id, title: 'title1', description: description_mentions) } + let!(:snippet2) { snippets.create!(project_id: project.id, author_id: author.id, title: 'title2', description: 'some description') } + let!(:snippet3) { snippets.create!(project_id: project.id, author_id: author.id, title: 'title3', description: 'description with an email@example.com and some other @ char here.') } + + let(:user_mentions) { snippet_user_mentions } + let(:resource) { snippet1 } + + it_behaves_like 'resource mentions migration', MigrateSnippetMentionsToDb, Snippet + + context 'mentions in note' do + let!(:note1) { notes.create!(noteable_id: snippet1.id, noteable_type: 'Snippet', project_id: project.id, author_id: author.id, note: description_mentions) } + let!(:note2) { notes.create!(noteable_id: snippet1.id, noteable_type: 'Snippet', project_id: project.id, author_id: author.id, note: 'sample note') } + let!(:note3) { notes.create!(noteable_id: snippet1.id, noteable_type: 'Snippet', project_id: project.id, author_id: author.id, note: description_mentions, system: true) } + # this not does not have actual mentions + let!(:note4) { notes.create!(noteable_id: snippet1.id, noteable_type: 'Snippet', project_id: project.id, author_id: author.id, note: 'note3 for an email@somesite.com and some other rando @ ref' ) } + # this note points to an innexistent noteable record in snippets table + let!(:note5) { notes.create!(noteable_id: snippets.maximum(:id) + 10, noteable_type: 'Snippet', project_id: project.id, author_id: author.id, note: description_mentions) } + + it_behaves_like 'resource notes mentions migration', MigrateSnippetNotesMentionsToDb, Snippet + end + end + end + + context 'checks no_quote_columns' do + it 'has correct no_quote_columns' do + expect(Gitlab::BackgroundMigration::UserMentions::Models::Snippet.no_quote_columns).to match([:note_id, :snippet_id]) + end + end +end diff --git a/spec/migrations/cleanup_empty_snippet_user_mentions_spec.rb b/spec/migrations/cleanup_empty_snippet_user_mentions_spec.rb new file mode 100644 index 00000000000..d229f0b2b59 --- /dev/null +++ b/spec/migrations/cleanup_empty_snippet_user_mentions_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20200127111953_cleanup_empty_snippet_user_mentions') + +describe CleanupEmptySnippetUserMentions, :migration, :sidekiq do + let(:users) { table(:users) } + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } + let(:snippets) { table(:snippets) } + let(:snippet_user_mentions) { table(:snippet_user_mentions) } + let(:notes) { table(:notes) } + + let(:user) { users.create!(name: 'root', email: 'root@example.com', username: 'root', projects_limit: 0) } + let(:group) { namespaces.create!(name: 'group1', path: 'group1', owner_id: user.id) } + let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) } + let(:snippet) { snippets.create!(title: "title1", title_html: 'title1', description: 'snippet description with @root mention', project_id: project.id, author_id: user.id) } + + let!(:resource1) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet') } + let!(:resource2) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet', system: true) } + let!(:resource3) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet') } + + # non-migrateable resources + # this note is already migrated, as it has a record in the snippet_user_mentions table + let!(:resource4) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet') } + let!(:user_mention) { snippet_user_mentions.create!(snippet_id: snippet.id, note_id: resource4.id, mentioned_users_ids: [1]) } + # this note points to an innexistent noteable record + let!(:resource5) { notes.create!(note: 'note for @root to check', noteable_id: snippets.maximum(:id) + 10, noteable_type: 'Snippet') } + + # these should get cleanup, by the migration + let!(:blank_snippet_user_mention1) { snippet_user_mentions.create!(snippet_id: snippet.id, note_id: resource1.id)} + let!(:blank_snippet_user_mention2) { snippet_user_mentions.create!(snippet_id: snippet.id, note_id: resource2.id)} + let!(:blank_snippet_user_mention3) { snippet_user_mentions.create!(snippet_id: snippet.id, note_id: resource3.id)} + + it 'cleanups blank user mentions' do + expect(snippet_user_mentions.count).to eq 4 + + migrate! + + expect(snippet_user_mentions.count).to eq 1 + end +end diff --git a/spec/migrations/migrate_snippet_mentions_to_db_spec.rb b/spec/migrations/migrate_snippet_mentions_to_db_spec.rb new file mode 100644 index 00000000000..6644329fc11 --- /dev/null +++ b/spec/migrations/migrate_snippet_mentions_to_db_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20200127131953_migrate_snippet_mentions_to_db') + +describe MigrateSnippetMentionsToDb, :migration, :sidekiq do + let(:users) { table(:users) } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:snippets) { table(:snippets) } + let(:snippet_user_mentions) { table(:snippet_user_mentions) } + + let(:user) { users.create!(name: 'root', email: 'root@example.com', username: 'root', projects_limit: 0) } + let(:group) { namespaces.create!(name: 'group1', path: 'group1', owner_id: user.id) } + let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) } + let!(:resource1) { snippets.create!(title: "title1", title_html: 'title1', description: 'snippet description with @root mention', project_id: project.id, author_id: user.id) } + let!(:resource2) { snippets.create!(title: "title2", title_html: "title2", description: 'snippet description with @group mention', project_id: project.id, author_id: user.id) } + let!(:resource3) { snippets.create!(title: "title3", title_html: "title3", description: 'snippet description with @project mention', project_id: project.id, author_id: user.id) } + + # non-migrateable resources + # this snippet is already migrated, as it has a record in the snippet_user_mentions table + let!(:resource4) { snippets.create!(title: "title4", title_html: "title4", description: 'snippet description with @project mention', project_id: project.id, author_id: user.id) } + let!(:user_mention) { snippet_user_mentions.create!(snippet_id: resource4.id, mentioned_users_ids: [1]) } + # this snippet has no mentions so should be filtered out + let!(:resource5) { snippets.create!(title: "title5", title_html: "title5", description: 'snippet description with no mention', project_id: project.id, author_id: user.id) } + + it_behaves_like 'schedules resource mentions migration', Snippet, false +end diff --git a/spec/migrations/migrate_snippet_notes_mentions_to_db_spec.rb b/spec/migrations/migrate_snippet_notes_mentions_to_db_spec.rb new file mode 100644 index 00000000000..2ebe80e6ae3 --- /dev/null +++ b/spec/migrations/migrate_snippet_notes_mentions_to_db_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20200127151953_migrate_snippet_notes_mentions_to_db') + +describe MigrateSnippetNotesMentionsToDb, :migration, :sidekiq do + let(:users) { table(:users) } + let(:projects) { table(:projects) } + let(:namespaces) { table(:namespaces) } + let(:snippets) { table(:snippets) } + let(:snippet_user_mentions) { table(:snippet_user_mentions) } + let(:notes) { table(:notes) } + + let(:user) { users.create!(name: 'root', email: 'root@example.com', username: 'root', projects_limit: 0) } + let(:group) { namespaces.create!(name: 'group1', path: 'group1', owner_id: user.id) } + let(:project) { projects.create!(name: 'gitlab1', path: 'gitlab1', namespace_id: group.id, visibility_level: 0) } + let(:snippet) { snippets.create!(title: "title1", title_html: 'title1', description: 'snippet description with @root mention', project_id: project.id, author_id: user.id) } + + let!(:resource1) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet') } + let!(:resource2) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet', system: true) } + let!(:resource3) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet') } + + # non-migrateable resources + # this note is already migrated, as it has a record in the snippet_user_mentions table + let!(:resource4) { notes.create!(note: 'note for @root to check', noteable_id: snippet.id, noteable_type: 'Snippet') } + let!(:user_mention) { snippet_user_mentions.create!(snippet_id: snippet.id, note_id: resource4.id, mentioned_users_ids: [1]) } + # this note points to an innexistent noteable record + let!(:resource5) { notes.create!(note: 'note for @root to check', noteable_id: snippets.maximum(:id) + 10, noteable_type: 'Snippet') } + + it_behaves_like 'schedules resource mentions migration', Snippet, true +end diff --git a/spec/migrations/schedule_migrate_security_scans_spec.rb b/spec/migrations/schedule_migrate_security_scans_spec.rb new file mode 100644 index 00000000000..ae066e8f91c --- /dev/null +++ b/spec/migrations/schedule_migrate_security_scans_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20200217225719_schedule_migrate_security_scans.rb') + +# rubocop: disable RSpec/FactoriesInMigrationSpecs +describe ScheduleMigrateSecurityScans, :migration, :sidekiq do + let(:migration) { described_class.new } + let(:namespaces) { table(:namespaces) } + let(:projects) { table(:projects) } + let(:builds) { table(:ci_builds) } + let(:job_artifacts) { table(:ci_job_artifacts) } + + let(:namespace) { namespaces.create!(name: "foo", path: "bar") } + let(:project) { projects.create!(namespace_id: namespace.id) } + let(:build) { builds.create! } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 1) + stub_const("#{described_class.name}::INTERVAL", 5.minutes.to_i) + end + + context 'no security job artifacts' do + before do + table(:ci_job_artifacts) + end + + it 'does not schedule migration' do + Sidekiq::Testing.fake! do + migrate! + + expect(BackgroundMigrationWorker.jobs).to be_empty + end + end + end + + context 'has security job artifacts' do + let!(:job_artifact_1) { job_artifacts.create!(project_id: project.id, job_id: build.id, file_type: 5) } + let!(:job_artifact_2) { job_artifacts.create!(project_id: project.id, job_id: build.id, file_type: 8) } + + it 'schedules migration of security scans' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migration.up + + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, job_artifact_1.id, job_artifact_1.id) + expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, job_artifact_2.id, job_artifact_2.id) + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end + end + + context 'has non-security job artifacts' do + let!(:job_artifact_1) { job_artifacts.create!(project_id: project.id, job_id: build.id, file_type: 4) } + let!(:job_artifact_2) { job_artifacts.create!(project_id: project.id, job_id: build.id, file_type: 9) } + + it 'schedules migration of security scans' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migration.up + + expect(BackgroundMigrationWorker.jobs).to be_empty + end + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb index 8f5bfdacc3a..f5b889ef720 100644 --- a/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb @@ -72,7 +72,7 @@ shared_examples 'schedules resource mentions migration' do |resource_class, is_f it 'schedules background migrations' do Sidekiq::Testing.fake! do Timecop.freeze do - resource_count = is_for_notes ? Note.count : resource_class.count + resource_count = is_for_notes ? Note.where(noteable_type: resource_class.to_s).count : resource_class.count expect(resource_count).to eq 5 migrate! |