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:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_configuration_options.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue37
-rw-r--r--app/assets/javascripts/boards/components/issue_card_inner.vue20
-rw-r--r--app/assets/javascripts/dropzone_input.js2
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_view.vue6
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue4
-rw-r--r--app/assets/javascripts/snippets/mixins/snippets.js4
-rw-r--r--app/assets/javascripts/snippets/queries/snippet.query.graphql15
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue6
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss1
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss68
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss89
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline_schedules.scss (renamed from app/assets/stylesheets/pages/pipeline_schedules.scss)10
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/graphql/mutations/terraform/state/base.rb22
-rw-r--r--app/graphql/mutations/terraform/state/delete.rb18
-rw-r--r--app/graphql/mutations/terraform/state/lock.rb29
-rw-r--r--app/graphql/mutations/terraform/state/unlock.rb18
-rw-r--r--app/graphql/queries/snippet/project_permissions.query.graphql (renamed from app/assets/javascripts/snippets/queries/projectPermissions.query.graphql)2
-rw-r--r--app/graphql/queries/snippet/snippet.query.graphql65
-rw-r--r--app/graphql/queries/snippet/snippet_blob_content.query.graphql (renamed from app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql)4
-rw-r--r--app/graphql/queries/snippet/user_permissions.query.graphql (renamed from app/assets/javascripts/snippets/queries/userPermissions.query.graphql)2
-rw-r--r--app/graphql/types/mutation_type.rb3
-rw-r--r--app/helpers/invite_members_helper.rb4
-rw-r--r--app/helpers/page_layout_helper.rb5
-rw-r--r--app/models/ci/daily_build_group_report_result.rb18
-rw-r--r--app/services/issuable/common_system_notes_service.rb6
-rw-r--r--app/services/merge_requests/refresh_service.rb6
-rw-r--r--app/services/system_note_service.rb8
-rw-r--r--app/services/system_notes/merge_requests_service.rb10
-rw-r--r--app/views/admin/dev_ops_report/_report.html.haml46
-rw-r--r--app/views/admin/dev_ops_report/show.html.haml9
-rw-r--r--app/views/dashboard/groups/index.html.haml3
-rw-r--r--app/views/projects/pipeline_schedules/edit.html.haml1
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml2
-rw-r--r--app/views/projects/pipeline_schedules/new.html.haml1
-rw-r--r--app/views/shared/groups/_empty_state.html.haml11
-rw-r--r--app/views/snippets/show.html.haml6
-rw-r--r--changelogs/unreleased/216571-terraform-state-mutations.yml5
-rw-r--r--changelogs/unreleased/231494-draft-system-notes.yml5
-rw-r--r--changelogs/unreleased/271542-prefetch-requests-link.yml5
-rw-r--r--changelogs/unreleased/boards-css-clean-up.yml5
-rw-r--r--changelogs/unreleased/fix-broken-paperclip.yml5
-rw-r--r--changelogs/unreleased/refactor-secondary_navigation_elements.yml5
-rw-r--r--changelogs/unreleased/remove-cycle-analytics-from-en.yml5
-rw-r--r--changelogs/unreleased/vij-add-storage-move-table.yml5
-rw-r--r--config/application.rb5
-rw-r--r--config/feature_flags/development/avatar_with_host.yml7
-rw-r--r--db/migrate/20201022144501_create_snippet_repository_storage_move.rb32
-rw-r--r--db/schema_migrations/202010221445011
-rw-r--r--db/structure.sql31
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql124
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json424
-rw-r--r--doc/api/graphql/reference/index.md38
-rw-r--r--doc/user/project/repository/repository_mirroring.md4
-rw-r--r--lib/gitlab/experimentation.rb3
-rw-r--r--locale/en/gitlab.po9
-rw-r--r--locale/gitlab.pot3
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb2
-rw-r--r--spec/factories/ci/daily_build_group_report_results.rb2
-rw-r--r--spec/features/projects/files/user_browses_files_spec.rb49
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap2
-rw-r--r--spec/graphql/mutations/terraform/state/delete_spec.rb55
-rw-r--r--spec/graphql/mutations/terraform/state/lock_spec.rb68
-rw-r--r--spec/graphql/mutations/terraform/state/unlock_spec.rb61
-rw-r--r--spec/helpers/invite_members_helper_spec.rb120
-rw-r--r--spec/helpers/page_layout_helper_spec.rb29
-rw-r--r--spec/models/ci/daily_build_group_report_result_spec.rb24
-rw-r--r--spec/requests/api/graphql/terraform/state/delete_spec.rb23
-rw-r--r--spec/requests/api/graphql/terraform/state/lock_spec.rb25
-rw-r--r--spec/requests/api/graphql/terraform/state/unlock_spec.rb24
-rw-r--r--spec/services/issuable/common_system_notes_service_spec.rb8
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb12
-rw-r--r--spec/services/system_note_service_spec.rb12
-rw-r--r--spec/services/system_notes/merge_requests_service_spec.rb20
-rw-r--r--spec/support/shared_examples/services/common_system_notes_shared_examples.rb4
79 files changed, 1485 insertions, 349 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index badd2d86bf4..194413fa84c 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-2c750a230dae024d4f59a85c7dba66bac5546fe6
+6689311d652362fc41e5b5cb53aeffede352c8b7
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index d1cc23b4c9b..7a468abddf1 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -127,7 +127,7 @@ export default {
'board-type-assignee': list.type === 'assignee',
}"
:data-id="list.id"
- class="board gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal"
+ class="board gl-display-inline-block gl-h-full gl-px-3 gl-vertical-align-top gl-white-space-normal"
data-qa-selector="board_list"
>
<div
diff --git a/app/assets/javascripts/boards/components/board_configuration_options.vue b/app/assets/javascripts/boards/components/board_configuration_options.vue
index ad3d653b905..754b00b54e0 100644
--- a/app/assets/javascripts/boards/components/board_configuration_options.vue
+++ b/app/assets/javascripts/boards/components/board_configuration_options.vue
@@ -43,7 +43,7 @@ export default {
<template>
<div class="append-bottom-20">
- <label class="form-section-title label-bold" for="board-new-name">
+ <label class="label-bold gl-font-lg" for="board-new-name">
{{ __('List options') }}
</label>
<p class="text-secondary gl-mb-3">
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index 793c594cf16..e4ef3600ff9 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -196,9 +196,7 @@ export default {
<p v-if="isDeleteForm">{{ __('Are you sure you want to delete this board?') }}</p>
<form v-else class="js-board-config-modal" @submit.prevent>
<div v-if="!readonly" class="append-bottom-20">
- <label class="form-section-title label-bold" for="board-new-name">{{
- __('Title')
- }}</label>
+ <label class="label-bold gl-font-lg" for="board-new-name">{{ __('Title') }}</label>
<input
id="board-new-name"
ref="name"
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index bb9a1b79d91..65ee73547f9 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -188,8 +188,9 @@ export default {
'gl-py-3 gl-h-full': !list.isExpanded && !isSwimlanesHeader,
'gl-border-b-0': !list.isExpanded || isSwimlanesHeader,
'gl-py-2': !list.isExpanded && isSwimlanesHeader,
+ 'gl-flex-direction-column': !list.isExpanded,
}"
- class="board-title gl-m-0 gl-display-flex js-board-handle"
+ class="board-title gl-m-0 gl-display-flex gl-align-items-center gl-font-base gl-px-3 js-board-handle"
>
<gl-button
v-if="list.isExpandable"
@@ -202,7 +203,15 @@ export default {
@click="toggleExpanded"
/>
<!-- The following is only true in EE and if it is a milestone -->
- <span v-if="showMilestoneListDetails" aria-hidden="true" class="gl-mr-2 milestone-icon">
+ <span
+ v-if="showMilestoneListDetails"
+ aria-hidden="true"
+ class="milestone-icon"
+ :class="{
+ 'gl-mt-3 gl-rotate-90': !list.isExpanded,
+ 'gl-mr-2': list.isExpanded,
+ }"
+ >
<gl-icon name="timer" />
</span>
@@ -210,6 +219,9 @@ export default {
v-if="showAssigneeListDetails"
:href="list.assignee.path"
class="user-avatar-link js-no-trigger"
+ :class="{
+ 'gl-mt-3 gl-rotate-90': !list.isExpanded,
+ }"
>
<img
v-gl-tooltip.hover.bottom
@@ -223,20 +235,28 @@ export default {
</a>
<div
class="board-title-text"
- :class="{ 'gl-display-none': !list.isExpanded && isSwimlanesHeader }"
+ :class="{
+ 'gl-display-none': !list.isExpanded && isSwimlanesHeader,
+ 'gl-flex-grow-0 gl-my-3 gl-mx-0': !list.isExpanded,
+ 'gl-flex-grow-1': list.isExpanded,
+ }"
>
<span
v-if="list.type !== 'label'"
v-gl-tooltip.hover
:class="{
- 'gl-display-inline-block': list.type === 'milestone',
+ 'gl-display-block': !list.isExpanded || list.type === 'milestone',
}"
:title="listTitle"
- class="board-title-main-text block-truncated"
+ class="board-title-main-text gl-text-truncate"
>
{{ list.title }}
</span>
- <span v-if="list.type === 'assignee'" class="board-title-sub-text gl-ml-2">
+ <span
+ v-if="list.type === 'assignee'"
+ class="board-title-sub-text gl-ml-2 gl-font-weight-normal"
+ :class="{ 'gl-display-none': !list.isExpanded }"
+ >
@{{ listAssignee }}
</span>
<gl-label
@@ -279,7 +299,10 @@ export default {
<div
v-if="showBoardListAndBoardInfo"
class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag text-secondary"
- :class="{ 'gl-display-none!': !list.isExpanded && isSwimlanesHeader }"
+ :class="{
+ 'gl-display-none!': !list.isExpanded && isSwimlanesHeader,
+ 'gl-p-0': !list.isExpanded,
+ }"
>
<span class="gl-display-inline-flex">
<gl-tooltip :target="() => $refs.issueCount" :title="issuesTooltipLabel" />
diff --git a/app/assets/javascripts/boards/components/issue_card_inner.vue b/app/assets/javascripts/boards/components/issue_card_inner.vue
index a181ea51c4a..e7926c176a9 100644
--- a/app/assets/javascripts/boards/components/issue_card_inner.vue
+++ b/app/assets/javascripts/boards/components/issue_card_inner.vue
@@ -133,7 +133,7 @@ export default {
</script>
<template>
<div>
- <div class="d-flex board-card-header" dir="auto">
+ <div class="gl-display-flex" dir="auto">
<h4 class="board-card-title gl-mb-0 gl-mt-0">
<gl-icon
v-if="issue.blocked"
@@ -156,7 +156,7 @@ export default {
}}</a>
</h4>
</div>
- <div v-if="showLabelFooter" class="board-card-labels gl-mt-2 d-flex flex-wrap">
+ <div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
<template v-for="label in orderedLabels">
<gl-label
:key="label.id"
@@ -169,24 +169,26 @@ export default {
/>
</template>
</div>
- <div class="board-card-footer d-flex justify-content-between align-items-end">
+ <div
+ class="board-card-footer gl-display-flex gl-justify-content-space-between gl-align-items-flex-end"
+ >
<div
- class="d-flex align-items-start flex-wrap-reverse board-card-number-container overflow-hidden js-board-card-number-container"
+ class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden js-board-card-number-container"
>
<span
v-if="issue.referencePath"
- class="board-card-number overflow-hidden d-flex gl-mr-3 gl-mt-3"
+ class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3"
>
<tooltip-on-truncate
v-if="issueReferencePath"
:title="issueReferencePath"
placement="bottom"
- class="board-issue-path block-truncated bold"
+ class="board-issue-path gl-text-truncate gl-font-weight-bold"
>{{ issueReferencePath }}</tooltip-on-truncate
>
#{{ issue.iid }}
</span>
- <span class="board-info-items gl-mt-3 d-inline-block">
+ <span class="board-info-items gl-mt-3 gl-display-inline-block">
<issue-due-date v-if="issue.dueDate" :date="issue.dueDate" :closed="issue.closed" />
<issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
<issue-card-weight
@@ -196,7 +198,7 @@ export default {
/>
</span>
</div>
- <div class="board-card-assignee d-flex">
+ <div class="board-card-assignee gl-display-flex">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
v-if="shouldRenderAssignee(index)"
@@ -209,7 +211,7 @@ export default {
tooltip-placement="bottom"
>
<span class="js-assignee-tooltip">
- <span class="bold d-block">{{ __('Assignee') }}</span>
+ <span class="gl-font-weight-bold gl-display-block">{{ __('Assignee') }}</span>
{{ assignee.name }}
<span class="text-white-50">@{{ assignee.username }}</span>
</span>
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index f56c402091f..69961d2e07a 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -26,7 +26,7 @@ function getErrorMessage(res) {
export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
const divHover = '<div class="div-dropzone-hover"></div>';
- const iconPaperclip = spriteIcon('paperclip', 'div-dropzone-icon');
+ const iconPaperclip = spriteIcon('paperclip', 'div-dropzone-icon s24');
const $attachButton = form.find('.button-attach-file');
const $attachingFileMessage = form.find('.attaching-file-message');
const $cancelButton = form.find('.button-cancel-uploading-files');
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_view.vue b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
index e88126ea56a..8e817fdbe31 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_view.vue
@@ -1,9 +1,9 @@
<script>
+import GetBlobContent from 'shared_queries/snippet/snippet_blob_content.query.graphql';
+
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobContent from '~/blob/components/blob_content.vue';
-import GetBlobContent from '../queries/snippet.blob.content.query.graphql';
-
import {
SIMPLE_BLOB_VIEWER,
RICH_BLOB_VIEWER,
@@ -21,7 +21,7 @@ export default {
query: GetBlobContent,
variables() {
return {
- ids: this.snippet.id,
+ ids: [this.snippet.id],
rich: this.activeViewerType === RICH_BLOB_VIEWER,
paths: [this.blob.path],
};
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index 30de5a9d0e0..951ef25e3ef 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -11,12 +11,12 @@ import {
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
+import CanCreatePersonalSnippet from 'shared_queries/snippet/user_permissions.query.graphql';
+import CanCreateProjectSnippet from 'shared_queries/snippet/project_permissions.query.graphql';
import { __ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
-import CanCreatePersonalSnippet from '../queries/userPermissions.query.graphql';
-import CanCreateProjectSnippet from '../queries/projectPermissions.query.graphql';
import { joinPaths } from '~/lib/utils/url_utility';
import { fetchPolicies } from '~/lib/graphql';
diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js
index d5e69e2a889..cd9e6312548 100644
--- a/app/assets/javascripts/snippets/mixins/snippets.js
+++ b/app/assets/javascripts/snippets/mixins/snippets.js
@@ -1,4 +1,4 @@
-import GetSnippetQuery from '../queries/snippet.query.graphql';
+import GetSnippetQuery from 'shared_queries/snippet/snippet.query.graphql';
const blobsDefault = [];
@@ -8,7 +8,7 @@ export const getSnippetMixin = {
query: GetSnippetQuery,
variables() {
return {
- ids: this.snippetGid,
+ ids: [this.snippetGid],
};
},
update: data => {
diff --git a/app/assets/javascripts/snippets/queries/snippet.query.graphql b/app/assets/javascripts/snippets/queries/snippet.query.graphql
deleted file mode 100644
index 2f385050d89..00000000000
--- a/app/assets/javascripts/snippets/queries/snippet.query.graphql
+++ /dev/null
@@ -1,15 +0,0 @@
-#import '../fragments/snippetBase.fragment.graphql'
-#import '../fragments/project.fragment.graphql'
-#import "~/graphql_shared/fragments/author.fragment.graphql"
-
-query GetSnippetQuery($ids: [ID!]) {
- snippets(ids: $ids) {
- nodes {
- ...SnippetBase
- ...SnippetProject
- author {
- ...Author
- }
- }
- }
-}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 5d47aed9643..71f415baf0d 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -61,17 +61,17 @@ export default {
<span v-if="canAttachFile" class="uploading-container">
<span class="uploading-progress-container hide">
<template>
- <gl-icon name="media" :size="16" class="gl-vertical-align-text-bottom" />
+ <gl-icon name="media" />
</template>
<span class="attaching-file-message"></span>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<span class="uploading-progress">0%</span>
- <gl-loading-icon inline class="align-text-bottom" />
+ <gl-loading-icon inline />
</span>
<span class="uploading-error-container hide">
<span class="uploading-error-icon">
<template>
- <gl-icon name="media" :size="16" class="gl-vertical-align-text-bottom" />
+ <gl-icon name="media" />
</template>
</span>
<span class="uploading-error-message"></span>
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index f3bf2dfc61c..321a077c2d2 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -25,7 +25,6 @@
@import './pages/notes';
@import './pages/notifications';
@import './pages/pages';
-@import './pages/pipeline_schedules';
@import './pages/pipelines';
@import './pages/profile';
@import './pages/profiles/preferences';
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 7ebc972ac37..3e218de6af9 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -69,7 +69,7 @@
line-height: 28px;
white-space: normal;
- /* Small devices (phones, tablets, 768px and lower) */
+ /* Small devices (phones, 768px and lower) */
@include media-breakpoint-down(xs) {
width: 100%;
}
@@ -92,7 +92,7 @@
padding: 16px 15px 11px;
}
- /* Small devices (phones, tablets, 768px and lower) */
+ /* Small devices (phones, 768px and lower) */
@include media-breakpoint-down(sm) {
width: 100%;
}
@@ -102,15 +102,6 @@
display: inline-block;
text-align: right;
- @include media-breakpoint-down(sm) {
- margin-top: $gl-padding-8;
- }
-
- @include media-breakpoint-up(md) {
- display: flex;
- align-items: center;
- }
-
> .btn,
> .btn-group,
> .btn-container,
@@ -146,6 +137,35 @@
}
}
+ @include media-breakpoint-up(md) {
+ display: flex;
+ align-items: center;
+ }
+
+ @include media-breakpoint-down(md) {
+ $controls-margin: $btn-margin-5 - 2px;
+ flex: 0 0 100%;
+ margin-top: $gl-padding-8;
+
+ .controls-item,
+ .controls-item-full,
+ .controls-item:last-child {
+ flex: 1 1 35%;
+ display: block;
+ width: 100%;
+ margin: $controls-margin;
+
+ .btn,
+ .dropdown {
+ margin: 0;
+ }
+ }
+
+ .controls-item-full {
+ flex: 1 1 100%;
+ }
+ }
+
@include media-breakpoint-down(sm) {
padding-bottom: 0;
width: 100%;
@@ -239,32 +259,6 @@
pre {
width: 100%;
}
-
- @include media-breakpoint-down(md) {
- .nav-controls {
- $controls-margin: $btn-margin-5 - 2px;
- flex: 0 0 100%;
- margin-top: $gl-padding-8;
-
- .controls-item,
- .controls-item-full,
- .controls-item:last-child {
- flex: 1 1 35%;
- display: block;
- width: 100%;
- margin: $controls-margin;
-
- .btn,
- .dropdown {
- margin: 0;
- }
- }
-
- .controls-item-full {
- flex: 1 1 100%;
- }
- }
- }
}
.scrolling-tabs-container {
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index e908e3622ed..ffc15af6329 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -83,9 +83,6 @@
}
.board {
- // the next line cannot be replaced with .d-inline-block because it breaks display: none of SortableJS
- // see https://gitlab.com/gitlab-org/gitlab-foss/issues/64828
- display: inline-block;
width: calc(85vw - 15px);
@include media-breakpoint-up(sm) {
@@ -116,39 +113,10 @@
&.is-collapsed {
width: 50px;
- .board-title {
- flex-direction: column;
- }
-
.board-title-caret {
margin-top: 1px;
}
- .user-avatar-link,
- .milestone-icon {
- margin-top: $gl-padding-8;
- transform: rotate(90deg);
- }
-
- .board-title-text {
- flex-grow: 0;
- margin: $gl-padding-8 0;
-
- .board-title-main-text {
- display: block;
- }
-
- .board-title-sub-text {
- display: none;
- }
- }
-
- .issue-count-badge {
- border: 0;
- white-space: nowrap;
- padding: 0;
- }
-
.board-title-text > span,
.issue-count-badge > span {
height: 16px;
@@ -197,10 +165,7 @@
}
.board-title {
- align-items: center;
- font-size: 1em;
border-bottom: 1px solid var(--gray-100, $gray-100);
- padding: 0 $gl-spacing-scale-3;
height: 3rem;
.js-max-issue-size::before {
@@ -208,21 +173,6 @@
}
}
-.board-title-text {
- flex-grow: 1;
-}
-
-.board-delete.gl-button {
- background-color: transparent;
- outline: 0;
-
- &:hover {
- color: var(--blue-600, $blue-600);
- box-shadow: none;
- }
-}
-
-.board-blank-state,
.board-promotion-state {
background-color: var(--white, $white);
flex: 1;
@@ -230,19 +180,6 @@
overflow-x: hidden;
}
-.board-blank-state-list {
- > li:not(:last-child) {
- margin-bottom: 8px;
- }
-
- .label-color {
- top: 2px;
- width: 16px;
- height: 16px;
- margin-right: 3px;
- }
-}
-
.board-list-component {
min-height: 0; // firefox fix
}
@@ -311,10 +248,6 @@
}
}
-.board-card-header {
- text-align: initial;
-}
-
.board-card-assignee {
margin-top: -$gl-padding-4;
margin-bottom: -$gl-padding-4;
@@ -586,28 +519,6 @@
}
}
-.board-swimlanes {
- overflow-x: auto;
-}
-
.board-header-collapsed-info-icon:hover {
color: var(--gray-900, $gray-900);
}
-
-$epic-icons-spacing: 40px;
-
-.board-epic-lane {
- max-width: calc(100vw - #{$contextual-sidebar-width} - #{$epic-icons-spacing});
-
- .page-with-icon-sidebar & {
- max-width: calc(100vw - #{$contextual-sidebar-collapsed-width} - #{$epic-icons-spacing});
- }
-
- .page-with-icon-sidebar .is-compact & {
- max-width: calc(100vw - #{$contextual-sidebar-collapsed-width} - #{$gutter-width} - #{$epic-icons-spacing});
- }
-
- .is-compact & {
- max-width: calc(100vw - #{$contextual-sidebar-width} - #{$gutter-width} - #{$epic-icons-spacing});
- }
-}
diff --git a/app/assets/stylesheets/pages/pipeline_schedules.scss b/app/assets/stylesheets/page_bundles/pipeline_schedules.scss
index 81716991a36..8a12096e044 100644
--- a/app/assets/stylesheets/pages/pipeline_schedules.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline_schedules.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.pipeline-schedule-form {
.gl-field-error {
margin: 10px 0 0;
@@ -32,11 +34,11 @@
}
.next-run-cell {
- color: $gl-text-color-secondary;
+ color: var(--gray-500, $gray-500);
}
a {
- color: $text-color;
+ color: var(--gl-text-color, $gl-text-color);
}
svg {
@@ -46,13 +48,13 @@
.pipeline-schedules-user-callout {
.bordered-box.content-block {
- border: 1px solid $border-color;
+ border: 1px solid var(--border-color, $border-color);
background-color: transparent;
padding: 16px;
}
#dismiss-callout-btn {
- color: $gl-text-color;
+ color: var(--gl-text-color, $gl-text-color);
}
}
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 6f8dc75f6bd..61c99dd352a 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -193,6 +193,8 @@ class GroupsController < Groups::ApplicationController
protected
def render_show_html
+ record_experiment_user(:invite_members_empty_group_version_a) if ::Gitlab.com?
+
render 'groups/show', locals: { trial: params[:trial] }
end
diff --git a/app/graphql/mutations/terraform/state/base.rb b/app/graphql/mutations/terraform/state/base.rb
new file mode 100644
index 00000000000..b1721c784b1
--- /dev/null
+++ b/app/graphql/mutations/terraform/state/base.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Terraform
+ module State
+ class Base < BaseMutation
+ authorize :admin_terraform_state
+
+ argument :id,
+ Types::GlobalIDType[::Terraform::State],
+ required: true,
+ description: 'Global ID of the Terraform state'
+
+ private
+
+ def find_object(id:)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/terraform/state/delete.rb b/app/graphql/mutations/terraform/state/delete.rb
new file mode 100644
index 00000000000..f08219cb395
--- /dev/null
+++ b/app/graphql/mutations/terraform/state/delete.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Terraform
+ module State
+ class Delete < Base
+ graphql_name 'TerraformStateDelete'
+
+ def resolve(id:)
+ state = authorized_find!(id: id)
+ state.destroy
+
+ { errors: errors_on_object(state) }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/terraform/state/lock.rb b/app/graphql/mutations/terraform/state/lock.rb
new file mode 100644
index 00000000000..d22c8de2560
--- /dev/null
+++ b/app/graphql/mutations/terraform/state/lock.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Terraform
+ module State
+ class Lock < Base
+ graphql_name 'TerraformStateLock'
+
+ def resolve(id:)
+ state = authorized_find!(id: id)
+
+ if state.locked?
+ state.errors.add(:base, 'state is already locked')
+ else
+ state.update(lock_xid: lock_xid, locked_by_user: current_user, locked_at: Time.current)
+ end
+
+ { errors: errors_on_object(state) }
+ end
+
+ private
+
+ def lock_xid
+ SecureRandom.uuid
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/terraform/state/unlock.rb b/app/graphql/mutations/terraform/state/unlock.rb
new file mode 100644
index 00000000000..0818dbd7fb3
--- /dev/null
+++ b/app/graphql/mutations/terraform/state/unlock.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Terraform
+ module State
+ class Unlock < Base
+ graphql_name 'TerraformStateUnlock'
+
+ def resolve(id:)
+ state = authorized_find!(id: id)
+ state.update(lock_xid: nil, locked_by_user: nil, locked_at: nil)
+
+ { errors: errors_on_object(state) }
+ end
+ end
+ end
+ end
+end
diff --git a/app/assets/javascripts/snippets/queries/projectPermissions.query.graphql b/app/graphql/queries/snippet/project_permissions.query.graphql
index 03c81460fb5..0c38e4f8a07 100644
--- a/app/assets/javascripts/snippets/queries/projectPermissions.query.graphql
+++ b/app/graphql/queries/snippet/project_permissions.query.graphql
@@ -1,6 +1,8 @@
query CanCreateProjectSnippet($fullPath: ID!) {
project(fullPath: $fullPath) {
+ __typename
userPermissions {
+ __typename
createSnippet
}
}
diff --git a/app/graphql/queries/snippet/snippet.query.graphql b/app/graphql/queries/snippet/snippet.query.graphql
new file mode 100644
index 00000000000..60a8fe000b0
--- /dev/null
+++ b/app/graphql/queries/snippet/snippet.query.graphql
@@ -0,0 +1,65 @@
+query GetSnippetQuery($ids: [ID!]) {
+ snippets(ids: $ids) {
+ __typename
+ nodes {
+ __typename
+ id
+ title
+ description
+ descriptionHtml
+ createdAt
+ updatedAt
+ visibilityLevel
+ webUrl
+ httpUrlToRepo
+ sshUrlToRepo
+ blobs {
+ __typename
+ nodes {
+ __typename
+ binary
+ name
+ path
+ rawPath
+ size
+ externalStorage
+ renderedAsText
+ simpleViewer {
+ __typename
+ collapsed
+ renderError
+ tooLarge
+ type
+ fileType
+ }
+ richViewer {
+ __typename
+ collapsed
+ renderError
+ tooLarge
+ type
+ fileType
+ }
+ }
+ }
+ userPermissions {
+ __typename
+ adminSnippet
+ updateSnippet
+ }
+ project {
+ __typename
+ fullPath
+ webUrl
+ }
+ author {
+ __typename
+ id
+ avatarUrl
+ name
+ username
+ webUrl
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql b/app/graphql/queries/snippet/snippet_blob_content.query.graphql
index 0e04ee9b7b8..005f42ff726 100644
--- a/app/assets/javascripts/snippets/queries/snippet.blob.content.query.graphql
+++ b/app/graphql/queries/snippet/snippet_blob_content.query.graphql
@@ -1,9 +1,13 @@
query SnippetBlobContent($ids: [ID!], $rich: Boolean!, $paths: [String!]) {
snippets(ids: $ids) {
+ __typename
nodes {
+ __typename
id
blobs(paths: $paths) {
+ __typename
nodes {
+ __typename
path
richData @include(if: $rich)
plainData @skip(if: $rich)
diff --git a/app/assets/javascripts/snippets/queries/userPermissions.query.graphql b/app/graphql/queries/snippet/user_permissions.query.graphql
index c3e5519e266..a4914189807 100644
--- a/app/assets/javascripts/snippets/queries/userPermissions.query.graphql
+++ b/app/graphql/queries/snippet/user_permissions.query.graphql
@@ -1,6 +1,8 @@
query CanCreatePersonalSnippet {
currentUser {
+ __typename
userPermissions {
+ __typename
createSnippet
}
}
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 3f48e7b4a16..0551a745b9b 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -54,6 +54,9 @@ module Types
'If the body of the Note contains only quick actions, the Note will be ' \
'destroyed during the update, and no Note will be returned'
mount_mutation Mutations::Notes::Destroy
+ mount_mutation Mutations::Terraform::State::Delete
+ mount_mutation Mutations::Terraform::State::Lock
+ mount_mutation Mutations::Terraform::State::Unlock
mount_mutation Mutations::Todos::MarkDone
mount_mutation Mutations::Todos::Restore
mount_mutation Mutations::Todos::MarkAllDone
diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb
index ac6ac9979b3..cea28fd4611 100644
--- a/app/helpers/invite_members_helper.rb
+++ b/app/helpers/invite_members_helper.rb
@@ -18,4 +18,8 @@ module InviteMembersHelper
experiment_enabled?(:invite_members_version_b) && !can_import_members?
end
end
+
+ def invite_group_members?(group)
+ experiment_enabled?(:invite_members_empty_group_version_a) && Ability.allowed?(current_user, :admin_group_member, group)
+ end
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 6808ffc3e27..10eb3d71dad 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -57,7 +57,10 @@ module PageLayoutHelper
subject = @project || @user || @group
- image = subject.avatar_url if subject.present?
+ args = {}
+ args[:only_path] = false if Feature.enabled?(:avatar_with_host)
+
+ image = subject.avatar_url(args) if subject.present?
image || default
end
diff --git a/app/models/ci/daily_build_group_report_result.rb b/app/models/ci/daily_build_group_report_result.rb
index e6f02f2e4f3..9c1670378d6 100644
--- a/app/models/ci/daily_build_group_report_result.rb
+++ b/app/models/ci/daily_build_group_report_result.rb
@@ -12,13 +12,21 @@ module Ci
validates :data, json_schema: { filename: "daily_build_group_report_result_data" }
scope :with_included_projects, -> { includes(:project) }
+ scope :by_projects, -> (ids) { where(project_id: ids) }
+ scope :with_coverage, -> { where("(data->'coverage') IS NOT NULL") }
- def self.upsert_reports(data)
- upsert_all(data, unique_by: :index_daily_build_group_report_results_unique_columns) if data.any?
- end
+ store_accessor :data, :coverage
+
+ class << self
+ def upsert_reports(data)
+ upsert_all(data, unique_by: :index_daily_build_group_report_results_unique_columns) if data.any?
+ end
- def self.recent_results(attrs, limit: nil)
- where(attrs).order(date: :desc, group_name: :asc).limit(limit)
+ def recent_results(attrs, limit: nil)
+ where(attrs).order(date: :desc, group_name: :asc).limit(limit)
+ end
end
end
end
+
+Ci::DailyBuildGroupReportResult.prepend_if_ee('EE::Ci::DailyBuildGroupReportResult')
diff --git a/app/services/issuable/common_system_notes_service.rb b/app/services/issuable/common_system_notes_service.rb
index fbc72dc867a..fd2dc3787c2 100644
--- a/app/services/issuable/common_system_notes_service.rb
+++ b/app/services/issuable/common_system_notes_service.rb
@@ -51,11 +51,11 @@ module Issuable
end
end
- def create_wip_note(old_title)
+ def create_draft_note(old_title)
return unless issuable.is_a?(MergeRequest)
if MergeRequest.work_in_progress?(old_title) != issuable.work_in_progress?
- SystemNoteService.handle_merge_request_wip(issuable, issuable.project, current_user)
+ SystemNoteService.handle_merge_request_draft(issuable, issuable.project, current_user)
end
end
@@ -69,7 +69,7 @@ module Issuable
end
def create_title_change_note(old_title)
- create_wip_note(old_title)
+ create_draft_note(old_title)
if issuable.wipless_title_changed(old_title)
SystemNoteService.change_title(issuable, issuable.project, current_user, old_title)
diff --git a/app/services/merge_requests/refresh_service.rb b/app/services/merge_requests/refresh_service.rb
index e5d0b216d6c..ed977a5a872 100644
--- a/app/services/merge_requests/refresh_service.rb
+++ b/app/services/merge_requests/refresh_service.rb
@@ -42,7 +42,7 @@ module MergeRequests
end
notify_about_push(mr)
- mark_mr_as_wip_from_commits(mr)
+ mark_mr_as_draft_from_commits(mr)
execute_mr_web_hooks(mr)
end
@@ -246,7 +246,7 @@ module MergeRequests
notification_service.push_to_merge_request(merge_request, @current_user, new_commits: new_commits, existing_commits: existing_commits)
end
- def mark_mr_as_wip_from_commits(merge_request)
+ def mark_mr_as_draft_from_commits(merge_request)
return unless @commits.present?
commit_shas = merge_request.commit_shas
@@ -257,7 +257,7 @@ module MergeRequests
if wip_commit && !merge_request.work_in_progress?
merge_request.update(title: merge_request.wip_title)
- SystemNoteService.add_merge_request_wip_from_commit(
+ SystemNoteService.add_merge_request_draft_from_commit(
merge_request,
merge_request.project,
@current_user,
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 1a4374f2e94..eacc88f98a3 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -130,12 +130,12 @@ module SystemNoteService
::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).abort_merge_when_pipeline_succeeds(reason)
end
- def handle_merge_request_wip(noteable, project, author)
- ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).handle_merge_request_wip
+ def handle_merge_request_draft(noteable, project, author)
+ ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).handle_merge_request_draft
end
- def add_merge_request_wip_from_commit(noteable, project, author, commit)
- ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).add_merge_request_wip_from_commit(commit)
+ def add_merge_request_draft_from_commit(noteable, project, author, commit)
+ ::SystemNotes::MergeRequestsService.new(noteable: noteable, project: project, author: author).add_merge_request_draft_from_commit(commit)
end
def resolve_all_discussions(merge_request, project, author)
diff --git a/app/services/system_notes/merge_requests_service.rb b/app/services/system_notes/merge_requests_service.rb
index 9b5c9ba20b2..a51e2053394 100644
--- a/app/services/system_notes/merge_requests_service.rb
+++ b/app/services/system_notes/merge_requests_service.rb
@@ -26,16 +26,16 @@ module SystemNotes
create_note(NoteSummary.new(noteable, project, author, body, action: 'merge'))
end
- def handle_merge_request_wip
- prefix = noteable.work_in_progress? ? "marked" : "unmarked"
+ def handle_merge_request_draft
+ action = noteable.work_in_progress? ? "draft" : "ready"
- body = "#{prefix} as a **Work In Progress**"
+ body = "marked this merge request as **#{action}**"
create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
- def add_merge_request_wip_from_commit(commit)
- body = "marked as a **Work In Progress** from #{commit.to_reference(project)}"
+ def add_merge_request_draft_from_commit(commit)
+ body = "marked this merge request as **draft** from #{commit.to_reference(project)}"
create_note(NoteSummary.new(noteable, project, author, body, action: 'title'))
end
diff --git a/app/views/admin/dev_ops_report/_report.html.haml b/app/views/admin/dev_ops_report/_report.html.haml
index 7e464bcde3b..444b1db1500 100644
--- a/app/views/admin/dev_ops_report/_report.html.haml
+++ b/app/views/admin/dev_ops_report/_report.html.haml
@@ -1,20 +1,30 @@
-.devops
- .devops-header
- %h2.devops-header-title{ class: "devops-#{score_level(@metric.average_percentage_score)}-score" }
- = number_to_percentage(@metric.average_percentage_score, precision: 1)
- .devops-header-subtitle
- = _('DevOps')
- %br
- = _('Score')
- = link_to sprite_icon('question-o', css_class: 'devops-header-icon'), help_page_path('user/admin_area/analytics/dev_ops_report')
+- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
- .devops-cards.board-card-container
- - @metric.cards.each do |card|
- = render 'card', card: card
+- if usage_ping_enabled && show_callout?('dev_ops_report_intro_callout_dismissed')
+ = render 'callout'
- .devops-steps.d-none.d-lg-block
- - @metric.idea_to_production_steps.each_with_index do |step, index|
- .devops-step{ class: "devops-#{score_level(step.percentage_score)}-score" }
- = custom_icon("i2p_step_#{index + 1}")
- %h4.devops-step-title
- = step.title
+- if !usage_ping_enabled
+ #js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/product_analytics/usage_ping') } }
+- elsif @metric.blank?
+ = render 'no_data'
+- else
+ .devops
+ .devops-header
+ %h2.devops-header-title{ class: "devops-#{score_level(@metric.average_percentage_score)}-score" }
+ = number_to_percentage(@metric.average_percentage_score, precision: 1)
+ .devops-header-subtitle
+ = _('DevOps')
+ %br
+ = _('Score')
+ = link_to sprite_icon('question-o', css_class: 'devops-header-icon'), help_page_path('user/admin_area/analytics/dev_ops_report')
+
+ .devops-cards.board-card-container
+ - @metric.cards.each do |card|
+ = render 'card', card: card
+
+ .devops-steps.d-none.d-lg-block
+ - @metric.idea_to_production_steps.each_with_index do |step, index|
+ .devops-step{ class: "devops-#{score_level(step.percentage_score)}-score" }
+ = custom_icon("i2p_step_#{index + 1}")
+ %h4.devops-step-title
+ = step.title
diff --git a/app/views/admin/dev_ops_report/show.html.haml b/app/views/admin/dev_ops_report/show.html.haml
index 2ebdadb99a6..3499b68ee1a 100644
--- a/app/views/admin/dev_ops_report/show.html.haml
+++ b/app/views/admin/dev_ops_report/show.html.haml
@@ -1,15 +1,6 @@
- page_title _('DevOps Report')
-- usage_ping_enabled = Gitlab::CurrentSettings.usage_ping_enabled
- add_page_specific_style 'page_bundles/dev_ops_report'
.container
- - if usage_ping_enabled && show_callout?('dev_ops_report_intro_callout_dismissed')
- = render 'callout'
-
.gl-mt-3
- - if !usage_ping_enabled
- #js-devops-empty-state{ data: { is_admin: current_user&.admin.to_s, empty_state_svg_path: image_path('illustrations/convdev/convdev_no_index.svg'), enable_usage_ping_link: metrics_and_profiling_admin_application_settings_path(anchor: 'js-usage-settings'), docs_link: help_page_path('development/product_analytics/usage_ping') } }
- - elsif @metric.blank?
- = render 'no_data'
- - else
= render 'report'
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index 9536ff940f5..afe4f1b84c2 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -6,6 +6,7 @@
= render 'dashboard/groups_head'
- if params[:filter].blank? && @groups.empty?
- = render 'shared/groups/empty_state'
+ .empty-state
+ = render 'shared/groups/empty_state'
- else
= render 'groups'
diff --git a/app/views/projects/pipeline_schedules/edit.html.haml b/app/views/projects/pipeline_schedules/edit.html.haml
index d95fa6da903..29896500ea1 100644
--- a/app/views/projects/pipeline_schedules/edit.html.haml
+++ b/app/views/projects/pipeline_schedules/edit.html.haml
@@ -1,6 +1,7 @@
- add_to_breadcrumbs _("Schedules"), pipeline_schedules_path(@project)
- breadcrumb_title "##{@schedule.id}"
- page_title _("Edit"), @schedule.description, _("Pipeline Schedule")
+- add_page_specific_style 'page_bundles/pipeline_schedules'
%h3.page-title
= _("Edit Pipeline Schedule %{id}") % { id: @schedule.id }
diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index 91083cc0768..1f9547500d6 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -1,6 +1,6 @@
- breadcrumb_title _("Schedules")
-
- page_title _("Pipeline Schedules")
+- add_page_specific_style 'page_bundles/pipeline_schedules'
#pipeline-schedules-callout{ data: { docs_url: help_page_path('ci/pipelines/schedules'), image_url: image_path('illustrations/pipeline_schedule_callout.svg') } }
.top-area
diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml
index cfdaf6d43bb..a2652304768 100644
--- a/app/views/projects/pipeline_schedules/new.html.haml
+++ b/app/views/projects/pipeline_schedules/new.html.haml
@@ -1,6 +1,7 @@
- breadcrumb_title "Schedules"
- @breadcrumb_link = namespace_project_pipeline_schedules_path(@project.namespace, @project)
- page_title _("New Pipeline Schedule")
+- add_page_specific_style 'page_bundles/pipeline_schedules'
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
diff --git a/app/views/shared/groups/_empty_state.html.haml b/app/views/shared/groups/_empty_state.html.haml
index f6b3a49eacb..1d3bc1d6959 100644
--- a/app/views/shared/groups/_empty_state.html.haml
+++ b/app/views/shared/groups/_empty_state.html.haml
@@ -1,8 +1,13 @@
-.group-empty-state.row.align-items-center.justify-content-center
- .icon.text-center.order-md-2
+.row.gl-align-items-center.gl-justify-content-center
+ .order-md-2
= custom_icon("icon_empty_groups")
- .text-content.m-0.order-md-1
+ .text-content.order-md-1{ class: 'gl-m-0!' }
%h4= s_("GroupsEmptyState|A group is a collection of several projects.")
%p= s_("GroupsEmptyState|If you organize your projects under a group, it works like a folder.")
%p= s_("GroupsEmptyState|You can manage your group member’s permissions and access to each project in the group.")
+ - if invite_group_members?(@group)
+ = link_to _('Invite your team'),
+ group_group_members_path(@group),
+ class: 'gl-button btn btn-success-secondary',
+ data: { track_event: 'click_invite_team_group_empty_state', track_label: 'invite_team_group_empty_state' }
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 77a6ff5455e..beb4cf4a6aa 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -1,3 +1,9 @@
+- add_page_startup_graphql_call('snippet/snippet', { ids: [@snippet.to_global_id.uri] })
+- add_page_startup_graphql_call('snippet/snippet_blob_content', { ids: [@snippet.to_global_id.uri], rich: false, paths: [@snippet.file_name] })
+- if @snippet.project_id?
+ - add_page_startup_graphql_call('snippet/project_permissions', { fullPath: @snippet.project_id })
+- else
+ - add_page_startup_graphql_call('snippet/user_permissions')
- @hide_top_links = true
- @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout
- add_to_breadcrumbs _("Snippets"), dashboard_snippets_path
diff --git a/changelogs/unreleased/216571-terraform-state-mutations.yml b/changelogs/unreleased/216571-terraform-state-mutations.yml
new file mode 100644
index 00000000000..fdb29c74f72
--- /dev/null
+++ b/changelogs/unreleased/216571-terraform-state-mutations.yml
@@ -0,0 +1,5 @@
+---
+title: Add GraphQL endpoints to lock, unlock and delete Terraform states
+merge_request: 43955
+author:
+type: added
diff --git a/changelogs/unreleased/231494-draft-system-notes.yml b/changelogs/unreleased/231494-draft-system-notes.yml
new file mode 100644
index 00000000000..adabce0f707
--- /dev/null
+++ b/changelogs/unreleased/231494-draft-system-notes.yml
@@ -0,0 +1,5 @@
+---
+title: Update system note when marking merge request as draft or ready
+merge_request: 45644
+author:
+type: changed
diff --git a/changelogs/unreleased/271542-prefetch-requests-link.yml b/changelogs/unreleased/271542-prefetch-requests-link.yml
new file mode 100644
index 00000000000..c0fdf7eaa6c
--- /dev/null
+++ b/changelogs/unreleased/271542-prefetch-requests-link.yml
@@ -0,0 +1,5 @@
+---
+title: Pre-fetched GraphQL queries for snippet view
+merge_request: 46130
+author:
+type: changed
diff --git a/changelogs/unreleased/boards-css-clean-up.yml b/changelogs/unreleased/boards-css-clean-up.yml
new file mode 100644
index 00000000000..d4b5319741f
--- /dev/null
+++ b/changelogs/unreleased/boards-css-clean-up.yml
@@ -0,0 +1,5 @@
+---
+title: Boards - Fix Milestone icon alignment in header
+merge_request: 45965
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-broken-paperclip.yml b/changelogs/unreleased/fix-broken-paperclip.yml
new file mode 100644
index 00000000000..4f8b15386f9
--- /dev/null
+++ b/changelogs/unreleased/fix-broken-paperclip.yml
@@ -0,0 +1,5 @@
+---
+title: Fix dropzone paperclip and loading icons
+merge_request: 46093
+author:
+type: fixed
diff --git a/changelogs/unreleased/refactor-secondary_navigation_elements.yml b/changelogs/unreleased/refactor-secondary_navigation_elements.yml
new file mode 100644
index 00000000000..3fa55295d5c
--- /dev/null
+++ b/changelogs/unreleased/refactor-secondary_navigation_elements.yml
@@ -0,0 +1,5 @@
+---
+title: Refactor secondary_navigation_elements.scss
+merge_request: 45763
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/remove-cycle-analytics-from-en.yml b/changelogs/unreleased/remove-cycle-analytics-from-en.yml
new file mode 100644
index 00000000000..a9bb98cd2a0
--- /dev/null
+++ b/changelogs/unreleased/remove-cycle-analytics-from-en.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Cycle Analytics message from en i18n message
+merge_request: 45178
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/vij-add-storage-move-table.yml b/changelogs/unreleased/vij-add-storage-move-table.yml
new file mode 100644
index 00000000000..b6af0e53535
--- /dev/null
+++ b/changelogs/unreleased/vij-add-storage-move-table.yml
@@ -0,0 +1,5 @@
+---
+title: Create snippet_repository_storage_moves database table
+merge_request: 45990
+author:
+type: added
diff --git a/config/application.rb b/config/application.rb
index de133414bb0..6f479468d6f 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -174,6 +174,7 @@ module Gitlab
config.assets.precompile << "notify.css"
config.assets.precompile << "mailers/*.css"
config.assets.precompile << "page_bundles/_mixins_and_variables_and_functions.css"
+ config.assets.precompile << "page_bundles/alert_management_details.css"
config.assets.precompile << "page_bundles/boards.css"
config.assets.precompile << "page_bundles/ci_status.css"
config.assets.precompile << "page_bundles/cycle_analytics.css"
@@ -191,13 +192,13 @@ module Gitlab
config.assets.precompile << "page_bundles/milestone.css"
config.assets.precompile << "page_bundles/pipeline.css"
config.assets.precompile << "page_bundles/pipelines.css"
+ config.assets.precompile << "page_bundles/pipeline_schedules.css"
config.assets.precompile << "page_bundles/productivity_analytics.css"
config.assets.precompile << "page_bundles/terminal.css"
config.assets.precompile << "page_bundles/todos.css"
config.assets.precompile << "page_bundles/reports.css"
- config.assets.precompile << "page_bundles/xterm.css"
config.assets.precompile << "page_bundles/wiki.css"
- config.assets.precompile << "page_bundles/alert_management_details.css"
+ config.assets.precompile << "page_bundles/xterm.css"
config.assets.precompile << "lazy_bundles/cropper.css"
config.assets.precompile << "performance_bar.css"
config.assets.precompile << "lib/ace.js"
diff --git a/config/feature_flags/development/avatar_with_host.yml b/config/feature_flags/development/avatar_with_host.yml
new file mode 100644
index 00000000000..10e4b825fc6
--- /dev/null
+++ b/config/feature_flags/development/avatar_with_host.yml
@@ -0,0 +1,7 @@
+---
+name: avatar_with_host
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45776
+rollout_issue_url:
+type: development
+group: group::editor
+default_enabled: false
diff --git a/db/migrate/20201022144501_create_snippet_repository_storage_move.rb b/db/migrate/20201022144501_create_snippet_repository_storage_move.rb
new file mode 100644
index 00000000000..7db38191942
--- /dev/null
+++ b/db/migrate/20201022144501_create_snippet_repository_storage_move.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class CreateSnippetRepositoryStorageMove < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ unless table_exists?(:snippet_repository_storage_moves)
+ with_lock_retries do
+ create_table :snippet_repository_storage_moves do |t|
+ t.timestamps_with_timezone
+ t.references :snippet, index: true, null: false, foreign_key: { on_delete: :cascade }
+ t.integer :state, limit: 2, default: 1, null: false
+ t.text :source_storage_name, null: false
+ t.text :destination_storage_name, null: false
+ end
+ end
+ end
+
+ add_text_limit(:snippet_repository_storage_moves, :source_storage_name, 255, constraint_name: 'snippet_repository_storage_moves_source_storage_name')
+ add_text_limit(:snippet_repository_storage_moves, :destination_storage_name, 255, constraint_name: 'snippet_repository_storage_moves_destination_storage_name')
+ end
+
+ def down
+ with_lock_retries do
+ drop_table :snippet_repository_storage_moves
+ end
+ end
+end
diff --git a/db/schema_migrations/20201022144501 b/db/schema_migrations/20201022144501
new file mode 100644
index 00000000000..4e82b2785d0
--- /dev/null
+++ b/db/schema_migrations/20201022144501
@@ -0,0 +1 @@
+f9a573d50f8b4aeb3d8d2cc2f0223ab9970776d663e49e0f022e96158593d929 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 9fa21c498d5..077c351e555 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -16146,6 +16146,27 @@ CREATE TABLE snippet_repositories (
CONSTRAINT snippet_repositories_verification_failure_text_limit CHECK ((char_length(verification_failure) <= 255))
);
+CREATE TABLE snippet_repository_storage_moves (
+ id bigint NOT NULL,
+ created_at timestamp with time zone NOT NULL,
+ updated_at timestamp with time zone NOT NULL,
+ snippet_id bigint NOT NULL,
+ state smallint DEFAULT 1 NOT NULL,
+ source_storage_name text NOT NULL,
+ destination_storage_name text NOT NULL,
+ CONSTRAINT snippet_repository_storage_moves_destination_storage_name CHECK ((char_length(destination_storage_name) <= 255)),
+ CONSTRAINT snippet_repository_storage_moves_source_storage_name CHECK ((char_length(source_storage_name) <= 255))
+);
+
+CREATE SEQUENCE snippet_repository_storage_moves_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE snippet_repository_storage_moves_id_seq OWNED BY snippet_repository_storage_moves.id;
+
CREATE TABLE snippet_statistics (
snippet_id bigint NOT NULL,
repository_size bigint DEFAULT 0 NOT NULL,
@@ -18048,6 +18069,8 @@ ALTER TABLE ONLY slack_integrations ALTER COLUMN id SET DEFAULT nextval('slack_i
ALTER TABLE ONLY smartcard_identities ALTER COLUMN id SET DEFAULT nextval('smartcard_identities_id_seq'::regclass);
+ALTER TABLE ONLY snippet_repository_storage_moves ALTER COLUMN id SET DEFAULT nextval('snippet_repository_storage_moves_id_seq'::regclass);
+
ALTER TABLE ONLY snippet_user_mentions ALTER COLUMN id SET DEFAULT nextval('snippet_user_mentions_id_seq'::regclass);
ALTER TABLE ONLY snippets ALTER COLUMN id SET DEFAULT nextval('snippets_id_seq'::regclass);
@@ -19434,6 +19457,9 @@ ALTER TABLE ONLY smartcard_identities
ALTER TABLE ONLY snippet_repositories
ADD CONSTRAINT snippet_repositories_pkey PRIMARY KEY (snippet_id);
+ALTER TABLE ONLY snippet_repository_storage_moves
+ ADD CONSTRAINT snippet_repository_storage_moves_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY snippet_statistics
ADD CONSTRAINT snippet_statistics_pkey PRIMARY KEY (snippet_id);
@@ -21700,6 +21726,8 @@ CREATE UNIQUE INDEX index_snippet_repositories_on_disk_path ON snippet_repositor
CREATE INDEX index_snippet_repositories_on_shard_id ON snippet_repositories USING btree (shard_id);
+CREATE INDEX index_snippet_repository_storage_moves_on_snippet_id ON snippet_repository_storage_moves USING btree (snippet_id);
+
CREATE UNIQUE INDEX index_snippet_user_mentions_on_note_id ON snippet_user_mentions USING btree (note_id) WHERE (note_id IS NOT NULL);
CREATE INDEX index_snippets_on_author_id ON snippets USING btree (author_id);
@@ -23408,6 +23436,9 @@ ALTER TABLE ONLY ci_pipeline_artifacts
ALTER TABLE ONLY group_deletion_schedules
ADD CONSTRAINT fk_rails_4b8c694a6c FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
+ALTER TABLE ONLY snippet_repository_storage_moves
+ ADD CONSTRAINT fk_rails_4b950f5b94 FOREIGN KEY (snippet_id) REFERENCES snippets(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY design_management_designs
ADD CONSTRAINT fk_rails_4bb1073360 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index c0aa6360ed2..30948f59fb3 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -2515,6 +2515,26 @@ Identifier of Clusters::Cluster
"""
scalar ClustersClusterID
+"""
+Represents the code coverage summary for a project
+"""
+type CodeCoverageSummary {
+ """
+ Average percentage of the different code coverage results available for the project.
+ """
+ averageCoverage: Float
+
+ """
+ Number of different code coverage results available.
+ """
+ coverageCount: Int
+
+ """
+ Latest date when the code coverage was created for the project.
+ """
+ lastUpdatedAt: Time
+}
+
type Commit {
"""
Author of the commit
@@ -12531,6 +12551,9 @@ type Mutation {
removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload
revertVulnerabilityToDetected(input: RevertVulnerabilityToDetectedInput!): RevertVulnerabilityToDetectedPayload @deprecated(reason: "Use vulnerabilityRevertToDetected. Deprecated in 13.5")
runDastScan(input: RunDASTScanInput!): RunDASTScanPayload @deprecated(reason: "Use DastOnDemandScanCreate. Deprecated in 13.4")
+ terraformStateDelete(input: TerraformStateDeleteInput!): TerraformStateDeletePayload
+ terraformStateLock(input: TerraformStateLockInput!): TerraformStateLockPayload
+ terraformStateUnlock(input: TerraformStateUnlockInput!): TerraformStateUnlockPayload
todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
todoRestore(input: TodoRestoreInput!): TodoRestorePayload
todoRestoreMany(input: TodoRestoreManyInput!): TodoRestoreManyPayload
@@ -13792,6 +13815,12 @@ type Project {
): ClusterAgentConnection
"""
+ Code coverages summary associated with the project. Available only when
+ feature flag `group_coverage_data_report` is enabled
+ """
+ codeCoverageSummary: CodeCoverageSummary
+
+ """
Compliance frameworks associated with the project
"""
complianceFrameworks(
@@ -19011,6 +19040,36 @@ type TerraformStateConnection {
}
"""
+Autogenerated input type of TerraformStateDelete
+"""
+input TerraformStateDeleteInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Global ID of the Terraform state
+ """
+ id: TerraformStateID!
+}
+
+"""
+Autogenerated return type of TerraformStateDelete
+"""
+type TerraformStateDeletePayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+}
+
+"""
An edge in a connection.
"""
type TerraformStateEdge {
@@ -19026,6 +19085,71 @@ type TerraformStateEdge {
}
"""
+Identifier of Terraform::State
+"""
+scalar TerraformStateID
+
+"""
+Autogenerated input type of TerraformStateLock
+"""
+input TerraformStateLockInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Global ID of the Terraform state
+ """
+ id: TerraformStateID!
+}
+
+"""
+Autogenerated return type of TerraformStateLock
+"""
+type TerraformStateLockPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+}
+
+"""
+Autogenerated input type of TerraformStateUnlock
+"""
+input TerraformStateUnlockInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Global ID of the Terraform state
+ """
+ id: TerraformStateID!
+}
+
+"""
+Autogenerated return type of TerraformStateUnlock
+"""
+type TerraformStateUnlockPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+}
+
+"""
Represents the Geo sync and verification state of a terraform state version
"""
type TerraformStateVersionRegistry {
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index c18a87fc67c..675c23c4f52 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -6821,6 +6821,61 @@
},
{
"kind": "OBJECT",
+ "name": "CodeCoverageSummary",
+ "description": "Represents the code coverage summary for a project",
+ "fields": [
+ {
+ "name": "averageCoverage",
+ "description": "Average percentage of the different code coverage results available for the project.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Float",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "coverageCount",
+ "description": "Number of different code coverage results available.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "lastUpdatedAt",
+ "description": "Latest date when the code coverage was created for the project.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Time",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
"name": "Commit",
"description": null,
"fields": [
@@ -36353,6 +36408,87 @@
"deprecationReason": "Use DastOnDemandScanCreate. Deprecated in 13.4"
},
{
+ "name": "terraformStateDelete",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "TerraformStateDeleteInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "TerraformStateDeletePayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "terraformStateLock",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "TerraformStateLockInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "TerraformStateLockPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "terraformStateUnlock",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "TerraformStateUnlockInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "TerraformStateUnlockPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "todoMarkDone",
"description": null,
"args": [
@@ -40475,6 +40611,20 @@
"deprecationReason": null
},
{
+ "name": "codeCoverageSummary",
+ "description": "Code coverages summary associated with the project. Available only when feature flag `group_coverage_data_report` is enabled",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "CodeCoverageSummary",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "complianceFrameworks",
"description": "Compliance frameworks associated with the project",
"args": [
@@ -55104,6 +55254,94 @@
"possibleTypes": null
},
{
+ "kind": "INPUT_OBJECT",
+ "name": "TerraformStateDeleteInput",
+ "description": "Autogenerated input type of TerraformStateDelete",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "id",
+ "description": "Global ID of the Terraform state",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "TerraformStateID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "TerraformStateDeletePayload",
+ "description": "Autogenerated return type of TerraformStateDelete",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "OBJECT",
"name": "TerraformStateEdge",
"description": "An edge in a connection.",
@@ -55149,6 +55387,192 @@
"possibleTypes": null
},
{
+ "kind": "SCALAR",
+ "name": "TerraformStateID",
+ "description": "Identifier of Terraform::State",
+ "fields": null,
+ "inputFields": null,
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "TerraformStateLockInput",
+ "description": "Autogenerated input type of TerraformStateLock",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "id",
+ "description": "Global ID of the Terraform state",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "TerraformStateID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "TerraformStateLockPayload",
+ "description": "Autogenerated return type of TerraformStateLock",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "INPUT_OBJECT",
+ "name": "TerraformStateUnlockInput",
+ "description": "Autogenerated input type of TerraformStateUnlock",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "id",
+ "description": "Global ID of the Terraform state",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "TerraformStateID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "TerraformStateUnlockPayload",
+ "description": "Autogenerated return type of TerraformStateUnlock",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "OBJECT",
"name": "TerraformStateVersionRegistry",
"description": "Represents the Geo sync and verification state of a terraform state version",
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 08abc50fbf4..d2e2a9c2cce 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -390,6 +390,16 @@ Autogenerated return type of ClusterAgentTokenDelete.
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+### CodeCoverageSummary
+
+Represents the code coverage summary for a project.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `averageCoverage` | Float | Average percentage of the different code coverage results available for the project. |
+| `coverageCount` | Int | Number of different code coverage results available. |
+| `lastUpdatedAt` | Time | Latest date when the code coverage was created for the project. |
+
### Commit
| Field | Type | Description |
@@ -1984,6 +1994,7 @@ Autogenerated return type of PipelineRetry.
| `avatarUrl` | String | URL to avatar image file of the project |
| `board` | Board | A single board of the project |
| `clusterAgent` | ClusterAgent | Find a single cluster agent by name |
+| `codeCoverageSummary` | CodeCoverageSummary | Code coverages summary associated with the project. Available only when feature flag `group_coverage_data_report` is enabled |
| `containerExpirationPolicy` | ContainerExpirationPolicy | The container expiration policy of the project |
| `containerRegistryEnabled` | Boolean | Indicates if the project stores Docker container images in a container registry |
| `createdAt` | Time | Timestamp of the project creation |
@@ -2629,6 +2640,33 @@ Completion status of tasks.
| `name` | String! | Name of the Terraform state |
| `updatedAt` | Time! | Timestamp the Terraform state was updated |
+### TerraformStateDeletePayload
+
+Autogenerated return type of TerraformStateDelete.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+
+### TerraformStateLockPayload
+
+Autogenerated return type of TerraformStateLock.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+
+### TerraformStateUnlockPayload
+
+Autogenerated return type of TerraformStateUnlock.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+
### TerraformStateVersionRegistry
Represents the Geo sync and verification state of a terraform state version.
diff --git a/doc/user/project/repository/repository_mirroring.md b/doc/user/project/repository/repository_mirroring.md
index 188699e0c77..1f9835f4f59 100644
--- a/doc/user/project/repository/repository_mirroring.md
+++ b/doc/user/project/repository/repository_mirroring.md
@@ -577,3 +577,7 @@ Should an error occur during a push, GitLab will display an "Error" highlight fo
### 13:Received RST_STREAM with error code 2 with GitHub
If you receive an "13:Received RST_STREAM with error code 2" while mirroring to a GitHub repository, your GitHub settings might be set to block pushes that expose your email address used in commits. Either set your email address on GitHub to be public, or disable the [Block command line pushes that expose my email](https://github.com/settings/emails) setting.
+
+### 4:Deadline Exceeded
+
+When upgrading to GitLab 11.11.8 or newer, a change in how usernames are represented means that you may need to update your mirroring username and password to ensure that `%40` characters are replaced with `@`.
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index d5da260406f..b85fb899348 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -51,6 +51,9 @@ module Gitlab
invite_members_version_b: {
tracking_category: 'Growth::Expansion::Experiment::InviteMembersVersionB'
},
+ invite_members_empty_group_version_a: {
+ tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyGroupVersionA'
+ },
new_create_project_ui: {
tracking_category: 'Manage::Import::Experiment::NewCreateProjectUi'
},
diff --git a/locale/en/gitlab.po b/locale/en/gitlab.po
index 151fbffeb02..4c7435ebfcb 100644
--- a/locale/en/gitlab.po
+++ b/locale/en/gitlab.po
@@ -305,12 +305,6 @@ msgstr ""
msgid "Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out %{notification_link}."
msgstr ""
-msgid "Cycle Analytics"
-msgstr ""
-
-msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
-msgstr ""
-
msgid "CycleAnalyticsStage|Code"
msgstr ""
@@ -483,9 +477,6 @@ msgstr ""
msgid "Interval Pattern"
msgstr ""
-msgid "Introducing Cycle Analytics"
-msgstr ""
-
msgid "Jobs for last month"
msgstr ""
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index cc321ec6579..9234989c62d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14425,6 +14425,9 @@ msgstr ""
msgid "Invite teammates (optional)"
msgstr ""
+msgid "Invite your team"
+msgstr ""
+
msgid "InviteEmail|%{inviter} invited you to join the %{project_or_group_name} %{project_or_group} as a %{role}"
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb
index e81ebd5fa9d..7f3c3049499 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb
@@ -22,7 +22,7 @@ module QA
it 'allows 2FA code recovery via ssh' do
recovery_code = Support::SSH.perform do |ssh|
ssh.key = ssh_key
- ssh.uri = address.gsub(uri.port.to_s, ssh_port)
+ ssh.uri = address.gsub(/(?<=:)(#{uri.port})/, ssh_port)
ssh.setup
output = ssh.reset_2fa_codes
output.scan(/([A-Za-z0-9]{16})\n/).flatten.first
diff --git a/spec/factories/ci/daily_build_group_report_results.rb b/spec/factories/ci/daily_build_group_report_results.rb
index 8653316b51a..fbb235f80ed 100644
--- a/spec/factories/ci/daily_build_group_report_results.rb
+++ b/spec/factories/ci/daily_build_group_report_results.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :ci_daily_build_group_report_result, class: 'Ci::DailyBuildGroupReportResult' do
ref_path { Gitlab::Git::BRANCH_REF_PREFIX + 'master' }
- date { Time.zone.now.to_date }
+ date { Date.current }
project
last_pipeline factory: :ci_pipeline
group_name { 'rspec' }
diff --git a/spec/features/projects/files/user_browses_files_spec.rb b/spec/features/projects/files/user_browses_files_spec.rb
index a6126fbcb33..4e9e129042c 100644
--- a/spec/features/projects/files/user_browses_files_spec.rb
+++ b/spec/features/projects/files/user_browses_files_spec.rb
@@ -87,26 +87,22 @@ RSpec.describe "User browses files" do
end
it "shows correct files and links" do
- # rubocop:disable Lint/Void
- # Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
- find("a", text: /^empty$/)["href"] == project_tree_url(project, "markdown")
- find("a", text: /^#id$/)["href"] == project_tree_url(project, "markdown", anchor: "#id")
- find("a", text: %r{^/#id$})["href"] == project_tree_url(project, "markdown", anchor: "#id")
- find("a", text: /^README.md#id$/)["href"] == project_blob_url(project, "markdown/README.md", anchor: "#id")
- find("a", text: %r{^d/README.md#id$})["href"] == project_blob_url(project, "d/markdown/README.md", anchor: "#id")
- # rubocop:enable Lint/Void
-
expect(current_path).to eq(project_tree_path(project, "markdown"))
expect(page).to have_content("README.md")
- .and have_content("CHANGELOG")
- .and have_content("Welcome to GitLab GitLab is a free project and repository management application")
- .and have_link("GitLab API doc")
- .and have_link("GitLab API website")
- .and have_link("Rake tasks")
- .and have_link("backup and restore procedure")
- .and have_link("GitLab API doc directory")
- .and have_link("Maintenance")
- .and have_header_with_correct_id_and_link(2, "Application details", "application-details")
+ .and have_content("CHANGELOG")
+ .and have_content("Welcome to GitLab GitLab is a free project and repository management application")
+ .and have_link("GitLab API doc")
+ .and have_link("GitLab API website")
+ .and have_link("Rake tasks")
+ .and have_link("backup and restore procedure")
+ .and have_link("GitLab API doc directory")
+ .and have_link("Maintenance")
+ .and have_header_with_correct_id_and_link(2, "Application details", "application-details")
+ .and have_link("empty", href: "")
+ .and have_link("#id", href: "#id")
+ .and have_link("/#id", href: project_blob_path(project, "markdown/README.md", anchor: "id"))
+ .and have_link("README.md#id", href: project_blob_path(project, "markdown/README.md", anchor: "id"))
+ .and have_link("d/README.md#id", href: project_blob_path(project, "markdown/db/README.md", anchor: "id"))
end
it "shows correct content of file" do
@@ -114,10 +110,10 @@ RSpec.describe "User browses files" do
expect(current_path).to eq(project_blob_path(project, "markdown/doc/api/README.md"))
expect(page).to have_content("All API requests require authentication")
- .and have_content("Contents")
- .and have_link("Users")
- .and have_link("Rake tasks")
- .and have_header_with_correct_id_and_link(1, "GitLab API", "gitlab-api")
+ .and have_content("Contents")
+ .and have_link("Users")
+ .and have_link("Rake tasks")
+ .and have_header_with_correct_id_and_link(1, "GitLab API", "gitlab-api")
click_link("Users")
@@ -148,16 +144,13 @@ RSpec.describe "User browses files" do
click_link("d")
end
- # rubocop:disable Lint/Void
- # Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
- find("a", text: "..")["href"] == project_tree_url(project, "markdown/d")
- # rubocop:enable Lint/Void
+ expect(page).to have_link("..", href: project_tree_path(project, "markdown/"))
page.within(".tree-table") do
click_link("README.md")
end
- # Test the full URLs of links instead of relative paths by `have_link(text: "...", href: "...")`.
- find("a", text: /^empty$/)["href"] == project_blob_url(project, "markdown/d/README.md")
+
+ expect(page).to have_link("empty", href: "")
end
it "shows correct content of directory" do
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index 7c11d073566..cef5f8cc528 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -53,7 +53,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
class="div-dropzone-hover"
>
<svg
- class="div-dropzone-icon"
+ class="div-dropzone-icon s24"
>
<use
xlink:href="undefined#paperclip"
diff --git a/spec/graphql/mutations/terraform/state/delete_spec.rb b/spec/graphql/mutations/terraform/state/delete_spec.rb
new file mode 100644
index 00000000000..313a85a4bac
--- /dev/null
+++ b/spec/graphql/mutations/terraform/state/delete_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Terraform::State::Delete do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:state) { create(:terraform_state) }
+
+ let(:mutation) do
+ described_class.new(
+ object: double,
+ context: { current_user: user },
+ field: double
+ )
+ end
+
+ it { expect(described_class.graphql_name).to eq('TerraformStateDelete') }
+ it { expect(described_class).to require_graphql_authorizations(:admin_terraform_state) }
+
+ describe '#resolve' do
+ let(:global_id) { state.to_global_id }
+
+ subject { mutation.resolve(id: global_id) }
+
+ context 'user does not have permission' do
+ it 'raises an error', :aggregate_failures do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ expect { state.reload }.not_to raise_error
+ end
+ end
+
+ context 'user has permission' do
+ before do
+ state.project.add_maintainer(user)
+ end
+
+ it 'deletes the state', :aggregate_failures do
+ expect do
+ expect(subject).to eq(errors: [])
+ end.to change { ::Terraform::State.count }.by(-1)
+
+ expect { state.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+
+ context 'with invalid params' do
+ let(:global_id) { user.to_global_id }
+
+ it 'raises an error', :aggregate_failures do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ expect { state.reload }.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/terraform/state/lock_spec.rb b/spec/graphql/mutations/terraform/state/lock_spec.rb
new file mode 100644
index 00000000000..c83563040fd
--- /dev/null
+++ b/spec/graphql/mutations/terraform/state/lock_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Terraform::State::Lock do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:state) { create(:terraform_state) }
+
+ let(:mutation) do
+ described_class.new(
+ object: double,
+ context: { current_user: user },
+ field: double
+ )
+ end
+
+ it { expect(described_class.graphql_name).to eq('TerraformStateLock') }
+ it { expect(described_class).to require_graphql_authorizations(:admin_terraform_state) }
+
+ describe '#resolve' do
+ let(:global_id) { state.to_global_id }
+
+ subject { mutation.resolve(id: global_id) }
+
+ context 'user does not have permission' do
+ it 'raises an error', :aggregate_failures do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ expect(state.reload).not_to be_locked
+ end
+ end
+
+ context 'user has permission' do
+ before do
+ state.project.add_maintainer(user)
+ end
+
+ it 'locks the state', :aggregate_failures do
+ expect(subject).to eq(errors: [])
+
+ expect(state.reload).to be_locked
+ expect(state.locked_by_user).to eq(user)
+ expect(state.lock_xid).to be_present
+ expect(state.locked_at).to be_present
+ end
+
+ context 'state is already locked' do
+ let(:locked_by_user) { create(:user) }
+ let(:state) { create(:terraform_state, :locked, locked_by_user: locked_by_user) }
+
+ it 'does not modify the existing lock', :aggregate_failures do
+ expect(subject).to eq(errors: ['state is already locked'])
+
+ expect(state.reload).to be_locked
+ expect(state.locked_by_user).to eq(locked_by_user)
+ end
+ end
+ end
+
+ context 'with invalid params' do
+ let(:global_id) { user.to_global_id }
+
+ it 'raises an error', :aggregate_failures do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ expect(state.reload).not_to be_locked
+ end
+ end
+ end
+end
diff --git a/spec/graphql/mutations/terraform/state/unlock_spec.rb b/spec/graphql/mutations/terraform/state/unlock_spec.rb
new file mode 100644
index 00000000000..4918a1c4abf
--- /dev/null
+++ b/spec/graphql/mutations/terraform/state/unlock_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Mutations::Terraform::State::Unlock do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:state) { create(:terraform_state, :locked) }
+
+ let(:mutation) do
+ described_class.new(
+ object: double,
+ context: { current_user: user },
+ field: double
+ )
+ end
+
+ it { expect(described_class.graphql_name).to eq('TerraformStateUnlock') }
+ it { expect(described_class).to require_graphql_authorizations(:admin_terraform_state) }
+
+ describe '#resolve' do
+ let(:global_id) { state.to_global_id }
+
+ subject { mutation.resolve(id: global_id) }
+
+ context 'user does not have permission' do
+ it 'raises an error', :aggregate_failures do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ expect(state.reload).to be_locked
+ end
+ end
+
+ context 'user has permission' do
+ before do
+ state.project.add_maintainer(user)
+ end
+
+ it 'unlocks the state', :aggregate_failures do
+ expect(subject).to eq(errors: [])
+ expect(state.reload).not_to be_locked
+ end
+
+ context 'state is already unlocked' do
+ let(:state) { create(:terraform_state) }
+
+ it 'does not modify the state' do
+ expect(subject).to eq(errors: [])
+ expect(state.reload).not_to be_locked
+ end
+ end
+ end
+
+ context 'with invalid params' do
+ let(:global_id) { user.to_global_id }
+
+ it 'raises an error', :aggregate_failures do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
+ expect(state.reload).to be_locked
+ end
+ end
+ end
+end
diff --git a/spec/helpers/invite_members_helper_spec.rb b/spec/helpers/invite_members_helper_spec.rb
index b4e05d67553..d75b3c9f2e3 100644
--- a/spec/helpers/invite_members_helper_spec.rb
+++ b/spec/helpers/invite_members_helper_spec.rb
@@ -7,70 +7,110 @@ RSpec.describe InviteMembersHelper do
let_it_be(:developer) { create(:user, developer_projects: [project]) }
let(:owner) { project.owner }
- before do
- assign(:project, project)
- end
+ context 'with project' do
+ before do
+ assign(:project, project)
+ end
- describe "#directly_invite_members?" do
- context 'when the user is an owner' do
- before do
- allow(helper).to receive(:current_user) { owner }
- end
+ describe "#directly_invite_members?" do
+ context 'when the user is an owner' do
+ before do
+ allow(helper).to receive(:current_user) { owner }
+ end
+
+ it 'returns false' do
+ allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { false }
- it 'returns false' do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { false }
+ expect(helper.directly_invite_members?).to eq false
+ end
- expect(helper.directly_invite_members?).to eq false
+ it 'returns true' do
+ allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { true }
+
+ expect(helper.directly_invite_members?).to eq true
+ end
end
- it 'returns true' do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { true }
+ context 'when the user is a developer' do
+ before do
+ allow(helper).to receive(:current_user) { developer }
+ end
+
+ it 'returns false' do
+ allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { true }
- expect(helper.directly_invite_members?).to eq true
+ expect(helper.directly_invite_members?).to eq false
+ end
end
end
- context 'when the user is a developer' do
- before do
- allow(helper).to receive(:current_user) { developer }
+ describe "#indirectly_invite_members?" do
+ context 'when a user is a developer' do
+ before do
+ allow(helper).to receive(:current_user) { developer }
+ end
+
+ it 'returns false' do
+ allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { false }
+
+ expect(helper.indirectly_invite_members?).to eq false
+ end
+
+ it 'returns true' do
+ allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true }
+
+ expect(helper.indirectly_invite_members?).to eq true
+ end
end
- it 'returns false' do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_a) { true }
+ context 'when a user is an owner' do
+ before do
+ allow(helper).to receive(:current_user) { owner }
+ end
- expect(helper.directly_invite_members?).to eq false
+ it 'returns false' do
+ allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true }
+
+ expect(helper.indirectly_invite_members?).to eq false
+ end
end
end
end
- describe "#indirectly_invite_members?" do
- context 'when a user is a developer' do
- before do
- allow(helper).to receive(:current_user) { developer }
- end
+ context 'with group' do
+ let_it_be(:group) { create(:group) }
- it 'returns false' do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { false }
+ describe "#invite_group_members?" do
+ context 'when the user is an owner' do
+ before do
+ group.add_owner(owner)
+ allow(helper).to receive(:current_user) { owner }
+ end
- expect(helper.indirectly_invite_members?).to eq false
- end
+ it 'returns false' do
+ allow(helper).to receive(:experiment_enabled?).with(:invite_members_empty_group_version_a) { false }
- it 'returns true' do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true }
+ expect(helper.invite_group_members?(group)).to eq false
+ end
- expect(helper.indirectly_invite_members?).to eq true
- end
- end
+ it 'returns true' do
+ allow(helper).to receive(:experiment_enabled?).with(:invite_members_empty_group_version_a) { true }
- context 'when a user is an owner' do
- before do
- allow(helper).to receive(:current_user) { owner }
+ expect(helper.invite_group_members?(group)).to eq true
+ end
end
- it 'returns false' do
- allow(helper).to receive(:experiment_enabled?).with(:invite_members_version_b) { true }
+ context 'when the user is a developer' do
+ before do
+ group.add_developer(developer)
+ allow(helper).to receive(:current_user) { developer }
+ end
+
+ it 'returns false' do
+ allow(helper).to receive(:experiment_enabled?).with(:invite_members_empty_group_version_a) { true }
- expect(helper.indirectly_invite_members?).to eq false
+ expect(helper.invite_group_members?(group)).to eq false
+ end
end
end
end
diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb
index e8a5c4613fe..1869e46ae6a 100644
--- a/spec/helpers/page_layout_helper_spec.rb
+++ b/spec/helpers/page_layout_helper_spec.rb
@@ -56,19 +56,24 @@ RSpec.describe PageLayoutHelper do
end
%w(project user group).each do |type|
+ let(:object) { build(type, trait) }
+ let(:trait) { :with_avatar }
+
context "with @#{type} assigned" do
- it "uses #{type.titlecase} avatar if available" do
- object = double(avatar_url: 'http://example.com/uploads/-/system/avatar.png')
+ before do
assign(type, object)
+ end
- expect(helper.page_image).to eq object.avatar_url
+ it "uses #{type.titlecase} avatar full url" do
+ expect(helper.page_image).to eq object.avatar_url(only_path: false)
end
- it 'falls back to the default when avatar_url is nil' do
- object = double(avatar_url: nil)
- assign(type, object)
+ context 'when avatar_url is nil' do
+ let(:trait) { nil }
- expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png'
+ it 'falls back to the default when avatar_url is nil' do
+ expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png'
+ end
end
end
@@ -77,6 +82,16 @@ RSpec.describe PageLayoutHelper do
expect(helper.page_image).to match_asset_path 'assets/gitlab_logo.png'
end
end
+
+ context 'if avatar_with_host is disabled' do
+ it "#{type.titlecase} does not generate avatar full url" do
+ stub_feature_flags(avatar_with_host: false)
+
+ assign(type, object)
+
+ expect(helper.page_image).to eq object.avatar_url(only_path: true)
+ end
+ end
end
end
diff --git a/spec/models/ci/daily_build_group_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb
index 326366666cb..761fd1882dc 100644
--- a/spec/models/ci/daily_build_group_report_result_spec.rb
+++ b/spec/models/ci/daily_build_group_report_result_spec.rb
@@ -81,4 +81,28 @@ RSpec.describe Ci::DailyBuildGroupReportResult do
end
end
end
+
+ describe 'scopes' do
+ let_it_be(:project) { create(:project) }
+ let(:recent_build_group_report_result) { create(:ci_daily_build_group_report_result, project: project) }
+ let(:old_build_group_report_result) do
+ create(:ci_daily_build_group_report_result, date: 1.week.ago, project: project)
+ end
+
+ describe '.by_projects' do
+ subject { described_class.by_projects([project.id]) }
+
+ it 'returns records by projects' do
+ expect(subject).to contain_exactly(recent_build_group_report_result, old_build_group_report_result)
+ end
+ end
+
+ describe '.with_coverage' do
+ subject { described_class.with_coverage }
+
+ it 'returns data with coverage' do
+ expect(subject).to contain_exactly(recent_build_group_report_result, old_build_group_report_result)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/graphql/terraform/state/delete_spec.rb b/spec/requests/api/graphql/terraform/state/delete_spec.rb
new file mode 100644
index 00000000000..35927d03b49
--- /dev/null
+++ b/spec/requests/api/graphql/terraform/state/delete_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'delete a terraform state' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ let(:state) { create(:terraform_state, project: project) }
+ let(:mutation) { graphql_mutation(:terraform_state_delete, id: state.to_global_id.to_s) }
+
+ before do
+ post_graphql_mutation(mutation, current_user: user)
+ end
+
+ include_examples 'a working graphql query'
+
+ it 'deletes the state' do
+ expect { state.reload }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+end
diff --git a/spec/requests/api/graphql/terraform/state/lock_spec.rb b/spec/requests/api/graphql/terraform/state/lock_spec.rb
new file mode 100644
index 00000000000..e4d3b6336ab
--- /dev/null
+++ b/spec/requests/api/graphql/terraform/state/lock_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'lock a terraform state' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ let(:state) { create(:terraform_state, project: project) }
+ let(:mutation) { graphql_mutation(:terraform_state_lock, id: state.to_global_id.to_s) }
+
+ before do
+ expect(state).not_to be_locked
+ post_graphql_mutation(mutation, current_user: user)
+ end
+
+ include_examples 'a working graphql query'
+
+ it 'locks the state' do
+ expect(state.reload).to be_locked
+ expect(state.locked_by_user).to eq(user)
+ end
+end
diff --git a/spec/requests/api/graphql/terraform/state/unlock_spec.rb b/spec/requests/api/graphql/terraform/state/unlock_spec.rb
new file mode 100644
index 00000000000..e90730f2d8f
--- /dev/null
+++ b/spec/requests/api/graphql/terraform/state/unlock_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'unlock a terraform state' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user, maintainer_projects: [project]) }
+
+ let(:state) { create(:terraform_state, :locked, project: project) }
+ let(:mutation) { graphql_mutation(:terraform_state_unlock, id: state.to_global_id.to_s) }
+
+ before do
+ expect(state).to be_locked
+ post_graphql_mutation(mutation, current_user: user)
+ end
+
+ include_examples 'a working graphql query'
+
+ it 'unlocks the state' do
+ expect(state.reload).not_to be_locked
+ end
+end
diff --git a/spec/services/issuable/common_system_notes_service_spec.rb b/spec/services/issuable/common_system_notes_service_spec.rb
index fc01ee8f672..a988ab81754 100644
--- a/spec/services/issuable/common_system_notes_service_spec.rb
+++ b/spec/services/issuable/common_system_notes_service_spec.rb
@@ -36,28 +36,28 @@ RSpec.describe Issuable::CommonSystemNotesService do
context 'adding Draft note' do
let(:issuable) { create(:merge_request, title: "merge request") }
- it_behaves_like 'system note creation', { title: "Draft: merge request" }, 'marked as a **Work In Progress**'
+ it_behaves_like 'system note creation', { title: "Draft: merge request" }, 'marked this merge request as **draft**'
context 'and changing title' do
before do
issuable.update_attribute(:title, "Draft: changed title")
end
- it_behaves_like 'draft notes creation', 'marked'
+ it_behaves_like 'draft notes creation', 'draft'
end
end
context 'removing Draft note' do
let(:issuable) { create(:merge_request, title: "Draft: merge request") }
- it_behaves_like 'system note creation', { title: "merge request" }, 'unmarked as a **Work In Progress**'
+ it_behaves_like 'system note creation', { title: "merge request" }, 'marked this merge request as **ready**'
context 'and changing title' do
before do
issuable.update_attribute(:title, "changed title")
end
- it_behaves_like 'draft notes creation', 'unmarked'
+ it_behaves_like 'draft notes creation', 'ready'
end
end
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index d603cbb16aa..3ccf02fcdfb 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -683,14 +683,14 @@ RSpec.describe MergeRequests::RefreshService do
end
end
- context 'marking the merge request as work in progress' do
+ context 'marking the merge request as draft' do
let(:refresh_service) { service.new(@project, @user) }
before do
allow(refresh_service).to receive(:execute_hooks)
end
- it 'marks the merge request as work in progress from fixup commits' do
+ it 'marks the merge request as draft from fixup commits' do
fixup_merge_request = create(:merge_request,
source_project: @project,
source_branch: 'wip',
@@ -705,11 +705,11 @@ RSpec.describe MergeRequests::RefreshService do
expect(fixup_merge_request.work_in_progress?).to eq(true)
expect(fixup_merge_request.notes.last.note).to match(
- /marked as a \*\*Work In Progress\*\* from #{Commit.reference_pattern}/
+ /marked this merge request as \*\*draft\*\* from #{Commit.reference_pattern}/
)
end
- it 'references the commit that caused the Work in Progress status' do
+ it 'references the commit that caused the draft status' do
wip_merge_request = create(:merge_request,
source_project: @project,
source_branch: 'wip',
@@ -724,11 +724,11 @@ RSpec.describe MergeRequests::RefreshService do
refresh_service.execute(oldrev, newrev, 'refs/heads/wip')
expect(wip_merge_request.reload.notes.last.note).to eq(
- "marked as a **Work In Progress** from #{wip_commit.id}"
+ "marked this merge request as **draft** from #{wip_commit.id}"
)
end
- it 'does not mark as WIP based on commits that do not belong to an MR' do
+ it 'does not mark as draft based on commits that do not belong to an MR' do
allow(refresh_service).to receive(:find_new_commits)
refresh_service.instance_variable_set("@commits", [
double(
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index abb0b79de84..a4ae7e42958 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -566,25 +566,25 @@ RSpec.describe SystemNoteService do
end
end
- describe '.handle_merge_request_wip' do
+ describe '.handle_merge_request_draft' do
it 'calls MergeRequestsService' do
expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
- expect(service).to receive(:handle_merge_request_wip)
+ expect(service).to receive(:handle_merge_request_draft)
end
- described_class.handle_merge_request_wip(noteable, project, author)
+ described_class.handle_merge_request_draft(noteable, project, author)
end
end
- describe '.add_merge_request_wip_from_commit' do
+ describe '.add_merge_request_draft_from_commit' do
it 'calls MergeRequestsService' do
commit = double
expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service|
- expect(service).to receive(:add_merge_request_wip_from_commit).with(commit)
+ expect(service).to receive(:add_merge_request_draft_from_commit).with(commit)
end
- described_class.add_merge_request_wip_from_commit(noteable, project, author, commit)
+ described_class.add_merge_request_draft_from_commit(noteable, project, author, commit)
end
end
diff --git a/spec/services/system_notes/merge_requests_service_spec.rb b/spec/services/system_notes/merge_requests_service_spec.rb
index 067e1cef64d..50d16231e8f 100644
--- a/spec/services/system_notes/merge_requests_service_spec.rb
+++ b/spec/services/system_notes/merge_requests_service_spec.rb
@@ -51,44 +51,44 @@ RSpec.describe ::SystemNotes::MergeRequestsService do
end
end
- describe '.handle_merge_request_wip' do
+ describe '.handle_merge_request_draft' do
context 'adding draft note' do
let(:noteable) { create(:merge_request, source_project: project, title: 'Draft: Lorem ipsum') }
- subject { service.handle_merge_request_wip }
+ subject { service.handle_merge_request_draft }
it_behaves_like 'a system note' do
let(:action) { 'title' }
end
it 'sets the note text' do
- expect(subject.note).to eq 'marked as a **Work In Progress**'
+ expect(subject.note).to eq 'marked this merge request as **draft**'
end
end
- context 'removing wip note' do
- subject { service.handle_merge_request_wip }
+ context 'removing draft note' do
+ subject { service.handle_merge_request_draft }
it_behaves_like 'a system note' do
let(:action) { 'title' }
end
it 'sets the note text' do
- expect(subject.note).to eq 'unmarked as a **Work In Progress**'
+ expect(subject.note).to eq 'marked this merge request as **ready**'
end
end
end
- describe '.add_merge_request_wip_from_commit' do
- subject { service.add_merge_request_wip_from_commit(noteable.diff_head_commit) }
+ describe '.add_merge_request_draft_from_commit' do
+ subject { service.add_merge_request_draft_from_commit(noteable.diff_head_commit) }
it_behaves_like 'a system note' do
let(:action) { 'title' }
end
- it "posts the 'marked as a Work In Progress from commit' system note" do
+ it "posts the 'marked this merge request as draft from commit' system note" do
expect(subject.note).to match(
- /marked as a \*\*Work In Progress\*\* from #{Commit.reference_pattern}/
+ /marked this merge request as \*\*draft\*\* from #{Commit.reference_pattern}/
)
end
end
diff --git a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
index 5b95a5753a1..7b277d4bede 100644
--- a/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
+++ b/spec/support/shared_examples/services/common_system_notes_shared_examples.rb
@@ -17,13 +17,13 @@ RSpec.shared_examples 'system note creation' do |update_params, note_text|
end
end
-RSpec.shared_examples 'draft notes creation' do |wip_action|
+RSpec.shared_examples 'draft notes creation' do |action|
subject { described_class.new(project, user).execute(issuable, old_labels: []) }
it 'creates Draft toggle and title change notes' do
expect { subject }.to change { Note.count }.from(0).to(2)
- expect(Note.first.note).to match("#{wip_action} as a **Work In Progress**")
+ expect(Note.first.note).to match("marked this merge request as **#{action}**")
expect(Note.second.note).to match('changed title')
end
end