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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock1
-rw-r--r--README.md2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue129
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/helpers/keyset_helper.rb13
-rw-r--r--app/models/hooks/project_hook.rb5
-rw-r--r--app/models/hooks/web_hook.rb5
-rw-r--r--app/services/web_hook_service.rb4
-rw-r--r--app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json28
-rw-r--r--app/views/kaminari/gitlab/_keyset_paginator.html.haml30
-rw-r--r--changelogs/unreleased/330402-remove-unicorn-gitlab-unicorn-sampler.yml5
-rw-r--r--changelogs/unreleased/330737_fix_blob_preview.yml5
-rw-r--r--changelogs/unreleased/dz-redirect-old-repo-routes.yml5
-rw-r--r--changelogs/unreleased/handle-missing-branch-error.yml5
-rw-r--r--config/feature_flags/development/ci_drop_cyclical_triggered_pipelines.yml2
-rw-r--r--config/gitlab.yml.example8
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--config/initializers/7_prometheus_metrics.rb6
-rw-r--r--config/initializers/active_record_keyset_pagination.rb12
-rw-r--r--config/metrics/counts_all/20210216174832_cycle_analytics_views.yml8
-rw-r--r--config/routes/project.rb2
-rw-r--r--config/routes/repository.rb90
-rw-r--r--config/routes/repository_scoped.rb74
-rw-r--r--doc/administration/monitoring/performance/index.md1
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md16
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md49
-rw-r--r--doc/development/snowplow/index.md12
-rw-r--r--doc/development/usage_ping/dictionary.md8
-rw-r--r--lib/api/branches.rb4
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml19
-rw-r--r--lib/gitlab/metrics/samplers/unicorn_sampler.rb73
-rw-r--r--lib/gitlab/pagination/keyset/paginator.rb176
-rw-r--r--lib/gitlab/pagination/keyset/simple_order_builder.rb2
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb34
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js63
-rw-r--r--spec/helpers/keyset_helper_spec.rb94
-rw-r--r--spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb141
-rw-r--r--spec/lib/gitlab/pagination/keyset/paginator_spec.rb120
-rw-r--r--spec/models/hooks/project_hook_spec.rb11
-rw-r--r--spec/models/hooks/service_hook_spec.rb10
-rw-r--r--spec/models/hooks/system_hook_spec.rb10
-rw-r--r--spec/requests/api/branches_spec.rb39
-rw-r--r--spec/routing/project_routing_spec.rb28
-rw-r--r--spec/services/web_hook_service_spec.rb14
48 files changed, 840 insertions, 537 deletions
diff --git a/Gemfile b/Gemfile
index cc7d59ef732..724c7143119 100644
--- a/Gemfile
+++ b/Gemfile
@@ -341,7 +341,6 @@ group :metrics do
# Prometheus
gem 'prometheus-client-mmap', '~> 0.12.0'
- gem 'raindrops', '~> 0.18'
end
group :development do
diff --git a/Gemfile.lock b/Gemfile.lock
index fcf5acf6c29..6b0c9222858 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1592,7 +1592,6 @@ DEPENDENCIES
rails-controller-testing
rails-i18n (~> 6.0)
rainbow (~> 3.0)
- raindrops (~> 0.18)
rblineprof (~> 0.3.6)
rbtrace (~> 0.4)
rdoc (~> 6.1.2)
diff --git a/README.md b/README.md
index 103c23f152c..bc173b5403c 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,7 @@ GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL/OpenSUSE
- Ruby (MRI) 2.7.2
- Git 2.31+
-- Redis 4.0+
+- Redis 5.0+
- PostgreSQL 11+
For more information please see the [architecture](https://docs.gitlab.com/ee/development/architecture.html) and [requirements](https://docs.gitlab.com/ee/install/requirements.html) documentation.
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index 2cb1b6a195f..9775a9119c6 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -21,7 +21,7 @@ export const DEFAULT_ITERATIONS = DEFAULT_NONE_ANY.concat([
{ value: FILTER_CURRENT, text: __(FILTER_CURRENT) },
]);
-export const DEFAULT_LABELS = [{ value: 'No label', text: __('No label') }]; // eslint-disable-line @gitlab/require-i18n-strings
+export const DEFAULT_LABELS = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
{ value: 'Upcoming', text: __('Upcoming') }, // eslint-disable-line @gitlab/require-i18n-strings
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index 6ebc5431012..b325f846934 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -120,7 +120,7 @@ export default {
}, DEBOUNCE_DELAY);
},
handleTokenValueSelected(activeTokenValue) {
- if (this.isRecentTokenValuesEnabled) {
+ if (this.isRecentTokenValuesEnabled && activeTokenValue) {
setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue);
}
},
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index 76b005772ec..a8868da2dcc 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -1,27 +1,20 @@
<script>
-import {
- GlToken,
- GlFilteredSearchToken,
- GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
-} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { GlToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { deprecatedCreateFlash as createFlash } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-import { DEFAULT_LABELS, DEBOUNCE_DELAY } from '../constants';
+import { DEFAULT_LABELS } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
+import BaseToken from './base_token.vue';
+
export default {
components: {
+ BaseToken,
GlToken,
- GlFilteredSearchToken,
GlFilteredSearchSuggestion,
- GlDropdownDivider,
- GlLoadingIcon,
},
props: {
config: {
@@ -32,43 +25,24 @@ export default {
type: Object,
required: true,
},
+ active: {
+ type: Boolean,
+ required: true,
+ },
},
data() {
return {
labels: this.config.initialLabels || [],
defaultLabels: this.config.defaultLabels || DEFAULT_LABELS,
- loading: true,
+ loading: false,
};
},
- computed: {
- currentValue() {
- return this.value.data.toLowerCase();
- },
- activeLabel() {
- return this.labels.find(
- (label) => this.getLabelName(label).toLowerCase() === stripQuotes(this.currentValue),
+ methods: {
+ getActiveLabel(labels, currentValue) {
+ return labels.find(
+ (label) => this.getLabelName(label).toLowerCase() === stripQuotes(currentValue),
);
},
- containerStyle() {
- if (this.activeLabel) {
- const { color, textColor } = convertObjectPropsToCamelCase(this.activeLabel);
-
- return { backgroundColor: color, color: textColor };
- }
- return {};
- },
- },
- watch: {
- active: {
- immediate: true,
- handler(newValue) {
- if (!newValue && !this.labels.length) {
- this.fetchLabelBySearchTerm(this.value.data);
- }
- },
- },
- },
- methods: {
/**
* There's an inconsistency between private and public API
* for labels where label name is included in a different
@@ -84,6 +58,16 @@ export default {
getLabelName(label) {
return label.name || label.title;
},
+ getContainerStyle(activeLabel) {
+ if (activeLabel) {
+ const { color: backgroundColor, textColor: color } = convertObjectPropsToCamelCase(
+ activeLabel,
+ );
+
+ return { backgroundColor, color };
+ }
+ return {};
+ },
fetchLabelBySearchTerm(searchTerm) {
this.loading = true;
this.config
@@ -99,50 +83,47 @@ export default {
this.loading = false;
});
},
- searchLabels: debounce(function debouncedSearch({ data }) {
- if (!this.loading) this.fetchLabelBySearchTerm(data);
- }, DEBOUNCE_DELAY),
},
};
</script>
<template>
- <gl-filtered-search-token
- :config="config"
- v-bind="{ ...$props, ...$attrs }"
- v-on="$listeners"
- @input="searchLabels"
+ <base-token
+ :token-config="config"
+ :token-value="value"
+ :token-active="active"
+ :tokens-list-loading="loading"
+ :token-values="labels"
+ :fn-active-token-value="getActiveLabel"
+ :default-token-values="defaultLabels"
+ :recent-token-values-storage-key="config.recentTokenValuesStorageKey"
+ @fetch-token-values="fetchLabelBySearchTerm"
>
- <template #view-token="{ inputValue, cssClasses, listeners }">
- <gl-token variant="search-value" :class="cssClasses" :style="containerStyle" v-on="listeners"
- >~{{ activeLabel ? getLabelName(activeLabel) : inputValue }}</gl-token
+ <template
+ #view-token="{ viewTokenProps: { inputValue, cssClasses, listeners, activeTokenValue } }"
+ >
+ <gl-token
+ variant="search-value"
+ :class="cssClasses"
+ :style="getContainerStyle(activeTokenValue)"
+ v-on="listeners"
+ >~{{ activeTokenValue ? getLabelName(activeTokenValue) : inputValue }}</gl-token
>
</template>
- <template #suggestions>
+ <template #token-values-list="{ tokenValues }">
<gl-filtered-search-suggestion
- v-for="label in defaultLabels"
- :key="label.value"
- :value="label.value"
+ v-for="label in tokenValues"
+ :key="label.id"
+ :value="getLabelName(label)"
>
- {{ label.text }}
+ <div class="gl-display-flex gl-align-items-center">
+ <span
+ :style="{ backgroundColor: label.color }"
+ class="gl-display-inline-block mr-2 p-2"
+ ></span>
+ <div>{{ getLabelName(label) }}</div>
+ </div>
</gl-filtered-search-suggestion>
- <gl-dropdown-divider v-if="defaultLabels.length" />
- <gl-loading-icon v-if="loading" />
- <template v-else>
- <gl-filtered-search-suggestion
- v-for="label in labels"
- :key="label.id"
- :value="getLabelName(label)"
- >
- <div class="gl-display-flex gl-align-items-center">
- <span
- :style="{ backgroundColor: label.color }"
- class="gl-display-inline-block mr-2 p-2"
- ></span>
- <div>{{ getLabelName(label) }}</div>
- </div>
- </gl-filtered-search-suggestion>
- </template>
</template>
- </gl-filtered-search-token>
+ </base-token>
</template>
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index dbe628cb43a..17af44672d9 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -92,7 +92,7 @@ class Projects::BlobController < Projects::ApplicationController
@blob.load_all_data!
diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true)
diff_lines = diffy.diff.scan(/.*\n/)[2..-1]
- diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines)
+ diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines).to_a
@diff_lines = Gitlab::Diff::Highlight.new(diff_lines, repository: @repository).highlight
render layout: false
diff --git a/app/helpers/keyset_helper.rb b/app/helpers/keyset_helper.rb
new file mode 100644
index 00000000000..e7f6f884091
--- /dev/null
+++ b/app/helpers/keyset_helper.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module KeysetHelper
+ def keyset_paginate(paginator, without_first_and_last_pages: false)
+ page_params = params.to_unsafe_h
+
+ render('kaminari/gitlab/keyset_paginator', {
+ paginator: paginator,
+ without_first_and_last_pages: without_first_and_last_pages,
+ page_params: page_params
+ })
+ end
+end
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index a28b97e63e5..d1584a62bfb 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -39,6 +39,11 @@ class ProjectHook < WebHook
def rate_limit
project.actual_limits.limit_for(:web_hook_calls)
end
+
+ override :application_context
+ def application_context
+ super.merge(project: project)
+ end
end
ProjectHook.prepend_mod_with('ProjectHook')
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 02b4feb4ccc..f0c3791079a 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -80,6 +80,11 @@ class WebHook < ApplicationRecord
nil
end
+ # Custom attributes to be included in the worker context.
+ def application_context
+ { related_class: type }
+ end
+
private
def web_hooks_disable_failed?
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index c24ab10ad86..82a7e8501cd 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -93,7 +93,9 @@ class WebHookService
if rate_limited?(hook)
log_rate_limit(hook)
else
- WebHookWorker.perform_async(hook.id, data, hook_name)
+ Gitlab::ApplicationContext.with_context(hook.application_context) do
+ WebHookWorker.perform_async(hook.id, data, hook_name)
+ end
end
end
diff --git a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
index 7c3720dd2e6..dc08ce9dfad 100644
--- a/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
+++ b/app/validators/json_schemas/security_ci_configuration_schemas/sast_ui_schema.json
@@ -109,20 +109,6 @@
]
},
{
- "name": "kubesec",
- "label": "Kubesec",
- "enabled" : true,
- "description": "Kubernetes manifests, Helm Charts",
- "variables": []
- },
- {
- "name": "nodejs-scan",
- "label": "Node.js Scan",
- "enabled" : true,
- "description": "Node.js",
- "variables": []
- },
- {
"name": "gosec",
"label": "Gosec",
"enabled" : true,
@@ -140,6 +126,20 @@
]
},
{
+ "name": "kubesec",
+ "label": "Kubesec",
+ "enabled" : true,
+ "description": "Kubernetes manifests, Helm Charts",
+ "variables": []
+ },
+ {
+ "name": "nodejs-scan",
+ "label": "Node.js Scan",
+ "enabled" : true,
+ "description": "Node.js",
+ "variables": []
+ },
+ {
"name": "phpcs-security-audit",
"label": "PHP Security Audit",
"enabled" : true,
diff --git a/app/views/kaminari/gitlab/_keyset_paginator.html.haml b/app/views/kaminari/gitlab/_keyset_paginator.html.haml
new file mode 100644
index 00000000000..f64c70dadfc
--- /dev/null
+++ b/app/views/kaminari/gitlab/_keyset_paginator.html.haml
@@ -0,0 +1,30 @@
+- previous_path = url_for(page_params.merge(cursor: paginator.cursor_for_previous_page))
+- next_path = url_for(page_params.merge(cursor: paginator.cursor_for_next_page))
+
+.gl-pagination.gl-mt-3
+ %ul.pagination.justify-content-center
+
+ - if paginator.has_previous_page?
+ - unless without_first_and_last_pages
+ %li.page-item
+ - first_page_path = url_for(page_params.merge(cursor: paginator.cursor_for_first_page))
+ = link_to first_page_path, rel: 'first', class: 'page-link' do
+ = sprite_icon('angle-double-left', size: 8)
+ = s_('Pagination|First')
+
+ %li.page-item.prev
+ = link_to previous_path, rel: 'prev', class: 'page-link' do
+ = sprite_icon('angle-left', size: 8)
+ = s_('Pagination|Prev')
+
+ - if paginator.has_next_page?
+ %li.page-item.next
+ = link_to next_path, rel: 'next', class: 'page-link' do
+ = s_('Pagination|Next')
+ = sprite_icon('angle-right', size: 8)
+ - unless without_first_and_last_pages
+ %li.page-item
+ - last_page_path = url_for(page_params.merge(cursor: paginator.cursor_for_last_page))
+ = link_to last_page_path, rel: 'last', class: 'page-link' do
+ = s_('Pagination|Last')
+ = sprite_icon('angle-double-right', size: 8)
diff --git a/changelogs/unreleased/330402-remove-unicorn-gitlab-unicorn-sampler.yml b/changelogs/unreleased/330402-remove-unicorn-gitlab-unicorn-sampler.yml
new file mode 100644
index 00000000000..1773ea98cab
--- /dev/null
+++ b/changelogs/unreleased/330402-remove-unicorn-gitlab-unicorn-sampler.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Unicorn Sampler
+merge_request: 62090
+author:
+type: removed
diff --git a/changelogs/unreleased/330737_fix_blob_preview.yml b/changelogs/unreleased/330737_fix_blob_preview.yml
new file mode 100644
index 00000000000..8c322dce5bb
--- /dev/null
+++ b/changelogs/unreleased/330737_fix_blob_preview.yml
@@ -0,0 +1,5 @@
+---
+title: Fix blob preview error
+merge_request: 62128
+author:
+type: fixed
diff --git a/changelogs/unreleased/dz-redirect-old-repo-routes.yml b/changelogs/unreleased/dz-redirect-old-repo-routes.yml
new file mode 100644
index 00000000000..a6c72e43433
--- /dev/null
+++ b/changelogs/unreleased/dz-redirect-old-repo-routes.yml
@@ -0,0 +1,5 @@
+---
+title: Redirect some of deprecated repository routes
+merge_request: 34867
+author:
+type: removed
diff --git a/changelogs/unreleased/handle-missing-branch-error.yml b/changelogs/unreleased/handle-missing-branch-error.yml
new file mode 100644
index 00000000000..01c6362bbe8
--- /dev/null
+++ b/changelogs/unreleased/handle-missing-branch-error.yml
@@ -0,0 +1,5 @@
+---
+title: Return 404 from branches API when repository does not exist
+merge_request:
+author:
+type: fixed
diff --git a/config/feature_flags/development/ci_drop_cyclical_triggered_pipelines.yml b/config/feature_flags/development/ci_drop_cyclical_triggered_pipelines.yml
index 6a08d4aa72c..6d411dd377f 100644
--- a/config/feature_flags/development/ci_drop_cyclical_triggered_pipelines.yml
+++ b/config/feature_flags/development/ci_drop_cyclical_triggered_pipelines.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329390
milestone: '13.12'
type: development
group: group::continuous integration
-default_enabled: true
+default_enabled: false
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index aee9dd455af..81eb9878376 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -33,7 +33,7 @@ production: &base
host: localhost
port: 80 # Set to 443 if using HTTPS, see installation.md#using-https for additional HTTPS configuration details
https: false # Set to true if using HTTPS, see installation.md#using-https for additional HTTPS configuration details
- # The maximum time unicorn/puma can spend on the request. This needs to be smaller than the worker timeout.
+ # The maximum time Puma can spend on the request. This needs to be smaller than the worker timeout.
# Default is 95% of the worker timeout
max_request_duration_seconds: 57
@@ -153,7 +153,7 @@ production: &base
### GraphQL Settings
# Tells the rails application how long it has to complete a GraphQL request.
# We suggest this value to be higher than the database timeout value
- # and lower than the worker timeout set in unicorn/puma. (default: 30)
+ # and lower than the worker timeout set in Puma. (default: 30)
# graphql_timeout: 30
## Repository downloads directory
@@ -1212,8 +1212,6 @@ production: &base
## Monitoring
# Built in monitoring settings
monitoring:
- # Time between sampling of unicorn socket metrics, in seconds
- # unicorn_sampler_interval: 10
# IP whitelist to access monitoring endpoints
ip_whitelist:
- 127.0.0.0/8
@@ -1225,7 +1223,7 @@ production: &base
# address: localhost
# port: 8082
- # Web exporter is webserver built in to Unicorn/Puma to expose Prometheus metrics
+ # Web exporter is a dedicated Rack server running alongside Puma to expose Prometheus metrics
# It runs alongside the `/metrics` endpoints to ease the publish of metrics
web_exporter:
# enabled: true
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 8d5e19afc20..c00d0c04e5f 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -902,7 +902,6 @@ Settings.webpack.dev_server['https'] ||= false
#
Settings['monitoring'] ||= Settingslogic.new({})
Settings.monitoring['ip_whitelist'] ||= ['127.0.0.1/8']
-Settings.monitoring['unicorn_sampler_interval'] ||= 10
Settings.monitoring['sidekiq_exporter'] ||= Settingslogic.new({})
Settings.monitoring.sidekiq_exporter['enabled'] ||= false
Settings.monitoring.sidekiq_exporter['log_enabled'] ||= false
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index a304f861db8..35acf26c796 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -8,8 +8,6 @@ def prometheus_default_multiproc_dir
if Gitlab::Runtime.sidekiq?
Rails.root.join('tmp/prometheus_multiproc_dir/sidekiq')
- elsif Gitlab::Runtime.unicorn?
- Rails.root.join('tmp/prometheus_multiproc_dir/unicorn')
elsif Gitlab::Runtime.puma?
Rails.root.join('tmp/prometheus_multiproc_dir/puma')
else
@@ -49,9 +47,7 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
::Prometheus::Client.reinitialize_on_pid_change(force: true)
- if Gitlab::Runtime.unicorn?
- Gitlab::Metrics::Samplers::UnicornSampler.instance(Settings.monitoring.unicorn_sampler_interval).start
- elsif Gitlab::Runtime.puma?
+ if Gitlab::Runtime.puma?
Gitlab::Metrics::Samplers::PumaSampler.instance.start
end
diff --git a/config/initializers/active_record_keyset_pagination.rb b/config/initializers/active_record_keyset_pagination.rb
new file mode 100644
index 00000000000..f8c2ada5255
--- /dev/null
+++ b/config/initializers/active_record_keyset_pagination.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module PaginatorExtension
+ # This method loads the records for the requested page and returns a keyset paginator object.
+ def keyset_paginate(cursor: nil, per_page: 20)
+ Gitlab::Pagination::Keyset::Paginator.new(scope: self.dup, cursor: cursor, per_page: per_page)
+ end
+end
+
+ActiveSupport.on_load(:active_record) do
+ ActiveRecord::Relation.include(PaginatorExtension)
+end
diff --git a/config/metrics/counts_all/20210216174832_cycle_analytics_views.yml b/config/metrics/counts_all/20210216174832_cycle_analytics_views.yml
index 77baa673bb2..ce8674574f2 100644
--- a/config/metrics/counts_all/20210216174832_cycle_analytics_views.yml
+++ b/config/metrics/counts_all/20210216174832_cycle_analytics_views.yml
@@ -1,6 +1,6 @@
---
key_path: counts.cycle_analytics_views
-description:
+description: Total visits to VSA (both group- and project-level) all time
product_section: dev
product_stage: manage
product_group: group::optimize
@@ -8,9 +8,11 @@ product_category:
value_type: number
status: data_available
time_frame: all
-data_source: database
+data_source: redis
distribution:
- ce
+- ee
tier:
- free
-skip_validation: true
+- premium
+- ultimate
diff --git a/config/routes/project.rb b/config/routes/project.rb
index d62e2f1b2f2..935e816f73c 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -386,7 +386,6 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
# The wiki and repository routing contains wildcard characters so
# its preferable to keep it below all other project routes
draw :repository_scoped
- draw :repository
draw :wiki
namespace :import do
@@ -584,6 +583,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
# Introduced in 12.0.
# Should be removed with https://gitlab.com/gitlab-org/gitlab/issues/28848.
Gitlab::Routing.redirect_legacy_paths(self, :mirror, :tags, :hooks,
+ :commits, :commit, :find_file, :files, :compare,
:cycle_analytics, :mattermost, :variables, :triggers,
:environments, :protected_environments, :error_tracking, :alert_management,
:tracing,
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
index 58de3d29bb0..854c6517cd5 100644
--- a/config/routes/repository.rb
+++ b/config/routes/repository.rb
@@ -1,84 +1,36 @@
# frozen_string_literal: true
# All routing related to repository browsing
+#
+# NOTE: Add new routes to repository_scoped.rb instead (see
+# https://docs.gitlab.com/ee/development/routing.html#project-routes).
resource :repository, only: [:create]
-resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
- member do
- get :branches
- get :pipelines
- post :revert
- post :cherry_pick
- get :diff_for_path
- get :diff_files
- get :merge_requests
- end
-end
-
-# NOTE: Add new routes to repository_scoped.rb instead (see
-# https://docs.gitlab.com/ee/development/routing.html#project-routes).
-#
# Don't use format parameter as file extension (old 3.0.x behavior)
# See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
scope format: false do
- get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
+ get '/refs/switch',
+ to: redirect('%{namespace_id}/%{project_id}/-/refs/switch')
- resources :compare, only: [:index, :create] do
- collection do
- get :diff_for_path
- get :signatures
- end
- end
-
- resources :refs, only: [] do
- collection do
- get 'switch'
- end
+ get '/refs/:id/logs_tree',
+ to: redirect('%{namespace_id}/%{project_id}/-/refs/%{id}/logs_tree'),
+ constraints: { id: Gitlab::PathRegex.git_reference_regex }
- member do
- # tree viewer logs
- get 'logs_tree', constraints: { id: Gitlab::PathRegex.git_reference_regex }
- # Directories with leading dots erroneously get rejected if git
- # ref regex used in constraints. Regex verification now done in controller.
- get 'logs_tree/*path', action: :logs_tree, as: :logs_file, format: false, constraints: {
- id: /.*/,
- path: /[^\0]*/
- }
- end
- end
+ get '/refs/:id/logs_tree/*path',
+ to: redirect('%{namespace_id}/%{project_id}/-/refs/%{id}/logs_tree/%{path}'),
+ constraints: { id: /.*/, path: /[^\0]*/ }
scope constraints: { id: /[^\0]+/ } do
- scope controller: :blob do
- get '/new/*id', action: :new, as: :new_blob
- post '/create/*id', action: :create, as: :create_blob
- get '/edit/*id', action: :edit, as: :edit_blob
- put '/update/*id', action: :update, as: :update_blob
- post '/preview/*id', action: :preview, as: :preview_blob
-
- scope path: '/blob/*id', as: :blob do
- get :diff
- get '/', action: :show
- delete '/', action: :destroy
- post '/', action: :create
- put '/', action: :update
- end
- end
-
- get '/tree/*id', to: 'tree#show', as: :tree
- get '/raw/*id', to: 'raw#show', as: :raw
- get '/blame/*id', to: 'blame#show', as: :blame
-
- get '/commits', to: 'commits#commits_root', as: :commits_root
- get '/commits/*id/signatures', to: 'commits#signatures', as: :signatures
- get '/commits/*id', to: 'commits#show', as: :commits
-
- post '/create_dir/*id', to: 'tree#create_dir', as: :create_dir
-
- scope controller: :find_file do
- get '/find_file/*id', action: :show, as: :find_file
-
- get '/files/*id', action: :list, as: :files
- end
+ # Deprecated. Keep for compatibility.
+ # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/118849
+ get '/tree/*id', to: 'tree#show', as: :deprecated_tree
+ get '/blob/*id', to: 'blob#show', as: :deprecated_blob
+ get '/raw/*id', to: 'raw#show', as: :deprecated_raw
+ get '/blame/*id', to: 'blame#show', as: :deprecated_blame
+
+ # Redirect those explicitly since `redirect_legacy_paths` conflicts with project new/edit actions
+ get '/new/*id', to: redirect('%{namespace_id}/%{project_id}/-/new/%{id}')
+ get '/edit/*id', to: redirect('%{namespace_id}/%{project_id}/-/edit/%{id}')
end
end
diff --git a/config/routes/repository_scoped.rb b/config/routes/repository_scoped.rb
index 7fabf3ff895..d2be18c62f9 100644
--- a/config/routes/repository_scoped.rb
+++ b/config/routes/repository_scoped.rb
@@ -6,6 +6,33 @@
# Don't use format parameter as file extension (old 3.0.x behavior)
# See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
scope format: false do
+ get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
+
+ resources :compare, only: [:index, :create] do
+ collection do
+ get :diff_for_path
+ get :signatures
+ end
+ end
+
+ resources :refs, only: [] do
+ collection do
+ get 'switch'
+ end
+
+ member do
+ # tree viewer logs
+ get 'logs_tree', constraints: { id: Gitlab::PathRegex.git_reference_regex }
+
+ # Directories with leading dots erroneously get rejected if git
+ # ref regex used in constraints. Regex verification now done in controller.
+ get 'logs_tree/*path', action: :logs_tree, as: :logs_file, format: false, constraints: {
+ id: /.*/,
+ path: /[^\0]*/
+ }
+ end
+ end
+
scope constraints: { id: Gitlab::PathRegex.git_reference_regex } do
resources :network, only: [:show]
@@ -38,4 +65,51 @@ scope format: false do
end
end
end
+
+ scope constraints: { id: /[^\0]+/ } do
+ scope controller: :blob do
+ get '/new/*id', action: :new, as: :new_blob
+ post '/create/*id', action: :create, as: :create_blob
+ get '/edit/*id', action: :edit, as: :edit_blob
+ put '/update/*id', action: :update, as: :update_blob
+ post '/preview/*id', action: :preview, as: :preview_blob
+
+ scope path: '/blob/*id', as: :blob do
+ get :diff
+ get '/', action: :show
+ delete '/', action: :destroy
+ post '/', action: :create
+ put '/', action: :update
+ end
+ end
+
+ get '/tree/*id', to: 'tree#show', as: :tree
+ get '/raw/*id', to: 'raw#show', as: :raw
+ get '/blame/*id', to: 'blame#show', as: :blame
+
+ get '/commits', to: 'commits#commits_root', as: :commits_root
+ get '/commits/*id/signatures', to: 'commits#signatures', as: :signatures
+ get '/commits/*id', to: 'commits#show', as: :commits
+
+ post '/create_dir/*id', to: 'tree#create_dir', as: :create_dir
+
+ scope controller: :find_file do
+ get '/find_file/*id', action: :show, as: :find_file
+ get '/files/*id', action: :list, as: :files
+ end
+ end
+end
+
+resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
+ member do
+ get :branches
+ get :pipelines
+ post :revert
+ post :cherry_pick
+ get :diff_for_path
+ get :diff_files
+ get :merge_requests
+ end
end
+
+resource :repository, only: [:create]
diff --git a/doc/administration/monitoring/performance/index.md b/doc/administration/monitoring/performance/index.md
index 15c54a36f6c..f3db6ac9f03 100644
--- a/doc/administration/monitoring/performance/index.md
+++ b/doc/administration/monitoring/performance/index.md
@@ -69,6 +69,5 @@ The following environment variables are recognized:
- `DATABASE_SAMPLER_INTERVAL_SECONDS`
- `ACTION_CABLE_SAMPLER_INTERVAL_SECONDS`
- `PUMA_SAMPLER_INTERVAL_SECONDS`
-- `UNICORN_SAMPLER_INTERVAL_SECONDS`
- `THREADS_SAMPLER_INTERVAL_SECONDS`
- `GLOBAL_SEARCH_SAMPLER_INTERVAL_SECONDS`
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index f29db9ead38..faa24e76a0e 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -308,20 +308,8 @@ Some basic Ruby runtime metrics are available:
| `ruby_process_proportional_memory_bytes` | Gauge | 13.0 | Memory usage by process (PSS/Proportional Set Size) |
| `ruby_process_start_time_seconds` | Gauge | 12.0 | UNIX timestamp of process start time |
-## Unicorn Metrics
-
-Unicorn specific metrics, when Unicorn is used.
-
-| Metric | Type | Since | Description |
-|:-----------------------------|:------|:------|:---------------------------------------------------|
-| `unicorn_active_connections` | Gauge | 11.0 | The number of active Unicorn connections (workers) |
-| `unicorn_queued_connections` | Gauge | 11.0 | The number of queued Unicorn connections |
-| `unicorn_workers` | Gauge | 12.0 | The number of Unicorn workers |
-
## Puma Metrics
-When Puma is used instead of Unicorn, the following metrics are available:
-
| Metric | Type | Since | Description |
|:--------------------------------- |:------- |:----- |:----------- |
| `puma_workers` | Gauge | 12.0 | Total number of workers |
@@ -352,8 +340,8 @@ instance (`cache`, `shared_state` etc.).
## Metrics shared directory
The GitLab Prometheus client requires a directory to store metrics data shared between multi-process services.
-Those files are shared among all instances running under Unicorn server.
-The directory must be accessible to all running Unicorn's processes, or
+Those files are shared among all instances running under Puma server.
+The directory must be accessible to all running Puma's processes, or
metrics can't function correctly.
This directory's location is configured using environment variable `prometheus_multiproc_dir`.
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index 588be73e786..dfa17b4e85d 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -863,55 +863,6 @@ license.save
License.current # check to make sure it applied
```
-## Unicorn
-
-From [Zendesk ticket #91083](https://gitlab.zendesk.com/agent/tickets/91083) (internal)
-
-### Poll Unicorn requests by seconds
-
-```ruby
-require 'rubygems'
-require 'unicorn'
-
-# Usage for this program
-def usage
- puts "ruby unicorn_status.rb <path to unix socket> <poll interval in seconds>"
- puts "Polls the given Unix socket every interval in seconds. Will not allow you to drop below 3 second poll intervals."
- puts "Example: /opt/gitlab/embedded/bin/ruby poll_unicorn.rb /var/opt/gitlab/gitlab-rails/sockets/gitlab.socket 10"
-end
-
-# Look for required args. Throw usage and exit if they don't exist.
-if ARGV.count < 2
- usage
- exit 1
-end
-
-# Get the socket and threshold values.
-socket = ARGV[0]
-threshold = (ARGV[1]).to_i
-
-# Check threshold - is it less than 3? If so, set to 3 seconds. Safety first!
-if threshold.to_i < 3
- threshold = 3
-end
-
-# Check - does that socket exist?
-unless File.exist?(socket)
- puts "Socket file not found: #{socket}"
- exit 1
-end
-
-# Poll the given socket every THRESHOLD seconds as specified above.
-puts "Running infinite loop. Use CTRL+C to exit."
-puts "------------------------------------------"
-loop do
- Raindrops::Linux.unix_listener_stats([socket]).each do |addr, stats|
- puts DateTime.now.to_s + " Active: " + stats.active.to_s + " Queued: " + stats.queued.to_s
- end
- sleep threshold
-end
-```
-
## Registry
### Registry Disk Space Usage by Project
diff --git a/doc/development/snowplow/index.md b/doc/development/snowplow/index.md
index ece72dbbf03..6e2bf00f191 100644
--- a/doc/development/snowplow/index.md
+++ b/doc/development/snowplow/index.md
@@ -444,7 +444,15 @@ There are several tools for developing and testing Snowplow Event
**{check-circle}** Available, **{status_preparing}** In progress, **{dotted-circle}** Not Planned
-### Snowplow Analytics Debugger Chrome Extension
+### Test frontend events
+
+To test frontend events in development:
+
+- [Enable Snowplow in the admin area](#enabling-snowplow).
+- Turn off any ad blockers that would prevent Snowplow JS from loading in your environment.
+- Turn off "Do Not Track" (DNT) in your browser.
+
+#### Snowplow Analytics Debugger Chrome Extension
Snowplow Analytics Debugger is a browser extension for testing frontend events. This works on production, staging and local development environments.
@@ -452,7 +460,7 @@ Snowplow Analytics Debugger is a browser extension for testing frontend events.
1. Open Chrome DevTools to the Snowplow Analytics Debugger tab.
1. Learn more at [Igloo Analytics](https://www.iglooanalytics.com/blog/snowplow-analytics-debugger-chrome-extension.html).
-### Snowplow Inspector Chrome Extension
+#### Snowplow Inspector Chrome Extension
Snowplow Inspector Chrome Extension is a browser extension for testing frontend events. This works on production, staging and local development environments.
diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md
index 250945ac47f..c69b9e76103 100644
--- a/doc/development/usage_ping/dictionary.md
+++ b/doc/development/usage_ping/dictionary.md
@@ -936,7 +936,7 @@ Tiers: `free`
### `counts.cycle_analytics_views`
-Missing description
+Total visits to VSA (both group- and project-level) all time
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/config/metrics/counts_all/20210216174832_cycle_analytics_views.yml)
@@ -944,7 +944,7 @@ Group: `group::optimize`
Status: `data_available`
-Tiers: `free`
+Tiers: `free`, `premium`, `ultimate`
### `counts.dast_jobs`
@@ -4128,7 +4128,7 @@ Tiers: `free`, `premium`, `ultimate`
### `counts.productivity_analytics_views`
-Missing description
+Total visits to /groups/:group/-/analytics/productivity_analytics all time
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_all/20210216174834_productivity_analytics_views.yml)
@@ -4136,7 +4136,7 @@ Group: `group::optimize`
Status: `data_available`
-Tiers: `free`
+Tiers: `free`, `premium`, `ultimate`
### `counts.project_clusters_disabled`
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 1ee120f982a..0db5bb82296 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -17,6 +17,10 @@ module API
authorize! :download_code, user_project
end
+ rescue_from Gitlab::Git::Repository::NoRepository do
+ not_found!
+ end
+
helpers do
params :filter_params do
optional :search, type: String, desc: 'Return list of branches matching the search criteria'
diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
index c628e30b2c7..92e74f9eb00 100644
--- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
@@ -1,10 +1,21 @@
-# Read more about this feature here: https://docs.gitlab.com/ee/user/application_security/container_scanning/
+# Use this template to enable container scanning in your project.
+# You should add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword.
+# The template should work without modifications but you can customize the template settings if
+# needed: https://docs.gitlab.com/ee/user/application_security/container_scanning/#customizing-the-container-scanning-settings
+#
+# Requirements:
+# - You must define the image to be scanned in the DOCKER_IMAGE variable. If DOCKER_IMAGE is the
+# same as $CI_APPLICATION_REPOSITORY:$CI_APPLICATION_TAG, you can skip this.
+# - Container registry credentials defined by `DOCKER_USER` and `DOCKER_PASSWORD` variables if the image to be scanned is in a private registry.
+# - For auto-remediation, a readable Dockerfile in the root of the project or as defined by the
+# DOCKERFILE_PATH variable.
+#
+# For more information, see https://docs.gitlab.com/ee/user/application_security/container_scanning/#available-variables
variables:
- # Setting this variable will affect all Security templates
- # (SAST, Dependency Scanning, ...)
+ # Setting this variable will affect all Security templates (e.g.: SAST, Dependency Scanning)
SECURE_ANALYZERS_PREFIX: "registry.gitlab.com/gitlab-org/security-products/analyzers"
- CS_MAJOR_VERSION: 3
+ CS_MAJOR_VERSION: 3 # The major version of the analyzer image to be used for scanning
.cs_common:
stage: test
diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
deleted file mode 100644
index 2fa324f3fea..00000000000
--- a/lib/gitlab/metrics/samplers/unicorn_sampler.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module Metrics
- module Samplers
- class UnicornSampler < BaseSampler
- DEFAULT_SAMPLING_INTERVAL_SECONDS = 5
-
- def metrics
- @metrics ||= init_metrics
- end
-
- def init_metrics
- {
- unicorn_active_connections: ::Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max),
- unicorn_queued_connections: ::Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max),
- unicorn_workers: ::Gitlab::Metrics.gauge(:unicorn_workers, 'Unicorn workers')
- }
- end
-
- def enabled?
- # Raindrops::Linux.tcp_listener_stats is only present on Linux
- unicorn_with_listeners? && Raindrops::Linux.respond_to?(:tcp_listener_stats)
- end
-
- def sample
- Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats|
- set_unicorn_connection_metrics('tcp', addr, stats)
- end
- Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats|
- set_unicorn_connection_metrics('unix', addr, stats)
- end
-
- metrics[:unicorn_workers].set({}, unicorn_workers_count)
- end
-
- private
-
- def tcp_listeners
- @tcp_listeners ||= Unicorn.listener_names.grep(%r{\A[^/]+:\d+\z})
- end
-
- def set_unicorn_connection_metrics(type, addr, stats)
- labels = { socket_type: type, socket_address: addr }
-
- metrics[:unicorn_active_connections].set(labels, stats.active)
- metrics[:unicorn_queued_connections].set(labels, stats.queued)
- end
-
- def unix_listeners
- @unix_listeners ||= Unicorn.listener_names - tcp_listeners
- end
-
- def unicorn_with_listeners?
- defined?(Unicorn) && Unicorn.listener_names.any?
- end
-
- def unicorn_workers_count
- http_servers.sum(&:worker_processes)
- end
-
- # Traversal of ObjectSpace is expensive, on fully loaded application
- # it takes around 80ms. The instances of HttpServers are not a subject
- # to change so we can cache the list of servers.
- def http_servers
- return [] unless Gitlab::Runtime.unicorn?
-
- @http_servers ||= ObjectSpace.each_object(::Unicorn::HttpServer).to_a
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/pagination/keyset/paginator.rb b/lib/gitlab/pagination/keyset/paginator.rb
new file mode 100644
index 00000000000..2ec4472fcd6
--- /dev/null
+++ b/lib/gitlab/pagination/keyset/paginator.rb
@@ -0,0 +1,176 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Pagination
+ module Keyset
+ class Paginator
+ include Enumerable
+
+ module Base64CursorConverter
+ def self.dump(cursor_attributes)
+ Base64.urlsafe_encode64(Gitlab::Json.dump(cursor_attributes))
+ end
+
+ def self.parse(cursor)
+ Gitlab::Json.parse(Base64.urlsafe_decode64(cursor)).with_indifferent_access
+ end
+ end
+
+ FORWARD_DIRECTION = 'n'
+ BACKWARD_DIRECTION = 'p'
+
+ UnsupportedScopeOrder = Class.new(StandardError)
+
+ # scope - ActiveRecord::Relation object with order by clause
+ # cursor - Encoded cursor attributes as String. Empty value will requests the first page.
+ # per_page - Number of items per page.
+ # cursor_converter - Object that serializes and de-serializes the cursor attributes. Implements dump and parse methods.
+ # direction_key - Symbol that will be the hash key of the direction within the cursor. (default: _kd => keyset direction)
+ def initialize(scope:, cursor: nil, per_page: 20, cursor_converter: Base64CursorConverter, direction_key: :_kd)
+ @keyset_scope = build_scope(scope)
+ @order = Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(@keyset_scope)
+ @per_page = per_page
+ @cursor_converter = cursor_converter
+ @direction_key = direction_key
+ @has_another_page = false
+ @at_last_page = false
+ @at_first_page = false
+ @cursor_attributes = decode_cursor_attributes(cursor)
+
+ set_pagination_helper_flags!
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def records
+ @records ||= begin
+ items = if paginate_backward?
+ reversed_order
+ .apply_cursor_conditions(keyset_scope, cursor_attributes)
+ .reorder(reversed_order)
+ .limit(per_page_plus_one)
+ .to_a
+ else
+ order
+ .apply_cursor_conditions(keyset_scope, cursor_attributes)
+ .limit(per_page_plus_one)
+ .to_a
+ end
+
+ @has_another_page = items.size == per_page_plus_one
+ items.pop if @has_another_page
+ items.reverse! if paginate_backward?
+ items
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # This and has_previous_page? methods are direction aware. In case we paginate backwards,
+ # has_next_page? will mean that we have a previous page.
+ def has_next_page?
+ records
+
+ if at_last_page?
+ false
+ elsif paginate_forward?
+ @has_another_page
+ elsif paginate_backward?
+ true
+ end
+ end
+
+ def has_previous_page?
+ records
+
+ if at_first_page?
+ false
+ elsif paginate_backward?
+ @has_another_page
+ elsif paginate_forward?
+ true
+ end
+ end
+
+ def cursor_for_next_page
+ if has_next_page?
+ data = order.cursor_attributes_for_node(records.last)
+ data[direction_key] = FORWARD_DIRECTION
+ cursor_converter.dump(data)
+ else
+ nil
+ end
+ end
+
+ def cursor_for_previous_page
+ if has_previous_page?
+ data = order.cursor_attributes_for_node(records.first)
+ data[direction_key] = BACKWARD_DIRECTION
+ cursor_converter.dump(data)
+ end
+ end
+
+ def cursor_for_first_page
+ cursor_converter.dump({ direction_key => FORWARD_DIRECTION })
+ end
+
+ def cursor_for_last_page
+ cursor_converter.dump({ direction_key => BACKWARD_DIRECTION })
+ end
+
+ delegate :each, :empty?, :any?, to: :records
+
+ private
+
+ attr_reader :keyset_scope, :order, :per_page, :cursor_converter, :direction_key, :cursor_attributes
+
+ delegate :reversed_order, to: :order
+
+ def at_last_page?
+ @at_last_page
+ end
+
+ def at_first_page?
+ @at_first_page
+ end
+
+ def per_page_plus_one
+ per_page + 1
+ end
+
+ def decode_cursor_attributes(cursor)
+ cursor.blank? ? {} : cursor_converter.parse(cursor)
+ end
+
+ def set_pagination_helper_flags!
+ @direction = cursor_attributes.delete(direction_key.to_s)
+
+ if cursor_attributes.blank? && @direction.blank?
+ @at_first_page = true
+ @direction = FORWARD_DIRECTION
+ elsif cursor_attributes.blank?
+ if paginate_forward?
+ @at_first_page = true
+ else
+ @at_last_page = true
+ end
+ end
+ end
+
+ def paginate_backward?
+ @direction == BACKWARD_DIRECTION
+ end
+
+ def paginate_forward?
+ @direction == FORWARD_DIRECTION
+ end
+
+ def build_scope(scope)
+ keyset_aware_scope, success = Gitlab::Pagination::Keyset::SimpleOrderBuilder.build(scope)
+
+ raise(UnsupportedScopeOrder, 'The order on the scope does not support keyset pagination') unless success
+
+ keyset_aware_scope
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/pagination/keyset/simple_order_builder.rb b/lib/gitlab/pagination/keyset/simple_order_builder.rb
index 5ac5737c3be..76d6bbadaa4 100644
--- a/lib/gitlab/pagination/keyset/simple_order_builder.rb
+++ b/lib/gitlab/pagination/keyset/simple_order_builder.rb
@@ -26,6 +26,8 @@ module Gitlab
def build
order = if order_values.empty?
primary_key_descending_order
+ elsif Gitlab::Pagination::Keyset::Order.keyset_aware?(scope)
+ Gitlab::Pagination::Keyset::Order.extract_keyset_order_object(scope)
elsif ordered_by_primary_key?
primary_key_order
elsif ordered_by_other_column?
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 56965ae57c1..5c9754b74ec 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -23703,6 +23703,9 @@ msgstr ""
msgid "Pages Domain"
msgstr ""
+msgid "Pagination|First"
+msgstr ""
+
msgid "Pagination|Go to first page"
msgstr ""
@@ -23715,6 +23718,9 @@ msgstr ""
msgid "Pagination|Go to previous page"
msgstr ""
+msgid "Pagination|Last"
+msgstr ""
+
msgid "Pagination|Last ยป"
msgstr ""
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index b965feee645..9493215247a 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -444,6 +444,40 @@ RSpec.describe Projects::BlobController do
end
end
+ describe 'POST preview' do
+ subject(:request) { post :preview, params: default_params }
+
+ let(:user) { create(:user) }
+ let(:filename) { 'preview.md' }
+ let(:default_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: "#{project.default_branch}/#{filename}",
+ content: "Bar\n"
+ }
+ end
+
+ before do
+ project.add_developer(user)
+ sign_in(user)
+
+ project.repository.create_file(
+ project.creator,
+ filename,
+ "Foo\n",
+ message: 'Test',
+ branch_name: project.default_branch
+ )
+ end
+
+ it 'is successful' do
+ request
+
+ expect(response).to be_successful
+ end
+ end
+
describe 'POST create' do
let(:user) { create(:user) }
let(:default_params) do
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index 57514a0c499..b3cb3142911 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -1,5 +1,4 @@
import {
- GlFilteredSearchToken,
GlFilteredSearchSuggestion,
GlFilteredSearchTokenSegment,
GlDropdownDivider,
@@ -18,6 +17,7 @@ import {
DEFAULT_LABELS,
DEFAULT_NONE_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import { mockLabelToken } from '../mock_data';
@@ -25,6 +25,7 @@ import { mockLabelToken } from '../mock_data';
jest.mock('~/flash');
const defaultStubs = {
Portal: true,
+ BaseToken,
GlFilteredSearchSuggestionList: {
template: '<div></div>',
methods: {
@@ -68,55 +69,17 @@ describe('LabelToken', () => {
wrapper.destroy();
});
- describe('computed', () => {
- beforeEach(async () => {
- // Label title with spaces is always enclosed in quotations by component.
- wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
-
- wrapper.setData({
- labels: mockLabels,
- });
-
- await wrapper.vm.$nextTick();
- });
-
- describe('currentValue', () => {
- it('returns lowercase string for `value.data`', () => {
- expect(wrapper.vm.currentValue).toBe('"foo label"');
- });
- });
-
- describe('activeLabel', () => {
- it('returns object for currently present `value.data`', () => {
- expect(wrapper.vm.activeLabel).toEqual(mockRegularLabel);
- });
- });
-
- describe('containerStyle', () => {
- it('returns object containing `backgroundColor` and `color` properties based on `activeLabel` value', () => {
- expect(wrapper.vm.containerStyle).toEqual({
- backgroundColor: mockRegularLabel.color,
- color: mockRegularLabel.textColor,
- });
- });
-
- it('returns empty object when `activeLabel` is not set', async () => {
- wrapper.setData({
- labels: [],
- });
-
- await wrapper.vm.$nextTick();
-
- expect(wrapper.vm.containerStyle).toEqual({});
- });
- });
- });
-
describe('methods', () => {
beforeEach(() => {
wrapper = createComponent();
});
+ describe('getActiveLabel', () => {
+ it('returns label object from labels array based on provided `currentValue` param', () => {
+ expect(wrapper.vm.getActiveLabel(mockLabels, 'foo label')).toEqual(mockRegularLabel);
+ });
+ });
+
describe('getLabelName', () => {
it('returns value of `name` or `title` property present in provided label param', () => {
let mockLabel = {
@@ -187,8 +150,14 @@ describe('LabelToken', () => {
await wrapper.vm.$nextTick();
});
- it('renders gl-filtered-search-token component', () => {
- expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
+ it('renders base-token component', () => {
+ const baseTokenEl = wrapper.find(BaseToken);
+
+ expect(baseTokenEl.exists()).toBe(true);
+ expect(baseTokenEl.props()).toMatchObject({
+ tokenValues: mockLabels,
+ fnActiveTokenValue: wrapper.vm.getActiveLabel,
+ });
});
it('renders token item when value is selected', () => {
diff --git a/spec/helpers/keyset_helper_spec.rb b/spec/helpers/keyset_helper_spec.rb
new file mode 100644
index 00000000000..2e4bf537e2f
--- /dev/null
+++ b/spec/helpers/keyset_helper_spec.rb
@@ -0,0 +1,94 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe KeysetHelper, type: :controller do
+ controller(Admin::UsersController) do
+ def index
+ @users = User
+ .where(admin: false)
+ .order(id: :desc)
+ .keyset_paginate(cursor: params[:cursor], per_page: 2)
+
+ render inline: "<%= keyset_paginate @users %>", layout: false # rubocop: disable Rails/RenderInline
+ end
+ end
+
+ render_views
+
+ let(:admin) { create(:admin) }
+
+ before do
+ sign_in(admin)
+ end
+
+ context 'with admin mode', :enable_admin_mode do
+ context 'when no users are present' do
+ it 'does not render pagination links' do
+ get :index
+
+ expect(response.body).not_to include(s_('Pagination|First'))
+ expect(response.body).not_to include(s_('Pagination|Prev'))
+ expect(response.body).not_to include(s_('Pagination|Next'))
+ expect(response.body).not_to include(s_('Pagination|Last'))
+ end
+ end
+
+ context 'when one user is present' do
+ before do
+ create(:user)
+ end
+
+ it 'does not render pagination links' do
+ get :index
+
+ expect(response.body).not_to include(s_('Pagination|First'))
+ expect(response.body).not_to include(s_('Pagination|Prev'))
+ expect(response.body).not_to include(s_('Pagination|Next'))
+ expect(response.body).not_to include(s_('Pagination|Last'))
+ end
+ end
+
+ context 'when more users are present' do
+ let_it_be(:users) { create_list(:user, 5) }
+
+ let(:paginator) { User.where(admin: false).order(id: :desc).keyset_paginate(per_page: 2) }
+
+ context 'when on the first page' do
+ it 'renders the next and last links' do
+ get :index
+
+ expect(response.body).not_to include(s_('Pagination|First'))
+ expect(response.body).not_to include(s_('Pagination|Prev'))
+ expect(response.body).to include(s_('Pagination|Next'))
+ expect(response.body).to include(s_('Pagination|Last'))
+ end
+ end
+
+ context 'when at the last page' do
+ it 'renders the prev and first links' do
+ cursor = paginator.cursor_for_last_page
+
+ get :index, params: { cursor: cursor }
+
+ expect(response.body).to include(s_('Pagination|First'))
+ expect(response.body).to include(s_('Pagination|Prev'))
+ expect(response.body).not_to include(s_('Pagination|Next'))
+ expect(response.body).not_to include(s_('Pagination|Last'))
+ end
+ end
+
+ context 'when at the second page' do
+ it 'renders all links' do
+ cursor = paginator.cursor_for_next_page
+
+ get :index, params: { cursor: cursor }
+
+ expect(response.body).to include(s_('Pagination|First'))
+ expect(response.body).to include(s_('Pagination|Prev'))
+ expect(response.body).to include(s_('Pagination|Next'))
+ expect(response.body).to include(s_('Pagination|Last'))
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
deleted file mode 100644
index 7971a7cabd5..00000000000
--- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
+++ /dev/null
@@ -1,141 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Metrics::Samplers::UnicornSampler do
- subject { described_class.new(1.second) }
-
- it_behaves_like 'metrics sampler', 'UNICORN_SAMPLER'
-
- describe '#sample' do
- let(:unicorn) { Module.new }
- let(:raindrops) { double('raindrops') }
- let(:stats) { double('stats') }
-
- before do
- stub_const('Unicorn', unicorn)
- stub_const('Raindrops::Linux', raindrops)
- allow(raindrops).to receive(:unix_listener_stats).and_return({})
- allow(raindrops).to receive(:tcp_listener_stats).and_return({})
- end
-
- context 'unicorn listens on unix sockets' do
- let(:socket_address) { '/some/sock' }
- let(:sockets) { [socket_address] }
-
- before do
- allow(unicorn).to receive(:listener_names).and_return(sockets)
- end
-
- it 'samples socket data' do
- expect(raindrops).to receive(:unix_listener_stats).with(sockets)
-
- subject.sample
- end
-
- context 'stats collected' do
- before do
- allow(stats).to receive(:active).and_return('active')
- allow(stats).to receive(:queued).and_return('queued')
- allow(raindrops).to receive(:unix_listener_stats).and_return({ socket_address => stats })
- end
-
- it 'updates metrics type unix and with addr' do
- labels = { socket_type: 'unix', socket_address: socket_address }
-
- expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
- expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
-
- subject.sample
- end
- end
- end
-
- context 'unicorn listens on tcp sockets' do
- let(:tcp_socket_address) { '0.0.0.0:8080' }
- let(:tcp_sockets) { [tcp_socket_address] }
-
- before do
- allow(unicorn).to receive(:listener_names).and_return(tcp_sockets)
- end
-
- it 'samples socket data' do
- expect(raindrops).to receive(:tcp_listener_stats).with(tcp_sockets)
-
- subject.sample
- end
-
- context 'stats collected' do
- before do
- allow(stats).to receive(:active).and_return('active')
- allow(stats).to receive(:queued).and_return('queued')
- allow(raindrops).to receive(:tcp_listener_stats).and_return({ tcp_socket_address => stats })
- end
-
- it 'updates metrics type unix and with addr' do
- labels = { socket_type: 'tcp', socket_address: tcp_socket_address }
-
- expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
- expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
-
- subject.sample
- end
- end
- end
-
- context 'unicorn workers' do
- before do
- allow(unicorn).to receive(:listener_names).and_return([])
- end
-
- context 'without http server' do
- it "does set unicorn_workers to 0" do
- expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, 0)
-
- subject.sample
- end
- end
-
- context 'with http server' do
- let(:http_server_class) { Struct.new(:worker_processes) }
- let!(:http_server) { http_server_class.new(5) }
-
- before do
- stub_const('Unicorn::HttpServer', http_server_class)
- end
-
- it "sets additional metrics" do
- expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, 5)
-
- subject.sample
- end
- end
- end
- end
-
- describe '#start' do
- context 'when enabled' do
- before do
- allow(subject).to receive(:enabled?).and_return(true)
- end
-
- it 'creates new thread' do
- expect(Thread).to receive(:new)
-
- subject.start
- end
- end
-
- context 'when disabled' do
- before do
- allow(subject).to receive(:enabled?).and_return(false)
- end
-
- it "doesn't create new thread" do
- expect(Thread).not_to receive(:new)
-
- subject.start
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/pagination/keyset/paginator_spec.rb b/spec/lib/gitlab/pagination/keyset/paginator_spec.rb
new file mode 100644
index 00000000000..3c9a8913876
--- /dev/null
+++ b/spec/lib/gitlab/pagination/keyset/paginator_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Pagination::Keyset::Paginator do
+ let_it_be(:project_1) { create(:project, created_at: 10.weeks.ago) }
+ let_it_be(:project_2) { create(:project, created_at: 2.weeks.ago) }
+ let_it_be(:project_3) { create(:project, created_at: 3.weeks.ago) }
+ let_it_be(:project_4) { create(:project, created_at: 5.weeks.ago) }
+ let_it_be(:project_5) { create(:project, created_at: 2.weeks.ago) }
+
+ describe 'pagination' do
+ let(:per_page) { 10 }
+ let(:cursor) { nil }
+ let(:scope) { Project.order(created_at: :asc, id: :asc) }
+ let(:expected_order) { [project_1, project_4, project_3, project_2, project_5] }
+
+ subject(:paginator) { scope.keyset_paginate(cursor: cursor, per_page: per_page) }
+
+ context 'when per_page is greater than the record count' do
+ it { expect(paginator.records).to eq(expected_order) }
+ it { is_expected.not_to have_next_page }
+ it { is_expected.not_to have_previous_page }
+
+ it 'has no next and previous cursor values' do
+ expect(paginator.cursor_for_next_page).to be_nil
+ expect(paginator.cursor_for_previous_page).to be_nil
+ end
+ end
+
+ context 'when 0 records are returned' do
+ let(:scope) { Project.where(id: non_existing_record_id).order(created_at: :asc, id: :asc) }
+
+ it { expect(paginator.records).to be_empty }
+ it { is_expected.not_to have_next_page }
+ it { is_expected.not_to have_previous_page }
+ end
+
+ context 'when page size is smaller than the record count' do
+ let(:per_page) { 2 }
+
+ it { expect(paginator.records).to eq(expected_order.first(2)) }
+ it { is_expected.to have_next_page }
+ it { is_expected.not_to have_previous_page }
+
+ it 'has next page cursor' do
+ expect(paginator.cursor_for_next_page).not_to be_nil
+ end
+
+ it 'does not have previous page cursor' do
+ expect(paginator.cursor_for_previous_page).to be_nil
+ end
+
+ context 'when on the second page' do
+ let(:cursor) { scope.keyset_paginate(per_page: per_page).cursor_for_next_page }
+
+ it { expect(paginator.records).to eq(expected_order[2...4]) }
+ it { is_expected.to have_next_page }
+ it { is_expected.to have_previous_page }
+
+ context 'and then going back to the first page' do
+ let(:previous_page_cursor) { scope.keyset_paginate(cursor: cursor, per_page: per_page).cursor_for_previous_page }
+
+ subject(:paginator) { scope.keyset_paginate(cursor: previous_page_cursor, per_page: per_page) }
+
+ it { expect(paginator.records).to eq(expected_order.first(2)) }
+ it { is_expected.to have_next_page }
+ it { is_expected.not_to have_previous_page }
+ end
+ end
+
+ context 'when jumping to the last page' do
+ let(:cursor) { scope.keyset_paginate(per_page: per_page).cursor_for_last_page }
+
+ it { expect(paginator.records).to eq(expected_order.last(2)) }
+ it { is_expected.not_to have_next_page }
+ it { is_expected.to have_previous_page }
+
+ context 'when paginating backwards' do
+ let(:previous_page_cursor) { scope.keyset_paginate(cursor: cursor, per_page: per_page).cursor_for_previous_page }
+
+ subject(:paginator) { scope.keyset_paginate(cursor: previous_page_cursor, per_page: per_page) }
+
+ it { expect(paginator.records).to eq(expected_order[-4...-2]) }
+ it { is_expected.to have_next_page }
+ it { is_expected.to have_previous_page }
+ end
+
+ context 'when jumping to the first page' do
+ let(:first_page_cursor) { scope.keyset_paginate(cursor: cursor, per_page: per_page).cursor_for_first_page }
+
+ subject(:paginator) { scope.keyset_paginate(cursor: first_page_cursor, per_page: per_page) }
+
+ it { expect(paginator.records).to eq(expected_order.first(2)) }
+ it { is_expected.to have_next_page }
+ it { is_expected.not_to have_previous_page }
+ end
+ end
+ end
+
+ describe 'default keyset direction parameter' do
+ let(:cursor_converter_class) { Gitlab::Pagination::Keyset::Paginator::Base64CursorConverter }
+ let(:per_page) { 2 }
+
+ it 'exposes the direction parameter in the cursor' do
+ cursor = paginator.cursor_for_next_page
+
+ expect(cursor_converter_class.parse(cursor)[:_kd]).to eq(described_class::FORWARD_DIRECTION)
+ end
+ end
+ end
+
+ context 'when unsupported order is given' do
+ it 'raises error' do
+ scope = Project.order(path: :asc, name: :asc, id: :desc) # Cannot build 3 column order automatically
+
+ expect { scope.keyset_paginate }.to raise_error(/does not support keyset pagination/)
+ end
+ end
+end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 88149465232..d811f67d16b 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -39,4 +39,15 @@ RSpec.describe ProjectHook do
expect(hook.rate_limit).to be(100)
end
end
+
+ describe '#application_context' do
+ let_it_be(:hook) { build(:project_hook) }
+
+ it 'includes the type and project' do
+ expect(hook.application_context).to include(
+ related_class: 'ProjectHook',
+ project: hook.project
+ )
+ end
+ end
end
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 651716c3280..4ce2e729d89 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -30,4 +30,14 @@ RSpec.describe ServiceHook do
expect(hook.rate_limit).to be_nil
end
end
+
+ describe '#application_context' do
+ let(:hook) { build(:service_hook) }
+
+ it 'includes the type' do
+ expect(hook.application_context).to eq(
+ related_class: 'ServiceHook'
+ )
+ end
+ end
end
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index a72034f1ac5..a99263078b3 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -177,4 +177,14 @@ RSpec.describe SystemHook do
expect(hook.rate_limit).to be_nil
end
end
+
+ describe '#application_context' do
+ let(:hook) { build(:system_hook) }
+
+ it 'includes the type' do
+ expect(hook.application_context).to eq(
+ related_class: 'SystemHook'
+ )
+ end
+ end
end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index a38ba782c44..009c8bd795e 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -8,8 +8,8 @@ RSpec.describe API::Branches do
let(:guest) { create(:user).tap { |u| project.add_guest(u) } }
let(:branch_name) { 'feature' }
let(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
- let(:branch_with_dot) { project.repository.find_branch('ends-with.json') }
- let(:branch_with_slash) { project.repository.find_branch('improve/awesome') }
+ let(:branch_with_dot) { 'ends-with.json' }
+ let(:branch_with_slash) { 'improve/awesome' }
let(:project_id) { project.id }
let(:current_user) { nil }
@@ -285,6 +285,13 @@ RSpec.describe API::Branches do
let(:request) { get api(route, current_user) }
end
end
+
+ context 'when repository does not exist' do
+ it_behaves_like '404 response' do
+ let(:project) { create(:project, creator: user) }
+ let(:request) { get api(route, current_user) }
+ end
+ end
end
context 'when unauthenticated', 'and project is public' do
@@ -320,19 +327,19 @@ RSpec.describe API::Branches do
end
context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot.name }
+ let(:branch_name) { branch_with_dot }
it_behaves_like 'repository branch'
end
context 'when branch contains dot txt' do
- let(:branch_name) { project.repository.find_branch('ends-with.txt').name }
+ let(:branch_name) { 'ends-with.txt' }
it_behaves_like 'repository branch'
end
context 'when branch contains a slash' do
- let(:branch_name) { branch_with_slash.name }
+ let(:branch_name) { branch_with_slash }
it_behaves_like '404 response' do
let(:request) { get api(route, current_user) }
@@ -340,7 +347,7 @@ RSpec.describe API::Branches do
end
context 'when branch contains an escaped slash' do
- let(:branch_name) { CGI.escape(branch_with_slash.name) }
+ let(:branch_name) { CGI.escape(branch_with_slash) }
it_behaves_like 'repository branch'
end
@@ -351,7 +358,7 @@ RSpec.describe API::Branches do
it_behaves_like 'repository branch'
context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot.name }
+ let(:branch_name) { branch_with_dot }
it_behaves_like 'repository branch'
end
@@ -475,13 +482,13 @@ RSpec.describe API::Branches do
it_behaves_like 'repository new protected branch'
context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot.name }
+ let(:branch_name) { branch_with_dot }
it_behaves_like 'repository new protected branch'
end
context 'when branch contains a slash' do
- let(:branch_name) { branch_with_slash.name }
+ let(:branch_name) { branch_with_slash }
it_behaves_like '404 response' do
let(:request) { put api(route, current_user) }
@@ -489,7 +496,7 @@ RSpec.describe API::Branches do
end
context 'when branch contains an escaped slash' do
- let(:branch_name) { CGI.escape(branch_with_slash.name) }
+ let(:branch_name) { CGI.escape(branch_with_slash) }
it_behaves_like 'repository new protected branch'
end
@@ -500,7 +507,7 @@ RSpec.describe API::Branches do
it_behaves_like 'repository new protected branch'
context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot.name }
+ let(:branch_name) { branch_with_dot }
it_behaves_like 'repository new protected branch'
end
@@ -609,13 +616,13 @@ RSpec.describe API::Branches do
it_behaves_like 'repository unprotected branch'
context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot.name }
+ let(:branch_name) { branch_with_dot }
it_behaves_like 'repository unprotected branch'
end
context 'when branch contains a slash' do
- let(:branch_name) { branch_with_slash.name }
+ let(:branch_name) { branch_with_slash }
it_behaves_like '404 response' do
let(:request) { put api(route, current_user) }
@@ -623,7 +630,7 @@ RSpec.describe API::Branches do
end
context 'when branch contains an escaped slash' do
- let(:branch_name) { CGI.escape(branch_with_slash.name) }
+ let(:branch_name) { CGI.escape(branch_with_slash) }
it_behaves_like 'repository unprotected branch'
end
@@ -634,7 +641,7 @@ RSpec.describe API::Branches do
it_behaves_like 'repository unprotected branch'
context 'when branch contains a dot' do
- let(:branch_name) { branch_with_dot.name }
+ let(:branch_name) { branch_with_dot }
it_behaves_like 'repository unprotected branch'
end
@@ -732,7 +739,7 @@ RSpec.describe API::Branches do
end
it 'removes a branch with dots in the branch name' do
- delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot.name}", user)
+ delete api("/projects/#{project.id}/repository/branches/#{branch_with_dot}", user)
expect(response).to have_gitlab_http_status(:no_content)
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 056f4d30ea5..43d7c31d87f 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -202,6 +202,16 @@ RSpec.describe 'project routing' do
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "stable", path: "new\n\nline.txt" })
end
+
+ it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/refs/switch', '/gitlab/gitlabhq/-/refs/switch'
+
+ it_behaves_like 'redirecting a legacy path',
+ '/gitlab/gitlabhq/refs/feature%2345/logs_tree',
+ '/gitlab/gitlabhq/-/refs/feature%2345/logs_tree'
+
+ it_behaves_like 'redirecting a legacy path',
+ '/gitlab/gitlabhq/refs/stable/logs_tree/new%0A%0Aline.txt',
+ '/gitlab/gitlabhq/-/refs/stable/logs_tree/new%0A%0Aline.txt'
end
describe Projects::MergeRequestsController, 'routing' do
@@ -357,9 +367,7 @@ RSpec.describe 'project routing' do
expect(get('/gitlab/gitlabhq/-/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5')
end
- it 'to #show unscoped routing' do
- expect(get('/gitlab/gitlabhq/commit/4246fbd')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd')
- end
+ it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/commit/4246fbd", "/gitlab/gitlabhq/-/commit/4246fbd"
end
# patch_project_commit GET /:project_id/commits/:id/patch(.:format) commits#patch
@@ -376,9 +384,7 @@ RSpec.describe 'project routing' do
expect(get('/gitlab/gitlabhq/-/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.atom')
end
- it 'to #show unscoped routing' do
- expect(get('/gitlab/gitlabhq/commits/master.atom')).to route_to('projects/commits#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master.atom')
- end
+ it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/commits/master", "/gitlab/gitlabhq/-/commits/master"
end
# project_project_members GET /:project_id/project_members(.:format) project_members#index
@@ -517,6 +523,7 @@ RSpec.describe 'project routing' do
end
it 'to #show from unscoped routing' do
+ expect(get('/gitlab/gitlabhq/tree/master')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master')
expect(get('/gitlab/gitlabhq/tree/master/app/models/project.rb')).to route_to('projects/tree#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master/app/models/project.rb')
end
end
@@ -545,6 +552,9 @@ RSpec.describe 'project routing' do
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "#{newline_file}" })
end
+
+ it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/find_file", "/gitlab/gitlabhq/-/find_file"
+ it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/files/master", "/gitlab/gitlabhq/-/files/master"
end
describe Projects::BlobController, 'routing' do
@@ -575,6 +585,9 @@ RSpec.describe 'project routing' do
namespace_id: 'gitlab', project_id: 'gitlabhq',
id: "master/docs/#{newline_file}" })
end
+
+ it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/new/master", "/gitlab/gitlabhq/-/new/master"
+ it_behaves_like 'redirecting a legacy path', "/gitlab/gitlabhq/edit/master/README", "/gitlab/gitlabhq/-/edit/master/README"
end
# project_raw GET /:project_id/-/raw/:id(.:format) raw#show {id: /[^\0]+/, project_id: /[^\/]+/}
@@ -610,6 +623,9 @@ RSpec.describe 'project routing' do
expect(get('/gitlab/gitlabhq/-/compare/master...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'master', to: 'stable')
expect(get('/gitlab/gitlabhq/-/compare/issue/1234...stable')).to route_to('projects/compare#show', namespace_id: 'gitlab', project_id: 'gitlabhq', from: 'issue/1234', to: 'stable')
end
+
+ it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/compare', '/gitlab/gitlabhq/-/compare'
+ it_behaves_like 'redirecting a legacy path', '/gitlab/gitlabhq/compare/master...stable', '/gitlab/gitlabhq/-/compare/master...stable'
end
describe Projects::NetworkController, 'routing' do
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index e1954c32227..4ec8922e187 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -429,5 +429,19 @@ RSpec.describe WebHookService do
end
end
end
+
+ context 'when hook has custom context attributes' do
+ it 'includes the attributes in the worker context' do
+ expect(WebHookWorker).to receive(:perform_async) do
+ expect(Gitlab::ApplicationContext.current).to include(
+ 'meta.project' => project_hook.project.full_path,
+ 'meta.root_namespace' => project.root_ancestor.path,
+ 'meta.related_class' => 'ProjectHook'
+ )
+ end
+
+ service_instance.async_execute
+ end
+ end
end
end