Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-02 12:09:02 +00:00
parent 6aab18704a
commit b0107e8756
37 changed files with 939 additions and 125 deletions

View File

@ -328,6 +328,9 @@ Cop/SidekiqOptionsQueue:
Graphql/AuthorizeTypes: Graphql/AuthorizeTypes:
Enabled: true Enabled: true
Include:
- 'app/graphql/types/**/*'
- 'ee/app/graphql/types/**/*'
Exclude: Exclude:
- 'spec/**/*.rb' - 'spec/**/*.rb'
- 'ee/spec/**/*.rb' - 'ee/spec/**/*.rb'

View File

@ -3,7 +3,7 @@ import LogLine from './line.vue';
import LogLineHeader from './line_header.vue'; import LogLineHeader from './line_header.vue';
export default { export default {
name: 'CollpasibleLogSection', name: 'CollapsibleLogSection',
components: { components: {
LogLine, LogLine,
LogLineHeader, LogLineHeader,

View File

@ -1,11 +1,11 @@
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import CollpasibleLogSection from './collapsible_section.vue'; import CollapsibleLogSection from './collapsible_section.vue';
import LogLine from './line.vue'; import LogLine from './line.vue';
export default { export default {
components: { components: {
CollpasibleLogSection, CollapsibleLogSection,
LogLine, LogLine,
}, },
computed: { computed: {
@ -51,7 +51,7 @@ export default {
<template> <template>
<code class="job-log d-block" data-qa-selector="job_log_content"> <code class="job-log d-block" data-qa-selector="job_log_content">
<template v-for="(section, index) in trace"> <template v-for="(section, index) in trace">
<collpasible-log-section <collapsible-log-section
v-if="section.isHeader" v-if="section.isHeader"
:key="`collapsible-${index}`" :key="`collapsible-${index}`"
:section="section" :section="section"

View File

@ -195,7 +195,7 @@ export const receiveTraceError = ({ dispatch }) => {
flash(__('An error occurred while fetching the job log.')); flash(__('An error occurred while fetching the job log.'));
}; };
/** /**
* When the user clicks a collpasible line in the job * When the user clicks a collapsible line in the job
* log, we commit a mutation to update the state * log, we commit a mutation to update the state
* *
* @param {Object} section * @param {Object} section

View File

@ -11,7 +11,7 @@ export const parseLine = (line = {}, lineNumber) => ({
/** /**
* When a line has `section_header` set to true, we create a new * When a line has `section_header` set to true, we create a new
* structure to allow to nest the lines that belong to the * structure to allow to nest the lines that belong to the
* collpasible section * collapsible section
* *
* @param Object line * @param Object line
* @param Number lineNumber * @param Number lineNumber
@ -91,7 +91,7 @@ export const getIncrementalLineNumber = acc => {
* Parses the job log content into a structure usable by the template * Parses the job log content into a structure usable by the template
* *
* For collaspible lines (section_header = true): * For collaspible lines (section_header = true):
* - creates a new array to hold the lines that are collpasible, * - creates a new array to hold the lines that are collapsible,
* - adds a isClosed property to handle toggle * - adds a isClosed property to handle toggle
* - adds a isHeader property to handle template logic * - adds a isHeader property to handle template logic
* - adds the section_duration * - adds the section_duration

View File

@ -0,0 +1,91 @@
<script>
import { GlTabs, GlTab, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import createFlash from '~/flash';
import ForkGroupsListItem from './fork_groups_list_item.vue';
export default {
components: {
GlTabs,
GlTab,
GlLoadingIcon,
GlSearchBoxByType,
ForkGroupsListItem,
},
props: {
hasReachedProjectLimit: {
type: Boolean,
required: true,
},
endpoint: {
type: String,
required: true,
},
},
data() {
return {
namespaces: null,
filter: '',
};
},
computed: {
filteredNamespaces() {
return this.namespaces.filter(n => n.name.toLowerCase().includes(this.filter.toLowerCase()));
},
},
mounted() {
this.loadGroups();
},
methods: {
loadGroups() {
axios
.get(this.endpoint)
.then(response => {
this.namespaces = response.data.namespaces;
})
.catch(() => createFlash(__('There was a problem fetching groups.')));
},
},
i18n: {
searchPlaceholder: __('Search by name'),
},
};
</script>
<template>
<gl-tabs class="fork-groups">
<gl-tab :title="__('Groups and subgroups')">
<gl-loading-icon v-if="!namespaces" size="md" class="gl-mt-3" />
<template v-else-if="namespaces.length === 0">
<div class="gl-text-center">
<div class="h5">{{ __('No available groups to fork the project.') }}</div>
<p class="gl-mt-5">
{{ __('You must have permission to create a project in a group before forking.') }}
</p>
</div>
</template>
<div v-else-if="filteredNamespaces.length === 0" class="gl-text-center gl-mt-3">
{{ s__('GroupsTree|No groups matched your search') }}
</div>
<ul v-else class="groups-list group-list-tree">
<fork-groups-list-item
v-for="(namespace, index) in filteredNamespaces"
:key="index"
:group="namespace"
:has-reached-project-limit="hasReachedProjectLimit"
/>
</ul>
</gl-tab>
<template #tabs-end>
<gl-search-box-by-type
v-if="namespaces && namespaces.length"
v-model="filter"
:placeholder="$options.i18n.searchPlaceholder"
class="gl-align-self-center gl-ml-auto fork-filtered-search"
/>
</template>
</gl-tabs>
</template>

View File

@ -0,0 +1,147 @@
<script>
import {
GlLink,
GlButton,
GlIcon,
GlAvatar,
GlTooltipDirective,
GlTooltip,
GlBadge,
} from '@gitlab/ui';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/groups/constants';
import { __ } from '~/locale';
import csrf from '~/lib/utils/csrf';
export default {
components: {
GlIcon,
GlAvatar,
GlBadge,
GlButton,
GlTooltip,
GlLink,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
group: {
type: Object,
required: true,
},
hasReachedProjectLimit: {
type: Boolean,
required: true,
},
},
data() {
return { namespaces: null };
},
computed: {
rowClass() {
return {
'has-description': this.group.description,
'being-removed': this.isGroupPendingRemoval,
};
},
isGroupPendingRemoval() {
return this.group.marked_for_deletion;
},
hasForkedProject() {
return Boolean(this.group.forked_project_path);
},
visibilityIcon() {
return VISIBILITY_TYPE_ICON[this.group.visibility];
},
visibilityTooltip() {
return GROUP_VISIBILITY_TYPE[this.group.visibility];
},
isSelectButtonDisabled() {
return this.hasReachedProjectLimit || !this.group.can_create_project;
},
selectButtonDisabledTooltip() {
return this.hasReachedProjectLimit
? this.$options.i18n.hasReachedProjectLimitMessage
: this.$options.i18n.insufficientPermissionsMessage;
},
},
i18n: {
hasReachedProjectLimitMessage: __('You have reached your project limit'),
insufficientPermissionsMessage: __(
'You must have permission to create a project in a namespace before forking.',
),
},
csrf,
};
</script>
<template>
<li :class="rowClass" class="group-row">
<div class="group-row-contents gl-display-flex gl-align-items-center gl-py-3 gl-pr-5">
<div class="folder-toggle-wrap gl-mr-2 gl-display-flex gl-align-items-center">
<gl-icon name="folder-o" />
</div>
<gl-link
:href="group.relative_path"
class="gl-display-none gl-flex-shrink-0 gl-display-sm-flex gl-mr-3"
>
<gl-avatar :size="32" shape="rect" :entity-name="group.name" :src="group.avatarUrl" />
</gl-link>
<div class="gl-min-w-0 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1 gl-align-items-center">
<div class="gl-min-w-0 gl-flex-grow-1 flex-shrink-1">
<div class="title gl-display-flex gl-align-items-center gl-flex-wrap gl-mr-3">
<gl-link :href="group.relative_path" class="gl-mt-3 gl-mr-3 gl-text-gray-900!">{{
group.full_name
}}</gl-link>
<gl-icon
v-gl-tooltip.hover.bottom
class="gl-mr-0 gl-inline-flex gl-mt-3 text-secondary"
:name="visibilityIcon"
:title="visibilityTooltip"
/>
<gl-badge
v-if="isGroupPendingRemoval"
variant="warning"
class="gl-display-none gl-display-sm-flex gl-mt-3 gl-mr-1"
>{{ __('pending removal') }}</gl-badge
>
<span v-if="group.permission" class="user-access-role gl-mt-3">
{{ group.permission }}
</span>
</div>
<div v-if="group.description" class="description">
<span v-html="group.markdown_description"> </span>
</div>
</div>
<div class="gl-display-flex gl-flex-shrink-0">
<gl-button
v-if="hasForkedProject"
class="gl-h-7 gl-text-decoration-none!"
:href="group.forked_project_path"
>{{ __('Go to fork') }}</gl-button
>
<template v-else>
<div ref="selectButtonWrapper">
<form method="POST" :action="group.fork_path">
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
<gl-button
type="submit"
class="gl-h-7 gl-text-decoration-none!"
:data-qa-name="group.full_name"
variant="success"
:disabled="isSelectButtonDisabled"
>{{ __('Select') }}</gl-button
>
</form>
</div>
<gl-tooltip v-if="isSelectButtonDisabled" :target="() => $refs.selectButtonWrapper">
{{ selectButtonDisabledTooltip }}
</gl-tooltip>
</template>
</div>
</div>
</div>
</li>
</template>

View File

@ -17,11 +17,8 @@ module Routable
after_validation :set_path_errors after_validation :set_path_errors
before_validation do before_validation :prepare_route
if full_path_changed? || full_name_changed? before_save :prepare_route # in case validation is skipped
prepare_route
end
end
end end
class_methods do class_methods do
@ -118,6 +115,8 @@ module Routable
end end
def prepare_route def prepare_route
return unless full_path_changed? || full_name_changed?
route || build_route(source: self) route || build_route(source: self)
route.path = build_full_path route.path = build_full_path
route.name = build_full_name route.name = build_full_name

View File

@ -14,6 +14,7 @@ class ProjectStatistics < ApplicationRecord
COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size].freeze COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size].freeze
INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze INCREMENTABLE_COLUMNS = { build_artifacts_size: %i[storage_size], packages_size: %i[storage_size] }.freeze
NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size].freeze NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size].freeze
FLAGGED_NAMESPACE_RELATABLE_COLUMNS = [*NAMESPACE_RELATABLE_COLUMNS, :snippets_size].freeze
scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) } scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
@ -31,7 +32,7 @@ class ProjectStatistics < ApplicationRecord
end end
end end
if only.empty? || only.any? { |column| NAMESPACE_RELATABLE_COLUMNS.include?(column) } if only.empty? || only.any? { |column| namespace_relatable_columns.include?(column) }
schedule_namespace_aggregation_worker schedule_namespace_aggregation_worker
end end
@ -110,6 +111,10 @@ class ProjectStatistics < ApplicationRecord
Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id) Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id)
end end
end end
def namespace_relatable_columns
Feature.enabled?(:namespace_snippets_size_stat) ? FLAGGED_NAMESPACE_RELATABLE_COLUMNS : NAMESPACE_RELATABLE_COLUMNS
end
end end
ProjectStatistics.prepend_if_ee('EE::ProjectStatistics') ProjectStatistics.prepend_if_ee('EE::ProjectStatistics')

View File

@ -1527,7 +1527,7 @@
- :name: project_update_repository_storage - :name: project_update_repository_storage
:feature_category: :gitaly :feature_category: :gitaly
:has_external_dependencies: :has_external_dependencies:
:urgency: :low :urgency: :throttled
:resource_boundary: :unknown :resource_boundary: :unknown
:weight: 1 :weight: 1
:idempotent: true :idempotent: true

View File

@ -5,6 +5,7 @@ class ProjectUpdateRepositoryStorageWorker
idempotent! idempotent!
feature_category :gitaly feature_category :gitaly
urgency :throttled
def perform(project_id, new_repository_storage_key, repository_storage_move_id = nil) def perform(project_id, new_repository_storage_key, repository_storage_move_id = nil)
repository_storage_move = repository_storage_move =

View File

@ -0,0 +1,5 @@
---
title: Throttle ProjectUpdateRepositoryStorageWorker Jobs
merge_request: 35230
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Create associated routes when a new bot user is created
merge_request: 35711
author:
type: fixed

View File

@ -10,17 +10,30 @@ Sidekiq::Testing.inline! do
# we use randomized approach (e.g. `Array#sample`). # we use randomized approach (e.g. `Array#sample`).
return unless source_project return unless source_project
fork_project = Projects::ForkService.new( Sidekiq::Worker.skipping_transaction_check do
source_project, fork_project = Projects::ForkService.new(
user, source_project,
namespace: user.namespace, user,
skip_disk_validation: true namespace: user.namespace,
).execute skip_disk_validation: true
).execute
if fork_project.valid? # Seed-Fu runs this entire fixture in a transaction, so the `after_commit`
print '.' # hook won't run until after the fixture is loaded. That is too late
else # since the Sidekiq::Testing block has already exited. Force clearing
print 'F' # the `after_commit` queue to ensure the job is run now.
fork_project.send(:_run_after_commit_queue)
fork_project.import_state.send(:_run_after_commit_queue)
# Expire repository cache after import to ensure
# valid_repo? call below returns a correct answer
fork_project.repository.expire_all_method_caches
if fork_project.valid? && fork_project.valid_repo?
print '.'
else
print 'F'
end
end end
end end
end end

View File

@ -11310,6 +11310,56 @@ type RunDASTScanPayload {
pipelineUrl: String pipelineUrl: String
} }
"""
Represents a resource scanned by a security scan
"""
type ScannedResource {
"""
The HTTP request method used to access the URL
"""
requestMethod: String
"""
The URL scanned by the scanner
"""
url: String
}
"""
The connection type for ScannedResource.
"""
type ScannedResourceConnection {
"""
A list of edges.
"""
edges: [ScannedResourceEdge]
"""
A list of nodes.
"""
nodes: [ScannedResource]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type ScannedResourceEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: ScannedResource
}
""" """
Represents summary of a security report Represents summary of a security report
""" """
@ -11349,6 +11399,31 @@ type SecurityReportSummary {
Represents a section of a summary of a security report Represents a section of a summary of a security report
""" """
type SecurityReportSummarySection { type SecurityReportSummarySection {
"""
A list of the first 20 scanned resources
"""
scannedResources(
"""
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
"""
Returns the last _n_ elements from the list.
"""
last: Int
): ScannedResourceConnection
""" """
Total number of scanned resources Total number of scanned resources
""" """

View File

@ -33217,6 +33217,159 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "ScannedResource",
"description": "Represents a resource scanned by a security scan",
"fields": [
{
"name": "requestMethod",
"description": "The HTTP request method used to access the URL",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "url",
"description": "The URL scanned by the scanner",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ScannedResourceConnection",
"description": "The connection type for ScannedResource.",
"fields": [
{
"name": "edges",
"description": "A list of edges.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ScannedResourceEdge",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "nodes",
"description": "A list of nodes.",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "ScannedResource",
"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": "ScannedResourceEdge",
"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": "ScannedResource",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "SecurityReportSummary", "name": "SecurityReportSummary",
@ -33319,6 +33472,59 @@
"name": "SecurityReportSummarySection", "name": "SecurityReportSummarySection",
"description": "Represents a section of a summary of a security report", "description": "Represents a section of a summary of a security report",
"fields": [ "fields": [
{
"name": "scannedResources",
"description": "A list of the first 20 scanned resources",
"args": [
{
"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": "ScannedResourceConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "scannedResourcesCount", "name": "scannedResourcesCount",
"description": "Total number of scanned resources", "description": "Total number of scanned resources",

View File

@ -1637,6 +1637,15 @@ Autogenerated return type of RunDASTScan
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `pipelineUrl` | String | URL of the pipeline that was created. | | `pipelineUrl` | String | URL of the pipeline that was created. |
## ScannedResource
Represents a resource scanned by a security scan
| Name | Type | Description |
| --- | ---- | ---------- |
| `requestMethod` | String | The HTTP request method used to access the URL |
| `url` | String | The URL scanned by the scanner |
## SecurityReportSummary ## SecurityReportSummary
Represents summary of a security report Represents summary of a security report

View File

@ -1,5 +1,5 @@
--- ---
redirect_to: '../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues--starter' redirect_to: '../user/project/description_templates.md'
--- ---
This document was moved to [description_templates](../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues--starter). This document was moved to [description_templates](../user/project/description_templates.md).

View File

@ -666,8 +666,9 @@ appear to be associated to any of the services running, since they all appear to
| `clusters_applications_runner` | `usage_activity_by_stage` | `verify` | | | Unique clusters with Runner enabled | | `clusters_applications_runner` | `usage_activity_by_stage` | `verify` | | | Unique clusters with Runner enabled |
| `projects_reporting_ci_cd_back_to_github: 0` | `usage_activity_by_stage` | `verify` | | | Unique projects with a GitHub pipeline enabled | | `projects_reporting_ci_cd_back_to_github: 0` | `usage_activity_by_stage` | `verify` | | | Unique projects with a GitHub pipeline enabled |
| `merge_requests_users` | `usage_activity_by_stage_monthly` | `create` | | | Unique count of users who used a merge request | | `merge_requests_users` | `usage_activity_by_stage_monthly` | `create` | | | Unique count of users who used a merge request |
| `nodes` | `topology` | `enablement` | | | The list of server nodes on which GitLab components are running |
| `duration_s` | `topology` | `enablement` | | | Time it took to collect topology data | | `duration_s` | `topology` | `enablement` | | | Time it took to collect topology data |
| `application_requests_per_hour` | `topology` | `enablement` | | | Number of requests to the web application per hour |
| `nodes` | `topology` | `enablement` | | | The list of server nodes on which GitLab components are running |
| `node_memory_total_bytes` | `topology > nodes` | `enablement` | | | The total available memory of this node | | `node_memory_total_bytes` | `topology > nodes` | `enablement` | | | The total available memory of this node |
| `node_cpus` | `topology > nodes` | `enablement` | | | The number of CPU cores of this node | | `node_cpus` | `topology > nodes` | `enablement` | | | The number of CPU cores of this node |
| `node_services` | `topology > nodes` | `enablement` | | | The list of GitLab services running on this node | | `node_services` | `topology > nodes` | `enablement` | | | The list of GitLab services running on this node |
@ -873,6 +874,8 @@ The following is example content of the Usage Ping payload.
} }
}, },
"topology": { "topology": {
"duration_s": 0.013836685999194742,
"application_requests_per_hour": 4224,
"nodes": [ "nodes": [
{ {
"node_memory_total_bytes": 33269903360, "node_memory_total_bytes": 33269903360,
@ -897,8 +900,7 @@ The following is example content of the Usage Ping payload.
... ...
}, },
... ...
], ]
"duration_s": 0.013836685999194742
} }
} }
``` ```

View File

@ -81,7 +81,7 @@ changes you made after picking the template and return it to its initial status.
![Description templates](img/description_templates.png) ![Description templates](img/description_templates.png)
## Setting a default template for merge requests and issues **(STARTER)** ## Setting a default template for merge requests and issues **(STARTER)**
> - This feature was introduced before [description templates](#overview) and is available in [GitLab Starter](https://about.gitlab.com/pricing/). It can be enabled in the project's settings. > - This feature was introduced before [description templates](#overview) and is available in [GitLab Starter](https://about.gitlab.com/pricing/). It can be enabled in the project's settings.
> - Templates for issues were [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28) in GitLab EE 8.1. > - Templates for issues were [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28) in GitLab EE 8.1.

View File

@ -18,6 +18,7 @@ module Gitlab
MASS_INSERT_PROJECT_START = 'mass_insert_project_' MASS_INSERT_PROJECT_START = 'mass_insert_project_'
MASS_INSERT_USER_START = 'mass_insert_user_' MASS_INSERT_USER_START = 'mass_insert_user_'
REPORTED_USER_START = 'reported_user_'
ESTIMATED_INSERT_PER_MINUTE = 2_000_000 ESTIMATED_INSERT_PER_MINUTE = 2_000_000
MASS_INSERT_ENV = 'MASS_INSERT' MASS_INSERT_ENV = 'MASS_INSERT'
@ -36,7 +37,7 @@ module Gitlab
included do included do
scope :not_mass_generated, -> do scope :not_mass_generated, -> do
where.not("username LIKE '#{MASS_INSERT_USER_START}%'") where.not("username LIKE '#{MASS_INSERT_USER_START}%' OR username LIKE '#{REPORTED_USER_START}%'")
end end
end end
end end

View File

@ -28,11 +28,20 @@ module Gitlab
def topology_fetch_all_data def topology_fetch_all_data
with_prometheus_client(fallback: {}) do |client| with_prometheus_client(fallback: {}) do |client|
{ {
application_requests_per_hour: topology_app_requests_per_hour(client),
nodes: topology_node_data(client) nodes: topology_node_data(client)
} }.compact
end end
end end
def topology_app_requests_per_hour(client)
result = client.query(one_week_average('gitlab_usage_ping:ops:rate5m')).first
return unless result
# the metric is recorded as a per-second rate
(result['value'].last.to_f * 1.hour).to_i
end
def topology_node_data(client) def topology_node_data(client)
# node-level data # node-level data
by_instance_mem = topology_node_memory(client) by_instance_mem = topology_node_memory(client)

View File

@ -10964,6 +10964,9 @@ msgstr ""
msgid "Go to find file" msgid "Go to find file"
msgstr "" msgstr ""
msgid "Go to fork"
msgstr ""
msgid "Go to issue boards" msgid "Go to issue boards"
msgstr "" msgstr ""
@ -11531,6 +11534,9 @@ msgstr ""
msgid "Groups and projects" msgid "Groups and projects"
msgstr "" msgstr ""
msgid "Groups and subgroups"
msgstr ""
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}." msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr "" msgstr ""
@ -15131,6 +15137,9 @@ msgstr ""
msgid "No authentication methods configured." msgid "No authentication methods configured."
msgstr "" msgstr ""
msgid "No available groups to fork the project."
msgstr ""
msgid "No available namespaces to fork the project." msgid "No available namespaces to fork the project."
msgstr "" msgstr ""
@ -19831,6 +19840,9 @@ msgstr ""
msgid "Search by author" msgid "Search by author"
msgstr "" msgstr ""
msgid "Search by name"
msgstr ""
msgid "Search files" msgid "Search files"
msgstr "" msgstr ""
@ -22979,6 +22991,9 @@ msgstr ""
msgid "There was a problem communicating with your device." msgid "There was a problem communicating with your device."
msgstr "" msgstr ""
msgid "There was a problem fetching groups."
msgstr ""
msgid "There was a problem fetching project branches." msgid "There was a problem fetching project branches."
msgstr "" msgstr ""
@ -26256,6 +26271,9 @@ msgstr ""
msgid "You must have maintainer access to force delete a lock" msgid "You must have maintainer access to force delete a lock"
msgstr "" msgstr ""
msgid "You must have permission to create a project in a group before forking."
msgstr ""
msgid "You must have permission to create a project in a namespace before forking." msgid "You must have permission to create a project in a namespace before forking."
msgstr "" msgstr ""

View File

@ -40,7 +40,7 @@
"@babel/plugin-syntax-import-meta": "^7.10.1", "@babel/plugin-syntax-import-meta": "^7.10.1",
"@babel/preset-env": "^7.10.1", "@babel/preset-env": "^7.10.1",
"@gitlab/at.js": "1.5.5", "@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.146.0", "@gitlab/svgs": "1.147.0",
"@gitlab/ui": "17.10.1", "@gitlab/ui": "17.10.1",
"@gitlab/visual-review-tools": "1.6.1", "@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.3-1", "@rails/actioncable": "^6.0.3-1",

View File

@ -83,13 +83,13 @@ module QA
end end
def api_get_from(get_path) def api_get_from(get_path)
url = Runtime::API::Request.new(api_client, get_path).url request = Runtime::API::Request.new(api_client, get_path)
response = get(url) response = get(request.url)
if response.code == HTTP_STATUS_SERVER_ERROR if response.code == HTTP_STATUS_SERVER_ERROR
raise InternalServerError, "Failed to GET #{url} - (#{response.code}): `#{response}`." raise InternalServerError, "Failed to GET #{request.mask_url} - (#{response.code}): `#{response}`."
elsif response.code != HTTP_STATUS_OK elsif response.code != HTTP_STATUS_OK
raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`." raise ResourceNotFoundError, "Resource at #{request.mask_url} could not be found (#{response.code}): `#{response}`."
end end
response response
@ -108,11 +108,11 @@ module QA
end end
def api_delete def api_delete
url = Runtime::API::Request.new(api_client, api_delete_path).url request = Runtime::API::Request.new(api_client, api_delete_path)
response = delete(url) response = delete(request.url)
unless [HTTP_STATUS_NO_CONTENT, HTTP_STATUS_ACCEPTED].include? response.code unless [HTTP_STATUS_NO_CONTENT, HTTP_STATUS_ACCEPTED].include? response.code
raise ResourceNotDeletedError, "Resource at #{url} could not be deleted (#{response.code}): `#{response}`." raise ResourceNotDeletedError, "Resource at #{request.mask_url} could not be deleted (#{response.code}): `#{response}`."
end end
response response

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
module QA module QA
context 'Plan', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/225409', type: :bug } do context 'Plan' do
describe 'Jira issue import', :jira, :orchestrated, :requires_admin do describe 'Jira issue import', :jira, :orchestrated, :requires_admin do
let(:jira_project_key) { "JITD" } let(:jira_project_key) { "JITD" }
let(:jira_issue_title) { "[#{jira_project_key}-1] Jira to GitLab Test Issue" } let(:jira_issue_title) { "[#{jira_project_key}-1] Jira to GitLab Test Issue" }

View File

@ -22,6 +22,12 @@ describe QA::Runtime::API::Request do
end end
end end
describe '#mask_url' do
it 'returns the full API request url with the token masked' do
expect(request.mask_url).to eq 'http://example.com/api/v4/users?private_token=[****]'
end
end
describe '#request_path' do describe '#request_path' do
it 'prepends the api path' do it 'prepends the api path' do
expect(request.request_path('/users')).to eq '/api/v4/users' expect(request.request_path('/users')).to eq '/api/v4/users'

View File

@ -7,8 +7,6 @@ module RuboCop
MSG = 'Add an `authorize :ability` call to the type: '\ MSG = 'Add an `authorize :ability` call to the type: '\
'https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#type-authorization' 'https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#type-authorization'
TYPES_DIR = 'app/graphql/types'
# We want to exclude our own basetypes and scalars # We want to exclude our own basetypes and scalars
WHITELISTED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType WHITELISTED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType
QueryType GraphQL::Schema BaseUnion].freeze QueryType GraphQL::Schema BaseUnion].freeze
@ -18,7 +16,6 @@ module RuboCop
PATTERN PATTERN
def on_class(node) def on_class(node)
return unless in_type?(node)
return if whitelisted?(class_constant(node)) return if whitelisted?(class_constant(node))
return if whitelisted?(superclass_constant(node)) return if whitelisted?(superclass_constant(node))
@ -27,12 +24,6 @@ module RuboCop
private private
def in_type?(node)
path = node.location.expression.source_buffer.name
path.include? TYPES_DIR
end
def whitelisted?(class_node) def whitelisted?(class_node)
class_const = class_node&.const_name class_const = class_node&.const_name

View File

@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import CollpasibleSection from '~/jobs/components/log/collapsible_section.vue'; import CollapsibleSection from '~/jobs/components/log/collapsible_section.vue';
import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data'; import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data';
describe('Job Log Collapsible Section', () => { describe('Job Log Collapsible Section', () => {
@ -11,7 +11,7 @@ describe('Job Log Collapsible Section', () => {
const findCollapsibleLineSvg = () => wrapper.find('.collapsible-line svg'); const findCollapsibleLineSvg = () => wrapper.find('.collapsible-line svg');
const createComponent = (props = {}) => { const createComponent = (props = {}) => {
wrapper = mount(CollpasibleSection, { wrapper = mount(CollapsibleSection, {
propsData: { propsData: {
...props, ...props,
}, },

View File

@ -181,7 +181,7 @@ describe('Jobs Store Utils', () => {
}); });
}); });
describe('collpasible section', () => { describe('collapsible section', () => {
it('adds a `isClosed` property', () => { it('adds a `isClosed` property', () => {
expect(result[1].isClosed).toEqual(false); expect(result[1].isClosed).toEqual(false);
}); });
@ -190,7 +190,7 @@ describe('Jobs Store Utils', () => {
expect(result[1].isHeader).toEqual(true); expect(result[1].isHeader).toEqual(true);
}); });
it('creates a lines array property with the content of the collpasible section', () => { it('creates a lines array property with the content of the collapsible section', () => {
expect(result[1].lines.length).toEqual(2); expect(result[1].lines.length).toEqual(2);
expect(result[1].lines[0].content).toEqual(utilsMockData[2].content); expect(result[1].lines[0].content).toEqual(utilsMockData[2].content);
expect(result[1].lines[1].content).toEqual(utilsMockData[3].content); expect(result[1].lines[1].content).toEqual(utilsMockData[3].content);

View File

@ -0,0 +1,78 @@
import { shallowMount } from '@vue/test-utils';
import { GlBadge, GlButton, GlLink } from '@gitlab/ui';
import ForkGroupsListItem from '~/pages/projects/forks/new/components/fork_groups_list_item.vue';
describe('Fork groups list item component', () => {
let wrapper;
const DEFAULT_PROPS = {
hasReachedProjectLimit: false,
};
const DEFAULT_GROUP_DATA = {
id: 22,
name: 'Gitlab Org',
description: 'Ad et ipsam earum id aut nobis.',
visibility: 'public',
full_name: 'Gitlab Org',
created_at: '2020-06-22T03:32:05.664Z',
updated_at: '2020-06-22T03:32:05.664Z',
avatar_url: null,
fork_path: '/twitter/typeahead-js/-/forks?namespace_key=22',
forked_project_path: null,
permission: 'Owner',
relative_path: '/gitlab-org',
markdown_description:
'<p data-sourcepos="1:1-1:31" dir="auto">Ad et ipsam earum id aut nobis.</p>',
can_create_project: true,
marked_for_deletion: false,
};
const DUMMY_PATH = '/dummy/path';
const createWrapper = propsData => {
wrapper = shallowMount(ForkGroupsListItem, {
propsData: {
...DEFAULT_PROPS,
...propsData,
},
});
};
it('renders pending removal badge if applicable', () => {
createWrapper({ group: { ...DEFAULT_GROUP_DATA, marked_for_deletion: true } });
expect(wrapper.find(GlBadge).text()).toBe('pending removal');
});
it('renders go to fork button if has forked project', () => {
createWrapper({ group: { ...DEFAULT_GROUP_DATA, forked_project_path: DUMMY_PATH } });
expect(wrapper.find(GlButton).text()).toBe('Go to fork');
expect(wrapper.find(GlButton).attributes().href).toBe(DUMMY_PATH);
});
it('renders select button if has no forked project', () => {
createWrapper({
group: { ...DEFAULT_GROUP_DATA, forked_project_path: null, fork_path: DUMMY_PATH },
});
expect(wrapper.find(GlButton).text()).toBe('Select');
expect(wrapper.find('form').attributes().action).toBe(DUMMY_PATH);
});
it('renders link to current group', () => {
const DUMMY_FULL_NAME = 'dummy';
createWrapper({
group: { ...DEFAULT_GROUP_DATA, relative_path: DUMMY_PATH, full_name: DUMMY_FULL_NAME },
});
expect(
wrapper
.findAll(GlLink)
.filter(w => w.text() === DUMMY_FULL_NAME)
.at(0)
.attributes().href,
).toBe(DUMMY_PATH);
});
});

View File

@ -0,0 +1,133 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { nextTick } from 'vue';
import createFlash from '~/flash';
import ForkGroupsList from '~/pages/projects/forks/new/components/fork_groups_list.vue';
import ForkGroupsListItem from '~/pages/projects/forks/new/components/fork_groups_list_item.vue';
import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/flash', () => jest.fn());
describe('Fork groups list component', () => {
let wrapper;
let axiosMock;
const DEFAULT_PROPS = {
endpoint: '/dummy',
hasReachedProjectLimit: false,
};
const replyWith = (...args) => axiosMock.onGet(DEFAULT_PROPS.endpoint).reply(...args);
const createWrapper = propsData => {
wrapper = shallowMount(ForkGroupsList, {
propsData: {
...DEFAULT_PROPS,
...propsData,
},
stubs: {
GlTabs: {
template: '<div><slot></slot><slot name="tabs-end"></slot></div>',
},
},
});
};
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
});
afterEach(() => {
axiosMock.reset();
if (wrapper) {
wrapper.destroy();
wrapper = null;
}
});
it('fires load groups request on mount', async () => {
replyWith(200, { namespaces: [] });
createWrapper();
await waitForPromises();
expect(axiosMock.history.get[0].url).toBe(DEFAULT_PROPS.endpoint);
});
it('displays flash if loading groups fails', async () => {
replyWith(500);
createWrapper();
await waitForPromises();
expect(createFlash).toHaveBeenCalled();
});
it('displays loading indicator while loading groups', () => {
replyWith(() => new Promise(() => {}));
createWrapper();
expect(wrapper.contains(GlLoadingIcon)).toBe(true);
});
it('displays empty text if no groups are available', async () => {
const EMPTY_TEXT = 'No available groups to fork the project.';
replyWith(200, { namespaces: [] });
createWrapper();
await waitForPromises();
expect(wrapper.text()).toContain(EMPTY_TEXT);
});
it('displays filter field when groups are available', async () => {
replyWith(200, { namespaces: [{ name: 'dummy1' }, { name: 'dummy2' }] });
createWrapper();
await waitForPromises();
expect(wrapper.contains(GlSearchBoxByType)).toBe(true);
});
it('renders list items for each available group', async () => {
const namespaces = [{ name: 'dummy1' }, { name: 'dummy2' }, { name: 'otherdummy' }];
const hasReachedProjectLimit = true;
replyWith(200, { namespaces });
createWrapper({ hasReachedProjectLimit });
await waitForPromises();
expect(wrapper.findAll(ForkGroupsListItem)).toHaveLength(namespaces.length);
namespaces.forEach((namespace, idx) => {
expect(
wrapper
.findAll(ForkGroupsListItem)
.at(idx)
.props(),
).toStrictEqual({ group: namespace, hasReachedProjectLimit });
});
});
it('filters repositories on the fly', async () => {
replyWith(200, {
namespaces: [{ name: 'dummy1' }, { name: 'dummy2' }, { name: 'otherdummy' }],
});
createWrapper();
await waitForPromises();
wrapper.find(GlSearchBoxByType).vm.$emit('input', 'other');
await nextTick();
expect(wrapper.findAll(ForkGroupsListItem)).toHaveLength(1);
expect(
wrapper
.findAll(ForkGroupsListItem)
.at(0)
.props().group.name,
).toBe('otherdummy');
});
});

View File

@ -22,6 +22,7 @@ RSpec.describe Gitlab::UsageDataConcerns::Topology do
context 'tracking node metrics' do context 'tracking node metrics' do
it 'contains node level metrics for each instance' do it 'contains node level metrics for each instance' do
expect_prometheus_api_to( expect_prometheus_api_to(
receive_app_request_volume_query,
receive_node_memory_query, receive_node_memory_query,
receive_node_cpu_count_query, receive_node_cpu_count_query,
receive_node_service_memory_rss_query, receive_node_service_memory_rss_query,
@ -32,6 +33,7 @@ RSpec.describe Gitlab::UsageDataConcerns::Topology do
expect(subject[:topology]).to eq({ expect(subject[:topology]).to eq({
duration_s: 0, duration_s: 0,
application_requests_per_hour: 36,
nodes: [ nodes: [
{ {
node_memory_total_bytes: 512, node_memory_total_bytes: 512,
@ -76,6 +78,7 @@ RSpec.describe Gitlab::UsageDataConcerns::Topology do
context 'and some node memory metrics are missing' do context 'and some node memory metrics are missing' do
it 'removes the respective entries' do it 'removes the respective entries' do
expect_prometheus_api_to( expect_prometheus_api_to(
receive_app_request_volume_query(result: []),
receive_node_memory_query(result: []), receive_node_memory_query(result: []),
receive_node_cpu_count_query, receive_node_cpu_count_query,
receive_node_service_memory_rss_query(result: []), receive_node_service_memory_rss_query(result: []),
@ -149,6 +152,17 @@ RSpec.describe Gitlab::UsageDataConcerns::Topology do
end end
end end
def receive_app_request_volume_query(result: nil)
receive(:query)
.with(/gitlab_usage_ping:ops:rate/)
.and_return(result || [
{
'metric' => { 'component' => 'http_requests', 'service' => 'workhorse' },
'value' => [1000, '0.01']
}
])
end
def receive_node_memory_query(result: nil) def receive_node_memory_query(result: nil)
receive(:query) receive(:query)
.with(/node_memory_total_bytes/, an_instance_of(Hash)) .with(/node_memory_total_bytes/, an_instance_of(Hash))

View File

@ -189,6 +189,26 @@ RSpec.describe ProjectStatistics do
statistics.refresh! statistics.refresh!
end end
end end
context 'when snippets_size is updated' do
it 'schedules the aggregation worker' do
expect(Namespaces::ScheduleAggregationWorker)
.to receive(:perform_async)
statistics.refresh!(only: [:snippets_size])
end
context 'when feature flag :namespace_snippets_size_stat is disabled' do
it 'does not schedules an aggregation worker' do
stub_feature_flags(namespace_snippets_size_stat: false)
expect(Namespaces::ScheduleAggregationWorker)
.not_to receive(:perform_async)
statistics.refresh!(only: [:snippets_size])
end
end
end
end end
context 'when the column is not namespace relatable' do context 'when the column is not namespace relatable' do

View File

@ -4766,6 +4766,12 @@ RSpec.describe User do
end.to change { User.where(user_type: bot_type).count }.by(1) end.to change { User.where(user_type: bot_type).count }.by(1)
end end
it 'creates a route for the namespace of the created user' do
bot_user = described_class.public_send(bot_type)
expect(bot_user.namespace.route).to be_present
end
it 'does not create a new user if it already exists' do it 'does not create a new user if it already exists' do
described_class.public_send(bot_type) described_class.public_send(bot_type)

View File

@ -10,83 +10,60 @@ RSpec.describe RuboCop::Cop::Graphql::AuthorizeTypes, type: :rubocop do
subject(:cop) { described_class.new } subject(:cop) { described_class.new }
context 'when NOT in a type folder' do it 'adds an offense when there is no authorize call' do
before do inspect_source(<<~TYPE)
allow(cop).to receive(:in_type?).and_return(false) module Types
end class AType < BaseObject
field :a_thing
it 'does not add an offense even though there is no authorize call' do field :another_thing
expect_no_offenses(<<~TYPE.strip)
module Types
class AType < BaseObject
field :a_thing
field :another_thing
end
end end
TYPE end
end TYPE
expect(cop.offenses.size).to eq 1
end end
context 'when in a type folder' do it 'does not add an offense for classes that have an authorize call' do
before do expect_no_offenses(<<~TYPE.strip)
allow(cop).to receive(:in_type?).and_return(true) module Types
end class AType < BaseObject
graphql_name 'ATypeName'
it 'adds an offense when there is no authorize call' do authorize :an_ability, :second_ability
inspect_source(<<~TYPE)
module Types field :a_thing
class AType < BaseObject
field :a_thing
field :another_thing
end
end end
TYPE end
TYPE
end
expect(cop.offenses.size).to eq 1 it 'does not add an offense for classes that only have an authorize call' do
end expect_no_offenses(<<~TYPE.strip)
module Types
it 'does not add an offense for classes that have an authorize call' do class AType < SuperClassWithFields
expect_no_offenses(<<~TYPE.strip) authorize :an_ability
module Types
class AType < BaseObject
graphql_name 'ATypeName'
authorize :an_ability, :second_ability
field :a_thing
end
end end
TYPE end
end TYPE
end
it 'does not add an offense for classes that only have an authorize call' do it 'does not add an offense for base types' do
expect_no_offenses(<<~TYPE.strip) expect_no_offenses(<<~TYPE)
module Types module Types
class AType < SuperClassWithFields class AType < BaseEnum
authorize :an_ability field :a_thing
end
end end
TYPE end
end TYPE
end
it 'does not add an offense for base types' do it 'does not add an offense for Enums' do
expect_no_offenses(<<~TYPE) expect_no_offenses(<<~TYPE)
module Types module Types
class AType < BaseEnum class ATypeEnum < AnotherEnum
field :a_thing field :a_thing
end
end end
TYPE end
end TYPE
it 'does not add an offense for Enums' do
expect_no_offenses(<<~TYPE)
module Types
class ATypeEnum < AnotherEnum
field :a_thing
end
end
TYPE
end
end end
end end

View File

@ -843,10 +843,10 @@
eslint-plugin-vue "^6.2.1" eslint-plugin-vue "^6.2.1"
vue-eslint-parser "^7.0.0" vue-eslint-parser "^7.0.0"
"@gitlab/svgs@1.146.0": "@gitlab/svgs@1.147.0":
version "1.146.0" version "1.147.0"
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.146.0.tgz#c74118a3f1ab47ae77211d42597f553f395deb5d" resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.147.0.tgz#1b2cc986cb3219609136cab641e2c384d724700f"
integrity sha512-2/k9pAZPgHpZ5Ad0fz9i1109sWcShDE4XcjrjzltNNksbi86lqCKbsSe580ujtlG8KShgGMkDkmUa6AHZi64Xw== integrity sha512-KnjN7ms7bEPajYl7q0nKv7HMKtqR/JxCVSBRGXH5ezkeGKy4wb4yEYtvRK8no7ix+Iw4rc0KTqOwKp9nkl/KdA==
"@gitlab/ui@17.10.1": "@gitlab/ui@17.10.1":
version "17.10.1" version "17.10.1"