Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
ce06ce825b
commit
691ed55a05
|
@ -4,7 +4,6 @@ import { GlLoadingIcon } from '@gitlab/ui';
|
||||||
import TestSuiteTable from './test_suite_table.vue';
|
import TestSuiteTable from './test_suite_table.vue';
|
||||||
import TestSummary from './test_summary.vue';
|
import TestSummary from './test_summary.vue';
|
||||||
import TestSummaryTable from './test_summary_table.vue';
|
import TestSummaryTable from './test_summary_table.vue';
|
||||||
import store from '~/pipelines/stores/test_reports';
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'TestReports',
|
name: 'TestReports',
|
||||||
|
@ -14,7 +13,6 @@ export default {
|
||||||
TestSummary,
|
TestSummary,
|
||||||
TestSummaryTable,
|
TestSummaryTable,
|
||||||
},
|
},
|
||||||
store,
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['isLoading', 'selectedSuite', 'testReports']),
|
...mapState(['isLoading', 'selectedSuite', 'testReports']),
|
||||||
showSuite() {
|
showSuite() {
|
||||||
|
@ -25,8 +23,11 @@ export default {
|
||||||
return testSuites.length > 0;
|
return testSuites.length > 0;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.fetchSummary();
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['setSelectedSuite', 'removeSelectedSuite']),
|
...mapActions(['fetchSummary', 'setSelectedSuite', 'removeSelectedSuite']),
|
||||||
summaryBackClick() {
|
summaryBackClick() {
|
||||||
this.removeSelectedSuite();
|
this.removeSelectedSuite();
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import Icon from '~/vue_shared/components/icon.vue';
|
import Icon from '~/vue_shared/components/icon.vue';
|
||||||
import store from '~/pipelines/stores/test_reports';
|
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
import { GlTooltipDirective } from '@gitlab/ui';
|
import { GlTooltipDirective } from '@gitlab/ui';
|
||||||
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
|
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
|
||||||
|
@ -15,7 +14,6 @@ export default {
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
},
|
},
|
||||||
store,
|
|
||||||
props: {
|
props: {
|
||||||
heading: {
|
heading: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import { s__ } from '~/locale';
|
import { s__ } from '~/locale';
|
||||||
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
|
||||||
import store from '~/pipelines/stores/test_reports';
|
|
||||||
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
|
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -14,7 +13,6 @@ export default {
|
||||||
directives: {
|
directives: {
|
||||||
GlTooltip: GlTooltipDirective,
|
GlTooltip: GlTooltipDirective,
|
||||||
},
|
},
|
||||||
store,
|
|
||||||
props: {
|
props: {
|
||||||
heading: {
|
heading: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
|
@ -10,8 +10,7 @@ import PipelinesMediator from './pipeline_details_mediator';
|
||||||
import pipelineHeader from './components/header_component.vue';
|
import pipelineHeader from './components/header_component.vue';
|
||||||
import eventHub from './event_hub';
|
import eventHub from './event_hub';
|
||||||
import TestReports from './components/test_reports/test_reports.vue';
|
import TestReports from './components/test_reports/test_reports.vue';
|
||||||
import testReportsStore from './stores/test_reports';
|
import createTestReportsStore from './stores/test_reports';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
|
||||||
|
|
||||||
Vue.use(Translate);
|
Vue.use(Translate);
|
||||||
|
|
||||||
|
@ -93,15 +92,11 @@ const createPipelineHeaderApp = mediator => {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPipelinesTabs = dataset => {
|
const createPipelinesTabs = testReportsStore => {
|
||||||
const tabsElement = document.querySelector('.pipelines-tabs');
|
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(
|
const isTestTabActive = Boolean(
|
||||||
document.querySelector('.pipelines-tabs > li > a.test-tab.active'),
|
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
|
// eslint-disable-next-line no-new
|
||||||
new Vue({
|
new Vue({
|
||||||
el: '#js-pipeline-tests-detail',
|
el: '#js-pipeline-tests-detail',
|
||||||
components: {
|
components: {
|
||||||
TestReports,
|
TestReports,
|
||||||
},
|
},
|
||||||
|
store: testReportsStore,
|
||||||
render(createElement) {
|
render(createElement) {
|
||||||
return createElement('test-reports');
|
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 = () => {
|
const createDagApp = () => {
|
||||||
|
@ -178,7 +170,6 @@ export default () => {
|
||||||
|
|
||||||
createPipelinesDetailApp(mediator);
|
createPipelinesDetailApp(mediator);
|
||||||
createPipelineHeaderApp(mediator);
|
createPipelineHeaderApp(mediator);
|
||||||
createPipelinesTabs(dataset);
|
createTestDetails(dataset.testReportEndpoint, dataset.testReportsCountEndpoint);
|
||||||
createTestDetails(dataset.testReportsCountEndpoint);
|
|
||||||
createDagApp();
|
createDagApp();
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,17 +3,30 @@ import * as types from './mutation_types';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import { s__ } from '~/locale';
|
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');
|
dispatch('toggleLoading');
|
||||||
|
|
||||||
return axios
|
return axios
|
||||||
.get(state.endpoint)
|
.get(state.fullReportEndpoint)
|
||||||
.then(response => {
|
.then(({ data }) => commit(types.SET_REPORTS, data))
|
||||||
const { data } = response;
|
|
||||||
commit(types.SET_REPORTS, data);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
createFlash(s__('TestReports|There was an error fetching the test reports.'));
|
createFlash(s__('TestReports|There was an error fetching the test reports.'));
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,9 +7,10 @@ import mutations from './mutations';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default initialState =>
|
||||||
actions,
|
new Vuex.Store({
|
||||||
getters,
|
actions,
|
||||||
mutations,
|
getters,
|
||||||
state,
|
mutations,
|
||||||
});
|
state: state(initialState),
|
||||||
|
});
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const SET_ENDPOINT = 'SET_ENDPOINT';
|
|
||||||
export const SET_REPORTS = 'SET_REPORTS';
|
export const SET_REPORTS = 'SET_REPORTS';
|
||||||
export const SET_SELECTED_SUITE = 'SET_SELECTED_SUITE';
|
export const SET_SELECTED_SUITE = 'SET_SELECTED_SUITE';
|
||||||
|
export const SET_SUMMARY = 'SET_SUMMARY';
|
||||||
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
|
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
|
||||||
|
|
|
@ -1,10 +1,6 @@
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
[types.SET_ENDPOINT](state, endpoint) {
|
|
||||||
Object.assign(state, { endpoint });
|
|
||||||
},
|
|
||||||
|
|
||||||
[types.SET_REPORTS](state, testReports) {
|
[types.SET_REPORTS](state, testReports) {
|
||||||
Object.assign(state, { testReports });
|
Object.assign(state, { testReports });
|
||||||
},
|
},
|
||||||
|
@ -13,6 +9,10 @@ export default {
|
||||||
Object.assign(state, { selectedSuite });
|
Object.assign(state, { selectedSuite });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[types.SET_SUMMARY](state, summary) {
|
||||||
|
Object.assign(state, { summary });
|
||||||
|
},
|
||||||
|
|
||||||
[types.TOGGLE_LOADING](state) {
|
[types.TOGGLE_LOADING](state) {
|
||||||
Object.assign(state, { isLoading: !state.isLoading });
|
Object.assign(state, { isLoading: !state.isLoading });
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
export default () => ({
|
export default ({ fullReportEndpoint = '', summaryEndpoint = '' }) => ({
|
||||||
endpoint: '',
|
summaryEndpoint,
|
||||||
|
fullReportEndpoint,
|
||||||
testReports: {},
|
testReports: {},
|
||||||
selectedSuite: {},
|
selectedSuite: {},
|
||||||
|
summary: {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,7 +22,6 @@ class ApplicationController < ActionController::Base
|
||||||
include Impersonation
|
include Impersonation
|
||||||
include Gitlab::Logging::CloudflareHelper
|
include Gitlab::Logging::CloudflareHelper
|
||||||
include Gitlab::Utils::StrongMemoize
|
include Gitlab::Utils::StrongMemoize
|
||||||
include ControllerWithFeatureCategory
|
|
||||||
|
|
||||||
before_action :authenticate_user!, except: [:route_not_found]
|
before_action :authenticate_user!, except: [:route_not_found]
|
||||||
before_action :enforce_terms!, if: :should_enforce_terms?
|
before_action :enforce_terms!, if: :should_enforce_terms?
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
return render json: { errors: _("Project %{project_repo} could not be found") % { project_repo: "#{@project_key}/#{@repo_slug}" } }, status: :unprocessable_entity
|
||||||
end
|
end
|
||||||
|
|
||||||
project_name = params[:new_name].presence || repo.name
|
result = Import::BitbucketServerService.new(client, current_user, params).execute(credentials)
|
||||||
namespace_path = params[:new_namespace].presence || current_user.username
|
|
||||||
target_namespace = find_or_create_namespace(namespace_path, current_user)
|
|
||||||
|
|
||||||
if current_user.can?(:create_projects, target_namespace)
|
if result[:status] == :success
|
||||||
project = Gitlab::BitbucketServerImport::ProjectCreator.new(@project_key, @repo_slug, repo, project_name, target_namespace, current_user, credentials).execute
|
render json: ProjectSerializer.new.represent(result[:project], serializer: :import)
|
||||||
|
|
||||||
if project.persisted?
|
|
||||||
render json: ProjectSerializer.new.represent(project, serializer: :import)
|
|
||||||
else
|
|
||||||
render json: { errors: project_save_error(project) }, status: :unprocessable_entity
|
|
||||||
end
|
|
||||||
else
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
def configure
|
def configure
|
||||||
session[personal_access_token_key] = params[:personal_access_token]
|
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]
|
session[bitbucket_server_url_key] = params[:bitbucket_server_url]
|
||||||
|
|
||||||
redirect_to status_import_bitbucket_server_path
|
redirect_to status_import_bitbucket_server_path
|
||||||
|
@ -127,8 +119,8 @@ class Import::BitbucketServerController < Import::BaseController
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_import_params
|
def validate_import_params
|
||||||
@project_key = params[:project]
|
@project_key = params[:bitbucketServerProject]
|
||||||
@repo_slug = params[:repository]
|
@repo_slug = params[:bitbucketServerRepo]
|
||||||
|
|
||||||
return render_validation_error('Missing project key') unless @project_key.present? && @repo_slug.present?
|
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?
|
return render_validation_error('Missing repository slug') unless @repo_slug.present?
|
||||||
|
|
|
@ -45,13 +45,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
||||||
|
|
||||||
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
|
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
|
def index
|
||||||
@merge_requests = @issuables
|
@merge_requests = @issuables
|
||||||
|
|
||||||
|
|
|
@ -285,6 +285,22 @@ module GitlabRoutingHelper
|
||||||
end
|
end
|
||||||
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)
|
def gitlab_snippet_notes_path(snippet, *args)
|
||||||
new_args = snippet_query_params(snippet, *args)
|
new_args = snippet_query_params(snippet, *args)
|
||||||
snippet_notes_path(snippet, *new_args)
|
snippet_notes_path(snippet, *new_args)
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Namespace::RootStorageStatistics < ApplicationRecord
|
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
|
self.primary_key = :namespace_id
|
||||||
|
|
||||||
|
@ -13,11 +14,15 @@ class Namespace::RootStorageStatistics < ApplicationRecord
|
||||||
delegate :all_projects, to: :namespace
|
delegate :all_projects, to: :namespace
|
||||||
|
|
||||||
def recalculate!
|
def recalculate!
|
||||||
update!(attributes_from_project_statistics)
|
update!(merged_attributes)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def merged_attributes
|
||||||
|
attributes_from_project_statistics.merge!(attributes_from_personal_snippets) { |key, v1, v2| v1 + v2 }
|
||||||
|
end
|
||||||
|
|
||||||
def attributes_from_project_statistics
|
def attributes_from_project_statistics
|
||||||
from_project_statistics
|
from_project_statistics
|
||||||
.take
|
.take
|
||||||
|
@ -35,7 +40,21 @@ class Namespace::RootStorageStatistics < ApplicationRecord
|
||||||
'COALESCE(SUM(ps.lfs_objects_size), 0) AS lfs_objects_size',
|
'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.build_artifacts_size), 0) AS build_artifacts_size',
|
||||||
'COALESCE(SUM(ps.packages_size), 0) AS packages_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
|
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
|
end
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class SnippetBlobPresenter < BlobPresenter
|
class SnippetBlobPresenter < BlobPresenter
|
||||||
|
include GitlabRoutingHelper
|
||||||
|
|
||||||
def rich_data
|
def rich_data
|
||||||
return if blob.binary?
|
return if blob.binary?
|
||||||
return unless blob.rich_viewer
|
return unless blob.rich_viewer
|
||||||
|
@ -15,15 +17,17 @@ class SnippetBlobPresenter < BlobPresenter
|
||||||
end
|
end
|
||||||
|
|
||||||
def raw_path
|
def raw_path
|
||||||
if snippet.is_a?(ProjectSnippet)
|
return gitlab_raw_snippet_blob_path(blob) if snippet_multiple_files?
|
||||||
raw_project_snippet_path(snippet.project, snippet)
|
|
||||||
else
|
gitlab_raw_snippet_path(snippet)
|
||||||
raw_snippet_path(snippet)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def snippet_multiple_files?
|
||||||
|
blob.container.repository_exists? && Feature.enabled?(:snippet_multiple_files, current_user)
|
||||||
|
end
|
||||||
|
|
||||||
def snippet
|
def snippet
|
||||||
blob.container
|
blob.container
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -17,7 +17,7 @@
|
||||||
.form-group.row
|
.form-group.row
|
||||||
= label_tag :bitbucket_server_url, 'Username', class: 'col-form-label col-md-2'
|
= label_tag :bitbucket_server_url, 'Username', class: 'col-form-label col-md-2'
|
||||||
.col-md-4
|
.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
|
.form-group.row
|
||||||
= label_tag :personal_access_token, 'Password/Personal Access Token', class: 'col-form-label col-md-2'
|
= label_tag :personal_access_token, 'Password/Personal Access Token', class: 'col-form-label col-md-2'
|
||||||
.col-md-4
|
.col-md-4
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
= project.human_import_status_name
|
= project.human_import_status_name
|
||||||
|
|
||||||
- @repos.each do |repo|
|
- @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
|
%td
|
||||||
= sanitize(link_to(repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer'), attributes: %w(href target rel))
|
= sanitize(link_to(repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer'), attributes: %w(href target rel))
|
||||||
%td.import-target
|
%td.import-target
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
- page_title _("GitLab Import")
|
- page_title _("GitLab Import")
|
||||||
- header_title _("Projects"), root_path
|
- header_title _("Projects"), root_path
|
||||||
|
|
||||||
%h3.page-title
|
%h3.page-title.d-flex
|
||||||
= icon('gitlab')
|
.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')
|
= _('Import an exported GitLab project')
|
||||||
%hr
|
%hr
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
- if gitlab_project_import_enabled?
|
- if gitlab_project_import_enabled?
|
||||||
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
|
.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
|
= 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?
|
- if github_import_enabled?
|
||||||
%div
|
%div
|
||||||
|
@ -32,7 +33,8 @@
|
||||||
%div
|
%div
|
||||||
= link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}",
|
= 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
|
**tracking_attrs(track_label, 'click_button', 'gitlab_com') do
|
||||||
= icon('gitlab', text: 'GitLab.com')
|
= sprite_icon('tanuki')
|
||||||
|
= _("GitLab.com")
|
||||||
- unless gitlab_import_configured?
|
- unless gitlab_import_configured?
|
||||||
= render 'projects/gitlab_import_modal'
|
= render 'projects/gitlab_import_modal'
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
module WorkerAttributes
|
module WorkerAttributes
|
||||||
extend ActiveSupport::Concern
|
extend ActiveSupport::Concern
|
||||||
include Gitlab::ClassAttributes
|
|
||||||
|
|
||||||
# Resource boundaries that workers can declare through the
|
# Resource boundaries that workers can declare through the
|
||||||
# `resource_boundary` attribute
|
# `resource_boundary` attribute
|
||||||
|
@ -31,24 +30,24 @@ module WorkerAttributes
|
||||||
}.stringify_keys.freeze
|
}.stringify_keys.freeze
|
||||||
|
|
||||||
class_methods do
|
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
|
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
|
end
|
||||||
|
|
||||||
# Special case: mark this work as not associated with a feature category
|
# Special case: mark this work as not associated with a feature category
|
||||||
# this should be used for cross-cutting concerns, such as mailer workers.
|
# this should be used for cross-cutting concerns, such as mailer workers.
|
||||||
def feature_category_not_owned!
|
def feature_category_not_owned!
|
||||||
class_attributes[:feature_category] = :not_owned
|
worker_attributes[:feature_category] = :not_owned
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_feature_category
|
def get_feature_category
|
||||||
get_class_attribute(:feature_category)
|
get_worker_attribute(:feature_category)
|
||||||
end
|
end
|
||||||
|
|
||||||
def feature_category_not_owned?
|
def feature_category_not_owned?
|
||||||
get_feature_category == :not_owned
|
get_worker_attribute(:feature_category) == :not_owned
|
||||||
end
|
end
|
||||||
|
|
||||||
# This should be set to :high for jobs that need to be run
|
# This should be set to :high for jobs that need to be run
|
||||||
|
@ -62,11 +61,11 @@ module WorkerAttributes
|
||||||
def urgency(urgency)
|
def urgency(urgency)
|
||||||
raise "Invalid urgency: #{urgency}" unless VALID_URGENCIES.include?(urgency)
|
raise "Invalid urgency: #{urgency}" unless VALID_URGENCIES.include?(urgency)
|
||||||
|
|
||||||
class_attributes[:urgency] = urgency
|
worker_attributes[:urgency] = urgency
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_urgency
|
def get_urgency
|
||||||
class_attributes[:urgency] || :low
|
worker_attributes[:urgency] || :low
|
||||||
end
|
end
|
||||||
|
|
||||||
# Set this attribute on a job when it will call to services outside of the
|
# 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
|
# doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies for
|
||||||
# details
|
# details
|
||||||
def worker_has_external_dependencies!
|
def worker_has_external_dependencies!
|
||||||
class_attributes[:external_dependencies] = true
|
worker_attributes[:external_dependencies] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
# Returns a truthy value if the worker has external dependencies.
|
# Returns a truthy value if the worker has external dependencies.
|
||||||
# See doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies
|
# See doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies
|
||||||
# for details
|
# for details
|
||||||
def worker_has_external_dependencies?
|
def worker_has_external_dependencies?
|
||||||
class_attributes[:external_dependencies]
|
worker_attributes[:external_dependencies]
|
||||||
end
|
end
|
||||||
|
|
||||||
def worker_resource_boundary(boundary)
|
def worker_resource_boundary(boundary)
|
||||||
raise "Invalid boundary" unless VALID_RESOURCE_BOUNDARIES.include? boundary
|
raise "Invalid boundary" unless VALID_RESOURCE_BOUNDARIES.include? boundary
|
||||||
|
|
||||||
class_attributes[:resource_boundary] = boundary
|
worker_attributes[:resource_boundary] = boundary
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_worker_resource_boundary
|
def get_worker_resource_boundary
|
||||||
class_attributes[:resource_boundary] || :unknown
|
worker_attributes[:resource_boundary] || :unknown
|
||||||
end
|
end
|
||||||
|
|
||||||
def idempotent!
|
def idempotent!
|
||||||
class_attributes[:idempotent] = true
|
worker_attributes[:idempotent] = true
|
||||||
end
|
end
|
||||||
|
|
||||||
def idempotent?
|
def idempotent?
|
||||||
class_attributes[:idempotent]
|
worker_attributes[:idempotent]
|
||||||
end
|
end
|
||||||
|
|
||||||
def weight(value)
|
def weight(value)
|
||||||
class_attributes[:weight] = value
|
worker_attributes[:weight] = value
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_weight
|
def get_weight
|
||||||
class_attributes[:weight] ||
|
worker_attributes[:weight] ||
|
||||||
NAMESPACE_WEIGHTS[queue_namespace] ||
|
NAMESPACE_WEIGHTS[queue_namespace] ||
|
||||||
1
|
1
|
||||||
end
|
end
|
||||||
|
|
||||||
def tags(*values)
|
def tags(*values)
|
||||||
class_attributes[:tags] = values
|
worker_attributes[:tags] = values
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_tags
|
def get_tags
|
||||||
Array(class_attributes[:tags])
|
Array(worker_attributes[:tags])
|
||||||
end
|
end
|
||||||
|
|
||||||
def deduplicate(strategy, options = {})
|
def deduplicate(strategy, options = {})
|
||||||
class_attributes[:deduplication_strategy] = strategy
|
worker_attributes[:deduplication_strategy] = strategy
|
||||||
class_attributes[:deduplication_options] = options
|
worker_attributes[:deduplication_options] = options
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_deduplicate_strategy
|
def get_deduplicate_strategy
|
||||||
class_attributes[:deduplication_strategy] ||
|
worker_attributes[:deduplication_strategy] ||
|
||||||
Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DEFAULT_STRATEGY
|
Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DEFAULT_STRATEGY
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_deduplication_options
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: 'Resolve Feature proposal: API for import from BitBucket Server'
|
||||||
|
merge_request: 33097
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Include personal snippets size in RootStorageStatistics
|
||||||
|
merge_request: 35984
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -39,6 +39,10 @@ MARKDOWN
|
||||||
OPTIONAL_REVIEW_TEMPLATE = "%{role} review is optional for %{category}".freeze
|
OPTIONAL_REVIEW_TEMPLATE = "%{role} review is optional for %{category}".freeze
|
||||||
NOT_AVAILABLE_TEMPLATE = 'No %{role} available'.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)
|
def note_for_category_role(spin, role)
|
||||||
if spin.optional_role == role
|
if spin.optional_role == role
|
||||||
return OPTIONAL_REVIEW_TEMPLATE % { role: role.capitalize, category: helper.label_for_category(spin.category) }
|
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']
|
branch_name = gitlab.mr_json['source_branch']
|
||||||
|
|
||||||
roulette_spins = roulette.spin(project, categories, branch_name)
|
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, [])
|
unknown = changes.fetch(:unknown, [])
|
||||||
|
|
||||||
|
|
|
@ -29,3 +29,41 @@ Example response:
|
||||||
"full_name": "Administrator / my-repo"
|
"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"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
|
@ -325,9 +325,19 @@ tenses, words, and phrases:
|
||||||
- Avoid using the word *currently* when talking about the product or its
|
- 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
|
features. The documentation describes the product as it is, and not as it
|
||||||
will be at some indeterminate point in the future.
|
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
|
- Don't use profanity or obscenities. Doing so may negatively affect other
|
||||||
users and contributors, which is contrary to GitLab's value of
|
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:
|
- 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 *primary* and *secondary* for database and server relationships.
|
||||||
- Use *allowlist* and *denylist* to describe access control lists.
|
- 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
|
### 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 |
|
| Recommended | Used for | Replaces |
|
||||||
|:------------|:---------------------------|:---------------------------|
|
|:--------------------|:---------------------------|:---------------------------|
|
||||||
| "click" | buttons, links, menu items | "hit", "press", "select" |
|
| *click* | buttons, links, menu items | "hit", "press", "select" |
|
||||||
| "check" | checkboxes | "enable", "click", "press" |
|
| *select* or *clear* | checkboxes | "enable", "click", "press" |
|
||||||
| "select" | dropdowns | "pick" |
|
| *select* | dropdowns | "pick" |
|
||||||
| "expand" | expandable sections | "open" |
|
| *expand* | expandable sections | "open" |
|
||||||
|
|
||||||
### Other Verbs
|
### Other Verbs
|
||||||
|
|
||||||
| Recommended | Used for | Alternatives |
|
| Recommended | Used for | Replaces |
|
||||||
|:------------|:--------------------------------|:-------------------|
|
|:------------|:--------------------------------|:----------------------|
|
||||||
| "go" | making a browser go to location | "navigate", "open" |
|
| *go to* | making a browser go to location | "navigate to", "open" |
|
||||||
|
|
||||||
## GitLab versions and tiers
|
## 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. How the user can confirm they have the problem.
|
||||||
1. Steps the user can take towards resolution of 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
|
## Feature flags
|
||||||
|
|
||||||
Learn how to [document features deployed behind flags](feature_flags.md).
|
Learn how to [document features deployed behind flags](feature_flags.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
|
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.
|
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
|
### Sidebar wasn't loading for some users
|
||||||
|
|
||||||
|
|
|
@ -310,8 +310,7 @@ sudo adduser --disabled-login --gecos 'GitLab' git
|
||||||
## 6. Database
|
## 6. Database
|
||||||
|
|
||||||
NOTE: **Note:**
|
NOTE: **Note:**
|
||||||
Starting from GitLab 12.1, only PostgreSQL is supported. Because we need to make
|
Starting from GitLab 12.1, only PostgreSQL is supported. Since GitLab 13.0, we require PostgreSQL 11+.
|
||||||
use of extensions and concurrent index removal, you need at least PostgreSQL 9.2.
|
|
||||||
|
|
||||||
1. Install the database packages:
|
1. Install the database packages:
|
||||||
|
|
||||||
|
@ -426,11 +425,20 @@ cd /home/git
|
||||||
|
|
||||||
### Clone the Source
|
### Clone the Source
|
||||||
|
|
||||||
|
Clone Community Edition:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Clone GitLab repository
|
# Clone GitLab repository
|
||||||
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-foss.git -b X-Y-stable gitlab
|
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
|
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
|
version you want to install. For example, if you want to install 11.8 you would
|
||||||
use the branch name `11-8-stable`.
|
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
|
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
|
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`
|
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.
|
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.
|
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
|
### 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.
|
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.
|
||||||
|
|
|
@ -141,7 +141,7 @@ enables verbose output from Clair by setting the `CLAIR_OUTPUT` environment vari
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
include:
|
include:
|
||||||
template: Container-Scanning.gitlab-ci.yml
|
- template: Container-Scanning.gitlab-ci.yml
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
CLAIR_OUTPUT: High
|
CLAIR_OUTPUT: High
|
||||||
|
@ -184,7 +184,7 @@ specify any additional keys. For example:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
include:
|
include:
|
||||||
template: Container-Scanning.gitlab-ci.yml
|
- template: Container-Scanning.gitlab-ci.yml
|
||||||
|
|
||||||
container_scanning:
|
container_scanning:
|
||||||
variables:
|
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)
|
When overriding the template, you must use [`rules`](../../../ci/yaml/README.md#rules)
|
||||||
instead.
|
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
|
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).
|
[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
|
1. Define the allowlisted vulnerabilities in a YAML file named `vulnerability-allowlist.yml`. This must use
|
||||||
the format described in the [whitelist example file](https://github.com/arminc/clair-scanner/blob/v12/example-whitelist.yaml).
|
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 `clair-whitelist.yml` file to your project's Git repository.
|
1. Add the `vulnerability-allowlist.yml` file to your project's Git repository.
|
||||||
|
|
||||||
### Running Container Scanning in an offline environment
|
### Running Container Scanning in an offline environment
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ For example, if you remove a user from the SCIM app, SCIM removes that same user
|
||||||
## Configuring your Identity Provider
|
## Configuring your Identity Provider
|
||||||
|
|
||||||
1. Navigate to the group and click **Settings > SAML SSO**.
|
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 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. 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).
|
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:
|
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. 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. 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 **Enable SAML authentication for this group** toggle switch.
|
||||||
1. Click the **Save changes** button.
|
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.
|
- [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.
|
- [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.
|
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) |
|
| Identifier | Identifier (Entity ID) |
|
||||||
| Assertion consumer service URL | Reply URL (Assertion Consumer Service URL) |
|
| Assertion consumer service URL | Reply URL (Assertion Consumer Service URL) |
|
||||||
| GitLab single sign on URL | Sign on URL |
|
| GitLab single sign-on URL | Sign on URL |
|
||||||
| Identity provider single sign on URL | Login URL |
|
| Identity provider single sign-on URL | Login URL |
|
||||||
| Certificate fingerprint | Thumbprint |
|
| Certificate fingerprint | Thumbprint |
|
||||||
|
|
||||||
We recommend:
|
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 |
|
| GitLab Setting | Okta Field |
|
||||||
|--------------|----------------|
|
|--------------|----------------|
|
||||||
| Identifier | Audience URI |
|
| Identifier | Audience URI |
|
||||||
| Assertion consumer service URL | 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) |
|
| 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 |
|
| 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:
|
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 | Recipient |
|
||||||
| Assertion consumer service URL | ACS (Consumer) URL |
|
| Assertion consumer service URL | ACS (Consumer) URL |
|
||||||
| Assertion consumer service URL (escaped version) | ACS (Consumer) URL Validator |
|
| Assertion consumer service URL (escaped version) | ACS (Consumer) URL Validator |
|
||||||
| GitLab single sign on URL | Login URL |
|
| GitLab single sign-on URL | Login URL |
|
||||||
| Identity provider single sign on URL | SAML 2.0 Endpoint |
|
| Identity provider single sign-on URL | SAML 2.0 Endpoint |
|
||||||
|
|
||||||
Recommended `NameID` value: `OneLogin ID`.
|
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:
|
To link SAML to your existing GitLab.com account:
|
||||||
|
|
||||||
1. Sign in to your 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. Click **Authorize**.
|
||||||
1. Enter your credentials on the Identity Provider if prompted.
|
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.
|
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"
|
### 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
|
### The NameID has changed
|
||||||
|
|
||||||
|
|
|
@ -163,7 +163,7 @@ As long as [Group SAML](index.md) has been configured, prior to turning on sync,
|
||||||
- By following these steps:
|
- By following these steps:
|
||||||
|
|
||||||
1. Sign in to GitLab.com if needed.
|
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.
|
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.
|
New users and existing users on subsequent visits can access the group through the identify provider's dashboard or by visiting links directly.
|
||||||
|
|
|
@ -155,6 +155,7 @@ The following table depicts the various user permission levels in a project.
|
||||||
| Remove project | | | | | ✓ |
|
| Remove project | | | | | ✓ |
|
||||||
| Archive project | | | | | ✓ |
|
| Archive project | | | | | ✓ |
|
||||||
| Delete issues | | | | | ✓ |
|
| Delete issues | | | | | ✓ |
|
||||||
|
| Delete pipelines | | | | | ✓ |
|
||||||
| Delete merge request | | | | | ✓ |
|
| Delete merge request | | | | | ✓ |
|
||||||
| Disable notification emails | | | | | ✓ |
|
| Disable notification emails | | | | | ✓ |
|
||||||
| Force push to protected branches (*4*) | | | | | |
|
| Force push to protected branches (*4*) | | | | | |
|
||||||
|
|
|
@ -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. |
|
| `/create_merge_request <branch name>` | ✓ | | | Create a new merge request starting from the current issue. |
|
||||||
| `/done` | ✓ | ✓ | ✓ | Mark To-Do as done. |
|
| `/done` | ✓ | ✓ | ✓ | Mark To-Do as done. |
|
||||||
| `/due <date>` | ✓ | | | Set due date. Examples of valid `<date>` include `in 2 days`, `this Friday` and `December 31st`. |
|
| `/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)** |
|
| `/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`. |
|
| `/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)** |
|
| `/iteration *iteration:iteration` | ✓ | | | Set iteration ([Introduced in GitLab 13.1](https://gitlab.com/gitlab-org/gitlab/-/issues/196795)) **(STARTER)** |
|
||||||
|
|
|
@ -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)
|
![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
|
```yaml
|
||||||
service_desk_email:
|
service_desk_email:
|
||||||
|
@ -167,6 +169,32 @@ service_desk_email:
|
||||||
expunge_deleted: true
|
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
|
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`.
|
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.
|
As a result, a new Service Desk issue is created from this email in the `mygroup/myproject` project.
|
||||||
|
|
|
@ -156,6 +156,7 @@ module API
|
||||||
mount ::API::Groups
|
mount ::API::Groups
|
||||||
mount ::API::GroupContainerRepositories
|
mount ::API::GroupContainerRepositories
|
||||||
mount ::API::GroupVariables
|
mount ::API::GroupVariables
|
||||||
|
mount ::API::ImportBitbucketServer
|
||||||
mount ::API::ImportGithub
|
mount ::API::ImportGithub
|
||||||
mount ::API::Issues
|
mount ::API::Issues
|
||||||
mount ::API::JobArtifacts
|
mount ::API::JobArtifacts
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
|
@ -6,7 +6,6 @@ module Gitlab
|
||||||
module Danger
|
module Danger
|
||||||
module Roulette
|
module Roulette
|
||||||
ROULETTE_DATA_URL = 'https://gitlab-org.gitlab.io/gitlab-roulette/roulette.json'
|
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)
|
Spin = Struct.new(:category, :reviewer, :maintainer, :optional_role)
|
||||||
|
|
||||||
|
@ -119,11 +118,7 @@ module Gitlab
|
||||||
reviewer = spin_for_person(reviewers + traintainers + traintainers, random: random)
|
reviewer = spin_for_person(reviewers + traintainers + traintainers, random: random)
|
||||||
maintainer = spin_for_person(maintainers, random: random)
|
maintainer = spin_for_person(maintainers, random: random)
|
||||||
|
|
||||||
Spin.new(category, reviewer, maintainer).tap do |spin|
|
Spin.new(category, reviewer, maintainer)
|
||||||
if OPTIONAL_CATEGORIES.include?(category)
|
|
||||||
spin.optional_role = :maintainer
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -32,10 +32,6 @@ module Gitlab
|
||||||
|
|
||||||
action = "#{controller.action_name}"
|
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.
|
# Devise exposes a method called "request_format" that does the below.
|
||||||
# However, this method is not available to all controllers (e.g. certain
|
# However, this method is not available to all controllers (e.g. certain
|
||||||
# Doorkeeper controllers). As such we use the underlying code directly.
|
# Doorkeeper controllers). As such we use the underlying code directly.
|
||||||
|
@ -49,7 +45,7 @@ module Gitlab
|
||||||
action = "#{action}.#{suffix}"
|
action = "#{action}.#{suffix}"
|
||||||
end
|
end
|
||||||
|
|
||||||
{ controller: controller.class.name, action: action, feature_category: feature_category }
|
{ controller: controller.class.name, action: action }
|
||||||
end
|
end
|
||||||
|
|
||||||
def labels_from_endpoint
|
def labels_from_endpoint
|
||||||
|
@ -65,10 +61,7 @@ module Gitlab
|
||||||
|
|
||||||
if route
|
if route
|
||||||
path = endpoint_paths_cache[route.request_method][route.path]
|
path = endpoint_paths_cache[route.request_method][route.path]
|
||||||
|
{ controller: 'Grape', action: "#{route.request_method} #{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: '' }
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -409,6 +409,9 @@ msgstr ""
|
||||||
msgid "%{labelStart}File:%{labelEnd} %{file}"
|
msgid "%{labelStart}File:%{labelEnd} %{file}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "%{labelStart}Headers:%{labelEnd} %{headers}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "%{labelStart}Image:%{labelEnd} %{image}"
|
msgid "%{labelStart}Image:%{labelEnd} %{image}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -427,6 +430,12 @@ msgstr ""
|
||||||
msgid "%{labelStart}Severity:%{labelEnd} %{severity}"
|
msgid "%{labelStart}Severity:%{labelEnd} %{severity}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "%{labelStart}Status:%{labelEnd} %{status}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "%{labelStart}URL:%{labelEnd} %{url}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "%{label_for_message} unavailable"
|
msgid "%{label_for_message} unavailable"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -10813,6 +10822,9 @@ msgstr ""
|
||||||
msgid "GitLab commit"
|
msgid "GitLab commit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "GitLab export"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "GitLab for Slack"
|
msgid "GitLab for Slack"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -10837,7 +10849,7 @@ msgstr ""
|
||||||
msgid "GitLab restart is required to apply changes."
|
msgid "GitLab restart is required to apply changes."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "GitLab single sign on URL"
|
msgid "GitLab single sign-on URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "GitLab uses %{jaeger_link} to monitor distributed systems."
|
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."
|
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 ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "GitLab.com"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "GitLab.com import"
|
msgid "GitLab.com import"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -11386,7 +11401,7 @@ msgstr ""
|
||||||
msgid "GroupSAML|Identity"
|
msgid "GroupSAML|Identity"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "GroupSAML|Identity provider single sign on URL"
|
msgid "GroupSAML|Identity provider single sign-on URL"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "GroupSAML|Make sure you save this token — you won't be able to access it again."
|
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."
|
msgid "TestReports|There are no tests to show."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "TestReports|There was an error fetching the summary."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "TestReports|There was an error fetching the test reports."
|
msgid "TestReports|There was an error fetching the test reports."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -25680,6 +25698,12 @@ msgstr ""
|
||||||
msgid "Vulnerability|Project"
|
msgid "Vulnerability|Project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Vulnerability|Request"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Vulnerability|Response"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Vulnerability|Scanner"
|
msgid "Vulnerability|Scanner"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -46,7 +46,7 @@ RSpec.describe Import::BitbucketServerController do
|
||||||
.to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything)
|
.to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything)
|
||||||
.and_return(double(execute: project))
|
.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)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
end
|
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)
|
.to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything)
|
||||||
.and_return(double(execute: project))
|
.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)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns an error when an invalid project key is used' do
|
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)
|
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns an error when an invalid repository slug is used' do
|
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)
|
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||||
end
|
end
|
||||||
|
@ -80,7 +80,7 @@ RSpec.describe Import::BitbucketServerController do
|
||||||
it 'returns an error when the project cannot be found' 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)
|
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)
|
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||||
end
|
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)
|
.to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything)
|
||||||
.and_return(double(execute: build(:project)))
|
.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)
|
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns an error when the server can't be contacted" do
|
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)
|
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||||
end
|
end
|
||||||
|
@ -123,7 +123,9 @@ RSpec.describe Import::BitbucketServerController do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'sets the session variables' do
|
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_url]).to eq(url)
|
||||||
expect(session[:bitbucket_server_username]).to eq(username)
|
expect(session[:bitbucket_server_username]).to eq(username)
|
||||||
|
|
|
@ -383,8 +383,8 @@ RSpec.describe 'Pipeline', :js do
|
||||||
context 'without test reports' do
|
context 'without test reports' do
|
||||||
let(:pipeline) { create(:ci_pipeline, project: project) }
|
let(:pipeline) { create(:ci_pipeline, project: project) }
|
||||||
|
|
||||||
it 'shows nothing' do
|
it 'shows zero' do
|
||||||
expect(page.find('.js-test-report-badge-counter', visible: :all).text).to eq("")
|
expect(page.find('.js-test-report-badge-counter', visible: :all).text).to eq("0")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,33 +2,51 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Projects::BranchesController, '(JavaScript fixtures)', type: :controller do
|
RSpec.describe 'Branches (JavaScript fixtures)' do
|
||||||
include JavaScriptFixturesHelpers
|
include JavaScriptFixturesHelpers
|
||||||
|
|
||||||
let(:admin) { create(:admin) }
|
let_it_be(:admin) { create(:admin) }
|
||||||
let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
|
let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
|
||||||
let(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
|
let_it_be(:project) { create(:project, :repository, namespace: namespace, path: 'branches-project') }
|
||||||
|
|
||||||
render_views
|
|
||||||
|
|
||||||
before(:all) do
|
before(:all) do
|
||||||
clean_frontend_fixtures('branches/')
|
clean_frontend_fixtures('branches/')
|
||||||
|
clean_frontend_fixtures('api/branches/')
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
after(:all) do
|
||||||
sign_in(admin)
|
|
||||||
end
|
|
||||||
|
|
||||||
after do
|
|
||||||
remove_repository(project)
|
remove_repository(project)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'branches/new_branch.html' do
|
describe Projects::BranchesController, '(JavaScript fixtures)', type: :controller do
|
||||||
get :new, params: {
|
render_views
|
||||||
namespace_id: project.namespace.to_param,
|
|
||||||
project_id: project
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,34 +2,55 @@
|
||||||
|
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do
|
RSpec.describe 'Commit (JavaScript fixtures)' do
|
||||||
include JavaScriptFixturesHelpers
|
include JavaScriptFixturesHelpers
|
||||||
|
|
||||||
let_it_be(:project) { create(:project, :repository) }
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
let(:commit) { project.commit("master") }
|
let_it_be(:commit) { project.commit("master") }
|
||||||
|
|
||||||
render_views
|
|
||||||
|
|
||||||
before(:all) do
|
before(:all) do
|
||||||
clean_frontend_fixtures('commit/')
|
clean_frontend_fixtures('commit/')
|
||||||
|
clean_frontend_fixtures('api/commits/')
|
||||||
|
|
||||||
|
project.add_maintainer(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
before do
|
||||||
project.add_maintainer(user)
|
|
||||||
sign_in(user)
|
|
||||||
allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
|
allow(SecureRandom).to receive(:hex).and_return('securerandomhex:thereisnospoon')
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'commit/show.html' do
|
after(:all) do
|
||||||
params = {
|
remove_repository(project)
|
||||||
namespace_id: project.namespace,
|
end
|
||||||
project_id: project,
|
|
||||||
id: commit.id
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -14,12 +14,16 @@ describe('Actions TestReports Store', () => {
|
||||||
let state;
|
let state;
|
||||||
|
|
||||||
const testReports = getJSONFixture('pipelines/test_report.json');
|
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 = {
|
const defaultState = {
|
||||||
endpoint,
|
fullReportEndpoint,
|
||||||
|
summaryEndpoint,
|
||||||
testReports: {},
|
testReports: {},
|
||||||
selectedSuite: {},
|
selectedSuite: {},
|
||||||
|
summary: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -31,14 +35,47 @@ describe('Actions TestReports Store', () => {
|
||||||
mock.restore();
|
mock.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fetch reports', () => {
|
describe('fetch report summary', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mock.onGet(`${TEST_HOST}/test_reports.json`).replyOnce(200, testReports, {});
|
mock.onGet(summaryEndpoint).replyOnce(200, summary, {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sets testReports and shows tests', done => {
|
it('sets testReports and shows tests', done => {
|
||||||
testAction(
|
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,
|
null,
|
||||||
state,
|
state,
|
||||||
[{ type: types.SET_REPORTS, payload: testReports }],
|
[{ type: types.SET_REPORTS, payload: testReports }],
|
||||||
|
@ -49,10 +86,10 @@ describe('Actions TestReports Store', () => {
|
||||||
|
|
||||||
it('should create flash on API error', done => {
|
it('should create flash on API error', done => {
|
||||||
testAction(
|
testAction(
|
||||||
actions.fetchReports,
|
actions.fetchFullReport,
|
||||||
null,
|
null,
|
||||||
{
|
{
|
||||||
endpoint: null,
|
fullReportEndpoint: null,
|
||||||
},
|
},
|
||||||
[],
|
[],
|
||||||
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
|
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
|
||||||
|
|
|
@ -18,15 +18,6 @@ describe('Mutations TestReports Store', () => {
|
||||||
mockState = defaultState;
|
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', () => {
|
describe('set reports', () => {
|
||||||
it('should set testReports', () => {
|
it('should set testReports', () => {
|
||||||
const expectedState = { ...mockState, 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', () => {
|
describe('toggle loading', () => {
|
||||||
it('should set to true', () => {
|
it('should set to true', () => {
|
||||||
const expectedState = { ...mockState, isLoading: true };
|
const expectedState = { ...mockState, isLoading: true };
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
import { getJSONFixture } from 'helpers/fixtures';
|
import { getJSONFixture } from 'helpers/fixtures';
|
||||||
import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
|
import TestReports from '~/pipelines/components/test_reports/test_reports.vue';
|
||||||
import * as actions from '~/pipelines/stores/test_reports/actions';
|
import * as actions from '~/pipelines/stores/test_reports/actions';
|
||||||
|
|
||||||
|
const localVue = createLocalVue();
|
||||||
|
localVue.use(Vuex);
|
||||||
|
|
||||||
describe('Test reports app', () => {
|
describe('Test reports app', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let store;
|
let store;
|
||||||
|
@ -22,11 +25,15 @@ describe('Test reports app', () => {
|
||||||
testReports,
|
testReports,
|
||||||
...state,
|
...state,
|
||||||
},
|
},
|
||||||
actions,
|
actions: {
|
||||||
|
...actions,
|
||||||
|
fetchSummary: () => {},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper = shallowMount(TestReports, {
|
wrapper = shallowMount(TestReports, {
|
||||||
store,
|
store,
|
||||||
|
localVue,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
import Vuex from 'vuex';
|
import Vuex from 'vuex';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
||||||
import { getJSONFixture } from 'helpers/fixtures';
|
import { getJSONFixture } from 'helpers/fixtures';
|
||||||
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
|
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
|
||||||
import * as getters from '~/pipelines/stores/test_reports/getters';
|
import * as getters from '~/pipelines/stores/test_reports/getters';
|
||||||
import { TestStatus } from '~/pipelines/constants';
|
import { TestStatus } from '~/pipelines/constants';
|
||||||
import skippedTestCases from './mock_data';
|
import skippedTestCases from './mock_data';
|
||||||
|
|
||||||
|
const localVue = createLocalVue();
|
||||||
|
localVue.use(Vuex);
|
||||||
|
|
||||||
describe('Test reports suite table', () => {
|
describe('Test reports suite table', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let store;
|
let store;
|
||||||
|
@ -32,6 +35,7 @@ describe('Test reports suite table', () => {
|
||||||
|
|
||||||
wrapper = shallowMount(SuiteTable, {
|
wrapper = shallowMount(SuiteTable, {
|
||||||
store,
|
store,
|
||||||
|
localVue,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -181,6 +181,23 @@ RSpec.describe GitlabRoutingHelper do
|
||||||
end
|
end
|
||||||
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
|
describe '#gitlab_raw_snippet_url' do
|
||||||
it 'returns the raw personal 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")
|
expect(gitlab_raw_snippet_url(personal_snippet)).to eq("http://test.host/snippets/#{personal_snippet.id}/raw")
|
||||||
|
|
|
@ -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
|
|
|
@ -155,8 +155,8 @@ RSpec.describe Gitlab::Danger::Roulette do
|
||||||
context 'when change contains QA category' do
|
context 'when change contains QA category' do
|
||||||
let(:categories) { [:qa] }
|
let(:categories) { [:qa] }
|
||||||
|
|
||||||
it 'assigns QA reviewer and sets optional QA maintainer' do
|
it 'assigns QA reviewer' do
|
||||||
expect(spins).to contain_exactly(matching_spin(:qa, reviewer: software_engineer_in_test, optional: :maintainer))
|
expect(spins).to contain_exactly(matching_spin(:qa, reviewer: software_engineer_in_test))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -171,8 +171,8 @@ RSpec.describe Gitlab::Danger::Roulette do
|
||||||
context 'when change contains test category' do
|
context 'when change contains test category' do
|
||||||
let(:categories) { [:test] }
|
let(:categories) { [:test] }
|
||||||
|
|
||||||
it 'assigns corresponding SET and sets optional test maintainer' do
|
it 'assigns corresponding SET' do
|
||||||
expect(spins).to contain_exactly(matching_spin(:test, reviewer: software_engineer_in_test, optional: :maintainer))
|
expect(spins).to contain_exactly(matching_spin(:test, reviewer: software_engineer_in_test))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -70,9 +70,6 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#labels' do
|
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
|
context 'when request goes to Grape endpoint' do
|
||||||
before do
|
before do
|
||||||
route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)')
|
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
|
env['api.endpoint'] = endpoint
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'provides labels with the method and path of the route in the grape endpoint' do
|
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
|
end
|
||||||
|
|
||||||
it 'does not provide labels if route infos are missing' do
|
it 'does not provide labels if route infos are missing' do
|
||||||
|
@ -96,21 +92,24 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when request goes to ActionController' do
|
context 'when request goes to ActionController' do
|
||||||
|
let(:request) { double(:request, format: double(:format, ref: :html)) }
|
||||||
|
|
||||||
before do
|
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
|
env['action_controller.instance'] = controller
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'tags a transaction with the name and action of a controller' do
|
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
|
end
|
||||||
|
|
||||||
context 'when the request content type is not :html' do
|
context 'when the request content type is not :html' do
|
||||||
let(:request) { double(:request, format: double(:format, ref: :json)) }
|
let(:request) { double(:request, format: double(:format, ref: :json)) }
|
||||||
|
|
||||||
it 'appends the mime type to the transaction action' do
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -118,29 +117,9 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
|
||||||
let(:request) { double(:request, format: double(:format, ref: 'http://example.com')) }
|
let(:request) { double(:request, format: double(:format, ref: 'http://example.com')) }
|
||||||
|
|
||||||
it 'does not append the MIME type to the transaction action' do
|
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
|
||||||
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
|
end
|
||||||
|
|
||||||
it 'returns no labels when no route information is present in env' do
|
it 'returns no labels when no route information is present in env' do
|
||||||
|
|
|
@ -70,7 +70,16 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
|
||||||
end
|
end
|
||||||
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 'data refresh'
|
||||||
|
it_behaves_like 'does not include personal snippets'
|
||||||
|
|
||||||
context 'with subgroups' do
|
context 'with subgroups' do
|
||||||
let(:subgroup1) { create(:group, parent: namespace)}
|
let(:subgroup1) { create(:group, parent: namespace)}
|
||||||
|
@ -80,12 +89,45 @@ RSpec.describe Namespace::RootStorageStatistics, type: :model do
|
||||||
let(:project2) { create(:project, namespace: subgroup2) }
|
let(:project2) { create(:project, namespace: subgroup2) }
|
||||||
|
|
||||||
it_behaves_like 'data refresh'
|
it_behaves_like 'data refresh'
|
||||||
|
it_behaves_like 'does not include personal snippets'
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with a personal namespace' do
|
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'
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -109,22 +109,38 @@ RSpec.describe SnippetBlobPresenter do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#raw_path' do
|
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
|
before do
|
||||||
let!(:project) { create(:project) }
|
project.add_developer(user)
|
||||||
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
|
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with PersonalSnippet' do
|
subject { described_class.new(snippet.blobs.first, current_user: user).raw_path }
|
||||||
let(:snippet) { create(:personal_snippet) }
|
|
||||||
|
|
||||||
it 'returns the raw path' do
|
it_behaves_like 'snippet blob raw path'
|
||||||
expect(subject).to eq "/snippets/#{snippet.id}/raw"
|
|
||||||
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue