Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-10 12:08:54 +00:00
parent 1385b54a3e
commit 766b24b86b
74 changed files with 1639 additions and 364 deletions

View File

@ -1 +1 @@
47f676eea28871563414671e1016fb28b1b3e167
d2e978f8e8f47a49c3bcfbd470b2f790e52c5ee2

View File

@ -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"

View File

@ -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),
});

View File

@ -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) {

View File

@ -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,
};
};

View File

@ -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;

View File

@ -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) {

View File

@ -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

View File

@ -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)

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: Resolve Fix validation on External Wiki service template form
merge_request: 41964
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Expose Instance Statistics measurements (object counts) via GraphQL
merge_request: 40871
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Refuse to perform an LFS clean on projects that are fork roots
merge_request: 41703
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Display merged commit sha in fast-forward merge mode
merge_request: 41369
author: Mycroft Kang @TaehyeokKang
type: added

View File

@ -0,0 +1,5 @@
---
title: Ensure namespace settings are backfilled via migration
merge_request: 41679
author:
type: other

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
2311967a9f68e1a428662e0231752ad0d844063d66cca895211d38f9ae928d94

View File

@ -76,6 +76,7 @@ Citus
clonable
Cloudwatch
Cobertura
Codepen
Cognito
colocated
colocating

View File

@ -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

View File

@ -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
"""

View File

@ -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",

View File

@ -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 |

View File

@ -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"

View File

@ -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

View File

@ -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:

View File

@ -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`.

View File

@ -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

View File

@ -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

View File

@ -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` files history.
NOTE: **Note:**
If the latest pipeline used [Auto DevOps](../../../topics/autodevops/index.md),
all security features are configured by default.

View File

@ -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 theyre 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.

View File

@ -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

View File

@ -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.

View File

@ -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

29
lib/api/usage_data.rb Normal file
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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" }
}

View File

@ -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);
});
});
});

View File

@ -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);

View File

@ -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';

View File

@ -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}`);
}
});
});
});

View File

@ -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);

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)) }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"