Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ca2a7ed5bd
commit
67cdfd2683
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>'),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Remove visibility check from epic descendant counts
|
||||
merge_request: 25975
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
title: Migrate mentions for snippet and snippet notes to snippet_user_mentions DB
|
||||
table
|
||||
merge_request: 23783
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Correctly style scoped labels in sidebar after updating
|
||||
merge_request: 22071
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Improve audit log header layout
|
||||
merge_request: 25821
|
||||
author:
|
||||
type: changed
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Schedule worker to migrate security job artifacts to security scans
|
||||
merge_request: 24125
|
||||
author:
|
||||
type: other
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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))"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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!
|
||||
|
|
|
|||
Loading…
Reference in New Issue