Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-09-22 09:09:43 +00:00
parent c8eee7e7e8
commit 1086ac5177
52 changed files with 805 additions and 139 deletions

View File

@ -71,6 +71,7 @@ export default {
selectedDesigns: [],
isDraggingDesign: false,
reorderedDesigns: null,
isReorderingInProgress: false,
};
},
computed: {
@ -277,6 +278,7 @@ export default {
return variables;
},
reorderDesigns({ moved: { newIndex, element } }) {
this.isReorderingInProgress = true;
this.$apollo
.mutate({
mutation: moveDesignMutation,
@ -287,6 +289,9 @@ export default {
})
.catch(() => {
createFlash(MOVE_DESIGN_ERROR);
})
.finally(() => {
this.isReorderingInProgress = false;
});
},
onDesignMove(designs) {
@ -358,7 +363,7 @@ export default {
<vue-draggable
v-else
:value="designs"
:disabled="!isLatestVersion"
:disabled="!isLatestVersion || isReorderingInProgress"
v-bind="$options.dragOptions"
tag="ol"
draggable=".js-design-tile"

View File

@ -1,8 +1,7 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings, vue/no-v-html */
import { GlTooltipDirective } from '@gitlab/ui';
/* eslint-disable @gitlab/vue-require-i18n-strings */
import { GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
export default {
@ -11,6 +10,7 @@ export default {
components: {
GlModal: DeprecatedModal2,
GlSprintf,
},
directives: {
@ -24,27 +24,6 @@ export default {
},
},
computed: {
noStopActionMessage() {
return sprintf(
s__(
`Environments|Note that this action will stop the environment,
but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment
due to no stop environment action being defined
in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file.`,
),
{
emphasisStart: '<strong>',
emphasisEnd: '</strong>',
ciConfigLinkStart:
'<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">',
ciConfigLinkEnd: '</a>',
},
false,
);
},
},
methods: {
onSubmit() {
eventHub.$emit('stopEnvironment', this.environment);
@ -72,7 +51,25 @@ export default {
<p>{{ s__('Environments|Are you sure you want to stop this environment?') }}</p>
<div v-if="!environment.has_stop_action" class="warning_message">
<p v-html="noStopActionMessage"></p>
<p>
<gl-sprintf
:message="
s__(`Environments|Note that this action will stop the environment,
but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment
due to no stop environment action being defined
in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file.`)
"
>
<template #emphasis="{ content }">
<strong>{{ content }}</strong>
</template>
<template #ciConfigLink="{ content }">
<a href="https://docs.gitlab.com/ee/ci/yaml/" target="_blank" rel="noopener noreferrer">
{{ content }}</a
>
</template>
</gl-sprintf>
</p>
<a
href="https://docs.gitlab.com/ee/ci/environments.html#stopping-an-environment"
target="_blank"

View File

@ -1,8 +1,8 @@
<script>
/* eslint-disable vue/no-v-html */
import { escape } from 'lodash';
import { mapState, mapGetters, createNamespacedHelpers } from 'vuex';
import { sprintf, s__ } from '~/locale';
import { GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
import consts from '../../stores/modules/commit/constants';
import RadioGroup from './radio_group.vue';
import NewMergeRequestOption from './new_merge_request_option.vue';
@ -13,6 +13,7 @@ const { mapState: mapCommitState, mapActions: mapCommitActions } = createNamespa
export default {
components: {
GlSprintf,
RadioGroup,
NewMergeRequestOption,
},
@ -20,12 +21,8 @@ export default {
...mapState(['currentBranchId', 'changedFiles', 'stagedFiles']),
...mapCommitState(['commitAction']),
...mapGetters(['currentBranch', 'emptyRepo', 'canPushToBranch']),
commitToCurrentBranchText() {
return sprintf(
s__('IDE|Commit to %{branchName} branch'),
{ branchName: `<strong class="monospace">${escape(this.currentBranchId)}</strong>` },
false,
);
currentBranchText() {
return escape(this.currentBranchId);
},
containsStagedChanges() {
return this.changedFiles.length > 0 && this.stagedFiles.length > 0;
@ -77,11 +74,13 @@ export default {
:disabled="!canPushToBranch"
:title="$options.currentBranchPermissionsTooltip"
>
<span
class="ide-option-label"
data-qa-selector="commit_to_current_branch_radio"
v-html="commitToCurrentBranchText"
></span>
<span class="ide-option-label" data-qa-selector="commit_to_current_branch_radio">
<gl-sprintf :message="s__('IDE|Commit to %{branchName} branch')">
<template #branchName>
<strong class="monospace">{{ currentBranchText }}</strong>
</template>
</gl-sprintf>
</span>
</radio-group>
<template v-if="!emptyRepo">
<radio-group

View File

@ -2,8 +2,8 @@
import { escape, find, countBy } from 'lodash';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as Flash } from '~/flash';
import { n__, s__, __ } from '~/locale';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVEL_NONE } from './constants';
import { n__, s__, __, sprintf } from '~/locale';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
export default class AccessDropdown {
@ -11,6 +11,7 @@ export default class AccessDropdown {
const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options;
this.options = options;
this.hasLicense = hasLicense;
this.deployKeysOnProtectedBranchesEnabled = gon.features.deployKeysOnProtectedBranches;
this.groups = [];
this.accessLevel = accessLevel;
this.accessLevelsData = accessLevelsData.roles;
@ -18,6 +19,7 @@ export default class AccessDropdown {
this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
this.usersPath = '/-/autocomplete/users.json';
this.groupsPath = '/-/autocomplete/project_groups.json';
this.deployKeysPath = '/-/autocomplete/deploy_keys_with_owners.json';
this.defaultLabel = this.$dropdown.data('defaultLabel');
this.setSelectedItems([]);
@ -146,6 +148,8 @@ export default class AccessDropdown {
obj.access_level = item.access_level;
} else if (item.type === LEVEL_TYPES.USER) {
obj.user_id = item.user_id;
} else if (item.type === LEVEL_TYPES.DEPLOY_KEY) {
obj.deploy_key_id = item.deploy_key_id;
} else if (item.type === LEVEL_TYPES.GROUP) {
obj.group_id = item.group_id;
}
@ -177,6 +181,9 @@ export default class AccessDropdown {
case LEVEL_TYPES.GROUP:
comparator = LEVEL_ID_PROP.GROUP;
break;
case LEVEL_TYPES.DEPLOY_KEY:
comparator = LEVEL_ID_PROP.DEPLOY_KEY;
break;
case LEVEL_TYPES.USER:
comparator = LEVEL_ID_PROP.USER;
break;
@ -218,6 +225,11 @@ export default class AccessDropdown {
group_id: selectedItem.id,
type: LEVEL_TYPES.GROUP,
};
} else if (selectedItem.type === LEVEL_TYPES.DEPLOY_KEY) {
itemToAdd = {
deploy_key_id: selectedItem.id,
type: LEVEL_TYPES.DEPLOY_KEY,
};
}
this.items.push(itemToAdd);
@ -233,11 +245,12 @@ export default class AccessDropdown {
return true;
}
if (item.type === LEVEL_TYPES.USER && item.user_id === itemToDelete.id) {
index = i;
} else if (item.type === LEVEL_TYPES.ROLE && item.access_level === itemToDelete.id) {
index = i;
} else if (item.type === LEVEL_TYPES.GROUP && item.group_id === itemToDelete.id) {
if (
(item.type === LEVEL_TYPES.USER && item.user_id === itemToDelete.id) ||
(item.type === LEVEL_TYPES.ROLE && item.access_level === itemToDelete.id) ||
(item.type === LEVEL_TYPES.DEPLOY_KEY && item.deploy_key_id === itemToDelete.id) ||
(item.type === LEVEL_TYPES.GROUP && item.group_id === itemToDelete.id)
) {
index = i;
}
@ -289,6 +302,10 @@ export default class AccessDropdown {
labelPieces.push(n__('1 user', '%d users', counts[LEVEL_TYPES.USER]));
}
if (counts[LEVEL_TYPES.DEPLOY_KEY] > 0) {
labelPieces.push(n__('1 deploy key', '%d deploy keys', counts[LEVEL_TYPES.DEPLOY_KEY]));
}
if (counts[LEVEL_TYPES.GROUP] > 0) {
labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
}
@ -299,20 +316,31 @@ export default class AccessDropdown {
getData(query, callback) {
if (this.hasLicense) {
Promise.all([
this.getDeployKeys(query),
this.getUsers(query),
this.groupsData ? Promise.resolve(this.groupsData) : this.getGroups(),
])
.then(([usersResponse, groupsResponse]) => {
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
this.groupsData = groupsResponse;
callback(this.consolidateData(usersResponse.data, groupsResponse.data));
callback(
this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data),
);
})
.catch(() => Flash(__('Failed to load groups & users.')));
.catch(() => {
if (this.deployKeysOnProtectedBranchesEnabled) {
Flash(__('Failed to load groups, users and deploy keys.'));
} else {
Flash(__('Failed to load groups & users.'));
}
});
} else {
callback(this.consolidateData());
this.getDeployKeys(query)
.then(deployKeysResponse => callback(this.consolidateData(deployKeysResponse.data)))
.catch(() => Flash(__('Failed to load deploy keys.')));
}
}
consolidateData(usersResponse = [], groupsResponse = []) {
consolidateData(deployKeysResponse, usersResponse = [], groupsResponse = []) {
let consolidatedData = [];
// ID property is handled differently locally from the server
@ -328,6 +356,10 @@ export default class AccessDropdown {
// For Users
// In dropdown: `id`
// For submit: `user_id`
//
// For Deploy Keys
// In dropdown: `id`
// For submit: `deploy_key_id`
/*
* Build roles
@ -410,6 +442,38 @@ export default class AccessDropdown {
}
}
if (this.deployKeysOnProtectedBranchesEnabled) {
const deployKeys = deployKeysResponse.map(response => {
const {
id,
fingerprint,
title,
owner: { avatar_url, name, username },
} = response;
const shortFingerprint = `(${fingerprint.substring(0, 14)}...)`;
return {
id,
title: title.concat(' ', shortFingerprint),
avatar_url,
fullname: name,
username,
type: LEVEL_TYPES.DEPLOY_KEY,
};
});
if (this.accessLevel === ACCESS_LEVELS.PUSH) {
if (deployKeys.length) {
consolidatedData = consolidatedData.concat(
[{ type: 'divider' }],
[{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
deployKeys,
);
}
}
}
return consolidatedData;
}
@ -433,6 +497,22 @@ export default class AccessDropdown {
});
}
getDeployKeys(query) {
if (this.deployKeysOnProtectedBranchesEnabled) {
return axios.get(this.buildUrl(gon.relative_url_root, this.deployKeysPath), {
params: {
search: query,
per_page: 20,
active: true,
project_id: gon.current_project_id,
push_code: true,
},
});
}
return Promise.resolve({ data: [] });
}
buildUrl(urlRoot, url) {
let newUrl;
if (urlRoot != null) {
@ -454,6 +534,9 @@ export default class AccessDropdown {
case LEVEL_TYPES.ROLE:
criteria = { access_level: item.id };
break;
case LEVEL_TYPES.DEPLOY_KEY:
criteria = { deploy_key_id: item.id };
break;
case LEVEL_TYPES.GROUP:
criteria = { group_id: item.id };
break;
@ -470,6 +553,10 @@ export default class AccessDropdown {
case LEVEL_TYPES.ROLE:
groupRowEl = this.roleRowHtml(item, isActive);
break;
case LEVEL_TYPES.DEPLOY_KEY:
groupRowEl =
this.accessLevel === ACCESS_LEVELS.PUSH ? this.deployKeyRowHtml(item, isActive) : '';
break;
case LEVEL_TYPES.GROUP:
groupRowEl = this.groupRowHtml(item, isActive);
break;
@ -495,6 +582,31 @@ export default class AccessDropdown {
`;
}
deployKeyRowHtml(key, isActive) {
const isActiveClass = isActive || '';
return `
<li>
<a href="#" class="${isActiveClass}">
<strong>${key.title}</strong>
<p>
${sprintf(
__('Owned by %{image_tag}'),
{
image_tag: `<img src="${key.avatar_url}" class="avatar avatar-inline s26" width="30">`,
},
false,
)}
<strong class="dropdown-menu-user-full-name gl-display-inline">${escape(
key.fullname,
)}</strong>
<span class="dropdown-menu-user-username gl-display-inline">${key.username}</span>
</p>
</a>
</li>
`;
}
groupRowHtml(group, isActive) {
const isActiveClass = isActive || '';
const avatarEl = group.avatar_url

View File

@ -1,13 +1,20 @@
export const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
DEPLOY_KEY: 'deploy_key',
GROUP: 'group',
};
export const LEVEL_ID_PROP = {
ROLE: 'access_level',
USER: 'user_id',
DEPLOY_KEY: 'deploy_key_id',
GROUP: 'group_id',
};
export const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
};
export const ACCESS_LEVEL_NONE = 0;

View File

@ -7,12 +7,14 @@ export const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group',
DEPLOY_KEY: 'deploy_key',
};
export const LEVEL_ID_PROP = {
ROLE: 'access_level',
USER: 'user_id',
GROUP: 'group_id',
DEPLOY_KEY: 'deploy_key_id',
};
export const ACCESS_LEVEL_NONE = 0;

View File

@ -108,6 +108,10 @@ export default class ProtectedBranchCreate {
levelAttributes.push({
group_id: item.group_id,
});
} else if (item.type === LEVEL_TYPES.DEPLOY_KEY) {
levelAttributes.push({
deploy_key_id: item.deploy_key_id,
});
}
});

View File

@ -4,6 +4,7 @@ class InvitesController < ApplicationController
include Gitlab::Utils::StrongMemoize
before_action :member
before_action :ensure_member_exists
before_action :invite_details
skip_before_action :authenticate_user!, only: :decline
@ -59,14 +60,16 @@ class InvitesController < ApplicationController
end
def member
return @member if defined?(@member)
strong_memoize(:member) do
@token = params[:id]
Member.find_by_invite_token(@token)
end
end
@token = params[:id]
@member = Member.find_by_invite_token(@token)
def ensure_member_exists
return if member
return render_404 unless @member
@member
render_404
end
def authenticate_user!
@ -76,10 +79,7 @@ class InvitesController < ApplicationController
notice << "or create an account" if Gitlab::CurrentSettings.allow_signup?
notice = notice.join(' ') + "."
# this is temporary finder instead of using member method due to render_404 possibility
# will be resolved via https://gitlab.com/gitlab-org/gitlab/-/issues/245325
initial_member = Member.find_by_invite_token(params[:id])
redirect_params = initial_member ? { invite_email: initial_member.invite_email } : {}
redirect_params = member ? { invite_email: member.invite_email } : {}
store_location_for :user, request.fullpath
@ -87,20 +87,20 @@ class InvitesController < ApplicationController
end
def invite_details
@invite_details ||= case @member.source
@invite_details ||= case member.source
when Project
{
name: @member.source.full_name,
url: project_url(@member.source),
name: member.source.full_name,
url: project_url(member.source),
title: _("project"),
path: project_path(@member.source)
path: project_path(member.source)
}
when Group
{
name: @member.source.name,
url: group_url(@member.source),
name: member.source.name,
url: group_url(member.source),
title: _("group"),
path: group_path(@member.source)
path: group_path(member.source)
}
end
end

View File

@ -62,7 +62,7 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
end
def access_level_attributes
%i[access_level id _destroy]
%i[access_level id _destroy deploy_key_id]
end
end

View File

@ -7,6 +7,7 @@ module Projects
before_action :define_variables, only: [:create_deploy_token]
before_action do
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
push_frontend_feature_flag(:deploy_keys_on_protected_branches, @project)
end
def show
@ -125,6 +126,7 @@ module Projects
gon.push(protectable_tags_for_dropdown)
gon.push(protectable_branches_for_dropdown)
gon.push(access_levels_options)
gon.push(current_project_id: project.id) if project
end
end
end

View File

@ -16,6 +16,10 @@ module Resolvers
required: false,
description: 'Filter projects by IDs'
argument :search_namespaces, GraphQL::BOOLEAN_TYPE,
required: false,
description: 'Include namespace in project search'
def resolve(**args)
ProjectsFinder
.new(current_user: current_user, params: project_finder_params(args), project_ids_relation: parse_gids(args[:ids]))
@ -28,7 +32,8 @@ module Resolvers
{
without_deleted: true,
non_public: params[:membership],
search: params[:search]
search: params[:search],
search_namespaces: params[:search_namespaces]
}.compact
end

View File

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Types
module DesignManagement
class DesignCollectionCopyStateEnum < BaseEnum
graphql_name 'DesignCollectionCopyState'
description 'Copy state of a DesignCollection'
DESCRIPTION_VARIANTS = {
in_progress: 'is being copied',
error: 'encountered an error during a copy',
ready: 'has no copy in progress'
}.freeze
def self.description_variant(copy_state)
DESCRIPTION_VARIANTS[copy_state.to_sym] ||
(raise ArgumentError, "Unknown copy state: #{copy_state}")
end
::DesignManagement::DesignCollection.state_machines[:copy_state].states.keys.each do |copy_state|
value copy_state.upcase,
value: copy_state.to_s,
description: "The DesignCollection #{description_variant(copy_state)}"
end
end
end
end

View File

@ -39,6 +39,10 @@ module Types
null: true,
resolver: ::Resolvers::DesignManagement::DesignResolver,
description: 'Find a specific design'
field :copy_state, ::Types::DesignManagement::DesignCollectionCopyStateEnum,
null: true,
description: 'Copy state of the design collection'
end
end
end

View File

@ -257,11 +257,7 @@ class MergeRequest < ApplicationRecord
scope :join_project, -> { joins(:target_project) }
scope :join_metrics, -> do
query = joins(:metrics)
if Feature.enabled?(:improved_mr_merged_at_queries, default_enabled: true)
query = query.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
end
query = query.where(MergeRequest.arel_table[:target_project_id].eq(MergeRequest::Metrics.arel_table[:target_project_id]))
query
end
scope :references_project, -> { references(:target_project) }

View File

@ -509,6 +509,8 @@ class MergeRequestDiff < ApplicationRecord
end
def encode_in_base64?(diff_text)
return false if diff_text.nil?
(diff_text.encoding == Encoding::BINARY && !diff_text.ascii_only?) ||
diff_text.include?("\0")
end
@ -536,7 +538,7 @@ class MergeRequestDiff < ApplicationRecord
rows.each do |row|
data = row.delete(:diff)
row[:external_diff_offset] = file.pos
row[:external_diff_size] = data.bytesize
row[:external_diff_size] = data&.bytesize || 0
file.write(data)
end

View File

@ -71,8 +71,6 @@ class User < ApplicationRecord
MINIMUM_INACTIVE_DAYS = 180
ignore_column :bio, remove_with: '13.4', remove_after: '2020-09-22'
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
# rubocop: disable CodeReuse/ServiceClass

View File

@ -9,9 +9,9 @@
= form_for @hook, as: :hook, url: admin_hook_path do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
%span>= f.submit _('Save changes'), class: 'btn btn-success gl-mr-3'
%span>= f.submit _('Save changes'), class: 'btn gl-button btn-success gl-mr-3'
= render 'shared/web_hooks/test_button', hook: @hook
= link_to _('Delete'), admin_hook_path(@hook), method: :delete, class: 'btn btn-remove float-right', data: { confirm: _('Are you sure?') }
= link_to _('Delete'), admin_hook_path(@hook), method: :delete, class: 'btn gl-button btn-remove float-right', data: { confirm: _('Are you sure?') }
%hr

View File

@ -7,7 +7,7 @@
.col-lg-8.gl-mb-3
= form_for @hook, as: :hook, url: admin_hooks_path do |f|
= render partial: 'form', locals: { form: f, hook: @hook }
= f.submit _('Add system hook'), class: 'btn btn-success'
= f.submit _('Add system hook'), class: 'btn gl-button btn-success'
= render 'shared/web_hooks/index', hooks: @hooks, hook_class: @hook.class

View File

@ -1,3 +1,5 @@
- select_mode_for_dropdown = Feature.enabled?(:deploy_keys_on_protected_branches, @project) ? 'js-multiselect' : ''
- content_for :merge_access_levels do
.merge_access_levels-container
= dropdown_tag('Select',
@ -7,7 +9,7 @@
- content_for :push_access_levels do
.push_access_levels-container
= dropdown_tag('Select',
options: { toggle_class: 'js-allowed-to-push qa-allowed-to-push-select wide',
options: { toggle_class: "js-allowed-to-push qa-allowed-to-push-select #{select_mode_for_dropdown} wide",
dropdown_class: 'dropdown-menu-selectable qa-allowed-to-push-dropdown rspec-allowed-to-push-dropdown capitalize-header',
data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})

View File

@ -0,0 +1,5 @@
---
title: Add DesignCollection copyState GraphQL field
merge_request: 42919
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Resolve Error when quickly reordering designs
merge_request: 42818
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Refactor the invites controller member method
merge_request: 42727
author:
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix migrating some empty diffs
merge_request: 42825
author:
type: fixed

View File

@ -0,0 +1,7 @@
---
name: deploy_keys_on_protected_branches
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35638
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/247866
group: group::progressive delivery
type: development
default_enabled: false

View File

@ -1,7 +0,0 @@
---
name: improved_mr_merged_at_queries
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39329
rollout_issue_url:
group: group::analytics
type: development
default_enabled: true

View File

@ -1,7 +1,7 @@
---
name: new_pipeline_form
introduced_by_url:
rollout_issue_url:
group:
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35674
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/229632
group: group::continuous integration
type: development
default_enabled: false

View File

@ -1021,6 +1021,9 @@ The second facet presents the only real solution. For this, we developed
## Troubleshooting Gitaly
Check [Gitaly timeouts](../../user/admin_area/settings/gitaly_timeouts.md) when troubleshooting
Gitaly.
### Checking versions when using standalone Gitaly servers
When using standalone Gitaly servers, you must make sure they are the same version

View File

@ -66,6 +66,8 @@ To set up GitLab and its components to accommodate up to 10,000 users:
1. [Configure Prometheus](#configure-prometheus) to monitor your GitLab environment.
1. [Configure the Object Storage](#configure-the-object-storage)
used for shared data objects.
1. [Configure Advanced Search (optional)](#configure-advanced-search) for faster,
more advanced code search across your entire GitLab instance.
1. [Configure NFS (Optional)](#configure-nfs-optional)
to have shared disk storage service as an alternative to Gitaly and/or Object Storage (although
not recommended). NFS is required for GitLab Pages, you can skip this step if you're not using
@ -2033,6 +2035,25 @@ work.
</a>
</div>
## Configure Advanced Search **(STARTER ONLY)**
NOTE: **Note:**
Elasticsearch cluster design and requirements are dependent on your specific data.
For recommended best practices on how to set up your Elasticsearch cluster
alongside your instance, read how to
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
You can leverage Elasticsearch and enable Advanced Search for faster, more
advanced code search across your entire GitLab instance.
[Learn how to set it up.](../../integration/elasticsearch.md)
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Configure NFS (optional)
[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly)

View File

@ -47,3 +47,16 @@ You can also optionally configure GitLab to use an
[external PostgreSQL service](../postgresql/external.md) or an
[external object storage service](../high_availability/object_storage.md) for
added performance and reliability at a reduced complexity cost.
## Configure Advanced Search **(STARTER ONLY)**
NOTE: **Note:**
Elasticsearch cluster design and requirements are dependent on your specific data.
For recommended best practices on how to set up your Elasticsearch cluster
alongside your instance, read how to
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
You can leverage Elasticsearch and enable Advanced Search for faster, more
advanced code search across your entire GitLab instance.
[Learn how to set it up.](../../integration/elasticsearch.md)

View File

@ -66,6 +66,8 @@ To set up GitLab and its components to accommodate up to 25,000 users:
1. [Configure Prometheus](#configure-prometheus) to monitor your GitLab environment.
1. [Configure the Object Storage](#configure-the-object-storage)
used for shared data objects.
1. [Configure Advanced Search (optional)](#configure-advanced-search) for faster,
more advanced code search across your entire GitLab instance.
1. [Configure NFS (Optional)](#configure-nfs-optional)
to have shared disk storage service as an alternative to Gitaly and/or Object Storage (although
not recommended). NFS is required for GitLab Pages, you can skip this step if you're not using
@ -2033,6 +2035,25 @@ work.
</a>
</div>
## Configure Advanced Search **(STARTER ONLY)**
NOTE: **Note:**
Elasticsearch cluster design and requirements are dependent on your specific data.
For recommended best practices on how to set up your Elasticsearch cluster
alongside your instance, read how to
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
You can leverage Elasticsearch and enable Advanced Search for faster, more
advanced code search across your entire GitLab instance.
[Learn how to set it up.](../../integration/elasticsearch.md)
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Configure NFS (optional)
[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly)

View File

@ -55,6 +55,8 @@ To set up GitLab and its components to accommodate up to 2,000 users:
environment.
1. [Configure the object storage](#configure-the-object-storage) used for
shared data objects.
1. [Configure Advanced Search (optional)](#configure-advanced-search) for faster,
more advanced code search across your entire GitLab instance.
1. [Configure NFS](#configure-nfs-optional) (optional, and not recommended)
to have shared disk storage service as an alternative to Gitaly or object
storage. You can skip this step if you're not using GitLab Pages (which
@ -851,6 +853,25 @@ functioning backups is encountered.
</a>
</div>
## Configure Advanced Search **(STARTER ONLY)**
NOTE: **Note:**
Elasticsearch cluster design and requirements are dependent on your specific data.
For recommended best practices on how to set up your Elasticsearch cluster
alongside your instance, read how to
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
You can leverage Elasticsearch and enable Advanced Search for faster, more
advanced code search across your entire GitLab instance.
[Learn how to set it up.](../../integration/elasticsearch.md)
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Configure NFS (optional)
For improved performance, [object storage](#configure-the-object-storage),

View File

@ -70,6 +70,8 @@ To set up GitLab and its components to accommodate up to 3,000 users:
1. [Configure Prometheus](#configure-prometheus) to monitor your GitLab environment.
1. [Configure the Object Storage](#configure-the-object-storage)
used for shared data objects.
1. [Configure Advanced Search (optional)](#configure-advanced-search) for faster,
more advanced code search across your entire GitLab instance.
1. [Configure NFS (Optional)](#configure-nfs-optional)
to have shared disk storage service as an alternative to Gitaly and/or Object Storage (although
not recommended). NFS is required for GitLab Pages, you can skip this step if you're not using
@ -1759,6 +1761,25 @@ work.
</a>
</div>
## Configure Advanced Search **(STARTER ONLY)**
NOTE: **Note:**
Elasticsearch cluster design and requirements are dependent on your specific data.
For recommended best practices on how to set up your Elasticsearch cluster
alongside your instance, read how to
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
You can leverage Elasticsearch and enable Advanced Search for faster, more
advanced code search across your entire GitLab instance.
[Learn how to set it up.](../../integration/elasticsearch.md)
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Configure NFS (optional)
[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly)

View File

@ -66,6 +66,8 @@ To set up GitLab and its components to accommodate up to 50,000 users:
1. [Configure Prometheus](#configure-prometheus) to monitor your GitLab environment.
1. [Configure the Object Storage](#configure-the-object-storage)
used for shared data objects.
1. [Configure Advanced Search (optional)](#configure-advanced-search) for faster,
more advanced code search across your entire GitLab instance.
1. [Configure NFS (Optional)](#configure-nfs-optional)
to have shared disk storage service as an alternative to Gitaly and/or Object Storage (although
not recommended). NFS is required for GitLab Pages, you can skip this step if you're not using
@ -2033,6 +2035,25 @@ work.
</a>
</div>
## Configure Advanced Search **(STARTER ONLY)**
NOTE: **Note:**
Elasticsearch cluster design and requirements are dependent on your specific data.
For recommended best practices on how to set up your Elasticsearch cluster
alongside your instance, read how to
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
You can leverage Elasticsearch and enable Advanced Search for faster, more
advanced code search across your entire GitLab instance.
[Learn how to set it up.](../../integration/elasticsearch.md)
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Configure NFS (optional)
[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly)

View File

@ -70,6 +70,8 @@ To set up GitLab and its components to accommodate up to 5,000 users:
1. [Configure Prometheus](#configure-prometheus) to monitor your GitLab environment.
1. [Configure the Object Storage](#configure-the-object-storage)
used for shared data objects.
1. [Configure Advanced Search (optional)](#configure-advanced-search) for faster,
more advanced code search across your entire GitLab instance.
1. [Configure NFS (Optional)](#configure-nfs-optional)
to have shared disk storage service as an alternative to Gitaly and/or Object Storage (although
not recommended). NFS is required for GitLab Pages, you can skip this step if you're not using
@ -1758,6 +1760,25 @@ work.
</a>
</div>
## Configure Advanced Search **(STARTER ONLY)**
NOTE: **Note:**
Elasticsearch cluster design and requirements are dependent on your specific data.
For recommended best practices on how to set up your Elasticsearch cluster
alongside your instance, read how to
[choose the optimal cluster configuration](../../integration/elasticsearch.md#guidance-on-choosing-optimal-cluster-configuration).
You can leverage Elasticsearch and enable Advanced Search for faster, more
advanced code search across your entire GitLab instance.
[Learn how to set it up.](../../integration/elasticsearch.md)
<div align="right">
<a type="button" class="btn btn-default" href="#setup-components">
Back to setup components <i class="fa fa-angle-double-up" aria-hidden="true"></i>
</a>
</div>
## Configure NFS (optional)
[Object storage](#configure-the-object-storage), along with [Gitaly](#configure-gitaly)

View File

@ -3919,6 +3919,11 @@ type DesignAtVersionEdge {
A collection of designs
"""
type DesignCollection {
"""
Copy state of the design collection
"""
copyState: DesignCollectionCopyState
"""
Find a specific design
"""
@ -4046,6 +4051,26 @@ type DesignCollection {
): DesignVersionConnection!
}
"""
Copy state of a DesignCollection
"""
enum DesignCollectionCopyState {
"""
The DesignCollection encountered an error during a copy
"""
ERROR
"""
The DesignCollection is being copied
"""
IN_PROGRESS
"""
The DesignCollection has no copy in progress
"""
READY
}
"""
The connection type for Design.
"""
@ -14023,6 +14048,11 @@ type Query {
Search query for project name, path, or description
"""
search: String
"""
Include namespace in project search
"""
searchNamespaces: Boolean
): ProjectConnection
"""

View File

@ -10789,6 +10789,20 @@
"name": "DesignCollection",
"description": "A collection of designs",
"fields": [
{
"name": "copyState",
"description": "Copy state of the design collection",
"args": [
],
"type": {
"kind": "ENUM",
"name": "DesignCollectionCopyState",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "design",
"description": "Find a specific design",
@ -11106,6 +11120,35 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "DesignCollectionCopyState",
"description": "Copy state of a DesignCollection",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "READY",
"description": "The DesignCollection has no copy in progress",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "IN_PROGRESS",
"description": "The DesignCollection is being copied",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "ERROR",
"description": "The DesignCollection encountered an error during a copy",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DesignConnection",
@ -41107,6 +41150,16 @@
},
"defaultValue": null
},
{
"name": "searchNamespaces",
"description": "Include namespace in project search",
"type": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
},
"defaultValue": null
},
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",

View File

@ -685,6 +685,7 @@ A collection of designs.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `copyState` | DesignCollectionCopyState | Copy state of the design collection |
| `design` | Design | Find a specific design |
| `designAtVersion` | DesignAtVersion | Find a design as of a version |
| `issue` | Issue! | Issue associated with the design collection |
@ -3027,6 +3028,16 @@ Mode of a commit action.
| `PASSED_VALIDATION` | Site validation process finished successfully |
| `PENDING_VALIDATION` | Site validation process has not started |
### DesignCollectionCopyState
Copy state of a DesignCollection.
| Value | Description |
| ----- | ----------- |
| `ERROR` | The DesignCollection encountered an error during a copy |
| `IN_PROGRESS` | The DesignCollection is being copied |
| `READY` | The DesignCollection has no copy in progress |
### DesignVersionEvent
Mutation event of a design within a version.

View File

@ -3,36 +3,28 @@ stage: Create
group: Gitaly
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers"
type: reference
type: reference
---
# Gitaly timeouts
# Gitaly timeouts **(CORE ONLY)**
![Gitaly timeouts](img/gitaly_timeouts.png)
[Gitaly](../../../administration/gitaly/index.md) timeouts are configurable. The timeouts can be
configured to make sure that long running Gitaly calls don't needlessly take up resources.
3 timeout types can be configured to make sure that long running
Gitaly calls don't needlessly take up resources.
To access Gitaly timeout settings:
- Default timeout
1. Go to **Admin Area > Settings > Preferences**.
1. Expand the **Gitaly** section.
This timeout is the default for most Gitaly calls.
It should be shorter than the worker timeout that can be configured
for
[Puma](https://docs.gitlab.com/omnibus/settings/puma.html#puma-settings)
or [Unicorn](https://docs.gitlab.com/omnibus/settings/unicorn.html).
This makes sure that Gitaly calls made within a web request cannot
exceed these the entire request timeout.
## Available timeouts
The default for this timeout is 55 seconds.
The following timeouts can be modified:
- Fast timeout
- **Default Timeout Period**. This timeout is the default for most Gitaly calls. It should be shorter than the
worker timeout that can be configured for [Puma](https://docs.gitlab.com/omnibus/settings/puma.html#puma-settings)
or [Unicorn](https://docs.gitlab.com/omnibus/settings/unicorn.html). Used to make sure that Gitaly
calls made within a web request cannot exceed the entire request timeout.
Defaults to 55 seconds.
This is the timeout for very short Gitaly calls.
The default for this timeout is 10 seconds.
- Medium timeout
This timeout should be between the default and the fast timeout
The default for this timeout is 30 seconds.
- **Fast Timeout Period**. This is the timeout for very short Gitaly calls. Defaults to 10 seconds.
- **Medium Timeout Period**. This timeout should be between the default and the fast timeout.
Defaults to 30 seconds.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View File

@ -19,8 +19,9 @@ then in the left sidebar go to **Security & Compliance > Configuration**.
For each security control the page displays:
- **Status** - Status of the security control: enabled, not enabled, or available.
- **Manage** - A management option or a link to the documentation.
- **Security Control:** Name, description, and a documentation link.
- **Status:** The security control's status (enabled, not enabled, or available).
- **Manage:** A management option or a documentation link.
## Status

View File

@ -79,7 +79,7 @@ which apply to the entire Web IDE screen.
> - Support for validation based on custom schemas [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/226982) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.4.
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/).
based on the [JSON Schema Store](https://www.schemastore.org/json/).
### Predefined schemas
@ -423,9 +423,12 @@ when:
### Limitations
Interactive Terminals is in a beta phase and continues to be improved in upcoming
releases. In the meantime, please note that the user is limited to having only one
active terminal at a time.
The Web IDE has a few limitations:
- Interactive Terminals is in a beta phase and continues to be improved in upcoming releases. In the meantime, please note that the user is limited to having only one
active terminal at a time.
- LFS files can be rendered and displayed but they cannot be updated and committed using the Web IDE. If an LFS file is modified and pushed to the repository, the LFS pointer in the repository will be overwritten with the modified LFS file content.
### Troubleshooting

View File

@ -1024,6 +1024,11 @@ msgid_plural "%d days"
msgstr[0] ""
msgstr[1] ""
msgid "1 deploy key"
msgid_plural "%d deploy keys"
msgstr[0] ""
msgstr[1] ""
msgid "1 group"
msgid_plural "%d groups"
msgstr[0] ""
@ -1324,6 +1329,9 @@ msgstr ""
msgid "Access to Pages websites are controlled based on the user's membership to a given project. By checking this box, users will be required to be logged in to have access to all Pages websites in your instance."
msgstr ""
msgid "AccessDropdown|Deploy Keys"
msgstr ""
msgid "AccessDropdown|Groups"
msgstr ""
@ -10644,6 +10652,9 @@ msgstr ""
msgid "Failed to load branches. Please try again."
msgstr ""
msgid "Failed to load deploy keys."
msgstr ""
msgid "Failed to load emoji list."
msgstr ""
@ -10659,6 +10670,9 @@ msgstr ""
msgid "Failed to load groups & users."
msgstr ""
msgid "Failed to load groups, users and deploy keys."
msgstr ""
msgid "Failed to load labels. Please try again."
msgstr ""
@ -11147,6 +11161,9 @@ msgstr ""
msgid "Filter by user"
msgstr ""
msgid "Filter parameters are not valid. Make sure that the end date is after the start date."
msgstr ""
msgid "Filter pipelines"
msgstr ""
@ -11171,7 +11188,7 @@ msgstr ""
msgid "Find File"
msgstr ""
msgid "Find bugs in your code with coverage-guided fuzzing"
msgid "Find bugs in your code with coverage-guided fuzzing."
msgstr ""
msgid "Find by path"
@ -17913,6 +17930,9 @@ msgstr ""
msgid "Overwrite diverged branches"
msgstr ""
msgid "Owned by %{image_tag}"
msgstr ""
msgid "Owned by anyone"
msgstr ""
@ -22460,6 +22480,9 @@ msgstr ""
msgid "SecurityConfiguration|Manage"
msgstr ""
msgid "SecurityConfiguration|More information"
msgstr ""
msgid "SecurityConfiguration|Not enabled"
msgstr ""
@ -22472,9 +22495,6 @@ msgstr ""
msgid "SecurityConfiguration|Security Control"
msgstr ""
msgid "SecurityConfiguration|See documentation"
msgstr ""
msgid "SecurityConfiguration|Status"
msgstr ""
@ -24192,9 +24212,6 @@ msgstr ""
msgid "Start and due date"
msgstr ""
msgid "Start by choosing a group to see how your team is spending time. You can then drill down to the project level."
msgstr ""
msgid "Start by choosing a group to start exploring the merge requests in that group. You can then proceed to filter by projects, labels, milestones and authors."
msgstr ""

View File

@ -17,8 +17,16 @@ RSpec.describe InvitesController, :snowplow do
}
end
before do
controller.instance_variable_set(:@member, member)
shared_examples 'invalid token' do
context 'when invite token is not valid' do
let(:params) { { id: '_bogus_token_' } }
it 'renders the 404 page' do
request
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
describe 'GET #show' do
@ -39,7 +47,7 @@ RSpec.describe InvitesController, :snowplow do
end
it 'forces re-confirmation if email does not match signed in user' do
member.invite_email = 'bogus@email.com'
member.update!(invite_email: 'bogus@email.com')
expect do
request
@ -80,6 +88,8 @@ RSpec.describe InvitesController, :snowplow do
expect_snowplow_event(snowplow_event.merge(action: 'accepted'))
end
end
it_behaves_like 'invalid token'
end
context 'when not logged in' do
@ -139,5 +149,27 @@ RSpec.describe InvitesController, :snowplow do
expect_snowplow_event(snowplow_event.merge(action: 'accepted'))
end
end
it_behaves_like 'invalid token'
end
describe 'POST #decline for link in UI' do
before do
sign_in(user)
end
subject(:request) { post :decline, params: params }
it_behaves_like 'invalid token'
end
describe 'GET #decline for link in email' do
before do
sign_in(user)
end
subject(:request) { get :decline, params: params }
it_behaves_like 'invalid token'
end
end

View File

@ -9,6 +9,10 @@ RSpec.describe 'Protected Branches', :js do
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository) }
before do
stub_feature_flags(deploy_keys_on_protected_branches: false)
end
context 'logged in as developer' do
before do
project.add_developer(user)
@ -163,4 +167,14 @@ RSpec.describe 'Protected Branches', :js do
include_examples "protected branches > access control > CE"
end
end
context 'when the users for protected branches feature is off' do
before do
stub_licensed_features(protected_refs_for_users: false)
end
include_examples 'when the deploy_keys_on_protected_branches FF is turned on' do
let(:all_dropdown_sections) { %w(Roles Deploy\ Keys) }
end
end
end

View File

@ -99,6 +99,7 @@ describe('Design management index page', () => {
const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
const findDesignsWrapper = () => wrapper.find('[data-testid="designs-root"]');
const findDesigns = () => wrapper.findAll(Design);
const draggableAttributes = () => wrapper.find(VueDraggable).vm.$attrs;
async function moveDesigns(localWrapper) {
await jest.runOnlyPendingTimers();
@ -676,6 +677,20 @@ describe('Design management index page', () => {
).toBe('2');
});
it('prevents reordering when reorderDesigns mutation is in progress', async () => {
createComponentWithApollo({});
await moveDesigns(wrapper);
expect(draggableAttributes().disabled).toBe(true);
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
await wrapper.vm.$nextTick(); // kick off the DOM update
await wrapper.vm.$nextTick(); // kick off the DOM update for finally block
expect(draggableAttributes().disabled).toBe(false);
});
it('displays flash if mutation had a recoverable error', async () => {
createComponentWithApollo({
moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors),

View File

@ -83,12 +83,12 @@ describe('IDE commit sidebar actions', () => {
});
});
describe('commitToCurrentBranchText', () => {
describe('currentBranchText', () => {
it('escapes current branch', () => {
const injectedSrc = '<img src="x" />';
createComponent({ currentBranchId: injectedSrc });
expect(vm.commitToCurrentBranchText).not.toContain(injectedSrc);
expect(vm.currentBranchText).not.toContain(injectedSrc);
});
});

View File

@ -14,6 +14,7 @@ describe('AccessDropdown', () => {
`);
const $dropdown = $('#dummy-dropdown');
$dropdown.data('defaultLabel', defaultLabel);
gon.features = { deployKeysOnProtectedBranches: true };
const options = {
$dropdown,
accessLevelsData: {
@ -37,6 +38,9 @@ describe('AccessDropdown', () => {
{ type: LEVEL_TYPES.GROUP },
{ type: LEVEL_TYPES.GROUP },
{ type: LEVEL_TYPES.GROUP },
{ type: LEVEL_TYPES.DEPLOY_KEY },
{ type: LEVEL_TYPES.DEPLOY_KEY },
{ type: LEVEL_TYPES.DEPLOY_KEY },
];
beforeEach(() => {
@ -49,7 +53,7 @@ describe('AccessDropdown', () => {
const label = dropdown.toggleLabel();
expect(label).toBe('1 role, 2 users, 3 groups');
expect(label).toBe('1 role, 2 users, 3 deploy keys, 3 groups');
expect($dropdownToggleText).not.toHaveClass('is-default');
});
@ -122,6 +126,21 @@ describe('AccessDropdown', () => {
expect($dropdownToggleText).not.toHaveClass('is-default');
});
});
describe('with users and deploy keys', () => {
beforeEach(() => {
const selectedTypes = [LEVEL_TYPES.DEPLOY_KEY, LEVEL_TYPES.USER];
dropdown.setSelectedItems(dummyItems.filter(item => selectedTypes.includes(item.type)));
$dropdownToggleText.addClass('is-default');
});
it('displays number of deploy keys', () => {
const label = dropdown.toggleLabel();
expect(label).toBe('2 users, 3 deploy keys');
expect($dropdownToggleText).not.toHaveClass('is-default');
});
});
});
describe('userRowHtml', () => {

View File

@ -8,10 +8,15 @@ RSpec.describe Resolvers::ProjectsResolver do
describe '#resolve' do
subject { resolve(described_class, obj: nil, args: filters, ctx: { current_user: current_user }) }
let_it_be(:group) { create(:group, name: 'public-group') }
let_it_be(:private_group) { create(:group, name: 'private-group') }
let_it_be(:project) { create(:project, :public) }
let_it_be(:other_project) { create(:project, :public) }
let_it_be(:group_project) { create(:project, :public, group: group) }
let_it_be(:private_project) { create(:project, :private) }
let_it_be(:other_private_project) { create(:project, :private) }
let_it_be(:other_private_project) { create(:project, :private) }
let_it_be(:private_group_project) { create(:project, :private, group: private_group) }
let_it_be(:user) { create(:user) }
@ -20,6 +25,7 @@ RSpec.describe Resolvers::ProjectsResolver do
before_all do
project.add_developer(user)
private_project.add_developer(user)
private_group.add_developer(user)
end
context 'when user is not logged in' do
@ -27,7 +33,7 @@ RSpec.describe Resolvers::ProjectsResolver do
context 'when no filters are applied' do
it 'returns all public projects' do
is_expected.to contain_exactly(project, other_project)
is_expected.to contain_exactly(project, other_project, group_project)
end
context 'when search filter is provided' do
@ -45,6 +51,22 @@ RSpec.describe Resolvers::ProjectsResolver do
is_expected.to be_empty
end
end
context 'when searchNamespaces filter is provided' do
let(:filters) { { search: 'group', search_namespaces: true } }
it 'returns projects in a matching namespace' do
is_expected.to contain_exactly(group_project)
end
end
context 'when searchNamespaces filter false' do
let(:filters) { { search: 'group', search_namespaces: false } }
it 'returns ignores namespace matches' do
is_expected.to be_empty
end
end
end
end
@ -53,7 +75,7 @@ RSpec.describe Resolvers::ProjectsResolver do
context 'when no filters are applied' do
it 'returns all visible projects for the user' do
is_expected.to contain_exactly(project, other_project, private_project)
is_expected.to contain_exactly(project, other_project, group_project, private_project, private_group_project)
end
context 'when search filter is provided' do
@ -68,7 +90,23 @@ RSpec.describe Resolvers::ProjectsResolver do
let(:filters) { { membership: true } }
it 'returns projects that user is member of' do
is_expected.to contain_exactly(project, private_project)
is_expected.to contain_exactly(project, private_project, private_group_project)
end
end
context 'when searchNamespaces filter is provided' do
let(:filters) { { search: 'group', search_namespaces: true } }
it 'returns projects from matching group' do
is_expected.to contain_exactly(group_project, private_group_project)
end
end
context 'when searchNamespaces filter false' do
let(:filters) { { search: 'group', search_namespaces: false } }
it 'returns ignores namespace matches' do
is_expected.to be_empty
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['DesignCollectionCopyState'] do
it { expect(described_class.graphql_name).to eq('DesignCollectionCopyState') }
it 'exposes the correct event states' do
expect(described_class.values.keys).to match_array(%w(READY IN_PROGRESS ERROR))
end
end

View File

@ -6,7 +6,7 @@ RSpec.describe GitlabSchema.types['DesignCollection'] do
it { expect(described_class).to require_graphql_authorizations(:read_design) }
it 'has the expected fields' do
expected_fields = %i[project issue designs versions version designAtVersion design]
expected_fields = %i[project issue designs versions version designAtVersion design copyState]
expect(described_class).to have_graphql_fields(*expected_fields)
end

View File

@ -180,6 +180,17 @@ RSpec.describe MergeRequestDiff do
expect(diff.external_diff_store).to eq(file_store)
end
it 'migrates a nil diff file' do
expect(diff).not_to be_stored_externally
MergeRequestDiffFile.where(merge_request_diff_id: diff.id).update_all(diff: nil)
stub_external_diffs_setting(enabled: true)
diff.migrate_files_to_external_storage!
expect(diff).to be_stored_externally
end
it 'safely handles a transaction error when migrating to external storage' do
expect(diff).not_to be_stored_externally
expect(diff.external_diff).not_to be_exists

View File

@ -0,0 +1,95 @@
# frozen_string_literal: true
RSpec.shared_examples 'when the deploy_keys_on_protected_branches FF is turned on' do
before do
stub_feature_flags(deploy_keys_on_protected_branches: true)
project.add_maintainer(user)
sign_in(user)
end
let(:dropdown_sections_minus_deploy_keys) { all_dropdown_sections - ['Deploy Keys'] }
context 'when deploy keys are enabled to this project' do
let!(:deploy_key_1) { create(:deploy_key, title: 'title 1', projects: [project]) }
let!(:deploy_key_2) { create(:deploy_key, title: 'title 2', projects: [project]) }
context 'when only one deploy key can push' do
before do
deploy_key_1.deploy_keys_projects.first.update!(can_push: true)
end
it "shows all dropdown sections in the 'Allowed to push' main dropdown, with only one deploy key" do
visit project_protected_branches_path(project)
find(".js-allowed-to-push").click
wait_for_requests
within('.qa-allowed-to-push-dropdown') do
dropdown_headers = page.all('.dropdown-header').map(&:text)
expect(dropdown_headers).to contain_exactly(*all_dropdown_sections)
expect(page).to have_content('title 1')
expect(page).not_to have_content('title 2')
end
end
it "shows all sections but not deploy keys in the 'Allowed to merge' main dropdown" do
visit project_protected_branches_path(project)
find(".js-allowed-to-merge").click
wait_for_requests
within('.qa-allowed-to-merge-dropdown') do
dropdown_headers = page.all('.dropdown-header').map(&:text)
expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
end
end
it "shows all sections in the 'Allowed to push' update dropdown" do
create(:protected_branch, :no_one_can_push, project: project, name: 'master')
visit project_protected_branches_path(project)
within(".js-protected-branch-edit-form") do
find(".js-allowed-to-push").click
wait_for_requests
dropdown_headers = page.all('.dropdown-header').map(&:text)
expect(dropdown_headers).to contain_exactly(*all_dropdown_sections)
end
end
end
context 'when no deploy key can push' do
it "just shows all sections but not deploy keys in the 'Allowed to push' dropdown" do
visit project_protected_branches_path(project)
find(".js-allowed-to-push").click
wait_for_requests
within('.qa-allowed-to-push-dropdown') do
dropdown_headers = page.all('.dropdown-header').map(&:text)
expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
end
end
it "just shows all sections but not deploy keys in the 'Allowed to push' update dropdown" do
create(:protected_branch, :no_one_can_push, project: project, name: 'master')
visit project_protected_branches_path(project)
within(".js-protected-branch-edit-form") do
find(".js-allowed-to-push").click
wait_for_requests
dropdown_headers = page.all('.dropdown-header').map(&:text)
expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys)
end
end
end
end
end