Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-31 12:10:02 +00:00
parent 9c15dfa1ef
commit 2f2c8f84bf
58 changed files with 1564 additions and 1084 deletions

View File

@ -168,7 +168,7 @@ export default {
};
},
error() {
this.errored = true;
this.hasError = true;
},
},
alertsCount: {
@ -187,10 +187,9 @@ export default {
data() {
return {
searchTerm: '',
errored: false,
hasError: false,
errorMessage: '',
isAlertDismissed: false,
isErrorAlertDismissed: false,
sort: 'STARTED_AT_DESC',
statusFilter: [],
filteredByStatus: '',
@ -203,16 +202,13 @@ export default {
computed: {
showNoAlertsMsg() {
return (
!this.errored &&
!this.hasError &&
!this.loading &&
this.alertsCount?.all === 0 &&
!this.searchTerm &&
!this.isAlertDismissed
);
},
showErrorMsg() {
return this.errored && !this.isErrorAlertDismissed;
},
loading() {
return this.$apollo.queries.alerts.loading;
},
@ -306,11 +302,11 @@ export default {
};
},
handleAlertError(errorMessage) {
this.errored = true;
this.hasError = true;
this.errorMessage = errorMessage;
},
dismissError() {
this.isErrorAlertDismissed = true;
this.hasError = false;
this.errorMessage = '';
},
},
@ -332,12 +328,7 @@ export default {
</template>
</gl-sprintf>
</gl-alert>
<gl-alert
v-if="showErrorMsg"
variant="danger"
data-testid="alert-error"
@dismiss="dismissError"
>
<gl-alert v-if="hasError" variant="danger" data-testid="alert-error" @dismiss="dismissError">
<p v-html="errorMessage || $options.i18n.errorMsg"></p>
</gl-alert>

View File

@ -11,13 +11,15 @@ import {
GlSearchBoxByType,
GlIcon,
GlPagination,
GlTabs,
GlTab,
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { debounce, trim } from 'lodash';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { s__ } from '~/locale';
import { mergeUrlParams, joinPaths, visitUrl } from '~/lib/utils/url_utility';
import getIncidents from '../graphql/queries/get_incidents.query.graphql';
import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY } from '../constants';
import { I18N, DEFAULT_PAGE_SIZE, INCIDENT_SEARCH_DELAY, INCIDENT_STATE_TABS } from '../constants';
const tdClass =
'table-col gl-display-flex d-md-table-cell gl-align-items-center gl-white-space-nowrap';
@ -35,6 +37,7 @@ const initialPaginationState = {
export default {
i18n: I18N,
stateTabs: INCIDENT_STATE_TABS,
fields: [
{
key: 'title',
@ -67,6 +70,8 @@ export default {
GlSearchBoxByType,
GlIcon,
GlPagination,
GlTabs,
GlTab,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -78,6 +83,7 @@ export default {
variables() {
return {
searchTerm: this.searchTerm,
state: this.stateFilter,
projectPath: this.projectPath,
labelNames: ['incident'],
firstPageSize: this.pagination.firstPageSize,
@ -105,6 +111,7 @@ export default {
searchTerm: '',
pagination: initialPaginationState,
incidents: {},
stateFilter: '',
};
},
computed: {
@ -138,14 +145,17 @@ export default {
return mergeUrlParams({ issuable_template: this.incidentTemplateName }, this.newIssuePath);
},
},
watch: {
searchTerm: debounce(function debounceSearch(input) {
if (input !== this.searchTerm) {
this.searchTerm = input;
methods: {
onInputChange: debounce(function debounceSearch(input) {
const trimmedInput = trim(input);
if (trimmedInput !== this.searchTerm) {
this.searchTerm = trimmedInput;
}
}, INCIDENT_SEARCH_DELAY),
},
methods: {
filterIncidentsByState(tabIndex) {
const { filters } = this.$options.stateTabs[tabIndex];
this.stateFilter = filters;
},
hasAssignees(assignees) {
return Boolean(assignees.nodes?.length);
},
@ -183,9 +193,17 @@ export default {
{{ $options.i18n.errorMsg }}
</gl-alert>
<div class="gl-display-flex gl-justify-content-end">
<div class="incident-management-list-header gl-display-flex gl-justify-content-space-between">
<gl-tabs content-class="gl-p-0" @input="filterIncidentsByState">
<gl-tab v-for="tab in $options.stateTabs" :key="tab.state" :data-testid="tab.state">
<template #title>
<span>{{ tab.title }}</span>
</template>
</gl-tab>
</gl-tabs>
<gl-button
class="gl-mt-3 gl-mb-3 create-incident-button"
class="gl-my-3 create-incident-button"
data-testid="createIncidentBtn"
:loading="redirecting"
:disabled="redirecting"
@ -200,9 +218,10 @@ export default {
<div class="gl-bg-gray-10 gl-p-5 gl-border-b-solid gl-border-b-1 gl-border-gray-100">
<gl-search-box-by-type
v-model.trim="searchTerm"
:value="searchTerm"
class="gl-bg-white"
:placeholder="$options.i18n.searchPlaceholder"
@input="onInputChange"
/>
</div>
@ -221,7 +240,7 @@ export default {
@row-clicked="navigateToIncidentDetails"
>
<template #cell(title)="{ item }">
<div class="gl-display-flex gl-justify-content-center">
<div class="gl-display-sm-flex gl-align-items-center">
<div class="gl-max-w-full text-truncate" :title="item.title">{{ item.title }}</div>
<gl-icon
v-if="item.state === 'closed'"

View File

@ -8,5 +8,23 @@ export const I18N = {
searchPlaceholder: __('Search or filter results...'),
};
export const INCIDENT_STATE_TABS = [
{
title: s__('IncidentManagement|Open'),
state: 'OPENED',
filters: 'opened',
},
{
title: s__('IncidentManagement|Closed'),
state: 'CLOSED',
filters: 'closed',
},
{
title: s__('IncidentManagement|All incidents'),
state: 'ALL',
filters: 'all',
},
];
export const INCIDENT_SEARCH_DELAY = 300;
export const DEFAULT_PAGE_SIZE = 10;

View File

@ -80,31 +80,6 @@
}
}
.fa-stack {
position: relative;
display: inline-block;
width: 2em;
height: 2em;
line-height: 2em;
vertical-align: middle;
}
.fa-stack-1x,
.fa-stack-2x {
position: absolute;
left: 0;
width: 100%;
text-align: center;
}
.fa-stack-1x {
line-height: inherit;
}
.fa-stack-2x {
font-size: 2em;
}
.fa-inverse {
color: $white;
}
@ -256,10 +231,6 @@
content: '\f111';
}
.fa-certificate::before {
content: '\f0a3';
}
.fa-bitbucket::before {
content: '\f171';
}

View File

@ -90,6 +90,10 @@
}
@include media-breakpoint-down(xs) {
.incident-management-list-header {
flex-direction: column-reverse;
};
.create-incident-button {
@include gl-w-full;
}

View File

@ -36,7 +36,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:multiline_comments, @project)
push_frontend_feature_flag(:file_identifier_hash)
push_frontend_feature_flag(:batch_suggestions, @project, default_enabled: true)
push_frontend_feature_flag(:auto_expand_collapsed_diffs, @project)
push_frontend_feature_flag(:auto_expand_collapsed_diffs, @project, default_enabled: true)
push_frontend_feature_flag(:hide_jump_to_next_unresolved_in_threads, @project)
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module Mutations
module ResolvesSubscription
extend ActiveSupport::Concern
included do
argument :subscribed_state,
GraphQL::BOOLEAN_TYPE,
required: true,
description: 'The desired state of the subscription'
end
def resolve(project_path:, iid:, subscribed_state:)
resource = authorized_find!(project_path: project_path, iid: iid)
project = resource.project
resource.set_subscription(current_user, subscribed_state, project)
{
resource.class.name.underscore.to_sym => resource,
errors: errors_on_object(resource)
}
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module Mutations
module Issues
class SetSubscription < Base
graphql_name 'IssueSetSubscription'
include ResolvesSubscription
end
end
end

View File

@ -5,22 +5,7 @@ module Mutations
class SetSubscription < Base
graphql_name 'MergeRequestSetSubscription'
argument :subscribed_state,
GraphQL::BOOLEAN_TYPE,
required: true,
description: 'The desired state of the subscription'
def resolve(project_path:, iid:, subscribed_state:)
merge_request = authorized_find!(project_path: project_path, iid: iid)
project = merge_request.project
merge_request.set_subscription(current_user, subscribed_state, project)
{
merge_request: merge_request,
errors: errors_on_object(merge_request)
}
end
include ResolvesSubscription
end
end
end

View File

@ -8,5 +8,6 @@ module Types
value 'opened'
value 'closed'
value 'locked'
value 'all'
end
end

View File

@ -20,6 +20,7 @@ module Types
mount_mutation Mutations::Issues::SetConfidential
mount_mutation Mutations::Issues::SetLocked
mount_mutation Mutations::Issues::SetDueDate
mount_mutation Mutations::Issues::SetSubscription
mount_mutation Mutations::Issues::Update
mount_mutation Mutations::MergeRequests::Create
mount_mutation Mutations::MergeRequests::Update

View File

@ -205,7 +205,7 @@ module IssuablesHelper
author_output
end
output << content_tag(:span, (issuable_first_contribution_icon if issuable.first_contribution?), class: 'has-tooltip gl-ml-2', title: _('1st contribution!'))
output << content_tag(:span, (sprite_icon('first-contribution', size: 16, css_class: 'gl-icon gl-vertical-align-middle') if issuable.first_contribution?), class: 'has-tooltip gl-ml-2', title: _('1st contribution!'))
output << content_tag(:span, (issuable.task_status if issuable.tasks?), id: "task_status", class: "d-none d-sm-none d-md-inline-block gl-ml-3")
output << content_tag(:span, (issuable.task_status_short if issuable.tasks?), id: "task_status_short", class: "d-md-none")
@ -247,13 +247,6 @@ module IssuablesHelper
html.html_safe
end
def issuable_first_contribution_icon
content_tag(:span, class: 'fa-stack') do
concat(icon('certificate', class: "fa-stack-2x"))
concat(content_tag(:strong, '1', class: 'fa-inverse fa-stack-1x'))
end
end
def assigned_issuables_count(issuable_type)
case issuable_type
when :issues

View File

@ -1801,7 +1801,6 @@ class Project < ApplicationRecord
return unless namespace
mark_pages_as_not_deployed unless destroyed?
::Projects::UpdatePagesConfigurationService.new(self).execute
# 1. We rename pages to temporary directory
# 2. We wait 5 minutes, due to NFS caching

View File

@ -33,7 +33,7 @@ module Metrics
def from_cache(project_id, user_id, grafana_url)
project = Project.find(project_id)
user = User.find(user_id)
user = User.find(user_id) if user_id.present?
new(project, user, grafana_url: grafana_url)
end
@ -56,7 +56,7 @@ module Metrics
end
def cache_key(*args)
[project.id, current_user.id, grafana_url]
[project.id, current_user&.id, grafana_url]
end
# Required for ReactiveCaching; Usage overridden by

View File

@ -1,7 +1,7 @@
- access = note_max_access_for_user(note)
- if note.has_special_role?(Note::SpecialRole::FIRST_TIME_CONTRIBUTOR)
%span.note-role.note-role-special.has-tooltip{ title: _("This is the author's first Merge Request to this project.") }
= issuable_first_contribution_icon
= sprite_icon('first-contribution', size: 16, css_class: 'gl-icon gl-vertical-align-top')
- if access.nonzero?
%span.note-role.user-access-role= Gitlab::Access.human_access(access)

File diff suppressed because it is too large Load Diff

View File

@ -33,7 +33,10 @@ class GitGarbageCollectWorker # rubocop:disable Scalability/IdempotentWorker
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
project.repository.expire_statistics_caches if task != :pack_refs
if task != :pack_refs
project.repository.expire_statistics_caches
Projects::UpdateStatisticsService.new(project, nil, statistics: [:repository_size, :lfs_objects_size]).execute
end
# In case pack files are deleted, release libgit2 cache and open file
# descriptors ASAP instead of waiting for Ruby garbage collection

View File

@ -4,7 +4,7 @@
class PropagateServiceTemplateWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
feature_category :source_code_management
feature_category :integrations
LEASE_TIMEOUT = 4.hours.to_i

View File

@ -0,0 +1,5 @@
---
title: Allow anonymous users to view embedded Grafana metrics in public project
merge_request: 37844
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Replace fa-certificate icon with first-contribution svg
merge_request: 38154
author:
type: changed

View File

@ -0,0 +1,6 @@
---
title: Immediately update project statistics when running housekeeping or repository
cleanup
merge_request: 37579
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Add incident state columns
merge_request: 37889
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Allows setting of issue subscribe status in GraphQL API.
merge_request: 38051
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Fix 500 for pipeline charts page
merge_request: 38226
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Auto expand collapsed diffs when viewing diffs file-by-file
merge_request: 38296
author:
type: added

View File

@ -14,10 +14,10 @@ if ENV['ENABLE_SIDEKIQ_CLUSTER']
if Process.ppid != parent
Process.kill(:TERM, Process.pid)
# Wait for just a few extra seconds for a final attempt to
# gracefully terminate. Considering the parent (cluster) process
# have changed (SIGKILL'd), it shouldn't take long to shutdown.
sleep(5)
# Allow sidekiq to cleanly terminate and push any running jobs back
# into the queue. We use the configured timeout and add a small
# grace period
sleep(Sidekiq.options[:timeout] + 5)
# Signaling the Sidekiq Pgroup as KILL is not forwarded to
# a possible child process. In Sidekiq Cluster, all child Sidekiq

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
class ReplaceUniqueIndexOnCycleAnalyticsStages < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
OLD_INDEX_NAME = 'index_analytics_ca_group_stages_on_group_id_and_name'
NEW_INDEX_NAME = 'index_group_stages_on_group_id_group_value_stream_id_and_name'
disable_ddl_transaction!
def up
add_concurrent_index(:analytics_cycle_analytics_group_stages,
[:group_id, :group_value_stream_id, :name],
unique: true,
name: NEW_INDEX_NAME)
remove_concurrent_index_by_name :analytics_cycle_analytics_group_stages, OLD_INDEX_NAME
end
def down
# Removing duplicated records (group_id, name) that would prevent re-creating the old index.
execute <<-SQL
DELETE FROM analytics_cycle_analytics_group_stages
USING (
SELECT group_id, name, MIN(id) as min_id
FROM analytics_cycle_analytics_group_stages
GROUP BY group_id, name
HAVING COUNT(id) > 1
) as analytics_cycle_analytics_group_stages_name_duplicates
WHERE analytics_cycle_analytics_group_stages_name_duplicates.group_id = analytics_cycle_analytics_group_stages.group_id
AND analytics_cycle_analytics_group_stages_name_duplicates.name = analytics_cycle_analytics_group_stages.name
AND analytics_cycle_analytics_group_stages_name_duplicates.min_id <> analytics_cycle_analytics_group_stages.id
SQL
add_concurrent_index(:analytics_cycle_analytics_group_stages,
[:group_id, :name],
unique: true,
name: OLD_INDEX_NAME)
remove_concurrent_index_by_name :analytics_cycle_analytics_group_stages, NEW_INDEX_NAME
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
class AddIndexToCiPipelineProjectIdCreatedAt < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :ci_pipelines, [:project_id, :created_at]
end
def down
remove_concurrent_index :ci_pipelines, [:project_id, :created_at]
end
end

View File

@ -0,0 +1 @@
546555a009e8923ea8b976ce38d882d387407fb03e7bbcb9c760df53bafd1f91

View File

@ -0,0 +1 @@
2976f459ac9cd0780e90077ebe4ce5ca8dc41e62b4dab1f96e39738624ad9d04

View File

@ -18880,8 +18880,6 @@ CREATE INDEX index_analytics_ca_group_stages_on_end_event_label_id ON public.ana
CREATE INDEX index_analytics_ca_group_stages_on_group_id ON public.analytics_cycle_analytics_group_stages USING btree (group_id);
CREATE UNIQUE INDEX index_analytics_ca_group_stages_on_group_id_and_name ON public.analytics_cycle_analytics_group_stages USING btree (group_id, name);
CREATE INDEX index_analytics_ca_group_stages_on_relative_position ON public.analytics_cycle_analytics_group_stages USING btree (relative_position);
CREATE INDEX index_analytics_ca_group_stages_on_start_event_label_id ON public.analytics_cycle_analytics_group_stages USING btree (start_event_label_id);
@ -19150,6 +19148,8 @@ CREATE INDEX index_ci_pipelines_on_merge_request_id ON public.ci_pipelines USING
CREATE INDEX index_ci_pipelines_on_pipeline_schedule_id ON public.ci_pipelines USING btree (pipeline_schedule_id);
CREATE INDEX index_ci_pipelines_on_project_id_and_created_at ON public.ci_pipelines USING btree (project_id, created_at);
CREATE INDEX index_ci_pipelines_on_project_id_and_id_desc ON public.ci_pipelines USING btree (project_id, id DESC);
CREATE UNIQUE INDEX index_ci_pipelines_on_project_id_and_iid ON public.ci_pipelines USING btree (project_id, iid) WHERE (iid IS NOT NULL);
@ -19654,6 +19654,8 @@ CREATE INDEX index_group_group_links_on_shared_with_group_id ON public.group_gro
CREATE INDEX index_group_import_states_on_group_id ON public.group_import_states USING btree (group_id);
CREATE UNIQUE INDEX index_group_stages_on_group_id_group_value_stream_id_and_name ON public.analytics_cycle_analytics_group_stages USING btree (group_id, group_value_stream_id, name);
CREATE UNIQUE INDEX index_group_wiki_repositories_on_disk_path ON public.group_wiki_repositories USING btree (disk_path);
CREATE INDEX index_group_wiki_repositories_on_shard_id ON public.group_wiki_repositories USING btree (shard_id);

View File

@ -5861,6 +5861,7 @@ type InstanceSecurityDashboard {
State of a GitLab issue or merge request
"""
enum IssuableState {
all
closed
locked
opened
@ -6433,6 +6434,51 @@ type IssueSetLockedPayload {
issue: Issue
}
"""
Autogenerated input type of IssueSetSubscription
"""
input IssueSetSubscriptionInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The IID of the issue to mutate
"""
iid: String!
"""
The project the issue to mutate is in
"""
projectPath: ID!
"""
The desired state of the subscription
"""
subscribedState: Boolean!
}
"""
Autogenerated return type of IssueSetSubscription
"""
type IssueSetSubscriptionPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
The issue after mutation
"""
issue: Issue
}
"""
Autogenerated input type of IssueSetWeight
"""
@ -6562,6 +6608,7 @@ enum IssueSort {
State of a GitLab issue
"""
enum IssueState {
all
closed
locked
opened
@ -7987,6 +8034,7 @@ type MergeRequestSetWipPayload {
State of a GitLab merge request
"""
enum MergeRequestState {
all
closed
locked
merged
@ -8351,6 +8399,7 @@ type Mutation {
issueSetDueDate(input: IssueSetDueDateInput!): IssueSetDueDatePayload
issueSetIteration(input: IssueSetIterationInput!): IssueSetIterationPayload
issueSetLocked(input: IssueSetLockedInput!): IssueSetLockedPayload
issueSetSubscription(input: IssueSetSubscriptionInput!): IssueSetSubscriptionPayload
issueSetWeight(input: IssueSetWeightInput!): IssueSetWeightPayload
jiraImportStart(input: JiraImportStartInput!): JiraImportStartPayload
jiraImportUsers(input: JiraImportUsersInput!): JiraImportUsersPayload

View File

@ -16186,6 +16186,12 @@
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "all",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
@ -17852,6 +17858,136 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "IssueSetSubscriptionInput",
"description": "Autogenerated input type of IssueSetSubscription",
"fields": null,
"inputFields": [
{
"name": "projectPath",
"description": "The project the issue to mutate is in",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "iid",
"description": "The IID of the issue to mutate",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "subscribedState",
"description": "The desired state of the subscription",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"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": "IssueSetSubscriptionPayload",
"description": "Autogenerated return type of IssueSetSubscription",
"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": "issue",
"description": "The issue after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Issue",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "IssueSetWeightInput",
@ -18108,6 +18244,12 @@
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "all",
"description": null,
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
@ -22394,6 +22536,12 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "all",
"description": null,
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "merged",
"description": null,
@ -24357,6 +24505,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issueSetSubscription",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "IssueSetSubscriptionInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "IssueSetSubscriptionPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "issueSetWeight",
"description": null,

View File

@ -985,6 +985,16 @@ Autogenerated return type of IssueSetLocked
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue after mutation |
## IssueSetSubscriptionPayload
Autogenerated return type of IssueSetSubscription
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `issue` | Issue | The issue after mutation |
## IssueSetWeightPayload
Autogenerated return type of IssueSetWeight

View File

@ -230,7 +230,7 @@ Instead these should be sent to the [Release Manager](https://about.gitlab.com/c
- Ask for clarification. ("I didn't understand. Can you clarify?")
- Avoid selective ownership of code. ("mine", "not mine", "yours")
- Avoid using terms that could be seen as referring to personal traits. ("dumb",
"stupid"). Assume everyone is attractive, intelligent, and well-meaning.
"stupid"). Assume everyone is intelligent and well-meaning.
- Be explicit. Remember people don't always understand your intentions online.
- Be humble. ("I'm not sure - let's look it up.")
- Don't use hyperbole. ("always", "never", "endlessly", "nothing")

View File

@ -481,7 +481,7 @@ We treat documentation as code, and so use tests in our CI pipeline to maintain
standards and quality of the docs. The current tests, which run in CI jobs when a
merge request with new or changed docs is submitted, are:
- [`docs lint`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/docs.gitlab-ci.yml#L48):
- [`docs lint`](https://gitlab.com/gitlab-org/gitlab/-/blob/0b562014f7b71f98540e682c8d662275f0011f2f/.gitlab/ci/docs.gitlab-ci.yml#L41):
Runs several tests on the content of the docs themselves:
- [`lint-doc.sh` script](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/lint-doc.sh)
runs the following checks and linters:
@ -492,33 +492,20 @@ merge request with new or changed docs is submitted, are:
- [markdownlint](#markdownlint).
- [Vale](#vale).
- Nanoc tests:
- [`internal_links`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/docs.gitlab-ci.yml#L67)
- [`internal_links`](https://gitlab.com/gitlab-org/gitlab/-/blob/0b562014f7b71f98540e682c8d662275f0011f2f/.gitlab/ci/docs.gitlab-ci.yml#L58)
checks that all internal links (ex: `[link](../index.md)`) are valid.
- [`internal_anchors`](https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab/ci/docs.gitlab-ci.yml#L69)
- [`internal_anchors`](https://gitlab.com/gitlab-org/gitlab/-/blob/0b562014f7b71f98540e682c8d662275f0011f2f/.gitlab/ci/docs.gitlab-ci.yml#L60)
checks that all internal anchors (ex: `[link](../index.md#internal_anchor)`)
are valid.
- [`ui-docs-links lint`](https://gitlab.com/gitlab-org/gitlab/-/blob/0b562014f7b71f98540e682c8d662275f0011f2f/.gitlab/ci/docs.gitlab-ci.yml#L62)
checks that all links to docs from UI elements (`app/views` files, for example)
are linking to valid docs and anchors.
### Running tests
### Run tests locally
Apart from [previewing your changes locally](#previewing-the-changes-live), you can also run all lint checks
and Nanoc tests locally.
#### Nanoc tests
To execute Nanoc tests locally:
1. Navigate to the [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs) directory.
1. Run:
```shell
# Check for broken internal links
bundle exec nanoc check internal_links
# Check for broken external links (might take a lot of time to complete).
# This test is set to be allowed to fail and is run only in the gitlab-docs project CI
bundle exec nanoc check internal_anchors
```
#### Lint checks
Lint checks are performed by the [`lint-doc.sh`](https://gitlab.com/gitlab-org/gitlab/blob/master/scripts/lint-doc.sh)
@ -550,6 +537,57 @@ The output should be similar to:
Note that this requires you to either have the required lint tools installed on your machine,
or a working Docker installation, in which case an image with these tools pre-installed will be used.
#### Nanoc tests
To execute Nanoc tests locally:
1. Navigate to the [`gitlab-docs`](https://gitlab.com/gitlab-org/gitlab-docs) directory.
1. Run:
```shell
# Check for broken internal links
bundle exec nanoc check internal_links
# Check for broken external links (might take a lot of time to complete).
# This test is set to be allowed to fail and is run only in the gitlab-docs project CI
bundle exec nanoc check internal_anchors
```
#### `ui-docs-links` test
The `ui-docs-links lint` job uses `haml-lint` to test that all links to docs from
UI elements (`app/views` files, for example) are linking to valid docs and anchors.
To run the `ui-docs-links` test locally:
1. Open the `gitlab` directory in a terminal window.
1. Run:
```shell
bundle exec haml-lint -i DocumentationLinks
```
If you receive an error the first time you run this test, run `bundle install`, which
installs GitLab's dependencies, and try again.
If you don't want to install all of GitLab's dependencies to test the links, you can:
1. Open the `gitlab` directory in a terminal window.
1. Install `haml-lint`:
```shell
gem install haml_lint
```
1. Run:
```shell
haml-lint -i DocumentationLinks
```
If you manually install `haml-lint` with this process, it will not update automatically
and you should make sure your version matches the version used by GitLab.
### Local linters
To help adhere to the [documentation style guidelines](styleguide.md), and improve the content

View File

@ -85,6 +85,7 @@ Default client accepts two parameters: `resolvers` and `config`.
- `cacheConfig` field accepts an optional object of settings to [customize Apollo cache](https://www.apollographql.com/docs/react/caching/cache-configuration/#configuring-the-cache)
- `baseUrl` allows us to pass a URL for GraphQL endpoint different from our main endpoint (i.e.`${gon.relative_url_root}/api/graphql`)
- `assumeImmutableResults` (set to `false` by default) - this setting, when set to `true`, will assume that every single operation on updating Apollo Cache is immutable. It also sets `freezeResults` to `true`, so any attempt on mutating Apollo Cache will throw a console warning in development environment. Please ensure you're following the immutability pattern on cache update operations before setting this option to `true`.
- `fetchPolicy` determines how you want your component to interact with the Apollo cache. Defaults to "cache-first".
## GraphQL Queries
@ -167,9 +168,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
Vue.use(VueApollo);
const defaultClient = createDefaultClient({
resolvers: {}
});
const defaultClient = createDefaultClient();
defaultClient.cache.writeData({
data: {
@ -257,10 +256,7 @@ We need to pass resolvers object to our existing Apollo Client:
import createDefaultClient from '~/lib/graphql';
import resolvers from './graphql/resolvers';
const defaultClient = createDefaultClient(
{},
resolvers,
);
const defaultClient = createDefaultClient(resolvers);
```
Now every single time on attempt to fetch a version, our client will fetch `id` and `sha` from the remote API endpoint and will assign our hardcoded values to `author` and `createdAt` version properties. With this data, frontend developers are able to work on UI part without being blocked by backend. When actual response is added to the API, a custom local resolver can be removed fast and the only change to query/fragment is `@client` directive removal.

View File

@ -218,9 +218,9 @@ Project.select(:id, :user_id).joins(:merge_requests)
## Plucking IDs
This can't be stressed enough: **never** use ActiveRecord's `pluck` to pluck a
set of values into memory only to use them as an argument for another query. For
example, this will make the database **very** sad:
Never use ActiveRecord's `pluck` to pluck a set of values into memory only to
use them as an argument for another query. For example, this will execute an
extra unecessary database query and load a lot of unecessary data into memory:
```ruby
projects = Project.all.pluck(:id)

View File

@ -31,8 +31,6 @@ file path fragments to start seeing results.
## Syntax highlighting
> Support for `.gitlab-ci.yml` validation [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218472) in GitLab 13.2.
As expected from an IDE, syntax highlighting for many languages within
the Web IDE will make your direct editing even easier.
@ -44,14 +42,6 @@ The Web IDE currently provides:
- IntelliSense and validation support (displaying errors and warnings, providing
smart completions, formatting, and outlining) for some languages. For example:
TypeScript, JavaScript, CSS, LESS, SCSS, JSON, and HTML.
- Validation support for certain JSON and YAML files using schemas based on the
[JSON Schema Store](https://www.schemastore.org/json/). This feature
is only supported for the `.gitlab-ci.yml` file.
NOTE: **Note:**
Validation support based on schemas is hidden behind
the feature flag `:schema_linting` on self-managed installations. To enable the
feature, you can [turn on the feature flag in Rails console](../../../administration/feature_flags.md#how-to-enable-and-disable-features-behind-flags).
Because the Web IDE is based on the [Monaco Editor](https://microsoft.github.io/monaco-editor/),
you can find a more complete list of supported languages in the
@ -63,6 +53,37 @@ If you are missing Syntax Highlighting support for any language, we prepared a s
NOTE: **Note:**
Single file editing is based on the [Ace Editor](https://ace.c9.io).
### Schema based validation
> - Support for `.gitlab-ci.yml` validation [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218472) in GitLab 13.2.
> - It was deployed behind a feature flag, disabled by default.
> - It's enabled on GitLab.com.
> - It cannot be enabled or disabled per-project.
> - For GitLab self-managed instances, GitLab administrators can opt to [enable it](#enable-or-disable-schema-based-validation-core-only).
The Web IDE provides validation support for certain JSON and YAML files using schemas
based on the [JSON Schema Store](https://www.schemastore.org/json/). This feature is
only supported for the `.gitlab-ci.yml` file.
#### Enable or disable Schema based validation **(CORE ONLY)**
Schema based validation is under development and not ready for production use. It is
deployed behind a feature flag that is **disabled by default** for self-managed instances,
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
can enable it for your instance.
To enable it:
```ruby
Feature.enable(:schema_linting)
```
To disable it:
```ruby
Feature.disable(:schema_linting)
```
### Themes
> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in GitLab in 13.0.

View File

@ -8653,6 +8653,9 @@ msgstr ""
msgid "Edit Release"
msgstr ""
msgid "Edit Requirement"
msgstr ""
msgid "Edit Slack integration"
msgstr ""
@ -12713,9 +12716,15 @@ msgstr ""
msgid "Incident Management Limits"
msgstr ""
msgid "IncidentManagement|All incidents"
msgstr ""
msgid "IncidentManagement|Assignees"
msgstr ""
msgid "IncidentManagement|Closed"
msgstr ""
msgid "IncidentManagement|Create incident"
msgstr ""
@ -12731,6 +12740,9 @@ msgstr ""
msgid "IncidentManagement|No incidents to display."
msgstr ""
msgid "IncidentManagement|Open"
msgstr ""
msgid "IncidentManagement|There was an error displaying the incidents."
msgstr ""
@ -15803,6 +15815,9 @@ msgstr ""
msgid "New Project"
msgstr ""
msgid "New Requirement"
msgstr ""
msgid "New Snippet"
msgstr ""
@ -20335,9 +20350,6 @@ msgstr ""
msgid "Required in this project."
msgstr ""
msgid "Requirement"
msgstr ""
msgid "Requirement %{reference} has been added"
msgstr ""

View File

@ -217,7 +217,7 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures do
it_behaves_like 'all pipelines'
it 'includes custom filters' do
it 'includes custom filters', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/233077' do
aggregate_failures 'UploadLinkFilter' do
expect(doc).to parse_upload_links
end
@ -282,7 +282,7 @@ RSpec.describe 'GitLab Markdown', :aggregate_failures do
it_behaves_like 'all pipelines'
it 'includes custom filters' do
it 'includes custom filters', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/233077' do
aggregate_failures 'UploadLinkFilter' do
expect(doc).to parse_upload_links
end

View File

@ -2,9 +2,28 @@
require 'spec_helper'
RSpec.shared_examples_for 'snippet editor' do
RSpec.describe 'Projects > Snippets > Create Snippet', :js do
include DropzoneHelper
let_it_be(:user) { create(:user) }
let_it_be(:project) do
create(:project, :public, creator: user).tap do |p|
p.add_maintainer(user)
end
end
let(:title) { 'My Snippet Title' }
let(:file_content) { 'Hello World!' }
let(:md_description) { 'My Snippet **Description**' }
let(:description) { 'My Snippet Description' }
before do
stub_feature_flags(snippets_vue: false)
stub_feature_flags(snippets_edit_vue: false)
sign_in(user)
visit new_project_snippet_path(project)
end
def description_field
@ -12,137 +31,81 @@ RSpec.shared_examples_for 'snippet editor' do
end
def fill_form
fill_in 'project_snippet_title', with: 'My Snippet Title'
fill_in 'project_snippet_title', with: title
# Click placeholder first to expand full description field
description_field.click
fill_in 'project_snippet_description', with: 'My Snippet **Description**'
fill_in 'project_snippet_description', with: md_description
page.within('.file-editor') do
el = find('.inputarea')
el.send_keys 'Hello World!'
el.send_keys file_content
end
end
context 'when a user is authenticated' do
before do
stub_feature_flags(snippets_vue: false)
project.add_maintainer(user)
sign_in(user)
it 'shows collapsible description input' do
collapsed = description_field
visit project_snippets_path(project)
expect(page).not_to have_field('project_snippet_description')
expect(collapsed).to be_visible
# Wait for the SVG to ensure the button location doesn't shift
within('.empty-state') { find('img.js-lazy-loaded') }
click_on('New snippet')
wait_for_requests
end
collapsed.click
it 'shows collapsible description input' do
collapsed = description_field
expect(page).to have_field('project_snippet_description')
expect(collapsed).not_to be_visible
end
expect(page).not_to have_field('project_snippet_description')
expect(collapsed).to be_visible
it 'creates a new snippet' do
fill_form
click_button('Create snippet')
wait_for_requests
collapsed.click
expect(page).to have_field('project_snippet_description')
expect(collapsed).not_to be_visible
end
it 'creates a new snippet' do
fill_form
click_button('Create snippet')
wait_for_requests
expect(page).to have_content('My Snippet Title')
expect(page).to have_content('Hello World!')
page.within('.snippet-header .description') do
expect(page).to have_content('My Snippet Description')
expect(page).to have_selector('strong')
end
end
it 'uploads a file when dragging into textarea' do
fill_form
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(page.find_field("project_snippet_description").value).to have_content('banana_sample')
click_button('Create snippet')
wait_for_requests
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/#{Regexp.escape(project.full_path)}/uploads/\h{32}/banana_sample\.gif\z})
end
it 'creates a snippet when all required fields are filled in after validation failing' do
fill_in 'project_snippet_title', with: 'My Snippet Title'
click_button('Create snippet')
expect(page).to have_selector('#error_explanation')
fill_form
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
find("input[value='Create snippet']").send_keys(:return)
wait_for_requests
expect(page).to have_content('My Snippet Title')
expect(page).to have_content('Hello World!')
page.within('.snippet-header .description') do
expect(page).to have_content('My Snippet Description')
expect(page).to have_selector('strong')
end
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/#{Regexp.escape(project.full_path)}/uploads/\h{32}/banana_sample\.gif\z})
end
context 'when the git operation fails' do
let(:error) { 'Error creating the snippet' }
before do
allow_next_instance_of(Snippets::CreateService) do |instance|
allow(instance).to receive(:create_commit).and_raise(StandardError, error)
end
fill_form
click_button('Create snippet')
wait_for_requests
end
it 'displays the error' do
expect(page).to have_content(error)
end
it 'renders new page' do
expect(page).to have_content('New Snippet')
end
expect(page).to have_content(title)
expect(page).to have_content(file_content)
page.within('.snippet-header .description') do
expect(page).to have_content(description)
expect(page).to have_selector('strong')
end
end
context 'when a user is not authenticated' do
it 'uploads a file when dragging into textarea' do
fill_form
dropzone_file Rails.root.join('spec', 'fixtures', 'banana_sample.gif')
expect(page.find_field('project_snippet_description').value).to have_content('banana_sample')
click_button('Create snippet')
wait_for_requests
link = find('a.no-attachment-icon img[alt="banana_sample"]')['src']
expect(link).to match(%r{/#{Regexp.escape(project.full_path)}/uploads/\h{32}/banana_sample\.gif\z})
end
it 'displays validation errors' do
fill_in 'project_snippet_title', with: title
click_button('Create snippet')
wait_for_requests
expect(page).to have_selector('#error_explanation')
end
context 'when the git operation fails' do
let(:error) { 'Error creating the snippet' }
before do
stub_feature_flags(snippets_vue: false)
allow_next_instance_of(Snippets::CreateService) do |instance|
allow(instance).to receive(:create_commit).and_raise(StandardError, error)
end
fill_form
click_button('Create snippet')
wait_for_requests
end
it 'shows a public snippet on the index page but not the New snippet button' do
snippet = create(:project_snippet, :public, :repository, project: project)
visit project_snippets_path(project)
expect(page).to have_content(snippet.title)
expect(page).not_to have_content('New snippet')
it 'renders the new page and displays the error' do
expect(page).to have_content(error)
expect(page).to have_content('New Snippet')
end
end
end
RSpec.describe 'Projects > Snippets > Create Snippet', :js do
include DropzoneHelper
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public) }
it_behaves_like "snippet editor"
end

View File

@ -3,157 +3,41 @@
require 'spec_helper'
RSpec.describe 'Projects > Snippets > Project snippet', :js do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:snippet) { create(:project_snippet, project: project, file_name: file_name, content: content) }
let_it_be(:user) { create(:user) }
let_it_be(:project) do
create(:project, creator: user).tap do |p|
p.add_maintainer(user)
end
end
let_it_be(:snippet) { create(:project_snippet, :repository, project: project, author: user) }
before do
stub_feature_flags(snippets_vue: false)
project.add_maintainer(user)
sign_in(user)
end
context 'Ruby file' do
let(:file_name) { 'popen.rb' }
let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data }
it_behaves_like 'show and render proper snippet blob' do
let(:anchor) { nil }
before do
visit project_snippet_path(project, snippet)
subject do
visit project_snippet_path(project, snippet, anchor: anchor)
wait_for_requests
end
it 'displays the blob' do
aggregate_failures do
# shows highlighted Ruby code
expect(page).to have_content("require 'fileutils'")
# does not show a viewer switcher
expect(page).not_to have_selector('.js-blob-viewer-switcher')
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
# shows a raw button
expect(page).to have_link('Open raw')
# shows a download button
expect(page).to have_link('Download')
end
end
end
context 'Markdown file' do
let(:file_name) { 'ruby-style-guide.md' }
let(:content) { project.repository.blob_at('master', 'files/markdown/ruby-style-guide.md').data }
context 'visiting directly' do
before do
visit project_snippet_path(project, snippet)
wait_for_requests
end
it 'displays the blob using the rich viewer' do
aggregate_failures do
# hides the simple viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
# shows rendered Markdown
expect(page).to have_link("PEP-8")
# shows a viewer switcher
expect(page).to have_selector('.js-blob-viewer-switcher')
# shows a disabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
# shows a raw button
expect(page).to have_link('Open raw')
# shows a download button
expect(page).to have_link('Download')
end
end
context 'switching to the simple viewer' do
before do
find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
wait_for_requests
end
it 'displays the blob using the simple viewer' do
aggregate_failures do
# hides the rich viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
# shows highlighted Markdown code
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
end
end
context 'switching to the rich viewer again' do
before do
find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
wait_for_requests
end
it 'displays the blob using the rich viewer' do
aggregate_failures do
# hides the simple viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
end
end
end
end
end
context 'visiting with a line number anchor' do
before do
visit project_snippet_path(project, snippet, anchor: 'L1')
wait_for_requests
end
it 'displays the blob using the simple viewer' do
aggregate_failures do
# hides the rich viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
# highlights the line in question
expect(page).to have_selector('#LC1.hll')
# shows highlighted Markdown code
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
end
end
end
end
it_behaves_like 'showing user status' do
let(:file_name) { 'ruby-style-guide.md' }
let(:content) { project.repository.blob_at('master', 'files/markdown/ruby-style-guide.md').data }
let(:file_path) { 'files/ruby/popen.rb' }
let(:user_with_status) { snippet.author }
subject do
visit project_snippet_path(project, snippet)
wait_for_requests
end
subject { visit project_snippet_path(project, snippet) }
end
it_behaves_like 'does not show New Snippet button' do
let(:file_path) { 'files/ruby/popen.rb' }
subject { visit project_snippet_path(project, snippet) }
end
end

View File

@ -3,180 +3,33 @@
require 'spec_helper'
RSpec.describe 'Snippet', :js do
let(:project) { create(:project, :repository) }
let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content) }
let_it_be(:user) { create(:user) }
let_it_be(:snippet) { create(:personal_snippet, :public, :repository, author: user) }
before do
stub_feature_flags(snippets_vue: false)
end
context 'Ruby file' do
let(:file_name) { 'popen.rb' }
let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data }
it_behaves_like 'show and render proper snippet blob' do
let(:anchor) { nil }
before do
visit snippet_path(snippet)
subject do
visit snippet_path(snippet, anchor: anchor)
wait_for_requests
end
it 'displays the blob' do
aggregate_failures do
# shows highlighted Ruby code
expect(page).to have_content("require 'fileutils'")
# does not show a viewer switcher
expect(page).not_to have_selector('.js-blob-viewer-switcher')
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
# shows a raw button
expect(page).to have_link('Open raw')
# shows a download button
expect(page).to have_link('Download')
end
end
end
context 'Markdown file' do
let(:file_name) { 'ruby-style-guide.md' }
let(:content) { project.repository.blob_at('master', 'files/markdown/ruby-style-guide.md').data }
context 'visiting directly' do
before do
visit snippet_path(snippet)
wait_for_requests
end
it 'displays the blob using the rich viewer' do
aggregate_failures do
# hides the simple viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
# shows rendered Markdown
expect(page).to have_link("PEP-8")
# shows a viewer switcher
expect(page).to have_selector('.js-blob-viewer-switcher')
# shows a disabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
# shows a raw button
expect(page).to have_link('Open raw')
# shows a download button
expect(page).to have_link('Download')
end
end
context 'Markdown rendering' do
let(:snippet) { create(:personal_snippet, :public, file_name: file_name, content: content) }
let(:file_name) { 'test.md' }
let(:content) { "1. one\n - sublist\n" }
context 'when rendering default markdown' do
it 'renders using CommonMark' do
expect(page).to have_content("sublist")
expect(page).not_to have_xpath("//ol//li//ul")
end
end
end
context 'switching to the simple viewer' do
before do
find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
wait_for_requests
end
it 'displays the blob using the simple viewer' do
aggregate_failures do
# hides the rich viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
# shows highlighted Markdown code
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
end
end
context 'switching to the rich viewer again' do
before do
find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
wait_for_requests
end
it 'displays the blob using the rich viewer' do
aggregate_failures do
# hides the simple viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
end
end
end
end
end
context 'visiting with a line number anchor' do
before do
visit snippet_path(snippet, anchor: 'L1')
wait_for_requests
end
it 'displays the blob using the simple viewer' do
aggregate_failures do
# hides the rich viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
# highlights the line in question
expect(page).to have_selector('#LC1.hll')
# shows highlighted Markdown code
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
end
end
end
end
it_behaves_like 'showing user status' do
let(:file_name) { 'popen.rb' }
let(:content) { project.repository.blob_at('master', 'files/ruby/popen.rb').data }
let(:file_path) { 'files/ruby/popen.rb' }
let(:user_with_status) { snippet.author }
subject { visit snippet_path(snippet) }
end
context 'when user cannot create snippets' do
let(:user) { create(:user, :external) }
let(:snippet) { create(:personal_snippet, :public) }
it_behaves_like 'does not show New Snippet button' do
let(:file_path) { 'files/ruby/popen.rb' }
before do
sign_in(user)
visit snippet_path(snippet)
wait_for_requests
end
it 'does not show the "New Snippet" button' do
expect(page).not_to have_link('New snippet')
end
subject { visit snippet_path(snippet) }
end
end

View File

@ -13,6 +13,7 @@ import {
GlSearchBoxByType,
} from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import waitForPromises from 'helpers/wait_for_promises';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import AlertManagementTable from '~/alert_management/components/alert_management_table.vue';
import { ALERTS_STATUS_TABS, trackAlertStatusUpdateOptions } from '~/alert_management/constants';
@ -44,6 +45,7 @@ describe('AlertManagementTable', () => {
const findPagination = () => wrapper.find(GlPagination);
const findSearch = () => wrapper.find(GlSearchBoxByType);
const findIssueFields = () => wrapper.findAll('[data-testid="issueField"]');
const findAlertError = () => wrapper.find('[data-testid="alert-error"]');
const alertsCount = {
open: 14,
triggered: 10,
@ -51,6 +53,11 @@ describe('AlertManagementTable', () => {
resolved: 1,
all: 16,
};
const selectFirstStatusOption = () => {
findFirstStatusOption().vm.$emit('click');
return waitForPromises();
};
function mountComponent({
props = {
@ -138,7 +145,7 @@ describe('AlertManagementTable', () => {
it('error state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { errors: ['error'] }, alertsCount: null, errored: true },
data: { alerts: { errors: ['error'] }, alertsCount: null, hasError: true },
loading: false,
});
expect(findAlertsTable().exists()).toBe(true);
@ -155,7 +162,7 @@ describe('AlertManagementTable', () => {
it('empty state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: [], pageInfo: {} }, alertsCount: { all: 0 }, errored: false },
data: { alerts: { list: [], pageInfo: {} }, alertsCount: { all: 0 }, hasError: false },
loading: false,
});
expect(findAlertsTable().exists()).toBe(true);
@ -172,7 +179,7 @@ describe('AlertManagementTable', () => {
it('has data state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
expect(findLoader().exists()).toBe(false);
@ -188,7 +195,7 @@ describe('AlertManagementTable', () => {
it('displays status dropdown', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
expect(findStatusDropdown().exists()).toBe(true);
@ -197,7 +204,7 @@ describe('AlertManagementTable', () => {
it('does not display a dropdown status header', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
expect(findStatusDropdown().contains('.dropdown-title')).toBe(false);
@ -206,7 +213,7 @@ describe('AlertManagementTable', () => {
it('shows correct severity icons', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
@ -223,7 +230,7 @@ describe('AlertManagementTable', () => {
it('renders severity text', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
@ -237,7 +244,7 @@ describe('AlertManagementTable', () => {
it('renders Unassigned when no assignee(s) present', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
@ -251,7 +258,7 @@ describe('AlertManagementTable', () => {
it('renders username(s) when assignee(s) present', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
@ -265,7 +272,7 @@ describe('AlertManagementTable', () => {
it('navigates to the detail page when alert row is clicked', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
@ -279,7 +286,7 @@ describe('AlertManagementTable', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
});
@ -323,7 +330,7 @@ describe('AlertManagementTable', () => {
],
},
alertsCount,
errored: false,
hasError: false,
},
loading: false,
});
@ -343,7 +350,7 @@ describe('AlertManagementTable', () => {
},
],
alertsCount,
errored: false,
hasError: false,
},
loading: false,
});
@ -358,7 +365,7 @@ describe('AlertManagementTable', () => {
it('should highlight the row when alert is new', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: [newAlert] }, alertsCount, errored: false },
data: { alerts: { list: [newAlert] }, alertsCount, hasError: false },
loading: false,
});
@ -372,7 +379,7 @@ describe('AlertManagementTable', () => {
it('should not highlight the row when alert is not new', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: [oldAlert] }, alertsCount, errored: false },
data: { alerts: { list: [oldAlert] }, alertsCount, hasError: false },
loading: false,
});
@ -392,7 +399,7 @@ describe('AlertManagementTable', () => {
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: {
alerts: { list: mockAlerts },
errored: false,
hasError: false,
sort: 'STARTED_AT_DESC',
alertsCount,
},
@ -429,7 +436,7 @@ describe('AlertManagementTable', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
});
@ -448,19 +455,36 @@ describe('AlertManagementTable', () => {
});
});
it('shows an error when request fails', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
findFirstStatusOption().vm.$emit('click');
wrapper.setData({
errored: true,
describe('when a request fails', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
});
return wrapper.vm.$nextTick(() => {
expect(wrapper.find('[data-testid="alert-error"]').exists()).toBe(true);
it('shows an error', async () => {
await selectFirstStatusOption();
expect(findAlertError().text()).toContain(
'There was an error while updating the status of the alert.',
);
});
it('shows an error when triggered a second time', async () => {
await selectFirstStatusOption();
wrapper.find(GlAlert).vm.$emit('dismiss');
await wrapper.vm.$nextTick();
// Assert that the error has been dismissed in the setup
expect(findAlertError().exists()).toBe(false);
await selectFirstStatusOption();
expect(findAlertError().exists()).toBe(true);
});
});
it('shows an error when response includes HTML errors', () => {
it('shows an error when response includes HTML errors', async () => {
const mockUpdatedMutationErrorResult = {
data: {
updateAlertStatus: {
@ -474,13 +498,11 @@ describe('AlertManagementTable', () => {
};
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationErrorResult);
findFirstStatusOption().vm.$emit('click');
wrapper.setData({ errored: true });
return wrapper.vm.$nextTick(() => {
expect(wrapper.contains('[data-testid="alert-error"]')).toBe(true);
expect(wrapper.contains('[data-testid="htmlError"]')).toBe(true);
});
await selectFirstStatusOption();
expect(findAlertError().exists()).toBe(true);
expect(findAlertError().contains('[data-testid="htmlError"]')).toBe(true);
});
});
@ -510,7 +532,7 @@ describe('AlertManagementTable', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts, pageInfo: {} }, alertsCount, errored: false },
data: { alerts: { list: mockAlerts, pageInfo: {} }, alertsCount, hasError: false },
loading: false,
});
});
@ -570,7 +592,7 @@ describe('AlertManagementTable', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
});

View File

@ -6,11 +6,12 @@ import {
GlAvatar,
GlPagination,
GlSearchBoxByType,
GlTab,
} from '@gitlab/ui';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import IncidentsList from '~/incidents/components/incidents_list.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { I18N } from '~/incidents/constants';
import { I18N, INCIDENT_STATE_TABS } from '~/incidents/constants';
import mockIncidents from '../mocks/incidents.json';
jest.mock('~/lib/utils/url_utility', () => ({
@ -34,6 +35,7 @@ describe('Incidents List', () => {
const findSearch = () => wrapper.find(GlSearchBoxByType);
const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
const findPagination = () => wrapper.find(GlPagination);
const findStatusFilterTabs = () => wrapper.findAll(GlTab);
function mountComponent({ data = { incidents: [] }, loading = false }) {
wrapper = mount(IncidentsList, {
@ -280,5 +282,25 @@ describe('Incidents List', () => {
expect(wrapper.vm.$data.searchTerm).toBe(SEARCH_TERM);
});
});
describe('State Filter Tabs', () => {
beforeEach(() => {
mountComponent({
data: { incidents: mockIncidents },
loading: false,
stubs: {
GlTab: true,
},
});
});
it('should display filter tabs', () => {
const tabs = findStatusFilterTabs().wrappers;
tabs.forEach((tab, i) => {
expect(tab.attributes('data-testid')).toContain(INCIDENT_STATE_TABS[i].state);
});
});
});
});
});

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Issues::SetSubscription do
it_behaves_like 'a subscribeable graphql resource' do
let_it_be(:resource) { create(:issue) }
let(:permission_name) { :update_issue }
end
end

View File

@ -3,44 +3,8 @@
require 'spec_helper'
RSpec.describe Mutations::MergeRequests::SetSubscription do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:user) { create(:user) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
specify { expect(described_class).to require_graphql_authorizations(:update_merge_request) }
describe '#resolve' do
let(:subscribe) { true }
let(:mutated_merge_request) { subject[:merge_request] }
subject { mutation.resolve(project_path: merge_request.project.full_path, iid: merge_request.iid, subscribed_state: subscribe) }
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when the user can update the merge request' do
before do
merge_request.project.add_developer(user)
end
it 'returns the merge request as discussion locked' do
expect(mutated_merge_request).to eq(merge_request)
expect(mutated_merge_request.subscribed?(user, project)).to eq(true)
expect(subject[:errors]).to be_empty
end
context 'when passing subscribe as false' do
let(:subscribe) { false }
it 'unsubscribes from the discussion' do
merge_request.subscribe(user, project)
expect(mutated_merge_request.subscribed?(user, project)).to eq(false)
end
end
end
it_behaves_like 'a subscribeable graphql resource' do
let_it_be(:resource) { create(:merge_request) }
let(:permission_name) { :update_merge_request }
end
end

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20200728080250_replace_unique_index_on_cycle_analytics_stages.rb')
RSpec.describe ReplaceUniqueIndexOnCycleAnalyticsStages, :migration, schema: 20200728080250 do
let(:namespaces) { table(:namespaces) }
let(:group_value_streams) { table(:analytics_cycle_analytics_group_value_streams) }
let(:group_stages) { table(:analytics_cycle_analytics_group_stages) }
let(:group) { namespaces.create!(type: 'Group', name: 'test', path: 'test') }
let(:value_stream_1) { group_value_streams.create!(group_id: group.id, name: 'vs1') }
let(:value_stream_2) { group_value_streams.create!(group_id: group.id, name: 'vs2') }
let(:duplicated_stage_1) { group_stages.create!(group_id: group.id, group_value_stream_id: value_stream_1.id, name: 'stage', start_event_identifier: 1, end_event_identifier: 1) }
let(:duplicated_stage_2) { group_stages.create!(group_id: group.id, group_value_stream_id: value_stream_2.id, name: 'stage', start_event_identifier: 1, end_event_identifier: 1) }
let(:stage_record) { group_stages.create!(group_id: group.id, group_value_stream_id: value_stream_2.id, name: 'other stage', start_event_identifier: 1, end_event_identifier: 1) }
describe '#down' do
subject { described_class.new.down }
before do
described_class.new.up
duplicated_stage_1
duplicated_stage_2
stage_record
end
it 'removes duplicated stage records' do
subject
stage = group_stages.find_by_id(duplicated_stage_2.id)
expect(stage).to be_nil
end
it 'does not change the first duplicated stage record' do
expect { subject }.not_to change { duplicated_stage_1.reload.attributes }
end
it 'does not change not duplicated stage record' do
expect { subject }.not_to change { stage_record.reload.attributes }
end
end
end

View File

@ -4125,7 +4125,6 @@ RSpec.describe Project do
end
it 'removes the pages directory and marks the project as not having pages deployed' do
expect_any_instance_of(Projects::UpdatePagesConfigurationService).to receive(:execute)
expect_any_instance_of(Gitlab::PagesTransfer).to receive(:rename_project).and_return(true)
expect(PagesWorker).to receive(:perform_in).with(5.minutes, :remove, namespace.full_path, anything)

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Setting subscribed status of an issue' do
include GraphqlHelpers
it_behaves_like 'a subscribable resource api' do
let_it_be(:resource) { create(:issue) }
let(:mutation_name) { :issue_set_subscription }
end
end

View File

@ -5,59 +5,8 @@ require 'spec_helper'
RSpec.describe 'Setting subscribed status of a merge request' do
include GraphqlHelpers
let(:current_user) { create(:user) }
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
let(:input) { { subscribed_state: true } }
let(:mutation) do
variables = {
project_path: project.full_path,
iid: merge_request.iid.to_s
}
graphql_mutation(:merge_request_set_subscription, variables.merge(input),
<<-QL.strip_heredoc
clientMutationId
errors
mergeRequest {
id
subscribed
}
QL
)
end
def mutation_response
graphql_mutation_response(:merge_request_set_subscription)['mergeRequest']['subscribed']
end
before do
project.add_developer(current_user)
end
it 'returns an error if the user is not allowed to update the merge request' do
post_graphql_mutation(mutation, current_user: create(:user))
expect(graphql_errors).not_to be_empty
end
it 'marks the merge request as WIP' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(true)
end
context 'when passing subscribe false as input' do
let(:input) { { subscribed_state: false } }
it 'unmarks the merge request as subscribed' do
merge_request.subscribe(current_user, project)
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(false)
end
it_behaves_like 'a subscribable resource api' do
let_it_be(:resource) { create(:merge_request) }
let(:mutation_name) { :merge_request_set_subscription }
end
end

View File

@ -7,7 +7,7 @@ RSpec.describe Metrics::Dashboard::GrafanaMetricEmbedService do
include ReactiveCachingHelpers
include GrafanaApiHelpers
let_it_be(:project) { build(:project) }
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:grafana_integration) { create(:grafana_integration, project: project) }
@ -15,7 +15,7 @@ RSpec.describe Metrics::Dashboard::GrafanaMetricEmbedService do
valid_grafana_dashboard_link(grafana_integration.grafana_url)
end
before do
before_all do
project.add_maintainer(user)
end
@ -58,6 +58,31 @@ RSpec.describe Metrics::Dashboard::GrafanaMetricEmbedService do
expect(subject.current_user).to eq(user)
expect(subject.params[:grafana_url]).to eq(grafana_url)
end
context 'with unknown users' do
let(:params) { [project.id, current_user_id, grafana_url] }
context 'when anonymous' do
where(:current_user_id) do
[nil, '']
end
with_them do
it 'sets current_user as nil' do
expect(subject.current_user).to be_nil
end
end
end
context 'when invalid' do
let(:current_user_id) { non_existing_record_id }
it 'raise record not found error' do
expect { subject }
.to raise_error(ActiveRecord::RecordNotFound, /Couldn't find User/)
end
end
end
end
describe '#get_dashboard', :use_clean_rails_memory_store_caching do
@ -145,7 +170,17 @@ RSpec.describe Metrics::Dashboard::GrafanaMetricEmbedService do
stub_datasource_request(grafana_integration.grafana_url)
end
it_behaves_like 'valid embedded dashboard service response'
context 'when project is private and user is member' do
it_behaves_like 'valid embedded dashboard service response'
end
context 'when project is public and user is anonymous' do
let(:project) { create(:project, :public) }
let(:user) { nil }
let(:grafana_integration) { create(:grafana_integration, project: project) }
it_behaves_like 'valid embedded dashboard service response'
end
end
end

View File

@ -50,3 +50,145 @@ RSpec.shared_examples 'tabs with counts' do
expect(tab.find('.badge').text).to eq(counts[:public])
end
end
RSpec.shared_examples 'does not show New Snippet button' do
let(:user) { create(:user, :external) }
specify do
sign_in(user)
subject
wait_for_requests
expect(page).not_to have_link('New snippet')
end
end
RSpec.shared_examples 'show and render proper snippet blob' do
before do
allow_any_instance_of(Snippet).to receive(:blobs).and_return([snippet.repository.blob_at('master', file_path)])
end
context 'Ruby file' do
let(:file_path) { 'files/ruby/popen.rb' }
it 'displays the blob' do
subject
aggregate_failures do
# shows highlighted Ruby code
expect(page).to have_content("require 'fileutils'")
# does not show a viewer switcher
expect(page).not_to have_selector('.js-blob-viewer-switcher')
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
# shows a raw button
expect(page).to have_link('Open raw')
# shows a download button
expect(page).to have_link('Download')
end
end
end
context 'Markdown file' do
let(:file_path) { 'files/markdown/ruby-style-guide.md' }
context 'visiting directly' do
before do
subject
end
it 'displays the blob using the rich viewer' do
aggregate_failures do
# hides the simple viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
# shows rendered Markdown
expect(page).to have_link("PEP-8")
# shows a viewer switcher
expect(page).to have_selector('.js-blob-viewer-switcher')
# shows a disabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
# shows a raw button
expect(page).to have_link('Open raw')
# shows a download button
expect(page).to have_link('Download')
end
end
context 'switching to the simple viewer' do
before do
find('.js-blob-viewer-switch-btn[data-viewer=simple]').click
wait_for_requests
end
it 'displays the blob using the simple viewer' do
aggregate_failures do
# hides the rich viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
# shows highlighted Markdown code
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
end
end
context 'switching to the rich viewer again' do
before do
find('.js-blob-viewer-switch-btn[data-viewer=rich]').click
wait_for_requests
end
it 'displays the blob using the rich viewer' do
aggregate_failures do
# hides the simple viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]', visible: false)
expect(page).to have_selector('.blob-viewer[data-type="rich"]')
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
end
end
end
end
end
context 'visiting with a line number anchor' do
let(:anchor) { 'L1' }
it 'displays the blob using the simple viewer' do
subject
aggregate_failures do
# hides the rich viewer
expect(page).to have_selector('.blob-viewer[data-type="simple"]')
expect(page).to have_selector('.blob-viewer[data-type="rich"]', visible: false)
# highlights the line in question
expect(page).to have_selector('#LC1.hll')
# shows highlighted Markdown code
expect(page).to have_content("[PEP-8](http://www.python.org/dev/peps/pep-0008/)")
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
end
end
end
end
end

View File

@ -0,0 +1,45 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'a subscribeable graphql resource' do
let(:project) { resource.project }
let_it_be(:user) { create(:user) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
specify { expect(described_class).to require_graphql_authorizations(permission_name) }
describe '#resolve' do
let(:subscribe) { true }
let(:mutated_resource) { subject[resource.class.name.underscore.to_sym] }
subject { mutation.resolve(project_path: resource.project.full_path, iid: resource.iid, subscribed_state: subscribe) }
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
context 'when the user can update the resource' do
before do
resource.project.add_developer(user)
end
it 'subscribes to the resource' do
expect(mutated_resource).to eq(resource)
expect(mutated_resource.subscribed?(user, project)).to eq(true)
expect(subject[:errors]).to be_empty
end
context 'when passing subscribe as false' do
let(:subscribe) { false }
it 'unsubscribes from the discussion' do
resource.subscribe(user, project)
expect(mutated_resource.subscribed?(user, project)).to eq(false)
end
end
end
end
end

View File

@ -0,0 +1,69 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.shared_examples 'a subscribable resource api' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let(:project) { resource.project }
let(:input) { { subscribed_state: true } }
let(:resource_ref) { resource.class.name.camelize(:lower) }
let(:mutation) do
variables = {
project_path: project.full_path,
iid: resource.iid.to_s
}
graphql_mutation(
mutation_name,
variables.merge(input),
<<-QL.strip_heredoc
clientMutationId
errors
#{resource_ref} {
id
subscribed
}
QL
)
end
def mutation_response
graphql_mutation_response(mutation_name)[resource_ref]['subscribed']
end
context 'when the user is not authorized' do
it_behaves_like 'a mutation that returns top-level errors',
errors: ["The resource that you are attempting to access "\
"does not exist or you don't have permission to "\
"perform this action"]
end
context 'when user is authorized' do
before do
project.add_developer(current_user)
end
it 'marks the resource as subscribed' do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(true)
end
context 'when passing subscribe false as input' do
let(:input) { { subscribed_state: false } }
it 'unmarks the resource as subscribed' do
resource.subscribe(current_user, project)
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response).to eq(false)
end
end
end
end

View File

@ -78,6 +78,12 @@ RSpec.shared_examples 'raises error for users with insufficient permissions' do
it_behaves_like 'misconfigured dashboard service response', :unauthorized
end
context 'when the user is anonymous' do
let(:user) { nil }
it_behaves_like 'misconfigured dashboard service response', :unauthorized
end
end
RSpec.shared_examples 'valid dashboard cloning process' do |dashboard_template, sequence|

View File

@ -11,31 +11,57 @@ RSpec.describe GitGarbageCollectWorker do
let(:shell) { Gitlab::Shell.new }
let!(:lease_uuid) { SecureRandom.uuid }
let!(:lease_key) { "project_housekeeping:#{project.id}" }
let(:params) { [project.id, task, lease_key, lease_uuid] }
subject { described_class.new }
shared_examples 'it calls Gitaly' do
specify do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(gitaly_task)
.and_return(nil)
subject.perform(*params)
end
end
shared_examples 'it updates the project statistics' do
specify do
expect_any_instance_of(Projects::UpdateStatisticsService).to receive(:execute).and_call_original
expect(Projects::UpdateStatisticsService)
.to receive(:new)
.with(project, nil, statistics: [:repository_size, :lfs_objects_size])
.and_call_original
subject.perform(*params)
end
end
describe "#perform" do
let(:gitaly_task) { :garbage_collect }
let(:task) { :gc }
context 'with active lease_uuid' do
before do
allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
end
it_behaves_like 'it calls Gitaly'
it_behaves_like 'it updates the project statistics'
it "flushes ref caches when the task if 'gc'" do
expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
.and_return(nil)
expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
subject.perform(project.id, :gc, lease_key, lease_uuid)
subject.perform(*params)
end
it 'handles gRPC errors' do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect).and_raise(GRPC::NotFound)
expect { subject.perform(project.id, :gc, lease_key, lease_uuid) }.to raise_exception(Gitlab::Git::Repository::NoRepository)
expect { subject.perform(*params) }.to raise_exception(Gitlab::Git::Repository::NoRepository)
end
end
@ -49,11 +75,13 @@ RSpec.describe GitGarbageCollectWorker do
expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
subject.perform(project.id, :gc, lease_key, lease_uuid)
subject.perform(*params)
end
end
context 'with no active lease' do
let(:params) { [project.id] }
before do
allow(subject).to receive(:get_lease_uuid).and_return(false)
end
@ -63,15 +91,16 @@ RSpec.describe GitGarbageCollectWorker do
allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
end
it_behaves_like 'it calls Gitaly'
it_behaves_like 'it updates the project statistics'
it "flushes ref caches when the task if 'gc'" do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
.and_return(nil)
expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
expect_any_instance_of(Repository).to receive(:has_visible_content?).and_call_original
expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
subject.perform(project.id)
subject.perform(*params)
end
context 'when the repository has joined a pool' do
@ -81,7 +110,7 @@ RSpec.describe GitGarbageCollectWorker do
it 'ensures the repositories are linked' do
expect_any_instance_of(PoolRepository).to receive(:link_repository).once
subject.perform(project.id)
subject.perform(*params)
end
end
end
@ -97,48 +126,55 @@ RSpec.describe GitGarbageCollectWorker do
expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
subject.perform(project.id)
subject.perform(*params)
end
end
end
context "repack_full" do
let(:task) { :full_repack }
let(:gitaly_task) { :repack_full }
before do
expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
end
it "calls Gitaly" do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_full)
.and_return(nil)
subject.perform(project.id, :full_repack, lease_key, lease_uuid)
end
it_behaves_like 'it calls Gitaly'
it_behaves_like 'it updates the project statistics'
end
context "pack_refs" do
let(:task) { :pack_refs }
let(:gitaly_task) { :pack_refs }
before do
expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
end
it "calls Gitaly" do
expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(:pack_refs)
expect_any_instance_of(Gitlab::GitalyClient::RefService).to receive(task)
.and_return(nil)
subject.perform(project.id, :pack_refs, lease_key, lease_uuid)
subject.perform(*params)
end
it 'does not update the project statistics' do
expect(Projects::UpdateStatisticsService).not_to receive(:new)
subject.perform(*params)
end
end
context "repack_incremental" do
let(:task) { :incremental_repack }
let(:gitaly_task) { :repack_incremental }
before do
expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
end
it "calls Gitaly" do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_incremental)
.and_return(nil)
subject.perform(project.id, :incremental_repack, lease_key, lease_uuid)
end
it_behaves_like 'it calls Gitaly'
it_behaves_like 'it updates the project statistics'
end
shared_examples 'gc tasks' do