diff options
298 files changed, 3825 insertions, 1004 deletions
diff --git a/.gitlab/issue_templates/Feature proposal.md b/.gitlab/issue_templates/Feature proposal.md index 8a49715e0e8..2ba6b68a53e 100644 --- a/.gitlab/issue_templates/Feature proposal.md +++ b/.gitlab/issue_templates/Feature proposal.md @@ -26,7 +26,7 @@ Add all known Documentation Requirements here, per https://docs.gitlab.com/ee/de ### Testing -<!-- What risks does this change pose? How might it affect the quality of the product? What additional test coverage or changes to tests will be needed? Will it require cross-browser testing? See the test engineering process for further guidelines: https://about.gitlab.com/handbook/engineering/quality/guidelines/test-engineering/ --> +<!-- What risks does this change pose? How might it affect the quality of the product? What additional test coverage or changes to tests will be needed? Will it require cross-browser testing? See the test engineering process for further help: https://about.gitlab.com/handbook/engineering/quality/test-engineering/ --> ### What does success look like, and how can we measure that? diff --git a/.gitlab/issue_templates/Test plan.md b/.gitlab/issue_templates/Test plan.md index 3aedd5859d3..f194adebc87 100644 --- a/.gitlab/issue_templates/Test plan.md +++ b/.gitlab/issue_templates/Test plan.md @@ -89,7 +89,7 @@ New end-to-end and integration tests (Selenium and API) should be added to the Please note if automated tests already exist. -When adding new automated tests, please keep [testing levels](https://docs.gitlab.com/ce/development/testing_guide/testing_levels.html) +When adding new automated tests, please keep [testing levels](https://docs.gitlab.com/ee/development/testing_guide/testing_levels.html) in mind. --> diff --git a/.rubocop.yml b/.rubocop.yml index 79e06439ac2..b75c63e1f58 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -8,7 +8,7 @@ require: - rubocop-rspec AllCops: - TargetRubyVersion: 2.5 + TargetRubyVersion: 2.6 TargetRailsVersion: 5.0 Exclude: - 'vendor/**/*' diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index f269cd1b8b6..bb120e876c6 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -1.58.0
\ No newline at end of file +1.59.0 @@ -51,6 +51,7 @@ gem 'jwt', '~> 2.1.0' # Spam and anti-bot protection gem 'recaptcha', '~> 4.11', require: 'recaptcha/rails' gem 'akismet', '~> 2.0' +gem 'invisible_captcha', '~> 0.12.1' # Two-factor authentication gem 'devise-two-factor', '~> 3.0.0' @@ -297,6 +298,9 @@ gem 'batch-loader', '~> 1.4.0' # Perf bar gem 'peek', '~> 1.0.1' +# Snowplow events tracking +gem 'snowplow-tracker', '~> 0.6.1' + # Memory benchmarks gem 'derailed_benchmarks', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 6aa96d54abb..fcc0fb64897 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -152,6 +152,7 @@ GEM concurrent-ruby-ext (1.1.5) concurrent-ruby (= 1.1.5) connection_pool (2.2.2) + contracts (0.11.0) crack (0.4.3) safe_yaml (~> 1.0.0) crass (1.0.4) @@ -437,6 +438,8 @@ GEM influxdb (0.2.3) cause json + invisible_captcha (0.12.1) + rails (>= 3.2.0) ipaddress (0.8.3) jaeger-client (0.10.0) opentracing (~> 0.3) @@ -843,7 +846,7 @@ GEM rubyntlm (0.6.2) rubypants (0.2.0) rubyzip (1.2.2) - rugged (0.28.2) + rugged (0.28.3.1) safe_yaml (1.0.4) sanitize (4.6.6) crass (~> 1.0.2) @@ -901,6 +904,8 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.2) slack-notifier (1.5.1) + snowplow-tracker (0.6.1) + contracts (~> 0.7, <= 0.11) spring (2.0.2) activesupport (>= 4.2) spring-commands-rspec (1.0.4) @@ -1126,6 +1131,7 @@ DEPENDENCIES httparty (~> 0.16.4) icalendar influxdb (~> 0.2) + invisible_captcha (~> 0.12.1) jira-ruby (~> 1.4) js_regex (~> 3.1) json-schema (~> 2.8.0) @@ -1229,6 +1235,7 @@ DEPENDENCIES simple_po_parser (~> 1.1.2) simplecov (~> 0.16.1) slack-notifier (~> 1.5.1) + snowplow-tracker (~> 0.6.1) spring (~> 2.0.0) spring-commands-rspec (~> 1.0.4) sprockets (~> 3.7.0) @@ -1,4 +1,12 @@ -Copyright GitLab B.V. +Copyright (c) 2011-present GitLab B.V. + +Portions of this software are licensed as follows: + +* All content residing under the "doc/" directory of this repository is licensed under "Creative Commons: CC BY-SA 4.0 license". +* All content that resides under the "ee/" directory of this repository, if that directory exists, is licensed under the license defined in "ee/LICENSE". +* All client-side JavaScript (when served directly or after being compiled, arranged, augmented, or combined), is licensed under the "MIT Expat" license. +* All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component. +* Content outside of the above mentioned directories or restrictions above is available under the "MIT Expat" license as defined below. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 73bfcc988cc..054e2d02461 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,6 @@ To see how GitLab looks please see the [features page on our website](https://ab - Used by more than 100,000 organizations, GitLab is the most popular solution to manage Git repositories on-premises - Completely free and open source (MIT Expat license) -## Hiring - -We're hiring developers, support people, and production engineers all the time, please see our [jobs page](https://about.gitlab.com/jobs/). - ## Editions There are two editions of GitLab: @@ -31,6 +27,15 @@ There are two editions of GitLab: - GitLab Community Edition (CE) is available freely under the MIT Expat license. - GitLab Enterprise Edition (EE) includes [extra features](https://about.gitlab.com/pricing/#compare-options) that are more useful for organizations with more than 100 users. To use EE and get official support please [become a subscriber](https://about.gitlab.com/pricing/). +## Licensing + +See the [LICENSE](LICENSE) file for licensing information as it pertains to +files in this repository. + +## Hiring + +We're hiring developers, support people, and production engineers all the time, please see our [jobs page](https://about.gitlab.com/jobs/). + ## Website On [about.gitlab.com](https://about.gitlab.com/) you can find more information about: @@ -58,14 +63,6 @@ There are various other options to install GitLab, please refer to the [installa GitLab is an open source project and we are very happy to accept community contributions. Please refer to [Contributing to GitLab page](https://about.gitlab.com/contributing/) for more details. -## Licensing - -GitLab Community Edition (CE) is available freely under the MIT Expat license. - -All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component. - -All Documentation content that resides under the `doc/` directory of this repository is licensed under Creative Commons: CC BY-SA 4.0. - ## Install a development environment To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit). diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 9e97f345717..ba33d72b1f3 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -107,6 +107,7 @@ function deferredInitialisation() { .then(() => { $('select.select2').select2({ width: 'resolve', + minimumResultsForSearch: 10, dropdownAutoWidth: true, }); diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue index 5b950f8c966..838447e6c75 100644 --- a/app/assets/javascripts/monitoring/components/charts/area.vue +++ b/app/assets/javascripts/monitoring/components/charts/area.vue @@ -1,7 +1,6 @@ <script> import { __ } from '~/locale'; -import { mapState } from 'vuex'; -import { GlLink, GlButton } from '@gitlab/ui'; +import { GlLink } from '@gitlab/ui'; import { GlAreaChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts'; import dateFormat from 'dateformat'; import { debounceByAnimationFrame, roundOffFloat } from '~/lib/utils/common_utils'; @@ -16,7 +15,6 @@ let debouncedResize; export default { components: { GlAreaChart, - GlButton, GlChartSeriesLabel, GlLink, Icon, @@ -69,7 +67,6 @@ export default { }; }, computed: { - ...mapState('monitoringDashboard', ['exportMetricsToCsvEnabled']), chartData() { // Transforms & supplements query data to render appropriate labels & styles // Input: [{ queryAttributes1 }, { queryAttributes2 }] @@ -179,18 +176,6 @@ export default { yAxisLabel() { return `${this.graphData.y_label}`; }, - csvText() { - const chartData = this.chartData[0].data; - const header = `timestamp,${this.graphData.y_label}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings - return chartData.reduce((csv, data) => { - const row = data.join(','); - return `${csv}${row}\r\n`; - }, header); - }, - downloadLink() { - const data = new Blob([this.csvText], { type: 'text/plain' }); - return window.URL.createObjectURL(data); - }, }, watch: { containerWidth: 'onResize', @@ -259,16 +244,6 @@ export default { <div :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }"> <div class="prometheus-graph-header"> <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5> - <gl-button - v-if="exportMetricsToCsvEnabled" - :href="downloadLink" - :title="__('Download CSV')" - :aria-label="__('Download CSV')" - style="margin-left: 200px;" - download="chart_metrics.csv" - > - {{ __('Download CSV') }} - </gl-button> <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div> </div> <gl-area-chart diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue index 782e4310f3e..587392adbc3 100644 --- a/app/assets/javascripts/monitoring/components/dashboard.vue +++ b/app/assets/javascripts/monitoring/components/dashboard.vue @@ -235,6 +235,19 @@ export default { chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)), ); }, + csvText(graphData) { + const chartData = graphData.queries[0].result[0].values; + const yLabel = graphData.y_label; + const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings + return chartData.reduce((csv, data) => { + const row = data.join(','); + return `${csv}${row}\r\n`; + }, header); + }, + downloadCsv(graphData) { + const data = new Blob([this.csvText(graphData)], { type: 'text/plain' }); + return window.URL.createObjectURL(data); + }, // TODO: BEGIN, Duplicated code with panel_type until feature flag is removed // Issue number: https://gitlab.com/gitlab-org/gitlab-ce/issues/63845 getGraphAlerts(queries) { @@ -448,7 +461,6 @@ export default { @setAlerts="setAlerts" /> <gl-dropdown - v-if="alertWidgetAvailable" v-gl-tooltip class="mx-2" toggle-class="btn btn-transparent border-0" @@ -459,6 +471,9 @@ export default { <template slot="button-content"> <icon name="ellipsis_v" class="text-secondary" /> </template> + <gl-dropdown-item :href="downloadCsv(graphData)" download="chart_metrics.csv"> + {{ __('Download CSV') }} + </gl-dropdown-item> <gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}-${graphIndex}`" diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue index 295c0851f12..3fbac71f3d7 100644 --- a/app/assets/javascripts/monitoring/components/panel_type.vue +++ b/app/assets/javascripts/monitoring/components/panel_type.vue @@ -1,7 +1,14 @@ <script> import { mapState } from 'vuex'; import _ from 'underscore'; -import { GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui'; +import { + GlDropdown, + GlDropdownItem, + GlModal, + GlModalDirective, + GlTooltipDirective, +} from '@gitlab/ui'; +import Icon from '~/vue_shared/components/icon.vue'; import MonitorAreaChart from './charts/area.vue'; import MonitorSingleStatChart from './charts/single_stat.vue'; import MonitorEmptyChart from './charts/empty_chart.vue'; @@ -11,12 +18,14 @@ export default { MonitorAreaChart, MonitorSingleStatChart, MonitorEmptyChart, + Icon, GlDropdown, GlDropdownItem, GlModal, }, directives: { GlModal: GlModalDirective, + GlTooltip: GlTooltipDirective, }, props: { graphData: { @@ -41,6 +50,19 @@ export default { graphDataHasMetrics() { return this.graphData.queries[0].result.length > 0; }, + csvText() { + const chartData = this.graphData.queries[0].result[0].values; + const yLabel = this.graphData.y_label; + const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings + return chartData.reduce((csv, data) => { + const row = data.join(','); + return `${csv}${row}\r\n`; + }, header); + }, + downloadCsv() { + const data = new Blob([this.csvText], { type: 'text/plain' }); + return window.URL.createObjectURL(data); + }, }, methods: { getGraphAlerts(queries) { @@ -81,7 +103,6 @@ export default { @setAlerts="setAlerts" /> <gl-dropdown - v-if="alertWidgetAvailable" v-gl-tooltip class="mx-2" toggle-class="btn btn-transparent border-0" @@ -92,6 +113,9 @@ export default { <template slot="button-content"> <icon name="ellipsis_v" class="text-secondary" /> </template> + <gl-dropdown-item :href="downloadCsv" download="chart_metrics.csv"> + {{ __('Download CSV') }} + </gl-dropdown-item> <gl-dropdown-item v-if="alertWidgetAvailable" v-gl-modal="`alert-modal-${index}`"> {{ __('Alerts') }} </gl-dropdown-item> diff --git a/app/assets/javascripts/monitoring/monitoring_bundle.js b/app/assets/javascripts/monitoring/monitoring_bundle.js index 366034becd0..c0fee1ebb99 100644 --- a/app/assets/javascripts/monitoring/monitoring_bundle.js +++ b/app/assets/javascripts/monitoring/monitoring_bundle.js @@ -13,7 +13,6 @@ export default (props = {}) => { prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint, multipleDashboardsEnabled: gon.features.environmentMetricsShowMultipleDashboards, additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes, - exportMetricsToCsvEnabled: gon.features.exportMetricsToCsvEnabled, }); } diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js index a9c491c7c6c..0cbad179f17 100644 --- a/app/assets/javascripts/monitoring/stores/actions.js +++ b/app/assets/javascripts/monitoring/stores/actions.js @@ -37,17 +37,11 @@ export const setEndpoints = ({ commit }, endpoints) => { export const setFeatureFlags = ( { commit }, - { - prometheusEndpointEnabled, - multipleDashboardsEnabled, - additionalPanelTypesEnabled, - exportMetricsToCsvEnabled, - }, + { prometheusEndpointEnabled, multipleDashboardsEnabled, additionalPanelTypesEnabled }, ) => { commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled); commit(types.SET_MULTIPLE_DASHBOARDS_ENABLED, multipleDashboardsEnabled); commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled); - commit(types.SET_EXPORT_METRICS_TO_CSV_ENABLED, exportMetricsToCsvEnabled); }; export const setShowErrorBanner = ({ commit }, enabled) => { diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js index 9ec8214b167..4b1aadbcf05 100644 --- a/app/assets/javascripts/monitoring/stores/mutation_types.js +++ b/app/assets/javascripts/monitoring/stores/mutation_types.js @@ -17,4 +17,3 @@ export const SET_ENDPOINTS = 'SET_ENDPOINTS'; export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'; export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE'; export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER'; -export const SET_EXPORT_METRICS_TO_CSV_ENABLED = 'SET_EXPORT_METRICS_TO_CSV_ENABLED'; diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js index a2dceb21fc0..b19520d6638 100644 --- a/app/assets/javascripts/monitoring/stores/mutations.js +++ b/app/assets/javascripts/monitoring/stores/mutations.js @@ -99,7 +99,4 @@ export default { [types.SET_SHOW_ERROR_BANNER](state, enabled) { state.showErrorBanner = enabled; }, - [types.SET_EXPORT_METRICS_TO_CSV_ENABLED](state, enabled) { - state.exportMetricsToCsvEnabled = enabled; - }, }; diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js index a14a25e3a20..440bdc951e0 100644 --- a/app/assets/javascripts/monitoring/stores/state.js +++ b/app/assets/javascripts/monitoring/stores/state.js @@ -10,7 +10,6 @@ export default () => ({ useDashboardEndpoint: false, multipleDashboardsEnabled: false, additionalPanelTypesEnabled: false, - exportMetricsToCsvEnabled: false, emptyState: 'gettingStarted', showEmptyState: true, showErrorBanner: true, diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue index a347269c916..53bf9d5ab6f 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_merge_help.vue @@ -23,7 +23,7 @@ export default { }; </script> <template> - <section class="mr-widget-help"> + <section class="mr-widget-help font-italic"> <template v-if="missingBranch"> {{ missingBranchInfo }} </template> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue index 76b96c8c1c0..8fdf61a6b8d 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue @@ -18,8 +18,8 @@ export default { Deployment, MrWidgetContainer, MrWidgetPipeline, - MergeTrainInfo: () => - import('ee_component/vue_merge_request_widget/components/merge_train_info.vue'), + MergeTrainPositionIndicator: () => + import('ee_component/vue_merge_request_widget/components/merge_train_position_indicator.vue'), }, props: { mr: { @@ -62,7 +62,7 @@ export default { showVisualReviewAppLink() { return this.mr.visualReviewAppAvailable; }, - showMergeTrainInfo() { + showMergeTrainPositionIndicator() { return _.isNumber(this.mr.mergeTrainIndex); }, }, @@ -90,8 +90,8 @@ export default { :visual-review-app-meta="visualReviewAppMeta" /> </div> - <merge-train-info - v-if="showMergeTrainInfo" + <merge-train-position-indicator + v-if="showMergeTrainPositionIndicator" class="mr-widget-extension" :merge-train-index="mr.mergeTrainIndex" /> diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue index d4514767912..e294e1de976 100644 --- a/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue +++ b/app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue @@ -94,9 +94,6 @@ export default { return __('Merge'); }, - shouldShowMergeOptionsDropdown() { - return this.isAutoMergeAvailable && !this.mr.onlyAllowMergeIfPipelineSucceeds; - }, isRemoveSourceBranchButtonDisabled() { return this.isMergeButtonDisabled; }, @@ -246,7 +243,7 @@ export default { {{ mergeButtonText }} </button> <button - v-if="isAutoMergeAvailable" + v-if="shouldShowMergeImmediatelyDropdown" :disabled="isMergeButtonDisabled" type="button" class="btn btn-sm btn-info dropdown-toggle js-merge-moment" @@ -256,7 +253,7 @@ export default { <i class="fa fa-chevron-down qa-merge-moment-dropdown" aria-hidden="true"></i> </button> <ul - v-if="shouldShowMergeOptionsDropdown" + v-if="shouldShowMergeImmediatelyDropdown" class="dropdown-menu dropdown-menu-right" role="menu" > diff --git a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js index 116d537c463..eef49e20159 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js +++ b/app/assets/javascripts/vue_merge_request_widget/mixins/ready_to_merge.js @@ -15,5 +15,8 @@ export default { // MWPS is currently the only auto merge strategy available in CE return __('Merge when pipeline succeeds'); }, + shouldShowMergeImmediatelyDropdown() { + return this.mr.isPipelineActive && !this.mr.onlyAllowMergeIfPipelineSucceeds; + }, }, }; diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index 3eab8e6fc0b..0f55bebd3fc 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -31,6 +31,9 @@ export default class MergeRequestStore { this.targetBranchSha = data.target_branch_sha; this.sourceBranch = data.source_branch; this.sourceBranchProtected = data.source_branch_protected; + this.conflictsDocsPath = data.conflicts_docs_path; + this.mergeRequestPipelinesHelpPath = data.merge_request_pipelines_docs_path; + this.mergeTrainWhenPipelineSucceedsDocsPath = data.merge_train_when_pipeline_succeeds_docs_path; this.mergeStatus = data.merge_status; this.commitMessage = data.default_merge_commit_message; this.shortMergeCommitSha = data.short_merge_commit_sha; diff --git a/app/assets/stylesheets/framework/callout.scss b/app/assets/stylesheets/framework/callout.scss index 643b20c56bc..c5bb2a1256a 100644 --- a/app/assets/stylesheets/framework/callout.scss +++ b/app/assets/stylesheets/framework/callout.scss @@ -9,7 +9,9 @@ .bs-callout { margin: $gl-padding 0; padding: $gl-padding; - border-left: 3px solid $border-color; + border-color: $border-color; + border-style: solid; + border-width: 0 0 0 3px; color: $text-color; background: $gray-light; @@ -48,6 +50,10 @@ background-color: $blue-100; border-color: $blue-200; color: $blue-700; + + h4 { + color: $blue-700; + } } .bs-callout-success { diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index 3c1e384d6ed..c8d155706a9 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -397,7 +397,6 @@ .mr-widget-help { padding: 10px 16px 10px ($gl-padding-8 * 7); - font-style: italic; } .ci-coverage { @@ -906,7 +905,7 @@ } .deploy-heading, -.merge-train-info { +.merge-train-position-indicator { @include media-breakpoint-up(md) { padding: $gl-padding-8 $gl-padding; } diff --git a/app/assets/stylesheets/pages/wiki.scss b/app/assets/stylesheets/pages/wiki.scss index 60400f10ca5..379df1c4db1 100644 --- a/app/assets/stylesheets/pages/wiki.scss +++ b/app/assets/stylesheets/pages/wiki.scss @@ -1,19 +1,3 @@ -.new-wiki-page { - .new-wiki-page-slug-tip { - display: inline-block; - max-width: 100%; - margin-top: 5px; - } -} - -.wiki-form { - .edit-wiki-page-slug-tip { - display: inline-block; - max-width: 100%; - margin-top: 5px; - } -} - .title .edit-wiki-header { width: 780px; margin-left: auto; @@ -22,7 +6,6 @@ } .wiki-page-header { - @extend .top-area; position: relative; .wiki-breadcrumb { diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1d55a073f3b..5e65084a110 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -116,7 +116,7 @@ class ApplicationController < ActionController::Base def render(*args) super.tap do # Set a header for custom error pages to prevent them from being intercepted by gitlab-workhorse - if response.content_type == 'text/html' && (400..599).cover?(response.status) + if (400..599).cover?(response.status) && workhorse_excluded_content_types.include?(response.content_type) response.headers['X-GitLab-Custom-Error'] = '1' end end @@ -124,6 +124,10 @@ class ApplicationController < ActionController::Base protected + def workhorse_excluded_content_types + @workhorse_excluded_content_types ||= %w(text/html application/json) + end + def append_info_to_payload(payload) super diff --git a/app/controllers/concerns/invisible_captcha.rb b/app/controllers/concerns/invisible_captcha.rb new file mode 100644 index 00000000000..c9f66e5c194 --- /dev/null +++ b/app/controllers/concerns/invisible_captcha.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module InvisibleCaptcha + extend ActiveSupport::Concern + + included do + invisible_captcha only: :create, on_spam: :on_honeypot_spam_callback, on_timestamp_spam: :on_timestamp_spam_callback + end + + def on_honeypot_spam_callback + return unless Feature.enabled?(:invisible_captcha) + + invisible_captcha_honeypot_counter.increment + log_request('Invisible_Captcha_Honeypot_Request') + + head(200) + end + + def on_timestamp_spam_callback + return unless Feature.enabled?(:invisible_captcha) + + invisible_captcha_timestamp_counter.increment + log_request('Invisible_Captcha_Timestamp_Request') + + redirect_to new_user_session_path, alert: InvisibleCaptcha.timestamp_error_message + end + + def invisible_captcha_honeypot_counter + @invisible_captcha_honeypot_counter ||= + Gitlab::Metrics.counter(:bot_blocked_by_invisible_captcha_honeypot, + 'Counter of blocked sign up attempts with filled honeypot') + end + + def invisible_captcha_timestamp_counter + @invisible_captcha_timestamp_counter ||= + Gitlab::Metrics.counter(:bot_blocked_by_invisible_captcha_timestamp, + 'Counter of blocked sign up attempts with invalid timestamp') + end + + def log_request(message) + request_information = { + message: message, + env: :invisible_captcha_signup_bot_detected, + ip: request.ip, + request_method: request.request_method, + fullpath: request.fullpath + } + + Gitlab::AuthLogger.error(request_information) + end +end diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb index 5472ef05d7c..886d1f99d69 100644 --- a/app/controllers/groups_controller.rb +++ b/app/controllers/groups_controller.rb @@ -176,6 +176,7 @@ class GroupsController < Groups::ApplicationController [ :avatar, :description, + :emails_disabled, :lfs_enabled, :name, :path, diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb index 2d46a71bf99..3b0abecf2c9 100644 --- a/app/controllers/projects/cycle_analytics_controller.rb +++ b/app/controllers/projects/cycle_analytics_controller.rb @@ -14,8 +14,14 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController @cycle_analytics_no_data = @cycle_analytics.no_stats? respond_to do |format| - format.html - format.json { render json: cycle_analytics_json } + format.html do + Gitlab::UsageDataCounters::CycleAnalyticsCounter.count(:views) + + render :show + end + format.json do + render json: cycle_analytics_json + end end end diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb index df9e55fda2a..5a1f93dc609 100644 --- a/app/controllers/projects/environments_controller.rb +++ b/app/controllers/projects/environments_controller.rb @@ -15,7 +15,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards) push_frontend_feature_flag(:environment_metrics_additional_panel_types) push_frontend_feature_flag(:prometheus_computed_alerts) - push_frontend_feature_flag(:export_metrics_to_csv_enabled) end def index diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb index 956093b972b..abf8407a51c 100644 --- a/app/controllers/projects/git_http_client_controller.rb +++ b/app/controllers/projects/git_http_client_controller.rb @@ -49,7 +49,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController send_final_spnego_response return # Allow access end - elsif project && download_request? && Guest.can?(:download_code, project) + elsif project && download_request? && http_allowed? && Guest.can?(:download_code, project) + @authentication_result = Gitlab::Auth::Result.new(nil, project, :none, [:download_code]) return # Allow access @@ -113,4 +114,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController def ci? authentication_result.ci?(project) end + + def http_allowed? + Gitlab::ProtocolAccess.allowed?('http') + end end diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb index 3254229d9cb..c94fdd9483d 100644 --- a/app/controllers/projects/raw_controller.rb +++ b/app/controllers/projects/raw_controller.rb @@ -26,7 +26,7 @@ class Projects::RawController < Projects::ApplicationController limiter.log_request(request, :raw_blob_request_limit, current_user) flash[:alert] = _('You cannot access the raw file. Please wait a minute.') - redirect_to project_blob_path(@project, File.join(@ref, @path)) + redirect_to project_blob_path(@project, File.join(@ref, @path)), status: :too_many_requests end def raw_blob_request_limit diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index d4ff72c2314..e04cbf10470 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -361,6 +361,7 @@ class ProjectsController < Projects::ApplicationController :container_registry_enabled, :default_branch, :description, + :emails_disabled, :external_authorization_classification_label, :import_url, :issues_tracker, diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb index 638934694e0..db10515c0b4 100644 --- a/app/controllers/registrations_controller.rb +++ b/app/controllers/registrations_controller.rb @@ -4,6 +4,7 @@ class RegistrationsController < Devise::RegistrationsController include Recaptcha::Verify include AcceptsPendingInvitations include RecaptchaExperimentHelper + include InvisibleCaptcha prepend_before_action :check_captcha, only: :create before_action :whitelist_query_limiting, only: [:destroy] diff --git a/app/finders/remote_mirror_finder.rb b/app/finders/remote_mirror_finder.rb deleted file mode 100644 index 420db0077aa..00000000000 --- a/app/finders/remote_mirror_finder.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -class RemoteMirrorFinder - attr_accessor :params - - def initialize(params) - @params = params - end - - # rubocop: disable CodeReuse/ActiveRecord - def execute - RemoteMirror.find_by(id: params[:id]) - end - # rubocop: enable CodeReuse/ActiveRecord -end diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb index acbcf0ded17..0ab19f1d2d2 100644 --- a/app/helpers/application_settings_helper.rb +++ b/app/helpers/application_settings_helper.rb @@ -270,7 +270,11 @@ module ApplicationSettingsHelper :diff_max_patch_bytes, :commit_email_hostname, :protected_ci_variables, - :local_markdown_version + :local_markdown_version, + :snowplow_collector_hostname, + :snowplow_cookie_domain, + :snowplow_enabled, + :snowplow_site_id ] end diff --git a/app/helpers/sessions_helper.rb b/app/helpers/sessions_helper.rb new file mode 100644 index 00000000000..af98a611b8b --- /dev/null +++ b/app/helpers/sessions_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module SessionsHelper + def unconfirmed_email? + flash[:alert] == t(:unconfirmed, scope: [:devise, :failure]) + end +end diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb index 645160077f5..38142bc68cb 100644 --- a/app/helpers/todos_helper.rb +++ b/app/helpers/todos_helper.rb @@ -26,7 +26,7 @@ module TodosHelper end def todo_target_link(todo) - text = raw("#{todo.target_type.titleize.downcase} ") + + text = raw(todo_target_type_name(todo) + ' ') + if todo.for_commit? content_tag(:span, todo.target_reference, class: 'commit-sha') else @@ -36,23 +36,34 @@ module TodosHelper link_to text, todo_target_path(todo), class: 'has-tooltip', title: todo.target.title end + def todo_target_type_name(todo) + todo.target_type.titleize.downcase + end + def todo_target_path(todo) return unless todo.target.present? - anchor = dom_id(todo.note) if todo.note.present? + path_options = todo_target_path_options(todo) if todo.for_commit? - project_commit_path(todo.project, - todo.target, anchor: anchor) + project_commit_path(todo.project, todo.target, path_options) else path = [todo.parent, todo.target] path.unshift(:pipelines) if todo.build_failed? - polymorphic_path(path, anchor: anchor) + polymorphic_path(path, path_options) end end + def todo_target_path_options(todo) + { anchor: todo_target_path_anchor(todo) } + end + + def todo_target_path_anchor(todo) + dom_id(todo.note) if todo.note.present? + end + def todo_target_state_pill(todo) return unless show_todo_state?(todo) diff --git a/app/helpers/tracking_helper.rb b/app/helpers/tracking_helper.rb index 51ea79d1ddd..221d9692661 100644 --- a/app/helpers/tracking_helper.rb +++ b/app/helpers/tracking_helper.rb @@ -2,6 +2,21 @@ module TrackingHelper def tracking_attrs(label, event, property) - {} # CE has no tracking features + return {} unless tracking_enabled? + + { + data: { + track_label: label, + track_event: event, + track_property: property + } + } + end + + private + + def tracking_enabled? + Rails.env.production? && + ::Gitlab::CurrentSettings.snowplow_enabled? end end diff --git a/app/mailers/emails/remote_mirrors.rb b/app/mailers/emails/remote_mirrors.rb index 2d8137843ec..f3938a052b0 100644 --- a/app/mailers/emails/remote_mirrors.rb +++ b/app/mailers/emails/remote_mirrors.rb @@ -3,7 +3,7 @@ module Emails module RemoteMirrors def remote_mirror_update_failed_email(remote_mirror_id, recipient_id) - @remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute + @remote_mirror = RemoteMirror.find_by_id(remote_mirror_id) @project = @remote_mirror.project mail(to: recipient(recipient_id, @project.group), subject: subject('Remote mirror update failed')) diff --git a/app/models/analytics/cycle_analytics.rb b/app/models/analytics/cycle_analytics.rb new file mode 100644 index 00000000000..626fc91cc41 --- /dev/null +++ b/app/models/analytics/cycle_analytics.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + def self.table_name_prefix + 'analytics_cycle_analytics_' + end + end +end diff --git a/app/models/analytics/cycle_analytics/project_stage.rb b/app/models/analytics/cycle_analytics/project_stage.rb new file mode 100644 index 00000000000..88c8cb40ccb --- /dev/null +++ b/app/models/analytics/cycle_analytics/project_stage.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module Analytics + module CycleAnalytics + class ProjectStage < ApplicationRecord + belongs_to :project + end + end +end diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb index cb6346421ec..2a99c6e5c59 100644 --- a/app/models/application_setting.rb +++ b/app/models/application_setting.rb @@ -99,6 +99,11 @@ class ApplicationSetting < ApplicationRecord presence: true, if: :plantuml_enabled + validates :snowplow_collector_hostname, + presence: true, + hostname: true, + if: :snowplow_enabled + validates :max_attachment_size, presence: true, numericality: { only_integer: true, greater_than: 0 } diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb index b7a4d7aa803..55ac1e129cf 100644 --- a/app/models/application_setting_implementation.rb +++ b/app/models/application_setting_implementation.rb @@ -97,6 +97,10 @@ module ApplicationSettingImplementation usage_stats_set_by_user_id: nil, diff_max_patch_bytes: Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES, commit_email_hostname: default_commit_email_hostname, + snowplow_collector_hostname: nil, + snowplow_cookie_domain: nil, + snowplow_enabled: false, + snowplow_site_id: nil, protected_ci_variables: false, local_markdown_version: 0, outbound_local_requests_whitelist: [], diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index ac88d9714ac..f705e67121f 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -384,7 +384,7 @@ module Ci return unless has_environment? strong_memoize(:expanded_environment_name) do - ExpandVariables.expand(environment, simple_variables) + ExpandVariables.expand(environment, -> { simple_variables }) end end diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb index 2fc1b67dfd2..6bd7473c8ff 100644 --- a/app/models/clusters/applications/cert_manager.rb +++ b/app/models/clusters/applications/cert_manager.rb @@ -64,11 +64,15 @@ module Clusters end def delete_private_key - "kubectl delete secret -n #{Gitlab::Kubernetes::Helm::NAMESPACE} #{private_key_name} --ignore-not-found" if private_key_name.present? + return unless private_key_name.present? + + args = %W(secret -n #{Gitlab::Kubernetes::Helm::NAMESPACE} #{private_key_name} --ignore-not-found) + + Gitlab::Kubernetes::KubectlCmd.delete(*args) end def delete_crd(definition) - "kubectl delete crd #{definition} --ignore-not-found" + Gitlab::Kubernetes::KubectlCmd.delete("crd", definition, "--ignore-not-found") end def cluster_issuer_file diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb index 3a175fec148..455cf200fbc 100644 --- a/app/models/clusters/applications/helm.rb +++ b/app/models/clusters/applications/helm.rb @@ -41,7 +41,7 @@ module Clusters extra_apps = Clusters::Applications::Helm.where('EXISTS (?)', klass.select(1).where(cluster_id: cluster_id)) - applications = applications.present? ? applications.or(extra_apps) : extra_apps + applications = applications ? applications.or(extra_apps) : extra_apps end !applications.exists? diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb index 5eae23659ae..244fe738396 100644 --- a/app/models/clusters/applications/knative.rb +++ b/app/models/clusters/applications/knative.rb @@ -89,7 +89,7 @@ module Clusters def delete_knative_services cluster.kubernetes_namespaces.map do |kubernetes_namespace| - "kubectl delete ksvc --all -n #{kubernetes_namespace.namespace}" + Gitlab::Kubernetes::KubectlCmd.delete("ksvc", "--all", "-n", kubernetes_namespace.namespace) end end @@ -99,14 +99,14 @@ module Clusters def delete_knative_namespaces [ - "kubectl delete --ignore-not-found ns knative-serving", - "kubectl delete --ignore-not-found ns knative-build" + Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "ns", "knative-serving"), + Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "ns", "knative-build") ] end def delete_knative_and_istio_crds api_resources.map do |crd| - "kubectl delete --ignore-not-found crd #{crd}" + Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "crd", "#{crd}") end end @@ -119,13 +119,13 @@ module Clusters def install_knative_metrics return [] unless cluster.application_prometheus_available? - ["kubectl apply -f #{METRICS_CONFIG}"] + [Gitlab::Kubernetes::KubectlCmd.apply_file(METRICS_CONFIG)] end def delete_knative_istio_metrics return [] unless cluster.application_prometheus_available? - ["kubectl delete --ignore-not-found -f #{METRICS_CONFIG}"] + [Gitlab::Kubernetes::KubectlCmd.delete("--ignore-not-found", "-f", METRICS_CONFIG)] end def verify_cluster? diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb index 08e52f32bb3..f31a6b8b50e 100644 --- a/app/models/clusters/applications/prometheus.rb +++ b/app/models/clusters/applications/prometheus.rb @@ -106,13 +106,13 @@ module Clusters def install_knative_metrics return [] unless cluster.application_knative_available? - ["kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] + [Gitlab::Kubernetes::KubectlCmd.apply_file(Clusters::Applications::Knative::METRICS_CONFIG)] end def delete_knative_istio_metrics return [] unless cluster.application_knative_available? - ["kubectl delete -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] + [Gitlab::Kubernetes::KubectlCmd.delete("-f", Clusters::Applications::Knative::METRICS_CONFIG)] end end end diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index a88cac6b8e6..4be4d95b4a1 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -40,8 +40,11 @@ class CommitStatus < ApplicationRecord scope :ordered, -> { order(:name) } scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } + scope :before_stage, -> (index) { where('stage_idx < ?', index) } + scope :for_stage, -> (index) { where(stage_idx: index) } scope :after_stage, -> (index) { where('stage_idx > ?', index) } scope :processables, -> { where(type: %w[Ci::Build Ci::Bridge]) } + scope :for_ids, -> (ids) { where(id: ids) } scope :with_needs, -> (names = nil) do needs = Ci::BuildNeed.scoped_build.select(1) @@ -49,8 +52,10 @@ class CommitStatus < ApplicationRecord where('EXISTS (?)', needs).preload(:needs) end - scope :without_needs, -> do - where('NOT EXISTS (?)', Ci::BuildNeed.scoped_build.select(1)) + scope :without_needs, -> (names = nil) do + needs = Ci::BuildNeed.scoped_build.select(1) + needs = needs.where(name: names) if names + where('NOT EXISTS (?)', needs) end # We use `CommitStatusEnums.failure_reasons` here so that EE can more easily @@ -149,6 +154,18 @@ class CommitStatus < ApplicationRecord end end + def self.names + select(:name) + end + + def self.status_for_prior_stages(index) + before_stage(index).latest.status || 'success' + end + + def self.status_for_names(names) + where(name: names).latest.status || 'success' + end + def locking_enabled? will_save_change_to_status? end diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb index 27a5c3d5286..71ebb586c13 100644 --- a/app/models/concerns/has_status.rb +++ b/app/models/concerns/has_status.rb @@ -106,10 +106,15 @@ module HasStatus scope :running_or_pending, -> { with_status(:running, :pending) } scope :finished, -> { with_status(:success, :failed, :canceled) } scope :failed_or_canceled, -> { with_status(:failed, :canceled) } + scope :incomplete, -> { without_statuses(completed_statuses) } scope :cancelable, -> do where(status: [:running, :preparing, :pending, :created, :scheduled]) end + + scope :without_statuses, -> (names) do + with_status(all_state_names - names.to_a) + end end def started? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 4306dd9266f..bfd636fa62a 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -220,18 +220,7 @@ class MergeRequest < ApplicationRecord end def rebase_in_progress? - (rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid)) || - gitaly_rebase_in_progress? - end - - # TODO: remove the Gitaly lookup after v12.1, when rebase_jid will be reliable - def gitaly_rebase_in_progress? - strong_memoize(:gitaly_rebase_in_progress) do - # The source project can be deleted - next false unless source_project - - source_project.repository.rebase_in_progress?(id) - end + rebase_jid.present? && Gitlab::SidekiqStatus.running?(rebase_jid) end # Use this method whenever you need to make sure the head_pipeline is synced with the diff --git a/app/models/namespace.rb b/app/models/namespace.rb index 058350b16ce..9f9c4288667 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -172,6 +172,13 @@ class Namespace < ApplicationRecord end end + # any ancestor can disable emails for all descendants + def emails_disabled? + strong_memoize(:emails_disabled) do + Feature.enabled?(:emails_disabled, self, default_enabled: true) && self_and_ancestors.where(emails_disabled: true).exists? + end + end + def lfs_enabled? # User namespace will always default to the global setting Gitlab.config.lfs.enabled diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb index a7f73c0f29c..8e44e3d8e17 100644 --- a/app/models/notification_recipient.rb +++ b/app/models/notification_recipient.rb @@ -4,6 +4,7 @@ class NotificationRecipient include Gitlab::Utils::StrongMemoize attr_reader :user, :type, :reason + def initialize(user, type, **opts) unless NotificationSetting.levels.key?(type) || type == :subscription raise ArgumentError, "invalid type: #{type.inspect}" @@ -30,6 +31,7 @@ class NotificationRecipient def notifiable? return false unless has_access? + return false if emails_disabled? return false if own_activity? # even users with :disabled notifications receive manual subscriptions @@ -109,6 +111,12 @@ class NotificationRecipient private + # They are disabled if the project or group has disallowed it. + # No need to check the group if there is already a project + def emails_disabled? + @project ? @project.emails_disabled? : @group&.emails_disabled? + end + def read_ability return if @skip_read_ability return @read_ability if instance_variable_defined?(:@read_ability) diff --git a/app/models/project.rb b/app/models/project.rb index a6e43efa1f3..8efe4b06f87 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -283,6 +283,7 @@ class Project < ApplicationRecord has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :remote_mirrors, inverse_of: :project + has_many :cycle_analytics_stages, class_name: 'Analytics::CycleAnalytics::ProjectStage' accepts_nested_attributes_for :variables, allow_destroy: true accepts_nested_attributes_for :project_feature, update_only: true @@ -631,6 +632,13 @@ class Project < ApplicationRecord alias_method :ancestors, :ancestors_upto + def emails_disabled? + strong_memoize(:emails_disabled) do + # disabling in the namespace overrides the project setting + Feature.enabled?(:emails_disabled, self, default_enabled: true) && (super || namespace.emails_disabled?) + end + end + def lfs_enabled? return namespace.lfs_enabled? if self[:lfs_enabled].nil? @@ -1230,6 +1238,14 @@ class Project < ApplicationRecord end end + def has_active_hooks?(hooks_scope = :push_hooks) + hooks.hooks_for(hooks_scope).any? || SystemHook.hooks_for(hooks_scope).any? + end + + def has_active_services?(hooks_scope = :push_hooks) + services.public_send(hooks_scope).any? # rubocop:disable GitlabSecurity/PublicSend + end + def valid_repo? repository.exists? rescue diff --git a/app/models/project_services/chat_message/pipeline_message.rb b/app/models/project_services/chat_message/pipeline_message.rb index 4edf263433f..a3793d9937b 100644 --- a/app/models/project_services/chat_message/pipeline_message.rb +++ b/app/models/project_services/chat_message/pipeline_message.rb @@ -68,7 +68,7 @@ module ChatMessage title_link: pipeline_url, fields: attachments_fields, footer: project.name, - footer_icon: project.avatar_url, + footer_icon: project.avatar_url(only_path: false), ts: finished_at }] end diff --git a/app/models/project_services/emails_on_push_service.rb b/app/models/project_services/emails_on_push_service.rb index 45de64a9990..8ca40138a8f 100644 --- a/app/models/project_services/emails_on_push_service.rb +++ b/app/models/project_services/emails_on_push_service.rb @@ -24,6 +24,7 @@ class EmailsOnPushService < Service def execute(push_data) return unless supported_events.include?(push_data[:object_kind]) + return if project.emails_disabled? EmailsOnPushWorker.perform_async( project_id, diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb index 5f5cff97808..cb16ad75d14 100644 --- a/app/models/project_services/slash_commands_service.rb +++ b/app/models/project_services/slash_commands_service.rb @@ -35,6 +35,8 @@ class SlashCommandsService < Service chat_user = find_chat_user(params) if chat_user&.user + return Gitlab::SlashCommands::Presenters::Access.new.access_denied unless chat_user.user.can?(:use_slash_commands) + Gitlab::SlashCommands::Command.new(project, chat_user, params).execute else url = authorize_chat_name_url(params) diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb index 6b5605f9999..c9ee0653d86 100644 --- a/app/models/remote_mirror.rb +++ b/app/models/remote_mirror.rb @@ -4,6 +4,8 @@ class RemoteMirror < ApplicationRecord include AfterCommitQueue include MirrorAuthentication + MAX_FIRST_RUNTIME = 3.hours + MAX_INCREMENTAL_RUNTIME = 1.hour PROTECTED_BACKOFF_DELAY = 1.minute UNPROTECTED_BACKOFF_DELAY = 5.minutes @@ -31,11 +33,18 @@ class RemoteMirror < ApplicationRecord scope :enabled, -> { where(enabled: true) } scope :started, -> { with_update_status(:started) } - scope :stuck, -> { started.where('last_update_at < ? OR (last_update_at IS NULL AND updated_at < ?)', 1.hour.ago, 3.hours.ago) } + + scope :stuck, -> do + started + .where('(last_update_started_at < ? AND last_update_at IS NOT NULL)', + MAX_INCREMENTAL_RUNTIME.ago) + .or(where('(last_update_started_at < ? AND last_update_at IS NULL)', + MAX_FIRST_RUNTIME.ago)) + end state_machine :update_status, initial: :none do event :update_start do - transition [:none, :finished, :failed] => :started + transition any => :started end event :update_finish do @@ -46,9 +55,14 @@ class RemoteMirror < ApplicationRecord transition started: :failed end + event :update_retry do + transition started: :to_retry + end + state :started state :finished state :failed + state :to_retry after_transition any => :started do |remote_mirror, _| Gitlab::Metrics.add_event(:remote_mirrors_running) @@ -138,16 +152,27 @@ class RemoteMirror < ApplicationRecord end def updated_since?(timestamp) - last_update_started_at && last_update_started_at > timestamp && !update_failed? + return false if failed? + + last_update_started_at && last_update_started_at > timestamp end def mark_for_delete_if_blank_url mark_for_destruction if url.blank? end - def mark_as_failed(error_message) - update_column(:last_error, Gitlab::UrlSanitizer.sanitize(error_message)) - update_fail + def update_error_message(error_message) + self.last_error = Gitlab::UrlSanitizer.sanitize(error_message) + end + + def mark_for_retry!(error_message) + update_error_message(error_message) + update_retry! + end + + def mark_as_failed!(error_message) + update_error_message(error_message) + update_fail! end def url=(value) @@ -190,6 +215,18 @@ class RemoteMirror < ApplicationRecord update_column(:error_notification_sent, true) end + def backoff_delay + if self.only_protected_branches + PROTECTED_BACKOFF_DELAY + else + UNPROTECTED_BACKOFF_DELAY + end + end + + def max_runtime + last_update_at.present? ? MAX_INCREMENTAL_RUNTIME : MAX_FIRST_RUNTIME + end + private def store_credentials @@ -219,14 +256,6 @@ class RemoteMirror < ApplicationRecord self.last_update_started_at >= Time.now - backoff_delay end - def backoff_delay - if self.only_protected_branches - PROTECTED_BACKOFF_DELAY - else - UNPROTECTED_BACKOFF_DELAY - end - end - def reset_fields update_columns( last_error: nil, diff --git a/app/models/repository.rb b/app/models/repository.rb index a89f573e3d6..9d45a12fa6e 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -418,25 +418,29 @@ class Repository end # Runs code before pushing (= creating or removing) a tag. + # + # Note that this doesn't expire the tags. You may need to call + # expire_caches_for_tags or expire_tags_cache. def before_push_tag + repository_event(:push_tag) + end + + def expire_caches_for_tags expire_statistics_caches expire_emptiness_caches expire_tags_cache - - repository_event(:push_tag) end # Runs code before removing a tag. def before_remove_tag - expire_tags_cache - expire_statistics_caches + expire_caches_for_tags repository_event(:remove_tag) end # Runs code after removing a tag. def after_remove_tag - expire_tags_cache + expire_caches_for_tags end # Runs code after the HEAD of a repository is changed. @@ -460,8 +464,8 @@ class Repository end # Runs code after a new branch has been created. - def after_create_branch - expire_branches_cache + def after_create_branch(expire_cache: true) + expire_branches_cache if expire_cache repository_event(:push_branch) end @@ -474,8 +478,8 @@ class Repository end # Runs code after an existing branch has been removed. - def after_remove_branch - expire_branches_cache + def after_remove_branch(expire_cache: true) + expire_branches_cache if expire_cache end def method_missing(msg, *args, &block) diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 134de1c9ace..311aab0dcd4 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -33,6 +33,7 @@ class GlobalPolicy < BasePolicy enable :access_git enable :receive_notifications enable :use_quick_actions + enable :use_slash_commands end rule { blocked | internal }.policy do @@ -40,6 +41,7 @@ class GlobalPolicy < BasePolicy prevent :access_api prevent :access_git prevent :receive_notifications + prevent :use_slash_commands end rule { required_terms_not_accepted }.policy do @@ -57,6 +59,7 @@ class GlobalPolicy < BasePolicy rule { access_locked }.policy do prevent :log_in + prevent :use_slash_commands end rule { ~(anonymous & restricted_public_level) }.policy do diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb index 52c944491bf..c686e7763bb 100644 --- a/app/policies/group_policy.rb +++ b/app/policies/group_policy.rb @@ -92,6 +92,7 @@ class GroupPolicy < BasePolicy enable :change_visibility_level enable :set_note_created_at + enable :set_emails_disabled end rule { can?(:read_nested_project_resources) }.policy do diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb index e79bac6bee3..b8dee1b0789 100644 --- a/app/policies/project_policy.rb +++ b/app/policies/project_policy.rb @@ -162,6 +162,7 @@ class ProjectPolicy < BasePolicy enable :set_issue_created_at enable :set_issue_updated_at enable :set_note_created_at + enable :set_emails_disabled end rule { can?(:guest_access) }.policy do diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index 99d4ff9ecd1..f4bd457ebc6 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -42,14 +42,19 @@ module Ci return false unless trigger_build_ids.present? return false unless Feature.enabled?(:ci_dag_support, project) - # rubocop: disable CodeReuse/ActiveRecord - trigger_build_names = pipeline.statuses - .where(id: trigger_build_ids) - .select(:name) - # rubocop: enable CodeReuse/ActiveRecord + # we find processables that are dependent: + # 1. because of current dependency, + trigger_build_names = pipeline.processables.latest + .for_ids(trigger_build_ids).names + # 2. does not have builds that not yet complete + incomplete_build_names = pipeline.processables.latest + .incomplete.names + + # Each found processable is guaranteed here to have completed status created_processables .with_needs(trigger_build_names) + .without_needs(incomplete_build_names) .find_each .map(&method(:process_build_with_needs)) .any? @@ -70,17 +75,13 @@ module Ci end end - # rubocop: disable CodeReuse/ActiveRecord def status_for_prior_stages(index) - pipeline.builds.where('stage_idx < ?', index).latest.status || 'success' + pipeline.processables.status_for_prior_stages(index) end - # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def status_for_build_needs(needs) - pipeline.builds.where(name: needs).latest.status || 'success' + pipeline.processables.status_for_names(needs) end - # rubocop: enable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord def stage_indexes_of_created_processables_without_needs @@ -89,12 +90,10 @@ module Ci end # rubocop: enable CodeReuse/ActiveRecord - # rubocop: disable CodeReuse/ActiveRecord def created_processables_in_stage_without_needs(index) created_processables_without_needs - .where(stage_idx: index) + .for_stage(index) end - # rubocop: enable CodeReuse/ActiveRecord def created_processables_without_needs if Feature.enabled?(:ci_dag_support, project) diff --git a/app/services/git/base_hooks_service.rb b/app/services/git/base_hooks_service.rb index d30df34e54b..1db18fcf401 100644 --- a/app/services/git/base_hooks_service.rb +++ b/app/services/git/base_hooks_service.rb @@ -19,7 +19,7 @@ module Git update_remote_mirrors - push_data + success end private @@ -33,7 +33,7 @@ module Git end def limited_commits - commits.last(PROCESS_COMMIT_LIMIT) + @limited_commits ||= commits.last(PROCESS_COMMIT_LIMIT) end def commits_count @@ -48,21 +48,25 @@ module Git [] end + # Push events in the activity feed only show information for the + # last commit. def create_events - EventCreateService.new.push(project, current_user, push_data) + EventCreateService.new.push(project, current_user, event_push_data) end def create_pipelines return unless params.fetch(:create_pipelines, true) Ci::CreatePipelineService - .new(project, current_user, push_data) + .new(project, current_user, base_params) .execute(:push, pipeline_options) end def execute_project_hooks - project.execute_hooks(push_data, hook_name) - project.execute_services(push_data, hook_name) + # Creating push_data invokes one CommitDelta RPC per commit. Only + # build this data if we actually need it. + project.execute_hooks(push_data, hook_name) if project.has_active_hooks?(hook_name) + project.execute_services(push_data, hook_name) if project.has_active_services?(hook_name) end def enqueue_invalidate_cache @@ -73,18 +77,35 @@ module Git ) end - def push_data - @push_data ||= Gitlab::DataBuilder::Push.build( - project: project, - user: current_user, + def base_params + { oldrev: params[:oldrev], newrev: params[:newrev], ref: params[:ref], - commits: limited_commits, + push_options: params[:push_options] || {} + } + end + + def push_data_params(commits:, with_changed_files: true) + base_params.merge( + project: project, + user: current_user, + commits: commits, message: event_message, commits_count: commits_count, - push_options: params[:push_options] || {} + with_changed_files: with_changed_files ) + end + + def event_push_data + # We only need the last commit for the event push, and we don't + # need the full deltas either. + @event_push_data ||= Gitlab::DataBuilder::Push.build( + push_data_params(commits: commits.last, with_changed_files: false)) + end + + def push_data + @push_data ||= Gitlab::DataBuilder::Push.build(push_data_params(commits: limited_commits)) # Dependent code may modify the push data, so return a duplicate each time @push_data.dup diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb index c41f445c3c4..431a5aedf2e 100644 --- a/app/services/git/branch_hooks_service.rb +++ b/app/services/git/branch_hooks_service.rb @@ -63,7 +63,7 @@ module Git end def branch_create_hooks - project.repository.after_create_branch + project.repository.after_create_branch(expire_cache: false) project.after_create_default_branch if default_branch? end @@ -78,7 +78,7 @@ module Git end def branch_remove_hooks - project.repository.after_remove_branch + project.repository.after_remove_branch(expire_cache: false) end # Schedules processing of commit messages diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb index 73e1e00dc33..116756bacfe 100644 --- a/app/services/groups/update_service.rb +++ b/app/services/groups/update_service.rb @@ -46,6 +46,11 @@ module Groups params.delete(:parent_id) end + # overridden in EE + def remove_unallowed_params + params.delete(:emails_disabled) unless can?(current_user, :set_emails_disabled, group) + end + def valid_share_with_group_lock_change? return true unless changing_share_with_group_lock? return true if can?(current_user, :change_share_with_group_lock, group) diff --git a/app/services/merge_requests/rebase_service.rb b/app/services/merge_requests/rebase_service.rb index 8d3b9b05819..27c16ba1777 100644 --- a/app/services/merge_requests/rebase_service.rb +++ b/app/services/merge_requests/rebase_service.rb @@ -15,7 +15,8 @@ module MergeRequests end def rebase - if merge_request.gitaly_rebase_in_progress? + # Ensure Gitaly isn't already running a rebase + if source_project.repository.rebase_in_progress?(merge_request.id) log_error('Rebase task canceled: Another rebase is already in progress', save_message_on_model: true) return false end diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb index 21fab22e0d4..83710ffce2f 100644 --- a/app/services/notification_service.rb +++ b/app/services/notification_service.rb @@ -321,6 +321,9 @@ class NotificationService end def decline_project_invite(project_member) + # Must always send, regardless of project/namespace configuration since it's a + # response to the user's action. + mailer.member_invite_declined_email( project_member.real_source_type, project_member.project.id, @@ -351,8 +354,8 @@ class NotificationService end def decline_group_invite(group_member) - # always send this one, since it's a response to the user's own - # action + # Must always send, regardless of project/namespace configuration since it's a + # response to the user's action. mailer.member_invite_declined_email( group_member.real_source_type, @@ -410,6 +413,10 @@ class NotificationService end def pipeline_finished(pipeline, recipients = nil) + # Must always check project configuration since recipients could be a list of emails + # from the PipelinesEmailService integration. + return if pipeline.project.emails_disabled? + email_template = "pipeline_#{pipeline.status}_email" return unless mailer.respond_to?(email_template) @@ -428,6 +435,8 @@ class NotificationService end def autodevops_disabled(pipeline, recipients) + return if pipeline.project.emails_disabled? + recipients.each do |recipient| mailer.autodevops_disabled_email(pipeline, recipient).deliver_later end @@ -472,10 +481,14 @@ class NotificationService end def repository_cleanup_success(project, user) + return if project.emails_disabled? + mailer.send(:repository_cleanup_success_email, project, user).deliver_later end def repository_cleanup_failure(project, user, error) + return if project.emails_disabled? + mailer.send(:repository_cleanup_failure_email, project, user, error).deliver_later end diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb index 1244a0f72a7..13a467a3ef9 100644 --- a/app/services/projects/update_remote_mirror_service.rb +++ b/app/services/projects/update_remote_mirror_service.rb @@ -2,31 +2,52 @@ module Projects class UpdateRemoteMirrorService < BaseService - attr_reader :errors + MAX_TRIES = 3 - def execute(remote_mirror) + def execute(remote_mirror, tries) return success unless remote_mirror.enabled? - errors = [] + update_mirror(remote_mirror) - begin - remote_mirror.ensure_remote! - repository.fetch_remote(remote_mirror.remote_name, ssh_auth: remote_mirror, no_tags: true) + success + rescue Gitlab::Git::CommandError => e + # This happens if one of the gitaly calls above fail, for example when + # branches have diverged, or the pre-receive hook fails. + retry_or_fail(remote_mirror, e.message, tries) - opts = {} - if remote_mirror.only_protected_branches? - opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name) - end + error(e.message) + rescue => e + remote_mirror.mark_as_failed!(e.message) + raise e + end + + private + + def update_mirror(remote_mirror) + remote_mirror.update_start! + + remote_mirror.ensure_remote! + repository.fetch_remote(remote_mirror.remote_name, ssh_auth: remote_mirror, no_tags: true) - remote_mirror.update_repository(opts) - rescue => e - errors << e.message.strip + opts = {} + if remote_mirror.only_protected_branches? + opts[:only_branches_matching] = project.protected_branches.select(:name).map(&:name) end - if errors.present? - error(errors.join("\n\n")) + remote_mirror.update_repository(opts) + + remote_mirror.update_finish! + end + + def retry_or_fail(mirror, message, tries) + if tries < MAX_TRIES + mirror.mark_for_retry!(message) else - success + # It's not likely we'll be able to recover from this ourselves, so we'll + # notify the users of the problem, and don't trigger any sidekiq retries + # Instead, we'll wait for the next change to try the push again, or until + # a user manually retries. + mirror.mark_as_failed!(message) end end end diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb index caab946174d..8acbdc7e02b 100644 --- a/app/services/projects/update_service.rb +++ b/app/services/projects/update_service.rb @@ -9,6 +9,7 @@ module Projects # rubocop: disable CodeReuse/ActiveRecord def execute + remove_unallowed_params validate! ensure_wiki_exists if enabling_wiki? @@ -54,6 +55,10 @@ module Projects end end + def remove_unallowed_params + params.delete(:emails_disabled) unless can?(current_user, :set_emails_disabled, project) + end + def after_update todos_features_changes = %w( issues_access_level diff --git a/app/services/update_deployment_service.rb b/app/services/update_deployment_service.rb index 49a7d0178f4..dcafebae52d 100644 --- a/app/services/update_deployment_service.rb +++ b/app/services/update_deployment_service.rb @@ -42,7 +42,7 @@ class UpdateDeploymentService return unless environment_url @expanded_environment_url = - ExpandVariables.expand(environment_url, variables) + ExpandVariables.expand(environment_url, -> { variables }) end def environment_url diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml new file mode 100644 index 00000000000..b60b5d55a1b --- /dev/null +++ b/app/views/admin/application_settings/_snowplow.html.haml @@ -0,0 +1,30 @@ +- expanded = true if !@application_setting.valid? && @application_setting.errors.any? { |k| k.to_s.start_with?('snowplow_') } +%section.settings.as-snowplow.no-animate#js-snowplow-settings{ class: ('expanded' if expanded) } + .settings-header + %h4 + = _('Snowplow') + %button.btn.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') + %p + = _('Configure the %{link} integration.').html_safe % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank') } + .settings-content + + = form_for @application_setting, url: integrations_admin_application_settings_path, html: { class: 'fieldset-form' } do |f| + = form_errors(@application_setting) + + %fieldset + .form-group + .form-check + = f.check_box :snowplow_enabled, class: 'form-check-input' + = f.label :snowplow_enabled, _('Enable snowplow tracking'), class: 'form-check-label' + .form-group + = f.label :snowplow_collector_hostname, _('Collector hostname'), class: 'label-light' + = f.text_field :snowplow_collector_hostname, class: 'form-control', placeholder: 'snowplow.example.com' + .form-group + = f.label :snowplow_site_id, _('Site ID'), class: 'label-light' + = f.text_field :snowplow_site_id, class: 'form-control' + .form-group + = f.label :snowplow_cookie_domain, _('Cookie domain'), class: 'label-light' + = f.text_field :snowplow_cookie_domain, class: 'form-control' + + = f.submit _('Save changes'), class: 'btn btn-success' diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml index 2f10f08c839..0b1d3d1ddb3 100644 --- a/app/views/devise/sessions/_new_base.html.haml +++ b/app/views/devise/sessions/_new_base.html.haml @@ -1,20 +1,23 @@ = form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user gl-show-field-errors', 'aria-live' => 'assertive'}) do |f| .form-group - = f.label "Username or email", for: "user_login", class: 'label-bold' - = f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required.", data: { qa_selector: 'login_field' } + = f.label _('Username or email'), for: 'user_login', class: 'label-bold' + = f.text_field :login, class: 'form-control top', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', required: true, title: _('This field is required.'), data: { qa_selector: 'login_field' } .form-group = f.label :password, class: 'label-bold' - = f.password_field :password, class: "form-control bottom", required: true, title: "This field is required.", data: { qa_selector: 'password_field' } + = f.password_field :password, class: 'form-control bottom', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' } - if devise_mapping.rememberable? .remember-me - %label{ for: "user_remember_me" } + %label{ for: 'user_remember_me' } = f.check_box :remember_me, class: 'remember-me-checkbox' %span Remember me - .float-right.forgot-password - = link_to "Forgot your password?", new_password_path(:user) + .float-right + - if unconfirmed_email? + = link_to _('Resend confirmation email'), new_user_confirmation_path + - else + = link_to _('Forgot your password?'), new_password_path(:user) %div - if captcha_enabled? = recaptcha_tags .submit-container.move-submit-down - = f.submit "Sign in", class: "btn btn-success", data: { qa_selector: 'sign_in_button' } + = f.submit _('Sign in'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' } diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml index 074edf645ba..2cd77af6877 100644 --- a/app/views/devise/shared/_signup_box.html.haml +++ b/app/views/devise/shared/_signup_box.html.haml @@ -5,6 +5,8 @@ = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f| .devise-errors = render "devise/shared/error_messages", resource: resource + - if Feature.enabled?(:invisible_captcha) + = invisible_captcha .name.form-group = f.label :name, _('Full name'), class: 'label-bold' = f.text_field :name, class: "form-control top js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length }, :qa_selector => 'new_user_name_field' }, required: true, title: _("This field is required.") diff --git a/app/views/layouts/_snowplow.html.haml b/app/views/layouts/_snowplow.html.haml new file mode 100644 index 00000000000..5f5c5e984c5 --- /dev/null +++ b/app/views/layouts/_snowplow.html.haml @@ -0,0 +1,29 @@ +- return unless Gitlab::CurrentSettings.snowplow_enabled? + += javascript_tag nonce: true do + :plain + ;(function(p,l,o,w,i,n,g){if(!p[i]){p.GlobalSnowplowNamespace=p.GlobalSnowplowNamespace||[]; + p.GlobalSnowplowNamespace.push(i);p[i]=function(){(p[i].q=p[i].q||[]).push(arguments) + };p[i].q=p[i].q||[];n=l.createElement(o);g=l.getElementsByTagName(o)[0];n.async=1; + n.src=w;g.parentNode.insertBefore(n,g)}}(window,document,"script","#{asset_url('snowplow/sp.js')}","snowplow")); + + window.snowplow('newTracker', '#{Gitlab::SnowplowTracker::NAMESPACE}', '#{Gitlab::CurrentSettings.snowplow_collector_hostname}', { + appId: '#{Gitlab::CurrentSettings.snowplow_site_id}', + cookieDomain: '#{Gitlab::CurrentSettings.snowplow_cookie_domain}', + userFingerprint: false, + respectDoNotTrack: true, + forceSecureTracker: true, + post: true, + contexts: { webPage: true }, + stateStorageStrategy: "localStorage" + }); + + window.snowplow('enableActivityTracking', 30, 30); + window.snowplow('trackPageView'); + +- return unless Feature.enabled?(:additional_snowplow_tracking, @group) + += javascript_tag nonce: true do + :plain + window.snowplow('enableFormTracking'); + window.snowplow('enableLinkClickTracking'); diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml index d16e2dddbe0..d99063e344f 100644 --- a/app/views/profiles/preferences/show.html.haml +++ b/app/views/profiles/preferences/show.html.haml @@ -45,20 +45,20 @@ .form-group = f.label :layout, class: 'label-bold' do = s_('Preferences|Layout width') - = f.select :layout, layout_choices, {}, class: 'form-control' + = f.select :layout, layout_choices, {}, class: 'select2' .form-text.text-muted = s_('Preferences|Choose between fixed (max. 1280px) and fluid (100%%) application layout.') .form-group = f.label :dashboard, class: 'label-bold' do = s_('Preferences|Default dashboard') - = f.select :dashboard, dashboard_choices, {}, class: 'form-control' + = f.select :dashboard, dashboard_choices, {}, class: 'select2' = render_if_exists 'profiles/preferences/group_overview_selector', f: f # EE-specific .form-group = f.label :project_view, class: 'label-bold' do = s_('Preferences|Project overview content') - = f.select :project_view, project_view_choices, {}, class: 'form-control' + = f.select :project_view, project_view_choices, {}, class: 'select2' .form-text.text-muted = s_('Preferences|Choose what content you want to see on a project’s overview page.') @@ -82,7 +82,7 @@ .form-group = f.label :first_day_of_week, class: 'label-bold' do = _('First day of the week') - = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control' + = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'select2' - if Feature.enabled?(:user_time_settings) .col-sm-12 %hr diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml index 280ec6d715b..eb100e5cf47 100644 --- a/app/views/projects/mirrors/_mirror_repos.html.haml +++ b/app/views/projects/mirrors/_mirror_repos.html.haml @@ -43,7 +43,8 @@ = _('Mirrored repositories') = render_if_exists 'projects/mirrors/mirrored_repositories_count' %th= _('Direction') - %th= _('Last update') + %th= _('Last update attempt') + %th= _('Last successful update') %th %th %tbody.js-mirrors-table-body @@ -53,6 +54,8 @@ %tr.qa-mirrored-repository-row.rspec-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) } %td.qa-mirror-repository-url= mirror.safe_url %td= _('Push') + %td + = mirror.last_update_started_at.present? ? time_ago_with_tooltip(mirror.last_update_started_at) : _('Never') %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never') %td - if mirror.disabled? diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml index efabb7f7b19..149b0d6cddd 100644 --- a/app/views/projects/project_members/_new_project_member.html.haml +++ b/app/views/projects/project_members/_new_project_member.html.haml @@ -2,7 +2,7 @@ .col-sm-12 = form_for @project_member, as: :project_member, url: project_project_members_path(@project), html: { class: 'users-project-form' } do |f| .form-group - = label_tag :user_ids, _("Select members to invite"), class: "label-bold" + = label_tag :user_ids, _("GitLab member or Email address"), class: "label-bold" = users_select_tag(:user_ids, multiple: true, class: "input-clamp qa-member-select-input", scope: :all, email_user: true, placeholder: "Search for members to update or invite") .form-group = label_tag :access_level, _("Choose a role permission"), class: "label-bold" diff --git a/app/views/projects/update.js.haml b/app/views/projects/update.js.haml index 70f1bf8ef46..c5eecc900b2 100644 --- a/app/views/projects/update.js.haml +++ b/app/views/projects/update.js.haml @@ -4,7 +4,7 @@ location.reload(); - else :plain - $(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); + $(".flash-container").html("#{escape_javascript(render('errors'))}"); $('.save-project-loader').hide(); $('.project-edit-container').show(); $('.edit-project .js-btn-success-general-project-settings').enable(); diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml index 66a614b0197..858731b2dda 100644 --- a/app/views/projects/wikis/_form.html.haml +++ b/app/views/projects/wikis/_form.html.haml @@ -14,7 +14,7 @@ .col-sm-12 = f.text_field :title, class: 'form-control qa-wiki-title-textbox', value: @page.title - if @page.persisted? - %span.edit-wiki-page-slug-tip + %span.d-inline-block.mw-100.prepend-top-5 = icon('lightbulb-o') = s_("WikiEditPageTip|Tip: You can move this page by adding the path to the beginning of the title.") = link_to icon('question-circle'), help_page_path('user/project/wiki/index', anchor: 'moving-a-wiki-page'), target: '_blank' diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml index dc12e368b35..2c675c0de9c 100644 --- a/app/views/projects/wikis/_new.html.haml +++ b/app/views/projects/wikis/_new.html.haml @@ -11,7 +11,7 @@ = label_tag :new_wiki_path do %span= s_("WikiPage|Page slug") = text_field_tag :new_wiki_path, nil, placeholder: s_("WikiNewPagePlaceholder|how-to-setup"), class: 'form-control', required: true, :'data-wikis-path' => project_wikis_path(@project), autofocus: true - %span.new-wiki-page-slug-tip + %span.d-inline-block.mw-100.prepend-top-5 = icon('lightbulb-o') = s_("WikiNewPageTip|Tip: You can specify the full path for the new file. We will automatically create any missing directories.") .form-actions diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml index 1277ea6c743..e8b59a3b8c4 100644 --- a/app/views/projects/wikis/edit.html.haml +++ b/app/views/projects/wikis/edit.html.haml @@ -5,7 +5,7 @@ = wiki_page_errors(@error) -.wiki-page-header.has-sidebar-toggle +.wiki-page-header.top-area.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml index 8c2cbd495a0..009133be117 100644 --- a/app/views/projects/wikis/git_access.html.haml +++ b/app/views/projects/wikis/git_access.html.haml @@ -1,7 +1,7 @@ - @content_class = "limit-container-width" unless fluid_layout - page_title s_("WikiClone|Git Access"), _("Wiki") -.wiki-page-header.has-sidebar-toggle +.wiki-page-header.top-area.has-sidebar-toggle %button.btn.btn-default.d-block.d-sm-block.d-md-none.float-right.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml index c5fbeeafa54..f8468ef9a78 100644 --- a/app/views/projects/wikis/history.html.haml +++ b/app/views/projects/wikis/history.html.haml @@ -1,6 +1,6 @@ - page_title _("History"), @page.human_title, _("Wiki") -.wiki-page-header.has-sidebar-toggle +.wiki-page-header.top-area.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml index 2191e5ab287..f7999c3f1bd 100644 --- a/app/views/projects/wikis/pages.html.haml +++ b/app/views/projects/wikis/pages.html.haml @@ -5,7 +5,7 @@ - sort_title = wiki_sort_title(params[:sort]) %div{ class: container_class } - .wiki-page-header + .wiki-page-header.top-area .nav-text.flex-fill %h2.wiki-page-title diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml index 95cd3356ec8..1d649886331 100644 --- a/app/views/projects/wikis/show.html.haml +++ b/app/views/projects/wikis/show.html.haml @@ -4,7 +4,7 @@ - page_title @page.human_title, _("Wiki") - add_to_breadcrumbs _("Wiki"), project_wiki_path(@project, :home) -.wiki-page-header.has-sidebar-toggle +.wiki-page-header.top-area.has-sidebar-toggle %button.btn.btn-default.sidebar-toggle.js-sidebar-wiki-toggle{ role: "button", type: "button" } = icon('angle-double-left') diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 400becdd023..991a177018e 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -88,7 +88,6 @@ - pipeline_processing:ci_build_prepare - pipeline_processing:build_queue - pipeline_processing:build_success -- pipeline_processing:build_process - pipeline_processing:pipeline_process - pipeline_processing:pipeline_success - pipeline_processing:pipeline_update diff --git a/app/workers/build_process_worker.rb b/app/workers/build_process_worker.rb deleted file mode 100644 index 9cd9519df1f..00000000000 --- a/app/workers/build_process_worker.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -class BuildProcessWorker - include ApplicationWorker - include PipelineQueue - - queue_namespace :pipeline_processing - - # rubocop: disable CodeReuse/ActiveRecord - def perform(build_id) - CommitStatus.find_by(id: build_id).try do |build| - build.pipeline.process!([build_id]) - end - end - # rubocop: enable CodeReuse/ActiveRecord -end diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb index dba7837bd12..622bd6f1f48 100644 --- a/app/workers/post_receive.rb +++ b/app/workers/post_receive.rb @@ -42,6 +42,11 @@ class PostReceive user = identify_user(post_received) return false unless user + # Expire the branches cache so we have updated data for this push + post_received.project.repository.expire_branches_cache if post_received.includes_branches? + # We only need to expire tags once per push + post_received.project.repository.expire_caches_for_tags if post_received.includes_tags? + post_received.enum_for(:changes_refs).with_index do |(oldrev, newrev, ref), index| service_klass = if Gitlab::Git.tag_ref?(ref) @@ -72,6 +77,7 @@ class PostReceive def after_project_changes_hooks(post_received, user, refs, changes) hook_data = Gitlab::DataBuilder::Repository.update(post_received.project, user, changes, refs) SystemHooksService.new.execute_hooks(hook_data, :repository_update_hooks) + Gitlab::UsageDataCounters::SourceCodeCounter.count(:pushes) end def process_wiki_changes(post_received) diff --git a/app/workers/remote_mirror_notification_worker.rb b/app/workers/remote_mirror_notification_worker.rb index 5bafe8e2046..368abfeda99 100644 --- a/app/workers/remote_mirror_notification_worker.rb +++ b/app/workers/remote_mirror_notification_worker.rb @@ -4,7 +4,7 @@ class RemoteMirrorNotificationWorker include ApplicationWorker def perform(remote_mirror_id) - remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute + remote_mirror = RemoteMirror.find_by_id(remote_mirror_id) # We check again if there's an error because a newer run since this job was # fired could've completed successfully. diff --git a/app/workers/repository_update_remote_mirror_worker.rb b/app/workers/repository_update_remote_mirror_worker.rb index 03a7ff2cd7a..d13c7641eb3 100644 --- a/app/workers/repository_update_remote_mirror_worker.rb +++ b/app/workers/repository_update_remote_mirror_worker.rb @@ -1,50 +1,53 @@ # frozen_string_literal: true class RepositoryUpdateRemoteMirrorWorker - UpdateAlreadyInProgressError = Class.new(StandardError) UpdateError = Class.new(StandardError) include ApplicationWorker + include Gitlab::ExclusiveLeaseHelpers sidekiq_options retry: 3, dead: false - sidekiq_retry_in { |count| 30 * count } + LOCK_WAIT_TIME = 30.seconds + MAX_TRIES = 3 - sidekiq_retries_exhausted do |msg, _| - Sidekiq.logger.warn "Failed #{msg['class']} with #{msg['args']}: #{msg['error_message']}" - end - - def perform(remote_mirror_id, scheduled_time) - remote_mirror = RemoteMirrorFinder.new(id: remote_mirror_id).execute + def perform(remote_mirror_id, scheduled_time, tries = 0) + remote_mirror = RemoteMirror.find_by_id(remote_mirror_id) + return unless remote_mirror return if remote_mirror.updated_since?(scheduled_time) - raise UpdateAlreadyInProgressError if remote_mirror.update_in_progress? + # If the update is already running, wait for it to finish before running again + # This will wait for a total of 90 seconds in 3 steps + in_lock(remote_mirror_update_lock(remote_mirror.id), + retries: 3, + ttl: remote_mirror.max_runtime, + sleep_sec: LOCK_WAIT_TIME) do + update_mirror(remote_mirror, scheduled_time, tries) + end + rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError + # If an update runs longer than 1.5 minutes, we'll reschedule it + # with a backoff. The next run will check if the previous update would + # include the changes that triggered this update and become a no-op. + self.class.perform_in(remote_mirror.backoff_delay, remote_mirror.id, scheduled_time, tries) + end - remote_mirror.update_start + private - project = remote_mirror.project + def update_mirror(mirror, scheduled_time, tries) + project = mirror.project current_user = project.creator - result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(remote_mirror) - raise UpdateError, result[:message] if result[:status] == :error - - remote_mirror.update_finish - rescue UpdateAlreadyInProgressError - raise - rescue UpdateError => ex - fail_remote_mirror(remote_mirror, ex.message) - raise - rescue => ex - return unless remote_mirror + result = Projects::UpdateRemoteMirrorService.new(project, current_user).execute(mirror, tries) - fail_remote_mirror(remote_mirror, ex.message) - raise UpdateError, "#{ex.class}: #{ex.message}" + if result[:status] == :error && mirror.to_retry? + schedule_retry(mirror, scheduled_time, tries) + end end - private - - def fail_remote_mirror(remote_mirror, message) - remote_mirror.mark_as_failed(message) + def remote_mirror_update_lock(mirror_id) + [self.class.name, mirror_id].join(':') + end - Rails.logger.error(message) # rubocop:disable Gitlab/RailsLogger + def schedule_retry(mirror, scheduled_time, tries) + self.class.perform_in(mirror.backoff_delay, mirror.id, scheduled_time, tries + 1) end end diff --git a/bin/changelog b/bin/changelog index 328d9495b96..ec068d06507 100755 --- a/bin/changelog +++ b/bin/changelog @@ -23,7 +23,7 @@ module ChangelogHelpers Abort = Class.new(StandardError) Done = Class.new(StandardError) - MAX_FILENAME_LENGTH = 140 # ecryptfs has a limit of 140 characters + MAX_FILENAME_LENGTH = 99 # GNU tar has a 99 character limit def capture_stdout(cmd) output = IO.popen(cmd, &:read) diff --git a/changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml b/changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml new file mode 100644 index 00000000000..f249eff572c --- /dev/null +++ b/changelogs/unreleased/10-adjust-copy-for-adding-additional-members.yml @@ -0,0 +1,5 @@ +--- +title: Adjust copy for adding additional members +merge_request: 31726 +author: +type: changed diff --git a/changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml b/changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml new file mode 100644 index 00000000000..ccfd929b6ba --- /dev/null +++ b/changelogs/unreleased/12502-add-view-stats-to-cycle-analytics.yml @@ -0,0 +1,5 @@ +--- +title: Track page views for cycle analytics show page +merge_request: 31717 +author: +type: added diff --git a/changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml b/changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml new file mode 100644 index 00000000000..f0cc7fe9b6d --- /dev/null +++ b/changelogs/unreleased/34414-update-personal-access-token-scope-descriptions-to-reflect-registry-permissions.yml @@ -0,0 +1,6 @@ +--- +title: Updated the personal access token api scope description to reflect the permissions + it grants +merge_request: 31759 +author: +type: other diff --git a/changelogs/unreleased/44036-someone-edited-the-issue-at-the-same-time-is-regularly-seen-despite-that-being-a-filthy-lie.yml b/changelogs/unreleased/44036-fix-someone-edited-the-issue-at-the-same-time-false-warning.yml index 674d53286e6..674d53286e6 100644 --- a/changelogs/unreleased/44036-someone-edited-the-issue-at-the-same-time-is-regularly-seen-despite-that-being-a-filthy-lie.yml +++ b/changelogs/unreleased/44036-fix-someone-edited-the-issue-at-the-same-time-false-warning.yml diff --git a/changelogs/unreleased/50020-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml b/changelogs/unreleased/50020-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml new file mode 100644 index 00000000000..9137e9339aa --- /dev/null +++ b/changelogs/unreleased/50020-allow-email-notifications-to-be-disabled-for-all-users-of-a-group.yml @@ -0,0 +1,5 @@ +--- +title: Allow email notifications to be disabled for all members of a group or project +merge_request: 30755 +author: Dustin Spicuzza +type: added diff --git a/changelogs/unreleased/59829-fix-style-lint-wiki.yml b/changelogs/unreleased/59829-fix-style-lint-wiki.yml new file mode 100644 index 00000000000..48242a77c6b --- /dev/null +++ b/changelogs/unreleased/59829-fix-style-lint-wiki.yml @@ -0,0 +1,5 @@ +--- +title: Fix the style-lint errors and warnings for `app/assets/stylesheets/pages/wiki.scss` +merge_request: 31656 +author: +type: other diff --git a/changelogs/unreleased/61787-the-colour-selector-for-broadcast-messages-should-provide-a-few-default-options-with-descriptive-labels-like.yml b/changelogs/unreleased/61787-broadcast-messages-colour-selector-provide-default-options-with-descriptive-labels.yml index ec6e9c5aff8..ec6e9c5aff8 100644 --- a/changelogs/unreleased/61787-the-colour-selector-for-broadcast-messages-should-provide-a-few-default-options-with-descriptive-labels-like.yml +++ b/changelogs/unreleased/61787-broadcast-messages-colour-selector-provide-default-options-with-descriptive-labels.yml diff --git a/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml b/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml new file mode 100644 index 00000000000..10f2b7eaed5 --- /dev/null +++ b/changelogs/unreleased/62286-Consistent-selection-elements-in-user-settings-preferences.yml @@ -0,0 +1,5 @@ +--- +title: Harmonize selections in user settings +merge_request: 31110 +author: Marc Schwede +type: other diff --git a/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml b/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml new file mode 100644 index 00000000000..21771c76873 --- /dev/null +++ b/changelogs/unreleased/64950-move-download-csv-button-functionality-in-metrics-dashboard-cards-i.yml @@ -0,0 +1,5 @@ +--- +title: 'feat: adds a download to csv functionality to the dropdown in prometheus metrics' +merge_request: 31679 +author: +type: changed diff --git a/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml b/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml new file mode 100644 index 00000000000..a5f62dbcd56 --- /dev/null +++ b/changelogs/unreleased/65483-add-a-resend-confirmation-link.yml @@ -0,0 +1,5 @@ +--- +title: Allow users to resend a confirmation link when the grace period has expired +merge_request: 31476 +author: +type: changed diff --git a/changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml b/changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml new file mode 100644 index 00000000000..217db8aa05a --- /dev/null +++ b/changelogs/unreleased/65803-invalidate-branches-cache-on-refresh.yml @@ -0,0 +1,5 @@ +--- +title: Invalidate branches cache on PostReceive +merge_request: 31653 +author: +type: fixed diff --git a/changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml b/changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml new file mode 100644 index 00000000000..df0ac649ac1 --- /dev/null +++ b/changelogs/unreleased/66008-fix-project-image-in-slack-notifications.yml @@ -0,0 +1,5 @@ +--- +title: Fix project avatar image in Slack pipeline notifications +merge_request: 31788 +author: +type: fixed diff --git a/changelogs/unreleased/bump_helm_kubectl_gitlab.yml b/changelogs/unreleased/bump_helm_kubectl_gitlab.yml new file mode 100644 index 00000000000..d768462e130 --- /dev/null +++ b/changelogs/unreleased/bump_helm_kubectl_gitlab.yml @@ -0,0 +1,5 @@ +--- +title: Bump Helm to 2.14.3 and kubectl to 1.11.10 for Kubernetes integration +merge_request: 31716 +author: +type: other diff --git a/changelogs/unreleased/bvl-remote-mirror-exception-handling.yml b/changelogs/unreleased/bvl-remote-mirror-exception-handling.yml new file mode 100644 index 00000000000..962376086b0 --- /dev/null +++ b/changelogs/unreleased/bvl-remote-mirror-exception-handling.yml @@ -0,0 +1,6 @@ +--- +title: Retry push mirrors faster when running concurrently, improve error handling + when push mirrors fail +merge_request: 31247 +author: +type: changed diff --git a/changelogs/unreleased/dblessing-fix-public-project-ssh-only-ci-failure.yml b/changelogs/unreleased/dblessing-fix-public-project-ssh-only-ci-failure.yml new file mode 100644 index 00000000000..615a1571e95 --- /dev/null +++ b/changelogs/unreleased/dblessing-fix-public-project-ssh-only-ci-failure.yml @@ -0,0 +1,5 @@ +--- +title: Allow CI to clone public projects when HTTP protocol is disabled +merge_request: 31632 +author: +type: fixed diff --git a/changelogs/unreleased/georgekoltsov-48854-fix-empty-flash-message.yml b/changelogs/unreleased/georgekoltsov-48854-fix-empty-flash-message.yml new file mode 100644 index 00000000000..e28dbd6f0c4 --- /dev/null +++ b/changelogs/unreleased/georgekoltsov-48854-fix-empty-flash-message.yml @@ -0,0 +1,6 @@ +--- +title: Fix empty error flash message on profile:account page when updating username + with username that has already been taken +merge_request: 31809 +author: +type: fixed diff --git a/changelogs/unreleased/gitaly-version-v1.59.0.yml b/changelogs/unreleased/gitaly-version-v1.59.0.yml new file mode 100644 index 00000000000..d103f6b248e --- /dev/null +++ b/changelogs/unreleased/gitaly-version-v1.59.0.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade to Gitaly v1.59.0 +merge_request: 31743 +author: +type: changed diff --git a/changelogs/unreleased/id-source-code-smau.yml b/changelogs/unreleased/id-source-code-smau.yml new file mode 100644 index 00000000000..6ba5068544e --- /dev/null +++ b/changelogs/unreleased/id-source-code-smau.yml @@ -0,0 +1,5 @@ +--- +title: Add usage pings for source code pushes +merge_request: 31734 +author: +type: added diff --git a/changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml b/changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml new file mode 100644 index 00000000000..606c60721b8 --- /dev/null +++ b/changelogs/unreleased/issue-61873-no-error-message-for-general-settings.yml @@ -0,0 +1,5 @@ +--- +title: error message for general settings +merge_request: 31636 +author: Mesut Güneş +type: fixed diff --git a/changelogs/unreleased/new-cycle-analytics-backend-migrations.yml b/changelogs/unreleased/new-cycle-analytics-backend-migrations.yml new file mode 100644 index 00000000000..d56a07fe569 --- /dev/null +++ b/changelogs/unreleased/new-cycle-analytics-backend-migrations.yml @@ -0,0 +1,5 @@ +--- +title: Create database tables for the new cycle analytics backend +merge_request: 31621 +author: +type: other diff --git a/changelogs/unreleased/post-migrate-private-profile.yml b/changelogs/unreleased/post-migrate-private-profile.yml new file mode 100644 index 00000000000..53a55661aa0 --- /dev/null +++ b/changelogs/unreleased/post-migrate-private-profile.yml @@ -0,0 +1,5 @@ +--- +title: Migrate remaining users with null private_profile +merge_request: 31708 +author: +type: other diff --git a/changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml b/changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml new file mode 100644 index 00000000000..cd31fe0f35c --- /dev/null +++ b/changelogs/unreleased/security-2873-blocked-user-slash-command-bypass-master.yml @@ -0,0 +1,5 @@ +--- +title: Restrict slash commands to users who can log in +merge_request: +author: +type: security diff --git a/changelogs/unreleased/sh-fix-discussions-api-perf.yml b/changelogs/unreleased/sh-fix-discussions-api-perf.yml new file mode 100644 index 00000000000..8cdbbf03dab --- /dev/null +++ b/changelogs/unreleased/sh-fix-discussions-api-perf.yml @@ -0,0 +1,5 @@ +--- +title: Eliminate many Gitaly calls in discussions API +merge_request: 31834 +author: +type: performance diff --git a/changelogs/unreleased/sh-only-flush-tags-once-per-push.yml b/changelogs/unreleased/sh-only-flush-tags-once-per-push.yml new file mode 100644 index 00000000000..502fc22ebbd --- /dev/null +++ b/changelogs/unreleased/sh-only-flush-tags-once-per-push.yml @@ -0,0 +1,5 @@ +--- +title: Only expire tag cache once per push +merge_request: 31641 +author: +type: performance diff --git a/changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml b/changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml new file mode 100644 index 00000000000..cd63b9bf425 --- /dev/null +++ b/changelogs/unreleased/sh-optimize-commit-deltas-post-receive.yml @@ -0,0 +1,5 @@ +--- +title: Reduce Gitaly calls in PostReceive +merge_request: 31741 +author: +type: performance diff --git a/changelogs/unreleased/sh-update-rugged-0-28-3.yml b/changelogs/unreleased/sh-update-rugged-0-28-3.yml new file mode 100644 index 00000000000..86446564e12 --- /dev/null +++ b/changelogs/unreleased/sh-update-rugged-0-28-3.yml @@ -0,0 +1,5 @@ +--- +title: Upgrade Rugged to 0.28.3 +merge_request: 31794 +author: +type: security diff --git a/config/application.rb b/config/application.rb index 733f8652286..2554dd8cca2 100644 --- a/config/application.rb +++ b/config/application.rb @@ -41,11 +41,6 @@ module Gitlab #{config.root}/app/models/hooks #{config.root}/app/models/members #{config.root}/app/models/project_services - #{config.root}/app/workers/concerns - #{config.root}/app/policies/concerns - #{config.root}/app/services/concerns - #{config.root}/app/serializers/concerns - #{config.root}/app/finders/concerns #{config.root}/app/graphql/resolvers/concerns #{config.root}/app/graphql/mutations/concerns]) diff --git a/config/initializers/elastic_client_setup.rb b/config/initializers/elastic_client_setup.rb index 2ecb7956007..a1abb29838b 100644 --- a/config/initializers/elastic_client_setup.rb +++ b/config/initializers/elastic_client_setup.rb @@ -5,46 +5,42 @@ require 'gitlab/current_settings' Gitlab.ee do + require 'elasticsearch/model' + + ### Modified from elasticsearch-model/lib/elasticsearch/model.rb + + [ + Elasticsearch::Model::Client::ClassMethods, + Elasticsearch::Model::Naming::ClassMethods, + Elasticsearch::Model::Indexing::ClassMethods, + Elasticsearch::Model::Searching::ClassMethods + ].each do |mod| + Elasticsearch::Model::Proxy::ClassMethodsProxy.include mod + end + + [ + Elasticsearch::Model::Client::InstanceMethods, + Elasticsearch::Model::Naming::InstanceMethods, + Elasticsearch::Model::Indexing::InstanceMethods, + Elasticsearch::Model::Serializing::InstanceMethods + ].each do |mod| + Elasticsearch::Model::Proxy::InstanceMethodsProxy.include mod + end + + Elasticsearch::Model::Proxy::InstanceMethodsProxy.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def as_indexed_json(options={}) + target.respond_to?(:as_indexed_json) ? target.__send__(:as_indexed_json, options) : super + end + CODE + + ### Monkey patches + Elasticsearch::Model::Response::Records.prepend GemExtensions::Elasticsearch::Model::Response::Records Elasticsearch::Model::Adapter::Multiple::Records.prepend GemExtensions::Elasticsearch::Model::Adapter::Multiple::Records Elasticsearch::Model::Indexing::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Indexing::InstanceMethods - - module Elasticsearch - module Model - module Client - # This mutex is only used to synchronize *creation* of a new client, so - # all including classes can share the same client instance - CLIENT_MUTEX = Mutex.new - - cattr_accessor :cached_client - cattr_accessor :cached_config - - module ClassMethods - # Override the default ::Elasticsearch::Model::Client implementation to - # return a client configured from application settings. All including - # classes will use the same instance, which is refreshed automatically - # if the settings change. - # - # _client is present to match the arity of the overridden method, where - # it is also not used. - # - # @return [Elasticsearch::Transport::Client] - def client(_client = nil) - store = ::Elasticsearch::Model::Client - - store::CLIENT_MUTEX.synchronize do - config = Gitlab::CurrentSettings.elasticsearch_config - - if store.cached_client.nil? || config != store.cached_config - store.cached_client = ::Gitlab::Elastic::Client.build(config) - store.cached_config = config - end - end - - store.cached_client - end - end - end - end - end + Elasticsearch::Model::Adapter::ActiveRecord::Importing.prepend GemExtensions::Elasticsearch::Model::Adapter::ActiveRecord::Importing + Elasticsearch::Model::Client::InstanceMethods.prepend GemExtensions::Elasticsearch::Model::Client + Elasticsearch::Model::Client::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client + Elasticsearch::Model::ClassMethods.prepend GemExtensions::Elasticsearch::Model::Client + Elasticsearch::Model.singleton_class.prepend GemExtensions::Elasticsearch::Model::Client end diff --git a/config/initializers/invisible_captcha.rb b/config/initializers/invisible_captcha.rb new file mode 100644 index 00000000000..5177c730596 --- /dev/null +++ b/config/initializers/invisible_captcha.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +InvisibleCaptcha.setup do |config| + config.honeypots = %w(firstname lastname) + config.timestamp_enabled = true + config.timestamp_threshold = 4 +end diff --git a/config/locales/doorkeeper.en.yml b/config/locales/doorkeeper.en.yml index a8234263275..258d8a99986 100644 --- a/config/locales/doorkeeper.en.yml +++ b/config/locales/doorkeeper.en.yml @@ -69,7 +69,7 @@ en: email: Allows read-only access to the user's primary email address using OpenID Connect scope_desc: api: - Grants complete read/write access to the API, including all groups and projects. + Grants complete read/write access to the API, including all groups and projects, the container registry, and the package registry. read_user: Grants read-only access to the authenticated user's profile through the /user API endpoint, which includes username, public email, and full name. Also grants access to read-only API endpoints under /users. read_repository: diff --git a/config/locales/invisible_captcha.en.yml b/config/locales/invisible_captcha.en.yml new file mode 100644 index 00000000000..5978549c0c3 --- /dev/null +++ b/config/locales/invisible_captcha.en.yml @@ -0,0 +1,4 @@ +en: + invisible_captcha: + sentence_for_humans: If you are human, please ignore this field. + timestamp_error_message: That was a bit too quick! Please resubmit. diff --git a/config/routes/project.rb b/config/routes/project.rb index b9258a35f0c..a207ee44d47 100644 --- a/config/routes/project.rb +++ b/config/routes/project.rb @@ -505,7 +505,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do get :discussions, format: :json Gitlab.ee do - get 'designs(/*vueroute)', to: 'issues#show', format: false + get 'designs(/*vueroute)', to: 'issues#show', as: :designs, format: false end end diff --git a/config/routes/user.rb b/config/routes/user.rb index 3f768d5d384..d4616c8080d 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -43,13 +43,6 @@ scope '-/users', module: :users do end end -scope '-/users', module: :users do - resources :terms, only: [:index] do - post :accept, on: :member - post :decline, on: :member - end -end - scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) do scope(path: 'users/:username', as: :user, diff --git a/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb b/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb new file mode 100644 index 00000000000..2d3243f3357 --- /dev/null +++ b/db/migrate/20190715173819_add_object_storage_flag_to_geo_node.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddObjectStorageFlagToGeoNode < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default :geo_nodes, :sync_object_storage, :boolean, default: false + end + + def down + remove_column :geo_nodes, :sync_object_storage + end +end diff --git a/db/migrate/20190715215532_add_project_emails_disabled.rb b/db/migrate/20190715215532_add_project_emails_disabled.rb new file mode 100644 index 00000000000..536ea34c0fb --- /dev/null +++ b/db/migrate/20190715215532_add_project_emails_disabled.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddProjectEmailsDisabled < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + add_column :projects, :emails_disabled, :boolean + end +end diff --git a/db/migrate/20190715215549_add_group_emails_disabled.rb b/db/migrate/20190715215549_add_group_emails_disabled.rb new file mode 100644 index 00000000000..d3fd4d2d923 --- /dev/null +++ b/db/migrate/20190715215549_add_group_emails_disabled.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddGroupEmailsDisabled < ActiveRecord::Migration[5.2] + DOWNTIME = false + + def change + add_column :namespaces, :emails_disabled, :boolean + end +end diff --git a/db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb b/db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb new file mode 100644 index 00000000000..5c005377b00 --- /dev/null +++ b/db/migrate/20190716144222_create_analytics_cycle_analytics_project_stages.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class CreateAnalyticsCycleAnalyticsProjectStages < ActiveRecord::Migration[5.2] + DOWNTIME = false + + INDEX_PREFIX = 'index_analytics_ca_project_stages_' + + def change + create_table :analytics_cycle_analytics_project_stages do |t| + t.timestamps_with_timezone + t.integer :relative_position + t.integer :start_event_identifier, null: false + t.integer :end_event_identifier, null: false + t.references(:project, { + null: false, + foreign_key: { to_table: :projects, on_delete: :cascade }, + index: { name: INDEX_PREFIX + 'on_project_id' } + }) + t.references(:start_event_label, { + foreign_key: { to_table: :labels, on_delete: :cascade }, + index: { name: INDEX_PREFIX + 'on_start_event_label_id' } + }) + t.references(:end_event_label, { + foreign_key: { to_table: :labels, on_delete: :cascade }, + index: { name: INDEX_PREFIX + 'on_end_event_label_id' } + }) + t.boolean :hidden, default: false, null: false + t.boolean :custom, default: true, null: false + t.string :name, null: false, limit: 255 + end + + add_index :analytics_cycle_analytics_project_stages, [:project_id, :name], unique: true, name: INDEX_PREFIX + 'on_project_id_and_name' + add_index :analytics_cycle_analytics_project_stages, [:relative_position], name: INDEX_PREFIX + 'on_relative_position' + end +end diff --git a/db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb b/db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb new file mode 100644 index 00000000000..5b327dc5332 --- /dev/null +++ b/db/migrate/20190729062536_create_analytics_cycle_analytics_group_stages.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +class CreateAnalyticsCycleAnalyticsGroupStages < ActiveRecord::Migration[5.2] + DOWNTIME = false + + INDEX_PREFIX = 'index_analytics_ca_group_stages_' + + def change + create_table :analytics_cycle_analytics_group_stages do |t| + t.timestamps_with_timezone + t.integer :relative_position + t.integer :start_event_identifier, null: false + t.integer :end_event_identifier, null: false + t.references(:group, { + null: false, + foreign_key: { to_table: :namespaces, on_delete: :cascade }, + index: { name: INDEX_PREFIX + 'on_group_id' } + }) + t.references(:start_event_label, { + foreign_key: { to_table: :labels, on_delete: :cascade }, + index: { name: INDEX_PREFIX + 'on_start_event_label_id' } + }) + t.references(:end_event_label, { + foreign_key: { to_table: :labels, on_delete: :cascade }, + index: { name: INDEX_PREFIX + 'on_end_event_label_id' } + }) + t.boolean :hidden, default: false, null: false + t.boolean :custom, default: true, null: false + t.string :name, null: false, limit: 255 + end + + add_index :analytics_cycle_analytics_group_stages, [:group_id, :name], unique: true, name: INDEX_PREFIX + 'on_group_id_and_name' + add_index :analytics_cycle_analytics_group_stages, [:relative_position], name: INDEX_PREFIX + 'on_relative_position' + end +end diff --git a/db/post_migrate/20190812070645_migrate_private_profile_nulls.rb b/db/post_migrate/20190812070645_migrate_private_profile_nulls.rb new file mode 100644 index 00000000000..063c1e16c27 --- /dev/null +++ b/db/post_migrate/20190812070645_migrate_private_profile_nulls.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +class MigratePrivateProfileNulls < ActiveRecord::Migration[5.2] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + DELAY = 5.minutes.to_i + BATCH_SIZE = 1_000 + + disable_ddl_transaction! + + class User < ActiveRecord::Base + self.table_name = 'users' + + include ::EachBatch + end + + def up + # Migration will take about 7 hours + User.where(private_profile: nil).each_batch(of: BATCH_SIZE) do |batch, index| + range = batch.pluck(Arel.sql("MIN(id)"), Arel.sql("MAX(id)")).first + delay = index * DELAY + + BackgroundMigrationWorker.perform_in(delay.seconds, 'MigrateNullPrivateProfileToFalse', [*range]) + end + end + + def down + # noop + end +end diff --git a/db/schema.rb b/db/schema.rb index 591758af0e4..cf7f0fc0a3d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_08_06_071559) do +ActiveRecord::Schema.define(version: 2019_08_12_070645) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -26,6 +26,44 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.integer "cached_markdown_version" end + create_table "analytics_cycle_analytics_group_stages", force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "relative_position" + t.integer "start_event_identifier", null: false + t.integer "end_event_identifier", null: false + t.bigint "group_id", null: false + t.bigint "start_event_label_id" + t.bigint "end_event_label_id" + t.boolean "hidden", default: false, null: false + t.boolean "custom", default: true, null: false + t.string "name", limit: 255, null: false + t.index ["end_event_label_id"], name: "index_analytics_ca_group_stages_on_end_event_label_id" + t.index ["group_id", "name"], name: "index_analytics_ca_group_stages_on_group_id_and_name", unique: true + t.index ["group_id"], name: "index_analytics_ca_group_stages_on_group_id" + t.index ["relative_position"], name: "index_analytics_ca_group_stages_on_relative_position" + t.index ["start_event_label_id"], name: "index_analytics_ca_group_stages_on_start_event_label_id" + end + + create_table "analytics_cycle_analytics_project_stages", force: :cascade do |t| + t.datetime_with_timezone "created_at", null: false + t.datetime_with_timezone "updated_at", null: false + t.integer "relative_position" + t.integer "start_event_identifier", null: false + t.integer "end_event_identifier", null: false + t.bigint "project_id", null: false + t.bigint "start_event_label_id" + t.bigint "end_event_label_id" + t.boolean "hidden", default: false, null: false + t.boolean "custom", default: true, null: false + t.string "name", limit: 255, null: false + t.index ["end_event_label_id"], name: "index_analytics_ca_project_stages_on_end_event_label_id" + t.index ["project_id", "name"], name: "index_analytics_ca_project_stages_on_project_id_and_name", unique: true + t.index ["project_id"], name: "index_analytics_ca_project_stages_on_project_id" + t.index ["relative_position"], name: "index_analytics_ca_project_stages_on_relative_position" + t.index ["start_event_label_id"], name: "index_analytics_ca_project_stages_on_start_event_label_id" + end + create_table "appearances", id: :serial, force: :cascade do |t| t.string "title", null: false t.text "description", null: false @@ -1456,6 +1494,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.integer "container_repositories_max_capacity", default: 10, null: false t.datetime_with_timezone "created_at" t.datetime_with_timezone "updated_at" + t.boolean "sync_object_storage", default: false, null: false t.index ["access_key"], name: "index_geo_nodes_on_access_key" t.index ["name"], name: "index_geo_nodes_on_name", unique: true t.index ["primary"], name: "index_geo_nodes_on_primary" @@ -2174,6 +2213,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.boolean "membership_lock", default: false t.integer "last_ci_minutes_usage_notification_level" t.integer "subgroup_creation_level", default: 1 + t.boolean "emails_disabled" t.index ["created_at"], name: "index_namespaces_on_created_at" t.index ["custom_project_templates_group_id", "type"], name: "index_namespaces_on_custom_project_templates_group_id_and_type", where: "(custom_project_templates_group_id IS NOT NULL)" t.index ["file_template_project_id"], name: "index_namespaces_on_file_template_project_id" @@ -2744,6 +2784,7 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.boolean "reset_approvals_on_push", default: true t.boolean "service_desk_enabled", default: true t.integer "approvals_before_merge", default: 0, null: false + t.boolean "emails_disabled" t.index ["archived", "pending_delete", "merge_requests_require_code_owner_approval"], name: "projects_requiring_code_owner_approval", where: "((pending_delete = false) AND (archived = false) AND (merge_requests_require_code_owner_approval = true))" t.index ["created_at"], name: "index_projects_on_created_at" t.index ["creator_id"], name: "index_projects_on_creator_id" @@ -3629,6 +3670,12 @@ ActiveRecord::Schema.define(version: 2019_08_06_071559) do t.index ["type"], name: "index_web_hooks_on_type" end + add_foreign_key "analytics_cycle_analytics_group_stages", "labels", column: "end_event_label_id", on_delete: :cascade + add_foreign_key "analytics_cycle_analytics_group_stages", "labels", column: "start_event_label_id", on_delete: :cascade + add_foreign_key "analytics_cycle_analytics_group_stages", "namespaces", column: "group_id", on_delete: :cascade + add_foreign_key "analytics_cycle_analytics_project_stages", "labels", column: "end_event_label_id", on_delete: :cascade + add_foreign_key "analytics_cycle_analytics_project_stages", "labels", column: "start_event_label_id", on_delete: :cascade + add_foreign_key "analytics_cycle_analytics_project_stages", "projects", on_delete: :cascade add_foreign_key "application_settings", "namespaces", column: "custom_project_templates_group_id", on_delete: :nullify add_foreign_key "application_settings", "projects", column: "file_template_project_id", name: "fk_ec757bd087", on_delete: :nullify add_foreign_key "application_settings", "projects", column: "instance_administration_project_id", on_delete: :nullify diff --git a/doc/README.md b/doc/README.md index c60e4eb177d..8ce5d2e240a 100644 --- a/doc/README.md +++ b/doc/README.md @@ -354,6 +354,7 @@ The following documentation relates to the DevOps **Secure** stage: | Secure Topics | Description | |:------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------| | [Container Scanning](user/application_security/container_scanning/index.md) **(ULTIMATE)** | Use Clair to scan docker images for known vulnerabilities. | +| [Dependency List](user/application_security/dependency_list/index.md) **(ULTIMATE)** | View your project's dependencies and their known vulnerabilities. | | [Dependency Scanning](user/application_security/dependency_scanning/index.md) **(ULTIMATE)** | Analyze your dependencies for known vulnerabilities. | | [Dynamic Application Security Testing (DAST)](user/application_security/dast/index.md) **(ULTIMATE)** | Analyze running web applications for known vulnerabilities. | | [Group Security Dashboard](user/application_security/security_dashboard/index.md) **(ULTIMATE)** | View vulnerabilities in all the projects in a group and its subgroups. | diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md index e418938451a..d0adeb89543 100644 --- a/doc/administration/container_registry.md +++ b/doc/administration/container_registry.md @@ -669,6 +669,39 @@ To get around this, you can [change the group path](../user/group/index.md#chang branch name. Another option is to create a [push rule](../push_rules/push_rules.html) to prevent this at the instance level. +### Image push errors + +When getting errors or "retrying" loops in an attempt to push an image but `docker login` works fine, +there is likely an issue with the headers forwarded to the registry by NGINX. The default recommended +NGINX configurations should handle this, but it might occur in custom setups where the SSL is +offloaded to a third party reverse proxy. + +This problem was discussed in a [docker project issue][docker-image-push-issue] and a simple solution +would be to enable relative urls in the registry. + +**For Omnibus installations** + +1. Edit `/etc/gitlab/gitlab.rb`: + + ```ruby + registry['env'] = { + "REGISTRY_HTTP_RELATIVEURLS" => true + } + ``` + +1. Save the file and [reconfigure GitLab][] for the changes to take effect. + +**For installations from source** + +1. Edit the YML configuration file you created when you [deployed the registry][registry-deploy]. Add the following snippet: + + ```yaml + http: + relativeurls: true + ``` + +1. Restart the registry for the changes to take affect. + [ce-18239]: https://gitlab.com/gitlab-org/gitlab-ce/issues/18239 [docker-insecure-self-signed]: https://docs.docker.com/registry/insecure/#use-self-signed-certificates [reconfigure gitlab]: restart_gitlab.md#omnibus-gitlab-reconfigure @@ -687,3 +720,4 @@ this at the instance level. [new-domain]: #configure-container-registry-under-its-own-domain [notifications-config]: https://docs.docker.com/registry/notifications/ [registry-notifications-config]: https://docs.docker.com/registry/configuration/#notifications +[docker-image-push-issue]: https://github.com/docker/distribution/issues/970 diff --git a/doc/administration/monitoring/gitlab_instance_administration_project/index.md b/doc/administration/monitoring/gitlab_instance_administration_project/index.md new file mode 100644 index 00000000000..8e33cea6217 --- /dev/null +++ b/doc/administration/monitoring/gitlab_instance_administration_project/index.md @@ -0,0 +1,36 @@ +# GitLab instance administration project + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/56883) in GitLab 12.2. + +GitLab has been adding the ability for administrators to see insights into the health of +their GitLab instance. In order to surface this experience in a native way, similar to how +you would interact with an application deployed via GitLab, a base project called +"GitLab Instance Administration" with +[internal visibility](../../../public_access/public_access.md#internal-projects) will be +added under a group called "GitLab Instance Administrators" specifically created for +visualizing and configuring the monitoring of your GitLab instance. + +All administrators at the time of creation of the project and group will be added +as maintainers of the group and project, and as an admin, you'll be able to add new +members to the group in order to give them maintainer access to the project. + +This project will be used for self-monitoring your GitLab instance. + +## Connection to Prometheus + +The project will be automatically configured to connect to the +[internal Prometheus](../prometheus/index.md) instance if the Prometheus +instance is present (should be the case if GitLab was installed via Omnibus +and you haven't disabled it). + +If that's not the case or if you have an external Prometheus instance or an HA setup, +you should +[configure it manually](../../../user/project/integrations/prometheus.md#manual-configuration-of-prometheus). + +## Taking action on Prometheus alerts **[ULTIMATE]** + +You can [add a webhook](../../../user/project/integrations/prometheus.md#external-prometheus-instances) +to the Prometheus config in order for GitLab to receive notifications of any alerts. + +Once the webhook is setup, you can +[take action on incoming alerts](../../../user/project/integrations/prometheus.md#taking-action-on-incidents-ultimate). diff --git a/doc/administration/monitoring/index.md b/doc/administration/monitoring/index.md index fa0459b24ff..2b3daec42bd 100644 --- a/doc/administration/monitoring/index.md +++ b/doc/administration/monitoring/index.md @@ -2,6 +2,9 @@ Explore our features to monitor your GitLab instance: +- [GitLab self-monitoring](gitlab_instance_administration_project/index.md): The + GitLab instance administration project helps to monitor the GitLab instance and + take action on alerts. - [Performance monitoring](performance/index.md): GitLab Performance Monitoring makes it possible to measure a wide variety of statistics of your instance. - [Prometheus](prometheus/index.md): Prometheus is a powerful time-series monitoring service, providing a flexible platform for monitoring GitLab and other software products. - [GitHub imports](github_imports.md): Monitor the health and progress of GitLab's GitHub importer with various Prometheus metrics. diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md index 054fa547704..ec26c0b2e7e 100644 --- a/doc/administration/monitoring/prometheus/gitlab_metrics.md +++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md @@ -120,7 +120,6 @@ When Puma is used instead of Unicorn, following metrics are available: | puma_workers | Gauge | 12.0 | Total number of workers | | puma_running_workers | Gauge | 12.0 | Number of booted workers | | puma_stale_workers | Gauge | 12.0 | Number of old workers | -| puma_phase | Gauge | 12.0 | Phase number (increased during phased restarts) | | puma_running | Gauge | 12.0 | Number of running threads | | puma_queued_connections | Gauge | 12.0 | Number of connections in that worker's "todo" set waiting for a worker thread | | puma_active_connections | Gauge | 12.0 | Number of threads processing a request | diff --git a/doc/api/dependencies.md b/doc/api/dependencies.md index 2496b038c7f..015ffbe60f6 100644 --- a/doc/api/dependencies.md +++ b/doc/api/dependencies.md @@ -11,7 +11,7 @@ Every call to this endpoint requires authentication. To perform this call, user ## List project dependencies Get a list of project dependencies. This API partially mirroring -[Dependency List](../user/application_security/dependency_scanning/index.md#dependency-list) feature. +[Dependency List](../user/application_security/dependency_list/index.md) feature. This list can be generated only for [languages and package managers](../user/application_security/dependency_scanning/index.md#supported-languages-and-package-managers) supported by Gemnasium. diff --git a/doc/api/geo_nodes.md b/doc/api/geo_nodes.md index e3af5d60533..d0b33ab467f 100644 --- a/doc/api/geo_nodes.md +++ b/doc/api/geo_nodes.md @@ -10,7 +10,7 @@ GET /geo_nodes ``` ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes ``` Example response: @@ -27,8 +27,15 @@ Example response: "current": true, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit", + "_links": { + "self": "https://primary.example.com/api/v4/geo_nodes/1", + "status":"https://primary.example.com/api/v4/geo_nodes/1/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/1/repair" + } }, { "id": 2, @@ -40,8 +47,17 @@ Example response: "current": false, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "sync_object_storage": true, + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/2/edit", + "web_geo_projects_url": "https://secondary.example.com/admin/geo/projects", + "_links": { + "self":"https://primary.example.com/api/v4/geo_nodes/2", + "status":"https://primary.example.com/api/v4/geo_nodes/2/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/2/repair" + } } ] ``` @@ -53,7 +69,7 @@ GET /geo_nodes/:id ``` ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/1 +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/1 ``` Example response: @@ -69,8 +85,15 @@ Example response: "current": true, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit", + "_links": { + "self": "https://primary.example.com/api/v4/geo_nodes/1", + "status":"https://primary.example.com/api/v4/geo_nodes/1/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/1/repair" + } } ``` @@ -84,16 +107,18 @@ _This can only be run against a primary Geo node._ PUT /geo_nodes/:id ``` -| Attribute | Type | Required | Description | -|----------------------|---------|-----------|---------------------------------------------------------------------------| -| `id` | integer | yes | The ID of the Geo node. | -| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. | -| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in gitlab.rb, otherwise it must match `external_url`. | -| `url` | string | yes | The user-facing URL of the Geo node. | -| `internal_url` | string | no | The URL defined on the primary node that secondary nodes should use to contact it. Returns `url` if not set.| -| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. | -| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. | -| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. | +| Attribute | Type | Required | Description | +|-----------------------------|---------|-----------|---------------------------------------------------------------------------| +| `id` | integer | yes | The ID of the Geo node. | +| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. | +| `name` | string | yes | The unique identifier for the Geo node. Must match `geo_node_name` if it is set in gitlab.rb, otherwise it must match `external_url`. | +| `url` | string | yes | The user-facing URL of the Geo node. | +| `internal_url` | string | no | The URL defined on the primary node that secondary nodes should use to contact it. Returns `url` if not set.| +| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. | +| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. | +| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. | +| `container_repositories_max_capacity` | integer | no | Control the maximum concurrency of container repository sync for this node. | +| `sync_object_storage` | boolean | no | Flag indicating if the secondary Geo node will replicate blobs in Object Storage. | Example response: @@ -108,8 +133,17 @@ Example response: "current": true, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "sync_object_storage": true, + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/2/edit", + "web_geo_projects_url": "https://secondary.example.com/admin/geo/projects", + "_links": { + "self":"https://primary.example.com/api/v4/geo_nodes/2", + "status":"https://primary.example.com/api/v4/geo_nodes/2/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/2/repair" + } } ``` @@ -151,8 +185,15 @@ Example response: "current": true, "files_max_capacity": 10, "repos_max_capacity": 25, + "container_repositories_max_capacity": 10, "verification_max_capacity": 100, - "clone_protocol": "http" + "clone_protocol": "http", + "web_edit_url": "https://primary.example.com/admin/geo/nodes/1/edit", + "_links": { + "self": "https://primary.example.com/api/v4/geo_nodes/1", + "status":"https://primary.example.com/api/v4/geo_nodes/1/status", + "repair":"https://primary.example.com/api/v4/geo_nodes/1/repair" + } } ``` @@ -163,7 +204,7 @@ GET /geo_nodes/status ``` ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/status +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/status ``` Example response: @@ -314,7 +355,7 @@ GET /geo_nodes/:id/status ``` ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/2/status +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/2/status ``` Example response: @@ -388,7 +429,7 @@ GET /geo_nodes/current/failures This endpoint uses [Pagination](README.md#pagination). ```bash -curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/geo_nodes/current/failures +curl --header "PRIVATE-TOKEN: <your_access_token>" https://primary.example.com/api/v4/geo_nodes/current/failures ``` Example response: diff --git a/doc/api/releases/links.md b/doc/api/releases/links.md index 9c91264ed65..b1b75abe32f 100644 --- a/doc/api/releases/links.md +++ b/doc/api/releases/links.md @@ -21,7 +21,7 @@ GET /projects/:id/releases/:tag_name/assets/links Example request: ```sh -curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links" +curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links" ``` Example response: @@ -60,7 +60,7 @@ GET /projects/:id/releases/:tag_name/assets/links/:link_id Example request: ```sh -curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1" +curl --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1" ``` Example response: @@ -96,7 +96,7 @@ curl --request POST \ --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" \ --data name="awesome-v0.2.dmg" \ --data url="http://192.168.10.15:3000" \ - "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links" + "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links" ``` Example response: @@ -132,7 +132,7 @@ You have to specify at least one of `name` or `url` Example request: ```sh -curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1" +curl --request PUT --data name="new name" --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1" ``` Example response: @@ -163,7 +163,7 @@ DELETE /projects/:id/releases/:tag_name/assets/links/:link_id Example request: ```sh -curl --request DELETE --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "http://localhost:3000/api/v4/projects/24/releases/v0.1/assets/links/1" +curl --request DELETE --header "PRIVATE-TOKEN: gDybLx3yrUK_HLp3qPjS" "https://gitlab.example.com/api/v4/projects/24/releases/v0.1/assets/links/1" ``` Example response: diff --git a/doc/api/settings.md b/doc/api/settings.md index 83125aff264..248d19461f6 100644 --- a/doc/api/settings.md +++ b/doc/api/settings.md @@ -321,4 +321,8 @@ are listed in the descriptions of the relevant settings. | `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push project code via SSH" warning shown to users with no uploaded SSH key. | | `version_check_enabled` | boolean | no | Let GitLab inform you when an update is available. | | `local_markdown_version` | integer | no | Increase this value when any cached markdown should be invalidated. | +| `snowplow_enabled` | boolean | no | Enable snowplow tracking. | +| `snowplow_collector_hostname` | string | required by: `snowplow_enabled` | The Snowplow collector hostname. (e.g. `snowplow.trx.gitlab.net`) | +| `snowplow_site_id` | string | no | The Snowplow site name / application id. (e.g. `gitlab`) | +| `snowplow_cookie_domain` | string | no | The Snowplow cookie domain. (e.g. `.gitlab.com`) | | `geo_node_allowed_ips` | string | yes | **(PREMIUM)** Comma-separated list of IPs and CIDRs of allowed secondary nodes. For example, `1.1.1.1, 2.2.2.0/24`. | diff --git a/doc/ci/README.md b/doc/ci/README.md index f3006528d01..ca9d0aa61bd 100644 --- a/doc/ci/README.md +++ b/doc/ci/README.md @@ -16,7 +16,7 @@ through the [continuous methodologies](introduction/index.md#introduction-to-cic NOTE: **Out-of-the-box management systems can decrease hours spent on maintaining toolchains by 10% or more.** Watch our ["Mastering continuous software development"](https://about.gitlab.com/webcast/mastering-ci-cd/) -webcast to learn about continuous methods and how GitLab’s built-in CI can help you simplify and scale software development. +webcast to learn about continuous methods and how GitLab’s built-in CI can help you simplify and scale software development. ## Overview @@ -67,11 +67,11 @@ to your needs: For a broader overview, see the [CI/CD getting started](quick_start/README.md) guide. Once you're familiar with how GitLab CI/CD works, see the -[`. gitlab-ci.yml` full reference](yaml/README.md) +[`.gitlab-ci.yml` full reference](yaml/README.md) for all the attributes you can set and use. NOTE: **Note:** -GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](../user/admin_area/settings/continuous_integration.md#extra-shared-runners-pipeline-minutes-quota-free-only). +GitLab CI/CD and [shared runners](runners/README.md#shared-specific-and-group-runners) are enabled in GitLab.com and available for all users, limited only to the [user's pipelines quota](../user/gitlab_com/index.md#shared-runners). ## Configuration diff --git a/doc/ci/environments.md b/doc/ci/environments.md index 9e1a62bae71..f6c47a99712 100644 --- a/doc/ci/environments.md +++ b/doc/ci/environments.md @@ -619,6 +619,10 @@ versions of the app, all without leaving GitLab. Add a [button to the Monitoring dashboard](../user/project/operations/linking_to_an_external_dashboard.md) linking directly to your existing external dashboards. +#### Embedding metrics in GitLab Flavored Markdown + +Metric charts can be embedded within GitLab Flavored Markdown. See [Embedding Metrics within GitLab Flavored Markdown](../user/project/integrations/prometheus.md#embedding-metric-charts-within-gitlab-flavored-markdown) for more details. + ### Web terminals > Web terminals were added in GitLab 8.15 and are only available to project Maintainers and Owners. diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md index c3678fc948e..8474d4ef66e 100644 --- a/doc/ci/runners/README.md +++ b/doc/ci/runners/README.md @@ -319,21 +319,21 @@ How this feature will work: 1. You set the _maximum job timeout_ for a Runner to 24 hours 1. You set the _CI/CD Timeout_ for a project to **2 hours** 1. You start a job -1. The job, if running longer, will be timeouted after **2 hours** +1. The job, if running longer, will be timed out after **2 hours** **Example 2 - Runner timeout not configured** 1. You remove the _maximum job timeout_ configuration from a Runner 1. You set the _CI/CD Timeout_ for a project to **2 hours** 1. You start a job -1. The job, if running longer, will be timeouted after **2 hours** +1. The job, if running longer, will be timed out after **2 hours** **Example 3 - Runner timeout smaller than project timeout** 1. You set the _maximum job timeout_ for a Runner to **30 minutes** 1. You set the _CI/CD Timeout_ for a project to 2 hours 1. You start a job -1. The job, if running longer, will be timeouted after **30 minutes** +1. The job, if running longer, will be timed out after **30 minutes** ### Be careful with sensitive information diff --git a/doc/customization/issue_and_merge_request_template.md b/doc/customization/issue_and_merge_request_template.md index adaa120a37e..ebf711e105b 100644 --- a/doc/customization/issue_and_merge_request_template.md +++ b/doc/customization/issue_and_merge_request_template.md @@ -1,5 +1,5 @@ --- -redirect_to: '../user/project/description_templates.md#setting-a-default-template-for-issues-and-merge-requests--starter' +redirect_to: '../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues--starter' --- -This document was moved to [description_templates](../user/project/description_templates.md#setting-a-default-template-for-issues-and-merge-requests--starter). +This document was moved to [description_templates](../user/project/description_templates.md#setting-a-default-template-for-merge-requests-and-issues--starter). diff --git a/doc/development/automatic_ce_ee_merge.md b/doc/development/automatic_ce_ee_merge.md index 423b35a9e3a..98b8a48abf4 100644 --- a/doc/development/automatic_ce_ee_merge.md +++ b/doc/development/automatic_ce_ee_merge.md @@ -171,6 +171,19 @@ Now, every time you create an MR for CE and EE: job failed, you are required to submit the EE MR so that you can fix the conflicts in EE before merging your changes into CE. +## How we run the Automatic CE->EE merge at GitLab + +At GitLab, we use the [Merge Train](https://gitlab.com/gitlab-org/merge-train) +project to keep our [gitlab-ee](https://gitlab.com/gitlab-org/gitlab-ee) +repository updated with commits from +[gitlab-ce](https://gitlab.com/gitlab-org/gitlab-ce). + +We have a mirror of the [Merge Train](https://gitlab.com/gitlab-org/merge-train) +project [configured](https://ops.gitlab.net/gitlab-org/merge-train) to run an +automatic CE->EE merge job every twenty minutes as a scheduled CI job. The +[configured](https://ops.gitlab.net/gitlab-org/merge-train) Merge Train project +is only accessible to authorized GitLab staff. + ## FAQ ### How does automatic merging work? diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index 59c8bfe2964..680f2cd13c2 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -877,10 +877,10 @@ Other text includes deprecation notices and version-specific how-to information. When a feature is available in EE-only tiers, add the corresponding tier according to the feature availability: +- For GitLab Core and GitLab.com Free: `**(CORE)**`. - For GitLab Starter and GitLab.com Bronze: `**(STARTER)**`. - For GitLab Premium and GitLab.com Silver: `**(PREMIUM)**`. - For GitLab Ultimate and GitLab.com Gold: `**(ULTIMATE)**`. -- For GitLab Core and GitLab.com Free: `**(CORE)**`. To exclude GitLab.com tiers (when the feature is not available in GitLab.com), add the keyword "only": @@ -892,6 +892,7 @@ keyword "only": For GitLab.com only tiers (when the feature is not available for self-hosted instances): +- For GitLab Free and higher tiers: `**(FREE ONLY)**`. - For GitLab Bronze and higher tiers: `**(BRONZE ONLY)**`. - For GitLab Silver and higher tiers: `**(SILVER ONLY)**`. - For GitLab Gold: `**(GOLD ONLY)**`. diff --git a/doc/development/elasticsearch.md b/doc/development/elasticsearch.md index 0965db29557..635895051bc 100644 --- a/doc/development/elasticsearch.md +++ b/doc/development/elasticsearch.md @@ -148,6 +148,36 @@ Uses an [Edge NGram token filter](https://www.elastic.co/guide/en/elasticsearch/ - Searches can have their own analyzers. Remember to check when editing analyzers - `Character` filters (as opposed to token filters) always replace the original character, so they're not a good choice as they can hinder exact searches +## Architecture + +GitLab uses `elasticsearch-rails` for handling communication with Elasticsearch server. However, in order to achieve zero-downtime deployment during schema changes, an extra abstraction layer is built to allow: + +* Indexing (writes) to multiple indexes, with different mappings +* Switching to different index for searches (reads) on the fly + +Currently we are on the process of migrating models to this new design (e.g. `Snippet`), and it is hardwired to work with a single version for now. + +Traditionally, `elasticsearch-rails` provides class and instance level `__elasticsearch__` proxy methods. If you call `Issue.__elasticsearch__`, you will get an instance of `Elasticsearch::Model::Proxy::ClassMethodsProxy`, and if you call `Issue.first.__elasticsearch__`, you will get an instance of `Elasticsearch::Model::Proxy::InstanceMethodsProxy`. These proxy objects would talk to Elasticsearch server directly. + +In the new design, `__elasticsearch__` instead represents one extra layer of proxy. It would keep multiple versions of the actual proxy objects, and it would forward read and write calls to the proxy of the intended version. + +The `elasticsearch-rails`'s way of specifying each model's mappings and other settings is to create a module for the model to include. However in the new design, each model would have its own corresponding subclassed proxy object, where the settings reside in. For example, snippet related setting in the past reside in `SnippetsSearch` module, but in the new design would reside in `SnippetClassProxy` (which is a subclass of `Elasticsearch::Model::Proxy::ClassMethodsProxy`). This reduces namespace pollution in model classes. + +The global configurations per version are now in the `Elastic::(Version)::Config` class. You can change mappings there. + +### Creating new version of schema + +Currently GitLab would still work with a single version of setting. Once it is implemented, multiple versions of setting can exists in different folders (e.g. `ee/lib/elastic/v12p1` and `ee/lib/elastic/v12p3`). To keep a continuous git history, the latest version lives under the `/latest` folder, but is aliased as the latest version. + +If the current version is `v12p1`, and we need to create a new version for `v12p3`, the steps are as follows: + +1. Copy the entire folder of `v12p1` as `v12p3` +1. Change the namespace for files under `v12p3` folder from `V12p1` to `V12p3` (which are still aliased to `Latest`) +1. Delete `v12p1` folder +1. Copy the entire folder of `latest` as `v12p1` +1. Change the namespace for files under `v12p1` folder from `Latest` to `V12p1` +1. Make changes to `Latest` as needed + ## Troubleshooting ### Getting `flood stage disk watermark [95%] exceeded` diff --git a/doc/development/fe_guide/event_tracking.md b/doc/development/fe_guide/event_tracking.md index 715d91c6db6..1b417d4c8c2 100644 --- a/doc/development/fe_guide/event_tracking.md +++ b/doc/development/fe_guide/event_tracking.md @@ -1,6 +1,8 @@ -# Event Tracking +# Event tracking -We use a tracking interface that wraps up [Snowplow](https://github.com/snowplow/snowplow) for tracking custom events. Snowplow implements page tracking, but also exposes custom event tracking. +GitLab provides `Tracking`, an interface that wraps +[Snowplow](https://github.com/snowplow/snowplow) for tracking custom events. +It uses Snowplow's custom event tracking functions. The tracking interface can be imported in JS files as follows: diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md index 6bf6113dd81..deaef8e768b 100644 --- a/doc/development/fe_guide/index.md +++ b/doc/development/fe_guide/index.md @@ -17,6 +17,8 @@ Working with our frontend assets requires Node (v8.10.0 or greater) and Yarn For our currently-supported browsers, see our [requirements][requirements]. +Use [BrowserStack](https://www.browserstack.com/) to test with our supported browsers. Login to BrowserStack with the credentials saved in GitLab's [shared 1Password account](https://about.gitlab.com/handbook/security/#1password-for-teams). + ## Initiatives Current high-level frontend goals are listed on [Frontend Epics](https://gitlab.com/groups/gitlab-org/-/epics?label_name%5B%5D=frontend). diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md index 448d9fd01c4..9d6792e9139 100644 --- a/doc/development/testing_guide/best_practices.md +++ b/doc/development/testing_guide/best_practices.md @@ -70,6 +70,7 @@ bundle exec rspec spec/[path]/[to]/[spec].rb - On `before` and `after` hooks, prefer it scoped to `:context` over `:all` - When using `evaluate_script("$('.js-foo').testSomething()")` (or `execute_script`) which acts on a given element, use a Capyabara matcher beforehand (e.g. `find('.js-foo')`) to ensure the element actually exists. +- Use `focus: true` to isolate parts of the specs you want to run. [four-phase-test]: https://robots.thoughtbot.com/four-phase-test diff --git a/doc/development/understanding_explain_plans.md b/doc/development/understanding_explain_plans.md index 11aafd7b639..7c926c83a36 100644 --- a/doc/development/understanding_explain_plans.md +++ b/doc/development/understanding_explain_plans.md @@ -199,7 +199,7 @@ more common ones here. A full list of all the available nodes and their descriptions can be found in the [PostgreSQL source file -"plannodes.h"](https://github.com/postgres/postgres/blob/master/src/include/nodes/plannodes.h) +"plannodes.h"](https://gitlab.com/postgres/postgres/blob/master/src/include/nodes/plannodes.h) ### Seq Scan @@ -224,7 +224,7 @@ used when we would read too much data from an index scan, but too little to perform a sequential scan. A bitmap scan uses what is known as a [bitmap index](https://en.wikipedia.org/wiki/Bitmap_index) to perform its work. -The [source code of PostgreSQL](https://github.com/postgres/postgres/blob/1c2cb2744bf3d8ad751cd5cf3b347f10f48492b3/src/include/nodes/plannodes.h#L446-L457) +The [source code of PostgreSQL](https://gitlab.com/postgres/postgres/blob/REL_11_STABLE/src/include/nodes/plannodes.h#L441) states the following on bitmap scans: > Bitmap Index Scan delivers a bitmap of potential tuple locations; it does not diff --git a/doc/install/installation.md b/doc/install/installation.md index 72a3514e2d5..295d9804497 100644 --- a/doc/install/installation.md +++ b/doc/install/installation.md @@ -172,7 +172,7 @@ sudo make install # Download and compile from source cd /tmp curl --remote-name --location --progress https://www.kernel.org/pub/software/scm/git/git-2.22.0.tar.gz -echo 'a4b7e4365bee43caa12a38d646d2c93743d755d1cea5eab448ffb40906c9da0b' git-2.22.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.22.0.tar.gz +echo 'a4b7e4365bee43caa12a38d646d2c93743d755d1cea5eab448ffb40906c9da0b git-2.22.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.22.0.tar.gz cd git-2.22.0/ ./configure --with-libpcre make prefix=/usr/local all @@ -202,8 +202,8 @@ Then select 'Internet Site' and press enter to confirm the hostname. The Ruby interpreter is required to run GitLab. -**Note:** The current supported Ruby (MRI) version is 2.5.x. GitLab 11.6 - dropped support for Ruby 2.4.x. +**Note:** The current supported Ruby (MRI) version is 2.6.x. GitLab 12.2 + dropped support for Ruby 2.5.x. The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab in production, frequently leads to hard to diagnose problems. For example, diff --git a/doc/install/requirements.md b/doc/install/requirements.md index 83a9e7fe294..234e5acb394 100644 --- a/doc/install/requirements.md +++ b/doc/install/requirements.md @@ -40,7 +40,7 @@ Please consider using a virtual machine to run GitLab. ## Ruby versions -GitLab requires Ruby (MRI) 2.5. Support for Ruby versions below 2.5 (2.3, 2.4) will stop with GitLab 11.6. +GitLab requires Ruby (MRI) 2.6. Support for Ruby versions below 2.6 (2.4, 2.5) will stop with GitLab 12.2. You will have to use the standard MRI implementation of Ruby. We love [JRuby](https://www.jruby.org/) and [Rubinius](https://rubinius.com) but GitLab diff --git a/doc/user/admin_area/settings/img/additional_minutes.png b/doc/subscriptions/img/additional_minutes.png Binary files differindex b159b98c9ce..b159b98c9ce 100644 --- a/doc/user/admin_area/settings/img/additional_minutes.png +++ b/doc/subscriptions/img/additional_minutes.png diff --git a/doc/user/admin_area/settings/img/buy_btn.png b/doc/subscriptions/img/buy_btn.png Binary files differindex 4fd05c0fba7..4fd05c0fba7 100644 --- a/doc/user/admin_area/settings/img/buy_btn.png +++ b/doc/subscriptions/img/buy_btn.png diff --git a/doc/user/admin_area/settings/img/buy_minutes_card.png b/doc/subscriptions/img/buy_minutes_card.png Binary files differindex cab098300cd..cab098300cd 100644 --- a/doc/user/admin_area/settings/img/buy_minutes_card.png +++ b/doc/subscriptions/img/buy_minutes_card.png diff --git a/doc/subscriptions/index.md b/doc/subscriptions/index.md index 9317ba0c71e..13c406727ab 100644 --- a/doc/subscriptions/index.md +++ b/doc/subscriptions/index.md @@ -221,6 +221,52 @@ The following table describes details of your subscription for groups: | Subscription start date | Date your subscription started. If this is for a Free plan, is the date you transitioned off your group's paid plan. | | Subscription end date | Date your current subscription will end. Does not apply to Free plans. | +#### Extra Shared Runners pipeline minutes + +If you're using GitLab.com, you can purchase additional CI minutes so your +pipelines will not be blocked after you have used all your CI minutes from your +main quota. Additional minutes: + +- Are only used once the shared quota included in your subscription runs out. +- Roll over month to month. + +In order to purchase additional minutes, you should follow these steps: + +1. Go to **Group > Settings > Pipelines quota**. Once you are on that page, click on **Buy additional minutes**. + + ![Buy additional minutes](img/buy_btn.png) + +1. Locate the subscription card that is linked to your group on GitLab.com, + click on **Buy more CI minutes**, and complete the details about the transaction. + + ![Buy additional minutes](img/buy_minutes_card.png) + +1. Once we have processed your payment, the extra CI minutes + will be synced to your Group and you can visualize it from the + **Group > Settings > Pipelines quota** page: + + ![Additional minutes](img/additional_minutes.png) + +Be aware that: + +1. If you have purchased extra CI minutes before the purchase of a paid plan, + we will calculate a pro-rated charge for your paid plan. That means you may + be charged for less than one year since your subscription was previously + created with the extra CI minutes. +1. Once the extra CI minutes has been assigned to a Group they cannot be transferred + to a different Group. +1. If you have some minutes used over your default quota, these minutes will + be deducted from your Additional Minutes quota immediately after your purchase of additional + minutes. + +##### What happens when my CI minutes run out + +When the CI minutes run out, an email is sent automatically to notify the owner(s) +of the group/namespace, including a link to [purchase more minutes](https://customers.gitlab.com/plans). + +If you are not the owner of the group, you will need to contact them to let them know they need to +[purchase more minutes](https://customers.gitlab.com/plans). + ## Subscription changes and your data When your subscription or trial expires, GitLab does not delete your data. diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md index 0aef40262c9..df35638cba2 100644 --- a/doc/update/upgrading_from_source.md +++ b/doc/update/upgrading_from_source.md @@ -47,8 +47,8 @@ sudo service gitlab stop ### 3. Update Ruby -NOTE: Beginning in GitLab 11.6, we only support Ruby 2.5 or higher, and dropped -support for Ruby 2.4. Be sure to upgrade if necessary. +NOTE: Beginning in GitLab 12.2, we only support Ruby 2.6 or higher, and dropped +support for Ruby 2.5. Be sure to upgrade if necessary. You can check which version you are running with `ruby -v`. diff --git a/doc/user/admin_area/settings/account_and_limit_settings.md b/doc/user/admin_area/settings/account_and_limit_settings.md index 7fbb4d84cfc..6faab685b26 100644 --- a/doc/user/admin_area/settings/account_and_limit_settings.md +++ b/doc/user/admin_area/settings/account_and_limit_settings.md @@ -4,6 +4,17 @@ type: reference # Account and limit settings +## Max attachment size + +You can change the maximum file size for attachments in comments and replies in GitLab. +Navigate to **Admin Area (wrench icon) > Settings > General**, then expand **Account and Limit**. +From here, you can increase or decrease by changing the value in `Maximum attachment size (MB)`. + +NOTE: **Note:** +If you choose a size larger than what is currently configured for the web server, +you will likely get errors. See the [troubleshooting section](#troubleshooting) for more +details. + ## Repository size limit **(STARTER)** > [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/740) in [GitLab Enterprise Edition 8.12](https://about.gitlab.com/2016/09/22/gitlab-8-12-released/#limit-project-size-ee). @@ -51,14 +62,18 @@ For details on manually purging files, see [reducing the repository size using G NOTE: **Note:** GitLab.com repository size [is set by GitLab](../../gitlab_com/index.md#repository-size-limit). -<!-- ## Troubleshooting +## Troubleshooting + +### 413 Request Entity Too Large + +If you are attaching a file to a comment or reply in GitLab and receive the `413 Request Entity Too Large` +error, it is likely caused by having a [max attachment size](#max-attachment-size) +larger than what the web server is configured to allow. -Include any troubleshooting steps that you can foresee. If you know beforehand what issues -one might have when setting this up, or when something is changed, or on upgrading, it's -important to describe those, too. Think of things that may go wrong and include them here. -This is important to minimize requests for support, and to avoid doc comments with -questions that you know someone might ask. +If you wanted to increase the max attachment size to 200m in a GitLab +[Omnibus](https://docs.gitlab.com/omnibus/) install, for example, you might need to +add the line below to `/etc/gitlab/gitlab.rb` before increasing the max attachment size: -Each scenario can be a third-level heading, e.g. `### Getting error message X`. -If you have none to add when creating a doc, leave this section in place -but commented out to help encourage others to add to it in the future. --> +``` +nginx['client_max_body_size'] = "200m" +``` diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md index 43640f1b16a..bd76b052422 100644 --- a/doc/user/admin_area/settings/continuous_integration.md +++ b/doc/user/admin_area/settings/continuous_integration.md @@ -94,52 +94,6 @@ a group in the **Usage Quotas** page available to the group page settings list. ![Group pipelines quota](img/group_pipelines_quota.png) -## Extra Shared Runners pipeline minutes quota **(FREE ONLY)** - -If you're using GitLab.com, you can purchase additional CI minutes so your -pipelines will not be blocked after you have used all your CI minutes from your -main quota. Additional minutes: - -- Are only used once the shared quota included in your subscription runs out. -- Roll over month to month. - -In order to purchase additional minutes, you should follow these steps: - -1. Go to **Group > Settings > Pipelines quota**. Once you are on that page, click on **Buy additional minutes**. - - ![Buy additional minutes](img/buy_btn.png) - -1. Locate the subscription card that is linked to your group on GitLab.com, - click on **Buy more CI minutes**, and complete the details about the transaction. - - ![Buy additional minutes](img/buy_minutes_card.png) - -1. Once we have processed your payment, the extra CI minutes - will be synced to your Group and you can visualize it from the - **Group > Settings > Pipelines quota** page: - - ![Additional minutes](img/additional_minutes.png) - -Be aware that: - -1. If you have purchased extra CI minutes before the purchase of a paid plan, - we will calculate a pro-rated charge for your paid plan. That means you may - be charged for less than one year since your subscription was previously - created with the extra CI minutes. -1. Once the extra CI minutes has been assigned to a Group they cannot be transferred - to a different Group. -1. If you have some minutes used over your default quota, these minutes will - be deducted from your Additional Minutes quota immediately after your purchase of additional - minutes. - -## What happens when my CI minutes quota run out - -When the CI minutes quota run out, an email is sent automatically to notifies the owner(s) of the group/namespace which -includes a link to [purchase more minutes](https://customers.gitlab.com/plans). - -If you are not the owner of the group, you will need to contact them to let them know they need to -[purchase more minutes](https://customers.gitlab.com/plans). - ## Archive jobs **(CORE ONLY)** Archiving jobs is useful for reducing the CI/CD footprint on the system by diff --git a/doc/user/analytics/productivity_analytics.md b/doc/user/analytics/productivity_analytics.md new file mode 100644 index 00000000000..db3e37bd0fb --- /dev/null +++ b/doc/user/analytics/productivity_analytics.md @@ -0,0 +1,69 @@ +# Productivity Analytics **(PREMIUM)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12079) in [GitLab Premium](https://about.gitlab.com/pricing/) 12.2 (enabled by feature flags `analytics` and `productivity_analytics`). + +Track development velocity with Productivity Analytics. + +For many companies, the development cycle is a blackbox and getting an estimate of how +long, on average, it takes to deliver features is an enormous endeavor. + +While [Cycle Analytics](../project/cycle_analytics.md) focuses on the entire +SDLC process, Productivity Analytics provides a way for Engineering Management to +drill down in a systematic way to uncover patterns and causes for success or failure at +an individual, project or group level. + +Productivity can slow down for many reasons ranging from degrading code base to quickly +growing teams. In order to investigate, department or team leaders can start by visualizing the time +it takes for merge requests to be merged. + +## Supported features + +Productivity Analytics allows GitLab users to: + +- Visualize typical Merge Request lifetime and statistics. Use a histogram + that shows the distribution of the time elapsed between creating and merging + Merge Requests. +- Drill down into the most time consuming Merge Requests, select a number of outliers, + and filter down all subsequent charts to investigate potential causes. +- Filter by group, project, author, label, milestone, or a specific date range. + Filter down, for example, to the Merge Requests of a specific author in a group + or project during a milestone or specific date range. + +## Accessing metrics and visualizations + +To access the **Productivity Analytics** page, go to **Analytics > Productivity Analytics**. + +The following metrics and visualizations are available on a project or group level: + +- Histogram showing the number of Merge Request that took a specified number of days to + merge after creation. Select a specific column to filter down subsequent charts. +- Histogram showing a breakdown of the time taken (in hours) to merge a Merge Request. + The following intervals are available: + - Time from first commit to first comment. + - Time from first comment until last commit. + - Time from last commit to merge. +- Histogram showing the size or complexity of a Merge Request, using the following: + - Number of commits per Merge Request. + - Number of lines of code per commit. + - Number of files touched. +- Table showing list of Merge Requests with their respective times and size metrics. + Can be sorted by the above metrics. + - Users can sort by any of the above metrics + +## Retrieving data + +Users can retrieve three months of data when they deploy Productivity Analytics for the first time. + +To retrieve data for a different time span, run the following in the GitLab directory: + +```sh +MERGED_AT_AFTER = <your_date> rake gitlab:productivity_analytics:recalc +``` + +## Permissions + +The **Productivity Analytics** dashboard can be accessed only: + +- On GitLab instances and namespaces on + [Premium or Silver tier](https://about.gitlab.com/pricing/) and above. +- By users with [Reporter access](../permissions.md) and above. diff --git a/doc/user/application_security/dependency_list/img/dependency_list_v12_2.png b/doc/user/application_security/dependency_list/img/dependency_list_v12_2.png Binary files differnew file mode 100644 index 00000000000..af9cee08d71 --- /dev/null +++ b/doc/user/application_security/dependency_list/img/dependency_list_v12_2.png diff --git a/doc/user/application_security/dependency_list/index.md b/doc/user/application_security/dependency_list/index.md new file mode 100644 index 00000000000..38c38bbd8a9 --- /dev/null +++ b/doc/user/application_security/dependency_list/index.md @@ -0,0 +1,49 @@ +# Dependency List **(ULTIMATE)** + +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/10075) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.0. + +The Dependency list allows you to see your project's dependencies, and key +details about them, including their known vulnerabilities. To see it, +navigate to **Security & Compliance > Dependency List** in your project's +sidebar. + +## Requirements + +1. The [Dependency Scanning](../dependency_scanning/index.md) CI job must be + configured for your project. +1. Your project uses at least one of the + [languages and package managers](../dependency_scanning/index.md#supported-languages-and-package-managers) + supported by Gemnasium. + +## Viewing dependencies + +![Dependency List](img/dependency_list_v12_2.png) + +Dependencies are displayed with the following information: + +| Field | Description | +| --------- | ----------- | +| Status | Displays whether or not the dependency has any known vulnerabilities | +| Component | The dependency's name | +| Version | The exact locked version of the dependency your project uses | +| Packager | The packager used to install the depedency | +| Location | A link to the packager-specific lockfile in your project that declared the dependency | + +Dependencies shown are initially sorted by their names. They can also be sorted +by the packager they were installed by, or by the severity of their known +vulnerabilities. + +There is a second list under the `Vulnerable components` tab displaying only +those dependencies with known vulnerabilities. If there are none, this tab is +disabled. + +### Vulnerabilities + +If a dependency has known vulnerabilities, they can be viewed by clicking on the +`Status` cell of that dependency. The severity and description of each +vulnerability will then be displayed below it. + +## Downloading the Dependency List + +Your project's full list of dependencies and their details can be downloaded in +`JSON` format by clicking on the download button. diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index 10b4d9d4c7c..3148ec63c79 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -327,16 +327,11 @@ Once a vulnerability is found, you can interact with it. Read more on how to For more information about the vulnerabilities database update, check the [maintenance table](../index.md#maintenance-and-update-of-the-vulnerabilities-database). -## Dependency List +## Dependency List **(ULTIMATE)** -> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/10075) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.0. - -An additional benefit of Dependency Scanning is the ability to get a list of your -project's dependencies with their versions. This list can be generated only for -[languages and package managers](#supported-languages-and-package-managers) -supported by Gemnasium. - -To see the generated dependency list, navigate to your project's **Security & Compliance > Dependency List**. +An additional benefit of Dependency Scanning is the ability to view your +project's dependencies and their known vulnerabilities. Read more about +the [Dependency List](../dependency_list/index.md). ## Versioning and release process diff --git a/doc/user/application_security/index.md b/doc/user/application_security/index.md index 4dcb416c110..83ea0ea3386 100644 --- a/doc/user/application_security/index.md +++ b/doc/user/application_security/index.md @@ -25,6 +25,7 @@ GitLab can scan and report any vulnerabilities found in your project. | Secure scanning tool | Description | |:-----------------------------------------------------------------------------|:-----------------------------------------------------------------------| | [Container Scanning](container_scanning/index.md) **(ULTIMATE)** | Scan Docker containers for known vulnerabilities. | +| [Dependency List](dependency_list/index.md) **(ULTIMATE)** | View your project's dependencies and their known vulnerabilities. | | [Dependency Scanning](dependency_scanning/index.md) **(ULTIMATE)** | Analyze your dependencies for known vulnerabilities. | | [Dynamic Application Security Testing (DAST)](dast/index.md) **(ULTIMATE)** | Analyze running web applications for known vulnerabilities. | | [License Management](license_management/index.md) **(ULTIMATE)** | Search your project's dependencies for their licenses. | diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md index 928950126da..c9fbd7effa0 100644 --- a/doc/user/gitlab_com/index.md +++ b/doc/user/gitlab_com/index.md @@ -81,13 +81,12 @@ IP based firewall can be configured by looking up all ## Shared Runners -Shared Runners on GitLab.com run in [autoscale mode] and powered by -Google Cloud Platform. Autoscaling means reduced -waiting times to spin up CI/CD jobs, and isolated VMs for each project, -thus maximizing security. -They're free to use for public open source projects and limited to 2000 CI -minutes per month per group for private projects. Read about all -[GitLab.com plans](https://about.gitlab.com/pricing/). +Shared Runners on GitLab.com run in [autoscale mode] and powered by Google Cloud Platform. +Autoscaling means reduced waiting times to spin up CI/CD jobs, and isolated VMs for each project, +thus maximizing security. They're free to use for public open source projects and limited +to 2000 CI minutes per month per group for private projects. More minutes +[can be purchased](../../subscriptions/index.md#extra-shared-runners-pipeline-minutes), if +needed. Read about all [GitLab.com plans](https://about.gitlab.com/pricing/). All your CI/CD jobs run on [n1-standard-1 instances](https://cloud.google.com/compute/docs/machine-types) with 3.75GB of RAM, CoreOS and the latest Docker Engine installed. Instances provide 1 vCPU and 25GB of HDD disk space. The default @@ -223,7 +222,7 @@ and the following environment variables: | Setting | GitLab.com | Default | |-------- |----------- |-------- | -| `SIDEKIQ_MEMORY_KILLER_MAX_RSS` | `1000000` | `1000000` | +| `SIDEKIQ_MEMORY_KILLER_MAX_RSS` | `1000000` | `2000000` | | `SIDEKIQ_MEMORY_KILLER_SHUTDOWN_SIGNAL` | `SIGKILL` | - | | `SIDEKIQ_LOG_ARGUMENTS` | `1` | - | diff --git a/doc/user/markdown.md b/doc/user/markdown.md index edb1e904f2b..6cfc8b6429b 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -533,6 +533,10 @@ This snippet links to `<wiki_root>/miscellaneous.md`: [Link to Related Page](/miscellaneous.md) ``` +### Embedding metrics in GitLab Flavored Markdown + +Metric charts can be embedded within GitLab Flavored Markdown. See [Embedding Metrics within GitLab flavored Markdown](../user/project/integrations/prometheus.md#embedding-metric-charts-within-gitlab-flavored-markdown) for more details. + ## Standard markdown and extensions in GitLab All standard markdown formatting should work as expected within GitLab. Some standard diff --git a/doc/user/permissions.md b/doc/user/permissions.md index d92435ef724..16684b9f72b 100644 --- a/doc/user/permissions.md +++ b/doc/user/permissions.md @@ -47,6 +47,7 @@ The following table depicts the various user permission levels in a project. | View approved/blacklisted licenses **(ULTIMATE)** | ✓ | ✓ | ✓ | ✓ | ✓ | | View license management reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | View Security reports **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | +| View Dependency list **(ULTIMATE)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | View [Design Management](project/issues/design_management.md) pages **(PREMIUM)** | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | View project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | | Pull project code | ✓ (*1*) | ✓ | ✓ | ✓ | ✓ | @@ -93,7 +94,7 @@ The following table depicts the various user permission levels in a project. | Remove a container registry image | | | ✓ | ✓ | ✓ | | Create/edit/delete project milestones | | | ✓ | ✓ | ✓ | | Use security dashboard **(ULTIMATE)** | | | ✓ | ✓ | ✓ | -| View dependency list **(ULTIMATE)** | | | ✓ | ✓ | ✓ | +| View vulnerabilities in Dependency list **(ULTIMATE)** | | | ✓ | ✓ | ✓ | | Create issue from vulnerability **(ULTIMATE)** | | | ✓ | ✓ | ✓ | | Dismiss vulnerability **(ULTIMATE)** | | | ✓ | ✓ | ✓ | | Apply code change suggestions | | | ✓ | ✓ | ✓ | @@ -232,6 +233,16 @@ nested groups if you have membership in one of its parents. To learn more, read through the documentation on [subgroups memberships](group/subgroups/index.md#membership). +## Guest User + +Create a user and assign to a project with a role as `Guest` user, this user +will be considered as guest user by GitLab and will not take up the license. +There is no specific `Guest` role for newly created users. If this user will +be assigned a higher role to any of the projects and groups then this user will +take a license seat. If a user creates a project this user becomes a maintainer, +therefore, takes up a license seat as well, in order to prevent this you have +to go and edit user profile and mark the user as External. + ## External users permissions In cases where it is desired that a user has access only to some internal or diff --git a/doc/user/profile/personal_access_tokens.md b/doc/user/profile/personal_access_tokens.md index 248188a6457..d556daa3460 100644 --- a/doc/user/profile/personal_access_tokens.md +++ b/doc/user/profile/personal_access_tokens.md @@ -44,7 +44,7 @@ the following table. | Scope | Introduced in | Description | | ------------------ | ------------- | ----------- | | `read_user` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Allows access to the read-only endpoints under `/users`. Essentially, any of the `GET` requests in the [Users API][users] are allowed. | -| `api` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Grants complete access to the API and Container Registry (read/write). | +| `api` | [GitLab 8.15](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5951) | Grants complete read/write access to the API, including all groups and projects, the container registry, and the package registry. | | `read_registry` | [GitLab 9.3](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11845) | Allows to read (pull) [container registry] images if a project is private and authorization is required. | | `sudo` | [GitLab 10.2](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14838) | Allows performing API actions as any user in the system (if the authenticated user is an admin). | | `read_repository` | [GitLab 10.7](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/17894) | Allows read-only access (pull) to the repository through git clone. | diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md index 6707b88c317..424bee6e9f1 100644 --- a/doc/user/project/cycle_analytics.md +++ b/doc/user/project/cycle_analytics.md @@ -1,5 +1,7 @@ # Cycle Analytics +> - [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/12077) at a group level in [GitLab Premium and Silver](https://about.gitlab.com/pricing/) 12.2 (enabled by feature flag `analytics`). + Cycle Analytics measures the time spent to go from an [idea to production] - also known as cycle time - for each of your projects. Cycle Analytics displays the median time for an idea to reach production, along with the time typically spent in each DevOps stage along the way. @@ -13,10 +15,16 @@ calculates a separate median for each stage. ## Overview -You can find the Cycle Analytics page under your project's **Project ➔ Cycle -Analytics** tab. +Cycle Analytics are available at a: + +- Group level from the top navigation bar **Analytics > Cycle Analytics**. **(PREMIUM)** + + In the future, multiple groups will be selectable which will effectively make this an + instance-level feature. + +- Project level from a project's **Project > Cycle Analytics**. -![Cycle Analytics landing page](img/cycle_analytics_landing_page.png) + ![Cycle Analytics landing page](img/cycle_analytics_landing_page.png) There are seven stages that are tracked as part of the Cycle Analytics calculations. @@ -134,7 +142,7 @@ A few notes: ## Permissions -The current permissions on the Cycle Analytics dashboard are: +The current permissions on the Project Cycle Analytics dashboard are: - Public projects - anyone can access - Internal projects - any authenticated user can access @@ -142,6 +150,18 @@ The current permissions on the Cycle Analytics dashboard are: You can [read more about permissions][permissions] in general. +NOTE: **Note:** +As of GitLab 12.2, the project-level page is deprecated. You should access +project-level Cycle Analytics from **Analytics > Cycle Analytics** in the top +navigation bar. We will ensure that the same project-level functionality is available +to CE users in the new analytics space. + +For Cycle Analytics functionality introduced in GitLab 12.2 and later: + +- Users must have Reporter access or above. +- Features are available only on + [Premium or Silver tiers](https://about.gitlab.com/pricing/) and above. + ## More resources Learn more about Cycle Analytics in the following resources: diff --git a/doc/user/project/description_templates.md b/doc/user/project/description_templates.md index 196874fdc86..f53dc056010 100644 --- a/doc/user/project/description_templates.md +++ b/doc/user/project/description_templates.md @@ -55,7 +55,7 @@ changes you made after picking the template and return it to its initial status. ![Description templates](img/description_templates.png) -## Setting a default template for issues and merge requests **(STARTER)** +## Setting a default template for merge requests and issues **(STARTER)** > **Notes:** > @@ -66,20 +66,20 @@ changes you made after picking the template and return it to its initial status. > - Templates for merge requests were [introduced][ee-7478ece] in GitLab EE 6.9. The visibility of issues and/or merge requests should be set to either "Everyone -with access" or "Only Project Members" in your project's **Settings** otherwise the +with access" or "Only Project Members" in your project's **Settings / Visibility, project features, permissions** section, otherwise the template text areas won't show. This is the default behavior so in most cases you should be fine. -Go to your project's **Settings** and fill in the "Default description template -for issues" and "Default description template for merge requests" text areas -for issues and merge requests respectively. Since GitLab issues and merge -request support [Markdown](../markdown.md), you can use special markup like +Go to your project's **Settings** and under the **Merge requests** header, click *Expand* and fill in the "Default description template +for merge requests" text area. Under the **Default issue template**, click *Expand* and fill in "Default description template for issues" text area. Since GitLab merge request and issues + support [Markdown](../markdown.md), you can use special markup like headings, lists, etc. -![Default description templates](img/description_templates_default_settings.png) +![Default merge request description templates](img/description_templates_merge_request_settings.png) +![Default issue description templates](img/description_templates_issue_settings.png) After you add the description, hit **Save changes** for the settings to take -effect. Now, every time a new issue or merge request is created, it will be +effect. Now, every time a new merge request or issue is created, it will be pre-filled with the text you entered in the template(s). ## Description template example diff --git a/doc/user/project/img/description_templates_default_settings.png b/doc/user/project/img/description_templates_default_settings.png Binary files differdeleted file mode 100644 index ab314e83d06..00000000000 --- a/doc/user/project/img/description_templates_default_settings.png +++ /dev/null diff --git a/doc/user/project/img/description_templates_issue_settings.png b/doc/user/project/img/description_templates_issue_settings.png Binary files differnew file mode 100644 index 00000000000..53328108835 --- /dev/null +++ b/doc/user/project/img/description_templates_issue_settings.png diff --git a/doc/user/project/img/description_templates_merge_request_settings.png b/doc/user/project/img/description_templates_merge_request_settings.png Binary files differnew file mode 100644 index 00000000000..eda264f7f37 --- /dev/null +++ b/doc/user/project/img/description_templates_merge_request_settings.png diff --git a/doc/user/project/index.md b/doc/user/project/index.md index 45e96437517..30ff0e9ff07 100644 --- a/doc/user/project/index.md +++ b/doc/user/project/index.md @@ -99,6 +99,7 @@ When you create a project in GitLab, you'll have access to a large number of - [NPM packages](packages/npm_registry.md): your private NPM package registry in GitLab. **(PREMIUM)** - [Code owners](code_owners.md): specify code owners for certain files **(STARTER)** - [License Management](../application_security/license_management/index.md): approve and blacklist licenses for projects. **(ULTIMATE)** +- [Dependency List](../application_security/dependency_list/index.md): view project dependencies. **(ULTIMATE)** ### Project integrations diff --git a/doc/user/project/integrations/img/embed_metrics.png b/doc/user/project/integrations/img/embed_metrics.png Binary files differnew file mode 100644 index 00000000000..6f9660c9aec --- /dev/null +++ b/doc/user/project/integrations/img/embed_metrics.png diff --git a/doc/user/project/integrations/mattermost.md b/doc/user/project/integrations/mattermost.md index ea58a08e127..6e0f39956d3 100644 --- a/doc/user/project/integrations/mattermost.md +++ b/doc/user/project/integrations/mattermost.md @@ -14,7 +14,7 @@ To enable Mattermost integration you must create an incoming webhook integration 1. Save it, copy the **Webhook URL**, we'll need this later for GitLab. There might be some cases that Incoming Webhooks are blocked by admin, ask your mattermost admin to enable -it on `https://mattermost.example/admin_console/integrations/custom`. +it on **Mattermost System Console > Integrations > Integration Management**, or on **Mattermost System Console > Integrations > Custom Integrations** in Mattermost versions 5.11 and earlier. Display name override is not enabled by default, you need to ask your admin to enable it on that same section. diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index 44439b59e77..aa7db97c413 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -354,6 +354,27 @@ Prometheus server. ![Merge Request with Performance Impact](img/merge_request_performance.png) +## Embedding metric charts within Gitlab Flavored Markdown + +> [Introduced][ce-29691] in GitLab 12.2. +> Requires [Kubernetes](prometheus_library/kubernetes.md) metrics. + +It is possible to display metrics charts within [GitLab Flavored Markdown](../../markdown.md#gitlab-flavored-markdown-gfm). + +To display a metric chart, include a link of the form `https://<root_url>/<project>/environments/<environment_id>/metrics`. + +The following requirements must be met for the metric to unfurl: + +- The `<environment_id>` must correspond to a real environment. +- Prometheus must be monitoring the environment. +- The GitLab instance must be configured to receive data from the environment. +- The user must be allowed access to the monitoring dashboard for the environment ([Reporter or higher](../../permissions.md)). +- The dashboard must have data within the last 8 hours. + + If all of the above are true, then the metric will unfurl as seen below: + +![Embedded Metrics](img/embed_metrics.png) + ## Troubleshooting If the "No data found" screen continues to appear, it could be due to: @@ -376,4 +397,5 @@ If the "No data found" screen continues to appear, it could be due to: [ci-environment-slug]: ../../../ci/variables/#predefined-environment-variables [ce-8935]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8935 [ce-10408]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10408 +[ce-29691]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/29691 [promgldocs]: ../../../administration/monitoring/prometheus/index.md diff --git a/doc/user/project/issues/issue_data_and_actions.md b/doc/user/project/issues/issue_data_and_actions.md index 7b031f83cb1..d7d168710ef 100644 --- a/doc/user/project/issues/issue_data_and_actions.md +++ b/doc/user/project/issues/issue_data_and_actions.md @@ -50,7 +50,12 @@ The button to do this has a different label depending on whether the issue is al #### 3. Assignee -An issue can be assigned to yourself, another person, or [many people](#31-multiple-assignees-STARTER). +An issue can be assigned to: + +- Yourself. +- Another person. +- [Many people](#31-multiple-assignees-STARTER). **(STARTER)** + The assignee(s) can be changed as often as needed. The idea is that the assignees are responsible for that issue until it's reassigned to someone else to take it from there. When assigned to someone, it will appear in their assigned issues list. diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md index 7ff30d1b813..4c8bd230b3f 100644 --- a/doc/user/project/merge_requests/index.md +++ b/doc/user/project/merge_requests/index.md @@ -287,6 +287,8 @@ as pushing changes: - Set the target of the merge request to a particular branch. - Set the merge request to merge when its pipeline succeeds. - Set the merge request to remove the source branch when it's merged. +- Set the title of the merge request to a particular title. +- Set the description of the merge request to a particular description. ### Create a new merge request using git push options @@ -332,6 +334,8 @@ git push -o merge_request.create -o merge_request.merge_when_pipeline_succeeds ### Set removing the source branch using git push options +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/64320) in GitLab 12.2. + To set an existing merge request to remove the source branch when the merge request is merged, the `merge_request.remove_source_branch` push option can be used: @@ -345,6 +349,8 @@ You can also use this push option in addition to the ### Set merge request title using git push options +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/64320) in GitLab 12.2. + To set the title of an existing merge request, use the `merge_request.title` push option: @@ -357,6 +363,8 @@ You can also use this push option in addition to the ### Set merge request description using git push options +> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/64320) in GitLab 12.2. + To set the description of an existing merge request, use the `merge_request.description` push option: diff --git a/doc/user/project/merge_requests/merge_request_approvals.md b/doc/user/project/merge_requests/merge_request_approvals.md index 509576c8d6f..5161b25de99 100644 --- a/doc/user/project/merge_requests/merge_request_approvals.md +++ b/doc/user/project/merge_requests/merge_request_approvals.md @@ -229,12 +229,6 @@ The default approval settings can now be overridden when creating a in the **No. approvals required** box. 1. Click **Update approvers**. -There are however some restrictions: - -- The amount of required approvals, if changed, must be greater than the default - set at the project level. This ensures that you're not forced to adjust settings - when someone is unavailable for approval, yet the process is still enforced. - NOTE: **Note:** If you are contributing to a forked project, things are a little different. Read what happens when the @@ -242,14 +236,9 @@ Read what happens when the ## Overriding merge request approvals default settings **(PREMIUM)** -In GitLab Premium, when the approval rules are [set at the project level](#editing-approvals-premium), and -**Can override approvers and approvals required per merge request** is checked, there are a few more -restrictions (compared to [GitLab Starter](#overriding-the-merge-request-approvals-default-settings)): - -- Approval rules can be added to an MR with no restriction. -- For project sourced approval rules, editing and removing approvers is not allowed. -- The approvals required of all approval rules is configurable, but if a rule is backed by a project rule, then it is restricted - to the minimum approvals required set in the project's corresponding rule. +In GitLab Premium, when the approval rules are [set at the project level](#editing-approvals-premium), +and **Can override approvers and approvals required per merge request** is checked, +approval rules can be added to an MR with no restriction. ## Resetting approvals on push diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md index 4e25d8545e9..f3e9c950efd 100644 --- a/doc/user/project/pipelines/schedules.md +++ b/doc/user/project/pipelines/schedules.md @@ -6,7 +6,9 @@ type: reference, howto > - Introduced in GitLab 9.1 as [Trigger Schedule](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10533). > - [Renamed to Pipeline Schedule](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10853) in GitLab 9.2. -> - Cron notation is parsed by [Fugit](https://github.com/floraison/fugit). + +NOTE: **Note:** +Cron notation is parsed by [Fugit](https://github.com/floraison/fugit). Pipelines are normally run based on certain conditions being met. For example, when a branch is pushed to repository. diff --git a/lib/api/discussions.rb b/lib/api/discussions.rb index cc62ce22a1b..6c1acc3963f 100644 --- a/lib/api/discussions.rb +++ b/lib/api/discussions.rb @@ -4,6 +4,7 @@ module API class Discussions < Grape::API include PaginationParams helpers ::API::Helpers::NotesHelpers + helpers ::RendersNotes before { authenticate! } @@ -23,21 +24,15 @@ module API requires :noteable_id, types: [Integer, String], desc: 'The ID of the noteable' use :pagination end - # rubocop: disable CodeReuse/ActiveRecord + get ":id/#{noteables_path}/:noteable_id/discussions" do noteable = find_noteable(parent_type, params[:id], noteable_type, params[:noteable_id]) - notes = noteable.notes - .inc_relations_for_view - .includes(:noteable) - .fresh - - notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } + notes = readable_discussion_notes(noteable) discussions = Kaminari.paginate_array(Discussion.build_collection(notes, noteable)) present paginate(discussions), with: Entities::Discussion end - # rubocop: enable CodeReuse/ActiveRecord desc "Get a single #{noteable_type.to_s.downcase} discussion" do success Entities::Discussion @@ -226,13 +221,24 @@ module API helpers do # rubocop: disable CodeReuse/ActiveRecord - def readable_discussion_notes(noteable, discussion_id) + def readable_discussion_notes(noteable, discussion_id = nil) notes = noteable.notes - .where(discussion_id: discussion_id) + notes = notes.where(discussion_id: discussion_id) if discussion_id + notes = notes .inc_relations_for_view .includes(:noteable) .fresh + # Without RendersActions#prepare_notes_for_rendering, + # Note#cross_reference_not_visible_for? will attempt to render + # Markdown references mentioned in the note to see whether they + # should be redacted. For notes that reference a commit, this + # would also incur a Gitaly call to verify the commit exists. + # + # With prepare_notes_for_rendering, we can avoid Gitaly calls + # because notes are redacted if they point to projects that + # cannot be accessed by the user. + notes = prepare_notes_for_rendering(notes) notes.reject { |n| n.cross_reference_not_visible_for?(current_user) } end # rubocop: enable CodeReuse/ActiveRecord diff --git a/lib/api/settings.rb b/lib/api/settings.rb index 196ef1fcdfa..c36ee5af63f 100644 --- a/lib/api/settings.rb +++ b/lib/api/settings.rb @@ -125,6 +125,12 @@ module API optional :instance_statistics_visibility_private, type: Boolean, desc: 'When set to `true` Instance statistics will only be available to admins' optional :local_markdown_version, type: Integer, desc: "Local markdown version, increase this value when any cached markdown should be invalidated" optional :allow_local_requests_from_hooks_and_services, type: Boolean, desc: 'Deprecated: Use :allow_local_requests_from_web_hooks_and_services instead. Allow requests to the local network from hooks and services.' # support legacy names, can be removed in v5 + optional :snowplow_enabled, type: Grape::API::Boolean, desc: 'Enable Snowplow tracking' + given snowplow_enabled: ->(val) { val } do + requires :snowplow_collector_hostname, type: String, desc: 'The Snowplow collector hostname' + optional :snowplow_cookie_domain, type: String, desc: 'The Snowplow cookie domain' + optional :snowplow_site_id, type: String, desc: 'The Snowplow site name / application ic' + end ApplicationSetting::SUPPORTED_KEY_TYPES.each do |type| optional :"#{type}_key_restriction", diff --git a/lib/api/todos.rb b/lib/api/todos.rb index 7260ecfb5ee..404675bfaec 100644 --- a/lib/api/todos.rb +++ b/lib/api/todos.rb @@ -13,6 +13,13 @@ module API 'issues' => ->(iid) { find_project_issue(iid) } }.freeze + helpers do + # EE::API::Todos would override this method + def find_todos + TodosFinder.new(current_user, params).execute + end + end + params do requires :id, type: String, desc: 'The ID of a project' end @@ -41,10 +48,6 @@ module API resource :todos do helpers do - def find_todos - TodosFinder.new(current_user, params).execute - end - def issuable_and_awardable?(type) obj_type = Object.const_get(type) @@ -107,3 +110,5 @@ module API end end end + +API::Todos.prepend_if_ee('EE::API::Todos') diff --git a/lib/expand_variables.rb b/lib/expand_variables.rb index c83cec9dc4a..45af30f46dc 100644 --- a/lib/expand_variables.rb +++ b/lib/expand_variables.rb @@ -3,6 +3,20 @@ module ExpandVariables class << self def expand(value, variables) + variables_hash = nil + + value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do + variables_hash ||= transform_variables(variables) + variables_hash[$1 || $2] + end + end + + private + + def transform_variables(variables) + # Lazily initialise variables + variables = variables.call if variables.is_a?(Proc) + # Convert hash array to variables if variables.is_a?(Array) variables = variables.reduce({}) do |hash, variable| @@ -11,9 +25,7 @@ module ExpandVariables end end - value.gsub(/\$([a-zA-Z_][a-zA-Z0-9_]*)|\${\g<1>}|%\g<1>%/) do - variables[$1 || $2] - end + variables end end end diff --git a/lib/feature/gitaly.rb b/lib/feature/gitaly.rb index edfd2fb17f3..9ded1aed4e3 100644 --- a/lib/feature/gitaly.rb +++ b/lib/feature/gitaly.rb @@ -7,7 +7,7 @@ class Feature # Server feature flags should use '_' to separate words. SERVER_FEATURE_FLAGS = [ - 'get_commit_signatures'.freeze + # 'get_commit_signatures'.freeze ].freeze DEFAULT_ON_FLAGS = Set.new([]).freeze diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index 2fd76bc3690..29a52b9da17 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -16,8 +16,11 @@ module Gitlab dependencies needs before_script after_script variables environment coverage retry parallel extends].freeze + REQUIRED_BY_NEEDS = %i[stage].freeze + validations do validates :config, allowed_keys: ALLOWED_KEYS + validates :config, required_keys: REQUIRED_BY_NEEDS, if: :has_needs? validates :config, presence: true validates :script, presence: true validates :name, presence: true diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb index 0405292a25b..65029f5ce7f 100644 --- a/lib/gitlab/ci/pipeline/chain/populate.rb +++ b/lib/gitlab/ci/pipeline/chain/populate.rb @@ -23,12 +23,17 @@ module Gitlab @command.seeds_block&.call(pipeline) ## - # Populate pipeline with all stages, and stages with builds. + # Gather all runtime build/stage errors # - pipeline.stage_seeds.each do |stage| - pipeline.stages << stage.to_resource + if seeds_errors = pipeline.stage_seeds.flat_map(&:errors).compact.presence + return error(seeds_errors.join("\n")) end + ## + # Populate pipeline with all stages, and stages with builds. + # + pipeline.stages = pipeline.stage_seeds.map(&:to_resource) + if pipeline.stages.none? return error('No stages / jobs for this pipeline.') end diff --git a/lib/gitlab/ci/pipeline/seed/base.rb b/lib/gitlab/ci/pipeline/seed/base.rb index 1fd3a61017f..e9e22569ae0 100644 --- a/lib/gitlab/ci/pipeline/seed/base.rb +++ b/lib/gitlab/ci/pipeline/seed/base.rb @@ -13,6 +13,10 @@ module Gitlab raise NotImplementedError end + def errors + raise NotImplementedError + end + def to_resource raise NotImplementedError end diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb index ab0d4c38ab6..b0ce7457926 100644 --- a/lib/gitlab/ci/pipeline/seed/build.rb +++ b/lib/gitlab/ci/pipeline/seed/build.rb @@ -9,10 +9,15 @@ module Gitlab delegate :dig, to: :@attributes + # When the `ci_dag_limit_needs` is enabled it uses the lower limit + LOW_NEEDS_LIMIT = 5 + HARD_NEEDS_LIMIT = 50 + def initialize(pipeline, attributes, previous_stages) @pipeline = pipeline @attributes = attributes @previous_stages = previous_stages + @needs_attributes = dig(:needs_attributes) @only = Gitlab::Ci::Build::Policy .fabricate(attributes.delete(:only)) @@ -27,8 +32,15 @@ module Gitlab def included? strong_memoize(:inclusion) do all_of_only? && - none_of_except? && - all_of_needs? + none_of_except? + end + end + + def errors + return unless included? + + strong_memoize(:errors) do + needs_errors end end @@ -48,6 +60,18 @@ module Gitlab @attributes.to_h.dig(:options, :trigger).present? end + def to_resource + strong_memoize(:resource) do + if bridge? + ::Ci::Bridge.new(attributes) + else + ::Ci::Build.new(attributes) + end + end + end + + private + def all_of_only? @only.all? { |spec| spec.satisfied_by?(@pipeline, self) } end @@ -56,24 +80,30 @@ module Gitlab @except.none? { |spec| spec.satisfied_by?(@pipeline, self) } end - def all_of_needs? - return true unless Feature.enabled?(:ci_dag_support, @pipeline.project) - return true if dig(:needs_attributes).nil? + def needs_errors + return if @needs_attributes.nil? + + if @needs_attributes.size > max_needs_allowed + return [ + "#{name}: one job can only need #{max_needs_allowed} others, but you have listed #{@needs_attributes.size}. " \ + "See needs keyword documentation for more details" + ] + end - dig(:needs_attributes).all? do |need| - @previous_stages.any? do |stage| + @needs_attributes.flat_map do |need| + result = @previous_stages.any? do |stage| stage.seeds_names.include?(need[:name]) end - end + + "#{name}: needs '#{need[:name]}'" unless result + end.compact end - def to_resource - strong_memoize(:resource) do - if bridge? - ::Ci::Bridge.new(attributes) - else - ::Ci::Build.new(attributes) - end + def max_needs_allowed + if Feature.enabled?(:ci_dag_limit_needs, @project, default_enabled: true) + LOW_NEEDS_LIMIT + else + HARD_NEEDS_LIMIT end end end diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb index 7c737027445..b600df2f656 100644 --- a/lib/gitlab/ci/pipeline/seed/stage.rb +++ b/lib/gitlab/ci/pipeline/seed/stage.rb @@ -33,6 +33,12 @@ module Gitlab end end + def errors + strong_memoize(:errors) do + seeds.flat_map(&:errors).compact + end + end + def seeds_names strong_memoize(:seeds_names) do seeds.map(&:name).to_set diff --git a/lib/gitlab/config/entry/attributable.rb b/lib/gitlab/config/entry/attributable.rb index 560fe63df0e..87bd257f69a 100644 --- a/lib/gitlab/config/entry/attributable.rb +++ b/lib/gitlab/config/entry/attributable.rb @@ -18,6 +18,10 @@ module Gitlab config[attribute] end + + define_method("has_#{attribute}?") do + config.is_a?(Hash) && config.key?(attribute) + end end end end diff --git a/lib/gitlab/config/entry/validators.rb b/lib/gitlab/config/entry/validators.rb index 6796fcce75f..0289e675c6b 100644 --- a/lib/gitlab/config/entry/validators.rb +++ b/lib/gitlab/config/entry/validators.rb @@ -26,6 +26,17 @@ module Gitlab end end + class RequiredKeysValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + present_keys = options[:in] - value.try(:keys).to_a + + if present_keys.any? + record.errors.add(attribute, "missing required keys: " + + present_keys.join(', ')) + end + end + end + class AllowedValuesValidator < ActiveModel::EachValidator def validate_each(record, attribute, value) unless options[:in].include?(value.to_s) diff --git a/lib/gitlab/danger/helper.rb b/lib/gitlab/danger/helper.rb index c0a12318990..332ca8bf9b8 100644 --- a/lib/gitlab/danger/helper.rb +++ b/lib/gitlab/danger/helper.rb @@ -113,7 +113,7 @@ module Gitlab yarn\.lock )\z}x => :frontend, - %r{\A(ee/)?db/} => :database, + %r{\A(ee/)?db/(?!fixtures)[^/]+} => :database, %r{\A(ee/)?lib/gitlab/(database|background_migration|sql|github_import)(/|\.rb)} => :database, %r{\A(app/models/project_authorization|app/services/users/refresh_authorized_projects_service)(/|\.rb)} => :database, %r{\Arubocop/cop/migration(/|\.rb)} => :database, diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb index 40bda3410e1..37fadb47736 100644 --- a/lib/gitlab/data_builder/push.rb +++ b/lib/gitlab/data_builder/push.rb @@ -60,7 +60,8 @@ module Gitlab # rubocop:disable Metrics/ParameterLists def build( project:, user:, ref:, oldrev: nil, newrev: nil, - commits: [], commits_count: nil, message: nil, push_options: {}) + commits: [], commits_count: nil, message: nil, push_options: {}, + with_changed_files: true) commits = Array(commits) @@ -75,7 +76,7 @@ module Gitlab # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38259 commit_attrs = Gitlab::GitalyClient.allow_n_plus_1_calls do commits_limited.map do |commit| - commit.hook_attrs(with_changed_files: true) + commit.hook_attrs(with_changed_files: with_changed_files) end end diff --git a/lib/gitlab/git_post_receive.rb b/lib/gitlab/git_post_receive.rb index d98b85fecc4..24d752b8a4b 100644 --- a/lib/gitlab/git_post_receive.rb +++ b/lib/gitlab/git_post_receive.rb @@ -27,6 +27,18 @@ module Gitlab end end + def includes_branches? + enum_for(:changes_refs).any? do |_oldrev, _newrev, ref| + Gitlab::Git.branch_ref?(ref) + end + end + + def includes_tags? + enum_for(:changes_refs).any? do |_oldrev, _newrev, ref| + Gitlab::Git.tag_ref?(ref) + end + end + private def deserialize_changes(changes) diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml index 1b7fc5fa10f..bd0f3e70749 100644 --- a/lib/gitlab/import_export/import_export.yml +++ b/lib/gitlab/import_export/import_export.yml @@ -137,6 +137,7 @@ excluded_attributes: - :packages_enabled - :mirror_last_update_at - :mirror_last_successful_update_at + - :emails_disabled namespaces: - :runners_token - :runners_token_encrypted diff --git a/lib/gitlab/kubernetes/helm.rb b/lib/gitlab/kubernetes/helm.rb index 42c4745ff98..6e4286589c1 100644 --- a/lib/gitlab/kubernetes/helm.rb +++ b/lib/gitlab/kubernetes/helm.rb @@ -3,8 +3,8 @@ module Gitlab module Kubernetes module Helm - HELM_VERSION = '2.12.3'.freeze - KUBECTL_VERSION = '1.11.7'.freeze + HELM_VERSION = '2.14.3'.freeze + KUBECTL_VERSION = '1.11.10'.freeze NAMESPACE = 'gitlab-managed-apps'.freeze SERVICE_ACCOUNT = 'tiller'.freeze CLUSTER_ROLE_BINDING = 'tiller-admin'.freeze diff --git a/lib/gitlab/kubernetes/helm/reset_command.rb b/lib/gitlab/kubernetes/helm/reset_command.rb index 37e1d8573ab..a35ffa34c58 100644 --- a/lib/gitlab/kubernetes/helm/reset_command.rb +++ b/lib/gitlab/kubernetes/helm/reset_command.rb @@ -38,9 +38,9 @@ module Gitlab # Tracking this method to be removed here: # https://gitlab.com/gitlab-org/gitlab-ce/issues/52791#note_199374155 def delete_tiller_replicaset - command = %w[kubectl delete replicaset -n gitlab-managed-apps -l name=tiller] + delete_args = %w[replicaset -n gitlab-managed-apps -l name=tiller] - command.shelljoin + Gitlab::Kubernetes::KubectlCmd.delete(*delete_args) end def reset_helm_command diff --git a/lib/gitlab/kubernetes/kubectl_cmd.rb b/lib/gitlab/kubernetes/kubectl_cmd.rb new file mode 100644 index 00000000000..981eb5681dc --- /dev/null +++ b/lib/gitlab/kubernetes/kubectl_cmd.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module Kubernetes + module KubectlCmd + class << self + def delete(*args) + %w(kubectl delete).concat(args).shelljoin + end + + def apply_file(filename, *args) + raise ArgumentError, "filename is not present" unless filename.present? + + %w(kubectl apply -f).concat([filename], args).shelljoin + end + end + end + end +end diff --git a/lib/gitlab/metrics/samplers/puma_sampler.rb b/lib/gitlab/metrics/samplers/puma_sampler.rb index 4e835f37c04..8a24d4f3663 100644 --- a/lib/gitlab/metrics/samplers/puma_sampler.rb +++ b/lib/gitlab/metrics/samplers/puma_sampler.rb @@ -15,7 +15,6 @@ module Gitlab puma_workers: ::Gitlab::Metrics.gauge(:puma_workers, 'Total number of workers'), puma_running_workers: ::Gitlab::Metrics.gauge(:puma_running_workers, 'Number of active workers'), puma_stale_workers: ::Gitlab::Metrics.gauge(:puma_stale_workers, 'Number of stale workers'), - puma_phase: ::Gitlab::Metrics.gauge(:puma_phase, 'Phase number (increased during phased restarts)'), puma_running: ::Gitlab::Metrics.gauge(:puma_running, 'Number of running threads'), puma_queued_connections: ::Gitlab::Metrics.gauge(:puma_queued_connections, 'Number of connections in that worker\'s "todo" set waiting for a worker thread'), puma_active_connections: ::Gitlab::Metrics.gauge(:puma_active_connections, 'Number of threads processing a request'), @@ -54,7 +53,6 @@ module Gitlab last_status = worker['last_status'] labels = { worker: "worker_#{worker['index']}" } - metrics[:puma_phase].set(labels, worker['phase']) set_worker_metrics(last_status, labels) if last_status.present? end end @@ -76,7 +74,6 @@ module Gitlab metrics[:puma_workers].set(labels, stats['workers']) metrics[:puma_running_workers].set(labels, stats['booted_workers']) metrics[:puma_stale_workers].set(labels, stats['old_workers']) - metrics[:puma_phase].set(labels, stats['phase']) end def set_worker_metrics(stats, labels = {}) diff --git a/lib/gitlab/project_template.rb b/lib/gitlab/project_template.rb index dbf469a44c1..fa1d1203842 100644 --- a/lib/gitlab/project_template.rb +++ b/lib/gitlab/project_template.rb @@ -24,6 +24,14 @@ module Gitlab "#{preview}.git" end + def project_path + URI.parse(preview).path.sub(%r{\A/}, '') + end + + def uri_encoded_project_path + ERB::Util.url_encode(project_path) + end + def ==(other) name == other.name && title == other.title end diff --git a/lib/gitlab/snowplow_tracker.rb b/lib/gitlab/snowplow_tracker.rb new file mode 100644 index 00000000000..9f12513e09e --- /dev/null +++ b/lib/gitlab/snowplow_tracker.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'snowplow-tracker' + +module Gitlab + module SnowplowTracker + NAMESPACE = 'cf' + + class << self + def track_event(category, action, label: nil, property: nil, value: nil, context: nil) + tracker&.track_struct_event(category, action, label, property, value, context, Time.now.to_i) + end + + private + + def tracker + return unless enabled? + + @tracker ||= ::SnowplowTracker::Tracker.new(emitter, subject, NAMESPACE, Gitlab::CurrentSettings.snowplow_site_id) + end + + def subject + ::SnowplowTracker::Subject.new + end + + def emitter + ::SnowplowTracker::Emitter.new(Gitlab::CurrentSettings.snowplow_collector_hostname) + end + + def enabled? + Gitlab::CurrentSettings.snowplow_enabled? + end + end + end +end diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb index 7e3a695e52a..038553c5dd7 100644 --- a/lib/gitlab/usage_data.rb +++ b/lib/gitlab/usage_data.rb @@ -142,7 +142,9 @@ module Gitlab Gitlab::UsageDataCounters::WebIdeCounter, Gitlab::UsageDataCounters::NoteCounter, Gitlab::UsageDataCounters::SnippetCounter, - Gitlab::UsageDataCounters::SearchCounter + Gitlab::UsageDataCounters::SearchCounter, + Gitlab::UsageDataCounters::CycleAnalyticsCounter, + Gitlab::UsageDataCounters::SourceCodeCounter ] end diff --git a/lib/gitlab/usage_data_counters/cycle_analytics_counter.rb b/lib/gitlab/usage_data_counters/cycle_analytics_counter.rb new file mode 100644 index 00000000000..1ff4296ef65 --- /dev/null +++ b/lib/gitlab/usage_data_counters/cycle_analytics_counter.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Gitlab::UsageDataCounters + class CycleAnalyticsCounter < BaseCounter + KNOWN_EVENTS = %w[views].freeze + PREFIX = 'cycle_analytics' + end +end diff --git a/lib/gitlab/usage_data_counters/source_code_counter.rb b/lib/gitlab/usage_data_counters/source_code_counter.rb new file mode 100644 index 00000000000..8a1771a7bd1 --- /dev/null +++ b/lib/gitlab/usage_data_counters/source_code_counter.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +module Gitlab::UsageDataCounters + class SourceCodeCounter < BaseCounter + KNOWN_EVENTS = %w[pushes].freeze + PREFIX = 'source_code' + end +end diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake index 8267c235a7f..fdcd34320b1 100644 --- a/lib/tasks/gitlab/update_templates.rake +++ b/lib/tasks/gitlab/update_templates.rake @@ -40,7 +40,6 @@ namespace :gitlab do templates.each do |template| params = { - import_url: template.clone_url, namespace_id: tmp_namespace.id, path: template.name, skip_wiki: true @@ -53,22 +52,46 @@ namespace :gitlab do raise "Failed to create project: #{project.errors.messages}" end - loop do - if project.import_finished? - puts "Import finished for #{template.name}" - break + uri_encoded_project_path = template.uri_encoded_project_path + + # extract a concrete commit for signing off what we actually downloaded + # this way we do the right thing even if the repository gets updated in the meantime + get_commits_response = Gitlab::HTTP.get("https://gitlab.com/api/v4/projects/#{uri_encoded_project_path}/repository/commits", + query: { page: 1, per_page: 1 } + ) + raise "Failed to retrieve latest commit for template '#{template.name}'" unless get_commits_response.success? + + commit_sha = get_commits_response.parsed_response.dig(0, 'id') + + project_archive_uri = "https://gitlab.com/api/v4/projects/#{uri_encoded_project_path}/repository/archive.tar.gz?sha=#{commit_sha}" + commit_message = <<~MSG + Initialized from '#{template.title}' project template + + Template repository: #{template.preview} + Commit SHA: #{commit_sha} + MSG + + Dir.mktmpdir do |tmpdir| + Dir.chdir(tmpdir) do + Gitlab::TaskHelpers.run_command!(['wget', project_archive_uri, '-O', 'archive.tar.gz']) + Gitlab::TaskHelpers.run_command!(['tar', 'xf', 'archive.tar.gz']) + extracted_project_basename = Dir['*/'].first + Dir.chdir(extracted_project_basename) do + Gitlab::TaskHelpers.run_command!(%w(git init)) + Gitlab::TaskHelpers.run_command!(%w(git add .)) + Gitlab::TaskHelpers.run_command!(['git', 'commit', '--author', 'GitLab <root@localhost>', '--message', commit_message]) + + # Hacky workaround to push to the project in a way that works with both GDK and the test environment + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + Gitlab::TaskHelpers.run_command!(['git', 'remote', 'add', 'origin', "file://#{project.repository.raw.path}"]) + end + Gitlab::TaskHelpers.run_command!(['git', 'push', '-u', 'origin', 'master']) + end end - - if project.import_failed? - raise "Failed to import from #{project_params[:import_url]}" - end - - puts "Waiting for the import to finish" - - sleep(5) - project.reset end + project.reset + Projects::ImportExport::ExportService.new(project, admin).execute downloader.call(project.export_file, template.archive_path) diff --git a/lib/tasks/services.rake b/lib/tasks/services.rake index 56b81106c5f..4ec4fdd281f 100644 --- a/lib/tasks/services.rake +++ b/lib/tasks/services.rake @@ -86,7 +86,7 @@ namespace :services do doc_start = Time.now doc_path = File.join(Rails.root, 'doc', 'api', 'services.md') - result = ERB.new(services_template, 0, '>') + result = ERB.new(services_template, trim_mode: '>') .result(OpenStruct.new(services: services).instance_eval { binding }) File.open(doc_path, 'w') do |f| diff --git a/locale/gitlab.pot b/locale/gitlab.pot index d33c62031c4..dd69fa1f8f6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2953,6 +2953,9 @@ msgstr "" msgid "Collapse sidebar" msgstr "" +msgid "Collector hostname" +msgstr "" + msgid "ComboSearch is not defined" msgstr "" @@ -3120,6 +3123,9 @@ msgstr "" msgid "Configure storage path settings." msgstr "" +msgid "Configure the %{link} integration." +msgstr "" + msgid "Configure the way a user creates a new account." msgstr "" @@ -3261,6 +3267,9 @@ msgstr "" msgid "ConvDev Index" msgstr "" +msgid "Cookie domain" +msgstr "" + msgid "Copied" msgstr "" @@ -4253,6 +4262,9 @@ msgstr "" msgid "Enable shared Runners" msgstr "" +msgid "Enable snowplow tracking" +msgstr "" + msgid "Enable two-factor authentication" msgstr "" @@ -5065,6 +5077,9 @@ msgstr "" msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)" msgstr "" +msgid "Forgot your password?" +msgstr "" + msgid "Fork" msgstr "" @@ -5215,6 +5230,9 @@ msgstr "" msgid "GitLab User" msgstr "" +msgid "GitLab member or Email address" +msgstr "" + msgid "GitLab project export" msgstr "" @@ -6419,9 +6437,15 @@ msgstr "" msgid "Last seen" msgstr "" +msgid "Last successful update" +msgstr "" + msgid "Last update" msgstr "" +msgid "Last update attempt" +msgstr "" + msgid "Last updated" msgstr "" @@ -9906,9 +9930,6 @@ msgstr "" msgid "Select branch/tag" msgstr "" -msgid "Select members to invite" -msgstr "" - msgid "Select merge moment" msgstr "" @@ -10280,6 +10301,9 @@ msgstr "" msgid "Similar issues" msgstr "" +msgid "Site ID" +msgstr "" + msgid "Size and domain settings for static websites" msgstr "" @@ -10310,6 +10334,9 @@ msgstr "" msgid "SnippetsEmptyState|They can be either public or private." msgstr "" +msgid "Snowplow" +msgstr "" + msgid "Some email servers do not support overriding the email sender name. Enable this option to include the name of the author of the issue, merge request or comment in the email body instead." msgstr "" @@ -12503,6 +12530,9 @@ msgstr "" msgid "Username is available." msgstr "" +msgid "Username or email" +msgstr "" + msgid "Users" msgstr "" diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index c2b0482d789..94245bbfcba 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -52,13 +52,11 @@ module QA raise NotImplementedError if Runtime::User.ldap_user? && user&.credentials_given? if Runtime::User.ldap_user? - sign_in_using_ldap_credentials + sign_in_using_ldap_credentials(user || Runtime::User) else sign_in_using_gitlab_credentials(user || Runtime::User) end end - - Page::Main::Menu.perform(&:has_personal_area?) end def sign_in_using_admin_credentials @@ -76,6 +74,25 @@ module QA Page::Main::Menu.perform(&:has_personal_area?) end + def sign_in_using_ldap_credentials(user) + # Log out if already logged in + Page::Main::Menu.perform do |menu| + menu.sign_out if menu.has_personal_area?(wait: 0) + end + + using_wait_time 0 do + set_initial_password_if_present + + switch_to_ldap_tab + + fill_element :username_field, user.ldap_username + fill_element :password_field, user.ldap_password + click_element :sign_in_button + end + + Page::Main::Menu.perform(&:has_personal_area?) + end + def self.path '/users/sign_in' end @@ -133,14 +150,6 @@ module QA private - def sign_in_using_ldap_credentials - switch_to_ldap_tab - - fill_element :username_field, Runtime::User.ldap_username - fill_element :password_field, Runtime::User.ldap_password - click_element :sign_in_button - end - def sign_in_using_gitlab_credentials(user) switch_to_sign_in_tab if has_sign_in_tab? switch_to_standard_tab if has_standard_tab? diff --git a/qa/qa/page/project/issue/new.rb b/qa/qa/page/project/issue/new.rb index 0d138417176..65c02801d67 100644 --- a/qa/qa/page/project/issue/new.rb +++ b/qa/qa/page/project/issue/new.rb @@ -6,7 +6,7 @@ module QA module Issue class New < Page::Base view 'app/views/shared/issuable/_form.html.haml' do - element :submit_issue_button, 'form.submit "Submit' # rubocop:disable QA/ElementWithPattern + element :issuable_create_button end view 'app/views/shared/issuable/form/_title.html.haml' do @@ -26,7 +26,7 @@ module QA end def create_new_issue - click_on 'Submit issue' + click_element :issuable_create_button, Page::Project::Issue::Show end end end diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 507dccb52d0..45dad9bc0ae 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -14,7 +14,7 @@ module QA end view 'app/assets/javascripts/notes/components/discussion_filter.vue' do - element :discussion_filter + element :discussion_filter, required: true element :filter_options end diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index 3fe048f752a..838d59b59cb 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -5,7 +5,7 @@ module QA module Project class Menu < Page::Base include SubMenus::Common - + include SubMenus::Project include SubMenus::CiCd include SubMenus::Issues include SubMenus::Operations diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb index 7969de726e4..45ab2396a04 100644 --- a/qa/qa/resource/merge_request.rb +++ b/qa/qa/resource/merge_request.rb @@ -5,7 +5,8 @@ require 'securerandom' module QA module Resource class MergeRequest < Base - attr_accessor :title, + attr_accessor :id, + :title, :description, :source_branch, :target_branch, @@ -74,6 +75,28 @@ module QA page.create_merge_request end end + + def fabricate_via_api! + populate(:target, :source) + super + end + + def api_get_path + "/projects/#{project.id}/merge_requests/#{id}" + end + + def api_post_path + "/projects/#{project.id}/merge_requests" + end + + def api_post_body + { + description: @description, + source_branch: @source_branch, + target_branch: @target_branch, + title: @title + } + end end end end diff --git a/qa/qa/scenario/test/sanity/selectors.rb b/qa/qa/scenario/test/sanity/selectors.rb index b4d70fc191a..632a0f5f2a9 100644 --- a/qa/qa/scenario/test/sanity/selectors.rb +++ b/qa/qa/scenario/test/sanity/selectors.rb @@ -7,11 +7,13 @@ module QA class Selectors < Scenario::Template include Scenario::Bootable - PAGES = [QA::Page].freeze + def pages + @pages ||= [QA::Page] + end def perform(*) - validators = PAGES.map do |pages| - Page::Validator.new(pages) + validators = pages.map do |page| + Page::Validator.new(page) end validators.flat_map(&:errors).tap do |errors| diff --git a/qa/qa/service/omnibus.rb b/qa/qa/service/omnibus.rb index b54fd5628f2..c5cddff56cd 100644 --- a/qa/qa/service/omnibus.rb +++ b/qa/qa/service/omnibus.rb @@ -11,11 +11,12 @@ module QA end def gitlab_ctl(command, input: nil) - if input.nil? - shell "docker exec #{@name} gitlab-ctl #{command}" - else - shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'" - end + docker_exec("gitlab-ctl #{command}", input: input) + end + + def docker_exec(command, input: nil) + command = "#{input} | #{command}" if input + shell "docker exec #{@name} bash -c '#{command}'" end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb index 9e48ee7ca2a..891cef6c420 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb @@ -7,13 +7,18 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - @merge_request = Resource::MergeRequest.fabricate! do |merge_request| + project = Resource::Project.fabricate_via_api! do |project| + project.name = 'project' + end + + @merge_request = Resource::MergeRequest.fabricate_via_api! do |merge_request| + merge_request.project = project merge_request.title = 'This is a merge request' - merge_request.description = 'For downloading patches and diffs' + merge_request.description = '... for downloading patches and diffs' end end - it 'user views merge request email patches' do + it 'views the merge request email patches' do @merge_request.visit! Page::MergeRequest::Show.perform(&:view_email_patches) @@ -22,7 +27,7 @@ module QA expect(page).to have_content('diff --git a/added_file.txt b/added_file.txt') end - it 'user views merge request plain diff' do + it 'views the merge request plain diff' do @merge_request.visit! Page::MergeRequest::Show.perform(&:view_plain_diff) diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index 2fe4e4d9d1f..f6411d8c5ad 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -3,36 +3,29 @@ require 'digest/sha1' module QA - # Failure issue: https://gitlab.com/gitlab-org/quality/nightly/issues/70 - context 'Release', :docker, :quarantine do + context 'Release', :docker do describe 'Git clone using a deploy key' do - def login + before do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - end - - before(:all) do - login @runner_name = "qa-runner-#{Time.now.to_i}" - @project = Resource::Project.fabricate! do |resource| + @project = Resource::Project.fabricate_via_api! do |resource| resource.name = 'deploy-key-clone-project' end @repository_location = @project.repository_ssh_location - Resource::Runner.fabricate! do |resource| + Resource::Runner.fabricate_via_browser_ui! do |resource| resource.project = @project resource.name = @runner_name resource.tags = %w[qa docker] resource.image = 'gitlab/gitlab-runner:ubuntu' end - - Page::Main::Menu.perform(&:sign_out) end - after(:all) do + after do Service::Runner.new(@runner_name).remove! end @@ -46,9 +39,7 @@ module QA it "user sets up a deploy key with #{key_class}(#{bits}) to clone code using pipelines" do key = key_class.new(*bits) - login - - Resource::DeployKey.fabricate! do |resource| + Resource::DeployKey.fabricate_via_browser_ui! do |resource| resource.project = @project resource.title = "deploy key #{key.name}(#{key.bits})" resource.key = key.public_key @@ -56,7 +47,7 @@ module QA deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}" - Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate_via_browser_ui! do |resource| resource.project = @project resource.key = deploy_key_name resource.value = key.private_key diff --git a/qa/qa/vendor/saml_idp/page/login.rb b/qa/qa/vendor/saml_idp/page/login.rb index 9c1f9904a7a..1b8c926532a 100644 --- a/qa/qa/vendor/saml_idp/page/login.rb +++ b/qa/qa/vendor/saml_idp/page/login.rb @@ -12,6 +12,14 @@ module QA fill_in 'password', with: 'user1pass' click_on 'Login' end + + def login_if_required + login if login_required? + end + + def login_required? + page.has_text?('Enter your username and password') + end end end end diff --git a/qa/spec/resource/repository/push_spec.rb b/qa/spec/resource/repository/push_spec.rb index bf3ebce0cfe..2f9e4958ae1 100644 --- a/qa/spec/resource/repository/push_spec.rb +++ b/qa/spec/resource/repository/push_spec.rb @@ -19,7 +19,11 @@ describe QA::Resource::Repository::Push do expect { subject.files = [] }.to raise_error(ArgumentError) end - it 'does not raise if files is an array' do + it 'raises an error if files is not an array of hashes with :name and :content keys' do + expect { subject.files = [{ foo: 'foo' }] }.to raise_error(ArgumentError) + end + + it 'does not raise if files is an array of hashes with :name and :content keys' do expect { subject.files = files }.not_to raise_error end end diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index caf96a213e1..340831aa06d 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -192,6 +192,30 @@ describe QA::Runtime::Env do end end + describe '.knapsack?' do + it 'returns true if KNAPSACK_GENERATE_REPORT is defined' do + stub_env('KNAPSACK_GENERATE_REPORT', 'true') + + expect(described_class.knapsack?).to be_truthy + end + + it 'returns true if KNAPSACK_REPORT_PATH is defined' do + stub_env('KNAPSACK_REPORT_PATH', '/a/path') + + expect(described_class.knapsack?).to be_truthy + end + + it 'returns true if KNAPSACK_TEST_FILE_PATTERN is defined' do + stub_env('KNAPSACK_TEST_FILE_PATTERN', '/a/**/pattern') + + expect(described_class.knapsack?).to be_truthy + end + + it 'returns false if neither KNAPSACK_GENERATE_REPORT nor KNAPSACK_REPORT_PATH nor KNAPSACK_TEST_FILE_PATTERN are defined' do + expect(described_class.knapsack?).to be_falsey + end + end + describe '.require_github_access_token!' do it 'raises ArgumentError if GITHUB_ACCESS_TOKEN is not defined' do stub_env('GITHUB_ACCESS_TOKEN', nil) diff --git a/spec/bin/changelog_spec.rb b/spec/bin/changelog_spec.rb index 84bbe0525e5..c3a5abf2b7a 100644 --- a/spec/bin/changelog_spec.rb +++ b/spec/bin/changelog_spec.rb @@ -15,7 +15,7 @@ describe 'bin/changelog' do allow(entry).to receive(:branch_name).and_return('long-branch-' * 100) file_path = entry.send(:file_path) - expect(file_path.length).to eq(140) + expect(file_path.length).to eq(99) end end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 84bbbac39b0..0b3833e6515 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -641,24 +641,32 @@ describe ApplicationController do end end - it 'does not set a custom header' do + it 'sets a custom header' do get :index, format: :json - expect(response.headers['X-GitLab-Custom-Error']).to be_nil + expect(response.headers['X-GitLab-Custom-Error']).to eq '1' end - end - context 'given a json response for an html request' do - controller do - def index - render json: {}, status: :unprocessable_entity + context 'for html request' do + it 'sets a custom header' do + get :index + + expect(response.headers['X-GitLab-Custom-Error']).to eq '1' end end - it 'does not set a custom header' do - get :index + context 'for 200 response' do + controller do + def index + render json: {}, status: :ok + end + end - expect(response.headers['X-GitLab-Custom-Error']).to be_nil + it 'does not set a custom header' do + get :index, format: :json + + expect(response.headers['X-GitLab-Custom-Error']).to be_nil + end end end end diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb index 2dc97e18113..5e6ceef2517 100644 --- a/spec/controllers/projects/cycle_analytics_controller_spec.rb +++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb @@ -11,6 +11,20 @@ describe Projects::CycleAnalyticsController do project.add_maintainer(user) end + context "counting page views for 'show'" do + it 'increases the counter' do + expect(Gitlab::UsageDataCounters::CycleAnalyticsCounter).to receive(:count).with(:views) + + get(:show, + params: { + namespace_id: project.namespace, + project_id: project + }) + + expect(response).to be_success + end + end + describe 'cycle analytics not set up flag' do context 'with no data' do it 'is true' do diff --git a/spec/controllers/projects/git_http_controller_spec.rb b/spec/controllers/projects/git_http_controller_spec.rb index bf099e8deeb..88fa2236e33 100644 --- a/spec/controllers/projects/git_http_controller_spec.rb +++ b/spec/controllers/projects/git_http_controller_spec.rb @@ -12,4 +12,15 @@ describe Projects::GitHttpController do expect(response.status).to eq(403) end end + + describe 'GET #info_refs' do + it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do + stub_application_setting(enabled_git_access_protocol: 'ssh') + project = create(:project, :public, :repository) + + get :info_refs, params: { service: 'git-upload-pack', namespace_id: project.namespace.to_param, project_id: project.path + '.git' } + + expect(response.status).to eq(401) + end + end end diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb index 8ee3168273f..b958f419a19 100644 --- a/spec/controllers/projects/raw_controller_spec.rb +++ b/spec/controllers/projects/raw_controller_spec.rb @@ -60,7 +60,7 @@ describe Projects::RawController do execute_raw_requests(requests: 6, project: project, file_path: file_path) expect(flash[:alert]).to eq('You cannot access the raw file. Please wait a minute.') - expect(response).to redirect_to(project_blob_path(project, file_path)) + expect(response).to have_gitlab_http_status(429) end it 'logs the event on auth.log' do @@ -92,7 +92,7 @@ describe Projects::RawController do execute_raw_requests(requests: 3, project: project, file_path: modified_path) expect(flash[:alert]).to eq('You cannot access the raw file. Please wait a minute.') - expect(response).to redirect_to(project_blob_path(project, modified_path)) + expect(response).to have_gitlab_http_status(429) end end @@ -120,7 +120,7 @@ describe Projects::RawController do execute_raw_requests(requests: 6, project: project, file_path: file_path) expect(flash[:alert]).to eq('You cannot access the raw file. Please wait a minute.') - expect(response).to redirect_to(project_blob_path(project, file_path)) + expect(response).to have_gitlab_http_status(429) # Accessing upcase version of readme file_path = "#{commit_sha}/README.md" diff --git a/spec/controllers/registrations_controller_spec.rb b/spec/controllers/registrations_controller_spec.rb index faf3c990cb2..d05482f095e 100644 --- a/spec/controllers/registrations_controller_spec.rb +++ b/spec/controllers/registrations_controller_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' describe RegistrationsController do include TermsHelper + before do + stub_feature_flags(invisible_captcha: false) + end + describe '#create' do let(:base_user_params) { { name: 'new_user', username: 'new_username', email: 'new@user.com', password: 'Any_password' } } let(:user_params) { { user: base_user_params } } @@ -88,6 +92,88 @@ describe RegistrationsController do end end + context 'when invisible captcha is enabled' do + before do + stub_feature_flags(invisible_captcha: true) + InvisibleCaptcha.timestamp_threshold = treshold + end + + let(:treshold) { 4 } + let(:session_params) { { invisible_captcha_timestamp: form_rendered_time.iso8601 } } + let(:form_rendered_time) { Time.current } + let(:submit_time) { form_rendered_time + treshold } + let(:auth_log_attributes) do + { + message: auth_log_message, + env: :invisible_captcha_signup_bot_detected, + ip: '0.0.0.0', + request_method: 'POST', + fullpath: '/users' + } + end + + describe 'the honeypot has not been filled and the signup form has not been submitted too quickly' do + it 'creates an account' do + travel_to(submit_time) do + expect { post(:create, params: user_params, session: session_params) }.to change(User, :count).by(1) + end + end + end + + describe 'honeypot spam detection' do + let(:user_params) { super().merge(firstname: 'Roy', lastname: 'Batty') } + let(:auth_log_message) { 'Invisible_Captcha_Honeypot_Request' } + + it 'logs the request, refuses to create an account and renders an empty body' do + travel_to(submit_time) do + expect(Gitlab::Metrics).to receive(:counter) + .with(:bot_blocked_by_invisible_captcha_honeypot, 'Counter of blocked sign up attempts with filled honeypot') + .and_call_original + expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once + expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count) + expect(response).to have_gitlab_http_status(200) + expect(response.body).to be_empty + end + end + end + + describe 'timestamp spam detection' do + let(:auth_log_message) { 'Invisible_Captcha_Timestamp_Request' } + + context 'the sign up form has been submitted without the invisible_captcha_timestamp parameter' do + let(:session_params) { nil } + + it 'logs the request, refuses to create an account and displays a flash alert' do + travel_to(submit_time) do + expect(Gitlab::Metrics).to receive(:counter) + .with(:bot_blocked_by_invisible_captcha_timestamp, 'Counter of blocked sign up attempts with invalid timestamp') + .and_call_original + expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once + expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count) + expect(response).to redirect_to(new_user_session_path) + expect(flash[:alert]).to include 'That was a bit too quick! Please resubmit.' + end + end + end + + context 'the sign up form has been submitted too quickly' do + let(:submit_time) { form_rendered_time } + + it 'logs the request, refuses to create an account and displays a flash alert' do + travel_to(submit_time) do + expect(Gitlab::Metrics).to receive(:counter) + .with(:bot_blocked_by_invisible_captcha_timestamp, 'Counter of blocked sign up attempts with invalid timestamp') + .and_call_original + expect(Gitlab::AuthLogger).to receive(:error).with(auth_log_attributes).once + expect { post(:create, params: user_params, session: session_params) }.not_to change(User, :count) + expect(response).to redirect_to(new_user_session_path) + expect(flash[:alert]).to include 'That was a bit too quick! Please resubmit.' + end + end + end + end + end + context 'when terms are enforced' do before do enforce_terms diff --git a/spec/factories/services.rb b/spec/factories/services.rb index f3e662ad4f5..b2d6ada91fa 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -16,6 +16,19 @@ FactoryBot.define do ) end + factory :emails_on_push_service do + project + type 'EmailsOnPushService' + active true + push_events true + tag_push_events true + properties( + recipients: 'test@example.com', + disable_diffs: true, + send_from_committer_email: true + ) + end + factory :mock_deployment_service do project type 'MockDeploymentService' diff --git a/spec/features/invites_spec.rb b/spec/features/invites_spec.rb index 855cf22642e..832c4a57aa3 100644 --- a/spec/features/invites_spec.rb +++ b/spec/features/invites_spec.rb @@ -10,6 +10,7 @@ describe 'Invites' do let(:group_invite) { group.group_members.invite.last } before do + stub_feature_flags(invisible_captcha: false) project.add_maintainer(owner) group.add_user(owner, Gitlab::Access::OWNER) group.add_developer('user@example.com', owner) diff --git a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb index 5e52c82a234..4dbdea02e27 100644 --- a/spec/features/profiles/user_visits_profile_preferences_page_spec.rb +++ b/spec/features/profiles/user_visits_profile_preferences_page_spec.rb @@ -38,7 +38,7 @@ describe 'User visits the profile preferences page' do describe 'User changes their default dashboard', :js do it 'creates a flash message' do - select 'Starred Projects', from: 'user_dashboard' + select2('stars', from: '#user_dashboard') click_button 'Save' wait_for_requests @@ -47,7 +47,7 @@ describe 'User visits the profile preferences page' do end it 'updates their preference' do - select 'Starred Projects', from: 'user_dashboard' + select2('stars', from: '#user_dashboard') click_button 'Save' wait_for_requests diff --git a/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb b/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb new file mode 100644 index 00000000000..6d587053b4f --- /dev/null +++ b/spec/features/projects/raw/user_interacts_with_raw_endpoint_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe 'Projects > Raw > User interacts with raw endpoint' do + include RepoHelpers + + let(:user) { create(:user) } + let(:project) { create(:project, :repository, :public) } + let(:file_path) { 'master/README.md' } + + before do + stub_application_setting(raw_blob_request_limit: 3) + project.add_developer(user) + create_file_in_repo(project, 'master', 'master', 'README.md', 'readme content') + + sign_in(user) + end + + context 'when user access a raw file' do + it 'renders the page successfully' do + visit project_raw_url(project, file_path) + + expect(source).to eq('') # Body is filled in by gitlab-workhorse + end + end + + context 'when user goes over the rate requests limit' do + it 'returns too many requests' do + 4.times do + visit project_raw_url(project, file_path) + end + + expect(source).to have_content('You are being redirected') + click_link('redirected') + expect(page).to have_content('You cannot access the raw file. Please wait a minute.') + end + end +end diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb index dac8c8e7a29..1d8c9e7e426 100644 --- a/spec/features/users/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -95,6 +95,42 @@ describe 'Login' do end end + describe 'with an unconfirmed email address' do + let!(:user) { create(:user, confirmed_at: nil) } + let(:grace_period) { 2.days } + + before do + stub_application_setting(send_user_confirmation_email: true) + allow(User).to receive(:allow_unconfirmed_access_for).and_return grace_period + end + + context 'within the grace period' do + it 'allows to login' do + expect(authentication_metrics).to increment(:user_authenticated_counter) + + gitlab_sign_in(user) + + expect(page).not_to have_content('You have to confirm your email address before continuing.') + expect(page).not_to have_link('Resend confirmation email', href: new_user_confirmation_path) + end + end + + context 'when the confirmation grace period is expired' do + it 'prevents the user from logging in and renders a resend confirmation email link' do + travel_to((grace_period + 1.day).from_now) do + expect(authentication_metrics) + .to increment(:user_unauthenticated_counter) + .and increment(:user_session_destroyed_counter).twice + + gitlab_sign_in(user) + + expect(page).to have_content('You have to confirm your email address before continuing.') + expect(page).to have_link('Resend confirmation email', href: new_user_confirmation_path) + end + end + end + end + describe 'with the ghost user' do it 'disallows login' do expect(authentication_metrics) diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb index f5897bffaf0..cf57fafc4f5 100644 --- a/spec/features/users/signup_spec.rb +++ b/spec/features/users/signup_spec.rb @@ -5,6 +5,10 @@ require 'spec_helper' describe 'Signup' do include TermsHelper + before do + stub_feature_flags(invisible_captcha: false) + end + let(:new_user) { build_stubbed(:user) } describe 'username validation', :js do diff --git a/spec/javascripts/environments/confirm_rollback_modal_spec.js b/spec/frontend/environments/confirm_rollback_modal_spec.js index 05715bce38f..a1a22274e8f 100644 --- a/spec/javascripts/environments/confirm_rollback_modal_spec.js +++ b/spec/frontend/environments/confirm_rollback_modal_spec.js @@ -61,7 +61,7 @@ describe('Confirm Rollback Modal Component', () => { environment, }, }); - const eventHubSpy = spyOn(eventHub, '$emit'); + const eventHubSpy = jest.spyOn(eventHub, '$emit'); const modal = component.find(GlModal); modal.vm.$emit('ok'); diff --git a/spec/javascripts/environments/environment_rollback_spec.js b/spec/frontend/environments/environment_rollback_spec.js index 8c47f6a12c0..fb62a096c3d 100644 --- a/spec/javascripts/environments/environment_rollback_spec.js +++ b/spec/frontend/environments/environment_rollback_spec.js @@ -1,46 +1,38 @@ -import Vue from 'vue'; -import { shallowMount } from '@vue/test-utils'; +import { shallowMount, mount } from '@vue/test-utils'; import { GlButton } from '@gitlab/ui'; import eventHub from '~/environments/event_hub'; -import rollbackComp from '~/environments/components/environment_rollback.vue'; +import RollbackComponent from '~/environments/components/environment_rollback.vue'; describe('Rollback Component', () => { const retryUrl = 'https://gitlab.com/retry'; - let RollbackComponent; - - beforeEach(() => { - RollbackComponent = Vue.extend(rollbackComp); - }); it('Should render Re-deploy label when isLastDeployment is true', () => { - const component = new RollbackComponent({ - el: document.querySelector('.test-dom-element'), + const wrapper = mount(RollbackComponent, { propsData: { retryUrl, isLastDeployment: true, environment: {}, }, - }).$mount(); + }); - expect(component.$el).toHaveSpriteIcon('repeat'); + expect(wrapper.element).toHaveSpriteIcon('repeat'); }); it('Should render Rollback label when isLastDeployment is false', () => { - const component = new RollbackComponent({ - el: document.querySelector('.test-dom-element'), + const wrapper = mount(RollbackComponent, { propsData: { retryUrl, isLastDeployment: false, environment: {}, }, - }).$mount(); + }); - expect(component.$el).toHaveSpriteIcon('redo'); + expect(wrapper.element).toHaveSpriteIcon('redo'); }); it('should emit a "rollback" event on button click', () => { - const eventHubSpy = spyOn(eventHub, '$emit'); - const component = shallowMount(RollbackComponent, { + const eventHubSpy = jest.spyOn(eventHub, '$emit'); + const wrapper = shallowMount(RollbackComponent, { propsData: { retryUrl, environment: { @@ -48,7 +40,7 @@ describe('Rollback Component', () => { }, }, }); - const button = component.find(GlButton); + const button = wrapper.find(GlButton); button.vm.$emit('click'); diff --git a/spec/frontend/matchers.js b/spec/frontend/matchers.js new file mode 100644 index 00000000000..35c362d0bf5 --- /dev/null +++ b/spec/frontend/matchers.js @@ -0,0 +1,38 @@ +export default { + toHaveSpriteIcon: (element, iconName) => { + if (!iconName) { + throw new Error('toHaveSpriteIcon is missing iconName argument!'); + } + + if (!(element instanceof HTMLElement)) { + throw new Error(`${element} is not a DOM element!`); + } + + const iconReferences = [].slice.apply(element.querySelectorAll('svg use')); + const matchingIcon = iconReferences.find(reference => + reference.getAttribute('xlink:href').endsWith(`#${iconName}`), + ); + + const pass = Boolean(matchingIcon); + + let message; + if (pass) { + message = `${element.outerHTML} contains the sprite icon "${iconName}"!`; + } else { + message = `${element.outerHTML} does not contain the sprite icon "${iconName}"!`; + + const existingIcons = iconReferences.map(reference => { + const iconUrl = reference.getAttribute('xlink:href'); + return `"${iconUrl.replace(/^.+#/, '')}"`; + }); + if (existingIcons.length > 0) { + message += ` (only found ${existingIcons.join(',')})`; + } + } + + return { + pass, + message: () => message, + }; + }, +}; diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js index 8b6f7802b15..df8a625319b 100644 --- a/spec/frontend/test_setup.js +++ b/spec/frontend/test_setup.js @@ -6,6 +6,7 @@ import { config as testUtilsConfig } from '@vue/test-utils'; import { initializeTestTimeout } from './helpers/timeout'; import { loadHTMLFixture, setHTMLFixture } from './helpers/fixtures'; import { setupManualMocks } from './mocks/mocks_helper'; +import customMatchers from './matchers'; // Expose jQuery so specs using jQuery plugins can be imported nicely. // Here is an issue to explore better alternatives: @@ -67,6 +68,8 @@ Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => { }); }); +expect.extend(customMatchers); + // Tech debt issue TBD testUtilsConfig.logModifiedComponents = false; diff --git a/spec/helpers/sessions_helper_spec.rb b/spec/helpers/sessions_helper_spec.rb new file mode 100644 index 00000000000..647771ace92 --- /dev/null +++ b/spec/helpers/sessions_helper_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe SessionsHelper do + describe '#unconfirmed_email?' do + it 'returns true when the flash alert contains a devise failure unconfirmed message' do + flash[:alert] = t(:unconfirmed, scope: [:devise, :failure]) + expect(helper.unconfirmed_email?).to be_truthy + end + + it 'returns false when the flash alert does not contain a devise failure unconfirmed message' do + flash[:alert] = 'something else' + expect(helper.unconfirmed_email?).to be_falsey + end + end +end diff --git a/spec/helpers/tracking_helper_spec.rb b/spec/helpers/tracking_helper_spec.rb index 71505e8ea69..b0c98be4130 100644 --- a/spec/helpers/tracking_helper_spec.rb +++ b/spec/helpers/tracking_helper_spec.rb @@ -4,8 +4,32 @@ require 'spec_helper' describe TrackingHelper do describe '#tracking_attrs' do - it 'returns an empty hash' do - expect(helper.tracking_attrs('a', 'b', 'c')).to eq({}) + using RSpec::Parameterized::TableSyntax + + let(:input) { %w(a b c) } + let(:results) do + { + no_data: {}, + with_data: { data: { track_label: 'a', track_event: 'b', track_property: 'c' } } + } + end + + where(:snowplow_enabled, :environment, :result) do + true | 'production' | :with_data + false | 'production' | :no_data + true | 'development' | :no_data + false | 'development' | :no_data + true | 'test' | :no_data + false | 'test' | :no_data + end + + with_them do + it 'returns a hash' do + stub_application_setting(snowplow_enabled: snowplow_enabled) + allow(Rails).to receive(:env).and_return(environment.inquiry) + + expect(helper.tracking_attrs(*input)).to eq(results[result]) + end end end end diff --git a/spec/javascripts/monitoring/charts/area_spec.js b/spec/javascripts/monitoring/charts/area_spec.js index 4541119dd2e..57f99a09002 100644 --- a/spec/javascripts/monitoring/charts/area_spec.js +++ b/spec/javascripts/monitoring/charts/area_spec.js @@ -24,7 +24,6 @@ describe('Area component', () => { store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data); store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData); - store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: true }); [mockGraphData] = store.state.monitoringDashboard.groups[0].metrics; areaChart = shallowMount(Area, { @@ -109,16 +108,6 @@ describe('Area component', () => { }); }); - describe('when exportMetricsToCsvEnabled is disabled', () => { - beforeEach(() => { - store.dispatch('monitoringDashboard/setFeatureFlags', { exportMetricsToCsvEnabled: false }); - }); - - it('does not render the Download CSV button', () => { - expect(areaChart.contains('glbutton-stub')).toBe(false); - }); - }); - describe('methods', () => { describe('formatTooltipText', () => { const mockDate = deploymentData[0].created_at; @@ -264,23 +253,5 @@ describe('Area component', () => { expect(areaChart.vm.yAxisLabel).toBe('CPU'); }); }); - - describe('csvText', () => { - it('converts data from json to csv', () => { - const header = `timestamp,${mockGraphData.y_label}`; - const data = mockGraphData.queries[0].result[0].values; - const firstRow = `${data[0][0]},${data[0][1]}`; - - expect(areaChart.vm.csvText).toMatch(`^${header}\r\n${firstRow}`); - }); - }); - - describe('downloadLink', () => { - it('produces a link to download metrics as csv', () => { - const link = areaChart.vm.downloadLink; - - expect(link).toContain('blob:'); - }); - }); }); }); diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js index 36f650d5933..b78896c45fc 100644 --- a/spec/javascripts/monitoring/dashboard_spec.js +++ b/spec/javascripts/monitoring/dashboard_spec.js @@ -5,7 +5,7 @@ import { timeWindows, timeWindowsKeyNames } from '~/monitoring/constants'; import * as types from '~/monitoring/stores/mutation_types'; import { createStore } from '~/monitoring/stores'; import axios from '~/lib/utils/axios_utils'; -import { +import MonitoringMock, { metricsGroupsAPIResponse, mockApiEndpoint, environmentData, @@ -40,6 +40,7 @@ describe('Dashboard', () => { let mock; let store; let component; + let mockGraphData; beforeEach(() => { setFixtures(` @@ -482,4 +483,36 @@ describe('Dashboard', () => { }); }); }); + + describe('when downloading metrics data as CSV', () => { + beforeEach(() => { + component = new DashboardComponent({ + propsData: { + ...propsData, + }, + store, + }); + store.commit( + `monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, + MonitoringMock.data, + ); + [mockGraphData] = component.$store.state.monitoringDashboard.groups[0].metrics; + }); + + describe('csvText', () => { + it('converts metrics data from json to csv', () => { + const header = `timestamp,${mockGraphData.y_label}`; + const data = mockGraphData.queries[0].result[0].values; + const firstRow = `${data[0][0]},${data[0][1]}`; + + expect(component.csvText(mockGraphData)).toMatch(`^${header}\r\n${firstRow}`); + }); + }); + + describe('downloadCsv', () => { + it('produces a link with a Blob', () => { + expect(component.downloadCsv(mockGraphData)).toContain(`blob:`); + }); + }); + }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index ba3ba01944d..53e1f077610 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -236,24 +236,26 @@ describe('ReadyToMerge', () => { }); }); - describe('shouldShowMergeOptionsDropdown', () => { - it('should return false when no auto merge strategies are available', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + describe('shouldShowMergeImmediatelyDropdown', () => { + it('should return false if no pipeline is active', () => { + Vue.set(vm.mr, 'isPipelineActive', false); + Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false); - expect(vm.shouldShowMergeOptionsDropdown).toBe(false); + expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false); }); - it('should return true when at least one auto merge strategy is available', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]); + it('should return false if "Pipelines must succeed" is enabled for the current project', () => { + Vue.set(vm.mr, 'isPipelineActive', true); + Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true); - expect(vm.shouldShowMergeOptionsDropdown).toBe(true); + expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false); }); - it('should return false when pipeline active but only merge when pipeline succeeds set in project options', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', [ATMTWPS_MERGE_STRATEGY]); - Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true); + it('should return true if the MR\'s pipeline is active and "Pipelines must succeed" is not enabled for the current project', () => { + Vue.set(vm.mr, 'isPipelineActive', true); + Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false); - expect(vm.shouldShowMergeOptionsDropdown).toBe(false); + expect(vm.shouldShowMergeImmediatelyDropdown).toBe(true); }); }); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 253413ae43e..a55d5537df7 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -233,6 +233,8 @@ export default { 'http://localhost:3000/root/acets-app/commit/53027d060246c8f47e4a9310fb332aa52f221775', troubleshooting_docs_path: 'help', merge_request_pipelines_docs_path: '/help/ci/merge_request_pipelines/index.md', + merge_train_when_pipeline_succeeds_docs_path: + '/help/ci/merge_request_pipelines/pipelines_for_merged_results/merge_trains/#startadd-to-merge-train-when-pipeline-succeeds', squash: true, visual_review_app_available: true, merge_trains_enabled: true, diff --git a/spec/lib/expand_variables_spec.rb b/spec/lib/expand_variables_spec.rb index 099d7b6b67c..394efa85701 100644 --- a/spec/lib/expand_variables_spec.rb +++ b/spec/lib/expand_variables_spec.rb @@ -4,62 +4,131 @@ require 'spec_helper' describe ExpandVariables do describe '#expand' do - subject { described_class.expand(value, variables) } + context 'table tests' do + using RSpec::Parameterized::TableSyntax - tests = [ - { value: 'key', - result: 'key', - variables: [] }, - { value: 'key$variable', - result: 'key', - variables: [] }, - { value: 'key$variable', - result: 'keyvalue', - variables: [ - { key: 'variable', value: 'value' } - ] }, - { value: 'key${variable}', - result: 'keyvalue', - variables: [ - { key: 'variable', value: 'value' } - ] }, - { value: 'key$variable$variable2', - result: 'keyvalueresult', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] }, - { value: 'key${variable}${variable2}', - result: 'keyvalueresult', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] }, - { value: 'key$variable2$variable', - result: 'keyresultvalue', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] }, - { value: 'key${variable2}${variable}', - result: 'keyresultvalue', - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ] }, - { value: 'review/$CI_COMMIT_REF_NAME', - result: 'review/feature/add-review-apps', - variables: [ - { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' } - ] } - ] + where do + { + "no expansion": { + value: 'key', + result: 'key', + variables: [] + }, + "missing variable": { + value: 'key$variable', + result: 'key', + variables: [] + }, + "simple expansion": { + value: 'key$variable', + result: 'keyvalue', + variables: [ + { key: 'variable', value: 'value' } + ] + }, + "simple with hash of variables": { + value: 'key$variable', + result: 'keyvalue', + variables: { + 'variable' => 'value' + } + }, + "complex expansion": { + value: 'key${variable}', + result: 'keyvalue', + variables: [ + { key: 'variable', value: 'value' } + ] + }, + "simple expansions": { + value: 'key$variable$variable2', + result: 'keyvalueresult', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "complex expansions": { + value: 'key${variable}${variable2}', + result: 'keyvalueresult', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "complex expansions with missing variable": { + value: 'key${variable}${variable2}', + result: 'keyvalue', + variables: [ + { key: 'variable', value: 'value' } + ] + }, + "out-of-order expansion": { + value: 'key$variable2$variable', + result: 'keyresultvalue', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "out-of-order complex expansion": { + value: 'key${variable2}${variable}', + result: 'keyresultvalue', + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ] + }, + "review-apps expansion": { + value: 'review/$CI_COMMIT_REF_NAME', + result: 'review/feature/add-review-apps', + variables: [ + { key: 'CI_COMMIT_REF_NAME', value: 'feature/add-review-apps' } + ] + }, + "do not lazily access variables when no expansion": { + value: 'key', + result: 'key', + variables: -> { raise NotImplementedError } + }, + "lazily access variables": { + value: 'key$variable', + result: 'keyvalue', + variables: -> { [{ key: 'variable', value: 'value' }] } + } + } + end + + with_them do + subject { ExpandVariables.expand(value, variables) } # rubocop:disable RSpec/DescribedClass + + it { is_expected.to eq(result) } + end + end + + context 'lazily inits variables' do + let(:variables) { -> { [{ key: 'variable', value: 'result' }] } } + + subject { described_class.expand(value, variables) } + + context 'when expanding variable' do + let(:value) { 'key$variable$variable2' } + + it 'calls block at most once' do + expect(variables).to receive(:call).once.and_call_original + + is_expected.to eq('keyresult') + end + end + + context 'when no expansion is needed' do + let(:value) { 'key' } - tests.each do |test| - context "#{test[:value]} resolves to #{test[:result]}" do - let(:value) { test[:value] } - let(:variables) { test[:variables] } + it 'does not call block' do + expect(variables).not_to receive(:call) - it { is_expected.to eq(test[:result]) } + is_expected.to eq('key') + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 800ef122203..415ade7a096 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -89,14 +89,23 @@ describe Gitlab::Ci::Config::Entry::Job do context 'when has needs' do let(:config) do - { script: 'echo', needs: ['another-job'] } + { + stage: 'test', + script: 'echo', + needs: ['another-job'] + } end it { expect(entry).to be_valid } context 'when has dependencies' do let(:config) do - { script: 'echo', dependencies: ['another-job'], needs: ['another-job'] } + { + stage: 'test', + script: 'echo', + dependencies: ['another-job'], + needs: ['another-job'] + } end it { expect(entry).to be_valid } @@ -256,7 +265,11 @@ describe Gitlab::Ci::Config::Entry::Job do context 'when has needs' do context 'that are not a array of strings' do let(:config) do - { script: 'echo', needs: 'build-job' } + { + stage: 'test', + script: 'echo', + needs: 'build-job' + } end it 'returns error about invalid type' do @@ -267,7 +280,12 @@ describe Gitlab::Ci::Config::Entry::Job do context 'when have dependencies that are not subset of needs' do let(:config) do - { script: 'echo', dependencies: ['another-job'], needs: ['build-job'] } + { + stage: 'test', + script: 'echo', + dependencies: ['another-job'], + needs: ['build-job'] + } end it 'returns error about invalid data' do @@ -275,6 +293,20 @@ describe Gitlab::Ci::Config::Entry::Job do expect(entry.errors).to include 'job dependencies the another-job should be part of needs' end end + + context 'when stage: is missing' do + let(:config) do + { + script: 'echo', + needs: ['build-job'] + } + end + + it 'returns error about invalid data' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'job config missing required keys: stage' + end + end end end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 762025f9bd9..5d4dec5899a 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -386,17 +386,28 @@ describe Gitlab::Ci::Pipeline::Seed::Build do describe 'applying needs: dependency' do subject { seed_build } + let(:needs_count) { 1 } + + let(:needs_attributes) do + Array.new(needs_count, name: 'build') + end + let(:attributes) do { name: 'rspec', - needs_attributes: [{ - name: 'build' - }] + needs_attributes: needs_attributes } end context 'when build job is not present in prior stages' do - it { is_expected.not_to be_included } + it "is included" do + is_expected.to be_included + end + + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: needs 'build'") + end end context 'when build job is part of prior stages' do @@ -414,7 +425,39 @@ describe Gitlab::Ci::Pipeline::Seed::Build do let(:previous_stages) { [stage_seed] } - it { is_expected.to be_included } + it "is included" do + is_expected.to be_included + end + + it "does not have errors" do + expect(subject.errors).to be_empty + end + end + + context 'when lower limit of needs is reached' do + before do + stub_feature_flags(ci_dag_limit_needs: true) + end + + let(:needs_count) { described_class::LOW_NEEDS_LIMIT + 1 } + + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 5 others, but you have listed 6. See needs keyword documentation for more details") + end + end + + context 'when upper limit of needs is reached' do + before do + stub_feature_flags(ci_dag_limit_needs: false) + end + + let(:needs_count) { described_class::HARD_NEEDS_LIMIT + 1 } + + it "returns an error" do + expect(subject.errors).to contain_exactly( + "rspec: one job can only need 50 others, but you have listed 51. See needs keyword documentation for more details") + end end end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb index 6fba9f37d91..a13335f63d5 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb @@ -121,6 +121,16 @@ describe Gitlab::Ci::Pipeline::Seed::Stage do end end + describe '#seeds_errors' do + it 'returns all errors from seeds' do + expect(subject.seeds.first) + .to receive(:errors) { ["build error"] } + + expect(subject.errors).to contain_exactly( + "build error") + end + end + describe '#to_resource' do it 'builds a valid stage object with all builds' do subject.to_resource.save! diff --git a/spec/lib/gitlab/config/entry/attributable_spec.rb b/spec/lib/gitlab/config/entry/attributable_spec.rb index 82614bb8238..6b548d5c4a8 100644 --- a/spec/lib/gitlab/config/entry/attributable_spec.rb +++ b/spec/lib/gitlab/config/entry/attributable_spec.rb @@ -25,7 +25,9 @@ describe Gitlab::Config::Entry::Attributable do end it 'returns the value of config' do + expect(instance).to have_name expect(instance.name).to eq 'some name' + expect(instance).to have_test expect(instance.test).to eq 'some test' end @@ -42,6 +44,7 @@ describe Gitlab::Config::Entry::Attributable do end it 'returns nil' do + expect(instance).not_to have_test expect(instance.test).to be_nil end end diff --git a/spec/lib/gitlab/danger/helper_spec.rb b/spec/lib/gitlab/danger/helper_spec.rb index f11f68ab3c2..2990594c538 100644 --- a/spec/lib/gitlab/danger/helper_spec.rb +++ b/spec/lib/gitlab/danger/helper_spec.rb @@ -101,13 +101,13 @@ describe Gitlab::Danger::Helper do describe '#changes_by_category' do it 'categorizes changed files' do - expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/foo lib/gitlab/database/foo.rb qa/foo ee/changelogs/foo.yml] } + expect(fake_git).to receive(:added_files) { %w[foo foo.md foo.rb foo.js db/migrate/foo lib/gitlab/database/foo.rb qa/foo ee/changelogs/foo.yml] } allow(fake_git).to receive(:modified_files) { [] } allow(fake_git).to receive(:renamed_files) { [] } expect(helper.changes_by_category).to eq( backend: %w[foo.rb], - database: %w[db/foo lib/gitlab/database/foo.rb], + database: %w[db/migrate/foo lib/gitlab/database/foo.rb], frontend: %w[foo.js], none: %w[ee/changelogs/foo.yml foo.md], qa: %w[qa/foo], @@ -173,8 +173,13 @@ describe Gitlab::Danger::Helper do 'ee/FOO_VERSION' | :unknown - 'db/foo' | :database - 'ee/db/foo' | :database + 'db/schema.rb' | :database + 'db/migrate/foo' | :database + 'db/post_migrate/foo' | :database + 'ee/db/migrate/foo' | :database + 'ee/db/post_migrate/foo' | :database + 'ee/db/geo/migrate/foo' | :database + 'ee/db/geo/post_migrate/foo' | :database 'app/models/project_authorization.rb' | :database 'app/services/users/refresh_authorized_projects_service.rb' | :database 'lib/gitlab/background_migration.rb' | :database @@ -188,6 +193,9 @@ describe Gitlab::Danger::Helper do 'lib/gitlab/sql/foo' | :database 'rubocop/cop/migration/foo' | :database + 'db/fixtures/foo.rb' | :backend + 'ee/db/fixtures/foo.rb' | :backend + 'qa/foo' | :qa 'ee/qa/foo' | :qa diff --git a/spec/lib/gitlab/data_builder/push_spec.rb b/spec/lib/gitlab/data_builder/push_spec.rb index cc31f88d365..e8a9f0b06a8 100644 --- a/spec/lib/gitlab/data_builder/push_spec.rb +++ b/spec/lib/gitlab/data_builder/push_spec.rb @@ -3,9 +3,43 @@ require 'spec_helper' describe Gitlab::DataBuilder::Push do + include RepoHelpers + let(:project) { create(:project, :repository) } let(:user) { build(:user, public_email: 'public-email@example.com') } + describe '.build' do + let(:sample) { RepoHelpers.sample_compare } + let(:commits) { project.repository.commits_between(sample.commits.first, sample.commits.last) } + let(:subject) do + described_class.build(project: project, + user: user, + ref: sample.target_branch, + commits: commits, + commits_count: commits.length, + message: 'test message', + with_changed_files: with_changed_files) + end + + context 'with changed files' do + let(:with_changed_files) { true } + + it 'returns commit hook data' do + expect(subject[:project]).to eq(project.hook_attrs) + expect(subject[:commits].first.keys).to include(*%i(added removed modified)) + end + end + + context 'without changed files' do + let(:with_changed_files) { false } + + it 'returns commit hook data without include deltas' do + expect(subject[:project]).to eq(project.hook_attrs) + expect(subject[:commits].first.keys).not_to include(*%i(added removed modified)) + end + end + end + describe '.build_sample' do let(:data) { described_class.build_sample(project, user) } diff --git a/spec/lib/gitlab/git_post_receive_spec.rb b/spec/lib/gitlab/git_post_receive_spec.rb new file mode 100644 index 00000000000..4c20d945585 --- /dev/null +++ b/spec/lib/gitlab/git_post_receive_spec.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe ::Gitlab::GitPostReceive do + let(:project) { create(:project) } + + subject { described_class.new(project, "project-#{project.id}", changes.dup, {}) } + + describe '#includes_branches?' do + context 'with no branches' do + let(:changes) do + <<~EOF + 654321 210987 refs/nobranches/tag1 + 654322 210986 refs/tags/test1 + 654323 210985 refs/merge-requests/mr1 + EOF + end + + it 'returns false' do + expect(subject.includes_branches?).to be_falsey + end + end + + context 'with branches' do + let(:changes) do + <<~EOF + 654322 210986 refs/heads/test1 + 654321 210987 refs/tags/tag1 + 654323 210985 refs/merge-requests/mr1 + EOF + end + + it 'returns true' do + expect(subject.includes_branches?).to be_truthy + end + end + + context 'with malformed changes' do + let(:changes) do + <<~EOF + ref/heads/1 a + somebranch refs/heads/2 + EOF + end + + it 'returns false' do + expect(subject.includes_branches?).to be_falsey + end + end + end + + describe '#includes_tags?' do + context 'with no tags' do + let(:changes) do + <<~EOF + 654321 210987 refs/notags/tag1 + 654322 210986 refs/heads/test1 + 654323 210985 refs/merge-requests/mr1 + EOF + end + + it 'returns false' do + expect(subject.includes_tags?).to be_falsey + end + end + + context 'with tags' do + let(:changes) do + <<~EOF + 654322 210986 refs/heads/test1 + 654321 210987 refs/tags/tag1 + 654323 210985 refs/merge-requests/mr1 + EOF + end + + it 'returns true' do + expect(subject.includes_tags?).to be_truthy + end + end + + context 'with malformed changes' do + let(:changes) do + <<~EOF + ref/tags/1 a + sometag refs/tags/2 + EOF + end + + it 'returns false' do + expect(subject.includes_tags?).to be_falsey + end + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index fddb5066d6f..3c6b17c10ec 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -242,6 +242,7 @@ project: - cluster_project - cluster_ingresses - creator +- cycle_analytics_stages - group - namespace - boards diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb index 06c8d127951..fce2aded786 100644 --- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb @@ -30,7 +30,7 @@ describe Gitlab::Kubernetes::Helm::Pod do it 'generates the appropriate specifications for the container' do container = subject.generate.spec.containers.first expect(container.name).to eq('helm') - expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.12.3-kube-1.11.7') + expect(container.image).to eq('registry.gitlab.com/gitlab-org/cluster-integration/helm-install-image/releases/2.14.3-kube-1.11.10') expect(container.env.count).to eq(3) expect(container.env.map(&:name)).to match_array([:HELM_VERSION, :TILLER_NAMESPACE, :COMMAND_SCRIPT]) expect(container.command).to match_array(["/bin/sh"]) diff --git a/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb new file mode 100644 index 00000000000..f24ab5579df --- /dev/null +++ b/spec/lib/gitlab/kubernetes/kubectl_cmd_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Gitlab::Kubernetes::KubectlCmd do + describe '.delete' do + it 'constructs string properly' do + args = %w(resource_type type --flag-1 --flag-2) + + expected_command = 'kubectl delete resource_type type --flag-1 --flag-2' + + expect(described_class.delete(*args)).to eq expected_command + end + end + + describe '.apply_file' do + context 'without optional args' do + it 'requires filename to be present' do + expect { described_class.apply_file(nil) }.to raise_error(ArgumentError, "filename is not present") + expect { described_class.apply_file(" ") }.to raise_error(ArgumentError, "filename is not present") + end + + it 'constructs string properly' do + expected_command = 'kubectl apply -f filename' + + expect(described_class.apply_file('filename')).to eq expected_command + end + end + + context 'with optional args' do + it 'constructs command properly with many args' do + args = %w(arg-1 --flag-0-1 arg-2 --flag-0-2) + + expected_command = 'kubectl apply -f filename arg-1 --flag-0-1 arg-2 --flag-0-2' + + expect(described_class.apply_file('filename', *args)).to eq expected_command + end + + it 'constructs command properly with single arg' do + args = "arg-1" + + expected_command = 'kubectl apply -f filename arg-1' + + expect(described_class.apply_file('filename', args)).to eq(expected_command) + end + end + end +end diff --git a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb index f4a6e1fc7d9..b8add3c1324 100644 --- a/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb +++ b/spec/lib/gitlab/metrics/samplers/puma_sampler_spec.rb @@ -46,8 +46,6 @@ describe Gitlab::Metrics::Samplers::PumaSampler do expect(subject.metrics[:puma_workers]).to receive(:set).with(labels, 2) expect(subject.metrics[:puma_running_workers]).to receive(:set).with(labels, 2) expect(subject.metrics[:puma_stale_workers]).to receive(:set).with(labels, 0) - expect(subject.metrics[:puma_phase]).to receive(:set).once.with(labels, 2) - expect(subject.metrics[:puma_phase]).to receive(:set).once.with({ worker: 'worker_0' }, 1) subject.sample end diff --git a/spec/lib/gitlab/project_template_spec.rb b/spec/lib/gitlab/project_template_spec.rb index 8b82ea7faa5..c7c82d07508 100644 --- a/spec/lib/gitlab/project_template_spec.rb +++ b/spec/lib/gitlab/project_template_spec.rb @@ -28,6 +28,18 @@ describe Gitlab::ProjectTemplate do end end + describe '#project_path' do + subject { described_class.new('name', 'title', 'description', 'https://gitlab.com/some/project/path').project_path } + + it { is_expected.to eq 'some/project/path' } + end + + describe '#uri_encoded_project_path' do + subject { described_class.new('name', 'title', 'description', 'https://gitlab.com/some/project/path').uri_encoded_project_path } + + it { is_expected.to eq 'some%2Fproject%2Fpath' } + end + describe '.find' do subject { described_class.find(query) } diff --git a/spec/lib/gitlab/snowplow_tracker_spec.rb b/spec/lib/gitlab/snowplow_tracker_spec.rb new file mode 100644 index 00000000000..073a33e5973 --- /dev/null +++ b/spec/lib/gitlab/snowplow_tracker_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe Gitlab::SnowplowTracker do + let(:timestamp) { Time.utc(2017, 3, 22) } + + around do |example| + Timecop.freeze(timestamp) { example.run } + end + + subject { described_class.track_event('epics', 'action', property: 'what', value: 'doit') } + + context '.track_event' do + context 'when Snowplow tracker is disabled' do + it 'does not track the event' do + expect(SnowplowTracker::Tracker).not_to receive(:new) + + subject + end + end + + context 'when Snowplow tracker is enabled' do + before do + stub_application_setting(snowplow_enabled: true) + stub_application_setting(snowplow_site_id: 'awesome gitlab') + stub_application_setting(snowplow_collector_hostname: 'url.com') + end + + it 'tracks the event' do + tracker = double + + expect(::SnowplowTracker::Tracker).to receive(:new) + .with( + an_instance_of(::SnowplowTracker::Emitter), + an_instance_of(::SnowplowTracker::Subject), + 'cf', 'awesome gitlab' + ).and_return(tracker) + expect(tracker).to receive(:track_struct_event) + .with('epics', 'action', nil, 'what', 'doit', nil, timestamp.to_i) + + subject + end + end + end +end diff --git a/spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb new file mode 100644 index 00000000000..71be37692e2 --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/cycle_analytics_counter_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::UsageDataCounters::CycleAnalyticsCounter do + it_behaves_like 'a redis usage counter', 'CycleAnalytics', :views + + it_behaves_like 'a redis usage counter with totals', :cycle_analytics, views: 3 +end diff --git a/spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb new file mode 100644 index 00000000000..47077345e0c --- /dev/null +++ b/spec/lib/gitlab/usage_data_counters/source_code_counter_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::UsageDataCounters::SourceCodeCounter do + it_behaves_like 'a redis usage counter', 'Source Code', :pushes + + it_behaves_like 'a redis usage counter with totals', :source_code, pushes: 5 +end diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb index bf36273251b..588c68d1fb0 100644 --- a/spec/lib/gitlab/usage_data_spec.rb +++ b/spec/lib/gitlab/usage_data_spec.rb @@ -59,6 +59,7 @@ describe Gitlab::UsageData do avg_cycle_analytics influxdb_metrics_enabled prometheus_metrics_enabled + cycle_analytics_views )) expect(subject).to include( @@ -71,7 +72,9 @@ describe Gitlab::UsageData do web_ide_views: a_kind_of(Integer), web_ide_commits: a_kind_of(Integer), web_ide_merge_requests: a_kind_of(Integer), - navbar_searches: a_kind_of(Integer) + navbar_searches: a_kind_of(Integer), + cycle_analytics_views: a_kind_of(Integer), + source_code_pushes: a_kind_of(Integer) ) end diff --git a/spec/models/analytics/cycle_analytics/project_stage_spec.rb b/spec/models/analytics/cycle_analytics/project_stage_spec.rb new file mode 100644 index 00000000000..4e3923e82b1 --- /dev/null +++ b/spec/models/analytics/cycle_analytics/project_stage_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Analytics::CycleAnalytics::ProjectStage do + describe 'associations' do + it { is_expected.to belong_to(:project) } + end +end diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb index d4f8b552088..00b5c72a3d3 100644 --- a/spec/models/clusters/applications/helm_spec.rb +++ b/spec/models/clusters/applications/helm_spec.rb @@ -23,7 +23,7 @@ describe Clusters::Applications::Helm do Clusters::Cluster::APPLICATIONS.keys.each do |application_name| next if application_name == 'helm' - it do + it "is false when #{application_name} is installed" do cluster_application = create("clusters_applications_#{application_name}".to_sym) helm = cluster_application.cluster.application_helm @@ -31,6 +31,14 @@ describe Clusters::Applications::Helm do expect(helm.allowed_to_uninstall?).to be_falsy end end + + it 'executes a single query only' do + cluster_application = create(:clusters_applications_ingress) + helm = cluster_application.cluster.application_helm + + query_count = ActiveRecord::QueryRecorder.new { helm.allowed_to_uninstall? }.count + expect(query_count).to eq(1) + end end context "without other existing applications" do diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 53424204db7..d344a6d0f0d 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -3015,9 +3015,6 @@ describe MergeRequest do subject { merge_request.rebase_in_progress? } it do - # Stub out the legacy gitaly implementation - allow(merge_request).to receive(:gitaly_rebase_in_progress?) { false } - allow(Gitlab::SidekiqStatus).to receive(:running?).with(rebase_jid) { jid_valid } merge_request.rebase_jid = rebase_jid @@ -3027,42 +3024,6 @@ describe MergeRequest do end end - describe '#gitaly_rebase_in_progress?' do - let(:repo_path) do - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - subject.source_project.repository.path - end - end - let(:rebase_path) { File.join(repo_path, "gitlab-worktree", "rebase-#{subject.id}") } - - before do - system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} worktree add --detach #{rebase_path} master)) - end - - it 'returns true when there is a current rebase directory' do - expect(subject.rebase_in_progress?).to be_truthy - end - - it 'returns false when there is no rebase directory' do - FileUtils.rm_rf(rebase_path) - - expect(subject.rebase_in_progress?).to be_falsey - end - - it 'returns false when the rebase directory has expired' do - time = 20.minutes.ago.to_time - File.utime(time, time, rebase_path) - - expect(subject.rebase_in_progress?).to be_falsey - end - - it 'returns false when the source project has been removed' do - allow(subject).to receive(:source_project).and_return(nil) - - expect(subject.rebase_in_progress?).to be_falsey - end - end - describe '#allow_collaboration' do let(:merge_request) do build(:merge_request, source_branch: 'fixes', allow_collaboration: true) diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 2b9c3c43af9..972f26ac745 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -853,4 +853,64 @@ describe Namespace do it { is_expected.to be_falsy } end end + + describe '#emails_disabled?' do + context 'when not a subgroup' do + it 'returns false' do + group = create(:group, emails_disabled: false) + + expect(group.emails_disabled?).to be_falsey + end + + it 'returns true' do + group = create(:group, emails_disabled: true) + + expect(group.emails_disabled?).to be_truthy + end + end + + context 'when a subgroup' do + let(:grandparent) { create(:group) } + let(:parent) { create(:group, parent: grandparent) } + let(:group) { create(:group, parent: parent) } + + it 'returns false' do + expect(group.emails_disabled?).to be_falsey + end + + context 'when ancestor emails are disabled' do + it 'returns true' do + grandparent.update_attribute(:emails_disabled, true) + + expect(group.emails_disabled?).to be_truthy + end + end + end + + context 'when :emails_disabled feature flag is off' do + before do + stub_feature_flags(emails_disabled: false) + end + + context 'when not a subgroup' do + it 'returns false' do + group = create(:group, emails_disabled: true) + + expect(group.emails_disabled?).to be_falsey + end + end + + context 'when a subgroup and ancestor emails are disabled' do + let(:grandparent) { create(:group) } + let(:parent) { create(:group, parent: grandparent) } + let(:group) { create(:group, parent: parent) } + + it 'returns false' do + grandparent.update_attribute(:emails_disabled, true) + + expect(group.emails_disabled?).to be_falsey + end + end + end + end end diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb index 4122736c148..2ba53818e54 100644 --- a/spec/models/notification_recipient_spec.rb +++ b/spec/models/notification_recipient_spec.rb @@ -9,6 +9,38 @@ describe NotificationRecipient do subject(:recipient) { described_class.new(user, :watch, target: target, project: project) } + describe '#notifiable?' do + let(:recipient) { described_class.new(user, :mention, target: target, project: project) } + + context 'when emails are disabled' do + it 'returns false if group disabled' do + expect(project.namespace).to receive(:emails_disabled?).and_return(true) + expect(recipient).to receive(:emails_disabled?).and_call_original + expect(recipient.notifiable?).to eq false + end + + it 'returns false if project disabled' do + expect(project).to receive(:emails_disabled?).and_return(true) + expect(recipient).to receive(:emails_disabled?).and_call_original + expect(recipient.notifiable?).to eq false + end + end + + context 'when emails are enabled' do + it 'returns true if group enabled' do + expect(project.namespace).to receive(:emails_disabled?).and_return(false) + expect(recipient).to receive(:emails_disabled?).and_call_original + expect(recipient.notifiable?).to eq true + end + + it 'returns true if project enabled' do + expect(project).to receive(:emails_disabled?).and_return(false) + expect(recipient).to receive(:emails_disabled?).and_call_original + expect(recipient.notifiable?).to eq true + end + end + end + describe '#has_access?' do before do allow(user).to receive(:can?).and_call_original diff --git a/spec/models/project_services/chat_message/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb index 619ab96af94..cf7c7bf7e61 100644 --- a/spec/models/project_services/chat_message/pipeline_message_spec.rb +++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb @@ -42,9 +42,9 @@ describe ChatMessage::PipelineMessage do before do test_commit = double("A test commit", committer: args[:user], title: "A test commit message") - test_project = double("A test project", - commit_by: test_commit, name: args[:project][:name], - web_url: args[:project][:web_url], avatar_url: args[:project][:avatar_url]) + test_project = double("A test project", commit_by: test_commit, name: args[:project][:name], web_url: args[:project][:web_url]) + allow(test_project).to receive(:avatar_url).with(no_args).and_return("/avatar") + allow(test_project).to receive(:avatar_url).with(only_path: false).and_return(args[:project][:avatar_url]) allow(Project).to receive(:find) { test_project } test_pipeline = double("A test pipeline", has_yaml_errors?: has_yaml_errors, diff --git a/spec/models/project_services/emails_on_push_service_spec.rb b/spec/models/project_services/emails_on_push_service_spec.rb index 0a58eb367e3..ffe241aa880 100644 --- a/spec/models/project_services/emails_on_push_service_spec.rb +++ b/spec/models/project_services/emails_on_push_service_spec.rb @@ -20,4 +20,24 @@ describe EmailsOnPushService do it { is_expected.not_to validate_presence_of(:recipients) } end end + + context 'project emails' do + let(:push_data) { { object_kind: 'push' } } + let(:project) { create(:project, :repository) } + let(:service) { create(:emails_on_push_service, project: project) } + + it 'does not send emails when disabled' do + expect(project).to receive(:emails_disabled?).and_return(true) + expect(EmailsOnPushWorker).not_to receive(:perform_async) + + service.execute(push_data) + end + + it 'does send emails when enabled' do + expect(project).to receive(:emails_disabled?).and_return(false) + expect(EmailsOnPushWorker).to receive(:perform_async) + + service.execute(push_data) + end + end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 29a589eba20..ff9e94afc12 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -98,6 +98,7 @@ describe Project do it { is_expected.to have_many(:lfs_file_locks) } it { is_expected.to have_many(:project_deploy_tokens) } it { is_expected.to have_many(:deploy_tokens).through(:project_deploy_tokens) } + it { is_expected.to have_many(:cycle_analytics_stages) } it 'has an inverse relationship with merge requests' do expect(described_class.reflect_on_association(:merge_requests).has_inverse?).to eq(:target_project) @@ -2252,6 +2253,21 @@ describe Project do end end + describe '#mark_stuck_remote_mirrors_as_failed!' do + it 'fails stuck remote mirrors' do + project = create(:project, :repository, :remote_mirror) + + project.remote_mirrors.first.update( + update_status: :started, + last_update_started_at: 2.days.ago + ) + + expect do + project.mark_stuck_remote_mirrors_as_failed! + end.to change { project.remote_mirrors.stuck.count }.from(1).to(0) + end + end + describe '#ancestors_upto' do let(:parent) { create(:group) } let(:child) { create(:group, parent: parent) } @@ -2300,6 +2316,57 @@ describe Project do end end + describe '#emails_disabled?' do + let(:project) { create(:project, emails_disabled: false) } + + context 'emails disabled in group' do + it 'returns true' do + allow(project.namespace).to receive(:emails_disabled?) { true } + + expect(project.emails_disabled?).to be_truthy + end + end + + context 'emails enabled in group' do + before do + allow(project.namespace).to receive(:emails_disabled?) { false } + end + + it 'returns false' do + expect(project.emails_disabled?).to be_falsey + end + + it 'returns true' do + project.update_attribute(:emails_disabled, true) + + expect(project.emails_disabled?).to be_truthy + end + end + + context 'when :emails_disabled feature flag is off' do + before do + stub_feature_flags(emails_disabled: false) + end + + context 'emails disabled in group' do + it 'returns false' do + allow(project.namespace).to receive(:emails_disabled?) { true } + + expect(project.emails_disabled?).to be_falsey + end + end + + context 'emails enabled in group' do + it 'returns false' do + allow(project.namespace).to receive(:emails_disabled?) { false } + project.update_attribute(:emails_disabled, true) + + expect(project.emails_disabled?).to be_falsey + end + end + end + end + describe '#lfs_enabled?' do let(:project) { create(:project) } @@ -4297,6 +4364,39 @@ describe Project do end end + describe '#has_active_hooks?' do + set(:project) { create(:project) } + + it { expect(project.has_active_hooks?).to be_falsey } + + it 'returns true when a matching push hook exists' do + create(:project_hook, push_events: true, project: project) + + expect(project.has_active_hooks?(:merge_request_events)).to be_falsey + expect(project.has_active_hooks?).to be_truthy + end + + it 'returns true when a matching system hook exists' do + create(:system_hook, push_events: true) + + expect(project.has_active_hooks?(:merge_request_events)).to be_falsey + expect(project.has_active_hooks?).to be_truthy + end + end + + describe '#has_active_services?' do + set(:project) { create(:project) } + + it { expect(project.has_active_services?).to be_falsey } + + it 'returns true when a matching service exists' do + create(:custom_issue_tracker_service, push_events: true, merge_requests_events: false, project: project) + + expect(project.has_active_services?(:merge_request_hooks)).to be_falsey + expect(project.has_active_services?).to be_truthy + end + end + describe '#badges' do let(:project_group) { create(:group) } let(:project) { create(:project, path: 'avatar', namespace: project_group) } diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb index 687b0935c55..7edeb56efe2 100644 --- a/spec/models/remote_mirror_spec.rb +++ b/spec/models/remote_mirror_spec.rb @@ -153,14 +153,14 @@ describe RemoteMirror, :mailer do end end - describe '#mark_as_failed' do + describe '#mark_as_failed!' do let(:remote_mirror) { create(:remote_mirror) } let(:error_message) { 'http://user:pass@test.com/root/repoC.git/' } let(:sanitized_error_message) { 'http://*****:*****@test.com/root/repoC.git/' } subject do remote_mirror.update_start - remote_mirror.mark_as_failed(error_message) + remote_mirror.mark_as_failed!(error_message) end it 'sets the update_status to failed' do @@ -204,8 +204,8 @@ describe RemoteMirror, :mailer do it 'includes mirrors that were started over an hour ago' do mirror = create_mirror(url: 'http://cantbeblank', update_status: 'started', - last_update_at: 3.hours.ago, - updated_at: 2.hours.ago) + last_update_started_at: 3.hours.ago, + last_update_at: 2.hours.ago) expect(described_class.stuck.last).to eq(mirror) end @@ -214,7 +214,7 @@ describe RemoteMirror, :mailer do mirror = create_mirror(url: 'http://cantbeblank', update_status: 'started', last_update_at: nil, - updated_at: 4.hours.ago) + last_update_started_at: 4.hours.ago) expect(described_class.stuck.last).to eq(mirror) end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 12dff440ce2..e68de2e73a8 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1744,12 +1744,23 @@ describe Repository do end end - describe '#before_push_tag' do + describe '#expires_caches_for_tags' do it 'flushes the cache' do expect(repository).to receive(:expire_statistics_caches) expect(repository).to receive(:expire_emptiness_caches) expect(repository).to receive(:expire_tags_cache) + repository.expire_caches_for_tags + end + end + + describe '#before_push_tag' do + it 'logs an event' do + expect(repository).not_to receive(:expire_statistics_caches) + expect(repository).not_to receive(:expire_emptiness_caches) + expect(repository).not_to receive(:expire_tags_cache) + expect(repository).to receive(:repository_event).with(:push_tag) + repository.before_push_tag end end @@ -1781,6 +1792,12 @@ describe Repository do repository.after_create_branch end + + it 'does not expire the branch caches when specified' do + expect(repository).not_to receive(:expire_branches_cache) + + repository.after_create_branch(expire_cache: false) + end end describe '#after_remove_branch' do @@ -1789,6 +1806,12 @@ describe Repository do repository.after_remove_branch end + + it 'does not expire the branch caches when specified' do + expect(repository).not_to receive(:expire_branches_cache) + + repository.after_remove_branch(expire_cache: false) + end end describe '#after_create' do diff --git a/spec/policies/clusters/instance_policy_spec.rb b/spec/policies/clusters/instance_policy_spec.rb index 7b61819e079..2373fef8aa6 100644 --- a/spec/policies/clusters/instance_policy_spec.rb +++ b/spec/policies/clusters/instance_policy_spec.rb @@ -9,6 +9,8 @@ describe Clusters::InstancePolicy do describe 'rules' do context 'when user' do it { expect(policy).to be_disallowed :read_cluster } + it { expect(policy).to be_disallowed :add_cluster } + it { expect(policy).to be_disallowed :create_cluster } it { expect(policy).to be_disallowed :update_cluster } it { expect(policy).to be_disallowed :admin_cluster } end @@ -17,6 +19,8 @@ describe Clusters::InstancePolicy do let(:user) { create(:admin) } it { expect(policy).to be_allowed :read_cluster } + it { expect(policy).to be_allowed :add_cluster } + it { expect(policy).to be_allowed :create_cluster } it { expect(policy).to be_allowed :update_cluster } it { expect(policy).to be_allowed :admin_cluster } end diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb index 12be3927e18..df6cc526eb0 100644 --- a/spec/policies/global_policy_spec.rb +++ b/spec/policies/global_policy_spec.rb @@ -226,4 +226,32 @@ describe GlobalPolicy do it { is_expected.not_to be_allowed(:read_instance_statistics) } end end + + describe 'slash commands' do + context 'regular user' do + it { is_expected.to be_allowed(:use_slash_commands) } + end + + context 'when internal' do + let(:current_user) { User.ghost } + + it { is_expected.not_to be_allowed(:use_slash_commands) } + end + + context 'when blocked' do + before do + current_user.block + end + + it { is_expected.not_to be_allowed(:use_slash_commands) } + end + + context 'when access locked' do + before do + current_user.lock_access! + end + + it { is_expected.not_to be_allowed(:use_slash_commands) } + end + end end diff --git a/spec/requests/api/discussions_spec.rb b/spec/requests/api/discussions_spec.rb index ca1ffe3c524..ef09c6effbb 100644 --- a/spec/requests/api/discussions_spec.rb +++ b/spec/requests/api/discussions_spec.rb @@ -9,6 +9,61 @@ describe API::Discussions do project.add_developer(user) end + context 'with cross-reference system notes', :request_store do + let(:merge_request) { create(:merge_request) } + let(:project) { merge_request.project } + let(:new_merge_request) { create(:merge_request) } + let(:commit) { new_merge_request.project.commit } + let!(:note) { create(:system_note, noteable: merge_request, project: project, note: cross_reference) } + let!(:note_metadata) { create(:system_note_metadata, note: note, action: 'cross_reference') } + let(:cross_reference) { "test commit #{commit.to_reference(project)}" } + let(:pat) { create(:personal_access_token, user: user) } + + let(:url) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/discussions" } + + before do + project.add_developer(user) + new_merge_request.project.add_developer(user) + end + + it 'returns only the note that the user should see' do + hidden_merge_request = create(:merge_request) + new_cross_reference = "test commit #{hidden_merge_request.project.commit}" + new_note = create(:system_note, noteable: merge_request, project: project, note: new_cross_reference) + create(:system_note_metadata, note: new_note, action: 'cross_reference') + + get api(url, user, personal_access_token: pat) + expect(response).to have_gitlab_http_status(200) + expect(json_response.count).to eq(1) + expect(json_response.first['notes'].count).to eq(1) + + parsed_note = json_response.first['notes'].first + expect(parsed_note['id']).to eq(note.id) + expect(parsed_note['body']).to eq(cross_reference) + expect(parsed_note['system']).to be true + end + + it 'avoids Git calls and N+1 SQL queries' do + expect_any_instance_of(Repository).not_to receive(:find_commit).with(commit.id) + + control = ActiveRecord::QueryRecorder.new do + get api(url, user, personal_access_token: pat) + end + + expect(response).to have_gitlab_http_status(200) + + RequestStore.clear! + + new_note = create(:system_note, noteable: merge_request, project: project, note: cross_reference) + create(:system_note_metadata, note: new_note, action: 'cross_reference') + + RequestStore.clear! + + expect { get api(url, user, personal_access_token: pat) }.not_to exceed_query_limit(control) + expect(response).to have_gitlab_http_status(200) + end + end + context 'when noteable is an Issue' do let!(:issue) { create(:issue, project: project, author: user) } let!(:issue_note) { create(:discussion_note_on_issue, noteable: issue, project: project, author: user) } diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 184c00a356a..590107d5161 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -144,6 +144,7 @@ describe API::Settings, 'Settings' do external_auth_client_key_pass: "5iveL!fe" } end + let(:attribute_names) { settings.keys.map(&:to_s) } it 'includes the attributes in the API' do @@ -165,6 +166,56 @@ describe API::Settings, 'Settings' do end end + context "snowplow tracking settings" do + let(:settings) do + { + snowplow_collector_hostname: "snowplow.example.com", + snowplow_cookie_domain: ".example.com", + snowplow_enabled: true, + snowplow_site_id: "site_id" + } + end + + let(:attribute_names) { settings.keys.map(&:to_s) } + + it "includes the attributes in the API" do + get api("/application/settings", admin) + + expect(response).to have_gitlab_http_status(200) + attribute_names.each do |attribute| + expect(json_response.keys).to include(attribute) + end + end + + it "allows updating the settings" do + put api("/application/settings", admin), params: settings + + expect(response).to have_gitlab_http_status(200) + settings.each do |attribute, value| + expect(ApplicationSetting.current.public_send(attribute)).to eq(value) + end + end + + context "missing snowplow_collector_hostname value when snowplow_enabled is true" do + it "returns a blank parameter error message" do + put api("/application/settings", admin), params: { snowplow_enabled: true } + + expect(response).to have_gitlab_http_status(400) + expect(json_response["error"]).to eq("snowplow_collector_hostname is missing") + end + + it "handles validation errors" do + put api("/application/settings", admin), params: settings.merge({ + snowplow_collector_hostname: nil + }) + + expect(response).to have_gitlab_http_status(400) + message = json_response["message"] + expect(message["snowplow_collector_hostname"]).to include("can't be blank") + end + end + end + context "missing plantuml_url value when plantuml_enabled is true" do it "returns a blank parameter error message" do put api("/application/settings", admin), params: { plantuml_enabled: true } diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 7e2f311a065..deb68899309 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1113,7 +1113,7 @@ describe Ci::CreatePipelineService do test_a: { stage: "test", script: "ls", - only: %w[master feature tags], + only: %w[master feature], needs: %w[build_a] }, deploy: { @@ -1143,6 +1143,7 @@ describe Ci::CreatePipelineService do it 'does not create a pipeline as test_a depends on build_a' do expect(pipeline).not_to be_persisted expect(pipeline.builds).to be_empty + expect(pipeline.errors[:base]).to contain_exactly("test_a: needs 'build_a'") end end diff --git a/spec/services/git/base_hooks_service_spec.rb b/spec/services/git/base_hooks_service_spec.rb index 4a2ec769116..874df9a68cd 100644 --- a/spec/services/git/base_hooks_service_spec.rb +++ b/spec/services/git/base_hooks_service_spec.rb @@ -14,6 +14,78 @@ describe Git::BaseHooksService do let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0 let(:ref) { 'refs/tags/v1.1.0' } + describe '#execute_project_hooks' do + class TestService < described_class + def hook_name + :push_hooks + end + + def commits + [] + end + end + + let(:project) { create(:project, :repository) } + + subject { TestService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) } + + context '#execute_hooks' do + before do + expect(project).to receive(:has_active_hooks?).and_return(active) + end + + context 'active hooks' do + let(:active) { true } + + it 'executes the hooks' do + expect(subject).to receive(:push_data).at_least(:once).and_call_original + expect(project).to receive(:execute_hooks) + + subject.execute + end + end + + context 'inactive hooks' do + let(:active) { false } + + it 'does not execute the hooks' do + expect(subject).not_to receive(:push_data) + expect(project).not_to receive(:execute_hooks) + + subject.execute + end + end + end + + context '#execute_services' do + before do + expect(project).to receive(:has_active_services?).and_return(active) + end + + context 'active services' do + let(:active) { true } + + it 'executes the services' do + expect(subject).to receive(:push_data).at_least(:once).and_call_original + expect(project).to receive(:execute_services) + + subject.execute + end + end + + context 'inactive services' do + let(:active) { false } + + it 'does not execute the services' do + expect(subject).not_to receive(:push_data) + expect(project).not_to receive(:execute_services) + + subject.execute + end + end + end + end + describe 'with remote mirrors' do class TestService < described_class def commits diff --git a/spec/services/git/branch_hooks_service_spec.rb b/spec/services/git/branch_hooks_service_spec.rb index 23be400059e..8af51848b7b 100644 --- a/spec/services/git/branch_hooks_service_spec.rb +++ b/spec/services/git/branch_hooks_service_spec.rb @@ -25,7 +25,7 @@ describe Git::BranchHooksService do end describe "Git Push Data" do - subject(:push_data) { service.execute } + subject(:push_data) { service.send(:push_data) } it 'has expected push data attributes' do is_expected.to match a_hash_including( @@ -109,6 +109,7 @@ describe Git::BranchHooksService do expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) expect(event.push_event_payload.commit_from).to eq(oldrev) expect(event.push_event_payload.commit_to).to eq(newrev) + expect(event.push_event_payload.commit_title).to eq('Change some files') expect(event.push_event_payload.ref).to eq('master') expect(event.push_event_payload.commit_count).to eq(1) end @@ -124,6 +125,7 @@ describe Git::BranchHooksService do expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) expect(event.push_event_payload.commit_from).to be_nil expect(event.push_event_payload.commit_to).to eq(newrev) + expect(event.push_event_payload.commit_title).to eq('Initial commit') expect(event.push_event_payload.ref).to eq('master') expect(event.push_event_payload.commit_count).to be > 1 end diff --git a/spec/services/git/branch_push_service_spec.rb b/spec/services/git/branch_push_service_spec.rb index 6e39fa6b3c0..ad5d296f5c1 100644 --- a/spec/services/git/branch_push_service_spec.rb +++ b/spec/services/git/branch_push_service_spec.rb @@ -78,7 +78,10 @@ describe Git::BranchPushService, services: true do it "creates a new pipeline" do expect { subject }.to change { Ci::Pipeline.count } - expect(Ci::Pipeline.last).to be_push + + pipeline = Ci::Pipeline.last + expect(pipeline).to be_push + expect(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.ref).to eq(ref) end end @@ -123,6 +126,10 @@ describe Git::BranchPushService, services: true do describe "Webhooks" do context "execute webhooks" do + before do + create(:project_hook, push_events: true, project: project) + end + it "when pushing a branch for the first time" do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") diff --git a/spec/services/git/tag_hooks_service_spec.rb b/spec/services/git/tag_hooks_service_spec.rb index f5938a5c708..e362577d289 100644 --- a/spec/services/git/tag_hooks_service_spec.rb +++ b/spec/services/git/tag_hooks_service_spec.rb @@ -26,7 +26,8 @@ describe Git::TagHooksService, :service do describe 'System hooks' do it 'Executes system hooks' do - push_data = service.execute + push_data = service.send(:push_data) + expect(project).to receive(:has_active_hooks?).and_return(true) expect_next_instance_of(SystemHooksService) do |system_hooks_service| expect(system_hooks_service) @@ -40,6 +41,7 @@ describe Git::TagHooksService, :service do describe "Webhooks" do it "executes hooks on the project" do + expect(project).to receive(:has_active_hooks?).and_return(true) expect(project).to receive(:execute_hooks) service.execute @@ -61,7 +63,7 @@ describe Git::TagHooksService, :service do describe 'Push data' do shared_examples_for 'tag push data expectations' do - subject(:push_data) { service.execute } + subject(:push_data) { service.send(:push_data) } it 'has expected push data attributes' do is_expected.to match a_hash_including( object_kind: 'tag_push', diff --git a/spec/services/git/tag_push_service_spec.rb b/spec/services/git/tag_push_service_spec.rb index 418952b52da..7e008637182 100644 --- a/spec/services/git/tag_push_service_spec.rb +++ b/spec/services/git/tag_push_service_spec.rb @@ -26,8 +26,8 @@ describe Git::TagPushService do subject end - it 'flushes the tags cache' do - expect(project.repository).to receive(:expire_tags_cache) + it 'does not flush the tags cache' do + expect(project.repository).not_to receive(:expire_tags_cache) subject end diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index 5d4576139f7..12e9c2b2f3a 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -86,6 +86,7 @@ describe Groups::UpdateService do context "unauthorized visibility_level validation" do let!(:service) { described_class.new(internal_group, user, visibility_level: 99) } + before do internal_group.add_user(user, Gitlab::Access::MAINTAINER) end @@ -96,6 +97,20 @@ describe Groups::UpdateService do end end + context 'when updating #emails_disabled' do + let(:service) { described_class.new(internal_group, user, emails_disabled: true) } + + it 'updates the attribute' do + internal_group.add_user(user, Gitlab::Access::OWNER) + + expect { service.execute }.to change { internal_group.emails_disabled }.to(true) + end + + it 'does not update when not group owner' do + expect { service.execute }.not_to change { internal_group.emails_disabled } + end + end + context 'rename group' do let!(:service) { described_class.new(internal_group, user, path: SecureRandom.hex) } diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb index ee9caaf2f47..7b8c94c86fe 100644 --- a/spec/services/merge_requests/rebase_service_spec.rb +++ b/spec/services/merge_requests/rebase_service_spec.rb @@ -25,7 +25,7 @@ describe MergeRequests::RebaseService do describe '#execute' do context 'when another rebase is already in progress' do before do - allow(merge_request).to receive(:gitaly_rebase_in_progress?).and_return(true) + allow(repository).to receive(:rebase_in_progress?).with(merge_request.id).and_return(true) end it 'saves the error message' do diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 1dcade1de0d..d925aa2b6c3 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -240,45 +240,50 @@ describe NotificationService, :mailer do end describe '#new_note' do - it do - add_users(project) - add_user_subscriptions(issue) - reset_delivered_emails! + context do + before do + add_users(project) + add_user_subscriptions(issue) + reset_delivered_emails! + end - expect(SentNotification).to receive(:record).with(issue, any_args).exactly(10).times + it do + expect(SentNotification).to receive(:record).with(issue, any_args).exactly(10).times - notification.new_note(note) + notification.new_note(note) - should_email(@u_watcher) - should_email(note.noteable.author) - should_email(note.noteable.assignees.first) - should_email(@u_custom_global) - should_email(@u_mentioned) - should_email(@subscriber) - should_email(@watcher_and_subscriber) - should_email(@subscribed_participant) - should_email(@u_custom_off) - should_email(@unsubscribed_mentioned) - should_not_email(@u_guest_custom) - should_not_email(@u_guest_watcher) - should_not_email(note.author) - should_not_email(@u_participating) - should_not_email(@u_disabled) - should_not_email(@unsubscriber) - should_not_email(@u_outsider_mentioned) - should_not_email(@u_lazy_participant) - end + should_email(@u_watcher) + should_email(note.noteable.author) + should_email(note.noteable.assignees.first) + should_email(@u_custom_global) + should_email(@u_mentioned) + should_email(@subscriber) + should_email(@watcher_and_subscriber) + should_email(@subscribed_participant) + should_email(@u_custom_off) + should_email(@unsubscribed_mentioned) + should_not_email(@u_guest_custom) + should_not_email(@u_guest_watcher) + should_not_email(note.author) + should_not_email(@u_participating) + should_not_email(@u_disabled) + should_not_email(@unsubscriber) + should_not_email(@u_outsider_mentioned) + should_not_email(@u_lazy_participant) + end - it "emails the note author if they've opted into notifications about their activity" do - add_users(project) - add_user_subscriptions(issue) - reset_delivered_emails! + it "emails the note author if they've opted into notifications about their activity" do + note.author.notified_of_own_activity = true - note.author.notified_of_own_activity = true + notification.new_note(note) - notification.new_note(note) + should_email(note.author) + end - should_email(note.author) + it_behaves_like 'project emails are disabled' do + let(:notification_target) { note } + let(:notification_trigger) { notification.new_note(note) } + end end it 'filters out "mentioned in" notes' do @@ -337,6 +342,11 @@ describe NotificationService, :mailer do it_behaves_like 'new note notifications' + it_behaves_like 'project emails are disabled' do + let(:notification_target) { note } + let(:notification_trigger) { notification.new_note(note) } + end + context 'which is a subgroup' do let!(:parent) { create(:group) } let!(:group) { create(:group, parent: parent) } @@ -472,6 +482,11 @@ describe NotificationService, :mailer do expect(Notify).not_to receive(:note_issue_email) notification.new_note(mentioned_note) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { note } + let(:notification_trigger) { notification.new_note(note) } + end end end @@ -619,6 +634,11 @@ describe NotificationService, :mailer do notification.new_note(note) should_not_email(@u_committer) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { note } + let(:notification_trigger) { notification.new_note(note) } + end end end @@ -645,6 +665,11 @@ describe NotificationService, :mailer do .to contain_exactly(*merge_request.assignees.pluck(:id), merge_request.author.id, @u_watcher.id) expect(SentNotification.last.in_reply_to_discussion_id).to eq(note.discussion_id) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { note } + let(:notification_trigger) { notification.new_note(note) } + end end end end @@ -819,6 +844,11 @@ describe NotificationService, :mailer do should_email(user_4) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.new_issue(issue, @u_disabled) } + end + context 'confidential issues' do let(:author) { create(:user) } let(:assignee) { create(:user) } @@ -861,6 +891,11 @@ describe NotificationService, :mailer do let(:mentionable) { issue } include_examples 'notifications for new mentions' + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned) } + end end describe '#reassigned_issue' do @@ -969,6 +1004,11 @@ describe NotificationService, :mailer do let(:issuable) { issue } let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) } + end end describe '#relabeled_issue' do @@ -1028,6 +1068,11 @@ describe NotificationService, :mailer do should_email(subscriber_to_both) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled) } + end + context 'confidential issues' do let(:author) { create(:user) } let(:assignee) { create(:user) } @@ -1065,12 +1110,19 @@ describe NotificationService, :mailer do end describe '#removed_milestone_issue' do - it_behaves_like 'altered milestone notification on issue' do + context do let(:milestone) { create(:milestone, project: project, issues: [issue]) } let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } } - before do - notification.removed_milestone_issue(issue, issue.author) + it_behaves_like 'altered milestone notification on issue' do + before do + notification.removed_milestone_issue(issue, issue.author) + end + end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.removed_milestone_issue(issue, issue.author) } end end @@ -1110,12 +1162,19 @@ describe NotificationService, :mailer do end describe '#changed_milestone_issue' do - it_behaves_like 'altered milestone notification on issue' do + context do let(:new_milestone) { create(:milestone, project: project, issues: [issue]) } let!(:subscriber_to_new_milestone) { create(:user) { |u| issue.toggle_subscription(u, project) } } - before do - notification.changed_milestone_issue(issue, new_milestone, issue.author) + it_behaves_like 'altered milestone notification on issue' do + before do + notification.changed_milestone_issue(issue, new_milestone, issue.author) + end + end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.changed_milestone_issue(issue, new_milestone, issue.author) } end end @@ -1183,6 +1242,11 @@ describe NotificationService, :mailer do let(:issuable) { issue } let(:notification_trigger) { notification.close_issue(issue, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.close_issue(issue, @u_disabled) } + end end describe '#reopen_issue' do @@ -1214,6 +1278,11 @@ describe NotificationService, :mailer do let(:issuable) { issue } let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.reopen_issue(issue, @u_disabled) } + end end describe '#issue_moved' do @@ -1240,6 +1309,11 @@ describe NotificationService, :mailer do let(:issuable) { issue } let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.issue_moved(issue, new_issue, @u_disabled) } + end end describe '#issue_due' do @@ -1280,6 +1354,11 @@ describe NotificationService, :mailer do let(:issuable) { issue } let(:notification_trigger) { notification.issue_due(issue) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { issue } + let(:notification_trigger) { notification.issue_due(issue) } + end end end @@ -1374,6 +1453,11 @@ describe NotificationService, :mailer do should_email(user_4) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.new_merge_request(merge_request, @u_disabled) } + end + context 'participating' do it_should_behave_like 'participating by assignee notification' do let(:participant) { create(:user, username: 'user-participant')} @@ -1406,6 +1490,11 @@ describe NotificationService, :mailer do let(:mentionable) { merge_request } include_examples 'notifications for new mentions' + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { send_notifications(@u_watcher, @u_participant_mentioned, @u_custom_global, @u_mentioned) } + end end describe '#reassigned_merge_request' do @@ -1449,6 +1538,11 @@ describe NotificationService, :mailer do let(:issuable) { merge_request } let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, [assignee]) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.reassigned_merge_request(merge_request, current_user, [assignee]) } + end end describe '#push_to_merge_request' do @@ -1479,6 +1573,11 @@ describe NotificationService, :mailer do let(:issuable) { merge_request } let(:notification_trigger) { notification.push_to_merge_request(merge_request, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.push_to_merge_request(merge_request, @u_disabled) } + end end describe '#relabel_merge_request' do @@ -1512,28 +1611,43 @@ describe NotificationService, :mailer do should_not_email(@u_participating) should_not_email(@u_lazy_participant) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.relabeled_merge_request(merge_request, [group_label_2, label_2], @u_disabled) } + end end describe '#removed_milestone_merge_request' do - it_behaves_like 'altered milestone notification on merge request' do - let(:milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } - let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } + let(:milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } + let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } + it_behaves_like 'altered milestone notification on merge request' do before do notification.removed_milestone_merge_request(merge_request, merge_request.author) end end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.removed_milestone_merge_request(merge_request, merge_request.author) } + end end describe '#changed_milestone_merge_request' do - it_behaves_like 'altered milestone notification on merge request' do - let(:new_milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } - let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } + let(:new_milestone) { create(:milestone, project: project, merge_requests: [merge_request]) } + let!(:subscriber_to_new_milestone) { create(:user) { |u| merge_request.toggle_subscription(u, project) } } + it_behaves_like 'altered milestone notification on merge request' do before do notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author) end end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.changed_milestone_merge_request(merge_request, new_milestone, merge_request.author) } + end end describe '#merge_request_unmergeable' do @@ -1544,6 +1658,11 @@ describe NotificationService, :mailer do expect(email_recipients.size).to eq(1) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.merge_request_unmergeable(merge_request) } + end + describe 'when merge_when_pipeline_succeeds is true' do before do merge_request.update( @@ -1590,6 +1709,11 @@ describe NotificationService, :mailer do let(:issuable) { merge_request } let(:notification_trigger) { notification.close_mr(merge_request, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.close_mr(merge_request, @u_disabled) } + end end describe '#merged_merge_request' do @@ -1642,6 +1766,11 @@ describe NotificationService, :mailer do let(:issuable) { merge_request } let(:notification_trigger) { notification.merge_mr(merge_request, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.merge_mr(merge_request, @u_disabled) } + end end describe '#reopen_merge_request' do @@ -1672,6 +1801,11 @@ describe NotificationService, :mailer do let(:issuable) { merge_request } let(:notification_trigger) { notification.reopen_mr(merge_request, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.reopen_mr(merge_request, @u_disabled) } + end end describe "#resolve_all_discussions" do @@ -1695,6 +1829,11 @@ describe NotificationService, :mailer do let(:issuable) { merge_request } let(:notification_trigger) { notification.resolve_all_discussions(merge_request, @u_disabled) } end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { merge_request } + let(:notification_trigger) { notification.resolve_all_discussions(merge_request, @u_disabled) } + end end end @@ -1719,6 +1858,11 @@ describe NotificationService, :mailer do should_not_email(@u_disabled) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.project_was_moved(project, "gitlab/gitlab") } + end + context 'users not having access to the new location' do it 'does not send email' do old_user = create(:user) @@ -1762,6 +1906,11 @@ describe NotificationService, :mailer do should_only_email(@u_participating) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.project_exported(project, @u_participating) } + end end describe '#project_not_exported' do @@ -1770,6 +1919,11 @@ describe NotificationService, :mailer do should_only_email(@u_participating) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.project_not_exported(project, @u_participating, ['error']) } + end end end end @@ -1800,6 +1954,11 @@ describe NotificationService, :mailer do should_email(maintainer) should_not_email(developer) end + + it_behaves_like 'group emails are disabled' do + let(:notification_target) { group } + let(:notification_trigger) { group.request_access(added_user) } + end end describe '#decline_group_invite' do @@ -1839,6 +1998,11 @@ describe NotificationService, :mailer do should_not_email_anyone end end + + it_behaves_like 'group emails are disabled' do + let(:notification_target) { group } + let(:notification_trigger) { group.add_guest(added_user) } + end end end @@ -1859,6 +2023,11 @@ describe NotificationService, :mailer do should_only_email(project.owner) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { project.request_access(added_user) } + end end context 'for a project in a group' do @@ -1878,7 +2047,7 @@ describe NotificationService, :mailer do end end - describe '#decline_group_invite' do + describe '#decline_project_invite' do let(:member) { create(:user) } before do @@ -1900,6 +2069,11 @@ describe NotificationService, :mailer do should_only_email(added_user) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { create_member! } + end + context 'when notifications are disabled' do before do create_global_setting_for(added_user, :disabled) @@ -2071,6 +2245,11 @@ describe NotificationService, :mailer do should_only_email(u_custom_notification_enabled, kind: :bcc) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { pipeline } + let(:notification_trigger) { notification.pipeline_finished(pipeline) } + end + context 'when the creator has group notification email set' do let(:group_notification_email) { 'user+group@example.com' } @@ -2100,6 +2279,11 @@ describe NotificationService, :mailer do should_only_email(u_member, kind: :bcc) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { pipeline } + let(:notification_trigger) { notification.pipeline_finished(pipeline) } + end + context 'when the creator has group notification email set' do let(:group_notification_email) { 'user+group@example.com' } @@ -2215,6 +2399,11 @@ describe NotificationService, :mailer do should_only_email(u_maintainer1, u_maintainer2, u_owner) end + it_behaves_like 'project emails are disabled' do + let(:notification_target) { domain } + let(:notification_trigger) { notify! } + end + it 'emails nobody if the project is missing' do domain.project = nil @@ -2224,30 +2413,6 @@ describe NotificationService, :mailer do end end end - - describe '#pages_domain_verification_failed' do - it 'emails current watching maintainers' do - notification.pages_domain_verification_failed(domain) - - should_only_email(u_maintainer1, u_maintainer2, u_owner) - end - end - - describe '#pages_domain_enabled' do - it 'emails current watching maintainers' do - notification.pages_domain_enabled(domain) - - should_only_email(u_maintainer1, u_maintainer2, u_owner) - end - end - - describe '#pages_domain_disabled' do - it 'emails current watching maintainers' do - notification.pages_domain_disabled(domain) - - should_only_email(u_maintainer1, u_maintainer2, u_owner) - end - end end context 'Auto DevOps notifications' do @@ -2266,6 +2431,11 @@ describe NotificationService, :mailer do should_email(owner, times: 1) # Once for the disable pipeline. should_email(pipeline_user, times: 2) # Once for the new permission, once for the disable. end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.autodevops_disabled(pipeline, [owner.email, pipeline_user.email]) } + end end end @@ -2279,6 +2449,11 @@ describe NotificationService, :mailer do should_email(user) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.repository_cleanup_success(project, user) } + end end describe '#repository_cleanup_failure' do @@ -2287,6 +2462,11 @@ describe NotificationService, :mailer do should_email(user) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.repository_cleanup_failure(project, user, 'Some error') } + end end end @@ -2320,6 +2500,11 @@ describe NotificationService, :mailer do should_only_email(u_maintainer1, u_maintainer2, u_owner) end + + it_behaves_like 'project emails are disabled' do + let(:notification_target) { project } + let(:notification_trigger) { notification.remote_mirror_update_failed(remote_mirror) } + end end end diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb index be2811ab1e7..4396ccab584 100644 --- a/spec/services/projects/update_remote_mirror_service_spec.rb +++ b/spec/services/projects/update_remote_mirror_service_spec.rb @@ -10,49 +10,91 @@ describe Projects::UpdateRemoteMirrorService do subject(:service) { described_class.new(project, project.creator) } - describe "#execute" do + describe '#execute' do + subject(:execute!) { service.execute(remote_mirror, 0) } + before do project.repository.add_branch(project.owner, 'existing-branch', 'master') allow(remote_mirror).to receive(:update_repository).and_return(true) end - it "ensures the remote exists" do + it 'ensures the remote exists' do stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror) expect(remote_mirror).to receive(:ensure_remote!) - service.execute(remote_mirror) + execute! end - it "fetches the remote repository" do + it 'fetches the remote repository' do expect(project.repository) .to receive(:fetch_remote) - .with(remote_mirror.remote_name, no_tags: true, ssh_auth: remote_mirror) + .with(remote_mirror.remote_name, no_tags: true, ssh_auth: remote_mirror) - service.execute(remote_mirror) + execute! end - it "returns success when updated succeeds" do + it 'marks the mirror as started when beginning' do + expect(remote_mirror).to receive(:update_start!).and_call_original + + execute! + end + + it 'marks the mirror as successfully finished' do stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror) - result = service.execute(remote_mirror) + result = execute! expect(result[:status]).to eq(:success) + expect(remote_mirror).to be_finished + end + + it 'marks the mirror as failed and raises the error when an unexpected error occurs' do + allow(project.repository).to receive(:fetch_remote).and_raise('Badly broken') + + expect { execute! }.to raise_error /Badly broken/ + + expect(remote_mirror).to be_failed + expect(remote_mirror.last_error).to include('Badly broken') + end + + context 'when the update fails because of a `Gitlab::Git::CommandError`' do + before do + allow(project.repository).to receive(:fetch_remote).and_raise(Gitlab::Git::CommandError.new('fetch failed')) + end + + it 'wraps `Gitlab::Git::CommandError`s in a service error' do + expect(execute!).to eq(status: :error, message: 'fetch failed') + end + + it 'marks the mirror as to be retried' do + execute! + + expect(remote_mirror).to be_to_retry + expect(remote_mirror.last_error).to include('fetch failed') + end + + it "marks the mirror as failed after #{described_class::MAX_TRIES} tries" do + service.execute(remote_mirror, described_class::MAX_TRIES) + + expect(remote_mirror).to be_failed + expect(remote_mirror.last_error).to include('fetch failed') + end end context 'when syncing all branches' do - it "push all the branches the first time" do + it 'push all the branches the first time' do stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror) expect(remote_mirror).to receive(:update_repository).with({}) - service.execute(remote_mirror) + execute! end end context 'when only syncing protected branches' do - it "sync updated protected branches" do + it 'sync updated protected branches' do stub_fetch_remote(project, remote_name: remote_name, ssh_auth: remote_mirror) protected_branch = create_protected_branch(project) remote_mirror.only_protected_branches = true @@ -61,7 +103,7 @@ describe Projects::UpdateRemoteMirrorService do .to receive(:update_repository) .with(only_branches_matching: [protected_branch.name]) - service.execute(remote_mirror) + execute! end def create_protected_branch(project) diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 82010dd283c..31bd0f0f836 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -369,9 +369,28 @@ describe Projects::UpdateService do end end + context 'when updating #emails_disabled' do + it 'updates the attribute for the project owner' do + expect { update_project(project, user, emails_disabled: true) } + .to change { project.emails_disabled } + .to(true) + end + + it 'does not update when not project owner' do + maintainer = create(:user) + project.add_user(maintainer, :maintainer) + + expect { update_project(project, maintainer, emails_disabled: true) } + .not_to change { project.emails_disabled } + end + end + context 'with external authorization enabled' do before do enable_external_authorization_service_check + + allow(::Gitlab::ExternalAuthorization) + .to receive(:access_allowed?).with(user, 'default_label', project.full_path).and_call_original end it 'does not save the project with an error if the service denies access' do @@ -402,8 +421,7 @@ describe Projects::UpdateService do end it 'does not check the label when it does not change' do - expect(::Gitlab::ExternalAuthorization) - .not_to receive(:access_allowed?) + expect(::Gitlab::ExternalAuthorization).to receive(:access_allowed?).once update_project(project, user, { name: 'New name' }) end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bcc133790d1..bd504f1553b 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -48,6 +48,9 @@ Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } quality_level = Quality::TestLevel.new RSpec.configure do |config| + config.filter_run focus: true + config.run_all_when_everything_filtered = true + config.use_transactional_fixtures = true config.use_instantiated_fixtures = false config.fixture_path = Rails.root diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb index 83ba654fab3..024340310a1 100644 --- a/spec/support/helpers/email_helpers.rb +++ b/spec/support/helpers/email_helpers.rb @@ -31,6 +31,10 @@ module EmailHelpers expect(ActionMailer::Base.deliveries).to be_empty end + def should_email_anyone + expect(ActionMailer::Base.deliveries).not_to be_empty + end + def email_recipients(kind: :to) ActionMailer::Base.deliveries.flat_map(&kind) end diff --git a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb index 82975027e5b..dcc92dda950 100644 --- a/spec/support/shared_examples/chat_slash_commands_shared_examples.rb +++ b/spec/support/shared_examples/chat_slash_commands_shared_examples.rb @@ -93,6 +93,19 @@ RSpec.shared_examples 'chat slash commands service' do subject.trigger(params) end + + context 'when user is blocked' do + before do + chat_name.user.block + end + + it 'blocks command execution' do + expect_any_instance_of(Gitlab::SlashCommands::Command).not_to receive(:execute) + + result = subject.trigger(params) + expect(result).to include(text: /^Whoops! This action is not allowed/) + end + end end end end diff --git a/spec/support/shared_examples/policies/clusterable_shared_examples.rb b/spec/support/shared_examples/policies/clusterable_shared_examples.rb index 4f9873d53e4..0b427c23256 100644 --- a/spec/support/shared_examples/policies/clusterable_shared_examples.rb +++ b/spec/support/shared_examples/policies/clusterable_shared_examples.rb @@ -13,7 +13,11 @@ shared_examples 'clusterable policies' do clusterable.add_developer(current_user) end + it { expect_disallowed(:read_cluster) } it { expect_disallowed(:add_cluster) } + it { expect_disallowed(:create_cluster) } + it { expect_disallowed(:update_cluster) } + it { expect_disallowed(:admin_cluster) } end context 'with a maintainer' do @@ -22,7 +26,11 @@ shared_examples 'clusterable policies' do end context 'with no clusters' do + it { expect_allowed(:read_cluster) } it { expect_allowed(:add_cluster) } + it { expect_allowed(:create_cluster) } + it { expect_allowed(:update_cluster) } + it { expect_allowed(:admin_cluster) } end end end diff --git a/spec/support/shared_examples/services/notification_service_shared_examples.rb b/spec/support/shared_examples/services/notification_service_shared_examples.rb new file mode 100644 index 00000000000..dd338ea47c7 --- /dev/null +++ b/spec/support/shared_examples/services/notification_service_shared_examples.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# Note that we actually update the attribute on the target_project/group, rather than +# using `allow`. This is because there are some specs where, based on how the notification +# is done, using an `allow` doesn't change the correct object. +shared_examples 'project emails are disabled' do + let(:target_project) { notification_target.is_a?(Project) ? notification_target : notification_target.project } + + before do + reset_delivered_emails! + target_project.clear_memoization(:emails_disabled) + end + + it 'sends no emails with project emails disabled' do + target_project.update_attribute(:emails_disabled, true) + + notification_trigger + + should_not_email_anyone + end + + it 'sends emails to someone' do + target_project.update_attribute(:emails_disabled, false) + + notification_trigger + + should_email_anyone + end +end + +shared_examples 'group emails are disabled' do + let(:target_group) { notification_target.is_a?(Group) ? notification_target : notification_target.project.group } + + before do + reset_delivered_emails! + target_group.clear_memoization(:emails_disabled) + end + + it 'sends no emails with group emails disabled' do + target_group.update_attribute(:emails_disabled, true) + + notification_trigger + + should_not_email_anyone + end + + it 'sends emails to someone' do + target_group.update_attribute(:emails_disabled, false) + + notification_trigger + + should_email_anyone + end +end diff --git a/spec/tasks/gitlab/update_templates_rake_spec.rb b/spec/tasks/gitlab/update_templates_rake_spec.rb index 7b17549b8c7..14b4ad5e3d8 100644 --- a/spec/tasks/gitlab/update_templates_rake_spec.rb +++ b/spec/tasks/gitlab/update_templates_rake_spec.rb @@ -8,9 +8,18 @@ describe 'gitlab:update_project_templates rake task' do before do Rake.application.rake_require 'tasks/gitlab/update_templates' create(:admin) + allow(Gitlab::ProjectTemplate) .to receive(:archive_directory) .and_return(Pathname.new(tmpdir)) + + # Gitlab::HTTP resolves the domain to an IP prior to WebMock taking effect, hence the wildcard + stub_request(:get, %r{^https://.*/api/v4/projects/gitlab-org%2Fproject-templates%2Frails/repository/commits\?page=1&per_page=1}) + .to_return( + status: 200, + body: [{ id: '67812735b83cb42710f22dc98d73d42c8bf4d907' }].to_json, + headers: { 'Content-Type' => 'application/json' } + ) end after do diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb index cbb4199954a..70cdc08b4b6 100644 --- a/spec/views/layouts/_head.html.haml_spec.rb +++ b/spec/views/layouts/_head.html.haml_spec.rb @@ -70,6 +70,23 @@ describe 'layouts/_head' do expect(rendered).to match('<link rel="stylesheet" media="all" href="/stylesheets/highlight/themes/solarised-light.css" />') end + context 'when an asset_host is set and snowplow url is set' do + let(:asset_host) { 'http://test.host' } + + before do + allow(ActionController::Base).to receive(:asset_host).and_return(asset_host) + allow(Gitlab::CurrentSettings).to receive(:snowplow_enabled?).and_return(true) + allow(Gitlab::CurrentSettings).to receive(:snowplow_collector_hostname).and_return('www.snow.plow') + end + + it 'add a snowplow script tag with asset host' do + render + expect(rendered).to match('http://test.host/assets/snowplow/') + expect(rendered).to match('window.snowplow') + expect(rendered).to match('www.snow.plow') + end + end + def stub_helper_with_safe_string(method) allow_any_instance_of(PageLayoutHelper).to receive(method) .and_return(%q{foo" http-equiv="refresh}.html_safe) diff --git a/spec/workers/build_process_worker_spec.rb b/spec/workers/build_process_worker_spec.rb deleted file mode 100644 index d9a02ece142..00000000000 --- a/spec/workers/build_process_worker_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -describe BuildProcessWorker do - describe '#perform' do - context 'when build exists' do - let(:pipeline) { create(:ci_pipeline) } - let(:build) { create(:ci_build, pipeline: pipeline) } - - it 'processes build' do - expect_any_instance_of(Ci::Pipeline).to receive(:process!) - .with([build.id]) - - described_class.new.perform(build.id) - end - end - - context 'when build does not exist' do - it 'does not raise exception' do - expect { described_class.new.perform(123) } - .not_to raise_error - end - end - end -end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index 39f1beb4efa..3b69b81f12e 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -14,6 +14,10 @@ describe PostReceive do create(:project, :repository, auto_cancel_pending_pipelines: 'disabled') end + def perform(changes: base64_changes) + described_class.new.perform(gl_repository, key_id, changes) + end + context "as a sidekiq worker" do it "responds to #perform" do expect(described_class.new).to respond_to(:perform) @@ -28,7 +32,7 @@ describe PostReceive do it "returns false and logs an error" do expect(Gitlab::GitLogger).to receive(:error).with("POST-RECEIVE: #{error_message}") - expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be(false) + expect(perform).to be(false) end end @@ -39,7 +43,7 @@ describe PostReceive do expect(Git::TagPushService).not_to receive(:new) expect_next_instance_of(SystemHooksService) { |service| expect(service).to receive(:execute_hooks) } - described_class.new.perform(gl_repository, key_id, "") + perform(changes: "") end end @@ -50,40 +54,80 @@ describe PostReceive do expect(Git::BranchPushService).not_to receive(:new) expect(Git::TagPushService).not_to receive(:new) - expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be false + expect(perform).to be false end end context 'with changes' do before do allow_any_instance_of(Gitlab::GitPostReceive).to receive(:identify).and_return(project.owner) + allow(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT]) end context "branches" do - let(:changes) { "123456 789012 refs/heads/tést" } + let(:changes) do + <<~EOF + '123456 789012 refs/heads/tést1' + '123456 789012 refs/heads/tést2' + EOF + end - it "calls Git::BranchPushService" do - expect_next_instance_of(Git::BranchPushService) do |service| + it 'expires the branches cache' do + expect(project.repository).to receive(:expire_branches_cache).once + + described_class.new.perform(gl_repository, key_id, base64_changes) + end + + it 'calls Git::BranchPushService' do + expect_any_instance_of(Git::BranchPushService) do |service| expect(service).to receive(:execute).and_return(true) end expect(Git::TagPushService).not_to receive(:new) - described_class.new.perform(gl_repository, key_id, base64_changes) + perform end end context "tags" do - let(:changes) { "123456 789012 refs/tags/tag" } + let(:changes) do + <<~EOF + 654321 210987 refs/tags/tag1 + 654322 210986 refs/tags/tag2 + 654323 210985 refs/tags/tag3 + 654324 210984 refs/tags/tag4 + 654325 210983 refs/tags/tag5 + EOF + end + + before do + expect(Gitlab::GlRepository).to receive(:parse).and_return([project, Gitlab::GlRepository::PROJECT]) + end + + it 'does not expire branches cache' do + expect(project.repository).not_to receive(:expire_branches_cache) + + described_class.new.perform(gl_repository, key_id, base64_changes) + end + + it "only invalidates tags once" do + expect(project.repository).to receive(:repository_event).exactly(5).times.with(:push_tag).and_call_original + expect(project.repository).to receive(:expire_caches_for_tags).once.and_call_original + expect(project.repository).to receive(:expire_tags_cache).once.and_call_original + + described_class.new.perform(gl_repository, key_id, base64_changes) + end it "calls Git::TagPushService" do - expect(Git::BranchPushService).not_to receive(:execute) + expect(Git::BranchPushService).not_to receive(:new) - expect_next_instance_of(Git::TagPushService) do |service| + expect_any_instance_of(Git::TagPushService) do |service| expect(service).to receive(:execute).and_return(true) end - described_class.new.perform(gl_repository, key_id, base64_changes) + expect(Git::BranchPushService).not_to receive(:new) + + perform end end @@ -94,7 +138,7 @@ describe PostReceive do expect(Git::BranchPushService).not_to receive(:new) expect(Git::TagPushService).not_to receive(:new) - described_class.new.perform(gl_repository, key_id, base64_changes) + perform end end @@ -111,7 +155,7 @@ describe PostReceive do let(:changes_count) { changes.lines.count } - subject { described_class.new.perform(gl_repository, key_id, base64_changes) } + subject { perform } context "with valid .gitlab-ci.yml" do before do @@ -180,7 +224,13 @@ describe PostReceive do it 'calls SystemHooksService' do expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(fake_hook_data, :repository_update_hooks).and_return(true) - described_class.new.perform(gl_repository, key_id, base64_changes) + perform + end + + it 'increments the usage data counter of pushes event' do + counter = Gitlab::UsageDataCounters::SourceCodeCounter + + expect { perform }.to change { counter.read(:pushes) }.by(1) end end end @@ -197,7 +247,7 @@ describe PostReceive do # a second to ensure we see changes. Timecop.freeze(1.second.from_now) do expect do - described_class.new.perform(gl_repository, key_id, base64_changes) + perform project.reload end.to change(project, :last_activity_at) .and change(project, :last_repository_updated_at) @@ -208,7 +258,8 @@ describe PostReceive do context "webhook" do it "fetches the correct project" do expect(Project).to receive(:find_by).with(id: project.id.to_s) - described_class.new.perform(gl_repository, key_id, base64_changes) + + perform end it "does not run if the author is not in the project" do @@ -218,16 +269,18 @@ describe PostReceive do expect(project).not_to receive(:execute_hooks) - expect(described_class.new.perform(gl_repository, key_id, base64_changes)).to be_falsey + expect(perform).to be_falsey end it "asks the project to trigger all hooks" do + create(:project_hook, push_events: true, tag_push_events: true, project: project) + create(:custom_issue_tracker_service, push_events: true, merge_requests_events: false, project: project) allow(Project).to receive(:find_by).and_return(project) expect(project).to receive(:execute_hooks).twice expect(project).to receive(:execute_services).twice - described_class.new.perform(gl_repository, key_id, base64_changes) + perform end it "enqueues a UpdateMergeRequestsWorker job" do @@ -235,7 +288,7 @@ describe PostReceive do expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args) - described_class.new.perform(gl_repository, key_id, base64_changes) + perform end end end diff --git a/spec/workers/repository_update_remote_mirror_worker_spec.rb b/spec/workers/repository_update_remote_mirror_worker_spec.rb index 4de51ecb3e9..66d517332ba 100644 --- a/spec/workers/repository_update_remote_mirror_worker_spec.rb +++ b/spec/workers/repository_update_remote_mirror_worker_spec.rb @@ -2,99 +2,70 @@ require 'rails_helper' -describe RepositoryUpdateRemoteMirrorWorker do +describe RepositoryUpdateRemoteMirrorWorker, :clean_gitlab_redis_shared_state do subject { described_class.new } - let(:remote_mirror) { create(:project, :repository, :remote_mirror).remote_mirrors.first } + let(:remote_mirror) { create(:remote_mirror) } let(:scheduled_time) { Time.now - 5.minutes } around do |example| Timecop.freeze(Time.now) { example.run } end - describe '#perform' do - context 'with status none' do - before do - remote_mirror.update(update_status: 'none') - end - - it 'sets status as finished when update remote mirror service executes successfully' do - expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success) - - expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.update_status }.to('finished') - end - - it 'resets the notification flag upon success' do - expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success) - remote_mirror.update_column(:error_notification_sent, true) - - expect { subject.perform(remote_mirror.id, Time.now) }.to change { remote_mirror.reload.error_notification_sent }.to(false) - end - - it 'sets status as failed when update remote mirror service executes with errors' do - error_message = 'fail!' - - expect_next_instance_of(Projects::UpdateRemoteMirrorService) do |service| - expect(service).to receive(:execute).with(remote_mirror).and_return(status: :error, message: error_message) - end + def expect_mirror_service_to_return(mirror, result, tries = 0) + expect_next_instance_of(Projects::UpdateRemoteMirrorService) do |service| + expect(service).to receive(:execute).with(mirror, tries).and_return(result) + end + end - # Mock the finder so that it returns an object we can set expectations on - expect_next_instance_of(RemoteMirrorFinder) do |finder| - expect(finder).to receive(:execute).and_return(remote_mirror) - end - expect(remote_mirror).to receive(:mark_as_failed).with(error_message) + describe '#perform' do + it 'calls out to the service to perform the update' do + expect_mirror_service_to_return(remote_mirror, status: :success) - expect do - subject.perform(remote_mirror.id, Time.now) - end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError, error_message) - end + subject.perform(remote_mirror.id, scheduled_time) + end - it 'does nothing if last_update_started_at is higher than the time the job was scheduled in' do - remote_mirror.update(last_update_started_at: Time.now) + it 'does not do anything if the mirror was already updated' do + remote_mirror.update(last_update_started_at: Time.now, update_status: :finished) - expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(true) - expect_any_instance_of(Projects::UpdateRemoteMirrorService).not_to receive(:execute).with(remote_mirror) + expect(Projects::UpdateRemoteMirrorService).not_to receive(:new) - expect(subject.perform(remote_mirror.id, scheduled_time)).to be_nil - end + subject.perform(remote_mirror.id, scheduled_time) end - context 'with unexpected error' do - it 'marks mirror as failed' do - allow_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_raise(RuntimeError) + it 'schedules a retry when the mirror is marked for retrying' do + remote_mirror = create(:remote_mirror, update_status: :to_retry) + expect_mirror_service_to_return(remote_mirror, status: :error, message: 'Retry!') - expect do - subject.perform(remote_mirror.id, Time.now) - end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateError) - expect(remote_mirror.reload.update_status).to eq('failed') - end - end + expect(described_class) + .to receive(:perform_in) + .with(remote_mirror.backoff_delay, remote_mirror.id, scheduled_time, 1) - context 'with another worker already running' do - before do - remote_mirror.update(update_status: 'started') - end - - it 'raises RemoteMirrorUpdateAlreadyInProgressError' do - expect do - subject.perform(remote_mirror.id, Time.now) - end.to raise_error(RepositoryUpdateRemoteMirrorWorker::UpdateAlreadyInProgressError) - end + subject.perform(remote_mirror.id, scheduled_time) end - context 'with status failed' do - before do - remote_mirror.update(update_status: 'failed') + it 'clears the lease if there was an unexpected exception' do + expect_next_instance_of(Projects::UpdateRemoteMirrorService) do |service| + expect(service).to receive(:execute).with(remote_mirror, 1).and_raise('Unexpected!') end + expect { subject.perform(remote_mirror.id, Time.now, 1) }.to raise_error('Unexpected!') - it 'sets status as finished if last_update_started_at is higher than the time the job was scheduled in' do - remote_mirror.update(last_update_started_at: Time.now) + lease = Gitlab::ExclusiveLease.new("#{described_class.name}:#{remote_mirror.id}", timeout: 1.second) - expect_any_instance_of(RemoteMirror).to receive(:updated_since?).with(scheduled_time).and_return(false) - expect_any_instance_of(Projects::UpdateRemoteMirrorService).to receive(:execute).with(remote_mirror).and_return(status: :success) + expect(lease.try_obtain).not_to be_nil + end - expect { subject.perform(remote_mirror.id, scheduled_time) }.to change { remote_mirror.reload.update_status }.to('finished') - end + it 'retries 3 times for the worker to finish before rescheduling' do + expect(subject).to receive(:in_lock) + .with("#{described_class.name}:#{remote_mirror.id}", + retries: 3, + ttl: remote_mirror.max_runtime, + sleep_sec: described_class::LOCK_WAIT_TIME) + .and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError) + expect(described_class).to receive(:perform_in) + .with(remote_mirror.backoff_delay, remote_mirror.id, scheduled_time, 0) + + subject.perform(remote_mirror.id, scheduled_time) end end end |