Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
1385b54a3e
commit
766b24b86b
|
@ -1 +1 @@
|
|||
47f676eea28871563414671e1016fb28b1b3e167
|
||||
d2e978f8e8f47a49c3bcfbd470b2f790e52c5ee2
|
||||
|
|
|
@ -1,20 +1,22 @@
|
|||
<script>
|
||||
import { GlBanner } from '@gitlab/ui';
|
||||
import { s__ } from '~/locale';
|
||||
import { parseBoolean, setCookie, getCookie } from '~/lib/utils/common_utils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlBanner,
|
||||
},
|
||||
inject: ['svgPath', 'inviteMembersPath'],
|
||||
inject: ['svgPath', 'inviteMembersPath', 'isDismissedKey'],
|
||||
data() {
|
||||
return {
|
||||
visible: true,
|
||||
isDismissed: parseBoolean(getCookie(this.isDismissedKey)),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleClose() {
|
||||
this.visible = false;
|
||||
setCookie(this.isDismissedKey, true);
|
||||
this.isDismissed = true;
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
|
@ -29,7 +31,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<gl-banner
|
||||
v-if="visible"
|
||||
v-if="!isDismissed"
|
||||
ref="banner"
|
||||
:title="$options.i18n.title"
|
||||
:button-text="$options.i18n.button_text"
|
||||
|
|
|
@ -8,13 +8,14 @@ export default function initInviteMembersBanner() {
|
|||
return false;
|
||||
}
|
||||
|
||||
const { svgPath, inviteMembersPath } = el.dataset;
|
||||
const { svgPath, inviteMembersPath, isDismissedKey } = el.dataset;
|
||||
|
||||
return new Vue({
|
||||
el,
|
||||
provide: {
|
||||
svgPath,
|
||||
inviteMembersPath,
|
||||
isDismissedKey,
|
||||
},
|
||||
render: createElement => createElement(InviteMembersBanner),
|
||||
});
|
||||
|
|
|
@ -68,7 +68,7 @@ export default {
|
|||
return templatedContent;
|
||||
},
|
||||
onInputChange(newVal) {
|
||||
this.parsedSource.sync(newVal, this.isWysiwygMode);
|
||||
this.parsedSource.syncContent(newVal, this.isWysiwygMode);
|
||||
this.isModified = this.parsedSource.isModified();
|
||||
},
|
||||
onModeChange(mode) {
|
||||
|
|
|
@ -1,78 +1,49 @@
|
|||
import getFrontMatterLanguageDefinition from './parse_source_file_language_support';
|
||||
import grayMatter from 'gray-matter';
|
||||
|
||||
const parseSourceFile = (raw, options = { frontMatterLanguage: 'yaml' }) => {
|
||||
const { open, close } = getFrontMatterLanguageDefinition(options.frontMatterLanguage);
|
||||
const anyChar = '[\\s\\S]';
|
||||
const frontMatterBlock = `^${open}$${anyChar}*?^${close}$`;
|
||||
const frontMatterRegex = new RegExp(`${frontMatterBlock}`, 'm');
|
||||
const preGroupedRegex = new RegExp(`(${anyChar}*?)(${frontMatterBlock})(\\s*)(${anyChar}*)`, 'm'); // preFrontMatter, frontMatter, spacing, and content
|
||||
let initial;
|
||||
let editable;
|
||||
const parseSourceFile = raw => {
|
||||
const remake = source => grayMatter(source, {});
|
||||
|
||||
const hasFrontMatter = source => frontMatterRegex.test(source);
|
||||
let editable = remake(raw);
|
||||
|
||||
const buildPayload = (source, header, spacing, body) => {
|
||||
return { raw: source, header, spacing, body };
|
||||
};
|
||||
|
||||
const parse = source => {
|
||||
if (hasFrontMatter(source)) {
|
||||
const match = source.match(preGroupedRegex);
|
||||
const [, preFrontMatter, frontMatter, spacing, content] = match;
|
||||
const header = preFrontMatter + frontMatter;
|
||||
|
||||
return buildPayload(source, header, spacing, content);
|
||||
const syncContent = (newVal, isBody) => {
|
||||
if (isBody) {
|
||||
editable.content = newVal;
|
||||
} else {
|
||||
editable = remake(newVal);
|
||||
}
|
||||
|
||||
return buildPayload(source, '', '', source);
|
||||
};
|
||||
|
||||
const syncEditable = () => {
|
||||
/*
|
||||
We re-parse as markdown editing could have added non-body changes (preFrontMatter, frontMatter, or spacing).
|
||||
Re-parsing additionally gets us the desired body that was extracted from the potentially mutated editable.raw
|
||||
*/
|
||||
editable = parse(editable.raw);
|
||||
const trimmedEditable = () => grayMatter.stringify(editable).trim();
|
||||
|
||||
const content = (isBody = false) => (isBody ? editable.content.trim() : trimmedEditable()); // gray-matter internally adds an eof newline so we trim to bypass, open issue: https://github.com/jonschlinkert/gray-matter/issues/96
|
||||
|
||||
const matter = () => editable.matter;
|
||||
|
||||
const syncMatter = newMatter => {
|
||||
const targetMatter = newMatter.replace(/---/gm, ''); // TODO dynamic delimiter removal vs. hard code
|
||||
const currentMatter = matter();
|
||||
const currentContent = content();
|
||||
const newSource = currentContent.replace(currentMatter, targetMatter);
|
||||
syncContent(newSource);
|
||||
editable.matter = newMatter;
|
||||
};
|
||||
|
||||
const refreshEditableRaw = () => {
|
||||
editable.raw = `${editable.header}${editable.spacing}${editable.body}`;
|
||||
const matterObject = () => editable.data;
|
||||
|
||||
const syncMatterObject = obj => {
|
||||
editable.data = obj;
|
||||
};
|
||||
|
||||
const sync = (newVal, isBodyToRaw) => {
|
||||
const editableKey = isBodyToRaw ? 'body' : 'raw';
|
||||
editable[editableKey] = newVal;
|
||||
|
||||
if (isBodyToRaw) {
|
||||
refreshEditableRaw();
|
||||
}
|
||||
|
||||
syncEditable();
|
||||
};
|
||||
|
||||
const frontMatter = () => editable.header;
|
||||
|
||||
const setFrontMatter = val => {
|
||||
editable.header = val;
|
||||
refreshEditableRaw();
|
||||
};
|
||||
|
||||
const content = (isBody = false) => {
|
||||
const editableKey = isBody ? 'body' : 'raw';
|
||||
return editable[editableKey];
|
||||
};
|
||||
|
||||
const isModified = () => initial.raw !== editable.raw;
|
||||
|
||||
initial = parse(raw);
|
||||
editable = parse(raw);
|
||||
const isModified = () => trimmedEditable() !== raw;
|
||||
|
||||
return {
|
||||
frontMatter,
|
||||
setFrontMatter,
|
||||
matter,
|
||||
syncMatter,
|
||||
matterObject,
|
||||
syncMatterObject,
|
||||
content,
|
||||
syncContent,
|
||||
isModified,
|
||||
sync,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
const frontMatterLanguageDefinitions = [
|
||||
{ name: 'yaml', open: '---', close: '---' },
|
||||
{ name: 'toml', open: '\\+\\+\\+', close: '\\+\\+\\+' },
|
||||
{ name: 'json', open: '{', close: '}' },
|
||||
];
|
||||
|
||||
const getFrontMatterLanguageDefinition = name => {
|
||||
const languageDefinition = frontMatterLanguageDefinitions.find(def => def.name === name);
|
||||
|
||||
if (!languageDefinition) {
|
||||
throw new Error(`Unsupported front matter language: ${name}`);
|
||||
}
|
||||
|
||||
return languageDefinition;
|
||||
};
|
||||
|
||||
export default getFrontMatterLanguageDefinition;
|
|
@ -45,8 +45,8 @@ export default class MergeRequestStore {
|
|||
this.mergeTrainWhenPipelineSucceedsDocsPath = data.merge_train_when_pipeline_succeeds_docs_path;
|
||||
this.mergeStatus = data.merge_status;
|
||||
this.commitMessage = data.default_merge_commit_message;
|
||||
this.shortMergeCommitSha = data.short_merge_commit_sha;
|
||||
this.mergeCommitSha = data.merge_commit_sha;
|
||||
this.shortMergeCommitSha = data.short_merged_commit_sha;
|
||||
this.mergeCommitSha = data.merged_commit_sha;
|
||||
this.commitMessageWithDescription = data.default_merge_commit_message_with_description;
|
||||
this.commitsCount = data.commits_count;
|
||||
this.divergedCommitsCount = data.diverged_commits_count;
|
||||
|
@ -135,7 +135,7 @@ export default class MergeRequestStore {
|
|||
this.createIssueToResolveDiscussionsPath = data.create_issue_to_resolve_discussions_path;
|
||||
this.mergePath = data.merge_path;
|
||||
this.canMerge = Boolean(data.merge_path);
|
||||
this.mergeCommitPath = data.merge_commit_path;
|
||||
this.mergeCommitPath = data.merged_commit_path;
|
||||
this.canPushToSourceBranch = data.can_push_to_source_branch;
|
||||
|
||||
if (data.work_in_progress !== undefined) {
|
||||
|
|
|
@ -64,7 +64,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
|
|||
render: ->(partial, locals) { view_to_html_string(partial, locals) }
|
||||
}
|
||||
|
||||
options = additional_attributes.merge(diff_view: diff_view)
|
||||
options = additional_attributes.merge(diff_view: Feature.enabled?(:unified_diff_lines, @merge_request.project) ? "inline" : diff_view)
|
||||
|
||||
if @merge_request.project.context_commits_enabled?
|
||||
options[:context_commits] = @merge_request.recent_context_commits
|
||||
|
|
|
@ -428,7 +428,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42438')
|
||||
end
|
||||
|
||||
def reports_response(report_comparison)
|
||||
def reports_response(report_comparison, pipeline = nil)
|
||||
if pipeline&.active?
|
||||
::Gitlab::PollingInterval.set_header(response, interval: 3000)
|
||||
|
||||
render json: '', status: :no_content && return
|
||||
end
|
||||
|
||||
case report_comparison[:status]
|
||||
when :parsing
|
||||
::Gitlab::PollingInterval.set_header(response, interval: 3000)
|
||||
|
|
|
@ -29,11 +29,11 @@ module Mutations
|
|||
|
||||
argument :move_before_id, GraphQL::ID_TYPE,
|
||||
required: false,
|
||||
description: 'ID of issue before which the current issue will be positioned at'
|
||||
description: 'ID of issue that should be placed before the current issue'
|
||||
|
||||
argument :move_after_id, GraphQL::ID_TYPE,
|
||||
required: false,
|
||||
description: 'ID of issue after which the current issue will be positioned at'
|
||||
description: 'ID of issue that should be placed after the current issue'
|
||||
|
||||
def ready?(**args)
|
||||
if move_arguments(args).blank?
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
module Admin
|
||||
module Analytics
|
||||
module InstanceStatistics
|
||||
class MeasurementsResolver < BaseResolver
|
||||
include Gitlab::Graphql::Authorize::AuthorizeResource
|
||||
|
||||
type Types::Admin::Analytics::InstanceStatistics::MeasurementType, null: true
|
||||
|
||||
argument :identifier, Types::Admin::Analytics::InstanceStatistics::MeasurementIdentifierEnum,
|
||||
required: true,
|
||||
description: 'The type of measurement/statistics to retrieve'
|
||||
|
||||
def resolve(identifier:)
|
||||
authorize!
|
||||
|
||||
::Analytics::InstanceStatistics::Measurement
|
||||
.with_identifier(identifier)
|
||||
.order_by_latest
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def authorize!
|
||||
admin? || raise_resource_not_available_error!
|
||||
end
|
||||
|
||||
def admin?
|
||||
context[:current_user].present? && context[:current_user].admin?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Admin
|
||||
module Analytics
|
||||
module InstanceStatistics
|
||||
class MeasurementIdentifierEnum < BaseEnum
|
||||
graphql_name 'MeasurementIdentifier'
|
||||
description 'Possible identifier types for a measurement'
|
||||
|
||||
value 'PROJECTS', 'Project count', value: :projects
|
||||
value 'USERS', 'User count', value: :users
|
||||
value 'ISSUES', 'Issue count', value: :issues
|
||||
value 'MERGE_REQUESTS', 'Merge request count', value: :merge_requests
|
||||
value 'GROUPS', 'Group count', value: :groups
|
||||
value 'PIPELINES', 'Pipeline count', value: :pipelines
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
# rubocop:disable Graphql/AuthorizeTypes
|
||||
|
||||
module Types
|
||||
module Admin
|
||||
module Analytics
|
||||
module InstanceStatistics
|
||||
class MeasurementType < BaseObject
|
||||
graphql_name 'InstanceStatisticsMeasurement'
|
||||
description 'Represents a recorded measurement (object count) for the Admins'
|
||||
|
||||
field :recorded_at, Types::TimeType, null: true,
|
||||
description: 'The time the measurement was recorded'
|
||||
|
||||
field :count, GraphQL::INT_TYPE, null: false,
|
||||
description: 'Object count'
|
||||
|
||||
field :identifier, Types::Admin::Analytics::InstanceStatistics::MeasurementIdentifierEnum, null: false,
|
||||
description: 'The type of objects being measured'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -76,6 +76,11 @@ module Types
|
|||
argument :id, ::Types::GlobalIDType[::Issue], required: true, description: 'The global ID of the Issue'
|
||||
end
|
||||
|
||||
field :instance_statistics_measurements, Types::Admin::Analytics::InstanceStatistics::MeasurementType.connection_type,
|
||||
null: true,
|
||||
description: 'Get statistics on the instance',
|
||||
resolver: Resolvers::Admin::Analytics::InstanceStatistics::MeasurementsResolver
|
||||
|
||||
def design_management
|
||||
DesignManagementObject.new(nil)
|
||||
end
|
||||
|
|
|
@ -3,10 +3,20 @@
|
|||
module Analytics
|
||||
module InstanceStatistics
|
||||
class Measurement < ApplicationRecord
|
||||
enum identifier: { projects: 1, users: 2 }
|
||||
enum identifier: {
|
||||
projects: 1,
|
||||
users: 2,
|
||||
issues: 3,
|
||||
merge_requests: 4,
|
||||
groups: 5,
|
||||
pipelines: 6
|
||||
}
|
||||
|
||||
validates :recorded_at, :identifier, :count, presence: true
|
||||
validates :recorded_at, uniqueness: { scope: :identifier }
|
||||
|
||||
scope :order_by_latest, -> { order(recorded_at: :desc) }
|
||||
scope :with_identifier, -> (identifier) { where(identifier: identifier) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1473,6 +1473,19 @@ class MergeRequest < ApplicationRecord
|
|||
Commit.truncate_sha(merge_commit_sha) if merge_commit_sha
|
||||
end
|
||||
|
||||
def merged_commit_sha
|
||||
return unless merged?
|
||||
|
||||
sha = merge_commit_sha || squash_commit_sha || diff_head_sha
|
||||
sha.presence
|
||||
end
|
||||
|
||||
def short_merged_commit_sha
|
||||
if sha = merged_commit_sha
|
||||
Commit.truncate_sha(sha)
|
||||
end
|
||||
end
|
||||
|
||||
def can_be_reverted?(current_user)
|
||||
return false unless merge_commit
|
||||
return false unless merged_at
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
class MergeRequestPollCachedWidgetEntity < IssuableEntity
|
||||
expose :auto_merge_enabled
|
||||
expose :state
|
||||
expose :merge_commit_sha
|
||||
expose :short_merge_commit_sha
|
||||
expose :merged_commit_sha
|
||||
expose :short_merged_commit_sha
|
||||
expose :merge_error
|
||||
expose :public_merge_status, as: :merge_status
|
||||
expose :merge_user_id
|
||||
|
@ -56,9 +56,9 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
|
|||
presenter(merge_request).target_branch_tree_path
|
||||
end
|
||||
|
||||
expose :merge_commit_path do |merge_request|
|
||||
if merge_request.merge_commit_sha
|
||||
project_commit_path(merge_request.project, merge_request.merge_commit_sha)
|
||||
expose :merged_commit_path do |merge_request|
|
||||
if sha = merge_request.merged_commit_sha
|
||||
project_commit_path(merge_request.project, sha)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
%p #{@service.description} template.
|
||||
|
||||
= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form' } do |form|
|
||||
= form_for :service, url: admin_application_settings_service_path, method: :put, html: { class: 'fieldset-form js-integration-settings-form' } do |form|
|
||||
= render 'shared/service_settings', form: form, integration: @service
|
||||
|
||||
.footer-block.row-content-block
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
= content_for :group_invite_members_banner do
|
||||
.container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" }
|
||||
.js-group-invite-members-banner{ data: { svg_path: image_path('illustrations/merge_requests.svg'),
|
||||
is_dismissed_key: "invite_#{@group.id}_#{current_user.id}",
|
||||
invite_members_path: group_group_members_path(@group) } }
|
||||
|
||||
= content_for :meta_tags do
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Resolve Fix validation on External Wiki service template form
|
||||
merge_request: 41964
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Expose Instance Statistics measurements (object counts) via GraphQL
|
||||
merge_request: 40871
|
||||
author:
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Refuse to perform an LFS clean on projects that are fork roots
|
||||
merge_request: 41703
|
||||
author:
|
||||
type: fixed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Display merged commit sha in fast-forward merge mode
|
||||
merge_request: 41369
|
||||
author: Mycroft Kang @TaehyeokKang
|
||||
type: added
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Ensure namespace settings are backfilled via migration
|
||||
merge_request: 41679
|
||||
author:
|
||||
type: other
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: usage_data_api
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41301
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/235459
|
||||
group: group::telemetry
|
||||
type: development
|
||||
default_enabled: false
|
|
@ -0,0 +1,35 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CompleteNamespaceSettingsMigration < ActiveRecord::Migration[5.2]
|
||||
DOWNTIME = false
|
||||
BATCH_SIZE = 10000
|
||||
|
||||
class Namespace < ActiveRecord::Base
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'namespaces'
|
||||
end
|
||||
|
||||
def up
|
||||
Gitlab::BackgroundMigration.steal('BackfillNamespaceSettings')
|
||||
|
||||
ensure_data_migration
|
||||
end
|
||||
|
||||
def down
|
||||
# no-op
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def ensure_data_migration
|
||||
Namespace.each_batch(of: BATCH_SIZE) do |query|
|
||||
missing_count = query.where("NOT EXISTS (SELECT 1 FROM namespace_settings WHERE namespace_settings.namespace_id=namespaces.id)").limit(1).size
|
||||
if missing_count > 0
|
||||
min, max = query.pluck("MIN(id), MAX(id)").flatten
|
||||
# we expect low record count so inline execution is fine.
|
||||
Gitlab::BackgroundMigration::BackfillNamespaceSettings.new.perform(min, max)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
2311967a9f68e1a428662e0231752ad0d844063d66cca895211d38f9ae928d94
|
|
@ -76,6 +76,7 @@ Citus
|
|||
clonable
|
||||
Cloudwatch
|
||||
Cobertura
|
||||
Codepen
|
||||
Cognito
|
||||
colocated
|
||||
colocating
|
||||
|
|
|
@ -455,7 +455,7 @@ POST /projects/:id/boards/:board_id/lists
|
|||
NOTE: **Note:**
|
||||
Label, assignee and milestone arguments are mutually exclusive,
|
||||
that is, only one of them are accepted in a request.
|
||||
Check the [Issue Board docs](../user/project/issue_board.md#summary-of-features-per-tier)
|
||||
Check the [Issue Board docs](../user/project/issue_board.md)
|
||||
for more information regarding the required license for each list type.
|
||||
|
||||
```shell
|
||||
|
|
|
@ -7410,6 +7410,61 @@ type InstanceSecurityDashboard {
|
|||
): VulnerabilitySeveritiesCount
|
||||
}
|
||||
|
||||
"""
|
||||
Represents a recorded measurement (object count) for the Admins
|
||||
"""
|
||||
type InstanceStatisticsMeasurement {
|
||||
"""
|
||||
Object count
|
||||
"""
|
||||
count: Int!
|
||||
|
||||
"""
|
||||
The type of objects being measured
|
||||
"""
|
||||
identifier: MeasurementIdentifier!
|
||||
|
||||
"""
|
||||
The time the measurement was recorded
|
||||
"""
|
||||
recordedAt: Time
|
||||
}
|
||||
|
||||
"""
|
||||
The connection type for InstanceStatisticsMeasurement.
|
||||
"""
|
||||
type InstanceStatisticsMeasurementConnection {
|
||||
"""
|
||||
A list of edges.
|
||||
"""
|
||||
edges: [InstanceStatisticsMeasurementEdge]
|
||||
|
||||
"""
|
||||
A list of nodes.
|
||||
"""
|
||||
nodes: [InstanceStatisticsMeasurement]
|
||||
|
||||
"""
|
||||
Information to aid in pagination.
|
||||
"""
|
||||
pageInfo: PageInfo!
|
||||
}
|
||||
|
||||
"""
|
||||
An edge in a connection.
|
||||
"""
|
||||
type InstanceStatisticsMeasurementEdge {
|
||||
"""
|
||||
A cursor for use in pagination.
|
||||
"""
|
||||
cursor: String!
|
||||
|
||||
"""
|
||||
The item at the end of the edge.
|
||||
"""
|
||||
node: InstanceStatisticsMeasurement
|
||||
}
|
||||
|
||||
"""
|
||||
Incident severity
|
||||
"""
|
||||
|
@ -7872,12 +7927,12 @@ input IssueMoveListInput {
|
|||
iid: String!
|
||||
|
||||
"""
|
||||
ID of issue after which the current issue will be positioned at
|
||||
ID of issue that should be placed after the current issue
|
||||
"""
|
||||
moveAfterId: ID
|
||||
|
||||
"""
|
||||
ID of issue before which the current issue will be positioned at
|
||||
ID of issue that should be placed before the current issue
|
||||
"""
|
||||
moveBeforeId: ID
|
||||
|
||||
|
@ -9044,6 +9099,41 @@ type MarkAsSpamSnippetPayload {
|
|||
snippet: Snippet
|
||||
}
|
||||
|
||||
"""
|
||||
Possible identifier types for a measurement
|
||||
"""
|
||||
enum MeasurementIdentifier {
|
||||
"""
|
||||
Group count
|
||||
"""
|
||||
GROUPS
|
||||
|
||||
"""
|
||||
Issue count
|
||||
"""
|
||||
ISSUES
|
||||
|
||||
"""
|
||||
Merge request count
|
||||
"""
|
||||
MERGE_REQUESTS
|
||||
|
||||
"""
|
||||
Pipeline count
|
||||
"""
|
||||
PIPELINES
|
||||
|
||||
"""
|
||||
Project count
|
||||
"""
|
||||
PROJECTS
|
||||
|
||||
"""
|
||||
User count
|
||||
"""
|
||||
USERS
|
||||
}
|
||||
|
||||
interface MemberInterface {
|
||||
"""
|
||||
GitLab::Access level
|
||||
|
@ -13510,6 +13600,36 @@ type Query {
|
|||
"""
|
||||
instanceSecurityDashboard: InstanceSecurityDashboard
|
||||
|
||||
"""
|
||||
Get statistics on the instance
|
||||
"""
|
||||
instanceStatisticsMeasurements(
|
||||
"""
|
||||
Returns the elements in the list that come after the specified cursor.
|
||||
"""
|
||||
after: String
|
||||
|
||||
"""
|
||||
Returns the elements in the list that come before the specified cursor.
|
||||
"""
|
||||
before: String
|
||||
|
||||
"""
|
||||
Returns the first _n_ elements from the list.
|
||||
"""
|
||||
first: Int
|
||||
|
||||
"""
|
||||
The type of measurement/statistics to retrieve
|
||||
"""
|
||||
identifier: MeasurementIdentifier!
|
||||
|
||||
"""
|
||||
Returns the last _n_ elements from the list.
|
||||
"""
|
||||
last: Int
|
||||
): InstanceStatisticsMeasurementConnection
|
||||
|
||||
"""
|
||||
Find an issue
|
||||
"""
|
||||
|
|
|
@ -20447,6 +20447,181 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "InstanceStatisticsMeasurement",
|
||||
"description": "Represents a recorded measurement (object count) for the Admins",
|
||||
"fields": [
|
||||
{
|
||||
"name": "count",
|
||||
"description": "Object count",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "identifier",
|
||||
"description": "The type of objects being measured",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "MeasurementIdentifier",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "recordedAt",
|
||||
"description": "The time the measurement was recorded",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Time",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "InstanceStatisticsMeasurementConnection",
|
||||
"description": "The connection type for InstanceStatisticsMeasurement.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "edges",
|
||||
"description": "A list of edges.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "InstanceStatisticsMeasurementEdge",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "nodes",
|
||||
"description": "A list of nodes.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "InstanceStatisticsMeasurement",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "pageInfo",
|
||||
"description": "Information to aid in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "PageInfo",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "InstanceStatisticsMeasurementEdge",
|
||||
"description": "An edge in a connection.",
|
||||
"fields": [
|
||||
{
|
||||
"name": "cursor",
|
||||
"description": "A cursor for use in pagination.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "node",
|
||||
"description": "The item at the end of the edge.",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "InstanceStatisticsMeasurement",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
|
@ -21730,7 +21905,7 @@
|
|||
},
|
||||
{
|
||||
"name": "moveBeforeId",
|
||||
"description": "ID of issue before which the current issue will be positioned at",
|
||||
"description": "ID of issue that should be placed before the current issue",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
|
@ -21740,7 +21915,7 @@
|
|||
},
|
||||
{
|
||||
"name": "moveAfterId",
|
||||
"description": "ID of issue after which the current issue will be positioned at",
|
||||
"description": "ID of issue that should be placed after the current issue",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
|
@ -25093,6 +25268,53 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "ENUM",
|
||||
"name": "MeasurementIdentifier",
|
||||
"description": "Possible identifier types for a measurement",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": [
|
||||
{
|
||||
"name": "PROJECTS",
|
||||
"description": "Project count",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "USERS",
|
||||
"description": "User count",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "ISSUES",
|
||||
"description": "Issue count",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "MERGE_REQUESTS",
|
||||
"description": "Merge request count",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "GROUPS",
|
||||
"description": "Group count",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "PIPELINES",
|
||||
"description": "Pipeline count",
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "INTERFACE",
|
||||
"name": "MemberInterface",
|
||||
|
@ -39660,6 +39882,73 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "instanceStatisticsMeasurements",
|
||||
"description": "Get statistics on the instance",
|
||||
"args": [
|
||||
{
|
||||
"name": "identifier",
|
||||
"description": "The type of measurement/statistics to retrieve",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "ENUM",
|
||||
"name": "MeasurementIdentifier",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "after",
|
||||
"description": "Returns the elements in the list that come after the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "before",
|
||||
"description": "Returns the elements in the list that come before the specified cursor.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "first",
|
||||
"description": "Returns the first _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "last",
|
||||
"description": "Returns the last _n_ elements from the list.",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Int",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "InstanceStatisticsMeasurementConnection",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "issue",
|
||||
"description": "Find an issue",
|
||||
|
|
|
@ -1100,6 +1100,16 @@ Represents a Group Membership
|
|||
| `vulnerabilityGrades` | VulnerableProjectsByGrade! => Array | Represents vulnerable project counts for each grade |
|
||||
| `vulnerabilitySeveritiesCount` | VulnerabilitySeveritiesCount | Counts for each vulnerability severity from projects selected in Instance Security Dashboard |
|
||||
|
||||
## InstanceStatisticsMeasurement
|
||||
|
||||
Represents a recorded measurement (object count) for the Admins
|
||||
|
||||
| Name | Type | Description |
|
||||
| --- | ---- | ---------- |
|
||||
| `count` | Int! | Object count |
|
||||
| `identifier` | MeasurementIdentifier! | The type of objects being measured |
|
||||
| `recordedAt` | Time | The time the measurement was recorded |
|
||||
|
||||
## Issue
|
||||
|
||||
| Name | Type | Description |
|
||||
|
|
|
@ -1181,9 +1181,9 @@ PUT /projects/:id/issues/:issue_iid/reorder
|
|||
| Attribute | Type | Required | Description |
|
||||
|-------------|---------|----------|--------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `issue_iid` | integer | yes | The internal ID of a project's issue |
|
||||
| `move_after_id` | integer | no | The ID of a project's issue to move this issue after |
|
||||
| `move_before_id` | integer | no | The ID of a project's issue to move this issue before |
|
||||
| `issue_iid` | integer | yes | The internal ID of the project's issue |
|
||||
| `move_after_id` | integer | no | The ID of a project's issue that should be placed after this issue |
|
||||
| `move_before_id` | integer | no | The ID of a project's issue that should be placed before this issue |
|
||||
|
||||
```shell
|
||||
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/4/issues/85/reorder?move_after_id=51&move_before_id=92"
|
||||
|
|
|
@ -68,7 +68,7 @@ should be a one-to-one mapping of `licensed` feature flags to licensed features.
|
|||
|
||||
`licensed` feature flags must be `default_enabled: true`, because that's the only
|
||||
supported option in the current implementation. This is under development as per
|
||||
the [related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/218667.
|
||||
the [related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/218667).
|
||||
|
||||
The `licensed` type has a dedicated set of functions to check if a licensed
|
||||
feature is available for a project or namespace. This check validates
|
||||
|
|
|
@ -312,6 +312,28 @@ Implemented using Redis methods [PFADD](https://redis.io/commands/pfadd) and [PF
|
|||
end
|
||||
```
|
||||
|
||||
1. Track event using `UsageData` API
|
||||
|
||||
Increment unique users count using Redis HLL, for given event name.
|
||||
|
||||
In order to be able to increment the values the related feature `usage_data<event_name>` should be enabled.
|
||||
|
||||
```plaintext
|
||||
POST /usage_data/increment_unique_users
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
| :-------- | :--- | :------- | :---------- |
|
||||
| `event` | string | yes | The event name it should be tracked |
|
||||
|
||||
Response
|
||||
|
||||
Return 200 if tracking failed for any reason.
|
||||
|
||||
- `401 Unauthorized` if user is not authenticated
|
||||
- `400 Bad request` if event parameter is missing
|
||||
- `200` if event was tracked or any errors
|
||||
|
||||
1. Track event using base module `Gitlab::UsageDataCounters::HLLRedisCounter.track_event(entity_id, event_name)`.
|
||||
|
||||
Arguments:
|
||||
|
|
|
@ -135,9 +135,9 @@ docker stop gitlab-gitaly-ha praefect postgres gitaly3 gitaly2 gitaly1
|
|||
docker rm gitlab-gitaly-ha praefect postgres gitaly3 gitaly2 gitaly1
|
||||
```
|
||||
|
||||
## Guide to run and debug monitor tests
|
||||
## Guide to run and debug Monitor tests
|
||||
|
||||
## How to set up
|
||||
### How to set up
|
||||
|
||||
To run the Monitor tests locally, against the GDK, please follow the preparation steps below:
|
||||
|
||||
|
@ -149,7 +149,7 @@ To enable Auto DevOps in GDK, follow the [associated setup](https://gitlab.com/g
|
|||
|
||||
You might see NGINX issues when you run `gdk start` or `gdk restart`. In that case, run `sft login` to revalidate your credentials and regain access the QA Tunnel.
|
||||
|
||||
## How to run
|
||||
### How to run
|
||||
|
||||
Navigate to the folder in `/your-gdk/gitlab/qa` and issue the command:
|
||||
|
||||
|
@ -174,7 +174,7 @@ At the moment of this writing, there are two specs which run monitor tests:
|
|||
-`qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb` - has the specs of features in GitLab Core
|
||||
-`qa/specs/features/ee/browser_ui/8_monitor/all_monitor_features_spec.rb` - has the specs of features for paid GitLab (Enterprise Edition)
|
||||
|
||||
## How to debug
|
||||
### How to debug
|
||||
|
||||
The monitor tests follow this setup flow:
|
||||
|
||||
|
@ -187,7 +187,7 @@ The monitor tests follow this setup flow:
|
|||
The test requires a number of components. The setup requires time to collect the metrics of a real deployment.
|
||||
The complexity of the setup may lead to problems unrelated to the app. The following sections include common strategies to debug possible issues.
|
||||
|
||||
### Deployment with Auto DevOps
|
||||
#### Deployment with Auto DevOps
|
||||
|
||||
When debugging issues in the CI or locally in the CLI, open the Kubernetes job in the pipeline.
|
||||
In the job log window, click on the top right icon labeled as *"Show complete raw"* to reveal raw job logs.
|
||||
|
@ -205,7 +205,7 @@ The long test setup does not take screenshots of failures, which is a known [iss
|
|||
However, if the spec fails (after a successful deployment) then you should be able to find screenshots which display the feature failure.
|
||||
To access them in CI, go to the main job log window, look on the left side panel's Job artifacts section, and click Browse.
|
||||
|
||||
### Common issues
|
||||
#### Common issues
|
||||
|
||||
**Container Registry**
|
||||
|
||||
|
@ -259,3 +259,137 @@ gitlab-managed-apps install-runner 0/1 Evicted
|
|||
```
|
||||
|
||||
You can free some memory with either of the following commands: `docker prune system` or `docker prune volume`.
|
||||
|
||||
## Geo tests
|
||||
|
||||
Geo end-to-end tests can run locally against a [Geo GDK setup](https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/master/doc/howto/geo.md) or on Geo spun up in Docker containers.
|
||||
|
||||
### Using Geo GDK
|
||||
|
||||
Run from the [`qa/` directory](https://gitlab.com/gitlab-org/gitlab/-/blob/f7272b77e80215c39d1ffeaed27794c220dbe03f/qa) with both GDK Geo primary and Geo secondary instances running:
|
||||
|
||||
```shell
|
||||
CHROME_HEADLESS=false bundle exec bin/qa QA::EE::Scenario::Test::Geo --primary-address http://localhost:3001 --secondary-address http://localhost:3002 --without-setup
|
||||
```
|
||||
|
||||
### Using Geo in Docker
|
||||
|
||||
You can use [GitLab-QA Orchestrator](https://gitlab.com/gitlab-org/gitlab-qa) to orchestrate two GitLab containers and configure them as a Geo setup.
|
||||
|
||||
Geo requires an EE license. To visit the Geo sites in your browser, you will need a reverse proxy server (for example, [NGINX](https://www.nginx.com/)).
|
||||
|
||||
1. Export your EE license
|
||||
|
||||
```shell
|
||||
export EE_LICENSE=$(cat <path/to/your/gitlab_license>)
|
||||
```
|
||||
|
||||
1. (Optional) Pull the GitLab image
|
||||
|
||||
This step is optional because pulling the Docker image is part of the [`Test::Integration::Geo` orchestrated scenario](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/d8c5c40607c2be0eda58bbca1b9f534b00889a0b/lib/gitlab/qa/scenario/test/integration/geo.rb). However, it's easier to monitor the download progress if you pull the image first, and the scenario will skip this step after checking that the image is up to date.
|
||||
|
||||
```shell
|
||||
# For the most recent nightly image
|
||||
docker pull gitlab/gitlab-ee:nightly
|
||||
|
||||
# For a specific release
|
||||
docker pull gitlab/gitlab-ee:13.0.10-ee.0
|
||||
|
||||
# For a specific image
|
||||
docker pull registry.gitlab.com/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:examplesha123456789
|
||||
```
|
||||
|
||||
1. Run the [`Test::Integration::Geo` orchestrated scenario](https://gitlab.com/gitlab-org/gitlab-qa/-/blob/d8c5c40607c2be0eda58bbca1b9f534b00889a0b/lib/gitlab/qa/scenario/test/integration/geo.rb) with the `--no-teardown` option to build the GitLab containers, configure the Geo setup, and run Geo end-to-end tests. Running the tests after the Geo setup is complete is optional; the containers will keep running after you stop the tests.
|
||||
|
||||
```shell
|
||||
# Using the most recent nightly image
|
||||
gitlab-qa Test::Integration::Geo EE --no-teardown
|
||||
|
||||
# Using a specific GitLab release
|
||||
gitlab-qa Test::Integration::Geo EE:13.0.10-ee.0 --no-teardown
|
||||
|
||||
# Using a full image address
|
||||
GITLAB_QA_ACCESS_TOKEN=your-token-here gitlab-qa Test::Integration::Geo registry.gitlab.com/gitlab-org/build/omnibus-gitlab-mirror/gitlab-ee:examplesha123456789 --no-teardown
|
||||
```
|
||||
|
||||
You can use the `--no-tests` option to build the containers only, and then run the [`EE::Scenario::Test::Geo` scenario](https://gitlab.com/gitlab-org/gitlab/-/blob/f7272b77e80215c39d1ffeaed27794c220dbe03f/qa/qa/ee/scenario/test/geo.rb) from your GDK to complete setup and run tests. However, there might be configuration issues if your GDK and the containers are based on different GitLab versions. With the `--no-teardown` option, GitLab-QA uses the same GitLab version for the GitLab containers and the GitLab QA container used to configure the Geo setup.
|
||||
|
||||
1. To visit the Geo sites in your browser, proxy requests to the hostnames used inside the containers. NGINX is used as the reverse proxy server for this example.
|
||||
|
||||
_Map the hostnames to the local IP in `/etc/hosts` file on your machine:_
|
||||
|
||||
```plaintext
|
||||
127.0.0.1 gitlab-primary.geo gitlab-secondary.geo
|
||||
```
|
||||
|
||||
_Note the assigned ports:_
|
||||
|
||||
```shell
|
||||
$ docker port gitlab-primary
|
||||
|
||||
80/tcp -> 0.0.0.0:32768
|
||||
|
||||
$ docker port gitlab-secondary
|
||||
|
||||
80/tcp -> 0.0.0.0:32769
|
||||
```
|
||||
|
||||
_Configure the reverse proxy server with the assigned ports in `nginx.conf` file (usually found in `/usr/local/etc/nginx` on a Mac):_
|
||||
|
||||
```plaintext
|
||||
server {
|
||||
server_name gitlab-primary.geo;
|
||||
location / {
|
||||
proxy_pass http://localhost:32768; # Change port to your assigned port
|
||||
proxy_set_header Host gitlab-primary.geo;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
server_name gitlab-secondary.geo;
|
||||
location / {
|
||||
proxy_pass http://localhost:32769; # Change port to your assigned port
|
||||
proxy_set_header Host gitlab-secondary.geo;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
_Start or reload the reverse proxy server:_
|
||||
|
||||
```shell
|
||||
sudo nginx
|
||||
# or
|
||||
sudo nginx -s reload
|
||||
```
|
||||
|
||||
1. To run end-to-end tests from your local GDK, run the [`EE::Scenario::Test::Geo` scenario](https://gitlab.com/gitlab-org/gitlab/-/blob/f7272b77e80215c39d1ffeaed27794c220dbe03f/qa/qa/ee/scenario/test/geo.rb) from the [`gitlab/qa/` directory](https://gitlab.com/gitlab-org/gitlab/-/blob/f7272b77e80215c39d1ffeaed27794c220dbe03f/qa). Include `--without-setup` to skip the Geo configuration steps.
|
||||
|
||||
```shell
|
||||
QA_DEBUG=true GITLAB_QA_ACCESS_TOKEN=[add token here] GITLAB_QA_ADMIN_ACCESS_TOKEN=[add token here] bundle exec bin/qa QA::EE::Scenario::Test::Geo \
|
||||
--primary-address http://gitlab-primary.geo \
|
||||
--secondary-address http://gitlab-secondary.geo \
|
||||
--without-setup
|
||||
```
|
||||
|
||||
If the containers need to be configured first (for example, if you used the `--no-tests` option in the previous step), run the `QA::EE::Scenario::Test::Geo scenario` as shown below to first do the Geo configuration steps, and then run Geo end-to-end tests. Make sure that `EE_LICENSE` is (still) defined in your shell session.
|
||||
|
||||
```shell
|
||||
QA_DEBUG=true bundle exec bin/qa QA::EE::Scenario::Test::Geo \
|
||||
--primary-address http://gitlab-primary.geo \
|
||||
--primary-name gitlab-primary \
|
||||
--secondary-address http://gitlab-secondary.geo \
|
||||
--secondary-name gitlab-secondary
|
||||
```
|
||||
|
||||
1. Stop and remove containers
|
||||
|
||||
```shell
|
||||
docker stop gitlab-primary gitlab-secondary
|
||||
docker rm gitlab-primary gitlab-secondary
|
||||
```
|
||||
|
||||
#### Notes
|
||||
|
||||
- You can find the full image address from a pipeline by [following these instructions](https://about.gitlab.com/handbook/engineering/quality/guidelines/tips-and-tricks/#running-gitlab-qa-pipeline-against-a-specific-gitlab-release). You might be prompted to set the `GITLAB_QA_ACCESS_TOKEN` variable if you specify the full image address.
|
||||
- You can increase the wait time for replication by setting `GEO_MAX_FILE_REPLICATION_TIME` and `GEO_MAX_DB_REPLICATION_TIME`. The default is 120 seconds.
|
||||
- To save time during tests, create a Personal Access Token with API access on the Geo primary node, and pass that value in as `GITLAB_QA_ACCESS_TOKEN` and `GITLAB_QA_ADMIN_ACCESS_TOKEN`.
|
||||
|
|
|
@ -10,6 +10,10 @@ DANGER: **Danger:**
|
|||
Do not run this within 12 hours of a GitLab upgrade. This is to ensure that all background migrations
|
||||
have finished, which otherwise may lead to data loss.
|
||||
|
||||
CAUTION: **WARNING:**
|
||||
Removing LFS files from a project with forks is currently unsafe. The rake task
|
||||
will refuse to run on projects with forks.
|
||||
|
||||
When you remove LFS files from a repository's history, they become orphaned and continue to consume
|
||||
disk space. With this Rake task, you can remove invalid references from the database, which
|
||||
will allow garbage collection of LFS files.
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
|
@ -313,6 +313,19 @@ To create a value stream:
|
|||
|
||||
![New value stream](img/new_value_stream_v13_3.png "Creating a new value stream")
|
||||
|
||||
### Deleting a value stream
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/221205) in GitLab 13.4.
|
||||
|
||||
To delete a custom value stream:
|
||||
|
||||
1. Navigate to your group's **Analytics > Value Stream**.
|
||||
1. Click the Value stream dropdown and select the value stream you would like to delete.
|
||||
1. Click the **Delete (name of value stream)**.
|
||||
1. Click the **Delete** button to confirm.
|
||||
|
||||
![Delete value stream](img/delete_value_stream_v13.4.png "Deleting a custom value stream")
|
||||
|
||||
### Disabling custom value streams
|
||||
|
||||
Custom value streams are enabled by default. If you have a self-managed instance, an
|
||||
|
|
|
@ -15,18 +15,22 @@ The Security Configuration page displays the configuration state of each securit
|
|||
current project.
|
||||
|
||||
To view a project's security configuration, go to the project's home page,
|
||||
then in the left sidebar, go to **Security & Compliance** > **Configuration**.
|
||||
then in the left sidebar go to **Security & Compliance > Configuration**.
|
||||
|
||||
For each security control the page displays:
|
||||
|
||||
- **Status** - Status of the security control: enabled, not enabled, or available.
|
||||
- **Manage** - A management option or a link to the documentation.
|
||||
|
||||
## Status
|
||||
|
||||
For each security control, the page displays the status and either a management option or a
|
||||
documentation link.
|
||||
|
||||
The status of each security control is determined by the project's latest default branch
|
||||
[CI pipeline](../../../ci/pipelines/index.md).
|
||||
If a job with the expected security report artifact exists in the pipeline, the feature's status is
|
||||
_enabled_.
|
||||
|
||||
For SAST, click **View history** to see the `.gitlab-ci.yml` file’s history.
|
||||
|
||||
NOTE: **Note:**
|
||||
If the latest pipeline used [Auto DevOps](../../../topics/autodevops/index.md),
|
||||
all security features are configured by default.
|
||||
|
|
|
@ -15,59 +15,45 @@ organize, and visualize a workflow for a feature or product release.
|
|||
It can be used as a [Kanban](https://en.wikipedia.org/wiki/Kanban_(development)) or a
|
||||
[Scrum](https://en.wikipedia.org/wiki/Scrum_(software_development)) board.
|
||||
|
||||
It pairs issue tracking and project management,
|
||||
keeping everything in the same place, so that you don't need to jump
|
||||
between different platforms to organize your workflow.
|
||||
It pairs issue tracking and project management, keeping everything in the same place,
|
||||
so that you don't need to jump between different platforms to organize your workflow.
|
||||
|
||||
With issue boards, you organize your issues in lists that correspond to
|
||||
their assigned labels, visualizing issues designed as cards throughout those lists.
|
||||
Issue boards build on the existing [issue tracking functionality](issues/index.md#issues-list) and
|
||||
[labels](labels.md). Your issues appear as cards in vertical lists, organized by their assigned
|
||||
labels, [milestones](#milestone-lists), or [assignees](#assignee-lists).
|
||||
|
||||
You define your process, and GitLab organizes it for you. You add your labels
|
||||
then create the corresponding list to pull in your existing issues. When
|
||||
you're ready, you can drag and drop your issue cards from one step to the next.
|
||||
Issue boards help you to visualize and manage your entire process in GitLab.
|
||||
You add your labels, and then create the corresponding list for your existing issues.
|
||||
When you're ready, you can drag your issue cards from one step to another one.
|
||||
|
||||
An issue board can show you what issues your team is working on, who is assigned to each,
|
||||
and where in the workflow those issues are.
|
||||
|
||||
To let your team members organize their own workflows, use
|
||||
[multiple issue boards](#use-cases-for-multiple-issue-boards). This allows creating multiple issue
|
||||
boards in the same project.
|
||||
|
||||
![GitLab issue board - Core](img/issue_boards_core.png)
|
||||
|
||||
Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/),
|
||||
as shown in the following table:
|
||||
|
||||
| Tier | Number of project issue boards | Number of [group issue boards](#group-issue-boards) | [Configurable issue boards](#configurable-issue-boards) | [Assignee lists](#assignee-lists) |
|
||||
|------------------|--------------------------------|------------------------------|---------------------------|----------------|
|
||||
| Core / Free | Multiple | 1 | No | No |
|
||||
| Starter / Bronze | Multiple | 1 | Yes | No |
|
||||
| Premium / Silver | Multiple | Multiple | Yes | Yes |
|
||||
| Ultimate / Gold | Multiple | Multiple | Yes | Yes |
|
||||
|
||||
To learn more, visit [GitLab Enterprise features for issue boards](#gitlab-enterprise-features-for-issue-boards) below.
|
||||
|
||||
![GitLab issue board - Premium](img/issue_boards_premium.png)
|
||||
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
Watch a [video presentation](https://youtu.be/UWsJ8tkHAa8) of
|
||||
the Issue Board feature (introduced in GitLab 8.11 - August 2016).
|
||||
Watch a [video presentation](https://youtu.be/vjccjHI7aGI) of
|
||||
the Issue Board feature.
|
||||
|
||||
### Advanced features of issue boards
|
||||
|
||||
- Create multiple issue boards per project.
|
||||
- Create multiple issue boards per group. **(PREMIUM)**
|
||||
- Add lists for [assignees](#assignee-lists) and [milestones](#milestone-lists). **(PREMIUM)**
|
||||
|
||||
Check all the [GitLab Enterprise features for issue boards](#gitlab-enterprise-features-for-issue-boards).
|
||||
|
||||
![GitLab issue boards - Premium](img/issue_boards_premium.png)
|
||||
|
||||
## How it works
|
||||
|
||||
The Issue Board feature builds on GitLab's existing
|
||||
[issue tracking functionality](issues/index.md#issues-list) and
|
||||
[labels](labels.md) by using them as lists of the Scrum board.
|
||||
|
||||
With issue boards you can have a different view of your issues while
|
||||
maintaining the same filtering and sorting abilities you see across the
|
||||
issue tracker. An issue board is based on its project's label structure, so it
|
||||
applies the same descriptive labels to indicate placement on the board, keeping
|
||||
consistency throughout the entire development lifecycle.
|
||||
|
||||
An issue board shows you what issues your team is working on, who is assigned to each,
|
||||
and where in the workflow those issues are.
|
||||
|
||||
You create issues, host code, perform reviews, build, test,
|
||||
and deploy from one single platform. Issue boards help you to visualize
|
||||
and manage the entire process in GitLab.
|
||||
|
||||
With [multiple issue boards](#use-cases-for-multiple-issue-boards),
|
||||
you go even further, as you can not only keep yourself and your project
|
||||
organized from a broader perspective with one issue board per project,
|
||||
but also allow your team members to organize their own workflow by creating
|
||||
multiple issue boards within the same project.
|
||||
|
||||
## Use cases
|
||||
## Issue boards use cases
|
||||
|
||||
You can tailor GitLab issue boards to your own preferred workflow.
|
||||
Here are some common use cases for issue boards.
|
||||
|
@ -138,8 +124,7 @@ to improve their workflow with multiple boards.
|
|||
|
||||
#### Quick assignments
|
||||
|
||||
Create lists for each of your team members and quickly drag and drop issues onto each team member's
|
||||
list.
|
||||
Create lists for each of your team members and quickly drag issues onto each team member's list.
|
||||
|
||||
## Issue board terminology
|
||||
|
||||
|
@ -172,23 +157,37 @@ card includes:
|
|||
Users with the [Reporter and higher roles](../permissions.md) can use all the functionality of the
|
||||
Issue Board feature to create or delete lists and drag issues from one list to another.
|
||||
|
||||
## How GitLab orders issues in a list
|
||||
|
||||
When visiting a board, issues appear ordered in any list. You're able to change
|
||||
that order by dragging the issues. The changed order is saved, so that anybody who visits the same
|
||||
board later sees the reordering, with some exceptions.
|
||||
|
||||
The first time a given issue appears in any board (that is, the first time a user
|
||||
loads a board containing that issue), it is ordered in relation to other issues in that list
|
||||
according to [label priority](labels.md#label-priority).
|
||||
|
||||
At this point, that issue is assigned a relative order value by the system,
|
||||
representing its relative order with respect to the other issues in the list. Any time
|
||||
you reorder that issue by dragging, its relative order value changes accordingly.
|
||||
|
||||
Also, any time that issue appears in any board when it's loaded by a user,
|
||||
the updated relative order value is used for the ordering. It's only the first
|
||||
time an issue appears that it takes from the priority order mentioned above. This means that
|
||||
if issue `A` is reordered by dragging to be above issue `B` by any user in
|
||||
a given board inside your GitLab instance, any time those two issues are subsequently
|
||||
loaded in any board in the same instance (could be a different project board or a different group
|
||||
board, for example), that ordering is maintained.
|
||||
|
||||
This ordering also affects [issue lists](issues/sorting_issue_lists.md).
|
||||
Changing the order in an issue board changes the ordering in an issue list,
|
||||
and vice versa.
|
||||
|
||||
## GitLab Enterprise features for issue boards
|
||||
|
||||
GitLab issue boards are available on GitLab Core and GitLab.com Free tiers, but some
|
||||
advanced functionality is present in [higher tiers only](https://about.gitlab.com/pricing/).
|
||||
|
||||
### Summary of features per tier
|
||||
|
||||
Different issue board features are available in different [GitLab tiers](https://about.gitlab.com/pricing/),
|
||||
as shown in the following table:
|
||||
|
||||
| Tier | Number of Project issue boards | Number of Group issue boards | Configurable issue boards | Assignee lists |
|
||||
|------------------|--------------------------------|------------------------------|---------------------------|----------------|
|
||||
| Core / Free | Multiple | 1 | No | No |
|
||||
| Starter / Bronze | Multiple | 1 | Yes | No |
|
||||
| Premium / Silver | Multiple | Multiple | Yes | Yes |
|
||||
| Ultimate / Gold | Multiple | Multiple | Yes | Yes |
|
||||
|
||||
### Multiple issue boards
|
||||
|
||||
> - [Introduced](https://about.gitlab.com/releases/2016/10/22/gitlab-8-13-released/) in GitLab 8.13.
|
||||
|
@ -248,6 +247,10 @@ clicking **View scope**.
|
|||
|
||||
![Viewing board configuration](img/issue_board_view_scope.png)
|
||||
|
||||
<i class="fa fa-youtube-play youtube" aria-hidden="true"></i>
|
||||
Watch a [video presentation](https://youtu.be/m5UTNCSqaDk) of
|
||||
the Configurable Issue Board feature.
|
||||
|
||||
### Focus mode
|
||||
|
||||
> - [Introduced]((https://about.gitlab.com/releases/2017/04/22/gitlab-9-1-released/#issue-boards-focus-mode-ees-eep)) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.1.
|
||||
|
@ -362,7 +365,6 @@ status.
|
|||
- [Create workflows](#create-workflows).
|
||||
- [Drag issues between lists](#drag-issues-between-lists).
|
||||
- [Multi-select issue cards](#multi-select-issue-cards).
|
||||
- [Re-order issues in lists](#issue-ordering-in-a-list).
|
||||
- Drag and reorder the lists.
|
||||
- Change issue labels (by dragging an issue between lists).
|
||||
- Close an issue (by dragging it to the **Done** list).
|
||||
|
@ -441,8 +443,9 @@ You can filter by author, assignee, milestone, and label.
|
|||
### Create workflows
|
||||
|
||||
By reordering your lists, you can create workflows. As lists in issue boards are
|
||||
based on labels, it works out of the box with your existing issues. So if you've
|
||||
already labeled things with 'Backend' and 'Frontend', the issue appears in
|
||||
based on labels, it works out of the box with your existing issues.
|
||||
|
||||
So if you've already labeled things with **Backend** and **Frontend**, the issue appears in
|
||||
the lists as you create them. In addition, this means you can easily move
|
||||
something between lists by changing a label.
|
||||
|
||||
|
@ -456,20 +459,22 @@ A typical workflow of using an issue board would be:
|
|||
1. You move issues around in lists so that your team knows who should be working
|
||||
on what issue.
|
||||
1. When the work by one team is done, the issue can be dragged to the next list
|
||||
so someone else can pick up.
|
||||
so someone else can pick it up.
|
||||
1. When the issue is finally resolved, the issue is moved to the **Done** list
|
||||
and gets automatically closed.
|
||||
|
||||
For instance you can create a list based on the label of 'Frontend' and one for
|
||||
'Backend'. A designer can start working on an issue by adding it to the
|
||||
'Frontend' list. That way, everyone knows that this issue is now being
|
||||
worked on by the designers. Then, once they're done, all they have to do is
|
||||
drag it over to the next list, 'Backend', where a backend developer can
|
||||
For example, you can create a list based on the label of **Frontend** and one for
|
||||
**Backend**. A designer can start working on an issue by adding it to the
|
||||
**Frontend** list. That way, everyone knows that this issue is now being
|
||||
worked on by the designers.
|
||||
|
||||
Then, once they're done, all they have to do is
|
||||
drag it to the next list, **Backend**, where a backend developer can
|
||||
eventually pick it up. Once they’re done, they move it to **Done**, to close the
|
||||
issue.
|
||||
|
||||
This process can be seen clearly when visiting an issue since with every move
|
||||
to another list the label changes and a system not is recorded.
|
||||
to another list the label changes and a system note is recorded.
|
||||
|
||||
![issue board system notes](img/issue_board_system_notes.png)
|
||||
|
||||
|
@ -497,33 +502,6 @@ To select and move multiple cards:
|
|||
|
||||
![Multi-select Issue Cards](img/issue_boards_multi_select_v12_4.png)
|
||||
|
||||
### Issue ordering in a list
|
||||
|
||||
When visiting a board, issues appear ordered in any list. You're able to change
|
||||
that order by dragging and dropping the issues. The changed order will be saved
|
||||
to the system so that anybody who visits the same board later will see the reordering,
|
||||
with some exceptions.
|
||||
|
||||
The first time a given issue appears in any board (that is, the first time a user
|
||||
loads a board containing that issue), it is ordered with
|
||||
respect to other issues in that list according to [Priority order](labels.md#label-priority).
|
||||
|
||||
At that point, that issue is assigned a relative order value by the system
|
||||
representing its relative order with respect to the other issues in the list. Any time
|
||||
you drag-and-drop reorder that issue, its relative order value changes accordingly.
|
||||
|
||||
Also, any time that issue appears in any board when it's loaded by a user,
|
||||
the updated relative order value is used for the ordering. (It's only the first
|
||||
time an issue appears that it takes from the Priority order mentioned above.) This means that
|
||||
if issue `A` is drag-and-drop reordered to be above issue `B` by any user in
|
||||
a given board inside your GitLab instance, any time those two issues are subsequently
|
||||
loaded in any board in the same instance (could be a different project board or a different group
|
||||
board, for example), that ordering is maintained.
|
||||
|
||||
This ordering also affects [issue lists](issues/sorting_issue_lists.md).
|
||||
Changing the order in an issue board changes the ordering in an issue list,
|
||||
and vice versa.
|
||||
|
||||
## Tips
|
||||
|
||||
A few things to remember:
|
||||
|
@ -537,4 +515,4 @@ A few things to remember:
|
|||
and show only the issues from all lists that have that label.
|
||||
- For performance and visibility reasons, each list shows the first 20 issues
|
||||
by default. If you have more than 20 issues, start scrolling down and the next
|
||||
20 appears.
|
||||
20 appear.
|
||||
|
|
|
@ -1,3 +1,9 @@
|
|||
---
|
||||
stage: Create
|
||||
group: Knowledge
|
||||
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers"
|
||||
---
|
||||
|
||||
# Design Management
|
||||
|
||||
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/660) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2.
|
||||
|
@ -225,6 +231,40 @@ Note that your resolved comment pins will disappear from the Design to free up s
|
|||
However, if you need to revisit or find a resolved discussion, all of your resolved threads will be
|
||||
available in the **Resolved Comment** area at the bottom of the right sidebar.
|
||||
|
||||
## Add To-Do for Designs
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/198439) in GitLab 13.4.
|
||||
> - It's [deployed behind a feature flag](../../feature_flags.md), disabled by default.
|
||||
> - It's disabled on GitLab.com.
|
||||
> - It's not recommended for production use.
|
||||
> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-the-design-to-do-button). **(CORE ONLY)**
|
||||
|
||||
CAUTION: **Warning:**
|
||||
This feature might not be available to you. Check the **version history** note above for details.
|
||||
|
||||
Add a To-Do for a design by clicking **Add a To-Do** on the design sidebar:
|
||||
|
||||
![To-Do button](img/design_todo_button_v13_4.png)
|
||||
|
||||
### Enable or disable the design To-Do button **(CORE ONLY)**
|
||||
|
||||
The design To-Do button is under development and not ready for production use. It is
|
||||
deployed behind a feature flag that is **disabled by default**.
|
||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
|
||||
can enable it.
|
||||
|
||||
To enable it:
|
||||
|
||||
```ruby
|
||||
Feature.enable(:design_management_todo_button)
|
||||
```
|
||||
|
||||
To disable it:
|
||||
|
||||
```ruby
|
||||
Feature.disable(:design_management_todo_button)
|
||||
```
|
||||
|
||||
## Referring to designs in Markdown
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/217160) in **GitLab 13.1**.
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 163 KiB |
|
@ -11,7 +11,7 @@ etc. The available sorting options can change based on the context of the list.
|
|||
For sorting by issue priority, see [Label Priority](../labels.md#label-priority).
|
||||
|
||||
In group and project issue lists, it is also possible to order issues manually,
|
||||
similar to [issue boards](../issue_board.md#issue-ordering-in-a-list).
|
||||
similar to [issue boards](../issue_board.md#how-gitlab-orders-issues-in-a-list).
|
||||
|
||||
## Manual sorting
|
||||
|
||||
|
@ -31,6 +31,6 @@ a given list inside your GitLab instance, any time those two issues are subseque
|
|||
loaded in any list in the same instance (could be a different project issue list or a
|
||||
different group issue list, for example), that ordering will be maintained.
|
||||
|
||||
This ordering also affects [issue boards](../issue_board.md#issue-ordering-in-a-list).
|
||||
This ordering also affects [issue boards](../issue_board.md#how-gitlab-orders-issues-in-a-list).
|
||||
Changing the order in an issue list changes the ordering in an issue board,
|
||||
and vice versa.
|
||||
|
|
|
@ -235,6 +235,7 @@ module API
|
|||
mount ::API::Templates
|
||||
mount ::API::Todos
|
||||
mount ::API::Triggers
|
||||
mount ::API::UsageData
|
||||
mount ::API::UserCounts
|
||||
mount ::API::Users
|
||||
mount ::API::Variables
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module API
|
||||
class UsageData < Grape::API::Instance
|
||||
before { authenticate! }
|
||||
|
||||
namespace 'usage_data' do
|
||||
before do
|
||||
not_found! unless Feature.enabled?(:usage_data_api)
|
||||
end
|
||||
|
||||
desc 'Track usage data events' do
|
||||
detail 'This feature was introduced in GitLab 13.4.'
|
||||
end
|
||||
|
||||
params do
|
||||
requires :event, type: String, desc: 'The event name that should be tracked'
|
||||
end
|
||||
|
||||
post 'increment_unique_users' do
|
||||
event_name = params[:event]
|
||||
|
||||
increment_unique_values(event_name, current_user.id)
|
||||
|
||||
status :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -17,6 +17,14 @@ module Gitlab
|
|||
end
|
||||
|
||||
def run!
|
||||
# If this project is an LFS storage project (e.g. is the root of a fork
|
||||
# network), what it is safe to remove depends on the sum of its forks.
|
||||
# For now, skip cleaning up LFS for this complicated case
|
||||
if project.forks_count > 0 && project.lfs_storage_project == project
|
||||
log_info("Skipping orphan LFS check for #{project.name_with_namespace} as it is a fork root")
|
||||
return
|
||||
end
|
||||
|
||||
log_info("Looking for orphan LFS files for project #{project.name_with_namespace}")
|
||||
|
||||
remove_orphan_references
|
||||
|
|
|
@ -44,7 +44,10 @@ module Gitlab
|
|||
# "+ # Test change",
|
||||
# "- # Old change" ]
|
||||
def changed_lines(changed_file)
|
||||
git.diff_for_file(changed_file).patch.split("\n").select { |line| %r{^[+-]}.match?(line) }
|
||||
diff = git.diff_for_file(changed_file)
|
||||
return [] unless diff
|
||||
|
||||
diff.patch.split("\n").select { |line| %r{^[+-]}.match?(line) }
|
||||
end
|
||||
|
||||
def all_ee_changes
|
||||
|
|
|
@ -32,7 +32,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def self.build
|
||||
Gitlab::SafeRequestStore[self.cache_key] ||= new(self.full_log_path)
|
||||
Gitlab::SafeRequestStore[self.cache_key] ||=
|
||||
new(self.full_log_path, level: ::Logger::DEBUG)
|
||||
end
|
||||
|
||||
def self.full_log_path
|
||||
|
|
|
@ -91,6 +91,7 @@ module Gitlab
|
|||
params '%"milestone"'
|
||||
types Issue, MergeRequest
|
||||
condition do
|
||||
quick_action_target.supports_milestone? &&
|
||||
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project) &&
|
||||
find_milestones(project, state: 'active').any?
|
||||
end
|
||||
|
@ -113,6 +114,7 @@ module Gitlab
|
|||
condition do
|
||||
quick_action_target.persisted? &&
|
||||
quick_action_target.milestone_id? &&
|
||||
quick_action_target.supports_milestone? &&
|
||||
current_user.can?(:"admin_#{quick_action_target.to_ability_name}", project)
|
||||
end
|
||||
command :remove_milestone do
|
||||
|
|
|
@ -872,6 +872,9 @@ msgstr ""
|
|||
msgid "'%{name}' Value Stream created"
|
||||
msgstr ""
|
||||
|
||||
msgid "'%{name}' Value Stream deleted"
|
||||
msgstr ""
|
||||
|
||||
msgid "'%{name}' stage already exists"
|
||||
msgstr ""
|
||||
|
||||
|
@ -3250,6 +3253,9 @@ msgstr ""
|
|||
msgid "Are you sure you want to close this blocked issue?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to delete \"%{name}\" Value Stream?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to delete %{name}?"
|
||||
msgstr ""
|
||||
|
||||
|
@ -8089,12 +8095,18 @@ msgstr ""
|
|||
msgid "Delete"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete %{name}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Comment"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Snippet"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete Value Stream"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete account"
|
||||
msgstr ""
|
||||
|
||||
|
@ -22112,6 +22124,9 @@ msgstr ""
|
|||
msgid "SecurityConfiguration|Using custom settings. You won't receive automatic updates on this variable. %{anchorStart}Restore to default%{anchorEnd}"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityConfiguration|View history"
|
||||
msgstr ""
|
||||
|
||||
msgid "SecurityConfiguration|You can quickly enable all security scanning tools by enabling %{linkStart}Auto DevOps%{linkEnd}."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
"glob": "^7.1.6",
|
||||
"graphql": "^14.7.0",
|
||||
"graphql-tag": "^2.10.1",
|
||||
"gray-matter": "^4.0.2",
|
||||
"immer": "^7.0.7",
|
||||
"imports-loader": "^0.8.0",
|
||||
"ipaddr.js": "^1.9.1",
|
||||
|
|
|
@ -3,7 +3,15 @@
|
|||
FactoryBot.define do
|
||||
factory :instance_statistics_measurement, class: 'Analytics::InstanceStatistics::Measurement' do
|
||||
recorded_at { Time.now }
|
||||
identifier { Analytics::InstanceStatistics::Measurement.identifiers[:projects] }
|
||||
identifier { :projects }
|
||||
count { 1_000 }
|
||||
|
||||
trait :project_count do
|
||||
identifier { :projects }
|
||||
end
|
||||
|
||||
trait :group_count do
|
||||
identifier { :groups }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,12 +12,38 @@ RSpec.describe 'User accepts a merge request', :js, :sidekiq_might_not_need_inli
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
it 'presents merged merge request content' do
|
||||
visit(merge_request_path(merge_request))
|
||||
context 'presents merged merge request content' do
|
||||
it 'when merge method is set to merge commit' do
|
||||
visit(merge_request_path(merge_request))
|
||||
|
||||
click_button('Merge')
|
||||
click_button('Merge')
|
||||
|
||||
expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merge_commit_sha}")
|
||||
expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
|
||||
end
|
||||
|
||||
context 'when merge method is set to fast-forward merge' do
|
||||
let(:project) { create(:project, :public, :repository, merge_requests_ff_only_enabled: true) }
|
||||
|
||||
it 'accepts a merge request with rebase and merge' do
|
||||
merge_request = create(:merge_request, :rebased, source_project: project)
|
||||
|
||||
visit(merge_request_path(merge_request))
|
||||
|
||||
click_button('Merge')
|
||||
|
||||
expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
|
||||
end
|
||||
|
||||
it 'accepts a merge request with squash and merge' do
|
||||
merge_request = create(:merge_request, :rebased, source_project: project, squash: true)
|
||||
|
||||
visit(merge_request_path(merge_request))
|
||||
|
||||
click_button('Merge')
|
||||
|
||||
expect(page).to have_content("The changes were merged into #{merge_request.target_branch} with #{merge_request.short_merged_commit_sha}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with removing the source branch' do
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
"title": { "type": "string" },
|
||||
"auto_merge_enabled": { "type": "boolean" },
|
||||
"state": { "type": "string" },
|
||||
"merge_commit_sha": { "type": ["string", "null"] },
|
||||
"short_merge_commit_sha": { "type": ["string", "null"] },
|
||||
"merged_commit_sha": { "type": ["string", "null"] },
|
||||
"short_merged_commit_sha": { "type": ["string", "null"] },
|
||||
"merge_error": { "type": ["string", "null"] },
|
||||
"merge_status": { "type": "string" },
|
||||
"merge_user_id": { "type": ["integer", "null"] },
|
||||
|
@ -40,7 +40,7 @@
|
|||
"diverged_commits_count": { "type": "integer" },
|
||||
"target_branch_commits_path": { "type": "string" },
|
||||
"target_branch_tree_path": { "type": "string" },
|
||||
"merge_commit_path": { "type": ["string", "null"] },
|
||||
"merged_commit_path": { "type": ["string", "null"] },
|
||||
"source_branch_with_namespace_link": { "type": "string" },
|
||||
"source_branch_path": { "type": "string" }
|
||||
}
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlBanner } from '@gitlab/ui';
|
||||
import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
|
||||
import { setCookie, parseBoolean } from '~/lib/utils/common_utils';
|
||||
|
||||
const expectedTitle = 'Collaborate with your team';
|
||||
const expectedBody =
|
||||
jest.mock('~/lib/utils/common_utils');
|
||||
|
||||
const isDismissedKey = 'invite_99_1';
|
||||
const title = 'Collaborate with your team';
|
||||
const body =
|
||||
"We noticed that you haven't invited anyone to this group. Invite your colleagues so you can discuss issues, collaborate on merge requests, and share your knowledge";
|
||||
const expectedSvgPath = '/illustrations/background';
|
||||
const expectedInviteMembersPath = 'groups/members';
|
||||
const expectedButtonText = 'Invite your colleagues';
|
||||
const svgPath = '/illustrations/background';
|
||||
const inviteMembersPath = 'groups/members';
|
||||
const buttonText = 'Invite your colleagues';
|
||||
|
||||
const createComponent = (stubs = {}) => {
|
||||
return shallowMount(InviteMembersBanner, {
|
||||
provide: {
|
||||
svgPath: expectedSvgPath,
|
||||
inviteMembersPath: expectedInviteMembersPath,
|
||||
svgPath,
|
||||
inviteMembersPath,
|
||||
isDismissedKey,
|
||||
},
|
||||
stubs,
|
||||
});
|
||||
|
@ -37,23 +42,23 @@ describe('InviteMembersBanner', () => {
|
|||
});
|
||||
|
||||
it('uses the svgPath for the banner svgpath', () => {
|
||||
expect(findBanner().attributes('svgpath')).toBe(expectedSvgPath);
|
||||
expect(findBanner().attributes('svgpath')).toBe(svgPath);
|
||||
});
|
||||
|
||||
it('uses the title from options for title', () => {
|
||||
expect(findBanner().attributes('title')).toBe(expectedTitle);
|
||||
expect(findBanner().attributes('title')).toBe(title);
|
||||
});
|
||||
|
||||
it('includes the body text from options', () => {
|
||||
expect(findBanner().html()).toContain(expectedBody);
|
||||
expect(findBanner().html()).toContain(body);
|
||||
});
|
||||
|
||||
it('uses the button_text text from options for buttontext', () => {
|
||||
expect(findBanner().attributes('buttontext')).toBe(expectedButtonText);
|
||||
expect(findBanner().attributes('buttontext')).toBe(buttonText);
|
||||
});
|
||||
|
||||
it('uses the href from inviteMembersPath for buttonlink', () => {
|
||||
expect(findBanner().attributes('buttonlink')).toBe(expectedInviteMembersPath);
|
||||
expect(findBanner().attributes('buttonlink')).toBe(inviteMembersPath);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -61,16 +66,35 @@ describe('InviteMembersBanner', () => {
|
|||
const findButton = () => {
|
||||
return wrapper.find('button');
|
||||
};
|
||||
const stubs = {
|
||||
GlBanner,
|
||||
};
|
||||
|
||||
it('sets visible to false', () => {
|
||||
wrapper = createComponent(stubs);
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ GlBanner });
|
||||
|
||||
findButton().trigger('click');
|
||||
});
|
||||
|
||||
expect(wrapper.vm.visible).toBe(false);
|
||||
it('sets iDismissed to true', () => {
|
||||
expect(wrapper.vm.isDismissed).toBe(true);
|
||||
});
|
||||
|
||||
it('sets the cookie with the isDismissedKey', () => {
|
||||
expect(setCookie).toHaveBeenCalledWith(isDismissedKey, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a dismiss cookie exists', () => {
|
||||
beforeEach(() => {
|
||||
parseBoolean.mockReturnValue(true);
|
||||
|
||||
wrapper = createComponent({ GlBanner });
|
||||
});
|
||||
|
||||
it('sets isDismissed to true', () => {
|
||||
expect(wrapper.vm.isDismissed).toBe(true);
|
||||
});
|
||||
|
||||
it('does not render the banner', () => {
|
||||
expect(wrapper.contains(GlBanner)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -81,7 +81,7 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
|
|||
|
||||
it('updates parsedSource with new content', () => {
|
||||
const newContent = 'New content';
|
||||
const spySyncParsedSource = jest.spyOn(wrapper.vm.parsedSource, 'sync');
|
||||
const spySyncParsedSource = jest.spyOn(wrapper.vm.parsedSource, 'syncContent');
|
||||
|
||||
findRichContentEditor().vm.$emit('input', newContent);
|
||||
|
||||
|
|
|
@ -1,31 +1,22 @@
|
|||
export const sourceContentHeaderYAML = `---
|
||||
layout: handbook-page-toc
|
||||
title: Handbook
|
||||
twitter_image: '/images/tweets/handbook-gitlab.png'
|
||||
twitter_image: /images/tweets/handbook-gitlab.png
|
||||
---`;
|
||||
export const sourceContentHeaderTOML = `+++
|
||||
layout: "handbook-page-toc"
|
||||
title: "Handbook"
|
||||
twitter_image: "/images/tweets/handbook-gitlab.png"
|
||||
+++`;
|
||||
export const sourceContentHeaderJSON = `{
|
||||
"layout": "handbook-page-toc",
|
||||
"title": "Handbook",
|
||||
"twitter_image": "/images/tweets/handbook-gitlab.png",
|
||||
}`;
|
||||
export const sourceContentSpacing = `
|
||||
`;
|
||||
export const sourceContentHeaderObjYAML = {
|
||||
layout: 'handbook-page-toc',
|
||||
title: 'Handbook',
|
||||
twitter_image: '/images/tweets/handbook-gitlab.png',
|
||||
};
|
||||
export const sourceContentSpacing = `\n`;
|
||||
export const sourceContentBody = `## On this page
|
||||
{:.no_toc .hidden-md .hidden-lg}
|
||||
|
||||
- TOC
|
||||
{:toc .hidden-md .hidden-lg}
|
||||
|
||||
![image](path/to/image1.png)
|
||||
`;
|
||||
![image](path/to/image1.png)`;
|
||||
export const sourceContentYAML = `${sourceContentHeaderYAML}${sourceContentSpacing}${sourceContentBody}`;
|
||||
export const sourceContentTOML = `${sourceContentHeaderTOML}${sourceContentSpacing}${sourceContentBody}`;
|
||||
export const sourceContentJSON = `${sourceContentHeaderJSON}${sourceContentSpacing}${sourceContentBody}`;
|
||||
export const sourceContentTitle = 'Handbook';
|
||||
|
||||
export const username = 'gitlabuser';
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import getFrontMatterLanguageDefinition from '~/static_site_editor/services/parse_source_file_language_support';
|
||||
|
||||
describe('static_site_editor/services/parse_source_file_language_support', () => {
|
||||
describe('getFrontMatterLanguageDefinition', () => {
|
||||
it.each`
|
||||
languageName
|
||||
${'yaml'}
|
||||
${'toml'}
|
||||
${'json'}
|
||||
${'abcd'}
|
||||
`('returns $hasMatch when provided $languageName', ({ languageName }) => {
|
||||
try {
|
||||
const definition = getFrontMatterLanguageDefinition(languageName);
|
||||
expect(definition.name).toBe(languageName);
|
||||
} catch (error) {
|
||||
expect(error.message).toBe(`Unsupported front matter language: ${languageName}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,10 +1,7 @@
|
|||
import {
|
||||
sourceContentYAML as content,
|
||||
sourceContentTOML as tomlContent,
|
||||
sourceContentJSON as jsonContent,
|
||||
sourceContentHeaderYAML as yamlFrontMatter,
|
||||
sourceContentHeaderTOML as tomlFrontMatter,
|
||||
sourceContentHeaderJSON as jsonFrontMatter,
|
||||
sourceContentHeaderObjYAML as yamlFrontMatterObj,
|
||||
sourceContentBody as body,
|
||||
} from '../mock_data';
|
||||
|
||||
|
@ -18,20 +15,15 @@ describe('static_site_editor/services/parse_source_file', () => {
|
|||
const newContentComplex = `${contentComplex} ${edit}`;
|
||||
|
||||
describe('unmodified front matter', () => {
|
||||
const yamlOptions = { frontMatterLanguage: 'yaml' };
|
||||
|
||||
it.each`
|
||||
parsedSource | targetFrontMatter
|
||||
${parseSourceFile(content)} | ${yamlFrontMatter}
|
||||
${parseSourceFile(contentComplex)} | ${yamlFrontMatter}
|
||||
${parseSourceFile(content, yamlOptions)} | ${yamlFrontMatter}
|
||||
${parseSourceFile(contentComplex, yamlOptions)} | ${yamlFrontMatter}
|
||||
${parseSourceFile(tomlContent, { frontMatterLanguage: 'toml' })} | ${tomlFrontMatter}
|
||||
${parseSourceFile(jsonContent, { frontMatterLanguage: 'json' })} | ${jsonFrontMatter}
|
||||
parsedSource | targetFrontMatter
|
||||
${parseSourceFile(content)} | ${yamlFrontMatter}
|
||||
${parseSourceFile(contentComplex)} | ${yamlFrontMatter}
|
||||
`(
|
||||
'returns $targetFrontMatter when frontMatter queried',
|
||||
({ parsedSource, targetFrontMatter }) => {
|
||||
expect(parsedSource.frontMatter()).toBe(targetFrontMatter);
|
||||
expect(targetFrontMatter).toContain(parsedSource.matter());
|
||||
expect(parsedSource.matterObject()).toEqual(yamlFrontMatterObj);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
@ -63,6 +55,7 @@ describe('static_site_editor/services/parse_source_file', () => {
|
|||
|
||||
describe('modified front matter', () => {
|
||||
const newYamlFrontMatter = '---\nnewKey: newVal\n---';
|
||||
const newYamlFrontMatterObj = { newKey: 'newVal' };
|
||||
const contentWithNewFrontMatter = content.replace(yamlFrontMatter, newYamlFrontMatter);
|
||||
const contentComplexWithNewFrontMatter = contentComplex.replace(
|
||||
yamlFrontMatter,
|
||||
|
@ -76,11 +69,12 @@ describe('static_site_editor/services/parse_source_file', () => {
|
|||
`(
|
||||
'returns the correct front matter and modified content',
|
||||
({ parsedSource, targetContent }) => {
|
||||
expect(parsedSource.frontMatter()).toBe(yamlFrontMatter);
|
||||
expect(yamlFrontMatter).toContain(parsedSource.matter());
|
||||
|
||||
parsedSource.setFrontMatter(newYamlFrontMatter);
|
||||
parsedSource.syncMatter(newYamlFrontMatter);
|
||||
|
||||
expect(parsedSource.frontMatter()).toBe(newYamlFrontMatter);
|
||||
expect(parsedSource.matter()).toBe(newYamlFrontMatter);
|
||||
expect(parsedSource.matterObject()).toEqual(newYamlFrontMatterObj);
|
||||
expect(parsedSource.content()).toBe(targetContent);
|
||||
},
|
||||
);
|
||||
|
@ -99,7 +93,7 @@ describe('static_site_editor/services/parse_source_file', () => {
|
|||
`(
|
||||
'returns $isModified after a $targetRaw sync',
|
||||
({ parsedSource, isModified, targetRaw, targetBody }) => {
|
||||
parsedSource.sync(targetRaw);
|
||||
parsedSource.syncContent(targetRaw);
|
||||
|
||||
expect(parsedSource.isModified()).toBe(isModified);
|
||||
expect(parsedSource.content()).toBe(targetRaw);
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Resolvers::Admin::Analytics::InstanceStatistics::MeasurementsResolver do
|
||||
include GraphqlHelpers
|
||||
|
||||
describe '#resolve' do
|
||||
let_it_be(:user) { create(:user) }
|
||||
let_it_be(:admin_user) { create(:user, :admin) }
|
||||
|
||||
let_it_be(:project_measurement_new) { create(:instance_statistics_measurement, :project_count, recorded_at: 2.days.ago) }
|
||||
let_it_be(:project_measurement_old) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago) }
|
||||
|
||||
subject { resolve_measurements({ identifier: 'projects' }, { current_user: current_user }) }
|
||||
|
||||
context 'when requesting project count measurements' do
|
||||
context 'as an admin user' do
|
||||
let(:current_user) { admin_user }
|
||||
|
||||
it 'returns the records, latest first' do
|
||||
expect(subject).to eq([project_measurement_new, project_measurement_old])
|
||||
end
|
||||
end
|
||||
|
||||
context 'as a non-admin user' do
|
||||
let(:current_user) { user }
|
||||
|
||||
it 'raises ResourceNotAvailable error' do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
end
|
||||
|
||||
context 'as an unauthenticated user' do
|
||||
let(:current_user) { nil }
|
||||
|
||||
it 'raises ResourceNotAvailable error' do
|
||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_measurements(args = {}, context = {})
|
||||
resolve(described_class, args: args, ctx: context)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,13 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['MeasurementIdentifier'] do
|
||||
specify { expect(described_class.graphql_name).to eq('MeasurementIdentifier') }
|
||||
|
||||
it 'exposes all the existing identifier values' do
|
||||
identifiers = Analytics::InstanceStatistics::Measurement.identifiers.keys.map(&:upcase)
|
||||
|
||||
expect(described_class.values.keys).to match_array(identifiers)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe GitlabSchema.types['InstanceStatisticsMeasurement'] do
|
||||
subject { described_class }
|
||||
|
||||
it { is_expected.to have_graphql_field(:recorded_at) }
|
||||
it { is_expected.to have_graphql_field(:identifier) }
|
||||
it { is_expected.to have_graphql_field(:count) }
|
||||
end
|
|
@ -21,6 +21,7 @@ RSpec.describe GitlabSchema.types['Query'] do
|
|||
user
|
||||
users
|
||||
issue
|
||||
instance_statistics_measurements
|
||||
]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields).at_least
|
||||
|
@ -62,4 +63,12 @@ RSpec.describe GitlabSchema.types['Query'] do
|
|||
is_expected.to have_graphql_type(Types::IssueType)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'instance_statistics_measurements field' do
|
||||
subject { described_class.fields['instanceStatisticsMeasurements'] }
|
||||
|
||||
it 'returns issue' do
|
||||
is_expected.to have_graphql_type(Types::Admin::Analytics::InstanceStatistics::MeasurementType.connection_type)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Gitlab::Cleanup::OrphanLfsFileReferences do
|
||||
include ProjectForksHelper
|
||||
|
||||
let(:null_logger) { Logger.new('/dev/null') }
|
||||
let(:project) { create(:project, :repository, lfs_enabled: true) }
|
||||
let(:lfs_object) { create(:lfs_object) }
|
||||
|
@ -85,4 +87,42 @@ RSpec.describe Gitlab::Cleanup::OrphanLfsFileReferences do
|
|||
.to receive(:get_all_lfs_pointers)
|
||||
.and_return(oids.map { |oid| OpenStruct.new(lfs_oid: oid) })
|
||||
end
|
||||
|
||||
context 'LFS for forked projects' do
|
||||
let!(:fork_root) { create(:project, :repository, lfs_enabled: true) }
|
||||
let!(:fork_internal) { fork_project(fork_root, nil, repository: true) }
|
||||
let!(:fork_leaf) { fork_project(fork_internal, nil, repository: true) }
|
||||
|
||||
let(:dry_run) { true }
|
||||
|
||||
context 'root node' do
|
||||
let(:project) { fork_root }
|
||||
|
||||
it 'skips cleanup' do
|
||||
expect(service).not_to receive(:remove_orphan_references)
|
||||
|
||||
service.run!
|
||||
end
|
||||
end
|
||||
|
||||
context 'internal node' do
|
||||
let(:project) { fork_internal }
|
||||
|
||||
it 'runs cleanup' do
|
||||
expect(service).to receive(:remove_orphan_references)
|
||||
|
||||
service.run!
|
||||
end
|
||||
end
|
||||
|
||||
context 'leaf node' do
|
||||
let(:project) { fork_leaf }
|
||||
|
||||
it 'runs cleanup' do
|
||||
expect(service).to receive(:remove_orphan_references)
|
||||
|
||||
service.run!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -76,6 +76,30 @@ RSpec.describe Gitlab::Danger::Helper do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#changed_lines' do
|
||||
subject { helper.changed_lines('changed_file.rb') }
|
||||
|
||||
before do
|
||||
allow(fake_git).to receive(:diff_for_file).with('changed_file.rb').and_return(diff)
|
||||
end
|
||||
|
||||
context 'when file has diff' do
|
||||
let(:diff) { double(:diff, patch: "+ # New change here\n+ # New change there") }
|
||||
|
||||
it 'returns file changes' do
|
||||
is_expected.to eq(['+ # New change here', '+ # New change there'])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when file has no diff (renamed without changes)' do
|
||||
let(:diff) { nil }
|
||||
|
||||
it 'returns a blank array' do
|
||||
is_expected.to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "changed_files" do
|
||||
it 'returns list of changed files matching given regex' do
|
||||
expect(helper).to receive(:all_changed_files).and_return(%w[migration.rb usage_data.rb])
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
require Rails.root.join('db', 'post_migrate', '20200907124300_complete_namespace_settings_migration.rb')
|
||||
|
||||
RSpec.describe CompleteNamespaceSettingsMigration, :redis do
|
||||
let(:migration) { spy('migration') }
|
||||
|
||||
context 'when still legacy artifacts exist' do
|
||||
let(:namespaces) { table(:namespaces) }
|
||||
let(:namespace_settings) { table(:namespace_settings) }
|
||||
let!(:namespace) { namespaces.create!(name: 'gitlab', path: 'gitlab-org') }
|
||||
|
||||
it 'steals sidekiq jobs from BackfillNamespaceSettings background migration' do
|
||||
expect(Gitlab::BackgroundMigration).to receive(:steal).with('BackfillNamespaceSettings')
|
||||
|
||||
migrate!
|
||||
end
|
||||
|
||||
it 'migrates namespaces without namespace_settings' do
|
||||
expect { migrate! }.to change { namespace_settings.count }.from(0).to(1)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,4 +11,35 @@ RSpec.describe Analytics::InstanceStatistics::Measurement, type: :model do
|
|||
it { is_expected.to validate_presence_of(:count) }
|
||||
it { is_expected.to validate_uniqueness_of(:recorded_at).scoped_to(:identifier) }
|
||||
end
|
||||
|
||||
describe 'identifiers enum' do
|
||||
it 'maps to the correct values' do
|
||||
expect(described_class.identifiers).to eq({
|
||||
projects: 1,
|
||||
users: 2,
|
||||
issues: 3,
|
||||
merge_requests: 4,
|
||||
groups: 5,
|
||||
pipelines: 6
|
||||
}.with_indifferent_access)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'scopes' do
|
||||
let_it_be(:measurement_1) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago) }
|
||||
let_it_be(:measurement_2) { create(:instance_statistics_measurement, :project_count, recorded_at: 2.days.ago) }
|
||||
let_it_be(:measurement_3) { create(:instance_statistics_measurement, :group_count, recorded_at: 5.days.ago) }
|
||||
|
||||
describe '.order_by_latest' do
|
||||
subject { described_class.order_by_latest }
|
||||
|
||||
it { is_expected.to eq([measurement_2, measurement_3, measurement_1]) }
|
||||
end
|
||||
|
||||
describe '.with_identifier' do
|
||||
subject { described_class.with_identifier(:projects) }
|
||||
|
||||
it { is_expected.to match_array([measurement_1, measurement_2]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -30,49 +30,51 @@ RSpec.describe ApplicationRecord do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.safe_find_or_create_by' do
|
||||
it 'creates the user avoiding race conditions' do
|
||||
expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
|
||||
allow(Suggestion).to receive(:find_or_create_by).and_call_original
|
||||
context 'safe find or create methods' do
|
||||
let_it_be(:note) { create(:diff_note_on_merge_request) }
|
||||
|
||||
expect { Suggestion.safe_find_or_create_by(build(:suggestion).attributes) }
|
||||
.to change { Suggestion.count }.by(1)
|
||||
let(:suggestion_attributes) { attributes_for(:suggestion).merge!(note_id: note.id) }
|
||||
|
||||
describe '.safe_find_or_create_by' do
|
||||
it 'creates the suggestion avoiding race conditions' do
|
||||
expect(Suggestion).to receive(:find_or_create_by).and_raise(ActiveRecord::RecordNotUnique)
|
||||
allow(Suggestion).to receive(:find_or_create_by).and_call_original
|
||||
|
||||
expect { Suggestion.safe_find_or_create_by(suggestion_attributes) }
|
||||
.to change { Suggestion.count }.by(1)
|
||||
end
|
||||
|
||||
it 'passes a block to find_or_create_by' do
|
||||
expect do |block|
|
||||
Suggestion.safe_find_or_create_by(suggestion_attributes, &block)
|
||||
end.to yield_with_args(an_object_having_attributes(suggestion_attributes))
|
||||
end
|
||||
|
||||
it 'does not create a record when is not valid' do
|
||||
raw_usage_data = RawUsageData.safe_find_or_create_by({ recorded_at: nil })
|
||||
|
||||
expect(raw_usage_data.id).to be_nil
|
||||
expect(raw_usage_data).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
it 'passes a block to find_or_create_by' do
|
||||
attributes = build(:suggestion).attributes
|
||||
describe '.safe_find_or_create_by!' do
|
||||
it 'creates a record using safe_find_or_create_by' do
|
||||
expect(Suggestion).to receive(:find_or_create_by).and_call_original
|
||||
|
||||
expect do |block|
|
||||
Suggestion.safe_find_or_create_by(attributes, &block)
|
||||
end.to yield_with_args(an_object_having_attributes(attributes))
|
||||
end
|
||||
expect(Suggestion.safe_find_or_create_by!(suggestion_attributes))
|
||||
.to be_a(Suggestion)
|
||||
end
|
||||
|
||||
it 'does not create a record when is not valid' do
|
||||
raw_usage_data = RawUsageData.safe_find_or_create_by({ recorded_at: nil })
|
||||
it 'raises a validation error if the record was not persisted' do
|
||||
expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
|
||||
expect(raw_usage_data.id).to be_nil
|
||||
expect(raw_usage_data).not_to be_valid
|
||||
end
|
||||
end
|
||||
|
||||
describe '.safe_find_or_create_by!' do
|
||||
it 'creates a record using safe_find_or_create_by' do
|
||||
expect(Suggestion).to receive(:find_or_create_by).and_call_original
|
||||
|
||||
expect(Suggestion.safe_find_or_create_by!(build(:suggestion).attributes))
|
||||
.to be_a(Suggestion)
|
||||
end
|
||||
|
||||
it 'raises a validation error if the record was not persisted' do
|
||||
expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid)
|
||||
end
|
||||
|
||||
it 'passes a block to find_or_create_by' do
|
||||
attributes = build(:suggestion).attributes
|
||||
|
||||
expect do |block|
|
||||
Suggestion.safe_find_or_create_by!(attributes, &block)
|
||||
end.to yield_with_args(an_object_having_attributes(attributes))
|
||||
it 'passes a block to find_or_create_by' do
|
||||
expect do |block|
|
||||
Suggestion.safe_find_or_create_by!(suggestion_attributes, &block)
|
||||
end.to yield_with_args(an_object_having_attributes(suggestion_attributes))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2160,6 +2160,60 @@ RSpec.describe MergeRequest, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#merged_commit_sha' do
|
||||
it 'returns nil when not merged' do
|
||||
expect(subject.merged_commit_sha).to be_nil
|
||||
end
|
||||
|
||||
context 'when the MR is merged' do
|
||||
let(:sha) { 'f7ce827c314c9340b075657fd61c789fb01cf74d' }
|
||||
|
||||
before do
|
||||
subject.mark_as_merged!
|
||||
end
|
||||
|
||||
it 'returns merge_commit_sha when there is a merge_commit_sha' do
|
||||
subject.update_attribute(:merge_commit_sha, sha)
|
||||
|
||||
expect(subject.merged_commit_sha).to eq(sha)
|
||||
end
|
||||
|
||||
it 'returns squash_commit_sha when there is a squash_commit_sha' do
|
||||
subject.update_attribute(:squash_commit_sha, sha)
|
||||
|
||||
expect(subject.merged_commit_sha).to eq(sha)
|
||||
end
|
||||
|
||||
it 'returns diff_head_sha when there are no merge_commit_sha and squash_commit_sha' do
|
||||
allow(subject).to receive(:diff_head_sha).and_return(sha)
|
||||
|
||||
expect(subject.merged_commit_sha).to eq(sha)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#short_merged_commit_sha' do
|
||||
context 'when merged_commit_sha is nil' do
|
||||
before do
|
||||
allow(subject).to receive(:merged_commit_sha).and_return(nil)
|
||||
end
|
||||
|
||||
it 'returns nil' do
|
||||
expect(subject.short_merged_commit_sha).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when merged_commit_sha is present' do
|
||||
before do
|
||||
allow(subject).to receive(:merged_commit_sha).and_return('f7ce827c314c9340b075657fd61c789fb01cf74d')
|
||||
end
|
||||
|
||||
it 'returns shortened merged_commit_sha' do
|
||||
expect(subject.short_merged_commit_sha).to eq('f7ce827c')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#can_be_reverted?' do
|
||||
subject { create(:merge_request, source_project: create(:project, :repository)) }
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'InstanceStatisticsMeasurements' do
|
||||
include GraphqlHelpers
|
||||
|
||||
let(:current_user) { create(:user, :admin) }
|
||||
let!(:instance_statistics_measurement_1) { create(:instance_statistics_measurement, :project_count, recorded_at: 20.days.ago, count: 5) }
|
||||
let!(:instance_statistics_measurement_2) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago, count: 10) }
|
||||
|
||||
let(:query) { graphql_query_for(:instanceStatisticsMeasurements, 'identifier: PROJECTS', 'nodes { count }') }
|
||||
|
||||
before do
|
||||
post_graphql(query, current_user: current_user)
|
||||
end
|
||||
|
||||
it 'returns measurement objects' do
|
||||
expect(graphql_data.dig('instanceStatisticsMeasurements', 'nodes')).to eq([{ "count" => 10 }, { "count" => 5 }])
|
||||
end
|
||||
end
|
|
@ -0,0 +1,67 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe API::UsageData do
|
||||
let_it_be(:user) { create(:user) }
|
||||
|
||||
describe 'POST /usage_data/increment_unique_users' do
|
||||
let(:endpoint) { '/usage_data/increment_unique_users' }
|
||||
let(:known_event) { 'g_compliance_dashboard' }
|
||||
let(:unknown_event) { 'unknown' }
|
||||
|
||||
context 'usage_data_api feature not enabled' do
|
||||
it 'returns not_found' do
|
||||
stub_feature_flags(usage_data_api: false)
|
||||
|
||||
post api(endpoint, user), params: { event: known_event }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
|
||||
context 'without authentication' do
|
||||
it 'returns 401 response' do
|
||||
post api(endpoint), params: { event: known_event }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with authentication' do
|
||||
before do
|
||||
stub_feature_flags(usage_data_api: true)
|
||||
stub_feature_flags("usage_data_#{known_event}" => true)
|
||||
stub_application_setting(usage_ping_enabled: true)
|
||||
end
|
||||
|
||||
context 'when event is missing from params' do
|
||||
it 'returns bad request' do
|
||||
post api(endpoint, user), params: {}
|
||||
|
||||
expect(response).to have_gitlab_http_status(:bad_request)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with correct params' do
|
||||
it 'returns status ok' do
|
||||
expect(Gitlab::Redis::HLL).to receive(:add)
|
||||
|
||||
post api(endpoint, user), params: { event: known_event }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with unknown event' do
|
||||
it 'returns status ok' do
|
||||
expect(Gitlab::Redis::HLL).not_to receive(:add)
|
||||
|
||||
post api(endpoint, user), params: { event: unknown_event }
|
||||
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -30,10 +30,9 @@ RSpec.describe Notes::QuickActionsService do
|
|||
end
|
||||
|
||||
it 'closes noteable, sets labels, assigns, and sets milestone to noteable, and leave no note' do
|
||||
content, update_params = service.execute(note)
|
||||
service.apply_updates(update_params, note)
|
||||
content = execute(note)
|
||||
|
||||
expect(content).to eq ''
|
||||
expect(content).to be_empty
|
||||
expect(note.noteable).to be_closed
|
||||
expect(note.noteable.labels).to match_array(labels)
|
||||
expect(note.noteable.assignees).to eq([assignee])
|
||||
|
@ -54,19 +53,13 @@ RSpec.describe Notes::QuickActionsService do
|
|||
end
|
||||
|
||||
it 'does not create issue relation' do
|
||||
expect do
|
||||
_, update_params = service.execute(note)
|
||||
service.apply_updates(update_params, note)
|
||||
end.not_to change { IssueLink.count }
|
||||
expect { execute(note) }.not_to change { IssueLink.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'user is allowed to relate issues' do
|
||||
it 'creates issue relation' do
|
||||
expect do
|
||||
_, update_params = service.execute(note)
|
||||
service.apply_updates(update_params, note)
|
||||
end.to change { IssueLink.count }.by(1)
|
||||
expect { execute(note) }.to change { IssueLink.count }.by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -79,10 +72,9 @@ RSpec.describe Notes::QuickActionsService do
|
|||
let(:note_text) { '/reopen' }
|
||||
|
||||
it 'opens the noteable, and leave no note' do
|
||||
content, update_params = service.execute(note)
|
||||
service.apply_updates(update_params, note)
|
||||
content = execute(note)
|
||||
|
||||
expect(content).to eq ''
|
||||
expect(content).to be_empty
|
||||
expect(note.noteable).to be_open
|
||||
end
|
||||
end
|
||||
|
@ -92,10 +84,9 @@ RSpec.describe Notes::QuickActionsService do
|
|||
let(:note_text) { '/spend 1h' }
|
||||
|
||||
it 'adds time to noteable, adds timelog with nil note_id and has no content' do
|
||||
content, update_params = service.execute(note)
|
||||
service.apply_updates(update_params, note)
|
||||
content = execute(note)
|
||||
|
||||
expect(content).to eq ''
|
||||
expect(content).to be_empty
|
||||
expect(note.noteable.time_spent).to eq(3600)
|
||||
expect(Timelog.last.note_id).to be_nil
|
||||
end
|
||||
|
@ -122,8 +113,7 @@ RSpec.describe Notes::QuickActionsService do
|
|||
end
|
||||
|
||||
it 'closes noteable, sets labels, assigns, and sets milestone to noteable' do
|
||||
content, update_params = service.execute(note)
|
||||
service.apply_updates(update_params, note)
|
||||
content = execute(note)
|
||||
|
||||
expect(content).to eq "HELLO\nWORLD"
|
||||
expect(note.noteable).to be_closed
|
||||
|
@ -141,14 +131,87 @@ RSpec.describe Notes::QuickActionsService do
|
|||
let(:note_text) { "HELLO\n/reopen\nWORLD" }
|
||||
|
||||
it 'opens the noteable' do
|
||||
content, update_params = service.execute(note)
|
||||
service.apply_updates(update_params, note)
|
||||
content = execute(note)
|
||||
|
||||
expect(content).to eq "HELLO\nWORLD"
|
||||
expect(note.noteable).to be_open
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '/milestone' do
|
||||
let(:issue) { create(:issue, project: project) }
|
||||
let(:note_text) { %(/milestone %"#{milestone.name}") }
|
||||
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
|
||||
|
||||
context 'on an incident' do
|
||||
before do
|
||||
issue.update!(issue_type: :incident)
|
||||
end
|
||||
|
||||
it 'leaves the note empty' do
|
||||
expect(execute(note)).to be_empty
|
||||
end
|
||||
|
||||
it 'does not assign the milestone' do
|
||||
expect { execute(note) }.not_to change { issue.reload.milestone }
|
||||
end
|
||||
end
|
||||
|
||||
context 'on a merge request' do
|
||||
let(:note_mr) { create(:note_on_merge_request, project: project, note: note_text) }
|
||||
|
||||
it 'leaves the note empty' do
|
||||
expect(execute(note_mr)).to be_empty
|
||||
end
|
||||
|
||||
it 'assigns the milestone' do
|
||||
expect { execute(note) }.to change { issue.reload.milestone }.from(nil).to(milestone)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '/remove_milestone' do
|
||||
let(:issue) { create(:issue, project: project, milestone: milestone) }
|
||||
let(:note_text) { '/remove_milestone' }
|
||||
let(:note) { create(:note_on_issue, noteable: issue, project: project, note: note_text) }
|
||||
|
||||
context 'on an issue' do
|
||||
it 'leaves the note empty' do
|
||||
expect(execute(note)).to be_empty
|
||||
end
|
||||
|
||||
it 'removes the milestone' do
|
||||
expect { execute(note) }.to change { issue.reload.milestone }.from(milestone).to(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'on an incident' do
|
||||
before do
|
||||
issue.update!(issue_type: :incident)
|
||||
end
|
||||
|
||||
it 'leaves the note empty' do
|
||||
expect(execute(note)).to be_empty
|
||||
end
|
||||
|
||||
it 'does not remove the milestone' do
|
||||
expect { execute(note) }.not_to change { issue.reload.milestone }
|
||||
end
|
||||
end
|
||||
|
||||
context 'on a merge request' do
|
||||
let(:note_mr) { create(:note_on_merge_request, project: project, note: note_text) }
|
||||
|
||||
it 'leaves the note empty' do
|
||||
expect(execute(note_mr)).to be_empty
|
||||
end
|
||||
|
||||
it 'removes the milestone' do
|
||||
expect { execute(note) }.to change { issue.reload.milestone }.from(milestone).to(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.noteable_update_service' do
|
||||
|
@ -215,7 +278,7 @@ RSpec.describe Notes::QuickActionsService do
|
|||
end
|
||||
|
||||
it_behaves_like 'note on noteable that supports quick actions' do
|
||||
let_it_be(:merge_request, reload: true) { create(:merge_request, source_project: project) }
|
||||
let(:merge_request) { create(:merge_request, source_project: project) }
|
||||
let(:note) { build(:note_on_merge_request, project: project, noteable: merge_request) }
|
||||
end
|
||||
end
|
||||
|
@ -239,11 +302,17 @@ RSpec.describe Notes::QuickActionsService do
|
|||
end
|
||||
|
||||
it 'adds only one assignee from the list' do
|
||||
_, update_params = service.execute(note)
|
||||
service.apply_updates(update_params, note)
|
||||
execute(note)
|
||||
|
||||
expect(note.noteable.assignees.count).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def execute(note)
|
||||
content, update_params = service.execute(note)
|
||||
service.apply_updates(update_params, note)
|
||||
|
||||
content
|
||||
end
|
||||
end
|
||||
|
|
25
yarn.lock
25
yarn.lock
|
@ -5581,6 +5581,16 @@ graphql@^14.7.0:
|
|||
dependencies:
|
||||
iterall "^1.2.2"
|
||||
|
||||
gray-matter@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.2.tgz#9aa379e3acaf421193fce7d2a28cebd4518ac454"
|
||||
integrity sha512-7hB/+LxrOjq/dd8APlK0r24uL/67w7SkYnfwhNFwg/VDIGWGmduTDYf3WNstLW2fbbmRwrDGCVSJ2isuf2+4Hw==
|
||||
dependencies:
|
||||
js-yaml "^3.11.0"
|
||||
kind-of "^6.0.2"
|
||||
section-matter "^1.0.0"
|
||||
strip-bom-string "^1.0.0"
|
||||
|
||||
growly@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/growly/-/growly-1.3.0.tgz#f10748cbe76af964b7c96c93c6bcc28af120c081"
|
||||
|
@ -7106,7 +7116,7 @@ js-cookie@^2.2.1:
|
|||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
|
||||
js-yaml@^3.12.0, js-yaml@^3.13.1, js-yaml@~3.13.1:
|
||||
js-yaml@^3.11.0, js-yaml@^3.12.0, js-yaml@^3.13.1, js-yaml@~3.13.1:
|
||||
version "3.13.1"
|
||||
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847"
|
||||
integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==
|
||||
|
@ -10492,6 +10502,14 @@ scss-tokenizer@^0.2.3:
|
|||
js-base64 "^2.1.8"
|
||||
source-map "^0.4.2"
|
||||
|
||||
section-matter@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167"
|
||||
integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==
|
||||
dependencies:
|
||||
extend-shallow "^2.0.1"
|
||||
kind-of "^6.0.0"
|
||||
|
||||
select-hose@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca"
|
||||
|
@ -11154,6 +11172,11 @@ strip-ansi@^6.0.0:
|
|||
dependencies:
|
||||
ansi-regex "^5.0.0"
|
||||
|
||||
strip-bom-string@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92"
|
||||
integrity sha1-5SEekiQ2n7uB1jOi8ABE3IztrZI=
|
||||
|
||||
strip-bom@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-2.0.0.tgz#6219a85616520491f35788bdbf1447a99c7e6b0e"
|
||||
|
|
Loading…
Reference in New Issue