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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-29 18:10:08 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-29 18:10:08 +0300
commit20fda899a62cc27a4d40a168640e7e926c69eb62 (patch)
tree8fa2bca2431010c15b681fdec8c0cfba2ad78885 /app
parent933a571ac8c9ada219dd15079221ff3dba8043be (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue50
-rw-r--r--app/assets/javascripts/blob/suggest_web_ide_ci/index.js20
-rw-r--r--app/assets/javascripts/blob/viewer/index.js4
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js6
-rw-r--r--app/assets/javascripts/code_navigation/index.js10
-rw-r--r--app/assets/javascripts/code_navigation/store/index.js11
-rw-r--r--app/assets/javascripts/design_management/components/toolbar/design_navigation.vue4
-rw-r--r--app/assets/javascripts/design_management/components/toolbar/index.vue8
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue8
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue8
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue2
-rw-r--r--app/assets/javascripts/diffs/diff_file.js12
-rw-r--r--app/assets/javascripts/diffs/store/actions.js9
-rw-r--r--app/assets/javascripts/diffs/store/getters.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js4
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js43
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js5
-rw-r--r--app/assets/javascripts/filtered_search/constants.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js29
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue2
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js2
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss3
-rw-r--r--app/controllers/projects/blob_controller.rb1
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/finders/merge_requests/by_approvals_finder.rb93
-rw-r--r--app/finders/merge_requests_finder.rb16
-rw-r--r--app/helpers/gitpod_helper.rb10
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/helpers/user_callouts_helper.rb5
-rw-r--r--app/models/concerns/approvable_base.rb24
-rw-r--r--app/models/concerns/integration.rb4
-rw-r--r--app/models/data_list.rb10
-rw-r--r--app/models/group.rb10
-rw-r--r--app/models/notification_recipient.rb2
-rw-r--r--app/models/project.rb1
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/service_list.rb12
-rw-r--r--app/models/user.rb2
-rw-r--r--app/models/user_callout.rb2
-rw-r--r--app/models/user_interacted_project.rb2
-rw-r--r--app/presenters/project_presenter.rb6
-rw-r--r--app/serializers/diffs_entity.rb4
-rw-r--r--app/serializers/paginated_diff_entity.rb2
-rw-r--r--app/services/admin/propagate_integration_service.rb52
-rw-r--r--app/services/admin/propagate_service_template.rb6
-rw-r--r--app/services/bulk_create_integration_service.rb59
-rw-r--r--app/services/bulk_update_integration_service.rb32
-rw-r--r--app/services/ci/create_job_artifacts_service.rb9
-rw-r--r--app/services/ci/retry_build_service.rb2
-rw-r--r--app/services/ci/update_build_queue_service.rb4
-rw-r--r--app/services/concerns/admin/propagate_service.rb51
-rw-r--r--app/views/admin/application_settings/_gitpod.html.haml3
-rw-r--r--app/views/profiles/preferences/_gitpod.html.haml4
-rw-r--r--app/views/projects/blob/edit.html.haml3
-rw-r--r--app/views/projects/merge_requests/show.html.haml1
-rw-r--r--app/views/shared/issuable/_approved_by_dropdown.html.haml16
-rw-r--r--app/workers/all_queues.yml24
-rw-r--r--app/workers/propagate_integration_group_worker.rb19
-rw-r--r--app/workers/propagate_integration_inherit_worker.rb19
-rw-r--r--app/workers/propagate_integration_project_worker.rb19
60 files changed, 498 insertions, 281 deletions
diff --git a/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue b/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue
deleted file mode 100644
index 1308ca53e74..00000000000
--- a/app/assets/javascripts/blob/suggest_web_ide_ci/components/web_ide_alert.vue
+++ /dev/null
@@ -1,50 +0,0 @@
-<script>
-import { GlAlert, GlButton } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
-
-export default {
- components: {
- GlAlert,
- GlButton,
- },
- props: {
- dismissEndpoint: {
- type: String,
- required: true,
- },
- featureId: {
- type: String,
- required: true,
- },
- editPath: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- showAlert: true,
- };
- },
- methods: {
- dismissAlert() {
- this.showAlert = false;
-
- return axios.post(this.dismissEndpoint, {
- feature_name: this.featureId,
- });
- },
- },
-};
-</script>
-
-<template>
- <gl-alert v-if="showAlert" class="gl-mt-5" @dismiss="dismissAlert">
- {{ __('The Web IDE offers advanced syntax highlighting capabilities and more.') }}
- <div class="gl-mt-5">
- <gl-button :href="editPath" category="primary" variant="info">{{
- __('Open Web IDE')
- }}</gl-button>
- </div>
- </gl-alert>
-</template>
diff --git a/app/assets/javascripts/blob/suggest_web_ide_ci/index.js b/app/assets/javascripts/blob/suggest_web_ide_ci/index.js
deleted file mode 100644
index eadf3cd6216..00000000000
--- a/app/assets/javascripts/blob/suggest_web_ide_ci/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import Vue from 'vue';
-import WebIdeAlert from './components/web_ide_alert.vue';
-
-export default el => {
- const { dismissEndpoint, featureId, editPath } = el.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- render(createElement) {
- return createElement(WebIdeAlert, {
- props: {
- dismissEndpoint,
- featureId,
- editPath,
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 05ee8e49eb1..0fb803cdfec 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -179,9 +179,7 @@ export default class BlobViewer {
viewer.innerHTML = data.html;
viewer.setAttribute('data-loaded', 'true');
- if (window.gon?.features?.codeNavigation) {
- eventHub.$emit('showBlobInteractionZones', viewer.dataset.path);
- }
+ eventHub.$emit('showBlobInteractionZones', viewer.dataset.path);
return viewer;
});
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index c9972f0b43c..d1e5dad7971 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -7,14 +7,12 @@ import BlobFileDropzone from '../blob/blob_file_dropzone';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
import { disableButtonIfEmptyField, setCookie } from '~/lib/utils/common_utils';
import Tracking from '~/tracking';
-import initWebIdeAlert from '~/blob/suggest_web_ide_ci';
export default () => {
const editBlobForm = $('.js-edit-blob-form');
const uploadBlobForm = $('.js-upload-blob-form');
const deleteBlobForm = $('.js-delete-blob-form');
const suggestEl = document.querySelector('.js-suggest-gitlab-ci-yml');
- const alertEl = document.getElementById('js-suggest-web-ide-ci');
if (editBlobForm.length) {
const urlRoot = editBlobForm.data('relativeUrlRoot');
@@ -85,8 +83,4 @@ export default () => {
});
}
}
-
- if (alertEl) {
- initWebIdeAlert(alertEl);
- }
};
diff --git a/app/assets/javascripts/code_navigation/index.js b/app/assets/javascripts/code_navigation/index.js
index 362c26ae065..fa5835245bc 100644
--- a/app/assets/javascripts/code_navigation/index.js
+++ b/app/assets/javascripts/code_navigation/index.js
@@ -1,13 +1,17 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import store from './store';
+import createStore from './store';
import App from './components/app.vue';
-Vue.use(Vuex);
-
export default initialData => {
const el = document.getElementById('js-code-navigation');
+ if (!el) return null;
+
+ Vue.use(Vuex);
+
+ const store = createStore();
+
store.dispatch('setInitialData', initialData);
return new Vue({
diff --git a/app/assets/javascripts/code_navigation/store/index.js b/app/assets/javascripts/code_navigation/store/index.js
index fe48f3ac7f5..9b60fc337fe 100644
--- a/app/assets/javascripts/code_navigation/store/index.js
+++ b/app/assets/javascripts/code_navigation/store/index.js
@@ -3,8 +3,9 @@ import createState from './state';
import actions from './actions';
import mutations from './mutations';
-export default new Vuex.Store({
- actions,
- mutations,
- state: createState(),
-});
+export default () =>
+ new Vuex.Store({
+ actions,
+ mutations,
+ state: createState(),
+ });
diff --git a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue
index afca8ed2c6f..2719d701c12 100644
--- a/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue
+++ b/app/assets/javascripts/design_management/components/toolbar/design_navigation.vue
@@ -64,9 +64,9 @@ export default {
</script>
<template>
- <div v-if="designsCount" class="d-flex align-items-center">
+ <div v-if="designsCount" class="gl-display-flex gl-align-items-center">
{{ paginationText }}
- <gl-button-group class="ml-3 mr-3">
+ <gl-button-group class="gl-mx-5">
<gl-button
:disabled="!previousDesign"
:title="s__('DesignManagement|Go to previous design')"
diff --git a/app/assets/javascripts/design_management/components/toolbar/index.vue b/app/assets/javascripts/design_management/components/toolbar/index.vue
index a1cb57123ab..8d25d467d59 100644
--- a/app/assets/javascripts/design_management/components/toolbar/index.vue
+++ b/app/assets/javascripts/design_management/components/toolbar/index.vue
@@ -106,12 +106,12 @@ export default {
>
<gl-icon name="close" />
</router-link>
- <div class="overflow-hidden d-flex align-items-center">
- <h2 class="m-0 str-truncated-100 gl-font-base">{{ filename }}</h2>
- <small v-if="updatedAt" class="text-secondary">{{ updatedText }}</small>
+ <div class="gl-overflow-hidden gl-display-flex gl-align-items-center">
+ <h2 class="gl-m-0 str-truncated-100 gl-font-base">{{ filename }}</h2>
+ <small v-if="updatedAt" class="gl-text-gray-500">{{ updatedText }}</small>
</div>
</div>
- <design-navigation :id="id" class="ml-auto flex-shrink-0" />
+ <design-navigation :id="id" class="gl-ml-auto gl-flex-shrink-0" />
<gl-button :href="image" icon="download" />
<delete-button
v-if="isLatestVersion && canDeleteDesign"
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index 9ecb9a44443..e68260b3e62 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -85,11 +85,9 @@ export default {
},
},
updated() {
- if (window.gon?.features?.codeNavigation) {
- this.$nextTick(() => {
- eventHub.$emit('showBlobInteractionZones', this.diffFile.new_path);
- });
- }
+ this.$nextTick(() => {
+ eventHub.$emit('showBlobInteractionZones', this.diffFile.new_path);
+ });
},
methods: {
...mapActions('diffs', ['saveDiffDiscussion', 'closeDiffFileCommentForm']),
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 02396a4ba1b..529723a349d 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -44,7 +44,7 @@ export default {
return {
isLoadingCollapsedDiff: false,
forkMessageVisible: false,
- isCollapsed: this.file.viewer.collapsed || false,
+ isCollapsed: this.file.viewer.automaticallyCollapsed || false,
};
},
computed: {
@@ -96,16 +96,16 @@ export default {
},
'file.file_hash': {
handler: function watchFileHash() {
- if (this.viewDiffsFileByFile && this.file.viewer.collapsed) {
+ if (this.viewDiffsFileByFile && this.file.viewer.automaticallyCollapsed) {
this.isCollapsed = false;
this.handleLoadCollapsedDiff();
} else {
- this.isCollapsed = this.file.viewer.collapsed || false;
+ this.isCollapsed = this.file.viewer.automaticallyCollapsed || false;
}
},
immediate: true,
},
- 'file.viewer.collapsed': function setIsCollapsed(newVal) {
+ 'file.viewer.automaticallyCollapsed': function setIsCollapsed(newVal) {
if (!this.viewDiffsFileByFile) {
this.isCollapsed = newVal;
}
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index cea98d6dcd3..6205d113e5e 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -330,7 +330,7 @@ export default {
</gl-dropdown-item>
</template>
- <template v-if="!diffFile.viewer.collapsed">
+ <template v-if="!diffFile.viewer.automaticallyCollapsed">
<gl-dropdown-divider
v-if="!diffFile.is_fully_expanded || diffHasDiscussions(diffFile)"
/>
diff --git a/app/assets/javascripts/diffs/diff_file.js b/app/assets/javascripts/diffs/diff_file.js
index 610b71235d9..933197a2c7f 100644
--- a/app/assets/javascripts/diffs/diff_file.js
+++ b/app/assets/javascripts/diffs/diff_file.js
@@ -18,9 +18,21 @@ function fileSymlinkInformation(file, fileList) {
);
}
+function collapsed(file) {
+ const viewer = file.viewer || {};
+
+ return {
+ automaticallyCollapsed: viewer.automaticallyCollapsed || viewer.collapsed || false,
+ };
+}
+
export function prepareRawDiffFile({ file, allFiles }) {
Object.assign(file, {
brokenSymlink: fileSymlinkInformation(file, allFiles),
+ viewer: {
+ ...file.viewer,
+ ...collapsed(file),
+ },
});
return file;
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 0f275f1cb3e..966b706fc31 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -103,7 +103,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
commit(types.VIEW_DIFF_FILE, state.diffFiles[0].file_hash);
}
- if (gon.features?.codeNavigation) {
+ if (state.diffFiles?.length) {
// eslint-disable-next-line promise/catch-or-return,promise/no-nesting
import('~/code_navigation').then(m =>
m.default({
@@ -236,7 +236,7 @@ export const renderFileForDiscussionId = ({ commit, rootState, state }, discussi
commit(types.RENDER_FILE, file);
}
- if (file.viewer.collapsed) {
+ if (file.viewer.automaticallyCollapsed) {
eventHub.$emit(`loadCollapsedDiff/${file.file_hash}`);
scrollToElement(document.getElementById(file.file_hash));
} else {
@@ -252,7 +252,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => {
const nextFile = state.diffFiles.find(
file =>
!file.renderIt &&
- (file.viewer && (!file.viewer.collapsed || file.viewer.name !== diffViewerModes.text)),
+ (file.viewer &&
+ (!file.viewer.automaticallyCollapsed || file.viewer.name !== diffViewerModes.text)),
);
if (nextFile) {
@@ -631,7 +632,7 @@ export function switchToFullDiffFromRenamedFile({ commit, dispatch, state }, { d
filePath: diffFile.file_path,
viewer: {
...diffFile.alternate_viewer,
- collapsed: false,
+ automaticallyCollapsed: false,
},
});
commit(types.SET_CURRENT_VIEW_DIFF_FILE_LINES, { filePath: diffFile.file_path, lines });
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index 0a33f978f4f..91425c7825b 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -9,7 +9,7 @@ export const isParallelView = state => state.diffViewType === PARALLEL_DIFF_VIEW
export const isInlineView = state => state.diffViewType === INLINE_DIFF_VIEW_TYPE;
export const hasCollapsedFile = state =>
- state.diffFiles.some(file => file.viewer && file.viewer.collapsed);
+ state.diffFiles.some(file => file.viewer && file.viewer.automaticallyCollapsed);
export const commitId = state => (state.commit && state.commit.id ? state.commit.id : null);
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 7925c620c4e..13ecf6a997d 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -172,7 +172,7 @@ export default {
state.diffFiles.forEach(file => {
Object.assign(file, {
viewer: Object.assign(file.viewer, {
- collapsed: false,
+ automaticallyCollapsed: false,
}),
});
});
@@ -355,7 +355,7 @@ export default {
const file = state.diffFiles.find(f => f.file_path === filePath);
if (file && file.viewer) {
- file.viewer.collapsed = collapsed;
+ file.viewer.automaticallyCollapsed = collapsed;
}
},
[types.SET_HIDDEN_VIEW_DIFF_FILE_LINES](state, { filePath, lines }) {
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index 80f78c154ee..1bfbab9ef96 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -63,4 +63,47 @@ export default IssuableTokenKeys => {
IssuableTokenKeys.tokenKeys.push(targetBranchToken);
IssuableTokenKeys.tokenKeysWithAlternative.push(targetBranchToken);
+
+ const approvedBy = {
+ token: {
+ formattedKey: __('Approved-By'),
+ key: 'approved-by',
+ type: 'array',
+ param: 'usernames[]',
+ symbol: '@',
+ icon: 'approval',
+ tag: '@approved-by',
+ },
+ condition: [
+ {
+ url: 'approved_by_usernames[]=None',
+ tokenKey: 'approved-by',
+ value: __('None'),
+ operator: '=',
+ },
+ {
+ url: 'not[approved_by_usernames][]=None',
+ tokenKey: 'approved-by',
+ value: __('None'),
+ operator: '!=',
+ },
+ {
+ url: 'approved_by_usernames[]=Any',
+ tokenKey: 'approved-by',
+ value: __('Any'),
+ operator: '=',
+ },
+ {
+ url: 'not[approved_by_usernames][]=Any',
+ tokenKey: 'approved-by',
+ value: __('Any'),
+ operator: '!=',
+ },
+ ],
+ };
+
+ const tokenPosition = 2;
+ IssuableTokenKeys.tokenKeys.splice(tokenPosition, 0, ...[approvedBy.token]);
+ IssuableTokenKeys.tokenKeysWithAlternative.splice(tokenPosition, 0, ...[approvedBy.token]);
+ IssuableTokenKeys.conditions.push(...approvedBy.condition);
};
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index 49bd3cda127..5b4af96c861 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -69,6 +69,11 @@ export default class AvailableDropdownMappings {
gl: DropdownUser,
element: this.container.querySelector('#js-dropdown-assignee'),
},
+ 'approved-by': {
+ reference: null,
+ gl: DropdownUser,
+ element: this.container.querySelector('#js-dropdown-approved-by'),
+ },
milestone: {
reference: null,
gl: DropdownNonUser,
diff --git a/app/assets/javascripts/filtered_search/constants.js b/app/assets/javascripts/filtered_search/constants.js
index 0b9fe969da1..6cd6f9c9906 100644
--- a/app/assets/javascripts/filtered_search/constants.js
+++ b/app/assets/javascripts/filtered_search/constants.js
@@ -1,4 +1,4 @@
-export const USER_TOKEN_TYPES = ['author', 'assignee'];
+export const USER_TOKEN_TYPES = ['author', 'assignee', 'approved-by'];
export const DROPDOWN_TYPE = {
hint: 'hint',
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index bc3a130432a..cc338237191 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -4,6 +4,7 @@ import { escape, template } from 'lodash';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import glRegexp from './lib/utils/regexp';
import AjaxCache from './lib/utils/ajax_cache';
+import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from './lib/utils/common_utils';
import * as Emoji from '~/emoji';
@@ -61,6 +62,7 @@ class GfmAutoComplete {
this.dataSources = dataSources;
this.cachedData = {};
this.isLoadingData = {};
+ this.previousQuery = '';
}
setup(input, enableMap = defaultAutocompleteConfig) {
@@ -526,7 +528,7 @@ class GfmAutoComplete {
}
getDefaultCallbacks() {
- const fetchData = this.fetchData.bind(this);
+ const self = this;
return {
sorter(query, items, searchKey) {
@@ -539,7 +541,15 @@ class GfmAutoComplete {
},
filter(query, data, searchKey) {
if (GfmAutoComplete.isLoading(data)) {
- fetchData(this.$inputor, this.at);
+ self.fetchData(this.$inputor, this.at);
+ return data;
+ }
+ if (
+ GfmAutoComplete.typesWithBackendFiltering.includes(GfmAutoComplete.atTypeMap[this.at]) &&
+ self.previousQuery !== query
+ ) {
+ self.fetchData(this.$inputor, this.at, query);
+ self.previousQuery = query;
return data;
}
return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
@@ -587,13 +597,22 @@ class GfmAutoComplete {
};
}
- fetchData($input, at) {
+ fetchData($input, at, search) {
if (this.isLoadingData[at]) return;
this.isLoadingData[at] = true;
const dataSource = this.dataSources[GfmAutoComplete.atTypeMap[at]];
- if (this.cachedData[at]) {
+ if (GfmAutoComplete.typesWithBackendFiltering.includes(GfmAutoComplete.atTypeMap[at])) {
+ axios
+ .get(dataSource, { params: { search } })
+ .then(({ data }) => {
+ this.loadData($input, at, data);
+ })
+ .catch(() => {
+ this.isLoadingData[at] = false;
+ });
+ } else if (this.cachedData[at]) {
this.loadData($input, at, this.cachedData[at]);
} else if (GfmAutoComplete.atTypeMap[at] === 'emojis') {
Emoji.initEmojiMap()
@@ -687,6 +706,8 @@ GfmAutoComplete.atTypeMap = {
$: 'snippets',
};
+GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities'];
+
// Emoji
GfmAutoComplete.glEmojiTag = null;
GfmAutoComplete.Emoji = {
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index c01cd8f8037..a4271852563 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -76,7 +76,7 @@ export default {
:discussion-path="discussion.discussion_path"
:diff-file="discussion.diff_file"
:can-current-user-fork="false"
- :expanded="!discussion.diff_file.viewer.collapsed"
+ :expanded="!discussion.diff_file.viewer.automaticallyCollapsed"
/>
<div v-if="isTextFile" class="diff-content">
<table class="code js-syntax-highlight" :class="$options.userColorSchemeClass">
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index 988bea5343a..812e7176b4c 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -58,7 +58,7 @@ document.addEventListener('DOMContentLoaded', () => {
const codeNavEl = document.getElementById('js-code-navigation');
- if (gon.features?.codeNavigation && codeNavEl) {
+ if (codeNavEl) {
const { codeNavigationPath, blobPath, definitionPathPrefix } = codeNavEl.dataset;
// eslint-disable-next-line promise/catch-or-return
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 8615bd4bb2a..63288587606 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -777,7 +777,6 @@ button.mini-pipeline-graph-dropdown-toggle {
.ci-action-icon-container {
position: absolute;
right: 8px;
- top: 8px;
&.ci-action-icon-wrapper {
height: $ci-action-dropdown-button-size;
@@ -799,8 +798,6 @@ button.mini-pipeline-graph-dropdown-toggle {
width: $ci-action-dropdown-svg-size;
height: $ci-action-dropdown-svg-size;
fill: $gl-text-color-secondary;
- position: relative;
- top: 1px;
vertical-align: initial;
}
}
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 1568d9966dd..5df4385ecea 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -33,7 +33,6 @@ class Projects::BlobController < Projects::ApplicationController
before_action :set_last_commit_sha, only: [:edit, :update]
before_action only: :show do
- push_frontend_feature_flag(:code_navigation, @project, default_enabled: true)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
push_frontend_feature_flag(:gitlab_ci_yml_preview, @project, default_enabled: false)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 92785540172..e2fa373a88c 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -29,7 +29,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action only: [:show] do
push_frontend_feature_flag(:deploy_from_footer, @project, default_enabled: true)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
- push_frontend_feature_flag(:code_navigation, @project, default_enabled: true)
push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
push_frontend_feature_flag(:merge_ref_head_comments, @project, default_enabled: true)
push_frontend_feature_flag(:mr_commit_neighbor_nav, @project, default_enabled: true)
diff --git a/app/finders/merge_requests/by_approvals_finder.rb b/app/finders/merge_requests/by_approvals_finder.rb
new file mode 100644
index 00000000000..e6ab1467f06
--- /dev/null
+++ b/app/finders/merge_requests/by_approvals_finder.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module MergeRequests
+ # Used to filter MergeRequest collections by approvers
+ class ByApprovalsFinder
+ attr_reader :usernames, :ids
+
+ # We apply a limitation to the amount of elements that can be part of the filter condition
+ MAX_FILTER_ELEMENTS = 5
+
+ # Initialize the finder
+ #
+ # @param [Array<String>] usernames
+ # @param [Array<Integers>] ids
+ def initialize(usernames, ids)
+ # rubocop:disable CodeReuse/ActiveRecord
+ @usernames = Array(usernames).map(&:to_s).uniq.take(MAX_FILTER_ELEMENTS)
+ @ids = Array(ids).uniq.take(MAX_FILTER_ELEMENTS)
+ # rubocop:enable CodeReuse/ActiveRecord
+ end
+
+ # Filter MergeRequest collections by approvers
+ #
+ # @param [ActiveRecord::Relation] items the activerecord relation
+ def execute(items)
+ if by_no_approvals?
+ without_approvals(items)
+ elsif by_any_approvals?
+ with_any_approvals(items)
+ elsif ids.present?
+ find_approved_by_ids(items)
+ elsif usernames.present?
+ find_approved_by_names(items)
+ else
+ items
+ end
+ end
+
+ private
+
+ # Is param using special condition: "None" ?
+ #
+ # @return [Boolean] whether special condition "None" is being used
+ def by_no_approvals?
+ includes_special_label?(IssuableFinder::Params::FILTER_NONE)
+ end
+
+ # Is param using special condition: "Any" ?
+ #
+ # @return [Boolean] whether special condition "Any" is being used
+ def by_any_approvals?
+ includes_special_label?(IssuableFinder::Params::FILTER_ANY)
+ end
+
+ # Check if we have the special label in ids or usernames field
+ #
+ # @param [String] label the special label
+ # @return [Boolean] whether ids or usernames includes the special label
+ def includes_special_label?(label)
+ ids.first.to_s.downcase == label || usernames.map(&:downcase).include?(label)
+ end
+
+ # Merge Requests without any approval
+ #
+ # @param [ActiveRecord::Relation] items
+ def without_approvals(items)
+ items.without_approvals
+ end
+
+ # Merge Requests with any number of approvals
+ #
+ # @param [ActiveRecord::Relation] items the activerecord relation
+ def with_any_approvals(items)
+ items.select_from_union([
+ items.with_approvals
+ ])
+ end
+
+ # Merge Requests approved by given usernames
+ #
+ # @param [ActiveRecord::Relation] items the activerecord relation
+ def find_approved_by_names(items)
+ items.approved_by_users_with_usernames(*usernames)
+ end
+
+ # Merge Requests approved by given user IDs
+ #
+ # @param [ActiveRecord::Relation] items the activerecord relation
+ def find_approved_by_ids(items)
+ items.approved_by_users_with_ids(*ids)
+ end
+ end
+end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index 37da29b32ff..7bdc98d2e3d 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -33,7 +33,11 @@ class MergeRequestsFinder < IssuableFinder
include MergedAtFilter
def self.scalar_params
- @scalar_params ||= super + [:wip, :draft, :target_branch, :merged_after, :merged_before]
+ @scalar_params ||= super + [:wip, :draft, :target_branch, :merged_after, :merged_before, :approved_by_ids]
+ end
+
+ def self.array_params
+ @array_params ||= super.merge(approved_by_usernames: [])
end
def klass
@@ -47,6 +51,7 @@ class MergeRequestsFinder < IssuableFinder
items = by_draft(items)
items = by_target_branch(items)
items = by_merged_at(items)
+ items = by_approvals(items)
by_source_project_id(items)
end
@@ -131,6 +136,15 @@ class MergeRequestsFinder < IssuableFinder
def deployment_id
@deployment_id ||= params[:deployment_id].presence
end
+
+ # Filter by merge requests that had been approved by specific users
+ # rubocop: disable CodeReuse/Finder
+ def by_approvals(items)
+ MergeRequests::ByApprovalsFinder
+ .new(params[:approved_by_usernames], params[:approved_by_ids])
+ .execute(items)
+ end
+ # rubocop: enable CodeReuse/Finder
end
MergeRequestsFinder.prepend_if_ee('EE::MergeRequestsFinder')
diff --git a/app/helpers/gitpod_helper.rb b/app/helpers/gitpod_helper.rb
new file mode 100644
index 00000000000..7edf7dc218d
--- /dev/null
+++ b/app/helpers/gitpod_helper.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module GitpodHelper
+ def gitpod_enable_description
+ link_start = '<a href="https://gitpod.io/" target="_blank" rel="noopener noreferrer">'.html_safe
+ link_end = "#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}</a>".html_safe
+
+ s_('Enable %{link_start}Gitpod%{link_end} integration to launch a development environment in your browser directly from GitLab.').html_safe % { link_start: link_start, link_end: link_end }
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 72cc07b13a5..48bbc979947 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -758,10 +758,6 @@ module ProjectsHelper
!project.repository.gitlab_ci_yml
end
- def native_code_navigation_enabled?(project)
- Feature.enabled?(:code_navigation, project, default_enabled: true)
- end
-
def show_visibility_confirm_modal?(project)
project.unlink_forks_upon_visibility_decrease_enabled? && project.visibility_level > Gitlab::VisibilityLevel::PRIVATE && project.forks_count > 0
end
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 967271a8431..b0cfda67ad4 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -9,7 +9,6 @@ module UserCalloutsHelper
TABS_POSITION_HIGHLIGHT = 'tabs_position_highlight'
WEBHOOKS_MOVED = 'webhooks_moved'
CUSTOMIZE_HOMEPAGE = 'customize_homepage'
- WEB_IDE_ALERT_DISMISSED = 'web_ide_alert_dismissed'
def show_admin_integrations_moved?
!user_dismissed?(ADMIN_INTEGRATIONS_MOVED)
@@ -51,10 +50,6 @@ module UserCalloutsHelper
customize_homepage && !user_dismissed?(CUSTOMIZE_HOMEPAGE)
end
- def show_web_ide_alert?
- !user_dismissed?(WEB_IDE_ALERT_DISMISSED)
- end
-
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
diff --git a/app/models/concerns/approvable_base.rb b/app/models/concerns/approvable_base.rb
index d07c4ec43ac..c2d94b50f8d 100644
--- a/app/models/concerns/approvable_base.rb
+++ b/app/models/concerns/approvable_base.rb
@@ -2,10 +2,34 @@
module ApprovableBase
extend ActiveSupport::Concern
+ include FromUnion
included do
has_many :approvals, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
has_many :approved_by_users, through: :approvals, source: :user
+
+ scope :without_approvals, -> { left_outer_joins(:approvals).where(approvals: { id: nil }) }
+ scope :with_approvals, -> { joins(:approvals) }
+ scope :approved_by_users_with_ids, -> (*user_ids) do
+ with_approvals
+ .merge(Approval.with_user)
+ .where(users: { id: user_ids })
+ .group(:id)
+ .having("COUNT(users.id) = ?", user_ids.size)
+ end
+ scope :approved_by_users_with_usernames, -> (*usernames) do
+ with_approvals
+ .merge(Approval.with_user)
+ .where(users: { username: usernames })
+ .group(:id)
+ .having("COUNT(users.id) = ?", usernames.size)
+ end
+ end
+
+ class_methods do
+ def select_from_union(relations)
+ where(id: from_union(relations))
+ end
end
def approved_by?(user)
diff --git a/app/models/concerns/integration.rb b/app/models/concerns/integration.rb
index 34ff5bb1195..9d446841a9f 100644
--- a/app/models/concerns/integration.rb
+++ b/app/models/concerns/integration.rb
@@ -16,7 +16,7 @@ module Integration
Project.where(id: custom_integration_project_ids)
end
- def ids_without_integration(integration, limit)
+ def without_integration(integration)
services = Service
.select('1')
.where('services.project_id = projects.id')
@@ -26,8 +26,6 @@ module Integration
.where('NOT EXISTS (?)', services)
.where(pending_delete: false)
.where(archived: false)
- .limit(limit)
- .pluck(:id)
end
end
end
diff --git a/app/models/data_list.rb b/app/models/data_list.rb
index 2cee3447886..adad8e3013e 100644
--- a/app/models/data_list.rb
+++ b/app/models/data_list.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
class DataList
- def initialize(batch_ids, data_fields_hash, klass)
- @batch_ids = batch_ids
+ def initialize(batch, data_fields_hash, klass)
+ @batch = batch
@data_fields_hash = data_fields_hash
@klass = klass
end
@@ -13,15 +13,15 @@ class DataList
private
- attr_reader :batch_ids, :data_fields_hash, :klass
+ attr_reader :batch, :data_fields_hash, :klass
def columns
data_fields_hash.keys << 'service_id'
end
def values
- batch_ids.map do |row|
- data_fields_hash.values << row['id']
+ batch.map do |record|
+ data_fields_hash.values << record['id']
end
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index c0f145997cc..abb3f6c96c4 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -15,6 +15,7 @@ class Group < Namespace
include WithUploads
include Gitlab::Utils::StrongMemoize
include GroupAPICompatibility
+ include EachBatch
ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT = 10
@@ -140,6 +141,15 @@ class Group < Namespace
end
end
+ def without_integration(integration)
+ services = Service
+ .select('1')
+ .where('services.group_id = namespaces.id')
+ .where(type: integration.type)
+
+ where('NOT EXISTS (?)', services)
+ end
+
private
def public_to_user_arel(user)
diff --git a/app/models/notification_recipient.rb b/app/models/notification_recipient.rb
index 6a6b2bb1b58..79a84231083 100644
--- a/app/models/notification_recipient.rb
+++ b/app/models/notification_recipient.rb
@@ -5,7 +5,7 @@ class NotificationRecipient
attr_reader :user, :type, :reason
- def initialize(user, type, **opts)
+ def initialize(user, type, opts = {})
unless NotificationSetting.levels.key?(type) || type == :subscription
raise ArgumentError, "invalid type: #{type.inspect}"
end
diff --git a/app/models/project.rb b/app/models/project.rb
index dacad903439..8934c09f7b1 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -33,6 +33,7 @@ class Project < ApplicationRecord
include FromUnion
include IgnorableColumns
include Integration
+ include EachBatch
extend Gitlab::Cache::RequestCache
extend Gitlab::ConfigHelper
diff --git a/app/models/service.rb b/app/models/service.rb
index 087985b192a..48fa62d3d46 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -63,6 +63,7 @@ class Service < ApplicationRecord
scope :active, -> { where(active: true) }
scope :by_type, -> (type) { where(type: type) }
scope :by_active_flag, -> (flag) { where(active: flag) }
+ scope :inherit_from_id, -> (id) { where(inherit_from_id: id) }
scope :for_group, -> (group) { where(group_id: group, type: available_services_types) }
scope :for_template, -> { where(template: true, type: available_services_types) }
scope :for_instance, -> { where(instance: true, type: available_services_types) }
diff --git a/app/models/service_list.rb b/app/models/service_list.rb
index 9cbc5e68059..5eca5f2bda1 100644
--- a/app/models/service_list.rb
+++ b/app/models/service_list.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
class ServiceList
- def initialize(batch_ids, service_hash, association)
- @batch_ids = batch_ids
+ def initialize(batch, service_hash, association)
+ @batch = batch
@service_hash = service_hash
@association = association
end
@@ -13,15 +13,15 @@ class ServiceList
private
- attr_reader :batch_ids, :service_hash, :association
+ attr_reader :batch, :service_hash, :association
def columns
- (service_hash.keys << "#{association}_id")
+ service_hash.keys << "#{association}_id"
end
def values
- batch_ids.map do |id|
- (service_hash.values << id)
+ batch.select(:id).map do |record|
+ service_hash.values << record.id
end
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 30e468ee5f1..9305e2518c1 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -69,7 +69,7 @@ class User < ApplicationRecord
LOGIN_FORBIDDEN = "Your account does not have the required permission to login. Please contact your GitLab " \
"administrator if you think this is an error."
- MINIMUM_INACTIVE_DAYS = 180
+ MINIMUM_INACTIVE_DAYS = 90
# Override Devise::Models::Trackable#update_tracked_fields!
# to limit database writes to at most once every hour
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 0ba319aa444..e39ff8712fc 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -19,7 +19,7 @@ class UserCallout < ApplicationRecord
webhooks_moved: 13,
service_templates_deprecated: 14,
admin_integrations_moved: 15,
- web_ide_alert_dismissed: 16,
+ web_ide_alert_dismissed: 16, # no longer in use
active_user_count_threshold: 18, # EE-only
buy_pipeline_minutes_notification_dot: 19, # EE-only
personal_access_token_expiry: 21, # EE-only
diff --git a/app/models/user_interacted_project.rb b/app/models/user_interacted_project.rb
index 1c615777018..7e7a387d3d4 100644
--- a/app/models/user_interacted_project.rb
+++ b/app/models/user_interacted_project.rb
@@ -21,7 +21,7 @@ class UserInteractedProject < ApplicationRecord
user_id: event.author_id
}
- cached_exists?(attributes) do
+ cached_exists?(**attributes) do
transaction(requires_new: true) do
where(attributes).select(1).first || create!(attributes)
true # not caching the whole record here for now
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index ef75c160b2d..0be8a4d5472 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -118,10 +118,6 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
add_special_file_path(file_name: ci_config_path_or_default)
end
- def add_ci_yml_ide_path
- ide_edit_path(project, default_branch_or_master, ci_config_path_or_default)
- end
-
def add_readme_path
add_special_file_path(file_name: 'README.md')
end
@@ -330,7 +326,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if cicd_missing?
AnchorData.new(false,
statistic_icon + _('Set up CI/CD'),
- add_ci_yml_ide_path)
+ add_ci_yml_path)
elsif repository.gitlab_ci_yml.present?
AnchorData.new(false,
statistic_icon('doc-text') + _('CI/CD configuration'),
diff --git a/app/serializers/diffs_entity.rb b/app/serializers/diffs_entity.rb
index 6ef524b5bec..0b4f21c55f4 100644
--- a/app/serializers/diffs_entity.rb
+++ b/app/serializers/diffs_entity.rb
@@ -78,7 +78,7 @@ class DiffsEntity < Grape::Entity
options[:merge_request_diffs]
end
- expose :definition_path_prefix, if: -> (diff_file) { Feature.enabled?(:code_navigation, merge_request.project, default_enabled: true) } do |diffs|
+ expose :definition_path_prefix do |diffs|
project_blob_path(merge_request.project, diffs.diff_refs&.head_sha)
end
@@ -89,8 +89,6 @@ class DiffsEntity < Grape::Entity
private
def code_navigation_path(diffs)
- return unless Feature.enabled?(:code_navigation, merge_request.project, default_enabled: true)
-
Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha)
end
diff --git a/app/serializers/paginated_diff_entity.rb b/app/serializers/paginated_diff_entity.rb
index 37c48338e55..f24571f7d7d 100644
--- a/app/serializers/paginated_diff_entity.rb
+++ b/app/serializers/paginated_diff_entity.rb
@@ -37,8 +37,6 @@ class PaginatedDiffEntity < Grape::Entity
private
def code_navigation_path(diffs)
- return unless Feature.enabled?(:code_navigation, merge_request.project, default_enabled: true)
-
Gitlab::CodeNavigationPath.new(merge_request.project, diffs.diff_refs&.head_sha)
end
diff --git a/app/services/admin/propagate_integration_service.rb b/app/services/admin/propagate_integration_service.rb
index 34d6008cb6a..80e27c21d5b 100644
--- a/app/services/admin/propagate_integration_service.rb
+++ b/app/services/admin/propagate_integration_service.rb
@@ -14,59 +14,19 @@ module Admin
private
# rubocop: disable Cop/InBatches
- # rubocop: disable CodeReuse/ActiveRecord
def update_inherited_integrations
- Service.where(type: integration.type, inherit_from_id: integration.id).in_batches(of: BATCH_SIZE) do |batch|
- bulk_update_from_integration(batch)
+ Service.by_type(integration.type).inherit_from_id(integration.id).in_batches(of: BATCH_SIZE) do |services|
+ min_id, max_id = services.pick("MIN(services.id), MAX(services.id)")
+ PropagateIntegrationInheritWorker.perform_async(integration.id, min_id, max_id)
end
end
# rubocop: enable Cop/InBatches
- # rubocop: enable CodeReuse/ActiveRecord
-
- # rubocop: disable CodeReuse/ActiveRecord
- def bulk_update_from_integration(batch)
- # Retrieving the IDs instantiates the ActiveRecord relation (batch)
- # into concrete models, otherwise update_all will clear the relation.
- # https://stackoverflow.com/q/34811646/462015
- batch_ids = batch.pluck(:id)
-
- Service.transaction do
- batch.update_all(service_hash)
-
- if data_fields_present?
- integration.data_fields.class.where(service_id: batch_ids).update_all(data_fields_hash)
- end
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
def create_integration_for_groups_without_integration
- loop do
- batch = Group.uncached { group_ids_without_integration(integration, BATCH_SIZE) }
-
- bulk_create_from_integration(batch, 'group') unless batch.empty?
-
- break if batch.size < BATCH_SIZE
+ Group.without_integration(integration).each_batch(of: BATCH_SIZE) do |groups|
+ min_id, max_id = groups.pick("MIN(namespaces.id), MAX(namespaces.id)")
+ PropagateIntegrationGroupWorker.perform_async(integration.id, min_id, max_id)
end
end
-
- def service_hash
- @service_hash ||= integration.to_service_hash
- .tap { |json| json['inherit_from_id'] = integration.id }
- end
-
- # rubocop:disable CodeReuse/ActiveRecord
- def group_ids_without_integration(integration, limit)
- services = Service
- .select('1')
- .where('services.group_id = namespaces.id')
- .where(type: integration.type)
-
- Group
- .where('NOT EXISTS (?)', services)
- .limit(limit)
- .pluck(:id)
- end
- # rubocop:enable CodeReuse/ActiveRecord
end
end
diff --git a/app/services/admin/propagate_service_template.rb b/app/services/admin/propagate_service_template.rb
index cd0d2d5d03f..07be3c1027d 100644
--- a/app/services/admin/propagate_service_template.rb
+++ b/app/services/admin/propagate_service_template.rb
@@ -9,11 +9,5 @@ module Admin
create_integration_for_projects_without_integration
end
-
- private
-
- def service_hash
- @service_hash ||= integration.to_service_hash
- end
end
end
diff --git a/app/services/bulk_create_integration_service.rb b/app/services/bulk_create_integration_service.rb
new file mode 100644
index 00000000000..23b89b0d8a9
--- /dev/null
+++ b/app/services/bulk_create_integration_service.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+class BulkCreateIntegrationService
+ def initialize(integration, batch, association)
+ @integration = integration
+ @batch = batch
+ @association = association
+ end
+
+ def execute
+ service_list = ServiceList.new(batch, service_hash, association).to_array
+
+ Service.transaction do
+ results = bulk_insert(*service_list)
+
+ if integration.data_fields_present?
+ data_list = DataList.new(results, data_fields_hash, integration.data_fields.class).to_array
+
+ bulk_insert(*data_list)
+ end
+
+ run_callbacks(batch) if association == 'project'
+ end
+ end
+
+ private
+
+ attr_reader :integration, :batch, :association
+
+ def bulk_insert(klass, columns, values_array)
+ items_to_insert = values_array.map { |array| Hash[columns.zip(array)] }
+
+ klass.insert_all(items_to_insert, returning: [:id])
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def run_callbacks(batch)
+ if integration.issue_tracker?
+ Project.where(id: batch.select(:id)).update_all(has_external_issue_tracker: true)
+ end
+
+ if integration.type == 'ExternalWikiService'
+ Project.where(id: batch.select(:id)).update_all(has_external_wiki: true)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def service_hash
+ if integration.template?
+ integration.to_service_hash
+ else
+ integration.to_service_hash.tap { |json| json['inherit_from_id'] = integration.id }
+ end
+ end
+
+ def data_fields_hash
+ integration.to_data_fields_hash
+ end
+end
diff --git a/app/services/bulk_update_integration_service.rb b/app/services/bulk_update_integration_service.rb
new file mode 100644
index 00000000000..74d77618f2c
--- /dev/null
+++ b/app/services/bulk_update_integration_service.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class BulkUpdateIntegrationService
+ def initialize(integration, batch)
+ @integration = integration
+ @batch = batch
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def execute
+ Service.transaction do
+ batch.update_all(service_hash)
+
+ if integration.data_fields_present?
+ integration.data_fields.class.where(service_id: batch.select(:id)).update_all(data_fields_hash)
+ end
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ attr_reader :integration, :batch
+
+ def service_hash
+ integration.to_service_hash.tap { |json| json['inherit_from_id'] = integration.id }
+ end
+
+ def data_fields_hash
+ integration.to_data_fields_hash
+ end
+end
diff --git a/app/services/ci/create_job_artifacts_service.rb b/app/services/ci/create_job_artifacts_service.rb
index 1fe65898d55..5efb3805bf7 100644
--- a/app/services/ci/create_job_artifacts_service.rb
+++ b/app/services/ci/create_job_artifacts_service.rb
@@ -52,24 +52,15 @@ module Ci
attr_reader :job, :project
def validate_requirements(artifact_type:, filesize:)
- return forbidden_type_error(artifact_type) if forbidden_type?(artifact_type)
return too_large_error if too_large?(artifact_type, filesize)
success
end
- def forbidden_type?(type)
- lsif?(type) && !code_navigation_enabled?
- end
-
def too_large?(type, size)
size > max_size(type) if size
end
- def code_navigation_enabled?
- Feature.enabled?(:code_navigation, project, default_enabled: true)
- end
-
def lsif?(type)
type == LSIF_ARTIFACT_TYPE
end
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 6b2e6c245f3..f397ada0696 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -58,7 +58,7 @@ module Ci
build = project.builds.new(attributes)
build.assign_attributes(::Gitlab::Ci::Pipeline::Seed::Build.environment_attributes_for(build))
build.retried = false
- BulkInsertableAssociations.with_bulk_insert(enabled: ::Gitlab::Ci::Features.bulk_insert_on_create?(project)) do
+ BulkInsertableAssociations.with_bulk_insert do
build.save!
end
build
diff --git a/app/services/ci/update_build_queue_service.rb b/app/services/ci/update_build_queue_service.rb
index 31c7178c9e7..241eba733ea 100644
--- a/app/services/ci/update_build_queue_service.rb
+++ b/app/services/ci/update_build_queue_service.rb
@@ -9,9 +9,7 @@ module Ci
private
def tick_for(build, runners)
- if Feature.enabled?(:ci_update_queues_for_online_runners, build.project, default_enabled: true)
- runners = runners.with_recent_runner_queue
- end
+ runners = runners.with_recent_runner_queue
runners.each do |runner|
runner.pick_build!(build)
diff --git a/app/services/concerns/admin/propagate_service.rb b/app/services/concerns/admin/propagate_service.rb
index 974408f678c..065ab6f7ff9 100644
--- a/app/services/concerns/admin/propagate_service.rb
+++ b/app/services/concerns/admin/propagate_service.rb
@@ -4,9 +4,7 @@ module Admin
module PropagateService
extend ActiveSupport::Concern
- BATCH_SIZE = 100
-
- delegate :data_fields_present?, to: :integration
+ BATCH_SIZE = 10_000
class_methods do
def propagate(integration)
@@ -23,51 +21,10 @@ module Admin
attr_reader :integration
def create_integration_for_projects_without_integration
- loop do
- batch_ids = Project.uncached { Project.ids_without_integration(integration, BATCH_SIZE) }
-
- bulk_create_from_integration(batch_ids, 'project') unless batch_ids.empty?
-
- break if batch_ids.size < BATCH_SIZE
- end
- end
-
- def bulk_create_from_integration(batch_ids, association)
- service_list = ServiceList.new(batch_ids, service_hash, association).to_array
-
- Service.transaction do
- results = bulk_insert(*service_list)
-
- if data_fields_present?
- data_list = DataList.new(results, data_fields_hash, integration.data_fields.class).to_array
-
- bulk_insert(*data_list)
- end
-
- run_callbacks(batch_ids) if association == 'project'
+ Project.without_integration(integration).each_batch(of: BATCH_SIZE) do |projects|
+ min_id, max_id = projects.pick("MIN(projects.id), MAX(projects.id)")
+ PropagateIntegrationProjectWorker.perform_async(integration.id, min_id, max_id)
end
end
-
- def bulk_insert(klass, columns, values_array)
- items_to_insert = values_array.map { |array| Hash[columns.zip(array)] }
-
- klass.insert_all(items_to_insert, returning: [:id])
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def run_callbacks(batch_ids)
- if integration.issue_tracker?
- Project.where(id: batch_ids).update_all(has_external_issue_tracker: true)
- end
-
- if integration.type == 'ExternalWikiService'
- Project.where(id: batch_ids).update_all(has_external_wiki: true)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def data_fields_hash
- @data_fields_hash ||= integration.to_data_fields_hash
- end
end
end
diff --git a/app/views/admin/application_settings/_gitpod.html.haml b/app/views/admin/application_settings/_gitpod.html.haml
index bbad5155ada..cf40eb7b108 100644
--- a/app/views/admin/application_settings/_gitpod.html.haml
+++ b/app/views/admin/application_settings/_gitpod.html.haml
@@ -1,6 +1,5 @@
- return unless Gitlab::Gitpod.feature_available?
- expanded = integration_expanded?('gitpod_')
-- gitpod_link = link_to("Gitpod#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}".html_safe, 'https://gitpod.io/', target: '_blank', rel: 'noopener noreferrer')
%section.settings.no-animate#js-gitpod-settings{ class: ('expanded' if expanded) }
.settings-header
@@ -9,7 +8,7 @@
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
- = s_('Enable %{gitpod_link} integration to launch a development environment in your browser directly from GitLab.').html_safe % { gitpod_link: gitpod_link }
+ = gitpod_enable_description
= link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information')
diff --git a/app/views/profiles/preferences/_gitpod.html.haml b/app/views/profiles/preferences/_gitpod.html.haml
index 69c9443ebbb..589c3a27c18 100644
--- a/app/views/profiles/preferences/_gitpod.html.haml
+++ b/app/views/profiles/preferences/_gitpod.html.haml
@@ -1,5 +1,3 @@
-- gitpod_link = link_to("Gitpod#{sprite_icon('external-link', size: 12, css_class: 'ml-1 vertical-align-center')}".html_safe, 'https://gitpod.io/', target: '_blank', rel: 'noopener noreferrer')
-
%label.label-bold#gitpod
= s_('Gitpod')
= link_to sprite_icon('question-o'), help_page_path('integration/gitpod.md'), target: '_blank', class: 'has-tooltip', title: _('More information')
@@ -8,4 +6,4 @@
= f.label :gitpod_enabled, class: 'form-check-label' do
= s_('Gitpod|Enable Gitpod integration').html_safe
.form-text.text-muted
- = s_('Enable %{gitpod_link} integration to launch a development environment in your browser directly from GitLab.').html_safe % { gitpod_link: gitpod_link }
+ = gitpod_enable_description
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 9bb4342ffb4..54c47e7af38 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -9,9 +9,6 @@
= link_to "the file", project_blob_path(@project, tree_join(@branch_name, @file_path)), target: "_blank", rel: 'noopener noreferrer', class: 'gl-link'
and make sure your changes will not unintentionally remove theirs.
-- if editing_ci_config? && show_web_ide_alert?
- #js-suggest-web-ide-ci{ data: { dismiss_endpoint: user_callouts_path, feature_id: UserCalloutsHelper::WEB_IDE_ALERT_DISMISSED, edit_path: ide_edit_path } }
-
.editor-title-row
%h3.page-title.blob-edit-page-title
Edit file
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 9a3078991c0..fcb79782acf 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -43,7 +43,6 @@
.tab-content#diff-notes-app
#js-diff-file-finder
- - if native_code_navigation_enabled?(@project)
#js-code-navigation
= render "projects/merge_requests/tabs/pane", id: "notes", class: "notes voting_notes" do
.row
diff --git a/app/views/shared/issuable/_approved_by_dropdown.html.haml b/app/views/shared/issuable/_approved_by_dropdown.html.haml
new file mode 100644
index 00000000000..8014545ab85
--- /dev/null
+++ b/app/views/shared/issuable/_approved_by_dropdown.html.haml
@@ -0,0 +1,16 @@
+#js-dropdown-approved-by.filtered-search-input-dropdown-menu.dropdown-menu
+ %ul{ data: { dropdown: true } }
+ %li.filter-dropdown-item{ data: { value: 'None' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('None')
+ %li.filter-dropdown-item{ data: { value: 'Any' } }
+ %button.btn.btn-link{ type: 'button' }
+ = _('Any')
+ %li.divider.droplab-item-ignore
+ - if current_user
+ = render 'shared/issuable/user_dropdown_item',
+ user: current_user
+ %ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
+ = render 'shared/issuable/user_dropdown_item',
+ user: User.new(username: '{{username}}', name: '{{name}}'),
+ avatar: { lazy: true, url: '{{avatar_url}}' }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 6a22844038e..d8cf05b9136 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -1724,6 +1724,30 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: propagate_integration_group
+ :feature_category: :integrations
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
+- :name: propagate_integration_inherit
+ :feature_category: :integrations
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
+- :name: propagate_integration_project
+ :feature_category: :integrations
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: propagate_service_template
:feature_category: :integrations
:has_external_dependencies:
diff --git a/app/workers/propagate_integration_group_worker.rb b/app/workers/propagate_integration_group_worker.rb
new file mode 100644
index 00000000000..e539c6d4719
--- /dev/null
+++ b/app/workers/propagate_integration_group_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class PropagateIntegrationGroupWorker
+ include ApplicationWorker
+
+ feature_category :integrations
+ idempotent!
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(integration_id, min_id, max_id)
+ integration = Service.find_by_id(integration_id)
+ return unless integration
+
+ batch = Group.where(id: min_id..max_id).without_integration(integration)
+
+ BulkCreateIntegrationService.new(integration, batch, 'group').execute
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+end
diff --git a/app/workers/propagate_integration_inherit_worker.rb b/app/workers/propagate_integration_inherit_worker.rb
new file mode 100644
index 00000000000..ef3132202f6
--- /dev/null
+++ b/app/workers/propagate_integration_inherit_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class PropagateIntegrationInheritWorker
+ include ApplicationWorker
+
+ feature_category :integrations
+ idempotent!
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(integration_id, min_id, max_id)
+ integration = Service.find_by_id(integration_id)
+ return unless integration
+
+ services = Service.where(id: min_id..max_id).by_type(integration.type).inherit_from_id(integration.id)
+
+ BulkUpdateIntegrationService.new(integration, services).execute
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+end
diff --git a/app/workers/propagate_integration_project_worker.rb b/app/workers/propagate_integration_project_worker.rb
new file mode 100644
index 00000000000..c1e286b24fc
--- /dev/null
+++ b/app/workers/propagate_integration_project_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class PropagateIntegrationProjectWorker
+ include ApplicationWorker
+
+ feature_category :integrations
+ idempotent!
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(integration_id, min_id, max_id)
+ integration = Service.find_by_id(integration_id)
+ return unless integration
+
+ batch = Project.where(id: min_id..max_id).without_integration(integration)
+
+ BulkCreateIntegrationService.new(integration, batch, 'project').execute
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+end