summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js37
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/actions.js27
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/index.js13
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js2
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/mutations.js8
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/state.js6
-rw-r--r--app/controllers/application_controller.rb1
-rw-r--r--app/controllers/concerns/controller_with_feature_category.rb45
-rw-r--r--app/controllers/concerns/controller_with_feature_category/config.rb38
-rw-r--r--app/controllers/import/bitbucket_server_controller.rb22
-rw-r--r--app/controllers/projects/merge_requests_controller.rb7
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/models/namespace/root_storage_statistics.rb25
-rw-r--r--app/presenters/snippet_blob_presenter.rb14
-rw-r--r--app/services/import/bitbucket_server_service.rb104
-rw-r--r--app/views/import/bitbucket_server/new.html.haml2
-rw-r--r--app/views/import/bitbucket_server/status.html.haml2
-rw-r--r--app/views/import/gitlab_projects/new.html.haml5
-rw-r--r--app/views/projects/_import_project_pane.html.haml6
-rw-r--r--app/workers/concerns/worker_attributes.rb64
-rw-r--r--changelogs/unreleased/36788-feature-proposal-api-for-import-from-bitbucket-server.yml5
-rw-r--r--changelogs/unreleased/fj-225964-include-personal-snippets-in-snippets-size.yml5
-rw-r--r--danger/roulette/Dangerfile15
-rw-r--r--doc/api/import.md38
-rw-r--r--doc/development/documentation/styleguide.md35
-rw-r--r--doc/development/multi_version_compatibility.md2
-rw-r--r--doc/install/installation.md17
-rw-r--r--doc/user/application_security/container_scanning/index.md14
-rw-r--r--doc/user/group/saml_sso/index.md28
-rw-r--r--doc/user/group/saml_sso/scim_setup.md2
-rw-r--r--doc/user/permissions.md1
-rw-r--r--doc/user/project/quick_actions.md2
-rw-r--r--doc/user/project/service_desk.md30
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/import_bitbucket_server.rb44
-rw-r--r--lib/gitlab/class_attributes.rb30
-rw-r--r--lib/gitlab/danger/roulette.rb7
-rw-r--r--lib/gitlab/metrics/web_transaction.rb11
-rw-r--r--locale/gitlab.pot28
-rw-r--r--spec/controllers/concerns/controller_with_feature_category/config_spec.rb53
-rw-r--r--spec/controllers/concerns/controller_with_feature_category_spec.rb66
-rw-r--r--spec/controllers/every_controller_spec.rb82
-rw-r--r--spec/controllers/import/bitbucket_server_controller_spec.rb20
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb4
-rw-r--r--spec/frontend/fixtures/branches.rb50
-rw-r--r--spec/frontend/fixtures/commit.rb49
-rw-r--r--spec/frontend/fixtures/tags.rb28
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js51
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js18
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js11
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js6
-rw-r--r--spec/helpers/gitlab_routing_helper_spec.rb17
-rw-r--r--spec/lib/gitlab/class_attributes_spec.rb41
-rw-r--r--spec/lib/gitlab/danger/roulette_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/web_transaction_spec.rb37
-rw-r--r--spec/models/namespace/root_storage_statistics_spec.rb44
-rw-r--r--spec/presenters/snippet_blob_presenter_spec.rb38
-rw-r--r--spec/requests/api/import_bitbucket_server_spec.rb214
-rw-r--r--spec/services/import/bitbucket_server_service_spec.rb113
-rw-r--r--spec/support/shared_examples/snippet_blob_shared_examples.rb24
63 files changed, 1118 insertions, 626 deletions
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
index 33c7810983a..6f2d3ad42ea 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
@@ -4,7 +4,6 @@ import { GlLoadingIcon } from '@gitlab/ui';
import TestSuiteTable from './test_suite_table.vue';
import TestSummary from './test_summary.vue';
import TestSummaryTable from './test_summary_table.vue';
-import store from '~/pipelines/stores/test_reports';
export default {
name: 'TestReports',
@@ -14,7 +13,6 @@ export default {
TestSummary,
TestSummaryTable,
},
- store,
computed: {
...mapState(['isLoading', 'selectedSuite', 'testReports']),
showSuite() {
@@ -25,8 +23,11 @@ export default {
return testSuites.length > 0;
},
},
+ created() {
+ this.fetchSummary();
+ },
methods: {
- ...mapActions(['setSelectedSuite', 'removeSelectedSuite']),
+ ...mapActions(['fetchSummary', 'setSelectedSuite', 'removeSelectedSuite']),
summaryBackClick() {
this.removeSelectedSuite();
},
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
index f014660e14d..dee82eb5c42 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
@@ -1,7 +1,6 @@
<script>
import { mapGetters } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
-import store from '~/pipelines/stores/test_reports';
import { __ } from '~/locale';
import { GlTooltipDirective } from '@gitlab/ui';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
@@ -15,7 +14,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- store,
props: {
heading: {
type: String,
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
index a8343680763..aa53bbb0e97 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_summary_table.vue
@@ -2,7 +2,6 @@
import { mapGetters } from 'vuex';
import { s__ } from '~/locale';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import store from '~/pipelines/stores/test_reports';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
export default {
@@ -14,7 +13,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- store,
props: {
heading: {
type: String,
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 7ac04b18932..f9370e8ca1c 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -10,8 +10,7 @@ import PipelinesMediator from './pipeline_details_mediator';
import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub';
import TestReports from './components/test_reports/test_reports.vue';
-import testReportsStore from './stores/test_reports';
-import axios from '~/lib/utils/axios_utils';
+import createTestReportsStore from './stores/test_reports';
Vue.use(Translate);
@@ -93,15 +92,11 @@ const createPipelineHeaderApp = mediator => {
});
};
-const createPipelinesTabs = dataset => {
+const createPipelinesTabs = testReportsStore => {
const tabsElement = document.querySelector('.pipelines-tabs');
- const testReportsEnabled =
- window.gon && window.gon.features && window.gon.features.junitPipelineView;
-
- if (tabsElement && testReportsEnabled) {
- const fetchReportsAction = 'fetchReports';
- testReportsStore.dispatch('setEndpoint', dataset.testReportEndpoint);
+ if (tabsElement) {
+ const fetchReportsAction = 'fetchFullReport';
const isTestTabActive = Boolean(
document.querySelector('.pipelines-tabs > li > a.test-tab.active'),
);
@@ -121,28 +116,25 @@ const createPipelinesTabs = dataset => {
}
};
-const createTestDetails = detailsEndpoint => {
+const createTestDetails = (fullReportEndpoint, summaryEndpoint) => {
+ if (!window.gon?.features?.junitPipelineView) {
+ return;
+ }
+
+ const testReportsStore = createTestReportsStore({ fullReportEndpoint, summaryEndpoint });
+ createPipelinesTabs(testReportsStore);
+
// eslint-disable-next-line no-new
new Vue({
el: '#js-pipeline-tests-detail',
components: {
TestReports,
},
+ store: testReportsStore,
render(createElement) {
return createElement('test-reports');
},
});
-
- axios
- .get(detailsEndpoint)
- .then(({ data }) => {
- if (!data.total_count) {
- return;
- }
-
- document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count;
- })
- .catch(() => {});
};
const createDagApp = () => {
@@ -178,7 +170,6 @@ export default () => {
createPipelinesDetailApp(mediator);
createPipelineHeaderApp(mediator);
- createPipelinesTabs(dataset);
- createTestDetails(dataset.testReportsCountEndpoint);
+ createTestDetails(dataset.testReportEndpoint, dataset.testReportsCountEndpoint);
createDagApp();
};
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/actions.js b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
index 71d875c1a83..e7f7995ef96 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/actions.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/actions.js
@@ -3,17 +3,30 @@ import * as types from './mutation_types';
import createFlash from '~/flash';
import { s__ } from '~/locale';
-export const setEndpoint = ({ commit }, data) => commit(types.SET_ENDPOINT, data);
+export const fetchSummary = ({ state, commit }) => {
+ return axios
+ .get(state.summaryEndpoint)
+ .then(({ data }) => {
+ commit(types.SET_SUMMARY, data);
-export const fetchReports = ({ state, commit, dispatch }) => {
+ // Set the tab counter badge to total_count
+ // This is temporary until we can server-side render that count number
+ // (see https://gitlab.com/gitlab-org/gitlab/-/issues/223134)
+ if (data.total_count !== undefined) {
+ document.querySelector('.js-test-report-badge-counter').innerHTML = data.total_count;
+ }
+ })
+ .catch(() => {
+ createFlash(s__('TestReports|There was an error fetching the summary.'));
+ });
+};
+
+export const fetchFullReport = ({ state, commit, dispatch }) => {
dispatch('toggleLoading');
return axios
- .get(state.endpoint)
- .then(response => {
- const { data } = response;
- commit(types.SET_REPORTS, data);
- })
+ .get(state.fullReportEndpoint)
+ .then(({ data }) => commit(types.SET_REPORTS, data))
.catch(() => {
createFlash(s__('TestReports|There was an error fetching the test reports.'));
})
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/index.js b/app/assets/javascripts/pipelines/stores/test_reports/index.js
index 318dff5bcb2..88f61b09025 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/index.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/index.js
@@ -7,9 +7,10 @@ import mutations from './mutations';
Vue.use(Vuex);
-export default new Vuex.Store({
- actions,
- getters,
- mutations,
- state,
-});
+export default initialState =>
+ new Vuex.Store({
+ actions,
+ getters,
+ mutations,
+ state: state(initialState),
+ });
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
index 832e45cf7a1..b9c23bf6059 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutation_types.js
@@ -1,4 +1,4 @@
-export const SET_ENDPOINT = 'SET_ENDPOINT';
export const SET_REPORTS = 'SET_REPORTS';
export const SET_SELECTED_SUITE = 'SET_SELECTED_SUITE';
+export const SET_SUMMARY = 'SET_SUMMARY';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
index 349e6ec0469..62770246c12 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/mutations.js
@@ -1,10 +1,6 @@
import * as types from './mutation_types';
export default {
- [types.SET_ENDPOINT](state, endpoint) {
- Object.assign(state, { endpoint });
- },
-
[types.SET_REPORTS](state, testReports) {
Object.assign(state, { testReports });
},
@@ -13,6 +9,10 @@ export default {
Object.assign(state, { selectedSuite });
},
+ [types.SET_SUMMARY](state, summary) {
+ Object.assign(state, { summary });
+ },
+
[types.TOGGLE_LOADING](state) {
Object.assign(state, { isLoading: !state.isLoading });
},
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/state.js b/app/assets/javascripts/pipelines/stores/test_reports/state.js
index 80a0c2a46a0..0fc49c63ab1 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/state.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/state.js
@@ -1,6 +1,8 @@
-export default () => ({
- endpoint: '',
+export default ({ fullReportEndpoint = '', summaryEndpoint = '' }) => ({
+ summaryEndpoint,
+ fullReportEndpoint,
testReports: {},
selectedSuite: {},
+ summary: {},
isLoading: false,
});
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 789213a5fc6..1baaf0b9ee8 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -22,7 +22,6 @@ class ApplicationController < ActionController::Base
include Impersonation
include Gitlab::Logging::CloudflareHelper
include Gitlab::Utils::StrongMemoize
- include ControllerWithFeatureCategory
before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms?
diff --git a/app/controllers/concerns/controller_with_feature_category.rb b/app/controllers/concerns/controller_with_feature_category.rb
deleted file mode 100644
index f8985cf0950..00000000000
--- a/app/controllers/concerns/controller_with_feature_category.rb
+++ /dev/null
@@ -1,45 +0,0 @@
-# frozen_string_literal: true
-
-module ControllerWithFeatureCategory
- extend ActiveSupport::Concern
- include Gitlab::ClassAttributes
-
- class_methods do
- def feature_category(category, config = {})
- validate_config!(config)
-
- category_config = Config.new(category, config[:only], config[:except], config[:if], config[:unless])
- # Add the config to the beginning. That way, the last defined one takes precedence.
- feature_category_configuration.unshift(category_config)
- end
-
- def feature_category_for_action(action)
- category_config = feature_category_configuration.find { |config| config.matches?(action) }
-
- category_config&.category || superclass_feature_category_for_action(action)
- end
-
- private
-
- def validate_config!(config)
- invalid_keys = config.keys - [:only, :except, :if, :unless]
- if invalid_keys.any?
- raise ArgumentError, "unknown arguments: #{invalid_keys} "
- end
-
- if config.key?(:only) && config.key?(:except)
- raise ArgumentError, "cannot configure both `only` and `except`"
- end
- end
-
- def feature_category_configuration
- class_attributes[:feature_category_config] ||= []
- end
-
- def superclass_feature_category_for_action(action)
- return unless superclass.respond_to?(:feature_category_for_action)
-
- superclass.feature_category_for_action(action)
- end
- end
-end
diff --git a/app/controllers/concerns/controller_with_feature_category/config.rb b/app/controllers/concerns/controller_with_feature_category/config.rb
deleted file mode 100644
index 624691ee4f6..00000000000
--- a/app/controllers/concerns/controller_with_feature_category/config.rb
+++ /dev/null
@@ -1,38 +0,0 @@
-# frozen_string_literal: true
-
-module ControllerWithFeatureCategory
- class Config
- attr_reader :category
-
- def initialize(category, only, except, if_proc, unless_proc)
- @category = category.to_sym
- @only, @except = only&.map(&:to_s), except&.map(&:to_s)
- @if_proc, @unless_proc = if_proc, unless_proc
- end
-
- def matches?(action)
- included?(action) && !excluded?(action) &&
- if_proc?(action) && !unless_proc?(action)
- end
-
- private
-
- attr_reader :only, :except, :if_proc, :unless_proc
-
- def if_proc?(action)
- if_proc.nil? || if_proc.call(action)
- end
-
- def unless_proc?(action)
- unless_proc.present? && unless_proc.call(action)
- end
-
- def included?(action)
- only.nil? || only.include?(action)
- end
-
- def excluded?(action)
- except.present? && except.include?(action)
- end
- end
-end
diff --git a/app/controllers/import/bitbucket_server_controller.rb b/app/controllers/import/bitbucket_server_controller.rb
index 9aa8110257d..b595d69f101 100644
--- a/app/controllers/import/bitbucket_server_controller.rb
+++ b/app/controllers/import/bitbucket_server_controller.rb
@@ -34,26 +34,18 @@ class Import::BitbucketServerController < Import::BaseController
return render json: { errors: _("Project %{project_repo} could not be found") % { project_repo: "#{@project_key}/#{@repo_slug}" } }, status: :unprocessable_entity
end
- project_name = params[:new_name].presence || repo.name
- namespace_path = params[:new_namespace].presence || current_user.username
- target_namespace = find_or_create_namespace(namespace_path, current_user)
+ result = Import::BitbucketServerService.new(client, current_user, params).execute(credentials)
- if current_user.can?(:create_projects, target_namespace)
- project = Gitlab::BitbucketServerImport::ProjectCreator.new(@project_key, @repo_slug, repo, project_name, target_namespace, current_user, credentials).execute
-
- if project.persisted?
- render json: ProjectSerializer.new.represent(project, serializer: :import)
- else
- render json: { errors: project_save_error(project) }, status: :unprocessable_entity
- end
+ if result[:status] == :success
+ render json: ProjectSerializer.new.represent(result[:project], serializer: :import)
else
- render json: { errors: _('This namespace has already been taken! Please choose another one.') }, status: :unprocessable_entity
+ render json: { errors: result[:message] }, status: result[:http_status]
end
end
def configure
session[personal_access_token_key] = params[:personal_access_token]
- session[bitbucket_server_username_key] = params[:bitbucket_username]
+ session[bitbucket_server_username_key] = params[:bitbucket_server_username]
session[bitbucket_server_url_key] = params[:bitbucket_server_url]
redirect_to status_import_bitbucket_server_path
@@ -127,8 +119,8 @@ class Import::BitbucketServerController < Import::BaseController
end
def validate_import_params
- @project_key = params[:project]
- @repo_slug = params[:repository]
+ @project_key = params[:bitbucketServerProject]
+ @repo_slug = params[:bitbucketServerRepo]
return render_validation_error('Missing project key') unless @project_key.present? && @repo_slug.present?
return render_validation_error('Missing repository slug') unless @repo_slug.present?
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 1aa6002e9c5..20ef14e8546 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -45,13 +45,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
- feature_category :source_code_management,
- unless: -> (action) { action.ends_with?("_reports") }
- feature_category :code_testing,
- only: [:test_reports, :coverage_reports, :terraform_reports]
- feature_category :accessibility_testing,
- only: [:accessibility_reports]
-
def index
@merge_requests = @issuables
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index e9ef7278d44..04f34f5a3ae 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -285,6 +285,22 @@ module GitlabRoutingHelper
end
end
+ def gitlab_raw_snippet_blob_path(blob, ref = nil)
+ snippet = blob.container
+
+ params = {
+ snippet_id: snippet,
+ ref: ref || blob.repository.root_ref,
+ path: blob.path
+ }
+
+ if snippet.is_a?(ProjectSnippet)
+ project_snippet_blob_raw_path(snippet.project, params)
+ else
+ snippet_blob_raw_path(params)
+ end
+ end
+
def gitlab_snippet_notes_path(snippet, *args)
new_args = snippet_query_params(snippet, *args)
snippet_notes_path(snippet, *new_args)
diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb
index 301018220b8..2ad6ea59588 100644
--- a/app/models/namespace/root_storage_statistics.rb
+++ b/app/models/namespace/root_storage_statistics.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
class Namespace::RootStorageStatistics < ApplicationRecord
- STATISTICS_ATTRIBUTES = %w(storage_size repository_size wiki_size lfs_objects_size build_artifacts_size packages_size snippets_size).freeze
+ SNIPPETS_SIZE_STAT_NAME = 'snippets_size'.freeze
+ STATISTICS_ATTRIBUTES = %W(storage_size repository_size wiki_size lfs_objects_size build_artifacts_size packages_size #{SNIPPETS_SIZE_STAT_NAME}).freeze
self.primary_key = :namespace_id
@@ -13,11 +14,15 @@ class Namespace::RootStorageStatistics < ApplicationRecord
delegate :all_projects, to: :namespace
def recalculate!
- update!(attributes_from_project_statistics)
+ update!(merged_attributes)
end
private
+ def merged_attributes
+ attributes_from_project_statistics.merge!(attributes_from_personal_snippets) { |key, v1, v2| v1 + v2 }
+ end
+
def attributes_from_project_statistics
from_project_statistics
.take
@@ -35,7 +40,21 @@ class Namespace::RootStorageStatistics < ApplicationRecord
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
'COALESCE(SUM(ps.build_artifacts_size), 0) AS build_artifacts_size',
'COALESCE(SUM(ps.packages_size), 0) AS packages_size',
- 'COALESCE(SUM(ps.snippets_size), 0) AS snippets_size'
+ "COALESCE(SUM(ps.snippets_size), 0) AS #{SNIPPETS_SIZE_STAT_NAME}"
)
end
+
+ def attributes_from_personal_snippets
+ # Return if the type of namespace does not belong to a user
+ return {} unless namespace.type.nil?
+
+ from_personal_snippets.take.slice(SNIPPETS_SIZE_STAT_NAME)
+ end
+
+ def from_personal_snippets
+ PersonalSnippet
+ .joins('INNER JOIN snippet_statistics s ON s.snippet_id = snippets.id')
+ .where(author: namespace.owner_id)
+ .select("COALESCE(SUM(s.repository_size), 0) AS #{SNIPPETS_SIZE_STAT_NAME}")
+ end
end
diff --git a/app/presenters/snippet_blob_presenter.rb b/app/presenters/snippet_blob_presenter.rb
index ed9c28bbc2c..d27fe751ab7 100644
--- a/app/presenters/snippet_blob_presenter.rb
+++ b/app/presenters/snippet_blob_presenter.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class SnippetBlobPresenter < BlobPresenter
+ include GitlabRoutingHelper
+
def rich_data
return if blob.binary?
return unless blob.rich_viewer
@@ -15,15 +17,17 @@ class SnippetBlobPresenter < BlobPresenter
end
def raw_path
- if snippet.is_a?(ProjectSnippet)
- raw_project_snippet_path(snippet.project, snippet)
- else
- raw_snippet_path(snippet)
- end
+ return gitlab_raw_snippet_blob_path(blob) if snippet_multiple_files?
+
+ gitlab_raw_snippet_path(snippet)
end
private
+ def snippet_multiple_files?
+ blob.container.repository_exists? && Feature.enabled?(:snippet_multiple_files, current_user)
+ end
+
def snippet
blob.container
end
diff --git a/app/services/import/bitbucket_server_service.rb b/app/services/import/bitbucket_server_service.rb
new file mode 100644
index 00000000000..5b39c5bbc32
--- /dev/null
+++ b/app/services/import/bitbucket_server_service.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+module Import
+ class BitbucketServerService < Import::BaseService
+ attr_reader :client, :params, :current_user
+
+ def execute(credentials)
+ if blocked_url?
+ return log_and_return_error("Invalid URL: #{url}", :bad_request)
+ end
+
+ unless authorized?
+ return log_and_return_error("You don't have permissions to create this project", :unauthorized)
+ end
+
+ unless repo
+ return log_and_return_error("Project %{project_repo} could not be found" % { project_repo: "#{project_key}/#{repo_slug}" }, :unprocessable_entity)
+ end
+
+ project = create_project(credentials)
+
+ if project.persisted?
+ success(project)
+ else
+ log_and_return_error(project_save_error(project), :unprocessable_entity)
+ end
+ rescue BitbucketServer::Connection::ConnectionError => e
+ log_and_return_error("Import failed due to a BitBucket Server error: #{e}", :bad_request)
+ end
+
+ private
+
+ def create_project(credentials)
+ Gitlab::BitbucketServerImport::ProjectCreator.new(
+ project_key,
+ repo_slug,
+ repo,
+ project_name,
+ target_namespace,
+ current_user,
+ credentials
+ ).execute
+ end
+
+ def repo
+ @repo ||= client.repo(project_key, repo_slug)
+ end
+
+ def project_name
+ @project_name ||= params[:new_name].presence || repo.name
+ end
+
+ def namespace_path
+ @namespace_path ||= params[:new_namespace].presence || current_user.namespace_path
+ end
+
+ def target_namespace
+ @target_namespace ||= find_or_create_namespace(namespace_path, current_user.namespace_path)
+ end
+
+ def repo_slug
+ @repo_slug ||= params[:bitbucket_server_repo] || params[:bitbucketServerRepo]
+ end
+
+ def project_key
+ @project_key ||= params[:bitbucket_server_project] || params[:bitbucketServerProject]
+ end
+
+ def url
+ @url ||= params[:bitbucket_server_url]
+ end
+
+ def authorized?
+ can?(current_user, :create_projects, target_namespace)
+ end
+
+ def allow_local_requests?
+ Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
+ end
+
+ def blocked_url?
+ Gitlab::UrlBlocker.blocked_url?(
+ url,
+ {
+ allow_localhost: allow_local_requests?,
+ allow_local_network: allow_local_requests?,
+ schemes: %w(http https)
+ }
+ )
+ end
+
+ def log_and_return_error(message, error_type)
+ log_error(message)
+ error(_(message), error_type)
+ end
+
+ def log_error(message)
+ Gitlab::Import::Logger.error(
+ message: 'Import failed due to a BitBucket Server error',
+ error: message
+ )
+ end
+ end
+end
diff --git a/app/views/import/bitbucket_server/new.html.haml b/app/views/import/bitbucket_server/new.html.haml
index 18ccad9ca30..735535ffc36 100644
--- a/app/views/import/bitbucket_server/new.html.haml
+++ b/app/views/import/bitbucket_server/new.html.haml
@@ -17,7 +17,7 @@
.form-group.row
= label_tag :bitbucket_server_url, 'Username', class: 'col-form-label col-md-2'
.col-md-4
- = text_field_tag :bitbucket_username, '', class: 'form-control gl-mr-3', placeholder: _('username'), size: 40
+ = text_field_tag :bitbucket_server_username, '', class: 'form-control gl-mr-3', placeholder: _('username'), size: 40
.form-group.row
= label_tag :personal_access_token, 'Password/Personal Access Token', class: 'col-form-label col-md-2'
.col-md-4
diff --git a/app/views/import/bitbucket_server/status.html.haml b/app/views/import/bitbucket_server/status.html.haml
index 9d56128d85b..3c6229ab8fd 100644
--- a/app/views/import/bitbucket_server/status.html.haml
+++ b/app/views/import/bitbucket_server/status.html.haml
@@ -55,7 +55,7 @@
= project.human_import_status_name
- @repos.each do |repo|
- %tr{ id: "repo_#{repo.project_key}___#{repo.slug}", data: { project: repo.project_key, repository: repo.slug } }
+ %tr{ id: "repo_#{repo.project_key}___#{repo.slug}", data: { bitbucket_server_project: repo.project_key, bitbucket_server_repo: repo.slug } }
%td
= sanitize(link_to(repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer'), attributes: %w(href target rel))
%td.import-target
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index feebbccf46a..b667d2aa0d7 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -1,8 +1,9 @@
- page_title _("GitLab Import")
- header_title _("Projects"), root_path
-%h3.page-title
- = icon('gitlab')
+%h3.page-title.d-flex
+ .gl-display-flex.gl-align-items-center.gl-justify-content-center
+ = sprite_icon('tanuki', size: 16, css_class: 'gl-mr-2')
= _('Import an exported GitLab project')
%hr
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index 3ae37254e39..bb278fbf311 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -9,7 +9,8 @@
- if gitlab_project_import_enabled?
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit', **tracking_attrs(track_label, 'click_button', 'gitlab_export') do
- = icon('gitlab', text: 'GitLab export')
+ = sprite_icon('tanuki')
+ = _("GitLab export")
- if github_import_enabled?
%div
@@ -32,7 +33,8 @@
%div
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}",
**tracking_attrs(track_label, 'click_button', 'gitlab_com') do
- = icon('gitlab', text: 'GitLab.com')
+ = sprite_icon('tanuki')
+ = _("GitLab.com")
- unless gitlab_import_configured?
= render 'projects/gitlab_import_modal'
diff --git a/app/workers/concerns/worker_attributes.rb b/app/workers/concerns/worker_attributes.rb
index 2efa703684c..b19217b15de 100644
--- a/app/workers/concerns/worker_attributes.rb
+++ b/app/workers/concerns/worker_attributes.rb
@@ -2,7 +2,6 @@
module WorkerAttributes
extend ActiveSupport::Concern
- include Gitlab::ClassAttributes
# Resource boundaries that workers can declare through the
# `resource_boundary` attribute
@@ -31,24 +30,24 @@ module WorkerAttributes
}.stringify_keys.freeze
class_methods do
- def feature_category(value, *extras)
+ def feature_category(value)
raise "Invalid category. Use `feature_category_not_owned!` to mark a worker as not owned" if value == :not_owned
- class_attributes[:feature_category] = value
+ worker_attributes[:feature_category] = value
end
# Special case: mark this work as not associated with a feature category
# this should be used for cross-cutting concerns, such as mailer workers.
def feature_category_not_owned!
- class_attributes[:feature_category] = :not_owned
+ worker_attributes[:feature_category] = :not_owned
end
def get_feature_category
- get_class_attribute(:feature_category)
+ get_worker_attribute(:feature_category)
end
def feature_category_not_owned?
- get_feature_category == :not_owned
+ get_worker_attribute(:feature_category) == :not_owned
end
# This should be set to :high for jobs that need to be run
@@ -62,11 +61,11 @@ module WorkerAttributes
def urgency(urgency)
raise "Invalid urgency: #{urgency}" unless VALID_URGENCIES.include?(urgency)
- class_attributes[:urgency] = urgency
+ worker_attributes[:urgency] = urgency
end
def get_urgency
- class_attributes[:urgency] || :low
+ worker_attributes[:urgency] || :low
end
# Set this attribute on a job when it will call to services outside of the
@@ -74,64 +73,85 @@ module WorkerAttributes
# doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies for
# details
def worker_has_external_dependencies!
- class_attributes[:external_dependencies] = true
+ worker_attributes[:external_dependencies] = true
end
# Returns a truthy value if the worker has external dependencies.
# See doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies
# for details
def worker_has_external_dependencies?
- class_attributes[:external_dependencies]
+ worker_attributes[:external_dependencies]
end
def worker_resource_boundary(boundary)
raise "Invalid boundary" unless VALID_RESOURCE_BOUNDARIES.include? boundary
- class_attributes[:resource_boundary] = boundary
+ worker_attributes[:resource_boundary] = boundary
end
def get_worker_resource_boundary
- class_attributes[:resource_boundary] || :unknown
+ worker_attributes[:resource_boundary] || :unknown
end
def idempotent!
- class_attributes[:idempotent] = true
+ worker_attributes[:idempotent] = true
end
def idempotent?
- class_attributes[:idempotent]
+ worker_attributes[:idempotent]
end
def weight(value)
- class_attributes[:weight] = value
+ worker_attributes[:weight] = value
end
def get_weight
- class_attributes[:weight] ||
+ worker_attributes[:weight] ||
NAMESPACE_WEIGHTS[queue_namespace] ||
1
end
def tags(*values)
- class_attributes[:tags] = values
+ worker_attributes[:tags] = values
end
def get_tags
- Array(class_attributes[:tags])
+ Array(worker_attributes[:tags])
end
def deduplicate(strategy, options = {})
- class_attributes[:deduplication_strategy] = strategy
- class_attributes[:deduplication_options] = options
+ worker_attributes[:deduplication_strategy] = strategy
+ worker_attributes[:deduplication_options] = options
end
def get_deduplicate_strategy
- class_attributes[:deduplication_strategy] ||
+ worker_attributes[:deduplication_strategy] ||
Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DEFAULT_STRATEGY
end
def get_deduplication_options
- class_attributes[:deduplication_options] || {}
+ worker_attributes[:deduplication_options] || {}
+ end
+
+ protected
+
+ # Returns a worker attribute declared on this class or its parent class.
+ # This approach allows declared attributes to be inherited by
+ # child classes.
+ def get_worker_attribute(name)
+ worker_attributes[name] || superclass_worker_attributes(name)
+ end
+
+ private
+
+ def worker_attributes
+ @attributes ||= {}
+ end
+
+ def superclass_worker_attributes(name)
+ return unless superclass.include? WorkerAttributes
+
+ superclass.get_worker_attribute(name)
end
end
end
diff --git a/changelogs/unreleased/36788-feature-proposal-api-for-import-from-bitbucket-server.yml b/changelogs/unreleased/36788-feature-proposal-api-for-import-from-bitbucket-server.yml
new file mode 100644
index 00000000000..2d82aafd95f
--- /dev/null
+++ b/changelogs/unreleased/36788-feature-proposal-api-for-import-from-bitbucket-server.yml
@@ -0,0 +1,5 @@
+---
+title: 'Resolve Feature proposal: API for import from BitBucket Server'
+merge_request: 33097
+author:
+type: added
diff --git a/changelogs/unreleased/fj-225964-include-personal-snippets-in-snippets-size.yml b/changelogs/unreleased/fj-225964-include-personal-snippets-in-snippets-size.yml
new file mode 100644
index 00000000000..1997abffcaf
--- /dev/null
+++ b/changelogs/unreleased/fj-225964-include-personal-snippets-in-snippets-size.yml
@@ -0,0 +1,5 @@
+---
+title: Include personal snippets size in RootStorageStatistics
+merge_request: 35984
+author:
+type: changed
diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile
index 54559af4066..a70bc8f561c 100644
--- a/danger/roulette/Dangerfile
+++ b/danger/roulette/Dangerfile
@@ -39,6 +39,10 @@ MARKDOWN
OPTIONAL_REVIEW_TEMPLATE = "%{role} review is optional for %{category}".freeze
NOT_AVAILABLE_TEMPLATE = 'No %{role} available'.freeze
+def mr_author
+ roulette.team.find { |person| person.username == gitlab.mr_author }
+end
+
def note_for_category_role(spin, role)
if spin.optional_role == role
return OPTIONAL_REVIEW_TEMPLATE % { role: role.capitalize, category: helper.label_for_category(spin.category) }
@@ -68,7 +72,16 @@ if changes.any?
branch_name = gitlab.mr_json['source_branch']
roulette_spins = roulette.spin(project, categories, branch_name)
- rows = roulette_spins.map { |spin| markdown_row_for_spin(spin) }
+ rows = roulette_spins.map do |spin|
+ # MR includes QA changes, but also other changes, and author isn't an SET
+ if spin.category == :qa && categories.size > 1 && !mr_author.reviewer?(project, spin.category, [])
+ spin.optional_role = :maintainer
+ end
+
+ spin.optional_role = :maintainer if spin.category == :test
+
+ markdown_row_for_spin(spin)
+ end
unknown = changes.fetch(:unknown, [])
diff --git a/doc/api/import.md b/doc/api/import.md
index 307796f8acb..a64d8783da4 100644
--- a/doc/api/import.md
+++ b/doc/api/import.md
@@ -29,3 +29,41 @@ Example response:
"full_name": "Administrator / my-repo"
}
```
+
+## Import repository from Bitbucket Server
+
+Import your projects from Bitbucket Server to GitLab via the API.
+
+NOTE: **Note:**
+The Bitbucket Project Key is only used for finding the repository in Bitbucket.
+You must specify a `target_namespace` if you want to import the repository to a GitLab group.
+If you do not specify `target_namespace`, the project will import to your personal user namespace.
+
+```plaintext
+POST /import/bitbucket_server
+```
+
+| Attribute | Type | Required | Description |
+|------------|---------|----------|---------------------|
+
+| `bitbucket_server_url` | string | yes | Bitbucket Server URL |
+| `bitbucket_server_username` | string | yes | Bitbucket Server Username |
+| `personal_access_token` | string | yes | Bitbucket Server personal access token/password |
+| `bitbucket_server_project` | string | yes | Bitbucket Project Key |
+| `bitbucket_server_repo` | string | yes | Bitbucket Repository Name |
+| `new_name` | string | no | New repo name |
+| `target_namespace` | string | no | Namespace to import repo into |
+
+```shell
+curl --request POST \
+ --url https://gitlab.example.com/api/v4/import/bitbucket/server \
+ --header "content-type: application/json" \
+ --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" \
+ --data '{
+ "bitbucket_server_url": "http://bitbucket.example.com",
+ "bitbucket_server_username": "root",
+ "personal_access_token": "Nzk4MDcxODY4MDAyOiP8y410zF3tGAyLnHRv/E0+3xYs",
+ "bitbucket_server_project": "NEW",
+ "bitbucket_server_repo": "my-repo"
+}'
+```
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 72989a3c368..824f8e97dfd 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -325,9 +325,19 @@ tenses, words, and phrases:
- Avoid using the word *currently* when talking about the product or its
features. The documentation describes the product as it is, and not as it
will be at some indeterminate point in the future.
+- Avoid the using the word *scalability* with increasing GitLab's performance
+ for additional users. Using the words *scale* or *scaling* in other cases is
+ acceptable, but references to increasing GitLab's performance for additional
+ users should direct readers to the GitLab
+ [reference architectures](../../administration/reference_architectures/index.md)
+ page.
+- Avoid all forms of the phrases *high availability* and *HA*, and instead
+ direct readers to the GitLab [reference architectures](../../administration/reference_architectures/index.md)
+ for information about configuring GitLab to have the performance needed for
+ additional users over time.
- Don't use profanity or obscenities. Doing so may negatively affect other
users and contributors, which is contrary to GitLab's value of
- [diversity and inclusion](https://about.gitlab.com/handbook/values/#diversity-inclusion).
+ [Diversity, Inclusion, and Belonging](https://about.gitlab.com/handbook/values/#diversity-inclusion).
- Avoid the use of [racially-insensitive terminology or phrases](https://www.marketplace.org/2020/06/17/tech-companies-update-language-to-avoid-offensive-terms/). For example:
- Use *primary* and *secondary* for database and server relationships.
- Use *allowlist* and *denylist* to describe access control lists.
@@ -1311,20 +1321,20 @@ The following are styles to follow when describing UI elements on a screen:
### Verbs for UI elements
-The following are recommended verbs for specific uses.
+The following are recommended verbs for specific uses with UI elements:
-| Recommended | Used for | Alternatives |
-|:------------|:---------------------------|:---------------------------|
-| "click" | buttons, links, menu items | "hit", "press", "select" |
-| "check" | checkboxes | "enable", "click", "press" |
-| "select" | dropdowns | "pick" |
-| "expand" | expandable sections | "open" |
+| Recommended | Used for | Replaces |
+|:--------------------|:---------------------------|:---------------------------|
+| *click* | buttons, links, menu items | "hit", "press", "select" |
+| *select* or *clear* | checkboxes | "enable", "click", "press" |
+| *select* | dropdowns | "pick" |
+| *expand* | expandable sections | "open" |
### Other Verbs
-| Recommended | Used for | Alternatives |
-|:------------|:--------------------------------|:-------------------|
-| "go" | making a browser go to location | "navigate", "open" |
+| Recommended | Used for | Replaces |
+|:------------|:--------------------------------|:----------------------|
+| *go to* | making a browser go to location | "navigate to", "open" |
## GitLab versions and tiers
@@ -1598,6 +1608,9 @@ can facilitate this by making sure the troubleshooting content addresses:
1. How the user can confirm they have the problem.
1. Steps the user can take towards resolution of the problem.
+If the contents of each category can be summarized in one line and a list of steps aren't required, consider setting up a
+[table](#tables) with headers of *Problem* \| *Cause* \| *Solution* (or *Workaround* if the fix is temporary), or *Error message* \| *Solution*.
+
## Feature flags
Learn how to [document features deployed behind flags](feature_flags.md).
diff --git a/doc/development/multi_version_compatibility.md b/doc/development/multi_version_compatibility.md
index 001517d44ea..aedd5c1ffb7 100644
--- a/doc/development/multi_version_compatibility.md
+++ b/doc/development/multi_version_compatibility.md
@@ -45,7 +45,7 @@ and set this column to `false`. The old servers were still updating the old colu
that updated the new column from the old one. For the new servers though, they were only updating the new column and that same trigger
was now working against us and setting it back to the wrong value.
-For more information, see this [confidential issue](../user/project/issues/confidential_issues.md) `https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/9176`.
+For more information, see [the relevant issue](https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/9176).
### Sidebar wasn't loading for some users
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 0997062006d..8b285e0c9f1 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -310,8 +310,7 @@ sudo adduser --disabled-login --gecos 'GitLab' git
## 6. Database
NOTE: **Note:**
-Starting from GitLab 12.1, only PostgreSQL is supported. Because we need to make
-use of extensions and concurrent index removal, you need at least PostgreSQL 9.2.
+Starting from GitLab 12.1, only PostgreSQL is supported. Since GitLab 13.0, we require PostgreSQL 11+.
1. Install the database packages:
@@ -426,11 +425,20 @@ cd /home/git
### Clone the Source
+Clone Community Edition:
+
```shell
# Clone GitLab repository
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-foss.git -b X-Y-stable gitlab
```
+Clone Enterprise Edition:
+
+```shell
+# Clone GitLab repository
+sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ee.git -b X-Y-stable gitlab
+```
+
Make sure to replace `X-Y-stable` with the stable branch that matches the
version you want to install. For example, if you want to install 11.8 you would
use the branch name `11-8-stable`.
@@ -601,7 +609,7 @@ You can specify a different Git repository by providing it as an extra parameter
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production
```
-### Install GitLab-Elasticsearch-indexer
+### Install GitLab-Elasticsearch-indexer on Enterprise Edition
GitLab-Elasticsearch-Indexer uses [GNU Make](https://www.gnu.org/software/make/). The
following command-line will install GitLab-Elasticsearch-Indexer in `/home/git/gitlab-elasticsearch-indexer`
@@ -620,6 +628,9 @@ sudo -u git -H bundle exec rake "gitlab:indexer:install[/home/git/gitlab-elastic
The source code will first be fetched to the path specified by the first parameter. Then a binary will be built under its `bin` directory.
You will then need to update `gitlab.yml`'s `production -> elasticsearch -> indexer_path` setting to point to that binary.
+NOTE: **Note:**
+Elasticsearch is a feature of GitLab Enterprise Edition and isn't included in GitLab Community Edition.
+
### Install GitLab Pages
GitLab Pages uses [GNU Make](https://www.gnu.org/software/make/). This step is optional and only needed if you wish to host static sites from within GitLab. The following commands will install GitLab Pages in `/home/git/gitlab-pages`. For additional setup steps, consult the [administration guide](https://gitlab.com/gitlab-org/gitlab/blob/master/doc/administration/pages/source.md) for your version of GitLab as the GitLab Pages daemon can be run several different ways.
diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md
index 4908f8a5edb..45f8784da88 100644
--- a/doc/user/application_security/container_scanning/index.md
+++ b/doc/user/application_security/container_scanning/index.md
@@ -141,7 +141,7 @@ enables verbose output from Clair by setting the `CLAIR_OUTPUT` environment vari
```yaml
include:
- template: Container-Scanning.gitlab-ci.yml
+ - template: Container-Scanning.gitlab-ci.yml
variables:
CLAIR_OUTPUT: High
@@ -184,7 +184,7 @@ specify any additional keys. For example:
```yaml
include:
- template: Container-Scanning.gitlab-ci.yml
+ - template: Container-Scanning.gitlab-ci.yml
container_scanning:
variables:
@@ -196,15 +196,15 @@ GitLab 13.0 and later doesn't support [`only` and `except`](../../../ci/yaml/REA
When overriding the template, you must use [`rules`](../../../ci/yaml/README.md#rules)
instead.
-### Vulnerability whitelisting
+### Vulnerability allowlisting
-To whitelist specific vulnerabilities, follow these steps:
+To allowlist specific vulnerabilities, follow these steps:
1. Set `GIT_STRATEGY: fetch` in your `.gitlab-ci.yml` file by following the instructions in
[overriding the Container Scanning template](#overriding-the-container-scanning-template).
-1. Define the whitelisted vulnerabilities in a YAML file named `clair-whitelist.yml`. This must use
- the format described in the [whitelist example file](https://github.com/arminc/clair-scanner/blob/v12/example-whitelist.yaml).
-1. Add the `clair-whitelist.yml` file to your project's Git repository.
+1. Define the allowlisted vulnerabilities in a YAML file named `vulnerability-allowlist.yml`. This must use
+ the format described in the [allowlist example file](https://gitlab.com/gitlab-org/security-products/analyzers/klar/-/raw/master/testdata/vulnerability-allowlist.yml).
+1. Add the `vulnerability-allowlist.yml` file to your project's Git repository.
### Running Container Scanning in an offline environment
diff --git a/doc/user/group/saml_sso/index.md b/doc/user/group/saml_sso/index.md
index dfb6d87ebe3..6aa2ea67407 100644
--- a/doc/user/group/saml_sso/index.md
+++ b/doc/user/group/saml_sso/index.md
@@ -21,7 +21,7 @@ For example, if you remove a user from the SCIM app, SCIM removes that same user
## Configuring your Identity Provider
1. Navigate to the group and click **Settings > SAML SSO**.
-1. Configure your SAML server using the **Assertion consumer service URL**, **Identifier**, and **GitLab single sign on URL**. Alternatively GitLab provides [metadata XML configuration](#metadata-configuration). See [specific identity provider documentation](#providers) for more details.
+1. Configure your SAML server using the **Assertion consumer service URL**, **Identifier**, and **GitLab single sign-on URL**. Alternatively GitLab provides [metadata XML configuration](#metadata-configuration). See [specific identity provider documentation](#providers) for more details.
1. Configure the SAML response to include a NameID that uniquely identifies each user.
1. Configure [required assertions](group_managed_accounts.md#assertions) if using [Group Managed Accounts](group_managed_accounts.md).
1. Once the identity provider is set up, move on to [configuring GitLab](#configuring-gitlab).
@@ -61,7 +61,7 @@ GitLab provides metadata XML that can be used to configure your Identity Provide
Once you've set up your identity provider to work with GitLab, you'll need to configure GitLab to use it for authentication:
1. Navigate to the group's **Settings > SAML SSO**.
-1. Find the SSO URL from your Identity Provider and enter it the **Identity provider single sign on URL** field.
+1. Find the SSO URL from your Identity Provider and enter it the **Identity provider single sign-on URL** field.
1. Find and enter the fingerprint for the SAML token signing certificate in the **Certificate** field.
1. Click the **Enable SAML authentication for this group** toggle switch.
1. Click the **Save changes** button.
@@ -76,7 +76,7 @@ Please note that the certificate [fingerprint algorithm](#additional-providers-a
- [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5291) in GitLab 11.8.
- [Improved](https://gitlab.com/gitlab-org/gitlab/-/issues/9255) in GitLab 11.11 with ongoing enforcement in the GitLab UI.
-With this option enabled, users must go through your group's GitLab single sign on URL. They may also be added via SCIM, if configured. Users cannot be added manually, and may only access project/group resources via the UI by signing in through the SSO URL.
+With this option enabled, users must go through your group's GitLab single sign-on URL. They may also be added via SCIM, if configured. Users cannot be added manually, and may only access project/group resources via the UI by signing in through the SSO URL.
However, users will not be prompted to sign in through SSO on each visit. GitLab will check whether a user has authenticated through SSO, and will only prompt the user to sign in via SSO if the session has expired.
@@ -108,8 +108,8 @@ For a demo of the Azure SAML setup including SCIM, see [SCIM Provisioning on Azu
|--------------|----------------|
| Identifier | Identifier (Entity ID) |
| Assertion consumer service URL | Reply URL (Assertion Consumer Service URL) |
-| GitLab single sign on URL | Sign on URL |
-| Identity provider single sign on URL | Login URL |
+| GitLab single sign-on URL | Sign on URL |
+| Identity provider single sign-on URL | Login URL |
| Certificate fingerprint | Thumbprint |
We recommend:
@@ -125,11 +125,11 @@ For a demo of the Okta SAML setup including SCIM, see [Demo: Okta Group SAML & S
| GitLab Setting | Okta Field |
|--------------|----------------|
| Identifier | Audience URI |
-| Assertion consumer service URL | Single sign on URL |
-| GitLab single sign on URL | Login page URL (under **Application Login Page** settings) |
-| Identity provider single sign on URL | Identity Provider Single Sign-On URL |
+| Assertion consumer service URL | Single sign-on URL |
+| GitLab single sign-on URL | Login page URL (under **Application Login Page** settings) |
+| Identity provider single sign-on URL | Identity Provider Single Sign-On URL |
-Under Okta's **Single sign on URL** field, check the option **Use this for Recipient URL and Destination URL**.
+Under Okta's **Single sign-on URL** field, check the option **Use this for Recipient URL and Destination URL**.
We recommend:
@@ -147,8 +147,8 @@ For GitLab.com, use a generic SAML Test Connector such as the SAML Test Connecto
| Assertion consumer service URL | Recipient |
| Assertion consumer service URL | ACS (Consumer) URL |
| Assertion consumer service URL (escaped version) | ACS (Consumer) URL Validator |
-| GitLab single sign on URL | Login URL |
-| Identity provider single sign on URL | SAML 2.0 Endpoint |
+| GitLab single sign-on URL | Login URL |
+| Identity provider single sign-on URL | SAML 2.0 Endpoint |
Recommended `NameID` value: `OneLogin ID`.
@@ -200,7 +200,7 @@ When a user tries to sign in with Group SSO, they will need an account that's co
To link SAML to your existing GitLab.com account:
1. Sign in to your GitLab.com account.
-1. Locate and visit the **GitLab single sign on URL** for the group you are signing in to. A group Admin can find this on the group's **Settings > SAML SSO** page. If the sign-in URL is configured, users can connect to the GitLab app from the Identity Provider.
+1. Locate and visit the **GitLab single sign-on URL** for the group you are signing in to. A group Admin can find this on the group's **Settings > SAML SSO** page. If the sign-in URL is configured, users can connect to the GitLab app from the Identity Provider.
1. Click **Authorize**.
1. Enter your credentials on the Identity Provider if prompted.
1. You will be redirected back to GitLab.com and should now have access to the group. In the future, you can use SAML to sign in to GitLab.com.
@@ -358,9 +358,9 @@ Ensure that the user who is trying to link their GitLab account has been added a
### Stuck in a login "loop"
-Ensure that the **GitLab single sign on URL** has been configured as "Login URL" (or similarly named field) in the identity provider's SAML app.
+Ensure that the **GitLab single sign-on URL** has been configured as "Login URL" (or similarly named field) in the identity provider's SAML app.
-Alternatively, when users need to [link SAML to their existing GitLab.com account](#linking-saml-to-your-existing-gitlabcom-account), provide the **GitLab single sign on URL** and instruct users not to use the SAML app on first sign in.
+Alternatively, when users need to [link SAML to their existing GitLab.com account](#linking-saml-to-your-existing-gitlabcom-account), provide the **GitLab single sign-on URL** and instruct users not to use the SAML app on first sign in.
### The NameID has changed
diff --git a/doc/user/group/saml_sso/scim_setup.md b/doc/user/group/saml_sso/scim_setup.md
index 0f845a66bb0..a0297f55a37 100644
--- a/doc/user/group/saml_sso/scim_setup.md
+++ b/doc/user/group/saml_sso/scim_setup.md
@@ -163,7 +163,7 @@ As long as [Group SAML](index.md) has been configured, prior to turning on sync,
- By following these steps:
1. Sign in to GitLab.com if needed.
- 1. Click on the GitLab app in the identity provider's dashboard or visit the **GitLab single sign on URL**.
+ 1. Click on the GitLab app in the identity provider's dashboard or visit the **GitLab single sign-on URL**.
1. Click on the **Authorize** button.
New users and existing users on subsequent visits can access the group through the identify provider's dashboard or by visiting links directly.
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 658a3648775..c57c1dfe2f3 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -155,6 +155,7 @@ The following table depicts the various user permission levels in a project.
| Remove project | | | | | ✓ |
| Archive project | | | | | ✓ |
| Delete issues | | | | | ✓ |
+| Delete pipelines | | | | | ✓ |
| Delete merge request | | | | | ✓ |
| Disable notification emails | | | | | ✓ |
| Force push to protected branches (*4*) | | | | | |
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 7c2aa031fea..def05bf94e4 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -41,7 +41,7 @@ The following quick actions are applicable to descriptions, discussions and thre
| `/create_merge_request <branch name>` | ✓ | | | Create a new merge request starting from the current issue. |
| `/done` | ✓ | ✓ | ✓ | Mark To-Do as done. |
| `/due <date>` | ✓ | | | Set due date. Examples of valid `<date>` include `in 2 days`, `this Friday` and `December 31st`. |
-| `/duplicate <#issue>` | ✓ | | | Mark this issue as a duplicate of another issue and mark them as related. **(STARTER)** |
+| `/duplicate <#issue>` | ✓ | | | Close this issue and mark as a duplicate of another issue. **(CORE)** Also, mark both as related. **(STARTER)** |
| `/epic <epic>` | ✓ | | | Add to epic `<epic>`. The `<epic>` value should be in the format of `&epic`, `group&epic`, or a URL to an epic. **(PREMIUM)** |
| `/estimate <<W>w <DD>d <hh>h <mm>m>` | ✓ | ✓ | | Set time estimate. For example, `/estimate 1w 3d 2h 14m`. |
| `/iteration *iteration:iteration` | ✓ | | | Set iteration ([Introduced in GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/issues/196795)) **(STARTER)** |
diff --git a/doc/user/project/service_desk.md b/doc/user/project/service_desk.md
index ffb1f6a1407..05d616f75dc 100644
--- a/doc/user/project/service_desk.md
+++ b/doc/user/project/service_desk.md
@@ -149,7 +149,9 @@ It can contain only lowercase letters (`a-z`), numbers (`0-9`), or underscores (
![Setting custom Service Desk email address](img/service_desk_custom_email_address_v13_0.png)
-For example, suppose you add the following to your configuration:
+You can add the following snippets to your configuration.
+
+Example for installations from source:
```yaml
service_desk_email:
@@ -167,6 +169,32 @@ service_desk_email:
expunge_deleted: true
```
+Example for Omnibus GitLab installations:
+
+```ruby
+gitlab_rails['service_desk_email_enabled'] = true
+
+gitlab_rails['service_desk_email_address'] = "project_contact+%{key}@gmail.com"
+
+gitlab_rails['service_desk_email_email'] = "project_support@gmail.com"
+
+gitlab_rails['service_desk_email_password'] = "[REDACTED]"
+
+gitlab_rails['service_desk_email_mailbox_name'] = "inbox"
+
+gitlab_rails['service_desk_email_idle_timeout'] = 60
+
+gitlab_rails['service_desk_email_log_file'] = "/var/log/gitlab/mailroom/mail_room_json.log"
+
+gitlab_rails['service_desk_email_host'] = "imap.gmail.com"
+
+gitlab_rails['service_desk_email_port'] = 993
+
+gitlab_rails['service_desk_email_ssl'] = true
+
+gitlab_rails['service_desk_email_start_tls'] = false
+```
+
In this case, suppose the `mygroup/myproject` project Service Desk settings has the project name
suffix set to `support`, and a user sends an email to `project_contact+mygroup-myproject-support@example.com`.
As a result, a new Service Desk issue is created from this email in the `mygroup/myproject` project.
diff --git a/lib/api/api.rb b/lib/api/api.rb
index e5bc49de32e..69bac061fe2 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -156,6 +156,7 @@ module API
mount ::API::Groups
mount ::API::GroupContainerRepositories
mount ::API::GroupVariables
+ mount ::API::ImportBitbucketServer
mount ::API::ImportGithub
mount ::API::Issues
mount ::API::JobArtifacts
diff --git a/lib/api/import_bitbucket_server.rb b/lib/api/import_bitbucket_server.rb
new file mode 100644
index 00000000000..df3235420e9
--- /dev/null
+++ b/lib/api/import_bitbucket_server.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module API
+ class ImportBitbucketServer < Grape::API::Instance
+ helpers do
+ def client
+ @client ||= BitbucketServer::Client.new(credentials)
+ end
+
+ def credentials
+ @credentials ||= {
+ base_uri: params[:bitbucket_server_url],
+ user: params[:bitbucket_server_username],
+ password: params[:personal_access_token]
+ }
+ end
+ end
+
+ desc 'Import a BitBucket Server repository' do
+ detail 'This feature was introduced in GitLab 13.2.'
+ success ::ProjectEntity
+ end
+
+ params do
+ requires :bitbucket_server_url, type: String, desc: 'Bitbucket Server URL'
+ requires :bitbucket_server_username, type: String, desc: 'BitBucket Server Username'
+ requires :personal_access_token, type: String, desc: 'BitBucket Server personal access token/password'
+ requires :bitbucket_server_project, type: String, desc: 'BitBucket Server Project Key'
+ requires :bitbucket_server_repo, type: String, desc: 'BitBucket Server Repository Name'
+ optional :new_name, type: String, desc: 'New repo name'
+ optional :new_namespace, type: String, desc: 'Namespace to import repo into'
+ end
+
+ post 'import/bitbucket_server' do
+ result = Import::BitbucketServerService.new(client, current_user, params).execute(credentials)
+
+ if result[:status] == :success
+ present ProjectSerializer.new.represent(result[:project], serializer: :import)
+ else
+ render_api_error!({ error: result[:message] }, result[:http_status])
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/class_attributes.rb b/lib/gitlab/class_attributes.rb
deleted file mode 100644
index 6560c97b2e6..00000000000
--- a/lib/gitlab/class_attributes.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module ClassAttributes
- extend ActiveSupport::Concern
-
- class_methods do
- protected
-
- # Returns an attribute declared on this class or its parent class.
- # This approach allows declared attributes to be inherited by
- # child classes.
- def get_class_attribute(name)
- class_attributes[name] || superclass_attributes(name)
- end
-
- private
-
- def class_attributes
- @class_attributes ||= {}
- end
-
- def superclass_attributes(name)
- return unless superclass.include? Gitlab::ClassAttributes
-
- superclass.get_class_attribute(name)
- end
- end
- end
-end
diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb
index 9783e517bbb..380b035293c 100644
--- a/lib/gitlab/danger/roulette.rb
+++ b/lib/gitlab/danger/roulette.rb
@@ -6,7 +6,6 @@ module Gitlab
module Danger
module Roulette
ROULETTE_DATA_URL = 'https://gitlab-org.gitlab.io/gitlab-roulette/roulette.json'
- OPTIONAL_CATEGORIES = [:qa, :test].freeze
Spin = Struct.new(:category, :reviewer, :maintainer, :optional_role)
@@ -119,11 +118,7 @@ module Gitlab
reviewer = spin_for_person(reviewers + traintainers + traintainers, random: random)
maintainer = spin_for_person(maintainers, random: random)
- Spin.new(category, reviewer, maintainer).tap do |spin|
- if OPTIONAL_CATEGORIES.include?(category)
- spin.optional_role = :maintainer
- end
- end
+ Spin.new(category, reviewer, maintainer)
end
end
end
diff --git a/lib/gitlab/metrics/web_transaction.rb b/lib/gitlab/metrics/web_transaction.rb
index 2064f9290d3..fa17548723e 100644
--- a/lib/gitlab/metrics/web_transaction.rb
+++ b/lib/gitlab/metrics/web_transaction.rb
@@ -32,10 +32,6 @@ module Gitlab
action = "#{controller.action_name}"
- # Try to get the feature category, but don't fail when the controller is
- # not an ApplicationController.
- feature_category = controller.class.try(:feature_category_for_action, action).to_s
-
# Devise exposes a method called "request_format" that does the below.
# However, this method is not available to all controllers (e.g. certain
# Doorkeeper controllers). As such we use the underlying code directly.
@@ -49,7 +45,7 @@ module Gitlab
action = "#{action}.#{suffix}"
end
- { controller: controller.class.name, action: action, feature_category: feature_category }
+ { controller: controller.class.name, action: action }
end
def labels_from_endpoint
@@ -65,10 +61,7 @@ module Gitlab
if route
path = endpoint_paths_cache[route.request_method][route.path]
-
- # Feature categories will be added for grape endpoints in
- # https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/462
- { controller: 'Grape', action: "#{route.request_method} #{path}", feature_category: '' }
+ { controller: 'Grape', action: "#{route.request_method} #{path}" }
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 13b8e8a1190..3b28b67ca0d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -409,6 +409,9 @@ msgstr ""
msgid "%{labelStart}File:%{labelEnd} %{file}"
msgstr ""
+msgid "%{labelStart}Headers:%{labelEnd} %{headers}"
+msgstr ""
+
msgid "%{labelStart}Image:%{labelEnd} %{image}"
msgstr ""
@@ -427,6 +430,12 @@ msgstr ""
msgid "%{labelStart}Severity:%{labelEnd} %{severity}"
msgstr ""
+msgid "%{labelStart}Status:%{labelEnd} %{status}"
+msgstr ""
+
+msgid "%{labelStart}URL:%{labelEnd} %{url}"
+msgstr ""
+
msgid "%{label_for_message} unavailable"
msgstr ""
@@ -10813,6 +10822,9 @@ msgstr ""
msgid "GitLab commit"
msgstr ""
+msgid "GitLab export"
+msgstr ""
+
msgid "GitLab for Slack"
msgstr ""
@@ -10837,7 +10849,7 @@ msgstr ""
msgid "GitLab restart is required to apply changes."
msgstr ""
-msgid "GitLab single sign on URL"
+msgid "GitLab single sign-on URL"
msgstr ""
msgid "GitLab uses %{jaeger_link} to monitor distributed systems."
@@ -10846,6 +10858,9 @@ msgstr ""
msgid "GitLab will run a background job that will produce pseudonymized CSVs of the GitLab database that will be uploaded to your configured object storage directory."
msgstr ""
+msgid "GitLab.com"
+msgstr ""
+
msgid "GitLab.com import"
msgstr ""
@@ -11386,7 +11401,7 @@ msgstr ""
msgid "GroupSAML|Identity"
msgstr ""
-msgid "GroupSAML|Identity provider single sign on URL"
+msgid "GroupSAML|Identity provider single sign-on URL"
msgstr ""
msgid "GroupSAML|Make sure you save this token — you won't be able to access it again."
@@ -22626,6 +22641,9 @@ msgstr ""
msgid "TestReports|There are no tests to show."
msgstr ""
+msgid "TestReports|There was an error fetching the summary."
+msgstr ""
+
msgid "TestReports|There was an error fetching the test reports."
msgstr ""
@@ -25680,6 +25698,12 @@ msgstr ""
msgid "Vulnerability|Project"
msgstr ""
+msgid "Vulnerability|Request"
+msgstr ""
+
+msgid "Vulnerability|Response"
+msgstr ""
+
msgid "Vulnerability|Scanner"
msgstr ""
diff --git a/spec/controllers/concerns/controller_with_feature_category/config_spec.rb b/spec/controllers/concerns/controller_with_feature_category/config_spec.rb
deleted file mode 100644
index 9b8ffd2baab..00000000000
--- a/spec/controllers/concerns/controller_with_feature_category/config_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-require "fast_spec_helper"
-require "rspec-parameterized"
-require_relative "../../../../app/controllers/concerns/controller_with_feature_category/config"
-
-RSpec.describe ControllerWithFeatureCategory::Config do
- describe "#matches?" do
- using RSpec::Parameterized::TableSyntax
-
- where(:only_actions, :except_actions, :if_proc, :unless_proc, :test_action, :expected) do
- nil | nil | nil | nil | "action" | true
- [:included] | nil | nil | nil | "action" | false
- [:included] | nil | nil | nil | "included" | true
- nil | [:excluded] | nil | nil | "excluded" | false
- nil | nil | true | nil | "action" | true
- [:included] | nil | true | nil | "action" | false
- [:included] | nil | true | nil | "included" | true
- nil | [:excluded] | true | nil | "excluded" | false
- nil | nil | false | nil | "action" | false
- [:included] | nil | false | nil | "action" | false
- [:included] | nil | false | nil | "included" | false
- nil | [:excluded] | false | nil | "excluded" | false
- nil | nil | nil | true | "action" | false
- [:included] | nil | nil | true | "action" | false
- [:included] | nil | nil | true | "included" | false
- nil | [:excluded] | nil | true | "excluded" | false
- nil | nil | nil | false | "action" | true
- [:included] | nil | nil | false | "action" | false
- [:included] | nil | nil | false | "included" | true
- nil | [:excluded] | nil | false | "excluded" | false
- nil | nil | true | false | "action" | true
- [:included] | nil | true | false | "action" | false
- [:included] | nil | true | false | "included" | true
- nil | [:excluded] | true | false | "excluded" | false
- nil | nil | false | true | "action" | false
- [:included] | nil | false | true | "action" | false
- [:included] | nil | false | true | "included" | false
- nil | [:excluded] | false | true | "excluded" | false
- end
-
- with_them do
- let(:config) do
- if_to_proc = if_proc.nil? ? nil : -> (_) { if_proc }
- unless_to_proc = unless_proc.nil? ? nil : -> (_) { unless_proc }
-
- described_class.new(:category, only_actions, except_actions, if_to_proc, unless_to_proc)
- end
-
- specify { expect(config.matches?(test_action)).to be(expected) }
- end
- end
-end
diff --git a/spec/controllers/concerns/controller_with_feature_category_spec.rb b/spec/controllers/concerns/controller_with_feature_category_spec.rb
deleted file mode 100644
index e603a7d14c4..00000000000
--- a/spec/controllers/concerns/controller_with_feature_category_spec.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-require 'fast_spec_helper'
-require_relative "../../../app/controllers/concerns/controller_with_feature_category"
-require_relative "../../../app/controllers/concerns/controller_with_feature_category/config"
-
-RSpec.describe ControllerWithFeatureCategory do
- describe ".feature_category_for_action" do
- let(:base_controller) do
- Class.new do
- include ControllerWithFeatureCategory
- end
- end
-
- let(:controller) do
- Class.new(base_controller) do
- feature_category :baz
- feature_category :foo, except: %w(update edit)
- feature_category :bar, only: %w(index show)
- feature_category :quux, only: %w(destroy)
- feature_category :quuz, only: %w(destroy)
- end
- end
-
- let(:subclass) do
- Class.new(controller) do
- feature_category :qux, only: %w(index)
- end
- end
-
- it "is nil when nothing was defined" do
- expect(base_controller.feature_category_for_action("hello")).to be_nil
- end
-
- it "returns the expected category", :aggregate_failures do
- expect(controller.feature_category_for_action("update")).to eq(:baz)
- expect(controller.feature_category_for_action("hello")).to eq(:foo)
- expect(controller.feature_category_for_action("index")).to eq(:bar)
- end
-
- it "returns the closest match for categories defined in subclasses" do
- expect(subclass.feature_category_for_action("index")).to eq(:qux)
- expect(subclass.feature_category_for_action("show")).to eq(:bar)
- end
-
- it "returns the last defined feature category when multiple match" do
- expect(controller.feature_category_for_action("destroy")).to eq(:quuz)
- end
-
- it "raises an error when using including and excluding the same action" do
- expect do
- Class.new(base_controller) do
- feature_category :hello, only: [:world], except: [:world]
- end
- end.to raise_error(%r(cannot configure both `only` and `except`))
- end
-
- it "raises an error when using unknown arguments" do
- expect do
- Class.new(base_controller) do
- feature_category :hello, hello: :world
- end
- end.to raise_error(%r(unknown arguments))
- end
- end
-end
diff --git a/spec/controllers/every_controller_spec.rb b/spec/controllers/every_controller_spec.rb
deleted file mode 100644
index 4785ee9ed8f..00000000000
--- a/spec/controllers/every_controller_spec.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe "Every controller" do
- context "feature categories" do
- let_it_be(:feature_categories) do
- YAML.load_file(Rails.root.join('config', 'feature_categories.yml')).map(&:to_sym).to_set
- end
-
- let_it_be(:controller_actions) do
- # This will return tuples of all controller actions defined in the routes
- # Only for controllers inheriting ApplicationController
- # Excluding controllers from gems (OAuth, Sidekiq)
- Rails.application.routes.routes
- .map { |route| route.required_defaults.presence }
- .compact
- .select { |route| route[:controller].present? && route[:action].present? }
- .map { |route| [constantize_controller(route[:controller]), route[:action]] }
- .reject { |route| route.first.nil? || !route.first.include?(ControllerWithFeatureCategory) }
- end
-
- let_it_be(:routes_without_category) do
- controller_actions.map do |controller, action|
- "#{controller}##{action}" unless controller.feature_category_for_action(action)
- end.compact
- end
-
- it "has feature categories" do
- pending("We'll work on defining categories for all controllers: "\
- "https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/463")
-
- expect(routes_without_category).to be_empty, "#{routes_without_category.first(10)} did not have a category"
- end
-
- it "completed controllers don't get new routes without categories" do
- completed_controllers = [Projects::MergeRequestsController].map(&:to_s)
-
- newly_introduced_missing_category = routes_without_category.select do |route|
- completed_controllers.any? { |controller| route.start_with?(controller) }
- end
-
- expect(newly_introduced_missing_category).to be_empty
- end
-
- it "recognizes the feature categories" do
- routes_unknown_category = controller_actions.map do |controller, action|
- used_category = controller.feature_category_for_action(action)
- next unless used_category
- next if used_category == :not_owned
-
- ["#{controller}##{action}", used_category] unless feature_categories.include?(used_category)
- end.compact
-
- expect(routes_unknown_category).to be_empty, "#{routes_unknown_category.first(10)} had an unknown category"
- end
-
- it "doesn't define or exclude categories on removed actions", :aggregate_failures do
- controller_actions.group_by(&:first).each do |controller, controller_action|
- existing_actions = controller_action.map(&:last)
- used_actions = actions_defined_in_feature_category_config(controller)
- non_existing_used_actions = used_actions - existing_actions
-
- expect(non_existing_used_actions).to be_empty,
- "#{controller} used #{non_existing_used_actions} to define feature category, but the route does not exist"
- end
- end
- end
-
- def constantize_controller(name)
- "#{name.camelize}Controller".constantize
- rescue NameError
- nil # some controllers, like the omniauth ones are dynamic
- end
-
- def actions_defined_in_feature_category_config(controller)
- feature_category_configs = controller.send(:class_attributes)[:feature_category_config]
- feature_category_configs.map do |config|
- Array(config.send(:only)) + Array(config.send(:except))
- end.flatten.uniq.map(&:to_s)
- end
-end
diff --git a/spec/controllers/import/bitbucket_server_controller_spec.rb b/spec/controllers/import/bitbucket_server_controller_spec.rb
index af471b478fa..3292a1ab39e 100644
--- a/spec/controllers/import/bitbucket_server_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_server_controller_spec.rb
@@ -46,7 +46,7 @@ RSpec.describe Import::BitbucketServerController do
.to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything)
.and_return(double(execute: project))
- post :create, params: { project: project_key, repository: repo_slug }, format: :json
+ post :create, params: { bitbucketServerProject: project_key, bitbucketServerRepo: repo_slug }, format: :json
expect(response).to have_gitlab_http_status(:ok)
end
@@ -59,20 +59,20 @@ RSpec.describe Import::BitbucketServerController do
.to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything)
.and_return(double(execute: project))
- post :create, params: { project: project_key, repository: repo_slug, format: :json }
+ post :create, params: { bitbucketServerProject: project_key, bitbucketServerRepo: repo_slug, format: :json }
expect(response).to have_gitlab_http_status(:ok)
end
end
it 'returns an error when an invalid project key is used' do
- post :create, params: { project: 'some&project' }
+ post :create, params: { bitbucket_server_project: 'some&project' }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
it 'returns an error when an invalid repository slug is used' do
- post :create, params: { project: 'some-project', repository: 'try*this' }
+ post :create, params: { bitbucket_server_project: 'some-project', bitbucket_server_repo: 'try*this' }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
@@ -80,7 +80,7 @@ RSpec.describe Import::BitbucketServerController do
it 'returns an error when the project cannot be found' do
allow(client).to receive(:repo).with(project_key, repo_slug).and_return(nil)
- post :create, params: { project: project_key, repository: repo_slug }, format: :json
+ post :create, params: { bitbucket_server_project: project_key, bitbucket_server_repo: repo_slug }, format: :json
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
@@ -90,15 +90,15 @@ RSpec.describe Import::BitbucketServerController do
.to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything)
.and_return(double(execute: build(:project)))
- post :create, params: { project: project_key, repository: repo_slug }, format: :json
+ post :create, params: { bitbucket_server_project: project_key, bitbucket_server_repo: repo_slug }, format: :json
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
it "returns an error when the server can't be contacted" do
- expect(client).to receive(:repo).with(project_key, repo_slug).and_raise(::BitbucketServer::Connection::ConnectionError)
+ allow(client).to receive(:repo).with(project_key, repo_slug).and_return([nil, nil])
- post :create, params: { project: project_key, repository: repo_slug }, format: :json
+ post :create, params: { bitbucket_server_project: project_key, bitbucket_server_repo: repo_slug }, format: :json
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
@@ -123,7 +123,9 @@ RSpec.describe Import::BitbucketServerController do
end
it 'sets the session variables' do
- post :configure, params: { personal_access_token: token, bitbucket_username: username, bitbucket_server_url: url }
+ allow(controller).to receive(:allow_local_requests?).and_return(true)
+
+ post :configure, params: { personal_access_token: token, bitbucket_server_username: username, bitbucket_server_url: url }
expect(session[:bitbucket_server_url]).to eq(url)
expect(session[:bitbucket_server_username]).to eq(username)
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index c6a002ad18b..78024b2e93c 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -383,8 +383,8 @@ RSpec.describe 'Pipeline', :js do
context 'without test reports' do
let(:pipeline) { create(:ci_pipeline, project: project) }
- it 'shows nothing' do
- expect(page.find('.js-test-report-badge-counter', visible: :all).text).to eq("")
+ it 'shows zero' do
+ expect(page.find('.js-test-report-badge-counter', visible: :all).text).to eq("0")
end
end
end
diff --git a/spec/frontend/fixtures/branches.rb b/spec/frontend/fixtures/branches.rb
index 4667dfb69f8..df2d1af7ecf 100644
--- a/spec/frontend/fixtures/branches.rb
+++ b/spec/frontend/fixtures/branches.rb
@@ -2,33 +2,51 @@
require 'spec_helper'
-RSpec.describe Projects::BranchesController, '(JavaScript fixtures)', type: :controller do
+RSpec.describe 'Branches (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
- let(:admin) { create(:admin) }
- let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
- let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
-
- render_views
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
before(:all) do
clean_frontend_fixtures('branches/')
+ clean_frontend_fixtures('api/branches/')
end
- before do
- sign_in(admin)
+ after(:all) do
+ remove_repository(project)
end
- after do
- remove_repository(project)
+ describe Projects::BranchesController, '(JavaScript fixtures)', type: :controller do
+ render_views
+
+ before do
+ sign_in(admin)
+ end
+
+ it 'branches/new_branch.html' do
+ get :new, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project
+ }
+
+ expect(response).to be_successful
+ end
end
- it 'branches/new_branch.html' do
- get :new, params: {
- namespace_id: project.namespace.to_param,
- project_id: project
- }
+ describe API::Branches, '(JavaScript fixtures)', type: :request do
+ include ApiHelpers
+
+ it 'api/branches/branches.json' do
+ # The search query "ma" matches a few branch names in the test
+ # repository with a variety of different properties, including:
+ # - "master": default, protected
+ # - "markdown": non-default, protected
+ # - "many_files": non-default, not protected
+ get api("/projects/#{project.id}/repository/branches?search=ma", admin)
- expect(response).to be_successful
+ expect(response).to be_successful
+ end
end
end
diff --git a/spec/frontend/fixtures/commit.rb b/spec/frontend/fixtures/commit.rb
index c5c00afd4ca..9175a757b73 100644
--- a/spec/frontend/fixtures/commit.rb
+++ b/spec/frontend/fixtures/commit.rb
@@ -2,34 +2,55 @@
require 'spec_helper'
-RSpec.describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do
+RSpec.describe 'Commit (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { create(:user) }
- let(:commit) { project.commit("master") }
-
- render_views
+ let_it_be(:commit) { project.commit("master") }
before(:all) do
clean_frontend_fixtures('commit/')
+ clean_frontend_fixtures('api/commits/')
+
+ project.add_maintainer(user)
end
before do
- project.add_maintainer(user)
- sign_in(user)
allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
end
- it 'commit/show.html' do
- params = {
- namespace_id: project.namespace,
- project_id: project,
- id: commit.id
- }
+ after(:all) do
+ remove_repository(project)
+ end
+
+ describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do
+ render_views
+
+ before do
+ sign_in(user)
+ end
+
+ it 'commit/show.html' do
+ params = {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: commit.id
+ }
+
+ get :show, params: params
+
+ expect(response).to be_successful
+ end
+ end
+
+ describe API::Commits, '(JavaScript fixtures)', type: :request do
+ include ApiHelpers
- get :show, params: params
+ it 'api/commits/commit.json' do
+ get api("/projects/#{project.id}/repository/commits/#{commit.id}", user)
- expect(response).to be_successful
+ expect(response).to be_successful
+ end
end
end
diff --git a/spec/frontend/fixtures/tags.rb b/spec/frontend/fixtures/tags.rb
new file mode 100644
index 00000000000..b2a5429fac8
--- /dev/null
+++ b/spec/frontend/fixtures/tags.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Tags (JavaScript fixtures)' do
+ include JavaScriptFixturesHelpers
+
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:project) { create(:project, :repository, path: 'tags-project') }
+
+ before(:all) do
+ clean_frontend_fixtures('api/tags/')
+ end
+
+ after(:all) do
+ remove_repository(project)
+ end
+
+ describe API::Tags, '(JavaScript fixtures)', type: :request do
+ include ApiHelpers
+
+ it 'api/tags/tags.json' do
+ get api("/projects/#{project.id}/repository/tags", admin)
+
+ expect(response).to be_successful
+ end
+ end
+end
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index 56148361e0a..bd42db36645 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -14,12 +14,16 @@ describe('Actions TestReports Store', () => {
let state;
const testReports = getJSONFixture('pipelines/test_report.json');
+ const summary = { total_count: 1 };
- const endpoint = `${TEST_HOST}/test_reports.json`;
+ const fullReportEndpoint = `${TEST_HOST}/test_reports.json`;
+ const summaryEndpoint = `${TEST_HOST}/test_reports/summary.json`;
const defaultState = {
- endpoint,
+ fullReportEndpoint,
+ summaryEndpoint,
testReports: {},
selectedSuite: {},
+ summary: {},
};
beforeEach(() => {
@@ -31,14 +35,47 @@ describe('Actions TestReports Store', () => {
mock.restore();
});
- describe('fetch reports', () => {
+ describe('fetch report summary', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/test_reports.json`).replyOnce(200, testReports, {});
+ mock.onGet(summaryEndpoint).replyOnce(200, summary, {});
});
it('sets testReports and shows tests', done => {
testAction(
- actions.fetchReports,
+ actions.fetchSummary,
+ null,
+ state,
+ [{ type: types.SET_SUMMARY, payload: summary }],
+ [],
+ done,
+ );
+ });
+
+ it('should create flash on API error', done => {
+ testAction(
+ actions.fetchSummary,
+ null,
+ {
+ summaryEndpoint: null,
+ },
+ [],
+ [],
+ () => {
+ expect(createFlash).toHaveBeenCalled();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('fetch full report', () => {
+ beforeEach(() => {
+ mock.onGet(fullReportEndpoint).replyOnce(200, testReports, {});
+ });
+
+ it('sets testReports and shows tests', done => {
+ testAction(
+ actions.fetchFullReport,
null,
state,
[{ type: types.SET_REPORTS, payload: testReports }],
@@ -49,10 +86,10 @@ describe('Actions TestReports Store', () => {
it('should create flash on API error', done => {
testAction(
- actions.fetchReports,
+ actions.fetchFullReport,
null,
{
- endpoint: null,
+ fullReportEndpoint: null,
},
[],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
index a0eb93c4e6b..cf54cacab60 100644
--- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -18,15 +18,6 @@ describe('Mutations TestReports Store', () => {
mockState = defaultState;
});
- describe('set endpoint', () => {
- it('should set endpoint', () => {
- const expectedState = { ...mockState, endpoint: 'foo' };
- mutations[types.SET_ENDPOINT](mockState, 'foo');
-
- expect(mockState.endpoint).toEqual(expectedState.endpoint);
- });
- });
-
describe('set reports', () => {
it('should set testReports', () => {
const expectedState = { ...mockState, testReports };
@@ -45,6 +36,15 @@ describe('Mutations TestReports Store', () => {
});
});
+ describe('set summary', () => {
+ it('should set summary', () => {
+ const summary = { total_count: 1 };
+ mutations[types.SET_SUMMARY](mockState, summary);
+
+ expect(mockState.summary).toEqual(summary);
+ });
+ });
+
describe('toggle loading', () => {
it('should set to true', () => {
const expectedState = { ...mockState, isLoading: true };
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
index cc86ba6d46d..dd2b7220d7c 100644
--- a/spec/frontend/pipelines/test_reports/test_reports_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -1,9 +1,12 @@
import Vuex from 'vuex';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import { getJSONFixture } from 'helpers/fixtures';
import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
import * as actions from '~/pipelines/stores/test_reports/actions';
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
describe('Test reports app', () => {
let wrapper;
let store;
@@ -22,11 +25,15 @@ describe('Test reports app', () => {
testReports,
...state,
},
- actions,
+ actions: {
+ ...actions,
+ fetchSummary: () => {},
+ },
});
wrapper = shallowMount(TestReports, {
store,
+ localVue,
});
};
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
index a5b093cf769..5d448bcb439 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -1,11 +1,14 @@
import Vuex from 'vuex';
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
import { getJSONFixture } from 'helpers/fixtures';
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
import * as getters from '~/pipelines/stores/test_reports/getters';
import { TestStatus } from '~/pipelines/constants';
import skippedTestCases from './mock_data';
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
describe('Test reports suite table', () => {
let wrapper;
let store;
@@ -32,6 +35,7 @@ describe('Test reports suite table', () => {
wrapper = shallowMount(SuiteTable, {
store,
+ localVue,
});
};
diff --git a/spec/helpers/gitlab_routing_helper_spec.rb b/spec/helpers/gitlab_routing_helper_spec.rb
index 99c3fad660b..bd48fc7cee2 100644
--- a/spec/helpers/gitlab_routing_helper_spec.rb
+++ b/spec/helpers/gitlab_routing_helper_spec.rb
@@ -181,6 +181,23 @@ RSpec.describe GitlabRoutingHelper do
end
end
+ describe '#gitlab_raw_snippet_blob_path' do
+ let(:ref) { 'test-ref' }
+
+ it_behaves_like 'snippet blob raw path' do
+ subject { gitlab_raw_snippet_blob_path(blob, ref) }
+ end
+
+ context 'without a ref' do
+ let(:blob) { personal_snippet.blobs.first }
+ let(:ref) { blob.repository.root_ref }
+
+ it 'uses the root ref' do
+ expect(gitlab_raw_snippet_blob_path(blob)).to eq("/-/snippets/#{personal_snippet.id}/raw/#{ref}/#{blob.path}")
+ end
+ end
+ end
+
describe '#gitlab_raw_snippet_url' do
it 'returns the raw personal snippet url' do
expect(gitlab_raw_snippet_url(personal_snippet)).to eq("http://test.host/snippets/#{personal_snippet.id}/raw")
diff --git a/spec/lib/gitlab/class_attributes_spec.rb b/spec/lib/gitlab/class_attributes_spec.rb
deleted file mode 100644
index f8766f20495..00000000000
--- a/spec/lib/gitlab/class_attributes_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-# frozen_string_literal: true
-require 'fast_spec_helper'
-
-RSpec.describe Gitlab::ClassAttributes do
- let(:klass) do
- Class.new do
- include Gitlab::ClassAttributes
-
- def self.get_attribute(name)
- get_class_attribute(name)
- end
-
- def self.set_attribute(name, value)
- class_attributes[name] = value
- end
- end
- end
-
- let(:subclass) { Class.new(klass) }
-
- describe ".get_class_attribute" do
- it "returns values set on the class" do
- klass.set_attribute(:foo, :bar)
-
- expect(klass.get_attribute(:foo)).to eq(:bar)
- end
-
- it "returns values set on a superclass" do
- klass.set_attribute(:foo, :bar)
-
- expect(subclass.get_attribute(:foo)).to eq(:bar)
- end
-
- it "returns values from the subclass over attributes from a superclass" do
- klass.set_attribute(:foo, :baz)
- subclass.set_attribute(:foo, :bar)
-
- expect(subclass.get_attribute(:foo)).to eq(:bar)
- end
- end
-end
diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb
index 272666fceac..24f547a2e92 100644
--- a/spec/lib/gitlab/danger/roulette_spec.rb
+++ b/spec/lib/gitlab/danger/roulette_spec.rb
@@ -155,8 +155,8 @@ RSpec.describe Gitlab::Danger::Roulette do
context 'when change contains QA category' do
let(:categories) { [:qa] }
- it 'assigns QA reviewer and sets optional QA maintainer' do
- expect(spins).to contain_exactly(matching_spin(:qa, reviewer: software_engineer_in_test, optional: :maintainer))
+ it 'assigns QA reviewer' do
+ expect(spins).to contain_exactly(matching_spin(:qa, reviewer: software_engineer_in_test))
end
end
@@ -171,8 +171,8 @@ RSpec.describe Gitlab::Danger::Roulette do
context 'when change contains test category' do
let(:categories) { [:test] }
- it 'assigns corresponding SET and sets optional test maintainer' do
- expect(spins).to contain_exactly(matching_spin(:test, reviewer: software_engineer_in_test, optional: :maintainer))
+ it 'assigns corresponding SET' do
+ expect(spins).to contain_exactly(matching_spin(:test, reviewer: software_engineer_in_test))
end
end
end
diff --git a/spec/lib/gitlab/metrics/web_transaction_spec.rb b/spec/lib/gitlab/metrics/web_transaction_spec.rb
index 389a0a04904..6ceba186660 100644
--- a/spec/lib/gitlab/metrics/web_transaction_spec.rb
+++ b/spec/lib/gitlab/metrics/web_transaction_spec.rb
@@ -70,9 +70,6 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
end
describe '#labels' do
- let(:request) { double(:request, format: double(:format, ref: :html)) }
- let(:controller_class) { double(:controller_class, name: 'TestController') }
-
context 'when request goes to Grape endpoint' do
before do
route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)')
@@ -80,9 +77,8 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
env['api.endpoint'] = endpoint
end
-
it 'provides labels with the method and path of the route in the grape endpoint' do
- expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive', feature_category: '' })
+ expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive' })
end
it 'does not provide labels if route infos are missing' do
@@ -96,21 +92,24 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
end
context 'when request goes to ActionController' do
+ let(:request) { double(:request, format: double(:format, ref: :html)) }
+
before do
- controller = double(:controller, class: controller_class, action_name: 'show', request: request)
+ klass = double(:klass, name: 'TestController')
+ controller = double(:controller, class: klass, action_name: 'show', request: request)
env['action_controller.instance'] = controller
end
it 'tags a transaction with the name and action of a controller' do
- expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: '' })
+ expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' })
end
context 'when the request content type is not :html' do
let(:request) { double(:request, format: double(:format, ref: :json)) }
it 'appends the mime type to the transaction action' do
- expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json', feature_category: '' })
+ expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json' })
end
end
@@ -118,31 +117,11 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
let(:request) { double(:request, format: double(:format, ref: 'http://example.com')) }
it 'does not append the MIME type to the transaction action' do
- expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: '' })
- end
- end
-
- context 'when the feature category is known' do
- it 'includes it in the feature category label' do
- expect(controller_class).to receive(:feature_category_for_action).with('show').and_return(:source_code_management)
- expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: "source_code_management" })
+ expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' })
end
end
end
- it 'returns the same labels for API and controller requests' do
- route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)')
- endpoint = double(:endpoint, route: route)
- api_env = { 'api.endpoint' => endpoint }
- api_labels = described_class.new(api_env).labels
-
- controller = double(:controller, class: controller_class, action_name: 'show', request: request)
- controller_env = { 'action_controller.instance' => controller }
- controller_labels = described_class.new(controller_env).labels
-
- expect(api_labels.keys).to contain_exactly(*controller_labels.keys)
- end
-
it 'returns no labels when no route information is present in env' do
expect(transaction.labels).to eq({})
end
diff --git a/spec/models/namespace/root_storage_statistics_spec.rb b/spec/models/namespace/root_storage_statistics_spec.rb
index 9d516dc5ec2..ce6f875ee09 100644
--- a/spec/models/namespace/root_storage_statistics_spec.rb
+++ b/spec/models/namespace/root_storage_statistics_spec.rb
@@ -70,7 +70,16 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
end
end
+ shared_examples 'does not include personal snippets' do
+ specify do
+ expect(root_storage_statistics).not_to receive(:from_personal_snippets)
+
+ root_storage_statistics.recalculate!
+ end
+ end
+
it_behaves_like 'data refresh'
+ it_behaves_like 'does not include personal snippets'
context 'with subgroups' do
let(:subgroup1) { create(:group, parent: namespace)}
@@ -80,12 +89,45 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
let(:project2) { create(:project, namespace: subgroup2) }
it_behaves_like 'data refresh'
+ it_behaves_like 'does not include personal snippets'
end
context 'with a personal namespace' do
- let(:namespace) { create(:user).namespace }
+ let_it_be(:user) { create(:user) }
+ let(:namespace) { user.namespace }
it_behaves_like 'data refresh'
+
+ context 'when user has personal snippets' do
+ let(:total_project_snippets_size) { stat1.snippets_size + stat2.snippets_size }
+
+ it 'aggregates personal and project snippets size' do
+ # This is just a a snippet authored by other user
+ # to ensure we only pick snippets from the namespace
+ # user
+ create(:personal_snippet, :repository).statistics.refresh!
+
+ snippets = create_list(:personal_snippet, 3, :repository, author: user)
+ snippets.each { |s| s.statistics.refresh! }
+
+ total_personal_snippets_size = snippets.map { |s| s.statistics.repository_size }.sum
+
+ root_storage_statistics.recalculate!
+
+ expect(root_storage_statistics.snippets_size).to eq(total_personal_snippets_size + total_project_snippets_size)
+ end
+
+ context 'when personal snippets do not have statistics' do
+ it 'does not raise any error' do
+ snippets = create_list(:personal_snippet, 2, :repository, author: user)
+ snippets.last.statistics.refresh!
+
+ root_storage_statistics.recalculate!
+
+ expect(root_storage_statistics.snippets_size).to eq(total_project_snippets_size + snippets.last.statistics.repository_size)
+ end
+ end
+ end
end
end
end
diff --git a/spec/presenters/snippet_blob_presenter_spec.rb b/spec/presenters/snippet_blob_presenter_spec.rb
index 28c86468c78..7464c0ac15b 100644
--- a/spec/presenters/snippet_blob_presenter_spec.rb
+++ b/spec/presenters/snippet_blob_presenter_spec.rb
@@ -109,22 +109,38 @@ RSpec.describe SnippetBlobPresenter do
end
describe '#raw_path' do
- subject { described_class.new(snippet.blob).raw_path }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:personal_snippet) { create(:personal_snippet, :repository, author: user) }
+ let_it_be(:project_snippet) { create(:project_snippet, :repository, project: project, author: user) }
- context 'with ProjectSnippet' do
- let!(:project) { create(:project) }
- let(:snippet) { create(:project_snippet, project: project) }
+ before do
+ project.add_developer(user)
+ end
+
+ subject { described_class.new(snippet.blobs.first, current_user: user).raw_path }
- it 'returns the raw path' do
- expect(subject).to eq "/#{snippet.project.full_path}/snippets/#{snippet.id}/raw"
+ it_behaves_like 'snippet blob raw path'
+
+ context 'with snippet_multiple_files feature disabled' do
+ before do
+ stub_feature_flags(snippet_multiple_files: false)
end
- end
- context 'with PersonalSnippet' do
- let(:snippet) { create(:personal_snippet) }
+ context 'with ProjectSnippet' do
+ let(:snippet) { project_snippet }
- it 'returns the raw path' do
- expect(subject).to eq "/snippets/#{snippet.id}/raw"
+ it 'returns the raw path' do
+ expect(subject).to eq "/#{snippet.project.full_path}/snippets/#{snippet.id}/raw"
+ end
+ end
+
+ context 'with PersonalSnippet' do
+ let(:snippet) { personal_snippet }
+
+ it 'returns the raw path' do
+ expect(subject).to eq "/snippets/#{snippet.id}/raw"
+ end
end
end
end
diff --git a/spec/requests/api/import_bitbucket_server_spec.rb b/spec/requests/api/import_bitbucket_server_spec.rb
new file mode 100644
index 00000000000..be5afdfab61
--- /dev/null
+++ b/spec/requests/api/import_bitbucket_server_spec.rb
@@ -0,0 +1,214 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::ImportBitbucketServer do
+ let(:base_uri) { "https://test:7990" }
+ let(:user) { create(:user) }
+ let(:token) { "asdasd12345" }
+ let(:secret) { "sekrettt" }
+ let(:project_key) { 'TES' }
+ let(:repo_slug) { 'vim' }
+ let(:repo) { { name: 'vim' } }
+
+ describe "POST /import/bitbucket_server" do
+ context 'with no optional parameters' do
+ let_it_be(:project) { create(:project) }
+ let(:client) { double(BitbucketServer::Client) }
+
+ before do
+ Grape::Endpoint.before_each do |endpoint|
+ allow(endpoint).to receive(:client).and_return(client.as_null_object)
+ allow(client).to receive(:repo).with(project_key, repo_slug).and_return(double(name: repo_slug))
+ end
+ end
+
+ after do
+ Grape::Endpoint.before_each nil
+ end
+
+ it 'returns 201 response when the project is imported successfully' do
+ allow(Gitlab::BitbucketServerImport::ProjectCreator)
+ .to receive(:new).with(project_key, repo_slug, anything, repo_slug, user.namespace, user, anything)
+ .and_return(double(execute: project))
+
+ post api("/import/bitbucket_server", user), params: {
+ bitbucket_server_url: base_uri,
+ bitbucket_server_username: user,
+ personal_access_token: token,
+ bitbucket_server_project: project_key,
+ bitbucket_server_repo: repo_slug
+ }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to be_a Hash
+ expect(json_response['name']).to eq(project.name)
+ end
+ end
+
+ context 'with a new project name' do
+ let_it_be(:project) { create(:project, name: 'new-name') }
+ let(:client) { instance_double(BitbucketServer::Client) }
+
+ before do
+ Grape::Endpoint.before_each do |endpoint|
+ allow(endpoint).to receive(:client).and_return(client)
+ allow(client).to receive(:repo).with(project_key, repo_slug).and_return(double(name: repo_slug))
+ end
+ end
+
+ after do
+ Grape::Endpoint.before_each nil
+ end
+
+ it 'returns 201 response when the project is imported successfully with a new project name' do
+ allow(Gitlab::BitbucketServerImport::ProjectCreator)
+ .to receive(:new).with(project_key, repo_slug, anything, project.name, user.namespace, user, anything)
+ .and_return(double(execute: project))
+
+ post api("/import/bitbucket_server", user), params: {
+ bitbucket_server_url: base_uri,
+ bitbucket_server_username: user,
+ personal_access_token: token,
+ bitbucket_server_project: project_key,
+ bitbucket_server_repo: repo_slug,
+ new_name: 'new-name'
+ }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to be_a Hash
+ expect(json_response['name']).to eq('new-name')
+ end
+ end
+
+ context 'with an invalid URL' do
+ let_it_be(:project) { create(:project, name: 'new-name') }
+ let(:client) { instance_double(BitbucketServer::Client) }
+
+ before do
+ Grape::Endpoint.before_each do |endpoint|
+ allow(endpoint).to receive(:client).and_return(client)
+ allow(client).to receive(:repo).with(project_key, repo_slug).and_return(double(name: repo_slug))
+ end
+ end
+
+ after do
+ Grape::Endpoint.before_each nil
+ end
+
+ it 'returns 400 response due to a blcoked URL' do
+ allow(Gitlab::BitbucketServerImport::ProjectCreator)
+ .to receive(:new).with(project_key, repo_slug, anything, project.name, user.namespace, user, anything)
+ .and_return(double(execute: project))
+
+ allow(Gitlab::UrlBlocker)
+ .to receive(:blocked_url?)
+ .and_return(true)
+ post api("/import/bitbucket_server", user), params: {
+ bitbucket_server_url: base_uri,
+ bitbucket_server_username: user,
+ personal_access_token: token,
+ bitbucket_server_project: project_key,
+ bitbucket_server_repo: repo_slug,
+ new_name: 'new-name'
+ }
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'with a new namespace' do
+ let(:bitbucket_client) { instance_double(BitbucketServer::Client) }
+
+ before do
+ Grape::Endpoint.before_each do |endpoint|
+ allow(endpoint).to receive(:client).and_return(bitbucket_client)
+ repo = double(name: repo_slug, full_path: "/other-namespace/#{repo_slug}")
+ allow(bitbucket_client).to receive(:repo).with(project_key, repo_slug).and_return(repo)
+ end
+ end
+
+ after do
+ Grape::Endpoint.before_each nil
+ end
+
+ it 'returns 201 response when the project is imported successfully to a new namespace' do
+ allow(Gitlab::BitbucketServerImport::ProjectCreator)
+ .to receive(:new).with(project_key, repo_slug, anything, repo_slug, an_instance_of(Group), user, anything)
+ .and_return(double(execute: create(:project, name: repo_slug)))
+
+ post api("/import/bitbucket_server", user), params: {
+ bitbucket_server_url: base_uri,
+ bitbucket_server_username: user,
+ personal_access_token: token,
+ bitbucket_server_project: project_key,
+ bitbucket_server_repo: repo_slug,
+ new_namespace: 'new-namespace'
+ }
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response).to be_a Hash
+ expect(json_response['full_path']).not_to eq("/#{user.namespace}/#{repo_slug}")
+ end
+ end
+
+ context 'with a private inaccessible namespace' do
+ let(:bitbucket_client) { instance_double(BitbucketServer::Client) }
+ let(:project) { create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim', namespace: 'private-group/vim') }
+
+ before do
+ Grape::Endpoint.before_each do |endpoint|
+ allow(endpoint).to receive(:client).and_return(bitbucket_client)
+ repo = double(name: repo_slug, full_path: "/private-group/#{repo_slug}")
+ allow(bitbucket_client).to receive(:repo).with(project_key, repo_slug).and_return(repo)
+ end
+ end
+
+ after do
+ Grape::Endpoint.before_each nil
+ end
+
+ it 'returns 401 response when user can not create projects in the chosen namespace' do
+ allow(Gitlab::BitbucketServerImport::ProjectCreator)
+ .to receive(:new).with(project_key, repo_slug, anything, repo_slug, an_instance_of(Group), user, anything)
+ .and_return(double(execute: build(:project)))
+
+ other_namespace = create(:group, :private, name: 'private-group')
+
+ post api("/import/bitbucket_server", user), params: {
+ bitbucket_server_url: base_uri,
+ bitbucket_server_username: user,
+ personal_access_token: token,
+ bitbucket_server_project: project_key,
+ bitbucket_server_repo: repo_slug,
+ new_namespace: other_namespace.name
+ }
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+
+ context 'with an inaccessible bitbucket server instance' do
+ let(:bitbucket_client) { instance_double(BitbucketServer::Client) }
+ let(:project) { create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim', namespace: 'private-group/vim') }
+
+ before do
+ Grape::Endpoint.before_each do |endpoint|
+ allow(endpoint).to receive(:client).and_return(bitbucket_client)
+ allow(bitbucket_client).to receive(:repo).with(project_key, repo_slug).and_raise(::BitbucketServer::Connection::ConnectionError)
+ end
+ end
+
+ it 'raises a connection error' do
+ post api("/import/bitbucket_server", user), params: {
+ bitbucket_server_url: base_uri,
+ bitbucket_server_username: user,
+ personal_access_token: token,
+ bitbucket_server_project: project_key,
+ bitbucket_server_repo: repo_slug,
+ new_namespace: 'new-namespace'
+ }
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+ end
+end
diff --git a/spec/services/import/bitbucket_server_service_spec.rb b/spec/services/import/bitbucket_server_service_spec.rb
new file mode 100644
index 00000000000..c548e87b040
--- /dev/null
+++ b/spec/services/import/bitbucket_server_service_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Import::BitbucketServerService do
+ let_it_be(:user) { create(:user) }
+ let(:base_uri) { "https://test:7990" }
+ let(:token) { "asdasd12345" }
+ let(:secret) { "sekrettt" }
+ let(:project_key) { 'TES' }
+ let(:repo_slug) { 'vim' }
+ let(:repo) do
+ {
+ name: 'vim',
+ description: 'test',
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC,
+ browse_url: 'http://repo.com/repo/repo',
+ clone_url: 'http://repo.com/repo/repo.git'
+ }
+ end
+
+ let(:client) { double(BitbucketServer::Client) }
+
+ let(:credentials) { { base_uri: base_uri, user: user, password: token } }
+ let(:params) { { bitbucket_server_url: base_uri, bitbucket_server_username: user, personal_access_token: token, bitbucket_server_project: project_key, bitbucket_server_repo: repo_slug } }
+
+ subject { described_class.new(client, user, params) }
+
+ before do
+ allow(subject).to receive(:authorized?).and_return(true)
+ end
+
+ context 'when no repo is found' do
+ before do
+ allow(subject).to receive(:authorized?).and_return(true)
+ allow(client).to receive(:repo).and_return(nil)
+ end
+
+ it 'returns an error' do
+ result = subject.execute(credentials)
+
+ expect(result).to include(
+ message: "Project #{project_key}/#{repo_slug} could not be found",
+ status: :error,
+ http_status: :unprocessable_entity
+ )
+ end
+ end
+
+ context 'when user is unauthorized' do
+ before do
+ allow(subject).to receive(:authorized?).and_return(false)
+ end
+
+ it 'returns an error' do
+ result = subject.execute(credentials)
+
+ expect(result).to include(
+ message: "You don't have permissions to create this project",
+ status: :error,
+ http_status: :unauthorized
+ )
+ end
+ end
+
+ context 'verify url' do
+ shared_examples 'denies local request' do
+ before do
+ allow(client).to receive(:repo).with(project_key, repo_slug).and_return(double(repo))
+ end
+
+ it 'does not allow requests' do
+ result = subject.execute(credentials)
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to include("Invalid URL:")
+ end
+ end
+
+ context 'when host is localhost' do
+ before do
+ allow(subject).to receive(:url).and_return('https://localhost:3000')
+ end
+
+ include_examples 'denies local request'
+ end
+
+ context 'when host is on local network' do
+ before do
+ allow(subject).to receive(:url).and_return('https://192.168.0.191')
+ end
+
+ include_examples 'denies local request'
+ end
+
+ context 'when host is ftp protocol' do
+ before do
+ allow(subject).to receive(:url).and_return('ftp://testing')
+ end
+
+ include_examples 'denies local request'
+ end
+ end
+
+ it 'raises an exception for unknown error causes' do
+ exception = StandardError.new('Not Implemented')
+
+ allow(client).to receive(:repo).and_raise(exception)
+
+ expect(Gitlab::Import::Logger).not_to receive(:error)
+
+ expect { subject.execute(credentials) }.to raise_error(exception)
+ end
+end
diff --git a/spec/support/shared_examples/snippet_blob_shared_examples.rb b/spec/support/shared_examples/snippet_blob_shared_examples.rb
new file mode 100644
index 00000000000..ba97688d017
--- /dev/null
+++ b/spec/support/shared_examples/snippet_blob_shared_examples.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.shared_examples 'snippet blob raw path' do
+ let(:blob) { snippet.blobs.first }
+ let(:ref) { blob.repository.root_ref }
+
+ context 'for PersonalSnippets' do
+ let(:snippet) { personal_snippet }
+
+ it 'returns the raw personal snippet blob path' do
+ expect(subject).to eq("/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}")
+ end
+ end
+
+ context 'for ProjectSnippets' do
+ let(:snippet) { project_snippet }
+
+ it 'returns the raw project snippet blob path' do
+ expect(subject).to eq("/#{snippet.project.full_path}/-/snippets/#{snippet.id}/raw/#{ref}/#{blob.path}")
+ end
+ end
+end