Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-04-09 12:09:10 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-04-09 12:09:10 +0300
commit7484851b5f762fad81ddecb4735d22828e205af1 (patch)
treeb7e5da6212d2b32626b9b3d70c9089b98d05bb92
parent760822a53715549c2f115370ed24a19db1b7d63a (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_manual_todo.yml9
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue6
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue (renamed from app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue)48
-rw-r--r--app/assets/javascripts/boards/constants.js11
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/stores/actions.js20
-rw-r--r--app/assets/javascripts/issue_show/components/header_actions.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue6
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql8
-rw-r--r--app/graphql/mutations/concerns/mutations/assignable.rb23
-rw-r--r--app/graphql/mutations/issues/set_assignees.rb13
-rw-r--r--app/models/ci/pipeline.rb4
-rw-r--r--app/models/clusters/agent.rb1
-rw-r--r--app/models/clusters/agent_token.rb4
-rw-r--r--app/models/deployment.rb16
-rw-r--r--app/services/deployments/link_merge_requests_service.rb2
-rw-r--r--app/services/merge_requests/update_assignees_service.rb34
-rw-r--r--app/views/help/index.html.haml27
-rw-r--r--app/views/jira_connect/subscriptions/index.html.haml4
-rw-r--r--app/views/jira_connect/users/show.html.haml2
-rw-r--r--app/views/profiles/show.html.haml2
-rw-r--r--app/views/shared/notes/_comment_button.html.haml2
-rw-r--r--changelogs/unreleased/322128-token-order.yml5
-rw-r--r--changelogs/unreleased/323548-unify-metrics-definition-yaml-file-in-one-file.yml5
-rw-r--r--changelogs/unreleased/326475-reducing-sql-queries.yml5
-rw-r--r--changelogs/unreleased/Externalise-strings-in-help-index-html-haml.yml5
-rw-r--r--changelogs/unreleased/fix-previous-deployment.yml5
-rw-r--r--changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-models-blob-viewer.yml5
-rw-r--r--changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-policies.yml5
-rw-r--r--changelogs/unreleased/issue-detail-view-deprecated-button.yml5
-rw-r--r--db/migrate/20210407152925_add_cluster_agent_token_last_used.rb20
-rw-r--r--db/schema_migrations/202104071529251
-rw-r--r--db/structure.sql2
-rw-r--r--doc/development/usage_ping/dictionary.md24
-rw-r--r--doc/development/usage_ping/index.md31
-rw-r--r--lib/api/merge_requests.rb8
-rw-r--r--lib/api/usage_data.rb15
-rw-r--r--lib/gitlab/usage/metric_definition.rb4
-rw-r--r--lib/gitlab/usage_data_counters/known_events/epic_events.yml6
-rw-r--r--locale/gitlab.pot76
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb4
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js6
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js (renamed from spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js)36
-rw-r--r--spec/frontend/boards/stores/actions_spec.js16
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb105
-rw-r--r--spec/lib/gitlab/usage/metric_definition_spec.rb59
-rw-r--r--spec/models/blob_viewer/gitlab_ci_yml_spec.rb1
-rw-r--r--spec/models/blob_viewer/metrics_dashboard_yml_spec.rb1
-rw-r--r--spec/models/clusters/agent_spec.rb1
-rw-r--r--spec/models/clusters/agent_token_spec.rb13
-rw-r--r--spec/models/deployment_spec.rb91
-rw-r--r--spec/policies/application_setting/term_policy_spec.rb1
-rw-r--r--spec/policies/ci/build_policy_spec.rb1
-rw-r--r--spec/policies/design_management/design_policy_spec.rb3
-rw-r--r--spec/policies/group_deploy_keys_group_policy_spec.rb1
-rw-r--r--spec/policies/group_policy_spec.rb1
-rw-r--r--spec/policies/project_snippet_policy_spec.rb1
-rw-r--r--spec/policies/service_policy_spec.rb1
-rw-r--r--spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb2
-rw-r--r--spec/requests/api/merge_requests_spec.rb17
-rw-r--r--spec/requests/api/usage_data_spec.rb19
-rw-r--r--spec/services/merge_requests/update_assignees_service_spec.rb6
-rw-r--r--spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb21
64 files changed, 654 insertions, 228 deletions
diff --git a/.rubocop_manual_todo.yml b/.rubocop_manual_todo.yml
index ece475279a4..aa22a908cee 100644
--- a/.rubocop_manual_todo.yml
+++ b/.rubocop_manual_todo.yml
@@ -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
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 21b14124f59..46359cc2bca 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -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"
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
index 0aa65a30b46..b8d3107c377 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_issue_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
@@ -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>
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 68c40d98563..ab5e5fda1d8 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -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,
+ },
+};
diff --git a/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql
index 62e6c1352a6..6ad12d982e0 100644
--- a/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_set_title.mutation.graphql
@@ -1,5 +1,5 @@
mutation issueSetTitle($input: UpdateIssueInput!) {
- updateIssue(input: $input) {
+ updateIssuableTitle: updateIssue(input: $input) {
issue {
title
}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 9211406d8b2..edae4c75de3 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -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,
});
},
diff --git a/app/assets/javascripts/issue_show/components/header_actions.vue b/app/assets/javascripts/issue_show/components/header_actions.vue
index bade2cc8399..2bddbe4faa0 100644
--- a/app/assets/javascripts/issue_show/components/header_actions.vue
+++ b/app/assets/javascripts/issue_show/components/header_actions.vue
@@ -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"
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 5a098bfda14..a2f5f69efb4 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -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"
/>
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 6a60e4de518..a25862a587b 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -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()"
diff --git a/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql
new file mode 100644
index 00000000000..317b48c142d
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/update_epic_title.mutation.graphql
@@ -0,0 +1,8 @@
+mutation updateEpic($input: UpdateEpicInput!) {
+ updateIssuableTitle: updateEpic(input: $input) {
+ epic {
+ title
+ }
+ errors
+ }
+}
diff --git a/app/graphql/mutations/concerns/mutations/assignable.rb b/app/graphql/mutations/concerns/mutations/assignable.rb
index eb2bc6cac2d..d3ab0a1779a 100644
--- a/app/graphql/mutations/concerns/mutations/assignable.rb
+++ b/app/graphql/mutations/concerns/mutations/assignable.rb
@@ -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
+
+ def new_assignees(resource, usernames)
+ UsersFinder.new(current_user, username: usernames).execute.to_a
+ end
- transform_list(mode, resource, new)
+ def assignee_ids(resource, users, mode)
+ transform_list(mode, resource, users.map(&:id))
end
def current_assignee_ids(resource)
diff --git a/app/graphql/mutations/issues/set_assignees.rb b/app/graphql/mutations/issues/set_assignees.rb
index a4d1c755b53..8413c89b010 100644
--- a/app/graphql/mutations/issues/set_assignees.rb
+++ b/app/graphql/mutations/issues/set_assignees.rb
@@ -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
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 00e585abd01..0fe29f09140 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -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?
diff --git a/app/models/clusters/agent.rb b/app/models/clusters/agent.rb
index c5b9dddb1da..7c42fc28a8d 100644
--- a/app/models/clusters/agent.rb
+++ b/app/models/clusters/agent.rb
@@ -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) }
diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb
index d42279502c5..27a3cd8d13d 100644
--- a/app/models/clusters/agent_token.rb
+++ b/app/models/clusters/agent_token.rb
@@ -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 }
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 9bcc6fd90b2..0bfe6172154 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -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
diff --git a/app/services/deployments/link_merge_requests_service.rb b/app/services/deployments/link_merge_requests_service.rb
index caddea46284..39fbef5dee2 100644
--- a/app/services/deployments/link_merge_requests_service.rb
+++ b/app/services/deployments/link_merge_requests_service.rb
@@ -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
diff --git a/app/services/merge_requests/update_assignees_service.rb b/app/services/merge_requests/update_assignees_service.rb
index 48da8e4aed2..e2f724d1aa6 100644
--- a/app/services/merge_requests/update_assignees_service.rb
+++ b/app/services/merge_requests/update_assignees_service.rb
@@ -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
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 03f8539293b..a56eaaf685f 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -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'
diff --git a/app/views/jira_connect/subscriptions/index.html.haml b/app/views/jira_connect/subscriptions/index.html.haml
index c7873991010..8ce46a28252 100644
--- a/app/views/jira_connect/subscriptions/index.html.haml
+++ b/app/views/jira_connect/subscriptions/index.html.haml
@@ -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 cross‑site cookies. Please make sure to use %{firefox_link_start}Firefox%{firefox_link_end} or enable cross‑site 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 cross‑site cookies. Use %{firefox_link_start}Firefox%{firefox_link_end}, or enable cross‑site 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?
diff --git a/app/views/jira_connect/users/show.html.haml b/app/views/jira_connect/users/show.html.haml
index 2ff92ab0dc8..9cc2b41e5c8 100644
--- a/app/views/jira_connect/users/show.html.haml
+++ b/app/views/jira_connect/users/show.html.haml
@@ -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.')
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index cc5ce00669e..15544fb9c45 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -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
diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index d415c64b929..1129fed9c3b 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -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 } }
diff --git a/changelogs/unreleased/322128-token-order.yml b/changelogs/unreleased/322128-token-order.yml
new file mode 100644
index 00000000000..cf2ccd57898
--- /dev/null
+++ b/changelogs/unreleased/322128-token-order.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to order cluster token by last used
+merge_request: 57520
+author:
+type: changed
diff --git a/changelogs/unreleased/323548-unify-metrics-definition-yaml-file-in-one-file.yml b/changelogs/unreleased/323548-unify-metrics-definition-yaml-file-in-one-file.yml
new file mode 100644
index 00000000000..e863c461369
--- /dev/null
+++ b/changelogs/unreleased/323548-unify-metrics-definition-yaml-file-in-one-file.yml
@@ -0,0 +1,5 @@
+---
+title: Add unified metrics definition YAML file API endpoint
+merge_request: 57270
+author:
+type: added
diff --git a/changelogs/unreleased/326475-reducing-sql-queries.yml b/changelogs/unreleased/326475-reducing-sql-queries.yml
new file mode 100644
index 00000000000..bd0c678582e
--- /dev/null
+++ b/changelogs/unreleased/326475-reducing-sql-queries.yml
@@ -0,0 +1,5 @@
+---
+title: Add caching to variables calculation of builds
+merge_request: 58286
+author:
+type: performance
diff --git a/changelogs/unreleased/Externalise-strings-in-help-index-html-haml.yml b/changelogs/unreleased/Externalise-strings-in-help-index-html-haml.yml
new file mode 100644
index 00000000000..ee27af86507
--- /dev/null
+++ b/changelogs/unreleased/Externalise-strings-in-help-index-html-haml.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize strings in help/index.html.haml
+merge_request: 58441
+author: nuwe1
+type: other
diff --git a/changelogs/unreleased/fix-previous-deployment.yml b/changelogs/unreleased/fix-previous-deployment.yml
new file mode 100644
index 00000000000..04d992dbd76
--- /dev/null
+++ b/changelogs/unreleased/fix-previous-deployment.yml
@@ -0,0 +1,5 @@
+---
+title: Fix previous deployment fetches wrong deployment
+merge_request: 58567
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-models-blob-viewer.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-models-blob-viewer.yml
new file mode 100644
index 00000000000..3a1c7af10fe
--- /dev/null
+++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-models-blob-viewer.yml
@@ -0,0 +1,5 @@
+---
+title: Fix EmptyLineAfterFinalLetItBe offenses in spec/models/blob_viewer
+merge_request: 58325
+author: Huzaifa Iftikhar @huzaifaiftikhar
+type: fixed
diff --git a/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-policies.yml b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-policies.yml
new file mode 100644
index 00000000000..3d61b9fa7f2
--- /dev/null
+++ b/changelogs/unreleased/issue-325836-fix-empty-line-after-let-it-be-policies.yml
@@ -0,0 +1,5 @@
+---
+title: Fix EmptyLineAfterFinalLetItBe offenses in spec/policies
+merge_request: 58393
+author: Huzaifa Iftikhar @huzaifaiftikhar
+type: fixed
diff --git a/changelogs/unreleased/issue-detail-view-deprecated-button.yml b/changelogs/unreleased/issue-detail-view-deprecated-button.yml
new file mode 100644
index 00000000000..fafdc6d131f
--- /dev/null
+++ b/changelogs/unreleased/issue-detail-view-deprecated-button.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deprecated button classes from issue detail view
+merge_request: 57763
+author:
+type: changed
diff --git a/db/migrate/20210407152925_add_cluster_agent_token_last_used.rb b/db/migrate/20210407152925_add_cluster_agent_token_last_used.rb
new file mode 100644
index 00000000000..8ade2971539
--- /dev/null
+++ b/db/migrate/20210407152925_add_cluster_agent_token_last_used.rb
@@ -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
diff --git a/db/schema_migrations/20210407152925 b/db/schema_migrations/20210407152925
new file mode 100644
index 00000000000..1a248d78c8f
--- /dev/null
+++ b/db/schema_migrations/20210407152925
@@ -0,0 +1 @@
+079ca92ac58519ce8f575c4cb94bfe6cf209e0c9eac20d3d3a294f5b468bc586 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index a553b47ff84..98f3206ab40 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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);
diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md
index 05375183e18..67466a607cf 100644
--- a/doc/development/usage_ping/dictionary.md
+++ b/doc/development/usage_ping/dictionary.md
@@ -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
diff --git a/doc/development/usage_ping/index.md b/doc/development/usage_ping/index.md
index a62b58d6095..4b584c6ecc9 100644
--- a/doc/development/usage_ping/index.md
+++ b/doc/development/usage_ping/index.md
@@ -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.
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 44fe167e05c..0a1fa9b4fac 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -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)
diff --git a/lib/api/usage_data.rb b/lib/api/usage_data.rb
index 622637e4fc3..7deec15dcac 100644
--- a/lib/api/usage_data.rb
+++ b/lib/api/usage_data.rb
@@ -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
diff --git a/lib/gitlab/usage/metric_definition.rb b/lib/gitlab/usage/metric_definition.rb
index 05b06227039..9c4255a7c92 100644
--- a/lib/gitlab/usage/metric_definition.rb
+++ b/lib/gitlab/usage/metric_definition.rb
@@ -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!
diff --git a/lib/gitlab/usage_data_counters/known_events/epic_events.yml b/lib/gitlab/usage_data_counters/known_events/epic_events.yml
index 816f040920f..e4aaa1f19dc 100644
--- a/lib/gitlab/usage_data_counters/known_events/epic_events.yml
+++ b/lib/gitlab/usage_data_counters/known_events/epic_events.yml
@@ -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
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3054d7ede7c..6e53001aaed 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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 cross‑site cookies. Please make sure to use %{firefox_link_start}Firefox%{firefox_link_end} or enable cross‑site cookies in your browser when adding a namespace."
+msgid "Integrations|Adding a namespace works only in browsers that allow cross‑site cookies. Use %{firefox_link_start}Firefox%{firefox_link_end}, or enable cross‑site 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."
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index b6ec5b72d28..a6dfae72912 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -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
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 7c18f0a438e..7f949739891 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -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', () => {
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
index 723d0345f76..c8ccd4c88a5 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_issue_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
@@ -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();
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 8cb2c35d503..9b034c49075 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -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);
});
});
diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb
index bd5b6d53b01..264076859cb 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb
@@ -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(: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_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user, developer_projects: [project]) }
+ let(:seeds_block) { }
+ 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
diff --git a/spec/lib/gitlab/usage/metric_definition_spec.rb b/spec/lib/gitlab/usage/metric_definition_spec.rb
index 445a4ce972b..e99d720058a 100644
--- a/spec/lib/gitlab/usage/metric_definition_spec.rb
+++ b/spec/lib/gitlab/usage/metric_definition_spec.rb
@@ -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
+
+ 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)
- def write_metric(metric, path, content)
- path = File.join(metric, path)
- dir = File.dirname(path)
- FileUtils.mkdir_p(dir)
- File.write(path, content)
+ is_expected.to eq([attributes, other_attributes].map(&:deep_stringify_keys).to_yaml)
end
end
end
diff --git a/spec/models/blob_viewer/gitlab_ci_yml_spec.rb b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb
index cd885d312dc..803614d90a5 100644
--- a/spec/models/blob_viewer/gitlab_ci_yml_spec.rb
+++ b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb
@@ -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 }
diff --git a/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
index 84dfc5186a8..8d5c7ce84f6 100644
--- a/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
+++ b/spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
@@ -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') }
diff --git a/spec/models/clusters/agent_spec.rb b/spec/models/clusters/agent_spec.rb
index a85a72eba0b..ea7a55480a8 100644
--- a/spec/models/clusters/agent_spec.rb
+++ b/spec/models/clusters/agent_spec.rb
@@ -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) }
diff --git a/spec/models/clusters/agent_token_spec.rb b/spec/models/clusters/agent_token_spec.rb
index 680b351d24a..bde4798abec 100644
--- a/spec/models/clusters/agent_token_spec.rb
+++ b/spec/models/clusters/agent_token_spec.rb
@@ -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)
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index aae0cedc079..c9544569ad6 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -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
- )
-
- expect(deploy2.previous_deployment).to eq(deploy1)
- end
+ using RSpec::Parameterized::TableSyntax
- 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
+ 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
+
+ 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) }
diff --git a/spec/policies/application_setting/term_policy_spec.rb b/spec/policies/application_setting/term_policy_spec.rb
index 00b48402fa6..fd361c8b649 100644
--- a/spec/policies/application_setting/term_policy_spec.rb
+++ b/spec/policies/application_setting/term_policy_spec.rb
@@ -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) }
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 098efd7daa6..1ec749fb394 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -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) }
diff --git a/spec/policies/design_management/design_policy_spec.rb b/spec/policies/design_management/design_policy_spec.rb
index 117279d1638..c62e97dcdb9 100644
--- a/spec/policies/design_management/design_policy_spec.rb
+++ b/spec/policies/design_management/design_policy_spec.rb
@@ -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"
diff --git a/spec/policies/group_deploy_keys_group_policy_spec.rb b/spec/policies/group_deploy_keys_group_policy_spec.rb
index 7ad9b655411..50d555d2094 100644
--- a/spec/policies/group_deploy_keys_group_policy_spec.rb
+++ b/spec/policies/group_deploy_keys_group_policy_spec.rb
@@ -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
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 1aab6f5ca29..f5e389ff338 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -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) }
diff --git a/spec/policies/project_snippet_policy_spec.rb b/spec/policies/project_snippet_policy_spec.rb
index bdf9eaedbf1..8b96aa99f69 100644
--- a/spec/policies/project_snippet_policy_spec.rb
+++ b/spec/policies/project_snippet_policy_spec.rb
@@ -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
diff --git a/spec/policies/service_policy_spec.rb b/spec/policies/service_policy_spec.rb
index 5d2c9c1f6c3..84c74ca7e31 100644
--- a/spec/policies/service_policy_spec.rb
+++ b/spec/policies/service_policy_spec.rb
@@ -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) }
diff --git a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
index e00bdb171bf..bcede4d37dd 100644
--- a/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
+++ b/spec/requests/api/graphql/mutations/merge_requests/set_assignees_spec.rb
@@ -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]
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 3b1842f1c37..37cb8fb7ee5 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -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
{
diff --git a/spec/requests/api/usage_data_spec.rb b/spec/requests/api/usage_data_spec.rb
index d44f179eed8..bacaf960e6a 100644
--- a/spec/requests/api/usage_data_spec.rb
+++ b/spec/requests/api/usage_data_spec.rb
@@ -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
diff --git a/spec/services/merge_requests/update_assignees_service_spec.rb b/spec/services/merge_requests/update_assignees_service_spec.rb
index d2ed24f3460..aa6aad854a6 100644
--- a/spec/services/merge_requests/update_assignees_service_spec.rb
+++ b/spec/services/merge_requests/update_assignees_service_spec.rb
@@ -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)
diff --git a/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb
index 21d62546ccb..022e2308517 100644
--- a/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb
+++ b/spec/support/shared_examples/graphql/mutations/set_assignees_shared_examples.rb
@@ -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