Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-07-06 18:09:13 +00:00
parent ce06ce825b
commit 691ed55a05
63 changed files with 1120 additions and 628 deletions

View File

@ -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();
},

View File

@ -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,

View File

@ -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,

View File

@ -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();
};

View File

@ -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.'));
})

View File

@ -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),
});

View File

@ -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';

View File

@ -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 });
},

View File

@ -1,6 +1,8 @@
export default () => ({
endpoint: '',
export default ({ fullReportEndpoint = '', summaryEndpoint = '' }) => ({
summaryEndpoint,
fullReportEndpoint,
testReports: {},
selectedSuite: {},
summary: {},
isLoading: false,
});

View File

@ -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?

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -0,0 +1,5 @@
---
title: 'Resolve Feature proposal: API for import from BitBucket Server'
merge_request: 33097
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Include personal snippets size in RootStorageStatistics
merge_request: 35984
author:
type: changed

View File

@ -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, [])

View File

@ -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"
}'
```

View File

@ -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).

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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*) | | | | | |

View File

@ -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)** |

View File

@ -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.

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)
end
after do
after(:all) do
remove_repository(project)
end
it 'branches/new_branch.html' do
get :new, params: {
namespace_id: project.namespace.to_param,
project_id: project
}
describe Projects::BranchesController, '(JavaScript fixtures)', type: :controller do
render_views
expect(response).to be_successful
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
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
end
end
end

View File

@ -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
get :show, params: params
describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do
render_views
expect(response).to be_successful
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
it 'api/commits/commit.json' do
get api("/projects/#{project.id}/repository/commits/#{commit.id}", user)
expect(response).to be_successful
end
end
end

View File

@ -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

View File

@ -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' }],

View File

@ -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 };

View File

@ -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,
});
};

View File

@ -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,
});
};

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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,29 +117,9 @@ 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: '' })
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' })
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" })
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

View File

@ -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

View File

@ -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) }
it 'returns the raw path' do
expect(subject).to eq "/#{snippet.project.full_path}/snippets/#{snippet.id}/raw"
end
before do
project.add_developer(user)
end
context 'with PersonalSnippet' do
let(:snippet) { create(:personal_snippet) }
subject { described_class.new(snippet.blobs.first, current_user: user).raw_path }
it 'returns the raw path' do
expect(subject).to eq "/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
context 'with ProjectSnippet' do
let(:snippet) { project_snippet }
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

View File

@ -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

View File

@ -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

View File

@ -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