Commit ae832eef authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 91f8b22c 4ca0e8e7
...@@ -36,6 +36,9 @@ export default { ...@@ -36,6 +36,9 @@ export default {
computed: { computed: {
...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']), ...mapGetters(['isSidebarOpen', 'shouldUseGraphQL']),
...mapState(['activeId', 'sidebarType', 'boardLists']), ...mapState(['activeId', 'sidebarType', 'boardLists']),
isWipLimitsOn() {
return this.glFeatures.wipLimits;
},
activeList() { activeList() {
/* /*
Warning: Though a computed property it is not reactive because we are Warning: Though a computed property it is not reactive because we are
...@@ -105,7 +108,10 @@ export default { ...@@ -105,7 +108,10 @@ export default {
:active-list="activeList" :active-list="activeList"
:board-list-type="boardListType" :board-list-type="boardListType"
/> />
<board-settings-sidebar-wip-limit :max-issue-count="activeList.maxIssueCount" /> <board-settings-sidebar-wip-limit
v-if="isWipLimitsOn"
:max-issue-count="activeList.maxIssueCount"
/>
<div v-if="canAdminList && !activeList.preset && activeList.id" class="gl-m-4"> <div v-if="canAdminList && !activeList.preset && activeList.id" class="gl-m-4">
<gl-button <gl-button
variant="danger" variant="danger"
......
<script> <script>
import { mapActions } from 'vuex';
import { GlAlert, GlButton } from '@gitlab/ui'; import { GlAlert, GlButton } from '@gitlab/ui';
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants'; import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
import eventHub from '../event_hub';
export default { export default {
components: { components: {
...@@ -36,13 +35,12 @@ export default { ...@@ -36,13 +35,12 @@ export default {
}, },
methods: { methods: {
...mapActions('diffs', ['expandAllFiles']),
dismiss() { dismiss() {
this.isDismissed = true; this.isDismissed = true;
this.$emit('dismiss'); this.$emit('dismiss');
}, },
expand() { expand() {
this.expandAllFiles(); eventHub.$emit('mr:diffs:expandAllFiles');
this.dismiss(); this.dismiss();
}, },
}, },
......
...@@ -7,6 +7,7 @@ import CompareDropdownLayout from './compare_dropdown_layout.vue'; ...@@ -7,6 +7,7 @@ import CompareDropdownLayout from './compare_dropdown_layout.vue';
import SettingsDropdown from './settings_dropdown.vue'; import SettingsDropdown from './settings_dropdown.vue';
import DiffStats from './diff_stats.vue'; import DiffStats from './diff_stats.vue';
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants'; import { CENTERED_LIMITED_CONTAINER_CLASSES } from '../constants';
import eventHub from '../event_hub';
export default { export default {
components: { components: {
...@@ -67,9 +68,11 @@ export default { ...@@ -67,9 +68,11 @@ export default {
...mapActions('diffs', [ ...mapActions('diffs', [
'setInlineDiffViewType', 'setInlineDiffViewType',
'setParallelDiffViewType', 'setParallelDiffViewType',
'expandAllFiles',
'toggleShowTreeList', 'toggleShowTreeList',
]), ]),
expandAllFiles() {
eventHub.$emit('mr:diffs:expandAllFiles');
},
}, },
}; };
</script> </script>
......
...@@ -6,13 +6,14 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; ...@@ -6,13 +6,14 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { sprintf } from '~/locale'; import { sprintf } from '~/locale';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper'; import { hasDiff } from '~/helpers/diffs_helper';
import eventHub from '../../notes/event_hub'; import notesEventHub from '../../notes/event_hub';
import DiffFileHeader from './diff_file_header.vue'; import DiffFileHeader from './diff_file_header.vue';
import DiffContent from './diff_content.vue'; import DiffContent from './diff_content.vue';
import { diffViewerErrors } from '~/ide/constants'; import { diffViewerErrors } from '~/ide/constants';
import { collapsedType, isCollapsed } from '../diff_file'; import { collapsedType, isCollapsed } from '../diff_file';
import { DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE } from '../constants'; import { DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE } from '../constants';
import { DIFF_FILE, GENERIC_ERROR } from '../i18n'; import { DIFF_FILE, GENERIC_ERROR } from '../i18n';
import eventHub from '../event_hub';
export default { export default {
components: { components: {
...@@ -151,7 +152,11 @@ export default { ...@@ -151,7 +152,11 @@ export default {
}, },
}, },
created() { created() {
eventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.requestDiff); notesEventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.requestDiff);
eventHub.$on('mr:diffs:expandAllFiles', this.expandAllListener);
},
beforeDestroy() {
eventHub.$off('mr:diffs:expandAllFiles', this.expandAllListener);
}, },
methods: { methods: {
...mapActions('diffs', [ ...mapActions('diffs', [
...@@ -160,6 +165,11 @@ export default { ...@@ -160,6 +165,11 @@ export default {
'setRenderIt', 'setRenderIt',
'setFileCollapsedByUser', 'setFileCollapsedByUser',
]), ]),
expandAllListener() {
if (this.isCollapsed) {
this.handleToggle();
}
},
handleToggle() { handleToggle() {
const currentCollapsedFlag = this.isCollapsed; const currentCollapsedFlag = this.isCollapsed;
......
import eventHubFactory from '~/helpers/event_hub_factory';
export default eventHubFactory();
...@@ -364,10 +364,6 @@ export const loadCollapsedDiff = ({ commit, getters, state }, file) => ...@@ -364,10 +364,6 @@ export const loadCollapsedDiff = ({ commit, getters, state }, file) =>
}); });
}); });
export const expandAllFiles = ({ commit }) => {
commit(types.EXPAND_ALL_FILES);
};
/** /**
* Toggles the file discussions after user clicked on the toggle discussions button. * Toggles the file discussions after user clicked on the toggle discussions button.
* *
......
...@@ -13,7 +13,6 @@ export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS'; ...@@ -13,7 +13,6 @@ export const SET_MERGE_REQUEST_DIFFS = 'SET_MERGE_REQUEST_DIFFS';
export const TOGGLE_LINE_HAS_FORM = 'TOGGLE_LINE_HAS_FORM'; export const TOGGLE_LINE_HAS_FORM = 'TOGGLE_LINE_HAS_FORM';
export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES'; export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS'; export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
export const RENDER_FILE = 'RENDER_FILE'; export const RENDER_FILE = 'RENDER_FILE';
export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE'; export const SET_LINE_DISCUSSIONS_FOR_FILE = 'SET_LINE_DISCUSSIONS_FOR_FILE';
export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE'; export const REMOVE_LINE_DISCUSSIONS_FOR_FILE = 'REMOVE_LINE_DISCUSSIONS_FOR_FILE';
......
...@@ -176,17 +176,6 @@ export default { ...@@ -176,17 +176,6 @@ export default {
Object.assign(selectedFile, { ...newFileData }); Object.assign(selectedFile, { ...newFileData });
}, },
[types.EXPAND_ALL_FILES](state) {
state.diffFiles.forEach(file => {
Object.assign(file, {
viewer: Object.assign(file.viewer, {
automaticallyCollapsed: false,
manuallyCollapsed: false,
}),
});
});
},
[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) { [types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { discussion, diffPositionByLineCode, hash }) {
const { latestDiff } = state; const { latestDiff } = state;
......
...@@ -16,63 +16,53 @@ const frequentItemDropdowns = [ ...@@ -16,63 +16,53 @@ const frequentItemDropdowns = [
}, },
]; ];
const initFrequentItemList = (namespace, key) => {
const el = document.getElementById(`js-${namespace}-dropdown`);
// Don't do anything if element doesn't exist (No groups dropdown)
// This is for when the user accesses GitLab without logging in
if (!el) {
return;
}
import('./components/app.vue')
.then(({ default: FrequentItems }) => {
// eslint-disable-next-line no-new
new Vue({
el,
data() {
const { dataset } = this.$options.el;
const item = {
id: Number(dataset[`${key}Id`]),
name: dataset[`${key}Name`],
namespace: dataset[`${key}Namespace`],
webUrl: dataset[`${key}WebUrl`],
avatarUrl: dataset[`${key}AvatarUrl`] || null,
lastAccessedOn: Date.now(),
};
return {
currentUserName: dataset.userName,
currentItem: item,
};
},
render(createElement) {
return createElement(FrequentItems, {
props: {
namespace,
currentUserName: this.currentUserName,
currentItem: this.currentItem,
},
});
},
});
})
.catch(() => {});
};
export default function initFrequentItemDropdowns() { export default function initFrequentItemDropdowns() {
frequentItemDropdowns.forEach(dropdown => { frequentItemDropdowns.forEach(dropdown => {
const { namespace, key } = dropdown; const { namespace, key } = dropdown;
const el = document.getElementById(`js-${namespace}-dropdown`);
const navEl = document.getElementById(`nav-${namespace}-dropdown`); const navEl = document.getElementById(`nav-${namespace}-dropdown`);
// Don't do anything if element doesn't exist (No groups dropdown) // Don't do anything if element doesn't exist (No groups dropdown)
// This is for when the user accesses GitLab without logging in // This is for when the user accesses GitLab without logging in
if (!navEl) { if (!el || !navEl) {
return; return;
} }
import('./components/app.vue')
.then(({ default: FrequentItems }) => {
// eslint-disable-next-line no-new
new Vue({
el,
data() {
const { dataset } = this.$options.el;
const item = {
id: Number(dataset[`${key}Id`]),
name: dataset[`${key}Name`],
namespace: dataset[`${key}Namespace`],
webUrl: dataset[`${key}WebUrl`],
avatarUrl: dataset[`${key}AvatarUrl`] || null,
lastAccessedOn: Date.now(),
};
return {
currentUserName: dataset.userName,
currentItem: item,
};
},
render(createElement) {
return createElement(FrequentItems, {
props: {
namespace,
currentUserName: this.currentUserName,
currentItem: this.currentItem,
},
});
},
});
})
.catch(() => {});
$(navEl).on('shown.bs.dropdown', () => { $(navEl).on('shown.bs.dropdown', () => {
initFrequentItemList(namespace, key);
eventHub.$emit(`${namespace}-dropdownOpen`); eventHub.$emit(`${namespace}-dropdownOpen`);
}); });
}); });
......
---
title: Add Caching to BitBucket Server Import for pull requests
merge_request: 45790
author: Simon Schrottner
type: performance
---
title: Fix tracking of frequently visited projects / groups
merge_request: 46348
author:
type: fixed
---
title: Add licensed check for wip limits
merge_request: 46387
author:
type: fixed
...@@ -22,7 +22,7 @@ describe('ee/BoardSettingsSidebar', () => { ...@@ -22,7 +22,7 @@ describe('ee/BoardSettingsSidebar', () => {
const listId = 1; const listId = 1;
let mock; let mock;
const createComponent = (actions = {}) => { const createComponent = (actions = {}, isWipLimitsOn = false) => {
storeActions = actions; storeActions = actions;
const store = new Vuex.Store({ const store = new Vuex.Store({
...@@ -34,6 +34,11 @@ describe('ee/BoardSettingsSidebar', () => { ...@@ -34,6 +34,11 @@ describe('ee/BoardSettingsSidebar', () => {
wrapper = shallowMount(BoardSettingsSidebar, { wrapper = shallowMount(BoardSettingsSidebar, {
store, store,
localVue, localVue,
provide: {
glFeatures: {
wipLimits: isWipLimitsOn,
},
},
stubs: { stubs: {
'board-settings-sidebar-wip-limit': BoardSettingsWipLimit, 'board-settings-sidebar-wip-limit': BoardSettingsWipLimit,
'board-settings-list-types': BoardSettingsListTypes, 'board-settings-list-types': BoardSettingsListTypes,
...@@ -59,7 +64,7 @@ describe('ee/BoardSettingsSidebar', () => { ...@@ -59,7 +64,7 @@ describe('ee/BoardSettingsSidebar', () => {
list_type: 'label', list_type: 'label',
}); });
createComponent(); createComponent({}, true);
expect(wrapper.find(BoardSettingsWipLimit).exists()).toBe(true); expect(wrapper.find(BoardSettingsWipLimit).exists()).toBe(true);
}); });
......
...@@ -8,6 +8,8 @@ module API ...@@ -8,6 +8,8 @@ module API
helpers ::API::Helpers::BadgesHelpers helpers ::API::Helpers::BadgesHelpers
feature_category :continuous_integration
helpers do helpers do
def find_source_if_admin(source_type) def find_source_if_admin(source_type)
source = find_source(source_type, params[:id]) source = find_source(source_type, params[:id])
......
...@@ -9,6 +9,8 @@ module API ...@@ -9,6 +9,8 @@ module API
before { authenticate! } before { authenticate! }
feature_category :boards
helpers do helpers do
def board_parent def board_parent
user_project user_project
......
...@@ -10,6 +10,8 @@ module API ...@@ -10,6 +10,8 @@ module API
after_validation { content_type "application/json" } after_validation { content_type "application/json" }
feature_category :source_code_management
before do before do
require_repository_enabled! require_repository_enabled!
authorize! :download_code, user_project authorize! :download_code, user_project
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class BroadcastMessages < ::API::Base class BroadcastMessages < ::API::Base
include PaginationParams include PaginationParams
feature_category :navigation
resource :broadcast_messages do resource :broadcast_messages do
helpers do helpers do
def find_message def find_message
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
before { authenticate! } before { authenticate! }
feature_category :continuous_integration
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
before { authenticate_non_get! } before { authenticate_non_get! }
feature_category :continuous_integration
params do params do
requires :id, type: String, desc: 'The project ID' requires :id, type: String, desc: 'The project ID'
end end
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
content_type :txt, 'text/plain' content_type :txt, 'text/plain'
feature_category :continuous_integration
resource :runners do resource :runners do
desc 'Registers a new Runner' do desc 'Registers a new Runner' do
success Entities::RunnerRegistrationDetails success Entities::RunnerRegistrationDetails
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
before { authenticate! } before { authenticate! }
feature_category :continuous_integration
resource :runners do resource :runners do
desc 'Get runners available for user' do desc 'Get runners available for user' do
success Entities::Runner success Entities::Runner
......
...@@ -4,6 +4,8 @@ require 'mime/types' ...@@ -4,6 +4,8 @@ require 'mime/types'
module API module API
class CommitStatuses < ::API::Base class CommitStatuses < ::API::Base
feature_category :continuous_integration
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
......
...@@ -6,6 +6,8 @@ module API ...@@ -6,6 +6,8 @@ module API
class Commits < ::API::Base class Commits < ::API::Base
include PaginationParams include PaginationParams
feature_category :source_code_management
before do before do
require_repository_enabled! require_repository_enabled!
authorize! :download_code, user_project authorize! :download_code, user_project
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class ContainerRegistryEvent < ::API::Base class ContainerRegistryEvent < ::API::Base
DOCKER_DISTRIBUTION_EVENTS_V1_JSON = 'application/vnd.docker.distribution.events.v1+json' DOCKER_DISTRIBUTION_EVENTS_V1_JSON = 'application/vnd.docker.distribution.events.v1+json'
feature_category :package_registry
before { authenticate_registry_notification! } before { authenticate_registry_notification! }
resource :container_registry_event do resource :container_registry_event do
......
...@@ -6,6 +6,8 @@ module API ...@@ -6,6 +6,8 @@ module API
before { authenticate! } before { authenticate! }
feature_category :continuous_delivery
helpers do helpers do
def add_deploy_keys_project(project, attrs = {}) def add_deploy_keys_project(project, attrs = {})
project.deploy_keys_projects.create(attrs) project.deploy_keys_projects.create(attrs)
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class DeployTokens < ::API::Base class DeployTokens < ::API::Base
include PaginationParams include PaginationParams
feature_category :continuous_delivery
helpers do helpers do
def scope_params def scope_params
scopes = params.delete(:scopes) scopes = params.delete(:scopes)
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
before { authenticate! } before { authenticate! }
feature_category :continuous_delivery
params do params do
requires :id, type: String, desc: 'The project ID' requires :id, type: String, desc: 'The project ID'
end end
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
before { authenticate! } before { authenticate! }
feature_category :continuous_delivery
params do params do
requires :id, type: String, desc: 'The project ID' requires :id, type: String, desc: 'The project ID'
end end
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class ErrorTracking < ::API::Base class ErrorTracking < ::API::Base
before { authenticate! } before { authenticate! }
feature_category :error_tracking
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
......
...@@ -8,6 +8,8 @@ module API ...@@ -8,6 +8,8 @@ module API
allow_access_with_scope :read_user, if: -> (request) { request.get? } allow_access_with_scope :read_user, if: -> (request) { request.get? }
feature_category :users
resource :events do resource :events do
desc "List currently authenticated user's events" do desc "List currently authenticated user's events" do
detail 'This feature was introduced in GitLab 9.3.' detail 'This feature was introduced in GitLab 9.3.'
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
ENVIRONMENT_SCOPE_ENDPOINT_REQUIREMENTS = FeatureFlags::FEATURE_FLAG_ENDPOINT_REQUIREMENTS ENVIRONMENT_SCOPE_ENDPOINT_REQUIREMENTS = FeatureFlags::FEATURE_FLAG_ENDPOINT_REQUIREMENTS
.merge(environment_scope: API::NO_SLASH_URL_PART_REGEX) .merge(environment_scope: API::NO_SLASH_URL_PART_REGEX)
feature_category :feature_flags
before do before do
authorize_read_feature_flags! authorize_read_feature_flags!
end end
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
FEATURE_FLAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS FEATURE_FLAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS
.merge(name: API::NO_SLASH_URL_PART_REGEX) .merge(name: API::NO_SLASH_URL_PART_REGEX)
feature_category :feature_flags
before do before do
authorize_read_feature_flags! authorize_read_feature_flags!
end end
......
...@@ -8,6 +8,8 @@ module API ...@@ -8,6 +8,8 @@ module API
message.is_a?(String) ? { message: message }.to_json : message.to_json message.is_a?(String) ? { message: message }.to_json : message.to_json
} }
feature_category :feature_flags
before do before do
authorize_admin_feature_flags_user_lists! authorize_admin_feature_flags_user_lists!
end end
......
...@@ -4,6 +4,8 @@ module API ...@@ -4,6 +4,8 @@ module API
class Features < ::API::Base class Features < ::API::Base
before { authenticated_as_admin! } before { authenticated_as_admin! }
feature_category :feature_flags
helpers do helpers do
def gate_value(params) def gate_value(params)
case params[:value] case params[:value]
......
...@@ -9,6 +9,8 @@ module API ...@@ -9,6 +9,8 @@ module API
# Prevents returning plain/text responses for files with .txt extension # Prevents returning plain/text responses for files with .txt extension
after_validation { content_type "application/json" } after_validation { content_type "application/json" }
feature_category :source_code_management
helpers ::API::Helpers::HeadersHelpers helpers ::API::Helpers::HeadersHelpers
helpers do helpers do
......
...@@ -6,6 +6,8 @@ module API ...@@ -6,6 +6,8 @@ module API
before { authenticate! } before { authenticate! }
feature_category :continuous_delivery
params do params do
requires :id, type: String, desc: 'The ID of a project' requires :id, type: String, desc: 'The ID of a project'
end end
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
prepend_if_ee('EE::API::BoardsResponses') # rubocop: disable Cop/InjectEnterpriseEditionModule prepend_if_ee('EE::API::BoardsResponses') # rubocop: disable Cop/InjectEnterpriseEditionModule
feature_category :boards
before do before do
authenticate! authenticate!
end end
......
...@@ -6,6 +6,8 @@ module API ...@@ -6,6 +6,8 @@ module API
before { authenticate! } before { authenticate! }
feature_category :kubernetes_management
params do params do
requires :id, type: String, desc: 'The ID of the group' requires :id, type: String, desc: 'The ID of the group'
end end
......
...@@ -8,6 +8,8 @@ module API ...@@ -8,6 +8,8 @@ module API
before { authorize_read_group_container_images! } before { authorize_read_group_container_images! }
feature_category :package_registry
REPOSITORY_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge( REPOSITORY_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(
tag_name: API::NO_SLASH_URL_PART_REGEX) tag_name: API::NO_SLASH_URL_PART_REGEX)
......
...@@ -10,6 +10,8 @@ module API ...@@ -10,6 +10,8 @@ module API
authorize! :admin_group, user_group authorize! :admin_group, user_group
end end
feature_category :importers
params do params do
requires :id, type: String, desc: 'The ID of a group' requires :id, type: String, desc: 'The ID of a group'
end end
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
module API module API
class GroupImport < ::API::Base class GroupImport < ::API::Base
feature_category :importers
helpers Helpers::FileUploadHelpers helpers Helpers::FileUploadHelpers
helpers do helpers do
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
before { authenticate! } before { authenticate! }
feature_category :issue_tracking
params do params do
requires :id, type: String, desc: 'The ID of a group' requires :id, type: String, desc: 'The ID of a group'
end end
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
before { authenticate! } before { authenticate! }
feature_category :issue_tracking
params do params do
requires :id, type: String, desc: 'The ID of a group' requires :id, type: String, desc: 'The ID of a group'
end end
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
before { authenticate! } before { authenticate! }
before { authorize! :admin_build, user_group } before { authorize! :admin_build, user_group }
feature_category :continuous_integration
params do params do
requires :id, type: String, desc: 'The ID of a group' requires :id, type: String, desc: 'The ID of a group'
end end
......
...@@ -7,6 +7,8 @@ module API ...@@ -7,6 +7,8 @@ module API
before { authenticate_non_get! } before { authenticate_non_get! }
feature_category :subgroups
helpers Helpers::GroupsHelpers helpers Helpers::GroupsHelpers
helpers do helpers do
......
...@@ -4,11 +4,14 @@ module Gitlab ...@@ -4,11 +4,14 @@ module Gitlab
module BitbucketServerImport module BitbucketServerImport
class Importer class Importer
attr_reader :recover_missing_commits attr_reader :recover_missing_commits
attr_reader :project, :project_key, :repository_slug, :client, :errors, :users attr_reader :project, :project_key, :repository_slug, :client, :errors, :users, :already_imported_cache_key
attr_accessor :logger attr_accessor :logger
REMOTE_NAME = 'bitbucket_server' REMOTE_NAME = 'bitbucket_server'
BATCH_SIZE = 100 BATCH_SIZE = 100
# The base cache key to use for tracking already imported objects.
ALREADY_IMPORTED_CACHE_KEY =
'bitbucket_server-importer/already-imported/%{project}/%{collection}'
TempBranch = Struct.new(:name, :sha) TempBranch = Struct.new(:name, :sha)
...@@ -36,6 +39,12 @@ module Gitlab ...@@ -36,6 +39,12 @@ module Gitlab
@users = {} @users = {}
@temp_branches = [] @temp_branches = []
@logger = Gitlab::Import::Logger.build @logger = Gitlab::Import::Logger.build
@already_imported_cache_key = ALREADY_IMPORTED_CACHE_KEY %
{ project: project.id, collection: collection_method }
end
def collection_method
:pull_requests
end end
def execute def execute
...@@ -48,6 +57,7 @@ module Gitlab ...@@ -48,6 +57,7 @@ module Gitlab
log_info(stage: "complete") log_info(stage: "complete")
Gitlab::Cache::Import::Caching.expire(already_imported_cache_key, 15.minutes.to_i)
true true
end end
...@@ -167,6 +177,7 @@ module Gitlab ...@@ -167,6 +177,7 @@ module Gitlab
# on the remote server. Then we have to issue a `git fetch` to download these # on the remote server. Then we have to issue a `git fetch` to download these
# branches. # branches.
def import_pull_requests def import_pull_requests
log_info(stage: 'import_pull_requests', message: 'starting')
pull_requests = client.pull_requests(project_key, repository_slug).to_a pull_requests = client.pull_requests(project_key, repository_slug).to_a
# Creating branches on the server and fetching the newly-created branches # Creating branches on the server and fetching the newly-created branches
...@@ -176,7 +187,11 @@ module Gitlab ...@@ -176,7 +187,11 @@ module Gitlab
restore_branches(batch) if recover_missing_commits restore_branches(batch) if recover_missing_commits
batch.each do |pull_request| batch.each do |pull_request|
import_bitbucket_pull_request(pull_request) if already_imported?(pull_request)
log_info(stage: 'import_pull_requests', message: 'already imported', iid: pull_request.iid)
else
import_bitbucket_pull_request(pull_request)
end
rescue StandardError => e rescue StandardError => e
Gitlab::ErrorTracking.log_exception( Gitlab::ErrorTracking.log_exception(
e, e,
...@@ -189,6 +204,19 @@ module Gitlab ...@@ -189,6 +204,19 @@ module Gitlab
end end
end end
# Returns true if the given object has already been imported, false
# otherwise.
#
# object - The object to check.
def already_imported?(pull_request)
Gitlab::Cache::Import::Caching.set_includes?(already_imported_cache_key, pull_request.iid)
end
# Marks the given object as "already imported".
def mark_as_imported(pull_request)
Gitlab::Cache::Import::Caching.set_add(already_imported_cache_key, pull_request.iid)
end
def delete_temp_branches def delete_temp_branches
@temp_branches.each do |branch| @temp_branches.each do |branch|
client.delete_branch(project_key, repository_slug, branch.name, branch.sha) client.delete_branch(project_key, repository_slug, branch.name, branch.sha)
...@@ -236,6 +264,7 @@ module Gitlab ...@@ -236,6 +264,7 @@ module Gitlab
end end
log_info(stage: 'import_bitbucket_pull_requests', message: 'finished', iid: pull_request.iid) log_info(stage: 'import_bitbucket_pull_requests', message: 'finished', iid: pull_request.iid)
mark_as_imported(pull_request)
end end
def import_pull_request_comments(pull_request, merge_request) def import_pull_request_comments(pull_request, merge_request)
......
...@@ -6,7 +6,7 @@ module CrystalballEnv ...@@ -6,7 +6,7 @@ module CrystalballEnv
extend self extend self
def start! def start!
return unless ENV['CRYSTALBALL'] && ENV['CI_PIPELINE_SOURCE'] == 'schedule' return unless ENV['CRYSTALBALL'] && ENV['CI_PIPELINE_SOURCE'] == 'schedule' && ENV['FREQUENCY'] == '2-hourly'
require 'crystalball' require 'crystalball'
require_relative '../tooling/lib/tooling/crystalball/coverage_lines_execution_detector' require_relative '../tooling/lib/tooling/crystalball/coverage_lines_execution_detector'
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Frequently visited items', :js do
let_it_be(:user) { create(:user) }
before do
sign_in(user)
end
context 'for projects' do
let_it_be(:project) { create(:project, :public) }
it 'increments localStorage counter when visiting the project' do
visit project_path(project)
frequent_projects = nil
wait_for('localStorage frequent-projects') do
frequent_projects = page.evaluate_script("localStorage['#{user.username}/frequent-projects']")
frequent_projects.present?
end
expect(Gitlab::Json.parse(frequent_projects)).to contain_exactly(a_hash_including('id' => project.id, 'frequency' => 1))
end
end
context 'for groups' do
let_it_be(:group) { create(:group, :public) }
it 'increments localStorage counter when visiting the group' do
visit group_path(group)
frequent_groups = nil
wait_for('localStorage frequent-groups') do
frequent_groups = page.evaluate_script("localStorage['#{user.username}/frequent-groups']")
frequent_groups.present?
end
expect(Gitlab::Json.parse(frequent_groups)).to contain_exactly(a_hash_including('id' => group.id, 'frequency' => 1))
end
end
end
...@@ -294,7 +294,7 @@ RSpec.describe 'User interacts with awards' do ...@@ -294,7 +294,7 @@ RSpec.describe 'User interacts with awards' do
end end
end end
it 'toggles the smiley emoji on a note', :js do it 'toggles the smiley emoji on a note', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/267525' do
toggle_smiley_emoji(true) toggle_smiley_emoji(true)
within('.note-body') do within('.note-body') do
......
...@@ -3,6 +3,7 @@ import { shallowMount, mount, createLocalVue } from '@vue/test-utils'; ...@@ -3,6 +3,7 @@ import { shallowMount, mount, createLocalVue } from '@vue/test-utils';
import createStore from '~/diffs/store/modules'; import createStore from '~/diffs/store/modules';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue'; import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
import { CENTERED_LIMITED_CONTAINER_CLASSES } from '~/diffs/constants'; import { CENTERED_LIMITED_CONTAINER_CLASSES } from '~/diffs/constants';
import eventHub from '~/diffs/event_hub';
const propsData = { const propsData = {
limited: true, limited: true,
...@@ -76,13 +77,13 @@ describe('CollapsedFilesWarning', () => { ...@@ -76,13 +77,13 @@ describe('CollapsedFilesWarning', () => {
expect(wrapper.find('[data-testid="root"]').exists()).toBe(false); expect(wrapper.find('[data-testid="root"]').exists()).toBe(false);
}); });
it('triggers the expandAllFiles action when the alert action button is clicked', () => { it('emits the `mr:diffs:expandAllFiles` event when the alert action button is clicked', () => {
createComponent({}, { full: true }); createComponent({}, { full: true });
jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(undefined); jest.spyOn(eventHub, '$emit');
getAlertActionButton().vm.$emit('click'); getAlertActionButton().vm.$emit('click');
expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/expandAllFiles', undefined); expect(eventHub.$emit).toHaveBeenCalledWith('mr:diffs:expandAllFiles');
}); });
}); });
...@@ -9,6 +9,8 @@ import DiffFileComponent from '~/diffs/components/diff_file.vue'; ...@@ -9,6 +9,8 @@ import DiffFileComponent from '~/diffs/components/diff_file.vue';
import DiffFileHeaderComponent from '~/diffs/components/diff_file_header.vue'; import DiffFileHeaderComponent from '~/diffs/components/diff_file_header.vue';
import DiffContentComponent from '~/diffs/components/diff_content.vue'; import DiffContentComponent from '~/diffs/components/diff_content.vue';
import eventHub from '~/diffs/event_hub';
import { diffViewerModes, diffViewerErrors } from '~/ide/constants'; import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
function changeViewer(store, index, { automaticallyCollapsed, manuallyCollapsed, name }) { function changeViewer(store, index, { automaticallyCollapsed, manuallyCollapsed, name }) {
...@@ -138,6 +140,30 @@ describe('DiffFile', () => { ...@@ -138,6 +140,30 @@ describe('DiffFile', () => {
}); });
describe('collapsing', () => { describe('collapsing', () => {
describe('`mr:diffs:expandAllFiles` event', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'handleToggle').mockImplementation(() => {});
});
it('performs the normal file toggle when the file is collapsed', async () => {
makeFileAutomaticallyCollapsed(store);
await wrapper.vm.$nextTick();
eventHub.$emit('mr:diffs:expandAllFiles');
expect(wrapper.vm.handleToggle).toHaveBeenCalledTimes(1);
});
it('does nothing when the file is not collapsed', async () => {
eventHub.$emit('mr:diffs:expandAllFiles');
await wrapper.vm.$nextTick();
expect(wrapper.vm.handleToggle).not.toHaveBeenCalled();
});
});
describe('user collapsed', () => { describe('user collapsed', () => {
beforeEach(() => { beforeEach(() => {
makeFileManuallyCollapsed(store); makeFileManuallyCollapsed(store);
......
...@@ -27,7 +27,6 @@ import { ...@@ -27,7 +27,6 @@ import {
scrollToLineIfNeededInline, scrollToLineIfNeededInline,
scrollToLineIfNeededParallel, scrollToLineIfNeededParallel,
loadCollapsedDiff, loadCollapsedDiff,
expandAllFiles,
toggleFileDiscussions, toggleFileDiscussions,
saveDiffDiscussion, saveDiffDiscussion,
setHighlightedRow, setHighlightedRow,
...@@ -658,23 +657,6 @@ describe('DiffsStoreActions', () => { ...@@ -658,23 +657,6 @@ describe('DiffsStoreActions', () => {
}); });
}); });
describe('expandAllFiles', () => {
it('should change the collapsed prop from the diffFiles', done => {
testAction(
expandAllFiles,
null,
{},
[
{
type: types.EXPAND_ALL_FILES,
},
],
[],
done,
);
});
});
describe('toggleFileDiscussions', () => { describe('toggleFileDiscussions', () => {
it('should dispatch collapseDiscussion when all discussions are expanded', () => { it('should dispatch collapseDiscussion when all discussions are expanded', () => {
const getters = { const getters = {
......
...@@ -126,21 +126,6 @@ describe('DiffsStoreMutations', () => { ...@@ -126,21 +126,6 @@ describe('DiffsStoreMutations', () => {
}); });
}); });
describe('EXPAND_ALL_FILES', () => {
it('should change the collapsed prop from diffFiles', () => {
const diffFile = {
viewer: {
automaticallyCollapsed: true,
},
};
const state = { expandAllFiles: true, diffFiles: [diffFile] };
mutations[types.EXPAND_ALL_FILES](state);
expect(state.diffFiles[0].viewer.automaticallyCollapsed).toEqual(false);
});
});
describe('ADD_CONTEXT_LINES', () => { describe('ADD_CONTEXT_LINES', () => {
it('should call utils.addContextLines with proper params', () => { it('should call utils.addContextLines with proper params', () => {
const options = { const options = {
......
...@@ -22,7 +22,17 @@ RSpec.describe 'Every API endpoint' do ...@@ -22,7 +22,17 @@ RSpec.describe 'Every API endpoint' do
completed_classes = [ completed_classes = [
::API::Users, ::API::Issues, ::API::AccessRequests, ::API::Admin::Ci::Variables, ::API::Users, ::API::Issues, ::API::AccessRequests, ::API::Admin::Ci::Variables,
::API::Admin::InstanceClusters, ::API::Admin::Sidekiq, ::API::Appearance, ::API::Admin::InstanceClusters, ::API::Admin::Sidekiq, ::API::Appearance,
::API::Applications, ::API::Avatar, ::API::AwardEmoji ::API::Applications, ::API::Avatar, ::API::AwardEmoji, API::Badges,
::API::Boards, ::API::Branches, ::API::BroadcastMessages, ::API::Ci::Pipelines,
::API::Ci::PipelineSchedules, ::API::Ci::Runners, ::API::Ci::Runner,
::API::Commits, ::API::CommitStatuses, ::API::ContainerRegistryEvent,
::API::DeployKeys, ::API::DeployTokens, ::API::Deployments, ::API::Environments,
::API::ErrorTracking, ::API::Events, ::API::FeatureFlags, ::API::FeatureFlagScopes,
::API::FeatureFlagsUserLists, ::API::Features, ::API::Files, ::API::FreezePeriods,
::API::GroupBoards, ::API::GroupClusters, ::API::GroupExport, ::API::GroupImport,
::API::GroupLabels, ::API::GroupMilestones, ::API::Groups,
::API::GroupContainerRepositories, ::API::GroupVariables
] ]
next unless completed_classes.include?(klass) next unless completed_classes.include?(klass)
......
...@@ -115,6 +115,12 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do ...@@ -115,6 +115,12 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
allow(subject.client).to receive(:pull_requests).and_return([pull_request]) allow(subject.client).to receive(:pull_requests).and_return([pull_request])
end end
# As we are using Caching with redis, it is best to clean the cache after each test run, else we need to wait for
# the expiration by the importer
after do
Gitlab::Cache::Import::Caching.expire(subject.already_imported_cache_key, 0)
end
it 'imports merge event' do it 'imports merge event' do
expect(subject.client).to receive(:activities).and_return([merge_event]) expect(subject.client).to receive(:activities).and_return([merge_event])
...@@ -463,6 +469,47 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do ...@@ -463,6 +469,47 @@ RSpec.describe Gitlab::BitbucketServerImport::Importer do
subject.execute subject.execute
end end
describe 'import pull requests with caching' do
let(:pull_request_already_imported) do
instance_double(
BitbucketServer::Representation::PullRequest,
iid: 11)
end
let(:pull_request_to_be_imported) do
instance_double(
BitbucketServer::Representation::PullRequest,
iid: 12,
source_branch_sha: sample.commits.last,
source_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.source_branch,
target_branch_sha: sample.commits.first,
target_branch_name: Gitlab::Git::BRANCH_REF_PREFIX + sample.target_branch,
title: 'This is a title',
description: 'This is a test pull request',
state: 'merged',
author: 'Test Author',
author_email: pull_request_author.email,
author_username: pull_request_author.username,
created_at: Time.now,
updated_at: Time.now,
raw: {},
merged?: true)
end
before do
Gitlab::Cache::Import::Caching.set_add(subject.already_imported_cache_key, pull_request_already_imported.iid)
allow(subject.client).to receive(:pull_requests).and_return([pull_request_to_be_imported, pull_request_already_imported])
end
it 'only imports one Merge Request, as the other on is in the cache' do
expect(subject.client).to receive(:activities).and_return([merge_event])
expect { subject.execute }.to change { MergeRequest.count }.by(1)
expect(Gitlab::Cache::Import::Caching.set_includes?(subject.already_imported_cache_key, pull_request_already_imported.iid)).to eq(true)
expect(Gitlab::Cache::Import::Caching.set_includes?(subject.already_imported_cache_key, pull_request_to_be_imported.iid)).to eq(true)
end
end
end end
describe 'inaccessible branches' do describe 'inaccessible branches' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment