Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-08-28 18:10:51 +00:00
parent c41b66bd05
commit 261c96684a
34 changed files with 636 additions and 308 deletions

View file

@ -16,9 +16,11 @@
## Author's checklist (required) ## Author's checklist (required)
- [ ] Follow the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html). - [ ] Follow the [Documentation Guidelines](https://docs.gitlab.com/ee/development/documentation/) and [Style Guide](https://docs.gitlab.com/ee/development/documentation/styleguide.html).
- If you have `developer` access or higher (for example, GitLab team members or [Core Team](https://about.gitlab.com/community/core-team/) members)
- If you have **Developer** permissions or higher:
- [ ] Ensure that the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide.html#product-badges) is added to doc's `h1`.
- [ ] Apply the ~documentation label, plus: - [ ] Apply the ~documentation label, plus:
- The corresponding DevOps stage and group label, if applicable. - The corresponding DevOps stage and group labels, if applicable.
- ~"development guidelines" when changing docs under `doc/development/*`, `CONTRIBUTING.md`, or `README.md`. - ~"development guidelines" when changing docs under `doc/development/*`, `CONTRIBUTING.md`, or `README.md`.
- ~"development guidelines" and ~"Documentation guidelines" when changing docs under `development/documentation/*`. - ~"development guidelines" and ~"Documentation guidelines" when changing docs under `development/documentation/*`.
- ~"development guidelines" and ~"Description templates (.gitlab/\*)" when creating/updating issue and MR description templates. - ~"development guidelines" and ~"Description templates (.gitlab/\*)" when creating/updating issue and MR description templates.
@ -30,10 +32,9 @@ When applicable:
- [ ] Update the [permissions table](https://docs.gitlab.com/ee/user/permissions.html). - [ ] Update the [permissions table](https://docs.gitlab.com/ee/user/permissions.html).
- [ ] Link docs to and from the higher-level index page, plus other related docs where helpful. - [ ] Link docs to and from the higher-level index page, plus other related docs where helpful.
- [ ] Add the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide.html#product-badges) accordingly.
- [ ] Add [GitLab's version history note(s)](https://docs.gitlab.com/ee/development/documentation/styleguide.html#text-for-documentation-requiring-version-text). - [ ] Add [GitLab's version history note(s)](https://docs.gitlab.com/ee/development/documentation/styleguide.html#text-for-documentation-requiring-version-text).
- [ ] Add the [product tier badge](https://docs.gitlab.com/ee/development/documentation/styleguide.html#product-badges).
- [ ] Add/update the [feature flag section](https://docs.gitlab.com/ee/development/documentation/feature_flags.html). - [ ] Add/update the [feature flag section](https://docs.gitlab.com/ee/development/documentation/feature_flags.html).
- [ ] If you're changing document headings, search `doc/*`, `app/views/*`, and `ee/app/views/*` for old headings replacing with the new ones to [avoid broken anchors](https://docs.gitlab.com/ee/development/documentation/styleguide.html#anchor-links).
## Review checklist ## Review checklist
@ -46,8 +47,9 @@ All reviewers can help ensure accuracy, clarity, completeness, and adherence to
**2. Technical Writer** **2. Technical Writer**
- [ ] Technical writer review. If not requested for this MR, must be scheduled post-merge. To request for this MR, assign the writer listed for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/product-categories/#devops-stages). - [ ] Technical writer review. If not requested for this MR, must be scheduled post-merge. To request for this MR, assign the writer listed for the applicable [DevOps stage](https://about.gitlab.com/handbook/product/product-categories/#devops-stages).
- [ ] Ensure ~"Technical Writing", ~"documentation", and a `docs::` scoped label are added. - [ ] Ensure docs metadata are present and up-to-date.
- [ ] Add ~docs-only when the only files changed are under `doc/*`. - [ ] Ensure ~"Technical Writing" and ~"documentation" are added.
- [ ] Add the corresponding `docs::` scoped label.
- [ ] Add ~"tw::doing" when starting work on the MR. - [ ] Add ~"tw::doing" when starting work on the MR.
- [ ] Add ~"tw::finished" if Technical Writing team work on the MR is complete but it remains open. - [ ] Add ~"tw::finished" if Technical Writing team work on the MR is complete but it remains open.

View file

@ -516,7 +516,7 @@ GEM
googleapis-common-protos-types (~> 1.0) googleapis-common-protos-types (~> 1.0)
gssapi (1.2.0) gssapi (1.2.0)
ffi (>= 1.0.1) ffi (>= 1.0.1)
guard (2.15.1) guard (2.16.2)
formatador (>= 0.2.4) formatador (>= 0.2.4)
listen (>= 2.7, < 4.0) listen (>= 2.7, < 4.0)
lumberjack (>= 1.0.12, < 2.0) lumberjack (>= 1.0.12, < 2.0)
@ -649,10 +649,9 @@ GEM
xml-simple xml-simple
licensee (8.9.2) licensee (8.9.2)
rugged (~> 0.24) rugged (~> 0.24)
listen (3.1.5) listen (3.2.1)
rb-fsevent (~> 0.9, >= 0.9.4) rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.7) rb-inotify (~> 0.9, >= 0.9.10)
ruby_dep (~> 1.2)
locale (2.1.3) locale (2.1.3)
lockbox (0.3.3) lockbox (0.3.3)
lograge (0.11.2) lograge (0.11.2)
@ -664,7 +663,7 @@ GEM
crass (~> 1.0.2) crass (~> 1.0.2)
nokogiri (>= 1.5.9) nokogiri (>= 1.5.9)
lru_redux (1.1.0) lru_redux (1.1.0)
lumberjack (1.0.13) lumberjack (1.2.7)
mail (2.7.1) mail (2.7.1)
mini_mime (>= 0.1.1) mini_mime (>= 0.1.1)
marcel (0.3.3) marcel (0.3.3)
@ -899,9 +898,9 @@ GEM
rainbow (3.0.0) rainbow (3.0.0)
raindrops (0.19.1) raindrops (0.19.1)
rake (12.3.3) rake (12.3.3)
rb-fsevent (0.10.2) rb-fsevent (0.10.4)
rb-inotify (0.9.10) rb-inotify (0.10.1)
ffi (>= 0.5.0, < 2) ffi (~> 1.0)
rblineprof (0.3.6) rblineprof (0.3.6)
debugger-ruby_core_source (~> 1.3) debugger-ruby_core_source (~> 1.3)
rbtrace (0.4.14) rbtrace (0.4.14)
@ -1020,7 +1019,6 @@ GEM
nokogiri (>= 1.5.10) nokogiri (>= 1.5.10)
ruby-statistics (2.1.2) ruby-statistics (2.1.2)
ruby2_keywords (0.0.2) ruby2_keywords (0.0.2)
ruby_dep (1.5.0)
ruby_parser (3.13.1) ruby_parser (3.13.1)
sexp_processor (~> 4.9) sexp_processor (~> 4.9)
rubyntlm (0.6.2) rubyntlm (0.6.2)

View file

@ -1,8 +1,13 @@
/* eslint-disable @gitlab/require-i18n-strings */
import { __ } from '~/locale'; import { __ } from '~/locale';
export const ANY_AUTHOR = 'Any'; export const ANY_AUTHOR = 'Any';
export const NO_LABEL = 'No label'; const DEFAULT_LABEL_NO_LABEL = { value: 'No label', text: __('No label') };
export const DEFAULT_LABEL_NONE = { value: 'None', text: __('None') };
export const DEFAULT_LABEL_ANY = { value: 'Any', text: __('Any') };
export const DEFAULT_LABELS = [DEFAULT_LABEL_NO_LABEL];
export const DEBOUNCE_DELAY = 200; export const DEBOUNCE_DELAY = 200;
@ -11,13 +16,11 @@ export const SortDirection = {
ascending: 'ascending', ascending: 'ascending',
}; };
export const defaultMilestones = [ export const DEFAULT_MILESTONES = [
// eslint-disable-next-line @gitlab/require-i18n-strings DEFAULT_LABEL_NONE,
{ value: 'None', text: __('None') }, DEFAULT_LABEL_ANY,
// eslint-disable-next-line @gitlab/require-i18n-strings
{ value: 'Any', text: __('Any') },
// eslint-disable-next-line @gitlab/require-i18n-strings
{ value: 'Upcoming', text: __('Upcoming') }, { value: 'Upcoming', text: __('Upcoming') },
// eslint-disable-next-line @gitlab/require-i18n-strings
{ value: 'Started', text: __('Started') }, { value: 'Started', text: __('Started') },
]; ];
/* eslint-enable @gitlab/require-i18n-strings */

View file

@ -14,10 +14,9 @@ import { __ } from '~/locale';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { stripQuotes } from '../filtered_search_utils'; import { stripQuotes } from '../filtered_search_utils';
import { NO_LABEL, DEBOUNCE_DELAY } from '../constants'; import { DEFAULT_LABELS, DEBOUNCE_DELAY } from '../constants';
export default { export default {
noLabel: NO_LABEL,
components: { components: {
GlToken, GlToken,
GlFilteredSearchToken, GlFilteredSearchToken,
@ -38,6 +37,7 @@ export default {
data() { data() {
return { return {
labels: this.config.initialLabels || [], labels: this.config.initialLabels || [],
defaultLabels: this.config.defaultLabels || DEFAULT_LABELS,
loading: true, loading: true,
}; };
}, },
@ -105,9 +105,13 @@ export default {
> >
</template> </template>
<template #suggestions> <template #suggestions>
<gl-filtered-search-suggestion :value="$options.noLabel">{{ <gl-filtered-search-suggestion
__('No label') v-for="label in defaultLabels"
}}</gl-filtered-search-suggestion> :key="label.value"
:value="label.value"
>
{{ label.text }}
</gl-filtered-search-suggestion>
<gl-dropdown-divider /> <gl-dropdown-divider />
<gl-loading-icon v-if="loading" /> <gl-loading-icon v-if="loading" />
<template v-else> <template v-else>

View file

@ -11,10 +11,9 @@ import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { stripQuotes } from '../filtered_search_utils'; import { stripQuotes } from '../filtered_search_utils';
import { defaultMilestones, DEBOUNCE_DELAY } from '../constants'; import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants';
export default { export default {
defaultMilestones,
components: { components: {
GlFilteredSearchToken, GlFilteredSearchToken,
GlFilteredSearchSuggestion, GlFilteredSearchSuggestion,
@ -34,6 +33,7 @@ export default {
data() { data() {
return { return {
milestones: this.config.initialMilestones || [], milestones: this.config.initialMilestones || [],
defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES,
loading: true, loading: true,
}; };
}, },
@ -89,11 +89,12 @@ export default {
</template> </template>
<template #suggestions> <template #suggestions>
<gl-filtered-search-suggestion <gl-filtered-search-suggestion
v-for="milestone in $options.defaultMilestones" v-for="milestone in defaultMilestones"
:key="milestone.value" :key="milestone.value"
:value="milestone.value" :value="milestone.value"
>{{ milestone.text }}</gl-filtered-search-suggestion
> >
{{ milestone.text }}
</gl-filtered-search-suggestion>
<gl-dropdown-divider /> <gl-dropdown-divider />
<gl-loading-icon v-if="loading" /> <gl-loading-icon v-if="loading" />
<template v-else> <template v-else>

View file

@ -16,14 +16,6 @@ module Mutations
required: true, required: true,
description: 'Title of the snippet' description: 'Title of the snippet'
argument :file_name, GraphQL::STRING_TYPE,
required: false,
description: 'File name of the snippet'
argument :content, GraphQL::STRING_TYPE,
required: false,
description: 'Content of the snippet'
argument :description, GraphQL::STRING_TYPE, argument :description, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Description of the snippet' description: 'Description of the snippet'

View file

@ -14,14 +14,6 @@ module Mutations
required: false, required: false,
description: 'Title of the snippet' description: 'Title of the snippet'
argument :file_name, GraphQL::STRING_TYPE,
required: false,
description: 'File name of the snippet'
argument :content, GraphQL::STRING_TYPE,
required: false,
description: 'Content of the snippet'
argument :description, GraphQL::STRING_TYPE, argument :description, GraphQL::STRING_TYPE,
required: false, required: false,
description: 'Description of the snippet' description: 'Description of the snippet'

View file

@ -2,11 +2,13 @@
module Ci module Ci
class RetryBuildService < ::BaseService class RetryBuildService < ::BaseService
CLONE_ACCESSORS = %i[pipeline project ref tag options name def self.clone_accessors
allow_failure stage stage_id stage_idx trigger_request %i[pipeline project ref tag options name
yaml_variables when environment coverage_regex allow_failure stage stage_id stage_idx trigger_request
description tag_list protected needs_attributes yaml_variables when environment coverage_regex
resource_group scheduling_type].freeze description tag_list protected needs_attributes
resource_group scheduling_type].freeze
end
def execute(build) def execute(build)
build.ensure_scheduling_type! build.ensure_scheduling_type!
@ -28,7 +30,7 @@ module Ci
raise Gitlab::Access::AccessDeniedError raise Gitlab::Access::AccessDeniedError
end end
attributes = CLONE_ACCESSORS.map do |attribute| attributes = self.class.clone_accessors.map do |attribute|
[attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend [attribute, build.public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend
end.to_h end.to_h
@ -68,3 +70,5 @@ module Ci
end end
end end
end end
Ci::RetryBuildService.prepend_if_ee('EE::Ci::RetryBuildService')

View file

@ -0,0 +1,5 @@
---
title: Remove file_name and content in snippet mutations
merge_request: 40727
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Improve group search users scope performance
merge_request: 38701
author:
type: performance

View file

@ -0,0 +1,5 @@
---
title: Remove pipeline_id column from requirements_test_reports
merge_request: 38924
author:
type: deprecated

View file

@ -0,0 +1,5 @@
---
title: Add kubernetes_agents usage metric
merge_request: 40559
author:
type: other

View file

@ -0,0 +1,21 @@
# frozen_string_literal: true
class RemovePipelineIdFromTestReports < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
remove_column :requirements_management_test_reports, :pipeline_id
end
def down
add_column :requirements_management_test_reports, :pipeline_id, :integer
with_lock_retries do
# rubocop:disable Migration/AddConcurrentForeignKey
add_foreign_key :requirements_management_test_reports, :ci_pipelines, column: :pipeline_id, on_delete: :nullify
# rubocop:enable Migration/AddConcurrentForeignKey
end
end
end

View file

@ -0,0 +1 @@
66653e275889da8e695843f648af36c8a4e275b4d3215119eab4942db1b4b823

View file

@ -15120,7 +15120,6 @@ CREATE TABLE public.requirements_management_test_reports (
id bigint NOT NULL, id bigint NOT NULL,
created_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL,
requirement_id bigint NOT NULL, requirement_id bigint NOT NULL,
pipeline_id bigint,
author_id bigint, author_id bigint,
state smallint NOT NULL, state smallint NOT NULL,
build_id bigint build_id bigint
@ -20644,8 +20643,6 @@ CREATE INDEX index_requirements_management_test_reports_on_author_id ON public.r
CREATE INDEX index_requirements_management_test_reports_on_build_id ON public.requirements_management_test_reports USING btree (build_id); CREATE INDEX index_requirements_management_test_reports_on_build_id ON public.requirements_management_test_reports USING btree (build_id);
CREATE INDEX index_requirements_management_test_reports_on_pipeline_id ON public.requirements_management_test_reports USING btree (pipeline_id);
CREATE INDEX index_requirements_management_test_reports_on_requirement_id ON public.requirements_management_test_reports USING btree (requirement_id); CREATE INDEX index_requirements_management_test_reports_on_requirement_id ON public.requirements_management_test_reports USING btree (requirement_id);
CREATE INDEX index_requirements_on_author_id ON public.requirements USING btree (author_id); CREATE INDEX index_requirements_on_author_id ON public.requirements USING btree (author_id);
@ -22205,9 +22202,6 @@ ALTER TABLE ONLY public.service_desk_settings
ALTER TABLE ONLY public.group_custom_attributes ALTER TABLE ONLY public.group_custom_attributes
ADD CONSTRAINT fk_rails_246e0db83a FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_246e0db83a FOREIGN KEY (group_id) REFERENCES public.namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY public.requirements_management_test_reports
ADD CONSTRAINT fk_rails_24cecc1e68 FOREIGN KEY (pipeline_id) REFERENCES public.ci_pipelines(id) ON DELETE SET NULL;
ALTER TABLE ONLY public.cluster_agents ALTER TABLE ONLY public.cluster_agents
ADD CONSTRAINT fk_rails_25e9fc2d5d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE; ADD CONSTRAINT fk_rails_25e9fc2d5d FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;

View file

@ -2746,21 +2746,11 @@ input CreateSnippetInput {
""" """
clientMutationId: String clientMutationId: String
"""
Content of the snippet
"""
content: String
""" """
Description of the snippet Description of the snippet
""" """
description: String description: String
"""
File name of the snippet
"""
fileName: String
""" """
The project full path the snippet is associated with The project full path the snippet is associated with
""" """
@ -2959,6 +2949,66 @@ type DastScannerProfileEdge {
node: DastScannerProfile node: DastScannerProfile
} }
"""
Identifier of DastScannerProfile
"""
scalar DastScannerProfileID
"""
Autogenerated input type of DastScannerProfileUpdate
"""
input DastScannerProfileUpdateInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The project the scanner profile belongs to.
"""
fullPath: ID!
"""
ID of the scanner profile to be updated.
"""
id: DastScannerProfileID!
"""
The name of the scanner profile.
"""
profileName: String!
"""
The maximum number of seconds allowed for the spider to traverse the site.
"""
spiderTimeout: Int!
"""
The maximum number of seconds allowed for the site under test to respond to a request.
"""
targetTimeout: Int!
}
"""
Autogenerated return type of DastScannerProfileUpdate
"""
type DastScannerProfileUpdatePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
ID of the scanner profile.
"""
id: DastScannerProfileID
}
""" """
Represents a DAST Site Profile. Represents a DAST Site Profile.
""" """
@ -9757,6 +9807,7 @@ type Mutation {
createSnippet(input: CreateSnippetInput!): CreateSnippetPayload createSnippet(input: CreateSnippetInput!): CreateSnippetPayload
dastOnDemandScanCreate(input: DastOnDemandScanCreateInput!): DastOnDemandScanCreatePayload dastOnDemandScanCreate(input: DastOnDemandScanCreateInput!): DastOnDemandScanCreatePayload
dastScannerProfileCreate(input: DastScannerProfileCreateInput!): DastScannerProfileCreatePayload dastScannerProfileCreate(input: DastScannerProfileCreateInput!): DastScannerProfileCreatePayload
dastScannerProfileUpdate(input: DastScannerProfileUpdateInput!): DastScannerProfileUpdatePayload
dastSiteProfileCreate(input: DastSiteProfileCreateInput!): DastSiteProfileCreatePayload dastSiteProfileCreate(input: DastSiteProfileCreateInput!): DastSiteProfileCreatePayload
dastSiteProfileDelete(input: DastSiteProfileDeleteInput!): DastSiteProfileDeletePayload dastSiteProfileDelete(input: DastSiteProfileDeleteInput!): DastSiteProfileDeletePayload
dastSiteProfileUpdate(input: DastSiteProfileUpdateInput!): DastSiteProfileUpdatePayload dastSiteProfileUpdate(input: DastSiteProfileUpdateInput!): DastSiteProfileUpdatePayload
@ -16566,21 +16617,11 @@ input UpdateSnippetInput {
""" """
clientMutationId: String clientMutationId: String
"""
Content of the snippet
"""
content: String
""" """
Description of the snippet Description of the snippet
""" """
description: String description: String
"""
File name of the snippet
"""
fileName: String
""" """
The global id of the snippet to update The global id of the snippet to update
""" """

View file

@ -7390,26 +7390,6 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "fileName",
"description": "File name of the snippet",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "content",
"description": "Content of the snippet",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "description", "name": "description",
"description": "Description of the snippet", "description": "Description of the snippet",
@ -8016,6 +7996,174 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "SCALAR",
"name": "DastScannerProfileID",
"description": "Identifier of DastScannerProfile",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DastScannerProfileUpdateInput",
"description": "Autogenerated input type of DastScannerProfileUpdate",
"fields": null,
"inputFields": [
{
"name": "fullPath",
"description": "The project the scanner profile belongs to.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "id",
"description": "ID of the scanner profile to be updated.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DastScannerProfileID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "profileName",
"description": "The name of the scanner profile.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "spiderTimeout",
"description": "The maximum number of seconds allowed for the spider to traverse the site.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "targetTimeout",
"description": "The maximum number of seconds allowed for the site under test to respond to a request.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DastScannerProfileUpdatePayload",
"description": "Autogenerated return type of DastScannerProfileUpdate",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "ID of the scanner profile.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "DastScannerProfileID",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "DastSiteProfile", "name": "DastSiteProfile",
@ -27986,6 +28134,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "dastScannerProfileUpdate",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DastScannerProfileUpdateInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DastScannerProfileUpdatePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "dastSiteProfileCreate", "name": "dastSiteProfileCreate",
"description": null, "description": null,
@ -48753,26 +48928,6 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "fileName",
"description": "File name of the snippet",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "content",
"description": "Content of the snippet",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "description", "name": "description",
"description": "Description of the snippet", "description": "Description of the snippet",

View file

@ -502,6 +502,16 @@ Autogenerated return type of DastScannerProfileCreate
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `id` | ID | ID of the scanner profile. | | `id` | ID | ID of the scanner profile. |
## DastScannerProfileUpdatePayload
Autogenerated return type of DastScannerProfileUpdate
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `id` | DastScannerProfileID | ID of the scanner profile. |
## DastSiteProfile ## DastSiteProfile
Represents a DAST Site Profile. Represents a DAST Site Profile.

View file

@ -15,7 +15,7 @@ Elasticsearch is enabled, you'll have the benefit of fast search response times
and the advantage of the following special searches: and the advantage of the following special searches:
- [Advanced Search](../user/search/advanced_global_search.md) - [Advanced Search](../user/search/advanced_global_search.md)
- [Advanced Syntax Search](../user/search/advanced_search_syntax.md) - [Advanced Search Syntax](../user/search/advanced_search_syntax.md)
## Version requirements ## Version requirements
@ -746,6 +746,17 @@ Here are some common pitfalls and how to overcome them:
You can run `sudo gitlab-rake gitlab:elastic:projects_not_indexed` to display projects that aren't indexed. You can run `sudo gitlab-rake gitlab:elastic:projects_not_indexed` to display projects that aren't indexed.
- **No new data is added to the Elasticsearch index when I push code**
NOTE: **Note:**
This was [fixed in GitLab 13.2](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35936) and the Rake task is not available for versions greater than that.
When performing the initial indexing of blobs, we lock all projects until the project finishes indexing. It could happen that an error during the process causes one or multiple projects to remain locked. In order to unlock them, run:
```shell
sudo gitlab-rake gitlab:elastic:clear_locked_projects
```
- **"Can't specify parent if no parent field has been configured"** - **"Can't specify parent if no parent field has been configured"**
If you enabled Elasticsearch before GitLab 8.12 and have not rebuilt indexes you will get If you enabled Elasticsearch before GitLab 8.12 and have not rebuilt indexes you will get

View file

@ -57,7 +57,7 @@ With GitLab Enterprise Edition, you can also:
- [Multiple Issue Boards](project/issue_board.md#multiple-issue-boards). - [Multiple Issue Boards](project/issue_board.md#multiple-issue-boards).
- Create formal relationships between issues with [Related Issues](project/issues/related_issues.md). - Create formal relationships between issues with [Related Issues](project/issues/related_issues.md).
- Use [Burndown Charts](project/milestones/burndown_charts.md) to track progress during a sprint or while working on a new version of their software. - Use [Burndown Charts](project/milestones/burndown_charts.md) to track progress during a sprint or while working on a new version of their software.
- Leverage [Elasticsearch](../integration/elasticsearch.md) with [Advanced Search](search/advanced_global_search.md) and [Advanced Syntax Search](search/advanced_search_syntax.md) for faster, more advanced code search across your entire GitLab instance. - Leverage [Elasticsearch](../integration/elasticsearch.md) with [Advanced Search](search/advanced_global_search.md) and [Advanced Search Syntax](search/advanced_search_syntax.md) for faster, more advanced code search across your entire GitLab instance.
- [Authenticate users with Kerberos](../integration/kerberos.md). - [Authenticate users with Kerberos](../integration/kerberos.md).
- [Mirror a repository](project/repository/repository_mirroring.md) from elsewhere on your local server. - [Mirror a repository](project/repository/repository_mirroring.md) from elsewhere on your local server.
- [Export issues as CSV](project/issues/csv_export.md). - [Export issues as CSV](project/issues/csv_export.md).

View file

@ -60,7 +60,7 @@ project you have access to.
![Advanced Search](img/advanced_global_search.png) ![Advanced Search](img/advanced_global_search.png)
You can also use the [Advanced Syntax Search](advanced_search_syntax.md) which You can also use the [Advanced Search Syntax](advanced_search_syntax.md) which
provides some useful queries. provides some useful queries.
NOTE: **Note:** NOTE: **Note:**

View file

@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated
type: reference type: reference
--- ---
# Advanced Syntax Search **(STARTER)** # Advanced Search Syntax **(STARTER)**
> - Introduced in [GitLab Enterprise Starter](https://about.gitlab.com/pricing/) 9.2 > - Introduced in [GitLab Enterprise Starter](https://about.gitlab.com/pricing/) 9.2
@ -19,7 +19,7 @@ visit the [administrator documentation](../../integration/elasticsearch.md).
## Overview ## Overview
The Advanced Syntax Search is a subset of the The Advanced Search Syntax is a subset of the
[Advanced Search](advanced_global_search.md), which you can use if you [Advanced Search](advanced_global_search.md), which you can use if you
want to have more specific search results. want to have more specific search results.
@ -38,9 +38,9 @@ not so sure.
In that case, using the advanced search syntax in your query will yield much In that case, using the advanced search syntax in your query will yield much
better results. better results.
## Using the Advanced Syntax Search ## Using the Advanced Search Syntax
The Advanced Syntax Search supports fuzzy or exact search queries with prefixes, The Advanced Search Syntax supports fuzzy or exact search queries with prefixes,
boolean operators, and much more. boolean operators, and much more.
Full details can be found in the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/5.3/query-dsl-simple-query-string-query.html#_simple_query_string_syntax), but Full details can be found in the [Elasticsearch documentation](https://www.elastic.co/guide/en/elasticsearch/reference/5.3/query-dsl-simple-query-string-query.html#_simple_query_string_syntax), but
@ -57,7 +57,7 @@ here's a quick guide:
### Syntax search filters ### Syntax search filters
The Advanced Syntax Search also supports the use of filters. The available filters are: The Advanced Search Syntax also supports the use of filters. The available filters are:
- filename: Filters by filename. You can use the glob (`*`) operator for fuzzy matching. - filename: Filters by filename. You can use the glob (`*`) operator for fuzzy matching.
- path: Filters by path. You can use the glob (`*`) operator for fuzzy matching. - path: Filters by path. You can use the glob (`*`) operator for fuzzy matching.

View file

@ -215,8 +215,8 @@ GitLab instance.
[Learn how to use the Advanced Search.](advanced_global_search.md) [Learn how to use the Advanced Search.](advanced_global_search.md)
## Advanced Syntax Search **(STARTER)** ## Advanced Search Syntax **(STARTER)**
Use advanced queries for more targeted search results. Use advanced queries for more targeted search results.
[Learn how to use the Advanced Syntax Search.](advanced_search_syntax.md) [Learn how to use the Advanced Search Syntax.](advanced_search_syntax.md)

View file

@ -12,20 +12,24 @@ module Gitlab
# rubocop:disable CodeReuse/ActiveRecord # rubocop:disable CodeReuse/ActiveRecord
def users def users
# 1: get all groups the current user has access to # get all groups the current user has access to
groups = GroupsFinder.new(current_user).execute.joins(:users) # ignore order inherited from GroupsFinder to improve performance
current_user_groups = GroupsFinder.new(current_user).execute.unscope(:order)
# 2: Get the group's whole hierarchy # the hierarchy of the current group
group_users = @group.direct_and_indirect_users group_groups = @group.self_and_hierarchy.unscope(:order)
# 3: get all users the current user has access to (-> # the groups where the above hierarchies intersect
# `SearchResults#users`), which also applies the query. intersect_groups = group_groups.where(id: current_user_groups)
# members of @group hierarchy where the user has access to the groups
members = GroupMember.where(group: intersect_groups).non_invite
# get all users the current user has access to (-> `SearchResults#users`), which also applies the query
users = super users = super
# 4: filter for users that belong to the previously selected groups # filter users that belong to the previously selected groups
users users.where(id: members.select(:user_id))
.where(id: group_users.select('id'))
.where(id: groups.select('members.user_id'))
end end
# rubocop:enable CodeReuse/ActiveRecord # rubocop:enable CodeReuse/ActiveRecord

View file

@ -111,6 +111,7 @@ module Gitlab
clusters_applications_jupyter: count(::Clusters::Applications::Jupyter.available), clusters_applications_jupyter: count(::Clusters::Applications::Jupyter.available),
clusters_applications_cilium: count(::Clusters::Applications::Cilium.available), clusters_applications_cilium: count(::Clusters::Applications::Cilium.available),
clusters_management_project: count(::Clusters::Cluster.with_management_project), clusters_management_project: count(::Clusters::Cluster.with_management_project),
kubernetes_agents: count(::Clusters::Agent),
in_review_folder: count(::Environment.in_review_folder), in_review_folder: count(::Environment.in_review_folder),
grafana_integrated_projects: count(GrafanaIntegration.enabled), grafana_integrated_projects: count(GrafanaIntegration.enabled),
groups: count(Group), groups: count(Group),

View file

@ -28080,6 +28080,9 @@ msgstr ""
msgid "You are not authorized to perform this action" msgid "You are not authorized to perform this action"
msgstr "" msgstr ""
msgid "You are not authorized to update this scanner profile"
msgstr ""
msgid "You are now impersonating %{username}" msgid "You are now impersonating %{username}"
msgstr "" msgstr ""

View file

@ -65,6 +65,9 @@ FactoryBot.define do
create(:alert_management_alert, issue: alert_bot_issues[0], project: projects[0]) create(:alert_management_alert, issue: alert_bot_issues[0], project: projects[0])
create(:self_managed_prometheus_alert_event, related_issues: [issues[1]], project: projects[0]) create(:self_managed_prometheus_alert_event, related_issues: [issues[1]], project: projects[0])
# Kubernetes agents
create(:cluster_agent, project: projects[0])
# Enabled clusters # Enabled clusters
gcp_cluster = create(:cluster_provider_gcp, :created).cluster gcp_cluster = create(:cluster_provider_gcp, :created).cluster
create(:cluster_provider_aws, :created) create(:cluster_provider_aws, :created)

View file

@ -1,5 +1,9 @@
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; import {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlFilteredSearchTokenSegment,
} from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { import {
@ -9,13 +13,32 @@ import {
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
DEFAULT_LABELS,
DEFAULT_LABEL_NONE,
DEFAULT_LABEL_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue'; import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import { mockLabelToken } from '../mock_data'; import { mockLabelToken } from '../mock_data';
jest.mock('~/flash'); jest.mock('~/flash');
const defaultStubs = {
Portal: true,
GlFilteredSearchSuggestionList: {
template: '<div></div>',
methods: {
getValue: () => '=',
},
},
};
const createComponent = ({ config = mockLabelToken, value = { data: '' }, active = false } = {}) => const createComponent = ({
config = mockLabelToken,
value = { data: '' },
active = false,
stubs = defaultStubs,
} = {}) =>
mount(LabelToken, { mount(LabelToken, {
propsData: { propsData: {
config, config,
@ -26,15 +49,7 @@ const createComponent = ({ config = mockLabelToken, value = { data: '' }, active
portalName: 'fake target', portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {}, alignSuggestions: function fakeAlignSuggestions() {},
}, },
stubs: { stubs,
Portal: true,
GlFilteredSearchSuggestionList: {
template: '<div></div>',
methods: {
getValue: () => '=',
},
},
},
}); });
describe('LabelToken', () => { describe('LabelToken', () => {
@ -43,7 +58,6 @@ describe('LabelToken', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
wrapper = createComponent();
}); });
afterEach(() => { afterEach(() => {
@ -96,6 +110,10 @@ describe('LabelToken', () => {
}); });
describe('methods', () => { describe('methods', () => {
beforeEach(() => {
wrapper = createComponent();
});
describe('fetchLabelBySearchTerm', () => { describe('fetchLabelBySearchTerm', () => {
it('calls `config.fetchLabels` with provided searchTerm param', () => { it('calls `config.fetchLabels` with provided searchTerm param', () => {
jest.spyOn(wrapper.vm.config, 'fetchLabels'); jest.spyOn(wrapper.vm.config, 'fetchLabels');
@ -138,6 +156,8 @@ describe('LabelToken', () => {
}); });
describe('template', () => { describe('template', () => {
const defaultLabels = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
beforeEach(async () => { beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } }); wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
@ -164,5 +184,43 @@ describe('LabelToken', () => {
.attributes('style'), .attributes('style'),
).toBe('background-color: rgb(186, 218, 85); color: rgb(255, 255, 255);'); ).toBe('background-color: rgb(186, 218, 85); color: rgb(255, 255, 255);');
}); });
it('renders provided defaultLabels as suggestions', async () => {
wrapper = createComponent({
active: true,
config: { ...mockLabelToken, defaultLabels },
stubs: { Portal: true },
});
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await wrapper.vm.$nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(defaultLabels.length);
defaultLabels.forEach((label, index) => {
expect(suggestions.at(index).text()).toBe(label.text);
});
});
it('renders `DEFAULT_LABELS` as default suggestions', async () => {
wrapper = createComponent({
active: true,
config: { ...mockLabelToken },
stubs: { Portal: true },
});
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await wrapper.vm.$nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(DEFAULT_LABELS.length);
DEFAULT_LABELS.forEach((label, index) => {
expect(suggestions.at(index).text()).toBe(label.text);
});
});
}); });
}); });

View file

@ -1,10 +1,15 @@
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'; import {
GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlFilteredSearchTokenSegment,
} from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { DEFAULT_MILESTONES } from '~/vue_shared/components/filtered_search_bar/constants';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue'; import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import { import {
@ -16,10 +21,21 @@ import {
jest.mock('~/flash'); jest.mock('~/flash');
const defaultStubs = {
Portal: true,
GlFilteredSearchSuggestionList: {
template: '<div></div>',
methods: {
getValue: () => '=',
},
},
};
const createComponent = ({ const createComponent = ({
config = mockMilestoneToken, config = mockMilestoneToken,
value = { data: '' }, value = { data: '' },
active = false, active = false,
stubs = defaultStubs,
} = {}) => } = {}) =>
mount(MilestoneToken, { mount(MilestoneToken, {
propsData: { propsData: {
@ -31,15 +47,7 @@ const createComponent = ({
portalName: 'fake target', portalName: 'fake target',
alignSuggestions: function fakeAlignSuggestions() {}, alignSuggestions: function fakeAlignSuggestions() {},
}, },
stubs: { stubs,
Portal: true,
GlFilteredSearchSuggestionList: {
template: '<div></div>',
methods: {
getValue: () => '=',
},
},
},
}); });
describe('MilestoneToken', () => { describe('MilestoneToken', () => {
@ -126,6 +134,8 @@ describe('MilestoneToken', () => {
}); });
describe('template', () => { describe('template', () => {
const defaultMilestones = [{ text: 'foo', value: 'foo' }, { text: 'bar', value: 'baz' }];
beforeEach(async () => { beforeEach(async () => {
wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } }); wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } });
@ -146,5 +156,43 @@ describe('MilestoneToken', () => {
expect(tokenSegments).toHaveLength(3); // Milestone, =, '%"4.0"' expect(tokenSegments).toHaveLength(3); // Milestone, =, '%"4.0"'
expect(tokenSegments.at(2).text()).toBe(`%"${mockRegularMilestone.title}"`); // "4.0 RC1" expect(tokenSegments.at(2).text()).toBe(`%"${mockRegularMilestone.title}"`); // "4.0 RC1"
}); });
it('renders provided defaultMilestones as suggestions', async () => {
wrapper = createComponent({
active: true,
config: { ...mockMilestoneToken, defaultMilestones },
stubs: { Portal: true },
});
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await wrapper.vm.$nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(defaultMilestones.length);
defaultMilestones.forEach((milestone, index) => {
expect(suggestions.at(index).text()).toBe(milestone.text);
});
});
it('renders `DEFAULT_MILESTONES` as default suggestions', async () => {
wrapper = createComponent({
active: true,
config: { ...mockMilestoneToken },
stubs: { Portal: true },
});
const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
const suggestionsSegment = tokenSegments.at(2);
suggestionsSegment.vm.$emit('activate');
await wrapper.vm.$nextTick();
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(DEFAULT_MILESTONES.length);
DEFAULT_MILESTONES.forEach((milestone, index) => {
expect(suggestions.at(index).text()).toBe(milestone.text);
});
});
}); });
}); });

View file

@ -3,8 +3,10 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe Gitlab::GroupSearchResults do RSpec.describe Gitlab::GroupSearchResults do
# group creation calls GroupFinder, so need to create the group
# before so expect(GroupsFinder) check works
let_it_be(:group) { create(:group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:group) { create(:group) }
subject(:results) { described_class.new(user, 'gob', anything, group: group) } subject(:results) { described_class.new(user, 'gob', anything, group: group) }
@ -60,6 +62,19 @@ RSpec.describe Gitlab::GroupSearchResults do
is_expected.to be_empty is_expected.to be_empty
end end
it 'does not return the user invited to the group' do
user = create(:user, username: 'gob_bluth')
create(:group_member, :invited, :developer, user: user, group: group)
is_expected.to be_empty
end
it 'calls GroupFinder during execution' do
expect(GroupsFinder).to receive(:new).with(user).and_call_original
subject
end
end end
describe "#issuable_params" do describe "#issuable_params" do

View file

@ -447,6 +447,7 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
expect(count_data[:clusters_applications_jupyter]).to eq(1) expect(count_data[:clusters_applications_jupyter]).to eq(1)
expect(count_data[:clusters_applications_cilium]).to eq(1) expect(count_data[:clusters_applications_cilium]).to eq(1)
expect(count_data[:clusters_management_project]).to eq(1) expect(count_data[:clusters_management_project]).to eq(1)
expect(count_data[:kubernetes_agents]).to eq(1)
expect(count_data[:deployments]).to eq(4) expect(count_data[:deployments]).to eq(4)
expect(count_data[:successful_deployments]).to eq(2) expect(count_data[:successful_deployments]).to eq(2)

View file

@ -7,22 +7,24 @@ RSpec.describe 'Creating a Snippet' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:content) { 'Initial content' }
let(:description) { 'Initial description' } let(:description) { 'Initial description' }
let(:title) { 'Initial title' } let(:title) { 'Initial title' }
let(:file_name) { 'Initial file_name' }
let(:visibility_level) { 'public' } let(:visibility_level) { 'public' }
let(:action) { :create }
let(:file_1) { { filePath: 'example_file1', content: 'This is the example file 1' }}
let(:file_2) { { filePath: 'example_file2', content: 'This is the example file 2' }}
let(:actions) { [{ action: action }.merge(file_1), { action: action }.merge(file_2)] }
let(:project_path) { nil } let(:project_path) { nil }
let(:uploaded_files) { nil } let(:uploaded_files) { nil }
let(:mutation_vars) do let(:mutation_vars) do
{ {
content: content,
description: description, description: description,
visibility_level: visibility_level, visibility_level: visibility_level,
file_name: file_name,
title: title, title: title,
project_path: project_path, project_path: project_path,
uploaded_files: uploaded_files uploaded_files: uploaded_files,
blob_actions: actions
} }
end end
@ -62,68 +64,6 @@ RSpec.describe 'Creating a Snippet' do
context 'when the user has permission' do context 'when the user has permission' do
let(:current_user) { user } let(:current_user) { user }
context 'with PersonalSnippet' do
it 'creates the Snippet' do
expect do
subject
end.to change { Snippet.count }.by(1)
end
it 'returns the created Snippet' do
subject
expect(mutation_response['snippet']['blob']['richData']).to be_nil
expect(mutation_response['snippet']['blob']['plainData']).to match(content)
expect(mutation_response['snippet']['title']).to eq(title)
expect(mutation_response['snippet']['description']).to eq(description)
expect(mutation_response['snippet']['fileName']).to eq(file_name)
expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level)
expect(mutation_response['snippet']['project']).to be_nil
end
end
context 'with ProjectSnippet' do
let(:project_path) { project.full_path }
before do
project.add_developer(current_user)
end
it 'creates the Snippet' do
expect do
subject
end.to change { Snippet.count }.by(1)
end
it 'returns the created Snippet' do
subject
expect(mutation_response['snippet']['blob']['richData']).to be_nil
expect(mutation_response['snippet']['blob']['plainData']).to match(content)
expect(mutation_response['snippet']['title']).to eq(title)
expect(mutation_response['snippet']['description']).to eq(description)
expect(mutation_response['snippet']['fileName']).to eq(file_name)
expect(mutation_response['snippet']['visibilityLevel']).to eq(visibility_level)
expect(mutation_response['snippet']['project']['fullPath']).to eq(project_path)
end
context 'when the project path is invalid' do
let(:project_path) { 'foobar' }
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
context 'when the feature is disabled' do
before do
project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::DISABLED)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
end
shared_examples 'does not create snippet' do shared_examples 'does not create snippet' do
it 'does not create the Snippet' do it 'does not create the Snippet' do
expect do expect do
@ -138,29 +78,11 @@ RSpec.describe 'Creating a Snippet' do
end end
end end
context 'when snippet is created using the files param' do shared_examples 'creates snippet' do
let(:action) { :create } it 'returns the created Snippet' do
let(:file_1) { { filePath: 'example_file1', content: 'This is the example file 1' }}
let(:file_2) { { filePath: 'example_file2', content: 'This is the example file 2' }}
let(:actions) { [{ action: action }.merge(file_1), { action: action }.merge(file_2)] }
let(:mutation_vars) do
{
description: description,
visibility_level: visibility_level,
project_path: project_path,
title: title,
blob_actions: actions
}
end
it 'creates the Snippet' do
expect do expect do
subject subject
end.to change { Snippet.count }.by(1) end.to change { Snippet.count }.by(1)
end
it 'returns the created Snippet' do
subject
expect(mutation_response['snippet']['title']).to eq(title) expect(mutation_response['snippet']['title']).to eq(title)
expect(mutation_response['snippet']['description']).to eq(description) expect(mutation_response['snippet']['description']).to eq(description)
@ -179,6 +101,36 @@ RSpec.describe 'Creating a Snippet' do
end end
end end
context 'with PersonalSnippet' do
it_behaves_like 'creates snippet'
end
context 'with ProjectSnippet' do
let(:project_path) { project.full_path }
before do
project.add_developer(current_user)
end
it_behaves_like 'creates snippet'
context 'when the project path is invalid' do
let(:project_path) { 'foobar' }
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
context 'when the feature is disabled' do
before do
project.project_feature.update_attribute(:snippets_access_level, ProjectFeature::DISABLED)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: [Gitlab::Graphql::Authorize::AuthorizeResource::RESOURCE_ACCESS_ERROR]
end
end
context 'when there are ActiveRecord validation errors' do context 'when there are ActiveRecord validation errors' do
let(:title) { '' } let(:title) { '' }
@ -187,7 +139,7 @@ RSpec.describe 'Creating a Snippet' do
end end
context 'when there non ActiveRecord errors' do context 'when there non ActiveRecord errors' do
let(:file_name) { 'invalid://file/path' } let(:file_1) { { filePath: 'invalid://file/path', content: 'foobar' }}
it_behaves_like 'a mutation that returns errors in the response', errors: ['Repository Error creating the snippet - Invalid file name'] it_behaves_like 'a mutation that returns errors in the response', errors: ['Repository Error creating the snippet - Invalid file name']
it_behaves_like 'does not create snippet' it_behaves_like 'does not create snippet'

View file

@ -12,18 +12,20 @@ RSpec.describe 'Updating a Snippet' do
let(:updated_content) { 'Updated content' } let(:updated_content) { 'Updated content' }
let(:updated_description) { 'Updated description' } let(:updated_description) { 'Updated description' }
let(:updated_title) { 'Updated_title' } let(:updated_title) { 'Updated_title' }
let(:updated_file_name) { 'Updated file_name' }
let(:current_user) { snippet.author } let(:current_user) { snippet.author }
let(:updated_file) { 'CHANGELOG' }
let(:deleted_file) { 'README' }
let(:snippet_gid) { GitlabSchema.id_from_object(snippet).to_s } let(:snippet_gid) { GitlabSchema.id_from_object(snippet).to_s }
let(:mutation_vars) do let(:mutation_vars) do
{ {
id: snippet_gid, id: snippet_gid,
content: updated_content,
description: updated_description, description: updated_description,
visibility_level: 'public', visibility_level: 'public',
file_name: updated_file_name, title: updated_title,
title: updated_title blob_actions: [
{ action: :update, filePath: updated_file, content: updated_content },
{ action: :delete, filePath: deleted_file }
]
} }
end end
@ -50,21 +52,32 @@ RSpec.describe 'Updating a Snippet' do
end end
context 'when the user has permission' do context 'when the user has permission' do
it 'updates the Snippet' do it 'updates the snippet record' do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
expect(snippet.reload.title).to eq(updated_title) expect(snippet.reload.title).to eq(updated_title)
end end
it 'returns the updated Snippet' do it 'updates the Snippet' do
blob_to_update = blob_at(updated_file)
blob_to_delete = blob_at(deleted_file)
expect(blob_to_update.data).not_to eq updated_content
expect(blob_to_delete).to be_present
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['blob']['richData']).to be_nil blob_to_update = blob_at(updated_file)
expect(mutation_response['snippet']['blob']['plainData']).to match(updated_content) blob_to_delete = blob_at(deleted_file)
expect(mutation_response['snippet']['title']).to eq(updated_title)
expect(mutation_response['snippet']['description']).to eq(updated_description) aggregate_failures do
expect(mutation_response['snippet']['fileName']).to eq(updated_file_name) expect(blob_to_update.data).to eq updated_content
expect(mutation_response['snippet']['visibilityLevel']).to eq('public') expect(blob_to_delete).to be_nil
expect(blob_in_mutation_response(updated_file)['plainData']).to match(updated_content)
expect(mutation_response['snippet']['title']).to eq(updated_title)
expect(mutation_response['snippet']['description']).to eq(updated_description)
expect(mutation_response['snippet']['visibilityLevel']).to eq('public')
end
end end
context 'when there are ActiveRecord validation errors' do context 'when there are ActiveRecord validation errors' do
@ -79,16 +92,29 @@ RSpec.describe 'Updating a Snippet' do
end end
it 'returns the Snippet with its original values' do it 'returns the Snippet with its original values' do
blob_to_update = blob_at(updated_file)
blob_to_delete = blob_at(deleted_file)
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['snippet']['blob']['richData']).to be_nil aggregate_failures do
expect(mutation_response['snippet']['blob']['plainData']).to match(original_content) expect(blob_at(updated_file).data).to eq blob_to_update.data
expect(mutation_response['snippet']['title']).to eq(original_title) expect(blob_at(deleted_file).data).to eq blob_to_delete.data
expect(mutation_response['snippet']['description']).to eq(original_description) expect(blob_in_mutation_response(deleted_file)['plainData']).not_to be_nil
expect(mutation_response['snippet']['fileName']).to eq(original_file_name) expect(mutation_response['snippet']['title']).to eq(original_title)
expect(mutation_response['snippet']['visibilityLevel']).to eq('private') expect(mutation_response['snippet']['description']).to eq(original_description)
expect(mutation_response['snippet']['visibilityLevel']).to eq('private')
end
end end
end end
def blob_in_mutation_response(filename)
mutation_response['snippet']['blobs'].select { |blob| blob['name'] == filename }[0]
end
def blob_at(filename)
snippet.repository.blob_at('HEAD', filename)
end
end end
end end
@ -96,6 +122,7 @@ RSpec.describe 'Updating a Snippet' do
let(:snippet) do let(:snippet) do
create(:personal_snippet, create(:personal_snippet,
:private, :private,
:repository,
file_name: original_file_name, file_name: original_file_name,
title: original_title, title: original_title,
content: original_content, content: original_content,
@ -111,6 +138,7 @@ RSpec.describe 'Updating a Snippet' do
let(:snippet) do let(:snippet) do
create(:project_snippet, create(:project_snippet,
:private, :private,
:repository,
project: project, project: project,
author: create(:user), author: create(:user),
file_name: original_file_name, file_name: original_file_name,
@ -149,40 +177,4 @@ RSpec.describe 'Updating a Snippet' do
it_behaves_like 'when the snippet is not found' it_behaves_like 'when the snippet is not found'
end end
context 'when using the files params' do
let!(:snippet) { create(:personal_snippet, :private, :repository) }
let(:updated_content) { 'updated_content' }
let(:updated_file) { 'CHANGELOG' }
let(:deleted_file) { 'README' }
let(:mutation_vars) do
{
id: snippet_gid,
blob_actions: [
{ action: :update, filePath: updated_file, content: updated_content },
{ action: :delete, filePath: deleted_file }
]
}
end
it 'updates the Snippet' do
blob_to_update = blob_at(updated_file)
expect(blob_to_update.data).not_to eq updated_content
blob_to_delete = blob_at(deleted_file)
expect(blob_to_delete).to be_present
post_graphql_mutation(mutation, current_user: current_user)
blob_to_update = blob_at(updated_file)
expect(blob_to_update.data).to eq updated_content
blob_to_delete = blob_at(deleted_file)
expect(blob_to_delete).to be_nil
end
def blob_at(filename)
snippet.repository.blob_at('HEAD', filename)
end
end
end end

View file

@ -22,7 +22,7 @@ RSpec.describe Ci::RetryBuildService do
described_class.new(project, user) described_class.new(project, user)
end end
clone_accessors = described_class::CLONE_ACCESSORS clone_accessors = described_class.clone_accessors
reject_accessors = reject_accessors =
%i[id status user token token_encrypted coverage trace runner %i[id status user token token_encrypted coverage trace runner
@ -143,6 +143,8 @@ RSpec.describe Ci::RetryBuildService do
Ci::Build.reflect_on_all_associations.map(&:name) + Ci::Build.reflect_on_all_associations.map(&:name) +
[:tag_list, :needs_attributes] [:tag_list, :needs_attributes]
current_accessors << :secrets if Gitlab.ee?
current_accessors.uniq! current_accessors.uniq!
expect(current_accessors).to include(*processed_accessors) expect(current_accessors).to include(*processed_accessors)