diff options
152 files changed, 2560 insertions, 351 deletions
diff --git a/app/assets/javascripts/analytics/instance_statistics/components/app.vue b/app/assets/javascripts/analytics/instance_statistics/components/app.vue index eb0b67a1629..64c1a2565be 100644 --- a/app/assets/javascripts/analytics/instance_statistics/components/app.vue +++ b/app/assets/javascripts/analytics/instance_statistics/components/app.vue @@ -1,14 +1,19 @@ <script> import InstanceCounts from './instance_counts.vue'; +import PipelinesChart from './pipelines_chart.vue'; export default { name: 'InstanceStatisticsApp', components: { InstanceCounts, + PipelinesChart, }, }; </script> <template> - <instance-counts /> + <div> + <instance-counts /> + <pipelines-chart /> + </div> </template> diff --git a/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue b/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue new file mode 100644 index 00000000000..279fcfe736f --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/components/pipelines_chart.vue @@ -0,0 +1,216 @@ +<script> +import { GlLineChart } from '@gitlab/ui/dist/charts'; +import { GlAlert } from '@gitlab/ui'; +import { mapKeys, mapValues, pick, some, sum } from 'lodash'; +import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; +import { s__ } from '~/locale'; +import { formatDateAsMonth, getDayDifference } from '~/lib/utils/datetime_utility'; +import { getAverageByMonth, sortByDate, extractValues } from '../utils'; +import pipelineStatsQuery from '../graphql/queries/pipeline_stats.query.graphql'; +import { TODAY, START_DATE } from '../constants'; + +const DATA_KEYS = [ + 'pipelinesTotal', + 'pipelinesSucceeded', + 'pipelinesFailed', + 'pipelinesCanceled', + 'pipelinesSkipped', +]; +const PREFIX = 'pipelines'; + +export default { + name: 'PipelinesChart', + components: { + GlLineChart, + GlAlert, + ChartSkeletonLoader, + }, + startDate: START_DATE, + endDate: TODAY, + i18n: { + loadPipelineChartError: s__( + 'InstanceAnalytics|Could not load the pipelines chart. Please refresh the page to try again.', + ), + noDataMessage: s__('InstanceAnalytics|There is no data available.'), + total: s__('InstanceAnalytics|Total'), + succeeded: s__('InstanceAnalytics|Succeeded'), + failed: s__('InstanceAnalytics|Failed'), + canceled: s__('InstanceAnalytics|Canceled'), + skipped: s__('InstanceAnalytics|Skipped'), + chartTitle: s__('InstanceAnalytics|Pipelines'), + yAxisTitle: s__('InstanceAnalytics|Items'), + xAxisTitle: s__('InstanceAnalytics|Month'), + }, + data() { + return { + loading: true, + loadingError: null, + }; + }, + apollo: { + pipelineStats: { + query: pipelineStatsQuery, + variables() { + return { + firstTotal: this.totalDaysToShow, + firstSucceeded: this.totalDaysToShow, + firstFailed: this.totalDaysToShow, + firstCanceled: this.totalDaysToShow, + firstSkipped: this.totalDaysToShow, + }; + }, + update(data) { + const allData = extractValues(data, DATA_KEYS, PREFIX, 'nodes'); + const allPageInfo = extractValues(data, DATA_KEYS, PREFIX, 'pageInfo'); + + return { + ...mapValues(allData, sortByDate), + ...allPageInfo, + }; + }, + result() { + if (this.hasNextPage) { + this.fetchNextPage(); + } + }, + error() { + this.handleError(); + }, + }, + }, + computed: { + isLoading() { + return this.$apollo.queries.pipelineStats.loading; + }, + totalDaysToShow() { + return getDayDifference(this.$options.startDate, this.$options.endDate); + }, + firstVariables() { + const allData = pick(this.pipelineStats, [ + 'nodesTotal', + 'nodesSucceeded', + 'nodesFailed', + 'nodesCanceled', + 'nodesSkipped', + ]); + const allDayDiffs = mapValues(allData, data => { + const firstdataPoint = data[0]; + if (!firstdataPoint) { + return 0; + } + + return Math.max( + 0, + getDayDifference(this.$options.startDate, new Date(firstdataPoint.recordedAt)), + ); + }); + + return mapKeys(allDayDiffs, (value, key) => key.replace('nodes', 'first')); + }, + cursorVariables() { + const pageInfoKeys = [ + 'pageInfoTotal', + 'pageInfoSucceeded', + 'pageInfoFailed', + 'pageInfoCanceled', + 'pageInfoSkipped', + ]; + + return extractValues(this.pipelineStats, pageInfoKeys, 'pageInfo', 'endCursor'); + }, + hasNextPage() { + return ( + sum(Object.values(this.firstVariables)) > 0 && + some(this.pipelineStats, ({ hasNextPage }) => hasNextPage) + ); + }, + hasEmptyDataSet() { + return this.chartData.every(({ data }) => data.length === 0); + }, + chartData() { + const allData = pick(this.pipelineStats, [ + 'nodesTotal', + 'nodesSucceeded', + 'nodesFailed', + 'nodesCanceled', + 'nodesSkipped', + ]); + const options = { shouldRound: true }; + return Object.keys(allData).map(key => { + const i18nName = key.slice('nodes'.length).toLowerCase(); + return { + name: this.$options.i18n[i18nName], + data: getAverageByMonth(allData[key], options), + }; + }); + }, + range() { + return { + min: this.$options.startDate, + max: this.$options.endDate, + }; + }, + differenceInMonths() { + const yearDiff = this.$options.endDate.getYear() - this.$options.startDate.getYear(); + const monthDiff = this.$options.endDate.getMonth() - this.$options.startDate.getMonth(); + + return monthDiff + 12 * yearDiff; + }, + chartOptions() { + return { + xAxis: { + ...this.range, + name: this.$options.i18n.xAxisTitle, + type: 'time', + splitNumber: this.differenceInMonths + 1, + axisLabel: { + interval: 0, + showMinLabel: false, + showMaxLabel: false, + align: 'right', + formatter: formatDateAsMonth, + }, + }, + yAxis: { + name: this.$options.i18n.yAxisTitle, + }, + }; + }, + }, + methods: { + handleError() { + this.loadingError = true; + }, + fetchNextPage() { + this.$apollo.queries.pipelineStats + .fetchMore({ + variables: { + ...this.firstVariables, + ...this.cursorVariables, + }, + updateQuery: (previousResult, { fetchMoreResult }) => { + return Object.keys(fetchMoreResult).reduce((memo, key) => { + const { nodes, ...rest } = fetchMoreResult[key]; + const previousNodes = previousResult[key].nodes; + return { ...memo, [key]: { ...rest, nodes: [...previousNodes, ...nodes] } }; + }, {}); + }, + }) + .catch(this.handleError); + }, + }, +}; +</script> +<template> + <div> + <h3>{{ $options.i18n.chartTitle }}</h3> + <gl-alert v-if="loadingError" variant="danger" :dismissible="false" class="gl-mt-3"> + {{ this.$options.i18n.loadPipelineChartError }} + </gl-alert> + <chart-skeleton-loader v-else-if="isLoading" /> + <gl-alert v-else-if="hasEmptyDataSet" variant="info" :dismissible="false" class="gl-mt-3"> + {{ $options.i18n.noDataMessage }} + </gl-alert> + <gl-line-chart v-else :option="chartOptions" :include-legend-avg-max="true" :data="chartData" /> + </div> +</template> diff --git a/app/assets/javascripts/analytics/instance_statistics/constants.js b/app/assets/javascripts/analytics/instance_statistics/constants.js new file mode 100644 index 00000000000..5ea5d17c974 --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/constants.js @@ -0,0 +1,5 @@ +import { getDateInPast } from '~/lib/utils/datetime_utility'; + +const TOTAL_DAYS_TO_SHOW = 365; +export const TODAY = new Date(); +export const START_DATE = getDateInPast(TODAY, TOTAL_DAYS_TO_SHOW); diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/count.fragment.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/count.fragment.graphql new file mode 100644 index 00000000000..40cef95c2e7 --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/count.fragment.graphql @@ -0,0 +1,4 @@ +fragment Count on InstanceStatisticsMeasurement { + count + recordedAt +} diff --git a/app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql new file mode 100644 index 00000000000..3bf40403f91 --- /dev/null +++ b/app/assets/javascripts/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql @@ -0,0 +1,76 @@ +#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" +#import "./count.fragment.graphql" + +query pipelineStats( + $firstTotal: Int + $firstSucceeded: Int + $firstFailed: Int + $firstCanceled: Int + $firstSkipped: Int + $endCursorTotal: String + $endCursorSucceeded: String + $endCursorFailed: String + $endCursorCanceled: String + $endCursorSkipped: String +) { + pipelinesTotal: instanceStatisticsMeasurements( + identifier: PIPELINES + first: $firstTotal + after: $endCursorTotal + ) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } + pipelinesSucceeded: instanceStatisticsMeasurements( + identifier: PIPELINES_SUCCEEDED + first: $firstSucceeded + after: $endCursorSucceeded + ) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } + pipelinesFailed: instanceStatisticsMeasurements( + identifier: PIPELINES_FAILED + first: $firstFailed + after: $endCursorFailed + ) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } + pipelinesCanceled: instanceStatisticsMeasurements( + identifier: PIPELINES_CANCELED + first: $firstCanceled + after: $endCursorCanceled + ) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } + pipelinesSkipped: instanceStatisticsMeasurements( + identifier: PIPELINES_SKIPPED + first: $firstSkipped + after: $endCursorSkipped + ) { + nodes { + ...Count + } + pageInfo { + ...PageInfo + } + } +} diff --git a/app/assets/javascripts/analytics/instance_statistics/utils.js b/app/assets/javascripts/analytics/instance_statistics/utils.js index 30c6205b7ff..907482c0c72 100644 --- a/app/assets/javascripts/analytics/instance_statistics/utils.js +++ b/app/assets/javascripts/analytics/instance_statistics/utils.js @@ -1,4 +1,5 @@ import { masks } from 'dateformat'; +import { mapKeys, mapValues, pick, sortBy } from 'lodash'; import { formatDate } from '~/lib/utils/datetime_utility'; const { isoDate } = masks; @@ -38,3 +39,31 @@ export function getAverageByMonth(items = [], options = {}) { return [month, avg]; }); } + +/** + * Extracts values given a data set and a set of keys + * @example + * const data = { fooBar: { baz: 'quis' }, ignored: 'ignored' }; + * extractValues(data, ['fooBar'], 'foo', 'baz') => { bazBar: 'quis' } + * @param {Object} data set to extract values from + * @param {Array} dataKeys keys describing where to look for values in the data set + * @param {String} replaceKey name key to be replaced in the data set + * @param {String} nestedKey key nested in the data set to be extracted, + * this is also used to rename the newly created data set + * @return {Object} the newly created data set with the extracted values + */ +export function extractValues(data, dataKeys = [], replaceKey, nestedKey) { + return mapKeys(pick(mapValues(data, nestedKey), dataKeys), (value, key) => + key.replace(replaceKey, nestedKey), + ); +} + +/** + * Creates a new array of items sorted by the date string of each item + * @param {Array} items [description] + * @param {String} items[0] date string + * @return {Array} the new sorted array. + */ +export function sortByDate(items = []) { + return sortBy(items, ({ recordedAt }) => new Date(recordedAt).getTime()); +} diff --git a/app/assets/javascripts/mirrors/mirror_repos.js b/app/assets/javascripts/mirrors/mirror_repos.js index cc787613c52..818ca8aa847 100644 --- a/app/assets/javascripts/mirrors/mirror_repos.js +++ b/app/assets/javascripts/mirrors/mirror_repos.js @@ -4,6 +4,7 @@ import { __ } from '~/locale'; import { deprecatedCreateFlash as Flash } from '~/flash'; import axios from '~/lib/utils/axios_utils'; import SSHMirror from './ssh_mirror'; +import { hide } from '~/tooltips'; export default class MirrorRepos { constructor(container) { @@ -115,7 +116,7 @@ export default class MirrorRepos { /* eslint-disable class-methods-use-this */ removeRow($target) { const row = $target.closest('tr'); - $('.js-delete-mirror', row).tooltip('hide'); + hide($('.js-delete-mirror', row)); row.remove(); } /* eslint-enable class-methods-use-this */ diff --git a/app/assets/javascripts/pages/projects/environments/index/index.js b/app/assets/javascripts/pages/projects/environments/index/index.js index ace8af00ece..4d5106f6d5f 100644 --- a/app/assets/javascripts/pages/projects/environments/index/index.js +++ b/app/assets/javascripts/pages/projects/environments/index/index.js @@ -1,3 +1,3 @@ -import initEnviroments from '~/environments/'; +import initEnvironments from '~/environments/'; -document.addEventListener('DOMContentLoaded', initEnviroments); +document.addEventListener('DOMContentLoaded', initEnvironments); diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js index ce0b5c80927..94a12cc2706 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js @@ -7,16 +7,15 @@ import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered import { FILTERED_SEARCH } from '~/pages/constants'; import { ISSUABLE_INDEX } from '~/pages/projects/constants'; -document.addEventListener('DOMContentLoaded', () => { - addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys); +new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new - initFilteredSearch({ - page: FILTERED_SEARCH.MERGE_REQUESTS, - filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, - useDefaultState: true, - }); +addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys); - new IssuableIndex(ISSUABLE_INDEX.MERGE_REQUEST); // eslint-disable-line no-new - new ShortcutsNavigation(); // eslint-disable-line no-new - new UsersSelect(); // eslint-disable-line no-new +initFilteredSearch({ + page: FILTERED_SEARCH.MERGE_REQUESTS, + filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys, + useDefaultState: true, }); + +new UsersSelect(); // eslint-disable-line no-new +new ShortcutsNavigation(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js index 29ebf656fe1..602d749ee07 100644 --- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js +++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js @@ -5,12 +5,10 @@ import initShow from '../init_merge_request_show'; import initIssuableHeaderWarning from '~/vue_shared/components/issuable/init_issuable_header_warning'; import store from '~/mr_notes/stores'; -document.addEventListener('DOMContentLoaded', () => { - initShow(); - if (gon.features && !gon.features.vueIssuableSidebar) { - initSidebarBundle(); - } - initMrNotes(); - initReviewBar(); - initIssuableHeaderWarning(store); -}); +initShow(); +if (gon.features && !gon.features.vueIssuableSidebar) { + initSidebarBundle(); +} +initMrNotes(); +initReviewBar(); +initIssuableHeaderWarning(store); diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue index 5841716c8c5..20067f6646f 100644 --- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue +++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue @@ -392,6 +392,7 @@ export default { type="submit" category="primary" variant="success" + class="js-no-auto-disable" data-qa-selector="run_pipeline_button" >{{ s__('Pipeline|Run Pipeline') }}</gl-button > diff --git a/app/assets/javascripts/projects/default_sample_data_templates.js b/app/assets/javascripts/projects/default_sample_data_templates.js new file mode 100644 index 00000000000..7c45e7ac62f --- /dev/null +++ b/app/assets/javascripts/projects/default_sample_data_templates.js @@ -0,0 +1,12 @@ +import { s__ } from '~/locale'; + +export default { + basic: { + text: s__('ProjectTemplates|Basic'), + icon: '.template-option .icon-basic', + }, + serenity_valley: { + text: s__('ProjectTemplates|Serenity Valley'), + icon: '.template-option .icon-serenity_valley', + }, +}; diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js index 599aa52831b..d74a2d06786 100644 --- a/app/assets/javascripts/projects/project_new.js +++ b/app/assets/javascripts/projects/project_new.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import DEFAULT_PROJECT_TEMPLATES from 'ee_else_ce/projects/default_project_templates'; +import DEFAULT_SAMPLE_DATA_TEMPLATES from '~/projects/default_sample_data_templates'; import { addSelectOnFocusBehaviour } from '../lib/utils/common_utils'; import { convertToTitleCase, @@ -146,7 +147,8 @@ const bindEvents = () => { $selectedIcon.empty(); const value = $(this).val(); - const selectedTemplate = DEFAULT_PROJECT_TEMPLATES[value]; + const selectedTemplate = + DEFAULT_PROJECT_TEMPLATES[value] || DEFAULT_SAMPLE_DATA_TEMPLATES[value]; $selectedTemplateText.text(selectedTemplate.text); $(selectedTemplate.icon) .clone() diff --git a/app/controllers/admin/sessions_controller.rb b/app/controllers/admin/sessions_controller.rb index 3c811e7c3f5..9c378f4c883 100644 --- a/app/controllers/admin/sessions_controller.rb +++ b/app/controllers/admin/sessions_controller.rb @@ -67,7 +67,10 @@ class Admin::SessionsController < ApplicationController end def valid_otp_attempt?(user) - valid_otp_attempt = user.validate_and_consume_otp!(user_params[:otp_attempt]) + otp_validation_result = + ::Users::ValidateOtpService.new(user).execute(user_params[:otp_attempt]) + valid_otp_attempt = otp_validation_result[:status] == :success + return valid_otp_attempt if Gitlab::Database.read_only? valid_otp_attempt || user.invalidate_otp_backup_code!(user_params[:otp_attempt]) diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb index 7d82d6a3bcb..e2f8baa8226 100644 --- a/app/controllers/profiles/two_factor_auths_controller.rb +++ b/app/controllers/profiles/two_factor_auths_controller.rb @@ -47,7 +47,10 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController end def create - if current_user.validate_and_consume_otp!(params[:pin_code]) + otp_validation_result = + ::Users::ValidateOtpService.new(current_user).execute(params[:pin_code]) + + if otp_validation_result[:status] == :success ActiveSession.destroy_all_but_current(current_user, session) Users::UpdateService.new(current_user, user: current_user, otp_required_for_login: true).execute! do |user| diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 60f18b7c9be..e0321b0b657 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -217,7 +217,7 @@ class Projects::IssuesController < Projects::ApplicationController end def export_csv - ExportCsvWorker.perform_async(current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker + IssuableExportCsvWorker.perform_async(:issue, current_user.id, project.id, finder_options.to_h) # rubocop:disable CodeReuse/Worker index_path = project_issues_path(project) message = _('Your CSV export has started. It will be emailed to %{email} when complete.') % { email: current_user.notification_email } @@ -326,7 +326,7 @@ class Projects::IssuesController < Projects::ApplicationController end def store_uri - if request.get? && !request.xhr? + if request.get? && request.format.html? store_location_for :user, request.fullpath end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index ae9744b224a..61120c5b7d1 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -264,8 +264,11 @@ class SessionsController < Devise::SessionsController end def valid_otp_attempt?(user) - user.validate_and_consume_otp!(user_params[:otp_attempt]) || - user.invalidate_otp_backup_code!(user_params[:otp_attempt]) + otp_validation_result = + ::Users::ValidateOtpService.new(user).execute(user_params[:otp_attempt]) + return true if otp_validation_result[:status] == :success + + user.invalidate_otp_backup_code!(user_params[:otp_attempt]) end def log_audit_event(user, resource, options = {}) diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb index f35ff6e4992..d543267bd44 100644 --- a/app/finders/issuable_finder.rb +++ b/app/finders/issuable_finder.rb @@ -153,8 +153,10 @@ class IssuableFinder end def row_count + fast_fail = Feature.enabled?(:soft_fail_count_by_state, params.group || params.project) + Gitlab::IssuablesCountForState - .new(self, nil, fast_fail: Feature.enabled?(:soft_fail_count_by_state, parent)) + .new(self, nil, fast_fail: fast_fail) .for_state_or_opened(params[:state]) end diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb index cc44f52092c..70ffbf3b1dd 100644 --- a/app/helpers/issuables_helper.rb +++ b/app/helpers/issuables_helper.rb @@ -82,6 +82,7 @@ module IssuablesHelper when Issue IssueSerializer when MergeRequest + opts[:experiment_enabled] = :suggest_pipeline if experiment_enabled?(:suggest_pipeline) && opts[:serializer] == 'widget' MergeRequestSerializer end diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb index bcf60bea0e0..10a1da90e9e 100644 --- a/app/mailers/emails/issues.rb +++ b/app/mailers/emails/issues.rb @@ -93,7 +93,7 @@ module Emails def issues_csv_email(user, project, csv_data, export_status) @project = project - @issues_count = export_status.fetch(:rows_expected) + @count = export_status.fetch(:rows_expected) @written_count = export_status.fetch(:rows_written) @truncated = export_status.fetch(:truncated) diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb index 2d1d271882d..28ac752f550 100644 --- a/app/mailers/emails/merge_requests.rb +++ b/app/mailers/emails/merge_requests.rb @@ -110,6 +110,20 @@ module Emails mail_answer_thread(@merge_request, merge_request_thread_options(mwps_set_by_user_id, recipient_id, reason)) end + def merge_requests_csv_email(user, project, csv_data, export_status) + @project = project + @count = export_status.fetch(:rows_expected) + @written_count = export_status.fetch(:rows_written) + @truncated = export_status.fetch(:truncated) + + filename = "#{project.full_path.parameterize}_merge_requests_#{Date.current.iso8601}.csv" + attachments[filename] = { content: csv_data, mime_type: 'text/csv' } + mail(to: user.notification_email_for(@project.group), subject: subject("Exported merge requests")) do |format| + format.html { render layout: 'mailer' } + format.text { render layout: 'mailer' } + end + end + private def setup_merge_request_mail(merge_request_id, recipient_id, present: false) diff --git a/app/models/blob_viewer/markup.rb b/app/models/blob_viewer/markup.rb index c899ac514d3..37a8e01d0f1 100644 --- a/app/models/blob_viewer/markup.rb +++ b/app/models/blob_viewer/markup.rb @@ -14,7 +14,7 @@ module BlobViewer {}.tap do |h| h[:rendered] = blob.rendered_markup if blob.respond_to?(:rendered_markup) - if Feature.enabled?(:cached_markdown_blob, blob.project) + if Feature.enabled?(:cached_markdown_blob, blob.project, default_enabled: true) h[:cache_key] = ['blob', blob.id, 'commit', blob.commit_id] end end diff --git a/app/models/user.rb b/app/models/user.rb index 41cb15677a8..ef77e207215 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -793,7 +793,7 @@ class User < ApplicationRecord end def two_factor_otp_enabled? - otp_required_for_login? + otp_required_for_login? || Feature.enabled?(:forti_authenticator, self) end def two_factor_u2f_enabled? diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb index 4567a188bd8..44cbcfc5044 100644 --- a/app/serializers/merge_request_widget_entity.rb +++ b/app/serializers/merge_request_widget_entity.rb @@ -67,15 +67,15 @@ class MergeRequestWidgetEntity < Grape::Entity ) end - expose :user_callouts_path, if: -> (*) { Gitlab::Experimentation.enabled?(:suggest_pipeline) } do |_merge_request| + expose :user_callouts_path, if: -> (_, opts) { opts[:experiment_enabled] == :suggest_pipeline } do |_merge_request| user_callouts_path end - expose :suggest_pipeline_feature_id, if: -> (*) { Gitlab::Experimentation.enabled?(:suggest_pipeline) } do |_merge_request| + expose :suggest_pipeline_feature_id, if: -> (_, opts) { opts[:experiment_enabled] == :suggest_pipeline } do |_merge_request| SUGGEST_PIPELINE end - expose :is_dismissed_suggest_pipeline, if: -> (*) { Gitlab::Experimentation.enabled?(:suggest_pipeline) } do |_merge_request| + expose :is_dismissed_suggest_pipeline, if: -> (_, opts) { opts[:experiment_enabled] == :suggest_pipeline } do |_merge_request| current_user && current_user.dismissed_callout?(feature_name: SUGGEST_PIPELINE) end diff --git a/app/services/merge_requests/export_csv_service.rb b/app/services/merge_requests/export_csv_service.rb index 2755fc6687c..1e7f0c8e722 100644 --- a/app/services/merge_requests/export_csv_service.rb +++ b/app/services/merge_requests/export_csv_service.rb @@ -8,7 +8,8 @@ module MergeRequests # Target attachment size before base64 encoding TARGET_FILESIZE = 15.megabytes - def initialize(merge_requests) + def initialize(merge_requests, project) + @project = project @merge_requests = merge_requests end @@ -16,6 +17,10 @@ module MergeRequests csv_builder.render(TARGET_FILESIZE) end + def email(user) + Notify.merge_requests_csv_email(user, @project, csv_data, csv_builder.status).deliver_now + end + private def csv_builder diff --git a/app/services/projects/create_from_template_service.rb b/app/services/projects/create_from_template_service.rb index a207fd2c574..45b52a1861c 100644 --- a/app/services/projects/create_from_template_service.rb +++ b/app/services/projects/create_from_template_service.rb @@ -14,10 +14,16 @@ module Projects def execute return project unless validate_template! - file = built_in_template&.file + file = built_in_template&.file || sample_data_template&.file override_params = params.dup - params[:file] = file + + if built_in_template + params[:file] = built_in_template.file + elsif sample_data_template + params[:file] = sample_data_template.file + params[:sample_data] = true + end GitlabProjectsImportService.new(current_user, params, override_params).execute ensure @@ -27,7 +33,7 @@ module Projects private def validate_template! - return true if built_in_template + return true if built_in_template || sample_data_template project.errors.add(:template_name, _("'%{template_name}' is unknown or invalid" % { template_name: template_name })) false @@ -39,6 +45,12 @@ module Projects end end + def sample_data_template + strong_memoize(:sample_data_template) do + Gitlab::SampleDataTemplate.find(template_name) + end + end + def project @project ||= ::Project.new(namespace_id: params[:namespace_id]) end diff --git a/app/services/projects/gitlab_projects_import_service.rb b/app/services/projects/gitlab_projects_import_service.rb index 2e192942b9c..27cce15f97d 100644 --- a/app/services/projects/gitlab_projects_import_service.rb +++ b/app/services/projects/gitlab_projects_import_service.rb @@ -66,6 +66,7 @@ module Projects end if template_file + data[:sample_data] = params.delete(:sample_data) if params.key?(:sample_data) params[:import_type] = 'gitlab_project' end diff --git a/app/services/users/validate_otp_service.rb b/app/services/users/validate_otp_service.rb new file mode 100644 index 00000000000..a9ce7959aea --- /dev/null +++ b/app/services/users/validate_otp_service.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module Users + class ValidateOtpService < BaseService + def initialize(current_user) + @current_user = current_user + @strategy = if Feature.enabled?(:forti_authenticator, current_user) + ::Gitlab::Auth::Otp::Strategies::FortiAuthenticator.new(current_user) + else + ::Gitlab::Auth::Otp::Strategies::Devise.new(current_user) + end + end + + def execute(otp_code) + strategy.validate(otp_code) + rescue StandardError => ex + Gitlab::ErrorTracking.log_exception(ex) + error(message: ex.message) + end + + private + + attr_reader :strategy + end +end diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml index f6dc808aa55..c5d7b148e69 100644 --- a/app/views/layouts/header/_default.html.haml +++ b/app/views/layouts/header/_default.html.haml @@ -92,7 +92,7 @@ %li.nav-item %div - sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in') - = link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in' + = link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'gl-button btn btn-sign-in' %button.navbar-toggler.d-block.d-sm-none{ type: 'button' } %span.sr-only= _('Toggle navigation') diff --git a/app/views/notify/_issuable_csv_export.html.haml b/app/views/notify/_issuable_csv_export.html.haml new file mode 100644 index 00000000000..239b5b14966 --- /dev/null +++ b/app/views/notify/_issuable_csv_export.html.haml @@ -0,0 +1,6 @@ +%p{ style: 'font-size:18px; text-align:center; line-height:30px;' } + - project_link = link_to(@project.full_name, project_url(@project), style: "color:#3777b0; text-decoration:none; display:block;") + = _('Your CSV export of %{count} from project %{project_link} has been added to this email as an attachment.').html_safe % { count: pluralize(@written_count, type.to_s), project_link: project_link } + - if @truncated + %p + = _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, count: @count } diff --git a/app/views/notify/issues_csv_email.html.haml b/app/views/notify/issues_csv_email.html.haml index 77502a45f02..4cd47f12061 100644 --- a/app/views/notify/issues_csv_email.html.haml +++ b/app/views/notify/issues_csv_email.html.haml @@ -1,6 +1 @@ -%p{ style: 'font-size:18px; text-align:center; line-height:30px;' } - - project_link = link_to(@project.full_name, project_url(@project), style: "color:#3777b0; text-decoration:none; display:block;") - = _('Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment.').html_safe % { issues_count: pluralize(@written_count, 'issue'), project_link: project_link } - - if @truncated - %p - = _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, issues_count: @issues_count } += render 'issuable_csv_export', type: :issue diff --git a/app/views/notify/merge_requests_csv_email.html.haml b/app/views/notify/merge_requests_csv_email.html.haml new file mode 100644 index 00000000000..225c81117b3 --- /dev/null +++ b/app/views/notify/merge_requests_csv_email.html.haml @@ -0,0 +1 @@ += render 'issuable_csv_export', type: :merge_request diff --git a/app/views/notify/merge_requests_csv_email.text.erb b/app/views/notify/merge_requests_csv_email.text.erb new file mode 100644 index 00000000000..9ed971bbe9c --- /dev/null +++ b/app/views/notify/merge_requests_csv_email.text.erb @@ -0,0 +1,5 @@ +<%= _('Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment.') % { written_count: pluralize(@written_count, 'merge request'), project_name: @project.full_name, project_url: project_url(@project) } %> + +<% if @truncated %> + <%= _('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{merge_requests_count} issues have been included. Consider re-exporting with a narrower selection of issues.') % { written_count: @written_count, merge_requests_count: @merge_requests_count} %> +<% end %> diff --git a/app/views/projects/_project_templates.html.haml b/app/views/projects/_project_templates.html.haml index 98fdb1d7a0b..79221c59ae4 100644 --- a/app/views/projects/_project_templates.html.haml +++ b/app/views/projects/_project_templates.html.haml @@ -1,7 +1,21 @@ - f ||= local_assigns[:f] -.project-templates-buttons.import-buttons.col-sm-12 - = render 'projects/project_templates/built_in_templates' +.project-templates-buttons.col-sm-12 + %ul.nav-tabs.nav-links.nav.scrolling-tabs + %li.built-in-tab + %a.nav-link.active{ href: "#built-in", data: { toggle: 'tab'} } + = _('Built-in') + %span.badge.badge-pill= Gitlab::ProjectTemplate.all.count + %li.sample-data-templates-tab + %a.nav-link{ href: "#sample-data-templates", data: { toggle: 'tab'} } + = _('Sample Data') + %span.badge.badge-pill= Gitlab::SampleDataTemplate.all.count + +.tab-content + .project-templates-buttons.import-buttons.tab-pane.active#built-in + = render partial: 'projects/project_templates/template', collection: Gitlab::ProjectTemplate.all + .project-templates-buttons.import-buttons.tab-pane#sample-data-templates + = render partial: 'projects/project_templates/template', collection: Gitlab::SampleDataTemplate.all .project-fields-form = render 'projects/project_templates/project_fields_form' diff --git a/app/views/projects/project_templates/_built_in_templates.html.haml b/app/views/projects/project_templates/_built_in_templates.html.haml deleted file mode 100644 index 43352952b37..00000000000 --- a/app/views/projects/project_templates/_built_in_templates.html.haml +++ /dev/null @@ -1,17 +0,0 @@ -- Gitlab::ProjectTemplate.all.each do |template| - .template-option.d-flex.align-items-center{ data: { qa_selector: 'template_option_row' } } - .logo.gl-mr-3.px-1 - = image_tag template.logo, size: 32, class: "btn-template-icon icon-#{template.name}" - .description - %strong - = template.title - %br - .text-muted - = template.description - .controls.d-flex.align-items-center - %a.btn.btn-default.gl-mr-3{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "template_preview", track_property: template.name, track_event: "click_button", track_value: "" } } - = _("Preview") - %label.btn.btn-success.template-button.choose-template.gl-mb-0{ for: template.name } - %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "template_use", track_property: template.name, track_event: "click_button", track_value: "" } } - %span{ data: { qa_selector: 'use_template_button' } } - = _("Use template") diff --git a/app/views/projects/project_templates/_template.html.haml b/app/views/projects/project_templates/_template.html.haml new file mode 100644 index 00000000000..e2bfd0881b5 --- /dev/null +++ b/app/views/projects/project_templates/_template.html.haml @@ -0,0 +1,16 @@ +.template-option.d-flex.align-items-center{ data: { qa_selector: 'template_option_row' } } + .logo.gl-mr-3.px-1 + = image_tag template.logo, size: 32, class: "btn-template-icon icon-#{template.name}" + .description + %strong + = template.title + %br + .text-muted + = template.description + .controls.d-flex.align-items-center + %a.btn.gl-button.btn-default.gl-mr-3{ href: template.preview, rel: 'noopener noreferrer', target: '_blank', data: { track_label: "template_preview", track_property: template.name, track_event: "click_button", track_value: "" } } + = _("Preview") + %label.btn.gl-button.btn-success.template-button.choose-template.gl-mb-0{ for: template.name } + %input{ type: "radio", autocomplete: "off", name: "project[template_name]", id: template.name, value: template.name, data: { track_label: "template_use", track_property: template.name, track_event: "click_button", track_value: "" } } + %span{ data: { qa_selector: 'use_template_button' } } + = _("Use template") diff --git a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml index d378e6cb22c..6f70c927147 100644 --- a/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml +++ b/app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml @@ -1,13 +1,13 @@ - if show_auto_devops_implicitly_enabled_banner?(project, current_user) - .qa-auto-devops-banner.auto-devops-implicitly-enabled-banner.alert.alert-info - - more_information_link = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank', class: 'alert-link' - - auto_devops_message = s_("AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}") % { more_information_link: more_information_link } - = auto_devops_message.html_safe - .alert-link-group - = link_to _('Settings'), project_settings_ci_cd_path(project), class: 'alert-link' - | - = link_to _('Dismiss'), '#', class: 'hide-auto-devops-implicitly-enabled-banner alert-link', data: { project_id: project.id } + .qa-auto-devops-banner.auto-devops-implicitly-enabled-banner.gl-alert.gl-alert-info + = sprite_icon('information-o', css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title') + %button.js-close.gl-alert-dismiss{ type: 'button', 'aria-label' => _('Dismiss'), class: 'hide-auto-devops-implicitly-enabled-banner alert-link', data: { project_id: project.id } } + = sprite_icon('close', css_class: 'gl-icon') + .gl-alert-body + = s_("AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found.") - unless Gitlab.config.registry.enabled %div - = icon('exclamation-triangle') = _('Container registry is not enabled on this GitLab instance. Ask an administrator to enable it in order for Auto DevOps to work.') + .gl-alert-actions.gl-mt-3 + = link_to _('Settings'), project_settings_ci_cd_path(project), class: 'alert-link btn gl-button btn-info' + = link_to _('More information'), help_page_path('topics/autodevops/index.md'), target: '_blank', class: 'alert-link btn gl-button btn-default gl-ml-2' diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 452c201ec77..30b89f37562 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -1571,6 +1571,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: issuable_export_csv + :feature_category: :issue_tracking + :has_external_dependencies: + :urgency: :low + :resource_boundary: :cpu + :weight: 1 + :idempotent: + :tags: [] - :name: issue_placement :feature_category: :issue_tracking :has_external_dependencies: diff --git a/app/workers/export_csv_worker.rb b/app/workers/export_csv_worker.rb index e7baaf40a41..f2da381a34a 100644 --- a/app/workers/export_csv_worker.rb +++ b/app/workers/export_csv_worker.rb @@ -15,8 +15,6 @@ class ExportCsvWorker # rubocop:disable Scalability/IdempotentWorker params[:project_id] = project_id params.delete(:sort) - issues = IssuesFinder.new(@current_user, params).execute - - Issues::ExportCsvService.new(issues, @project).email(@current_user) + IssuableExportCsvWorker.perform_async(:issue, @current_user.id, @project.id, params) end end diff --git a/app/workers/issuable_export_csv_worker.rb b/app/workers/issuable_export_csv_worker.rb new file mode 100644 index 00000000000..d91ba77287f --- /dev/null +++ b/app/workers/issuable_export_csv_worker.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +class IssuableExportCsvWorker # rubocop:disable Scalability/IdempotentWorker + include ApplicationWorker + + feature_category :issue_tracking + worker_resource_boundary :cpu + loggable_arguments 2 + + PERMITTED_TYPES = [:merge_request, :issue].freeze + + def perform(type, current_user_id, project_id, params) + @type = type.to_sym + check_permitted_type! + process_params!(params, project_id) + + @current_user = User.find(current_user_id) + @project = Project.find(project_id) + @service = service(find_objects(params)) + + @service.email(@current_user) + end + + private + + def find_objects(params) + case @type + when :issue + IssuesFinder.new(@current_user, params).execute + when :merge_request + MergeRequestsFinder.new(@current_user, params).execute + end + end + + def service(issuables) + case @type + when :issue + Issues::ExportCsvService.new(issuables, @project) + when :merge_request + MergeRequests::ExportCsvService.new(issuables, @project) + end + end + + def process_params!(params, project_id) + params.symbolize_keys! + params[:project_id] = project_id + params.delete(:sort) + end + + def check_permitted_type! + raise ArgumentError, "type parameter must be :issue or :merge_request, it was #{@type}" unless PERMITTED_TYPES.include?(@type) + end +end diff --git a/changelogs/unreleased/233664-bootstrap-alert-auto-devops.yml b/changelogs/unreleased/233664-bootstrap-alert-auto-devops.yml new file mode 100644 index 00000000000..c32ac9522a2 --- /dev/null +++ b/changelogs/unreleased/233664-bootstrap-alert-auto-devops.yml @@ -0,0 +1,5 @@ +--- +title: Migrate auto devops message from bootstrap +merge_request: 45221 +author: +type: other diff --git a/changelogs/unreleased/247496-add-rollback-migration-helpers-for-change-column-type-concurrently.yml b/changelogs/unreleased/247496-add-rollback-migration-helpers-for-change-column-type-concurrently.yml new file mode 100644 index 00000000000..c060c044050 --- /dev/null +++ b/changelogs/unreleased/247496-add-rollback-migration-helpers-for-change-column-type-concurrently.yml @@ -0,0 +1,5 @@ +--- +title: Add undo helpers for change_column_type_concurrently and cleanup_concurrent_column_type_change +merge_request: 44155 +author: +type: other diff --git a/changelogs/unreleased/263406-enable-cached-markdown-blob-default.yml b/changelogs/unreleased/263406-enable-cached-markdown-blob-default.yml new file mode 100644 index 00000000000..8bafc2abd89 --- /dev/null +++ b/changelogs/unreleased/263406-enable-cached-markdown-blob-default.yml @@ -0,0 +1,5 @@ +--- +title: Enable caching of markdown when viewing blob +merge_request: 45367 +author: +type: performance diff --git a/changelogs/unreleased/267583-fix-storing-of-issue-json-for-redirect.yml b/changelogs/unreleased/267583-fix-storing-of-issue-json-for-redirect.yml new file mode 100644 index 00000000000..c7f2dfbf9b6 --- /dev/null +++ b/changelogs/unreleased/267583-fix-storing-of-issue-json-for-redirect.yml @@ -0,0 +1,5 @@ +--- +title: Fix redirects to issue sidebar JSON when visiting the login page +merge_request: 45194 +author: +type: fixed diff --git a/changelogs/unreleased/gy-add-demo-templates.yml b/changelogs/unreleased/gy-add-demo-templates.yml new file mode 100644 index 00000000000..7e11468f5c3 --- /dev/null +++ b/changelogs/unreleased/gy-add-demo-templates.yml @@ -0,0 +1,5 @@ +--- +title: Add Sample Data +merge_request: 41699 +author: +type: added diff --git a/config/feature_flags/development/cached_markdown_blob.yml b/config/feature_flags/development/cached_markdown_blob.yml index a7f42c94595..de7a7c52b66 100644 --- a/config/feature_flags/development/cached_markdown_blob.yml +++ b/config/feature_flags/development/cached_markdown_blob.yml @@ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44300 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/263406 type: development group: group::source code -default_enabled: false +default_enabled: true diff --git a/config/feature_flags/development/forti_authenticator.yml b/config/feature_flags/development/forti_authenticator.yml new file mode 100644 index 00000000000..31f5256753f --- /dev/null +++ b/config/feature_flags/development/forti_authenticator.yml @@ -0,0 +1,7 @@ +--- +name: forti_authenticator +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45055 +rollout_issue_url: +type: development +group: group::access +default_enabled: false diff --git a/config/feature_flags/development/push_rules_supersede_code_owners.yml b/config/feature_flags/development/push_rules_supersede_code_owners.yml deleted file mode 100644 index d185d19522d..00000000000 --- a/config/feature_flags/development/push_rules_supersede_code_owners.yml +++ /dev/null @@ -1,7 +0,0 @@ ---- -name: push_rules_supersede_code_owners -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44126 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/262019 -type: development -group: group::source code -default_enabled: false diff --git a/config/feature_flags/development/upload_middleware_jwt_params_handler.yml b/config/feature_flags/development/upload_middleware_jwt_params_handler.yml index b467ade8609..1c3545cb728 100644 --- a/config/feature_flags/development/upload_middleware_jwt_params_handler.yml +++ b/config/feature_flags/development/upload_middleware_jwt_params_handler.yml @@ -4,4 +4,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/33277 rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/233895 group: group::package type: development -default_enabled: false +default_enabled: true diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index bef6d4e48d1..de389514cce 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -1022,6 +1022,21 @@ production: &base # cas3: # session_duration: 28800 + # FortiAuthenticator settings + forti_authenticator: + # Allow using FortiAuthenticator as OTP provider + enabled: false + + # Host and port of FortiAuthenticator instance + # host: forti_authenticator.example.com + # port: 443 + + # Username for accessing FortiAuthenticator API + # username: john + + # Access token for FortiAuthenticator API + # access_token: 123s3cr3t456 + # Shared file storage settings shared: # path: /mnt/gitlab # Default: shared diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index baf728fb0dc..affbc85d5a9 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -767,6 +767,13 @@ Gitlab.ee do end # +# FortiAuthenticator +# +Settings['forti_authenticator'] ||= Settingslogic.new({}) +Settings.forti_authenticator['enabled'] = false if Settings.forti_authenticator['enabled'].nil? +Settings.forti_authenticator['port'] = 443 if Settings.forti_authenticator['port'].to_i == 0 + +# # Extra customization # Settings['extra'] ||= Settingslogic.new({}) diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 6129ff1f409..f061efeb427 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -152,6 +152,8 @@ - 2 - - irker - 1 +- - issuable_export_csv + - 1 - - issue_placement - 2 - - issue_rebalancing diff --git a/doc/.vale/gitlab/spelling-exceptions.txt b/doc/.vale/gitlab/spelling-exceptions.txt index 32e210892ea..c0a85fc6b70 100644 --- a/doc/.vale/gitlab/spelling-exceptions.txt +++ b/doc/.vale/gitlab/spelling-exceptions.txt @@ -146,6 +146,7 @@ failsafe Falco fastlane favicon +Figma Filebeat Fio firewalled @@ -312,6 +313,7 @@ passwordless Patroni performant PgBouncer +Phabricator phaser phasers Pipfile @@ -352,6 +354,7 @@ Python Qualys Rackspace Raspbian +Rdoc reachability rebase rebased @@ -433,7 +436,10 @@ smartcard smartcards SMTP Sobelow +Solarized Sourcegraph +sparkline +sparklines spidering Splunk SpotBugs @@ -449,6 +455,8 @@ subfolder subfolders subgraph subgraphs +subkey +subkeys sublicense sublicensed sublicenses @@ -468,6 +476,7 @@ subtrees sudo syslog tcpdump +Thanos Tiller timecop todos @@ -490,6 +499,7 @@ unarchived unarchives unarchiving unassign +unassigning unassigns uncheck unchecked diff --git a/doc/administration/uploads.md b/doc/administration/uploads.md index df3a667f09d..91de089c45e 100644 --- a/doc/administration/uploads.md +++ b/doc/administration/uploads.md @@ -2,6 +2,35 @@ Uploads represent all user data that may be sent to GitLab as a single file. As an example, avatars and notes' attachments are uploads. Uploads are integral to GitLab functionality, and therefore cannot be disabled. +## Upload parameters + +> - [Changed](https://gitlab.com/gitlab-org/gitlab/-/issues/214785) in GitLab 13.5. +> - It's [deployed behind a feature flag](../user/feature_flags.md), enabled by default. +> - It's enabled on GitLab.com. +> - It's recommended for production use. +> - For GitLab self-managed instances, GitLab administrators can opt to disable it. **(CORE ONLY)** + +In 13.5 and later, upload parameters are passed [between Workhorse and GitLab Rails](../development/architecture.md#simplified-component-overview) differently than they +were before. + +This change is deployed behind a feature flag that is **enabled by default**. + +If you experience any issues with upload, +[GitLab administrators with access to the GitLab Rails console](./feature_flags.md) +can opt to disable it. + +To enable it: + +```ruby +Feature.enable(:upload_middleware_jwt_params_handler) +``` + +To disable it: + +```ruby +Feature.disable(:upload_middleware_jwt_params_handler) +``` + ## Using local storage NOTE: **Note:** diff --git a/doc/api/experiments.md b/doc/api/experiments.md new file mode 100644 index 00000000000..66c444e54ce --- /dev/null +++ b/doc/api/experiments.md @@ -0,0 +1,40 @@ +--- +stage: Growth +group: Expansion +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers +--- + +# Experiments API + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/262725) in GitLab 13.5. + +This API is for listing Experiments [experiment use in development of GitLab](../development/experiment_guide/index.md). + +All methods require user be a [GitLab team member](https://gitlab.com/groups/gitlab-com/-/group_members) for authorization. + +## List all experiments + +Get a list of all experiments, with its enabled status. + +```plaintext +GET /experiments +``` + +```shell +curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/experiments" +``` + +Example response: + +```json +[ + { + "key": "experiment_1", + "enabled": true + }, + { + "key": "experiment_2", + "enabled": false + } +] +``` diff --git a/doc/api/namespaces.md b/doc/api/namespaces.md index 35a8226b4d8..0792c6d4a3b 100644 --- a/doc/api/namespaces.md +++ b/doc/api/namespaces.md @@ -87,10 +87,12 @@ the `plan` parameter associated with a namespace: ] ``` -Users on GitLab.com will also see a `max_seats_used` parameter. `max_seats_used` -is the highest number of users the group had. +Users on GitLab.com will also see `max_seats_used` and `seats_in_use` parameters. +`max_seats_used` is the highest number of users the group had. `seats_in_use` is +the number of license seats currently being used. Both values are updated +once a day. -`max_seats_used` will be non-zero only for namespaces on paid plans. +`max_seats_used` and `seats_in_use` will be non-zero only for namespaces on paid plans. ```json [ @@ -99,6 +101,7 @@ is the highest number of users the group had. "name": "user1", "billable_members_count": 2, "max_seats_used": 3, + "seats_in_use": 2, ... } ] @@ -141,6 +144,7 @@ Example response: "members_count_with_descendants": 2, "billable_members_count": 2, "max_seats_used": 0, + "seats_in_use": 0, "plan": "default", "trial_ends_on": null, "trial": false @@ -181,6 +185,7 @@ Example response: "members_count_with_descendants": 2, "billable_members_count": 2, "max_seats_used": 0, + "seats_in_use": 0, "plan": "default", "trial_ends_on": null, "trial": false @@ -208,6 +213,7 @@ Example response: "members_count_with_descendants": 2, "billable_members_count": 2, "max_seats_used": 0, + "seats_in_use": 0, "plan": "default", "trial_ends_on": null, "trial": false diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md index eccc8b4212d..cc8bb20b003 100644 --- a/doc/api/project_snippets.md +++ b/doc/api/project_snippets.md @@ -83,12 +83,17 @@ POST /projects/:id/snippets Parameters: -- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user -- `title` (required) - The title of a snippet -- `file_name` (required) - The name of a snippet file -- `description` (optional) - The description of a snippet -- `content` (required) - The content of a snippet -- `visibility` (required) - The snippet's visibility +| Attribute | Type | Required | Description | +|:------------------|:----------------|:---------|:----------------------------------------------------------------------------------------------------------------| +| `id` | integer | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `title` | string | yes | Title of a snippet | +| `file_name` | string | no | Deprecated: Use `files` instead. Name of a snippet file | +| `content` | string | no | Deprecated: Use `files` instead. Content of a snippet | +| `description` | string | no | Description of a snippet | +| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level) | +| `files` | array of hashes | no | An array of snippet files | +| `files:file_path` | string | yes | File path of the snippet file | +| `files:content` | string | yes | Content of the snippet file | Example request: @@ -105,9 +110,13 @@ curl --request POST "https://gitlab.com/api/v4/projects/:id/snippets" \ { "title" : "Example Snippet Title", "description" : "More verbose snippet description", - "file_name" : "example.txt", - "content" : "source code \n with multiple lines\n", - "visibility" : "private" + "visibility" : "private", + "files": [ + { + "file_path": "example.txt", + "content" : "source code \n with multiple lines\n", + } + ] } ``` @@ -121,13 +130,22 @@ PUT /projects/:id/snippets/:snippet_id Parameters: -- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user -- `snippet_id` (required) - The ID of a project's snippet -- `title` (optional) - The title of a snippet -- `file_name` (optional) - The name of a snippet file -- `description` (optional) - The description of a snippet -- `content` (optional) - The content of a snippet -- `visibility` (optional) - The snippet's visibility +| Attribute | Type | Required | Description | +|:----------------------|:----------------|:---------|:----------------------------------------------------------------------------------------------------------------| +| `id` | integer | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user | +| `snippet_id` | integer | yes | The ID of a project's snippet | +| `title` | string | no | Title of a snippet | +| `file_name` | string | no | Deprecated: Use `files` instead. Name of a snippet file | +| `content` | string | no | Deprecated: Use `files` instead. Content of a snippet | +| `description` | string | no | Description of a snippet | +| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level) | +| `files` | array of hashes | no | An array of snippet files | +| `files:action` | string | yes | Type of action to perform on the file, one of: 'create', 'update', 'delete', 'move' | +| `files:file_path` | string | no | File path of the snippet file | +| `files:previous_path` | string | no | Previous path of the snippet file | +| `files:content` | string | no | Content of the snippet file | + +Updates to snippets with multiple files *must* use the `files` attribute. Example request: @@ -144,9 +162,14 @@ curl --request PUT "https://gitlab.com/api/v4/projects/:id/snippets/:snippet_id" { "title" : "Updated Snippet Title", "description" : "More verbose snippet description", - "file_name" : "new_filename.txt", - "content" : "updated source code \n with multiple lines\n", - "visibility" : "private" + "visibility" : "private", + "files": [ + { + "action": "update", + "file_path": "example.txt", + "content" : "updated source code \n with multiple lines\n" + } + ] } ``` diff --git a/doc/api/projects.md b/doc/api/projects.md index ad26457ad99..b1307e673db 100644 --- a/doc/api/projects.md +++ b/doc/api/projects.md @@ -2241,6 +2241,113 @@ PUT /projects/:id/transfer | `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) | | `namespace` | integer/string | yes | The ID or path of the namespace to transfer to project to | +Example request: + +```shell +curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/transfer?namespace=14" +``` + +Example response: + +```json + { + "id": 7, + "description": "", + "name": "hello-world", + "name_with_namespace": "cute-cats / hello-world", + "path": "hello-world", + "path_with_namespace": "cute-cats/hello-world", + "created_at": "2020-10-15T16:25:22.415Z", + "default_branch": "master", + "tag_list": [], + "ssh_url_to_repo": "git@gitlab.example.com:cute-cats/hello-world.git", + "http_url_to_repo": "https://gitlab.example.com/cute-cats/hello-world.git", + "web_url": "https://gitlab.example.com/cute-cats/hello-world", + "readme_url": "https://gitlab.example.com/cute-cats/hello-world/-/blob/master/README.md", + "avatar_url": null, + "forks_count": 0, + "star_count": 0, + "last_activity_at": "2020-10-15T16:25:22.415Z", + "namespace": { + "id": 18, + "name": "cute-cats", + "path": "cute-cats", + "kind": "group", + "full_path": "cute-cats", + "parent_id": null, + "avatar_url": null, + "web_url": "https://gitlab.example.com/groups/cute-cats" + }, + "_links": { + "self": "https://gitlab.example.com/api/v4/projects/7", + "issues": "https://gitlab.example.com/api/v4/projects/7/issues", + "merge_requests": "https://gitlab.example.com/api/v4/projects/7/merge_requests", + "repo_branches": "https://gitlab.example.com/api/v4/projects/7/repository/branches", + "labels": "https://gitlab.example.com/api/v4/projects/7/labels", + "events": "https://gitlab.example.com/api/v4/projects/7/events", + "members": "https://gitlab.example.com/api/v4/projects/7/members" + }, + "packages_enabled": true, + "empty_repo": false, + "archived": false, + "visibility": "private", + "resolve_outdated_diff_discussions": false, + "container_registry_enabled": true, + "container_expiration_policy": { + "cadence": "7d", + "enabled": false, + "keep_n": null, + "older_than": null, + "name_regex": null, + "name_regex_keep": null, + "next_run_at": "2020-10-22T16:25:22.746Z" + }, + "issues_enabled": true, + "merge_requests_enabled": true, + "wiki_enabled": true, + "jobs_enabled": true, + "snippets_enabled": true, + "service_desk_enabled": false, + "service_desk_address": null, + "can_create_merge_request_in": true, + "issues_access_level": "enabled", + "repository_access_level": "enabled", + "merge_requests_access_level": "enabled", + "forking_access_level": "enabled", + "wiki_access_level": "enabled", + "builds_access_level": "enabled", + "snippets_access_level": "enabled", + "pages_access_level": "enabled", + "emails_disabled": null, + "shared_runners_enabled": true, + "lfs_enabled": true, + "creator_id": 2, + "import_status": "none", + "open_issues_count": 0, + "ci_default_git_depth": 50, + "public_jobs": true, + "build_timeout": 3600, + "auto_cancel_pending_pipelines": "enabled", + "build_coverage_regex": null, + "ci_config_path": null, + "shared_with_groups": [], + "only_allow_merge_if_pipeline_succeeds": false, + "allow_merge_on_skipped_pipeline": null, + "request_access_enabled": true, + "only_allow_merge_if_all_discussions_are_resolved": false, + "remove_source_branch_after_merge": true, + "printing_merge_request_link_enabled": true, + "merge_method": "merge", + "suggestion_commit_message": null, + "auto_devops_enabled": true, + "auto_devops_deploy_strategy": "continuous", + "autoclose_referenced_issues": true, + "approvals_before_merge": 0, + "mirror": false, + "compliance_frameworks": [] +} +``` + ## Branches Read more in the [Branches](branches.md) documentation. diff --git a/doc/api/snippets.md b/doc/api/snippets.md index 6863763ff24..aab9394e888 100644 --- a/doc/api/snippets.md +++ b/doc/api/snippets.md @@ -198,22 +198,40 @@ POST /snippets Parameters: -| Attribute | Type | Required | Description | -|:--------------|:-------|:---------|:---------------------------------------------------| -| `title` | string | yes | Title of a snippet. | -| `file_name` | string | yes | Name of a snippet file. | -| `content` | string | yes | Content of a snippet. | -| `description` | string | no | Description of a snippet. | -| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level). | +| Attribute | Type | Required | Description | +|:------------------|:----------------|:---------|:--------------------------------------------------------| +| `title` | string | yes | Title of a snippet | +| `file_name` | string | no | Deprecated: Use `files` instead. Name of a snippet file | +| `content` | string | no | Deprecated: Use `files` instead. Content of a snippet | +| `description` | string | no | Description of a snippet | +| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level) | +| `files` | array of hashes | no | An array of snippet files | +| `files:file_path` | string | yes | File path of the snippet file | +| `files:content` | string | yes | Content of the snippet file | Example request: ```shell -curl --request POST \ - --data '{"title": "This is a snippet", "content": "Hello world", "description": "Hello World snippet", "file_name": "test.txt", "visibility": "internal" }' \ +curl --request POST "https://gitlab.example.com/api/v4/snippets" \ --header 'Content-Type: application/json' \ --header "PRIVATE-TOKEN: <your_access_token>" \ - "https://gitlab.example.com/api/v4/snippets" + -d @snippet.json +``` + +`snippet.json` used in the above example request: + +```json +{ + "title": "This is a snippet", + "description": "Hello World snippet", + "visibility": "internal", + "files": [ + { + "content": "Hello world", + "file_path": "test.txt" + } + ] +} ``` Example response: @@ -222,7 +240,6 @@ Example response: { "id": 1, "title": "This is a snippet", - "file_name": "test.txt", "description": "Hello World snippet", "visibility": "internal", "author": { @@ -238,7 +255,16 @@ Example response: "created_at": "2012-06-28T10:52:04Z", "project_id": null, "web_url": "http://example.com/snippets/1", - "raw_url": "http://example.com/snippets/1/raw" + "raw_url": "http://example.com/snippets/1/raw", + "ssh_url_to_repo": "ssh://git@gitlab.example.com:snippets/1.git", + "http_url_to_repo": "https://gitlab.example.com/snippets/1.git", + "file_name": "test.txt", + "files": [ + { + "path": "text.txt", + "raw_url": "https://gitlab.example.com/-/snippets/1/raw/master/renamed.md" + } + ] } ``` @@ -255,23 +281,44 @@ PUT /snippets/:id Parameters: -| Attribute | Type | Required | Description | -|:--------------|:--------|:---------|:---------------------------------------------------| -| `id` | integer | yes | ID of snippet to update. | -| `title` | string | no | Title of a snippet. | -| `file_name` | string | no | Name of a snippet file. | -| `description` | string | no | Description of a snippet. | -| `content` | string | no | Content of a snippet. | -| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level). | +| Attribute | Type | Required | Description | +|:----------------------|:----------------|:---------|:------------------------------------------------------------------------------------| +| `id` | integer | yes | ID of snippet to update | +| `title` | string | no | Title of a snippet | +| `file_name` | string | no | Deprecated: Use `files` instead. Name of a snippet file | +| `content` | string | no | Deprecated: Use `files` instead. Content of a snippet | +| `description` | string | no | Description of a snippet | +| `visibility` | string | no | Snippet's [visibility](#snippet-visibility-level) | +| `files` | array of hashes | no | An array of snippet files | +| `files:action` | string | yes | Type of action to perform on the file, one of: 'create', 'update', 'delete', 'move' | +| `files:file_path` | string | no | File path of the snippet file | +| `files:previous_path` | string | no | Previous path of the snippet file | +| `files:content` | string | no | Content of the snippet file | + +Updates to snippets with multiple files *must* use the `files` attribute. Example request: ```shell -curl --request PUT \ - --data '{"title": "foo", "content": "bar"}' \ +curl --request PUT "https://gitlab.example.com/api/v4/snippets/1" \ --header 'Content-Type: application/json' \ --header "PRIVATE-TOKEN: <your_access_token>" \ - "https://gitlab.example.com/api/v4/snippets/1" + -d @snippet.json +``` + +`snippet.json` used in the above example request: + +```json +{ + "title": "foo", + "files": [ + { + "action": "move", + "previous_path": "test.txt", + "file_path": "renamed.md" + } + ] +} ``` Example response: @@ -280,7 +327,6 @@ Example response: { "id": 1, "title": "test", - "file_name": "add.rb", "description": "description of snippet", "visibility": "internal", "author": { @@ -296,7 +342,16 @@ Example response: "created_at": "2012-06-28T10:52:04Z", "project_id": null, "web_url": "http://example.com/snippets/1", - "raw_url": "http://example.com/snippets/1/raw" + "raw_url": "http://example.com/snippets/1/raw", + "ssh_url_to_repo": "ssh://git@gitlab.example.com:snippets/1.git", + "http_url_to_repo": "https://gitlab.example.com/snippets/1.git", + "file_name": "renamed.md", + "files": [ + { + "path": "renamed.md", + "raw_url": "https://gitlab.example.com/-/snippets/1/raw/master/renamed.md" + } + ] } ``` diff --git a/doc/development/what_requires_downtime.md b/doc/development/what_requires_downtime.md index 7c99bcde413..9063fb867e2 100644 --- a/doc/development/what_requires_downtime.md +++ b/doc/development/what_requires_downtime.md @@ -176,7 +176,7 @@ class ChangeUsersUsernameStringToText < ActiveRecord::Migration[4.2] end def down - cleanup_concurrent_column_type_change :users, :username + undo_change_column_type_concurrently :users, :username end end ``` @@ -197,7 +197,7 @@ class ChangeUsersUsernameStringToTextCleanup < ActiveRecord::Migration[4.2] end def down - change_column_type_concurrently :users, :username, :string + undo_cleanup_concurrent_column_type_change :users, :username, :string end end ``` diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md index 65e76998673..806eaa6c98e 100644 --- a/doc/gitlab-basics/create-project.md +++ b/doc/gitlab-basics/create-project.md @@ -58,7 +58,7 @@ To create a new blank project on the **New project** page: Project templates can pre-populate a new project with the necessary files to get you started quickly. -There are two types of project templates: +There are two main types of project templates: - [Built-in templates](#built-in-templates), sourced from the following groups: - [`project-templates`](https://gitlab.com/gitlab-org/project-templates) diff --git a/doc/operations/incident_management/alerts.md b/doc/operations/incident_management/alerts.md index f4af305c061..a6168386024 100644 --- a/doc/operations/incident_management/alerts.md +++ b/doc/operations/incident_management/alerts.md @@ -204,6 +204,19 @@ Select the **To-Do List** **{todo-done}** in the navigation bar to view your cur ![Alert Details Added to do](./img/alert_detail_added_todo_v13_1.png) +## Link runbooks to alerts + +> Runbook URLs [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/39315) in GitLab 13.3. + +When creating alerts from the metrics dashboard for +[managed Prometheus instances](../metrics/alerts.md#managed-prometheus-instances), +you can link a runbook. When the alert triggers, you can access the runbook through +the [chart context menu](../metrics/dashboards/index.md#chart-context-menu) in the +upper-right corner of the metrics chart, making it easy for you to locate and access +the correct runbook: + +![Linked Runbook in charts](img/link_runbooks_to_alerts_v13_5.png) + ## View the environment that generated the alert > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/232492) in GitLab 13.5. diff --git a/doc/operations/incident_management/img/link_runbooks_to_alerts_v13_5.png b/doc/operations/incident_management/img/link_runbooks_to_alerts_v13_5.png Binary files differnew file mode 100644 index 00000000000..a63001b4cde --- /dev/null +++ b/doc/operations/incident_management/img/link_runbooks_to_alerts_v13_5.png diff --git a/doc/user/admin_area/settings/project_integration_management.md b/doc/user/admin_area/settings/project_integration_management.md index 750bec1d91a..748d608676d 100644 --- a/doc/user/admin_area/settings/project_integration_management.md +++ b/doc/user/admin_area/settings/project_integration_management.md @@ -6,74 +6,53 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Project integration management -Project integrations can be configured and enabled by project administrators. As a GitLab instance administrator, you can set default configuration parameters for a given integration that all projects can inherit and use, enabling the integration for all projects that are not already using custom settings. +Project integrations can be configured and enabled by project administrators. As a GitLab instance +administrator, you can set default configuration parameters for a given integration that all projects +can inherit and use. This enables the integration for all projects that are not already using custom +settings. -You can update these default settings at any time, changing the settings used for all -projects that are set to use instance-level or group-level defaults. Updating the -default settings also enables the integration for all projects that didn't have it -already enabled. +You can update these default settings at any time, changing the settings used for all projects that +are set to use instance-level defaults. Updating the default settings also enables the integration +for all projects that didn't have it already enabled. -Only the complete settings for an integration can be inherited. Per-field inheritance is [planned](https://gitlab.com/groups/gitlab-org/-/epics/2137). +Only the complete settings for an integration can be inherited. Per-field inheritance is +[planned](https://gitlab.com/groups/gitlab-org/-/epics/2137) as is +[group-level management](https://gitlab.com/groups/gitlab-org/-/epics/2543) of integration settings. ## Manage instance-level default settings for a project integration **(CORE ONLY)** > [Introduced in](https://gitlab.com/groups/gitlab-org/-/epics/2137) GitLab 13.3. 1. Navigate to **Admin Area > Settings > Integrations**. -1. Select an integration. +1. Select a project integration. 1. Enter configuration details and click **Save changes**. CAUTION: **Caution:** -This may affect all or most of the groups and projects on your GitLab instance. Please review the details below. +This may affect all or most of the projects on your GitLab instance. Please review the details +below. If this is the first time you are setting up instance-level settings for an integration: -- The integration is enabled for all groups and projects that do not already have this integration configured if you have the **Enable integration** toggle turned on in the instance-level settings. -- Groups and projects that already have the integration configured are not affected, but can choose to use the inherited settings at any time. +- The integration is enabled for all projects that don't already have this integration configured, + if you have the **Enable integration** toggle turned on in the instance-level settings. +- Projects that already have the integration configured are not affected, but can choose to use the + inherited settings at any time. When you make further changes to the instance defaults: -- They are immediately applied to all groups and projects that have the integration set to use default settings. -- They are immediately applied to newer groups and projects, created since you last - saved defaults for the integration. If your instance-level default setting has the - **Enable integration** toggle turned on, the integration is automatically enabled for - all such groups and projects. -- Groups and projects with custom settings selected for the integration are not immediately affected and may choose to use the latest defaults at any time. +- They are immediately applied to all projects that have the integration set to use default settings. +- They are immediately applied to newer projects, created since you last saved defaults for the + integration. If your instance-level default setting has the **Enable integration** toggle turned + on, the integration is automatically enabled for all such projects. +- Projects with custom settings selected for the integration are not immediately affected and may + choose to use the latest defaults at any time. Only the complete settings for an integration can be inherited. Per-field inheritance is [planned](https://gitlab.com/groups/gitlab-org/-/epics/2137). This would allow -administrators to update settings inherited by groups and projects without enabling the -integration on all non-configured groups and projects by default. +administrators to update settings inherited by projects without enabling the +integration on all non-configured projects by default. -## Manage group-level default settings for a project integration - -> [Introduced in](https://gitlab.com/groups/gitlab-org/-/epics/2543) GitLab 13.5. - -1. Navigate to the group's **Settings > Integrations**. -1. Select an integration. -1. Enter configuration details and click **Save changes**. - -CAUTION: **Caution:** -This may affect all or most of the subgroups and projects belonging to the group. Please review the details below. - -If this is the first time you are setting up group-level settings for an integration: - -- The integration is enabled for all subgroups and projects belonging to the group that do not already have this integration configured if you have the **Enable integration** toggle turned on in the group-level settings. -- Subgroups and projects that already have the integration configured are not affected, but can choose to use the inherited settings at any time. - -When you make further changes to the group defaults: - -- They are immediately applied to all subgroups and projects belonging to the group that have the integration set to use default settings. -- They are immediately applied to newer subgroups and projects, created since you last saved defaults for the integration. - - If your group-level default setting has the **Enable integration** toggle turned on, the integration is automatically enabled for all such subgroups and projects. -- Subgroups and projects with custom settings selected for the integration are not immediately affected and may choose to use the latest defaults at any time. - -Only the complete settings for an integration can be inherited. Per-field inheritance -is [planned](https://gitlab.com/groups/gitlab-org/-/epics/2137). This would allow -administrators to update settings inherited by subgroups and projects without enabling the -integration on all non-configured groups and projects by default. - -## Use instance-level or group-level default settings for a project integration +## Use instance-level default settings for a project integration 1. Navigate to **Project > Settings > Integrations**. 1. Choose the integration you want to enable or update. @@ -81,9 +60,9 @@ integration on all non-configured groups and projects by default. 1. Ensure the toggle is set to **Enable integration**. 1. Click **Save changes**. -## Use custom settings for a group or project integration +## Use custom settings for a project integration -1. Navigate to project or group's **Settings > Integrations**. +1. Navigate to project's **Settings > Integrations**. 1. Choose the integration you want to enable or update. 1. From the drop-down, select **Use custom settings**. 1. Ensure the toggle is set to **Enable integration** and enter all required settings. diff --git a/doc/user/group/index.md b/doc/user/group/index.md index 92ca7033dcb..2c838724cb3 100644 --- a/doc/user/group/index.md +++ b/doc/user/group/index.md @@ -742,7 +742,6 @@ To enable prevent project forking: - **Audit Events**: View [Audit Events](../../administration/audit_events.md) for the group. **(STARTER ONLY)** - **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. #### Storage usage quota **(STARTER)** diff --git a/doc/user/markdown.md b/doc/user/markdown.md index e0612bad3b8..8b65da4ab94 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -82,7 +82,7 @@ We flag any significant differences between Redcarpet and CommonMark Markdown in If you have a large volume of Markdown files, it can be tedious to determine if they display correctly or not. You can use the -[diff_redcarpet_cmark](https://gitlab.com/digitalmoksha/diff_redcarpet_cmark) +[`diff_redcarpet_cmark`](https://gitlab.com/digitalmoksha/diff_redcarpet_cmark) tool (not an officially supported product) to generate a list of files and the differences between how RedCarpet and CommonMark render the files. It gives an indication if anything needs to be changed - often nothing needs diff --git a/doc/user/packages/container_registry/index.md b/doc/user/packages/container_registry/index.md index 3152966a6b3..baadd3c91a7 100644 --- a/doc/user/packages/container_registry/index.md +++ b/doc/user/packages/container_registry/index.md @@ -67,7 +67,7 @@ Images follow this naming convention: ``` If your project is `gitlab.example.com/mynamespace/myproject`, for example, -then your image must be named `gitlab.example.com/mynamespace/myproject/my-app` at a mimimum. +then your image must be named `gitlab.example.com/mynamespace/myproject/my-app` at a minimum. You can append additional names to the end of an image name, up to three levels deep. diff --git a/doc/user/packages/workflows/monorepo.md b/doc/user/packages/workflows/monorepo.md index 94a2b8b8dba..f20f3427ac5 100644 --- a/doc/user/packages/workflows/monorepo.md +++ b/doc/user/packages/workflows/monorepo.md @@ -7,7 +7,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Monorepo package management workflows Oftentimes, one project or Git repository may contain multiple different -subprojects or submodules that all get packaged and published individually. +sub-projects or submodules that all get packaged and published individually. ## Publishing different packages to the parent project @@ -36,9 +36,9 @@ as well as `Foo`. Following the instructions in the [GitLab NPM registry documentation](../npm_registry/index.md), publishing `MyProject` consists of modifying the `package.json` file with a -`publishConfig` section, as well as either modifying your local NPM config with +`publishConfig` section, as well as either modifying your local NPM configuration with CLI commands like `npm config set`, or saving a `.npmrc` file in the root of the -project specifying these config settings. +project specifying these configuration settings. If you follow the instructions you can publish `MyProject` by running `npm publish` from the root directory. @@ -65,7 +65,7 @@ A package is associated with a project on GitLab, but the package does not need to be associated with the code in that project. Notice when configuring NPM or Maven, you only use the `Project ID` to set the registry URL that the package is to be uploaded to. If you set this to any project that you have -access to and update any other config similarly depending on the package type, +access to and update any other configuration similarly depending on the package type, your packages are published to that project. This means you can publish multiple packages to one project, even if their code does not exist in the same place. See the [project registry workflow documentation](./project_registry.md) diff --git a/doc/user/permissions.md b/doc/user/permissions.md index 92d89a303d2..2e9f36360c6 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -399,7 +399,7 @@ Administrators can add members with a "minimal access" role to a parent group. S automatically have access to projects and subgroups underneath. To support such access, administrators must explicitly add these "minimal access" users to the specific subgroups/projects. Users with minimal access can list the group in the UI and through the API. However, they cannot see -details such as projects or subgroups. They do not have access to the group's page or list any of itssubgroups or projects. +details such as projects or subgroups. They do not have access to the group's page or list any of its subgroups or projects. ## Project features diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index f84fc1ae898..61944bb9d0b 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -84,7 +84,7 @@ The default syntax theme is White, and you can choose among 5 different themes: [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in 13.0, the theme you choose also applies to the [Web IDE](../project/web_ide/index.md)'s code editor and [Snippets](../snippets.md). The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808) and -the [solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228), +the [Solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228), which apply to the entire Web IDE screen. ## Behavior @@ -131,15 +131,9 @@ You can choose between 2 options: ### Project overview content -The project overview content setting allows you to choose what content you want to +The **Project overview content** setting allows you to choose what content you want to see on a project’s home page. -You can choose between 3 options: - -- Files and Readme (default) -- Readme -- Activity - ### Tab width You can set the displayed width of tab characters across various parts of diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md index 00be3ea54b3..5816bdf782b 100644 --- a/doc/user/project/clusters/index.md +++ b/doc/user/project/clusters/index.md @@ -252,7 +252,7 @@ GitLab CI/CD build environment. | `KUBE_NAMESPACE` | The namespace associated with the project's deployment service account. In the format `<project_name>-<project_id>-<environment>`. For GitLab-managed clusters, a matching namespace is automatically created by GitLab in the cluster. If your cluster was created before GitLab 12.2, the default `KUBE_NAMESPACE` is set to `<project_name>-<project_id>`. | | `KUBE_CA_PEM_FILE` | Path to a file containing PEM data. Only present if a custom CA bundle was specified. | | `KUBE_CA_PEM` | (**deprecated**) Raw PEM data. Only if a custom CA bundle was specified. | -| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. This config also embeds the same token defined in `KUBE_TOKEN` so you likely will only need this variable. This variable name is also automatically picked up by `kubectl` so you won't actually need to reference it explicitly if using `kubectl`. | +| `KUBECONFIG` | Path to a file containing `kubeconfig` for this deployment. CA bundle would be embedded if specified. This configuration also embeds the same token defined in `KUBE_TOKEN` so you likely will only need this variable. This variable name is also automatically picked up by `kubectl` so you won't actually need to reference it explicitly if using `kubectl`. | | `KUBE_INGRESS_BASE_DOMAIN` | From GitLab 11.8, this variable can be used to set a domain per cluster. See [cluster domains](#base-domain) for more information. | ### Custom namespace diff --git a/doc/user/project/clusters/serverless/index.md b/doc/user/project/clusters/serverless/index.md index d70d4e26ee0..836df77a79b 100644 --- a/doc/user/project/clusters/serverless/index.md +++ b/doc/user/project/clusters/serverless/index.md @@ -789,7 +789,7 @@ or with other versions of Python. kubectl edit gateway knative-ingress-gateway --namespace knative-serving ``` - Update the gateway to include the following tls: section and configuration: + Update the gateway to include the following `tls:` section and configuration: ```shell tls: diff --git a/doc/user/project/code_intelligence.md b/doc/user/project/code_intelligence.md index f56673e69b7..d0c5a24826a 100644 --- a/doc/user/project/code_intelligence.md +++ b/doc/user/project/code_intelligence.md @@ -58,9 +58,9 @@ relevant language. | Language | Implementation | |---|---| -| Go | [sourcegraph/lsif-go](https://github.com/sourcegraph/lsif-go) | -| JavaScript | [sourcegraph/lsif-node](https://github.com/sourcegraph/lsif-node) | -| TypeScript | [sourcegraph/lsif-node](https://github.com/sourcegraph/lsif-node) | +| Go | [`sourcegraph/lsif-go`](https://github.com/sourcegraph/lsif-go) | +| JavaScript | [`sourcegraph/lsif-node`](https://github.com/sourcegraph/lsif-node) | +| TypeScript | [`sourcegraph/lsif-node`](https://github.com/sourcegraph/lsif-node) | View a complete list of [available LSIF indexers](https://lsif.dev/#implementations-server) on their website and refer to their documentation to see how to generate an LSIF file for your specific language. diff --git a/doc/user/project/code_owners.md b/doc/user/project/code_owners.md index 7c71d18e5be..4ae3d5ec032 100644 --- a/doc/user/project/code_owners.md +++ b/doc/user/project/code_owners.md @@ -75,7 +75,6 @@ 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)** -NOTE: **Note:** Developer or higher [permissions](../permissions.md) are required in order to approve a merge request. @@ -93,12 +92,14 @@ 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 -changes for the specified files/paths, even if their role is included in the +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 protected branch, but can restrict pushing to certain files where a review by Code Owners is required. +[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35097) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.5, users and groups who are allowed to push to protected branches do not require a merge request to merge their feature branches. Thus, they can skip merge request approval rules, Code Owners included. + ## The syntax of Code Owners files Files can be specified using the same kind of patterns you would use diff --git a/doc/user/project/deploy_boards.md b/doc/user/project/deploy_boards.md index 536d5950228..3c6494d5f1a 100644 --- a/doc/user/project/deploy_boards.md +++ b/doc/user/project/deploy_boards.md @@ -85,7 +85,7 @@ To display the Deploy Boards for a specific [environment](../../ci/environments/ [`kubernetes`](https://docs.gitlab.com/runner/executors/kubernetes.html) executor. 1. Configure the [Kubernetes integration](clusters/index.md) in your project for the cluster. The Kubernetes namespace is of particular note as you will need it - for your deployment scripts (exposed by the `KUBE_NAMESPACE` env variable). + for your deployment scripts (exposed by the `KUBE_NAMESPACE` environment variable). 1. Ensure Kubernetes annotations of `app.gitlab.com/env: $CI_ENVIRONMENT_SLUG` and `app.gitlab.com/app: $CI_PROJECT_PATH_SLUG` are applied to the deployments, replica sets, and pods, where `$CI_ENVIRONMENT_SLUG` and diff --git a/doc/user/project/file_lock.md b/doc/user/project/file_lock.md index 6fd33901621..46c2e211d57 100644 --- a/doc/user/project/file_lock.md +++ b/doc/user/project/file_lock.md @@ -69,7 +69,7 @@ brew install git-lfs ``` Once installed, **open your local repository in a terminal window** and -install Git LFS in your repo. If you're sure that LFS is already installed, +install Git LFS in your repository. If you're sure that LFS is already installed, you can skip this step. If you're unsure, re-installing it won't do any harm: ```shell @@ -159,7 +159,7 @@ command line interface, file locks can be created for any file. ### View exclusively-locked files To list all the files locked with LFS locally, open a terminal window in your -repo and run: +repository and run: ```shell git lfs locks @@ -189,7 +189,7 @@ Suggested workflow for shared projects: 1. Lock the file. 1. Edit the file. 1. Commit your changes. -1. Push to the repo. +1. Push to the repository. 1. Get your changes reviewed, approved, and merged. 1. Unlock the file. diff --git a/doc/user/project/import/gemnasium.md b/doc/user/project/import/gemnasium.md index f21ec26bdef..2d0caa7d46e 100644 --- a/doc/user/project/import/gemnasium.md +++ b/doc/user/project/import/gemnasium.md @@ -96,7 +96,7 @@ back to both GitLab and GitHub when completed. The mirroring is pull-only by default, so you may create or update the file on GitHub: - ![Edit gitlab-ci.yml file](img/gemnasium/edit_gitlab-ci.png) + ![Edit YAML file](img/gemnasium/edit_gitlab-ci.png) 1. Once your file has been committed, a new pipeline will be automatically triggered if your file is valid: diff --git a/doc/user/project/import/github.md b/doc/user/project/import/github.md index be1641f8b16..6c0105aaded 100644 --- a/doc/user/project/import/github.md +++ b/doc/user/project/import/github.md @@ -101,7 +101,7 @@ If you are using a self-managed GitLab instance or if you are importing from Git 1. From the top navigation bar, click **+** and select **New project**. 1. Select the **Import project** tab and then select **GitHub**. 1. Select the first button to **List your GitHub repositories**. You are redirected to a page on [GitHub](https://github.com) to authorize the GitLab application. -1. Click **Authorize gitlabhq**. You are redirected back to GitLab's Import page and all of your GitHub repositories are listed. +1. Click **Authorize GitlabHQ**. You are redirected back to GitLab's Import page and all of your GitHub repositories are listed. 1. Continue on to [selecting which repositories to import](#selecting-which-repositories-to-import). ### Using a GitHub token @@ -119,7 +119,7 @@ If you are not using the GitHub integration, you can still perform an authorizat 1. Go to <https://github.com/settings/tokens/new> 1. Enter a token description. -1. Select the repo scope. +1. Select the repository scope. 1. Click **Generate token**. 1. Copy the token hash. 1. Go back to GitLab and provide the token to the GitHub importer. @@ -136,10 +136,10 @@ your GitHub repositories are listed. 1. Select the **Import** button next to any number of repositories, or select **Import all repositories**. Additionally, you can filter projects by name. If filter is applied, **Import all repositories** only imports matched repositories. 1. The **Status** column shows the import status of each repository. You can choose to leave the page open and it will - update in realtime or you can return to it later. + update in real-time or you can return to it later. 1. Once a repository has been imported, click its GitLab path to open its GitLab URL. -![Github importer page](img/import_projects_from_github_importer_v12_3.png) +![GitHub importer page](img/import_projects_from_github_importer_v12_3.png) ## Mirroring and pipeline status sharing @@ -149,7 +149,7 @@ your imported repository in sync with its GitHub copy. Additionally, you can configure GitLab to send pipeline status updates back GitHub with the [GitHub Project Integration](../integrations/github.md). **(PREMIUM)** -If you import your project using [CI/CD for external repo](../../../ci/ci_cd_for_external_repos/index.md), then both +If you import your project using [CI/CD for external repository](../../../ci/ci_cd_for_external_repos/index.md), then both of the above are automatically configured. **(PREMIUM)** ## Improving the speed of imports on self-managed instances diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md index 86b671c8371..a1c28cfa2b7 100644 --- a/doc/user/project/import/index.md +++ b/doc/user/project/import/index.md @@ -18,7 +18,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w 1. [From Perforce](perforce.md) 1. [From SVN](svn.md) 1. [From TFVC](tfvc.md) -1. [From repo by URL](repo_by_url.md) +1. [From repository by URL](repo_by_url.md) 1. [By uploading a manifest file (AOSP)](manifest.md) 1. [From Gemnasium](gemnasium.md) 1. [From Phabricator](phabricator.md) @@ -32,7 +32,7 @@ There is also the option of [connecting your external repository to get CI/CD be ## Migrating from self-managed GitLab to GitLab.com -If you only need to migrate Git repos, you can [import each project by URL](repo_by_url.md). Issues and merge requests can't be imported. +If you only need to migrate Git repositories, you can [import each project by URL](repo_by_url.md). Issues and merge requests can't be imported. If you want to retain all metadata like issues and merge requests, you can use the [import/export feature](../settings/import_export.md) to export projects from self-managed GitLab and import those projects into GitLab.com. diff --git a/doc/user/project/import/manifest.md b/doc/user/project/import/manifest.md index 60524f3cc69..ba1e2011d08 100644 --- a/doc/user/project/import/manifest.md +++ b/doc/user/project/import/manifest.md @@ -56,7 +56,7 @@ You can start the import with: 1. From your GitLab dashboard click **New project** 1. Switch to the **Import project** tab 1. Click on the **Manifest file** button -1. Provide GitLab with a manifest xml file +1. Provide GitLab with a manifest XML file 1. Select a group you want to import to (you need to create a group first if you don't have one) 1. Click **List available repositories**. At this point, you will be redirected to the import status page with projects list based on the manifest file. diff --git a/doc/user/project/import/perforce.md b/doc/user/project/import/perforce.md index dbc1c491493..4ccc34efe30 100644 --- a/doc/user/project/import/perforce.md +++ b/doc/user/project/import/perforce.md @@ -20,7 +20,7 @@ Git: it creates an integration record in their proprietary database for every file in the branch, regardless how many were actually changed. Whereas Git was implemented with a different architecture so that a single SHA acts as a pointer - to the state of the whole repo after the changes, making it very easy to branch. + to the state of the whole repository after the changes, making it very easy to branch. This is what made feature branching workflows so easy to adopt with Git. 1. Also, context switching between branches is much easier in Git. If your manager said 'You need to stop work on that new feature and fix this security diff --git a/doc/user/project/import/repo_by_url.md b/doc/user/project/import/repo_by_url.md index 9b5e43aae79..5c53b6eaf06 100644 --- a/doc/user/project/import/repo_by_url.md +++ b/doc/user/project/import/repo_by_url.md @@ -5,7 +5,7 @@ group: Import info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers --- -# Import project from repo by URL +# Import project from repository by URL You can import your existing repositories by providing the Git URL: @@ -16,4 +16,4 @@ You can import your existing repositories by providing the Git URL: 1. Click **Create project** to begin the import process 1. Once complete, you will be redirected to your newly created project -![Import project by repo URL](img/import_projects_from_repo_url.png) +![Import project by repository URL](img/import_projects_from_repo_url.png) diff --git a/doc/user/project/integrations/irker.md b/doc/user/project/integrations/irker.md index 443ca11be27..bb4a5b2b97f 100644 --- a/doc/user/project/integrations/irker.md +++ b/doc/user/project/integrations/irker.md @@ -14,8 +14,8 @@ See the project homepage for further information: <https://gitlab.com/esr/irker> ## Needed setup -You will first need an Irker daemon. You can download the Irker code from its -repository on <https://gitlab.com/esr/irker>: +You will first need an Irker daemon. You can download the Irker code +[from its repository](https://gitlab.com/esr/irker): ```shell git clone https://gitlab.com/esr/irker.git @@ -55,6 +55,6 @@ case, `Aorimn` is treated as a nick and no more as a channel name. Irker can also join password-protected channels. Users need to append `?key=thesecretpassword` to the channel name. When using this feature remember to **not** put the `#` sign in front of the channel name; failing to do so will -result on irker joining a channel literally named `#chan?key=password` henceforth +result on Irker joining a channel literally named `#chan?key=password` henceforth leaking the channel key through the `/whois` IRC command (depending on IRC server -configuration). This is due to a long standing irker bug. +configuration). This is due to a long standing Irker bug. diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md index 5843272bb5a..bce40e9a838 100644 --- a/doc/user/project/issue_board.md +++ b/doc/user/project/issue_board.md @@ -368,7 +368,7 @@ If you're not able to do some of the things above, make sure you have the right ### First time using an issue board -> The automatic creation of the **To Do** and **Doing** lists was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202144) in GitLab 13.4. +> The automatic creation of the **To Do** and **Doing** lists was [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/202144) in GitLab 13.5. The first time you open an issue board, you are presented with the default lists (**Open**, **To Do**, **Doing**, and **Closed**). diff --git a/doc/user/project/merge_requests/accessibility_testing.md b/doc/user/project/merge_requests/accessibility_testing.md index f3a0aac9ff4..a07a155745e 100644 --- a/doc/user/project/merge_requests/accessibility_testing.md +++ b/doc/user/project/merge_requests/accessibility_testing.md @@ -55,7 +55,7 @@ include: ``` creates an `a11y` job in your CI/CD pipeline, runs -Pa11y against the webpages defined in `a11y_urls`, and builds an HTML report for each. +Pa11y against the web pages defined in `a11y_urls`, and builds an HTML report for each. The report for each URL is saved as an artifact that can be [viewed directly in your browser](../../../ci/pipelines/job_artifacts.md#browsing-artifacts). diff --git a/doc/user/project/merge_requests/load_performance_testing.md b/doc/user/project/merge_requests/load_performance_testing.md index daebd71e14f..2675f509eed 100644 --- a/doc/user/project/merge_requests/load_performance_testing.md +++ b/doc/user/project/merge_requests/load_performance_testing.md @@ -164,8 +164,8 @@ For example: 1. Capture the dynamic URL and save it into a `.env` file, e.g. `echo "ENVIRONMENT_URL=$CI_ENVIRONMENT_URL" >> review.env`. 1. Set the `.env` file to be a [job artifact](../../../ci/pipelines/job_artifacts.md#job-artifacts). 1. In the `load_performance` job: - 1. Set it to depend on the review job, so it inherits the env file. - 1. Set the `K6_DOCKER_OPTIONS` variable with the [Docker cli option for env files](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file), for example `--env-file review.env`. + 1. Set it to depend on the review job, so it inherits the environment file. + 1. Set the `K6_DOCKER_OPTIONS` variable with the [Docker CLI option for environment files](https://docs.docker.com/engine/reference/commandline/run/#set-environment-variables--e---env---env-file), for example `--env-file review.env`. 1. Configure the k6 test script to use the environment variable in it's steps. Your `.gitlab-ci.yml` file might be similar to: 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 2e0c0d7aeeb..aef68e0e771 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 @@ -84,7 +84,7 @@ Click **Expand file** on any file to view the changes for that file. > - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#enable-or-disable-file-by-file-diff-navigation). 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 navbar, click **Settings**, and go to **Preferences** on the left +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. diff --git a/doc/user/project/merge_requests/squash_and_merge.md b/doc/user/project/merge_requests/squash_and_merge.md index 69a0dd6e84f..68f5478038a 100644 --- a/doc/user/project/merge_requests/squash_and_merge.md +++ b/doc/user/project/merge_requests/squash_and_merge.md @@ -65,8 +65,8 @@ meaningful commit messages and: ## Enabling squash for a merge request Anyone who can create or edit a merge request can choose for it to be squashed -on the merge request form. Users can select or unselect the checkbox at the moment -they are creating the merge request: +on the merge request form. Users can select or clear the check box when they +create the merge request: ![Squash commits checkbox on edit form](img/squash_edit_form.png) diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md index 735d27ec04d..810538ab460 100644 --- a/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md +++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/dns_concepts.md @@ -30,6 +30,8 @@ to do it for you. To help you out, we've gathered some instructions on how to do that for the most popular hosting services: +<!-- vale gitlab.Spelling = NO --> + - [Amazon](https://docs.aws.amazon.com/AmazonS3/latest/dev/website-hosting-custom-domain-walkthrough.html) - [Bluehost](https://www.bluehost.com/help/article/dns-management-add-edit-or-delete-dns-entries) - [Cloudflare](https://support.cloudflare.com/hc/en-us/articles/201720164-Creating-a-Cloudflare-account-and-adding-a-website) @@ -41,6 +43,8 @@ for the most popular hosting services: - [Media Temple](https://mediatemple.net/community/products/dv/204403794/how-can-i-change-the-dns-records-for-my-domain) - [Microsoft](https://docs.microsoft.com/en-us/previous-versions/windows/it-pro/windows-2000-server/bb727018(v=technet.10)) +<!-- vale gitlab.Spelling = YES --> + If your hosting service is not listed above, you can just try to search the web for `how to add dns record on <my hosting service>`. diff --git a/doc/user/project/pages/getting_started/new_or_existing_website.md b/doc/user/project/pages/getting_started/new_or_existing_website.md index 86f36447b93..f19334a1764 100644 --- a/doc/user/project/pages/getting_started/new_or_existing_website.md +++ b/doc/user/project/pages/getting_started/new_or_existing_website.md @@ -2,4 +2,4 @@ redirect_to: 'pages_ci_cd_template.md' --- -This document was moved to [pages_ci_cd_template.md](pages_ci_cd_template.md). +This document was moved to [another location](pages_ci_cd_template.md). diff --git a/doc/user/project/pages/getting_started/pages_forked_sample_project.md b/doc/user/project/pages/getting_started/pages_forked_sample_project.md index de9bd97b262..7dc3d2197b5 100644 --- a/doc/user/project/pages/getting_started/pages_forked_sample_project.md +++ b/doc/user/project/pages/getting_started/pages_forked_sample_project.md @@ -53,4 +53,4 @@ You can take some **optional** further steps: ![Change repo's path](../img/change_path_v12_10.png) - Now go to your SSG's configuration file and change the [base URL](../getting_started_part_one.md#urls-and-baseurls) - from `"project-name"` to `""`. The project name setting varies by SSG and may not be in the config file. + from `"project-name"` to `""`. The project name setting varies by SSG and may not be in the configuration file. diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md index 09757bc73eb..7265fd330e3 100644 --- a/doc/user/project/protected_branches.md +++ b/doc/user/project/protected_branches.md @@ -185,6 +185,8 @@ When enabled, all merge requests targeting these branches will 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. +[Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/35097) in [GitLab Premium](https://about.gitlab.com/pricing/) 13.5, users and groups who are allowed to push to protected branches do not require a merge request to merge their feature branches. Thus, they can skip merge request approval rules. + ## Running pipelines on protected branches The permission to merge or push to protected branches is used to define if a user can diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md index 536cae263b8..5473439a162 100644 --- a/doc/user/project/repository/index.md +++ b/doc/user/project/repository/index.md @@ -86,7 +86,7 @@ according to the markup language. | [reStructuredText](https://docutils.sourceforge.io/rst.html) | `rst` | | [AsciiDoc](../../asciidoc.md) | `adoc`, `ad`, `asciidoc` | | [Textile](https://textile-lang.com/) | `textile` | -| [rdoc](http://rdoc.sourceforge.net/doc/index.html) | `rdoc` | +| [Rdoc](http://rdoc.sourceforge.net/doc/index.html) | `rdoc` | | [Org mode](https://orgmode.org/) | `org` | | [creole](http://www.wikicreole.org/) | `creole` | | [MediaWiki](https://www.mediawiki.org/wiki/MediaWiki) | `wiki`, `mediawiki` | @@ -234,7 +234,7 @@ lock your files to prevent any conflicting changes. ## Repository's API -You can access your repos via [repository API](../../../api/repositories.md). +You can access your repositories via [repository API](../../../api/repositories.md). ## Clone in Apple Xcode diff --git a/doc/user/project/requirements/img/requirement_create_v13_5.png b/doc/user/project/requirements/img/requirement_create_v13_5.png Binary files differnew file mode 100644 index 00000000000..ef1bab6e6d2 --- /dev/null +++ b/doc/user/project/requirements/img/requirement_create_v13_5.png diff --git a/doc/user/project/requirements/img/requirement_view_v13_5.png b/doc/user/project/requirements/img/requirement_view_v13_5.png Binary files differnew file mode 100644 index 00000000000..7fcb24a5e3b --- /dev/null +++ b/doc/user/project/requirements/img/requirement_view_v13_5.png diff --git a/doc/user/project/requirements/img/requirements_list_v13_1.png b/doc/user/project/requirements/img/requirements_list_v13_1.png Binary files differdeleted file mode 100644 index 0ebda571928..00000000000 --- a/doc/user/project/requirements/img/requirements_list_v13_1.png +++ /dev/null diff --git a/doc/user/project/requirements/img/requirements_list_v13_5.png b/doc/user/project/requirements/img/requirements_list_v13_5.png Binary files differnew file mode 100644 index 00000000000..19516e5e66e --- /dev/null +++ b/doc/user/project/requirements/img/requirements_list_v13_5.png diff --git a/doc/user/project/requirements/index.md b/doc/user/project/requirements/index.md index ffdb7e21472..f533f8807d2 100644 --- a/doc/user/project/requirements/index.md +++ b/doc/user/project/requirements/index.md @@ -7,7 +7,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Requirements Management **(ULTIMATE)** -> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2703) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10. +> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2703) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10. +> - The ability to add and edit a requirement's long description [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/224622) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.5. With requirements, you can set criteria to check your products against. They can be based on users, stakeholders, system, software, or anything else you find important to capture. @@ -22,7 +23,7 @@ When a feature is no longer necessary, you can [archive the related requirement] <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> For an overview, see [GitLab 12.10 Introduces Requirements Management](https://www.youtube.com/watch?v=uSS7oUNSEoU). -![requirements list view](img/requirements_list_v13_1.png) +![requirements list view](img/requirements_list_v13_5.png) ## Create a requirement @@ -32,31 +33,43 @@ can create a new requirement. To create a requirement: 1. From your project page, go to **{requirements}** **Requirements**. -1. Click **New requirement**. -1. Enter a descriptive title and click **Create requirement**. +1. Select **New requirement**. +1. Enter a title and description and select **Create requirement**. -You will see the newly created requirement on the top of the list, as the requirements -list is sorted by creation date in descending order. +![requirement create view](img/requirement_create_v13_5.png) + +You can see the newly created requirement on the top of the list, with the requirements +list being sorted by creation date, in descending order. + +## View a requirement + +You can view a requirement from the list by selecting it. + +![requirement view](img/requirement_view_v13_5.png) + +To edit a requirement while viewing it, select the **Edit** icon (**{pencil}**) +next to the requirement title. ## Edit a requirement -> - [Added](https://gitlab.com/gitlab-org/gitlab/-/issues/218607) ability to mark a requirement as Satisfied in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.5. +> The ability to mark a requirement as Satisfied [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/218607) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.5. You can edit a requirement (if you have the necessary privileges) from the requirements list page. To edit a requirement: -1. From the requirements list, click **Edit** (**{pencil}**). -1. Update the title in text input field. You can also mark a requirement as satisfied in the edit form by using the checkbox labeled "Satisfied". -1. Click **Save changes**. +1. From the requirements list, select the **Edit** icon (**{pencil}**). +1. Update the title and description in text input field. You can also mark a + requirement as satisfied in the edit form by using the check box **Satisfied**. +1. Select **Save changes**. ## Archive a requirement You can archive an open requirement (if you have the necessary privileges) while you're in the **Open** tab. -To archive a requirement, click **Archive** (**{archive}**). +To archive a requirement, select **Archive** (**{archive}**). As soon as a requirement is archived, it no longer appears in the **Open** tab. @@ -66,7 +79,7 @@ You can view the list of archived requirements in the **Archived** tab. ![archived requirements list](img/requirements_archived_list_view_v13_1.png) -To reopen an archived requirement, click **Reopen**. +To reopen an archived requirement, select **Reopen**. As soon as a requirement is reopened, it no longer appears in the **Archived** tab. @@ -82,7 +95,7 @@ You can search for a requirement from the requirements list page based on the fo To search for a requirement: 1. In a project, go to **{requirements}** **Requirements > List**. -1. Click the **Search or filter results** field. A dropdown menu appears. +1. Select the **Search or filter results** field. A dropdown menu appears. 1. Select the requirement author from the dropdown or enter plain text to search by requirement title. 1. Press <kbd>Enter</kbd> on your keyboard to filter the list. diff --git a/lib/banzai/reference_parser.rb b/lib/banzai/reference_parser.rb index c08d3364a87..8b5d689a984 100644 --- a/lib/banzai/reference_parser.rb +++ b/lib/banzai/reference_parser.rb @@ -2,6 +2,8 @@ module Banzai module ReferenceParser + InvalidReferenceType = Class.new(StandardError) + # Returns the reference parser class for the given type # # Example: @@ -11,6 +13,8 @@ module Banzai # This would return the `Banzai::ReferenceParser::IssueParser` class. def self.[](name) const_get("#{name.to_s.camelize}Parser", false) + rescue NameError + raise InvalidReferenceType end end end diff --git a/lib/banzai/reference_redactor.rb b/lib/banzai/reference_redactor.rb index 936436982e7..81e4fd45966 100644 --- a/lib/banzai/reference_redactor.rb +++ b/lib/banzai/reference_redactor.rb @@ -111,6 +111,7 @@ module Banzai parser = Banzai::ReferenceParser[type].new(context) visible.merge(parser.nodes_visible_to_user(user, nodes)) + rescue Banzai::ReferenceParser::InvalidReferenceType end visible diff --git a/lib/gitlab/auth/otp/strategies/base.rb b/lib/gitlab/auth/otp/strategies/base.rb new file mode 100644 index 00000000000..718630e0e31 --- /dev/null +++ b/lib/gitlab/auth/otp/strategies/base.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Otp + module Strategies + class Base + def initialize(user) + @user = user + end + + private + + attr_reader :user + + def success + { status: :success } + end + + def error(message, http_status = nil) + result = { message: message, + status: :error } + + result[:http_status] = http_status if http_status + + result + end + end + end + end + end +end diff --git a/lib/gitlab/auth/otp/strategies/devise.rb b/lib/gitlab/auth/otp/strategies/devise.rb new file mode 100644 index 00000000000..93068d6c9b0 --- /dev/null +++ b/lib/gitlab/auth/otp/strategies/devise.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Otp + module Strategies + class Devise < Base + def validate(otp_code) + user.validate_and_consume_otp!(otp_code) ? success : error('invalid OTP code') + end + end + end + end + end +end diff --git a/lib/gitlab/auth/otp/strategies/forti_authenticator.rb b/lib/gitlab/auth/otp/strategies/forti_authenticator.rb new file mode 100644 index 00000000000..fbcb9fd8cdb --- /dev/null +++ b/lib/gitlab/auth/otp/strategies/forti_authenticator.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Gitlab + module Auth + module Otp + module Strategies + class FortiAuthenticator < Base + def validate(otp_code) + body = { username: user.username, + token_code: otp_code } + + response = Gitlab::HTTP.post( + auth_url, + headers: { 'Content-Type': 'application/json' }, + body: body.to_json, + basic_auth: api_credentials) + + # Successful authentication results in HTTP 200: OK + # https://docs.fortinet.com/document/fortiauthenticator/6.2.0/rest-api-solution-guide/704555/authentication-auth + response.ok? ? success : error(message: response.message, http_status: response.code) + end + + private + + def auth_url + host = ::Gitlab.config.forti_authenticator.host + port = ::Gitlab.config.forti_authenticator.port + path = 'api/v1/auth/' + + "https://#{host}:#{port}/#{path}" + end + + def api_credentials + { username: ::Gitlab.config.forti_authenticator.username, + password: ::Gitlab.config.forti_authenticator.token } + end + end + end + end + end +end diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 373170c8e12..66b6ce1ec55 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -544,6 +544,16 @@ module Gitlab rename_column_concurrently(table, column, temp_column, type: new_type, type_cast_function: type_cast_function, batch_column_name: batch_column_name) end + # Reverses operations performed by change_column_type_concurrently. + # + # table - The table containing the column. + # column - The name of the column to change. + def undo_change_column_type_concurrently(table, column) + temp_column = "#{column}_for_type_change" + + undo_rename_column_concurrently(table, column, temp_column) + end + # Performs cleanup of a concurrent type change. # # table - The table containing the column. @@ -560,6 +570,65 @@ module Gitlab end end + # Reverses operations performed by cleanup_concurrent_column_type_change. + # + # table - The table containing the column. + # column - The name of the column to change. + # old_type - The type of the original column used with change_column_type_concurrently. + # type_cast_function - Required if the conversion back to the original type is not automatic + # batch_column_name - option for tables without a primary key, in this case + # another unique integer column can be used. Example: :user_id + def undo_cleanup_concurrent_column_type_change(table, column, old_type, type_cast_function: nil, batch_column_name: :id) + temp_column = "#{column}_for_type_change" + + # Using a descriptive name that includes orinal column's name risks + # taking us above the 63 character limit, so we use a hash + identifier = "#{table}_#{column}_for_type_change" + hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) + temp_undo_cleanup_column = "tmp_undo_cleanup_column_#{hashed_identifier}" + + unless column_exists?(table, batch_column_name) + raise "Column #{batch_column_name} does not exist on #{table}" + end + + if transaction_open? + raise 'undo_cleanup_concurrent_column_type_change can not be run inside a transaction' + end + + check_trigger_permissions!(table) + + begin + create_column_from( + table, + column, + temp_undo_cleanup_column, + type: old_type, + batch_column_name: batch_column_name, + type_cast_function: type_cast_function + ) + + transaction do + # This has to be performed in a transaction as otherwise we might + # have inconsistent data. + rename_column(table, column, temp_column) + rename_column(table, temp_undo_cleanup_column, column) + + install_rename_triggers(table, column, temp_column) + end + rescue + # create_column_from can not run inside a transaction, which means + # that there is a risk that if any of the operations that follow it + # fail, we'll be left with an inconsistent schema + # For those reasons, we make sure that we drop temp_undo_cleanup_column + # if an error is caught + if column_exists?(table, temp_undo_cleanup_column) + remove_column(table, temp_undo_cleanup_column) + end + + raise + end + end + # Cleans up a concurrent column name. # # This method takes care of removing previously installed triggers as well diff --git a/lib/gitlab/import_export/json/ndjson_reader.rb b/lib/gitlab/import_export/json/ndjson_reader.rb index e9b05afc7d4..0d9839b86cf 100644 --- a/lib/gitlab/import_export/json/ndjson_reader.rb +++ b/lib/gitlab/import_export/json/ndjson_reader.rb @@ -35,6 +35,7 @@ module Gitlab # This reads from `tree/project/merge_requests.ndjson` path = file_path(importable_path, "#{key}.ndjson") + next unless File.exist?(path) File.foreach(path, MAX_JSON_DOCUMENT_SIZE).with_index do |line, line_num| @@ -43,6 +44,11 @@ module Gitlab end end + # TODO: Move clear logic into main comsume_relation method (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430465330) + def clear_consumed_relations + @consumed_relations.clear + end + private def json_decode(string) diff --git a/lib/gitlab/import_export/project/sample/date_calculator.rb b/lib/gitlab/import_export/project/sample/date_calculator.rb new file mode 100644 index 00000000000..2d989d21166 --- /dev/null +++ b/lib/gitlab/import_export/project/sample/date_calculator.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module Gitlab + module ImportExport + module Project + module Sample + class DateCalculator + include Gitlab::Utils::StrongMemoize + + def initialize(dates) + @dates = dates.dup + @dates.flatten! + @dates.compact! + @dates.sort! + @dates.map! { |date| date.to_time.to_f } + end + + def closest_date_to_average + strong_memoize(:closest_date_to_average) do + next if @dates.empty? + + average_date = (@dates.first + @dates.last) / 2.0 + closest_date = @dates.min_by { |date| (date - average_date).abs } + Time.zone.at(closest_date) + end + end + + def calculate_by_closest_date_to_average(date) + return date unless closest_date_to_average && closest_date_to_average < Time.current + + date + (Time.current - closest_date_to_average).seconds + end + end + end + end + end +end diff --git a/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer.rb b/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer.rb new file mode 100644 index 00000000000..b0c3940b5f9 --- /dev/null +++ b/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module Gitlab + module ImportExport + module Project + module Sample + class SampleDataRelationTreeRestorer < RelationTreeRestorer + DATE_MODELS = %i[issues milestones].freeze + + def initialize(*args) + super + + date_calculator + end + + private + + def build_relation(relation_key, relation_definition, data_hash) + # Override due date attributes in data hash for Sample Data templates + # Dates are moved by taking the closest one to average and moving that (and rest around it) to the date of import + # TODO: To move this logic to RelationFactory (see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430465333) + override_date_attributes!(relation_key, data_hash) + super + end + + def override_date_attributes!(relation_key, data_hash) + return unless DATE_MODELS.include?(relation_key.to_sym) + + data_hash['start_date'] = date_calculator.calculate_by_closest_date_to_average(data_hash['start_date'].to_time) unless data_hash['start_date'].nil? + data_hash['due_date'] = date_calculator.calculate_by_closest_date_to_average(data_hash['due_date'].to_time) unless data_hash['due_date'].nil? + end + + # TODO: Move clear logic into main comsume_relation method (see https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430465330) + def dates + unless relation_reader.legacy? + DATE_MODELS.map do |tag| + relation_reader.consume_relation(@importable_path, tag).map { |model| model.first['due_date'] }.tap do + relation_reader.clear_consumed_relations + end + end + end + end + + def date_calculator + @date_calculator ||= Gitlab::ImportExport::Project::Sample::DateCalculator.new(dates) + end + end + end + end + end +end diff --git a/lib/gitlab/import_export/project/tree_restorer.rb b/lib/gitlab/import_export/project/tree_restorer.rb index a16ffe36054..b1d647281ab 100644 --- a/lib/gitlab/import_export/project/tree_restorer.rb +++ b/lib/gitlab/import_export/project/tree_restorer.rb @@ -70,7 +70,7 @@ module Gitlab end def relation_tree_restorer - @relation_tree_restorer ||= RelationTreeRestorer.new( + @relation_tree_restorer ||= relation_tree_restorer_class.new( user: @user, shared: @shared, relation_reader: relation_reader, @@ -84,6 +84,14 @@ module Gitlab ) end + def relation_tree_restorer_class + sample_data_template? ? Sample::SampleDataRelationTreeRestorer : RelationTreeRestorer + end + + def sample_data_template? + @project&.import_data&.data&.dig('sample_data') + end + def members_mapper @members_mapper ||= Gitlab::ImportExport::MembersMapper.new(exported_members: @project_members, user: @user, diff --git a/lib/gitlab/markdown_cache.rb b/lib/gitlab/markdown_cache.rb index da3b597a74e..36e9a6ccef6 100644 --- a/lib/gitlab/markdown_cache.rb +++ b/lib/gitlab/markdown_cache.rb @@ -3,7 +3,7 @@ module Gitlab module MarkdownCache # Increment this number every time the renderer changes its output - CACHE_COMMONMARK_VERSION = 25 + CACHE_COMMONMARK_VERSION = 26 CACHE_COMMONMARK_VERSION_START = 10 BaseError = Class.new(StandardError) diff --git a/lib/gitlab/middleware/multipart.rb b/lib/gitlab/middleware/multipart.rb index e7e18b3bb82..7e98f1fc1f7 100644 --- a/lib/gitlab/middleware/multipart.rb +++ b/lib/gitlab/middleware/multipart.rb @@ -229,7 +229,7 @@ module Gitlab private def handler_class - if Feature.enabled?(:upload_middleware_jwt_params_handler) + if Feature.enabled?(:upload_middleware_jwt_params_handler, default_enabled: true) ::Gitlab::Middleware::Multipart::HandlerForJWTParams else ::Gitlab::Middleware::Multipart::Handler diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb index fa3af269bbf..e1574533fda 100644 --- a/lib/gitlab/project_template.rb +++ b/lib/gitlab/project_template.rb @@ -36,36 +36,37 @@ module Gitlab name == other.name && title == other.title end - def self.localized_templates_table - [ - ProjectTemplate.new('rails', 'Ruby on Rails', _('Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/rails', 'illustrations/logos/rails.svg'), - ProjectTemplate.new('spring', 'Spring', _('Includes an MVC structure, mvnw and pom.xml to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/spring', 'illustrations/logos/spring.svg'), - ProjectTemplate.new('express', 'NodeJS Express', _('Includes an MVC structure to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/express', 'illustrations/logos/express.svg'), - ProjectTemplate.new('iosswift', 'iOS (Swift)', _('A ready-to-go template for use with iOS Swift apps.'), 'https://gitlab.com/gitlab-org/project-templates/iosswift', 'illustrations/logos/swift.svg'), - ProjectTemplate.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore', 'illustrations/logos/dotnet.svg'), - ProjectTemplate.new('android', 'Android', _('A ready-to-go template for use with Android apps.'), 'https://gitlab.com/gitlab-org/project-templates/android', 'illustrations/logos/android.svg'), - ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development.'), 'https://gitlab.com/gitlab-org/project-templates/go-micro', 'illustrations/logos/gomicro.svg'), - ProjectTemplate.new('gatsby', 'Pages/Gatsby', _('Everything you need to create a GitLab Pages site using Gatsby.'), 'https://gitlab.com/pages/gatsby'), - ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo.'), 'https://gitlab.com/pages/hugo', 'illustrations/logos/hugo.svg'), - ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll.'), 'https://gitlab.com/pages/jekyll', 'illustrations/logos/jekyll.svg'), - ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML.'), 'https://gitlab.com/pages/plain-html'), - ProjectTemplate.new('gitbook', 'Pages/GitBook', _('Everything you need to create a GitLab Pages site using GitBook.'), 'https://gitlab.com/pages/gitbook', 'illustrations/logos/gitbook.svg'), - ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo.'), 'https://gitlab.com/pages/hexo', 'illustrations/logos/hexo.svg'), - ProjectTemplate.new('sse_middleman', 'Static Site Editor/Middleman', _('Middleman project with Static Site Editor support'), 'https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman'), - ProjectTemplate.new('gitpod_spring_petclinic', 'Gitpod/Spring Petclinic', _('A Gitpod configured Webapplication in Spring and Java'), 'https://gitlab.com/gitlab-org/project-templates/gitpod-spring-petclinic', 'illustrations/logos/gitpod.svg'), - ProjectTemplate.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhugo', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'), - ProjectTemplate.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'), - ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg'), - ProjectTemplate.new('jsonnet', 'Jsonnet for Dynamic Child Pipelines', _('An example showing how to use Jsonnet with GitLab dynamic child pipelines'), 'https://gitlab.com/gitlab-org/project-templates/jsonnet'), - ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab.'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management') - ].freeze - end - class << self + # TODO: Review child inheritance of this table (see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41699#note_430928221) + def localized_templates_table + [ + ProjectTemplate.new('rails', 'Ruby on Rails', _('Includes an MVC structure, Gemfile, Rakefile, along with many others, to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/rails', 'illustrations/logos/rails.svg'), + ProjectTemplate.new('spring', 'Spring', _('Includes an MVC structure, mvnw and pom.xml to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/spring', 'illustrations/logos/spring.svg'), + ProjectTemplate.new('express', 'NodeJS Express', _('Includes an MVC structure to help you get started.'), 'https://gitlab.com/gitlab-org/project-templates/express', 'illustrations/logos/express.svg'), + ProjectTemplate.new('iosswift', 'iOS (Swift)', _('A ready-to-go template for use with iOS Swift apps.'), 'https://gitlab.com/gitlab-org/project-templates/iosswift', 'illustrations/logos/swift.svg'), + ProjectTemplate.new('dotnetcore', '.NET Core', _('A .NET Core console application template, customizable for any .NET Core project'), 'https://gitlab.com/gitlab-org/project-templates/dotnetcore', 'illustrations/logos/dotnet.svg'), + ProjectTemplate.new('android', 'Android', _('A ready-to-go template for use with Android apps.'), 'https://gitlab.com/gitlab-org/project-templates/android', 'illustrations/logos/android.svg'), + ProjectTemplate.new('gomicro', 'Go Micro', _('Go Micro is a framework for micro service development.'), 'https://gitlab.com/gitlab-org/project-templates/go-micro', 'illustrations/logos/gomicro.svg'), + ProjectTemplate.new('gatsby', 'Pages/Gatsby', _('Everything you need to create a GitLab Pages site using Gatsby.'), 'https://gitlab.com/pages/gatsby'), + ProjectTemplate.new('hugo', 'Pages/Hugo', _('Everything you need to create a GitLab Pages site using Hugo.'), 'https://gitlab.com/pages/hugo', 'illustrations/logos/hugo.svg'), + ProjectTemplate.new('jekyll', 'Pages/Jekyll', _('Everything you need to create a GitLab Pages site using Jekyll.'), 'https://gitlab.com/pages/jekyll', 'illustrations/logos/jekyll.svg'), + ProjectTemplate.new('plainhtml', 'Pages/Plain HTML', _('Everything you need to create a GitLab Pages site using plain HTML.'), 'https://gitlab.com/pages/plain-html'), + ProjectTemplate.new('gitbook', 'Pages/GitBook', _('Everything you need to create a GitLab Pages site using GitBook.'), 'https://gitlab.com/pages/gitbook', 'illustrations/logos/gitbook.svg'), + ProjectTemplate.new('hexo', 'Pages/Hexo', _('Everything you need to create a GitLab Pages site using Hexo.'), 'https://gitlab.com/pages/hexo', 'illustrations/logos/hexo.svg'), + ProjectTemplate.new('sse_middleman', 'Static Site Editor/Middleman', _('Middleman project with Static Site Editor support'), 'https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman'), + ProjectTemplate.new('gitpod_spring_petclinic', 'Gitpod/Spring Petclinic', _('A Gitpod configured Webapplication in Spring and Java'), 'https://gitlab.com/gitlab-org/project-templates/gitpod-spring-petclinic', 'illustrations/logos/gitpod.svg'), + ProjectTemplate.new('nfhugo', 'Netlify/Hugo', _('A Hugo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhugo', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfjekyll', 'Netlify/Jekyll', _('A Jekyll site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfjekyll', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfplainhtml', 'Netlify/Plain HTML', _('A plain HTML site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfplain-html', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfgitbook', 'Netlify/GitBook', _('A GitBook site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfgitbook', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('nfhexo', 'Netlify/Hexo', _('A Hexo site that uses Netlify for CI/CD instead of GitLab, but still with all the other great GitLab features.'), 'https://gitlab.com/pages/nfhexo', 'illustrations/logos/netlify.svg'), + ProjectTemplate.new('salesforcedx', 'SalesforceDX', _('A project boilerplate for Salesforce App development with Salesforce Developer tools.'), 'https://gitlab.com/gitlab-org/project-templates/salesforcedx'), + ProjectTemplate.new('serverless_framework', 'Serverless Framework/JS', _('A basic page and serverless function that uses AWS Lambda, AWS API Gateway, and GitLab Pages'), 'https://gitlab.com/gitlab-org/project-templates/serverless-framework', 'illustrations/logos/serverless_framework.svg'), + ProjectTemplate.new('jsonnet', 'Jsonnet for Dynamic Child Pipelines', _('An example showing how to use Jsonnet with GitLab dynamic child pipelines'), 'https://gitlab.com/gitlab-org/project-templates/jsonnet'), + ProjectTemplate.new('cluster_management', 'GitLab Cluster Management', _('An example project for managing Kubernetes clusters integrated with GitLab.'), 'https://gitlab.com/gitlab-org/project-templates/cluster-management') + ].freeze + end + def all localized_templates_table end diff --git a/lib/gitlab/sample_data_template.rb b/lib/gitlab/sample_data_template.rb new file mode 100644 index 00000000000..ae74dc710b7 --- /dev/null +++ b/lib/gitlab/sample_data_template.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + class SampleDataTemplate < ProjectTemplate + class << self + def localized_templates_table + [ + SampleDataTemplate.new('basic', 'Basic', _('Basic Sample Data template with Issues, Merge Requests and Milestones.'), 'https://gitlab.com/gitlab-org/sample-data-templates/basic'), + SampleDataTemplate.new('serenity_valley', 'Serenity Valley', _('Serenity Valley Sample Data template.'), 'https://gitlab.com/gitlab-org/sample-data-templates/serenity-valley') + ].freeze + end + + def all + localized_templates_table + end + + def archive_directory + Rails.root.join("vendor/sample_data_templates") + end + end + end +end diff --git a/lib/gitlab_danger.rb b/lib/gitlab_danger.rb index acce9a455bd..bbb64e0d5da 100644 --- a/lib/gitlab_danger.rb +++ b/lib/gitlab_danger.rb @@ -24,6 +24,7 @@ class GitlabDanger ce_ee_vue_templates sidekiq_queues specialization_labels + ci_templates ].freeze MESSAGE_PREFIX = '==>'.freeze diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 6cfb05768a2..79680b3a17e 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -3923,7 +3923,7 @@ msgstr "" msgid "AutoDevOps|Learn more in the %{link_to_documentation}" msgstr "" -msgid "AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found. %{more_information_link}" +msgid "AutoDevOps|The Auto DevOps pipeline has been enabled and will be used if no alternative CI configuration file is found." msgstr "" msgid "AutoRemediation|If you're using dependency and/or container scanning, and auto-fix is enabled, auto-fix automatically creates merge requests with fixes to vulnerabilities." @@ -4115,6 +4115,9 @@ msgstr "" msgid "Based on" msgstr "" +msgid "Basic Sample Data template with Issues, Merge Requests and Milestones." +msgstr "" + msgid "Be careful. Changing the project's namespace can have unintended side effects." msgstr "" @@ -14095,6 +14098,36 @@ msgstr "" msgid "Instance administrators group already exists" msgstr "" +msgid "InstanceAnalytics|Canceled" +msgstr "" + +msgid "InstanceAnalytics|Could not load the pipelines chart. Please refresh the page to try again." +msgstr "" + +msgid "InstanceAnalytics|Failed" +msgstr "" + +msgid "InstanceAnalytics|Items" +msgstr "" + +msgid "InstanceAnalytics|Month" +msgstr "" + +msgid "InstanceAnalytics|Pipelines" +msgstr "" + +msgid "InstanceAnalytics|Skipped" +msgstr "" + +msgid "InstanceAnalytics|Succeeded" +msgstr "" + +msgid "InstanceAnalytics|There is no data available." +msgstr "" + +msgid "InstanceAnalytics|Total" +msgstr "" + msgid "InstanceStatistics|Groups" msgstr "" @@ -20773,6 +20806,9 @@ msgstr "" msgid "ProjectTemplates|Android" msgstr "" +msgid "ProjectTemplates|Basic" +msgstr "" + msgid "ProjectTemplates|GitLab Cluster Management" msgstr "" @@ -20827,6 +20863,9 @@ msgstr "" msgid "ProjectTemplates|SalesforceDX" msgstr "" +msgid "ProjectTemplates|Serenity Valley" +msgstr "" + msgid "ProjectTemplates|Serverless Framework/JS" msgstr "" @@ -22806,6 +22845,9 @@ msgstr "" msgid "SSL Verification:" msgstr "" +msgid "Sample Data" +msgstr "" + msgid "Satisfied" msgstr "" @@ -23739,6 +23781,9 @@ msgstr "" msgid "September" msgstr "" +msgid "Serenity Valley Sample Data template." +msgstr "" + msgid "SeriesFinalConjunction|and" msgstr "" @@ -26690,9 +26735,15 @@ msgstr "" msgid "This application will be able to:" msgstr "" +msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{count} issues have been included. Consider re-exporting with a narrower selection of issues." +msgstr "" + msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{issues_count} issues have been included. Consider re-exporting with a narrower selection of issues." msgstr "" +msgid "This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB. %{written_count} of %{merge_requests_count} issues have been included. Consider re-exporting with a narrower selection of issues." +msgstr "" + msgid "This block is self-referential" msgstr "" @@ -30339,7 +30390,7 @@ msgstr "" msgid "Your CSV export has started. It will be emailed to %{email} when complete." msgstr "" -msgid "Your CSV export of %{issues_count} from project %{project_link} has been added to this email as an attachment." +msgid "Your CSV export of %{count} from project %{project_link} has been added to this email as an attachment." msgstr "" msgid "Your CSV export of %{written_count} from project %{project_name} (%{project_url}) has been added to this email as an attachment." diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index f6c015f64ea..bfc825990b6 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -27,7 +27,7 @@ module QA element :import_github, "icon('github', text: 'GitHub')" # rubocop:disable QA/ElementWithPattern end - view 'app/views/projects/project_templates/_built_in_templates.html.haml' do + view 'app/views/projects/project_templates/_template.html.haml' do element :use_template_button element :template_option_row end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index ed5198bf015..f956baa0e22 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -388,15 +388,23 @@ RSpec.describe Projects::IssuesController do # Rails router. A controller-style spec matches the wrong route, and # session['user_return_to'] becomes incorrect. describe 'Redirect after sign in', type: :request do - context 'with an AJAX request' do + before_all do + project.add_developer(user) + end + + before do + login_as(user) + end + + context 'with a JSON request' do it 'does not store the visited URL' do - get project_issue_path(project, issue), xhr: true + get project_issue_path(project, issue, format: :json) expect(session['user_return_to']).to be_blank end end - context 'without an AJAX request' do + context 'with an HTML request' do it 'stores the visited URL' do get project_issue_path(project, issue) @@ -1642,7 +1650,7 @@ RSpec.describe Projects::IssuesController do end it 'allows CSV export' do - expect(ExportCsvWorker).to receive(:perform_async).with(viewer.id, project.id, anything) + expect(IssuableExportCsvWorker).to receive(:perform_async).with(:issue, viewer.id, project.id, anything) request_csv @@ -1657,7 +1665,7 @@ RSpec.describe Projects::IssuesController do it 'redirects to the sign in page' do request_csv - expect(ExportCsvWorker).not_to receive(:perform_async) + expect(IssuableExportCsvWorker).not_to receive(:perform_async) expect(response).to redirect_to(new_user_session_path) end end diff --git a/spec/features/issues/csv_spec.rb b/spec/features/issues/csv_spec.rb index 8d06bf24f8b..c93693ec40a 100644 --- a/spec/features/issues/csv_spec.rb +++ b/spec/features/issues/csv_spec.rb @@ -31,13 +31,13 @@ RSpec.describe 'Issues csv' do end it 'triggers an email export' do - expect(ExportCsvWorker).to receive(:perform_async).with(user.id, project.id, hash_including("project_id" => project.id)) + expect(IssuableExportCsvWorker).to receive(:perform_async).with(:issue, user.id, project.id, hash_including("project_id" => project.id)) request_csv end it "doesn't send request params to ExportCsvWorker" do - expect(ExportCsvWorker).to receive(:perform_async).with(anything, anything, hash_excluding("controller" => anything, "action" => anything)) + expect(IssuableExportCsvWorker).to receive(:perform_async).with(:issue, anything, anything, hash_excluding("controller" => anything, "action" => anything)) request_csv end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 6c1e1eab968..6baeb4ce368 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -6,25 +6,35 @@ RSpec.describe 'Project' do include ProjectForksHelper include MobileHelpers - describe 'creating from template' do + describe 'template' do let(:user) { create(:user) } - let(:template) { Gitlab::ProjectTemplate.find(:rails) } before do sign_in user visit new_project_path end - it "allows creation from templates", :js do - find('#create-from-template-tab').click - find("label[for=#{template.name}]").click - fill_in("project_name", with: template.name) + shared_examples 'creates from template' do |template, sub_template_tab = nil| + it "is created from template", :js do + find('#create-from-template-tab').click + find(".project-template #{sub_template_tab}").click if sub_template_tab + find("label[for=#{template.name}]").click + fill_in("project_name", with: template.name) - page.within '#content-body' do - click_button "Create project" + page.within '#content-body' do + click_button "Create project" + end + + expect(page).to have_content template.name end + end + + context 'create with project template' do + it_behaves_like 'creates from template', Gitlab::ProjectTemplate.find(:rails) + end - expect(page).to have_content template.name + context 'create with sample data template' do + it_behaves_like 'creates from template', Gitlab::SampleDataTemplate.find(:basic), '.sample-data-templates-tab' end end diff --git a/spec/fixtures/lib/gitlab/import_export/sample_data/tree/project.json b/spec/fixtures/lib/gitlab/import_export/sample_data/tree/project.json new file mode 100644 index 00000000000..12136c6df3b --- /dev/null +++ b/spec/fixtures/lib/gitlab/import_export/sample_data/tree/project.json @@ -0,0 +1 @@ +{"description":"Nisi et repellendus ut enim quo accusamus vel magnam.","import_type":"gitlab_project","creator_id":2147483547,"visibility_level":10,"archived":false,"hooks":[]} diff --git a/spec/fixtures/lib/gitlab/import_export/sample_data/tree/project/issues.ndjson b/spec/fixtures/lib/gitlab/import_export/sample_data/tree/project/issues.ndjson new file mode 100644 index 00000000000..efe0d34bcb1 --- /dev/null +++ b/spec/fixtures/lib/gitlab/import_export/sample_data/tree/project/issues.ndjson @@ -0,0 +1,10 @@ +{"id":40,"title":"Voluptatem","author_id":22,"project_id":5,"created_at":"2016-06-14T15:02:08.340Z","updated_at":"2016-06-14T15:02:47.967Z","position":0,"branch_name":null,"description":"Aliquam enim illo et possimus.","state":"opened","iid":10,"updated_by_id":null,"confidential":false,"due_date":"2020-08-07","moved_to_id":null,"test_ee_field":"test","issue_assignees":[{"user_id":1,"issue_id":40},{"user_id":15,"issue_id":40},{"user_id":16,"issue_id":40},{"user_id":16,"issue_id":40},{"user_id":6,"issue_id":40}],"award_emoji":[{"id":1,"name":"musical_keyboard","user_id":1,"awardable_type":"Issue","awardable_id":40,"created_at":"2020-01-07T11:55:22.234Z","updated_at":"2020-01-07T11:55:22.234Z"}],"zoom_meetings":[{"id":1,"project_id":5,"issue_id":40,"url":"https://zoom.us/j/123456789","issue_status":1,"created_at":"2016-06-14T15:02:04.418Z","updated_at":"2016-06-14T15:02:04.418Z"}],"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"events":[{"id":487,"target_type":"Milestone","target_id":1,"project_id":46,"created_at":"2016-06-14T15:02:04.418Z","updated_at":"2016-06-14T15:02:04.418Z","action":1,"author_id":18}]},"label_links":[{"id":2,"label_id":2,"target_id":40,"target_type":"Issue","created_at":"2016-07-22T08:57:02.840Z","updated_at":"2016-07-22T08:57:02.840Z","label":{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel"}},{"id":3,"label_id":3,"target_id":40,"target_type":"Issue","created_at":"2016-07-22T08:57:02.841Z","updated_at":"2016-07-22T08:57:02.841Z","label":{"id":3,"title":"test3","color":"#428bca","group_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","project_id":null,"type":"GroupLabel","priorities":[{"id":1,"project_id":5,"label_id":1,"priority":1,"created_at":"2016-10-18T09:35:43.338Z","updated_at":"2016-10-18T09:35:43.338Z"}]}}],"notes":[{"id":351,"note":"Quo reprehenderit aliquam qui dicta impedit cupiditate eligendi.","note_html":"<p>something else entirely</p>","cached_markdown_version":917504,"noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:47.770Z","updated_at":"2016-06-14T15:02:47.770Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[],"award_emoji":[{"id":1,"name":"clapper","user_id":1,"awardable_type":"Note","awardable_id":351,"created_at":"2020-01-07T11:55:22.234Z","updated_at":"2020-01-07T11:55:22.234Z"}]},{"id":352,"note":"Est reprehenderit quas aut aspernatur autem recusandae voluptatem.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:47.795Z","updated_at":"2016-06-14T15:02:47.795Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":353,"note":"Perspiciatis suscipit voluptates in eius nihil.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:47.823Z","updated_at":"2016-06-14T15:02:47.823Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":354,"note":"Aut vel voluptas corrupti nisi provident laboriosam magnam aut.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:47.850Z","updated_at":"2016-06-14T15:02:47.850Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":355,"note":"Officia dolore consequatur in saepe cum magni.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:47.876Z","updated_at":"2016-06-14T15:02:47.876Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":356,"note":"Cum ipsum rem voluptas eaque et ea.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:47.908Z","updated_at":"2016-06-14T15:02:47.908Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":357,"note":"Recusandae excepturi asperiores suscipit autem nostrum.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:47.937Z","updated_at":"2016-06-14T15:02:47.937Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":358,"note":"Et hic est id similique et non nesciunt voluptate.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:47.965Z","updated_at":"2016-06-14T15:02:47.965Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":40,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}],"resource_label_events":[{"id":244,"action":"remove","issue_id":40,"merge_request_id":null,"label_id":2,"user_id":1,"created_at":"2018-08-28T08:24:00.494Z","label":{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel"}}],"sentry_issue":{"id":1,"issue_id":40,"sentry_issue_identifier":1234567891}} +{"id":39,"title":"Issue without assignees","author_id":22,"project_id":5,"created_at":"2016-06-14T15:02:08.233Z","updated_at":"2016-06-14T15:02:48.194Z","position":0,"branch_name":null,"description":"Voluptate vel reprehenderit facilis omnis voluptas magnam tenetur.","state":"opened","iid":9,"updated_by_id":null,"confidential":false,"due_date":"2020-08-14","moved_to_id":null,"issue_assignees":[],"milestone":{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":null,"created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"events":[{"id":487,"target_type":"Milestone","target_id":1,"project_id":46,"created_at":"2016-06-14T15:02:04.418Z","updated_at":"2016-06-14T15:02:04.418Z","action":1,"author_id":18}]},"notes":[{"id":359,"note":"Quo eius velit quia et id quam.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.009Z","updated_at":"2016-06-14T15:02:48.009Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":360,"note":"Nulla commodi ratione cumque id autem.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.032Z","updated_at":"2016-06-14T15:02:48.032Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":361,"note":"Illum non ea sed dolores corrupti.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.056Z","updated_at":"2016-06-14T15:02:48.056Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":362,"note":"Facere dolores ipsum dolorum maiores omnis occaecati ab.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.082Z","updated_at":"2016-06-14T15:02:48.082Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":363,"note":"Quod laudantium similique sint aut est ducimus.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.113Z","updated_at":"2016-06-14T15:02:48.113Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":364,"note":"Aut omnis eos esse incidunt vero reiciendis.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:48.139Z","updated_at":"2016-06-14T15:02:48.139Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":365,"note":"Beatae dolore et doloremque asperiores sunt.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:48.162Z","updated_at":"2016-06-14T15:02:48.162Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":366,"note":"Doloribus ipsam ex delectus rerum libero recusandae modi repellendus.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:48.192Z","updated_at":"2016-06-14T15:02:48.192Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":39,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]} +{"id":38,"title":"Quasi adipisci non cupiditate dolorem quo qui earum sed.","author_id":6,"project_id":5,"created_at":"2016-06-14T15:02:08.154Z","updated_at":"2016-06-14T15:02:48.614Z","position":0,"branch_name":null,"description":"Ea recusandae neque autem tempora.","state":"closed","iid":8,"updated_by_id":null,"confidential":false,"due_date":"2020-08-21","moved_to_id":null,"label_links":[{"id":99,"label_id":2,"target_id":38,"target_type":"Issue","created_at":"2016-07-22T08:57:02.840Z","updated_at":"2016-07-22T08:57:02.840Z","label":{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel"}}],"notes":[{"id":367,"note":"Accusantium fugiat et eaque quisquam esse corporis.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.235Z","updated_at":"2016-06-14T15:02:48.235Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":368,"note":"Ea labore eum nam qui laboriosam.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.261Z","updated_at":"2016-06-14T15:02:48.261Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":369,"note":"Accusantium quis sed molestiae et.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.294Z","updated_at":"2016-06-14T15:02:48.294Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":370,"note":"Corporis numquam a voluptatem pariatur asperiores dolorem delectus autem.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.523Z","updated_at":"2016-06-14T15:02:48.523Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":371,"note":"Ea accusantium maxime voluptas rerum.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.546Z","updated_at":"2016-06-14T15:02:48.546Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":372,"note":"Pariatur iusto et et excepturi similique ipsam eum.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:48.569Z","updated_at":"2016-06-14T15:02:48.569Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":373,"note":"Aliquam et culpa officia iste eius.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:48.591Z","updated_at":"2016-06-14T15:02:48.591Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":374,"note":"Ab id velit id unde laborum.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:48.613Z","updated_at":"2016-06-14T15:02:48.613Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":38,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]} +{"id":37,"title":"Cupiditate quo aut ducimus minima molestiae vero numquam possimus.","author_id":20,"project_id":5,"created_at":"2016-06-14T15:02:08.051Z","updated_at":"2016-06-14T15:02:48.854Z","position":0,"branch_name":null,"description":"Maiores architecto quos in dolorem.","state":"opened","iid":7,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":375,"note":"Quasi fugit qui sed eligendi aut quia.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.647Z","updated_at":"2016-06-14T15:02:48.647Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":376,"note":"Esse nesciunt voluptatem ex vero est consequatur.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.674Z","updated_at":"2016-06-14T15:02:48.674Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":377,"note":"Similique qui quas non aut et velit sequi in.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.696Z","updated_at":"2016-06-14T15:02:48.696Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":378,"note":"Eveniet ut cupiditate repellendus numquam in esse eius.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.720Z","updated_at":"2016-06-14T15:02:48.720Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":379,"note":"Velit est dolorem adipisci rerum sed iure.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.755Z","updated_at":"2016-06-14T15:02:48.755Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":380,"note":"Voluptatem ullam ab ut illo ut quo.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:48.793Z","updated_at":"2016-06-14T15:02:48.793Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":381,"note":"Voluptatem impedit beatae quasi ipsa earum consectetur.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:48.823Z","updated_at":"2016-06-14T15:02:48.823Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":382,"note":"Nihil officiis eaque incidunt sunt voluptatum excepturi.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:48.852Z","updated_at":"2016-06-14T15:02:48.852Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":37,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]} +{"id":36,"title":"Necessitatibus dolor est enim quia rem suscipit quidem voluptas ullam.","author_id":16,"project_id":5,"created_at":"2016-06-14T15:02:07.958Z","updated_at":"2016-06-14T15:02:49.044Z","position":0,"branch_name":null,"description":"Ut aut ut et tenetur velit aut id modi.","state":"opened","iid":6,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":383,"note":"Excepturi deleniti sunt rerum nesciunt vero fugiat possimus.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:48.885Z","updated_at":"2016-06-14T15:02:48.885Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":384,"note":"Et est nemo sed nam sed.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:48.910Z","updated_at":"2016-06-14T15:02:48.910Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":385,"note":"Animi mollitia nulla facere amet aut quaerat.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:48.934Z","updated_at":"2016-06-14T15:02:48.934Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":386,"note":"Excepturi id voluptas ut odio officiis omnis.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:48.955Z","updated_at":"2016-06-14T15:02:48.955Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":387,"note":"Molestiae labore officiis magni et eligendi quasi maxime.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:48.978Z","updated_at":"2016-06-14T15:02:48.978Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":388,"note":"Officia tenetur praesentium rem nam non.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.001Z","updated_at":"2016-06-14T15:02:49.001Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":389,"note":"Et et et molestiae reprehenderit.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.022Z","updated_at":"2016-06-14T15:02:49.022Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":390,"note":"Aperiam in consequatur est sunt cum quia.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.043Z","updated_at":"2016-06-14T15:02:49.043Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":36,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]} +{"id":35,"title":"Repellat praesentium deserunt maxime incidunt harum porro qui.","author_id":20,"project_id":5,"created_at":"2016-06-14T15:02:07.832Z","updated_at":"2016-06-14T15:02:49.226Z","position":0,"branch_name":null,"description":"Dicta nisi nihil non ipsa velit.","state":"closed","iid":5,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":391,"note":"Qui magnam et assumenda quod id dicta necessitatibus.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.075Z","updated_at":"2016-06-14T15:02:49.075Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":392,"note":"Consectetur deserunt possimus dolor est odio.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.095Z","updated_at":"2016-06-14T15:02:49.095Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":393,"note":"Labore nisi quo cumque voluptas consequatur aut qui.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.117Z","updated_at":"2016-06-14T15:02:49.117Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":394,"note":"Et totam facilis voluptas et enim.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.138Z","updated_at":"2016-06-14T15:02:49.138Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":395,"note":"Ratione sint pariatur sed omnis eligendi quo libero exercitationem.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.160Z","updated_at":"2016-06-14T15:02:49.160Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":396,"note":"Iure hic autem id voluptatem.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.182Z","updated_at":"2016-06-14T15:02:49.182Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":397,"note":"Excepturi eum laboriosam delectus repellendus odio nisi et voluptatem.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.205Z","updated_at":"2016-06-14T15:02:49.205Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":398,"note":"Ut quis ex soluta consequatur et blanditiis.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.225Z","updated_at":"2016-06-14T15:02:49.225Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":35,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]} +{"id":34,"title":"Ullam expedita deserunt libero consequatur quia dolor harum perferendis facere quidem.","author_id":1,"project_id":5,"created_at":"2016-06-14T15:02:07.717Z","updated_at":"2016-06-14T15:02:49.416Z","position":0,"branch_name":null,"description":"Ut et explicabo vel voluptatem consequuntur ut sed.","state":"closed","iid":4,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":399,"note":"Dolor iste tempora tenetur non vitae maiores voluptatibus.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.256Z","updated_at":"2016-06-14T15:02:49.256Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":400,"note":"Aut sit quidem qui adipisci maxime excepturi iusto.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.284Z","updated_at":"2016-06-14T15:02:49.284Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":401,"note":"Et a necessitatibus autem quidem animi sunt voluptatum rerum.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.305Z","updated_at":"2016-06-14T15:02:49.305Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":402,"note":"Esse laboriosam quo voluptatem quis molestiae.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.328Z","updated_at":"2016-06-14T15:02:49.328Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":403,"note":"Nemo magnam distinctio est ut voluptate ea.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.350Z","updated_at":"2016-06-14T15:02:49.350Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":404,"note":"Omnis sed rerum neque rerum quae quam nulla officiis.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.372Z","updated_at":"2016-06-14T15:02:49.372Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":405,"note":"Quo soluta dolorem vitae ad consequatur qui aut dicta.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.394Z","updated_at":"2016-06-14T15:02:49.394Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":406,"note":"Magni minus est aut aut totam ut.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.414Z","updated_at":"2016-06-14T15:02:49.414Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":34,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]} +{"id":33,"title":"Numquam accusamus eos iste exercitationem magni non inventore.","author_id":26,"project_id":5,"created_at":"2016-06-14T15:02:07.611Z","updated_at":"2016-06-14T15:02:49.661Z","position":0,"branch_name":null,"description":"Non asperiores velit accusantium voluptate.","state":"closed","iid":3,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":407,"note":"Quod ea et possimus architecto.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.450Z","updated_at":"2016-06-14T15:02:49.450Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":408,"note":"Reiciendis est et unde perferendis dicta ut praesentium quasi.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.503Z","updated_at":"2016-06-14T15:02:49.503Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":409,"note":"Magni quia odio blanditiis pariatur voluptas.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.527Z","updated_at":"2016-06-14T15:02:49.527Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":410,"note":"Enim quam ut et et et.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.551Z","updated_at":"2016-06-14T15:02:49.551Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":411,"note":"Fugit voluptatem ratione maxime expedita.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.578Z","updated_at":"2016-06-14T15:02:49.578Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":412,"note":"Voluptatem enim aut ipsa et et ducimus.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.604Z","updated_at":"2016-06-14T15:02:49.604Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":413,"note":"Quia repellat fugiat consectetur quidem.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.631Z","updated_at":"2016-06-14T15:02:49.631Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":414,"note":"Corporis ipsum et ea necessitatibus quod assumenda repudiandae quam.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.659Z","updated_at":"2016-06-14T15:02:49.659Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":33,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]} +{"id":32,"title":"Necessitatibus magnam qui at velit consequatur perspiciatis.","author_id":15,"project_id":5,"created_at":"2016-06-14T15:02:07.431Z","updated_at":"2016-06-14T15:02:49.884Z","position":0,"branch_name":null,"description":"Molestiae corporis magnam et fugit aliquid nulla quia.","state":"closed","iid":2,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"notes":[{"id":415,"note":"Nemo consequatur sed blanditiis qui id iure dolores.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.694Z","updated_at":"2016-06-14T15:02:49.694Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":416,"note":"Voluptas ab accusantium dicta in.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.718Z","updated_at":"2016-06-14T15:02:49.718Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":417,"note":"Esse odit qui a et eum ducimus.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.741Z","updated_at":"2016-06-14T15:02:49.741Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":418,"note":"Sequi dolor doloribus ratione placeat repellendus.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:49.767Z","updated_at":"2016-06-14T15:02:49.767Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":419,"note":"Quae aspernatur rem est similique.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:49.796Z","updated_at":"2016-06-14T15:02:49.796Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":420,"note":"Voluptate omnis et id rerum non nesciunt laudantium assumenda.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:49.825Z","updated_at":"2016-06-14T15:02:49.825Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":421,"note":"Quia enim ab et eligendi.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:49.853Z","updated_at":"2016-06-14T15:02:49.853Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":422,"note":"In fugiat rerum voluptas quas officia.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:49.881Z","updated_at":"2016-06-14T15:02:49.881Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":32,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]} +{"id":31,"title":"issue_with_timelogs","author_id":16,"project_id":5,"created_at":"2016-06-14T15:02:07.280Z","updated_at":"2016-06-14T15:02:50.134Z","position":0,"branch_name":null,"description":"Quod ad architecto qui est sed quia.","state":"closed","iid":1,"updated_by_id":null,"confidential":false,"due_date":null,"moved_to_id":null,"timelogs":[{"id":1,"time_spent":72000,"user_id":1,"created_at":"2019-12-27T09:15:22.302Z","updated_at":"2019-12-27T09:15:22.302Z","spent_at":"2019-12-27T00:00:00.000Z"}],"notes":[{"id":423,"note":"A mollitia qui iste consequatur eaque iure omnis sunt.","noteable_type":"Issue","author_id":26,"created_at":"2016-06-14T15:02:49.933Z","updated_at":"2016-06-14T15:02:49.933Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 4"},"events":[]},{"id":424,"note":"Eveniet est et blanditiis sequi alias.","noteable_type":"Issue","author_id":25,"created_at":"2016-06-14T15:02:49.965Z","updated_at":"2016-06-14T15:02:49.965Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 3"},"events":[]},{"id":425,"note":"Commodi tempore voluptas doloremque est.","noteable_type":"Issue","author_id":22,"created_at":"2016-06-14T15:02:49.996Z","updated_at":"2016-06-14T15:02:49.996Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"User 0"},"events":[]},{"id":426,"note":"Quo libero impedit odio debitis rerum aspernatur.","noteable_type":"Issue","author_id":20,"created_at":"2016-06-14T15:02:50.024Z","updated_at":"2016-06-14T15:02:50.024Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ottis Schuster II"},"events":[]},{"id":427,"note":"Dolorem voluptatem qui labore deserunt.","noteable_type":"Issue","author_id":16,"created_at":"2016-06-14T15:02:50.049Z","updated_at":"2016-06-14T15:02:50.049Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Rhett Emmerich IV"},"events":[]},{"id":428,"note":"Est blanditiis laboriosam enim ipsam.","noteable_type":"Issue","author_id":15,"created_at":"2016-06-14T15:02:50.077Z","updated_at":"2016-06-14T15:02:50.077Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Burdette Bernier"},"events":[]},{"id":429,"note":"Et in voluptatem animi dolorem eos.","noteable_type":"Issue","author_id":6,"created_at":"2016-06-14T15:02:50.107Z","updated_at":"2016-06-14T15:02:50.107Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Ari Wintheiser"},"events":[]},{"id":430,"note":"Unde culpa voluptate qui sint quos.","noteable_type":"Issue","author_id":1,"created_at":"2016-06-14T15:02:50.132Z","updated_at":"2016-06-14T15:02:50.132Z","project_id":5,"attachment":{"url":null},"line_code":null,"commit_id":null,"noteable_id":31,"system":false,"st_diff":null,"updated_by_id":null,"author":{"name":"Administrator"},"events":[]}]} diff --git a/spec/fixtures/lib/gitlab/import_export/sample_data/tree/project/labels.ndjson b/spec/fixtures/lib/gitlab/import_export/sample_data/tree/project/labels.ndjson new file mode 100644 index 00000000000..c36b6970e83 --- /dev/null +++ b/spec/fixtures/lib/gitlab/import_export/sample_data/tree/project/labels.ndjson @@ -0,0 +1,2 @@ +{"id":2,"title":"test2","color":"#428bca","project_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","type":"ProjectLabel","priorities":[]} +{"id":3,"title":"test3","color":"#428bca","group_id":8,"created_at":"2016-07-22T08:55:44.161Z","updated_at":"2016-07-22T08:55:44.161Z","template":false,"description":"","project_id":null,"type":"GroupLabel","priorities":[{"id":1,"project_id":5,"label_id":1,"priority":1,"created_at":"2016-10-18T09:35:43.338Z","updated_at":"2016-10-18T09:35:43.338Z"}]} diff --git a/spec/fixtures/lib/gitlab/import_export/sample_data/tree/project/milestones.ndjson b/spec/fixtures/lib/gitlab/import_export/sample_data/tree/project/milestones.ndjson new file mode 100644 index 00000000000..ebb8203ece3 --- /dev/null +++ b/spec/fixtures/lib/gitlab/import_export/sample_data/tree/project/milestones.ndjson @@ -0,0 +1,3 @@ +{"id":1,"title":"test milestone","project_id":8,"description":"test milestone","due_date":"2020-08-07","created_at":"2016-06-14T15:02:04.415Z","updated_at":"2016-06-14T15:02:04.415Z","state":"active","iid":1,"events":[{"id":487,"target_type":"Milestone","target_id":1,"project_id":46,"created_at":"2016-06-14T15:02:04.418Z","updated_at":"2016-06-14T15:02:04.418Z","action":1,"author_id":18}]} +{"id":20,"title":"v4.0","project_id":5,"description":"Totam quam laborum id magnam natus eaque aspernatur.","due_date":"2020-08-14","created_at":"2016-06-14T15:02:04.590Z","updated_at":"2016-06-14T15:02:04.590Z","state":"active","iid":5,"events":[{"id":240,"target_type":"Milestone","target_id":20,"project_id":36,"created_at":"2016-06-14T15:02:04.593Z","updated_at":"2016-06-14T15:02:04.593Z","action":1,"author_id":1},{"id":60,"target_type":"Milestone","target_id":20,"project_id":5,"created_at":"2016-06-14T15:02:04.593Z","updated_at":"2016-06-14T15:02:04.593Z","action":1,"author_id":20}]} +{"id":19,"title":"v3.0","project_id":5,"description":"Rerum at autem exercitationem ea voluptates harum quam placeat.","due_date":"2020-08-21","created_at":"2016-06-14T15:02:04.583Z","updated_at":"2016-06-14T15:02:04.583Z","state":"active","iid":4,"events":[{"id":241,"target_type":"Milestone","target_id":19,"project_id":36,"created_at":"2016-06-14T15:02:04.585Z","updated_at":"2016-06-14T15:02:04.585Z","action":1,"author_id":1},{"id":59,"target_type":"Milestone","target_id":19,"project_id":5,"created_at":"2016-06-14T15:02:04.585Z","updated_at":"2016-06-14T15:02:04.585Z","action":1,"author_id":25}]} diff --git a/spec/frontend/analytics/instance_statistics/apollo_mock_data.js b/spec/frontend/analytics/instance_statistics/apollo_mock_data.js new file mode 100644 index 00000000000..2e4eaf3fc96 --- /dev/null +++ b/spec/frontend/analytics/instance_statistics/apollo_mock_data.js @@ -0,0 +1,30 @@ +const defaultPageInfo = { hasPreviousPage: false, startCursor: null, endCursor: null }; + +export function getApolloResponse(options = {}) { + const { + pipelinesTotal = [], + pipelinesSucceeded = [], + pipelinesFailed = [], + pipelinesCanceled = [], + pipelinesSkipped = [], + hasNextPage = false, + } = options; + return { + data: { + pipelinesTotal: { pageInfo: { ...defaultPageInfo, hasNextPage }, nodes: pipelinesTotal }, + pipelinesSucceeded: { + pageInfo: { ...defaultPageInfo, hasNextPage }, + nodes: pipelinesSucceeded, + }, + pipelinesFailed: { pageInfo: { ...defaultPageInfo, hasNextPage }, nodes: pipelinesFailed }, + pipelinesCanceled: { + pageInfo: { ...defaultPageInfo, hasNextPage }, + nodes: pipelinesCanceled, + }, + pipelinesSkipped: { + pageInfo: { ...defaultPageInfo, hasNextPage }, + nodes: pipelinesSkipped, + }, + }, + }; +} diff --git a/spec/frontend/analytics/instance_statistics/components/__snapshots__/pipelines_chart_spec.js.snap b/spec/frontend/analytics/instance_statistics/components/__snapshots__/pipelines_chart_spec.js.snap new file mode 100644 index 00000000000..0b3b685a9f2 --- /dev/null +++ b/spec/frontend/analytics/instance_statistics/components/__snapshots__/pipelines_chart_spec.js.snap @@ -0,0 +1,161 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`PipelinesChart when fetching more data when the fetchMore query returns data passes the data to the line chart 1`] = ` +Array [ + Object { + "data": Array [ + Array [ + "2020-06-01", + 21, + ], + Array [ + "2020-07-01", + 10, + ], + Array [ + "2020-08-01", + 5, + ], + ], + "name": "Total", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 21, + ], + Array [ + "2020-07-01", + 10, + ], + Array [ + "2020-08-01", + 5, + ], + ], + "name": "Succeeded", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 22, + ], + Array [ + "2020-07-01", + 41, + ], + Array [ + "2020-08-01", + 5, + ], + ], + "name": "Failed", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 21, + ], + Array [ + "2020-07-01", + 10, + ], + Array [ + "2020-08-01", + 5, + ], + ], + "name": "Canceled", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 21, + ], + Array [ + "2020-07-01", + 10, + ], + Array [ + "2020-08-01", + 5, + ], + ], + "name": "Skipped", + }, +] +`; + +exports[`PipelinesChart with data passes the data to the line chart 1`] = ` +Array [ + Object { + "data": Array [ + Array [ + "2020-06-01", + 22, + ], + Array [ + "2020-07-01", + 41, + ], + ], + "name": "Total", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 21, + ], + Array [ + "2020-07-01", + 10, + ], + ], + "name": "Succeeded", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 21, + ], + Array [ + "2020-07-01", + 10, + ], + ], + "name": "Failed", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 22, + ], + Array [ + "2020-07-01", + 41, + ], + ], + "name": "Canceled", + }, + Object { + "data": Array [ + Array [ + "2020-06-01", + 22, + ], + Array [ + "2020-07-01", + 41, + ], + ], + "name": "Skipped", + }, +] +`; diff --git a/spec/frontend/analytics/instance_statistics/components/app_spec.js b/spec/frontend/analytics/instance_statistics/components/app_spec.js index 242621dc40c..39f6d1b450a 100644 --- a/spec/frontend/analytics/instance_statistics/components/app_spec.js +++ b/spec/frontend/analytics/instance_statistics/components/app_spec.js @@ -1,6 +1,7 @@ import { shallowMount } from '@vue/test-utils'; import InstanceStatisticsApp from '~/analytics/instance_statistics/components/app.vue'; import InstanceCounts from '~/analytics/instance_statistics/components//instance_counts.vue'; +import PipelinesChart from '~/analytics/instance_statistics/components/pipelines_chart.vue'; describe('InstanceStatisticsApp', () => { let wrapper; @@ -21,4 +22,8 @@ describe('InstanceStatisticsApp', () => { it('displays the instance counts component', () => { expect(wrapper.find(InstanceCounts).exists()).toBe(true); }); + + it('displays the pipelines chart component', () => { + expect(wrapper.find(PipelinesChart).exists()).toBe(true); + }); }); diff --git a/spec/frontend/analytics/instance_statistics/components/pipelines_chart_spec.js b/spec/frontend/analytics/instance_statistics/components/pipelines_chart_spec.js new file mode 100644 index 00000000000..a06d66f783e --- /dev/null +++ b/spec/frontend/analytics/instance_statistics/components/pipelines_chart_spec.js @@ -0,0 +1,189 @@ +import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlLineChart } from '@gitlab/ui/dist/charts'; +import { GlAlert } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'jest/helpers/mock_apollo_helper'; +import PipelinesChart from '~/analytics/instance_statistics/components/pipelines_chart.vue'; +import pipelinesStatsQuery from '~/analytics/instance_statistics/graphql/queries/pipeline_stats.query.graphql'; +import ChartSkeletonLoader from '~/vue_shared/components/resizable_chart/skeleton_loader.vue'; +import { mockCountsData1, mockCountsData2 } from '../mock_data'; +import { getApolloResponse } from '../apollo_mock_data'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('PipelinesChart', () => { + let wrapper; + let queryHandler; + + const createApolloProvider = pipelineStatsHandler => { + return createMockApollo([[pipelinesStatsQuery, pipelineStatsHandler]]); + }; + + const createComponent = apolloProvider => { + return shallowMount(PipelinesChart, { + localVue, + apolloProvider, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const findLoader = () => wrapper.find(ChartSkeletonLoader); + const findChart = () => wrapper.find(GlLineChart); + const findAlert = () => wrapper.find(GlAlert); + + describe('while loading', () => { + beforeEach(() => { + queryHandler = jest.fn().mockReturnValue(new Promise(() => {})); + const apolloProvider = createApolloProvider(queryHandler); + wrapper = createComponent(apolloProvider); + }); + + it('requests data', () => { + expect(queryHandler).toBeCalledTimes(1); + }); + + it('displays the skeleton loader', () => { + expect(findLoader().exists()).toBe(true); + }); + + it('hides the chart', () => { + expect(findChart().exists()).toBe(false); + }); + + it('does not show an error', () => { + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('without data', () => { + beforeEach(() => { + const emptyResponse = getApolloResponse(); + queryHandler = jest.fn().mockResolvedValue(emptyResponse); + const apolloProvider = createApolloProvider(queryHandler); + wrapper = createComponent(apolloProvider); + }); + + it('renders an no data message', () => { + expect(findAlert().text()).toBe('There is no data available.'); + }); + + it('hides the skeleton loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findChart().exists()).toBe(false); + }); + }); + + describe('with data', () => { + beforeEach(() => { + const response = getApolloResponse({ + pipelinesTotal: mockCountsData1, + pipelinesSucceeded: mockCountsData2, + pipelinesFailed: mockCountsData2, + pipelinesCanceled: mockCountsData1, + pipelinesSkipped: mockCountsData1, + }); + queryHandler = jest.fn().mockResolvedValue(response); + const apolloProvider = createApolloProvider(queryHandler); + wrapper = createComponent(apolloProvider); + }); + + it('requests data', () => { + expect(queryHandler).toBeCalledTimes(1); + }); + + it('hides the skeleton loader', () => { + expect(findLoader().exists()).toBe(false); + }); + + it('renders the chart', () => { + expect(findChart().exists()).toBe(true); + }); + + it('passes the data to the line chart', () => { + expect(findChart().props('data')).toMatchSnapshot(); + }); + + it('does not show an error', () => { + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('when fetching more data', () => { + const recordedAt = '2020-08-01'; + describe('when the fetchMore query returns data', () => { + beforeEach(async () => { + const newData = { recordedAt, count: 5 }; + const firstResponse = getApolloResponse({ + pipelinesTotal: mockCountsData2, + pipelinesSucceeded: mockCountsData2, + pipelinesFailed: mockCountsData1, + pipelinesCanceled: mockCountsData2, + pipelinesSkipped: mockCountsData2, + hasNextPage: true, + }); + const secondResponse = getApolloResponse({ + pipelinesTotal: [newData], + pipelinesSucceeded: [newData], + pipelinesFailed: [newData], + pipelinesCanceled: [newData], + pipelinesSkipped: [newData], + hasNextPage: false, + }); + queryHandler = jest + .fn() + .mockResolvedValueOnce(firstResponse) + .mockResolvedValueOnce(secondResponse); + const apolloProvider = createApolloProvider(queryHandler); + wrapper = createComponent(apolloProvider); + + await wrapper.vm.$nextTick(); + }); + + it('requests data twice', () => { + expect(queryHandler).toBeCalledTimes(2); + }); + + it('passes the data to the line chart', () => { + expect(findChart().props('data')).toMatchSnapshot(); + }); + }); + + describe('when the fetchMore query throws an error', () => { + beforeEach(async () => { + const response = getApolloResponse({ + pipelinesTotal: mockCountsData2, + pipelinesSucceeded: mockCountsData2, + pipelinesFailed: mockCountsData1, + pipelinesCanceled: mockCountsData2, + pipelinesSkipped: mockCountsData2, + hasNextPage: true, + }); + queryHandler = jest.fn().mockResolvedValue(response); + const apolloProvider = createApolloProvider(queryHandler); + wrapper = createComponent(apolloProvider); + jest + .spyOn(wrapper.vm.$apollo.queries.pipelineStats, 'fetchMore') + .mockImplementation(jest.fn().mockRejectedValue()); + await wrapper.vm.$nextTick(); + }); + + it('calls fetchMore', () => { + expect(wrapper.vm.$apollo.queries.pipelineStats.fetchMore).toHaveBeenCalledTimes(1); + }); + + it('show an error message', () => { + expect(findAlert().text()).toBe( + 'Could not load the pipelines chart. Please refresh the page to try again.', + ); + }); + }); + }); +}); diff --git a/spec/frontend/analytics/instance_statistics/utils_spec.js b/spec/frontend/analytics/instance_statistics/utils_spec.js index f6ea81eb678..d480238419b 100644 --- a/spec/frontend/analytics/instance_statistics/utils_spec.js +++ b/spec/frontend/analytics/instance_statistics/utils_spec.js @@ -1,4 +1,8 @@ -import { getAverageByMonth } from '~/analytics/instance_statistics/utils'; +import { + getAverageByMonth, + extractValues, + sortByDate, +} from '~/analytics/instance_statistics/utils'; import { mockCountsData1, mockCountsData2, @@ -39,3 +43,42 @@ describe('getAverageByMonth', () => { }); }); }); + +describe('extractValues', () => { + it('extracts only requested values', () => { + const data = { fooBar: { baz: 'quis' }, ignored: 'ignored' }; + expect(extractValues(data, ['fooBar'], 'foo', 'baz')).toEqual({ bazBar: 'quis' }); + }); + + it('is able to extract multiple values', () => { + const data = { + fooBar: { baz: 'quis' }, + fooBaz: { baz: 'quis' }, + fooQuis: { baz: 'quis' }, + }; + expect(extractValues(data, ['fooBar', 'fooBaz', 'fooQuis'], 'foo', 'baz')).toEqual({ + bazBar: 'quis', + bazBaz: 'quis', + bazQuis: 'quis', + }); + }); + + it('returns empty data set when keys are not found', () => { + const data = { foo: { baz: 'quis' }, ignored: 'ignored' }; + expect(extractValues(data, ['fooBar'], 'foo', 'baz')).toEqual({}); + }); + + it('returns empty data when params are missing', () => { + expect(extractValues()).toEqual({}); + }); +}); + +describe('sortByDate', () => { + it('sorts the array by date', () => { + expect(sortByDate(mockCountsData1)).toStrictEqual([...mockCountsData1].reverse()); + }); + + it('does not modify the original array', () => { + expect(sortByDate(countsMonthlyChartData1)).not.toBe(countsMonthlyChartData1); + }); +}); diff --git a/spec/frontend/ci_variable_list/components/ci_enviroments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js index cbe8aaf7fcf..7bcd558c60f 100644 --- a/spec/frontend/ci_variable_list/components/ci_enviroments_dropdown_spec.js +++ b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js @@ -35,12 +35,12 @@ describe('Ci environments dropdown', () => { wrapper = null; }); - describe('No enviroments found', () => { + describe('No environments found', () => { beforeEach(() => { createComponent('stable'); }); - it('renders create button with search term if enviroments do not contain search term', () => { + it('renders create button with search term if environments do not contain search term', () => { expect(findAllDropdownItems()).toHaveLength(2); expect(findDropdownItemByIndex(1).text()).toBe('Create wildcard: stable'); }); @@ -55,7 +55,7 @@ describe('Ci environments dropdown', () => { createComponent(''); }); - it('renders all enviroments when search term is empty', () => { + it('renders all environments when search term is empty', () => { expect(findAllDropdownItems()).toHaveLength(3); expect(findDropdownItemByIndex(0).text()).toBe('dev'); expect(findDropdownItemByIndex(1).text()).toBe('prod'); @@ -67,19 +67,19 @@ describe('Ci environments dropdown', () => { }); }); - describe('Enviroments found', () => { + describe('Environments found', () => { beforeEach(() => { createComponent('prod'); }); - it('renders only the enviroment searched for', () => { + it('renders only the environment searched for', () => { expect(findAllDropdownItems()).toHaveLength(1); expect(findDropdownItemByIndex(0).text()).toBe('prod'); }); it('should not display create button', () => { - const enviroments = findAllDropdownItems().filter(env => env.text().startsWith('Create')); - expect(enviroments).toHaveLength(0); + const environments = findAllDropdownItems().filter(env => env.text().startsWith('Create')); + expect(environments).toHaveLength(0); expect(findAllDropdownItems()).toHaveLength(1); }); diff --git a/spec/frontend/ci_variable_list/store/getters_spec.js b/spec/frontend/ci_variable_list/store/getters_spec.js index 7ad96545652..92f22b18763 100644 --- a/spec/frontend/ci_variable_list/store/getters_spec.js +++ b/spec/frontend/ci_variable_list/store/getters_spec.js @@ -3,7 +3,7 @@ import mockData from '../services/mock_data'; describe('Ci variable getters', () => { describe('joinedEnvironments', () => { - it('should join fetched enviroments with variable environment scopes', () => { + it('should join fetched environments with variable environment scopes', () => { const state = { environments: ['All (default)', 'staging', 'deployment', 'prod'], variables: mockData.mockVariableScopes, diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js index 663b3486a17..a333fb7d8f9 100644 --- a/spec/frontend/ci_variable_list/store/mutations_spec.js +++ b/spec/frontend/ci_variable_list/store/mutations_spec.js @@ -73,7 +73,7 @@ describe('CI variable list mutations', () => { }); describe('ADD_WILD_CARD_SCOPE', () => { - it('should add wild card scope to enviroments array and sort', () => { + it('should add wild card scope to environments array and sort', () => { stateCopy.environments = ['dev', 'staging']; mutations[types.ADD_WILD_CARD_SCOPE](stateCopy, 'production'); diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 821440db551..2475620221c 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -369,4 +369,35 @@ RSpec.describe IssuablesHelper do expect(helper.sidebar_milestone_tooltip_label(milestone)).to eq('<img onerror=alert(1)><br/>Milestone') end end + + describe '#serialize_issuable' do + context 'when it is a merge request' do + let(:merge_request) { build(:merge_request) } + let(:user) { build(:user) } + + before do + allow(helper).to receive(:current_user) { user } + end + + it 'has suggest_pipeline experiment enabled' do + allow(helper).to receive(:experiment_enabled?).with(:suggest_pipeline) { true } + + expect_next_instance_of(MergeRequestSerializer) do |serializer| + expect(serializer).to receive(:represent).with(merge_request, { serializer: 'widget', experiment_enabled: :suggest_pipeline }) + end + + helper.serialize_issuable(merge_request, serializer: 'widget') + end + + it 'suggest_pipeline experiment disabled' do + allow(helper).to receive(:experiment_enabled?).with(:suggest_pipeline) { false } + + expect_next_instance_of(MergeRequestSerializer) do |serializer| + expect(serializer).to receive(:represent).with(merge_request, { serializer: 'widget' }) + end + + helper.serialize_issuable(merge_request, serializer: 'widget') + end + end + end end diff --git a/spec/lib/banzai/reference_redactor_spec.rb b/spec/lib/banzai/reference_redactor_spec.rb index de774267b81..668e427cfa2 100644 --- a/spec/lib/banzai/reference_redactor_spec.rb +++ b/spec/lib/banzai/reference_redactor_spec.rb @@ -182,5 +182,12 @@ RSpec.describe Banzai::ReferenceRedactor do expect(redactor.nodes_visible_to_user([node])).to eq(Set.new([node])) end + + it 'handles invalid references gracefully' do + doc = Nokogiri::HTML.fragment('<a data-reference-type="some_invalid_type"></a>') + node = doc.children[0] + + expect(redactor.nodes_visible_to_user([node])).to be_empty + end end end diff --git a/spec/lib/gitlab/auth/otp/strategies/devise_spec.rb b/spec/lib/gitlab/auth/otp/strategies/devise_spec.rb new file mode 100644 index 00000000000..0c88421d456 --- /dev/null +++ b/spec/lib/gitlab/auth/otp/strategies/devise_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::Otp::Strategies::Devise do + let_it_be(:user) { create(:user) } + let(:otp_code) { 42 } + + subject(:validate) { described_class.new(user).validate(otp_code) } + + it 'calls Devise' do + expect(user).to receive(:validate_and_consume_otp!).with(otp_code) + + validate + end +end diff --git a/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb new file mode 100644 index 00000000000..18fd6d08057 --- /dev/null +++ b/spec/lib/gitlab/auth/otp/strategies/forti_authenticator_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Auth::Otp::Strategies::FortiAuthenticator do + let_it_be(:user) { create(:user) } + let(:otp_code) { 42 } + + let(:host) { 'forti_authenticator.example.com' } + let(:port) { '444' } + let(:api_username) { 'janedoe' } + let(:api_token) { 's3cr3t' } + + let(:forti_authenticator_auth_url) { "https://#{host}:#{port}/api/v1/auth/" } + + subject(:validate) { described_class.new(user).validate(otp_code) } + + before do + stub_feature_flags(forti_authenticator: true) + + stub_forti_authenticator_config( + host: host, + port: port, + username: api_username, + token: api_token + ) + + request_body = { username: user.username, + token_code: otp_code } + + stub_request(:post, forti_authenticator_auth_url) + .with(body: JSON(request_body), headers: { 'Content-Type' => 'application/json' }) + .to_return(status: response_status, body: '', headers: {}) + end + + context 'successful validation' do + let(:response_status) { 200 } + + it 'returns success' do + expect(validate[:status]).to eq(:success) + end + end + + context 'unsuccessful validation' do + let(:response_status) { 401 } + + it 'returns error' do + expect(validate[:status]).to eq(:error) + end + end + + def stub_forti_authenticator_config(forti_authenticator_settings) + allow(::Gitlab.config.forti_authenticator).to(receive_messages(forti_authenticator_settings)) + end +end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 4cf207bf9d8..a8edcc5f7e5 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -933,6 +933,19 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end end + describe '#undo_change_column_type_concurrently' do + it 'reverses the operations of change_column_type_concurrently' do + expect(model).to receive(:check_trigger_permissions!).with(:users) + + expect(model).to receive(:remove_rename_triggers_for_postgresql) + .with(:users, /trigger_.{12}/) + + expect(model).to receive(:remove_column).with(:users, "old_for_type_change") + + model.undo_change_column_type_concurrently(:users, :old) + end + end + describe '#cleanup_concurrent_column_type_change' do it 'cleans up the type changing procedure' do expect(model).to receive(:cleanup_concurrent_column_rename) @@ -945,6 +958,94 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end end + describe '#undo_cleanup_concurrent_column_type_change' do + context 'in a transaction' do + it 'raises RuntimeError' do + allow(model).to receive(:transaction_open?).and_return(true) + + expect { model.undo_cleanup_concurrent_column_type_change(:users, :old, :new) } + .to raise_error(RuntimeError) + end + end + + context 'outside a transaction' do + let(:temp_column) { "old_for_type_change" } + + let(:temp_undo_cleanup_column) do + identifier = "users_old_for_type_change" + hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) + "tmp_undo_cleanup_column_#{hashed_identifier}" + end + + let(:trigger_name) { model.rename_trigger_name(:users, :old, :old_for_type_change) } + + before do + allow(model).to receive(:transaction_open?).and_return(false) + end + + it 'reverses the operations of cleanup_concurrent_column_type_change' do + expect(model).to receive(:check_trigger_permissions!).with(:users) + + expect(model).to receive(:create_column_from).with( + :users, + :old, + temp_undo_cleanup_column, + type: :string, + batch_column_name: :id, + type_cast_function: nil + ).and_return(true) + + expect(model).to receive(:rename_column) + .with(:users, :old, temp_column) + + expect(model).to receive(:rename_column) + .with(:users, temp_undo_cleanup_column, :old) + + expect(model).to receive(:install_rename_triggers_for_postgresql) + .with(trigger_name, '"users"', '"old"', '"old_for_type_change"') + + model.undo_cleanup_concurrent_column_type_change(:users, :old, :string) + end + + it 'passes the type_cast_function and batch_column_name' do + expect(model).to receive(:column_exists?).with(:users, :other_batch_column).and_return(true) + expect(model).to receive(:check_trigger_permissions!).with(:users) + + expect(model).to receive(:create_column_from).with( + :users, + :old, + temp_undo_cleanup_column, + type: :string, + batch_column_name: :other_batch_column, + type_cast_function: :custom_type_cast_function + ).and_return(true) + + expect(model).to receive(:rename_column) + .with(:users, :old, temp_column) + + expect(model).to receive(:rename_column) + .with(:users, temp_undo_cleanup_column, :old) + + expect(model).to receive(:install_rename_triggers_for_postgresql) + .with(trigger_name, '"users"', '"old"', '"old_for_type_change"') + + model.undo_cleanup_concurrent_column_type_change( + :users, + :old, + :string, + type_cast_function: :custom_type_cast_function, + batch_column_name: :other_batch_column + ) + end + + it 'raises an error with invalid batch_column_name' do + expect do + model.undo_cleanup_concurrent_column_type_change(:users, :old, :new, batch_column_name: :invalid) + end.to raise_error(RuntimeError, /Column invalid does not exist on users/) + end + end + end + describe '#install_rename_triggers_for_postgresql' do it 'installs the triggers for PostgreSQL' do expect(model).to receive(:execute) diff --git a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb index a347d835428..e208a1c383c 100644 --- a/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb +++ b/spec/lib/gitlab/import_export/json/ndjson_reader_spec.rb @@ -102,4 +102,14 @@ RSpec.describe Gitlab::ImportExport::JSON::NdjsonReader do end end end + + describe '#clear_consumed_relations' do + let(:dir_path) { fixture } + + subject { ndjson_reader.clear_consumed_relations } + + it 'returns empty set' do + expect(subject).to be_empty + end + end end diff --git a/spec/lib/gitlab/import_export/project/sample/date_calculator_spec.rb b/spec/lib/gitlab/import_export/project/sample/date_calculator_spec.rb new file mode 100644 index 00000000000..82f59245519 --- /dev/null +++ b/spec/lib/gitlab/import_export/project/sample/date_calculator_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::ImportExport::Project::Sample::DateCalculator do + describe '#closest date to average' do + subject { described_class.new(dates).closest_date_to_average } + + context 'when dates are empty' do + let(:dates) { [] } + + it { is_expected.to be_nil } + end + + context 'when dates are not empty' do + let(:dates) { [[nil, '2020-01-01 00:00:00 +0000'], [nil, '2021-01-01 00:00:00 +0000'], [nil, '2022-01-01 23:59:59 +0000']] } + + it { is_expected.to eq(Time.zone.parse('2021-01-01 00:00:00 +0000')) } + end + end + + describe '#calculate_by_closest_date_to_average' do + let(:calculator) { described_class.new([]) } + let(:date) { Time.current } + + subject { calculator.calculate_by_closest_date_to_average(date) } + + context 'when average date is nil' do + before do + allow(calculator).to receive(:closest_date_to_average).and_return(nil) + end + + it { is_expected.to eq(date) } + end + + context 'when average date is in the past' do + before do + allow(calculator).to receive(:closest_date_to_average).and_return(date - 365.days) + allow(Time).to receive(:current).and_return(date) + end + + it { is_expected.to eq(date + 365.days) } + end + + context 'when average date is in the future' do + before do + allow(calculator).to receive(:closest_date_to_average).and_return(date + 10.days) + end + + it { is_expected.to eq(date) } + end + end +end diff --git a/spec/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer_spec.rb new file mode 100644 index 00000000000..f173345a4c6 --- /dev/null +++ b/spec/lib/gitlab/import_export/project/sample/sample_data_relation_tree_restorer_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# This spec is a lightweight version of: +# * project/tree_restorer_spec.rb +# +# In depth testing is being done in the above specs. +# This spec tests that restore of the sample project works +# but does not have 100% relation coverage. + +require 'spec_helper' + +RSpec.describe Gitlab::ImportExport::Project::Sample::SampleDataRelationTreeRestorer do + include_context 'relation tree restorer shared context' + + let(:sample_data_relation_tree_restorer) do + described_class.new( + user: user, + shared: shared, + relation_reader: relation_reader, + object_builder: object_builder, + members_mapper: members_mapper, + relation_factory: relation_factory, + reader: reader, + importable: importable, + importable_path: importable_path, + importable_attributes: attributes + ) + end + + subject { sample_data_relation_tree_restorer.restore } + + shared_examples 'import project successfully' do + it 'restores project tree' do + expect(subject).to eq(true) + end + + describe 'imported project' do + let(:project) { Project.find_by_path('project') } + + before do + subject + end + + it 'has the project attributes and relations', :aggregate_failures do + expect(project.description).to eq('Nisi et repellendus ut enim quo accusamus vel magnam.') + expect(project.issues.count).to eq(10) + expect(project.milestones.count).to eq(3) + expect(project.labels.count).to eq(2) + expect(project.project_feature).not_to be_nil + end + + it 'has issues with correctly updated due dates' do + due_dates = due_dates(project.issues) + + expect(due_dates).to match_array([Date.today - 7.days, Date.today, Date.today + 7.days]) + end + + it 'has milestones with correctly updated due dates' do + due_dates = due_dates(project.milestones) + + expect(due_dates).to match_array([Date.today - 7.days, Date.today, Date.today + 7.days]) + end + + def due_dates(relations) + due_dates = relations.map { |relation| relation['due_date'] } + due_dates.compact! + due_dates.sort + end + end + end + + context 'when restoring a project' do + let(:importable) { create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') } + let(:importable_name) { 'project' } + let(:importable_path) { 'project' } + let(:object_builder) { Gitlab::ImportExport::Project::ObjectBuilder } + let(:relation_factory) { Gitlab::ImportExport::Project::RelationFactory } + let(:reader) { Gitlab::ImportExport::Reader.new(shared: shared) } + + context 'using ndjson reader' do + let(:path) { 'spec/fixtures/lib/gitlab/import_export/sample_data/tree' } + let(:relation_reader) { Gitlab::ImportExport::JSON::NdjsonReader.new(path) } + + it_behaves_like 'import project successfully' + end + end +end diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index f75494aa7c7..c05968c9a85 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -1040,6 +1040,41 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do it_behaves_like 'project tree restorer work properly', :legacy_reader, true it_behaves_like 'project tree restorer work properly', :ndjson_reader, true + + context 'Sample Data JSON' do + let(:user) { create(:user) } + let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } + let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } + + before do + setup_import_export_config('sample_data') + setup_reader(:ndjson_reader) + end + + context 'with sample_data_template' do + before do + allow(project).to receive_message_chain(:import_data, :data, :dig).with('sample_data') { true } + end + + it 'initialize SampleDataRelationTreeRestorer' do + expect_next_instance_of(Gitlab::ImportExport::Project::Sample::SampleDataRelationTreeRestorer) do |restorer| + expect(restorer).to receive(:restore).and_return(true) + end + + expect(project_tree_restorer.restore).to eq(true) + end + end + + context 'without sample_data_template' do + it 'initialize RelationTreeRestorer' do + expect_next_instance_of(Gitlab::ImportExport::RelationTreeRestorer) do |restorer| + expect(restorer).to receive(:restore).and_return(true) + end + + expect(project_tree_restorer.restore).to eq(true) + end + end + end end context 'disable ndjson import' do diff --git a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb index ddc96b83208..bd9ac6d6697 100644 --- a/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/relation_tree_restorer_spec.rb @@ -10,15 +10,7 @@ require 'spec_helper' RSpec.describe Gitlab::ImportExport::RelationTreeRestorer do - include ImportExport::CommonUtil - - let(:user) { create(:user) } - let(:shared) { Gitlab::ImportExport::Shared.new(importable) } - let(:attributes) { relation_reader.consume_attributes(importable_name) } - - let(:members_mapper) do - Gitlab::ImportExport::MembersMapper.new(exported_members: {}, user: user, importable: importable) - end + include_context 'relation tree restorer shared context' let(:relation_tree_restorer) do described_class.new( diff --git a/spec/lib/gitlab/sample_data_template_spec.rb b/spec/lib/gitlab/sample_data_template_spec.rb new file mode 100644 index 00000000000..7d0d415b3af --- /dev/null +++ b/spec/lib/gitlab/sample_data_template_spec.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::SampleDataTemplate do + describe '.all' do + it 'returns all templates' do + expected = %w[ + basic + serenity_valley + ] + + expect(described_class.all).to be_an(Array) + expect(described_class.all.map(&:name)).to match_array(expected) + end + end + + describe '.find' do + subject { described_class.find(query) } + + context 'when there is a match' do + let(:query) { :basic } + + it { is_expected.to be_a(described_class) } + end + + context 'when there is no match' do + let(:query) { 'no-match' } + + it { is_expected.to be(nil) } + end + end + + describe '.archive_directory' do + subject { described_class.archive_directory } + + it { is_expected.to be_a Pathname } + end + + describe 'validate all templates' do + let_it_be(:admin) { create(:admin) } + + described_class.all.each do |template| + it "#{template.name} has a valid archive" do + archive = template.archive_path + + expect(File.exist?(archive)).to be(true) + end + + context 'with valid parameters' do + it 'can be imported' do + params = { + template_name: template.name, + namespace_id: admin.namespace.id, + path: template.name + } + + project = Projects::CreateFromTemplateService.new(admin, params).execute + + expect(project).to be_valid + expect(project).to be_persisted + end + end + end + end +end diff --git a/spec/mailers/emails/merge_requests_spec.rb b/spec/mailers/emails/merge_requests_spec.rb index 477fb16400a..9235a946394 100644 --- a/spec/mailers/emails/merge_requests_spec.rb +++ b/spec/mailers/emails/merge_requests_spec.rb @@ -33,4 +33,37 @@ RSpec.describe Emails::MergeRequests do expect(subject).to have_content current_user.name end end + + describe '#merge_requests_csv_email' do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:merge_requests) { create_list(:merge_request, 10) } + let(:export_status) do + { + rows_expected: 10, + rows_written: 10, + truncated: false + } + end + + let(:csv_data) { MergeRequests::ExportCsvService.new(MergeRequest.all, project).csv_data } + + subject { Notify.merge_requests_csv_email(user, project, csv_data, export_status) } + + it { expect(subject.subject).to eq("#{project.name} | Exported merge requests") } + it { expect(subject.to).to contain_exactly(user.notification_email_for(project.group)) } + it { expect(subject).to have_content('Your CSV export of 10 merge requests from project')} + + context 'when truncated' do + let(:export_status) do + { + rows_expected: 10, + rows_written: 10, + truncated: true + } + end + + it { expect(subject).to have_content('This attachment has been truncated to avoid exceeding the maximum allowed attachment size of 15MB.') } + end + end end diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb index 086d87c27eb..5cad35eaedf 100644 --- a/spec/serializers/merge_request_widget_entity_spec.rb +++ b/spec/serializers/merge_request_widget_entity_spec.rb @@ -299,9 +299,7 @@ RSpec.describe MergeRequestWidgetEntity do describe 'user callouts' do context 'when suggest pipeline feature is enabled' do - before do - stub_experiment(suggest_pipeline: true) - end + subject { described_class.new(resource, request: request, experiment_enabled: :suggest_pipeline).as_json } it 'provides a valid path value for user callout path' do expect(subject[:user_callouts_path]).to eq '/-/user_callouts' @@ -335,10 +333,6 @@ RSpec.describe MergeRequestWidgetEntity do end context 'when suggest pipeline feature is not enabled' do - before do - stub_experiment(suggest_pipeline: false) - end - it 'provides no valid value for user callout path' do expect(subject[:user_callouts_path]).to be_nil end diff --git a/spec/services/merge_requests/export_csv_service_spec.rb b/spec/services/merge_requests/export_csv_service_spec.rb index 8161a444231..ecb17b3fe77 100644 --- a/spec/services/merge_requests/export_csv_service_spec.rb +++ b/spec/services/merge_requests/export_csv_service_spec.rb @@ -6,7 +6,7 @@ RSpec.describe MergeRequests::ExportCsvService do let_it_be(:merge_request) { create(:merge_request) } let(:csv) { CSV.parse(subject.csv_data, headers: true).first } - subject { described_class.new(MergeRequest.where(id: merge_request.id)) } + subject { described_class.new(MergeRequest.where(id: merge_request.id), merge_request.project) } describe 'csv_data' do it 'contains the correct information', :aggregate_failures do diff --git a/spec/services/users/validate_otp_service_spec.rb b/spec/services/users/validate_otp_service_spec.rb new file mode 100644 index 00000000000..826755d6145 --- /dev/null +++ b/spec/services/users/validate_otp_service_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::ValidateOtpService do + let_it_be(:user) { create(:user) } + let(:otp_code) { 42 } + + subject(:validate) { described_class.new(user).execute(otp_code) } + + context 'Devise' do + it 'calls Devise strategy' do + expect_next_instance_of(::Gitlab::Auth::Otp::Strategies::Devise) do |strategy| + expect(strategy).to receive(:validate).with(otp_code).once + end + + validate + end + end + + context 'FortiAuthenticator' do + before do + stub_feature_flags(forti_authenticator: true) + end + + it 'calls FortiAuthenticator strategy' do + expect_next_instance_of(::Gitlab::Auth::Otp::Strategies::FortiAuthenticator) do |strategy| + expect(strategy).to receive(:validate).with(otp_code).once + end + + validate + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6e41ae3fbfd..11a45e005b8 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -212,6 +212,10 @@ RSpec.configure do |config| # for now whilst we migrate as much as we can over the GraphQL stub_feature_flags(merge_request_widget_graphql: false) + # Using FortiAuthenticator as OTP provider is disabled by default in + # tests, until we introduce it in user settings + stub_feature_flags(forti_authenticator: false) + enable_rugged = example.metadata[:enable_rugged].present? # Disable Rugged features by default diff --git a/spec/support/helpers/multipart_helpers.rb b/spec/support/helpers/multipart_helpers.rb index 043cb6e1420..2e8db0e9e42 100644 --- a/spec/support/helpers/multipart_helpers.rb +++ b/spec/support/helpers/multipart_helpers.rb @@ -31,7 +31,7 @@ module MultipartHelpers raise ArgumentError, "can't handle #{mode} mode" end - return result if ::Feature.disabled?(:upload_middleware_jwt_params_handler) + return result if ::Feature.disabled?(:upload_middleware_jwt_params_handler, default_enabled: true) # the HandlerForJWTParams expects a jwt token with the upload parameters # *without* the "#{key}." prefix diff --git a/spec/support/shared_contexts/lib/gitlab/import_export/relation_tree_restorer_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/import_export/relation_tree_restorer_shared_context.rb new file mode 100644 index 00000000000..6b9ddc70691 --- /dev/null +++ b/spec/support/shared_contexts/lib/gitlab/import_export/relation_tree_restorer_shared_context.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.shared_context 'relation tree restorer shared context' do + include ImportExport::CommonUtil + + let(:user) { create(:user) } + let(:shared) { Gitlab::ImportExport::Shared.new(importable) } + let(:attributes) { relation_reader.consume_attributes(importable_name) } + + let(:members_mapper) do + Gitlab::ImportExport::MembersMapper.new(exported_members: {}, user: user, importable: importable) + end +end diff --git a/spec/workers/export_csv_worker_spec.rb b/spec/workers/export_csv_worker_spec.rb index 1a5b17ee35b..88ccfac0a02 100644 --- a/spec/workers/export_csv_worker_spec.rb +++ b/spec/workers/export_csv_worker_spec.rb @@ -10,25 +10,9 @@ RSpec.describe ExportCsvWorker do described_class.new.perform(user.id, project.id, params) end - it 'emails a CSV' do - expect {perform}.to change(ActionMailer::Base.deliveries, :size).by(1) - end - - it 'ensures that project_id is passed to issues_finder' do - expect(IssuesFinder).to receive(:new).with(anything, hash_including(project_id: project.id)).and_call_original + it 'delegates call to IssuableExportCsvWorker' do + expect(IssuableExportCsvWorker).to receive(:perform_async).with(:issue, user.id, project.id, anything) perform end - - it 'removes sort parameter' do - expect(IssuesFinder).to receive(:new).with(anything, hash_not_including(:sort)).and_call_original - - perform - end - - it 'converts controller string keys to symbol keys for IssuesFinder' do - expect(IssuesFinder).to receive(:new).with(anything, hash_including(test_key: true)).and_call_original - - perform('test_key' => true) - end end diff --git a/spec/workers/issuable_export_csv_worker_spec.rb b/spec/workers/issuable_export_csv_worker_spec.rb new file mode 100644 index 00000000000..bcc2420996d --- /dev/null +++ b/spec/workers/issuable_export_csv_worker_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe IssuableExportCsvWorker do + let(:user) { create(:user) } + let(:project) { create(:project, creator: user) } + let(:params) { {} } + + subject { described_class.new.perform(issuable_type, user.id, project.id, params) } + + context 'when issuable type is Issue' do + let(:issuable_type) { :issue } + + it 'emails a CSV' do + expect { subject }.to change(ActionMailer::Base.deliveries, :size).by(1) + end + + it 'ensures that project_id is passed to issues_finder' do + expect(IssuesFinder).to receive(:new).with(anything, hash_including(project_id: project.id)).and_call_original + + subject + end + + it 'removes sort parameter' do + expect(IssuesFinder).to receive(:new).with(anything, hash_not_including(:sort)).and_call_original + + subject + end + + it 'calls the issue export service' do + expect(Issues::ExportCsvService).to receive(:new).once.and_call_original + + subject + end + + context 'with params' do + let(:params) { { 'test_key' => true } } + + it 'converts controller string keys to symbol keys for IssuesFinder' do + expect(IssuesFinder).to receive(:new).with(user, hash_including(test_key: true)).and_call_original + + subject + end + end + end + + context 'when issuable type is MergeRequest' do + let(:issuable_type) { :merge_request } + + it 'emails a CSV' do + expect { subject }.to change(ActionMailer::Base.deliveries, :size).by(1) + end + + it 'calls the MR export service' do + expect(MergeRequests::ExportCsvService).to receive(:new).with(anything, project).once.and_call_original + + subject + end + + it 'calls the MergeRequest finder' do + expect(MergeRequestsFinder).to receive(:new).once.and_call_original + + subject + end + end + + context 'when issuable type is User' do + let(:issuable_type) { :user } + + it { expect { subject }.to raise_error(ArgumentError) } + end +end diff --git a/vendor/sample_data_templates/basic.tar.gz b/vendor/sample_data_templates/basic.tar.gz Binary files differnew file mode 100644 index 00000000000..1ab09f8dc41 --- /dev/null +++ b/vendor/sample_data_templates/basic.tar.gz diff --git a/vendor/sample_data_templates/serenity_valley.tar.gz b/vendor/sample_data_templates/serenity_valley.tar.gz Binary files differnew file mode 100644 index 00000000000..e469d61ca7b --- /dev/null +++ b/vendor/sample_data_templates/serenity_valley.tar.gz |