Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-04-09 09:09:10 +00:00
parent 760822a537
commit 7484851b5f
64 changed files with 652 additions and 226 deletions

View File

@ -855,8 +855,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
- spec/models/abuse_report_spec.rb
- spec/models/alert_management/alert_spec.rb
- spec/models/audit_event_spec.rb
- spec/models/blob_viewer/gitlab_ci_yml_spec.rb
- spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
- spec/models/chat_name_spec.rb
- spec/models/chat_team_spec.rb
- spec/models/clusters/kubernetes_namespace_spec.rb
@ -898,13 +896,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
- spec/models/user_spec.rb
- spec/models/wiki_page/meta_spec.rb
- spec/models/wiki_page_spec.rb
- spec/policies/application_setting/term_policy_spec.rb
- spec/policies/ci/build_policy_spec.rb
- spec/policies/design_management/design_policy_spec.rb
- spec/policies/group_deploy_keys_group_policy_spec.rb
- spec/policies/group_policy_spec.rb
- spec/policies/project_snippet_policy_spec.rb
- spec/policies/service_policy_spec.rb
- spec/presenters/alert_management/alert_presenter_spec.rb
- spec/presenters/ci/pipeline_presenter_spec.rb
- spec/presenters/label_presenter_spec.rb

View File

@ -2,11 +2,11 @@
import { GlDrawer } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
import { contentTop } from '~/lib/utils/common_utils';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
@ -16,7 +16,7 @@ export default {
headerHeight: `${contentTop()}px`,
components: {
GlDrawer,
BoardSidebarIssueTitle,
BoardSidebarTitle,
SidebarAssigneesWidget,
BoardSidebarTimeTracker,
BoardSidebarLabelsSelect,
@ -67,7 +67,7 @@ export default {
>
<template #header>{{ __('Issue details') }}</template>
<template #default>
<board-sidebar-issue-title />
<board-sidebar-title />
<sidebar-assignees-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"

View File

@ -27,12 +27,12 @@ export default {
};
},
computed: {
...mapGetters({ issue: 'activeBoardItem' }),
...mapGetters({ item: 'activeBoardItem' }),
pendingChangesStorageKey() {
return this.getPendingChangesKey(this.issue);
return this.getPendingChangesKey(this.item);
},
projectPath() {
const referencePath = this.issue.referencePath || '';
const referencePath = this.item.referencePath || '';
return referencePath.slice(0, referencePath.indexOf('#'));
},
validationState() {
@ -40,29 +40,29 @@ export default {
},
},
watch: {
issue: {
handler(updatedIssue, formerIssue) {
if (formerIssue?.title !== this.title) {
localStorage.setItem(this.getPendingChangesKey(formerIssue), this.title);
item: {
handler(updatedItem, formerItem) {
if (formerItem?.title !== this.title) {
localStorage.setItem(this.getPendingChangesKey(formerItem), this.title);
}
this.title = updatedIssue.title;
this.title = updatedItem.title;
this.setPendingState();
},
immediate: true,
},
},
methods: {
...mapActions(['setActiveIssueTitle']),
getPendingChangesKey(issue) {
if (!issue) {
...mapActions(['setActiveItemTitle']),
getPendingChangesKey(item) {
if (!item) {
return '';
}
return joinPaths(
window.location.pathname.slice(1),
String(issue.id),
'issue-title-pending-changes',
String(item.id),
'item-title-pending-changes',
);
},
async setPendingState() {
@ -78,7 +78,7 @@ export default {
}
},
cancel() {
this.title = this.issue.title;
this.title = this.item.title;
this.$refs.sidebarItem.collapse();
this.showChangesAlert = false;
localStorage.removeItem(this.pendingChangesStorageKey);
@ -86,24 +86,24 @@ export default {
async setTitle() {
this.$refs.sidebarItem.collapse();
if (!this.title || this.title === this.issue.title) {
if (!this.title || this.title === this.item.title) {
return;
}
try {
this.loading = true;
await this.setActiveIssueTitle({ title: this.title, projectPath: this.projectPath });
await this.setActiveItemTitle({ title: this.title, projectPath: this.projectPath });
localStorage.removeItem(this.pendingChangesStorageKey);
this.showChangesAlert = false;
} catch (e) {
this.title = this.issue.title;
this.title = this.item.title;
createFlash({ message: this.$options.i18n.updateTitleError });
} finally {
this.loading = false;
}
},
handleOffClick() {
if (this.title !== this.issue.title) {
if (this.title !== this.item.title) {
this.showChangesAlert = true;
localStorage.setItem(this.pendingChangesStorageKey, this.title);
} else {
@ -112,11 +112,11 @@ export default {
},
},
i18n: {
issueTitlePlaceholder: __('Issue title'),
titlePlaceholder: __('Title'),
submitButton: __('Save changes'),
cancelButton: __('Cancel'),
updateTitleError: __('An error occurred when updating the issue title'),
invalidFeedback: __('An issue title is required'),
updateTitleError: __('An error occurred when updating the title'),
invalidFeedback: __('A title is required'),
reviewYourChanges: __('Changes to the title have not been saved'),
},
};
@ -131,10 +131,10 @@ export default {
@off-click="handleOffClick"
>
<template #title>
<span class="gl-font-weight-bold" data-testid="issue-title">{{ issue.title }}</span>
<span class="gl-font-weight-bold" data-testid="item-title">{{ item.title }}</span>
</template>
<template #collapsed>
<span class="gl-text-gray-800">{{ issue.referencePath }}</span>
<span class="gl-text-gray-800">{{ item.referencePath }}</span>
</template>
<gl-alert v-if="showChangesAlert" variant="warning" class="gl-mb-5" :dismissible="false">
{{ $options.i18n.reviewYourChanges }}
@ -144,7 +144,7 @@ export default {
<gl-form-input
v-model="title"
v-autofocusonshow
:placeholder="$options.i18n.issueTitlePlaceholder"
:placeholder="$options.i18n.titlePlaceholder"
:state="validationState"
/>
</gl-form-group>

View File

@ -1,5 +1,7 @@
import { __ } from '~/locale';
import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
import boardBlockingIssuesQuery from './graphql/board_blocking_issues.query.graphql';
import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql';
export const issuableTypes = {
issue: 'issue',
@ -52,3 +54,12 @@ export const blockingIssuablesQueries = {
query: boardBlockingIssuesQuery,
},
};
export const titleQueries = {
[issuableTypes.issue]: {
mutation: issueSetTitleMutation,
},
[issuableTypes.epic]: {
mutation: updateEpicTitleMutation,
},
};

View File

@ -1,5 +1,5 @@
mutation issueSetTitle($input: UpdateIssueInput!) {
updateIssue(input: $input) {
updateIssuableTitle: updateIssue(input: $input) {
issue {
title
}

View File

@ -8,6 +8,7 @@ import {
inactiveId,
flashAnimationDuration,
ISSUABLE,
titleQueries,
} from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
@ -33,7 +34,6 @@ import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.grap
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql';
import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.mutation.graphql';
import issueSetTitleMutation from '../graphql/issue_set_title.mutation.graphql';
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
import * as types from './mutation_types';
@ -526,27 +526,31 @@ export default {
});
},
setActiveIssueTitle: async ({ commit, getters }, input) => {
const { activeBoardItem } = getters;
setActiveItemTitle: async ({ commit, getters, state }, input) => {
const { activeBoardItem, isEpicBoard } = getters;
const { fullPath, issuableType } = state;
const workspacePath = isEpicBoard
? { groupPath: fullPath }
: { projectPath: input.projectPath };
const { data } = await gqlClient.mutate({
mutation: issueSetTitleMutation,
mutation: titleQueries[issuableType].mutation,
variables: {
input: {
...workspacePath,
iid: String(activeBoardItem.iid),
projectPath: input.projectPath,
title: input.title,
},
},
});
if (data.updateIssue?.errors?.length > 0) {
throw new Error(data.updateIssue.errors);
if (data.updateIssuableTitle?.errors?.length > 0) {
throw new Error(data.updateIssuableTitle.errors);
}
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
itemId: activeBoardItem.id,
prop: 'title',
value: data.updateIssue.issue.title,
value: data.updateIssuableTitle[issuableType].title,
});
},

View File

@ -25,7 +25,6 @@ export default {
},
actionPrimary: {
text: __('Yes, close issue'),
attributes: [{ variant: 'warning' }],
},
i18n: {
promoteErrorMessage: __(
@ -220,7 +219,6 @@ export default {
<gl-button
v-if="showToggleIssueStateButton"
class="gl-display-none gl-sm-display-inline-flex!"
category="secondary"
:data-qa-selector="qaSelector"
:loading="isToggleStateButtonLoading"
@click="toggleIssueState"

View File

@ -336,7 +336,7 @@ export default {
icon="pencil"
size="small"
category="tertiary"
class="note-action-button js-note-edit btn btn-transparent"
class="note-action-button js-note-edit"
data-qa-selector="note_edit_button"
@click="onEdit"
/>
@ -348,7 +348,7 @@ export default {
size="small"
icon="remove"
category="tertiary"
class="note-action-button js-note-delete btn btn-transparent"
class="note-action-button js-note-delete"
@click="onDelete"
/>
<div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions">
@ -359,7 +359,7 @@ export default {
icon="ellipsis_v"
size="small"
category="tertiary"
class="note-action-button more-actions-toggle btn btn-transparent"
class="note-action-button more-actions-toggle"
data-toggle="dropdown"
@click="closeTooltip"
/>

View File

@ -424,7 +424,7 @@ export default {
<gl-button
:disabled="isDisabled"
category="primary"
variant="success"
variant="confirm"
data-qa-selector="reply_comment_button"
class="gl-mr-3 js-vue-issue-save js-comment-button"
@click="handleUpdate()"

View File

@ -0,0 +1,8 @@
mutation updateEpic($input: UpdateEpicInput!) {
updateIssuableTitle: updateEpic(input: $input) {
epic {
title
}
errors
}
}

View File

@ -19,12 +19,9 @@ module Mutations
def resolve(project_path:, iid:, assignee_usernames:, operation_mode:)
resource = authorized_find!(project_path: project_path, iid: iid)
users = new_assignees(resource, assignee_usernames)
update_service_class.new(
resource.project,
current_user,
assignee_ids: assignee_ids(resource, assignee_usernames, operation_mode)
).execute(resource)
assign!(resource, users, operation_mode)
{
resource.class.name.underscore.to_sym => resource,
@ -34,10 +31,20 @@ module Mutations
private
def assignee_ids(resource, usernames, mode)
new = UsersFinder.new(current_user, username: usernames).execute.map(&:id)
def assign!(resource, users, operation_mode)
update_service_class.new(
resource.project,
current_user,
assignee_ids: assignee_ids(resource, users, operation_mode)
).execute(resource)
end
transform_list(mode, resource, new)
def new_assignees(resource, usernames)
UsersFinder.new(current_user, username: usernames).execute.to_a
end
def assignee_ids(resource, users, mode)
transform_list(mode, resource, users.map(&:id))
end
def current_assignee_ids(resource)

View File

@ -7,6 +7,19 @@ module Mutations
include Assignable
def assign!(issue, users, mode)
permitted, forbidden = users.partition { |u| u.can?(:read_issue, issue) }
super(issue, permitted, mode)
forbidden.each do |user|
issue.errors.add(
:assignees,
"Cannot assign #{user.to_reference} to #{issue.to_reference}"
)
end
end
def update_service_class
::Issues::UpdateService
end

View File

@ -683,7 +683,9 @@ module Ci
end
def has_kubernetes_active?
project.deployment_platform&.active?
strong_memoize(:has_kubernetes_active) do
project.deployment_platform&.active?
end
end
def freeze_period?

View File

@ -8,6 +8,7 @@ module Clusters
belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project
has_many :agent_tokens, class_name: 'Clusters::AgentToken'
has_many :last_used_agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken'
scope :ordered_by_name, -> { order(:name) }
scope :with_name, -> (name) { where(name: name) }

View File

@ -6,7 +6,7 @@ module Clusters
include TokenAuthenticatable
add_authentication_token_field :token, encrypted: :required, token_generator: -> { Devise.friendly_token(50) }
cached_attr_reader :last_contacted_at
cached_attr_reader :last_used_at
self.table_name = 'cluster_agent_tokens'
@ -21,6 +21,8 @@ module Clusters
validates :description, length: { maximum: 1024 }
validates :name, presence: true, length: { maximum: 255 }
scope :order_last_used_at_desc, -> { order(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
def track_usage
track_values = { last_used_at: Time.current.utc }

View File

@ -240,18 +240,10 @@ class Deployment < ApplicationRecord
def previous_deployment
@previous_deployment ||=
self.class.for_environment(environment_id)
.where(ref: ref)
.where.not(id: id)
.order(id: :desc)
.take
end
def previous_environment_deployment
self.class.for_environment(environment_id)
.success
.where.not(id: self.id)
.order(id: :desc)
.take
.success
.where('id < ?', id)
.order(id: :desc)
.take
end
def stop_action

View File

@ -33,7 +33,7 @@ module Deployments
# meaningful way (i.e. they can't just retry the deploy themselves).
return unless deployment.success?
if (prev = deployment.previous_environment_deployment)
if (prev = deployment.previous_deployment)
link_merge_requests_for_range(prev.sha, deployment.sha)
else
# When no previous deployment is found we fall back to linking all merge

View File

@ -7,15 +7,20 @@ module MergeRequests
# This saves a lot of queries for irrelevant things that cannot possibly
# change in the execution of this service.
def execute(merge_request)
return unless current_user&.can?(:update_merge_request, merge_request)
return merge_request unless current_user&.can?(:update_merge_request, merge_request)
old_ids = merge_request.assignees.map(&:id)
return if old_ids.to_set == update_attrs[:assignee_ids].to_set # no-change
new_ids = new_assignee_ids(merge_request)
return merge_request if new_ids.size != update_attrs[:assignee_ids].size
return merge_request if old_ids.to_set == new_ids.to_set # no-change
merge_request.update!(**update_attrs)
attrs = update_attrs.merge(assignee_ids: new_ids)
merge_request.update!(**attrs)
# Defer the more expensive operations (handle_assignee_changes) to the background
MergeRequests::AssigneesChangeWorker.perform_async(merge_request.id, current_user.id, old_ids)
merge_request
end
def handle_assignee_changes(merge_request, old_assignees)
@ -31,10 +36,33 @@ module MergeRequests
private
def new_assignee_ids(merge_request)
# prime the cache - prevent N+1 lookup during authorization loop.
merge_request.project.team.max_member_access_for_user_ids(update_attrs[:assignee_ids])
User.id_in(update_attrs[:assignee_ids]).map do |user|
if user.can?(:read_merge_request, merge_request)
user.id
else
merge_request.errors.add(
:assignees,
"Cannot assign #{user.to_reference} to #{merge_request.to_reference}"
)
nil
end
end.compact
end
def assignee_ids
params.fetch(:assignee_ids).first(1)
end
def params
ps = super
# allow either assignee_id or assignee_ids, preferring assignee_id if passed.
{ assignee_ids: ps.key?(:assignee_id) ? Array.wrap(ps[:assignee_id]) : ps[:assignee_ids] }
end
def update_attrs
@attrs ||= { updated_at: Time.current, updated_by: current_user, assignee_ids: assignee_ids }
end

View File

@ -13,19 +13,20 @@
- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
%p.slead
GitLab is open source software to collaborate on code.
= _('GitLab is open source software to collaborate on code.')
%br
Manage git repositories with fine-grained access controls that keep your code secure.
= _('Manage git repositories with fine-grained access controls that keep your code secure.')
%br
Perform code reviews and enhance collaboration with merge requests.
= _('Perform code reviews and enhance collaboration with merge requests.')
%br
Each project can also have an issue tracker and a wiki.
= _('Each project can also have an issue tracker and a wiki.')
%br
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
= _('Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.')
%br
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}.
- link_to_promo = link_to(promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer')
= _("Read more about GitLab at %{link_to_promo}.").html_safe % { link_to_promo: link_to_promo }
%p= link_to 'Check the current instance configuration ', help_instance_configuration_url
%p= link_to _('Check the current instance configuration '), help_instance_configuration_url
%hr
.row.gl-mt-3
@ -35,15 +36,15 @@
.col-md-4
.card.links-card
.card-header
Quick help
= _('Quick help')
%ul.content-list
%li= link_to 'See our website for getting help', support_url
%li= link_to _('See our website for getting help'), support_url
%li
%button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' }
Use the search bar on the top of this page
= _('Use the search bar on the top of this page')
%li
%button.btn-blank.btn-link.js-trigger-shortcut{ type: 'button' }
Use shortcuts
= _('Use shortcuts')
- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
%li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/'
%li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare'
%li= link_to _('Get a support subscription'), 'https://about.gitlab.com/pricing/'
%li= link_to _('Compare GitLab editions'), 'https://about.gitlab.com/features/#compare'

View File

@ -39,13 +39,13 @@
- else
.gl-text-center
%h4= s_('Integrations|No linked namespaces')
%p= s_('Integrations|Namespaces are your GitLab groups and subgroups that will be linked to this Jira instance.')
%p= s_('Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.')
%p.jira-connect-app-body.gl-mt-7.gl-font-base.gl-text-center
%strong= s_('Integrations|Browser limitations')
- firefox_link_url = 'https://www.mozilla.org/en-US/firefox/'
- firefox_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: firefox_link_url }
= s_('Integrations|Adding a namespace currently works only in browsers that allow crosssite cookies. Please make sure to use %{firefox_link_start}Firefox%{firefox_link_end} or enable crosssite cookies in your browser when adding a namespace.').html_safe % { firefox_link_start: firefox_link_start, firefox_link_end: '</a>'.html_safe }
= s_('Integrations|Adding a namespace works only in browsers that allow crosssite cookies. Use %{firefox_link_start}Firefox%{firefox_link_end}, or enable crosssite cookies in your browser, when adding a namespace.').html_safe % { firefox_link_start: firefox_link_start, firefox_link_end: '</a>'.html_safe }
= link_to _('Learn more'), 'https://gitlab.com/gitlab-org/gitlab/-/issues/284211', target: '_blank', rel: 'noopener noreferrer'
= webpack_bundle_tag 'performance_bar' if performance_bar_enabled?

View File

@ -1,6 +1,6 @@
.jira-connect-users-container.gl-text-center
- user_link = link_to(current_user.to_reference, user_path(current_user), target: '_blank', rel: 'noopener noreferrer')
%h2= _('You are signed into GitLab as %{user_link}').html_safe % { user_link: user_link }
%h2= _('You are signed in to GitLab as %{user_link}').html_safe % { user_link: user_link }
%p= s_('Integrations|You can now close this window and return to the GitLab for Jira application.')

View File

@ -84,7 +84,7 @@
.col-lg-8
-# TODO: might need an entry in user/profile.md to describe some of these settings
-# https://gitlab.com/gitlab-org/gitlab-foss/issues/60070
%h5= ("Time zone")
%h5= _("Time zone")
= dropdown_tag(_("Select a timezone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
%input.hidden{ :type => 'hidden', :id => 'user_timezone', :name => 'user[timezone]', value: @user.timezone }
.col-lg-12

View File

@ -4,7 +4,7 @@
%input.btn.gl-button.btn-confirm.js-comment-button.js-comment-submit-button{ type: 'submit', value: _('Comment'), data: { qa_selector: 'comment_button' } }
- if @note.can_be_discussion_note?
= button_tag type: 'button', class: 'gl-button btn dropdown-toggle btn-confirm btn-icon js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => _('Open comment type dropdown') do
= button_tag type: 'button', class: 'gl-button btn dropdown-toggle btn-confirm js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => _('Open comment type dropdown') do
= sprite_icon('chevron-down')
%ul#resolvable-comment-menu.dropdown-menu.dropdown-open-top{ data: { dropdown: true } }

View File

@ -0,0 +1,5 @@
---
title: Add ability to order cluster token by last used
merge_request: 57520
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Add unified metrics definition YAML file API endpoint
merge_request: 57270
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Add caching to variables calculation of builds
merge_request: 58286
author:
type: performance

View File

@ -0,0 +1,5 @@
---
title: Externalize strings in help/index.html.haml
merge_request: 58441
author: nuwe1
type: other

View File

@ -0,0 +1,5 @@
---
title: Fix previous deployment fetches wrong deployment
merge_request: 58567
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix EmptyLineAfterFinalLetItBe offenses in spec/models/blob_viewer
merge_request: 58325
author: Huzaifa Iftikhar @huzaifaiftikhar
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix EmptyLineAfterFinalLetItBe offenses in spec/policies
merge_request: 58393
author: Huzaifa Iftikhar @huzaifaiftikhar
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Remove deprecated button classes from issue detail view
merge_request: 57763
author:
type: changed

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class AddClusterAgentTokenLastUsed < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
INDEX = 'index_cluster_agent_tokens_on_last_used_at'
disable_ddl_transaction!
def up
add_concurrent_index :cluster_agent_tokens,
:last_used_at,
name: INDEX,
order: { last_used_at: 'DESC NULLS LAST' }
end
def down
remove_concurrent_index_by_name :cluster_agent_tokens, INDEX
end
end

View File

@ -0,0 +1 @@
079ca92ac58519ce8f575c4cb94bfe6cf209e0c9eac20d3d3a294f5b468bc586

View File

@ -22309,6 +22309,8 @@ CREATE INDEX index_cluster_agent_tokens_on_agent_id ON cluster_agent_tokens USIN
CREATE INDEX index_cluster_agent_tokens_on_created_by_user_id ON cluster_agent_tokens USING btree (created_by_user_id);
CREATE INDEX index_cluster_agent_tokens_on_last_used_at ON cluster_agent_tokens USING btree (last_used_at DESC NULLS LAST);
CREATE UNIQUE INDEX index_cluster_agent_tokens_on_token_encrypted ON cluster_agent_tokens USING btree (token_encrypted);
CREATE INDEX index_cluster_agents_on_created_by_user_id ON cluster_agents USING btree (created_by_user_id);

View File

@ -9896,6 +9896,30 @@ Status: `implemented`
Tiers: `premium`, `ultimate`
### `redis_hll_counters.epics_usage.g_project_management_epic_issue_moved_from_project_monthly`
Counts of MAU moving epic issues between projects
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210405190240_g_project_management_epic_issue_moved_from_project_monthly.yml)
Group: `group::product planning`
Status: `implemented`
Tiers: `premium`, `ultimate`
### `redis_hll_counters.epics_usage.g_project_management_epic_issue_moved_from_project_weekly`
Counts of WAU moving epic issues between projects
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210405185814_g_project_management_epic_issue_moved_from_project_weekly.yml)
Group: `group::product planning`
Status: `implemented`
Tiers: `premium`, `ultimate`
### `redis_hll_counters.epics_usage.g_project_management_epic_issue_removed_monthly`
Count of MAU removing issues from epics

View File

@ -1411,6 +1411,37 @@ bin/rake gitlab:usage_data:dump_sql_in_json
bin/rake gitlab:usage_data:dump_sql_in_yaml > ~/Desktop/usage-metrics-2020-09-02.yaml
```
## Export metric definitions as a single YAML file
Use this API endpoint to export all metric definitions as a single YAML file, similar to the [Metrics Dictionary](dictionary.md), for easier importing.
```plaintext
GET /usage_data/metric_definitions
```
Response
```yaml
---
- key_path: redis_hll_counters.search.i_search_paid_monthly
description: Calculated unique users to perform a search with a paid license enabled
by month
product_section: enablement
product_stage: enablement
product_group: group::global search
product_category: global_search
value_type: number
status: data_available
time_frame: 28d
data_source: redis_hll
distribution:
- ee
tier:
- premium
- ultimate
...
```
## Generating and troubleshooting usage ping
This activity is to be done via a detached screen session on a remote server.

View File

@ -424,7 +424,13 @@ module API
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params.has_key?(:remove_source_branch)
mr_params = convert_parameters_from_legacy_format(mr_params)
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
service = if mr_params.one? && (mr_params.keys & %i[assignee_id assignee_ids]).one?
::MergeRequests::UpdateAssigneesService
else
::MergeRequests::UpdateService
end
merge_request = service.new(user_project, current_user, mr_params).execute(merge_request)
handle_merge_request_errors!(merge_request)

View File

@ -2,7 +2,7 @@
module API
class UsageData < ::API::Base
before { authenticate! }
before { authenticate_non_get! }
feature_category :usage_ping
@ -15,11 +15,9 @@ module API
desc 'Track usage data events' do
detail 'This feature was introduced in GitLab 13.4.'
end
params do
requires :event, type: String, desc: 'The event name that should be tracked'
end
post 'increment_counter' do
event_name = params[:event]
@ -31,7 +29,6 @@ module API
params do
requires :event, type: String, desc: 'The event name that should be tracked'
end
post 'increment_unique_users' do
event_name = params[:event]
@ -39,6 +36,16 @@ module API
status :ok
end
desc 'Get a list of all metric definitions' do
detail 'This feature was introduced in GitLab 13.11.'
end
get 'metric_definitions' do
content_type 'application/yaml'
env['api.format'] = :binary
Gitlab::Usage::MetricDefinition.dump_metrics_yaml
end
end
end
end

View File

@ -69,6 +69,10 @@ module Gitlab
@schemer ||= ::JSONSchemer.schema(Pathname.new(METRIC_SCHEMA_PATH))
end
def dump_metrics_yaml
@metrics_yaml ||= definitions.values.map(&:to_h).map(&:deep_stringify_keys).to_yaml
end
private
def load_all!

View File

@ -93,6 +93,12 @@
aggregation: daily
feature_flag: track_epics_activity
- name: g_project_management_epic_issue_moved_from_project
category: epics_usage
redis_slot: project_management
aggregation: daily
feature_flag: track_epics_activity
- name: g_project_management_epic_closed
category: epics_usage
redis_slot: project_management

View File

@ -3362,10 +3362,10 @@ msgstr ""
msgid "An error occurred when updating the issue due date"
msgstr ""
msgid "An error occurred when updating the issue title"
msgid "An error occurred when updating the issue weight"
msgstr ""
msgid "An error occurred when updating the issue weight"
msgid "An error occurred when updating the title"
msgstr ""
msgid "An error occurred while acknowledging the notification. Refresh the page and try again."
@ -3698,9 +3698,6 @@ msgstr ""
msgid "An issue already exists"
msgstr ""
msgid "An issue title is required"
msgstr ""
msgid "An unauthenticated user"
msgstr ""
@ -5913,6 +5910,9 @@ msgstr ""
msgid "Check the %{docs_link_start}documentation%{docs_link_end}."
msgstr ""
msgid "Check the current instance configuration "
msgstr ""
msgid "Check the elasticsearch.log file to debug why the migration was halted and make any changes before retrying the migration. When you fix the cause of the failure, click \"Retry migration\", and the migration will be scheduled to be retried in the background."
msgstr ""
@ -7889,6 +7889,9 @@ msgstr ""
msgid "Compare Git revisions"
msgstr ""
msgid "Compare GitLab editions"
msgstr ""
msgid "Compare Revisions"
msgstr ""
@ -11355,6 +11358,9 @@ msgstr ""
msgid "Dynamic Application Security Testing (DAST)"
msgstr ""
msgid "Each project can also have an issue tracker and a wiki."
msgstr ""
msgid "Edit"
msgstr ""
@ -14351,6 +14357,9 @@ msgstr ""
msgid "Get a free instance review"
msgstr ""
msgid "Get a support subscription"
msgstr ""
msgid "Get started"
msgstr ""
@ -14480,6 +14489,9 @@ msgstr ""
msgid "GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later."
msgstr ""
msgid "GitLab is open source software to collaborate on code."
msgstr ""
msgid "GitLab is undergoing maintenance and is operating in a read-only mode."
msgstr ""
@ -15095,6 +15107,9 @@ msgstr ""
msgid "GroupSAML|Before enforcing SSO, enable SAML authentication."
msgstr ""
msgid "GroupSAML|Before enforcing SSO-access for Git, enable SSO-only authentication for web activity."
msgstr ""
msgid "GroupSAML|Certificate fingerprint"
msgstr ""
@ -16847,7 +16862,7 @@ msgstr ""
msgid "Integrations|Add namespace"
msgstr ""
msgid "Integrations|Adding a namespace currently works only in browsers that allow crosssite cookies. Please make sure to use %{firefox_link_start}Firefox%{firefox_link_end} or enable crosssite cookies in your browser when adding a namespace."
msgid "Integrations|Adding a namespace works only in browsers that allow crosssite cookies. Use %{firefox_link_start}Firefox%{firefox_link_end}, or enable crosssite cookies in your browser, when adding a namespace."
msgstr ""
msgid "Integrations|All details"
@ -16907,7 +16922,7 @@ msgstr ""
msgid "Integrations|Namespace successfully linked"
msgstr ""
msgid "Integrations|Namespaces are your GitLab groups and subgroups that will be linked to this Jira instance."
msgid "Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance."
msgstr ""
msgid "Integrations|No available namespaces."
@ -17420,9 +17435,6 @@ msgstr ""
msgid "Issue published on status page."
msgstr ""
msgid "Issue title"
msgstr ""
msgid "Issue types"
msgstr ""
@ -18969,6 +18981,9 @@ msgstr ""
msgid "Manage applications that you've authorized to use your account."
msgstr ""
msgid "Manage git repositories with fine-grained access controls that keep your code secure."
msgstr ""
msgid "Manage group labels"
msgstr ""
@ -22659,6 +22674,9 @@ msgstr ""
msgid "Pending comments"
msgstr ""
msgid "Pending sync…"
msgstr ""
msgid "People without permission will never get a notification and won't be able to comment."
msgstr ""
@ -22674,6 +22692,9 @@ msgstr ""
msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
msgstr ""
msgid "Perform code reviews and enhance collaboration with merge requests."
msgstr ""
msgid "Perform common operations on GitLab project"
msgstr ""
@ -25545,6 +25566,9 @@ msgstr ""
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
msgid "Quick help"
msgstr ""
msgid "Quick range"
msgstr ""
@ -25584,6 +25608,9 @@ msgstr ""
msgid "Read more"
msgstr ""
msgid "Read more about GitLab at %{link_to_promo}."
msgstr ""
msgid "Read more about project permissions %{help_link_open}here%{help_link_close}"
msgstr ""
@ -27771,6 +27798,9 @@ msgstr ""
msgid "See metrics"
msgstr ""
msgid "See our website for getting help"
msgstr ""
msgid "See the affected projects in the GitLab admin panel"
msgstr ""
@ -29854,6 +29884,9 @@ msgstr ""
msgid "Successfully unlocked"
msgstr ""
msgid "Successfully updated %{last_updated_timeago}."
msgstr ""
msgid "Successfully verified domain ownership"
msgstr ""
@ -32068,6 +32101,9 @@ msgstr ""
msgid "Time until first merge request"
msgstr ""
msgid "Time zone"
msgstr ""
msgid "TimeTrackingEstimated|Est"
msgstr ""
@ -33169,6 +33205,12 @@ msgstr ""
msgid "Update %{sourcePath} file"
msgstr ""
msgid "Update Now"
msgstr ""
msgid "Update Scheduled…"
msgstr ""
msgid "Update all"
msgstr ""
@ -33241,6 +33283,9 @@ msgstr ""
msgid "Updating"
msgstr ""
msgid "Updating…"
msgstr ""
msgid "Upgrade offers available!"
msgstr ""
@ -33586,9 +33631,15 @@ msgstr ""
msgid "Use one line per URI"
msgstr ""
msgid "Use shortcuts"
msgstr ""
msgid "Use template"
msgstr ""
msgid "Use the search bar on the top of this page"
msgstr ""
msgid "Use this token to validate received payloads."
msgstr ""
@ -33604,6 +33655,9 @@ msgstr ""
msgid "Used by members to sign in to your group in GitLab"
msgstr ""
msgid "Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises."
msgstr ""
msgid "Used programming language"
msgstr ""
@ -35218,7 +35272,7 @@ msgstr ""
msgid "You are receiving this message because you are a GitLab administrator for %{url}."
msgstr ""
msgid "You are signed into GitLab as %{user_link}"
msgid "You are signed in to GitLab as %{user_link}"
msgstr ""
msgid "You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico."

View File

@ -173,7 +173,7 @@ RSpec.describe 'Merge request > User posts notes', :js do
it 'allows using markdown buttons after saving a note and then trying to edit it again' do
page.within('.current-note-edit-form') do
fill_in 'note[note]', with: 'This is the new content'
find('.btn-success').click
find('.btn-confirm').click
end
find('.note').hover
@ -191,7 +191,7 @@ RSpec.describe 'Merge request > User posts notes', :js do
it 'appends the edited at time to the note' do
page.within('.current-note-edit-form') do
fill_in 'note[note]', with: 'Some new content'
find('.btn-success').click
find('.btn-confirm').click
end
page.within("#note_#{note.id}") do

View File

@ -4,10 +4,10 @@ import Vuex from 'vuex';
import { stubComponent } from 'helpers/stub_component';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
@ -102,8 +102,8 @@ describe('BoardContentSidebar', () => {
expect(wrapper.find(BoardSidebarLabelsSelect).exists()).toBe(true);
});
it('renders BoardSidebarIssueTitle', () => {
expect(wrapper.find(BoardSidebarIssueTitle).exists()).toBe(true);
it('renders BoardSidebarTitle', () => {
expect(wrapper.find(BoardSidebarTitle).exists()).toBe(true);
});
it('renders BoardSidebarDueDate', () => {

View File

@ -1,11 +1,11 @@
import { GlAlert, GlFormInput, GlForm } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { createStore } from '~/boards/stores';
import createFlash from '~/flash';
const TEST_TITLE = 'New issue title';
const TEST_TITLE = 'New item title';
const TEST_ISSUE_A = {
id: 'gid://gitlab/Issue/1',
iid: 8,
@ -21,7 +21,7 @@ const TEST_ISSUE_B = {
jest.mock('~/flash');
describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
let wrapper;
let store;
@ -32,12 +32,12 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
wrapper = null;
});
const createWrapper = (issue = TEST_ISSUE_A) => {
const createWrapper = (item = TEST_ISSUE_A) => {
store = createStore();
store.state.boardItems = { [issue.id]: { ...issue } };
store.dispatch('setActiveId', { id: issue.id });
store.state.boardItems = { [item.id]: { ...item } };
store.dispatch('setActiveId', { id: item.id });
wrapper = shallowMount(BoardSidebarIssueTitle, {
wrapper = shallowMount(BoardSidebarTitle, {
store,
provide: {
canUpdate: true,
@ -53,7 +53,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
const findFormInput = () => wrapper.find(GlFormInput);
const findEditableItem = () => wrapper.find(BoardEditableItem);
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findTitle = () => wrapper.find('[data-testid="issue-title"]');
const findTitle = () => wrapper.find('[data-testid="item-title"]');
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
it('renders title and reference', () => {
@ -73,7 +73,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
beforeEach(async () => {
createWrapper();
jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
store.state.boardItems[TEST_ISSUE_A.id].title = TEST_TITLE;
});
findFormInput().vm.$emit('input', TEST_TITLE);
@ -87,7 +87,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
});
it('commits change to the server', () => {
expect(wrapper.vm.setActiveIssueTitle).toHaveBeenCalledWith({
expect(wrapper.vm.setActiveItemTitle).toHaveBeenCalledWith({
title: TEST_TITLE,
projectPath: 'h/b',
});
@ -98,14 +98,14 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
beforeEach(async () => {
createWrapper();
jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {});
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {});
findFormInput().vm.$emit('input', '');
findForm().vm.$emit('submit', { preventDefault: () => {} });
await wrapper.vm.$nextTick();
});
it('commits change to the server', () => {
expect(wrapper.vm.setActiveIssueTitle).not.toHaveBeenCalled();
expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled();
});
});
@ -122,7 +122,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
it('does not collapses sidebar and shows alert', () => {
expect(findCollapsed().isVisible()).toBe(false);
expect(findAlert().exists()).toBe(true);
expect(localStorage.getItem(`${TEST_ISSUE_A.id}/issue-title-pending-changes`)).toBe(
expect(localStorage.getItem(`${TEST_ISSUE_A.id}/item-title-pending-changes`)).toBe(
TEST_TITLE,
);
});
@ -130,7 +130,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
describe('when accessing the form with pending changes', () => {
beforeAll(() => {
localStorage.setItem(`${TEST_ISSUE_A.id}/issue-title-pending-changes`, TEST_TITLE);
localStorage.setItem(`${TEST_ISSUE_A.id}/item-title-pending-changes`, TEST_TITLE);
createWrapper();
});
@ -146,7 +146,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
beforeEach(async () => {
createWrapper(TEST_ISSUE_B);
jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
store.state.boardItems[TEST_ISSUE_B.id].title = TEST_TITLE;
});
findFormInput().vm.$emit('input', TEST_TITLE);
@ -155,7 +155,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
});
it('collapses sidebar and render former title', () => {
expect(wrapper.vm.setActiveIssueTitle).not.toHaveBeenCalled();
expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled();
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toBe(TEST_ISSUE_B.title);
});
@ -165,7 +165,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
beforeEach(async () => {
createWrapper(TEST_ISSUE_B);
jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
throw new Error(['failed mutation']);
});
findFormInput().vm.$emit('input', 'Invalid title');
@ -173,7 +173,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
await wrapper.vm.$nextTick();
});
it('collapses sidebar and renders former issue title', () => {
it('collapses sidebar and renders former item title', () => {
expect(findCollapsed().isVisible()).toBe(true);
expect(findTitle().text()).toContain(TEST_ISSUE_B.title);
expect(createFlash).toHaveBeenCalled();

View File

@ -1223,9 +1223,13 @@ describe('setActiveIssueMilestone', () => {
});
});
describe('setActiveIssueTitle', () => {
const state = { boardItems: { [mockIssue.id]: mockIssue } };
const getters = { activeBoardItem: mockIssue };
describe('setActiveItemTitle', () => {
const state = {
boardItems: { [mockIssue.id]: mockIssue },
issuableType: 'issue',
fullPath: 'path/f',
};
const getters = { activeBoardItem: mockIssue, isEpicBoard: false };
const testTitle = 'Test Title';
const input = {
title: testTitle,
@ -1235,7 +1239,7 @@ describe('setActiveIssueTitle', () => {
it('should commit title after setting the issue', (done) => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
updateIssue: {
updateIssuableTitle: {
issue: {
title: testTitle,
},
@ -1251,7 +1255,7 @@ describe('setActiveIssueTitle', () => {
};
testAction(
actions.setActiveIssueTitle,
actions.setActiveItemTitle,
input,
{ ...state, ...getters },
[
@ -1270,7 +1274,7 @@ describe('setActiveIssueTitle', () => {
.spyOn(gqlClient, 'mutate')
.mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
await expect(actions.setActiveIssueTitle({ getters }, input)).rejects.toThrow(Error);
await expect(actions.setActiveItemTitle({ getters }, input)).rejects.toThrow(Error);
});
});

View File

@ -3,18 +3,11 @@
require 'spec_helper'
RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
let(:project) { create(:project, :repository) }
let(:user) { create(:user, developer_projects: [project]) }
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user, developer_projects: [project]) }
let(:seeds_block) { }
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
project: project,
current_user: user,
origin_ref: 'master',
seeds_block: seeds_block)
end
let(:command) { initialize_command }
let(:pipeline) { build(:ci_pipeline, project: project) }
describe '#perform!' do
@ -27,13 +20,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
end
subject(:run_chain) do
[
Gitlab::Ci::Pipeline::Chain::Config::Content.new(pipeline, command),
Gitlab::Ci::Pipeline::Chain::Config::Process.new(pipeline, command),
Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules.new(pipeline, command)
].map(&:perform!)
described_class.new(pipeline, command).perform!
run_previous_chain(pipeline, command)
perform_seed(pipeline, command)
end
it 'allocates next IID' do
@ -228,5 +216,86 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
end
end
end
context 'N+1 queries' do
it 'avoids N+1 queries when calculating variables of jobs' do
pipeline1, command1 = prepare_pipeline1
pipeline2, command2 = prepare_pipeline2
control = ActiveRecord::QueryRecorder.new do
perform_seed(pipeline1, command1)
end
expect { perform_seed(pipeline2, command2) }.not_to exceed_query_limit(
control.count + expected_extra_queries
)
end
private
def prepare_pipeline1
config1 = { build: { stage: 'build', script: 'build' } }
stub_ci_pipeline_yaml_file(YAML.dump(config1))
pipeline1 = build(:ci_pipeline, project: project)
command1 = initialize_command
run_previous_chain(pipeline1, command1)
[pipeline1, command1]
end
def prepare_pipeline2
config2 = { build1: { stage: 'build', script: 'build1' },
build2: { stage: 'build', script: 'build2' },
test: { stage: 'build', script: 'test' } }
stub_ci_pipeline_yaml_file(YAML.dump(config2))
pipeline2 = build(:ci_pipeline, project: project)
command2 = initialize_command
run_previous_chain(pipeline2, command2)
[pipeline2, command2]
end
def expected_extra_queries
extra_jobs = 2
non_handled_sql_queries = 3
# 1. Ci::Build Load () SELECT "ci_builds".* FROM "ci_builds"
# WHERE "ci_builds"."type" = 'Ci::Build'
# AND "ci_builds"."commit_id" IS NULL
# AND ("ci_builds"."retried" = FALSE OR "ci_builds"."retried" IS NULL)
# AND (stage_idx < 1)
# 2. Ci::InstanceVariable Load => `Ci::InstanceVariable#cached_data` => already cached with `fetch_memory_cache`
# 3. Ci::Variable Load => `Project#ci_variables_for` => already cached with `Gitlab::SafeRequestStore`
extra_jobs * non_handled_sql_queries
end
end
private
def run_previous_chain(pipeline, command)
[
Gitlab::Ci::Pipeline::Chain::Config::Content.new(pipeline, command),
Gitlab::Ci::Pipeline::Chain::Config::Process.new(pipeline, command),
Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules.new(pipeline, command)
].map(&:perform!)
end
def perform_seed(pipeline, command)
described_class.new(pipeline, command).perform!
end
end
private
def initialize_command
Gitlab::Ci::Pipeline::Chain::Command.new(
project: project,
current_user: user,
origin_ref: 'master',
seeds_block: seeds_block
)
end
end

View File

@ -25,6 +25,13 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
let(:definition) { described_class.new(path, attributes) }
let(:yaml_content) { attributes.deep_stringify_keys.to_yaml }
def write_metric(metric, path, content)
path = File.join(metric, path)
dir = File.dirname(path)
FileUtils.mkdir_p(dir)
File.write(path, content)
end
it 'has all definitons valid' do
expect { described_class.definitions }.not_to raise_error(Gitlab::Usage::Metric::InvalidMetricError)
end
@ -145,12 +152,54 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
FileUtils.rm_rf(metric1)
FileUtils.rm_rf(metric2)
end
end
def write_metric(metric, path, content)
path = File.join(metric, path)
dir = File.dirname(path)
FileUtils.mkdir_p(dir)
File.write(path, content)
describe 'dump_metrics_yaml' do
let(:other_attributes) do
{
description: 'Test metric definition',
value_type: 'string',
product_category: 'collection',
product_stage: 'growth',
status: 'data_available',
default_generation: 'generation_1',
key_path: 'counter.category.event',
product_group: 'group::product analytics',
time_frame: 'none',
data_source: 'database',
distribution: %w(ee ce),
tier: %w(free starter premium ultimate bronze silver gold)
}
end
let(:other_yaml_content) { other_attributes.deep_stringify_keys.to_yaml }
let(:other_path) { File.join('metrics', 'test_metric.yml') }
let(:metric1) { Dir.mktmpdir('metric1') }
let(:metric2) { Dir.mktmpdir('metric2') }
before do
allow(described_class).to receive(:paths).and_return(
[
File.join(metric1, '**', '*.yml'),
File.join(metric2, '**', '*.yml')
]
)
# Reset memoized `definitions` result
described_class.instance_variable_set(:@definitions, nil)
end
after do
FileUtils.rm_rf(metric1)
FileUtils.rm_rf(metric2)
end
subject { described_class.dump_metrics_yaml }
it 'returns a YAML with both metrics in a sequence' do
write_metric(metric1, path, yaml_content)
write_metric(metric2, other_path, other_yaml_content)
is_expected.to eq([attributes, other_attributes].map(&:deep_stringify_keys).to_yaml)
end
end
end

View File

@ -8,6 +8,7 @@ RSpec.describe BlobViewer::GitlabCiYml do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
let(:blob) { fake_blob(path: '.gitlab-ci.yml', data: data) }
let(:sha) { sample_commit.id }

View File

@ -7,6 +7,7 @@ RSpec.describe BlobViewer::MetricsDashboardYml do
include RepoHelpers
let_it_be(:project) { create(:project, :repository) }
let(:blob) { fake_blob(path: '.gitlab/dashboards/custom-dashboard.yml', data: data) }
let(:sha) { sample_commit.id }
let(:data) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }

View File

@ -8,6 +8,7 @@ RSpec.describe Clusters::Agent do
it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
it { is_expected.to belong_to(:project).class_name('::Project') }
it { is_expected.to have_many(:agent_tokens).class_name('Clusters::AgentToken') }
it { is_expected.to have_many(:last_used_agent_tokens).class_name('Clusters::AgentToken') }
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(63) }

View File

@ -9,6 +9,19 @@ RSpec.describe Clusters::AgentToken do
it { is_expected.to validate_length_of(:name).is_at_most(255) }
it { is_expected.to validate_presence_of(:name) }
describe 'scopes' do
describe '.order_last_used_at_desc' do
let_it_be(:token_1) { create(:cluster_agent_token, last_used_at: 7.days.ago) }
let_it_be(:token_2) { create(:cluster_agent_token, last_used_at: nil) }
let_it_be(:token_3) { create(:cluster_agent_token, last_used_at: 2.days.ago) }
it 'sorts by last_used_at descending, with null values at last' do
expect(described_class.order_last_used_at_desc)
.to eq([token_3, token_1, token_2])
end
end
end
describe '#token' do
it 'is generated on save' do
agent_token = build(:cluster_agent_token, token_encrypted: nil)

View File

@ -573,27 +573,39 @@ RSpec.describe Deployment do
end
describe '#previous_deployment' do
it 'returns the previous deployment' do
deploy1 = create(:deployment, :success)
deploy2 = create(
:deployment,
project: deploy1.project,
environment: deploy1.environment
)
using RSpec::Parameterized::TableSyntax
expect(deploy2.previous_deployment).to eq(deploy1)
let_it_be(:project) { create(:project, :repository) }
let_it_be(:production) { create(:environment, :production, project: project) }
let_it_be(:staging) { create(:environment, :staging, project: project) }
let_it_be(:production_deployment_1) { create(:deployment, :success, project: project, environment: production) }
let_it_be(:production_deployment_2) { create(:deployment, :success, project: project, environment: production) }
let_it_be(:production_deployment_3) { create(:deployment, :failed, project: project, environment: production) }
let_it_be(:production_deployment_4) { create(:deployment, :canceled, project: project, environment: production) }
let_it_be(:staging_deployment_1) { create(:deployment, :failed, project: project, environment: staging) }
let_it_be(:staging_deployment_2) { create(:deployment, :success, project: project, environment: staging) }
let_it_be(:production_deployment_5) { create(:deployment, :success, project: project, environment: production) }
let_it_be(:staging_deployment_3) { create(:deployment, :success, project: project, environment: staging) }
where(:pointer, :expected_previous_deployment) do
'production_deployment_1' | nil
'production_deployment_2' | 'production_deployment_1'
'production_deployment_3' | 'production_deployment_2'
'production_deployment_4' | 'production_deployment_2'
'staging_deployment_1' | nil
'staging_deployment_2' | nil
'production_deployment_5' | 'production_deployment_2'
'staging_deployment_3' | 'staging_deployment_2'
end
it 'returns nothing if the refs do not match' do
deploy1 = create(:deployment, :success)
deploy2 = create(
:deployment,
:review_app,
project: deploy1.project,
environment: deploy1.environment
)
expect(deploy2.previous_deployment).to be_nil
with_them do
it 'returns the previous deployment' do
if expected_previous_deployment.nil?
expect(send(pointer).previous_deployment).to eq(expected_previous_deployment)
else
expect(send(pointer).previous_deployment).to eq(send(expected_previous_deployment))
end
end
end
end
@ -643,45 +655,6 @@ RSpec.describe Deployment do
end
end
describe '#previous_environment_deployment' do
it 'returns the previous deployment of the same environment' do
deploy1 = create(:deployment, :success)
deploy2 = create(
:deployment,
:success,
project: deploy1.project,
environment: deploy1.environment
)
expect(deploy2.previous_environment_deployment).to eq(deploy1)
end
it 'ignores deployments that were not successful' do
deploy1 = create(:deployment, :failed)
deploy2 = create(
:deployment,
:success,
project: deploy1.project,
environment: deploy1.environment
)
expect(deploy2.previous_environment_deployment).to be_nil
end
it 'ignores deployments for different environments' do
deploy1 = create(:deployment, :success)
preprod = create(:environment, project: deploy1.project, name: 'preprod')
deploy2 = create(
:deployment,
:success,
project: deploy1.project,
environment: preprod
)
expect(deploy2.previous_environment_deployment).to be_nil
end
end
describe '#create_ref' do
let(:deployment) { build(:deployment) }

View File

@ -6,6 +6,7 @@ RSpec.describe ApplicationSetting::TermPolicy do
include TermsHelper
let_it_be(:term) { create(:term) }
let(:user) { create(:user) }
subject(:policy) { described_class.new(user, term) }

View File

@ -283,6 +283,7 @@ RSpec.describe Ci::BuildPolicy do
describe 'manage a web ide terminal' do
let(:build_permissions) { %i[read_web_ide_terminal create_build_terminal update_web_ide_terminal create_build_service_proxy] }
let_it_be(:maintainer) { create(:user) }
let(:owner) { create(:owner) }
let(:admin) { create(:admin) }
let(:maintainer) { create(:user) }

View File

@ -16,6 +16,7 @@ RSpec.describe DesignManagement::DesignPolicy do
let_it_be(:admin) { create(:admin) }
let_it_be(:project) { create(:project, :public, namespace: owner.namespace) }
let_it_be(:issue) { create(:issue, project: project) }
let(:design) { create(:design, issue: issue) }
subject(:design_policy) { described_class.new(current_user, design) }
@ -131,6 +132,7 @@ RSpec.describe DesignManagement::DesignPolicy do
context "for guests in private projects" do
let_it_be(:project) { create(:project, :private) }
let(:current_user) { guest }
it_behaves_like "read-only design abilities"
@ -163,6 +165,7 @@ RSpec.describe DesignManagement::DesignPolicy do
context "when the project is archived" do
let_it_be(:project) { create(:project, :public, :archived) }
let_it_be(:issue) { create(:issue, project: project) }
let(:current_user) { owner }
it_behaves_like "read-only design abilities"

View File

@ -8,6 +8,7 @@ RSpec.describe GroupDeployKeysGroupPolicy do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:group_deploy_key) { create(:group_deploy_key) }
let(:group_deploy_keys_group) { create(:group_deploy_keys_group, group: group, group_deploy_key: group_deploy_key) }
describe 'edit a group deploy key for a given group' do

View File

@ -722,6 +722,7 @@ RSpec.describe GroupPolicy do
describe 'design activity' do
let_it_be(:group) { create(:group, :public) }
let(:current_user) { nil }
subject { described_class.new(current_user, group) }

View File

@ -8,6 +8,7 @@ RSpec.describe ProjectSnippetPolicy do
let_it_be(:other_user) { create(:user) }
let_it_be(:external_user) { create(:user, :external) }
let_it_be(:project) { create(:project, :public) }
let(:snippet) { create(:project_snippet, snippet_visibility, project: project, author: author) }
let(:author) { other_user }
let(:author_permissions) do

View File

@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe ServicePolicy, :models do
let_it_be(:user) { create(:user) }
let(:project) { integration.project }
subject(:policy) { Ability.policy_for(user, integration) }

View File

@ -80,7 +80,7 @@ RSpec.describe 'Setting assignees of a merge request' do
end
context 'with assignees already assigned' do
let(:db_query_limit) { 38 }
let(:db_query_limit) { 39 }
before do
merge_request.assignees = [assignee2]

View File

@ -2151,6 +2151,23 @@ RSpec.describe API::MergeRequests do
let(:entity) { merge_request }
end
context 'when only assignee_ids are provided' do
let(:params) do
{
assignee_ids: [user2.id]
}
end
it 'sets the assignees' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['assignees']).to contain_exactly(
a_hash_including('name' => user2.name)
)
end
end
context 'accepts reviewer_ids' do
let(:params) do
{

View File

@ -161,4 +161,23 @@ RSpec.describe API::UsageData do
end
end
end
describe 'GET /usage_data/metric_definitions' do
let(:endpoint) { '/usage_data/metric_definitions' }
let(:metric_yaml) do
{ 'key_path' => 'counter.category.event', 'description' => 'Metric description' }.to_yaml
end
context 'without authentication' do
it 'returns a YAML file', :aggregate_failures do
allow(Gitlab::Usage::MetricDefinition).to receive(:dump_metrics_yaml).and_return(metric_yaml)
get api(endpoint)
expect(response).to have_gitlab_http_status(:ok)
expect(response.media_type).to eq('application/yaml')
expect(response.body).to eq(metric_yaml)
end
end
end
end

View File

@ -47,6 +47,12 @@ RSpec.describe MergeRequests::UpdateAssigneesService do
.and change(merge_request, :updated_by).to(user)
end
it 'does not update the assignees if they do not have access' do
opts[:assignee_ids] = [create(:user).id]
expect { update_merge_request }.not_to change(merge_request, :assignee_ids)
end
it 'is more efficient than using the full update-service' do
allow(MergeRequests::AssigneesChangeWorker)
.to receive(:perform_async)

View File

@ -22,17 +22,28 @@ RSpec.shared_examples 'an assignable resource' do
assignee_usernames: assignee_usernames)
end
before do
resource.project.add_developer(assignee)
resource.project.add_developer(assignee2)
end
it 'raises an error if the resource is not accessible to the user' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
it 'does not change assignees if the resource is not accessible to the assignees' do
resource.project.add_developer(user)
expect { subject }.not_to change { resource.reload.assignee_ids }
end
it 'returns an operational error if the resource is not accessible to the assignees' do
resource.project.add_developer(user)
result = subject
expect(result[:errors]).to include a_string_matching(/Cannot assign/)
end
context 'when the user can update the resource' do
before do
resource.project.add_developer(assignee)
resource.project.add_developer(assignee2)
resource.project.add_developer(user)
end