diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-29 15:09:40 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-01-29 15:09:40 +0000 |
commit | 10052df7536415c192788799b294c9a5ecf07ce7 (patch) | |
tree | 0404e80139c6784aabf4985ee8f4e33d82278c61 | |
parent | a4df3f0dbbe9a18ee6cadaef0d3313669eb1c7d6 (diff) | |
download | gitlab-ce-10052df7536415c192788799b294c9a5ecf07ce7.tar.gz |
Add latest changes from gitlab-org/gitlab@master
133 files changed, 3017 insertions, 1059 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 35c5e6f75ec..770e48dcdab 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -2b34fc78dfb8e7f55f7f2fc30602381b43c54fc3 +e342c59d0c6575245a335bbe9dfe95d9a06b3a2f diff --git a/app/assets/images/mailers/in_product_marketing/create-0.png b/app/assets/images/mailers/in_product_marketing/create-0.png Binary files differnew file mode 100644 index 00000000000..7fc992f14f2 --- /dev/null +++ b/app/assets/images/mailers/in_product_marketing/create-0.png diff --git a/app/assets/images/mailers/in_product_marketing/create-1.png b/app/assets/images/mailers/in_product_marketing/create-1.png Binary files differnew file mode 100644 index 00000000000..0315ffefb31 --- /dev/null +++ b/app/assets/images/mailers/in_product_marketing/create-1.png diff --git a/app/assets/images/mailers/in_product_marketing/create-2.png b/app/assets/images/mailers/in_product_marketing/create-2.png Binary files differnew file mode 100644 index 00000000000..619f9fcd659 --- /dev/null +++ b/app/assets/images/mailers/in_product_marketing/create-2.png diff --git a/app/assets/images/mailers/in_product_marketing/gitlab-logo-gray-rgb.png b/app/assets/images/mailers/in_product_marketing/gitlab-logo-gray-rgb.png Binary files differnew file mode 100644 index 00000000000..31083af512e --- /dev/null +++ b/app/assets/images/mailers/in_product_marketing/gitlab-logo-gray-rgb.png diff --git a/app/assets/images/mailers/in_product_marketing/team-0.png b/app/assets/images/mailers/in_product_marketing/team-0.png Binary files differnew file mode 100644 index 00000000000..f10ae998efa --- /dev/null +++ b/app/assets/images/mailers/in_product_marketing/team-0.png diff --git a/app/assets/images/mailers/in_product_marketing/team-1.png b/app/assets/images/mailers/in_product_marketing/team-1.png Binary files differnew file mode 100644 index 00000000000..cd68464e6e8 --- /dev/null +++ b/app/assets/images/mailers/in_product_marketing/team-1.png diff --git a/app/assets/images/mailers/in_product_marketing/team-2.png b/app/assets/images/mailers/in_product_marketing/team-2.png Binary files differnew file mode 100644 index 00000000000..b199c659943 --- /dev/null +++ b/app/assets/images/mailers/in_product_marketing/team-2.png diff --git a/app/assets/images/mailers/in_product_marketing/trial-0.png b/app/assets/images/mailers/in_product_marketing/trial-0.png Binary files differnew file mode 100644 index 00000000000..3b0d7a8ecd8 --- /dev/null +++ b/app/assets/images/mailers/in_product_marketing/trial-0.png diff --git a/app/assets/images/mailers/in_product_marketing/trial-1.png b/app/assets/images/mailers/in_product_marketing/trial-1.png Binary files differnew file mode 100644 index 00000000000..3a30b2acaee --- /dev/null +++ b/app/assets/images/mailers/in_product_marketing/trial-1.png diff --git a/app/assets/images/mailers/in_product_marketing/trial-2.png b/app/assets/images/mailers/in_product_marketing/trial-2.png Binary files differnew file mode 100644 index 00000000000..95bd965b49f --- /dev/null +++ b/app/assets/images/mailers/in_product_marketing/trial-2.png diff --git a/app/assets/images/mailers/in_product_marketing/verify-0.png b/app/assets/images/mailers/in_product_marketing/verify-0.png Binary files differnew file mode 100644 index 00000000000..04b6f172b37 --- /dev/null +++ b/app/assets/images/mailers/in_product_marketing/verify-0.png diff --git a/app/assets/images/mailers/in_product_marketing/verify-1.png b/app/assets/images/mailers/in_product_marketing/verify-1.png Binary files differnew file mode 100644 index 00000000000..8997e8ba575 --- /dev/null +++ b/app/assets/images/mailers/in_product_marketing/verify-1.png diff --git a/app/assets/images/mailers/in_product_marketing/verify-2.png b/app/assets/images/mailers/in_product_marketing/verify-2.png Binary files differnew file mode 100644 index 00000000000..93c99dee246 --- /dev/null +++ b/app/assets/images/mailers/in_product_marketing/verify-2.png diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js index ef70a094f7c..24ee799fd25 100644 --- a/app/assets/javascripts/boards/index.js +++ b/app/assets/javascripts/boards/index.js @@ -73,6 +73,7 @@ export default () => { boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours); } + // eslint-disable-next-line @gitlab/no-runtime-template-compiler issueBoardsApp = new Vue({ el: $boardApp, components: { @@ -275,7 +276,7 @@ export default () => { }, }); - // eslint-disable-next-line no-new + // eslint-disable-next-line no-new, @gitlab/no-runtime-template-compiler new Vue({ el: document.getElementById('js-add-list'), data: { diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue index 80e2e73f420..1d966f37374 100644 --- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue +++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue @@ -1,78 +1,184 @@ <script> -import { GlLoadingIcon } from '@gitlab/ui'; +import { + GlEmptyState, + GlIcon, + GlLink, + GlLoadingIcon, + GlSearchBoxByClick, + GlSprintf, +} from '@gitlab/ui'; +import { s__ } from '~/locale'; import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql'; import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql'; import setTargetNamespaceMutation from '../graphql/mutations/set_target_namespace.mutation.graphql'; import setNewNameMutation from '../graphql/mutations/set_new_name.mutation.graphql'; import importGroupMutation from '../graphql/mutations/import_group.mutation.graphql'; import ImportTableRow from './import_table_row.vue'; - -const mapApolloMutations = (mutations) => - Object.fromEntries( - Object.entries(mutations).map(([key, mutation]) => [ - key, - function mutate(config) { - return this.$apollo.mutate({ - mutation, - ...config, - }); - }, - ]), - ); +import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; export default { components: { + GlEmptyState, + GlIcon, + GlLink, GlLoadingIcon, + GlSearchBoxByClick, + GlSprintf, ImportTableRow, + PaginationLinks, + }, + + props: { + sourceUrl: { + type: String, + required: true, + }, + }, + + data() { + return { + filter: '', + page: 1, + }; }, apollo: { - bulkImportSourceGroups: bulkImportSourceGroupsQuery, + bulkImportSourceGroups: { + query: bulkImportSourceGroupsQuery, + variables() { + return { page: this.page, filter: this.filter }; + }, + }, availableNamespaces: availableNamespacesQuery, }, + computed: { + hasGroups() { + return this.bulkImportSourceGroups?.nodes?.length > 0; + }, + + hasEmptyFilter() { + return this.filter.length > 0 && !this.hasGroups; + }, + + statusMessage() { + return this.filter.length === 0 + ? s__('BulkImport|Showing %{start}-%{end} of %{total} from %{link}') + : s__( + 'BulkImport|Showing %{start}-%{end} of %{total} matching filter "%{filter}" from %{link}', + ); + }, + + paginationInfo() { + const { page, perPage, total } = this.bulkImportSourceGroups?.pageInfo ?? { + page: 1, + perPage: 0, + total: 0, + }; + const start = (page - 1) * perPage + 1; + const end = start + (this.bulkImportSourceGroups.nodes?.length ?? 0) - 1; + + return { start, end, total }; + }, + }, + + watch: { + filter() { + this.page = 1; + }, + }, + methods: { - ...mapApolloMutations({ - setTargetNamespace: setTargetNamespaceMutation, - setNewName: setNewNameMutation, - importGroup: importGroupMutation, - }), + setPage(page) { + this.page = page; + }, + + updateTargetNamespace(sourceGroupId, targetNamespace) { + this.$apollo.mutate({ + mutation: setTargetNamespaceMutation, + variables: { sourceGroupId, targetNamespace }, + }); + }, + + updateNewName(sourceGroupId, newName) { + this.$apollo.mutate({ + mutation: setNewNameMutation, + variables: { sourceGroupId, newName }, + }); + }, + + importGroup(sourceGroupId) { + this.$apollo.mutate({ + mutation: importGroupMutation, + variables: { sourceGroupId }, + }); + }, }, }; </script> <template> <div> - <gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" /> - <div v-else-if="bulkImportSourceGroups.length"> - <table class="gl-w-full"> - <thead class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1"> - <th class="gl-py-4 import-jobs-from-col">{{ s__('BulkImport|From source group') }}</th> - <th class="gl-py-4 import-jobs-to-col">{{ s__('BulkImport|To new group') }}</th> - <th class="gl-py-4 import-jobs-status-col">{{ __('Status') }}</th> - <th class="gl-py-4 import-jobs-cta-col"></th> - </thead> - <tbody> - <template v-for="group in bulkImportSourceGroups"> - <import-table-row - :key="group.id" - :group="group" - :available-namespaces="availableNamespaces" - @update-target-namespace=" - setTargetNamespace({ - variables: { sourceGroupId: group.id, targetNamespace: $event }, - }) - " - @update-new-name=" - setNewName({ - variables: { sourceGroupId: group.id, newName: $event }, - }) - " - @import-group="importGroup({ variables: { sourceGroupId: group.id } })" - /> + <div + class="gl-py-5 gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex gl-align-items-center" + > + <span> + <gl-sprintf v-if="!$apollo.loading && hasGroups" :message="statusMessage"> + <template #start> + <strong>{{ paginationInfo.start }}</strong> + </template> + <template #end> + <strong>{{ paginationInfo.end }}</strong> + </template> + <template #total> + <strong>{{ n__('%d group', '%d groups', paginationInfo.total) }}</strong> </template> - </tbody> - </table> + <template #filter> + <strong>{{ filter }}</strong> + </template> + <template #link> + <gl-link class="gl-display-inline-block" :href="sourceUrl" target="_blank"> + {{ sourceUrl }} <gl-icon name="external-link" class="vertical-align-middle" /> + </gl-link> + </template> + </gl-sprintf> + </span> + <gl-search-box-by-click class="gl-ml-auto" @submit="filter = $event" @clear="filter = ''" /> </div> + <gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" /> + <template v-else> + <gl-empty-state v-if="hasEmptyFilter" :title="__('Sorry, your filter produced no results')" /> + <gl-empty-state + v-else-if="!hasGroups" + :title="s__('BulkImport|No groups available for import')" + /> + <div v-else class="gl-display-flex gl-flex-direction-column gl-align-items-center"> + <table class="gl-w-full"> + <thead class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1"> + <th class="gl-py-4 import-jobs-from-col">{{ s__('BulkImport|From source group') }}</th> + <th class="gl-py-4 import-jobs-to-col">{{ s__('BulkImport|To new group') }}</th> + <th class="gl-py-4 import-jobs-status-col">{{ __('Status') }}</th> + <th class="gl-py-4 import-jobs-cta-col"></th> + </thead> + <tbody> + <template v-for="group in bulkImportSourceGroups.nodes"> + <import-table-row + :key="group.id" + :group="group" + :available-namespaces="availableNamespaces" + @update-target-namespace="updateTargetNamespace(group.id, $event)" + @update-new-name="updateNewName(group.id, $event)" + @import-group="importGroup(group.id)" + /> + </template> + </tbody> + </table> + <pagination-links + :change="setPage" + :page-info="bulkImportSourceGroups.pageInfo" + class="gl-mt-3" + /> + </div> + </template> </div> </template> diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js index 8f2d488d661..beb058417e5 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js @@ -1,4 +1,5 @@ import axios from '~/lib/utils/axios_utils'; +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; import createDefaultClient from '~/lib/graphql'; import { s__ } from '~/locale'; import createFlash from '~/flash'; @@ -8,8 +9,10 @@ import { SourceGroupsManager } from './services/source_groups_manager'; import { StatusPoller } from './services/status_poller'; export const clientTypenames = { + BulkImportSourceGroupConnection: 'ClientBulkImportSourceGroupConnection', BulkImportSourceGroup: 'ClientBulkImportSourceGroup', AvailableNamespace: 'ClientAvailableNamespace', + BulkImportPageInfo: 'ClientBulkImportPageInfo', }; export function createResolvers({ endpoints }) { @@ -17,22 +20,39 @@ export function createResolvers({ endpoints }) { return { Query: { - async bulkImportSourceGroups(_, __, { client }) { + async bulkImportSourceGroups(_, vars, { client }) { const { data: { availableNamespaces }, } = await client.query({ query: availableNamespacesQuery }); - return axios.get(endpoints.status).then(({ data }) => { - return data.importable_data.map((group) => ({ - __typename: clientTypenames.BulkImportSourceGroup, - ...group, - status: STATUSES.NONE, - import_target: { - new_name: group.full_path, - target_namespace: availableNamespaces[0].full_path, + return axios + .get(endpoints.status, { + params: { + page: vars.page, + per_page: vars.perPage, + filter: vars.filter, }, - })); - }); + }) + .then(({ headers, data }) => { + const pagination = parseIntPagination(normalizeHeaders(headers)); + + return { + __typename: clientTypenames.BulkImportSourceGroupConnection, + nodes: data.importable_data.map((group) => ({ + __typename: clientTypenames.BulkImportSourceGroup, + ...group, + status: STATUSES.NONE, + import_target: { + new_name: group.full_path, + target_namespace: availableNamespaces[0].full_path, + }, + })), + pageInfo: { + __typename: clientTypenames.BulkImportPageInfo, + ...pagination, + }, + }; + }); }, availableNamespaces: () => diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql index 8d52d94925c..28dfefdf8a7 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql +++ b/app/assets/javascripts/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql @@ -1,7 +1,15 @@ #import "../fragments/bulk_import_source_group_item.fragment.graphql" -query bulkImportSourceGroups { - bulkImportSourceGroups @client { - ...BulkImportSourceGroupItem +query bulkImportSourceGroups($page: Int = 1, $perPage: Int = 20, $filter: String = "") { + bulkImportSourceGroups(page: $page, filter: $filter, perPage: $perPage) @client { + nodes { + ...BulkImportSourceGroupItem + } + pageInfo { + page + perPage + total + totalPages + } } } diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js index 41dd25b9150..886cf24081b 100644 --- a/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js +++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/status_poller.js @@ -46,7 +46,10 @@ export class StatusPoller { const { bulkImportSourceGroups } = this.client.readQuery({ query: bulkImportSourceGroupsQuery, }); - const groupsInProgress = bulkImportSourceGroups.filter((g) => g.status === STATUSES.STARTED); + + const groupsInProgress = bulkImportSourceGroups.nodes.filter( + (g) => g.status === STATUSES.STARTED, + ); if (groupsInProgress.length) { const { data: results } = await this.client.query({ query: generateGroupsQuery(groupsInProgress), diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js index bf427075564..1ce74bf4f60 100644 --- a/app/assets/javascripts/import_entities/import_groups/index.js +++ b/app/assets/javascripts/import_entities/import_groups/index.js @@ -10,7 +10,12 @@ Vue.use(VueApollo); export function mountImportGroupsApp(mountElement) { if (!mountElement) return undefined; - const { statusPath, availableNamespacesPath, createBulkImportPath } = mountElement.dataset; + const { + statusPath, + availableNamespacesPath, + createBulkImportPath, + sourceUrl, + } = mountElement.dataset; const apolloProvider = new VueApollo({ defaultClient: createApolloClient({ endpoints: { @@ -25,7 +30,11 @@ export function mountImportGroupsApp(mountElement) { el: mountElement, apolloProvider, render(createElement) { - return createElement(ImportTable); + return createElement(ImportTable, { + props: { + sourceUrl, + }, + }); }, }); } diff --git a/app/assets/javascripts/performance/vue_performance_plugin.js b/app/assets/javascripts/performance/vue_performance_plugin.js index 18ff9f7f8c4..7329b83b1d1 100644 --- a/app/assets/javascripts/performance/vue_performance_plugin.js +++ b/app/assets/javascripts/performance/vue_performance_plugin.js @@ -1,6 +1,3 @@ -// This is a false violation of @gitlab/no-runtime-template-compiler, since it -// is simply defining a global Vue mixin. -/* eslint-disable @gitlab/no-runtime-template-compiler */ const ComponentPerformancePlugin = { install(Vue, options) { Vue.mixin({ diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue index 5429d51dae0..81d23a563e2 100644 --- a/app/assets/javascripts/projects/components/project_delete_button.vue +++ b/app/assets/javascripts/projects/components/project_delete_button.vue @@ -22,10 +22,10 @@ export default { strings: { alertTitle: __('You are about to permanently delete this project'), alertBody: __( - 'Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc.', + 'Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.', ), modalBody: __( - "This action cannot be undone. You will lose this project's repository and all content: issues, merge requests, etc.", + "This action cannot be undone. You will lose this project's repository and all related resources, including issues, merge requests, etc.", ), }, }; diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue index 823fa4ab413..07ee3c6083b 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue @@ -1,6 +1,4 @@ <script> -// Work around for https://github.com/vuejs/eslint-plugin-vue/issues/1411 -/* eslint-disable vue/one-component-per-file */ import { GlDropdown } from '@gitlab/ui'; import Tracking from '~/tracking'; import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; diff --git a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue index a445a85a2f3..6d21936791c 100644 --- a/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue +++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions.vue @@ -1,6 +1,4 @@ <script> -// Work around for https://github.com/vuejs/eslint-plugin-vue/issues/1411 -/* eslint-disable vue/one-component-per-file */ import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { __ } from '~/locale'; import Tracking from '~/tracking'; diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue index bc23ca6b1fc..677c50ed930 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue @@ -43,10 +43,9 @@ export default { <gl-button v-if="showDisabledButton" - type="button" category="primary" variant="success" - class="js-disabled-merge-button" + data-testid="disabled-merge-button" :disabled="true" > {{ s__('mrWidget|Merge') }} diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue index a5ec095b8ec..f411f91245d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue @@ -1,6 +1,6 @@ <script> import { GlButton } from '@gitlab/ui'; -import { n__ } from '~/locale'; +import { sprintf, s__, n__ } from '~/locale'; import { stripHtml } from '~/lib/utils/text_utility'; import statusIcon from '../mr_widget_status_icon.vue'; import eventHub from '../../event_hub'; @@ -31,7 +31,15 @@ export default { computed: { mergeError() { - return this.mr.mergeError ? stripHtml(this.mr.mergeError, ' ').trim() : ''; + const mergeError = this.mr.mergeError ? stripHtml(this.mr.mergeError, ' ').trim() : ''; + + return sprintf( + s__('mrWidget|%{mergeError}.'), + { + mergeError, + }, + false, + ); }, timerText() { return n__( diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index 519576d9fe6..99105a6eb62 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -191,7 +191,7 @@ export default { } return sprintf( - s__('mrWidget|Merge failed: %{mergeError}. Please try again.'), + s__('mrWidget|%{mergeError}. Try again.'), { mergeError, }, diff --git a/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js b/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js index 3b46e677636..e1734809bce 100644 --- a/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js +++ b/app/assets/javascripts/vue_shared/gl_feature_flags_plugin.js @@ -1,6 +1,3 @@ -// This is a false violation of @gitlab/no-runtime-template-compiler, since it -// is simply defining a global Vue mixin. -/* eslint-disable @gitlab/no-runtime-template-compiler */ export default (Vue) => { Vue.mixin({ provide: { diff --git a/app/assets/javascripts/vue_shared/translate.js b/app/assets/javascripts/vue_shared/translate.js index 2390aff3bca..616848639f1 100644 --- a/app/assets/javascripts/vue_shared/translate.js +++ b/app/assets/javascripts/vue_shared/translate.js @@ -1,6 +1,3 @@ -// This is a false violation of @gitlab/no-runtime-template-compiler, since it -// is simply defining a global Vue mixin. -/* eslint-disable @gitlab/no-runtime-template-compiler */ import { __, n__, s__, sprintf } from '../locale'; export default (Vue) => { diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb index 7394e8bf615..61eb9a27560 100644 --- a/app/controllers/import/bulk_imports_controller.rb +++ b/app/controllers/import/bulk_imports_controller.rb @@ -22,7 +22,13 @@ class Import::BulkImportsController < ApplicationController def status respond_to do |format| format.json do - render json: { importable_data: serialized_importable_data } + data = importable_data + + pagination_headers.each do |header| + response.set_header(header, data.headers[header]) + end + + render json: { importable_data: serialized_data(data.parsed_response) } end format.html do @source_url = session[url_key] @@ -44,8 +50,12 @@ class Import::BulkImportsController < ApplicationController private - def serialized_importable_data - serializer.represent(importable_data, {}, Import::BulkImportEntity) + def pagination_headers + %w[x-next-page x-page x-per-page x-prev-page x-total x-total-pages] + end + + def serialized_data(data) + serializer.represent(data, {}, Import::BulkImportEntity) end def serializer @@ -53,7 +63,7 @@ class Import::BulkImportsController < ApplicationController end def importable_data - client.get('groups', query_params).parsed_response + client.get('groups', query_params) end # Default query string params used to fetch groups from GitLab source instance @@ -74,7 +84,9 @@ class Import::BulkImportsController < ApplicationController def client @client ||= BulkImports::Clients::Http.new( uri: session[url_key], - token: session[access_token_key] + token: session[access_token_key], + per_page: params[:per_page], + page: params[:page] ) end diff --git a/app/helpers/in_product_marketing_helper.rb b/app/helpers/in_product_marketing_helper.rb new file mode 100644 index 00000000000..9fdc9ee44d6 --- /dev/null +++ b/app/helpers/in_product_marketing_helper.rb @@ -0,0 +1,375 @@ +# frozen_string_literal: true + +module InProductMarketingHelper + def subject_line(track, series) + { + create: [ + s_('InProductMarketing|Create a project in GitLab in 5 minutes'), + s_('InProductMarketing|Import your project and code from GitHub, Bitbucket and others'), + s_('InProductMarketing|Understand repository mirroring') + ], + verify: [ + s_('InProductMarketing|Feel the need for speed?'), + s_('InProductMarketing|3 ways to dive into GitLab CI/CD'), + s_('InProductMarketing|Explore the power of GitLab CI/CD') + ], + trial: [ + s_('InProductMarketing|Go farther with GitLab'), + s_('InProductMarketing|Automated security scans directly within GitLab'), + s_('InProductMarketing|Take your source code management to the next level') + ], + team: [ + s_('InProductMarketing|Working in GitLab = more efficient'), + s_("InProductMarketing|Multiple owners, confusing workstreams? We've got you covered"), + s_('InProductMarketing|Your teams can be more efficient') + ] + }[track][series] + end + + def in_product_marketing_logo(track, series) + inline_image_link('mailers/in_product_marketing', "#{track}-#{series}.png", width: '150') + end + + def about_link(folder, image, width) + link_to inline_image_link(folder, image, { width: width, alt: s_('InProductMarketing|go to about.gitlab.com') }), 'https://about.gitlab.com/' + end + + def in_product_marketing_tagline(track, series) + { + create: [ + s_('InProductMarketing|Get started today'), + s_('InProductMarketing|Get our import guides'), + s_('InProductMarketing|Need an alternative to importing?') + ], + verify: [ + s_('InProductMarketing|Use GitLab CI/CD'), + s_('InProductMarketing|Test, create, deploy'), + s_('InProductMarketing|Are your runners ready?') + ], + trial: [ + s_('InProductMarketing|Start a free trial of GitLab Gold – no CC required'), + s_('InProductMarketing|Improve app security with a 30-day trial'), + s_('InProductMarketing|Start with a GitLab Gold free trial') + ], + team: [ + s_('InProductMarketing|Invite your colleagues to join in less than one minute'), + s_('InProductMarketing|Get your team set up on GitLab'), + nil + ] + }[track][series] + end + + def in_product_marketing_title(track, series) + { + create: [ + s_('InProductMarketing|Take your first steps with GitLab'), + s_('InProductMarketing|Start by importing your projects'), + s_('InProductMarketing|How (and why) mirroring makes sense') + ], + verify: [ + s_('InProductMarketing|Rapid development, simplified'), + s_('InProductMarketing|Get started with GitLab CI/CD'), + s_('InProductMarketing|Launch GitLab CI/CD in 20 minutes or less') + ], + trial: [ + s_('InProductMarketing|Give us one minute...'), + s_("InProductMarketing|Security that's integrated into your development lifecycle"), + s_('InProductMarketing|Improve code quality and streamline reviews') + ], + team: [ + s_('InProductMarketing|Team work makes the dream work'), + s_('InProductMarketing|*GitLab*, noun: a synonym for efficient teams'), + s_('InProductMarketing|Find out how your teams are really doing') + ] + }[track][series] + end + + def in_product_marketing_subtitle(track, series) + { + create: [ + s_('InProductMarketing|Dig in and create a project and a repo'), + s_("InProductMarketing|Here's what you need to know"), + s_('InProductMarketing|Try it out') + ], + verify: [ + s_('InProductMarketing|How to build and test faster'), + s_('InProductMarketing|Explore the options'), + s_('InProductMarketing|Follow our steps') + ], + trial: [ + s_('InProductMarketing|...and you can get a free trial of GitLab Gold'), + s_('InProductMarketing|Try GitLab Gold for free'), + s_('InProductMarketing|Better code in less time') + ], + team: [ + s_('InProductMarketing|Actually, GitLab makes the team work (better)'), + s_('InProductMarketing|Our tool brings all the things together'), + s_("InProductMarketing|It's all in the stats") + ] + }[track][series] + end + + def in_product_marketing_body_line1(track, series, format: nil) + { + create: [ + s_("InProductMarketing|To understand and get the most out of GitLab, start at the beginning and %{project_link}. In GitLab, repositories are part of a project, so after you've created your project you can go ahead and %{repo_link}.") % { project_link: project_link(format), repo_link: repo_link(format) }, + s_("InProductMarketing|Making the switch? It's easier than you think to import your projects into GitLab. Move %{github_link}, or import something %{bitbucket_link}.") % { github_link: github_link(format), bitbucket_link: bitbucket_link(format) }, + s_("InProductMarketing|Sometimes you're not ready to make a full transition to a new tool. If you're not ready to fully commit, %{mirroring_link} gives you a safe way to try out GitLab in parallel with your current tool.") % { mirroring_link: mirroring_link(format) } + ], + verify: [ + s_("InProductMarketing|Tired of wrestling with disparate tool chains, information silos and inefficient processes? GitLab's CI/CD is built on a DevOps platform with source code management, planning, monitoring and more ready to go. Find out %{ci_link}.") % { ci_link: ci_link(format) }, + s_("InProductMarketing|GitLab's CI/CD makes software development easier. Don't believe us? Here are three ways you can take it for a fast (and satisfying) test drive:"), + s_("InProductMarketing|Get going with CI/CD quickly using our %{quick_start_link}. Start with an available runner and then create a CI .yml file – it's really that easy.") % { quick_start_link: quick_start_link(format) } + ], + trial: [ + [ + s_("InProductMarketing|GitLab's premium tiers are designed to make you, your team and your application more efficient and more secure with features including but not limited to:"), + list([ + s_('InProductMarketing|%{strong_start}Company wide portfolio management%{strong_end} — including multi-level epics, scoped labels').html_safe % strong_options(format), + s_('InProductMarketing|%{strong_start}Multiple approval roles%{strong_end} — including code owners and required merge approvals').html_safe % strong_options(format), + s_('InProductMarketing|%{strong_start}Advanced application security%{strong_end} — including SAST, DAST scanning, FUZZ testing, dependency scanning, license compliance, secrete detection').html_safe % strong_options(format), + s_('InProductMarketing|%{strong_start}Executive level insights%{strong_end} — including reporting on productivity, tasks by type, days to completion, value stream').html_safe % strong_options(format) + ], format) + ].join("\n"), + s_('InProductMarketing|GitLab provides static application security testing (SAST), dynamic application security testing (DAST), container scanning, and dependency scanning to help you deliver secure applications along with license compliance.'), + s_('InProductMarketing|By enabling code owners and required merge approvals the right person will review the right MR. This is a win-win: cleaner code and a more efficient review process.') + ], + team: [ + [ + s_('InProductMarketing|Did you know teams that use GitLab are far more efficient?'), + list([ + s_('InProductMarketing|Goldman Sachs went from 1 build every two weeks to thousands of builds a day'), + s_('InProductMarketing|Ticketmaster decreased their CI build time by 15X') + ], format) + ].join("\n"), + s_("InProductMarketing|We know a thing or two about efficiency and we don't want to keep that to ourselves. Sign up for a free trial of GitLab Gold and your teams will be on it from day one."), + [ + s_('InProductMarketing|Stop wondering and use GitLab to answer questions like:'), + list([ + s_('InProductMarketing|How long does it take us to close issues/MRs by types like feature requests, bugs, tech debt, security?'), + s_('InProductMarketing|How many days does it take our team to complete various tasks?'), + s_('InProductMarketing|What does our value stream timeline look like from product to development to review and production?') + ], format) + ].join("\n") + ] + }[track][series] + end + + def in_product_marketing_body_line2(track, series, format: nil) + { + create: [ + s_("InProductMarketing|That's all it takes to get going with GitLab, but if you're new to working with Git, check out our %{basics_link} for helpful tips and tricks for getting started.") % { basics_link: basics_link(format) }, + s_("InProductMarketing|Have a different instance you'd like to import? Here's our %{import_link}.") % { import_link: import_link(format) }, + s_("InProductMarketing|It's also possible to simply %{external_repo_link} in order to take advantage of GitLab's CI/CD.") % { external_repo_link: external_repo_link(format) } + ], + verify: [ + nil, + list([ + s_('InProductMarketing|Start by %{performance_link}').html_safe % { performance_link: performance_link(format) }, + s_('InProductMarketing|Move on to easily creating a Pages website %{ci_template_link}').html_safe % { ci_template_link: ci_template_link(format) }, + s_('InProductMarketing|And finally %{deploy_link} a Python application.').html_safe % { deploy_link: deploy_link(format) } + ], format), + nil + ], + trial: [ + s_('InProductMarketing|Start a GitLab Gold trial today in less than one minute, no credit card required.'), + s_('InProductMarketing|Get started today with a 30-day GitLab Gold trial, no credit card required.'), + s_('InProductMarketing|Code owners and required merge approvals are part of the paid tiers of GitLab. You can start a free 30-day trial of GitLab Gold and enable these features in less than 5 minutes with no credit card required.') + ], + team: [ + s_('InProductMarketing|Invite your colleagues and start shipping code faster.'), + s_("InProductMarketing|Streamline code review, know at a glance who's unavailable, communicate in comments or in email and integrate with Slack so everyone's on the same page."), + s_('InProductMarketing|When your team is on GitLab these answers are a click away.') + ] + }[track][series] + end + + def cta_link(track, series, group, format: nil) + case format + when :html + link_to in_product_marketing_cta_text(track, series), in_product_marketing_cta_link(track, series, group), target: '_blank', rel: 'noopener noreferrer' + else + [in_product_marketing_cta_text(track, series), in_product_marketing_cta_link(track, series, group)].join(' >> ') + end + end + + def in_product_marketing_progress(track, series) + s_('InProductMarketing|This is email %{series} of 3 in the %{track} series.') % { series: series + 1, track: track.to_s.humanize } + end + + def footer_links(format: nil) + links = [ + [s_('InProductMarketing|Blog'), 'https://about.gitlab.com/blog'], + [s_('InProductMarketing|Twitter'), 'https://twitter.com/gitlab'], + [s_('InProductMarketing|Facebook'), 'https://www.facebook.com/gitlab'], + [s_('InProductMarketing|YouTube'), 'https://www.youtube.com/channel/UCnMGQ8QHMAnVIsI3xJrihhg'] + ] + case format + when :html + links.map do |text, link| + link_to(text, link) + end + else + '| ' + links.map do |text, link| + [text, link].join(' ') + end.join("\n| ") + end + end + + def address(format: nil) + s_('InProductMarketing|%{strong_start}GitLab Inc.%{strong_end} 268 Bush Street, #350, San Francisco, CA 94104, USA').html_safe % strong_options(format) + end + + def unsubscribe(format: nil) + parts = [ + s_('InProductMarketing|If you no longer wish to receive marketing emails from us,'), + s_('InProductMarketing|you may %{unsubscribe_link} at any time.') % { unsubscribe_link: unsubscribe_link(format) } + ] + case format + when :html + parts.join(' ') + else + parts.join("\n" + ' ' * 16) + end + end + + private + + def in_product_marketing_cta_text(track, series) + { + create: [ + s_('InProductMarketing|Create your first project!'), + s_('InProductMarketing|Master the art of importing!'), + s_('InProductMarketing|Understand your project options') + ], + verify: [ + s_('InProductMarketing|Get to know GitLab CI/CD'), + s_('InProductMarketing|Try it yourself'), + s_('InProductMarketing|Explore GitLab CI/CD') + ], + trial: [ + s_('InProductMarketing|Start a trial'), + s_('InProductMarketing|Beef up your security'), + s_('InProductMarketing|Go for the gold!') + ], + team: [ + s_('InProductMarketing|Invite your colleagues today'), + s_('InProductMarketing|Invite your team in less than 60 seconds'), + s_('InProductMarketing|Invite your team now') + ] + }[track][series] + end + + def in_product_marketing_cta_link(track, series, group) + { + create: [ + new_project_url, + new_project_url(anchor: 'import_project'), + help_page_url('user/project/repository/repository_mirroring') + ], + verify: [ + project_pipelines_url(group.projects.first), + project_pipelines_url(group.projects.first), + project_pipelines_url(group.projects.first) + ], + trial: [ + 'https://about.gitlab.com/free-trial/', + 'https://about.gitlab.com/free-trial/', + 'https://about.gitlab.com/free-trial/' + ], + team: [ + group_group_members_url(group), + group_group_members_url(group), + group_group_members_url(group) + ] + }[track][series] + end + + def project_link(format) + link(s_('InProductMarketing|create a project'), help_page_url('gitlab-basics/create-project'), format) + end + + def repo_link(format) + link(s_('InProductMarketing|set up a repo'), help_page_url('user/project/repository/index', anchor: 'create-a-repository'), format) + end + + def github_link(format) + link(s_('InProductMarketing|GitHub Enterprise projects to GitLab'), help_page_url('integration/github'), format) + end + + def bitbucket_link(format) + link(s_('InProductMarketing|from Bitbucket'), help_page_url('user/project/import/bitbucket_server'), format) + end + + def mirroring_link(format) + link(s_('InProductMarketing|repository mirroring'), help_page_url('user/project/repository/repository_mirroring'), format) + end + + def ci_link(format) + link(s_('InProductMarketing|how easy it is to get started'), help_page_url('ci/README'), format) + end + + def performance_link(format) + link(s_('InProductMarketing|testing browser performance'), help_page_url('user/project/merge_requests/browser_performance_testing'), format) + end + + def ci_template_link(format) + link(s_('InProductMarketing|using a CI/CD template'), help_page_url('user/project/pages/getting_started/pages_ci_cd_template'), format) + end + + def deploy_link(format) + link(s_('InProductMarketing|test and deploy'), help_page_url('ci/examples/test-and-deploy-python-application-to-heroku'), format) + end + + def quick_start_link(format) + link(s_('InProductMarketing|quick start guide'), help_page_url('ci/quick_start/README'), format) + end + + def basics_link(format) + link(s_('InProductMarketing|Git basics'), help_page_url('gitlab-basics/README'), format) + end + + def import_link(format) + link(s_('InProductMarketing|comprehensive guide'), help_page_url('user/project/import/index'), format) + end + + def external_repo_link(format) + link(s_('InProductMarketing|connect an external repository'), new_project_url(anchor: 'cicd_for_external_repo'), format) + end + + def unsubscribe_link(format) + link(s_('InProductMarketing|unsubscribe'), '%tag_unsubscribe_url%', format) + end + + def link(text, link, format) + case format + when :html + link_to text, link + else + "#{text} (#{link})" + end + end + + def list(array, format) + case format + when :html + tag.ul { array.map { |item| concat tag.li item} } + else + '- ' + array.join("\n- ") + end + end + + def strong_options(format) + case format + when :html + { strong_start: '<b>'.html_safe, strong_end: '</b>'.html_safe } + else + { strong_start: '', strong_end: '' } + end + end + + def inline_image_link(folder, image, **options) + attachments[image] = File.read(Rails.root.join("app/assets/images", folder, image)) + image_tag attachments[image].url, **options + end +end diff --git a/app/mailers/emails/in_product_marketing.rb b/app/mailers/emails/in_product_marketing.rb new file mode 100644 index 00000000000..0be9ec5f915 --- /dev/null +++ b/app/mailers/emails/in_product_marketing.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Emails + module InProductMarketing + include InProductMarketingHelper + + FROM_ADDRESS = 'GitLab <team@gitlab.com>'.freeze + CUSTOM_HEADERS = { + 'X-Mailgun-Track' => 'yes', + 'X-Mailgun-Track-Clicks' => 'yes', + 'X-Mailgun-Track-Opens' => 'yes', + 'X-Mailgun-Tag' => 'marketing' + }.freeze + + def in_product_marketing_email(recipient_id, group_id, track, series) + @track = track + @series = series + @group = Group.find(group_id) + + email = User.find(recipient_id).notification_email_for(@group) + subject = subject_line(track, series) + mail_to(to: email, subject: subject) + end + + private + + def mail_to(to:, subject:) + mail(to: to, subject: subject, from: FROM_ADDRESS, reply_to: FROM_ADDRESS, **CUSTOM_HEADERS) do |format| + format.html { render layout: nil } + format.text { render layout: nil } + end + end + end +end diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb index ebf6dd68ec7..8f947ea7113 100644 --- a/app/mailers/notify.rb +++ b/app/mailers/notify.rb @@ -21,6 +21,7 @@ class Notify < ApplicationMailer include Emails::Groups include Emails::Reviews include Emails::ServiceDesk + include Emails::InProductMarketing helper TimeboxesHelper helper MergeRequestsHelper @@ -32,6 +33,7 @@ class Notify < ApplicationMailer helper AvatarsHelper helper GitlabRoutingHelper helper IssuablesHelper + helper InProductMarketingHelper def test_email(recipient_email, subject, body) mail(to: recipient_email, diff --git a/app/models/concerns/atomic_internal_id.rb b/app/models/concerns/atomic_internal_id.rb index baa99fa5a7f..bbf9ecbcfe9 100644 --- a/app/models/concerns/atomic_internal_id.rb +++ b/app/models/concerns/atomic_internal_id.rb @@ -26,20 +26,31 @@ module AtomicInternalId extend ActiveSupport::Concern + MissingValueError = Class.new(StandardError) + class_methods do def has_internal_id( # rubocop:disable Naming/PredicateName - column, scope:, init: :not_given, ensure_if: nil, track_if: nil, - presence: true, backfill: false, hook_names: :create) + column, scope:, init: :not_given, ensure_if: nil, track_if: nil, presence: true, hook_names: :create) raise "has_internal_id init must not be nil if given." if init.nil? raise "has_internal_id needs to be defined on association." unless self.reflect_on_association(scope) init = infer_init(scope) if init == :not_given - before_validation :"track_#{scope}_#{column}!", on: hook_names, if: track_if - before_validation :"ensure_#{scope}_#{column}!", on: hook_names, if: ensure_if - validates column, presence: presence + callback_names = Array.wrap(hook_names).map { |hook_name| :"before_#{hook_name}" } + callback_names.each do |callback_name| + # rubocop:disable GitlabSecurity/PublicSend + public_send(callback_name, :"track_#{scope}_#{column}!", if: track_if) + public_send(callback_name, :"ensure_#{scope}_#{column}!", if: ensure_if) + # rubocop:enable GitlabSecurity/PublicSend + end + after_rollback :"clear_#{scope}_#{column}!", on: hook_names, if: ensure_if + + if presence + before_create :"validate_#{column}_exists!" + before_update :"validate_#{column}_exists!" + end define_singleton_internal_id_methods(scope, column, init) - define_instance_internal_id_methods(scope, column, init, backfill) + define_instance_internal_id_methods(scope, column, init) end private @@ -62,10 +73,8 @@ module AtomicInternalId # - track_{scope}_{column}! # - reset_{scope}_{column} # - {column}= - def define_instance_internal_id_methods(scope, column, init, backfill) + def define_instance_internal_id_methods(scope, column, init) define_method("ensure_#{scope}_#{column}!") do - return if backfill && self.class.where(column => nil).exists? - scope_value = internal_id_read_scope(scope) value = read_attribute(column) return value unless scope_value @@ -79,6 +88,8 @@ module AtomicInternalId internal_id_scope_usage, init) write_attribute(column, value) + + @internal_id_set_manually = false end value @@ -110,6 +121,7 @@ module AtomicInternalId super(value).tap do |v| # Indicate the iid was set from externally @internal_id_needs_tracking = true + @internal_id_set_manually = true end end @@ -128,6 +140,20 @@ module AtomicInternalId read_attribute(column) end + + define_method("clear_#{scope}_#{column}!") do + return if @internal_id_set_manually + + return unless public_send(:"#{column}_previously_changed?") # rubocop:disable GitlabSecurity/PublicSend + + write_attribute(column, nil) + end + + define_method("validate_#{column}_exists!") do + value = read_attribute(column) + + raise MissingValueError, "#{column} was unexpectedly blank!" if value.blank? + end end # Defines class methods: diff --git a/app/models/experiment.rb b/app/models/experiment.rb index 7dbc95f617a..354b1e0b6b9 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -10,6 +10,10 @@ class Experiment < ApplicationRecord find_or_create_by!(name: name).record_user_and_group(user, group_type, context) end + def self.add_group(name, variant:, group:) + find_or_create_by!(name: name).record_group_and_variant!(group, variant) + end + def self.record_conversion_event(name, user) find_or_create_by!(name: name).record_conversion_event_for_user(user) end @@ -24,4 +28,8 @@ class Experiment < ApplicationRecord def record_conversion_event_for_user(user) experiment_users.find_by(user: user, converted_at: nil)&.touch(:converted_at) end + + def record_group_and_variant!(group, variant) + experiment_subjects.find_or_initialize_by(group: group).update!(variant: variant) + end end diff --git a/app/models/onboarding_progress.rb b/app/models/onboarding_progress.rb index 419bbd595e9..38a9489a3ad 100644 --- a/app/models/onboarding_progress.rb +++ b/app/models/onboarding_progress.rb @@ -22,6 +22,24 @@ class OnboardingProgress < ApplicationRecord :repository_mirrored ].freeze + scope :incomplete_actions, -> (actions) do + Array.wrap(actions).inject(self) { |scope, action| scope.where(column_name(action) => nil) } + end + + scope :completed_actions, -> (actions) do + Array.wrap(actions).inject(self) { |scope, action| scope.where.not(column_name(action) => nil) } + end + + scope :completed_actions_with_latest_in_range, -> (actions, range) do + actions = Array(actions) + if actions.size == 1 + where(column_name(actions[0]) => range) + else + action_columns = actions.map { |action| arel_table[column_name(action)] } + completed_actions(actions).where(Arel::Nodes::NamedFunction.new('GREATEST', action_columns).between(range)) + end + end + class << self def onboard(namespace) return unless root_namespace?(namespace) @@ -44,12 +62,12 @@ class OnboardingProgress < ApplicationRecord where(namespace: namespace).where.not(action_column => nil).exists? end - private - def column_name(action) :"#{action}_at" end + private + def root_namespace?(namespace) namespace && namespace.root? end diff --git a/app/services/namespaces/in_product_marketing_emails_service.rb b/app/services/namespaces/in_product_marketing_emails_service.rb new file mode 100644 index 00000000000..45b4619ddbe --- /dev/null +++ b/app/services/namespaces/in_product_marketing_emails_service.rb @@ -0,0 +1,112 @@ +# frozen_string_literal: true + +module Namespaces + class InProductMarketingEmailsService + include Gitlab::Experimentation::GroupTypes + + INTERVAL_DAYS = [1, 5, 10].freeze + TRACKS = { + create: :git_write, + verify: :pipeline_created, + trial: :trial_started, + team: :user_added + }.freeze + + def self.send_for_all_tracks_and_intervals + TRACKS.each_key do |track| + INTERVAL_DAYS.each do |interval| + new(track, interval).execute + end + end + end + + def initialize(track, interval) + @track = track + @interval = interval + @sent_email_user_ids = [] + end + + def execute + groups_for_track.each_batch do |groups| + groups.each do |group| + send_email_for_group(group) + end + end + end + + private + + attr_reader :track, :interval, :sent_email_user_ids + + def send_email_for_group(group) + experiment_enabled_for_group = experiment_enabled_for_group?(group) + experiment_add_group(group, experiment_enabled_for_group) + return unless experiment_enabled_for_group + + users_for_group(group).each do |user| + send_email(user, group) if can_perform_action?(user, group) + end + end + + def experiment_enabled_for_group?(group) + Gitlab::Experimentation.in_experiment_group?(:in_product_marketing_emails, subject: group) + end + + def experiment_add_group(group, experiment_enabled_for_group) + variant = experiment_enabled_for_group ? GROUP_EXPERIMENTAL : GROUP_CONTROL + Experiment.add_group(:in_product_marketing_emails, variant: variant, group: group) + end + + # rubocop: disable CodeReuse/ActiveRecord + def groups_for_track + onboarding_progress_scope = OnboardingProgress + .completed_actions_with_latest_in_range(completed_actions, range) + .incomplete_actions(incomplete_action) + + Group.joins(:onboarding_progress).merge(onboarding_progress_scope) + end + + def users_for_group(group) + group.users.where(email_opted_in: true) + .where.not(id: sent_email_user_ids) + end + # rubocop: enable CodeReuse/ActiveRecord + + def can_perform_action?(user, group) + case track + when :create + user.can?(:create_projects, group) + when :verify + user.can?(:create_projects, group) + when :trial + user.can?(:start_trial, group) + when :team + user.can?(:admin_group_member, group) + else + raise NotImplementedError, "No ability defined for track #{track}" + end + end + + def send_email(user, group) + NotificationService.new.in_product_marketing(user.id, group.id, track, series) + sent_email_user_ids << user.id + end + + def completed_actions + index = TRACKS.keys.index(track) + index == 0 ? [:created] : TRACKS.values[0..index - 1] + end + + def range + (interval + 1).days.ago.beginning_of_day..(interval + 1).days.ago.end_of_day + end + + def incomplete_action + TRACKS[track] + end + + def series + INTERVAL_DAYS.index(interval) + end + end +end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 5a71e0eac7c..15ba1015dd0 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -664,6 +664,10 @@ class NotificationService end end + def in_product_marketing(user_id, group_id, track, series) + mailer.in_product_marketing_email(user_id, group_id, track, series).deliver_later + end + protected def new_resource_email(target, method) diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml index 67ac9d1c7b8..e6f12f4785a 100644 --- a/app/views/admin/appearances/_form.html.haml +++ b/app/views/admin/appearances/_form.html.haml @@ -54,10 +54,10 @@ .col-lg-8 .form-group = f.label :title, class: 'col-form-label label-bold' - = f.text_field :title, class: "form-control" + = f.text_field :title, class: "form-control gl-form-input" .form-group = f.label :description, class: 'col-form-label label-bold' - = f.text_area :description, class: "form-control", rows: 10 + = f.text_area :description, class: "form-control gl-form-input", rows: 10 .hint = parsed_with_gfm .form-group @@ -83,7 +83,7 @@ .form-group = f.label :new_project_guidelines, class: 'col-form-label label-bold' %p - = f.text_area :new_project_guidelines, class: "form-control", rows: 10 + = f.text_area :new_project_guidelines, class: "form-control gl-form-input", rows: 10 .hint = parsed_with_gfm @@ -96,7 +96,7 @@ .form-group = f.label :profile_image_guidelines, class: 'col-form-label label-bold' %p - = f.text_area :profile_image_guidelines, class: "form-control", rows: 10 + = f.text_area :profile_image_guidelines, class: "form-control gl-form-input", rows: 10 .hint = parsed_with_gfm diff --git a/app/views/admin/appearances/_system_header_footer_form.html.haml b/app/views/admin/appearances/_system_header_footer_form.html.haml index b50778a1076..4571d34a497 100644 --- a/app/views/admin/appearances/_system_header_footer_form.html.haml +++ b/app/views/admin/appearances/_system_header_footer_form.html.haml @@ -9,10 +9,10 @@ .col-lg-8 .form-group = form.label :header_message, _('Header message'), class: 'col-form-label label-bold' - = form.text_area :header_message, placeholder: _('State your message to activate'), class: "form-control js-autosize" + = form.text_area :header_message, placeholder: _('State your message to activate'), class: "form-control gl-form-input js-autosize" .form-group = form.label :footer_message, _('Footer message'), class: 'col-form-label label-bold' - = form.text_area :footer_message, placeholder: _('State your message to activate'), class: "form-control js-autosize" + = form.text_area :footer_message, placeholder: _('State your message to activate'), class: "form-control gl-form-input js-autosize" .form-group .form-check = form.check_box :email_header_and_footer_enabled, class: 'form-check-input' @@ -27,7 +27,7 @@ = _('Customize colors') .form-group.js-toggle-colors-container.hide = form.label :message_background_color, _('Background Color'), class: 'col-form-label label-bold' - = form.color_field :message_background_color, class: "form-control" + = form.color_field :message_background_color, class: "form-control gl-form-input" .form-group.js-toggle-colors-container.hide = form.label :message_font_color, _('Font Color'), class: 'col-form-label label-bold' - = form.color_field :message_font_color, class: "form-control" + = form.color_field :message_font_color, class: "form-control gl-form-input" diff --git a/app/views/admin/appearances/preview_sign_in.html.haml b/app/views/admin/appearances/preview_sign_in.html.haml index eec4719c13c..6e5bb45c3cc 100644 --- a/app/views/admin/appearances/preview_sign_in.html.haml +++ b/app/views/admin/appearances/preview_sign_in.html.haml @@ -3,10 +3,10 @@ %form.gl-show-field-errors .form-group = label_tag :login - = text_field_tag :login, nil, class: "form-control top", title: 'Please provide your username or email address.' + = text_field_tag :login, nil, class: "form-control gl-form-input top", title: 'Please provide your username or email address.' .form-group = label_tag :password - = password_field_tag :password, nil, class: "form-control bottom", title: 'This field is required.' + = password_field_tag :password, nil, class: "form-control gl-form-input bottom", title: 'This field is required.' .form-group = button_tag "Sign in", class: "btn gl-button btn-success" diff --git a/app/views/import/bulk_imports/status.html.haml b/app/views/import/bulk_imports/status.html.haml index 6757c32d1e1..17b1169609c 100644 --- a/app/views/import/bulk_imports/status.html.haml +++ b/app/views/import/bulk_imports/status.html.haml @@ -4,9 +4,8 @@ %h1.gl-my-0.gl-py-4.gl-font-size-h1.gl-border-solid.gl-border-gray-200.gl-border-0.gl-border-b-1 = s_('BulkImport|Import groups from GitLab') -%p.gl-my-0.gl-py-5.gl-border-solid.gl-border-gray-200.gl-border-0.gl-border-b-1 - = s_('BulkImport|Importing groups from %{link}').html_safe % { link: external_link(@source_url, @source_url) } #import-groups-mount-element{ data: { status_path: status_import_bulk_imports_path(format: :json), available_namespaces_path: import_available_namespaces_path(format: :json), - create_bulk_import_path: import_bulk_imports_path(format: :json) } } + create_bulk_import_path: import_bulk_imports_path(format: :json), + source_url: @source_url } } diff --git a/app/views/notify/in_product_marketing_email.html.haml b/app/views/notify/in_product_marketing_email.html.haml new file mode 100644 index 00000000000..024cfa97abc --- /dev/null +++ b/app/views/notify/in_product_marketing_email.html.haml @@ -0,0 +1,204 @@ +!!! +%html{ lang: "en" } + %head + %meta{ content: "text/html; charset=utf-8", "http-equiv" => "Content-Type" } + %meta{ content: "width=device-width, initial-scale=1", name: "viewport" } + %link{ href: "https://fonts.googleapis.com/css?family=Source+Sans+Pro:400,600", rel: "stylesheet", type: "text/css" } + %title= message.subject + :css + /* CLIENT-SPECIFIC STYLES */ + body, + table, + td, + a { + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + } + + table, + td { + mso-table-lspace: 0pt; + mso-table-rspace: 0pt; + } + + img { + -ms-interpolation-mode: bicubic; + } + + /* RESET STYLES */ + img { + border: 0; + height: auto; + line-height: 100%; + outline: none; + text-decoration: none; + } + + table { + border-collapse: collapse !important; + } + + body { + height: 100% !important; + margin: 0 !important; + padding: 0 !important; + width: 100% !important; + background-color: #ffffff; + color: #424242; + } + + a { + color: #6b4fbb; + text-decoration: underline; + } + + .cta_link a { + font-size: 24px; + font-family: 'Source Sans Pro', helvetica, arial, sans-serif; + color: #ffffff; + text-decoration: none; + border-radius: 5px; + -webkit-border-radius: 5px; + background-color: #6e49cb; + border-top: 15px solid #6e49cb; + border-bottom: 15px solid #6e49cb; + border-right: 40px solid #6e49cb; + border-left: 40px solid #6e49cb; + display: inline-block; + } + + .footernav { + display: inline !important; + } + + .footernav a { + color: #6e49cb; + } + + .address { + margin: 0; + font-size: 16px; + line-height: 26px; + } + + :css + /* iOS BLUE LINKS */ + a[x-apple-data-detectors] { + color: inherit !important; + text-decoration: none !important; + font-size: inherit !important; + font-family: inherit !important; + font-weight: inherit !important; + line-height: inherit !important; + } + /[if gte mso 9] + <xml> + <o:OfficeDocumentSettings> + <o:AllowPNG/> + <o:PixelsPerInch>96</o:PixelsPerInch> + </o:OfficeDocumentSettings> + </xml> + /[if (mso)|(mso 16)] + <style type="text/css"> + body, table, td, a, span { font-family: Arial, Helvetica, sans-serif !important; } + </style> + :css + @media only screen and (max-width: 595px) { + + .wrapper { + width: 100% !important; + margin: 0 auto !important; + padding: 0 !important; + } + + p, + li { + font-size: 18px !important; + line-height: 26px !important; + } + + .stack { + width: 100% !important; + } + + .stack-mobile-padding { + width: 100% !important; + margin-top: 20px !important; + } + + .callout { + padding-bottom: 20px !important; + } + + .redbutton { + text-align: center; + } + + .stack33 { + display: block !important; + width: 100% !important; + max-width: 100% !important; + direction: ltr !important; + text-align: center !important; + } + } + + @media only screen and (max-width: 480px) { + u~div { + width: 100vw !important; + } + + div>u~div { + width: 100% !important; + } + } + %body#body{ width: "100%" } + %table{ border: "0", cellpadding: "0", cellspacing: "0", role: "presentation", width: "100%" } + %tr + %td{ align: "center", style: "padding: 0px;" } + %table.wrapper{ border: "0", cellpadding: "0", cellspacing: "0", role: "presentation", width: "600" } + %tr + %td{ style: "padding: 0px;" } + #main-story.mktEditable{ mktoname: "main-story" } + %table{ border: "0", cellpadding: "0", cellspacing: "0", role: "presentation", width: "100%" } + %tr + %td{ align: "left", style: "padding: 0px;" } + = about_link('mailers/in_product_marketing', 'gitlab-logo-gray-rgb.png', 200) + %tr + %td{ "aria-hidden" => "true", height: "30", style: "font-size: 0; line-height: 0;" } + %tr + %td{ bgcolor: "#ffffff", height: "auto", style: "max-width: 600px; width: 100%; text-align: center; height: 200px; padding: 25px 15px; mso-line-height-rule: exactly; min-height: 40px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;", valign: "middle", width: "100%" } + = in_product_marketing_logo(@track, @series) + %h1{ style: "font-size: 40px; line-height: 46x; color: #000000; padding: 20px 0 0 0; font-weight: normal;" } + = in_product_marketing_title(@track, @series) + %h2{ style: "font-size: 28px; line-height: 34px; color: #000000; padding: 0; font-weight: 400;" } + = in_product_marketing_subtitle(@track, @series) + %tr + %td{ style: "padding: 10px 20px 30px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#000000; font-size: 18px; line-height: 24px;" } + %p{ style: "margin: 0 0 20px 0;" } + = in_product_marketing_body_line1(@track, @series, format: :html).html_safe + %p{ style: "margin: 0 0 20px 0;" } + = in_product_marketing_body_line2(@track, @series, format: :html).html_safe + %tr + %td{ align: "center", style: "padding: 10px 20px 80px 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" } + .cta_link= cta_link(@track, @series, @group, format: :html) + %tr{ style: "background-color: #ffffff;" } + %td{ style: "color: #424242; padding: 10px 30px; text-align: center; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;font-size: 16px; line-height: 22px; border: 1px solid #dddddd" } + %p + = in_product_marketing_progress(@track, @series) + %tr{ style: "background-color: #ffffff;" } + %td{ align: "center", style: "padding:50px 20px 0 20px;" } + = about_link('', 'gitlab_logo.png', 80) + %tr{ style: "background-color: #ffffff;" } + %td{ align: "center", style: "padding:0px ;" } + %tr{ style: "background-color: #ffffff;" } + %td{ align: "center", style: "padding:0px 10px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; " } + %span.footernav{ style: "color: #6e49cb; font-size: 16px; line-height: 26px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" } + = footer_links(format: :html).join(' ' * 3 + '|' + ' ' * 4).html_safe + %tr{ style: "background-color:#ffffff;" } + %td{ align: "center", style: "padding: 40px 30px 20px 30px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif;" } + .address= address(format: :html) + %tr{ style: "background-color: #ffffff;" } + %td{ align: "left", style: "padding:20px 30px 20px 30px;" } + %span.footernav{ style: "color: #6e49cb; font-size: 14px; line-height: 20px; font-family: 'Source Sans Pro', helvetica, arial, sans-serif; color:#424242;" } + = unsubscribe(format: :html).html_safe diff --git a/app/views/notify/in_product_marketing_email.text.erb b/app/views/notify/in_product_marketing_email.text.erb new file mode 100644 index 00000000000..ecc4c565b73 --- /dev/null +++ b/app/views/notify/in_product_marketing_email.text.erb @@ -0,0 +1,23 @@ +<%= in_product_marketing_tagline(@track, @series) %> + +<%= in_product_marketing_title(@track, @series) %> +<%= in_product_marketing_subtitle(@track, @series) %> + + +<%= in_product_marketing_body_line1(@track, @series) %> + +<%= in_product_marketing_body_line2(@track, @series) %> + +<%= cta_link(@track, @series, @group) %> + + + + + + + +<%= footer_links %> + +<%= address %> + +<%= unsubscribe %> diff --git a/app/views/projects/_export.html.haml b/app/views/projects/_export.html.haml index 86dfcda6d1b..f095f96779d 100644 --- a/app/views/projects/_export.html.haml +++ b/app/views/projects/_export.html.haml @@ -4,22 +4,20 @@ .sub-section{ data: { qa_selector: 'export_project_content' } } %h4= _('Export project') - %p= _('Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the "New Project" page.') - - .bs-callout.bs-callout-info - %p.gl-mb-0 - %p= _('The following items will be exported:') - %ul - - project_export_descriptions.each do |desc| - %li= desc - %p= _('The following items will NOT be exported:') - %ul - %li= _('Job logs and artifacts') - %li= _('Container registry images') - %li= _('CI variables') - %li= _('Webhooks') - %li= _('Any encrypted tokens') - %p= _('Once the exported file is ready, you will receive a notification email with a download link, or you can download it from this page.') + - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/import_export') } + %p= _('Export this project with all its related data in order to move it to a new GitLab instance. When the exported file is ready, you can download it from this page or from the download link in the email notification you will receive. You can then import it when creating a new project. %{link_start}Learn more.%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } + %p.gl-mb-0 + %p= _('The following items will be exported:') + %ul + - project_export_descriptions.each do |desc| + %li= desc + %p= _('The following items will NOT be exported:') + %ul + %li= _('Job logs and artifacts') + %li= _('Container registry images') + %li= _('CI variables') + %li= _('Webhooks') + %li= _('Any encrypted tokens') - if project.export_status == :finished = link_to _('Download export'), download_export_project_path(project), rel: 'nofollow', download: '', method: :get, class: "btn btn-default", data: { qa_selector: 'download_export_link' } diff --git a/app/views/projects/_remove.html.haml b/app/views/projects/_remove.html.haml index c246c45d0f7..e991a9b0ec7 100644 --- a/app/views/projects/_remove.html.haml +++ b/app/views/projects/_remove.html.haml @@ -3,7 +3,8 @@ .sub-section %h4.danger-title= _('Delete project') %p - %strong= _('Deleting the project will delete its repository and all related resources including issues, merge requests etc.') + %strong= _('Deleting the project will delete its repository and all related resources including issues, merge requests, etc.') + = link_to _('Learn more.'), help_page_path('user/project/settings/index', anchor: 'removing-a-fork-relationship'), target: '_blank', rel: 'noopener noreferrer' %p %strong= _('Deleted projects cannot be restored!') #js-project-delete-button{ data: { form_path: project_path(project), confirm_phrase: project.path } } diff --git a/app/views/projects/_remove_fork.html.haml b/app/views/projects/_remove_fork.html.haml index 2a7453902a8..8fa21966683 100644 --- a/app/views/projects/_remove_fork.html.haml +++ b/app/views/projects/_remove_fork.html.haml @@ -7,4 +7,5 @@ = form_for @project, url: remove_fork_project_path(@project), method: :delete, remote: true, html: { class: 'transfer-project' } do |f| %p %strong= _('Once removed, the fork relationship cannot be restored. This project will no longer be able to receive or send merge requests to the source project or other forks.') + = link_to _('Learn more.'), help_page_path('user/project/settings/index', anchor: 'removing-a-fork-relationship'), target: '_blank', rel: 'noopener noreferrer' = button_to _('Remove fork relationship'), '#', class: "gl-button btn btn-danger js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) } diff --git a/app/views/projects/_transfer.html.haml b/app/views/projects/_transfer.html.haml index eb7feb7bd3b..ee717c2deca 100644 --- a/app/views/projects/_transfer.html.haml +++ b/app/views/projects/_transfer.html.haml @@ -4,13 +4,14 @@ %h4.danger-title= _('Transfer project') = form_for @project, url: transfer_project_path(@project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } do |f| .form-group - = label_tag :new_namespace_id, nil, class: 'label-bold' do - %span= _('Select a new namespace') - .form-group - = select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2' + - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'transferring-an-existing-project-into-another-namespace') } + %p= _("Transfer your project into another namespace. %{link_start}Learn more.%{link_end}").html_safe % { link_start: link_start, link_end: '</a>'.html_safe } %ul %li= _("Be careful. Changing the project's namespace can have unintended side effects.") %li= _('You can only transfer the project to namespaces you manage.') %li= _('You will need to update your local repositories to point to the new location.') %li= _('Project visibility level will be changed to match namespace rules when transferring to a group.') + = label_tag :new_namespace_id, _('Select a new namespace'), class: 'gl-font-weight-bold' + .form-group + = select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2' = f.submit 'Transfer project', class: "gl-button btn btn-danger js-confirm-danger qa-transfer-button", data: { "confirm-danger-message" => transfer_project_message(@project) } diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml index b0317d84cdc..f9d11ec33d2 100644 --- a/app/views/projects/blob/_editor.html.haml +++ b/app/views/projects/blob/_editor.html.haml @@ -10,14 +10,14 @@ - if current_action?(:edit) || current_action?(:update) %span.float-left.gl-mr-3 = text_field_tag 'file_path', (params[:file_path] || @path), - class: 'form-control new-file-path js-file-path-name-input' + class: 'form-control gl-form-input new-file-path js-file-path-name-input' = render 'template_selectors' - if current_action?(:new) || current_action?(:create) %span.float-left.gl-mr-3 \/ = text_field_tag 'file_name', params[:file_name], placeholder: "File name", - required: true, class: 'form-control new-file-name js-file-path-name-input', value: params[:file_name] || (should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : '') + required: true, class: 'form-control gl-form-input new-file-name js-file-path-name-input', value: params[:file_name] || (should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : '') = render 'template_selectors' - if should_suggest_gitlab_ci_yml? .js-suggest-gitlab-ci-yml{ data: { target: '#gitlab-ci-yml-selector', diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index cc2319ab90a..ed088cd5f04 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -68,7 +68,9 @@ .settings-content .sub-section %h4= _('Housekeeping') - %p= _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.') + %p + = _('Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects.') + = link_to _('Learn more.'), help_page_path('administration/housekeeping'), target: '_blank', rel: 'noopener noreferrer' = link_to _('Run housekeeping'), housekeeping_project_path(@project), method: :post, class: "gl-button btn btn-default" @@ -80,6 +82,13 @@ = render 'projects/errors' = form_for @project do |f| .form-group + - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'renaming-a-repository') } + %p= _("A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}").html_safe % { link_start: link_start, link_end: '</a>'.html_safe } + %ul + %li= _("Be careful. Renaming a project's repository can have unintended side effects.") + %li= _('You will need to update your local repositories to point to the new location.') + - if @project.deployment_platform.present? + %li= _('Your deployment services will be broken, you will need to manually fix the services after renaming.') = f.label :path, _('Path'), class: 'label-bold' .form-group .input-group @@ -87,11 +96,6 @@ .input-group-text #{Gitlab::Utils.append_path(root_url, @project.namespace.full_path)}/ = f.text_field :path, class: 'form-control qa-project-path-field h-auto' - %ul - %li= _("Be careful. Renaming a project's repository can have unintended side effects.") - %li= _('You will need to update your local repositories to point to the new location.') - - if @project.deployment_platform.present? - %li= _('Your deployment services will be broken, you will need to manually fix the services after renaming.') = f.submit _('Change path'), class: "gl-button btn btn-warning qa-change-path-button" = render 'transfer', project: @project diff --git a/app/views/projects/pipeline_schedules/_form.html.haml b/app/views/projects/pipeline_schedules/_form.html.haml index ee0fe43e79c..8a369202555 100644 --- a/app/views/projects/pipeline_schedules/_form.html.haml +++ b/app/views/projects/pipeline_schedules/_form.html.haml @@ -39,5 +39,5 @@ = f.check_box :active, required: false, value: @schedule.active? = f.label :active, _('Active'), class: 'gl-font-weight-normal' .footer-block.row-content-block - = f.submit _('Save pipeline schedule'), class: 'btn btn-success' - = link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn btn-cancel' + = f.submit _('Save pipeline schedule'), class: 'btn gl-button btn-success' + = link_to _('Cancel'), pipeline_schedules_path(@project), class: 'btn gl-button btn-default btn-cancel' diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml index 45aaf2b64bf..e17c905e092 100644 --- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml +++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml @@ -27,14 +27,14 @@ %td .float-right.btn-group - if can?(current_user, :play_pipeline_schedule, pipeline_schedule) - = link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('Play'), class: 'btn btn-svg gl-display-flex gl-align-items-center gl-justify-content-center' do + = link_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('Play'), class: 'btn gl-button btn-default btn-svg' do = sprite_icon('play') - if can?(current_user, :take_ownership_pipeline_schedule, pipeline_schedule) - = link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn' do + = link_to take_ownership_pipeline_schedule_path(pipeline_schedule), method: :post, title: s_('PipelineSchedules|Take ownership'), class: 'btn gl-button btn-default' do = s_('PipelineSchedules|Take ownership') - if can?(current_user, :update_pipeline_schedule, pipeline_schedule) - = link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn gl-display-flex' do + = link_to edit_pipeline_schedule_path(pipeline_schedule), title: _('Edit'), class: 'btn gl-button btn-default' do = sprite_icon('pencil') - if can?(current_user, :admin_pipeline_schedule, pipeline_schedule) - = link_to pipeline_schedule_path(pipeline_schedule), title: _('Delete'), method: :delete, class: 'gl-button btn btn-danger', data: { confirm: _("Are you sure you want to delete this pipeline schedule?") } do + = link_to pipeline_schedule_path(pipeline_schedule), title: _('Delete'), method: :delete, class: 'btn gl-button btn-danger', data: { confirm: _("Are you sure you want to delete this pipeline schedule?") } do = sprite_icon('remove') diff --git a/app/views/projects/pipeline_schedules/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml index 90417a852d5..558c12c04e4 100644 --- a/app/views/projects/pipeline_schedules/index.html.haml +++ b/app/views/projects/pipeline_schedules/index.html.haml @@ -9,7 +9,7 @@ - if can?(current_user, :create_pipeline_schedule, @project) .nav-controls - = link_to new_project_pipeline_schedule_path(@project), class: 'btn btn-success' do + = link_to new_project_pipeline_schedule_path(@project), class: 'btn gl-button btn-success' do %span= _('New schedule') - if @schedules.present? diff --git a/app/views/projects/settings/_archive.html.haml b/app/views/projects/settings/_archive.html.haml index 9fc643341f4..4300ebb4852 100644 --- a/app/views/projects/settings/_archive.html.haml +++ b/app/views/projects/settings/_archive.html.haml @@ -7,12 +7,14 @@ - else = _('Archive project') - if @project.archived? - %p= _("Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } + - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'unarchiving-a-project') } + %p= _("Unarchiving the project will restore its members' ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe } = link_to _('Unarchive project'), unarchive_project_path(@project), data: { confirm: _("Are you sure that you want to unarchive this project?"), qa_selector: 'unarchive_project_link' }, method: :post, class: "gl-button btn btn-success" - else - %p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe } + - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/settings/index', anchor: 'archiving-a-project') } + %p= _("Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}").html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, link_start: link_start, link_end: '</a>'.html_safe } = link_to _('Archive project'), archive_project_path(@project), data: { confirm: _("Are you sure that you want to archive this project?"), qa_selector: 'archive_project_link' }, method: :post, class: "gl-button btn btn-warning" diff --git a/app/views/shared/_commit_message_container.html.haml b/app/views/shared/_commit_message_container.html.haml index d65b7492690..47ecc75af1f 100644 --- a/app/views/shared/_commit_message_container.html.haml +++ b/app/views/shared/_commit_message_container.html.haml @@ -8,7 +8,7 @@ .max-width-marker = text_area_tag 'commit_message', (params[:commit_message] || local_assigns[:text] || local_assigns[:placeholder]), - class: 'form-control js-commit-message', placeholder: local_assigns[:placeholder], + class: 'form-control gl-form-input js-commit-message', placeholder: local_assigns[:placeholder], data: descriptions, required: true, rows: (local_assigns[:rows] || 3), id: "commit_message-#{nonce}" diff --git a/app/views/shared/_new_commit_form.html.haml b/app/views/shared/_new_commit_form.html.haml index 81c33eeea4f..62ba89e2576 100644 --- a/app/views/shared/_new_commit_form.html.haml +++ b/app/views/shared/_new_commit_form.html.haml @@ -10,7 +10,7 @@ .form-group.row.branch = label_tag 'branch_name', _('Target Branch'), class: 'col-form-label col-sm-2' .col-sm-10 - = text_field_tag 'branch_name', branch_name, required: true, class: "form-control js-branch-name ref-name" + = text_field_tag 'branch_name', branch_name, required: true, class: "form-control gl-form-input js-branch-name ref-name" .js-create-merge-request-container = render 'shared/new_merge_request_checkbox' diff --git a/app/views/shared/issuable/_feed_buttons.html.haml b/app/views/shared/issuable/_feed_buttons.html.haml index 86c2e243718..1fac1d27583 100644 --- a/app/views/shared/issuable/_feed_buttons.html.haml +++ b/app/views/shared/issuable/_feed_buttons.html.haml @@ -1,4 +1,4 @@ -= link_to safe_params.merge(rss_url_options), class: 'btn btn-svg has-tooltip', data: { container: 'body', testid: 'rss-feed-link' }, title: _('Subscribe to RSS feed') do += link_to safe_params.merge(rss_url_options), class: 'btn gl-button btn-default has-tooltip', data: { container: 'body', testid: 'rss-feed-link' }, title: _('Subscribe to RSS feed') do = sprite_icon('rss', css_class: 'qa-rss-icon') -= link_to safe_params.merge(calendar_url_options), class: 'btn has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do += link_to safe_params.merge(calendar_url_options), class: 'btn gl-button btn-default has-tooltip', data: { container: 'body' }, title: _('Subscribe to calendar') do = sprite_icon('calendar') diff --git a/app/views/shared/issuable/csv_export/_button.html.haml b/app/views/shared/issuable/csv_export/_button.html.haml index 3584c9c1ed5..ab68e1d69b8 100644 --- a/app/views/shared/issuable/csv_export/_button.html.haml +++ b/app/views/shared/issuable/csv_export/_button.html.haml @@ -1,4 +1,4 @@ - if current_user - %button.csv_download_link.btn.gl-button.has-tooltip{ title: _('Export as CSV'), + %button.csv_download_link.btn.gl-button.btn-default.has-tooltip{ title: _('Export as CSV'), data: { toggle: 'modal', target: ".#{issuable_type}-export-modal", qa_selector: 'export_as_csv_button' } } = sprite_icon('export') diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 9b6c9625e28..8c26cb02d4b 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -251,6 +251,14 @@ :weight: 1 :idempotent: true :tags: [] +- :name: cronjob:namespaces_in_product_marketing_emails + :feature_category: :subgroups + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: + :tags: [] - :name: cronjob:namespaces_prune_aggregation_schedules :feature_category: :source_code_management :has_external_dependencies: diff --git a/app/workers/namespaces/in_product_marketing_emails_worker.rb b/app/workers/namespaces/in_product_marketing_emails_worker.rb new file mode 100644 index 00000000000..66d140928a7 --- /dev/null +++ b/app/workers/namespaces/in_product_marketing_emails_worker.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Namespaces + class InProductMarketingEmailsWorker # rubocop:disable Scalability/IdempotentWorker + include ApplicationWorker + include CronjobQueue # rubocop:disable Scalability/CronWorkerContext + + feature_category :subgroups + urgency :low + + def perform + return unless Gitlab::Experimentation.active?(:in_product_marketing_emails) + + Namespaces::InProductMarketingEmailsService.send_for_all_tracks_and_intervals + end + end +end diff --git a/changelogs/unreleased/296133-engineering-in-product-email-campaigns-in-saas.yml b/changelogs/unreleased/296133-engineering-in-product-email-campaigns-in-saas.yml new file mode 100644 index 00000000000..c796d23c93f --- /dev/null +++ b/changelogs/unreleased/296133-engineering-in-product-email-campaigns-in-saas.yml @@ -0,0 +1,5 @@ +--- +title: Add indexes for onboarding progress table +merge_request: 50679 +author: +type: performance diff --git a/changelogs/unreleased/300156-external-request-tracking-errors-on-ipv6-host.yml b/changelogs/unreleased/300156-external-request-tracking-errors-on-ipv6-host.yml new file mode 100644 index 00000000000..919b550cc2c --- /dev/null +++ b/changelogs/unreleased/300156-external-request-tracking-errors-on-ipv6-host.yml @@ -0,0 +1,5 @@ +--- +title: Handle IPv6 hostname in ExternalHTTP instrumenter +merge_request: 52691 +author: +type: fixed diff --git a/changelogs/unreleased/30141-improve-merge-failed-error-fe.yml b/changelogs/unreleased/30141-improve-merge-failed-error-fe.yml new file mode 100644 index 00000000000..697a1819d83 --- /dev/null +++ b/changelogs/unreleased/30141-improve-merge-failed-error-fe.yml @@ -0,0 +1,5 @@ +--- +title: Improve merge failed error +merge_request: 52555 +author: +type: changed diff --git a/changelogs/unreleased/apply-gl-button-schedules.yml b/changelogs/unreleased/apply-gl-button-schedules.yml new file mode 100644 index 00000000000..301f5a98b9b --- /dev/null +++ b/changelogs/unreleased/apply-gl-button-schedules.yml @@ -0,0 +1,5 @@ +--- +title: Apply new GitLab UI for buttons in pipeline schedules +merge_request: +author: +type: other diff --git a/changelogs/unreleased/new-gitlab-ui-blob-editor.yml b/changelogs/unreleased/new-gitlab-ui-blob-editor.yml new file mode 100644 index 00000000000..ffec3021a33 --- /dev/null +++ b/changelogs/unreleased/new-gitlab-ui-blob-editor.yml @@ -0,0 +1,5 @@ +--- +title: Apply new GitLab UI for input fields in file editor +merge_request: 52461 +author: Yogi (@yo) +type: other diff --git a/changelogs/unreleased/xanf-add-filtering-and-pagination-to-bulk-import.yml b/changelogs/unreleased/xanf-add-filtering-and-pagination-to-bulk-import.yml new file mode 100644 index 00000000000..bf8345bd121 --- /dev/null +++ b/changelogs/unreleased/xanf-add-filtering-and-pagination-to-bulk-import.yml @@ -0,0 +1,5 @@ +--- +title: 'Add pagination and filtering to htoup imports' +merge_request: 52340 +author: +type: changed diff --git a/changelogs/unreleased/yo-gl-button-issues.yml b/changelogs/unreleased/yo-gl-button-issues.yml new file mode 100644 index 00000000000..7a68ea32d7f --- /dev/null +++ b/changelogs/unreleased/yo-gl-button-issues.yml @@ -0,0 +1,5 @@ +--- +title: Apply new GitLab UI for subscribe buttons in issues +merge_request: 52401 +author: Yogi (@yo) +type: other diff --git a/changelogs/unreleased/yo-gl-new-ui-admin-appearance.yml b/changelogs/unreleased/yo-gl-new-ui-admin-appearance.yml new file mode 100644 index 00000000000..b7e5a8ad9a7 --- /dev/null +++ b/changelogs/unreleased/yo-gl-new-ui-admin-appearance.yml @@ -0,0 +1,5 @@ +--- +title: Apply new GitLab UI for input field in admin/appearance +merge_request: 52409 +author: Yogi (@yo) +type: other diff --git a/config/feature_flags/experiment/in_product_marketing_emails_experiment_percentage.yml b/config/feature_flags/experiment/in_product_marketing_emails_experiment_percentage.yml new file mode 100644 index 00000000000..8cb198a2102 --- /dev/null +++ b/config/feature_flags/experiment/in_product_marketing_emails_experiment_percentage.yml @@ -0,0 +1,8 @@ +--- +name: in_product_marketing_emails_experiment_percentage +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50679 +rollout_issue_url: https://gitlab.com/gitlab-org/growth/team-tasks/-/issues/303 +milestone: "13.9" +type: experiment +group: group::activation +default_enabled: false diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index bbed08f5044..6339624ab7e 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -544,6 +544,9 @@ Settings.cron_jobs['schedule_merge_request_cleanup_refs_worker']['job_class'] = Settings.cron_jobs['manage_evidence_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['manage_evidence_worker']['cron'] ||= '0 * * * *' Settings.cron_jobs['manage_evidence_worker']['job_class'] = 'Releases::ManageEvidenceWorker' +Settings.cron_jobs['namespaces_in_product_marketing_emails_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['cron'] ||= '0 9 * * *' +Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['job_class'] = 'Namespaces::InProductMarketingEmailsWorker' Gitlab.ee do Settings.cron_jobs['analytics_devops_adoption_create_all_snapshots_worker'] ||= Settingslogic.new({}) diff --git a/db/migrate/20210114142443_add_indexes_to_onboarding_progresses.rb b/db/migrate/20210114142443_add_indexes_to_onboarding_progresses.rb new file mode 100644 index 00000000000..39964047e7f --- /dev/null +++ b/db/migrate/20210114142443_add_indexes_to_onboarding_progresses.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +class AddIndexesToOnboardingProgresses < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + CREATE_TRACK_INDEX_NAME = 'index_onboarding_progresses_for_create_track' + VERIFY_TRACK_INDEX_NAME = 'index_onboarding_progresses_for_verify_track' + TRIAL_TRACK_INDEX_NAME = 'index_onboarding_progresses_for_trial_track' + TEAM_TRACK_INDEX_NAME = 'index_onboarding_progresses_for_team_track' + + disable_ddl_transaction! + + def up + add_concurrent_index :onboarding_progresses, :created_at, where: 'git_write_at IS NULL', name: CREATE_TRACK_INDEX_NAME + add_concurrent_index :onboarding_progresses, :git_write_at, where: 'git_write_at IS NOT NULL AND pipeline_created_at IS NULL', name: VERIFY_TRACK_INDEX_NAME + add_concurrent_index :onboarding_progresses, 'GREATEST(git_write_at, pipeline_created_at)', where: 'git_write_at IS NOT NULL AND pipeline_created_at IS NOT NULL AND trial_started_at IS NULL', name: TRIAL_TRACK_INDEX_NAME + add_concurrent_index :onboarding_progresses, 'GREATEST(git_write_at, pipeline_created_at, trial_started_at)', where: 'git_write_at IS NOT NULL AND pipeline_created_at IS NOT NULL AND trial_started_at IS NOT NULL AND user_added_at IS NULL', name: TEAM_TRACK_INDEX_NAME + end + + def down + remove_concurrent_index_by_name :onboarding_progresses, CREATE_TRACK_INDEX_NAME + remove_concurrent_index_by_name :onboarding_progresses, VERIFY_TRACK_INDEX_NAME + remove_concurrent_index_by_name :onboarding_progresses, TRIAL_TRACK_INDEX_NAME + remove_concurrent_index_by_name :onboarding_progresses, TEAM_TRACK_INDEX_NAME + end +end diff --git a/db/schema_migrations/20210114142443 b/db/schema_migrations/20210114142443 new file mode 100644 index 00000000000..d4e771a56f5 --- /dev/null +++ b/db/schema_migrations/20210114142443 @@ -0,0 +1 @@ +7ef5cb1f167c133c67fc98c0abe929516ec700179747d3353d19cf8219ebd0ef
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index d67bd52781d..c7c6fae7a10 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -22511,6 +22511,14 @@ CREATE INDEX index_on_users_lower_username ON users USING btree (lower((username CREATE INDEX index_on_users_name_lower ON users USING btree (lower((name)::text)); +CREATE INDEX index_onboarding_progresses_for_create_track ON onboarding_progresses USING btree (created_at) WHERE (git_write_at IS NULL); + +CREATE INDEX index_onboarding_progresses_for_team_track ON onboarding_progresses USING btree (GREATEST(git_write_at, pipeline_created_at, trial_started_at)) WHERE ((git_write_at IS NOT NULL) AND (pipeline_created_at IS NOT NULL) AND (trial_started_at IS NOT NULL) AND (user_added_at IS NULL)); + +CREATE INDEX index_onboarding_progresses_for_trial_track ON onboarding_progresses USING btree (GREATEST(git_write_at, pipeline_created_at)) WHERE ((git_write_at IS NOT NULL) AND (pipeline_created_at IS NOT NULL) AND (trial_started_at IS NULL)); + +CREATE INDEX index_onboarding_progresses_for_verify_track ON onboarding_progresses USING btree (git_write_at) WHERE ((git_write_at IS NOT NULL) AND (pipeline_created_at IS NULL)); + CREATE UNIQUE INDEX index_onboarding_progresses_on_namespace_id ON onboarding_progresses USING btree (namespace_id); CREATE INDEX index_open_project_tracker_data_on_service_id ON open_project_tracker_data USING btree (service_id); diff --git a/doc/administration/auth/ldap/index.md b/doc/administration/auth/ldap/index.md index 80c3f173763..9945d330db0 100644 --- a/doc/administration/auth/ldap/index.md +++ b/doc/administration/auth/ldap/index.md @@ -53,8 +53,8 @@ are already logged in or are using Git over SSH are be able to access GitLab for up to one hour. Manually block the user in the GitLab Admin Area to immediately block all access. -GitLab Enterprise Edition Starter supports a -[configurable sync time](#adjusting-ldap-user-sync-schedule). **(STARTER)** +GitLab Enterprise Edition Premium supports a +[configurable sync time](#adjusting-ldap-user-sync-schedule). **(PREMIUM)** ## Git password authentication **(FREE SELF)** @@ -205,7 +205,7 @@ LDAP attributes that GitLab uses to create an account for the LDAP user. The spe | `first_name` | LDAP attribute for user first name. Used when the attribute configured for `name` does not exist. | no | `'givenName'` | | `last_name` | LDAP attribute for user last name. Used when the attribute configured for `name` does not exist. | no | `'sn'` | -### LDAP Sync Configuration Settings **(STARTER ONLY)** +### LDAP Sync Configuration Settings **(PREMIUM SELF)** | Setting | Description | Required | Examples | | ------- | ----------- | -------- | -------- | @@ -254,7 +254,7 @@ group, you can use the following syntax: For more information about this "LDAP_MATCHING_RULE_IN_CHAIN" filter, see the following [Microsoft Search Filter Syntax](https://docs.microsoft.com/en-us/windows/win32/adsi/search-filter-syntax) document. Support for nested members in the user filter should not be confused with -[group sync nested groups support](#supported-ldap-group-typesattributes). **(STARTER ONLY)** +[group sync nested groups support](#supported-ldap-group-typesattributes). **(PREMIUM SELF)** Please note that GitLab does not support the custom filter syntax used by OmniAuth LDAP. @@ -467,7 +467,7 @@ You should disable anonymous LDAP authentication and enable simple or SASL authentication. The TLS client authentication setting in your LDAP server cannot be mandatory and clients cannot be authenticated with the TLS protocol. -## Multiple LDAP servers **(STARTER ONLY)** +## Multiple LDAP servers **(PREMIUM SELF)** With GitLab Enterprise Edition Starter, you can configure multiple LDAP servers that your GitLab instance connects to. @@ -515,7 +515,7 @@ gitlab_rails['ldap_servers'] = { If you configure multiple LDAP servers, use a unique naming convention for the `label` section of each entry. That label is used as the display name of the tab shown on the sign-in page. -## User sync **(STARTER ONLY)** +## User sync **(PREMIUM SELF)** Once per day, GitLab runs a worker to check and update GitLab users against LDAP. @@ -546,7 +546,7 @@ The LDAP sync process: - Updates existing users. - Creates new users on first sign in. -### Adjusting LDAP user sync schedule **(STARTER ONLY)** +### Adjusting LDAP user sync schedule **(PREMIUM SELF)** By default, GitLab runs a worker once per day at 01:30 a.m. server time to check and update GitLab users against LDAP. @@ -579,7 +579,7 @@ sync to run once every 12 hours at the top of the hour. 1. [Restart GitLab](../../restart_gitlab.md#installations-from-source) for the changes to take effect. -## Group Sync **(STARTER ONLY)** +## Group Sync **(PREMIUM SELF)** If your LDAP supports the `memberof` property, when the user signs in for the first time GitLab triggers a sync for groups the user should be a member of. @@ -629,11 +629,11 @@ following. To take advantage of group sync, group owners or maintainers need to [create one or more LDAP group links](#adding-group-links). -### Adding group links **(STARTER ONLY)** +### Adding group links **(PREMIUM SELF)** For information on adding group links via CNs and filters, refer to [the GitLab groups documentation](../../../user/group/index.md#manage-group-memberships-via-ldap). -### Administrator sync **(STARTER ONLY)** +### Administrator sync **(PREMIUM SELF)** As an extension of group sync, you can automatically manage your global GitLab administrators. Specify a group CN for `admin_group` and all members of the @@ -677,7 +677,7 @@ group, as opposed to the full DN. 1. [Restart GitLab](../../restart_gitlab.md#installations-from-source) for the changes to take effect. -### Global group memberships lock **(STARTER ONLY)** +### Global group memberships lock **(PREMIUM SELF)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1793) in GitLab 12.0. @@ -696,7 +696,7 @@ To enable it you need to: 1. Navigate to **(admin)** **Admin Area > Settings -> Visibility and access controls**. 1. Make sure the "Lock memberships to LDAP synchronization" checkbox is enabled. -### Adjusting LDAP group sync schedule **(STARTER ONLY)** +### Adjusting LDAP group sync schedule **(PREMIUM SELF)** By default, GitLab runs a group sync process every hour, on the hour. The values shown are in cron format. If needed, you can use a @@ -735,7 +735,7 @@ sync to run once every 2 hours at the top of the hour. 1. [Restart GitLab](../../restart_gitlab.md#installations-from-source) for the changes to take effect. -### External groups **(STARTER ONLY)** +### External groups **(PREMIUM SELF)** Using the `external_groups` setting will allow you to mark all users belonging to these groups as [external users](../../../user/permissions.md#external-users). diff --git a/doc/administration/auth/ldap/ldap-troubleshooting.md b/doc/administration/auth/ldap/ldap-troubleshooting.md index 1976bab03c6..5640e938651 100644 --- a/doc/administration/auth/ldap/ldap-troubleshooting.md +++ b/doc/administration/auth/ldap/ldap-troubleshooting.md @@ -52,7 +52,7 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server admin_group: 'my_admin_group' ``` -#### Query LDAP **(STARTER ONLY)** +#### Query LDAP **(PREMIUM SELF)** The following allows you to perform a search in LDAP using the rails console. Depending on what you're trying to do, it may make more sense to query [a @@ -210,7 +210,7 @@ ldapsearch -H ldaps://$host:$port -D "$bind_dn" -y bind_dn_password.txt -b "$ba port. - We are assuming the password for the `bind_dn` user is in `bind_dn_password.txt`. -#### Sync all users **(STARTER ONLY)** +#### Sync all users **(PREMIUM SELF)** The output from a manual [user sync](index.md#user-sync) can show you what happens when GitLab tries to sync its users against LDAP. Enter the [rails console](#rails-console) @@ -225,7 +225,7 @@ LdapSyncWorker.new.perform Next, [learn how to read the output](#example-console-output-after-a-user-sync). -##### Example console output after a user sync **(STARTER ONLY)** +##### Example console output after a user sync **(PREMIUM SELF)** The output from a [manual user sync](#sync-all-users) will be very verbose, and a single user's successful sync can look like this: @@ -316,9 +316,9 @@ adapter = Gitlab::Auth::Ldap::Adapter.new('ldapmain') # If `main` is the LDAP pr Gitlab::Auth::Ldap::Person.find_by_uid('<uid>', adapter) ``` -### Group memberships **(STARTER ONLY)** +### Group memberships **(PREMIUM SELF)** -#### Membership(s) not granted **(STARTER ONLY)** +#### Membership(s) not granted **(PREMIUM SELF)** Sometimes you may think a particular user should be added to a GitLab group via LDAP group sync, but for some reason it's not happening. There are several @@ -376,7 +376,7 @@ group sync](#sync-all-groups) in the rails console and [look through the output](#example-console-output-after-a-group-sync) to see what happens when GitLab syncs the `admin_group`. -#### Sync all groups **(STARTER ONLY)** +#### Sync all groups **(PREMIUM SELF)** NOTE: To sync all groups manually when debugging is unnecessary, [use the Rake @@ -394,7 +394,7 @@ LdapAllGroupsSyncWorker.new.perform Next, [learn how to read the output](#example-console-output-after-a-group-sync). -##### Example console output after a group sync **(STARTER ONLY)** +##### Example console output after a group sync **(PREMIUM SELF)** Like the output from the user sync, the output from the [manual group sync](#sync-all-groups) will also be very verbose. However, it contains lots @@ -484,7 +484,7 @@ stating as such: No `admin_group` configured for 'ldapmain' provider. Skipping ``` -#### Sync one group **(STARTER ONLY)** +#### Sync one group **(PREMIUM SELF)** [Syncing all groups](#sync-all-groups) can produce a lot of noise in the output, which can be distracting when you're only interested in troubleshooting the memberships of @@ -506,7 +506,7 @@ EE::Gitlab::Auth::Ldap::Sync::Group.execute_all_providers(group) The output will be similar to [that you'd get from syncing all groups](#example-console-output-after-a-group-sync). -#### Query a group in LDAP **(STARTER ONLY)** +#### Query a group in LDAP **(PREMIUM SELF)** When you'd like to confirm that GitLab can read a LDAP group and see all its members, you can run the following: @@ -562,7 +562,7 @@ emails.each do |username, email| end ``` -You can then [run a UserSync](#sync-all-users) **(STARTER ONLY)** to sync the latest DN +You can then [run a UserSync](#sync-all-users) **(PREMIUM SELF)** to sync the latest DN for each of these users. ## Debugging Tools diff --git a/doc/administration/raketasks/ldap.md b/doc/administration/raketasks/ldap.md index 7fa158178dc..37598fa99d8 100644 --- a/doc/administration/raketasks/ldap.md +++ b/doc/administration/raketasks/ldap.md @@ -34,7 +34,7 @@ limit by passing a number to the check task: rake gitlab:ldap:check[50] ``` -## Run a group sync **(STARTER ONLY)** +## Run a group sync **(PREMIUM SELF)** > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/14735) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.2. diff --git a/doc/api/groups.md b/doc/api/groups.md index 1497dd0a98b..25571af9874 100644 --- a/doc/api/groups.md +++ b/doc/api/groups.md @@ -764,8 +764,8 @@ Parameters: | `request_access_enabled` | boolean | no | Allow users to request member access. | | `parent_id` | integer | no | The parent group ID for creating nested group. | | `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). Default to the global level default branch protection setting. | -| `shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` | -| `extra_shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). | +| `shared_runners_minutes_limit` | integer | no | **(PREMIUM SELF)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` | +| `extra_shared_runners_minutes_limit` | integer | no | **(PREMIUM SELF)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). | ### Options for `default_branch_protection` @@ -838,8 +838,8 @@ PUT /groups/:id | `request_access_enabled` | boolean | no | Allow users to request member access. | | `default_branch_protection` | integer | no | See [Options for `default_branch_protection`](#options-for-default_branch_protection). | | `file_template_project_id` | integer | no | **(PREMIUM)** The ID of a project to load custom file templates from. | -| `shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` | -| `extra_shared_runners_minutes_limit` | integer | no | **(STARTER ONLY)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). | +| `shared_runners_minutes_limit` | integer | no | **(PREMIUM SELF)** Pipeline minutes quota for this group (included in plan). Can be `nil` (default; inherit system default), `0` (unlimited) or `> 0` | +| `extra_shared_runners_minutes_limit` | integer | no | **(PREMIUM SELF)** Extra pipeline minutes quota for this group (purchased in addition to the minutes included in the plan). | | `prevent_forking_outside_group` | boolean | no | **(PREMIUM)** When enabled, users can **not** fork projects from this group to external namespaces | `shared_runners_setting` | string | no | See [Options for `shared_runners_setting`](#options-for-shared_runners_setting). Enable or disable shared runners for a group's subgroups and projects. | @@ -1135,7 +1135,7 @@ DELETE /groups/:id/hooks/:hook_id Group audit events can be accessed via the [Group Audit Events API](audit_events.md#group-audit-events) -## Sync group with LDAP **(STARTER ONLY)** +## Sync group with LDAP **(PREMIUM SELF)** Syncs the group with its linked LDAP group. Only available to group owners and administrators. @@ -1155,7 +1155,7 @@ Please consult the [Group Members](members.md) documentation. List, add, and delete LDAP group links. -### List LDAP group links **(STARTER ONLY)** +### List LDAP group links **(PREMIUM SELF)** Lists LDAP group links. @@ -1167,7 +1167,7 @@ GET /groups/:id/ldap_group_links | --------- | -------------- | -------- | ----------- | | `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) | -### Add LDAP group link with CN or filter **(STARTER ONLY)** +### Add LDAP group link with CN or filter **(PREMIUM SELF)** Adds an LDAP group link using a CN or filter. Adding a group link by filter is only supported in the Premium tier and above. @@ -1186,7 +1186,7 @@ POST /groups/:id/ldap_group_links NOTE: To define the LDAP group link, provide either a `cn` or a `filter`, but not both. -### Delete LDAP group link **(STARTER ONLY)** +### Delete LDAP group link **(PREMIUM SELF)** Deletes an LDAP group link. Deprecated. Scheduled for removal in a future release. @@ -1211,7 +1211,7 @@ DELETE /groups/:id/ldap_group_links/:provider/:cn | `cn` | string | yes | The CN of an LDAP group | | `provider` | string | yes | LDAP provider for the LDAP group link | -### Delete LDAP group link with CN or filter **(STARTER ONLY)** +### Delete LDAP group link with CN or filter **(PREMIUM SELF)** Deletes an LDAP group link using a CN or filter. Deleting by filter is only supported in the Premium tier and above. diff --git a/doc/development/feature_flags/controls.md b/doc/development/feature_flags/controls.md index adcf3175c45..48179f3acc7 100644 --- a/doc/development/feature_flags/controls.md +++ b/doc/development/feature_flags/controls.md @@ -226,6 +226,22 @@ you should fully roll out the feature by enabling the flag **globally** by runni This changes the feature flag state to be **enabled** always, which overrides the existing gates (e.g. `--group=gitlab-org`) in the above processes. +##### Disabling feature flags + +To disable a feature flag that has been globally enabled you can run: + +```shell +/chatops run feature set some_feature false +``` + +To disable a feature flag that has been enabled for a specific project you can run: + +```shell +/chatops run feature set --group=gitlab-org some_feature false +``` + +You cannot selectively disable feature flags for a specific project/group/user without applying a [specific method of implementing](development.md#selectively-disable-by-actor) the feature flags. + ### Feature flag change logging #### Chatops level diff --git a/doc/integration/kerberos.md b/doc/integration/kerberos.md index 46190b347f9..d8297ac87b3 100644 --- a/doc/integration/kerberos.md +++ b/doc/integration/kerberos.md @@ -5,7 +5,7 @@ info: "To determine the technical writer assigned to the Stage/Group associated type: reference, how-to --- -# Kerberos integration **(STARTER ONLY)** +# Kerberos integration **(PREMIUM SELF)** GitLab can integrate with [Kerberos](https://web.mit.edu/kerberos/) as an authentication mechanism. diff --git a/doc/integration/saml.md b/doc/integration/saml.md index ad7f59f9a97..c2da839d547 100644 --- a/doc/integration/saml.md +++ b/doc/integration/saml.md @@ -187,7 +187,7 @@ The name of the attribute can be anything you like, but it must contain the grou to which a user belongs. In order to tell GitLab where to find these groups, you need to add a `groups_attribute:` element to your SAML settings. -### Required groups **(STARTER ONLY)** +### Required groups **(PREMIUM SELF)** Your IdP passes Group Information to the SP (GitLab) in the SAML Response. You need to configure GitLab to identify: @@ -213,7 +213,7 @@ Example: } } ``` -### External Groups **(STARTER ONLY)** +### External groups **(PREMIUM SELF)** SAML login supports automatic identification on whether a user should be considered an [external](../user/permissions.md) user. This is based on the user's group membership in the SAML identity provider. @@ -231,7 +231,7 @@ SAML login supports automatic identification on whether a user should be conside } } ``` -### Admin Groups **(STARTER ONLY)** +### Admin groups **(PREMIUM SELF)** The requirements are the same as the previous settings, your IdP needs to pass Group information to GitLab, you need to tell GitLab where to look for the groups in the SAML response, and which group(s) should be @@ -251,7 +251,7 @@ considered admin users. } } ``` -### Auditor Groups **(STARTER ONLY)** +### Auditor groups **(PREMIUM SELF)** > Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 11.4. diff --git a/doc/user/discussions/img/resolve_thread_open_issue.png b/doc/user/discussions/img/resolve_thread_open_issue.png Binary files differdeleted file mode 100644 index 2dd4ea3cb1b..00000000000 --- a/doc/user/discussions/img/resolve_thread_open_issue.png +++ /dev/null diff --git a/doc/user/discussions/img/resolve_thread_open_issue_v13_9.png b/doc/user/discussions/img/resolve_thread_open_issue_v13_9.png Binary files differnew file mode 100644 index 00000000000..6611ca7b1ff --- /dev/null +++ b/doc/user/discussions/img/resolve_thread_open_issue_v13_9.png diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 33ced680c54..5924db3b2c9 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -115,7 +115,7 @@ are resolved](#only-allow-merge-requests-to-be-merged-if-all-threads-are-resolve there will be an **open an issue to resolve them later** link in the merge request widget. -![Link in merge request widget](img/resolve_thread_open_issue.png) +![Link in merge request widget](img/resolve_thread_open_issue_v13_9.png) This will prepare an issue with its content referring to the merge request and the unresolved threads. @@ -161,7 +161,7 @@ box and hit **Save** for the changes to take effect. From now on, you will not be able to merge from the UI until all threads are resolved. -![Only allow merge if all the threads are resolved message](img/resolve_thread_open_issue.png) +![Only allow merge if all the threads are resolved message](img/resolve_thread_open_issue_v13_9.png) ### Automatically resolve merge request diff threads when they become outdated diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 0cad3c24fa0..96e3f8250f6 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -327,7 +327,7 @@ A group's **Details** page includes tabs for: > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/207164) in GitLab [Starter](https://about.gitlab.com/pricing/) 12.10 as a [beta feature](https://about.gitlab.com/handbook/product/#beta) -The group details view also shows the number of the following items created in the last 90 days: **(STARTER)** +The group details view also shows the number of the following items created in the last 90 days: **(PREMIUM)** - Merge requests. - Issues. @@ -389,7 +389,7 @@ To share a given group, for example, 'Frontend' with another group, for example, All the members of the 'Engineering' group will have been added to 'Frontend'. -## Manage group memberships via LDAP **(STARTER ONLY)** +## Manage group memberships via LDAP **(PREMIUM SELF)** Group syncing allows LDAP groups to be mapped to GitLab groups. This provides more control over per-group user management. To configure group syncing edit the `group_base` **DN** (`'OU=Global Groups,OU=GitLab INT,DC=GitLab,DC=org'`). This **OU** contains all groups that will be associated with GitLab groups. @@ -400,7 +400,7 @@ For more information on the administration of LDAP and group sync, refer to the NOTE: If an LDAP user is a group member when LDAP Synchronization is added, and they are not part of the LDAP group, they will be removed from the group. -### Creating group links via CN **(STARTER ONLY)** +### Creating group links via CN **(PREMIUM SELF)** To create group links via CN: @@ -428,7 +428,7 @@ To create group links via filter: ![Creating group links via filter](img/ldap_sync_filter_v13_1.png) -### Overriding user permissions **(STARTER ONLY)** +### Overriding user permissions **(PREMIUM SELF)** In GitLab [8.15](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/822) and later, LDAP user permissions can now be manually overridden by an admin user. To override a user's permissions: @@ -616,7 +616,7 @@ To enable this feature, navigate to the group settings page. Select ![Checkbox for share with group lock](img/share_with_group_lock.png) -#### Member Lock **(STARTER)** +#### Member Lock **(PREMIUM)** Member lock lets a group owner prevent any new project membership to all of the projects within a group, allowing tighter control over project membership. @@ -814,11 +814,11 @@ To enable prevent project forking: - **Webhooks**: Configure [webhooks](../project/integrations/webhooks.md) for your group. - **Kubernetes cluster integration**: Connect your GitLab group with [Kubernetes clusters](clusters/index.md). - **Audit Events**: View [Audit Events](../../administration/audit_events.md) - for the group. **(STARTER ONLY)** + for the group. **(PREMIUM SELF)** - **Pipelines quota**: Keep track of the [pipeline quota](../admin_area/settings/continuous_integration.md) for the group. - **Integrations**: Configure [integrations](../admin_area/settings/project_integration_management.md) for your group. -#### Group push rules **(STARTER)** +#### Group push rules **(PREMIUM)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34370) in [GitLab Starter](https://about.gitlab.com/pricing/) 12.8. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/224129) in GitLab 13.4. @@ -839,7 +839,7 @@ When set, new subgroups have push rules set for them based on either: For information about setting a maximum artifact size for a group, see [Maximum artifacts size](../admin_area/settings/continuous_integration.md#maximum-artifacts-size). -## User contribution analysis **(STARTER)** +## User contribution analysis **(PREMIUM)** With [GitLab Contribution Analytics](contribution_analytics/index.md), you have an overview of the contributions (pushes, merge requests, diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md index 63ea84e42c9..e0947c182c0 100644 --- a/doc/user/project/code_owners.md +++ b/doc/user/project/code_owners.md @@ -7,9 +7,8 @@ type: reference # Code Owners **(STARTER)** -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6916) -in [GitLab Starter](https://about.gitlab.com/pricing/) 11.3. -> - Code Owners for Merge Request approvals was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/4418) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.9. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/6916) in GitLab 11.3. +> - Code Owners for Merge Request approvals was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/4418) in GitLab Premium 11.9. ## Introduction @@ -18,7 +17,7 @@ to find out who should review or approve merge requests. Additionally, if you have a question over a specific file or code block, it may be difficult to know who to find the answer from. -GitLab Code Owners is a feature to define who owns specific +The GitLab Code Owners feature defines who owns specific files or paths in a repository, allowing other users to understand who is responsible for each file or path. @@ -32,7 +31,7 @@ the process of finding the right reviewers and approvers for a given merge request. In larger organizations or popular open source projects, Code Owners -can also be useful to understand who to contact if you have +can help you understand who to contact if you have a question that may not be related to code review or a merge request approval. @@ -49,12 +48,12 @@ You can choose to add the `CODEOWNERS` file in three places: - Inside the `docs/` directory The `CODEOWNERS` file is valid for the branch where it lives. For example, if you change the code owners -in a feature branch, they won't be valid in the main branch until the feature branch is merged. +in a feature branch, the changes aren't valid in the main branch until the feature branch is merged. If you introduce new files to your repository and you want to identify the code owners for that file, -you have to update `CODEOWNERS` accordingly. If you update the code owners when you are adding the files (in the same -branch), GitLab will count the owners as soon as the branch is merged. If -you don't, you can do that later, but your new files will not belong to anyone until you update your +you must update `CODEOWNERS` accordingly. If you update the code owners when you are adding the files (in the same +branch), GitLab counts the owners as soon as the branch is merged. If +you don't, you can do that later, but your new files don't belong to anyone until you update your `CODEOWNERS` file in the TARGET branch. When a file matches multiple entries in the `CODEOWNERS` file, @@ -73,29 +72,32 @@ The user that would show for `README.md` would be `@user2`. ## Approvals by Code Owners -Once you've added Code Owners to a project, you can configure it to +After you've added Code Owners to a project, you can configure it to be used for merge request approvals: - As [merge request eligible approvers](merge_requests/merge_request_approvals.md#code-owners-as-eligible-approvers). - As required approvers for [protected branches](protected_branches.md#protected-branches-approval-by-code-owners). **(PREMIUM)** -Developer or higher [permissions](../permissions.md) are required in order to +Developer or higher [permissions](../permissions.md) are required to approve a merge request. -Once set, Code Owners are displayed in merge requests widgets: +After it's set, Code Owners are displayed in merge request widgets: ![MR widget - Code Owners](img/code_owners_mr_widget_v12_4.png) -While the `CODEOWNERS` file can be used in addition to Merge Request [Approval Rules](merge_requests/merge_request_approvals.md#approval-rules), -it can also be used as the sole driver of merge request approvals -(without using [Approval Rules](merge_requests/merge_request_approvals.md#approval-rules)). -To do so, create the file in one of the three locations specified above and -set the code owners as required approvers for [protected branches](protected_branches.md#protected-branches-approval-by-code-owners). -Use [the syntax of Code Owners files](code_owners.md#the-syntax-of-code-owners-files) -to specify the actual owners and granular permissions. +While you can use the `CODEOWNERS` file in addition to Merge Request +[Approval Rules](merge_requests/merge_request_approvals.md#approval-rules), +you can also use it as the sole driver of merge request approvals +without using [Approval Rules](merge_requests/merge_request_approvals.md#approval-rules): + +1. Create the file in one of the three locations specified above. +1. Set the code owners as required approvers for + [protected branches](protected_branches.md#protected-branches-approval-by-code-owners). +1. Use [the syntax of Code Owners files](code_owners.md#the-syntax-of-code-owners-files) + to specify the actual owners and granular permissions. Using Code Owners in conjunction with [Protected Branches](protected_branches.md#protected-branches-approval-by-code-owners) -will prevent any user who is not specified in the `CODEOWNERS` file from pushing +prevents any user who is not specified in the `CODEOWNERS` file from pushing changes for the specified files/paths, except those included in the **Allowed to push** column. This allows for a more inclusive push strategy, as administrators don't have to restrict developers from pushing directly to the @@ -114,13 +116,13 @@ in the `.gitignore` file followed by one or more of: - The `@name` of one or more groups that should be owners of the file. - Lines starting with `#` are ignored. -The order in which the paths are defined is significant: the last pattern that -matches a given path will be used to find the code owners. +The path definition order is significant: the last pattern +matching a given path is used to find the code owners. ### Groups as Code Owners -> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53182) in GitLab Starter 12.1. -> - Group and subgroup hierarchy support was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32432) in [GitLab Starter](https://about.gitlab.com/pricing/) 13.0. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53182) in GitLab 12.1. +> - Group and subgroup hierarchy support was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32432) in GitLab 13.0. Groups and subgroups members are inherited as eligible Code Owners to a project, as long as the hierarchy is respected. @@ -131,7 +133,7 @@ suppose you have a project called "Project A" within the group and a "Project B" within the subgroup. The eligible Code Owners to Project B are both the members of the Group X and -the Subgroup Y. And the eligible Code Owners to the Project A are just the +the Subgroup Y. The eligible Code Owners to the Project A are just the members of the Group X, given that Project A doesn't belong to the Subgroup Y: ![Eligible Code Owners](img/code_owners_members_v13_4.png) @@ -142,9 +144,9 @@ Code Owners: ![Invite subgroup members to become eligible Code Owners](img/code_owners_invite_members_v13_4.png) -Once invited, any member (`@user`) of the group or subgroup can be set -as Code Owner to files of the Project A or B, as well as the entire Group X -(`@group-x`) or Subgroup Y (`@group-x/subgroup-y`), as exemplified below: +After being invited, any member (`@user`) of the group or subgroup can be set +as Code Owner to files of the Project A or B, and the entire Group X +(`@group-x`) or Subgroup Y (`@group-x/subgroup-y`), as follows: ```plaintext # A member of the group or subgroup as Code Owner to a file @@ -162,7 +164,7 @@ file.md @group-x @group-x/subgroup-y ### Code Owners Sections **(PREMIUM)** -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12137) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.2 behind a feature flag, enabled by default. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12137) in GitLab Premium 13.2 behind a feature flag, enabled by default. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42389) in GitLab 13.4. Code Owner rules can be grouped into named sections. This allows for better @@ -185,8 +187,8 @@ changes, to set their own independent patterns by specifying discrete sections i teams can be added as reviewers. Sections can be added to `CODEOWNERS` files as a new line with the name of the -section inside square brackets. Every entry following it is assigned to that -section. The following example would create 2 Code Owner rules for the "README +section inside square brackets. Every entry following is assigned to that +section. The following example would create two Code Owner rules for the "README Owners" section: ```plaintext @@ -196,7 +198,7 @@ internal/README.md @user2 ``` Multiple sections can be used, even with matching file or directory patterns. -Reusing the same section name will group the results together under the same +Reusing the same section name groups the results together under the same section, with the most specific rule or last matching pattern being used. For example, consider the following entries in a `CODEOWNERS` file: @@ -213,7 +215,7 @@ model/db @gl-database README.md @gl-docs ``` -This will result in 3 entries under the "Documentation" section header, and 2 +This results in three entries under the "Documentation" section header, and two entries under "Database". Case is not considered when combining sections, so in this example, entries defined under the sections "Documentation" and "DOCUMENTATION" would be combined into one, using the case of the first instance @@ -227,9 +229,10 @@ the rules for "Groups" and "Documentation" sections: #### Optional Code Owners Sections **(PREMIUM)** -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232995) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.8 behind a feature flag, enabled by default. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232995) in GitLab Premium 13.8 behind a feature flag, enabled by default. -When you want to make a certain section optional, you can do so by adding a code owners section prepended with the caret `^` character. Approvals from owners listed in the section will **not** be required. For example: +To make a certain section optional, add a code owners section prepended with the +caret `^` character. Approvals from owners listed in the section are **not** required. For example: ```plaintext [Documentation] @@ -242,13 +245,13 @@ When you want to make a certain section optional, you can do so by adding a code *.go @root ``` -The optional code owners section will be displayed in merge requests under the **Approval Rules** area: +The optional code owners section displays in merge requests under the **Approval Rules** area: ![MR widget - Optional Code Owners Sections](img/optional_code_owners_sections_v13_8.png) -If a section is duplicated in the file, and one of them is marked as optional and the other isn't, the requirement prevails. +If a section is duplicated in the file, and one of them is marked as optional and the other isn't, the requirement prevails. -For example, the code owners of the "Documentation" section below will still be required to approve merge requests: +For example, the code owners of the "Documentation" section below is still required to approve merge requests: ```plaintext [Documentation] @@ -264,12 +267,12 @@ For example, the code owners of the "Documentation" section below will still be *.txt @root ``` -Optional sections in the code owners file are currently treated as optional only -when changes are submitted via merge requests. If a change is submitted directly -to the protected branch, approval from code owners will still be required, even if the -section is marked as optional. We plan to change this in a +Optional sections in the code owners file are treated as optional only +when changes are submitted by using merge requests. If a change is submitted directly +to the protected branch, approval from code owners is still required, even if the +section is marked as optional. We plan to change this behavior in a [future release](https://gitlab.com/gitlab-org/gitlab/-/issues/297638), -where direct pushes to the protected branch will be allowed for sections marked as optional. +and allow direct pushes to the protected branch for sections marked as optional. ## Example `CODEOWNERS` file diff --git a/doc/user/project/highlighting.md b/doc/user/project/highlighting.md index a49a942ab75..5ffc2878269 100644 --- a/doc/user/project/highlighting.md +++ b/doc/user/project/highlighting.md @@ -7,7 +7,7 @@ type: reference # Syntax Highlighting -GitLab provides syntax highlighting on all files through the [Rouge](https://rubygems.org/gems/rouge) Ruby gem. It will try to guess what language to use based on the file extension, which most of the time is sufficient. +GitLab provides syntax highlighting on all files through the [Rouge](https://rubygems.org/gems/rouge) Ruby gem. It attempts to guess what language to use based on the file extension, which most of the time is sufficient. NOTE: The [Web IDE](web_ide/index.md) and [Snippets](../snippets.md) use [Monaco Editor](https://microsoft.github.io/monaco-editor/) @@ -25,10 +25,10 @@ you can add the following to your `.gitattributes` file: ``` <!-- vale gitlab.Spelling = NO --> -When you check in and push that change, all `*.pl` files in your project will be highlighted as Prolog. +When you check in and push that change, all `*.pl` files in your project are highlighted as Prolog. <!-- vale gitlab.Spelling = YES --> -The paths here are simply Git's built-in [`.gitattributes` interface](https://git-scm.com/docs/gitattributes). So, if you were to invent a file format called a `Nicefile` at the root of your project that used Ruby syntax, all you need is: +The paths here are Git's built-in [`.gitattributes` interface](https://git-scm.com/docs/gitattributes). So, if you were to invent a file format called a `Nicefile` at the root of your project that used Ruby syntax, all you need is: ``` conf /Nicefile gitlab-language=ruby @@ -44,7 +44,8 @@ To disable highlighting entirely, use `gitlab-language=text`. Lots more fun shen /other-file gitlab-language=text?token=Error ``` -Please note that these configurations will only take effect when the `.gitattributes` file is in your default branch (usually `master`). +Please note that these configurations only take effect when the `.gitattributes` +file is in your default branch (usually `master`). NOTE: The Web IDE does not support `.gitattribute` files, but it's [planned for a future release](https://gitlab.com/gitlab-org/gitlab/-/issues/22014). diff --git a/doc/user/project/index.md b/doc/user/project/index.md index c092738b01c..607049b512e 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -17,12 +17,12 @@ the number of private projects you create. ## Project features -When you create a project in GitLab, you'll have access to a large number of +When you create a project in GitLab, you receive access to a large number of [features](https://about.gitlab.com/features/): **Repositories:** -- [Issue tracker](issues/index.md): Discuss implementations with your team within issues +- [Issue tracker](issues/index.md): Discuss implementations with your team in issues - [Issue Boards](issue_board.md): Organize and prioritize your workflow - [Multiple Issue Boards](issue_board.md#multiple-issue-boards): Allow your teams to create their own workflows (Issue Boards) for the same project - [Repositories](repository/index.md): Host your code in a fully @@ -42,13 +42,13 @@ When you create a project in GitLab, you'll have access to a large number of **Issues and merge requests:** -- [Issue tracker](issues/index.md): Discuss implementations with your team within issues +- [Issue tracker](issues/index.md): Discuss implementations with your team in issues - [Issue Boards](issue_board.md): Organize and prioritize your workflow - [Multiple Issue Boards](issue_board.md#multiple-issue-boards): Allow your teams to create their own workflows (Issue Boards) for the same project - [Merge Requests](merge_requests/index.md): Apply your branching strategy and get reviewed by your team - [Merge Request Approvals](merge_requests/merge_request_approvals.md): Ask for approval before - implementing a change **(STARTER)** + implementing a change - [Fix merge conflicts from the UI](merge_requests/resolve_conflicts.md): Your Git diff tool right from the GitLab UI - [Review Apps](../../ci/review_apps/index.md): Live preview the results @@ -108,7 +108,7 @@ When you create a project in GitLab, you'll have access to a large number of - [Conan packages](../packages/conan_repository/index.md): your private Conan repository in GitLab. - [Maven packages](../packages/maven_repository/index.md): your private Maven repository in GitLab. - [NPM packages](../packages/npm_registry/index.md): your private NPM package registry in GitLab. -- [Code owners](code_owners.md): specify code owners for certain files **(STARTER)** +- [Code owners](code_owners.md): specify code owners for certain files - [License Compliance](../compliance/license_compliance/index.md): approve and deny licenses for projects. **(ULTIMATE)** - [Dependency List](../application_security/dependency_list/index.md): view project dependencies. **(ULTIMATE)** - [Requirements](requirements/index.md): Requirements allow you to create criteria to check your products against. **(ULTIMATE)** @@ -192,7 +192,7 @@ To delete a project, first navigate to the home page for that project. 1. Click **Delete project** 1. Confirm this action by typing in the expected text. -Projects in personal namespaces are deleted immediately on request. For information on delayed deletion of projects within a group, please see [Enabling delayed project removal](../group/index.md#enabling-delayed-project-removal). +Projects in personal namespaces are deleted immediately on request. For information on delayed deletion of projects in a group, please see [Enabling delayed project removal](../group/index.md#enabling-delayed-project-removal). ## CI/CD for external repositories **(PREMIUM)** @@ -214,11 +214,11 @@ filtered by **Push events**, **Merge events**, **Issue events**, **Comments**, ### Leave a project -**Leave project** will only display on the project's dashboard +**Leave project** only displays on the project's dashboard when a project is part of a group (under a [group namespace](../group/index.md#namespaces)). -If you choose to leave a project you will no longer be a project -member, therefore, unable to contribute. +If you choose to leave a project you are no longer a project +member, and cannot contribute. ## Project's landing page @@ -230,15 +230,15 @@ with [permissions to view the project's code](../permissions.md#project-members- - The content of a [`README` or an index file](repository/#repository-readme-and-index-files) - is displayed (if any), followed by the list of directories within the + is displayed (if any), followed by the list of directories in the project's repository. - If the project doesn't contain either of these files, the - visitor will see the list of files and directories of the repository. + visitor sees the list of files and directories of the repository. -For users without permissions to view the project's code: +For users without permissions to view the project's code, GitLab displays: -- The wiki homepage is displayed, if any. -- The list of issues within the project is displayed. +- The wiki homepage, if any. +- The list of issues in the project. ## GitLab Workflow - VS Code extension @@ -259,15 +259,15 @@ Depending on the situation, different things apply. When [renaming a user](../profile/index.md#changing-your-username), [changing a group path](../group/index.md#changing-a-groups-path) or [renaming a repository](settings/index.md#renaming-a-repository): -- Existing web URLs for the namespace and anything under it (e.g., projects) will +- Existing web URLs for the namespace and anything under it (such as projects) will redirect to the new URLs. - Starting with GitLab 10.3, existing Git remote URLs for projects under the - namespace will redirect to the new remote URL. Every time you push/pull to a + namespace redirect to the new remote URL. Every time you push/pull to a repository that has changed its location, a warning message to update - your remote will be displayed instead of rejecting your action. - This means that any automation scripts, or Git clients will continue to + your remote is displayed instead of rejecting your action. + This means that any automation scripts, or Git clients continue to work after a rename, making any transition a lot smoother. -- The redirects will be available as long as the original path is not claimed by +- The redirects are available as long as the original path is not claimed by another group, user or project. ## Use your project as a Go package @@ -278,11 +278,11 @@ and `godoc.org` discovery requests, including the [`go-source`](https://github.com/golang/gddo/wiki/Source-Code-Links) meta tags. Private projects, including projects in subgroups, can be used as a Go package, -but may require configuration to work correctly. GitLab will respond correctly +but may require configuration to work correctly. GitLab responds correctly to `go get` discovery requests for projects that *are not* in subgroups, regardless of authentication or authorization. [Authentication](#authenticate-go-requests) is required to use a private project -in a subgroup as a Go package. Otherwise, GitLab will truncate the path for +in a subgroup as a Go package. Otherwise, GitLab truncates the path for private projects in subgroups to the first two segments, causing `go get` to fail. @@ -302,10 +302,10 @@ queries), and [`GONOSUMDB`](../../development/go_guide/dependencies.md#fetching) `GOPRIVATE`, `GONOPROXY`, and `GONOSUMDB` are comma-separated lists of Go modules and Go module prefixes. For example, -`GOPRIVATE=gitlab.example.com/my/private/project` will disable queries for that -one project, but `GOPRIVATE=gitlab.example.com` will disable queries for *all* -projects on GitLab.com. Go will not query module proxies if the module name or a -prefix of it appears in `GOPRIVATE` or `GONOPROXY`. Go will not query checksum +`GOPRIVATE=gitlab.example.com/my/private/project` disables queries for that +one project, but `GOPRIVATE=gitlab.example.com` disables queries for *all* +projects on GitLab.com. Go does not query module proxies if the module name or a +prefix of it appears in `GOPRIVATE` or `GONOPROXY`. Go does not query checksum databases if the module name or a prefix of it appears in `GONOPRIVATE` or `GONOSUMDB`. @@ -315,8 +315,8 @@ To authenticate requests to private projects made by Go, use a [`.netrc` file](https://ec.haxx.se/usingcurl-netrc.html) and a [personal access token](../profile/personal_access_tokens.md) in the password field. **This only works if your GitLab instance can be accessed with HTTPS.** The `go` command -will not transmit credentials over insecure connections. This will authenticate -all HTTPS requests made directly by Go but will not authenticate requests made +does not transmit credentials over insecure connections. This authenticates +all HTTPS requests made directly by Go, but does not authenticate requests made through Git. For example: @@ -332,16 +332,18 @@ On Windows, Go reads `~/_netrc` instead of `~/.netrc`. ### Authenticate Git fetches -If a module cannot be fetched from a proxy, Go will fall back to using Git (for -GitLab projects). Git will use `.netrc` to authenticate requests. Alternatively, -Git can be configured to embed specific credentials in the request URL, or to -use SSH instead of HTTPS (as Go always uses HTTPS to fetch Git repositories): +If a module cannot be fetched from a proxy, Go falls back to using Git (for +GitLab projects). Git uses `.netrc` to authenticate requests. You can also +configure Git to either: + +- Embed specific credentials in the request URL. +- Use SSH instead of HTTPS, as Go always uses HTTPS to fetch Git repositories. ```shell -# embed credentials in any request to GitLab.com: +# Embed credentials in any request to GitLab.com: git config --global url."https://${user}:${personal_access_token}@gitlab.example.com".insteadOf "https://gitlab.example.com" -# use SSH instead of HTTPS: +# Use SSH instead of HTTPS: git config --global url."git@gitlab.example.com".insteadOf "https://gitlab.example.com" ``` @@ -354,7 +356,7 @@ visit the `/projects/:id` URL in your browser or other tool accessing the projec ## Project aliases **(PREMIUM SELF)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3264) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.1. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/3264) in GitLab Premium 12.1. When migrating repositories to GitLab and they are being accessed by other systems, it's very useful to be able to access them using the same name especially when @@ -369,9 +371,9 @@ A project alias can be only created via API and only by GitLab administrators. Follow the [Project Aliases API documentation](../../api/project_aliases.md) for more details. -Once an alias has been created for a project (e.g., an alias `gitlab` for the -project `https://gitlab.com/gitlab-org/gitlab`), the repository can be cloned -using the alias (e.g `git clone git@gitlab.com:gitlab.git` instead of +After an alias has been created for a project (such as an alias `gitlab` for the +project `https://gitlab.com/gitlab-org/gitlab`), you can clone the repository +with the alias (e.g `git clone git@gitlab.com:gitlab.git` instead of `git clone git@gitlab.com:gitlab-org/gitlab.git`). ## Project activity analytics overview **(ULTIMATE SELF)** diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md index 4e87876b036..36fb5cea4b0 100644 --- a/doc/user/project/merge_requests/cherry_pick_changes.md +++ b/doc/user/project/merge_requests/cherry_pick_changes.md @@ -13,12 +13,13 @@ with introducing a **Cherry-pick** button in merge requests and commit details. ## Cherry-picking a merge request -After the merge request has been merged, a **Cherry-pick** button will be available +After the merge request has been merged, a **Cherry-pick** button displays to cherry-pick the changes introduced by that merge request. ![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png) -After you click that button, a modal will appear showing a [branch filter search box](../repository/branches/index.md#branch-filter-search-box) +After you click that button, a modal displays a +[branch filter search box](../repository/branches/index.md#branch-filter-search-box) where you can choose to either: - Cherry-pick the changes directly into the selected branch. @@ -28,12 +29,12 @@ where you can choose to either: > [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2675) in GitLab 12.9. -When you cherry-pick a merge commit, GitLab will output a system note to the related merge -request thread crosslinking the new commit and the existing merge request. +When you cherry-pick a merge commit, GitLab displays a system note to the related merge +request thread. It crosslinks the new commit and the existing merge request. ![Cherry-pick tracking in Merge Request timeline](img/cherry_pick_mr_timeline_v12_9.png) -Each deployment's [list of associated merge requests](../../../api/deployments.md#list-of-merge-requests-associated-with-a-deployment) will include cherry-picked merge commits. +Each deployment's [list of associated merge requests](../../../api/deployments.md#list-of-merge-requests-associated-with-a-deployment) includes cherry-picked merge commits. NOTE: We only track cherry-pick executed from GitLab (both UI and API). Support for [tracking cherry-picked commits through the command line](https://gitlab.com/gitlab-org/gitlab/-/issues/202215) is planned for a future release. @@ -44,15 +45,15 @@ You can cherry-pick a commit from the commit details page: ![Cherry-pick commit](img/cherry_pick_changes_commit.png) -Similar to cherry-picking a merge request, you can opt to cherry-pick the changes +Similar to cherry-picking a merge request, you can cherry-pick the changes directly into the target branch or create a new merge request to cherry-pick the changes. -Please note that when cherry-picking merge commits, the mainline will always be the -first parent. If you want to use a different mainline then you need to do that +When cherry-picking merge commits, the mainline is always the +first parent. If you want to use a different mainline, you need to do that from the command line. -Here is a quick example to cherry-pick a merge commit using the second parent as the +Here's a quick example to cherry-pick a merge commit using the second parent as the mainline: ```shell diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md index 71bdd92cb06..41cc0506a3f 100644 --- a/doc/user/project/merge_requests/merge_request_approvals.md +++ b/doc/user/project/merge_requests/merge_request_approvals.md @@ -10,8 +10,8 @@ type: reference, concepts > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/580) in GitLab Enterprise Edition 7.2. Available in GitLab Core and higher tiers. > - Redesign [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/1979) in [GitLab Premium](https://about.gitlab.com/pricing/) 11.8 and [feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/10685) in 12.0. -Code review is an essential practice of every successful project, and giving your -approval once a merge request is in good shape is an important part of the review +Code review is an essential practice of every successful project. Approving a +merge request is an important part of the review process, as it clearly communicates the ability to merge the change. ## Optional Approvals @@ -19,8 +19,8 @@ process, as it clearly communicates the ability to merge the change. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/27426) in GitLab 13.2. Any user with Developer or greater [permissions](../../permissions.md) can approve a merge request in GitLab Core and higher tiers. -This provides a consistent mechanism for reviewers to approve merge requests, and makes it easy for -maintainers to know when a change is ready to merge. Approvals in Core are optional and do +This provides a consistent mechanism for reviewers to approve merge requests, and ensures +maintainers know a change is ready to merge. Approvals in Core are optional, and do not prevent a merge request from being merged when there is no approval. ## Required Approvals **(STARTER)** @@ -50,8 +50,9 @@ be merged, and optionally which users should do the approving. Approvals can be - [As project defaults](#adding--editing-a-default-approval-rule). - [Per merge request](#editing--overriding-approval-rules-per-merge-request). -If no approval rules are defined, any user can approve a merge request, though the default -minimum number of required approvers can still be set in the [project settings for merge request approvals](#merge-request-approvals-project-settings). +If no approval rules are defined, any user can approve a merge request. However, the default +minimum number of required approvers can still be set in the +[project settings for merge request approvals](#merge-request-approvals-project-settings). You can opt to define one single rule to approve a merge request among the available rules or choose more than one with [multiple approval rules](#multiple-approval-rules). @@ -81,20 +82,21 @@ A group of users can also be added as approvers. In the future, group approvers [restricted to only groups with share access to the project](https://gitlab.com/gitlab-org/gitlab/-/issues/2048). If a user is added as an individual approver and is also part of a group approver, -then that user is just counted once. The merge request author, as well as users who have committed +then that user is just counted once. The merge request author, and users who have committed to the merge request, do not count as eligible approvers, if [**Prevent author approval**](#allowing-merge-request-authors-to-approve-their-own-merge-requests) (enabled by default) and [**Prevent committers approval**](#prevent-approval-of-merge-requests-by-their-committers) (disabled by default) are enabled on the project settings. -When an eligible approver comments on a merge request, it appears in the **Commented by** column of the Approvals widget, -indicating who has engaged in the merge request review. Authors and reviewers can also easily identify who they should reach out -to if they have any questions or inputs about the content of the merge request. +When an eligible approver comments on a merge request, it displays in the +**Commented by** column of the Approvals widget. It indicates who participated in +the merge request review. Authors and reviewers can also identify who they should reach out +to if they have any questions about the content of the merge request. ##### Implicit Approvers If the number of required approvals is greater than the number of assigned approvers, -approvals from other users will count towards meeting the requirement. These would be +approvals from other users counts towards meeting the requirement. These would be users with developer [permissions](../../permissions.md) or higher in the project who were not explicitly listed in the approval rules. @@ -103,7 +105,7 @@ were not explicitly listed in the approval rules. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/7933) in [GitLab Starter](https://about.gitlab.com/pricing/) 11.5. If you add [Code Owners](../code_owners.md) to your repository, the owners to the -corresponding files will become eligible approvers, together with members with Developer +corresponding files become eligible approvers, together with members with Developer or higher [permissions](../../permissions.md). To enable this merge request approval rule: @@ -115,7 +117,7 @@ To enable this merge request approval rule: ![MR approvals by Code Owners](img/mr_approvals_by_code_owners_v12_7.png) Once set, merge requests can only be merged once approved by the -number of approvals you've set. GitLab will accept approvals from +number of approvals you've set. GitLab accepts approvals from users with Developer or higher permissions, as well as by Code Owners, indistinguishably. @@ -154,27 +156,27 @@ To add or edit the default merge request approval rule: 1. Click **Add approval rule**, or **Edit**. - Add or change the **Rule name**. - Set the number of required approvals in **Approvals required**. The minimum value is `0`. - - (Optional) Search for users or groups that will be [eligible to approve](#eligible-approvers) + - (Optional) Search for users or groups that are [eligible to approve](#eligible-approvers) merge requests and click the **Add** button to add them as approvers. Before typing - in the search field, approvers will be suggested based on the previous authors of + in the search field, approvers are suggested based on the previous authors of the files being changed by the merge request. - (Optional) Click the **{remove}** **Remove** button next to a group or user to delete it from the rule. 1. Click **Add approval rule** or **Update approval rule**. When [approval rule overrides](#prevent-overriding-default-approvals) are allowed, -changes to these default rules will **not** be applied to existing merge +changes to these default rules are not applied to existing merge requests, except for changes to the [target branch](#scoped-to-protected-branch) of the rule. When approval rule overrides are not allowed, all changes to these default rules -will be applied to existing merge requests. Any approval rules that had previously been +are applied to existing merge requests. Any approval rules that had previously been manually [overridden](#editing--overriding-approval-rules-per-merge-request) during a -period when approval rule overrides where allowed, will not be modified. +period when approval rule overrides where allowed, are not modified. NOTE: If a merge request targets a different project, such as from a fork to the upstream project, -the default approval rules will be taken from the target (upstream) project, not the +the default approval rules are taken from the target (upstream) project, not the source (fork). ##### Editing / overriding approval rules per merge request @@ -193,8 +195,8 @@ the same steps as [Adding / editing a default approval rule](#adding--editing-a- #### Set up an optional approval rule -MR approvals can be configured to be optional. -This can be useful if you're working on a team where approvals are appreciated, but not required. +MR approvals can be configured to be optional, which can help if you're working +on a team where approvals are appreciated, but not required. To configure an approval to be optional, set the number of required approvals in **Approvals required** to `0`. @@ -209,16 +211,16 @@ as well as multiple default approval rules per project. Adding or editing multiple default rules is identical to [adding or editing a single default approval rule](#adding--editing-a-default-approval-rule), -except the **Add approval rule** button will be available to add more rules, even after +except the **Add approval rule** button is available to add more rules, even after a rule is already defined. Similarly, editing or overriding multiple approval rules per merge request is identical to [editing or overriding approval rules per merge request](#editing--overriding-approval-rules-per-merge-request), -except the **Add approval rule** button will be available to add more rules, even after +except the **Add approval rule** button is available to add more rules, even after a rule is already defined. -When an [eligible approver](#eligible-approvers) approves a merge request, it will -reduce the number of approvals left for all rules that the approver belongs to. +When an [eligible approver](#eligible-approvers) approves a merge request, it +reduces the number of approvals left for all rules that the approver belongs to. ![Approvals premium merge request widget](img/approvals_premium_mr_widget_v13_3.png) @@ -267,7 +269,7 @@ The merge request author is not allowed to approve their own merge request if [**Prevent author approval**](#allowing-merge-request-authors-to-approve-their-own-merge-requests) is enabled in the project settings. -Once the approval rules have been met, the merge request can be merged if there is nothing +After the approval rules have been met, the merge request can be merged if there is nothing else blocking it. Note that the merge request could still be blocked by other conditions, such as merge conflicts, [pending discussions](../../discussions/index.md#only-allow-merge-requests-to-be-merged-if-all-threads-are-resolved), or a [failed CI/CD pipeline](merge_when_pipeline_succeeds.md). @@ -289,7 +291,7 @@ To prevent that from happening: #### Resetting approvals on push You can force all approvals on a merge request to be removed when new commits are -pushed to the source branch of the merge request. If disabled, approvals will persist +pushed to the source branch of the merge request. If disabled, approvals persist even if there are changes added to the merge request. To enable this feature: 1. Check the **Require new approvals when new commits are added to an MR.** @@ -298,7 +300,7 @@ even if there are changes added to the merge request. To enable this feature: NOTE: Approvals do not get reset when [rebasing a merge request](fast_forward_merge.md) -from the UI. However, approvals will be reset if the target branch is changed. +from the UI. However, approvals are reset if the target branch is changed. #### Allowing merge request authors to approve their own merge requests @@ -330,7 +332,7 @@ enable this feature: NOTE: To require authentication when approving a merge request, you must enable **Password authentication enabled for web interface** under [sign-in restrictions](../../admin_area/settings/sign_in_restrictions.md#password-authentication-enabled). -in the Admin area. +in the Admin Area. You can force the approver to enter a password in order to authenticate before adding the approval. This enables an Electronic Signature for approvals such as the one defined diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md index 99e70f35d6d..b2aedb7d95f 100644 --- a/doc/user/project/merge_requests/resolve_conflicts.md +++ b/doc/user/project/merge_requests/resolve_conflicts.md @@ -10,13 +10,13 @@ type: reference, concepts Merge conflicts occur when two branches have different changes that cannot be merged automatically. -Git is able to automatically merge changes between branches in most cases, but -there are situations where Git will require your assistance to resolve the +Git can merge changes between branches in most cases, but +occasionally Git requires your assistance to resolve the conflicts manually. Typically, this is necessary when people change the same parts of the same files. -GitLab will prevent merge requests from being merged until all conflicts are -resolved. Conflicts can be resolved locally, or in many cases within GitLab +GitLab prevents merge requests from being merged until all conflicts are +resolved. Conflicts can be resolved locally, or in many cases in GitLab (see [conflicts available for resolution](#conflicts-available-for-resolution) for information on when this is available). @@ -24,35 +24,30 @@ for information on when this is available). NOTE: GitLab resolves conflicts by creating a merge commit in the source branch that -is not automatically merged into the target branch. This allows the merge -commit to be reviewed and tested before the changes are merged, preventing +is not automatically merged into the target branch. The merge +commit can be reviewed and tested before the changes are merged. This prevents unintended changes entering the target branch without review or breaking the build. ## Resolve conflicts: interactive mode -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5479) in GitLab 8.11. - -Clicking this will show a list of files with conflicts, with conflict sections +Clicking **Resolve Conflicts** displays a list of files with conflicts, with conflict sections highlighted: ![Conflict section](img/conflict_section.png) -Once all conflicts have been marked as using 'ours' or 'theirs', the conflict -can be resolved. This will perform a merge of the target branch of the merge -request into the source branch, resolving the conflicts using the options +After all conflicts have been marked as using 'ours' or 'theirs', the conflict +can be resolved. Resolving conflicts merges the target branch of the merge +request into the source branch, using the options chosen. If the source branch is `feature` and the target branch is `master`, this is similar to performing `git checkout feature; git merge master` locally. ## Resolve conflicts: inline editor -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/6374) in GitLab 8.13. - -The merge conflict resolution editor allows for more complex merge conflicts, -which require the user to manually modify a file in order to resolve a conflict, -to be solved right form the GitLab interface. Use the **Edit inline** button -to open the editor. Once you're sure about your changes, hit the -**Commit to source branch** button. +Some merge conflicts are more complex, requiring you to manually modify a file to +resolve them. Use the merge conflict resolution editor to resolve complex +conflicts in the GitLab interface. Click **Edit inline** to open the editor. +After you're sure about your changes, click **Commit to source branch**. ![Merge conflict editor](img/merge_conflict_editor.png) @@ -66,13 +61,16 @@ GitLab allows resolving conflicts in a file where all of the below are true: - The file, with conflict markers added, is not over 200 KB in size - The file exists under the same path in both branches -If any file with conflicts in that merge request does not meet all of these -criteria, the conflicts for that merge request cannot be resolved in the UI. +If any file in your merge request containing conflicts can't meet all of these +criteria, you can't resolve the merge conflict in the UI. Additionally, GitLab does not detect conflicts in renames away from a path. For -example, this will not create a conflict: on branch `a`, doing `git mv file1 -file2`; on branch `b`, doing `git mv file1 file3`. Instead, both files will be -present in the branch after the merge request is merged. +example, this does not create a conflict: + +1. On branch `a`, doing `git mv file1 file2` +1. On branch `b`, doing `git mv file1 file3`. + +Instead, both files are present in the branch after the merge request is merged. <!-- ## Troubleshooting diff --git a/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md b/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md index 893a917d31f..b611a4f850c 100644 --- a/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md +++ b/doc/user/project/merge_requests/reviewing_and_managing_merge_requests.md @@ -13,10 +13,10 @@ which is then reviewed, and accepted (or rejected). ## View project merge requests -View all the merge requests within a project by navigating to **Project > Merge Requests**. +View all the merge requests in a project by navigating to **Project > Merge Requests**. -When you access your project's merge requests, GitLab will present them in a list, -and you can use the tabs available to quickly filter by open and closed. You can also [search and filter the results](../../search/index.md#filtering-issue-and-merge-request-lists). +When you access your project's merge requests, GitLab displays them in a list. +Use the tabs to quickly filter by open and closed. You can also [search and filter the results](../../search/index.md#filtering-issue-and-merge-request-lists). ![Project merge requests list view](img/project_merge_requests_list_view_v13_5.png) @@ -32,7 +32,7 @@ You can [search and filter the results](../../search/index.md#filtering-issue-an A merge commit is created for every merge, but the branch is only merged if a fast-forward merge is possible. This ensures that if the merge request build -succeeded, the target branch build will also succeed after merging. +succeeded, the target branch build also succeeds after the merge. Navigate to a project's settings, select the **Merge commit with semi-linear history** option under **Merge Requests: Merge method** and save your changes. @@ -80,12 +80,15 @@ Click **Expand file** on any file to view the changes for that file. > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/222790) in GitLab 13.2. > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/229848) in GitLab 13.7. -For larger merge requests it might sometimes be useful to review single files at a time. To enable, -from your avatar on the top-right navigation bar, click **Settings**, and go to **Preferences** on the left -sidebar. Scroll down to the **Behavior** section and select **Show one file at a time on merge request's Changes tab**. -Click **Save changes** to apply. +For larger merge requests, consider reviewing one file at a time. To enable this feature: -From there, when reviewing merge requests' **Changes** tab, you will see only one file at a time. You can then click the buttons **Prev** and **Next** to view the other files changed. +1. In the top right corner of the navigation bar, click your user avatar. +1. Click **Settings**. +1. In the left sidebar, go to **Preferences**. +1. Scroll to the **Behavior** section and select **Show one file at a time on merge request's Changes tab**. +1. Click **Save changes** to apply. + +After you enable this setting, GitLab displays only one file at a time in the **Changes** tab when you review merge requests. You can click **Prev** and **Next** to view other changed files. ![File-by-file diff navigation](img/file_by_file_v13_2.png) @@ -104,10 +107,14 @@ browser's cookies or change this behavior again. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/18140) in GitLab 13.0. -To seamlessly navigate among commits in a merge request, from the **Commits** tab, click one of -the commits to open the single-commit view. From there, you can navigate among the commits -by clicking the **Prev** and **Next** buttons on the top-right of the page or by using the -<kbd>X</kbd> and <kbd>C</kbd> keyboard shortcuts. +To seamlessly navigate among commits in a merge request: + +1. Click the **Commits** tab. +1. Click a commit to open it in the single-commit view. +1. Navigate through the commits by either: + + - Clicking **Prev** and **Next** buttons on the top-right of the page. + - Using the <kbd>X</kbd> and <kbd>C</kbd> keyboard shortcuts. ![Merge requests commit navigation](img/commit_nav_v13_4.png) @@ -120,7 +127,7 @@ to expand the entire file. ![Incrementally expand merge request diffs](img/incrementally_expand_merge_request_diffs_v12_2.png) -[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/205401) in GitLab 13.1, when viewing a +In GitLab [versions 13.1 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/205401), when viewing a merge request's **Changes** tab, if a certain file was only renamed, you can expand it to see the entire content by clicking **Show file contents**. @@ -145,7 +152,7 @@ whitespace changes. > - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-file-views). **(FREE SELF)** When reviewing a merge request with many files multiple times, it may be useful to the reviewer -to focus on new changes and ignore the files that they have already reviewed and don't want to +to focus on new changes and ignore the files that they have already reviewed and don't want to see anymore unless they are changed again. To mark a file as viewed: @@ -154,7 +161,7 @@ To mark a file as viewed: 1. On the right-top of the file, locate the **Viewed** checkbox. 1. Check it to mark the file as viewed. -Once checked, the file will remain marked for that reviewer unless there are newly introduced +Once checked, the file remains marked for that reviewer unless there are newly introduced changes to its content or the checkbox is unchecked. ### Enable or disable file views **(FREE SELF)** @@ -180,8 +187,9 @@ Feature.disable(:local_file_reviews) > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/13950) in GitLab 11.5. -GitLab provides a way of leaving comments in any part of the file being changed -in a Merge Request. To do so, click the **{comment}** **comment** icon in the gutter of the Merge Request diff UI to expand the diff lines and leave a comment, just as you would for a changed line. +In a merge request, you can leave comments in any part of the file being changed. +In the Merge Request Diff UI, click the **{comment}** **comment** icon in the gutter +to expand the diff lines and leave a comment, just as you would for a changed line. ![Comment on any diff file line](img/comment-on-any-diff-line.png) @@ -231,31 +239,30 @@ Feature.enable(:multiline_comments) ## Pipeline status in merge requests widgets If you've set up [GitLab CI/CD](../../../ci/README.md) in your project, -you will be able to see: +you can see: - Both pre-merge and post-merge pipelines and the environment information if any. - Which deployments are in progress. -If there's an [environment](../../../ci/environments/index.md) and the application is -successfully deployed to it, the deployed environment and the link to the -Review App will be shown as well. +If an application is successfully deployed to an +[environment](../../../ci/environments/index.md), the deployed environment and the link to the +Review App are both shown. NOTE: -When the default branch (for example, `main`) is red due to a failed CI pipeline, the `merge` button -When the pipeline fails in a merge request but it can be merged nonetheless, -the **Merge** button will be colored in red. +When the pipeline fails in a merge request but it can still be merged, +the **Merge** button is colored red. ### Post-merge pipeline status When a merge request is merged, you can see the post-merge pipeline status of the branch the merge request was merged into. For example, when a merge request -is merged into the master branch and then triggers a deployment to the staging +is merged into the `master` branch and then triggers a deployment to the staging environment. -Deployments that are ongoing will be shown, as well as the deploying/deployed state +Ongoing deployments are shown, and the state (deploying or deployed) for environments. If it's the first time the branch is deployed, the link -will return a `404` error until done. During the deployment, the stop button will -be disabled. If the pipeline fails to deploy, the deployment information will be hidden. +returns a `404` error until done. During the deployment, the stop button is +disabled. If the pipeline fails to deploy, the deployment information is hidden. ![Merge request pipeline](img/merge_request_pipeline.png) @@ -263,14 +270,15 @@ For more information, [read about pipelines](../../../ci/pipelines/index.md). ### Merge when pipeline succeeds (MWPS) -Set a merge request that looks ready to merge to [merge automatically when CI pipeline succeeds](merge_when_pipeline_succeeds.md). +Set a merge request that looks ready to merge to +[merge automatically when CI pipeline succeeds](merge_when_pipeline_succeeds.md). ### Live preview with Review Apps If you configured [Review Apps](https://about.gitlab.com/stages-devops-lifecycle/review-apps/) for your project, -you can preview the changes submitted to a feature-branch through a merge request -in a per-branch basis. No need to checkout the branch, install and preview locally; -all your changes will be available to preview by anyone with the Review Apps link. +you can preview the changes submitted to a feature branch through a merge request +on a per-branch basis. You don't need to checkout the branch, install, and preview locally. +All your changes are available to preview by anyone with the Review Apps link. With GitLab [Route Maps](../../../ci/review_apps/index.md#route-maps) set, the merge request widget takes you directly to the pages changed, making it easier and @@ -280,21 +288,26 @@ faster to preview proposed modifications. ## Associated features -There is also a large number of features to associated to merge requests: - -| Feature | Description | -|-------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------| -| [Bulk editing merge requests](../../project/bulk_editing.md) | Update the attributes of multiple merge requests simultaneously. | -| [Cherry-pick changes](cherry_pick_changes.md) | Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button in a merged merge requests or a commit. | -| [Fast-forward merge requests](fast_forward_merge.md) | For a linear Git history and a way to accept merge requests without creating merge commits | -| [Find the merge request that introduced a change](versions.md) | When viewing the commit details page, GitLab will link to the merge request(s) containing that commit. | -| [Merge requests versions](versions.md) | Select and compare the different versions of merge request diffs | -| [Resolve conflicts](resolve_conflicts.md) | GitLab can provide the option to resolve certain merge request conflicts in the GitLab UI. | -| [Revert changes](revert_changes.md) | Revert changes from any commit from within a merge request. | +These features are associated with merge requests: + +- [Bulk editing merge requests](../../project/bulk_editing.md): + Update the attributes of multiple merge requests simultaneously. +- [Cherry-pick changes](cherry_pick_changes.md): + Cherry-pick any commit in the UI by clicking the **Cherry-pick** button in a merged merge requests or a commit. +- [Fast-forward merge requests](fast_forward_merge.md): + For a linear Git history and a way to accept merge requests without creating merge commits +- [Find the merge request that introduced a change](versions.md): + When viewing the commit details page, GitLab links to the merge request(s) containing that commit. +- [Merge requests versions](versions.md): + Select and compare the different versions of merge request diffs +- [Resolve conflicts](resolve_conflicts.md): + GitLab can provide the option to resolve certain merge request conflicts in the GitLab UI. +- [Revert changes](revert_changes.md): + Revert changes from any commit from a merge request. ## Troubleshooting -Sometimes things don't go as expected in a merge request, here are some +Sometimes things don't go as expected in a merge request. Here are some troubleshooting steps. ### Merge request cannot retrieve the pipeline status @@ -304,7 +317,7 @@ This can occur if Sidekiq doesn't pick up the changes fast enough. #### Sidekiq Sidekiq didn't process the CI state change fast enough. Please wait a few -seconds and the status will update automatically. +seconds and the status should update automatically. #### Bug @@ -320,12 +333,9 @@ Merge Request again. ## Tips -Here are some tips that will help you be more efficient with merge requests in +Here are some tips to help you be more efficient with merge requests in the command line. -NOTE: -This section might move in its own document in the future. - ### Copy the branch name for local checkout > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/23767) in GitLab 13.4. @@ -334,7 +344,7 @@ The merge request sidebar contains the branch reference for the source branch used to contribute changes for this merge request. To copy the branch reference into your clipboard, click the **Copy branch name** button -(**{copy-to-clipboard}**) in the right sidebar. You can then use it to checkout the branch locally +(**{copy-to-clipboard}**) in the right sidebar. Use it to checkout the branch locally via command line by running `git checkout <branch-name>`. ### Checkout merge requests locally through the `head` ref @@ -343,7 +353,7 @@ A merge request contains all the history from a repository, plus the additional commits added to the branch associated with the merge request. Here's a few ways to checkout a merge request locally. -Please note that you can checkout a merge request locally even if the source +You can checkout a merge request locally even if the source project is a fork (even a private fork) of the target project. This relies on the merge request `head` ref (`refs/merge-requests/:iid/head`) @@ -352,10 +362,10 @@ request via its ID instead of its branch. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/223156) in GitLab 13.4, 14 days after a merge request gets closed or merged, the merge request -`head` ref will be deleted. This means that the merge request will not be available +`head` ref is deleted. This means that the merge request is not available for local checkout via the merge request `head` ref anymore. The merge request -can still be re-opened. Also, as long as the merge request's branch -exists, you can still check out the branch as it won't be affected. +can still be re-opened. If the merge request's branch +exists, you can still check out the branch, as it isn't affected. #### Checkout locally by adding a Git alias @@ -374,7 +384,7 @@ from the `origin` remote, do: git mr origin 5 ``` -This will fetch the merge request into a local `mr-origin-5` branch and check +This fetches the merge request into a local `mr-origin-5` branch and check it out. #### Checkout locally by modifying `.git/config` for a given repository diff --git a/doc/user/project/merge_requests/work_in_progress_merge_requests.md b/doc/user/project/merge_requests/work_in_progress_merge_requests.md index 3d84be802ec..43ab03114fa 100644 --- a/doc/user/project/merge_requests/work_in_progress_merge_requests.md +++ b/doc/user/project/merge_requests/work_in_progress_merge_requests.md @@ -9,8 +9,8 @@ type: reference, concepts If a merge request is not yet ready to be merged, perhaps due to continued development or open threads, you can prevent it from being accepted before it's ready by flagging -it as a **Draft**. This will disable the "Merge" button, preventing it from -being merged, and it will stay disabled until the "Draft" flag has been removed. +it as a **Draft**. This disables the **Merge** button, preventing it from +being merged. It stays disabled until the **Draft** flag has been removed. ![Blocked Merge Button](img/draft_blocked_merge_button_v13_2.png) @@ -22,7 +22,7 @@ To run pipelines for merged results, you must [remove the draft status](#removin ## Adding the "Draft" flag to a merge request -> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32692) in GitLab 13.2, Work-In-Progress (WIP) merge requests were renamed to **Draft**. Support for using **WIP** will be removed in GitLab 14.0. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/32692) in GitLab 13.2, Work-In-Progress (WIP) merge requests were renamed to **Draft**. Support for using **WIP** is scheduled for removal in GitLab 14.0. > - **Mark as draft** and **Mark as ready** buttons [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/227421) in GitLab 13.5. There are several ways to flag a merge request as a Draft: @@ -30,15 +30,15 @@ There are several ways to flag a merge request as a Draft: - Click the **Mark as draft** button on the top-right corner of the merge request's page. - Add `[Draft]`, `Draft:` or `(Draft)` to the start of the merge request's title. Clicking on **Start the title with Draft:**, under the title box, when editing the merge request's - description will have the same effect. + description has the same effect. - **Deprecated** Add `[WIP]` or `WIP:` to the start of the merge request's title. - **WIP** still works but was deprecated in favor of **Draft**. It will be removed in the next major version (GitLab 14.0). + **WIP** still works but was deprecated in favor of **Draft**. It is scheduled for removal in the next major version (GitLab 14.0). - Add the `/draft` (or `/wip`) [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics) in a comment in the merge request. This is a toggle, and can be repeated - to change the status back. Note that any other text in the comment will be discarded. + to change the status back. Note that any other text in the comment is discarded. - Add `draft:`, `Draft:`, `fixup!`, or `Fixup!` to the beginning of a commit message targeting the merge request's source branch. This is not a toggle, and doing it again in another - commit will have no effect. + commit has no effect. ## Removing the "Draft" flag from a merge request @@ -48,10 +48,10 @@ Similar to above, when a Merge Request is ready to be merged, you can remove the - Click the **Mark as ready** button on the top-right corner of the merge request's page. - Remove `[Draft]`, `Draft:` or `(Draft)` from the start of the merge request's title. Clicking on **Remove the Draft: prefix from the title**, under the title box, when editing the merge - request's description, will have the same effect. + request's description, has the same effect. - Add the `/draft` (or `/wip`) [quick action](../quick_actions.md#quick-actions-for-issues-merge-requests-and-epics) in a comment in the merge request. This is a toggle, and can be repeated - to change the status back. Note that any other text in the comment will be discarded. + to change the status back. Note that any other text in the comment is discarded. - Click on the **Resolve Draft status** button near the bottom of the merge request description, next to the **Merge** button (see [image above](#draft-merge-requests)). Must have at least Developer level permissions on the project for the button to @@ -60,8 +60,8 @@ Similar to above, when a Merge Request is ready to be merged, you can remove the ## Including/excluding WIP merge requests when searching When viewing/searching the merge requests list, you can choose to include or exclude -WIP merge requests by adding a "WIP" filter in the search box, and choosing "Yes" -(to include) or "No" (to exclude). +WIP merge requests. Add a **WIP** filter in the search box, and choose **Yes** +to include, or **No** to exclude. ![Filter WIP MRs](img/filter_wip_merge_requests.png) diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md index 5754a3b7a9d..ede130513bc 100644 --- a/doc/user/project/protected_branches.md +++ b/doc/user/project/protected_branches.md @@ -13,7 +13,7 @@ further restrictions on certain branches, they can be protected. ## Overview -By default, a protected branch does four simple things: +By default, a protected branch does these things: - It prevents its creation, if not already created, from everybody except users with Maintainer permission. @@ -30,49 +30,47 @@ The default branch protection level is set in the [Admin Area](../admin_area/set ## Configuring protected branches -To protect a branch, you need to have at least Maintainer permission level. Note -that the `master` branch is protected by default. +To protect a branch, you need to have at least Maintainer permission level. +The `master` branch is protected by default. -1. Navigate to your project's **Settings ➔ Repository** +1. In your project, go to **Settings > Repository**. 1. Scroll to find the **Protected branches** section. 1. From the **Branch** dropdown menu, select the branch you want to protect and click **Protect**. In the screenshot below, we chose the `develop` branch. ![Protected branches page](img/protected_branches_page_v12_3.png) -1. Once done, the protected branch will appear in the "Protected branches" list. +1. Once done, the protected branch displays in the **Protected branches** list. ![Protected branches list](img/protected_branches_list_v12_3.png) ## Using the Allowed to merge and Allowed to push settings -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5081) in GitLab 8.11. - In GitLab 8.11 and later, we added another layer of branch protection which provides -more granular management of protected branches. The "Developers can push" -option was replaced by an "Allowed to push" setting which can be set to -allow/prohibit Maintainers and/or Developers to push to a protected branch. +more granular management of protected branches. The **Developers can push** +option was replaced by **Allowed to push**. You can set this value to allow +or prohibit Maintainers and/or Developers to push to a protected branch. -Using the "Allowed to push" and "Allowed to merge" settings, you can control +Using the **Allowed to push** and **Allowed to merge** settings, you can control the actions that different roles can perform with the protected branch. -For example, you could set "Allowed to push" to "No one", and "Allowed to merge" -to "Developers + Maintainers", to require _everyone_ to submit a merge request for +For example, you could set **Allowed to push** to "No one", and **Allowed to merge** +to "Developers + Maintainers", to require everyone to submit a merge request for changes going into the protected branch. This is compatible with workflows like the [GitLab workflow](../../topics/gitlab_flow.md). However, there are workflows where that is not needed, and only protecting from force pushes and branch removal is useful. For those workflows, you can allow everyone with write access to push to a protected branch by setting -"Allowed to push" to "Developers + Maintainers". +**Allowed to push** to "Developers + Maintainers". -You can set the "Allowed to push" and "Allowed to merge" options while creating +You can set the **Allowed to push** and **Allowed to merge** options while creating a protected branch or afterwards by selecting the option you want from the -dropdown list in the "Already protected" area. +dropdown list in the **Already protected** area. ![Developers can push](img/protected_branches_devs_can_push_v12_3.png) If you don't choose any of those options while creating a protected branch, -they are set to "Maintainers" by default. +they are set to Maintainers by default. ### Allow Deploy Keys to push to a protected branch @@ -88,7 +86,7 @@ Deploy keys can be selected in the **Allowed to push** dropdown when: - Defining a protected branch. - Updating an existing branch. -Select a deploy key to allow the owner of the key to push to the chosen protected branch, +Select a deploy key to allow the key's owner to push to the chosen protected branch, even if they aren't a member of the related project. The owner of the selected deploy key must have at least read access to the given project. @@ -103,24 +101,20 @@ Deploy Keys are not available in the **Allowed to merge** dropdown. ## Restricting push and merge access to certain users **(STARTER)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5081) in [GitLab Starter](https://about.gitlab.com/pricing/) 8.11. - With GitLab Enterprise Edition you can restrict access to protected branches -by choosing a role (Maintainers, Developers) as well as certain users. From the +by choosing a role (Maintainers, Developers) and certain users. From the dropdown menu select the role and/or the users you want to have merge or push access. ![Select roles and users](img/protected_branches_select_roles_and_users.png) -Click **Protect** and the branch will appear in the "Protected branch" list. +Click **Protect** and the branch displays in the **Protected branch** list. ![Roles and users list](img/protected_branches_select_roles_and_users_list.png) ## Wildcard protected branches -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4665) in GitLab 8.10. - -You can specify a wildcard protected branch, which will protect all branches +You can specify a wildcard protected branch, which protects all branches matching the wildcard. For example: | Wildcard Protected Branch | Matching Branches | @@ -129,15 +123,15 @@ matching the wildcard. For example: | `production/*` | `production/app-server`, `production/load-balancer` | | `*gitlab*` | `gitlab`, `gitlab/staging`, `master/gitlab/production` | -Protected branch settings (like "Developers can push") apply to all matching +Protected branch settings, like **Developers can push**, apply to all matching branches. Two different wildcards can potentially match the same branch. For example, `*-stable` and `production-*` would both match a `production-stable` branch. In that case, if _any_ of these protected branches have a setting like -"Allowed to push", then `production-stable` will also inherit this setting. +"Allowed to push", then `production-stable` also inherit this setting. -If you click on a protected branch's name, you will be presented with a list of +If you click on a protected branch's name, GitLab displays a list of all matching branches: ![Protected branch matches](img/protected_branches_matches.png) @@ -151,41 +145,36 @@ When a protected branch or wildcard protected branches are set to Developers (and users with higher [permission levels](../permissions.md)) are allowed to create a new protected branch as long as they are [**Allowed to merge**](#using-the-allowed-to-merge-and-allowed-to-push-settings). -This can only be done via the UI or through the API (to avoid creating protected -branches accidentally from the command line or from a Git client application). +This can only be done by using the UI or through the API, to avoid creating protected +branches accidentally from the command line or from a Git client application. To create a new branch through the user interface: -1. Visit **Repository > Branches**. +1. Go to **Repository > Branches**. 1. Click on **New branch**. -1. Fill in the branch name and select an existing branch, tag, or commit that - the new branch will be based off. Only existing protected branches and commits - that are already in protected branches will be accepted. +1. Fill in the branch name and select an existing branch, tag, or commit to + base the new branch on. Only existing protected branches and commits + that are already in protected branches are accepted. ## Deleting a protected branch -> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/21393) in GitLab 9.3. - -From time to time, it may be required to delete or clean up branches that are -protected. +From time to time, you may need to delete or clean up protected branches. +User with [Maintainer permissions](../permissions.md) and greater can manually delete protected +branches by using the GitLab web interface: -User with [Maintainer permissions](../permissions.md) and up can manually delete protected -branches via the GitLab web interface: - -1. Visit **Repository > Branches** -1. Click on the delete icon next to the branch you wish to delete -1. In order to prevent accidental deletion, an additional confirmation is - required +1. Go to **Repository > Branches**. +1. Click on the delete icon next to the branch you wish to delete. +1. To prevent accidental deletion, an additional confirmation is required. ![Delete protected branches](img/protected_branches_delete.png) -Deleting a protected branch is only allowed via the web interface, not via Git. +Deleting a protected branch is allowed only by using the web interface; not from Git. This means that you can't accidentally delete a protected branch from your command line or a Git client application. ## Protected Branches approval by Code Owners **(PREMIUM)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13251) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.4. +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13251) in GitLab Premium 12.4. It is possible to require at least one approval by a [Code Owner](code_owners.md) to a file changed by the @@ -208,7 +197,7 @@ To enable Code Owner's approval to branches already protected: ![Code Owners approval - branch already protected](img/code_owners_approval_protected_branch_v12_4.png) -When enabled, all merge requests targeting these branches will require approval +When enabled, all merge requests targeting these branches require approval by a Code Owner per matched rule before they can be merged. Additionally, direct pushes to the protected branch are denied if a rule is matched. @@ -224,26 +213,9 @@ for details about the pipelines security model. ## Changelog -**13.5** - -- [Allow Deploy keys to push to protected branches once more](https://gitlab.com/gitlab-org/gitlab/-/issues/30769). - -**11.9** - -- [Allow protected branches to be created](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53361) by Developers (and users with higher permission levels) through the API and the user interface. - -**9.2** - -- Allow deletion of protected branches via the web interface ([issue #21393](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/21393)). - -**8.11** - -- Allow creating protected branches that can't be pushed to ([merge request !5081](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5081)). - -**8.10** - -- Allow developers without push access to merge into a protected branch ([merge request !4892](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4892)). -- Allow specifying protected branches using wildcards ([merge request !4665](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/4665)). +- **13.5**: [Allow Deploy keys to push to protected branches once more](https://gitlab.com/gitlab-org/gitlab/-/issues/30769). +- **11.9**: [Allow protected branches to be created](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/53361) + by Developers (and users with higher permission levels) through the API and the user interface. <!-- ## Troubleshooting diff --git a/doc/user/project/protected_tags.md b/doc/user/project/protected_tags.md index 7e09c526312..a6f2d645198 100644 --- a/doc/user/project/protected_tags.md +++ b/doc/user/project/protected_tags.md @@ -15,11 +15,13 @@ This feature evolved out of [protected branches](protected_branches.md) ## Overview -Protected tags will prevent anyone from updating or deleting the tag, and will prevent creation of matching tags based on the permissions you have selected. By default, anyone without Maintainer permission will be prevented from creating tags. +Protected tags prevent anyone from updating or deleting the tag, and prevent +creation of matching tags based on the permissions you have selected. By default, +anyone without Maintainer [permissions](../permissions.md) is prevented from creating tags. ## Configuring protected tags -To protect a tag, you need to have at least Maintainer permission level. +To protect a tag, you need to have at least Maintainer [permissions](../permissions.md). 1. Navigate to the project's **Settings > Repository**: @@ -29,17 +31,18 @@ To protect a tag, you need to have at least Maintainer permission level. ![Protected tags page](img/protected_tags_page_v12_3.png) -1. From the **Allowed to create** dropdown, select who will have permission to create matching tags and then click **Protect**: +1. From the **Allowed to create** dropdown, select users with permission to create + matching tags, and click **Protect**: ![Allowed to create tags dropdown](img/protected_tags_permissions_dropdown_v12_3.png) -1. Once done, the protected tag will appear in the **Protected tags** list: +1. After done, the protected tag displays in the **Protected tags** list: ![Protected tags list](img/protected_tags_list_v12_3.png) ## Wildcard protected tags -You can specify a wildcard protected tag, which will protect all tags +You can specify a wildcard protected tag, which protects all tags matching the wildcard. For example: | Wildcard Protected Tag | Matching Tags | @@ -52,9 +55,9 @@ matching the wildcard. For example: Two different wildcards can potentially match the same tag. For example, `*-stable` and `production-*` would both match a `production-stable` tag. In that case, if _any_ of these protected tags have a setting like -**Allowed to create**, then `production-stable` will also inherit this setting. +**Allowed to create**, then `production-stable` also inherit this setting. -If you click on a protected tag's name, you will be presented with a list of +If you click on a protected tag's name, GitLab displays a list of all matching tags: ![Protected tag matches](img/protected_tag_matches.png) diff --git a/doc/user/project/settings/project_access_tokens.md b/doc/user/project/settings/project_access_tokens.md index 68f69f3ac8b..590f549577e 100644 --- a/doc/user/project/settings/project_access_tokens.md +++ b/doc/user/project/settings/project_access_tokens.md @@ -8,7 +8,7 @@ type: reference, howto # Project access tokens NOTE: -Project access tokens are supported for self-managed instances on Core and above. They are also supported on GitLab.com Bronze and above (excluding [trial licenses](https://about.gitlab.com/free-trial/)). +Project access tokens are supported for self-managed instances on Free and above. They are also supported on GitLab SaaS Premium and above (excluding [trial licenses](https://about.gitlab.com/free-trial/)). > - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2587) in GitLab 13.0. > - [Became available on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/235765) in GitLab 13.5 for paid groups only. diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb index 6e5f0dc9a03..c96a1fcbc2c 100644 --- a/lib/gitlab/experimentation.rb +++ b/lib/gitlab/experimentation.rb @@ -109,6 +109,9 @@ module Gitlab }, trial_onboarding_issues: { tracking_category: 'Growth::Conversion::Experiment::TrialOnboardingIssues' + }, + in_product_marketing_emails: { + tracking_category: 'Growth::Activation::Experiment::InProductMarketingEmails' } }.freeze diff --git a/lib/peek/views/external_http.rb b/lib/peek/views/external_http.rb index 7a42fceebc1..bd0e4c64127 100644 --- a/lib/peek/views/external_http.rb +++ b/lib/peek/views/external_http.rb @@ -30,15 +30,9 @@ module Peek end def format_call_details(call) - uri = URI("") - uri.scheme = call[:scheme] - uri.host = call[:host] - uri.port = call[:port] - uri.path = call[:path] - uri.query = call[:query] - + full_path = generate_path(call) super.merge( - label: "#{call[:method]} #{uri}", + label: "#{call[:method]} #{full_path}", code: code(call), proxy: proxy(call), error: error(call) @@ -82,6 +76,22 @@ module Peek nil end end + + def generate_path(call) + uri = URI("") + uri.scheme = call[:scheme] + # The host can be a domain, IPv4 or IPv6. + # Ruby handle IPv6 for us at + # https://github.com/ruby/ruby/blob/v2_6_0/lib/uri/generic.rb#L662 + uri.hostname = call[:host] + uri.port = call[:port] + uri.path = call[:path] + uri.query = call[:query] + + uri.to_s + rescue URI::Error + 'unknown' + end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index bf3ed0beb04..8b6a60b8003 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -200,6 +200,11 @@ msgid_plural "%d fixed test results" msgstr[0] "" msgstr[1] "" +msgid "%d group" +msgid_plural "%d groups" +msgstr[0] "" +msgstr[1] "" + msgid "%d group selected" msgid_plural "%d groups selected" msgstr[0] "" @@ -1331,6 +1336,9 @@ msgstr "" msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}." msgstr "" +msgid "A project’s repository name defines its URL (the one you use to access the project via a browser) and its place on the file disk where GitLab is installed. %{link_start}Learn more.%{link_end}" +msgstr "" + msgid "A ready-to-go template for use with Android apps" msgstr "" @@ -3722,7 +3730,7 @@ msgstr "" msgid "Archived projects" msgstr "" -msgid "Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end}" +msgid "Archiving the project will make it entirely read only. It is hidden from the dashboard and doesn't show up in searches. %{strong_start}The repository cannot be committed to, and no issues, comments, or other entities can be created.%{strong_end} %{link_start}Learn more.%{link_end}" msgstr "" msgid "Are you ABSOLUTELY SURE you wish to delete this project?" @@ -4885,10 +4893,16 @@ msgstr "" msgid "BulkImport|Import groups from GitLab" msgstr "" -msgid "BulkImport|Importing groups from %{link}" +msgid "BulkImport|Importing the group failed" msgstr "" -msgid "BulkImport|Importing the group failed" +msgid "BulkImport|No groups available for import" +msgstr "" + +msgid "BulkImport|Showing %{start}-%{end} of %{total} from %{link}" +msgstr "" + +msgid "BulkImport|Showing %{start}-%{end} of %{total} matching filter \"%{filter}\" from %{link}" msgstr "" msgid "BulkImport|To new group" @@ -9403,7 +9417,7 @@ msgstr "" msgid "Deleting a project places it into a read-only state until %{date}, at which point the project will be permanently deleted. Are you ABSOLUTELY sure?" msgstr "" -msgid "Deleting the project will delete its repository and all related resources including issues, merge requests etc." +msgid "Deleting the project will delete its repository and all related resources including issues, merge requests, etc." msgstr "" msgid "Deletion pending. This project will be removed on %{date}. Repository and other project resources are read-only." @@ -9772,6 +9786,9 @@ msgstr "" msgid "DeploymentFrequencyCharts|Something went wrong while getting deployment frequency data" msgstr "" +msgid "DeploymentFrequencyCharts|These charts display the frequency of deployments to the production environment, as part of the DORA 4 metrics. The environment must be named %{codeStart}production%{codeEnd} for its data to appear in these charts." +msgstr "" + msgid "Deployments" msgstr "" @@ -11880,7 +11897,7 @@ msgstr "" msgid "Export this group with all related data to a new GitLab instance. Once complete, you can import the data file from the \"New Group\" page." msgstr "" -msgid "Export this project with all its related data in order to move your project to a new GitLab instance. Once the export is finished, you can import the file from the \"New Project\" page." +msgid "Export this project with all its related data in order to move it to a new GitLab instance. When the exported file is ready, you can download it from this page or from the download link in the email notification you will receive. You can then import it when creating a new project. %{link_start}Learn more.%{link_end}" msgstr "" msgid "Export variable to pipelines running on protected branches and tags only." @@ -14961,6 +14978,351 @@ msgstr "" msgid "In progress" msgstr "" +msgid "InProductMarketing|%{strong_start}Advanced application security%{strong_end} — including SAST, DAST scanning, FUZZ testing, dependency scanning, license compliance, secrete detection" +msgstr "" + +msgid "InProductMarketing|%{strong_start}Company wide portfolio management%{strong_end} — including multi-level epics, scoped labels" +msgstr "" + +msgid "InProductMarketing|%{strong_start}Executive level insights%{strong_end} — including reporting on productivity, tasks by type, days to completion, value stream" +msgstr "" + +msgid "InProductMarketing|%{strong_start}GitLab Inc.%{strong_end} 268 Bush Street, #350, San Francisco, CA 94104, USA" +msgstr "" + +msgid "InProductMarketing|%{strong_start}Multiple approval roles%{strong_end} — including code owners and required merge approvals" +msgstr "" + +msgid "InProductMarketing|*GitLab*, noun: a synonym for efficient teams" +msgstr "" + +msgid "InProductMarketing|...and you can get a free trial of GitLab Gold" +msgstr "" + +msgid "InProductMarketing|3 ways to dive into GitLab CI/CD" +msgstr "" + +msgid "InProductMarketing|Actually, GitLab makes the team work (better)" +msgstr "" + +msgid "InProductMarketing|And finally %{deploy_link} a Python application." +msgstr "" + +msgid "InProductMarketing|Are your runners ready?" +msgstr "" + +msgid "InProductMarketing|Automated security scans directly within GitLab" +msgstr "" + +msgid "InProductMarketing|Beef up your security" +msgstr "" + +msgid "InProductMarketing|Better code in less time" +msgstr "" + +msgid "InProductMarketing|Blog" +msgstr "" + +msgid "InProductMarketing|By enabling code owners and required merge approvals the right person will review the right MR. This is a win-win: cleaner code and a more efficient review process." +msgstr "" + +msgid "InProductMarketing|Code owners and required merge approvals are part of the paid tiers of GitLab. You can start a free 30-day trial of GitLab Gold and enable these features in less than 5 minutes with no credit card required." +msgstr "" + +msgid "InProductMarketing|Create a project in GitLab in 5 minutes" +msgstr "" + +msgid "InProductMarketing|Create your first project!" +msgstr "" + +msgid "InProductMarketing|Did you know teams that use GitLab are far more efficient?" +msgstr "" + +msgid "InProductMarketing|Dig in and create a project and a repo" +msgstr "" + +msgid "InProductMarketing|Explore GitLab CI/CD" +msgstr "" + +msgid "InProductMarketing|Explore the options" +msgstr "" + +msgid "InProductMarketing|Explore the power of GitLab CI/CD" +msgstr "" + +msgid "InProductMarketing|Facebook" +msgstr "" + +msgid "InProductMarketing|Feel the need for speed?" +msgstr "" + +msgid "InProductMarketing|Find out how your teams are really doing" +msgstr "" + +msgid "InProductMarketing|Follow our steps" +msgstr "" + +msgid "InProductMarketing|Get going with CI/CD quickly using our %{quick_start_link}. Start with an available runner and then create a CI .yml file – it's really that easy." +msgstr "" + +msgid "InProductMarketing|Get our import guides" +msgstr "" + +msgid "InProductMarketing|Get started today" +msgstr "" + +msgid "InProductMarketing|Get started today with a 30-day GitLab Gold trial, no credit card required." +msgstr "" + +msgid "InProductMarketing|Get started with GitLab CI/CD" +msgstr "" + +msgid "InProductMarketing|Get to know GitLab CI/CD" +msgstr "" + +msgid "InProductMarketing|Get your team set up on GitLab" +msgstr "" + +msgid "InProductMarketing|Git basics" +msgstr "" + +msgid "InProductMarketing|GitHub Enterprise projects to GitLab" +msgstr "" + +msgid "InProductMarketing|GitLab provides static application security testing (SAST), dynamic application security testing (DAST), container scanning, and dependency scanning to help you deliver secure applications along with license compliance." +msgstr "" + +msgid "InProductMarketing|GitLab's CI/CD makes software development easier. Don't believe us? Here are three ways you can take it for a fast (and satisfying) test drive:" +msgstr "" + +msgid "InProductMarketing|GitLab's premium tiers are designed to make you, your team and your application more efficient and more secure with features including but not limited to:" +msgstr "" + +msgid "InProductMarketing|Give us one minute..." +msgstr "" + +msgid "InProductMarketing|Go farther with GitLab" +msgstr "" + +msgid "InProductMarketing|Go for the gold!" +msgstr "" + +msgid "InProductMarketing|Goldman Sachs went from 1 build every two weeks to thousands of builds a day" +msgstr "" + +msgid "InProductMarketing|Have a different instance you'd like to import? Here's our %{import_link}." +msgstr "" + +msgid "InProductMarketing|Here's what you need to know" +msgstr "" + +msgid "InProductMarketing|How (and why) mirroring makes sense" +msgstr "" + +msgid "InProductMarketing|How long does it take us to close issues/MRs by types like feature requests, bugs, tech debt, security?" +msgstr "" + +msgid "InProductMarketing|How many days does it take our team to complete various tasks?" +msgstr "" + +msgid "InProductMarketing|How to build and test faster" +msgstr "" + +msgid "InProductMarketing|If you no longer wish to receive marketing emails from us," +msgstr "" + +msgid "InProductMarketing|Import your project and code from GitHub, Bitbucket and others" +msgstr "" + +msgid "InProductMarketing|Improve app security with a 30-day trial" +msgstr "" + +msgid "InProductMarketing|Improve code quality and streamline reviews" +msgstr "" + +msgid "InProductMarketing|Invite your colleagues and start shipping code faster." +msgstr "" + +msgid "InProductMarketing|Invite your colleagues to join in less than one minute" +msgstr "" + +msgid "InProductMarketing|Invite your colleagues today" +msgstr "" + +msgid "InProductMarketing|Invite your team in less than 60 seconds" +msgstr "" + +msgid "InProductMarketing|Invite your team now" +msgstr "" + +msgid "InProductMarketing|It's all in the stats" +msgstr "" + +msgid "InProductMarketing|It's also possible to simply %{external_repo_link} in order to take advantage of GitLab's CI/CD." +msgstr "" + +msgid "InProductMarketing|Launch GitLab CI/CD in 20 minutes or less" +msgstr "" + +msgid "InProductMarketing|Making the switch? It's easier than you think to import your projects into GitLab. Move %{github_link}, or import something %{bitbucket_link}." +msgstr "" + +msgid "InProductMarketing|Master the art of importing!" +msgstr "" + +msgid "InProductMarketing|Move on to easily creating a Pages website %{ci_template_link}" +msgstr "" + +msgid "InProductMarketing|Multiple owners, confusing workstreams? We've got you covered" +msgstr "" + +msgid "InProductMarketing|Need an alternative to importing?" +msgstr "" + +msgid "InProductMarketing|Our tool brings all the things together" +msgstr "" + +msgid "InProductMarketing|Rapid development, simplified" +msgstr "" + +msgid "InProductMarketing|Security that's integrated into your development lifecycle" +msgstr "" + +msgid "InProductMarketing|Sometimes you're not ready to make a full transition to a new tool. If you're not ready to fully commit, %{mirroring_link} gives you a safe way to try out GitLab in parallel with your current tool." +msgstr "" + +msgid "InProductMarketing|Start a GitLab Gold trial today in less than one minute, no credit card required." +msgstr "" + +msgid "InProductMarketing|Start a free trial of GitLab Gold – no CC required" +msgstr "" + +msgid "InProductMarketing|Start a trial" +msgstr "" + +msgid "InProductMarketing|Start by %{performance_link}" +msgstr "" + +msgid "InProductMarketing|Start by importing your projects" +msgstr "" + +msgid "InProductMarketing|Start with a GitLab Gold free trial" +msgstr "" + +msgid "InProductMarketing|Stop wondering and use GitLab to answer questions like:" +msgstr "" + +msgid "InProductMarketing|Streamline code review, know at a glance who's unavailable, communicate in comments or in email and integrate with Slack so everyone's on the same page." +msgstr "" + +msgid "InProductMarketing|Take your first steps with GitLab" +msgstr "" + +msgid "InProductMarketing|Take your source code management to the next level" +msgstr "" + +msgid "InProductMarketing|Team work makes the dream work" +msgstr "" + +msgid "InProductMarketing|Test, create, deploy" +msgstr "" + +msgid "InProductMarketing|That's all it takes to get going with GitLab, but if you're new to working with Git, check out our %{basics_link} for helpful tips and tricks for getting started." +msgstr "" + +msgid "InProductMarketing|This is email %{series} of 3 in the %{track} series." +msgstr "" + +msgid "InProductMarketing|Ticketmaster decreased their CI build time by 15X" +msgstr "" + +msgid "InProductMarketing|Tired of wrestling with disparate tool chains, information silos and inefficient processes? GitLab's CI/CD is built on a DevOps platform with source code management, planning, monitoring and more ready to go. Find out %{ci_link}." +msgstr "" + +msgid "InProductMarketing|To understand and get the most out of GitLab, start at the beginning and %{project_link}. In GitLab, repositories are part of a project, so after you've created your project you can go ahead and %{repo_link}." +msgstr "" + +msgid "InProductMarketing|Try GitLab Gold for free" +msgstr "" + +msgid "InProductMarketing|Try it out" +msgstr "" + +msgid "InProductMarketing|Try it yourself" +msgstr "" + +msgid "InProductMarketing|Twitter" +msgstr "" + +msgid "InProductMarketing|Understand repository mirroring" +msgstr "" + +msgid "InProductMarketing|Understand your project options" +msgstr "" + +msgid "InProductMarketing|Use GitLab CI/CD" +msgstr "" + +msgid "InProductMarketing|We know a thing or two about efficiency and we don't want to keep that to ourselves. Sign up for a free trial of GitLab Gold and your teams will be on it from day one." +msgstr "" + +msgid "InProductMarketing|What does our value stream timeline look like from product to development to review and production?" +msgstr "" + +msgid "InProductMarketing|When your team is on GitLab these answers are a click away." +msgstr "" + +msgid "InProductMarketing|Working in GitLab = more efficient" +msgstr "" + +msgid "InProductMarketing|YouTube" +msgstr "" + +msgid "InProductMarketing|Your teams can be more efficient" +msgstr "" + +msgid "InProductMarketing|comprehensive guide" +msgstr "" + +msgid "InProductMarketing|connect an external repository" +msgstr "" + +msgid "InProductMarketing|create a project" +msgstr "" + +msgid "InProductMarketing|from Bitbucket" +msgstr "" + +msgid "InProductMarketing|go to about.gitlab.com" +msgstr "" + +msgid "InProductMarketing|how easy it is to get started" +msgstr "" + +msgid "InProductMarketing|quick start guide" +msgstr "" + +msgid "InProductMarketing|repository mirroring" +msgstr "" + +msgid "InProductMarketing|set up a repo" +msgstr "" + +msgid "InProductMarketing|test and deploy" +msgstr "" + +msgid "InProductMarketing|testing browser performance" +msgstr "" + +msgid "InProductMarketing|unsubscribe" +msgstr "" + +msgid "InProductMarketing|using a CI/CD template" +msgstr "" + +msgid "InProductMarketing|you may %{unsubscribe_link} at any time." +msgstr "" + msgid "Incident" msgstr "" @@ -19444,6 +19806,9 @@ msgstr "" msgid "No matching results for \"%{query}\"" msgstr "" +msgid "No matching results..." +msgstr "" + msgid "No members found" msgstr "" @@ -19993,10 +20358,10 @@ msgstr "" msgid "OnDemandScans|Could not run the scan. Please try again." msgstr "" -msgid "OnDemandScans|Create a new scanner profile" +msgid "OnDemandScans|Create new scanner profile" msgstr "" -msgid "OnDemandScans|Create a new site profile" +msgid "OnDemandScans|Create new site profile" msgstr "" msgid "OnDemandScans|Description (optional)" @@ -20008,9 +20373,18 @@ msgstr "" msgid "OnDemandScans|For example: Tests the login page for SQL injections" msgstr "" +msgid "OnDemandScans|Manage DAST scans" +msgstr "" + msgid "OnDemandScans|Manage profiles" msgstr "" +msgid "OnDemandScans|Manage scanner profiles" +msgstr "" + +msgid "OnDemandScans|Manage site profiles" +msgstr "" + msgid "OnDemandScans|My daily scan" msgstr "" @@ -20062,10 +20436,10 @@ msgstr "" msgid "OnDemandScans|You cannot run an active scan against an unvalidated site." msgstr "" -msgid "Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc." +msgid "Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc." msgstr "" -msgid "Once a project is permanently deleted it cannot be recovered. You will lose this project's repository and all content: issues, merge requests etc." +msgid "Once a project is permanently deleted, it cannot be recovered. You will lose this project's repository and all related resources, including issues, merge requests etc." msgstr "" msgid "Once imported, repositories can be mirrored over SSH. Read more %{link_start}here%{link_end}." @@ -28978,16 +29352,16 @@ msgstr "" msgid "This action cannot be undone, and will permanently delete the %{key} SSH key" msgstr "" -msgid "This action cannot be undone. You will lose this project's repository and all content: issues, merge requests, etc." +msgid "This action cannot be undone. You will lose this project's repository and all related resources, including issues, merge requests, etc." msgstr "" msgid "This action has been performed too many times. Try again later." msgstr "" -msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all content: issues, merge requests, etc." +msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}immediately%{strongClose}, including its repositories and all related resources, including issues, merge requests, etc." msgstr "" -msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}on %{date}%{strongClose}, including its repositories and all content: issues, merge requests, etc." +msgid "This action will %{strongOpen}permanently delete%{strongClose} %{codeOpen}%{project}%{codeClose} %{strongOpen}on %{date}%{strongClose}, including its repositories and all related resources, including issues, merge requests, etc." msgstr "" msgid "This also resolves all related threads" @@ -30122,6 +30496,9 @@ msgstr "" msgid "Transfer project" msgstr "" +msgid "Transfer your project into another namespace. %{link_start}Learn more.%{link_end}" +msgstr "" + msgid "TransferGroup|Cannot transfer group to one of its subgroup." msgstr "" @@ -30499,7 +30876,7 @@ msgstr "" msgid "Unarchive project" msgstr "" -msgid "Unarchiving the project will restore people's ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end}" +msgid "Unarchiving the project will restore its members' ability to make changes to it. The repository can be committed to, and issues, comments, and other entities can be created. %{strong_start}Once active, this project shows up in the search and on the dashboard.%{strong_end} %{link_start}Learn more.%{link_end}" msgstr "" msgid "Unassign from commenting user" @@ -34063,6 +34440,12 @@ msgstr "" msgid "mrWidget|%{link_start}Learn more about resolving conflicts%{link_end}" msgstr "" +msgid "mrWidget|%{mergeError}." +msgstr "" + +msgid "mrWidget|%{mergeError}. Try again." +msgstr "" + msgid "mrWidget|%{metricsLinkStart} Memory %{metricsLinkEnd} usage %{emphasisStart} decreased %{emphasisEnd} from %{memoryFrom}MB to %{memoryTo}MB" msgstr "" @@ -34189,9 +34572,6 @@ msgstr "" msgid "mrWidget|Merge failed." msgstr "" -msgid "mrWidget|Merge failed: %{mergeError}. Please try again." -msgstr "" - msgid "mrWidget|Merge locally" msgstr "" diff --git a/package.json b/package.json index c42290837df..4b37c7ed640 100644 --- a/package.json +++ b/package.json @@ -163,7 +163,7 @@ }, "devDependencies": { "@babel/plugin-transform-modules-commonjs": "^7.10.1", - "@gitlab/eslint-plugin": "7.0.0", + "@gitlab/eslint-plugin": "7.0.2", "@testing-library/dom": "^7.16.2", "@vue/test-utils": "1.1.2", "acorn": "^6.3.0", diff --git a/spec/controllers/import/bulk_imports_controller_spec.rb b/spec/controllers/import/bulk_imports_controller_spec.rb index d1c138617bb..9927ef0903a 100644 --- a/spec/controllers/import/bulk_imports_controller_spec.rb +++ b/spec/controllers/import/bulk_imports_controller_spec.rb @@ -59,7 +59,14 @@ RSpec.describe Import::BulkImportsController do parsed_response: [ { 'id' => 1, 'full_name' => 'group1', 'full_path' => 'full/path/group1', 'web_url' => 'http://demo.host/full/path/group1' }, { 'id' => 2, 'full_name' => 'group2', 'full_path' => 'full/path/group2', 'web_url' => 'http://demo.host/full/path/group1' } - ] + ], + headers: { + 'x-next-page' => '2', + 'x-page' => '1', + 'x-per-page' => '20', + 'x-total' => '37', + 'x-total-pages' => '2' + } ) end @@ -81,6 +88,17 @@ RSpec.describe Import::BulkImportsController do expect(json_response).to eq({ importable_data: client_response.parsed_response }.as_json) end + it 'forwards pagination headers' do + get :status, format: :json + + expect(response.headers['x-per-page']).to eq client_response.headers['x-per-page'] + expect(response.headers['x-page']).to eq client_response.headers['x-page'] + expect(response.headers['x-next-page']).to eq client_response.headers['x-next-page'] + expect(response.headers['x-prev-page']).to eq client_response.headers['x-prev-page'] + expect(response.headers['x-total']).to eq client_response.headers['x-total'] + expect(response.headers['x-total-pages']).to eq client_response.headers['x-total-pages'] + end + context 'when filtering' do it 'returns filtered result' do filter = 'test' diff --git a/spec/features/groups/import_export/connect_instance_spec.rb b/spec/features/groups/import_export/connect_instance_spec.rb index 98212df0c01..73de49101ea 100644 --- a/spec/features/groups/import_export/connect_instance_spec.rb +++ b/spec/features/groups/import_export/connect_instance_spec.rb @@ -23,8 +23,8 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do source_url = 'https://gitlab.com' pat = 'demo-pat' stub_path = 'stub-group' - - stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=30&top_level_only=true&min_access_level=40" % { url: source_url }).to_return( + total = 37 + stub_request(:get, "%{url}/api/v4/groups?page=1&per_page=20&top_level_only=true&min_access_level=40&search=" % { url: source_url }).to_return( body: [{ id: 2595438, web_url: 'https://gitlab.com/groups/auto-breakfast', @@ -33,7 +33,14 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do full_name: 'Stub', full_path: stub_path }].to_json, - headers: { 'Content-Type' => 'application/json' } + headers: { + 'Content-Type' => 'application/json', + 'X-Next-Page' => 2, + 'X-Page' => 1, + 'X-Per-Page' => 20, + 'X-Total' => total, + 'X-Total-Pages' => 2 + } ) expect(page).to have_content 'Import groups from another instance of GitLab' @@ -44,7 +51,7 @@ RSpec.describe 'Import/Export - Connect to another instance', :js do click_on 'Connect instance' - expect(page).to have_content 'Importing groups from %{url}' % { url: source_url } + expect(page).to have_content 'Showing 1-1 of %{total} groups from %{url}' % { url: source_url, total: total } expect(page).to have_content stub_path end end diff --git a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb index 5e99383e4a1..885b41c3227 100644 --- a/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_request/user_merges_when_pipeline_succeeds_spec.rb @@ -145,7 +145,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do before do merge_request.update!( merge_user: merge_request.author, - merge_error: 'Something went wrong.' + merge_error: 'Something went wrong' ) refresh end @@ -155,7 +155,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do wait_for_requests page.within('.mr-section-container') do - expect(page).to have_content('Merge failed: Something went wrong. Please try again.') + expect(page).to have_content('Something went wrong. Try again.') end end end @@ -174,7 +174,7 @@ RSpec.describe 'Merge request > User merges when pipeline succeeds', :js do wait_for_requests page.within('.mr-section-container') do - expect(page).to have_content('Merge failed: Something went wrong. Please try again.') + expect(page).to have_content('Something went wrong. Try again.') end end end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index c2b2ada47be..9bd6cd8863f 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -319,7 +319,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do wait_for_requests page.within('.mr-section-container') do - expect(page).to have_content('Merge failed: Something went wrong') + expect(page).to have_content('Something went wrong.') end end end @@ -340,7 +340,7 @@ RSpec.describe 'Merge request > User sees merge widget', :js do wait_for_requests page.within('.mr-section-container') do - expect(page).to have_content('Merge failed: Something went wrong') + expect(page).to have_content('Something went wrong.') end end end diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js index cd184bb65cc..34e2a945333 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -1,6 +1,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlEmptyState, GlLoadingIcon, GlSearchBoxByClick, GlSprintf } from '@gitlab/ui'; import waitForPromises from 'helpers/wait_for_promises'; import createMockApollo from 'helpers/mock_apollo_helper'; import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue'; @@ -8,6 +8,7 @@ import ImportTable from '~/import_entities/import_groups/components/import_table import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql'; import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql'; import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql'; +import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; import { STATUSES } from '~/import_entities/constants'; @@ -20,6 +21,9 @@ describe('import table', () => { let wrapper; let apolloProvider; + const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE }); + const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 }; + const createComponent = ({ bulkImportSourceGroups }) => { apolloProvider = createMockApollo([], { Query: { @@ -34,6 +38,12 @@ describe('import table', () => { }); wrapper = shallowMount(ImportTable, { + propsData: { + sourceUrl: 'https://demo.host', + }, + stubs: { + GlSprintf, + }, localVue, apolloProvider, }); @@ -62,25 +72,50 @@ describe('import table', () => { expect(wrapper.find(GlLoadingIcon).exists()).toBe(false); }); + it('renders message about empty state when no groups are available for import', async () => { + createComponent({ + bulkImportSourceGroups: () => ({ + nodes: [], + pageInfo: FAKE_PAGE_INFO, + }), + }); + await waitForPromises(); + + expect(wrapper.find(GlEmptyState).props().title).toBe('No groups available for import'); + }); + it('renders import row for each group in response', async () => { const FAKE_GROUPS = [ generateFakeEntry({ id: 1, status: STATUSES.NONE }), generateFakeEntry({ id: 2, status: STATUSES.FINISHED }), ]; createComponent({ - bulkImportSourceGroups: () => FAKE_GROUPS, + bulkImportSourceGroups: () => ({ + nodes: FAKE_GROUPS, + pageInfo: FAKE_PAGE_INFO, + }), }); await waitForPromises(); expect(wrapper.findAll(ImportTableRow)).toHaveLength(FAKE_GROUPS.length); }); - describe('converts row events to mutation invocations', () => { - const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE }); + it('does not render status string when result list is empty', async () => { + createComponent({ + bulkImportSourceGroups: jest.fn().mockResolvedValue({ + nodes: [], + pageInfo: FAKE_PAGE_INFO, + }), + }); + await waitForPromises(); + expect(wrapper.text()).not.toContain('Showing 1-0'); + }); + + describe('converts row events to mutation invocations', () => { beforeEach(() => { createComponent({ - bulkImportSourceGroups: () => [FAKE_GROUP], + bulkImportSourceGroups: () => ({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }), }); return waitForPromises(); }); @@ -100,4 +135,115 @@ describe('import table', () => { }); }); }); + + describe('pagination', () => { + const bulkImportSourceGroupsQueryMock = jest + .fn() + .mockResolvedValue({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }); + + beforeEach(() => { + createComponent({ + bulkImportSourceGroups: bulkImportSourceGroupsQueryMock, + }); + return waitForPromises(); + }); + + it('correctly passes pagination info from query', () => { + expect(wrapper.find(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO); + }); + + it('updates page when page change is requested', async () => { + const REQUESTED_PAGE = 2; + wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE); + + await waitForPromises(); + expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ page: REQUESTED_PAGE }), + expect.anything(), + expect.anything(), + ); + }); + + it('updates status text when page is changed', async () => { + const REQUESTED_PAGE = 2; + bulkImportSourceGroupsQueryMock.mockResolvedValue({ + nodes: [FAKE_GROUP], + pageInfo: { + page: 2, + total: 38, + perPage: 20, + totalPages: 2, + }, + }); + wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE); + await waitForPromises(); + + expect(wrapper.text()).toContain('Showing 21-21 of 38'); + }); + }); + + describe('filters', () => { + const bulkImportSourceGroupsQueryMock = jest + .fn() + .mockResolvedValue({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }); + + beforeEach(() => { + createComponent({ + bulkImportSourceGroups: bulkImportSourceGroupsQueryMock, + }); + return waitForPromises(); + }); + + const findFilterInput = () => wrapper.find(GlSearchBoxByClick); + + it('properly passes filter to graphql query when search box is submitted', async () => { + createComponent({ + bulkImportSourceGroups: bulkImportSourceGroupsQueryMock, + }); + await waitForPromises(); + + const FILTER_VALUE = 'foo'; + findFilterInput().vm.$emit('submit', FILTER_VALUE); + await waitForPromises(); + + expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ filter: FILTER_VALUE }), + expect.anything(), + expect.anything(), + ); + }); + + it('updates status string when search box is submitted', async () => { + createComponent({ + bulkImportSourceGroups: bulkImportSourceGroupsQueryMock, + }); + await waitForPromises(); + + const FILTER_VALUE = 'foo'; + findFilterInput().vm.$emit('submit', FILTER_VALUE); + await waitForPromises(); + + expect(wrapper.text()).toContain('Showing 1-1 of 40 groups matching filter "foo"'); + }); + + it('properly resets filter in graphql query when search box is cleared', async () => { + const FILTER_VALUE = 'foo'; + findFilterInput().vm.$emit('submit', FILTER_VALUE); + await waitForPromises(); + + bulkImportSourceGroupsQueryMock.mockClear(); + await apolloProvider.defaultClient.resetStore(); + findFilterInput().vm.$emit('clear'); + await waitForPromises(); + + expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ filter: '' }), + expect.anything(), + expect.anything(), + ); + }); + }); }); diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js index 514ed411138..94a2e4f75b4 100644 --- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js +++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js @@ -79,33 +79,56 @@ describe('Bulk import resolvers', () => { axiosMockAdapter .onGet(FAKE_ENDPOINTS.availableNamespaces) .reply(httpStatus.OK, availableNamespacesFixture); - - const response = await client.query({ query: bulkImportSourceGroupsQuery }); - results = response.data.bulkImportSourceGroups; }); - it('mirrors REST endpoint response fields', () => { - const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url']; - expect( - results.every((r, idx) => - MIRRORED_FIELDS.every( - (field) => r[field] === statusEndpointFixture.importable_data[idx][field], + describe('when called', () => { + beforeEach(async () => { + const response = await client.query({ query: bulkImportSourceGroupsQuery }); + results = response.data.bulkImportSourceGroups.nodes; + }); + + it('mirrors REST endpoint response fields', () => { + const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url']; + expect( + results.every((r, idx) => + MIRRORED_FIELDS.every( + (field) => r[field] === statusEndpointFixture.importable_data[idx][field], + ), ), - ), - ).toBe(true); - }); + ).toBe(true); + }); - it('populates each result instance with status field default to none', () => { - expect(results.every((r) => r.status === STATUSES.NONE)).toBe(true); - }); + it('populates each result instance with status field default to none', () => { + expect(results.every((r) => r.status === STATUSES.NONE)).toBe(true); + }); - it('populates each result instance with import_target defaulted to first available namespace', () => { - expect( - results.every( - (r) => r.import_target.target_namespace === availableNamespacesFixture[0].full_path, - ), - ).toBe(true); + it('populates each result instance with import_target defaulted to first available namespace', () => { + expect( + results.every( + (r) => r.import_target.target_namespace === availableNamespacesFixture[0].full_path, + ), + ).toBe(true); + }); }); + + it.each` + variable | queryParam | value + ${'filter'} | ${'filter'} | ${'demo'} + ${'perPage'} | ${'per_page'} | ${30} + ${'page'} | ${'page'} | ${3} + `( + 'properly passes GraphQL variable $variable as REST $queryParam query parameter', + async ({ variable, queryParam, value }) => { + await client.query({ + query: bulkImportSourceGroupsQuery, + variables: { [variable]: value }, + }); + const restCall = axiosMockAdapter.history.get.find( + (q) => q.url === FAKE_ENDPOINTS.status, + ); + expect(restCall.params[queryParam]).toBe(value); + }, + ); }); }); @@ -117,20 +140,28 @@ describe('Bulk import resolvers', () => { client.writeQuery({ query: bulkImportSourceGroupsQuery, data: { - bulkImportSourceGroups: [ - { - __typename: clientTypenames.BulkImportSourceGroup, - id: GROUP_ID, - status: STATUSES.NONE, - web_url: 'https://fake.host/1', - full_path: 'fake_group_1', - full_name: 'fake_name_1', - import_target: { - target_namespace: 'root', - new_name: 'group1', + bulkImportSourceGroups: { + nodes: [ + { + __typename: clientTypenames.BulkImportSourceGroup, + id: GROUP_ID, + status: STATUSES.NONE, + web_url: 'https://fake.host/1', + full_path: 'fake_group_1', + full_name: 'fake_name_1', + import_target: { + target_namespace: 'root', + new_name: 'group1', + }, }, + ], + pageInfo: { + page: 1, + perPage: 20, + total: 37, + totalPages: 2, }, - ], + }, }, }); @@ -140,7 +171,7 @@ describe('Bulk import resolvers', () => { fetchPolicy: 'cache-only', }) .subscribe(({ data }) => { - results = data.bulkImportSourceGroups; + results = data.bulkImportSourceGroups.nodes; }); }); @@ -174,7 +205,9 @@ describe('Bulk import resolvers', () => { }); await waitForPromises(); - const { bulkImportSourceGroups: intermediateResults } = client.readQuery({ + const { + bulkImportSourceGroups: { nodes: intermediateResults }, + } = client.readQuery({ query: bulkImportSourceGroupsQuery, }); diff --git a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js index e7f1626f81d..8d0e1dbd65f 100644 --- a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js +++ b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js @@ -4,6 +4,7 @@ import waitForPromises from 'helpers/wait_for_promises'; import createFlash from '~/flash'; import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller'; +import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory'; import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql'; import { STATUSES } from '~/import_entities/constants'; import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager'; @@ -17,6 +18,7 @@ jest.mock('~/import_entities/import_groups/graphql/services/source_groups_manage })); const TEST_POLL_INTERVAL = 1000; +const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 }; describe('Bulk import status poller', () => { let poller; @@ -25,6 +27,25 @@ describe('Bulk import status poller', () => { const listQueryCacheCalls = () => clientMock.readQuery.mock.calls.filter((call) => call[0].query === bulkImportSourceGroupsQuery); + const generateFakeGroups = (statuses) => + statuses.map((status, idx) => generateFakeEntry({ status, id: idx })); + + const writeFakeGroupsQuery = (nodes) => { + clientMock.cache.writeQuery({ + query: bulkImportSourceGroupsQuery, + data: { + bulkImportSourceGroups: { + __typename: clientTypenames.BulkImportSourceGroupConnection, + nodes, + pageInfo: { + __typename: clientTypenames.BulkImportPageInfo, + ...FAKE_PAGE_INFO, + }, + }, + }, + }); + }; + beforeEach(() => { clientMock = createMockClient({ cache: new InMemoryCache({ @@ -42,10 +63,7 @@ describe('Bulk import status poller', () => { describe('general behavior', () => { beforeEach(() => { - clientMock.cache.writeQuery({ - query: bulkImportSourceGroupsQuery, - data: { bulkImportSourceGroups: [] }, - }); + writeFakeGroupsQuery([]); }); it('does not perform polling when constructed', () => { @@ -94,14 +112,7 @@ describe('Bulk import status poller', () => { }); it('does not query server when no groups have STARTED status', async () => { - clientMock.cache.writeQuery({ - query: bulkImportSourceGroupsQuery, - data: { - bulkImportSourceGroups: [STATUSES.NONE, STATUSES.FINISHED].map((status, idx) => - generateFakeEntry({ status, id: idx }), - ), - }, - }); + writeFakeGroupsQuery(generateFakeGroups([STATUSES.NONE, STATUSES.FINISHED])); jest.spyOn(clientMock, 'query'); poller.startPolling(); @@ -111,44 +122,23 @@ describe('Bulk import status poller', () => { describe('when there are groups which have STARTED status', () => { const TARGET_NAMESPACE = 'root'; - const STARTED_GROUP_1 = { + const STARTED_GROUP_1 = generateFakeEntry({ status: STATUSES.STARTED, id: 'started1', - import_target: { - target_namespace: TARGET_NAMESPACE, - new_name: 'group1', - }, - }; + }); - const STARTED_GROUP_2 = { + const STARTED_GROUP_2 = generateFakeEntry({ status: STATUSES.STARTED, id: 'started2', - import_target: { - target_namespace: TARGET_NAMESPACE, - new_name: 'group2', - }, - }; + }); - const NOT_STARTED_GROUP = { + const NOT_STARTED_GROUP = generateFakeEntry({ status: STATUSES.NONE, id: 'not_started', - import_target: { - target_namespace: TARGET_NAMESPACE, - new_name: 'group3', - }, - }; + }); it('query server only for groups with STATUSES.STARTED', async () => { - clientMock.cache.writeQuery({ - query: bulkImportSourceGroupsQuery, - data: { - bulkImportSourceGroups: [ - STARTED_GROUP_1, - NOT_STARTED_GROUP, - STARTED_GROUP_2, - ].map((group) => generateFakeEntry(group)), - }, - }); + writeFakeGroupsQuery([STARTED_GROUP_1, NOT_STARTED_GROUP, STARTED_GROUP_2]); clientMock.query = jest.fn().mockResolvedValue({ data: {} }); poller.startPolling(); @@ -166,14 +156,7 @@ describe('Bulk import status poller', () => { }); it('updates statuses only for groups in response', async () => { - clientMock.cache.writeQuery({ - query: bulkImportSourceGroupsQuery, - data: { - bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map((group) => - generateFakeEntry(group), - ), - }, - }); + writeFakeGroupsQuery([STARTED_GROUP_1, STARTED_GROUP_2]); clientMock.query = jest.fn().mockResolvedValue({ data: { group0: {} } }); poller.startPolling(); @@ -188,14 +171,7 @@ describe('Bulk import status poller', () => { describe('when error occurs', () => { beforeEach(() => { - clientMock.cache.writeQuery({ - query: bulkImportSourceGroupsQuery, - data: { - bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map((group) => - generateFakeEntry(group), - ), - }, - }); + writeFakeGroupsQuery([STARTED_GROUP_1, STARTED_GROUP_2]); clientMock.query = jest.fn().mockRejectedValue(new Error('dummy error')); poller.startPolling(); diff --git a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap index 0b9f095a700..f0d72124379 100644 --- a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap +++ b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap @@ -53,12 +53,12 @@ exports[`Project remove modal initialized matches the snapshot 1`] = ` variant="danger" > <gl-sprintf-stub - message="Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc." + message="Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc." /> </gl-alert-stub> <p> - This action cannot be undone. You will lose this project's repository and all content: issues, merge requests, etc. + This action cannot be undone. You will lose this project's repository and all related resources, including issues, merge requests, etc. </p> <p diff --git a/spec/frontend/tracking_spec.js b/spec/frontend/tracking_spec.js index 6fd1a330bb2..a516a4a8269 100644 --- a/spec/frontend/tracking_spec.js +++ b/spec/frontend/tracking_spec.js @@ -1,5 +1,3 @@ -// Work around for https://github.com/vuejs/eslint-plugin-vue/issues/1411 -/* eslint-disable vue/one-component-per-file */ import { setHTMLFixture } from 'helpers/fixtures'; import Tracking, { initUserTracking, initDefaultTrackers } from '~/tracking'; diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js index 6c3b4a01659..c25e10c5249 100644 --- a/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js +++ b/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js @@ -1,48 +1,60 @@ -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; +import { GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue'; describe('MR widget status icon component', () => { - let vm; - let Component; + let wrapper; - beforeEach(() => { - Component = Vue.extend(mrStatusIcon); - }); + const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + const findDisabledMergeButton = () => wrapper.find('[data-testid="disabled-merge-button"]'); + + const createWrapper = (props, mountFn = shallowMount) => { + wrapper = mountFn(mrStatusIcon, { + propsData: { + ...props, + }, + }); + }; afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); describe('while loading', () => { it('renders loading icon', () => { - vm = mountComponent(Component, { status: 'loading' }); + createWrapper({ status: 'loading' }); - expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('gl-spinner'); + expect(findLoadingIcon().exists()).toBe(true); }); }); describe('with status icon', () => { - it('renders ci status icon', () => { - vm = mountComponent(Component, { status: 'failed' }); + it('renders success status icon', () => { + createWrapper({ status: 'success' }, mount); + + expect(wrapper.find('[data-testid="status_success-icon"]').exists()).toBe(true); + }); + + it('renders failed status icon', () => { + createWrapper({ status: 'failed' }, mount); - expect(vm.$el.querySelector('.js-ci-status-icon-failed')).not.toBeNull(); + expect(wrapper.find('[data-testid="status_failed-icon"]').exists()).toBe(true); }); }); describe('with disabled button', () => { it('renders a disabled button', () => { - vm = mountComponent(Component, { status: 'failed', showDisabledButton: true }); + createWrapper({ status: 'failed', showDisabledButton: true }); - expect(vm.$el.querySelector('.js-disabled-merge-button').textContent.trim()).toEqual('Merge'); + expect(findDisabledMergeButton().exists()).toBe(true); }); }); describe('without disabled button', () => { it('does not render a disabled button', () => { - vm = mountComponent(Component, { status: 'failed' }); + createWrapper({ status: 'failed' }); - expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeNull(); + expect(findDisabledMergeButton().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js index 48c1a9eedf9..c1471314c4a 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js @@ -54,7 +54,7 @@ describe('MRWidgetFailedToMerge', () => { Vue.nextTick() .then(() => { - expect(vm.mergeError).toBe('contains line breaks'); + expect(vm.mergeError).toBe('contains line breaks.'); }) .then(done) .catch(done.fail); @@ -113,14 +113,14 @@ describe('MRWidgetFailedToMerge', () => { describe('while it is not regresing', () => { it('renders warning icon and disabled merge button', () => { expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull(); - expect(vm.$el.querySelector('.js-disabled-merge-button').getAttribute('disabled')).toEqual( - 'disabled', - ); + expect( + vm.$el.querySelector('[data-testid="disabled-merge-button"]').getAttribute('disabled'), + ).toEqual('disabled'); }); it('renders given error', () => { expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual( - 'Merge error happened', + 'Merge error happened.', ); }); diff --git a/spec/lib/gitlab/access/branch_protection_spec.rb b/spec/lib/gitlab/access/branch_protection_spec.rb index 9b736a30c7e..44c30d1f596 100644 --- a/spec/lib/gitlab/access/branch_protection_spec.rb +++ b/spec/lib/gitlab/access/branch_protection_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe Gitlab::Access::BranchProtection do - describe '#any?' do - using RSpec::Parameterized::TableSyntax + using RSpec::Parameterized::TableSyntax + describe '#any?' do where(:level, :result) do Gitlab::Access::PROTECTION_NONE | false Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true @@ -19,8 +19,6 @@ RSpec.describe Gitlab::Access::BranchProtection do end describe '#developer_can_push?' do - using RSpec::Parameterized::TableSyntax - where(:level, :result) do Gitlab::Access::PROTECTION_NONE | false Gitlab::Access::PROTECTION_DEV_CAN_PUSH | true @@ -36,8 +34,6 @@ RSpec.describe Gitlab::Access::BranchProtection do end describe '#developer_can_merge?' do - using RSpec::Parameterized::TableSyntax - where(:level, :result) do Gitlab::Access::PROTECTION_NONE | false Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false @@ -53,8 +49,6 @@ RSpec.describe Gitlab::Access::BranchProtection do end describe '#fully_protected?' do - using RSpec::Parameterized::TableSyntax - where(:level, :result) do Gitlab::Access::PROTECTION_NONE | false Gitlab::Access::PROTECTION_DEV_CAN_PUSH | false diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb index 80427eaa6ee..247f4b63910 100644 --- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Cache do + using RSpec::Parameterized::TableSyntax + subject(:entry) { described_class.new(config) } describe 'validations' do @@ -56,8 +58,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end context 'with `policy`' do - using RSpec::Parameterized::TableSyntax - where(:policy, :result) do 'pull-push' | 'pull-push' 'push' | 'push' @@ -77,8 +77,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end context 'with `when`' do - using RSpec::Parameterized::TableSyntax - where(:when_config, :result) do 'on_success' | 'on_success' 'on_failure' | 'on_failure' @@ -109,8 +107,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end context 'with `policy`' do - using RSpec::Parameterized::TableSyntax - where(:policy, :valid) do 'pull-push' | true 'push' | true @@ -126,8 +122,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end context 'with `when`' do - using RSpec::Parameterized::TableSyntax - where(:when_config, :valid) do 'on_success' | true 'on_failure' | true diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 6b709cba5b3..6de7fc3a50e 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -1874,7 +1874,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do has_internal_id :iid, scope: :project, init: ->(s, _scope) { s&.project&.issues&.maximum(:iid) }, - backfill: true, presence: false end end @@ -1928,258 +1927,6 @@ RSpec.describe Gitlab::Database::MigrationHelpers do expect(issue_b.iid).to eq(3) end - context 'when the new code creates a row post deploy but before the migration runs' do - it 'does not change the row iid' do - project = setup - issue = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - expect(issue.reload.iid).to eq(1) - end - - it 'backfills iids for rows already in the database' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_c.reload.iid).to eq(3) - end - - it 'backfills iids across multiple projects' do - project_a = setup - project_b = setup - issue_a = issues.create!(project_id: project_a.id) - issue_b = issues.create!(project_id: project_b.id) - issue_c = Issue.create!(project_id: project_a.id) - issue_d = Issue.create!(project_id: project_b.id) - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(1) - expect(issue_c.reload.iid).to eq(2) - expect(issue_d.reload.iid).to eq(2) - end - - it 'generates iids properly for models created after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - issue_d = Issue.create!(project_id: project.id) - issue_e = Issue.create!(project_id: project.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_c.reload.iid).to eq(3) - expect(issue_d.iid).to eq(4) - expect(issue_e.iid).to eq(5) - end - - it 'backfills iids and properly generates iids for new models across multiple projects' do - project_a = setup - project_b = setup - issue_a = issues.create!(project_id: project_a.id) - issue_b = issues.create!(project_id: project_b.id) - issue_c = Issue.create!(project_id: project_a.id) - issue_d = Issue.create!(project_id: project_b.id) - - model.backfill_iids('issues') - - issue_e = Issue.create!(project_id: project_a.id) - issue_f = Issue.create!(project_id: project_b.id) - issue_g = Issue.create!(project_id: project_a.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(1) - expect(issue_c.reload.iid).to eq(2) - expect(issue_d.reload.iid).to eq(2) - expect(issue_e.iid).to eq(3) - expect(issue_f.iid).to eq(3) - expect(issue_g.iid).to eq(4) - end - end - - context 'when the new code creates a model and then old code creates a model post deploy but before the migration runs' do - it 'backfills iids' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = Issue.create!(project_id: project.id) - issue_c = issues.create!(project_id: project.id) - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_c.reload.iid).to eq(3) - end - - it 'generates an iid for a new model after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_d = issues.create!(project_id: project.id) - - model.backfill_iids('issues') - - issue_e = Issue.create!(project_id: project.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_c.reload.iid).to eq(3) - expect(issue_d.reload.iid).to eq(4) - expect(issue_e.iid).to eq(5) - end - end - - context 'when the new code and old code alternate creating models post deploy but before the migration runs' do - it 'backfills iids' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = Issue.create!(project_id: project.id) - issue_c = issues.create!(project_id: project.id) - issue_d = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_c.reload.iid).to eq(3) - expect(issue_d.reload.iid).to eq(4) - end - - it 'generates an iid for a new model after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_d = issues.create!(project_id: project.id) - issue_e = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - issue_f = Issue.create!(project_id: project.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_c.reload.iid).to eq(3) - expect(issue_d.reload.iid).to eq(4) - expect(issue_e.reload.iid).to eq(5) - expect(issue_f.iid).to eq(6) - end - end - - context 'when the new code creates and deletes a model post deploy but before the migration runs' do - it 'backfills iids for rows already in the database' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_c.delete - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - end - - it 'successfully creates a new model after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_c.delete - - model.backfill_iids('issues') - - issue_d = Issue.create!(project_id: project.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_d.iid).to eq(3) - end - end - - context 'when the new code creates and deletes a model and old code creates a model post deploy but before the migration runs' do - it 'backfills iids' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_c.delete - issue_d = issues.create!(project_id: project.id) - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_d.reload.iid).to eq(3) - end - - it 'successfully creates a new model after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_c.delete - issue_d = issues.create!(project_id: project.id) - - model.backfill_iids('issues') - - issue_e = Issue.create!(project_id: project.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_d.reload.iid).to eq(3) - expect(issue_e.iid).to eq(4) - end - end - - context 'when the new code creates and deletes a model and then creates another model post deploy but before the migration runs' do - it 'successfully generates an iid for a new model after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_c.delete - issue_d = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_d.reload.iid).to eq(3) - end - - it 'successfully generates an iid for a new model after the migration' do - project = setup - issue_a = issues.create!(project_id: project.id) - issue_b = issues.create!(project_id: project.id) - issue_c = Issue.create!(project_id: project.id) - issue_c.delete - issue_d = Issue.create!(project_id: project.id) - - model.backfill_iids('issues') - - issue_e = Issue.create!(project_id: project.id) - - expect(issue_a.reload.iid).to eq(1) - expect(issue_b.reload.iid).to eq(2) - expect(issue_d.reload.iid).to eq(3) - expect(issue_e.iid).to eq(4) - end - end - context 'when the first model is created for a project after the migration' do it 'generates an iid' do project_a = setup diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb index 220ae705e71..563399ff0d9 100644 --- a/spec/lib/gitlab/database/with_lock_retries_spec.rb +++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb @@ -54,6 +54,10 @@ RSpec.describe Gitlab::Database::WithLockRetries do lock_fiber.resume # start the transaction and lock the table end + after do + lock_fiber.resume if lock_fiber.alive? + end + context 'lock_fiber' do it 'acquires lock successfully' do check_exclusive_lock_query = """ diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index c437b6bcceb..158d472f7ea 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::SearchResults do include ProjectForksHelper include SearchHelpers + using RSpec::Parameterized::TableSyntax let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, name: 'foo') } @@ -41,8 +42,6 @@ RSpec.describe Gitlab::SearchResults do end describe '#formatted_count' do - using RSpec::Parameterized::TableSyntax - where(:scope, :count_method, :expected) do 'projects' | :limited_projects_count | max_limited_count 'issues' | :limited_issues_count | max_limited_count @@ -61,8 +60,6 @@ RSpec.describe Gitlab::SearchResults do end describe '#highlight_map' do - using RSpec::Parameterized::TableSyntax - where(:scope, :expected) do 'projects' | {} 'issues' | {} @@ -80,8 +77,6 @@ RSpec.describe Gitlab::SearchResults do end describe '#formatted_limited_count' do - using RSpec::Parameterized::TableSyntax - where(:count, :expected) do 23 | '23' 99 | '99' diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb index 1052d4cbacc..665eebdfd9e 100644 --- a/spec/lib/gitlab/utils_spec.rb +++ b/spec/lib/gitlab/utils_spec.rb @@ -116,8 +116,6 @@ RSpec.describe Gitlab::Utils do end describe '.ms_to_round_sec' do - using RSpec::Parameterized::TableSyntax - where(:original, :expected) do 1999.8999 | 1.9999 12384 | 12.384 @@ -169,8 +167,6 @@ RSpec.describe Gitlab::Utils do end describe '.remove_line_breaks' do - using RSpec::Parameterized::TableSyntax - where(:original, :expected) do "foo\nbar\nbaz" | "foobarbaz" "foo\r\nbar\r\nbaz" | "foobarbaz" @@ -281,8 +277,6 @@ RSpec.describe Gitlab::Utils do end describe '.append_path' do - using RSpec::Parameterized::TableSyntax - where(:host, :path, :result) do 'http://test/' | '/foo/bar' | 'http://test/foo/bar' 'http://test/' | '//foo/bar' | 'http://test/foo/bar' @@ -393,8 +387,6 @@ RSpec.describe Gitlab::Utils do end describe ".safe_downcase!" do - using RSpec::Parameterized::TableSyntax - where(:str, :result) do "test".freeze | "test" "Test".freeze | "test" diff --git a/spec/lib/peek/views/external_http_spec.rb b/spec/lib/peek/views/external_http_spec.rb index b90bb74c313..cc1813db622 100644 --- a/spec/lib/peek/views/external_http_spec.rb +++ b/spec/lib/peek/views/external_http_spec.rb @@ -84,4 +84,108 @@ RSpec.describe Peek::Views::ExternalHttp, :request_store do results[:details].map { |data| data.slice(:duration, :label, :code, :proxy, :error, :warnings) } ).to match_array(expected) end + + context 'when the host is in IPv4 format' do + it 'displays IPv4 in the label' do + subscriber.request( + double(:event, payload: { + method: 'POST', code: "200", duration: 0.03, + scheme: 'https', host: '1.2.3.4', port: 80, path: '/api/v4/projects', + query: 'current=true' + }) + ) + expect( + subject.results[:details].map { |data| data.slice(:duration, :label, :code, :proxy, :error, :warnings) } + ).to match_array( + [ + { + duration: 30.0, + label: "POST https://1.2.3.4:80/api/v4/projects?current=true", + code: "Response status: 200", + proxy: nil, + error: nil, + warnings: [] + } + ] + ) + end + end + + context 'when the host is in IPv6 foramat' do + it 'displays IPv6 in the label' do + subscriber.request( + double(:event, payload: { + method: 'POST', code: "200", duration: 0.03, + scheme: 'https', host: '2606:4700:90:0:f22e:fbec:5bed:a9b9', port: 80, path: '/api/v4/projects', + query: 'current=true' + }) + ) + expect( + subject.results[:details].map { |data| data.slice(:duration, :label, :code, :proxy, :error, :warnings) } + ).to match_array( + [ + { + duration: 30.0, + label: "POST https://[2606:4700:90:0:f22e:fbec:5bed:a9b9]:80/api/v4/projects?current=true", + code: "Response status: 200", + proxy: nil, + error: nil, + warnings: [] + } + ] + ) + end + end + + context 'when the host is invalid' do + it 'displays unknown in the label' do + subscriber.request( + double(:event, payload: { + method: 'POST', code: "200", duration: 0.03, + scheme: 'https', host: '!@#%!@#%!@#%', port: 80, path: '/api/v4/projects', + query: 'current=true' + }) + ) + expect( + subject.results[:details].map { |data| data.slice(:duration, :label, :code, :proxy, :error, :warnings) } + ).to match_array( + [ + { + duration: 30.0, + label: "POST unknown", + code: "Response status: 200", + proxy: nil, + error: nil, + warnings: [] + } + ] + ) + end + end + + context 'when another URI component is invalid' do + it 'displays unknown in the label' do + subscriber.request( + double(:event, payload: { + method: 'POST', code: "200", duration: 0.03, + scheme: 'https', host: 'invalid', port: 'invalid', path: '/api/v4/projects', + query: 'current=true' + }) + ) + expect( + subject.results[:details].map { |data| data.slice(:duration, :label, :code, :proxy, :error, :warnings) } + ).to match_array( + [ + { + duration: 30.0, + label: "POST unknown", + code: "Response status: 200", + proxy: nil, + error: nil, + warnings: [] + } + ] + ) + end + end end diff --git a/spec/models/concerns/atomic_internal_id_spec.rb b/spec/models/concerns/atomic_internal_id_spec.rb index 5ee3c012dc9..35b0f107676 100644 --- a/spec/models/concerns/atomic_internal_id_spec.rb +++ b/spec/models/concerns/atomic_internal_id_spec.rb @@ -87,6 +87,158 @@ RSpec.describe AtomicInternalId do end end + describe '#clear_scope_iid!' do + context 'when no ensure_if condition is given' do + it 'clears automatically set IIDs' do + expect(milestone).to receive(:clear_project_iid!).and_call_original + + expect_iid_to_be_set_and_rollback(milestone) + + expect(milestone.iid).to be_nil + end + + it 'does not clear manually set IIDS' do + milestone.iid = external_iid + + expect(milestone).to receive(:clear_project_iid!).and_call_original + + expect_iid_to_be_set_and_rollback(milestone) + + expect(milestone.iid).to eq(external_iid) + end + end + + context 'when an ensure_if condition is given' do + let(:test_class) do + Class.new(ApplicationRecord) do + include AtomicInternalId + include Importable + + self.table_name = :milestones + + belongs_to :project + + has_internal_id :iid, scope: :project, track_if: -> { !importing }, ensure_if: -> { !importing } + + def self.name + 'TestClass' + end + end + end + + let(:instance) { test_class.new(milestone.attributes) } + + context 'when the ensure_if condition evaluates to true' do + it 'clears automatically set IIDs' do + expect(instance).to receive(:clear_project_iid!).and_call_original + + expect_iid_to_be_set_and_rollback(instance) + + expect(instance.iid).to be_nil + end + + it 'does not clear manually set IIDs' do + instance.iid = external_iid + + expect(instance).to receive(:clear_project_iid!).and_call_original + + expect_iid_to_be_set_and_rollback(instance) + + expect(instance.iid).to eq(external_iid) + end + end + + context 'when the ensure_if condition evaluates to false' do + before do + instance.importing = true + end + + it 'does not clear IIDs' do + instance.iid = external_iid + + expect(instance).not_to receive(:clear_project_iid!) + + expect_iid_to_be_set_and_rollback(instance) + + expect(instance.iid).to eq(external_iid) + end + end + end + + def expect_iid_to_be_set_and_rollback(instance) + ActiveRecord::Base.transaction(requires_new: true) do + instance.save! + + expect(instance.iid).not_to be_nil + + raise ActiveRecord::Rollback + end + end + end + + describe '#validate_scope_iid_exists!' do + let(:test_class) do + Class.new(ApplicationRecord) do + include AtomicInternalId + include Importable + + self.table_name = :milestones + + belongs_to :project + + def self.name + 'TestClass' + end + end + end + + let(:instance) { test_class.new(milestone.attributes) } + + before do + test_class.has_internal_id :iid, scope: :project, presence: presence, ensure_if: -> { !importing } + + instance.importing = true + end + + context 'when the presence flag is set' do + let(:presence) { true } + + it 'raises an error for blank iids on create' do + expect do + instance.save! + end.to raise_error(described_class::MissingValueError, 'iid was unexpectedly blank!') + end + + it 'raises an error for blank iids on update' do + instance.iid = 100 + instance.save! + + instance.iid = nil + + expect do + instance.save! + end.to raise_error(described_class::MissingValueError, 'iid was unexpectedly blank!') + end + end + + context 'when the presence flag is not set' do + let(:presence) { false } + + it 'does not raise an error for blank iids on create' do + expect { instance.save! }.not_to raise_error + end + + it 'does not raise an error for blank iids on update' do + instance.iid = 100 + instance.save! + + instance.iid = nil + + expect { instance.save! }.not_to raise_error + end + end + end + describe '.with_project_iid_supply' do let(:iid) { 100 } diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index ff5b270cf33..3545c8e9686 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Issuable do include ProjectForksHelper + using RSpec::Parameterized::TableSyntax let(:issuable_class) { Issue } let(:issue) { create(:issue, title: 'An issue', description: 'A description') } @@ -45,13 +46,17 @@ RSpec.describe Issuable do end it { is_expected.to validate_presence_of(:project) } - it { is_expected.to validate_presence_of(:iid) } it { is_expected.to validate_presence_of(:author) } it { is_expected.to validate_presence_of(:title) } it { is_expected.to validate_length_of(:title).is_at_most(described_class::TITLE_LENGTH_MAX) } it { is_expected.to validate_length_of(:description).is_at_most(described_class::DESCRIPTION_LENGTH_MAX).on(:create) } - it_behaves_like 'validates description length with custom validation' + it_behaves_like 'validates description length with custom validation' do + before do + allow(InternalId).to receive(:generate_next).and_call_original + end + end + it_behaves_like 'truncates the description to its allowed maximum length on import' end end @@ -820,8 +825,6 @@ RSpec.describe Issuable do end describe '#supports_time_tracking?' do - using RSpec::Parameterized::TableSyntax - where(:issuable_type, :supports_time_tracking) do :issue | true :incident | true @@ -838,8 +841,6 @@ RSpec.describe Issuable do end describe '#supports_severity?' do - using RSpec::Parameterized::TableSyntax - where(:issuable_type, :supports_severity) do :issue | false :incident | true @@ -856,8 +857,6 @@ RSpec.describe Issuable do end describe '#incident?' do - using RSpec::Parameterized::TableSyntax - where(:issuable_type, :incident) do :issue | false :incident | true @@ -874,8 +873,6 @@ RSpec.describe Issuable do end describe '#supports_issue_type?' do - using RSpec::Parameterized::TableSyntax - where(:issuable_type, :supports_issue_type) do :issue | true :merge_request | false @@ -894,8 +891,6 @@ RSpec.describe Issuable do subject { issuable.severity } context 'when issuable is not an incident' do - using RSpec::Parameterized::TableSyntax - where(:issuable_type, :severity) do :issue | 'unknown' :merge_request | 'unknown' diff --git a/spec/models/experiment_spec.rb b/spec/models/experiment_spec.rb index 171bfd116d3..22bbf2df8fd 100644 --- a/spec/models/experiment_spec.rb +++ b/spec/models/experiment_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Experiment do + include AfterNextHelpers + subject { build(:experiment) } describe 'associations' do @@ -67,6 +69,33 @@ RSpec.describe Experiment do end end + describe '.add_group' do + let_it_be(:experiment_name) { :experiment_key } + let_it_be(:variant) { :control } + let_it_be(:group) { build(:group) } + + subject(:add_group) { described_class.add_group(experiment_name, variant: variant, group: group) } + + context 'when an experiment with the provided name does not exist' do + it 'creates a new experiment record' do + allow_next(described_class, name: :experiment_key) + .to receive(:record_group_and_variant!).with(group, variant) + + expect { add_group }.to change(described_class, :count).by(1) + end + end + + context 'when an experiment with the provided name already exists' do + before do + create(:experiment, name: experiment_name) + end + + it 'does not create a new experiment record' do + expect { add_group }.not_to change(described_class, :count) + end + end + end + describe '.record_conversion_event' do let_it_be(:user) { build(:user) } @@ -136,6 +165,34 @@ RSpec.describe Experiment do end end + describe '#record_group_and_variant!' do + let_it_be(:group) { create(:group) } + let_it_be(:variant) { :control } + let_it_be(:experiment) { create(:experiment) } + + subject(:record_group_and_variant!) { experiment.record_group_and_variant!(group, variant) } + + context 'when no existing experiment_subject record exists for the given group' do + it 'creates an experiment_subject record' do + expect_next(ExperimentSubject).to receive(:update!).with(variant: variant).and_call_original + + expect { record_group_and_variant! }.to change(ExperimentSubject, :count).by(1) + end + end + + context 'when an existing experiment_subject exists for the given group' do + context 'but it belonged to a different variant' do + let!(:experiment_subject) do + create(:experiment_subject, experiment: experiment, group: group, user: nil, variant: :experimental) + end + + it 'updates the variant value' do + expect { record_group_and_variant! }.to change { experiment_subject.reload.variant }.to('control') + end + end + end + end + describe '#record_user_and_group' do let_it_be(:experiment) { create(:experiment) } let_it_be(:user) { create(:user) } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 9ba379167ee..d82f95bb1bb 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -271,8 +271,6 @@ RSpec.describe MergeRequest, factory_default: :keep do stub_feature_flags(stricter_mr_branch_name: false) end - using RSpec::Parameterized::TableSyntax - where(:branch_name, :valid) do 'foo' | true 'foo:bar' | false @@ -2778,8 +2776,6 @@ RSpec.describe MergeRequest, factory_default: :keep do end context 'with skip_ci_check option' do - using RSpec::Parameterized::TableSyntax - before do allow(subject).to receive_messages(check_mergeability: nil, can_be_merged?: true, @@ -2803,8 +2799,6 @@ RSpec.describe MergeRequest, factory_default: :keep do end context 'with skip_discussions_check option' do - using RSpec::Parameterized::TableSyntax - before do allow(subject).to receive_messages(mergeable_ci_state?: true, check_mergeability: nil, diff --git a/spec/models/onboarding_progress_spec.rb b/spec/models/onboarding_progress_spec.rb index bd951846bb8..02fe0015a14 100644 --- a/spec/models/onboarding_progress_spec.rb +++ b/spec/models/onboarding_progress_spec.rb @@ -29,6 +29,67 @@ RSpec.describe OnboardingProgress do end end + describe 'scopes' do + describe '.incomplete_actions' do + subject { described_class.incomplete_actions(actions) } + + let!(:no_actions_completed) { create(:onboarding_progress) } + let!(:one_action_completed_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => Time.current) } + + context 'when given one action' do + let(:actions) { action } + + it { is_expected.to eq [no_actions_completed] } + end + + context 'when given an array of actions' do + let(:actions) { [action, :git_write] } + + it { is_expected.to eq [no_actions_completed] } + end + end + + describe '.completed_actions' do + subject { described_class.completed_actions(actions) } + + let!(:one_action_completed_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => Time.current) } + let!(:both_actions_completed) { create(:onboarding_progress, "#{action}_at" => Time.current, git_write_at: Time.current) } + + context 'when given one action' do + let(:actions) { action } + + it { is_expected.to eq [one_action_completed_one_action_incompleted, both_actions_completed] } + end + + context 'when given an array of actions' do + let(:actions) { [action, :git_write] } + + it { is_expected.to eq [both_actions_completed] } + end + end + + describe '.completed_actions_with_latest_in_range' do + subject { described_class.completed_actions_with_latest_in_range(actions, 1.day.ago.beginning_of_day..1.day.ago.end_of_day) } + + let!(:one_action_completed_in_range_one_action_incompleted) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day) } + let!(:git_write_action_completed_in_range) { create(:onboarding_progress, git_write_at: 1.day.ago.middle_of_day) } + let!(:both_actions_completed_latest_action_out_of_range) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: Time.current) } + let!(:both_actions_completed_latest_action_in_range) { create(:onboarding_progress, "#{action}_at" => 1.day.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day) } + + context 'when given one action' do + let(:actions) { :git_write } + + it { is_expected.to eq [git_write_action_completed_in_range] } + end + + context 'when given an array of actions' do + let(:actions) { [action, :git_write] } + + it { is_expected.to eq [both_actions_completed_latest_action_in_range] } + end + end + end + describe '.onboard' do subject(:onboard) { described_class.onboard(namespace) } @@ -104,4 +165,10 @@ RSpec.describe OnboardingProgress do end end end + + describe '.column_name' do + subject { described_class.column_name(action) } + + it { is_expected.to eq(:subscription_created_at) } + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1b24c1f5436..59464c89dc1 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2247,8 +2247,6 @@ RSpec.describe Project, factory_default: :keep do end describe '#ci_config_path=' do - using RSpec::Parameterized::TableSyntax - let(:project) { build_stubbed(:project) } where(:default_ci_config_path, :project_ci_config_path, :expected_ci_config_path) do @@ -3947,7 +3945,6 @@ RSpec.describe Project, factory_default: :keep do describe '.filter_by_feature_visibility' do include_context 'ProjectPolicyTable context' include ProjectHelpers - using RSpec::Parameterized::TableSyntax let_it_be(:group) { create(:group) } let!(:project) { create(:project, project_level, namespace: group ) } @@ -4197,8 +4194,6 @@ RSpec.describe Project, factory_default: :keep do end describe '#git_transfer_in_progress?' do - using RSpec::Parameterized::TableSyntax - let(:project) { build(:project) } subject { project.git_transfer_in_progress? } @@ -5822,8 +5817,6 @@ RSpec.describe Project, factory_default: :keep do end describe 'validation #changing_shared_runners_enabled_is_allowed' do - using RSpec::Parameterized::TableSyntax - where(:shared_runners_setting, :project_shared_runners_enabled, :valid_record) do 'enabled' | true | true 'enabled' | false | true @@ -6046,8 +6039,6 @@ RSpec.describe Project, factory_default: :keep do end describe '#closest_setting' do - using RSpec::Parameterized::TableSyntax - shared_examples_for 'fetching closest setting' do let!(:namespace) { create(:namespace) } let!(:project) { create(:project, namespace: namespace) } diff --git a/spec/models/prometheus_metric_spec.rb b/spec/models/prometheus_metric_spec.rb index 9588167bbcc..a20f4edcf4a 100644 --- a/spec/models/prometheus_metric_spec.rb +++ b/spec/models/prometheus_metric_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe PrometheusMetric do + using RSpec::Parameterized::TableSyntax + subject { build(:prometheus_metric) } it_behaves_like 'having unique enum values' @@ -14,8 +16,6 @@ RSpec.describe PrometheusMetric do it { is_expected.to validate_uniqueness_of(:identifier).scoped_to(:project_id).allow_nil } describe 'common metrics' do - using RSpec::Parameterized::TableSyntax - where(:common, :with_project, :result) do false | true | true false | false | false @@ -34,8 +34,6 @@ RSpec.describe PrometheusMetric do end describe '#query_series' do - using RSpec::Parameterized::TableSyntax - where(:legend, :type) do 'Some other legend' | NilClass 'Status Code' | Array @@ -72,8 +70,6 @@ RSpec.describe PrometheusMetric do end describe '#priority' do - using RSpec::Parameterized::TableSyntax - where(:group, :priority) do :nginx_ingress_vts | 10 :nginx_ingress | 10 @@ -97,8 +93,6 @@ RSpec.describe PrometheusMetric do end describe '#required_metrics' do - using RSpec::Parameterized::TableSyntax - where(:group, :required_metrics) do :nginx_ingress_vts | %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg) :nginx_ingress | %w(nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum) diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb index d162d288129..648d899f1a8 100644 --- a/spec/requests/api/generic_packages_spec.rb +++ b/spec/requests/api/generic_packages_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe API::GenericPackages do include HttpBasicAuthHelpers + using RSpec::Parameterized::TableSyntax let_it_be(:personal_access_token) { create(:personal_access_token) } let_it_be(:project, reload: true) { create(:project) } @@ -76,8 +77,6 @@ RSpec.describe API::GenericPackages do describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name/authorize' do context 'with valid project' do - using RSpec::Parameterized::TableSyntax - where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do 'PUBLIC' | :developer | true | :personal_access_token | :success 'PUBLIC' | :guest | true | :personal_access_token | :forbidden @@ -194,8 +193,6 @@ RSpec.describe API::GenericPackages do let(:params) { { file: file_upload } } context 'authentication' do - using RSpec::Parameterized::TableSyntax - where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do 'PUBLIC' | :guest | true | :personal_access_token | :forbidden 'PUBLIC' | :guest | true | :user_basic_auth | :forbidden @@ -373,8 +370,6 @@ RSpec.describe API::GenericPackages do end context 'application security' do - using RSpec::Parameterized::TableSyntax - where(:param_name, :param_value) do :package_name | 'my-package/../' :package_name | 'my-package%2f%2e%2e%2f' @@ -404,8 +399,6 @@ RSpec.describe API::GenericPackages do end describe 'GET /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do - using RSpec::Parameterized::TableSyntax - let_it_be(:package) { create(:generic_package, project: project) } let_it_be(:package_file) { create(:package_file, :generic, package: package) } @@ -527,8 +520,6 @@ RSpec.describe API::GenericPackages do end context 'application security' do - using RSpec::Parameterized::TableSyntax - where(:param_name, :param_value) do :package_name | 'my-package/../' :package_name | 'my-package%2f%2e%2e%2f' diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb index 72a470dca4b..94ecd177890 100644 --- a/spec/requests/api/pypi_packages_spec.rb +++ b/spec/requests/api/pypi_packages_spec.rb @@ -5,6 +5,7 @@ RSpec.describe API::PypiPackages do include WorkhorseHelpers include PackagesManagerApiSpecHelpers include HttpBasicAuthHelpers + using RSpec::Parameterized::TableSyntax let_it_be(:user) { create(:user) } let_it_be(:project, reload: true) { create(:project, :public) } @@ -20,8 +21,6 @@ RSpec.describe API::PypiPackages do subject { get api(url) } context 'with valid project' do - using RSpec::Parameterized::TableSyntax - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'PyPI package versions' | :success 'PUBLIC' | :guest | true | true | 'PyPI package versions' | :success @@ -83,8 +82,6 @@ RSpec.describe API::PypiPackages do subject { post api(url), headers: headers } context 'with valid project' do - using RSpec::Parameterized::TableSyntax - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'process PyPI api request' | :success 'PUBLIC' | :guest | true | true | 'process PyPI api request' | :forbidden @@ -149,8 +146,6 @@ RSpec.describe API::PypiPackages do end context 'with valid project' do - using RSpec::Parameterized::TableSyntax - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'PyPI package creation' | :created 'PUBLIC' | :guest | true | true | 'process PyPI api request' | :forbidden @@ -239,8 +234,6 @@ RSpec.describe API::PypiPackages do subject { get api(url) } context 'with valid project' do - using RSpec::Parameterized::TableSyntax - where(:project_visibility_level, :user_role, :member, :user_token, :shared_examples_name, :expected_status) do 'PUBLIC' | :developer | true | true | 'PyPI package download' | :success 'PUBLIC' | :guest | true | true | 'PyPI package download' | :success diff --git a/spec/services/namespaces/in_product_marketing_emails_service_spec.rb b/spec/services/namespaces/in_product_marketing_emails_service_spec.rb new file mode 100644 index 00000000000..7346a5b95ae --- /dev/null +++ b/spec/services/namespaces/in_product_marketing_emails_service_spec.rb @@ -0,0 +1,159 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Namespaces::InProductMarketingEmailsService, '#execute' do + subject(:execute_service) { described_class.new(track, interval).execute } + + let(:track) { :create } + let(:interval) { 1 } + + let(:previous_action_completed_at) { 2.days.ago.middle_of_day } + let(:current_action_completed_at) { nil } + let(:experiment_enabled) { true } + let(:user_can_perform_current_track_action) { true } + let(:actions_completed) { { created_at: previous_action_completed_at, git_write_at: current_action_completed_at } } + + let_it_be(:group) { create(:group) } + let_it_be(:user) { create(:user, email_opted_in: true) } + + before do + create(:onboarding_progress, namespace: group, **actions_completed) + group.add_developer(user) + stub_experiment_for_subject(in_product_marketing_emails: experiment_enabled) + allow(Ability).to receive(:allowed?).with(user, anything, anything).and_return(user_can_perform_current_track_action) + allow(Notify).to receive(:in_product_marketing_email).and_return(double(deliver_later: nil)) + end + + RSpec::Matchers.define :send_in_product_marketing_email do |*args| + match do + expect(Notify).to have_received(:in_product_marketing_email).with(*args).once + end + + match_when_negated do + expect(Notify).not_to have_received(:in_product_marketing_email) + end + end + + context 'for each track and series with the right conditions' do + using RSpec::Parameterized::TableSyntax + + where(:track, :interval, :actions_completed) do + :create | 1 | { created_at: 2.days.ago.middle_of_day } + :create | 5 | { created_at: 6.days.ago.middle_of_day } + :create | 10 | { created_at: 11.days.ago.middle_of_day } + :verify | 1 | { created_at: 2.days.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day } + :verify | 5 | { created_at: 6.days.ago.middle_of_day, git_write_at: 6.days.ago.middle_of_day } + :verify | 10 | { created_at: 11.days.ago.middle_of_day, git_write_at: 11.days.ago.middle_of_day } + :trial | 1 | { created_at: 2.days.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day, pipeline_created_at: 2.days.ago.middle_of_day } + :trial | 5 | { created_at: 6.days.ago.middle_of_day, git_write_at: 6.days.ago.middle_of_day, pipeline_created_at: 6.days.ago.middle_of_day } + :trial | 10 | { created_at: 11.days.ago.middle_of_day, git_write_at: 11.days.ago.middle_of_day, pipeline_created_at: 11.days.ago.middle_of_day } + :team | 1 | { created_at: 2.days.ago.middle_of_day, git_write_at: 2.days.ago.middle_of_day, pipeline_created_at: 2.days.ago.middle_of_day, trial_started_at: 2.days.ago.middle_of_day } + :team | 5 | { created_at: 6.days.ago.middle_of_day, git_write_at: 6.days.ago.middle_of_day, pipeline_created_at: 6.days.ago.middle_of_day, trial_started_at: 6.days.ago.middle_of_day } + :team | 10 | { created_at: 11.days.ago.middle_of_day, git_write_at: 11.days.ago.middle_of_day, pipeline_created_at: 11.days.ago.middle_of_day, trial_started_at: 11.days.ago.middle_of_day } + end + + with_them do + it { is_expected.to send_in_product_marketing_email(user.id, group.id, track, described_class::INTERVAL_DAYS.index(interval)) } + end + end + + context 'when initialized with a different track' do + let(:track) { :verify } + + it { is_expected.not_to send_in_product_marketing_email } + + context 'when the previous track actions have been completed' do + let(:current_action_completed_at) { 2.days.ago.middle_of_day } + + it { is_expected.to send_in_product_marketing_email(user.id, group.id, :verify, 0) } + end + end + + context 'when initialized with a different interval' do + let(:interval) { 5 } + + it { is_expected.not_to send_in_product_marketing_email } + + context 'when the previous track action was completed within the intervals range' do + let(:previous_action_completed_at) { 6.days.ago.middle_of_day } + + it { is_expected.to send_in_product_marketing_email(user.id, group.id, :create, 1) } + end + end + + describe 'experimentation' do + context 'when the experiment is enabled' do + it 'adds the group as an experiment subject in the experimental group' do + expect(Experiment).to receive(:add_group) + .with(:in_product_marketing_emails, variant: :experimental, group: group) + + execute_service + end + end + + context 'when the experiment is disabled' do + let(:experiment_enabled) { false } + + it 'adds the group as an experiment subject in the control group' do + expect(Experiment).to receive(:add_group) + .with(:in_product_marketing_emails, variant: :control, group: group) + + execute_service + end + + it { is_expected.not_to send_in_product_marketing_email } + end + end + + context 'when the previous track action is not yet completed' do + let(:previous_action_completed_at) { nil } + + it { is_expected.not_to send_in_product_marketing_email } + end + + context 'when the previous track action is completed outside the intervals range' do + let(:previous_action_completed_at) { 3.days.ago } + + it { is_expected.not_to send_in_product_marketing_email } + end + + context 'when the current track action is completed' do + let(:current_action_completed_at) { Time.current } + + it { is_expected.not_to send_in_product_marketing_email } + end + + context "when the user cannot perform the current track's action" do + let(:user_can_perform_current_track_action) { false } + + it { is_expected.not_to send_in_product_marketing_email } + end + + context 'when the user has not opted into marketing emails' do + let(:user) { create(:user, email_opted_in: false) } + + it { is_expected.not_to send_in_product_marketing_email } + end + + context 'when the user has already received a marketing email as part of another group' do + before do + other_group = create(:group) + other_group.add_developer(user) + create(:onboarding_progress, namespace: other_group, created_at: previous_action_completed_at, git_write_at: current_action_completed_at) + end + + # For any group Notify is called exactly once + it { is_expected.to send_in_product_marketing_email(user.id, anything, :create, 0) } + end + + context 'when invoked with a non existing track' do + let(:track) { :foo } + + before do + stub_const("#{described_class}::TRACKS", { foo: :git_write }) + end + + it { expect { subject }.to raise_error(NotImplementedError, 'No ability defined for track foo') } + end +end diff --git a/spec/services/projects/prometheus/alerts/notify_service_spec.rb b/spec/services/projects/prometheus/alerts/notify_service_spec.rb index 8ae47ec266c..e196220eabe 100644 --- a/spec/services/projects/prometheus/alerts/notify_service_spec.rb +++ b/spec/services/projects/prometheus/alerts/notify_service_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Projects::Prometheus::Alerts::NotifyService do include PrometheusHelpers + using RSpec::Parameterized::TableSyntax let_it_be(:project, reload: true) { create(:project) } @@ -61,8 +62,6 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do end context 'with project specific cluster' do - using RSpec::Parameterized::TableSyntax - where(:cluster_enabled, :status, :configured_token, :token_input, :result) do true | :installed | token | token | :success true | :installed | nil | nil | :success @@ -104,8 +103,6 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do end context 'with manual prometheus installation' do - using RSpec::Parameterized::TableSyntax - where(:alerting_setting, :configured_token, :token_input, :result) do true | token | token | :success true | token | 'x' | :failure @@ -139,8 +136,6 @@ RSpec.describe Projects::Prometheus::Alerts::NotifyService do end context 'with HTTP integration' do - using RSpec::Parameterized::TableSyntax - where(:active, :token, :result) do :active | :valid | :success :active | :invalid | :failure diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb index 446741221b3..b2a7d349ce6 100644 --- a/spec/services/users/build_service_spec.rb +++ b/spec/services/users/build_service_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Users::BuildService do + using RSpec::Parameterized::TableSyntax + describe '#execute' do let(:params) { build_stubbed(:user).slice(:first_name, :last_name, :username, :email, :password) } @@ -72,8 +74,6 @@ RSpec.describe Users::BuildService do end context 'with "user_default_external" application setting' do - using RSpec::Parameterized::TableSyntax - where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do true | nil | 'fl@example.com' | nil | true true | true | 'fl@example.com' | nil | true @@ -192,8 +192,6 @@ RSpec.describe Users::BuildService do end context 'with "user_default_external" application setting' do - using RSpec::Parameterized::TableSyntax - where(:user_default_external, :external, :email, :user_default_internal_regex, :result) do true | nil | 'fl@example.com' | nil | true true | true | 'fl@example.com' | nil | true diff --git a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb index fe99b1cacd9..42f82987989 100644 --- a/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb +++ b/spec/support/shared_examples/models/atomic_internal_id_shared_examples.rb @@ -9,19 +9,30 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| end describe 'Validation' do - before do - allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!") - - instance.valid? - end - context 'when presence validation is required' do before do skip unless validate_presence end - it 'validates presence' do - expect(instance.errors[internal_id_attribute]).to include("can't be blank") + context 'when creating an object' do + before do + allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!") + end + + it 'raises an error if the internal id is blank' do + expect { instance.save! }.to raise_error(AtomicInternalId::MissingValueError) + end + end + + context 'when updating an object' do + it 'raises an error if the internal id is blank' do + instance.save! + + write_internal_id(nil) + allow(instance).to receive(:"ensure_#{scope}_#{internal_id_attribute}!") + + expect { instance.save! }.to raise_error(AtomicInternalId::MissingValueError) + end end end @@ -30,8 +41,27 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| skip if validate_presence end - it 'does not validate presence' do - expect(instance.errors[internal_id_attribute]).to be_empty + context 'when creating an object' do + before do + allow_any_instance_of(described_class).to receive(:"ensure_#{scope}_#{internal_id_attribute}!") + end + + it 'does not raise an error if the internal id is blank' do + expect(read_internal_id).to be_nil + + expect { instance.save! }.not_to raise_error + end + end + + context 'when updating an object' do + it 'does not raise an error if the internal id is blank' do + instance.save! + + write_internal_id(nil) + allow(instance).to receive(:"ensure_#{scope}_#{internal_id_attribute}!") + + expect { instance.save! }.not_to raise_error + end end end end @@ -76,6 +106,51 @@ RSpec.shared_examples 'AtomicInternalId' do |validate_presence: true| end end + describe 'unsetting the instance internal id on rollback' do + context 'when the internal id has been changed' do + context 'when the internal id is automatically set' do + it 'clears it on the instance' do + expect_iid_to_be_set_and_rollback + + expect(read_internal_id).to be_nil + end + end + + context 'when the internal id is manually set' do + it 'does not clear it on the instance' do + write_internal_id(100) + + expect_iid_to_be_set_and_rollback + + expect(read_internal_id).not_to be_nil + end + end + end + + context 'when the internal id has not been changed' do + it 'preserves the value on the instance' do + instance.save! + original_id = read_internal_id + + expect(original_id).not_to be_nil + + expect_iid_to_be_set_and_rollback + + expect(read_internal_id).to eq(original_id) + end + end + + def expect_iid_to_be_set_and_rollback + ActiveRecord::Base.transaction(requires_new: true) do + instance.save! + + expect(read_internal_id).not_to be_nil + + raise ActiveRecord::Rollback + end + end + end + describe 'supply of internal ids' do let(:scope_value) { scope_attrs.each_value.first } let(:method_name) { :"with_#{scope}_#{internal_id_attribute}_supply" } diff --git a/spec/tasks/gitlab/pages_rake_spec.rb b/spec/tasks/gitlab/pages_rake_spec.rb index 81b1bd92820..9c26d3d73c8 100644 --- a/spec/tasks/gitlab/pages_rake_spec.rb +++ b/spec/tasks/gitlab/pages_rake_spec.rb @@ -2,7 +2,7 @@ require 'rake_helper' -RSpec.describe 'gitlab:pages:migrate_legacy_storagerake task', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/300123' do +RSpec.describe 'gitlab:pages:migrate_legacy_storagerake task' do before(:context) do Rake.application.rake_require 'tasks/gitlab/pages' end diff --git a/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb b/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb new file mode 100644 index 00000000000..722ecfc1dec --- /dev/null +++ b/spec/workers/namespaces/in_product_marketing_emails_worker_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Namespaces::InProductMarketingEmailsWorker, '#perform' do + context 'when the experiment is inactive' do + before do + stub_experiment(in_product_marketing_emails: false) + end + + it 'does not execute the in product marketing emails service' do + expect(Namespaces::InProductMarketingEmailsService).not_to receive(:send_for_all_tracks_and_intervals) + + subject.perform + end + end + + context 'when the experiment is active' do + before do + stub_experiment(in_product_marketing_emails: true) + end + + it 'calls the send_for_all_tracks_and_intervals method on the in product marketing emails service' do + expect(Namespaces::InProductMarketingEmailsService).to receive(:send_for_all_tracks_and_intervals) + + subject.perform + end + end +end diff --git a/yarn.lock b/yarn.lock index 0f3504345d9..72fe9d33e45 100644 --- a/yarn.lock +++ b/yarn.lock @@ -845,10 +845,10 @@ resolved "https://registry.yarnpkg.com/@gitlab/at.js/-/at.js-1.5.7.tgz#1ee6f838cc4410a1d797770934df91d90df8179e" integrity sha512-c6ySRK/Ma7lxwpIVbSAF3P+xiTLrNTGTLRx4/pHK111AdFxwgUwrYF6aVZFXvmG65jHOJHoa0eQQ21RW6rm0Rg== -"@gitlab/eslint-plugin@7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-7.0.0.tgz#3c46d88dde2f7aa0be2a7df5af8e593006becea9" - integrity sha512-XqISaNqQwJ12jTanESvAFNVAniqFN/UFKj068ESiNumlsxnQA36V945wZ6LnwI7WgSCGQCUfHi9MEgyjUvuvdg== +"@gitlab/eslint-plugin@7.0.2": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@gitlab/eslint-plugin/-/eslint-plugin-7.0.2.tgz#cb2ca7a54ed1f8c274829e050de720dbd0fc36d4" + integrity sha512-ypFd4b5PL6mPFQHL9APU/Sq8KBa7GbPGgwnQgGEce9iioiriXFazBsoNgyBIHapCS0DIkYq+kC+pdIB+5s+ypA== dependencies: babel-eslint "^10.0.3" eslint-config-airbnb-base "^14.0.0" @@ -858,7 +858,7 @@ eslint-plugin-import "^2.20.1" eslint-plugin-jest "^23.8.2" eslint-plugin-promise "^4.2.1" - eslint-plugin-vue "^7.4.1" + eslint-plugin-vue "^7.5.0" vue-eslint-parser "^7.0.0" "@gitlab/favicon-overlay@2.0.0": @@ -4652,15 +4652,15 @@ eslint-plugin-promise@^4.2.1: resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== -eslint-plugin-vue@^7.4.1: - version "7.4.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-7.4.1.tgz#2526ef0c010c218824a89423dbe6ddbe76f04fd6" - integrity sha512-W/xPNHYIkGJphLUM2UIYYGKbRw3BcDoMIPY9lu1TTa2YLiZoxurddfnmOP+UOVywxb5vi438ejzwvKdZqydtIw== +eslint-plugin-vue@^7.5.0: + version "7.5.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-vue/-/eslint-plugin-vue-7.5.0.tgz#cc6d983eb22781fa2440a7573cf39af439bb5725" + integrity sha512-QnMMTcyV8PLxBz7QQNAwISSEs6LYk2LJvGlxalXvpCtfKnqo7qcY0aZTIxPe8QOnHd7WCwiMZLOJzg6A03T0Gw== dependencies: eslint-utils "^2.1.0" natural-compare "^1.4.0" semver "^7.3.2" - vue-eslint-parser "^7.3.0" + vue-eslint-parser "^7.4.1" eslint-rule-composer@^0.3.0: version "0.3.0" @@ -5161,16 +5161,7 @@ find-cache-dir@^2.0.0, find-cache-dir@^2.1.0: make-dir "^2.0.0" pkg-dir "^3.0.0" -find-cache-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.0.0.tgz#cd4b7dd97b7185b7e17dbfe2d6e4115ee3eeb8fc" - integrity sha512-t7ulV1fmbxh5G9l/492O1p5+EBbr3uwpt6odhFTMc+nWyhmbloe+ja9BZ8pIBtqFWhOmCWVjx+pTW4zDkFoclw== - dependencies: - commondir "^1.0.1" - make-dir "^3.0.0" - pkg-dir "^4.1.0" - -find-cache-dir@^3.3.1: +find-cache-dir@^3.0.0, find-cache-dir@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== @@ -7980,14 +7971,7 @@ make-dir@^2.0.0, make-dir@^2.1.0: pify "^4.0.1" semver "^5.6.0" -make-dir@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.0.tgz#1b5f39f6b9270ed33f9f054c5c0f84304989f801" - integrity sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw== - dependencies: - semver "^6.0.0" - -make-dir@^3.0.2: +make-dir@^3.0.0, make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== @@ -9937,14 +9921,7 @@ quick-lru@^1.0.0: resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= -randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5: - version "2.0.6" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.0.6.tgz#d302c522948588848a8d300c932b44c24231da80" - integrity sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A== - dependencies: - safe-buffer "^5.1.0" - -randombytes@^2.1.0: +randombytes@^2.0.0, randombytes@^2.0.1, randombytes@^2.0.5, randombytes@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== @@ -10464,14 +10441,7 @@ rfdc@^1.1.4: resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.1.4.tgz#ba72cc1367a0ccd9cf81a870b3b58bd3ad07f8c2" integrity sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug== -rimraf@2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.3: - version "2.7.1" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" - integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== - dependencies: - glob "^7.1.3" - -rimraf@2.6.3: +rimraf@2, rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== @@ -12473,10 +12443,10 @@ vue-apollo@^3.0.3: serialize-javascript "^2.1.0" throttle-debounce "^2.1.0" -vue-eslint-parser@^7.0.0, vue-eslint-parser@^7.3.0: - version "7.3.0" - resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.3.0.tgz#894085839d99d81296fa081d19643733f23d7559" - integrity sha512-n5PJKZbyspD0+8LnaZgpEvNCrjQx1DyDHw8JdWwoxhhC+yRip4TAvSDpXGf9SWX6b0umeB5aR61gwUo6NVvFxw== +vue-eslint-parser@^7.0.0, vue-eslint-parser@^7.4.1: + version "7.4.1" + resolved "https://registry.yarnpkg.com/vue-eslint-parser/-/vue-eslint-parser-7.4.1.tgz#e4adcf7876a7379758d9056a72235af18a587f92" + integrity sha512-AFvhdxpFvliYq1xt/biNBslTHE/zbEvSnr1qfHA/KxRIpErmEDrQZlQnvEexednRHmLfDNOMuDYwZL5xkLzIXQ== dependencies: debug "^4.1.1" eslint-scope "^5.0.0" |