summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/labels_select.js34
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js4
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue8
-rw-r--r--app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue6
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js4
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue4
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue4
-rw-r--r--app/assets/javascripts/pages/projects/wikis/components/delete_wiki_modal.vue4
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js4
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/loading_button.vue21
-rw-r--r--changelogs/unreleased/196718-remove-filter-epic-counts.yml5
-rw-r--r--changelogs/unreleased/198323-migrate-snippet-mentions-to-db-table.yml6
-rw-r--r--changelogs/unreleased/38145-replace-labels-in-non-vue-js-with-gitlab-ui-component.yml5
-rw-r--r--changelogs/unreleased/jlouw-improve-audit-log-header-layout.yml5
-rw-r--r--changelogs/unreleased/migrate-security-scans.yml5
-rw-r--r--db/post_migrate/20200127111953_cleanup_empty_snippet_user_mentions.rb28
-rw-r--r--db/post_migrate/20200127131953_migrate_snippet_mentions_to_db.rb35
-rw-r--r--db/post_migrate/20200127141953_add_temporary_snippet_notes_with_mentions_index.rb20
-rw-r--r--db/post_migrate/20200127151953_migrate_snippet_notes_mentions_to_db.rb38
-rw-r--r--db/post_migrate/20200217223651_add_index_to_job_artifact_secure_reports.rb24
-rw-r--r--db/post_migrate/20200217225719_schedule_migrate_security_scans.rb31
-rw-r--r--db/schema.rb2
-rw-r--r--doc/user/clusters/applications.md57
-rw-r--r--lib/gitlab/background_migration/migrate_security_scans.rb13
-rw-r--r--lib/gitlab/background_migration/user_mentions/models/snippet.rb43
-rw-r--r--lib/gitlab/background_migration/user_mentions/models/snippet_user_mention.rb18
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/frontend/labels_select_spec.js30
-rw-r--r--spec/lib/gitlab/background_migration/user_mentions/create_resource_user_mention_spec.rb77
-rw-r--r--spec/migrations/cleanup_empty_snippet_user_mentions_spec.rb42
-rw-r--r--spec/migrations/migrate_snippet_mentions_to_db_spec.rb28
-rw-r--r--spec/migrations/migrate_snippet_notes_mentions_to_db_spec.rb31
-rw-r--r--spec/migrations/schedule_migrate_security_scans_spec.rb68
-rw-r--r--spec/support/shared_examples/lib/gitlab/background_migration/mentions_migration_shared_examples.rb2
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!